diff --git a/adapter/inbound.go b/adapter/inbound.go index 44b327372..8717a05ee 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -94,6 +94,7 @@ type InboundContext struct { SourceHostname string QueryType uint16 FakeIP bool + PreMatch bool // rule cache diff --git a/adapter/outbound.go b/adapter/outbound.go index 3108b756b..a1a420ace 100644 --- a/adapter/outbound.go +++ b/adapter/outbound.go @@ -22,14 +22,14 @@ type Outbound interface { type OutboundWithPreferredRoutes interface { Outbound - PreferredDomain(domain string) bool - PreferredAddress(address netip.Addr) bool + PreferredDomain(metadata *InboundContext, domain string) bool + PreferredAddress(metadata *InboundContext, address netip.Addr) bool } type FlowOutbound interface { Outbound tun.Port - SupportsFlow(network string) bool + PreMatchFlow(network string, destination netip.Addr) PreMatchAction } type OutboundRegistry interface { diff --git a/docs/configuration/outbound/bridge.md b/docs/configuration/outbound/bridge.md index f3223c35c..d33827325 100644 --- a/docs/configuration/outbound/bridge.md +++ b/docs/configuration/outbound/bridge.md @@ -18,6 +18,14 @@ icon: material/new-box or other L3 endpoints via the `route` action in [Pre-match](/configuration/shared/pre-match/); L4 connections will be rejected. +Traffic to local addresses of the machine (loopback, or addresses assigned to its +network interfaces) will be rejected. + +It is recommended to use [`preferred_by`](/configuration/route/rule/#preferred_by) +as a gate in the `route` rule: it only matches in +[pre-match](/configuration/shared/pre-match/) and excludes local addresses that +cannot be routed. + ### Structure ```json diff --git a/docs/configuration/outbound/bridge.zh.md b/docs/configuration/outbound/bridge.zh.md index 76f574117..f4d7974fc 100644 --- a/docs/configuration/outbound/bridge.zh.md +++ b/docs/configuration/outbound/bridge.zh.md @@ -16,6 +16,11 @@ icon: material/new-box 通过[预匹配](/zh/configuration/shared/pre-match/)中的 `route` 动作,将 L3 流量从 TUN 或其他 L3 endpoints 路由到它;L4 连接将被拒绝。 +到本机本地地址(loopback 或分配给本机网络接口的地址)的流量将被拒绝。 + +建议使用 [`preferred_by`](/zh/configuration/route/rule/#preferred_by) 作为 `route` +规则的门禁:它仅在[预匹配](/zh/configuration/shared/pre-match/)中匹配,且排除了无法路由的本地地址。 + ### 结构 ```json diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 97bbe3760..6d95c9312 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -465,10 +465,11 @@ See [Wi-Fi State](/configuration/shared/wifi-state/) for details. Match specified outbounds' preferred routes. -| Type | Match | -|-------------|-----------------------------------------------| -| `tailscale` | Match MagicDNS domains and peers' allowed IPs | -| `wireguard` | Match peers's allowed IPs | +| Type | Match | +|-------------|----------------------------------------------------| +| `tailscale` | Match MagicDNS domains and peers' allowed IPs | +| `wireguard` | Match peers's allowed IPs | +| `bridge` | Match all addresses except local addresses of the machine, only in [pre-match](/configuration/shared/pre-match/) | #### source_mac_address diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index d55b565dd..87d24df77 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -467,6 +467,7 @@ icon: material/new-box |-------------|--------------------------------| | `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs | | `wireguard` | 匹配对端的 allowed IPs | +| `bridge` | 匹配除本机本地地址外的所有地址,仅在[预匹配](/zh/configuration/shared/pre-match/)中 | #### source_mac_address diff --git a/experimental/libbox/bridge_service_darwin.go b/experimental/libbox/bridge_service_darwin.go index b9919afc4..3a9db58d6 100644 --- a/experimental/libbox/bridge_service_darwin.go +++ b/experimental/libbox/bridge_service_darwin.go @@ -3,12 +3,8 @@ package libbox import ( - "context" "net/netip" - "os" - "time" - "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/protocol/bridge" E "github.com/sagernet/sing/common/exceptions" ) @@ -35,8 +31,6 @@ func NewBridgeService(options *BridgeOptions) (BridgeSession, error) { } serviceOptions.Inet6Port = inet6Port } - logFactory := log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now()}, os.Stderr, "", nil, false) - serviceOptions.Logger = logFactory.Logger() service, err := bridge.NewService(serviceOptions) if err != nil { return nil, err diff --git a/experimental/libbox/bridge_service_linux.go b/experimental/libbox/bridge_service_linux.go index 0e9920667..6fe5b41dc 100644 --- a/experimental/libbox/bridge_service_linux.go +++ b/experimental/libbox/bridge_service_linux.go @@ -3,12 +3,8 @@ package libbox import ( - "context" "net/netip" - "os" - "time" - "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/protocol/bridge" E "github.com/sagernet/sing/common/exceptions" ) @@ -37,8 +33,6 @@ func NewBridgeService(options *BridgeOptions) (BridgeSession, error) { } serviceOptions.Inet6Port = inet6Port } - logFactory := log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now()}, os.Stderr, "", nil, false) - serviceOptions.Logger = logFactory.Logger() service, err := bridge.NewService(serviceOptions) if err != nil { return nil, err diff --git a/go.mod b/go.mod index 568fe4841..f9a1caf88 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( 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.20260707160752-9162aa68ac1e + github.com/sagernet/sing-tun v0.8.12-0.20260708061915-3b7dd334b582 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 diff --git a/go.sum b/go.sum index 403bb7c8d..ca028e5ea 100644 --- a/go.sum +++ b/go.sum @@ -274,8 +274,8 @@ 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.20260707160752-9162aa68ac1e h1:cMMz0qQtCGT1CxXKo2mheM0hFlPaosYBWBqwZHdokYE= -github.com/sagernet/sing-tun v0.8.12-0.20260707160752-9162aa68ac1e/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= +github.com/sagernet/sing-tun v0.8.12-0.20260708061915-3b7dd334b582 h1:3p7Odviib/LlgsCjE50s2EcF5Oa6fuHbZFSqUf7g6Rc= +github.com/sagernet/sing-tun v0.8.12-0.20260708061915-3b7dd334b582/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= diff --git a/protocol/bridge/backend_darwin.go b/protocol/bridge/backend_darwin.go index 548824fce..e73b8f8e0 100644 --- a/protocol/bridge/backend_darwin.go +++ b/protocol/bridge/backend_darwin.go @@ -2,15 +2,17 @@ package bridge import ( "context" + "errors" "net/netip" "slices" "sync" + "syscall" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/gtcpip/header" - "github.com/sagernet/sing/common/control" + "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/service" @@ -31,8 +33,10 @@ type backendDarwin struct { inet4Local netip.Addr inet6Local netip.Addr + batchTUN tun.DarwinTUN + writeAccess sync.Mutex - writeBuffer []byte + writeBatch []*buf.Buffer pfDevice *pfDevice pfToken uint64 @@ -44,7 +48,7 @@ type backendDarwin struct { func newBackend(ctx context.Context, logger logger.ContextLogger, networkManager adapter.NetworkManager, tag string, options option.BridgeOutboundOptions) (Backend, error) { instance := &backendDarwin{ - writeBuffer: make([]byte, tun.PacketOffset+maxPacketLength), + writeBatch: make([]*buf.Buffer, 0, bridgeWriteBatchSize), } err := instance.init(ctx, logger, networkManager, tag, options) if err != nil { @@ -78,11 +82,13 @@ func (b *backendDarwin) start() error { b.tunName = tun.CalculateInterfaceName(b.bridgeName) b.anchorName = "com.apple/sing-box-" + b.tunName tunInterface, err := tun.New(tun.Options{ - Name: b.tunName, - MTU: bridgeTunMTU, - AutoRoute: false, - InterfaceMonitor: b.networkManager.InterfaceMonitor(), - Logger: b.logger, + Name: b.tunName, + MTU: bridgeTunMTU, + AutoRoute: false, + InterfaceMonitor: b.networkManager.InterfaceMonitor(), + Logger: b.logger, + EXP_ExternalConfiguration: true, + EXP_MultiPendingPackets: true, }) if err != nil { return E.Cause(err, "create bridge tun") @@ -106,11 +112,12 @@ func (b *backendDarwin) start() error { if err != nil { return E.Cause(err, "enable pf") } + b.batchTUN = tunInterface.(tun.DarwinTUN) b.closed = make(chan struct{}) b.readDone = make(chan struct{}) b.registerMonitors(b.syncEgress) b.syncEgress() - go b.readLoop() + go b.batchReadLoop() b.logger.Info("bridge started at ", b.tunName, " (masquerade, egress ", b.egressLabel(), ")") return nil } @@ -137,6 +144,7 @@ func (b *backendDarwin) startPlatform() error { FileDescriptor: session.FileDescriptor(), Logger: b.logger, EXP_ExternalConfiguration: true, + EXP_MultiPendingPackets: true, }) if err != nil { return E.Cause(err, "create bridge tun") @@ -146,40 +154,16 @@ func (b *backendDarwin) startPlatform() error { if err != nil { return E.Cause(err, "start bridge tun") } + b.batchTUN = tunInterface.(tun.DarwinTUN) b.closed = make(chan struct{}) b.readDone = make(chan struct{}) b.registerMonitors(b.syncSessionEgress) b.syncSessionEgress() - go b.readLoop() + go b.batchReadLoop() b.logger.Info("bridge started at ", b.tunName, " (platform, egress ", b.egressLabel(), ")") return nil } -func (b *backendDarwin) registerMonitors(syncFunc func()) { - var unregisterFuncs []func() - networkMonitor := b.networkManager.NetworkMonitor() - if networkMonitor != nil { - networkElement := networkMonitor.RegisterCallback(syncFunc) - unregisterFuncs = append(unregisterFuncs, func() { networkMonitor.UnregisterCallback(networkElement) }) - } else if b.boundInterface != "" { - b.logger.Debug("network monitor unavailable, pinned egress will not track interface changes") - } - if b.boundInterface == "" { - interfaceMonitor := b.networkManager.InterfaceMonitor() - if interfaceMonitor != nil { - interfaceElement := interfaceMonitor.RegisterCallback(func(_ *control.Interface, _ int) { syncFunc() }) - unregisterFuncs = append(unregisterFuncs, func() { interfaceMonitor.UnregisterCallback(interfaceElement) }) - } - } - if len(unregisterFuncs) > 0 { - b.unregister = func() { - for _, unregisterFunc := range unregisterFuncs { - unregisterFunc() - } - } - } -} - func (b *backendDarwin) egressLabel() string { if b.boundInterface != "" { return b.boundInterface @@ -231,18 +215,27 @@ func (b *backendDarwin) PortMTU() uint32 { func (b *backendDarwin) WritePackets(packets [][]byte) error { b.writeAccess.Lock() defer b.writeAccess.Unlock() - for _, packet := range packets { - if len(packet) == 0 || len(packet) > maxPacketLength { + for len(packets) > 0 { + chunk := packets + if len(chunk) > bridgeWriteBatchSize { + chunk = chunk[:bridgeWriteBatchSize] + } + packets = packets[len(chunk):] + batch := b.writeBatch[:0] + for _, packet := range chunk { + if len(packet) == 0 || len(packet) > maxPacketLength { + continue + } + ipVersion := header.IPVersion(packet) + if ipVersion != header.IPv4Version && ipVersion != header.IPv6Version { + continue + } + batch = append(batch, buf.As(packet)) + } + if len(batch) == 0 { continue } - ipVersion := header.IPVersion(packet) - if ipVersion != header.IPv4Version && ipVersion != header.IPv6Version { - continue - } - buffer := b.writeBuffer[:tun.PacketOffset+len(packet)] - tun.PacketFillHeader(buffer, ipVersion) - copy(buffer[tun.PacketOffset:], packet) - _, err := b.tunInterface.Write(buffer) + err := b.batchTUN.BatchWrite(batch) if err != nil { return err } @@ -250,6 +243,81 @@ func (b *backendDarwin) WritePackets(packets [][]byte) error { return nil } +func (b *backendDarwin) batchReadLoop() { + defer close(b.readDone) + headroom := -1 + var buffers [][]byte + var batch [][]byte + for { + packets, err := b.batchTUN.BatchRead() + if err != nil { + select { + case <-b.closed: + return + default: + } + if E.IsClosed(err) || errors.Is(err, syscall.EBADF) { + return + } + b.logger.Debug(E.Cause(err, "bridge tun read")) + continue + } + if len(packets) == 0 { + continue + } + b.returnAccess.Lock() + returnPaths := b.returnPaths + b.returnAccess.Unlock() + if len(returnPaths) == 0 { + buf.ReleaseMulti(packets) + continue + } + pathHeadroom := returnPaths[0].ReturnHeadroom() + if pathHeadroom != headroom { + headroom = pathHeadroom + buffers = buffers[:0] + } + for len(buffers) < len(packets) { + buffers = append(buffers, make([]byte, headroom+bridgeTunMTU)) + } + batch = batch[:0] + for _, packet := range packets { + payload := packet.Bytes() + if len(payload) == 0 { + continue + } + fixReturnChecksum(payload) + buffer := buffers[len(batch)][:headroom+len(payload)] + copy(buffer[headroom:], payload) + batch = append(batch, buffer) + } + buf.ReleaseMulti(packets) + if len(batch) == 0 { + continue + } + unconsumed := batch + currentHeadroom := headroom + for _, returnPath := range returnPaths { + if len(unconsumed) == 0 { + break + } + nextHeadroom := returnPath.ReturnHeadroom() + if nextHeadroom != currentHeadroom { + rebuffered := make([][]byte, 0, len(unconsumed)) + for _, packet := range unconsumed { + payload := packet[currentHeadroom:] + buffer := make([]byte, nextHeadroom+len(payload)) + copy(buffer[nextHeadroom:], payload) + rebuffered = append(rebuffered, buffer) + } + unconsumed = rebuffered + currentHeadroom = nextHeadroom + } + unconsumed = returnPath.ReturnPackets(unconsumed) + } + } +} + func (b *backendDarwin) syncEgress() { b.egressAccess.Lock() defer b.egressAccess.Unlock() diff --git a/protocol/bridge/backend_linux.go b/protocol/bridge/backend_linux.go index 9111b2c8b..734c6a5df 100644 --- a/protocol/bridge/backend_linux.go +++ b/protocol/bridge/backend_linux.go @@ -20,7 +20,6 @@ import ( const ( defaultBridgeRuleIndex = 100 defaultBridgeTableIndexBase = 2200 - bridgeWriteBatchSize = 32 ) type backendLinux struct { @@ -83,12 +82,12 @@ func (b *backendLinux) start() error { b.tunName = tun.CalculateInterfaceName(b.bridgeName) b.nftTableName = "sing-box-" + b.tunName tunInterface, err := tun.New(tun.Options{ - Name: b.tunName, - MTU: bridgeTunMTU, - GSO: true, - AutoRoute: false, - InterfaceMonitor: b.networkManager.InterfaceMonitor(), - Logger: b.logger, + Name: b.tunName, + MTU: bridgeTunMTU, + GSO: true, + InterfaceMonitor: b.networkManager.InterfaceMonitor(), + Logger: b.logger, + EXP_ExternalConfiguration: true, }) if err != nil { return E.Cause(err, "create bridge tun") @@ -184,6 +183,7 @@ func (b *backendLinux) startPlatform() error { tunInterface, err := tun.New(tun.Options{ Name: b.tunName, MTU: bridgeTunMTU, + GSO: true, FileDescriptor: session.FileDescriptor(), Logger: b.logger, }) @@ -195,9 +195,22 @@ func (b *backendLinux) startPlatform() error { if err != nil { return E.Cause(err, "start bridge tun") } + linuxTUN := tunInterface.(tun.LinuxTUN) + if linuxTUN.BatchSize() > 1 { + b.batchTUN = linuxTUN + b.writeHeadroom = linuxTUN.FrontHeadroom() + b.writeBuffers = make([][]byte, bridgeWriteBatchSize) + for i := range b.writeBuffers { + b.writeBuffers[i] = make([]byte, b.writeHeadroom+maxPacketLength) + } + } b.closed = make(chan struct{}) b.readDone = make(chan struct{}) - go b.readLoop() + if b.batchTUN != nil { + go b.batchReadLoop() + } else { + go b.readLoop() + } monitor := b.networkManager.InterfaceMonitor() if monitor != nil { element := monitor.RegisterCallback(func(_ *control.Interface, _ int) { b.syncSessionEgress() }) diff --git a/protocol/bridge/outbound.go b/protocol/bridge/outbound.go index 086923aca..a85340d6b 100644 --- a/protocol/bridge/outbound.go +++ b/protocol/bridge/outbound.go @@ -4,6 +4,7 @@ import ( "context" "net" "net/netip" + "slices" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" @@ -22,9 +23,10 @@ func RegisterOutbound(registry *outbound.Registry) { } var ( - _ adapter.Outbound = (*Outbound)(nil) - _ adapter.FlowOutbound = (*Outbound)(nil) - _ adapter.Lifecycle = (*Outbound)(nil) + _ adapter.Outbound = (*Outbound)(nil) + _ adapter.FlowOutbound = (*Outbound)(nil) + _ adapter.OutboundWithPreferredRoutes = (*Outbound)(nil) + _ adapter.Lifecycle = (*Outbound)(nil) ) type Backend interface { @@ -34,7 +36,10 @@ type Backend interface { type Outbound struct { outbound.Adapter - backend Backend + logger log.ContextLogger + networkManager adapter.NetworkManager + platformInterface adapter.PlatformInterface + backend Backend } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.BridgeOutboundOptions) (adapter.Outbound, error) { @@ -44,8 +49,11 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } return &Outbound{ - Adapter: outbound.NewAdapter(C.TypeBridge, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil), - backend: outboundBackend, + Adapter: outbound.NewAdapter(C.TypeBridge, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil), + logger: logger, + networkManager: networkManager, + platformInterface: service.FromContext[adapter.PlatformInterface](ctx), + backend: outboundBackend, }, nil } @@ -57,8 +65,41 @@ func (o *Outbound) Close() error { return o.backend.Close() } -func (o *Outbound) SupportsFlow(network string) bool { - return true +func (o *Outbound) PreferredDomain(metadata *adapter.InboundContext, domain string) bool { + return false +} + +func (o *Outbound) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool { + return metadata.PreMatch && !o.isLocalDestination(address) +} + +func (o *Outbound) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction { + if o.isLocalDestination(destination) { + o.logger.Warn("rejected connection to local destination ", destination, ": traffic to local addresses is not supported by bridge, exclude them in route rules") + return adapter.PreMatchReject + } + return adapter.PreMatchFlow +} + +func (o *Outbound) isLocalDestination(destination netip.Addr) bool { + if !destination.IsValid() { + return false + } + destination = destination.Unmap() + if destination.IsLoopback() || destination.IsUnspecified() { + return true + } + if o.platformInterface != nil && slices.Contains(o.platformInterface.MyInterfaceAddress(), destination) { + return true + } + for _, netInterface := range o.networkManager.InterfaceFinder().Interfaces() { + for _, prefix := range netInterface.Addresses { + if prefix.Addr() == destination { + return true + } + } + } + return false } func (o *Outbound) PortAddresses() (netip.Addr, netip.Addr) { @@ -82,9 +123,9 @@ func (o *Outbound) WritePackets(packets [][]byte) error { } func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return nil, E.New("Only L3 traffic is supported by bridge") + return nil, E.New("only L3 traffic is supported by bridge") } func (o *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return nil, E.New("Only L3 traffic is supported by bridge") + return nil, E.New("only L3 traffic is supported by bridge") } diff --git a/protocol/bridge/packet.go b/protocol/bridge/packet.go index 5bbf6a045..64466d63d 100644 --- a/protocol/bridge/packet.go +++ b/protocol/bridge/packet.go @@ -13,9 +13,10 @@ import ( ) const ( - bridgeTunMTU = 1500 - maxPacketLength = 0xffff - bridgeMaxInstances = 254 + bridgeTunMTU = 1500 + maxPacketLength = 0xffff + bridgeMaxInstances = 254 + bridgeWriteBatchSize = 32 ) var ( diff --git a/protocol/bridge/rules_darwin.go b/protocol/bridge/rules_darwin.go index cfb276a60..e8bb62361 100644 --- a/protocol/bridge/rules_darwin.go +++ b/protocol/bridge/rules_darwin.go @@ -63,8 +63,8 @@ func buildBridgeAnchorRules(ruleLogger logger.ContextLogger, tunName string, egr } } // pf rules are last-match: the pass rules below override the route-to pin - // for destinations in connected subnets and for addresses owned by the host - // itself, so they reach local delivery on their own interface. + // for destinations in connected subnets, so the routing table delivers them + // on their own interface. for _, prefix := range localPrefixes { port := inet4Port if !prefix.Addr().Is4() { @@ -72,16 +72,6 @@ func buildBridgeAnchorRules(ruleLogger logger.ContextLogger, tunName string, egr } rules = append(rules, pfPassInRule(tunName, port, prefix)) } - for _, address := range hostAddresses() { - port := inet4Port - if !address.Is4() { - port = inet6Port - } - if !port.IsValid() { - continue - } - rules = append(rules, pfPassInRule(tunName, port, netip.PrefixFrom(address, address.BitLen()))) - } return rules } @@ -153,31 +143,6 @@ func collectLocalSegments(egress string, boundInterface string, inet4Active bool return } -// hostAddresses stands in for pfctl's `self`, which expands to every address -// assigned to any interface at ruleset load time. -func hostAddresses() []netip.Addr { - interfaceAddrs, err := net.InterfaceAddrs() - if err != nil { - return nil - } - var addresses []netip.Addr - for _, interfaceAddr := range interfaceAddrs { - ipNet, isIPNet := interfaceAddr.(*net.IPNet) - if !isIPNet { - continue - } - address, valid := netip.AddrFromSlice(ipNet.IP) - if !valid { - continue - } - address = address.Unmap() - if !slices.Contains(addresses, address) { - addresses = append(addresses, address) - } - } - return addresses -} - func pfScrubRule(egress string, port netip.Addr, maxMSS uint16) pfAnchorRule { rule := pfRule{ Action: pfActionScrub, diff --git a/protocol/bridge/service_linux.go b/protocol/bridge/service_linux.go index 7e5f821be..989298668 100644 --- a/protocol/bridge/service_linux.go +++ b/protocol/bridge/service_linux.go @@ -3,6 +3,7 @@ package bridge import ( "net" "net/netip" + _ "unsafe" "github.com/sagernet/netlink" "github.com/sagernet/sing-tun" @@ -65,10 +66,18 @@ func NewService(options ServiceOptions) (*Service, error) { func (s *Service) start(bridgeName string) error { s.tunName = tun.CalculateInterfaceName(bridgeName) s.nftTableName = "sing-box-" + s.tunName - tunFileDescriptor, err := openBridgeTun(s.tunName) + tunFileDescriptor, err := openTUN(s.tunName, true) if err != nil { return E.Cause(err, "create bridge tun") } + err = setTCPOffload(tunFileDescriptor) + if err != nil { + s.logger.Warn(E.Cause(err, "set TCP offload")) + } + err = setUDPOffload(tunFileDescriptor) + if err != nil { + s.logger.Warn(E.Cause(err, "set UDP offload")) + } s.tunFileDescriptor = tunFileDescriptor tunLink, err := netlink.LinkByName(s.tunName) if err != nil { @@ -223,29 +232,11 @@ func isDefaultDestination(destination *net.IPNet) bool { return ones == 0 } -func openBridgeTun(name string) (int, error) { - tunFileDescriptor, err := unix.Open("/dev/net/tun", unix.O_RDWR, 0) - if err != nil { - tunFileDescriptor, err = unix.Open("/dev/tun", unix.O_RDWR, 0) - } - if err != nil { - return -1, E.Cause(err, "open tun control device") - } - ifreq, err := unix.NewIfreq(name) - if err != nil { - unix.Close(tunFileDescriptor) - return -1, err - } - ifreq.SetUint16(unix.IFF_TUN | unix.IFF_NO_PI) - err = unix.IoctlIfreq(tunFileDescriptor, unix.TUNSETIFF, ifreq) - if err != nil { - unix.Close(tunFileDescriptor) - return -1, E.Cause(err, "TUNSETIFF") - } - err = unix.SetNonblock(tunFileDescriptor, true) - if err != nil { - unix.Close(tunFileDescriptor) - return -1, E.Cause(err, "set nonblock") - } - return tunFileDescriptor, nil -} +//go:linkname openTUN github.com/sagernet/sing-tun.open +func openTUN(name string, vnetHdr bool) (int, error) + +//go:linkname setTCPOffload github.com/sagernet/sing-tun.setTCPOffload +func setTCPOffload(fd int) error + +//go:linkname setUDPOffload github.com/sagernet/sing-tun.setUDPOffload +func setUDPOffload(fd int) error diff --git a/protocol/direct/outbound.go b/protocol/direct/outbound.go index 74552739d..fdf45988b 100644 --- a/protocol/direct/outbound.go +++ b/protocol/direct/outbound.go @@ -155,8 +155,11 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n return conn, nil } -func (h *Outbound) SupportsFlow(network string) bool { - return network == N.NetworkICMP && h.icmpPort != nil +func (h *Outbound) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction { + if network == N.NetworkICMP && h.icmpPort != nil { + return adapter.PreMatchFlow + } + return adapter.PreMatchContinue } func (h *Outbound) PortAddresses() (netip.Addr, netip.Addr) { diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 3b5b754ba..09a4daf26 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -825,7 +825,7 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } -func (t *Endpoint) PreferredDomain(domain string) bool { +func (t *Endpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool { routeDomains := t.routeDomains.Load() if routeDomains == nil { return false @@ -833,7 +833,7 @@ func (t *Endpoint) PreferredDomain(domain string) bool { return routeDomains[strings.ToLower(domain)] } -func (t *Endpoint) PreferredAddress(address netip.Addr) bool { +func (t *Endpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool { routePrefixes := t.routePrefixes.Load() if routePrefixes == nil { return false diff --git a/protocol/tailscale/port.go b/protocol/tailscale/port.go index cac1e15b8..cfeb5eca8 100644 --- a/protocol/tailscale/port.go +++ b/protocol/tailscale/port.go @@ -9,19 +9,13 @@ import ( "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) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction { + return adapter.PreMatchFlow } func (t *Endpoint) PortAddresses() (netip.Addr, netip.Addr) { diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 885819469..f5d7cab35 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -325,17 +325,26 @@ func (t *Inbound) Start(stage adapter.StartStage) error { case adapter.StartStateStart: if t.platformInterface == nil && ((C.IsLinux && !t.tunOptions.GSO) || (C.IsDarwin && !t.tunOptions.EXP_MultiPendingPackets)) { + outboundManager := service.FromContext[adapter.OutboundManager](t.ctx) 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 + for _, outbound := range outboundManager.Outbounds() { + if _, isFlowOutbound := outbound.(adapter.FlowOutbound); isFlowOutbound { + if C.IsLinux { + t.tunOptions.GSO = true + } else { + t.tunOptions.EXP_MultiPendingPackets = true } + break + } + } + for _, endpoint := range endpointManager.Endpoints() { + if _, isFlowOutbound := endpoint.(adapter.FlowOutbound); isFlowOutbound { + if C.IsLinux { + t.tunOptions.GSO = true + } else { + t.tunOptions.EXP_MultiPendingPackets = true + } + break } } } diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index c8d5bf747..9b65ac9cc 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -136,13 +136,8 @@ func (w *Endpoint) Close() error { return w.endpoint.Close() } -func (w *Endpoint) SupportsFlow(network string) bool { - switch network { - case N.NetworkTCP, N.NetworkUDP, N.NetworkICMP: - return true - default: - return false - } +func (w *Endpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction { + return adapter.PreMatchFlow } func (w *Endpoint) PortAddresses() (netip.Addr, netip.Addr) { @@ -276,11 +271,11 @@ func (w *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n return packetConn, nil } -func (w *Endpoint) PreferredDomain(domain string) bool { +func (w *Endpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool { return false } -func (w *Endpoint) PreferredAddress(address netip.Addr) bool { +func (w *Endpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool { if !w.started.Load() { return false } diff --git a/route/route.go b/route/route.go index 980057a7d..caf1f27cc 100644 --- a/route/route.go +++ b/route/route.go @@ -9,7 +9,6 @@ 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" "github.com/sagernet/sing-box/log" @@ -308,6 +307,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m func (r *Router) PreMatch(metadata adapter.InboundContext, firstPacket []byte) adapter.PreMatchResult { ctx := log.ContextWithNewID(r.ctx) + metadata.PreMatch = true 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) { @@ -464,16 +464,13 @@ func (r *Router) preMatchFlow(ctx context.Context, metadata *adapter.InboundCont 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.DebugContext(ctx, "pre-match bypass ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) - return adapter.PreMatchResult{Action: adapter.PreMatchBypass, Outbound: outbound} - } - } + if !isFlowOutbound { return continueResult } + flowAction := flowOutbound.PreMatchFlow(metadata.Network, metadata.Destination.Addr) + if flowAction != adapter.PreMatchFlow { + return adapter.PreMatchResult{Action: flowAction, Outbound: outbound} + } result := adapter.PreMatchResult{Action: adapter.PreMatchFlow, Outbound: outbound} if metadata.Network == N.NetworkUDP { if metadata.UDPTimeout > 0 { @@ -507,6 +504,10 @@ func (r *Router) preMatchFlow(ctx context.Context, metadata *adapter.InboundCont } return adapter.PreMatchResult{Action: adapter.PreMatchReject} } + flowAction = flowOutbound.PreMatchFlow(metadata.Network, newDestination) + if flowAction != adapter.PreMatchFlow { + return adapter.PreMatchResult{Action: flowAction, Outbound: outbound} + } result.Destination = netip.AddrPortFrom(newDestination, metadata.Destination.Port) } else if metadata.Destination != packetDestination { result.Destination = metadata.Destination.AddrPort() diff --git a/route/rule/rule_item_preferred_by.go b/route/rule/rule_item_preferred_by.go index 42c8a6278..3d0c19c03 100644 --- a/route/rule/rule_item_preferred_by.go +++ b/route/rule/rule_item_preferred_by.go @@ -50,14 +50,14 @@ func (r *PreferredByItem) Match(metadata *adapter.InboundContext) bool { } if domainHost != "" { for _, outbound := range r.outbounds { - if outbound.PreferredDomain(domainHost) { + if outbound.PreferredDomain(metadata, domainHost) { return true } } } if metadata.Destination.IsIP() { for _, outbound := range r.outbounds { - if outbound.PreferredAddress(metadata.Destination.Addr) { + if outbound.PreferredAddress(metadata, metadata.Destination.Addr) { return true } } @@ -65,7 +65,7 @@ func (r *PreferredByItem) Match(metadata *adapter.InboundContext) bool { if len(metadata.DestinationAddresses) > 0 { for _, address := range metadata.DestinationAddresses { for _, outbound := range r.outbounds { - if outbound.PreferredAddress(address) { + if outbound.PreferredAddress(metadata, address) { return true } }