# RouteWatch RouteWatch is an MIT-licensed Go daemon by @sneak that keeps a live copy of the internet's BGP routing table and answers questions about IP addresses over HTTP. BGP is the protocol by which the networks of the internet tell each other which addresses they can reach. Each network is an autonomous system, an AS, with a number such as `AS15169`. A prefix is a block of addresses, such as `192.0.2.0/24`. A route says which AS announces a prefix and through which ASes the announcement came. The AS that announces the prefix is its origin AS. The list of ASes is the AS path. RouteWatch does three things: - It streams BGP updates from the RIPE RIS Live service. - It applies them to a routing table that it keeps in memory, with a copy in a SQLite database. - It serves web pages and a JSON API that answer from that table. ## Rationale RouteWatch exists to serve our other services as an internal API for information about IP addresses. A service asks RouteWatch instead of working the answer out for itself. RIPE runs route collectors around the world. Many routers, the RIS peers, send the collectors their routes. RIS Live streams every change the collectors receive, as it happens. RouteWatch applies those changes to its own routing table. From that table it sees directly: - which AS each IP address is routed to - which announced prefix holds the address, and how big that prefix is - how many peers carry the prefix, and with which origin ASes RouteWatch adds details of each AS from WHOIS, the public directory of the internet registries. The details include the organisation that holds the AS and its abuse contact. RouteWatch keeps them in its database and fetches them again once they are 30 days old. An IP lookup answers from the stored details and never waits for WHOIS. An IP lookup also says which public blocklists list the address (see Blocklists). ## Getting started RouteWatch is built to run in Docker. Build the image, which is tagged `routewatch`: ```bash script/docker ``` The build runs the tests and the linter first, and fails when either fails. Run the image: ```bash docker run --memory=5g --memory-swap=5g -p 8080:8080 \ -v routewatch:/var/lib/berlin.sneak.app.routewatch routewatch ``` The memory limit is explained in Memory. The volume holds the database and the copies of the blocklists. To run the binary on the host instead, build it with `make build`. The build needs Go and a C compiler, because the SQLite library is C: ```bash make build ./bin/routewatch ``` `make run` builds the binary and runs it with debug logging, with a copy of the log in `log.txt`. Then open `http://localhost:8080/` in a browser, or ask about an address: ```bash curl http://localhost:8080/ip/192.0.2.1 ``` The routing table starts empty. RIS Live sends no routing table when a client connects, only the changes from then on. So the table holds the routes announced since RouteWatch first started, and it fills as the feed runs. An IP lookup finds no route for an address until a prefix that holds it is announced. The database keeps the table across restarts. ## HTTP Every route answers `HEAD` as it answers `GET`, without the body. The server listens on `PORT`, `8080` by default. The pages and the API read routes from the routing table in memory. For each prefix and origin AS, the table keeps the two newest routes in full, with their AS paths and times. This document calls them the kept routes. The table keeps only the address of every other peer that carries the prefix with that origin AS. ### Web pages - `/`: the home page. It has a lookup by AS number, a search of AS names and a lookup by IP address. A card shows the `curl` command that looks up the visitor's own address, 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, each with its prefix, origin AS and time - the AS most recently seen in an AS path, and the AS seen longest ago - the AS that originates the most prefixes - how many ASes originate a single prefix, and the newest kept route of those ASes - the number of bogon announcements, linked to `/bogons` - the number of prefixes with more than one origin AS, linked to `/multiple-origins` - fifteen 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. - `/as/{asn}`: an AS, the prefixes it originates and the ASes it peers with. 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. - `/prefix/{prefix}/{length}`: a prefix, such as `/prefix/192.0.2.0/24`, with its origin ASes and every peer that carries it. Only the kept routes show an AS path, a next hop and a time. - `/prefixlength/{length}`: up to 500 IPv4 prefixes with that mask length, picked at random. Each shows the origin AS that the most peers carry it with, and the age of its newest kept route. - `/prefixlength6/{length}`: the same for IPv6. - `/bogons`: the bogon announcements. A bogon announcement is a route whose prefix or origin AS is reserved. Each row shows the prefix, the origin AS, the number of peers and the reason. The page lists at most 1,000 rows, in prefix order, and says how many there are in all. - `/multiple-origins`: the prefixes that more than one origin AS announces. Each row shows the origin ASes and how many peers carry the prefix with each. The page lists at most 1,000 rows, in prefix order, and says how many there are in all. - `/status`: a dashboard of the figures of `/api/v1/stats`, refreshed every 2 seconds. 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` An AS number is reserved when it is one of these: - an AS number for private use or for documentation - `AS23456`, which stands in for a 32-bit AS number - the last AS number of the 16-bit range or of the 32-bit range Address space and AS numbers that the IANA has not handed out yet are not counted. ### JSON API A JSON answer is `{"status": "ok", "data": ...}`. An error answer is `{"status": "error", "error": {"msg": ..., "code": ...}}`. Every JSON answer also carries an `@meta` object: - `time_zone`: always `UTC` - `api_version`: `1` - `execution_time_ms`: the number of milliseconds the request took Every value with a unit is a number. The unit ends the field name: `_ms`, `_seconds` or `_bytes`. Every time is in UTC. The routes: - `GET /api/v1/ip/{ip}`: what RouteWatch knows about each address in `{ip}`. See IP lookup below. - `GET /ip/{addr}`: the same answer as `/api/v1/ip/{ip}`. - `GET /ip/`: the same answer, for the caller's own address. That is the address the request came from. For a request from a trusted proxy, it is the client address in `X-Forwarded-For` (see `TRUSTED_PROXIES` in Configuration). - `GET /api/v1/as/{asn}`: the AS in `asn`, and the prefixes it originates in `ipv4_prefixes` and `ipv6_prefixes`, counted in `total_count`. Each prefix has `last_updated`, the time of its newest kept route with the AS. 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}`: the prefix, one route for each peer that carries it in `routes`, and the same routes grouped by origin AS in `origins`. `peer_count` and `origin_count` count them. Only the kept routes have `as_path`, `next_hop` and `last_updated`. - `GET /api/v1/search/as?q={text}`: 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). So a match can be an AS that the database has no record of. - `GET /status.json`: figures of the feed, the memory, the database and the routing table. The figures whose names do not say what they mean: - `uptime_seconds`: the seconds since the feed last connected, or 0 when it has never connected - `memory_usage_bytes`: the size of the Go heap in use - `live_routes`, `ipv4_routes` and `ipv6_routes`: the number of peer and prefix pairs, where each peer that carries a prefix counts once for it - `oldest_route` and `newest_route`: the times of the oldest and the newest kept route - `origins_without_kept_route`: the number of prefix and origin AS pairs that peers carry but that keep no route - `whois_stats.current_interval_seconds`: the WHOIS fetcher's wait between two fetches, which can include a fraction of a second - `GET /api/v1/stats`: the `/status.json` figures plus the reconnection count, and figures of the garbage collector, the stream and the handler queues. `connection_duration_seconds` is the seconds the feed has been connected, or null while it is not connected. `stream.route_churn_per_min` is the number of announcements and withdrawals, from all peers, in the last full clock minute. - `GET /.well-known/healthcheck.json`: the health check. It answers 200 when the database answers and the feed is connected, otherwise 503. `data.status` is `ok` or `error`, and `data.checks` says which check failed. The 503 answer keeps the `{"status": "ok", "data": ...}` form. ### IP lookup `{ip}` is an IP address, a host name, or a comma-separated list of at most 10 of these. A host name is resolved, and only its public addresses are looked up. The answer holds `query`, the text looked up, and `results`, with one entry for each address. Every entry has the same fields: - `ip`: the address - `ptr`: the names DNS gives for the address, its 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: - `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 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 When a host name fails, `errors` lists it with the reason. A name that does not resolve is reported as `could not resolve host name`. A name with no public address, such as one that resolves to a private address, is reported as `host name has no public address`. The answer is 400 when: - `{ip}` lists more than 10 addresses and host names - `{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 Each PTR lookup gets 2 seconds. An address whose lookup takes longer is answered without PTR names. A successful answer carries `Cache-Control` with a `max-age` of 15 minutes, the shortest blocklist interval. The answer for the caller's own address (`/ip/`) is `private`, and the others are `public`. ### Errors - 400, for: - an AS number, prefix or mask length that does not parse - a mask length out of range for its IP version - an AS name search without `q` - an IP lookup with too many addresses, or none (see IP lookup) - 404, for: - AS 0 - an AS that originates no prefix and has no record in the database or in the AS list built into RouteWatch - a prefix that no peer carries - under `/api/v1` and `/ip/`, a path that matches no route, with the message `Not found` - 405, with the message `Method not allowed`, for any method other than `GET` and `HEAD` on the routes under `/api/v1` and `/ip/`, on `/status.json` and on the health check. - 500: `Internal server error`. The detail goes to the log. - 504: `Request timeout`. A request may take 30 seconds. The database reads behind `/status.json` and `/api/v1/stats` may take 4 seconds. The pages give these errors as plain text. ### AS 0 An origin AS of 0 means the route has no known origin. Its AS path ends in an AS set: a group of several ASes, any of which may have originated the route. The 0 appears in these fields: - `origin_asn` in a route - `asn` and `origin_asns` in an IP lookup - the key `0` in a prefix's `origins` The pages show such a route as having no known origin. `/as/0` answers 404, and the AS name search never lists AS 0. ### Headers 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. The pages keep their CSS and JavaScript in inline `