Harden import, the kernel task and the RTR client after review

Three independent reviews (hijack resistance, reliability, code health)
of the whole tree. This is what they found that could let a bad route in
or take the daemon down.

Hijacks:
- A route had two paths on a two-octet session, AS_PATH and AS4_PATH,
  read in two places by two rules. A peer could make the origin checked
  on import differ from the one checked at revalidation, and get an
  RPKI-invalid route into the table. AS4_PATH is now merged into AS_PATH
  once, on receipt (RFC 6793 §4.2.3), and nothing else knows of it.
- The next hop is judged on import: it must be usable, agreed on
  (RFC 8950), and by default the peer itself.
- Our own originated prefixes are not taken from other networks.
- A route for a network the machine is on is never put in the kernel.
- Paths with an AS_SET are refused (RFC 9774).
- A configured peer gets one session, not one per BGP Identifier.
- More IPv6 special-purpose prefixes are bogons.

Reliability:
- A batch of requests the kernel refused could overflow the netlink
  socket and end the daemon. The socket has room now, a lost answer or a
  silent kernel means asking what is there, and neither is fatal.
- Working the routes out again within the grace period after a restart
  no longer drops the routes being kept.
- That work can no longer be put off for ever by interface changes.
- Peers whose sessions keep failing are slowed down.
- Unused attribute sets are swept when they pile up, not only each
  minute.
- Log lines a peer can cause are said once in a while.
- An RTR answer is bounded in size and time; data cannot expire between
  two refreshes.
- The shard of a key is taken from a stirred hash: neighbouring bits of
  the hash proved alike for prefixes in a row. The test that showed it,
  failing one run in sixteen, is deterministic now.

The lab has a phase, hijacks, in which FRR tries the first three.
route::apply is a struct with methods instead of nested closures, and
the kernel task's loop is shorter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-02 19:13:40 +02:00
1 parent fd2248feb5
commit 9ca86ede2f
34 files changed
+1696 -470

No files matched your search

+32 -3
View File
@@ -57,13 +57,30 @@ table (`import` in a peer's configuration; see `config.proto`):
- Refused for any peer unless turned off: prefixes that are not routed on
the Internet (private, documentation, multicast, reserved), prefixes longer
than /24 or /48, a default route, paths with a private or reserved AS
number, and paths that do not begin with the peer's own AS.
- Always refused: a path with our own AS number in it.
number, paths that do not begin with the peer's own AS, and paths with an
AS_SET in them (RFC 9774).
- The next hop has to be the peer itself (`accept_third_party_next_hop`
turns that off; a peer whose role is `ROUTE_SERVER` is exempt). A next
hop is where our traffic will be sent: a peer may draw it to itself, not
point it at another member of an exchange or at a machine on some other
network of ours.
- The prefixes this router originates, and anything inside them, are not
taken from another network, unless the peer's `prefixes` list names them
(a customer with part of our space).
- Always refused: a path with our own AS number in it, and a next hop that
is no address to send to (unspecified, loopback, multicast, our own).
- If configured: a list of the prefixes the peer may announce and of the AS
numbers its routes may originate from.
- `max_prefixes`: one route more ends the session with Cease / Maximum
Number of Prefixes Reached and keeps the peer down for ten minutes.
A configured peer is one router: a second connection from its address
under another BGP Identifier is refused, so the limits above are not
multiplied by opening more sessions. And a peer whose sessions keep coming
up and failing within a minute is kept down, from the third time for a
second and for twice as long each time after, up to five minutes: every
such session costs a look through the whole table.
A refused route is dropped, along with the route it would have replaced.
`bgp_routes_rejected_total` counts them by reason, and the status page shows
how many routes each peer has and how many were refused.
@@ -254,6 +271,12 @@ kernel {
its own: it does not use anyone else's, puts its own back if one is
removed, and removes them when it stops. `without_nexthop_objects: true`
has every route carry its own gateway instead, as on older kernels.
- **What is the machine's own.** A route for a network the machine is
itself on, or for part of one, is never installed, whatever the policy
let into the table: the kernel reaches those directly. A peer on a shared
LAN that announces part of the LAN would otherwise take the traffic to
its neighbors, and our sessions with them. The status page counts such
routes as "inside a network of this machine".
- **What is ours.** Routes are marked `proto bgp`. Every route with that
mark in the configured table with the configured metric is taken to be
this daemon's, and no other route is ever changed or removed. So do not
@@ -266,8 +289,13 @@ kernel {
SIGTERM the daemon closes its sessions and then removes its routes. After
a crash they stay until the next start.
- **Interfaces.** The daemon learns the machine's interfaces and addresses
from the kernel and is told when they change; a second later it works all
from the kernel and is told when they change; a second after the last
change, and five seconds after the first at the latest, it works all
routes out again, since any next hop may resolve differently.
- **When the kernel does not answer.** The kernel says which requests it
refused. If it had more to say than there was room for, or says nothing
for ten seconds, the daemon asks it what it has of our routes and puts
right what is not as it should be.
- **Privileges.** Changing routes needs the `CAP_NET_ADMIN` capability, and
listening on port 179 needs `CAP_NET_BIND_SERVICE`; neither needs root.
Without the first the daemon stops as soon as the kernel refuses it.
@@ -327,6 +355,7 @@ Routinator 0.15.2. What it covers:
| `restart` | The daemon stopped in good order (kernel emptied, peers told) and killed (routes kept, stale one cleared by the next start) |
| `scale` | A table from BIRD (`SCALE_ROUTES`, a million for the Internet's size) into the table and the kernel; the daemon killed and restarted under it; the table withdrawn |
| `limits` | A peer whose role does not fit gets no session; a peer over its prefix limit gets Cease / Maximum Number of Prefixes and is held down |
| `hijacks` | FRR announces our own prefix, a route whose next hop is BIRD, and the exchange LAN's own prefix: the first two are refused, the third stays out of the kernel |
| `v6only` | The exchange LAN without IPv4 addresses: one IPv6 session with each router for both families, IPv4 routes with IPv6 next hops (RFC 8950) in BGP and in the kernel, IPv4 pings across |
| `linklocal` | The exchange LAN with link-local addresses only, and the same ones on a second interface: sessions with FRR (by interface, with radvd beside the daemon) and BIRD at `fe80::…%ix0`, both families in BGP and in the kernel, pings across, the interface taken down and up |