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 (