Files
sing-box/protocol/bridge/rules_darwin.go
T
2026-07-09 11:20:34 +08:00

291 lines
9.1 KiB
Go

package bridge
import (
"net"
"net/netip"
"slices"
"strconv"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"golang.org/x/sys/unix"
)
func buildBridgeAnchorRules(ruleLogger logger.ContextLogger, tunName string, egress string, boundInterface string, inet4Port netip.Addr, inet6Port netip.Addr) []pfAnchorRule {
egressInterface, err := net.InterfaceByName(egress)
if err != nil {
return nil
}
// 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)))
}
if inet6Port.IsValid() {
rules = append(rules, pfScrubRule(egress, inet6Port, uint16(mtu-60)))
}
if inet4Port.IsValid() {
rules = append(rules, pfNatRule(egress, inet4Port))
for _, name := range inet4Interfaces {
rules = append(rules, pfNatRule(name, inet4Port))
}
}
if inet6Port.IsValid() {
rules = append(rules, pfNatRule(egress, inet6Port))
for _, name := range inet6Interfaces {
rules = append(rules, pfNatRule(name, inet6Port))
}
}
// pf evaluates translation on the interface the routing table picks, and
// route-to on an out rule does not re-run it on the new interface: when
// another tun holds the default route the nat-on-egress rule never matches.
// route-to on the in side redirects before routing, so the packet actually
// leaves via the egress and the nat rule applies there.
if inet4Port.IsValid() {
gateway := interfaceGateway(egressInterface.Index, true)
if gateway.IsValid() {
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() {
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
// for destinations in connected subnets, so the routing table delivers them
// on their own interface.
for _, prefix := range localPrefixes {
port := inet4Port
if !prefix.Addr().Is4() {
port = inet6Port
}
rules = append(rules, pfPassInRule(tunName, port, prefix))
}
return rules
}
// collectLocalSegments returns the connected subnets whose destinations bypass
// the route-to pin so the routing table delivers them on their own interface,
// plus the non-egress interfaces that then need their own masquerade rule.
// With a pinned egress only its own subnets bypass, matching the Linux backend.
func collectLocalSegments(egress string, boundInterface string, inet4Active bool, inet6Active bool) (prefixes []netip.Prefix, inet4Interfaces []string, inet6Interfaces []string) {
localInterfaces, err := net.Interfaces()
if err != nil {
return
}
for _, localInterface := range localInterfaces {
if boundInterface != "" && localInterface.Name != boundInterface {
continue
}
if localInterface.Flags&net.FlagUp == 0 || localInterface.Flags&net.FlagBroadcast == 0 ||
localInterface.Flags&net.FlagLoopback != 0 || localInterface.Flags&net.FlagPointToPoint != 0 {
continue
}
interfaceAddrs, addrsErr := localInterface.Addrs()
if addrsErr != nil {
continue
}
var (
hasInet4 bool
hasInet6 bool
)
for _, interfaceAddr := range interfaceAddrs {
ipNet, isIPNet := interfaceAddr.(*net.IPNet)
if !isIPNet {
continue
}
address, valid := netip.AddrFromSlice(ipNet.IP)
if !valid {
continue
}
address = address.Unmap()
if address.IsLinkLocalUnicast() {
continue
}
if address.Is4() {
if !inet4Active {
continue
}
hasInet4 = true
} else {
if !inet6Active {
continue
}
hasInet6 = true
}
bits, _ := ipNet.Mask.Size()
prefix := netip.PrefixFrom(address, bits).Masked()
if !slices.Contains(prefixes, prefix) {
prefixes = append(prefixes, prefix)
}
}
if localInterface.Name == egress {
continue
}
if hasInet4 {
inet4Interfaces = append(inet4Interfaces, localInterface.Name)
}
if hasInet6 {
inet6Interfaces = append(inet6Interfaces, localInterface.Name)
}
}
return
}
func pfScrubRule(egress string, port netip.Addr, maxMSS uint16) pfAnchorRule {
rule := pfRule{
Action: pfActionScrub,
AF: pfFamily(port.Is4()),
Proto: unix.IPPROTO_TCP,
MaxMSS: maxMSS,
}
copy(rule.IfName[:], egress)
rule.Src.Addr = pfHostAddress(port)
return pfAnchorRule{RulesetIndex: pfRulesetScrub, Rule: rule}
}
func pfNatRule(interfaceName string, port netip.Addr) pfAnchorRule {
rule := pfRule{
Action: pfActionNat,
AF: pfFamily(port.Is4()),
}
rule.RPool.ProxyPort = [2]uint16{pfNatProxyPortLow, pfNatProxyPortHigh}
copy(rule.IfName[:], interfaceName)
rule.Src.Addr = pfHostAddress(port)
return pfAnchorRule{
RulesetIndex: pfRulesetNat,
Rule: rule,
Pool: pfPoolAddr{Addr: pfDynamicInterfaceAddress(interfaceName, port.Is4())},
}
}
func pfPassInRule(tunName string, port netip.Addr, destination netip.Prefix) pfAnchorRule {
rule := pfRule{
Action: pfActionPass,
Direction: pfDirectionIn,
AF: pfFamily(port.Is4()),
KeepState: pfStateNormal,
}
copy(rule.IfName[:], tunName)
rule.Src.Addr = pfHostAddress(port)
if destination.IsValid() {
rule.Dst.Addr = pfPrefixAddress(destination)
}
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)}
copy(anchorRule.Pool.IfName[:], egress)
return anchorRule
}
// NECP's drop-all enforcement for includeAllNetworks runs in ip_output, which
// forwarded replies traverse (ip_forward -> ip_output toward the bridge tun)
// while route-to'd packets do not (pf_route emits via ifnet_output directly).
// A reply-to state built from the tag left by the route-to rule sends replies
// back through pf_route on the egress in side, skipping ip_output the same way.
func pfReplyToRule(tunName string, egress string, port netip.Addr) pfAnchorRule {
rule := pfRule{
Action: pfActionPass,
Direction: pfDirectionOut,
AF: pfFamily(port.Is4()),
KeepState: pfStateNormal,
RouteAction: pfRouteActionReplyTo,
}
copy(rule.IfName[:], egress)
copy(rule.MatchTagName[:], bridgeTagName(tunName))
anchorRule := pfAnchorRule{
RulesetIndex: pfRulesetFilter,
Rule: rule,
Pool: pfPoolAddr{Addr: pfHostAddress(port)},
}
copy(anchorRule.Pool.IfName[:], tunName)
return anchorRule
}
func bridgeTagName(tunName string) string {
return "sing-box-" + tunName
}
// Assigning the port as the utun's point-to-point destination makes the kernel
// install the host route itself; a plain interface route against an address-less
// utun fails with ENETUNREACH.
func assignBridgePortAddress(tunName string, local netip.Addr, port netip.Addr) error {
if !port.IsValid() {
return nil
}
err := assignPointToPointAddress(tunName, local, port)
if err != nil {
return E.Cause(err, "assign bridge address")
}
err = addInterfaceHostRoute(port, tunName)
if err != nil {
return E.Cause(err, "add bridge host route")
}
return nil
}
func enableDarwinForwarding(forwardingLogger logger.ContextLogger, inet4Active bool, inet6Active bool) []sysctlState {
var restore []sysctlState
enable := func(name string) {
mib := forwardingMibs[name]
value, err := getSysctlInt32(mib)
if err != nil {
forwardingLogger.Debug(E.Cause(err, "read ", name))
return
}
if value == 1 {
return
}
err = setSysctlInt32(mib, 1)
if err != nil {
forwardingLogger.Debug(E.Cause(err, "enable ", name))
return
}
restore = append(restore, sysctlState{name: name, value: strconv.Itoa(int(value))})
}
if inet4Active {
enable("net.inet.ip.forwarding")
}
if inet6Active {
enable("net.inet6.ip6.forwarding")
}
return restore
}
func restoreDarwinForwarding(states []sysctlState) {
for _, state := range states {
value, err := strconv.Atoi(state.value)
if err != nil {
continue
}
_ = setSysctlInt32(forwardingMibs[state.name], int32(value))
}
}