Add bridge outbound
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user