2024-11-09 21:16:11 +08:00
|
|
|
package outbound
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
"github.com/sagernet/sing-box/adapter"
|
|
|
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
|
|
|
C "github.com/sagernet/sing-box/constant"
|
|
|
|
"github.com/sagernet/sing-box/log"
|
|
|
|
"github.com/sagernet/sing/common"
|
|
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
|
|
"github.com/sagernet/sing/common/logger"
|
|
|
|
)
|
|
|
|
|
|
|
|
var _ adapter.OutboundManager = (*Manager)(nil)
|
|
|
|
|
|
|
|
type Manager struct {
|
|
|
|
logger log.ContextLogger
|
|
|
|
registry adapter.OutboundRegistry
|
2024-11-21 18:10:41 +08:00
|
|
|
endpoint adapter.EndpointManager
|
2024-11-09 21:16:11 +08:00
|
|
|
defaultTag string
|
|
|
|
access sync.Mutex
|
|
|
|
started bool
|
|
|
|
stage adapter.StartStage
|
|
|
|
outbounds []adapter.Outbound
|
|
|
|
outboundByTag map[string]adapter.Outbound
|
|
|
|
dependByTag map[string][]string
|
|
|
|
defaultOutbound adapter.Outbound
|
|
|
|
defaultOutboundFallback adapter.Outbound
|
|
|
|
}
|
|
|
|
|
2024-11-21 18:10:41 +08:00
|
|
|
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
|
2024-11-09 21:16:11 +08:00
|
|
|
return &Manager{
|
|
|
|
logger: logger,
|
|
|
|
registry: registry,
|
2024-11-21 18:10:41 +08:00
|
|
|
endpoint: endpoint,
|
2024-11-09 21:16:11 +08:00
|
|
|
defaultTag: defaultTag,
|
|
|
|
outboundByTag: make(map[string]adapter.Outbound),
|
|
|
|
dependByTag: make(map[string][]string),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Manager) Initialize(defaultOutboundFallback adapter.Outbound) {
|
|
|
|
m.defaultOutboundFallback = defaultOutboundFallback
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Manager) Start(stage adapter.StartStage) error {
|
|
|
|
m.access.Lock()
|
|
|
|
if m.started && m.stage >= stage {
|
|
|
|
panic("already started")
|
|
|
|
}
|
|
|
|
m.started = true
|
|
|
|
m.stage = stage
|
2024-11-10 12:11:21 +08:00
|
|
|
outbounds := m.outbounds
|
|
|
|
m.access.Unlock()
|
2024-11-09 21:16:11 +08:00
|
|
|
if stage == adapter.StartStateStart {
|
2024-11-21 18:10:41 +08:00
|
|
|
if m.defaultTag != "" && m.defaultOutbound == nil {
|
|
|
|
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
|
|
|
|
if !loaded {
|
|
|
|
return E.New("default outbound not found: ", m.defaultTag)
|
|
|
|
}
|
|
|
|
m.defaultOutbound = defaultEndpoint
|
|
|
|
}
|
|
|
|
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
|
2024-11-09 21:16:11 +08:00
|
|
|
} else {
|
2024-11-10 12:11:21 +08:00
|
|
|
for _, outbound := range outbounds {
|
2024-11-09 21:16:11 +08:00
|
|
|
err := adapter.LegacyStart(outbound, stage)
|
|
|
|
if err != nil {
|
2024-11-10 16:46:59 +08:00
|
|
|
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
2024-11-09 21:16:11 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-11-10 12:11:21 +08:00
|
|
|
func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
2024-11-09 21:16:11 +08:00
|
|
|
monitor := taskmonitor.New(m.logger, C.StartTimeout)
|
|
|
|
started := make(map[string]bool)
|
|
|
|
for {
|
|
|
|
canContinue := false
|
|
|
|
startOne:
|
2024-11-10 12:11:21 +08:00
|
|
|
for _, outboundToStart := range outbounds {
|
2024-11-09 21:16:11 +08:00
|
|
|
outboundTag := outboundToStart.Tag()
|
|
|
|
if started[outboundTag] {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
dependencies := outboundToStart.Dependencies()
|
|
|
|
for _, dependency := range dependencies {
|
|
|
|
if !started[dependency] {
|
|
|
|
continue startOne
|
|
|
|
}
|
|
|
|
}
|
|
|
|
started[outboundTag] = true
|
|
|
|
canContinue = true
|
2024-11-21 18:10:41 +08:00
|
|
|
if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
|
|
|
|
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
|
|
|
err := starter.Start(adapter.StartStateStart)
|
|
|
|
monitor.Finish()
|
|
|
|
if err != nil {
|
|
|
|
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
|
|
|
}
|
|
|
|
} else if starter, isStarter := outboundToStart.(interface {
|
2024-11-09 21:16:11 +08:00
|
|
|
Start() error
|
|
|
|
}); isStarter {
|
|
|
|
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
|
|
|
err := starter.Start()
|
|
|
|
monitor.Finish()
|
|
|
|
if err != nil {
|
|
|
|
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-11-10 12:11:21 +08:00
|
|
|
if len(started) == len(outbounds) {
|
2024-11-09 21:16:11 +08:00
|
|
|
break
|
|
|
|
}
|
|
|
|
if canContinue {
|
|
|
|
continue
|
|
|
|
}
|
2024-11-10 12:11:21 +08:00
|
|
|
currentOutbound := common.Find(outbounds, func(it adapter.Outbound) bool {
|
2024-11-09 21:16:11 +08:00
|
|
|
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)
|
|
|
|
}
|
2024-11-10 12:11:21 +08:00
|
|
|
m.access.Lock()
|
2024-11-09 21:16:11 +08:00
|
|
|
problemOutbound := m.outboundByTag[problemOutboundTag]
|
2024-11-10 12:11:21 +08:00
|
|
|
m.access.Unlock()
|
2024-11-09 21:16:11 +08:00
|
|
|
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) Close() error {
|
|
|
|
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
|
|
|
m.access.Lock()
|
|
|
|
if !m.started {
|
2024-11-10 12:11:21 +08:00
|
|
|
m.access.Unlock()
|
|
|
|
return nil
|
2024-11-09 21:16:11 +08:00
|
|
|
}
|
|
|
|
m.started = false
|
|
|
|
outbounds := m.outbounds
|
|
|
|
m.outbounds = nil
|
|
|
|
m.access.Unlock()
|
|
|
|
var err error
|
|
|
|
for _, outbound := range outbounds {
|
|
|
|
if closer, isCloser := outbound.(io.Closer); isCloser {
|
|
|
|
monitor.Start("close outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
|
|
|
err = E.Append(err, closer.Close(), func(err error) error {
|
|
|
|
return E.Cause(err, "close outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
|
|
|
})
|
|
|
|
monitor.Finish()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Manager) Outbounds() []adapter.Outbound {
|
|
|
|
m.access.Lock()
|
|
|
|
defer m.access.Unlock()
|
|
|
|
return m.outbounds
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
|
|
|
m.access.Lock()
|
|
|
|
outbound, found := m.outboundByTag[tag]
|
2024-11-21 18:10:41 +08:00
|
|
|
m.access.Unlock()
|
|
|
|
if found {
|
|
|
|
return outbound, true
|
|
|
|
}
|
|
|
|
return m.endpoint.Get(tag)
|
2024-11-09 21:16:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Manager) Default() adapter.Outbound {
|
|
|
|
m.access.Lock()
|
|
|
|
defer m.access.Unlock()
|
|
|
|
if m.defaultOutbound != nil {
|
|
|
|
return m.defaultOutbound
|
|
|
|
} else {
|
|
|
|
return m.defaultOutboundFallback
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Manager) Remove(tag string) error {
|
|
|
|
m.access.Lock()
|
|
|
|
outbound, found := m.outboundByTag[tag]
|
|
|
|
if !found {
|
|
|
|
m.access.Unlock()
|
|
|
|
return os.ErrInvalid
|
|
|
|
}
|
|
|
|
delete(m.outboundByTag, tag)
|
|
|
|
index := common.Index(m.outbounds, func(it adapter.Outbound) bool {
|
|
|
|
return it == outbound
|
|
|
|
})
|
|
|
|
if index == -1 {
|
|
|
|
panic("invalid inbound index")
|
|
|
|
}
|
|
|
|
m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...)
|
|
|
|
started := m.started
|
|
|
|
if m.defaultOutbound == outbound {
|
|
|
|
if len(m.outbounds) > 0 {
|
|
|
|
m.defaultOutbound = m.outbounds[0]
|
|
|
|
m.logger.Info("updated default outbound to ", m.defaultOutbound.Tag())
|
|
|
|
} else {
|
|
|
|
m.defaultOutbound = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
dependBy := m.dependByTag[tag]
|
|
|
|
if len(dependBy) > 0 {
|
|
|
|
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
|
|
|
|
}
|
|
|
|
dependencies := outbound.Dependencies()
|
|
|
|
for _, dependency := range dependencies {
|
|
|
|
if len(m.dependByTag[dependency]) == 1 {
|
|
|
|
delete(m.dependByTag, dependency)
|
|
|
|
} else {
|
|
|
|
m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool {
|
|
|
|
return it != tag
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
m.access.Unlock()
|
|
|
|
if started {
|
|
|
|
return common.Close(outbound)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
if m.started {
|
|
|
|
for _, stage := range adapter.ListStartStages {
|
|
|
|
err = adapter.LegacyStart(outbound, stage)
|
|
|
|
if err != nil {
|
2024-11-10 16:46:59 +08:00
|
|
|
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
2024-11-09 21:16:11 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
|
|
|
|
if m.started {
|
|
|
|
err = common.Close(existsOutbound)
|
|
|
|
if err != nil {
|
|
|
|
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool {
|
|
|
|
return it == existsOutbound
|
|
|
|
})
|
|
|
|
if existsIndex == -1 {
|
|
|
|
panic("invalid inbound index")
|
|
|
|
}
|
|
|
|
m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...)
|
|
|
|
}
|
|
|
|
m.outbounds = append(m.outbounds, outbound)
|
|
|
|
m.outboundByTag[tag] = outbound
|
|
|
|
dependencies := outbound.Dependencies()
|
|
|
|
for _, dependency := range dependencies {
|
|
|
|
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
|
|
|
|
}
|
|
|
|
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
|
|
|
|
m.defaultOutbound = outbound
|
|
|
|
if m.started {
|
|
|
|
m.logger.Info("updated default outbound to ", outbound.Tag())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|