374 lines
9.6 KiB
Go
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
|
|
}
|