From 2f10bfc8ea71ae8633169330f68a3e6b2ee43629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 24 Sep 2026 15:36:18 +0800 Subject: [PATCH] Fix bridge forwarding with auto-redirect --- adapter/network.go | 2 + docs/configuration/outbound/bridge.md | 4 +- docs/configuration/outbound/bridge.zh.md | 4 +- experimental/libbox/redirect.go | 3 + protocol/bridge/backend_linux.go | 150 +++++++------------- protocol/bridge/netfilter_linux.go | 170 ++++++++++++++++++++--- protocol/bridge/service_linux.go | 95 ++++--------- protocol/tun/inbound.go | 1 + route/network.go | 16 +++ 9 files changed, 251 insertions(+), 194 deletions(-) diff --git a/adapter/network.go b/adapter/network.go index 8a760fd37..fc52bcd30 100644 --- a/adapter/network.go +++ b/adapter/network.go @@ -28,6 +28,8 @@ type NetworkManager interface { RegisterAutoRedirectOutputMark(mark uint32) error AutoRedirectOutputMark() uint32 AutoRedirectOutputMarkFunc() control.Func + RegisterBridgeInterface(interfaceName string) + BridgeInterfaces() []string NetworkMonitor() tun.NetworkUpdateMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor PackageManager() tun.PackageManager diff --git a/docs/configuration/outbound/bridge.md b/docs/configuration/outbound/bridge.md index 201afd58c..20d9f2c7b 100644 --- a/docs/configuration/outbound/bridge.md +++ b/docs/configuration/outbound/bridge.md @@ -58,9 +58,9 @@ Not effective on Apple platforms. !!! quote "" - Only supported on Linux, and only takes effect when `interface` is set. + Only supported on Linux. -Linux iproute2 table index for pinned egress routes. +Linux iproute2 table index. `2200` + instance index is used by default. diff --git a/docs/configuration/outbound/bridge.zh.md b/docs/configuration/outbound/bridge.zh.md index ec97accff..2c462e478 100644 --- a/docs/configuration/outbound/bridge.zh.md +++ b/docs/configuration/outbound/bridge.zh.md @@ -53,9 +53,9 @@ icon: material/new-box !!! quote "" - 仅支持 Linux,且仅在设置了 `interface` 时生效。 + 仅支持 Linux。 -用于固定出口路由的 Linux iproute2 路由表索引。 +Linux iproute2 路由表索引。 默认使用 `2200` + 实例索引。 diff --git a/experimental/libbox/redirect.go b/experimental/libbox/redirect.go index 7bddeec1f..85a910792 100644 --- a/experimental/libbox/redirect.go +++ b/experimental/libbox/redirect.go @@ -36,6 +36,7 @@ type autoRedirectOptions struct { Inet6RouteExcludeAddress []netip.Prefix `json:"inet6_route_exclude_address,omitempty"` IncludeInterface []string `json:"include_interface,omitempty"` ExcludeInterface []string `json:"exclude_interface,omitempty"` + BridgeInterface []string `json:"bridge_interface,omitempty"` IncludeUID []ranges.Range[uint32] `json:"include_uid,omitempty"` ExcludeUID []ranges.Range[uint32] `json:"exclude_uid,omitempty"` IncludeMACAddress []net.HardwareAddr `json:"include_mac_address,omitempty"` @@ -70,6 +71,7 @@ func encodeAutoRedirectOptions(options adapter.AutoRedirectOptions) ([]byte, err Inet6RouteExcludeAddress: tunOptions.Inet6RouteExcludeAddress, IncludeInterface: tunOptions.IncludeInterface, ExcludeInterface: tunOptions.ExcludeInterface, + BridgeInterface: tunOptions.BridgeInterface, IncludeUID: tunOptions.IncludeUID, ExcludeUID: tunOptions.ExcludeUID, IncludeMACAddress: tunOptions.IncludeMACAddress, @@ -113,6 +115,7 @@ func decodeAutoRedirectOptions(content []byte) (*tun.Options, string, uint16, er Inet6RouteExcludeAddress: options.Inet6RouteExcludeAddress, IncludeInterface: options.IncludeInterface, ExcludeInterface: options.ExcludeInterface, + BridgeInterface: options.BridgeInterface, IncludeUID: options.IncludeUID, ExcludeUID: options.ExcludeUID, IncludeMACAddress: options.IncludeMACAddress, diff --git a/protocol/bridge/backend_linux.go b/protocol/bridge/backend_linux.go index 8bc6a4e32..03abc535b 100644 --- a/protocol/bridge/backend_linux.go +++ b/protocol/bridge/backend_linux.go @@ -51,9 +51,7 @@ func newBackend(ctx context.Context, logger logger.ContextLogger, networkManager if instance.ruleIndex == 0 { instance.ruleIndex = defaultBridgeRuleIndex } - if instance.boundInterface != "" || instance.platform != nil { - instance.routeTable = options.IPRoute2TableIndex - } + instance.routeTable = options.IPRoute2TableIndex return instance, nil } @@ -64,7 +62,7 @@ func (b *backendLinux) Start(stage adapter.StartStage, scope *adapter.Scope) err if err != nil { return err } - if b.routeTable == 0 && (b.boundInterface != "" || b.platform != nil) { + if b.routeTable == 0 { b.routeTable = defaultBridgeTableIndexBase + int(b.index) } case adapter.StartStateStart: @@ -101,6 +99,7 @@ func (b *backendLinux) start(scope *adapter.Scope) error { if err != nil { return E.Cause(err, "start bridge tun") } + b.networkManager.RegisterBridgeInterface(b.tunName) linuxTUN := tunInterface.(tun.LinuxTUN) if linuxTUN.BatchSize() > 1 { b.batchTUN = linuxTUN @@ -130,32 +129,29 @@ func (b *backendLinux) start(scope *adapter.Scope) error { restoreBridgeForwarding(forwardingRestore) return nil }) - if b.routeTable != 0 { - scope.Add(func() error { - b.egressAccess.Lock() - flushBridgeRouteTable(b.routeTable) - b.egressAccess.Unlock() - return nil - }) - } - if b.boundInterface != "" { - b.syncEgress() - } scope.Add(func() error { b.egressAccess.Lock() - removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET, b.inet4Port) - removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET6, b.inet6Port) + flushBridgeRouteTable(b.routeTable) b.egressAccess.Unlock() return nil }) - err = setupBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET, b.inet4Port) + b.syncEgress() + preferMainTable := b.boundInterface == "" + scope.Add(func() error { + b.egressAccess.Lock() + removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, preferMainTable, unix.AF_INET, b.inet4Port) + removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, preferMainTable, unix.AF_INET6, b.inet6Port) + b.egressAccess.Unlock() + return nil + }) + err = setupBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, preferMainTable, 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) + err = setupBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, preferMainTable, 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) + removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, preferMainTable, unix.AF_INET6, b.inet6Port) b.inet6Port = netip.Addr{} } if b.batchTUN != nil { @@ -166,28 +162,9 @@ func (b *backendLinux) start(scope *adapter.Scope) error { egress := "auto" if b.boundInterface != "" { egress = b.boundInterface - monitor := b.networkManager.NetworkMonitor() - if monitor != nil { - element := monitor.RegisterCallback(func() { b.syncEgress() }) - scope.Add(func() error { - monitor.UnregisterCallback(element) - return nil - }) - } 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() }) - scope.Add(func() error { - monitor.UnregisterCallback(element) - return nil - }) - } - b.updateClamp() } + b.registerMonitors(scope, b.syncEgress) + b.syncEgress() natMode := "masquerade" if fullConeSupported() { natMode = "full-cone NAT" @@ -211,6 +188,7 @@ func (b *backendLinux) startPlatform(scope *adapter.Scope) error { scope.Add(session.Close) b.session = session b.tunName = session.Name() + b.networkManager.RegisterBridgeInterface(b.tunName) if !session.Inet6Active() { b.inet6Port = netip.Addr{} } @@ -318,13 +296,11 @@ func (b *backendLinux) batchReadLoop() { batch := make([][]byte, 0, batchSize) headroom := -1 var ( - buffers [][]byte - readRetry tun.ReadRetry + buffers [][]byte + returnPaths []tun.Return + readRetry tun.ReadRetry ) for { - b.returnAccess.Lock() - returnPaths := b.returnPaths - b.returnAccess.Unlock() pathHeadroom := 0 if len(returnPaths) > 0 { pathHeadroom = returnPaths[0].ReturnHeadroom() @@ -351,6 +327,9 @@ func (b *backendLinux) batchReadLoop() { } else { readRetry.Reset() } + b.returnAccess.Lock() + returnPaths = b.returnPaths + b.returnAccess.Unlock() if n == 0 || len(returnPaths) == 0 { continue } @@ -384,9 +363,6 @@ func (b *backendLinux) batchReadLoop() { } } -// 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() @@ -396,63 +372,37 @@ func (b *backendLinux) syncEgress() { 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 + egress := b.resolveEgress() + var link netlink.Link + if egress != "" { + link, _ = netlink.LinkByName(egress) } + fallbackType := unix.RTN_UNREACHABLE + if b.boundInterface != "" { + fallbackType = unix.RTN_BLACKHOLE + } + var routes []netlink.Route 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) + if link == nil { + routes = append(routes, unroutableBridgeRoute(b.routeTable, family, fallbackType)) + } else { + routes = append(routes, egressRoutes(b.routeTable, family, link, fallbackType)...) } } - 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: + if bridgeRoutesEqual(listBridgeRoutes(b.routeTable), routes) { return - default: } - b.updateClampLocked() + if link == nil { + if b.boundInterface != "" { + b.logger.Debug("bridge egress ", egress, " absent, dropping forwarded traffic") + } else { + b.logger.Debug("no default interface for bridge egress") + } + } + err := applyBridgeRoutes(b.routeTable, routes, fallbackType) + if err != nil { + b.logger.Debug(E.Cause(err, "pin bridge egress default route")) + } } func (b *backendLinux) updateClampLocked() { diff --git a/protocol/bridge/netfilter_linux.go b/protocol/bridge/netfilter_linux.go index 38a8588b4..bc16a850d 100644 --- a/protocol/bridge/netfilter_linux.go +++ b/protocol/bridge/netfilter_linux.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "runtime" + "slices" "strconv" "strings" "sync" @@ -289,7 +290,7 @@ func cleanupBridgeIptablesTable(binary string, table string, hookChain string, t _ = exec.Command(path, "-t", table, "-X", tableName).Run() } -func setupBridgeFamily(tunName string, ruleIndex int, routeTable int, family int, port netip.Addr) error { +func setupBridgeFamily(tunName string, ruleIndex int, routeTable int, preferMainTable bool, family int, port netip.Addr) error { if !port.IsValid() { return nil } @@ -301,7 +302,7 @@ func setupBridgeFamily(tunName string, ruleIndex int, routeTable int, family int if err != nil { return E.Cause(err, "add route") } - for _, rule := range bridgeFamilyRules(tunName, ruleIndex, routeTable, family, port) { + for _, rule := range bridgeFamilyRules(tunName, ruleIndex, routeTable, preferMainTable, family, port) { _ = netlink.RuleDel(rule) err = netlink.RuleAdd(rule) if err != nil { @@ -311,7 +312,7 @@ func setupBridgeFamily(tunName string, ruleIndex int, routeTable int, family int return nil } -func removeBridgeFamily(tunName string, ruleIndex int, routeTable int, family int, port netip.Addr) { +func removeBridgeFamily(tunName string, ruleIndex int, routeTable int, preferMainTable bool, family int, port netip.Addr) { if !port.IsValid() { return } @@ -319,7 +320,7 @@ func removeBridgeFamily(tunName string, ruleIndex int, routeTable int, family in if err == nil { _ = netlink.RouteDel(bridgeFamilyRoute(link.Attrs().Index, family, port)) } - for _, rule := range bridgeFamilyRules(tunName, ruleIndex, routeTable, family, port) { + for _, rule := range bridgeFamilyRules(tunName, ruleIndex, routeTable, preferMainTable, family, port) { _ = netlink.RuleDel(rule) } } @@ -337,47 +338,174 @@ func bridgeFamilyRoute(linkIndex int, family int, port netip.Addr) *netlink.Rout 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 +// The rules sit ahead of sing-tun auto_route's rules (9000 by default) so forwarded +// packets never loop back into a tun. netd keeps every network's routes in the +// network's own table behind fwmark and "iif lo" rules that forwarded packets +// never satisfy, so with preferMainTable the main lookup finds nothing on Android +// and the bridge table, filled from the egress interface, takes over. +func bridgeFamilyRules(tunName string, ruleIndex int, routeTable int, preferMainTable bool, family int, port netip.Addr) []*netlink.Rule { + var rules []*netlink.Rule + priority := ruleIndex + if preferMainTable { + mainRule := netlink.NewRule() + mainRule.Priority = priority + mainRule.IifName = tunName + mainRule.Table = unix.RT_TABLE_MAIN + mainRule.Family = family + rules = append(rules, mainRule) + priority++ } iifRule := netlink.NewRule() - iifRule.Priority = ruleIndex + iifRule.Priority = priority iifRule.IifName = tunName - iifRule.Table = forwardTable + iifRule.Table = routeTable iifRule.Family = family + priority++ toRule := netlink.NewRule() - toRule.Priority = ruleIndex + 1 + toRule.Priority = priority toRule.Dst = netip.PrefixFrom(port, port.BitLen()) toRule.Table = unix.RT_TABLE_MAIN toRule.Family = family - return []*netlink.Rule{iifRule, toRule} + return append(rules, iifRule, toRule) } -func flushBridgeRouteTable(routeTable int) { +// Default routes are dumped without RTA_DST, and the netlink client refuses to +// delete a route with neither destination nor gateway. +func listBridgeRoutes(routeTable int) []netlink.Route { + var routes []netlink.Route for _, family := range []int{unix.AF_INET, unix.AF_INET6} { - routes, err := netlink.RouteListFiltered(family, &netlink.Route{Table: routeTable}, netlink.RT_FILTER_TABLE) + familyRoutes, 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) + for _, route := range familyRoutes { + if route.Dst == nil { + route.Dst = defaultDestination(family) + } + routes = append(routes, route) } } + return routes +} + +func flushBridgeRouteTable(routeTable int) { + for _, route := range listBridgeRoutes(routeTable) { + _ = netlink.RouteDel(&route) + } } -func blackholeBridgeDefault(routeTable int, family int) { - _ = netlink.RouteReplace(&netlink.Route{ +func unroutableBridgeDefault(routeTable int, family int, routeType int) { + route := unroutableBridgeRoute(routeTable, family, routeType) + _ = netlink.RouteReplace(&route) +} + +func unroutableBridgeRoute(routeTable int, family int, routeType int) netlink.Route { + return netlink.Route{ Table: routeTable, Family: family, - Type: unix.RTN_BLACKHOLE, + Type: routeType, Dst: defaultDestination(family), - }) + } +} + +func applyBridgeRoutes(routeTable int, routes []netlink.Route, fallbackType int) error { + flushBridgeRouteTable(routeTable) + var applyErr error + for index := range routes { + err := netlink.RouteReplace(&routes[index]) + if err != nil && routes[index].Type == unix.RTN_UNICAST && isDefaultDestination(routes[index].Dst) { + unroutableBridgeDefault(routeTable, routes[index].Family, fallbackType) + applyErr = E.Errors(applyErr, err) + } + } + return applyErr +} + +// The kernel drops routes through a link that goes down and routes whose +// preferred source is removed, so the table is compared against what the kernel +// holds rather than against what was last written. +func bridgeRoutesEqual(current []netlink.Route, desired []netlink.Route) bool { + if len(current) != len(desired) { + return false + } + matched := make([]bool, len(current)) + for _, route := range desired { + found := false + for index := range current { + if matched[index] || !bridgeRouteEqual(current[index], route) { + continue + } + matched[index] = true + found = true + break + } + if !found { + return false + } + } + return true +} + +func bridgeRouteEqual(left netlink.Route, right netlink.Route) bool { + if left.Family != right.Family || left.Type != right.Type { + return false + } + if left.Type == unix.RTN_UNICAST && left.LinkIndex != right.LinkIndex { + return false + } + return left.Dst.String() == right.Dst.String() && left.Gw.Equal(right.Gw) +} + +// The connected routes are collected from every table because netd stores them +// in the network's own table instead of main. +func egressRoutes(routeTable int, family int, link netlink.Link, fallbackType int) []netlink.Route { + var routes []netlink.Route + linkRoutes, err := netlink.RouteListFiltered(family, &netlink.Route{ + LinkIndex: link.Attrs().Index, + Table: unix.RT_TABLE_UNSPEC, + }, netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE) + if err == nil { + for _, route := range linkRoutes { + if route.Table == unix.RT_TABLE_LOCAL || route.Table == routeTable || + route.Type != unix.RTN_UNICAST || route.Gw != nil || isDefaultDestination(route.Dst) { + continue + } + connected := netlink.Route{ + LinkIndex: link.Attrs().Index, + Table: routeTable, + Family: family, + Type: unix.RTN_UNICAST, + Scope: route.Scope, + Dst: route.Dst, + Priority: route.Priority, + } + if slices.ContainsFunc(routes, func(existing netlink.Route) bool { + return existing.Dst.String() == connected.Dst.String() && existing.Priority == connected.Priority + }) { + continue + } + routes = append(routes, connected) + } + } + resolved, err := netlink.RouteGetWithOptions(probeAddress(family), &netlink.RouteGetOptions{Oif: link.Attrs().Name}) + if err != nil || len(resolved) == 0 { + return append(routes, unroutableBridgeRoute(routeTable, family, fallbackType)) + } + defaultRoute := netlink.Route{ + LinkIndex: link.Attrs().Index, + Table: routeTable, + Family: family, + Type: unix.RTN_UNICAST, + Dst: defaultDestination(family), + } + if len(resolved[0].Gw) > 0 { + defaultRoute.Gw = resolved[0].Gw + defaultRoute.Flags = int(unix.RTNH_F_ONLINK) + } + return append(routes, defaultRoute) } func activeBridgeFamilies(inet6Port netip.Addr) []int { diff --git a/protocol/bridge/service_linux.go b/protocol/bridge/service_linux.go index 52e9958cb..9c15d1c40 100644 --- a/protocol/bridge/service_linux.go +++ b/protocol/bridge/service_linux.go @@ -99,93 +99,50 @@ func (s *Service) start(bridgeName string) error { 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) + err = setupBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, false, 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) + err = setupBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, false, 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) + removeBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, false, unix.AF_INET6, s.inet6Port) s.inet6Port = netip.Addr{} } for _, family := range activeBridgeFamilies(s.inet6Port) { - blackholeBridgeDefault(s.routeTable, family) + unroutableBridgeDefault(s.routeTable, family, unix.RTN_BLACKHOLE) } s.startNetworkMonitor() return nil } func (s *Service) syncEgressLocked() error { - flushBridgeRouteTable(s.routeTable) - if s.egressName == "" { - for _, family := range activeBridgeFamilies(s.inet6Port) { - blackholeBridgeDefault(s.routeTable, family) - } - return nil - } - 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 nil + var link netlink.Link + if s.egressName != "" { + link, _ = netlink.LinkByName(s.egressName) } + var routes []netlink.Route for _, family := range activeBridgeFamilies(s.inet6Port) { - s.syncEgressFamilyLocked(family, link.Attrs().Index) + if link == nil { + routes = append(routes, unroutableBridgeRoute(s.routeTable, family, unix.RTN_BLACKHOLE)) + } else { + routes = append(routes, egressRoutes(s.routeTable, family, link, unix.RTN_BLACKHOLE)...) + } } - s.updateClampLocked(link.Attrs().MTU) - return nil -} - -// 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 link != nil { + s.updateClampLocked(link.Attrs().MTU) + } + if bridgeRoutesEqual(listBridgeRoutes(s.routeTable), routes) { + return nil + } + if s.egressName != "" && link == nil { + s.logger.Debug("bridge egress ", s.egressName, " absent, dropping forwarded traffic") + } + err := applyBridgeRoutes(s.routeTable, routes, unix.RTN_BLACKHOLE) 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")) } + return nil } func (s *Service) updateClampLocked(egressMTU int) { @@ -212,8 +169,8 @@ func (s *Service) Close() error { 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) + removeBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, false, unix.AF_INET, s.inet4Port) + removeBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, false, unix.AF_INET6, s.inet6Port) flushBridgeRouteTable(s.routeTable) } restoreBridgeForwarding(s.forwardingRestore) diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 96b8c860b..d14d3d037 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -369,6 +369,7 @@ func (t *Inbound) Start(stage adapter.StartStage, scope *adapter.Scope) error { if t.tunOptions.Name == "" { t.tunOptions.Name = tun.CalculateInterfaceName("") } + t.tunOptions.BridgeInterface = t.networkManager.BridgeInterfaces() if t.tunOptions.NetNs != "" { manager := service.FromContext[adapter.NetworkNamespaceManager](t.ctx) if manager != nil { diff --git a/route/network.go b/route/network.go index d56340518..b803542c2 100644 --- a/route/network.go +++ b/route/network.go @@ -42,6 +42,8 @@ type NetworkManager struct { autoDetectInterface bool defaultOptions adapter.NetworkOptions autoRedirectOutputMark uint32 + bridgeInterfaceAccess sync.Mutex + bridgeInterfaces []string networkMonitor tun.NetworkUpdateMonitor interfaceMonitor tun.DefaultInterfaceMonitor packageManager tun.PackageManager @@ -392,6 +394,20 @@ func (r *NetworkManager) AutoRedirectOutputMark() uint32 { return r.autoRedirectOutputMark } +func (r *NetworkManager) RegisterBridgeInterface(interfaceName string) { + r.bridgeInterfaceAccess.Lock() + defer r.bridgeInterfaceAccess.Unlock() + if !slices.Contains(r.bridgeInterfaces, interfaceName) { + r.bridgeInterfaces = append(r.bridgeInterfaces, interfaceName) + } +} + +func (r *NetworkManager) BridgeInterfaces() []string { + r.bridgeInterfaceAccess.Lock() + defer r.bridgeInterfaceAccess.Unlock() + return slices.Clone(r.bridgeInterfaces) +} + func (r *NetworkManager) AutoRedirectOutputMarkFunc() control.Func { return func(network, address string, conn syscall.RawConn) error { if r.autoRedirectOutputMark == 0 {