Ver Fonte

Update PR #602: implement `join_multicast_group()` for IPv6

This patch rebases @jgallagher's to the tip of the main branch,
adding support for IPv6 multicast groups. As in the original PR,
it is only possible to join groups by sending an initial MLDv2
Report packet. It is not yet possible to resend the change report,
leave groups, respond to queries, etc.
Lucas C. Villa Real há 1 ano atrás
pai
commit
6c06cd9f8b

+ 6 - 2
Cargo.toml

@@ -218,10 +218,10 @@ reassembly-buffer-count-8 = []
 reassembly-buffer-count-16 = []
 reassembly-buffer-count-32 = []
 
-ipv6-hbh-max-options-1 = [] # Default
+ipv6-hbh-max-options-1 = []
 ipv6-hbh-max-options-2 = []
 ipv6-hbh-max-options-3 = []
-ipv6-hbh-max-options-4 = []
+ipv6-hbh-max-options-4 = [] # Default
 ipv6-hbh-max-options-8 = []
 ipv6-hbh-max-options-16 = []
 ipv6-hbh-max-options-32 = []
@@ -296,6 +296,10 @@ required-features = ["log", "medium-ethernet", "proto-ipv4", "socket-tcp"]
 name = "multicast"
 required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "proto-igmp", "socket-udp"]
 
+[[example]]
+name = "multicast6"
+required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv6", "socket-udp"]
+
 [[example]]
 name = "benchmark"
 required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "socket-raw", "socket-udp"]

+ 1 - 1
README.md

@@ -286,7 +286,7 @@ Maximum length of DNS names that can be queried. Default: 255.
 
 ### IPV6_HBH_MAX_OPTIONS
 
-The maximum amount of parsed options the IPv6 Hop-by-Hop header can hold. Default: 1.
+The maximum amount of parsed options the IPv6 Hop-by-Hop header can hold. Default: 4.
 
 ## Hosted usage examples
 

+ 1 - 1
build.rs

@@ -15,7 +15,7 @@ static CONFIGS: &[(&str, usize)] = &[
     ("ASSEMBLER_MAX_SEGMENT_COUNT", 4),
     ("REASSEMBLY_BUFFER_SIZE", 1500),
     ("REASSEMBLY_BUFFER_COUNT", 1),
-    ("IPV6_HBH_MAX_OPTIONS", 1),
+    ("IPV6_HBH_MAX_OPTIONS", 4),
     ("DNS_MAX_RESULT_COUNT", 1),
     ("DNS_MAX_SERVER_COUNT", 1),
     ("DNS_MAX_NAME_SIZE", 255),

+ 2 - 2
ci.sh

@@ -21,7 +21,7 @@ FEATURES_TEST=(
     "std,medium-ethernet,proto-ipv4,proto-igmp,socket-raw,socket-dns"
     "std,medium-ethernet,proto-ipv4,socket-udp,socket-tcp,socket-dns"
     "std,medium-ethernet,proto-ipv4,proto-dhcpv4,socket-udp"
-    "std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv6,socket-udp,socket-dns"
+    "std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv6,proto-igmp,socket-udp,socket-dns"
     "std,medium-ethernet,proto-ipv6,socket-tcp"
     "std,medium-ethernet,medium-ip,proto-ipv4,socket-icmp,socket-tcp"
     "std,medium-ip,proto-ipv6,socket-icmp,socket-tcp"
@@ -29,7 +29,7 @@ FEATURES_TEST=(
     "std,medium-ieee802154,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp"
     "std,medium-ieee802154,proto-rpl,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp"
     "std,medium-ip,proto-ipv4,proto-ipv6,socket-tcp,socket-udp"
-    "std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv4,proto-ipv6,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
+    "std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv4,proto-ipv6,proto-igmp,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
     "std,medium-ieee802154,medium-ip,proto-ipv4,socket-raw"
     "std,medium-ethernet,proto-ipv4,proto-ipsec,socket-raw"
 )

+ 90 - 0
examples/multicast6.rs

@@ -0,0 +1,90 @@
+mod utils;
+
+use std::os::unix::io::AsRawFd;
+
+use smoltcp::iface::{Config, Interface, SocketSet};
+use smoltcp::phy::wait as phy_wait;
+use smoltcp::phy::{Device, Medium};
+use smoltcp::socket::udp;
+use smoltcp::time::Instant;
+use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, Ipv6Address};
+
+// Note: If testing with a tap interface in linux, you may need to specify the
+// interface index when addressing. E.g.,
+//
+// ```
+// ncat -u ff02::1234%tap0 8123
+// ```
+//
+// will send packets to the multicast group we join below on tap0.
+
+const PORT: u16 = 8123;
+const GROUP: [u16; 8] = [0xff02, 0, 0, 0, 0, 0, 0, 0x1234];
+const LOCAL_ADDR: [u16; 8] = [0xfe80, 0, 0, 0, 0, 0, 0, 0x101];
+const ROUTER_ADDR: [u16; 8] = [0xfe80, 0, 0, 0, 0, 0, 0, 0x100];
+
+fn main() {
+    utils::setup_logging("warn");
+
+    let (mut opts, mut free) = utils::create_options();
+    utils::add_tuntap_options(&mut opts, &mut free);
+    utils::add_middleware_options(&mut opts, &mut free);
+
+    let mut matches = utils::parse_options(&opts, free);
+    let device = utils::parse_tuntap_options(&mut matches);
+    let fd = device.as_raw_fd();
+    let mut device =
+        utils::parse_middleware_options(&mut matches, device, /*loopback=*/ false);
+
+    // Create interface
+    let local_addr = Ipv6Address::from_parts(&LOCAL_ADDR);
+    let ethernet_addr = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]);
+    let mut config = match device.capabilities().medium {
+        Medium::Ethernet => Config::new(ethernet_addr.into()),
+        Medium::Ip => Config::new(smoltcp::wire::HardwareAddress::Ip),
+        Medium::Ieee802154 => todo!(),
+    };
+    config.random_seed = rand::random();
+
+    let mut iface = Interface::new(config, &mut device, Instant::now());
+    iface.update_ip_addrs(|ip_addrs| {
+        ip_addrs
+            .push(IpCidr::new(IpAddress::from(local_addr), 64))
+            .unwrap();
+    });
+    iface
+        .routes_mut()
+        .add_default_ipv6_route(Ipv6Address::from_parts(&ROUTER_ADDR))
+        .unwrap();
+
+    // Create sockets
+    let mut sockets = SocketSet::new(vec![]);
+    let udp_rx_buffer = udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY; 4], vec![0; 1024]);
+    let udp_tx_buffer = udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY], vec![0; 0]);
+    let udp_socket = udp::Socket::new(udp_rx_buffer, udp_tx_buffer);
+    let udp_handle = sockets.add(udp_socket);
+
+    // Join a multicast group
+    iface
+        .join_multicast_group(&mut device, Ipv6Address::from_parts(&GROUP), Instant::now())
+        .unwrap();
+
+    loop {
+        let timestamp = Instant::now();
+        iface.poll(timestamp, &mut device, &mut sockets);
+
+        let socket = sockets.get_mut::<udp::Socket>(udp_handle);
+        if !socket.is_open() {
+            socket.bind(PORT).unwrap()
+        }
+
+        if socket.can_recv() {
+            socket
+                .recv()
+                .map(|(data, sender)| println!("traffic: {} UDP bytes from {}", data.len(), sender))
+                .unwrap_or_else(|e| println!("Recv UDP error: {:?}", e));
+        }
+
+        phy_wait(fd, iface.poll_delay(timestamp, &sockets)).expect("wait error");
+    }
+}

+ 1 - 1
gen_config.py

@@ -36,7 +36,7 @@ feature("fragmentation_buffer_size", default=1500, min=256, max=65536, pow2=True
 feature("assembler_max_segment_count", default=4, min=1, max=32, pow2=4)
 feature("reassembly_buffer_size", default=1500, min=256, max=65536, pow2=True)
 feature("reassembly_buffer_count", default=1, min=1, max=32, pow2=4)
-feature("ipv6_hbh_max_options", default=1, min=1, max=32, pow2=4)
+feature("ipv6_hbh_max_options", default=4, min=1, max=32, pow2=4)
 feature("dns_max_result_count", default=1, min=1, max=32, pow2=4)
 feature("dns_max_server_count", default=1, min=1, max=32, pow2=4)
 feature("dns_max_name_size", default=255, min=64, max=255, pow2=True)

+ 39 - 2
src/iface/interface/igmp.rs

@@ -10,6 +10,8 @@ pub enum MulticastError {
     GroupTableFull,
     /// IPv6 multicast is not yet supported.
     Ipv6NotSupported,
+    /// Cannot join/leave the given multicast group.
+    Unaddressable,
 }
 
 impl core::fmt::Display for MulticastError {
@@ -18,6 +20,7 @@ impl core::fmt::Display for MulticastError {
             MulticastError::Exhausted => write!(f, "Exhausted"),
             MulticastError::GroupTableFull => write!(f, "GroupTableFull"),
             MulticastError::Ipv6NotSupported => write!(f, "Ipv6NotSupported"),
+            MulticastError::Unaddressable => write!(f, "Unaddressable"),
         }
     }
 }
@@ -68,9 +71,43 @@ impl Interface {
                     Ok(false)
                 }
             }
-            // Multicast is not yet implemented for other address families
+            #[cfg(feature = "proto-ipv6")]
+            IpAddress::Ipv6(addr) => {
+                // Build report packet containing this new address
+                let initial_report_record: &[MldAddressRecordRepr] = &[MldAddressRecordRepr {
+                    num_srcs: 0,
+                    mcast_addr: addr,
+                    record_type: MldRecordType::ChangeToInclude,
+                    aux_data_len: 0,
+                    payload: &[],
+                }];
+
+                let is_not_new = self
+                    .inner
+                    .ipv6_multicast_groups
+                    .insert(addr, ())
+                    .map_err(|_| MulticastError::GroupTableFull)?
+                    .is_some();
+                if is_not_new {
+                    Ok(false)
+                } else if let Some(pkt) = self.inner.mldv2_report_packet(initial_report_record) {
+                    // Send initial membership report
+                    let tx_token = device
+                        .transmit(timestamp)
+                        .ok_or(MulticastError::Exhausted)?;
+
+                    // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery.
+                    self.inner
+                        .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter)
+                        .unwrap();
+
+                    Ok(true)
+                } else {
+                    Ok(false)
+                }
+            }
             #[allow(unreachable_patterns)]
-            _ => Err(MulticastError::Ipv6NotSupported),
+            _ => Err(MulticastError::Unaddressable),
         }
     }
 

+ 65 - 1
src/iface/interface/ipv6.rs

@@ -238,7 +238,8 @@ impl InterfaceInner {
 
         for opt_repr in &hbh_repr.options {
             match opt_repr {
-                Ipv6OptionRepr::Pad1 | Ipv6OptionRepr::PadN(_) => (),
+                Ipv6OptionRepr::Pad1 | Ipv6OptionRepr::PadN(_) | Ipv6OptionRepr::RouterAlert(_) => {
+                }
                 #[cfg(feature = "proto-rpl")]
                 Ipv6OptionRepr::Rpl(_) => {}
 
@@ -472,4 +473,67 @@ impl InterfaceInner {
             IpPayload::Icmpv6(icmp_repr),
         ))
     }
+
+    pub(super) fn mldv2_report_packet<'any>(
+        &self,
+        records: &'any [MldAddressRecordRepr<'any>],
+    ) -> Option<Packet<'any>> {
+        // Per [RFC 3810 § 5.2.13], source addresses must be link-local, falling
+        // back to the unspecified address if we haven't acquired one.
+        // [RFC 3810 § 5.2.13]: https://tools.ietf.org/html/rfc3810#section-5.2.13
+        let src_addr = self
+            .link_local_ipv6_address()
+            .unwrap_or(Ipv6Address::UNSPECIFIED);
+
+        // Per [RFC 3810 § 5.2.14], all MLDv2 reports are sent to ff02::16.
+        // [RFC 3810 § 5.2.14]: https://tools.ietf.org/html/rfc3810#section-5.2.14
+        let dst_addr = Ipv6Address::LINK_LOCAL_ALL_MLDV2_ROUTERS;
+
+        // Create a dummy IPv6 extension header so we can calculate the total length of the packet.
+        // The actual extension header will be created later by Packet::emit_payload().
+        let dummy_ext_hdr = Ipv6ExtHeaderRepr {
+            next_header: IpProtocol::Unknown(0),
+            length: 0,
+            data: &[],
+        };
+
+        let hbh_repr = Ipv6HopByHopRepr::mldv2_router_alert(0);
+        let mld_repr = MldRepr::ReportRecordReprs(records);
+        let records_len = records
+            .iter()
+            .map(MldAddressRecordRepr::buffer_len)
+            .sum::<usize>();
+
+        // All MLDv2 messages must be sent with an IPv6 Hop limit of 1.
+        Some(Packet::new_ipv6(
+            Ipv6Repr {
+                src_addr,
+                dst_addr,
+                next_header: IpProtocol::HopByHop,
+                payload_len: dummy_ext_hdr.header_len()
+                    + hbh_repr.buffer_len()
+                    + mld_repr.buffer_len()
+                    + records_len,
+                hop_limit: 1,
+            },
+            IpPayload::HopByHopIcmpv6(hbh_repr, Icmpv6Repr::Mld(mld_repr)),
+        ))
+    }
+
+    /// Get the first link-local IPv6 address of the interface, if present.
+    fn link_local_ipv6_address(&self) -> Option<Ipv6Address> {
+        self.ip_addrs.iter().find_map(|addr| match *addr {
+            #[cfg(feature = "proto-ipv4")]
+            IpCidr::Ipv4(_) => None,
+            #[cfg(feature = "proto-ipv6")]
+            IpCidr::Ipv6(cidr) => {
+                let addr = cidr.address();
+                if addr.is_link_local() {
+                    Some(addr)
+                } else {
+                    None
+                }
+            }
+        })
+    }
 }

+ 9 - 3
src/iface/interface/mod.rs

@@ -114,6 +114,8 @@ pub struct InterfaceInner {
     routes: Routes,
     #[cfg(feature = "proto-igmp")]
     ipv4_multicast_groups: LinearMap<Ipv4Address, (), IFACE_MAX_MULTICAST_GROUP_COUNT>,
+    #[cfg(feature = "proto-ipv6")]
+    ipv6_multicast_groups: LinearMap<Ipv6Address, (), IFACE_MAX_MULTICAST_GROUP_COUNT>,
     /// When to report for (all or) the next multicast group membership via IGMP
     #[cfg(feature = "proto-igmp")]
     igmp_report_state: IgmpReportState,
@@ -228,6 +230,8 @@ impl Interface {
                 neighbor_cache: NeighborCache::new(),
                 #[cfg(feature = "proto-igmp")]
                 ipv4_multicast_groups: LinearMap::new(),
+                #[cfg(feature = "proto-ipv6")]
+                ipv6_multicast_groups: LinearMap::new(),
                 #[cfg(feature = "proto-igmp")]
                 igmp_report_state: IgmpReportState::Inactive,
                 #[cfg(feature = "medium-ieee802154")]
@@ -771,11 +775,13 @@ impl InterfaceInner {
                     || self.ipv4_multicast_groups.get(&key).is_some()
             }
             #[cfg(feature = "proto-ipv6")]
-            IpAddress::Ipv6(Ipv6Address::LINK_LOCAL_ALL_NODES) => true,
+            IpAddress::Ipv6(key) => {
+                key == Ipv6Address::LINK_LOCAL_ALL_NODES
+                    || self.has_solicited_node(key)
+                    || self.ipv6_multicast_groups.get(&key).is_some()
+            }
             #[cfg(feature = "proto-rpl")]
             IpAddress::Ipv6(Ipv6Address::LINK_LOCAL_ALL_RPL_NODES) => true,
-            #[cfg(feature = "proto-ipv6")]
-            IpAddress::Ipv6(addr) => self.has_solicited_node(addr),
             #[allow(unreachable_patterns)]
             _ => false,
         }

+ 118 - 0
src/iface/interface/tests/ipv6.rs

@@ -1169,3 +1169,121 @@ fn get_source_address_empty_interface() {
         Ipv6Address::LOOPBACK
     );
 }
+
+#[rstest]
+#[case(Medium::Ip)]
+#[cfg(feature = "medium-ip")]
+#[case(Medium::Ethernet)]
+#[cfg(feature = "medium-ethernet")]
+fn test_join_ipv6_multicast_group(#[case] medium: Medium) {
+    fn recv_icmpv6(
+        device: &mut crate::tests::TestingDevice,
+        timestamp: Instant,
+    ) -> std::vec::Vec<Ipv6Packet<std::vec::Vec<u8>>> {
+        let caps = device.capabilities();
+        recv_all(device, timestamp)
+            .iter()
+            .filter_map(|frame| {
+                let ipv6_packet = match caps.medium {
+                    #[cfg(feature = "medium-ethernet")]
+                    Medium::Ethernet => {
+                        let eth_frame = EthernetFrame::new_checked(frame).ok()?;
+                        Ipv6Packet::new_checked(eth_frame.payload()).ok()?
+                    }
+                    #[cfg(feature = "medium-ip")]
+                    Medium::Ip => Ipv6Packet::new_checked(&frame[..]).ok()?,
+                    #[cfg(feature = "medium-ieee802154")]
+                    Medium::Ieee802154 => todo!(),
+                };
+                let buf = ipv6_packet.into_inner().to_vec();
+                Some(Ipv6Packet::new_unchecked(buf))
+            })
+            .collect::<std::vec::Vec<_>>()
+    }
+
+    let (mut iface, _sockets, mut device) = setup(medium);
+
+    let groups = [
+        Ipv6Address::from_parts(&[0xff05, 0, 0, 0, 0, 0, 0, 0x0001]),
+        Ipv6Address::from_parts(&[0xff0e, 0, 0, 0, 0, 0, 0, 0x0017]),
+    ];
+
+    let timestamp = Instant::from_millis(0);
+
+    for &group in &groups {
+        iface
+            .join_multicast_group(&mut device, group, timestamp)
+            .unwrap();
+        assert!(iface.has_multicast_group(group));
+    }
+    assert!(iface.has_multicast_group(Ipv6Address::LINK_LOCAL_ALL_NODES));
+
+    let reports = recv_icmpv6(&mut device, timestamp);
+    assert_eq!(reports.len(), 2);
+
+    let caps = device.capabilities();
+    let checksum_caps = &caps.checksum;
+    for (&group_addr, ipv6_packet) in groups.iter().zip(reports) {
+        let buf = ipv6_packet.into_inner();
+        let ipv6_packet = Ipv6Packet::new_unchecked(buf.as_slice());
+
+        let _ipv6_repr = Ipv6Repr::parse(&ipv6_packet).unwrap();
+        let ip_payload = ipv6_packet.payload();
+
+        // The first 2 octets of this payload hold the next-header indicator and the
+        // Hop-by-Hop header length (in 8-octet words, minus 1), which we parse as an
+        // Unknown option. The remaining 6 octets hold the Hop-by-Hop PadN and Router
+        // Alert options.
+        let hbh_header = Ipv6HopByHopHeader::new_checked(&ip_payload[..8]).unwrap();
+        let hbh_repr = Ipv6HopByHopRepr::parse(&hbh_header).unwrap();
+
+        let mut expected_hbh_repr = Ipv6HopByHopRepr::mldv2_router_alert(0);
+        expected_hbh_repr
+            .options
+            .insert(
+                0,
+                Ipv6OptionRepr::Unknown {
+                    type_: Ipv6OptionType::Unknown(IpProtocol::Icmpv6.into()),
+                    length: 0,
+                    data: &[],
+                },
+            )
+            .unwrap();
+        assert_eq!(hbh_repr, expected_hbh_repr);
+
+        let icmpv6_packet =
+            Icmpv6Packet::new_checked(&ip_payload[hbh_repr.buffer_len()..]).unwrap();
+        let icmpv6_repr = Icmpv6Repr::parse(
+            &ipv6_packet.src_addr(),
+            &ipv6_packet.dst_addr(),
+            &icmpv6_packet,
+            checksum_caps,
+        )
+        .unwrap();
+
+        let record_data = match icmpv6_repr {
+            Icmpv6Repr::Mld(MldRepr::Report {
+                nr_mcast_addr_rcrds,
+                data,
+            }) => {
+                assert_eq!(nr_mcast_addr_rcrds, 1);
+                data
+            }
+            other => panic!("unexpected icmpv6_repr: {:?}", other),
+        };
+
+        let record = MldAddressRecord::new_checked(record_data).unwrap();
+        let record_repr = MldAddressRecordRepr::parse(&record).unwrap();
+
+        assert_eq!(
+            record_repr,
+            MldAddressRecordRepr {
+                num_srcs: 0,
+                mcast_addr: group_addr,
+                record_type: MldRecordType::ChangeToInclude,
+                aux_data_len: 0,
+                payload: &[],
+            }
+        );
+    }
+}

+ 35 - 0
src/iface/packet.rs

@@ -98,6 +98,37 @@ impl<'p> Packet<'p> {
                     &caps.checksum,
                 )
             }
+            #[cfg(feature = "proto-ipv6")]
+            IpPayload::HopByHopIcmpv6(hbh_repr, icmpv6_repr) => {
+                let ipv6_repr = match _ip_repr {
+                    #[cfg(feature = "proto-ipv4")]
+                    IpRepr::Ipv4(_) => unreachable!(),
+                    IpRepr::Ipv6(repr) => repr,
+                };
+
+                let ipv6_ext_hdr = Ipv6ExtHeaderRepr {
+                    next_header: IpProtocol::Icmpv6,
+                    length: 0,
+                    data: &[],
+                };
+                ipv6_ext_hdr.emit(&mut Ipv6ExtHeader::new_unchecked(
+                    &mut payload[..ipv6_ext_hdr.header_len()],
+                ));
+
+                let hbh_start = ipv6_ext_hdr.header_len();
+                let hbh_end = hbh_start + hbh_repr.buffer_len();
+                hbh_repr.emit(&mut Ipv6HopByHopHeader::new_unchecked(
+                    &mut payload[hbh_start..hbh_end],
+                ));
+
+                icmpv6_repr.emit(
+                    &ipv6_repr.src_addr,
+                    &ipv6_repr.dst_addr,
+                    &mut Icmpv6Packet::new_unchecked(&mut payload[hbh_end..]),
+                    &caps.checksum,
+                );
+            }
+
             #[cfg(feature = "socket-raw")]
             IpPayload::Raw(raw_packet) => payload.copy_from_slice(raw_packet),
             #[cfg(any(feature = "socket-udp", feature = "socket-dns"))]
@@ -180,6 +211,8 @@ pub(crate) enum IpPayload<'p> {
     Igmp(IgmpRepr),
     #[cfg(feature = "proto-ipv6")]
     Icmpv6(Icmpv6Repr<'p>),
+    #[cfg(feature = "proto-ipv6")]
+    HopByHopIcmpv6(Ipv6HopByHopRepr<'p>, Icmpv6Repr<'p>),
     #[cfg(feature = "socket-raw")]
     Raw(&'p [u8]),
     #[cfg(any(feature = "socket-udp", feature = "socket-dns"))]
@@ -200,6 +233,8 @@ impl<'p> IpPayload<'p> {
             Self::Dhcpv4(..) => unreachable!(),
             #[cfg(feature = "proto-ipv6")]
             Self::Icmpv6(_) => SixlowpanNextHeader::Uncompressed(IpProtocol::Icmpv6),
+            #[cfg(feature = "proto-ipv6")]
+            Self::HopByHopIcmpv6(_, _) => unreachable!(),
             #[cfg(feature = "proto-igmp")]
             Self::Igmp(_) => unreachable!(),
             #[cfg(feature = "socket-tcp")]

+ 1 - 1
src/lib.rs

@@ -146,7 +146,7 @@ pub mod config {
     pub const REASSEMBLY_BUFFER_SIZE: usize = 1500;
     pub const RPL_RELATIONS_BUFFER_COUNT: usize = 16;
     pub const RPL_PARENTS_BUFFER_COUNT: usize = 8;
-    pub const IPV6_HBH_MAX_OPTIONS: usize = 2;
+    pub const IPV6_HBH_MAX_OPTIONS: usize = 4;
 }
 
 #[cfg(not(test))]

+ 8 - 0
src/wire/ipv6.rs

@@ -87,6 +87,14 @@ impl Address {
         0x02,
     ]);
 
+    /// The link-local [all MLVDv2-capable routers multicast address].
+    ///
+    /// [all MLVDv2-capable routers multicast address]: https://tools.ietf.org/html/rfc3810#section-11
+    pub const LINK_LOCAL_ALL_MLDV2_ROUTERS: Address = Address([
+        0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x16,
+    ]);
+
     /// The link-local [all RPL nodes multicast address].
     ///
     /// [all RPL nodes multicast address]: https://www.rfc-editor.org/rfc/rfc6550.html#section-20.19

+ 15 - 2
src/wire/ipv6hbh.rs

@@ -1,5 +1,6 @@
 use super::{Error, Ipv6Option, Ipv6OptionRepr, Ipv6OptionsIterator, Result};
-
+use crate::config;
+use crate::wire::ipv6option::RouterAlert;
 use heapless::Vec;
 
 /// A read/write wrapper around an IPv6 Hop-by-Hop Header buffer.
@@ -61,7 +62,7 @@ impl<'a, T: AsRef<[u8]> + AsMut<[u8]> + ?Sized> Header<&'a mut T> {
 #[derive(Debug, PartialEq, Eq, Clone)]
 #[cfg_attr(feature = "defmt", derive(defmt::Format))]
 pub struct Repr<'a> {
-    pub options: heapless::Vec<Ipv6OptionRepr<'a>, { crate::config::IPV6_HBH_MAX_OPTIONS }>,
+    pub options: Vec<Ipv6OptionRepr<'a>, { config::IPV6_HBH_MAX_OPTIONS }>,
 }
 
 impl<'a> Repr<'a> {
@@ -105,6 +106,18 @@ impl<'a> Repr<'a> {
             buffer = &mut buffer[opt.buffer_len()..];
         }
     }
+
+    /// The hop-by-hop header containing a PadN and a MLDv2 router alert option.
+    pub fn mldv2_router_alert(n: u8) -> Self {
+        let mut options = Vec::new();
+        options.push(Ipv6OptionRepr::PadN(n)).unwrap();
+        options
+            .push(Ipv6OptionRepr::RouterAlert(
+                RouterAlert::MulticastListenerDiscovery,
+            ))
+            .unwrap();
+        Self { options }
+    }
 }
 
 #[cfg(test)]

+ 105 - 4
src/wire/ipv6option.rs

@@ -2,6 +2,7 @@ use super::{Error, Result};
 #[cfg(feature = "proto-rpl")]
 use super::{RplHopByHopPacket, RplHopByHopRepr};
 
+use byteorder::{ByteOrder, NetworkEndian};
 use core::fmt;
 
 enum_with_unknown! {
@@ -11,6 +12,8 @@ enum_with_unknown! {
         Pad1 = 0,
         /// Multiple bytes of padding
         PadN = 1,
+        /// Router Alert
+        RouterAlert = 5,
         /// RPL Option
         Rpl  = 0x63,
     }
@@ -22,11 +25,32 @@ impl fmt::Display for Type {
             Type::Pad1 => write!(f, "Pad1"),
             Type::PadN => write!(f, "PadN"),
             Type::Rpl => write!(f, "RPL"),
+            Type::RouterAlert => write!(f, "RouterAlert"),
             Type::Unknown(id) => write!(f, "{id}"),
         }
     }
 }
 
+enum_with_unknown! {
+    /// A high-level representation of an IPv6 Router Alert Header Option.
+    ///
+    /// Router Alert options always contain exactly one `u16`; see [RFC 2711 § 2.1].
+    ///
+    /// [RFC 2711 § 2.1]: https://tools.ietf.org/html/rfc2711#section-2.1
+    pub enum RouterAlert(u16) {
+        MulticastListenerDiscovery = 0,
+        Rsvp = 1,
+        ActiveNetworks = 2,
+    }
+}
+
+impl RouterAlert {
+    /// Per [RFC 2711 § 2.1], Router Alert options always have 2 bytes of data.
+    ///
+    /// [RFC 2711 § 2.1]: https://tools.ietf.org/html/rfc2711#section-2.1
+    pub const DATA_LEN: u8 = 2;
+}
+
 enum_with_unknown! {
     /// Action required when parsing the given IPv6 Extension
     /// Header Option Type fails
@@ -226,6 +250,7 @@ impl<'a, T: AsRef<[u8]> + ?Sized> fmt::Display for Ipv6Option<&'a T> {
 pub enum Repr<'a> {
     Pad1,
     PadN(u8),
+    RouterAlert(RouterAlert),
     #[cfg(feature = "proto-rpl")]
     Rpl(RplHopByHopRepr),
     Unknown {
@@ -245,7 +270,14 @@ impl<'a> Repr<'a> {
         match opt.option_type() {
             Type::Pad1 => Ok(Repr::Pad1),
             Type::PadN => Ok(Repr::PadN(opt.data_len())),
-
+            Type::RouterAlert => {
+                if opt.data_len() == RouterAlert::DATA_LEN {
+                    let raw = NetworkEndian::read_u16(opt.data());
+                    Ok(Repr::RouterAlert(RouterAlert::from(raw)))
+                } else {
+                    Err(Error)
+                }
+            }
             #[cfg(feature = "proto-rpl")]
             Type::Rpl => Ok(Repr::Rpl(RplHopByHopRepr::parse(
                 &RplHopByHopPacket::new_checked(opt.data())?,
@@ -270,6 +302,7 @@ impl<'a> Repr<'a> {
         match *self {
             Repr::Pad1 => 1,
             Repr::PadN(length) => field::DATA(length).end,
+            Repr::RouterAlert(_) => field::DATA(RouterAlert::DATA_LEN).end,
             #[cfg(feature = "proto-rpl")]
             Repr::Rpl(opt) => field::DATA(opt.buffer_len() as u8).end,
             Repr::Unknown { length, .. } => field::DATA(length).end,
@@ -288,6 +321,11 @@ impl<'a> Repr<'a> {
                     *x = 0
                 }
             }
+            Repr::RouterAlert(router_alert) => {
+                opt.set_option_type(Type::RouterAlert);
+                opt.set_data_len(RouterAlert::DATA_LEN);
+                NetworkEndian::write_u16(opt.data_mut(), router_alert.into());
+            }
             #[cfg(feature = "proto-rpl")]
             Repr::Rpl(rpl) => {
                 opt.set_option_type(Type::Rpl);
@@ -371,6 +409,7 @@ impl<'a> fmt::Display for Repr<'a> {
         match *self {
             Repr::Pad1 => write!(f, "{} ", Type::Pad1),
             Repr::PadN(len) => write!(f, "{} length={} ", Type::PadN, len),
+            Repr::RouterAlert(alert) => write!(f, "{} value={:?}", Type::RouterAlert, alert),
             #[cfg(feature = "proto-rpl")]
             Repr::Rpl(rpl) => write!(f, "{} {rpl}", Type::Rpl),
             Repr::Unknown { type_, length, .. } => write!(f, "{type_} length={length} "),
@@ -385,6 +424,10 @@ mod test {
     static IPV6OPTION_BYTES_PAD1: [u8; 1] = [0x0];
     static IPV6OPTION_BYTES_PADN: [u8; 3] = [0x1, 0x1, 0x0];
     static IPV6OPTION_BYTES_UNKNOWN: [u8; 5] = [0xff, 0x3, 0x0, 0x0, 0x0];
+    static IPV6OPTION_BYTES_ROUTER_ALERT_MLD: [u8; 4] = [0x05, 0x02, 0x00, 0x00];
+    static IPV6OPTION_BYTES_ROUTER_ALERT_RSVP: [u8; 4] = [0x05, 0x02, 0x00, 0x01];
+    static IPV6OPTION_BYTES_ROUTER_ALERT_ACTIVE_NETWORKS: [u8; 4] = [0x05, 0x02, 0x00, 0x02];
+    static IPV6OPTION_BYTES_ROUTER_ALERT_UNKNOWN: [u8; 4] = [0x05, 0x02, 0xbe, 0xef];
     #[cfg(feature = "proto-rpl")]
     static IPV6OPTION_BYTES_RPL: [u8; 6] = [0x63, 0x04, 0x00, 0x1e, 0x08, 0x00];
 
@@ -413,6 +456,17 @@ mod test {
             Ipv6Option::new_unchecked(&IPV6OPTION_BYTES_PADN).check_len()
         );
 
+        // router alert with truncated data
+        assert_eq!(
+            Err(Error),
+            Ipv6Option::new_unchecked(&IPV6OPTION_BYTES_ROUTER_ALERT_MLD[..3]).check_len()
+        );
+        // router alert
+        assert_eq!(
+            Ok(()),
+            Ipv6Option::new_unchecked(&IPV6OPTION_BYTES_ROUTER_ALERT_MLD).check_len()
+        );
+
         // unknown option type with truncated data
         assert_eq!(
             Err(Error),
@@ -469,6 +523,12 @@ mod test {
         assert_eq!(opt.data_len(), 7);
         assert_eq!(opt.data(), &[0, 0, 0, 0, 0, 0, 0]);
 
+        // router alert
+        let opt = Ipv6Option::new_unchecked(&IPV6OPTION_BYTES_ROUTER_ALERT_MLD);
+        assert_eq!(opt.option_type(), Type::RouterAlert);
+        assert_eq!(opt.data_len(), 2);
+        assert_eq!(opt.data(), &[0, 0]);
+
         // unrecognized option
         let bytes: [u8; 1] = [0xff];
         let opt = Ipv6Option::new_unchecked(&bytes);
@@ -500,6 +560,38 @@ mod test {
         assert_eq!(padn, Repr::PadN(1));
         assert_eq!(padn.buffer_len(), 3);
 
+        // router alert (MLD)
+        let opt = Ipv6Option::new_unchecked(&IPV6OPTION_BYTES_ROUTER_ALERT_MLD);
+        let alert = Repr::parse(&opt).unwrap();
+        assert_eq!(
+            alert,
+            Repr::RouterAlert(RouterAlert::MulticastListenerDiscovery)
+        );
+        assert_eq!(alert.buffer_len(), 4);
+
+        // router alert (RSVP)
+        let opt = Ipv6Option::new_unchecked(&IPV6OPTION_BYTES_ROUTER_ALERT_RSVP);
+        let alert = Repr::parse(&opt).unwrap();
+        assert_eq!(alert, Repr::RouterAlert(RouterAlert::Rsvp));
+        assert_eq!(alert.buffer_len(), 4);
+
+        // router alert (active networks)
+        let opt = Ipv6Option::new_unchecked(&IPV6OPTION_BYTES_ROUTER_ALERT_ACTIVE_NETWORKS);
+        let alert = Repr::parse(&opt).unwrap();
+        assert_eq!(alert, Repr::RouterAlert(RouterAlert::ActiveNetworks));
+        assert_eq!(alert.buffer_len(), 4);
+
+        // router alert (unknown)
+        let opt = Ipv6Option::new_unchecked(&IPV6OPTION_BYTES_ROUTER_ALERT_UNKNOWN);
+        let alert = Repr::parse(&opt).unwrap();
+        assert_eq!(alert, Repr::RouterAlert(RouterAlert::Unknown(0xbeef)));
+        assert_eq!(alert.buffer_len(), 4);
+
+        // router alert (incorrect data length)
+        let opt = Ipv6Option::new_unchecked(&[0x05, 0x03, 0x00, 0x00, 0x00]);
+        let alert = Repr::parse(&opt);
+        assert_eq!(alert, Err(Error));
+
         // unrecognized option type
         let data = [0u8; 3];
         let opt = Ipv6Option::new_unchecked(&IPV6OPTION_BYTES_UNKNOWN);
@@ -545,6 +637,12 @@ mod test {
         repr.emit(&mut opt);
         assert_eq!(opt.into_inner(), &IPV6OPTION_BYTES_PADN);
 
+        let repr = Repr::RouterAlert(RouterAlert::MulticastListenerDiscovery);
+        let mut bytes = [255u8; 4]; // don't assume bytes are initialized to zero
+        let mut opt = Ipv6Option::new_unchecked(&mut bytes);
+        repr.emit(&mut opt);
+        assert_eq!(opt.into_inner(), &IPV6OPTION_BYTES_ROUTER_ALERT_MLD);
+
         let data = [0u8; 3];
         let repr = Repr::Unknown {
             type_: Type::Unknown(255),
@@ -573,6 +671,8 @@ mod test {
         assert_eq!(failure_type, FailureType::Skip);
         failure_type = Type::PadN.into();
         assert_eq!(failure_type, FailureType::Skip);
+        failure_type = Type::RouterAlert.into();
+        assert_eq!(failure_type, FailureType::Skip);
         failure_type = Type::Unknown(0b01000001).into();
         assert_eq!(failure_type, FailureType::Discard);
         failure_type = Type::Unknown(0b10100000).into();
@@ -584,8 +684,8 @@ mod test {
     #[test]
     fn test_options_iter() {
         let options = [
-            0x00, 0x01, 0x01, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x11, 0x00, 0x01,
-            0x08, 0x00,
+            0x00, 0x01, 0x01, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x11, 0x00, 0x05,
+            0x02, 0x00, 0x01, 0x01, 0x08, 0x00,
         ];
 
         let iterator = Ipv6OptionsIterator::new(&options);
@@ -604,7 +704,8 @@ mod test {
                         ..
                     }),
                 ) => continue,
-                (6, Err(Error)) => continue,
+                (6, Ok(Repr::RouterAlert(RouterAlert::Rsvp))) => continue,
+                (7, Err(Error)) => continue,
                 (i, res) => panic!("Unexpected option `{res:?}` at index {i}"),
             }
         }

+ 59 - 0
src/wire/mld.rs

@@ -294,6 +294,52 @@ impl<T: AsRef<[u8]> + AsMut<[u8]>> AddressRecord<T> {
     }
 }
 
+/// A high level representation of an MLDv2 Listener Report Message Address Record.
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct AddressRecordRepr<'a> {
+    pub record_type: RecordType,
+    pub aux_data_len: u8,
+    pub num_srcs: u16,
+    pub mcast_addr: Ipv6Address,
+    pub payload: &'a [u8],
+}
+
+impl<'a> AddressRecordRepr<'a> {
+    /// Parse an MLDv2 address record and return a high-level representation.
+    pub fn parse<T>(record: &AddressRecord<&'a T>) -> Result<Self>
+    where
+        T: AsRef<[u8]> + ?Sized,
+    {
+        Ok(Self {
+            num_srcs: record.num_srcs(),
+            mcast_addr: record.mcast_addr(),
+            record_type: record.record_type(),
+            aux_data_len: record.aux_data_len(),
+            payload: record.payload(),
+        })
+    }
+
+    /// Return the length of a record that will be emitted from this high-level
+    /// representation, not including any payload data.
+    pub fn buffer_len(&self) -> usize {
+        field::RECORD_MCAST_ADDR.end
+    }
+
+    /// Emit a high-level representation into an MLDv2 address record.
+    pub fn emit<T: AsRef<[u8]> + AsMut<[u8]>>(&self, record: &mut AddressRecord<T>) {
+        record.set_record_type(self.record_type);
+        record.set_aux_data_len(self.aux_data_len);
+        record.set_num_srcs(self.num_srcs);
+        record.set_mcast_addr(self.mcast_addr);
+        let record_payload = record.payload_mut();
+        if self.payload.len() == record_payload.len() {
+            // TODO: handle the case where the payload sizes are different
+            record_payload.copy_from_slice(self.payload);
+        }
+    }
+}
+
 /// A high-level representation of an MLDv2 packet header.
 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
 #[cfg_attr(feature = "defmt", derive(defmt::Format))]
@@ -311,6 +357,7 @@ pub enum Repr<'a> {
         nr_mcast_addr_rcrds: u16,
         data: &'a [u8],
     },
+    ReportRecordReprs(&'a [AddressRecordRepr<'a>]),
 }
 
 impl<'a> Repr<'a> {
@@ -343,6 +390,7 @@ impl<'a> Repr<'a> {
         match self {
             Repr::Query { data, .. } => field::QUERY_NUM_SRCS.end + data.len(),
             Repr::Report { data, .. } => field::NR_MCAST_RCRDS.end + data.len(),
+            Repr::ReportRecordReprs(_data) => field::NR_MCAST_RCRDS.end,
         }
     }
 
@@ -386,6 +434,17 @@ impl<'a> Repr<'a> {
                 packet.set_nr_mcast_addr_rcrds(*nr_mcast_addr_rcrds);
                 packet.payload_mut().copy_from_slice(&data[..]);
             }
+            Repr::ReportRecordReprs(records) => {
+                packet.set_msg_type(Message::MldReport);
+                packet.set_msg_code(0);
+                packet.clear_reserved();
+                packet.set_nr_mcast_addr_rcrds(records.len() as u16);
+                let mut payload = packet.payload_mut();
+                for record in *records {
+                    record.emit(&mut AddressRecord::new_unchecked(&mut *payload));
+                    payload = &mut payload[record.buffer_len()..];
+                }
+            }
         }
     }
 }

+ 5 - 2
src/wire/mod.rs

@@ -201,7 +201,7 @@ pub use self::ipv6::{
 #[cfg(feature = "proto-ipv6")]
 pub use self::ipv6option::{
     FailureType as Ipv6OptionFailureType, Ipv6Option, Ipv6OptionsIterator, Repr as Ipv6OptionRepr,
-    Type as Ipv6OptionType,
+    RouterAlert as Ipv6OptionRouterAlert, Type as Ipv6OptionType,
 };
 
 #[cfg(feature = "proto-ipv6")]
@@ -256,7 +256,10 @@ pub use self::ndiscoption::{
 };
 
 #[cfg(feature = "proto-ipv6")]
-pub use self::mld::{AddressRecord as MldAddressRecord, Repr as MldRepr};
+pub use self::mld::{
+    AddressRecord as MldAddressRecord, AddressRecordRepr as MldAddressRecordRepr,
+    RecordType as MldRecordType, Repr as MldRepr,
+};
 
 pub use self::udp::{Packet as UdpPacket, Repr as UdpRepr, HEADER_LEN as UDP_HEADER_LEN};