Procházet zdrojové kódy

Crazy sekai overturns the small pond

世界 před 1 rokem
rodič
revize
8304295c48
100 změnil soubory, kde provedl 2164 přidání a 1260 odebrání
  1. 0 104
      adapter/conn_router.go
  2. 26 0
      adapter/handler.go
  3. 19 16
      adapter/inbound.go
  4. 0 5
      adapter/outbound.go
  5. 13 22
      adapter/router.go
  6. 38 0
      adapter/rule.go
  7. 115 62
      adapter/upstream.go
  8. 216 0
      adapter/upstream_legacy.go
  9. 1 3
      adapter/v2ray.go
  10. 2 2
      cmd/sing-box/cmd_rule_set_match.go
  11. 4 4
      cmd/sing-box/internal/convertor/adguard/convertor_test.go
  12. 15 2
      common/mux/router.go
  13. 0 32
      common/mux/v2ray_legacy.go
  14. 1 1
      common/sniff/sniff.go
  15. 36 3
      common/uot/router.go
  16. 15 0
      constant/rule.go
  17. 1 2
      experimental/clashapi/rules.go
  18. 20 15
      experimental/clashapi/trafficontrol/tracker.go
  19. 6 6
      go.mod
  20. 12 12
      go.sum
  21. 32 19
      inbound/default.go
  22. 7 11
      inbound/default_tcp.go
  23. 33 54
      inbound/default_udp.go
  24. 28 21
      inbound/direct.go
  25. 22 17
      inbound/http.go
  26. 15 11
      inbound/mixed.go
  27. 17 9
      inbound/naive.go
  28. 5 4
      inbound/redirect.go
  29. 27 12
      inbound/shadowsocks.go
  30. 19 14
      inbound/shadowsocks_multi.go
  31. 18 13
      inbound/shadowsocks_relay.go
  32. 9 0
      inbound/shadowtls.go
  33. 10 9
      inbound/socks.go
  34. 23 20
      inbound/tproxy.go
  35. 11 15
      inbound/trojan.go
  36. 46 42
      inbound/tun.go
  37. 11 15
      inbound/vless.go
  38. 11 15
      inbound/vmess.go
  39. 2 1
      include/quic_stub.go
  40. 1 0
      option/inbound.go
  41. 40 8
      option/rule.go
  42. 166 0
      option/rule_action.go
  43. 40 17
      option/rule_dns.go
  44. 23 3
      option/types.go
  45. 2 0
      outbound/block.go
  46. 5 5
      outbound/default.go
  47. 8 6
      outbound/direct.go
  48. 2 0
      outbound/dns.go
  49. 0 8
      outbound/http.go
  50. 0 8
      outbound/hysteria.go
  51. 0 8
      outbound/hysteria2.go
  52. 3 0
      outbound/proxy.go
  53. 14 2
      outbound/selector.go
  54. 0 8
      outbound/shadowsocks.go
  55. 0 8
      outbound/shadowtls.go
  56. 4 0
      outbound/socks.go
  57. 0 8
      outbound/ssh.go
  58. 0 8
      outbound/tor.go
  59. 0 8
      outbound/trojan.go
  60. 0 8
      outbound/tuic.go
  61. 4 0
      outbound/urltest.go
  62. 0 8
      outbound/vless.go
  63. 0 8
      outbound/vmess.go
  64. 4 0
      outbound/wireguard.go
  65. 2 1
      route/geo_resources.go
  66. 583 0
      route/route.go
  67. 50 55
      route/route_dns.go
  68. 10 384
      route/router.go
  69. 10 10
      route/rule/rule_abstract.go
  70. 228 0
      route/rule/rule_action.go
  71. 31 17
      route/rule/rule_default.go
  72. 20 43
      route/rule/rule_dns.go
  73. 1 1
      route/rule/rule_headless.go
  74. 1 1
      route/rule/rule_item_adguard.go
  75. 1 1
      route/rule/rule_item_auth_user.go
  76. 1 1
      route/rule/rule_item_cidr.go
  77. 1 1
      route/rule/rule_item_clash_mode.go
  78. 1 1
      route/rule/rule_item_client.go
  79. 1 1
      route/rule/rule_item_domain.go
  80. 1 1
      route/rule/rule_item_domain_keyword.go
  81. 1 1
      route/rule/rule_item_domain_regex.go
  82. 1 1
      route/rule/rule_item_geoip.go
  83. 1 1
      route/rule/rule_item_geosite.go
  84. 1 1
      route/rule/rule_item_inbound.go
  85. 1 1
      route/rule/rule_item_ip_is_private.go
  86. 1 1
      route/rule/rule_item_ipversion.go
  87. 1 1
      route/rule/rule_item_network.go
  88. 1 1
      route/rule/rule_item_outbound.go
  89. 1 1
      route/rule/rule_item_package_name.go
  90. 1 1
      route/rule/rule_item_port.go
  91. 1 1
      route/rule/rule_item_port_range.go
  92. 1 1
      route/rule/rule_item_process_name.go
  93. 1 1
      route/rule/rule_item_process_path.go
  94. 1 1
      route/rule/rule_item_process_path_regex.go
  95. 1 1
      route/rule/rule_item_protocol.go
  96. 1 1
      route/rule/rule_item_query_type.go
  97. 1 1
      route/rule/rule_item_rule_set.go
  98. 1 1
      route/rule/rule_item_user.go
  99. 1 1
      route/rule/rule_item_user_id.go
  100. 1 1
      route/rule/rule_item_wifi_bssid.go

+ 0 - 104
adapter/conn_router.go

@@ -1,104 +0,0 @@
-package adapter
-
-import (
-	"context"
-	"net"
-
-	"github.com/sagernet/sing/common/logger"
-	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
-)
-
-type ConnectionRouter interface {
-	RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
-	RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
-}
-
-func NewRouteHandler(
-	metadata InboundContext,
-	router ConnectionRouter,
-	logger logger.ContextLogger,
-) UpstreamHandlerAdapter {
-	return &routeHandlerWrapper{
-		metadata: metadata,
-		router:   router,
-		logger:   logger,
-	}
-}
-
-func NewRouteContextHandler(
-	router ConnectionRouter,
-	logger logger.ContextLogger,
-) UpstreamHandlerAdapter {
-	return &routeContextHandlerWrapper{
-		router: router,
-		logger: logger,
-	}
-}
-
-var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
-
-type routeHandlerWrapper struct {
-	metadata InboundContext
-	router   ConnectionRouter
-	logger   logger.ContextLogger
-}
-
-func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
-	myMetadata := w.metadata
-	if metadata.Source.IsValid() {
-		myMetadata.Source = metadata.Source
-	}
-	if metadata.Destination.IsValid() {
-		myMetadata.Destination = metadata.Destination
-	}
-	return w.router.RouteConnection(ctx, conn, myMetadata)
-}
-
-func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
-	myMetadata := w.metadata
-	if metadata.Source.IsValid() {
-		myMetadata.Source = metadata.Source
-	}
-	if metadata.Destination.IsValid() {
-		myMetadata.Destination = metadata.Destination
-	}
-	return w.router.RoutePacketConnection(ctx, conn, myMetadata)
-}
-
-func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
-	w.logger.ErrorContext(ctx, err)
-}
-
-var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
-
-type routeContextHandlerWrapper struct {
-	router ConnectionRouter
-	logger logger.ContextLogger
-}
-
-func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
-	myMetadata := ContextFrom(ctx)
-	if metadata.Source.IsValid() {
-		myMetadata.Source = metadata.Source
-	}
-	if metadata.Destination.IsValid() {
-		myMetadata.Destination = metadata.Destination
-	}
-	return w.router.RouteConnection(ctx, conn, *myMetadata)
-}
-
-func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
-	myMetadata := ContextFrom(ctx)
-	if metadata.Source.IsValid() {
-		myMetadata.Source = metadata.Source
-	}
-	if metadata.Destination.IsValid() {
-		myMetadata.Destination = metadata.Destination
-	}
-	return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
-}
-
-func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
-	w.logger.ErrorContext(ctx, err)
-}

+ 26 - 0
adapter/handler.go

@@ -6,27 +6,53 @@ import (
 
 	"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"
 )
 
+// Deprecated
 type ConnectionHandler interface {
 	NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
 }
 
+type ConnectionHandlerEx interface {
+	NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
+}
+
+// Deprecated: use PacketHandlerEx instead
 type PacketHandler interface {
 	NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
 }
 
+type PacketHandlerEx interface {
+	NewPacketEx(buffer *buf.Buffer, source M.Socksaddr)
+}
+
+// Deprecated: use OOBPacketHandlerEx instead
 type OOBPacketHandler interface {
 	NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error
 }
 
+type OOBPacketHandlerEx interface {
+	NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
+}
+
+// Deprecated
 type PacketConnectionHandler interface {
 	NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
 }
 
+type PacketConnectionHandlerEx interface {
+	NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
+}
+
 type UpstreamHandlerAdapter interface {
 	N.TCPConnectionHandler
 	N.UDPConnectionHandler
 	E.Handler
 }
+
+type UpstreamHandlerAdapterEx interface {
+	N.TCPConnectionHandlerEx
+	N.UDPConnectionHandlerEx
+}

+ 19 - 16
adapter/inbound.go

@@ -2,13 +2,11 @@ package adapter
 
 import (
 	"context"
-	"net"
 	"net/netip"
 
 	"github.com/sagernet/sing-box/common/process"
 	"github.com/sagernet/sing-box/option"
 	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
 )
 
 type Inbound interface {
@@ -17,11 +15,14 @@ type Inbound interface {
 	Tag() string
 }
 
-type InjectableInbound interface {
+type TCPInjectableInbound interface {
 	Inbound
-	Network() []string
-	NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
-	NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
+	ConnectionHandlerEx
+}
+
+type UDPInjectableInbound interface {
+	Inbound
+	PacketConnectionHandlerEx
 }
 
 type InboundContext struct {
@@ -43,16 +44,18 @@ type InboundContext struct {
 
 	// cache
 
-	InboundDetour        string
-	LastInbound          string
-	OriginDestination    M.Socksaddr
-	InboundOptions       option.InboundOptions
-	DestinationAddresses []netip.Addr
-	SourceGeoIPCode      string
-	GeoIPCode            string
-	ProcessInfo          *process.Info
-	QueryType            uint16
-	FakeIP               bool
+	InboundDetour     string
+	LastInbound       string
+	OriginDestination M.Socksaddr
+	// Deprecated
+	InboundOptions            option.InboundOptions
+	UDPDisableDomainUnmapping bool
+	DestinationAddresses      []netip.Addr
+	SourceGeoIPCode           string
+	GeoIPCode                 string
+	ProcessInfo               *process.Info
+	QueryType                 uint16
+	FakeIP                    bool
 
 	// rule cache
 

+ 0 - 5
adapter/outbound.go

@@ -1,9 +1,6 @@
 package adapter
 
 import (
-	"context"
-	"net"
-
 	N "github.com/sagernet/sing/common/network"
 )
 
@@ -15,6 +12,4 @@ type Outbound interface {
 	Network() []string
 	Dependencies() []string
 	N.Dialer
-	NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
-	NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
 }

+ 13 - 22
adapter/router.go

@@ -34,6 +34,7 @@ type Router interface {
 	FakeIPStore() FakeIPStore
 
 	ConnectionRouter
+	ConnectionRouterEx
 
 	GeoIPReader() *geoip.Reader
 	LoadGeosite(code string) (Rule, error)
@@ -70,34 +71,24 @@ type Router interface {
 	ResetNetwork() error
 }
 
-func ContextWithRouter(ctx context.Context, router Router) context.Context {
-	return service.ContextWith(ctx, router)
+// Deprecated: Use ConnectionRouterEx instead.
+type ConnectionRouter interface {
+	RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
+	RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
 }
 
-func RouterFromContext(ctx context.Context) Router {
-	return service.FromContext[Router](ctx)
-}
-
-type HeadlessRule interface {
-	Match(metadata *InboundContext) bool
-	String() string
+type ConnectionRouterEx interface {
+	ConnectionRouter
+	RouteConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
+	RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
 }
 
-type Rule interface {
-	HeadlessRule
-	Service
-	Type() string
-	UpdateGeosite() error
-	Outbound() string
+func ContextWithRouter(ctx context.Context, router Router) context.Context {
+	return service.ContextWith(ctx, router)
 }
 
-type DNSRule interface {
-	Rule
-	DisableCache() bool
-	RewriteTTL() *uint32
-	ClientSubnet() *netip.Prefix
-	WithAddressLimit() bool
-	MatchAddressLimit(metadata *InboundContext) bool
+func RouterFromContext(ctx context.Context) Router {
+	return service.FromContext[Router](ctx)
 }
 
 type RuleSet interface {

+ 38 - 0
adapter/rule.go

@@ -0,0 +1,38 @@
+package adapter
+
+import (
+	C "github.com/sagernet/sing-box/constant"
+)
+
+type HeadlessRule interface {
+	Match(metadata *InboundContext) bool
+	String() string
+}
+
+type Rule interface {
+	HeadlessRule
+	Service
+	Type() string
+	UpdateGeosite() error
+	Action() RuleAction
+}
+
+type DNSRule interface {
+	Rule
+	WithAddressLimit() bool
+	MatchAddressLimit(metadata *InboundContext) bool
+}
+
+type RuleAction interface {
+	Type() string
+	String() string
+}
+
+func IsFinalAction(action RuleAction) bool {
+	switch action.Type() {
+	case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
+		return false
+	default:
+		return true
+	}
+}

+ 115 - 62
adapter/upstream.go

@@ -4,112 +4,165 @@ import (
 	"context"
 	"net"
 
-	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
 
 type (
-	ConnectionHandlerFunc       = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
-	PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
+	ConnectionHandlerFuncEx       = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
+	PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
 )
 
-func NewUpstreamHandler(
+func NewUpstreamHandlerEx(
 	metadata InboundContext,
-	connectionHandler ConnectionHandlerFunc,
-	packetHandler PacketConnectionHandlerFunc,
-	errorHandler E.Handler,
-) UpstreamHandlerAdapter {
-	return &myUpstreamHandlerWrapper{
+	connectionHandler ConnectionHandlerFuncEx,
+	packetHandler PacketConnectionHandlerFuncEx,
+) UpstreamHandlerAdapterEx {
+	return &myUpstreamHandlerWrapperEx{
 		metadata:          metadata,
 		connectionHandler: connectionHandler,
 		packetHandler:     packetHandler,
-		errorHandler:      errorHandler,
 	}
 }
 
-var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
+var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil)
 
-type myUpstreamHandlerWrapper struct {
+type myUpstreamHandlerWrapperEx struct {
 	metadata          InboundContext
-	connectionHandler ConnectionHandlerFunc
-	packetHandler     PacketConnectionHandlerFunc
-	errorHandler      E.Handler
+	connectionHandler ConnectionHandlerFuncEx
+	packetHandler     PacketConnectionHandlerFuncEx
 }
 
-func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
+func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	myMetadata := w.metadata
-	if metadata.Source.IsValid() {
-		myMetadata.Source = metadata.Source
+	if source.IsValid() {
+		myMetadata.Source = source
 	}
-	if metadata.Destination.IsValid() {
-		myMetadata.Destination = metadata.Destination
+	if destination.IsValid() {
+		myMetadata.Destination = destination
 	}
-	return w.connectionHandler(ctx, conn, myMetadata)
+	w.connectionHandler(ctx, conn, myMetadata, onClose)
 }
 
-func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
+func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	myMetadata := w.metadata
-	if metadata.Source.IsValid() {
-		myMetadata.Source = metadata.Source
+	if source.IsValid() {
+		myMetadata.Source = source
 	}
-	if metadata.Destination.IsValid() {
-		myMetadata.Destination = metadata.Destination
+	if destination.IsValid() {
+		myMetadata.Destination = destination
 	}
-	return w.packetHandler(ctx, conn, myMetadata)
+	w.packetHandler(ctx, conn, myMetadata, onClose)
 }
 
-func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
-	w.errorHandler.NewError(ctx, err)
-}
-
-func UpstreamMetadata(metadata InboundContext) M.Metadata {
-	return M.Metadata{
-		Source:      metadata.Source,
-		Destination: metadata.Destination,
-	}
-}
+var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil)
 
-type myUpstreamContextHandlerWrapper struct {
-	connectionHandler ConnectionHandlerFunc
-	packetHandler     PacketConnectionHandlerFunc
-	errorHandler      E.Handler
+type myUpstreamContextHandlerWrapperEx struct {
+	connectionHandler ConnectionHandlerFuncEx
+	packetHandler     PacketConnectionHandlerFuncEx
 }
 
-func NewUpstreamContextHandler(
-	connectionHandler ConnectionHandlerFunc,
-	packetHandler PacketConnectionHandlerFunc,
-	errorHandler E.Handler,
-) UpstreamHandlerAdapter {
-	return &myUpstreamContextHandlerWrapper{
+func NewUpstreamContextHandlerEx(
+	connectionHandler ConnectionHandlerFuncEx,
+	packetHandler PacketConnectionHandlerFuncEx,
+) UpstreamHandlerAdapterEx {
+	return &myUpstreamContextHandlerWrapperEx{
 		connectionHandler: connectionHandler,
 		packetHandler:     packetHandler,
-		errorHandler:      errorHandler,
 	}
 }
 
-func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
+func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	myMetadata := ContextFrom(ctx)
-	if metadata.Source.IsValid() {
-		myMetadata.Source = metadata.Source
+	if source.IsValid() {
+		myMetadata.Source = source
 	}
-	if metadata.Destination.IsValid() {
-		myMetadata.Destination = metadata.Destination
+	if destination.IsValid() {
+		myMetadata.Destination = destination
 	}
-	return w.connectionHandler(ctx, conn, *myMetadata)
+	w.connectionHandler(ctx, conn, *myMetadata, onClose)
 }
 
-func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
+func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
 	myMetadata := ContextFrom(ctx)
-	if metadata.Source.IsValid() {
-		myMetadata.Source = metadata.Source
+	if source.IsValid() {
+		myMetadata.Source = source
+	}
+	if destination.IsValid() {
+		myMetadata.Destination = destination
+	}
+	w.packetHandler(ctx, conn, *myMetadata, onClose)
+}
+
+func NewRouteHandlerEx(
+	metadata InboundContext,
+	router ConnectionRouterEx,
+) UpstreamHandlerAdapterEx {
+	return &routeHandlerWrapperEx{
+		metadata: metadata,
+		router:   router,
+	}
+}
+
+var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil)
+
+type routeHandlerWrapperEx struct {
+	metadata InboundContext
+	router   ConnectionRouterEx
+}
+
+func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+	if source.IsValid() {
+		r.metadata.Source = source
 	}
-	if metadata.Destination.IsValid() {
-		myMetadata.Destination = metadata.Destination
+	if destination.IsValid() {
+		r.metadata.Destination = destination
 	}
-	return w.packetHandler(ctx, conn, *myMetadata)
+	r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose)
 }
 
-func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
-	w.errorHandler.NewError(ctx, err)
+func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+	if source.IsValid() {
+		r.metadata.Source = source
+	}
+	if destination.IsValid() {
+		r.metadata.Destination = destination
+	}
+	r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose)
+}
+
+func NewRouteContextHandlerEx(
+	router ConnectionRouterEx,
+) UpstreamHandlerAdapterEx {
+	return &routeContextHandlerWrapperEx{
+		router: router,
+	}
+}
+
+var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil)
+
+type routeContextHandlerWrapperEx struct {
+	router ConnectionRouterEx
+}
+
+func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+	metadata := ContextFrom(ctx)
+	if source.IsValid() {
+		metadata.Source = source
+	}
+	if destination.IsValid() {
+		metadata.Destination = destination
+	}
+	r.router.RouteConnectionEx(ctx, conn, *metadata, onClose)
+}
+
+func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+	metadata := ContextFrom(ctx)
+	if source.IsValid() {
+		metadata.Source = source
+	}
+	if destination.IsValid() {
+		metadata.Destination = destination
+	}
+	r.router.RoutePacketConnectionEx(ctx, conn, *metadata, onClose)
 }

+ 216 - 0
adapter/upstream_legacy.go

@@ -0,0 +1,216 @@
+package adapter
+
+import (
+	"context"
+	"net"
+
+	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 (
+	// Deprecated
+	ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
+	// Deprecated
+	PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
+)
+
+// Deprecated
+func NewUpstreamHandler(
+	metadata InboundContext,
+	connectionHandler ConnectionHandlerFunc,
+	packetHandler PacketConnectionHandlerFunc,
+	errorHandler E.Handler,
+) UpstreamHandlerAdapter {
+	return &myUpstreamHandlerWrapper{
+		metadata:          metadata,
+		connectionHandler: connectionHandler,
+		packetHandler:     packetHandler,
+		errorHandler:      errorHandler,
+	}
+}
+
+var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
+
+// Deprecated
+type myUpstreamHandlerWrapper struct {
+	metadata          InboundContext
+	connectionHandler ConnectionHandlerFunc
+	packetHandler     PacketConnectionHandlerFunc
+	errorHandler      E.Handler
+}
+
+func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
+	myMetadata := w.metadata
+	if metadata.Source.IsValid() {
+		myMetadata.Source = metadata.Source
+	}
+	if metadata.Destination.IsValid() {
+		myMetadata.Destination = metadata.Destination
+	}
+	return w.connectionHandler(ctx, conn, myMetadata)
+}
+
+func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
+	myMetadata := w.metadata
+	if metadata.Source.IsValid() {
+		myMetadata.Source = metadata.Source
+	}
+	if metadata.Destination.IsValid() {
+		myMetadata.Destination = metadata.Destination
+	}
+	return w.packetHandler(ctx, conn, myMetadata)
+}
+
+func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
+	w.errorHandler.NewError(ctx, err)
+}
+
+// Deprecated
+func UpstreamMetadata(metadata InboundContext) M.Metadata {
+	return M.Metadata{
+		Source:      metadata.Source,
+		Destination: metadata.Destination,
+	}
+}
+
+// Deprecated
+type myUpstreamContextHandlerWrapper struct {
+	connectionHandler ConnectionHandlerFunc
+	packetHandler     PacketConnectionHandlerFunc
+	errorHandler      E.Handler
+}
+
+// Deprecated
+func NewUpstreamContextHandler(
+	connectionHandler ConnectionHandlerFunc,
+	packetHandler PacketConnectionHandlerFunc,
+	errorHandler E.Handler,
+) UpstreamHandlerAdapter {
+	return &myUpstreamContextHandlerWrapper{
+		connectionHandler: connectionHandler,
+		packetHandler:     packetHandler,
+		errorHandler:      errorHandler,
+	}
+}
+
+func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
+	myMetadata := ContextFrom(ctx)
+	if metadata.Source.IsValid() {
+		myMetadata.Source = metadata.Source
+	}
+	if metadata.Destination.IsValid() {
+		myMetadata.Destination = metadata.Destination
+	}
+	return w.connectionHandler(ctx, conn, *myMetadata)
+}
+
+func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
+	myMetadata := ContextFrom(ctx)
+	if metadata.Source.IsValid() {
+		myMetadata.Source = metadata.Source
+	}
+	if metadata.Destination.IsValid() {
+		myMetadata.Destination = metadata.Destination
+	}
+	return w.packetHandler(ctx, conn, *myMetadata)
+}
+
+func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
+	w.errorHandler.NewError(ctx, err)
+}
+
+// Deprecated: Use ConnectionRouterEx instead.
+func NewRouteHandler(
+	metadata InboundContext,
+	router ConnectionRouter,
+	logger logger.ContextLogger,
+) UpstreamHandlerAdapter {
+	return &routeHandlerWrapper{
+		metadata: metadata,
+		router:   router,
+		logger:   logger,
+	}
+}
+
+// Deprecated: Use ConnectionRouterEx instead.
+func NewRouteContextHandler(
+	router ConnectionRouter,
+	logger logger.ContextLogger,
+) UpstreamHandlerAdapter {
+	return &routeContextHandlerWrapper{
+		router: router,
+		logger: logger,
+	}
+}
+
+var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
+
+// Deprecated: Use ConnectionRouterEx instead.
+type routeHandlerWrapper struct {
+	metadata InboundContext
+	router   ConnectionRouter
+	logger   logger.ContextLogger
+}
+
+func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
+	myMetadata := w.metadata
+	if metadata.Source.IsValid() {
+		myMetadata.Source = metadata.Source
+	}
+	if metadata.Destination.IsValid() {
+		myMetadata.Destination = metadata.Destination
+	}
+	return w.router.RouteConnection(ctx, conn, myMetadata)
+}
+
+func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
+	myMetadata := w.metadata
+	if metadata.Source.IsValid() {
+		myMetadata.Source = metadata.Source
+	}
+	if metadata.Destination.IsValid() {
+		myMetadata.Destination = metadata.Destination
+	}
+	return w.router.RoutePacketConnection(ctx, conn, myMetadata)
+}
+
+func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
+	w.logger.ErrorContext(ctx, err)
+}
+
+var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
+
+// Deprecated: Use ConnectionRouterEx instead.
+type routeContextHandlerWrapper struct {
+	router ConnectionRouter
+	logger logger.ContextLogger
+}
+
+func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
+	myMetadata := ContextFrom(ctx)
+	if metadata.Source.IsValid() {
+		myMetadata.Source = metadata.Source
+	}
+	if metadata.Destination.IsValid() {
+		myMetadata.Destination = metadata.Destination
+	}
+	return w.router.RouteConnection(ctx, conn, *myMetadata)
+}
+
+func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
+	myMetadata := ContextFrom(ctx)
+	if metadata.Source.IsValid() {
+		myMetadata.Source = metadata.Source
+	}
+	if metadata.Destination.IsValid() {
+		myMetadata.Destination = metadata.Destination
+	}
+	return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
+}
+
+func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
+	w.logger.ErrorContext(ctx, err)
+}

+ 1 - 3
adapter/v2ray.go

@@ -4,7 +4,6 @@ import (
 	"context"
 	"net"
 
-	E "github.com/sagernet/sing/common/exceptions"
 	N "github.com/sagernet/sing/common/network"
 )
 
@@ -16,8 +15,7 @@ type V2RayServerTransport interface {
 }
 
 type V2RayServerTransportHandler interface {
-	N.TCPConnectionHandler
-	E.Handler
+	N.TCPConnectionHandlerEx
 }
 
 type V2RayClientTransport interface {

+ 2 - 2
cmd/sing-box/cmd_rule_set_match.go

@@ -10,7 +10,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-box/route"
+	"github.com/sagernet/sing-box/route/rule"
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
 	"github.com/sagernet/sing/common/json"
@@ -83,7 +83,7 @@ func ruleSetMatch(sourcePath string, domain string) error {
 	}
 	for i, ruleOptions := range plainRuleSet.Rules {
 		var currentRule adapter.HeadlessRule
-		currentRule, err = route.NewHeadlessRule(nil, ruleOptions)
+		currentRule, err = rule.NewHeadlessRule(nil, ruleOptions)
 		if err != nil {
 			return E.Cause(err, "parse rule_set.rules.[", i, "]")
 		}

+ 4 - 4
cmd/sing-box/internal/convertor/adguard/convertor_test.go

@@ -5,7 +5,7 @@ import (
 	"testing"
 
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/route"
+	"github.com/sagernet/sing-box/route/rule"
 
 	"github.com/stretchr/testify/require"
 )
@@ -26,7 +26,7 @@ example.arpa
 `))
 	require.NoError(t, err)
 	require.Len(t, rules, 1)
-	rule, err := route.NewHeadlessRule(nil, rules[0])
+	rule, err := rule.NewHeadlessRule(nil, rules[0])
 	require.NoError(t, err)
 	matchDomain := []string{
 		"example.org",
@@ -85,7 +85,7 @@ func TestHosts(t *testing.T) {
 `))
 	require.NoError(t, err)
 	require.Len(t, rules, 1)
-	rule, err := route.NewHeadlessRule(nil, rules[0])
+	rule, err := rule.NewHeadlessRule(nil, rules[0])
 	require.NoError(t, err)
 	matchDomain := []string{
 		"google.com",
@@ -115,7 +115,7 @@ www.example.org
 `))
 	require.NoError(t, err)
 	require.Len(t, rules, 1)
-	rule, err := route.NewHeadlessRule(nil, rules[0])
+	rule, err := rule.NewHeadlessRule(nil, rules[0])
 	require.NoError(t, err)
 	matchDomain := []string{
 		"example.com",

+ 15 - 2
common/mux/router.go

@@ -15,11 +15,11 @@ import (
 )
 
 type Router struct {
-	router  adapter.ConnectionRouter
+	router  adapter.ConnectionRouterEx
 	service *mux.Service
 }
 
-func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouter, error) {
+func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouterEx, error) {
 	if !options.Enabled {
 		return router, nil
 	}
@@ -54,6 +54,7 @@ func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.Context
 
 func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	if metadata.Destination == mux.Destination {
+		// TODO: check if WithContext is necessary
 		return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata))
 	} else {
 		return r.router.RouteConnection(ctx, conn, metadata)
@@ -63,3 +64,15 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
 func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	return r.router.RoutePacketConnection(ctx, conn, metadata)
 }
+
+func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	if metadata.Destination == mux.Destination {
+		r.service.NewConnectionEx(adapter.WithContext(ctx, &metadata), conn, metadata.Source, metadata.Destination, onClose)
+		return
+	}
+	r.router.RouteConnectionEx(ctx, conn, metadata, onClose)
+}
+
+func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
+}

+ 0 - 32
common/mux/v2ray_legacy.go

@@ -1,32 +0,0 @@
-package mux
-
-import (
-	"context"
-	"net"
-
-	"github.com/sagernet/sing-box/adapter"
-	vmess "github.com/sagernet/sing-vmess"
-	"github.com/sagernet/sing/common/logger"
-	N "github.com/sagernet/sing/common/network"
-)
-
-type V2RayLegacyRouter struct {
-	router adapter.ConnectionRouter
-	logger logger.ContextLogger
-}
-
-func NewV2RayLegacyRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) adapter.ConnectionRouter {
-	return &V2RayLegacyRouter{router, logger}
-}
-
-func (r *V2RayLegacyRouter) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	if metadata.Destination.Fqdn == vmess.MuxDestination.Fqdn {
-		r.logger.InfoContext(ctx, "inbound legacy multiplex connection")
-		return vmess.HandleMuxConnection(ctx, conn, adapter.NewRouteHandler(metadata, r.router, r.logger))
-	}
-	return r.router.RouteConnection(ctx, conn, metadata)
-}
-
-func (r *V2RayLegacyRouter) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return r.router.RoutePacketConnection(ctx, conn, metadata)
-}

+ 1 - 1
common/sniff/sniff.go

@@ -18,7 +18,7 @@ type (
 	PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
 )
 
-func Skip(metadata adapter.InboundContext) bool {
+func Skip(metadata *adapter.InboundContext) bool {
 	// skip server first protocols
 	switch metadata.Destination.Port {
 	case 25, 465, 587:

+ 36 - 3
common/uot/router.go

@@ -13,14 +13,14 @@ import (
 	"github.com/sagernet/sing/common/uot"
 )
 
-var _ adapter.ConnectionRouter = (*Router)(nil)
+var _ adapter.ConnectionRouterEx = (*Router)(nil)
 
 type Router struct {
-	router adapter.ConnectionRouter
+	router adapter.ConnectionRouterEx
 	logger logger.ContextLogger
 }
 
-func NewRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) *Router {
+func NewRouter(router adapter.ConnectionRouterEx, logger logger.ContextLogger) *Router {
 	return &Router{router, logger}
 }
 
@@ -51,3 +51,36 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
 func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	return r.router.RoutePacketConnection(ctx, conn, metadata)
 }
+
+func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	switch metadata.Destination.Fqdn {
+	case uot.MagicAddress:
+		request, err := uot.ReadRequest(conn)
+		if err != nil {
+			err = E.Cause(err, "UoT read request")
+			r.logger.ErrorContext(ctx, "process connection from ", metadata.Source, ": ", err)
+			N.CloseOnHandshakeFailure(conn, onClose, err)
+			return
+		}
+		if request.IsConnect {
+			r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination)
+		} else {
+			r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination)
+		}
+		metadata.Domain = metadata.Destination.Fqdn
+		metadata.Destination = request.Destination
+		r.router.RoutePacketConnectionEx(ctx, uot.NewConn(conn, *request), metadata, onClose)
+		return
+	case uot.LegacyMagicAddress:
+		r.logger.InfoContext(ctx, "inbound legacy UoT connection")
+		metadata.Domain = metadata.Destination.Fqdn
+		metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()}
+		r.RoutePacketConnectionEx(ctx, uot.NewConn(conn, uot.Request{}), metadata, onClose)
+		return
+	}
+	r.router.RouteConnectionEx(ctx, conn, metadata, onClose)
+}
+
+func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
+}

+ 15 - 0
constant/rule.go

@@ -23,3 +23,18 @@ const (
 	RuleSetVersion2
 	RuleSetVersionCurrent = RuleSetVersion2
 )
+
+const (
+	RuleActionTypeRoute     = "route"
+	RuleActionTypeReturn    = "return"
+	RuleActionTypeReject    = "reject"
+	RuleActionTypeHijackDNS = "hijack-dns"
+	RuleActionTypeSniff     = "sniff"
+	RuleActionTypeResolve   = "resolve"
+)
+
+const (
+	RuleActionRejectMethodDefault         = "default"
+	RuleActionRejectMethodPortUnreachable = "port-unreachable"
+	RuleActionRejectMethodDrop            = "drop"
+)

+ 1 - 2
experimental/clashapi/rules.go

@@ -30,10 +30,9 @@ func getRules(router adapter.Router) func(w http.ResponseWriter, r *http.Request
 			rules = append(rules, Rule{
 				Type:    rule.Type(),
 				Payload: rule.String(),
-				Proxy:   rule.Outbound(),
+				Proxy:   rule.Action().String(),
 			})
 		}
-
 		render.JSON(w, r, render.M{
 			"rules": rules,
 		})

+ 20 - 15
experimental/clashapi/trafficontrol/tracker.go

@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	R "github.com/sagernet/sing-box/route/rule"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/atomic"
 	"github.com/sagernet/sing/common/bufio"
@@ -60,7 +61,7 @@ func (t TrackerMetadata) MarshalJSON() ([]byte, error) {
 	}
 	var rule string
 	if t.Rule != nil {
-		rule = F.ToString(t.Rule, " => ", t.Rule.Outbound())
+		rule = F.ToString(t.Rule, " => ", t.Rule.Action())
 	} else {
 		rule = "final"
 	}
@@ -131,19 +132,21 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundCont
 		outbound     string
 		outboundType string
 	)
-	if rule == nil {
-		if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
-			next = defaultOutbound.Tag()
-		}
-	} else {
-		next = rule.Outbound()
+	var action adapter.RuleAction
+	if rule != nil {
+		action = rule.Action()
+	}
+	if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction {
+		next = routeAction.Outbound
+	} else if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
+		next = defaultOutbound.Tag()
 	}
 	for {
-		chain = append(chain, next)
 		detour, loaded := router.Outbound(next)
 		if !loaded {
 			break
 		}
+		chain = append(chain, next)
 		outbound = detour.Tag()
 		outboundType = detour.Type()
 		group, isGroup := detour.(adapter.OutboundGroup)
@@ -218,19 +221,21 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.Inbound
 		outbound     string
 		outboundType string
 	)
-	if rule == nil {
-		if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil {
-			next = defaultOutbound.Tag()
-		}
-	} else {
-		next = rule.Outbound()
+	var action adapter.RuleAction
+	if rule != nil {
+		action = rule.Action()
+	}
+	if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction {
+		next = routeAction.Outbound
+	} else if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil {
+		next = defaultOutbound.Tag()
 	}
 	for {
-		chain = append(chain, next)
 		detour, loaded := router.Outbound(next)
 		if !loaded {
 			break
 		}
+		chain = append(chain, next)
 		outbound = detour.Tag()
 		outboundType = detour.Type()
 		group, isGroup := detour.(adapter.OutboundGroup)

+ 6 - 6
go.mod

@@ -28,14 +28,14 @@ require (
 	github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff
 	github.com/sagernet/quic-go v0.48.2-beta.1
 	github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
-	github.com/sagernet/sing v0.5.1
-	github.com/sagernet/sing-dns v0.3.0
-	github.com/sagernet/sing-mux v0.2.1
-	github.com/sagernet/sing-quic v0.3.2
+	github.com/sagernet/sing v0.6.0-beta.12
+	github.com/sagernet/sing-dns v0.4.0-beta.2
+	github.com/sagernet/sing-mux v0.3.0-alpha.1
+	github.com/sagernet/sing-quic v0.4.0-beta.4
 	github.com/sagernet/sing-shadowsocks v0.2.7
 	github.com/sagernet/sing-shadowsocks2 v0.2.0
-	github.com/sagernet/sing-shadowtls v0.1.5
-	github.com/sagernet/sing-tun v0.4.6
+	github.com/sagernet/sing-shadowtls v0.2.0-alpha.2
+	github.com/sagernet/sing-tun v0.6.0-beta.8
 	github.com/sagernet/sing-vmess v0.1.13
 	github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
 	github.com/sagernet/utls v1.6.7

+ 12 - 12
go.sum

@@ -124,22 +124,22 @@ github.com/sagernet/quic-go v0.48.2-beta.1/go.mod h1:1WgdDIVD1Gybp40JTWketeSfKA/
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
 github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
-github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
-github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
-github.com/sagernet/sing-dns v0.3.0 h1:uHCIlbCwBxALJwXcEK1d75d7t3vzCSVEQsPfZR1cxQE=
-github.com/sagernet/sing-dns v0.3.0/go.mod h1:TqLIelI+FAbVEdiTRolhGLOwvhVjY7oT+wezlOJUQ7M=
-github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
-github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
-github.com/sagernet/sing-quic v0.3.2 h1:bFawJFX4KPx0teFfH1toQm5MfDVz/RwsMbLueeiD2l8=
-github.com/sagernet/sing-quic v0.3.2/go.mod h1:g8b5Fj88KRM0H9lpKAxJj0EpkL/Yk06qXJAG7FuZd2I=
+github.com/sagernet/sing v0.6.0-beta.12 h1:2DnTJcvypK3/PM/8JjmgG8wVK48gdcpRwU98c4J/a7s=
+github.com/sagernet/sing v0.6.0-beta.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
+github.com/sagernet/sing-dns v0.4.0-beta.2 h1:HW94bUEp7K/vf5DlYz646LTZevQtJ0250jZa/UZRlbY=
+github.com/sagernet/sing-dns v0.4.0-beta.2/go.mod h1:8wuFcoFkWM4vJuQyg8e97LyvDwe0/Vl7G839WLcKDs8=
+github.com/sagernet/sing-mux v0.3.0-alpha.1 h1:IgNX5bJBpL41gGbp05pdDOvh/b5eUQ6cv9240+Ngipg=
+github.com/sagernet/sing-mux v0.3.0-alpha.1/go.mod h1:FTcImmdfW38Lz7b+HQ+mxxOth1lz4ao8uEnz+MwIJQE=
+github.com/sagernet/sing-quic v0.4.0-beta.4 h1:kKiMLGaxvVLDCSvCMYo4PtWd1xU6FTL7xvUAQfXO09g=
+github.com/sagernet/sing-quic v0.4.0-beta.4/go.mod h1:1UNObFodd8CnS3aCT53x9cigjPSCl3P//8dfBMCwBDM=
 github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
 github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
 github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
 github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
-github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
-github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
-github.com/sagernet/sing-tun v0.4.6 h1:TBBr3akUqC0o6Vrwet9D56IDHylfM67hYJkVAfdlvCQ=
-github.com/sagernet/sing-tun v0.4.6/go.mod h1:1WQVMelJQjrtlzhzHwwPTSa7n41b3zSWP2DeJqWxruk=
+github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 h1:RPrpgAdkP5td0vLfS5ldvYosFjSsZtRPxiyLV6jyKg0=
+github.com/sagernet/sing-shadowtls v0.2.0-alpha.2/go.mod h1:0j5XlzKxaWRIEjc1uiSKmVoWb0k+L9QgZVb876+thZA=
+github.com/sagernet/sing-tun v0.6.0-beta.8 h1:GFNt/w8r1v30zC/hfCytk8C9+N/f1DfvosFXJkyJlrw=
+github.com/sagernet/sing-tun v0.6.0-beta.8/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
 github.com/sagernet/sing-vmess v0.1.13 h1:/GSfD1Rt6/mVfE80WFHNBykNT7KJNWWmvcMP9DoElEs=
 github.com/sagernet/sing-vmess v0.1.13/go.mod h1:D+g+lhv4iOk1Pn08pd3MtUd72khL5+wgEE3xVH8J+ow=
 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=

+ 32 - 19
inbound/default.go

@@ -22,13 +22,13 @@ type myInboundAdapter struct {
 	protocol         string
 	network          []string
 	ctx              context.Context
-	router           adapter.ConnectionRouter
+	router           adapter.ConnectionRouterEx
 	logger           log.ContextLogger
 	tag              string
 	listenOptions    option.ListenOptions
-	connHandler      adapter.ConnectionHandler
-	packetHandler    adapter.PacketHandler
-	oobPacketHandler adapter.OOBPacketHandler
+	connHandler      adapter.ConnectionHandlerEx
+	packetHandler    adapter.PacketHandlerEx
+	oobPacketHandler adapter.OOBPacketHandlerEx
 	packetUpstream   any
 
 	// http mixed
@@ -55,10 +55,6 @@ func (a *myInboundAdapter) Tag() string {
 	return a.tag
 }
 
-func (a *myInboundAdapter) Network() []string {
-	return a.network
-}
-
 func (a *myInboundAdapter) Start() error {
 	var err error
 	if common.Contains(a.network, N.NetworkTCP) {
@@ -150,6 +146,31 @@ func (a *myInboundAdapter) newPacketConnection(ctx context.Context, conn N.Packe
 	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
@@ -167,25 +188,17 @@ func (a *myInboundAdapter) createMetadata(conn net.Conn, metadata adapter.Inboun
 	return metadata
 }
 
-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()
-	}
-	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) {

+ 7 - 11
inbound/default_tcp.go

@@ -71,18 +71,14 @@ func (a *myInboundAdapter) injectTCP(conn net.Conn, metadata adapter.InboundCont
 	ctx := log.ContextWithNewID(a.ctx)
 	metadata = a.createMetadata(conn, metadata)
 	a.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
-	hErr := a.connHandler.NewConnection(ctx, conn, metadata)
-	if hErr != nil {
-		conn.Close()
-		a.NewError(ctx, E.Cause(hErr, "process connection from ", metadata.Source))
-	}
+	a.connHandler.NewConnectionEx(ctx, conn, metadata, nil)
 }
 
-func (a *myInboundAdapter) routeTCP(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) {
+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)
-	hErr := a.newConnection(ctx, conn, metadata)
-	if hErr != nil {
-		conn.Close()
-		a.NewError(ctx, E.Cause(hErr, "process connection from ", metadata.Source))
-	}
+	a.connHandler.NewConnectionEx(ctx, conn, metadata, onClose)
 }

+ 33 - 54
inbound/default_udp.go

@@ -42,7 +42,6 @@ func (a *myInboundAdapter) loopUDPIn() {
 	defer buffer.Release()
 	buffer.IncRef()
 	defer buffer.DecRef()
-	packetService := (*myInboundPacketAdapter)(a)
 	for {
 		buffer.Reset()
 		n, addr, err := a.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes())
@@ -50,16 +49,7 @@ func (a *myInboundAdapter) loopUDPIn() {
 			return
 		}
 		buffer.Truncate(n)
-		var metadata adapter.InboundContext
-		metadata.Inbound = a.tag
-		metadata.InboundType = a.protocol
-		metadata.InboundOptions = a.listenOptions.InboundOptions
-		metadata.Source = M.SocksaddrFromNetIP(addr).Unwrap()
-		metadata.OriginDestination = a.udpAddr
-		err = a.packetHandler.NewPacket(a.ctx, packetService, buffer, metadata)
-		if err != nil {
-			a.newError(E.Cause(err, "process packet from ", metadata.Source))
-		}
+		a.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap())
 	}
 }
 
@@ -69,7 +59,6 @@ func (a *myInboundAdapter) loopUDPOOBIn() {
 	defer buffer.Release()
 	buffer.IncRef()
 	defer buffer.DecRef()
-	packetService := (*myInboundPacketAdapter)(a)
 	oob := make([]byte, 1024)
 	for {
 		buffer.Reset()
@@ -78,22 +67,12 @@ func (a *myInboundAdapter) loopUDPOOBIn() {
 			return
 		}
 		buffer.Truncate(n)
-		var metadata adapter.InboundContext
-		metadata.Inbound = a.tag
-		metadata.InboundType = a.protocol
-		metadata.InboundOptions = a.listenOptions.InboundOptions
-		metadata.Source = M.SocksaddrFromNetIP(addr).Unwrap()
-		metadata.OriginDestination = a.udpAddr
-		err = a.oobPacketHandler.NewPacket(a.ctx, packetService, buffer, oob[:oobN], metadata)
-		if err != nil {
-			a.newError(E.Cause(err, "process packet from ", metadata.Source))
-		}
+		a.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap())
 	}
 }
 
 func (a *myInboundAdapter) loopUDPInThreadSafe() {
 	defer close(a.packetOutboundClosed)
-	packetService := (*myInboundPacketAdapter)(a)
 	for {
 		buffer := buf.NewPacket()
 		n, addr, err := a.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes())
@@ -102,23 +81,12 @@ func (a *myInboundAdapter) loopUDPInThreadSafe() {
 			return
 		}
 		buffer.Truncate(n)
-		var metadata adapter.InboundContext
-		metadata.Inbound = a.tag
-		metadata.InboundType = a.protocol
-		metadata.InboundOptions = a.listenOptions.InboundOptions
-		metadata.Source = M.SocksaddrFromNetIP(addr).Unwrap()
-		metadata.OriginDestination = a.udpAddr
-		err = a.packetHandler.NewPacket(a.ctx, packetService, buffer, metadata)
-		if err != nil {
-			buffer.Release()
-			a.newError(E.Cause(err, "process packet from ", metadata.Source))
-		}
+		a.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap())
 	}
 }
 
 func (a *myInboundAdapter) loopUDPOOBInThreadSafe() {
 	defer close(a.packetOutboundClosed)
-	packetService := (*myInboundPacketAdapter)(a)
 	oob := make([]byte, 1024)
 	for {
 		buffer := buf.NewPacket()
@@ -128,17 +96,7 @@ func (a *myInboundAdapter) loopUDPOOBInThreadSafe() {
 			return
 		}
 		buffer.Truncate(n)
-		var metadata adapter.InboundContext
-		metadata.Inbound = a.tag
-		metadata.InboundType = a.protocol
-		metadata.InboundOptions = a.listenOptions.InboundOptions
-		metadata.Source = M.SocksaddrFromNetIP(addr).Unwrap()
-		metadata.OriginDestination = a.udpAddr
-		err = a.oobPacketHandler.NewPacket(a.ctx, packetService, buffer, oob[:oobN], metadata)
-		if err != nil {
-			buffer.Release()
-			a.newError(E.Cause(err, "process packet from ", metadata.Source))
-		}
+		a.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap())
 	}
 }
 
@@ -148,7 +106,7 @@ func (a *myInboundAdapter) loopUDPOut() {
 		case packet := <-a.packetOutbound:
 			err := a.writePacket(packet.buffer, packet.destination)
 			if err != nil && !E.IsClosed(err) {
-				a.newError(E.New("write back udp: ", err))
+				a.logger.Error(E.New("write back udp: ", err))
 			}
 			continue
 		case <-a.packetOutboundClosed:
@@ -164,15 +122,36 @@ func (a *myInboundAdapter) loopUDPOut() {
 	}
 }
 
+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()
-	if destination.IsFqdn() {
-		udpAddr, err := net.ResolveUDPAddr(N.NetworkUDP, destination.String())
-		if err != nil {
-			return err
-		}
-		return common.Error(a.udpConn.WriteTo(buffer.Bytes(), udpAddr))
-	}
 	return common.Error(a.udpConn.WriteToUDPAddrPort(buffer.Bytes(), destination.AddrPort()))
 }
 

+ 28 - 21
inbound/direct.go

@@ -3,7 +3,6 @@ package inbound
 import (
 	"context"
 	"net"
-	"net/netip"
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
@@ -13,14 +12,14 @@ import (
 	"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/udpnat"
+	"github.com/sagernet/sing/common/udpnat2"
 )
 
 var _ adapter.Inbound = (*Direct)(nil)
 
 type Direct struct {
 	myInboundAdapter
-	udpNat              *udpnat.Service[netip.AddrPort]
+	udpNat              *udpnat.Service
 	overrideOption      int
 	overrideDestination M.Socksaddr
 }
@@ -54,10 +53,9 @@ func NewDirect(ctx context.Context, router adapter.Router, logger log.ContextLog
 	} else {
 		udpTimeout = C.UDPTimeout
 	}
-	inbound.udpNat = udpnat.New[netip.AddrPort](int64(udpTimeout.Seconds()), adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound))
+	inbound.udpNat = udpnat.New(inbound, inbound.preparePacketConnection, udpTimeout, false)
 	inbound.connHandler = inbound
 	inbound.packetHandler = inbound
-	inbound.packetUpstream = inbound.udpNat
 	return inbound
 }
 
@@ -76,29 +74,38 @@ func (d *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adap
 	return d.router.RouteConnection(ctx, conn, metadata)
 }
 
-func (d *Direct) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error {
+func (d *Direct) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) {
+	var destination M.Socksaddr
 	switch d.overrideOption {
 	case 1:
-		metadata.Destination = d.overrideDestination
+		destination = d.overrideDestination
 	case 2:
-		destination := d.overrideDestination
-		destination.Port = metadata.Destination.Port
-		metadata.Destination = destination
+		destination = d.overrideDestination
+		destination.Port = source.Port
 	case 3:
-		metadata.Destination.Port = d.overrideDestination.Port
+		destination = source
+		destination.Port = d.overrideDestination.Port
 	}
-	d.udpNat.NewContextPacket(ctx, metadata.Source.AddrPort(), buffer, adapter.UpstreamMetadata(metadata), func(natConn N.PacketConn) (context.Context, N.PacketWriter) {
-		return adapter.WithContext(log.ContextWithNewID(ctx), &metadata), &udpnat.DirectBackWriter{Source: conn, Nat: natConn}
-	})
-	return nil
+	d.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, destination, nil)
 }
 
-func (d *Direct) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return d.router.RouteConnection(ctx, conn, metadata)
+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 (d *Direct) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	ctx = log.ContextWithNewID(ctx)
-	d.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
-	return d.router.RoutePacketConnection(ctx, conn, metadata)
+func (w *directPacketWriter) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error {
+	return w.writer.WritePacket(buffer, w.source)
 }

+ 22 - 17
inbound/http.go

@@ -4,7 +4,6 @@ import (
 	std_bufio "bufio"
 	"context"
 	"net"
-	"os"
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/tls"
@@ -20,8 +19,8 @@ import (
 )
 
 var (
-	_ adapter.Inbound           = (*HTTP)(nil)
-	_ adapter.InjectableInbound = (*HTTP)(nil)
+	_ adapter.Inbound              = (*HTTP)(nil)
+	_ adapter.TCPInjectableInbound = (*HTTP)(nil)
 )
 
 type HTTP struct {
@@ -72,7 +71,15 @@ func (h *HTTP) Close() error {
 	)
 }
 
-func (h *HTTP) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+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)
@@ -80,35 +87,33 @@ func (h *HTTP) NewConnection(ctx context.Context, conn net.Conn, metadata adapte
 			return err
 		}
 	}
-	return http.HandleConnection(ctx, conn, std_bufio.NewReader(conn), h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata))
-}
-
-func (h *HTTP) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return os.ErrInvalid
+	return http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, onClose)
 }
 
-func (a *myInboundAdapter) upstreamUserHandler(metadata adapter.InboundContext) adapter.UpstreamHandlerAdapter {
-	return adapter.NewUpstreamHandler(metadata, a.newUserConnection, a.streamUserPacketConnection, a)
+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) error {
+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)
-		return a.router.RouteConnection(ctx, conn, metadata)
+		a.router.RouteConnectionEx(ctx, conn, metadata, onClose)
+		return
 	}
 	metadata.User = user
 	a.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
-	return a.router.RouteConnection(ctx, conn, metadata)
+	a.router.RouteConnectionEx(ctx, conn, metadata, onClose)
 }
 
-func (a *myInboundAdapter) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+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)
-		return a.router.RoutePacketConnection(ctx, conn, metadata)
+		a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
+		return
 	}
 	metadata.User = user
 	a.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
-	return a.router.RoutePacketConnection(ctx, conn, metadata)
+	a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
 }

+ 15 - 11
inbound/mixed.go

@@ -4,7 +4,6 @@ import (
 	std_bufio "bufio"
 	"context"
 	"net"
-	"os"
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/uot"
@@ -12,6 +11,7 @@ import (
 	"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"
@@ -20,8 +20,8 @@ import (
 )
 
 var (
-	_ adapter.Inbound           = (*Mixed)(nil)
-	_ adapter.InjectableInbound = (*Mixed)(nil)
+	_ adapter.Inbound              = (*Mixed)(nil)
+	_ adapter.TCPInjectableInbound = (*Mixed)(nil)
 )
 
 type Mixed struct {
@@ -47,20 +47,24 @@ func NewMixed(ctx context.Context, router adapter.Router, logger log.ContextLogg
 	return inbound
 }
 
-func (h *Mixed) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+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 err
+		return E.Cause(err, "peek first byte")
 	}
 	switch headerBytes[0] {
 	case socks4.Version, socks5.Version:
-		return socks.HandleConnection0(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata))
+		return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, metadata.Destination, onClose)
 	default:
-		return http.HandleConnection(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata))
+		return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, onClose)
 	}
 }
-
-func (h *Mixed) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return os.ErrInvalid
-}

+ 17 - 9
inbound/naive.go

@@ -17,6 +17,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/transport/v2rayhttp"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/auth"
 	"github.com/sagernet/sing/common/buf"
@@ -164,13 +165,13 @@ func (n *Naive) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
 			n.badRequest(ctx, request, E.New("hijack failed"))
 			return
 		}
-		n.newConnection(ctx, &naiveH1Conn{Conn: conn}, userName, source, destination)
+		n.newConnection(ctx, false, &naiveH1Conn{Conn: conn}, userName, source, destination)
 	} else {
-		n.newConnection(ctx, &naiveH2Conn{reader: request.Body, writer: writer, flusher: writer.(http.Flusher)}, userName, source, destination)
+		n.newConnection(ctx, true, &naiveH2Conn{reader: request.Body, writer: writer, flusher: writer.(http.Flusher)}, userName, source, destination)
 	}
 }
 
-func (n *Naive) newConnection(ctx context.Context, conn net.Conn, userName string, source, destination M.Socksaddr) {
+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)
@@ -178,19 +179,26 @@ func (n *Naive) newConnection(ctx context.Context, conn net.Conn, userName strin
 		n.logger.InfoContext(ctx, "inbound connection from ", source)
 		n.logger.InfoContext(ctx, "inbound connection to ", destination)
 	}
-	hErr := n.router.RouteConnection(ctx, conn, n.createMetadata(conn, adapter.InboundContext{
+	metadata := n.createMetadata(conn, adapter.InboundContext{
 		Source:      source,
 		Destination: destination,
 		User:        userName,
-	}))
-	if hErr != nil {
-		conn.Close()
-		n.NewError(ctx, E.Cause(hErr, "process connection from ", source))
+	})
+	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.NewError(ctx, E.Cause(err, "process connection from ", request.RemoteAddr))
+	n.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr))
 }
 
 func rejectHTTP(writer http.ResponseWriter, statusCode int) {

+ 5 - 4
inbound/redirect.go

@@ -9,7 +9,6 @@ import (
 	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"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
@@ -34,11 +33,13 @@ func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextL
 	return redirect
 }
 
-func (r *Redirect) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (r *Redirect) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
 	destination, err := redir.GetOriginalDestination(conn)
 	if err != nil {
-		return E.Cause(err, "get redirect destination")
+		conn.Close()
+		r.logger.ErrorContext(ctx, "process connection from ", conn.RemoteAddr(), ": get redirect destination: ", err)
+		return
 	}
 	metadata.Destination = M.SocksaddrFromNetIP(destination)
-	return r.newConnection(ctx, conn, metadata)
+	r.newConnectionEx(ctx, conn, metadata, onClose)
 }

+ 27 - 12
inbound/shadowsocks.go

@@ -3,7 +3,6 @@ package inbound
 import (
 	"context"
 	"net"
-	"os"
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
@@ -18,6 +17,7 @@ import (
 	"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"
 )
@@ -36,8 +36,8 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 }
 
 var (
-	_ adapter.Inbound           = (*Shadowsocks)(nil)
-	_ adapter.InjectableInbound = (*Shadowsocks)(nil)
+	_ adapter.Inbound              = (*Shadowsocks)(nil)
+	_ adapter.TCPInjectableInbound = (*Shadowsocks)(nil)
 )
 
 type Shadowsocks struct {
@@ -74,11 +74,11 @@ func newShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 	}
 	switch {
 	case options.Method == shadowsocks.MethodNone:
-		inbound.service = shadowsocks.NewNoneService(int64(udpTimeout.Seconds()), inbound.upstreamContextHandler())
+		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()), inbound.upstreamContextHandler())
+		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()), inbound.upstreamContextHandler(), ntp.TimeFuncFromContext(ctx))
+		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)
 	}
@@ -86,14 +86,29 @@ func newShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 	return inbound, err
 }
 
-func (h *Shadowsocks) 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 *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) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error {
-	return h.service.NewPacket(adapter.WithContext(ctx, &metadata), conn, buffer, adapter.UpstreamMetadata(metadata))
+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 {
-	return os.ErrInvalid
+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))
 }

+ 19 - 14
inbound/shadowsocks_multi.go

@@ -20,13 +20,14 @@ import (
 	"github.com/sagernet/sing/common/buf"
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
+	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.InjectableInbound = (*ShadowsocksMulti)(nil)
+	_ adapter.Inbound              = (*ShadowsocksMulti)(nil)
+	_ adapter.TCPInjectableInbound = (*ShadowsocksMulti)(nil)
 )
 
 type ShadowsocksMulti struct {
@@ -66,14 +67,15 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.
 			options.Method,
 			options.Password,
 			int64(udpTimeout.Seconds()),
-			adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound),
+			adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound),
 			ntp.TimeFuncFromContext(ctx),
 		)
 	} else if common.Contains(shadowaead.List, options.Method) {
 		service, err = shadowaead.NewMultiService[int](
 			options.Method,
 			int64(udpTimeout.Seconds()),
-			adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound))
+			adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound),
+		)
 	} else {
 		return nil, E.New("unsupported method: " + options.Method)
 	}
@@ -94,16 +96,19 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.
 	return inbound, err
 }
 
-func (h *ShadowsocksMulti) 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 *ShadowsocksMulti) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error {
-	return h.service.NewPacket(adapter.WithContext(ctx, &metadata), conn, buffer, adapter.UpstreamMetadata(metadata))
+func (h *ShadowsocksMulti) 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 *ShadowsocksMulti) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return os.ErrInvalid
+func (h *ShadowsocksMulti) 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 *ShadowsocksMulti) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
@@ -118,7 +123,7 @@ 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, metadata)
+	return h.router.RouteConnection(ctx, conn, h.createMetadata(conn, metadata))
 }
 
 func (h *ShadowsocksMulti) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
@@ -135,5 +140,5 @@ 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, metadata)
+	return h.router.RoutePacketConnection(ctx, conn, h.createPacketMetadata(conn, metadata))
 }

+ 18 - 13
inbound/shadowsocks_relay.go

@@ -16,13 +16,15 @@ import (
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/auth"
 	"github.com/sagernet/sing/common/buf"
+	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
+	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
 
 var (
-	_ adapter.Inbound           = (*ShadowsocksRelay)(nil)
-	_ adapter.InjectableInbound = (*ShadowsocksRelay)(nil)
+	_ adapter.Inbound              = (*ShadowsocksRelay)(nil)
+	_ adapter.TCPInjectableInbound = (*ShadowsocksRelay)(nil)
 )
 
 type ShadowsocksRelay struct {
@@ -61,7 +63,7 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log.
 		options.Method,
 		options.Password,
 		int64(udpTimeout.Seconds()),
-		adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound),
+		adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound),
 	)
 	if err != nil {
 		return nil, err
@@ -79,16 +81,19 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log.
 	return inbound, err
 }
 
-func (h *ShadowsocksRelay) 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 *ShadowsocksRelay) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error {
-	return h.service.NewPacket(adapter.WithContext(ctx, &metadata), conn, buffer, adapter.UpstreamMetadata(metadata))
+func (h *ShadowsocksRelay) 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 *ShadowsocksRelay) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return os.ErrInvalid
+func (h *ShadowsocksRelay) 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 *ShadowsocksRelay) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
@@ -103,7 +108,7 @@ 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, metadata)
+	return h.router.RouteConnection(ctx, conn, h.createMetadata(conn, metadata))
 }
 
 func (h *ShadowsocksRelay) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
@@ -120,5 +125,5 @@ 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, metadata)
+	return h.router.RoutePacketConnection(ctx, conn, h.createPacketMetadata(conn, metadata))
 }

+ 9 - 0
inbound/shadowtls.go

@@ -12,6 +12,7 @@ import (
 	"github.com/sagernet/sing-shadowtls"
 	"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"
 )
 
@@ -91,3 +92,11 @@ 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) {
+	err := h.NewConnection(ctx, conn, metadata)
+	N.CloseOnHandshakeFailure(conn, onClose, err)
+	if err != nil {
+		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+	}
+}

+ 10 - 9
inbound/socks.go

@@ -1,9 +1,9 @@
 package inbound
 
 import (
+	std_bufio "bufio"
 	"context"
 	"net"
-	"os"
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/uot"
@@ -11,13 +11,14 @@ import (
 	"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.InjectableInbound = (*Socks)(nil)
+	_ adapter.Inbound              = (*Socks)(nil)
+	_ adapter.TCPInjectableInbound = (*Socks)(nil)
 )
 
 type Socks struct {
@@ -42,10 +43,10 @@ func NewSocks(ctx context.Context, router adapter.Router, logger log.ContextLogg
 	return inbound
 }
 
-func (h *Socks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return socks.HandleConnection(ctx, conn, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata))
-}
-
-func (h *Socks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return os.ErrInvalid
+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))
+	}
 }

+ 23 - 20
inbound/tproxy.go

@@ -18,12 +18,12 @@ import (
 	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/udpnat"
+	"github.com/sagernet/sing/common/udpnat2"
 )
 
 type TProxy struct {
 	myInboundAdapter
-	udpNat *udpnat.Service[netip.AddrPort]
+	udpNat *udpnat.Service
 }
 
 func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TProxyInboundOptions) *TProxy {
@@ -46,8 +46,7 @@ func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLog
 	}
 	tproxy.connHandler = tproxy
 	tproxy.oobPacketHandler = tproxy
-	tproxy.udpNat = udpnat.New[netip.AddrPort](int64(udpTimeout.Seconds()), tproxy.upstreamContextHandler())
-	tproxy.packetUpstream = tproxy.udpNat
+	tproxy.udpNat = udpnat.New(tproxy, tproxy.preparePacketConnection, udpTimeout, false)
 	return tproxy
 }
 
@@ -75,35 +74,43 @@ func (t *TProxy) Start() error {
 	return nil
 }
 
-func (t *TProxy) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+func (t *TProxy) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
 	metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
-	return t.newConnection(ctx, conn, metadata)
+	t.newConnectionEx(ctx, conn, metadata, onClose)
 }
 
-func (t *TProxy) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata adapter.InboundContext) error {
+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)
+}
+
+func (t *TProxy) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) {
 	destination, err := redir.GetOriginalDestinationFromOOB(oob)
 	if err != nil {
-		return E.Cause(err, "get tproxy destination")
+		t.logger.Warn("process packet from ", source, ": get tproxy destination: ", err)
+		return
 	}
-	metadata.Destination = M.SocksaddrFromNetIP(destination).Unwrap()
-	t.udpNat.NewContextPacket(ctx, metadata.Source.AddrPort(), buffer, adapter.UpstreamMetadata(metadata), func(natConn N.PacketConn) (context.Context, N.PacketWriter) {
-		return adapter.WithContext(log.ContextWithNewID(ctx), &metadata), &tproxyPacketWriter{ctx: ctx, source: natConn, destination: metadata.Destination}
-	})
-	return nil
+	t.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, M.SocksaddrFromNetIP(destination), nil)
 }
 
 type tproxyPacketWriter struct {
 	ctx         context.Context
-	source      N.PacketConn
+	source      netip.AddrPort
 	destination M.Socksaddr
 	conn        *net.UDPConn
 }
 
+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) {
+		common.Close(common.PtrOrNil(writer.conn))
+	}
+}
+
 func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
 	defer buffer.Release()
 	conn := w.conn
 	if w.destination == destination && conn != nil {
-		_, err := conn.WriteToUDPAddrPort(buffer.Bytes(), M.AddrPortFromNet(w.source.LocalAddr()))
+		_, err := conn.WriteToUDPAddrPort(buffer.Bytes(), w.source)
 		if err != nil {
 			w.conn = nil
 		}
@@ -122,9 +129,5 @@ func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socks
 	} else {
 		defer udpConn.Close()
 	}
-	return common.Error(udpConn.WriteToUDPAddrPort(buffer.Bytes(), M.AddrPortFromNet(w.source.LocalAddr())))
-}
-
-func (w *tproxyPacketWriter) Close() error {
-	return common.Close(common.PtrOrNil(w.conn))
+	return common.Error(udpConn.WriteToUDPAddrPort(buffer.Bytes(), w.source))
 }

+ 11 - 15
inbound/trojan.go

@@ -22,8 +22,8 @@ import (
 )
 
 var (
-	_ adapter.Inbound           = (*Trojan)(nil)
-	_ adapter.InjectableInbound = (*Trojan)(nil)
+	_ adapter.Inbound              = (*Trojan)(nil)
+	_ adapter.TCPInjectableInbound = (*Trojan)(nil)
 )
 
 type Trojan struct {
@@ -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, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*trojanTransportHandler)(inbound))
+		inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*trojanTransportHandler)(inbound))
 		if err != nil {
 			return nil, E.Cause(err, "create server transport: ", options.Transport.Type)
 		}
@@ -149,11 +149,6 @@ func (h *Trojan) Close() error {
 	)
 }
 
-func (h *Trojan) newTransportConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	h.injectTCP(conn, metadata)
-	return nil
-}
-
 func (h *Trojan) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	var err error
 	if h.tlsConfig != nil && h.transport == nil {
@@ -165,8 +160,12 @@ 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) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return os.ErrInvalid
+func (h *Trojan) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	err := h.NewConnection(ctx, conn, metadata)
+	N.CloseOnHandshakeFailure(conn, onClose, err)
+	if err != nil {
+		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+	}
 }
 
 func (h *Trojan) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
@@ -226,9 +225,6 @@ var _ adapter.V2RayServerTransportHandler = (*trojanTransportHandler)(nil)
 
 type trojanTransportHandler Trojan
 
-func (t *trojanTransportHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
-	return (*Trojan)(t).newTransportConnection(ctx, conn, adapter.InboundContext{
-		Source:      metadata.Source,
-		Destination: metadata.Destination,
-	})
+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)
 }

+ 46 - 42
inbound/tun.go

@@ -28,17 +28,18 @@ import (
 	"go4.org/netipx"
 )
 
-var _ adapter.Inbound = (*Tun)(nil)
+var _ adapter.Inbound = (*TUN)(nil)
 
-type Tun struct {
-	tag                         string
-	ctx                         context.Context
-	router                      adapter.Router
-	logger                      log.ContextLogger
+type TUN struct {
+	tag    string
+	ctx    context.Context
+	router adapter.Router
+	logger log.ContextLogger
+	// Deprecated
 	inboundOptions              option.InboundOptions
 	tunOptions                  tun.Options
 	endpointIndependentNat      bool
-	udpTimeout                  int64
+	udpTimeout                  time.Duration
 	stack                       string
 	tunIf                       tun.Tun
 	tunStack                    tun.Stack
@@ -53,7 +54,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 NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*TUN, error) {
 	address := options.Address
 	var deprecatedAddressUsed bool
 	//nolint:staticcheck
@@ -162,7 +163,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
 		outputMark = tun.DefaultAutoRedirectOutputMark
 	}
 
-	inbound := &Tun{
+	inbound := &TUN{
 		tag:            tag,
 		ctx:            ctx,
 		router:         router,
@@ -194,7 +195,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
 			InterfaceMonitor:         router.InterfaceMonitor(),
 		},
 		endpointIndependentNat: options.EndpointIndependentNat,
-		udpTimeout:             int64(udpTimeout.Seconds()),
+		udpTimeout:             udpTimeout,
 		stack:                  options.Stack,
 		platformInterface:      platformInterface,
 		platformOptions:        common.PtrValueOrDefault(options.Platform),
@@ -207,7 +208,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
 		inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{
 			TunOptions:             &inbound.tunOptions,
 			Context:                ctx,
-			Handler:                inbound,
+			Handler:                (*autoRedirectHandler)(inbound),
 			Logger:                 logger,
 			NetworkMonitor:         router.NetworkMonitor(),
 			InterfaceFinder:        router.InterfaceFinder(),
@@ -283,17 +284,17 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges.
 	return uidRanges, nil
 }
 
-func (t *Tun) Type() string {
+func (t *TUN) Type() string {
 	return C.TypeTun
 }
 
-func (t *Tun) Tag() string {
+func (t *TUN) Tag() string {
 	return t.tag
 }
 
-func (t *Tun) Start() error {
+func (t *TUN) Start() error {
 	if C.IsAndroid && t.platformInterface == nil {
-		t.tunOptions.BuildAndroidRules(t.router.PackageManager(), t)
+		t.tunOptions.BuildAndroidRules(t.router.PackageManager())
 	}
 	if t.tunOptions.Name == "" {
 		t.tunOptions.Name = tun.CalculateInterfaceName("")
@@ -327,7 +328,6 @@ func (t *Tun) Start() error {
 		Context:                t.ctx,
 		Tun:                    tunInterface,
 		TunOptions:             t.tunOptions,
-		EndpointIndependentNat: t.endpointIndependentNat,
 		UDPTimeout:             t.udpTimeout,
 		Handler:                t,
 		Logger:                 t.logger,
@@ -349,7 +349,7 @@ func (t *Tun) Start() error {
 	return nil
 }
 
-func (t *Tun) PostStart() error {
+func (t *TUN) PostStart() error {
 	monitor := taskmonitor.New(t.logger, C.StartTimeout)
 	if t.autoRedirect != nil {
 		t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet)
@@ -388,7 +388,7 @@ func (t *Tun) PostStart() error {
 	return nil
 }
 
-func (t *Tun) updateRouteAddressSet(it adapter.RuleSet) {
+func (t *TUN) 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()
@@ -396,7 +396,7 @@ func (t *Tun) updateRouteAddressSet(it adapter.RuleSet) {
 	t.routeExcludeAddressSet = nil
 }
 
-func (t *Tun) Close() error {
+func (t *TUN) Close() error {
 	return common.Close(
 		t.tunStack,
 		t.tunIf,
@@ -404,44 +404,48 @@ func (t *Tun) Close() error {
 	)
 }
 
-func (t *Tun) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata M.Metadata) error {
+func (t *TUN) PrepareConnection(source M.Socksaddr, destination M.Socksaddr) error {
+	// TODO: implement rejects
+	return nil
+}
+
+func (t *TUN) 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
 	metadata.InboundType = C.TypeTun
-	metadata.Source = upstreamMetadata.Source
-	metadata.Destination = upstreamMetadata.Destination
+	metadata.Source = source
+	metadata.Destination = destination
 	metadata.InboundOptions = t.inboundOptions
-	if upstreamMetadata.Protocol != "" {
-		t.logger.InfoContext(ctx, "inbound ", upstreamMetadata.Protocol, " connection from ", metadata.Source)
-	} else {
-		t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
-	}
+	t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
 	t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
-	err := t.router.RouteConnection(ctx, conn, metadata)
-	if err != nil {
-		t.NewError(ctx, err)
-	}
-	return nil
+	t.router.RouteConnectionEx(ctx, conn, metadata, onClose)
 }
 
-func (t *Tun) NewPacketConnection(ctx context.Context, conn N.PacketConn, upstreamMetadata M.Metadata) error {
+func (t *TUN) 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
 	metadata.InboundType = C.TypeTun
-	metadata.Source = upstreamMetadata.Source
-	metadata.Destination = upstreamMetadata.Destination
+	metadata.Source = source
+	metadata.Destination = destination
 	metadata.InboundOptions = t.inboundOptions
 	t.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
 	t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
-	err := t.router.RoutePacketConnection(ctx, conn, metadata)
-	if err != nil {
-		t.NewError(ctx, err)
-	}
-	return nil
+	t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
 }
 
-func (t *Tun) NewError(ctx context.Context, err error) {
-	NewError(t.logger, ctx, err)
+type autoRedirectHandler TUN
+
+func (t *autoRedirectHandler) 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
+	metadata.InboundType = C.TypeTun
+	metadata.Source = source
+	metadata.Destination = destination
+	metadata.InboundOptions = t.inboundOptions
+	t.logger.InfoContext(ctx, "inbound redirect connection from ", metadata.Source)
+	t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+	t.router.RouteConnectionEx(ctx, conn, metadata, onClose)
 }

+ 11 - 15
inbound/vless.go

@@ -25,8 +25,8 @@ import (
 )
 
 var (
-	_ adapter.Inbound           = (*VLESS)(nil)
-	_ adapter.InjectableInbound = (*VLESS)(nil)
+	_ adapter.Inbound              = (*VLESS)(nil)
+	_ adapter.TCPInjectableInbound = (*VLESS)(nil)
 )
 
 type VLESS struct {
@@ -73,7 +73,7 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
 		}
 	}
 	if options.Transport != nil {
-		inbound.transport, err = v2ray.NewServerTransport(ctx, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vlessTransportHandler)(inbound))
+		inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vlessTransportHandler)(inbound))
 		if err != nil {
 			return nil, E.Cause(err, "create server transport: ", options.Transport.Type)
 		}
@@ -128,11 +128,6 @@ func (h *VLESS) Close() error {
 	)
 }
 
-func (h *VLESS) newTransportConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	h.injectTCP(conn, metadata)
-	return nil
-}
-
 func (h *VLESS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	var err error
 	if h.tlsConfig != nil && h.transport == nil {
@@ -144,8 +139,12 @@ 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) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return os.ErrInvalid
+func (h *VLESS) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	err := h.NewConnection(ctx, conn, metadata)
+	N.CloseOnHandshakeFailure(conn, onClose, err)
+	if err != nil {
+		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+	}
 }
 
 func (h *VLESS) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
@@ -188,9 +187,6 @@ var _ adapter.V2RayServerTransportHandler = (*vlessTransportHandler)(nil)
 
 type vlessTransportHandler VLESS
 
-func (t *vlessTransportHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
-	return (*VLESS)(t).newTransportConnection(ctx, conn, adapter.InboundContext{
-		Source:      metadata.Source,
-		Destination: metadata.Destination,
-	})
+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)
 }

+ 11 - 15
inbound/vmess.go

@@ -25,8 +25,8 @@ import (
 )
 
 var (
-	_ adapter.Inbound           = (*VMess)(nil)
-	_ adapter.InjectableInbound = (*VMess)(nil)
+	_ adapter.Inbound              = (*VMess)(nil)
+	_ adapter.TCPInjectableInbound = (*VMess)(nil)
 )
 
 type VMess struct {
@@ -83,7 +83,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
 		}
 	}
 	if options.Transport != nil {
-		inbound.transport, err = v2ray.NewServerTransport(ctx, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vmessTransportHandler)(inbound))
+		inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vmessTransportHandler)(inbound))
 		if err != nil {
 			return nil, E.Cause(err, "create server transport: ", options.Transport.Type)
 		}
@@ -142,11 +142,6 @@ func (h *VMess) Close() error {
 	)
 }
 
-func (h *VMess) newTransportConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	h.injectTCP(conn, metadata)
-	return nil
-}
-
 func (h *VMess) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	var err error
 	if h.tlsConfig != nil && h.transport == nil {
@@ -158,8 +153,12 @@ 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) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return os.ErrInvalid
+func (h *VMess) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	err := h.NewConnection(ctx, conn, metadata)
+	N.CloseOnHandshakeFailure(conn, onClose, err)
+	if err != nil {
+		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+	}
 }
 
 func (h *VMess) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
@@ -202,9 +201,6 @@ var _ adapter.V2RayServerTransportHandler = (*vmessTransportHandler)(nil)
 
 type vmessTransportHandler VMess
 
-func (t *vmessTransportHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
-	return (*VMess)(t).newTransportConnection(ctx, conn, adapter.InboundContext{
-		Source:      metadata.Source,
-		Destination: metadata.Destination,
-	})
+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)
 }

+ 2 - 1
include/quic_stub.go

@@ -11,6 +11,7 @@ import (
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/transport/v2ray"
 	"github.com/sagernet/sing-dns"
+	"github.com/sagernet/sing/common/logger"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
@@ -20,7 +21,7 @@ func init() {
 		return nil, C.ErrQUICNotIncluded
 	})
 	v2ray.RegisterQUICConstructor(
-		func(ctx context.Context, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) {
+		func(ctx context.Context, logger logger.ContextLogger, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) {
 			return nil, C.ErrQUICNotIncluded
 		},
 		func(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) {

+ 1 - 0
option/inbound.go

@@ -98,6 +98,7 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
 	return nil
 }
 
+// Deprecated: Use rule action instead
 type InboundOptions struct {
 	SniffEnabled              bool           `json:"sniff,omitempty"`
 	SniffOverrideDestination  bool           `json:"sniff_override_destination,omitempty"`

+ 40 - 8
option/rule.go

@@ -64,7 +64,7 @@ func (r Rule) IsValid() bool {
 	}
 }
 
-type DefaultRule struct {
+type RawDefaultRule struct {
 	Inbound                  Listable[string] `json:"inbound,omitempty"`
 	IPVersion                int              `json:"ip_version,omitempty"`
 	Network                  Listable[string] `json:"network,omitempty"`
@@ -98,26 +98,58 @@ type DefaultRule struct {
 	RuleSet                  Listable[string] `json:"rule_set,omitempty"`
 	RuleSetIPCIDRMatchSource bool             `json:"rule_set_ip_cidr_match_source,omitempty"`
 	Invert                   bool             `json:"invert,omitempty"`
-	Outbound                 string           `json:"outbound,omitempty"`
 
 	// Deprecated: renamed to rule_set_ip_cidr_match_source
 	Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
 }
 
+type DefaultRule struct {
+	RawDefaultRule
+	RuleAction
+}
+
+func (r *DefaultRule) MarshalJSON() ([]byte, error) {
+	return MarshallObjects(r.RawDefaultRule, r.RuleAction)
+}
+
+func (r *DefaultRule) UnmarshalJSON(data []byte) error {
+	err := json.Unmarshal(data, &r.RawDefaultRule)
+	if err != nil {
+		return err
+	}
+	return UnmarshallExcluded(data, &r.RawDefaultRule, &r.RuleAction)
+}
+
 func (r *DefaultRule) IsValid() bool {
 	var defaultValue DefaultRule
 	defaultValue.Invert = r.Invert
-	defaultValue.Outbound = r.Outbound
+	defaultValue.Action = r.Action
 	return !reflect.DeepEqual(r, defaultValue)
 }
 
+type _LogicalRule struct {
+	Mode   string `json:"mode"`
+	Rules  []Rule `json:"rules,omitempty"`
+	Invert bool   `json:"invert,omitempty"`
+}
+
 type LogicalRule struct {
-	Mode     string `json:"mode"`
-	Rules    []Rule `json:"rules,omitempty"`
-	Invert   bool   `json:"invert,omitempty"`
-	Outbound string `json:"outbound,omitempty"`
+	_LogicalRule
+	RuleAction
+}
+
+func (r *LogicalRule) MarshalJSON() ([]byte, error) {
+	return MarshallObjects(r._LogicalRule, r.RuleAction)
+}
+
+func (r *LogicalRule) UnmarshalJSON(data []byte) error {
+	err := json.Unmarshal(data, &r._LogicalRule)
+	if err != nil {
+		return err
+	}
+	return UnmarshallExcluded(data, &r._LogicalRule, &r.RuleAction)
 }
 
-func (r LogicalRule) IsValid() bool {
+func (r *LogicalRule) IsValid() bool {
 	return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid)
 }

+ 166 - 0
option/rule_action.go

@@ -0,0 +1,166 @@
+package option
+
+import (
+	C "github.com/sagernet/sing-box/constant"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/json"
+)
+
+type _RuleAction struct {
+	Action         string              `json:"action,omitempty"`
+	RouteOptions   RouteActionOptions  `json:"-"`
+	RejectOptions  RejectActionOptions `json:"-"`
+	SniffOptions   RouteActionSniff    `json:"-"`
+	ResolveOptions RouteActionResolve  `json:"-"`
+}
+
+type RuleAction _RuleAction
+
+func (r RuleAction) MarshalJSON() ([]byte, error) {
+	var v any
+	switch r.Action {
+	case C.RuleActionTypeRoute:
+		r.Action = ""
+		v = r.RouteOptions
+	case C.RuleActionTypeReturn:
+		v = nil
+	case C.RuleActionTypeReject:
+		v = r.RejectOptions
+	case C.RuleActionTypeHijackDNS:
+		v = nil
+	case C.RuleActionTypeSniff:
+		v = r.SniffOptions
+	case C.RuleActionTypeResolve:
+		v = r.ResolveOptions
+	default:
+		return nil, E.New("unknown rule action: " + r.Action)
+	}
+	if v == nil {
+		return MarshallObjects((_RuleAction)(r))
+	}
+	return MarshallObjects((_RuleAction)(r), v)
+}
+
+func (r *RuleAction) UnmarshalJSON(data []byte) error {
+	err := json.Unmarshal(data, (*_RuleAction)(r))
+	if err != nil {
+		return err
+	}
+	var v any
+	switch r.Action {
+	case "", C.RuleActionTypeRoute:
+		r.Action = C.RuleActionTypeRoute
+		v = &r.RouteOptions
+	case C.RuleActionTypeReturn:
+		v = nil
+	case C.RuleActionTypeReject:
+		v = &r.RejectOptions
+	case C.RuleActionTypeHijackDNS:
+		v = nil
+	case C.RuleActionTypeSniff:
+		v = &r.SniffOptions
+	case C.RuleActionTypeResolve:
+		v = &r.ResolveOptions
+	default:
+		return E.New("unknown rule action: " + r.Action)
+	}
+	if v == nil {
+		// check unknown fields
+		return json.UnmarshalDisallowUnknownFields(data, &_RuleAction{})
+	}
+	return UnmarshallExcluded(data, (*_RuleAction)(r), v)
+}
+
+type _DNSRuleAction struct {
+	Action         string                `json:"action,omitempty"`
+	RouteOptions   DNSRouteActionOptions `json:"-"`
+	RejectOptions  RejectActionOptions   `json:"-"`
+	SniffOptions   RouteActionSniff      `json:"-"`
+	ResolveOptions RouteActionResolve    `json:"-"`
+}
+
+type DNSRuleAction _DNSRuleAction
+
+func (r DNSRuleAction) MarshalJSON() ([]byte, error) {
+	var v any
+	switch r.Action {
+	case C.RuleActionTypeRoute:
+		r.Action = ""
+		v = r.RouteOptions
+	case C.RuleActionTypeReturn:
+		v = nil
+	case C.RuleActionTypeReject:
+		v = r.RejectOptions
+	default:
+		return nil, E.New("unknown DNS rule action: " + r.Action)
+	}
+	if v == nil {
+		return MarshallObjects((_DNSRuleAction)(r))
+	}
+	return MarshallObjects((_DNSRuleAction)(r), v)
+}
+
+func (r *DNSRuleAction) UnmarshalJSON(data []byte) error {
+	err := json.Unmarshal(data, (*_DNSRuleAction)(r))
+	if err != nil {
+		return err
+	}
+	var v any
+	switch r.Action {
+	case "", C.RuleActionTypeRoute:
+		r.Action = C.RuleActionTypeRoute
+		v = &r.RouteOptions
+	case C.RuleActionTypeReturn:
+		v = nil
+	case C.RuleActionTypeReject:
+		v = &r.RejectOptions
+	default:
+		return E.New("unknown DNS rule action: " + r.Action)
+	}
+	if v == nil {
+		// check unknown fields
+		return json.UnmarshalDisallowUnknownFields(data, &_DNSRuleAction{})
+	}
+	return UnmarshallExcluded(data, (*_DNSRuleAction)(r), v)
+}
+
+type RouteActionOptions struct {
+	Outbound                  string `json:"outbound"`
+	UDPDisableDomainUnmapping bool   `json:"udp_disable_domain_unmapping,omitempty"`
+}
+
+type DNSRouteActionOptions struct {
+	Server       string      `json:"server"`
+	DisableCache bool        `json:"disable_cache,omitempty"`
+	RewriteTTL   *uint32     `json:"rewrite_ttl,omitempty"`
+	ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"`
+}
+
+type RejectActionOptions struct {
+	Method RejectMethod `json:"method,omitempty"`
+}
+
+type RejectMethod string
+
+func (m *RejectMethod) UnmarshalJSON(bytes []byte) error {
+	err := json.Unmarshal(bytes, (*string)(m))
+	if err != nil {
+		return err
+	}
+	switch *m {
+	case C.RuleActionRejectMethodDefault, C.RuleActionRejectMethodPortUnreachable, C.RuleActionRejectMethodDrop:
+		return nil
+	default:
+		return E.New("unknown reject method: " + *m)
+	}
+}
+
+type RouteActionSniff struct {
+	Sniffer Listable[string] `json:"sniffer,omitempty"`
+	Timeout Duration         `json:"timeout,omitempty"`
+}
+
+type RouteActionResolve struct {
+	Strategy DomainStrategy `json:"strategy,omitempty"`
+	Server   string         `json:"server,omitempty"`
+}

+ 40 - 17
option/rule_dns.go

@@ -64,7 +64,7 @@ func (r DNSRule) IsValid() bool {
 	}
 }
 
-type DefaultDNSRule struct {
+type RawDefaultDNSRule struct {
 	Inbound                  Listable[string]       `json:"inbound,omitempty"`
 	IPVersion                int                    `json:"ip_version,omitempty"`
 	QueryType                Listable[DNSQueryType] `json:"query_type,omitempty"`
@@ -100,35 +100,58 @@ type DefaultDNSRule struct {
 	RuleSetIPCIDRMatchSource bool                   `json:"rule_set_ip_cidr_match_source,omitempty"`
 	RuleSetIPCIDRAcceptEmpty bool                   `json:"rule_set_ip_cidr_accept_empty,omitempty"`
 	Invert                   bool                   `json:"invert,omitempty"`
-	Server                   string                 `json:"server,omitempty"`
-	DisableCache             bool                   `json:"disable_cache,omitempty"`
-	RewriteTTL               *uint32                `json:"rewrite_ttl,omitempty"`
-	ClientSubnet             *AddrPrefix            `json:"client_subnet,omitempty"`
 
 	// Deprecated: renamed to rule_set_ip_cidr_match_source
 	Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
 }
 
+type DefaultDNSRule struct {
+	RawDefaultDNSRule
+	DNSRuleAction
+}
+
+func (r *DefaultDNSRule) MarshalJSON() ([]byte, error) {
+	return MarshallObjects(r.RawDefaultDNSRule, r.DNSRuleAction)
+}
+
+func (r *DefaultDNSRule) UnmarshalJSON(data []byte) error {
+	err := json.Unmarshal(data, &r.RawDefaultDNSRule)
+	if err != nil {
+		return err
+	}
+	return UnmarshallExcluded(data, &r.RawDefaultDNSRule, &r.DNSRuleAction)
+}
+
 func (r *DefaultDNSRule) IsValid() bool {
 	var defaultValue DefaultDNSRule
 	defaultValue.Invert = r.Invert
-	defaultValue.Server = r.Server
-	defaultValue.DisableCache = r.DisableCache
-	defaultValue.RewriteTTL = r.RewriteTTL
-	defaultValue.ClientSubnet = r.ClientSubnet
+	defaultValue.DNSRuleAction = r.DNSRuleAction
 	return !reflect.DeepEqual(r, defaultValue)
 }
 
+type _LogicalDNSRule struct {
+	Mode   string    `json:"mode"`
+	Rules  []DNSRule `json:"rules,omitempty"`
+	Invert bool      `json:"invert,omitempty"`
+}
+
 type LogicalDNSRule struct {
-	Mode         string      `json:"mode"`
-	Rules        []DNSRule   `json:"rules,omitempty"`
-	Invert       bool        `json:"invert,omitempty"`
-	Server       string      `json:"server,omitempty"`
-	DisableCache bool        `json:"disable_cache,omitempty"`
-	RewriteTTL   *uint32     `json:"rewrite_ttl,omitempty"`
-	ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"`
+	_LogicalDNSRule
+	DNSRuleAction
+}
+
+func (r *LogicalDNSRule) MarshalJSON() ([]byte, error) {
+	return MarshallObjects(r._LogicalDNSRule, r.DNSRuleAction)
+}
+
+func (r *LogicalDNSRule) UnmarshalJSON(data []byte) error {
+	err := json.Unmarshal(data, &r._LogicalDNSRule)
+	if err != nil {
+		return err
+	}
+	return UnmarshallExcluded(data, &r._LogicalDNSRule, &r.DNSRuleAction)
 }
 
-func (r LogicalDNSRule) IsValid() bool {
+func (r *LogicalDNSRule) IsValid() bool {
 	return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid)
 }

+ 23 - 3
option/types.go

@@ -81,8 +81,11 @@ func (a *AddrPrefix) UnmarshalJSON(content []byte) error {
 	return prefixErr
 }
 
-func (a AddrPrefix) Build() netip.Prefix {
-	return netip.Prefix(a)
+func (a *AddrPrefix) Build() netip.Prefix {
+	if a == nil {
+		return netip.Prefix{}
+	}
+	return netip.Prefix(*a)
 }
 
 type NetworkList string
@@ -143,12 +146,29 @@ func (l *Listable[T]) UnmarshalJSON(content []byte) error {
 
 type DomainStrategy dns.DomainStrategy
 
+func (s DomainStrategy) String() string {
+	switch dns.DomainStrategy(s) {
+	case dns.DomainStrategyAsIS:
+		return ""
+	case dns.DomainStrategyPreferIPv4:
+		return "prefer_ipv4"
+	case dns.DomainStrategyPreferIPv6:
+		return "prefer_ipv6"
+	case dns.DomainStrategyUseIPv4:
+		return "ipv4_only"
+	case dns.DomainStrategyUseIPv6:
+		return "ipv6_only"
+	default:
+		panic(E.New("unknown domain strategy: ", s))
+	}
+}
+
 func (s DomainStrategy) MarshalJSON() ([]byte, error) {
 	var value string
 	switch dns.DomainStrategy(s) {
 	case dns.DomainStrategyAsIS:
 		value = ""
-		// value = "AsIS"
+		// value = "as_is"
 	case dns.DomainStrategyPreferIPv4:
 		value = "prefer_ipv4"
 	case dns.DomainStrategyPreferIPv6:

+ 2 - 0
outbound/block.go

@@ -39,12 +39,14 @@ func (h *Block) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.
 	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)

+ 5 - 5
outbound/default.go

@@ -69,7 +69,7 @@ func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata a
 	if err != nil {
 		return N.ReportHandshakeFailure(conn, err)
 	}
-	err = N.ReportHandshakeSuccess(conn)
+	err = N.ReportConnHandshakeSuccess(conn, outConn)
 	if err != nil {
 		outConn.Close()
 		return err
@@ -96,7 +96,7 @@ func NewDirectConnection(ctx context.Context, router adapter.Router, this N.Dial
 	if err != nil {
 		return N.ReportHandshakeFailure(conn, err)
 	}
-	err = N.ReportHandshakeSuccess(conn)
+	err = N.ReportConnHandshakeSuccess(conn, outConn)
 	if err != nil {
 		outConn.Close()
 		return err
@@ -117,14 +117,14 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn,
 	if err != nil {
 		return N.ReportHandshakeFailure(conn, err)
 	}
-	err = N.ReportHandshakeSuccess(conn)
+	err = N.ReportPacketConnHandshakeSuccess(conn, outConn)
 	if err != nil {
 		outConn.Close()
 		return err
 	}
 	if destinationAddress.IsValid() {
 		if metadata.Destination.IsFqdn() {
-			if metadata.InboundOptions.UDPDisableDomainUnmapping {
+			if metadata.UDPDisableDomainUnmapping {
 				outConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination)
 			} else {
 				outConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination)
@@ -165,7 +165,7 @@ func NewDirectPacketConnection(ctx context.Context, router adapter.Router, this
 	if err != nil {
 		return N.ReportHandshakeFailure(conn, err)
 	}
-	err = N.ReportHandshakeSuccess(conn)
+	err = N.ReportPacketConnHandshakeSuccess(conn, outConn)
 	if err != nil {
 		outConn.Close()
 		return err

+ 8 - 6
outbound/direct.go

@@ -30,7 +30,7 @@ type Direct struct {
 	fallbackDelay       time.Duration
 	overrideOption      int
 	overrideDestination M.Socksaddr
-	loopBack            *loopBackDetector
+	// loopBack *loopBackDetector
 }
 
 func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (*Direct, error) {
@@ -51,7 +51,7 @@ func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, opti
 		domainStrategy: dns.DomainStrategy(options.DomainStrategy),
 		fallbackDelay:  time.Duration(options.FallbackDelay),
 		dialer:         outboundDialer,
-		loopBack:       newLoopBackDetector(router),
+		// loopBack:       newLoopBackDetector(router),
 	}
 	if options.ProxyProtocol != 0 {
 		return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
@@ -90,11 +90,12 @@ func (h *Direct) DialContext(ctx context.Context, network string, destination M.
 	case N.NetworkUDP:
 		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 	}
-	conn, err := h.dialer.DialContext(ctx, network, destination)
+	/*conn, err := h.dialer.DialContext(ctx, network, destination)
 	if err != nil {
 		return nil, err
 	}
-	return h.loopBack.NewConn(conn), nil
+	return h.loopBack.NewConn(conn), nil*/
+	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) {
@@ -148,14 +149,14 @@ func (h *Direct) ListenPacket(ctx context.Context, destination M.Socksaddr) (net
 	if err != nil {
 		return nil, err
 	}
-	conn = h.loopBack.NewPacketConn(bufio.NewPacketConn(conn), destination)
+	// conn = h.loopBack.NewPacketConn(bufio.NewPacketConn(conn), destination)
 	if originDestination != destination {
 		conn = bufio.NewNATPacketConn(bufio.NewPacketConn(conn), destination, originDestination)
 	}
 	return conn, nil
 }
 
-func (h *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+/*func (h *Direct) 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)
 	}
@@ -168,3 +169,4 @@ func (h *Direct) NewPacketConnection(ctx context.Context, conn N.PacketConn, met
 	}
 	return NewPacketConnection(ctx, h, conn, metadata)
 }
+*/

+ 2 - 0
outbound/dns.go

@@ -45,6 +45,7 @@ func (d *DNS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.Pa
 	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()
@@ -97,6 +98,7 @@ func (d *DNS) handleConnection(ctx context.Context, conn net.Conn, metadata adap
 	return nil
 }
 
+// Deprecated
 func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	metadata.Destination = M.Socksaddr{}
 	var reader N.PacketReader = conn

+ 0 - 8
outbound/http.go

@@ -64,11 +64,3 @@ func (h *HTTP) DialContext(ctx context.Context, network string, destination M.So
 func (h *HTTP) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	return nil, os.ErrInvalid
 }
-
-func (h *HTTP) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewConnection(ctx, h, conn, metadata)
-}
-
-func (h *HTTP) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return os.ErrInvalid
-}

+ 0 - 8
outbound/hysteria.go

@@ -122,14 +122,6 @@ func (h *Hysteria) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
 	return h.client.ListenPacket(ctx, destination)
 }
 
-func (h *Hysteria) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewConnection(ctx, h, conn, metadata)
-}
-
-func (h *Hysteria) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return NewPacketConnection(ctx, h, conn, metadata)
-}
-
 func (h *Hysteria) InterfaceUpdated() {
 	h.client.CloseWithError(E.New("network changed"))
 }

+ 0 - 8
outbound/hysteria2.go

@@ -108,14 +108,6 @@ func (h *Hysteria2) ListenPacket(ctx context.Context, destination M.Socksaddr) (
 	return h.client.ListenPacket(ctx)
 }
 
-func (h *Hysteria2) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewConnection(ctx, h, conn, metadata)
-}
-
-func (h *Hysteria2) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return NewPacketConnection(ctx, h, conn, metadata)
-}
-
 func (h *Hysteria2) InterfaceUpdated() {
 	h.client.CloseWithError(E.New("network changed"))
 }

+ 3 - 0
outbound/proxy.go

@@ -94,6 +94,9 @@ func (l *ProxyListener) acceptLoop() {
 	}
 }
 
+// TODO: migrate to new api
+//
+//nolint:staticcheck
 func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error {
 	return socks.HandleConnection(ctx, conn, l.authenticator, l, M.Metadata{})
 }

+ 14 - 2
outbound/selector.go

@@ -146,14 +146,26 @@ func (s *Selector) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
 	return s.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil
 }
 
+// TODO
+// Deprecated
 func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	ctx = interrupt.ContextWithIsExternalConnection(ctx)
-	return s.selected.NewConnection(ctx, conn, metadata)
+	if legacyHandler, ok := s.selected.(adapter.ConnectionHandler); ok {
+		return legacyHandler.NewConnection(ctx, conn, metadata)
+	} else {
+		return NewConnection(ctx, s.selected, conn, metadata)
+	}
 }
 
+// TODO
+// Deprecated
 func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	ctx = interrupt.ContextWithIsExternalConnection(ctx)
-	return s.selected.NewPacketConnection(ctx, conn, metadata)
+	if legacyHandler, ok := s.selected.(adapter.PacketConnectionHandler); ok {
+		return legacyHandler.NewPacketConnection(ctx, conn, metadata)
+	} else {
+		return NewPacketConnection(ctx, s.selected, conn, metadata)
+	}
 }
 
 func RealTag(detour adapter.Outbound) string {

+ 0 - 8
outbound/shadowsocks.go

@@ -125,14 +125,6 @@ func (h *Shadowsocks) ListenPacket(ctx context.Context, destination M.Socksaddr)
 	}
 }
 
-func (h *Shadowsocks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewConnection(ctx, h, conn, metadata)
-}
-
-func (h *Shadowsocks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return NewPacketConnection(ctx, h, conn, metadata)
-}
-
 func (h *Shadowsocks) InterfaceUpdated() {
 	if h.multiplexDialer != nil {
 		h.multiplexDialer.Reset()

+ 0 - 8
outbound/shadowtls.go

@@ -106,11 +106,3 @@ func (h *ShadowTLS) DialContext(ctx context.Context, network string, destination
 func (h *ShadowTLS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	return nil, os.ErrInvalid
 }
-
-func (h *ShadowTLS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewConnection(ctx, h, conn, metadata)
-}
-
-func (h *ShadowTLS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return os.ErrInvalid
-}

+ 4 - 0
outbound/socks.go

@@ -113,6 +113,8 @@ func (h *Socks) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.
 	return h.client.ListenPacket(ctx, destination)
 }
 
+// TODO
+// Deprecated
 func (h *Socks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	if h.resolve {
 		return NewDirectConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4)
@@ -121,6 +123,8 @@ func (h *Socks) NewConnection(ctx context.Context, conn net.Conn, metadata adapt
 	}
 }
 
+// TODO
+// Deprecated
 func (h *Socks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	if h.resolve {
 		return NewDirectPacketConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4)

+ 0 - 8
outbound/ssh.go

@@ -199,11 +199,3 @@ func (s *SSH) DialContext(ctx context.Context, network string, destination M.Soc
 func (s *SSH) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	return nil, os.ErrInvalid
 }
-
-func (s *SSH) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewConnection(ctx, s, conn, metadata)
-}
-
-func (s *SSH) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return os.ErrInvalid
-}

+ 0 - 8
outbound/tor.go

@@ -211,11 +211,3 @@ func (t *Tor) DialContext(ctx context.Context, network string, destination M.Soc
 func (t *Tor) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	return nil, os.ErrInvalid
 }
-
-func (t *Tor) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewConnection(ctx, t, conn, metadata)
-}
-
-func (t *Tor) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return os.ErrInvalid
-}

+ 0 - 8
outbound/trojan.go

@@ -99,14 +99,6 @@ func (h *Trojan) ListenPacket(ctx context.Context, destination M.Socksaddr) (net
 	}
 }
 
-func (h *Trojan) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewConnection(ctx, h, conn, metadata)
-}
-
-func (h *Trojan) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return NewPacketConnection(ctx, h, conn, metadata)
-}
-
 func (h *Trojan) InterfaceUpdated() {
 	if h.transport != nil {
 		h.transport.Close()

+ 0 - 8
outbound/tuic.go

@@ -136,14 +136,6 @@ func (h *TUIC) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.P
 	}
 }
 
-func (h *TUIC) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewConnection(ctx, h, conn, metadata)
-}
-
-func (h *TUIC) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return NewPacketConnection(ctx, h, conn, metadata)
-}
-
 func (h *TUIC) InterfaceUpdated() {
 	_ = h.client.CloseWithError(E.New("network changed"))
 }

+ 4 - 0
outbound/urltest.go

@@ -167,11 +167,15 @@ func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (ne
 	return nil, err
 }
 
+// TODO
+// 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)
 }
 
+// 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)

+ 0 - 8
outbound/vless.go

@@ -118,14 +118,6 @@ func (h *VLESS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.
 	}
 }
 
-func (h *VLESS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewConnection(ctx, h, conn, metadata)
-}
-
-func (h *VLESS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return NewPacketConnection(ctx, h, conn, metadata)
-}
-
 func (h *VLESS) InterfaceUpdated() {
 	if h.transport != nil {
 		h.transport.Close()

+ 0 - 8
outbound/vmess.go

@@ -146,14 +146,6 @@ func (h *VMess) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.
 	}
 }
 
-func (h *VMess) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewConnection(ctx, h, conn, metadata)
-}
-
-func (h *VMess) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return NewPacketConnection(ctx, h, conn, metadata)
-}
-
 type vmessDialer VMess
 
 func (h *vmessDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {

+ 4 - 0
outbound/wireguard.go

@@ -241,10 +241,14 @@ func (w *WireGuard) ListenPacket(ctx context.Context, destination M.Socksaddr) (
 	return w.tunDevice.ListenPacket(ctx, destination)
 }
 
+// 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)
 }
 
+// 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)
 }

+ 2 - 1
route/router_geo_resources.go → route/geo_resources.go

@@ -13,6 +13,7 @@ import (
 	"github.com/sagernet/sing-box/common/geosite"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/experimental/deprecated"
+	R "github.com/sagernet/sing-box/route/rule"
 	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
 	"github.com/sagernet/sing/common/rw"
@@ -32,7 +33,7 @@ func (r *Router) LoadGeosite(code string) (adapter.Rule, error) {
 	if err != nil {
 		return nil, err
 	}
-	rule, err = NewDefaultRule(r.ctx, r, nil, geosite.Compile(items))
+	rule, err = R.NewDefaultRule(r.ctx, r, nil, geosite.Compile(items))
 	if err != nil {
 		return nil, err
 	}

+ 583 - 0
route/route.go

@@ -0,0 +1,583 @@
+package route
+
+import (
+	"context"
+	"errors"
+	"net"
+	"net/netip"
+	"os"
+	"os/user"
+	"strings"
+	"syscall"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	"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"
+	"github.com/sagernet/sing-tun"
+	"github.com/sagernet/sing-vmess"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/bufio"
+	"github.com/sagernet/sing/common/bufio/deadline"
+	E "github.com/sagernet/sing/common/exceptions"
+	F "github.com/sagernet/sing/common/format"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+	"github.com/sagernet/sing/common/uot"
+)
+
+// Deprecated: use RouteConnectionEx instead.
+func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	return r.routeConnection(ctx, conn, metadata, nil)
+}
+
+func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	err := r.routeConnection(ctx, conn, metadata, onClose)
+	if err != nil {
+		N.CloseOnHandshakeFailure(conn, onClose, err)
+		if E.IsClosedOrCanceled(err) {
+			r.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			r.logger.ErrorContext(ctx, err)
+		}
+	}
+}
+
+func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
+	if r.pauseManager.IsDevicePaused() {
+		return E.New("reject connection to ", metadata.Destination, " while device paused")
+	}
+
+	if metadata.InboundDetour != "" {
+		if metadata.LastInbound == metadata.InboundDetour {
+			return E.New("routing loop on detour: ", metadata.InboundDetour)
+		}
+		detour := r.inboundByTag[metadata.InboundDetour]
+		if detour == nil {
+			return E.New("inbound detour not found: ", metadata.InboundDetour)
+		}
+		injectable, isInjectable := detour.(adapter.TCPInjectableInbound)
+		if !isInjectable {
+			return E.New("inbound detour is not TCP injectable: ", metadata.InboundDetour)
+		}
+		metadata.LastInbound = metadata.Inbound
+		metadata.Inbound = metadata.InboundDetour
+		metadata.InboundDetour = ""
+		injectable.NewConnectionEx(ctx, conn, metadata, onClose)
+		return nil
+	}
+	conntrack.KillerCheck()
+	metadata.Network = N.NetworkTCP
+	switch metadata.Destination.Fqdn {
+	case mux.Destination.Fqdn:
+		return E.New("global multiplex is deprecated since sing-box v1.7.0, enable multiplex in inbound options instead.")
+	case vmess.MuxDestination.Fqdn:
+		return E.New("global multiplex (v2ray legacy) not supported since sing-box v1.7.0.")
+	case uot.MagicAddress:
+		return E.New("global UoT not supported since sing-box v1.7.0.")
+	case uot.LegacyMagicAddress:
+		return E.New("global UoT (legacy) not supported since sing-box v1.7.0.")
+	}
+	if deadline.NeedAdditionalReadDeadline(conn) {
+		conn = deadline.NewConn(conn)
+	}
+	selectedRule, _, buffers, err := r.matchRule(ctx, &metadata, conn, nil, -1)
+	if err != nil {
+		return err
+	}
+	var selectedOutbound adapter.Outbound
+	var selectReturn bool
+	if selectedRule != nil {
+		switch action := selectedRule.Action().(type) {
+		case *rule.RuleActionRoute:
+			var loaded bool
+			selectedOutbound, loaded = r.Outbound(action.Outbound)
+			if !loaded {
+				buf.ReleaseMulti(buffers)
+				return E.New("outbound not found: ", action.Outbound)
+			}
+		case *rule.RuleActionReturn:
+			selectReturn = true
+		case *rule.RuleActionReject:
+			buf.ReleaseMulti(buffers)
+			var rejectErr error
+			switch action.Method {
+			case C.RuleActionRejectMethodDefault:
+				rejectErr = os.ErrClosed
+			case C.RuleActionRejectMethodPortUnreachable:
+				rejectErr = syscall.ECONNREFUSED
+			case C.RuleActionRejectMethodDrop:
+				rejectErr = tun.ErrDrop
+			}
+			N.CloseOnHandshakeFailure(conn, onClose, rejectErr)
+			return nil
+		}
+	}
+	if selectedRule == nil || selectReturn {
+		if r.defaultOutboundForConnection == nil {
+			buf.ReleaseMulti(buffers)
+			return E.New("missing default outbound with TCP support")
+		}
+		selectedOutbound = r.defaultOutboundForConnection
+	}
+	if !common.Contains(selectedOutbound.Network(), N.NetworkTCP) {
+		buf.ReleaseMulti(buffers)
+		return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag())
+	}
+	for _, buffer := range buffers {
+		conn = bufio.NewCachedConn(conn, buffer)
+	}
+	if r.clashServer != nil {
+		trackerConn, tracker := r.clashServer.RoutedConnection(ctx, conn, metadata, selectedRule)
+		defer tracker.Leave()
+		conn = trackerConn
+	}
+	if r.v2rayServer != nil {
+		if statsService := r.v2rayServer.StatsService(); statsService != nil {
+			conn = statsService.RoutedConnection(metadata.Inbound, selectedOutbound.Tag(), metadata.User, conn)
+		}
+	}
+	legacyOutbound, isLegacy := selectedOutbound.(adapter.ConnectionHandler)
+	if isLegacy {
+		err = legacyOutbound.NewConnection(ctx, conn, metadata)
+		if err != nil {
+			conn.Close()
+			if onClose != nil {
+				onClose(err)
+			}
+			return E.Cause(err, "outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]")
+		} else {
+			if onClose != nil {
+				onClose(nil)
+			}
+		}
+		return nil
+	}
+	// TODO
+	err = outbound.NewConnection(ctx, selectedOutbound, conn, metadata)
+	if err != nil {
+		conn.Close()
+		if onClose != nil {
+			onClose(err)
+		}
+		return E.Cause(err, "outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]")
+	} else {
+		if onClose != nil {
+			onClose(nil)
+		}
+	}
+	return nil
+}
+
+func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	err := r.routePacketConnection(ctx, conn, metadata, nil)
+	if err != nil {
+		conn.Close()
+		if E.IsClosedOrCanceled(err) {
+			r.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			r.logger.ErrorContext(ctx, err)
+		}
+	}
+	return nil
+}
+
+func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	err := r.routePacketConnection(ctx, conn, metadata, onClose)
+	if err != nil {
+		N.CloseOnHandshakeFailure(conn, onClose, err)
+		if E.IsClosedOrCanceled(err) {
+			r.logger.DebugContext(ctx, "connection closed: ", err)
+		} else {
+			r.logger.ErrorContext(ctx, err)
+		}
+	} else if onClose != nil {
+		onClose(nil)
+	}
+}
+
+func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
+	if r.pauseManager.IsDevicePaused() {
+		return E.New("reject packet connection to ", metadata.Destination, " while device paused")
+	}
+	if metadata.InboundDetour != "" {
+		if metadata.LastInbound == metadata.InboundDetour {
+			return E.New("routing loop on detour: ", metadata.InboundDetour)
+		}
+		detour := r.inboundByTag[metadata.InboundDetour]
+		if detour == nil {
+			return E.New("inbound detour not found: ", metadata.InboundDetour)
+		}
+		injectable, isInjectable := detour.(adapter.UDPInjectableInbound)
+		if !isInjectable {
+			return E.New("inbound detour is not UDP injectable: ", metadata.InboundDetour)
+		}
+		metadata.LastInbound = metadata.Inbound
+		metadata.Inbound = metadata.InboundDetour
+		metadata.InboundDetour = ""
+		injectable.NewPacketConnectionEx(ctx, conn, metadata, onClose)
+		return nil
+	}
+	conntrack.KillerCheck()
+
+	// TODO: move to UoT
+	metadata.Network = N.NetworkUDP
+
+	// Currently we don't have deadline usages for UDP connections
+	/*if deadline.NeedAdditionalReadDeadline(conn) {
+		conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn))
+	}*/
+
+	selectedRule, _, buffers, err := r.matchRule(ctx, &metadata, nil, conn, -1)
+	if err != nil {
+		return err
+	}
+	var selectedOutbound adapter.Outbound
+	var selectReturn bool
+	if selectedRule != nil {
+		switch action := selectedRule.Action().(type) {
+		case *rule.RuleActionRoute:
+			var loaded bool
+			selectedOutbound, loaded = r.Outbound(action.Outbound)
+			if !loaded {
+				buf.ReleaseMulti(buffers)
+				return E.New("outbound not found: ", action.Outbound)
+			}
+			metadata.UDPDisableDomainUnmapping = action.UDPDisableDomainUnmapping
+		case *rule.RuleActionReturn:
+			selectReturn = true
+		case *rule.RuleActionReject:
+			buf.ReleaseMulti(buffers)
+			N.CloseOnHandshakeFailure(conn, onClose, syscall.ECONNREFUSED)
+			return nil
+		}
+	}
+	if selectedRule == nil || selectReturn {
+		if r.defaultOutboundForPacketConnection == nil {
+			buf.ReleaseMulti(buffers)
+			return E.New("missing default outbound with UDP support")
+		}
+		selectedOutbound = r.defaultOutboundForPacketConnection
+	}
+	if !common.Contains(selectedOutbound.Network(), N.NetworkUDP) {
+		buf.ReleaseMulti(buffers)
+		return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag())
+	}
+	for _, buffer := range buffers {
+		// TODO: check if metadata.Destination == packet destination
+		conn = bufio.NewCachedPacketConn(conn, buffer, metadata.Destination)
+	}
+	if r.clashServer != nil {
+		trackerConn, tracker := r.clashServer.RoutedPacketConnection(ctx, conn, metadata, selectedRule)
+		defer tracker.Leave()
+		conn = trackerConn
+	}
+	if r.v2rayServer != nil {
+		if statsService := r.v2rayServer.StatsService(); statsService != nil {
+			conn = statsService.RoutedPacketConnection(metadata.Inbound, selectedOutbound.Tag(), metadata.User, conn)
+		}
+	}
+	if metadata.FakeIP {
+		conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
+	}
+	legacyOutbound, isLegacy := selectedOutbound.(adapter.PacketConnectionHandler)
+	if isLegacy {
+		err = legacyOutbound.NewPacketConnection(ctx, conn, metadata)
+		N.CloseOnHandshakeFailure(conn, onClose, err)
+		if err != nil {
+			return E.Cause(err, "outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]")
+		}
+		return nil
+	}
+	// TODO
+	err = outbound.NewPacketConnection(ctx, selectedOutbound, conn, metadata)
+	N.CloseOnHandshakeFailure(conn, onClose, err)
+	if err != nil {
+		return E.Cause(err, "outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]")
+	}
+	return nil
+}
+
+func (r *Router) matchRule(
+	ctx context.Context, metadata *adapter.InboundContext,
+	inputConn net.Conn, inputPacketConn N.PacketConn, ruleIndex int,
+) (selectedRule adapter.Rule, selectedRuleIndex int, buffers []*buf.Buffer, fatalErr error) {
+	if r.processSearcher != nil && metadata.ProcessInfo == nil {
+		var originDestination netip.AddrPort
+		if metadata.OriginDestination.IsValid() {
+			originDestination = metadata.OriginDestination.AddrPort()
+		} else if metadata.Destination.IsIP() {
+			originDestination = metadata.Destination.AddrPort()
+		}
+		processInfo, fErr := process.FindProcessInfo(r.processSearcher, ctx, metadata.Network, metadata.Source.AddrPort(), originDestination)
+		if fErr != nil {
+			r.logger.InfoContext(ctx, "failed to search process: ", fErr)
+		} else {
+			if processInfo.ProcessPath != "" {
+				r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
+			} else if processInfo.PackageName != "" {
+				r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName)
+			} else if processInfo.UserId != -1 {
+				if /*needUserName &&*/ true {
+					osUser, _ := user.LookupId(F.ToString(processInfo.UserId))
+					if osUser != nil {
+						processInfo.User = osUser.Username
+					}
+				}
+				if processInfo.User != "" {
+					r.logger.InfoContext(ctx, "found user: ", processInfo.User)
+				} else {
+					r.logger.InfoContext(ctx, "found user id: ", processInfo.UserId)
+				}
+			}
+			metadata.ProcessInfo = processInfo
+		}
+	}
+	if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) {
+		domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr)
+		if !loaded {
+			fatalErr = E.New("missing fakeip record, try to configure experimental.cache_file")
+			return
+		}
+		metadata.OriginDestination = metadata.Destination
+		metadata.Destination = M.Socksaddr{
+			Fqdn: domain,
+			Port: metadata.Destination.Port,
+		}
+		metadata.FakeIP = true
+		r.logger.DebugContext(ctx, "found fakeip domain: ", domain)
+	}
+	if r.dnsReverseMapping != nil && metadata.Domain == "" {
+		domain, loaded := r.dnsReverseMapping.Query(metadata.Destination.Addr)
+		if loaded {
+			metadata.Domain = domain
+			r.logger.DebugContext(ctx, "found reserve mapped domain: ", metadata.Domain)
+		}
+	}
+	if metadata.Destination.IsIPv4() {
+		metadata.IPVersion = 4
+	} else if metadata.Destination.IsIPv6() {
+		metadata.IPVersion = 6
+	}
+
+	//nolint:staticcheck
+	if metadata.InboundOptions != common.DefaultValue[option.InboundOptions]() {
+		if metadata.InboundOptions.SniffEnabled {
+			newBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{
+				OverrideDestination: metadata.InboundOptions.SniffOverrideDestination,
+				Timeout:             time.Duration(metadata.InboundOptions.SniffTimeout),
+			}, inputConn, inputPacketConn)
+			if newErr != nil {
+				fatalErr = newErr
+				return
+			}
+			buffers = append(buffers, newBuffers...)
+		}
+		if dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS {
+			fatalErr = r.actionResolve(ctx, metadata, &rule.RuleActionResolve{
+				Strategy: dns.DomainStrategy(metadata.InboundOptions.DomainStrategy),
+			})
+			if fatalErr != nil {
+				return
+			}
+		}
+		if metadata.InboundOptions.UDPDisableDomainUnmapping {
+			metadata.UDPDisableDomainUnmapping = true
+		}
+		metadata.InboundOptions = option.InboundOptions{}
+	}
+
+match:
+	for ruleIndex < len(r.rules) {
+		rules := r.rules
+		if ruleIndex != -1 {
+			rules = rules[ruleIndex+1:]
+		}
+		var (
+			currentRule      adapter.Rule
+			currentRuleIndex int
+			matched          bool
+		)
+		for currentRuleIndex, currentRule = range rules {
+			if currentRule.Match(metadata) {
+				matched = true
+				break
+			}
+		}
+		if !matched {
+			break
+		}
+		r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action())
+		switch action := currentRule.Action().(type) {
+		case *rule.RuleActionSniff:
+			newBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn)
+			if newErr != nil {
+				fatalErr = newErr
+				return
+			}
+			buffers = append(buffers, newBuffers...)
+		case *rule.RuleActionResolve:
+			fatalErr = r.actionResolve(ctx, metadata, action)
+			if fatalErr != nil {
+				return
+			}
+		default:
+			selectedRule = currentRule
+			selectedRuleIndex = currentRuleIndex
+			break match
+		}
+		ruleIndex = currentRuleIndex
+	}
+	if metadata.Destination.Addr.IsUnspecified() {
+		newBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{}, inputConn, inputPacketConn)
+		if newErr != nil {
+			fatalErr = newErr
+			return
+		}
+		buffers = append(buffers, newBuffers...)
+	}
+	return
+}
+
+func (r *Router) actionSniff(
+	ctx context.Context, metadata *adapter.InboundContext, action *rule.RuleActionSniff,
+	inputConn net.Conn, inputPacketConn N.PacketConn,
+) (buffers []*buf.Buffer, fatalErr error) {
+	if sniff.Skip(metadata) {
+		return
+	} else if inputConn != nil && len(action.StreamSniffers) > 0 {
+		buffer := buf.NewPacket()
+		err := sniff.PeekStream(
+			ctx,
+			metadata,
+			inputConn,
+			buffer,
+			action.Timeout,
+			action.StreamSniffers...,
+		)
+		if err == nil {
+			//goland:noinspection GoDeprecation
+			if action.OverrideDestination && M.IsDomainName(metadata.Domain) {
+				metadata.Destination = M.Socksaddr{
+					Fqdn: metadata.Domain,
+					Port: metadata.Destination.Port,
+				}
+			}
+			if metadata.Domain != "" && metadata.Client != "" {
+				r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client)
+			} else if metadata.Domain != "" {
+				r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
+			} else {
+				r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol)
+			}
+		}
+		if !buffer.IsEmpty() {
+			buffers = append(buffers, buffer)
+		} else {
+			buffer.Release()
+		}
+	} else if inputPacketConn != nil && len(action.PacketSniffers) > 0 {
+		for {
+			var (
+				buffer      = buf.NewPacket()
+				destination M.Socksaddr
+				done        = make(chan struct{})
+				err         error
+			)
+			go func() {
+				sniffTimeout := C.ReadPayloadTimeout
+				if action.Timeout > 0 {
+					sniffTimeout = action.Timeout
+				}
+				inputPacketConn.SetReadDeadline(time.Now().Add(sniffTimeout))
+				destination, err = inputPacketConn.ReadPacket(buffer)
+				inputPacketConn.SetReadDeadline(time.Time{})
+				close(done)
+			}()
+			select {
+			case <-done:
+			case <-ctx.Done():
+				inputPacketConn.Close()
+				fatalErr = ctx.Err()
+				return
+			}
+			if err != nil {
+				buffer.Release()
+				if !errors.Is(err, os.ErrDeadlineExceeded) {
+					fatalErr = err
+					return
+				}
+			} else {
+				// TODO: maybe always override destination
+				if metadata.Destination.Addr.IsUnspecified() {
+					metadata.Destination = destination
+				}
+				if len(buffers) > 0 {
+					err = sniff.PeekPacket(
+						ctx,
+						metadata,
+						buffer.Bytes(),
+						sniff.QUICClientHello,
+					)
+				} else {
+					err = sniff.PeekPacket(
+						ctx, metadata,
+						buffer.Bytes(),
+						action.PacketSniffers...,
+					)
+				}
+				buffers = append(buffers, buffer)
+				if E.IsMulti(err, sniff.ErrClientHelloFragmented) && len(buffers) == 0 {
+					r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello")
+					continue
+				}
+				if metadata.Protocol != "" {
+					//goland:noinspection GoDeprecation
+					if action.OverrideDestination && M.IsDomainName(metadata.Domain) {
+						metadata.Destination = M.Socksaddr{
+							Fqdn: metadata.Domain,
+							Port: metadata.Destination.Port,
+						}
+					}
+					if metadata.Domain != "" && metadata.Client != "" {
+						r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client)
+					} else if metadata.Domain != "" {
+						r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
+					} else if metadata.Client != "" {
+						r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", client: ", metadata.Client)
+					} else {
+						r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol)
+					}
+				}
+			}
+			break
+		}
+	}
+	return
+}
+
+func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundContext, action *rule.RuleActionResolve) error {
+	if metadata.Destination.IsFqdn() {
+		// TODO: check if WithContext is necessary
+		addresses, err := r.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, action.Strategy)
+		if err != nil {
+			return err
+		}
+		metadata.DestinationAddresses = addresses
+		r.dnsLogger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]")
+		if metadata.Destination.IsIPv4() {
+			metadata.IPVersion = 4
+		} else if metadata.Destination.IsIPv6() {
+			metadata.IPVersion = 6
+		}
+	}
+	return nil
+}

+ 50 - 55
route/router_dns.go → route/route_dns.go

@@ -8,6 +8,7 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	R "github.com/sagernet/sing-box/route/rule"
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing/common/cache"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -36,15 +37,16 @@ func (m *DNSReverseMapping) Query(address netip.Addr) (string, bool) {
 	return domain, loaded
 }
 
-func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int, isAddressQuery bool) (context.Context, dns.Transport, dns.DomainStrategy, adapter.DNSRule, int) {
+func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, isAddressQuery bool) (dns.Transport, dns.QueryOptions, adapter.DNSRule, int) {
 	metadata := adapter.ContextFrom(ctx)
 	if metadata == nil {
 		panic("no context")
 	}
-	if index < len(r.dnsRules) {
+	var options dns.QueryOptions
+	if ruleIndex < len(r.dnsRules) {
 		dnsRules := r.dnsRules
-		if index != -1 {
-			dnsRules = dnsRules[index+1:]
+		if ruleIndex != -1 {
+			dnsRules = dnsRules[ruleIndex+1:]
 		}
 		for currentRuleIndex, rule := range dnsRules {
 			if rule.WithAddressLimit() && !isAddressQuery {
@@ -52,43 +54,42 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int, isAd
 			}
 			metadata.ResetRuleCache()
 			if rule.Match(metadata) {
-				detour := rule.Outbound()
-				transport, loaded := r.transportMap[detour]
-				if !loaded {
-					r.dnsLogger.ErrorContext(ctx, "transport not found: ", detour)
-					continue
+				displayRuleIndex := currentRuleIndex
+				if displayRuleIndex != -1 {
+					displayRuleIndex += displayRuleIndex + 1
 				}
-				_, isFakeIP := transport.(adapter.FakeIPTransport)
-				if isFakeIP && !allowFakeIP {
-					continue
-				}
-				ruleIndex := currentRuleIndex
-				if index != -1 {
-					ruleIndex += index + 1
-				}
-				r.dnsLogger.DebugContext(ctx, "match[", ruleIndex, "] ", rule.String(), " => ", detour)
-				if isFakeIP || rule.DisableCache() {
-					ctx = dns.ContextWithDisableCache(ctx, true)
-				}
-				if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil {
-					ctx = dns.ContextWithRewriteTTL(ctx, *rewriteTTL)
-				}
-				if clientSubnet := rule.ClientSubnet(); clientSubnet != nil {
-					ctx = dns.ContextWithClientSubnet(ctx, *clientSubnet)
-				}
-				if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded {
-					return ctx, transport, domainStrategy, rule, ruleIndex
+				if routeAction, isRoute := rule.Action().(*R.RuleActionDNSRoute); isRoute {
+					transport, loaded := r.transportMap[routeAction.Server]
+					if !loaded {
+						r.dnsLogger.ErrorContext(ctx, "transport not found: ", routeAction.Server)
+						continue
+					}
+					_, isFakeIP := transport.(adapter.FakeIPTransport)
+					if isFakeIP && !allowFakeIP {
+						continue
+					}
+					options.DisableCache = isFakeIP || routeAction.DisableCache
+					options.RewriteTTL = routeAction.RewriteTTL
+					options.ClientSubnet = routeAction.ClientSubnet
+					if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded {
+						options.Strategy = domainStrategy
+					} else {
+						options.Strategy = r.defaultDomainStrategy
+					}
+					r.dnsLogger.DebugContext(ctx, "match[", displayRuleIndex, "] ", rule.String(), " => ", rule.Action())
+					return transport, options, rule, currentRuleIndex
 				} else {
-					return ctx, transport, r.defaultDomainStrategy, rule, ruleIndex
+					return nil, options, rule, currentRuleIndex
 				}
 			}
 		}
 	}
 	if domainStrategy, dsLoaded := r.transportDomainStrategy[r.defaultTransport]; dsLoaded {
-		return ctx, r.defaultTransport, domainStrategy, nil, -1
+		options.Strategy = domainStrategy
 	} else {
-		return ctx, r.defaultTransport, r.defaultDomainStrategy, nil, -1
+		options.Strategy = r.defaultDomainStrategy
 	}
+	return r.defaultTransport, options, nil, -1
 }
 
 func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
@@ -117,21 +118,18 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er
 			metadata.Domain = fqdnToDomain(message.Question[0].Name)
 		}
 		var (
-			strategy  dns.DomainStrategy
+			options   dns.QueryOptions
 			rule      adapter.DNSRule
 			ruleIndex int
 		)
 		ruleIndex = -1
 		for {
-			var (
-				dnsCtx       context.Context
-				addressLimit bool
-			)
-			dnsCtx, transport, strategy, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message))
-			dnsCtx = adapter.OverrideContext(dnsCtx)
+			dnsCtx := adapter.OverrideContext(ctx)
+			var addressLimit bool
+			transport, options, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message))
 			if rule != nil && rule.WithAddressLimit() {
 				addressLimit = true
-				response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, strategy, func(response *mDNS.Msg) bool {
+				response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, options, func(response *mDNS.Msg) bool {
 					addresses, addrErr := dns.MessageToAddresses(response)
 					if addrErr != nil {
 						return false
@@ -141,7 +139,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er
 				})
 			} else {
 				addressLimit = false
-				response, err = r.dnsClient.Exchange(dnsCtx, transport, message, strategy)
+				response, err = r.dnsClient.Exchange(dnsCtx, transport, message, options)
 			}
 			var rejected bool
 			if err != nil {
@@ -199,31 +197,28 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS
 	metadata.Destination = M.Socksaddr{}
 	metadata.Domain = domain
 	var (
-		transport         dns.Transport
-		transportStrategy dns.DomainStrategy
-		rule              adapter.DNSRule
-		ruleIndex         int
+		transport dns.Transport
+		options   dns.QueryOptions
+		rule      adapter.DNSRule
+		ruleIndex int
 	)
 	ruleIndex = -1
 	for {
-		var (
-			dnsCtx       context.Context
-			addressLimit bool
-		)
-		dnsCtx, transport, transportStrategy, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true)
-		dnsCtx = adapter.OverrideContext(dnsCtx)
-		if strategy == dns.DomainStrategyAsIS {
-			strategy = transportStrategy
+		dnsCtx := adapter.OverrideContext(ctx)
+		var addressLimit bool
+		transport, options, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true)
+		if strategy != dns.DomainStrategyAsIS {
+			options.Strategy = strategy
 		}
 		if rule != nil && rule.WithAddressLimit() {
 			addressLimit = true
-			responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool {
+			responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, options, func(responseAddrs []netip.Addr) bool {
 				metadata.DestinationAddresses = responseAddrs
 				return rule.MatchAddressLimit(metadata)
 			})
 		} else {
 			addressLimit = false
-			responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy)
+			responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, options)
 		}
 		if err != nil {
 			if errors.Is(err, dns.ErrResponseRejectedCached) {

+ 10 - 384
route/router.go

@@ -3,11 +3,9 @@ package route
 import (
 	"context"
 	"errors"
-	"net"
 	"net/netip"
 	"net/url"
 	"os"
-	"os/user"
 	"runtime"
 	"strings"
 	"syscall"
@@ -19,22 +17,16 @@ import (
 	"github.com/sagernet/sing-box/common/geoip"
 	"github.com/sagernet/sing-box/common/geosite"
 	"github.com/sagernet/sing-box/common/process"
-	"github.com/sagernet/sing-box/common/sniff"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	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"
-	"github.com/sagernet/sing-box/outbound"
+	R "github.com/sagernet/sing-box/route/rule"
 	"github.com/sagernet/sing-box/transport/fakeip"
 	"github.com/sagernet/sing-dns"
-	"github.com/sagernet/sing-mux"
 	"github.com/sagernet/sing-tun"
-	"github.com/sagernet/sing-vmess"
 	"github.com/sagernet/sing/common"
-	"github.com/sagernet/sing/common/buf"
-	"github.com/sagernet/sing/common/bufio"
-	"github.com/sagernet/sing/common/bufio/deadline"
 	"github.com/sagernet/sing/common/control"
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
@@ -42,7 +34,6 @@ import (
 	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/ntp"
 	"github.com/sagernet/sing/common/task"
-	"github.com/sagernet/sing/common/uot"
 	"github.com/sagernet/sing/common/winpowrprof"
 	"github.com/sagernet/sing/service"
 	"github.com/sagernet/sing/service/pause"
@@ -154,14 +145,14 @@ func NewRouter(
 		Logger: router.dnsLogger,
 	})
 	for i, ruleOptions := range options.Rules {
-		routeRule, err := NewRule(ctx, router, router.logger, ruleOptions, true)
+		routeRule, err := R.NewRule(ctx, router, router.logger, ruleOptions, true)
 		if err != nil {
 			return nil, E.Cause(err, "parse rule[", i, "]")
 		}
 		router.rules = append(router.rules, routeRule)
 	}
 	for i, dnsRuleOptions := range dnsOptions.Rules {
-		dnsRule, err := NewDNSRule(ctx, router, router.logger, dnsRuleOptions, true)
+		dnsRule, err := R.NewDNSRule(ctx, router, router.logger, dnsRuleOptions, true)
 		if err != nil {
 			return nil, E.Cause(err, "parse dns rule[", i, "]")
 		}
@@ -171,7 +162,7 @@ func NewRouter(
 		if _, exists := router.ruleSetMap[ruleSetOptions.Tag]; exists {
 			return nil, E.New("duplicate rule-set tag: ", ruleSetOptions.Tag)
 		}
-		ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions)
+		ruleSet, err := R.NewRuleSet(ctx, router, router.logger, ruleSetOptions)
 		if err != nil {
 			return nil, E.Cause(err, "parse rule-set[", i, "]")
 		}
@@ -437,8 +428,12 @@ func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outb
 	r.defaultOutboundForPacketConnection = defaultOutboundForPacketConnection
 	r.outboundByTag = outboundByTag
 	for i, rule := range r.rules {
-		if _, loaded := outboundByTag[rule.Outbound()]; !loaded {
-			return E.New("outbound not found for rule[", i, "]: ", rule.Outbound())
+		routeAction, isRoute := rule.Action().(*R.RuleActionRoute)
+		if !isRoute {
+			continue
+		}
+		if _, loaded := outboundByTag[routeAction.Outbound]; !loaded {
+			return E.New("outbound not found for rule[", i, "]: ", routeAction.Outbound)
 		}
 	}
 	return nil
@@ -804,375 +799,6 @@ func (r *Router) NeedWIFIState() bool {
 	return r.needWIFIState
 }
 
-func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	if r.pauseManager.IsDevicePaused() {
-		return E.New("reject connection to ", metadata.Destination, " while device paused")
-	}
-
-	if metadata.InboundDetour != "" {
-		if metadata.LastInbound == metadata.InboundDetour {
-			return E.New("routing loop on detour: ", metadata.InboundDetour)
-		}
-		detour := r.inboundByTag[metadata.InboundDetour]
-		if detour == nil {
-			return E.New("inbound detour not found: ", metadata.InboundDetour)
-		}
-		injectable, isInjectable := detour.(adapter.InjectableInbound)
-		if !isInjectable {
-			return E.New("inbound detour is not injectable: ", metadata.InboundDetour)
-		}
-		if !common.Contains(injectable.Network(), N.NetworkTCP) {
-			return E.New("inject: TCP unsupported")
-		}
-		metadata.LastInbound = metadata.Inbound
-		metadata.Inbound = metadata.InboundDetour
-		metadata.InboundDetour = ""
-		err := injectable.NewConnection(ctx, conn, metadata)
-		if err != nil {
-			return E.Cause(err, "inject ", detour.Tag())
-		}
-		return nil
-	}
-	conntrack.KillerCheck()
-	metadata.Network = N.NetworkTCP
-	switch metadata.Destination.Fqdn {
-	case mux.Destination.Fqdn:
-		return E.New("global multiplex is deprecated since sing-box v1.7.0, enable multiplex in inbound options instead.")
-	case vmess.MuxDestination.Fqdn:
-		return E.New("global multiplex (v2ray legacy) not supported since sing-box v1.7.0.")
-	case uot.MagicAddress:
-		return E.New("global UoT not supported since sing-box v1.7.0.")
-	case uot.LegacyMagicAddress:
-		return E.New("global UoT (legacy) not supported since sing-box v1.7.0.")
-	}
-
-	if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) {
-		domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr)
-		if !loaded {
-			return E.New("missing fakeip context")
-		}
-		metadata.OriginDestination = metadata.Destination
-		metadata.Destination = M.Socksaddr{
-			Fqdn: domain,
-			Port: metadata.Destination.Port,
-		}
-		metadata.FakeIP = true
-		r.logger.DebugContext(ctx, "found fakeip domain: ", domain)
-	}
-
-	if deadline.NeedAdditionalReadDeadline(conn) {
-		conn = deadline.NewConn(conn)
-	}
-
-	if metadata.InboundOptions.SniffEnabled && !sniff.Skip(metadata) {
-		buffer := buf.NewPacket()
-		err := sniff.PeekStream(
-			ctx,
-			&metadata,
-			conn,
-			buffer,
-			time.Duration(metadata.InboundOptions.SniffTimeout),
-			sniff.TLSClientHello,
-			sniff.HTTPHost,
-			sniff.StreamDomainNameQuery,
-			sniff.SSH,
-			sniff.BitTorrent,
-		)
-		if err == nil {
-			if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) {
-				metadata.Destination = M.Socksaddr{
-					Fqdn: metadata.Domain,
-					Port: metadata.Destination.Port,
-				}
-			}
-			if metadata.Domain != "" {
-				r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
-			} else {
-				r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol)
-			}
-		}
-		if !buffer.IsEmpty() {
-			conn = bufio.NewCachedConn(conn, buffer)
-		} else {
-			buffer.Release()
-		}
-	}
-
-	if r.dnsReverseMapping != nil && metadata.Domain == "" {
-		domain, loaded := r.dnsReverseMapping.Query(metadata.Destination.Addr)
-		if loaded {
-			metadata.Domain = domain
-			r.logger.DebugContext(ctx, "found reserve mapped domain: ", metadata.Domain)
-		}
-	}
-
-	if metadata.Destination.IsFqdn() && dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS {
-		addresses, err := r.Lookup(adapter.WithContext(ctx, &metadata), metadata.Destination.Fqdn, dns.DomainStrategy(metadata.InboundOptions.DomainStrategy))
-		if err != nil {
-			return err
-		}
-		metadata.DestinationAddresses = addresses
-		r.dnsLogger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]")
-	}
-	if metadata.Destination.IsIPv4() {
-		metadata.IPVersion = 4
-	} else if metadata.Destination.IsIPv6() {
-		metadata.IPVersion = 6
-	}
-	ctx, matchedRule, detour, err := r.match(ctx, &metadata, r.defaultOutboundForConnection)
-	if err != nil {
-		return err
-	}
-	if !common.Contains(detour.Network(), N.NetworkTCP) {
-		return E.New("missing supported outbound, closing connection")
-	}
-	if r.clashServer != nil {
-		trackerConn, tracker := r.clashServer.RoutedConnection(ctx, conn, metadata, matchedRule)
-		defer tracker.Leave()
-		conn = trackerConn
-	}
-	if r.v2rayServer != nil {
-		if statsService := r.v2rayServer.StatsService(); statsService != nil {
-			conn = statsService.RoutedConnection(metadata.Inbound, detour.Tag(), metadata.User, conn)
-		}
-	}
-	return detour.NewConnection(ctx, conn, metadata)
-}
-
-func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	if r.pauseManager.IsDevicePaused() {
-		return E.New("reject packet connection to ", metadata.Destination, " while device paused")
-	}
-	if metadata.InboundDetour != "" {
-		if metadata.LastInbound == metadata.InboundDetour {
-			return E.New("routing loop on detour: ", metadata.InboundDetour)
-		}
-		detour := r.inboundByTag[metadata.InboundDetour]
-		if detour == nil {
-			return E.New("inbound detour not found: ", metadata.InboundDetour)
-		}
-		injectable, isInjectable := detour.(adapter.InjectableInbound)
-		if !isInjectable {
-			return E.New("inbound detour is not injectable: ", metadata.InboundDetour)
-		}
-		if !common.Contains(injectable.Network(), N.NetworkUDP) {
-			return E.New("inject: UDP unsupported")
-		}
-		metadata.LastInbound = metadata.Inbound
-		metadata.Inbound = metadata.InboundDetour
-		metadata.InboundDetour = ""
-		err := injectable.NewPacketConnection(ctx, conn, metadata)
-		if err != nil {
-			return E.Cause(err, "inject ", detour.Tag())
-		}
-		return nil
-	}
-	conntrack.KillerCheck()
-	metadata.Network = N.NetworkUDP
-
-	if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) {
-		domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr)
-		if !loaded {
-			return E.New("missing fakeip context")
-		}
-		metadata.OriginDestination = metadata.Destination
-		metadata.Destination = M.Socksaddr{
-			Fqdn: domain,
-			Port: metadata.Destination.Port,
-		}
-		metadata.FakeIP = true
-		r.logger.DebugContext(ctx, "found fakeip domain: ", domain)
-	}
-
-	// Currently we don't have deadline usages for UDP connections
-	/*if deadline.NeedAdditionalReadDeadline(conn) {
-		conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn))
-	}*/
-
-	if metadata.InboundOptions.SniffEnabled || metadata.Destination.Addr.IsUnspecified() {
-		var bufferList []*buf.Buffer
-		for {
-			var (
-				buffer      = buf.NewPacket()
-				destination M.Socksaddr
-				done        = make(chan struct{})
-				err         error
-			)
-			go func() {
-				sniffTimeout := C.ReadPayloadTimeout
-				if metadata.InboundOptions.SniffTimeout > 0 {
-					sniffTimeout = time.Duration(metadata.InboundOptions.SniffTimeout)
-				}
-				conn.SetReadDeadline(time.Now().Add(sniffTimeout))
-				destination, err = conn.ReadPacket(buffer)
-				conn.SetReadDeadline(time.Time{})
-				close(done)
-			}()
-			select {
-			case <-done:
-			case <-ctx.Done():
-				conn.Close()
-				return ctx.Err()
-			}
-			if err != nil {
-				buffer.Release()
-				if !errors.Is(err, os.ErrDeadlineExceeded) {
-					return err
-				}
-			} else {
-				if metadata.Destination.Addr.IsUnspecified() {
-					metadata.Destination = destination
-				}
-				if metadata.InboundOptions.SniffEnabled {
-					if len(bufferList) > 0 {
-						err = sniff.PeekPacket(
-							ctx,
-							&metadata,
-							buffer.Bytes(),
-							sniff.QUICClientHello,
-						)
-					} else {
-						err = sniff.PeekPacket(
-							ctx, &metadata,
-							buffer.Bytes(),
-							sniff.DomainNameQuery,
-							sniff.QUICClientHello,
-							sniff.STUNMessage,
-							sniff.UTP,
-							sniff.UDPTracker,
-							sniff.DTLSRecord)
-					}
-					if E.IsMulti(err, sniff.ErrClientHelloFragmented) && len(bufferList) == 0 {
-						bufferList = append(bufferList, buffer)
-						r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello")
-						continue
-					}
-					if metadata.Protocol != "" {
-						if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) {
-							metadata.Destination = M.Socksaddr{
-								Fqdn: metadata.Domain,
-								Port: metadata.Destination.Port,
-							}
-						}
-						if metadata.Domain != "" && metadata.Client != "" {
-							r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client)
-						} else if metadata.Domain != "" {
-							r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
-						} else if metadata.Client != "" {
-							r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", client: ", metadata.Client)
-						} else {
-							r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol)
-						}
-					}
-				}
-				conn = bufio.NewCachedPacketConn(conn, buffer, destination)
-			}
-			for _, cachedBuffer := range common.Reverse(bufferList) {
-				conn = bufio.NewCachedPacketConn(conn, cachedBuffer, destination)
-			}
-			break
-		}
-	}
-	if r.dnsReverseMapping != nil && metadata.Domain == "" {
-		domain, loaded := r.dnsReverseMapping.Query(metadata.Destination.Addr)
-		if loaded {
-			metadata.Domain = domain
-			r.logger.DebugContext(ctx, "found reserve mapped domain: ", metadata.Domain)
-		}
-	}
-	if metadata.Destination.IsFqdn() && dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS {
-		addresses, err := r.Lookup(adapter.WithContext(ctx, &metadata), metadata.Destination.Fqdn, dns.DomainStrategy(metadata.InboundOptions.DomainStrategy))
-		if err != nil {
-			return err
-		}
-		metadata.DestinationAddresses = addresses
-		r.dnsLogger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]")
-	}
-	if metadata.Destination.IsIPv4() {
-		metadata.IPVersion = 4
-	} else if metadata.Destination.IsIPv6() {
-		metadata.IPVersion = 6
-	}
-	ctx, matchedRule, detour, err := r.match(ctx, &metadata, r.defaultOutboundForPacketConnection)
-	if err != nil {
-		return err
-	}
-	if !common.Contains(detour.Network(), N.NetworkUDP) {
-		return E.New("missing supported outbound, closing packet connection")
-	}
-	if r.clashServer != nil {
-		trackerConn, tracker := r.clashServer.RoutedPacketConnection(ctx, conn, metadata, matchedRule)
-		defer tracker.Leave()
-		conn = trackerConn
-	}
-	if r.v2rayServer != nil {
-		if statsService := r.v2rayServer.StatsService(); statsService != nil {
-			conn = statsService.RoutedPacketConnection(metadata.Inbound, detour.Tag(), metadata.User, conn)
-		}
-	}
-	if metadata.FakeIP {
-		conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
-	}
-	return detour.NewPacketConnection(ctx, conn, metadata)
-}
-
-func (r *Router) match(ctx context.Context, metadata *adapter.InboundContext, defaultOutbound adapter.Outbound) (context.Context, adapter.Rule, adapter.Outbound, error) {
-	matchRule, matchOutbound := r.match0(ctx, metadata, defaultOutbound)
-	if contextOutbound, loaded := outbound.TagFromContext(ctx); loaded {
-		if contextOutbound == matchOutbound.Tag() {
-			return nil, nil, nil, E.New("connection loopback in outbound/", matchOutbound.Type(), "[", matchOutbound.Tag(), "]")
-		}
-	}
-	ctx = outbound.ContextWithTag(ctx, matchOutbound.Tag())
-	return ctx, matchRule, matchOutbound, nil
-}
-
-func (r *Router) match0(ctx context.Context, metadata *adapter.InboundContext, defaultOutbound adapter.Outbound) (adapter.Rule, adapter.Outbound) {
-	if r.processSearcher != nil {
-		var originDestination netip.AddrPort
-		if metadata.OriginDestination.IsValid() {
-			originDestination = metadata.OriginDestination.AddrPort()
-		} else if metadata.Destination.IsIP() {
-			originDestination = metadata.Destination.AddrPort()
-		}
-		processInfo, err := process.FindProcessInfo(r.processSearcher, ctx, metadata.Network, metadata.Source.AddrPort(), originDestination)
-		if err != nil {
-			r.logger.InfoContext(ctx, "failed to search process: ", err)
-		} else {
-			if processInfo.ProcessPath != "" {
-				r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
-			} else if processInfo.PackageName != "" {
-				r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName)
-			} else if processInfo.UserId != -1 {
-				if /*needUserName &&*/ true {
-					osUser, _ := user.LookupId(F.ToString(processInfo.UserId))
-					if osUser != nil {
-						processInfo.User = osUser.Username
-					}
-				}
-				if processInfo.User != "" {
-					r.logger.InfoContext(ctx, "found user: ", processInfo.User)
-				} else {
-					r.logger.InfoContext(ctx, "found user id: ", processInfo.UserId)
-				}
-			}
-			metadata.ProcessInfo = processInfo
-		}
-	}
-	for i, rule := range r.rules {
-		metadata.ResetRuleCache()
-		if rule.Match(metadata) {
-			detour := rule.Outbound()
-			r.logger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour)
-			if outbound, loaded := r.Outbound(detour); loaded {
-				return rule, outbound
-			}
-			r.logger.ErrorContext(ctx, "outbound not found: ", detour)
-		}
-	}
-	return nil, defaultOutbound
-}
-
 func (r *Router) InterfaceFinder() control.InterfaceFinder {
 	return r.interfaceFinder
 }

+ 10 - 10
route/rule_abstract.go → route/rule/rule_abstract.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"io"
@@ -20,7 +20,7 @@ type abstractDefaultRule struct {
 	allItems                []RuleItem
 	ruleSetItem             RuleItem
 	invert                  bool
-	outbound                string
+	action                  adapter.RuleAction
 }
 
 func (r *abstractDefaultRule) Type() string {
@@ -150,8 +150,8 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
 	return !r.invert
 }
 
-func (r *abstractDefaultRule) Outbound() string {
-	return r.outbound
+func (r *abstractDefaultRule) Action() adapter.RuleAction {
+	return r.action
 }
 
 func (r *abstractDefaultRule) String() string {
@@ -163,10 +163,10 @@ func (r *abstractDefaultRule) String() string {
 }
 
 type abstractLogicalRule struct {
-	rules    []adapter.HeadlessRule
-	mode     string
-	invert   bool
-	outbound string
+	rules  []adapter.HeadlessRule
+	mode   string
+	invert bool
+	action adapter.RuleAction
 }
 
 func (r *abstractLogicalRule) Type() string {
@@ -231,8 +231,8 @@ func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
 	}
 }
 
-func (r *abstractLogicalRule) Outbound() string {
-	return r.outbound
+func (r *abstractLogicalRule) Action() adapter.RuleAction {
+	return r.action
 }
 
 func (r *abstractLogicalRule) String() string {

+ 228 - 0
route/rule/rule_action.go

@@ -0,0 +1,228 @@
+package rule
+
+import (
+	"net/netip"
+	"strings"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/sniff"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-dns"
+	E "github.com/sagernet/sing/common/exceptions"
+	F "github.com/sagernet/sing/common/format"
+)
+
+func NewRuleAction(action option.RuleAction) (adapter.RuleAction, error) {
+	switch action.Action {
+	case C.RuleActionTypeRoute:
+		return &RuleActionRoute{
+			Outbound:                  action.RouteOptions.Outbound,
+			UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping,
+		}, nil
+	case C.RuleActionTypeReturn:
+		return &RuleActionReject{}, nil
+	case C.RuleActionTypeReject:
+		return &RuleActionReject{
+			Method: string(action.RejectOptions.Method),
+		}, nil
+	case C.RuleActionTypeHijackDNS:
+		return &RuleActionHijackDNS{}, nil
+	case C.RuleActionTypeSniff:
+		sniffAction := &RuleActionSniff{
+			snifferNames: action.SniffOptions.Sniffer,
+			Timeout:      time.Duration(action.SniffOptions.Timeout),
+		}
+		return sniffAction, sniffAction.build()
+	case C.RuleActionTypeResolve:
+		return &RuleActionResolve{
+			Strategy: dns.DomainStrategy(action.ResolveOptions.Strategy),
+			Server:   action.ResolveOptions.Server,
+		}, nil
+	default:
+		panic(F.ToString("unknown rule action: ", action.Action))
+	}
+}
+
+func NewDNSRuleAction(action option.DNSRuleAction) adapter.RuleAction {
+	switch action.Action {
+	case C.RuleActionTypeRoute:
+		return &RuleActionDNSRoute{
+			Server:       action.RouteOptions.Server,
+			DisableCache: action.RouteOptions.DisableCache,
+			RewriteTTL:   action.RouteOptions.RewriteTTL,
+			ClientSubnet: action.RouteOptions.ClientSubnet.Build(),
+		}
+	case C.RuleActionTypeReturn:
+		return &RuleActionReturn{}
+	case C.RuleActionTypeReject:
+		return &RuleActionReject{
+			Method: string(action.RejectOptions.Method),
+		}
+	default:
+		panic(F.ToString("unknown rule action: ", action.Action))
+	}
+}
+
+type RuleActionRoute struct {
+	Outbound                  string
+	UDPDisableDomainUnmapping bool
+}
+
+func (r *RuleActionRoute) Type() string {
+	return C.RuleActionTypeRoute
+}
+
+func (r *RuleActionRoute) String() string {
+	return F.ToString("route(", r.Outbound, ")")
+}
+
+type RuleActionDNSRoute struct {
+	Server       string
+	DisableCache bool
+	RewriteTTL   *uint32
+	ClientSubnet netip.Prefix
+}
+
+func (r *RuleActionDNSRoute) Type() string {
+	return C.RuleActionTypeRoute
+}
+
+func (r *RuleActionDNSRoute) String() string {
+	return F.ToString("route(", r.Server, ")")
+}
+
+type RuleActionReturn struct{}
+
+func (r *RuleActionReturn) Type() string {
+	return C.RuleActionTypeReturn
+}
+
+func (r *RuleActionReturn) String() string {
+	return "return"
+}
+
+type RuleActionReject struct {
+	Method string
+}
+
+func (r *RuleActionReject) Type() string {
+	return C.RuleActionTypeReject
+}
+
+func (r *RuleActionReject) String() string {
+	if r.Method == C.RuleActionRejectMethodDefault {
+		return "reject"
+	}
+	return F.ToString("reject(", r.Method, ")")
+}
+
+type RuleActionHijackDNS struct{}
+
+func (r *RuleActionHijackDNS) Type() string {
+	return C.RuleActionTypeHijackDNS
+}
+
+func (r *RuleActionHijackDNS) String() string {
+	return "hijack-dns"
+}
+
+type RuleActionSniff struct {
+	snifferNames   []string
+	StreamSniffers []sniff.StreamSniffer
+	PacketSniffers []sniff.PacketSniffer
+	Timeout        time.Duration
+	// Deprecated
+	OverrideDestination bool
+}
+
+func (r *RuleActionSniff) Type() string {
+	return C.RuleActionTypeSniff
+}
+
+func (r *RuleActionSniff) build() error {
+	if len(r.StreamSniffers) > 0 || len(r.PacketSniffers) > 0 {
+		return nil
+	}
+	if len(r.snifferNames) > 0 {
+		for _, name := range r.snifferNames {
+			switch name {
+			case C.ProtocolTLS:
+				r.StreamSniffers = append(r.StreamSniffers, sniff.TLSClientHello)
+			case C.ProtocolHTTP:
+				r.StreamSniffers = append(r.StreamSniffers, sniff.HTTPHost)
+			case C.ProtocolQUIC:
+				r.PacketSniffers = append(r.PacketSniffers, sniff.QUICClientHello)
+			case C.ProtocolDNS:
+				r.StreamSniffers = append(r.StreamSniffers, sniff.StreamDomainNameQuery)
+				r.PacketSniffers = append(r.PacketSniffers, sniff.DomainNameQuery)
+			case C.ProtocolSTUN:
+				r.PacketSniffers = append(r.PacketSniffers, sniff.STUNMessage)
+			case C.ProtocolBitTorrent:
+				r.StreamSniffers = append(r.StreamSniffers, sniff.BitTorrent)
+				r.PacketSniffers = append(r.PacketSniffers, sniff.UTP)
+				r.PacketSniffers = append(r.PacketSniffers, sniff.UDPTracker)
+			case C.ProtocolDTLS:
+				r.PacketSniffers = append(r.PacketSniffers, sniff.DTLSRecord)
+			case C.ProtocolSSH:
+				r.StreamSniffers = append(r.StreamSniffers, sniff.SSH)
+			case C.ProtocolRDP:
+				r.StreamSniffers = append(r.StreamSniffers, sniff.RDP)
+			default:
+				return E.New("unknown sniffer: ", name)
+			}
+		}
+	} else {
+		r.StreamSniffers = []sniff.StreamSniffer{
+			sniff.TLSClientHello,
+			sniff.HTTPHost,
+			sniff.StreamDomainNameQuery,
+			sniff.BitTorrent,
+			sniff.SSH,
+			sniff.RDP,
+		}
+		r.PacketSniffers = []sniff.PacketSniffer{
+			sniff.DomainNameQuery,
+			sniff.QUICClientHello,
+			sniff.STUNMessage,
+			sniff.UTP,
+			sniff.UDPTracker,
+			sniff.DTLSRecord,
+		}
+	}
+	return nil
+}
+
+func (r *RuleActionSniff) String() string {
+	if len(r.snifferNames) == 0 && r.Timeout == 0 {
+		return "sniff"
+	} else if len(r.snifferNames) > 0 && r.Timeout == 0 {
+		return F.ToString("sniff(", strings.Join(r.snifferNames, ","), ")")
+	} else if len(r.snifferNames) == 0 && r.Timeout > 0 {
+		return F.ToString("sniff(", r.Timeout.String(), ")")
+	} else {
+		return F.ToString("sniff(", strings.Join(r.snifferNames, ","), ",", r.Timeout.String(), ")")
+	}
+}
+
+type RuleActionResolve struct {
+	Strategy dns.DomainStrategy
+	Server   string
+}
+
+func (r *RuleActionResolve) Type() string {
+	return C.RuleActionTypeResolve
+}
+
+func (r *RuleActionResolve) String() string {
+	if r.Strategy == dns.DomainStrategyAsIS && r.Server == "" {
+		return F.ToString("resolve")
+	} else if r.Strategy != dns.DomainStrategyAsIS && r.Server == "" {
+		return F.ToString("resolve(", option.DomainStrategy(r.Strategy).String(), ")")
+	} else if r.Strategy == dns.DomainStrategyAsIS && r.Server != "" {
+		return F.ToString("resolve(", r.Server, ")")
+	} else {
+		return F.ToString("resolve(", option.DomainStrategy(r.Strategy).String(), ",", r.Server, ")")
+	}
+}

+ 31 - 17
route/rule_default.go → route/rule/rule_default.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"context"
@@ -17,16 +17,22 @@ func NewRule(ctx context.Context, router adapter.Router, logger log.ContextLogge
 		if !options.DefaultOptions.IsValid() {
 			return nil, E.New("missing conditions")
 		}
-		if options.DefaultOptions.Outbound == "" && checkOutbound {
-			return nil, E.New("missing outbound field")
+		switch options.DefaultOptions.Action {
+		case "", C.RuleActionTypeRoute:
+			if options.DefaultOptions.RouteOptions.Outbound == "" && checkOutbound {
+				return nil, E.New("missing outbound field")
+			}
 		}
 		return NewDefaultRule(ctx, router, logger, options.DefaultOptions)
 	case C.RuleTypeLogical:
 		if !options.LogicalOptions.IsValid() {
 			return nil, E.New("missing conditions")
 		}
-		if options.LogicalOptions.Outbound == "" && checkOutbound {
-			return nil, E.New("missing outbound field")
+		switch options.LogicalOptions.Action {
+		case "", C.RuleActionTypeRoute:
+			if options.LogicalOptions.RouteOptions.Outbound == "" && checkOutbound {
+				return nil, E.New("missing outbound field")
+			}
 		}
 		return NewLogicalRule(ctx, router, logger, options.LogicalOptions)
 	default:
@@ -46,10 +52,14 @@ type RuleItem interface {
 }
 
 func NewDefaultRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.DefaultRule) (*DefaultRule, error) {
+	action, err := NewRuleAction(options.RuleAction)
+	if err != nil {
+		return nil, E.Cause(err, "action")
+	}
 	rule := &DefaultRule{
 		abstractDefaultRule{
-			invert:   options.Invert,
-			outbound: options.Outbound,
+			invert: options.Invert,
+			action: action,
 		},
 	}
 	if len(options.Inbound) > 0 {
@@ -244,27 +254,31 @@ type LogicalRule struct {
 }
 
 func NewLogicalRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) {
-	r := &LogicalRule{
+	action, err := NewRuleAction(options.RuleAction)
+	if err != nil {
+		return nil, E.Cause(err, "action")
+	}
+	rule := &LogicalRule{
 		abstractLogicalRule{
-			rules:    make([]adapter.HeadlessRule, len(options.Rules)),
-			invert:   options.Invert,
-			outbound: options.Outbound,
+			rules:  make([]adapter.HeadlessRule, len(options.Rules)),
+			invert: options.Invert,
+			action: action,
 		},
 	}
 	switch options.Mode {
 	case C.LogicalTypeAnd:
-		r.mode = C.LogicalTypeAnd
+		rule.mode = C.LogicalTypeAnd
 	case C.LogicalTypeOr:
-		r.mode = C.LogicalTypeOr
+		rule.mode = C.LogicalTypeOr
 	default:
 		return nil, E.New("unknown logical mode: ", options.Mode)
 	}
-	for i, subRule := range options.Rules {
-		rule, err := NewRule(ctx, router, logger, subRule, false)
+	for i, subOptions := range options.Rules {
+		subRule, err := NewRule(ctx, router, logger, subOptions, false)
 		if err != nil {
 			return nil, E.Cause(err, "sub rule[", i, "]")
 		}
-		r.rules[i] = rule
+		rule.rules[i] = subRule
 	}
-	return r, nil
+	return rule, nil
 }

+ 20 - 43
route/rule_dns.go → route/rule/rule_dns.go

@@ -1,8 +1,7 @@
-package route
+package rule
 
 import (
 	"context"
-	"net/netip"
 
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
@@ -19,16 +18,22 @@ func NewDNSRule(ctx context.Context, router adapter.Router, logger log.ContextLo
 		if !options.DefaultOptions.IsValid() {
 			return nil, E.New("missing conditions")
 		}
-		if options.DefaultOptions.Server == "" && checkServer {
-			return nil, E.New("missing server field")
+		switch options.DefaultOptions.Action {
+		case "", C.RuleActionTypeRoute:
+			if options.DefaultOptions.RouteOptions.Server == "" && checkServer {
+				return nil, E.New("missing server field")
+			}
 		}
 		return NewDefaultDNSRule(ctx, router, logger, options.DefaultOptions)
 	case C.RuleTypeLogical:
 		if !options.LogicalOptions.IsValid() {
 			return nil, E.New("missing conditions")
 		}
-		if options.LogicalOptions.Server == "" && checkServer {
-			return nil, E.New("missing server field")
+		switch options.LogicalOptions.Action {
+		case "", C.RuleActionTypeRoute:
+			if options.LogicalOptions.RouteOptions.Server == "" && checkServer {
+				return nil, E.New("missing server field")
+			}
 		}
 		return NewLogicalDNSRule(ctx, router, logger, options.LogicalOptions)
 	default:
@@ -40,20 +45,14 @@ var _ adapter.DNSRule = (*DefaultDNSRule)(nil)
 
 type DefaultDNSRule struct {
 	abstractDefaultRule
-	disableCache bool
-	rewriteTTL   *uint32
-	clientSubnet *netip.Prefix
 }
 
 func NewDefaultDNSRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) {
 	rule := &DefaultDNSRule{
 		abstractDefaultRule: abstractDefaultRule{
-			invert:   options.Invert,
-			outbound: options.Server,
+			invert: options.Invert,
+			action: NewDNSRuleAction(options.DNSRuleAction),
 		},
-		disableCache: options.DisableCache,
-		rewriteTTL:   options.RewriteTTL,
-		clientSubnet: (*netip.Prefix)(options.ClientSubnet),
 	}
 	if len(options.Inbound) > 0 {
 		item := NewInboundRule(options.Inbound)
@@ -245,16 +244,8 @@ func NewDefaultDNSRule(ctx context.Context, router adapter.Router, logger log.Co
 	return rule, nil
 }
 
-func (r *DefaultDNSRule) DisableCache() bool {
-	return r.disableCache
-}
-
-func (r *DefaultDNSRule) RewriteTTL() *uint32 {
-	return r.rewriteTTL
-}
-
-func (r *DefaultDNSRule) ClientSubnet() *netip.Prefix {
-	return r.clientSubnet
+func (r *DefaultDNSRule) Action() adapter.RuleAction {
+	return r.action
 }
 
 func (r *DefaultDNSRule) WithAddressLimit() bool {
@@ -289,21 +280,15 @@ var _ adapter.DNSRule = (*LogicalDNSRule)(nil)
 
 type LogicalDNSRule struct {
 	abstractLogicalRule
-	disableCache bool
-	rewriteTTL   *uint32
-	clientSubnet *netip.Prefix
 }
 
 func NewLogicalDNSRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) {
 	r := &LogicalDNSRule{
 		abstractLogicalRule: abstractLogicalRule{
-			rules:    make([]adapter.HeadlessRule, len(options.Rules)),
-			invert:   options.Invert,
-			outbound: options.Server,
+			rules:  make([]adapter.HeadlessRule, len(options.Rules)),
+			invert: options.Invert,
+			action: NewDNSRuleAction(options.DNSRuleAction),
 		},
-		disableCache: options.DisableCache,
-		rewriteTTL:   options.RewriteTTL,
-		clientSubnet: (*netip.Prefix)(options.ClientSubnet),
 	}
 	switch options.Mode {
 	case C.LogicalTypeAnd:
@@ -323,16 +308,8 @@ func NewLogicalDNSRule(ctx context.Context, router adapter.Router, logger log.Co
 	return r, nil
 }
 
-func (r *LogicalDNSRule) DisableCache() bool {
-	return r.disableCache
-}
-
-func (r *LogicalDNSRule) RewriteTTL() *uint32 {
-	return r.rewriteTTL
-}
-
-func (r *LogicalDNSRule) ClientSubnet() *netip.Prefix {
-	return r.clientSubnet
+func (r *LogicalDNSRule) Action() adapter.RuleAction {
+	return r.action
 }
 
 func (r *LogicalDNSRule) WithAddressLimit() bool {

+ 1 - 1
route/rule_headless.go → route/rule/rule_headless.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"github.com/sagernet/sing-box/adapter"

+ 1 - 1
route/rule_item_adguard.go → route/rule/rule_item_adguard.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_auth_user.go → route/rule/rule_item_auth_user.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_cidr.go → route/rule/rule_item_cidr.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"net/netip"

+ 1 - 1
route/rule_item_clash_mode.go → route/rule/rule_item_clash_mode.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_client.go → route/rule/rule_item_client.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_domain.go → route/rule/rule_item_domain.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_domain_keyword.go → route/rule/rule_item_domain_keyword.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_domain_regex.go → route/rule/rule_item_domain_regex.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"regexp"

+ 1 - 1
route/rule_item_geoip.go → route/rule/rule_item_geoip.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"net/netip"

+ 1 - 1
route/rule_item_geosite.go → route/rule/rule_item_geosite.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_inbound.go → route/rule/rule_item_inbound.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_ip_is_private.go → route/rule/rule_item_ip_is_private.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"net/netip"

+ 1 - 1
route/rule_item_ipversion.go → route/rule/rule_item_ipversion.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"github.com/sagernet/sing-box/adapter"

+ 1 - 1
route/rule_item_network.go → route/rule/rule_item_network.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_outbound.go → route/rule/rule_item_outbound.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_package_name.go → route/rule/rule_item_package_name.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_port.go → route/rule/rule_item_port.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_port_range.go → route/rule/rule_item_port_range.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strconv"

+ 1 - 1
route/rule_item_process_name.go → route/rule/rule_item_process_name.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"path/filepath"

+ 1 - 1
route/rule_item_process_path.go → route/rule/rule_item_process_path.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_process_path_regex.go → route/rule/rule_item_process_path_regex.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"regexp"

+ 1 - 1
route/rule_item_protocol.go → route/rule/rule_item_protocol.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_query_type.go → route/rule/rule_item_query_type.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_rule_set.go → route/rule/rule_item_rule_set.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_user.go → route/rule/rule_item_user.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_user_id.go → route/rule/rule_item_user_id.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

+ 1 - 1
route/rule_item_wifi_bssid.go → route/rule/rule_item_wifi_bssid.go

@@ -1,4 +1,4 @@
-package route
+package rule
 
 import (
 	"strings"

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů