diff --git a/pkg/util/net/interface.go b/pkg/util/net/interface.go index e8d090e4..42816bd7 100644 --- a/pkg/util/net/interface.go +++ b/pkg/util/net/interface.go @@ -36,19 +36,40 @@ const ( familyIPv6 AddressFamily = 6 ) +const ( + ipv4RouteFile = "/proc/net/route" + ipv6RouteFile = "/proc/net/ipv6_route" +) + type Route struct { Interface string Destination net.IP Gateway net.IP - // TODO: add more fields here if needed + Family AddressFamily } -// getRoutes obtains the IPv4 routes, and filters out non-default routes. -func getRoutes(input io.Reader) ([]Route, error) { - routes := []Route{} - if input == nil { - return nil, fmt.Errorf("input is nil") +type RouteFile struct { + name string + parse func(input io.Reader) ([]Route, error) +} + +var ( + v4File = RouteFile{name: ipv4RouteFile, parse: getIPv4DefaultRoutes} + v6File = RouteFile{name: ipv6RouteFile, parse: getIPv6DefaultRoutes} +) + +func (rf RouteFile) extract() ([]Route, error) { + file, err := os.Open(rf.name) + if err != nil { + return nil, err } + defer file.Close() + return rf.parse(file) +} + +// getIPv4DefaultRoutes obtains the IPv4 routes, and filters out non-default routes. +func getIPv4DefaultRoutes(input io.Reader) ([]Route, error) { + routes := []Route{} scanner := bufio.NewReader(input) for { line, err := scanner.ReadString('\n') @@ -60,11 +81,15 @@ func getRoutes(input io.Reader) ([]Route, error) { continue } fields := strings.Fields(line) - dest, err := parseHexToIPv4(fields[1]) + // Interested in fields: + // 0 - interface name + // 1 - destination address + // 2 - gateway + dest, err := parseIP(fields[1], familyIPv4) if err != nil { return nil, err } - gw, err := parseHexToIPv4(fields[2]) + gw, err := parseIP(fields[2], familyIPv4) if err != nil { return nil, err } @@ -75,15 +100,52 @@ func getRoutes(input io.Reader) ([]Route, error) { Interface: fields[0], Destination: dest, Gateway: gw, + Family: familyIPv4, }) } return routes, nil } -// parseHexToIPv4 takes the hex IP address string from route file and converts it -// from little endian to big endian for creation of a net.IP address. -// a net.IP, using big endian ordering. -func parseHexToIPv4(str string) (net.IP, error) { +func getIPv6DefaultRoutes(input io.Reader) ([]Route, error) { + routes := []Route{} + scanner := bufio.NewReader(input) + for { + line, err := scanner.ReadString('\n') + if err == io.EOF { + break + } + fields := strings.Fields(line) + // Interested in fields: + // 0 - destination address + // 4 - gateway + // 9 - interface name + dest, err := parseIP(fields[0], familyIPv6) + if err != nil { + return nil, err + } + gw, err := parseIP(fields[4], familyIPv6) + if err != nil { + return nil, err + } + if !dest.Equal(net.IPv6zero) { + continue + } + if gw.Equal(net.IPv6zero) { + continue // loopback + } + routes = append(routes, Route{ + Interface: fields[9], + Destination: dest, + Gateway: gw, + Family: familyIPv6, + }) + } + return routes, nil +} + +// parseIP takes the hex IP address string from route file and converts it +// to a net.IP address. For IPv4, the value must be converted to big endian. +func parseIP(str string, family AddressFamily) (net.IP, error) { if str == "" { return nil, fmt.Errorf("input is nil") } @@ -91,10 +153,17 @@ func parseHexToIPv4(str string) (net.IP, error) { if err != nil { return nil, err } - if len(bytes) != net.IPv4len { - return nil, fmt.Errorf("invalid IPv4 address in route") + if family == familyIPv4 { + if len(bytes) != net.IPv4len { + return nil, fmt.Errorf("invalid IPv4 address in route") + } + return net.IP([]byte{bytes[3], bytes[2], bytes[1], bytes[0]}), nil + } + // Must be IPv6 + if len(bytes) != net.IPv6len { + return nil, fmt.Errorf("invalid IPv6 address in route") } - return net.IP([]byte{bytes[3], bytes[2], bytes[1], bytes[0]}), nil + return net.IP(bytes), nil } func isInterfaceUp(intf *net.Interface) bool { @@ -112,17 +181,8 @@ func isLoopbackOrPointToPoint(intf *net.Interface) bool { return intf.Flags&(net.FlagLoopback|net.FlagPointToPoint) != 0 } -func inFamily(ip net.IP, expectedFamily AddressFamily) bool { - ipFamily := familyIPv4 - if ip.To4() == nil { - ipFamily = familyIPv6 - } - return ipFamily == expectedFamily -} - -// getMatchingGlobalIP method checks all the IP addresses of a Interface looking -// for a valid non-loopback/link-local address of the requested family and returns -// it, if found. +// getMatchingGlobalIP returns the first valid global unicast address of the given +// 'family' from the list of 'addrs'. func getMatchingGlobalIP(addrs []net.Addr, family AddressFamily) (net.IP, error) { if len(addrs) > 0 { for i := range addrs { @@ -131,12 +191,12 @@ func getMatchingGlobalIP(addrs []net.Addr, family AddressFamily) (net.IP, error) if err != nil { return nil, err } - if inFamily(ip, family) { + if memberOf(ip, family) { if ip.IsGlobalUnicast() { glog.V(4).Infof("IP found %v", ip) return ip, nil } else { - glog.V(4).Infof("non-global IP found %v", ip) + glog.V(4).Infof("Non-global unicast address found %v", ip) } } else { glog.V(4).Infof("%v is not an IPv%d address", ip, int(family)) @@ -147,6 +207,8 @@ func getMatchingGlobalIP(addrs []net.Addr, family AddressFamily) (net.IP, error) return nil, nil } +// getIPFromInterface gets the IPs on an interface and returns a global unicast address, if any. The +// interface must be up, the IP must in the family requested, and the IP must be a global unicast address. func getIPFromInterface(intfName string, forFamily AddressFamily, nw networkInterfacer) (net.IP, error) { intf, err := nw.InterfaceByName(intfName) if err != nil { @@ -231,21 +293,21 @@ func chooseIPFromHostInterfaces(nw networkInterfacer) (net.IP, error) { return nil, fmt.Errorf("no acceptable interface with global unicast address found on host") } -//ChooseHostInterface is a method used fetch an IP for a daemon. -//It uses data from /proc/net/route file. -//For a node with no internet connection ,it returns error -//For a multi n/w interface node it returns the IP of the interface with gateway on it. +// ChooseHostInterface is a method used fetch an IP for a daemon. +// If there is no routing info file, it will choose a global IP from the system +// interfaces. Otherwise, it will use IPv4 and IPv6 route information to return the +// IP of the interface with a gateway on it (with priority given to IPv4). For a node +// with no internet connection, it returns error. func ChooseHostInterface() (net.IP, error) { var nw networkInterfacer = networkInterface{} - inFile, err := os.Open("/proc/net/route") + if _, err := os.Stat(ipv4RouteFile); os.IsNotExist(err) { + return chooseIPFromHostInterfaces(nw) + } + routes, err := getAllDefaultRoutes() if err != nil { - if os.IsNotExist(err) { - return chooseIPFromHostInterfaces(nw) - } return nil, err } - defer inFile.Close() - return chooseHostInterfaceFromRoute(inFile, nw) + return chooseHostInterfaceFromRoute(routes, nw) } // networkInterfacer defines an interface for several net library functions. Production @@ -273,22 +335,33 @@ func (_ networkInterface) Interfaces() ([]net.Interface, error) { return net.Interfaces() } -func chooseHostInterfaceFromRoute(inFile io.Reader, nw networkInterfacer) (net.IP, error) { - routes, err := getRoutes(inFile) +// getAllDefaultRoutes obtains IPv4 and IPv6 default routes on the node. If unable +// to read the IPv4 routing info file, we return an error. If unable to read the IPv6 +// routing info file (which is optional), we'll just use the IPv4 route information. +// Using all the routing info, if no default routes are found, an error is returned. +func getAllDefaultRoutes() ([]Route, error) { + routes, err := v4File.extract() if err != nil { return nil, err } + v6Routes, _ := v6File.extract() + routes = append(routes, v6Routes...) if len(routes) == 0 { return nil, fmt.Errorf("No default routes.") } - // TODO: append IPv6 routes for processing - currently only have IPv4 routes + return routes, nil +} + +// chooseHostInterfaceFromRoute cycles through each default route provided, looking for a +// global IP address from the interface for the route. Will first look all each IPv4 route for +// an IPv4 IP, and then will look at each IPv6 route for an IPv6 IP. +func chooseHostInterfaceFromRoute(routes []Route, nw networkInterfacer) (net.IP, error) { for _, family := range []AddressFamily{familyIPv4, familyIPv6} { glog.V(4).Infof("Looking for default routes with IPv%d addresses", uint(family)) for _, route := range routes { - // TODO: When have IPv6 routes, filter here to speed up processing - // if route.Family != family { - // continue - // } + if route.Family != family { + continue + } glog.V(4).Infof("Default route transits interface %q", route.Interface) finalIP, err := getIPFromInterface(route.Interface, family, nw) if err != nil { diff --git a/pkg/util/net/interface_test.go b/pkg/util/net/interface_test.go index 373af00b..5f42852c 100644 --- a/pkg/util/net/interface_test.go +++ b/pkg/util/net/interface_test.go @@ -18,8 +18,9 @@ package net import ( "fmt" - "io" + "io/ioutil" "net" + "os" "strings" "testing" ) @@ -67,10 +68,30 @@ docker0 000011AC 00000000 0001 0 0 0 0000FFFF 0 0 0 virbr0 007AA8C0 00000000 0001 0 0 0 00FFFFFF 0 0 0 ` -// Based on DigitalOcean COREOS -const gatewayfirstLinkLocal = `Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT -eth0 00000000 0120372D 0001 0 0 0 00000000 0 0 0 -eth0 00000000 00000000 0001 0 0 2048 00000000 0 0 0 +const v6gatewayfirst = `00000000000000000000000000000000 00 00000000000000000000000000000000 00 20010001000000000000000000000001 00000064 00000000 00000000 00000003 eth3 +20010002000000000000000000000000 40 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001 eth3 +00000000000000000000000000000000 60 00000000000000000000000000000000 00 00000000000000000000000000000000 00000400 00000000 00000000 00200200 lo +` +const v6gatewaylast = `20010002000000000000000000000000 40 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001 eth3 +00000000000000000000000000000000 60 00000000000000000000000000000000 00 00000000000000000000000000000000 00000400 00000000 00000000 00200200 lo +00000000000000000000000000000000 00 00000000000000000000000000000000 00 20010001000000000000000000000001 00000064 00000000 00000000 00000003 eth3 +` +const v6gatewaymiddle = `20010002000000000000000000000000 40 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001 eth3 +00000000000000000000000000000000 00 00000000000000000000000000000000 00 20010001000000000000000000000001 00000064 00000000 00000000 00000003 eth3 +00000000000000000000000000000000 60 00000000000000000000000000000000 00 00000000000000000000000000000000 00000400 00000000 00000000 00200200 lo +` +const v6noDefaultRoutes = `00000000000000000000000000000000 60 00000000000000000000000000000000 00 00000000000000000000000000000000 00000400 00000000 00000000 00200200 lo +20010001000000000000000000000000 40 00000000000000000000000000000000 00 00000000000000000000000000000000 00000400 00000000 00000000 00000001 docker0 +20010002000000000000000000000000 40 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001 eth3 +fe800000000000000000000000000000 40 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001 eth3 +` +const v6nothing = `` +const v6badDestination = `2001000200000000 7a 00000000000000000000000000000000 00 00000000000000000000000000000000 00000400 00000000 00000000 00200200 lo +` +const v6badGateway = `00000000000000000000000000000000 00 00000000000000000000000000000000 00 200100010000000000000000000000000012 00000064 00000000 00000000 00000003 eth3 +` +const v6route_Invalidhex = `000000000000000000000000000000000 00 00000000000000000000000000000000 00 fe80000000000000021fcafffea0ec00 00000064 00000000 00000000 00000003 enp1s0f0 + ` const ( @@ -98,10 +119,18 @@ var ( ) var ( - ipv4Route = Route{Interface: "eth3", Gateway: net.ParseIP("10.254.0.1")} + ipv4Route = Route{Interface: "eth3", Destination: net.ParseIP("0.0.0.0"), Gateway: net.ParseIP("10.254.0.1"), Family: familyIPv4} + ipv6Route = Route{Interface: "eth3", Destination: net.ParseIP("::"), Gateway: net.ParseIP("2001:1::1"), Family: familyIPv6} ) -func TestGetRoutes(t *testing.T) { +var ( + noRoutes = []Route{} + routeV4 = []Route{ipv4Route} + routeV6 = []Route{ipv6Route} + bothRoutes = []Route{ipv4Route, ipv6Route} +) + +func TestGetIPv4Routes(t *testing.T) { testCases := []struct { tcase string route string @@ -120,7 +149,49 @@ func TestGetRoutes(t *testing.T) { } for _, tc := range testCases { r := strings.NewReader(tc.route) - routes, err := getRoutes(r) + routes, err := getIPv4DefaultRoutes(r) + if err != nil { + if !strings.Contains(err.Error(), tc.errStrFrag) { + t.Errorf("case[%s]: Error string %q does not contain %q", tc.tcase, err, tc.errStrFrag) + } + } else if tc.errStrFrag != "" { + t.Errorf("case[%s]: Error %q expected, but not seen", tc.tcase, tc.errStrFrag) + } else { + if tc.count != len(routes) { + t.Errorf("case[%s]: expected %d routes, have %v", tc.tcase, tc.count, routes) + } else if tc.count == 1 { + if !tc.expected.Gateway.Equal(routes[0].Gateway) { + t.Errorf("case[%s]: expected %v, got %v .err : %v", tc.tcase, tc.expected, routes, err) + } + if !routes[0].Destination.Equal(net.IPv4zero) { + t.Errorf("case[%s}: destination is not for default route (not zero)", tc.tcase) + } + + } + } + } +} + +func TestGetIPv6Routes(t *testing.T) { + testCases := []struct { + tcase string + route string + count int + expected *Route + errStrFrag string + }{ + {"v6 gatewayfirst", v6gatewayfirst, 1, &ipv6Route, ""}, + {"v6 gatewaymiddle", v6gatewaymiddle, 1, &ipv6Route, ""}, + {"v6 gatewaylast", v6gatewaylast, 1, &ipv6Route, ""}, + {"v6 no routes", v6nothing, 0, nil, ""}, + {"v6 badDestination", v6badDestination, 0, nil, "invalid IPv6"}, + {"v6 badGateway", v6badGateway, 0, nil, "invalid IPv6"}, + {"v6 route_Invalidhex", v6route_Invalidhex, 0, nil, "odd length hex string"}, + {"v6 no default routes", v6noDefaultRoutes, 0, nil, ""}, + } + for _, tc := range testCases { + r := strings.NewReader(tc.route) + routes, err := getIPv6DefaultRoutes(r) if err != nil { if !strings.Contains(err.Error(), tc.errStrFrag) { t.Errorf("case[%s]: Error string %q does not contain %q", tc.tcase, err, tc.errStrFrag) @@ -130,8 +201,13 @@ func TestGetRoutes(t *testing.T) { } else { if tc.count != len(routes) { t.Errorf("case[%s]: expected %d routes, have %v", tc.tcase, tc.count, routes) - } else if tc.count == 1 && !tc.expected.Gateway.Equal(routes[0].Gateway) { - t.Errorf("case[%s]: expected %v, got %v .err : %v", tc.tcase, tc.expected, routes, err) + } else if tc.count == 1 { + if !tc.expected.Gateway.Equal(routes[0].Gateway) { + t.Errorf("case[%s]: expected %v, got %v .err : %v", tc.tcase, tc.expected, routes, err) + } + if !routes[0].Destination.Equal(net.IPv6zero) { + t.Errorf("case[%s}: destination is not for default route (not zero)", tc.tcase) + } } } } @@ -141,19 +217,23 @@ func TestParseIP(t *testing.T) { testCases := []struct { tcase string ip string + family AddressFamily success bool expected net.IP }{ - {"empty", "", false, nil}, - {"too short", "AA", false, nil}, - {"too long", "0011223344", false, nil}, - {"invalid", "invalid!", false, nil}, - {"zero", "00000000", true, net.IP{0, 0, 0, 0}}, - {"ffff", "FFFFFFFF", true, net.IP{0xff, 0xff, 0xff, 0xff}}, - {"valid", "12345678", true, net.IP{120, 86, 52, 18}}, + {"empty", "", familyIPv4, false, nil}, + {"too short", "AA", familyIPv4, false, nil}, + {"too long", "0011223344", familyIPv4, false, nil}, + {"invalid", "invalid!", familyIPv4, false, nil}, + {"zero", "00000000", familyIPv4, true, net.IP{0, 0, 0, 0}}, + {"ffff", "FFFFFFFF", familyIPv4, true, net.IP{0xff, 0xff, 0xff, 0xff}}, + {"valid v4", "12345678", familyIPv4, true, net.IP{120, 86, 52, 18}}, + {"valid v6", "fe800000000000000000000000000000", familyIPv6, true, net.IP{0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}, + {"v6 too short", "fe80000000000000021fcafffea0ec0", familyIPv6, false, nil}, + {"v6 too long", "fe80000000000000021fcafffea0ec002", familyIPv6, false, nil}, } for _, tc := range testCases { - ip, err := parseHexToIPv4(tc.ip) + ip, err := parseIP(tc.ip, tc.family) if !ip.Equal(tc.expected) { t.Errorf("case[%v]: expected %q, got %q . err : %v", tc.tcase, tc.expected, ip, err) } @@ -244,6 +324,23 @@ func (_ validNetworkInterface) Interfaces() ([]net.Interface, error) { return []net.Interface{upIntf}, nil } +// Both IPv4 and IPv6 addresses (expecting IPv4 to be used) +type v4v6NetworkInterface struct { +} + +func (_ v4v6NetworkInterface) InterfaceByName(intfName string) (*net.Interface, error) { + return &upIntf, nil +} +func (_ v4v6NetworkInterface) Addrs(intf *net.Interface) ([]net.Addr, error) { + var ifat []net.Addr + ifat = []net.Addr{ + addrStruct{val: "2001::10/64"}, addrStruct{val: "10.254.71.145/17"}} + return ifat, nil +} +func (_ v4v6NetworkInterface) Interfaces() ([]net.Interface, error) { + return []net.Interface{upIntf}, nil +} + // Interface with only IPv6 address type ipv6NetworkInterface struct { } @@ -436,26 +533,25 @@ func TestGetIPFromInterface(t *testing.T) { func TestChooseHostInterfaceFromRoute(t *testing.T) { testCases := []struct { tcase string - inFile io.Reader + routes []Route nw networkInterfacer expected net.IP }{ - {"ipv4", strings.NewReader(gatewayfirst), validNetworkInterface{}, net.ParseIP("10.254.71.145")}, - {"ipv6", strings.NewReader(gatewaymiddle), ipv6NetworkInterface{}, net.ParseIP("2001::200")}, - {"no non-link-local ip", strings.NewReader(gatewaymiddle), networkInterfaceWithOnlyLinkLocals{}, nil}, - {"no routes", strings.NewReader(nothing), validNetworkInterface{}, nil}, - {"no route file", nil, validNetworkInterface{}, nil}, - {"no interfaces", nil, noNetworkInterface{}, nil}, - {"no interface addrs", strings.NewReader(gatewaymiddle), networkInterfaceWithNoAddrs{}, nil}, - {"fail get addrs", strings.NewReader(gatewaymiddle), networkInterfaceFailGetAddrs{}, nil}, + {"ipv4", routeV4, validNetworkInterface{}, net.ParseIP("10.254.71.145")}, + {"ipv6", routeV6, ipv6NetworkInterface{}, net.ParseIP("2001::200")}, + {"prefer ipv4", bothRoutes, v4v6NetworkInterface{}, net.ParseIP("10.254.71.145")}, + {"all LLA", routeV4, networkInterfaceWithOnlyLinkLocals{}, nil}, + {"no routes", noRoutes, validNetworkInterface{}, nil}, + {"fail get IP", routeV4, networkInterfaceFailGetAddrs{}, nil}, } for _, tc := range testCases { - ip, err := chooseHostInterfaceFromRoute(tc.inFile, tc.nw) + ip, err := chooseHostInterfaceFromRoute(tc.routes, tc.nw) if !ip.Equal(tc.expected) { t.Errorf("case[%v]: expected %v, got %+v .err : %v", tc.tcase, tc.expected, ip, err) } } } + func TestMemberOf(t *testing.T) { testCases := []struct { tcase string @@ -505,3 +601,125 @@ func TestGetIPFromHostInterfaces(t *testing.T) { } } } + +func makeRouteFile(content string, t *testing.T) (*os.File, error) { + routeFile, err := ioutil.TempFile("", "route") + if err != nil { + return nil, err + } + + if _, err := routeFile.Write([]byte(content)); err != nil { + return routeFile, err + } + err = routeFile.Close() + return routeFile, err +} + +func TestFailGettingIPv4Routes(t *testing.T) { + defer func() { v4File.name = ipv4RouteFile }() + + // Try failure to open file (should not occur, as caller ensures we have IPv4 route file, but being thorough) + v4File.name = "no-such-file" + errStrFrag := "no such file" + _, err := v4File.extract() + if err == nil { + fmt.Errorf("Expected error trying to read non-existent v4 route file") + } + if !strings.Contains(err.Error(), errStrFrag) { + t.Errorf("Unable to find %q in error string %q", errStrFrag, err.Error()) + } +} + +func TestFailGettingIPv6Routes(t *testing.T) { + defer func() { v6File.name = ipv6RouteFile }() + + // Try failure to open file (this would be ignored by caller) + v6File.name = "no-such-file" + errStrFrag := "no such file" + _, err := v6File.extract() + if err == nil { + fmt.Errorf("Expected error trying to read non-existent v6 route file") + } + if !strings.Contains(err.Error(), errStrFrag) { + t.Errorf("Unable to find %q in error string %q", errStrFrag, err.Error()) + } +} + +func TestGetAllDefaultRoutesFailNoV4RouteFile(t *testing.T) { + defer func() { v4File.name = ipv4RouteFile }() + + // Should not occur, as caller ensures we have IPv4 route file, but being thorough + v4File.name = "no-such-file" + errStrFrag := "no such file" + _, err := getAllDefaultRoutes() + if err == nil { + fmt.Errorf("Expected error trying to read non-existent v4 route file") + } + if !strings.Contains(err.Error(), errStrFrag) { + t.Errorf("Unable to find %q in error string %q", errStrFrag, err.Error()) + } +} + +func TestGetAllDefaultRoutes(t *testing.T) { + testCases := []struct { + tcase string + v4Info string + v6Info string + count int + expected []Route + errStrFrag string + }{ + {"no routes", noInternetConnection, v6noDefaultRoutes, 0, nil, "No default routes"}, + {"only v4 route", gatewayfirst, v6noDefaultRoutes, 1, routeV4, ""}, + {"only v6 route", noInternetConnection, v6gatewayfirst, 1, routeV6, ""}, + {"v4 and v6 routes", gatewayfirst, v6gatewayfirst, 2, bothRoutes, ""}, + } + defer func() { + v4File.name = ipv4RouteFile + v6File.name = ipv6RouteFile + }() + + for _, tc := range testCases { + routeFile, err := makeRouteFile(tc.v4Info, t) + if routeFile != nil { + defer os.Remove(routeFile.Name()) + } + if err != nil { + t.Errorf("case[%s]: test setup failure for IPv4 route file: %v", tc.tcase, err) + } + v4File.name = routeFile.Name() + v6routeFile, err := makeRouteFile(tc.v6Info, t) + if v6routeFile != nil { + defer os.Remove(v6routeFile.Name()) + } + if err != nil { + t.Errorf("case[%s]: test setup failure for IPv6 route file: %v", tc.tcase, err) + } + v6File.name = v6routeFile.Name() + + routes, err := getAllDefaultRoutes() + if err != nil { + if !strings.Contains(err.Error(), tc.errStrFrag) { + t.Errorf("case[%s]: Error string %q does not contain %q", tc.tcase, err, tc.errStrFrag) + } + } else if tc.errStrFrag != "" { + t.Errorf("case[%s]: Error %q expected, but not seen", tc.tcase, tc.errStrFrag) + } else { + if tc.count != len(routes) { + t.Errorf("case[%s]: expected %d routes, have %v", tc.tcase, tc.count, routes) + } + for i, expected := range tc.expected { + if !expected.Gateway.Equal(routes[i].Gateway) { + t.Errorf("case[%s]: at %d expected %v, got %v .err : %v", tc.tcase, i, tc.expected, routes, err) + } + zeroIP := net.IPv4zero + if expected.Family == familyIPv6 { + zeroIP = net.IPv6zero + } + if !routes[i].Destination.Equal(zeroIP) { + t.Errorf("case[%s}: at %d destination is not for default route (not %v)", tc.tcase, i, zeroIP) + } + } + } + } +}