diff --git a/pkg/ip/cidr_test.go b/pkg/ip/cidr_test.go index 5ede450..8e3eff1 100644 --- a/pkg/ip/cidr_test.go +++ b/pkg/ip/cidr_test.go @@ -100,6 +100,181 @@ func TestCIDRKeys_VerifyNetworkAddress(t *testing.T) { } } +func TestCIDRKeys_VerifyNetworkAddressIPv6(t *testing.T) { + keys := CIDRKeys("2001:db8:1:2:3:4:5:6") + tests := []struct { + bits int + want string + }{ + {64, "2001:db8:1:2::/64"}, + {48, "2001:db8:1::/48"}, + {32, "2001:db8::/32"}, + } + for _, tt := range tests { + if got := keys[128-tt.bits]; got != tt.want { + t.Errorf("/%d network should be %s, got %s", tt.bits, tt.want, got) + } + } +} + +// TestCIDRKeys_MatchNormalizeCIDR covers the invariant range support rests on: the key +// written for a decision is a key looked up for the IPs it covers, and only those. +func TestCIDRKeys_MatchNormalizeCIDR(t *testing.T) { + tests := []struct { + cidr string + ip string + match bool + }{ + {cidr: "10.0.0.0/8", ip: "10.1.2.3", match: true}, + {cidr: "10.0.0.0/24", ip: "10.0.0.1", match: true}, + {cidr: "10.0.0.0/24", ip: "10.0.1.1", match: false}, + {cidr: "1.2.3.4/32", ip: "1.2.3.4", match: true}, + {cidr: "1.2.3.4/32", ip: "1.2.3.5", match: false}, + {cidr: "0.0.0.0/0", ip: "8.8.8.8", match: true}, + // LAPI does not have to send a network address, the host bits are dropped. + {cidr: "10.0.0.5/24", ip: "10.0.0.9", match: true}, + {cidr: " 192.168.1.0/24 ", ip: "192.168.1.42", match: true}, + {cidr: "2001:db8::/32", ip: "2001:db8::1", match: true}, + {cidr: "2001:db8::/32", ip: "2001:db9::1", match: false}, + {cidr: "::/0", ip: "2001:db8::1", match: true}, + // An IPv4 range and an IPv4 mapped client still have to meet. + {cidr: "10.0.0.0/8", ip: "::ffff:10.1.2.3", match: true}, + {cidr: "::ffff:10.0.0.0/104", ip: "10.1.2.3", match: true}, + // Families do not mix. + {cidr: "::/0", ip: "8.8.8.8", match: false}, + {cidr: "2001:db8::/32", ip: "::ffff:10.0.0.1", match: false}, + } + for _, tt := range tests { + t.Run(tt.cidr+"_"+tt.ip, func(t *testing.T) { + key := NormalizeCIDR(tt.cidr) + if key == "" { + t.Fatalf("NormalizeCIDR(%q) returned nothing", tt.cidr) + } + found := false + for _, candidate := range CIDRKeys(tt.ip) { + if candidate == key { + found = true + break + } + } + if found != tt.match { + t.Errorf("key %q of %q found in CIDRKeys(%q) = %v, want %v", key, tt.cidr, tt.ip, found, tt.match) + } + }) + } +} + +func TestCIDRLookupKeys(t *testing.T) { + tests := []struct { + name string + ip string + prefixLens []int + want []string + }{ + { + name: "most specific first", + ip: "10.1.2.3", + prefixLens: []int{8, 32, 16}, + want: []string{"10.1.2.3/32", "10.1.0.0/16", "10.0.0.0/8"}, + }, + { + name: "duplicates are dropped", + ip: "10.1.2.3", + prefixLens: []int{24, 24, 24}, + want: []string{"10.1.2.0/24"}, + }, + { + name: "lengths of the other family are skipped", + ip: "10.1.2.3", + prefixLens: []int{48, 64, 24}, + want: []string{"10.1.2.0/24"}, + }, + { + name: "out of range lengths are skipped", + ip: "10.1.2.3", + prefixLens: []int{-1, 33, 129, 8}, + want: []string{"10.0.0.0/8"}, + }, + { + name: "ipv6 keeps its own lengths", + ip: "2001:db8::1", + prefixLens: []int{32, 64}, + want: []string{"2001:db8::/64", "2001:db8::/32"}, + }, + { + name: "no length gives no key", + ip: "10.1.2.3", + prefixLens: []int{}, + want: []string{}, + }, + { + name: "invalid ip gives no key", + ip: "invalid", + prefixLens: []int{24}, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CIDRLookupKeys(tt.ip, tt.prefixLens) + if len(got) != len(tt.want) { + t.Fatalf("CIDRLookupKeys(%q, %v) = %v, want %v", tt.ip, tt.prefixLens, got, tt.want) + } + for i := range got { + if got[i] != tt.want[i] { + t.Errorf("CIDRLookupKeys(%q, %v)[%d] = %q, want %q", tt.ip, tt.prefixLens, i, got[i], tt.want[i]) + } + } + }) + } +} + +// TestCIDRLookupKeys_SubsetOfCIDRKeys: restricting the lengths only removes candidates, +// it never changes the key of a length that is kept. +func TestCIDRLookupKeys_SubsetOfCIDRKeys(t *testing.T) { + for _, ipStr := range []string{"10.1.2.3", "2001:db8::1", "::ffff:10.1.2.3"} { + t.Run(ipStr, func(t *testing.T) { + all := CIDRKeys(ipStr) + maxBits := len(all) - 1 + for bits := 0; bits <= maxBits; bits++ { + got := CIDRLookupKeys(ipStr, []int{bits}) + if len(got) != 1 { + t.Fatalf("CIDRLookupKeys(%q, [%d]) returned %d keys", ipStr, bits, len(got)) + } + if want := all[maxBits-bits]; got[0] != want { + t.Errorf("CIDRLookupKeys(%q, [%d]) = %q, want %q", ipStr, bits, got[0], want) + } + } + }) + } +} + +func TestCIDRPrefixLen(t *testing.T) { + tests := []struct { + input string + want int + }{ + {"10.0.0.0/8", 8}, + {"10.0.0.0/32", 32}, + {"0.0.0.0/0", 0}, + {"10.0.0.5/24", 24}, + {" 10.0.0.0/16 ", 16}, + {"2001:db8::/32", 32}, + {"2001:db8::/128", 128}, + {"::/0", 0}, + {"10.0.0.1", -1}, + {"invalid", -1}, + {"", -1}, + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + if got := CIDRPrefixLen(tt.input); got != tt.want { + t.Errorf("CIDRPrefixLen(%q) = %d, want %d", tt.input, got, tt.want) + } + }) + } +} + func TestNormalizeCIDR(t *testing.T) { tests := []struct { input string