diff --git a/adapter/outbound.go b/adapter/outbound.go index 91fb9c650..3108b756b 100644 --- a/adapter/outbound.go +++ b/adapter/outbound.go @@ -3,7 +3,6 @@ package adapter import ( "context" "net/netip" - "time" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -27,9 +26,10 @@ type OutboundWithPreferredRoutes interface { PreferredAddress(address netip.Addr) bool } -type DirectRouteOutbound interface { +type FlowOutbound interface { Outbound - NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) + tun.Port + SupportsFlow(network string) bool } type OutboundRegistry interface { diff --git a/adapter/router.go b/adapter/router.go index 1264bcae1..ba5fd3612 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -3,9 +3,11 @@ package adapter import ( "context" "net" - "time" + "net/netip" "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/gtcpip/header" + M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/x/list" @@ -15,7 +17,7 @@ import ( type Router interface { Lifecycle ConnectionRouter - PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error) + PreMatch(metadata InboundContext) PreMatchResult ConnectionRouterEx RuleSet(tag string) (RuleSet, bool) Rules() []Rule @@ -26,6 +28,72 @@ type Router interface { ResetNetwork() } +type PreMatchAction uint8 + +const ( + PreMatchContinue PreMatchAction = iota + PreMatchFlow + PreMatchReject + PreMatchDrop + PreMatchBypass +) + +type PreMatchResult struct { + Action PreMatchAction + Outbound Outbound + Destination netip.AddrPort +} + +func JudgeFlow(router Router, inbound string, inboundType string, network uint8, source netip.AddrPort, destination netip.AddrPort) tun.FlowVerdict { + var networkName string + switch network { + case uint8(header.TCPProtocolNumber): + networkName = N.NetworkTCP + case uint8(header.UDPProtocolNumber): + networkName = N.NetworkUDP + case uint8(header.ICMPv4ProtocolNumber), uint8(header.ICMPv6ProtocolNumber): + networkName = N.NetworkICMP + default: + return tun.FlowVerdict{Action: tun.ActionAccept} + } + metadata := InboundContext{ + Inbound: inbound, + InboundType: inboundType, + Network: networkName, + Source: M.SocksaddrFromNetIP(source), + Destination: M.SocksaddrFromNetIP(destination), + } + if networkName == N.NetworkICMP { + metadata.Source.Port = 0 + metadata.Destination.Port = 0 + } + result := router.PreMatch(metadata) + switch result.Action { + case PreMatchFlow: + port, isPort := result.Outbound.(tun.Port) + if !isPort { + return tun.FlowVerdict{Action: tun.ActionAccept} + } + verdict := tun.FlowVerdict{Action: tun.ActionFlow, Port: port} + if result.Destination.IsValid() { + destinationPort := result.Destination.Port() + if networkName == N.NetworkICMP { + destinationPort = destination.Port() + } + verdict.Destination = netip.AddrPortFrom(result.Destination.Addr(), destinationPort) + } + return verdict + case PreMatchReject: + return tun.FlowVerdict{Action: tun.ActionReject} + case PreMatchDrop: + return tun.FlowVerdict{Action: tun.ActionDrop} + case PreMatchBypass: + return tun.FlowVerdict{Action: tun.ActionBypass} + default: + return tun.FlowVerdict{Action: tun.ActionAccept} + } +} + type ConnectionTracker interface { RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) net.Conn RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) N.PacketConn diff --git a/dns/router.go b/dns/router.go index 7c13c551d..2143f20e4 100644 --- a/dns/router.go +++ b/dns/router.go @@ -15,7 +15,6 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" R "github.com/sagernet/sing-box/route/rule" - "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" @@ -500,7 +499,7 @@ func (r *Router) exchangeWithRules(ctx context.Context, rules []adapter.DNSRule, case C.RuleActionRejectMethodDrop: return exchangeWithRulesResult{ rejectAction: action, - err: tun.ErrDrop, + err: R.ErrDrop, } } case *R.RuleActionPredefined: @@ -691,7 +690,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte Question: []mDNS.Question{message.Question[0]}, }, nil case C.RuleActionRejectMethodDrop: - return nil, tun.ErrDrop + return nil, R.ErrDrop } case *R.RuleActionPredefined: err = nil @@ -767,6 +766,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ r.logger.DebugContext(ctx, "response rejected for ", domain) } else if R.IsRejected(err) { r.logger.DebugContext(ctx, "lookup rejected for ", domain) + } else if errors.Is(err, ErrNotCached) { + r.logger.DebugContext(ctx, "cache-only lookup missed for ", domain) } else { r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain)) } diff --git a/docs/configuration/shared/pre-match.md b/docs/configuration/shared/pre-match.md index a0faf5772..80738b707 100644 --- a/docs/configuration/shared/pre-match.md +++ b/docs/configuration/shared/pre-match.md @@ -4,6 +4,10 @@ icon: material/new-box # Pre-match +!!! quote "Changes in sing-box 1.14.0" + + :material-alert: [route](#route) + !!! quote "Changes in sing-box 1.13.0" :material-plus: [bypass](#bypass) @@ -12,7 +16,7 @@ Pre-match is rule matching that runs before the connection is established. ### How it works -When TUN receives a connection request, the connection has not yet been established, +When an L3 inbound (TUN, WireGuard, or Tailscale) receives a connection request, the connection has not yet been established, so no connection data can be read. In this phase, sing-box runs the routing rules in pre-match mode. Since connection data is unavailable, only actions that do not require connection data can be executed. @@ -28,7 +32,24 @@ See [reject](/configuration/route/rule_action/#reject) for details. #### route -Route ICMP connections to the specified outbound for direct reply. +!!! quote "Changes in sing-box 1.14.0" + + Since sing-box 1.14.0, TCP and UDP connections can also be forwarded at L3; + previously only ICMP connections were supported. + +Forward connections directly at L3 to the specified outbound, +without going through L3 to L4 translation. + +Supported targets: + +- ICMP connections: Direct outbounds and WireGuard / Tailscale endpoints. +- TCP and UDP connections: WireGuard and Tailscale endpoints. + +L3 forwarding also applies when no rule matches and the default outbound is a supported +target; for outbound groups, the currently selected outbound is used. + +FakeIP destinations require a `resolve` action performed in pre-match, +otherwise connections will be rejected. See [route](/configuration/route/rule_action/#route) for details. diff --git a/docs/configuration/shared/pre-match.zh.md b/docs/configuration/shared/pre-match.zh.md index 06d78f106..9f096f909 100644 --- a/docs/configuration/shared/pre-match.zh.md +++ b/docs/configuration/shared/pre-match.zh.md @@ -4,6 +4,10 @@ icon: material/new-box # 预匹配 +!!! quote "sing-box 1.14.0 中的更改" + + :material-alert: [route](#route) + !!! quote "sing-box 1.13.0 中的更改" :material-plus: [bypass](#bypass) @@ -12,7 +16,7 @@ icon: material/new-box ### 工作原理 -当 TUN 收到连接请求时,连接尚未建立,因此无法读取连接数据。在此阶段,sing-box 在预匹配模式下运行路由规则。 +当 L3 入站(TUN、WireGuard 或 Tailscale)收到连接请求时,连接尚未建立,因此无法读取连接数据。在此阶段,sing-box 在预匹配模式下运行路由规则。 由于连接数据不可用,只有不需要连接数据的动作才能执行。当规则匹配到需要已建立连接的动作时,预匹配将在该规则处停止。 @@ -26,7 +30,20 @@ icon: material/new-box #### route -将 ICMP 连接路由到指定出站以直接回复。 +!!! quote "sing-box 1.14.0 中的更改" + + 自 sing-box 1.14.0 起,TCP 和 UDP 连接也可以在 L3 转发;此前仅支持 ICMP 连接。 + +将连接直接在 L3 转发到指定出站,不经过 L3 到 L4 转换。 + +支持的目标: + +- ICMP 连接:direct 出站和 WireGuard / Tailscale 端点。 +- TCP 和 UDP 连接:WireGuard 和 Tailscale 端点。 + +当没有规则匹配且默认出站为受支持的目标时,L3 转发同样生效;对于出站组,使用当前选中的出站。 + +FakeIP 目标需要在预匹配中先执行 `resolve` 动作,否则连接将被拒绝。 详情参阅 [route](/zh/configuration/route/rule_action/#route)。 diff --git a/go.mod b/go.mod index 3705665fb..554e02688 100644 --- a/go.mod +++ b/go.mod @@ -42,19 +42,19 @@ require ( github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d - github.com/sagernet/sing-cloudflared v0.1.2 + github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 github.com/sagernet/sing-mux v0.3.5 github.com/sagernet/sing-quic v0.6.4-0.20260709030527-cff0b763485d github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1 github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814 - github.com/sagernet/sing-tun v0.8.12-0.20260629021427-b3c6babbd353 + github.com/sagernet/sing-tun v0.8.12-0.20260706130635-4b50856586ba github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 - github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260527101438-dc40932c32d9 - github.com/sagernet/wireguard-go v0.0.3 + github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3 + github.com/sagernet/wireguard-go v0.0.5-0.20260706130655-57baac9504a8 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 10c5f94ef..a6160ee72 100644 --- a/go.sum +++ b/go.sum @@ -260,8 +260,8 @@ github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgj github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d h1:BhsQU0Iug1tU4xR52cjm8Sc+LBo+KwdyLTRn3ie9moo= github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= -github.com/sagernet/sing-cloudflared v0.1.2 h1:rEz98+q2nvbNlQXaDXJQlRKngK81FHK+rm7pNCSLXB4= -github.com/sagernet/sing-cloudflared v0.1.2/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= +github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8= +github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg= github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI= github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.4-0.20260709030527-cff0b763485d h1:peXYrU5srsiMDUVOFdEoaQ6mRdywGXr6poWePWGmU0M= @@ -274,18 +274,18 @@ github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkG github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814 h1:xfnkRpjVRVeJhVvDZA8PzTLlKGTb1o2kdI4uv1YymXo= github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY= -github.com/sagernet/sing-tun v0.8.12-0.20260629021427-b3c6babbd353 h1:HA0TGrBQSFfvcoVXL1DxzF+i8pmaGOGb33jdReB7L4s= -github.com/sagernet/sing-tun v0.8.12-0.20260629021427-b3c6babbd353/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= +github.com/sagernet/sing-tun v0.8.12-0.20260706130635-4b50856586ba h1:/+QtKNDDJ6MEAIbmRWvnQSHNJHxXbtYTRX12xRY26Cg= +github.com/sagernet/sing-tun v0.8.12-0.20260706130635-4b50856586ba/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb h1:KEMbfexD4DvrQGYWwx6r+AwH9Veh8z6cnBZmtCS2G+0= github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb/go.mod h1:D4CnJX3MNAAANhbQUxfIRgBdnvlTEaV7h6ojedcs+pw= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260527101438-dc40932c32d9 h1:jOkKeYI0A0M+jVEu2omQLId4q5GVP7G8FSZh1eUArIk= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260527101438-dc40932c32d9/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= -github.com/sagernet/wireguard-go v0.0.3 h1:6ebmwj/SFQRnYv6/nRCnwUzf+KFepF8tIBd57IAq1jE= -github.com/sagernet/wireguard-go v0.0.3/go.mod h1:hEqi4y5czEg6LYtX2Bpjg+lV0b/J1n+5rA885Z66Mx0= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3 h1:eczvica8YiS5j3GfpHg6JG1Icur4Z2D6ffSrLZTfD1E= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3/go.mod h1:p8Ms8FbGlwQJyHb862XmdShTS50fFJ8C71VdO6xvWyk= +github.com/sagernet/wireguard-go v0.0.5-0.20260706130655-57baac9504a8 h1:gfukXANr9v5kcrKGeoCV5c+IdessM9NRGzWE7Aot9sw= +github.com/sagernet/wireguard-go v0.0.5-0.20260706130655-57baac9504a8/go.mod h1:hEqi4y5czEg6LYtX2Bpjg+lV0b/J1n+5rA885Z66Mx0= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= diff --git a/protocol/cloudflare/inbound.go b/protocol/cloudflare/inbound.go index 2ad3fb296..c33674475 100644 --- a/protocol/cloudflare/inbound.go +++ b/protocol/cloudflare/inbound.go @@ -5,6 +5,7 @@ package cloudflare import ( "context" "net" + "net/netip" "time" "github.com/sagernet/sing-box/adapter" @@ -13,7 +14,6 @@ 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/rule" "github.com/sagernet/sing-cloudflared" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/bufio" @@ -137,32 +137,44 @@ type icmpRouterHandler struct { tag string } -func (h *icmpRouterHandler) RouteICMPConnection(ctx context.Context, session tun.DirectRouteSession, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - var ipVersion uint8 - if session.Destination.Is4() { - ipVersion = 4 - } else { - ipVersion = 6 - } - destination := M.SocksaddrFrom(session.Destination, 0) - routeDestination, err := h.router.PreMatch(adapter.InboundContext{ - Inbound: h.tag, - InboundType: C.TypeCloudflared, - IPVersion: ipVersion, - Network: N.NetworkICMP, - Source: M.SocksaddrFrom(session.Source, 0), - Destination: destination, - OriginDestination: destination, - }, routeContext, timeout, false) - if err != nil { - switch { - case rule.IsBypassed(err): - err = nil - case rule.IsRejected(err): - h.logger.Trace("reject ICMP connection from ", session.Source, " to ", session.Destination) - default: - h.logger.Warn(E.Cause(err, "link ICMP connection from ", session.Source, " to ", session.Destination)) +func (h *icmpRouterHandler) RouteICMPFlow(source netip.Addr, destination netip.Addr) (tun.Port, error) { + result := h.router.PreMatch(adapter.InboundContext{ + Inbound: h.tag, + InboundType: C.TypeCloudflared, + Network: N.NetworkICMP, + Source: M.SocksaddrFrom(source, 0), + Destination: M.SocksaddrFrom(destination, 0), + }) + switch result.Action { + case adapter.PreMatchFlow: + flowOutbound, isFlowOutbound := result.Outbound.(adapter.FlowOutbound) + if !isFlowOutbound { + return nil, E.New("outbound is not a flow outbound") } + if result.Destination.IsValid() && result.Destination.Addr() != destination { + h.logger.Trace("drop ICMP flow from ", source, " to ", destination, ": destination override is not supported from cloudflared") + return nil, E.New("destination override is not supported") + } + inet4Address, inet6Address := flowOutbound.PortAddresses() + var portAddress netip.Addr + if destination.Is4() { + portAddress = inet4Address + } else { + portAddress = inet6Address + } + if !portAddress.IsValid() || !portAddress.IsUnspecified() { + h.logger.Trace("drop ICMP flow from ", source, " to ", destination, ": forwarding ICMP to outbound/", result.Outbound.Type(), "[", result.Outbound.Tag(), "] is not supported from cloudflared") + return nil, E.New("unsupported flow outbound") + } + h.logger.Debug("link ICMP flow from ", source, " to ", destination, " via outbound/", result.Outbound.Type(), "[", result.Outbound.Tag(), "]") + return flowOutbound, nil + case adapter.PreMatchReject: + h.logger.Trace("reject ICMP flow from ", source, " to ", destination) + return nil, E.New("rejected") + case adapter.PreMatchDrop: + return nil, E.New("dropped") + default: + h.logger.Trace("drop ICMP flow from ", source, " to ", destination, ": no direct route") + return nil, E.New("no direct route") } - return routeDestination, err } diff --git a/protocol/direct/outbound.go b/protocol/direct/outbound.go index b4e7eba03..74552739d 100644 --- a/protocol/direct/outbound.go +++ b/protocol/direct/outbound.go @@ -16,6 +16,7 @@ import ( "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" @@ -31,7 +32,7 @@ var ( _ N.ParallelDialer = (*Outbound)(nil) _ dialer.ParallelNetworkDialer = (*Outbound)(nil) _ dialer.DirectDialer = (*Outbound)(nil) - _ adapter.DirectRouteOutbound = (*Outbound)(nil) + _ adapter.FlowOutbound = (*Outbound)(nil) ) type Outbound struct { @@ -44,6 +45,7 @@ type Outbound struct { fallbackDelay time.Duration isEmpty bool myAddresses common.TypedValue[[]netip.Prefix] + icmpPort *ping.Port } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (adapter.Outbound, error) { @@ -75,6 +77,11 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL if options.ProxyProtocol != 0 { return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0") } + if defaultDialer, isDefaultDialer := common.Cast[*dialer.DefaultDialer](outbound.dialer); isDefaultDialer { + outbound.icmpPort = ping.NewPort(ctx, logger, func(destination netip.Addr) control.Func { + return defaultDialer.DialerForICMPDestination(destination).Control + }, 0) + } return outbound, nil } @@ -148,14 +155,35 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n return conn, nil } -func (h *Outbound) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - ctx := log.ContextWithNewID(h.ctx) - destination, err := ping.ConnectDestination(ctx, h.logger, common.MustCast[*dialer.DefaultDialer](h.dialer).DialerForICMPDestination(metadata.Destination.Addr).Control, metadata.Destination.Addr, routeContext, timeout) - if err != nil { - return nil, err +func (h *Outbound) SupportsFlow(network string) bool { + return network == N.NetworkICMP && h.icmpPort != nil +} + +func (h *Outbound) PortAddresses() (netip.Addr, netip.Addr) { + return h.icmpPort.PortAddresses() +} + +func (h *Outbound) PortMTU() uint32 { + return h.icmpPort.PortMTU() +} + +func (h *Outbound) AttachReturn(returnPath tun.Return) error { + return h.icmpPort.AttachReturn(returnPath) +} + +func (h *Outbound) DetachReturn(returnPath tun.Return) error { + return h.icmpPort.DetachReturn(returnPath) +} + +func (h *Outbound) WritePackets(packets [][]byte) error { + return h.icmpPort.WritePackets(packets) +} + +func (h *Outbound) Close() error { + if h.icmpPort != nil { + return h.icmpPort.Close() } - h.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) - return destination, nil + return nil } func (h *Outbound) DialParallel(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr) (net.Conn, error) { diff --git a/protocol/group/selector.go b/protocol/group/selector.go index 607387bd7..2b1cb0407 100644 --- a/protocol/group/selector.go +++ b/protocol/group/selector.go @@ -3,7 +3,6 @@ package group import ( "context" "net" - "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" @@ -12,7 +11,6 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - tun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" @@ -182,14 +180,6 @@ func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, m } } -func (s *Selector) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - selected := s.selected.Load() - if !common.Contains(selected.Network(), metadata.Network) { - return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag()) - } - return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout) -} - func RealTag(detour adapter.Outbound) string { if group, isGroup := detour.(adapter.OutboundGroup); isGroup { return group.Now() diff --git a/protocol/group/urltest.go b/protocol/group/urltest.go index bc13b6373..69e310370 100644 --- a/protocol/group/urltest.go +++ b/protocol/group/urltest.go @@ -14,7 +14,6 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" @@ -169,21 +168,6 @@ func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, me s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose) } -func (s *URLTest) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - s.group.Touch() - selected := s.group.selectedOutboundTCP - if selected == nil { - selected, _ = s.group.Select(N.NetworkTCP) - } - if selected == nil { - return nil, E.New("missing supported outbound") - } - if !common.Contains(selected.Network(), metadata.Network) { - return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag()) - } - return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout) -} - type URLTestGroup struct { ctx context.Context outbound adapter.OutboundManager diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 3593d1602..3b5b754ba 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -15,6 +15,7 @@ import ( "reflect" "runtime" "strings" + "sync" "sync/atomic" "syscall" "time" @@ -34,7 +35,6 @@ import ( "github.com/sagernet/sing-box/protocol/tailscale/tailssh" R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/control" @@ -56,7 +56,6 @@ import ( tsTUN "github.com/sagernet/tailscale/net/tstun" "github.com/sagernet/tailscale/tailcfg" "github.com/sagernet/tailscale/tsnet" - "github.com/sagernet/tailscale/types/ipproto" "github.com/sagernet/tailscale/types/nettype" "github.com/sagernet/tailscale/version" "github.com/sagernet/tailscale/wgengine" @@ -70,8 +69,8 @@ import ( var ( _ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil) - _ adapter.DirectRouteOutbound = (*Endpoint)(nil) _ dialer.PacketDialerWithDestination = (*Endpoint)(nil) + _ tun.Port = (*Endpoint)(nil) ) func init() { @@ -95,6 +94,9 @@ type Endpoint struct { stack *stack.Stack icmpForwarder *tun.ICMPForwarder filter *atomic.Pointer[filter.Filter] + returnAccess sync.Mutex + returnPath tun.Return + wgEngine wgengine.ExportedUserspaceEngine onReconfigHook wgengine.ReconfigListener sshReconfigHook wgengine.ReconfigListener @@ -287,6 +289,7 @@ func (t *Endpoint) start() error { if mtu == 0 { mtu = uint32(tsTUN.DefaultTUNMTU()) } + t.systemInterfaceMTU = mtu tunName := t.systemInterfaceName if tunName == "" { tunName = tun.CalculateInterfaceName("tailscale") @@ -361,7 +364,9 @@ func (t *Endpoint) postStart() error { }, true }) } - t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine).SetOnReconfigListener(t.onReconfig) + wgEngine := t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine) + wgEngine.SetOnReconfigListener(t.onReconfig) + t.wgEngine = wgEngine ipStack := t.server.ExportNetstack().ExportIPStack() gErr := ipStack.SetSpoofing(tun.DefaultNIC, true) @@ -372,7 +377,7 @@ func (t *Endpoint) postStart() error { if gErr != nil { return gonet.TranslateNetstackError(gErr) } - icmpForwarder := tun.NewICMPForwarder(t.ctx, ipStack, t.logger, t, t.icmpTimeout) + icmpForwarder := tun.NewICMPForwarder(ipStack, t, t.logger) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) t.stack = ipStack @@ -622,6 +627,10 @@ func (t *Endpoint) Logout(ctx context.Context) error { func (t *Endpoint) Close() error { var err error t.started.Store(false) + if t.icmpForwarder != nil { + t.icmpForwarder.Close() + t.icmpForwarder = nil + } common.Close(common.PtrOrNil(t.sshServerInstance)) t.sshServerInstance = nil if t.serverStarted { @@ -776,62 +785,6 @@ func (t *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n return packetConn, nil } -func (t *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - if !t.started.Load() { - return nil, E.New("Tailscale is not ready yet") - } - tsFilter := t.filter.Load() - if tsFilter != nil { - var ipProto ipproto.Proto - switch N.NetworkName(network) { - case N.NetworkTCP: - ipProto = ipproto.TCP - case N.NetworkUDP: - ipProto = ipproto.UDP - case N.NetworkICMP: - if !destination.IsIPv6() { - ipProto = ipproto.ICMPv4 - } else { - ipProto = ipproto.ICMPv6 - } - } - response := tsFilter.Check(source.Addr, destination.Addr, destination.Port, ipProto) - switch response { - case filter.Drop: - return nil, syscall.ECONNREFUSED - case filter.DropSilently: - return nil, tun.ErrDrop - } - } - var ipVersion uint8 - if !destination.IsIPv6() { - ipVersion = 4 - } else { - ipVersion = 6 - } - routeDestination, err := t.router.PreMatch(adapter.InboundContext{ - Inbound: t.Tag(), - InboundType: t.Type(), - IPVersion: ipVersion, - Network: network, - Source: source, - Destination: destination, - }, routeContext, timeout, false) - if err != nil { - switch { - case R.IsBypassed(err): - err = nil - case R.IsRejected(err): - t.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) - default: - if network == N.NetworkICMP { - t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())) - } - } - } - return routeDestination, err -} - func (t *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext metadata.Inbound = t.Tag() @@ -872,40 +825,6 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } -func (t *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - if !t.started.Load() { - return nil, E.New("Tailscale is not ready yet") - } - ctx := log.ContextWithNewID(t.ctx) - var destination tun.DirectRouteDestination - var err error - if t.systemDialer != nil { - destination, err = ping.ConnectDestination( - ctx, t.logger, - t.systemDialer.DialerForICMPDestination(metadata.Destination.Addr).Control, - metadata.Destination.Addr, routeContext, timeout, - ) - } else { - inet4Address, inet6Address := t.server.TailscaleIPs() - if metadata.Destination.Addr.Is4() && !inet4Address.IsValid() || metadata.Destination.Addr.Is6() && !inet6Address.IsValid() { - return nil, E.New("Tailscale is not ready yet") - } - destination, err = ping.ConnectGVisor( - ctx, t.logger, - metadata.Source.Addr, metadata.Destination.Addr, - routeContext, - t.stack, - inet4Address, inet6Address, - timeout, - ) - } - if err != nil { - return nil, err - } - t.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) - return destination, nil -} - func (t *Endpoint) PreferredDomain(domain string) bool { routeDomains := t.routeDomains.Load() if routeDomains == nil { @@ -933,15 +852,6 @@ func (t *Endpoint) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCf if (t.cfg != nil && reflect.DeepEqual(t.cfg, cfg)) && (t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg)) { return } - var inet4Address, inet6Address netip.Addr - for _, address := range cfg.Addresses { - if address.Addr().Is4() { - inet4Address = address.Addr() - } else if address.Addr().Is6() { - inet6Address = address.Addr() - } - } - t.icmpForwarder.SetLocalAddresses(inet4Address, inet6Address) t.cfg = cfg t.dnsCfg = dnsCfg diff --git a/protocol/tailscale/port.go b/protocol/tailscale/port.go new file mode 100644 index 000000000..3e5b7a70f --- /dev/null +++ b/protocol/tailscale/port.go @@ -0,0 +1,133 @@ +//go:build with_gvisor + +package tailscale + +import ( + "net/netip" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/gtcpip/header" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + tsTUN "github.com/sagernet/tailscale/net/tstun" + "github.com/sagernet/tailscale/types/ipproto" + "github.com/sagernet/tailscale/wgengine/filter" +) + +func (t *Endpoint) SupportsFlow(network string) bool { + switch network { + case N.NetworkTCP, N.NetworkUDP, N.NetworkICMP: + return true + default: + return false + } +} + +func (t *Endpoint) PortAddresses() (netip.Addr, netip.Addr) { + if !t.started.Load() { + return netip.Addr{}, netip.Addr{} + } + return t.server.TailscaleIPs() +} + +func (t *Endpoint) PortMTU() uint32 { + if t.systemInterface { + return t.systemInterfaceMTU + } + return uint32(tsTUN.DefaultTUNMTU()) +} + +func (t *Endpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort) tun.FlowVerdict { + inet4Address, inet6Address := t.PortAddresses() + if destination.Addr() == inet4Address || destination.Addr() == inet6Address { + return tun.FlowVerdict{Action: tun.ActionAccept} + } + if t.filter != nil { + tsFilter := t.filter.Load() + if tsFilter != nil { + var ( + ipProto ipproto.Proto + destinationPort uint16 + ) + switch network { + case uint8(header.TCPProtocolNumber): + ipProto = ipproto.TCP + destinationPort = destination.Port() + case uint8(header.UDPProtocolNumber): + ipProto = ipproto.UDP + destinationPort = destination.Port() + case uint8(header.ICMPv4ProtocolNumber): + ipProto = ipproto.ICMPv4 + case uint8(header.ICMPv6ProtocolNumber): + ipProto = ipproto.ICMPv6 + } + switch tsFilter.Check(source.Addr(), destination.Addr(), destinationPort, ipProto) { + case filter.Drop: + return tun.FlowVerdict{Action: tun.ActionReject} + case filter.DropSilently: + return tun.FlowVerdict{Action: tun.ActionDrop} + } + } + } + return adapter.JudgeFlow(t.router, t.Tag(), t.Type(), network, source, destination) +} + +func (t *Endpoint) AttachReturn(returnPath tun.Return) error { + t.returnAccess.Lock() + defer t.returnAccess.Unlock() + if t.returnPath == returnPath { + return nil + } + if t.returnPath != nil { + return E.New("return path already attached") + } + err := t.wgEngine.SetReturnPath(returnPath) + if err != nil { + return err + } + t.returnPath = returnPath + return nil +} + +func (t *Endpoint) DetachReturn(returnPath tun.Return) error { + t.returnAccess.Lock() + defer t.returnAccess.Unlock() + if t.returnPath == returnPath { + t.returnPath = nil + } + return nil +} + +func (t *Endpoint) WritePackets(packets [][]byte) error { + if !t.started.Load() { + return E.New("Tailscale is not ready yet") + } + unmatched, err := t.wgEngine.InputPackets(packets) + if err != nil || len(unmatched) == 0 { + return err + } + t.returnAccess.Lock() + returnPath := t.returnPath + t.returnAccess.Unlock() + if returnPath == nil { + return nil + } + headroom := returnPath.ReturnHeadroom() + inet4Address, inet6Address := t.PortAddresses() + var replies [][]byte + for _, packet := range unmatched { + source := inet4Address + if header.IPVersion(packet) == header.IPv6Version { + source = inet6Address + } + reply, replyOk := tun.BuildUnreachable(packet, source, headroom) + if replyOk { + replies = append(replies, reply) + } + } + if len(replies) > 0 { + returnPath.ReturnPackets(replies) + } + return nil +} diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 44c34ef91..2bbf9b8de 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -16,7 +16,6 @@ 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/rule" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" @@ -99,7 +98,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo platformInterface := service.FromContext[adapter.PlatformInterface](ctx) tunMTU := options.MTU - enableGSO := C.IsLinux && options.Stack == "gvisor" && platformInterface == nil && tunMTU > 0 && tunMTU < 49152 if tunMTU == 0 { if platformInterface != nil && platformInterface.UnderNetworkExtension() { // In Network Extension, when MTU exceeds 4064 (4096-UTUN_IF_HEADROOM_SIZE), the performance of tun will drop significantly, which may be a system bug. @@ -111,6 +109,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo tunMTU = 65535 } } + var enableGSO bool + if C.IsLinux && platformInterface == nil { + enableGSO = (options.Stack == "gvisor" && tunMTU < 49152) + } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) @@ -178,7 +180,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo excludeMACAddress = append(excludeMACAddress, mac) } networkManager := service.FromContext[adapter.NetworkManager](ctx) - multiPendingPackets := C.IsDarwin && ((options.Stack == "gvisor" && tunMTU < 32768) || (options.Stack != "gvisor" && options.MTU <= 9000)) + multiPendingPackets := C.IsDarwin && ((options.Stack == "gvisor" && tunMTU < 32768) || (options.Stack != "gvisor" && tunMTU <= 9000)) inbound := &Inbound{ tag: tag, ctx: ctx, @@ -320,6 +322,22 @@ func (t *Inbound) Start(stage adapter.StartStage) error { t.dnsHijackAddress = append(inet4DNSAddress, inet6DNSAddress...) } case adapter.StartStateStart: + if t.platformInterface == nil && + ((C.IsLinux && !t.tunOptions.GSO) || (C.IsDarwin && !t.tunOptions.EXP_MultiPendingPackets)) { + endpointManager := service.FromContext[adapter.EndpointManager](t.ctx) + if endpointManager != nil { + for _, managedEndpoint := range endpointManager.Endpoints() { + if _, isFlowOutbound := managedEndpoint.(adapter.FlowOutbound); isFlowOutbound { + if C.IsLinux { + t.tunOptions.GSO = true + } else { + t.tunOptions.EXP_MultiPendingPackets = true + } + break + } + } + } + } if C.IsAndroid && t.platformInterface == nil { t.tunOptions.BuildAndroidRules(t.networkManager.PackageManager()) } @@ -460,34 +478,8 @@ func (t *Inbound) Close() error { ) } -func (t *Inbound) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - var ipVersion uint8 - if !destination.IsIPv6() { - ipVersion = 4 - } else { - ipVersion = 6 - } - routeDestination, err := t.router.PreMatch(adapter.InboundContext{ - Inbound: t.tag, - InboundType: C.TypeTun, - IPVersion: ipVersion, - Network: network, - Source: source, - Destination: destination, - }, routeContext, timeout, false) - if err != nil { - switch { - case rule.IsBypassed(err): - err = nil - case rule.IsRejected(err): - t.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) - default: - if network == N.NetworkICMP { - t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())) - } - } - } - return routeDestination, err +func (t *Inbound) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort) tun.FlowVerdict { + return adapter.JudgeFlow(t.router, t.tag, C.TypeTun, network, source, destination) } func (t *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { @@ -534,34 +526,8 @@ func (t *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, type autoRedirectHandler Inbound -func (t *autoRedirectHandler) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - var ipVersion uint8 - if !destination.IsIPv6() { - ipVersion = 4 - } else { - ipVersion = 6 - } - routeDestination, err := t.router.PreMatch(adapter.InboundContext{ - Inbound: t.tag, - InboundType: C.TypeTun, - IPVersion: ipVersion, - Network: network, - Source: source, - Destination: destination, - }, routeContext, timeout, true) - if err != nil { - switch { - case rule.IsBypassed(err): - t.logger.Trace("bypass ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) - case rule.IsRejected(err): - t.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) - default: - if network == N.NetworkICMP { - t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())) - } - } - } - return routeDestination, err +func (t *autoRedirectHandler) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort) tun.FlowVerdict { + return (*Inbound)(t).JudgeFlow(network, source, destination) } func (t *autoRedirectHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index 63009bfda..013a99ad0 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -13,7 +13,6 @@ 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/rule" "github.com/sagernet/sing-box/transport/wireguard" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" @@ -137,37 +136,45 @@ func (w *Endpoint) Close() error { return w.endpoint.Close() } -func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - if !w.started.Load() { - return nil, E.New("WireGuard is not ready yet") +func (w *Endpoint) SupportsFlow(network string) bool { + switch network { + case N.NetworkTCP, N.NetworkUDP, N.NetworkICMP: + return true + default: + return false } - var ipVersion uint8 - if !destination.IsIPv6() { - ipVersion = 4 - } else { - ipVersion = 6 - } - routeDestination, err := w.router.PreMatch(adapter.InboundContext{ - Inbound: w.Tag(), - InboundType: w.Type(), - IPVersion: ipVersion, - Network: network, - Source: source, - Destination: destination, - }, routeContext, timeout, false) - if err != nil { - switch { - case rule.IsBypassed(err): - err = nil - case rule.IsRejected(err): - w.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) - default: - if network == N.NetworkICMP { - w.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())) - } +} + +func (w *Endpoint) PortAddresses() (netip.Addr, netip.Addr) { + return w.endpoint.PortAddresses() +} + +func (w *Endpoint) PortMTU() uint32 { + return w.endpoint.PortMTU() +} + +func (w *Endpoint) AttachReturn(returnPath tun.Return) error { + return w.endpoint.AttachReturn(returnPath) +} + +func (w *Endpoint) DetachReturn(returnPath tun.Return) error { + return w.endpoint.DetachReturn(returnPath) +} + +func (w *Endpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort) tun.FlowVerdict { + for _, localPrefix := range w.localAddresses { + if localPrefix.Contains(destination.Addr()) { + return tun.FlowVerdict{Action: tun.ActionAccept} } } - return routeDestination, err + return adapter.JudgeFlow(w.router, w.Tag(), w.Type(), network, source, destination) +} + +func (w *Endpoint) WritePackets(packets [][]byte) error { + if !w.started.Load() { + return E.New("WireGuard is not ready yet") + } + return w.endpoint.WritePackets(packets) } func (w *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { @@ -279,10 +286,3 @@ func (w *Endpoint) PreferredAddress(address netip.Addr) bool { } return w.endpoint.Lookup(address) != nil } - -func (w *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - if !w.started.Load() { - return nil, E.New("WireGuard is not ready yet") - } - return w.endpoint.NewDirectRouteConnection(metadata, routeContext, timeout) -} diff --git a/route/route.go b/route/route.go index 66dbdc17d..db1332d9b 100644 --- a/route/route.go +++ b/route/route.go @@ -9,12 +9,11 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-mux" - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing-vmess" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" @@ -95,7 +94,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad if deadline.NeedAdditionalReadDeadline(conn) { conn = deadline.NewConn(conn) } - selectedRule, _, buffers, _, err := r.matchRule(ctx, &metadata, false, false, conn, nil) + selectedRule, _, buffers, _, err := r.matchRule(ctx, &metadata, conn, nil) if err != nil { return err } @@ -226,7 +225,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m if metadata.InboundType == C.TypeTun && metadata.Protocol == C.ProtocolDNS { return r.hijackDNSPacket(ctx, conn, nil, metadata, onClose) } - selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, false, false, nil, conn) + selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, nil, conn) if err != nil { return err } @@ -295,119 +294,157 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m return nil } -func (r *Router) PreMatch(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error) { - selectedRule, _, _, _, err := r.matchRule(r.ctx, &metadata, true, supportBypass, nil, nil) - if err != nil { - return nil, err +func (r *Router) PreMatch(metadata adapter.InboundContext) adapter.PreMatchResult { + continueResult := adapter.PreMatchResult{Action: adapter.PreMatchContinue} + packetDestination := metadata.Destination + if metadata.Destination.Addr.IsValid() && r.dnsTransport.FakeIP() != nil && r.dnsTransport.FakeIP().Store().Contains(metadata.Destination.Addr) { + domain, loaded := r.dnsTransport.FakeIP().Store().Lookup(metadata.Destination.Addr) + if !loaded || domain == "" { + return continueResult + } + metadata.OriginDestination = metadata.Destination + metadata.Destination = M.Socksaddr{ + Fqdn: domain, + Port: metadata.Destination.Port, + } + metadata.FakeIP = true } - var directRouteOutbound adapter.DirectRouteOutbound - if selectedRule != nil { - switch action := selectedRule.Action().(type) { - case *R.RuleActionReject: - switch metadata.Network { - case N.NetworkTCP: - if action.Method == C.RuleActionRejectMethodReply { - return nil, E.New("reject method `reply` is not supported for TCP connections") - } - case N.NetworkUDP: - if action.Method == C.RuleActionRejectMethodReply { - return nil, E.New("reject method `reply` is not supported for UDP connections") - } + if metadata.Destination.IsIPv4() { + metadata.IPVersion = 4 + } else if metadata.Destination.IsIPv6() { + metadata.IPVersion = 6 + } + for currentRuleIndex, currentRule := range r.rules { + metadata.ResetRuleCache() + if !currentRule.Match(&metadata) { + continue + } + switch action := currentRule.Action().(type) { + case *R.RuleActionSniff: + if metadata.Network == N.NetworkICMP { + continue } - return nil, action.Error(context.Background()) - case *R.RuleActionBypass: - if supportBypass { - return nil, &R.BypassedError{Cause: tun.ErrBypass} - } - if routeContext == nil { - return nil, nil - } - outbound, loaded := r.outbound.Outbound(action.Outbound) - if !loaded { - return nil, E.New("outbound not found: ", action.Outbound) - } - if !common.Contains(outbound.Network(), metadata.Network) { - return nil, E.New(metadata.Network, " is not supported by outbound: ", action.Outbound) - } - directRouteOutbound = outbound.(adapter.DirectRouteOutbound) + return continueResult + case *R.RuleActionRouteOptions: + applyRouteOptionsOverride(&metadata, action) case *R.RuleActionRoute: - if routeContext == nil { - return nil, nil + applyRouteOptionsOverride(&metadata, &action.RuleActionRouteOptions) + r.logger.Debug("pre-match[", currentRuleIndex, "] ", currentRule, " => ", action) + return r.preMatchFlow(&metadata, packetDestination, action.Outbound) + case *R.RuleActionBypass: + applyRouteOptionsOverride(&metadata, &action.RuleActionRouteOptions) + r.logger.Debug("pre-match[", currentRuleIndex, "] ", currentRule, " => ", action) + if action.Outbound == "" { + if metadata.Destination.IsDomain() || metadata.Destination != packetDestination { + return continueResult + } + return adapter.PreMatchResult{Action: adapter.PreMatchBypass} } - outbound, loaded := r.outbound.Outbound(action.Outbound) - if !loaded { - return nil, E.New("outbound not found: ", action.Outbound) + return r.preMatchFlow(&metadata, packetDestination, action.Outbound) + case *R.RuleActionReject: + r.logger.Debug("pre-match[", currentRuleIndex, "] ", currentRule, " => ", action) + rejectErr := action.Error(r.ctx) + if errors.Is(rejectErr, R.ErrDrop) { + return adapter.PreMatchResult{Action: adapter.PreMatchDrop} } - if !common.Contains(outbound.Network(), metadata.Network) { - return nil, E.New(metadata.Network, " is not supported by outbound: ", action.Outbound) + return adapter.PreMatchResult{Action: adapter.PreMatchReject} + case *R.RuleActionResolve: + resolveErr := r.actionResolve(adapter.WithContext(r.ctx, &metadata), &metadata, action) + if resolveErr != nil { + r.logger.Debug("pre-match[", currentRuleIndex, "] ", currentRule, " => ", action, ": ", resolveErr) + return adapter.PreMatchResult{Action: adapter.PreMatchReject} } - directRouteOutbound = outbound.(adapter.DirectRouteOutbound) + default: + return continueResult } } - if directRouteOutbound == nil { - if selectedRule != nil || metadata.Network != N.NetworkICMP { - return nil, nil + return r.preMatchFlow(&metadata, packetDestination, "") +} + +func applyRouteOptionsOverride(metadata *adapter.InboundContext, routeOptions *R.RuleActionRouteOptions) { + if routeOptions.OverrideAddress.IsValid() { + metadata.Destination = M.Socksaddr{ + Addr: routeOptions.OverrideAddress.Addr, + Port: metadata.Destination.Port, + Fqdn: routeOptions.OverrideAddress.Fqdn, } - defaultOutbound := r.outbound.Default() - if !common.Contains(defaultOutbound.Network(), metadata.Network) { - return nil, E.New(metadata.Network, " is not supported by default outbound: ", defaultOutbound.Tag()) - } - directRouteOutbound = defaultOutbound.(adapter.DirectRouteOutbound) } + if routeOptions.OverridePort > 0 { + metadata.Destination = M.Socksaddr{ + Addr: metadata.Destination.Addr, + Port: routeOptions.OverridePort, + Fqdn: metadata.Destination.Fqdn, + } + } +} + +func (r *Router) preMatchFlow(metadata *adapter.InboundContext, packetDestination M.Socksaddr, outboundTag string) adapter.PreMatchResult { + continueResult := adapter.PreMatchResult{Action: adapter.PreMatchContinue} + var outbound adapter.Outbound + if outboundTag == "" { + outbound = r.outbound.Default() + } else { + var loaded bool + outbound, loaded = r.outbound.Outbound(outboundTag) + if !loaded { + return continueResult + } + } + for range 8 { + group, isGroup := outbound.(adapter.OutboundGroup) + if !isGroup { + break + } + selectedOutbound, selectedLoaded := r.outbound.Outbound(group.Now()) + if !selectedLoaded { + return continueResult + } + outbound = selectedOutbound + } + if !common.Contains(outbound.Network(), metadata.Network) { + return continueResult + } + flowOutbound, isFlowOutbound := outbound.(adapter.FlowOutbound) + if !isFlowOutbound || !flowOutbound.SupportsFlow(metadata.Network) { + if outbound.Type() == C.TypeDirect { + directDialer, isDirectDialer := outbound.(dialer.DirectDialer) + if isDirectDialer && directDialer.IsEmpty() && !metadata.Destination.IsDomain() && metadata.Destination == packetDestination { + r.logger.Debug("pre-match bypass ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) + return adapter.PreMatchResult{Action: adapter.PreMatchBypass, Outbound: outbound} + } + } + return continueResult + } + result := adapter.PreMatchResult{Action: adapter.PreMatchFlow, Outbound: outbound} if metadata.Destination.IsDomain() { - if len(metadata.DestinationAddresses) == 0 { - var strategy C.DomainStrategy - if metadata.Source.IsIPv4() { - strategy = C.DomainStrategyIPv4Only - } else { - strategy = C.DomainStrategyIPv6Only - } - err = r.actionResolve(r.ctx, &metadata, &R.RuleActionResolve{ - Strategy: strategy, - }) - if err != nil { - return nil, err - } + if !metadata.FakeIP { + return continueResult } var newDestination netip.Addr - if metadata.Source.IsIPv4() { - for _, address := range metadata.DestinationAddresses { - if address.Is4() { - newDestination = address - break - } - } - } else { - for _, address := range metadata.DestinationAddresses { - if address.Is6() { - newDestination = address - break - } + for _, address := range metadata.DestinationAddresses { + if address.Is4() == packetDestination.IsIPv4() { + newDestination = address + break } } if !newDestination.IsValid() { - if metadata.Source.IsIPv4() { - return nil, E.New("no IPv4 address found for domain: ", metadata.Destination.Fqdn) + if len(metadata.DestinationAddresses) == 0 { + r.logger.Warn("pre-match: reject ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to fake destination ", metadata.Destination.Fqdn, ": a resolve action is required before routing to outbound/", outbound.Type(), "[", outbound.Tag(), "]") } else { - return nil, E.New("no IPv6 address found for domain: ", metadata.Destination.Fqdn) + r.logger.Debug("pre-match: reject ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to fake destination ", metadata.Destination.Fqdn, ": no resolved address for this address family") } + return adapter.PreMatchResult{Action: adapter.PreMatchReject} } - metadata.Destination = M.Socksaddr{ - Addr: newDestination, - } - routeContext = ping.NewContextDestinationWriter(routeContext, metadata.OriginDestination.Addr) - var routeDestination tun.DirectRouteDestination - routeDestination, err = directRouteOutbound.NewDirectRouteConnection(metadata, routeContext, timeout) - if err != nil { - return nil, err - } - return ping.NewDestinationWriter(routeDestination, newDestination), nil + result.Destination = netip.AddrPortFrom(newDestination, metadata.Destination.Port) + } else if metadata.Destination != packetDestination { + result.Destination = metadata.Destination.AddrPort() } - return directRouteOutbound.NewDirectRouteConnection(metadata, routeContext, timeout) + r.logger.Debug("pre-match forward ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString(), " via outbound/", outbound.Type(), "[", outbound.Tag(), "]") + return result } func (r *Router) matchRule( - ctx context.Context, metadata *adapter.InboundContext, preMatch bool, supportBypass bool, + ctx context.Context, metadata *adapter.InboundContext, inputConn net.Conn, inputPacketConn N.PacketConn, ) ( selectedRule adapter.Rule, selectedRuleIndex int, @@ -465,23 +502,11 @@ match: if !currentRule.Match(metadata) { continue } - if !preMatch { - ruleDescription := currentRule.String() - if ruleDescription != "" { - r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) - } else { - r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action()) - } + ruleDescription := currentRule.String() + if ruleDescription != "" { + r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) } else { - switch currentRule.Action().Type() { - case C.RuleActionTypeReject: - ruleDescription := currentRule.String() - if ruleDescription != "" { - r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) - } else { - r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] => ", currentRule.Action()) - } - } + r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action()) } var routeOptions *R.RuleActionRouteOptions switch action := currentRule.Action().(type) { @@ -500,20 +525,9 @@ match: metadata.RouteOriginalDestination = metadata.Destination } if routeOptions.OverrideAddress.IsValid() { - metadata.Destination = M.Socksaddr{ - Addr: routeOptions.OverrideAddress.Addr, - Port: metadata.Destination.Port, - Fqdn: routeOptions.OverrideAddress.Fqdn, - } metadata.DestinationAddresses = nil } - if routeOptions.OverridePort > 0 { - metadata.Destination = M.Socksaddr{ - Addr: metadata.Destination.Addr, - Port: routeOptions.OverridePort, - Fqdn: metadata.Destination.Fqdn, - } - } + applyRouteOptionsOverride(metadata, routeOptions) if routeOptions.NetworkStrategy != nil { metadata.NetworkStrategy = routeOptions.NetworkStrategy } @@ -549,21 +563,15 @@ match: } switch action := currentRule.Action().(type) { case *R.RuleActionSniff: - if !preMatch { - newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers, packetBuffers) - if newBuffer != nil { - buffers = append(buffers, newBuffer) - } else if len(newPacketBuffers) > 0 { - packetBuffers = append(packetBuffers, newPacketBuffers...) - } - if newErr != nil { - fatalErr = newErr - return - } - } else if metadata.Network != N.NetworkICMP { - selectedRule = currentRule - selectedRuleIndex = currentRuleIndex - break match + newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers, packetBuffers) + if newBuffer != nil { + buffers = append(buffers, newBuffer) + } else if len(newPacketBuffers) > 0 { + packetBuffers = append(packetBuffers, newPacketBuffers...) + } + if newErr != nil { + fatalErr = newErr + return } case *R.RuleActionResolve: fatalErr = r.actionResolve(ctx, metadata, action) @@ -581,7 +589,7 @@ match: } if actionType == C.RuleActionTypeBypass { bypassAction := currentRule.Action().(*R.RuleActionBypass) - if !supportBypass && bypassAction.Outbound == "" { + if bypassAction.Outbound == "" { continue match } selectedRule = currentRule diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 207945c41..5d64c12cb 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -14,7 +14,6 @@ import ( "github.com/sagernet/sing-box/common/tlsspoof" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" @@ -388,6 +387,11 @@ func (r *RuleActionDirect) String() string { return "direct" + r.description } +var ( + ErrReset = E.New("connection reset") + ErrDrop = E.New("packet dropped") +) + type RejectedError struct { Cause error } @@ -445,9 +449,9 @@ func (r *RuleActionReject) Error(ctx context.Context) error { var returnErr error switch r.Method { case C.RuleActionRejectMethodDefault: - returnErr = &RejectedError{tun.ErrReset} + returnErr = &RejectedError{ErrReset} case C.RuleActionRejectMethodDrop: - return &RejectedError{tun.ErrDrop} + return &RejectedError{ErrDrop} case C.RuleActionRejectMethodReply: return nil default: @@ -467,7 +471,7 @@ func (r *RuleActionReject) Error(ctx context.Context) error { if ctx != nil { r.logger.DebugContext(ctx, "dropped due to flooding") } - return &RejectedError{tun.ErrDrop} + return &RejectedError{ErrDrop} } return returnErr } diff --git a/route/rule/rule_item_rule_set_test.go b/route/rule/rule_item_rule_set_test.go index 21d2070d9..70d440f41 100644 --- a/route/rule/rule_item_rule_set_test.go +++ b/route/rule/rule_item_rule_set_test.go @@ -5,10 +5,8 @@ import ( "net" "sync/atomic" "testing" - "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-tun" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/x/list" @@ -22,8 +20,8 @@ type ruleSetItemTestRouter struct { func (r *ruleSetItemTestRouter) Start(adapter.StartStage) error { return nil } func (r *ruleSetItemTestRouter) Close() error { return nil } -func (r *ruleSetItemTestRouter) PreMatch(adapter.InboundContext, tun.DirectRouteContext, time.Duration, bool) (tun.DirectRouteDestination, error) { - return nil, nil +func (r *ruleSetItemTestRouter) PreMatch(adapter.InboundContext) adapter.PreMatchResult { + return adapter.PreMatchResult{} } func (r *ruleSetItemTestRouter) RouteConnection(context.Context, net.Conn, adapter.InboundContext) error { diff --git a/transport/wireguard/device.go b/transport/wireguard/device.go index 81d610eae..0cfbce6ff 100644 --- a/transport/wireguard/device.go +++ b/transport/wireguard/device.go @@ -5,7 +5,6 @@ import ( "net/netip" "time" - "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" @@ -45,8 +44,3 @@ func NewDevice(options DeviceOptions) (Device, error) { return newSystemStackDevice(options) } } - -type NatDevice interface { - Device - CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) -} diff --git a/transport/wireguard/device_nat.go b/transport/wireguard/device_nat.go deleted file mode 100644 index d214b737b..000000000 --- a/transport/wireguard/device_nat.go +++ /dev/null @@ -1,103 +0,0 @@ -package wireguard - -import ( - "context" - "sync/atomic" - "time" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/ping" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/logger" -) - -var _ Device = (*natDeviceWrapper)(nil) - -type natDeviceWrapper struct { - Device - ctx context.Context - logger logger.ContextLogger - packetOutbound chan *buf.Buffer - rewriter *ping.SourceRewriter - buffer [][]byte -} - -func NewNATDevice(ctx context.Context, logger logger.ContextLogger, upstream Device) NatDevice { - wrapper := &natDeviceWrapper{ - Device: upstream, - ctx: ctx, - logger: logger, - packetOutbound: make(chan *buf.Buffer, 256), - rewriter: ping.NewSourceRewriter(ctx, logger, upstream.Inet4Address(), upstream.Inet6Address()), - } - return wrapper -} - -func (d *natDeviceWrapper) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) { - select { - case packet := <-d.packetOutbound: - defer packet.Release() - sizes[0] = copy(bufs[0][offset:], packet.Bytes()) - return 1, nil - default: - } - return d.Device.Read(bufs, sizes, offset) -} - -func (d *natDeviceWrapper) Write(bufs [][]byte, offset int) (int, error) { - for _, buffer := range bufs { - handled, err := d.rewriter.WriteBack(buffer[offset:]) - if handled { - if err != nil { - return 0, err - } - } else { - d.buffer = append(d.buffer, buffer) - } - } - if len(d.buffer) > 0 { - _, err := d.Device.Write(d.buffer, offset) - if err != nil { - return 0, err - } - d.buffer = d.buffer[:0] - } - return 0, nil -} - -func (d *natDeviceWrapper) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - ctx := log.ContextWithNewID(d.ctx) - session := tun.DirectRouteSession{ - Source: metadata.Source.Addr, - Destination: metadata.Destination.Addr, - } - d.rewriter.CreateSession(session, routeContext) - d.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) - return &natDestination{device: d, session: session}, nil -} - -var _ tun.DirectRouteDestination = (*natDestination)(nil) - -type natDestination struct { - device *natDeviceWrapper - session tun.DirectRouteSession - closed atomic.Bool -} - -func (d *natDestination) WritePacket(buffer *buf.Buffer) error { - d.device.rewriter.RewritePacket(buffer.Bytes()) - d.device.packetOutbound <- buffer - return nil -} - -func (d *natDestination) Close() error { - d.closed.Store(true) - d.device.rewriter.DeleteSession(d.session) - return nil -} - -func (d *natDestination) IsClosed() bool { - return d.closed.Load() -} diff --git a/transport/wireguard/device_stack.go b/transport/wireguard/device_stack.go index 2ca5cd6f8..949575e51 100644 --- a/transport/wireguard/device_stack.go +++ b/transport/wireguard/device_stack.go @@ -8,7 +8,6 @@ import ( "net/netip" "os" "sync" - "time" "github.com/sagernet/gvisor/pkg/buffer" "github.com/sagernet/gvisor/pkg/tcpip" @@ -20,10 +19,7 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/transport/icmp" "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" @@ -32,11 +28,9 @@ import ( wgTun "github.com/sagernet/wireguard-go/tun" ) -var _ NatDevice = (*stackDevice)(nil) +var _ Device = (*stackDevice)(nil) type stackDevice struct { - ctx context.Context - logger log.ContextLogger stack *stack.Stack mtu uint32 events chan wgTun.Event @@ -47,12 +41,11 @@ type stackDevice struct { dispatcher stack.NetworkDispatcher inet4Address netip.Addr inet6Address netip.Addr + icmpForwarder *tun.ICMPForwarder } func newStackDevice(options DeviceOptions) (*stackDevice, error) { tunDevice := &stackDevice{ - ctx: options.Context, - logger: options.Logger, mtu: options.MTU, events: make(chan wgTun.Event, 1), outbound: make(chan *stack.PacketBuffer, 256), @@ -63,10 +56,6 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) { if err != nil { return nil, err } - var ( - inet4Address netip.Addr - inet6Address netip.Addr - ) for _, prefix := range options.Address { addr := tun.AddressFromAddr(prefix.Addr()) protoAddr := tcpip.ProtocolAddress{ @@ -76,12 +65,10 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) { }, } if prefix.Addr().Is4() { - inet4Address = prefix.Addr() - tunDevice.inet4Address = inet4Address + tunDevice.inet4Address = prefix.Addr() protoAddr.Protocol = ipv4.ProtocolNumber } else { - inet6Address = prefix.Addr() - tunDevice.inet6Address = inet6Address + tunDevice.inet6Address = prefix.Addr() protoAddr.Protocol = ipv6.ProtocolNumber } gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{}) @@ -93,10 +80,10 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) { if options.Handler != nil { ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket) ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket) - icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Logger, options.Handler, options.ICMPTimeout) - icmpForwarder.SetLocalAddresses(inet4Address, inet6Address) + icmpForwarder := tun.NewICMPForwarder(ipStack, options.Handler, options.Logger) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) + tunDevice.icmpForwarder = icmpForwarder } return tunDevice, nil } @@ -255,6 +242,9 @@ func (w *stackDevice) Close() error { w.closeOnce.Do(func() { close(w.done) close(w.events) + if w.icmpForwarder != nil { + w.icmpForwarder.Close() + } w.stack.Close() for _, endpoint := range w.stack.CleanupEndpoints() { endpoint.Abort() @@ -268,23 +258,6 @@ func (w *stackDevice) BatchSize() int { return 1 } -func (w *stackDevice) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - ctx := log.ContextWithNewID(w.ctx) - destination, err := ping.ConnectGVisor( - ctx, w.logger, - metadata.Source.Addr, metadata.Destination.Addr, - routeContext, - w.stack, - w.inet4Address, w.inet6Address, - timeout, - ) - if err != nil { - return nil, err - } - w.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) - return destination, nil -} - var _ stack.LinkEndpoint = (*wireEndpoint)(nil) type wireEndpoint stackDevice diff --git a/transport/wireguard/device_system_stack.go b/transport/wireguard/device_system_stack.go index 3414b9a41..0db1297c5 100644 --- a/transport/wireguard/device_system_stack.go +++ b/transport/wireguard/device_system_stack.go @@ -3,10 +3,8 @@ package wireguard import ( - "context" "net/netip" "sync" - "time" "github.com/sagernet/gvisor/pkg/buffer" "github.com/sagernet/gvisor/pkg/tcpip" @@ -17,12 +15,8 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/transport/icmp" "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/ping" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/logger" "github.com/sagernet/wireguard-go/device" ) @@ -30,12 +24,11 @@ var _ Device = (*systemStackDevice)(nil) type systemStackDevice struct { *systemDevice - ctx context.Context - logger logger.ContextLogger - stack *stack.Stack - endpoint *deviceEndpoint - writeBufs [][]byte - closeOnce sync.Once + stack *stack.Stack + endpoint *deviceEndpoint + icmpForwarder *tun.ICMPForwarder + writeBufs [][]byte + closeOnce sync.Once } func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) { @@ -51,10 +44,6 @@ func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) { if err != nil { return nil, err } - var ( - inet4Address netip.Addr - inet6Address netip.Addr - ) for _, prefix := range options.Address { addr := tun.AddressFromAddr(prefix.Addr()) protoAddr := tcpip.ProtocolAddress{ @@ -64,10 +53,8 @@ func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) { }, } if prefix.Addr().Is4() { - inet4Address = prefix.Addr() protoAddr.Protocol = ipv4.ProtocolNumber } else { - inet6Address = prefix.Addr() protoAddr.Protocol = ipv6.ProtocolNumber } gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{}) @@ -75,21 +62,20 @@ func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) { return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", gErr.String()) } } - if options.Handler != nil { - ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket) - ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket) - icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Logger, options.Handler, options.ICMPTimeout) - icmpForwarder.SetLocalAddresses(inet4Address, inet6Address) - ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) - ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) - } - return &systemStackDevice{ - ctx: options.Context, - logger: options.Logger, + stackDevice := &systemStackDevice{ systemDevice: system, stack: ipStack, endpoint: endpoint, - }, nil + } + if options.Handler != nil { + ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket) + icmpForwarder := tun.NewICMPForwarder(ipStack, options.Handler, options.Logger) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) + stackDevice.icmpForwarder = icmpForwarder + } + return stackDevice, nil } func (w *systemStackDevice) SetDevice(device *device.Device) { @@ -129,6 +115,9 @@ func (w *systemStackDevice) Close() error { var err error w.closeOnce.Do(func() { close(w.endpoint.done) + if w.icmpForwarder != nil { + w.icmpForwarder.Close() + } w.stack.Close() for _, endpoint := range w.stack.CleanupEndpoints() { endpoint.Abort() @@ -165,23 +154,6 @@ func (w *systemStackDevice) writeStack(packet []byte) bool { return true } -func (w *systemStackDevice) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - ctx := log.ContextWithNewID(w.ctx) - destination, err := ping.ConnectGVisor( - ctx, w.logger, - metadata.Source.Addr, metadata.Destination.Addr, - routeContext, - w.stack, - w.inet4Address, w.inet6Address, - timeout, - ) - if err != nil { - return nil, err - } - w.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) - return destination, nil -} - type deviceEndpoint struct { mtu uint32 done chan struct{} diff --git a/transport/wireguard/endpoint.go b/transport/wireguard/endpoint.go index c86825889..ce650ef1b 100644 --- a/transport/wireguard/endpoint.go +++ b/transport/wireguard/endpoint.go @@ -10,12 +10,9 @@ import ( "os" "reflect" "strings" - "time" "unsafe" - "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" - "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" @@ -35,7 +32,7 @@ type Endpoint struct { ipcConf string allowedAddress []netip.Prefix tunDevice Device - natDevice NatDevice + returnDevice *returnDeviceWrapper device *device.Device allowedIPs *device.AllowedIPs pause pause.Manager @@ -120,17 +117,13 @@ func NewEndpoint(options EndpointOptions) (*Endpoint, error) { if err != nil { return nil, E.Cause(err, "create WireGuard device") } - natDevice, isNatDevice := tunDevice.(NatDevice) - if !isNatDevice { - natDevice = NewNATDevice(options.Context, options.Logger, tunDevice) - } return &Endpoint{ options: options, peers: peers, ipcConf: ipcConf, allowedAddress: allowedAddresses, tunDevice: tunDevice, - natDevice: natDevice, + returnDevice: &returnDeviceWrapper{Device: tunDevice}, }, nil } @@ -157,7 +150,11 @@ func (e *Endpoint) Start(resolve bool) error { var bind conn.Bind wgListener, isWgListener := common.Cast[dialer.WireGuardListener](e.options.Dialer) if isWgListener { - bind = conn.NewStdNetBind(wgListener.WireGuardControl()) + stdBind := conn.NewStdNetBind(wgListener.WireGuardControl()) + if e.options.ListenPort == 0 && len(e.peers) == 1 && e.peers[0].endpoint.IsValid() { + stdBind.(*conn.StdNetBind).SetSinglePeerMode() + } + bind = stdBind } else { var ( isConnect bool @@ -190,13 +187,7 @@ func (e *Endpoint) Start(resolve bool) error { e.options.Logger.Error(fmt.Sprintf(strings.ToLower(format), args...)) }, } - var deviceInput Device - if e.natDevice != nil { - deviceInput = e.natDevice - } else { - deviceInput = e.tunDevice - } - wgDevice := device.NewDevice(e.options.Context, deviceInput, bind, logger, e.options.Workers) + wgDevice := device.NewDevice(e.options.Context, e.returnDevice, bind, logger, e.options.Workers) e.tunDevice.SetDevice(wgDevice) var ipcConf strings.Builder ipcConf.WriteString(e.ipcConf) @@ -251,13 +242,6 @@ func (e *Endpoint) Lookup(address netip.Addr) *device.Peer { return e.allowedIPs.Lookup(address.AsSlice()) } -func (e *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - if e.natDevice == nil { - return nil, os.ErrInvalid - } - return e.natDevice.CreateDestination(metadata, routeContext, timeout) -} - func (e *Endpoint) onPauseUpdated(event int) { switch event { case pause.EventDevicePaused, pause.EventNetworkPause: diff --git a/transport/wireguard/port.go b/transport/wireguard/port.go new file mode 100644 index 000000000..f410bad52 --- /dev/null +++ b/transport/wireguard/port.go @@ -0,0 +1,157 @@ +package wireguard + +import ( + "net/netip" + "sync/atomic" + + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/gtcpip/header" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/wireguard-go/device" +) + +func (e *Endpoint) PortAddresses() (netip.Addr, netip.Addr) { + return e.tunDevice.Inet4Address(), e.tunDevice.Inet6Address() +} + +func (e *Endpoint) PortMTU() uint32 { + return e.options.MTU +} + +func (e *Endpoint) WritePackets(packets [][]byte) error { + wgDevice := e.device + if wgDevice == nil { + return E.New("WireGuard device is not ready") + } + packetRefs := make([]*device.InputPacketRef, 0, len(packets)) + refs := make([]device.InputPacketRef, len(packets)) + packetSlices := make([][]byte, len(packets)) + for i, packet := range packets { + if len(packet) == 0 { + continue + } + var destination []byte + switch header.IPVersion(packet) { + case header.IPv4Version: + if len(packet) < header.IPv4MinimumSize { + continue + } + destination = header.IPv4(packet).DestinationAddressSlice() + case header.IPv6Version: + if len(packet) < header.IPv6MinimumSize { + continue + } + destination = header.IPv6(packet).DestinationAddressSlice() + default: + continue + } + packetSlices[i] = packet + refs[i] = device.InputPacketRef{ + Destination: destination, + PacketSlices: packetSlices[i : i+1], + } + packetRefs = append(packetRefs, &refs[i]) + } + if len(packetRefs) == 0 { + return nil + } + unmatchedRefs := wgDevice.InputPackets(packetRefs) + if len(unmatchedRefs) == 0 { + return nil + } + state := e.returnDevice.state.Load() + if state == nil { + return nil + } + var replies [][]byte + for _, packetRef := range unmatchedRefs { + packet := packetRef.PacketSlices[0] + var source netip.Addr + if header.IPVersion(packet) == header.IPv4Version { + source = e.tunDevice.Inet4Address() + } else { + source = e.tunDevice.Inet6Address() + } + reply, replyOk := tun.BuildUnreachable(packet, source, state.headroom) + if replyOk { + replies = append(replies, reply) + } + } + if len(replies) > 0 { + state.returnPath.ReturnPackets(replies) + } + return nil +} + +func (e *Endpoint) AttachReturn(returnPath tun.Return) error { + headroom := returnPath.ReturnHeadroom() + if headroom > device.MessageTransportOffsetContent { + return E.New("return path headroom ", headroom, " exceeds available ", device.MessageTransportOffsetContent) + } + newState := &returnPathState{ + returnPath: returnPath, + headroom: headroom, + } + for { + currentState := e.returnDevice.state.Load() + if currentState != nil { + if currentState.returnPath == returnPath { + return nil + } + return E.New("return path already attached") + } + if e.returnDevice.state.CompareAndSwap(nil, newState) { + return nil + } + } +} + +func (e *Endpoint) DetachReturn(returnPath tun.Return) error { + currentState := e.returnDevice.state.Load() + if currentState != nil && currentState.returnPath == returnPath { + e.returnDevice.state.CompareAndSwap(currentState, nil) + } + return nil +} + +type returnPathState struct { + returnPath tun.Return + headroom int +} + +type returnDeviceWrapper struct { + Device + state atomic.Pointer[returnPathState] +} + +func (d *returnDeviceWrapper) Write(bufs [][]byte, offset int) (int, error) { + state := d.state.Load() + if state == nil || len(bufs) == 0 { + return d.Device.Write(bufs, offset) + } + packets := make([][]byte, len(bufs)) + for i, packet := range bufs { + // wireguard-go leaves device.MessageTransportOffsetContent writable bytes in front of the decrypted packet. + packets[i] = packet[offset-state.headroom:] + } + unconsumed := state.returnPath.ReturnPackets(packets) + if len(unconsumed) == 0 { + return 0, nil + } + if len(unconsumed) == len(bufs) { + return d.Device.Write(bufs, offset) + } + remaining := make([][]byte, 0, len(unconsumed)) + searchIndex := 0 + for _, packet := range unconsumed { + for searchIndex < len(bufs) && &packet[0] != &bufs[searchIndex][offset-state.headroom] { + searchIndex++ + } + if searchIndex == len(bufs) { + break + } + remaining = append(remaining, bufs[searchIndex]) + searchIndex++ + } + return d.Device.Write(remaining, offset) +}