mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-02 07:06:41 +00:00
Replace exclusion routes with scoped default + IP_BOUND_IF on macOS
This commit is contained in:
5
client/net/dialer_init_darwin.go
Normal file
5
client/net/dialer_init_darwin.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package net
|
||||
|
||||
func (d *Dialer) init() {
|
||||
d.Dialer.Control = applyBoundIfToSocket
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !linux && !windows
|
||||
//go:build !linux && !windows && !darwin
|
||||
|
||||
package net
|
||||
|
||||
|
||||
67
client/net/env_darwin.go
Normal file
67
client/net/env_darwin.go
Normal file
@@ -0,0 +1,67 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/iface/netstack"
|
||||
)
|
||||
|
||||
var (
|
||||
vpnInterfaceName string
|
||||
vpnInitMutex sync.RWMutex
|
||||
|
||||
advancedRoutingSupported bool
|
||||
)
|
||||
|
||||
func Init() {
|
||||
advancedRoutingSupported = checkAdvancedRoutingSupport()
|
||||
}
|
||||
|
||||
func checkAdvancedRoutingSupport() bool {
|
||||
var err error
|
||||
var legacyRouting bool
|
||||
if val := os.Getenv(envUseLegacyRouting); val != "" {
|
||||
legacyRouting, err = strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
log.Warnf("failed to parse %s: %v", envUseLegacyRouting, err)
|
||||
}
|
||||
}
|
||||
|
||||
if legacyRouting || netstack.IsEnabled() {
|
||||
log.Info("advanced routing has been requested to be disabled")
|
||||
return false
|
||||
}
|
||||
|
||||
log.Info("system supports advanced routing")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// AdvancedRouting reports whether routing loops can be avoided without using exclusion routes
|
||||
func AdvancedRouting() bool {
|
||||
return advancedRoutingSupported
|
||||
}
|
||||
|
||||
// GetVPNInterfaceName returns the stored VPN interface name
|
||||
func GetVPNInterfaceName() string {
|
||||
vpnInitMutex.RLock()
|
||||
defer vpnInitMutex.RUnlock()
|
||||
return vpnInterfaceName
|
||||
}
|
||||
|
||||
// SetVPNInterfaceName sets the VPN interface name for lazy initialization
|
||||
func SetVPNInterfaceName(name string) {
|
||||
vpnInitMutex.Lock()
|
||||
defer vpnInitMutex.Unlock()
|
||||
vpnInterfaceName = name
|
||||
|
||||
if name != "" {
|
||||
log.Infof("VPN interface name set to %s for route exclusion", name)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !linux && !windows && !android
|
||||
//go:build !linux && !windows && !android && (ios || !darwin)
|
||||
|
||||
package net
|
||||
|
||||
|
||||
5
client/net/listener_init_darwin.go
Normal file
5
client/net/listener_init_darwin.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package net
|
||||
|
||||
func (l *ListenerConfig) init() {
|
||||
l.ListenConfig.Control = applyBoundIfToSocket
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !linux && !windows
|
||||
//go:build !linux && !windows && !darwin
|
||||
|
||||
package net
|
||||
|
||||
|
||||
150
client/net/net_darwin.go
Normal file
150
client/net/net_darwin.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// On darwin IPV6_BOUND_IF also scopes v4-mapped egress from dual-stack
|
||||
// (IPV6_V6ONLY=0) AF_INET6 sockets, so a single setsockopt on "udp6"/"tcp6"
|
||||
// covers both families. Setting IP_BOUND_IF on an AF_INET6 socket returns
|
||||
// EINVAL regardless of V6ONLY because the IPPROTO_IP ctloutput path is
|
||||
// dispatched by socket domain (AF_INET only) not by inp_vflag.
|
||||
|
||||
// boundIface holds the physical interface chosen at routing setup time. Sockets
|
||||
// created via nbnet.NewDialer / nbnet.NewListener bind to it via IP_BOUND_IF
|
||||
// (IPv4) or IPV6_BOUND_IF (IPv6 / dual-stack) so their scoped route lookup
|
||||
// hits the RTF_IFSCOPE default installed by the routemanager, rather than
|
||||
// following the VPN's split default.
|
||||
var (
|
||||
boundIfaceMu sync.RWMutex
|
||||
boundIface4 *net.Interface
|
||||
boundIface6 *net.Interface
|
||||
)
|
||||
|
||||
// SetBoundInterface records the egress interface for an address family. Called
|
||||
// by the routemanager after a scoped default route has been installed.
|
||||
// af must be unix.AF_INET or unix.AF_INET6; other values are ignored.
|
||||
// nil iface is rejected — use ClearBoundInterfaces to clear all slots.
|
||||
func SetBoundInterface(af int, iface *net.Interface) {
|
||||
if iface == nil {
|
||||
log.Warnf("SetBoundInterface: nil iface for AF %d, ignored", af)
|
||||
return
|
||||
}
|
||||
boundIfaceMu.Lock()
|
||||
defer boundIfaceMu.Unlock()
|
||||
switch af {
|
||||
case unix.AF_INET:
|
||||
boundIface4 = iface
|
||||
case unix.AF_INET6:
|
||||
boundIface6 = iface
|
||||
default:
|
||||
log.Warnf("SetBoundInterface: unsupported address family %d", af)
|
||||
}
|
||||
}
|
||||
|
||||
// ClearBoundInterfaces resets the cached egress interfaces. Called by the
|
||||
// routemanager during cleanup.
|
||||
func ClearBoundInterfaces() {
|
||||
boundIfaceMu.Lock()
|
||||
defer boundIfaceMu.Unlock()
|
||||
boundIface4 = nil
|
||||
boundIface6 = nil
|
||||
}
|
||||
|
||||
func boundInterfaceFor(network, address string) *net.Interface {
|
||||
if iface := zoneInterface(address); iface != nil {
|
||||
return iface
|
||||
}
|
||||
|
||||
boundIfaceMu.RLock()
|
||||
defer boundIfaceMu.RUnlock()
|
||||
|
||||
if isV6Network(network) {
|
||||
return boundIface6
|
||||
}
|
||||
return boundIface4
|
||||
}
|
||||
|
||||
func isV6Network(network string) bool {
|
||||
return strings.HasSuffix(network, "6")
|
||||
}
|
||||
|
||||
// zoneInterface extracts an explicit interface from an IPv6 link-local zone (e.g. fe80::1%en0).
|
||||
func zoneInterface(address string) *net.Interface {
|
||||
if address == "" {
|
||||
return nil
|
||||
}
|
||||
addr, err := netip.ParseAddrPort(address)
|
||||
if err != nil {
|
||||
a, err := netip.ParseAddr(address)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
addr = netip.AddrPortFrom(a, 0)
|
||||
}
|
||||
zone := addr.Addr().Zone()
|
||||
if zone == "" {
|
||||
return nil
|
||||
}
|
||||
if iface, err := net.InterfaceByName(zone); err == nil {
|
||||
return iface
|
||||
}
|
||||
if idx, err := strconv.Atoi(zone); err == nil {
|
||||
if iface, err := net.InterfaceByIndex(idx); err == nil {
|
||||
return iface
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setIPv4BoundIf(fd uintptr, iface *net.Interface) error {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index); err != nil {
|
||||
return fmt.Errorf("set IP_BOUND_IF: %w (interface: %s, index: %d)", err, iface.Name, iface.Index)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setIPv6BoundIf(fd uintptr, iface *net.Interface) error {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index); err != nil {
|
||||
return fmt.Errorf("set IPV6_BOUND_IF: %w (interface: %s, index: %d)", err, iface.Name, iface.Index)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyBoundIfToSocket binds the socket to the cached physical egress interface
|
||||
// so scoped route lookup avoids the VPN utun and egresses the underlay directly.
|
||||
func applyBoundIfToSocket(network, address string, c syscall.RawConn) error {
|
||||
if !AdvancedRouting() {
|
||||
return nil
|
||||
}
|
||||
|
||||
iface := boundInterfaceFor(network, address)
|
||||
if iface == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
isV6 := isV6Network(network)
|
||||
var controlErr error
|
||||
if err := c.Control(func(fd uintptr) {
|
||||
if isV6 {
|
||||
controlErr = setIPv6BoundIf(fd, iface)
|
||||
} else {
|
||||
controlErr = setIPv4BoundIf(fd, iface)
|
||||
}
|
||||
if controlErr == nil {
|
||||
log.Debugf("set BOUND_IF=%d on %s for %s to %s", iface.Index, iface.Name, network, address)
|
||||
}
|
||||
}); err != nil {
|
||||
return fmt.Errorf("control: %w", err)
|
||||
}
|
||||
return controlErr
|
||||
}
|
||||
Reference in New Issue
Block a user