check / check (push) Canceled after 0s
The home page's cards are worked out once a minute by Overview, which walks every prefix of the routing table under the table's read lock. After each batch of 10,000 prefixes, the walk let the lock go and took it back at once. The prefix handler, waiting for the write lock, then got in only once per batch. During a walk, its queue could fill, and the streamer then dropped updates for it. The walk now lets the lock go for as long as it held the lock for the batch, as PeerDown does. A test applies another peer's 1,000 announcements while Overview walks a table with more prefixes than five of the walk's batches. Model: opus-5-5
265 lines
7.8 KiB
Go
265 lines
7.8 KiB
Go
package routetable
|
|
|
|
import (
|
|
"cmp"
|
|
"net/netip"
|
|
"slices"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/bogon"
|
|
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
|
)
|
|
|
|
// 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 visits every prefix in the table, in batches
|
|
// of walkBatch. After each batch, it lets the table's lock go for as long as
|
|
// it held the lock for the batch.
|
|
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 for walkBatch prefixes at a time. After each batch, it
|
|
// lets the table's lock go for as long as it held the lock for the batch.
|
|
// During a walk over many batches, updates can then take the lock about half
|
|
// of the time. The walk may or may not visit a prefix that is added or removed
|
|
// while the lock is let go. Go allows this in a range over a map that changes.
|
|
// 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
|
|
batchStart := time.Now()
|
|
|
|
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 {
|
|
held := time.Since(batchStart)
|
|
|
|
t.mu.RUnlock()
|
|
time.Sleep(held)
|
|
t.mu.RLock()
|
|
|
|
batchStart = time.Now()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|