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
260 lines
7.6 KiB
Go
260 lines
7.6 KiB
Go
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)
|
|
}
|