# RouteWatch RouteWatch is an MIT-licensed Go daemon by @sneak that monitors the BGP routing table in real time. It works like this: - It streams BGP UPDATE messages from the RIPE RIS Live service. - It keeps the whole routing table in memory, with a copy in SQLite. - It serves web pages and a JSON API for looking routes up. ## Rationale 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 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 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 - Real-time streaming of BGP updates from RIPE RIS Live - Maintains live IPv4 and IPv6 routing tables - Tracks AS peering relationships - HTTP API for IP-to-AS lookups, prefix details, and AS information - IP lookups report the public blocklists an address is on, from copies kept in memory and on disk - Automatic reconnection with exponential backoff - Batched database writes for high performance - Backpressure handling to prevent memory exhaustion ## Installation ```bash go build -o routewatch ./cmd/routewatch ``` ## Usage ```bash # Run the daemon (listens on port 8080 by default) ./routewatch # Set custom port PORT=3000 ./routewatch # Enable debug logging DEBUG=routewatch ./routewatch ``` ## HTTP Endpoints 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. Other cards show figures of the routing table, with links to the AS and prefix pages: - the newest and the oldest kept route, with its prefix, origin AS and time - the AS last seen in an AS path most recently, and the one last seen longest ago - the AS that originates the most prefixes - how many ASes originate a single prefix, and the most recently updated of them - the number of bogon announcements, linked to `/bogons` - the number of prefixes with more than one origin AS, linked to `/multiple-origins` - well-known networks, such as Apple, Google, Cogent and MIT, each with its number of prefixes and the time of its newest kept route, newest first RouteWatch works these figures out once a minute, so they can be a minute old. - `GET /bogons` - The bogon announcements (HTML): the routes whose prefix or origin AS is reserved. A prefix is reserved when it lies inside one of these: - a block that the IANA special-purpose address registries mark as not globally reachable - the multicast space, `224.0.0.0/4` and `ff00::/8`, which the IANA IPv4 and IPv6 address space registries list as Multicast A reserved AS number is one of these: - an AS number for private use or for documentation - `AS23456` - the last AS number of the 16-bit or the 32-bit range Address space and AS numbers that the IANA has not handed out yet are not counted. Each row shows the prefix, the origin AS, the number of peers and why it is a bogon. The page shows at most 1,000 rows, in prefix order, and says how many there are in all. - `GET /multiple-origins` - The prefixes announced by more than one origin AS (HTML). Each row shows the origin ASes and how many peers carry the prefix with each. Like `/bogons`, it shows at most 1,000 rows, in prefix order, and says how many there are in all. - `GET /status` - HTML status dashboard, which reads `/api/v1/stats` - `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 - `GET /as/{asn}` - AS detail page (HTML): the prefixes the AS originates and its peers. When the database has no record of the AS, its handle and 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`: 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 (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 came from or, when that is a trusted proxy (`TRUSTED_PROXIES`, see Configuration), the client address in its `X-Forwarded-For` ### API v1 - `GET /api/v1/stats` - The `/status.json` figures plus connection, garbage collector, stream and handler queue statistics. `stream.route_churn_per_min` is the number of prefix announcements and withdrawals, from all peers, in the last full clock minute. - `GET /api/v1/ip/{ip}` - Looks up each address. `{ip}` is an IP address, a host name, or a comma-separated list of at most 10 of these. `results` has one entry for each address. Every entry has the same fields: - `ip`: the address - `ptr`: the address's PTR names, or an empty list when it has none - `blocklists`: the public blocklists the address is on (see Blocklists), or an empty list when it is on none Every other field is about the address's live route. Each is null when the address has no live route. The fields about the route: - `netblock`: the most specific prefix that holds the address - `mask_length`: the netblock's mask length - `ip_version`: the netblock's IP version, 4 or 6 - `num_peers`: the number of peers that carry the netblock - `asn`: the origin AS that the most peers carry the netblock with - `num_origins`: the number of origin ASes that peers carry the netblock with - `origin_asns`: those origin ASes, in AS number order - `first_seen`: when RouteWatch first saw the netblock, or null when the database has not stored the netblock yet - `last_seen`: the time of the netblock's newest kept route, or null when the netblock keeps no route The fields about the AS in `asn` come from the database. Each is also null when the database does not have it: - `handle`: the AS's handle - `description`: the AS's description - `as_name`: the AS's name in its WHOIS details - `org_name`: the name of the organisation that holds the AS - `org_id`: the organisation's ID at its registry - `address`: the organisation's postal address - `country_code`: the organisation's country code - `abuse_email`: the address that takes abuse reports for the AS - `rir`: the regional internet registry of the AS Of the addresses a host name resolves to, only public ones are looked up. A name with none, such as one that resolves to a private address, is reported as `host name has no public address`, without its addresses. `errors` has one entry for each host name that failed. The answer is 400 when: - `{ip}` lists more than 10 - `{ip}` comes to more than 10 addresses once its names are resolved - nothing in `{ip}` is an IP address or a host name with a public address A PTR lookup gets 2 seconds. An address whose lookup takes longer is answered without PTR names. - `GET /api/v1/as/{asn}` - The AS and the prefixes it originates, IPv4 and IPv6 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}` - 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 - `total`: the number of ASes that match - `results`: the first 50 of them in AS number order, each with `asn`, `handle` and `description` The search reads the AS list built into RouteWatch, not the database. The list comes from [ipverse/as-metadata](https://github.com/ipverse/as-metadata). A match can therefore be an AS that the database has no record of. A `q` that is missing, empty or only spaces answers 400. A JSON answer is `{"status": "ok", "data": ...}`, or for an error `{"status": "error", "error": {"msg": ..., "code": ...}}`, and carries an `@meta` object with the time zone, the API version and the time the request took. The health check is the exception: its 503 answer is also `{"status": "ok", "data": ...}`, with `error` in `data.status`. 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 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 `Permissions-Policy`. The Content-Security-Policy lets a page load content only from the server, and does not allow inline content with `'unsafe-inline'`: the pages keep their CSS and JavaScript in inline `