package outbound import ( "context" "os" "strings" "sync" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" ) var _ adapter.OutboundManager = (*Manager)(nil) type Manager struct { registry adapter.OutboundRegistry endpoint adapter.EndpointManager defaultTag string access sync.RWMutex outbounds []adapter.Outbound outboundByTag map[string]adapter.Outbound defaultOutbound adapter.Outbound defaultOutboundFallback func() (adapter.Outbound, error) } func NewManager(registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager { return &Manager{ registry: registry, endpoint: endpoint, defaultTag: defaultTag, outboundByTag: make(map[string]adapter.Outbound), } } func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, error)) { m.defaultOutboundFallback = defaultOutboundFallback } func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error { m.access.Lock() if stage == adapter.StartStateInitialize { if m.defaultTag != "" && m.defaultOutbound == nil { defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag) if !loaded { m.access.Unlock() return E.New("default outbound not found: ", m.defaultTag) } m.defaultOutbound = defaultEndpoint } if m.defaultOutbound == nil { directOutbound, err := m.defaultOutboundFallback() if err != nil { m.access.Unlock() return E.Cause(err, "create direct outbound for fallback") } m.outbounds = append(m.outbounds, directOutbound) m.outboundByTag[directOutbound.Tag()] = directOutbound m.defaultOutbound = directOutbound } } outbounds := m.outbounds m.access.Unlock() if stage == adapter.StartStateStart { return m.startOutbounds(scope, append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...)) } for _, outbound := range outbounds { lifecycle, isLifecycle := outbound.(adapter.Lifecycle) if !isLifecycle { continue } name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" err := scope.Start(name, lifecycle, stage) if err != nil { return err } } return nil } func (m *Manager) startOutbounds(scope *adapter.Scope, outbounds []adapter.Outbound) error { started := make(map[string]bool) for { canContinue := false startOne: for _, outboundToStart := range outbounds { outboundTag := outboundToStart.Tag() if started[outboundTag] { continue } dependencies := outboundToStart.Dependencies() for _, dependency := range dependencies { if !started[dependency] { continue startOne } } started[outboundTag] = true canContinue = true if endpoint, isEndpoint := outboundToStart.(adapter.Endpoint); isEndpoint { err := m.endpoint.StartEndpoint(endpoint) if err != nil { return err } continue } lifecycle, isLifecycle := outboundToStart.(adapter.Lifecycle) if !isLifecycle { continue } name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]" err := scope.Start(name, lifecycle, adapter.StartStateStart) if err != nil { return err } } if len(started) == len(outbounds) { break } if canContinue { continue } currentOutbound := common.Find(outbounds, func(it adapter.Outbound) bool { return !started[it.Tag()] }) var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error { problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool { return !started[it] }) if common.Contains(oTree, problemOutboundTag) { return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag) } problemOutbound := common.Find(outbounds, func(it adapter.Outbound) bool { return it.Tag() == problemOutboundTag }) if problemOutbound == nil { return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]") } return lintOutbound(append(oTree, problemOutboundTag), problemOutbound) } return lintOutbound([]string{currentOutbound.Tag()}, currentOutbound) } return nil } func (m *Manager) Outbounds() []adapter.Outbound { m.access.RLock() defer m.access.RUnlock() return m.outbounds } func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) { m.access.RLock() outbound, found := m.outboundByTag[tag] m.access.RUnlock() if found { return outbound, true } return m.endpoint.Get(tag) } func (m *Manager) Default() adapter.Outbound { m.access.RLock() defer m.access.RUnlock() return m.defaultOutbound } func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error { if tag == "" { return os.ErrInvalid } outbound, err := m.registry.CreateOutbound(ctx, router, logger, tag, inboundType, options) if err != nil { return err } m.access.Lock() defer m.access.Unlock() _, loaded := m.outboundByTag[tag] if loaded { return E.New("duplicate outbound tag: ", tag) } m.outbounds = append(m.outbounds, outbound) m.outboundByTag[tag] = outbound if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) { m.defaultOutbound = outbound } return nil }