170 lines
4.5 KiB
Go
170 lines
4.5 KiB
Go
|
|
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]})
|
||
|
|
}
|