diff --git a/adapter/outbound.go b/adapter/outbound.go index 03c99d51..0d4b6792 100644 --- a/adapter/outbound.go +++ b/adapter/outbound.go @@ -19,6 +19,11 @@ type Outbound interface { NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error } +type SniffOutbound interface { + Outbound + UseSniffedDestination() bool +} + type IPOutbound interface { Outbound NewIPConnection(ctx context.Context, conn tun.RouteContext, metadata InboundContext) (tun.DirectDestination, error) diff --git a/docs/configuration/outbound/socks.md b/docs/configuration/outbound/socks.md index 94d83fe5..b91f52d7 100644 --- a/docs/configuration/outbound/socks.md +++ b/docs/configuration/outbound/socks.md @@ -14,6 +14,7 @@ "password": "admin", "network": "udp", "udp_over_tcp": false | {}, + "use_sniffed_destination": false ... // Dial Fields } @@ -61,6 +62,10 @@ UDP over TCP protocol settings. See [UDP Over TCP](/configuration/shared/udp-over-tcp) for details. +#### use_sniffed_destination + +When an inbound request is routed to the outbound, the detected domain name will be used to override the connection target address before establishing a connection. This option is only effective when `sniff` is set to `true` and `sniff_override_destination` is `false` in the inbound. + ### Dial Fields See [Dial Fields](/configuration/shared/dial) for details. diff --git a/docs/configuration/outbound/socks.zh.md b/docs/configuration/outbound/socks.zh.md index 75548da7..710955c1 100644 --- a/docs/configuration/outbound/socks.zh.md +++ b/docs/configuration/outbound/socks.zh.md @@ -14,6 +14,7 @@ "password": "admin", "network": "udp", "udp_over_tcp": false | {}, + "use_sniffed_destination": false ... // 拨号字段 } @@ -61,6 +62,10 @@ UDP over TCP 配置。 参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp)。 +#### use_sniffed_destination + +当入站请求路由到该出站时,在建立连接前会用探测出的域名覆盖连接目标地址,仅当入站设置了`sniff`为`true`同时`sniff_override_destination`为`false`时该选项有效。 + ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/option/outbound.go b/option/outbound.go index abea81a0..4985997a 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -144,6 +144,10 @@ func (o ServerOptions) Build() M.Socksaddr { return M.ParseSocksaddrHostPort(o.Server, o.ServerPort) } +type SniffOptions struct { + UseSniffedDestination bool `json:"use_sniffed_destination,omitempty"` +} + type MultiplexOptions struct { Enabled bool `json:"enabled,omitempty"` Protocol string `json:"protocol,omitempty"` diff --git a/option/simple.go b/option/simple.go index 60cf074b..d0eb9260 100644 --- a/option/simple.go +++ b/option/simple.go @@ -17,6 +17,7 @@ type HTTPMixedInboundOptions struct { type SocksOutboundOptions struct { DialerOptions ServerOptions + SniffOptions Version string `json:"version,omitempty"` Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` diff --git a/outbound/socks.go b/outbound/socks.go index 3fc4b6e9..a609bd20 100644 --- a/outbound/socks.go +++ b/outbound/socks.go @@ -17,13 +17,18 @@ import ( "github.com/sagernet/sing/protocol/socks" ) -var _ adapter.Outbound = (*Socks)(nil) +var _ adapter.SniffOutbound = (*Socks)(nil) type Socks struct { myOutboundAdapter - client *socks.Client - resolve bool - uotClient *uot.Client + client *socks.Client + resolve bool + uotClient *uot.Client + useSniffedDestination bool +} + +func (h *Socks) UseSniffedDestination() bool { + return h.useSniffedDestination } func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, options option.SocksOutboundOptions) (*Socks, error) { @@ -45,8 +50,9 @@ func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, optio logger: logger, tag: tag, }, - client: socks.NewClient(dialer.New(router, options.DialerOptions), options.ServerOptions.Build(), version, options.Username, options.Password), - resolve: version == socks.Version4, + client: socks.NewClient(dialer.New(router, options.DialerOptions), options.ServerOptions.Build(), version, options.Username, options.Password), + resolve: version == socks.Version4, + useSniffedDestination: options.UseSniffedDestination, } uotOptions := common.PtrValueOrDefault(options.UDPOverTCPOptions) if uotOptions.Enabled { diff --git a/route/router.go b/route/router.go index d46ca16c..47bb9951 100644 --- a/route/router.go +++ b/route/router.go @@ -657,6 +657,7 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad r.logger.DebugContext(ctx, "found fakeip domain: ", domain) } + var sniffedDestination M.Socksaddr if metadata.InboundOptions.SniffEnabled { buffer := buf.NewPacket() buffer.FullReset() @@ -664,11 +665,14 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad if sniffMetadata != nil { metadata.Protocol = sniffMetadata.Protocol metadata.Domain = sniffMetadata.Domain - if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) { - metadata.Destination = M.Socksaddr{ + if M.IsDomainName(metadata.Domain) { + sniffedDestination = M.Socksaddr{ Fqdn: metadata.Domain, Port: metadata.Destination.Port, } + if metadata.InboundOptions.SniffOverrideDestination { + metadata.Destination = sniffedDestination + } } if metadata.Domain != "" { r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) @@ -706,6 +710,13 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad if !common.Contains(detour.Network(), N.NetworkTCP) { return E.New("missing supported outbound, closing connection") } + if metadata.InboundOptions.SniffEnabled && !metadata.InboundOptions.SniffOverrideDestination && sniffedDestination.IsValid() { + sniffOutbound, loaded := detour.(adapter.SniffOutbound) + if loaded && sniffOutbound.UseSniffedDestination() { + r.logger.DebugContext(ctx, "use sniffed destination, detor: ", detour.Tag(), ", domain: ", metadata.Domain) + metadata.Destination = sniffedDestination + } + } if r.clashServer != nil { trackerConn, tracker := r.clashServer.RoutedConnection(ctx, conn, metadata, matchedRule) defer tracker.Leave() @@ -760,6 +771,7 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m r.logger.DebugContext(ctx, "found fakeip domain: ", domain) } + var sniffedDestination M.Socksaddr if metadata.InboundOptions.SniffEnabled { buffer := buf.NewPacket() buffer.FullReset() @@ -772,11 +784,14 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m if sniffMetadata != nil { metadata.Protocol = sniffMetadata.Protocol metadata.Domain = sniffMetadata.Domain - if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) { - metadata.Destination = M.Socksaddr{ + if M.IsDomainName(metadata.Domain) { + sniffedDestination = M.Socksaddr{ Fqdn: metadata.Domain, Port: metadata.Destination.Port, } + if metadata.InboundOptions.SniffOverrideDestination { + metadata.Destination = sniffedDestination + } } if metadata.Domain != "" { r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) @@ -808,6 +823,13 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m if !common.Contains(detour.Network(), N.NetworkUDP) { return E.New("missing supported outbound, closing packet connection") } + if metadata.InboundOptions.SniffEnabled && !metadata.InboundOptions.SniffOverrideDestination && sniffedDestination.IsValid() { + sniffOutbound, loaded := detour.(adapter.SniffOutbound) + if loaded && sniffOutbound.UseSniffedDestination() { + r.logger.DebugContext(ctx, "use sniffed destination, detor: ", detour.Tag(), ", domain: ", metadata.Domain) + metadata.Destination = sniffedDestination + } + } if r.clashServer != nil { trackerConn, tracker := r.clashServer.RoutedPacketConnection(ctx, conn, metadata, matchedRule) defer tracker.Leave()