Skip to content

Commit

Permalink
Merge pull request #864 from thvdveld/ipv6-source-address-selection
Browse files Browse the repository at this point in the history
Ipv6 source address selection based on RFC 6724
  • Loading branch information
thvdveld authored Dec 4, 2023
2 parents 9624adf + 434f7eb commit f0d1c37
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 30 deletions.
14 changes: 10 additions & 4 deletions examples/ping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
)
Expand Down
145 changes: 124 additions & 21 deletions src/iface/interface/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -927,23 +948,18 @@ impl InterfaceInner {
}

#[allow(unused)] // unused depending on which sockets are enabled
pub(crate) fn get_source_address(&mut self, dst_addr: IpAddress) -> Option<IpAddress> {
let v = dst_addr.version();
for cidr in self.ip_addrs.iter() {
let addr = cidr.address();
if addr.version() == v {
return Some(addr);
}
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()),
#[cfg(feature = "proto-ipv6")]
IpAddress::Ipv6(addr) => self.get_source_address_ipv6(addr).map(|a| a.into()),
}
None
}

#[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 {
Expand All @@ -955,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
}
None

// 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;
}
}

Some(candidate.address())
}

#[cfg(test)]
Expand Down
121 changes: 121 additions & 0 deletions src/iface/interface/tests/ipv6.rs
Original file line number Diff line number Diff line change
Expand Up @@ -735,3 +735,124 @@ 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)
);

assert_eq!(
iface.get_source_address_ipv6(&LINK_LOCAL_ADDR),
Some(OWN_LINK_LOCAL_ADDR)
);
assert_eq!(
iface.get_source_address_ipv6(&UNIQUE_LOCAL_ADDR1),
Some(OWN_UNIQUE_LOCAL_ADDR1)
);
assert_eq!(
iface.get_source_address_ipv6(&UNIQUE_LOCAL_ADDR2),
Some(OWN_UNIQUE_LOCAL_ADDR2)
);
assert_eq!(
iface.get_source_address_ipv6(&UNIQUE_LOCAL_ADDR3),
Some(OWN_UNIQUE_LOCAL_ADDR1)
);
assert_eq!(
iface.get_source_address_ipv6(&Ipv6Address::LINK_LOCAL_ALL_NODES),
Some(OWN_LINK_LOCAL_ADDR)
);
assert_eq!(
iface.get_source_address_ipv6(&GLOBAL_UNICAST_ADDR1),
Some(OWN_GLOBAL_UNICAST_ADDR1)
);
assert_eq!(
iface.get_source_address_ipv6(&GLOBAL_UNICAST_ADDR2),
Some(OWN_GLOBAL_UNICAST_ADDR1)
);
}
2 changes: 1 addition & 1 deletion src/socket/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ impl<'a> Socket<'a> {
};

let dst_addr = servers[pq.server_idx];
let src_addr = cx.get_source_address(dst_addr).unwrap(); // TODO remove unwrap
let src_addr = cx.get_source_address(&dst_addr).unwrap(); // TODO remove unwrap
let ip_repr = IpRepr::new(
src_addr,
dst_addr,
Expand Down
4 changes: 2 additions & 2 deletions src/socket/icmp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ impl<'a> Socket<'a> {
match *remote_endpoint {
#[cfg(feature = "proto-ipv4")]
IpAddress::Ipv4(dst_addr) => {
let src_addr = match cx.get_source_address_ipv4(dst_addr) {
let src_addr = match cx.get_source_address_ipv4(&dst_addr) {
Some(addr) => addr,
None => {
net_trace!(
Expand Down Expand Up @@ -571,7 +571,7 @@ impl<'a> Socket<'a> {
}
#[cfg(feature = "proto-ipv6")]
IpAddress::Ipv6(dst_addr) => {
let src_addr = match cx.get_source_address_ipv6(dst_addr) {
let src_addr = match cx.get_source_address_ipv6(&dst_addr) {
Some(addr) => addr,
None => {
net_trace!(
Expand Down
2 changes: 1 addition & 1 deletion src/socket/tcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,7 @@ impl<'a> Socket<'a> {
addr
}
None => cx
.get_source_address(remote_endpoint.addr)
.get_source_address(&remote_endpoint.addr)
.ok_or(ConnectError::Unaddressable)?,
},
port: local_endpoint.port,
Expand Down
2 changes: 1 addition & 1 deletion src/socket/udp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ impl<'a> Socket<'a> {
let res = self.tx_buffer.dequeue_with(|packet_meta, payload_buf| {
let src_addr = match endpoint.addr {
Some(addr) => addr,
None => match cx.get_source_address(packet_meta.endpoint.addr) {
None => match cx.get_source_address(&packet_meta.endpoint.addr) {
Some(addr) => addr,
None => {
net_trace!(
Expand Down
Loading

0 comments on commit f0d1c37

Please sign in to comment.