Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions internal/cni/bgp.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,28 @@ func publishBGPState(
})
}

// routeConflicts reports whether an existing route conflicts with the desired
// pod-subnet route. A conflict occurs when the destination matches but the
// gateway or link index differs.
func routeConflicts(existing, desired *netlink.Route) bool {
if existing.Dst == nil || desired.Dst == nil {
return false
}
if existing.Dst.String() != desired.Dst.String() {
return false
}
if (existing.Gw != nil) != (desired.Gw != nil) {
return true
}
if existing.Gw != nil && !existing.Gw.Equal(desired.Gw) {
return true
}
if existing.LinkIndex != 0 && desired.LinkIndex != 0 && existing.LinkIndex != desired.LinkIndex {
return true
}
return false
}

// configureHostVethGateway assigns the gateway address as a /128 host address on
// the host-side veth and installs an explicit pod-subnet route into the VRF table.
//
Expand Down Expand Up @@ -314,11 +336,42 @@ func configureHostVethGateway(vpc, vpcAttachment string, res *ipamResult) error
if err != nil {
return fmt.Errorf("get VRF table ID for pod subnet route: %w", err)
}
if err := netlink.RouteReplace(&netlink.Route{
desiredRoute := &netlink.Route{
Dst: res.subnet,
LinkIndex: hostLink.Attrs().Index,
Table: int(tableID),
}); err != nil {
}

// Check for existing routes with the same destination before installing.
existingRoutes, err := netlink.RouteListFiltered(
netlink.FAMILY_V6,
&netlink.Route{Table: int(tableID)},
netlink.RT_FILTER_TABLE,
)
if err != nil {
return fmt.Errorf("list routes in VRF table: %w", err)
}
for _, r := range existingRoutes {
if r.Dst == nil {
continue
}
if r.Dst.String() != desiredRoute.Dst.String() {
continue
}
if routeConflicts(&r, desiredRoute) {
return fmt.Errorf(
"existing route %v to %s conflicts with desired route %v",
r, desiredRoute.Dst, desiredRoute,
)
}
// Route already exists with matching attributes — idempotent, skip.
return nil
}

if err := netlink.RouteAdd(desiredRoute); err != nil {
if errors.Is(err, syscall.EEXIST) {
return nil // already installed by a concurrent caller
}
return fmt.Errorf("add pod subnet route to VRF table: %w", err)
}
return nil
Expand Down
104 changes: 104 additions & 0 deletions internal/cni/bgp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2025 Datum Cloud, Inc.
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package cni

import (
"net"
"testing"

"github.com/vishvananda/netlink"
)

// ---- routeConflicts ------------------------------------------------------

func TestRouteConflicts(t *testing.T) {
dst := mustParseCIDR(t, "fd00:10:ff01::1234/80")
gw1 := net.ParseIP("fd00:10:ff01::1")
gw2 := net.ParseIP("fd00:10:ff01::2")
otherDst := mustParseCIDR(t, "fd00:10:ff02::1234/80")

tests := []struct {
name string
existing *netlink.Route
desired *netlink.Route
want bool
}{
{
name: "nil existing destination — no conflict",
existing: &netlink.Route{Dst: nil},
desired: &netlink.Route{Dst: dst},
want: false,
},
{
name: "nil desired destination — no conflict",
existing: &netlink.Route{Dst: dst},
desired: &netlink.Route{Dst: nil},
want: false,
},
{
name: "different destinations — no conflict",
existing: &netlink.Route{Dst: otherDst},
desired: &netlink.Route{Dst: dst},
want: false,
},
{
name: "same destination, no gateway on either — no conflict",
existing: &netlink.Route{Dst: dst, LinkIndex: 5},
desired: &netlink.Route{Dst: dst, LinkIndex: 5},
want: false,
},
{
name: "same destination, same gateway — no conflict",
existing: &netlink.Route{Dst: dst, Gw: gw1, LinkIndex: 5},
desired: &netlink.Route{Dst: dst, Gw: gw1, LinkIndex: 5},
want: false,
},
{
name: "same destination, different gateway — conflict",
existing: &netlink.Route{Dst: dst, Gw: gw1},
desired: &netlink.Route{Dst: dst, Gw: gw2},
want: true,
},
{
name: "existing has gateway, desired does not — conflict",
existing: &netlink.Route{Dst: dst, Gw: gw1},
desired: &netlink.Route{Dst: dst},
want: true,
},
{
name: "desired has gateway, existing does not — conflict",
existing: &netlink.Route{Dst: dst},
desired: &netlink.Route{Dst: dst, Gw: gw1},
want: true,
},
{
name: "same destination, same gateway, different link index — conflict",
existing: &netlink.Route{Dst: dst, Gw: gw1, LinkIndex: 5},
desired: &netlink.Route{Dst: dst, Gw: gw1, LinkIndex: 7},
want: true,
},
{
name: "same destination, gateway set, link index zero on existing — no conflict",
existing: &netlink.Route{Dst: dst, Gw: gw1, LinkIndex: 0},
desired: &netlink.Route{Dst: dst, Gw: gw1, LinkIndex: 5},
want: false,
},
{
name: "same destination, no gateway, different link index — conflict",
existing: &netlink.Route{Dst: dst, LinkIndex: 5},
desired: &netlink.Route{Dst: dst, LinkIndex: 7},
want: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := routeConflicts(tt.existing, tt.desired)
if got != tt.want {
t.Errorf("routeConflicts() = %v, want %v", got, tt.want)
}
})
}
}
Loading