package routetable_test import ( "net/netip" "reflect" "testing" "git.eeqj.de/sneak/routewatch/internal/routetable" ) // Origin ASes that are not reserved, unlike asA, asB and asC, which are // documentation AS numbers and so make bogon announcements. const ( asOne = 1000 asTwo = 2000 asThree = 3000 asFour = 4000 asFive = 5000 // noKnownOrigin is the origin AS of a route whose path ends in an AS // set. noKnownOrigin = 0 // privateUseAS and asTrans are reserved AS numbers. privateUseAS = 64512 asTrans = 23456 // maxListed is how many bogon announcements and prefixes with more than // one origin AS the tests ask Overview for. maxListed = 10 ) // prefixRoute returns the PrefixRoute of prefix with origin AS asn, second // seconds after the start. func prefixRoute(prefix string, asn, second int) *routetable.PrefixRoute { return &routetable.PrefixRoute{ Prefix: netip.MustParsePrefix(prefix), OriginASN: asn, Time: at(second), } } func TestOverviewOfAnEmptyTable(t *testing.T) { t.Parallel() got := routetable.New(routetable.Params{}).Overview(maxListed) if !reflect.DeepEqual(got, routetable.Overview{}) { t.Errorf("Overview() = %+v, want no figures", got) } } // TestOverviewNewestAndOldestRoutes checks that the newest and the oldest kept // routes are found among every origin's kept routes, and that withdrawn routes // no longer count. func TestOverviewNewestAndOldestRoutes(t *testing.T) { t.Parallel() table := routetable.New(routetable.Params{}) announceAll(table, "1.0.0.0/24", asOne, 20, peerA, peerB) // The oldest is the older of this origin's two kept routes. announceAll(table, "2.0.0.0/24", asTwo, 10, peerA, peerB) announceAll(table, "2001:4860::/32", asThree, 40, peerC) // Withdrawn routes, newer and older than every other. announceAll(table, "3.0.0.0/24", asOne, 50, peerA) announceAll(table, "4.0.0.0/24", asOne, 5, peerB) table.Withdraw(netip.MustParsePrefix("3.0.0.0/24"), peerA) table.Withdraw(netip.MustParsePrefix("4.0.0.0/24"), peerB) got := table.Overview(maxListed) want := prefixRoute("2001:4860::/32", asThree, 40) if !reflect.DeepEqual(got.NewestRoute, want) { t.Errorf("NewestRoute = %+v, want %+v", got.NewestRoute, want) } want = prefixRoute("2.0.0.0/24", asTwo, 10) if !reflect.DeepEqual(got.OldestRoute, want) { t.Errorf("OldestRoute = %+v, want %+v", got.OldestRoute, want) } } // TestOverviewPrefixesPerOrigin checks the AS that originates the most // prefixes and the ASes that originate one. func TestOverviewPrefixesPerOrigin(t *testing.T) { t.Parallel() table := routetable.New(routetable.Params{}) // asOne and asTwo originate two prefixes each; the lower AS number counts. announceAll(table, "1.0.0.0/24", asTwo, 10, peerA) announceAll(table, "1.0.1.0/24", asTwo, 10, peerA) announceAll(table, "2.0.0.0/24", asOne, 10, peerA) announceAll(table, "2.0.1.0/24", asOne, 10, peerB) // Three ASes originate one prefix. asThree's route is the newest, and // asFive's origin keeps no route: peers B and C withdrew theirs. announceAll(table, "3.0.0.0/24", asThree, 30, peerA) announceAll(table, "4.0.0.0/24", asFour, 20, peerA) announceAll(table, "5.0.0.0/24", asFive, 40, peerA, peerB, peerC) table.Withdraw(netip.MustParsePrefix("5.0.0.0/24"), peerB) table.Withdraw(netip.MustParsePrefix("5.0.0.0/24"), peerC) got := table.Overview(maxListed) if got.MostPrefixesASN != asOne || got.MostPrefixes != 2 { t.Errorf("most prefixes: AS%d with %d, want AS%d with 2", got.MostPrefixesASN, got.MostPrefixes, asOne) } if got.SinglePrefixASes != 3 { t.Errorf("SinglePrefixASes = %d, want 3", got.SinglePrefixASes) } want := prefixRoute("3.0.0.0/24", asThree, 30) if !reflect.DeepEqual(got.NewestSinglePrefix, want) { t.Errorf("NewestSinglePrefix = %+v, want %+v", got.NewestSinglePrefix, want) } } // TestOverviewLeavesOutNoKnownOrigin checks that AS 0, which stands for no // known origin, counts in neither the most nor the single-prefix figures. It // originates one prefix, with a route newer than asOne's, and asOne would lose // the tie for the most prefixes to it. func TestOverviewLeavesOutNoKnownOrigin(t *testing.T) { t.Parallel() table := routetable.New(routetable.Params{}) announceAll(table, "1.0.0.0/24", asOne, 10, peerA) announceAll(table, "2.0.0.0/24", noKnownOrigin, 20, peerA) got := table.Overview(maxListed) if got.MostPrefixesASN != asOne || got.SinglePrefixASes != 1 { t.Errorf("most prefixes AS%d, %d single-prefix ASes; want AS%d, 1", got.MostPrefixesASN, got.SinglePrefixASes, asOne) } want := prefixRoute("1.0.0.0/24", asOne, 10) if !reflect.DeepEqual(got.NewestSinglePrefix, want) { t.Errorf("NewestSinglePrefix = %+v, want %+v", got.NewestSinglePrefix, want) } } // TestOverviewBogons checks which routes are bogon announcements and why, in // prefix order, and that a smaller maxListed lists fewer but counts them all. func TestOverviewBogons(t *testing.T) { t.Parallel() table := routetable.New(routetable.Params{}) // A reserved prefix, carried by two peers. announceAll(table, "192.168.0.0/16", asOne, 10, peerA, peerB) // A reserved origin AS. announceAll(table, "1.0.0.0/24", privateUseAS, 10, peerA) // Both. announceAll(table, "10.0.0.0/8", asTrans, 10, peerA) // Neither: a normal route, and a block that may be announced although it // lies inside a reserved one. announceAll(table, "1.0.1.0/24", asOne, 10, peerA) announceAll(table, "2001:4:112::/48", asTwo, 10, peerA) want := []routetable.Bogon{ { Prefix: netip.MustParsePrefix("1.0.0.0/24"), OriginASN: privateUseAS, Peers: 1, Reasons: []string{"Private-Use (AS64512-AS65534)"}, }, { Prefix: netip.MustParsePrefix("10.0.0.0/8"), OriginASN: asTrans, Peers: 1, Reasons: []string{ "Private-Use (10.0.0.0/8)", "AS_TRANS (AS23456)", }, }, { Prefix: netip.MustParsePrefix("192.168.0.0/16"), OriginASN: asOne, Peers: 2, Reasons: []string{"Private-Use (192.168.0.0/16)"}, }, } got := table.Overview(maxListed) if !reflect.DeepEqual(got.Bogons, want) || got.BogonCount != len(want) { t.Errorf("Overview(%d): %d bogons %+v, want %d %+v", maxListed, got.BogonCount, got.Bogons, len(want), want) } const fewer = 2 got = table.Overview(fewer) if !reflect.DeepEqual(got.Bogons, want[:fewer]) || got.BogonCount != len(want) { t.Errorf("Overview(%d): %d bogons %+v, want %d %+v", fewer, got.BogonCount, got.Bogons, len(want), want[:fewer]) } } // TestOverviewMultipleOrigins checks the prefixes with more than one origin // AS, in prefix order, and that a smaller maxListed lists fewer but counts them // all. func TestOverviewMultipleOrigins(t *testing.T) { t.Parallel() table := routetable.New(routetable.Params{}) announceAll(table, "2001:4860::/32", asThree, 10, peerA) announceAll(table, "2001:4860::/32", asTwo, 10, peerB) announceAll(table, "1.0.0.0/24", asOne, 10, peerA, peerB) announceAll(table, "1.0.0.0/24", asTwo, 30, peerC) // One origin only. announceAll(table, "2.0.0.0/24", asOne, 10, peerA, peerB) want := []routetable.MultipleOriginPrefix{ { Prefix: netip.MustParsePrefix("1.0.0.0/24"), Origins: []routetable.OriginPeers{ {ASN: asOne, Peers: 2}, {ASN: asTwo, Peers: 1}, }, }, { Prefix: netip.MustParsePrefix("2001:4860::/32"), Origins: []routetable.OriginPeers{ {ASN: asTwo, Peers: 1}, {ASN: asThree, Peers: 1}, }, }, } got := table.Overview(maxListed) if !reflect.DeepEqual(got.MultipleOrigins, want) || got.MultipleOriginCount != len(want) { t.Errorf("Overview(%d): %d prefixes %+v, want %d %+v", maxListed, got.MultipleOriginCount, got.MultipleOrigins, len(want), want) } got = table.Overview(1) if !reflect.DeepEqual(got.MultipleOrigins, want[:1]) || got.MultipleOriginCount != len(want) { t.Errorf("Overview(1): %d prefixes %+v, want %d %+v", got.MultipleOriginCount, got.MultipleOrigins, len(want), want[:1]) } }