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>
353 lines
12 KiB
Rust
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");
|
|
}
|
|
}
|