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:
1 parent
92e013ca01
commit
fd2248feb5
38 files changed
+975
-151
No files matched your search
+129
-12
@@ -60,8 +60,27 @@ fn is_link_local(address: &Ipv6Addr) -> bool {
|
||||
address.segments()[0] & 0xffc0 == 0xfe80
|
||||
}
|
||||
|
||||
/// The peer a route was learned from, as far as its next hop is concerned.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Neighbor {
|
||||
pub address: IpAddr,
|
||||
/// The interface the session with the peer is on, where the peer's
|
||||
/// address is a link-local one and says nothing without it; zero
|
||||
/// otherwise.
|
||||
pub interface: u32,
|
||||
}
|
||||
|
||||
impl From<IpAddr> for Neighbor {
|
||||
fn from(address: IpAddr) -> Self {
|
||||
Self {
|
||||
address,
|
||||
interface: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Works out where the kernel is to send packets for a route to `prefix`
|
||||
/// with the next hop `next_hop`, learned from the peer at `peer`.
|
||||
/// with the next hop `next_hop`, learned from `peer`.
|
||||
///
|
||||
/// Only a next hop that is a neighbor on one of our interfaces is
|
||||
/// resolved. A link-local next hop means something only on one link, and
|
||||
@@ -70,7 +89,7 @@ pub fn resolve(
|
||||
interfaces: &Interfaces,
|
||||
prefix: &IpPrefix,
|
||||
next_hop: Option<&NlriNextHop>,
|
||||
peer: Option<IpAddr>,
|
||||
peer: Option<Neighbor>,
|
||||
) -> Result<Target, Unresolved> {
|
||||
let next_hop = next_hop.ok_or(Unresolved::Missing)?;
|
||||
let version = match next_hop {
|
||||
@@ -97,12 +116,29 @@ pub fn resolve(
|
||||
};
|
||||
// The interface the peer is a neighbor on, for a link-local gateway.
|
||||
let on_peers_link = |gateway: Ipv6Addr| {
|
||||
peer.and_then(|peer| interfaces.connected(peer))
|
||||
.map(|connected| Target {
|
||||
gateway: gateway.into(),
|
||||
interface: connected.interface,
|
||||
})
|
||||
.ok_or(Unresolved::NotConnected)
|
||||
let interface = match peer.ok_or(Unresolved::NotConnected)? {
|
||||
// The session is on a link-local address itself, and so says
|
||||
// which link: every interface has fe80::/64, and looking the
|
||||
// peer up there would find any of them.
|
||||
Neighbor { interface, .. } if interface != 0 => interfaces
|
||||
.link(interface)
|
||||
.filter(|link| link.up)
|
||||
.map(|link| link.index),
|
||||
Neighbor { address, .. } => interfaces
|
||||
.connected(address)
|
||||
.map(|connected| connected.interface),
|
||||
}
|
||||
.ok_or(Unresolved::NotConnected)?;
|
||||
if interfaces
|
||||
.addresses_of(interface)
|
||||
.any(|ours| ours.local == IpAddr::V6(gateway))
|
||||
{
|
||||
return Err(Unresolved::Ourselves);
|
||||
}
|
||||
Ok(Target {
|
||||
gateway: gateway.into(),
|
||||
interface,
|
||||
})
|
||||
};
|
||||
match *next_hop {
|
||||
NlriNextHop::Ipv4(gateway) => direct(gateway.into()),
|
||||
@@ -110,6 +146,13 @@ pub fn resolve(
|
||||
NlriNextHop::Ipv6(gateway) => direct(gateway.into()),
|
||||
// The global address if it is a neighbor; else the link-local one,
|
||||
// which is there for exactly that case (RFC 2545 §3).
|
||||
// Some put the link-local address, or nothing, where the global
|
||||
// one goes, having no global one to give.
|
||||
NlriNextHop::Ipv6WithLl { global, link_local }
|
||||
if global.is_unspecified() || is_link_local(&global) =>
|
||||
{
|
||||
on_peers_link(link_local)
|
||||
}
|
||||
NlriNextHop::Ipv6WithLl { global, link_local } => match direct(global.into()) {
|
||||
Err(Unresolved::NotConnected) => on_peers_link(link_local),
|
||||
resolved => resolved,
|
||||
@@ -405,12 +448,16 @@ mod tests {
|
||||
interfaces
|
||||
}
|
||||
|
||||
fn neighbor(address: &str) -> Neighbor {
|
||||
address.parse::<IpAddr>().unwrap().into()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_next_hop_resolves_if_it_is_a_neighbor() {
|
||||
let interfaces = interfaces();
|
||||
let v4 = prefix("203.0.113.0/24");
|
||||
let v6 = prefix("2001:db8:100::/48");
|
||||
let peer = Some("192.0.2.2".parse().unwrap());
|
||||
let peer = Some(neighbor("192.0.2.2"));
|
||||
let v4_hop = |s: &str| NlriNextHop::Ipv4(s.parse().unwrap());
|
||||
let v6_hop = |s: &str| NlriNextHop::Ipv6(s.parse().unwrap());
|
||||
|
||||
@@ -452,7 +499,7 @@ mod tests {
|
||||
global: "2001:db8:ffff::2".parse().unwrap(),
|
||||
link_local: "fe80::2".parse().unwrap(),
|
||||
}),
|
||||
Some("2001:db8::2".parse().unwrap()),
|
||||
Some(neighbor("2001:db8::2")),
|
||||
),
|
||||
Ok(target("fe80::2", 2))
|
||||
);
|
||||
@@ -462,7 +509,7 @@ mod tests {
|
||||
fn a_link_local_next_hop_is_on_the_peers_link() {
|
||||
let interfaces = interfaces();
|
||||
let v6 = prefix("2001:db8:100::/48");
|
||||
let peer = Some("2001:db8::2".parse().unwrap());
|
||||
let peer = Some(neighbor("2001:db8::2"));
|
||||
let link_local: Ipv6Addr = "fe80::2".parse().unwrap();
|
||||
let both = |global: &str| NlriNextHop::Ipv6WithLl {
|
||||
global: global.parse().unwrap(),
|
||||
@@ -483,7 +530,7 @@ mod tests {
|
||||
Ok(target("fe80::2", 2))
|
||||
);
|
||||
// A peer that is not a neighbor says nothing about the link.
|
||||
let far = Some("2001:db8:ffff::9".parse().unwrap());
|
||||
let far = Some(neighbor("2001:db8:ffff::9"));
|
||||
assert_eq!(
|
||||
resolve(&interfaces, &v6, Some(&NlriNextHop::Ipv6(link_local)), far),
|
||||
Err(Unresolved::NotConnected)
|
||||
@@ -494,6 +541,76 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A session on link-local addresses: which link is known from the
|
||||
/// session and from nothing else, every interface having fe80::/64.
|
||||
#[test]
|
||||
fn a_link_local_peer_is_on_the_interface_of_its_session() {
|
||||
let mut interfaces = interfaces();
|
||||
for (index, local) in [(2, "fe80::a"), (3, "fe80::b")] {
|
||||
interfaces.apply(&Message::Address {
|
||||
present: true,
|
||||
address: Address {
|
||||
interface: index,
|
||||
local: local.parse().unwrap(),
|
||||
network: prefix("fe80::/64"),
|
||||
},
|
||||
});
|
||||
}
|
||||
let v4 = prefix("203.0.113.0/24");
|
||||
let v6 = prefix("2001:db8:100::/48");
|
||||
let link_local: Ipv6Addr = "fe80::2".parse().unwrap();
|
||||
let on = |interface| {
|
||||
Some(Neighbor {
|
||||
address: link_local.into(),
|
||||
interface,
|
||||
})
|
||||
};
|
||||
// However the peer says it: the address alone, or twice, or after
|
||||
// nothing where a global one would go.
|
||||
for next_hop in [
|
||||
NlriNextHop::Ipv6(link_local),
|
||||
NlriNextHop::Ipv6WithLl {
|
||||
global: link_local,
|
||||
link_local,
|
||||
},
|
||||
NlriNextHop::Ipv6WithLl {
|
||||
global: Ipv6Addr::UNSPECIFIED,
|
||||
link_local,
|
||||
},
|
||||
] {
|
||||
for interface in [2, 3] {
|
||||
for route in [&v4, &v6] {
|
||||
assert_eq!(
|
||||
resolve(&interfaces, route, Some(&next_hop), on(interface)),
|
||||
Ok(target("fe80::2", interface)),
|
||||
"{next_hop:?} on {interface}"
|
||||
);
|
||||
}
|
||||
}
|
||||
// An interface that is not there, or is down.
|
||||
assert_eq!(
|
||||
resolve(&interfaces, &v6, Some(&next_hop), on(9)),
|
||||
Err(Unresolved::NotConnected)
|
||||
);
|
||||
}
|
||||
// Our own address on that link, which on another is a neighbor's.
|
||||
let ours = NlriNextHop::Ipv6("fe80::a".parse().unwrap());
|
||||
assert_eq!(
|
||||
resolve(&interfaces, &v6, Some(&ours), on(2)),
|
||||
Err(Unresolved::Ourselves)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve(&interfaces, &v6, Some(&ours), on(3)),
|
||||
Ok(target("fe80::a", 3))
|
||||
);
|
||||
// A global next hop on such a session is looked up as any other.
|
||||
let global = NlriNextHop::Ipv6("2001:db8::2".parse().unwrap());
|
||||
assert_eq!(
|
||||
resolve(&interfaces, &v6, Some(&global), on(3)),
|
||||
Ok(target("2001:db8::2", 2))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_kernel_is_asked_only_for_what_changes() {
|
||||
let mut plan = Plan::default();
|
||||
|
||||
Reference in new issue
Block a user