blob: c1b8340416c1b62f2691998f7d987424e308a123 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// +build !build_with_native_toolchain
package routes_test
import (
"fmt"
"net"
"testing"
"go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/routes"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
var testRouteTable = routes.ExtendedRouteTable{
// nic, subnet, gateway, metric, tracksInterface, dynamic, enabled
createExtendedRoute(1, "127.0.0.1/32", "", 100, true, false, true), // loopback
createExtendedRoute(1, "::1/128", "", 100, true, false, true), // loopback
createExtendedRoute(4, "192.168.1.0/24", "", 100, true, true, true), // DHCP IP (eth)
createExtendedRoute(2, "192.168.100.0/24", "", 200, true, true, true), // DHCP IP (wlan)
createExtendedRoute(3, "10.1.2.0/23", "", 100, true, false, true), // static IP
createExtendedRoute(2, "110.34.0.0/16", "192.168.100.10", 500, false, false, true), // static route
// static route (eth)
createExtendedRoute(4, "2610:1:22:123:34:faed::/96", "fe80::250:a8ff:9:79", 100, false, false, true),
createExtendedRoute(4, "2622:0:2200:10::/64", "", 100, true, true, true), // RA (eth)
createExtendedRoute(4, "0.0.0.0/0", "192.168.1.1", 100, true, true, true), // default (eth)
createExtendedRoute(2, "0.0.0.0/0", "192.168.100.10", 200, true, true, true), // default (wlan)
createExtendedRoute(4, "::/0", "fe80::210:6e00:fe11:3265", 100, true, false, true), // default (eth)
}
func createRoute(nicid tcpip.NICID, subnet string, gateway string) tcpip.Route {
_, s, err := net.ParseCIDR(subnet)
if err != nil {
panic(err)
}
sn, err := tcpip.NewSubnet(tcpip.Address(s.IP), tcpip.AddressMask(s.Mask))
if err != nil {
panic(err)
}
return tcpip.Route{
Destination: sn,
Gateway: ipStringToAddress(gateway),
NIC: nicid,
}
}
func createExtendedRoute(nicid tcpip.NICID, subnet string, gateway string, metric routes.Metric, tracksInterface bool, dynamic bool, enabled bool) routes.ExtendedRoute {
return routes.ExtendedRoute{
Route: createRoute(nicid, subnet, gateway),
Metric: metric,
MetricTracksInterface: tracksInterface,
Dynamic: dynamic,
Enabled: enabled,
}
}
func ipStringToAddress(ipStr string) tcpip.Address {
return ipToAddress(net.ParseIP(ipStr))
}
func ipToAddress(ip net.IP) tcpip.Address {
if v4 := ip.To4(); v4 != nil {
return tcpip.Address(v4)
}
return tcpip.Address(ip)
}
func TestExtendedRouteMatch(t *testing.T) {
for _, tc := range []struct {
subnet string
addr string
want bool
}{
{"192.168.10.0/24", "192.168.10.0", true},
{"192.168.10.0/24", "192.168.10.1", true},
{"192.168.10.0/24", "192.168.10.10", true},
{"192.168.10.0/24", "192.168.10.255", true},
{"192.168.10.0/24", "192.168.11.1", false},
{"192.168.10.0/24", "192.168.0.1", false},
{"192.168.10.0/24", "192.167.10.1", false},
{"192.168.10.0/24", "193.168.10.1", false},
{"192.168.10.0/24", "0.0.0.0", false},
{"123.220.0.0/14", "123.220.11.22", true},
{"123.220.0.0/14", "123.221.11.22", true},
{"123.220.0.0/14", "123.222.11.22", true},
{"123.220.0.0/14", "123.223.11.22", true},
{"123.220.0.0/14", "123.224.11.22", false},
{"0.0.0.0/0", "0.0.0.0", true},
{"0.0.0.0/0", "1.1.1.1", true},
{"0.0.0.0/0", "255.255.255.255", true},
{"2402:f000:5:8401::/64", "2402:f000:5:8401::", true},
{"2402:f000:5:8401::/64", "2402:f000:5:8401::1", true},
{"2402:f000:5:8401::/64", "2402:f000:5:8401:1:12:123::", true},
{"2402:f000:5:8401::/64", "2402:f000:5:8402::", false},
{"2402:f000:5:8401::/64", "2402:f000:15:8401::", false},
{"2402:f000:5:8401::/64", "2402::5:8401::", false},
{"2402:f000:5:8401::/64", "2400:f000:5:8401::", false},
{"::/0", "::", true},
{"::/0", "1:1:1:1:1:1:1:1", true},
{"::/0", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true},
} {
t.Run("RouteMatch", func(t *testing.T) {
er := createExtendedRoute(1, tc.subnet, "", 100, true, true, true)
addr := ipStringToAddress(tc.addr)
if got := er.Match(addr); got != tc.want {
t.Errorf("got match addr %s in subnet %s = %t, want = %t", tc.addr, tc.subnet, got, tc.want)
}
})
}
}
func TestSortingLess(t *testing.T) {
for _, tc := range []struct {
subnet1 string
metric1 routes.Metric
nic1 tcpip.NICID
subnet2 string
metric2 routes.Metric
nic2 tcpip.NICID
want bool
}{
// non-default before default
{"100.99.24.12/32", 100, 1, "0.0.0.0/0", 100, 1, true},
{"10.144.0.0/12", 100, 1, "0.0.0.0/0", 100, 2, true},
{"127.0.0.1/24", 100, 1, "0.0.0.0/0", 100, 1, true},
{"2511:5f32:4:6:124::2:1/128", 100, 1, "::/0", 100, 1, true},
{"fe80:ffff:0:1234:5:6::/96", 100, 2, "::/0", 100, 1, true},
{"::1/128", 100, 2, "::/0", 100, 2, true},
// IPv4 before IPv6
{"100.99.24.12/32", 100, 1, "2511:5f32:4:6:124::2/120", 100, 1, true},
{"100.99.24.12/32", 100, 2, "2605:32::12/128", 100, 1, true},
{"127.0.0.1/24", 100, 1, "::1/128", 100, 1, true},
{"0.0.0.0/0", 100, 1, "::/0", 100, 3, true},
// longer prefix wins
{"100.99.24.12/32", 100, 2, "100.99.24.12/31", 100, 1, true},
{"100.99.24.12/32", 100, 1, "10.144.0.0/12", 100, 1, true},
{"100.99.24.128/25", 100, 5, "128.0.0.1/24", 100, 3, true},
{"2511:5f32:4:6:124::2:12/128", 100, 1, "2511:5f32:4:6:124::2:12/126", 100, 1, true},
{"2511:5f32:4:6:124::2:12/128", 100, 1, "2605:32::12/127", 100, 1, true},
{"fe80:ffff:0:1234:5:6::/96", 100, 2, "2511:5f32:4:6:124::/90", 100, 1, true},
{"2511:5f32:4:6:124::2:1/128", 100, 1, "::1/120", 100, 2, true},
// lower metric
{"100.99.24.12/32", 100, 1, "10.1.21.31/32", 101, 1, true},
{"100.99.24.12/32", 101, 1, "10.1.21.31/32", 100, 2, false},
{"100.99.2.0/23", 100, 3, "10.1.22.0/23", 101, 1, true},
{"100.99.2.0/23", 101, 1, "10.1.22.0/23", 100, 1, false},
{"2511:5f32:4:6:124::2:1/128", 100, 1, "2605:32::12/128", 101, 1, true},
{"2511:5f32:4:6:124::2:1/128", 101, 3, "2605:32::12/128", 100, 2, false},
{"2511:5f32:4:6:124::2:1/96", 100, 1, "fe80:ffff:0:1234:5:6::/96", 101, 4, true},
{"2511:5f32:4:6:124::2:1/96", 101, 1, "fe80:ffff:0:1234:5:6::/96", 100, 4, false},
// tie-breaker: destination IPs
{"10.1.21.31/32", 100, 2, "100.99.24.12/32", 100, 1, true},
{"100.99.24.12/32", 100, 1, "10.1.21.31/32", 100, 3, false},
{"2511:5f32:4:6:124::2:1/128", 100, 1, "2605:32::12/128", 100, 1, true},
{"2605:32::12/128", 100, 1, "2511:5f32:4:6:124::2:1/128", 100, 1, false},
// tie-breaker: NIC
{"10.1.21.31/32", 100, 1, "10.1.21.31/32", 100, 2, true},
{"10.1.21.31/32", 100, 2, "10.1.21.31/32", 100, 1, false},
{"2511:5f32:4:6:124::2:1/128", 100, 1, "2511:5f32:4:6:124::2:1/128", 100, 2, true},
{"2511:5f32:4:6:124::2:1/128", 100, 2, "2511:5f32:4:6:124::2:1/128", 100, 1, false},
} {
name := fmt.Sprintf("Test-%s@nic%d[m:%d]_<_%s@nic%d[m:%d]", tc.subnet1, tc.nic1, tc.metric1, tc.subnet2, tc.nic2, tc.metric2)
t.Run(name, func(t *testing.T) {
e1 := createExtendedRoute(tc.nic1, tc.subnet1, "", tc.metric1, true, true, true)
e2 := createExtendedRoute(tc.nic2, tc.subnet2, "", tc.metric2, true, true, true)
if got := routes.Less(&e1, &e2); got != tc.want {
t.Errorf("got Less(%s, %s) = %t, want = %t", &e1, &e2, got, tc.want)
}
// reverse test
reverseWant := !tc.want
if got := routes.Less(&e2, &e1); got != reverseWant {
t.Errorf("got Less(%s, %s) = %t, want = %t", &e2, &e1, got, reverseWant)
}
})
}
}
func isSameRouteTableImpl(rt1, rt2 []routes.ExtendedRoute, checkAttributes bool) bool {
if len(rt1) != len(rt2) {
return false
}
for i, r1 := range rt1 {
r2 := rt2[i]
if r1.Route != r2.Route {
return false
}
if checkAttributes && (r1.Metric != r2.Metric || r1.MetricTracksInterface != r2.MetricTracksInterface || r1.Dynamic != r2.Dynamic || r1.Enabled != r2.Enabled) {
return false
}
}
return true
}
func isSameRouteTable(rt1, rt2 []routes.ExtendedRoute) bool {
return isSameRouteTableImpl(rt1, rt2, true /* checkAttributes */)
}
func isSameRouteTableSkippingAttributes(rt1, rt2 []routes.ExtendedRoute) bool {
return isSameRouteTableImpl(rt1, rt2, false /* checkAttributes */)
}
func TestAddRoute(t *testing.T) {
for _, tc := range []struct {
name string
order []int
}{
// different orders
{"Add1", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
{"Add2", []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}},
{"Add3", []int{5, 3, 9, 2, 6, 0, 7, 10, 1, 4, 8}},
{"Add4", []int{6, 5, 7, 4, 8, 3, 9, 2, 10, 1, 0}},
// different orders and duplicates
{"Add5", []int{0, 0, 1, 2, 3, 4, 2, 5, 6, 7, 1, 8, 9, 10, 0}},
{"Add6", []int{10, 9, 8, 4, 7, 6, 5, 2, 4, 3, 2, 1, 0, 5, 5}},
{"Add7", []int{5, 3, 9, 2, 6, 0, 7, 10, 10, 1, 3, 4, 8}},
{"Add8", []int{6, 6, 6, 5, 7, 4, 8, 3, 9, 9, 2, 7, 10, 1, 0}},
} {
t.Run(tc.name, func(t *testing.T) {
// Create route table by adding all routes in the given order.
tb := routes.RouteTable{}
for _, j := range tc.order {
r := testRouteTable[j]
tb.AddRoute(r.Route, r.Metric, r.MetricTracksInterface, r.Dynamic, r.Enabled)
}
tableWanted := testRouteTable
tableGot := tb.GetExtendedRouteTable()
if len(tableGot) != len(tableWanted) {
t.Fatalf("got len(table) = %d, want len(table) = %d", len(tableGot), len(tableWanted))
}
if !isSameRouteTable(tableGot, tableWanted) {
t.Errorf("got\n%s, want\n%s", tableGot, tableWanted)
}
})
}
// Adding a route that already exists but with different dynamic/enabled
// attributes will just overwrite the entry with the new attributes. The
// position in the table stays the same.
t.Run("Changing dynamic/enabled", func(t *testing.T) {
tb := routes.RouteTable{}
tb.Set(testRouteTable)
for i, r := range testRouteTable {
r.Dynamic = !r.Dynamic
tb.AddRoute(r.Route, r.Metric, r.MetricTracksInterface, r.Dynamic, r.Enabled)
tableWanted := testRouteTable
tableGot := tb.GetExtendedRouteTable()
if tableGot[i].Dynamic != r.Dynamic {
t.Errorf("got tableGot[%d].Dynamic = %t, want %t", i, tableGot[i].Dynamic, r.Dynamic)
}
if !isSameRouteTableSkippingAttributes(tableGot, tableWanted) {
t.Errorf("got\n%s, want\n%s", tableGot, tableWanted)
}
r.Enabled = !r.Enabled
tb.AddRoute(r.Route, r.Metric, r.MetricTracksInterface, r.Dynamic, r.Enabled)
tableGot = tb.GetExtendedRouteTable()
if tableGot[i].Enabled != r.Enabled {
t.Errorf("got tableGot[%d].Enabled = %t, want %t", i, tableGot[i].Enabled, r.Enabled)
}
if !isSameRouteTableSkippingAttributes(tableGot, tableWanted) {
t.Errorf("got\n%s, want\n%s", tableGot, tableWanted)
}
}
})
// The metric is used as a tie-breaker when routes have the same prefix length
t.Run("Changing metric", func(t *testing.T) {
r0 := createRoute(1, "0.0.0.0/0", "192.168.1.1")
r1 := createRoute(2, "0.0.0.0/0", "192.168.100.10")
// 1.test - r0 gets lower metric.
{
var tb routes.RouteTable
tb.AddRoute(r0, 100, true, true, true)
tb.AddRoute(r1, 200, true, true, true)
tableGot := tb.GetExtendedRouteTable()
if got, want := tableGot[0].Route, r0; got != want {
t.Errorf("got = %s, want = %s", got, want)
}
if got, want := tableGot[1].Route, r1; got != want {
t.Errorf("got = %s, want = %s", got, want)
}
}
// 2.test - r1 gets lower metric.
{
var tb routes.RouteTable
tb.AddRoute(r0, 200, true, true, true)
tb.AddRoute(r1, 100, true, true, true)
tableGot := tb.GetExtendedRouteTable()
if got, want := tableGot[0].Route, r1; got != want {
t.Errorf("got = %s, want = %s", got, want)
}
if got, want := tableGot[1].Route, r0; got != want {
t.Errorf("got = %s, want = %s", got, want)
}
}
})
}
func TestDelRoute(t *testing.T) {
for _, tc := range []struct {
name string
order []int
}{
{"Del1", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
{"Del2", []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}},
{"Del3", []int{6, 5, 7, 4, 8, 3, 9, 2, 10, 1, 0}},
{"Del4", []int{0}},
{"Del5", []int{1}},
{"Del6", []int{9}},
{"Del7", []int{0, 0, 1, 1, 5, 5, 9, 9, 10, 10}},
{"Del8", []int{5, 8, 5, 0, 1, 9, 1, 5}},
} {
t.Run(tc.name, func(t *testing.T) {
tb := routes.RouteTable{}
tb.Set(testRouteTable)
validRoutes := make([]bool, len(testRouteTable))
for i := range validRoutes {
validRoutes[i] = true
}
for _, d := range tc.order {
toDel := testRouteTable[d]
tb.DelRoute(toDel.Route)
validRoutes[d] = false
}
tableGot := tb.GetExtendedRouteTable()
tableWant := routes.ExtendedRouteTable{}
for i, valid := range validRoutes {
if valid {
tableWant = append(tableWant, testRouteTable[i])
}
}
if !isSameRouteTable(tableGot, tableWant) {
t.Errorf("got\n%s, want\n%s", tableGot, tableWant)
}
})
}
}
func TestUpdateMetricByInterface(t *testing.T) {
// Updating the metric for the an interface updates the metric for all routes
// that track that interface.
for nicid := tcpip.NICID(1); nicid <= 4; nicid++ {
t.Run(fmt.Sprintf("Change-NIC-%d-metric-updates-route-metric", nicid), func(t *testing.T) {
tb := routes.RouteTable{}
tb.Set(testRouteTable)
newMetric := routes.Metric(1000 + nicid)
// Verify existing table does not use the new metric yet.
tableGot := tb.GetExtendedRouteTable()
for _, r := range tableGot {
if r.Metric == newMetric {
t.Fatalf("Error: r.Metric said to invalid value before test start")
}
}
tb.UpdateMetricByInterface(nicid, newMetric)
tableGot = tb.GetExtendedRouteTable()
for _, r := range tableGot {
if r.Route.NIC != nicid {
continue
}
if !r.MetricTracksInterface && r.Metric == newMetric {
t.Errorf("got MetricTracksInterface==false && Metric=%d, want metric!=%d", r.Metric, newMetric)
}
if r.MetricTracksInterface && r.Metric != newMetric {
t.Errorf("got MetricTracksInterface==true && Metric=%d, want metric=%d", r.Metric, newMetric)
}
}
})
}
// A metric change should trigger a re-sort of the routing table.
t.Run(fmt.Sprint("Change-metric-resorts-table"), func(t *testing.T) {
r0 := createRoute(0, "0.0.0.0/0", "192.168.1.1")
r1 := createRoute(1, "0.0.0.0/0", "192.168.100.10")
// Initially r0 has lower metric and is ahead in the table.
tb := routes.RouteTable{}
tb.AddRoute(r0, 100, true, true, true)
tb.AddRoute(r1, 200, true, true, true)
{
tableGot := tb.GetExtendedRouteTable()
if got, want := tableGot[0].Route, r0; got != want {
t.Errorf("got = %s, want = %s", got, want)
}
if got, want := tableGot[1].Route, r1; got != want {
t.Errorf("got = %s, want = %s", got, want)
}
}
// Lowering nic1's metric should be reflected in r1's metric and promote it
// in the route table.
tb.UpdateMetricByInterface(1, 50)
{
tableGot := tb.GetExtendedRouteTable()
if got, want := tableGot[0].Route, r1; got != want {
t.Errorf("got = %s, want = %s", got, want)
}
if got, want := tableGot[1].Route, r0; got != want {
t.Errorf("got = %s, want = %s", got, want)
}
}
})
}
func TestUpdateRoutesByInterface(t *testing.T) {
for nicid := tcpip.NICID(1); nicid <= 4; nicid++ {
// Test the normal case where on DOWN netstack removes dynamic routes and
// disables static ones (ActionDeleteDynamicDisableStatic), and on up
// it re-enables static routes (ActionEnableStatic).
t.Run(fmt.Sprintf("Down-Up_NIC-%d", nicid), func(t *testing.T) {
tb := routes.RouteTable{}
tb.Set(testRouteTable)
tableGot := tb.GetExtendedRouteTable()
for _, r := range tableGot {
if got, want := r.Enabled, true; got != want {
t.Errorf("got Enabled = %t, want = %t, route = %s", got, want, &r)
}
}
// Down -> 1.Remove dynamic routes.
tb.UpdateRoutesByInterface(nicid, routes.ActionDeleteDynamic)
// Verify all dynamic routes to this NIC are gone, and static ones are
// still enabled.
tableGot = tb.GetExtendedRouteTable()
for _, r := range tableGot {
if r.Route.NIC != nicid {
continue
}
if got, want := r.Enabled, true; got != want {
t.Errorf("got Enabled = %t, want = %t, route = %s", got, want, &r)
}
if got, want := r.Dynamic, false; got != want {
t.Errorf("got Dynamic = %t, want = %t, route = %s", got, want, &r)
}
}
// Down -> 2.Disable static ones.
tb.UpdateRoutesByInterface(nicid, routes.ActionDisableStatic)
// Verify all dynamic routes to this NIC are gone, and static ones are
// disabled.
tableGot = tb.GetExtendedRouteTable()
for _, r := range tableGot {
if r.Route.NIC != nicid {
continue
}
if got, want := r.Enabled, false; got != want {
t.Errorf("got Enabled = %t, want = %t, route = %s", got, want, &r)
}
if got, want := r.Dynamic, false; got != want {
t.Errorf("got Dynamic = %t, want = %t, route = %s", got, want, &r)
}
}
// Up -> Re-enable static routes.
tb.UpdateRoutesByInterface(nicid, routes.ActionEnableStatic)
// Verify dynamic routes to this NIC are still gone, and static ones are
// enabled.
tableGot = tb.GetExtendedRouteTable()
for _, r := range tableGot {
if r.Route.NIC != nicid {
continue
}
if got, want := r.Enabled, true; got != want {
t.Errorf("got Enabled = %t, want = %t, route = %s", got, want, &r)
}
if got, want := r.Dynamic, false; got != want {
t.Errorf("got Dynamic = %t, want = %t, route = %s", got, want, &r)
}
}
})
// Test the special case where the interface is removed and in response all
// routes to this interface are removed (ActionDeleteAll).
t.Run(fmt.Sprintf("Remove_NIC-%d", nicid), func(t *testing.T) {
tb := routes.RouteTable{}
tb.Set(testRouteTable)
tableGot := tb.GetExtendedRouteTable()
for _, r := range tableGot {
if got, want := r.Enabled, true; got != want {
t.Errorf("got Enabled = %t, want = %t, route = %s", got, want, &r)
}
}
// Remove all routes to nicid.
tb.UpdateRoutesByInterface(nicid, routes.ActionDeleteAll)
// Verify all routes to this NIC are gone.
tableGot = tb.GetExtendedRouteTable()
for _, r := range tableGot {
if r.Route.NIC == nicid {
t.Errorf("got route pointing to NIC-%d, want none, route = %s", nicid, &r)
}
}
})
}
}
func TestGetNetstackTable(t *testing.T) {
for _, tc := range []struct {
name string
disabled []int
}{
{"GetNsTable1", []int{}},
{"GetNsTable2", []int{0}},
{"GetNsTable3", []int{10}},
{"GetNsTable4", []int{1}},
{"GetNsTable5", []int{9}},
{"GetNsTable6", []int{3, 5, 8}},
{"GetNsTable7", []int{0, 1, 5, 6, 8, 9, 10}},
{"GetNsTable8", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
} {
t.Run(tc.name, func(t *testing.T) {
// We have no way to directly disable routes in the route table, but we
// can use the Set() command to set a table with pre-disabled routes.
testRouteTable := append([]routes.ExtendedRoute(nil), testRouteTable...)
// Disable a few routes.
for _, i := range tc.disabled {
testRouteTable[i].Enabled = false
}
var tb routes.RouteTable
tb.Set(testRouteTable)
var dummyStack stack.Stack
tb.UpdateStack(&dummyStack)
tableGot := dummyStack.GetRouteTable()
// Verify no disabled routes are in the Netstack table we got.
i := 0
for _, r := range testRouteTable {
if r.Enabled {
if got, want := tableGot[i], r.Route; got != want {
t.Errorf("got = %s, want = %s", got, want)
}
i++
}
}
})
}
}
func TestFindNIC(t *testing.T) {
for _, tc := range []struct {
name string
addr string
nicWanted tcpip.NICID // 0 means not found
}{
{"FindNic1", "127.0.0.1", 1},
{"FindNic2", "127.0.0.0", 0},
{"FindNic3", "192.168.1.234", 4},
{"FindNic4", "192.168.1.1", 4},
{"FindNic5", "192.168.2.1", 0},
{"FindNic6", "192.168.100.1", 2},
{"FindNic7", "192.168.100.10", 2},
{"FindNic8", "192.168.101.10", 0},
{"FindNic9", "10.1.2.1", 3},
{"FindNic10", "10.1.3.1", 3},
{"FindNic11", "10.1.4.1", 0},
} {
t.Run(tc.name, func(t *testing.T) {
tb := routes.RouteTable{}
tb.Set(testRouteTable)
nicGot, err := tb.FindNIC(ipStringToAddress(tc.addr))
if err != nil && tc.nicWanted > 0 {
t.Errorf("got nic = <unknown>, want = %d", tc.nicWanted)
} else if err == nil && tc.nicWanted != nicGot {
t.Errorf("got nic = %d, want = %d", nicGot, tc.nicWanted)
}
})
}
}