check / check (push) Canceled after 0s
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
207 lines
6.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|