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) }