Read every route from the routing table in memory (part of #122)
check / check (push) Canceled after 0s

The IP lookups, the AS and prefix pages and their API answers, the prefix-length pages and the stats read routes only from the routing table the prefix handler keeps. fx reads the table at startup and gives it to both. AS names, WHOIS details and first-seen times still come from the database.

The IP lookup names the origin AS most peers carry, with the time of the newest kept route. The prefix answers list peers whose routes are not kept without a path or time. The stats gain origins_without_kept_route. They no longer read route times from the per-peer tables, so the two helpers that did are gone.

Model: opus-5-5
This commit is contained in:
2026-10-27 15:07:46 +00:00
parent 2c304a0022
commit 16db3250bf
18 changed files with 1335 additions and 656 deletions
+322 -170
View File
@@ -5,12 +5,12 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"runtime"
"slices"
"sort"
"strconv"
"strings"
"time"
@@ -19,6 +19,7 @@ import (
"git.eeqj.de/sneak/routewatch/internal/database"
"git.eeqj.de/sneak/routewatch/internal/metrics"
"git.eeqj.de/sneak/routewatch/internal/ristypes"
"git.eeqj.de/sneak/routewatch/internal/routetable"
"git.eeqj.de/sneak/routewatch/internal/streamer"
"git.eeqj.de/sneak/routewatch/internal/templates"
asinfo "git.eeqj.de/sneak/routewatch/pkg/asinfo"
@@ -38,7 +39,7 @@ const (
const (
// maxLookupTargets is the most addresses and host names an IP lookup may
// list, and the most addresses it may have once the names are resolved:
// each address costs up to 113 database reads and a PTR lookup.
// each address costs two database reads and a PTR lookup.
maxLookupTargets = 10
// ptrLookupTimeout is each PTR lookup's own time limit, so that ten
@@ -227,7 +228,8 @@ type WHOISStatsInfo struct {
}
// Stats is the /status.json answer: uptime, message counts, database stats,
// and route information.
// and the routing table's figures. OldestRoute and NewestRoute are the times
// of the oldest and the newest kept routes.
//
//nolint:tagliatelle // field names are the existing JSON API
type Stats struct {
@@ -259,6 +261,10 @@ type Stats struct {
IPv4PrefixDistribution []database.PrefixDistribution `json:"ipv4_prefix_distribution"`
IPv6PrefixDistribution []database.PrefixDistribution `json:"ipv6_prefix_distribution"`
WHOISStats *WHOISStatsInfo `json:"whois_stats,omitempty"`
// OriginsWithoutKeptRoute counts the prefix and origin AS pairs that peers
// carry but whose kept routes were all withdrawn.
OriginsWithoutKeptRoute int `json:"origins_without_kept_route"`
}
// databaseStats gets the database stats, giving up when ctx is done; it then
@@ -317,15 +323,6 @@ func (s *Server) handleStatusJSON() http.HandlerFunc {
return
}
const bitsPerMegabit = 1000000.0
// Get route update metrics
routeMetrics := s.streamer.GetMetricsTracker().GetRouteMetrics()
// Get memory stats
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
// Get WHOIS stats if fetcher is available
var whoisStats *WHOISStatsInfo
if s.asnFetcher != nil {
@@ -337,44 +334,80 @@ func (s *Server) handleStatusJSON() http.HandlerFunc {
}
}
stats := Stats{
Uptime: streamUptime(metrics.ConnectedSince),
TotalMessages: metrics.TotalMessages,
TotalBytes: metrics.TotalBytes,
TotalWireBytes: metrics.TotalWireBytes,
MessagesPerSec: metrics.MessagesPerSec,
MbitsPerSec: metrics.BitsPerSec / bitsPerMegabit,
WireMbitsPerSec: metrics.WireBitsPerSec / bitsPerMegabit,
Connected: metrics.Connected,
GoVersion: runtime.Version(),
Goroutines: runtime.NumGoroutine(),
MemoryUsage: humanize.Bytes(memStats.Alloc),
ASNs: dbStats.ASNs,
Prefixes: dbStats.Prefixes,
IPv4Prefixes: dbStats.IPv4Prefixes,
IPv6Prefixes: dbStats.IPv6Prefixes,
Peerings: dbStats.Peerings,
Peers: dbStats.Peers,
DatabaseSizeBytes: dbStats.FileSizeBytes,
LiveRoutes: dbStats.LiveRoutes,
IPv4Routes: dbStats.IPv4Routes,
IPv6Routes: dbStats.IPv6Routes,
OldestRoute: dbStats.OldestRoute,
NewestRoute: dbStats.NewestRoute,
IPv4UpdatesPerSec: routeMetrics.IPv4UpdatesPerSec,
IPv6UpdatesPerSec: routeMetrics.IPv6UpdatesPerSec,
IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
WHOISStats: whoisStats,
}
err = writeJSONSuccess(w, stats)
err = writeJSONSuccess(w, s.statusStats(metrics, dbStats, whoisStats))
if err != nil {
s.logger.Error("Failed to encode stats", "error", err)
}
}
}
// statusStats builds the /status.json answer from the stream metrics, the
// database stats, the routing table's figures and the WHOIS stats, with the
// route update rates and memory use as they are now.
func (s *Server) statusStats(
streamMetrics metrics.StreamMetrics,
dbStats database.Stats,
whoisStats *WHOISStatsInfo,
) Stats {
const bitsPerMegabit = 1000000.0
routeMetrics := s.streamer.GetMetricsTracker().GetRouteMetrics()
counts := s.table.Counts()
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
return Stats{
Uptime: streamUptime(streamMetrics.ConnectedSince),
TotalMessages: streamMetrics.TotalMessages,
TotalBytes: streamMetrics.TotalBytes,
TotalWireBytes: streamMetrics.TotalWireBytes,
MessagesPerSec: streamMetrics.MessagesPerSec,
MbitsPerSec: streamMetrics.BitsPerSec / bitsPerMegabit,
WireMbitsPerSec: streamMetrics.WireBitsPerSec / bitsPerMegabit,
Connected: streamMetrics.Connected,
GoVersion: runtime.Version(),
Goroutines: runtime.NumGoroutine(),
MemoryUsage: humanize.Bytes(memStats.Alloc),
ASNs: dbStats.ASNs,
Prefixes: counts.IPv4Prefixes + counts.IPv6Prefixes,
IPv4Prefixes: counts.IPv4Prefixes,
IPv6Prefixes: counts.IPv6Prefixes,
Peerings: dbStats.Peerings,
Peers: dbStats.Peers,
DatabaseSizeBytes: dbStats.FileSizeBytes,
LiveRoutes: counts.IPv4Routes + counts.IPv6Routes,
IPv4Routes: counts.IPv4Routes,
IPv6Routes: counts.IPv6Routes,
OldestRoute: counts.OldestKeptRoute,
NewestRoute: counts.NewestKeptRoute,
IPv4UpdatesPerSec: routeMetrics.IPv4UpdatesPerSec,
IPv6UpdatesPerSec: routeMetrics.IPv6UpdatesPerSec,
IPv4PrefixDistribution: prefixDistribution(counts.IPv4PrefixesByLength),
IPv6PrefixDistribution: prefixDistribution(counts.IPv6PrefixesByLength),
WHOISStats: whoisStats,
OriginsWithoutKeptRoute: counts.OriginsWithoutKeptRoute,
}
}
// prefixDistribution lists the mask lengths that have at least one prefix, in
// ascending order, with their prefix counts. countsByLength[n] is the number
// of prefixes of mask length n.
func prefixDistribution(countsByLength []int) []database.PrefixDistribution {
var distribution []database.PrefixDistribution
for maskLength, count := range countsByLength {
if count > 0 {
distribution = append(distribution, database.PrefixDistribution{
MaskLength: maskLength, Count: count,
})
}
}
return distribution
}
// getWHOISStats builds WHOIS statistics from database and fetcher.
func (s *Server) getWHOISStats(ctx context.Context) *WHOISStatsInfo {
// Get database WHOIS stats
@@ -470,7 +503,8 @@ type StreamStats struct {
BGPPeerCount int `json:"bgp_peer_count"`
}
// StatsResponse represents the API statistics response
// StatsResponse represents the API statistics response. The fields it shares
// with Stats mean what they mean there.
//
//nolint:tagliatelle // field names are the existing JSON API
type StatsResponse struct {
@@ -507,6 +541,8 @@ type StatsResponse struct {
IPv4PrefixDistribution []database.PrefixDistribution `json:"ipv4_prefix_distribution"`
IPv6PrefixDistribution []database.PrefixDistribution `json:"ipv6_prefix_distribution"`
WHOISStats *WHOISStatsInfo `json:"whois_stats,omitempty"`
OriginsWithoutKeptRoute int `json:"origins_without_kept_route"`
}
// handlerStatsInfo converts the streamer's handler statistics for the API
@@ -626,8 +662,8 @@ func (s *Server) handleStats() http.HandlerFunc {
}
// statsResponse builds the /api/v1/stats answer from the stream metrics, the
// database stats and the WHOIS stats, with the route update rates, memory use
// and handler queues as they are now.
// database stats, the routing table's figures and the WHOIS stats, with the
// route update rates, memory use and handler queues as they are now.
func (s *Server) statsResponse(
streamMetrics metrics.StreamMetrics,
dbStats database.Stats,
@@ -637,6 +673,7 @@ func (s *Server) statsResponse(
// Get route update metrics
routeMetrics := s.streamer.GetMetricsTracker().GetRouteMetrics()
counts := s.table.Counts()
// Get memory stats
var memStats runtime.MemStats
@@ -660,23 +697,25 @@ func (s *Server) statsResponse(
GC: gcStats(&memStats),
Stream: s.streamStats(),
ASNs: dbStats.ASNs,
Prefixes: dbStats.Prefixes,
IPv4Prefixes: dbStats.IPv4Prefixes,
IPv6Prefixes: dbStats.IPv6Prefixes,
Prefixes: counts.IPv4Prefixes + counts.IPv6Prefixes,
IPv4Prefixes: counts.IPv4Prefixes,
IPv6Prefixes: counts.IPv6Prefixes,
Peerings: dbStats.Peerings,
Peers: dbStats.Peers,
DatabaseSizeBytes: dbStats.FileSizeBytes,
LiveRoutes: dbStats.LiveRoutes,
IPv4Routes: dbStats.IPv4Routes,
IPv6Routes: dbStats.IPv6Routes,
OldestRoute: dbStats.OldestRoute,
NewestRoute: dbStats.NewestRoute,
LiveRoutes: counts.IPv4Routes + counts.IPv6Routes,
IPv4Routes: counts.IPv4Routes,
IPv6Routes: counts.IPv6Routes,
OldestRoute: counts.OldestKeptRoute,
NewestRoute: counts.NewestKeptRoute,
IPv4UpdatesPerSec: routeMetrics.IPv4UpdatesPerSec,
IPv6UpdatesPerSec: routeMetrics.IPv6UpdatesPerSec,
HandlerStats: handlerStatsInfo(s.streamer.GetHandlerStats()),
IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
IPv4PrefixDistribution: prefixDistribution(counts.IPv4PrefixesByLength),
IPv6PrefixDistribution: prefixDistribution(counts.IPv6PrefixesByLength),
WHOISStats: whoisStats,
OriginsWithoutKeptRoute: counts.OriginsWithoutKeptRoute,
}
}
@@ -766,7 +805,7 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
// Look up each IP
for _, ip := range ipsToLookup {
ipInfo, err := s.db.GetIPInfoContext(ctx, ip)
ipInfo, err := s.ipInfo(ctx, ip)
if deadlinePassed(ctx) {
s.writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
@@ -815,6 +854,81 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
}
}
// ipInfo returns the IP lookup's answer for ip. The routing table gives the
// most specific prefix that holds the address, the number of peers that carry
// it, the origin AS that most of them carry, and the time of its newest kept
// route. The database gives the prefix's first-seen time and the AS's WHOIS
// details. It returns ErrNoRoute when no prefix holds the address, and
// ErrInvalidIP when ip does not parse.
func (s *Server) ipInfo(ctx context.Context, ip string) (*database.IPInfo, error) {
addr, err := netip.ParseAddr(ip)
if err != nil {
return nil, fmt.Errorf("%w: %s", database.ErrInvalidIP, ip)
}
summary, found := s.table.Lookup(addr)
if !found {
return nil, fmt.Errorf("%w for IP %s", database.ErrNoRoute, ip)
}
firstSeen, err := s.db.GetPrefixFirstSeen(ctx, summary.Prefix)
if err != nil {
return nil, err
}
asInfo, err := s.db.GetASWHOIS(ctx, summary.OriginASN)
if err != nil {
return nil, err
}
info := &database.IPInfo{
IP: ip,
Netblock: summary.Prefix.String(),
MaskLength: summary.Prefix.Bits(),
IPVersion: ipVersionOf(summary.Prefix),
NumPeers: summary.Peers,
ASN: summary.OriginASN,
ASName: asInfo.ASName,
Handle: asInfo.Handle,
Description: asInfo.Description,
OrgName: asInfo.OrgName,
OrgID: asInfo.OrgID,
Address: asInfo.Address,
CountryCode: asInfo.CountryCode,
AbuseEmail: asInfo.AbuseEmail,
RIR: asInfo.RIR,
FirstSeen: firstSeen,
NeedsWHOISRefresh: needsWHOISRefresh(asInfo),
}
if summary.Newest != nil {
info.LastSeen = *summary.Newest
}
return info, nil
}
// needsWHOISRefresh says whether the WHOIS details of asInfo were never
// fetched or are older than whoisStaleThreshold. AS 0, no known origin, has
// none to fetch.
func needsWHOISRefresh(asInfo *database.ASN) bool {
if asInfo.ASN == ristypes.NoOrigin {
return false
}
return asInfo.WHOISUpdatedAt == nil ||
time.Since(*asInfo.WHOISUpdatedAt) > whoisStaleThreshold
}
// ipVersionOf returns 4 or 6, the IP version of prefix.
func ipVersionOf(prefix netip.Prefix) int {
if prefix.Addr().Is4() {
return ipVersionV4
}
return ipVersionV6
}
// blocklistListings returns the blocklists that list ip. It returns an empty
// list, never nil, so that an answer's blocklists is [] rather than null.
func (s *Server) blocklistListings(ip string) []blocklist.Listing {
@@ -1043,7 +1157,7 @@ func (s *Server) handleASDetailJSON() http.HandlerFunc {
return
}
asInfo, prefixes, err := s.db.GetASDetailsContext(r.Context(), asn)
asInfo, prefixes, err := s.asDetails(r.Context(), asn)
if deadlinePassed(r.Context()) {
s.writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
@@ -1079,6 +1193,49 @@ func (s *Server) handleASDetailJSON() http.HandlerFunc {
}
}
// asDetails returns the AS's handle, description and seen times, from the
// database, and the prefixes it originates, from the routing table. Each
// prefix has the time of its newest kept route, if it keeps one. It returns
// ErrNoRoute for AS 0, which means no known origin. It also returns ErrNoRoute
// for an AS that the database, the AS list built into RouteWatch and the
// routing table all lack.
func (s *Server) asDetails(
ctx context.Context, asn int,
) (*database.ASN, []database.LiveRoute, error) {
if asn == ristypes.NoOrigin {
return nil, nil, fmt.Errorf("%w: AS%d", database.ErrNoRoute, asn)
}
asInfo, known, err := s.db.GetAS(ctx, asn)
if err != nil {
return nil, nil, err
}
originated := s.table.OriginatedPrefixes(asn)
if !known && len(originated) == 0 {
return nil, nil, fmt.Errorf("%w: AS%d", database.ErrNoRoute, asn)
}
prefixes := make([]database.LiveRoute, 0, len(originated))
for _, p := range originated {
route := database.LiveRoute{
Prefix: p.Prefix.String(),
MaskLength: p.Prefix.Bits(),
IPVersion: ipVersionOf(p.Prefix),
OriginASN: asn,
}
if p.Newest != nil {
route.LastUpdated = *p.Newest
}
prefixes = append(prefixes, route)
}
return asInfo, prefixes, nil
}
// splitRoutesByIPVersion groups routes into the IPv4 and the IPv6 ones. Neither
// group is nil, so that a JSON answer holds [] for an empty one and not null.
func splitRoutesByIPVersion(
@@ -1123,8 +1280,7 @@ func (s *Server) handlePrefixDetailJSON() http.HandlerFunc {
return
}
// Look the prefix up in the text form net/netip prints, the form the
// tables store, so that any valid way of writing it finds the route.
// Masking the prefix lets any valid way of writing it find its routes.
parsed, err := netip.ParsePrefix(prefixParam + "/" + lenParam)
if err != nil {
s.writeJSONError(w, http.StatusBadRequest, invalidPrefixMessage)
@@ -1132,27 +1288,18 @@ func (s *Server) handlePrefixDetailJSON() http.HandlerFunc {
return
}
prefix := parsed.Masked().String()
parsed = parsed.Masked()
prefix := parsed.String()
routes, _, err := s.db.GetPrefixDetailsContext(r.Context(), prefix)
if deadlinePassed(r.Context()) {
s.writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
origins := s.table.Origins(parsed)
if len(origins) == 0 {
s.writeJSONError(w, http.StatusNotFound,
database.ErrNoRoute.Error()+": "+prefix)
return
}
if err != nil {
if errors.Is(err, database.ErrNoRoute) {
s.writeJSONError(w, http.StatusNotFound, err.Error())
} else {
s.logger.Error("Failed to get prefix details",
"prefix", prefix, "error", err)
s.writeJSONError(w, http.StatusInternalServerError,
internalErrorMessage)
}
return
}
routes := prefixRoutes(parsed, origins)
// Group by origin AS
originMap := make(map[int][]database.LiveRoute)
@@ -1237,7 +1384,7 @@ func (s *Server) handleASDetail() http.HandlerFunc {
return
}
asInfo, prefixes, err := s.db.GetASDetailsContext(r.Context(), asn)
asInfo, prefixes, err := s.asDetails(r.Context(), asn)
if deadlinePassed(r.Context()) {
http.Error(w, requestTimeoutMessage, http.StatusGatewayTimeout)
@@ -1327,8 +1474,7 @@ func (s *Server) handlePrefixDetail() http.HandlerFunc {
return
}
// Look the prefix up in the text form net/netip prints, the form the
// tables store, so that any valid way of writing it finds the route.
// Masking the prefix lets any valid way of writing it find its routes.
parsed, err := netip.ParsePrefix(prefixParam + "/" + lenParam)
if err != nil {
http.Error(w, invalidPrefixMessage, http.StatusBadRequest)
@@ -1336,9 +1482,16 @@ func (s *Server) handlePrefixDetail() http.HandlerFunc {
return
}
prefix := parsed.Masked().String()
parsed = parsed.Masked()
routes, originNames, err := s.db.GetPrefixDetailsContext(r.Context(), prefix)
origins := s.table.Origins(parsed)
if len(origins) == 0 {
http.Error(w, "Prefix not found", http.StatusNotFound)
return
}
originInfos, err := s.originASNs(r.Context(), origins)
if deadlinePassed(r.Context()) {
http.Error(w, requestTimeoutMessage, http.StatusGatewayTimeout)
@@ -1346,27 +1499,13 @@ func (s *Server) handlePrefixDetail() http.HandlerFunc {
}
if err != nil {
if errors.Is(err, database.ErrNoRoute) {
http.Error(w, "Prefix not found", http.StatusNotFound)
} else {
s.logger.Error("Failed to get prefix details", "error", err)
http.Error(w, internalErrorMessage, http.StatusInternalServerError)
}
s.logger.Error("Failed to get the origin ASes' names", "error", err)
http.Error(w, internalErrorMessage, http.StatusInternalServerError)
return
}
// Group by origin AS and collect unique AS info
origins := originASNs(routes, originNames)
// Get the first route to extract some common info
var maskLength, ipVersion int
if len(routes) > 0 {
maskLength, ipVersion = prefixMaskLengthAndIPVersion(prefix)
}
// Create enhanced routes with AS path handles
enhancedRoutes := routesWithASPathHandles(routes)
routes := prefixRoutes(parsed, origins)
// Prepare template data
data := struct {
@@ -1379,13 +1518,13 @@ func (s *Server) handlePrefixDetail() http.HandlerFunc {
OriginCount int
CSPNonce string
}{
Prefix: prefix,
MaskLength: maskLength,
IPVersion: ipVersion,
Routes: enhancedRoutes,
Origins: origins,
Prefix: parsed.String(),
MaskLength: parsed.Bits(),
IPVersion: ipVersionOf(parsed),
Routes: routesWithASPathHandles(routes),
Origins: originInfos,
PeerCount: len(routes),
OriginCount: len(origins),
OriginCount: len(originInfos),
CSPNonce: cspNonce(r.Context()),
}
@@ -1415,34 +1554,72 @@ type ASNInfo struct {
PeerCount int
}
// originASNs groups routes by origin AS, with each AS's handle and description
// from names, as the prefix query returned them, and the number of routes it
// originates. The origin ASes come in no particular order.
func originASNs(
routes []database.LiveRoute,
names map[int]database.ASN,
) []*ASNInfo {
originMap := make(map[int]*ASNInfo)
// originASNs returns each of origins, a prefix's origin ASes, with its handle
// and description from the database and the number of peers that carry the
// prefix with it.
func (s *Server) originASNs(
ctx context.Context, origins []routetable.Origin,
) ([]*ASNInfo, error) {
infos := make([]*ASNInfo, 0, len(origins))
for _, route := range routes {
if _, exists := originMap[route.OriginASN]; !exists {
originMap[route.OriginASN] = &ASNInfo{
ASN: route.OriginASN,
Handle: names[route.OriginASN].Handle,
Description: names[route.OriginASN].Description,
PeerCount: 0,
}
for _, origin := range origins {
asInfo, _, err := s.db.GetAS(ctx, origin.ASN)
if err != nil {
return nil, err
}
originMap[route.OriginASN].PeerCount++
infos = append(infos, &ASNInfo{
ASN: origin.ASN,
Handle: asInfo.Handle,
Description: asInfo.Description,
PeerCount: len(origin.Peers),
})
}
origins := make([]*ASNInfo, 0, len(originMap))
for _, origin := range originMap {
origins = append(origins, origin)
return infos, nil
}
// prefixRoutes returns one route for each peer that carries prefix, by origin
// AS in the order of origins. Each origin's kept routes come first, newest
// first, with their AS path, next hop and time. Its other peers follow without
// them, since the routing table keeps only their addresses.
func prefixRoutes(
prefix netip.Prefix, origins []routetable.Origin,
) []database.LiveRoute {
var routes []database.LiveRoute
for _, origin := range origins {
base := database.LiveRoute{
Prefix: prefix.String(),
MaskLength: prefix.Bits(),
IPVersion: ipVersionOf(prefix),
OriginASN: origin.ASN,
}
kept := make(map[string]bool, len(origin.Routes))
for _, r := range origin.Routes {
kept[r.Peer] = true
route := base
route.PeerIP = r.Peer
route.ASPath = r.ASPath
// The next hop is the peer, as the prefix handler records it.
route.NextHop = r.Peer
route.LastUpdated = r.Time
routes = append(routes, route)
}
for _, peer := range origin.Peers {
if !kept[peer] {
route := base
route.PeerIP = peer
routes = append(routes, route)
}
}
}
return origins
return routes
}
// ASPathEntry is one AS of a route's AS path, with its handle.
@@ -1484,23 +1661,6 @@ func routesWithASPathHandles(routes []database.LiveRoute) []EnhancedRoute {
return enhancedRoutes
}
// prefixMaskLengthAndIPVersion returns the mask length and IP version of a
// prefix in CIDR notation, or zeros if it does not parse.
func prefixMaskLengthAndIPVersion(prefix string) (int, int) {
_, ipNet, err := net.ParseCIDR(prefix)
if err != nil {
return 0, 0
}
maskLength, _ := ipNet.Mask.Size()
if ipNet.IP.To4() != nil {
return maskLength, ipVersionV4
}
return maskLength, ipVersionV6
}
// handlePrefixLength shows a random sample of IPv4 prefixes with the
// specified mask length
func (s *Server) handlePrefixLength() http.HandlerFunc {
@@ -1539,29 +1699,11 @@ func (s *Server) prefixLengthHandler(ipVersion, maxMaskLength int) http.HandlerF
// Get random sample of prefixes
const maxPrefixes = 500
prefixes, err := s.db.GetRandomPrefixesByLengthContext(
r.Context(), maskLength, ipVersion, maxPrefixes)
if deadlinePassed(r.Context()) {
http.Error(w, requestTimeoutMessage, http.StatusGatewayTimeout)
return
}
if err != nil {
s.logger.Error("Failed to get prefixes by length", "error", err)
http.Error(w, internalErrorMessage, http.StatusInternalServerError)
return
}
prefixes := s.table.SamplePrefixes(ipVersion, maskLength, maxPrefixes)
// Sort prefixes for display
sort.Slice(prefixes, func(i, j int) bool {
// First compare by IP version
if prefixes[i].IPVersion != prefixes[j].IPVersion {
return prefixes[i].IPVersion < prefixes[j].IPVersion
}
// Then by prefix
return prefixes[i].Prefix < prefixes[j].Prefix
slices.SortFunc(prefixes, func(a, b routetable.PrefixSummary) int {
return a.Prefix.Compare(b.Prefix)
})
// Render template
@@ -1591,8 +1733,8 @@ func (s *Server) prefixLengthHandler(ipVersion, maxMaskLength int) http.HandlerF
}
}
// EnhancedPrefix is a live route on the prefix length page, with the
// description of its origin AS and its age.
// EnhancedPrefix is a prefix on the prefix length page, with the origin AS
// that most peers carry it with, that AS's description, and its age.
type EnhancedPrefix struct {
database.LiveRoute
@@ -1601,13 +1743,23 @@ type EnhancedPrefix struct {
}
// prefixesWithASDescriptions looks up the description of the origin AS of
// each prefix and gives each one's age.
func prefixesWithASDescriptions(prefixes []database.LiveRoute) []EnhancedPrefix {
// each prefix. It gives each prefix the age of its newest kept route, or no
// age when it keeps none.
func prefixesWithASDescriptions(
prefixes []routetable.PrefixSummary,
) []EnhancedPrefix {
enhancedPrefixes := make([]EnhancedPrefix, len(prefixes))
for i, prefix := range prefixes {
enhancedPrefixes[i] = EnhancedPrefix{
LiveRoute: prefix,
Age: formatAge(prefix.LastUpdated),
Prefix: prefix.Prefix.String(),
MaskLength: prefix.Prefix.Bits(),
IPVersion: ipVersionOf(prefix.Prefix),
OriginASN: prefix.OriginASN,
}
if prefix.Newest != nil {
enhancedPrefixes[i].LastUpdated = *prefix.Newest
enhancedPrefixes[i].Age = formatAge(*prefix.Newest)
}
// Get AS description