NodeConfig defaults + code-quality pass + fuzz tests + README

NodeConfig.Spec.Defaults adds per-node IPv6/IPv4 family defaults that pod
annotations can override; built-in baseline (v6=true, v4=false) still
applies when the field is omitted.

bird.Render now validates every operator-supplied value (peer addresses,
CIDRs, anycast IPs, source addresses) before templating — fuzz found a
peer address containing `}` produced unbalanced braces in bird.conf.
Failing input preserved as a regression seed.

Fuzz targets added for ParseAnnotations, ParseCNIArgs, HostIfaceName,
canonical, IPAM allocate sequences, embed.Embed, and bird.Render.
Hardened canonical/ipToU32 against nil and non-IPv4 inputs.

README rewritten for outside readers — quickstart, NodeConfig + annotation
reference with worked examples, anycast use cases, comparison vs Calico
and Cilium, requirements, limitations.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Donavan Fritz
2026-04-25 09:25:45 -05:00
parent 677aec2a42
commit 71e584cf96
17 changed files with 1583 additions and 100 deletions
+43 -20
View File
@@ -62,13 +62,15 @@ func (cryptoRand) PickIndex(n int) int {
}
// AllocRequest describes a pending allocation. Values come from Pod metadata
// + annotations at CNI ADD time.
// + annotations at CNI ADD time, with per-node FamilyDefaults already merged
// in (see ParseAnnotations).
type AllocRequest struct {
ContainerID string
Namespace string
Pod string
// WantV6 / WantV4 come from the ipv6 / ipv4 annotations (defaults in
// design doc: ipv6=true, ipv4=false).
// WantV6 / WantV4 are the post-merge address family selection (pod
// annotation > NodeConfig.Spec.Defaults > built-in baseline). At least
// one MUST be true; Allocate rejects the request otherwise.
WantV6 bool
WantV4 bool
// AnnCIDR6 / AnnCIDR4 come from the cidr6 / cidr4 annotations. Empty
@@ -224,34 +226,36 @@ func (i *IPAM) allocV6(cidr *net.IPNet, req AllocRequest) (net.IP, error) {
// randomV6 picks a random /128 inside cidr. The network prefix bits are
// preserved from cidr.IP; the host bits are filled from the random source.
//
// Implementation: walk the 16 IPv6 bytes once. For each byte we ask whether
// it's entirely inside the network mask (skip), entirely inside the host
// portion (overwrite with random), or split (combine bits from both).
func (i *IPAM) randomV6(cidr *net.IPNet) (net.IP, error) {
ones, bits := cidr.Mask.Size()
if bits != 128 {
return nil, fmt.Errorf("cidr %s is not IPv6", cidr)
}
out := make(net.IP, 16)
out := make(net.IP, net.IPv6len)
copy(out, cidr.IP.To16())
hostBits := 128 - ones
rnd := make([]byte, 16)
rnd := make([]byte, net.IPv6len)
i.randSrc.FillIID(rnd)
// Merge rnd into out where mask bit is 0.
for b := 0; b < 16; b++ {
// Host bits start at bit index `ones`, byte `b`.
for b := 0; b < net.IPv6len; b++ {
byteStart := b * 8
byteEnd := byteStart + 8
if byteEnd <= ones {
continue // entirely network
}
if byteStart >= ones {
out[b] = rnd[b] // entirely host
switch {
case byteEnd <= ones:
// Entirely inside the network prefix — leave untouched.
continue
case byteStart >= ones:
// Entirely inside the host portion — fully randomise.
out[b] = rnd[b]
default:
// Split byte: top (ones-byteStart) bits are network, rest host.
networkBits := ones - byteStart
hostMask := byte(0xFF) >> uint(networkBits)
out[b] = (out[b] & ^hostMask) | (rnd[b] & hostMask)
}
// Split byte: top (ones-byteStart) bits are network, rest is host.
networkBits := ones - byteStart
hostMask := byte(0xFF) >> uint(networkBits)
out[b] = (out[b] & ^hostMask) | (rnd[b] & hostMask)
}
_ = hostBits
return out, nil
}
@@ -360,15 +364,34 @@ func toStringSlice(ns []*net.IPNet) []string {
return out
}
// canonical returns the textual form of ip in its native family, so the same
// host address is always represented identically regardless of whether it
// arrived as a 4-byte slice, a 16-byte v4-in-v6 slice, or a string-parsed
// net.IP. Used as the key for the in-use map.
//
// Returns "" for nil input — callers MUST treat the returned key as opaque
// and never use the empty string as a sentinel.
func canonical(ip net.IP) string {
if ip == nil {
return ""
}
if v4 := ip.To4(); v4 != nil {
return v4.String()
}
return ip.To16().String()
if v16 := ip.To16(); v16 != nil {
return v16.String()
}
return ""
}
// ipToU32 reads a 4-byte IPv4 net.IP into a uint32. The caller is expected
// to have already validated that ip is an IPv4 address; mis-use returns 0
// rather than panicking.
func ipToU32(ip net.IP) uint32 {
v4 := ip.To4()
if v4 == nil {
return 0
}
return uint32(v4[0])<<24 | uint32(v4[1])<<16 | uint32(v4[2])<<8 | uint32(v4[3])
}