295 lines
7.9 KiB
Go
295 lines
7.9 KiB
Go
|
|
package agent
|
||
|
|
|
||
|
|
import (
|
||
|
|
"net"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"code.fritzlab.net/fritzlab/flock/pkg/embed"
|
||
|
|
)
|
||
|
|
|
||
|
|
// fakeRand is a deterministic randSource for tests.
|
||
|
|
type fakeRand struct {
|
||
|
|
nibbles []byte // queue of NibbleN results
|
||
|
|
iids [][]byte
|
||
|
|
picks []int
|
||
|
|
n, i, p int
|
||
|
|
}
|
||
|
|
|
||
|
|
func (f *fakeRand) NibbleN() byte {
|
||
|
|
v := f.nibbles[f.n%len(f.nibbles)]
|
||
|
|
f.n++
|
||
|
|
return v & 0x0F
|
||
|
|
}
|
||
|
|
func (f *fakeRand) FillIID(dst []byte) {
|
||
|
|
src := f.iids[f.i%len(f.iids)]
|
||
|
|
f.i++
|
||
|
|
copy(dst, src)
|
||
|
|
}
|
||
|
|
func (f *fakeRand) PickIndex(n int) int {
|
||
|
|
if len(f.picks) == 0 {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
v := f.picks[f.p%len(f.picks)]
|
||
|
|
f.p++
|
||
|
|
if v >= n {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
return v
|
||
|
|
}
|
||
|
|
|
||
|
|
func mustNet(t *testing.T, s string) *net.IPNet {
|
||
|
|
t.Helper()
|
||
|
|
_, n, err := net.ParseCIDR(s)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
return n
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIntersectCIDR(t *testing.T) {
|
||
|
|
v6a := mustNet(t, "2602:817:3000:f001::/64")
|
||
|
|
v6super := mustNet(t, "2602:817:3000::/48")
|
||
|
|
v6sub := mustNet(t, "2602:817:3000:f001::/96")
|
||
|
|
v6other := mustNet(t, "2602:817:3000:f002::/64")
|
||
|
|
v4 := mustNet(t, "10.0.0.0/24")
|
||
|
|
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
ann *net.IPNet
|
||
|
|
node *net.IPNet
|
||
|
|
want *net.IPNet
|
||
|
|
}{
|
||
|
|
{"equal → node", v6a, v6a, v6a},
|
||
|
|
{"supernet of node → node", v6super, v6a, v6a},
|
||
|
|
{"subnet of node → ann", v6sub, v6a, v6sub},
|
||
|
|
{"disjoint same family → nil", v6other, v6a, nil},
|
||
|
|
{"different family → nil", v4, v6a, nil},
|
||
|
|
}
|
||
|
|
for _, c := range cases {
|
||
|
|
got := intersectCIDR(c.ann, c.node)
|
||
|
|
if (got == nil) != (c.want == nil) {
|
||
|
|
t.Errorf("%s: got=%v want=%v", c.name, got, c.want)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if got != nil && !cidrEqual(got, c.want) {
|
||
|
|
t.Errorf("%s: got=%s want=%s", c.name, got, c.want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestResolveEffective(t *testing.T) {
|
||
|
|
nodes := []*net.IPNet{
|
||
|
|
mustNet(t, "2602:817:3000:f001::/64"),
|
||
|
|
mustNet(t, "2602:817:3000:f002::/64"),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Empty annotation → all node CIDRs.
|
||
|
|
if got, err := resolveEffective(nil, nodes); err != nil || len(got) != 2 {
|
||
|
|
t.Fatalf("empty ann: got=%v err=%v", got, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Subnet of one node CIDR → that subnet only.
|
||
|
|
ann := []*net.IPNet{mustNet(t, "2602:817:3000:f001::/96")}
|
||
|
|
got, err := resolveEffective(ann, nodes)
|
||
|
|
if err != nil || len(got) != 1 || got[0].String() != "2602:817:3000:f001::/96" {
|
||
|
|
t.Fatalf("subnet ann: got=%v err=%v", got, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Supernet matches both node CIDRs.
|
||
|
|
ann = []*net.IPNet{mustNet(t, "2602:817:3000::/48")}
|
||
|
|
got, err = resolveEffective(ann, nodes)
|
||
|
|
if err != nil || len(got) != 2 {
|
||
|
|
t.Fatalf("supernet ann: got=%v err=%v", got, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// First-match-wins: the first ann CIDR that intersects anything wins.
|
||
|
|
ann = []*net.IPNet{
|
||
|
|
mustNet(t, "2602:817:3000:ff::/64"), // no intersection
|
||
|
|
mustNet(t, "2602:817:3000:f001::/64"),
|
||
|
|
}
|
||
|
|
got, err = resolveEffective(ann, nodes)
|
||
|
|
if err != nil || len(got) != 1 || got[0].String() != "2602:817:3000:f001::/64" {
|
||
|
|
t.Fatalf("first-match: got=%v err=%v", got, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// No overlap → error.
|
||
|
|
ann = []*net.IPNet{mustNet(t, "2602:817:3000:ff::/64")}
|
||
|
|
if _, err := resolveEffective(ann, nodes); err == nil {
|
||
|
|
t.Fatalf("expected error for disjoint ann")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIPAM_AllocV6_Random(t *testing.T) {
|
||
|
|
i, err := NewIPAM([]string{"2602:817:3000:f001::/64"}, nil)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
i.randSrc = &fakeRand{
|
||
|
|
iids: [][]byte{
|
||
|
|
// 16 bytes; only the low 8 bytes are used for a /64 host portion.
|
||
|
|
{0, 0, 0, 0, 0, 0, 0, 0, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
res, err := i.Allocate(AllocRequest{
|
||
|
|
ContainerID: "c1", Namespace: "ns", Pod: "p", WantV6: true,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Allocate: %v", err)
|
||
|
|
}
|
||
|
|
if res.IP6.String() != "2602:817:3000:f001:aabb:ccdd:eeff:1122" {
|
||
|
|
t.Fatalf("IP6 = %s, want 2602:817:3000:f001:aabb:ccdd:eeff:1122", res.IP6)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIPAM_AllocV6_WithEmbed(t *testing.T) {
|
||
|
|
i, err := NewIPAM([]string{"2602:817:3000:f001::/64"}, nil)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
i.randSrc = &fakeRand{nibbles: []byte{0xe}}
|
||
|
|
res, err := i.Allocate(AllocRequest{
|
||
|
|
ContainerID: "c1", Namespace: "mail", Pod: "stalwart-0", WantV6: true,
|
||
|
|
IPAlgo: []embed.Field{embed.FieldNamespace, embed.FieldPod, embed.FieldImage},
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Allocate: %v", err)
|
||
|
|
}
|
||
|
|
// Last nibble from fakeRand NibbleN → 0xe.
|
||
|
|
if got := res.IP6[15] & 0x0F; got != 0xe {
|
||
|
|
t.Fatalf("last nibble = %x, want e", got)
|
||
|
|
}
|
||
|
|
mustNet(t, "2602:817:3000:f001::/64").Contains(res.IP6)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIPAM_AllocV6_CollisionRetry(t *testing.T) {
|
||
|
|
i, err := NewIPAM([]string{"2602:817:3000:f001::/64"}, nil)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
// Mark one specific address in-use, then feed the allocator that same
|
||
|
|
// address first and a new one on retry.
|
||
|
|
first := net.ParseIP("2602:817:3000:f001::1")
|
||
|
|
second := net.ParseIP("2602:817:3000:f001::2")
|
||
|
|
i.MarkInUse(first)
|
||
|
|
i.randSrc = &fakeRand{
|
||
|
|
iids: [][]byte{
|
||
|
|
append(make([]byte, 8), 0, 0, 0, 0, 0, 0, 0, 0x01), // -> ...:1 (collides)
|
||
|
|
append(make([]byte, 8), 0, 0, 0, 0, 0, 0, 0, 0x02), // -> ...:2 (ok)
|
||
|
|
},
|
||
|
|
}
|
||
|
|
res, err := i.Allocate(AllocRequest{
|
||
|
|
ContainerID: "c1", Namespace: "ns", Pod: "p", WantV6: true,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Allocate: %v", err)
|
||
|
|
}
|
||
|
|
if !res.IP6.Equal(second) {
|
||
|
|
t.Fatalf("IP6 = %s, want %s (collision retry)", res.IP6, second)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIPAM_AllocV4_SkipsNetworkAndGateway(t *testing.T) {
|
||
|
|
i, err := NewIPAM(nil, []string{"172.25.210.0/24"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
i.randSrc = &fakeRand{}
|
||
|
|
res, err := i.Allocate(AllocRequest{
|
||
|
|
ContainerID: "c1", Namespace: "ns", Pod: "p", WantV4: true,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Allocate: %v", err)
|
||
|
|
}
|
||
|
|
if res.IP4.String() != "172.25.210.2" {
|
||
|
|
t.Fatalf("IP4 = %s, want 172.25.210.2 (skip .0 network + .1 gateway)", res.IP4)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIPAM_AllocV4_Sequential(t *testing.T) {
|
||
|
|
i, err := NewIPAM(nil, []string{"172.25.210.0/29"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
i.randSrc = &fakeRand{}
|
||
|
|
want := []string{"172.25.210.2", "172.25.210.3", "172.25.210.4", "172.25.210.5", "172.25.210.6"}
|
||
|
|
for _, w := range want {
|
||
|
|
res, err := i.Allocate(AllocRequest{ContainerID: w, WantV4: true})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Allocate: %v", err)
|
||
|
|
}
|
||
|
|
if res.IP4.String() != w {
|
||
|
|
t.Fatalf("got %s, want %s", res.IP4, w)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Next allocation should fail — /29 exhausted (.0 network, .1 gateway, .7 broadcast).
|
||
|
|
if _, err := i.Allocate(AllocRequest{ContainerID: "extra", WantV4: true}); err == nil {
|
||
|
|
t.Fatalf("expected exhaustion error")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIPAM_DualStack(t *testing.T) {
|
||
|
|
i, err := NewIPAM(
|
||
|
|
[]string{"2602:817:3000:f001::/64"},
|
||
|
|
[]string{"172.25.210.0/24"},
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
i.randSrc = &fakeRand{
|
||
|
|
iids: [][]byte{
|
||
|
|
{0, 0, 0, 0, 0, 0, 0, 0, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
res, err := i.Allocate(AllocRequest{
|
||
|
|
ContainerID: "c1", WantV6: true, WantV4: true,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Allocate: %v", err)
|
||
|
|
}
|
||
|
|
if res.IP6 == nil || res.IP4 == nil {
|
||
|
|
t.Fatalf("dual-stack result missing IPs: %+v", res)
|
||
|
|
}
|
||
|
|
if res.IP4.String() != "172.25.210.2" {
|
||
|
|
t.Fatalf("IP4 = %s", res.IP4)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIPAM_RejectsNoFamily(t *testing.T) {
|
||
|
|
i, _ := NewIPAM([]string{"2602:817:3000:f001::/64"}, nil)
|
||
|
|
if _, err := i.Allocate(AllocRequest{ContainerID: "c"}); err == nil {
|
||
|
|
t.Fatalf("expected error for no v6/v4")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIPAM_Release(t *testing.T) {
|
||
|
|
i, _ := NewIPAM([]string{"2602:817:3000:f001::/64"}, nil)
|
||
|
|
i.randSrc = &fakeRand{
|
||
|
|
iids: [][]byte{
|
||
|
|
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
|
||
|
|
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
res1, err := i.Allocate(AllocRequest{ContainerID: "c1", WantV6: true})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
i.Release(res1.IP6)
|
||
|
|
// Next Allocate should be free to pick the same address again.
|
||
|
|
res2, err := i.Allocate(AllocRequest{ContainerID: "c2", WantV6: true})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !res2.IP6.Equal(res1.IP6) {
|
||
|
|
t.Fatalf("expected release to free %s; got %s", res1.IP6, res2.IP6)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIPAM_RejectsMismatchedFamily(t *testing.T) {
|
||
|
|
if _, err := NewIPAM([]string{"10.0.0.0/24"}, nil); err == nil {
|
||
|
|
t.Fatalf("expected v4 in cidr6 to fail")
|
||
|
|
}
|
||
|
|
if _, err := NewIPAM(nil, []string{"2602:817:3000::/64"}); err == nil {
|
||
|
|
t.Fatalf("expected v6 in cidr4 to fail")
|
||
|
|
}
|
||
|
|
}
|