Add bridge outbound
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
# Bridge
|
||||
|
||||
!!! quote ""
|
||||
|
||||
需要特权。支持 Linux、macOS、rooted Android 和越狱 iOS。
|
||||
|
||||
对于图形客户端:macOS 仅独立版本可用,且需要 Root Helper;Android 需要 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`。
|
||||
@@ -18,6 +18,7 @@
|
||||
| Type | Format |
|
||||
|----------------|--------------------------------|
|
||||
| `direct` | [Direct](./direct/) |
|
||||
| `bridge` | [Bridge](./bridge/) |
|
||||
| `block` | [Block](./block/) |
|
||||
| `socks` | [SOCKS](./socks/) |
|
||||
| `http` | [HTTP](./http/) |
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
| 类型 | 格式 |
|
||||
|----------------|--------------------------------|
|
||||
| `direct` | [Direct](./direct/) |
|
||||
| `bridge` | [Bridge](./bridge/) |
|
||||
| `block` | [Block](./block/) |
|
||||
| `socks` | [SOCKS](./socks/) |
|
||||
| `http` | [HTTP](./http/) |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user