mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
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:
committed by
gVisor bot
parent
88bbeb404a
commit
fd95313e83
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user