Files
kata-containers/src/runtime/virtcontainers/network_linux_test.go
Saul Paredes 83bbfedc08 network: preseed default-gateway neighbor
This change mirrors host networking into the guest as before, but now also
includes the default gateway neighbor entry for each interface.

Pods using overlay/synthetic gateways (e.g., 169.254.1.1) can hit a
first-connect race while the guest performs the initial ARP. Preseeding the
gateway neighbor removes that latency and makes early connections (e.g.,
to the API Service) deterministic.

Signed-off-by: Saul Paredes <saulparedes@microsoft.com>
2026-04-20 10:00:19 -07:00

617 lines
16 KiB
Go

// Copyright (c) 2016 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//
package virtcontainers
import (
"context"
"encoding/json"
"net"
"os"
"reflect"
"testing"
"golang.org/x/sys/unix"
"github.com/containernetworking/plugins/pkg/ns"
ktu "github.com/kata-containers/kata-containers/src/runtime/pkg/katatestutils"
pbTypes "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/agent/protocols"
vctypes "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/types"
"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/utils"
"github.com/stretchr/testify/assert"
"github.com/vishvananda/netlink"
)
func TestGenerateInterfacesAndRoutes(t *testing.T) {
//
//Create a couple of addresses
//
address1 := &net.IPNet{IP: net.IPv4(172, 17, 0, 2), Mask: net.CIDRMask(16, 32)}
address2 := &net.IPNet{IP: net.IPv4(182, 17, 0, 2), Mask: net.CIDRMask(16, 32)}
address3 := &net.IPNet{IP: net.ParseIP("2001:db8:1::242:ac11:2"), Mask: net.CIDRMask(64, 128)}
addrs := []netlink.Addr{
{IPNet: address1, Label: "phyaddr1"},
{IPNet: address2, Label: "phyaddr2"},
{IPNet: address3, Label: "phyaddr3"},
}
// Create a couple of routes:
dst2 := &net.IPNet{IP: net.IPv4(172, 17, 0, 0), Mask: net.CIDRMask(16, 32)}
src2 := net.IPv4(172, 17, 0, 2)
gw2 := net.IPv4(172, 17, 0, 1)
dstV6 := &net.IPNet{IP: net.ParseIP("2001:db8:1::"), Mask: net.CIDRMask(64, 128)}
gatewayV6 := net.ParseIP("2001:db8:1::1")
routes := []netlink.Route{
{LinkIndex: 329, Dst: nil, Src: nil, Gw: net.IPv4(172, 17, 0, 1), Scope: netlink.Scope(254)},
{LinkIndex: 329, Dst: dst2, Src: src2, Gw: gw2},
{LinkIndex: 329, Dst: dstV6, Src: nil, Gw: nil},
{LinkIndex: 329, Dst: nil, Src: nil, Gw: gatewayV6},
}
arpMAC, _ := net.ParseMAC("6a:92:3a:59:70:aa")
neighs := []netlink.Neigh{
{LinkIndex: 329, IP: net.IPv4(192, 168, 0, 101), State: netlink.NUD_PERMANENT, HardwareAddr: arpMAC},
}
networkInfo := NetworkInfo{
Iface: NetlinkIface{
LinkAttrs: netlink.LinkAttrs{MTU: 1500},
Type: "",
},
Addrs: addrs,
Routes: routes,
Neighbors: neighs,
}
ep0 := &PhysicalEndpoint{
IfaceName: "eth0",
HardAddr: net.HardwareAddr{0x02, 0x00, 0xca, 0xfe, 0x00, 0x04}.String(),
EndpointProperties: networkInfo,
}
endpoints := []Endpoint{ep0}
nns, err := NewNetwork(&NetworkConfig{NetworkID: "foobar", NetworkCreated: true})
assert.Nil(t, err)
nns.SetEndpoints(endpoints)
resInterfaces, resRoutes, resNeighs, err := generateVCNetworkStructures(context.Background(), nns.Endpoints())
//
// Build expected results:
//
expectedAddresses := []*pbTypes.IPAddress{
{Family: utils.ConvertAddressFamily(netlink.FAMILY_V4), Address: "172.17.0.2", Mask: "16"},
{Family: utils.ConvertAddressFamily(netlink.FAMILY_V4), Address: "182.17.0.2", Mask: "16"},
{Family: utils.ConvertAddressFamily(netlink.FAMILY_V6), Address: "2001:db8:1::242:ac11:2", Mask: "64"},
}
expectedInterfaces := []*pbTypes.Interface{
{Device: "eth0", Name: "eth0", IPAddresses: expectedAddresses, Mtu: 1500, HwAddr: "02:00:ca:fe:00:04"},
}
expectedRoutes := []*pbTypes.Route{
{Dest: "", Gateway: "172.17.0.1", Device: "eth0", Source: "", Scope: uint32(254)},
{Dest: "172.17.0.0/16", Gateway: "172.17.0.1", Device: "eth0", Source: "172.17.0.2"},
{Dest: "2001:db8:1::/64", Gateway: "", Device: "eth0", Source: ""},
{Dest: "", Gateway: "2001:db8:1::1", Device: "eth0", Source: ""},
}
expectedNeighs := []*pbTypes.ARPNeighbor{
{
Device: "eth0",
State: netlink.NUD_PERMANENT,
Lladdr: "6a:92:3a:59:70:aa",
ToIPAddress: &pbTypes.IPAddress{Address: "192.168.0.101", Family: utils.ConvertAddressFamily(netlink.FAMILY_V4)},
},
}
assert.Nil(t, err, "unexpected failure when calling generateKataInterfacesAndRoutes")
assert.True(t, reflect.DeepEqual(resInterfaces, expectedInterfaces),
"Interfaces returned didn't match: got %+v, expecting %+v", resInterfaces, expectedInterfaces)
assert.True(t, reflect.DeepEqual(resRoutes, expectedRoutes),
"Routes returned didn't match: got %+v, expecting %+v", resRoutes, expectedRoutes)
assert.True(t, reflect.DeepEqual(resNeighs, expectedNeighs),
"ARP Neighbors returned didn't match: got %+v, expecting %+v", resNeighs, expectedNeighs)
}
func TestCreateGetTunTapLink(t *testing.T) {
if tc.NotValid(ktu.NeedRoot()) {
t.Skip(testDisabledAsNonRoot)
}
assert := assert.New(t)
netHandle, err := netlink.NewHandle()
assert.NoError(err)
defer netHandle.Close()
assert.NoError(err)
tapName := "testtap0"
tapLink, fds, err := createLink(netHandle, tapName, &netlink.Tuntap{}, 1)
assert.NoError(err)
assert.NotNil(tapLink)
assert.NotZero(len(fds))
tapLink, err = getLinkByName(netHandle, tapName, &netlink.Tuntap{})
assert.NoError(err)
err = netHandle.LinkDel(tapLink)
assert.NoError(err)
}
func TestCreateMacVtap(t *testing.T) {
if tc.NotValid(ktu.NeedRoot()) {
t.Skip(testDisabledAsNonRoot)
}
assert := assert.New(t)
netHandle, err := netlink.NewHandle()
assert.NoError(err)
defer netHandle.Close()
assert.NoError(err)
tapName := "testtap0"
tapLink, _, err := createLink(netHandle, tapName, &netlink.Tuntap{}, 1)
assert.NoError(err)
attrs := tapLink.Attrs()
mcLink := &netlink.Macvtap{
Macvlan: netlink.Macvlan{
LinkAttrs: netlink.LinkAttrs{
TxQLen: attrs.TxQLen,
ParentIndex: attrs.Index,
},
},
}
macvtapName := "testmc0"
_, err = createMacVtap(netHandle, macvtapName, mcLink, 1)
assert.NoError(err)
macvtapLink, err := getLinkByName(netHandle, macvtapName, &netlink.Macvtap{})
assert.NoError(err)
err = netHandle.LinkDel(macvtapLink)
assert.NoError(err)
tapLink, err = getLinkByName(netHandle, tapName, &netlink.Tuntap{})
assert.NoError(err)
err = netHandle.LinkDel(tapLink)
assert.NoError(err)
}
func TestTcRedirectNetwork(t *testing.T) {
if tc.NotValid(ktu.NeedRoot()) {
t.Skip(testDisabledAsNonRoot)
}
assert := assert.New(t)
netHandle, err := netlink.NewHandle()
assert.NoError(err)
defer netHandle.Close()
// Create a test veth interface.
vethName := "foo"
veth := &netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: vethName, TxQLen: 200, MTU: 1400}, PeerName: "bar"}
err = netlink.LinkAdd(veth)
assert.NoError(err)
endpoint, err := createVethNetworkEndpoint(1, vethName, NetXConnectTCFilterModel)
assert.NoError(err)
link, err := netlink.LinkByName(vethName)
assert.NoError(err)
err = netHandle.LinkSetUp(link)
assert.NoError(err)
err = setupTCFiltering(context.Background(), endpoint, 1, true)
assert.NoError(err)
err = removeTCFiltering(context.Background(), endpoint)
assert.NoError(err)
// Remove the veth created for testing.
err = netHandle.LinkDel(link)
assert.NoError(err)
}
func TestRxRateLimiter(t *testing.T) {
if tc.NotValid(ktu.NeedRoot()) {
t.Skip(testDisabledAsNonRoot)
}
assert := assert.New(t)
netHandle, err := netlink.NewHandle()
assert.NoError(err)
defer netHandle.Close()
// Create a test veth interface.
vethName := "foo"
veth := &netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: vethName, TxQLen: 200, MTU: 1400}, PeerName: "bar"}
err = netlink.LinkAdd(veth)
assert.NoError(err)
endpoint, err := createVethNetworkEndpoint(1, vethName, NetXConnectTCFilterModel)
assert.NoError(err)
link, err := netlink.LinkByName(vethName)
assert.NoError(err)
err = netHandle.LinkSetUp(link)
assert.NoError(err)
err = setupTCFiltering(context.Background(), endpoint, 1, true)
assert.NoError(err)
// 10Mb
maxRate := uint64(10000000)
err = addRxRateLimiter(endpoint, maxRate)
assert.NoError(err)
currentNS, err := ns.GetCurrentNS()
assert.NoError(err)
err = removeRxRateLimiter(endpoint, currentNS.Path())
assert.NoError(err)
err = removeTCFiltering(context.Background(), endpoint)
assert.NoError(err)
// Remove the veth created for testing.
err = netHandle.LinkDel(link)
assert.NoError(err)
}
func TestTxRateLimiter(t *testing.T) {
if tc.NotValid(ktu.NeedRoot()) {
t.Skip(testDisabledAsNonRoot)
}
assert := assert.New(t)
netHandle, err := netlink.NewHandle()
assert.NoError(err)
defer netHandle.Close()
// Create a test veth interface.
vethName := "foo"
veth := &netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: vethName, TxQLen: 200, MTU: 1400}, PeerName: "bar"}
err = netlink.LinkAdd(veth)
assert.NoError(err)
endpoint, err := createVethNetworkEndpoint(1, vethName, NetXConnectTCFilterModel)
assert.NoError(err)
link, err := netlink.LinkByName(vethName)
assert.NoError(err)
err = netHandle.LinkSetUp(link)
assert.NoError(err)
err = setupTCFiltering(context.Background(), endpoint, 1, true)
assert.NoError(err)
// 10Mb
maxRate := uint64(10000000)
err = addTxRateLimiter(endpoint, maxRate)
assert.NoError(err)
currentNS, err := ns.GetCurrentNS()
assert.NoError(err)
err = removeTxRateLimiter(endpoint, currentNS.Path())
assert.NoError(err)
err = removeTCFiltering(context.Background(), endpoint)
assert.NoError(err)
// Remove the veth created for testing.
err = netHandle.LinkDel(link)
assert.NoError(err)
}
func TestConvertDanDeviceToNetworkInfo(t *testing.T) {
jsonData, err := os.ReadFile("testdata/dan-config.json")
assert.NoError(t, err)
var config vctypes.DanConfig
err = json.Unmarshal([]byte(jsonData), &config)
assert.NoError(t, err)
ni, err := convertDanDeviceToNetworkInfo(&config.Devices[0])
assert.NoError(t, err)
assert.Equal(t, 1500, ni.Iface.MTU)
assert.Len(t, ni.Addrs, 1)
assert.Equal(t, "10.10.0.5/24", ni.Addrs[0].String())
dest, _ := netlink.ParseIPNet("10.10.0.0/16")
dest.IP = dest.IP.To4()
routes := []netlink.Route{
{Family: unix.AF_INET, Dst: nil, Gw: net.ParseIP("10.0.0.1"), Src: nil, Scope: 0},
{Family: unix.AF_INET, Dst: dest, Gw: net.ParseIP("10.0.0.1"), Src: nil, Scope: 0},
}
assert.Equal(t, routes, ni.Routes)
neighMac, _ := net.ParseMAC("0a:58:0a:0a:0a:0a")
neigh := netlink.Neigh{
HardwareAddr: neighMac,
IP: net.ParseIP("10.10.10.10"),
}
assert.Len(t, ni.Neighbors, 1)
assert.Equal(t, neigh, ni.Neighbors[0])
}
func TestAddEndpoints_Dan(t *testing.T) {
network := &LinuxNetwork{
netNSPath: "net-123",
eps: []Endpoint{},
interworkingModel: NetXConnectDefaultModel,
netNSCreated: true,
danConfigPath: "testdata/dan-config.json",
}
ctx := context.TODO()
eps, err := network.AddEndpoints(ctx, nil, nil, true)
assert.NoError(t, err)
assert.Len(t, eps, 1)
ep := eps[0]
assert.Equal(t, ep.Name(), "eth0")
assert.Equal(t, ep.HardwareAddr(), "0a:58:0a:0a:00:05")
assert.Equal(t, ep.Type(), VfioEndpointType)
assert.Equal(t, ep.PciPath().String(), "")
}
func TestValidGuestNeighbor(t *testing.T) {
assert := assert.New(t)
// Setup gateway set with a known gateway IP
gatewaySet := map[string]struct{}{
"10.0.0.1": {},
}
neighborMAC, _ := net.ParseMAC("aa:bb:cc:dd:ee:ff")
tests := []struct {
name string
neighbor netlink.Neigh
gateways map[string]struct{}
expected bool
desc string
}{
{
name: "PermanentNeighborAlwaysValid",
neighbor: netlink.Neigh{
IP: net.ParseIP("192.168.1.1"),
HardwareAddr: neighborMAC,
State: netlink.NUD_PERMANENT,
},
gateways: gatewaySet,
expected: true,
desc: "PERMANENT neighbors should always be included regardless of gateway status",
},
{
name: "NonPermanentGatewayReachable",
neighbor: netlink.Neigh{
IP: net.ParseIP("10.0.0.1"),
HardwareAddr: neighborMAC,
State: netlink.NUD_REACHABLE,
},
gateways: gatewaySet,
expected: true,
desc: "Non-PERMANENT gateway neighbor in REACHABLE state should be included",
},
{
name: "NonPermanentGatewayNotReachable",
neighbor: netlink.Neigh{
IP: net.ParseIP("10.0.0.1"),
HardwareAddr: neighborMAC,
State: netlink.NUD_STALE,
},
gateways: gatewaySet,
expected: false,
desc: "Non-PERMANENT gateway neighbor in STALE state should be filtered out",
},
{
name: "NonPermanentNonGateway",
neighbor: netlink.Neigh{
IP: net.ParseIP("192.168.1.1"),
HardwareAddr: neighborMAC,
State: netlink.NUD_REACHABLE,
},
gateways: gatewaySet,
expected: false,
desc: "Non-PERMANENT non-gateway neighbor should be filtered out even if REACHABLE",
},
{
name: "MissingHardwareAddr",
neighbor: netlink.Neigh{
IP: net.ParseIP("10.0.0.1"),
State: netlink.NUD_REACHABLE,
},
gateways: gatewaySet,
expected: false,
desc: "Any neighbor without a MAC address should be filtered out",
},
{
name: "PermanentNeighborWithoutMAC",
neighbor: netlink.Neigh{
IP: net.ParseIP("10.0.0.1"),
State: netlink.NUD_PERMANENT,
},
gateways: gatewaySet,
expected: false,
desc: "Even PERMANENT neighbors need a MAC address",
},
{
name: "OtherTransientStateAsGateway",
neighbor: netlink.Neigh{
IP: net.ParseIP("10.0.0.1"),
HardwareAddr: neighborMAC,
State: netlink.NUD_DELAY,
},
gateways: gatewaySet,
expected: false,
desc: "Transient states like DELAY should be filtered even for gateways",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := validGuestNeighbor(tc.neighbor, tc.gateways)
assert.Equal(tc.expected, result, tc.desc)
})
}
}
func TestGatewaySetFromRoutes(t *testing.T) {
assert := assert.New(t)
gwIPv4 := net.ParseIP("10.0.0.1")
gwIPv6 := net.ParseIP("fe80::1")
nonGwIP := net.ParseIP("192.168.1.1")
tests := []struct {
name string
routes []netlink.Route
expectedGateways map[string]struct{}
desc string
}{
{
name: "DefaultRouteIPv4",
routes: []netlink.Route{
{
Dst: nil, // nil destination means default route
Gw: gwIPv4,
},
},
expectedGateways: map[string]struct{}{
"10.0.0.1": {},
},
desc: "IPv4 default route (nil destination) should add gateway to set",
},
{
name: "DefaultRouteIPv6",
routes: []netlink.Route{
{
Dst: nil,
Gw: gwIPv6,
},
},
expectedGateways: map[string]struct{}{
"fe80::1": {},
},
desc: "IPv6 default route (nil destination) should add gateway to set",
},
{
name: "ExplicitDefaultRouteIPv4",
routes: []netlink.Route{
{
Dst: &net.IPNet{
IP: net.IPv4(0, 0, 0, 0),
Mask: net.CIDRMask(0, 32),
},
Gw: gwIPv4,
},
},
expectedGateways: map[string]struct{}{
"10.0.0.1": {},
},
desc: "Explicit IPv4 default route (0.0.0.0/0) should add gateway to set",
},
{
name: "ExplicitDefaultRouteIPv6",
routes: []netlink.Route{
{
Dst: &net.IPNet{
IP: net.ParseIP("::"),
Mask: net.CIDRMask(0, 128),
},
Gw: gwIPv6,
},
},
expectedGateways: map[string]struct{}{
"fe80::1": {},
},
desc: "Explicit IPv6 default route (::/0) should add gateway to set",
},
{
name: "NonDefaultRouteFiltered",
routes: []netlink.Route{
{
Dst: &net.IPNet{
IP: net.IPv4(172, 16, 0, 0),
Mask: net.CIDRMask(16, 32),
},
Gw: nonGwIP,
},
},
expectedGateways: map[string]struct{}{},
desc: "Non-default routes should not add gateway to set",
},
{
name: "RouteWithoutGateway",
routes: []netlink.Route{
{
Dst: nil,
Gw: nil,
},
},
expectedGateways: map[string]struct{}{},
desc: "Routes without a gateway should be skipped",
},
{
name: "MultipleDefaultRoutes",
routes: []netlink.Route{
{
Dst: nil,
Gw: gwIPv4,
},
{
Dst: nil,
Gw: gwIPv6,
},
{
Dst: &net.IPNet{
IP: net.IPv4(172, 16, 0, 0),
Mask: net.CIDRMask(16, 32),
},
Gw: nonGwIP,
},
},
expectedGateways: map[string]struct{}{
"10.0.0.1": {},
"fe80::1": {},
},
desc: "Multiple default routes should populate the set; non-default routes should be ignored",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := gatewaySetFromRoutes(tc.routes)
assert.Equal(tc.expectedGateways, result, tc.desc)
})
}
}