2daa2a21f3
Build flock Image / build (push) Successful in 3m23s
Like anycast, addresses IPs are advertised via BGP (/128+/32) and get host routes via the AnycastReconciler. The sole difference: they are assigned to pod eth0 instead of lo, so workloads that inspect their primary interface (e.g. Plex remote-access detection) see the public IP directly. - annotations.go: annAddresses const, Addresses []net.IP in ParsedAnnotations - state.go: Addresses []string persisted in allocations.json - anycast.go: resolveAnycastTargets processes Anycast+Addresses together - netns_linux.go: configurePodSide assigns Addresses to eth0 - netns_stub.go: mirror Addresses field for non-Linux builds - handlers.go: thread Addresses through ADD path Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
113 lines
3.4 KiB
Go
113 lines
3.4 KiB
Go
package agent
|
|
|
|
import (
|
|
"net"
|
|
"sort"
|
|
)
|
|
|
|
// anycastNexthop is one (host-side veth, pod-eth0-IP) pair the kernel route
|
|
// can use as a multipath nexthop.
|
|
type anycastNexthop struct {
|
|
hostIface string
|
|
via net.IP
|
|
}
|
|
|
|
// anycastTarget describes the kernel route shape for one advertised anycast
|
|
// IP. When more than one Ready pod on this node binds the same anycast IP,
|
|
// every Ready pod contributes a nexthop and the kernel does per-flow ECMP
|
|
// across them.
|
|
//
|
|
// nexthops is sorted by canonical(via) for deterministic comparison and
|
|
// stable kernel-route ordering across reconcile passes — the
|
|
// AnycastReconciler skips kernel writes when the new and old targets are
|
|
// equal, which only works if the slice order is stable.
|
|
type anycastTarget struct {
|
|
nexthops []anycastNexthop
|
|
}
|
|
|
|
// equal reports whether two targets describe the same kernel route.
|
|
// Both sides are expected to be sorted (the canonical constructor sorts).
|
|
func (t anycastTarget) equal(o anycastTarget) bool {
|
|
if len(t.nexthops) != len(o.nexthops) {
|
|
return false
|
|
}
|
|
for i := range t.nexthops {
|
|
if t.nexthops[i].hostIface != o.nexthops[i].hostIface {
|
|
return false
|
|
}
|
|
if !t.nexthops[i].via.Equal(o.nexthops[i].via) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// resolveAnycastTargets walks the committed allocation set and returns the
|
|
// desired kernel-route shape for every anycast IP that has at least one
|
|
// Ready local pod binding it. Multiple Ready pods sharing the same anycast
|
|
// IP collapse into a single multi-nexthop target so the kernel can
|
|
// per-flow ECMP across them.
|
|
//
|
|
// Pure: no kernel calls, no informer access. Pods are surfaced via the
|
|
// isReady callback so the reconciler can plug in its informer; tests can
|
|
// pass any function that satisfies the signature.
|
|
//
|
|
// warn is invoked for human-facing skip reasons (e.g. anycast with no
|
|
// unicast of same family). nil-safe — pass nil to silently drop.
|
|
func resolveAnycastTargets(
|
|
allocations []Allocation,
|
|
isReady func(namespace, name string) bool,
|
|
warn func(string),
|
|
) map[string]anycastTarget {
|
|
if warn == nil {
|
|
warn = func(string) {}
|
|
}
|
|
out := map[string]anycastTarget{}
|
|
for _, a := range allocations {
|
|
if a.State != StateCommitted || (len(a.Anycast) == 0 && len(a.Addresses) == 0) {
|
|
continue
|
|
}
|
|
if !isReady(a.Namespace, a.PodName) {
|
|
continue
|
|
}
|
|
host := HostIfaceName(a.ContainerID)
|
|
via6 := net.ParseIP(a.IP6)
|
|
via4 := net.ParseIP(a.IP4)
|
|
// Anycast (lo-bound) and Addresses (eth0-bound) are advertised
|
|
// identically: /128 or /32 host route on the host, BGP via BIRD.
|
|
for _, ipStr := range append(a.Anycast, a.Addresses...) {
|
|
ip := net.ParseIP(ipStr)
|
|
if ip == nil {
|
|
continue
|
|
}
|
|
var via net.IP
|
|
if ip.To4() != nil {
|
|
via = via4
|
|
} else {
|
|
via = via6
|
|
}
|
|
if via == nil {
|
|
warn("anycast " + ipStr + " skipped: pod " +
|
|
a.Namespace + "/" + a.PodName +
|
|
" has no unicast of same family")
|
|
continue
|
|
}
|
|
key := canonical(ip)
|
|
t := out[key]
|
|
t.nexthops = append(t.nexthops, anycastNexthop{hostIface: host, via: via})
|
|
out[key] = t
|
|
}
|
|
}
|
|
// Sort each target's nexthops for stable comparison + stable kernel
|
|
// ordering. Sort key is canonical(via) — sufficient for stability
|
|
// because (host, via) pairs are 1:1 (one veth per pod, one v6+v4 per
|
|
// pod, so via uniquely identifies the nexthop).
|
|
for k, t := range out {
|
|
sort.Slice(t.nexthops, func(i, j int) bool {
|
|
return canonical(t.nexthops[i].via) < canonical(t.nexthops[j].via)
|
|
})
|
|
out[k] = t
|
|
}
|
|
return out
|
|
}
|