mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
net/interfaces: redo how we get the default interface on macOS and iOS
With #6566 we added an external mechanism for getting the default interface, and used it on macOS and iOS (see tailscale/corp#8201). 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 #6566. Updates #7184 Updates #7188 Signed-off-by: Mihai Parparita <mihai@tailscale.com>
This commit is contained in:
parent
21fda7f670
commit
fa932fefe7
@ -81,7 +81,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/mak from tailscale.com/syncs
|
||||
tailscale.com/util/mak from tailscale.com/syncs+
|
||||
tailscale.com/util/multierr from tailscale.com/health
|
||||
tailscale.com/util/set from tailscale.com/health
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
|
@ -3840,7 +3840,6 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
|
||||
// See the netns package for documentation on what this capability does.
|
||||
netns.SetBindToInterfaceByRoute(hasCapability(nm, tailcfg.CapabilityBindToInterfaceByRoute))
|
||||
interfaces.SetDisableAlternateDefaultRouteInterface(hasCapability(nm, tailcfg.CapabilityDebugDisableAlternateDefaultRouteInterface))
|
||||
netns.SetDisableBindConnToInterface(hasCapability(nm, tailcfg.CapabilityDebugDisableBindConnToInterface))
|
||||
|
||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
|
||||
|
@ -13,7 +13,6 @@
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/net/netaddr"
|
||||
@ -757,14 +756,3 @@ func HasCGNATInterface() (bool, error) {
|
||||
}
|
||||
return hasCGNATInterface, nil
|
||||
}
|
||||
|
||||
var disableAlternateDefaultRouteInterface atomic.Bool
|
||||
|
||||
// SetDisableAlternateDefaultRouteInterface disables the optional external/
|
||||
// alternate mechanism for getting the default route network interface.
|
||||
//
|
||||
// Currently, this only changes the behaviour on BSD-like sytems (FreeBSD and
|
||||
// Darwin).
|
||||
func SetDisableAlternateDefaultRouteInterface(v bool) {
|
||||
disableAlternateDefaultRouteInterface.Store(v)
|
||||
}
|
||||
|
@ -19,7 +19,6 @@
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
@ -40,19 +39,6 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
// 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) {
|
||||
disabledAlternateDefaultRouteInterface := false
|
||||
if f := defaultRouteInterfaceIndexFunc.Load(); f != nil {
|
||||
if ifIndex := f(); ifIndex != 0 {
|
||||
if !disableAlternateDefaultRouteInterface.Load() {
|
||||
return ifIndex, nil
|
||||
} else {
|
||||
disabledAlternateDefaultRouteInterface = true
|
||||
log.Printf("interfaces_bsd: alternate default route interface function disabled, would have returned interface %d", ifIndex)
|
||||
}
|
||||
}
|
||||
// Fallthrough if we can't use the alternate implementation.
|
||||
}
|
||||
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
// Internet:
|
||||
@ -81,8 +67,10 @@ func DefaultRouteInterfaceIndex() (int, error) {
|
||||
continue
|
||||
}
|
||||
if isDefaultGateway(rm) {
|
||||
if disabledAlternateDefaultRouteInterface {
|
||||
log.Printf("interfaces_bsd: alternate default route interface function disabled, default implementation returned %d", rm.Index)
|
||||
if delegatedIndex, err := getDelegatedInterface(rm.Index); err == nil && delegatedIndex != 0 {
|
||||
return delegatedIndex, nil
|
||||
} else if err != nil {
|
||||
log.Printf("interfaces_bsd: could not get delegated interface: %v", err)
|
||||
}
|
||||
return rm.Index, nil
|
||||
}
|
||||
@ -90,16 +78,6 @@ func DefaultRouteInterfaceIndex() (int, error) {
|
||||
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
|
||||
}
|
||||
|
@ -4,9 +4,15 @@
|
||||
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.
|
||||
@ -17,3 +23,73 @@ func fetchRoutingTable() (rib []byte, err error) {
|
||||
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
|
||||
}
|
||||
|
@ -22,3 +22,7 @@ func fetchRoutingTable() (rib []byte, err error) {
|
||||
func parseRoutingTable(rib []byte) ([]route.Message, error) {
|
||||
return route.ParseRIB(syscall.NET_RT_IFLIST, rib)
|
||||
}
|
||||
|
||||
func getDelegatedInterface(ifIndex int) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user