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>
241 lines
7.9 KiB
Rust
241 lines
7.9 KiB
Rust
//! The machine's interfaces and their addresses, as the kernel reports
|
|
//! them, and what they say about a next hop: whether it can be reached
|
|
//! directly, and from which interface.
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::net::IpAddr;
|
|
|
|
use bgp_primitives::constants::IpVersion;
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
|
|
use crate::netlink::{Address, Link, Message};
|
|
|
|
/// The interface a neighbor is reached on.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct Connected {
|
|
/// The interface's index.
|
|
pub interface: u32,
|
|
/// Our own address in the network the neighbor is in.
|
|
pub local: IpAddr,
|
|
}
|
|
|
|
/// The interfaces there are and the addresses they have.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct Interfaces {
|
|
links: HashMap<u32, Link>,
|
|
addresses: HashSet<Address>,
|
|
}
|
|
|
|
/// `address` as the prefix that is only it.
|
|
fn host(address: IpAddr) -> IpPrefix {
|
|
match address {
|
|
IpAddr::V4(v4) => IpPrefix::new(IpVersion::Ipv4, v4.octets(), 32),
|
|
IpAddr::V6(v6) => IpPrefix::new(IpVersion::Ipv6, v6.octets(), 128),
|
|
}
|
|
.expect("an address is a prefix of its own length")
|
|
}
|
|
|
|
impl Interfaces {
|
|
/// Takes in what a message from the kernel says about links and
|
|
/// addresses. Returns whether anything is different for it.
|
|
pub fn apply(&mut self, message: &Message) -> bool {
|
|
match message {
|
|
Message::Link {
|
|
present: true,
|
|
link,
|
|
} => self.links.insert(link.index, link.clone()).as_ref() != Some(link),
|
|
Message::Link {
|
|
present: false,
|
|
link,
|
|
} => {
|
|
self.addresses
|
|
.retain(|address| address.interface != link.index);
|
|
self.links.remove(&link.index).is_some()
|
|
}
|
|
Message::Address {
|
|
present: true,
|
|
address,
|
|
} => self.addresses.insert(*address),
|
|
Message::Address {
|
|
present: false,
|
|
address,
|
|
} => self.addresses.remove(address),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// The interfaces, by index.
|
|
pub fn links(&self) -> impl Iterator<Item = &Link> {
|
|
self.links.values()
|
|
}
|
|
|
|
pub fn link(&self, index: u32) -> Option<&Link> {
|
|
self.links.get(&index)
|
|
}
|
|
|
|
/// The addresses of the interface with this index.
|
|
pub fn addresses_of(&self, interface: u32) -> impl Iterator<Item = &Address> {
|
|
self.addresses
|
|
.iter()
|
|
.filter(move |address| address.interface == interface)
|
|
}
|
|
|
|
/// Whether `address` is one of this machine's own.
|
|
pub fn is_local(&self, address: IpAddr) -> bool {
|
|
self.addresses.iter().any(|ours| ours.local == address)
|
|
}
|
|
|
|
/// Whether `prefix` is the network of one of this machine's addresses,
|
|
/// or inside one. Whether the interface is up does not matter: the
|
|
/// network is ours either way.
|
|
pub fn has_network_around(&self, prefix: &IpPrefix) -> bool {
|
|
self.addresses
|
|
.iter()
|
|
.any(|address| address.network.contains(prefix))
|
|
}
|
|
|
|
/// Where `neighbor` can be reached without a gateway: the interface
|
|
/// that is up and has an address in a network `neighbor` is in, the
|
|
/// most specific such network if there are several. `None` if there is
|
|
/// none, or if `neighbor` is this machine itself.
|
|
pub fn connected(&self, neighbor: IpAddr) -> Option<Connected> {
|
|
if self.is_local(neighbor) {
|
|
return None;
|
|
}
|
|
let neighbor = host(neighbor);
|
|
self.addresses
|
|
.iter()
|
|
.filter(|address| address.network.contains(&neighbor))
|
|
.filter(|address| {
|
|
self.links
|
|
.get(&address.interface)
|
|
.is_some_and(|link| link.up)
|
|
})
|
|
.max_by_key(|address| address.network.length())
|
|
.map(|address| Connected {
|
|
interface: address.interface,
|
|
local: address.local,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn link(index: u32, name: &str, up: bool) -> Message {
|
|
Message::Link {
|
|
present: true,
|
|
link: Link {
|
|
index,
|
|
name: name.into(),
|
|
up,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn address(interface: u32, local: &str, network: &str) -> Address {
|
|
Address {
|
|
interface,
|
|
local: local.parse().unwrap(),
|
|
network: IpPrefix::try_from(network).unwrap(),
|
|
}
|
|
}
|
|
|
|
fn present(address: Address) -> Message {
|
|
Message::Address {
|
|
present: true,
|
|
address,
|
|
}
|
|
}
|
|
|
|
fn some() -> Interfaces {
|
|
let mut interfaces = Interfaces::default();
|
|
for message in [
|
|
link(1, "lo", true),
|
|
link(2, "eth0", true),
|
|
link(3, "eth1", true),
|
|
link(4, "eth2", false),
|
|
present(address(1, "127.0.0.1", "127.0.0.0/8")),
|
|
present(address(2, "192.0.2.1", "192.0.2.0/24")),
|
|
present(address(2, "2001:db8::1", "2001:db8::/64")),
|
|
// A more specific network inside eth0's, on eth1.
|
|
present(address(3, "192.0.2.129", "192.0.2.128/25")),
|
|
present(address(4, "198.51.100.1", "198.51.100.0/24")),
|
|
] {
|
|
assert!(interfaces.apply(&message));
|
|
}
|
|
interfaces
|
|
}
|
|
|
|
fn connected(interfaces: &Interfaces, neighbor: &str) -> Option<(u32, String)> {
|
|
interfaces
|
|
.connected(neighbor.parse().unwrap())
|
|
.map(|c| (c.interface, c.local.to_string()))
|
|
}
|
|
|
|
#[test]
|
|
fn a_neighbor_is_reached_from_the_interface_whose_network_it_is_in() {
|
|
let interfaces = some();
|
|
assert_eq!(
|
|
connected(&interfaces, "192.0.2.7"),
|
|
Some((2, "192.0.2.1".into()))
|
|
);
|
|
assert_eq!(
|
|
connected(&interfaces, "2001:db8::7"),
|
|
Some((2, "2001:db8::1".into()))
|
|
);
|
|
// The most specific network wins.
|
|
assert_eq!(
|
|
connected(&interfaces, "192.0.2.200"),
|
|
Some((3, "192.0.2.129".into()))
|
|
);
|
|
// Not in any network of ours.
|
|
assert_eq!(connected(&interfaces, "203.0.113.1"), None);
|
|
assert_eq!(connected(&interfaces, "2001:db8:1::1"), None);
|
|
// In the network of an interface that is down.
|
|
assert_eq!(connected(&interfaces, "198.51.100.7"), None);
|
|
// Ourselves.
|
|
assert_eq!(connected(&interfaces, "192.0.2.1"), None);
|
|
assert!(interfaces.is_local("192.0.2.1".parse().unwrap()));
|
|
assert!(!interfaces.is_local("192.0.2.7".parse().unwrap()));
|
|
}
|
|
|
|
#[test]
|
|
fn what_the_kernel_reports_changes_what_is_reachable() {
|
|
let mut interfaces = some();
|
|
// Told again what is known: nothing changes.
|
|
assert!(!interfaces.apply(&link(2, "eth0", true)));
|
|
assert!(!interfaces.apply(&present(address(2, "192.0.2.1", "192.0.2.0/24"))));
|
|
|
|
// An interface comes up.
|
|
assert!(interfaces.apply(&link(4, "eth2", true)));
|
|
assert_eq!(
|
|
connected(&interfaces, "198.51.100.7"),
|
|
Some((4, "198.51.100.1".into()))
|
|
);
|
|
// An address goes.
|
|
assert!(interfaces.apply(&Message::Address {
|
|
present: false,
|
|
address: address(3, "192.0.2.129", "192.0.2.128/25"),
|
|
}));
|
|
assert_eq!(
|
|
connected(&interfaces, "192.0.2.200"),
|
|
Some((2, "192.0.2.1".into()))
|
|
);
|
|
// An interface goes, and its addresses with it.
|
|
assert!(interfaces.apply(&Message::Link {
|
|
present: false,
|
|
link: Link {
|
|
index: 2,
|
|
name: "eth0".into(),
|
|
up: true
|
|
},
|
|
}));
|
|
assert_eq!(connected(&interfaces, "192.0.2.7"), None);
|
|
assert_eq!(interfaces.addresses_of(2).count(), 0);
|
|
// What is not about interfaces changes nothing.
|
|
assert!(!interfaces.apply(&Message::Done));
|
|
}
|
|
}
|