浏览代码

Merge pull request #914 from lucasvr/lucas/multicastv6

Implement `join/leave_multicast_group()` for IPv6
Thibaut Vandervelden 1 年之前
父节点
当前提交
35bb01afe3

+ 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,proto-rpl,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,proto-rpl,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)

+ 61 - 7
src/iface/interface/igmp.rs

@@ -8,8 +8,8 @@ pub enum MulticastError {
     Exhausted,
     /// The table of joined multicast groups is already full.
     GroupTableFull,
-    /// IPv6 multicast is not yet supported.
-    Ipv6NotSupported,
+    /// Cannot join/leave the given multicast group.
+    Unaddressable,
 }
 
 impl core::fmt::Display for MulticastError {
@@ -17,7 +17,7 @@ impl core::fmt::Display for MulticastError {
         match self {
             MulticastError::Exhausted => write!(f, "Exhausted"),
             MulticastError::GroupTableFull => write!(f, "GroupTableFull"),
-            MulticastError::Ipv6NotSupported => write!(f, "Ipv6NotSupported"),
+            MulticastError::Unaddressable => write!(f, "Unaddressable"),
         }
     }
 }
@@ -68,9 +68,39 @@ 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 report_record = &[MldAddressRecordRepr::new(
+                    MldRecordType::ChangeToInclude,
+                    addr,
+                )];
+                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(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),
         }
     }
 
@@ -110,9 +140,33 @@ impl Interface {
                     Ok(false)
                 }
             }
-            // Multicast is not yet implemented for other address families
+            #[cfg(feature = "proto-ipv6")]
+            IpAddress::Ipv6(addr) => {
+                let report_record = &[MldAddressRecordRepr::new(
+                    MldRecordType::ChangeToExclude,
+                    addr,
+                )];
+                let was_not_present = self.inner.ipv6_multicast_groups.remove(&addr).is_none();
+                if was_not_present {
+                    Ok(false)
+                } else if let Some(pkt) = self.inner.mldv2_report_packet(report_record) {
+                    // Send group leave packet
+                    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),
         }
     }
 

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

@@ -166,6 +166,23 @@ impl InterfaceInner {
         })
     }
 
+    /// 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
+                }
+            }
+        })
+    }
+
     pub(super) fn process_ipv6<'frame>(
         &mut self,
         sockets: &mut SocketSet,
@@ -238,7 +255,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 +490,52 @@ 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 mut hbh_repr = Ipv6HopByHopRepr::mldv2_router_alert();
+        hbh_repr.push_padn_option(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)),
+        ))
+    }
 }

+ 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,
         }

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

@@ -1169,3 +1169,126 @@ 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, 0x00fb]),
+        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). 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();
+
+        assert_eq!(hbh_repr.options.len(), 3);
+        assert_eq!(
+            hbh_repr.options[0],
+            Ipv6OptionRepr::Unknown {
+                type_: Ipv6OptionType::Unknown(IpProtocol::Icmpv6.into()),
+                length: 0,
+                data: &[],
+            }
+        );
+        assert_eq!(
+            hbh_repr.options[1],
+            Ipv6OptionRepr::RouterAlert(Ipv6OptionRouterAlert::MulticastListenerDiscovery)
+        );
+        assert_eq!(hbh_repr.options[2], Ipv6OptionRepr::PadN(0));
+
+        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: &[],
+            }
+        );
+
+        iface
+            .leave_multicast_group(&mut device, group_addr, timestamp)
+            .unwrap();
+        assert!(!iface.has_multicast_group(group_addr));
+    }
+}

+ 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))]

+ 2 - 0
src/socket/dns.rs

@@ -20,11 +20,13 @@ const MAX_RETRANSMIT_DELAY: Duration = Duration::from_millis(10_000);
 const RETRANSMIT_TIMEOUT: Duration = Duration::from_millis(10_000); // Should generally be 2-10 secs
 
 #[cfg(feature = "proto-ipv6")]
+#[allow(unused)]
 const MDNS_IPV6_ADDR: IpAddress = IpAddress::Ipv6(crate::wire::Ipv6Address([
     0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb,
 ]));
 
 #[cfg(feature = "proto-ipv4")]
+#[allow(unused)]
 const MDNS_IPV4_ADDR: IpAddress = IpAddress::Ipv4(crate::wire::Ipv4Address([224, 0, 0, 251]));
 
 /// Error returned by [`Socket::start_query`]

+ 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

+ 19 - 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,22 @@ impl<'a> Repr<'a> {
             buffer = &mut buffer[opt.buffer_len()..];
         }
     }
+
+    /// The hop-by-hop header containing a MLDv2 router alert option
+    pub fn mldv2_router_alert() -> Self {
+        let mut options = Vec::new();
+        options
+            .push(Ipv6OptionRepr::RouterAlert(
+                RouterAlert::MulticastListenerDiscovery,
+            ))
+            .unwrap();
+        Self { options }
+    }
+
+    /// Append a PadN option to the vector of hop-by-hop options
+    pub fn push_padn_option(&mut self, n: u8) {
+        self.options.push(Ipv6OptionRepr::PadN(n)).unwrap();
+    }
 }
 
 #[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}"),
             }
         }

+ 65 - 0
src/wire/mld.rs

@@ -294,6 +294,58 @@ 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> {
+    /// Create a new MLDv2 address record representation with an empty payload.
+    pub const fn new(record_type: RecordType, mcast_addr: Ipv6Address) -> Self {
+        Self {
+            record_type,
+            aux_data_len: 0,
+            num_srcs: 0,
+            mcast_addr,
+            payload: &[],
+        }
+    }
+
+    /// 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);
+    }
+}
+
 /// A high-level representation of an MLDv2 packet header.
 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
 #[cfg_attr(feature = "defmt", derive(defmt::Format))]
@@ -311,6 +363,7 @@ pub enum Repr<'a> {
         nr_mcast_addr_rcrds: u16,
         data: &'a [u8],
     },
+    ReportRecordReprs(&'a [AddressRecordRepr<'a>]),
 }
 
 impl<'a> Repr<'a> {
@@ -343,6 +396,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 +440,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};