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:
@@ -0,0 +1,169 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzIPAM_Allocate runs randomly-driven Allocate/Release sequences against
|
||||
// a /120 IPv6 + /28 IPv4 IPAM so the fuzzer can hit address exhaustion.
|
||||
//
|
||||
// Properties checked:
|
||||
//
|
||||
// 1. Allocate never panics regardless of the action stream.
|
||||
// 2. The set of in-use addresses never contains an address that has been
|
||||
// released without a subsequent successful Allocate.
|
||||
// 3. A successful v6 allocation always yields an address inside the
|
||||
// configured /120, and a successful v4 always inside the configured /28.
|
||||
// 4. ipToU32(canonical(allocated v4)) round-trips, and likewise that no
|
||||
// v4 allocation lands on .0 (network) or .15 (broadcast) of the /28.
|
||||
//
|
||||
// The fuzzed bytes are interpreted as an opcode stream:
|
||||
// - bytes[i] & 0x03 selects the action: 0=alloc-v6, 1=alloc-v4,
|
||||
// 2=alloc-dual, 3=release-most-recent.
|
||||
// - bytes[i]>>2 is fed into the deterministic random source so different
|
||||
// fuzzed bytes drive different IID/index choices.
|
||||
func FuzzIPAM_Allocate(f *testing.F) {
|
||||
f.Add([]byte{0, 0, 0, 0})
|
||||
f.Add([]byte{1, 1, 1, 1})
|
||||
f.Add([]byte{2, 2, 2, 2})
|
||||
f.Add([]byte{0, 1, 2, 3})
|
||||
f.Add([]byte(longString("\x00\x01\x02\x03", 256)))
|
||||
|
||||
f.Fuzz(func(t *testing.T, ops []byte) {
|
||||
ipam, err := NewIPAM(
|
||||
[]string{"2001:db8::/120"}, // 256 host slots; 16 bytes of fuzzed nibbles
|
||||
[]string{"10.0.0.0/28"}, // 14 usable hosts (.2..14)
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Deterministic source: replay nibbles cycled from `ops`.
|
||||
fr := &fakeRand{
|
||||
nibbles: append([]byte{}, ops...),
|
||||
iids: [][]byte{
|
||||
// 16 bytes of "host portion" — only the last byte matters
|
||||
// for a /120 prefix.
|
||||
makeIID(ops, 0),
|
||||
makeIID(ops, 1),
|
||||
makeIID(ops, 2),
|
||||
makeIID(ops, 3),
|
||||
},
|
||||
}
|
||||
if len(fr.nibbles) == 0 {
|
||||
fr.nibbles = []byte{0}
|
||||
}
|
||||
ipam.randSrc = fr
|
||||
|
||||
net6 := mustNet(t, "2001:db8::/120")
|
||||
net4 := mustNet(t, "10.0.0.0/28")
|
||||
|
||||
var live []AllocResult
|
||||
seen := map[string]struct{}{}
|
||||
|
||||
for idx, op := range ops {
|
||||
req := AllocRequest{ContainerID: idStr(idx)}
|
||||
switch op & 0x03 {
|
||||
case 0:
|
||||
req.WantV6 = true
|
||||
case 1:
|
||||
req.WantV4 = true
|
||||
case 2:
|
||||
req.WantV6, req.WantV4 = true, true
|
||||
case 3:
|
||||
if len(live) == 0 {
|
||||
continue
|
||||
}
|
||||
rel := live[len(live)-1]
|
||||
live = live[:len(live)-1]
|
||||
ipam.Release(rel.IP6, rel.IP4)
|
||||
delete(seen, canonical(rel.IP6))
|
||||
delete(seen, canonical(rel.IP4))
|
||||
continue
|
||||
}
|
||||
|
||||
res, err := ipam.Allocate(req)
|
||||
if err != nil {
|
||||
continue // exhaustion is acceptable
|
||||
}
|
||||
|
||||
if req.WantV6 {
|
||||
if res.IP6 == nil {
|
||||
t.Fatalf("requested v6 but got nil")
|
||||
}
|
||||
if !net6.Contains(res.IP6) {
|
||||
t.Fatalf("v6 %s outside /120", res.IP6)
|
||||
}
|
||||
if _, dup := seen[canonical(res.IP6)]; dup {
|
||||
t.Fatalf("v6 %s duplicated", res.IP6)
|
||||
}
|
||||
seen[canonical(res.IP6)] = struct{}{}
|
||||
}
|
||||
if req.WantV4 {
|
||||
if res.IP4 == nil {
|
||||
t.Fatalf("requested v4 but got nil")
|
||||
}
|
||||
if !net4.Contains(res.IP4) {
|
||||
t.Fatalf("v4 %s outside /28", res.IP4)
|
||||
}
|
||||
v4 := res.IP4.To4()
|
||||
if v4 == nil {
|
||||
t.Fatalf("v4 result not 4-byte: %s", res.IP4)
|
||||
}
|
||||
// Skip .0 (network) and .15 (broadcast). The allocator
|
||||
// should also skip .1 (gateway) by convention.
|
||||
last := v4[3]
|
||||
if last == 0 || last == 1 || last == 15 {
|
||||
t.Fatalf("v4 %s in reserved range", res.IP4)
|
||||
}
|
||||
if _, dup := seen[canonical(res.IP4)]; dup {
|
||||
t.Fatalf("v4 %s duplicated", res.IP4)
|
||||
}
|
||||
seen[canonical(res.IP4)] = struct{}{}
|
||||
}
|
||||
live = append(live, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzCanonical asserts that canonical never panics and is idempotent.
|
||||
func FuzzCanonical(f *testing.F) {
|
||||
f.Add([]byte{})
|
||||
f.Add([]byte{1, 2, 3, 4})
|
||||
f.Add([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
||||
f.Add([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 10, 0, 0, 1}) // v4-mapped v6
|
||||
f.Add([]byte{0xff})
|
||||
|
||||
f.Fuzz(func(t *testing.T, b []byte) {
|
||||
ip := net.IP(b)
|
||||
s1 := canonical(ip)
|
||||
// Idempotent: re-canonicalising the parsed form yields the same
|
||||
// string for any non-empty result.
|
||||
if s1 != "" {
|
||||
parsed := net.ParseIP(s1)
|
||||
if parsed == nil {
|
||||
t.Fatalf("canonical(%v)=%q is not parseable as IP", b, s1)
|
||||
}
|
||||
if got := canonical(parsed); got != s1 {
|
||||
t.Fatalf("not idempotent: %q -> %q", s1, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func makeIID(seed []byte, salt byte) []byte {
|
||||
out := make([]byte, net.IPv6len)
|
||||
for i := range out {
|
||||
if i < len(seed) {
|
||||
out[i] = seed[i] ^ salt
|
||||
} else {
|
||||
out[i] = salt
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func idStr(i int) string {
|
||||
const hex = "0123456789abcdef"
|
||||
return string([]byte{'c', '-', hex[(i>>4)&0xF], hex[i&0xF]})
|
||||
}
|
||||
Reference in New Issue
Block a user