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 }