check / check (push) Canceled after 0s
The table's PeerDown visits every prefix under the write lock, which takes over a second on a full table. The prefix handler called it inline, so its queue filled and the streamer dropped updates. PeerDown now lets the lock go after every 10,000 prefixes. The handler runs it on a goroutine of its own, one peer at a time, and holds back the down peer's later messages until it returns, then applies them in order. Stop waits for this before its last flush, and logs the held-back messages it could not apply. Model: opus-5-5
256 lines
7.4 KiB
Go
256 lines
7.4 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 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)
|
|
}
|