Files
routewatch/internal/bogon/bogon.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

203 lines
8.0 KiB
Go

// Package bogon tells whether a prefix or an AS number is reserved: set aside
// for a special purpose, so that no network should announce it on the
// internet. A route is a bogon announcement when its prefix is reserved or its
// origin AS is.
//
// 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
// - the last AS number of the 16-bit and of the 32-bit range
//
// Address space and AS numbers that the IANA has not handed out yet are not
// counted.
package bogon
import (
"net/netip"
"strconv"
)
// Prefix returns why prefix is reserved, and true, when it lies inside a
// reserved block. The most specific registry block that holds prefix decides,
// because some blocks that may be announced lie inside reserved ones. It
// returns "" and false when prefix is not reserved.
func Prefix(prefix netip.Prefix) (string, bool) {
var holder *block
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 = &registryBlocks[i]
}
}
if holder == nil || !holder.reserved {
return "", false
}
return holder.name + " (" + holder.prefix.String() + ")", true
}
// ASN returns why asn is reserved, and true, when it is. It returns "" and
// false when asn is not reserved.
func ASN(asn int) (string, bool) {
for _, r := range reservedASRanges {
if asn < r.first || asn > r.last {
continue
}
if r.first == r.last {
return r.name + " (AS" + strconv.Itoa(asn) + ")", true
}
return r.name + " (AS" + strconv.Itoa(r.first) + "-AS" +
strconv.Itoa(r.last) + ")", true
}
return "", false
}
// The names that the registries give to more than one row.
const (
privateUse = "Private-Use"
documentation = "Documentation"
benchmarking = "Benchmarking"
ietfProtocolAssignments = "IETF Protocol Assignments"
portControlAnycast = "Port Control Protocol Anycast"
natTraversalAnycast = "Traversal Using Relays around NAT Anycast"
nat64Discovery = "NAT64/DNS64 Discovery"
amt = "AMT"
as112DirectDelegation = "Direct Delegation AS112 Service"
ipv4IPv6Translation = "IPv4-IPv6 Translat."
multicast = "Multicast"
)
// block is one row of an IANA address registry.
type block struct {
prefix netip.Prefix
name string
// reserved says that no network should announce the row's addresses on
// the internet.
reserved bool
}
// 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 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}
}
// 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 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),
reservedBlock("100.64.0.0/10", "Shared Address Space"),
reservedBlock("127.0.0.0/8", "Loopback"),
reservedBlock("169.254.0.0/16", "Link Local"),
reservedBlock("172.16.0.0/12", privateUse),
reservedBlock("192.0.0.0/24", ietfProtocolAssignments),
reservedBlock("192.0.0.0/29", "IPv4 Service Continuity Prefix"),
reservedBlock("192.0.0.8/32", "IPv4 dummy address"),
reachableBlock("192.0.0.9/32", portControlAnycast),
reachableBlock("192.0.0.10/32", natTraversalAnycast),
reservedBlock("192.0.0.170/32", nat64Discovery),
reservedBlock("192.0.0.171/32", nat64Discovery),
reservedBlock("192.0.2.0/24", "Documentation (TEST-NET-1)"),
reachableBlock("192.31.196.0/24", "AS112-v4"),
reachableBlock("192.52.193.0/24", amt),
reservedBlock("192.88.99.2/32", "6a44-relay anycast address"),
reservedBlock("192.168.0.0/16", privateUse),
reachableBlock("192.175.48.0/24", as112DirectDelegation),
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"),
reservedBlock("::1/128", "Loopback Address"),
reservedBlock("::/128", "Unspecified Address"),
reservedBlock("::ffff:0:0/96", "IPv4-mapped Address"),
reachableBlock("64:ff9b::/96", ipv4IPv6Translation),
reservedBlock("64:ff9b:1::/48", ipv4IPv6Translation),
reservedBlock("100::/64", "Discard-Only Address Block"),
reservedBlock("100:0:0:1::/64", "Dummy IPv6 Prefix"),
reservedBlock("2001::/23", ietfProtocolAssignments),
reachableBlock("2001::/32", "TEREDO"),
reachableBlock("2001:1::1/128", portControlAnycast),
reachableBlock("2001:1::2/128", natTraversalAnycast),
reachableBlock("2001:1::3/128", "DNS-SD Service Registration Protocol Anycast"),
reservedBlock("2001:2::/48", benchmarking),
reachableBlock("2001:3::/32", amt),
reachableBlock("2001:4:112::/48", "AS112-v6"),
reachableBlock("2001:20::/28", "ORCHIDv2"),
reachableBlock("2001:30::/28", "Drone Remote ID Protocol Entity Tags (DETs) Prefix"),
reservedBlock("2001:db8::/32", documentation),
reachableBlock("2002::/16", "6to4"),
reachableBlock("2620:4f:8000::/48", as112DirectDelegation),
reservedBlock("3fff::/20", documentation),
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.
type asRange struct {
first, last int
name string
}
// The reserved AS numbers of the IANA special-purpose AS numbers registry.
const (
asTrans = 23456
firstDocumentationAS16 = 64496
lastDocumentationAS16 = 64511
firstPrivateUseAS16 = 64512
lastPrivateUseAS16 = 65534
lastAS16 = 65535
firstDocumentationAS32 = 65536
lastDocumentationAS32 = 65551
firstPrivateUseAS32 = 4200000000
lastPrivateUseAS32 = 4294967294
lastAS32 = 4294967295
)
// reservedASRanges are the reserved rows of the IANA special-purpose AS numbers
// registry, https://www.iana.org/assignments/iana-as-numbers-special-registry/.
// AS 0 is left out: RouteWatch uses it for a route with no known origin. AS 112
// is left out because the AS112 project announces routes with it.
//
//nolint:gochecknoglobals // a fixed list
var reservedASRanges = []asRange{
{first: asTrans, last: asTrans, name: "AS_TRANS"},
{first: firstDocumentationAS16, last: lastDocumentationAS16, name: documentation},
{first: firstPrivateUseAS16, last: lastPrivateUseAS16, name: privateUse},
{first: lastAS16, last: lastAS16, name: "Last 16-bit AS number"},
{first: firstDocumentationAS32, last: lastDocumentationAS32, name: documentation},
{first: firstPrivateUseAS32, last: lastPrivateUseAS32, name: privateUse},
{first: lastAS32, last: lastAS32, name: "Last 32-bit AS number"},
}