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 holds the lock for 10,000 prefixes at a time, then lets the lock go for as long again. The handler runs PeerDown on a separate goroutine, one peer at a time. The handler holds back the down peer's later messages until that PeerDown returns. Then the handler applies them in order, one at a time, and takes new messages in between. It holds back at most 20,000 messages. Stop waits for all of this before its last flush. 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)
|
|
}
|