diff --git a/Cargo.toml b/Cargo.toml index a8ad41200..12fe1f305 100644 --- a/Cargo.toml +++ b/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"] diff --git a/README.md b/README.md index 9642fdc09..b34eef9fd 100644 --- a/README.md +++ b/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 diff --git a/build.rs b/build.rs index 54662ed52..1ee723eb7 100644 --- a/build.rs +++ b/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), diff --git a/ci.sh b/ci.sh index ec20cc70e..9c27561ae 100755 --- a/ci.sh +++ b/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" ) diff --git a/examples/multicast6.rs b/examples/multicast6.rs new file mode 100644 index 000000000..814c4fe1e --- /dev/null +++ b/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_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"); + } +} diff --git a/gen_config.py b/gen_config.py index 25691929e..e33f74606 100644 --- a/gen_config.py +++ b/gen_config.py @@ -36,7 +36,7 @@ def feature(name, default, min, max, pow2=None): 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) diff --git a/src/iface/interface/igmp.rs b/src/iface/interface/igmp.rs index 7d339b2a5..fcfea32d2 100644 --- a/src/iface/interface/igmp.rs +++ b/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), } } diff --git a/src/iface/interface/ipv6.rs b/src/iface/interface/ipv6.rs index 490fdee15..608ba8310 100644 --- a/src/iface/interface/ipv6.rs +++ b/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 { + 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> { + // 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::(); + + // 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)), + )) + } } diff --git a/src/iface/interface/mod.rs b/src/iface/interface/mod.rs index 00f46d07d..de5178b6f 100644 --- a/src/iface/interface/mod.rs +++ b/src/iface/interface/mod.rs @@ -114,6 +114,8 @@ pub struct InterfaceInner { routes: Routes, #[cfg(feature = "proto-igmp")] ipv4_multicast_groups: LinearMap, + #[cfg(feature = "proto-ipv6")] + ipv6_multicast_groups: LinearMap, /// 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, } diff --git a/src/iface/interface/tests/ipv6.rs b/src/iface/interface/tests/ipv6.rs index f7debda9f..34b9a08a6 100644 --- a/src/iface/interface/tests/ipv6.rs +++ b/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>> { + 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::>() + } + + 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)); + } +} diff --git a/src/iface/packet.rs b/src/iface/packet.rs index b586d75e8..13f24256e 100644 --- a/src/iface/packet.rs +++ b/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")] diff --git a/src/lib.rs b/src/lib.rs index 040ff5749..3d5e46c9e 100644 --- a/src/lib.rs +++ b/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))] diff --git a/src/socket/dns.rs b/src/socket/dns.rs index 610d5c6b4..3b9fb6555 100644 --- a/src/socket/dns.rs +++ b/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`] diff --git a/src/wire/ipv6.rs b/src/wire/ipv6.rs index a436cc466..494128de4 100644 --- a/src/wire/ipv6.rs +++ b/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 diff --git a/src/wire/ipv6hbh.rs b/src/wire/ipv6hbh.rs index 9fa33a3af..8d6bb31da 100644 --- a/src/wire/ipv6hbh.rs +++ b/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, { crate::config::IPV6_HBH_MAX_OPTIONS }>, + pub options: Vec, { 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)] diff --git a/src/wire/ipv6option.rs b/src/wire/ipv6option.rs index 21024ae9c..998df887a 100644 --- a/src/wire/ipv6option.rs +++ b/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}"), } } diff --git a/src/wire/mld.rs b/src/wire/mld.rs index 6f447bd07..595006019 100644 --- a/src/wire/mld.rs +++ b/src/wire/mld.rs @@ -294,6 +294,58 @@ impl + AsMut<[u8]>> AddressRecord { } } +/// 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(record: &AddressRecord<&'a T>) -> Result + 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 + AsMut<[u8]>>(&self, record: &mut AddressRecord) { + 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()..]; + } + } } } } diff --git a/src/wire/mod.rs b/src/wire/mod.rs index e73e2305f..1ce6596e8 100644 --- a/src/wire/mod.rs +++ b/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};