From 2d5a2009593a8f1a5bf05075ec076191e77fa3db Mon Sep 17 00:00:00 2001 From: Koen Zandberg Date: Thu, 14 Nov 2024 07:54:26 +0100 Subject: [PATCH 1/6] feat(ipv6): Extend with sol node mcast check --- src/wire/ipv6.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/wire/ipv6.rs b/src/wire/ipv6.rs index 8dc05c51d..24904b60a 100644 --- a/src/wire/ipv6.rs +++ b/src/wire/ipv6.rs @@ -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; @@ -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 { let mut ones = true; let mut prefix_len = 0; @@ -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()); @@ -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] @@ -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] @@ -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] @@ -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] @@ -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] From 2a5f90ab0b664454d478c173cb0d28335f874553 Mon Sep 17 00:00:00 2001 From: Koen Zandberg Date: Thu, 14 Nov 2024 10:40:20 +0100 Subject: [PATCH 2/6] feat: Automatically join sol-node mcast addresses IPv6 over Ethernet should join the solicited-node multicast addresses required by the configured IPv6 addresses, as neighbor solicitations for these addresses have the solicited-node multicast address as destination address. This commit automatically leaves old solicited-node multicast addresses and joins the new set when the IP addresses on the interface are updated. --- src/iface/interface/mod.rs | 7 ++++++- src/iface/interface/multicast.rs | 34 +++++++++++++++++++++++++++++-- src/iface/interface/tests/ipv6.rs | 14 +++++++++---- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/iface/interface/mod.rs b/src/iface/interface/mod.rs index 4df6d7378..4a2458ab6 100644 --- a/src/iface/interface/mod.rs +++ b/src/iface/interface/mod.rs @@ -361,7 +361,12 @@ impl Interface { pub fn update_ip_addrs)>(&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. diff --git a/src/iface/interface/multicast.rs b/src/iface/interface/multicast.rs index 6f67c3d9c..e79194a24 100644 --- a/src/iface/interface/multicast.rs +++ b/src/iface/interface/multicast.rs @@ -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::*; @@ -156,6 +156,36 @@ 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() + .filter_map(|group_addr| match group_addr { + IpAddress::Ipv6(address) + if address.is_solicited_node_multicast() + && self.inner.has_solicited_node(*address) => + { + Some(*group_addr) + } + _ => None, + }) + .collect(); + for removal in removals { + let _ = self.leave_multicast_group(removal); + } + + let cidrs: Vec = 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. diff --git a/src/iface/interface/tests/ipv6.rs b/src/iface/interface/tests/ipv6.rs index 75687e5c3..ca1df2def 100644 --- a/src/iface/interface/tests/ipv6.rs +++ b/src/iface/interface/tests/ipv6.rs @@ -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)); @@ -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)); + } } } From 2be54073f1c9e298a5d7d80ad3a42b0355f47aa8 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Thu, 28 Nov 2024 00:56:34 +0100 Subject: [PATCH 3/6] remove multicast groups that we *don't* want as solicited node. --- src/iface/interface/multicast.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iface/interface/multicast.rs b/src/iface/interface/multicast.rs index e79194a24..37ae1f3c1 100644 --- a/src/iface/interface/multicast.rs +++ b/src/iface/interface/multicast.rs @@ -167,7 +167,7 @@ impl Interface { .filter_map(|group_addr| match group_addr { IpAddress::Ipv6(address) if address.is_solicited_node_multicast() - && self.inner.has_solicited_node(*address) => + && !self.inner.has_solicited_node(*address) => { Some(*group_addr) } From e9cf1c51a59307a3c8ca1d0d6b24052ef6eb08de Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Thu, 28 Nov 2024 00:58:01 +0100 Subject: [PATCH 4/6] iface: make iterator chain a bit more readable. --- src/iface/interface/multicast.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/iface/interface/multicast.rs b/src/iface/interface/multicast.rs index 37ae1f3c1..45261a8fd 100644 --- a/src/iface/interface/multicast.rs +++ b/src/iface/interface/multicast.rs @@ -164,15 +164,8 @@ impl Interface { .multicast .groups .keys() - .filter_map(|group_addr| match group_addr { - IpAddress::Ipv6(address) - if address.is_solicited_node_multicast() - && !self.inner.has_solicited_node(*address) => - { - Some(*group_addr) - } - _ => None, - }) + .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); From e9bf78c69d7500e1bb1497118f8bd57aadaa90d9 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Thu, 28 Nov 2024 00:58:14 +0100 Subject: [PATCH 5/6] iface: add test for ipv6 solicited node autojoin. --- src/iface/interface/tests/ipv6.rs | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/iface/interface/tests/ipv6.rs b/src/iface/interface/tests/ipv6.rs index ca1df2def..dec4bfd36 100644 --- a/src/iface/interface/tests/ipv6.rs +++ b/src/iface/interface/tests/ipv6.rs @@ -1568,3 +1568,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())); +} From 0385bad9332895292948ad3eb1ea2769af57d492 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Thu, 28 Nov 2024 01:05:06 +0100 Subject: [PATCH 6/6] iface: fix mld test. --- src/iface/interface/tests/ipv6.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/iface/interface/tests/ipv6.rs b/src/iface/interface/tests/ipv6.rs index dec4bfd36..5a75b5b8c 100644 --- a/src/iface/interface/tests/ipv6.rs +++ b/src/iface/interface/tests/ipv6.rs @@ -1420,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 @@ -1439,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]),