check / check (push) Canceled after 0s
The home page gets cards for the newest and oldest kept route, the newest and oldest updated AS, the AS with the most prefixes, the ASes with a single prefix and fifteen well-known networks. Two more cards count the bogon announcements and the prefixes with several origin ASes. They link to the new pages /bogons and /multiple-origins, which list up to 1,000 entries each. The new package internal/bogon tells reserved prefixes and AS numbers, from the IANA special-purpose and address space registries. Table.Overview walks the table and lets the read lock go every 10,000 prefixes. The server works the cards out once a minute and serves that copy. A new index on asns(last_seen) finds the newest and oldest updated AS. Model: opus-5-5
232 lines
7.7 KiB
Go
232 lines
7.7 KiB
Go
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])
|
|
}
|
|
}
|