A table given out anew to a session, at its start or when the journal had let changes go, was gathered as a verdict for each prefix, each with the route dressed for the session, and sent once the table was all there. Dressing a route parses its attributes and encodes them for the session, so a table of five million routes took seconds before the first UPDATE went, on one worker thread without a yield. And the routes that share attributes went out in separate UPDATEs after all: they were grouped by the dressed form, of which only so many are kept, and the routes of a set are spread over the whole table, so by the time the next was reached the form had been let go of and made anew. The mesh benchmark's new peer got four million routes in 3.8 million UPDATEs; on a RIPE RIS table, grouped whole, a full table is 160,000 UPDATEs and 21 MB, and grouped in slices of 4,096 routes it is 845,000 UPDATEs and 72 MB, so slices are not the answer either. The table is now gathered by what dresses its routes: the attribute set as the table has it, who the route is from, and its IP version. Nothing is dressed while gathering, which is a map insert a route. Once the table is all there, each set is dressed once, with the thread lent to the other tasks every few thousand, and then the sets go out one after the other, each as few UPDATEs as its routes fit in, with the peer reading the first while the rest are sent. A prefix heard of again while the table is read supersedes what was heard before: each word on a prefix is numbered, and only the latest goes. Path identifiers are given at gathering, in the order the table has a prefix's routes, so that they are as before. What was announced before and not heard of again is withdrawn after the table, as before. The exporter never blocks the thread on a table: the steady state's batch is small, and the gathered table is gone through with yields. Testing: a new test gives a session a table of 300 routes sharing three sets of attributes, spread over the shards, and checks they go in three UPDATEs. The mesh benchmark has a new phase in which a peer takes the first router's table once the mesh is complete, timing the first UPDATE and the whole; its feeders now negotiate no hold timer, since they say nothing after their tables, and `BENCH_LOG` prints the routers' logs. A new example, packing, counts the UPDATEs and octets a RIS table takes grouped whole and in slices. The server tests, the integration tests, the kernel tests and every lab phase pass. In the Linux VM, three rounds alternating with the previous commit, the new peer's first UPDATE comes 2.9 s after its session is up instead of 5.3 s, and it has all four million routes in 4.0 s instead of 8.0 s, in 667,000 UPDATEs instead of 3.76 million; the whole run takes 22.4 s of CPU time instead of 26.2 s. The routers' exchange of their tables, which does not go through this path once their sessions are up, is unchanged within its noise. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ferrix BGP
Ferrix is a BGP daemon for Linux routers, written in Rust. It exchanges routes with neighbors, filters them by policy and the RPKI, selects the best path for each prefix, installs it in the kernel, and passes routes on as BGP roles and communities permit. It aims to be small, safe, observable, and configured from one file that can be read by someone who did not write it.
Status
Ferrix routes the author's network, AS210036: three routers carrying full
tables from transit providers, route servers at several Internet exchanges,
and bilateral peers, with routes reflected between the routers over iBGP.
It is young software all the same. Read the limitations
before relying on it, and treat version 0.x as meaning that configuration
and APIs may still change between releases.
What it does:
- BGP-4 over IPv4 and IPv6, with four-octet AS numbers, multiprotocol reach for both families, IPv4 routes with IPv6 next hops (RFC 8950), route refresh, graceful restart and ADD-PATH receive.
- Import policy per neighbor: prefix limits, prefix and origin AS lists, bogon checks, AS path checks, and communities added on the way in.
- Route origin validation and ASPA path verification against an RTR cache.
- BGP roles (RFC 9234) and the Only to Customer attribute, with export limits as a second line of defence against route leaks.
- Best path selection and installation of the result in the Linux kernel over netlink, in a routing table of your choice.
- Route reflection between routers of the same AS, and passive neighbors.
- A status page, a JSON API, server-sent events, and Prometheus metrics.
- Graceful restart of the daemon itself for upgrades, with routes left in the kernel while it is away.
Not supported
- Per-client best path selection for route servers, and ADD-PATH for advertised routes
- TCP-AO, TCP MD5, and GTSM
- Long-Lived Graceful Restart, BFD, and configuration changes without a restart
- Address families other than IPv4 and IPv6 unicast
Building
You need a stable Rust toolchain from rustup.rs, and a
C compiler and make for the jemalloc allocator the daemon uses (for
example build-essential on Debian and Ubuntu, base-devel on Arch Linux,
or the Xcode command line tools on macOS). The Protocol Buffers compiler is
not needed: the configuration schema is compiled in pure Rust.
git clone https://git.rayhaan.net/rayhaan/ferrix.git
cd ferrix
cargo build --release --locked -p bgp
The binary is target/release/bgp. --locked builds with the dependency
versions recorded in Cargo.lock, which are the versions CI tested.
To try it without touching any router, run the quick-start configuration. It listens on loopback, needs no privileges, and serves the status page at http://127.0.0.1:8080; the quick start guide adds a test peer that announces routes to it.
target/release/bgp --check docs/examples/quick-start.textpb
target/release/bgp docs/examples/quick-start.textpb
Other ways to build:
- A static Linux executable that runs on any x86_64 distribution with
nothing else installed:
scripts/build-static.sh. It needs thex86_64-unknown-linux-musltarget and a musl C compiler; the script's header says how to install them, including on macOS. - A container image with the provided
Dockerfile:docker build -t ferrix .
Kernel route installation needs Linux and CAP_NET_ADMIN; everywhere else
Ferrix is a BGP speaker only. The installation
guide and the
deployment guide cover
privileges, the systemd unit, and upgrading without withdrawing routes.
Documentation
The documentation is a website built from docs/ with
Zola: zola --root docs serve, then open
http://127.0.0.1:1111. The sources are Markdown with Zola front matter,
shortcodes and internal links, so the forge shows them with those parts
unrendered; docs/README.md explains what each is. The
pages:
- Overview, installation and quick start
- User guide: BGP neighbors, import policy, BGP roles, RPKI, kernel route installation, route advertisement, graceful restart, configuration examples
- Deployment and monitoring
- Reference: configuration, command line, HTTP API, metrics, supported standards
- Architecture, testing and contributing
Complete configurations for an edge router, an exchange router and a
transit customer are in docs/examples/; CI checks that
each of them loads.
Security
- The status page and HTTP API are not authenticated and show the whole
configuration and routing table. Bind
http_endpointto a loopback address, or put it behind a proxy that authenticates. - Ferrix does not implement TCP-MD5 or TCP-AO. Filter TCP port 179 in the host firewall so that only configured neighbors can connect.
- Everything read from the network, the RPKI cache, and the kernel is
parsed without panicking; each parser has property tests and a fuzz
target.
unsafecode is forbidden throughout.
If you find a vulnerability, please contact the maintainer directly rather than opening a public issue.
Layout
| Crate | What is in it |
|---|---|
crates/primitives |
Prefixes, address families, parsing helpers |
crates/packet |
The wire format: messages, attributes, capabilities |
crates/rib |
The routing table, best route selection, the journal of changes |
crates/rpki |
The RTR client; origin validation and ASPA path verification |
crates/fib |
The best routes in the Linux kernel, over netlink |
crates/server |
Sessions, the state machine, policy, configuration |
crates/web |
The status page, HTTP API and metrics |
crates/integration-tests |
A test server and test peers, and the tests that run the daemon end to end |
crates/bench |
Benchmarks at the scale of the Internet routing table |
bin/bgp |
The daemon |
Development
CI runs these on every push, and fails on any warning:
cargo fmt --all -- --check
cargo clippy --locked --workspace --all-targets -- -D warnings
cargo test --locked --workspace
RUSTDOCFLAGS="-D warnings" cargo doc --locked --workspace --no-deps --document-private-items
zola --root docs build
It also runs the tests that need a real Linux kernel
(scripts/kernel-tests.sh) and a lab that peers the daemon with FRR, BIRD
and Routinator (scripts/interop/lab.sh); scripts/linux-vm.sh runs
either from macOS in a Lima VM.
cargo test includes property tests: generated messages must survive being
written and read back, and no input, however damaged, may make a parser
panic. They take about a second; for a longer hunt:
PROPTEST_CASES=1000000 cargo test --release -p bgp-packet --test properties
Coverage-guided fuzz targets for the same parsers are in fuzz/; see
fuzz/README.md. CI runs each of them for a minute.
Development with AI agents
Much of Ferrix is written with an AI coding agent (Claude Code) working
alongside the maintainer, who directs the work, reviews every change, and
runs the result on real routers. Nothing is merged on the agent's say-so:
every change passes the checks above, the kernel tests and the
interoperability lab, and the changes that matter are tried on the network
first. Commits made with an agent carry a Co-Authored-By trailer that
says so. If you contribute, you are welcome to work the same way, or not;
what is reviewed is the change, not how it was produced.
License
Apache License 2.0; see LICENSE.