diff --git a/pkg/tcpip/stack/addressable_endpoint_state.go b/pkg/tcpip/stack/addressable_endpoint_state.go index 91b615eb6..e1c151ebf 100644 --- a/pkg/tcpip/stack/addressable_endpoint_state.go +++ b/pkg/tcpip/stack/addressable_endpoint_state.go @@ -18,6 +18,7 @@ import ( "fmt" "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" ) func (lifetimes *AddressLifetimes) sanitize() { @@ -433,7 +434,7 @@ func (a *AddressableEndpointState) MainAddress() tcpip.AddressWithPrefix { a.mu.RLock() defer a.mu.RUnlock() - ep := a.acquirePrimaryAddressRLocked(func(ep *addressState) bool { + ep := a.acquirePrimaryAddressRLocked(tcpip.Address{}, func(ep *addressState) bool { switch kind := ep.GetKind(); kind { case Permanent: return a.networkEndpoint.Enabled() || !a.options.HiddenWhileDisabled @@ -461,7 +462,30 @@ func (a *AddressableEndpointState) MainAddress() tcpip.AddressWithPrefix { // valid according to isValid. // // +checklocksread:a.mu -func (a *AddressableEndpointState) acquirePrimaryAddressRLocked(isValid func(*addressState) bool) *addressState { +func (a *AddressableEndpointState) acquirePrimaryAddressRLocked(remoteAddr tcpip.Address, isValid func(*addressState) bool) *addressState { + // TODO: Move this out into IPv4-specific code. + // IPv6 handles source IP selection elsewhere. We have to do source + // selection only for IPv4, in which case ep is never deprecated. Thus + // we don't have to worry about refcounts. + if remoteAddr.Len() == header.IPv4AddressSize && remoteAddr != (tcpip.Address{}) { + var best *addressState + var bestLen uint8 + for _, state := range a.primary { + if !isValid(state) { + continue + } + stateLen := state.addr.Address.MatchingPrefix(remoteAddr) + if best == nil || bestLen < stateLen { + best = state + bestLen = stateLen + } + } + if best != nil { + best.IncRef() + } + return best + } + var deprecatedEndpoint *addressState for _, ep := range a.primary { if !isValid(ep) { @@ -599,7 +623,7 @@ func (a *AddressableEndpointState) AcquireOutgoingPrimaryAddress(remoteAddr tcpi a.mu.Lock() defer a.mu.Unlock() - ep := a.acquirePrimaryAddressRLocked(func(ep *addressState) bool { + ep := a.acquirePrimaryAddressRLocked(remoteAddr, func(ep *addressState) bool { return ep.IsAssigned(allowExpired) }) diff --git a/pkg/tcpip/stack/route.go b/pkg/tcpip/stack/route.go index 9755362af..9b921eaf4 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 7df71fa23..16bb05119 100644 --- a/pkg/tcpip/stack/stack_test.go +++ b/pkg/tcpip/stack/stack_test.go @@ -841,11 +841,11 @@ func testRoute(t *testing.T, s *stack.Stack, nic tcpip.NICID, srcAddr, dstAddr, defer r.Release() if r.LocalAddress() != expectedSrcAddr { - t.Fatalf("got Route.LocalAddress() = %s, want = %s", expectedSrcAddr, r.LocalAddress()) + t.Fatalf("got Route.LocalAddress() = %s, want = %s", r.LocalAddress(), expectedSrcAddr) } if r.RemoteAddress() != dstAddr { - t.Fatalf("got Route.RemoteAddress() = %s, want = %s", dstAddr, r.RemoteAddress()) + t.Fatalf("got Route.RemoteAddress() = %s, want = %s", r.RemoteAddress(), dstAddr) } } @@ -1310,7 +1310,7 @@ func TestRoutes(t *testing.T) { testRoute(t, s, 1, tcpip.AddrFromSlice([]byte("\x03\x00\x00\x00")), tcpip.AddrFromSlice([]byte("\x05\x00\x00\x00")), tcpip.AddrFromSlice([]byte("\x03\x00\x00\x00"))) // Test routes to even address. - testRoute(t, s, 0, tcpip.Address{}, tcpip.AddrFromSlice([]byte("\x06\x00\x00\x00")), tcpip.AddrFromSlice([]byte("\x02\x00\x00\x00"))) + testRoute(t, s, 0, tcpip.Address{}, tcpip.AddrFromSlice([]byte("\x06\x00\x00\x00")), tcpip.AddrFromSlice([]byte("\x04\x00\x00\x00"))) testRoute(t, s, 0, tcpip.AddrFromSlice([]byte("\x02\x00\x00\x00")), tcpip.AddrFromSlice([]byte("\x06\x00\x00\x00")), tcpip.AddrFromSlice([]byte("\x02\x00\x00\x00"))) testRoute(t, s, 2, tcpip.AddrFromSlice([]byte("\x02\x00\x00\x00")), tcpip.AddrFromSlice([]byte("\x06\x00\x00\x00")), tcpip.AddrFromSlice([]byte("\x02\x00\x00\x00"))) testRoute(t, s, 0, tcpip.AddrFromSlice([]byte("\x04\x00\x00\x00")), tcpip.AddrFromSlice([]byte("\x06\x00\x00\x00")), tcpip.AddrFromSlice([]byte("\x04\x00\x00\x00"))) @@ -5461,3 +5461,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()