Add bridge outbound

This commit is contained in:
世界
2026-07-08 00:10:00 +08:00
parent 8274ce41df
commit e2b25f17db
35 changed files with 3586 additions and 12 deletions
+21
View File
@@ -51,6 +51,27 @@ type PlatformInterface interface {
LookupSFTPServer() (string, error)
ReadSystemSSHHostKey() ([]byte, error)
TailscaleHostname() string
UsePlatformBridge() bool
CreateBridge(options BridgeOptions) (BridgeSession, error)
}
type BridgeOptions struct {
BridgeName string
MTU uint32
Inet4Port netip.Addr
Inet6Port netip.Addr
Interface string
RuleIndex int
RouteTable int
}
type BridgeSession interface {
FileDescriptor() int
Name() string
Inet6Active() bool
SetEgress(interfaceName string) error
Close() error
}
type PlatformUser struct {
+3 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"net"
"net/netip"
"time"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
@@ -42,6 +43,7 @@ type PreMatchResult struct {
Action PreMatchAction
Outbound Outbound
Destination netip.AddrPort
UDPTimeout time.Duration
NewTracker func() tun.FlowTracker
}
@@ -75,7 +77,7 @@ func JudgeFlow(router Router, inbound string, inboundType string, network uint8,
if !isPort {
return tun.FlowVerdict{Action: tun.ActionAccept}
}
verdict := tun.FlowVerdict{Action: tun.ActionFlow, Port: port, NewTracker: result.NewTracker}
verdict := tun.FlowVerdict{Action: tun.ActionFlow, Port: port, UDPTimeout: result.UDPTimeout, NewTracker: result.NewTracker}
if result.Destination.IsValid() {
destinationPort := result.Destination.Port()
if networkName == N.NetworkICMP {
+3
View File
@@ -5,6 +5,7 @@ const (
TypeRedirect = "redirect"
TypeTProxy = "tproxy"
TypeDirect = "direct"
TypeBridge = "bridge"
TypeBlock = "block"
TypeDNS = "dns"
TypeSOCKS = "socks"
@@ -56,6 +57,8 @@ func ProxyDisplayName(proxyType string) string {
return "TProxy"
case TypeDirect:
return "Direct"
case TypeBridge:
return "Bridge"
case TypeBlock:
return "Block"
case TypeDNS:
+69
View File
@@ -0,0 +1,69 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
# Bridge
!!! quote ""
Requires privileges. Supported on Linux, macOS, rooted Android, and jailbroken iOS.
For graphical clients: on macOS, only available in the standalone version and requires the
Root Helper; on Android, requires root permission; on iOS, requires jailbreak.
`bridge` is the L3 counterpart of the `direct` outbound: it forwards L3 connections
(TCP, UDP and ICMP) directly out of a network interface. Route L3 traffic to it from a TUN
or other L3 endpoints via the `route` action in
[Pre-match](/configuration/shared/pre-match/); L4 connections will be rejected.
### Structure
```json
{
"type": "bridge",
"tag": "bridge-out",
"interface": "",
"bridge_name": "",
"iproute2_table_index": 0,
"iproute2_rule_index": 0
}
```
### Fields
#### interface
Interface name for forwarded traffic to egress.
The default interface will be used by default.
Forwarded traffic will be dropped while the interface is unavailable.
#### bridge_name
Custom bridge TUN interface name prefix, `bridge` is used by default.
Not effective on Apple platforms.
#### iproute2_table_index
!!! quote ""
Only supported on Linux, and only takes effect when `interface` is set.
Linux iproute2 table index for pinned egress routes.
`2200` + instance index is used by default.
#### iproute2_rule_index
!!! quote ""
Only supported on Linux.
Linux iproute2 rule start index.
`100` is used by default.
+67
View File
@@ -0,0 +1,67 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
# Bridge
!!! quote ""
需要特权。支持 Linux、macOS、rooted Android 和越狱 iOS。
对于图形客户端:macOS 仅独立版本可用,且需要 Root HelperAndroid 需要 root 权限;iOS 需要越狱。
`bridge``direct` 出站的 L3 版本:它将 L3 连接(TCP、UDP 和 ICMP)直接从网络接口转发出去。
通过[预匹配](/zh/configuration/shared/pre-match/)中的 `route` 动作,将 L3 流量从 TUN
或其他 L3 endpoints 路由到它;L4 连接将被拒绝。
### 结构
```json
{
"type": "bridge",
"tag": "bridge-out",
"interface": "",
"bridge_name": "",
"iproute2_table_index": 0,
"iproute2_rule_index": 0
}
```
### 字段
#### interface
转发流量流出的网络接口名称。
默认使用默认接口。
接口不可用期间,转发流量将被丢弃。
#### bridge_name
自定义 bridge TUN 接口名前缀,默认使用 `bridge`
在 Apple 平台上无效。
#### iproute2_table_index
!!! quote ""
仅支持 Linux,且仅在设置了 `interface` 时生效。
用于固定出口路由的 Linux iproute2 路由表索引。
默认使用 `2200` + 实例索引。
#### iproute2_rule_index
!!! quote ""
仅支持 Linux。
Linux iproute2 规则起始索引。
默认使用 `100`
+1
View File
@@ -18,6 +18,7 @@
| Type | Format |
|----------------|--------------------------------|
| `direct` | [Direct](./direct/) |
| `bridge` | [Bridge](./bridge/) |
| `block` | [Block](./block/) |
| `socks` | [SOCKS](./socks/) |
| `http` | [HTTP](./http/) |
+1
View File
@@ -18,6 +18,7 @@
| 类型 | 格式 |
|----------------|--------------------------------|
| `direct` | [Direct](./direct/) |
| `bridge` | [Bridge](./bridge/) |
| `block` | [Block](./block/) |
| `socks` | [SOCKS](./socks/) |
| `http` | [HTTP](./http/) |
+2 -2
View File
@@ -43,8 +43,8 @@ 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.
- ICMP connections: Direct and Bridge outbounds, and WireGuard / Tailscale endpoints.
- TCP and UDP connections: Bridge outbounds, and WireGuard / 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.
+2 -2
View File
@@ -39,8 +39,8 @@ icon: material/new-box
支持的目标:
- ICMP 连接:direct 出站和 WireGuard / Tailscale 端点。
- TCP 和 UDP 连接:WireGuard Tailscale 端点。
- ICMP 连接:direct 和 bridge 出站以及 WireGuard / Tailscale 端点。
- TCP 和 UDP 连接:bridge 出站以及 WireGuard / Tailscale 端点。
当没有规则匹配且默认出站为受支持的目标时,L3 转发同样生效;对于出站组,使用当前选中的出站。
@@ -0,0 +1,69 @@
//go:build darwin
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"
)
func NewBridgeService(options *BridgeOptions) (BridgeSession, error) {
if options == nil {
return nil, E.New("missing bridge options")
}
serviceOptions := bridge.ServiceOptions{
MTU: int(options.MTU),
Interface: options.Interface,
}
if options.Inet4Port != "" {
inet4Port, err := netip.ParseAddr(options.Inet4Port)
if err != nil {
return nil, E.Cause(err, "parse inet4 port address")
}
serviceOptions.Inet4Port = inet4Port
}
if options.Inet6Port != "" {
inet6Port, err := netip.ParseAddr(options.Inet6Port)
if err != nil {
return nil, E.Cause(err, "parse inet6 port address")
}
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
}
return &bridgeServiceSession{service}, nil
}
type bridgeServiceSession struct {
service *bridge.Service
}
func (s *bridgeServiceSession) FileDescriptor() int32 {
return int32(s.service.FileDescriptor())
}
func (s *bridgeServiceSession) Name() string {
return s.service.Name()
}
func (s *bridgeServiceSession) Inet6Active() bool {
return s.service.Inet6Active()
}
func (s *bridgeServiceSession) SetEgress(interfaceName string) error {
return s.service.SetEgress(interfaceName)
}
func (s *bridgeServiceSession) Close() error {
return s.service.Close()
}
@@ -0,0 +1,71 @@
//go:build linux
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"
)
func NewBridgeService(options *BridgeOptions) (BridgeSession, error) {
if options == nil {
return nil, E.New("missing bridge options")
}
serviceOptions := bridge.ServiceOptions{
BridgeName: options.BridgeName,
MTU: int(options.MTU),
RuleIndex: int(options.RuleIndex),
RouteTable: int(options.RouteTable),
}
if options.Inet4Port != "" {
inet4Port, err := netip.ParseAddr(options.Inet4Port)
if err != nil {
return nil, E.Cause(err, "parse inet4 port address")
}
serviceOptions.Inet4Port = inet4Port
}
if options.Inet6Port != "" {
inet6Port, err := netip.ParseAddr(options.Inet6Port)
if err != nil {
return nil, E.Cause(err, "parse inet6 port address")
}
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
}
return &bridgeServiceSession{service}, nil
}
type bridgeServiceSession struct {
service *bridge.Service
}
func (s *bridgeServiceSession) FileDescriptor() int32 {
return int32(s.service.FileDescriptor())
}
func (s *bridgeServiceSession) Name() string {
return s.service.Name()
}
func (s *bridgeServiceSession) Inet6Active() bool {
return s.service.Inet6Active()
}
func (s *bridgeServiceSession) SetEgress(interfaceName string) error {
return s.service.SetEgress(interfaceName)
}
func (s *bridgeServiceSession) Close() error {
return s.service.Close()
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !linux && !darwin
package libbox
import E "github.com/sagernet/sing/common/exceptions"
func NewBridgeService(options *BridgeOptions) (BridgeSession, error) {
return nil, E.New("bridge service not supported on this platform")
}
+8
View File
@@ -181,6 +181,14 @@ func (s *platformInterfaceStub) TailscaleHostname() string {
return ""
}
func (s *platformInterfaceStub) UsePlatformBridge() bool {
return false
}
func (s *platformInterfaceStub) CreateBridge(options adapter.BridgeOptions) (adapter.BridgeSession, error) {
return nil, os.ErrInvalid
}
func (s *platformInterfaceStub) LookupUser(username string) (*adapter.PlatformUser, error) {
return nil, os.ErrInvalid
}
+20
View File
@@ -27,6 +27,26 @@ type PlatformInterface interface {
LookupSFTPServer() (string, error)
ReadSystemSSHHostKey() (string, error)
TailscaleHostname() string
UsePlatformBridge() bool
CreateBridge(options *BridgeOptions) (BridgeSession, error)
}
type BridgeOptions struct {
BridgeName string
MTU int32
Inet4Port string
Inet6Port string
Interface string
RuleIndex int32
RouteTable int32
}
type BridgeSession interface {
FileDescriptor() int32
Name() string
Inet6Active() bool
SetEgress(interfaceName string) error
Close() error
}
type PlatformUser struct {
+49
View File
@@ -285,6 +285,55 @@ func (w *platformInterfaceWrapper) TailscaleHostname() string {
return w.iif.TailscaleHostname()
}
func (w *platformInterfaceWrapper) UsePlatformBridge() bool {
return w.iif.UsePlatformBridge()
}
func (w *platformInterfaceWrapper) CreateBridge(options adapter.BridgeOptions) (adapter.BridgeSession, error) {
bridgeOptions := &BridgeOptions{
BridgeName: options.BridgeName,
MTU: int32(options.MTU),
Interface: options.Interface,
RuleIndex: int32(options.RuleIndex),
RouteTable: int32(options.RouteTable),
}
if options.Inet4Port.IsValid() {
bridgeOptions.Inet4Port = options.Inet4Port.String()
}
if options.Inet6Port.IsValid() {
bridgeOptions.Inet6Port = options.Inet6Port.String()
}
session, err := w.iif.CreateBridge(bridgeOptions)
if err != nil {
return nil, err
}
return &bridgeSessionWrapper{session}, nil
}
type bridgeSessionWrapper struct {
session BridgeSession
}
func (w *bridgeSessionWrapper) FileDescriptor() int {
return int(w.session.FileDescriptor())
}
func (w *bridgeSessionWrapper) Name() string {
return w.session.Name()
}
func (w *bridgeSessionWrapper) Inet6Active() bool {
return w.session.Inet6Active()
}
func (w *bridgeSessionWrapper) SetEgress(interfaceName string) error {
return w.session.SetEgress(interfaceName)
}
func (w *bridgeSessionWrapper) Close() error {
return w.session.Close()
}
func (w *platformInterfaceWrapper) LookupUser(username string) (*adapter.PlatformUser, error) {
platformUser, err := w.iif.LookupUser(username)
if err != nil {
+3 -3
View File
@@ -40,6 +40,8 @@ require (
github.com/sagernet/gliderssh v0.3.4-0.20260531100337-2194faca5648
github.com/sagernet/gomobile v0.1.12
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
github.com/sagernet/nftables v0.3.0-mod.3
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.3-0.20260706062323-d9787e794aa3
@@ -49,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.20260706153816-c4a2d2afd8b5
github.com/sagernet/sing-tun v0.8.12-0.20260707160752-9162aa68ac1e
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
@@ -148,8 +150,6 @@ require (
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260620135226-def9ff0fb992 // indirect
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260620135226-def9ff0fb992 // indirect
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-mod.2 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
+4 -4
View File
@@ -254,8 +254,8 @@ github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 h1:AzCE2RhBjLJ4WIWc/
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1/go.mod h1:NJKBtm9nVEK3iyOYWsUlrDQuoGh4zJ4KOPhSYVidvQ4=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje+vW5Q0OQ=
github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
github.com/sagernet/nftables v0.3.0-mod.3 h1:CVfbVTd3Z/LQVc1Z3c1hpiriplJ4xDVHjfQCETiN9RA=
github.com/sagernet/nftables v0.3.0-mod.3/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
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=
@@ -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.20260706153816-c4a2d2afd8b5 h1:NKQ3ZWhUrWFqWLP1CxUFAX1e4/JOMMF2TbjkOYzJWj4=
github.com/sagernet/sing-tun v0.8.12-0.20260706153816-c4a2d2afd8b5/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
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-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=
+2
View File
@@ -21,6 +21,7 @@ import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/anytls"
"github.com/sagernet/sing-box/protocol/block"
"github.com/sagernet/sing-box/protocol/bridge"
"github.com/sagernet/sing-box/protocol/direct"
"github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing-box/protocol/http"
@@ -80,6 +81,7 @@ func OutboundRegistry() *outbound.Registry {
registry := outbound.NewRegistry()
direct.RegisterOutbound(registry)
bridge.RegisterOutbound(registry)
block.RegisterOutbound(registry)
+1
View File
@@ -167,6 +167,7 @@ nav:
- Outbound:
- configuration/outbound/index.md
- Direct: configuration/outbound/direct.md
- Bridge: configuration/outbound/bridge.md
- Block: configuration/outbound/block.md
- SOCKS: configuration/outbound/socks.md
- HTTP: configuration/outbound/http.md
+8
View File
@@ -0,0 +1,8 @@
package option
type BridgeOutboundOptions struct {
Interface string `json:"interface,omitempty"`
BridgeName string `json:"bridge_name,omitempty"`
IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"`
IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"`
}
+178
View File
@@ -0,0 +1,178 @@
//go:build linux || darwin
package bridge
import (
"context"
"net/netip"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
type sysctlState struct {
name string
value string
}
type backendBase struct {
ctx context.Context
logger logger.ContextLogger
networkManager adapter.NetworkManager
tag string
index uint32
bridgeName string
tunName string
inet4Port netip.Addr
inet6Port netip.Addr
boundInterface string
tunInterface tun.Tun
returnAccess sync.Mutex
returnPaths []tun.Return
egressAccess sync.Mutex
forwardingRestore []sysctlState
unregister func()
session adapter.BridgeSession
currentEgress string
closeOnce sync.Once
closed chan struct{}
readDone chan struct{}
}
func (b *backendBase) init(ctx context.Context, logger logger.ContextLogger, networkManager adapter.NetworkManager, tag string, options option.BridgeOutboundOptions) error {
index, err := allocateBridgeIndex()
if err != nil {
return err
}
b.ctx = ctx
b.logger = logger
b.networkManager = networkManager
b.tag = tag
b.index = index
b.bridgeName = options.BridgeName
if b.bridgeName == "" {
b.bridgeName = "bridge"
}
b.boundInterface = options.Interface
b.inet4Port = addressAt(bridgeInet4Base, index)
b.inet6Port = addressAt(bridgeInet6Base, index)
return nil
}
func (b *backendBase) PortAddresses() (netip.Addr, netip.Addr) {
return b.inet4Port, b.inet6Port
}
func (b *backendBase) AttachReturn(returnPath tun.Return) error {
b.returnAccess.Lock()
defer b.returnAccess.Unlock()
if slices.Contains(b.returnPaths, returnPath) {
return nil
}
b.returnPaths = append(b.returnPaths[:len(b.returnPaths):len(b.returnPaths)], returnPath)
return nil
}
func (b *backendBase) DetachReturn(returnPath tun.Return) error {
b.returnAccess.Lock()
defer b.returnAccess.Unlock()
returnPaths := make([]tun.Return, 0, len(b.returnPaths))
for _, existing := range b.returnPaths {
if existing != returnPath {
returnPaths = append(returnPaths, existing)
}
}
b.returnPaths = returnPaths
return nil
}
func (b *backendBase) syncSessionEgress() {
b.egressAccess.Lock()
defer b.egressAccess.Unlock()
select {
case <-b.closed:
return
default:
}
egress := b.resolveEgress()
if egress == b.currentEgress {
return
}
err := b.session.SetEgress(egress)
if err != nil {
b.logger.Debug(E.Cause(err, "apply bridge egress ", egress))
return
}
b.currentEgress = egress
if egress == "" {
b.logger.Debug("bridge egress unavailable, dropping forwarded traffic")
} else {
b.logger.Debug("bridge egress ", egress)
}
}
func (b *backendBase) resolveEgress() string {
if b.boundInterface != "" {
return b.boundInterface
}
monitor := b.networkManager.InterfaceMonitor()
if monitor == nil {
return ""
}
defaultInterface := monitor.DefaultInterface()
if defaultInterface == nil {
return ""
}
return defaultInterface.Name
}
func (b *backendBase) readLoop() {
defer close(b.readDone)
buffer := make([]byte, tun.PacketOffset+bridgeTunMTU)
for {
n, err := b.tunInterface.Read(buffer)
if err != nil {
select {
case <-b.closed:
default:
b.logger.Debug(E.Cause(err, "bridge tun read"))
}
return
}
if n <= tun.PacketOffset {
continue
}
packet := buffer[tun.PacketOffset:n]
// On checksum-offloading NICs (notably virtio) the kernel leaves the L4
// checksum uncomputed when the forwarding path TXes to a tun; recompute it.
fixReturnChecksum(packet)
b.deliverReturn(packet)
}
}
func (b *backendBase) deliverReturn(packet []byte) {
b.returnAccess.Lock()
returnPaths := b.returnPaths
b.returnAccess.Unlock()
for _, returnPath := range returnPaths {
headroom := returnPath.ReturnHeadroom()
buffer := make([]byte, headroom+len(packet))
copy(buffer[headroom:], packet)
unconsumed := returnPath.ReturnPackets([][]byte{buffer})
if len(unconsumed) == 0 {
return
}
}
}
+295
View File
@@ -0,0 +1,295 @@
package bridge
import (
"context"
"net/netip"
"slices"
"sync"
"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"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
)
var (
bridgeInet4LocalBase = netip.MustParseAddr("198.51.100.1")
bridgeInet6LocalBase = netip.MustParseAddr("2001:db8:1::1")
)
type backendDarwin struct {
backendBase
// anchorName lives under com.apple/* so the stock pf.conf's wildcard
// nat/scrub/anchor references evaluate our rules without editing it.
anchorName string
inet4Local netip.Addr
inet6Local netip.Addr
writeAccess sync.Mutex
writeBuffer []byte
pfDevice *pfDevice
pfToken uint64
currentRules []pfAnchorRule
platform adapter.PlatformInterface
}
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),
}
err := instance.init(ctx, logger, networkManager, tag, options)
if err != nil {
return nil, err
}
instance.inet4Local = addressAt(bridgeInet4LocalBase, instance.index)
instance.inet6Local = addressAt(bridgeInet6LocalBase, instance.index)
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
if platformInterface != nil && platformInterface.UsePlatformBridge() {
instance.platform = platformInterface
}
return instance, nil
}
func (b *backendDarwin) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
err := b.start()
if err != nil {
b.Close()
return err
}
return nil
}
func (b *backendDarwin) start() error {
if b.platform != nil {
return b.startPlatform()
}
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,
})
if err != nil {
return E.Cause(err, "create bridge tun")
}
b.tunInterface = tunInterface
err = tunInterface.Start()
if err != nil {
return E.Cause(err, "start bridge tun")
}
b.forwardingRestore = enableDarwinForwarding(b.logger, b.inet4Port.IsValid(), b.inet6Port.IsValid())
err = assignBridgePortAddress(b.tunName, b.inet4Local, b.inet4Port)
if err != nil {
return E.Cause(err, "add bridge route")
}
err = assignBridgePortAddress(b.tunName, b.inet6Local, b.inet6Port)
if err != nil {
b.logger.Debug(E.Cause(err, "IPv6 bridge routing unavailable, disabling IPv6 forwarding"))
b.inet6Port = netip.Addr{}
}
err = b.enablePf()
if err != nil {
return E.Cause(err, "enable pf")
}
b.closed = make(chan struct{})
b.readDone = make(chan struct{})
b.registerMonitors(b.syncEgress)
b.syncEgress()
go b.readLoop()
b.logger.Info("bridge started at ", b.tunName, " (masquerade, egress ", b.egressLabel(), ")")
return nil
}
func (b *backendDarwin) startPlatform() error {
session, err := b.platform.CreateBridge(adapter.BridgeOptions{
BridgeName: b.bridgeName,
MTU: bridgeTunMTU,
Inet4Port: b.inet4Port,
Inet6Port: b.inet6Port,
Interface: b.boundInterface,
})
if err != nil {
return E.Cause(err, "create bridge")
}
b.session = session
b.tunName = session.Name()
if !session.Inet6Active() {
b.inet6Port = netip.Addr{}
}
tunInterface, err := tun.New(tun.Options{
Name: b.tunName,
MTU: bridgeTunMTU,
FileDescriptor: session.FileDescriptor(),
Logger: b.logger,
EXP_ExternalConfiguration: true,
})
if err != nil {
return E.Cause(err, "create bridge tun")
}
b.tunInterface = tunInterface
err = tunInterface.Start()
if err != nil {
return E.Cause(err, "start bridge tun")
}
b.closed = make(chan struct{})
b.readDone = make(chan struct{})
b.registerMonitors(b.syncSessionEgress)
b.syncSessionEgress()
go b.readLoop()
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
}
return "auto"
}
func (b *backendDarwin) Close() error {
b.closeOnce.Do(func() {
if b.closed != nil {
close(b.closed)
}
if b.unregister != nil {
b.unregister()
}
if b.pfDevice != nil && b.anchorName != "" {
b.egressAccess.Lock()
_ = b.pfDevice.LoadAnchor(b.anchorName, nil)
b.egressAccess.Unlock()
}
restoreDarwinForwarding(b.forwardingRestore)
b.forwardingRestore = nil
if b.pfDevice != nil {
if b.pfToken != 0 {
_ = b.pfDevice.StopReference(b.pfToken)
}
_ = b.pfDevice.Close()
}
if b.tunInterface != nil {
b.tunInterface.Close()
}
if b.readDone != nil {
<-b.readDone
}
if b.session != nil {
_ = b.session.Close()
}
releaseBridgeIndex(b.index)
})
return nil
}
// Zero tells the dispatcher not to clamp the TCP MSS or fragment; pf and the
// host kernel do both on the forwarding path instead (see buildBridgeAnchorRules).
func (b *backendDarwin) PortMTU() uint32 {
return 0
}
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 {
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)
if err != nil {
return err
}
}
return nil
}
func (b *backendDarwin) syncEgress() {
b.egressAccess.Lock()
defer b.egressAccess.Unlock()
select {
case <-b.closed:
return
default:
}
egress := b.resolveEgress()
var rules []pfAnchorRule
if egress != "" {
rules = buildBridgeAnchorRules(b.logger, b.tunName, egress, b.boundInterface, b.inet4Port, b.inet6Port)
}
if slices.Equal(rules, b.currentRules) {
return
}
err := b.pfDevice.LoadAnchor(b.anchorName, rules)
if err != nil {
b.logger.Debug(E.Cause(err, "apply bridge egress ", egress))
return
}
b.currentRules = rules
if len(rules) == 0 {
b.logger.Debug("bridge egress unavailable, dropping forwarded traffic")
} else {
b.logger.Debug("bridge egress ", egress)
}
}
func (b *backendDarwin) enablePf() error {
device, err := openPfDevice()
if err != nil {
return err
}
token, err := device.StartReference()
if err != nil {
_ = device.Close()
return err
}
b.pfDevice = device
b.pfToken = token
return nil
}
+461
View File
@@ -0,0 +1,461 @@
package bridge
import (
"context"
"net/netip"
"sync"
"github.com/sagernet/netlink"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"golang.org/x/sys/unix"
)
const (
defaultBridgeRuleIndex = 100
defaultBridgeTableIndexBase = 2200
bridgeWriteBatchSize = 32
)
type backendLinux struct {
backendBase
nftTableName string
routeTable int
ruleIndex int
platform adapter.PlatformInterface
batchTUN tun.LinuxTUN
writeAccess sync.Mutex
writeHeadroom int
writeBuffers [][]byte
clampMTU int
}
func newBackend(ctx context.Context, logger logger.ContextLogger, networkManager adapter.NetworkManager, tag string, options option.BridgeOutboundOptions) (Backend, error) {
instance := &backendLinux{}
err := instance.init(ctx, logger, networkManager, tag, options)
if err != nil {
return nil, err
}
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
if platformInterface != nil && platformInterface.UsePlatformBridge() {
instance.platform = platformInterface
}
instance.ruleIndex = options.IPRoute2RuleIndex
if instance.ruleIndex == 0 {
instance.ruleIndex = defaultBridgeRuleIndex
}
if instance.boundInterface != "" || instance.platform != nil {
instance.routeTable = options.IPRoute2TableIndex
if instance.routeTable == 0 {
instance.routeTable = defaultBridgeTableIndexBase + int(instance.index)
}
}
return instance, nil
}
func (b *backendLinux) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
err := b.start()
if err != nil {
b.Close()
return err
}
return nil
}
func (b *backendLinux) start() error {
if b.platform != nil {
return b.startPlatform()
}
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,
})
if err != nil {
return E.Cause(err, "create bridge tun")
}
b.tunInterface = tunInterface
err = tunInterface.Start()
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 {
// handleGRO coalesces same-flow packets by appending into the first
// packet's buffer capacity, up to the 0xffff total length limit.
b.writeBuffers[i] = make([]byte, b.writeHeadroom+maxPacketLength)
}
}
inet6Active, err := setupBridgeNetfilter(b.logger, b.nftTableName, b.tunName, b.inet6Port.IsValid())
if err != nil {
return E.Cause(err, "set up bridge netfilter")
}
if !inet6Active {
b.inet6Port = netip.Addr{}
}
b.forwardingRestore = enableBridgeForwarding(b.logger, b.tunName, b.inet4Port.IsValid(), b.inet6Port.IsValid())
if b.boundInterface != "" {
b.syncEgress()
}
err = setupBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET, b.inet4Port)
if err != nil {
return E.Cause(err, "set up bridge routing")
}
err = setupBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET6, b.inet6Port)
if err != nil {
b.logger.Debug(E.Cause(err, "IPv6 bridge routing unavailable, disabling IPv6 forwarding"))
removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET6, b.inet6Port)
b.inet6Port = netip.Addr{}
}
b.closed = make(chan struct{})
b.readDone = make(chan struct{})
if b.batchTUN != nil {
go b.batchReadLoop()
} else {
go b.readLoop()
}
egress := "auto"
if b.boundInterface != "" {
egress = b.boundInterface
monitor := b.networkManager.NetworkMonitor()
if monitor != nil {
element := monitor.RegisterCallback(func() { b.syncEgress() })
b.unregister = func() { monitor.UnregisterCallback(element) }
} else {
b.logger.Debug("network monitor unavailable, pinned egress will not track interface changes")
}
b.syncEgress()
} else {
monitor := b.networkManager.InterfaceMonitor()
if monitor != nil {
element := monitor.RegisterCallback(func(_ *control.Interface, _ int) { b.updateClamp() })
b.unregister = func() { monitor.UnregisterCallback(element) }
}
b.updateClamp()
}
natMode := "masquerade"
if fullConeSupported() {
natMode = "full-cone NAT"
}
b.logger.Info("bridge started at ", b.tunName, " (", natMode, ", egress ", egress, ")")
return nil
}
func (b *backendLinux) startPlatform() error {
session, err := b.platform.CreateBridge(adapter.BridgeOptions{
BridgeName: b.bridgeName,
MTU: bridgeTunMTU,
Inet4Port: b.inet4Port,
Inet6Port: b.inet6Port,
RuleIndex: b.ruleIndex,
RouteTable: b.routeTable,
})
if err != nil {
return E.Cause(err, "create bridge")
}
b.session = session
b.tunName = session.Name()
if !session.Inet6Active() {
b.inet6Port = netip.Addr{}
}
tunInterface, err := tun.New(tun.Options{
Name: b.tunName,
MTU: bridgeTunMTU,
FileDescriptor: session.FileDescriptor(),
Logger: b.logger,
})
if err != nil {
return E.Cause(err, "create bridge tun")
}
b.tunInterface = tunInterface
err = tunInterface.Start()
if err != nil {
return E.Cause(err, "start bridge tun")
}
b.closed = make(chan struct{})
b.readDone = make(chan struct{})
go b.readLoop()
monitor := b.networkManager.InterfaceMonitor()
if monitor != nil {
element := monitor.RegisterCallback(func(_ *control.Interface, _ int) { b.syncSessionEgress() })
b.unregister = func() { monitor.UnregisterCallback(element) }
}
b.syncSessionEgress()
egress := "auto"
if b.boundInterface != "" {
egress = b.boundInterface
}
b.logger.Info("bridge started at ", b.tunName, " (platform, egress ", egress, ")")
return nil
}
func (b *backendLinux) Close() error {
b.closeOnce.Do(func() {
if b.closed != nil {
close(b.closed)
}
if b.unregister != nil {
b.unregister()
}
if b.tunInterface != nil {
b.tunInterface.Close()
}
if b.readDone != nil {
<-b.readDone
}
if b.session != nil {
_ = b.session.Close()
} else {
b.egressAccess.Lock()
if b.tunName != "" {
cleanupBridgeNetfilter(b.nftTableName)
removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET, b.inet4Port)
removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET6, b.inet6Port)
}
if b.routeTable != 0 {
flushBridgeRouteTable(b.routeTable)
}
b.egressAccess.Unlock()
restoreBridgeForwarding(b.forwardingRestore)
b.forwardingRestore = nil
}
releaseBridgeIndex(b.index)
})
return nil
}
// Zero tells the dispatcher not to clamp the TCP MSS or fragment; the host kernel
// does both on the forwarding path instead (see setupBridgeClampRules).
func (b *backendLinux) PortMTU() uint32 {
return 0
}
func (b *backendLinux) WritePackets(packets [][]byte) error {
if b.batchTUN == nil {
for _, packet := range packets {
if len(packet) == 0 {
continue
}
_, err := b.tunInterface.Write(packet)
if err != nil {
return err
}
}
return nil
}
b.writeAccess.Lock()
defer b.writeAccess.Unlock()
for len(packets) > 0 {
chunk := packets
if len(chunk) > len(b.writeBuffers) {
chunk = chunk[:len(b.writeBuffers)]
}
packets = packets[len(chunk):]
batch := make([][]byte, 0, len(chunk))
for i, packet := range chunk {
if len(packet) == 0 || len(packet) > maxPacketLength {
continue
}
buffer := b.writeBuffers[i][:b.writeHeadroom+len(packet)]
copy(buffer[b.writeHeadroom:], packet)
batch = append(batch, buffer)
}
if len(batch) == 0 {
continue
}
_, err := b.batchTUN.BatchWrite(batch, b.writeHeadroom)
if err != nil {
return err
}
}
return nil
}
// BatchRead completes any kernel-deferred checksums while splitting GRO frames
// (virtio NEEDS_CSUM), so unlike readLoop no checksum fix is needed here.
func (b *backendLinux) batchReadLoop() {
defer close(b.readDone)
batchSize := b.batchTUN.BatchSize()
sizes := make([]int, batchSize)
batch := make([][]byte, 0, batchSize)
headroom := -1
var buffers [][]byte
for {
b.returnAccess.Lock()
returnPaths := b.returnPaths
b.returnAccess.Unlock()
pathHeadroom := 0
if len(returnPaths) > 0 {
pathHeadroom = returnPaths[0].ReturnHeadroom()
}
if pathHeadroom != headroom {
headroom = pathHeadroom
buffers = make([][]byte, batchSize)
for i := range buffers {
buffers[i] = make([]byte, headroom+bridgeTunMTU)
}
}
n, err := b.batchTUN.BatchRead(buffers, headroom, sizes)
if err != nil {
select {
case <-b.closed:
return
default:
}
if E.IsClosed(err) {
return
}
b.logger.Debug(E.Cause(err, "bridge tun read"))
continue
}
if n == 0 || len(returnPaths) == 0 {
continue
}
batch = batch[:0]
for i := range n {
if sizes[i] == 0 {
continue
}
batch = append(batch, buffers[i][:headroom+sizes[i]])
}
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)
}
}
}
// The policy rules default to priority 100/101, ahead of sing-tun auto_route's rules,
// so forwarded packets egress the physical interface instead of looping back into
// a tun.
func (b *backendLinux) syncEgress() {
b.egressAccess.Lock()
defer b.egressAccess.Unlock()
select {
case <-b.closed:
return
default:
}
b.updateClampLocked()
flushBridgeRouteTable(b.routeTable)
link, err := netlink.LinkByName(b.boundInterface)
if err != nil {
for _, family := range activeBridgeFamilies(b.inet6Port) {
blackholeBridgeDefault(b.routeTable, family)
}
b.logger.Debug("pinned egress ", b.boundInterface, " absent, dropping forwarded traffic")
return
}
for _, family := range activeBridgeFamilies(b.inet6Port) {
b.syncEgressFamily(family, link.Attrs().Index)
}
}
func (b *backendLinux) syncEgressFamily(family int, linkIndex int) {
connected, err := netlink.RouteListFiltered(family, &netlink.Route{
LinkIndex: linkIndex,
Table: unix.RT_TABLE_MAIN,
}, netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE)
if err == nil {
for _, route := range connected {
if route.Gw != nil || route.Dst == nil {
continue
}
pinned := route
pinned.Table = b.routeTable
pinned.ILinkIndex = 0
_ = netlink.RouteReplace(&pinned)
}
}
resolved, err := netlink.RouteGetWithOptions(probeAddress(family), &netlink.RouteGetOptions{Oif: b.boundInterface})
if err == nil && len(resolved) > 0 {
defaultRoute := &netlink.Route{
LinkIndex: linkIndex,
Table: b.routeTable,
Dst: defaultDestination(family),
}
if len(resolved[0].Gw) > 0 {
defaultRoute.Gw = resolved[0].Gw
}
err = netlink.RouteReplace(defaultRoute)
if err == nil {
return
}
}
blackholeBridgeDefault(b.routeTable, family)
}
func (b *backendLinux) updateClamp() {
b.egressAccess.Lock()
defer b.egressAccess.Unlock()
select {
case <-b.closed:
return
default:
}
b.updateClampLocked()
}
func (b *backendLinux) updateClampLocked() {
mtu := bridgeTunMTU
egress := b.resolveEgress()
if egress != "" {
mtu = b.egressMTU(egress)
}
if mtu == b.clampMTU {
return
}
err := setupBridgeClamp(b.nftTableName, b.tunName, b.inet4Port, b.inet6Port, mtu)
if err != nil {
b.logger.Debug(E.Cause(err, "update bridge MSS clamp"))
return
}
b.clampMTU = mtu
}
func (b *backendLinux) egressMTU(egress string) int {
iface, err := b.networkManager.InterfaceFinder().ByName(egress)
if err != nil || iface.MTU < 576 || iface.MTU > bridgeTunMTU {
return bridgeTunMTU
}
return iface.MTU
}
+16
View File
@@ -0,0 +1,16 @@
//go:build !linux && !darwin
package bridge
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
func newBackend(ctx context.Context, logger logger.ContextLogger, networkManager adapter.NetworkManager, tag string, options option.BridgeOutboundOptions) (Backend, error) {
return nil, E.New("bridge outbound is only supported on Linux, macOS, Android with ROOT and jailbroken iOS")
}
+11
View File
@@ -0,0 +1,11 @@
package bridge
import (
"unsafe"
)
//go:linkname unixIoctlPtr golang.org/x/sys/unix.ioctlPtr
func unixIoctlPtr(fd int, request uint, arg unsafe.Pointer) error
//go:linkname unixSysctl golang.org/x/sys/unix.sysctl
func unixSysctl(mib []int32, old *byte, oldLen *uintptr, newValue *byte, newLen uintptr) error
+540
View File
@@ -0,0 +1,540 @@
package bridge
import (
"net"
"net/netip"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"github.com/sagernet/netlink"
"github.com/sagernet/nftables"
"github.com/sagernet/nftables/binaryutil"
"github.com/sagernet/nftables/expr"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"golang.org/x/sys/unix"
)
// fullcone is an out-of-tree nftables verb (the nft_fullcone module), absent on
// stock kernels.
var (
fullConeProbeOnce sync.Once
fullConeProbeResult bool
)
func enableBridgeForwarding(logger logger.ContextLogger, tunName string, inet4 bool, inet6 bool) []sysctlState {
var restore []sysctlState
enable := func(path string) {
content, err := os.ReadFile(path)
if err != nil {
logger.Debug(E.Cause(err, "read ", path))
return
}
value := strings.TrimSpace(string(content))
if value == "1" {
return
}
err = os.WriteFile(path, []byte("1"), 0o644)
if err != nil {
logger.Debug(E.Cause(err, "enable ", path))
return
}
restore = append(restore, sysctlState{name: path, value: value})
}
if inet4 {
enable("/proc/sys/net/ipv4/ip_forward")
}
if inet6 {
enable("/proc/sys/net/ipv6/conf/all/forwarding")
}
_ = os.WriteFile("/proc/sys/net/ipv4/conf/"+tunName+"/rp_filter", []byte("2"), 0o644)
return restore
}
func restoreBridgeForwarding(states []sysctlState) {
for _, state := range states {
_ = os.WriteFile(state.name, []byte(state.value), 0o644)
}
}
var (
nftablesProbeOnce sync.Once
nftablesMissing bool
)
// A kernel built without CONFIG_NF_TABLES (common on pre-GKI Android) answers a
// whole nfnetlink batch with a single EOPNOTSUPP ack, while the client waits for
// one ack per batched message and blocks forever; only non-batch requests are
// answered reliably, so probe with a dump before the first batch operation.
func bridgeUseIptables() bool {
nftablesProbeOnce.Do(func() {
nft, err := nftables.New()
if err != nil {
nftablesMissing = true
return
}
_, err = nft.ListTablesOfFamily(nftables.TableFamilyINet)
nftablesMissing = err != nil
})
return nftablesMissing
}
func setupBridgeNetfilter(logger logger.ContextLogger, tableName string, tunName string, inet6 bool) (bool, error) {
if bridgeUseIptables() {
return setupBridgeIptables(logger, tableName, tunName, inet6)
}
err := setupBridgeNftables(tableName, tunName)
if err != nil {
return false, err
}
return inet6, nil
}
func setupBridgeClamp(tableName string, tunName string, inet4Port netip.Addr, inet6Port netip.Addr, mtu int) error {
if bridgeUseIptables() {
return setupBridgeClampIptables(tableName, tunName, inet4Port, inet6Port, mtu)
}
return setupBridgeClampRules(tableName, tunName, inet4Port, inet6Port, mtu)
}
func cleanupBridgeNetfilter(tableName string) {
if bridgeUseIptables() {
cleanupBridgeIptables(tableName)
return
}
cleanupBridgeNftables(tableName)
}
// Bit 30 stays clear of Android netd's fwmark, which occupies bits 0-20 (netid,
// explicit, protected, permission, uid billing).
const bridgeIptablesMark = "0x40000000/0x40000000"
// The libsu root process inherits a PATH without /system/bin.
func iptablesPath(binary string) string {
path, err := exec.LookPath(binary)
if err == nil {
return path
}
if runtime.GOOS == "android" {
return "/system/bin/" + binary
}
return binary
}
func runIptables(binary string, args ...string) error {
output, err := exec.Command(iptablesPath(binary), args...).CombinedOutput()
if err != nil {
return E.Cause(err, binary, " ", strings.Join(args, " "), ": ", strings.TrimSpace(string(output)))
}
return nil
}
// iptables refuses input interface matches in nat POSTROUTING, so the mangle
// FORWARD chain marks packets entering from the bridge tun and the nat chain
// masquerades by mark.
func setupBridgeIptables(logger logger.ContextLogger, tableName string, tunName string, inet6 bool) (bool, error) {
cleanupBridgeIptables(tableName)
err := setupBridgeIptablesFamily("iptables", tableName, tunName)
if err != nil {
cleanupBridgeIptablesFamily("iptables", tableName)
return false, err
}
if !inet6 {
return false, nil
}
err = setupBridgeIptablesFamily("ip6tables", tableName, tunName)
if err != nil {
cleanupBridgeIptablesFamily("ip6tables", tableName)
logger.Debug(E.Cause(err, "IPv6 NAT unavailable, disabling IPv6 forwarding"))
return false, nil
}
return true, nil
}
func setupBridgeIptablesFamily(binary string, tableName string, tunName string) error {
err := runIptables(binary, "-t", "nat", "-N", tableName)
if err != nil {
return err
}
err = runIptables(binary, "-t", "nat", "-A", tableName, "-m", "mark", "--mark", bridgeIptablesMark, "-j", "MASQUERADE")
if err != nil {
return err
}
err = runIptables(binary, "-t", "nat", "-I", "POSTROUTING", "-j", tableName)
if err != nil {
return err
}
err = runIptables(binary, "-t", "mangle", "-N", tableName)
if err != nil {
return err
}
err = runIptables(binary, "-t", "mangle", "-A", tableName, "-i", tunName, "-j", "MARK", "--set-xmark", bridgeIptablesMark)
if err != nil {
return err
}
err = runIptables(binary, "-t", "mangle", "-I", "FORWARD", "-j", tableName)
if err != nil {
return err
}
return setupBridgeFilterAcceptFamily(binary, tableName, tunName)
}
// netd installs an unconditional DROP in the filter FORWARD chain
// (tetherctrl_FORWARD).
func setupBridgeFilterAcceptFamily(binary string, tableName string, tunName string) error {
err := runIptables(binary, "-t", "filter", "-N", tableName)
if err != nil {
return err
}
err = runIptables(binary, "-t", "filter", "-A", tableName, "-i", tunName, "-j", "ACCEPT")
if err != nil {
return err
}
err = runIptables(binary, "-t", "filter", "-A", tableName, "-o", tunName, "-j", "ACCEPT")
if err != nil {
return err
}
return runIptables(binary, "-t", "filter", "-I", "FORWARD", "-j", tableName)
}
func setupBridgeClampIptables(tableName string, tunName string, inet4Port netip.Addr, inet6Port netip.Addr, mtu int) error {
families := []struct {
binary string
port netip.Addr
headerSize int
}{
{"iptables", inet4Port, 40},
{"ip6tables", inet6Port, 60},
}
for _, family := range families {
if !family.port.IsValid() {
continue
}
err := runIptables(family.binary, "-t", "mangle", "-F", tableName)
if err != nil {
return err
}
err = runIptables(family.binary, "-t", "mangle", "-A", tableName, "-i", tunName, "-j", "MARK", "--set-xmark", bridgeIptablesMark)
if err != nil {
return err
}
err = runIptables(family.binary, "-t", "mangle", "-A", tableName, "-i", tunName,
"-p", "tcp", "--tcp-flags", "SYN,RST", "SYN",
"-j", "TCPMSS", "--set-mss", strconv.Itoa(mtu-family.headerSize))
if err != nil {
return err
}
}
return nil
}
func cleanupBridgeIptables(tableName string) {
cleanupBridgeIptablesFamily("iptables", tableName)
cleanupBridgeIptablesFamily("ip6tables", tableName)
}
func cleanupBridgeIptablesFamily(binary string, tableName string) {
cleanupBridgeIptablesTable(binary, "nat", "POSTROUTING", tableName)
cleanupBridgeIptablesTable(binary, "mangle", "FORWARD", tableName)
cleanupBridgeIptablesTable(binary, "filter", "FORWARD", tableName)
}
func cleanupBridgeIptablesTable(binary string, table string, hookChain string, tableName string) {
path := iptablesPath(binary)
_ = exec.Command(path, "-t", table, "-D", hookChain, "-j", tableName).Run()
_ = exec.Command(path, "-t", table, "-F", tableName).Run()
_ = exec.Command(path, "-t", table, "-X", tableName).Run()
}
func setupBridgeFamily(tunName string, ruleIndex int, routeTable int, family int, port netip.Addr) error {
if !port.IsValid() {
return nil
}
link, err := netlink.LinkByName(tunName)
if err != nil {
return err
}
err = netlink.RouteReplace(bridgeFamilyRoute(link.Attrs().Index, family, port))
if err != nil {
return E.Cause(err, "add route")
}
for _, rule := range bridgeFamilyRules(tunName, ruleIndex, routeTable, family, port) {
_ = netlink.RuleDel(rule)
err = netlink.RuleAdd(rule)
if err != nil {
return E.Cause(err, "add rule")
}
}
return nil
}
func removeBridgeFamily(tunName string, ruleIndex int, routeTable int, family int, port netip.Addr) {
if !port.IsValid() {
return
}
link, err := netlink.LinkByName(tunName)
if err == nil {
_ = netlink.RouteDel(bridgeFamilyRoute(link.Attrs().Index, family, port))
}
for _, rule := range bridgeFamilyRules(tunName, ruleIndex, routeTable, family, port) {
_ = netlink.RuleDel(rule)
}
}
func bridgeFamilyRoute(linkIndex int, family int, port netip.Addr) *netlink.Route {
bits := port.BitLen()
route := &netlink.Route{
LinkIndex: linkIndex,
Dst: &net.IPNet{IP: port.AsSlice(), Mask: net.CIDRMask(bits, bits)},
Table: unix.RT_TABLE_MAIN,
}
if family == unix.AF_INET {
route.Scope = netlink.Scope(unix.RT_SCOPE_LINK)
}
return route
}
func bridgeFamilyRules(tunName string, ruleIndex int, routeTable int, family int, port netip.Addr) []*netlink.Rule {
forwardTable := unix.RT_TABLE_MAIN
if routeTable != 0 {
forwardTable = routeTable
}
iifRule := netlink.NewRule()
iifRule.Priority = ruleIndex
iifRule.IifName = tunName
iifRule.Table = forwardTable
iifRule.Family = family
toRule := netlink.NewRule()
toRule.Priority = ruleIndex + 1
toRule.Dst = netip.PrefixFrom(port, port.BitLen())
toRule.Table = unix.RT_TABLE_MAIN
toRule.Family = family
return []*netlink.Rule{iifRule, toRule}
}
func flushBridgeRouteTable(routeTable int) {
for _, family := range []int{unix.AF_INET, unix.AF_INET6} {
routes, err := netlink.RouteListFiltered(family, &netlink.Route{Table: routeTable}, netlink.RT_FILTER_TABLE)
if err != nil {
continue
}
for _, route := range routes {
toDelete := route
_ = netlink.RouteDel(&toDelete)
}
}
}
func blackholeBridgeDefault(routeTable int, family int) {
_ = netlink.RouteReplace(&netlink.Route{
Table: routeTable,
Family: family,
Type: unix.RTN_BLACKHOLE,
Dst: defaultDestination(family),
})
}
func activeBridgeFamilies(inet6Port netip.Addr) []int {
families := []int{unix.AF_INET}
if inet6Port.IsValid() {
families = append(families, unix.AF_INET6)
}
return families
}
func probeAddress(family int) net.IP {
if family == unix.AF_INET6 {
return net.ParseIP("2000::")
}
return net.IPv4(1, 1, 1, 1)
}
func defaultDestination(family int) *net.IPNet {
if family == unix.AF_INET6 {
return &net.IPNet{IP: net.IPv6zero, Mask: net.CIDRMask(0, 128)}
}
return &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)}
}
func setupBridgeNftables(tableName string, tunName string) error {
cleanupBridgeNftables(tableName)
nft, err := nftables.New()
if err != nil {
return err
}
table := nft.AddTable(&nftables.Table{
Family: nftables.TableFamilyINet,
Name: tableName,
})
chain := nft.AddChain(&nftables.Chain{
Name: "postrouting",
Table: table,
Type: nftables.ChainTypeNAT,
Hooknum: nftables.ChainHookPostrouting,
Priority: nftables.ChainPriorityNATSource,
})
// The nft_fullcone verb, like masquerade, sources from the routing-chosen egress
// interface.
var sourceNat expr.Any = &expr.Masq{}
if fullConeSupported() {
sourceNat = &expr.FullCone{}
}
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: nftIfname(tunName)},
sourceNat,
},
})
nft.AddChain(&nftables.Chain{
Name: "forward",
Table: table,
Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookForward,
Priority: nftables.ChainPriorityMangle,
})
return nft.Flush()
}
// nft_exthdr writes the MSS option unconditionally — unlike pf's max-mss or
// xt_TCPMSS it would also raise a smaller advertised MSS — so the rule matches
// only when the advertised MSS exceeds the clamp value.
func setupBridgeClampRules(tableName string, tunName string, inet4Port netip.Addr, inet6Port netip.Addr, mtu int) error {
nft, err := nftables.New()
if err != nil {
return err
}
table := &nftables.Table{
Family: nftables.TableFamilyINet,
Name: tableName,
}
chain := &nftables.Chain{
Name: "forward",
Table: table,
}
nft.FlushChain(chain)
families := []struct {
protocol byte
port netip.Addr
headerSize int
}{
{unix.NFPROTO_IPV4, inet4Port, 40},
{unix.NFPROTO_IPV6, inet6Port, 60},
}
for _, family := range families {
if !family.port.IsValid() {
continue
}
clamp := binaryutil.BigEndian.PutUint16(uint16(mtu - family.headerSize))
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyNFPROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{family.protocol}},
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: nftIfname(tunName)},
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{unix.IPPROTO_TCP}},
&expr.Payload{DestRegister: 1, Base: expr.PayloadBaseTransportHeader, Offset: 13, Len: 1},
&expr.Bitwise{SourceRegister: 1, DestRegister: 1, Len: 1, Mask: []byte{0x02}, Xor: []byte{0x00}},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{0x02}},
&expr.Exthdr{DestRegister: 1, Type: 2, Offset: 2, Len: 2, Op: expr.ExthdrOpTcpopt},
&expr.Cmp{Op: expr.CmpOpGt, Register: 1, Data: clamp},
&expr.Immediate{Register: 1, Data: clamp},
&expr.Exthdr{SourceRegister: 1, Type: 2, Offset: 2, Len: 2, Op: expr.ExthdrOpTcpopt},
},
})
}
return nft.Flush()
}
func cleanupBridgeNftables(tableName string) {
nft, err := nftables.New()
if err != nil {
return
}
table, err := nft.ListTableOfFamily(tableName, nftables.TableFamilyINet)
if err != nil || table == nil {
return
}
nft.DelTable(table)
_ = nft.Flush()
}
func fullConeSupported() bool {
if runtime.GOOS == "android" {
return false
}
if bridgeUseIptables() {
return false
}
fullConeProbeOnce.Do(func() {
fullConeProbeResult = probeFullCone()
})
return fullConeProbeResult
}
const fullConeProbeTable = "sing-box-fullcone-probe"
// The kernel loads and validates the expression's module when the batch commits:
// a clean flush means the verb is available, a rejected one rolls back atomically.
func probeFullCone() bool {
deleteFullConeProbe()
nft, err := nftables.New()
if err != nil {
return false
}
table := nft.AddTable(&nftables.Table{
Family: nftables.TableFamilyINet,
Name: fullConeProbeTable,
})
chain := nft.AddChain(&nftables.Chain{
Name: "postrouting",
Table: table,
Type: nftables.ChainTypeNAT,
Hooknum: nftables.ChainHookPostrouting,
Priority: nftables.ChainPriorityNATSource,
})
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: nftIfname("sing-box-probe0")},
&expr.FullCone{},
},
})
supported := nft.Flush() == nil
deleteFullConeProbe()
return supported
}
func deleteFullConeProbe() {
nft, err := nftables.New()
if err != nil {
return
}
table, err := nft.ListTableOfFamily(fullConeProbeTable, nftables.TableFamilyINet)
if err != nil || table == nil {
return
}
nft.DelTable(table)
_ = nft.Flush()
}
func nftIfname(name string) []byte {
padded := make([]byte, 16)
copy(padded, name)
return padded
}
+90
View File
@@ -0,0 +1,90 @@
package bridge
import (
"context"
"net"
"net/netip"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.BridgeOutboundOptions](registry, C.TypeBridge, NewOutbound)
}
var (
_ adapter.Outbound = (*Outbound)(nil)
_ adapter.FlowOutbound = (*Outbound)(nil)
_ adapter.Lifecycle = (*Outbound)(nil)
)
type Backend interface {
adapter.Lifecycle
tun.Port
}
type Outbound struct {
outbound.Adapter
backend Backend
}
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.BridgeOutboundOptions) (adapter.Outbound, error) {
networkManager := service.FromContext[adapter.NetworkManager](ctx)
outboundBackend, err := newBackend(ctx, logger, networkManager, tag, options)
if err != nil {
return nil, err
}
return &Outbound{
Adapter: outbound.NewAdapter(C.TypeBridge, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil),
backend: outboundBackend,
}, nil
}
func (o *Outbound) Start(stage adapter.StartStage) error {
return o.backend.Start(stage)
}
func (o *Outbound) Close() error {
return o.backend.Close()
}
func (o *Outbound) SupportsFlow(network string) bool {
return true
}
func (o *Outbound) PortAddresses() (netip.Addr, netip.Addr) {
return o.backend.PortAddresses()
}
func (o *Outbound) PortMTU() uint32 {
return o.backend.PortMTU()
}
func (o *Outbound) AttachReturn(returnPath tun.Return) error {
return o.backend.AttachReturn(returnPath)
}
func (o *Outbound) DetachReturn(returnPath tun.Return) error {
return o.backend.DetachReturn(returnPath)
}
func (o *Outbound) WritePackets(packets [][]byte) error {
return o.backend.WritePackets(packets)
}
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")
}
func (o *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return nil, E.New("Only L3 traffic is supported by bridge")
}
+121
View File
@@ -0,0 +1,121 @@
//go:build linux || darwin
package bridge
import (
"net/netip"
"sync"
"github.com/sagernet/sing-tun/gtcpip"
"github.com/sagernet/sing-tun/gtcpip/checksum"
"github.com/sagernet/sing-tun/gtcpip/header"
E "github.com/sagernet/sing/common/exceptions"
)
const (
bridgeTunMTU = 1500
maxPacketLength = 0xffff
bridgeMaxInstances = 254
)
var (
bridgeInet4Base = netip.MustParseAddr("192.0.2.1")
bridgeInet6Base = netip.MustParseAddr("2001:db8::1")
bridgeIndexAccess sync.Mutex
bridgeIndexInUse [bridgeMaxInstances]bool
)
func allocateBridgeIndex() (uint32, error) {
bridgeIndexAccess.Lock()
defer bridgeIndexAccess.Unlock()
for index := range bridgeMaxInstances {
if !bridgeIndexInUse[index] {
bridgeIndexInUse[index] = true
return uint32(index), nil
}
}
return 0, E.New("too many bridge outbounds: limit is ", bridgeMaxInstances)
}
func releaseBridgeIndex(index uint32) {
bridgeIndexAccess.Lock()
defer bridgeIndexAccess.Unlock()
bridgeIndexInUse[index] = false
}
func addressAt(base netip.Addr, offset uint32) netip.Addr {
addr := base
for range offset {
addr = addr.Next()
}
return addr
}
func fixReturnChecksum(packet []byte) {
switch header.IPVersion(packet) {
case header.IPv4Version:
if len(packet) < header.IPv4MinimumSize {
return
}
ipHdr := header.IPv4(packet)
if !ipHdr.IsValid(len(packet)) {
return
}
if ipHdr.Flags()&header.IPv4FlagMoreFragments != 0 || ipHdr.FragmentOffset() != 0 {
return
}
ipHdr.SetChecksum(0)
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
recomputeTransportChecksum(ipHdr.TransportProtocol(), ipHdr.Payload(), ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice())
case header.IPv6Version:
if len(packet) < header.IPv6MinimumSize {
return
}
ipHdr := header.IPv6(packet)
recomputeTransportChecksum(ipHdr.TransportProtocol(), ipHdr.Payload(), ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice())
}
}
func recomputeTransportChecksum(protocol tcpip.TransportProtocolNumber, transport []byte, source []byte, destination []byte) {
switch protocol {
case header.TCPProtocolNumber:
if len(transport) < header.TCPMinimumSize {
return
}
tcpHdr := header.TCP(transport)
tcpHdr.SetChecksum(0)
payloadChecksum := checksum.Checksum(tcpHdr.Payload(), 0)
pseudoChecksum := header.PseudoHeaderChecksum(header.TCPProtocolNumber, source, destination, uint16(len(transport)))
tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(checksum.Combine(pseudoChecksum, payloadChecksum)))
case header.UDPProtocolNumber:
if len(transport) < header.UDPMinimumSize {
return
}
udpHdr := header.UDP(transport)
udpHdr.SetChecksum(0)
payloadChecksum := checksum.Checksum(udpHdr.Payload(), 0)
pseudoChecksum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, source, destination, udpHdr.Length())
udpChecksum := ^udpHdr.CalculateChecksum(checksum.Combine(pseudoChecksum, payloadChecksum))
if udpChecksum == 0 {
udpChecksum = 0xffff
}
udpHdr.SetChecksum(udpChecksum)
case header.ICMPv4ProtocolNumber:
if len(transport) < header.ICMPv4MinimumSize {
return
}
icmpHdr := header.ICMPv4(transport)
icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0))
case header.ICMPv6ProtocolNumber:
if len(transport) < header.ICMPv6MinimumSize {
return
}
icmpHdr := header.ICMPv6(transport)
icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
Header: icmpHdr,
Src: source,
Dst: destination,
}))
}
}
+379
View File
@@ -0,0 +1,379 @@
package bridge
import (
"net"
"net/netip"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/unix"
)
// Layouts and values mirror bsd/net/pfvar.h from xnu, which the SDKs do not
// ship; unchanged from xnu-4570.1.46 (macOS 10.13) through xnu-12377.1.9.
const (
pfRulesetScrub = 0
pfRulesetFilter = 1
pfRulesetNat = 2
pfActionPass = 0
pfActionScrub = 2
pfActionNat = 4
pfDirectionIn = 1
pfAddrTypeAddressMask = 0
pfAddrTypeDynamicInterface = 2
pfRouteActionRouteTo = 2
pfStateNormal = 1
pfNatProxyPortLow = 50001
pfNatProxyPortHigh = 65535
)
type pfAddr [16]byte
type pfAddrWrap struct {
Addr pfAddr
Mask pfAddr
_ uint64
Type uint8
IFlags uint8
_ [6]byte
}
type pfRuleAddr struct {
Addr pfAddrWrap
_ [8]byte
Neg uint8
_ [7]byte
}
type pfPool struct {
_ [2]uint64
_ uint64
_ [16]byte
_ pfAddr
TableIndex int32
ProxyPort [2]uint16
PortOp uint8
Opts uint8
AF uint8
_ [5]byte
}
type pfRuleUserGroup struct {
Range [2]uint32
Op uint8
_ [3]byte
}
type pfRule struct {
Src pfRuleAddr
Dst pfRuleAddr
_ [8]uint64
Label [64]byte
IfName [16]byte
QName [64]byte
PQName [64]byte
TagName [64]byte
MatchTagName [64]byte
OverloadTable [32]byte
_ [2]uint64
RPool pfPool
Evaluations uint64
Packets [2]uint64
Bytes [2]uint64
Ticket uint64
Owner [64]byte
Priority uint32
_ uint32
_ [3]uint64
OSFingerprint uint32
RouteTableID uint32
Timeout [26]uint32
States uint32
MaxStates uint32
SrcNodes uint32
MaxSrcNodes uint32
MaxSrcStates uint32
MaxSrcConn uint32
MaxSrcConnRate [2]uint32
QID uint32
PQID uint32
RouteListID uint32
Nr uint32
Prob uint32
CreatorUID uint32
CreatorPID uint32
ReturnICMP uint16
ReturnICMP6 uint16
MaxMSS uint16
Tag uint16
MatchTag uint16
_ uint16
UID pfRuleUserGroup
GID pfRuleUserGroup
RuleFlag uint32
Action uint8
Direction uint8
Log uint8
LogIf uint8
Quick uint8
IfNot uint8
MatchTagNot uint8
NatPass uint8
KeepState uint8
AF uint8
Proto uint8
Type uint8
Code uint8
Flags uint8
FlagSet uint8
MinTTL uint8
AllowOpts uint8
RouteAction uint8
ReturnTTL uint8
TOS uint8
AnchorRelative uint8
AnchorWildcard uint8
Flush uint8
ProtoVariant uint8
ExtFilter uint8
ExtMap uint8
_ uint16
DummynetPipe uint32
DummynetType uint32
}
type pfPoolAddr struct {
Addr pfAddrWrap
_ [2]uint64
IfName [16]byte
_ uint64
}
type pfiocRule struct {
Action uint32
Ticket uint32
PoolTicket uint32
Nr uint32
Anchor [1024]byte
AnchorCall [1024]byte
Rule pfRule
}
type pfiocPoolAddr struct {
Action uint32
Ticket uint32
Nr uint32
RNum uint32
RAction uint8
RLast uint8
AF uint8
Anchor [1024]byte
_ [5]byte
Addr pfPoolAddr
}
type pfiocTransElement struct {
RulesetIndex int32
Anchor [1024]byte
Ticket uint32
}
type pfiocTrans struct {
Size int32
ElementSize int32
Array *pfiocTransElement
}
type pfiocRemoveToken struct {
Token uint64
RefCount uint64
}
const (
iocParamMask = 0x1fff
iocOut = 0x40000000
iocIn = 0x80000000
iocInOut = iocIn | iocOut
)
const (
diocAddRule = iocInOut | (uint(unsafe.Sizeof(pfiocRule{}))&iocParamMask)<<16 | 'D'<<8 | 4
diocStartRef = iocOut | 8<<16 | 'D'<<8 | 8
diocStopRef = iocInOut | (uint(unsafe.Sizeof(pfiocRemoveToken{}))&iocParamMask)<<16 | 'D'<<8 | 9
diocBeginAddrs = iocInOut | (uint(unsafe.Sizeof(pfiocPoolAddr{}))&iocParamMask)<<16 | 'D'<<8 | 51
diocAddAddr = iocInOut | (uint(unsafe.Sizeof(pfiocPoolAddr{}))&iocParamMask)<<16 | 'D'<<8 | 52
diocXBegin = iocInOut | (uint(unsafe.Sizeof(pfiocTrans{}))&iocParamMask)<<16 | 'D'<<8 | 81
diocXCommit = iocInOut | (uint(unsafe.Sizeof(pfiocTrans{}))&iocParamMask)<<16 | 'D'<<8 | 82
diocXRollback = iocInOut | (uint(unsafe.Sizeof(pfiocTrans{}))&iocParamMask)<<16 | 'D'<<8 | 83
)
type pfAnchorRule struct {
RulesetIndex int32
Rule pfRule
Pool pfPoolAddr
}
type pfDevice struct {
fd int
}
func openPfDevice() (*pfDevice, error) {
fd, err := unix.Open("/dev/pf", unix.O_RDWR|unix.O_CLOEXEC, 0)
if err != nil {
return nil, E.Cause(err, "open /dev/pf")
}
return &pfDevice{fd: fd}, nil
}
func (d *pfDevice) Close() error {
return unix.Close(d.fd)
}
func (d *pfDevice) ioctl(request uint, pointer unsafe.Pointer) error {
return unixIoctlPtr(d.fd, request, pointer)
}
func (d *pfDevice) StartReference() (uint64, error) {
var token uint64
err := d.ioctl(uint(diocStartRef), unsafe.Pointer(&token))
if err != nil {
return 0, E.Cause(err, "DIOCSTARTREF")
}
return token, nil
}
func (d *pfDevice) StopReference(token uint64) error {
remove := pfiocRemoveToken{Token: token}
err := d.ioctl(uint(diocStopRef), unsafe.Pointer(&remove))
if err != nil {
return E.Cause(err, "DIOCSTOPREF")
}
return nil
}
// LoadAnchor atomically replaces the anchor's scrub, nat and filter rulesets;
// empty rules flush the anchor.
func (d *pfDevice) LoadAnchor(anchor string, rules []pfAnchorRule) error {
elements := [3]pfiocTransElement{
{RulesetIndex: pfRulesetScrub},
{RulesetIndex: pfRulesetNat},
{RulesetIndex: pfRulesetFilter},
}
for i := range elements {
copy(elements[i].Anchor[:], anchor)
}
trans := pfiocTrans{
Size: int32(len(elements)),
ElementSize: int32(unsafe.Sizeof(pfiocTransElement{})),
Array: &elements[0],
}
err := d.ioctl(uint(diocXBegin), unsafe.Pointer(&trans))
if err != nil {
return E.Cause(err, "DIOCXBEGIN")
}
for _, rule := range rules {
err = d.addRule(anchor, &elements, rule)
if err != nil {
_ = d.ioctl(uint(diocXRollback), unsafe.Pointer(&trans))
return err
}
}
err = d.ioctl(uint(diocXCommit), unsafe.Pointer(&trans))
if err != nil {
return E.Cause(err, "DIOCXCOMMIT")
}
return nil
}
func (d *pfDevice) addRule(anchor string, elements *[3]pfiocTransElement, rule pfAnchorRule) error {
var pool pfiocPoolAddr
err := d.ioctl(uint(diocBeginAddrs), unsafe.Pointer(&pool))
if err != nil {
return E.Cause(err, "DIOCBEGINADDRS")
}
if rule.Pool != (pfPoolAddr{}) {
pool.Addr = rule.Pool
pool.AF = rule.Rule.AF
err = d.ioctl(uint(diocAddAddr), unsafe.Pointer(&pool))
if err != nil {
return E.Cause(err, "DIOCADDADDR")
}
}
var ticket uint32
for _, element := range elements {
if element.RulesetIndex == rule.RulesetIndex {
ticket = element.Ticket
}
}
request := pfiocRule{
Ticket: ticket,
PoolTicket: pool.Ticket,
Rule: rule.Rule,
}
copy(request.Anchor[:], anchor)
err = d.ioctl(uint(diocAddRule), unsafe.Pointer(&request))
if err != nil {
return E.Cause(err, "DIOCADDRULE")
}
return nil
}
func pfAddrOf(address netip.Addr) (result pfAddr) {
if address.Is4() {
addr4 := address.As4()
copy(result[:], addr4[:])
} else {
addr16 := address.As16()
copy(result[:], addr16[:])
}
return
}
func pfMaskOf(bits int, is4 bool) (result pfAddr) {
totalBits := 128
if is4 {
totalBits = 32
}
copy(result[:], net.CIDRMask(bits, totalBits))
return
}
func pfHostAddress(address netip.Addr) pfAddrWrap {
return pfPrefixAddress(netip.PrefixFrom(address, address.BitLen()))
}
func pfPrefixAddress(prefix netip.Prefix) pfAddrWrap {
return pfAddrWrap{
Type: pfAddrTypeAddressMask,
Addr: pfAddrOf(prefix.Addr()),
Mask: pfMaskOf(prefix.Bits(), prefix.Addr().Is4()),
}
}
func pfDynamicInterfaceAddress(interfaceName string, is4 bool) pfAddrWrap {
wrap := pfAddrWrap{
Type: pfAddrTypeDynamicInterface,
}
if is4 {
wrap.Mask = pfMaskOf(32, true)
} else {
wrap.Mask = pfMaskOf(128, false)
}
copy(wrap.Addr[:], interfaceName)
return wrap
}
func pfFamily(is4 bool) uint8 {
if is4 {
return unix.AF_INET
}
return unix.AF_INET6
}
+223
View File
@@ -0,0 +1,223 @@
package bridge
import (
"net"
"net/netip"
"os"
"sync/atomic"
"syscall"
"unsafe"
"github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
)
var routeMessageSeq atomic.Int32
func interfaceGateway(interfaceIndex int, is4 bool) netip.Addr {
socketFd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
return netip.Addr{}
}
defer unix.Close(socketFd)
_ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 1})
var destination route.Addr
if is4 {
destination = &route.Inet4Addr{}
} else {
destination = &route.Inet6Addr{}
}
seq := int(routeMessageSeq.Add(1))
message := route.RouteMessage{
Type: unix.RTM_GET,
Version: unix.RTM_VERSION,
Flags: unix.RTF_IFSCOPE,
Index: interfaceIndex,
ID: uintptr(os.Getpid()),
Seq: seq,
Addrs: []route.Addr{syscall.RTAX_DST: destination},
}
request, err := message.Marshal()
if err != nil {
return netip.Addr{}
}
_, err = unix.Write(socketFd, request)
if err != nil {
return netip.Addr{}
}
buffer := make([]byte, 2048)
for {
n, err := unix.Read(socketFd, buffer)
if err != nil {
return netip.Addr{}
}
messages, err := route.ParseRIB(route.RIBTypeRoute, buffer[:n])
if err != nil {
continue
}
for _, routeMessage := range messages {
reply, isRoute := routeMessage.(*route.RouteMessage)
if !isRoute || reply.Seq != seq || reply.ID != uintptr(os.Getpid()) {
continue
}
if reply.Err != nil || reply.Flags&unix.RTF_GATEWAY == 0 || len(reply.Addrs) <= syscall.RTAX_GATEWAY {
return netip.Addr{}
}
switch gateway := reply.Addrs[syscall.RTAX_GATEWAY].(type) {
case *route.Inet4Addr:
return netip.AddrFrom4(gateway.IP)
case *route.Inet6Addr:
return netip.AddrFrom16(gateway.IP)
default:
return netip.Addr{}
}
}
}
}
func addInterfaceHostRoute(destination netip.Addr, interfaceName string) error {
tunInterface, err := net.InterfaceByName(interfaceName)
if err != nil {
return err
}
var destinationAddr, maskAddr route.Addr
if destination.Is4() {
destinationAddr = &route.Inet4Addr{IP: destination.As4()}
maskAddr = &route.Inet4Addr{IP: [4]byte{255, 255, 255, 255}}
} else {
destinationAddr = &route.Inet6Addr{IP: destination.As16()}
maskAddr = &route.Inet6Addr{IP: [16]byte{
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
}}
}
message := route.RouteMessage{
Type: unix.RTM_ADD,
Version: unix.RTM_VERSION,
Flags: unix.RTF_UP | unix.RTF_HOST | unix.RTF_STATIC,
Seq: int(routeMessageSeq.Add(1)),
Addrs: []route.Addr{
syscall.RTAX_DST: destinationAddr,
syscall.RTAX_GATEWAY: &route.LinkAddr{Index: tunInterface.Index},
syscall.RTAX_NETMASK: maskAddr,
},
}
request, err := message.Marshal()
if err != nil {
return err
}
socketFd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
return err
}
defer unix.Close(socketFd)
_, err = unix.Write(socketFd, request)
if err != nil && err != unix.EEXIST {
return E.Cause(err, "RTM_ADD")
}
return nil
}
type ifAliasRequest struct {
Name [unix.IFNAMSIZ]byte
Addr unix.RawSockaddrInet4
DstAddr unix.RawSockaddrInet4
Mask unix.RawSockaddrInet4
}
type inet6AddrLifetime struct {
Expire float64
Preferred float64
Vltime uint32
Pltime uint32
}
type ifAliasRequest6 struct {
Name [unix.IFNAMSIZ]byte
Addr unix.RawSockaddrInet6
DstAddr unix.RawSockaddrInet6
Mask unix.RawSockaddrInet6
Flags uint32
Lifetime inet6AddrLifetime
}
func assignPointToPointAddress(interfaceName string, local netip.Addr, peer netip.Addr) error {
if local.Is4() {
request := ifAliasRequest{
Addr: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: local.As4(),
},
DstAddr: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: peer.As4(),
},
Mask: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: [4]byte{255, 255, 255, 255},
},
}
copy(request.Name[:], interfaceName)
return interfaceIoctl(unix.AF_INET, uint(unix.SIOCAIFADDR), unsafe.Pointer(&request))
}
request := ifAliasRequest6{
Addr: unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Addr: local.As16(),
},
DstAddr: unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Addr: peer.As16(),
},
Mask: unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Addr: [16]byte{
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
},
},
Flags: tun.IN6_IFF_NODAD | tun.IN6_IFF_SECURED,
Lifetime: inet6AddrLifetime{
Vltime: tun.ND6_INFINITE_LIFETIME,
Pltime: tun.ND6_INFINITE_LIFETIME,
},
}
copy(request.Name[:], interfaceName)
return interfaceIoctl(unix.AF_INET6, tun.SIOCAIFADDR_IN6, unsafe.Pointer(&request))
}
func interfaceIoctl(family int, request uint, pointer unsafe.Pointer) error {
socketFd, err := unix.Socket(family, unix.SOCK_DGRAM, 0)
if err != nil {
return err
}
defer unix.Close(socketFd)
return unixIoctlPtr(socketFd, request, pointer)
}
var forwardingMibs = map[string][]int32{
// CTL_NET, PF_INET, IPPROTO_IP, IPCTL_FORWARDING (netinet/in.h)
"net.inet.ip.forwarding": {syscall.CTL_NET, unix.AF_INET, 0, 1},
// CTL_NET, PF_INET6, IPPROTO_IPV6, IPV6CTL_FORWARDING (netinet6/in6.h)
"net.inet6.ip6.forwarding": {syscall.CTL_NET, unix.AF_INET6, unix.IPPROTO_IPV6, 1},
}
func getSysctlInt32(mib []int32) (int32, error) {
var value int32
valueLen := unsafe.Sizeof(value)
err := unixSysctl(mib, (*byte)(unsafe.Pointer(&value)), &valueLen, nil, 0)
return value, err
}
func setSysctlInt32(mib []int32, value int32) error {
return unixSysctl(mib, nil, nil, (*byte)(unsafe.Pointer(&value)), unsafe.Sizeof(value))
}
+285
View File
@@ -0,0 +1,285 @@
package bridge
import (
"net"
"net/netip"
"slices"
"strconv"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"golang.org/x/sys/unix"
)
func buildBridgeAnchorRules(ruleLogger logger.ContextLogger, tunName string, egress string, boundInterface string, inet4Port netip.Addr, inet6Port netip.Addr) []pfAnchorRule {
egressInterface, err := net.InterfaceByName(egress)
if err != nil {
return nil
}
mtu := egressInterface.MTU
if mtu < 576 || mtu > bridgeTunMTU {
mtu = bridgeTunMTU
}
localPrefixes, inet4Interfaces, inet6Interfaces := collectLocalSegments(egress, boundInterface, inet4Port.IsValid(), inet6Port.IsValid())
var rules []pfAnchorRule
if inet4Port.IsValid() {
rules = append(rules, pfScrubRule(egress, inet4Port, uint16(mtu-40)))
}
if inet6Port.IsValid() {
rules = append(rules, pfScrubRule(egress, inet6Port, uint16(mtu-60)))
}
if inet4Port.IsValid() {
rules = append(rules, pfNatRule(egress, inet4Port))
for _, name := range inet4Interfaces {
rules = append(rules, pfNatRule(name, inet4Port))
}
}
if inet6Port.IsValid() {
rules = append(rules, pfNatRule(egress, inet6Port))
for _, name := range inet6Interfaces {
rules = append(rules, pfNatRule(name, inet6Port))
}
}
// pf evaluates translation on the interface the routing table picks, and
// route-to on an out rule does not re-run it on the new interface: when
// another tun holds the default route the nat-on-egress rule never matches.
// route-to on the in side redirects before routing, so the packet actually
// leaves via the egress and the nat rule applies there.
if inet4Port.IsValid() {
gateway := interfaceGateway(egressInterface.Index, true)
if gateway.IsValid() {
rules = append(rules, pfRouteToRule(tunName, egress, gateway, inet4Port))
} else {
ruleLogger.Debug("no IPv4 gateway on ", egress, ", relying on the default route")
}
}
if inet6Port.IsValid() {
gateway := interfaceGateway(egressInterface.Index, false)
if gateway.IsValid() {
rules = append(rules, pfRouteToRule(tunName, egress, gateway, inet6Port))
} else {
ruleLogger.Debug("no IPv6 gateway on ", egress, ", relying on the default route")
}
}
// 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 _, prefix := range localPrefixes {
port := inet4Port
if !prefix.Addr().Is4() {
port = inet6Port
}
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
}
// collectLocalSegments returns the connected subnets whose destinations bypass
// the route-to pin so the routing table delivers them on their own interface,
// plus the non-egress interfaces that then need their own masquerade rule.
// With a pinned egress only its own subnets bypass, matching the Linux backend.
func collectLocalSegments(egress string, boundInterface string, inet4Active bool, inet6Active bool) (prefixes []netip.Prefix, inet4Interfaces []string, inet6Interfaces []string) {
localInterfaces, err := net.Interfaces()
if err != nil {
return
}
for _, localInterface := range localInterfaces {
if boundInterface != "" && localInterface.Name != boundInterface {
continue
}
if localInterface.Flags&net.FlagUp == 0 || localInterface.Flags&net.FlagBroadcast == 0 ||
localInterface.Flags&net.FlagLoopback != 0 || localInterface.Flags&net.FlagPointToPoint != 0 {
continue
}
interfaceAddrs, addrsErr := localInterface.Addrs()
if addrsErr != nil {
continue
}
var (
hasInet4 bool
hasInet6 bool
)
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 address.IsLinkLocalUnicast() {
continue
}
if address.Is4() {
if !inet4Active {
continue
}
hasInet4 = true
} else {
if !inet6Active {
continue
}
hasInet6 = true
}
bits, _ := ipNet.Mask.Size()
prefix := netip.PrefixFrom(address, bits).Masked()
if !slices.Contains(prefixes, prefix) {
prefixes = append(prefixes, prefix)
}
}
if localInterface.Name == egress {
continue
}
if hasInet4 {
inet4Interfaces = append(inet4Interfaces, localInterface.Name)
}
if hasInet6 {
inet6Interfaces = append(inet6Interfaces, localInterface.Name)
}
}
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,
AF: pfFamily(port.Is4()),
Proto: unix.IPPROTO_TCP,
MaxMSS: maxMSS,
}
copy(rule.IfName[:], egress)
rule.Src.Addr = pfHostAddress(port)
return pfAnchorRule{RulesetIndex: pfRulesetScrub, Rule: rule}
}
func pfNatRule(interfaceName string, port netip.Addr) pfAnchorRule {
rule := pfRule{
Action: pfActionNat,
AF: pfFamily(port.Is4()),
}
rule.RPool.ProxyPort = [2]uint16{pfNatProxyPortLow, pfNatProxyPortHigh}
copy(rule.IfName[:], interfaceName)
rule.Src.Addr = pfHostAddress(port)
return pfAnchorRule{
RulesetIndex: pfRulesetNat,
Rule: rule,
Pool: pfPoolAddr{Addr: pfDynamicInterfaceAddress(interfaceName, port.Is4())},
}
}
func pfPassInRule(tunName string, port netip.Addr, destination netip.Prefix) pfAnchorRule {
rule := pfRule{
Action: pfActionPass,
Direction: pfDirectionIn,
AF: pfFamily(port.Is4()),
KeepState: pfStateNormal,
}
copy(rule.IfName[:], tunName)
rule.Src.Addr = pfHostAddress(port)
if destination.IsValid() {
rule.Dst.Addr = pfPrefixAddress(destination)
}
return pfAnchorRule{RulesetIndex: pfRulesetFilter, Rule: rule}
}
func pfRouteToRule(tunName string, egress string, gateway netip.Addr, port netip.Addr) pfAnchorRule {
anchorRule := pfPassInRule(tunName, port, netip.Prefix{})
anchorRule.Rule.RouteAction = pfRouteActionRouteTo
anchorRule.Pool = pfPoolAddr{Addr: pfHostAddress(gateway)}
copy(anchorRule.Pool.IfName[:], egress)
return anchorRule
}
// Assigning the port as the utun's point-to-point destination makes the kernel
// install the host route itself; a plain interface route against an address-less
// utun fails with ENETUNREACH.
func assignBridgePortAddress(tunName string, local netip.Addr, port netip.Addr) error {
if !port.IsValid() {
return nil
}
err := assignPointToPointAddress(tunName, local, port)
if err != nil {
return E.Cause(err, "assign bridge address")
}
err = addInterfaceHostRoute(port, tunName)
if err != nil {
return E.Cause(err, "add bridge host route")
}
return nil
}
func enableDarwinForwarding(forwardingLogger logger.ContextLogger, inet4Active bool, inet6Active bool) []sysctlState {
var restore []sysctlState
enable := func(name string) {
mib := forwardingMibs[name]
value, err := getSysctlInt32(mib)
if err != nil {
forwardingLogger.Debug(E.Cause(err, "read ", name))
return
}
if value == 1 {
return
}
err = setSysctlInt32(mib, 1)
if err != nil {
forwardingLogger.Debug(E.Cause(err, "enable ", name))
return
}
restore = append(restore, sysctlState{name: name, value: strconv.Itoa(int(value))})
}
if inet4Active {
enable("net.inet.ip.forwarding")
}
if inet6Active {
enable("net.inet6.ip6.forwarding")
}
return restore
}
func restoreDarwinForwarding(states []sysctlState) {
for _, state := range states {
value, err := strconv.Atoi(state.value)
if err != nil {
continue
}
_ = setSysctlInt32(forwardingMibs[state.name], int32(value))
}
}
+99
View File
@@ -0,0 +1,99 @@
//go:build linux || darwin
package bridge
import (
"net/netip"
"os"
"sync"
"github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/common/x/list"
)
type serviceBase struct {
logger logger.ContextLogger
mtu int
inet4Port netip.Addr
inet6Port netip.Addr
tunName string
tunFileDescriptor int
forwardingRestore []sysctlState
networkMonitor tun.NetworkUpdateMonitor
monitorElement *list.Element[tun.NetworkUpdateCallback]
access sync.Mutex
egressName string
closed bool
applyEgress func()
}
func (s *serviceBase) FileDescriptor() int {
return s.tunFileDescriptor
}
func (s *serviceBase) Name() string {
return s.tunName
}
func (s *serviceBase) Inet6Active() bool {
return s.inet6Port.IsValid()
}
func (s *serviceBase) SetEgress(interfaceName string) error {
s.access.Lock()
defer s.access.Unlock()
if s.closed {
return os.ErrClosed
}
s.egressName = interfaceName
s.applyEgress()
return nil
}
func (s *serviceBase) syncEgress() {
s.access.Lock()
defer s.access.Unlock()
if s.closed {
return
}
s.applyEgress()
}
func (s *serviceBase) startNetworkMonitor() {
networkMonitor, err := tun.NewNetworkUpdateMonitor(s.logger)
if err != nil {
s.logger.Debug(E.Cause(err, "create network monitor, egress will not track route changes"))
return
}
s.monitorElement = networkMonitor.RegisterCallback(func() { s.syncEgress() })
s.networkMonitor = networkMonitor
err = networkMonitor.Start()
if err != nil {
s.logger.Debug(E.Cause(err, "start network monitor, egress will not track route changes"))
}
}
func (s *serviceBase) beginClose() bool {
s.access.Lock()
if s.closed {
s.access.Unlock()
return false
}
s.closed = true
networkMonitor := s.networkMonitor
monitorElement := s.monitorElement
s.networkMonitor = nil
s.monitorElement = nil
s.access.Unlock()
if networkMonitor != nil {
if monitorElement != nil {
networkMonitor.UnregisterCallback(monitorElement)
}
_ = networkMonitor.Close()
}
return true
}
+208
View File
@@ -0,0 +1,208 @@
package bridge
import (
"net/netip"
"os"
"slices"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"golang.org/x/sys/unix"
)
type ServiceOptions struct {
MTU int
Inet4Port netip.Addr
Inet6Port netip.Addr
Interface string
Logger logger.ContextLogger
}
type Service struct {
serviceBase
boundInterface string
inet4Local netip.Addr
inet6Local netip.Addr
anchorName string
pfDevice *pfDevice
pfToken uint64
currentRules []pfAnchorRule
}
func NewService(options ServiceOptions) (*Service, error) {
if !options.Inet4Port.IsValid() {
return nil, E.New("missing bridge IPv4 port address")
}
serviceLogger := options.Logger
if serviceLogger == nil {
serviceLogger = logger.NOP()
}
instance := &Service{
serviceBase: serviceBase{
logger: serviceLogger,
mtu: options.MTU,
inet4Port: options.Inet4Port,
inet6Port: options.Inet6Port,
tunFileDescriptor: -1,
},
boundInterface: options.Interface,
}
instance.applyEgress = instance.syncEgressLocked
index, err := bridgeIndexOf(options.Inet4Port)
if err != nil {
return nil, err
}
instance.inet4Local = addressAt(bridgeInet4LocalBase, index)
instance.inet6Local = addressAt(bridgeInet6LocalBase, index)
err = instance.start()
if err != nil {
instance.Close()
return nil, err
}
return instance, nil
}
func bridgeIndexOf(inet4Port netip.Addr) (uint32, error) {
for index := range uint32(bridgeMaxInstances) {
if addressAt(bridgeInet4Base, index) == inet4Port {
return index, nil
}
}
return 0, E.New("unexpected bridge IPv4 port address: ", inet4Port)
}
func (s *Service) start() error {
tunFileDescriptor, tunName, err := createBridgeTun(s.mtu)
if err != nil {
return E.Cause(err, "create bridge tun")
}
s.tunFileDescriptor = tunFileDescriptor
s.tunName = tunName
s.anchorName = bridgeAnchor(tunName)
s.forwardingRestore = enableDarwinForwarding(s.logger, s.inet4Port.IsValid(), s.inet6Port.IsValid())
err = assignBridgePortAddress(tunName, s.inet4Local, s.inet4Port)
if err != nil {
return E.Cause(err, "add bridge route")
}
err = assignBridgePortAddress(tunName, s.inet6Local, s.inet6Port)
if err != nil {
s.logger.Debug(E.Cause(err, "IPv6 bridge routing unavailable, disabling IPv6 forwarding"))
s.inet6Port = netip.Addr{}
}
device, err := openPfDevice()
if err != nil {
return E.Cause(err, "enable pf")
}
s.pfDevice = device
token, err := device.StartReference()
if err != nil {
return E.Cause(err, "enable pf")
}
s.pfToken = token
s.startNetworkMonitor()
return nil
}
func (s *Service) syncEgressLocked() {
var rules []pfAnchorRule
if s.egressName != "" {
rules = buildBridgeAnchorRules(s.logger, s.tunName, s.egressName, s.boundInterface, s.inet4Port, s.inet6Port)
}
if slices.Equal(rules, s.currentRules) {
return
}
err := s.pfDevice.LoadAnchor(s.anchorName, rules)
if err != nil {
s.logger.Debug(E.Cause(err, "apply bridge egress ", s.egressName))
return
}
s.currentRules = rules
if len(rules) == 0 {
s.logger.Debug("bridge egress unavailable, dropping forwarded traffic")
} else {
s.logger.Debug("bridge egress ", s.egressName)
}
}
func (s *Service) Close() error {
if !s.beginClose() {
return nil
}
s.access.Lock()
defer s.access.Unlock()
if s.pfDevice != nil {
// anchorName is set before pfDevice is opened, so a non-nil pfDevice means
// it holds the intended target (the sub-anchor on macOS, "" on iOS).
_ = s.pfDevice.LoadAnchor(s.anchorName, nil)
if s.pfToken != 0 {
_ = s.pfDevice.StopReference(s.pfToken)
}
_ = s.pfDevice.Close()
s.pfDevice = nil
}
restoreDarwinForwarding(s.forwardingRestore)
s.forwardingRestore = nil
if s.tunFileDescriptor >= 0 {
_ = unix.Close(s.tunFileDescriptor)
s.tunFileDescriptor = -1
}
return nil
}
// The stock macOS /etc/pf.conf ends its main ruleset with wildcard
// nat/rdr/scrub/anchor references to "com.apple/*", so rules loaded into a
// sub-anchor below it are evaluated without editing the main ruleset. iOS ships
// no /etc/pf.conf and no such references, leaving the main ruleset empty and
// pf-unused; there an anchor is never traversed, so we own the main ruleset
// directly (anchor "") instead.
func bridgeAnchor(tunName string) string {
_, err := os.Stat("/etc/pf.conf")
if err != nil {
return ""
}
return "com.apple/sing-box-" + tunName
}
func createBridgeTun(mtu int) (int, string, error) {
tunFd, err := unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, 2)
if err != nil {
return -1, "", os.NewSyscallError("socket", err)
}
ctlInfo := &unix.CtlInfo{}
copy(ctlInfo.Name[:], "com.apple.net.utun_control")
err = unix.IoctlCtlInfo(tunFd, ctlInfo)
if err != nil {
unix.Close(tunFd)
return -1, "", os.NewSyscallError("IoctlCtlInfo", err)
}
err = unix.Connect(tunFd, &unix.SockaddrCtl{ID: ctlInfo.Id, Unit: 0})
if err != nil {
unix.Close(tunFd)
return -1, "", os.NewSyscallError("Connect", err)
}
name, err := unix.GetsockoptString(
tunFd,
2, /* #define SYSPROTO_CONTROL 2 */
2, /* #define UTUN_OPT_IFNAME 2 */
)
if err != nil {
unix.Close(tunFd)
return -1, "", os.NewSyscallError("GetsockoptString", err)
}
socketFd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
unix.Close(tunFd)
return -1, "", os.NewSyscallError("socket", err)
}
ifr := unix.IfreqMTU{MTU: int32(mtu)}
copy(ifr.Name[:], name)
err = unix.IoctlSetIfreqMTU(socketFd, &ifr)
unix.Close(socketFd)
if err != nil {
unix.Close(tunFd)
return -1, "", os.NewSyscallError("IoctlSetIfreqMTU", err)
}
return tunFd, name, nil
}
+251
View File
@@ -0,0 +1,251 @@
package bridge
import (
"net"
"net/netip"
"github.com/sagernet/netlink"
"github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"golang.org/x/sys/unix"
)
type ServiceOptions struct {
BridgeName string
MTU int
Inet4Port netip.Addr
Inet6Port netip.Addr
RuleIndex int
RouteTable int
Logger logger.ContextLogger
}
type Service struct {
serviceBase
ruleIndex int
routeTable int
nftTableName string
clampMTU int
}
func NewService(options ServiceOptions) (*Service, error) {
if !options.Inet4Port.IsValid() {
return nil, E.New("missing bridge IPv4 port address")
}
if options.RouteTable == 0 {
return nil, E.New("missing bridge route table index")
}
serviceLogger := options.Logger
if serviceLogger == nil {
serviceLogger = logger.NOP()
}
instance := &Service{
serviceBase: serviceBase{
logger: serviceLogger,
mtu: options.MTU,
inet4Port: options.Inet4Port,
inet6Port: options.Inet6Port,
tunFileDescriptor: -1,
},
ruleIndex: options.RuleIndex,
routeTable: options.RouteTable,
}
instance.applyEgress = instance.syncEgressLocked
err := instance.start(options.BridgeName)
if err != nil {
instance.Close()
return nil, err
}
return instance, nil
}
func (s *Service) start(bridgeName string) error {
s.tunName = tun.CalculateInterfaceName(bridgeName)
s.nftTableName = "sing-box-" + s.tunName
tunFileDescriptor, err := openBridgeTun(s.tunName)
if err != nil {
return E.Cause(err, "create bridge tun")
}
s.tunFileDescriptor = tunFileDescriptor
tunLink, err := netlink.LinkByName(s.tunName)
if err != nil {
return E.Cause(err, "find bridge tun")
}
err = netlink.LinkSetMTU(tunLink, s.mtu)
if err != nil {
return E.Cause(err, "set bridge tun mtu")
}
err = netlink.LinkSetUp(tunLink)
if err != nil {
return E.Cause(err, "set bridge tun up")
}
inet6Active, err := setupBridgeNetfilter(s.logger, s.nftTableName, s.tunName, s.inet6Port.IsValid())
if err != nil {
return E.Cause(err, "set up bridge netfilter")
}
if !inet6Active {
s.inet6Port = netip.Addr{}
}
s.forwardingRestore = enableBridgeForwarding(s.logger, s.tunName, s.inet4Port.IsValid(), s.inet6Port.IsValid())
err = setupBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, unix.AF_INET, s.inet4Port)
if err != nil {
return E.Cause(err, "set up bridge routing")
}
err = setupBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, unix.AF_INET6, s.inet6Port)
if err != nil {
s.logger.Debug(E.Cause(err, "IPv6 bridge routing unavailable, disabling IPv6 forwarding"))
removeBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, unix.AF_INET6, s.inet6Port)
s.inet6Port = netip.Addr{}
}
for _, family := range activeBridgeFamilies(s.inet6Port) {
blackholeBridgeDefault(s.routeTable, family)
}
s.startNetworkMonitor()
return nil
}
func (s *Service) syncEgressLocked() {
flushBridgeRouteTable(s.routeTable)
if s.egressName == "" {
for _, family := range activeBridgeFamilies(s.inet6Port) {
blackholeBridgeDefault(s.routeTable, family)
}
return
}
link, err := netlink.LinkByName(s.egressName)
if err != nil {
for _, family := range activeBridgeFamilies(s.inet6Port) {
blackholeBridgeDefault(s.routeTable, family)
}
s.logger.Debug("bridge egress ", s.egressName, " absent, dropping forwarded traffic")
return
}
for _, family := range activeBridgeFamilies(s.inet6Port) {
s.syncEgressFamilyLocked(family, link.Attrs().Index)
}
s.updateClampLocked(link.Attrs().MTU)
}
// Unlike the in-process backend this copies routes from every table: on Android
// netd leaves the main table empty and keeps each network's routes in its own
// table, resolvable only through fwmark rules that forwarded packets never carry.
func (s *Service) syncEgressFamilyLocked(family int, linkIndex int) {
routes, err := netlink.RouteListFiltered(family, &netlink.Route{
LinkIndex: linkIndex,
Table: unix.RT_TABLE_UNSPEC,
}, netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE)
if err != nil {
blackholeBridgeDefault(s.routeTable, family)
return
}
var defaultRoute *netlink.Route
for _, route := range routes {
if route.Table == unix.RT_TABLE_LOCAL || route.Table == s.routeTable {
continue
}
if route.Type != unix.RTN_UNICAST {
continue
}
if isDefaultDestination(route.Dst) {
if defaultRoute == nil {
pinned := route
defaultRoute = &pinned
}
continue
}
if route.Gw != nil {
continue
}
connected := route
connected.Table = s.routeTable
connected.ILinkIndex = 0
_ = netlink.RouteReplace(&connected)
}
if defaultRoute == nil {
blackholeBridgeDefault(s.routeTable, family)
s.logger.Debug("no default route on bridge egress ", s.egressName)
return
}
defaultRoute.Table = s.routeTable
defaultRoute.ILinkIndex = 0
err = netlink.RouteReplace(defaultRoute)
if err != nil {
blackholeBridgeDefault(s.routeTable, family)
s.logger.Debug(E.Cause(err, "pin bridge egress default route"))
}
}
func (s *Service) updateClampLocked(egressMTU int) {
mtu := s.mtu
if egressMTU >= 576 && egressMTU < mtu {
mtu = egressMTU
}
if mtu == s.clampMTU {
return
}
err := setupBridgeClamp(s.nftTableName, s.tunName, s.inet4Port, s.inet6Port, mtu)
if err != nil {
s.logger.Debug(E.Cause(err, "update bridge MSS clamp"))
return
}
s.clampMTU = mtu
}
func (s *Service) Close() error {
if !s.beginClose() {
return nil
}
s.access.Lock()
defer s.access.Unlock()
if s.tunName != "" {
cleanupBridgeNetfilter(s.nftTableName)
removeBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, unix.AF_INET, s.inet4Port)
removeBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, unix.AF_INET6, s.inet6Port)
flushBridgeRouteTable(s.routeTable)
}
restoreBridgeForwarding(s.forwardingRestore)
s.forwardingRestore = nil
if s.tunFileDescriptor >= 0 {
_ = unix.Close(s.tunFileDescriptor)
s.tunFileDescriptor = -1
}
return nil
}
func isDefaultDestination(destination *net.IPNet) bool {
if destination == nil {
return true
}
ones, _ := destination.Mask.Size()
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
}
+16
View File
@@ -432,6 +432,9 @@ func applyRouteOptionsOverride(metadata *adapter.InboundContext, routeOptions *R
Fqdn: metadata.Destination.Fqdn,
}
}
if routeOptions.UDPTimeout > 0 {
metadata.UDPTimeout = routeOptions.UDPTimeout
}
}
func (r *Router) preMatchFlow(ctx context.Context, metadata *adapter.InboundContext, packetDestination M.Socksaddr, matchedRule adapter.Rule, outboundTag string) adapter.PreMatchResult {
@@ -472,6 +475,19 @@ func (r *Router) preMatchFlow(ctx context.Context, metadata *adapter.InboundCont
return continueResult
}
result := adapter.PreMatchResult{Action: adapter.PreMatchFlow, Outbound: outbound}
if metadata.Network == N.NetworkUDP {
if metadata.UDPTimeout > 0 {
result.UDPTimeout = metadata.UDPTimeout
} else {
protocol := metadata.Protocol
if protocol == "" {
protocol = C.PortProtocols[metadata.Destination.Port]
}
if protocol != "" {
result.UDPTimeout = C.ProtocolTimeouts[protocol]
}
}
}
if metadata.Destination.IsDomain() {
if !metadata.FakeIP {
return continueResult