check / check (push) Canceled after 0s
The prefix handler ran one PeerDown per peer, one after another, and stopped taking messages at 20,000 held-back messages. Sessions going down together queued up their walks, so a returning full-table peer could reach that limit. The table's PeersDown now takes several peers out of every prefix in one walk. One walk runs at a time. The next one takes every peer whose session went down meanwhile, so a message is held back for two walks at most. Each walk takes about 2.6 seconds for each full-table peer it takes out. The limit is now 150,000 messages, about 150 MiB: enough when four full-table peers go down together. The status page shows how many messages each handler holds back. Model: opus-5-5
269 lines
8.1 KiB
Go
269 lines
8.1 KiB
Go
package routetable_test
|
|
|
|
import (
|
|
"net/netip"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/routetable"
|
|
)
|
|
|
|
// announceAll has each listed peer announce prefix with origin AS asn, the
|
|
// peer at index i i*10 seconds after start.
|
|
func announceAll(
|
|
table *routetable.Table, prefix string, asn, start int, peers ...string,
|
|
) {
|
|
const secondsBetweenPeers = 10
|
|
|
|
for i, peer := range peers {
|
|
table.Announce(netip.MustParsePrefix(prefix), asn,
|
|
routeOf(peer, asn, start+i*secondsBetweenPeers))
|
|
}
|
|
}
|
|
|
|
func TestLookup(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
table := routetable.New(routetable.Params{})
|
|
announceAll(table, "10.0.0.0/8", asA, 10, peerA)
|
|
// Three peers with two origins: asB has more of them.
|
|
announceAll(table, "10.1.0.0/16", asA, 10, peerA)
|
|
announceAll(table, "10.1.0.0/16", asB, 20, peerB, peerC)
|
|
// Two origins with one peer each: the lower AS number wins.
|
|
announceAll(table, prefixV4, asB, 20, peerB)
|
|
announceAll(table, prefixV4, asA, 10, peerA)
|
|
// An origin whose kept routes were withdrawn: no newest time.
|
|
announceAll(table, "10.9.0.0/16", asA, 10, peerA, peerB, peerC)
|
|
table.Withdraw(netip.MustParsePrefix("10.9.0.0/16"), peerB)
|
|
table.Withdraw(netip.MustParsePrefix("10.9.0.0/16"), peerC)
|
|
announceAll(table, "2001:db8::/32", asA, 10, peerA)
|
|
announceAll(table, prefixV6, asB, 20, peerB)
|
|
announceAll(table, "3000::/16", asA, 10, peerA)
|
|
// Shorter than the lookup uses.
|
|
announceAll(table, "192.0.0.0/7", asA, 10, peerA)
|
|
announceAll(table, "2000::/15", asA, 10, peerA)
|
|
|
|
tests := []struct {
|
|
addr string
|
|
// want is the summary's prefix, or empty when none is found.
|
|
want string
|
|
originASN int
|
|
originASNs []int
|
|
peers int
|
|
newest int // in seconds after the start; 0 for none
|
|
}{
|
|
{"10.1.2.3", prefixV4, asA, []int{asA, asB}, 2, 20},
|
|
{"::ffff:10.1.2.3", prefixV4, asA, []int{asA, asB}, 2, 20},
|
|
{"10.1.3.4", "10.1.0.0/16", asB, []int{asA, asB}, 3, 30},
|
|
{"10.2.0.1", "10.0.0.0/8", asA, []int{asA}, 1, 10},
|
|
{"10.9.0.1", "10.9.0.0/16", asA, []int{asA}, 1, 0},
|
|
{"192.0.2.1", "", 0, nil, 0, 0},
|
|
{"2001:db8:1::5", prefixV6, asB, []int{asB}, 1, 20},
|
|
{"2001:db8:2::1", "2001:db8::/32", asA, []int{asA}, 1, 10},
|
|
{"3000::1", "3000::/16", asA, []int{asA}, 1, 10},
|
|
{"2001:db9::1", "", 0, nil, 0, 0},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.addr, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, found := table.Lookup(netip.MustParseAddr(tc.addr))
|
|
if tc.want == "" {
|
|
if found {
|
|
t.Errorf("Lookup(%s) = %+v, want none", tc.addr, got)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
want := routetable.PrefixSummary{
|
|
Prefix: netip.MustParsePrefix(tc.want),
|
|
OriginASN: tc.originASN,
|
|
OriginASNs: tc.originASNs,
|
|
Peers: tc.peers,
|
|
}
|
|
if tc.newest != 0 {
|
|
want.Newest = timeAt(tc.newest)
|
|
}
|
|
|
|
if !found || !reflect.DeepEqual(got, want) {
|
|
t.Errorf("Lookup(%s) = %+v, %v; want %+v", tc.addr, got, found, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOriginatedPrefixes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
table := routetable.New(routetable.Params{})
|
|
announceAll(table, "2001:db8::/32", asA, 30, peerA)
|
|
announceAll(table, prefixV4, asA, 10, peerA)
|
|
// asB's newer route for the same prefix does not count for asA.
|
|
announceAll(table, prefixV4, asB, 40, peerB)
|
|
announceAll(table, "10.1.0.0/16", asA, 10, peerA, peerB)
|
|
announceAll(table, "10.9.0.0/16", asA, 10, peerA, peerB, peerC)
|
|
table.Withdraw(netip.MustParsePrefix("10.9.0.0/16"), peerB)
|
|
table.Withdraw(netip.MustParsePrefix("10.9.0.0/16"), peerC)
|
|
|
|
want := []routetable.OriginatedPrefix{
|
|
{Prefix: netip.MustParsePrefix("10.1.0.0/16"), Newest: timeAt(20)},
|
|
{Prefix: netip.MustParsePrefix(prefixV4), Newest: timeAt(10)},
|
|
{Prefix: netip.MustParsePrefix("10.9.0.0/16")},
|
|
{Prefix: netip.MustParsePrefix("2001:db8::/32"), Newest: timeAt(30)},
|
|
}
|
|
|
|
got := table.OriginatedPrefixes(asA)
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("OriginatedPrefixes(%d) = %+v, want %+v", asA, got, want)
|
|
}
|
|
|
|
got = table.OriginatedPrefixes(asC)
|
|
if len(got) != 0 {
|
|
t.Errorf("OriginatedPrefixes(%d) = %+v, want none", asC, got)
|
|
}
|
|
}
|
|
|
|
func TestSamplePrefixes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
table := routetable.New(routetable.Params{})
|
|
|
|
announced := []string{
|
|
"10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24",
|
|
"10.0.4.0/24", "10.0.0.0/16",
|
|
"2001:db8::/48", "2001:db8:1::/48", "2001:db8:2::/48",
|
|
}
|
|
for _, prefix := range announced {
|
|
announceAll(table, prefix, asA, 10, peerA)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
ipVersion, maskLength, limit int
|
|
want int
|
|
}{
|
|
{"fewer than there are", 4, 24, 3, 3},
|
|
{"more than there are", 4, 24, 10, 5},
|
|
{"IPv6", 6, 48, 10, 3},
|
|
{"a mask length with none", 4, 20, 10, 0},
|
|
{"no such IP version", 5, 24, 10, 0},
|
|
{"a mask length too long", 4, 33, 10, 0},
|
|
{"a negative mask length", 4, -1, 10, 0},
|
|
{"a limit of zero", 4, 24, 0, 0},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sample := table.SamplePrefixes(tc.ipVersion, tc.maskLength, tc.limit)
|
|
if len(sample) != tc.want {
|
|
t.Fatalf("SamplePrefixes(%d, %d, %d) returned %d prefixes, want %d",
|
|
tc.ipVersion, tc.maskLength, tc.limit, len(sample), tc.want)
|
|
}
|
|
|
|
seen := make(map[netip.Prefix]bool)
|
|
|
|
for _, got := range sample {
|
|
want := routetable.PrefixSummary{
|
|
Prefix: got.Prefix, OriginASN: asA, OriginASNs: []int{asA},
|
|
Peers: 1, Newest: timeAt(10),
|
|
}
|
|
if seen[got.Prefix] || got.Prefix.Bits() != tc.maskLength ||
|
|
!reflect.DeepEqual(got, want) {
|
|
t.Errorf("SamplePrefixes(%d, %d, %d) returned %+v",
|
|
tc.ipVersion, tc.maskLength, tc.limit, got)
|
|
}
|
|
|
|
seen[got.Prefix] = true
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCounts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
table := routetable.New(routetable.Params{})
|
|
checkCounts(t, table, routetable.Counts{
|
|
IPv4PrefixesByLength: byLength(ipv4Bits, nil),
|
|
IPv6PrefixesByLength: byLength(ipv6Bits, nil),
|
|
})
|
|
|
|
announceAll(table, prefixV4, asA, 10, peerA, peerB)
|
|
announceAll(table, "10.2.0.0/16", asA, 15, peerA)
|
|
announceAll(table, "2001:db8::/32", asA, 5, peerA)
|
|
announceAll(table, "2001:db8::/32", asA, 25, peerC)
|
|
checkCounts(t, table, routetable.Counts{
|
|
Pairs: 3,
|
|
Peers: 3,
|
|
IPv4Prefixes: 2,
|
|
IPv6Prefixes: 1,
|
|
IPv4PrefixesByLength: byLength(ipv4Bits, map[int]int{16: 1, 24: 1}),
|
|
IPv6PrefixesByLength: byLength(ipv6Bits, map[int]int{32: 1}),
|
|
IPv4Routes: 3,
|
|
IPv6Routes: 2,
|
|
OldestKeptRoute: timeAt(5),
|
|
NewestKeptRoute: timeAt(25),
|
|
})
|
|
|
|
// peerC's route drops peerA's from the kept ones, and peerA leaves the
|
|
// IPv6 prefix.
|
|
announceAll(table, prefixV4, asA, 30, peerC)
|
|
table.Withdraw(netip.MustParsePrefix("2001:db8::/32"), peerA)
|
|
checkCounts(t, table, routetable.Counts{
|
|
Pairs: 3,
|
|
Peers: 3,
|
|
IPv4Prefixes: 2,
|
|
IPv6Prefixes: 1,
|
|
IPv4PrefixesByLength: byLength(ipv4Bits, map[int]int{16: 1, 24: 1}),
|
|
IPv6PrefixesByLength: byLength(ipv6Bits, map[int]int{32: 1}),
|
|
IPv4Routes: 4,
|
|
IPv6Routes: 1,
|
|
OldestKeptRoute: timeAt(15),
|
|
NewestKeptRoute: timeAt(30),
|
|
})
|
|
|
|
// peerC was the last peer of the IPv6 prefix. It keeps its number.
|
|
table.PeersDown([]string{peerC})
|
|
checkCounts(t, table, routetable.Counts{
|
|
Pairs: 2,
|
|
Peers: 3,
|
|
IPv4Prefixes: 2,
|
|
IPv4PrefixesByLength: byLength(ipv4Bits, map[int]int{16: 1, 24: 1}),
|
|
IPv6PrefixesByLength: byLength(ipv6Bits, nil),
|
|
IPv4Routes: 3,
|
|
OldestKeptRoute: timeAt(15),
|
|
NewestKeptRoute: timeAt(20),
|
|
})
|
|
}
|
|
|
|
// The address lengths of the two IP versions.
|
|
const (
|
|
ipv4Bits = 32
|
|
ipv6Bits = 128
|
|
)
|
|
|
|
// byLength returns the prefix counts by mask length, for addresses of bits
|
|
// bits, with counts[n] prefixes of mask length n and none of any other.
|
|
func byLength(bits int, counts map[int]int) []int {
|
|
all := make([]int, bits+1)
|
|
for n, count := range counts {
|
|
all[n] = count
|
|
}
|
|
|
|
return all
|
|
}
|
|
|
|
// checkCounts checks that the table's counts are want.
|
|
func checkCounts(t *testing.T, table *routetable.Table, want routetable.Counts) {
|
|
t.Helper()
|
|
|
|
got := table.Counts()
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("Counts() = %+v, want %+v", got, want)
|
|
}
|
|
}
|