check / check (push) Canceled after 0s
Every result of /ip/, /ip/{addr} and /api/v1/ip/{ip} now has every
field. A field without a value is null instead of left out, so an
address with no live route has all its route and AS fields set to null.
ptr is an empty list when there are no PTR names.
Two new fields, num_origins and origin_asns, give how many origin ASes
peers carry the netblock with, and which. The routing table's prefix
summary now lists them. asn still names the origin that most peers
carry.
first_seen is null, no longer the zero time, when the database has not
stored the netblock yet.
Model: opus-5-5
239 lines
6.2 KiB
Go
239 lines
6.2 KiB
Go
package routetable
|
|
|
|
import (
|
|
"net/netip"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/bits-and-blooms/bitset"
|
|
)
|
|
|
|
// PrefixSummary is what the table answers about one prefix.
|
|
type PrefixSummary struct {
|
|
Prefix netip.Prefix
|
|
// OriginASN is the origin AS that the most peers carry the prefix with.
|
|
// When several have as many peers, it is the lowest AS number.
|
|
OriginASN int
|
|
// OriginASNs are all the origin ASes that peers carry the prefix with, in
|
|
// AS number order.
|
|
OriginASNs []int
|
|
// Peers is how many peers carry the prefix.
|
|
Peers int
|
|
// Newest is the time of the newest kept route of the prefix, or nil when
|
|
// it keeps none.
|
|
Newest *time.Time
|
|
}
|
|
|
|
// OriginatedPrefix is one prefix an AS originates.
|
|
type OriginatedPrefix struct {
|
|
Prefix netip.Prefix
|
|
// Newest is the time of the newest kept route of the prefix with that
|
|
// origin AS, or nil when it keeps none.
|
|
Newest *time.Time
|
|
}
|
|
|
|
// Counts are the table's figures for the stats.
|
|
type Counts struct {
|
|
// Pairs counts the prefix and origin AS pairs that some peer carries.
|
|
Pairs int
|
|
// Peers counts the peers that have a number: those given in
|
|
// Params.PeerNumbers and those numbered since.
|
|
Peers int
|
|
IPv4Prefixes int
|
|
IPv6Prefixes int
|
|
// IPv4PrefixesByLength[n] and IPv6PrefixesByLength[n] are how many
|
|
// prefixes of mask length n some peer carries.
|
|
IPv4PrefixesByLength []int
|
|
IPv6PrefixesByLength []int
|
|
// IPv4Routes and IPv6Routes count peer and prefix pairs: each peer that
|
|
// carries a prefix counts once for it.
|
|
IPv4Routes int
|
|
IPv6Routes int
|
|
// OriginsWithoutKeptRoute counts the prefix and origin AS pairs that peers
|
|
// carry but whose kept routes were all withdrawn.
|
|
OriginsWithoutKeptRoute int
|
|
// OldestKeptRoute and NewestKeptRoute are the times of the oldest and the
|
|
// newest kept routes, or nil when the table keeps none.
|
|
OldestKeptRoute *time.Time
|
|
NewestKeptRoute *time.Time
|
|
}
|
|
|
|
// Lookup returns the most specific prefix that holds addr and that some peer
|
|
// carries. It ignores prefixes shorter than /8 for IPv4 and /16 for IPv6. It
|
|
// makes one map lookup per mask length, and reports false when no prefix
|
|
// holds addr.
|
|
func (t *Table) Lookup(addr netip.Addr) (PrefixSummary, bool) {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
|
|
// An IPv4 address written in IPv6 form (::ffff:192.0.2.1) is looked up as
|
|
// IPv4.
|
|
addr = addr.Unmap()
|
|
f := t.familyOf(addr)
|
|
|
|
for bits := addr.BitLen(); bits >= f.minLookupLength; bits-- {
|
|
prefix := netip.PrefixFrom(addr, bits).Masked()
|
|
|
|
origins, found := f.byLength[bits][prefix]
|
|
if found {
|
|
return summarize(prefix, origins), true
|
|
}
|
|
}
|
|
|
|
return PrefixSummary{}, false
|
|
}
|
|
|
|
// OriginatedPrefixes returns the prefixes AS asn originates, in prefix order.
|
|
func (t *Table) OriginatedPrefixes(asn int) []OriginatedPrefix {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
|
|
prefixes := make([]OriginatedPrefix, 0, len(t.byOrigin[asn]))
|
|
for prefix, o := range t.byOrigin[asn] {
|
|
prefixes = append(prefixes,
|
|
OriginatedPrefix{Prefix: prefix, Newest: o.newestTime()})
|
|
}
|
|
|
|
slices.SortFunc(prefixes, func(a, b OriginatedPrefix) int {
|
|
return a.Prefix.Compare(b.Prefix)
|
|
})
|
|
|
|
return prefixes
|
|
}
|
|
|
|
// Origins returns each origin AS of prefix, in AS number order, with its peers
|
|
// and its kept routes. It returns nil when no peer carries the prefix.
|
|
func (t *Table) Origins(prefix netip.Prefix) []Origin {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
|
|
if !prefix.IsValid() {
|
|
return nil
|
|
}
|
|
|
|
var origins []Origin
|
|
|
|
for _, o := range t.origins(prefix.Masked()) {
|
|
origins = append(origins, Origin{
|
|
ASN: o.asn,
|
|
Peers: t.addresses(&o.peers),
|
|
Routes: t.exportRoutes(o.routes),
|
|
})
|
|
}
|
|
|
|
return origins
|
|
}
|
|
|
|
// SamplePrefixes returns up to limit of the prefixes of IP version ipVersion
|
|
// (4 or 6) and mask length maskLength. It reads only the prefixes it returns.
|
|
// Go starts each walk over a map at a random place, so each call returns
|
|
// another sample.
|
|
func (t *Table) SamplePrefixes(ipVersion, maskLength, limit int) []PrefixSummary {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
|
|
var f *family
|
|
|
|
switch ipVersion {
|
|
case ipVersion4:
|
|
f = &t.ipv4
|
|
case ipVersion6:
|
|
f = &t.ipv6
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
if maskLength < 0 || maskLength >= len(f.byLength) || limit <= 0 {
|
|
return nil
|
|
}
|
|
|
|
prefixes := f.byLength[maskLength]
|
|
sample := make([]PrefixSummary, 0, min(limit, len(prefixes)))
|
|
|
|
for prefix, origins := range prefixes {
|
|
if len(sample) == limit {
|
|
break
|
|
}
|
|
|
|
sample = append(sample, summarize(prefix, origins))
|
|
}
|
|
|
|
return sample
|
|
}
|
|
|
|
// Counts returns the table's figures for the stats. The writes keep them up to
|
|
// date, so this reads no prefix.
|
|
func (t *Table) Counts() Counts {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
|
|
counts := Counts{
|
|
Peers: len(t.peerNumbers),
|
|
IPv4PrefixesByLength: t.ipv4.prefixesByLength(),
|
|
IPv6PrefixesByLength: t.ipv6.prefixesByLength(),
|
|
IPv4Routes: t.ipv4.routes,
|
|
IPv6Routes: t.ipv6.routes,
|
|
OriginsWithoutKeptRoute: t.originsWithoutKeptRoute,
|
|
}
|
|
|
|
for _, n := range counts.IPv4PrefixesByLength {
|
|
counts.IPv4Prefixes += n
|
|
}
|
|
|
|
for _, n := range counts.IPv6PrefixesByLength {
|
|
counts.IPv6Prefixes += n
|
|
}
|
|
|
|
for _, prefixes := range t.byOrigin {
|
|
counts.Pairs += len(prefixes)
|
|
}
|
|
|
|
oldest, found := t.keptTimes.Min()
|
|
if found {
|
|
counts.OldestKeptRoute = &oldest.time
|
|
}
|
|
|
|
newest, found := t.keptTimes.Max()
|
|
if found {
|
|
counts.NewestKeptRoute = &newest.time
|
|
}
|
|
|
|
return counts
|
|
}
|
|
|
|
// addresses returns the addresses of the peers in peers, in number order.
|
|
func (t *Table) addresses(peers *bitset.BitSet) []string {
|
|
addresses := make([]string, 0, peers.Count())
|
|
for number := range peers.EachSet() {
|
|
addresses = append(addresses, t.peerAddresses[number])
|
|
}
|
|
|
|
return addresses
|
|
}
|
|
|
|
// summarize returns the summary of prefix, whose origins are in AS number
|
|
// order.
|
|
func summarize(prefix netip.Prefix, origins []*origin) PrefixSummary {
|
|
summary := PrefixSummary{Prefix: prefix, OriginASNs: make([]int, 0, len(origins))}
|
|
mostPeers := 0
|
|
|
|
for _, o := range origins {
|
|
summary.OriginASNs = append(summary.OriginASNs, o.asn)
|
|
|
|
peers := int(o.peers.Count())
|
|
summary.Peers += peers
|
|
|
|
if peers > mostPeers {
|
|
mostPeers = peers
|
|
summary.OriginASN = o.asn
|
|
}
|
|
|
|
newest := o.newestTime()
|
|
if newest != nil && (summary.Newest == nil || newest.After(*summary.Newest)) {
|
|
summary.Newest = newest
|
|
}
|
|
}
|
|
|
|
return summary
|
|
}
|