Files
routewatch/internal/routetable/overview_test.go
T
clawbot 281d162e27
check / check (push) Canceled after 0s
Show figures of the routing table on the home page (closes #154)
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
2026-10-27 18:27:01 +01:00

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])
}
}