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,156 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzParseAnnotations explores the joint space of {ipv6, ipv4, cidr6, cidr4,
|
||||
// ip-algo, anycast} annotations with random byte strings. Every recognised
|
||||
// key is exercised by deriving a deterministic input map from the fuzzed
|
||||
// bytes; this gives the fuzzer reach into all parser branches at once.
|
||||
//
|
||||
// Properties checked:
|
||||
//
|
||||
// 1. The parser never panics on any input.
|
||||
// 2. On nil-error return, the result satisfies the design-doc invariant
|
||||
// that at least one of WantV6 / WantV4 is true (a pod always has at
|
||||
// least one address).
|
||||
// 3. Anycast IPs and IPAlgo fields are non-nil/empty only when the
|
||||
// annotation was supplied; never spontaneously populated.
|
||||
//
|
||||
// Seed corpus covers known edge cases the spec must handle.
|
||||
func FuzzParseAnnotations(f *testing.F) {
|
||||
// Seeds: each entry is six strings — the literal raw values for the
|
||||
// six parsed keys. Empty string for "key absent".
|
||||
type seed struct {
|
||||
ipv6, ipv4, cidr6, cidr4, ipAlgo, anycast string
|
||||
}
|
||||
seeds := []seed{
|
||||
{},
|
||||
{ipv4: "true"},
|
||||
{ipv6: "false", ipv4: "true"},
|
||||
{ipv6: "TRUE"},
|
||||
{ipv6: " true "},
|
||||
{ipv6: "yes"}, // invalid → expect error
|
||||
{ipv4: "1"}, // invalid
|
||||
{cidr6: ""}, // invalid (empty after split)
|
||||
{cidr6: ","}, // invalid (empty after trim)
|
||||
{cidr6: "2602:817:3000:f001::/64"}, // valid single
|
||||
{cidr6: "2602:817:3000:f001::/64,"}, // trailing comma
|
||||
{cidr6: " 2602:817:3000:f001::/64 "}, // surrounding whitespace
|
||||
{cidr6: "2602:817:3000:f001::/64, 2602:817:3000:f002::/64"},
|
||||
{cidr6: "10.0.0.0/8"}, // family mismatch
|
||||
{cidr4: "172.25.210.0/24"}, // valid
|
||||
{cidr4: "172.25.210.0/24,172.25.211.0/24"}, // multiple
|
||||
{cidr4: "2602:817::/32"}, // family mismatch
|
||||
{ipAlgo: "namespace,pod,image"},
|
||||
{ipAlgo: "namespace, pod , image"}, // whitespace
|
||||
{ipAlgo: "namespace,unknown"}, // invalid
|
||||
{ipAlgo: ""}, // invalid (empty)
|
||||
{ipAlgo: ","}, // invalid
|
||||
{anycast: "2602:817:3000:ac::1"},
|
||||
{anycast: "2602:817:3000:ac::1, 172.25.255.1"},
|
||||
{anycast: "::1"}, // loopback (allowed at parse time)
|
||||
{anycast: "fe80::1"}, // link-local (allowed at parse time)
|
||||
{anycast: "::ffff:10.0.0.1"}, // v4-mapped v6
|
||||
{anycast: "0.0.0.0"}, // unspecified
|
||||
{anycast: "definitely-not-an-ip"}, // invalid
|
||||
{anycast: ""}, // invalid
|
||||
// Embedded NUL bytes
|
||||
{ipv4: "true\x00"},
|
||||
{cidr6: "2602:817:3000:f001::/64\x00"},
|
||||
{anycast: "\x00\x00"},
|
||||
// Unicode
|
||||
{ipv4: "trüe"},
|
||||
{ipAlgo: "námespace"},
|
||||
// Very long
|
||||
{cidr6: longString("2602:817:3000:f001::/64,", 4096)},
|
||||
}
|
||||
for _, s := range seeds {
|
||||
f.Add(s.ipv6, s.ipv4, s.cidr6, s.cidr4, s.ipAlgo, s.anycast)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, ipv6, ipv4, cidr6, cidr4, ipAlgo, anycast string) {
|
||||
in := map[string]string{}
|
||||
// Treat empty as "key absent" so the seed table matches the run-time
|
||||
// shape; Kubernetes annotations cannot have a nil value but they CAN
|
||||
// be missing entirely. Empty-string-with-key is also a real case
|
||||
// (operator typo); add a separate seed below to cover it.
|
||||
if ipv6 != "" {
|
||||
in[annotationPrefix+annIPv6] = ipv6
|
||||
}
|
||||
if ipv4 != "" {
|
||||
in[annotationPrefix+annIPv4] = ipv4
|
||||
}
|
||||
if cidr6 != "" {
|
||||
in[annotationPrefix+annCIDR6] = cidr6
|
||||
}
|
||||
if cidr4 != "" {
|
||||
in[annotationPrefix+annCIDR4] = cidr4
|
||||
}
|
||||
if ipAlgo != "" {
|
||||
in[annotationPrefix+annIPAlgo] = ipAlgo
|
||||
}
|
||||
if anycast != "" {
|
||||
in[annotationPrefix+annAnycast] = anycast
|
||||
}
|
||||
|
||||
got, err := ParseAnnotations(in, BuiltinFamilyDefaults())
|
||||
if err != nil {
|
||||
return // any error is acceptable; we only require no panic
|
||||
}
|
||||
// Property: at least one family must be selected.
|
||||
if !got.WantV6 && !got.WantV4 {
|
||||
t.Fatalf("parser accepted but produced no family: in=%#v", in)
|
||||
}
|
||||
// Property: optional fields populated only when their key was set.
|
||||
if _, hasAlgo := in[annotationPrefix+annIPAlgo]; !hasAlgo && len(got.IPAlgo) != 0 {
|
||||
t.Fatalf("IPAlgo populated without annotation")
|
||||
}
|
||||
if _, hasAny := in[annotationPrefix+annAnycast]; !hasAny && len(got.Anycast) != 0 {
|
||||
t.Fatalf("Anycast populated without annotation")
|
||||
}
|
||||
if _, hasC6 := in[annotationPrefix+annCIDR6]; !hasC6 && len(got.CIDR6) != 0 {
|
||||
t.Fatalf("CIDR6 populated without annotation")
|
||||
}
|
||||
if _, hasC4 := in[annotationPrefix+annCIDR4]; !hasC4 && len(got.CIDR4) != 0 {
|
||||
t.Fatalf("CIDR4 populated without annotation")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzParseCNIArgs requires the parser to never panic on adversarial inputs.
|
||||
// The parser is permissive by spec — it returns a CNIArgs with whatever it
|
||||
// could extract — so the only invariant is "doesn't crash".
|
||||
func FuzzParseCNIArgs(f *testing.F) {
|
||||
f.Add("")
|
||||
f.Add("=")
|
||||
f.Add(";")
|
||||
f.Add(";=;=;")
|
||||
f.Add("K8S_POD_NAMESPACE=ns;K8S_POD_NAME=p")
|
||||
f.Add("K8S_POD_NAMESPACE=ns;K8S_POD_NAME=p;K8S_POD_UID=abc;K8S_POD_INFRA_CONTAINER_ID=def")
|
||||
f.Add("=value-only")
|
||||
f.Add("key-only=")
|
||||
f.Add("\x00\x00\x00")
|
||||
f.Add("K8S_POD_NAMESPACE=\xff\xfe\xfd")
|
||||
f.Add("K8S_POD_NAME=value;K8S_POD_NAME=other") // duplicate keys: last wins
|
||||
// Long input
|
||||
f.Add(longString("K8S_POD_NAME=x;", 4096))
|
||||
|
||||
f.Fuzz(func(t *testing.T, in string) {
|
||||
_ = ParseCNIArgs(in)
|
||||
})
|
||||
}
|
||||
|
||||
// longString returns s repeated to total >= n bytes, useful for piling up
|
||||
// realistic-looking but oversized inputs.
|
||||
func longString(s string, n int) string {
|
||||
if len(s) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b []byte
|
||||
for len(b) < n {
|
||||
b = append(b, s...)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
Reference in New Issue
Block a user