// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause // Common code for FreeBSD and Darwin. This might also work on other // BSD systems (e.g. OpenBSD) but has not been tested. //go:build darwin || freebsd package interfaces import ( "errors" "fmt" "log" "net" "net/netip" "syscall" "golang.org/x/net/route" "golang.org/x/sys/unix" "tailscale.com/net/netaddr" "tailscale.com/syncs" ) func defaultRoute() (d DefaultRouteDetails, err error) { idx, err := DefaultRouteInterfaceIndex() if err != nil { return d, err } iface, err := net.InterfaceByIndex(idx) if err != nil { return d, err } d.InterfaceName = iface.Name d.InterfaceIndex = idx return d, nil } // DefaultRouteInterfaceIndex returns the index of the network interface that // owns the default route. It returns the first IPv4 or IPv6 default route it // finds (it does not prefer one or the other). func DefaultRouteInterfaceIndex() (int, error) { if f := defaultRouteInterfaceIndexFunc.Load(); f != nil { if ifIndex := f(); ifIndex != 0 { return ifIndex, nil } // Fallthrough if we can't use the alternate implementation. } // $ netstat -nr // Routing tables // Internet: // Destination Gateway Flags Netif Expire // default 10.0.0.1 UGSc en0 <-- want this one // default 10.0.0.1 UGScI en1 // From man netstat: // U RTF_UP Route usable // G RTF_GATEWAY Destination requires forwarding by intermediary // S RTF_STATIC Manually added // c RTF_PRCLONING Protocol-specified generate new routes on use // I RTF_IFSCOPE Route is associated with an interface scope rib, err := fetchRoutingTable() if err != nil { return 0, fmt.Errorf("route.FetchRIB: %w", err) } msgs, err := parseRoutingTable(rib) if err != nil { return 0, fmt.Errorf("route.ParseRIB: %w", err) } for _, m := range msgs { rm, ok := m.(*route.RouteMessage) if !ok { continue } if isDefaultGateway(rm) { return rm.Index, nil } } return 0, errors.New("no gateway index found") } var defaultRouteInterfaceIndexFunc syncs.AtomicValue[func() int] // SetDefaultRouteInterfaceIndexFunc allows an alternate implementation of // DefaultRouteInterfaceIndex to be provided. If none is set, or if f() returns a 0 // (indicating an unknown interface index), then the default implementation (that parses // the routing table) will be used. func SetDefaultRouteInterfaceIndexFunc(f func() int) { defaultRouteInterfaceIndexFunc.Store(f) } func init() { likelyHomeRouterIP = likelyHomeRouterIPBSDFetchRIB } func likelyHomeRouterIPBSDFetchRIB() (ret netip.Addr, ok bool) { rib, err := fetchRoutingTable() if err != nil { log.Printf("routerIP/FetchRIB: %v", err) return ret, false } msgs, err := parseRoutingTable(rib) if err != nil { log.Printf("routerIP/ParseRIB: %v", err) return ret, false } for _, m := range msgs { rm, ok := m.(*route.RouteMessage) if !ok { continue } if !isDefaultGateway(rm) { continue } gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr) if !ok { continue } return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true } return ret, false } var v4default = [4]byte{0, 0, 0, 0} var v6default = [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} func isDefaultGateway(rm *route.RouteMessage) bool { if rm.Flags&unix.RTF_GATEWAY == 0 { return false } // Defined locally because FreeBSD does not have unix.RTF_IFSCOPE. const RTF_IFSCOPE = 0x1000000 if rm.Flags&RTF_IFSCOPE != 0 { return false } // Addrs is [RTAX_DST, RTAX_GATEWAY, RTAX_NETMASK, ...] if len(rm.Addrs) <= unix.RTAX_NETMASK { return false } dst := rm.Addrs[unix.RTAX_DST] netmask := rm.Addrs[unix.RTAX_NETMASK] if dst == nil || netmask == nil { return false } if dst.Family() == syscall.AF_INET && netmask.Family() == syscall.AF_INET { dstAddr, dstOk := dst.(*route.Inet4Addr) nmAddr, nmOk := netmask.(*route.Inet4Addr) if dstOk && nmOk && dstAddr.IP == v4default && nmAddr.IP == v4default { return true } } if dst.Family() == syscall.AF_INET6 && netmask.Family() == syscall.AF_INET6 { dstAddr, dstOk := dst.(*route.Inet6Addr) nmAddr, nmOk := netmask.(*route.Inet6Addr) if dstOk && nmOk && dstAddr.IP == v6default && nmAddr.IP == v6default { return true } } return false }