mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-11 13:18:53 +00:00
net/{interfaces,netmon}, all: merge net/interfaces package into net/netmon
In prep for most of the package funcs in net/interfaces to become methods in a long-lived netmon.Monitor that can cache things. (Many of the funcs are very heavy to call regularly, whereas the long-lived netmon.Monitor can subscribe to things from the OS and remember answers to questions it's asked regularly later) Updates tailscale/corp#10910 Updates tailscale/corp#18960 Updates #7967 Updates #3299 Change-Id: Ie4e8dedb70136af2d611b990b865a822cd1797e5 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:

committed by
Brad Fitzpatrick

parent
6b95219e3a
commit
b9adbe2002
26
net/netmon/defaultroute_bsd.go
Normal file
26
net/netmon/defaultroute_bsd.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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.
|
||||
// Not used on iOS. See defaultroute_ios.go.
|
||||
|
||||
//go:build !ios && (darwin || freebsd)
|
||||
|
||||
package netmon
|
||||
|
||||
import "net"
|
||||
|
||||
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
|
||||
}
|
108
net/netmon/defaultroute_ios.go
Normal file
108
net/netmon/defaultroute_ios.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ios
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
var (
|
||||
lastKnownDefaultRouteIfName syncs.AtomicValue[string]
|
||||
)
|
||||
|
||||
// UpdateLastKnownDefaultRouteInterface is called by ipn-go-bridge in the iOS app when
|
||||
// our NWPathMonitor instance detects a network path transition.
|
||||
func UpdateLastKnownDefaultRouteInterface(ifName string) {
|
||||
if ifName == "" {
|
||||
return
|
||||
}
|
||||
if old := lastKnownDefaultRouteIfName.Swap(ifName); old != ifName {
|
||||
log.Printf("defaultroute_ios: update from Swift, ifName = %s (was %s)", ifName, old)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
// We cannot rely on the delegated interface data on iOS. The NetworkExtension framework
|
||||
// seems to set the delegate interface only once, upon the *creation* of the VPN tunnel.
|
||||
// If a network transition (e.g. from Wi-Fi to Cellular) happens while the tunnel is
|
||||
// connected, it will be ignored and we will still try to set Wi-Fi as the default route
|
||||
// because the delegated interface is not updated by the NetworkExtension framework.
|
||||
//
|
||||
// We work around this on the Swift side with a NWPathMonitor instance that observes
|
||||
// the interface name of the first currently satisfied network path. Our Swift code will
|
||||
// call into `UpdateLastKnownDefaultRouteInterface`, so we can rely on that when it is set.
|
||||
//
|
||||
// If for any reason the Swift machinery didn't work and we don't get any updates, here
|
||||
// we also have some fallback logic: we try finding a hardcoded Wi-Fi interface called en0.
|
||||
// If en0 is down, we fall back to cellular (pdp_ip0) as a last resort. This doesn't handle
|
||||
// all edge cases like USB-Ethernet adapters or multiple Ethernet interfaces, but is good
|
||||
// enough to ensure connectivity isn't broken.
|
||||
|
||||
// Start by getting all available interfaces.
|
||||
interfaces, err := netInterfaces()
|
||||
if err != nil {
|
||||
log.Printf("defaultroute_ios: could not get interfaces: %v", err)
|
||||
return d, ErrNoGatewayIndexFound
|
||||
}
|
||||
|
||||
getInterfaceByName := func(name string) *Interface {
|
||||
for _, ifc := range interfaces {
|
||||
if ifc.Name != name {
|
||||
continue
|
||||
}
|
||||
|
||||
if !ifc.IsUp() {
|
||||
log.Printf("defaultroute_ios: %s is down", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
addrs, _ := ifc.Addrs()
|
||||
if len(addrs) == 0 {
|
||||
log.Printf("defaultroute_ios: %s has no addresses", name)
|
||||
return nil
|
||||
}
|
||||
return &ifc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Did Swift set lastKnownDefaultRouteInterface? If so, we should use it and don't bother
|
||||
// with anything else. However, for sanity, do check whether Swift gave us with an interface
|
||||
// that exists, is up, and has an address.
|
||||
if swiftIfName := lastKnownDefaultRouteIfName.Load(); swiftIfName != "" {
|
||||
ifc := getInterfaceByName(swiftIfName)
|
||||
if ifc != nil {
|
||||
d.InterfaceName = ifc.Name
|
||||
d.InterfaceIndex = ifc.Index
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Start of our fallback logic if Swift didn't give us an interface name, or gave us an invalid
|
||||
// one.
|
||||
// We start by attempting to use the Wi-Fi interface, which on iPhone is always called en0.
|
||||
enZeroIf := getInterfaceByName("en0")
|
||||
if enZeroIf != nil {
|
||||
log.Println("defaultroute_ios: using en0 (fallback)")
|
||||
d.InterfaceName = enZeroIf.Name
|
||||
d.InterfaceIndex = enZeroIf.Index
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Did it not work? Let's try with Cellular (pdp_ip0).
|
||||
cellIf := getInterfaceByName("pdp_ip0")
|
||||
if cellIf != nil {
|
||||
log.Println("defaultroute_ios: using pdp_ip0 (fallback)")
|
||||
d.InterfaceName = cellIf.Name
|
||||
d.InterfaceIndex = cellIf.Index
|
||||
return d, nil
|
||||
}
|
||||
|
||||
log.Println("defaultroute_ios: no running interfaces available")
|
||||
return d, ErrNoGatewayIndexFound
|
||||
}
|
183
net/netmon/interfaces_android.go
Normal file
183
net/netmon/interfaces_android.go
Normal file
@@ -0,0 +1,183 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
"sync/atomic"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/util/lineread"
|
||||
)
|
||||
|
||||
var (
|
||||
lastKnownDefaultRouteIfName syncs.AtomicValue[string]
|
||||
)
|
||||
|
||||
var procNetRoutePath = "/proc/net/route"
|
||||
|
||||
// maxProcNetRouteRead is the max number of lines to read from
|
||||
// /proc/net/route looking for a default route.
|
||||
const maxProcNetRouteRead = 1000
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPAndroid
|
||||
}
|
||||
|
||||
var procNetRouteErr atomic.Bool
|
||||
|
||||
// errStopReading is a sentinel error value used internally by
|
||||
// lineread.File callers to stop reading. It doesn't escape to
|
||||
// callers/users.
|
||||
var errStopReading = errors.New("stop reading")
|
||||
|
||||
/*
|
||||
Parse 10.0.0.1 out of:
|
||||
|
||||
$ cat /proc/net/route
|
||||
Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
|
||||
ens18 00000000 0100000A 0003 0 0 0 00000000 0 0 0
|
||||
ens18 0000000A 00000000 0001 0 0 0 0000FFFF 0 0 0
|
||||
*/
|
||||
func likelyHomeRouterIPAndroid() (ret netip.Addr, myIP netip.Addr, ok bool) {
|
||||
if procNetRouteErr.Load() {
|
||||
// If we failed to read /proc/net/route previously, don't keep trying.
|
||||
return likelyHomeRouterIPHelper()
|
||||
}
|
||||
lineNum := 0
|
||||
var f []mem.RO
|
||||
err := lineread.File(procNetRoutePath, func(line []byte) error {
|
||||
lineNum++
|
||||
if lineNum == 1 {
|
||||
// Skip header line.
|
||||
return nil
|
||||
}
|
||||
if lineNum > maxProcNetRouteRead {
|
||||
return errStopReading
|
||||
}
|
||||
f = mem.AppendFields(f[:0], mem.B(line))
|
||||
if len(f) < 4 {
|
||||
return nil
|
||||
}
|
||||
gwHex, flagsHex := f[2], f[3]
|
||||
flags, err := mem.ParseUint(flagsHex, 16, 16)
|
||||
if err != nil {
|
||||
return nil // ignore error, skip line and keep going
|
||||
}
|
||||
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
|
||||
return nil
|
||||
}
|
||||
ipu32, err := mem.ParseUint(gwHex, 16, 32)
|
||||
if err != nil {
|
||||
return nil // ignore error, skip line and keep going
|
||||
}
|
||||
ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
|
||||
if ip.IsPrivate() {
|
||||
ret = ip
|
||||
return errStopReading
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if errors.Is(err, errStopReading) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
procNetRouteErr.Store(true)
|
||||
return likelyHomeRouterIP()
|
||||
}
|
||||
if ret.IsValid() {
|
||||
// Try to get the local IP of the interface associated with
|
||||
// this route to short-circuit finding the IP associated with
|
||||
// this gateway. This isn't fatal if it fails.
|
||||
if len(f) > 0 && !disableLikelyHomeRouterIPSelf() {
|
||||
ForeachInterface(func(ni Interface, pfxs []netip.Prefix) {
|
||||
// Ensure this is the same interface
|
||||
if !f[0].EqualString(ni.Name) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the first IPv4 address and use it.
|
||||
for _, pfx := range pfxs {
|
||||
if addr := pfx.Addr(); addr.Is4() {
|
||||
myIP = addr
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return ret, myIP, true
|
||||
}
|
||||
if lineNum >= maxProcNetRouteRead {
|
||||
// If we went over our line limit without finding an answer, assume
|
||||
// we're a big fancy Linux router (or at least not a home system)
|
||||
// and set the error bit so we stop trying this in the future (and wasting CPU).
|
||||
// See https://github.com/tailscale/tailscale/issues/7621.
|
||||
//
|
||||
// Remember that "likelyHomeRouterIP" exists purely to find the port
|
||||
// mapping service (UPnP, PMP, PCP) often present on a home router. If we hit
|
||||
// the route (line) limit without finding an answer, we're unlikely to ever
|
||||
// find one in the future.
|
||||
procNetRouteErr.Store(true)
|
||||
}
|
||||
return netip.Addr{}, netip.Addr{}, false
|
||||
}
|
||||
|
||||
// Android apps don't have permission to read /proc/net/route, at
|
||||
// least on Google devices and the Android emulator.
|
||||
func likelyHomeRouterIPHelper() (ret netip.Addr, _ netip.Addr, ok bool) {
|
||||
cmd := exec.Command("/system/bin/ip", "route", "show", "table", "0")
|
||||
out, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("interfaces: running /system/bin/ip: %v", err)
|
||||
return
|
||||
}
|
||||
// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
|
||||
lineread.Reader(out, func(line []byte) error {
|
||||
const pfx = "default via "
|
||||
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
|
||||
return nil
|
||||
}
|
||||
line = line[len(pfx):]
|
||||
sp := bytes.IndexByte(line, ' ')
|
||||
if sp == -1 {
|
||||
return nil
|
||||
}
|
||||
ipb := line[:sp]
|
||||
if ip, err := netip.ParseAddr(string(ipb)); err == nil && ip.Is4() {
|
||||
ret = ip
|
||||
log.Printf("interfaces: found Android default route %v", ip)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
return ret, netip.Addr{}, ret.IsValid()
|
||||
}
|
||||
|
||||
// UpdateLastKnownDefaultRouteInterface is called by libtailscale in the Android app when
|
||||
// the connectivity manager detects a network path transition. If ifName is "", network has been lost.
|
||||
// After updating the interface, Android calls Monitor.InjectEvent(), triggering a link change.
|
||||
func UpdateLastKnownDefaultRouteInterface(ifName string) {
|
||||
if old := lastKnownDefaultRouteIfName.Swap(ifName); old != ifName {
|
||||
log.Printf("defaultroute: update from Android, ifName = %s (was %s)", ifName, old)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
if ifName := lastKnownDefaultRouteIfName.Load(); ifName != "" {
|
||||
d.InterfaceName = ifName
|
||||
}
|
||||
return d, nil
|
||||
}
|
153
net/netmon/interfaces_bsd.go
Normal file
153
net/netmon/interfaces_bsd.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// 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 netmon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/netaddr"
|
||||
)
|
||||
|
||||
// ErrNoGatewayIndexFound is returned by DefaultRouteInterfaceIndex when no
|
||||
// default route is found.
|
||||
var ErrNoGatewayIndexFound = errors.New("no gateway index found")
|
||||
|
||||
// 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) {
|
||||
// $ 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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
return 0, ErrNoGatewayIndexFound
|
||||
}
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPBSDFetchRIB
|
||||
}
|
||||
|
||||
func likelyHomeRouterIPBSDFetchRIB() (ret, myIP netip.Addr, ok bool) {
|
||||
rib, err := fetchRoutingTable()
|
||||
if err != nil {
|
||||
log.Printf("routerIP/FetchRIB: %v", err)
|
||||
return ret, myIP, false
|
||||
}
|
||||
msgs, err := parseRoutingTable(rib)
|
||||
if err != nil {
|
||||
log.Printf("routerIP/ParseRIB: %v", err)
|
||||
return ret, myIP, 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
|
||||
}
|
||||
// If the route entry has an interface address associated with
|
||||
// it, then parse and return that. This is optional.
|
||||
if len(rm.Addrs) >= unix.RTAX_IFA {
|
||||
if addr, ok := rm.Addrs[unix.RTAX_IFA].(*route.Inet4Addr); ok {
|
||||
myIP = netaddr.IPv4(addr.IP[0], addr.IP[1], addr.IP[2], addr.IP[3])
|
||||
}
|
||||
}
|
||||
|
||||
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), myIP, true
|
||||
}
|
||||
|
||||
return ret, myIP, 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
|
||||
}
|
111
net/netmon/interfaces_darwin.go
Normal file
111
net/netmon/interfaces_darwin.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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
|
||||
}
|
||||
|
||||
func init() {
|
||||
interfaceDebugExtras = interfaceDebugExtrasDarwin
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func interfaceDebugExtrasDarwin(ifIndex int) (string, error) {
|
||||
delegated, err := getDelegatedInterface(ifIndex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if delegated == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return fmt.Sprintf("delegated=%d", delegated), nil
|
||||
}
|
114
net/netmon/interfaces_darwin_test.go
Normal file
114
net/netmon/interfaces_darwin_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func TestLikelyHomeRouterIPSyscallExec(t *testing.T) {
|
||||
syscallIP, _, syscallOK := likelyHomeRouterIPBSDFetchRIB()
|
||||
netstatIP, netstatIf, netstatOK := likelyHomeRouterIPDarwinExec()
|
||||
|
||||
if syscallOK != netstatOK || syscallIP != netstatIP {
|
||||
t.Errorf("syscall() = %v, %v, netstat = %v, %v",
|
||||
syscallIP, syscallOK,
|
||||
netstatIP, netstatOK,
|
||||
)
|
||||
}
|
||||
|
||||
if !syscallOK {
|
||||
return
|
||||
}
|
||||
|
||||
def, err := defaultRoute()
|
||||
if err != nil {
|
||||
t.Errorf("defaultRoute() error: %v", err)
|
||||
}
|
||||
|
||||
if def.InterfaceName != netstatIf {
|
||||
t.Errorf("syscall default route interface %s differs from netstat %s", def.InterfaceName, netstatIf)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Parse out 10.0.0.1 and en0 from:
|
||||
|
||||
$ netstat -r -n -f inet
|
||||
Routing tables
|
||||
|
||||
Internet:
|
||||
Destination Gateway Flags Netif Expire
|
||||
default 10.0.0.1 UGSc en0
|
||||
default link#14 UCSI utun2
|
||||
10/16 link#4 UCS en0 !
|
||||
10.0.0.1/32 link#4 UCS en0 !
|
||||
...
|
||||
*/
|
||||
func likelyHomeRouterIPDarwinExec() (ret netip.Addr, netif string, ok bool) {
|
||||
if version.IsMobile() {
|
||||
// Don't try to do subprocesses on iOS. Ends up with log spam like:
|
||||
// kernel: "Sandbox: IPNExtension(86580) deny(1) process-fork"
|
||||
// This is why we have likelyHomeRouterIPDarwinSyscall.
|
||||
return ret, "", false
|
||||
}
|
||||
cmd := exec.Command("/usr/sbin/netstat", "-r", "-n", "-f", "inet")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return
|
||||
}
|
||||
defer cmd.Wait()
|
||||
defer io.Copy(io.Discard, stdout) // clear the pipe to prevent hangs
|
||||
|
||||
var f []mem.RO
|
||||
lineread.Reader(stdout, func(lineb []byte) error {
|
||||
line := mem.B(lineb)
|
||||
if !mem.Contains(line, mem.S("default")) {
|
||||
return nil
|
||||
}
|
||||
f = mem.AppendFields(f[:0], line)
|
||||
if len(f) < 4 || !f[0].EqualString("default") {
|
||||
return nil
|
||||
}
|
||||
ipm, flagsm, netifm := f[1], f[2], f[3]
|
||||
if !mem.Contains(flagsm, mem.S("G")) {
|
||||
return nil
|
||||
}
|
||||
if mem.Contains(flagsm, mem.S("I")) {
|
||||
return nil
|
||||
}
|
||||
ip, err := netip.ParseAddr(string(mem.Append(nil, ipm)))
|
||||
if err == nil && ip.IsPrivate() {
|
||||
ret = ip
|
||||
netif = netifm.StringCopy()
|
||||
// We've found what we're looking for.
|
||||
return errStopReadingNetstatTable
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return ret, netif, ret.IsValid()
|
||||
}
|
||||
|
||||
func TestFetchRoutingTable(t *testing.T) {
|
||||
// Issue 1345: this used to be flaky on darwin.
|
||||
for range 20 {
|
||||
_, err := fetchRoutingTable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var errStopReadingNetstatTable = errors.New("found private gateway")
|
20
net/netmon/interfaces_default_route_test.go
Normal file
20
net/netmon/interfaces_default_route_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || (darwin && !ts_macext)
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultRouteInterface(t *testing.T) {
|
||||
// tests /proc/net/route on the local system, cannot make an assertion about
|
||||
// the correct interface name, but good as a sanity check.
|
||||
v, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("got %q", v)
|
||||
}
|
14
net/netmon/interfaces_defaultrouteif_todo.go
Normal file
14
net/netmon/interfaces_defaultrouteif_todo.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux && !windows && !darwin && !freebsd && !android
|
||||
|
||||
package netmon
|
||||
|
||||
import "errors"
|
||||
|
||||
var errTODO = errors.New("TODO")
|
||||
|
||||
func defaultRoute() (DefaultRouteDetails, error) {
|
||||
return DefaultRouteDetails{}, errTODO
|
||||
}
|
28
net/netmon/interfaces_freebsd.go
Normal file
28
net/netmon/interfaces_freebsd.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// This might work on other BSDs, but only tested on FreeBSD.
|
||||
|
||||
//go:build freebsd
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP.
|
||||
func fetchRoutingTable() (rib []byte, err error) {
|
||||
return route.FetchRIB(syscall.AF_UNSPEC, unix.NET_RT_DUMP, 0)
|
||||
}
|
||||
|
||||
func parseRoutingTable(rib []byte) ([]route.Message, error) {
|
||||
return route.ParseRIB(syscall.NET_RT_IFLIST, rib)
|
||||
}
|
||||
|
||||
func getDelegatedInterface(ifIndex int) (int, error) {
|
||||
return 0, nil
|
||||
}
|
305
net/netmon/interfaces_linux.go
Normal file
305
net/netmon/interfaces_linux.go
Normal file
@@ -0,0 +1,305 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !android
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/jsimonetti/rtnetlink"
|
||||
"github.com/mdlayher/netlink"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/util/lineread"
|
||||
)
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPLinux
|
||||
}
|
||||
|
||||
var procNetRouteErr atomic.Bool
|
||||
|
||||
// errStopReading is a sentinel error value used internally by
|
||||
// lineread.File callers to stop reading. It doesn't escape to
|
||||
// callers/users.
|
||||
var errStopReading = errors.New("stop reading")
|
||||
|
||||
/*
|
||||
Parse 10.0.0.1 out of:
|
||||
|
||||
$ cat /proc/net/route
|
||||
Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
|
||||
ens18 00000000 0100000A 0003 0 0 0 00000000 0 0 0
|
||||
ens18 0000000A 00000000 0001 0 0 0 0000FFFF 0 0 0
|
||||
*/
|
||||
func likelyHomeRouterIPLinux() (ret netip.Addr, myIP netip.Addr, ok bool) {
|
||||
if procNetRouteErr.Load() {
|
||||
// If we failed to read /proc/net/route previously, don't keep trying.
|
||||
if runtime.GOOS == "android" {
|
||||
return likelyHomeRouterIPAndroid()
|
||||
}
|
||||
return ret, myIP, false
|
||||
}
|
||||
lineNum := 0
|
||||
var f []mem.RO
|
||||
err := lineread.File(procNetRoutePath, func(line []byte) error {
|
||||
lineNum++
|
||||
if lineNum == 1 {
|
||||
// Skip header line.
|
||||
return nil
|
||||
}
|
||||
if lineNum > maxProcNetRouteRead {
|
||||
return errStopReading
|
||||
}
|
||||
f = mem.AppendFields(f[:0], mem.B(line))
|
||||
if len(f) < 4 {
|
||||
return nil
|
||||
}
|
||||
gwHex, flagsHex := f[2], f[3]
|
||||
flags, err := mem.ParseUint(flagsHex, 16, 16)
|
||||
if err != nil {
|
||||
return nil // ignore error, skip line and keep going
|
||||
}
|
||||
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
|
||||
return nil
|
||||
}
|
||||
ipu32, err := mem.ParseUint(gwHex, 16, 32)
|
||||
if err != nil {
|
||||
return nil // ignore error, skip line and keep going
|
||||
}
|
||||
ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
|
||||
if ip.IsPrivate() {
|
||||
ret = ip
|
||||
return errStopReading
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if errors.Is(err, errStopReading) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
procNetRouteErr.Store(true)
|
||||
if runtime.GOOS == "android" {
|
||||
return likelyHomeRouterIPAndroid()
|
||||
}
|
||||
log.Printf("interfaces: failed to read /proc/net/route: %v", err)
|
||||
}
|
||||
if ret.IsValid() {
|
||||
// Try to get the local IP of the interface associated with
|
||||
// this route to short-circuit finding the IP associated with
|
||||
// this gateway. This isn't fatal if it fails.
|
||||
if len(f) > 0 && !disableLikelyHomeRouterIPSelf() {
|
||||
ForeachInterface(func(ni Interface, pfxs []netip.Prefix) {
|
||||
// Ensure this is the same interface
|
||||
if !f[0].EqualString(ni.Name) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the first IPv4 address and use it.
|
||||
for _, pfx := range pfxs {
|
||||
if addr := pfx.Addr(); addr.Is4() {
|
||||
myIP = addr
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return ret, myIP, true
|
||||
}
|
||||
if lineNum >= maxProcNetRouteRead {
|
||||
// If we went over our line limit without finding an answer, assume
|
||||
// we're a big fancy Linux router (or at least not a home system)
|
||||
// and set the error bit so we stop trying this in the future (and wasting CPU).
|
||||
// See https://github.com/tailscale/tailscale/issues/7621.
|
||||
//
|
||||
// Remember that "likelyHomeRouterIP" exists purely to find the port
|
||||
// mapping service (UPnP, PMP, PCP) often present on a home router. If we hit
|
||||
// the route (line) limit without finding an answer, we're unlikely to ever
|
||||
// find one in the future.
|
||||
procNetRouteErr.Store(true)
|
||||
}
|
||||
return netip.Addr{}, netip.Addr{}, false
|
||||
}
|
||||
|
||||
// Android apps don't have permission to read /proc/net/route, at
|
||||
// least on Google devices and the Android emulator.
|
||||
func likelyHomeRouterIPAndroid() (ret netip.Addr, _ netip.Addr, ok bool) {
|
||||
cmd := exec.Command("/system/bin/ip", "route", "show", "table", "0")
|
||||
out, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("interfaces: running /system/bin/ip: %v", err)
|
||||
return
|
||||
}
|
||||
// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
|
||||
lineread.Reader(out, func(line []byte) error {
|
||||
const pfx = "default via "
|
||||
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
|
||||
return nil
|
||||
}
|
||||
line = line[len(pfx):]
|
||||
sp := bytes.IndexByte(line, ' ')
|
||||
if sp == -1 {
|
||||
return nil
|
||||
}
|
||||
ipb := line[:sp]
|
||||
if ip, err := netip.ParseAddr(string(ipb)); err == nil && ip.Is4() {
|
||||
ret = ip
|
||||
log.Printf("interfaces: found Android default route %v", ip)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
return ret, netip.Addr{}, ret.IsValid()
|
||||
}
|
||||
|
||||
func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
v, err := defaultRouteInterfaceProcNet()
|
||||
if err == nil {
|
||||
d.InterfaceName = v
|
||||
return d, nil
|
||||
}
|
||||
// Issue 4038: the default route (such as on Unifi UDM Pro)
|
||||
// might be in a non-default table, so it won't show up in
|
||||
// /proc/net/route. Use netlink to find the default route.
|
||||
//
|
||||
// TODO(bradfitz): this allocates a fair bit. We should track
|
||||
// this in net/interfaces/monitor instead and have
|
||||
// interfaces.GetState take a netmon.Monitor or similar so the
|
||||
// routing table can be cached and the monitor's existing
|
||||
// subscription to route changes can update the cached state,
|
||||
// rather than querying the whole thing every time like
|
||||
// defaultRouteFromNetlink does.
|
||||
//
|
||||
// Then we should just always try to use the cached route
|
||||
// table from netlink every time, and only use /proc/net/route
|
||||
// as a fallback for weird environments where netlink might be
|
||||
// banned but /proc/net/route is emulated (e.g. stuff like
|
||||
// Cloud Run?).
|
||||
return defaultRouteFromNetlink()
|
||||
}
|
||||
|
||||
func defaultRouteFromNetlink() (d DefaultRouteDetails, err error) {
|
||||
c, err := rtnetlink.Dial(&netlink.Config{Strict: true})
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("defaultRouteFromNetlink: Dial: %w", err)
|
||||
}
|
||||
defer c.Close()
|
||||
rms, err := c.Route.List()
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("defaultRouteFromNetlink: List: %w", err)
|
||||
}
|
||||
for _, rm := range rms {
|
||||
if rm.Attributes.Gateway == nil {
|
||||
// A default route has a gateway. If it doesn't, skip it.
|
||||
continue
|
||||
}
|
||||
if rm.Attributes.Dst != nil {
|
||||
// A default route has a nil destination to mean anything
|
||||
// so ignore any route for a specific destination.
|
||||
// TODO(bradfitz): better heuristic?
|
||||
// empirically this seems like enough.
|
||||
continue
|
||||
}
|
||||
// TODO(bradfitz): care about address family, if
|
||||
// callers ever start caring about v4-vs-v6 default
|
||||
// route differences.
|
||||
idx := int(rm.Attributes.OutIface)
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
if iface, err := net.InterfaceByIndex(idx); err == nil {
|
||||
d.InterfaceName = iface.Name
|
||||
d.InterfaceIndex = idx
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
return d, errNoDefaultRoute
|
||||
}
|
||||
|
||||
var zeroRouteBytes = []byte("00000000")
|
||||
var procNetRoutePath = "/proc/net/route"
|
||||
|
||||
// maxProcNetRouteRead is the max number of lines to read from
|
||||
// /proc/net/route looking for a default route.
|
||||
const maxProcNetRouteRead = 1000
|
||||
|
||||
var errNoDefaultRoute = errors.New("no default route found")
|
||||
|
||||
func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) {
|
||||
f, err := os.Open(procNetRoutePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
br := bufio.NewReaderSize(f, bufsize)
|
||||
lineNum := 0
|
||||
for {
|
||||
lineNum++
|
||||
line, err := br.ReadSlice('\n')
|
||||
if err == io.EOF || lineNum > maxProcNetRouteRead {
|
||||
return "", errNoDefaultRoute
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !bytes.Contains(line, zeroRouteBytes) {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(string(line))
|
||||
ifc := fields[0]
|
||||
ip := fields[1]
|
||||
netmask := fields[7]
|
||||
|
||||
if strings.HasPrefix(ifc, "tailscale") ||
|
||||
strings.HasPrefix(ifc, "wg") {
|
||||
continue
|
||||
}
|
||||
if ip == "00000000" && netmask == "00000000" {
|
||||
// default route
|
||||
return ifc, nil // interface name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns string interface name and an error.
|
||||
// io.EOF: full route table processed, no default route found.
|
||||
// other io error: something went wrong reading the route file.
|
||||
func defaultRouteInterfaceProcNet() (string, error) {
|
||||
rc, err := defaultRouteInterfaceProcNetInternal(128)
|
||||
if rc == "" && (errors.Is(err, io.EOF) || err == nil) {
|
||||
// https://github.com/google/gvisor/issues/5732
|
||||
// On a regular Linux kernel you can read the first 128 bytes of /proc/net/route,
|
||||
// then come back later to read the next 128 bytes and so on.
|
||||
//
|
||||
// In Google Cloud Run, where /proc/net/route comes from gVisor, you have to
|
||||
// read it all at once. If you read only the first few bytes then the second
|
||||
// read returns 0 bytes no matter how much originally appeared to be in the file.
|
||||
//
|
||||
// At the time of this writing (Mar 2021) Google Cloud Run has eth0 and eth1
|
||||
// with a 384 byte /proc/net/route. We allocate a large buffer to ensure we'll
|
||||
// read it all in one call.
|
||||
return defaultRouteInterfaceProcNetInternal(4096)
|
||||
}
|
||||
return rc, err
|
||||
}
|
116
net/netmon/interfaces_linux_test.go
Normal file
116
net/netmon/interfaces_linux_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
// test the specific /proc/net/route path as found on Google Cloud Run instances
|
||||
func TestGoogleCloudRunDefaultRouteInterface(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
tstest.Replace(t, &procNetRoutePath, filepath.Join(dir, "CloudRun"))
|
||||
buf := []byte("Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n" +
|
||||
"eth0\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n" +
|
||||
"eth1\t00000000\t00000000\t0001\t0\t0\t0\t00000000\t0\t0\t0\n")
|
||||
err := os.WriteFile(procNetRoutePath, buf, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got != "eth1" {
|
||||
t.Fatalf("got %s, want eth1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// we read chunks of /proc/net/route at a time, test that files longer than the chunk
|
||||
// size can be handled.
|
||||
func TestExtremelyLongProcNetRoute(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
tstest.Replace(t, &procNetRoutePath, filepath.Join(dir, "VeryLong"))
|
||||
f, err := os.Create(procNetRoutePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = f.Write([]byte("Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for n := 0; n <= 900; n++ {
|
||||
line := fmt.Sprintf("eth%d\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n", n)
|
||||
_, err := f.Write([]byte(line))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
_, err = f.Write([]byte("tokenring1\t00000000\t00000000\t0001\t0\t0\t0\t00000000\t0\t0\t0\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got != "tokenring1" {
|
||||
t.Fatalf("got %q, want tokenring1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// test the specific /proc/net/route path as found on AWS App Runner instances
|
||||
func TestAwsAppRunnerDefaultRouteInterface(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
tstest.Replace(t, &procNetRoutePath, filepath.Join(dir, "CloudRun"))
|
||||
buf := []byte("Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n" +
|
||||
"eth0\t00000000\tF9AFFEA9\t0003\t0\t0\t0\t00000000\t0\t0\t0\n" +
|
||||
"*\tFEA9FEA9\t00000000\t0005\t0\t0\t0\tFFFFFFFF\t0\t0\t0\n" +
|
||||
"ecs-eth0\t02AAFEA9\t01ACFEA9\t0007\t0\t0\t0\tFFFFFFFF\t0\t0\t0\n" +
|
||||
"ecs-eth0\t00ACFEA9\t00000000\t0001\t0\t0\t0\t00FFFFFF\t0\t0\t0\n" +
|
||||
"eth0\t00AFFEA9\t00000000\t0001\t0\t0\t0\t00FFFFFF\t0\t0\t0\n")
|
||||
err := os.WriteFile(procNetRoutePath, buf, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got != "eth0" {
|
||||
t.Fatalf("got %s, want eth0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDefaultRouteInterface(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for range b.N {
|
||||
if _, err := DefaultRouteInterface(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteLinuxNetlink(t *testing.T) {
|
||||
d, err := defaultRouteFromNetlink()
|
||||
if errors.Is(err, fs.ErrPermission) {
|
||||
t.Skip(err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %+v", d)
|
||||
}
|
403
net/netmon/interfaces_test.go
Normal file
403
net/netmon/interfaces_test.go
Normal file
@@ -0,0 +1,403 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func TestGetState(t *testing.T) {
|
||||
st, err := GetState()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
j, err := json.MarshalIndent(st, "", "\t")
|
||||
if err != nil {
|
||||
t.Errorf("JSON: %v", err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
t.Logf("As string: %s", st)
|
||||
}
|
||||
|
||||
func TestLikelyHomeRouterIP(t *testing.T) {
|
||||
ipnet := func(s string) net.Addr {
|
||||
ip, ipnet, err := net.ParseCIDR(s)
|
||||
ipnet.IP = ip
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return ipnet
|
||||
}
|
||||
|
||||
mockInterfaces := []Interface{
|
||||
// Interface that's not running
|
||||
{
|
||||
Interface: &net.Interface{
|
||||
Index: 1,
|
||||
MTU: 1500,
|
||||
Name: "down0",
|
||||
Flags: net.FlagBroadcast | net.FlagMulticast,
|
||||
},
|
||||
AltAddrs: []net.Addr{
|
||||
ipnet("10.0.0.100/8"),
|
||||
},
|
||||
},
|
||||
|
||||
// Interface that's up, but only has an IPv6 address
|
||||
{
|
||||
Interface: &net.Interface{
|
||||
Index: 2,
|
||||
MTU: 1500,
|
||||
Name: "ipsixonly0",
|
||||
Flags: net.FlagUp | net.FlagBroadcast | net.FlagMulticast | net.FlagRunning,
|
||||
},
|
||||
AltAddrs: []net.Addr{
|
||||
ipnet("76f9:2e7d:55dd:48e1:48d0:763a:b591:b1bc/64"),
|
||||
},
|
||||
},
|
||||
|
||||
// Fake interface with a gateway to the internet
|
||||
{
|
||||
Interface: &net.Interface{
|
||||
Index: 3,
|
||||
MTU: 1500,
|
||||
Name: "fake0",
|
||||
Flags: net.FlagUp | net.FlagBroadcast | net.FlagMulticast | net.FlagRunning,
|
||||
},
|
||||
AltAddrs: []net.Addr{
|
||||
ipnet("23a1:99c9:3a88:1d29:74d4:957b:2133:3f4e/64"),
|
||||
ipnet("192.168.7.100/24"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Mock out the responses from netInterfaces()
|
||||
tstest.Replace(t, &altNetInterfaces, func() ([]Interface, error) {
|
||||
return mockInterfaces, nil
|
||||
})
|
||||
|
||||
// Mock out the likelyHomeRouterIP to return a known gateway.
|
||||
tstest.Replace(t, &likelyHomeRouterIP, func() (netip.Addr, netip.Addr, bool) {
|
||||
return netip.MustParseAddr("192.168.7.1"), netip.Addr{}, true
|
||||
})
|
||||
|
||||
gw, my, ok := LikelyHomeRouterIP()
|
||||
if !ok {
|
||||
t.Fatal("expected success")
|
||||
}
|
||||
t.Logf("myIP = %v; gw = %v", my, gw)
|
||||
|
||||
if want := netip.MustParseAddr("192.168.7.1"); gw != want {
|
||||
t.Errorf("got gateway %v; want %v", gw, want)
|
||||
}
|
||||
if want := netip.MustParseAddr("192.168.7.100"); my != want {
|
||||
t.Errorf("got self IP %v; want %v", my, want)
|
||||
}
|
||||
|
||||
// Verify that no IP is returned if there are no IPv4 addresses on
|
||||
// local interfaces.
|
||||
t.Run("NoIPv4Addrs", func(t *testing.T) {
|
||||
tstest.Replace(t, &mockInterfaces, []Interface{
|
||||
// Interface that's up, but only has an IPv6 address
|
||||
{
|
||||
Interface: &net.Interface{
|
||||
Index: 2,
|
||||
MTU: 1500,
|
||||
Name: "en0",
|
||||
Flags: net.FlagUp | net.FlagBroadcast | net.FlagMulticast | net.FlagRunning,
|
||||
},
|
||||
AltAddrs: []net.Addr{
|
||||
ipnet("76f9:2e7d:55dd:48e1:48d0:763a:b591:b1bc/64"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
_, _, ok := LikelyHomeRouterIP()
|
||||
if ok {
|
||||
t.Fatal("expected no success")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/tailscale/tailscale/issues/10466
|
||||
func TestLikelyHomeRouterIP_Prefix(t *testing.T) {
|
||||
ipnet := func(s string) net.Addr {
|
||||
ip, ipnet, err := net.ParseCIDR(s)
|
||||
ipnet.IP = ip
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return ipnet
|
||||
}
|
||||
|
||||
mockInterfaces := []Interface{
|
||||
// Valid and running interface that doesn't have a route to the
|
||||
// internet, and comes before the interface that does.
|
||||
{
|
||||
Interface: &net.Interface{
|
||||
Index: 1,
|
||||
MTU: 1500,
|
||||
Name: "docker0",
|
||||
Flags: net.FlagUp |
|
||||
net.FlagBroadcast |
|
||||
net.FlagMulticast |
|
||||
net.FlagRunning,
|
||||
},
|
||||
AltAddrs: []net.Addr{
|
||||
ipnet("172.17.0.0/16"),
|
||||
},
|
||||
},
|
||||
|
||||
// Fake interface with a gateway to the internet.
|
||||
{
|
||||
Interface: &net.Interface{
|
||||
Index: 2,
|
||||
MTU: 1500,
|
||||
Name: "fake0",
|
||||
Flags: net.FlagUp |
|
||||
net.FlagBroadcast |
|
||||
net.FlagMulticast |
|
||||
net.FlagRunning,
|
||||
},
|
||||
AltAddrs: []net.Addr{
|
||||
ipnet("192.168.7.100/24"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Mock out the responses from netInterfaces()
|
||||
tstest.Replace(t, &altNetInterfaces, func() ([]Interface, error) {
|
||||
return mockInterfaces, nil
|
||||
})
|
||||
|
||||
// Mock out the likelyHomeRouterIP to return a known gateway.
|
||||
tstest.Replace(t, &likelyHomeRouterIP, func() (netip.Addr, netip.Addr, bool) {
|
||||
return netip.MustParseAddr("192.168.7.1"), netip.Addr{}, true
|
||||
})
|
||||
|
||||
gw, my, ok := LikelyHomeRouterIP()
|
||||
if !ok {
|
||||
t.Fatal("expected success")
|
||||
}
|
||||
t.Logf("myIP = %v; gw = %v", my, gw)
|
||||
|
||||
if want := netip.MustParseAddr("192.168.7.1"); gw != want {
|
||||
t.Errorf("got gateway %v; want %v", gw, want)
|
||||
}
|
||||
if want := netip.MustParseAddr("192.168.7.100"); my != want {
|
||||
t.Errorf("got self IP %v; want %v", my, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLikelyHomeRouterIP_NoMocks(t *testing.T) {
|
||||
// Verify that this works properly when called on a real live system,
|
||||
// without any mocks.
|
||||
gw, my, ok := LikelyHomeRouterIP()
|
||||
t.Logf("LikelyHomeRouterIP: gw=%v my=%v ok=%v", gw, my, ok)
|
||||
}
|
||||
|
||||
func TestIsUsableV6(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ip string
|
||||
want bool
|
||||
}{
|
||||
{"first ULA", "fc00::1", true},
|
||||
{"Tailscale", "fd7a:115c:a1e0::1", false},
|
||||
{"Cloud Run", "fddf:3978:feb1:d745::1", true},
|
||||
{"zeros", "0::0", false},
|
||||
{"Link Local", "fe80::1", false},
|
||||
{"Global", "2602::1", true},
|
||||
{"IPv4 public", "192.0.2.1", false},
|
||||
{"IPv4 private", "192.168.1.1", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if got := isUsableV6(netip.MustParseAddr(test.ip)); got != test.want {
|
||||
t.Errorf("isUsableV6(%s) = %v, want %v", test.name, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
s *State
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "typical_linux",
|
||||
s: &State{
|
||||
DefaultRouteInterface: "eth0",
|
||||
Interface: map[string]Interface{
|
||||
"eth0": {
|
||||
Interface: &net.Interface{
|
||||
Flags: net.FlagUp,
|
||||
},
|
||||
},
|
||||
"wlan0": {
|
||||
Interface: &net.Interface{},
|
||||
},
|
||||
"lo": {
|
||||
Interface: &net.Interface{},
|
||||
},
|
||||
},
|
||||
InterfaceIPs: map[string][]netip.Prefix{
|
||||
"eth0": {
|
||||
netip.MustParsePrefix("10.0.0.2/8"),
|
||||
},
|
||||
"lo": {},
|
||||
},
|
||||
HaveV4: true,
|
||||
},
|
||||
want: `interfaces.State{defaultRoute=eth0 ifs={eth0:[10.0.0.2/8]} v4=true v6=false}`,
|
||||
},
|
||||
{
|
||||
name: "default_desc",
|
||||
s: &State{
|
||||
DefaultRouteInterface: "foo",
|
||||
Interface: map[string]Interface{
|
||||
"foo": {
|
||||
Desc: "a foo thing",
|
||||
Interface: &net.Interface{
|
||||
Flags: net.FlagUp,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `interfaces.State{defaultRoute=foo (a foo thing) ifs={foo:[]} v4=false v6=false}`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.s.String()
|
||||
if got != tt.want {
|
||||
t.Errorf("wrong\n got: %s\nwant: %s\n", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// tests (*State).Equal
|
||||
func TestEqual(t *testing.T) {
|
||||
pfxs := func(addrs ...string) (ret []netip.Prefix) {
|
||||
for _, addr := range addrs {
|
||||
ret = append(ret, netip.MustParsePrefix(addr))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
s1, s2 *State
|
||||
want bool // implies !wantMajor
|
||||
}{
|
||||
{
|
||||
name: "eq_nil",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "nil_mix",
|
||||
s2: new(State),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "eq",
|
||||
s1: &State{
|
||||
DefaultRouteInterface: "foo",
|
||||
InterfaceIPs: map[string][]netip.Prefix{
|
||||
"foo": {netip.MustParsePrefix("10.0.1.2/16")},
|
||||
},
|
||||
},
|
||||
s2: &State{
|
||||
DefaultRouteInterface: "foo",
|
||||
InterfaceIPs: map[string][]netip.Prefix{
|
||||
"foo": {netip.MustParsePrefix("10.0.1.2/16")},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "default-route-changed",
|
||||
s1: &State{
|
||||
DefaultRouteInterface: "foo",
|
||||
InterfaceIPs: map[string][]netip.Prefix{
|
||||
"foo": {netip.MustParsePrefix("10.0.1.2/16")},
|
||||
},
|
||||
},
|
||||
s2: &State{
|
||||
DefaultRouteInterface: "bar",
|
||||
InterfaceIPs: map[string][]netip.Prefix{
|
||||
"foo": {netip.MustParsePrefix("10.0.1.2/16")},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "some-interface-ips-changed",
|
||||
s1: &State{
|
||||
DefaultRouteInterface: "foo",
|
||||
InterfaceIPs: map[string][]netip.Prefix{
|
||||
"foo": {netip.MustParsePrefix("10.0.1.2/16")},
|
||||
},
|
||||
},
|
||||
s2: &State{
|
||||
DefaultRouteInterface: "foo",
|
||||
InterfaceIPs: map[string][]netip.Prefix{
|
||||
"foo": {netip.MustParsePrefix("10.0.1.3/16")},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "altaddrs-changed",
|
||||
s1: &State{
|
||||
Interface: map[string]Interface{
|
||||
"foo": {AltAddrs: []net.Addr{&net.TCPAddr{IP: net.ParseIP("1.2.3.4")}}},
|
||||
},
|
||||
},
|
||||
s2: &State{
|
||||
Interface: map[string]Interface{
|
||||
"foo": {AltAddrs: []net.Addr{&net.TCPAddr{IP: net.ParseIP("5.6.7.8")}}},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
|
||||
// See tailscale/corp#19124
|
||||
{
|
||||
name: "interface-removed",
|
||||
s1: &State{
|
||||
InterfaceIPs: map[string][]netip.Prefix{
|
||||
"rmnet16": pfxs("2607:1111:2222:3333:4444:5555:6666:7777/64"),
|
||||
"rmnet17": pfxs("2607:9999:8888:7777:666:5555:4444:3333/64"),
|
||||
"tun0": pfxs("100.64.1.2/32", "fd7a:115c:a1e0::1/128"),
|
||||
"v4-rmnet16": pfxs("192.0.0.4/32"),
|
||||
"wlan0": pfxs("10.0.0.111/24"), // removed below
|
||||
},
|
||||
},
|
||||
s2: &State{
|
||||
InterfaceIPs: map[string][]netip.Prefix{
|
||||
"rmnet16": pfxs("2607:1111:2222:3333:4444:5555:6666:7777/64"),
|
||||
"rmnet17": pfxs("2607:9999:8888:7777:666:5555:4444:3333/64"),
|
||||
"tun0": pfxs("100.64.1.2/32", "fd7a:115c:a1e0::1/128"),
|
||||
"v4-rmnet16": pfxs("192.0.0.4/32"),
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.s2.Equal(tt.s1); got != tt.want {
|
||||
t.Errorf("Equal = %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
278
net/netmon/interfaces_windows.go
Normal file
278
net/netmon/interfaces_windows.go
Normal file
@@ -0,0 +1,278 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/tsconst"
|
||||
)
|
||||
|
||||
const (
|
||||
fallbackInterfaceMetric = uint32(0) // Used if we cannot get the actual interface metric
|
||||
)
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPWindows
|
||||
getPAC = getPACWindows
|
||||
}
|
||||
|
||||
func likelyHomeRouterIPWindows() (ret netip.Addr, _ netip.Addr, ok bool) {
|
||||
rs, err := winipcfg.GetIPForwardTable2(windows.AF_INET)
|
||||
if err != nil {
|
||||
log.Printf("routerIP/GetIPForwardTable2 error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var ifaceMetricCache map[winipcfg.LUID]uint32
|
||||
|
||||
getIfaceMetric := func(luid winipcfg.LUID) (metric uint32) {
|
||||
if ifaceMetricCache == nil {
|
||||
ifaceMetricCache = make(map[winipcfg.LUID]uint32)
|
||||
} else if m, ok := ifaceMetricCache[luid]; ok {
|
||||
return m
|
||||
}
|
||||
|
||||
if iface, err := luid.IPInterface(windows.AF_INET); err == nil {
|
||||
metric = iface.Metric
|
||||
} else {
|
||||
log.Printf("routerIP/luid.IPInterface error: %v", err)
|
||||
metric = fallbackInterfaceMetric
|
||||
}
|
||||
|
||||
ifaceMetricCache[luid] = metric
|
||||
return
|
||||
}
|
||||
|
||||
v4unspec := netip.IPv4Unspecified()
|
||||
var best *winipcfg.MibIPforwardRow2 // best (lowest metric) found so far, or nil
|
||||
|
||||
for i := range rs {
|
||||
r := &rs[i]
|
||||
if r.Loopback || r.DestinationPrefix.PrefixLength != 0 || r.DestinationPrefix.Prefix().Addr().Unmap() != v4unspec {
|
||||
// Not a default route, so skip
|
||||
continue
|
||||
}
|
||||
|
||||
ip := r.NextHop.Addr().Unmap()
|
||||
if !ip.IsValid() {
|
||||
// Not a valid gateway, so skip (won't happen though)
|
||||
continue
|
||||
}
|
||||
|
||||
if best == nil {
|
||||
best = r
|
||||
ret = ip
|
||||
continue
|
||||
}
|
||||
|
||||
// We can get here only if there are multiple default gateways defined (rare case),
|
||||
// in which case we need to calculate the effective metric.
|
||||
// Effective metric is sum of interface metric and route metric offset
|
||||
if ifaceMetricCache == nil {
|
||||
// If we're here it means that previous route still isn't updated, so update it
|
||||
best.Metric += getIfaceMetric(best.InterfaceLUID)
|
||||
}
|
||||
r.Metric += getIfaceMetric(r.InterfaceLUID)
|
||||
|
||||
if best.Metric > r.Metric || best.Metric == r.Metric && ret.Compare(ip) > 0 {
|
||||
// Pick the route with lower metric, or lower IP if metrics are equal
|
||||
best = r
|
||||
ret = ip
|
||||
}
|
||||
}
|
||||
|
||||
if ret.IsValid() && !ret.IsPrivate() {
|
||||
// Default route has a non-private gateway
|
||||
return netip.Addr{}, netip.Addr{}, false
|
||||
}
|
||||
|
||||
return ret, netip.Addr{}, ret.IsValid()
|
||||
}
|
||||
|
||||
// NonTailscaleMTUs returns a map of interface LUID to interface MTU,
|
||||
// for all interfaces except Tailscale tunnels.
|
||||
func NonTailscaleMTUs() (map[winipcfg.LUID]uint32, error) {
|
||||
mtus := map[winipcfg.LUID]uint32{}
|
||||
ifs, err := NonTailscaleInterfaces()
|
||||
for luid, iface := range ifs {
|
||||
mtus[luid] = iface.MTU
|
||||
}
|
||||
return mtus, err
|
||||
}
|
||||
|
||||
func notTailscaleInterface(iface *winipcfg.IPAdapterAddresses) bool {
|
||||
// TODO(bradfitz): do this without the Description method's
|
||||
// utf16-to-string allocation. But at least we only do it for
|
||||
// the virtual interfaces, for which there won't be many.
|
||||
if iface.IfType != winipcfg.IfTypePropVirtual {
|
||||
return true
|
||||
}
|
||||
desc := iface.Description()
|
||||
return !(strings.Contains(desc, tsconst.WintunInterfaceDesc) ||
|
||||
strings.Contains(desc, tsconst.WintunInterfaceDesc0_14))
|
||||
}
|
||||
|
||||
// NonTailscaleInterfaces returns a map of interface LUID to interface
|
||||
// for all interfaces except Tailscale tunnels.
|
||||
func NonTailscaleInterfaces() (map[winipcfg.LUID]*winipcfg.IPAdapterAddresses, error) {
|
||||
return getInterfaces(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeAllInterfaces, notTailscaleInterface)
|
||||
}
|
||||
|
||||
// getInterfaces returns a map of interfaces keyed by their LUID for
|
||||
// all interfaces matching the provided match predicate.
|
||||
//
|
||||
// The family (AF_UNSPEC, AF_INET, or AF_INET6) and flags are passed
|
||||
// to winipcfg.GetAdaptersAddresses.
|
||||
func getInterfaces(family winipcfg.AddressFamily, flags winipcfg.GAAFlags, match func(*winipcfg.IPAdapterAddresses) bool) (map[winipcfg.LUID]*winipcfg.IPAdapterAddresses, error) {
|
||||
ifs, err := winipcfg.GetAdaptersAddresses(family, flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := map[winipcfg.LUID]*winipcfg.IPAdapterAddresses{}
|
||||
for _, iface := range ifs {
|
||||
if match(iface) {
|
||||
ret[iface.LUID] = iface
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// GetWindowsDefault returns the interface that has the non-Tailscale
|
||||
// default route for the given address family.
|
||||
//
|
||||
// It returns (nil, nil) if no interface is found.
|
||||
//
|
||||
// The family must be one of AF_INET or AF_INET6.
|
||||
func GetWindowsDefault(family winipcfg.AddressFamily) (*winipcfg.IPAdapterAddresses, error) {
|
||||
ifs, err := getInterfaces(family, winipcfg.GAAFlagIncludeAllInterfaces, func(iface *winipcfg.IPAdapterAddresses) bool {
|
||||
switch iface.IfType {
|
||||
case winipcfg.IfTypeSoftwareLoopback:
|
||||
return false
|
||||
}
|
||||
switch family {
|
||||
case windows.AF_INET:
|
||||
if iface.Flags&winipcfg.IPAAFlagIpv4Enabled == 0 {
|
||||
return false
|
||||
}
|
||||
case windows.AF_INET6:
|
||||
if iface.Flags&winipcfg.IPAAFlagIpv6Enabled == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return iface.OperStatus == winipcfg.IfOperStatusUp && notTailscaleInterface(iface)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := winipcfg.GetIPForwardTable2(family)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bestMetric := ^uint32(0)
|
||||
var bestIface *winipcfg.IPAdapterAddresses
|
||||
for _, route := range routes {
|
||||
if route.DestinationPrefix.PrefixLength != 0 {
|
||||
// Not a default route.
|
||||
continue
|
||||
}
|
||||
iface := ifs[route.InterfaceLUID]
|
||||
if iface == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Microsoft docs say:
|
||||
//
|
||||
// "The actual route metric used to compute the route
|
||||
// preferences for IPv4 is the summation of the route
|
||||
// metric offset specified in the Metric member of the
|
||||
// MIB_IPFORWARD_ROW2 structure and the interface
|
||||
// metric specified in this member for IPv4"
|
||||
metric := route.Metric
|
||||
switch family {
|
||||
case windows.AF_INET:
|
||||
metric += iface.Ipv4Metric
|
||||
case windows.AF_INET6:
|
||||
metric += iface.Ipv6Metric
|
||||
}
|
||||
if metric < bestMetric {
|
||||
bestMetric = metric
|
||||
bestIface = iface
|
||||
}
|
||||
}
|
||||
|
||||
return bestIface, nil
|
||||
}
|
||||
|
||||
func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
// We always return the IPv4 default route.
|
||||
// TODO(bradfitz): adjust API if/when anything cares. They could in theory differ, though,
|
||||
// in which case we might send traffic to the wrong interface.
|
||||
iface, err := GetWindowsDefault(windows.AF_INET)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
if iface != nil {
|
||||
d.InterfaceName = iface.FriendlyName()
|
||||
d.InterfaceDesc = iface.Description()
|
||||
d.InterfaceIndex = int(iface.IfIndex)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
var (
|
||||
winHTTP = windows.NewLazySystemDLL("winhttp.dll")
|
||||
detectAutoProxyConfigURL = winHTTP.NewProc("WinHttpDetectAutoProxyConfigUrl")
|
||||
|
||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
globalFree = kernel32.NewProc("GlobalFree")
|
||||
)
|
||||
|
||||
const (
|
||||
winHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001
|
||||
winHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002
|
||||
)
|
||||
|
||||
func getPACWindows() string {
|
||||
var res *uint16
|
||||
r, _, e := detectAutoProxyConfigURL.Call(
|
||||
winHTTP_AUTO_DETECT_TYPE_DHCP|winHTTP_AUTO_DETECT_TYPE_DNS_A,
|
||||
uintptr(unsafe.Pointer(&res)),
|
||||
)
|
||||
if r == 1 {
|
||||
if res == nil {
|
||||
log.Printf("getPACWindows: unexpected success with nil result")
|
||||
return ""
|
||||
}
|
||||
defer globalFree.Call(uintptr(unsafe.Pointer(res)))
|
||||
s := windows.UTF16PtrToString(res)
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return "" // Issue 2357: invalid URL "\n" from winhttp; ignoring
|
||||
}
|
||||
if _, err := url.Parse(s); err != nil {
|
||||
log.Printf("getPACWindows: invalid URL %q from winhttp; ignoring", s)
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
const (
|
||||
ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
|
||||
)
|
||||
if e == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
|
||||
// Common case on networks without advertised PAC.
|
||||
return ""
|
||||
}
|
||||
log.Printf("getPACWindows: %T=%v", e, e) // syscall.Errno=0x....
|
||||
return ""
|
||||
}
|
16
net/netmon/interfaces_windows_test.go
Normal file
16
net/netmon/interfaces_windows_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
import "testing"
|
||||
|
||||
func BenchmarkGetPACWindows(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := range b.N {
|
||||
v := getPACWindows()
|
||||
if i == 0 {
|
||||
b.Logf("Got: %q", v)
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,7 +14,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/set"
|
||||
@@ -162,7 +161,7 @@ func (m *Monitor) InterfaceState() *State {
|
||||
}
|
||||
|
||||
func (m *Monitor) interfaceStateUncached() (*State, error) {
|
||||
return interfaces.GetState()
|
||||
return GetState()
|
||||
}
|
||||
|
||||
// SetTailscaleInterfaceName sets the name of the Tailscale interface. For
|
||||
@@ -189,7 +188,7 @@ func (m *Monitor) GatewayAndSelfIP() (gw, myIP netip.Addr, ok bool) {
|
||||
if m.gwValid {
|
||||
return m.gw, m.gwSelfIP, true
|
||||
}
|
||||
gw, myIP, ok = interfaces.LikelyHomeRouterIP()
|
||||
gw, myIP, ok = LikelyHomeRouterIP()
|
||||
changed := false
|
||||
if ok {
|
||||
changed = m.gw != gw || m.gwSelfIP != myIP
|
||||
@@ -376,7 +375,7 @@ func (m *Monitor) notifyRuleDeleted(rdm ipRuleDeletedMessage) {
|
||||
// isInterestingInterface reports whether the provided interface should be
|
||||
// considered when checking for network state changes.
|
||||
// The ips parameter should be the IPs of the provided interface.
|
||||
func (m *Monitor) isInterestingInterface(i interfaces.Interface, ips []netip.Prefix) bool {
|
||||
func (m *Monitor) isInterestingInterface(i Interface, ips []netip.Prefix) bool {
|
||||
if !m.om.IsInterestingInterface(i.Name) {
|
||||
return false
|
||||
}
|
||||
|
@@ -11,7 +11,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
@@ -116,8 +115,6 @@ func TestMonitorMode(t *testing.T) {
|
||||
|
||||
// tests (*State).IsMajorChangeFrom
|
||||
func TestIsMajorChangeFrom(t *testing.T) {
|
||||
type State = interfaces.State
|
||||
type Interface = interfaces.Interface
|
||||
tests := []struct {
|
||||
name string
|
||||
s1, s2 *State
|
||||
|
@@ -1,8 +1,774 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package interfaces contains helpers for looking up system network interfaces.
|
||||
package netmon
|
||||
|
||||
import "tailscale.com/net/interfaces"
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
type State = interfaces.State // temporary (2024-04-27) alias during multi-step removal of net/interfaces
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
)
|
||||
|
||||
// LoginEndpointForProxyDetermination is the URL used for testing
|
||||
// which HTTP proxy the system should use.
|
||||
var LoginEndpointForProxyDetermination = "https://controlplane.tailscale.com/"
|
||||
|
||||
func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
|
||||
func isLoopback(nif *net.Interface) bool { return nif.Flags&net.FlagLoopback != 0 }
|
||||
|
||||
func isProblematicInterface(nif *net.Interface) bool {
|
||||
name := nif.Name
|
||||
// Don't try to send disco/etc packets over zerotier; they effectively
|
||||
// DoS each other by doing traffic amplification, both of them
|
||||
// preferring/trying to use each other for transport. See:
|
||||
// https://github.com/tailscale/tailscale/issues/1208
|
||||
if strings.HasPrefix(name, "zt") || (runtime.GOOS == "windows" && strings.Contains(name, "ZeroTier")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// LocalAddresses returns the machine's IP addresses, separated by
|
||||
// whether they're loopback addresses. If there are no regular addresses
|
||||
// it will return any IPv4 linklocal or IPv6 unique local addresses because we
|
||||
// know of environments where these are used with NAT to provide connectivity.
|
||||
func LocalAddresses() (regular, loopback []netip.Addr, err error) {
|
||||
// TODO(crawshaw): don't serve interface addresses that we are routing
|
||||
ifaces, err := netInterfaces()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var regular4, regular6, linklocal4, ula6 []netip.Addr
|
||||
for _, iface := range ifaces {
|
||||
stdIf := iface.Interface
|
||||
if !isUp(stdIf) || isProblematicInterface(stdIf) {
|
||||
// Skip down interfaces and ones that are
|
||||
// problematic that we don't want to try to
|
||||
// send Tailscale traffic over.
|
||||
continue
|
||||
}
|
||||
ifcIsLoopback := isLoopback(stdIf)
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, a := range addrs {
|
||||
switch v := a.(type) {
|
||||
case *net.IPNet:
|
||||
ip, ok := netip.AddrFromSlice(v.IP)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ip = ip.Unmap()
|
||||
// TODO(apenwarr): don't special case cgNAT.
|
||||
// In the general wireguard case, it might
|
||||
// very well be something we can route to
|
||||
// directly, because both nodes are
|
||||
// behind the same CGNAT router.
|
||||
if tsaddr.IsTailscaleIP(ip) {
|
||||
continue
|
||||
}
|
||||
if ip.IsLoopback() || ifcIsLoopback {
|
||||
loopback = append(loopback, ip)
|
||||
} else if ip.IsLinkLocalUnicast() {
|
||||
if ip.Is4() {
|
||||
linklocal4 = append(linklocal4, ip)
|
||||
}
|
||||
|
||||
// We know of no cases where the IPv6 fe80:: addresses
|
||||
// are used to provide WAN connectivity. It is also very
|
||||
// common for users to have no IPv6 WAN connectivity,
|
||||
// but their OS supports IPv6 so they have an fe80::
|
||||
// address. We don't want to report all of those
|
||||
// IPv6 LL to Control.
|
||||
} else if ip.Is6() && ip.IsPrivate() {
|
||||
// Google Cloud Run uses NAT with IPv6 Unique
|
||||
// Local Addresses to provide IPv6 connectivity.
|
||||
ula6 = append(ula6, ip)
|
||||
} else {
|
||||
if ip.Is4() {
|
||||
regular4 = append(regular4, ip)
|
||||
} else {
|
||||
regular6 = append(regular6, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(regular4) == 0 && len(regular6) == 0 {
|
||||
// if we have no usable IP addresses then be willing to accept
|
||||
// addresses we otherwise wouldn't, like:
|
||||
// + 169.254.x.x (AWS Lambda and Azure App Services use NAT with these)
|
||||
// + IPv6 ULA (Google Cloud Run uses these with address translation)
|
||||
regular4 = linklocal4
|
||||
regular6 = ula6
|
||||
}
|
||||
regular = append(regular4, regular6...)
|
||||
sortIPs(regular)
|
||||
sortIPs(loopback)
|
||||
return regular, loopback, nil
|
||||
}
|
||||
|
||||
func sortIPs(s []netip.Addr) {
|
||||
sort.Slice(s, func(i, j int) bool { return s[i].Less(s[j]) })
|
||||
}
|
||||
|
||||
// Interface is a wrapper around Go's net.Interface with some extra methods.
|
||||
type Interface struct {
|
||||
*net.Interface
|
||||
AltAddrs []net.Addr // if non-nil, returned by Addrs
|
||||
Desc string // extra description (used on Windows)
|
||||
}
|
||||
|
||||
func (i Interface) IsLoopback() bool { return isLoopback(i.Interface) }
|
||||
func (i Interface) IsUp() bool { return isUp(i.Interface) }
|
||||
func (i Interface) Addrs() ([]net.Addr, error) {
|
||||
if i.AltAddrs != nil {
|
||||
return i.AltAddrs, nil
|
||||
}
|
||||
return i.Interface.Addrs()
|
||||
}
|
||||
|
||||
// ForeachInterfaceAddress is a wrapper for GetList, then
|
||||
// List.ForeachInterfaceAddress.
|
||||
func ForeachInterfaceAddress(fn func(Interface, netip.Prefix)) error {
|
||||
ifaces, err := GetInterfaceList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ifaces.ForeachInterfaceAddress(fn)
|
||||
}
|
||||
|
||||
// ForeachInterfaceAddress calls fn for each interface in ifaces, with
|
||||
// all its addresses. The IPPrefix's IP is the IP address assigned to
|
||||
// the interface, and Bits are the subnet mask.
|
||||
func (ifaces InterfaceList) ForeachInterfaceAddress(fn func(Interface, netip.Prefix)) error {
|
||||
for _, iface := range ifaces {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, a := range addrs {
|
||||
switch v := a.(type) {
|
||||
case *net.IPNet:
|
||||
if pfx, ok := netaddr.FromStdIPNet(v); ok {
|
||||
fn(iface, pfx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForeachInterface is a wrapper for GetList, then
|
||||
// List.ForeachInterface.
|
||||
func ForeachInterface(fn func(Interface, []netip.Prefix)) error {
|
||||
ifaces, err := GetInterfaceList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ifaces.ForeachInterface(fn)
|
||||
}
|
||||
|
||||
// ForeachInterface calls fn for each interface in ifaces, with
|
||||
// all its addresses. The IPPrefix's IP is the IP address assigned to
|
||||
// the interface, and Bits are the subnet mask.
|
||||
func (ifaces InterfaceList) ForeachInterface(fn func(Interface, []netip.Prefix)) error {
|
||||
for _, iface := range ifaces {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var pfxs []netip.Prefix
|
||||
for _, a := range addrs {
|
||||
switch v := a.(type) {
|
||||
case *net.IPNet:
|
||||
if pfx, ok := netaddr.FromStdIPNet(v); ok {
|
||||
pfxs = append(pfxs, pfx)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(pfxs, func(i, j int) bool {
|
||||
return pfxs[i].Addr().Less(pfxs[j].Addr())
|
||||
})
|
||||
fn(iface, pfxs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// State is intended to store the state of the machine's network interfaces,
|
||||
// routing table, and other network configuration.
|
||||
// For now it's pretty basic.
|
||||
type State struct {
|
||||
// InterfaceIPs maps from an interface name to the IP addresses
|
||||
// configured on that interface. Each address is represented as an
|
||||
// IPPrefix, where the IP is the interface IP address and Bits is
|
||||
// the subnet mask.
|
||||
InterfaceIPs map[string][]netip.Prefix
|
||||
Interface map[string]Interface
|
||||
|
||||
// HaveV6 is whether this machine has an IPv6 Global or Unique Local Address
|
||||
// which might provide connectivity on a non-Tailscale interface that's up.
|
||||
HaveV6 bool
|
||||
|
||||
// HaveV4 is whether the machine has some non-localhost,
|
||||
// non-link-local IPv4 address on a non-Tailscale interface that's up.
|
||||
HaveV4 bool
|
||||
|
||||
// IsExpensive is whether the current network interface is
|
||||
// considered "expensive", which currently means LTE/etc
|
||||
// instead of Wifi. This field is not populated by GetState.
|
||||
IsExpensive bool
|
||||
|
||||
// DefaultRouteInterface is the interface name for the
|
||||
// machine's default route.
|
||||
//
|
||||
// It is not yet populated on all OSes.
|
||||
//
|
||||
// When non-empty, its value is the map key into Interface and
|
||||
// InterfaceIPs.
|
||||
DefaultRouteInterface string
|
||||
|
||||
// HTTPProxy is the HTTP proxy to use, if any.
|
||||
HTTPProxy string
|
||||
|
||||
// PAC is the URL to the Proxy Autoconfig URL, if applicable.
|
||||
PAC string
|
||||
}
|
||||
|
||||
func (s *State) String() string {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ", s.DefaultRouteInterface)
|
||||
if s.DefaultRouteInterface != "" {
|
||||
if iface, ok := s.Interface[s.DefaultRouteInterface]; ok && iface.Desc != "" {
|
||||
fmt.Fprintf(&sb, "(%s) ", iface.Desc)
|
||||
}
|
||||
}
|
||||
sb.WriteString("ifs={")
|
||||
var ifs []string
|
||||
for k := range s.Interface {
|
||||
if s.keepInterfaceInStringSummary(k) {
|
||||
ifs = append(ifs, k)
|
||||
}
|
||||
}
|
||||
sort.Slice(ifs, func(i, j int) bool {
|
||||
upi, upj := s.Interface[ifs[i]].IsUp(), s.Interface[ifs[j]].IsUp()
|
||||
if upi != upj {
|
||||
// Up sorts before down.
|
||||
return upi
|
||||
}
|
||||
return ifs[i] < ifs[j]
|
||||
})
|
||||
for i, ifName := range ifs {
|
||||
if i > 0 {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
iface := s.Interface[ifName]
|
||||
if iface.Interface == nil {
|
||||
fmt.Fprintf(&sb, "%s:nil", ifName)
|
||||
continue
|
||||
}
|
||||
if !iface.IsUp() {
|
||||
fmt.Fprintf(&sb, "%s:down", ifName)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&sb, "%s:[", ifName)
|
||||
needSpace := false
|
||||
for _, pfx := range s.InterfaceIPs[ifName] {
|
||||
a := pfx.Addr()
|
||||
if a.IsMulticast() {
|
||||
continue
|
||||
}
|
||||
fam := "4"
|
||||
if a.Is6() {
|
||||
fam = "6"
|
||||
}
|
||||
if needSpace {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
needSpace = true
|
||||
switch {
|
||||
case a.IsLoopback():
|
||||
fmt.Fprintf(&sb, "lo%s", fam)
|
||||
case a.IsLinkLocalUnicast():
|
||||
fmt.Fprintf(&sb, "llu%s", fam)
|
||||
default:
|
||||
fmt.Fprintf(&sb, "%s", pfx)
|
||||
}
|
||||
}
|
||||
sb.WriteString("]")
|
||||
}
|
||||
sb.WriteString("}")
|
||||
|
||||
if s.IsExpensive {
|
||||
sb.WriteString(" expensive")
|
||||
}
|
||||
if s.HTTPProxy != "" {
|
||||
fmt.Fprintf(&sb, " httpproxy=%s", s.HTTPProxy)
|
||||
}
|
||||
if s.PAC != "" {
|
||||
fmt.Fprintf(&sb, " pac=%s", s.PAC)
|
||||
}
|
||||
fmt.Fprintf(&sb, " v4=%v v6=%v}", s.HaveV4, s.HaveV6)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Equal reports whether s and s2 are exactly equal.
|
||||
func (s *State) Equal(s2 *State) bool {
|
||||
if s == nil && s2 == nil {
|
||||
return true
|
||||
}
|
||||
if s == nil || s2 == nil {
|
||||
return false
|
||||
}
|
||||
if s.HaveV6 != s2.HaveV6 ||
|
||||
s.HaveV4 != s2.HaveV4 ||
|
||||
s.IsExpensive != s2.IsExpensive ||
|
||||
s.DefaultRouteInterface != s2.DefaultRouteInterface ||
|
||||
s.HTTPProxy != s2.HTTPProxy ||
|
||||
s.PAC != s2.PAC {
|
||||
return false
|
||||
}
|
||||
// If s2 has more interfaces than s, it's not equal.
|
||||
if len(s.Interface) != len(s2.Interface) || len(s.InterfaceIPs) != len(s2.InterfaceIPs) {
|
||||
return false
|
||||
}
|
||||
// Now that we know that both states have the same number of
|
||||
// interfaces, we can check each interface in s against s2. If it's not
|
||||
// present or not exactly equal, then the states are not equal.
|
||||
for iname, i := range s.Interface {
|
||||
i2, ok := s2.Interface[iname]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !i.Equal(i2) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for iname, vv := range s.InterfaceIPs {
|
||||
if !slices.Equal(vv, s2.InterfaceIPs[iname]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// HasIP reports whether any interface has the provided IP address.
|
||||
func (s *State) HasIP(ip netip.Addr) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
for _, pv := range s.InterfaceIPs {
|
||||
for _, p := range pv {
|
||||
if p.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a Interface) Equal(b Interface) bool {
|
||||
if (a.Interface == nil) != (b.Interface == nil) {
|
||||
return false
|
||||
}
|
||||
if !(a.Desc == b.Desc && netAddrsEqual(a.AltAddrs, b.AltAddrs)) {
|
||||
return false
|
||||
}
|
||||
if a.Interface != nil && !(a.Index == b.Index &&
|
||||
a.MTU == b.MTU &&
|
||||
a.Name == b.Name &&
|
||||
a.Flags == b.Flags &&
|
||||
bytes.Equal([]byte(a.HardwareAddr), []byte(b.HardwareAddr))) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }
|
||||
|
||||
// AnyInterfaceUp reports whether any interface seems like it has Internet access.
|
||||
func (s *State) AnyInterfaceUp() bool {
|
||||
if runtime.GOOS == "js" || runtime.GOOS == "tamago" {
|
||||
return true
|
||||
}
|
||||
return s != nil && (s.HaveV4 || s.HaveV6)
|
||||
}
|
||||
|
||||
func netAddrsEqual(a, b []net.Addr) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, av := range a {
|
||||
if av.Network() != b[i].Network() || av.String() != b[i].String() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func hasTailscaleIP(pfxs []netip.Prefix) bool {
|
||||
for _, pfx := range pfxs {
|
||||
if tsaddr.IsTailscaleIP(pfx.Addr()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isTailscaleInterface(name string, ips []netip.Prefix) bool {
|
||||
if runtime.GOOS == "darwin" && strings.HasPrefix(name, "utun") && hasTailscaleIP(ips) {
|
||||
// On macOS in the sandboxed app (at least as of
|
||||
// 2021-02-25), we often see two utun devices
|
||||
// (e.g. utun4 and utun7) with the same IPv4 and IPv6
|
||||
// addresses. Just remove all utun devices with
|
||||
// Tailscale IPs until we know what's happening with
|
||||
// macOS NetworkExtensions and utun devices.
|
||||
return true
|
||||
}
|
||||
return name == "Tailscale" || // as it is on Windows
|
||||
strings.HasPrefix(name, "tailscale") // TODO: use --tun flag value, etc; see TODO in method doc
|
||||
}
|
||||
|
||||
// getPAC, if non-nil, returns the current PAC file URL.
|
||||
var getPAC func() string
|
||||
|
||||
// GetState returns the state of all the current machine's network interfaces.
|
||||
//
|
||||
// It does not set the returned State.IsExpensive. The caller can populate that.
|
||||
//
|
||||
// Deprecated: use netmon.Monitor.InterfaceState instead.
|
||||
func GetState() (*State, error) {
|
||||
s := &State{
|
||||
InterfaceIPs: make(map[string][]netip.Prefix),
|
||||
Interface: make(map[string]Interface),
|
||||
}
|
||||
if err := ForeachInterface(func(ni Interface, pfxs []netip.Prefix) {
|
||||
ifUp := ni.IsUp()
|
||||
s.Interface[ni.Name] = ni
|
||||
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], pfxs...)
|
||||
if !ifUp || isTailscaleInterface(ni.Name, pfxs) {
|
||||
return
|
||||
}
|
||||
for _, pfx := range pfxs {
|
||||
if pfx.Addr().IsLoopback() {
|
||||
continue
|
||||
}
|
||||
s.HaveV6 = s.HaveV6 || isUsableV6(pfx.Addr())
|
||||
s.HaveV4 = s.HaveV4 || isUsableV4(pfx.Addr())
|
||||
}
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dr, _ := DefaultRoute()
|
||||
s.DefaultRouteInterface = dr.InterfaceName
|
||||
|
||||
// Populate description (for Windows, primarily) if present.
|
||||
if desc := dr.InterfaceDesc; desc != "" {
|
||||
if iface, ok := s.Interface[dr.InterfaceName]; ok {
|
||||
iface.Desc = desc
|
||||
s.Interface[dr.InterfaceName] = iface
|
||||
}
|
||||
}
|
||||
|
||||
if s.AnyInterfaceUp() {
|
||||
req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil {
|
||||
s.HTTPProxy = u.String()
|
||||
}
|
||||
if getPAC != nil {
|
||||
s.PAC = getPAC()
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// HTTPOfListener returns the HTTP address to ln.
|
||||
// If the listener is listening on the unspecified address, it
|
||||
// it tries to find a reasonable interface address on the machine to use.
|
||||
func HTTPOfListener(ln net.Listener) string {
|
||||
ta, ok := ln.Addr().(*net.TCPAddr)
|
||||
if !ok || !ta.IP.IsUnspecified() {
|
||||
return fmt.Sprintf("http://%v/", ln.Addr())
|
||||
}
|
||||
|
||||
var goodIP string
|
||||
var privateIP string
|
||||
ForeachInterfaceAddress(func(i Interface, pfx netip.Prefix) {
|
||||
ip := pfx.Addr()
|
||||
if ip.IsPrivate() {
|
||||
if privateIP == "" {
|
||||
privateIP = ip.String()
|
||||
}
|
||||
return
|
||||
}
|
||||
goodIP = ip.String()
|
||||
})
|
||||
if privateIP != "" {
|
||||
goodIP = privateIP
|
||||
}
|
||||
if goodIP != "" {
|
||||
return fmt.Sprintf("http://%v/", net.JoinHostPort(goodIP, fmt.Sprint(ta.Port)))
|
||||
}
|
||||
return fmt.Sprintf("http://localhost:%v/", fmt.Sprint(ta.Port))
|
||||
|
||||
}
|
||||
|
||||
// likelyHomeRouterIP, if present, is a platform-specific function that is used
|
||||
// to determine the likely home router IP of the current system. The signature
|
||||
// of this function is:
|
||||
//
|
||||
// func() (homeRouter, localAddr netip.Addr, ok bool)
|
||||
//
|
||||
// It should return a homeRouter IP and ok=true, or no homeRouter IP and
|
||||
// ok=false. Optionally, an implementation can return the "self" IP address as
|
||||
// well, which will be used instead of attempting to determine it by reading
|
||||
// the system's interfaces.
|
||||
var likelyHomeRouterIP func() (netip.Addr, netip.Addr, bool)
|
||||
|
||||
// For debugging the new behaviour where likelyHomeRouterIP can return the
|
||||
// "self" IP; should remove after we're confidant this won't cause issues.
|
||||
var disableLikelyHomeRouterIPSelf = envknob.RegisterBool("TS_DEBUG_DISABLE_LIKELY_HOME_ROUTER_IP_SELF")
|
||||
|
||||
// LikelyHomeRouterIP returns the likely IP of the residential router,
|
||||
// which will always be an IPv4 private address, if found.
|
||||
// In addition, it returns the IP address of the current machine on
|
||||
// the LAN using that gateway.
|
||||
// This is used as the destination for UPnP, NAT-PMP, PCP, etc queries.
|
||||
func LikelyHomeRouterIP() (gateway, myIP netip.Addr, ok bool) {
|
||||
// If we don't have a way to get the home router IP, then we can't do
|
||||
// anything; just return.
|
||||
if likelyHomeRouterIP == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the gateway next; if that fails, we can't continue.
|
||||
gateway, myIP, ok = likelyHomeRouterIP()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// If the platform-specific implementation returned a valid myIP, then
|
||||
// we can return it as-is without needing to iterate through all
|
||||
// interface addresses.
|
||||
if disableLikelyHomeRouterIPSelf() {
|
||||
myIP = netip.Addr{}
|
||||
}
|
||||
if myIP.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
// The platform-specific implementation didn't return a valid myIP;
|
||||
// iterate through all interfaces and try to find the correct one.
|
||||
ForeachInterfaceAddress(func(i Interface, pfx netip.Prefix) {
|
||||
if !i.IsUp() {
|
||||
// Skip interfaces that aren't up.
|
||||
return
|
||||
} else if myIP.IsValid() {
|
||||
// We already have a valid self IP; skip this one.
|
||||
return
|
||||
}
|
||||
|
||||
ip := pfx.Addr()
|
||||
if !ip.IsValid() || !ip.Is4() {
|
||||
// Skip IPs that aren't valid or aren't IPv4, since we
|
||||
// always return an IPv4 address.
|
||||
return
|
||||
}
|
||||
|
||||
// If this prefix ("interface") doesn't contain the gateway,
|
||||
// then we skip it; this can happen if we have multiple valid
|
||||
// interfaces and the interface with the route to the internet
|
||||
// is ordered after another valid+running interface.
|
||||
if !pfx.Contains(gateway) {
|
||||
return
|
||||
}
|
||||
|
||||
if gateway.IsPrivate() && ip.IsPrivate() {
|
||||
myIP = ip
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
})
|
||||
return gateway, myIP, myIP.IsValid()
|
||||
}
|
||||
|
||||
// isUsableV4 reports whether ip is a usable IPv4 address which could
|
||||
// conceivably be used to get Internet connectivity. Globally routable and
|
||||
// private IPv4 addresses are always Usable, and link local 169.254.x.x
|
||||
// addresses are in some environments.
|
||||
func isUsableV4(ip netip.Addr) bool {
|
||||
if !ip.Is4() || ip.IsLoopback() {
|
||||
return false
|
||||
}
|
||||
if ip.IsLinkLocalUnicast() {
|
||||
switch hostinfo.GetEnvType() {
|
||||
case hostinfo.AWSLambda:
|
||||
return true
|
||||
case hostinfo.AzureAppService:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isUsableV6 reports whether ip is a usable IPv6 address which could
|
||||
// conceivably be used to get Internet connectivity. Globally routable
|
||||
// IPv6 addresses are always Usable, and Unique Local Addresses
|
||||
// (fc00::/7) are in some environments used with address translation.
|
||||
func isUsableV6(ip netip.Addr) bool {
|
||||
return v6Global1.Contains(ip) ||
|
||||
(ip.Is6() && ip.IsPrivate() && !tsaddr.TailscaleULARange().Contains(ip))
|
||||
}
|
||||
|
||||
var (
|
||||
v6Global1 = netip.MustParsePrefix("2000::/3")
|
||||
)
|
||||
|
||||
// keepInterfaceInStringSummary reports whether the named interface should be included
|
||||
// in the String method's summary string.
|
||||
func (s *State) keepInterfaceInStringSummary(ifName string) bool {
|
||||
iface, ok := s.Interface[ifName]
|
||||
if !ok || iface.Interface == nil {
|
||||
return false
|
||||
}
|
||||
if ifName == s.DefaultRouteInterface {
|
||||
return true
|
||||
}
|
||||
up := iface.IsUp()
|
||||
for _, p := range s.InterfaceIPs[ifName] {
|
||||
a := p.Addr()
|
||||
if a.IsLinkLocalUnicast() || a.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
if up || a.IsGlobalUnicast() || a.IsPrivate() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var altNetInterfaces func() ([]Interface, error)
|
||||
|
||||
// RegisterInterfaceGetter sets the function that's used to query
|
||||
// the system network interfaces.
|
||||
func RegisterInterfaceGetter(getInterfaces func() ([]Interface, error)) {
|
||||
altNetInterfaces = getInterfaces
|
||||
}
|
||||
|
||||
// InterfaceList is a list of interfaces on the machine.
|
||||
type InterfaceList []Interface
|
||||
|
||||
// GetInterfaceList returns the list of interfaces on the machine.
|
||||
func GetInterfaceList() (InterfaceList, error) {
|
||||
return netInterfaces()
|
||||
}
|
||||
|
||||
// netInterfaces is a wrapper around the standard library's net.Interfaces
|
||||
// that returns a []*Interface instead of a []net.Interface.
|
||||
// It exists because Android SDK 30 no longer permits Go's net.Interfaces
|
||||
// to work (Issue 2293); this wrapper lets us the Android app register
|
||||
// an alternate implementation.
|
||||
func netInterfaces() ([]Interface, error) {
|
||||
if altNetInterfaces != nil {
|
||||
return altNetInterfaces()
|
||||
}
|
||||
ifs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := make([]Interface, len(ifs))
|
||||
for i := range ifs {
|
||||
ret[i].Interface = &ifs[i]
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// DefaultRouteDetails are the details about a default route returned
|
||||
// by DefaultRoute.
|
||||
type DefaultRouteDetails struct {
|
||||
// InterfaceName is the interface name. It must always be populated.
|
||||
// It's like "eth0" (Linux), "Ethernet 2" (Windows), "en0" (macOS).
|
||||
InterfaceName string
|
||||
|
||||
// InterfaceDesc is populated on Windows at least. It's a
|
||||
// longer description, like "Red Hat VirtIO Ethernet Adapter".
|
||||
InterfaceDesc string
|
||||
|
||||
// InterfaceIndex is like net.Interface.Index.
|
||||
// Zero means not populated.
|
||||
InterfaceIndex int
|
||||
|
||||
// TODO(bradfitz): break this out into v4-vs-v6 once that need arises.
|
||||
}
|
||||
|
||||
// DefaultRouteInterface is like DefaultRoute but only returns the
|
||||
// interface name.
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
dr, err := DefaultRoute()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dr.InterfaceName, nil
|
||||
}
|
||||
|
||||
// DefaultRoute returns details of the network interface that owns
|
||||
// the default route, not including any tailscale interfaces.
|
||||
func DefaultRoute() (DefaultRouteDetails, error) {
|
||||
return defaultRoute()
|
||||
}
|
||||
|
||||
// HasCGNATInterface reports whether there are any non-Tailscale interfaces that
|
||||
// use a CGNAT IP range.
|
||||
func HasCGNATInterface() (bool, error) {
|
||||
hasCGNATInterface := false
|
||||
cgnatRange := tsaddr.CGNATRange()
|
||||
err := ForeachInterface(func(i Interface, pfxs []netip.Prefix) {
|
||||
if hasCGNATInterface || !i.IsUp() || isTailscaleInterface(i.Name, pfxs) {
|
||||
return
|
||||
}
|
||||
for _, pfx := range pfxs {
|
||||
if cgnatRange.Overlaps(pfx) {
|
||||
hasCGNATInterface = true
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return hasCGNATInterface, nil
|
||||
}
|
||||
|
||||
var interfaceDebugExtras func(ifIndex int) (string, error)
|
||||
|
||||
// InterfaceDebugExtras returns extra debugging information about an interface
|
||||
// if any (an empty string will be returned if there are no additional details).
|
||||
// Formatting is platform-dependent and should not be parsed.
|
||||
func InterfaceDebugExtras(ifIndex int) (string, error) {
|
||||
if interfaceDebugExtras != nil {
|
||||
return interfaceDebugExtras(ifIndex)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user