Read every route from the routing table in memory (part of #122)
check / check (push) Waiting to run

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
+49 -34
View File
@@ -12,16 +12,16 @@ table in real time. It works like this:
RIPE RIS Live streams the BGP updates that RIPE's route collectors receive, as
they happen, from the moment a client connects. It reports changes, not the
routing table they add up to, so it cannot say which AS an IP address is routed
to now or which routes a prefix has. RouteWatch applies the feed's updates to
live routing tables in its own SQLite database and answers IP-to-AS lookups and
prefix pages from them.
to now or which routes a prefix has. RouteWatch applies the feed's updates to a
routing table that it keeps in memory, with a copy in its own SQLite database.
It answers IP-to-AS lookups and prefix pages from that table.
The feed sends no table when a client connects, so these tables hold only the
routes the feed has announced since RouteWatch first started: an IP-to-AS lookup
finds nothing for an address whose routes have not been announced since then.
The tables also miss the updates RouteWatch drops when it falls behind under
load and those sent while it is stopped, so a lookup or a prefix page can show a
route the feed has withdrawn.
The feed sends no table when a client connects. So the routing table holds only
the routes announced since RouteWatch first started. An IP-to-AS lookup finds
nothing for an address whose routes have not been announced since then. The
routing table also misses updates RouteWatch drops when it falls behind, and
updates sent while it is stopped. So a lookup or a prefix page can show a route
the feed has withdrawn.
## Features
@@ -58,13 +58,24 @@ DEBUG=routewatch ./routewatch
Every route answers `HEAD` as it answers `GET`, without the body.
The pages and the API read routes from the routing table that RouteWatch keeps
in memory. For each prefix and origin AS, the table keeps the two newest routes
in full, with their AS paths and times. These are the kept routes. The table
keeps only the address of every other peer that carries the prefix. When both
kept routes of an origin AS are withdrawn, that origin has no kept route until
one of its peers sends an update.
### Web Interface
- `GET /` - Home page (HTML): headline figures, lookups by AS number and by IP
address, and a search of AS names. A card shows the `curl` command that looks
up the visitor's own address at `/ip/{addr}`, and that command's live answer.
- `GET /status` - HTML status dashboard, which reads `/api/v1/stats`
- `GET /status.json` - JSON statistics: feed, memory, database and route figures
- `GET /status.json` - JSON statistics: feed, memory, database and route
figures. The route figures come from the routing table. `oldest_route` and
`newest_route` are the times of the oldest and the newest kept routes.
`origins_without_kept_route` counts, across all prefixes, the origin ASes that
have no kept route.
- `GET /.well-known/healthcheck.json` - Health check (JSON): 200 when the
database answers and the RIS Live feed is connected, otherwise 503;
`data.status` is `ok` or `error` and `data.checks` says which check failed
@@ -73,10 +84,11 @@ Every route answers `HEAD` as it answers `GET`, without the body.
description come from the AS list built into RouteWatch. The page then shows
no first-seen date.
- `GET /prefix/{prefix}/{length}` - Prefix detail page (HTML), such as
`/prefix/192.0.2.0/24`: every live route for the prefix, one per peer, and its
origin ASes
`/prefix/192.0.2.0/24`: its origin ASes and every peer that carries it. Only
the kept routes show an AS path, a next hop and a time.
- `GET /prefixlength/{length}` - Up to 500 IPv4 prefixes with that mask length,
picked at random, each with its newest route (HTML)
picked at random (HTML). Each shows the origin AS that most peers carry it
with, and the age of its newest kept route.
- `GET /prefixlength6/{length}` - The same for IPv6
- `GET /ip/{addr}` - The same JSON answer as `/api/v1/ip/{ip}`
- `GET /ip/` - The same, for the caller's own address: the address the request
@@ -93,8 +105,10 @@ Every route answers `HEAD` as it answers `GET`, without the body.
name, or a comma-separated list of at most 10 of these. `results` has one
entry for each address, with:
- `ip` and `ptr`: the address and its PTR names
- `netblock` and the fields beside it: the address's most specific live
route, and that route's origin AS with its WHOIS details
- `netblock` and the fields beside it: the most specific prefix that holds
the address, and its origin AS with that AS's WHOIS details. For a prefix
with several origin ASes, `asn` is the one that most peers carry.
`last_seen` is the time of the prefix's newest kept route.
- `blocklists`: the public blocklists the address is on (see Blocklists)
An address with no live route has a result without the route and AS fields.
@@ -110,11 +124,13 @@ Every route answers `HEAD` as it answers `GET`, without the body.
answered without PTR names.
- `GET /api/v1/as/{asn}` - The AS and the prefixes it originates, IPv4 and IPv6
apart, each with when its newest route was updated. When the database has no
record of the AS, its handle and description come from the AS list built into
apart, each with the time of its newest kept route. A prefix that keeps no
route with the AS has no `last_updated`. When the database has no record of
the AS, its handle and description come from the AS list built into
RouteWatch. The answer then has no `first_seen` or `last_seen`.
- `GET /api/v1/prefix/{prefix}/{length}` - Every live route for the prefix, one
per peer, and the same routes grouped by origin AS in `origins`
- `GET /api/v1/prefix/{prefix}/{length}` - One route for each peer that carries
the prefix, and the same routes grouped by origin AS in `origins`. Only the
kept routes have `as_path`, `next_hop` and `last_updated`.
- `GET /api/v1/search/as?q={text}` - Finds the ASes whose handle or description
contains `{text}`, ignoring case. `data` holds:
- `query`: `{text}` without its leading and trailing spaces
@@ -137,16 +153,16 @@ took. The health check is the exception: its 503 answer is also
A request that runs past its time limit answers 504: the limit is 30 seconds,
and 4 seconds for the database reads behind `/status.json` and `/api/v1/stats`.
An AS number, prefix or mask length that does not parse answers 400, as does a
mask length out of range for its IP version. An AS answers 404 when it is not in
the AS list built into RouteWatch and the database has neither a record of it
nor a live route it originates. AS 0 always answers 404, because it means no
known origin. The AS name search never lists it. A prefix with no live route
answers 404 too. The HTML pages give these errors as plain text. Under `/api/v1`
and `/ip/`, a path no route matches answers 404 with the message `Not found`,
and `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, `TRACE` or `CONNECT` answers
405 with `Method not allowed`. A 500 answer says only `Internal server error`,
and a host name that does not resolve only `could not resolve host name`; the
detail goes to the log.
mask length out of range for its IP version. An AS answers 404 when it
originates no prefix and has no record in the database or in the AS list built
into RouteWatch. AS 0 always answers 404, because it means no known origin. The
AS name search never lists it. A prefix with no live route answers 404 too. The
HTML pages give these errors as plain text. Under `/api/v1` and `/ip/`, a path
no route matches answers 404 with the message `Not found`, and `POST`, `PUT`,
`PATCH`, `DELETE`, `OPTIONS`, `TRACE` or `CONNECT` answers 405 with
`Method not allowed`. A 500 answer says only `Internal server error`, and a host
name that does not resolve only `could not resolve host name`; the detail goes
to the log.
Every answer carries `Strict-Transport-Security`, `Content-Security-Policy`,
`X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy` and
@@ -340,11 +356,10 @@ routewatch/
with exponential backoff (5s-320s). The backoff resets after a stream that
lasted more than 30s. A stream with no line for 60s is dropped and
reconnected.
- **IP Lookup**: the most specific live route is found by looking up the
address's prefix at each mask length, longest first, in the prefix index (at
most 25 lookups for IPv4, 113 for IPv6); prefixes are stored in the text form
Go's `net/netip` prints. Routes shorter than /8 for IPv4 or /16 for IPv6, such
as a default route, are not used.
- **IP Lookup**: the routing table in memory finds the most specific prefix that
holds the address. It looks up the address's prefix at each mask length,
longest first: at most 25 lookups for IPv4 and 113 for IPv6. Prefixes shorter
than /8 for IPv4 or /16 for IPv6, such as a default route, are not used.
### Database Schema