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]}) }