From 5df592e2d17f3e7b887e76d0459db71f2d45edca Mon Sep 17 00:00:00 2001 From: Donavan Fritz Date: Wed, 26 Aug 2026 18:44:19 -0500 Subject: [PATCH] [bug-n1wxwkrtcnds] add pod link-local NUD source Co-Authored-By: Codex (GPT-5) --- README.md | 7 ++++--- go.mod | 2 +- pkg/agent/netns_linux.go | 21 +++++++++++++++++++++ pkg/agent/netns_linux_test.go | 25 +++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 pkg/agent/netns_linux_test.go diff --git a/README.md b/README.md index 82b8fd0..c566310 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,10 @@ both the kernel and BGP within one reconcile cycle (sub-second). NIC ↔ upstream router. No conntrack, no SNAT, no encapsulation. For IPv6 the host side of every veth carries the deterministic link-local -gateway `fe80::1`, so every pod can use a fixed default route. For IPv4 -the host side answers ARP for `169.254.1.1`, providing the same fixed -default route in v4. +gateway `fe80::1`, and the isolated pod side carries `fe80::2` for neighbor +discovery. Every IPv6 pod can use a fixed default route without depending on +its global `/128` for gateway resolution. For IPv4 the host side answers ARP +for `169.254.1.1`, providing the same fixed default route in v4. ## Requirements diff --git a/go.mod b/go.mod index 307e392..65e20dc 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/containernetworking/cni v1.3.0 github.com/containernetworking/plugins v1.9.1 github.com/vishvananda/netlink v1.3.1 + golang.org/x/sys v0.40.0 k8s.io/api v0.36.0 k8s.io/apimachinery v0.36.0 k8s.io/client-go v0.36.0 @@ -35,7 +36,6 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sys v0.40.0 // indirect golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect diff --git a/pkg/agent/netns_linux.go b/pkg/agent/netns_linux.go index c1d93ee..9011266 100644 --- a/pkg/agent/netns_linux.go +++ b/pkg/agent/netns_linux.go @@ -11,6 +11,7 @@ import ( "github.com/containernetworking/plugins/pkg/ns" "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" ) // SetupRequest is the netlink setup input for one pod. @@ -37,6 +38,11 @@ type SetupRequest struct { // LL DAD on the host side. var linkLocalGW = net.ParseIP("fe80::1") +// linkLocalPod is the deterministic pod-side address used for IPv6 neighbor +// discovery. Without a link-local source, gateway NUD probes use the pod's +// global /128 and the host does not answer them reliably after neighbor expiry. +var linkLocalPod = net.ParseIP("fe80::2") + // v4ProxyGW is the well-known link-local IPv4 used by container CNIs as a // next-hop for proxy-arp gateways (cilium, calico, kindnet — all use this). var v4ProxyGW = net.IPv4(169, 254, 1, 1) @@ -210,6 +216,14 @@ func configurePodSide(req SetupRequest) error { } if req.IP6 != nil { + // Keep automatic address generation disabled, but give every IPv6 pod + // veth a deterministic link-local address. This makes NUD for the + // fe80::1 gateway use an on-link source and prevents the default-route + // neighbor from aging into FAILED. DAD is unnecessary on a veth pair. + if err := netlink.AddrAdd(eth0, podLinkLocalAddr()); err != nil && !errors.Is(err, os.ErrExist) { + return fmt.Errorf("pod link-local add: %w", err) + } + a := &netlink.Addr{IPNet: &net.IPNet{IP: req.IP6, Mask: net.CIDRMask(128, 128)}} if err := netlink.AddrAdd(eth0, a); err != nil && !errors.Is(err, os.ErrExist) { return fmt.Errorf("pod ip6 add: %w", err) @@ -295,6 +309,13 @@ func configurePodSide(req SetupRequest) error { }) } +func podLinkLocalAddr() *netlink.Addr { + return &netlink.Addr{ + IPNet: &net.IPNet{IP: linkLocalPod, Mask: net.CIDRMask(64, 128)}, + Flags: unix.IFA_F_NODAD, + } +} + func setHostRoute(linkIndex int, ip net.IP, prefix int) error { r := &netlink.Route{ LinkIndex: linkIndex, diff --git a/pkg/agent/netns_linux_test.go b/pkg/agent/netns_linux_test.go new file mode 100644 index 0000000..c660044 --- /dev/null +++ b/pkg/agent/netns_linux_test.go @@ -0,0 +1,25 @@ +//go:build linux + +package agent + +import ( + "testing" + + "golang.org/x/sys/unix" +) + +func TestPodLinkLocalAddr(t *testing.T) { + addr := podLinkLocalAddr() + if got, want := addr.IP.String(), "fe80::2"; got != want { + t.Fatalf("pod link-local IP = %s, want %s", got, want) + } + if ones, bits := addr.Mask.Size(); ones != 64 || bits != 128 { + t.Fatalf("pod link-local mask = %d/%d, want 64/128", ones, bits) + } + if addr.Flags&unix.IFA_F_NODAD == 0 { + t.Fatal("pod link-local address must disable DAD") + } + if addr.IP.Equal(linkLocalGW) { + t.Fatal("pod and gateway link-local addresses must differ") + } +}