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 <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2021-12-29 10:37:33 -08:00 committed by Brad Fitzpatrick
parent 96cab21383
commit 04c2c5bd80
6 changed files with 132 additions and 24 deletions

View File

@ -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()
}

View File

@ -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.

View File

@ -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
}

View File

@ -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")

View File

@ -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)
}
})
}
}

View File

@ -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 (