Files
2026-07-16 18:36:03 +08:00

374 lines
9.6 KiB
Go

package bridge
import (
"context"
"errors"
"net/netip"
"slices"
"sync"
"syscall"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/buf"
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")
)
// macOS 26.5 (xnu-12377) kernel-panics ("Bounds safety trap") when pf
// route-to hands an unfragmented packet larger than one skywalk buflet to a
// skywalk-native interface: nx_netif_mbuf_to_kpkt() sizes the allocation
// against the TX pool, but nx_netif.c selects the copy routine from the RX
// pool's pp_max_frags, so pkt_copy_from_mbuf() writes past the 2048-byte
// buflet. utun_ctl_send() accepts writes of any size regardless of the
// interface MTU, so the limit must hold before packets are written; utun
// reserves UTUN_IF_HEADROOM_SIZE (32) bytes of the buflet, hence 2048-32.
const bridgeTunMTUDarwin = 2048 - 32
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
batchTUN tun.DarwinTUN
writeAccess sync.Mutex
writeBatch []*buf.Buffer
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{
writeBatch: make([]*buf.Buffer, 0, bridgeWriteBatchSize),
}
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: bridgeTunMTUDarwin,
AutoRoute: false,
InterfaceMonitor: b.networkManager.InterfaceMonitor(),
Logger: b.logger,
EXP_ExternalConfiguration: true,
EXP_MultiPendingPackets: true,
})
if err != nil {
return E.Cause(err, "create bridge tun")
}
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")
}
dropRules := bridgeDropRules(b.tunName, b.inet4Port, b.inet6Port)
err = b.pfDevice.LoadAnchor(b.anchorName, dropRules)
if err != nil {
return E.Cause(err, "initialize bridge pf rules")
}
b.currentRules = dropRules
b.batchTUN = tunInterface.(tun.DarwinTUN)
b.closed = make(chan struct{})
b.readDone = make(chan struct{})
b.registerMonitors(b.syncEgress)
b.syncEgress()
go b.batchReadLoop()
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: bridgeTunMTUDarwin,
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: bridgeTunMTUDarwin,
FileDescriptor: session.FileDescriptor(),
Logger: b.logger,
EXP_ExternalConfiguration: true,
EXP_MultiPendingPackets: 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.batchTUN = tunInterface.(tun.DarwinTUN)
b.closed = make(chan struct{})
b.readDone = make(chan struct{})
b.registerMonitors(b.syncSessionEgress)
b.syncSessionEgress()
go b.batchReadLoop()
b.logger.Info("bridge started at ", b.tunName, " (platform, egress ", b.egressLabel(), ")")
return nil
}
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
}
func (b *backendDarwin) PortMTU() uint32 {
return bridgeTunMTUDarwin
}
func (b *backendDarwin) WritePackets(packets [][]byte) error {
b.writeAccess.Lock()
defer b.writeAccess.Unlock()
for len(packets) > 0 {
chunk := packets
if len(chunk) > bridgeWriteBatchSize {
chunk = chunk[:bridgeWriteBatchSize]
}
packets = packets[len(chunk):]
batch := b.writeBatch[:0]
for _, packet := range chunk {
batch = append(batch, buf.As(packet))
}
err := b.batchTUN.BatchWrite(batch)
if err != nil {
return err
}
}
return nil
}
func (b *backendDarwin) batchReadLoop() {
defer close(b.readDone)
headroom := -1
var buffers [][]byte
var batch [][]byte
for {
packets, err := b.batchTUN.BatchRead()
if err != nil {
select {
case <-b.closed:
return
default:
}
if E.IsClosed(err) || errors.Is(err, syscall.EBADF) {
return
}
b.logger.Debug(E.Cause(err, "bridge tun read"))
continue
}
if len(packets) == 0 {
continue
}
b.returnAccess.Lock()
returnPaths := b.returnPaths
b.returnAccess.Unlock()
if len(returnPaths) == 0 {
buf.ReleaseMulti(packets)
continue
}
pathHeadroom := returnPaths[0].ReturnHeadroom()
if pathHeadroom != headroom {
headroom = pathHeadroom
buffers = buffers[:0]
}
for len(buffers) < len(packets) {
buffers = append(buffers, make([]byte, headroom+bridgeTunMTU))
}
batch = batch[:0]
for _, packet := range packets {
payload := packet.Bytes()
if len(payload) == 0 {
continue
}
fixReturnChecksum(payload)
buffer := buffers[len(batch)][:headroom+len(payload)]
copy(buffer[headroom:], payload)
batch = append(batch, buffer)
}
buf.ReleaseMulti(packets)
if len(batch) == 0 {
continue
}
unconsumed := batch
currentHeadroom := headroom
for _, returnPath := range returnPaths {
if len(unconsumed) == 0 {
break
}
nextHeadroom := returnPath.ReturnHeadroom()
if nextHeadroom != currentHeadroom {
rebuffered := make([][]byte, 0, len(unconsumed))
for _, packet := range unconsumed {
payload := packet[currentHeadroom:]
buffer := make([]byte, nextHeadroom+len(payload))
copy(buffer[nextHeadroom:], payload)
rebuffered = append(rebuffered, buffer)
}
unconsumed = rebuffered
currentHeadroom = nextHeadroom
}
unconsumed = returnPath.ReturnPackets(unconsumed)
}
}
}
func (b *backendDarwin) syncEgress() {
b.egressAccess.Lock()
defer b.egressAccess.Unlock()
select {
case <-b.closed:
return
default:
}
egress := b.resolveEgress()
rules := bridgeDropRules(b.tunName, b.inet4Port, b.inet6Port)
var buildErr error
if egress != "" {
rules, buildErr = buildBridgeAnchorRules(b.tunName, egress, b.boundInterface, b.inet4Port, b.inet6Port)
}
if slices.Equal(rules, b.currentRules) {
if buildErr != nil {
b.logger.Debug(buildErr)
}
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 buildErr != nil || egress == "" {
b.logger.Debug("bridge egress unavailable, dropping forwarded traffic")
} else {
b.logger.Debug("bridge egress ", egress)
}
if buildErr != nil {
b.logger.Debug(buildErr)
}
}
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
}