From 42280f04cedaf1d5192a91b57c2ec9c595f5e90c Mon Sep 17 00:00:00 2001 From: John Maguire Date: Tue, 13 Oct 2026 13:50:37 -0400 Subject: [PATCH] darwin: Assign host address to the utun and route vpn explicitly (#1931) Tailscale with an exit node treats every interface address prefix as an attached LAN and either claims it or excludes it, and configd then rewrites the matching route away from nebula's utun. Interfaces holding a single address are skipped, so assign /32 and /128 and add the network route ourselves for both families. --- overlay/tun_darwin.go | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/overlay/tun_darwin.go b/overlay/tun_darwin.go index a0834590..3a7b85cd 100644 --- a/overlay/tun_darwin.go +++ b/overlay/tun_darwin.go @@ -271,6 +271,7 @@ func (t *tun) Activate() error { return t.addRoutes(false) } +// activate4 assigns our vpn address as a /32 and routes the vpn network to the tun explicitly. func (t *tun) activate4(network netip.Prefix) error { s, err := unix.Socket( unix.AF_INET, @@ -297,22 +298,32 @@ func (t *tun) activate4(network netip.Prefix) error { MaskAddr: unix.RawSockaddrInet4{ Len: unix.SizeofSockaddrInet4, Family: unix.AF_INET, - Addr: prefixToMask(network).As4(), + Addr: [4]byte{255, 255, 255, 255}, //always make the address a /32 }, } + // On Darwin, VPNs don't edit the routing table directly. + // A network extension hands its routes to the system, and configd installs them. + // configd replaces any route already holding the same prefix, including ours. + // As an example, Tailscale with an exit node treats any interface prefix wider than one address as a LAN. + // With LAN access off, it routes each LAN into Tailscale's utun. + // With LAN access on, it excludes each LAN from its tunnel. + // configd routes an excluded prefix like its own routes would, ignoring ours, so it goes out the default route. + // Either way our route is replaced, and traffic for the vpn network no longer reaches nebula. + // A single address isn't a LAN to Tailscale, so our route is left alone. + // We route the vpn network to the tun explicitly instead. + if err := ioctl(uintptr(s), unix.SIOCAIFADDR, uintptr(unsafe.Pointer(&ifr))); err != nil { return fmt.Errorf("failed to set tun v4 address: %s", err) } - err = addRoute(network, t.linkAddr.Load()) - if err != nil { - return err + if err := addRoute(network, t.linkAddr.Load()); err != nil { + return fmt.Errorf("failed to add route for %v: %w", network.Masked(), err) } - return nil } +// activate6 is activate4 for IPv6 func (t *tun) activate6(network netip.Prefix) error { s, err := unix.Socket( unix.AF_INET6, @@ -334,20 +345,24 @@ func (t *tun) activate6(network netip.Prefix) error { PrefixMask: unix.RawSockaddrInet6{ Len: unix.SizeofSockaddrInet6, Family: unix.AF_INET6, - Addr: prefixToMask(network).As16(), + // Always set the netmask to be a /128. See activate4 for why we do this. + Addr: [16]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, }, Lifetime: addrLifetime{ // never expires Vltime: 0xffffffff, Pltime: 0xffffffff, }, - Flags: _IN6_IFF_NODAD, + Flags: _IN6_IFF_NODAD, // No DAD, a utun has no neighbors. } if err := ioctl(uintptr(s), _SIOCAIFADDR_IN6, uintptr(unsafe.Pointer(&ifr))); err != nil { return fmt.Errorf("failed to set tun address: %s", err) } + if err := addRoute(network, t.linkAddr.Load()); err != nil { + return fmt.Errorf("failed to add route for %v: %w", network.Masked(), err) + } return nil }