瀏覽代碼

refactor: Modular outbounds

世界 11 月之前
父節點
當前提交
0b2c7ec35c
共有 99 個文件被更改,包括 1003 次插入1021 次删除
  1. 9 0
      adapter/outbound.go
  2. 45 0
      adapter/outbound/adapter.go
  3. 0 47
      adapter/outbound/default.go
  4. 68 0
      adapter/outbound/registry.go
  5. 43 10
      box.go
  6. 3 1
      cmd/sing-box/cmd.go
  7. 2 1
      cmd/sing-box/cmd_format.go
  8. 4 10
      cmd/sing-box/cmd_merge.go
  9. 3 3
      cmd/sing-box/cmd_run.go
  10. 1 1
      common/dialer/default.go
  11. 4 0
      common/dialer/wireguard.go
  12. 0 11
      common/dialer/wireguard_control.go
  13. 0 9
      common/dialer/wiregurad_stub.go
  14. 5 5
      experimental/clashapi/api_meta_group.go
  15. 3 3
      experimental/clashapi/proxies.go
  16. 11 11
      experimental/libbox/command_group.go
  17. 2 2
      experimental/libbox/command_select.go
  18. 2 2
      experimental/libbox/command_urltest.go
  19. 2 0
      experimental/libbox/service.go
  20. 0 1
      experimental/libbox/setup.go
  21. 1 3
      go.mod
  22. 2 10
      go.sum
  23. 59 0
      include/outbound.go
  24. 10 0
      include/quic.go
  25. 14 0
      include/quic_stub.go
  26. 12 0
      include/wireguard.go
  27. 20 0
      include/wireguard_stub.go
  28. 3 2
      option/inbound.go
  29. 0 71
      option/json.go
  30. 8 2
      option/options.go
  31. 24 79
      option/outbound.go
  32. 103 0
      option/outbound_legacy.go
  33. 7 6
      option/rule.go
  34. 7 6
      option/rule_action.go
  35. 7 6
      option/rule_dns.go
  36. 7 6
      option/rule_set.go
  37. 1 1
      option/simple.go
  38. 3 2
      option/tls_acme.go
  39. 3 2
      option/v2ray_transport.go
  40. 0 54
      outbound/block.go
  41. 0 65
      outbound/builder.go
  42. 0 20
      outbound/hysteria_stub.go
  43. 0 14
      outbound/lookback.go
  44. 0 18
      outbound/shadowsocksr.go
  45. 0 16
      outbound/shadowsocksr_stub.go
  46. 0 15
      outbound/tor_embed.go
  47. 0 15
      outbound/tor_embed_mobile.go
  48. 0 9
      outbound/tor_external.go
  49. 0 16
      outbound/tuic_stub.go
  50. 0 16
      outbound/wireguard_stub.go
  51. 42 0
      protocol/block/outbound.go
  52. 1 1
      protocol/direct/loopback_detect.go
  53. 23 25
      protocol/direct/outbound.go
  54. 2 47
      protocol/dns/handle.go
  55. 61 0
      protocol/dns/outbound.go
  56. 21 20
      protocol/group/selector.go
  57. 15 14
      protocol/group/urltest.go
  58. 17 18
      protocol/http/outbound.go
  59. 22 22
      protocol/hysteria/outbound.go
  60. 22 22
      protocol/hysteria2/outbound.go
  61. 22 23
      protocol/shadowsocks/outbound.go
  62. 13 17
      protocol/shadowtls/outbound.go
  63. 27 24
      protocol/socks/outbound.go
  64. 20 22
      protocol/ssh/outbound.go
  65. 20 21
      protocol/tor/outbound.go
  66. 4 3
      protocol/tor/proxy.go
  67. 19 20
      protocol/trojan/outbound.go
  68. 19 23
      protocol/tuic/outbound.go
  69. 20 21
      protocol/vless/outbound.go
  70. 20 21
      protocol/vmess/vmess.go
  71. 10 0
      protocol/wireguard/init.go
  72. 30 32
      protocol/wireguard/outbound.go
  73. 3 3
      route/dns.go
  74. 1 1
      route/route.go
  75. 4 4
      test/brutal_test.go
  76. 1 1
      test/direct_test.go
  77. 1 1
      test/domain_inbound_test.go
  78. 3 3
      test/ech_test.go
  79. 1 1
      test/http_test.go
  80. 2 2
      test/hysteria2_test.go
  81. 2 2
      test/hysteria_test.go
  82. 1 1
      test/inbound_detour_test.go
  83. 2 2
      test/mux_cool_test.go
  84. 2 2
      test/mux_test.go
  85. 1 1
      test/shadowsocks_legacy_test.go
  86. 4 4
      test/shadowsocks_test.go
  87. 3 3
      test/shadowtls_test.go
  88. 1 1
      test/ss_plugin_test.go
  89. 1 1
      test/tfo_test.go
  90. 1 1
      test/tls_test.go
  91. 3 3
      test/trojan_test.go
  92. 2 2
      test/tuic_test.go
  93. 1 1
      test/v2ray_api_test.go
  94. 1 1
      test/v2ray_grpc_test.go
  95. 4 4
      test/v2ray_transport_test.go
  96. 1 1
      test/v2ray_ws_test.go
  97. 2 2
      test/vmess_test.go
  98. 1 1
      test/wireguard_test.go
  99. 6 5
      transport/wireguard/client_bind.go

+ 9 - 0
adapter/outbound.go

@@ -1,6 +1,10 @@
 package adapter
 
 import (
+	"context"
+
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
 	N "github.com/sagernet/sing/common/network"
 )
 
@@ -13,3 +17,8 @@ type Outbound interface {
 	Dependencies() []string
 	N.Dialer
 }
+
+type OutboundRegistry interface {
+	option.OutboundOptionsRegistry
+	CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
+}

+ 45 - 0
adapter/outbound/adapter.go

@@ -0,0 +1,45 @@
+package outbound
+
+import (
+	"github.com/sagernet/sing-box/option"
+)
+
+type Adapter struct {
+	protocol     string
+	network      []string
+	tag          string
+	dependencies []string
+}
+
+func NewAdapter(protocol string, network []string, tag string, dependencies []string) Adapter {
+	return Adapter{
+		protocol:     protocol,
+		network:      network,
+		tag:          tag,
+		dependencies: dependencies,
+	}
+}
+
+func NewAdapterWithDialerOptions(protocol string, network []string, tag string, dialOptions option.DialerOptions) Adapter {
+	var dependencies []string
+	if dialOptions.Detour != "" {
+		dependencies = []string{dialOptions.Detour}
+	}
+	return NewAdapter(protocol, network, tag, dependencies)
+}
+
+func (a *Adapter) Type() string {
+	return a.protocol
+}
+
+func (a *Adapter) Tag() string {
+	return a.tag
+}
+
+func (a *Adapter) Network() []string {
+	return a.network
+}
+
+func (a *Adapter) Dependencies() []string {
+	return a.dependencies
+}

+ 0 - 47
outbound/default.go → adapter/outbound/default.go

@@ -9,8 +9,6 @@ import (
 
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/buf"
@@ -21,42 +19,6 @@ import (
 	N "github.com/sagernet/sing/common/network"
 )
 
-type myOutboundAdapter struct {
-	protocol     string
-	network      []string
-	router       adapter.Router
-	logger       log.ContextLogger
-	tag          string
-	dependencies []string
-}
-
-func (a *myOutboundAdapter) Type() string {
-	return a.protocol
-}
-
-func (a *myOutboundAdapter) Tag() string {
-	return a.tag
-}
-
-func (a *myOutboundAdapter) Network() []string {
-	return a.network
-}
-
-func (a *myOutboundAdapter) Dependencies() []string {
-	return a.dependencies
-}
-
-func (a *myOutboundAdapter) NewError(ctx context.Context, err error) {
-	NewError(a.logger, ctx, err)
-}
-
-func withDialerDependency(options option.DialerOptions) []string {
-	if options.Detour != "" {
-		return []string{options.Detour}
-	}
-	return nil
-}
-
 func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext) error {
 	ctx = adapter.WithContext(ctx, &metadata)
 	var outConn net.Conn
@@ -233,12 +195,3 @@ func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) erro
 	}
 	return bufio.CopyConn(ctx, conn, serverConn)
 }
-
-func NewError(logger log.ContextLogger, ctx context.Context, err error) {
-	common.Close(err)
-	if E.IsClosedOrCanceled(err) {
-		logger.DebugContext(ctx, "connection closed: ", err)
-		return
-	}
-	logger.ErrorContext(ctx, err)
-}

+ 68 - 0
adapter/outbound/registry.go

@@ -0,0 +1,68 @@
+package outbound
+
+import (
+	"context"
+	"sync"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Outbound, error)
+
+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)))
+	})
+}
+
+var _ adapter.OutboundRegistry = (*Registry)(nil)
+
+type (
+	optionsConstructorFunc func() any
+	constructorFunc        func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error)
+)
+
+type Registry struct {
+	access       sync.Mutex
+	optiosnType  map[string]optionsConstructorFunc
+	constructors map[string]constructorFunc
+}
+
+func NewRegistry() *Registry {
+	return &Registry{
+		optiosnType:  make(map[string]optionsConstructorFunc),
+		constructors: make(map[string]constructorFunc),
+	}
+}
+
+func (r *Registry) CreateOptions(outboundType string) (any, bool) {
+	r.access.Lock()
+	defer r.access.Unlock()
+	optionsConstructor, loaded := r.optiosnType[outboundType]
+	if !loaded {
+		return nil, false
+	}
+	return optionsConstructor(), true
+}
+
+func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
+	r.access.Lock()
+	defer r.access.Unlock()
+	constructor, loaded := r.constructors[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.optiosnType[outboundType] = optionsConstructor
+	r.constructors[outboundType] = constructor
+}

+ 43 - 10
box.go

@@ -17,7 +17,7 @@ import (
 	"github.com/sagernet/sing-box/inbound"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-box/outbound"
+	"github.com/sagernet/sing-box/protocol/direct"
 	"github.com/sagernet/sing-box/route"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -48,12 +48,26 @@ type Options struct {
 	PlatformLogWriter log.PlatformWriter
 }
 
+func Context(ctx context.Context, registry adapter.OutboundRegistry) context.Context {
+	if service.FromContext[option.OutboundOptionsRegistry](ctx) != nil &&
+		service.FromContext[adapter.OutboundRegistry](ctx) != nil {
+		return ctx
+	}
+	ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, registry)
+	ctx = service.ContextWith[adapter.OutboundRegistry](ctx, registry)
+	return ctx
+}
+
 func New(options Options) (*Box, error) {
 	createdAt := time.Now()
 	ctx := options.Context
 	if ctx == nil {
 		ctx = context.Background()
 	}
+	outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
+	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)
@@ -98,6 +112,16 @@ func New(options Options) (*Box, error) {
 		return nil, E.Cause(err, "parse route options")
 	}
 	inbounds := make([]adapter.Inbound, 0, len(options.Inbounds))
+	//nolint:staticcheck
+	if len(options.LegacyOutbounds) > 0 {
+		for _, legacyOutbound := range options.LegacyOutbounds {
+			options.Outbounds = append(options.Outbounds, option.Outbound{
+				Type:    legacyOutbound.Type,
+				Tag:     legacyOutbound.Tag,
+				Options: common.Must1(legacyOutbound.RawOptions()),
+			})
+		}
+	}
 	outbounds := make([]adapter.Outbound, 0, len(options.Outbounds))
 	for i, inboundOptions := range options.Inbounds {
 		var in adapter.Inbound
@@ -121,29 +145,38 @@ func New(options Options) (*Box, error) {
 		inbounds = append(inbounds, in)
 	}
 	for i, outboundOptions := range options.Outbounds {
-		var out adapter.Outbound
+		var currentOutbound adapter.Outbound
 		var tag string
 		if outboundOptions.Tag != "" {
 			tag = outboundOptions.Tag
 		} else {
 			tag = F.ToString(i)
 		}
-		out, err = outbound.New(
-			ctx,
+		outboundCtx := ctx
+		if tag != "" {
+			// TODO: remove this
+			outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{
+				Outbound: tag,
+			})
+		}
+		currentOutbound, err = outboundRegistry.CreateOutbound(
+			outboundCtx,
 			router,
 			logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
 			tag,
-			outboundOptions)
+			outboundOptions.Type,
+			outboundOptions.Options,
+		)
 		if err != nil {
 			return nil, E.Cause(err, "parse outbound[", i, "]")
 		}
-		outbounds = append(outbounds, out)
+		outbounds = append(outbounds, currentOutbound)
 	}
 	err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
-		out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.Outbound{Type: "direct", Tag: "default"})
-		common.Must(oErr)
-		outbounds = append(outbounds, out)
-		return out
+		defaultOutbound, cErr := direct.NewOutbound(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.DirectOutboundOptions{})
+		common.Must(cErr)
+		outbounds = append(outbounds, defaultOutbound)
+		return defaultOutbound
 	})
 	if err != nil {
 		return nil, err

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

@@ -7,8 +7,9 @@ import (
 	"strconv"
 	"time"
 
+	"github.com/sagernet/sing-box"
 	"github.com/sagernet/sing-box/experimental/deprecated"
-	_ "github.com/sagernet/sing-box/include"
+	"github.com/sagernet/sing-box/include"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing/service"
 	"github.com/sagernet/sing/service/filemanager"
@@ -68,4 +69,5 @@ func preRun(cmd *cobra.Command, args []string) {
 		configPaths = append(configPaths, "config.json")
 	}
 	globalCtx = service.ContextWith(globalCtx, deprecated.NewEnvManager(log.StdLogger()))
+	globalCtx = box.Context(globalCtx, include.OutboundRegistry())
 }

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

@@ -2,6 +2,7 @@ package main
 
 import (
 	"bytes"
+	"context"
 	"os"
 	"path/filepath"
 
@@ -38,7 +39,7 @@ func format() error {
 		return err
 	}
 	for _, optionsEntry := range optionsList {
-		optionsEntry.options, err = badjson.Omitempty(optionsEntry.options)
+		optionsEntry.options, err = badjson.Omitempty(context.TODO(), optionsEntry.options)
 		if err != nil {
 			return err
 		}

+ 4 - 10
cmd/sing-box/cmd_merge.go

@@ -78,19 +78,14 @@ func mergePathResources(options *option.Options) error {
 		}
 		options.Inbounds[index] = inbound
 	}
-	for index, outbound := range options.Outbounds {
-		rawOptions, err := outbound.RawOptions()
-		if err != nil {
-			return err
-		}
+	for _, outbound := range options.Outbounds {
 		switch outbound.Type {
 		case C.TypeSSH:
-			outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions)
+			mergeSSHOutboundOptions(outbound.Options.(*option.SSHOutboundOptions))
 		}
-		if tlsOptions, containsTLSOptions := rawOptions.(option.OutboundTLSOptionsWrapper); containsTLSOptions {
+		if tlsOptions, containsTLSOptions := outbound.Options.(option.OutboundTLSOptionsWrapper); containsTLSOptions {
 			tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions()))
 		}
-		options.Outbounds[index] = outbound
 	}
 	return nil
 }
@@ -138,13 +133,12 @@ func mergeTLSOutboundOptions(options *option.OutboundTLSOptions) *option.Outboun
 	return options
 }
 
-func mergeSSHOutboundOptions(options option.SSHOutboundOptions) option.SSHOutboundOptions {
+func mergeSSHOutboundOptions(options *option.SSHOutboundOptions) {
 	if options.PrivateKeyPath != "" {
 		if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil {
 			options.PrivateKey = trimStringArray(strings.Split(string(content), "\n"))
 		}
 	}
-	return options
 }
 
 func trimStringArray(array []string) []string {

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

@@ -57,7 +57,7 @@ func readConfigAt(path string) (*OptionsEntry, error) {
 	if err != nil {
 		return nil, E.Cause(err, "read config at ", path)
 	}
-	options, err := json.UnmarshalExtended[option.Options](configContent)
+	options, err := json.UnmarshalExtendedContext[option.Options](globalCtx, configContent)
 	if err != nil {
 		return nil, E.Cause(err, "decode config at ", path)
 	}
@@ -109,13 +109,13 @@ func readConfigAndMerge() (option.Options, error) {
 	}
 	var mergedMessage json.RawMessage
 	for _, options := range optionsList {
-		mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage, false)
+		mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
 		if err != nil {
 			return option.Options{}, E.Cause(err, "merge config at ", options.path)
 		}
 	}
 	var mergedOptions option.Options
-	err = mergedOptions.UnmarshalJSON(mergedMessage)
+	err = mergedOptions.UnmarshalJSONContext(globalCtx, mergedMessage)
 	if err != nil {
 		return option.Options{}, E.Cause(err, "unmarshal merged config")
 	}

+ 1 - 1
common/dialer/default.go

@@ -125,7 +125,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
 		setMultiPathTCP(&dialer4)
 	}
 	if options.IsWireGuardListener {
-		for _, controlFn := range wgControlFns {
+		for _, controlFn := range WgControlFns {
 			listener.Control = control.Append(listener.Control, controlFn)
 		}
 	}

+ 4 - 0
common/dialer/wireguard.go

@@ -2,8 +2,12 @@ package dialer
 
 import (
 	"net"
+
+	"github.com/sagernet/sing/common/control"
 )
 
 type WireGuardListener interface {
 	ListenPacketCompat(network, address string) (net.PacketConn, error)
 }
+
+var WgControlFns []control.Func

+ 0 - 11
common/dialer/wireguard_control.go

@@ -1,11 +0,0 @@
-//go:build with_wireguard
-
-package dialer
-
-import (
-	"github.com/sagernet/wireguard-go/conn"
-)
-
-var _ WireGuardListener = (conn.Listener)(nil)
-
-var wgControlFns = conn.ControlFns

+ 0 - 9
common/dialer/wiregurad_stub.go

@@ -1,9 +0,0 @@
-//go:build !with_wireguard
-
-package dialer
-
-import (
-	"github.com/sagernet/sing/common/control"
-)
-
-var wgControlFns []control.Func

+ 5 - 5
experimental/clashapi/api_meta_group.go

@@ -10,7 +10,7 @@ import (
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/urltest"
-	"github.com/sagernet/sing-box/outbound"
+	"github.com/sagernet/sing-box/protocol/group"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/batch"
 	"github.com/sagernet/sing/common/json/badjson"
@@ -59,7 +59,7 @@ func getGroup(server *Server) func(w http.ResponseWriter, r *http.Request) {
 func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
 		proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
-		group, ok := proxy.(adapter.OutboundGroup)
+		outboundGroup, ok := proxy.(adapter.OutboundGroup)
 		if !ok {
 			render.Status(r, http.StatusNotFound)
 			render.JSON(w, r, ErrNotFound)
@@ -82,10 +82,10 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
 		defer cancel()
 
 		var result map[string]uint16
-		if urlTestGroup, isURLTestGroup := group.(adapter.URLTestGroup); isURLTestGroup {
+		if urlTestGroup, isURLTestGroup := outboundGroup.(adapter.URLTestGroup); isURLTestGroup {
 			result, err = urlTestGroup.URLTest(ctx)
 		} else {
-			outbounds := common.FilterNotNil(common.Map(group.All(), func(it string) adapter.Outbound {
+			outbounds := common.FilterNotNil(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
 				itOutbound, _ := server.router.Outbound(it)
 				return itOutbound
 			}))
@@ -95,7 +95,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
 			var resultAccess sync.Mutex
 			for _, detour := range outbounds {
 				tag := detour.Tag()
-				realTag := outbound.RealTag(detour)
+				realTag := group.RealTag(detour)
 				if checked[realTag] {
 					continue
 				}

+ 3 - 3
experimental/clashapi/proxies.go

@@ -11,7 +11,7 @@ import (
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/urltest"
 	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/outbound"
+	"github.com/sagernet/sing-box/protocol/group"
 	"github.com/sagernet/sing/common"
 	F "github.com/sagernet/sing/common/format"
 	"github.com/sagernet/sing/common/json/badjson"
@@ -168,7 +168,7 @@ func updateProxy(w http.ResponseWriter, r *http.Request) {
 	}
 
 	proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
-	selector, ok := proxy.(*outbound.Selector)
+	selector, ok := proxy.(*group.Selector)
 	if !ok {
 		render.Status(r, http.StatusBadRequest)
 		render.JSON(w, r, newError("Must be a Selector"))
@@ -204,7 +204,7 @@ func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
 
 		delay, err := urltest.URLTest(ctx, url, proxy)
 		defer func() {
-			realTag := outbound.RealTag(proxy)
+			realTag := group.RealTag(proxy)
 			if err != nil {
 				server.urlTestHistory.DeleteURLTestHistory(realTag)
 			} else {

+ 11 - 11
experimental/libbox/command_group.go

@@ -9,7 +9,7 @@ import (
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/urltest"
-	"github.com/sagernet/sing-box/outbound"
+	"github.com/sagernet/sing-box/protocol/group"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/varbin"
 	"github.com/sagernet/sing/service"
@@ -118,14 +118,14 @@ func writeGroups(writer io.Writer, boxService *BoxService) error {
 	}
 	var groups []OutboundGroup
 	for _, iGroup := range iGroups {
-		var group OutboundGroup
-		group.Tag = iGroup.Tag()
-		group.Type = iGroup.Type()
-		_, group.Selectable = iGroup.(*outbound.Selector)
-		group.Selected = iGroup.Now()
+		var outboundGroup OutboundGroup
+		outboundGroup.Tag = iGroup.Tag()
+		outboundGroup.Type = iGroup.Type()
+		_, outboundGroup.Selectable = iGroup.(*group.Selector)
+		outboundGroup.Selected = iGroup.Now()
 		if cacheFile != nil {
-			if isExpand, loaded := cacheFile.LoadGroupExpand(group.Tag); loaded {
-				group.IsExpand = isExpand
+			if isExpand, loaded := cacheFile.LoadGroupExpand(outboundGroup.Tag); loaded {
+				outboundGroup.IsExpand = isExpand
 			}
 		}
 
@@ -142,12 +142,12 @@ func writeGroups(writer io.Writer, boxService *BoxService) error {
 				item.URLTestTime = history.Time.Unix()
 				item.URLTestDelay = int32(history.Delay)
 			}
-			group.ItemList = append(group.ItemList, &item)
+			outboundGroup.ItemList = append(outboundGroup.ItemList, &item)
 		}
-		if len(group.ItemList) < 2 {
+		if len(outboundGroup.ItemList) < 2 {
 			continue
 		}
-		groups = append(groups, group)
+		groups = append(groups, outboundGroup)
 	}
 	return varbin.Write(writer, binary.BigEndian, groups)
 }

+ 2 - 2
experimental/libbox/command_select.go

@@ -4,7 +4,7 @@ import (
 	"encoding/binary"
 	"net"
 
-	"github.com/sagernet/sing-box/outbound"
+	"github.com/sagernet/sing-box/protocol/group"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/varbin"
 )
@@ -47,7 +47,7 @@ func (s *CommandServer) handleSelectOutbound(conn net.Conn) error {
 	if !isLoaded {
 		return writeError(conn, E.New("selector not found: ", groupTag))
 	}
-	selector, isSelector := outboundGroup.(*outbound.Selector)
+	selector, isSelector := outboundGroup.(*group.Selector)
 	if !isSelector {
 		return writeError(conn, E.New("outbound is not a selector: ", groupTag))
 	}

+ 2 - 2
experimental/libbox/command_urltest.go

@@ -7,7 +7,7 @@ import (
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/urltest"
-	"github.com/sagernet/sing-box/outbound"
+	"github.com/sagernet/sing-box/protocol/group"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/batch"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -49,7 +49,7 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error {
 	if !isOutboundGroup {
 		return writeError(conn, E.New("outbound is not a group: ", groupTag))
 	}
-	urlTest, isURLTest := abstractOutboundGroup.(*outbound.URLTest)
+	urlTest, isURLTest := abstractOutboundGroup.(*group.URLTest)
 	if isURLTest {
 		go urlTest.CheckOutbounds()
 	} else {

+ 2 - 0
experimental/libbox/service.go

@@ -17,6 +17,7 @@ import (
 	"github.com/sagernet/sing-box/experimental/deprecated"
 	"github.com/sagernet/sing-box/experimental/libbox/internal/procfs"
 	"github.com/sagernet/sing-box/experimental/libbox/platform"
+	"github.com/sagernet/sing-box/include"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-tun"
@@ -47,6 +48,7 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
 	}
 	runtimeDebug.FreeOSMemory()
 	ctx, cancel := context.WithCancel(context.Background())
+	ctx = box.Context(ctx, include.OutboundRegistry())
 	ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
 	urlTestHistoryStorage := urltest.NewHistoryStorage()
 	ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)

+ 0 - 1
experimental/libbox/setup.go

@@ -9,7 +9,6 @@ import (
 
 	"github.com/sagernet/sing-box/common/humanize"
 	C "github.com/sagernet/sing-box/constant"
-	_ "github.com/sagernet/sing-box/include"
 	"github.com/sagernet/sing-box/log"
 )
 

+ 1 - 3
go.mod

@@ -3,7 +3,6 @@ module github.com/sagernet/sing-box
 go 1.20
 
 require (
-	berty.tech/go-libtor v1.0.385
 	github.com/caddyserver/certmagic v0.20.0
 	github.com/cloudflare/circl v1.3.7
 	github.com/cretz/bine v0.2.0
@@ -17,7 +16,6 @@ require (
 	github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa
 	github.com/mholt/acmez v1.2.0
 	github.com/miekg/dns v1.1.62
-	github.com/ooni/go-libtor v1.1.8
 	github.com/oschwald/maxminddb-golang v1.12.0
 	github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
 	github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1
@@ -27,7 +25,7 @@ require (
 	github.com/sagernet/gvisor v0.0.0-20241021032506-a4324256e4a3
 	github.com/sagernet/quic-go v0.48.0-beta.1
 	github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
-	github.com/sagernet/sing v0.5.0-rc.4.0.20241023053048-94f058276959
+	github.com/sagernet/sing v0.5.0-rc.4.0.20241101160402-8452992a6369
 	github.com/sagernet/sing-dns v0.3.0-rc.2.0.20241023053951-feb6d5403f2a
 	github.com/sagernet/sing-mux v0.2.1-0.20241020175909-fe6153f7a9ec
 	github.com/sagernet/sing-quic v0.3.0-rc.1

+ 2 - 10
go.sum

@@ -1,5 +1,3 @@
-berty.tech/go-libtor v1.0.385 h1:RWK94C3hZj6Z2GdvePpHJLnWYobFr3bY/OdUJ5aoEXw=
-berty.tech/go-libtor v1.0.385/go.mod h1:9swOOQVb+kmvuAlsgWUK/4c52pm69AdbJsxLzk+fJEw=
 github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
 github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
 github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
@@ -9,7 +7,6 @@ github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NY
 github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
 github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
 github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
 github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -81,8 +78,6 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
 github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss=
 github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0=
 github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
-github.com/ooni/go-libtor v1.1.8 h1:Wo3V3DVTxl5vZdxtQakqYP+DAHx7pPtAFSl1bnAa08w=
-github.com/ooni/go-libtor v1.1.8/go.mod h1:q1YyLwRD9GeMyeerVvwc0vJ2YgwDLTp2bdVcrh/JXyI=
 github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
 github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
 github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
@@ -115,8 +110,8 @@ github.com/sagernet/quic-go v0.48.0-beta.1/go.mod h1:1WgdDIVD1Gybp40JTWketeSfKA/
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
 github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
-github.com/sagernet/sing v0.5.0-rc.4.0.20241023053048-94f058276959 h1:8BzTt5cU8h6HK4CcRq1UQHKsgUi942GjO0by/ntFZIs=
-github.com/sagernet/sing v0.5.0-rc.4.0.20241023053048-94f058276959/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
+github.com/sagernet/sing v0.5.0-rc.4.0.20241101160402-8452992a6369 h1:gfiUYWslwKM7OtvG37PV0iIDCWcacJSEUS2h29rpYac=
+github.com/sagernet/sing v0.5.0-rc.4.0.20241101160402-8452992a6369/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
 github.com/sagernet/sing-dns v0.3.0-rc.2.0.20241023053951-feb6d5403f2a h1:jpAlbmZxc1LymZrmJacsvHI57Wito5xy8qASZJMWoOQ=
 github.com/sagernet/sing-dns v0.3.0-rc.2.0.20241023053951-feb6d5403f2a/go.mod h1:TqLIelI+FAbVEdiTRolhGLOwvhVjY7oT+wezlOJUQ7M=
 github.com/sagernet/sing-mux v0.2.1-0.20241020175909-fe6153f7a9ec h1:6Fd/VsEsw9qIjaGi1IBTZSb4b4v5JYtNcoiBtGsQC48=
@@ -146,7 +141,6 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
@@ -168,7 +162,6 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
-golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
 golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
@@ -182,7 +175,6 @@ golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
 golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 59 - 0
include/outbound.go

@@ -0,0 +1,59 @@
+package include
+
+import (
+	"context"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/protocol/block"
+	"github.com/sagernet/sing-box/protocol/direct"
+	"github.com/sagernet/sing-box/protocol/dns"
+	"github.com/sagernet/sing-box/protocol/group"
+	"github.com/sagernet/sing-box/protocol/http"
+	"github.com/sagernet/sing-box/protocol/shadowsocks"
+	"github.com/sagernet/sing-box/protocol/shadowtls"
+	"github.com/sagernet/sing-box/protocol/socks"
+	"github.com/sagernet/sing-box/protocol/ssh"
+	"github.com/sagernet/sing-box/protocol/tor"
+	"github.com/sagernet/sing-box/protocol/trojan"
+	"github.com/sagernet/sing-box/protocol/vless"
+	"github.com/sagernet/sing-box/protocol/vmess"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+func OutboundRegistry() *outbound.Registry {
+	registry := outbound.NewRegistry()
+
+	direct.RegisterOutbound(registry)
+
+	block.RegisterOutbound(registry)
+	dns.RegisterOutbound(registry)
+
+	group.RegisterSelector(registry)
+	group.RegisterURLTest(registry)
+
+	socks.RegisterOutbound(registry)
+	http.RegisterOutbound(registry)
+	shadowsocks.RegisterOutbound(registry)
+	vmess.RegisterOutbound(registry)
+	trojan.RegisterOutbound(registry)
+	tor.RegisterOutbound(registry)
+	ssh.RegisterOutbound(registry)
+	shadowtls.RegisterOutbound(registry)
+	vless.RegisterOutbound(registry)
+
+	registerQUICOutbounds(registry)
+	registerWireGuardOutbound(registry)
+	registerStubForRemovedOutbounds(registry)
+
+	return registry
+}
+
+func registerStubForRemovedOutbounds(registry *outbound.Registry) {
+	outbound.Register[option.ShadowsocksROutboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksROutboundOptions) (adapter.Outbound, error) {
+		return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0")
+	})
+}

+ 10 - 0
include/quic.go

@@ -3,6 +3,16 @@
 package include
 
 import (
+	"github.com/sagernet/sing-box/adapter/outbound"
+	"github.com/sagernet/sing-box/protocol/hysteria"
+	"github.com/sagernet/sing-box/protocol/hysteria2"
+	"github.com/sagernet/sing-box/protocol/tuic"
 	_ "github.com/sagernet/sing-box/transport/v2rayquic"
 	_ "github.com/sagernet/sing-dns/quic"
 )
+
+func registerQUICOutbounds(registry *outbound.Registry) {
+	hysteria.RegisterOutbound(registry)
+	tuic.RegisterOutbound(registry)
+	hysteria2.RegisterOutbound(registry)
+}

+ 14 - 0
include/quic_stub.go

@@ -6,8 +6,10 @@ import (
 	"context"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/transport/v2ray"
 	"github.com/sagernet/sing-dns"
@@ -29,3 +31,15 @@ func init() {
 		},
 	)
 }
+
+func registerQUICOutbounds(registry *outbound.Registry) {
+	outbound.Register[option.HysteriaOutboundOptions](registry, C.TypeHysteria, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) {
+		return nil, C.ErrQUICNotIncluded
+	})
+	outbound.Register[option.TUICOutboundOptions](registry, C.TypeTUIC, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (adapter.Outbound, error) {
+		return nil, C.ErrQUICNotIncluded
+	})
+	outbound.Register[option.Hysteria2OutboundOptions](registry, C.TypeHysteria2, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (adapter.Outbound, error) {
+		return nil, C.ErrQUICNotIncluded
+	})
+}

+ 12 - 0
include/wireguard.go

@@ -0,0 +1,12 @@
+//go:build with_wireguard
+
+package include
+
+import (
+	"github.com/sagernet/sing-box/adapter/outbound"
+	"github.com/sagernet/sing-box/protocol/wireguard"
+)
+
+func registerWireGuardOutbound(registry *outbound.Registry) {
+	wireguard.RegisterOutbound(registry)
+}

+ 20 - 0
include/wireguard_stub.go

@@ -0,0 +1,20 @@
+//go:build !with_wireguard
+
+package include
+
+import (
+	"context"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+func registerWireGuardOutbound(registry *outbound.Registry) {
+	outbound.Register[option.WireGuardOutboundOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardOutboundOptions) (adapter.Outbound, error) {
+		return nil, E.New(`WireGuard is not included in this build, rebuild with -tags with_wireguard`)
+	})
+}

+ 3 - 2
option/inbound.go

@@ -6,6 +6,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badjson"
 )
 
 type _Inbound struct {
@@ -79,7 +80,7 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
 	if err != nil {
 		return nil, err
 	}
-	return MarshallObjects((_Inbound)(h), rawOptions)
+	return badjson.MarshallObjects((_Inbound)(h), rawOptions)
 }
 
 func (h *Inbound) UnmarshalJSON(bytes []byte) error {
@@ -91,7 +92,7 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
 	if err != nil {
 		return err
 	}
-	err = UnmarshallExcluded(bytes, (*_Inbound)(h), rawOptions)
+	err = badjson.UnmarshallExcluded(bytes, (*_Inbound)(h), rawOptions)
 	if err != nil {
 		return err
 	}

+ 0 - 71
option/json.go

@@ -1,71 +0,0 @@
-package option
-
-import (
-	"github.com/sagernet/sing/common"
-	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/json"
-	"github.com/sagernet/sing/common/json/badjson"
-)
-
-func ToMap(v any) (*badjson.JSONObject, error) {
-	inputContent, err := json.Marshal(v)
-	if err != nil {
-		return nil, err
-	}
-	var content badjson.JSONObject
-	err = content.UnmarshalJSON(inputContent)
-	if err != nil {
-		return nil, err
-	}
-	return &content, nil
-}
-
-func MergeObjects(objects ...any) (*badjson.JSONObject, error) {
-	var content badjson.JSONObject
-	for _, object := range objects {
-		objectMap, err := ToMap(object)
-		if err != nil {
-			return nil, err
-		}
-		content.PutAll(objectMap)
-	}
-	return &content, nil
-}
-
-func MarshallObjects(objects ...any) ([]byte, error) {
-	objects = common.FilterNotNil(objects)
-	if len(objects) == 1 {
-		return json.Marshal(objects[0])
-	}
-	content, err := MergeObjects(objects...)
-	if err != nil {
-		return nil, err
-	}
-	return content.MarshalJSON()
-}
-
-func UnmarshallExcluded(inputContent []byte, parentObject any, object any) error {
-	parentContent, err := ToMap(parentObject)
-	if err != nil {
-		return err
-	}
-	var content badjson.JSONObject
-	err = content.UnmarshalJSON(inputContent)
-	if err != nil {
-		return err
-	}
-	for _, key := range parentContent.Keys() {
-		content.Remove(key)
-	}
-	if object == nil {
-		if content.IsEmpty() {
-			return nil
-		}
-		return E.New("unexpected key: ", content.Keys()[0])
-	}
-	inputContent, err = content.MarshalJSON()
-	if err != nil {
-		return err
-	}
-	return json.UnmarshalDisallowUnknownFields(inputContent, object)
-}

+ 8 - 2
option/config.go → option/options.go

@@ -2,6 +2,7 @@ package option
 
 import (
 	"bytes"
+	"context"
 
 	"github.com/sagernet/sing/common/json"
 )
@@ -16,12 +17,15 @@ type _Options struct {
 	Outbounds    []Outbound           `json:"outbounds,omitempty"`
 	Route        *RouteOptions        `json:"route,omitempty"`
 	Experimental *ExperimentalOptions `json:"experimental,omitempty"`
+
+	// Deprecated: use Outbounds instead
+	LegacyOutbounds []LegacyOutbound `json:"_"`
 }
 
 type Options _Options
 
-func (o *Options) UnmarshalJSON(content []byte) error {
-	decoder := json.NewDecoder(bytes.NewReader(content))
+func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) error {
+	decoder := json.NewDecoderContext(ctx, bytes.NewReader(content))
 	decoder.DisallowUnknownFields()
 	err := decoder.Decode((*_Options)(o))
 	if err != nil {
@@ -38,3 +42,5 @@ type LogOptions struct {
 	Timestamp    bool   `json:"timestamp,omitempty"`
 	DisableColor bool   `json:"-"`
 }
+
+type StubOptions struct{}

+ 24 - 79
option/outbound.go

@@ -1,104 +1,49 @@
 package option
 
 import (
-	C "github.com/sagernet/sing-box/constant"
+	"context"
+
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badjson"
 	M "github.com/sagernet/sing/common/metadata"
+	"github.com/sagernet/sing/service"
 )
 
+type OutboundOptionsRegistry interface {
+	CreateOptions(outboundType string) (any, bool)
+}
+
 type _Outbound struct {
-	Type                string                      `json:"type"`
-	Tag                 string                      `json:"tag,omitempty"`
-	DirectOptions       DirectOutboundOptions       `json:"-"`
-	SocksOptions        SocksOutboundOptions        `json:"-"`
-	HTTPOptions         HTTPOutboundOptions         `json:"-"`
-	ShadowsocksOptions  ShadowsocksOutboundOptions  `json:"-"`
-	VMessOptions        VMessOutboundOptions        `json:"-"`
-	TrojanOptions       TrojanOutboundOptions       `json:"-"`
-	WireGuardOptions    WireGuardOutboundOptions    `json:"-"`
-	HysteriaOptions     HysteriaOutboundOptions     `json:"-"`
-	TorOptions          TorOutboundOptions          `json:"-"`
-	SSHOptions          SSHOutboundOptions          `json:"-"`
-	ShadowTLSOptions    ShadowTLSOutboundOptions    `json:"-"`
-	ShadowsocksROptions ShadowsocksROutboundOptions `json:"-"`
-	VLESSOptions        VLESSOutboundOptions        `json:"-"`
-	TUICOptions         TUICOutboundOptions         `json:"-"`
-	Hysteria2Options    Hysteria2OutboundOptions    `json:"-"`
-	SelectorOptions     SelectorOutboundOptions     `json:"-"`
-	URLTestOptions      URLTestOutboundOptions      `json:"-"`
+	Type    string `json:"type"`
+	Tag     string `json:"tag,omitempty"`
+	Options any    `json:"-"`
 }
 
 type Outbound _Outbound
 
-func (h *Outbound) RawOptions() (any, error) {
-	var rawOptionsPtr any
-	switch h.Type {
-	case C.TypeDirect:
-		rawOptionsPtr = &h.DirectOptions
-	case C.TypeBlock, C.TypeDNS:
-		rawOptionsPtr = nil
-	case C.TypeSOCKS:
-		rawOptionsPtr = &h.SocksOptions
-	case C.TypeHTTP:
-		rawOptionsPtr = &h.HTTPOptions
-	case C.TypeShadowsocks:
-		rawOptionsPtr = &h.ShadowsocksOptions
-	case C.TypeVMess:
-		rawOptionsPtr = &h.VMessOptions
-	case C.TypeTrojan:
-		rawOptionsPtr = &h.TrojanOptions
-	case C.TypeWireGuard:
-		rawOptionsPtr = &h.WireGuardOptions
-	case C.TypeHysteria:
-		rawOptionsPtr = &h.HysteriaOptions
-	case C.TypeTor:
-		rawOptionsPtr = &h.TorOptions
-	case C.TypeSSH:
-		rawOptionsPtr = &h.SSHOptions
-	case C.TypeShadowTLS:
-		rawOptionsPtr = &h.ShadowTLSOptions
-	case C.TypeShadowsocksR:
-		rawOptionsPtr = &h.ShadowsocksROptions
-	case C.TypeVLESS:
-		rawOptionsPtr = &h.VLESSOptions
-	case C.TypeTUIC:
-		rawOptionsPtr = &h.TUICOptions
-	case C.TypeHysteria2:
-		rawOptionsPtr = &h.Hysteria2Options
-	case C.TypeSelector:
-		rawOptionsPtr = &h.SelectorOptions
-	case C.TypeURLTest:
-		rawOptionsPtr = &h.URLTestOptions
-	case "":
-		return nil, E.New("missing outbound type")
-	default:
-		return nil, E.New("unknown outbound type: ", h.Type)
-	}
-	return rawOptionsPtr, nil
+func (h *Outbound) MarshalJSONContext(ctx context.Context) ([]byte, error) {
+	return badjson.MarshallObjectsContext(ctx, (*_Outbound)(h), h.Options)
 }
 
-func (h *Outbound) MarshalJSON() ([]byte, error) {
-	rawOptions, err := h.RawOptions()
-	if err != nil {
-		return nil, err
-	}
-	return MarshallObjects((*_Outbound)(h), rawOptions)
-}
-
-func (h *Outbound) UnmarshalJSON(bytes []byte) error {
-	err := json.Unmarshal(bytes, (*_Outbound)(h))
+func (h *Outbound) UnmarshalJSONContext(ctx context.Context, content []byte) error {
+	err := json.Unmarshal(content, (*_Outbound)(h))
 	if err != nil {
 		return err
 	}
-	rawOptions, err := h.RawOptions()
-	if err != nil {
-		return err
+	registry := service.FromContext[OutboundOptionsRegistry](ctx)
+	if registry == nil {
+		return E.New("missing outbound options registry in context")
+	}
+	options, loaded := registry.CreateOptions(h.Type)
+	if !loaded {
+		return E.New("unknown outbound type: ", h.Type)
 	}
-	err = UnmarshallExcluded(bytes, (*_Outbound)(h), rawOptions)
+	err = badjson.UnmarshallExcludedContext(ctx, content, (*_Outbound)(h), options)
 	if err != nil {
 		return err
 	}
+	h.Options = options
 	return nil
 }
 

+ 103 - 0
option/outbound_legacy.go

@@ -0,0 +1,103 @@
+package option
+
+import (
+	C "github.com/sagernet/sing-box/constant"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badjson"
+)
+
+type _LegacyOutbound struct {
+	Type                string                      `json:"type"`
+	Tag                 string                      `json:"tag,omitempty"`
+	DirectOptions       DirectOutboundOptions       `json:"-"`
+	SocksOptions        SOCKSOutboundOptions        `json:"-"`
+	HTTPOptions         HTTPOutboundOptions         `json:"-"`
+	ShadowsocksOptions  ShadowsocksOutboundOptions  `json:"-"`
+	VMessOptions        VMessOutboundOptions        `json:"-"`
+	TrojanOptions       TrojanOutboundOptions       `json:"-"`
+	WireGuardOptions    WireGuardOutboundOptions    `json:"-"`
+	HysteriaOptions     HysteriaOutboundOptions     `json:"-"`
+	TorOptions          TorOutboundOptions          `json:"-"`
+	SSHOptions          SSHOutboundOptions          `json:"-"`
+	ShadowTLSOptions    ShadowTLSOutboundOptions    `json:"-"`
+	ShadowsocksROptions ShadowsocksROutboundOptions `json:"-"`
+	VLESSOptions        VLESSOutboundOptions        `json:"-"`
+	TUICOptions         TUICOutboundOptions         `json:"-"`
+	Hysteria2Options    Hysteria2OutboundOptions    `json:"-"`
+	SelectorOptions     SelectorOutboundOptions     `json:"-"`
+	URLTestOptions      URLTestOutboundOptions      `json:"-"`
+}
+
+type LegacyOutbound _LegacyOutbound
+
+func (h *LegacyOutbound) RawOptions() (any, error) {
+	var rawOptionsPtr any
+	switch h.Type {
+	case C.TypeDirect:
+		rawOptionsPtr = &h.DirectOptions
+	case C.TypeBlock, C.TypeDNS:
+		rawOptionsPtr = new(StubOptions)
+	case C.TypeSOCKS:
+		rawOptionsPtr = &h.SocksOptions
+	case C.TypeHTTP:
+		rawOptionsPtr = &h.HTTPOptions
+	case C.TypeShadowsocks:
+		rawOptionsPtr = &h.ShadowsocksOptions
+	case C.TypeVMess:
+		rawOptionsPtr = &h.VMessOptions
+	case C.TypeTrojan:
+		rawOptionsPtr = &h.TrojanOptions
+	case C.TypeWireGuard:
+		rawOptionsPtr = &h.WireGuardOptions
+	case C.TypeHysteria:
+		rawOptionsPtr = &h.HysteriaOptions
+	case C.TypeTor:
+		rawOptionsPtr = &h.TorOptions
+	case C.TypeSSH:
+		rawOptionsPtr = &h.SSHOptions
+	case C.TypeShadowTLS:
+		rawOptionsPtr = &h.ShadowTLSOptions
+	case C.TypeShadowsocksR:
+		rawOptionsPtr = &h.ShadowsocksROptions
+	case C.TypeVLESS:
+		rawOptionsPtr = &h.VLESSOptions
+	case C.TypeTUIC:
+		rawOptionsPtr = &h.TUICOptions
+	case C.TypeHysteria2:
+		rawOptionsPtr = &h.Hysteria2Options
+	case C.TypeSelector:
+		rawOptionsPtr = &h.SelectorOptions
+	case C.TypeURLTest:
+		rawOptionsPtr = &h.URLTestOptions
+	case "":
+		return nil, E.New("missing outbound type")
+	default:
+		return nil, E.New("unknown outbound type: ", h.Type)
+	}
+	return rawOptionsPtr, nil
+}
+
+func (h *LegacyOutbound) MarshalJSON() ([]byte, error) {
+	rawOptions, err := h.RawOptions()
+	if err != nil {
+		return nil, err
+	}
+	return badjson.MarshallObjects((*_LegacyOutbound)(h), rawOptions)
+}
+
+func (h *LegacyOutbound) UnmarshalJSON(bytes []byte) error {
+	err := json.Unmarshal(bytes, (*_LegacyOutbound)(h))
+	if err != nil {
+		return err
+	}
+	rawOptions, err := h.RawOptions()
+	if err != nil {
+		return err
+	}
+	err = badjson.UnmarshallExcluded(bytes, (*_LegacyOutbound)(h), rawOptions)
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 7 - 6
option/rule.go

@@ -7,6 +7,7 @@ import (
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badjson"
 )
 
 type _Rule struct {
@@ -28,7 +29,7 @@ func (r Rule) MarshalJSON() ([]byte, error) {
 	default:
 		return nil, E.New("unknown rule type: " + r.Type)
 	}
-	return MarshallObjects((_Rule)(r), v)
+	return badjson.MarshallObjects((_Rule)(r), v)
 }
 
 func (r *Rule) UnmarshalJSON(bytes []byte) error {
@@ -46,7 +47,7 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error {
 	default:
 		return E.New("unknown rule type: " + r.Type)
 	}
-	err = UnmarshallExcluded(bytes, (*_Rule)(r), v)
+	err = badjson.UnmarshallExcluded(bytes, (*_Rule)(r), v)
 	if err != nil {
 		return err
 	}
@@ -109,7 +110,7 @@ type DefaultRule struct {
 }
 
 func (r *DefaultRule) MarshalJSON() ([]byte, error) {
-	return MarshallObjects(r.RawDefaultRule, r.RuleAction)
+	return badjson.MarshallObjects(r.RawDefaultRule, r.RuleAction)
 }
 
 func (r *DefaultRule) UnmarshalJSON(data []byte) error {
@@ -117,7 +118,7 @@ func (r *DefaultRule) UnmarshalJSON(data []byte) error {
 	if err != nil {
 		return err
 	}
-	return UnmarshallExcluded(data, &r.RawDefaultRule, &r.RuleAction)
+	return badjson.UnmarshallExcluded(data, &r.RawDefaultRule, &r.RuleAction)
 }
 
 func (r *DefaultRule) IsValid() bool {
@@ -139,7 +140,7 @@ type LogicalRule struct {
 }
 
 func (r *LogicalRule) MarshalJSON() ([]byte, error) {
-	return MarshallObjects(r._LogicalRule, r.RuleAction)
+	return badjson.MarshallObjects(r._LogicalRule, r.RuleAction)
 }
 
 func (r *LogicalRule) UnmarshalJSON(data []byte) error {
@@ -147,7 +148,7 @@ func (r *LogicalRule) UnmarshalJSON(data []byte) error {
 	if err != nil {
 		return err
 	}
-	return UnmarshallExcluded(data, &r._LogicalRule, &r.RuleAction)
+	return badjson.UnmarshallExcluded(data, &r._LogicalRule, &r.RuleAction)
 }
 
 func (r *LogicalRule) IsValid() bool {

+ 7 - 6
option/rule_action.go

@@ -4,6 +4,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badjson"
 )
 
 type _RuleAction struct {
@@ -36,9 +37,9 @@ func (r RuleAction) MarshalJSON() ([]byte, error) {
 		return nil, E.New("unknown rule action: " + r.Action)
 	}
 	if v == nil {
-		return MarshallObjects((_RuleAction)(r))
+		return badjson.MarshallObjects((_RuleAction)(r))
 	}
-	return MarshallObjects((_RuleAction)(r), v)
+	return badjson.MarshallObjects((_RuleAction)(r), v)
 }
 
 func (r *RuleAction) UnmarshalJSON(data []byte) error {
@@ -68,7 +69,7 @@ func (r *RuleAction) UnmarshalJSON(data []byte) error {
 		// check unknown fields
 		return json.UnmarshalDisallowUnknownFields(data, &_RuleAction{})
 	}
-	return UnmarshallExcluded(data, (*_RuleAction)(r), v)
+	return badjson.UnmarshallExcluded(data, (*_RuleAction)(r), v)
 }
 
 type _DNSRuleAction struct {
@@ -95,9 +96,9 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) {
 		return nil, E.New("unknown DNS rule action: " + r.Action)
 	}
 	if v == nil {
-		return MarshallObjects((_DNSRuleAction)(r))
+		return badjson.MarshallObjects((_DNSRuleAction)(r))
 	}
-	return MarshallObjects((_DNSRuleAction)(r), v)
+	return badjson.MarshallObjects((_DNSRuleAction)(r), v)
 }
 
 func (r *DNSRuleAction) UnmarshalJSON(data []byte) error {
@@ -121,7 +122,7 @@ func (r *DNSRuleAction) UnmarshalJSON(data []byte) error {
 		// check unknown fields
 		return json.UnmarshalDisallowUnknownFields(data, &_DNSRuleAction{})
 	}
-	return UnmarshallExcluded(data, (*_DNSRuleAction)(r), v)
+	return badjson.UnmarshallExcluded(data, (*_DNSRuleAction)(r), v)
 }
 
 type RouteActionOptions struct {

+ 7 - 6
option/rule_dns.go

@@ -7,6 +7,7 @@ import (
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badjson"
 )
 
 type _DNSRule struct {
@@ -28,7 +29,7 @@ func (r DNSRule) MarshalJSON() ([]byte, error) {
 	default:
 		return nil, E.New("unknown rule type: " + r.Type)
 	}
-	return MarshallObjects((_DNSRule)(r), v)
+	return badjson.MarshallObjects((_DNSRule)(r), v)
 }
 
 func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
@@ -46,7 +47,7 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
 	default:
 		return E.New("unknown rule type: " + r.Type)
 	}
-	err = UnmarshallExcluded(bytes, (*_DNSRule)(r), v)
+	err = badjson.UnmarshallExcluded(bytes, (*_DNSRule)(r), v)
 	if err != nil {
 		return err
 	}
@@ -111,7 +112,7 @@ type DefaultDNSRule struct {
 }
 
 func (r *DefaultDNSRule) MarshalJSON() ([]byte, error) {
-	return MarshallObjects(r.RawDefaultDNSRule, r.DNSRuleAction)
+	return badjson.MarshallObjects(r.RawDefaultDNSRule, r.DNSRuleAction)
 }
 
 func (r *DefaultDNSRule) UnmarshalJSON(data []byte) error {
@@ -119,7 +120,7 @@ func (r *DefaultDNSRule) UnmarshalJSON(data []byte) error {
 	if err != nil {
 		return err
 	}
-	return UnmarshallExcluded(data, &r.RawDefaultDNSRule, &r.DNSRuleAction)
+	return badjson.UnmarshallExcluded(data, &r.RawDefaultDNSRule, &r.DNSRuleAction)
 }
 
 func (r *DefaultDNSRule) IsValid() bool {
@@ -141,7 +142,7 @@ type LogicalDNSRule struct {
 }
 
 func (r *LogicalDNSRule) MarshalJSON() ([]byte, error) {
-	return MarshallObjects(r._LogicalDNSRule, r.DNSRuleAction)
+	return badjson.MarshallObjects(r._LogicalDNSRule, r.DNSRuleAction)
 }
 
 func (r *LogicalDNSRule) UnmarshalJSON(data []byte) error {
@@ -149,7 +150,7 @@ func (r *LogicalDNSRule) UnmarshalJSON(data []byte) error {
 	if err != nil {
 		return err
 	}
-	return UnmarshallExcluded(data, &r._LogicalDNSRule, &r.DNSRuleAction)
+	return badjson.UnmarshallExcluded(data, &r._LogicalDNSRule, &r.DNSRuleAction)
 }
 
 func (r *LogicalDNSRule) IsValid() bool {

+ 7 - 6
option/rule_set.go

@@ -9,6 +9,7 @@ import (
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
 	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badjson"
 
 	"go4.org/netipx"
 )
@@ -37,7 +38,7 @@ func (r RuleSet) MarshalJSON() ([]byte, error) {
 	default:
 		return nil, E.New("unknown rule-set type: " + r.Type)
 	}
-	return MarshallObjects((_RuleSet)(r), v)
+	return badjson.MarshallObjects((_RuleSet)(r), v)
 }
 
 func (r *RuleSet) UnmarshalJSON(bytes []byte) error {
@@ -71,7 +72,7 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error {
 	} else {
 		r.Format = ""
 	}
-	err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v)
+	err = badjson.UnmarshallExcluded(bytes, (*_RuleSet)(r), v)
 	if err != nil {
 		return err
 	}
@@ -107,7 +108,7 @@ func (r HeadlessRule) MarshalJSON() ([]byte, error) {
 	default:
 		return nil, E.New("unknown rule type: " + r.Type)
 	}
-	return MarshallObjects((_HeadlessRule)(r), v)
+	return badjson.MarshallObjects((_HeadlessRule)(r), v)
 }
 
 func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error {
@@ -125,7 +126,7 @@ func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error {
 	default:
 		return E.New("unknown rule type: " + r.Type)
 	}
-	err = UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v)
+	err = badjson.UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v)
 	if err != nil {
 		return err
 	}
@@ -203,7 +204,7 @@ func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) {
 	default:
 		return nil, E.New("unknown rule-set version: ", r.Version)
 	}
-	return MarshallObjects((_PlainRuleSetCompat)(r), v)
+	return badjson.MarshallObjects((_PlainRuleSetCompat)(r), v)
 }
 
 func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
@@ -220,7 +221,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
 	default:
 		return E.New("unknown rule-set version: ", r.Version)
 	}
-	err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v)
+	err = badjson.UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v)
 	if err != nil {
 		return err
 	}

+ 1 - 1
option/simple.go

@@ -14,7 +14,7 @@ type HTTPMixedInboundOptions struct {
 	InboundTLSOptionsContainer
 }
 
-type SocksOutboundOptions struct {
+type SOCKSOutboundOptions struct {
 	DialerOptions
 	ServerOptions
 	Version    string             `json:"version,omitempty"`

+ 3 - 2
option/tls_acme.go

@@ -4,6 +4,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badjson"
 )
 
 type InboundACMEOptions struct {
@@ -45,7 +46,7 @@ func (o ACMEDNS01ChallengeOptions) MarshalJSON() ([]byte, error) {
 	default:
 		return nil, E.New("unknown provider type: " + o.Provider)
 	}
-	return MarshallObjects((_ACMEDNS01ChallengeOptions)(o), v)
+	return badjson.MarshallObjects((_ACMEDNS01ChallengeOptions)(o), v)
 }
 
 func (o *ACMEDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error {
@@ -62,7 +63,7 @@ func (o *ACMEDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error {
 	default:
 		return E.New("unknown provider type: " + o.Provider)
 	}
-	err = UnmarshallExcluded(bytes, (*_ACMEDNS01ChallengeOptions)(o), v)
+	err = badjson.UnmarshallExcluded(bytes, (*_ACMEDNS01ChallengeOptions)(o), v)
 	if err != nil {
 		return err
 	}

+ 3 - 2
option/v2ray_transport.go

@@ -4,6 +4,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badjson"
 )
 
 type _V2RayTransportOptions struct {
@@ -35,7 +36,7 @@ func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) {
 	default:
 		return nil, E.New("unknown transport type: " + o.Type)
 	}
-	return MarshallObjects((_V2RayTransportOptions)(o), v)
+	return badjson.MarshallObjects((_V2RayTransportOptions)(o), v)
 }
 
 func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error {
@@ -58,7 +59,7 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error {
 	default:
 		return E.New("unknown transport type: " + o.Type)
 	}
-	err = UnmarshallExcluded(bytes, (*_V2RayTransportOptions)(o), v)
+	err = badjson.UnmarshallExcluded(bytes, (*_V2RayTransportOptions)(o), v)
 	if err != nil {
 		return err
 	}

+ 0 - 54
outbound/block.go

@@ -1,54 +0,0 @@
-package outbound
-
-import (
-	"context"
-	"io"
-	"net"
-
-	"github.com/sagernet/sing-box/adapter"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/log"
-	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
-)
-
-var _ adapter.Outbound = (*Block)(nil)
-
-type Block struct {
-	myOutboundAdapter
-}
-
-func NewBlock(logger log.ContextLogger, tag string) *Block {
-	return &Block{
-		myOutboundAdapter{
-			protocol: C.TypeBlock,
-			network:  []string{N.NetworkTCP, N.NetworkUDP},
-			logger:   logger,
-			tag:      tag,
-		},
-	}
-}
-
-func (h *Block) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
-	h.logger.InfoContext(ctx, "blocked connection to ", destination)
-	return nil, io.EOF
-}
-
-func (h *Block) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
-	h.logger.InfoContext(ctx, "blocked packet connection to ", destination)
-	return nil, io.EOF
-}
-
-// Deprecated
-func (h *Block) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	conn.Close()
-	h.logger.InfoContext(ctx, "blocked connection to ", metadata.Destination)
-	return nil
-}
-
-// Deprecated
-func (h *Block) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	conn.Close()
-	h.logger.InfoContext(ctx, "blocked packet connection to ", metadata.Destination)
-	return nil
-}

+ 0 - 65
outbound/builder.go

@@ -1,65 +0,0 @@
-package outbound
-
-import (
-	"context"
-
-	"github.com/sagernet/sing-box/adapter"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing-box/option"
-	E "github.com/sagernet/sing/common/exceptions"
-)
-
-func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Outbound) (adapter.Outbound, error) {
-	if tag != "" {
-		ctx = adapter.WithContext(ctx, &adapter.InboundContext{
-			Outbound: tag,
-		})
-	}
-	if options.Type == "" {
-		return nil, E.New("missing outbound type")
-	}
-	ctx = ContextWithTag(ctx, tag)
-	switch options.Type {
-	case C.TypeDirect:
-		return NewDirect(router, logger, tag, options.DirectOptions)
-	case C.TypeBlock:
-		return NewBlock(logger, tag), nil
-	case C.TypeDNS:
-		return NewDNS(router, tag), nil
-	case C.TypeSOCKS:
-		return NewSocks(router, logger, tag, options.SocksOptions)
-	case C.TypeHTTP:
-		return NewHTTP(ctx, router, logger, tag, options.HTTPOptions)
-	case C.TypeShadowsocks:
-		return NewShadowsocks(ctx, router, logger, tag, options.ShadowsocksOptions)
-	case C.TypeVMess:
-		return NewVMess(ctx, router, logger, tag, options.VMessOptions)
-	case C.TypeTrojan:
-		return NewTrojan(ctx, router, logger, tag, options.TrojanOptions)
-	case C.TypeWireGuard:
-		return NewWireGuard(ctx, router, logger, tag, options.WireGuardOptions)
-	case C.TypeHysteria:
-		return NewHysteria(ctx, router, logger, tag, options.HysteriaOptions)
-	case C.TypeTor:
-		return NewTor(ctx, router, logger, tag, options.TorOptions)
-	case C.TypeSSH:
-		return NewSSH(ctx, router, logger, tag, options.SSHOptions)
-	case C.TypeShadowTLS:
-		return NewShadowTLS(ctx, router, logger, tag, options.ShadowTLSOptions)
-	case C.TypeShadowsocksR:
-		return NewShadowsocksR(ctx, router, logger, tag, options.ShadowsocksROptions)
-	case C.TypeVLESS:
-		return NewVLESS(ctx, router, logger, tag, options.VLESSOptions)
-	case C.TypeTUIC:
-		return NewTUIC(ctx, router, logger, tag, options.TUICOptions)
-	case C.TypeHysteria2:
-		return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options)
-	case C.TypeSelector:
-		return NewSelector(ctx, router, logger, tag, options.SelectorOptions)
-	case C.TypeURLTest:
-		return NewURLTest(ctx, router, logger, tag, options.URLTestOptions)
-	default:
-		return nil, E.New("unknown outbound type: ", options.Type)
-	}
-}

+ 0 - 20
outbound/hysteria_stub.go

@@ -1,20 +0,0 @@
-//go:build !with_quic
-
-package outbound
-
-import (
-	"context"
-
-	"github.com/sagernet/sing-box/adapter"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing-box/option"
-)
-
-func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) {
-	return nil, C.ErrQUICNotIncluded
-}
-
-func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (adapter.Outbound, error) {
-	return nil, C.ErrQUICNotIncluded
-}

+ 0 - 14
outbound/lookback.go

@@ -1,14 +0,0 @@
-package outbound
-
-import "context"
-
-type outboundTagKey struct{}
-
-func ContextWithTag(ctx context.Context, outboundTag string) context.Context {
-	return context.WithValue(ctx, outboundTagKey{}, outboundTag)
-}
-
-func TagFromContext(ctx context.Context) (string, bool) {
-	value, loaded := ctx.Value(outboundTagKey{}).(string)
-	return value, loaded
-}

+ 0 - 18
outbound/shadowsocksr.go

@@ -1,18 +0,0 @@
-//go:build with_shadowsocksr
-
-package outbound
-
-import (
-	"context"
-	"os"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing-box/option"
-)
-
-var _ int = "ShadowsocksR is deprecated and removed in sing-box 1.6.0"
-
-func NewShadowsocksR(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksROutboundOptions) (adapter.Outbound, error) {
-	return nil, os.ErrInvalid
-}

+ 0 - 16
outbound/shadowsocksr_stub.go

@@ -1,16 +0,0 @@
-//go:build !with_shadowsocksr
-
-package outbound
-
-import (
-	"context"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing-box/option"
-	E "github.com/sagernet/sing/common/exceptions"
-)
-
-func NewShadowsocksR(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksROutboundOptions) (adapter.Outbound, error) {
-	return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0")
-}

+ 0 - 15
outbound/tor_embed.go

@@ -1,15 +0,0 @@
-//go:build with_embedded_tor && !(android || ios)
-
-package outbound
-
-import (
-	"berty.tech/go-libtor"
-	"github.com/cretz/bine/tor"
-)
-
-func newConfig() tor.StartConf {
-	return tor.StartConf{
-		ProcessCreator:         libtor.Creator,
-		UseEmbeddedControlConn: true,
-	}
-}

+ 0 - 15
outbound/tor_embed_mobile.go

@@ -1,15 +0,0 @@
-//go:build with_embedded_tor && (android || ios)
-
-package outbound
-
-import (
-	"github.com/cretz/bine/tor"
-	"github.com/ooni/go-libtor"
-)
-
-func newConfig() tor.StartConf {
-	return tor.StartConf{
-		ProcessCreator:         libtor.Creator,
-		UseEmbeddedControlConn: true,
-	}
-}

+ 0 - 9
outbound/tor_external.go

@@ -1,9 +0,0 @@
-//go:build !with_embedded_tor
-
-package outbound
-
-import "github.com/cretz/bine/tor"
-
-func newConfig() tor.StartConf {
-	return tor.StartConf{}
-}

+ 0 - 16
outbound/tuic_stub.go

@@ -1,16 +0,0 @@
-//go:build !with_quic
-
-package outbound
-
-import (
-	"context"
-
-	"github.com/sagernet/sing-box/adapter"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing-box/option"
-)
-
-func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (adapter.Outbound, error) {
-	return nil, C.ErrQUICNotIncluded
-}

+ 0 - 16
outbound/wireguard_stub.go

@@ -1,16 +0,0 @@
-//go:build !with_wireguard
-
-package outbound
-
-import (
-	"context"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing-box/option"
-	E "github.com/sagernet/sing/common/exceptions"
-)
-
-func NewWireGuard(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardOutboundOptions) (adapter.Outbound, error) {
-	return nil, E.New(`WireGuard is not included in this build, rebuild with -tags with_wireguard`)
-}

+ 42 - 0
protocol/block/outbound.go

@@ -0,0 +1,42 @@
+package block
+
+import (
+	"context"
+	"net"
+	"syscall"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common/logger"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.StubOptions](registry, C.TypeBlock, New)
+}
+
+type Outbound struct {
+	outbound.Adapter
+	logger logger.ContextLogger
+}
+
+func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, _ option.StubOptions) (adapter.Outbound, error) {
+	return &Outbound{
+		Adapter: outbound.NewAdapter(C.TypeBlock, []string{N.NetworkTCP, N.NetworkUDP}, tag, nil),
+		logger:  logger,
+	}, nil
+}
+
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	h.logger.InfoContext(ctx, "blocked connection to ", destination)
+	return nil, syscall.EPERM
+}
+
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	h.logger.InfoContext(ctx, "blocked packet connection to ", destination)
+	return nil, syscall.EPERM
+}

+ 1 - 1
outbound/direct_loopback_detect.go → protocol/direct/loopback_detect.go

@@ -1,4 +1,4 @@
-package outbound
+package direct
 
 import (
 	"net"

+ 23 - 25
outbound/direct.go → protocol/direct/outbound.go

@@ -1,4 +1,4 @@
-package outbound
+package direct
 
 import (
 	"context"
@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
@@ -14,17 +15,20 @@ import (
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing/common/bufio"
 	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"
 )
 
-var (
-	_ adapter.Outbound = (*Direct)(nil)
-	_ N.ParallelDialer = (*Direct)(nil)
-)
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.DirectOutboundOptions](registry, C.TypeDirect, NewOutbound)
+}
+
+var _ N.ParallelDialer = (*Outbound)(nil)
 
-type Direct struct {
-	myOutboundAdapter
+type Outbound struct {
+	outbound.Adapter
+	logger              logger.ContextLogger
 	dialer              N.Dialer
 	domainStrategy      dns.DomainStrategy
 	fallbackDelay       time.Duration
@@ -33,21 +37,15 @@ type Direct struct {
 	// loopBack *loopBackDetector
 }
 
-func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (*Direct, error) {
+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)
 	if err != nil {
 		return nil, err
 	}
-	outbound := &Direct{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeDirect,
-			network:      []string{N.NetworkTCP, N.NetworkUDP},
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
+	outbound := &Outbound{
+		Adapter:        outbound.NewAdapterWithDialerOptions(C.TypeDirect, []string{N.NetworkTCP, N.NetworkUDP}, tag, options.DialerOptions),
+		logger:         logger,
 		domainStrategy: dns.DomainStrategy(options.DomainStrategy),
 		fallbackDelay:  time.Duration(options.FallbackDelay),
 		dialer:         outboundDialer,
@@ -69,9 +67,9 @@ func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, opti
 	return outbound, nil
 }
 
-func (h *Direct) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	switch h.overrideOption {
 	case 1:
@@ -98,9 +96,9 @@ func (h *Direct) DialContext(ctx context.Context, network string, destination M.
 	return h.dialer.DialContext(ctx, network, destination)
 }
 
-func (h *Direct) DialParallel(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr) (net.Conn, error) {
+func (h *Outbound) DialParallel(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr) (net.Conn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	switch h.overrideOption {
 	case 1, 2:
@@ -125,9 +123,9 @@ func (h *Direct) DialParallel(ctx context.Context, network string, destination M
 	return N.DialParallel(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, h.fallbackDelay)
 }
 
-func (h *Direct) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	originDestination := destination
 	switch h.overrideOption {
@@ -156,14 +154,14 @@ func (h *Direct) ListenPacket(ctx context.Context, destination M.Socksaddr) (net
 	return conn, nil
 }
 
-/*func (h *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+/*func (h *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	if h.loopBack.CheckConn(metadata.Source.AddrPort(), M.AddrPortFromNet(conn.LocalAddr())) {
 		return E.New("reject loopback connection to ", metadata.Destination)
 	}
 	return NewConnection(ctx, h, conn, metadata)
 }
 
-func (h *Direct) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	if h.loopBack.CheckPacketConn(metadata.Source.AddrPort(), M.AddrPortFromNet(conn.LocalAddr())) {
 		return E.New("reject loopback packet connection to ", metadata.Destination)
 	}

+ 2 - 47
outbound/dns.go → protocol/dns/handle.go

@@ -1,15 +1,13 @@
-package outbound
+package dns
 
 import (
 	"context"
 	"encoding/binary"
 	"net"
-	"os"
-	"time"
 
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-dns"
+	dns "github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/buf"
 	"github.com/sagernet/sing/common/bufio"
@@ -21,44 +19,6 @@ import (
 	mDNS "github.com/miekg/dns"
 )
 
-var _ adapter.Outbound = (*DNS)(nil)
-
-type DNS struct {
-	myOutboundAdapter
-}
-
-func NewDNS(router adapter.Router, tag string) *DNS {
-	return &DNS{
-		myOutboundAdapter{
-			protocol: C.TypeDNS,
-			network:  []string{N.NetworkTCP, N.NetworkUDP},
-			router:   router,
-			tag:      tag,
-		},
-	}
-}
-
-func (d *DNS) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
-	return nil, os.ErrInvalid
-}
-
-func (d *DNS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
-	return nil, os.ErrInvalid
-}
-
-// Deprecated
-func (d *DNS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	metadata.Destination = M.Socksaddr{}
-	defer conn.Close()
-	for {
-		conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
-		err := HandleStreamDNSRequest(ctx, d.router, conn, metadata)
-		if err != nil {
-			return err
-		}
-	}
-}
-
 func HandleStreamDNSRequest(ctx context.Context, router adapter.Router, conn net.Conn, metadata adapter.InboundContext) error {
 	var queryLength uint16
 	err := binary.Read(conn, binary.BigEndian, &queryLength)
@@ -100,11 +60,6 @@ func HandleStreamDNSRequest(ctx context.Context, router adapter.Router, conn net
 	return nil
 }
 
-// Deprecated
-func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return NewDNSPacketConnection(ctx, d.router, conn, nil, metadata)
-}
-
 func NewDNSPacketConnection(ctx context.Context, router adapter.Router, conn N.PacketConn, cachedPackets []*N.PacketBuffer, metadata adapter.InboundContext) error {
 	metadata.Destination = M.Socksaddr{}
 	var reader N.PacketReader = conn

+ 61 - 0
protocol/dns/outbound.go

@@ -0,0 +1,61 @@
+package dns
+
+import (
+	"context"
+	"net"
+	"os"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common/logger"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.StubOptions](registry, C.TypeDNS, NewOutbound)
+}
+
+type Outbound struct {
+	outbound.Adapter
+	router adapter.Router
+	logger logger.ContextLogger
+}
+
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.StubOptions) (adapter.Outbound, error) {
+	return &Outbound{
+		Adapter: outbound.NewAdapter(C.TypeDNS, []string{N.NetworkTCP, N.NetworkUDP}, tag, nil),
+		router:  router,
+		logger:  logger,
+	}, nil
+}
+
+func (d *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	return nil, os.ErrInvalid
+}
+
+func (d *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	return nil, os.ErrInvalid
+}
+
+// Deprecated
+func (d *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	metadata.Destination = M.Socksaddr{}
+	defer conn.Close()
+	for {
+		conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
+		err := HandleStreamDNSRequest(ctx, d.router, conn, metadata)
+		if err != nil {
+			return err
+		}
+	}
+}
+
+// Deprecated
+func (d *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	return NewDNSPacketConnection(ctx, d.router, conn, nil, metadata)
+}

+ 21 - 20
outbound/selector.go → protocol/group/selector.go

@@ -1,28 +1,33 @@
-package outbound
+package group
 
 import (
 	"context"
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/interrupt"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	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"
 	"github.com/sagernet/sing/service"
 )
 
-var (
-	_ adapter.Outbound      = (*Selector)(nil)
-	_ adapter.OutboundGroup = (*Selector)(nil)
-)
+func RegisterSelector(registry *outbound.Registry) {
+	outbound.Register[option.SelectorOutboundOptions](registry, C.TypeSelector, NewSelector)
+}
+
+var _ adapter.OutboundGroup = (*Selector)(nil)
 
 type Selector struct {
-	myOutboundAdapter
+	outbound.Adapter
 	ctx                          context.Context
+	router                       adapter.Router
+	logger                       logger.ContextLogger
 	tags                         []string
 	defaultTag                   string
 	outbounds                    map[string]adapter.Outbound
@@ -31,16 +36,12 @@ type Selector struct {
 	interruptExternalConnections bool
 }
 
-func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) {
+func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (adapter.Outbound, error) {
 	outbound := &Selector{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeSelector,
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: options.Outbounds,
-		},
+		Adapter:                      outbound.NewAdapter(C.TypeSelector, nil, tag, options.Outbounds),
 		ctx:                          ctx,
+		router:                       router,
+		logger:                       logger,
 		tags:                         options.Outbounds,
 		defaultTag:                   options.Default,
 		outbounds:                    make(map[string]adapter.Outbound),
@@ -69,10 +70,10 @@ func (s *Selector) Start() error {
 		s.outbounds[tag] = detour
 	}
 
-	if s.tag != "" {
+	if s.Tag() != "" {
 		cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
 		if cacheFile != nil {
-			selected := cacheFile.LoadSelected(s.tag)
+			selected := cacheFile.LoadSelected(s.Tag())
 			if selected != "" {
 				detour, loaded := s.outbounds[selected]
 				if loaded {
@@ -113,10 +114,10 @@ func (s *Selector) SelectOutbound(tag string) bool {
 		return true
 	}
 	s.selected = detour
-	if s.tag != "" {
+	if s.Tag() != "" {
 		cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
 		if cacheFile != nil {
-			err := cacheFile.StoreSelected(s.tag, tag)
+			err := cacheFile.StoreSelected(s.Tag(), tag)
 			if err != nil {
 				s.logger.Error("store selected: ", err)
 			}
@@ -149,7 +150,7 @@ func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata ad
 	if legacyHandler, ok := s.selected.(adapter.ConnectionHandler); ok {
 		return legacyHandler.NewConnection(ctx, conn, metadata)
 	} else {
-		return NewConnection(ctx, s.selected, conn, metadata)
+		return outbound.NewConnection(ctx, s.selected, conn, metadata)
 	}
 }
 
@@ -160,7 +161,7 @@ func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, m
 	if legacyHandler, ok := s.selected.(adapter.PacketConnectionHandler); ok {
 		return legacyHandler.NewPacketConnection(ctx, conn, metadata)
 	} else {
-		return NewPacketConnection(ctx, s.selected, conn, metadata)
+		return outbound.NewPacketConnection(ctx, s.selected, conn, metadata)
 	}
 }
 

+ 15 - 14
outbound/urltest.go → protocol/group/urltest.go

@@ -1,4 +1,4 @@
-package outbound
+package group
 
 import (
 	"context"
@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/interrupt"
 	"github.com/sagernet/sing-box/common/urltest"
 	C "github.com/sagernet/sing-box/constant"
@@ -22,15 +23,20 @@ import (
 	"github.com/sagernet/sing/service/pause"
 )
 
+func RegisterURLTest(registry *outbound.Registry) {
+	outbound.Register[option.URLTestOutboundOptions](registry, C.TypeURLTest, NewURLTest)
+}
+
 var (
-	_ adapter.Outbound                = (*URLTest)(nil)
 	_ adapter.OutboundGroup           = (*URLTest)(nil)
 	_ adapter.InterfaceUpdateListener = (*URLTest)(nil)
 )
 
 type URLTest struct {
-	myOutboundAdapter
+	outbound.Adapter
 	ctx                          context.Context
+	router                       adapter.Router
+	logger                       log.ContextLogger
 	tags                         []string
 	link                         string
 	interval                     time.Duration
@@ -40,17 +46,12 @@ type URLTest struct {
 	interruptExternalConnections bool
 }
 
-func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) {
+func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (adapter.Outbound, error) {
 	outbound := &URLTest{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeURLTest,
-			network:      []string{N.NetworkTCP, N.NetworkUDP},
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: options.Outbounds,
-		},
+		Adapter:                      outbound.NewAdapter(C.TypeURLTest, []string{N.NetworkTCP, N.NetworkUDP}, tag, options.Outbounds),
 		ctx:                          ctx,
+		router:                       router,
+		logger:                       logger,
 		tags:                         options.Outbounds,
 		link:                         options.URL,
 		interval:                     time.Duration(options.Interval),
@@ -171,14 +172,14 @@ func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (ne
 // Deprecated
 func (s *URLTest) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	ctx = interrupt.ContextWithIsExternalConnection(ctx)
-	return NewConnection(ctx, s, conn, metadata)
+	return outbound.NewConnection(ctx, s, conn, metadata)
 }
 
 // TODO
 // Deprecated
 func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	ctx = interrupt.ContextWithIsExternalConnection(ctx)
-	return NewPacketConnection(ctx, s, conn, metadata)
+	return outbound.NewPacketConnection(ctx, s, conn, metadata)
 }
 
 func (s *URLTest) InterfaceUpdated() {

+ 17 - 18
outbound/http.go → protocol/http/outbound.go

@@ -1,4 +1,4 @@
-package outbound
+package http
 
 import (
 	"context"
@@ -6,25 +6,30 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/logger"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	sHTTP "github.com/sagernet/sing/protocol/http"
 )
 
-var _ adapter.Outbound = (*HTTP)(nil)
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.HTTPOutboundOptions](registry, C.TypeHTTP, NewOutbound)
+}
 
-type HTTP struct {
-	myOutboundAdapter
+type Outbound struct {
+	outbound.Adapter
+	logger logger.ContextLogger
 	client *sHTTP.Client
 }
 
-func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (*HTTP, error) {
+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)
 	if err != nil {
 		return nil, err
@@ -33,16 +38,10 @@ func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogge
 	if err != nil {
 		return nil, err
 	}
-	return &HTTP{
-		myOutboundAdapter{
-			protocol:     C.TypeHTTP,
-			network:      []string{N.NetworkTCP},
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
-		sHTTP.NewClient(sHTTP.Options{
+	return &Outbound{
+		Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHTTP, []string{N.NetworkTCP}, tag, options.DialerOptions),
+		logger:  logger,
+		client: sHTTP.NewClient(sHTTP.Options{
 			Dialer:   detour,
 			Server:   options.ServerOptions.Build(),
 			Username: options.Username,
@@ -53,14 +52,14 @@ func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogge
 	}, nil
 }
 
-func (h *HTTP) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	h.logger.InfoContext(ctx, "outbound connection to ", destination)
 	return h.client.DialContext(ctx, network, destination)
 }
 
-func (h *HTTP) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	return nil, os.ErrInvalid
 }

+ 22 - 22
outbound/hysteria.go → protocol/hysteria/outbound.go

@@ -1,6 +1,4 @@
-//go:build with_quic
-
-package outbound
+package hysteria
 
 import (
 	"context"
@@ -8,31 +6,39 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/humanize"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/protocol/tuic"
 	"github.com/sagernet/sing-quic/hysteria"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
 	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"
 )
 
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.HysteriaOutboundOptions](registry, C.TypeHysteria, NewOutbound)
+}
+
 var (
-	_ adapter.Outbound                = (*TUIC)(nil)
-	_ adapter.InterfaceUpdateListener = (*TUIC)(nil)
+	_ adapter.Outbound                = (*tuic.Outbound)(nil)
+	_ adapter.InterfaceUpdateListener = (*tuic.Outbound)(nil)
 )
 
-type Hysteria struct {
-	myOutboundAdapter
+type Outbound struct {
+	outbound.Adapter
+	logger logger.ContextLogger
 	client *hysteria.Client
 }
 
-func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (*Hysteria, error) {
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) {
 	options.UDPFragmentDefault = true
 	if options.TLS == nil || !options.TLS.Enabled {
 		return nil, C.ErrTLSRequired
@@ -88,20 +94,14 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
 	if err != nil {
 		return nil, err
 	}
-	return &Hysteria{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeHysteria,
-			network:      networkList,
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
-		client: client,
+	return &Outbound{
+		Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria, networkList, tag, options.DialerOptions),
+		logger:  logger,
+		client:  client,
 	}, nil
 }
 
-func (h *Hysteria) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	switch N.NetworkName(network) {
 	case N.NetworkTCP:
 		h.logger.InfoContext(ctx, "outbound connection to ", destination)
@@ -117,15 +117,15 @@ func (h *Hysteria) DialContext(ctx context.Context, network string, destination
 	}
 }
 
-func (h *Hysteria) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 	return h.client.ListenPacket(ctx, destination)
 }
 
-func (h *Hysteria) InterfaceUpdated() {
+func (h *Outbound) InterfaceUpdated() {
 	h.client.CloseWithError(E.New("network changed"))
 }
 
-func (h *Hysteria) Close() error {
+func (h *Outbound) Close() error {
 	return h.client.CloseWithError(os.ErrClosed)
 }

+ 22 - 22
outbound/hysteria2.go → protocol/hysteria2/outbound.go

@@ -1,6 +1,4 @@
-//go:build with_quic
-
-package outbound
+package hysteria2
 
 import (
 	"context"
@@ -8,31 +6,39 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/protocol/tuic"
 	"github.com/sagernet/sing-quic/hysteria"
 	"github.com/sagernet/sing-quic/hysteria2"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
 	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"
 )
 
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.Hysteria2OutboundOptions](registry, C.TypeHysteria2, NewOutbound)
+}
+
 var (
-	_ adapter.Outbound                = (*TUIC)(nil)
-	_ adapter.InterfaceUpdateListener = (*TUIC)(nil)
+	_ adapter.Outbound                = (*tuic.Outbound)(nil)
+	_ adapter.InterfaceUpdateListener = (*tuic.Outbound)(nil)
 )
 
-type Hysteria2 struct {
-	myOutboundAdapter
+type Outbound struct {
+	outbound.Adapter
+	logger logger.ContextLogger
 	client *hysteria2.Client
 }
 
-func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (*Hysteria2, error) {
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (adapter.Outbound, error) {
 	options.UDPFragmentDefault = true
 	if options.TLS == nil || !options.TLS.Enabled {
 		return nil, C.ErrTLSRequired
@@ -74,20 +80,14 @@ func NewHysteria2(ctx context.Context, router adapter.Router, logger log.Context
 	if err != nil {
 		return nil, err
 	}
-	return &Hysteria2{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeHysteria2,
-			network:      networkList,
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
-		client: client,
+	return &Outbound{
+		Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria2, networkList, tag, options.DialerOptions),
+		logger:  logger,
+		client:  client,
 	}, nil
 }
 
-func (h *Hysteria2) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	switch N.NetworkName(network) {
 	case N.NetworkTCP:
 		h.logger.InfoContext(ctx, "outbound connection to ", destination)
@@ -103,15 +103,15 @@ func (h *Hysteria2) DialContext(ctx context.Context, network string, destination
 	}
 }
 
-func (h *Hysteria2) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 	return h.client.ListenPacket(ctx)
 }
 
-func (h *Hysteria2) InterfaceUpdated() {
+func (h *Outbound) InterfaceUpdated() {
 	h.client.CloseWithError(E.New("network changed"))
 }
 
-func (h *Hysteria2) Close() error {
+func (h *Outbound) Close() error {
 	return h.client.CloseWithError(os.ErrClosed)
 }

+ 22 - 23
outbound/shadowsocks.go → protocol/shadowsocks/outbound.go

@@ -1,10 +1,11 @@
-package outbound
+package shadowsocks
 
 import (
 	"context"
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/mux"
 	C "github.com/sagernet/sing-box/constant"
@@ -15,15 +16,19 @@ import (
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
 	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"
 	"github.com/sagernet/sing/common/uot"
 )
 
-var _ adapter.Outbound = (*Shadowsocks)(nil)
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.ShadowsocksOutboundOptions](registry, C.TypeShadowsocks, NewOutbound)
+}
 
-type Shadowsocks struct {
-	myOutboundAdapter
+type Outbound struct {
+	outbound.Adapter
+	logger          logger.ContextLogger
 	dialer          N.Dialer
 	method          shadowsocks.Method
 	serverAddr      M.Socksaddr
@@ -32,7 +37,7 @@ type Shadowsocks struct {
 	multiplexDialer *mux.Client
 }
 
-func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksOutboundOptions) (*Shadowsocks, error) {
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksOutboundOptions) (adapter.Outbound, error) {
 	method, err := shadowsocks.CreateMethod(ctx, options.Method, shadowsocks.MethodOptions{
 		Password: options.Password,
 	})
@@ -43,15 +48,9 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 	if err != nil {
 		return nil, err
 	}
-	outbound := &Shadowsocks{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeShadowsocks,
-			network:      options.Network.Build(),
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
+	outbound := &Outbound{
+		Adapter:    outbound.NewAdapterWithDialerOptions(C.TypeShadowsocks, options.Network.Build(), tag, options.DialerOptions),
+		logger:     logger,
 		dialer:     outboundDialer,
 		method:     method,
 		serverAddr: options.ServerOptions.Build(),
@@ -78,9 +77,9 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 	return outbound, nil
 }
 
-func (h *Shadowsocks) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	if h.multiplexDialer == nil {
 		switch N.NetworkName(network) {
@@ -106,9 +105,9 @@ func (h *Shadowsocks) DialContext(ctx context.Context, network string, destinati
 	}
 }
 
-func (h *Shadowsocks) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	if h.multiplexDialer == nil {
 		if h.uotClient != nil {
@@ -125,24 +124,24 @@ func (h *Shadowsocks) ListenPacket(ctx context.Context, destination M.Socksaddr)
 	}
 }
 
-func (h *Shadowsocks) InterfaceUpdated() {
+func (h *Outbound) InterfaceUpdated() {
 	if h.multiplexDialer != nil {
 		h.multiplexDialer.Reset()
 	}
 	return
 }
 
-func (h *Shadowsocks) Close() error {
+func (h *Outbound) Close() error {
 	return common.Close(common.PtrOrNil(h.multiplexDialer))
 }
 
 var _ N.Dialer = (*shadowsocksDialer)(nil)
 
-type shadowsocksDialer Shadowsocks
+type shadowsocksDialer Outbound
 
 func (h *shadowsocksDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	switch N.NetworkName(network) {
 	case N.NetworkTCP:
@@ -170,7 +169,7 @@ func (h *shadowsocksDialer) DialContext(ctx context.Context, network string, des
 
 func (h *shadowsocksDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	outConn, err := h.dialer.DialContext(ctx, N.NetworkUDP, h.serverAddr)
 	if err != nil {

+ 13 - 17
outbound/shadowtls.go → protocol/shadowtls/outbound.go

@@ -1,4 +1,4 @@
-package outbound
+package shadowtls
 
 import (
 	"context"
@@ -6,6 +6,7 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
@@ -17,23 +18,18 @@ import (
 	N "github.com/sagernet/sing/common/network"
 )
 
-var _ adapter.Outbound = (*ShadowTLS)(nil)
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.ShadowTLSOutboundOptions](registry, C.TypeShadowTLS, NewOutbound)
+}
 
-type ShadowTLS struct {
-	myOutboundAdapter
+type Outbound struct {
+	outbound.Adapter
 	client *shadowtls.Client
 }
 
-func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSOutboundOptions) (*ShadowTLS, error) {
-	outbound := &ShadowTLS{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeShadowTLS,
-			network:      []string{N.NetworkTCP},
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSOutboundOptions) (adapter.Outbound, error) {
+	outbound := &Outbound{
+		Adapter: outbound.NewAdapterWithDialerOptions(C.TypeShadowTLS, []string{N.NetworkTCP}, tag, options.DialerOptions),
 	}
 	if options.TLS == nil || !options.TLS.Enabled {
 		return nil, C.ErrTLSRequired
@@ -91,9 +87,9 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context
 	return outbound, nil
 }
 
-func (h *ShadowTLS) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	switch N.NetworkName(network) {
 	case N.NetworkTCP:
@@ -103,6 +99,6 @@ func (h *ShadowTLS) DialContext(ctx context.Context, network string, destination
 	}
 }
 
-func (h *ShadowTLS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	return nil, os.ErrInvalid
 }

+ 27 - 24
outbound/socks.go → protocol/socks/outbound.go

@@ -1,10 +1,11 @@
-package outbound
+package socks
 
 import (
 	"context"
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
@@ -12,22 +13,29 @@ import (
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing/common"
 	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"
 	"github.com/sagernet/sing/common/uot"
 	"github.com/sagernet/sing/protocol/socks"
 )
 
-var _ adapter.Outbound = (*Socks)(nil)
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.SOCKSOutboundOptions](registry, C.TypeSOCKS, NewOutbound)
+}
+
+var _ adapter.Outbound = (*Outbound)(nil)
 
-type Socks struct {
-	myOutboundAdapter
+type Outbound struct {
+	outbound.Adapter
+	router    adapter.Router
+	logger    logger.ContextLogger
 	client    *socks.Client
 	resolve   bool
 	uotClient *uot.Client
 }
 
-func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, options option.SocksOutboundOptions) (*Socks, error) {
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SOCKSOutboundOptions) (adapter.Outbound, error) {
 	var version socks.Version
 	var err error
 	if options.Version != "" {
@@ -42,15 +50,10 @@ func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, optio
 	if err != nil {
 		return nil, err
 	}
-	outbound := &Socks{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeSOCKS,
-			network:      options.Network.Build(),
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
+	outbound := &Outbound{
+		Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSOCKS, options.Network.Build(), tag, options.DialerOptions),
+		router:  router,
+		logger:  logger,
 		client:  socks.NewClient(outboundDialer, options.ServerOptions.Build(), version, options.Username, options.Password),
 		resolve: version == socks.Version4,
 	}
@@ -64,9 +67,9 @@ func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, optio
 	return outbound, nil
 }
 
-func (h *Socks) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	switch N.NetworkName(network) {
 	case N.NetworkTCP:
@@ -90,9 +93,9 @@ func (h *Socks) DialContext(ctx context.Context, network string, destination M.S
 	return h.client.DialContext(ctx, network, destination)
 }
 
-func (h *Socks) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	if h.uotClient != nil {
 		h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
@@ -115,20 +118,20 @@ func (h *Socks) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.
 
 // TODO
 // Deprecated
-func (h *Socks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	if h.resolve {
-		return NewDirectConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4)
+		return outbound.NewDirectConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4)
 	} else {
-		return NewConnection(ctx, h, conn, metadata)
+		return outbound.NewConnection(ctx, h, conn, metadata)
 	}
 }
 
 // TODO
 // Deprecated
-func (h *Socks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	if h.resolve {
-		return NewDirectPacketConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4)
+		return outbound.NewDirectPacketConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4)
 	} else {
-		return NewPacketConnection(ctx, h, conn, metadata)
+		return outbound.NewPacketConnection(ctx, h, conn, metadata)
 	}
 }

+ 20 - 22
outbound/ssh.go → protocol/ssh/outbound.go

@@ -1,4 +1,4 @@
-package outbound
+package ssh
 
 import (
 	"bytes"
@@ -12,26 +12,30 @@ import (
 	"sync"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common"
 	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"
 
 	"golang.org/x/crypto/ssh"
 )
 
-var (
-	_ adapter.Outbound                = (*SSH)(nil)
-	_ adapter.InterfaceUpdateListener = (*SSH)(nil)
-)
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.SSHOutboundOptions](registry, C.TypeSSH, NewOutbound)
+}
 
-type SSH struct {
-	myOutboundAdapter
+var _ adapter.InterfaceUpdateListener = (*Outbound)(nil)
+
+type Outbound struct {
+	outbound.Adapter
 	ctx               context.Context
+	logger            logger.ContextLogger
 	dialer            N.Dialer
 	serverAddr        M.Socksaddr
 	user              string
@@ -44,21 +48,15 @@ type SSH struct {
 	client            *ssh.Client
 }
 
-func NewSSH(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHOutboundOptions) (*SSH, error) {
+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)
 	if err != nil {
 		return nil, err
 	}
-	outbound := &SSH{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeSSH,
-			network:      []string{N.NetworkTCP},
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
+	outbound := &Outbound{
+		Adapter:           outbound.NewAdapterWithDialerOptions(C.TypeSSH, []string{N.NetworkTCP}, tag, options.DialerOptions),
 		ctx:               ctx,
+		logger:            logger,
 		dialer:            outboundDialer,
 		serverAddr:        options.ServerOptions.Build(),
 		user:              options.User,
@@ -122,7 +120,7 @@ func randomVersion() string {
 	return version
 }
 
-func (s *SSH) connect() (*ssh.Client, error) {
+func (s *Outbound) connect() (*ssh.Client, error) {
 	if s.client != nil {
 		return s.client, nil
 	}
@@ -179,16 +177,16 @@ func (s *SSH) connect() (*ssh.Client, error) {
 	return client, nil
 }
 
-func (s *SSH) InterfaceUpdated() {
+func (s *Outbound) InterfaceUpdated() {
 	common.Close(s.clientConn)
 	return
 }
 
-func (s *SSH) Close() error {
+func (s *Outbound) Close() error {
 	return common.Close(s.clientConn)
 }
 
-func (s *SSH) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (s *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	client, err := s.connect()
 	if err != nil {
 		return nil, err
@@ -196,6 +194,6 @@ func (s *SSH) DialContext(ctx context.Context, network string, destination M.Soc
 	return client.Dial(network, destination.String())
 }
 
-func (s *SSH) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (s *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	return nil, os.ErrInvalid
 }

+ 20 - 21
outbound/tor.go → protocol/tor/outbound.go

@@ -1,4 +1,4 @@
-package outbound
+package tor
 
 import (
 	"context"
@@ -8,6 +8,7 @@ import (
 	"strings"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
@@ -15,6 +16,7 @@ import (
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
+	"github.com/sagernet/sing/common/logger"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/rw"
@@ -24,11 +26,14 @@ import (
 	"github.com/cretz/bine/tor"
 )
 
-var _ adapter.Outbound = (*Tor)(nil)
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.TorOutboundOptions](registry, C.TypeTor, NewOutbound)
+}
 
-type Tor struct {
-	myOutboundAdapter
+type Outbound struct {
+	outbound.Adapter
 	ctx         context.Context
+	logger      logger.ContextLogger
 	proxy       *ProxyListener
 	startConf   *tor.StartConf
 	options     map[string]string
@@ -37,8 +42,8 @@ type Tor struct {
 	socksClient *socks.Client
 }
 
-func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TorOutboundOptions) (*Tor, error) {
-	startConf := newConfig()
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TorOutboundOptions) (adapter.Outbound, error) {
+	var startConf tor.StartConf
 	startConf.DataDir = os.ExpandEnv(options.DataDirectory)
 	startConf.TempDataDirBase = os.TempDir()
 	startConf.ExtraArgs = options.ExtraArgs
@@ -74,23 +79,17 @@ func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger
 	if err != nil {
 		return nil, err
 	}
-	return &Tor{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeTor,
-			network:      []string{N.NetworkTCP},
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
+	return &Outbound{
+		Adapter:   outbound.NewAdapterWithDialerOptions(C.TypeTor, []string{N.NetworkTCP}, tag, options.DialerOptions),
 		ctx:       ctx,
+		logger:    logger,
 		proxy:     NewProxyListener(ctx, logger, outboundDialer),
 		startConf: &startConf,
 		options:   options.Options,
 	}, nil
 }
 
-func (t *Tor) Start() error {
+func (t *Outbound) Start() error {
 	err := t.start()
 	if err != nil {
 		t.Close()
@@ -106,7 +105,7 @@ var torLogEvents = []control.EventCode{
 	control.EventCodeLogWarn,
 }
 
-func (t *Tor) start() error {
+func (t *Outbound) start() error {
 	torInstance, err := tor.Start(t.ctx, t.startConf)
 	if err != nil {
 		return E.New(strings.ToLower(err.Error()))
@@ -168,7 +167,7 @@ func (t *Tor) start() error {
 	return nil
 }
 
-func (t *Tor) recvLoop() {
+func (t *Outbound) recvLoop() {
 	for rawEvent := range t.events {
 		switch event := rawEvent.(type) {
 		case *control.LogEvent:
@@ -191,7 +190,7 @@ func (t *Tor) recvLoop() {
 	}
 }
 
-func (t *Tor) Close() error {
+func (t *Outbound) Close() error {
 	err := common.Close(
 		common.PtrOrNil(t.proxy),
 		common.PtrOrNil(t.instance),
@@ -203,11 +202,11 @@ func (t *Tor) Close() error {
 	return err
 }
 
-func (t *Tor) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (t *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	t.logger.InfoContext(ctx, "outbound connection to ", destination)
 	return t.socksClient.DialContext(ctx, network, destination)
 }
 
-func (t *Tor) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (t *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	return nil, os.ErrInvalid
 }

+ 4 - 3
outbound/proxy.go → protocol/tor/proxy.go

@@ -1,4 +1,4 @@
-package outbound
+package tor
 
 import (
 	"context"
@@ -7,6 +7,7 @@ import (
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/auth"
@@ -106,7 +107,7 @@ func (l *ProxyListener) NewConnection(ctx context.Context, conn net.Conn, upstre
 	metadata.Network = N.NetworkTCP
 	metadata.Destination = upstreamMetadata.Destination
 	l.logger.InfoContext(ctx, "proxy connection to ", metadata.Destination)
-	return NewConnection(ctx, l.dialer, conn, metadata)
+	return outbound.NewConnection(ctx, l.dialer, conn, metadata)
 }
 
 func (l *ProxyListener) NewPacketConnection(ctx context.Context, conn N.PacketConn, upstreamMetadata M.Metadata) error {
@@ -114,5 +115,5 @@ func (l *ProxyListener) NewPacketConnection(ctx context.Context, conn N.PacketCo
 	metadata.Network = N.NetworkUDP
 	metadata.Destination = upstreamMetadata.Destination
 	l.logger.InfoContext(ctx, "proxy packet connection to ", metadata.Destination)
-	return NewPacketConnection(ctx, l.dialer, conn, metadata)
+	return outbound.NewPacketConnection(ctx, l.dialer, conn, metadata)
 }

+ 19 - 20
outbound/trojan.go → protocol/trojan/outbound.go

@@ -1,10 +1,11 @@
-package outbound
+package trojan
 
 import (
 	"context"
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/tls"
@@ -16,14 +17,18 @@ import (
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
 	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"
 )
 
-var _ adapter.Outbound = (*Trojan)(nil)
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.TrojanOutboundOptions](registry, C.TypeTrojan, NewOutbound)
+}
 
-type Trojan struct {
-	myOutboundAdapter
+type Outbound struct {
+	outbound.Adapter
+	logger          logger.ContextLogger
 	dialer          N.Dialer
 	serverAddr      M.Socksaddr
 	key             [56]byte
@@ -32,20 +37,14 @@ type Trojan struct {
 	transport       adapter.V2RayClientTransport
 }
 
-func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanOutboundOptions) (*Trojan, error) {
+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)
 	if err != nil {
 		return nil, err
 	}
-	outbound := &Trojan{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeTrojan,
-			network:      options.Network.Build(),
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
+	outbound := &Outbound{
+		Adapter:    outbound.NewAdapterWithDialerOptions(C.TypeTrojan, options.Network.Build(), tag, options.DialerOptions),
+		logger:     logger,
 		dialer:     outboundDialer,
 		serverAddr: options.ServerOptions.Build(),
 		key:        trojan.Key(options.Password),
@@ -69,7 +68,7 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog
 	return outbound, nil
 }
 
-func (h *Trojan) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	if h.multiplexDialer == nil {
 		switch N.NetworkName(network) {
 		case N.NetworkTCP:
@@ -89,7 +88,7 @@ func (h *Trojan) DialContext(ctx context.Context, network string, destination M.
 	}
 }
 
-func (h *Trojan) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	if h.multiplexDialer == nil {
 		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 		return (*trojanDialer)(h).ListenPacket(ctx, destination)
@@ -99,7 +98,7 @@ func (h *Trojan) ListenPacket(ctx context.Context, destination M.Socksaddr) (net
 	}
 }
 
-func (h *Trojan) InterfaceUpdated() {
+func (h *Outbound) InterfaceUpdated() {
 	if h.transport != nil {
 		h.transport.Close()
 	}
@@ -109,15 +108,15 @@ func (h *Trojan) InterfaceUpdated() {
 	return
 }
 
-func (h *Trojan) Close() error {
+func (h *Outbound) Close() error {
 	return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport)
 }
 
-type trojanDialer Trojan
+type trojanDialer Outbound
 
 func (h *trojanDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	var conn net.Conn
 	var err error

+ 19 - 23
outbound/tuic.go → protocol/tuic/outbound.go

@@ -1,6 +1,4 @@
-//go:build with_quic
-
-package outbound
+package tuic
 
 import (
 	"context"
@@ -9,6 +7,7 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
@@ -18,6 +17,7 @@ import (
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
 	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"
 	"github.com/sagernet/sing/common/uot"
@@ -25,18 +25,20 @@ import (
 	"github.com/gofrs/uuid/v5"
 )
 
-var (
-	_ adapter.Outbound                = (*TUIC)(nil)
-	_ adapter.InterfaceUpdateListener = (*TUIC)(nil)
-)
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.TUICOutboundOptions](registry, C.TypeTUIC, NewOutbound)
+}
+
+var _ adapter.InterfaceUpdateListener = (*Outbound)(nil)
 
-type TUIC struct {
-	myOutboundAdapter
+type Outbound struct {
+	outbound.Adapter
+	logger    logger.ContextLogger
 	client    *tuic.Client
 	udpStream bool
 }
 
-func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (*TUIC, error) {
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (adapter.Outbound, error) {
 	options.UDPFragmentDefault = true
 	if options.TLS == nil || !options.TLS.Enabled {
 		return nil, C.ErrTLSRequired
@@ -77,21 +79,15 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
 	if err != nil {
 		return nil, err
 	}
-	return &TUIC{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeTUIC,
-			network:      options.Network.Build(),
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
+	return &Outbound{
+		Adapter:   outbound.NewAdapterWithDialerOptions(C.TypeTUIC, options.Network.Build(), tag, options.DialerOptions),
+		logger:    logger,
 		client:    client,
 		udpStream: options.UDPOverStream,
 	}, nil
 }
 
-func (h *TUIC) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	switch N.NetworkName(network) {
 	case N.NetworkTCP:
 		h.logger.InfoContext(ctx, "outbound connection to ", destination)
@@ -119,7 +115,7 @@ func (h *TUIC) DialContext(ctx context.Context, network string, destination M.So
 	}
 }
 
-func (h *TUIC) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	if h.udpStream {
 		h.logger.InfoContext(ctx, "outbound stream packet connection to ", destination)
 		streamConn, err := h.client.DialConn(ctx, uot.RequestDestination(uot.Version))
@@ -136,10 +132,10 @@ func (h *TUIC) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.P
 	}
 }
 
-func (h *TUIC) InterfaceUpdated() {
+func (h *Outbound) InterfaceUpdated() {
 	_ = h.client.CloseWithError(E.New("network changed"))
 }
 
-func (h *TUIC) Close() error {
+func (h *Outbound) Close() error {
 	return h.client.CloseWithError(os.ErrClosed)
 }

+ 20 - 21
outbound/vless.go → protocol/vless/outbound.go

@@ -1,10 +1,11 @@
-package outbound
+package vless
 
 import (
 	"context"
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/tls"
@@ -17,14 +18,18 @@ import (
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
 	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"
 )
 
-var _ adapter.Outbound = (*VLESS)(nil)
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.VLESSOutboundOptions](registry, C.TypeVLESS, NewOutbound)
+}
 
-type VLESS struct {
-	myOutboundAdapter
+type Outbound struct {
+	outbound.Adapter
+	logger          logger.ContextLogger
 	dialer          N.Dialer
 	client          *vless.Client
 	serverAddr      M.Socksaddr
@@ -35,20 +40,14 @@ type VLESS struct {
 	xudp            bool
 }
 
-func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (*VLESS, error) {
+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)
 	if err != nil {
 		return nil, err
 	}
-	outbound := &VLESS{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeVLESS,
-			network:      options.Network.Build(),
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
+	outbound := &Outbound{
+		Adapter:    outbound.NewAdapterWithDialerOptions(C.TypeVLESS, options.Network.Build(), tag, options.DialerOptions),
+		logger:     logger,
 		dialer:     outboundDialer,
 		serverAddr: options.ServerOptions.Build(),
 	}
@@ -88,7 +87,7 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
 	return outbound, nil
 }
 
-func (h *VLESS) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	if h.multiplexDialer == nil {
 		switch N.NetworkName(network) {
 		case N.NetworkTCP:
@@ -108,7 +107,7 @@ func (h *VLESS) DialContext(ctx context.Context, network string, destination M.S
 	}
 }
 
-func (h *VLESS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	if h.multiplexDialer == nil {
 		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 		return (*vlessDialer)(h).ListenPacket(ctx, destination)
@@ -118,7 +117,7 @@ func (h *VLESS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.
 	}
 }
 
-func (h *VLESS) InterfaceUpdated() {
+func (h *Outbound) InterfaceUpdated() {
 	if h.transport != nil {
 		h.transport.Close()
 	}
@@ -128,15 +127,15 @@ func (h *VLESS) InterfaceUpdated() {
 	return
 }
 
-func (h *VLESS) Close() error {
+func (h *Outbound) Close() error {
 	return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport)
 }
 
-type vlessDialer VLESS
+type vlessDialer Outbound
 
 func (h *vlessDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	var conn net.Conn
 	var err error
@@ -179,7 +178,7 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
 func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	var conn net.Conn
 	var err error

+ 20 - 21
outbound/vmess.go → protocol/vmess/vmess.go

@@ -1,10 +1,11 @@
-package outbound
+package vmess
 
 import (
 	"context"
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/tls"
@@ -16,15 +17,19 @@ import (
 	"github.com/sagernet/sing-vmess/packetaddr"
 	"github.com/sagernet/sing/common"
 	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"
 	"github.com/sagernet/sing/common/ntp"
 )
 
-var _ adapter.Outbound = (*VMess)(nil)
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.VMessOutboundOptions](registry, C.TypeVMess, NewOutbound)
+}
 
-type VMess struct {
-	myOutboundAdapter
+type Outbound struct {
+	outbound.Adapter
+	logger          logger.ContextLogger
 	dialer          N.Dialer
 	client          *vmess.Client
 	serverAddr      M.Socksaddr
@@ -35,20 +40,14 @@ type VMess struct {
 	xudp            bool
 }
 
-func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessOutboundOptions) (*VMess, error) {
+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)
 	if err != nil {
 		return nil, err
 	}
-	outbound := &VMess{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeVMess,
-			network:      options.Network.Build(),
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
+	outbound := &Outbound{
+		Adapter:    outbound.NewAdapterWithDialerOptions(C.TypeVMess, options.Network.Build(), tag, options.DialerOptions),
+		logger:     logger,
 		dialer:     outboundDialer,
 		serverAddr: options.ServerOptions.Build(),
 	}
@@ -102,7 +101,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
 	return outbound, nil
 }
 
-func (h *VMess) InterfaceUpdated() {
+func (h *Outbound) InterfaceUpdated() {
 	if h.transport != nil {
 		h.transport.Close()
 	}
@@ -112,11 +111,11 @@ func (h *VMess) InterfaceUpdated() {
 	return
 }
 
-func (h *VMess) Close() error {
+func (h *Outbound) Close() error {
 	return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport)
 }
 
-func (h *VMess) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	if h.multiplexDialer == nil {
 		switch N.NetworkName(network) {
 		case N.NetworkTCP:
@@ -136,7 +135,7 @@ func (h *VMess) DialContext(ctx context.Context, network string, destination M.S
 	}
 }
 
-func (h *VMess) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	if h.multiplexDialer == nil {
 		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 		return (*vmessDialer)(h).ListenPacket(ctx, destination)
@@ -146,11 +145,11 @@ func (h *VMess) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.
 	}
 }
 
-type vmessDialer VMess
+type vmessDialer Outbound
 
 func (h *vmessDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	var conn net.Conn
 	var err error
@@ -178,7 +177,7 @@ func (h *vmessDialer) DialContext(ctx context.Context, network string, destinati
 
 func (h *vmessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
-	metadata.Outbound = h.tag
+	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
 	var conn net.Conn
 	var err error

+ 10 - 0
protocol/wireguard/init.go

@@ -0,0 +1,10 @@
+package wireguard
+
+import (
+	"github.com/sagernet/sing-box/common/dialer"
+	"github.com/sagernet/wireguard-go/conn"
+)
+
+func init() {
+	dialer.WgControlFns = conn.ControlFns
+}

+ 30 - 32
outbound/wireguard.go → protocol/wireguard/outbound.go

@@ -1,6 +1,4 @@
-//go:build with_wireguard
-
-package outbound
+package wireguard
 
 import (
 	"context"
@@ -12,6 +10,7 @@ import (
 	"strings"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/dialer"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
@@ -21,6 +20,7 @@ import (
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common"
 	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"
 	"github.com/sagernet/sing/common/x/list"
@@ -30,14 +30,17 @@ import (
 	"github.com/sagernet/wireguard-go/device"
 )
 
-var (
-	_ adapter.Outbound                = (*WireGuard)(nil)
-	_ adapter.InterfaceUpdateListener = (*WireGuard)(nil)
-)
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.WireGuardOutboundOptions](registry, C.TypeWireGuard, NewOutbound)
+}
 
-type WireGuard struct {
-	myOutboundAdapter
+var _ adapter.InterfaceUpdateListener = (*Outbound)(nil)
+
+type Outbound struct {
+	outbound.Adapter
 	ctx           context.Context
+	router        adapter.Router
+	logger        logger.ContextLogger
 	workers       int
 	peers         []wireguard.PeerConfig
 	useStdNetBind bool
@@ -51,17 +54,12 @@ type WireGuard struct {
 	tunDevice     wireguard.Device
 }
 
-func NewWireGuard(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardOutboundOptions) (*WireGuard, error) {
-	outbound := &WireGuard{
-		myOutboundAdapter: myOutboundAdapter{
-			protocol:     C.TypeWireGuard,
-			network:      options.Network.Build(),
-			router:       router,
-			logger:       logger,
-			tag:          tag,
-			dependencies: withDialerDependency(options.DialerOptions),
-		},
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardOutboundOptions) (adapter.Outbound, error) {
+	outbound := &Outbound{
+		Adapter:      outbound.NewAdapterWithDialerOptions(C.TypeWireGuard, options.Network.Build(), tag, options.DialerOptions),
 		ctx:          ctx,
+		router:       router,
+		logger:       logger,
 		workers:      options.Workers,
 		pauseManager: service.FromContext[pause.Manager](ctx),
 	}
@@ -111,7 +109,7 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
 	return outbound, nil
 }
 
-func (w *WireGuard) Start() error {
+func (w *Outbound) Start() error {
 	if common.Any(w.peers, func(peer wireguard.PeerConfig) bool {
 		return !peer.Endpoint.IsValid()
 	}) {
@@ -121,7 +119,7 @@ func (w *WireGuard) Start() error {
 	return w.start()
 }
 
-func (w *WireGuard) PostStart() error {
+func (w *Outbound) PostStart() error {
 	if common.All(w.peers, func(peer wireguard.PeerConfig) bool {
 		return peer.Endpoint.IsValid()
 	}) {
@@ -130,7 +128,7 @@ func (w *WireGuard) PostStart() error {
 	return w.start()
 }
 
-func (w *WireGuard) start() error {
+func (w *Outbound) start() error {
 	err := wireguard.ResolvePeers(w.ctx, w.router, w.peers)
 	if err != nil {
 		return err
@@ -150,7 +148,7 @@ func (w *WireGuard) start() error {
 			connectAddr = w.peers[0].Endpoint
 			reserved = w.peers[0].Reserved
 		}
-		bind = wireguard.NewClientBind(w.ctx, w, w.listener, isConnect, connectAddr, reserved)
+		bind = wireguard.NewClientBind(w.ctx, w.logger, w.listener, isConnect, connectAddr, reserved)
 	}
 	err = w.tunDevice.Start()
 	if err != nil {
@@ -177,7 +175,7 @@ func (w *WireGuard) start() error {
 	return nil
 }
 
-func (w *WireGuard) Close() error {
+func (w *Outbound) Close() error {
 	if w.device != nil {
 		w.device.Close()
 	}
@@ -187,12 +185,12 @@ func (w *WireGuard) Close() error {
 	return nil
 }
 
-func (w *WireGuard) InterfaceUpdated() {
+func (w *Outbound) InterfaceUpdated() {
 	w.device.BindUpdate()
 	return
 }
 
-func (w *WireGuard) onPauseUpdated(event int) {
+func (w *Outbound) onPauseUpdated(event int) {
 	switch event {
 	case pause.EventDevicePaused:
 		w.device.Down()
@@ -201,7 +199,7 @@ func (w *WireGuard) onPauseUpdated(event int) {
 	}
 }
 
-func (w *WireGuard) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+func (w *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	switch network {
 	case N.NetworkTCP:
 		w.logger.InfoContext(ctx, "outbound connection to ", destination)
@@ -218,7 +216,7 @@ func (w *WireGuard) DialContext(ctx context.Context, network string, destination
 	return w.tunDevice.DialContext(ctx, network, destination)
 }
 
-func (w *WireGuard) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (w *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	w.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 	if destination.IsFqdn() {
 		destinationAddresses, err := w.router.LookupDefault(ctx, destination.Fqdn)
@@ -236,12 +234,12 @@ func (w *WireGuard) ListenPacket(ctx context.Context, destination M.Socksaddr) (
 
 // TODO
 // Deprecated
-func (w *WireGuard) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewDirectConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS)
+func (w *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	return outbound.NewDirectConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS)
 }
 
 // TODO
 // Deprecated
-func (w *WireGuard) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return NewDirectPacketConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS)
+func (w *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	return outbound.NewDirectPacketConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS)
 }

+ 3 - 3
route/dns.go

@@ -7,7 +7,7 @@ import (
 
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/outbound"
+	dnsOutbound "github.com/sagernet/sing-box/protocol/dns"
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing/common/buf"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -22,7 +22,7 @@ func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata ad
 	metadata.Destination = M.Socksaddr{}
 	for {
 		conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
-		err := outbound.HandleStreamDNSRequest(ctx, r, conn, metadata)
+		err := dnsOutbound.HandleStreamDNSRequest(ctx, r, conn, metadata)
 		if err != nil {
 			return err
 		}
@@ -46,7 +46,7 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
 		})
 		return
 	}
-	err := outbound.NewDNSPacketConnection(ctx, r, conn, packetBuffers, metadata)
+	err := dnsOutbound.NewDNSPacketConnection(ctx, r, conn, packetBuffers, metadata)
 	if err != nil && !E.IsClosedOrCanceled(err) {
 		r.dnsLogger.ErrorContext(ctx, E.Cause(err, "process packet connection"))
 	}

+ 1 - 1
route/route.go

@@ -12,12 +12,12 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/common/conntrack"
 	"github.com/sagernet/sing-box/common/process"
 	"github.com/sagernet/sing-box/common/sniff"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-box/outbound"
 	"github.com/sagernet/sing-box/route/rule"
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing-mux"

+ 4 - 4
test/brutal_test.go

@@ -46,7 +46,7 @@ func TestBrutalShadowsocks(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -138,7 +138,7 @@ func TestBrutalTrojan(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -227,7 +227,7 @@ func TestBrutalVMess(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -326,7 +326,7 @@ func TestBrutalVLESS(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 1 - 1
test/direct_test.go

@@ -33,7 +33,7 @@ func _TestProxyProtocol(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 1 - 1
test/domain_inbound_test.go

@@ -49,7 +49,7 @@ func TestTUICDomainUDP(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 3 - 3
test/ech_test.go

@@ -55,7 +55,7 @@ func TestECH(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -145,7 +145,7 @@ func TestECHQUIC(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -235,7 +235,7 @@ func TestECHHysteria2(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 1 - 1
test/http_test.go

@@ -31,7 +31,7 @@ func TestHTTPSelf(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 2 - 2
test/hysteria2_test.go

@@ -63,7 +63,7 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -178,7 +178,7 @@ func TestHysteria2Outbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeHysteria2,
 				Hysteria2Options: option.Hysteria2OutboundOptions{

+ 2 - 2
test/hysteria_test.go

@@ -46,7 +46,7 @@ func TestHysteriaSelf(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -160,7 +160,7 @@ func TestHysteriaOutbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeHysteria,
 				HysteriaOptions: option.HysteriaOutboundOptions{

+ 1 - 1
test/inbound_detour_test.go

@@ -49,7 +49,7 @@ func TestChainedInbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 2 - 2
test/mux_cool_test.go

@@ -92,7 +92,7 @@ func TestMuxCoolClient(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeVMess,
 				VMessOptions: option.VMessOutboundOptions{
@@ -139,7 +139,7 @@ func TestMuxCoolSelf(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 2 - 2
test/mux_test.go

@@ -81,7 +81,7 @@ func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -154,7 +154,7 @@ func testVMessMux(t *testing.T, options option.OutboundMultiplexOptions) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 1 - 1
test/shadowsocks_legacy_test.go

@@ -35,7 +35,7 @@ func testShadowsocksLegacy(t *testing.T, method string) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeShadowsocks,
 				ShadowsocksOptions: option.ShadowsocksOutboundOptions{

+ 4 - 4
test/shadowsocks_test.go

@@ -135,7 +135,7 @@ func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, pas
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeShadowsocks,
 				ShadowsocksOptions: option.ShadowsocksOutboundOptions{
@@ -177,7 +177,7 @@ func testShadowsocksSelf(t *testing.T, method string, password string) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -244,7 +244,7 @@ func TestShadowsocksUoT(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -317,7 +317,7 @@ func testShadowsocks2022EIH(t *testing.T, method string, password string) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 3 - 3
test/shadowtls_test.go

@@ -80,7 +80,7 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool)
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeShadowsocks,
 				ShadowsocksOptions: option.ShadowsocksOutboundOptions{
@@ -232,7 +232,7 @@ func TestShadowTLSInbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -306,7 +306,7 @@ func TestShadowTLSOutbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeShadowsocks,
 				ShadowsocksOptions: option.ShadowsocksOutboundOptions{

+ 1 - 1
test/ss_plugin_test.go

@@ -44,7 +44,7 @@ func testShadowsocksPlugin(t *testing.T, name string, opts string, args string)
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeShadowsocks,
 				ShadowsocksOptions: option.ShadowsocksOutboundOptions{

+ 1 - 1
test/tfo_test.go

@@ -37,7 +37,7 @@ func TestTCPSlowOpen(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 1 - 1
test/tls_test.go

@@ -46,7 +46,7 @@ func TestUTLS(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 3 - 3
test/trojan_test.go

@@ -31,7 +31,7 @@ func TestTrojanOutbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeTrojan,
 				TrojanOptions: option.TrojanOutboundOptions{
@@ -92,7 +92,7 @@ func TestTrojanSelf(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -167,7 +167,7 @@ func TestTrojanPlainSelf(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 2 - 2
test/tuic_test.go

@@ -62,7 +62,7 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -171,7 +171,7 @@ func TestTUICOutbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeTUIC,
 				TUICOptions: option.TUICOutboundOptions{

+ 1 - 1
test/v2ray_api_test.go

@@ -26,7 +26,7 @@ func TestV2RayAPI(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 				Tag:  "out",

+ 1 - 1
test/v2ray_grpc_test.go

@@ -138,7 +138,7 @@ func testV2RayGRPCOutbound(t *testing.T, forceLite bool) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeVMess,
 				Tag:  "vmess-out",

+ 4 - 4
test/v2ray_transport_test.go

@@ -80,7 +80,7 @@ func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions,
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -169,7 +169,7 @@ func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions,
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -260,7 +260,7 @@ func TestVMessQUICSelf(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -340,7 +340,7 @@ func testV2RayTransportNOTLSSelf(t *testing.T, transport *option.V2RayTransportO
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 1 - 1
test/v2ray_ws_test.go

@@ -170,7 +170,7 @@ func testV2RayWebsocketOutbound(t *testing.T, maxEarlyData uint32, earlyDataHead
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeVMess,
 				Tag:  "vmess-out",

+ 2 - 2
test/vmess_test.go

@@ -240,7 +240,7 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, globalPadding boo
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeVMess,
 				VMessOptions: option.VMessOutboundOptions{
@@ -291,7 +291,7 @@ func testVMessSelf(t *testing.T, security string, alterId int, globalPadding boo
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},

+ 1 - 1
test/wireguard_test.go

@@ -32,7 +32,7 @@ func _TestWireGuard(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeWireGuard,
 				WireGuardOptions: option.WireGuardOutboundOptions{

+ 6 - 5
transport/wireguard/client_bind.go

@@ -10,6 +10,7 @@ import (
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
 	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"
 	"github.com/sagernet/sing/service"
@@ -21,10 +22,10 @@ var _ conn.Bind = (*ClientBind)(nil)
 
 type ClientBind struct {
 	ctx                 context.Context
+	logger              logger.Logger
 	pauseManager        pause.Manager
 	bindCtx             context.Context
 	bindDone            context.CancelFunc
-	errorHandler        E.Handler
 	dialer              N.Dialer
 	reservedForEndpoint map[netip.AddrPort][3]uint8
 	connAccess          sync.Mutex
@@ -35,11 +36,11 @@ type ClientBind struct {
 	reserved            [3]uint8
 }
 
-func NewClientBind(ctx context.Context, errorHandler E.Handler, dialer N.Dialer, isConnect bool, connectAddr netip.AddrPort, reserved [3]uint8) *ClientBind {
+func NewClientBind(ctx context.Context, logger logger.Logger, dialer N.Dialer, isConnect bool, connectAddr netip.AddrPort, reserved [3]uint8) *ClientBind {
 	return &ClientBind{
 		ctx:                 ctx,
+		logger:              logger,
 		pauseManager:        service.FromContext[pause.Manager](ctx),
-		errorHandler:        errorHandler,
 		dialer:              dialer,
 		reservedForEndpoint: make(map[netip.AddrPort][3]uint8),
 		done:                make(chan struct{}),
@@ -115,7 +116,7 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint)
 			return
 		default:
 		}
-		c.errorHandler.NewError(context.Background(), E.Cause(err, "connect to server"))
+		c.logger.Error(E.Cause(err, "connect to server"))
 		err = nil
 		c.pauseManager.WaitActive()
 		time.Sleep(time.Second)
@@ -127,7 +128,7 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint)
 		select {
 		case <-c.done:
 		default:
-			c.errorHandler.NewError(context.Background(), E.Cause(err, "read packet"))
+			c.logger.Error(context.Background(), E.Cause(err, "read packet"))
 			err = nil
 		}
 		return