Show figures of the routing table on the home page (closes #154)
check / check (push) Canceled after 0s
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
This commit was merged in pull request #196.
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/bogon"
|
||||
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
||||
)
|
||||
|
||||
// walkBatch is how many prefixes the walk over the whole table reads in one
|
||||
// hold of the read lock. The updates waiting for the lock get it in between.
|
||||
const walkBatch = 10000
|
||||
|
||||
// Overview holds figures about the whole table. Working them out takes a walk
|
||||
// over every prefix, so a caller asks for them now and then and keeps them.
|
||||
type Overview struct {
|
||||
// NewestRoute and OldestRoute are the newest and the oldest kept routes,
|
||||
// or nil when the table keeps none. Of several with the same time, it is
|
||||
// any one of them.
|
||||
NewestRoute *PrefixRoute
|
||||
OldestRoute *PrefixRoute
|
||||
|
||||
// MostPrefixesASN is the AS that originates the most prefixes, and
|
||||
// MostPrefixes is how many it originates. Of several with as many, it is
|
||||
// the lowest AS number. AS 0, no known origin, counts neither here nor
|
||||
// in the single-prefix figures.
|
||||
MostPrefixesASN int
|
||||
MostPrefixes int
|
||||
|
||||
// SinglePrefixASes counts the ASes that originate one prefix.
|
||||
SinglePrefixASes int
|
||||
// NewestSinglePrefix is the newest kept route of those ASes, or nil when
|
||||
// they keep none.
|
||||
NewestSinglePrefix *PrefixRoute
|
||||
|
||||
// Bogons are the bogon announcements, in prefix order: at most the
|
||||
// number Overview is asked for. BogonCount counts them all.
|
||||
Bogons []Bogon
|
||||
BogonCount int
|
||||
|
||||
// MultipleOrigins are the prefixes that peers carry with more than one
|
||||
// origin AS, in prefix order: at most the number Overview is asked for.
|
||||
// MultipleOriginCount counts them all.
|
||||
MultipleOrigins []MultipleOriginPrefix
|
||||
MultipleOriginCount int
|
||||
}
|
||||
|
||||
// PrefixRoute is the time of a kept route, with its prefix and origin AS.
|
||||
type PrefixRoute struct {
|
||||
Prefix netip.Prefix
|
||||
OriginASN int
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// Bogon is a bogon announcement: a prefix and origin AS pair whose prefix or
|
||||
// origin AS is reserved, as package bogon tells.
|
||||
type Bogon struct {
|
||||
Prefix netip.Prefix
|
||||
OriginASN int
|
||||
// Peers is how many peers carry the prefix with this origin AS.
|
||||
Peers int
|
||||
// Reasons say why the prefix, the origin AS or both are reserved.
|
||||
Reasons []string
|
||||
}
|
||||
|
||||
// MultipleOriginPrefix is a prefix that peers carry with more than one origin
|
||||
// AS.
|
||||
type MultipleOriginPrefix struct {
|
||||
Prefix netip.Prefix
|
||||
// Origins are its origin ASes, in AS number order.
|
||||
Origins []OriginPeers
|
||||
}
|
||||
|
||||
// OriginPeers is one origin AS of a prefix, and how many peers carry the prefix
|
||||
// with it.
|
||||
type OriginPeers struct {
|
||||
ASN int
|
||||
Peers int
|
||||
}
|
||||
|
||||
// Overview works out the table's overview. It lists at most maxListed bogon
|
||||
// announcements and at most maxListed prefixes with more than one origin AS:
|
||||
// the first in prefix order. It walks over every prefix, but holds the read
|
||||
// lock for walkBatch prefixes at a time.
|
||||
func (t *Table) Overview(maxListed int) Overview {
|
||||
var overview Overview
|
||||
|
||||
t.walkPrefixes(func(prefix netip.Prefix, origins []*origin) {
|
||||
overview.noteKeptRoutes(prefix, origins)
|
||||
overview.noteBogons(prefix, origins)
|
||||
overview.noteMultipleOrigins(prefix, origins)
|
||||
})
|
||||
|
||||
t.countPrefixesPerOrigin(&overview)
|
||||
|
||||
slices.SortFunc(overview.Bogons, func(a, b Bogon) int {
|
||||
return cmp.Or(a.Prefix.Compare(b.Prefix),
|
||||
cmp.Compare(a.OriginASN, b.OriginASN))
|
||||
})
|
||||
overview.BogonCount = len(overview.Bogons)
|
||||
overview.Bogons = overview.Bogons[:min(maxListed, overview.BogonCount)]
|
||||
|
||||
slices.SortFunc(overview.MultipleOrigins, func(a, b MultipleOriginPrefix) int {
|
||||
return a.Prefix.Compare(b.Prefix)
|
||||
})
|
||||
overview.MultipleOriginCount = len(overview.MultipleOrigins)
|
||||
overview.MultipleOrigins =
|
||||
overview.MultipleOrigins[:min(maxListed, overview.MultipleOriginCount)]
|
||||
|
||||
return overview
|
||||
}
|
||||
|
||||
// walkPrefixes calls visit with every prefix of the table and its origins. It
|
||||
// holds the read lock, but lets it go after every walkBatch prefixes, so the
|
||||
// updates waiting for it get in. A prefix that is added or removed in such a
|
||||
// gap may or may not be visited, as Go allows for a map that changes during a
|
||||
// range over it. visit must not keep origins once it returns.
|
||||
func (t *Table) walkPrefixes(visit func(prefix netip.Prefix, origins []*origin)) {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
visited := 0
|
||||
|
||||
for _, f := range []*family{&t.ipv4, &t.ipv6} {
|
||||
for _, prefixes := range f.byLength {
|
||||
for prefix, origins := range prefixes {
|
||||
visit(prefix, origins)
|
||||
|
||||
visited++
|
||||
if visited%walkBatch == 0 {
|
||||
t.mu.RUnlock()
|
||||
t.mu.RLock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// countPrefixesPerOrigin works out the overview's figures of how many prefixes
|
||||
// each AS originates. It holds the read lock once, for one look at each origin
|
||||
// AS, of which there are far fewer than prefixes.
|
||||
func (t *Table) countPrefixesPerOrigin(overview *Overview) {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
for asn, prefixes := range t.byOrigin {
|
||||
if asn == ristypes.NoOrigin {
|
||||
continue
|
||||
}
|
||||
|
||||
count := len(prefixes)
|
||||
if count > overview.MostPrefixes ||
|
||||
(count == overview.MostPrefixes && asn < overview.MostPrefixesASN) {
|
||||
overview.MostPrefixesASN = asn
|
||||
overview.MostPrefixes = count
|
||||
}
|
||||
|
||||
if count == 1 {
|
||||
overview.SinglePrefixASes++
|
||||
overview.noteSinglePrefix(asn, prefixes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// noteSinglePrefix makes the newest kept route of asn, whose one prefix is in
|
||||
// prefixes, the overview's newest single-prefix route when it is newer.
|
||||
func (overview *Overview) noteSinglePrefix(
|
||||
asn int, prefixes map[netip.Prefix]*origin,
|
||||
) {
|
||||
for prefix, o := range prefixes {
|
||||
newest := o.newestTime()
|
||||
if newest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if overview.NewestSinglePrefix == nil ||
|
||||
newest.After(overview.NewestSinglePrefix.Time) {
|
||||
overview.NewestSinglePrefix = &PrefixRoute{
|
||||
Prefix: prefix, OriginASN: asn, Time: *newest,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// noteKeptRoutes makes the newest and the oldest kept routes of prefix's
|
||||
// origins the overview's newest and oldest routes when they are newer or older.
|
||||
func (overview *Overview) noteKeptRoutes(prefix netip.Prefix, origins []*origin) {
|
||||
for _, o := range origins {
|
||||
if len(o.routes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
newest := o.routes[0].time
|
||||
if overview.NewestRoute == nil || newest.After(overview.NewestRoute.Time) {
|
||||
overview.NewestRoute = &PrefixRoute{
|
||||
Prefix: prefix, OriginASN: o.asn, Time: newest,
|
||||
}
|
||||
}
|
||||
|
||||
oldest := o.routes[len(o.routes)-1].time
|
||||
if overview.OldestRoute == nil || oldest.Before(overview.OldestRoute.Time) {
|
||||
overview.OldestRoute = &PrefixRoute{
|
||||
Prefix: prefix, OriginASN: o.asn, Time: oldest,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// noteBogons adds each origin of prefix to the overview's bogon announcements
|
||||
// when the prefix or the origin AS is reserved.
|
||||
func (overview *Overview) noteBogons(prefix netip.Prefix, origins []*origin) {
|
||||
prefixReason, prefixReserved := bogon.Prefix(prefix)
|
||||
|
||||
for _, o := range origins {
|
||||
asnReason, asnReserved := bogon.ASN(o.asn)
|
||||
if !prefixReserved && !asnReserved {
|
||||
continue
|
||||
}
|
||||
|
||||
announcement := Bogon{
|
||||
Prefix: prefix, OriginASN: o.asn, Peers: int(o.peers.Count()),
|
||||
}
|
||||
|
||||
if prefixReserved {
|
||||
announcement.Reasons = append(announcement.Reasons, prefixReason)
|
||||
}
|
||||
|
||||
if asnReserved {
|
||||
announcement.Reasons = append(announcement.Reasons, asnReason)
|
||||
}
|
||||
|
||||
overview.Bogons = append(overview.Bogons, announcement)
|
||||
}
|
||||
}
|
||||
|
||||
// noteMultipleOrigins adds prefix to the overview's prefixes with more than one
|
||||
// origin AS when it has more than one.
|
||||
func (overview *Overview) noteMultipleOrigins(
|
||||
prefix netip.Prefix, origins []*origin,
|
||||
) {
|
||||
if len(origins) <= 1 {
|
||||
return
|
||||
}
|
||||
|
||||
entry := MultipleOriginPrefix{
|
||||
Prefix: prefix,
|
||||
Origins: make([]OriginPeers, 0, len(origins)),
|
||||
}
|
||||
|
||||
for _, o := range origins {
|
||||
entry.Origins = append(entry.Origins,
|
||||
OriginPeers{ASN: o.asn, Peers: int(o.peers.Count())})
|
||||
}
|
||||
|
||||
overview.MultipleOrigins = append(overview.MultipleOrigins, entry)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestWalkPrefixesVisitsEveryPrefix walks a table with more prefixes than two
|
||||
// of the walk's batches, while another goroutine keeps adding and removing
|
||||
// other prefixes of the same mask length. The walk lets the lock go between
|
||||
// batches, so those updates get in while it runs. Every prefix that was in
|
||||
// the table all along must be visited once.
|
||||
func TestWalkPrefixesVisitsEveryPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
asn = 1000
|
||||
peer = "192.0.2.1"
|
||||
// prefixCount is more prefixes than two of the walk's batches.
|
||||
prefixCount = 2*walkBatch + 1
|
||||
)
|
||||
|
||||
routeTime := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
table := New(Params{})
|
||||
prefixes := make([]netip.Prefix, 0, prefixCount)
|
||||
addr := netip.MustParseAddr("1.0.0.0")
|
||||
|
||||
for range prefixCount {
|
||||
prefix := netip.PrefixFrom(addr, addr.BitLen())
|
||||
table.Announce(prefix, asn, Route{Peer: peer, Time: routeTime})
|
||||
prefixes = append(prefixes, prefix)
|
||||
addr = addr.Next()
|
||||
}
|
||||
|
||||
stop := make(chan struct{})
|
||||
updated := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(updated)
|
||||
|
||||
other := netip.MustParseAddr("2.0.0.0")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
prefix := netip.PrefixFrom(other, other.BitLen())
|
||||
table.Announce(prefix, asn, Route{Peer: peer, Time: routeTime})
|
||||
table.Withdraw(prefix, peer)
|
||||
|
||||
other = other.Next()
|
||||
}
|
||||
}()
|
||||
|
||||
visits := make(map[netip.Prefix]int)
|
||||
|
||||
table.walkPrefixes(func(prefix netip.Prefix, _ []*origin) {
|
||||
visits[prefix]++
|
||||
})
|
||||
|
||||
close(stop)
|
||||
<-updated
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
if visits[prefix] != 1 {
|
||||
t.Fatalf("the walk visited %s %d times, want once", prefix,
|
||||
visits[prefix])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestWalkPrefixesLetsUpdatesIn starts an update during the walk's first
|
||||
// batch. The update must have been made by the time the walk visits the first
|
||||
// prefix of its second batch, because the walk lets the lock go in between.
|
||||
func TestWalkPrefixesLetsUpdatesIn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
asn = 1000
|
||||
peer = "192.0.2.1"
|
||||
)
|
||||
|
||||
routeTime := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
table := New(Params{})
|
||||
addr := netip.MustParseAddr("1.0.0.0")
|
||||
|
||||
for range walkBatch + 1 {
|
||||
prefix := netip.PrefixFrom(addr, addr.BitLen())
|
||||
table.Announce(prefix, asn, Route{Peer: peer, Time: routeTime})
|
||||
|
||||
addr = addr.Next()
|
||||
}
|
||||
|
||||
updatePrefix := netip.MustParsePrefix("2.0.0.0/8")
|
||||
updateDone := make(chan struct{})
|
||||
visited := 0
|
||||
madeBeforeSecondBatch := false
|
||||
|
||||
table.walkPrefixes(func(netip.Prefix, []*origin) {
|
||||
visited++
|
||||
|
||||
switch visited {
|
||||
case 1:
|
||||
go func() {
|
||||
defer close(updateDone)
|
||||
|
||||
table.Announce(updatePrefix, asn, Route{Peer: peer, Time: routeTime})
|
||||
}()
|
||||
|
||||
// Wait until the update waits for the lock. TryRLock fails
|
||||
// once a writer waits.
|
||||
for table.mu.TryRLock() {
|
||||
table.mu.RUnlock()
|
||||
runtime.Gosched()
|
||||
}
|
||||
case walkBatch + 1:
|
||||
// The walk holds the read lock, so the table can be read.
|
||||
madeBeforeSecondBatch = table.origins(updatePrefix) != nil
|
||||
}
|
||||
})
|
||||
|
||||
<-updateDone
|
||||
|
||||
if !madeBeforeSecondBatch {
|
||||
t.Fatal("an update started during the walk's first batch was not made " +
|
||||
"before its second batch")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
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])
|
||||
}
|
||||
}
|
||||
@@ -625,6 +625,7 @@ func TestConcurrentUse(t *testing.T) {
|
||||
func(int) { table.OriginatedPrefixes(asA) },
|
||||
func(int) { table.SamplePrefixes(ipVersion, prefix.Bits(), sampleSize) },
|
||||
func(int) { table.Counts() },
|
||||
func(int) { table.Overview(sampleSize) },
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
Reference in New Issue
Block a user