From b9adbe2002788d537f705f3ee4f5f94b84ba06b7 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sat, 27 Apr 2024 21:18:18 -0700 Subject: [PATCH] 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 --- .github/workflows/test.yml | 2 +- cmd/derper/depaware.txt | 13 +- cmd/tailscale/cli/status.go | 4 +- cmd/tailscale/depaware.txt | 9 +- cmd/tailscaled/depaware.txt | 7 +- doctor/ethtool/ethtool_linux.go | 4 +- ipn/ipnlocal/local.go | 11 +- ipn/ipnlocal/local_test.go | 10 +- ipn/ipnlocal/peerapi.go | 11 +- net/interfaces/interfaces.go | 774 ------------------ net/netcheck/netcheck.go | 2 +- .../defaultroute_bsd.go | 2 +- .../defaultroute_ios.go | 2 +- .../interfaces_android.go | 2 +- net/{interfaces => netmon}/interfaces_bsd.go | 2 +- .../interfaces_darwin.go | 2 +- .../interfaces_darwin_test.go | 2 +- .../interfaces_default_route_test.go | 2 +- .../interfaces_defaultrouteif_todo.go | 2 +- .../interfaces_freebsd.go | 2 +- .../interfaces_linux.go | 2 +- .../interfaces_linux_test.go | 2 +- net/{interfaces => netmon}/interfaces_test.go | 2 +- .../interfaces_windows.go | 2 +- .../interfaces_windows_test.go | 2 +- net/netmon/netmon.go | 7 +- net/netmon/netmon_test.go | 3 - net/netmon/state.go | 770 ++++++++++++++++- net/netns/netns_darwin.go | 5 +- net/netns/netns_darwin_test.go | 4 +- net/netns/netns_linux.go | 3 +- net/netns/netns_windows.go | 5 +- net/portmapper/portmapper.go | 3 +- net/routetable/routetable_bsd.go | 14 +- net/routetable/routetable_bsd_test.go | 4 +- net/routetable/routetable_linux.go | 6 +- net/sockstats/sockstats_tsgo.go | 3 +- .../tailscaled_deps_test_darwin.go | 1 - .../tailscaled_deps_test_freebsd.go | 1 - .../integration/tailscaled_deps_test_linux.go | 1 - .../tailscaled_deps_test_openbsd.go | 1 - .../tailscaled_deps_test_windows.go | 1 - .../integration/vms/derive_bindhost_test.go | 6 +- wgengine/magicsock/magicsock.go | 3 +- wgengine/router/ifconfig_windows.go | 4 +- 45 files changed, 846 insertions(+), 874 deletions(-) delete mode 100644 net/interfaces/interfaces.go rename net/{interfaces => netmon}/defaultroute_bsd.go (96%) rename net/{interfaces => netmon}/defaultroute_ios.go (99%) rename net/{interfaces => netmon}/interfaces_android.go (99%) rename net/{interfaces => netmon}/interfaces_bsd.go (99%) rename net/{interfaces => netmon}/interfaces_darwin.go (99%) rename net/{interfaces => netmon}/interfaces_darwin_test.go (99%) rename net/{interfaces => netmon}/interfaces_default_route_test.go (95%) rename net/{interfaces => netmon}/interfaces_defaultrouteif_todo.go (93%) rename net/{interfaces => netmon}/interfaces_freebsd.go (96%) rename net/{interfaces => netmon}/interfaces_linux.go (99%) rename net/{interfaces => netmon}/interfaces_linux_test.go (99%) rename net/{interfaces => netmon}/interfaces_test.go (99%) rename net/{interfaces => netmon}/interfaces_windows.go (99%) rename net/{interfaces => netmon}/interfaces_windows_test.go (93%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e2525d0a1..c2c75173b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -356,7 +356,7 @@ jobs: # some Android breakages early. # TODO(bradfitz): better; see https://github.com/tailscale/tailscale/issues/4482 - name: build some - run: ./tool/go install ./net/netns ./ipn/ipnlocal ./wgengine/magicsock/ ./wgengine/ ./wgengine/router/ ./wgengine/netstack ./util/dnsname/ ./ipn/ ./net/interfaces ./wgengine/router/ ./tailcfg/ ./types/logger/ ./net/dns ./hostinfo ./version + run: ./tool/go install ./net/netns ./ipn/ipnlocal ./wgengine/magicsock/ ./wgengine/ ./wgengine/router/ ./wgengine/netstack ./util/dnsname/ ./ipn/ ./net/netmon ./wgengine/router/ ./tailcfg/ ./types/logger/ ./net/dns ./hostinfo ./version env: GOOS: android GOARCH: arm64 diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index b0a84d134..22e910179 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -20,7 +20,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa github.com/google/uuid from tailscale.com/util/fastuuid github.com/hdevalence/ed25519consensus from tailscale.com/tka L github.com/josharian/native from github.com/mdlayher/netlink+ - L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+ + L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink L 💣 github.com/mdlayher/netlink from github.com/google/nftables+ L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+ @@ -47,7 +47,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa github.com/x448/float16 from github.com/fxamacker/cbor/v2 💣 go4.org/mem from tailscale.com/client/tailscale+ go4.org/netipx from tailscale.com/net/tsaddr+ - W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+ + W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/netmon+ google.golang.org/protobuf/encoding/protodelim from github.com/prometheus/common/expfmt google.golang.org/protobuf/encoding/prototext from github.com/prometheus/common/expfmt+ google.golang.org/protobuf/encoding/protowire from google.golang.org/protobuf/encoding/protodelim+ @@ -90,17 +90,16 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa tailscale.com/drive from tailscale.com/client/tailscale+ tailscale.com/envknob from tailscale.com/client/tailscale+ tailscale.com/health from tailscale.com/net/tlsdial+ - tailscale.com/hostinfo from tailscale.com/net/interfaces+ + tailscale.com/hostinfo from tailscale.com/net/netmon+ tailscale.com/ipn from tailscale.com/client/tailscale tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+ tailscale.com/metrics from tailscale.com/cmd/derper+ tailscale.com/net/dnscache from tailscale.com/derp/derphttp tailscale.com/net/flowtrack from tailscale.com/net/packet+ - 💣 tailscale.com/net/interfaces from tailscale.com/net/netmon+ tailscale.com/net/ktimeout from tailscale.com/cmd/derper tailscale.com/net/netaddr from tailscale.com/ipn+ tailscale.com/net/netknob from tailscale.com/net/netns - tailscale.com/net/netmon from tailscale.com/derp/derphttp+ + 💣 tailscale.com/net/netmon from tailscale.com/derp/derphttp+ tailscale.com/net/netns from tailscale.com/derp/derphttp tailscale.com/net/netutil from tailscale.com/client/tailscale tailscale.com/net/packet from tailscale.com/wgengine/filter @@ -117,7 +116,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa tailscale.com/syncs from tailscale.com/cmd/derper+ tailscale.com/tailcfg from tailscale.com/client/tailscale+ tailscale.com/tka from tailscale.com/client/tailscale+ - W tailscale.com/tsconst from tailscale.com/net/interfaces + W tailscale.com/tsconst from tailscale.com/net/netmon tailscale.com/tstime from tailscale.com/derp+ tailscale.com/tstime/mono from tailscale.com/tstime/rate tailscale.com/tstime/rate from tailscale.com/derp+ @@ -149,7 +148,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa tailscale.com/util/httpm from tailscale.com/client/tailscale tailscale.com/util/lineread from tailscale.com/hostinfo+ L tailscale.com/util/linuxfw from tailscale.com/net/netns - tailscale.com/util/mak from tailscale.com/net/interfaces+ + tailscale.com/util/mak from tailscale.com/health+ tailscale.com/util/multierr from tailscale.com/health+ tailscale.com/util/nocasemaps from tailscale.com/types/ipproto tailscale.com/util/set from tailscale.com/derp+ diff --git a/cmd/tailscale/cli/status.go b/cmd/tailscale/cli/status.go index 863bed15b..e4dccc247 100644 --- a/cmd/tailscale/cli/status.go +++ b/cmd/tailscale/cli/status.go @@ -23,7 +23,7 @@ "golang.org/x/net/idna" "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" - "tailscale.com/net/interfaces" + "tailscale.com/net/netmon" "tailscale.com/util/dnsname" ) @@ -102,7 +102,7 @@ func runStatus(ctx context.Context, args []string) error { if err != nil { return err } - statusURL := interfaces.HTTPOfListener(ln) + statusURL := netmon.HTTPOfListener(ln) printf("Serving Tailscale status at %v ...\n", statusURL) go func() { <-ctx.Done() diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 268dd1b3e..c7b03ec9a 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -23,7 +23,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep github.com/gorilla/securecookie from github.com/gorilla/csrf github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+ L github.com/josharian/native from github.com/mdlayher/netlink+ - L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+ + L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli 💣 github.com/mattn/go-colorable from tailscale.com/cmd/tailscale/cli @@ -60,7 +60,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep github.com/x448/float16 from github.com/fxamacker/cbor/v2 💣 go4.org/mem from tailscale.com/client/tailscale+ go4.org/netipx from tailscale.com/net/tsaddr+ - W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+ + W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/netmon+ k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli nhooyr.io/websocket from tailscale.com/control/controlhttp+ nhooyr.io/websocket/internal/errd from nhooyr.io/websocket @@ -99,12 +99,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/net/dnscache from tailscale.com/control/controlhttp+ tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp tailscale.com/net/flowtrack from tailscale.com/net/packet+ - 💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+ tailscale.com/net/netaddr from tailscale.com/ipn+ tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli tailscale.com/net/neterror from tailscale.com/net/netcheck+ tailscale.com/net/netknob from tailscale.com/net/netns - tailscale.com/net/netmon from tailscale.com/cmd/tailscale/cli+ + 💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscale/cli+ tailscale.com/net/netns from tailscale.com/derp/derphttp+ tailscale.com/net/netutil from tailscale.com/client/tailscale+ tailscale.com/net/packet from tailscale.com/wgengine/capture+ @@ -123,7 +122,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/tailcfg from tailscale.com/client/tailscale+ tailscale.com/tempfork/spf13/cobra from tailscale.com/cmd/tailscale/cli/ffcomplete+ tailscale.com/tka from tailscale.com/client/tailscale+ - W tailscale.com/tsconst from tailscale.com/net/interfaces + W tailscale.com/tsconst from tailscale.com/net/netmon tailscale.com/tstime from tailscale.com/control/controlhttp+ tailscale.com/tstime/mono from tailscale.com/tstime/rate tailscale.com/tstime/rate from tailscale.com/cmd/tailscale/cli+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index cb5399640..218e48dac 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -119,7 +119,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de github.com/jellydator/ttlcache/v3 from tailscale.com/drive/driveimpl/compositedav L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm L github.com/josharian/native from github.com/mdlayher/netlink+ - L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+ + L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink github.com/klauspost/compress from github.com/klauspost/compress/zstd github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0 @@ -295,13 +295,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/net/dnscache from tailscale.com/control/controlclient+ tailscale.com/net/dnsfallback from tailscale.com/cmd/tailscaled+ tailscale.com/net/flowtrack from tailscale.com/net/packet+ - 💣 tailscale.com/net/interfaces from tailscale.com/doctor/ethtool+ tailscale.com/net/netaddr from tailscale.com/ipn+ tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock+ tailscale.com/net/neterror from tailscale.com/net/dns/resolver+ tailscale.com/net/netkernelconf from tailscale.com/ipn/ipnlocal tailscale.com/net/netknob from tailscale.com/logpolicy+ - tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+ + 💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+ tailscale.com/net/netns from tailscale.com/cmd/tailscaled+ W 💣 tailscale.com/net/netstat from tailscale.com/portlist tailscale.com/net/netutil from tailscale.com/client/tailscale+ @@ -333,7 +332,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tka from tailscale.com/client/tailscale+ - W tailscale.com/tsconst from tailscale.com/net/interfaces + W tailscale.com/tsconst from tailscale.com/net/netmon tailscale.com/tsd from tailscale.com/cmd/tailscaled+ tailscale.com/tstime from tailscale.com/control/controlclient+ tailscale.com/tstime/mono from tailscale.com/net/tstun+ diff --git a/doctor/ethtool/ethtool_linux.go b/doctor/ethtool/ethtool_linux.go index 07a7dd9cc..b8cc08002 100644 --- a/doctor/ethtool/ethtool_linux.go +++ b/doctor/ethtool/ethtool_linux.go @@ -8,7 +8,7 @@ "sort" "github.com/safchain/ethtool" - "tailscale.com/net/interfaces" + "tailscale.com/net/netmon" "tailscale.com/types/logger" "tailscale.com/util/set" ) @@ -21,7 +21,7 @@ func ethtoolImpl(logf logger.Logf) error { } defer et.Close() - interfaces.ForeachInterface(func(iface interfaces.Interface, _ []netip.Prefix) { + netmon.ForeachInterface(func(iface netmon.Interface, _ []netip.Prefix) { ilogf := logger.WithPrefix(logf, iface.Name+": ") features, err := et.Features(iface.Name) if err == nil { diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index e4ed287fc..8fa684a56 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -59,7 +59,6 @@ "tailscale.com/net/dns" "tailscale.com/net/dnscache" "tailscale.com/net/dnsfallback" - "tailscale.com/net/interfaces" "tailscale.com/net/netcheck" "tailscale.com/net/netkernelconf" "tailscale.com/net/netmon" @@ -2029,18 +2028,18 @@ func (b *LocalBackend) setFilter(f *filter.Filter) { // Given that "internal" routes don't leave the device, we choose to // trust them more, allowing access to them when an Exit Node is enabled. func internalAndExternalInterfaces() (internal, external []netip.Prefix, err error) { - il, err := interfaces.GetList() + il, err := netmon.GetInterfaceList() if err != nil { return nil, nil, err } return internalAndExternalInterfacesFrom(il, runtime.GOOS) } -func internalAndExternalInterfacesFrom(il interfaces.List, goos string) (internal, external []netip.Prefix, err error) { +func internalAndExternalInterfacesFrom(il netmon.InterfaceList, goos string) (internal, external []netip.Prefix, err error) { // We use an IPSetBuilder here to canonicalize the prefixes // and to remove any duplicate entries. var internalBuilder, externalBuilder netipx.IPSetBuilder - if err := il.ForeachInterfaceAddress(func(iface interfaces.Interface, pfx netip.Prefix) { + if err := il.ForeachInterfaceAddress(func(iface netmon.Interface, pfx netip.Prefix) { if tsaddr.IsTailscaleIP(pfx.Addr()) { return } @@ -2084,7 +2083,7 @@ func internalAndExternalInterfacesFrom(il interfaces.List, goos string) (interna func interfaceRoutes() (ips *netipx.IPSet, hostIPs []netip.Addr, err error) { var b netipx.IPSetBuilder - if err := interfaces.ForeachInterfaceAddress(func(_ interfaces.Interface, pfx netip.Prefix) { + if err := netmon.ForeachInterfaceAddress(func(_ netmon.Interface, pfx netip.Prefix) { if tsaddr.IsTailscaleIP(pfx.Addr()) { return } @@ -3647,7 +3646,7 @@ func shouldUseOneCGNATRoute(logf logger.Logf, controlKnobs *controlknobs.Knobs, // use fine-grained routes if another interfaces is also using the CGNAT // IP range. if versionOS == "macOS" { - hasCGNATInterface, err := interfaces.HasCGNATInterface() + hasCGNATInterface, err := netmon.HasCGNATInterface() if err != nil { logf("shouldUseOneCGNATRoute: Could not determine if any interfaces use CGNAT: %v", err) return false diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 741eeb6ac..4a966b79f 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -32,8 +32,8 @@ "tailscale.com/drive/driveimpl" "tailscale.com/ipn" "tailscale.com/ipn/store/mem" - "tailscale.com/net/interfaces" "tailscale.com/net/netcheck" + "tailscale.com/net/netmon" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/tsd" @@ -603,7 +603,7 @@ func TestFileTargets(t *testing.T) { func TestInternalAndExternalInterfaces(t *testing.T) { type interfacePrefix struct { - i interfaces.Interface + i netmon.Interface pfx netip.Prefix } @@ -613,7 +613,7 @@ type interfacePrefix struct { } return pfxs } - iList := func(ips ...interfacePrefix) (il interfaces.List) { + iList := func(ips ...interfacePrefix) (il netmon.InterfaceList) { for _, ip := range ips { il = append(il, ip.i) } @@ -621,7 +621,7 @@ type interfacePrefix struct { } newInterface := func(name, pfx string, wsl2, loopback bool) interfacePrefix { ippfx := netip.MustParsePrefix(pfx) - ip := interfaces.Interface{ + ip := netmon.Interface{ Interface: &net.Interface{}, AltAddrs: []net.Addr{ netipx.PrefixIPNet(ippfx), @@ -645,7 +645,7 @@ type interfacePrefix struct { tests := []struct { name string goos string - il interfaces.List + il netmon.InterfaceList wantInt []netip.Prefix wantExt []netip.Prefix }{ diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 9a2657034..e4d0d8ac6 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -34,7 +34,6 @@ "tailscale.com/health" "tailscale.com/hostinfo" "tailscale.com/ipn" - "tailscale.com/net/interfaces" "tailscale.com/net/netaddr" "tailscale.com/net/netmon" "tailscale.com/net/netutil" @@ -445,19 +444,19 @@ func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Re w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintln(w, "

Interfaces

") - if dr, err := interfaces.DefaultRoute(); err == nil { + if dr, err := netmon.DefaultRoute(); err == nil { fmt.Fprintf(w, "

Default route is %q(%d)

\n", html.EscapeString(dr.InterfaceName), dr.InterfaceIndex) } else { fmt.Fprintf(w, "

Could not get the default route: %s

\n", html.EscapeString(err.Error())) } - if hasCGNATInterface, err := interfaces.HasCGNATInterface(); hasCGNATInterface { + if hasCGNATInterface, err := netmon.HasCGNATInterface(); hasCGNATInterface { fmt.Fprintln(w, "

There is another interface using the CGNAT range.

") } else if err != nil { fmt.Fprintf(w, "

Could not check for CGNAT interfaces: %s

\n", html.EscapeString(err.Error())) } - i, err := interfaces.GetList() + i, err := netmon.GetInterfaceList() if err != nil { fmt.Fprintf(w, "Could not get interfaces: %s\n", html.EscapeString(err.Error())) return @@ -469,12 +468,12 @@ func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Re fmt.Fprintf(w, "%v ", v) } fmt.Fprint(w, "\n") - i.ForeachInterface(func(iface interfaces.Interface, ipps []netip.Prefix) { + i.ForeachInterface(func(iface netmon.Interface, ipps []netip.Prefix) { fmt.Fprint(w, "") for _, v := range []any{iface.Index, iface.Name, iface.MTU, iface.Flags, ipps} { fmt.Fprintf(w, "%s ", html.EscapeString(fmt.Sprintf("%v", v))) } - if extras, err := interfaces.InterfaceDebugExtras(iface.Index); err == nil && extras != "" { + if extras, err := netmon.InterfaceDebugExtras(iface.Index); err == nil && extras != "" { fmt.Fprintf(w, "%s ", html.EscapeString(extras)) } else if err != nil { fmt.Fprintf(w, "%s ", html.EscapeString(err.Error())) diff --git a/net/interfaces/interfaces.go b/net/interfaces/interfaces.go deleted file mode 100644 index 0682374a0..000000000 --- a/net/interfaces/interfaces.go +++ /dev/null @@ -1,774 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -// Package interfaces contains helpers for looking up system network interfaces. -package interfaces - -import ( - "bytes" - "fmt" - "net" - "net/http" - "net/netip" - "runtime" - "slices" - "sort" - "strings" - - "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 := GetList() - 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 List) 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 := GetList() - 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 List) 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 -} - -// List is a list of interfaces on the machine. -type List []Interface - -// GetList returns the list of interfaces on the machine. -func GetList() (List, 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 -} diff --git a/net/netcheck/netcheck.go b/net/netcheck/netcheck.go index a987df6f3..efcc1a5de 100644 --- a/net/netcheck/netcheck.go +++ b/net/netcheck/netcheck.go @@ -388,7 +388,7 @@ func sortRegions(dm *tailcfg.DERPMap, last *Report) (prev []*tailcfg.DERPRegion) // makeProbePlan generates the probe plan for a DERPMap, given the most // recent report and whether IPv6 is configured on an interface. -func makeProbePlan(dm *tailcfg.DERPMap, ifState *State, last *Report) (plan probePlan) { +func makeProbePlan(dm *tailcfg.DERPMap, ifState *netmon.State, last *Report) (plan probePlan) { if last == nil || len(last.RegionLatency) == 0 { return makeProbePlanInitial(dm, ifState) } diff --git a/net/interfaces/defaultroute_bsd.go b/net/netmon/defaultroute_bsd.go similarity index 96% rename from net/interfaces/defaultroute_bsd.go rename to net/netmon/defaultroute_bsd.go index 6efd53f82..b66a84636 100644 --- a/net/interfaces/defaultroute_bsd.go +++ b/net/netmon/defaultroute_bsd.go @@ -7,7 +7,7 @@ //go:build !ios && (darwin || freebsd) -package interfaces +package netmon import "net" diff --git a/net/interfaces/defaultroute_ios.go b/net/netmon/defaultroute_ios.go similarity index 99% rename from net/interfaces/defaultroute_ios.go rename to net/netmon/defaultroute_ios.go index f8011253b..63d5e49ce 100644 --- a/net/interfaces/defaultroute_ios.go +++ b/net/netmon/defaultroute_ios.go @@ -3,7 +3,7 @@ //go:build ios -package interfaces +package netmon import ( "log" diff --git a/net/interfaces/interfaces_android.go b/net/netmon/interfaces_android.go similarity index 99% rename from net/interfaces/interfaces_android.go rename to net/netmon/interfaces_android.go index 21a205f5a..a96423eb6 100644 --- a/net/interfaces/interfaces_android.go +++ b/net/netmon/interfaces_android.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -package interfaces +package netmon import ( "bytes" diff --git a/net/interfaces/interfaces_bsd.go b/net/netmon/interfaces_bsd.go similarity index 99% rename from net/interfaces/interfaces_bsd.go rename to net/netmon/interfaces_bsd.go index 76d265681..86bc5615d 100644 --- a/net/interfaces/interfaces_bsd.go +++ b/net/netmon/interfaces_bsd.go @@ -6,7 +6,7 @@ //go:build darwin || freebsd -package interfaces +package netmon import ( "errors" diff --git a/net/interfaces/interfaces_darwin.go b/net/netmon/interfaces_darwin.go similarity index 99% rename from net/interfaces/interfaces_darwin.go rename to net/netmon/interfaces_darwin.go index 6c68844fe..b175f980a 100644 --- a/net/interfaces/interfaces_darwin.go +++ b/net/netmon/interfaces_darwin.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -package interfaces +package netmon import ( "fmt" diff --git a/net/interfaces/interfaces_darwin_test.go b/net/netmon/interfaces_darwin_test.go similarity index 99% rename from net/interfaces/interfaces_darwin_test.go rename to net/netmon/interfaces_darwin_test.go index bcc508e18..d34040d60 100644 --- a/net/interfaces/interfaces_darwin_test.go +++ b/net/netmon/interfaces_darwin_test.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -package interfaces +package netmon import ( "errors" diff --git a/net/interfaces/interfaces_default_route_test.go b/net/netmon/interfaces_default_route_test.go similarity index 95% rename from net/interfaces/interfaces_default_route_test.go rename to net/netmon/interfaces_default_route_test.go index 4f81ed167..e231eea9a 100644 --- a/net/interfaces/interfaces_default_route_test.go +++ b/net/netmon/interfaces_default_route_test.go @@ -3,7 +3,7 @@ //go:build linux || (darwin && !ts_macext) -package interfaces +package netmon import ( "testing" diff --git a/net/interfaces/interfaces_defaultrouteif_todo.go b/net/netmon/interfaces_defaultrouteif_todo.go similarity index 93% rename from net/interfaces/interfaces_defaultrouteif_todo.go rename to net/netmon/interfaces_defaultrouteif_todo.go index f8c636e9d..df0820fa9 100644 --- a/net/interfaces/interfaces_defaultrouteif_todo.go +++ b/net/netmon/interfaces_defaultrouteif_todo.go @@ -3,7 +3,7 @@ //go:build !linux && !windows && !darwin && !freebsd && !android -package interfaces +package netmon import "errors" diff --git a/net/interfaces/interfaces_freebsd.go b/net/netmon/interfaces_freebsd.go similarity index 96% rename from net/interfaces/interfaces_freebsd.go rename to net/netmon/interfaces_freebsd.go index 085db0d96..654eb5316 100644 --- a/net/interfaces/interfaces_freebsd.go +++ b/net/netmon/interfaces_freebsd.go @@ -5,7 +5,7 @@ //go:build freebsd -package interfaces +package netmon import ( "syscall" diff --git a/net/interfaces/interfaces_linux.go b/net/netmon/interfaces_linux.go similarity index 99% rename from net/interfaces/interfaces_linux.go rename to net/netmon/interfaces_linux.go index cb164b2f8..ef7dcaaca 100644 --- a/net/interfaces/interfaces_linux.go +++ b/net/netmon/interfaces_linux.go @@ -3,7 +3,7 @@ //go:build !android -package interfaces +package netmon import ( "bufio" diff --git a/net/interfaces/interfaces_linux_test.go b/net/netmon/interfaces_linux_test.go similarity index 99% rename from net/interfaces/interfaces_linux_test.go rename to net/netmon/interfaces_linux_test.go index 98f9853f5..4f740ac28 100644 --- a/net/interfaces/interfaces_linux_test.go +++ b/net/netmon/interfaces_linux_test.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -package interfaces +package netmon import ( "errors" diff --git a/net/interfaces/interfaces_test.go b/net/netmon/interfaces_test.go similarity index 99% rename from net/interfaces/interfaces_test.go rename to net/netmon/interfaces_test.go index 08bfc2b68..edd4f6d6e 100644 --- a/net/interfaces/interfaces_test.go +++ b/net/netmon/interfaces_test.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -package interfaces +package netmon import ( "encoding/json" diff --git a/net/interfaces/interfaces_windows.go b/net/netmon/interfaces_windows.go similarity index 99% rename from net/interfaces/interfaces_windows.go rename to net/netmon/interfaces_windows.go index eaf759234..00b686e59 100644 --- a/net/interfaces/interfaces_windows.go +++ b/net/netmon/interfaces_windows.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -package interfaces +package netmon import ( "log" diff --git a/net/interfaces/interfaces_windows_test.go b/net/netmon/interfaces_windows_test.go similarity index 93% rename from net/interfaces/interfaces_windows_test.go rename to net/netmon/interfaces_windows_test.go index b02a58d03..91db7bcc5 100644 --- a/net/interfaces/interfaces_windows_test.go +++ b/net/netmon/interfaces_windows_test.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -package interfaces +package netmon import "testing" diff --git a/net/netmon/netmon.go b/net/netmon/netmon.go index 0b35bc301..47b540d6a 100644 --- a/net/netmon/netmon.go +++ b/net/netmon/netmon.go @@ -14,7 +14,6 @@ "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 } diff --git a/net/netmon/netmon_test.go b/net/netmon/netmon_test.go index 9c08b0df1..ce55d1946 100644 --- a/net/netmon/netmon_test.go +++ b/net/netmon/netmon_test.go @@ -11,7 +11,6 @@ "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 diff --git a/net/netmon/state.go b/net/netmon/state.go index 33d4a3fb3..55aef478b 100644 --- a/net/netmon/state.go +++ b/net/netmon/state.go @@ -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 +} diff --git a/net/netns/netns_darwin.go b/net/netns/netns_darwin.go index b9395f734..5a096d845 100644 --- a/net/netns/netns_darwin.go +++ b/net/netns/netns_darwin.go @@ -18,7 +18,6 @@ "golang.org/x/net/route" "golang.org/x/sys/unix" "tailscale.com/envknob" - "tailscale.com/net/interfaces" "tailscale.com/net/netmon" "tailscale.com/net/tsaddr" "tailscale.com/types/logger" @@ -62,12 +61,12 @@ func getInterfaceIndex(logf logger.Logf, netMon *netmon.Monitor, address string) // Helper so we can log errors. defaultIdx := func() (int, error) { if netMon == nil { - idx, err := interfaces.DefaultRouteInterfaceIndex() + idx, err := netmon.DefaultRouteInterfaceIndex() if err != nil { // It's somewhat common for there to be no default gateway route // (e.g. on a phone with no connectivity), don't log those errors // since they are expected. - if !errors.Is(err, interfaces.ErrNoGatewayIndexFound) { + if !errors.Is(err, netmon.ErrNoGatewayIndexFound) { logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err) } return -1, err diff --git a/net/netns/netns_darwin_test.go b/net/netns/netns_darwin_test.go index 17d1f9945..2030c169e 100644 --- a/net/netns/netns_darwin_test.go +++ b/net/netns/netns_darwin_test.go @@ -6,7 +6,7 @@ import ( "testing" - "tailscale.com/net/interfaces" + "tailscale.com/net/netmon" ) func TestGetInterfaceIndex(t *testing.T) { @@ -63,7 +63,7 @@ func TestGetInterfaceIndex(t *testing.T) { t.Skip("no tailscale interface on this machine") } - defaultIdx, err := interfaces.DefaultRouteInterfaceIndex() + defaultIdx, err := netmon.DefaultRouteInterfaceIndex() if err != nil { t.Fatal(err) } diff --git a/net/netns/netns_linux.go b/net/netns/netns_linux.go index bac14e9d7..aaf6dab4a 100644 --- a/net/netns/netns_linux.go +++ b/net/netns/netns_linux.go @@ -14,7 +14,6 @@ "golang.org/x/sys/unix" "tailscale.com/envknob" - "tailscale.com/net/interfaces" "tailscale.com/net/netmon" "tailscale.com/types/logger" "tailscale.com/util/linuxfw" @@ -119,7 +118,7 @@ func setBypassMark(fd uintptr) error { } func bindToDevice(fd uintptr) error { - ifc, err := interfaces.DefaultRouteInterface() + ifc, err := netmon.DefaultRouteInterface() if err != nil { // Make sure we bind to *some* interface, // or we could get a routing loop. diff --git a/net/netns/netns_windows.go b/net/netns/netns_windows.go index 8aa1da18d..fb842eeac 100644 --- a/net/netns/netns_windows.go +++ b/net/netns/netns_windows.go @@ -11,7 +11,6 @@ "golang.org/x/sys/cpu" "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" - "tailscale.com/net/interfaces" "tailscale.com/net/netmon" "tailscale.com/types/logger" ) @@ -52,7 +51,7 @@ func controlC(network, address string, c syscall.RawConn) error { } if canV4 { - iface, err := interfaces.GetWindowsDefault(windows.AF_INET) + iface, err := netmon.GetWindowsDefault(windows.AF_INET) if err != nil { return err } @@ -62,7 +61,7 @@ func controlC(network, address string, c syscall.RawConn) error { } if canV6 { - iface, err := interfaces.GetWindowsDefault(windows.AF_INET6) + iface, err := netmon.GetWindowsDefault(windows.AF_INET6) if err != nil { return err } diff --git a/net/portmapper/portmapper.go b/net/portmapper/portmapper.go index 8c42b17fe..cc0542c17 100644 --- a/net/portmapper/portmapper.go +++ b/net/portmapper/portmapper.go @@ -21,7 +21,6 @@ "go4.org/mem" "tailscale.com/control/controlknobs" - "tailscale.com/net/interfaces" "tailscale.com/net/netaddr" "tailscale.com/net/neterror" "tailscale.com/net/netmon" @@ -216,7 +215,7 @@ func NewClient(logf logger.Logf, netMon *netmon.Monitor, debug *DebugKnobs, cont ret := &Client{ logf: logf, netMon: netMon, - ipAndGateway: interfaces.LikelyHomeRouterIP, // TODO(bradfitz): move this to netMon + ipAndGateway: netmon.LikelyHomeRouterIP, // TODO(bradfitz): move this to method on netMon onChange: onChange, controlKnobs: controlKnobs, } diff --git a/net/routetable/routetable_bsd.go b/net/routetable/routetable_bsd.go index ff3e75ec2..1de1a2734 100644 --- a/net/routetable/routetable_bsd.go +++ b/net/routetable/routetable_bsd.go @@ -17,7 +17,7 @@ "golang.org/x/net/route" "golang.org/x/sys/unix" - "tailscale.com/net/interfaces" + "tailscale.com/net/netmon" "tailscale.com/types/logger" ) @@ -80,7 +80,7 @@ func (r RouteEntryBSD) Format(f fmt.State, verb rune) { // ipFromRMAddr returns a netip.Addr converted from one of the // route.Inet{4,6}Addr types. -func ipFromRMAddr(ifs map[int]interfaces.Interface, addr any) netip.Addr { +func ipFromRMAddr(ifs map[int]netmon.Interface, addr any) netip.Addr { switch v := addr.(type) { case *route.Inet4Addr: return netip.AddrFrom4(v.IP) @@ -102,7 +102,7 @@ func ipFromRMAddr(ifs map[int]interfaces.Interface, addr any) netip.Addr { } // populateGateway populates gateway fields on a RouteEntry/RouteEntryBSD. -func populateGateway(re *RouteEntry, reSys *RouteEntryBSD, ifs map[int]interfaces.Interface, addr any) { +func populateGateway(re *RouteEntry, reSys *RouteEntryBSD, ifs map[int]netmon.Interface, addr any) { // If the address type has a valid IP, use that. if ip := ipFromRMAddr(ifs, addr); ip.IsValid() { re.Gateway = ip @@ -128,7 +128,7 @@ func populateGateway(re *RouteEntry, reSys *RouteEntryBSD, ifs map[int]interface // populateDestination populates the 'Dst' field on a RouteEntry based on the // RouteMessage's destination and netmask fields. -func populateDestination(re *RouteEntry, ifs map[int]interfaces.Interface, rm *route.RouteMessage) { +func populateDestination(re *RouteEntry, ifs map[int]netmon.Interface, rm *route.RouteMessage) { dst := rm.Addrs[unix.RTAX_DST] if dst == nil { return @@ -194,7 +194,7 @@ func populateDestination(re *RouteEntry, ifs map[int]interfaces.Interface, rm *r // routeEntryFromMsg returns a RouteEntry from a single route.Message // returned by the operating system. -func routeEntryFromMsg(ifsByIdx map[int]interfaces.Interface, msg route.Message) (RouteEntry, bool) { +func routeEntryFromMsg(ifsByIdx map[int]netmon.Interface, msg route.Message) (RouteEntry, bool) { rm, ok := msg.(*route.RouteMessage) if !ok { return RouteEntry{}, false @@ -260,12 +260,12 @@ func routeEntryFromMsg(ifsByIdx map[int]interfaces.Interface, msg route.Message) func Get(max int) ([]RouteEntry, error) { // Fetching the list of interfaces can race with fetching our route // table, but we do it anyway since it's helpful for debugging. - ifs, err := interfaces.GetList() + ifs, err := netmon.GetInterfaceList() if err != nil { return nil, err } - ifsByIdx := make(map[int]interfaces.Interface) + ifsByIdx := make(map[int]netmon.Interface) for _, iif := range ifs { ifsByIdx[iif.Index] = iif } diff --git a/net/routetable/routetable_bsd_test.go b/net/routetable/routetable_bsd_test.go index fc134617f..29493d59b 100644 --- a/net/routetable/routetable_bsd_test.go +++ b/net/routetable/routetable_bsd_test.go @@ -15,11 +15,11 @@ "golang.org/x/net/route" "golang.org/x/sys/unix" - "tailscale.com/net/interfaces" + "tailscale.com/net/netmon" ) func TestRouteEntryFromMsg(t *testing.T) { - ifs := map[int]interfaces.Interface{ + ifs := map[int]netmon.Interface{ 1: { Interface: &net.Interface{ Name: "iface0", diff --git a/net/routetable/routetable_linux.go b/net/routetable/routetable_linux.go index d13e7d8be..88dc8535a 100644 --- a/net/routetable/routetable_linux.go +++ b/net/routetable/routetable_linux.go @@ -13,8 +13,8 @@ "github.com/tailscale/netlink" "golang.org/x/sys/unix" - "tailscale.com/net/interfaces" "tailscale.com/net/netaddr" + "tailscale.com/net/netmon" "tailscale.com/types/logger" ) @@ -141,12 +141,12 @@ func (r RouteEntryLinux) ScopeName() string { func Get(max int) ([]RouteEntry, error) { // Fetching the list of interfaces can race with fetching our route // table, but we do it anyway since it's helpful for debugging. - ifs, err := interfaces.GetList() + ifs, err := netmon.GetInterfaceList() if err != nil { return nil, err } - ifsByIdx := make(map[int]interfaces.Interface) + ifsByIdx := make(map[int]netmon.Interface) for _, iif := range ifs { ifsByIdx[iif.Index] = iif } diff --git a/net/sockstats/sockstats_tsgo.go b/net/sockstats/sockstats_tsgo.go index d94f279ad..2d1ccd5a3 100644 --- a/net/sockstats/sockstats_tsgo.go +++ b/net/sockstats/sockstats_tsgo.go @@ -15,7 +15,6 @@ "syscall" "time" - "tailscale.com/net/interfaces" "tailscale.com/net/netmon" "tailscale.com/types/logger" "tailscale.com/util/clientmetric" @@ -89,7 +88,7 @@ func withSockStats(ctx context.Context, label Label, logf logger.Logf) context.C // had a chance to populate knownInterfaces). In that case, we'll have // to get the list of interfaces ourselves. if len(sockStats.knownInterfaces) == 0 { - if ifaces, err := interfaces.GetList(); err == nil { + if ifaces, err := netmon.GetInterfaceList(); err == nil { for _, iface := range ifaces { counters.rxBytesByInterface[iface.Index] = &atomic.Uint64{} counters.txBytesByInterface[iface.Index] = &atomic.Uint64{} diff --git a/tstest/integration/tailscaled_deps_test_darwin.go b/tstest/integration/tailscaled_deps_test_darwin.go index 4ab429ff6..6d366d46e 100644 --- a/tstest/integration/tailscaled_deps_test_darwin.go +++ b/tstest/integration/tailscaled_deps_test_darwin.go @@ -27,7 +27,6 @@ _ "tailscale.com/logtail" _ "tailscale.com/net/dns" _ "tailscale.com/net/dnsfallback" - _ "tailscale.com/net/interfaces" _ "tailscale.com/net/netmon" _ "tailscale.com/net/netns" _ "tailscale.com/net/proxymux" diff --git a/tstest/integration/tailscaled_deps_test_freebsd.go b/tstest/integration/tailscaled_deps_test_freebsd.go index 4ab429ff6..6d366d46e 100644 --- a/tstest/integration/tailscaled_deps_test_freebsd.go +++ b/tstest/integration/tailscaled_deps_test_freebsd.go @@ -27,7 +27,6 @@ _ "tailscale.com/logtail" _ "tailscale.com/net/dns" _ "tailscale.com/net/dnsfallback" - _ "tailscale.com/net/interfaces" _ "tailscale.com/net/netmon" _ "tailscale.com/net/netns" _ "tailscale.com/net/proxymux" diff --git a/tstest/integration/tailscaled_deps_test_linux.go b/tstest/integration/tailscaled_deps_test_linux.go index 4ab429ff6..6d366d46e 100644 --- a/tstest/integration/tailscaled_deps_test_linux.go +++ b/tstest/integration/tailscaled_deps_test_linux.go @@ -27,7 +27,6 @@ _ "tailscale.com/logtail" _ "tailscale.com/net/dns" _ "tailscale.com/net/dnsfallback" - _ "tailscale.com/net/interfaces" _ "tailscale.com/net/netmon" _ "tailscale.com/net/netns" _ "tailscale.com/net/proxymux" diff --git a/tstest/integration/tailscaled_deps_test_openbsd.go b/tstest/integration/tailscaled_deps_test_openbsd.go index 4ab429ff6..6d366d46e 100644 --- a/tstest/integration/tailscaled_deps_test_openbsd.go +++ b/tstest/integration/tailscaled_deps_test_openbsd.go @@ -27,7 +27,6 @@ _ "tailscale.com/logtail" _ "tailscale.com/net/dns" _ "tailscale.com/net/dnsfallback" - _ "tailscale.com/net/interfaces" _ "tailscale.com/net/netmon" _ "tailscale.com/net/netns" _ "tailscale.com/net/proxymux" diff --git a/tstest/integration/tailscaled_deps_test_windows.go b/tstest/integration/tailscaled_deps_test_windows.go index 7607f75d2..015bf8284 100644 --- a/tstest/integration/tailscaled_deps_test_windows.go +++ b/tstest/integration/tailscaled_deps_test_windows.go @@ -35,7 +35,6 @@ _ "tailscale.com/logtail/backoff" _ "tailscale.com/net/dns" _ "tailscale.com/net/dnsfallback" - _ "tailscale.com/net/interfaces" _ "tailscale.com/net/netmon" _ "tailscale.com/net/netns" _ "tailscale.com/net/proxymux" diff --git a/tstest/integration/vms/derive_bindhost_test.go b/tstest/integration/vms/derive_bindhost_test.go index b80e29030..728f60c01 100644 --- a/tstest/integration/vms/derive_bindhost_test.go +++ b/tstest/integration/vms/derive_bindhost_test.go @@ -8,19 +8,19 @@ "runtime" "testing" - "tailscale.com/net/interfaces" + "tailscale.com/net/netmon" ) func deriveBindhost(t *testing.T) string { t.Helper() - ifName, err := interfaces.DefaultRouteInterface() + ifName, err := netmon.DefaultRouteInterface() if err != nil { t.Fatal(err) } var ret string - err = interfaces.ForeachInterfaceAddress(func(i interfaces.Interface, prefix netip.Prefix) { + err = netmon.ForeachInterfaceAddress(func(i netmon.Interface, prefix netip.Prefix) { if ret != "" || i.Name != ifName { return } diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index c5693e28a..f30682f13 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -33,7 +33,6 @@ "tailscale.com/hostinfo" "tailscale.com/ipn/ipnstate" "tailscale.com/net/connstats" - "tailscale.com/net/interfaces" "tailscale.com/net/netcheck" "tailscale.com/net/neterror" "tailscale.com/net/netmon" @@ -936,7 +935,7 @@ func (c *Conn) determineEndpoints(ctx context.Context) ([]tailcfg.Endpoint, erro eps = c.endpointTracker.update(time.Now(), eps) if localAddr := c.pconn4.LocalAddr(); localAddr.IP.IsUnspecified() { - ips, loopback, err := interfaces.LocalAddresses() + ips, loopback, err := netmon.LocalAddresses() if err != nil { return nil, err } diff --git a/wgengine/router/ifconfig_windows.go b/wgengine/router/ifconfig_windows.go index 7901ba211..b9b1873b8 100644 --- a/wgengine/router/ifconfig_windows.go +++ b/wgengine/router/ifconfig_windows.go @@ -20,7 +20,7 @@ "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "tailscale.com/health" - "tailscale.com/net/interfaces" + "tailscale.com/net/netmon" "tailscale.com/net/tsaddr" "tailscale.com/net/tstun" "tailscale.com/util/multierr" @@ -110,7 +110,7 @@ func monitorDefaultRoutes(tun *tun.NativeTun) (*winipcfg.RouteChangeCallback, er } func getDefaultRouteMTU() (uint32, error) { - mtus, err := interfaces.NonTailscaleMTUs() + mtus, err := netmon.NonTailscaleMTUs() if err != nil { return 0, err }