Install best routes in the Linux kernel over netlink

New crate bgp-fib. It follows the routing table's best routes and keeps
the kernel's routing table the same, and learns the machine's interfaces
and addresses from the kernel, which is what says whether a next hop can
be reached.

- netlink: the rtnetlink messages used (routes, addresses, links, errors),
  as bytes and back. No I/O; property tested and fuzzed.
- interfaces: what is directly reachable, and from which interface.
- plan: what to ask the kernel for, given the best routes and what the
  kernel has. No I/O; tested against a model kernel.
- kernel (Linux only): the sockets and the task. Requests go in batches;
  routes of ours found at startup are kept if still right and removed if
  not; a change of interfaces has every route worked out again; a clean
  stop removes our routes.

Only routes marked proto bgp with the configured table and metric are
ever touched. A best route is installed if its next hop is a neighbor;
next hops further away are not resolved through other routes yet, and
such prefixes are counted and shown instead.

Configured with `kernel { table metric }`; off by default. The daemon
needs CAP_NET_ADMIN and stops if the kernel refuses it. On shutdown it
closes sessions and then removes its routes.

/api/kernel and the status page show installed, unresolved and refused
counts, the last error and the interfaces; five bgp_kernel_* metrics.

Testing: scripts/kernel-tests.sh runs, in network namespaces of their
own and with only the capabilities the daemon needs, the installer
against `ip route`, the daemon with a peer, and two daemons that route
for each other with pings across over IPv4 and IPv6.
scripts/linux-vm.sh runs that, or anything else, in a Lima VM from
macOS. CI runs the kernel tests and fuzzes the netlink decoder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-02 15:15:43 +02:00
1 parent 1f84235e87
commit 1fc44798b7
38 files changed
+3745 -16

No files matched your search

+32
View File
@@ -2,6 +2,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::str::FromStr;
use std::time::Duration;
use bgp_fib::FibConfig;
use bgp_packet::community;
use bgp_packet::message::OriginPathAttribute;
use bgp_packet::open::AddPathSendRecv;
@@ -43,6 +44,8 @@ pub struct Config {
pub originate: Vec<LocalRoute>,
/// Address for the HTTP API; `None` disables it.
pub http_endpoint: Option<SocketAddr>,
/// Where to install the best routes in the kernel, if at all.
pub kernel: Option<FibConfig>,
/// Where route origin validation gets its data; `None` for no
/// validation.
pub rpki: Option<RpkiConfig>,
@@ -174,6 +177,14 @@ impl Config {
accept_unconfigured_peers: proto.accept_unconfigured_peers,
originate,
http_endpoint,
kernel: proto.kernel.as_ref().map(|kernel| {
let defaults = FibConfig::default();
let or = |value: u32, default: u32| if value == 0 { default } else { value };
FibConfig {
table: or(kernel.table, defaults.table),
metric: or(kernel.metric, defaults.metric),
}
}),
rpki,
})
}
@@ -725,6 +736,27 @@ mod tests {
);
}
#[test]
fn the_kernel_is_left_alone_unless_asked_for() {
assert_eq!(parse(BASE).unwrap().kernel, None);
assert_eq!(
parse(&format!("{BASE}kernel {{}}")).unwrap().kernel,
Some(FibConfig {
table: 254,
metric: 20
})
);
assert_eq!(
parse(&format!("{BASE}kernel {{ table: 1000 metric: 150 }}"))
.unwrap()
.kernel,
Some(FibConfig {
table: 1000,
metric: 150
})
);
}
#[test]
fn route_origin_validation_is_off_unless_caches_are_given() {
assert!(parse(BASE).unwrap().rpki.is_none());