Compare commits

..
1 Commits
Author SHA1 Message Date
sneak c90af9a873 Show figures of the routing table on the home page (closes #154)
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 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 16:48:32 +00:00
6 changed files with 19 additions and 124 deletions

No files matched your search

+3 -7
View File
@@ -88,13 +88,9 @@ one of its peers sends an update.
old. old.
- `GET /bogons` - The bogon announcements (HTML): the routes whose prefix or - `GET /bogons` - The bogon announcements (HTML): the routes whose prefix or
origin AS is reserved. A prefix is reserved when it lies inside one of these: origin AS is reserved. A prefix is reserved when it lies inside a block that
- a block that the IANA special-purpose address registries mark as not the IANA special-purpose address registries mark as not globally reachable. A
globally reachable reserved AS number is one of these:
- the multicast space, `224.0.0.0/4` and `ff00::/8`, which the IANA IPv4 and
IPv6 address space registries list as Multicast
A reserved AS number is one of these:
- an AS number for private use or for documentation - an AS number for private use or for documentation
- `AS23456` - `AS23456`
- the last AS number of the 16-bit or the 32-bit range - the last AS number of the 16-bit or the 32-bit range
+14 -25
View File
@@ -3,10 +3,9 @@
// internet. A route is a bogon announcement when its prefix is reserved or its // internet. A route is a bogon announcement when its prefix is reserved or its
// origin AS is. // origin AS is.
// //
// Most reserved prefixes come from the IANA special-purpose address registries // The reserved prefixes come from the IANA special-purpose address registries
// for IPv4 and IPv6. The two multicast blocks, 224.0.0.0/4 and ff00::/8, come // for IPv4 and IPv6. The reserved AS numbers come from the IANA special-purpose
// from the IANA IPv4 and IPv6 address space registries. The reserved AS // AS numbers registry:
// numbers come from the IANA special-purpose AS numbers registry:
// - the AS numbers for private use // - the AS numbers for private use
// - the AS numbers for documentation // - the AS numbers for documentation
// - AS 23456, which stands in for a 32-bit AS number // - AS 23456, which stands in for a 32-bit AS number
@@ -28,10 +27,10 @@ import (
func Prefix(prefix netip.Prefix) (string, bool) { func Prefix(prefix netip.Prefix) (string, bool) {
var holder *block var holder *block
for i, b := range registryBlocks { for i, b := range specialPurposeBlocks {
inside := b.prefix.Bits() <= prefix.Bits() && b.prefix.Contains(prefix.Addr()) inside := b.prefix.Bits() <= prefix.Bits() && b.prefix.Contains(prefix.Addr())
if inside && (holder == nil || b.prefix.Bits() > holder.prefix.Bits()) { if inside && (holder == nil || b.prefix.Bits() > holder.prefix.Bits()) {
holder = &registryBlocks[i] holder = &specialPurposeBlocks[i]
} }
} }
@@ -73,43 +72,35 @@ const (
amt = "AMT" amt = "AMT"
as112DirectDelegation = "Direct Delegation AS112 Service" as112DirectDelegation = "Direct Delegation AS112 Service"
ipv4IPv6Translation = "IPv4-IPv6 Translat." ipv4IPv6Translation = "IPv4-IPv6 Translat."
multicast = "Multicast"
) )
// block is one row of an IANA address registry. // block is one row of an IANA special-purpose address registry.
type block struct { type block struct {
prefix netip.Prefix prefix netip.Prefix
name string name string
// reserved says that no network should announce the row's addresses on // reserved says that the row's Globally Reachable column is False.
// the internet.
reserved bool reserved bool
} }
// reservedBlock returns a reserved row: a multicast block, or a special-purpose // reservedBlock returns a row whose Globally Reachable column is False.
// row whose Globally Reachable column is False.
func reservedBlock(prefix, name string) block { func reservedBlock(prefix, name string) block {
return block{prefix: netip.MustParsePrefix(prefix), name: name, reserved: true} return block{prefix: netip.MustParsePrefix(prefix), name: name, reserved: true}
} }
// reachableBlock returns a special-purpose row whose Globally Reachable column // reachableBlock returns a row whose Globally Reachable column is True or N/A.
// is True or N/A. Such a row matters only inside a reserved block, whose // Such a row matters only inside a reserved block, whose addresses it lets be
// addresses it lets be announced. // announced.
func reachableBlock(prefix, name string) block { func reachableBlock(prefix, name string) block {
return block{prefix: netip.MustParsePrefix(prefix), name: name} return block{prefix: netip.MustParsePrefix(prefix), name: name}
} }
// registryBlocks are the rows of the IANA special-purpose address registries, // specialPurposeBlocks are the rows of the IANA special-purpose address
// without the deprecated ones: // registries, without the deprecated ones:
// - https://www.iana.org/assignments/iana-ipv4-special-registry/ // - https://www.iana.org/assignments/iana-ipv4-special-registry/
// - https://www.iana.org/assignments/iana-ipv6-special-registry/ // - https://www.iana.org/assignments/iana-ipv6-special-registry/
// //
// They also hold the multicast blocks, which the IANA address space registries
// list as Multicast:
// - https://www.iana.org/assignments/ipv4-address-space/
// - https://www.iana.org/assignments/ipv6-address-space/
//
//nolint:gochecknoglobals // a fixed list, parsed once //nolint:gochecknoglobals // a fixed list, parsed once
var registryBlocks = []block{ var specialPurposeBlocks = []block{
reservedBlock("0.0.0.0/8", "This network"), reservedBlock("0.0.0.0/8", "This network"),
reservedBlock("0.0.0.0/32", "This host on this network"), reservedBlock("0.0.0.0/32", "This host on this network"),
reservedBlock("10.0.0.0/8", privateUse), reservedBlock("10.0.0.0/8", privateUse),
@@ -133,7 +124,6 @@ var registryBlocks = []block{
reservedBlock("198.18.0.0/15", benchmarking), reservedBlock("198.18.0.0/15", benchmarking),
reservedBlock("198.51.100.0/24", "Documentation (TEST-NET-2)"), reservedBlock("198.51.100.0/24", "Documentation (TEST-NET-2)"),
reservedBlock("203.0.113.0/24", "Documentation (TEST-NET-3)"), reservedBlock("203.0.113.0/24", "Documentation (TEST-NET-3)"),
reservedBlock("224.0.0.0/4", multicast),
reservedBlock("240.0.0.0/4", "Reserved"), reservedBlock("240.0.0.0/4", "Reserved"),
reservedBlock("255.255.255.255/32", "Limited Broadcast"), reservedBlock("255.255.255.255/32", "Limited Broadcast"),
@@ -161,7 +151,6 @@ var registryBlocks = []block{
reservedBlock("5f00::/16", "Segment Routing (SRv6) SIDs"), reservedBlock("5f00::/16", "Segment Routing (SRv6) SIDs"),
reservedBlock("fc00::/7", "Unique-Local"), reservedBlock("fc00::/7", "Unique-Local"),
reservedBlock("fe80::/10", "Link-Local Unicast"), reservedBlock("fe80::/10", "Link-Local Unicast"),
reservedBlock("ff00::/8", multicast),
} }
// asRange is a range of reserved AS numbers, first to last. // asRange is a range of reserved AS numbers, first to last.
-5
View File
@@ -21,11 +21,6 @@ func TestPrefix(t *testing.T) {
{"192.0.2.0/24", "Documentation (TEST-NET-1) (192.0.2.0/24)"}, {"192.0.2.0/24", "Documentation (TEST-NET-1) (192.0.2.0/24)"},
{"2001:db8:1::/48", "Documentation (2001:db8::/32)"}, {"2001:db8:1::/48", "Documentation (2001:db8::/32)"},
{"fd00::/8", "Unique-Local (fc00::/7)"}, {"fd00::/8", "Unique-Local (fc00::/7)"},
// Multicast space, from the IANA address space registries.
{"224.0.0.0/4", "Multicast (224.0.0.0/4)"},
{"239.255.0.0/16", "Multicast (224.0.0.0/4)"},
{"ff00::/8", "Multicast (ff00::/8)"},
{"ff02::/16", "Multicast (ff00::/8)"},
// The most specific block names the reason. // The most specific block names the reason.
{"0.0.0.0/32", "This host on this network (0.0.0.0/32)"}, {"0.0.0.0/32", "This host on this network (0.0.0.0/32)"},
{"2001:2::/48", "Benchmarking (2001:2::/48)"}, {"2001:2::/48", "Benchmarking (2001:2::/48)"},
@@ -2,7 +2,6 @@ package routetable
import ( import (
"net/netip" "net/netip"
"runtime"
"testing" "testing"
"time" "time"
) )
@@ -73,61 +72,3 @@ func TestWalkPrefixesVisitsEveryPrefix(t *testing.T) {
} }
} }
} }
// 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")
}
}
+1 -27
View File
@@ -173,7 +173,7 @@ func TestListPagesListTheirEntries(t *testing.T) {
}, },
multipleOriginsPath: { multipleOriginsPath: {
"Prefixes announced by more than one origin AS: 1.", "1.0.0.0/24", "Prefixes announced by more than one origin AS: 1.", "1.0.0.0/24",
asLink(twoPrefixASN), asLink(secondOrigin), ">1 peer<", asLink(twoPrefixASN), asLink(secondOrigin),
}, },
} }
@@ -315,32 +315,6 @@ func TestKeepHomeCardsFreshWorksThemOutEveryInterval(t *testing.T) {
} }
} }
// TestStartedServerWorksOutTheCards starts a server and waits until its home
// page shows the cards.
func TestStartedServerWorksOutTheCards(t *testing.T) {
t.Parallel()
const maxWait = 5 * time.Second
s := newServerWithTable(t, newCardsTable())
err := s.Start(t.Context())
if err != nil {
t.Fatalf("Start: %v", err)
}
defer func() { _ = s.Stop(context.Background()) }()
waitUntil(t, maxWait, func() bool {
return !strings.Contains(get(t, s, "/").Body.String(), cardsNotReadyText)
})
card := homeCard(t, get(t, s, "/").Body.String(), "Newest Updated Route")
if !strings.Contains(card, "8.8.8.0/24") {
t.Errorf("the newest route is not the table's: %s", card)
}
}
// waitUntil fails the test unless done reports true within maxWait. // waitUntil fails the test unless done reports true within maxWait.
func waitUntil(t *testing.T, maxWait time.Duration, done func() bool) { func waitUntil(t *testing.T, maxWait time.Duration, done func() bool) {
t.Helper() t.Helper()
+1 -1
View File
@@ -166,7 +166,7 @@
<td> <td>
<ul class="origins"> <ul class="origins">
{{ range .Origins }} {{ range .Origins }}
<li>{{ if .ASN }}<a href="/as/{{ .ASN }}">AS{{ .ASN }}</a>{{ else }}No known origin{{ end }} <span class="peers">{{ .Peers }} {{ if eq .Peers 1 }}peer{{ else }}peers{{ end }}</span></li> <li>{{ if .ASN }}<a href="/as/{{ .ASN }}">AS{{ .ASN }}</a>{{ else }}No known origin{{ end }} <span class="peers">{{ .Peers }} peers</span></li>
{{ end }} {{ end }}
</ul> </ul>
</td> </td>