Commit Graph
8 Commits
Author SHA1 Message Date
Rayhaan JaufeerallyandClaude Fable 5.1 9caaf5018e Review pass on the server: bugs fixed, session setup simplified
ci / check (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s
A review of the session driver, the server loop, the dialer, the
advertiser and the HTTP API, with the fixes.

Bugs:

- A peer that stopped reading held its session, and our routes, for as
  long as TCP kept retrying: a blocked write was awaited with no timer
  running. Writes now give up after the Send Hold Time of RFC 9687.
- Routes we originate were sent to a peer in our own AS with our AS
  number on the path and no LOCAL_PREF, which such a peer refuses. They
  now go with an empty path and a LOCAL_PREF.
- Collision detection knew a peer by its BGP Identifier alone. A
  neighbour with one session over IPv4 and one over IPv6 had the second
  closed as a collision, and any host that could connect could displace
  a peer's connection by claiming its identifier. A peer is now its
  address and identifier together.
- The dialer reset its backoff whenever TCP connected, so a peer that
  accepts the connection and refuses the session was dialled every few
  seconds for ever. It resets only after a session was established.
- A failure on our side (routes that cannot be stored) dropped the
  connection without a NOTIFICATION. It goes through the state machine
  and the peer is sent Cease. A queued message that cannot be encoded
  is logged and skipped instead of ending the session.
- The accept loop retried at once after an error, spinning when the
  process was out of file descriptors.
- Attribute sets no route used any more were dropped only when a peer
  went away. A task now does it every minute.
- The advertiser subscribed to session events inside its task, after the
  dialers were started, and could miss the first session.
- Shutdown waited without limit on a session whose queue was full.
- /api/routes/received took any limit and built the reply on a runtime
  thread. The limit is capped and the work is done off those threads.
- The default log level was TRACE, which logs every UPDATE.
- IPv6 listeners are IPv6-only: listening on 0.0.0.0 and [::] fails on
  systems where the second would take IPv4 too, and IPv4 peers arriving
  under mapped addresses matched no configured peer.
- A connection closed after a NOTIFICATION is kept for a moment so that
  the peer can read it.
- The configuration accepted two peers with one name or one address,
  and a router id of 0.0.0.0.
- An HTTP address that cannot be bound now stops the daemon at startup.

Simplifications:

- SessionConfig mixed what a server's sessions share with what is
  configured for one peer, and was cloned and patched field by field in
  two places. It is ServerContext, shared, and PeerSettings, made from a
  PeerConfig in one place.
- The attributes of an UPDATE as they were on the wire travelled from
  the reader to the table through a field of the session, around the
  state machine. They go through it, with the message.
- run_session returns how the session ended and whether it had been
  established; Ok no longer reads as "closed by peer".
- Session is not generic over its reader.
- BgpServerBuilder had nothing to build: BgpServer::bind takes the
  configuration.
- One shutdown path, close_sessions, for the signal handler and for a
  failed server.
- The advertiser's three copies of "send every route" are one.
- PeerId moved from the rib crate, which did not use it, to the server.
- Unused API removed or made private to the crate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 10:46:09 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 d49bab58a5 Add ADD-PATH (RFC 7911): several routes for a prefix from one peer
A peer can now send several routes for one prefix, told apart by a path
identifier, and each is a route of its own in the table.

Wire:
- The ADD-PATH capability lists several address families. It was read as
  exactly one, so the OPEN of a router offering it for IPv4 and IPv6 was
  malformed and the session never came up.
- Prefixes in UPDATEs are `Nlri`: a prefix and a path identifier. The
  identifier is on the wire, in the body and in MP_REACH/MP_UNREACH, for
  the families the session uses ADD-PATH for in that direction. Sending
  and receiving are settled separately, so a session has one codec context
  for each.

Negotiation:
- `add_path` on a peer in the configuration: off (the default), receive,
  send or both, offered for every family offered to the peer.
- In use for a family and a direction when one side offered to send and
  the other to receive. Shown on /statusz and in /api/peers.

Table:
- A route is known by its peer and path identifier together. Identifiers
  are stored only in rows that have a non-zero one, so routes of peers
  that send one route each cost nothing more.
- Equal routes from one peer are chosen between by the lower identifier.
- /api/routes/received gives each route's path_id.

Journal:
- A second journal in each shard records every change to any route, for
  followers that want all the routes of a prefix and not only the best:
  RibTable::reader_of_all_routes and Event::Routes. This is what sending
  several routes to a peer will read. It costs nothing while nobody
  follows it.

Sending: prefixes go out with a path identifier where that was agreed, but
there is still one route for each prefix to send: only originated routes
are announced.

Tested with the table and journal model tests extended to path identifiers
and to followers of all routes, the codec property tests and fuzz targets
extended to ADD-PATH sessions, and end to end in both directions and for
the case where only one side offers it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 00:57:15 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 b0762874e0 Make address families open values; keep "supported" a separate list
AFI and SAFI were closed enums, so a family without a variant could not be
represented at all, and the set of negotiated families was a struct with
one bool per family. Supporting another family meant touching both.

- `Afi`, `Safi` and `AfiSafi` in bgp-primitives are newtypes over the wire
  numbers, with constants and names for the common families. Capabilities
  and MP_REACH/MP_UNREACH use them, so any family a peer names is read as
  what it is. The special case that turned a capability for an unlisted
  family into an unknown capability is gone.
- `FamilySet` is a set of `AfiSafi`. What we can exchange routes for is the
  explicit `family::SUPPORTED` list; only those are offered in our OPEN.
- `AddressFamilyId` stays as the IP version of a prefix.
- NLRI is still a list of IP prefixes. MP_REACH/MP_UNREACH for a family
  whose NLRI is something else (labelled, VPN, EVPN, ...) is refused
  rather than misread as prefixes, which is what happened before for the
  SAFIs the old enum listed.

Bug found on the way: the reserved octet in MP_REACH_NLRI was skipped only
for IPv6, so an IPv4 MP_REACH_NLRI was parsed with an extra 0.0.0.0/0
route that the peer never sent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-01 22:38:40 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 22d0099a46 Negotiate address families and AS number width instead of assuming them
The OPEN used to offer IPv4 and IPv6 unicast unconditionally, and what the
peer offered was ignored apart from its four-octet AS number.

Address families (RFC 4760):
- A peer's `address_families` in the configuration says which to offer it;
  all of them by default.
- A session carries the families both OPENs offered. A peer that sends no
  Multiprotocol capability is a plain RFC 4271 speaker: IPv4 unicast.
- Routes of a family that was not negotiated are not sent, and are ignored
  when received; the rest of the UPDATE is applied and the session stays up.
- A session with no family in common is refused with Unsupported
  Capability, naming the capabilities we needed (RFC 5492).
- The negotiated families are shown on /statusz and in /api/peers.

Bugs found on the way:
- A capability naming an address family we have no constant for (EVPN,
  VPNv4, ...) failed the whole OPEN, so such a peer could never establish.
  It is now kept as an unknown capability and ignored.
- An UPDATE could not be encoded for a peer without four-octet AS support,
  which ended the session on the first route. AS_PATH is now written two
  octets wide with AS_TRANS where needed, and AS4_PATH carries the real
  numbers (RFC 6793).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-01 22:22:54 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 2ab6c1c38b Format the workspace with rustfmt
Default settings. No change in behaviour; the tree was last formatted
before the lecture work and had drifted to a mix of line widths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-01 20:58:12 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 b800e3a322 Originate routes from the configuration; make the HTTP API read-only
Routes were announced and withdrawn through an HTTP form, each tagged with a
free-text owner and checked against an allow-list and a per-owner limit.
That was the lab's interface, not a router's.

Routes now come from 'originate' entries in the configuration file and are
in the local RIB before any session starts. The local RIB holds one route
per prefix with no notion of owners or policy. local_route_policy is gone
from the configuration.

The HTTP API keeps its GET endpoints (status, config, peers, originated and
received routes, events, metrics). The announce, withdraw, wire-bytes and
well-known-community endpoints are removed, and the status page loses the
announce form, the community presets and the byte decoder. Community text
parsing moves from the web crate to bgp-packet so the configuration can use
it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-01 20:51:15 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 fd496d26bf feature: run on the router itself: protoc-free build, per-peer source address
Preparing to run ferrix next to BIRD on the production router.

- build: the config proto is now parsed in pure Rust, so building needs no
  `protoc` binary. It previously worked only where one happened to be
  installed; the Dockerfile in this repo could not have built.
- peers gain `local_address`, the source IP for connections we open (and
  therefore the next hop we advertise). When both speakers share a host
  the OS would otherwise pick an arbitrary local address and the peer,
  which identifies us by address, would refuse the session. Address-family
  mismatches are rejected at config load; an address the host lacks shows
  up as the peer's backoff reason.
- docs/lab/prod/on-router: static musl cross-build instructions, config,
  hardened systemd unit (unprivileged, SIGTERM gives BIRD a Cease),
  loopback addresses for networkd, the BIRD stanza for a same-host
  multihop session, and reverse-proxy examples with TLS and a password,
  since the API has no authentication.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 18:15:09 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 e2de44ba78 feature: active dialer for configured peers
Phase 4 of the teaching demo: the daemon now brings up its own sessions.

- New dialer task per configured peer: connect with a timeout, run the
  same FSM as the passive side, back off exponentially (from
  connect_retry_seconds, capped at two minutes) and retry forever. Stays
  idle while a session with the same remote IP and ASN already exists.
- The peer's OPEN must carry the configured ASN, otherwise the session is
  refused with a Bad Peer AS notification. Inbound connections from a
  configured peer's address get the same check plus the configured name
  and hold time.
- SessionRegistry tracks configured peers and their dial state
  (Idle / Connecting / Backoff with last error and retry time).
- /api/peers merges configured peers with live sessions so the page shows
  peers that are down, why, and when the next attempt is; unconfigured
  inbound sessions are listed after them.
- fake_peer gains --listen so it can stand in for a router the daemon
  dials.
- Integration tests: dial and establish, reconnect after the peer closes,
  wrong ASN refused, inbound from a configured address checked and named.

Not implemented: RFC 4271 §6.8 collision detection; a real router on the
other end resolves the rare race by closing one connection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 23:22:08 +02:00