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>
BGP
This crate provides the bgp binary, which runs the Border Gateway Protocol
daemon together with its read-only HTTP status interface.
cargo run -- path/to/config.textpb
The configuration format is defined in crates/server/proto/config.proto;
testdata/server.textpb is a worked example. The daemon is configured
entirely through that file: peers, and the routes it originates
(originate { prefix: ... }). There is no way to change either at runtime.
When http_endpoint is set the binary also serves:
| Path | Purpose |
|---|---|
GET /statusz |
Status page: configuration, peers and session state, originated and received routes, live event log |
GET /api/status |
Router ID, ASN, uptime and table sizes |
GET /api/config |
The configuration file as read at startup |
GET /api/peers |
Configured peers and established sessions |
GET /api/routes/local |
Originated routes |
GET /api/routes/received |
Adj-RIB-In contents |
GET /api/events |
Server-sent events for session and route changes |
GET /metricz |
Prometheus metrics |
Everything under it is read-only and unauthenticated. Bind it to loopback or put it behind a reverse proxy.
What it does today
It establishes sessions (both dialling configured peers and accepting connections, with RFC 4271 collision detection), announces its originated routes to every peer with itself as next hop, and records what peers send in an Adj-RIB-In. It does not select best paths, re-advertise received routes or program a forwarding table, and it has no import or export policy.
Trying it without a router
fake_peer is a minimal BGP speaker that connects to the daemon, announces a
prefix, prints every UPDATE it receives and, with --echo, re-announces each
received prefix back with its own ASN prepended (dropping NO_EXPORT routes).
That makes originated prefixes show up in the received table as if they had
travelled through a neighbouring AS:
cargo run -- testdata/server.textpb
cargo run -p bgp-integration-tests --example fake_peer -- \
--server 127.0.0.1:179 --asn 65001 --announce 203.0.113.0/24 --echo
Please refer to the other implementation crates in this repository where most of the actual implementation lives.
Warning: This is still very much a work in progress and is not yet ready for production use.