Ver Fonte

refactor: Modular inbound/outbound manager

世界 há 11 meses atrás
pai
commit
19fb214226
69 ficheiros alterados com 1169 adições e 737 exclusões
  1. 2 2
      adapter/experimental.go
  2. 9 1
      adapter/inbound.go
  3. 143 0
      adapter/inbound/manager.go
  4. 24 20
      adapter/inbound/registry.go
  5. 41 0
      adapter/lifecycle.go
  6. 33 0
      adapter/lifecycle_legacy.go
  7. 9 0
      adapter/outbound.go
  8. 265 0
      adapter/outbound/manager.go
  9. 6 2
      adapter/outbound/registry.go
  10. 0 8
      adapter/prestart.go
  11. 1 17
      adapter/router.go
  12. 63 75
      box.go
  13. 0 85
      box_outbound.go
  14. 3 3
      cmd/sing-box/cmd_tools.go
  15. 1 1
      cmd/sing-box/cmd_tools_connect.go
  16. 1 1
      cmd/sing-box/cmd_tools_fetch.go
  17. 1 1
      cmd/sing-box/cmd_tools_fetch_http3.go
  18. 1 2
      cmd/sing-box/cmd_tools_synctime.go
  19. 8 8
      common/dialer/detour.go
  20. 13 5
      common/dialer/dialer.go
  21. 10 18
      common/dialer/router.go
  22. 2 1
      common/settings/proxy_darwin.go
  23. 2 1
      common/tls/ech_client.go
  24. 1 2
      common/tls/reality_server.go
  25. 3 3
      experimental/clashapi.go
  26. 4 4
      experimental/clashapi/api_meta_group.go
  27. 7 12
      experimental/clashapi/proxies.go
  28. 33 31
      experimental/clashapi/server.go
  29. 2 6
      experimental/clashapi/server_resources.go
  30. 8 8
      experimental/clashapi/trafficontrol/tracker.go
  31. 14 15
      protocol/direct/inbound.go
  32. 2 2
      protocol/direct/outbound.go
  33. 3 3
      protocol/group/selector.go
  34. 8 2
      protocol/group/urltest.go
  35. 14 2
      protocol/http/inbound.go
  36. 1 1
      protocol/http/outbound.go
  37. 14 6
      protocol/hysteria/inbound.go
  38. 1 1
      protocol/hysteria/outbound.go
  39. 14 6
      protocol/hysteria2/inbound.go
  40. 1 1
      protocol/hysteria2/outbound.go
  41. 13 1
      protocol/mixed/inbound.go
  42. 2 0
      protocol/redirect/redirect.go
  43. 4 0
      protocol/redirect/tproxy.go
  44. 7 1
      protocol/shadowsocks/inbound.go
  45. 7 1
      protocol/shadowsocks/inbound_multi.go
  46. 7 1
      protocol/shadowsocks/inbound_relay.go
  47. 1 1
      protocol/shadowsocks/outbound.go
  48. 25 15
      protocol/shadowtls/inbound.go
  49. 1 1
      protocol/shadowtls/outbound.go
  50. 13 1
      protocol/socks/inbound.go
  51. 1 1
      protocol/socks/outbound.go
  52. 1 1
      protocol/ssh/outbound.go
  53. 1 1
      protocol/tor/outbound.go
  54. 18 6
      protocol/trojan/inbound.go
  55. 1 1
      protocol/trojan/outbound.go
  56. 14 6
      protocol/tuic/inbound.go
  57. 1 1
      protocol/tuic/outbound.go
  58. 14 6
      protocol/vless/inbound.go
  59. 1 1
      protocol/vless/outbound.go
  60. 14 6
      protocol/vmess/inbound.go
  61. 1 1
      protocol/vmess/outbound.go
  62. 1 1
      protocol/wireguard/outbound.go
  63. 0 1
      route/dns.go
  64. 217 290
      route/router.go
  65. 2 2
      route/rule/rule_action.go
  66. 2 2
      route/rule/rule_default.go
  67. 28 30
      route/rule/rule_set_remote.go
  68. 2 1
      transport/dhcp/server.go
  69. 2 1
      transport/fakeip/server.go

+ 2 - 2
adapter/experimental.go

@@ -15,7 +15,7 @@ import (
 
 type ClashServer interface {
 	Service
-	PreStarter
+	LegacyPreStarter
 	Mode() string
 	ModeList() []string
 	HistoryStorage() *urltest.HistoryStorage
@@ -25,7 +25,7 @@ type ClashServer interface {
 
 type CacheFile interface {
 	Service
-	PreStarter
+	LegacyPreStarter
 
 	StoreFakeIP() bool
 	FakeIPStorage

+ 9 - 1
adapter/inbound.go

@@ -28,7 +28,15 @@ type UDPInjectableInbound interface {
 
 type InboundRegistry interface {
 	option.InboundOptionsRegistry
-	CreateInbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Inbound, error)
+	Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
+}
+
+type InboundManager interface {
+	NewService
+	Inbounds() []Inbound
+	Get(tag string) (Inbound, bool)
+	Remove(tag string) error
+	Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error
 }
 
 type InboundContext struct {

+ 143 - 0
adapter/inbound/manager.go

@@ -0,0 +1,143 @@
+package inbound
+
+import (
+	"context"
+	"os"
+	"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"
+)
+
+var _ adapter.InboundManager = (*Manager)(nil)
+
+type Manager struct {
+	logger       log.ContextLogger
+	registry     adapter.InboundRegistry
+	access       sync.Mutex
+	started      bool
+	stage        adapter.StartStage
+	inbounds     []adapter.Inbound
+	inboundByTag map[string]adapter.Inbound
+}
+
+func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry) *Manager {
+	return &Manager{
+		logger:       logger,
+		registry:     registry,
+		inboundByTag: make(map[string]adapter.Inbound),
+	}
+}
+
+func (m *Manager) Start(stage adapter.StartStage) error {
+	m.access.Lock()
+	defer m.access.Unlock()
+	if m.started && m.stage >= stage {
+		panic("already started")
+	}
+	m.started = true
+	m.stage = stage
+	for _, inbound := range m.inbounds {
+		err := adapter.LegacyStart(inbound, stage)
+		if err != nil {
+			return E.Cause(err, stage.Action(), " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
+		}
+	}
+	return nil
+}
+
+func (m *Manager) Close() error {
+	m.access.Lock()
+	if !m.started {
+		panic("not started")
+	}
+	m.started = false
+	inbounds := m.inbounds
+	m.inbounds = nil
+	m.access.Unlock()
+	monitor := taskmonitor.New(m.logger, C.StopTimeout)
+	var err error
+	for _, inbound := range inbounds {
+		monitor.Start("close inbound/", inbound.Type(), "[", inbound.Tag(), "]")
+		err = E.Append(err, inbound.Close(), func(err error) error {
+			return E.Cause(err, "close inbound/", inbound.Type(), "[", inbound.Tag(), "]")
+		})
+		monitor.Finish()
+	}
+	return nil
+}
+
+func (m *Manager) Inbounds() []adapter.Inbound {
+	m.access.Lock()
+	defer m.access.Unlock()
+	return m.inbounds
+}
+
+func (m *Manager) Get(tag string) (adapter.Inbound, bool) {
+	m.access.Lock()
+	defer m.access.Unlock()
+	inbound, found := m.inboundByTag[tag]
+	return inbound, found
+}
+
+func (m *Manager) Remove(tag string) error {
+	m.access.Lock()
+	inbound, found := m.inboundByTag[tag]
+	if !found {
+		m.access.Unlock()
+		return os.ErrInvalid
+	}
+	delete(m.inboundByTag, tag)
+	index := common.Index(m.inbounds, func(it adapter.Inbound) bool {
+		return it == inbound
+	})
+	if index == -1 {
+		panic("invalid inbound index")
+	}
+	m.inbounds = append(m.inbounds[:index], m.inbounds[index+1:]...)
+	started := m.started
+	m.access.Unlock()
+	if started {
+		return inbound.Close()
+	}
+	return nil
+}
+
+func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
+	inbound, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
+	if err != nil {
+		return err
+	}
+	m.access.Lock()
+	defer m.access.Unlock()
+	if m.started {
+		for _, stage := range adapter.ListStartStages {
+			err = adapter.LegacyStart(inbound, stage)
+			if err != nil {
+				return E.Cause(err, stage.Action(), " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
+			}
+		}
+	}
+	if existsInbound, loaded := m.inboundByTag[tag]; loaded {
+		if m.started {
+			err = existsInbound.Close()
+			if err != nil {
+				return E.Cause(err, "close inbound/", existsInbound.Type(), "[", existsInbound.Tag(), "]")
+			}
+		}
+		existsIndex := common.Index(m.inbounds, func(it adapter.Inbound) bool {
+			return it == existsInbound
+		})
+		if existsIndex == -1 {
+			panic("invalid inbound index")
+		}
+		m.inbounds = append(m.inbounds[:existsIndex], m.inbounds[existsIndex+1:]...)
+	}
+	m.inbounds = append(m.inbounds, inbound)
+	m.inboundByTag[tag] = inbound
+	return nil
+}

+ 24 - 20
adapter/inbound/registry.go

@@ -15,8 +15,12 @@ type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, log
 func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
 	registry.register(outboundType, func() any {
 		return new(Options)
-	}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error) {
-		return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options)))
+	}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Inbound, error) {
+		var options *Options
+		if rawOptions != nil {
+			options = rawOptions.(*Options)
+		}
+		return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
 	})
 }
 
@@ -28,41 +32,41 @@ type (
 )
 
 type Registry struct {
-	access       sync.Mutex
-	optionsType  map[string]optionsConstructorFunc
-	constructors map[string]constructorFunc
+	access      sync.Mutex
+	optionsType map[string]optionsConstructorFunc
+	constructor map[string]constructorFunc
 }
 
 func NewRegistry() *Registry {
 	return &Registry{
-		optionsType:  make(map[string]optionsConstructorFunc),
-		constructors: make(map[string]constructorFunc),
+		optionsType: make(map[string]optionsConstructorFunc),
+		constructor: make(map[string]constructorFunc),
 	}
 }
 
-func (r *Registry) CreateOptions(outboundType string) (any, bool) {
-	r.access.Lock()
-	defer r.access.Unlock()
-	optionsConstructor, loaded := r.optionsType[outboundType]
+func (m *Registry) CreateOptions(outboundType string) (any, bool) {
+	m.access.Lock()
+	defer m.access.Unlock()
+	optionsConstructor, loaded := m.optionsType[outboundType]
 	if !loaded {
 		return nil, false
 	}
 	return optionsConstructor(), true
 }
 
-func (r *Registry) CreateInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
-	r.access.Lock()
-	defer r.access.Unlock()
-	constructor, loaded := r.constructors[outboundType]
+func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
+	m.access.Lock()
+	defer m.access.Unlock()
+	constructor, loaded := m.constructor[outboundType]
 	if !loaded {
 		return nil, E.New("outbound type not found: " + outboundType)
 	}
 	return constructor(ctx, router, logger, tag, options)
 }
 
-func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
-	r.access.Lock()
-	defer r.access.Unlock()
-	r.optionsType[outboundType] = optionsConstructor
-	r.constructors[outboundType] = constructor
+func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
+	m.access.Lock()
+	defer m.access.Unlock()
+	m.optionsType[outboundType] = optionsConstructor
+	m.constructor[outboundType] = constructor
 }

+ 41 - 0
adapter/lifecycle.go

@@ -0,0 +1,41 @@
+package adapter
+
+type StartStage uint8
+
+const (
+	StartStateInitialize StartStage = iota
+	StartStateStart
+	StartStatePostStart
+	StartStateStarted
+)
+
+var ListStartStages = []StartStage{
+	StartStateInitialize,
+	StartStateStart,
+	StartStatePostStart,
+	StartStateStarted,
+}
+
+func (s StartStage) Action() string {
+	switch s {
+	case StartStateInitialize:
+		return "initialize"
+	case StartStateStart:
+		return "start"
+	case StartStatePostStart:
+		return "post-start"
+	case StartStateStarted:
+		return "start-after-started"
+	default:
+		panic("unknown stage")
+	}
+}
+
+type NewService interface {
+	NewStarter
+	Close() error
+}
+
+type NewStarter interface {
+	Start(stage StartStage) error
+}

+ 33 - 0
adapter/lifecycle_legacy.go

@@ -0,0 +1,33 @@
+package adapter
+
+type LegacyPreStarter interface {
+	PreStart() error
+}
+
+type LegacyPostStarter interface {
+	PostStart() error
+}
+
+func LegacyStart(starter any, stage StartStage) error {
+	switch stage {
+	case StartStateInitialize:
+		if preStarter, isPreStarter := starter.(interface {
+			PreStart() error
+		}); isPreStarter {
+			return preStarter.PreStart()
+		}
+	case StartStateStart:
+		if starter, isStarter := starter.(interface {
+			Start() error
+		}); isStarter {
+			return starter.Start()
+		}
+	case StartStatePostStart:
+		if postStarter, isPostStarter := starter.(interface {
+			PostStart() error
+		}); isPostStarter {
+			return postStarter.PostStart()
+		}
+	}
+	return nil
+}

+ 9 - 0
adapter/outbound.go

@@ -22,3 +22,12 @@ type OutboundRegistry interface {
 	option.OutboundOptionsRegistry
 	CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
 }
+
+type OutboundManager interface {
+	NewService
+	Outbounds() []Outbound
+	Outbound(tag string) (Outbound, bool)
+	Default() Outbound
+	Remove(tag string) error
+	Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) error
+}

+ 265 - 0
adapter/outbound/manager.go

@@ -0,0 +1,265 @@
+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
+	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
+}
+
+func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, defaultTag string) *Manager {
+	return &Manager{
+		logger:        logger,
+		registry:      registry,
+		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()
+	defer m.access.Unlock()
+	if m.started && m.stage >= stage {
+		panic("already started")
+	}
+	m.started = true
+	m.stage = stage
+	if stage == adapter.StartStateStart {
+		m.startOutbounds()
+	} else {
+		for _, outbound := range m.outbounds {
+			err := adapter.LegacyStart(outbound, stage)
+			if err != nil {
+				return E.Cause(err, stage.Action(), " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
+			}
+		}
+	}
+	return nil
+}
+
+func (m *Manager) startOutbounds() error {
+	monitor := taskmonitor.New(m.logger, C.StartTimeout)
+	started := make(map[string]bool)
+	for {
+		canContinue := false
+	startOne:
+		for _, outboundToStart := range m.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 starter, isStarter := outboundToStart.(interface {
+				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, "]")
+				}
+			}
+		}
+		if len(started) == len(m.outbounds) {
+			break
+		}
+		if canContinue {
+			continue
+		}
+		currentOutbound := common.Find(m.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 := m.outboundByTag[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) Close() error {
+	monitor := taskmonitor.New(m.logger, C.StopTimeout)
+	m.access.Lock()
+	if !m.started {
+		panic("not started")
+	}
+	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()
+	defer m.access.Unlock()
+	outbound, found := m.outboundByTag[tag]
+	return outbound, found
+}
+
+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 {
+				return E.Cause(err, stage.Action(), " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
+			}
+		}
+	}
+	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
+}

+ 6 - 2
adapter/outbound/registry.go

@@ -15,8 +15,12 @@ type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, log
 func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
 	registry.register(outboundType, func() any {
 		return new(Options)
-	}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error) {
-		return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options)))
+	}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Outbound, error) {
+		var options *Options
+		if rawOptions != nil {
+			options = rawOptions.(*Options)
+		}
+		return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
 	})
 }
 

+ 0 - 8
adapter/prestart.go

@@ -1,9 +1 @@
 package adapter
-
-type PreStarter interface {
-	PreStart() error
-}
-
-type PostStarter interface {
-	PostStart() error
-}

+ 1 - 17
adapter/router.go

@@ -15,21 +15,13 @@ import (
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/x/list"
-	"github.com/sagernet/sing/service"
 
 	mdns "github.com/miekg/dns"
 	"go4.org/netipx"
 )
 
 type Router interface {
-	Service
-	PreStarter
-	PostStarter
-	Cleanup() error
-
-	Outbounds() []Outbound
-	Outbound(tag string) (Outbound, bool)
-	DefaultOutbound(network string) (Outbound, error)
+	NewService
 
 	FakeIPStore() FakeIPStore
 
@@ -84,14 +76,6 @@ type ConnectionRouterEx interface {
 	RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
 }
 
-func ContextWithRouter(ctx context.Context, router Router) context.Context {
-	return service.ContextWith(ctx, router)
-}
-
-func RouterFromContext(ctx context.Context) Router {
-	return service.FromContext[Router](ctx)
-}
-
 type RuleSet interface {
 	Name() string
 	StartContext(ctx context.Context, startContext *HTTPStartContext) error

+ 63 - 75
box.go

@@ -9,6 +9,8 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/experimental"
@@ -30,8 +32,8 @@ var _ adapter.Service = (*Box)(nil)
 type Box struct {
 	createdAt    time.Time
 	router       adapter.Router
-	inbounds     []adapter.Inbound
-	outbounds    []adapter.Outbound
+	inbound      *inbound.Manager
+	outbound     *outbound.Manager
 	logFactory   log.Factory
 	logger       log.ContextLogger
 	preServices1 map[string]adapter.Service
@@ -66,6 +68,7 @@ func New(options Options) (*Box, error) {
 	if ctx == nil {
 		ctx = context.Background()
 	}
+	ctx = service.ContextWithDefaultRegistry(ctx)
 	inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
 	if inboundRegistry == nil {
 		return nil, E.New("missing inbound registry in context")
@@ -74,7 +77,6 @@ func New(options Options) (*Box, error) {
 	if outboundRegistry == nil {
 		return nil, E.New("missing outbound registry in context")
 	}
-	ctx = service.ContextWithDefaultRegistry(ctx)
 	ctx = pause.WithDefaultManager(ctx)
 	experimentalOptions := common.PtrValueOrDefault(options.Experimental)
 	applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
@@ -106,10 +108,15 @@ func New(options Options) (*Box, error) {
 	if err != nil {
 		return nil, E.Cause(err, "create log factory")
 	}
+	routeOptions := common.PtrValueOrDefault(options.Route)
+	inboundManager := inbound.NewManager(logFactory.NewLogger("inbound-manager"), inboundRegistry)
+	outboundManager := outbound.NewManager(logFactory.NewLogger("outbound-manager"), outboundRegistry, routeOptions.Final)
+	ctx = service.ContextWith[adapter.InboundManager](ctx, inboundManager)
+	ctx = service.ContextWith[adapter.OutboundManager](ctx, outboundManager)
 	router, err := route.NewRouter(
 		ctx,
 		logFactory,
-		common.PtrValueOrDefault(options.Route),
+		routeOptions,
 		common.PtrValueOrDefault(options.DNS),
 		common.PtrValueOrDefault(options.NTP),
 		options.Inbounds,
@@ -118,15 +125,13 @@ func New(options Options) (*Box, error) {
 		return nil, E.Cause(err, "parse route options")
 	}
 	for i, inboundOptions := range options.Inbounds {
-		var currentInbound adapter.Inbound
 		var tag string
 		if inboundOptions.Tag != "" {
 			tag = inboundOptions.Tag
 		} else {
 			tag = F.ToString(i)
 		}
-		currentInbound, err = inboundRegistry.CreateInbound(
-			ctx,
+		err = inboundManager.Create(ctx,
 			router,
 			logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
 			tag,
@@ -134,12 +139,10 @@ func New(options Options) (*Box, error) {
 			inboundOptions.Options,
 		)
 		if err != nil {
-			return nil, E.Cause(err, "parse inbound[", i, "]")
+			return nil, E.Cause(err, "initialize inbound[", i, "]")
 		}
-		inbounds = append(inbounds, currentInbound)
 	}
 	for i, outboundOptions := range options.Outbounds {
-		var currentOutbound adapter.Outbound
 		var tag string
 		if outboundOptions.Tag != "" {
 			tag = outboundOptions.Tag
@@ -153,7 +156,7 @@ func New(options Options) (*Box, error) {
 				Outbound: tag,
 			})
 		}
-		currentOutbound, err = outboundRegistry.CreateOutbound(
+		err = outboundManager.Create(
 			outboundCtx,
 			router,
 			logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
@@ -162,16 +165,18 @@ func New(options Options) (*Box, error) {
 			outboundOptions.Options,
 		)
 		if err != nil {
-			return nil, E.Cause(err, "parse outbound[", i, "]")
+			return nil, E.Cause(err, "initialize outbound[", i, "]")
 		}
-		outbounds = append(outbounds, currentOutbound)
 	}
-	err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
-		defaultOutbound, cErr := direct.NewOutbound(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.DirectOutboundOptions{})
-		common.Must(cErr)
-		outbounds = append(outbounds, defaultOutbound)
-		return defaultOutbound
-	})
+	outboundManager.Initialize(common.Must1(
+		direct.NewOutbound(
+			ctx,
+			router,
+			logFactory.NewLogger("outbound/direct"),
+			"direct",
+			option.DirectOutboundOptions{},
+		),
+	))
 	if err != nil {
 		return nil, err
 	}
@@ -195,7 +200,7 @@ func New(options Options) (*Box, error) {
 	if needClashAPI {
 		clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
 		clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
-		clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), clashAPIOptions)
+		clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions)
 		if err != nil {
 			return nil, E.Cause(err, "create clash api server")
 		}
@@ -212,8 +217,8 @@ func New(options Options) (*Box, error) {
 	}
 	return &Box{
 		router:       router,
-		inbounds:     inbounds,
-		outbounds:    outbounds,
+		inbound:      inboundManager,
+		outbound:     outboundManager,
 		createdAt:    createdAt,
 		logFactory:   logFactory,
 		logger:       logFactory.Logger(),
@@ -271,7 +276,7 @@ func (s *Box) preStart() error {
 		return E.Cause(err, "start logger")
 	}
 	for serviceName, service := range s.preServices1 {
-		if preService, isPreService := service.(adapter.PreStarter); isPreService {
+		if preService, isPreService := service.(adapter.LegacyPreStarter); isPreService {
 			monitor.Start("pre-start ", serviceName)
 			err := preService.PreStart()
 			monitor.Finish()
@@ -281,7 +286,7 @@ func (s *Box) preStart() error {
 		}
 	}
 	for serviceName, service := range s.preServices2 {
-		if preService, isPreService := service.(adapter.PreStarter); isPreService {
+		if preService, isPreService := service.(adapter.LegacyPreStarter); isPreService {
 			monitor.Start("pre-start ", serviceName)
 			err := preService.PreStart()
 			monitor.Finish()
@@ -290,15 +295,15 @@ func (s *Box) preStart() error {
 			}
 		}
 	}
-	err = s.router.PreStart()
+	err = s.router.Start(adapter.StartStateInitialize)
 	if err != nil {
-		return E.Cause(err, "pre-start router")
+		return E.Cause(err, "initialize router")
 	}
-	err = s.startOutbounds()
+	err = s.outbound.Start(adapter.StartStateStart)
 	if err != nil {
 		return err
 	}
-	return s.router.Start()
+	return s.router.Start(adapter.StartStateStart)
 }
 
 func (s *Box) start() error {
@@ -318,52 +323,39 @@ func (s *Box) start() error {
 			return E.Cause(err, "start ", serviceName)
 		}
 	}
-	for i, in := range s.inbounds {
-		var tag string
-		if in.Tag() == "" {
-			tag = F.ToString(i)
-		} else {
-			tag = in.Tag()
-		}
-		err = in.Start()
-		if err != nil {
-			return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
-		}
-	}
-	err = s.postStart()
+	err = s.inbound.Start(adapter.StartStateStart)
 	if err != nil {
 		return err
 	}
-	return s.router.Cleanup()
-}
-
-func (s *Box) postStart() error {
 	for serviceName, service := range s.postServices {
 		err := service.Start()
 		if err != nil {
 			return E.Cause(err, "start ", serviceName)
 		}
 	}
-	// TODO: reorganize ALL start order
-	for _, out := range s.outbounds {
-		if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound {
-			err := lateOutbound.PostStart()
-			if err != nil {
-				return E.Cause(err, "post-start outbound/", out.Tag())
-			}
-		}
+	err = s.outbound.Start(adapter.StartStatePostStart)
+	if err != nil {
+		return err
 	}
-	err := s.router.PostStart()
+	err = s.router.Start(adapter.StartStatePostStart)
 	if err != nil {
 		return err
 	}
-	for _, in := range s.inbounds {
-		if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound {
-			err = lateInbound.PostStart()
-			if err != nil {
-				return E.Cause(err, "post-start inbound/", in.Tag())
-			}
-		}
+	err = s.inbound.Start(adapter.StartStatePostStart)
+	if err != nil {
+		return err
+	}
+	err = s.router.Start(adapter.StartStateStarted)
+	if err != nil {
+		return err
+	}
+	err = s.outbound.Start(adapter.StartStateStarted)
+	if err != nil {
+		return err
+	}
+	err = s.inbound.Start(adapter.StartStateStarted)
+	if err != nil {
+		return err
 	}
 	return nil
 }
@@ -384,20 +376,8 @@ func (s *Box) Close() error {
 		})
 		monitor.Finish()
 	}
-	for i, in := range s.inbounds {
-		monitor.Start("close inbound/", in.Type(), "[", i, "]")
-		errors = E.Append(errors, in.Close(), func(err error) error {
-			return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
-		})
-		monitor.Finish()
-	}
-	for i, out := range s.outbounds {
-		monitor.Start("close outbound/", out.Type(), "[", i, "]")
-		errors = E.Append(errors, common.Close(out), func(err error) error {
-			return E.Cause(err, "close outbound/", out.Type(), "[", i, "]")
-		})
-		monitor.Finish()
-	}
+	errors = E.Errors(errors, s.inbound.Close())
+	errors = E.Errors(errors, s.outbound.Close())
 	monitor.Start("close router")
 	if err := common.Close(s.router); err != nil {
 		errors = E.Append(errors, err, func(err error) error {
@@ -427,6 +407,14 @@ func (s *Box) Close() error {
 	return errors
 }
 
+func (s *Box) Inbound() adapter.InboundManager {
+	return s.inbound
+}
+
+func (s *Box) Outbound() adapter.OutboundManager {
+	return s.outbound
+}
+
 func (s *Box) Router() adapter.Router {
 	return s.router
 }

+ 0 - 85
box_outbound.go

@@ -1,85 +0,0 @@
-package box
-
-import (
-	"strings"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/taskmonitor"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing/common"
-	E "github.com/sagernet/sing/common/exceptions"
-	F "github.com/sagernet/sing/common/format"
-)
-
-func (s *Box) startOutbounds() error {
-	monitor := taskmonitor.New(s.logger, C.StartTimeout)
-	outboundTags := make(map[adapter.Outbound]string)
-	outbounds := make(map[string]adapter.Outbound)
-	for i, outboundToStart := range s.outbounds {
-		var outboundTag string
-		if outboundToStart.Tag() == "" {
-			outboundTag = F.ToString(i)
-		} else {
-			outboundTag = outboundToStart.Tag()
-		}
-		if _, exists := outbounds[outboundTag]; exists {
-			return E.New("outbound tag ", outboundTag, " duplicated")
-		}
-		outboundTags[outboundToStart] = outboundTag
-		outbounds[outboundTag] = outboundToStart
-	}
-	started := make(map[string]bool)
-	for {
-		canContinue := false
-	startOne:
-		for _, outboundToStart := range s.outbounds {
-			outboundTag := outboundTags[outboundToStart]
-			if started[outboundTag] {
-				continue
-			}
-			dependencies := outboundToStart.Dependencies()
-			for _, dependency := range dependencies {
-				if !started[dependency] {
-					continue startOne
-				}
-			}
-			started[outboundTag] = true
-			canContinue = true
-			if starter, isStarter := outboundToStart.(interface {
-				Start() error
-			}); isStarter {
-				monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
-				err := starter.Start()
-				monitor.Finish()
-				if err != nil {
-					return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
-				}
-			}
-		}
-		if len(started) == len(s.outbounds) {
-			break
-		}
-		if canContinue {
-			continue
-		}
-		currentOutbound := common.Find(s.outbounds, func(it adapter.Outbound) bool {
-			return !started[outboundTags[it]]
-		})
-		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 := outbounds[problemOutboundTag]
-			if problemOutbound == nil {
-				return E.New("dependency[", problemOutboundTag, "] not found for outbound[", outboundTags[oCurrent], "]")
-			}
-			return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
-		}
-		return lintOutbound([]string{outboundTags[currentOutbound]}, currentOutbound)
-	}
-	return nil
-}

+ 3 - 3
cmd/sing-box/cmd_tools.go

@@ -41,11 +41,11 @@ func createPreStartedClient() (*box.Box, error) {
 	return instance, nil
 }
 
-func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
+func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) {
 	if outboundTag == "" {
-		return instance.Router().DefaultOutbound(N.NetworkName(network))
+		return instance.Outbound().Default(), nil
 	} else {
-		outbound, loaded := instance.Router().Outbound(outboundTag)
+		outbound, loaded := instance.Outbound().Outbound(outboundTag)
 		if !loaded {
 			return nil, E.New("outbound not found: ", outboundTag)
 		}

+ 1 - 1
cmd/sing-box/cmd_tools_connect.go

@@ -45,7 +45,7 @@ func connect(address string) error {
 		return err
 	}
 	defer instance.Close()
-	dialer, err := createDialer(instance, commandConnectFlagNetwork, commandToolsFlagOutbound)
+	dialer, err := createDialer(instance, commandToolsFlagOutbound)
 	if err != nil {
 		return err
 	}

+ 1 - 1
cmd/sing-box/cmd_tools_fetch.go

@@ -48,7 +48,7 @@ func fetch(args []string) error {
 	httpClient = &http.Client{
 		Transport: &http.Transport{
 			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
-				dialer, err := createDialer(instance, network, commandToolsFlagOutbound)
+				dialer, err := createDialer(instance, commandToolsFlagOutbound)
 				if err != nil {
 					return nil, err
 				}

+ 1 - 1
cmd/sing-box/cmd_tools_fetch_http3.go

@@ -16,7 +16,7 @@ import (
 )
 
 func initializeHTTP3Client(instance *box.Box) error {
-	dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
+	dialer, err := createDialer(instance, commandToolsFlagOutbound)
 	if err != nil {
 		return err
 	}

+ 1 - 2
cmd/sing-box/cmd_tools_synctime.go

@@ -9,7 +9,6 @@ import (
 	"github.com/sagernet/sing-box/log"
 	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/ntp"
 
 	"github.com/spf13/cobra"
@@ -45,7 +44,7 @@ func syncTime() error {
 	if err != nil {
 		return err
 	}
-	dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
+	dialer, err := createDialer(instance, commandToolsFlagOutbound)
 	if err != nil {
 		return err
 	}

+ 8 - 8
common/dialer/detour.go

@@ -12,15 +12,15 @@ import (
 )
 
 type DetourDialer struct {
-	router   adapter.Router
-	detour   string
-	dialer   N.Dialer
-	initOnce sync.Once
-	initErr  error
+	outboundManager adapter.OutboundManager
+	detour          string
+	dialer          N.Dialer
+	initOnce        sync.Once
+	initErr         error
 }
 
-func NewDetour(router adapter.Router, detour string) N.Dialer {
-	return &DetourDialer{router: router, detour: detour}
+func NewDetour(outboundManager adapter.OutboundManager, detour string) N.Dialer {
+	return &DetourDialer{outboundManager: outboundManager, detour: detour}
 }
 
 func (d *DetourDialer) Start() error {
@@ -31,7 +31,7 @@ func (d *DetourDialer) Start() error {
 func (d *DetourDialer) Dialer() (N.Dialer, error) {
 	d.initOnce.Do(func() {
 		var loaded bool
-		d.dialer, loaded = d.router.Outbound(d.detour)
+		d.dialer, loaded = d.outboundManager.Outbound(d.detour)
 		if !loaded {
 			d.initErr = E.New("outbound detour not found: ", d.detour)
 		}

+ 13 - 5
common/dialer/dialer.go

@@ -1,21 +1,22 @@
 package dialer
 
 import (
+	"context"
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-dns"
+	E "github.com/sagernet/sing/common/exceptions"
 	N "github.com/sagernet/sing/common/network"
+	"github.com/sagernet/sing/service"
 )
 
-func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) {
+func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) {
+	router := service.FromContext[adapter.Router](ctx)
 	if options.IsWireGuardListener {
 		return NewDefault(router, options)
 	}
-	if router == nil {
-		return NewDefault(nil, options)
-	}
 	var (
 		dialer N.Dialer
 		err    error
@@ -26,7 +27,14 @@ func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error)
 			return nil, err
 		}
 	} else {
-		dialer = NewDetour(router, options.Detour)
+		outboundManager := service.FromContext[adapter.OutboundManager](ctx)
+		if outboundManager == nil {
+			return nil, E.New("missing outbound manager")
+		}
+		dialer = NewDetour(outboundManager, options.Detour)
+	}
+	if router == nil {
+		return NewDefault(router, options)
 	}
 	if options.Detour == "" {
 		dialer = NewResolveDialer(

+ 10 - 18
common/dialer/router.go

@@ -9,30 +9,22 @@ import (
 	N "github.com/sagernet/sing/common/network"
 )
 
-type RouterDialer struct {
-	router adapter.Router
+type DefaultOutboundDialer struct {
+	outboundManager adapter.OutboundManager
 }
 
-func NewRouter(router adapter.Router) N.Dialer {
-	return &RouterDialer{router: router}
+func NewDefaultOutbound(outboundManager adapter.OutboundManager) N.Dialer {
+	return &DefaultOutboundDialer{outboundManager: outboundManager}
 }
 
-func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
-	dialer, err := d.router.DefaultOutbound(network)
-	if err != nil {
-		return nil, err
-	}
-	return dialer.DialContext(ctx, network, destination)
+func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	return d.outboundManager.Default().DialContext(ctx, network, destination)
 }
 
-func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
-	dialer, err := d.router.DefaultOutbound(N.NetworkUDP)
-	if err != nil {
-		return nil, err
-	}
-	return dialer.ListenPacket(ctx, destination)
+func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	return d.outboundManager.Default().ListenPacket(ctx, destination)
 }
 
-func (d *RouterDialer) Upstream() any {
-	return d.router
+func (d *DefaultOutboundDialer) Upstream() any {
+	return d.outboundManager.Default()
 }

+ 2 - 1
common/settings/proxy_darwin.go

@@ -12,6 +12,7 @@ import (
 	M "github.com/sagernet/sing/common/metadata"
 	"github.com/sagernet/sing/common/shell"
 	"github.com/sagernet/sing/common/x/list"
+	"github.com/sagernet/sing/service"
 )
 
 type DarwinSystemProxy struct {
@@ -24,7 +25,7 @@ type DarwinSystemProxy struct {
 }
 
 func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*DarwinSystemProxy, error) {
-	interfaceMonitor := adapter.RouterFromContext(ctx).InterfaceMonitor()
+	interfaceMonitor := service.FromContext[adapter.Router](ctx).InterfaceMonitor()
 	if interfaceMonitor == nil {
 		return nil, E.New("missing interface monitor")
 	}

+ 2 - 1
common/tls/ech_client.go

@@ -19,6 +19,7 @@ import (
 	"github.com/sagernet/sing-dns"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/ntp"
+	"github.com/sagernet/sing/service"
 
 	mDNS "github.com/miekg/dns"
 )
@@ -214,7 +215,7 @@ func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverNam
 				},
 			},
 		}
-		response, err := adapter.RouterFromContext(ctx).Exchange(ctx, message)
+		response, err := service.FromContext[adapter.Router](ctx).Exchange(ctx, message)
 		if err != nil {
 			return nil, err
 		}

+ 1 - 2
common/tls/reality_server.go

@@ -11,7 +11,6 @@ import (
 	"time"
 
 	"github.com/sagernet/reality"
-	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -102,7 +101,7 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
 		tlsConfig.ShortIds[shortID] = true
 	}
 
-	handshakeDialer, err := dialer.New(adapter.RouterFromContext(ctx), options.Reality.Handshake.DialerOptions)
+	handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 3 - 3
experimental/clashapi.go

@@ -12,7 +12,7 @@ import (
 	"github.com/sagernet/sing/common"
 )
 
-type ClashServerConstructor = func(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error)
+type ClashServerConstructor = func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error)
 
 var clashServerConstructor ClashServerConstructor
 
@@ -20,11 +20,11 @@ func RegisterClashServerConstructor(constructor ClashServerConstructor) {
 	clashServerConstructor = constructor
 }
 
-func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
+func NewClashServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
 	if clashServerConstructor == nil {
 		return nil, os.ErrInvalid
 	}
-	return clashServerConstructor(ctx, router, logFactory, options)
+	return clashServerConstructor(ctx, logFactory, options)
 }
 
 func CalculateClashModeList(options option.Options) []string {

+ 4 - 4
experimental/clashapi/api_meta_group.go

@@ -23,7 +23,7 @@ func groupRouter(server *Server) http.Handler {
 	r := chi.NewRouter()
 	r.Get("/", getGroups(server))
 	r.Route("/{name}", func(r chi.Router) {
-		r.Use(parseProxyName, findProxyByName(server.router))
+		r.Use(parseProxyName, findProxyByName(server))
 		r.Get("/", getGroup(server))
 		r.Get("/delay", getGroupDelay(server))
 	})
@@ -32,7 +32,7 @@ func groupRouter(server *Server) http.Handler {
 
 func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
-		groups := common.Map(common.Filter(server.router.Outbounds(), func(it adapter.Outbound) bool {
+		groups := common.Map(common.Filter(server.outboundManager.Outbounds(), func(it adapter.Outbound) bool {
 			_, isGroup := it.(adapter.OutboundGroup)
 			return isGroup
 		}), func(it adapter.Outbound) *badjson.JSONObject {
@@ -86,7 +86,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
 			result, err = urlTestGroup.URLTest(ctx)
 		} else {
 			outbounds := common.FilterNotNil(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
-				itOutbound, _ := server.router.Outbound(it)
+				itOutbound, _ := server.outboundManager.Outbound(it)
 				return itOutbound
 			}))
 			b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
@@ -100,7 +100,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
 					continue
 				}
 				checked[realTag] = true
-				p, loaded := server.router.Outbound(realTag)
+				p, loaded := server.outboundManager.Outbound(realTag)
 				if !loaded {
 					continue
 				}

+ 7 - 12
experimental/clashapi/proxies.go

@@ -23,10 +23,10 @@ import (
 
 func proxyRouter(server *Server, router adapter.Router) http.Handler {
 	r := chi.NewRouter()
-	r.Get("/", getProxies(server, router))
+	r.Get("/", getProxies(server))
 
 	r.Route("/{name}", func(r chi.Router) {
-		r.Use(parseProxyName, findProxyByName(router))
+		r.Use(parseProxyName, findProxyByName(server))
 		r.Get("/", getProxy(server))
 		r.Get("/delay", getProxyDelay(server))
 		r.Put("/", updateProxy)
@@ -42,11 +42,11 @@ func parseProxyName(next http.Handler) http.Handler {
 	})
 }
 
-func findProxyByName(router adapter.Router) func(next http.Handler) http.Handler {
+func findProxyByName(server *Server) func(next http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			name := r.Context().Value(CtxKeyProxyName).(string)
-			proxy, exist := router.Outbound(name)
+			proxy, exist := server.outboundManager.Outbound(name)
 			if !exist {
 				render.Status(r, http.StatusNotFound)
 				render.JSON(w, r, ErrNotFound)
@@ -83,10 +83,10 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
 	return &info
 }
 
-func getProxies(server *Server, router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
+func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var proxyMap badjson.JSONObject
-		outbounds := common.Filter(router.Outbounds(), func(detour adapter.Outbound) bool {
+		outbounds := common.Filter(server.outboundManager.Outbounds(), func(detour adapter.Outbound) bool {
 			return detour.Tag() != ""
 		})
 
@@ -100,12 +100,7 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite
 			allProxies = append(allProxies, detour.Tag())
 		}
 
-		var defaultTag string
-		if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
-			defaultTag = defaultOutbound.Tag()
-		} else {
-			defaultTag = allProxies[0]
-		}
+		defaultTag := server.outboundManager.Default().Tag()
 
 		sort.SliceStable(allProxies, func(i, j int) bool {
 			return allProxies[i] == defaultTag

+ 33 - 31
experimental/clashapi/server.go

@@ -40,15 +40,16 @@ func init() {
 var _ adapter.ClashServer = (*Server)(nil)
 
 type Server struct {
-	ctx            context.Context
-	router         adapter.Router
-	logger         log.Logger
-	httpServer     *http.Server
-	trafficManager *trafficontrol.Manager
-	urlTestHistory *urltest.HistoryStorage
-	mode           string
-	modeList       []string
-	modeUpdateHook chan<- struct{}
+	ctx             context.Context
+	router          adapter.Router
+	outboundManager adapter.OutboundManager
+	logger          log.Logger
+	httpServer      *http.Server
+	trafficManager  *trafficontrol.Manager
+	urlTestHistory  *urltest.HistoryStorage
+	mode            string
+	modeList        []string
+	modeUpdateHook  chan<- struct{}
 
 	externalController       bool
 	externalUI               string
@@ -56,13 +57,14 @@ type Server struct {
 	externalUIDownloadDetour string
 }
 
-func NewServer(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
+func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
 	trafficManager := trafficontrol.NewManager()
 	chiRouter := chi.NewRouter()
-	server := &Server{
-		ctx:    ctx,
-		router: router,
-		logger: logFactory.NewLogger("clash-api"),
+	s := &Server{
+		ctx:             ctx,
+		router:          service.FromContext[adapter.Router](ctx),
+		outboundManager: service.FromContext[adapter.OutboundManager](ctx),
+		logger:          logFactory.NewLogger("clash-api"),
 		httpServer: &http.Server{
 			Addr:    options.ExternalController,
 			Handler: chiRouter,
@@ -73,18 +75,18 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
 		externalUIDownloadURL:    options.ExternalUIDownloadURL,
 		externalUIDownloadDetour: options.ExternalUIDownloadDetour,
 	}
-	server.urlTestHistory = service.PtrFromContext[urltest.HistoryStorage](ctx)
-	if server.urlTestHistory == nil {
-		server.urlTestHistory = urltest.NewHistoryStorage()
+	s.urlTestHistory = service.PtrFromContext[urltest.HistoryStorage](ctx)
+	if s.urlTestHistory == nil {
+		s.urlTestHistory = urltest.NewHistoryStorage()
 	}
 	defaultMode := "Rule"
 	if options.DefaultMode != "" {
 		defaultMode = options.DefaultMode
 	}
-	if !common.Contains(server.modeList, defaultMode) {
-		server.modeList = append([]string{defaultMode}, server.modeList...)
+	if !common.Contains(s.modeList, defaultMode) {
+		s.modeList = append([]string{defaultMode}, s.modeList...)
 	}
-	server.mode = defaultMode
+	s.mode = defaultMode
 	//goland:noinspection GoDeprecation
 	//nolint:staticcheck
 	if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" {
@@ -108,27 +110,27 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
 		r.Get("/logs", getLogs(logFactory))
 		r.Get("/traffic", traffic(trafficManager))
 		r.Get("/version", version)
-		r.Mount("/configs", configRouter(server, logFactory))
-		r.Mount("/proxies", proxyRouter(server, router))
-		r.Mount("/rules", ruleRouter(router))
-		r.Mount("/connections", connectionRouter(router, trafficManager))
+		r.Mount("/configs", configRouter(s, logFactory))
+		r.Mount("/proxies", proxyRouter(s, s.router))
+		r.Mount("/rules", ruleRouter(s.router))
+		r.Mount("/connections", connectionRouter(s.router, trafficManager))
 		r.Mount("/providers/proxies", proxyProviderRouter())
 		r.Mount("/providers/rules", ruleProviderRouter())
 		r.Mount("/script", scriptRouter())
 		r.Mount("/profile", profileRouter())
 		r.Mount("/cache", cacheRouter(ctx))
-		r.Mount("/dns", dnsRouter(router))
+		r.Mount("/dns", dnsRouter(s.router))
 
-		server.setupMetaAPI(r)
+		s.setupMetaAPI(r)
 	})
 	if options.ExternalUI != "" {
-		server.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI))
+		s.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI))
 		chiRouter.Group(func(r chi.Router) {
 			r.Get("/ui", http.RedirectHandler("/ui/", http.StatusMovedPermanently).ServeHTTP)
-			r.Handle("/ui/*", http.StripPrefix("/ui/", http.FileServer(http.Dir(server.externalUI))))
+			r.Handle("/ui/*", http.StripPrefix("/ui/", http.FileServer(http.Dir(s.externalUI))))
 		})
 	}
-	return server, nil
+	return s, nil
 }
 
 func (s *Server) PreStart() error {
@@ -232,12 +234,12 @@ func (s *Server) TrafficManager() *trafficontrol.Manager {
 }
 
 func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) {
-	tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.router, matchedRule)
+	tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule)
 	return tracker, tracker
 }
 
 func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) (N.PacketConn, adapter.Tracker) {
-	tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.router, matchedRule)
+	tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule)
 	return tracker, tracker
 }
 

+ 2 - 6
experimental/clashapi/server_resources.go

@@ -15,7 +15,6 @@ import (
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/service/filemanager"
 )
 
@@ -45,16 +44,13 @@ func (s *Server) downloadExternalUI() error {
 	s.logger.Info("downloading external ui")
 	var detour adapter.Outbound
 	if s.externalUIDownloadDetour != "" {
-		outbound, loaded := s.router.Outbound(s.externalUIDownloadDetour)
+		outbound, loaded := s.outboundManager.Outbound(s.externalUIDownloadDetour)
 		if !loaded {
 			return E.New("detour outbound not found: ", s.externalUIDownloadDetour)
 		}
 		detour = outbound
 	} else {
-		outbound, err := s.router.DefaultOutbound(N.NetworkTCP)
-		if err != nil {
-			return err
-		}
+		outbound := s.outboundManager.Default()
 		detour = outbound
 	}
 	httpClient := &http.Client{

+ 8 - 8
experimental/clashapi/trafficontrol/tracker.go

@@ -124,7 +124,7 @@ func (tt *TCPConn) WriterReplaceable() bool {
 	return true
 }
 
-func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *TCPConn {
+func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, rule adapter.Rule) *TCPConn {
 	id, _ := uuid.NewV4()
 	var (
 		chain        []string
@@ -138,11 +138,11 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundCont
 	}
 	if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction {
 		next = routeAction.Outbound
-	} else if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
-		next = defaultOutbound.Tag()
+	} else {
+		next = outboundManager.Default().Tag()
 	}
 	for {
-		detour, loaded := router.Outbound(next)
+		detour, loaded := outboundManager.Outbound(next)
 		if !loaded {
 			break
 		}
@@ -213,7 +213,7 @@ func (ut *UDPConn) WriterReplaceable() bool {
 	return true
 }
 
-func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *UDPConn {
+func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, rule adapter.Rule) *UDPConn {
 	id, _ := uuid.NewV4()
 	var (
 		chain        []string
@@ -227,11 +227,11 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.Inbound
 	}
 	if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction {
 		next = routeAction.Outbound
-	} else if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil {
-		next = defaultOutbound.Tag()
+	} else {
+		next = outboundManager.Default().Tag()
 	}
 	for {
-		detour, loaded := router.Outbound(next)
+		detour, loaded := outboundManager.Outbound(next)
 		if !loaded {
 			break
 		}

+ 14 - 15
protocol/direct/inbound.go

@@ -76,21 +76,6 @@ func (i *Inbound) Close() error {
 	return i.listener.Close()
 }
 
-func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	switch i.overrideOption {
-	case 1:
-		metadata.Destination = i.overrideDestination
-	case 2:
-		destination := i.overrideDestination
-		destination.Port = metadata.Destination.Port
-		metadata.Destination = destination
-	case 3:
-		metadata.Destination.Port = i.overrideDestination.Port
-	}
-	i.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
-	return i.router.RouteConnection(ctx, conn, metadata)
-}
-
 func (i *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) {
 	var destination M.Socksaddr
 	switch i.overrideOption {
@@ -107,9 +92,21 @@ func (i *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) {
 }
 
 func (i *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	switch i.overrideOption {
+	case 1:
+		metadata.Destination = i.overrideDestination
+	case 2:
+		destination := i.overrideDestination
+		destination.Port = metadata.Destination.Port
+		metadata.Destination = destination
+	case 3:
+		metadata.Destination.Port = i.overrideDestination.Port
+	}
 	i.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
 	metadata.Inbound = i.Tag()
 	metadata.InboundType = i.Type()
+	metadata.InboundDetour = i.listener.ListenOptions().Detour
+	metadata.InboundOptions = i.listener.ListenOptions().InboundOptions
 	i.router.RouteConnectionEx(ctx, conn, metadata, onClose)
 }
 
@@ -119,6 +116,8 @@ func (i *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
 	var metadata adapter.InboundContext
 	metadata.Inbound = i.Tag()
 	metadata.InboundType = i.Type()
+	metadata.InboundDetour = i.listener.ListenOptions().Detour
+	metadata.InboundOptions = i.listener.ListenOptions().InboundOptions
 	metadata.Source = source
 	metadata.Destination = destination
 	metadata.OriginDestination = i.listener.UDPAddr()

+ 2 - 2
protocol/direct/outbound.go

@@ -12,7 +12,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-dns"
+	dns "github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing/common/bufio"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
@@ -39,7 +39,7 @@ type Outbound struct {
 
 func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (adapter.Outbound, error) {
 	options.UDPFragmentDefault = true
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 3 - 3
protocol/group/selector.go

@@ -26,7 +26,7 @@ var _ adapter.OutboundGroup = (*Selector)(nil)
 type Selector struct {
 	outbound.Adapter
 	ctx                          context.Context
-	router                       adapter.Router
+	outboundManager              adapter.OutboundManager
 	logger                       logger.ContextLogger
 	tags                         []string
 	defaultTag                   string
@@ -40,7 +40,7 @@ func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextL
 	outbound := &Selector{
 		Adapter:                      outbound.NewAdapter(C.TypeSelector, nil, tag, options.Outbounds),
 		ctx:                          ctx,
-		router:                       router,
+		outboundManager:              service.FromContext[adapter.OutboundManager](ctx),
 		logger:                       logger,
 		tags:                         options.Outbounds,
 		defaultTag:                   options.Default,
@@ -63,7 +63,7 @@ func (s *Selector) Network() []string {
 
 func (s *Selector) Start() error {
 	for i, tag := range s.tags {
-		detour, loaded := s.router.Outbound(tag)
+		detour, loaded := s.outboundManager.Outbound(tag)
 		if !loaded {
 			return E.New("outbound ", i, " not found: ", tag)
 		}

+ 8 - 2
protocol/group/urltest.go

@@ -36,6 +36,7 @@ type URLTest struct {
 	outbound.Adapter
 	ctx                          context.Context
 	router                       adapter.Router
+	outboundManager              adapter.OutboundManager
 	logger                       log.ContextLogger
 	tags                         []string
 	link                         string
@@ -51,6 +52,7 @@ func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLo
 		Adapter:                      outbound.NewAdapter(C.TypeURLTest, []string{N.NetworkTCP, N.NetworkUDP}, tag, options.Outbounds),
 		ctx:                          ctx,
 		router:                       router,
+		outboundManager:              service.FromContext[adapter.OutboundManager](ctx),
 		logger:                       logger,
 		tags:                         options.Outbounds,
 		link:                         options.URL,
@@ -68,7 +70,7 @@ func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLo
 func (s *URLTest) Start() error {
 	outbounds := make([]adapter.Outbound, 0, len(s.tags))
 	for i, tag := range s.tags {
-		detour, loaded := s.router.Outbound(tag)
+		detour, loaded := s.outboundManager.Outbound(tag)
 		if !loaded {
 			return E.New("outbound ", i, " not found: ", tag)
 		}
@@ -77,6 +79,7 @@ func (s *URLTest) Start() error {
 	group, err := NewURLTestGroup(
 		s.ctx,
 		s.router,
+		s.outboundManager,
 		s.logger,
 		outbounds,
 		s.link,
@@ -190,6 +193,7 @@ func (s *URLTest) InterfaceUpdated() {
 type URLTestGroup struct {
 	ctx                          context.Context
 	router                       adapter.Router
+	outboundManager              adapter.OutboundManager
 	logger                       log.Logger
 	outbounds                    []adapter.Outbound
 	link                         string
@@ -214,6 +218,7 @@ type URLTestGroup struct {
 func NewURLTestGroup(
 	ctx context.Context,
 	router adapter.Router,
+	outboundManager adapter.OutboundManager,
 	logger log.Logger,
 	outbounds []adapter.Outbound,
 	link string,
@@ -244,6 +249,7 @@ func NewURLTestGroup(
 	return &URLTestGroup{
 		ctx:                          ctx,
 		router:                       router,
+		outboundManager:              outboundManager,
 		logger:                       logger,
 		outbounds:                    outbounds,
 		link:                         link,
@@ -385,7 +391,7 @@ func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint
 			continue
 		}
 		checked[realTag] = true
-		p, loaded := g.router.Outbound(realTag)
+		p, loaded := g.outboundManager.Outbound(realTag)
 		if !loaded {
 			continue
 		}

+ 14 - 2
protocol/http/inbound.go

@@ -73,7 +73,7 @@ func (h *Inbound) Start() error {
 
 func (h *Inbound) Close() error {
 	return common.Close(
-		&h.listener,
+		h.listener,
 		h.tlsConfig,
 	)
 }
@@ -82,7 +82,11 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
 	err := h.newConnection(ctx, conn, metadata, onClose)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		if E.IsClosedOrCanceled(err) {
+			h.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		}
 	}
 }
 
@@ -98,6 +102,10 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada
 }
 
 func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	user, loaded := auth.UserFromContext[string](ctx)
 	if !loaded {
 		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
@@ -110,6 +118,10 @@ func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata
 }
 
 func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	user, loaded := auth.UserFromContext[string](ctx)
 	if !loaded {
 		h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)

+ 1 - 1
protocol/http/outbound.go

@@ -30,7 +30,7 @@ type Outbound struct {
 }
 
 func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (adapter.Outbound, error) {
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 14 - 6
protocol/hysteria/inbound.go

@@ -17,6 +17,7 @@ import (
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/auth"
 	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
 
@@ -85,7 +86,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
 		XPlusPassword: options.Obfs,
 		TLSConfig:     tlsConfig,
 		UDPTimeout:    udpTimeout,
-		Handler:       adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil),
+		Handler:       inbound,
 
 		// Legacy options
 
@@ -117,12 +118,16 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
 	return inbound, nil
 }
 
-func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	ctx = log.ContextWithNewID(ctx)
+	var metadata adapter.InboundContext
 	metadata.Inbound = h.Tag()
 	metadata.InboundType = h.Type()
 	metadata.InboundDetour = h.listener.ListenOptions().Detour
 	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	metadata.OriginDestination = h.listener.UDPAddr()
+	metadata.Source = source
+	metadata.Destination = destination
 	h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
 	userID, _ := auth.UserFromContext[int](ctx)
 	if userName := h.userNameList[userID]; userName != "" {
@@ -131,16 +136,19 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada
 	} else {
 		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
 	}
-	return h.router.RouteConnection(ctx, conn, metadata)
+	h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
 }
 
-func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	ctx = log.ContextWithNewID(ctx)
+	var metadata adapter.InboundContext
 	metadata.Inbound = h.Tag()
 	metadata.InboundType = h.Type()
 	metadata.InboundDetour = h.listener.ListenOptions().Detour
 	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	metadata.OriginDestination = h.listener.UDPAddr()
+	metadata.Source = source
+	metadata.Destination = destination
 	h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
 	userID, _ := auth.UserFromContext[int](ctx)
 	if userName := h.userNameList[userID]; userName != "" {
@@ -149,7 +157,7 @@ func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, me
 	} else {
 		h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
 	}
-	return h.router.RoutePacketConnection(ctx, conn, metadata)
+	h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
 }
 
 func (h *Inbound) Start() error {
@@ -168,7 +176,7 @@ func (h *Inbound) Start() error {
 
 func (h *Inbound) Close() error {
 	return common.Close(
-		&h.listener,
+		h.listener,
 		h.tlsConfig,
 		common.PtrOrNil(h.service),
 	)

+ 1 - 1
protocol/hysteria/outbound.go

@@ -47,7 +47,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
 	if err != nil {
 		return nil, err
 	}
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 14 - 6
protocol/hysteria2/inbound.go

@@ -20,6 +20,7 @@ import (
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/auth"
 	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
 
@@ -108,7 +109,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
 		TLSConfig:             tlsConfig,
 		IgnoreClientBandwidth: options.IgnoreClientBandwidth,
 		UDPTimeout:            udpTimeout,
-		Handler:               adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil),
+		Handler:               inbound,
 		MasqueradeHandler:     masqueradeHandler,
 	})
 	if err != nil {
@@ -128,12 +129,16 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
 	return inbound, nil
 }
 
-func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	ctx = log.ContextWithNewID(ctx)
+	var metadata adapter.InboundContext
 	metadata.Inbound = h.Tag()
 	metadata.InboundType = h.Type()
 	metadata.InboundDetour = h.listener.ListenOptions().Detour
 	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	metadata.OriginDestination = h.listener.UDPAddr()
+	metadata.Source = source
+	metadata.Destination = destination
 	h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
 	userID, _ := auth.UserFromContext[int](ctx)
 	if userName := h.userNameList[userID]; userName != "" {
@@ -142,16 +147,19 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada
 	} else {
 		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
 	}
-	return h.router.RouteConnection(ctx, conn, metadata)
+	h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
 }
 
-func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	ctx = log.ContextWithNewID(ctx)
+	var metadata adapter.InboundContext
 	metadata.Inbound = h.Tag()
 	metadata.InboundType = h.Type()
 	metadata.InboundDetour = h.listener.ListenOptions().Detour
 	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	metadata.OriginDestination = h.listener.UDPAddr()
+	metadata.Source = source
+	metadata.Destination = destination
 	h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
 	userID, _ := auth.UserFromContext[int](ctx)
 	if userName := h.userNameList[userID]; userName != "" {
@@ -160,7 +168,7 @@ func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, me
 	} else {
 		h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
 	}
-	return h.router.RoutePacketConnection(ctx, conn, metadata)
+	h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
 }
 
 func (h *Inbound) Start() error {
@@ -179,7 +187,7 @@ func (h *Inbound) Start() error {
 
 func (h *Inbound) Close() error {
 	return common.Close(
-		&h.listener,
+		h.listener,
 		h.tlsConfig,
 		common.PtrOrNil(h.service),
 	)

+ 1 - 1
protocol/hysteria2/outbound.go

@@ -59,7 +59,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
 			return nil, E.New("unknown obfs type: ", options.Obfs.Type)
 		}
 	}
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 13 - 1
protocol/mixed/inbound.go

@@ -66,7 +66,11 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
 	err := h.newConnection(ctx, conn, metadata, onClose)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		if E.IsClosedOrCanceled(err) {
+			h.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		}
 	}
 }
 
@@ -85,6 +89,10 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada
 }
 
 func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	user, loaded := auth.UserFromContext[string](ctx)
 	if !loaded {
 		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
@@ -97,6 +105,10 @@ func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata
 }
 
 func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	user, loaded := auth.UserFromContext[string](ctx)
 	if !loaded {
 		h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)

+ 2 - 0
protocol/redirect/redirect.go

@@ -59,6 +59,8 @@ func (h *Redirect) NewConnectionEx(ctx context.Context, conn net.Conn, metadata
 	}
 	metadata.Inbound = h.Tag()
 	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	metadata.Destination = M.SocksaddrFromNetIP(destination)
 	h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
 	h.router.RouteConnectionEx(ctx, conn, metadata, onClose)

+ 4 - 0
protocol/redirect/tproxy.go

@@ -90,6 +90,8 @@ func (t *TProxy) Close() error {
 }
 
 func (t *TProxy) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	metadata.Inbound = t.Tag()
+	metadata.InboundType = t.Type()
 	metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
 	t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
 	t.router.RouteConnectionEx(ctx, conn, metadata, onClose)
@@ -101,6 +103,8 @@ func (t *TProxy) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, s
 	var metadata adapter.InboundContext
 	metadata.Inbound = t.Tag()
 	metadata.InboundType = t.Type()
+	metadata.InboundDetour = t.listener.ListenOptions().Detour
+	metadata.InboundOptions = t.listener.ListenOptions().InboundOptions
 	metadata.Source = source
 	metadata.Destination = destination
 	metadata.OriginDestination = t.listener.UDPAddr()

+ 7 - 1
protocol/shadowsocks/inbound.go

@@ -105,7 +105,11 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
 	err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata))
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		if E.IsClosedOrCanceled(err) {
+			h.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		}
 	}
 }
 
@@ -120,6 +124,8 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada
 	h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
 	metadata.Inbound = h.Tag()
 	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 

+ 7 - 1
protocol/shadowsocks/inbound_multi.go

@@ -113,7 +113,11 @@ func (h *MultiInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metad
 	err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata))
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		if E.IsClosedOrCanceled(err) {
+			h.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		}
 	}
 }
 
@@ -138,6 +142,8 @@ func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadat
 	h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
 	metadata.Inbound = h.Tag()
 	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 

+ 7 - 1
protocol/shadowsocks/inbound_relay.go

@@ -98,7 +98,11 @@ func (h *RelayInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metad
 	err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata))
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		if E.IsClosedOrCanceled(err) {
+			h.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		}
 	}
 }
 
@@ -123,6 +127,8 @@ func (h *RelayInbound) newConnection(ctx context.Context, conn net.Conn, metadat
 	h.logger.InfoContext(ctx, "[", destination, "] inbound connection to ", metadata.Destination)
 	metadata.Inbound = h.Tag()
 	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 

+ 1 - 1
protocol/shadowsocks/outbound.go

@@ -44,7 +44,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
 	if err != nil {
 		return nil, err
 	}
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 25 - 15
protocol/shadowtls/inbound.go

@@ -16,6 +16,7 @@ import (
 	"github.com/sagernet/sing/common/auth"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
+	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
 
@@ -46,7 +47,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
 	if options.Version > 1 {
 		handshakeForServerName = make(map[string]shadowtls.HandshakeConfig)
 		for serverName, serverOptions := range options.HandshakeForServerName {
-			handshakeDialer, err := dialer.New(router, serverOptions.DialerOptions)
+			handshakeDialer, err := dialer.New(ctx, serverOptions.DialerOptions)
 			if err != nil {
 				return nil, err
 			}
@@ -56,7 +57,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
 			}
 		}
 	}
-	handshakeDialer, err := dialer.New(router, options.Handshake.DialerOptions)
+	handshakeDialer, err := dialer.New(ctx, options.Handshake.DialerOptions)
 	if err != nil {
 		return nil, err
 	}
@@ -72,7 +73,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
 		},
 		HandshakeForServerName: handshakeForServerName,
 		StrictMode:             options.StrictMode,
-		Handler:                adapter.NewUpstreamContextHandler(inbound.newConnection, nil, nil),
+		Handler:                (*inboundHandler)(inbound),
 		Logger:                 logger,
 	})
 	if err != nil {
@@ -97,24 +98,33 @@ func (h *Inbound) Close() error {
 	return h.listener.Close()
 }
 
-func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	err := h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, metadata.Source, metadata.Destination, onClose)
+	N.CloseOnHandshakeFailure(conn, onClose, err)
+	if err != nil {
+		if E.IsClosedOrCanceled(err) {
+			h.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		}
+	}
 }
 
-func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+type inboundHandler Inbound
+
+func (h *inboundHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+	var metadata adapter.InboundContext
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	metadata.Source = source
+	metadata.Destination = destination
 	if userName, _ := auth.UserFromContext[string](ctx); userName != "" {
 		metadata.User = userName
 		h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination)
 	} else {
 		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
 	}
-	return h.router.RouteConnection(ctx, conn, metadata)
-}
-
-func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
-	err := h.NewConnection(ctx, conn, metadata)
-	N.CloseOnHandshakeFailure(conn, onClose, err)
-	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
-	}
+	h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
 }

+ 1 - 1
protocol/shadowtls/outbound.go

@@ -68,7 +68,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
 			tlsHandshakeFunc = shadowtls.DefaultTLSHandshakeFunc(options.Password, stdTLSConfig)
 		}
 	}
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 13 - 1
protocol/socks/inbound.go

@@ -62,11 +62,19 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
 	err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, metadata.Destination, onClose)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		if E.IsClosedOrCanceled(err) {
+			h.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		}
 	}
 }
 
 func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	user, loaded := auth.UserFromContext[string](ctx)
 	if !loaded {
 		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
@@ -79,6 +87,10 @@ func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata
 }
 
 func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	user, loaded := auth.UserFromContext[string](ctx)
 	if !loaded {
 		h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)

+ 1 - 1
protocol/socks/outbound.go

@@ -46,7 +46,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
 	if err != nil {
 		return nil, err
 	}
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 1 - 1
protocol/ssh/outbound.go

@@ -49,7 +49,7 @@ type Outbound struct {
 }
 
 func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHOutboundOptions) (adapter.Outbound, error) {
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 1 - 1
protocol/tor/outbound.go

@@ -75,7 +75,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
 		}
 		startConf.TorrcFile = torrcFile
 	}
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 18 - 6
protocol/trojan/inbound.go

@@ -149,7 +149,7 @@ func (h *Inbound) Start() error {
 
 func (h *Inbound) Close() error {
 	return common.Close(
-		&h.listener,
+		h.listener,
 		h.tlsConfig,
 		h.transport,
 	)
@@ -170,11 +170,19 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
 	err := h.NewConnection(ctx, conn, metadata)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		if E.IsClosedOrCanceled(err) {
+			h.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		}
 	}
 }
 
 func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -207,12 +215,20 @@ func (h *Inbound) fallbackConnection(ctx context.Context, conn net.Conn, metadat
 		}
 		fallbackAddr = h.fallbackAddr
 	}
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	h.logger.InfoContext(ctx, "fallback connection to ", fallbackAddr)
 	metadata.Destination = fallbackAddr
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
 func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -233,10 +249,6 @@ type inboundTransportHandler Inbound
 
 func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	var metadata adapter.InboundContext
-	metadata.Inbound = h.Tag()
-	metadata.InboundType = h.Type()
-	metadata.InboundDetour = h.listener.ListenOptions().Detour
-	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	metadata.Source = source
 	metadata.Destination = destination
 	h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)

+ 1 - 1
protocol/trojan/outbound.go

@@ -38,7 +38,7 @@ type Outbound struct {
 }
 
 func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanOutboundOptions) (adapter.Outbound, error) {
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 14 - 6
protocol/tuic/inbound.go

@@ -17,6 +17,7 @@ import (
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/auth"
 	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 
 	"github.com/gofrs/uuid/v5"
@@ -71,7 +72,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
 		ZeroRTTHandshake:  options.ZeroRTTHandshake,
 		Heartbeat:         time.Duration(options.Heartbeat),
 		UDPTimeout:        udpTimeout,
-		Handler:           adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil),
+		Handler:           inbound,
 	})
 	if err != nil {
 		return nil, err
@@ -99,12 +100,16 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
 	return inbound, nil
 }
 
-func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	ctx = log.ContextWithNewID(ctx)
+	var metadata adapter.InboundContext
 	metadata.Inbound = h.Tag()
 	metadata.InboundType = h.Type()
 	metadata.InboundDetour = h.listener.ListenOptions().Detour
 	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	metadata.OriginDestination = h.listener.UDPAddr()
+	metadata.Source = source
+	metadata.Destination = destination
 	h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
 	userID, _ := auth.UserFromContext[int](ctx)
 	if userName := h.userNameList[userID]; userName != "" {
@@ -113,16 +118,19 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada
 	} else {
 		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
 	}
-	return h.router.RouteConnection(ctx, conn, metadata)
+	h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
 }
 
-func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	ctx = log.ContextWithNewID(ctx)
+	var metadata adapter.InboundContext
 	metadata.Inbound = h.Tag()
 	metadata.InboundType = h.Type()
 	metadata.InboundDetour = h.listener.ListenOptions().Detour
 	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	metadata.OriginDestination = h.listener.UDPAddr()
+	metadata.Source = source
+	metadata.Destination = destination
 	h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
 	userID, _ := auth.UserFromContext[int](ctx)
 	if userName := h.userNameList[userID]; userName != "" {
@@ -131,7 +139,7 @@ func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, me
 	} else {
 		h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
 	}
-	return h.router.RoutePacketConnection(ctx, conn, metadata)
+	h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
 }
 
 func (h *Inbound) Start() error {
@@ -150,7 +158,7 @@ func (h *Inbound) Start() error {
 
 func (h *Inbound) Close() error {
 	return common.Close(
-		&h.listener,
+		h.listener,
 		h.tlsConfig,
 		common.PtrOrNil(h.server),
 	)

+ 1 - 1
protocol/tuic/outbound.go

@@ -60,7 +60,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
 	case "quic":
 		tuicUDPStream = true
 	}
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 14 - 6
protocol/vless/inbound.go

@@ -129,7 +129,7 @@ func (h *Inbound) Start() error {
 func (h *Inbound) Close() error {
 	return common.Close(
 		h.service,
-		&h.listener,
+		h.listener,
 		h.tlsConfig,
 		h.transport,
 	)
@@ -150,11 +150,19 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
 	err := h.NewConnection(ctx, conn, metadata)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		if E.IsClosedOrCanceled(err) {
+			h.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		}
 	}
 }
 
 func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -170,6 +178,10 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada
 }
 
 func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -196,10 +208,6 @@ type inboundTransportHandler Inbound
 
 func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	var metadata adapter.InboundContext
-	metadata.Inbound = h.Tag()
-	metadata.InboundType = h.Type()
-	metadata.InboundDetour = h.listener.ListenOptions().Detour
-	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	metadata.Source = source
 	metadata.Destination = destination
 	h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)

+ 1 - 1
protocol/vless/outbound.go

@@ -41,7 +41,7 @@ type Outbound struct {
 }
 
 func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (adapter.Outbound, error) {
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 14 - 6
protocol/vmess/inbound.go

@@ -143,7 +143,7 @@ func (h *Inbound) Start() error {
 func (h *Inbound) Close() error {
 	return common.Close(
 		h.service,
-		&h.listener,
+		h.listener,
 		h.tlsConfig,
 		h.transport,
 	)
@@ -164,11 +164,19 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
 	err := h.NewConnection(ctx, conn, metadata)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		if E.IsClosedOrCanceled(err) {
+			h.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+		}
 	}
 }
 
 func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -184,6 +192,10 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada
 }
 
 func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -210,10 +222,6 @@ type inboundTransportHandler Inbound
 
 func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	var metadata adapter.InboundContext
-	metadata.Inbound = h.Tag()
-	metadata.InboundType = h.Type()
-	metadata.InboundDetour = h.listener.ListenOptions().Detour
-	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	metadata.Source = source
 	metadata.Destination = destination
 	h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)

+ 1 - 1
protocol/vmess/outbound.go

@@ -41,7 +41,7 @@ type Outbound struct {
 }
 
 func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessOutboundOptions) (adapter.Outbound, error) {
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	outboundDialer, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 1 - 1
protocol/wireguard/outbound.go

@@ -78,7 +78,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
 		options.IsWireGuardListener = true
 		outbound.useStdNetBind = true
 	}
-	listener, err := dialer.New(router, options.DialerOptions)
+	listener, err := dialer.New(ctx, options.DialerOptions)
 	if err != nil {
 		return nil, err
 	}

+ 0 - 1
route/dns.go

@@ -77,7 +77,6 @@ func exchangeDNSPacket(ctx context.Context, router *Router, conn N.PacketConn, b
 		return err
 	}
 	err = conn.WritePacket(responseBuffer, destination)
-	responseBuffer.Release()
 	return err
 }
 

+ 217 - 290
route/router.go

@@ -87,7 +87,7 @@ type Router struct {
 	v2rayServer                        adapter.V2RayServer
 	platformInterface                  platform.Interface
 	needWIFIState                      bool
-	needPackageManager                 bool
+	enforcePackageManager              bool
 	wifiState                          adapter.WIFIState
 	started                            bool
 }
@@ -123,7 +123,7 @@ func NewRouter(
 		pauseManager:          service.FromContext[pause.Manager](ctx),
 		platformInterface:     service.FromContext[platform.Interface](ctx),
 		needWIFIState:         hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule),
-		needPackageManager: common.Any(inbounds, func(inbound option.Inbound) bool {
+		enforcePackageManager: common.Any(inbounds, func(inbound option.Inbound) bool {
 			if tunOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN && tunOptions.AutoRoute {
 				return true
 			}
@@ -191,7 +191,8 @@ func NewRouter(
 		transportTags[i] = tag
 		transportTagMap[tag] = true
 	}
-	ctx = adapter.ContextWithRouter(ctx, router)
+	ctx = service.ContextWith[adapter.Router](ctx, router)
+	outboundManager := service.FromContext[adapter.OutboundManager](ctx)
 	for {
 		lastLen := len(dummyTransportMap)
 		for i, server := range dnsOptions.Servers {
@@ -201,9 +202,9 @@ func NewRouter(
 			}
 			var detour N.Dialer
 			if server.Detour == "" {
-				detour = dialer.NewRouter(router)
+				detour = dialer.NewDefaultOutbound(outboundManager)
 			} else {
-				detour = dialer.NewDetour(router, server.Detour)
+				detour = dialer.NewDetour(outboundManager, server.Detour)
 			}
 			var serverProtocol string
 			switch server.Address {
@@ -327,7 +328,7 @@ func NewRouter(
 	}
 
 	usePlatformDefaultInterfaceMonitor := router.platformInterface != nil && router.platformInterface.UsePlatformDefaultInterfaceMonitor()
-	needInterfaceMonitor := options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool {
+	enforceInterfaceMonitor := options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool {
 		if httpMixedOptions, isHTTPMixed := inbound.Options.(*option.HTTPMixedInboundOptions); isHTTPMixed && httpMixedOptions.SetSystemProxy {
 			return true
 		}
@@ -339,7 +340,7 @@ func NewRouter(
 
 	if !usePlatformDefaultInterfaceMonitor {
 		networkMonitor, err := tun.NewNetworkUpdateMonitor(router.logger)
-		if !((err != nil && !needInterfaceMonitor) || errors.Is(err, os.ErrInvalid)) {
+		if !((err != nil && !enforceInterfaceMonitor) || errors.Is(err, os.ErrInvalid)) {
 			if err != nil {
 				return nil, err
 			}
@@ -362,7 +363,7 @@ func NewRouter(
 	}
 
 	if ntpOptions.Enabled {
-		ntpDialer, err := dialer.New(router, ntpOptions.DialerOptions)
+		ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions)
 		if err != nil {
 			return nil, E.Cause(err, "create NTP service")
 		}
@@ -380,73 +381,6 @@ func NewRouter(
 	return router, nil
 }
 
-func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outbound, defaultOutbound func() adapter.Outbound) error {
-	inboundByTag := make(map[string]adapter.Inbound)
-	for _, inbound := range inbounds {
-		inboundByTag[inbound.Tag()] = inbound
-	}
-	outboundByTag := make(map[string]adapter.Outbound)
-	for _, detour := range outbounds {
-		outboundByTag[detour.Tag()] = detour
-	}
-	var defaultOutboundForConnection adapter.Outbound
-	var defaultOutboundForPacketConnection adapter.Outbound
-	if r.defaultDetour != "" {
-		detour, loaded := outboundByTag[r.defaultDetour]
-		if !loaded {
-			return E.New("default detour not found: ", r.defaultDetour)
-		}
-		if common.Contains(detour.Network(), N.NetworkTCP) {
-			defaultOutboundForConnection = detour
-		}
-		if common.Contains(detour.Network(), N.NetworkUDP) {
-			defaultOutboundForPacketConnection = detour
-		}
-	}
-	if defaultOutboundForConnection == nil {
-		for _, detour := range outbounds {
-			if common.Contains(detour.Network(), N.NetworkTCP) {
-				defaultOutboundForConnection = detour
-				break
-			}
-		}
-	}
-	if defaultOutboundForPacketConnection == nil {
-		for _, detour := range outbounds {
-			if common.Contains(detour.Network(), N.NetworkUDP) {
-				defaultOutboundForPacketConnection = detour
-				break
-			}
-		}
-	}
-	if defaultOutboundForConnection == nil || defaultOutboundForPacketConnection == nil {
-		detour := defaultOutbound()
-		if defaultOutboundForConnection == nil {
-			defaultOutboundForConnection = detour
-		}
-		if defaultOutboundForPacketConnection == nil {
-			defaultOutboundForPacketConnection = detour
-		}
-		outbounds = append(outbounds, detour)
-		outboundByTag[detour.Tag()] = detour
-	}
-	r.inboundByTag = inboundByTag
-	r.outbounds = outbounds
-	r.defaultOutboundForConnection = defaultOutboundForConnection
-	r.defaultOutboundForPacketConnection = defaultOutboundForPacketConnection
-	r.outboundByTag = outboundByTag
-	for i, rule := range r.rules {
-		routeAction, isRoute := rule.Action().(*R.RuleActionRoute)
-		if !isRoute {
-			continue
-		}
-		if _, loaded := outboundByTag[routeAction.Outbound]; !loaded {
-			return E.New("outbound not found for rule[", i, "]: ", routeAction.Outbound)
-		}
-	}
-	return nil
-}
-
 func (r *Router) Outbounds() []adapter.Outbound {
 	if !r.started {
 		return nil
@@ -454,140 +388,240 @@ func (r *Router) Outbounds() []adapter.Outbound {
 	return r.outbounds
 }
 
-func (r *Router) PreStart() error {
+func (r *Router) Start(stage adapter.StartStage) error {
 	monitor := taskmonitor.New(r.logger, C.StartTimeout)
-	if r.interfaceMonitor != nil {
-		monitor.Start("initialize interface monitor")
-		err := r.interfaceMonitor.Start()
-		monitor.Finish()
-		if err != nil {
-			return err
-		}
-	}
-	if r.networkMonitor != nil {
-		monitor.Start("initialize network monitor")
-		err := r.networkMonitor.Start()
-		monitor.Finish()
-		if err != nil {
-			return err
+	switch stage {
+	case adapter.StartStateInitialize:
+		if r.interfaceMonitor != nil {
+			monitor.Start("initialize interface monitor")
+			err := r.interfaceMonitor.Start()
+			monitor.Finish()
+			if err != nil {
+				return err
+			}
 		}
-	}
-	if r.fakeIPStore != nil {
-		monitor.Start("initialize fakeip store")
-		err := r.fakeIPStore.Start()
-		monitor.Finish()
-		if err != nil {
-			return err
+		if r.networkMonitor != nil {
+			monitor.Start("initialize network monitor")
+			err := r.networkMonitor.Start()
+			monitor.Finish()
+			if err != nil {
+				return err
+			}
 		}
-	}
-	return nil
-}
-
-func (r *Router) Start() error {
-	monitor := taskmonitor.New(r.logger, C.StartTimeout)
-	if r.needGeoIPDatabase {
-		monitor.Start("initialize geoip database")
-		err := r.prepareGeoIPDatabase()
-		monitor.Finish()
-		if err != nil {
-			return err
+		if r.fakeIPStore != nil {
+			monitor.Start("initialize fakeip store")
+			err := r.fakeIPStore.Start()
+			monitor.Finish()
+			if err != nil {
+				return err
+			}
 		}
-	}
-	if r.needGeositeDatabase {
-		monitor.Start("initialize geosite database")
-		err := r.prepareGeositeDatabase()
-		monitor.Finish()
-		if err != nil {
-			return err
+	case adapter.StartStateStart:
+		if r.needGeoIPDatabase {
+			monitor.Start("initialize geoip database")
+			err := r.prepareGeoIPDatabase()
+			monitor.Finish()
+			if err != nil {
+				return err
+			}
 		}
-	}
-	if r.needGeositeDatabase {
-		for _, rule := range r.rules {
-			err := rule.UpdateGeosite()
+		if r.needGeositeDatabase {
+			monitor.Start("initialize geosite database")
+			err := r.prepareGeositeDatabase()
+			monitor.Finish()
 			if err != nil {
-				r.logger.Error("failed to initialize geosite: ", err)
+				return err
 			}
 		}
-		for _, rule := range r.dnsRules {
-			err := rule.UpdateGeosite()
+		if r.needGeositeDatabase {
+			for _, rule := range r.rules {
+				err := rule.UpdateGeosite()
+				if err != nil {
+					r.logger.Error("failed to initialize geosite: ", err)
+				}
+			}
+			for _, rule := range r.dnsRules {
+				err := rule.UpdateGeosite()
+				if err != nil {
+					r.logger.Error("failed to initialize geosite: ", err)
+				}
+			}
+			err := common.Close(r.geositeReader)
 			if err != nil {
-				r.logger.Error("failed to initialize geosite: ", err)
+				return err
 			}
+			r.geositeCache = nil
+			r.geositeReader = nil
 		}
-		err := common.Close(r.geositeReader)
-		if err != nil {
-			return err
+
+		if runtime.GOOS == "windows" {
+			powerListener, err := winpowrprof.NewEventListener(r.notifyWindowsPowerEvent)
+			if err == nil {
+				r.powerListener = powerListener
+			} else {
+				r.logger.Warn("initialize power listener: ", err)
+			}
 		}
-		r.geositeCache = nil
-		r.geositeReader = nil
-	}
 
-	if runtime.GOOS == "windows" {
-		powerListener, err := winpowrprof.NewEventListener(r.notifyWindowsPowerEvent)
-		if err == nil {
-			r.powerListener = powerListener
-		} else {
-			r.logger.Warn("initialize power listener: ", err)
+		if r.powerListener != nil {
+			monitor.Start("start power listener")
+			err := r.powerListener.Start()
+			monitor.Finish()
+			if err != nil {
+				return E.Cause(err, "start power listener")
+			}
 		}
-	}
 
-	if r.powerListener != nil {
-		monitor.Start("start power listener")
-		err := r.powerListener.Start()
+		monitor.Start("initialize DNS client")
+		r.dnsClient.Start()
 		monitor.Finish()
-		if err != nil {
-			return E.Cause(err, "start power listener")
-		}
-	}
 
-	monitor.Start("initialize DNS client")
-	r.dnsClient.Start()
-	monitor.Finish()
+		if C.IsAndroid && r.platformInterface == nil {
+			monitor.Start("initialize package manager")
+			packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{
+				Callback: r,
+				Logger:   r.logger,
+			})
+			monitor.Finish()
+			if err != nil {
+				return E.Cause(err, "create package manager")
+			}
+			if r.enforcePackageManager {
+				monitor.Start("start package manager")
+				err = packageManager.Start()
+				monitor.Finish()
+				if err != nil {
+					return E.Cause(err, "start package manager")
+				}
+			}
+			r.packageManager = packageManager
+		}
 
-	if C.IsAndroid && r.platformInterface == nil {
-		monitor.Start("initialize package manager")
-		packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{
-			Callback: r,
-			Logger:   r.logger,
-		})
-		monitor.Finish()
-		if err != nil {
-			return E.Cause(err, "create package manager")
+		for i, rule := range r.dnsRules {
+			monitor.Start("initialize DNS rule[", i, "]")
+			err := rule.Start()
+			monitor.Finish()
+			if err != nil {
+				return E.Cause(err, "initialize DNS rule[", i, "]")
+			}
 		}
-		if r.needPackageManager {
-			monitor.Start("start package manager")
-			err = packageManager.Start()
+		for i, transport := range r.transports {
+			monitor.Start("initialize DNS transport[", i, "]")
+			err := transport.Start()
 			monitor.Finish()
 			if err != nil {
-				return E.Cause(err, "start package manager")
+				return E.Cause(err, "initialize DNS server[", i, "]")
 			}
 		}
-		r.packageManager = packageManager
-	}
-
-	for i, rule := range r.dnsRules {
-		monitor.Start("initialize DNS rule[", i, "]")
-		err := rule.Start()
-		monitor.Finish()
-		if err != nil {
-			return E.Cause(err, "initialize DNS rule[", i, "]")
+		if r.timeService != nil {
+			monitor.Start("initialize time service")
+			err := r.timeService.Start()
+			monitor.Finish()
+			if err != nil {
+				return E.Cause(err, "initialize time service")
+			}
 		}
-	}
-	for i, transport := range r.transports {
-		monitor.Start("initialize DNS transport[", i, "]")
-		err := transport.Start()
-		monitor.Finish()
-		if err != nil {
-			return E.Cause(err, "initialize DNS server[", i, "]")
+	case adapter.StartStatePostStart:
+		var cacheContext *adapter.HTTPStartContext
+		if len(r.ruleSets) > 0 {
+			monitor.Start("initialize rule-set")
+			cacheContext = adapter.NewHTTPStartContext()
+			var ruleSetStartGroup task.Group
+			for i, ruleSet := range r.ruleSets {
+				ruleSetInPlace := ruleSet
+				ruleSetStartGroup.Append0(func(ctx context.Context) error {
+					err := ruleSetInPlace.StartContext(ctx, cacheContext)
+					if err != nil {
+						return E.Cause(err, "initialize rule-set[", i, "]")
+					}
+					return nil
+				})
+			}
+			ruleSetStartGroup.Concurrency(5)
+			ruleSetStartGroup.FastFail()
+			err := ruleSetStartGroup.Run(r.ctx)
+			monitor.Finish()
+			if err != nil {
+				return err
+			}
 		}
-	}
-	if r.timeService != nil {
-		monitor.Start("initialize time service")
-		err := r.timeService.Start()
-		monitor.Finish()
-		if err != nil {
-			return E.Cause(err, "initialize time service")
+		if cacheContext != nil {
+			cacheContext.Close()
+		}
+		needFindProcess := r.needFindProcess
+		needWIFIState := r.needWIFIState
+		for _, ruleSet := range r.ruleSets {
+			metadata := ruleSet.Metadata()
+			if metadata.ContainsProcessRule {
+				needFindProcess = true
+			}
+			if metadata.ContainsWIFIRule {
+				needWIFIState = true
+			}
+		}
+		if C.IsAndroid && r.platformInterface == nil && !r.enforcePackageManager {
+			if needFindProcess {
+				monitor.Start("start package manager")
+				err := r.packageManager.Start()
+				monitor.Finish()
+				if err != nil {
+					return E.Cause(err, "start package manager")
+				}
+			} else {
+				r.packageManager = nil
+			}
 		}
+		if needFindProcess {
+			if r.platformInterface != nil {
+				r.processSearcher = r.platformInterface
+			} else {
+				monitor.Start("initialize process searcher")
+				searcher, err := process.NewSearcher(process.Config{
+					Logger:         r.logger,
+					PackageManager: r.packageManager,
+				})
+				monitor.Finish()
+				if err != nil {
+					if err != os.ErrInvalid {
+						r.logger.Warn(E.Cause(err, "create process searcher"))
+					}
+				} else {
+					r.processSearcher = searcher
+				}
+			}
+		}
+		if needWIFIState && r.platformInterface != nil {
+			monitor.Start("initialize WIFI state")
+			r.needWIFIState = true
+			r.interfaceMonitor.RegisterCallback(func(_ int) {
+				r.updateWIFIState()
+			})
+			r.updateWIFIState()
+			monitor.Finish()
+		}
+		for i, rule := range r.rules {
+			monitor.Start("initialize rule[", i, "]")
+			err := rule.Start()
+			monitor.Finish()
+			if err != nil {
+				return E.Cause(err, "initialize rule[", i, "]")
+			}
+		}
+		for _, ruleSet := range r.ruleSets {
+			monitor.Start("post start rule_set[", ruleSet.Name(), "]")
+			err := ruleSet.PostStart()
+			monitor.Finish()
+			if err != nil {
+				return E.Cause(err, "post start rule_set[", ruleSet.Name(), "]")
+			}
+		}
+		r.started = true
+		return nil
+	case adapter.StartStateStarted:
+		for _, ruleSet := range r.ruleSetMap {
+			ruleSet.Cleanup()
+		}
+		runtime.GC()
 	}
 	return nil
 }
@@ -668,113 +702,6 @@ func (r *Router) Close() error {
 	return err
 }
 
-func (r *Router) PostStart() error {
-	monitor := taskmonitor.New(r.logger, C.StopTimeout)
-	var cacheContext *adapter.HTTPStartContext
-	if len(r.ruleSets) > 0 {
-		monitor.Start("initialize rule-set")
-		cacheContext = adapter.NewHTTPStartContext()
-		var ruleSetStartGroup task.Group
-		for i, ruleSet := range r.ruleSets {
-			ruleSetInPlace := ruleSet
-			ruleSetStartGroup.Append0(func(ctx context.Context) error {
-				err := ruleSetInPlace.StartContext(ctx, cacheContext)
-				if err != nil {
-					return E.Cause(err, "initialize rule-set[", i, "]")
-				}
-				return nil
-			})
-		}
-		ruleSetStartGroup.Concurrency(5)
-		ruleSetStartGroup.FastFail()
-		err := ruleSetStartGroup.Run(r.ctx)
-		monitor.Finish()
-		if err != nil {
-			return err
-		}
-	}
-	if cacheContext != nil {
-		cacheContext.Close()
-	}
-	needFindProcess := r.needFindProcess
-	needWIFIState := r.needWIFIState
-	for _, ruleSet := range r.ruleSets {
-		metadata := ruleSet.Metadata()
-		if metadata.ContainsProcessRule {
-			needFindProcess = true
-		}
-		if metadata.ContainsWIFIRule {
-			needWIFIState = true
-		}
-	}
-	if C.IsAndroid && r.platformInterface == nil && !r.needPackageManager {
-		if needFindProcess {
-			monitor.Start("start package manager")
-			err := r.packageManager.Start()
-			monitor.Finish()
-			if err != nil {
-				return E.Cause(err, "start package manager")
-			}
-		} else {
-			r.packageManager = nil
-		}
-	}
-	if needFindProcess {
-		if r.platformInterface != nil {
-			r.processSearcher = r.platformInterface
-		} else {
-			monitor.Start("initialize process searcher")
-			searcher, err := process.NewSearcher(process.Config{
-				Logger:         r.logger,
-				PackageManager: r.packageManager,
-			})
-			monitor.Finish()
-			if err != nil {
-				if err != os.ErrInvalid {
-					r.logger.Warn(E.Cause(err, "create process searcher"))
-				}
-			} else {
-				r.processSearcher = searcher
-			}
-		}
-	}
-	if needWIFIState && r.platformInterface != nil {
-		monitor.Start("initialize WIFI state")
-		r.needWIFIState = true
-		r.interfaceMonitor.RegisterCallback(func(_ int) {
-			r.updateWIFIState()
-		})
-		r.updateWIFIState()
-		monitor.Finish()
-	}
-	for i, rule := range r.rules {
-		monitor.Start("initialize rule[", i, "]")
-		err := rule.Start()
-		monitor.Finish()
-		if err != nil {
-			return E.Cause(err, "initialize rule[", i, "]")
-		}
-	}
-	for _, ruleSet := range r.ruleSets {
-		monitor.Start("post start rule_set[", ruleSet.Name(), "]")
-		err := ruleSet.PostStart()
-		monitor.Finish()
-		if err != nil {
-			return E.Cause(err, "post start rule_set[", ruleSet.Name(), "]")
-		}
-	}
-	r.started = true
-	return nil
-}
-
-func (r *Router) Cleanup() error {
-	for _, ruleSet := range r.ruleSetMap {
-		ruleSet.Cleanup()
-	}
-	runtime.GC()
-	return nil
-}
-
 func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
 	outbound, loaded := r.outboundByTag[tag]
 	return outbound, loaded

+ 2 - 2
route/rule/rule_action.go

@@ -22,7 +22,7 @@ import (
 	N "github.com/sagernet/sing/common/network"
 )
 
-func NewRuleAction(router adapter.Router, logger logger.ContextLogger, action option.RuleAction) (adapter.RuleAction, error) {
+func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action option.RuleAction) (adapter.RuleAction, error) {
 	switch action.Action {
 	case "":
 		return nil, nil
@@ -36,7 +36,7 @@ func NewRuleAction(router adapter.Router, logger logger.ContextLogger, action op
 			UDPConnect:                action.RouteOptionsOptions.UDPConnect,
 		}, nil
 	case C.RuleActionTypeDirect:
-		directDialer, err := dialer.New(router, option.DialerOptions(action.DirectOptions))
+		directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions))
 		if err != nil {
 			return nil, err
 		}

+ 2 - 2
route/rule/rule_default.go

@@ -52,7 +52,7 @@ type RuleItem interface {
 }
 
 func NewDefaultRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.DefaultRule) (*DefaultRule, error) {
-	action, err := NewRuleAction(router, logger, options.RuleAction)
+	action, err := NewRuleAction(ctx, logger, options.RuleAction)
 	if err != nil {
 		return nil, E.Cause(err, "action")
 	}
@@ -254,7 +254,7 @@ type LogicalRule struct {
 }
 
 func NewLogicalRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) {
-	action, err := NewRuleAction(router, logger, options.RuleAction)
+	action, err := NewRuleAction(ctx, logger, options.RuleAction)
 	if err != nil {
 		return nil, E.Cause(err, "action")
 	}

+ 28 - 30
route/rule/rule_set_remote.go

@@ -33,23 +33,24 @@ import (
 var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
 
 type RemoteRuleSet struct {
-	ctx            context.Context
-	cancel         context.CancelFunc
-	router         adapter.Router
-	logger         logger.ContextLogger
-	options        option.RuleSet
-	metadata       adapter.RuleSetMetadata
-	updateInterval time.Duration
-	dialer         N.Dialer
-	rules          []adapter.HeadlessRule
-	lastUpdated    time.Time
-	lastEtag       string
-	updateTicker   *time.Ticker
-	cacheFile      adapter.CacheFile
-	pauseManager   pause.Manager
-	callbackAccess sync.Mutex
-	callbacks      list.List[adapter.RuleSetUpdateCallback]
-	refs           atomic.Int32
+	ctx             context.Context
+	cancel          context.CancelFunc
+	router          adapter.Router
+	outboundManager adapter.OutboundManager
+	logger          logger.ContextLogger
+	options         option.RuleSet
+	metadata        adapter.RuleSetMetadata
+	updateInterval  time.Duration
+	dialer          N.Dialer
+	rules           []adapter.HeadlessRule
+	lastUpdated     time.Time
+	lastEtag        string
+	updateTicker    *time.Ticker
+	cacheFile       adapter.CacheFile
+	pauseManager    pause.Manager
+	callbackAccess  sync.Mutex
+	callbacks       list.List[adapter.RuleSetUpdateCallback]
+	refs            atomic.Int32
 }
 
 func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet {
@@ -61,13 +62,14 @@ func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.
 		updateInterval = 24 * time.Hour
 	}
 	return &RemoteRuleSet{
-		ctx:            ctx,
-		cancel:         cancel,
-		router:         router,
-		logger:         logger,
-		options:        options,
-		updateInterval: updateInterval,
-		pauseManager:   service.FromContext[pause.Manager](ctx),
+		ctx:             ctx,
+		cancel:          cancel,
+		router:          router,
+		outboundManager: service.FromContext[adapter.OutboundManager](ctx),
+		logger:          logger,
+		options:         options,
+		updateInterval:  updateInterval,
+		pauseManager:    service.FromContext[pause.Manager](ctx),
 	}
 }
 
@@ -83,17 +85,13 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter.
 	s.cacheFile = service.FromContext[adapter.CacheFile](s.ctx)
 	var dialer N.Dialer
 	if s.options.RemoteOptions.DownloadDetour != "" {
-		outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour)
+		outbound, loaded := s.outboundManager.Outbound(s.options.RemoteOptions.DownloadDetour)
 		if !loaded {
 			return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour)
 		}
 		dialer = outbound
 	} else {
-		outbound, err := s.router.DefaultOutbound(N.NetworkTCP)
-		if err != nil {
-			return err
-		}
-		dialer = outbound
+		dialer = s.outboundManager.Default()
 	}
 	s.dialer = dialer
 	if s.cacheFile != nil {

+ 2 - 1
transport/dhcp/server.go

@@ -23,6 +23,7 @@ import (
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/task"
 	"github.com/sagernet/sing/common/x/list"
+	"github.com/sagernet/sing/service"
 
 	"github.com/insomniacslk/dhcp/dhcpv4"
 	mDNS "github.com/miekg/dns"
@@ -53,7 +54,7 @@ func NewTransport(options dns.TransportOptions) (*Transport, error) {
 	if linkURL.Host == "" {
 		return nil, E.New("missing interface name for DHCP")
 	}
-	router := adapter.RouterFromContext(options.Context)
+	router := service.FromContext[adapter.Router](options.Context)
 	if router == nil {
 		return nil, E.New("missing router in context")
 	}

+ 2 - 1
transport/fakeip/server.go

@@ -9,6 +9,7 @@ import (
 	"github.com/sagernet/sing-dns"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
+	"github.com/sagernet/sing/service"
 
 	mDNS "github.com/miekg/dns"
 )
@@ -32,7 +33,7 @@ type Transport struct {
 }
 
 func NewTransport(options dns.TransportOptions) (*Transport, error) {
-	router := adapter.RouterFromContext(options.Context)
+	router := service.FromContext[adapter.Router](options.Context)
 	if router == nil {
 		return nil, E.New("missing router in context")
 	}