Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ipv6 source address selection based on RFC 6724 #864

Merged
merged 5 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading