Files
routewatch/internal/server/home.go
T
clawbot 281d162e27
check / check (push) Canceled after 0s
Show figures of the routing table on the home page (closes #154)
The home page gets cards for the newest and oldest kept route, the newest
and oldest updated AS, the AS with the most prefixes, the ASes with a
single prefix and fifteen well-known networks. Two more cards count the
bogon announcements and the prefixes with several origin ASes. They link
to the new pages /bogons and /multiple-origins, which list up to 1,000
entries each.

The new package internal/bogon tells reserved prefixes and AS numbers,
from the IANA special-purpose and address space registries. Table.Overview
walks the table and lets the read lock go every 10,000 prefixes. The
server works the cards out once a minute and serves that copy. A new index
on asns(last_seen) finds the newest and oldest updated AS.

Model: opus-5-5
2026-10-27 18:27:01 +01:00

207 lines
6.1 KiB
Go

package server
import (
"context"
"html/template"
"net/http"
"slices"
"time"
"git.eeqj.de/sneak/routewatch/internal/database"
"git.eeqj.de/sneak/routewatch/internal/routetable"
"git.eeqj.de/sneak/routewatch/internal/templates"
asinfo "git.eeqj.de/sneak/routewatch/pkg/asinfo"
)
const (
// homeCardsRefreshInterval is how often the server works out the home
// page's cards again. Some of their figures take a walk over the whole
// routing table, so no request works them out.
homeCardsRefreshInterval = time.Minute
// homeCardsDatabaseTimeout is how long the database read behind the
// cards of the newest and the oldest AS may take.
homeCardsDatabaseTimeout = 4 * time.Second
// maxListedEntries is how many entries the bogon and multiple-origin
// pages list at most.
maxListedEntries = 1000
)
// homeCards is what the home page's cards show. The bogon and multiple-origin
// pages list the entries their cards count. The server works it out every
// homeCardsRefreshInterval and serves this copy in between.
type homeCards struct {
routetable.Overview
// NewestAS and OldestAS are the ASes the asns table last saw most
// recently and longest ago. They are nil when the database did not
// answer or holds no AS.
NewestAS *database.ASN
OldestAS *database.ASN
// WellKnownNetworks are the networks of wellKnownASNs, newest update
// first.
WellKnownNetworks []wellKnownNetwork
}
// wellKnownNetwork is one network on the home page's card of well-known
// networks.
type wellKnownNetwork struct {
ASN int
// Name is its description in the AS list built into RouteWatch.
Name string
// Prefixes is how many prefixes it originates.
Prefixes int
// Newest is the time of its newest kept route, or nil when it keeps none.
Newest *time.Time
}
// listPageData is what the bogon and multiple-origin pages are rendered with.
type listPageData struct {
// CSPNonce is the nonce the page's style block carries.
CSPNonce string
// Cards is the copy of the home page's cards, or nil before the server
// has first worked them out.
Cards *homeCards
}
// The AS numbers of the networks the home page lists as well known.
const (
appleASN = 714
googleASN = 15169
amazonASN = 16509
microsoftASN = 8075
metaASN = 32934
cloudflareASN = 13335
akamaiASN = 20940
netflixASN = 2906
dodNetworkInfoCtrASN = 721
cogentASN = 174
hurricaneElectricASN = 6939
level3ASN = 3356
arelionASN = 1299
nttASN = 2914
mitASN = 3
)
// wellKnownASNs returns the AS numbers of the networks the home page lists as
// well known: large companies, large transit networks, the US Department of
// Defense's Network Information Center and a university.
func wellKnownASNs() []int {
return []int{
appleASN, googleASN, amazonASN, microsoftASN, metaASN, cloudflareASN,
akamaiASN, netflixASN, dodNetworkInfoCtrASN, cogentASN,
hurricaneElectricASN, level3ASN, arelionASN, nttASN, mitASN,
}
}
// keepHomeCardsFresh works out the home page's cards now, and again every
// interval until ctx ends.
func (s *Server) keepHomeCardsFresh(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
s.refreshHomeCards(ctx)
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}
// refreshHomeCards works out the home page's cards and keeps them for the
// pages to serve. When the database does not answer, the failure is logged
// and the cards of the newest and the oldest AS stay empty.
func (s *Server) refreshHomeCards(ctx context.Context) {
cards := &homeCards{
Overview: s.table.Overview(maxListedEntries),
WellKnownNetworks: s.wellKnownNetworks(),
}
ctx, cancel := context.WithTimeout(ctx, homeCardsDatabaseTimeout)
defer cancel()
newest, oldest, err := s.db.GetLastSeenASes(ctx)
if err != nil {
s.logger.Warn("Failed to read the newest and the oldest AS", "error", err)
} else {
cards.NewestAS, cards.OldestAS = newest, oldest
}
s.cards.Store(cards)
}
// wellKnownNetworks returns the networks of wellKnownASNs, each with how many
// prefixes it originates and the time of its newest kept route. They come
// newest first, and those that keep no route last.
func (s *Server) wellKnownNetworks() []wellKnownNetwork {
asns := wellKnownASNs()
networks := make([]wellKnownNetwork, 0, len(asns))
for _, asn := range asns {
originated := s.table.OriginatedPrefixes(asn)
network := wellKnownNetwork{ASN: asn, Prefixes: len(originated)}
if info, ok := asinfo.Get(asn); ok {
network.Name = info.Description
}
for _, p := range originated {
if p.Newest != nil &&
(network.Newest == nil || p.Newest.After(*network.Newest)) {
network.Newest = p.Newest
}
}
networks = append(networks, network)
}
slices.SortStableFunc(networks, func(a, b wellKnownNetwork) int {
switch {
case a.Newest == nil && b.Newest == nil:
return 0
case a.Newest == nil:
return 1
case b.Newest == nil:
return -1
}
return b.Newest.Compare(*a.Newest)
})
return networks
}
// handleBogons returns a handler that serves the page listing the bogon
// announcements.
func (s *Server) handleBogons() http.HandlerFunc {
return s.listPageHandler(templates.BogonsTemplate())
}
// handleMultipleOrigins returns a handler that serves the page listing the
// prefixes announced by more than one origin AS.
func (s *Server) handleMultipleOrigins() http.HandlerFunc {
return s.listPageHandler(templates.MultipleOriginsTemplate())
}
// listPageHandler returns a handler that renders tmpl, a page that lists
// entries the home page's cards count, from the copy of the cards.
func (s *Server) listPageHandler(tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := tmpl.Execute(w, listPageData{
CSPNonce: cspNonce(r.Context()),
Cards: s.cards.Load(),
})
if err != nil {
s.logger.Error("Failed to render a list page", "error", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
}