Harden import, the kernel task and the RTR client after review

Three independent reviews (hijack resistance, reliability, code health)
of the whole tree. This is what they found that could let a bad route in
or take the daemon down.

Hijacks:
- A route had two paths on a two-octet session, AS_PATH and AS4_PATH,
  read in two places by two rules. A peer could make the origin checked
  on import differ from the one checked at revalidation, and get an
  RPKI-invalid route into the table. AS4_PATH is now merged into AS_PATH
  once, on receipt (RFC 6793 §4.2.3), and nothing else knows of it.
- The next hop is judged on import: it must be usable, agreed on
  (RFC 8950), and by default the peer itself.
- Our own originated prefixes are not taken from other networks.
- A route for a network the machine is on is never put in the kernel.
- Paths with an AS_SET are refused (RFC 9774).
- A configured peer gets one session, not one per BGP Identifier.
- More IPv6 special-purpose prefixes are bogons.

Reliability:
- A batch of requests the kernel refused could overflow the netlink
  socket and end the daemon. The socket has room now, a lost answer or a
  silent kernel means asking what is there, and neither is fatal.
- Working the routes out again within the grace period after a restart
  no longer drops the routes being kept.
- That work can no longer be put off for ever by interface changes.
- Peers whose sessions keep failing are slowed down.
- Unused attribute sets are swept when they pile up, not only each
  minute.
- Log lines a peer can cause are said once in a while.
- An RTR answer is bounded in size and time; data cannot expire between
  two refreshes.
- The shard of a key is taken from a stirred hash: neighbouring bits of
  the hash proved alike for prefixes in a row. The test that showed it,
  failing one run in sixteen, is deterministic now.

The lab has a phase, hijacks, in which FRR tries the first three.
route::apply is a struct with methods instead of nested closures, and
the kernel task's loop is shorter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-02 19:13:40 +02:00
1 parent fd2248feb5
commit 9ca86ede2f
34 files changed
+1696 -470

No files matched your search

+35 -16
View File
@@ -1,5 +1,6 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use bgp_fib::FibConfig;
@@ -150,6 +151,17 @@ impl Config {
}
originate.push(route);
}
// What we originate is not taken from other networks.
let mut own_prefixes = PrefixSet::default();
for route in &originate {
own_prefixes.allow(route.prefix, route.prefix.address_family().bits());
}
let own_prefixes = Arc::new(own_prefixes);
for peer in &mut peers {
if peer.remote_asn != proto.local_asn {
peer.import.own_prefixes = Arc::clone(&own_prefixes);
}
}
let http_endpoint = match proto.http_endpoint.into_option() {
Some(ep) if ep.port == 0 => return Err(ConfigError::HttpPort),
@@ -179,24 +191,28 @@ 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),
nexthop_objects: !kernel.without_nexthop_objects,
stale_time: match kernel.stale_seconds {
0 => defaults.stale_time,
seconds => Duration::from_secs(seconds.into()),
},
}
}),
kernel: proto.kernel.as_ref().map(parse_kernel),
rpki,
})
}
}
fn parse_kernel(kernel: &proto::Kernel) -> FibConfig {
let defaults = FibConfig::default();
FibConfig {
// The kernel has no table 0 to put routes in.
table: match kernel.table {
0 => defaults.table,
table => table,
},
metric: kernel.metric.unwrap_or(defaults.metric),
nexthop_objects: !kernel.without_nexthop_objects,
stale_time: kernel.stale_seconds.map_or(defaults.stale_time, |seconds| {
Duration::from_secs(seconds.into())
}),
}
}
fn parse_peer(peer: Peer) -> Result<PeerConfig, PeerProblem> {
let ep = peer.endpoint.as_ref().ok_or(PeerProblem::MissingEndpoint)?;
let endpoint = parse_port(ep.port)
@@ -208,10 +224,10 @@ fn parse_peer(peer: Peer) -> Result<PeerConfig, PeerProblem> {
}
let hold_time_secs = match peer.hold_time_seconds {
0 => DEFAULT_HOLD_TIME_SECS,
None => DEFAULT_HOLD_TIME_SECS,
// RFC 4271 §4.2: zero, or at least three seconds.
1 | 2 => return Err(PeerProblem::HoldTime),
secs => u16::try_from(secs).map_err(|_| PeerProblem::HoldTime)?,
Some(1 | 2) => return Err(PeerProblem::HoldTime),
Some(secs) => u16::try_from(secs).map_err(|_| PeerProblem::HoldTime)?,
};
let local_address = if peer.local_address.is_empty() {
@@ -367,6 +383,9 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
accept_any_first_as: import.accept_any_first_as,
accept_rpki_invalid: import.accept_rpki_invalid,
accept_aspa_invalid: import.accept_aspa_invalid,
accept_as_sets: import.accept_as_sets,
accept_third_party_next_hop: import.accept_third_party_next_hop,
own_prefixes: Arc::default(),
max_as_path_length: match import.max_as_path_length {
0 => defaults.max_as_path_length,
length => u16::try_from(length).map_err(|_| ImportProblem::PathLength(length))?,