From 653b81cfd9dc087d1030d2645eb1fa43fb4be9a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 16 Jul 2026 13:15:33 +0800 Subject: [PATCH] Fix iOS brdige --- protocol/bridge/backend_darwin.go | 19 +++++++++-- protocol/bridge/pf_darwin.go | 1 + protocol/bridge/rules_darwin.go | 53 ++++++++++++++++++++++--------- protocol/bridge/service.go | 10 +++--- protocol/bridge/service_darwin.go | 21 ++++++++---- protocol/bridge/service_linux.go | 7 ++-- 6 files changed, 79 insertions(+), 32 deletions(-) diff --git a/protocol/bridge/backend_darwin.go b/protocol/bridge/backend_darwin.go index 2dd8d03e8..99580bbd8 100644 --- a/protocol/bridge/backend_darwin.go +++ b/protocol/bridge/backend_darwin.go @@ -121,6 +121,12 @@ func (b *backendDarwin) start() error { 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{}) @@ -324,11 +330,15 @@ func (b *backendDarwin) syncEgress() { default: } egress := b.resolveEgress() - var rules []pfAnchorRule + rules := bridgeDropRules(b.tunName, b.inet4Port, b.inet6Port) + var buildErr error if egress != "" { - rules = buildBridgeAnchorRules(b.logger, b.tunName, egress, b.boundInterface, b.inet4Port, b.inet6Port) + 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) @@ -337,11 +347,14 @@ func (b *backendDarwin) syncEgress() { return } b.currentRules = rules - if len(rules) == 0 { + 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 { diff --git a/protocol/bridge/pf_darwin.go b/protocol/bridge/pf_darwin.go index 75de1bd53..2a056107c 100644 --- a/protocol/bridge/pf_darwin.go +++ b/protocol/bridge/pf_darwin.go @@ -19,6 +19,7 @@ const ( pfRulesetNat = 2 pfActionPass = 0 + pfActionDrop = 1 pfActionScrub = 2 pfActionNat = 4 diff --git a/protocol/bridge/rules_darwin.go b/protocol/bridge/rules_darwin.go index 17fcd56e7..3fd7b7a77 100644 --- a/protocol/bridge/rules_darwin.go +++ b/protocol/bridge/rules_darwin.go @@ -5,6 +5,7 @@ import ( "net/netip" "slices" "strconv" + "strings" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" @@ -12,26 +13,28 @@ import ( "golang.org/x/sys/unix" ) -func buildBridgeAnchorRules(ruleLogger logger.ContextLogger, tunName string, egress string, boundInterface string, inet4Port netip.Addr, inet6Port netip.Addr) []pfAnchorRule { +func buildBridgeAnchorRules(tunName string, egress string, boundInterface string, inet4Port netip.Addr, inet6Port netip.Addr) ([]pfAnchorRule, error) { + rules := bridgeDropRules(tunName, inet4Port, inet6Port) egressInterface, err := net.InterfaceByName(egress) if err != nil { - return nil + return rules, E.Cause(err, "find bridge egress ", egress) } + isCellular := strings.HasPrefix(egressInterface.Name, "pdp_ip") + isPhysical := egressInterface.Flags&net.FlagBroadcast != 0 && egressInterface.Flags&net.FlagLoopback == 0 && + egressInterface.Flags&net.FlagPointToPoint == 0 + if boundInterface == "" && !isCellular && !isPhysical { + return rules, E.New("bridge egress ", egress, " is not a physical or cellular interface") + } + routeWithoutGateway := isCellular || boundInterface != "" && egressInterface.Flags&net.FlagPointToPoint != 0 // The flowswitch aggregates forwarded TCP into packets larger than the tun // MTU, and pf_route() only fragments when they exceed the egress MTU: a // large-MTU utun target feeds them whole into the overflow described at // bridgeTunMTUDarwin. - if egressInterface.Flags&net.FlagBroadcast == 0 || egressInterface.Flags&net.FlagLoopback != 0 || - egressInterface.Flags&net.FlagPointToPoint != 0 { - ruleLogger.Error("bridge egress ", egress, " is not a physical interface, dropping forwarded traffic") - return nil - } mtu := egressInterface.MTU if mtu < 576 || mtu > bridgeTunMTUDarwin { mtu = bridgeTunMTUDarwin } 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))) } @@ -57,20 +60,16 @@ func buildBridgeAnchorRules(ruleLogger logger.ContextLogger, tunName string, egr // leaves via the egress and the nat rule applies there. if inet4Port.IsValid() { gateway := interfaceGateway(egressInterface.Index, true) - if gateway.IsValid() { + if gateway.IsValid() || routeWithoutGateway { rules = append(rules, pfRouteToRule(tunName, egress, gateway, inet4Port)) rules = append(rules, pfReplyToRule(tunName, egress, 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() { + if gateway.IsValid() || routeWithoutGateway { rules = append(rules, pfRouteToRule(tunName, egress, gateway, inet6Port)) rules = append(rules, pfReplyToRule(tunName, egress, 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 @@ -83,6 +82,17 @@ func buildBridgeAnchorRules(ruleLogger logger.ContextLogger, tunName string, egr } rules = append(rules, pfPassInRule(tunName, port, prefix)) } + return rules, nil +} + +func bridgeDropRules(tunName string, inet4Port netip.Addr, inet6Port netip.Addr) []pfAnchorRule { + var rules []pfAnchorRule + if inet4Port.IsValid() { + rules = append(rules, pfDropInRule(tunName, inet4Port)) + } + if inet6Port.IsValid() { + rules = append(rules, pfDropInRule(tunName, inet6Port)) + } return rules } @@ -196,11 +206,24 @@ func pfPassInRule(tunName string, port netip.Addr, destination netip.Prefix) pfA return pfAnchorRule{RulesetIndex: pfRulesetFilter, Rule: rule} } +func pfDropInRule(tunName string, port netip.Addr) pfAnchorRule { + rule := pfRule{ + Action: pfActionDrop, + Direction: pfDirectionIn, + AF: pfFamily(port.Is4()), + } + copy(rule.IfName[:], tunName) + rule.Src.Addr = pfHostAddress(port) + 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 copy(anchorRule.Rule.TagName[:], bridgeTagName(tunName)) - anchorRule.Pool = pfPoolAddr{Addr: pfHostAddress(gateway)} + if gateway.IsValid() { + anchorRule.Pool.Addr = pfHostAddress(gateway) + } copy(anchorRule.Pool.IfName[:], egress) return anchorRule } diff --git a/protocol/bridge/service.go b/protocol/bridge/service.go index 6fc56e9f8..dfa1d1ee1 100644 --- a/protocol/bridge/service.go +++ b/protocol/bridge/service.go @@ -28,7 +28,7 @@ type serviceBase struct { access sync.Mutex egressName string closed bool - applyEgress func() + applyEgress func() error } func (s *serviceBase) FileDescriptor() int { @@ -50,8 +50,7 @@ func (s *serviceBase) SetEgress(interfaceName string) error { return os.ErrClosed } s.egressName = interfaceName - s.applyEgress() - return nil + return s.applyEgress() } func (s *serviceBase) syncEgress() { @@ -60,7 +59,10 @@ func (s *serviceBase) syncEgress() { if s.closed { return } - s.applyEgress() + err := s.applyEgress() + if err != nil { + s.logger.Debug(E.Cause(err, "update bridge egress")) + } } func (s *serviceBase) startNetworkMonitor() { diff --git a/protocol/bridge/service_darwin.go b/protocol/bridge/service_darwin.go index 8e5ad474c..b280d54bc 100644 --- a/protocol/bridge/service_darwin.go +++ b/protocol/bridge/service_darwin.go @@ -101,29 +101,36 @@ func (s *Service) start() error { return E.Cause(err, "enable pf") } s.pfToken = token + dropRules := bridgeDropRules(s.tunName, s.inet4Port, s.inet6Port) + err = s.pfDevice.LoadAnchor(s.anchorName, dropRules) + if err != nil { + return E.Cause(err, "initialize bridge pf rules") + } + s.currentRules = dropRules s.startNetworkMonitor() return nil } -func (s *Service) syncEgressLocked() { - var rules []pfAnchorRule +func (s *Service) syncEgressLocked() error { + rules := bridgeDropRules(s.tunName, s.inet4Port, s.inet6Port) + var buildErr error if s.egressName != "" { - rules = buildBridgeAnchorRules(s.logger, s.tunName, s.egressName, s.boundInterface, s.inet4Port, s.inet6Port) + rules, buildErr = buildBridgeAnchorRules(s.tunName, s.egressName, s.boundInterface, s.inet4Port, s.inet6Port) } if slices.Equal(rules, s.currentRules) { - return + return buildErr } err := s.pfDevice.LoadAnchor(s.anchorName, rules) if err != nil { - s.logger.Debug(E.Cause(err, "apply bridge egress ", s.egressName)) - return + return E.Cause(err, "apply bridge egress ", s.egressName) } s.currentRules = rules - if len(rules) == 0 { + if buildErr != nil || s.egressName == "" { s.logger.Debug("bridge egress unavailable, dropping forwarded traffic") } else { s.logger.Debug("bridge egress ", s.egressName) } + return buildErr } func (s *Service) Close() error { diff --git a/protocol/bridge/service_linux.go b/protocol/bridge/service_linux.go index 989298668..52e9958cb 100644 --- a/protocol/bridge/service_linux.go +++ b/protocol/bridge/service_linux.go @@ -116,13 +116,13 @@ func (s *Service) start(bridgeName string) error { return nil } -func (s *Service) syncEgressLocked() { +func (s *Service) syncEgressLocked() error { flushBridgeRouteTable(s.routeTable) if s.egressName == "" { for _, family := range activeBridgeFamilies(s.inet6Port) { blackholeBridgeDefault(s.routeTable, family) } - return + return nil } link, err := netlink.LinkByName(s.egressName) if err != nil { @@ -130,12 +130,13 @@ func (s *Service) syncEgressLocked() { blackholeBridgeDefault(s.routeTable, family) } s.logger.Debug("bridge egress ", s.egressName, " absent, dropping forwarded traffic") - return + return nil } for _, family := range activeBridgeFamilies(s.inet6Port) { s.syncEgressFamilyLocked(family, link.Attrs().Index) } s.updateClampLocked(link.Attrs().MTU) + return nil } // Unlike the in-process backend this copies routes from every table: on Android