Peer at link-local addresses: fe80::1%eth0

A peer's endpoint may be a link-local IPv6 address with the interface it
is on. Such a peer is the address and the interface together, wherever
peers are told apart: admission, collisions, hold-downs, the refusal log,
the status page. The interface is looked up by name each time it is
needed, so one that appears later or is made anew is found.

Our routes go out with our link-local address in both places of
MP_REACH_NLRI's next hop, as FRR sends its own. A peer's link-local next
hop is taken alone, twice, or after "::" (BIRD), and resolved in the
kernel on the interface of the session rather than by looking fe80::/64
up, which every interface has.

The lab has a new phase, linklocal: sessions with FRR (by interface, with
radvd beside the daemon) and BIRD over a LAN with link-local addresses
only, both families, pings across, the interface down and up.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-02 18:24:13 +02:00
1 parent 92e013ca01
commit fd2248feb5
38 files changed
+975 -151

No files matched your search

+50 -17
View File
@@ -11,6 +11,7 @@ use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_rpki::RpkiConfig;
use crate::address::{Endpoint, PeerAddress};
use crate::error::{ConfigError, ImportProblem, OriginateProblem, PeerProblem};
use crate::family::FamilySet;
use crate::policy::{ImportPolicy, PeerRole, PrefixSet};
@@ -54,7 +55,8 @@ pub struct Config {
#[derive(Debug, Clone)]
pub struct PeerConfig {
pub name: String,
pub endpoint: SocketAddr,
/// Where the peer is. A link-local address has its interface with it.
pub endpoint: Endpoint,
pub remote_asn: u32,
pub hold_time_secs: u16,
/// Source address for connections we open to this peer; `None` lets the
@@ -76,8 +78,8 @@ pub struct PeerConfig {
impl Config {
/// The configured peer at `address`, if there is one.
pub fn peer_at(&self, address: IpAddr) -> Option<&PeerConfig> {
self.peers.iter().find(|peer| peer.endpoint.ip() == address)
pub fn peer_at(&self, address: PeerAddress) -> Option<&PeerConfig> {
self.peers.iter().find(|peer| peer.endpoint.is_at(address))
}
pub fn from_proto(proto: BgpConfig) -> Result<Self, ConfigError> {
@@ -124,10 +126,10 @@ impl Config {
if peers.iter().any(|earlier| earlier.name == peer.name) {
return Err(rejected(PeerProblem::DuplicateName));
}
if peers
.iter()
.any(|earlier| earlier.endpoint.ip() == peer.endpoint.ip())
{
if peers.iter().any(|earlier| {
earlier.endpoint.ip == peer.endpoint.ip
&& earlier.endpoint.interface == peer.endpoint.interface
}) {
return Err(rejected(PeerProblem::DuplicateAddress));
}
peers.push(peer);
@@ -197,8 +199,9 @@ impl Config {
fn parse_peer(peer: Peer) -> Result<PeerConfig, PeerProblem> {
let ep = peer.endpoint.as_ref().ok_or(PeerProblem::MissingEndpoint)?;
let endpoint =
parse_endpoint(&ep.address, ep.port).map_err(|e| PeerProblem::Endpoint(Box::new(e)))?;
let endpoint = parse_port(ep.port)
.and_then(|port| Ok(Endpoint::parse(&ep.address, port)?))
.map_err(|e| PeerProblem::Endpoint(Box::new(e)))?;
if peer.remote_asn == 0 {
return Err(PeerProblem::RemoteAsn);
@@ -216,7 +219,7 @@ fn parse_peer(peer: Peer) -> Result<PeerConfig, PeerProblem> {
} else {
let ip = IpAddr::from_str(&peer.local_address)
.map_err(|_| PeerProblem::LocalAddress(peer.local_address.clone()))?;
if ip.is_ipv4() != endpoint.is_ipv4() {
if ip.is_ipv4() != endpoint.ip.is_ipv4() {
return Err(PeerProblem::AddressFamilyMismatch);
}
Some(ip)
@@ -266,7 +269,7 @@ fn parse_peer(peer: Peer) -> Result<PeerConfig, PeerProblem> {
Ok(PeerConfig {
// A peer without a name goes by its address.
name: if peer.name.is_empty() {
endpoint.ip().to_string()
endpoint.to_string()
} else {
peer.name
},
@@ -374,11 +377,15 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
})
}
fn parse_port(port: u32) -> Result<u16, ConfigError> {
match port {
0 => Ok(DEFAULT_BGP_PORT),
port => u16::try_from(port).map_err(|_| ConfigError::Port(port)),
}
}
fn parse_endpoint(address: &str, port: u32) -> Result<SocketAddr, ConfigError> {
let port = match port {
0 => DEFAULT_BGP_PORT,
port => u16::try_from(port).map_err(|_| ConfigError::Port(port))?,
};
let port = parse_port(port)?;
let ip = IpAddr::from_str(address).map_err(|_| ConfigError::Address(address.to_string()))?;
Ok(SocketAddr::new(ip, port))
}
@@ -624,6 +631,28 @@ mod tests {
PeerProblem::DuplicateAddress
);
// A link-local address is one peer on each interface it is on.
assert_eq!(
problem(peer("r1", "fe80::1%eth0") + &peer("r2", "fe80::1%eth0")),
PeerProblem::DuplicateAddress
);
let config = parse(&format!(
"{BASE}{}{}",
peer("", "fe80::1%eth0"),
peer("", "fe80::1%eth1")
))
.unwrap();
assert_eq!(
config.peers.iter().map(|p| &p.name).collect::<Vec<_>>(),
["fe80::1%eth0", "fe80::1%eth1"]
);
// And nothing without one.
assert_eq!(
problem(peer("r1", "fe80::1")).to_string(),
"endpoint is invalid: fe80::1 is a link-local address, which needs the interface it \
is on: fe80::1%eth0, for example"
);
// A peer without a name goes by its address, so two of them differ.
let config = parse(&format!(
"{BASE}{}{}",
@@ -635,11 +664,15 @@ mod tests {
assert_eq!(config.peers[1].name, "192.0.2.3");
assert_eq!(
config
.peer_at("192.0.2.3".parse().unwrap())
.peer_at("192.0.2.3".parse::<IpAddr>().unwrap().into())
.map(|p| &*p.name),
Some("192.0.2.3")
);
assert!(config.peer_at("192.0.2.4".parse().unwrap()).is_none());
assert!(
config
.peer_at("192.0.2.4".parse::<IpAddr>().unwrap().into())
.is_none()
);
}
/// A peer would refuse our OPEN.