ipv6: use RFC6724 for selecting IPv6 src address

RFC6724 defines how the source address should be selected when given a
destination address. Instead of selecting the first address in the list
of interface addresses, the source address is selected following the
standard.
This commit is contained in:
Thibaut Vandervelden 2023-12-01 11:01:12 +01:00
parent 45f9838ad9
commit dd9eff7cf9
3 changed files with 221 additions and 18 deletions

View File

@ -176,7 +176,7 @@ fn main() {
);
icmp_repr.emit(&mut icmp_packet, &device_caps.checksum);
}
IpAddress::Ipv6(_) => {
IpAddress::Ipv6(address) => {
let (icmp_repr, mut icmp_packet) = send_icmp_ping!(
Icmpv6Repr,
Icmpv6Packet,
@ -187,7 +187,10 @@ fn main() {
remote_addr
);
icmp_repr.emit(
&iface.ipv6_addr().unwrap().into_address(),
&iface
.get_source_address_ipv6(&address)
.unwrap()
.into_address(),
&remote_addr,
&mut icmp_packet,
&device_caps.checksum,
@ -217,11 +220,14 @@ fn main() {
received
);
}
IpAddress::Ipv6(_) => {
IpAddress::Ipv6(address) => {
let icmp_packet = Icmpv6Packet::new_checked(&payload).unwrap();
let icmp_repr = Icmpv6Repr::parse(
&remote_addr,
&iface.ipv6_addr().unwrap().into_address(),
&iface
.get_source_address_ipv6(&address)
.unwrap()
.into_address(),
&icmp_packet,
&device_caps.checksum,
)

View File

@ -464,6 +464,27 @@ impl Interface {
self.inner.ipv6_addr()
}
/// Get an address from the interface that could be used as source address. For IPv4, this is
/// the first IPv4 address from the list of addresses. For IPv6, the address is based on the
/// destination address and uses RFC6724 for selecting the source address.
pub fn get_source_address(&self, dst_addr: &IpAddress) -> Option<IpAddress> {
self.inner.get_source_address(*dst_addr)
}
/// Get an address from the interface that could be used as source address. This is the first
/// IPv4 address from the list of addresses in the interface.
#[cfg(feature = "proto-ipv4")]
pub fn get_source_address_ipv4(&self, dst_addr: &Ipv4Address) -> Option<Ipv4Address> {
self.inner.get_source_address_ipv4(*dst_addr)
}
/// Get an address from the interface that could be used as source address. The selection is
/// based on RFC6724.
#[cfg(feature = "proto-ipv6")]
pub fn get_source_address_ipv6(&self, dst_addr: &Ipv6Address) -> Option<Ipv6Address> {
self.inner.get_source_address_ipv6(*dst_addr)
}
/// Update the IP addresses of the interface.
///
/// # Panics
@ -927,7 +948,7 @@ impl InterfaceInner {
}
#[allow(unused)] // unused depending on which sockets are enabled
pub(crate) fn get_source_address(&mut self, dst_addr: IpAddress) -> Option<IpAddress> {
pub(crate) fn get_source_address(&self, dst_addr: IpAddress) -> Option<IpAddress> {
match dst_addr {
#[cfg(feature = "proto-ipv4")]
IpAddress::Ipv4(addr) => self.get_source_address_ipv4(addr).map(|a| a.into()),
@ -938,10 +959,7 @@ impl InterfaceInner {
#[cfg(feature = "proto-ipv4")]
#[allow(unused)]
pub(crate) fn get_source_address_ipv4(
&mut self,
_dst_addr: Ipv4Address,
) -> Option<Ipv4Address> {
pub(crate) fn get_source_address_ipv4(&self, _dst_addr: Ipv4Address) -> Option<Ipv4Address> {
for cidr in self.ip_addrs.iter() {
#[allow(irrefutable_let_patterns)] // if only ipv4 is enabled
if let IpCidr::Ipv4(cidr) = cidr {
@ -953,17 +971,104 @@ impl InterfaceInner {
#[cfg(feature = "proto-ipv6")]
#[allow(unused)]
pub(crate) fn get_source_address_ipv6(
&mut self,
_dst_addr: Ipv6Address,
) -> Option<Ipv6Address> {
for cidr in self.ip_addrs.iter() {
#[allow(irrefutable_let_patterns)] // if only ipv6 is enabled
if let IpCidr::Ipv6(cidr) = cidr {
return Some(cidr.address());
pub(crate) fn get_source_address_ipv6(&self, dst_addr: Ipv6Address) -> Option<Ipv6Address> {
// RFC 6724 describes how to select the correct source address depending on the destination
// address.
// See RFC 6724 Section 4: Candidate source address
fn is_candidate_source_address(dst_addr: &Ipv6Address, src_addr: &Ipv6Address) -> bool {
// For all multicast and link-local destination addresses, the candidate address MUST
// only be an address from the same link.
if dst_addr.is_link_local() && !src_addr.is_link_local() {
return false;
}
if dst_addr.is_multicast()
&& matches!(dst_addr.scope(), Ipv6AddressScope::LinkLocal)
&& src_addr.is_multicast()
&& !matches!(src_addr.scope(), Ipv6AddressScope::LinkLocal)
{
return false;
}
// Loopback addresses and multicast address can not be in the candidate source address
// list. Except when the destination multicast address has a link-local scope, then the
// source address can also be link-local multicast.
if src_addr.is_loopback() || src_addr.is_multicast() {
return false;
}
true
}
// See RFC 6724 Section 2.2: Common Prefix Length
fn common_prefix_length(dst_addr: &Ipv6Cidr, src_addr: &Ipv6Address) -> usize {
let addr = dst_addr.address();
let mut bits = 0;
for (l, r) in addr.as_bytes().iter().zip(src_addr.as_bytes().iter()) {
if l == r {
bits += 8;
} else {
bits += (l ^ r).leading_zeros();
break;
}
}
bits = bits.min(dst_addr.prefix_len() as u32);
bits as usize
}
// Get the first address that is a candidate address.
let mut candidate = self
.ip_addrs
.iter()
.filter_map(|a| match a {
#[cfg(feature = "proto-ipv4")]
IpCidr::Ipv4(_) => None,
#[cfg(feature = "proto-ipv6")]
IpCidr::Ipv6(a) => Some(a),
})
.find(|a| is_candidate_source_address(&dst_addr, &a.address()))
.unwrap();
for addr in self.ip_addrs.iter().filter_map(|a| match a {
#[cfg(feature = "proto-ipv4")]
IpCidr::Ipv4(_) => None,
#[cfg(feature = "proto-ipv6")]
IpCidr::Ipv6(a) => Some(a),
}) {
if !is_candidate_source_address(&dst_addr, &addr.address()) {
continue;
}
// Rule 1: prefer the address that is the same as the output destination address.
if candidate.address() != dst_addr && addr.address() == dst_addr {
candidate = addr;
}
// Rule 2: prefer appropriate scope.
if (candidate.address().scope() as u8) < (addr.address().scope() as u8) {
if (candidate.address().scope() as u8) < (dst_addr.scope() as u8) {
candidate = addr;
}
} else if (addr.address().scope() as u8) > (dst_addr.scope() as u8) {
candidate = addr;
}
// Rule 3: avoid deprecated addresses (TODO)
// Rule 4: prefer home addresses (TODO)
// Rule 5: prefer outgoing interfaces (TODO)
// Rule 5.5: prefer addresses in a prefix advertises by the next-hop (TODO).
// Rule 6: prefer matching label (TODO)
// Rule 7: prefer temporary addresses (TODO)
// Rule 8: use longest matching prefix
if common_prefix_length(candidate, &dst_addr) < common_prefix_length(addr, &dst_addr) {
candidate = addr;
}
}
None
Some(candidate.address())
}
#[cfg(test)]

View File

@ -735,3 +735,95 @@ fn test_icmp_reply_size(#[case] medium: Medium) {
))
);
}
#[cfg(feature = "medium-ip")]
#[test]
fn get_source_address() {
let (mut iface, _, _) = setup(Medium::Ip);
const OWN_LINK_LOCAL_ADDR: Ipv6Address = Ipv6Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 1);
const OWN_UNIQUE_LOCAL_ADDR1: Ipv6Address = Ipv6Address::new(0xfd00, 0, 0, 201, 1, 1, 1, 2);
const OWN_UNIQUE_LOCAL_ADDR2: Ipv6Address = Ipv6Address::new(0xfd01, 0, 0, 201, 1, 1, 1, 2);
const OWN_GLOBAL_UNICAST_ADDR1: Ipv6Address =
Ipv6Address::new(0x2001, 0x0db8, 0x0003, 0, 0, 0, 0, 1);
// List of addresses of the interface:
// fe80::1/64
// fd00::201:1:1:1:2/64
// fd01::201:1:1:1:2/64
// 2001:db8:3::1/64
// ::1/128
// ::/128
iface.update_ip_addrs(|addrs| {
addrs.clear();
addrs
.push(IpCidr::Ipv6(Ipv6Cidr::new(OWN_LINK_LOCAL_ADDR, 64)))
.unwrap();
addrs
.push(IpCidr::Ipv6(Ipv6Cidr::new(OWN_UNIQUE_LOCAL_ADDR1, 64)))
.unwrap();
addrs
.push(IpCidr::Ipv6(Ipv6Cidr::new(OWN_UNIQUE_LOCAL_ADDR2, 64)))
.unwrap();
addrs
.push(IpCidr::Ipv6(Ipv6Cidr::new(OWN_GLOBAL_UNICAST_ADDR1, 64)))
.unwrap();
// These should never be used:
addrs
.push(IpCidr::Ipv6(Ipv6Cidr::new(Ipv6Address::LOOPBACK, 128)))
.unwrap();
addrs
.push(IpCidr::Ipv6(Ipv6Cidr::new(Ipv6Address::UNSPECIFIED, 128)))
.unwrap();
});
// List of addresses we test:
// fe80::42 -> fe80::1
// fd00::201:1:1:1:1 -> fd00::201:1:1:1:2
// fd01::201:1:1:1:1 -> fd01::201:1:1:1:2
// fd02::201:1:1:1:1 -> fd00::201:1:1:1:2 (because first added in the list)
// ff02::1 -> fe80::1 (same scope)
// 2001:db8:3::2 -> 2001:db8:3::1
// 2001:db9:3::2 -> 2001:db8:3::1
const LINK_LOCAL_ADDR: Ipv6Address = Ipv6Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 42);
const UNIQUE_LOCAL_ADDR1: Ipv6Address = Ipv6Address::new(0xfd00, 0, 0, 201, 1, 1, 1, 1);
const UNIQUE_LOCAL_ADDR2: Ipv6Address = Ipv6Address::new(0xfd01, 0, 0, 201, 1, 1, 1, 1);
const UNIQUE_LOCAL_ADDR3: Ipv6Address = Ipv6Address::new(0xfd02, 0, 0, 201, 1, 1, 1, 1);
const GLOBAL_UNICAST_ADDR1: Ipv6Address =
Ipv6Address::new(0x2001, 0x0db8, 0x0003, 0, 0, 0, 0, 2);
const GLOBAL_UNICAST_ADDR2: Ipv6Address =
Ipv6Address::new(0x2001, 0x0db9, 0x0003, 0, 0, 0, 0, 2);
assert_eq!(
iface.inner.get_source_address_ipv6(LINK_LOCAL_ADDR),
Some(OWN_LINK_LOCAL_ADDR)
);
assert_eq!(
iface.inner.get_source_address_ipv6(UNIQUE_LOCAL_ADDR1),
Some(OWN_UNIQUE_LOCAL_ADDR1)
);
assert_eq!(
iface.inner.get_source_address_ipv6(UNIQUE_LOCAL_ADDR2),
Some(OWN_UNIQUE_LOCAL_ADDR2)
);
assert_eq!(
iface.inner.get_source_address_ipv6(UNIQUE_LOCAL_ADDR3),
Some(OWN_UNIQUE_LOCAL_ADDR1)
);
assert_eq!(
iface
.inner
.get_source_address_ipv6(Ipv6Address::LINK_LOCAL_ALL_NODES),
Some(OWN_LINK_LOCAL_ADDR)
);
assert_eq!(
iface.inner.get_source_address_ipv6(GLOBAL_UNICAST_ADDR1),
Some(OWN_GLOBAL_UNICAST_ADDR1)
);
assert_eq!(
iface.inner.get_source_address_ipv6(GLOBAL_UNICAST_ADDR2),
Some(OWN_GLOBAL_UNICAST_ADDR1)
);
}