Files
ferrix/crates/server/src/address.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 3d946c32ce Code health after review: docs, metrics, structure, one more fuzz target
What the code-health review found, and what a fuzz target written for it
found in its first seconds.

- A set of attributes with the same attribute twice is refused by the
  routing table. Its readers (the selection key, the path, the next hop)
  could each take a different one of the two. The decoder never lets one
  through; the new fuzz target `attrs` found that the table would.
- bgp_peer_up{peer}, to alert on a peer going away, and bgp_build_info.
  Every metric is there from the first scrape. Alerts for a peer down,
  hijack attempts and kernel errors are in the README.
- `bgp --check config` validates a configuration without starting, and
  `--version` says which build it is. CI runs the first on the example
  configuration, which a test now also parses with its commented fields
  put in; the example had to learn about the next hop check.
- Path attributes have a module of their own, packet::attributes,
  re-exported from packet::message as before.
- policy::From, which shadowed the trait, is RouteSource. The server's
  connection, dialer and fsm modules are private. Logging arguments live
  in the binary; the server library no longer depends on clap.
- One is_unicast_link_local (the standard library's) instead of two of
  our own; SelectionKey::origin(); one place where an Import is made.
- READMEs say what the daemon does today rather than what it did not do
  a year ago. Documentation links are checked in CI. Unused dependencies
  and bgp_web::serve are gone; the Docker build is --locked and does not
  ship the fuzz corpus.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 19:31:19 +02:00

353 lines
12 KiB
Rust

//! Where a peer is.
//!
//! Most peers are told apart by their IP address alone. A link-local
//! address (`fe80::/10`) means something only on one link, and the same
//! one is often used on every link a router has, so such a peer is the
//! address together with the interface it is reached on: `fe80::1%eth0`.
//!
//! The kernel knows an interface by a number that changes when the
//! interface is made anew, as a tunnel is when it is restarted. So the
//! configuration keeps the name, and the number is looked up whenever it
//! is needed.
use std::fmt;
use std::io;
use std::net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6};
/// The longest name the kernel gives an interface.
const MAX_INTERFACE_NAME: usize = 15;
/// A peer's IP address, and for a link-local address the interface it is
/// on.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PeerAddress {
pub ip: IpAddr,
/// The number of the interface, for a link-local address; zero for
/// any other.
pub interface: u32,
}
impl From<SocketAddr> for PeerAddress {
fn from(address: SocketAddr) -> Self {
let interface = match address {
SocketAddr::V6(v6) if v6.ip().is_unicast_link_local() => v6.scope_id(),
_ => 0,
};
Self {
ip: address.ip(),
interface,
}
}
}
impl PeerAddress {
/// Whether `text` is this address as someone might type it: as it is
/// written out, or without the interface.
pub fn is_called(&self, text: &str) -> bool {
text.parse() == Ok(self.ip) || self.to_string() == text
}
}
impl From<IpAddr> for PeerAddress {
fn from(ip: IpAddr) -> Self {
Self { ip, interface: 0 }
}
}
/// The address, and the interface by name if there is one: `fe80::1%eth0`.
/// An interface that is no longer there goes by its number.
impl fmt::Display for PeerAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.ip)?;
match self.interface {
0 => Ok(()),
index => match interface_name(index) {
Some(name) => write!(f, "%{name}"),
None => write!(f, "%{index}"),
},
}
}
}
/// `address` as text, with an interface by its name: `[fe80::1%eth0]:179`.
pub fn socket_text(address: SocketAddr) -> String {
match PeerAddress::from(address) {
PeerAddress { interface: 0, ip } => SocketAddr::new(ip, address.port()).to_string(),
scoped => format!("[{scoped}]:{}", address.port()),
}
}
/// The number of the interface called `name`, if there is one now.
pub fn interface_index(name: &str) -> Option<u32> {
nix::net::if_::if_nametoindex(name).ok()
}
/// The name of the interface with this number, if there is one now.
pub fn interface_name(index: u32) -> Option<String> {
nix::net::if_::if_indextoname(index)
.ok()?
.into_string()
.ok()
}
/// A peer's address as the configuration has it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Endpoint {
pub ip: IpAddr,
pub port: u16,
/// The interface a link-local address is on; `None` for any other
/// address.
pub interface: Option<String>,
}
/// What is wrong with a peer's address.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum EndpointProblem {
#[error("{0:?} is not an IP address")]
NotAnAddress(String),
#[error(
"{0} is a link-local address, which needs the interface it is on: {0}%eth0, for example"
)]
NoInterface(Ipv6Addr),
#[error("{0} is not a link-local address, and takes no interface")]
NotLinkLocal(IpAddr),
#[error("{0:?} is not the name of an interface")]
InterfaceName(String),
}
impl Endpoint {
/// Reads `text`: an IP address, with `%` and the name of an interface
/// after it if, and only if, it is link-local.
pub fn parse(text: &str, port: u16) -> Result<Self, EndpointProblem> {
let (address, interface) = match text.split_once('%') {
Some((address, interface)) => (address, Some(interface)),
None => (text, None),
};
let ip: IpAddr = address
.parse()
.map_err(|_| EndpointProblem::NotAnAddress(text.to_string()))?;
let link_local = match ip {
IpAddr::V6(v6) if v6.is_unicast_link_local() => Some(v6),
_ => None,
};
let interface = match (link_local, interface) {
(None, None) => None,
(Some(v6), None) => return Err(EndpointProblem::NoInterface(v6)),
(None, Some(_)) => return Err(EndpointProblem::NotLinkLocal(ip)),
(Some(_), Some(name)) => {
let fine = !name.is_empty()
&& name.len() <= MAX_INTERFACE_NAME
&& name
.chars()
.all(|c| c.is_ascii_graphic() && c != '/' && c != '%');
if !fine {
return Err(EndpointProblem::InterfaceName(name.to_string()));
}
Some(name.to_string())
}
};
Ok(Self {
ip,
port,
interface,
})
}
/// The number of the peer's interface as things are now: zero if the
/// address needs none, and `None` if the interface is not there.
fn scope(&self) -> Option<u32> {
match &self.interface {
None => Some(0),
Some(name) => interface_index(name),
}
}
/// What a connection from the peer comes from, as things are now.
/// `None` if the peer's interface is not there.
pub fn address(&self) -> Option<PeerAddress> {
Some(PeerAddress {
ip: self.ip,
interface: self.scope()?,
})
}
/// Whether a connection from `address` is one from this peer.
pub fn is_at(&self, address: PeerAddress) -> bool {
// The interface is looked up only for a peer whose address it is.
self.ip == address.ip && self.scope() == Some(address.interface)
}
/// Where to connect to reach the peer.
pub fn to_dial(&self) -> io::Result<SocketAddr> {
self.socket(self.ip, self.port)
}
/// `ip` and `port` as an address on the peer's link: for the peer
/// itself, and for our own address there.
pub fn socket(&self, ip: IpAddr, port: u16) -> io::Result<SocketAddr> {
let scope = self.scope().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!(
"there is no interface {}",
self.interface.as_deref().unwrap_or_default()
),
)
})?;
Ok(match ip {
IpAddr::V6(v6) if v6.is_unicast_link_local() => {
SocketAddrV6::new(v6, port, 0, scope).into()
}
ip => SocketAddr::new(ip, port),
})
}
}
/// The address and the interface: `fe80::1%eth0`. Without the port.
impl fmt::Display for Endpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.interface {
None => write!(f, "{}", self.ip),
Some(interface) => write!(f, "{}%{interface}", self.ip),
}
}
}
impl Endpoint {
/// The address and the port, as [`SocketAddr`] is written:
/// `[fe80::1%eth0]:179`.
pub fn with_port(&self) -> String {
match self.ip {
IpAddr::V4(v4) => format!("{v4}:{}", self.port),
IpAddr::V6(_) => format!("[{self}]:{}", self.port),
}
}
}
impl From<SocketAddr> for Endpoint {
/// An address that needs no interface. A link-local one keeps none
/// and so reaches nothing; those are read with [`Endpoint::parse`].
fn from(address: SocketAddr) -> Self {
Self {
ip: address.ip(),
port: address.port(),
interface: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// An interface every machine has, and its number.
fn loopback() -> (String, u32) {
["lo", "lo0"]
.iter()
.find_map(|name| Some((name.to_string(), interface_index(name)?)))
.expect("a loopback interface")
}
#[test]
fn a_link_local_address_is_read_with_its_interface() {
let peer = Endpoint::parse("fe80:1234::1%eno3", 179).unwrap();
assert_eq!(peer.ip, "fe80:1234::1".parse::<IpAddr>().unwrap());
assert_eq!(peer.interface.as_deref(), Some("eno3"));
assert_eq!(peer.to_string(), "fe80:1234::1%eno3");
assert_eq!(peer.with_port(), "[fe80:1234::1%eno3]:179");
let plain = Endpoint::parse("2001:db8::1", 1179).unwrap();
assert_eq!(plain.interface, None);
assert_eq!(plain.with_port(), "[2001:db8::1]:1179");
assert_eq!(
Endpoint::parse("192.0.2.1", 179).unwrap().with_port(),
"192.0.2.1:179"
);
}
#[test]
fn an_address_and_an_interface_that_do_not_go_together_are_refused() {
let problem = |text: &str| Endpoint::parse(text, 179).unwrap_err();
assert_eq!(
problem("fe80::1"),
EndpointProblem::NoInterface("fe80::1".parse().unwrap())
);
assert_eq!(
problem("febf::1"),
EndpointProblem::NoInterface("febf::1".parse().unwrap())
);
assert_eq!(
problem("2001:db8::1%eth0"),
EndpointProblem::NotLinkLocal("2001:db8::1".parse().unwrap())
);
assert_eq!(
problem("192.0.2.1%eth0"),
EndpointProblem::NotLinkLocal("192.0.2.1".parse().unwrap())
);
// fec0::/10 was site-local: not link-local.
assert!(Endpoint::parse("fec0::1", 179).is_ok());
for name in ["", "a/b", "an interface", "sixteen-letters-", "a%b"] {
assert_eq!(
problem(&format!("fe80::1%{name}")),
EndpointProblem::InterfaceName(name.to_string()),
);
}
assert_eq!(
problem("example"),
EndpointProblem::NotAnAddress("example".into())
);
}
/// The interface's number is whatever it is when it is asked for.
#[test]
fn the_interface_is_looked_up_when_it_is_needed() {
let (name, index) = loopback();
let peer = Endpoint::parse(&format!("fe80::1%{name}"), 179).unwrap();
let ip: IpAddr = "fe80::1".parse().unwrap();
let there = PeerAddress {
ip,
interface: index,
};
assert_eq!(peer.address(), Some(there));
assert!(peer.is_at(there));
// The same address on another link is another peer.
assert!(!peer.is_at(PeerAddress {
ip,
interface: index + 1000
}));
assert!(!peer.is_at(ip.into()));
match peer.to_dial().unwrap() {
SocketAddr::V6(v6) => assert_eq!((v6.scope_id(), v6.port()), (index, 179)),
SocketAddr::V4(_) => panic!("an IPv6 peer"),
}
assert_eq!(there.to_string(), format!("fe80::1%{name}"));
assert!(there.is_called(&format!("fe80::1%{name}")));
assert!(there.is_called("fe80::1"));
assert!(!there.is_called("fe80::1%other"));
assert!(!there.is_called("fe80::2"));
assert_eq!(
socket_text(peer.to_dial().unwrap()),
format!("[fe80::1%{name}]:179")
);
let gone = Endpoint::parse("fe80::1%not-there", 179).unwrap();
assert_eq!(gone.address(), None);
assert!(!gone.is_at(there));
assert_eq!(
gone.to_dial().unwrap_err().to_string(),
"there is no interface not-there"
);
}
/// Only a link-local address keeps the interface it came in on.
#[test]
fn only_a_link_local_address_has_an_interface() {
let global: SocketAddr =
SocketAddrV6::new("2001:db8::1".parse().unwrap(), 179, 0, 7).into();
assert_eq!(PeerAddress::from(global).interface, 0);
let local: SocketAddr = SocketAddrV6::new("fe80::1".parse().unwrap(), 179, 0, 7).into();
assert_eq!(PeerAddress::from(local).interface, 7);
assert_eq!(socket_text(global), "[2001:db8::1]:179");
}
}