From 925904e24eb381e8022696b692169c183dc1c1ad Mon Sep 17 00:00:00 2001 From: Kevin Krakauer Date: Wed, 8 Nov 2023 12:49:22 -0800 Subject: [PATCH] netstack: add IP source selection test that was rolled back See cl/557941259 for the original. PiperOrigin-RevId: 580624680 --- pkg/tcpip/stack/route.go | 5 + pkg/tcpip/stack/stack_test.go | 204 +++++++++++++++++++++++++++++++++ pkg/tcpip/testutil/testutil.go | 20 ++++ 3 files changed, 229 insertions(+) diff --git a/pkg/tcpip/stack/route.go b/pkg/tcpip/stack/route.go index 32ce9a7f0..4b0f52e55 100644 --- a/pkg/tcpip/stack/route.go +++ b/pkg/tcpip/stack/route.go @@ -101,6 +101,11 @@ func (r *Route) Loop() PacketLooping { return r.routeInfo.Loop } +// OutgoingNIC returns the route's outgoing NIC. +func (r *Route) OutgoingNIC() tcpip.NICID { + return r.outgoingNIC.id +} + // RouteInfo contains all of Route's exported fields. // // +stateify savable diff --git a/pkg/tcpip/stack/stack_test.go b/pkg/tcpip/stack/stack_test.go index b389e82c6..e9ef5ded2 100644 --- a/pkg/tcpip/stack/stack_test.go +++ b/pkg/tcpip/stack/stack_test.go @@ -5546,3 +5546,207 @@ func TestStaticGetLinkAddress(t *testing.T) { }) } } + +// TODO(b/221146133): Test with: +// - Multiple NICs +// - Gateway first route tables +// - IPv6 +// - Set local address +// - Set NIC +func TestFindRoute(t *testing.T) { + // Just use a consistent prefix length throughout tests for simplicity. + const prefixLen = 24 + + type nic struct { + id tcpip.NICID + addresses []string + } + type route struct { + gateway string + subnet string + nic tcpip.NICID + } + type query struct { + name string + remote string + wantID tcpip.NICID + wantLocal string + wantNextHop string + wantErr bool + } + stacks := []struct { + name string + nics []nic + routes []route + queries []query + }{ + { + name: "one NIC, multiple addresses, partially overlapping addresses", + nics: []nic{{id: 1, addresses: []string{"169.254.9.1", "169.254.169.1"}}}, + routes: []route{ + {gateway: "1.1.1.1", subnet: "0.0.0.0/0", nic: 1}, + {gateway: "1.1.1.1", subnet: "169.254.169.0/25", nic: 1}, + }, + queries: []query{ + { + name: "match default only", + remote: "2.2.2.2", + wantID: 1, + wantLocal: "169.254.9.1", + wantNextHop: "1.1.1.1", + }, + { + name: "match both, but prefer non-default", + remote: "169.254.169.2", + wantID: 1, + wantLocal: "169.254.169.1", + wantNextHop: "1.1.1.1", + }, + }, + }, + { + name: "one NIC, multiple addresses, addresses swapped", + nics: []nic{{id: 1, addresses: []string{"192.168.2.1", "192.168.1.1"}}}, + routes: []route{ + {gateway: "192.168.2.22", subnet: "192.168.2.0/24", nic: 1}, + {gateway: "192.168.1.11", subnet: "0.0.0.0/0", nic: 1}, + }, + queries: []query{ + { + name: "match default only", + remote: "1.1.1.1", + wantID: 1, + wantLocal: "192.168.2.1", + wantNextHop: "192.168.1.11", + }, + { + name: "match both, but prefer non-default", + remote: "192.168.2.2", + wantID: 1, + wantLocal: "192.168.2.1", + wantNextHop: "192.168.2.22", + }, + }, + }, + { + name: "one NIC, multiple addresses, gateway last", + nics: []nic{{id: 1, addresses: []string{"192.168.1.1", "192.168.2.1"}}}, + routes: []route{ + {gateway: "192.168.2.22", subnet: "192.168.2.0/24", nic: 1}, + {gateway: "192.168.1.11", subnet: "0.0.0.0/0", nic: 1}, + }, + queries: []query{ + { + name: "match default only", + remote: "1.1.1.1", + wantID: 1, + wantLocal: "192.168.1.1", + wantNextHop: "192.168.1.11", + }, + { + name: "match both, but prefer non-default", + remote: "192.168.2.2", + wantID: 1, + wantLocal: "192.168.2.1", + wantNextHop: "192.168.2.22", + }, + }, + }, + { + name: "one NIC, multiple addresses, no default gateway", + nics: []nic{{id: 1, addresses: []string{"192.168.1.1", "192.168.2.1"}}}, + routes: []route{ + {gateway: "192.168.2.22", subnet: "192.168.2.0/24", nic: 1}, + {gateway: "192.168.1.11", subnet: "192.168.1.0/24", nic: 1}, + }, + queries: []query{ + { + name: "match single A", + remote: "192.168.1.2", + wantID: 1, + wantLocal: "192.168.1.1", + wantNextHop: "192.168.1.11", + }, + { + name: "match single B", + remote: "192.168.2.2", + wantID: 1, + wantLocal: "192.168.2.1", + wantNextHop: "192.168.2.22", + }, + { + name: "match none", + remote: "3.3.3.3", + wantErr: true, + }, + }, + }, + } + + for _, stackConfig := range stacks { + t.Run(stackConfig.name, func(t *testing.T) { + // Create the stack. The channel endpoint is unused, but necessary for creation. + ep := channel.New(1, defaultMTU, "") + stk := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol /*, arp.NewProtocol*/}, + }) + + // Create NICs and assign addresses to them. + for _, nic := range stackConfig.nics { + if err := stk.CreateNIC(nic.id, ep); err != nil { + t.Fatal("NewNIC failed:", err) + } + for _, addr := range nic.addresses { + protocolAddr := tcpip.ProtocolAddress{ + Protocol: header.IPv4ProtocolNumber, + AddressWithPrefix: tcpip.AddressWithPrefix{ + Address: testutil.MustParse4(addr), + PrefixLen: prefixLen, + }, + } + if err := stk.AddProtocolAddress(nic.id, protocolAddr, stack.AddressProperties{}); err != nil { + t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", 1, protocolAddr, err) + } + } + } + + // Setup the route table. + var routeTable []tcpip.Route + for _, route := range stackConfig.routes { + routeTable = append(routeTable, tcpip.Route{ + Destination: testutil.MustParseSubnet4(route.subnet), + Gateway: testutil.MustParse4(route.gateway), + NIC: route.nic, + }) + } + stk.SetRouteTable(routeTable) + + for _, query := range stackConfig.queries { + t.Run(query.name, func(t *testing.T) { + route, err := stk.FindRoute( + 0, + tcpip.Address{}, + testutil.MustParse4(query.remote), + header.IPv4ProtocolNumber, + false, /* multicastLoop */ + ) + if err != nil { + if _, ok := err.(*tcpip.ErrHostUnreachable); query.wantErr && ok { + return + } + t.Fatalf("FoundRoute failed: %v", err) + } + if got, want := route.OutgoingNIC(), query.wantID; got != want { + t.Errorf("got outgoing NIC %d, but wanted %d", got, want) + } + if got, want := route.LocalAddress(), testutil.MustParse4(query.wantLocal); got != want { + t.Errorf("got local address %s, but wanted %s", got, want) + } + if got, want := route.NextHop(), testutil.MustParse4(query.wantNextHop); got != want { + t.Errorf("got next hop %s, but wanted %s", got, want) + } + }) + } + }) + } +} diff --git a/pkg/tcpip/testutil/testutil.go b/pkg/tcpip/testutil/testutil.go index 6455d1f82..53ce839bb 100644 --- a/pkg/tcpip/testutil/testutil.go +++ b/pkg/tcpip/testutil/testutil.go @@ -19,6 +19,7 @@ import ( "fmt" "net" "reflect" + "strconv" "strings" "gvisor.dev/gvisor/pkg/tcpip" @@ -44,6 +45,25 @@ func MustParse6(addr string) tcpip.Address { return tcpip.AddrFrom16Slice(ip) } +// MustParseSubnet4 parses an IPv4 subnet string (e.g. "192.168.1.0/24") into a +// tcpip.Subnet. +func MustParseSubnet4(subnet string) tcpip.Subnet { + parts := strings.Split(subnet, "/") + if len(parts) != 2 { + panic(fmt.Sprintf("MustParseSubnet4 expected CIDR notation (/), but got %q", subnet)) + } + addr := MustParse4(parts[0]) + prefixLen, err := strconv.Atoi(parts[1]) + if err != nil { + panic(fmt.Sprintf("Failed to parse prefix length %q: %v", parts[1], err)) + } + if prefixLen < 0 || prefixLen > 32 { + panic(fmt.Sprintf("Prefix length %d is invalid. It must be between 0 and 32", prefixLen)) + } + prefixed := tcpip.AddressWithPrefix{Address: addr, PrefixLen: prefixLen} + return prefixed.Subnet() +} + func checkFieldCounts(ref, multi reflect.Value) error { refTypeName := ref.Type().Name() multiTypeName := multi.Type().Name()