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

feat: Automatically join solicited-node multicast addresses #1012

Merged
merged 6 commits into from
Nov 28, 2024
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
7 changes: 6 additions & 1 deletion src/iface/interface/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,12 @@ impl Interface {
pub fn update_ip_addrs<F: FnOnce(&mut Vec<IpCidr, IFACE_MAX_ADDR_COUNT>)>(&mut self, f: F) {
f(&mut self.inner.ip_addrs);
InterfaceInner::flush_neighbor_cache(&mut self.inner);
InterfaceInner::check_ip_addrs(&self.inner.ip_addrs)
InterfaceInner::check_ip_addrs(&self.inner.ip_addrs);

#[cfg(all(feature = "proto-ipv6", feature = "multicast"))]
if self.inner.caps.medium == Medium::Ethernet {
self.update_solicited_node_groups();
}
}

/// Check whether the interface has the given IP address assigned.
Expand Down
27 changes: 25 additions & 2 deletions src/iface/interface/multicast.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use core::result::Result;
use heapless::LinearMap;
use heapless::{LinearMap, Vec};

#[cfg(any(feature = "proto-ipv4", feature = "proto-ipv6"))]
use super::{check, IpPayload, Packet};
use super::{Interface, InterfaceInner};
use crate::config::IFACE_MAX_MULTICAST_GROUP_COUNT;
use crate::config::{IFACE_MAX_ADDR_COUNT, IFACE_MAX_MULTICAST_GROUP_COUNT};
use crate::phy::{Device, PacketMeta};
use crate::wire::*;

Expand Down Expand Up @@ -156,6 +156,29 @@ impl Interface {
self.inner.has_multicast_group(addr)
}

#[cfg(feature = "proto-ipv6")]
pub(super) fn update_solicited_node_groups(&mut self) {
// Remove old solicited-node multicast addresses
let removals: Vec<_, IFACE_MAX_MULTICAST_GROUP_COUNT> = self
.inner
.multicast
.groups
.keys()
.cloned()
.filter(|a| matches!(a, IpAddress::Ipv6(a) if a.is_solicited_node_multicast() && !self.inner.has_solicited_node(*a)))
.collect();
for removal in removals {
let _ = self.leave_multicast_group(removal);
}

let cidrs: Vec<IpCidr, IFACE_MAX_ADDR_COUNT> = Vec::from_slice(self.ip_addrs()).unwrap();
for cidr in cidrs {
if let IpCidr::Ipv6(cidr) = cidr {
let _ = self.join_multicast_group(cidr.address().solicited_node());
}
}
}

/// Do multicast egress.
///
/// - Send join/leave packets according to the multicast group state.
Expand Down
59 changes: 50 additions & 9 deletions src/iface/interface/tests/ipv6.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,10 @@ fn test_join_ipv6_multicast_group(#[case] medium: Medium) {

let timestamp = Instant::from_millis(0);

// Drain the unsolicited node multicast report from the device
iface.poll(timestamp, &mut device, &mut sockets);
let _ = recv_icmpv6(&mut device, timestamp);

for &group in &groups {
iface.join_multicast_group(group).unwrap();
assert!(iface.has_multicast_group(group));
Expand Down Expand Up @@ -1372,10 +1376,12 @@ fn test_join_ipv6_multicast_group(#[case] medium: Medium) {
}
);

iface.leave_multicast_group(group_addr).unwrap();
assert!(!iface.has_multicast_group(group_addr));
iface.poll(timestamp, &mut device, &mut sockets);
assert!(!iface.has_multicast_group(group_addr));
if !group_addr.is_solicited_node_multicast() {
iface.leave_multicast_group(group_addr).unwrap();
assert!(!iface.has_multicast_group(group_addr));
iface.poll(timestamp, &mut device, &mut sockets);
assert!(!iface.has_multicast_group(group_addr));
}
}
}

Expand Down Expand Up @@ -1414,15 +1420,12 @@ fn test_handle_valid_multicast_query(#[case] medium: Medium) {

let mut eth_bytes = vec![0u8; 86];

let local_ip_addr = Ipv6Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 101);
let local_ip_addr = Ipv6Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 1);
let remote_ip_addr = Ipv6Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 100);
let remote_hw_addr = EthernetAddress([0x52, 0x54, 0x00, 0x00, 0x00, 0x00]);
let query_ip_addr = Ipv6Address::new(0xff02, 0, 0, 0, 0, 0, 0, 0x1234);

iface.join_multicast_group(query_ip_addr).unwrap();
iface
.join_multicast_group(local_ip_addr.solicited_node())
.unwrap();

iface.poll(timestamp, &mut device, &mut sockets);
// flush multicast reports from the join_multicast_group calls
Expand All @@ -1433,7 +1436,7 @@ fn test_handle_valid_multicast_query(#[case] medium: Medium) {
(
Ipv6Address::UNSPECIFIED,
IPV6_LINK_LOCAL_ALL_NODES,
vec![query_ip_addr, local_ip_addr.solicited_node()],
vec![local_ip_addr.solicited_node(), query_ip_addr],
),
// Address specific query, expect only the queried address back
(query_ip_addr, query_ip_addr, vec![query_ip_addr]),
Expand Down Expand Up @@ -1562,3 +1565,41 @@ fn test_handle_valid_multicast_query(#[case] medium: Medium) {
assert_eq!(record_reprs, expected_records);
}
}

#[rstest]
#[case(Medium::Ethernet)]
#[cfg(all(feature = "multicast", feature = "medium-ethernet"))]
fn test_solicited_node_multicast_autojoin(#[case] medium: Medium) {
let (mut iface, _, _) = setup(medium);

let addr1 = Ipv6Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 1);
let addr2 = Ipv6Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 2);

iface.update_ip_addrs(|ip_addrs| {
ip_addrs.clear();
ip_addrs.push(IpCidr::new(addr1.into(), 64)).unwrap();
});
assert!(iface.has_multicast_group(addr1.solicited_node()));
assert!(!iface.has_multicast_group(addr2.solicited_node()));

iface.update_ip_addrs(|ip_addrs| {
ip_addrs.clear();
ip_addrs.push(IpCidr::new(addr2.into(), 64)).unwrap();
});
assert!(!iface.has_multicast_group(addr1.solicited_node()));
assert!(iface.has_multicast_group(addr2.solicited_node()));

iface.update_ip_addrs(|ip_addrs| {
ip_addrs.clear();
ip_addrs.push(IpCidr::new(addr1.into(), 64)).unwrap();
ip_addrs.push(IpCidr::new(addr2.into(), 64)).unwrap();
});
assert!(iface.has_multicast_group(addr1.solicited_node()));
assert!(iface.has_multicast_group(addr2.solicited_node()));

iface.update_ip_addrs(|ip_addrs| {
ip_addrs.clear();
});
assert!(!iface.has_multicast_group(addr1.solicited_node()));
assert!(!iface.has_multicast_group(addr2.solicited_node()));
}
31 changes: 31 additions & 0 deletions src/wire/ipv6.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ pub(crate) trait AddressExt {
/// `x_` prefix is to avoid a collision with the still-unstable method in `core::ip`.
fn x_multicast_scope(&self) -> MulticastScope;

/// Query whether the IPv6 address is a [solicited-node multicast address].
///
/// [Solicited-node multicast address]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.7.1
fn is_solicited_node_multicast(&self) -> bool;

/// If `self` is a CIDR-compatible subnet mask, return `Some(prefix_len)`,
/// where `prefix_len` is the number of leading zeroes. Return `None` otherwise.
fn prefix_len(&self) -> Option<u8>;
Expand Down Expand Up @@ -193,6 +198,13 @@ impl AddressExt for Address {
}
}

fn is_solicited_node_multicast(&self) -> bool {
self.octets()[0..13]
== [
0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF,
]
}

fn prefix_len(&self) -> Option<u8> {
let mut ones = true;
let mut prefix_len = 0;
Expand Down Expand Up @@ -680,6 +692,8 @@ pub(crate) mod test {
const UNIQUE_LOCAL_ADDR: Address = Address::new(0xfd00, 0, 0, 201, 1, 1, 1, 1);
const GLOBAL_UNICAST_ADDR: Address = Address::new(0x2001, 0xdb8, 0x3, 0, 0, 0, 0, 1);

const TEST_SOL_NODE_MCAST_ADDR: Address = Address::new(0xff02, 0, 0, 0, 0, 1, 0xff01, 101);

#[test]
fn test_basic_multicast() {
assert!(!LINK_LOCAL_ALL_ROUTERS.is_unspecified());
Expand All @@ -688,12 +702,14 @@ pub(crate) mod test {
assert!(!LINK_LOCAL_ALL_ROUTERS.is_loopback());
assert!(!LINK_LOCAL_ALL_ROUTERS.x_is_unique_local());
assert!(!LINK_LOCAL_ALL_ROUTERS.is_global_unicast());
assert!(!LINK_LOCAL_ALL_ROUTERS.is_solicited_node_multicast());
assert!(!LINK_LOCAL_ALL_NODES.is_unspecified());
assert!(LINK_LOCAL_ALL_NODES.is_multicast());
assert!(!LINK_LOCAL_ALL_NODES.is_link_local());
assert!(!LINK_LOCAL_ALL_NODES.is_loopback());
assert!(!LINK_LOCAL_ALL_NODES.x_is_unique_local());
assert!(!LINK_LOCAL_ALL_NODES.is_global_unicast());
assert!(!LINK_LOCAL_ALL_NODES.is_solicited_node_multicast());
}

#[test]
Expand All @@ -704,6 +720,7 @@ pub(crate) mod test {
assert!(!LINK_LOCAL_ADDR.is_loopback());
assert!(!LINK_LOCAL_ADDR.x_is_unique_local());
assert!(!LINK_LOCAL_ADDR.is_global_unicast());
assert!(!LINK_LOCAL_ADDR.is_solicited_node_multicast());
}

#[test]
Expand All @@ -714,6 +731,7 @@ pub(crate) mod test {
assert!(Address::LOCALHOST.is_loopback());
assert!(!Address::LOCALHOST.x_is_unique_local());
assert!(!Address::LOCALHOST.is_global_unicast());
assert!(!Address::LOCALHOST.is_solicited_node_multicast());
}

#[test]
Expand All @@ -724,6 +742,7 @@ pub(crate) mod test {
assert!(!UNIQUE_LOCAL_ADDR.is_loopback());
assert!(UNIQUE_LOCAL_ADDR.x_is_unique_local());
assert!(!UNIQUE_LOCAL_ADDR.is_global_unicast());
assert!(!UNIQUE_LOCAL_ADDR.is_solicited_node_multicast());
}

#[test]
Expand All @@ -734,6 +753,18 @@ pub(crate) mod test {
assert!(!GLOBAL_UNICAST_ADDR.is_loopback());
assert!(!GLOBAL_UNICAST_ADDR.x_is_unique_local());
assert!(GLOBAL_UNICAST_ADDR.is_global_unicast());
assert!(!GLOBAL_UNICAST_ADDR.is_solicited_node_multicast());
}

#[test]
fn test_sollicited_node_multicast() {
assert!(!TEST_SOL_NODE_MCAST_ADDR.is_unspecified());
assert!(TEST_SOL_NODE_MCAST_ADDR.is_multicast());
assert!(!TEST_SOL_NODE_MCAST_ADDR.is_link_local());
assert!(!TEST_SOL_NODE_MCAST_ADDR.is_loopback());
assert!(!TEST_SOL_NODE_MCAST_ADDR.x_is_unique_local());
assert!(!TEST_SOL_NODE_MCAST_ADDR.is_global_unicast());
assert!(TEST_SOL_NODE_MCAST_ADDR.is_solicited_node_multicast());
}

#[test]
Expand Down