diff --git a/net/interfaces/interfaces_darwin.go b/net/interfaces/interfaces_darwin.go index 60335bf80..c95e7e058 100644 --- a/net/interfaces/interfaces_darwin.go +++ b/net/interfaces/interfaces_darwin.go @@ -10,6 +10,7 @@ "log" "net" "net/netip" + "os" "syscall" "golang.org/x/net/route" @@ -86,6 +87,76 @@ func DefaultRouteInterfaceIndex() (int, error) { return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen) } +// InterfaceIndexFor returns the interface index that we should bind to in +// order to send traffic to the provided address. +func InterfaceIndexFor(addr netip.Addr) (int, error) { + fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC) + if err != nil { + return 0, fmt.Errorf("creating AF_ROUTE socket: %w", err) + } + defer unix.Close(fd) + + var routeAddr route.Addr + if addr.Is4() { + routeAddr = &route.Inet4Addr{IP: addr.As4()} + } else { + routeAddr = &route.Inet6Addr{IP: addr.As16()} + } + + rm := route.RouteMessage{ + Version: unix.RTM_VERSION, + Type: unix.RTM_GET, + Flags: unix.RTF_UP, + ID: uintptr(os.Getpid()), + Seq: 1, + Addrs: []route.Addr{ + unix.RTAX_DST: routeAddr, + }, + } + b, err := rm.Marshal() + if err != nil { + return 0, fmt.Errorf("marshaling RouteMessage: %w", err) + } + _, err = unix.Write(fd, b) + if err != nil { + return 0, fmt.Errorf("writing message: %w") + } + var buf [2048]byte + n, err := unix.Read(fd, buf[:]) + if err != nil { + return 0, fmt.Errorf("reading message: %w", err) + } + msgs, err := route.ParseRIB(route.RIBTypeRoute, buf[:n]) + if err != nil { + return 0, fmt.Errorf("route.ParseRIB: %w", err) + } + if len(msgs) == 0 { + return 0, fmt.Errorf("no messages") + } + + for _, msg := range msgs { + rm, ok := msg.(*route.RouteMessage) + if !ok { + continue + } + if rm.Version < 3 || rm.Version > 5 || rm.Type != unix.RTM_GET { + continue + } + if len(rm.Addrs) < unix.RTAX_GATEWAY { + continue + } + + laddr, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.LinkAddr) + if !ok { + continue + } + + return laddr.Index, nil + } + + return 0, fmt.Errorf("no valid address found") +} + func init() { likelyHomeRouterIP = likelyHomeRouterIPDarwinFetchRIB } diff --git a/net/netns/netns_darwin_tailscaled.go b/net/netns/netns_darwin_tailscaled.go index ce35aa999..a1d0f69c8 100644 --- a/net/netns/netns_darwin_tailscaled.go +++ b/net/netns/netns_darwin_tailscaled.go @@ -9,6 +9,7 @@ import ( "fmt" + "net/netip" "strings" "syscall" @@ -32,12 +33,20 @@ func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) e // Don't bind to an interface for localhost connections. return nil } - idx, err := interfaces.DefaultRouteInterfaceIndex() + addr, err := netip.ParseAddr(address) if err != nil { - logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err) - return nil + return fmt.Errorf("netip.ParseAddr(%s): %w", address, err) } - v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6 + idx, err := interfaces.InterfaceIndexFor(addr) + if err != nil { + logf("[unexpected] netns: InterfaceIndexFor failed; falling back to default: %v", err) + idx, err = interfaces.DefaultRouteInterfaceIndex() + if err != nil { + logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err) + return nil + } + } + v6 := addr.Is6() proto := unix.IPPROTO_IP opt := unix.IP_BOUND_IF if v6 {