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:
+43
-20
@@ -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])
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user