Add L3 forwarding support

This commit is contained in:
世界
2026-07-06 14:26:32 +08:00
parent 585d3e639e
commit e6419b945f
24 changed files with 763 additions and 646 deletions
+3 -3
View File
@@ -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 {
+70 -2
View File
@@ -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
+4 -3
View File
@@ -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))
}
+23 -2
View File
@@ -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.
+19 -2
View File
@@ -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)。
+4 -4
View File
@@ -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.2-0.20260525051024-9467ede27fb7
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
+8 -8
View File
@@ -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.2-0.20260525051024-9467ede27fb7 h1:hFLPJ21uNZSbRnzhOKz4Zv0b4F93mpDorWyN93BeRcM=
@@ -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=
+39 -27
View File
@@ -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
}
+36 -8
View File
@@ -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) {
-10
View File
@@ -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()
-16
View File
@@ -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
+14 -104
View File
@@ -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
+133
View File
@@ -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
}
+25 -59
View File
@@ -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) {
+36 -36
View File
@@ -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)
}
+146 -138
View File
@@ -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
+8 -4
View File
@@ -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
}
+2 -4
View File
@@ -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 {
-6
View File
@@ -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)
}
-103
View File
@@ -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()
}
+9 -36
View File
@@ -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
+19 -47
View File
@@ -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{}
+8 -24
View File
@@ -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:
+157
View File
@@ -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)
}