package bridge import ( "net" "net/netip" _ "unsafe" "github.com/sagernet/netlink" "github.com/sagernet/sing-tun" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "golang.org/x/sys/unix" ) type ServiceOptions struct { BridgeName string MTU int Inet4Port netip.Addr Inet6Port netip.Addr RuleIndex int RouteTable int Logger logger.ContextLogger } type Service struct { serviceBase ruleIndex int routeTable int nftTableName string clampMTU int } func NewService(options ServiceOptions) (*Service, error) { if !options.Inet4Port.IsValid() { return nil, E.New("missing bridge IPv4 port address") } if options.RouteTable == 0 { return nil, E.New("missing bridge route table index") } serviceLogger := options.Logger if serviceLogger == nil { serviceLogger = logger.NOP() } instance := &Service{ serviceBase: serviceBase{ logger: serviceLogger, mtu: options.MTU, inet4Port: options.Inet4Port, inet6Port: options.Inet6Port, tunFileDescriptor: -1, }, ruleIndex: options.RuleIndex, routeTable: options.RouteTable, } instance.applyEgress = instance.syncEgressLocked err := instance.start(options.BridgeName) if err != nil { instance.Close() return nil, err } return instance, nil } func (s *Service) start(bridgeName string) error { s.tunName = tun.CalculateInterfaceName(bridgeName) s.nftTableName = "sing-box-" + s.tunName tunFileDescriptor, err := openTUN(s.tunName, true) if err != nil { return E.Cause(err, "create bridge tun") } err = setTCPOffload(tunFileDescriptor) if err != nil { s.logger.Warn(E.Cause(err, "set TCP offload")) } err = setUDPOffload(tunFileDescriptor) if err != nil { s.logger.Warn(E.Cause(err, "set UDP offload")) } s.tunFileDescriptor = tunFileDescriptor tunLink, err := netlink.LinkByName(s.tunName) if err != nil { return E.Cause(err, "find bridge tun") } err = netlink.LinkSetMTU(tunLink, s.mtu) if err != nil { return E.Cause(err, "set bridge tun mtu") } err = netlink.LinkSetUp(tunLink) if err != nil { return E.Cause(err, "set bridge tun up") } inet6Active, err := setupBridgeNetfilter(s.logger, s.nftTableName, s.tunName, s.inet6Port.IsValid()) if err != nil { return E.Cause(err, "set up bridge netfilter") } if !inet6Active { 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) if err != nil { return E.Cause(err, "set up bridge routing") } err = setupBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, 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) s.inet6Port = netip.Addr{} } for _, family := range activeBridgeFamilies(s.inet6Port) { blackholeBridgeDefault(s.routeTable, family) } 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 } 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 // 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 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")) } } func (s *Service) updateClampLocked(egressMTU int) { mtu := s.mtu if egressMTU >= 576 && egressMTU < mtu { mtu = egressMTU } if mtu == s.clampMTU { return } err := setupBridgeClamp(s.nftTableName, s.tunName, s.inet4Port, s.inet6Port, mtu) if err != nil { s.logger.Debug(E.Cause(err, "update bridge MSS clamp")) return } s.clampMTU = mtu } func (s *Service) Close() error { if !s.beginClose() { return nil } s.access.Lock() 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) flushBridgeRouteTable(s.routeTable) } restoreBridgeForwarding(s.forwardingRestore) s.forwardingRestore = nil if s.tunFileDescriptor >= 0 { _ = unix.Close(s.tunFileDescriptor) s.tunFileDescriptor = -1 } return nil } func isDefaultDestination(destination *net.IPNet) bool { if destination == nil { return true } ones, _ := destination.Mask.Size() return ones == 0 } //go:linkname openTUN github.com/sagernet/sing-tun.open func openTUN(name string, vnetHdr bool) (int, error) //go:linkname setTCPOffload github.com/sagernet/sing-tun.setTCPOffload func setTCPOffload(fd int) error //go:linkname setUDPOffload github.com/sagernet/sing-tun.setUDPOffload func setUDPOffload(fd int) error