tailscale/net/interfaces/interfaces_darwin.go
Mihai Parparita fa932fefe7 net/interfaces: redo how we get the default interface on macOS and iOS
With  we added an external mechanism for getting the default
interface, and used it on macOS and iOS (see ).
The goal was to be able to get the default physical interface even when
using an exit node (in which case the routing table would say that the
Tailscale utun* interface is the default).

However, the external mechanism turns out to be unreliable in some
cases, e.g. when multiple cellular interfaces are present/toggled (I
have occasionally gotten my phone into a state where it reports the pdp_ip1
interface as the default, even though it can't actually route traffic).

It was observed that `ifconfig -v` on macOS reports an "effective interface"
for the Tailscale utn* interface, which seems promising. By examining
the ifconfig source code, it turns out that this is done via a
SIOCGIFDELEGATE ioctl syscall. Though this is a private API, it appears
to have been around for a long time (e.g. it's in the 10.13 xnu release
at https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/net/if_types.h.auto.html)
and thus is unlikely to go away.

We can thus use this ioctl if the routing table says that a utun*
interface is the default, and go back to the simpler mechanism that
we had before .

Updates 
Updates 

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-02-10 16:23:37 -08:00

96 lines
2.8 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package interfaces
import (
"net"
"strings"
"sync"
"syscall"
"unsafe"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"tailscale.com/util/mak"
)
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2.
func fetchRoutingTable() (rib []byte, err error) {
return route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
}
func parseRoutingTable(rib []byte) ([]route.Message, error) {
return route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
}
var ifNames struct {
sync.Mutex
m map[int]string // ifindex => name
}
// getDelegatedInterface returns the interface index of the underlying interface
// for the given interface index. 0 is returned if the interface does not
// delegate.
func getDelegatedInterface(ifIndex int) (int, error) {
ifNames.Lock()
defer ifNames.Unlock()
// To get the delegated interface, we do what ifconfig does and use the
// SIOCGIFDELEGATE ioctl. It operates in term of a ifreq struct, which
// has to be populated with a interface name. To avoid having to do a
// interface index -> name lookup every time, we cache interface names
// (since indexes and names are stable after boot).
ifName, ok := ifNames.m[ifIndex]
if !ok {
iface, err := net.InterfaceByIndex(ifIndex)
if err != nil {
return 0, err
}
ifName = iface.Name
mak.Set(&ifNames.m, ifIndex, ifName)
}
// Only tunnels (like Tailscale itself) have a delegated interface, avoid
// the ioctl if we can.
if !strings.HasPrefix(ifName, "utun") {
return 0, nil
}
// We don't cache the result of the ioctl, since the delegated interface can
// change, e.g. if the user changes the preferred service order in the
// network preference pane.
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
return 0, err
}
defer unix.Close(fd)
// Match the ifreq struct/union from the bsd/net/if.h header in the Darwin
// open source release.
var ifr struct {
ifr_name [unix.IFNAMSIZ]byte
ifr_delegated uint32
}
copy(ifr.ifr_name[:], ifName)
// SIOCGIFDELEGATE is not in the Go x/sys package or in the public macOS
// <sys/sockio.h> headers. However, it is in the Darwin/xnu open source
// release (and is used by ifconfig, see
// https://github.com/apple-oss-distributions/network_cmds/blob/6ccdc225ad5aa0d23ea5e7d374956245d2462427/ifconfig.tproj/ifconfig.c#L2183-L2187).
// We generate its value by evaluating the `_IOWR('i', 157, struct ifreq)`
// macro, which is how it's defined in
// https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/sys/sockio.h#L264
const SIOCGIFDELEGATE = 0xc020699d
_, _, errno := syscall.Syscall(
syscall.SYS_IOCTL,
uintptr(fd),
uintptr(SIOCGIFDELEGATE),
uintptr(unsafe.Pointer(&ifr)))
if errno != 0 {
return 0, errno
}
return int(ifr.ifr_delegated), nil
}