|
|
|
|
@ -16,7 +16,571 @@ import ( |
|
|
|
|
"tailscale.com/types/ptr" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
func TestRegression(t *testing.T) { |
|
|
|
|
// These tests are specific triggers for subtle correctness issues
|
|
|
|
|
// that came up during initial implementation. Even if they seem
|
|
|
|
|
// arbitrary, please do not clean them up. They are checking edge
|
|
|
|
|
// cases that are very easy to get wrong, and quite difficult for
|
|
|
|
|
// the other statistical tests to trigger promptly.
|
|
|
|
|
|
|
|
|
|
t.Run("prefixes_aligned_on_stride_boundary", func(t *testing.T) { |
|
|
|
|
// Regression test for computePrefixSplit called with equal
|
|
|
|
|
// arguments.
|
|
|
|
|
tbl := &Table[int]{} |
|
|
|
|
slow := slowPrefixTable[int]{} |
|
|
|
|
p := netip.MustParsePrefix |
|
|
|
|
|
|
|
|
|
v := ptr.To(1) |
|
|
|
|
tbl.Insert(p("226.205.197.0/24"), v) |
|
|
|
|
slow.insert(p("226.205.197.0/24"), v) |
|
|
|
|
v = ptr.To(2) |
|
|
|
|
tbl.Insert(p("226.205.0.0/16"), v) |
|
|
|
|
slow.insert(p("226.205.0.0/16"), v) |
|
|
|
|
|
|
|
|
|
probe := netip.MustParseAddr("226.205.121.152") |
|
|
|
|
got, want := tbl.Get(probe), slow.get(probe) |
|
|
|
|
if got != want { |
|
|
|
|
t.Fatalf("got %v, want %v", got, want) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("parent_prefix_inserted_in_different_orders", func(t *testing.T) { |
|
|
|
|
// Regression test for the off-by-one correction applied
|
|
|
|
|
// within computePrefixSplit.
|
|
|
|
|
t1, t2 := &Table[int]{}, &Table[int]{} |
|
|
|
|
p := netip.MustParsePrefix |
|
|
|
|
v1, v2 := ptr.To(1), ptr.To(2) |
|
|
|
|
|
|
|
|
|
t1.Insert(p("136.20.0.0/16"), v1) |
|
|
|
|
t1.Insert(p("136.20.201.62/32"), v2) |
|
|
|
|
|
|
|
|
|
t2.Insert(p("136.20.201.62/32"), v2) |
|
|
|
|
t2.Insert(p("136.20.0.0/16"), v1) |
|
|
|
|
|
|
|
|
|
a := netip.MustParseAddr("136.20.54.139") |
|
|
|
|
got, want := t2.Get(a), t1.Get(a) |
|
|
|
|
if got != want { |
|
|
|
|
t.Errorf("Get(%q) is insertion order dependent (t1=%v, t2=%v)", a, want, got) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestComputePrefixSplit(t *testing.T) { |
|
|
|
|
// These tests are partially redundant with other tests. Please
|
|
|
|
|
// keep them anyway. computePrefixSplit's behavior is remarkably
|
|
|
|
|
// subtle, and all the test cases listed below come from
|
|
|
|
|
// hard-earned debugging of malformed route tables.
|
|
|
|
|
|
|
|
|
|
var tests = []struct { |
|
|
|
|
// prefixA can be a /8, /16 or /24 (v4).
|
|
|
|
|
// prefixB can be anything /9 or more specific.
|
|
|
|
|
prefixA, prefixB string |
|
|
|
|
lastCommon string |
|
|
|
|
aStride, bStride uint8 |
|
|
|
|
}{ |
|
|
|
|
{"192.168.1.0/24", "192.168.5.5/32", "192.168.0.0/16", 1, 5}, |
|
|
|
|
{"192.168.129.0/24", "192.168.128.0/17", "192.168.0.0/16", 129, 128}, |
|
|
|
|
{"192.168.5.0/24", "192.168.0.0/16", "192.0.0.0/8", 168, 168}, |
|
|
|
|
{"192.168.0.0/16", "192.168.0.0/16", "192.0.0.0/8", 168, 168}, |
|
|
|
|
{"ff:aaaa:aaaa::1/128", "ff:aaaa::/120", "ff:aaaa::/32", 170, 0}, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for _, test := range tests { |
|
|
|
|
a, b := netip.MustParsePrefix(test.prefixA), netip.MustParsePrefix(test.prefixB) |
|
|
|
|
gotLastCommon, gotAStride, gotBStride := computePrefixSplit(a, b) |
|
|
|
|
if want := netip.MustParsePrefix(test.lastCommon); gotLastCommon != want || gotAStride != test.aStride || gotBStride != test.bStride { |
|
|
|
|
t.Errorf("computePrefixSplit(%q, %q) = %s, %d, %d; want %s, %d, %d", a, b, gotLastCommon, gotAStride, gotBStride, want, test.aStride, test.bStride) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestInsert(t *testing.T) { |
|
|
|
|
tbl := &Table[int]{} |
|
|
|
|
p := netip.MustParsePrefix |
|
|
|
|
|
|
|
|
|
// Create a new leaf strideTable, with compressed path
|
|
|
|
|
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.168.0.2", -1}, |
|
|
|
|
{"192.168.0.3", -1}, |
|
|
|
|
{"192.168.0.255", -1}, |
|
|
|
|
{"192.168.1.1", -1}, |
|
|
|
|
{"192.170.1.1", -1}, |
|
|
|
|
{"192.180.0.1", -1}, |
|
|
|
|
{"192.180.3.5", -1}, |
|
|
|
|
{"10.0.0.5", -1}, |
|
|
|
|
{"10.0.0.15", -1}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Insert into previous leaf, no tree changes
|
|
|
|
|
tbl.Insert(p("192.168.0.2/32"), ptr.To(2)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.168.0.2", 2}, |
|
|
|
|
{"192.168.0.3", -1}, |
|
|
|
|
{"192.168.0.255", -1}, |
|
|
|
|
{"192.168.1.1", -1}, |
|
|
|
|
{"192.170.1.1", -1}, |
|
|
|
|
{"192.180.0.1", -1}, |
|
|
|
|
{"192.180.3.5", -1}, |
|
|
|
|
{"10.0.0.5", -1}, |
|
|
|
|
{"10.0.0.15", -1}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Insert into previous leaf, unaligned prefix covering the /32s
|
|
|
|
|
tbl.Insert(p("192.168.0.0/26"), ptr.To(7)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.168.0.2", 2}, |
|
|
|
|
{"192.168.0.3", 7}, |
|
|
|
|
{"192.168.0.255", -1}, |
|
|
|
|
{"192.168.1.1", -1}, |
|
|
|
|
{"192.170.1.1", -1}, |
|
|
|
|
{"192.180.0.1", -1}, |
|
|
|
|
{"192.180.3.5", -1}, |
|
|
|
|
{"10.0.0.5", -1}, |
|
|
|
|
{"10.0.0.15", -1}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Create a different leaf elsewhere
|
|
|
|
|
tbl.Insert(p("10.0.0.0/27"), ptr.To(3)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.168.0.2", 2}, |
|
|
|
|
{"192.168.0.3", 7}, |
|
|
|
|
{"192.168.0.255", -1}, |
|
|
|
|
{"192.168.1.1", -1}, |
|
|
|
|
{"192.170.1.1", -1}, |
|
|
|
|
{"192.180.0.1", -1}, |
|
|
|
|
{"192.180.3.5", -1}, |
|
|
|
|
{"10.0.0.5", 3}, |
|
|
|
|
{"10.0.0.15", 3}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Insert that creates a new intermediate table and a new child
|
|
|
|
|
tbl.Insert(p("192.168.1.1/32"), ptr.To(4)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.168.0.2", 2}, |
|
|
|
|
{"192.168.0.3", 7}, |
|
|
|
|
{"192.168.0.255", -1}, |
|
|
|
|
{"192.168.1.1", 4}, |
|
|
|
|
{"192.170.1.1", -1}, |
|
|
|
|
{"192.180.0.1", -1}, |
|
|
|
|
{"192.180.3.5", -1}, |
|
|
|
|
{"10.0.0.5", 3}, |
|
|
|
|
{"10.0.0.15", 3}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Insert that creates a new intermediate table but no new child
|
|
|
|
|
tbl.Insert(p("192.170.0.0/16"), ptr.To(5)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.168.0.2", 2}, |
|
|
|
|
{"192.168.0.3", 7}, |
|
|
|
|
{"192.168.0.255", -1}, |
|
|
|
|
{"192.168.1.1", 4}, |
|
|
|
|
{"192.170.1.1", 5}, |
|
|
|
|
{"192.180.0.1", -1}, |
|
|
|
|
{"192.180.3.5", -1}, |
|
|
|
|
{"10.0.0.5", 3}, |
|
|
|
|
{"10.0.0.15", 3}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// New leaf in a different subtree, so the next insert can test a
|
|
|
|
|
// variant of decompression.
|
|
|
|
|
tbl.Insert(p("192.180.0.1/32"), ptr.To(8)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.168.0.2", 2}, |
|
|
|
|
{"192.168.0.3", 7}, |
|
|
|
|
{"192.168.0.255", -1}, |
|
|
|
|
{"192.168.1.1", 4}, |
|
|
|
|
{"192.170.1.1", 5}, |
|
|
|
|
{"192.180.0.1", 8}, |
|
|
|
|
{"192.180.3.5", -1}, |
|
|
|
|
{"10.0.0.5", 3}, |
|
|
|
|
{"10.0.0.15", 3}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Insert that creates a new intermediate table but no new child,
|
|
|
|
|
// with an unaligned intermediate
|
|
|
|
|
tbl.Insert(p("192.180.0.0/21"), ptr.To(9)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.168.0.2", 2}, |
|
|
|
|
{"192.168.0.3", 7}, |
|
|
|
|
{"192.168.0.255", -1}, |
|
|
|
|
{"192.168.1.1", 4}, |
|
|
|
|
{"192.170.1.1", 5}, |
|
|
|
|
{"192.180.0.1", 8}, |
|
|
|
|
{"192.180.3.5", 9}, |
|
|
|
|
{"10.0.0.5", 3}, |
|
|
|
|
{"10.0.0.15", 3}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Insert a default route, those have their own codepath.
|
|
|
|
|
tbl.Insert(p("0.0.0.0/0"), ptr.To(6)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.168.0.2", 2}, |
|
|
|
|
{"192.168.0.3", 7}, |
|
|
|
|
{"192.168.0.255", 6}, |
|
|
|
|
{"192.168.1.1", 4}, |
|
|
|
|
{"192.170.1.1", 5}, |
|
|
|
|
{"192.180.0.1", 8}, |
|
|
|
|
{"192.180.3.5", 9}, |
|
|
|
|
{"10.0.0.5", 3}, |
|
|
|
|
{"10.0.0.15", 3}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Now all of the above again, but for IPv6.
|
|
|
|
|
|
|
|
|
|
// Create a new leaf strideTable, with compressed path
|
|
|
|
|
tbl.Insert(p("ff:aaaa::1/128"), ptr.To(1)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"ff:aaaa::1", 1}, |
|
|
|
|
{"ff:aaaa::2", -1}, |
|
|
|
|
{"ff:aaaa::3", -1}, |
|
|
|
|
{"ff:aaaa::255", -1}, |
|
|
|
|
{"ff:aaaa:aaaa::1", -1}, |
|
|
|
|
{"ff:aaaa:aaaa:bbbb::1", -1}, |
|
|
|
|
{"ff:cccc::1", -1}, |
|
|
|
|
{"ff:cccc::ff", -1}, |
|
|
|
|
{"ffff:bbbb::5", -1}, |
|
|
|
|
{"ffff:bbbb::15", -1}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Insert into previous leaf, no tree changes
|
|
|
|
|
tbl.Insert(p("ff:aaaa::2/128"), ptr.To(2)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"ff:aaaa::1", 1}, |
|
|
|
|
{"ff:aaaa::2", 2}, |
|
|
|
|
{"ff:aaaa::3", -1}, |
|
|
|
|
{"ff:aaaa::255", -1}, |
|
|
|
|
{"ff:aaaa:aaaa::1", -1}, |
|
|
|
|
{"ff:aaaa:aaaa:bbbb::1", -1}, |
|
|
|
|
{"ff:cccc::1", -1}, |
|
|
|
|
{"ff:cccc::ff", -1}, |
|
|
|
|
{"ffff:bbbb::5", -1}, |
|
|
|
|
{"ffff:bbbb::15", -1}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Insert into previous leaf, unaligned prefix covering the /128s
|
|
|
|
|
tbl.Insert(p("ff:aaaa::/125"), ptr.To(7)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"ff:aaaa::1", 1}, |
|
|
|
|
{"ff:aaaa::2", 2}, |
|
|
|
|
{"ff:aaaa::3", 7}, |
|
|
|
|
{"ff:aaaa::255", -1}, |
|
|
|
|
{"ff:aaaa:aaaa::1", -1}, |
|
|
|
|
{"ff:aaaa:aaaa:bbbb::1", -1}, |
|
|
|
|
{"ff:cccc::1", -1}, |
|
|
|
|
{"ff:cccc::ff", -1}, |
|
|
|
|
{"ffff:bbbb::5", -1}, |
|
|
|
|
{"ffff:bbbb::15", -1}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Create a different leaf elsewhere
|
|
|
|
|
tbl.Insert(p("ffff:bbbb::/120"), ptr.To(3)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"ff:aaaa::1", 1}, |
|
|
|
|
{"ff:aaaa::2", 2}, |
|
|
|
|
{"ff:aaaa::3", 7}, |
|
|
|
|
{"ff:aaaa::255", -1}, |
|
|
|
|
{"ff:aaaa:aaaa::1", -1}, |
|
|
|
|
{"ff:aaaa:aaaa:bbbb::1", -1}, |
|
|
|
|
{"ff:cccc::1", -1}, |
|
|
|
|
{"ff:cccc::ff", -1}, |
|
|
|
|
{"ffff:bbbb::5", 3}, |
|
|
|
|
{"ffff:bbbb::15", 3}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Insert that creates a new intermediate table and a new child
|
|
|
|
|
tbl.Insert(p("ff:aaaa:aaaa::1/128"), ptr.To(4)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"ff:aaaa::1", 1}, |
|
|
|
|
{"ff:aaaa::2", 2}, |
|
|
|
|
{"ff:aaaa::3", 7}, |
|
|
|
|
{"ff:aaaa::255", -1}, |
|
|
|
|
{"ff:aaaa:aaaa::1", 4}, |
|
|
|
|
{"ff:aaaa:aaaa:bbbb::1", -1}, |
|
|
|
|
{"ff:cccc::1", -1}, |
|
|
|
|
{"ff:cccc::ff", -1}, |
|
|
|
|
{"ffff:bbbb::5", 3}, |
|
|
|
|
{"ffff:bbbb::15", 3}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Insert that creates a new intermediate table but no new child
|
|
|
|
|
tbl.Insert(p("ff:aaaa:aaaa:bb00::/56"), ptr.To(5)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"ff:aaaa::1", 1}, |
|
|
|
|
{"ff:aaaa::2", 2}, |
|
|
|
|
{"ff:aaaa::3", 7}, |
|
|
|
|
{"ff:aaaa::255", -1}, |
|
|
|
|
{"ff:aaaa:aaaa::1", 4}, |
|
|
|
|
{"ff:aaaa:aaaa:bbbb::1", 5}, |
|
|
|
|
{"ff:cccc::1", -1}, |
|
|
|
|
{"ff:cccc::ff", -1}, |
|
|
|
|
{"ffff:bbbb::5", 3}, |
|
|
|
|
{"ffff:bbbb::15", 3}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// New leaf in a different subtree, so the next insert can test a
|
|
|
|
|
// variant of decompression.
|
|
|
|
|
tbl.Insert(p("ff:cccc::1/128"), ptr.To(8)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"ff:aaaa::1", 1}, |
|
|
|
|
{"ff:aaaa::2", 2}, |
|
|
|
|
{"ff:aaaa::3", 7}, |
|
|
|
|
{"ff:aaaa::255", -1}, |
|
|
|
|
{"ff:aaaa:aaaa::1", 4}, |
|
|
|
|
{"ff:aaaa:aaaa:bbbb::1", 5}, |
|
|
|
|
{"ff:cccc::1", 8}, |
|
|
|
|
{"ff:cccc::ff", -1}, |
|
|
|
|
{"ffff:bbbb::5", 3}, |
|
|
|
|
{"ffff:bbbb::15", 3}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Insert that creates a new intermediate table but no new child,
|
|
|
|
|
// with an unaligned intermediate
|
|
|
|
|
tbl.Insert(p("ff:cccc::/37"), ptr.To(9)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"ff:aaaa::1", 1}, |
|
|
|
|
{"ff:aaaa::2", 2}, |
|
|
|
|
{"ff:aaaa::3", 7}, |
|
|
|
|
{"ff:aaaa::255", -1}, |
|
|
|
|
{"ff:aaaa:aaaa::1", 4}, |
|
|
|
|
{"ff:aaaa:aaaa:bbbb::1", 5}, |
|
|
|
|
{"ff:cccc::1", 8}, |
|
|
|
|
{"ff:cccc::ff", 9}, |
|
|
|
|
{"ffff:bbbb::5", 3}, |
|
|
|
|
{"ffff:bbbb::15", 3}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Insert a default route, those have their own codepath.
|
|
|
|
|
tbl.Insert(p("::/0"), ptr.To(6)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"ff:aaaa::1", 1}, |
|
|
|
|
{"ff:aaaa::2", 2}, |
|
|
|
|
{"ff:aaaa::3", 7}, |
|
|
|
|
{"ff:aaaa::255", 6}, |
|
|
|
|
{"ff:aaaa:aaaa::1", 4}, |
|
|
|
|
{"ff:aaaa:aaaa:bbbb::1", 5}, |
|
|
|
|
{"ff:cccc::1", 8}, |
|
|
|
|
{"ff:cccc::ff", 9}, |
|
|
|
|
{"ffff:bbbb::5", 3}, |
|
|
|
|
{"ffff:bbbb::15", 3}, |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestDelete(t *testing.T) { |
|
|
|
|
t.Parallel() |
|
|
|
|
p := netip.MustParsePrefix |
|
|
|
|
|
|
|
|
|
t.Run("prefix_in_root", func(t *testing.T) { |
|
|
|
|
// Add/remove prefix from root table.
|
|
|
|
|
tbl := &Table[int]{} |
|
|
|
|
checkSize(t, tbl, 2) |
|
|
|
|
|
|
|
|
|
tbl.Insert(p("10.0.0.0/8"), ptr.To(1)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"10.0.0.1", 1}, |
|
|
|
|
{"255.255.255.255", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 2) |
|
|
|
|
tbl.Delete(p("10.0.0.0/8")) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"10.0.0.1", -1}, |
|
|
|
|
{"255.255.255.255", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 2) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("prefix_in_leaf", func(t *testing.T) { |
|
|
|
|
// Create, then delete a single leaf table.
|
|
|
|
|
tbl := &Table[int]{} |
|
|
|
|
checkSize(t, tbl, 2) |
|
|
|
|
|
|
|
|
|
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"255.255.255.255", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 3) |
|
|
|
|
tbl.Delete(p("192.168.0.1/32")) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", -1}, |
|
|
|
|
{"255.255.255.255", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 2) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("intermediate_no_routes", func(t *testing.T) { |
|
|
|
|
// Create an intermediate with 2 children, then delete one leaf.
|
|
|
|
|
tbl := &Table[int]{} |
|
|
|
|
checkSize(t, tbl, 2) |
|
|
|
|
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) |
|
|
|
|
tbl.Insert(p("192.180.0.1/32"), ptr.To(2)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.180.0.1", 2}, |
|
|
|
|
{"192.40.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 5) // 2 roots, 1 intermediate, 2 leaves
|
|
|
|
|
tbl.Delete(p("192.180.0.1/32")) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.180.0.1", -1}, |
|
|
|
|
{"192.40.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 3) // 2 roots, 1 leaf
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("intermediate_with_route", func(t *testing.T) { |
|
|
|
|
// Same, but the intermediate carries a route as well.
|
|
|
|
|
tbl := &Table[int]{} |
|
|
|
|
checkSize(t, tbl, 2) |
|
|
|
|
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) |
|
|
|
|
tbl.Insert(p("192.180.0.1/32"), ptr.To(2)) |
|
|
|
|
tbl.Insert(p("192.0.0.0/10"), ptr.To(3)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.180.0.1", 2}, |
|
|
|
|
{"192.40.0.1", 3}, |
|
|
|
|
{"192.255.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 5) // 2 roots, 1 intermediate, 2 leaves
|
|
|
|
|
tbl.Delete(p("192.180.0.1/32")) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.180.0.1", -1}, |
|
|
|
|
{"192.40.0.1", 3}, |
|
|
|
|
{"192.255.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 4) // 2 roots, 1 intermediate w/route, 1 leaf
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("intermediate_many_leaves", func(t *testing.T) { |
|
|
|
|
// Intermediate with 3 leaves, then delete one leaf.
|
|
|
|
|
tbl := &Table[int]{} |
|
|
|
|
checkSize(t, tbl, 2) |
|
|
|
|
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) |
|
|
|
|
tbl.Insert(p("192.180.0.1/32"), ptr.To(2)) |
|
|
|
|
tbl.Insert(p("192.200.0.1/32"), ptr.To(3)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.180.0.1", 2}, |
|
|
|
|
{"192.200.0.1", 3}, |
|
|
|
|
{"192.255.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 6) // 2 roots, 1 intermediate, 3 leaves
|
|
|
|
|
tbl.Delete(p("192.180.0.1/32")) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.180.0.1", -1}, |
|
|
|
|
{"192.200.0.1", 3}, |
|
|
|
|
{"192.255.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 5) // 2 roots, 1 intermediate, 2 leaves
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("nosuchprefix_missing_child", func(t *testing.T) { |
|
|
|
|
// Delete non-existent prefix, missing strideTable path.
|
|
|
|
|
tbl := &Table[int]{} |
|
|
|
|
checkSize(t, tbl, 2) |
|
|
|
|
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.255.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 3) // 2 roots, 1 leaf
|
|
|
|
|
tbl.Delete(p("200.0.0.0/32")) // lookup miss in root
|
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.255.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 3) // 2 roots, 1 leaf
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("nosuchprefix_wrong_turn", func(t *testing.T) { |
|
|
|
|
// Delete non-existent prefix, strideTable path exists but
|
|
|
|
|
// with a wrong turn.
|
|
|
|
|
tbl := &Table[int]{} |
|
|
|
|
checkSize(t, tbl, 2) |
|
|
|
|
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.255.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 3) // 2 roots, 1 leaf
|
|
|
|
|
tbl.Delete(p("192.40.0.0/32")) // finds wrong child
|
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.255.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 3) // 2 roots, 1 leaf
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("nosuchprefix_not_in_leaf", func(t *testing.T) { |
|
|
|
|
// Delete non-existent prefix, strideTable path exists but
|
|
|
|
|
// leaf doesn't contain route.
|
|
|
|
|
tbl := &Table[int]{} |
|
|
|
|
checkSize(t, tbl, 2) |
|
|
|
|
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.255.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 3) // 2 roots, 1 leaf
|
|
|
|
|
tbl.Delete(p("192.168.0.5/32")) // right leaf, no route
|
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.255.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 3) // 2 roots, 1 leaf
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("intermediate_with_deleted_route", func(t *testing.T) { |
|
|
|
|
// Intermediate table loses its last route and becomes
|
|
|
|
|
// compactable.
|
|
|
|
|
tbl := &Table[int]{} |
|
|
|
|
checkSize(t, tbl, 2) |
|
|
|
|
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) |
|
|
|
|
tbl.Insert(p("192.168.0.0/22"), ptr.To(2)) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.168.0.2", 2}, |
|
|
|
|
{"192.255.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 4) // 2 roots, 1 intermediate w/route, 1 leaf
|
|
|
|
|
tbl.Delete(p("192.168.0.0/22")) |
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"192.168.0.1", 1}, |
|
|
|
|
{"192.168.0.2", -1}, |
|
|
|
|
{"192.255.0.1", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 3) // 2 roots, 1 leaf
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("default_route", func(t *testing.T) { |
|
|
|
|
// Default routes have a special case in the code.
|
|
|
|
|
tbl := &Table[int]{} |
|
|
|
|
|
|
|
|
|
tbl.Insert(p("0.0.0.0/0"), ptr.To(1)) |
|
|
|
|
tbl.Delete(p("0.0.0.0/0")) |
|
|
|
|
|
|
|
|
|
checkRoutes(t, tbl, []tableTest{ |
|
|
|
|
{"1.2.3.4", -1}, |
|
|
|
|
}) |
|
|
|
|
checkSize(t, tbl, 2) // 2 roots
|
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestInsertCompare(t *testing.T) { |
|
|
|
|
// Create large route tables repeatedly, and compare Table's
|
|
|
|
|
// behavior to a naive and slow but correct implementation.
|
|
|
|
|
t.Parallel() |
|
|
|
|
pfxs := randomPrefixes(10_000) |
|
|
|
|
|
|
|
|
|
@ -27,7 +591,9 @@ func TestInsert(t *testing.T) { |
|
|
|
|
fast.Insert(pfx.pfx, pfx.val) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
t.Logf(fast.debugSummary()) |
|
|
|
|
if debugInsert { |
|
|
|
|
t.Logf(fast.debugSummary()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
seenVals4 := map[*int]bool{} |
|
|
|
|
seenVals6 := map[*int]bool{} |
|
|
|
|
@ -44,6 +610,7 @@ func TestInsert(t *testing.T) { |
|
|
|
|
t.Errorf("get(%q) = %p, want %p", a, fastVal, slowVal) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Empirically, 10k probes into 5k v4 prefixes and 5k v6 prefixes results in
|
|
|
|
|
// ~1k distinct values for v4 and ~300 for v6. distinct routes. This sanity
|
|
|
|
|
// check that we didn't just return a single route for everything should be
|
|
|
|
|
@ -57,36 +624,65 @@ func TestInsert(t *testing.T) { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestInsertShuffled(t *testing.T) { |
|
|
|
|
// The order in which you insert prefixes into a route table
|
|
|
|
|
// should not matter, as long as you're inserting the same set of
|
|
|
|
|
// routes. Verify that this is true, because ART does execute
|
|
|
|
|
// vastly different code depending on the order of insertion, even
|
|
|
|
|
// if the end result is identical.
|
|
|
|
|
//
|
|
|
|
|
// If you're here because this package's tests are slow and you
|
|
|
|
|
// want to make them faster, please do not delete this test (or
|
|
|
|
|
// any test, really). It may seem excessive to test this, but
|
|
|
|
|
// these shuffle tests found a lot of very nasty edge cases during
|
|
|
|
|
// development, and you _really_ don't want to be debugging a
|
|
|
|
|
// faulty route table in production.
|
|
|
|
|
t.Parallel() |
|
|
|
|
pfxs := randomPrefixes(10_000) |
|
|
|
|
pfxs := randomPrefixes(1000) |
|
|
|
|
var pfxs2 []slowPrefixEntry[int] |
|
|
|
|
|
|
|
|
|
rt := Table[int]{} |
|
|
|
|
for _, pfx := range pfxs { |
|
|
|
|
rt.Insert(pfx.pfx, pfx.val) |
|
|
|
|
} |
|
|
|
|
defer func() { |
|
|
|
|
if t.Failed() { |
|
|
|
|
t.Logf("pre-shuffle: %#v", pfxs) |
|
|
|
|
t.Logf("post-shuffle: %#v", pfxs2) |
|
|
|
|
} |
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
for i := 0; i < 10; i++ { |
|
|
|
|
pfxs2 := append([]slowPrefixEntry[int](nil), pfxs...) |
|
|
|
|
rand.Shuffle(len(pfxs2), func(i, j int) { pfxs2[i], pfxs2[j] = pfxs2[j], pfxs2[i] }) |
|
|
|
|
|
|
|
|
|
addrs := make([]netip.Addr, 0, 10_000) |
|
|
|
|
for i := 0; i < 10_000; i++ { |
|
|
|
|
addrs = append(addrs, randomAddr()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
rt := Table[int]{} |
|
|
|
|
rt2 := Table[int]{} |
|
|
|
|
|
|
|
|
|
for _, pfx := range pfxs { |
|
|
|
|
rt.Insert(pfx.pfx, pfx.val) |
|
|
|
|
} |
|
|
|
|
for _, pfx := range pfxs2 { |
|
|
|
|
rt2.Insert(pfx.pfx, pfx.val) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Diffing a deep tree of tables gives cmp.Diff a nervous breakdown, so
|
|
|
|
|
// test for equivalence statistically with random probes instead.
|
|
|
|
|
for i := 0; i < 10_000; i++ { |
|
|
|
|
a := randomAddr() |
|
|
|
|
for _, a := range addrs { |
|
|
|
|
val1 := rt.Get(a) |
|
|
|
|
val2 := rt2.Get(a) |
|
|
|
|
if val1 == nil && val2 == nil { |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
if (val1 == nil && val2 != nil) || (val1 != nil && val2 == nil) || (*val1 != *val2) { |
|
|
|
|
t.Errorf("get(%q) = %s, want %s", a, printIntPtr(val2), printIntPtr(val1)) |
|
|
|
|
t.Fatalf("get(%q) = %s, want %s", a, printIntPtr(val2), printIntPtr(val1)) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestDelete(t *testing.T) { |
|
|
|
|
func TestDeleteCompare(t *testing.T) { |
|
|
|
|
// Create large route tables repeatedly, delete half of their
|
|
|
|
|
// prefixes, and compare Table's behavior to a naive and slow but
|
|
|
|
|
// correct implementation.
|
|
|
|
|
t.Parallel() |
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
@ -104,6 +700,19 @@ func TestDelete(t *testing.T) { |
|
|
|
|
toDelete := append([]slowPrefixEntry[int](nil), all4[deleteCut:]...) |
|
|
|
|
toDelete = append(toDelete, all6[deleteCut:]...) |
|
|
|
|
|
|
|
|
|
defer func() { |
|
|
|
|
if t.Failed() { |
|
|
|
|
for _, pfx := range pfxs { |
|
|
|
|
fmt.Printf("%q, ", pfx.pfx) |
|
|
|
|
} |
|
|
|
|
fmt.Println("") |
|
|
|
|
for _, pfx := range toDelete { |
|
|
|
|
fmt.Printf("%q, ", pfx.pfx) |
|
|
|
|
} |
|
|
|
|
fmt.Println("") |
|
|
|
|
} |
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
slow := slowPrefixTable[int]{pfxs} |
|
|
|
|
fast := Table[int]{} |
|
|
|
|
|
|
|
|
|
@ -146,6 +755,18 @@ func TestDelete(t *testing.T) { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestDeleteShuffled(t *testing.T) { |
|
|
|
|
// The order in which you delete prefixes from a route table
|
|
|
|
|
// should not matter, as long as you're deleting the same set of
|
|
|
|
|
// routes. Verify that this is true, because ART does execute
|
|
|
|
|
// vastly different code depending on the order of deletions, even
|
|
|
|
|
// if the end result is identical.
|
|
|
|
|
//
|
|
|
|
|
// If you're here because this package's tests are slow and you
|
|
|
|
|
// want to make them faster, please do not delete this test (or
|
|
|
|
|
// any test, really). It may seem excessive to test this, but
|
|
|
|
|
// these shuffle tests found a lot of very nasty edge cases during
|
|
|
|
|
// development, and you _really_ don't want to be debugging a
|
|
|
|
|
// faulty route table in production.
|
|
|
|
|
t.Parallel() |
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
@ -205,6 +826,29 @@ func TestDeleteShuffled(t *testing.T) { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type tableTest struct { |
|
|
|
|
// addr is an IP address string to look up in a route table.
|
|
|
|
|
addr string |
|
|
|
|
// want is the expected >=0 value associated with the route, or -1
|
|
|
|
|
// if we expect a lookup miss.
|
|
|
|
|
want int |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// checkRoutes verifies that the route lookups in tt return the
|
|
|
|
|
// expected results on tbl.
|
|
|
|
|
func checkRoutes(t *testing.T, tbl *Table[int], tt []tableTest) { |
|
|
|
|
t.Helper() |
|
|
|
|
for _, tc := range tt { |
|
|
|
|
v := tbl.Get(netip.MustParseAddr(tc.addr)) |
|
|
|
|
if v == nil && tc.want != -1 { |
|
|
|
|
t.Errorf("lookup %q got nil, want %d", tc.addr, tc.want) |
|
|
|
|
} |
|
|
|
|
if v != nil && *v != tc.want { |
|
|
|
|
t.Errorf("lookup %q got %d, want %d", tc.addr, *v, tc.want) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 100k routes for IPv6, at the current size of strideTable and strideEntry, is
|
|
|
|
|
// in the ballpark of 4GiB if you assume worst-case prefix distribution. Future
|
|
|
|
|
// optimizations will knock down the memory consumption by over an order of
|
|
|
|
|
@ -402,6 +1046,32 @@ func (t *runningTimer) Elapsed() time.Duration { |
|
|
|
|
return t.cumulative |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func checkSize(t *testing.T, tbl *Table[int], want int) { |
|
|
|
|
t.Helper() |
|
|
|
|
if got := tbl.numStrides(); got != want { |
|
|
|
|
t.Errorf("wrong table size, got %d strides want %d", got, want) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (t *Table[T]) numStrides() int { |
|
|
|
|
seen := map[*strideTable[T]]bool{} |
|
|
|
|
return t.numStridesRec(seen, &t.v4) + t.numStridesRec(seen, &t.v6) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (t *Table[T]) numStridesRec(seen map[*strideTable[T]]bool, st *strideTable[T]) int { |
|
|
|
|
ret := 1 |
|
|
|
|
if st.childRefs == 0 { |
|
|
|
|
return ret |
|
|
|
|
} |
|
|
|
|
for i := firstHostIndex; i <= lastHostIndex; i++ { |
|
|
|
|
if c := st.entries[i].child; c != nil && !seen[c] { |
|
|
|
|
seen[c] = true |
|
|
|
|
ret += t.numStridesRec(seen, c) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return ret |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// slowPrefixTable is a routing table implemented as a set of prefixes that are
|
|
|
|
|
// explicitly scanned in full for every route lookup. It is very slow, but also
|
|
|
|
|
// reasonably easy to verify by inspection, and so a good correctness reference
|
|
|
|
|
@ -548,3 +1218,26 @@ func roundFloat64(f float64) float64 { |
|
|
|
|
} |
|
|
|
|
return ret |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func minimize(pfxs []slowPrefixEntry[int], f func(skip map[netip.Prefix]bool) error) (map[netip.Prefix]bool, error) { |
|
|
|
|
if f(nil) == nil { |
|
|
|
|
return nil, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
remove := map[netip.Prefix]bool{} |
|
|
|
|
for lastLen := -1; len(remove) != lastLen; lastLen = len(remove) { |
|
|
|
|
fmt.Println("len is ", len(remove)) |
|
|
|
|
for i, pfx := range pfxs { |
|
|
|
|
if remove[pfx.pfx] { |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
remove[pfx.pfx] = true |
|
|
|
|
fmt.Printf("%d %d: trying without %s\n", i, len(remove), pfx.pfx) |
|
|
|
|
if f(remove) == nil { |
|
|
|
|
delete(remove, pfx.pfx) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return remove, f(remove) |
|
|
|
|
} |
|
|
|
|
|