diff --git a/.github/workflows/conformance-e2e.yaml b/.github/workflows/conformance-e2e.yaml index 2a5f8d8c2eb8b..888acb518e541 100644 --- a/.github/workflows/conformance-e2e.yaml +++ b/.github/workflows/conformance-e2e.yaml @@ -130,6 +130,7 @@ jobs: kernel: '6.1-20240201.165956' kube-proxy: 'none' kpr: 'true' + devices: '{eth0,eth1}' tunnel: 'vxlan' lb-mode: 'snat' egress-gateway: 'true' diff --git a/pkg/datapath/linux/devices_controller_test.go b/pkg/datapath/linux/devices_controller_test.go index aa90c84ed99bb..d0b36cff1636a 100644 --- a/pkg/datapath/linux/devices_controller_test.go +++ b/pkg/datapath/linux/devices_controller_test.go @@ -7,6 +7,7 @@ package linux import ( "context" + "encoding/json" "errors" "net" "net/netip" @@ -84,15 +85,37 @@ func TestDevicesController(t *testing.T) { logging.SetLogLevelToDebug() - routeExists := func(routes []*tables.Route, linkIndex int, dst, src string) bool { + addrToString := func(addr netip.Addr) string { + if !addr.IsValid() { + return "" + } + return addr.String() + } + + routeExists := func(routes []*tables.Route, linkIndex int, dst, src, gw string) bool { for _, r := range routes { - if r.LinkIndex == linkIndex && r.Dst.String() == dst && r.Src.String() == src { + // undefined IP will stringify as "invalid IP", turn them into "". + actualDst, actualSrc, actualGw := r.Dst.String(), addrToString(r.Src), addrToString(r.Gw) + /*fmt.Printf("%d %s %s %s -- %d %s %s %s\n", + r.LinkIndex, actualDst, actualSrc, actualGw, + linkIndex, dst, src, gw)*/ + if r.LinkIndex == linkIndex && actualDst == dst && actualSrc == src && + actualGw == gw { return true } } return false } + v4Routes := func(routes []*tables.Route) (out []*tables.Route) { + for _, r := range routes { + if r.Dst.Addr().Is4() { + out = append(out, r) + } + } + return + } + orphanRoutes := func(devs []*tables.Device, routes []*tables.Route) bool { indexes := map[int]bool{} for _, dev := range devs { @@ -126,7 +149,7 @@ func TestDevicesController(t *testing.T) { devs[0].Name == "dummy0" && devs[0].Index > 0 && devs[0].Selected && - routeExists(routes, devs[0].Index, "192.168.0.0/24", "192.168.0.1") + routeExists(routes, devs[0].Index, "192.168.0.0/24", "192.168.0.1", "") }, }, { @@ -134,17 +157,21 @@ func TestDevicesController(t *testing.T) { func(t *testing.T) { // Create another dummy to check that the table updates. require.NoError(t, createDummy("dummy1", "192.168.1.1/24", false)) + + // Add a default route + assert.NoError(t, + addRoute(addRouteParams{iface: "dummy1", gw: "192.168.1.254", table: unix.RT_TABLE_MAIN})) }, func(t *testing.T, devs []*tables.Device, routes []*tables.Route) bool { // Since we're indexing by ifindex, we expect these to be in the order // they were added. return len(devs) == 2 && "dummy0" == devs[0].Name && - routeExists(routes, devs[0].Index, "192.168.0.0/24", "192.168.0.1") && + routeExists(routes, devs[0].Index, "192.168.0.0/24", "192.168.0.1", "") && devs[0].Selected && "dummy1" == devs[1].Name && devs[1].Selected && - routeExists(routes, devs[1].Index, "192.168.1.0/24", "192.168.1.1") + routeExists(routes, devs[1].Index, "192.168.1.0/24", "192.168.1.1", "") }, }, @@ -178,32 +205,62 @@ func TestDevicesController(t *testing.T) { }, { - "delete-dummy0", + "veth-with-default-gw", func(t *testing.T) { - require.NoError(t, deleteLink("dummy0")) + // Remove default route from dummy1 + assert.NoError(t, + delRoute(addRouteParams{iface: "dummy1", gw: "192.168.1.254", table: unix.RT_TABLE_MAIN})) + + // And add one for veth0. + assert.NoError(t, + addRoute(addRouteParams{iface: "veth0", gw: "192.168.4.254", table: unix.RT_TABLE_MAIN})) }, func(t *testing.T, devs []*tables.Device, routes []*tables.Route) bool { - return len(devs) == 1 && - "dummy1" == devs[0].Name && - containsAddress(devs[0], "192.168.1.1", primaryAddress) + return len(devs) == 3 && + devs[0].Name == "dummy0" && + devs[1].Name == "dummy1" && + devs[2].Name == "veth0" && + containsAddress(devs[2], "192.168.4.1", primaryAddress) && + routeExists(routes, devs[2].Index, "0.0.0.0/0", "", "192.168.4.254") }, }, { - "veth-with-default-gw", + "check-all-v4-routes", + func(t *testing.T) {}, + func(t *testing.T, devs []*tables.Device, routes []*tables.Route) bool { + routes = v4Routes(routes) + json, _ := json.Marshal(routes) + os.WriteFile("/tmp/routes.json", json, 0644) + return routeExists(routes, devs[0].Index, "192.168.0.0/24", "192.168.0.1", "") && + routeExists(routes, devs[0].Index, "192.168.0.1/32", "192.168.0.1", "") && + routeExists(routes, devs[0].Index, "192.168.0.255/32", "192.168.0.1", "") && + + routeExists(routes, devs[1].Index, "192.168.1.0/24", "192.168.1.1", "") && + routeExists(routes, devs[1].Index, "192.168.1.1/32", "192.168.1.1", "") && + routeExists(routes, devs[1].Index, "192.168.1.2/32", "192.168.1.1", "") && + routeExists(routes, devs[1].Index, "192.168.1.255/32", "192.168.1.1", "") && + + routeExists(routes, devs[2].Index, "192.168.4.0/24", "192.168.4.1", "") && + routeExists(routes, devs[2].Index, "192.168.4.1/32", "192.168.4.1", "") && + routeExists(routes, devs[2].Index, "192.168.4.255/32", "192.168.4.1", "") && + routeExists(routes, devs[2].Index, "0.0.0.0/0", "", "192.168.4.254") && + len(routes) == 11 + }, + }, + + { + "delete-dummy0", func(t *testing.T) { - assert.NoError(t, - addRoute(addRouteParams{iface: "veth0", gw: "192.168.4.254", table: unix.RT_TABLE_MAIN})) + require.NoError(t, deleteLink("dummy0")) }, func(t *testing.T, devs []*tables.Device, routes []*tables.Route) bool { return len(devs) == 2 && - devs[0].Name == "dummy1" && - devs[0].Selected && - devs[1].Name == "veth0" && - containsAddress(devs[1], "192.168.4.1", primaryAddress) && - devs[1].Selected + "dummy1" == devs[0].Name && + "veth0" == devs[1].Name }, }, + { "bond-is-selected", func(t *testing.T) { diff --git a/pkg/datapath/linux/devices_test.go b/pkg/datapath/linux/devices_test.go index 1ced60d61b86b..cd9fdcc7addb5 100644 --- a/pkg/datapath/linux/devices_test.go +++ b/pkg/datapath/linux/devices_test.go @@ -488,6 +488,44 @@ func addRoute(p addRouteParams) error { return nil } +func delRoute(p addRouteParams) error { + link, err := netlink.LinkByName(p.iface) + if err != nil { + return err + } + + var dst *net.IPNet + if p.dst != "" { + _, dst, err = net.ParseCIDR(p.dst) + if err != nil { + return err + } + } + + var src net.IP + if p.src != "" { + src = net.ParseIP(p.src) + } + + if p.table == 0 { + p.table = unix.RT_TABLE_MAIN + } + + route := &netlink.Route{ + LinkIndex: link.Attrs().Index, + Dst: dst, + Src: src, + Gw: net.ParseIP(p.gw), + Table: p.table, + Scope: p.scope, + } + if err := netlink.RouteDel(route); err != nil { + return err + } + + return nil +} + func delRoutes(iface string) error { link, err := netlink.LinkByName(iface) if err != nil { diff --git a/pkg/datapath/tables/route.go b/pkg/datapath/tables/route.go index 35d9247d065a2..5a5e16eb1371a 100644 --- a/pkg/datapath/tables/route.go +++ b/pkg/datapath/tables/route.go @@ -4,6 +4,7 @@ package tables import ( + "encoding/binary" "fmt" "net/netip" @@ -53,12 +54,12 @@ type RouteID struct { } func (id RouteID) Key() index.Key { - key := append(index.Uint64(uint64(id.Table)), '+') - key = append(key, index.Uint64(uint64(id.Table))...) - key = append(key, '+') - key = append(key, index.NetIPPrefix(id.Dst)...) - key = append(key, 0 /* termination */) - return key + key := make([]byte, 0, 4 /* table */ +4 /* link */ +17 /* prefix & bits */) + key = binary.BigEndian.AppendUint32(key, uint32(id.Table)) + key = binary.BigEndian.AppendUint32(key, uint32(id.LinkIndex)) + addrBytes := id.Dst.Addr().As16() + key = append(key, addrBytes[:]...) + return append(key, uint8(id.Dst.Bits())) } type Route struct {