Rayhaan JaufeerallyandClaude Fable 5.1 6d8f89e509
ci / check (push) Canceled after 0s
ci / docs (push) Canceled after 0s
ci / interop (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s
policy, fib: blackhole routing for routes with the BLACKHOLE community
Before this change, a route carrying the BLACKHOLE community (65535:666,
RFC 7999) from an EBGP neighbor was always rejected with the reason
"blackhole request", and nothing discarded traffic. A customer under a
denial-of-service attack had no way to ask this router to drop traffic
to an address.

This change adds `accept_blackholes` to the import policy. When it is
set, a route with the BLACKHOLE community is accepted if:

* its prefix is a single address, a /32 or a /128 (otherwise it is
  rejected with the new reason "blackhole for more than one address");
  the `max_ipv4_prefix_length` and `max_ipv6_prefix_length` limits do
  not apply, as RFC 7999 section 3.2 recommends;
* the address is inside a prefix the neighbor may announce, if
  `prefixes` is configured, regardless of the entry's `max_length`
  (new `PrefixSet::covers`); without a prefix list, an address inside
  the local AS's own prefixes is rejected as before ("prefix is our
  own");
* the AS path, origin and RPKI checks that apply to every route pass.

The next hop of an accepted blackhole route is not checked for
third-party addresses and is not resolved, because traffic is never
forwarded to it. The route takes part in best path selection as usual;
a host route is normally the active route for its address.

The kernel task installs the active route as a kernel blackhole route
(`RTN_BLACKHOLE`, shown by `ip route` as `blackhole PREFIX proto bgp`).
Blackhole routes left by an earlier run are adopted at startup like
other routes of ours. The route is advertised to other neighbors as the
export policy and export limits permit, with the community intact, so a
provider that honors BLACKHOLE can discard the traffic upstream.

Implementation:

* `plan::Target` becomes an enum: `Via { gateway, interface }` or
  `Discard`. `plan::resolve` takes a `blackhole` flag and returns
  `Discard` for such routes without looking at the next hop (the
  own-network check still applies). No nexthop object is made for a
  discard target.
* `AttrSet::is_blackhole` reads the community off the stored attribute
  bytes, like `next_hop` and `as_path`, so the kernel task, the
  revalidator and the RPKI revalidation can ask it of every route.
* `ImportPolicy::is_blackhole_request` and `check_blackhole_prefix`
  carry the import side; the import flow (`routes.rs`) computes once per
  UPDATE whether its routes are an accepted blackhole request and skips
  the next hop checks for them.
* `Status::blackholes` and the `blackholes` field of `/api/kernel` count
  the installed blackhole routes; the status page shows the count next
  to the installed routes.

Not covered: a prefix this router originates with the BLACKHOLE
community is advertised but not installed as a blackhole route. The
documentation says to add a kernel blackhole route by hand for that
case.

Documentation: the "Blackhole Routes" section of the import policy
guide replaces "Blackhole Requests", the kernel guide gains a "Blackhole
Routes" section, the configuration reference describes
`accept_blackholes`, the HTTP API reference describes `blackholes`, and
the RFC 7999 rows of the standards reference are updated.

Testing: unit tests for the attribute check, the prefix checks and the
plan; integration tests that a blackhole request is refused by default
and, with `accept_blackholes`, taken for one address inside the peer's
space and refused otherwise, with the recorded reasons; a kernel test
that a blackhole request becomes a kernel blackhole route whatever its
next hop, is removed on withdrawal and is adopted after a restart. The
existing kernel tests, the interoperability lab and the workspace tests
pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 23:08:50 +02:00
2026-10-02 22:47:57 +02:00
2026-10-02 22:47:57 +02:00
2026-10-06 00:06:06 +02:00

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 the x86_64-unknown-linux-musl target 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:

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_endpoint to 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. unsafe code 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.

S
Description
Ferrix BGP Speaker
Readme
5 MiB
0 Stars 0 Watchers 0 Forks
Languages
Rust 92.3%
Shell 4.2%
HTML 3.5%