diff --git a/adapter/inbound.go b/adapter/inbound.go
index 300f57e3..ca3e9e59 100644
--- a/adapter/inbound.go
+++ b/adapter/inbound.go
@@ -5,6 +5,7 @@ import (
 	"net/netip"
 
 	"github.com/sagernet/sing-box/common/process"
+	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	M "github.com/sagernet/sing/common/metadata"
 )
@@ -25,6 +26,11 @@ type UDPInjectableInbound interface {
 	PacketConnectionHandlerEx
 }
 
+type InboundRegistry interface {
+	option.InboundOptionsRegistry
+	CreateInbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Inbound, error)
+}
+
 type InboundContext struct {
 	Inbound     string
 	InboundType string
@@ -44,6 +50,7 @@ type InboundContext struct {
 
 	// cache
 
+	// Deprecated: implement in rule action
 	InboundDetour     string
 	LastInbound       string
 	OriginDestination M.Socksaddr
diff --git a/adapter/inbound/adapter.go b/adapter/inbound/adapter.go
new file mode 100644
index 00000000..1426104a
--- /dev/null
+++ b/adapter/inbound/adapter.go
@@ -0,0 +1,21 @@
+package inbound
+
+type Adapter struct {
+	inboundType string
+	inboundTag  string
+}
+
+func NewAdapter(inboundType string, inboundTag string) Adapter {
+	return Adapter{
+		inboundType: inboundType,
+		inboundTag:  inboundTag,
+	}
+}
+
+func (a *Adapter) Type() string {
+	return a.inboundType
+}
+
+func (a *Adapter) Tag() string {
+	return a.inboundTag
+}
diff --git a/adapter/inbound/registry.go b/adapter/inbound/registry.go
new file mode 100644
index 00000000..9f678c90
--- /dev/null
+++ b/adapter/inbound/registry.go
@@ -0,0 +1,68 @@
+package inbound
+
+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.Inbound, 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.Inbound, error) {
+		return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options)))
+	})
+}
+
+var _ adapter.InboundRegistry = (*Registry)(nil)
+
+type (
+	optionsConstructorFunc func() any
+	constructorFunc        func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error)
+)
+
+type Registry struct {
+	access       sync.Mutex
+	optionsType  map[string]optionsConstructorFunc
+	constructors map[string]constructorFunc
+}
+
+func NewRegistry() *Registry {
+	return &Registry{
+		optionsType:  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.optionsType[outboundType]
+	if !loaded {
+		return nil, false
+	}
+	return optionsConstructor(), true
+}
+
+func (r *Registry) CreateInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
+	r.access.Lock()
+	defer r.access.Unlock()
+	constructor, loaded := r.constructors[outboundType]
+	if !loaded {
+		return nil, E.New("outbound type not found: " + outboundType)
+	}
+	return constructor(ctx, router, logger, tag, options)
+}
+
+func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
+	r.access.Lock()
+	defer r.access.Unlock()
+	r.optionsType[outboundType] = optionsConstructor
+	r.constructors[outboundType] = constructor
+}
diff --git a/adapter/outbound.go b/adapter/outbound.go
index 312cdf3e..df11ed61 100644
--- a/adapter/outbound.go
+++ b/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)
+}
diff --git a/adapter/outbound/adapter.go b/adapter/outbound/adapter.go
new file mode 100644
index 00000000..481bb619
--- /dev/null
+++ b/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
+}
diff --git a/outbound/default.go b/adapter/outbound/default.go
similarity index 87%
rename from outbound/default.go
rename to adapter/outbound/default.go
index a34ac97a..78b9bfd8 100644
--- a/outbound/default.go
+++ b/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)
-}
diff --git a/adapter/outbound/registry.go b/adapter/outbound/registry.go
new file mode 100644
index 00000000..f25631cf
--- /dev/null
+++ b/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
+	optionsType  map[string]optionsConstructorFunc
+	constructors map[string]constructorFunc
+}
+
+func NewRegistry() *Registry {
+	return &Registry{
+		optionsType:  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.optionsType[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.optionsType[outboundType] = optionsConstructor
+	r.constructors[outboundType] = constructor
+}
diff --git a/box.go b/box.go
index 716b1b09..84da77c0 100644
--- a/box.go
+++ b/box.go
@@ -14,10 +14,9 @@ import (
 	"github.com/sagernet/sing-box/experimental"
 	"github.com/sagernet/sing-box/experimental/cachefile"
 	"github.com/sagernet/sing-box/experimental/libbox/platform"
-	"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"
@@ -44,16 +43,37 @@ type Box struct {
 type Options struct {
 	option.Options
 	Context           context.Context
-	PlatformInterface platform.Interface
 	PlatformLogWriter log.PlatformWriter
 }
 
+func Context(ctx context.Context, inboundRegistry adapter.InboundRegistry, outboundRegistry adapter.OutboundRegistry) context.Context {
+	if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
+		service.FromContext[adapter.InboundRegistry](ctx) == nil {
+		ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
+		ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)
+	}
+	if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil ||
+		service.FromContext[adapter.OutboundRegistry](ctx) == nil {
+		ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry)
+		ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
+	}
+	return ctx
+}
+
 func New(options Options) (*Box, error) {
 	createdAt := time.Now()
 	ctx := options.Context
 	if ctx == nil {
 		ctx = context.Background()
 	}
+	inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
+	if inboundRegistry == nil {
+		return nil, E.New("missing inbound registry in context")
+	}
+	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)
@@ -70,8 +90,9 @@ func New(options Options) (*Box, error) {
 	if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
 		needV2RayAPI = true
 	}
+	platformInterface := service.FromContext[platform.Interface](ctx)
 	var defaultLogWriter io.Writer
-	if options.PlatformInterface != nil {
+	if platformInterface != nil {
 		defaultLogWriter = io.Discard
 	}
 	logFactory, err := log.New(log.Options{
@@ -92,64 +113,92 @@ func New(options Options) (*Box, error) {
 		common.PtrValueOrDefault(options.DNS),
 		common.PtrValueOrDefault(options.NTP),
 		options.Inbounds,
-		options.PlatformInterface,
 	)
 	if err != nil {
 		return nil, E.Cause(err, "parse route options")
 	}
+	//nolint:staticcheck
+	if len(options.LegacyInbounds) > 0 {
+		for _, legacyInbound := range options.LegacyInbounds {
+			options.Inbounds = append(options.Inbounds, option.Inbound{
+				Type:    legacyInbound.Type,
+				Tag:     legacyInbound.Tag,
+				Options: common.Must1(legacyInbound.RawOptions()),
+			})
+		}
+	}
 	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
+		var currentInbound adapter.Inbound
 		var tag string
 		if inboundOptions.Tag != "" {
 			tag = inboundOptions.Tag
 		} else {
 			tag = F.ToString(i)
 		}
-		in, err = inbound.New(
+		currentInbound, err = inboundRegistry.CreateInbound(
 			ctx,
 			router,
 			logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
 			tag,
-			inboundOptions,
-			options.PlatformInterface,
+			inboundOptions.Type,
+			inboundOptions.Options,
 		)
 		if err != nil {
 			return nil, E.Cause(err, "parse inbound[", i, "]")
 		}
-		inbounds = append(inbounds, in)
+		inbounds = append(inbounds, currentInbound)
 	}
 	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
 	}
-	if options.PlatformInterface != nil {
-		err = options.PlatformInterface.Initialize(ctx, router)
+	if platformInterface != nil {
+		err = platformInterface.Initialize(ctx, router)
 		if err != nil {
 			return nil, E.Cause(err, "initialize platform interface")
 		}
diff --git a/cmd/sing-box/cmd.go b/cmd/sing-box/cmd.go
index a2d4a00b..dc7a8309 100644
--- a/cmd/sing-box/cmd.go
+++ b/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.NewStderrManager(log.StdLogger()))
+	globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry())
 }
diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go
index fc47c5a8..9856c763 100644
--- a/cmd/sing-box/cmd_format.go
+++ b/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
 		}
diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go
index 10dd38a1..fa194ed3 100644
--- a/cmd/sing-box/cmd_merge.go
+++ b/cmd/sing-box/cmd_merge.go
@@ -68,29 +68,19 @@ func merge(outputPath string) error {
 }
 
 func mergePathResources(options *option.Options) error {
-	for index, inbound := range options.Inbounds {
-		rawOptions, err := inbound.RawOptions()
-		if err != nil {
-			return err
-		}
-		if tlsOptions, containsTLSOptions := rawOptions.(option.InboundTLSOptionsWrapper); containsTLSOptions {
+	for _, inbound := range options.Inbounds {
+		if tlsOptions, containsTLSOptions := inbound.Options.(option.InboundTLSOptionsWrapper); containsTLSOptions {
 			tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions()))
 		}
-		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 +128,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 {
diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go
index 6850cd19..f31db9dc 100644
--- a/cmd/sing-box/cmd_run.go
+++ b/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")
 	}
diff --git a/common/dialer/default.go b/common/dialer/default.go
index b4bca55e..b8a0d5f4 100644
--- a/common/dialer/default.go
+++ b/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)
 		}
 	}
diff --git a/common/dialer/wireguard.go b/common/dialer/wireguard.go
index 195133c6..fbd323d8 100644
--- a/common/dialer/wireguard.go
+++ b/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
diff --git a/common/dialer/wireguard_control.go b/common/dialer/wireguard_control.go
deleted file mode 100644
index def86411..00000000
--- a/common/dialer/wireguard_control.go
+++ /dev/null
@@ -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
diff --git a/common/dialer/wiregurad_stub.go b/common/dialer/wiregurad_stub.go
deleted file mode 100644
index d30c223a..00000000
--- a/common/dialer/wiregurad_stub.go
+++ /dev/null
@@ -1,9 +0,0 @@
-//go:build !with_wireguard
-
-package dialer
-
-import (
-	"github.com/sagernet/sing/common/control"
-)
-
-var wgControlFns []control.Func
diff --git a/common/listener/listener.go b/common/listener/listener.go
new file mode 100644
index 00000000..b42b0434
--- /dev/null
+++ b/common/listener/listener.go
@@ -0,0 +1,136 @@
+package listener
+
+import (
+	"context"
+	"net"
+	"sync/atomic"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/settings"
+	"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"
+)
+
+type Listener struct {
+	ctx                      context.Context
+	logger                   logger.ContextLogger
+	network                  []string
+	listenOptions            option.ListenOptions
+	connHandler              adapter.ConnectionHandlerEx
+	packetHandler            adapter.PacketHandlerEx
+	oobPacketHandler         adapter.OOBPacketHandlerEx
+	threadUnsafePacketWriter bool
+	disablePacketOutput      bool
+	setSystemProxy           bool
+	systemProxySOCKS         bool
+
+	tcpListener          net.Listener
+	systemProxy          settings.SystemProxy
+	udpConn              *net.UDPConn
+	udpAddr              M.Socksaddr
+	packetOutbound       chan *N.PacketBuffer
+	packetOutboundClosed chan struct{}
+	shutdown             atomic.Bool
+}
+
+type Options struct {
+	Context                  context.Context
+	Logger                   logger.ContextLogger
+	Network                  []string
+	Listen                   option.ListenOptions
+	ConnectionHandler        adapter.ConnectionHandlerEx
+	PacketHandler            adapter.PacketHandlerEx
+	OOBPacketHandler         adapter.OOBPacketHandlerEx
+	ThreadUnsafePacketWriter bool
+	DisablePacketOutput      bool
+	SetSystemProxy           bool
+	SystemProxySOCKS         bool
+}
+
+func New(
+	options Options,
+) *Listener {
+	return &Listener{
+		ctx:                      options.Context,
+		logger:                   options.Logger,
+		network:                  options.Network,
+		listenOptions:            options.Listen,
+		connHandler:              options.ConnectionHandler,
+		packetHandler:            options.PacketHandler,
+		oobPacketHandler:         options.OOBPacketHandler,
+		threadUnsafePacketWriter: options.ThreadUnsafePacketWriter,
+		disablePacketOutput:      options.DisablePacketOutput,
+		setSystemProxy:           options.SetSystemProxy,
+		systemProxySOCKS:         options.SystemProxySOCKS,
+	}
+}
+
+func (l *Listener) Start() error {
+	if common.Contains(l.network, N.NetworkTCP) {
+		_, err := l.ListenTCP()
+		if err != nil {
+			return err
+		}
+		go l.loopTCPIn()
+	}
+	if common.Contains(l.network, N.NetworkUDP) {
+		_, err := l.ListenUDP()
+		if err != nil {
+			return err
+		}
+		l.packetOutboundClosed = make(chan struct{})
+		l.packetOutbound = make(chan *N.PacketBuffer, 64)
+		go l.loopUDPIn()
+		if !l.disablePacketOutput {
+			go l.loopUDPOut()
+		}
+	}
+	if l.setSystemProxy {
+		listenPort := M.SocksaddrFromNet(l.tcpListener.Addr()).Port
+		var listenAddrString string
+		listenAddr := l.listenOptions.Listen.Build()
+		if listenAddr.IsUnspecified() {
+			listenAddrString = "127.0.0.1"
+		} else {
+			listenAddrString = listenAddr.String()
+		}
+		systemProxy, err := settings.NewSystemProxy(l.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), l.systemProxySOCKS)
+		if err != nil {
+			return E.Cause(err, "initialize system proxy")
+		}
+		err = systemProxy.Enable()
+		if err != nil {
+			return E.Cause(err, "set system proxy")
+		}
+		l.systemProxy = systemProxy
+	}
+	return nil
+}
+
+func (l *Listener) Close() error {
+	l.shutdown.Store(true)
+	var err error
+	if l.systemProxy != nil && l.systemProxy.IsEnabled() {
+		err = l.systemProxy.Disable()
+	}
+	return E.Errors(err, common.Close(
+		l.tcpListener,
+		common.PtrOrNil(l.udpConn),
+	))
+}
+
+func (l *Listener) TCPListener() net.Listener {
+	return l.tcpListener
+}
+
+func (l *Listener) UDPConn() *net.UDPConn {
+	return l.udpConn
+}
+
+func (l *Listener) ListenOptions() option.ListenOptions {
+	return l.listenOptions
+}
diff --git a/inbound/default_tcp_go1.21.go b/common/listener/listener_go121.go
similarity index 90%
rename from inbound/default_tcp_go1.21.go
rename to common/listener/listener_go121.go
index 906818cb..5af1b05a 100644
--- a/inbound/default_tcp_go1.21.go
+++ b/common/listener/listener_go121.go
@@ -1,6 +1,6 @@
 //go:build go1.21
 
-package inbound
+package listener
 
 import "net"
 
diff --git a/common/listener/listener_go123.go b/common/listener/listener_go123.go
new file mode 100644
index 00000000..2e1f4cf4
--- /dev/null
+++ b/common/listener/listener_go123.go
@@ -0,0 +1,16 @@
+//go:build go1.23
+
+package listener
+
+import (
+	"net"
+	"time"
+)
+
+func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) {
+	listener.KeepAliveConfig = net.KeepAliveConfig{
+		Enable:   true,
+		Idle:     idle,
+		Interval: interval,
+	}
+}
diff --git a/inbound/default_tcp_nongo1.21.go b/common/listener/listener_nongo121.go
similarity index 87%
rename from inbound/default_tcp_nongo1.21.go
rename to common/listener/listener_nongo121.go
index d19adb19..36073afe 100644
--- a/inbound/default_tcp_nongo1.21.go
+++ b/common/listener/listener_nongo121.go
@@ -1,6 +1,6 @@
 //go:build !go1.21
 
-package inbound
+package listener
 
 import "net"
 
diff --git a/common/listener/listener_nongo123.go b/common/listener/listener_nongo123.go
new file mode 100644
index 00000000..e1582981
--- /dev/null
+++ b/common/listener/listener_nongo123.go
@@ -0,0 +1,15 @@
+//go:build !go1.23
+
+package listener
+
+import (
+	"net"
+	"time"
+
+	"github.com/sagernet/sing/common/control"
+)
+
+func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) {
+	listener.KeepAlive = idle
+	listener.Control = control.Append(listener.Control, control.SetKeepAlivePeriod(idle, interval))
+}
diff --git a/common/listener/listener_tcp.go b/common/listener/listener_tcp.go
new file mode 100644
index 00000000..02cef3f0
--- /dev/null
+++ b/common/listener/listener_tcp.go
@@ -0,0 +1,85 @@
+package listener
+
+import (
+	"net"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+
+	"github.com/metacubex/tfo-go"
+)
+
+func (l *Listener) ListenTCP() (net.Listener, error) {
+	var err error
+	bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort)
+	var tcpListener net.Listener
+	var listenConfig net.ListenConfig
+	if l.listenOptions.TCPKeepAlive >= 0 {
+		keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
+		if keepIdle == 0 {
+			keepIdle = C.TCPKeepAliveInitial
+		}
+		keepInterval := time.Duration(l.listenOptions.TCPKeepAliveInterval)
+		if keepInterval == 0 {
+			keepInterval = C.TCPKeepAliveInterval
+		}
+		setKeepAliveConfig(&listenConfig, keepIdle, keepInterval)
+	}
+	if l.listenOptions.TCPMultiPath {
+		if !go121Available {
+			return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
+		}
+		setMultiPathTCP(&listenConfig)
+	}
+	if l.listenOptions.TCPFastOpen {
+		var tfoConfig tfo.ListenConfig
+		tfoConfig.ListenConfig = listenConfig
+		tcpListener, err = tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
+	} else {
+		tcpListener, err = listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
+	}
+	if err == nil {
+		l.logger.Info("tcp server started at ", tcpListener.Addr())
+	}
+	//nolint:staticcheck
+	if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
+		return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
+	}
+	l.tcpListener = tcpListener
+	return tcpListener, err
+}
+
+func (l *Listener) loopTCPIn() {
+	tcpListener := l.tcpListener
+	var metadata adapter.InboundContext
+	for {
+		conn, err := tcpListener.Accept()
+		if err != nil {
+			//nolint:staticcheck
+			if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() {
+				l.logger.Error(err)
+				continue
+			}
+			if l.shutdown.Load() && E.IsClosed(err) {
+				return
+			}
+			l.tcpListener.Close()
+			l.logger.Error("tcp listener closed: ", err)
+			continue
+		}
+		//nolint:staticcheck
+		metadata.InboundDetour = l.listenOptions.Detour
+		//nolint:staticcheck
+		metadata.InboundOptions = l.listenOptions.InboundOptions
+		metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
+		metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
+		ctx := log.ContextWithNewID(l.ctx)
+		l.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
+		go l.connHandler.NewConnectionEx(ctx, conn, metadata, nil)
+	}
+}
diff --git a/common/listener/listener_udp.go b/common/listener/listener_udp.go
new file mode 100644
index 00000000..d871c672
--- /dev/null
+++ b/common/listener/listener_udp.go
@@ -0,0 +1,154 @@
+package listener
+
+import (
+	"net"
+	"os"
+	"time"
+
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/control"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+func (l *Listener) ListenUDP() (net.PacketConn, error) {
+	bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort)
+	var lc net.ListenConfig
+	var udpFragment bool
+	if l.listenOptions.UDPFragment != nil {
+		udpFragment = *l.listenOptions.UDPFragment
+	} else {
+		udpFragment = l.listenOptions.UDPFragmentDefault
+	}
+	if !udpFragment {
+		lc.Control = control.Append(lc.Control, control.DisableUDPFragment())
+	}
+	udpConn, err := lc.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
+	if err != nil {
+		return nil, err
+	}
+	l.udpConn = udpConn.(*net.UDPConn)
+	l.udpAddr = bindAddr
+	l.logger.Info("udp server started at ", udpConn.LocalAddr())
+	return udpConn, err
+}
+
+func (l *Listener) UDPAddr() M.Socksaddr {
+	return l.udpAddr
+}
+
+func (l *Listener) PacketWriter() N.PacketWriter {
+	return (*packetWriter)(l)
+}
+
+func (l *Listener) loopUDPIn() {
+	defer close(l.packetOutboundClosed)
+	var buffer *buf.Buffer
+	if !l.threadUnsafePacketWriter {
+		buffer = buf.NewPacket()
+		defer buffer.Release()
+		buffer.IncRef()
+		defer buffer.DecRef()
+	}
+	if l.oobPacketHandler != nil {
+		oob := make([]byte, 1024)
+		for {
+			if l.threadUnsafePacketWriter {
+				buffer = buf.NewPacket()
+			} else {
+				buffer.Reset()
+			}
+			n, oobN, _, addr, err := l.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob)
+			if err != nil {
+				if l.threadUnsafePacketWriter {
+					buffer.Release()
+				}
+				if l.shutdown.Load() && E.IsClosed(err) {
+					return
+				}
+				l.udpConn.Close()
+				l.logger.Error("udp listener closed: ", err)
+				return
+			}
+			buffer.Truncate(n)
+			l.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap())
+		}
+	} else {
+		for {
+			if l.threadUnsafePacketWriter {
+				buffer = buf.NewPacket()
+			} else {
+				buffer.Reset()
+			}
+			n, addr, err := l.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes())
+			if err != nil {
+				if l.threadUnsafePacketWriter {
+					buffer.Release()
+				}
+				if l.shutdown.Load() && E.IsClosed(err) {
+					return
+				}
+				l.udpConn.Close()
+				l.logger.Error("udp listener closed: ", err)
+				return
+			}
+			buffer.Truncate(n)
+			l.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap())
+		}
+	}
+}
+
+func (l *Listener) loopUDPOut() {
+	for {
+		select {
+		case packet := <-l.packetOutbound:
+			destination := packet.Destination.AddrPort()
+			_, err := l.udpConn.WriteToUDPAddrPort(packet.Buffer.Bytes(), destination)
+			packet.Buffer.Release()
+			N.PutPacketBuffer(packet)
+			if err != nil {
+				if l.shutdown.Load() && E.IsClosed(err) {
+					return
+				}
+				l.udpConn.Close()
+				l.logger.Error("udp listener write back: ", destination, ": ", err)
+				return
+			}
+			continue
+		case <-l.packetOutboundClosed:
+		}
+		for {
+			select {
+			case packet := <-l.packetOutbound:
+				packet.Buffer.Release()
+				N.PutPacketBuffer(packet)
+			case <-time.After(time.Second):
+				return
+			}
+		}
+	}
+}
+
+type packetWriter Listener
+
+func (w *packetWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+	packet := N.NewPacketBuffer()
+	packet.Buffer = buffer
+	packet.Destination = destination
+	select {
+	case w.packetOutbound <- packet:
+		return nil
+	default:
+		buffer.Release()
+		N.PutPacketBuffer(packet)
+		if w.shutdown.Load() {
+			return os.ErrClosed
+		}
+		w.logger.Trace("dropped packet to ", destination)
+		return nil
+	}
+}
+
+func (w *packetWriter) WriteIsThreadUnsafe() {
+}
diff --git a/experimental/clashapi/api_meta_group.go b/experimental/clashapi/api_meta_group.go
index 396dee7f..531311f4 100644
--- a/experimental/clashapi/api_meta_group.go
+++ b/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
 				}
diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go
index 7a807c1f..4a9564ee 100644
--- a/experimental/clashapi/proxies.go
+++ b/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 {
diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go
index 3a8d2a07..0915f56b 100644
--- a/experimental/libbox/command_group.go
+++ b/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)
 }
diff --git a/experimental/libbox/command_select.go b/experimental/libbox/command_select.go
index e1e67e60..f352005d 100644
--- a/experimental/libbox/command_select.go
+++ b/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))
 	}
diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go
index 6feda3f8..c72ded8a 100644
--- a/experimental/libbox/command_urltest.go
+++ b/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 {
diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go
index 56c56189..53889d30 100644
--- a/experimental/libbox/config.go
+++ b/experimental/libbox/config.go
@@ -10,6 +10,7 @@ import (
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/process"
 	"github.com/sagernet/sing-box/experimental/libbox/platform"
+	"github.com/sagernet/sing-box/include"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common/control"
@@ -17,10 +18,11 @@ import (
 	"github.com/sagernet/sing/common/json"
 	"github.com/sagernet/sing/common/logger"
 	"github.com/sagernet/sing/common/x/list"
+	"github.com/sagernet/sing/service"
 )
 
-func parseConfig(configContent string) (option.Options, error) {
-	options, err := json.UnmarshalExtended[option.Options]([]byte(configContent))
+func parseConfig(ctx context.Context, configContent string) (option.Options, error) {
+	options, err := json.UnmarshalExtendedContext[option.Options](ctx, []byte(configContent))
 	if err != nil {
 		return option.Options{}, E.Cause(err, "decode config")
 	}
@@ -28,16 +30,17 @@ func parseConfig(configContent string) (option.Options, error) {
 }
 
 func CheckConfig(configContent string) error {
-	options, err := parseConfig(configContent)
+	ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry())
+	options, err := parseConfig(ctx, configContent)
 	if err != nil {
 		return err
 	}
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
+	ctx = service.ContextWith[platform.Interface](ctx, (*platformInterfaceStub)(nil))
 	instance, err := box.New(box.Options{
-		Context:           ctx,
-		Options:           options,
-		PlatformInterface: (*platformInterfaceStub)(nil),
+		Context: ctx,
+		Options: options,
 	})
 	if err == nil {
 		instance.Close()
@@ -140,7 +143,7 @@ func (s *platformInterfaceStub) SendNotification(notification *platform.Notifica
 }
 
 func FormatConfig(configContent string) (string, error) {
-	options, err := parseConfig(configContent)
+	options, err := parseConfig(box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()), configContent)
 	if err != nil {
 		return "", err
 	}
diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go
index 20395e95..eadd4c27 100644
--- a/experimental/libbox/service.go
+++ b/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"
@@ -41,21 +42,22 @@ type BoxService struct {
 }
 
 func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
-	options, err := parseConfig(configContent)
+	ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry())
+	options, err := parseConfig(ctx, configContent)
 	if err != nil {
 		return nil, err
 	}
 	runtimeDebug.FreeOSMemory()
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(ctx)
 	ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
 	urlTestHistoryStorage := urltest.NewHistoryStorage()
 	ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
 	ctx = service.ContextWith[deprecated.Manager](ctx, new(deprecatedManager))
 	platformWrapper := &platformInterfaceWrapper{iif: platformInterface, useProcFS: platformInterface.UseProcFS()}
+	ctx = service.ContextWith[platform.Interface](ctx, platformWrapper)
 	instance, err := box.New(box.Options{
 		Context:           ctx,
 		Options:           options,
-		PlatformInterface: platformWrapper,
 		PlatformLogWriter: platformWrapper,
 	})
 	if err != nil {
diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go
index eb5d7c4e..daafecca 100644
--- a/experimental/libbox/setup.go
+++ b/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"
 )
 
diff --git a/go.mod b/go.mod
index 70cc8577..bf88f1d3 100644
--- a/go.mod
+++ b/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
diff --git a/go.sum b/go.sum
index ad5c611c..3b789c9b 100644
--- a/go.sum
+++ b/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=
@@ -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.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
 golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
@@ -182,7 +175,6 @@ golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
 golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
 golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
 golang.org/x/sync v0.9.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=
diff --git a/inbound/builder.go b/inbound/builder.go
deleted file mode 100644
index ddfd361d..00000000
--- a/inbound/builder.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package inbound
-
-import (
-	"context"
-
-	"github.com/sagernet/sing-box/adapter"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
-	"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.Inbound, platformInterface platform.Interface) (adapter.Inbound, error) {
-	if options.Type == "" {
-		return nil, E.New("missing inbound type")
-	}
-	switch options.Type {
-	case C.TypeTun:
-		return NewTun(ctx, router, logger, tag, options.TunOptions, platformInterface)
-	case C.TypeRedirect:
-		return NewRedirect(ctx, router, logger, tag, options.RedirectOptions), nil
-	case C.TypeTProxy:
-		return NewTProxy(ctx, router, logger, tag, options.TProxyOptions), nil
-	case C.TypeDirect:
-		return NewDirect(ctx, router, logger, tag, options.DirectOptions), nil
-	case C.TypeSOCKS:
-		return NewSocks(ctx, router, logger, tag, options.SocksOptions), nil
-	case C.TypeHTTP:
-		return NewHTTP(ctx, router, logger, tag, options.HTTPOptions)
-	case C.TypeMixed:
-		return NewMixed(ctx, router, logger, tag, options.MixedOptions), nil
-	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.TypeNaive:
-		return NewNaive(ctx, router, logger, tag, options.NaiveOptions)
-	case C.TypeHysteria:
-		return NewHysteria(ctx, router, logger, tag, options.HysteriaOptions)
-	case C.TypeShadowTLS:
-		return NewShadowTLS(ctx, router, logger, tag, options.ShadowTLSOptions)
-	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)
-	default:
-		return nil, E.New("unknown inbound type: ", options.Type)
-	}
-}
diff --git a/inbound/default.go b/inbound/default.go
deleted file mode 100644
index 880dd26f..00000000
--- a/inbound/default.go
+++ /dev/null
@@ -1,209 +0,0 @@
-package inbound
-
-import (
-	"context"
-	"net"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/settings"
-	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/atomic"
-	E "github.com/sagernet/sing/common/exceptions"
-	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
-)
-
-var _ adapter.Inbound = (*myInboundAdapter)(nil)
-
-type myInboundAdapter struct {
-	protocol         string
-	network          []string
-	ctx              context.Context
-	router           adapter.ConnectionRouterEx
-	logger           log.ContextLogger
-	tag              string
-	listenOptions    option.ListenOptions
-	connHandler      adapter.ConnectionHandlerEx
-	packetHandler    adapter.PacketHandlerEx
-	oobPacketHandler adapter.OOBPacketHandlerEx
-	packetUpstream   any
-
-	// http mixed
-
-	setSystemProxy bool
-	systemProxy    settings.SystemProxy
-
-	// internal
-
-	tcpListener          net.Listener
-	udpConn              *net.UDPConn
-	udpAddr              M.Socksaddr
-	packetOutboundClosed chan struct{}
-	packetOutbound       chan *myInboundPacket
-
-	inShutdown atomic.Bool
-}
-
-func (a *myInboundAdapter) Type() string {
-	return a.protocol
-}
-
-func (a *myInboundAdapter) Tag() string {
-	return a.tag
-}
-
-func (a *myInboundAdapter) Start() error {
-	var err error
-	if common.Contains(a.network, N.NetworkTCP) {
-		_, err = a.ListenTCP()
-		if err != nil {
-			return err
-		}
-		go a.loopTCPIn()
-	}
-	if common.Contains(a.network, N.NetworkUDP) {
-		_, err = a.ListenUDP()
-		if err != nil {
-			return err
-		}
-		a.packetOutboundClosed = make(chan struct{})
-		a.packetOutbound = make(chan *myInboundPacket)
-		if a.oobPacketHandler != nil {
-			if _, threadUnsafeHandler := common.Cast[N.ThreadUnsafeWriter](a.packetUpstream); !threadUnsafeHandler {
-				go a.loopUDPOOBIn()
-			} else {
-				go a.loopUDPOOBInThreadSafe()
-			}
-		} else {
-			if _, threadUnsafeHandler := common.Cast[N.ThreadUnsafeWriter](a.packetUpstream); !threadUnsafeHandler {
-				go a.loopUDPIn()
-			} else {
-				go a.loopUDPInThreadSafe()
-			}
-			go a.loopUDPOut()
-		}
-	}
-	if a.setSystemProxy {
-		listenPort := M.SocksaddrFromNet(a.tcpListener.Addr()).Port
-		var listenAddrString string
-		listenAddr := a.listenOptions.Listen.Build()
-		if listenAddr.IsUnspecified() {
-			listenAddrString = "127.0.0.1"
-		} else {
-			listenAddrString = listenAddr.String()
-		}
-		var systemProxy settings.SystemProxy
-		systemProxy, err = settings.NewSystemProxy(a.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), a.protocol == C.TypeMixed)
-		if err != nil {
-			return E.Cause(err, "initialize system proxy")
-		}
-		err = systemProxy.Enable()
-		if err != nil {
-			return E.Cause(err, "set system proxy")
-		}
-		a.systemProxy = systemProxy
-	}
-	return nil
-}
-
-func (a *myInboundAdapter) Close() error {
-	a.inShutdown.Store(true)
-	var err error
-	if a.systemProxy != nil && a.systemProxy.IsEnabled() {
-		err = a.systemProxy.Disable()
-	}
-	return E.Errors(err, common.Close(
-		a.tcpListener,
-		common.PtrOrNil(a.udpConn),
-	))
-}
-
-func (a *myInboundAdapter) upstreamHandler(metadata adapter.InboundContext) adapter.UpstreamHandlerAdapter {
-	return adapter.NewUpstreamHandler(metadata, a.newConnection, a.streamPacketConnection, a)
-}
-
-func (a *myInboundAdapter) upstreamContextHandler() adapter.UpstreamHandlerAdapter {
-	return adapter.NewUpstreamContextHandler(a.newConnection, a.newPacketConnection, a)
-}
-
-func (a *myInboundAdapter) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	a.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
-	return a.router.RouteConnection(ctx, conn, metadata)
-}
-
-func (a *myInboundAdapter) streamPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
-	return a.router.RoutePacketConnection(ctx, conn, metadata)
-}
-
-func (a *myInboundAdapter) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	ctx = log.ContextWithNewID(ctx)
-	a.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
-	a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
-	return a.router.RoutePacketConnection(ctx, conn, metadata)
-}
-
-func (a *myInboundAdapter) upstreamHandlerEx(metadata adapter.InboundContext) adapter.UpstreamHandlerAdapterEx {
-	return adapter.NewUpstreamHandlerEx(metadata, a.newConnectionEx, a.streamPacketConnectionEx)
-}
-
-func (a *myInboundAdapter) upstreamContextHandlerEx() adapter.UpstreamHandlerAdapterEx {
-	return adapter.NewUpstreamContextHandlerEx(a.newConnectionEx, a.newPacketConnectionEx)
-}
-
-func (a *myInboundAdapter) newConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
-	a.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
-	a.router.RouteConnectionEx(ctx, conn, metadata, onClose)
-}
-
-func (a *myInboundAdapter) newPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
-	ctx = log.ContextWithNewID(ctx)
-	a.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
-	a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
-	a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
-}
-
-func (a *myInboundAdapter) streamPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
-	a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
-	a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
-}
-
-func (a *myInboundAdapter) createMetadata(conn net.Conn, metadata adapter.InboundContext) adapter.InboundContext {
-	metadata.Inbound = a.tag
-	metadata.InboundType = a.protocol
-	metadata.InboundDetour = a.listenOptions.Detour
-	metadata.InboundOptions = a.listenOptions.InboundOptions
-	if !metadata.Source.IsValid() {
-		metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
-	}
-	if !metadata.Destination.IsValid() {
-		metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
-	}
-	if tcpConn, isTCP := common.Cast[*net.TCPConn](conn); isTCP {
-		metadata.OriginDestination = M.SocksaddrFromNet(tcpConn.LocalAddr()).Unwrap()
-	}
-	return metadata
-}
-
-// Deprecated: don't use
-func (a *myInboundAdapter) newError(err error) {
-	a.logger.Error(err)
-}
-
-// Deprecated: don't use
-func (a *myInboundAdapter) NewError(ctx context.Context, err error) {
-	NewError(a.logger, ctx, err)
-}
-
-// Deprecated: don't use
-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)
-}
diff --git a/inbound/default_tcp.go b/inbound/default_tcp.go
deleted file mode 100644
index d38f96fe..00000000
--- a/inbound/default_tcp.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package inbound
-
-import (
-	"context"
-	"net"
-
-	"github.com/sagernet/sing-box/adapter"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing/common/control"
-	E "github.com/sagernet/sing/common/exceptions"
-	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
-)
-
-func (a *myInboundAdapter) ListenTCP() (net.Listener, error) {
-	var err error
-	bindAddr := M.SocksaddrFrom(a.listenOptions.Listen.Build(), a.listenOptions.ListenPort)
-	var tcpListener net.Listener
-	var listenConfig net.ListenConfig
-	// TODO: Add an option to customize the keep alive period
-	listenConfig.KeepAlive = C.TCPKeepAliveInitial
-	listenConfig.Control = control.Append(listenConfig.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval))
-	if a.listenOptions.TCPMultiPath {
-		if !go121Available {
-			return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
-		}
-		setMultiPathTCP(&listenConfig)
-	}
-	if a.listenOptions.TCPFastOpen {
-		if !go120Available {
-			return nil, E.New("TCP Fast Open requires go1.20, please recompile your binary.")
-		}
-		tcpListener, err = listenTFO(listenConfig, a.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
-	} else {
-		tcpListener, err = listenConfig.Listen(a.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
-	}
-	if err == nil {
-		a.logger.Info("tcp server started at ", tcpListener.Addr())
-	}
-	if a.listenOptions.ProxyProtocol || a.listenOptions.ProxyProtocolAcceptNoHeader {
-		return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
-	}
-	a.tcpListener = tcpListener
-	return tcpListener, err
-}
-
-func (a *myInboundAdapter) loopTCPIn() {
-	tcpListener := a.tcpListener
-	for {
-		conn, err := tcpListener.Accept()
-		if err != nil {
-			//goland:noinspection GoDeprecation
-			//nolint:staticcheck
-			if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() {
-				a.logger.Error(err)
-				continue
-			}
-			if a.inShutdown.Load() && E.IsClosed(err) {
-				return
-			}
-			a.tcpListener.Close()
-			a.logger.Error("serve error: ", err)
-			continue
-		}
-		go a.injectTCP(conn, adapter.InboundContext{})
-	}
-}
-
-func (a *myInboundAdapter) injectTCP(conn net.Conn, metadata adapter.InboundContext) {
-	ctx := log.ContextWithNewID(a.ctx)
-	metadata = a.createMetadata(conn, metadata)
-	a.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
-	a.connHandler.NewConnectionEx(ctx, conn, metadata, nil)
-}
-
-func (a *myInboundAdapter) routeTCP(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
-	metadata := a.createMetadata(conn, adapter.InboundContext{
-		Source:      source,
-		Destination: destination,
-	})
-	a.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
-	a.connHandler.NewConnectionEx(ctx, conn, metadata, onClose)
-}
diff --git a/inbound/default_tcp_go1.20.go b/inbound/default_tcp_go1.20.go
deleted file mode 100644
index 23949b06..00000000
--- a/inbound/default_tcp_go1.20.go
+++ /dev/null
@@ -1,18 +0,0 @@
-//go:build go1.20
-
-package inbound
-
-import (
-	"context"
-	"net"
-
-	"github.com/metacubex/tfo-go"
-)
-
-const go120Available = true
-
-func listenTFO(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.Listener, error) {
-	var tfoConfig tfo.ListenConfig
-	tfoConfig.ListenConfig = listenConfig
-	return tfoConfig.Listen(ctx, network, address)
-}
diff --git a/inbound/default_tcp_nongo1.20.go b/inbound/default_tcp_nongo1.20.go
deleted file mode 100644
index e7a026bc..00000000
--- a/inbound/default_tcp_nongo1.20.go
+++ /dev/null
@@ -1,15 +0,0 @@
-//go:build !go1.20
-
-package inbound
-
-import (
-	"context"
-	"net"
-	"os"
-)
-
-const go120Available = false
-
-func listenTFO(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.Listener, error) {
-	return nil, os.ErrInvalid
-}
diff --git a/inbound/default_udp.go b/inbound/default_udp.go
deleted file mode 100644
index 6bcde79d..00000000
--- a/inbound/default_udp.go
+++ /dev/null
@@ -1,208 +0,0 @@
-package inbound
-
-import (
-	"net"
-	"os"
-	"time"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing/common"
-	"github.com/sagernet/sing/common/buf"
-	"github.com/sagernet/sing/common/control"
-	E "github.com/sagernet/sing/common/exceptions"
-	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
-)
-
-func (a *myInboundAdapter) ListenUDP() (net.PacketConn, error) {
-	bindAddr := M.SocksaddrFrom(a.listenOptions.Listen.Build(), a.listenOptions.ListenPort)
-	var lc net.ListenConfig
-	var udpFragment bool
-	if a.listenOptions.UDPFragment != nil {
-		udpFragment = *a.listenOptions.UDPFragment
-	} else {
-		udpFragment = a.listenOptions.UDPFragmentDefault
-	}
-	if !udpFragment {
-		lc.Control = control.Append(lc.Control, control.DisableUDPFragment())
-	}
-	udpConn, err := lc.ListenPacket(a.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
-	if err != nil {
-		return nil, err
-	}
-	a.udpConn = udpConn.(*net.UDPConn)
-	a.udpAddr = bindAddr
-	a.logger.Info("udp server started at ", udpConn.LocalAddr())
-	return udpConn, err
-}
-
-func (a *myInboundAdapter) loopUDPIn() {
-	defer close(a.packetOutboundClosed)
-	buffer := buf.NewPacket()
-	defer buffer.Release()
-	buffer.IncRef()
-	defer buffer.DecRef()
-	for {
-		buffer.Reset()
-		n, addr, err := a.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes())
-		if err != nil {
-			return
-		}
-		buffer.Truncate(n)
-		a.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap())
-	}
-}
-
-func (a *myInboundAdapter) loopUDPOOBIn() {
-	defer close(a.packetOutboundClosed)
-	buffer := buf.NewPacket()
-	defer buffer.Release()
-	buffer.IncRef()
-	defer buffer.DecRef()
-	oob := make([]byte, 1024)
-	for {
-		buffer.Reset()
-		n, oobN, _, addr, err := a.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob)
-		if err != nil {
-			return
-		}
-		buffer.Truncate(n)
-		a.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap())
-	}
-}
-
-func (a *myInboundAdapter) loopUDPInThreadSafe() {
-	defer close(a.packetOutboundClosed)
-	for {
-		buffer := buf.NewPacket()
-		n, addr, err := a.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes())
-		if err != nil {
-			buffer.Release()
-			return
-		}
-		buffer.Truncate(n)
-		a.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap())
-	}
-}
-
-func (a *myInboundAdapter) loopUDPOOBInThreadSafe() {
-	defer close(a.packetOutboundClosed)
-	oob := make([]byte, 1024)
-	for {
-		buffer := buf.NewPacket()
-		n, oobN, _, addr, err := a.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob)
-		if err != nil {
-			buffer.Release()
-			return
-		}
-		buffer.Truncate(n)
-		a.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap())
-	}
-}
-
-func (a *myInboundAdapter) loopUDPOut() {
-	for {
-		select {
-		case packet := <-a.packetOutbound:
-			err := a.writePacket(packet.buffer, packet.destination)
-			if err != nil && !E.IsClosed(err) {
-				a.logger.Error(E.New("write back udp: ", err))
-			}
-			continue
-		case <-a.packetOutboundClosed:
-		}
-		for {
-			select {
-			case packet := <-a.packetOutbound:
-				packet.buffer.Release()
-			default:
-				return
-			}
-		}
-	}
-}
-
-func (a *myInboundAdapter) packetConn() N.PacketConn {
-	return (*myInboundPacketAdapter)(a)
-}
-
-func (a *myInboundAdapter) createPacketMetadata(conn N.PacketConn, metadata adapter.InboundContext) adapter.InboundContext {
-	metadata.Inbound = a.tag
-	metadata.InboundType = a.protocol
-	metadata.InboundDetour = a.listenOptions.Detour
-	metadata.InboundOptions = a.listenOptions.InboundOptions
-	if !metadata.Destination.IsValid() {
-		metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
-	}
-	metadata.OriginDestination = a.udpAddr
-	return metadata
-}
-
-func (a *myInboundAdapter) createPacketMetadataEx(source M.Socksaddr, destination M.Socksaddr) adapter.InboundContext {
-	var metadata adapter.InboundContext
-	metadata.Inbound = a.tag
-	metadata.InboundType = a.protocol
-	metadata.InboundDetour = a.listenOptions.Detour
-	metadata.InboundOptions = a.listenOptions.InboundOptions
-	metadata.Source = source
-	metadata.Destination = destination
-	metadata.OriginDestination = a.udpAddr
-	return metadata
-}
-
-func (a *myInboundAdapter) writePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
-	defer buffer.Release()
-	return common.Error(a.udpConn.WriteToUDPAddrPort(buffer.Bytes(), destination.AddrPort()))
-}
-
-type myInboundPacketAdapter myInboundAdapter
-
-func (s *myInboundPacketAdapter) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) {
-	n, addr, err := s.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes())
-	if err != nil {
-		return M.Socksaddr{}, err
-	}
-	buffer.Truncate(n)
-	return M.SocksaddrFromNetIP(addr), nil
-}
-
-func (s *myInboundPacketAdapter) WriteIsThreadUnsafe() {
-}
-
-type myInboundPacket struct {
-	buffer      *buf.Buffer
-	destination M.Socksaddr
-}
-
-func (s *myInboundPacketAdapter) Upstream() any {
-	return s.udpConn
-}
-
-func (s *myInboundPacketAdapter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
-	select {
-	case s.packetOutbound <- &myInboundPacket{buffer, destination}:
-		return nil
-	case <-s.packetOutboundClosed:
-		return os.ErrClosed
-	}
-}
-
-func (s *myInboundPacketAdapter) Close() error {
-	return s.udpConn.Close()
-}
-
-func (s *myInboundPacketAdapter) LocalAddr() net.Addr {
-	return s.udpConn.LocalAddr()
-}
-
-func (s *myInboundPacketAdapter) SetDeadline(t time.Time) error {
-	return s.udpConn.SetDeadline(t)
-}
-
-func (s *myInboundPacketAdapter) SetReadDeadline(t time.Time) error {
-	return s.udpConn.SetReadDeadline(t)
-}
-
-func (s *myInboundPacketAdapter) SetWriteDeadline(t time.Time) error {
-	return s.udpConn.SetWriteDeadline(t)
-}
diff --git a/inbound/direct.go b/inbound/direct.go
deleted file mode 100644
index b9a99f12..00000000
--- a/inbound/direct.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package inbound
-
-import (
-	"context"
-	"net"
-	"time"
-
-	"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/common/buf"
-	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
-	"github.com/sagernet/sing/common/udpnat2"
-)
-
-var _ adapter.Inbound = (*Direct)(nil)
-
-type Direct struct {
-	myInboundAdapter
-	udpNat              *udpnat.Service
-	overrideOption      int
-	overrideDestination M.Socksaddr
-}
-
-func NewDirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectInboundOptions) *Direct {
-	options.UDPFragmentDefault = true
-	inbound := &Direct{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeDirect,
-			network:       options.Network.Build(),
-			ctx:           ctx,
-			router:        router,
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
-	}
-	if options.OverrideAddress != "" && options.OverridePort != 0 {
-		inbound.overrideOption = 1
-		inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort)
-	} else if options.OverrideAddress != "" {
-		inbound.overrideOption = 2
-		inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort)
-	} else if options.OverridePort != 0 {
-		inbound.overrideOption = 3
-		inbound.overrideDestination = M.Socksaddr{Port: options.OverridePort}
-	}
-	var udpTimeout time.Duration
-	if options.UDPTimeout != 0 {
-		udpTimeout = time.Duration(options.UDPTimeout)
-	} else {
-		udpTimeout = C.UDPTimeout
-	}
-	inbound.udpNat = udpnat.New(inbound, inbound.preparePacketConnection, udpTimeout, false)
-	inbound.connHandler = inbound
-	inbound.packetHandler = inbound
-	return inbound
-}
-
-func (d *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	switch d.overrideOption {
-	case 1:
-		metadata.Destination = d.overrideDestination
-	case 2:
-		destination := d.overrideDestination
-		destination.Port = metadata.Destination.Port
-		metadata.Destination = destination
-	case 3:
-		metadata.Destination.Port = d.overrideDestination.Port
-	}
-	d.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
-	return d.router.RouteConnection(ctx, conn, metadata)
-}
-
-func (d *Direct) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) {
-	var destination M.Socksaddr
-	switch d.overrideOption {
-	case 1:
-		destination = d.overrideDestination
-	case 2:
-		destination = d.overrideDestination
-		destination.Port = source.Port
-	case 3:
-		destination = source
-		destination.Port = d.overrideDestination.Port
-	}
-	d.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, destination, nil)
-}
-
-func (d *Direct) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
-	d.newConnectionEx(ctx, conn, metadata, onClose)
-}
-
-func (d *Direct) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
-	d.newPacketConnectionEx(ctx, conn, d.createPacketMetadataEx(source, destination), onClose)
-}
-
-func (d *Direct) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) {
-	return true, d.ctx, &directPacketWriter{d.packetConn(), source}, nil
-}
-
-type directPacketWriter struct {
-	writer N.PacketWriter
-	source M.Socksaddr
-}
-
-func (w *directPacketWriter) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error {
-	return w.writer.WritePacket(buffer, w.source)
-}
diff --git a/inbound/http.go b/inbound/http.go
deleted file mode 100644
index 20c8f690..00000000
--- a/inbound/http.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package inbound
-
-import (
-	std_bufio "bufio"
-	"context"
-	"net"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/tls"
-	"github.com/sagernet/sing-box/common/uot"
-	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/auth"
-	E "github.com/sagernet/sing/common/exceptions"
-	N "github.com/sagernet/sing/common/network"
-	"github.com/sagernet/sing/protocol/http"
-)
-
-var (
-	_ adapter.Inbound              = (*HTTP)(nil)
-	_ adapter.TCPInjectableInbound = (*HTTP)(nil)
-)
-
-type HTTP struct {
-	myInboundAdapter
-	authenticator *auth.Authenticator
-	tlsConfig     tls.ServerConfig
-}
-
-func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (*HTTP, error) {
-	inbound := &HTTP{
-		myInboundAdapter: myInboundAdapter{
-			protocol:       C.TypeHTTP,
-			network:        []string{N.NetworkTCP},
-			ctx:            ctx,
-			router:         uot.NewRouter(router, logger),
-			logger:         logger,
-			tag:            tag,
-			listenOptions:  options.ListenOptions,
-			setSystemProxy: options.SetSystemProxy,
-		},
-		authenticator: auth.NewAuthenticator(options.Users),
-	}
-	if options.TLS != nil {
-		tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
-		if err != nil {
-			return nil, err
-		}
-		inbound.tlsConfig = tlsConfig
-	}
-	inbound.connHandler = inbound
-	return inbound, nil
-}
-
-func (h *HTTP) Start() error {
-	if h.tlsConfig != nil {
-		err := h.tlsConfig.Start()
-		if err != nil {
-			return E.Cause(err, "create TLS config")
-		}
-	}
-	return h.myInboundAdapter.Start()
-}
-
-func (h *HTTP) Close() error {
-	return common.Close(
-		&h.myInboundAdapter,
-		h.tlsConfig,
-	)
-}
-
-func (h *HTTP) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
-	err := h.newConnection(ctx, conn, metadata, onClose)
-	N.CloseOnHandshakeFailure(conn, onClose, err)
-	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
-	}
-}
-
-func (h *HTTP) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
-	var err error
-	if h.tlsConfig != nil {
-		conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig)
-		if err != nil {
-			return err
-		}
-	}
-	return http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, onClose)
-}
-
-func (a *myInboundAdapter) upstreamUserHandlerEx(metadata adapter.InboundContext) adapter.UpstreamHandlerAdapterEx {
-	return adapter.NewUpstreamHandlerEx(metadata, a.newUserConnection, a.streamUserPacketConnection)
-}
-
-func (a *myInboundAdapter) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
-	user, loaded := auth.UserFromContext[string](ctx)
-	if !loaded {
-		a.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
-		a.router.RouteConnectionEx(ctx, conn, metadata, onClose)
-		return
-	}
-	metadata.User = user
-	a.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
-	a.router.RouteConnectionEx(ctx, conn, metadata, onClose)
-}
-
-func (a *myInboundAdapter) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
-	user, loaded := auth.UserFromContext[string](ctx)
-	if !loaded {
-		a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
-		a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
-		return
-	}
-	metadata.User = user
-	a.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
-	a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
-}
diff --git a/inbound/hysteria_stub.go b/inbound/hysteria_stub.go
deleted file mode 100644
index fab86bb5..00000000
--- a/inbound/hysteria_stub.go
+++ /dev/null
@@ -1,20 +0,0 @@
-//go:build !with_quic
-
-package inbound
-
-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.HysteriaInboundOptions) (adapter.Inbound, error) {
-	return nil, C.ErrQUICNotIncluded
-}
-
-func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) {
-	return nil, C.ErrQUICNotIncluded
-}
diff --git a/inbound/mixed.go b/inbound/mixed.go
deleted file mode 100644
index 81f6a43a..00000000
--- a/inbound/mixed.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package inbound
-
-import (
-	std_bufio "bufio"
-	"context"
-	"net"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/uot"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing/common/auth"
-	E "github.com/sagernet/sing/common/exceptions"
-	N "github.com/sagernet/sing/common/network"
-	"github.com/sagernet/sing/protocol/http"
-	"github.com/sagernet/sing/protocol/socks"
-	"github.com/sagernet/sing/protocol/socks/socks4"
-	"github.com/sagernet/sing/protocol/socks/socks5"
-)
-
-var (
-	_ adapter.Inbound              = (*Mixed)(nil)
-	_ adapter.TCPInjectableInbound = (*Mixed)(nil)
-)
-
-type Mixed struct {
-	myInboundAdapter
-	authenticator *auth.Authenticator
-}
-
-func NewMixed(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) *Mixed {
-	inbound := &Mixed{
-		myInboundAdapter{
-			protocol:       C.TypeMixed,
-			network:        []string{N.NetworkTCP},
-			ctx:            ctx,
-			router:         uot.NewRouter(router, logger),
-			logger:         logger,
-			tag:            tag,
-			listenOptions:  options.ListenOptions,
-			setSystemProxy: options.SetSystemProxy,
-		},
-		auth.NewAuthenticator(options.Users),
-	}
-	inbound.connHandler = inbound
-	return inbound
-}
-
-func (h *Mixed) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
-	err := h.newConnection(ctx, conn, metadata, onClose)
-	N.CloseOnHandshakeFailure(conn, onClose, err)
-	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
-	}
-}
-
-func (h *Mixed) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
-	reader := std_bufio.NewReader(conn)
-	headerBytes, err := reader.Peek(1)
-	if err != nil {
-		return E.Cause(err, "peek first byte")
-	}
-	switch headerBytes[0] {
-	case socks4.Version, socks5.Version:
-		return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, metadata.Destination, onClose)
-	default:
-		return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, onClose)
-	}
-}
diff --git a/inbound/naive_quic.go b/inbound/naive_quic.go
deleted file mode 100644
index 9f99bf27..00000000
--- a/inbound/naive_quic.go
+++ /dev/null
@@ -1,47 +0,0 @@
-//go:build with_quic
-
-package inbound
-
-import (
-	"github.com/sagernet/quic-go"
-	"github.com/sagernet/quic-go/http3"
-	"github.com/sagernet/sing-quic"
-	E "github.com/sagernet/sing/common/exceptions"
-)
-
-func (n *Naive) configureHTTP3Listener() error {
-	err := qtls.ConfigureHTTP3(n.tlsConfig)
-	if err != nil {
-		return err
-	}
-
-	udpConn, err := n.ListenUDP()
-	if err != nil {
-		return err
-	}
-
-	quicListener, err := qtls.ListenEarly(udpConn, n.tlsConfig, &quic.Config{
-		MaxIncomingStreams: 1 << 60,
-		Allow0RTT:          true,
-	})
-	if err != nil {
-		udpConn.Close()
-		return err
-	}
-
-	h3Server := &http3.Server{
-		Port:    int(n.listenOptions.ListenPort),
-		Handler: n,
-	}
-
-	go func() {
-		sErr := h3Server.ServeListener(quicListener)
-		udpConn.Close()
-		if sErr != nil && !E.IsClosedOrCanceled(sErr) {
-			n.logger.Error("http3 server serve error: ", sErr)
-		}
-	}()
-
-	n.h3Server = h3Server
-	return nil
-}
diff --git a/inbound/naive_quic_stub.go b/inbound/naive_quic_stub.go
deleted file mode 100644
index 90f697e4..00000000
--- a/inbound/naive_quic_stub.go
+++ /dev/null
@@ -1,11 +0,0 @@
-//go:build !with_quic
-
-package inbound
-
-import (
-	C "github.com/sagernet/sing-box/constant"
-)
-
-func (n *Naive) configureHTTP3Listener() error {
-	return C.ErrQUICNotIncluded
-}
diff --git a/inbound/redirect.go b/inbound/redirect.go
deleted file mode 100644
index c4c6faf3..00000000
--- a/inbound/redirect.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package inbound
-
-import (
-	"context"
-	"net"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/redir"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing-box/option"
-	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
-)
-
-type Redirect struct {
-	myInboundAdapter
-}
-
-func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions) *Redirect {
-	redirect := &Redirect{
-		myInboundAdapter{
-			protocol:      C.TypeRedirect,
-			network:       []string{N.NetworkTCP},
-			ctx:           ctx,
-			router:        router,
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
-	}
-	redirect.connHandler = redirect
-	return redirect
-}
-
-func (r *Redirect) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
-	destination, err := redir.GetOriginalDestination(conn)
-	if err != nil {
-		conn.Close()
-		r.logger.ErrorContext(ctx, "process connection from ", conn.RemoteAddr(), ": get redirect destination: ", err)
-		return
-	}
-	metadata.Destination = M.SocksaddrFromNetIP(destination)
-	r.newConnectionEx(ctx, conn, metadata, onClose)
-}
diff --git a/inbound/shadowsocks.go b/inbound/shadowsocks.go
deleted file mode 100644
index 3fff231d..00000000
--- a/inbound/shadowsocks.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package inbound
-
-import (
-	"context"
-	"net"
-	"time"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/mux"
-	"github.com/sagernet/sing-box/common/uot"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-shadowsocks"
-	"github.com/sagernet/sing-shadowsocks/shadowaead"
-	"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
-	"github.com/sagernet/sing/common"
-	"github.com/sagernet/sing/common/buf"
-	E "github.com/sagernet/sing/common/exceptions"
-	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
-	"github.com/sagernet/sing/common/ntp"
-)
-
-func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
-	if len(options.Users) > 0 && len(options.Destinations) > 0 {
-		return nil, E.New("users and destinations options must not be combined")
-	}
-	if len(options.Users) > 0 {
-		return newShadowsocksMulti(ctx, router, logger, tag, options)
-	} else if len(options.Destinations) > 0 {
-		return newShadowsocksRelay(ctx, router, logger, tag, options)
-	} else {
-		return newShadowsocks(ctx, router, logger, tag, options)
-	}
-}
-
-var (
-	_ adapter.Inbound              = (*Shadowsocks)(nil)
-	_ adapter.TCPInjectableInbound = (*Shadowsocks)(nil)
-)
-
-type Shadowsocks struct {
-	myInboundAdapter
-	service shadowsocks.Service
-}
-
-func newShadowsocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*Shadowsocks, error) {
-	inbound := &Shadowsocks{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeShadowsocks,
-			network:       options.Network.Build(),
-			ctx:           ctx,
-			router:        uot.NewRouter(router, logger),
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
-	}
-
-	inbound.connHandler = inbound
-	inbound.packetHandler = inbound
-	var err error
-	inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex))
-	if err != nil {
-		return nil, err
-	}
-
-	var udpTimeout time.Duration
-	if options.UDPTimeout != 0 {
-		udpTimeout = time.Duration(options.UDPTimeout)
-	} else {
-		udpTimeout = C.UDPTimeout
-	}
-	switch {
-	case options.Method == shadowsocks.MethodNone:
-		inbound.service = shadowsocks.NewNoneService(int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound))
-	case common.Contains(shadowaead.List, options.Method):
-		inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound))
-	case common.Contains(shadowaead_2022.List, options.Method):
-		inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx))
-	default:
-		err = E.New("unsupported method: ", options.Method)
-	}
-	inbound.packetUpstream = inbound.service
-	return inbound, err
-}
-
-func (h *Shadowsocks) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
-	err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata))
-	N.CloseOnHandshakeFailure(conn, onClose, err)
-	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
-	}
-}
-
-func (h *Shadowsocks) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) {
-	err := h.service.NewPacket(h.ctx, h.packetConn(), buffer, M.Metadata{Source: source})
-	if err != nil {
-		h.logger.Error(E.Cause(err, "process packet from ", source))
-	}
-}
-
-func (h *Shadowsocks) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
-	return h.router.RouteConnection(ctx, conn, h.createMetadata(conn, metadata))
-}
-
-func (h *Shadowsocks) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	ctx = log.ContextWithNewID(ctx)
-	h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
-	h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
-	return h.router.RoutePacketConnection(ctx, conn, h.createPacketMetadata(conn, metadata))
-}
diff --git a/inbound/socks.go b/inbound/socks.go
deleted file mode 100644
index 04b0a77d..00000000
--- a/inbound/socks.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package inbound
-
-import (
-	std_bufio "bufio"
-	"context"
-	"net"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/uot"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing/common/auth"
-	E "github.com/sagernet/sing/common/exceptions"
-	N "github.com/sagernet/sing/common/network"
-	"github.com/sagernet/sing/protocol/socks"
-)
-
-var (
-	_ adapter.Inbound              = (*Socks)(nil)
-	_ adapter.TCPInjectableInbound = (*Socks)(nil)
-)
-
-type Socks struct {
-	myInboundAdapter
-	authenticator *auth.Authenticator
-}
-
-func NewSocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SocksInboundOptions) *Socks {
-	inbound := &Socks{
-		myInboundAdapter{
-			protocol:      C.TypeSOCKS,
-			network:       []string{N.NetworkTCP},
-			ctx:           ctx,
-			router:        uot.NewRouter(router, logger),
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
-		auth.NewAuthenticator(options.Users),
-	}
-	inbound.connHandler = inbound
-	return inbound
-}
-
-func (h *Socks) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
-	err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, metadata.Destination, onClose)
-	N.CloseOnHandshakeFailure(conn, onClose, err)
-	if err != nil {
-		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
-	}
-}
diff --git a/inbound/tuic_stub.go b/inbound/tuic_stub.go
deleted file mode 100644
index bfd402ab..00000000
--- a/inbound/tuic_stub.go
+++ /dev/null
@@ -1,16 +0,0 @@
-//go:build !with_quic
-
-package inbound
-
-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.TUICInboundOptions) (adapter.Inbound, error) {
-	return nil, C.ErrQUICNotIncluded
-}
diff --git a/include/quic.go b/include/quic.go
index 1bcc0fbc..980b4581 100644
--- a/include/quic.go
+++ b/include/quic.go
@@ -3,6 +3,24 @@
 package include
 
 import (
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"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/naive/quic"
+	"github.com/sagernet/sing-box/protocol/tuic"
 	_ "github.com/sagernet/sing-box/transport/v2rayquic"
 	_ "github.com/sagernet/sing-dns/quic"
 )
+
+func registerQUICInbounds(registry *inbound.Registry) {
+	hysteria.RegisterInbound(registry)
+	tuic.RegisterInbound(registry)
+	hysteria2.RegisterInbound(registry)
+}
+
+func registerQUICOutbounds(registry *outbound.Registry) {
+	hysteria.RegisterOutbound(registry)
+	tuic.RegisterOutbound(registry)
+	hysteria2.RegisterOutbound(registry)
+}
diff --git a/include/quic_stub.go b/include/quic_stub.go
index 43aa58d9..66c08590 100644
--- a/include/quic_stub.go
+++ b/include/quic_stub.go
@@ -4,11 +4,18 @@ package include
 
 import (
 	"context"
+	"io"
+	"net/http"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/adapter/outbound"
+	"github.com/sagernet/sing-box/common/listener"
 	"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/naive"
 	"github.com/sagernet/sing-box/transport/v2ray"
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing/common/logger"
@@ -29,3 +36,30 @@ func init() {
 		},
 	)
 }
+
+func registerQUICInbounds(registry *inbound.Registry) {
+	inbound.Register[option.HysteriaInboundOptions](registry, C.TypeHysteria, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) {
+		return nil, C.ErrQUICNotIncluded
+	})
+	inbound.Register[option.TUICInboundOptions](registry, C.TypeTUIC, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) {
+		return nil, C.ErrQUICNotIncluded
+	})
+	inbound.Register[option.Hysteria2InboundOptions](registry, C.TypeHysteria2, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) {
+		return nil, C.ErrQUICNotIncluded
+	})
+	naive.ConfigureHTTP3ListenerFunc = func(listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, logger logger.Logger) (io.Closer, error) {
+		return nil, C.ErrQUICNotIncluded
+	}
+}
+
+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
+	})
+}
diff --git a/include/registry.go b/include/registry.go
new file mode 100644
index 00000000..03fb33f2
--- /dev/null
+++ b/include/registry.go
@@ -0,0 +1,95 @@
+package include
+
+import (
+	"context"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"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/mixed"
+	"github.com/sagernet/sing-box/protocol/naive"
+	"github.com/sagernet/sing-box/protocol/redirect"
+	"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/tun"
+	"github.com/sagernet/sing-box/protocol/vless"
+	"github.com/sagernet/sing-box/protocol/vmess"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+func InboundRegistry() *inbound.Registry {
+	registry := inbound.NewRegistry()
+
+	tun.RegisterInbound(registry)
+	redirect.RegisterRedirect(registry)
+	redirect.RegisterTProxy(registry)
+	direct.RegisterInbound(registry)
+
+	socks.RegisterInbound(registry)
+	http.RegisterInbound(registry)
+	mixed.RegisterInbound(registry)
+
+	shadowsocks.RegisterInbound(registry)
+	vmess.RegisterInbound(registry)
+	trojan.RegisterInbound(registry)
+	naive.RegisterInbound(registry)
+	shadowtls.RegisterInbound(registry)
+	vless.RegisterInbound(registry)
+
+	registerQUICInbounds(registry)
+	registerStubForRemovedInbounds(registry)
+
+	return registry
+}
+
+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 registerStubForRemovedInbounds(registry *inbound.Registry) {
+	inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
+		return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0")
+	})
+}
+
+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")
+	})
+}
diff --git a/include/wireguard.go b/include/wireguard.go
new file mode 100644
index 00000000..dfc3a242
--- /dev/null
+++ b/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)
+}
diff --git a/include/wireguard_stub.go b/include/wireguard_stub.go
new file mode 100644
index 00000000..a9e84522
--- /dev/null
+++ b/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`)
+	})
+}
diff --git a/option/inbound.go b/option/inbound.go
index d3879904..651d0284 100644
--- a/option/inbound.go
+++ b/option/inbound.go
@@ -1,100 +1,49 @@
 package option
 
 import (
+	"context"
 	"time"
 
-	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"
+	"github.com/sagernet/sing/service"
 )
 
+type InboundOptionsRegistry interface {
+	CreateOptions(outboundType string) (any, bool)
+}
+
 type _Inbound struct {
-	Type               string                    `json:"type"`
-	Tag                string                    `json:"tag,omitempty"`
-	TunOptions         TunInboundOptions         `json:"-"`
-	RedirectOptions    RedirectInboundOptions    `json:"-"`
-	TProxyOptions      TProxyInboundOptions      `json:"-"`
-	DirectOptions      DirectInboundOptions      `json:"-"`
-	SocksOptions       SocksInboundOptions       `json:"-"`
-	HTTPOptions        HTTPMixedInboundOptions   `json:"-"`
-	MixedOptions       HTTPMixedInboundOptions   `json:"-"`
-	ShadowsocksOptions ShadowsocksInboundOptions `json:"-"`
-	VMessOptions       VMessInboundOptions       `json:"-"`
-	TrojanOptions      TrojanInboundOptions      `json:"-"`
-	NaiveOptions       NaiveInboundOptions       `json:"-"`
-	HysteriaOptions    HysteriaInboundOptions    `json:"-"`
-	ShadowTLSOptions   ShadowTLSInboundOptions   `json:"-"`
-	VLESSOptions       VLESSInboundOptions       `json:"-"`
-	TUICOptions        TUICInboundOptions        `json:"-"`
-	Hysteria2Options   Hysteria2InboundOptions   `json:"-"`
+	Type    string `json:"type"`
+	Tag     string `json:"tag,omitempty"`
+	Options any    `json:"-"`
 }
 
 type Inbound _Inbound
 
-func (h *Inbound) RawOptions() (any, error) {
-	var rawOptionsPtr any
-	switch h.Type {
-	case C.TypeTun:
-		rawOptionsPtr = &h.TunOptions
-	case C.TypeRedirect:
-		rawOptionsPtr = &h.RedirectOptions
-	case C.TypeTProxy:
-		rawOptionsPtr = &h.TProxyOptions
-	case C.TypeDirect:
-		rawOptionsPtr = &h.DirectOptions
-	case C.TypeSOCKS:
-		rawOptionsPtr = &h.SocksOptions
-	case C.TypeHTTP:
-		rawOptionsPtr = &h.HTTPOptions
-	case C.TypeMixed:
-		rawOptionsPtr = &h.MixedOptions
-	case C.TypeShadowsocks:
-		rawOptionsPtr = &h.ShadowsocksOptions
-	case C.TypeVMess:
-		rawOptionsPtr = &h.VMessOptions
-	case C.TypeTrojan:
-		rawOptionsPtr = &h.TrojanOptions
-	case C.TypeNaive:
-		rawOptionsPtr = &h.NaiveOptions
-	case C.TypeHysteria:
-		rawOptionsPtr = &h.HysteriaOptions
-	case C.TypeShadowTLS:
-		rawOptionsPtr = &h.ShadowTLSOptions
-	case C.TypeVLESS:
-		rawOptionsPtr = &h.VLESSOptions
-	case C.TypeTUIC:
-		rawOptionsPtr = &h.TUICOptions
-	case C.TypeHysteria2:
-		rawOptionsPtr = &h.Hysteria2Options
-	case "":
-		return nil, E.New("missing inbound type")
-	default:
-		return nil, E.New("unknown inbound type: ", h.Type)
-	}
-	return rawOptionsPtr, nil
+func (h *Inbound) MarshalJSONContext(ctx context.Context) ([]byte, error) {
+	return badjson.MarshallObjectsContext(ctx, (*_Inbound)(h), h.Options)
 }
 
-func (h Inbound) MarshalJSON() ([]byte, error) {
-	rawOptions, err := h.RawOptions()
-	if err != nil {
-		return nil, err
-	}
-	return MarshallObjects((_Inbound)(h), rawOptions)
-}
-
-func (h *Inbound) UnmarshalJSON(bytes []byte) error {
-	err := json.Unmarshal(bytes, (*_Inbound)(h))
+func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) error {
+	err := json.Unmarshal(content, (*_Inbound)(h))
 	if err != nil {
 		return err
 	}
-	rawOptions, err := h.RawOptions()
-	if err != nil {
-		return err
-	}
-	err = UnmarshallExcluded(bytes, (*_Inbound)(h), rawOptions)
+	registry := service.FromContext[InboundOptionsRegistry](ctx)
+	if registry == nil {
+		return E.New("missing inbound options registry in context")
+	}
+	options, loaded := registry.CreateOptions(h.Type)
+	if !loaded {
+		return E.New("unknown inbound type: ", h.Type)
+	}
+	err = badjson.UnmarshallExcludedContext(ctx, content, (*_Inbound)(h), options)
 	if err != nil {
 		return err
 	}
+	h.Options = options
 	return nil
 }
 
@@ -105,19 +54,24 @@ type InboundOptions struct {
 	SniffTimeout              Duration       `json:"sniff_timeout,omitempty"`
 	DomainStrategy            DomainStrategy `json:"domain_strategy,omitempty"`
 	UDPDisableDomainUnmapping bool           `json:"udp_disable_domain_unmapping,omitempty"`
+	Detour                    string         `json:"detour,omitempty"`
 }
 
 type ListenOptions struct {
-	Listen                      *ListenAddress   `json:"listen,omitempty"`
-	ListenPort                  uint16           `json:"listen_port,omitempty"`
-	TCPFastOpen                 bool             `json:"tcp_fast_open,omitempty"`
-	TCPMultiPath                bool             `json:"tcp_multi_path,omitempty"`
-	UDPFragment                 *bool            `json:"udp_fragment,omitempty"`
-	UDPFragmentDefault          bool             `json:"-"`
-	UDPTimeout                  UDPTimeoutCompat `json:"udp_timeout,omitempty"`
-	ProxyProtocol               bool             `json:"proxy_protocol,omitempty"`
-	ProxyProtocolAcceptNoHeader bool             `json:"proxy_protocol_accept_no_header,omitempty"`
-	Detour                      string           `json:"detour,omitempty"`
+	Listen               *ListenAddress   `json:"listen,omitempty"`
+	ListenPort           uint16           `json:"listen_port,omitempty"`
+	TCPKeepAlive         Duration         `json:"tcp_keep_alive,omitempty"`
+	TCPKeepAliveInterval Duration         `json:"tcp_keep_alive_interval,omitempty"`
+	TCPFastOpen          bool             `json:"tcp_fast_open,omitempty"`
+	TCPMultiPath         bool             `json:"tcp_multi_path,omitempty"`
+	UDPFragment          *bool            `json:"udp_fragment,omitempty"`
+	UDPFragmentDefault   bool             `json:"-"`
+	UDPTimeout           UDPTimeoutCompat `json:"udp_timeout,omitempty"`
+
+	// Deprecated: removed
+	ProxyProtocol bool `json:"proxy_protocol,omitempty"`
+	// Deprecated: removed
+	ProxyProtocolAcceptNoHeader bool `json:"proxy_protocol_accept_no_header,omitempty"`
 	InboundOptions
 }
 
diff --git a/option/inbound_legacy.go b/option/inbound_legacy.go
new file mode 100644
index 00000000..e7f34678
--- /dev/null
+++ b/option/inbound_legacy.go
@@ -0,0 +1,98 @@
+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 _LegacyInbound struct {
+	Type               string                    `json:"type"`
+	Tag                string                    `json:"tag,omitempty"`
+	TunOptions         TunInboundOptions         `json:"-"`
+	RedirectOptions    RedirectInboundOptions    `json:"-"`
+	TProxyOptions      TProxyInboundOptions      `json:"-"`
+	DirectOptions      DirectInboundOptions      `json:"-"`
+	SocksOptions       SocksInboundOptions       `json:"-"`
+	HTTPOptions        HTTPMixedInboundOptions   `json:"-"`
+	MixedOptions       HTTPMixedInboundOptions   `json:"-"`
+	ShadowsocksOptions ShadowsocksInboundOptions `json:"-"`
+	VMessOptions       VMessInboundOptions       `json:"-"`
+	TrojanOptions      TrojanInboundOptions      `json:"-"`
+	NaiveOptions       NaiveInboundOptions       `json:"-"`
+	HysteriaOptions    HysteriaInboundOptions    `json:"-"`
+	ShadowTLSOptions   ShadowTLSInboundOptions   `json:"-"`
+	VLESSOptions       VLESSInboundOptions       `json:"-"`
+	TUICOptions        TUICInboundOptions        `json:"-"`
+	Hysteria2Options   Hysteria2InboundOptions   `json:"-"`
+}
+
+type LegacyInbound _LegacyInbound
+
+func (h *LegacyInbound) RawOptions() (any, error) {
+	var rawOptionsPtr any
+	switch h.Type {
+	case C.TypeTun:
+		rawOptionsPtr = &h.TunOptions
+	case C.TypeRedirect:
+		rawOptionsPtr = &h.RedirectOptions
+	case C.TypeTProxy:
+		rawOptionsPtr = &h.TProxyOptions
+	case C.TypeDirect:
+		rawOptionsPtr = &h.DirectOptions
+	case C.TypeSOCKS:
+		rawOptionsPtr = &h.SocksOptions
+	case C.TypeHTTP:
+		rawOptionsPtr = &h.HTTPOptions
+	case C.TypeMixed:
+		rawOptionsPtr = &h.MixedOptions
+	case C.TypeShadowsocks:
+		rawOptionsPtr = &h.ShadowsocksOptions
+	case C.TypeVMess:
+		rawOptionsPtr = &h.VMessOptions
+	case C.TypeTrojan:
+		rawOptionsPtr = &h.TrojanOptions
+	case C.TypeNaive:
+		rawOptionsPtr = &h.NaiveOptions
+	case C.TypeHysteria:
+		rawOptionsPtr = &h.HysteriaOptions
+	case C.TypeShadowTLS:
+		rawOptionsPtr = &h.ShadowTLSOptions
+	case C.TypeVLESS:
+		rawOptionsPtr = &h.VLESSOptions
+	case C.TypeTUIC:
+		rawOptionsPtr = &h.TUICOptions
+	case C.TypeHysteria2:
+		rawOptionsPtr = &h.Hysteria2Options
+	case "":
+		return nil, E.New("missing inbound type")
+	default:
+		return nil, E.New("unknown inbound type: ", h.Type)
+	}
+	return rawOptionsPtr, nil
+}
+
+func (h LegacyInbound) MarshalJSON() ([]byte, error) {
+	rawOptions, err := h.RawOptions()
+	if err != nil {
+		return nil, err
+	}
+	return badjson.MarshallObjects((_LegacyInbound)(h), rawOptions)
+}
+
+func (h *LegacyInbound) UnmarshalJSON(bytes []byte) error {
+	err := json.Unmarshal(bytes, (*_LegacyInbound)(h))
+	if err != nil {
+		return err
+	}
+	rawOptions, err := h.RawOptions()
+	if err != nil {
+		return err
+	}
+	err = badjson.UnmarshallExcluded(bytes, (*_LegacyInbound)(h), rawOptions)
+	if err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/option/json.go b/option/json.go
deleted file mode 100644
index 775141d5..00000000
--- a/option/json.go
+++ /dev/null
@@ -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)
-}
diff --git a/option/config.go b/option/options.go
similarity index 74%
rename from option/config.go
rename to option/options.go
index 3f5d7602..c4811fa9 100644
--- a/option/config.go
+++ b/option/options.go
@@ -2,6 +2,7 @@ package option
 
 import (
 	"bytes"
+	"context"
 
 	"github.com/sagernet/sing/common/json"
 )
@@ -16,12 +17,17 @@ type _Options struct {
 	Outbounds    []Outbound           `json:"outbounds,omitempty"`
 	Route        *RouteOptions        `json:"route,omitempty"`
 	Experimental *ExperimentalOptions `json:"experimental,omitempty"`
+
+	// Deprecated: use Inbounds instead
+	LegacyInbounds []LegacyInbound `json:"inbound,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 +44,5 @@ type LogOptions struct {
 	Timestamp    bool   `json:"timestamp,omitempty"`
 	DisableColor bool   `json:"-"`
 }
+
+type StubOptions struct{}
diff --git a/option/outbound.go b/option/outbound.go
index 6c943cd9..00a20aa5 100644
--- a/option/outbound.go
+++ b/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
-	}
-	err = UnmarshallExcluded(bytes, (*_Outbound)(h), rawOptions)
+	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 = badjson.UnmarshallExcludedContext(ctx, content, (*_Outbound)(h), options)
 	if err != nil {
 		return err
 	}
+	h.Options = options
 	return nil
 }
 
diff --git a/option/outbound_legacy.go b/option/outbound_legacy.go
new file mode 100644
index 00000000..d840884b
--- /dev/null
+++ b/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
+}
diff --git a/option/rule.go b/option/rule.go
index 0b11cbdd..07e6ddbe 100644
--- a/option/rule.go
+++ b/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 {
diff --git a/option/rule_action.go b/option/rule_action.go
index f446d81d..e752a2be 100644
--- a/option/rule_action.go
+++ b/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 {
diff --git a/option/rule_dns.go b/option/rule_dns.go
index b328c45c..8c4b6ab8 100644
--- a/option/rule_dns.go
+++ b/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 {
diff --git a/option/rule_set.go b/option/rule_set.go
index d4368de3..358821de 100644
--- a/option/rule_set.go
+++ b/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
 	}
diff --git a/option/simple.go b/option/simple.go
index ba9d6bf1..78171ce4 100644
--- a/option/simple.go
+++ b/option/simple.go
@@ -14,7 +14,7 @@ type HTTPMixedInboundOptions struct {
 	InboundTLSOptionsContainer
 }
 
-type SocksOutboundOptions struct {
+type SOCKSOutboundOptions struct {
 	DialerOptions
 	ServerOptions
 	Version    string             `json:"version,omitempty"`
diff --git a/option/tls_acme.go b/option/tls_acme.go
index 17d515e2..9c2e081f 100644
--- a/option/tls_acme.go
+++ b/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
 	}
diff --git a/option/v2ray_transport.go b/option/v2ray_transport.go
index fcd81f94..f87b175d 100644
--- a/option/v2ray_transport.go
+++ b/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
 	}
diff --git a/outbound/block.go b/outbound/block.go
deleted file mode 100644
index b6ccefe2..00000000
--- a/outbound/block.go
+++ /dev/null
@@ -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
-}
diff --git a/outbound/builder.go b/outbound/builder.go
deleted file mode 100644
index d895b56d..00000000
--- a/outbound/builder.go
+++ /dev/null
@@ -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)
-	}
-}
diff --git a/outbound/hysteria_stub.go b/outbound/hysteria_stub.go
deleted file mode 100644
index 84db5305..00000000
--- a/outbound/hysteria_stub.go
+++ /dev/null
@@ -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
-}
diff --git a/outbound/lookback.go b/outbound/lookback.go
deleted file mode 100644
index aeb7451d..00000000
--- a/outbound/lookback.go
+++ /dev/null
@@ -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
-}
diff --git a/outbound/shadowsocksr.go b/outbound/shadowsocksr.go
deleted file mode 100644
index 615a71e4..00000000
--- a/outbound/shadowsocksr.go
+++ /dev/null
@@ -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
-}
diff --git a/outbound/shadowsocksr_stub.go b/outbound/shadowsocksr_stub.go
deleted file mode 100644
index 94971da0..00000000
--- a/outbound/shadowsocksr_stub.go
+++ /dev/null
@@ -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")
-}
diff --git a/outbound/tor_embed.go b/outbound/tor_embed.go
deleted file mode 100644
index d80b49ae..00000000
--- a/outbound/tor_embed.go
+++ /dev/null
@@ -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,
-	}
-}
diff --git a/outbound/tor_embed_mobile.go b/outbound/tor_embed_mobile.go
deleted file mode 100644
index 0900d8c9..00000000
--- a/outbound/tor_embed_mobile.go
+++ /dev/null
@@ -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,
-	}
-}
diff --git a/outbound/tor_external.go b/outbound/tor_external.go
deleted file mode 100644
index 6bce95d1..00000000
--- a/outbound/tor_external.go
+++ /dev/null
@@ -1,9 +0,0 @@
-//go:build !with_embedded_tor
-
-package outbound
-
-import "github.com/cretz/bine/tor"
-
-func newConfig() tor.StartConf {
-	return tor.StartConf{}
-}
diff --git a/outbound/tuic_stub.go b/outbound/tuic_stub.go
deleted file mode 100644
index a6372c9e..00000000
--- a/outbound/tuic_stub.go
+++ /dev/null
@@ -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
-}
diff --git a/outbound/wireguard_stub.go b/outbound/wireguard_stub.go
deleted file mode 100644
index 3a8b0e87..00000000
--- a/outbound/wireguard_stub.go
+++ /dev/null
@@ -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`)
-}
diff --git a/protocol/block/outbound.go b/protocol/block/outbound.go
new file mode 100644
index 00000000..75bc7797
--- /dev/null
+++ b/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
+}
diff --git a/protocol/direct/inbound.go b/protocol/direct/inbound.go
new file mode 100644
index 00000000..568a72cb
--- /dev/null
+++ b/protocol/direct/inbound.go
@@ -0,0 +1,139 @@
+package direct
+
+import (
+	"context"
+	"net"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common/buf"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+	"github.com/sagernet/sing/common/udpnat2"
+)
+
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.DirectInboundOptions](registry, C.TypeDirect, NewInbound)
+}
+
+type Inbound struct {
+	inbound.Adapter
+	ctx                 context.Context
+	router              adapter.ConnectionRouterEx
+	logger              log.ContextLogger
+	listener            *listener.Listener
+	udpNat              *udpnat.Service
+	overrideOption      int
+	overrideDestination M.Socksaddr
+}
+
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectInboundOptions) (adapter.Inbound, error) {
+	options.UDPFragmentDefault = true
+	inbound := &Inbound{
+		Adapter: inbound.NewAdapter(C.TypeDirect, tag),
+		ctx:     ctx,
+		router:  router,
+		logger:  logger,
+	}
+	if options.OverrideAddress != "" && options.OverridePort != 0 {
+		inbound.overrideOption = 1
+		inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort)
+	} else if options.OverrideAddress != "" {
+		inbound.overrideOption = 2
+		inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort)
+	} else if options.OverridePort != 0 {
+		inbound.overrideOption = 3
+		inbound.overrideDestination = M.Socksaddr{Port: options.OverridePort}
+	}
+	var udpTimeout time.Duration
+	if options.UDPTimeout != 0 {
+		udpTimeout = time.Duration(options.UDPTimeout)
+	} else {
+		udpTimeout = C.UDPTimeout
+	}
+	inbound.udpNat = udpnat.New(inbound, inbound.preparePacketConnection, udpTimeout, false)
+	inbound.listener = listener.New(listener.Options{
+		Context:           ctx,
+		Logger:            logger,
+		Network:           options.Network.Build(),
+		Listen:            options.ListenOptions,
+		ConnectionHandler: inbound,
+		PacketHandler:     inbound,
+	})
+	return inbound, nil
+}
+
+func (i *Inbound) Start() error {
+	return i.listener.Start()
+}
+
+func (i *Inbound) Close() error {
+	return i.listener.Close()
+}
+
+func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	switch i.overrideOption {
+	case 1:
+		metadata.Destination = i.overrideDestination
+	case 2:
+		destination := i.overrideDestination
+		destination.Port = metadata.Destination.Port
+		metadata.Destination = destination
+	case 3:
+		metadata.Destination.Port = i.overrideDestination.Port
+	}
+	i.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+	return i.router.RouteConnection(ctx, conn, metadata)
+}
+
+func (i *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) {
+	var destination M.Socksaddr
+	switch i.overrideOption {
+	case 1:
+		destination = i.overrideDestination
+	case 2:
+		destination = i.overrideDestination
+		destination.Port = source.Port
+	case 3:
+		destination = source
+		destination.Port = i.overrideDestination.Port
+	}
+	i.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, destination, nil)
+}
+
+func (i *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	i.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+	metadata.Inbound = i.Tag()
+	metadata.InboundType = i.Type()
+	i.router.RouteConnectionEx(ctx, conn, metadata, onClose)
+}
+
+func (i *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+	i.logger.InfoContext(ctx, "inbound packet connection from ", source)
+	i.logger.InfoContext(ctx, "inbound packet connection to ", destination)
+	var metadata adapter.InboundContext
+	metadata.Inbound = i.Tag()
+	metadata.InboundType = i.Type()
+	metadata.Source = source
+	metadata.Destination = destination
+	metadata.OriginDestination = i.listener.UDPAddr()
+	i.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
+}
+
+func (i *Inbound) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) {
+	return true, log.ContextWithNewID(i.ctx), &directPacketWriter{i.listener.PacketWriter(), source}, nil
+}
+
+type directPacketWriter struct {
+	writer N.PacketWriter
+	source M.Socksaddr
+}
+
+func (w *directPacketWriter) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error {
+	return w.writer.WritePacket(buffer, w.source)
+}
diff --git a/outbound/direct_loopback_detect.go b/protocol/direct/loopback_detect.go
similarity index 99%
rename from outbound/direct_loopback_detect.go
rename to protocol/direct/loopback_detect.go
index 1469b9d0..5a184e69 100644
--- a/outbound/direct_loopback_detect.go
+++ b/protocol/direct/loopback_detect.go
@@ -1,4 +1,4 @@
-package outbound
+package direct
 
 import (
 	"net"
diff --git a/outbound/direct.go b/protocol/direct/outbound.go
similarity index 76%
rename from outbound/direct.go
rename to protocol/direct/outbound.go
index 415a72f3..32c1ed8f 100644
--- a/outbound/direct.go
+++ b/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)
+}
 
-type Direct struct {
-	myOutboundAdapter
+var _ N.ParallelDialer = (*Outbound)(nil)
+
+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)
 	}
diff --git a/outbound/dns.go b/protocol/dns/handle.go
similarity index 83%
rename from outbound/dns.go
rename to protocol/dns/handle.go
index d9c92f19..23ed1c0c 100644
--- a/outbound/dns.go
+++ b/protocol/dns/handle.go
@@ -1,11 +1,9 @@
-package outbound
+package dns
 
 import (
 	"context"
 	"encoding/binary"
 	"net"
-	"os"
-	"time"
 
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
@@ -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
diff --git a/protocol/dns/outbound.go b/protocol/dns/outbound.go
new file mode 100644
index 00000000..7ce9fde2
--- /dev/null
+++ b/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)
+}
diff --git a/outbound/selector.go b/protocol/group/selector.go
similarity index 82%
rename from outbound/selector.go
rename to protocol/group/selector.go
index 59e940df..32ab8b2a 100644
--- a/outbound/selector.go
+++ b/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)
 	}
 }
 
diff --git a/outbound/urltest.go b/protocol/group/urltest.go
similarity index 94%
rename from outbound/urltest.go
rename to protocol/group/urltest.go
index 564a0ddc..ccdf809d 100644
--- a/outbound/urltest.go
+++ b/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() {
diff --git a/protocol/http/inbound.go b/protocol/http/inbound.go
new file mode 100644
index 00000000..87ed9a10
--- /dev/null
+++ b/protocol/http/inbound.go
@@ -0,0 +1,122 @@
+package http
+
+import (
+	std_bufio "bufio"
+	"context"
+	"net"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
+	"github.com/sagernet/sing-box/common/tls"
+	"github.com/sagernet/sing-box/common/uot"
+	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/auth"
+	E "github.com/sagernet/sing/common/exceptions"
+	N "github.com/sagernet/sing/common/network"
+	"github.com/sagernet/sing/protocol/http"
+)
+
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.HTTPMixedInboundOptions](registry, C.TypeHTTP, NewInbound)
+}
+
+var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
+
+type Inbound struct {
+	inbound.Adapter
+	router        adapter.ConnectionRouterEx
+	logger        log.ContextLogger
+	listener      *listener.Listener
+	authenticator *auth.Authenticator
+	tlsConfig     tls.ServerConfig
+}
+
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) {
+	inbound := &Inbound{
+		Adapter:       inbound.NewAdapter(C.TypeHTTP, tag),
+		router:        uot.NewRouter(router, logger),
+		logger:        logger,
+		authenticator: auth.NewAuthenticator(options.Users),
+	}
+	if options.TLS != nil {
+		tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
+		if err != nil {
+			return nil, err
+		}
+		inbound.tlsConfig = tlsConfig
+	}
+	inbound.listener = listener.New(listener.Options{
+		Context:           ctx,
+		Logger:            logger,
+		Network:           []string{N.NetworkTCP},
+		Listen:            options.ListenOptions,
+		ConnectionHandler: inbound,
+		SetSystemProxy:    options.SetSystemProxy,
+		SystemProxySOCKS:  false,
+	})
+	return inbound, nil
+}
+
+func (h *Inbound) Start() error {
+	if h.tlsConfig != nil {
+		err := h.tlsConfig.Start()
+		if err != nil {
+			return E.Cause(err, "create TLS config")
+		}
+	}
+	return h.listener.Start()
+}
+
+func (h *Inbound) Close() error {
+	return common.Close(
+		&h.listener,
+		h.tlsConfig,
+	)
+}
+
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	err := h.newConnection(ctx, conn, metadata, onClose)
+	N.CloseOnHandshakeFailure(conn, onClose, err)
+	if err != nil {
+		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+	}
+}
+
+func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
+	var err error
+	if h.tlsConfig != nil {
+		conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig)
+		if err != nil {
+			return err
+		}
+	}
+	return http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose)
+}
+
+func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	user, loaded := auth.UserFromContext[string](ctx)
+	if !loaded {
+		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+		h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
+		return
+	}
+	metadata.User = user
+	h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
+	h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
+}
+
+func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	user, loaded := auth.UserFromContext[string](ctx)
+	if !loaded {
+		h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
+		h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
+		return
+	}
+	metadata.User = user
+	h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
+	h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
+}
diff --git a/outbound/http.go b/protocol/http/outbound.go
similarity index 56%
rename from outbound/http.go
rename to protocol/http/outbound.go
index 6f15afb5..4c930591 100644
--- a/outbound/http.go
+++ b/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
 }
diff --git a/inbound/hysteria.go b/protocol/hysteria/inbound.go
similarity index 71%
rename from inbound/hysteria.go
rename to protocol/hysteria/inbound.go
index e415d570..8127106b 100644
--- a/inbound/hysteria.go
+++ b/protocol/hysteria/inbound.go
@@ -1,6 +1,4 @@
-//go:build with_quic
-
-package inbound
+package hysteria
 
 import (
 	"context"
@@ -8,7 +6,9 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
 	"github.com/sagernet/sing-box/common/humanize"
+	"github.com/sagernet/sing-box/common/listener"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
@@ -20,16 +20,21 @@ import (
 	N "github.com/sagernet/sing/common/network"
 )
 
-var _ adapter.Inbound = (*Hysteria)(nil)
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.HysteriaInboundOptions](registry, C.TypeHysteria, NewInbound)
+}
 
-type Hysteria struct {
-	myInboundAdapter
+type Inbound struct {
+	inbound.Adapter
+	router       adapter.Router
+	logger       log.ContextLogger
+	listener     *listener.Listener
 	tlsConfig    tls.ServerConfig
 	service      *hysteria.Service[int]
 	userNameList []string
 }
 
-func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (*Hysteria, error) {
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) {
 	options.UDPFragmentDefault = true
 	if options.TLS == nil || !options.TLS.Enabled {
 		return nil, C.ErrTLSRequired
@@ -38,16 +43,15 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
 	if err != nil {
 		return nil, err
 	}
-	inbound := &Hysteria{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeHysteria,
-			network:       []string{N.NetworkUDP},
-			ctx:           ctx,
-			router:        router,
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
+	inbound := &Inbound{
+		Adapter: inbound.NewAdapter(C.TypeHysteria, tag),
+		router:  router,
+		logger:  logger,
+		listener: listener.New(listener.Options{
+			Context: ctx,
+			Logger:  logger,
+			Listen:  options.ListenOptions,
+		}),
 		tlsConfig: tlsConfig,
 	}
 	var sendBps, receiveBps uint64
@@ -113,9 +117,12 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
 	return inbound, nil
 }
 
-func (h *Hysteria) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	ctx = log.ContextWithNewID(ctx)
-	metadata = h.createMetadata(conn, metadata)
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
 	userID, _ := auth.UserFromContext[int](ctx)
 	if userName := h.userNameList[userID]; userName != "" {
@@ -127,9 +134,13 @@ func (h *Hysteria) newConnection(ctx context.Context, conn net.Conn, metadata ad
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
-func (h *Hysteria) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	ctx = log.ContextWithNewID(ctx)
-	metadata = h.createPacketMetadata(conn, metadata)
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	metadata.OriginDestination = h.listener.UDPAddr()
 	h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
 	userID, _ := auth.UserFromContext[int](ctx)
 	if userName := h.userNameList[userID]; userName != "" {
@@ -141,23 +152,23 @@ func (h *Hysteria) newPacketConnection(ctx context.Context, conn N.PacketConn, m
 	return h.router.RoutePacketConnection(ctx, conn, metadata)
 }
 
-func (h *Hysteria) Start() error {
+func (h *Inbound) Start() error {
 	if h.tlsConfig != nil {
 		err := h.tlsConfig.Start()
 		if err != nil {
 			return err
 		}
 	}
-	packetConn, err := h.myInboundAdapter.ListenUDP()
+	packetConn, err := h.listener.ListenUDP()
 	if err != nil {
 		return err
 	}
 	return h.service.Start(packetConn)
 }
 
-func (h *Hysteria) Close() error {
+func (h *Inbound) Close() error {
 	return common.Close(
-		&h.myInboundAdapter,
+		&h.listener,
 		h.tlsConfig,
 		common.PtrOrNil(h.service),
 	)
diff --git a/outbound/hysteria.go b/protocol/hysteria/outbound.go
similarity index 76%
rename from outbound/hysteria.go
rename to protocol/hysteria/outbound.go
index f513cf64..4722f4f0 100644
--- a/outbound/hysteria.go
+++ b/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)
 }
diff --git a/inbound/hysteria2.go b/protocol/hysteria2/inbound.go
similarity index 74%
rename from inbound/hysteria2.go
rename to protocol/hysteria2/inbound.go
index c13e9531..cbf81109 100644
--- a/inbound/hysteria2.go
+++ b/protocol/hysteria2/inbound.go
@@ -1,6 +1,4 @@
-//go:build with_quic
-
-package inbound
+package hysteria2
 
 import (
 	"context"
@@ -11,6 +9,8 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
@@ -23,16 +23,21 @@ import (
 	N "github.com/sagernet/sing/common/network"
 )
 
-var _ adapter.Inbound = (*Hysteria2)(nil)
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.Hysteria2InboundOptions](registry, C.TypeHysteria2, NewInbound)
+}
 
-type Hysteria2 struct {
-	myInboundAdapter
+type Inbound struct {
+	inbound.Adapter
+	router       adapter.Router
+	logger       log.ContextLogger
+	listener     *listener.Listener
 	tlsConfig    tls.ServerConfig
 	service      *hysteria2.Service[int]
 	userNameList []string
 }
 
-func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (*Hysteria2, error) {
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) {
 	options.UDPFragmentDefault = true
 	if options.TLS == nil || !options.TLS.Enabled {
 		return nil, C.ErrTLSRequired
@@ -76,16 +81,15 @@ func NewHysteria2(ctx context.Context, router adapter.Router, logger log.Context
 			return nil, E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme)
 		}
 	}
-	inbound := &Hysteria2{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeHysteria2,
-			network:       []string{N.NetworkUDP},
-			ctx:           ctx,
-			router:        router,
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
+	inbound := &Inbound{
+		Adapter: inbound.NewAdapter(C.TypeHysteria2, tag),
+		router:  router,
+		logger:  logger,
+		listener: listener.New(listener.Options{
+			Context: ctx,
+			Logger:  logger,
+			Listen:  options.ListenOptions,
+		}),
 		tlsConfig: tlsConfig,
 	}
 	var udpTimeout time.Duration
@@ -124,9 +128,12 @@ func NewHysteria2(ctx context.Context, router adapter.Router, logger log.Context
 	return inbound, nil
 }
 
-func (h *Hysteria2) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	ctx = log.ContextWithNewID(ctx)
-	metadata = h.createMetadata(conn, metadata)
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
 	userID, _ := auth.UserFromContext[int](ctx)
 	if userName := h.userNameList[userID]; userName != "" {
@@ -138,9 +145,13 @@ func (h *Hysteria2) newConnection(ctx context.Context, conn net.Conn, metadata a
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
-func (h *Hysteria2) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	ctx = log.ContextWithNewID(ctx)
-	metadata = h.createPacketMetadata(conn, metadata)
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	metadata.OriginDestination = h.listener.UDPAddr()
 	h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
 	userID, _ := auth.UserFromContext[int](ctx)
 	if userName := h.userNameList[userID]; userName != "" {
@@ -152,23 +163,23 @@ func (h *Hysteria2) newPacketConnection(ctx context.Context, conn N.PacketConn,
 	return h.router.RoutePacketConnection(ctx, conn, metadata)
 }
 
-func (h *Hysteria2) Start() error {
+func (h *Inbound) Start() error {
 	if h.tlsConfig != nil {
 		err := h.tlsConfig.Start()
 		if err != nil {
 			return err
 		}
 	}
-	packetConn, err := h.myInboundAdapter.ListenUDP()
+	packetConn, err := h.listener.ListenUDP()
 	if err != nil {
 		return err
 	}
 	return h.service.Start(packetConn)
 }
 
-func (h *Hysteria2) Close() error {
+func (h *Inbound) Close() error {
 	return common.Close(
-		&h.myInboundAdapter,
+		&h.listener,
 		h.tlsConfig,
 		common.PtrOrNil(h.service),
 	)
diff --git a/outbound/hysteria2.go b/protocol/hysteria2/outbound.go
similarity index 69%
rename from outbound/hysteria2.go
rename to protocol/hysteria2/outbound.go
index 5e46f6a8..5ebc6c91 100644
--- a/outbound/hysteria2.go
+++ b/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)
 }
diff --git a/protocol/mixed/inbound.go b/protocol/mixed/inbound.go
new file mode 100644
index 00000000..e57b791f
--- /dev/null
+++ b/protocol/mixed/inbound.go
@@ -0,0 +1,109 @@
+package mixed
+
+import (
+	std_bufio "bufio"
+	"context"
+	"net"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
+	"github.com/sagernet/sing-box/common/uot"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common/auth"
+	E "github.com/sagernet/sing/common/exceptions"
+	N "github.com/sagernet/sing/common/network"
+	"github.com/sagernet/sing/protocol/http"
+	"github.com/sagernet/sing/protocol/socks"
+	"github.com/sagernet/sing/protocol/socks/socks4"
+	"github.com/sagernet/sing/protocol/socks/socks5"
+)
+
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.HTTPMixedInboundOptions](registry, C.TypeMixed, NewInbound)
+}
+
+var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
+
+type Inbound struct {
+	inbound.Adapter
+	router        adapter.ConnectionRouterEx
+	logger        log.ContextLogger
+	listener      *listener.Listener
+	authenticator *auth.Authenticator
+}
+
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) {
+	inbound := &Inbound{
+		Adapter:       inbound.NewAdapter(C.TypeMixed, tag),
+		router:        uot.NewRouter(router, logger),
+		logger:        logger,
+		authenticator: auth.NewAuthenticator(options.Users),
+	}
+	inbound.listener = listener.New(listener.Options{
+		Context:           ctx,
+		Logger:            logger,
+		Network:           []string{N.NetworkTCP},
+		Listen:            options.ListenOptions,
+		ConnectionHandler: inbound,
+		SetSystemProxy:    options.SetSystemProxy,
+		SystemProxySOCKS:  true,
+	})
+	return inbound, nil
+}
+
+func (h *Inbound) Start() error {
+	return h.listener.Start()
+}
+
+func (h *Inbound) Close() error {
+	return h.listener.Close()
+}
+
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	err := h.newConnection(ctx, conn, metadata, onClose)
+	N.CloseOnHandshakeFailure(conn, onClose, err)
+	if err != nil {
+		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+	}
+}
+
+func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
+	reader := std_bufio.NewReader(conn)
+	headerBytes, err := reader.Peek(1)
+	if err != nil {
+		return E.Cause(err, "peek first byte")
+	}
+	switch headerBytes[0] {
+	case socks4.Version, socks5.Version:
+		return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, metadata.Destination, onClose)
+	default:
+		return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose)
+	}
+}
+
+func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	user, loaded := auth.UserFromContext[string](ctx)
+	if !loaded {
+		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+		h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
+		return
+	}
+	metadata.User = user
+	h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
+	h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
+}
+
+func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	user, loaded := auth.UserFromContext[string](ctx)
+	if !loaded {
+		h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
+		h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
+		return
+	}
+	metadata.User = user
+	h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
+	h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
+}
diff --git a/protocol/naive/inbound.go b/protocol/naive/inbound.go
new file mode 100644
index 00000000..d75f3be6
--- /dev/null
+++ b/protocol/naive/inbound.go
@@ -0,0 +1,247 @@
+package naive
+
+import (
+	"context"
+	"io"
+	"math/rand"
+	"net"
+	"net/http"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
+	"github.com/sagernet/sing-box/common/tls"
+	"github.com/sagernet/sing-box/common/uot"
+	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/v2rayhttp"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/auth"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/logger"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+	sHttp "github.com/sagernet/sing/protocol/http"
+)
+
+var ConfigureHTTP3ListenerFunc func(listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, logger logger.Logger) (io.Closer, error)
+
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.NaiveInboundOptions](registry, C.TypeNaive, NewInbound)
+}
+
+type Inbound struct {
+	inbound.Adapter
+	ctx              context.Context
+	router           adapter.ConnectionRouterEx
+	logger           logger.ContextLogger
+	listener         *listener.Listener
+	network          []string
+	networkIsDefault bool
+	authenticator    *auth.Authenticator
+	tlsConfig        tls.ServerConfig
+	httpServer       *http.Server
+	h3Server         io.Closer
+}
+
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveInboundOptions) (adapter.Inbound, error) {
+	inbound := &Inbound{
+		Adapter: inbound.NewAdapter(C.TypeNaive, tag),
+		ctx:     ctx,
+		router:  uot.NewRouter(router, logger),
+		listener: listener.New(listener.Options{
+			Context: ctx,
+			Logger:  logger,
+			Listen:  options.ListenOptions,
+		}),
+		networkIsDefault: options.Network == "",
+		network:          options.Network.Build(),
+		authenticator:    auth.NewAuthenticator(options.Users),
+	}
+	if common.Contains(inbound.network, N.NetworkUDP) {
+		if options.TLS == nil || !options.TLS.Enabled {
+			return nil, E.New("TLS is required for QUIC server")
+		}
+	}
+	if len(options.Users) == 0 {
+		return nil, E.New("missing users")
+	}
+	if options.TLS != nil {
+		tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
+		if err != nil {
+			return nil, err
+		}
+		inbound.tlsConfig = tlsConfig
+	}
+	return inbound, nil
+}
+
+func (n *Inbound) Start() error {
+	var tlsConfig *tls.STDConfig
+	if n.tlsConfig != nil {
+		err := n.tlsConfig.Start()
+		if err != nil {
+			return E.Cause(err, "create TLS config")
+		}
+		tlsConfig, err = n.tlsConfig.Config()
+		if err != nil {
+			return err
+		}
+	}
+	if common.Contains(n.network, N.NetworkTCP) {
+		tcpListener, err := n.listener.ListenTCP()
+		if err != nil {
+			return err
+		}
+		n.httpServer = &http.Server{
+			Handler:   n,
+			TLSConfig: tlsConfig,
+			BaseContext: func(listener net.Listener) context.Context {
+				return n.ctx
+			},
+		}
+		go func() {
+			var sErr error
+			if tlsConfig != nil {
+				sErr = n.httpServer.ServeTLS(tcpListener, "", "")
+			} else {
+				sErr = n.httpServer.Serve(tcpListener)
+			}
+			if sErr != nil && !E.IsClosedOrCanceled(sErr) {
+				n.logger.Error("http server serve error: ", sErr)
+			}
+		}()
+	}
+
+	if common.Contains(n.network, N.NetworkUDP) {
+		http3Server, err := ConfigureHTTP3ListenerFunc(n.listener, n, n.tlsConfig, n.logger)
+		if err == nil {
+			n.h3Server = http3Server
+		} else if len(n.network) > 1 {
+			n.logger.Warn(E.Cause(err, "naive http3 disabled"))
+		} else {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (n *Inbound) Close() error {
+	return common.Close(
+		&n.listener,
+		common.PtrOrNil(n.httpServer),
+		n.h3Server,
+		n.tlsConfig,
+	)
+}
+
+func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+	ctx := log.ContextWithNewID(request.Context())
+	if request.Method != "CONNECT" {
+		rejectHTTP(writer, http.StatusBadRequest)
+		n.badRequest(ctx, request, E.New("not CONNECT request"))
+		return
+	} else if request.Header.Get("Padding") == "" {
+		rejectHTTP(writer, http.StatusBadRequest)
+		n.badRequest(ctx, request, E.New("missing naive padding"))
+		return
+	}
+	userName, password, authOk := sHttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization"))
+	if authOk {
+		authOk = n.authenticator.Verify(userName, password)
+	}
+	if !authOk {
+		rejectHTTP(writer, http.StatusProxyAuthRequired)
+		n.badRequest(ctx, request, E.New("authorization failed"))
+		return
+	}
+	writer.Header().Set("Padding", generateNaivePaddingHeader())
+	writer.WriteHeader(http.StatusOK)
+	writer.(http.Flusher).Flush()
+
+	hostPort := request.URL.Host
+	if hostPort == "" {
+		hostPort = request.Host
+	}
+	source := sHttp.SourceAddress(request)
+	destination := M.ParseSocksaddr(hostPort)
+
+	if hijacker, isHijacker := writer.(http.Hijacker); isHijacker {
+		conn, _, err := hijacker.Hijack()
+		if err != nil {
+			n.badRequest(ctx, request, E.New("hijack failed"))
+			return
+		}
+		n.newConnection(ctx, false, &naiveH1Conn{Conn: conn}, userName, source, destination)
+	} else {
+		n.newConnection(ctx, true, &naiveH2Conn{reader: request.Body, writer: writer, flusher: writer.(http.Flusher)}, userName, source, destination)
+	}
+}
+
+func (n *Inbound) newConnection(ctx context.Context, waitForClose bool, conn net.Conn, userName string, source M.Socksaddr, destination M.Socksaddr) {
+	if userName != "" {
+		n.logger.InfoContext(ctx, "[", userName, "] inbound connection from ", source)
+		n.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", destination)
+	} else {
+		n.logger.InfoContext(ctx, "inbound connection from ", source)
+		n.logger.InfoContext(ctx, "inbound connection to ", destination)
+	}
+	var metadata adapter.InboundContext
+	metadata.Inbound = n.Tag()
+	metadata.InboundType = n.Type()
+	metadata.InboundDetour = n.listener.ListenOptions().Detour
+	metadata.InboundOptions = n.listener.ListenOptions().InboundOptions
+	metadata.Source = source
+	metadata.Destination = destination
+	metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
+	metadata.User = userName
+	if !waitForClose {
+		n.router.RouteConnectionEx(ctx, conn, metadata, nil)
+	} else {
+		done := make(chan struct{})
+		wrapper := v2rayhttp.NewHTTP2Wrapper(conn)
+		n.router.RouteConnectionEx(ctx, conn, metadata, N.OnceClose(func(it error) {
+			close(done)
+		}))
+		<-done
+		wrapper.CloseWrapper()
+	}
+}
+
+func (n *Inbound) badRequest(ctx context.Context, request *http.Request, err error) {
+	n.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr))
+}
+
+func rejectHTTP(writer http.ResponseWriter, statusCode int) {
+	hijacker, ok := writer.(http.Hijacker)
+	if !ok {
+		writer.WriteHeader(statusCode)
+		return
+	}
+	conn, _, err := hijacker.Hijack()
+	if err != nil {
+		writer.WriteHeader(statusCode)
+		return
+	}
+	if tcpConn, isTCP := common.Cast[*net.TCPConn](conn); isTCP {
+		tcpConn.SetLinger(0)
+	}
+	conn.Close()
+}
+
+func generateNaivePaddingHeader() string {
+	paddingLen := rand.Intn(32) + 30
+	padding := make([]byte, paddingLen)
+	bits := rand.Uint64()
+	for i := 0; i < 16; i++ {
+		// Codes that won't be Huffman coded.
+		padding[i] = "!#$()+<>?@[]^`{}"[bits&15]
+		bits >>= 4
+	}
+	for i := 16; i < paddingLen; i++ {
+		padding[i] = '~'
+	}
+	return string(padding)
+}
diff --git a/inbound/naive.go b/protocol/naive/inbound_conn.go
similarity index 58%
rename from inbound/naive.go
rename to protocol/naive/inbound_conn.go
index 498e823c..16944cba 100644
--- a/inbound/naive.go
+++ b/protocol/naive/inbound_conn.go
@@ -1,7 +1,6 @@
-package inbound
+package naive
 
 import (
-	"context"
 	"encoding/binary"
 	"io"
 	"math/rand"
@@ -11,228 +10,12 @@ import (
 	"strings"
 	"time"
 
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/tls"
-	"github.com/sagernet/sing-box/common/uot"
-	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/v2rayhttp"
 	"github.com/sagernet/sing/common"
-	"github.com/sagernet/sing/common/auth"
 	"github.com/sagernet/sing/common/buf"
-	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/rw"
-	sHttp "github.com/sagernet/sing/protocol/http"
 )
 
-var _ adapter.Inbound = (*Naive)(nil)
-
-type Naive struct {
-	myInboundAdapter
-	authenticator *auth.Authenticator
-	tlsConfig     tls.ServerConfig
-	httpServer    *http.Server
-	h3Server      any
-}
-
-func NewNaive(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveInboundOptions) (*Naive, error) {
-	inbound := &Naive{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeNaive,
-			network:       options.Network.Build(),
-			ctx:           ctx,
-			router:        uot.NewRouter(router, logger),
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
-		authenticator: auth.NewAuthenticator(options.Users),
-	}
-	if common.Contains(inbound.network, N.NetworkUDP) {
-		if options.TLS == nil || !options.TLS.Enabled {
-			return nil, E.New("TLS is required for QUIC server")
-		}
-	}
-	if len(options.Users) == 0 {
-		return nil, E.New("missing users")
-	}
-	if options.TLS != nil {
-		tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
-		if err != nil {
-			return nil, err
-		}
-		inbound.tlsConfig = tlsConfig
-	}
-	return inbound, nil
-}
-
-func (n *Naive) Start() error {
-	var tlsConfig *tls.STDConfig
-	if n.tlsConfig != nil {
-		err := n.tlsConfig.Start()
-		if err != nil {
-			return E.Cause(err, "create TLS config")
-		}
-		tlsConfig, err = n.tlsConfig.Config()
-		if err != nil {
-			return err
-		}
-	}
-
-	if common.Contains(n.network, N.NetworkTCP) {
-		tcpListener, err := n.ListenTCP()
-		if err != nil {
-			return err
-		}
-		n.httpServer = &http.Server{
-			Handler:   n,
-			TLSConfig: tlsConfig,
-			BaseContext: func(listener net.Listener) context.Context {
-				return n.ctx
-			},
-		}
-		go func() {
-			var sErr error
-			if tlsConfig != nil {
-				sErr = n.httpServer.ServeTLS(tcpListener, "", "")
-			} else {
-				sErr = n.httpServer.Serve(tcpListener)
-			}
-			if sErr != nil && !E.IsClosedOrCanceled(sErr) {
-				n.logger.Error("http server serve error: ", sErr)
-			}
-		}()
-	}
-
-	if common.Contains(n.network, N.NetworkUDP) {
-		err := n.configureHTTP3Listener()
-		if !C.WithQUIC && len(n.network) > 1 {
-			n.logger.Warn(E.Cause(err, "naive http3 disabled"))
-		} else if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func (n *Naive) Close() error {
-	return common.Close(
-		&n.myInboundAdapter,
-		common.PtrOrNil(n.httpServer),
-		n.h3Server,
-		n.tlsConfig,
-	)
-}
-
-func (n *Naive) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
-	ctx := log.ContextWithNewID(request.Context())
-	if request.Method != "CONNECT" {
-		rejectHTTP(writer, http.StatusBadRequest)
-		n.badRequest(ctx, request, E.New("not CONNECT request"))
-		return
-	} else if request.Header.Get("Padding") == "" {
-		rejectHTTP(writer, http.StatusBadRequest)
-		n.badRequest(ctx, request, E.New("missing naive padding"))
-		return
-	}
-	userName, password, authOk := sHttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization"))
-	if authOk {
-		authOk = n.authenticator.Verify(userName, password)
-	}
-	if !authOk {
-		rejectHTTP(writer, http.StatusProxyAuthRequired)
-		n.badRequest(ctx, request, E.New("authorization failed"))
-		return
-	}
-	writer.Header().Set("Padding", generateNaivePaddingHeader())
-	writer.WriteHeader(http.StatusOK)
-	writer.(http.Flusher).Flush()
-
-	hostPort := request.URL.Host
-	if hostPort == "" {
-		hostPort = request.Host
-	}
-	source := sHttp.SourceAddress(request)
-	destination := M.ParseSocksaddr(hostPort)
-
-	if hijacker, isHijacker := writer.(http.Hijacker); isHijacker {
-		conn, _, err := hijacker.Hijack()
-		if err != nil {
-			n.badRequest(ctx, request, E.New("hijack failed"))
-			return
-		}
-		n.newConnection(ctx, false, &naiveH1Conn{Conn: conn}, userName, source, destination)
-	} else {
-		n.newConnection(ctx, true, &naiveH2Conn{reader: request.Body, writer: writer, flusher: writer.(http.Flusher)}, userName, source, destination)
-	}
-}
-
-func (n *Naive) newConnection(ctx context.Context, waitForClose bool, conn net.Conn, userName string, source M.Socksaddr, destination M.Socksaddr) {
-	if userName != "" {
-		n.logger.InfoContext(ctx, "[", userName, "] inbound connection from ", source)
-		n.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", destination)
-	} else {
-		n.logger.InfoContext(ctx, "inbound connection from ", source)
-		n.logger.InfoContext(ctx, "inbound connection to ", destination)
-	}
-	metadata := n.createMetadata(conn, adapter.InboundContext{
-		Source:      source,
-		Destination: destination,
-		User:        userName,
-	})
-	if !waitForClose {
-		n.router.RouteConnectionEx(ctx, conn, metadata, nil)
-	} else {
-		done := make(chan struct{})
-		wrapper := v2rayhttp.NewHTTP2Wrapper(conn)
-		n.router.RouteConnectionEx(ctx, conn, metadata, N.OnceClose(func(it error) {
-			close(done)
-		}))
-		<-done
-		wrapper.CloseWrapper()
-	}
-}
-
-func (n *Naive) badRequest(ctx context.Context, request *http.Request, err error) {
-	n.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr))
-}
-
-func rejectHTTP(writer http.ResponseWriter, statusCode int) {
-	hijacker, ok := writer.(http.Hijacker)
-	if !ok {
-		writer.WriteHeader(statusCode)
-		return
-	}
-	conn, _, err := hijacker.Hijack()
-	if err != nil {
-		writer.WriteHeader(statusCode)
-		return
-	}
-	if tcpConn, isTCP := common.Cast[*net.TCPConn](conn); isTCP {
-		tcpConn.SetLinger(0)
-	}
-	conn.Close()
-}
-
-func generateNaivePaddingHeader() string {
-	paddingLen := rand.Intn(32) + 30
-	padding := make([]byte, paddingLen)
-	bits := rand.Uint64()
-	for i := 0; i < 16; i++ {
-		// Codes that won't be Huffman coded.
-		padding[i] = "!#$()+<>?@[]^`{}"[bits&15]
-		bits >>= 4
-	}
-	for i := 16; i < paddingLen; i++ {
-		padding[i] = '~'
-	}
-	return string(padding)
-}
-
 const kFirstPaddings = 8
 
 type naiveH1Conn struct {
diff --git a/protocol/naive/quic/inbound_init.go b/protocol/naive/quic/inbound_init.go
new file mode 100644
index 00000000..f495c860
--- /dev/null
+++ b/protocol/naive/quic/inbound_init.go
@@ -0,0 +1,52 @@
+package quic
+
+import (
+	"io"
+	"net/http"
+
+	"github.com/sagernet/quic-go"
+	"github.com/sagernet/quic-go/http3"
+	"github.com/sagernet/sing-box/common/listener"
+	"github.com/sagernet/sing-box/common/tls"
+	"github.com/sagernet/sing-box/protocol/naive"
+	"github.com/sagernet/sing-quic"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/logger"
+)
+
+func init() {
+	naive.ConfigureHTTP3ListenerFunc = func(listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, logger logger.Logger) (io.Closer, error) {
+		err := qtls.ConfigureHTTP3(tlsConfig)
+		if err != nil {
+			return nil, err
+		}
+
+		udpConn, err := listener.ListenUDP()
+		if err != nil {
+			return nil, err
+		}
+
+		quicListener, err := qtls.ListenEarly(udpConn, tlsConfig, &quic.Config{
+			MaxIncomingStreams: 1 << 60,
+			Allow0RTT:          true,
+		})
+		if err != nil {
+			udpConn.Close()
+			return nil, err
+		}
+
+		h3Server := &http3.Server{
+			Handler: handler,
+		}
+
+		go func() {
+			sErr := h3Server.ServeListener(quicListener)
+			udpConn.Close()
+			if sErr != nil && !E.IsClosedOrCanceled(sErr) {
+				logger.Error("http3 server closed: ", sErr)
+			}
+		}()
+
+		return quicListener, nil
+	}
+}
diff --git a/protocol/redirect/redirect.go b/protocol/redirect/redirect.go
new file mode 100644
index 00000000..71e1fced
--- /dev/null
+++ b/protocol/redirect/redirect.go
@@ -0,0 +1,65 @@
+package redirect
+
+import (
+	"context"
+	"net"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
+	"github.com/sagernet/sing-box/common/redir"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+func RegisterRedirect(registry *inbound.Registry) {
+	inbound.Register[option.RedirectInboundOptions](registry, C.TypeRedirect, NewRedirect)
+}
+
+type Redirect struct {
+	inbound.Adapter
+	router   adapter.Router
+	logger   log.ContextLogger
+	listener *listener.Listener
+}
+
+func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions) (adapter.Inbound, error) {
+	redirect := &Redirect{
+		Adapter: inbound.NewAdapter(C.TypeRedirect, tag),
+		router:  router,
+		logger:  logger,
+	}
+	redirect.listener = listener.New(listener.Options{
+		Context:           ctx,
+		Logger:            logger,
+		Network:           []string{N.NetworkTCP},
+		Listen:            options.ListenOptions,
+		ConnectionHandler: redirect,
+	})
+	return redirect, nil
+}
+
+func (h *Redirect) Start() error {
+	return h.listener.Start()
+}
+
+func (h *Redirect) Close() error {
+	return h.listener.Close()
+}
+
+func (h *Redirect) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	destination, err := redir.GetOriginalDestination(conn)
+	if err != nil {
+		conn.Close()
+		h.logger.ErrorContext(ctx, "process connection from ", conn.RemoteAddr(), ": get redirect destination: ", err)
+		return
+	}
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.Destination = M.SocksaddrFromNetIP(destination)
+	h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+	h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
+}
diff --git a/inbound/tproxy.go b/protocol/redirect/tproxy.go
similarity index 60%
rename from inbound/tproxy.go
rename to protocol/redirect/tproxy.go
index 40653c79..dee40ec5 100644
--- a/inbound/tproxy.go
+++ b/protocol/redirect/tproxy.go
@@ -1,4 +1,4 @@
-package inbound
+package redirect
 
 import (
 	"context"
@@ -8,6 +8,8 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
 	"github.com/sagernet/sing-box/common/redir"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
@@ -21,22 +23,25 @@ import (
 	"github.com/sagernet/sing/common/udpnat2"
 )
 
-type TProxy struct {
-	myInboundAdapter
-	udpNat *udpnat.Service
+func RegisterTProxy(registry *inbound.Registry) {
+	inbound.Register[option.TProxyInboundOptions](registry, C.TypeTProxy, NewTProxy)
 }
 
-func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TProxyInboundOptions) *TProxy {
+type TProxy struct {
+	inbound.Adapter
+	ctx      context.Context
+	router   adapter.Router
+	logger   log.ContextLogger
+	listener *listener.Listener
+	udpNat   *udpnat.Service
+}
+
+func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TProxyInboundOptions) (adapter.Inbound, error) {
 	tproxy := &TProxy{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeTProxy,
-			network:       options.Network.Build(),
-			ctx:           ctx,
-			router:        router,
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
+		Adapter: inbound.NewAdapter(C.TypeTProxy, tag),
+		ctx:     ctx,
+		router:  router,
+		logger:  logger,
 	}
 	var udpTimeout time.Duration
 	if options.UDPTimeout != 0 {
@@ -44,28 +49,34 @@ func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLog
 	} else {
 		udpTimeout = C.UDPTimeout
 	}
-	tproxy.connHandler = tproxy
-	tproxy.oobPacketHandler = tproxy
 	tproxy.udpNat = udpnat.New(tproxy, tproxy.preparePacketConnection, udpTimeout, false)
-	return tproxy
+	tproxy.listener = listener.New(listener.Options{
+		Context:           ctx,
+		Logger:            logger,
+		Network:           options.Network.Build(),
+		Listen:            options.ListenOptions,
+		ConnectionHandler: tproxy,
+		OOBPacketHandler:  tproxy,
+	})
+	return tproxy, nil
 }
 
 func (t *TProxy) Start() error {
-	err := t.myInboundAdapter.Start()
+	err := t.listener.Start()
 	if err != nil {
 		return err
 	}
-	if t.tcpListener != nil {
-		err = control.Conn(common.MustCast[syscall.Conn](t.tcpListener), func(fd uintptr) error {
-			return redir.TProxy(fd, M.SocksaddrFromNet(t.tcpListener.Addr()).Addr.Is6())
+	if listener := t.listener.TCPListener(); listener != nil {
+		err = control.Conn(common.MustCast[syscall.Conn](listener), func(fd uintptr) error {
+			return redir.TProxy(fd, M.SocksaddrFromNet(listener.Addr()).Addr.Is6())
 		})
 		if err != nil {
 			return E.Cause(err, "configure tproxy TCP listener")
 		}
 	}
-	if t.udpConn != nil {
-		err = control.Conn(t.udpConn, func(fd uintptr) error {
-			return redir.TProxy(fd, M.SocksaddrFromNet(t.udpConn.LocalAddr()).Addr.Is6())
+	if conn := t.listener.UDPConn(); conn != nil {
+		err = control.Conn(conn, func(fd uintptr) error {
+			return redir.TProxy(fd, M.SocksaddrFromNet(conn.LocalAddr()).Addr.Is6())
 		})
 		if err != nil {
 			return E.Cause(err, "configure tproxy UDP listener")
@@ -74,13 +85,26 @@ func (t *TProxy) Start() error {
 	return nil
 }
 
+func (t *TProxy) Close() error {
+	return t.listener.Close()
+}
+
 func (t *TProxy) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
 	metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
-	t.newConnectionEx(ctx, conn, metadata, onClose)
+	t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+	t.router.RouteConnectionEx(ctx, conn, metadata, onClose)
 }
 
 func (t *TProxy) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
-	t.newPacketConnectionEx(ctx, conn, t.createPacketMetadataEx(source, destination), onClose)
+	t.logger.InfoContext(ctx, "inbound packet connection from ", source)
+	t.logger.InfoContext(ctx, "inbound packet connection to ", destination)
+	var metadata adapter.InboundContext
+	metadata.Inbound = t.Tag()
+	metadata.InboundType = t.Type()
+	metadata.Source = source
+	metadata.Destination = destination
+	metadata.OriginDestination = t.listener.UDPAddr()
+	t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
 }
 
 func (t *TProxy) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) {
@@ -100,8 +124,9 @@ type tproxyPacketWriter struct {
 }
 
 func (t *TProxy) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) {
-	writer := &tproxyPacketWriter{ctx: t.ctx, source: source.AddrPort(), destination: destination}
-	return true, t.ctx, writer, func(it error) {
+	ctx := log.ContextWithNewID(t.ctx)
+	writer := &tproxyPacketWriter{ctx: ctx, source: source.AddrPort(), destination: destination}
+	return true, ctx, writer, func(it error) {
 		common.Close(common.PtrOrNil(writer.conn))
 	}
 }
diff --git a/protocol/shadowsocks/inbound.go b/protocol/shadowsocks/inbound.go
new file mode 100644
index 00000000..b23516d9
--- /dev/null
+++ b/protocol/shadowsocks/inbound.go
@@ -0,0 +1,179 @@
+package shadowsocks
+
+import (
+	"context"
+	"net"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
+	"github.com/sagernet/sing-box/common/mux"
+	"github.com/sagernet/sing-box/common/uot"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-shadowsocks"
+	"github.com/sagernet/sing-shadowsocks/shadowaead"
+	"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	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"
+)
+
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocks, NewInbound)
+}
+
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
+	if len(options.Users) > 0 && len(options.Destinations) > 0 {
+		return nil, E.New("users and destinations options must not be combined")
+	}
+	if len(options.Users) > 0 {
+		return newMultiInbound(ctx, router, logger, tag, options)
+	} else if len(options.Destinations) > 0 {
+		return newRelayInbound(ctx, router, logger, tag, options)
+	} else {
+		return newInbound(ctx, router, logger, tag, options)
+	}
+}
+
+var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
+
+type Inbound struct {
+	inbound.Adapter
+	ctx      context.Context
+	router   adapter.ConnectionRouterEx
+	logger   logger.ContextLogger
+	listener *listener.Listener
+	service  shadowsocks.Service
+}
+
+func newInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*Inbound, error) {
+	inbound := &Inbound{
+		Adapter: inbound.NewAdapter(C.TypeShadowsocks, tag),
+		ctx:     ctx,
+		router:  uot.NewRouter(router, logger),
+		logger:  logger,
+	}
+	var err error
+	inbound.router, err = mux.NewRouterWithOptions(router, logger, common.PtrValueOrDefault(options.Multiplex))
+	if err != nil {
+		return nil, err
+	}
+	var udpTimeout time.Duration
+	if options.UDPTimeout != 0 {
+		udpTimeout = time.Duration(options.UDPTimeout)
+	} else {
+		udpTimeout = C.UDPTimeout
+	}
+	switch {
+	case options.Method == shadowsocks.MethodNone:
+		inbound.service = shadowsocks.NewNoneService(int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound))
+	case common.Contains(shadowaead.List, options.Method):
+		inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound))
+	case common.Contains(shadowaead_2022.List, options.Method):
+		inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx))
+	default:
+		err = E.New("unsupported method: ", options.Method)
+	}
+	inbound.listener = listener.New(listener.Options{
+		Context:                  ctx,
+		Logger:                   logger,
+		Network:                  options.Network.Build(),
+		Listen:                   options.ListenOptions,
+		ConnectionHandler:        inbound,
+		PacketHandler:            inbound,
+		ThreadUnsafePacketWriter: true,
+	})
+	return inbound, err
+}
+
+func (h *Inbound) Start() error {
+	return h.listener.Start()
+}
+
+func (h *Inbound) Close() error {
+	return h.listener.Close()
+}
+
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata))
+	N.CloseOnHandshakeFailure(conn, onClose, err)
+	if err != nil {
+		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+	}
+}
+
+func (h *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) {
+	err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source})
+	if err != nil {
+		h.logger.Error(E.Cause(err, "process packet from ", source))
+	}
+}
+
+func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	return h.router.RouteConnection(ctx, conn, metadata)
+}
+
+func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	ctx = log.ContextWithNewID(ctx)
+	h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
+	h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	return h.router.RoutePacketConnection(ctx, conn, metadata)
+}
+
+var _ N.PacketConn = (*stubPacketConn)(nil)
+
+type stubPacketConn struct {
+	N.PacketWriter
+}
+
+func (c *stubPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
+	panic("stub!")
+}
+
+func (c *stubPacketConn) Close() error {
+	return nil
+}
+
+func (c *stubPacketConn) LocalAddr() net.Addr {
+	panic("stub!")
+}
+
+func (c *stubPacketConn) SetDeadline(t time.Time) error {
+	panic("stub!")
+}
+
+func (c *stubPacketConn) SetReadDeadline(t time.Time) error {
+	panic("stub!")
+}
+
+func (c *stubPacketConn) SetWriteDeadline(t time.Time) error {
+	panic("stub!")
+}
+
+func (h *Inbound) NewError(ctx context.Context, err error) {
+	NewError(h.logger, ctx, err)
+}
+
+// Deprecated: remove
+func NewError(logger logger.ContextLogger, ctx context.Context, err error) {
+	common.Close(err)
+	if E.IsClosedOrCanceled(err) {
+		logger.DebugContext(ctx, "connection closed: ", err)
+		return
+	}
+	logger.ErrorContext(ctx, err)
+}
diff --git a/inbound/shadowsocks_multi.go b/protocol/shadowsocks/inbound_multi.go
similarity index 59%
rename from inbound/shadowsocks_multi.go
rename to protocol/shadowsocks/inbound_multi.go
index 29534194..0e1efedf 100644
--- a/inbound/shadowsocks_multi.go
+++ b/protocol/shadowsocks/inbound_multi.go
@@ -1,4 +1,4 @@
-package inbound
+package shadowsocks
 
 import (
 	"context"
@@ -7,6 +7,8 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
 	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
@@ -20,36 +22,31 @@ import (
 	"github.com/sagernet/sing/common/buf"
 	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/ntp"
 )
 
-var (
-	_ adapter.Inbound              = (*ShadowsocksMulti)(nil)
-	_ adapter.TCPInjectableInbound = (*ShadowsocksMulti)(nil)
-)
+var _ adapter.TCPInjectableInbound = (*MultiInbound)(nil)
 
-type ShadowsocksMulti struct {
-	myInboundAdapter
-	service shadowsocks.MultiService[int]
-	users   []option.ShadowsocksUser
+type MultiInbound struct {
+	inbound.Adapter
+	ctx      context.Context
+	router   adapter.ConnectionRouterEx
+	logger   logger.ContextLogger
+	listener *listener.Listener
+	service  shadowsocks.MultiService[int]
+	users    []option.ShadowsocksUser
 }
 
-func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*ShadowsocksMulti, error) {
-	inbound := &ShadowsocksMulti{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeShadowsocks,
-			network:       options.Network.Build(),
-			ctx:           ctx,
-			router:        uot.NewRouter(router, logger),
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
+func newMultiInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*MultiInbound, error) {
+	inbound := &MultiInbound{
+		Adapter: inbound.NewAdapter(C.TypeShadowsocks, tag),
+		ctx:     ctx,
+		router:  uot.NewRouter(router, logger),
+		logger:  logger,
 	}
-	inbound.connHandler = inbound
-	inbound.packetHandler = inbound
 	var err error
 	inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex))
 	if err != nil {
@@ -91,12 +88,28 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.
 		return nil, err
 	}
 	inbound.service = service
-	inbound.packetUpstream = service
 	inbound.users = options.Users
+	inbound.listener = listener.New(listener.Options{
+		Context:                  ctx,
+		Logger:                   logger,
+		Network:                  options.Network.Build(),
+		Listen:                   options.ListenOptions,
+		ConnectionHandler:        inbound,
+		PacketHandler:            inbound,
+		ThreadUnsafePacketWriter: true,
+	})
 	return inbound, err
 }
 
-func (h *ShadowsocksMulti) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+func (h *MultiInbound) Start() error {
+	return h.listener.Start()
+}
+
+func (h *MultiInbound) Close() error {
+	return h.listener.Close()
+}
+
+func (h *MultiInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
 	err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata))
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
@@ -104,14 +117,14 @@ func (h *ShadowsocksMulti) NewConnectionEx(ctx context.Context, conn net.Conn, m
 	}
 }
 
-func (h *ShadowsocksMulti) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) {
-	err := h.service.NewPacket(h.ctx, h.packetConn(), buffer, M.Metadata{Source: source})
+func (h *MultiInbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) {
+	err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source})
 	if err != nil {
 		h.logger.Error(E.Cause(err, "process packet from ", source))
 	}
 }
 
-func (h *ShadowsocksMulti) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -123,10 +136,12 @@ func (h *ShadowsocksMulti) newConnection(ctx context.Context, conn net.Conn, met
 		metadata.User = user
 	}
 	h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
-	return h.router.RouteConnection(ctx, conn, h.createMetadata(conn, metadata))
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
-func (h *ShadowsocksMulti) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *MultiInbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -140,5 +155,13 @@ func (h *ShadowsocksMulti) newPacketConnection(ctx context.Context, conn N.Packe
 	ctx = log.ContextWithNewID(ctx)
 	h.logger.InfoContext(ctx, "[", user, "] inbound packet connection from ", metadata.Source)
 	h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
-	return h.router.RoutePacketConnection(ctx, conn, h.createPacketMetadata(conn, metadata))
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	return h.router.RoutePacketConnection(ctx, conn, metadata)
+}
+
+func (h *MultiInbound) NewError(ctx context.Context, err error) {
+	NewError(h.logger, ctx, err)
 }
diff --git a/inbound/shadowsocks_relay.go b/protocol/shadowsocks/inbound_relay.go
similarity index 57%
rename from inbound/shadowsocks_relay.go
rename to protocol/shadowsocks/inbound_relay.go
index 02246a3f..5818ca29 100644
--- a/inbound/shadowsocks_relay.go
+++ b/protocol/shadowsocks/inbound_relay.go
@@ -1,4 +1,4 @@
-package inbound
+package shadowsocks
 
 import (
 	"context"
@@ -7,6 +7,8 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
 	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
@@ -18,36 +20,31 @@ import (
 	"github.com/sagernet/sing/common/buf"
 	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"
 )
 
-var (
-	_ adapter.Inbound              = (*ShadowsocksRelay)(nil)
-	_ adapter.TCPInjectableInbound = (*ShadowsocksRelay)(nil)
-)
+var _ adapter.TCPInjectableInbound = (*RelayInbound)(nil)
 
-type ShadowsocksRelay struct {
-	myInboundAdapter
+type RelayInbound struct {
+	inbound.Adapter
+	ctx          context.Context
+	router       adapter.ConnectionRouterEx
+	logger       logger.ContextLogger
+	listener     *listener.Listener
 	service      *shadowaead_2022.RelayService[int]
 	destinations []option.ShadowsocksDestination
 }
 
-func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*ShadowsocksRelay, error) {
-	inbound := &ShadowsocksRelay{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeShadowsocks,
-			network:       options.Network.Build(),
-			ctx:           ctx,
-			router:        uot.NewRouter(router, logger),
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
+func newRelayInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*RelayInbound, error) {
+	inbound := &RelayInbound{
+		Adapter:      inbound.NewAdapter(C.TypeShadowsocks, tag),
+		ctx:          ctx,
+		router:       uot.NewRouter(router, logger),
+		logger:       logger,
 		destinations: options.Destinations,
 	}
-	inbound.connHandler = inbound
-	inbound.packetHandler = inbound
 	var err error
 	inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex))
 	if err != nil {
@@ -77,11 +74,27 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log.
 		return nil, err
 	}
 	inbound.service = service
-	inbound.packetUpstream = service
+	inbound.listener = listener.New(listener.Options{
+		Context:                  ctx,
+		Logger:                   logger,
+		Network:                  options.Network.Build(),
+		Listen:                   options.ListenOptions,
+		ConnectionHandler:        inbound,
+		PacketHandler:            inbound,
+		ThreadUnsafePacketWriter: true,
+	})
 	return inbound, err
 }
 
-func (h *ShadowsocksRelay) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+func (h *RelayInbound) Start() error {
+	return h.listener.Start()
+}
+
+func (h *RelayInbound) Close() error {
+	return h.listener.Close()
+}
+
+func (h *RelayInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
 	err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata))
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
@@ -89,14 +102,14 @@ func (h *ShadowsocksRelay) NewConnectionEx(ctx context.Context, conn net.Conn, m
 	}
 }
 
-func (h *ShadowsocksRelay) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) {
-	err := h.service.NewPacket(h.ctx, h.packetConn(), buffer, M.Metadata{Source: source})
+func (h *RelayInbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) {
+	err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source})
 	if err != nil {
 		h.logger.Error(E.Cause(err, "process packet from ", source))
 	}
 }
 
-func (h *ShadowsocksRelay) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *RelayInbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	destinationIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -108,10 +121,12 @@ func (h *ShadowsocksRelay) newConnection(ctx context.Context, conn net.Conn, met
 		metadata.User = destination
 	}
 	h.logger.InfoContext(ctx, "[", destination, "] inbound connection to ", metadata.Destination)
-	return h.router.RouteConnection(ctx, conn, h.createMetadata(conn, metadata))
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
-func (h *ShadowsocksRelay) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *RelayInbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	destinationIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -125,5 +140,13 @@ func (h *ShadowsocksRelay) newPacketConnection(ctx context.Context, conn N.Packe
 	ctx = log.ContextWithNewID(ctx)
 	h.logger.InfoContext(ctx, "[", destination, "] inbound packet connection from ", metadata.Source)
 	h.logger.InfoContext(ctx, "[", destination, "] inbound packet connection to ", metadata.Destination)
-	return h.router.RoutePacketConnection(ctx, conn, h.createPacketMetadata(conn, metadata))
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	return h.router.RoutePacketConnection(ctx, conn, metadata)
+}
+
+func (h *RelayInbound) NewError(ctx context.Context, err error) {
+	NewError(h.logger, ctx, err)
 }
diff --git a/outbound/shadowsocks.go b/protocol/shadowsocks/outbound.go
similarity index 80%
rename from outbound/shadowsocks.go
rename to protocol/shadowsocks/outbound.go
index 15354274..73b38385 100644
--- a/outbound/shadowsocks.go
+++ b/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 {
diff --git a/inbound/shadowtls.go b/protocol/shadowtls/inbound.go
similarity index 62%
rename from inbound/shadowtls.go
rename to protocol/shadowtls/inbound.go
index ca142286..6887e838 100644
--- a/inbound/shadowtls.go
+++ b/protocol/shadowtls/inbound.go
@@ -1,11 +1,13 @@
-package inbound
+package shadowtls
 
 import (
 	"context"
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
 	"github.com/sagernet/sing-box/common/dialer"
+	"github.com/sagernet/sing-box/common/listener"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -13,25 +15,27 @@ import (
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/auth"
 	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/logger"
 	N "github.com/sagernet/sing/common/network"
 )
 
-type ShadowTLS struct {
-	myInboundAdapter
-	service *shadowtls.Service
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.ShadowTLSInboundOptions](registry, C.TypeShadowTLS, NewInbound)
 }
 
-func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSInboundOptions) (*ShadowTLS, error) {
-	inbound := &ShadowTLS{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeShadowTLS,
-			network:       []string{N.NetworkTCP},
-			ctx:           ctx,
-			router:        router,
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
+type Inbound struct {
+	inbound.Adapter
+	router   adapter.Router
+	logger   logger.ContextLogger
+	listener *listener.Listener
+	service  *shadowtls.Service
+}
+
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSInboundOptions) (adapter.Inbound, error) {
+	inbound := &Inbound{
+		Adapter: inbound.NewAdapter(C.TypeShadowTLS, tag),
+		router:  router,
+		logger:  logger,
 	}
 
 	if options.Version == 0 {
@@ -68,22 +72,36 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context
 		},
 		HandshakeForServerName: handshakeForServerName,
 		StrictMode:             options.StrictMode,
-		Handler:                adapter.NewUpstreamContextHandler(inbound.newConnection, nil, inbound),
+		Handler:                adapter.NewUpstreamContextHandler(inbound.newConnection, nil, nil),
 		Logger:                 logger,
 	})
 	if err != nil {
 		return nil, err
 	}
 	inbound.service = service
-	inbound.connHandler = inbound
+	inbound.listener = listener.New(listener.Options{
+		Context:           ctx,
+		Logger:            logger,
+		Network:           []string{N.NetworkTCP},
+		Listen:            options.ListenOptions,
+		ConnectionHandler: inbound,
+	})
 	return inbound, nil
 }
 
-func (h *ShadowTLS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) Start() error {
+	return h.listener.Start()
+}
+
+func (h *Inbound) Close() error {
+	return h.listener.Close()
+}
+
+func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
 }
 
-func (h *ShadowTLS) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	if userName, _ := auth.UserFromContext[string](ctx); userName != "" {
 		metadata.User = userName
 		h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination)
@@ -93,7 +111,7 @@ func (h *ShadowTLS) newConnection(ctx context.Context, conn net.Conn, metadata a
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
-func (h *ShadowTLS) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
 	err := h.NewConnection(ctx, conn, metadata)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
diff --git a/outbound/shadowtls.go b/protocol/shadowtls/outbound.go
similarity index 74%
rename from outbound/shadowtls.go
rename to protocol/shadowtls/outbound.go
index ff1b9d6c..7d46a8f6 100644
--- a/outbound/shadowtls.go
+++ b/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
 }
diff --git a/protocol/socks/inbound.go b/protocol/socks/inbound.go
new file mode 100644
index 00000000..29649a88
--- /dev/null
+++ b/protocol/socks/inbound.go
@@ -0,0 +1,91 @@
+package socks
+
+import (
+	std_bufio "bufio"
+	"context"
+	"net"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
+	"github.com/sagernet/sing-box/common/uot"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common/auth"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/logger"
+	N "github.com/sagernet/sing/common/network"
+	"github.com/sagernet/sing/protocol/socks"
+)
+
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.SocksInboundOptions](registry, C.TypeSOCKS, NewInbound)
+}
+
+var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
+
+type Inbound struct {
+	inbound.Adapter
+	router        adapter.ConnectionRouterEx
+	logger        logger.ContextLogger
+	listener      *listener.Listener
+	authenticator *auth.Authenticator
+}
+
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SocksInboundOptions) (adapter.Inbound, error) {
+	inbound := &Inbound{
+		Adapter:       inbound.NewAdapter(C.TypeSOCKS, tag),
+		router:        uot.NewRouter(router, logger),
+		logger:        logger,
+		authenticator: auth.NewAuthenticator(options.Users),
+	}
+	inbound.listener = listener.New(listener.Options{
+		Context:           ctx,
+		Logger:            logger,
+		Network:           []string{N.NetworkTCP},
+		Listen:            options.ListenOptions,
+		ConnectionHandler: inbound,
+	})
+	return inbound, nil
+}
+
+func (h *Inbound) Start() error {
+	return h.listener.Start()
+}
+
+func (h *Inbound) Close() error {
+	return h.listener.Close()
+}
+
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, metadata.Destination, onClose)
+	N.CloseOnHandshakeFailure(conn, onClose, err)
+	if err != nil {
+		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+	}
+}
+
+func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	user, loaded := auth.UserFromContext[string](ctx)
+	if !loaded {
+		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+		h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
+		return
+	}
+	metadata.User = user
+	h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
+	h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
+}
+
+func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	user, loaded := auth.UserFromContext[string](ctx)
+	if !loaded {
+		h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
+		h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
+		return
+	}
+	metadata.User = user
+	h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
+	h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
+}
diff --git a/outbound/socks.go b/protocol/socks/outbound.go
similarity index 65%
rename from outbound/socks.go
rename to protocol/socks/outbound.go
index 575d6eb3..0194800a 100644
--- a/outbound/socks.go
+++ b/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)
+}
 
-type Socks struct {
-	myOutboundAdapter
+var _ adapter.Outbound = (*Outbound)(nil)
+
+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)
 	}
 }
diff --git a/outbound/ssh.go b/protocol/ssh/outbound.go
similarity index 80%
rename from outbound/ssh.go
rename to protocol/ssh/outbound.go
index 28abe9a5..62a2a8d9 100644
--- a/outbound/ssh.go
+++ b/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
 }
diff --git a/outbound/tor.go b/protocol/tor/outbound.go
similarity index 83%
rename from outbound/tor.go
rename to protocol/tor/outbound.go
index ccc0c0cf..89a295b8 100644
--- a/outbound/tor.go
+++ b/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
 }
diff --git a/outbound/proxy.go b/protocol/tor/proxy.go
similarity index 94%
rename from outbound/proxy.go
rename to protocol/tor/proxy.go
index 38c18453..ef60bd1f 100644
--- a/outbound/proxy.go
+++ b/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)
 }
diff --git a/inbound/trojan.go b/protocol/trojan/inbound.go
similarity index 68%
rename from inbound/trojan.go
rename to protocol/trojan/inbound.go
index ce003dda..010ae8ba 100644
--- a/inbound/trojan.go
+++ b/protocol/trojan/inbound.go
@@ -1,4 +1,4 @@
-package inbound
+package trojan
 
 import (
 	"context"
@@ -6,6 +6,8 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
 	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
@@ -21,13 +23,17 @@ import (
 	N "github.com/sagernet/sing/common/network"
 )
 
-var (
-	_ adapter.Inbound              = (*Trojan)(nil)
-	_ adapter.TCPInjectableInbound = (*Trojan)(nil)
-)
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.TrojanInboundOptions](registry, C.TypeTrojan, NewInbound)
+}
 
-type Trojan struct {
-	myInboundAdapter
+var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
+
+type Inbound struct {
+	inbound.Adapter
+	router                   adapter.ConnectionRouterEx
+	logger                   log.ContextLogger
+	listener                 *listener.Listener
 	service                  *trojan.Service[int]
 	users                    []option.TrojanUser
 	tlsConfig                tls.ServerConfig
@@ -36,18 +42,12 @@ type Trojan struct {
 	transport                adapter.V2RayServerTransport
 }
 
-func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanInboundOptions) (*Trojan, error) {
-	inbound := &Trojan{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeTrojan,
-			network:       []string{N.NetworkTCP},
-			ctx:           ctx,
-			router:        router,
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
-		users: options.Users,
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanInboundOptions) (adapter.Inbound, error) {
+	inbound := &Inbound{
+		Adapter: inbound.NewAdapter(C.TypeTrojan, tag),
+		router:  router,
+		logger:  logger,
+		users:   options.Users,
 	}
 	if options.TLS != nil {
 		tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
@@ -80,7 +80,7 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog
 		}
 		fallbackHandler = adapter.NewUpstreamContextHandler(inbound.fallbackConnection, nil, nil)
 	}
-	service := trojan.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound), fallbackHandler)
+	service := trojan.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, nil), fallbackHandler, logger)
 	err := service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.TrojanUser) int {
 		return index
 	}), common.Map(options.Users, func(it option.TrojanUser) string {
@@ -90,7 +90,7 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog
 		return nil, err
 	}
 	if options.Transport != nil {
-		inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*trojanTransportHandler)(inbound))
+		inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound))
 		if err != nil {
 			return nil, E.Cause(err, "create server transport: ", options.Transport.Type)
 		}
@@ -100,11 +100,17 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog
 		return nil, err
 	}
 	inbound.service = service
-	inbound.connHandler = inbound
+	inbound.listener = listener.New(listener.Options{
+		Context:           ctx,
+		Logger:            logger,
+		Network:           []string{N.NetworkTCP},
+		Listen:            options.ListenOptions,
+		ConnectionHandler: inbound,
+	})
 	return inbound, nil
 }
 
-func (h *Trojan) Start() error {
+func (h *Inbound) Start() error {
 	if h.tlsConfig != nil {
 		err := h.tlsConfig.Start()
 		if err != nil {
@@ -112,10 +118,10 @@ func (h *Trojan) Start() error {
 		}
 	}
 	if h.transport == nil {
-		return h.myInboundAdapter.Start()
+		return h.listener.Start()
 	}
 	if common.Contains(h.transport.Network(), N.NetworkTCP) {
-		tcpListener, err := h.myInboundAdapter.ListenTCP()
+		tcpListener, err := h.listener.ListenTCP()
 		if err != nil {
 			return err
 		}
@@ -127,7 +133,7 @@ func (h *Trojan) Start() error {
 		}()
 	}
 	if common.Contains(h.transport.Network(), N.NetworkUDP) {
-		udpConn, err := h.myInboundAdapter.ListenUDP()
+		udpConn, err := h.listener.ListenUDP()
 		if err != nil {
 			return err
 		}
@@ -141,15 +147,15 @@ func (h *Trojan) Start() error {
 	return nil
 }
 
-func (h *Trojan) Close() error {
+func (h *Inbound) Close() error {
 	return common.Close(
-		&h.myInboundAdapter,
+		&h.listener,
 		h.tlsConfig,
 		h.transport,
 	)
 }
 
-func (h *Trojan) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	var err error
 	if h.tlsConfig != nil && h.transport == nil {
 		conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig)
@@ -160,7 +166,7 @@ func (h *Trojan) NewConnection(ctx context.Context, conn net.Conn, metadata adap
 	return h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata))
 }
 
-func (h *Trojan) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
 	err := h.NewConnection(ctx, conn, metadata)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
@@ -168,7 +174,7 @@ func (h *Trojan) NewConnectionEx(ctx context.Context, conn net.Conn, metadata ad
 	}
 }
 
-func (h *Trojan) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -183,7 +189,7 @@ func (h *Trojan) newConnection(ctx context.Context, conn net.Conn, metadata adap
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
-func (h *Trojan) fallbackConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) fallbackConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	var fallbackAddr M.Socksaddr
 	if len(h.fallbackAddrTLSNextProto) > 0 {
 		if tlsConn, loaded := common.Cast[tls.Conn](conn); loaded {
@@ -206,7 +212,7 @@ func (h *Trojan) fallbackConnection(ctx context.Context, conn net.Conn, metadata
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
-func (h *Trojan) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -221,10 +227,18 @@ func (h *Trojan) newPacketConnection(ctx context.Context, conn N.PacketConn, met
 	return h.router.RoutePacketConnection(ctx, conn, metadata)
 }
 
-var _ adapter.V2RayServerTransportHandler = (*trojanTransportHandler)(nil)
+var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil)
 
-type trojanTransportHandler Trojan
+type inboundTransportHandler Inbound
 
-func (t *trojanTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
-	(*Trojan)(t).routeTCP(ctx, conn, source, destination, onClose)
+func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+	var metadata adapter.InboundContext
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	metadata.Source = source
+	metadata.Destination = destination
+	h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
+	(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose)
 }
diff --git a/outbound/trojan.go b/protocol/trojan/outbound.go
similarity index 79%
rename from outbound/trojan.go
rename to protocol/trojan/outbound.go
index ee0b2a4b..f64c48c3 100644
--- a/outbound/trojan.go
+++ b/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
diff --git a/inbound/tuic.go b/protocol/tuic/inbound.go
similarity index 68%
rename from inbound/tuic.go
rename to protocol/tuic/inbound.go
index b067c43c..33de10d5 100644
--- a/inbound/tuic.go
+++ b/protocol/tuic/inbound.go
@@ -1,6 +1,4 @@
-//go:build with_quic
-
-package inbound
+package tuic
 
 import (
 	"context"
@@ -8,6 +6,8 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
 	"github.com/sagernet/sing-box/common/tls"
 	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
@@ -22,16 +22,21 @@ import (
 	"github.com/gofrs/uuid/v5"
 )
 
-var _ adapter.Inbound = (*TUIC)(nil)
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.TUICInboundOptions](registry, C.TypeTUIC, NewInbound)
+}
 
-type TUIC struct {
-	myInboundAdapter
+type Inbound struct {
+	inbound.Adapter
+	router       adapter.ConnectionRouterEx
+	logger       log.ContextLogger
+	listener     *listener.Listener
 	tlsConfig    tls.ServerConfig
 	server       *tuic.Service[int]
 	userNameList []string
 }
 
-func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (*TUIC, error) {
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) {
 	options.UDPFragmentDefault = true
 	if options.TLS == nil || !options.TLS.Enabled {
 		return nil, C.ErrTLSRequired
@@ -40,16 +45,15 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
 	if err != nil {
 		return nil, err
 	}
-	inbound := &TUIC{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeTUIC,
-			network:       []string{N.NetworkUDP},
-			ctx:           ctx,
-			router:        uot.NewRouter(router, logger),
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
+	inbound := &Inbound{
+		Adapter: inbound.NewAdapter(C.TypeTUIC, tag),
+		router:  uot.NewRouter(router, logger),
+		logger:  logger,
+		listener: listener.New(listener.Options{
+			Context: ctx,
+			Logger:  logger,
+			Listen:  options.ListenOptions,
+		}),
 		tlsConfig: tlsConfig,
 	}
 	var udpTimeout time.Duration
@@ -95,9 +99,12 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
 	return inbound, nil
 }
 
-func (h *TUIC) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	ctx = log.ContextWithNewID(ctx)
-	metadata = h.createMetadata(conn, metadata)
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
 	h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
 	userID, _ := auth.UserFromContext[int](ctx)
 	if userName := h.userNameList[userID]; userName != "" {
@@ -109,9 +116,13 @@ func (h *TUIC) newConnection(ctx context.Context, conn net.Conn, metadata adapte
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
-func (h *TUIC) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	ctx = log.ContextWithNewID(ctx)
-	metadata = h.createPacketMetadata(conn, metadata)
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	metadata.OriginDestination = h.listener.UDPAddr()
 	h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
 	userID, _ := auth.UserFromContext[int](ctx)
 	if userName := h.userNameList[userID]; userName != "" {
@@ -123,23 +134,23 @@ func (h *TUIC) newPacketConnection(ctx context.Context, conn N.PacketConn, metad
 	return h.router.RoutePacketConnection(ctx, conn, metadata)
 }
 
-func (h *TUIC) Start() error {
+func (h *Inbound) Start() error {
 	if h.tlsConfig != nil {
 		err := h.tlsConfig.Start()
 		if err != nil {
 			return err
 		}
 	}
-	packetConn, err := h.myInboundAdapter.ListenUDP()
+	packetConn, err := h.listener.ListenUDP()
 	if err != nil {
 		return err
 	}
 	return h.server.Start(packetConn)
 }
 
-func (h *TUIC) Close() error {
+func (h *Inbound) Close() error {
 	return common.Close(
-		&h.myInboundAdapter,
+		&h.listener,
 		h.tlsConfig,
 		common.PtrOrNil(h.server),
 	)
diff --git a/outbound/tuic.go b/protocol/tuic/outbound.go
similarity index 76%
rename from outbound/tuic.go
rename to protocol/tuic/outbound.go
index aaf998b1..691d1658 100644
--- a/outbound/tuic.go
+++ b/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)
+}
 
-type TUIC struct {
-	myOutboundAdapter
+var _ adapter.InterfaceUpdateListener = (*Outbound)(nil)
+
+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)
 }
diff --git a/inbound/tun.go b/protocol/tun/inbound.go
similarity index 92%
rename from inbound/tun.go
rename to protocol/tun/inbound.go
index 11b16428..ff679c8e 100644
--- a/inbound/tun.go
+++ b/protocol/tun/inbound.go
@@ -1,4 +1,4 @@
-package inbound
+package tun
 
 import (
 	"context"
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/experimental/deprecated"
@@ -24,13 +25,16 @@ import (
 	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/ranges"
 	"github.com/sagernet/sing/common/x/list"
+	"github.com/sagernet/sing/service"
 
 	"go4.org/netipx"
 )
 
-var _ adapter.Inbound = (*TUN)(nil)
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.TunInboundOptions](registry, C.TypeTun, NewInbound)
+}
 
-type TUN struct {
+type Inbound struct {
 	tag    string
 	ctx    context.Context
 	router adapter.Router
@@ -55,7 +59,7 @@ type TUN struct {
 	routeExcludeAddressSet      []*netipx.IPSet
 }
 
-func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*TUN, error) {
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions) (adapter.Inbound, error) {
 	address := options.Address
 	var deprecatedAddressUsed bool
 	//nolint:staticcheck
@@ -164,7 +168,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
 		outputMark = tun.DefaultAutoRedirectOutputMark
 	}
 
-	inbound := &TUN{
+	inbound := &Inbound{
 		tag:            tag,
 		ctx:            ctx,
 		router:         router,
@@ -198,7 +202,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
 		endpointIndependentNat: options.EndpointIndependentNat,
 		udpTimeout:             udpTimeout,
 		stack:                  options.Stack,
-		platformInterface:      platformInterface,
+		platformInterface:      service.FromContext[platform.Interface](ctx),
 		platformOptions:        common.PtrValueOrDefault(options.Platform),
 	}
 	if options.AutoRedirect {
@@ -285,15 +289,15 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges.
 	return uidRanges, nil
 }
 
-func (t *TUN) Type() string {
+func (t *Inbound) Type() string {
 	return C.TypeTun
 }
 
-func (t *TUN) Tag() string {
+func (t *Inbound) Tag() string {
 	return t.tag
 }
 
-func (t *TUN) Start() error {
+func (t *Inbound) Start() error {
 	if C.IsAndroid && t.platformInterface == nil {
 		t.tunOptions.BuildAndroidRules(t.router.PackageManager())
 	}
@@ -350,7 +354,7 @@ func (t *TUN) Start() error {
 	return nil
 }
 
-func (t *TUN) PostStart() error {
+func (t *Inbound) PostStart() error {
 	monitor := taskmonitor.New(t.logger, C.StartTimeout)
 	if t.autoRedirect != nil {
 		t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet)
@@ -389,7 +393,7 @@ func (t *TUN) PostStart() error {
 	return nil
 }
 
-func (t *TUN) updateRouteAddressSet(it adapter.RuleSet) {
+func (t *Inbound) updateRouteAddressSet(it adapter.RuleSet) {
 	t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet)
 	t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet)
 	t.autoRedirect.UpdateRouteAddressSet()
@@ -397,7 +401,7 @@ func (t *TUN) updateRouteAddressSet(it adapter.RuleSet) {
 	t.routeExcludeAddressSet = nil
 }
 
-func (t *TUN) Close() error {
+func (t *Inbound) Close() error {
 	return common.Close(
 		t.tunStack,
 		t.tunIf,
@@ -405,7 +409,7 @@ func (t *TUN) Close() error {
 	)
 }
 
-func (t *TUN) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error {
+func (t *Inbound) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error {
 	return t.router.PreMatch(adapter.InboundContext{
 		Inbound:        t.tag,
 		InboundType:    C.TypeTun,
@@ -416,7 +420,7 @@ func (t *TUN) PrepareConnection(network string, source M.Socksaddr, destination
 	})
 }
 
-func (t *TUN) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+func (t *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	ctx = log.ContextWithNewID(ctx)
 	var metadata adapter.InboundContext
 	metadata.Inbound = t.tag
@@ -429,7 +433,7 @@ func (t *TUN) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socks
 	t.router.RouteConnectionEx(ctx, conn, metadata, onClose)
 }
 
-func (t *TUN) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+func (t *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	ctx = log.ContextWithNewID(ctx)
 	var metadata adapter.InboundContext
 	metadata.Inbound = t.tag
@@ -442,7 +446,7 @@ func (t *TUN) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, sour
 	t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
 }
 
-type autoRedirectHandler TUN
+type autoRedirectHandler Inbound
 
 func (t *autoRedirectHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	ctx = log.ContextWithNewID(ctx)
diff --git a/inbound/vless.go b/protocol/vless/inbound.go
similarity index 60%
rename from inbound/vless.go
rename to protocol/vless/inbound.go
index ec26bd88..0641549b 100644
--- a/inbound/vless.go
+++ b/protocol/vless/inbound.go
@@ -1,4 +1,4 @@
-package inbound
+package vless
 
 import (
 	"context"
@@ -6,6 +6,8 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
 	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/tls"
 	"github.com/sagernet/sing-box/common/uot"
@@ -20,37 +22,36 @@ import (
 	"github.com/sagernet/sing/common/auth"
 	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"
 )
 
-var (
-	_ adapter.Inbound              = (*VLESS)(nil)
-	_ adapter.TCPInjectableInbound = (*VLESS)(nil)
-)
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.VLESSInboundOptions](registry, C.TypeVLESS, NewInbound)
+}
 
-type VLESS struct {
-	myInboundAdapter
+var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
+
+type Inbound struct {
+	inbound.Adapter
 	ctx       context.Context
+	router    adapter.ConnectionRouterEx
+	logger    logger.ContextLogger
+	listener  *listener.Listener
 	users     []option.VLESSUser
 	service   *vless.Service[int]
 	tlsConfig tls.ServerConfig
 	transport adapter.V2RayServerTransport
 }
 
-func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (*VLESS, error) {
-	inbound := &VLESS{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeVLESS,
-			network:       []string{N.NetworkTCP},
-			ctx:           ctx,
-			router:        uot.NewRouter(router, logger),
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
-		ctx:   ctx,
-		users: options.Users,
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (adapter.Inbound, error) {
+	inbound := &Inbound{
+		Adapter: inbound.NewAdapter(C.TypeVLESS, tag),
+		ctx:     ctx,
+		router:  uot.NewRouter(router, logger),
+		logger:  logger,
+		users:   options.Users,
 	}
 	var err error
 	inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex))
@@ -73,16 +74,22 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
 		}
 	}
 	if options.Transport != nil {
-		inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vlessTransportHandler)(inbound))
+		inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound))
 		if err != nil {
 			return nil, E.Cause(err, "create server transport: ", options.Transport.Type)
 		}
 	}
-	inbound.connHandler = inbound
+	inbound.listener = listener.New(listener.Options{
+		Context:           ctx,
+		Logger:            logger,
+		Network:           []string{N.NetworkTCP},
+		Listen:            options.ListenOptions,
+		ConnectionHandler: inbound,
+	})
 	return inbound, nil
 }
 
-func (h *VLESS) Start() error {
+func (h *Inbound) Start() error {
 	if h.tlsConfig != nil {
 		err := h.tlsConfig.Start()
 		if err != nil {
@@ -90,10 +97,10 @@ func (h *VLESS) Start() error {
 		}
 	}
 	if h.transport == nil {
-		return h.myInboundAdapter.Start()
+		return h.listener.Start()
 	}
 	if common.Contains(h.transport.Network(), N.NetworkTCP) {
-		tcpListener, err := h.myInboundAdapter.ListenTCP()
+		tcpListener, err := h.listener.ListenTCP()
 		if err != nil {
 			return err
 		}
@@ -105,7 +112,7 @@ func (h *VLESS) Start() error {
 		}()
 	}
 	if common.Contains(h.transport.Network(), N.NetworkUDP) {
-		udpConn, err := h.myInboundAdapter.ListenUDP()
+		udpConn, err := h.listener.ListenUDP()
 		if err != nil {
 			return err
 		}
@@ -119,16 +126,16 @@ func (h *VLESS) Start() error {
 	return nil
 }
 
-func (h *VLESS) Close() error {
+func (h *Inbound) Close() error {
 	return common.Close(
 		h.service,
-		&h.myInboundAdapter,
+		&h.listener,
 		h.tlsConfig,
 		h.transport,
 	)
 }
 
-func (h *VLESS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	var err error
 	if h.tlsConfig != nil && h.transport == nil {
 		conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig)
@@ -139,7 +146,7 @@ func (h *VLESS) NewConnection(ctx context.Context, conn net.Conn, metadata adapt
 	return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
 }
 
-func (h *VLESS) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
 	err := h.NewConnection(ctx, conn, metadata)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
@@ -147,7 +154,7 @@ func (h *VLESS) NewConnectionEx(ctx context.Context, conn net.Conn, metadata ada
 	}
 }
 
-func (h *VLESS) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -162,7 +169,7 @@ func (h *VLESS) newConnection(ctx context.Context, conn net.Conn, metadata adapt
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
-func (h *VLESS) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -183,10 +190,32 @@ func (h *VLESS) newPacketConnection(ctx context.Context, conn N.PacketConn, meta
 	return h.router.RoutePacketConnection(ctx, conn, metadata)
 }
 
-var _ adapter.V2RayServerTransportHandler = (*vlessTransportHandler)(nil)
+var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil)
 
-type vlessTransportHandler VLESS
+type inboundTransportHandler Inbound
 
-func (t *vlessTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
-	t.routeTCP(ctx, conn, source, destination, onClose)
+func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+	var metadata adapter.InboundContext
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	metadata.Source = source
+	metadata.Destination = destination
+	h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
+	(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose)
+}
+
+func (h *Inbound) NewError(ctx context.Context, err error) {
+	NewError(h.logger, ctx, err)
+}
+
+// Deprecated: remove
+func NewError(logger logger.ContextLogger, ctx context.Context, err error) {
+	common.Close(err)
+	if E.IsClosedOrCanceled(err) {
+		logger.DebugContext(ctx, "connection closed: ", err)
+		return
+	}
+	logger.ErrorContext(ctx, err)
 }
diff --git a/outbound/vless.go b/protocol/vless/outbound.go
similarity index 84%
rename from outbound/vless.go
rename to protocol/vless/outbound.go
index 536a1e8f..1074549e 100644
--- a/outbound/vless.go
+++ b/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
diff --git a/inbound/vmess.go b/protocol/vmess/inbound.go
similarity index 62%
rename from inbound/vmess.go
rename to protocol/vmess/inbound.go
index 9099bd62..1c80f376 100644
--- a/inbound/vmess.go
+++ b/protocol/vmess/inbound.go
@@ -1,4 +1,4 @@
-package inbound
+package vmess
 
 import (
 	"context"
@@ -6,6 +6,8 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
 	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/tls"
 	"github.com/sagernet/sing-box/common/uot"
@@ -19,38 +21,37 @@ import (
 	"github.com/sagernet/sing/common/auth"
 	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/ntp"
 )
 
-var (
-	_ adapter.Inbound              = (*VMess)(nil)
-	_ adapter.TCPInjectableInbound = (*VMess)(nil)
-)
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.VMessInboundOptions](registry, C.TypeVMess, NewInbound)
+}
 
-type VMess struct {
-	myInboundAdapter
+var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
+
+type Inbound struct {
+	inbound.Adapter
 	ctx       context.Context
+	router    adapter.ConnectionRouterEx
+	logger    logger.ContextLogger
+	listener  *listener.Listener
 	service   *vmess.Service[int]
 	users     []option.VMessUser
 	tlsConfig tls.ServerConfig
 	transport adapter.V2RayServerTransport
 }
 
-func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessInboundOptions) (*VMess, error) {
-	inbound := &VMess{
-		myInboundAdapter: myInboundAdapter{
-			protocol:      C.TypeVMess,
-			network:       []string{N.NetworkTCP},
-			ctx:           ctx,
-			router:        uot.NewRouter(router, logger),
-			logger:        logger,
-			tag:           tag,
-			listenOptions: options.ListenOptions,
-		},
-		ctx:   ctx,
-		users: options.Users,
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessInboundOptions) (adapter.Inbound, error) {
+	inbound := &Inbound{
+		Adapter: inbound.NewAdapter(C.TypeVMess, tag),
+		ctx:     ctx,
+		router:  uot.NewRouter(router, logger),
+		logger:  logger,
+		users:   options.Users,
 	}
 	var err error
 	inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex))
@@ -83,16 +84,22 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
 		}
 	}
 	if options.Transport != nil {
-		inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vmessTransportHandler)(inbound))
+		inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound))
 		if err != nil {
 			return nil, E.Cause(err, "create server transport: ", options.Transport.Type)
 		}
 	}
-	inbound.connHandler = inbound
+	inbound.listener = listener.New(listener.Options{
+		Context:           ctx,
+		Logger:            logger,
+		Network:           []string{N.NetworkTCP},
+		Listen:            options.ListenOptions,
+		ConnectionHandler: inbound,
+	})
 	return inbound, nil
 }
 
-func (h *VMess) Start() error {
+func (h *Inbound) Start() error {
 	err := h.service.Start()
 	if err != nil {
 		return err
@@ -104,10 +111,10 @@ func (h *VMess) Start() error {
 		}
 	}
 	if h.transport == nil {
-		return h.myInboundAdapter.Start()
+		return h.listener.Start()
 	}
 	if common.Contains(h.transport.Network(), N.NetworkTCP) {
-		tcpListener, err := h.myInboundAdapter.ListenTCP()
+		tcpListener, err := h.listener.ListenTCP()
 		if err != nil {
 			return err
 		}
@@ -119,7 +126,7 @@ func (h *VMess) Start() error {
 		}()
 	}
 	if common.Contains(h.transport.Network(), N.NetworkUDP) {
-		udpConn, err := h.myInboundAdapter.ListenUDP()
+		udpConn, err := h.listener.ListenUDP()
 		if err != nil {
 			return err
 		}
@@ -133,16 +140,16 @@ func (h *VMess) Start() error {
 	return nil
 }
 
-func (h *VMess) Close() error {
+func (h *Inbound) Close() error {
 	return common.Close(
 		h.service,
-		&h.myInboundAdapter,
+		&h.listener,
 		h.tlsConfig,
 		h.transport,
 	)
 }
 
-func (h *VMess) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	var err error
 	if h.tlsConfig != nil && h.transport == nil {
 		conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig)
@@ -153,7 +160,7 @@ func (h *VMess) NewConnection(ctx context.Context, conn net.Conn, metadata adapt
 	return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
 }
 
-func (h *VMess) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
 	err := h.NewConnection(ctx, conn, metadata)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil {
@@ -161,7 +168,7 @@ func (h *VMess) NewConnectionEx(ctx context.Context, conn net.Conn, metadata ada
 	}
 }
 
-func (h *VMess) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -176,7 +183,7 @@ func (h *VMess) newConnection(ctx context.Context, conn net.Conn, metadata adapt
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
-func (h *VMess) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	userIndex, loaded := auth.UserFromContext[int](ctx)
 	if !loaded {
 		return os.ErrInvalid
@@ -197,10 +204,32 @@ func (h *VMess) newPacketConnection(ctx context.Context, conn N.PacketConn, meta
 	return h.router.RoutePacketConnection(ctx, conn, metadata)
 }
 
-var _ adapter.V2RayServerTransportHandler = (*vmessTransportHandler)(nil)
+var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil)
 
-type vmessTransportHandler VMess
+type inboundTransportHandler Inbound
 
-func (t *vmessTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
-	(*VMess)(t).routeTCP(ctx, conn, source, destination, onClose)
+func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+	var metadata adapter.InboundContext
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	metadata.Source = source
+	metadata.Destination = destination
+	h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
+	(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose)
+}
+
+func (h *Inbound) NewError(ctx context.Context, err error) {
+	NewError(h.logger, ctx, err)
+}
+
+// Deprecated: remove
+func NewError(logger logger.ContextLogger, ctx context.Context, err error) {
+	common.Close(err)
+	if E.IsClosedOrCanceled(err) {
+		logger.DebugContext(ctx, "connection closed: ", err)
+		return
+	}
+	logger.ErrorContext(ctx, err)
 }
diff --git a/outbound/vmess.go b/protocol/vmess/outbound.go
similarity index 84%
rename from outbound/vmess.go
rename to protocol/vmess/outbound.go
index 126d2fd0..759ea8ba 100644
--- a/outbound/vmess.go
+++ b/protocol/vmess/outbound.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
diff --git a/protocol/wireguard/init.go b/protocol/wireguard/init.go
new file mode 100644
index 00000000..848c113b
--- /dev/null
+++ b/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
+}
diff --git a/outbound/wireguard.go b/protocol/wireguard/outbound.go
similarity index 74%
rename from outbound/wireguard.go
rename to protocol/wireguard/outbound.go
index 8eb043f4..7251de9e 100644
--- a/outbound/wireguard.go
+++ b/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)
 }
diff --git a/route/dns.go b/route/dns.go
index 34299ebf..a0c376c2 100644
--- a/route/dns.go
+++ b/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"))
 	}
diff --git a/route/route.go b/route/route.go
index ebffdddd..6c68cf79 100644
--- a/route/route.go
+++ b/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"
diff --git a/route/router.go b/route/router.go
index 6308127f..ce480749 100644
--- a/route/router.go
+++ b/route/router.go
@@ -99,7 +99,6 @@ func NewRouter(
 	dnsOptions option.DNSOptions,
 	ntpOptions option.NTPOptions,
 	inbounds []option.Inbound,
-	platformInterface platform.Interface,
 ) (*Router, error) {
 	router := &Router{
 		ctx:                   ctx,
@@ -122,10 +121,13 @@ func NewRouter(
 		defaultInterface:      options.DefaultInterface,
 		defaultMark:           options.DefaultMark,
 		pauseManager:          service.FromContext[pause.Manager](ctx),
-		platformInterface:     platformInterface,
+		platformInterface:     service.FromContext[platform.Interface](ctx),
 		needWIFIState:         hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule),
 		needPackageManager: common.Any(inbounds, func(inbound option.Inbound) bool {
-			return len(inbound.TunOptions.IncludePackage) > 0 || len(inbound.TunOptions.ExcludePackage) > 0
+			if tunOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN && tunOptions.AutoRoute {
+				return true
+			}
+			return false
 		}),
 	}
 	router.dnsClient = dns.NewClient(dns.ClientOptions{
@@ -324,9 +326,15 @@ func NewRouter(
 		router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range)
 	}
 
-	usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor()
+	usePlatformDefaultInterfaceMonitor := router.platformInterface != nil && router.platformInterface.UsePlatformDefaultInterfaceMonitor()
 	needInterfaceMonitor := options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool {
-		return inbound.HTTPOptions.SetSystemProxy || inbound.MixedOptions.SetSystemProxy || inbound.TunOptions.AutoRoute
+		if httpMixedOptions, isHTTPMixed := inbound.Options.(*option.HTTPMixedInboundOptions); isHTTPMixed && httpMixedOptions.SetSystemProxy {
+			return true
+		}
+		if tunOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN && tunOptions.AutoRoute {
+			return true
+		}
+		return false
 	})
 
 	if !usePlatformDefaultInterfaceMonitor {
@@ -342,7 +350,7 @@ func NewRouter(
 			interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(router.networkMonitor, router.logger, tun.DefaultInterfaceMonitorOptions{
 				InterfaceFinder:       router.interfaceFinder,
 				OverrideAndroidVPN:    options.OverrideAndroidVPN,
-				UnderNetworkExtension: platformInterface != nil && platformInterface.UnderNetworkExtension(),
+				UnderNetworkExtension: router.platformInterface != nil && router.platformInterface.UnderNetworkExtension(),
 			})
 			if err != nil {
 				return nil, E.New("auto_detect_interface unsupported on current platform")
@@ -351,7 +359,7 @@ func NewRouter(
 			router.interfaceMonitor = interfaceMonitor
 		}
 	} else {
-		interfaceMonitor := platformInterface.CreateDefaultInterfaceMonitor(router.logger)
+		interfaceMonitor := router.platformInterface.CreateDefaultInterfaceMonitor(router.logger)
 		interfaceMonitor.RegisterCallback(router.notifyNetworkUpdate)
 		router.interfaceMonitor = interfaceMonitor
 	}
diff --git a/test/brutal_test.go b/test/brutal_test.go
index 18aae2e2..ce1d2c2a 100644
--- a/test/brutal_test.go
+++ b/test/brutal_test.go
@@ -15,7 +15,7 @@ func TestBrutalShadowsocks(t *testing.T) {
 	method := shadowaead_2022.List[0]
 	password := mkBase64(t, 16)
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -46,7 +46,7 @@ func TestBrutalShadowsocks(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -100,7 +100,7 @@ func TestBrutalTrojan(t *testing.T) {
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	password := mkBase64(t, 16)
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -138,7 +138,7 @@ func TestBrutalTrojan(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -197,7 +197,7 @@ func TestBrutalTrojan(t *testing.T) {
 func TestBrutalVMess(t *testing.T) {
 	user, _ := uuid.NewV4()
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -227,7 +227,7 @@ func TestBrutalVMess(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -279,7 +279,7 @@ func TestBrutalVMess(t *testing.T) {
 func TestBrutalVLESS(t *testing.T) {
 	user, _ := uuid.NewV4()
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -326,7 +326,7 @@ func TestBrutalVLESS(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/direct_test.go b/test/direct_test.go
index 1dbf1de1..c4fd8c5e 100644
--- a/test/direct_test.go
+++ b/test/direct_test.go
@@ -11,7 +11,7 @@ import (
 // Since this is a feature one-off added by outsiders, I won't address these anymore.
 func _TestProxyProtocol(t *testing.T) {
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -33,7 +33,7 @@ func _TestProxyProtocol(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/domain_inbound_test.go b/test/domain_inbound_test.go
index 1ca2121d..c82b0d29 100644
--- a/test/domain_inbound_test.go
+++ b/test/domain_inbound_test.go
@@ -6,7 +6,7 @@ import (
 
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
-	dns "github.com/sagernet/sing-dns"
+	"github.com/sagernet/sing-dns"
 
 	"github.com/gofrs/uuid/v5"
 )
@@ -14,7 +14,7 @@ import (
 func TestTUICDomainUDP(t *testing.T) {
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -49,7 +49,7 @@ func TestTUICDomainUDP(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/ech_test.go b/test/ech_test.go
index 90eae1f4..eeac1acb 100644
--- a/test/ech_test.go
+++ b/test/ech_test.go
@@ -16,7 +16,7 @@ func TestECH(t *testing.T) {
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false))
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -55,7 +55,7 @@ func TestECH(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -109,7 +109,7 @@ func TestECHQUIC(t *testing.T) {
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false))
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -145,7 +145,7 @@ func TestECHQUIC(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -199,7 +199,7 @@ func TestECHHysteria2(t *testing.T) {
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false))
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -235,7 +235,7 @@ func TestECHHysteria2(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/http_test.go b/test/http_test.go
index 88385c27..7e724005 100644
--- a/test/http_test.go
+++ b/test/http_test.go
@@ -10,7 +10,7 @@ import (
 
 func TestHTTPSelf(t *testing.T) {
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -31,7 +31,7 @@ func TestHTTPSelf(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/hysteria2_test.go b/test/hysteria2_test.go
index 9ca2f5d3..665da552 100644
--- a/test/hysteria2_test.go
+++ b/test/hysteria2_test.go
@@ -28,7 +28,7 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) {
 		}
 	}
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -63,7 +63,7 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -115,7 +115,7 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) {
 func TestHysteria2Inbound(t *testing.T) {
 	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeHysteria2,
 				Hysteria2Options: option.Hysteria2InboundOptions{
@@ -167,7 +167,7 @@ func TestHysteria2Outbound(t *testing.T) {
 		},
 	})
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				MixedOptions: option.HTTPMixedInboundOptions{
@@ -178,7 +178,7 @@ func TestHysteria2Outbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeHysteria2,
 				Hysteria2Options: option.Hysteria2OutboundOptions{
diff --git a/test/hysteria_test.go b/test/hysteria_test.go
index bde1b9fa..dce00390 100644
--- a/test/hysteria_test.go
+++ b/test/hysteria_test.go
@@ -11,7 +11,7 @@ import (
 func TestHysteriaSelf(t *testing.T) {
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -46,7 +46,7 @@ func TestHysteriaSelf(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -98,7 +98,7 @@ func TestHysteriaSelf(t *testing.T) {
 func TestHysteriaInbound(t *testing.T) {
 	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeHysteria,
 				HysteriaOptions: option.HysteriaInboundOptions{
@@ -149,7 +149,7 @@ func TestHysteriaOutbound(t *testing.T) {
 		},
 	})
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				MixedOptions: option.HTTPMixedInboundOptions{
@@ -160,7 +160,7 @@ func TestHysteriaOutbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeHysteria,
 				HysteriaOptions: option.HysteriaOutboundOptions{
diff --git a/test/inbound_detour_test.go b/test/inbound_detour_test.go
index 9505c217..c26c81a7 100644
--- a/test/inbound_detour_test.go
+++ b/test/inbound_detour_test.go
@@ -13,7 +13,7 @@ func TestChainedInbound(t *testing.T) {
 	method := shadowaead_2022.List[0]
 	password := mkBase64(t, 16)
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -49,7 +49,7 @@ func TestChainedInbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/mux_cool_test.go b/test/mux_cool_test.go
index 81130fad..e72f244f 100644
--- a/test/mux_cool_test.go
+++ b/test/mux_cool_test.go
@@ -37,7 +37,7 @@ func TestMuxCoolServer(t *testing.T) {
 	})
 
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeVMess,
 				VMessOptions: option.VMessInboundOptions{
@@ -81,7 +81,7 @@ func TestMuxCoolClient(t *testing.T) {
 	})
 
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				MixedOptions: option.HTTPMixedInboundOptions{
@@ -92,7 +92,7 @@ func TestMuxCoolClient(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeVMess,
 				VMessOptions: option.VMessOutboundOptions{
@@ -112,7 +112,7 @@ func TestMuxCoolClient(t *testing.T) {
 func TestMuxCoolSelf(t *testing.T) {
 	user := newUUID()
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -139,7 +139,7 @@ func TestMuxCoolSelf(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/mux_test.go b/test/mux_test.go
index 8d755185..335def2e 100644
--- a/test/mux_test.go
+++ b/test/mux_test.go
@@ -55,7 +55,7 @@ func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) {
 	method := shadowaead_2022.List[0]
 	password := mkBase64(t, 16)
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -81,7 +81,7 @@ func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -125,7 +125,7 @@ func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) {
 func testVMessMux(t *testing.T, options option.OutboundMultiplexOptions) {
 	user, _ := uuid.NewV4()
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -154,7 +154,7 @@ func testVMessMux(t *testing.T, options option.OutboundMultiplexOptions) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/naive_test.go b/test/naive_test.go
index 1a1547da..fe3e7dce 100644
--- a/test/naive_test.go
+++ b/test/naive_test.go
@@ -13,7 +13,7 @@ import (
 func TestNaiveInboundWithNginx(t *testing.T) {
 	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeNaive,
 				NaiveOptions: option.NaiveInboundOptions{
@@ -59,7 +59,7 @@ func TestNaiveInboundWithNginx(t *testing.T) {
 func TestNaiveInbound(t *testing.T) {
 	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeNaive,
 				NaiveOptions: option.NaiveInboundOptions{
@@ -103,7 +103,7 @@ func TestNaiveInbound(t *testing.T) {
 func TestNaiveHTTP3Inbound(t *testing.T) {
 	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeNaive,
 				NaiveOptions: option.NaiveInboundOptions{
diff --git a/test/shadowsocks_legacy_test.go b/test/shadowsocks_legacy_test.go
index 8075a7df..ae6f38e4 100644
--- a/test/shadowsocks_legacy_test.go
+++ b/test/shadowsocks_legacy_test.go
@@ -24,7 +24,7 @@ func testShadowsocksLegacy(t *testing.T, method string) {
 		},
 	})
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				MixedOptions: option.HTTPMixedInboundOptions{
@@ -35,7 +35,7 @@ func testShadowsocksLegacy(t *testing.T, method string) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeShadowsocks,
 				ShadowsocksOptions: option.ShadowsocksOutboundOptions{
diff --git a/test/shadowsocks_test.go b/test/shadowsocks_test.go
index 4ef1ee9d..0f7af765 100644
--- a/test/shadowsocks_test.go
+++ b/test/shadowsocks_test.go
@@ -99,7 +99,7 @@ func testShadowsocksInboundWithShadowsocksRust(t *testing.T, method string, pass
 		Cmd:        []string{"-s", F.ToString("127.0.0.1:", serverPort), "-b", F.ToString("0.0.0.0:", clientPort), "-m", method, "-k", password, "-U"},
 	})
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeShadowsocks,
 				ShadowsocksOptions: option.ShadowsocksInboundOptions{
@@ -124,7 +124,7 @@ func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, pas
 		Cmd:        []string{"-s", F.ToString("0.0.0.0:", serverPort), "-m", method, "-k", password, "-U"},
 	})
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				MixedOptions: option.HTTPMixedInboundOptions{
@@ -135,7 +135,7 @@ func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, pas
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeShadowsocks,
 				ShadowsocksOptions: option.ShadowsocksOutboundOptions{
@@ -154,7 +154,7 @@ func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, pas
 
 func testShadowsocksSelf(t *testing.T, method string, password string) {
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -177,7 +177,7 @@ func testShadowsocksSelf(t *testing.T, method string, password string) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -221,7 +221,7 @@ func TestShadowsocksUoT(t *testing.T) {
 	method := shadowaead_2022.List[0]
 	password := mkBase64(t, 16)
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -244,7 +244,7 @@ func TestShadowsocksUoT(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -289,7 +289,7 @@ func TestShadowsocksUoT(t *testing.T) {
 
 func testShadowsocks2022EIH(t *testing.T, method string, password string) {
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -317,7 +317,7 @@ func testShadowsocks2022EIH(t *testing.T, method string, password string) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/shadowtls_test.go b/test/shadowtls_test.go
index 6f9ee1e5..71e8d9fa 100644
--- a/test/shadowtls_test.go
+++ b/test/shadowtls_test.go
@@ -37,7 +37,7 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool)
 	method := shadowaead_2022.List[0]
 	ssPassword := mkBase64(t, 16)
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				MixedOptions: option.HTTPMixedInboundOptions{
@@ -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{
@@ -142,7 +142,7 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool)
 
 func TestShadowTLSFallback(t *testing.T) {
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeShadowTLS,
 				ShadowTLSOptions: option.ShadowTLSInboundOptions{
@@ -189,7 +189,7 @@ func TestShadowTLSInbound(t *testing.T) {
 		Cmd:        []string{"--v3", "--threads", "1", "client", "--listen", "0.0.0.0:" + F.ToString(otherPort), "--server", "127.0.0.1:" + F.ToString(serverPort), "--sni", "google.com", "--password", password},
 	})
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "in",
@@ -232,7 +232,7 @@ func TestShadowTLSInbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -283,7 +283,7 @@ func TestShadowTLSOutbound(t *testing.T) {
 		Env:        []string{"RUST_LOG=trace"},
 	})
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				MixedOptions: option.HTTPMixedInboundOptions{
@@ -306,7 +306,7 @@ func TestShadowTLSOutbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeShadowsocks,
 				ShadowsocksOptions: option.ShadowsocksOutboundOptions{
diff --git a/test/ss_plugin_test.go b/test/ss_plugin_test.go
index 94606b70..3f837b4e 100644
--- a/test/ss_plugin_test.go
+++ b/test/ss_plugin_test.go
@@ -33,7 +33,7 @@ func testShadowsocksPlugin(t *testing.T, name string, opts string, args string)
 		},
 	})
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				MixedOptions: option.HTTPMixedInboundOptions{
@@ -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{
diff --git a/test/tfo_test.go b/test/tfo_test.go
index 7bd34e2d..458a936d 100644
--- a/test/tfo_test.go
+++ b/test/tfo_test.go
@@ -13,7 +13,7 @@ func TestTCPSlowOpen(t *testing.T) {
 	method := shadowaead.List[0]
 	password := mkBase64(t, 16)
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -37,7 +37,7 @@ func TestTCPSlowOpen(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/tls_test.go b/test/tls_test.go
index cfc6c1a5..b42d924f 100644
--- a/test/tls_test.go
+++ b/test/tls_test.go
@@ -11,7 +11,7 @@ import (
 func TestUTLS(t *testing.T) {
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -46,7 +46,7 @@ func TestUTLS(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/trojan_test.go b/test/trojan_test.go
index f88ec885..1a206c66 100644
--- a/test/trojan_test.go
+++ b/test/trojan_test.go
@@ -20,7 +20,7 @@ func TestTrojanOutbound(t *testing.T) {
 		},
 	})
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				MixedOptions: option.HTTPMixedInboundOptions{
@@ -31,7 +31,7 @@ func TestTrojanOutbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeTrojan,
 				TrojanOptions: option.TrojanOutboundOptions{
@@ -57,7 +57,7 @@ func TestTrojanOutbound(t *testing.T) {
 func TestTrojanSelf(t *testing.T) {
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -92,7 +92,7 @@ func TestTrojanSelf(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -140,7 +140,7 @@ func TestTrojanSelf(t *testing.T) {
 
 func TestTrojanPlainSelf(t *testing.T) {
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -167,7 +167,7 @@ func TestTrojanPlainSelf(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/tuic_test.go b/test/tuic_test.go
index 5b838f22..41fb7599 100644
--- a/test/tuic_test.go
+++ b/test/tuic_test.go
@@ -29,7 +29,7 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) {
 		udpRelayMode = "quic"
 	}
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -62,7 +62,7 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -113,7 +113,7 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) {
 func TestTUICInbound(t *testing.T) {
 	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeTUIC,
 				TUICOptions: option.TUICInboundOptions{
@@ -160,7 +160,7 @@ func TestTUICOutbound(t *testing.T) {
 		},
 	})
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				MixedOptions: option.HTTPMixedInboundOptions{
@@ -171,7 +171,7 @@ func TestTUICOutbound(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeTUIC,
 				TUICOptions: option.TUICOutboundOptions{
diff --git a/test/v2ray_api_test.go b/test/v2ray_api_test.go
index 1bea41a6..cd7ae2c4 100644
--- a/test/v2ray_api_test.go
+++ b/test/v2ray_api_test.go
@@ -14,7 +14,7 @@ import (
 
 func TestV2RayAPI(t *testing.T) {
 	i := startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "in",
@@ -26,7 +26,7 @@ func TestV2RayAPI(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 				Tag:  "out",
diff --git a/test/v2ray_grpc_test.go b/test/v2ray_grpc_test.go
index fa43f753..5cf87543 100644
--- a/test/v2ray_grpc_test.go
+++ b/test/v2ray_grpc_test.go
@@ -27,7 +27,7 @@ func testV2RayGRPCInbound(t *testing.T, forceLite bool) {
 	require.NoError(t, err)
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeVMess,
 				VMessOptions: option.VMessInboundOptions{
@@ -126,7 +126,7 @@ func testV2RayGRPCOutbound(t *testing.T, forceLite bool) {
 		},
 	})
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -138,7 +138,7 @@ func testV2RayGRPCOutbound(t *testing.T, forceLite bool) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeVMess,
 				Tag:  "vmess-out",
diff --git a/test/v2ray_transport_test.go b/test/v2ray_transport_test.go
index c7362f34..27074e78 100644
--- a/test/v2ray_transport_test.go
+++ b/test/v2ray_transport_test.go
@@ -44,7 +44,7 @@ func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions,
 	require.NoError(t, err)
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -80,7 +80,7 @@ func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions,
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -133,7 +133,7 @@ func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions,
 	require.NoError(t, err)
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -169,7 +169,7 @@ func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions,
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -224,7 +224,7 @@ func TestVMessQUICSelf(t *testing.T) {
 	require.NoError(t, err)
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -260,7 +260,7 @@ func TestVMessQUICSelf(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
@@ -312,7 +312,7 @@ func testV2RayTransportNOTLSSelf(t *testing.T, transport *option.V2RayTransportO
 	user, err := uuid.DefaultGenerator.NewV4()
 	require.NoError(t, err)
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -340,7 +340,7 @@ func testV2RayTransportNOTLSSelf(t *testing.T, transport *option.V2RayTransportO
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/v2ray_ws_test.go b/test/v2ray_ws_test.go
index 0e238c28..de8d4bdc 100644
--- a/test/v2ray_ws_test.go
+++ b/test/v2ray_ws_test.go
@@ -61,7 +61,7 @@ func testV2RayWebsocketInbound(t *testing.T, maxEarlyData uint32, earlyDataHeade
 	require.NoError(t, err)
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeVMess,
 				VMessOptions: option.VMessInboundOptions{
@@ -158,7 +158,7 @@ func testV2RayWebsocketOutbound(t *testing.T, maxEarlyData uint32, earlyDataHead
 		},
 	})
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -170,7 +170,7 @@ func testV2RayWebsocketOutbound(t *testing.T, maxEarlyData uint32, earlyDataHead
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeVMess,
 				Tag:  "vmess-out",
diff --git a/test/vmess_test.go b/test/vmess_test.go
index fcf7bf8f..9f81d9a0 100644
--- a/test/vmess_test.go
+++ b/test/vmess_test.go
@@ -181,7 +181,7 @@ func testVMessInboundWithV2Ray(t *testing.T, security string, alterId int, authe
 	})
 
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeVMess,
 				VMessOptions: option.VMessInboundOptions{
@@ -229,7 +229,7 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, globalPadding boo
 	})
 
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				MixedOptions: option.HTTPMixedInboundOptions{
@@ -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{
@@ -263,7 +263,7 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, globalPadding boo
 func testVMessSelf(t *testing.T, security string, alterId int, globalPadding bool, authenticatedLength bool, packetAddr bool) {
 	user := newUUID()
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				Tag:  "mixed-in",
@@ -291,7 +291,7 @@ func testVMessSelf(t *testing.T, security string, alterId int, globalPadding boo
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeDirect,
 			},
diff --git a/test/wireguard_test.go b/test/wireguard_test.go
index 50e87ee0..70c0e5a5 100644
--- a/test/wireguard_test.go
+++ b/test/wireguard_test.go
@@ -21,7 +21,7 @@ func _TestWireGuard(t *testing.T) {
 	})
 	time.Sleep(5 * time.Second)
 	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
+		Inbounds: []option.LegacyInbound{
 			{
 				Type: C.TypeMixed,
 				MixedOptions: option.HTTPMixedInboundOptions{
@@ -32,7 +32,7 @@ func _TestWireGuard(t *testing.T) {
 				},
 			},
 		},
-		Outbounds: []option.Outbound{
+		LegacyOutbounds: []option.LegacyOutbound{
 			{
 				Type: C.TypeWireGuard,
 				WireGuardOptions: option.WireGuardOutboundOptions{
diff --git a/test/wrapper_test.go b/test/wrapper_test.go
index d2b6b9ff..a7c23f33 100644
--- a/test/wrapper_test.go
+++ b/test/wrapper_test.go
@@ -10,7 +10,7 @@ import (
 )
 
 func TestOptionsWrapper(t *testing.T) {
-	inbound := option.Inbound{
+	inbound := option.LegacyInbound{
 		Type: C.TypeHTTP,
 		HTTPOptions: option.HTTPMixedInboundOptions{
 			InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
diff --git a/transport/trojan/mux.go b/transport/trojan/mux.go
index b1cc9985..0329bd40 100644
--- a/transport/trojan/mux.go
+++ b/transport/trojan/mux.go
@@ -8,12 +8,13 @@ import (
 	"github.com/sagernet/sing/common/buf"
 	"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"
 	"github.com/sagernet/sing/common/task"
 	"github.com/sagernet/smux"
 )
 
-func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler) error {
+func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler, logger logger.ContextLogger) error {
 	session, err := smux.Server(conn, smuxConfig())
 	if err != nil {
 		return err
@@ -26,7 +27,7 @@ func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata
 			if err != nil {
 				return err
 			}
-			go newMuxConnection(ctx, stream, metadata, handler)
+			go newMuxConnection(ctx, stream, metadata, handler, logger)
 		}
 	})
 	group.Cleanup(func() {
@@ -35,10 +36,10 @@ func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata
 	return group.Run(ctx)
 }
 
-func newMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler) {
+func newMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler, logger logger.ContextLogger) {
 	err := newMuxConnection0(ctx, conn, metadata, handler)
 	if err != nil {
-		handler.NewError(ctx, E.Cause(err, "process trojan-go multiplex connection"))
+		logger.ErrorContext(ctx, E.Cause(err, "process trojan-go multiplex connection"))
 	}
 }
 
diff --git a/transport/trojan/service.go b/transport/trojan/service.go
index 97f674ab..978d737f 100644
--- a/transport/trojan/service.go
+++ b/transport/trojan/service.go
@@ -9,6 +9,7 @@ import (
 	"github.com/sagernet/sing/common/buf"
 	"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/rw"
@@ -17,7 +18,6 @@ import (
 type Handler interface {
 	N.TCPConnectionHandler
 	N.UDPConnectionHandler
-	E.Handler
 }
 
 type Service[K comparable] struct {
@@ -25,14 +25,16 @@ type Service[K comparable] struct {
 	keys            map[[56]byte]K
 	handler         Handler
 	fallbackHandler N.TCPConnectionHandler
+	logger          logger.ContextLogger
 }
 
-func NewService[K comparable](handler Handler, fallbackHandler N.TCPConnectionHandler) *Service[K] {
+func NewService[K comparable](handler Handler, fallbackHandler N.TCPConnectionHandler, logger logger.ContextLogger) *Service[K] {
 	return &Service[K]{
 		users:           make(map[K][56]byte),
 		keys:            make(map[[56]byte]K),
 		handler:         handler,
 		fallbackHandler: fallbackHandler,
+		logger:          logger,
 	}
 }
 
@@ -110,7 +112,7 @@ func (s *Service[K]) NewConnection(ctx context.Context, conn net.Conn, metadata
 		return s.handler.NewPacketConnection(ctx, &PacketConn{Conn: conn}, metadata)
 	// case CommandMux:
 	default:
-		return HandleMuxConnection(ctx, conn, metadata, s.handler)
+		return HandleMuxConnection(ctx, conn, metadata, s.handler, s.logger)
 	}
 }
 
diff --git a/transport/wireguard/client_bind.go b/transport/wireguard/client_bind.go
index 6c534532..20e7c079 100644
--- a/transport/wireguard/client_bind.go
+++ b/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
diff --git a/transport/wireguard/resolve.go b/transport/wireguard/resolve.go
index 5b4124d2..d7a1d19c 100644
--- a/transport/wireguard/resolve.go
+++ b/transport/wireguard/resolve.go
@@ -8,7 +8,7 @@ import (
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/option"
-	dns "github.com/sagernet/sing-dns"
+	"github.com/sagernet/sing-dns"
 	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
 )