From 04c2c5bd80e86b2113d4e04e840d79ec66981fda Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 29 Dec 2021 10:37:33 -0800 Subject: [PATCH] net/interfaces: define DefaultRouteInterface and State.DefaultRouteInterface It was pretty ill-defined before and mostly for logging. But I wanted to start depending on it, so define what it is and make Windows match the other operating systems, without losing the log output we had before. (and add tests for that) Change-Id: I0fbbba1cfc67a265d09dd6cb738b73f0f6005247 Signed-off-by: Brad Fitzpatrick --- net/interfaces/interfaces.go | 64 +++++++++++++++++-- net/interfaces/interfaces_darwin.go | 10 +-- .../interfaces_defaultrouteif_todo.go | 4 +- net/interfaces/interfaces_linux.go | 13 ++-- net/interfaces/interfaces_test.go | 52 +++++++++++++++ net/interfaces/interfaces_windows.go | 13 ++-- 6 files changed, 132 insertions(+), 24 deletions(-) diff --git a/net/interfaces/interfaces.go b/net/interfaces/interfaces.go index 8dacc59a7..b8f0de826 100644 --- a/net/interfaces/interfaces.go +++ b/net/interfaces/interfaces.go @@ -172,6 +172,7 @@ func sortIPs(s []netaddr.IP) { 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) } @@ -278,13 +279,16 @@ type State struct { // instead of Wifi. This field is not populated by GetState. IsExpensive bool - // DefaultRouteInterface is the interface name for the machine's default route. + // DefaultRouteInterface is the interface name for the + // machine's default route. + // // It is not yet populated on all OSes. - // Its exact value should not be assumed to be a map key for - // the Interface maps above; it's only used for debugging. + // + // When non-empty, its value is the map key into Interface and + // InterfaceIPs. DefaultRouteInterface string - // HTTPProxy is the HTTP proxy to use. + // HTTPProxy is the HTTP proxy to use, if any. HTTPProxy string // PAC is the URL to the Proxy Autoconfig URL, if applicable. @@ -293,7 +297,13 @@ type State struct { func (s *State) String() string { var sb strings.Builder - fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface) + 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={") ifs := make([]string, 0, len(s.Interface)) for k := range s.Interface { if anyInterestingIP(s.InterfaceIPs[k]) { @@ -507,7 +517,16 @@ func GetState() (*State, error) { return nil, err } - s.DefaultRouteInterface, _ = DefaultRouteInterface() + 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) @@ -667,3 +686,36 @@ func netInterfaces() ([]Interface, error) { } return ret, nil } + +// DefaultRouteDetails are the +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() +} diff --git a/net/interfaces/interfaces_darwin.go b/net/interfaces/interfaces_darwin.go index 77a4b154b..f0fe1f5b5 100644 --- a/net/interfaces/interfaces_darwin.go +++ b/net/interfaces/interfaces_darwin.go @@ -16,16 +16,18 @@ import ( "inet.af/netaddr" ) -func DefaultRouteInterface() (string, error) { +func defaultRoute() (d DefaultRouteDetails, err error) { idx, err := DefaultRouteInterfaceIndex() if err != nil { - return "", err + return d, err } iface, err := net.InterfaceByIndex(idx) if err != nil { - return "", err + return d, err } - return iface.Name, nil + d.InterfaceName = iface.Name + d.InterfaceIndex = idx + return d, nil } // fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2. diff --git a/net/interfaces/interfaces_defaultrouteif_todo.go b/net/interfaces/interfaces_defaultrouteif_todo.go index 0815e4f40..ff7045b3e 100644 --- a/net/interfaces/interfaces_defaultrouteif_todo.go +++ b/net/interfaces/interfaces_defaultrouteif_todo.go @@ -11,6 +11,6 @@ import "errors" var errTODO = errors.New("TODO") -func DefaultRouteInterface() (string, error) { - return "TODO", errTODO +func defaultRoute() (DefaultRouteDetails, error) { + return DefaultRouteDetails{}, errTODO } diff --git a/net/interfaces/interfaces_linux.go b/net/interfaces/interfaces_linux.go index 50dee351f..957a81161 100644 --- a/net/interfaces/interfaces_linux.go +++ b/net/interfaces/interfaces_linux.go @@ -122,17 +122,18 @@ func likelyHomeRouterIPAndroid() (ret netaddr.IP, ok bool) { return ret, !ret.IsZero() } -// DefaultRouteInterface returns the name of the network interface that owns -// the default route, not including any tailscale interfaces. -func DefaultRouteInterface() (string, error) { +func defaultRoute() (d DefaultRouteDetails, err error) { v, err := defaultRouteInterfaceProcNet() if err == nil { - return v, nil + d.InterfaceName = v + return d, nil } if runtime.GOOS == "android" { - return defaultRouteInterfaceAndroidIPRoute() + v, err = defaultRouteInterfaceAndroidIPRoute() + d.InterfaceName = v + return d, err } - return v, err + return d, err } var zeroRouteBytes = []byte("00000000") diff --git a/net/interfaces/interfaces_test.go b/net/interfaces/interfaces_test.go index 4ef46db28..e3afb669f 100644 --- a/net/interfaces/interfaces_test.go +++ b/net/interfaces/interfaces_test.go @@ -104,3 +104,55 @@ func TestStateEqualFilteredIPFilter(t *testing.T) { t.Errorf("%+v == %+v when restricting to interesting interfaces and IPs", s1, s2) } } + +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{}, + }, + }, + InterfaceIPs: map[string][]netaddr.IPPrefix{ + "eth0": []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("10.0.0.2/8"), + }, + }, + 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", + }, + }, + }, + want: `interfaces.State{defaultRoute=foo (a foo thing) ifs={} 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) + } + }) + } +} diff --git a/net/interfaces/interfaces_windows.go b/net/interfaces/interfaces_windows.go index 3ca8033f0..82d029060 100644 --- a/net/interfaces/interfaces_windows.go +++ b/net/interfaces/interfaces_windows.go @@ -5,7 +5,6 @@ package interfaces import ( - "fmt" "log" "net" "net/url" @@ -217,18 +216,20 @@ func GetWindowsDefault(family winipcfg.AddressFamily) (*winipcfg.IPAdapterAddres return bestIface, nil } -func DefaultRouteInterface() (string, error) { +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 "", err + return d, err } - if iface == nil { - return "(none)", nil + if iface != nil { + d.InterfaceName = iface.FriendlyName() + d.InterfaceDesc = iface.Description() + d.InterfaceIndex = int(iface.IfIndex) } - return fmt.Sprintf("%s (%s)", iface.FriendlyName(), iface.Description()), nil + return d, nil } var (