netstack: use longest prefix match to choose IPs on a NIC

Once a NIC is chosen for a route, we currently just grab the first IP on the
NIC as the source address. We should choose the address with the longest
matching prefix instead.

This is a simple linear search of the route table. That's slow for large
tables, but large tables are exceedingly rare for use with gVisor and we can
always reimplement this with a trie if it becomes necessary.

PiperOrigin-RevId: 557941259
This commit is contained in:
Kevin Krakauer
2023-08-17 14:45:53 -07:00
committed by gVisor bot
parent 88bbeb404a
commit fd95313e83
4 changed files with 259 additions and 6 deletions
+27 -3
View File
@@ -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)
})
+5
View File
@@ -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
+207 -3
View File
@@ -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)
}
})
}
})
}
}
+20
View File
@@ -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 (<addr>/<prefixLen>), 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()