1 Commits
Author SHA1 Message Date
sneak 385775de04 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 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 17:09:37 +00:00
6 changed files with 124 additions and 19 deletions
+7 -3
View File
@@ -88,9 +88,13 @@ one of its peers sends an update.
old.
- `GET /bogons` - The bogon announcements (HTML): the routes whose prefix or
origin AS is reserved. A prefix is reserved when it lies inside a block that
the IANA special-purpose address registries mark as not globally reachable. A
reserved AS number is one of these:
origin AS is reserved. A prefix is reserved when it lies inside one of these:
- a block that the IANA special-purpose address registries mark as not
globally reachable
- 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
- `AS23456`
- the last AS number of the 16-bit or the 32-bit range
+25 -14
View File
@@ -3,9 +3,10 @@
// internet. A route is a bogon announcement when its prefix is reserved or its
// origin AS is.
//
// The reserved prefixes come from the IANA special-purpose address registries
// for IPv4 and IPv6. The reserved AS numbers come from the IANA special-purpose
// AS numbers registry:
// Most 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
// from the IANA IPv4 and IPv6 address space registries. The reserved AS
// numbers come from the IANA special-purpose AS numbers registry:
// - the AS numbers for private use
// - the AS numbers for documentation
// - AS 23456, which stands in for a 32-bit AS number
@@ -27,10 +28,10 @@ import (
func Prefix(prefix netip.Prefix) (string, bool) {
var holder *block
for i, b := range specialPurposeBlocks {
for i, b := range registryBlocks {
inside := b.prefix.Bits() <= prefix.Bits() && b.prefix.Contains(prefix.Addr())
if inside && (holder == nil || b.prefix.Bits() > holder.prefix.Bits()) {
holder = &specialPurposeBlocks[i]
holder = &registryBlocks[i]
}
}
@@ -72,35 +73,43 @@ const (
amt = "AMT"
as112DirectDelegation = "Direct Delegation AS112 Service"
ipv4IPv6Translation = "IPv4-IPv6 Translat."
multicast = "Multicast"
)
// block is one row of an IANA special-purpose address registry.
// block is one row of an IANA address registry.
type block struct {
prefix netip.Prefix
name string
// reserved says that the row's Globally Reachable column is False.
// reserved says that no network should announce the row's addresses on
// the internet.
reserved bool
}
// reservedBlock returns a row whose Globally Reachable column is False.
// reservedBlock returns a reserved row: a multicast block, or a special-purpose
// row whose Globally Reachable column is False.
func reservedBlock(prefix, name string) block {
return block{prefix: netip.MustParsePrefix(prefix), name: name, reserved: true}
}
// reachableBlock returns a row whose Globally Reachable column is True or N/A.
// Such a row matters only inside a reserved block, whose addresses it lets be
// announced.
// reachableBlock returns a special-purpose row whose Globally Reachable column
// is True or N/A. Such a row matters only inside a reserved block, whose
// addresses it lets be announced.
func reachableBlock(prefix, name string) block {
return block{prefix: netip.MustParsePrefix(prefix), name: name}
}
// specialPurposeBlocks are the rows of the IANA special-purpose address
// registries, without the deprecated ones:
// registryBlocks are the rows of the IANA special-purpose address registries,
// without the deprecated ones:
// - https://www.iana.org/assignments/iana-ipv4-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
var specialPurposeBlocks = []block{
var registryBlocks = []block{
reservedBlock("0.0.0.0/8", "This network"),
reservedBlock("0.0.0.0/32", "This host on this network"),
reservedBlock("10.0.0.0/8", privateUse),
@@ -124,6 +133,7 @@ var specialPurposeBlocks = []block{
reservedBlock("198.18.0.0/15", benchmarking),
reservedBlock("198.51.100.0/24", "Documentation (TEST-NET-2)"),
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("255.255.255.255/32", "Limited Broadcast"),
@@ -151,6 +161,7 @@ var specialPurposeBlocks = []block{
reservedBlock("5f00::/16", "Segment Routing (SRv6) SIDs"),
reservedBlock("fc00::/7", "Unique-Local"),
reservedBlock("fe80::/10", "Link-Local Unicast"),
reservedBlock("ff00::/8", multicast),
}
// asRange is a range of reserved AS numbers, first to last.
+5
View File
@@ -21,6 +21,11 @@ func TestPrefix(t *testing.T) {
{"192.0.2.0/24", "Documentation (TEST-NET-1) (192.0.2.0/24)"},
{"2001:db8:1::/48", "Documentation (2001:db8::/32)"},
{"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.
{"0.0.0.0/32", "This host on this network (0.0.0.0/32)"},
{"2001:2::/48", "Benchmarking (2001:2::/48)"},
@@ -2,6 +2,7 @@ package routetable
import (
"net/netip"
"runtime"
"testing"
"time"
)
@@ -72,3 +73,61 @@ 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")
}
}
+27 -1
View File
@@ -173,7 +173,7 @@ func TestListPagesListTheirEntries(t *testing.T) {
},
multipleOriginsPath: {
"Prefixes announced by more than one origin AS: 1.", "1.0.0.0/24",
asLink(twoPrefixASN), asLink(secondOrigin),
asLink(twoPrefixASN), asLink(secondOrigin), ">1 peer<",
},
}
@@ -315,6 +315,32 @@ 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.
func waitUntil(t *testing.T, maxWait time.Duration, done func() bool) {
t.Helper()
+1 -1
View File
@@ -166,7 +166,7 @@
<td>
<ul class="origins">
{{ range .Origins }}
<li>{{ if .ASN }}<a href="/as/{{ .ASN }}">AS{{ .ASN }}</a>{{ else }}No known origin{{ end }} <span class="peers">{{ .Peers }} peers</span></li>
<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>
{{ end }}
</ul>
</td>