diff --git a/.gitignore b/.gitignore index 41ca801d0..d3ffa03e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target Cargo.lock *.pcap +sim_logs/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index a8ad41200..3ac980893 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,11 @@ defmt = ["dep:defmt", "heapless/defmt-03"] "proto-ipsec-ah" = [] "proto-ipsec-esp" = [] +"rpl-mop-0" = ["proto-rpl"] +"rpl-mop-1" = ["proto-rpl"] +"rpl-mop-2" = ["proto-rpl"] +"rpl-mop-3" = ["proto-rpl"] + "socket" = [] "socket-raw" = ["socket"] "socket-udp" = ["socket"] @@ -96,7 +101,8 @@ default = [ "proto-ipv4", "proto-igmp", "proto-dhcpv4", "proto-ipv6", "proto-dns", "proto-ipv4-fragmentation", "proto-sixlowpan-fragmentation", "socket-raw", "socket-icmp", "socket-udp", "socket-tcp", "socket-dhcpv4", "socket-dns", "socket-mdns", - "packetmeta-id", "async" + "packetmeta-id", "async", + "rpl-mop-0", "rpl-mop-1", "rpl-mop-2", "rpl-mop-3" ] # Private features @@ -261,6 +267,13 @@ rpl-parents-buffer-count-8 = [] # Default rpl-parents-buffer-count-16 = [] rpl-parents-buffer-count-32 = [] +rpl-max-options-1 = [] +rpl-max-options-2 = [] # Default +rpl-max-options-4 = [] +rpl-max-options-8 = [] +rpl-max-options-16 = [] +rpl-max-options-32 = [] + # END AUTOGENERATED CONFIG FEATURES [[example]] @@ -316,5 +329,9 @@ required-features = ["std", "medium-ieee802154", "phy-raw_socket", "proto-sixlow name = "dns" required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "socket-dns"] +[[test]] +name = "rpl" +required-features = ["std", "medium-ieee802154", "proto-sixlowpan", "rpl-mop-0", "rpl-mop-1", "rpl-mop-2", "rpl-mop-3"] + [profile.release] debug = 2 diff --git a/build.rs b/build.rs index 54662ed52..fe6881d99 100644 --- a/build.rs +++ b/build.rs @@ -11,6 +11,7 @@ static CONFIGS: &[(&str, usize)] = &[ ("IFACE_MAX_SIXLOWPAN_ADDRESS_CONTEXT_COUNT", 4), ("IFACE_NEIGHBOR_CACHE_COUNT", 4), ("IFACE_MAX_ROUTE_COUNT", 2), + ("IFACE_MAX_MULTICAST_DUPLICATION_COUNT", 16), ("FRAGMENTATION_BUFFER_SIZE", 1500), ("ASSEMBLER_MAX_SEGMENT_COUNT", 4), ("REASSEMBLY_BUFFER_SIZE", 1500), @@ -21,6 +22,8 @@ static CONFIGS: &[(&str, usize)] = &[ ("DNS_MAX_NAME_SIZE", 255), ("RPL_RELATIONS_BUFFER_COUNT", 16), ("RPL_PARENTS_BUFFER_COUNT", 8), + ("RPL_MAX_OPTIONS", 2), + ("RPL_MAX_NEXT_HOP_PER_DESTINATION", 4), // END AUTOGENERATED CONFIG FEATURES ]; diff --git a/ci.sh b/ci.sh index ec20cc70e..91487f367 100755 --- a/ci.sh +++ b/ci.sh @@ -27,7 +27,10 @@ FEATURES_TEST=( "std,medium-ip,proto-ipv6,socket-icmp,socket-tcp" "std,medium-ieee802154,proto-sixlowpan,socket-udp" "std,medium-ieee802154,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp" - "std,medium-ieee802154,proto-rpl,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp" + "std,medium-ieee802154,rpl-mop-0,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp" + "std,medium-ieee802154,rpl-mop-1,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp" + "std,medium-ieee802154,rpl-mop-2,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp" + "std,medium-ieee802154,rpl-mop-3,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-ieee802154,medium-ip,proto-ipv4,socket-raw" diff --git a/examples/benchmark.rs b/examples/benchmark.rs index ad2c6e142..c122c3aff 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -12,6 +12,7 @@ use std::thread; use smoltcp::iface::{Config, Interface, SocketSet}; use smoltcp::phy::{wait as phy_wait, Device, Medium}; use smoltcp::socket::tcp; +use smoltcp::storage::PacketMetadata; use smoltcp::time::{Duration, Instant}; use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr}; @@ -97,7 +98,13 @@ fn main() { }; config.random_seed = rand::random(); - let mut iface = Interface::new(config, &mut device, Instant::now()); + let mut iface = Interface::new( + config, + &mut device, + vec![PacketMetadata::EMPTY; 16], + vec![0; 2048], + Instant::now(), + ); iface.update_ip_addrs(|ip_addrs| { ip_addrs .push(IpCidr::new(IpAddress::v4(192, 168, 69, 1), 24)) diff --git a/examples/client.rs b/examples/client.rs index c18c08ff7..7f085ff1a 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -1,6 +1,7 @@ mod utils; use log::debug; +use smoltcp::storage::PacketMetadata; use std::os::unix::io::AsRawFd; use std::str::{self, FromStr}; @@ -38,7 +39,13 @@ fn main() { }; config.random_seed = rand::random(); - let mut iface = Interface::new(config, &mut device, Instant::now()); + let mut iface = Interface::new( + config, + &mut device, + vec![PacketMetadata::EMPTY; 16], + vec![0; 2048], + Instant::now(), + ); iface.update_ip_addrs(|ip_addrs| { ip_addrs .push(IpCidr::new(IpAddress::v4(192, 168, 69, 1), 24)) @@ -68,7 +75,7 @@ fn main() { let socket = sockets.get_mut::(tcp_handle); socket - .connect(iface.context(), (address, port), 49500) + .connect(iface.context_mut(), (address, port), 49500) .unwrap(); let mut tcp_active = false; diff --git a/examples/dhcp_client.rs b/examples/dhcp_client.rs index 348e9676c..46711846d 100644 --- a/examples/dhcp_client.rs +++ b/examples/dhcp_client.rs @@ -2,6 +2,7 @@ mod utils; use log::*; +use smoltcp::storage::PacketMetadata; use std::os::unix::io::AsRawFd; use smoltcp::iface::{Config, Interface, SocketSet}; @@ -36,7 +37,13 @@ fn main() { Medium::Ieee802154 => todo!(), }; config.random_seed = rand::random(); - let mut iface = Interface::new(config, &mut device, Instant::now()); + let mut iface = Interface::new( + config, + &mut device, + vec![PacketMetadata::EMPTY; 16], + vec![0; 2048], + Instant::now(), + ); // Create sockets let mut dhcp_socket = dhcpv4::Socket::new(); diff --git a/examples/dns.rs b/examples/dns.rs index 977f40546..4a003d04f 100644 --- a/examples/dns.rs +++ b/examples/dns.rs @@ -4,6 +4,7 @@ use smoltcp::iface::{Config, Interface, SocketSet}; use smoltcp::phy::Device; use smoltcp::phy::{wait as phy_wait, Medium}; use smoltcp::socket::dns::{self, GetQueryResultError}; +use smoltcp::storage::PacketMetadata; use smoltcp::time::Instant; use smoltcp::wire::{DnsQueryType, EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv6Address}; use std::os::unix::io::AsRawFd; @@ -33,7 +34,13 @@ fn main() { }; config.random_seed = rand::random(); - let mut iface = Interface::new(config, &mut device, Instant::now()); + let mut iface = Interface::new( + config, + &mut device, + vec![PacketMetadata::EMPTY; 16], + vec![0; 2048], + Instant::now(), + ); iface.update_ip_addrs(|ip_addrs| { ip_addrs .push(IpCidr::new(IpAddress::v4(192, 168, 69, 1), 24)) @@ -66,7 +73,7 @@ fn main() { let socket = sockets.get_mut::(dns_handle); let query = socket - .start_query(iface.context(), name, DnsQueryType::A) + .start_query(iface.context_mut(), name, DnsQueryType::A) .unwrap(); loop { diff --git a/examples/httpclient.rs b/examples/httpclient.rs index 8f3a53aa7..f8ca2f9e8 100644 --- a/examples/httpclient.rs +++ b/examples/httpclient.rs @@ -1,6 +1,7 @@ mod utils; use log::debug; +use smoltcp::storage::PacketMetadata; use std::os::unix::io::AsRawFd; use std::str::{self, FromStr}; use url::Url; @@ -38,7 +39,13 @@ fn main() { }; config.random_seed = rand::random(); - let mut iface = Interface::new(config, &mut device, Instant::now()); + let mut iface = Interface::new( + config, + &mut device, + vec![PacketMetadata::EMPTY; 16], + vec![0; 2048], + Instant::now(), + ); iface.update_ip_addrs(|ip_addrs| { ip_addrs .push(IpCidr::new(IpAddress::v4(192, 168, 69, 1), 24)) @@ -79,7 +86,7 @@ fn main() { iface.poll(timestamp, &mut device, &mut sockets); let socket = sockets.get_mut::(tcp_handle); - let cx = iface.context(); + let cx = iface.context_mut(); state = match state { State::Connect if !socket.is_active() => { diff --git a/examples/loopback.rs b/examples/loopback.rs index 7ca95b188..393783da4 100644 --- a/examples/loopback.rs +++ b/examples/loopback.rs @@ -12,6 +12,7 @@ use log::{debug, error, info}; use smoltcp::iface::{Config, Interface, SocketSet}; use smoltcp::phy::{Device, Loopback, Medium}; use smoltcp::socket::tcp; +use smoltcp::storage::PacketMetadata; use smoltcp::time::{Duration, Instant}; use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr}; @@ -91,7 +92,17 @@ fn main() { Medium::Ieee802154 => todo!(), }; - let mut iface = Interface::new(config, &mut device, Instant::now()); + // Setup multicast queues + let mut metadata = [PacketMetadata::EMPTY; 16]; + let mut multicast_packets = [0; 2048]; + + let mut iface = Interface::new( + config, + &mut device, + &mut metadata[..], + &mut multicast_packets[..], + Instant::now(), + ); iface.update_ip_addrs(|ip_addrs| { ip_addrs .push(IpCidr::new(IpAddress::v4(127, 0, 0, 1), 8)) @@ -149,7 +160,7 @@ fn main() { } let mut socket = sockets.get_mut::(client_handle); - let cx = iface.context(); + let cx = iface.context_mut(); if !socket.is_open() { if !did_connect { debug!("connecting"); diff --git a/examples/multicast.rs b/examples/multicast.rs index ea89a2e93..4bc6816c2 100644 --- a/examples/multicast.rs +++ b/examples/multicast.rs @@ -5,6 +5,7 @@ use std::os::unix::io::AsRawFd; use smoltcp::iface::{Config, Interface, SocketSet}; use smoltcp::phy::{wait as phy_wait, Device, Medium}; use smoltcp::socket::{raw, udp}; +use smoltcp::storage::PacketMetadata; use smoltcp::time::Instant; use smoltcp::wire::{ EthernetAddress, IgmpPacket, IgmpRepr, IpAddress, IpCidr, IpProtocol, IpVersion, Ipv4Address, @@ -37,7 +38,13 @@ fn main() { }; config.random_seed = rand::random(); - let mut iface = Interface::new(config, &mut device, Instant::now()); + let mut iface = Interface::new( + config, + &mut device, + vec![PacketMetadata::EMPTY; 16], + vec![0; 2048], + Instant::now(), + ); iface.update_ip_addrs(|ip_addrs| { ip_addrs .push(IpCidr::new(IpAddress::v4(192, 168, 69, 1), 24)) diff --git a/examples/ping.rs b/examples/ping.rs index 29c6bcd4c..a8dbd3ad3 100644 --- a/examples/ping.rs +++ b/examples/ping.rs @@ -2,6 +2,7 @@ mod utils; use byteorder::{ByteOrder, NetworkEndian}; use smoltcp::iface::{Interface, SocketSet}; +use smoltcp::storage::PacketMetadata; use std::cmp; use std::collections::HashMap; use std::os::unix::io::AsRawFd; @@ -114,7 +115,13 @@ fn main() { }; config.random_seed = rand::random(); - let mut iface = Interface::new(config, &mut device, Instant::now()); + let mut iface = Interface::new( + config, + &mut device, + vec![PacketMetadata::EMPTY; 16], + vec![0; 2048], + Instant::now(), + ); iface.update_ip_addrs(|ip_addrs| { ip_addrs .push(IpCidr::new(IpAddress::v4(192, 168, 69, 1), 24)) diff --git a/examples/server.rs b/examples/server.rs index 33d95c5d5..8754ee1ee 100644 --- a/examples/server.rs +++ b/examples/server.rs @@ -1,6 +1,7 @@ mod utils; use log::debug; +use smoltcp::storage::PacketMetadata; use std::fmt::Write; use std::os::unix::io::AsRawFd; @@ -34,7 +35,13 @@ fn main() { config.random_seed = rand::random(); - let mut iface = Interface::new(config, &mut device, Instant::now()); + let mut iface = Interface::new( + config, + &mut device, + vec![PacketMetadata::EMPTY; 16], + vec![0; 2048], + Instant::now(), + ); iface.update_ip_addrs(|ip_addrs| { ip_addrs .push(IpCidr::new(IpAddress::v4(192, 168, 69, 1), 24)) diff --git a/examples/sixlowpan.rs b/examples/sixlowpan.rs index 0d9ec21d8..44b7b8f39 100644 --- a/examples/sixlowpan.rs +++ b/examples/sixlowpan.rs @@ -43,6 +43,7 @@ mod utils; use log::debug; +use smoltcp::storage::PacketMetadata; use std::os::unix::io::AsRawFd; use std::str; @@ -78,8 +79,18 @@ fn main() { }; config.random_seed = rand::random(); config.pan_id = Some(Ieee802154Pan(0xbeef)); + #[cfg(feature = "proto-rpl")] + { + config.rpl_config = None; + } - let mut iface = Interface::new(config, &mut device, Instant::now()); + let mut iface = Interface::new( + config, + &mut device, + vec![PacketMetadata::EMPTY; 16], + vec![0; 2048], + Instant::now(), + ); iface.update_ip_addrs(|ip_addrs| { ip_addrs .push(IpCidr::new( diff --git a/examples/sixlowpan_benchmark.rs b/examples/sixlowpan_benchmark.rs index 4e61491fe..cba50c25c 100644 --- a/examples/sixlowpan_benchmark.rs +++ b/examples/sixlowpan_benchmark.rs @@ -49,6 +49,7 @@ use std::str; use smoltcp::iface::{Config, Interface, SocketSet}; use smoltcp::phy::{wait as phy_wait, Device, Medium, RawSocket}; use smoltcp::socket::tcp; +use smoltcp::storage::PacketMetadata; use smoltcp::wire::{EthernetAddress, Ieee802154Address, Ieee802154Pan, IpAddress, IpCidr}; //For benchmark @@ -159,7 +160,13 @@ fn main() { config.random_seed = rand::random(); config.pan_id = Some(Ieee802154Pan(0xbeef)); - let mut iface = Interface::new(config, &mut device, Instant::now()); + let mut iface = Interface::new( + config, + &mut device, + vec![PacketMetadata::EMPTY; 16], + vec![0; 2048], + Instant::now(), + ); iface.update_ip_addrs(|ip_addrs| { ip_addrs .push(IpCidr::new( diff --git a/gen_config.py b/gen_config.py index 25691929e..d4bd4a7bc 100644 --- a/gen_config.py +++ b/gen_config.py @@ -42,6 +42,7 @@ def feature(name, default, min, max, pow2=None): feature("dns_max_name_size", default=255, min=64, max=255, pow2=True) feature("rpl_relations_buffer_count", default=16, min=1, max=128, pow2=True) feature("rpl_parents_buffer_count", default=8, min=2, max=32, pow2=True) +feature("rpl_max_options", default=2, min=1, max=32, pow2=True) # ========= Update Cargo.toml diff --git a/src/iface/interface/ethernet.rs b/src/iface/interface/ethernet.rs index 4d29faa11..50d14b1c9 100644 --- a/src/iface/interface/ethernet.rs +++ b/src/iface/interface/ethernet.rs @@ -7,6 +7,7 @@ impl InterfaceInner { meta: crate::phy::PacketMeta, frame: &'frame [u8], fragments: &'frame mut FragmentsBuffer, + multicast_queue: &mut PacketBuffer<'_, MulticastMetadata>, ) -> Option> { let eth_frame = check!(EthernetFrame::new_checked(frame)); @@ -31,8 +32,14 @@ impl InterfaceInner { #[cfg(feature = "proto-ipv6")] EthernetProtocol::Ipv6 => { let ipv6_packet = check!(Ipv6Packet::new_checked(eth_frame.payload())); - self.process_ipv6(sockets, meta, &ipv6_packet) - .map(EthernetPacket::Ip) + self.process_ipv6( + sockets, + meta, + &ipv6_packet, + Some(ð_frame.src_addr().into()), + multicast_queue, + ) + .map(EthernetPacket::Ip) } // Drop all other traffic. _ => None, diff --git a/src/iface/interface/ieee802154.rs b/src/iface/interface/ieee802154.rs index c053ec3dd..a616ed5a5 100644 --- a/src/iface/interface/ieee802154.rs +++ b/src/iface/interface/ieee802154.rs @@ -15,6 +15,7 @@ impl InterfaceInner { meta: PacketMeta, sixlowpan_payload: &'payload [u8], _fragments: &'output mut FragmentsBuffer, + multicast_queue: &mut PacketBuffer<'_, MulticastMetadata>, ) -> Option> { let ieee802154_frame = check!(Ieee802154Frame::new_checked(sixlowpan_payload)); @@ -38,10 +39,17 @@ impl InterfaceInner { return None; } + self.current_frame = Some(ieee802154_repr); + match ieee802154_frame.payload() { - Some(payload) => { - self.process_sixlowpan(sockets, meta, &ieee802154_repr, payload, _fragments) - } + Some(payload) => self.process_sixlowpan( + sockets, + meta, + &ieee802154_repr, + payload, + _fragments, + multicast_queue, + ), None => None, } } @@ -51,7 +59,7 @@ impl InterfaceInner { ll_dst_a: Ieee802154Address, tx_token: Tx, meta: PacketMeta, - packet: Packet, + packet: PacketV6, frag: &mut Fragmenter, ) { let ll_src_a = self.hardware_addr.ieee802154_or_panic(); diff --git a/src/iface/interface/igmp.rs b/src/iface/interface/igmp.rs index 7d339b2a5..137ce5f41 100644 --- a/src/iface/interface/igmp.rs +++ b/src/iface/interface/igmp.rs @@ -1,126 +1,6 @@ use super::*; -/// Error type for `join_multicast_group`, `leave_multicast_group`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum MulticastError { - /// The hardware device transmit buffer is full. Try again later. - Exhausted, - /// The table of joined multicast groups is already full. - GroupTableFull, - /// IPv6 multicast is not yet supported. - Ipv6NotSupported, -} - -impl core::fmt::Display for MulticastError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - match self { - MulticastError::Exhausted => write!(f, "Exhausted"), - MulticastError::GroupTableFull => write!(f, "GroupTableFull"), - MulticastError::Ipv6NotSupported => write!(f, "Ipv6NotSupported"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for MulticastError {} - -impl Interface { - /// Add an address to a list of subscribed multicast IP addresses. - /// - /// Returns `Ok(announce_sent)` if the address was added successfully, where `announce_sent` - /// indicates whether an initial immediate announcement has been sent. - pub fn join_multicast_group>( - &mut self, - device: &mut D, - addr: T, - timestamp: Instant, - ) -> Result - where - D: Device + ?Sized, - { - self.inner.now = timestamp; - - match addr.into() { - IpAddress::Ipv4(addr) => { - let is_not_new = self - .inner - .ipv4_multicast_groups - .insert(addr, ()) - .map_err(|_| MulticastError::GroupTableFull)? - .is_some(); - if is_not_new { - Ok(false) - } else if let Some(pkt) = self.inner.igmp_report_packet(IgmpVersion::Version2, addr) - { - // 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) - } - } - // Multicast is not yet implemented for other address families - #[allow(unreachable_patterns)] - _ => Err(MulticastError::Ipv6NotSupported), - } - } - - /// Remove an address from the subscribed multicast IP addresses. - /// - /// Returns `Ok(leave_sent)` if the address was removed successfully, where `leave_sent` - /// indicates whether an immediate leave packet has been sent. - pub fn leave_multicast_group>( - &mut self, - device: &mut D, - addr: T, - timestamp: Instant, - ) -> Result - where - D: Device + ?Sized, - { - self.inner.now = timestamp; - - match addr.into() { - IpAddress::Ipv4(addr) => { - let was_not_present = self.inner.ipv4_multicast_groups.remove(&addr).is_none(); - if was_not_present { - Ok(false) - } else if let Some(pkt) = self.inner.igmp_leave_packet(addr) { - // 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) - } - } - // Multicast is not yet implemented for other address families - #[allow(unreachable_patterns)] - _ => Err(MulticastError::Ipv6NotSupported), - } - } - - /// Check whether the interface listens to given destination multicast IP address. - pub fn has_multicast_group>(&self, addr: T) -> bool { - self.inner.has_multicast_group(addr) - } - +impl Interface<'_> { /// Depending on `igmp_report_state` and the therein contained /// timeouts, send IGMP membership reports. pub(crate) fn igmp_egress(&mut self, device: &mut D) -> bool @@ -138,7 +18,14 @@ impl Interface { if let Some(tx_token) = device.transmit(self.inner.now) { // 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) + .dispatch_ip( + tx_token, + PacketMeta::default(), + pkt, + None, + &mut self.fragmenter, + &mut self.multicast_queue, + ) .unwrap(); } else { return false; @@ -172,7 +59,9 @@ impl Interface { tx_token, PacketMeta::default(), pkt, + None, &mut self.fragmenter, + &mut self.multicast_queue, ) .unwrap(); } else { diff --git a/src/iface/interface/ipv4.rs b/src/iface/interface/ipv4.rs index 3a5a864ee..0892be17f 100644 --- a/src/iface/interface/ipv4.rs +++ b/src/iface/interface/ipv4.rs @@ -1,6 +1,6 @@ use super::*; -impl Interface { +impl Interface<'_> { /// Process fragments that still need to be sent for IPv4 packets. /// /// This function returns a boolean value indicating whether any packets were diff --git a/src/iface/interface/ipv6.rs b/src/iface/interface/ipv6.rs index 490fdee15..b2a057d33 100644 --- a/src/iface/interface/ipv6.rs +++ b/src/iface/interface/ipv6.rs @@ -1,12 +1,17 @@ use super::*; +use crate::socket::AnySocket; + +use crate::phy::PacketMeta; +use crate::wire::*; + /// Enum used for the process_hopbyhop function. In some cases, when discarding a packet, an ICMMP /// parameter problem message needs to be transmitted to the source of the address. In other cases, /// the processing of the IP packet can continue. #[allow(clippy::large_enum_variant)] enum HopByHopResponse<'frame> { /// Continue processing the IPv6 packet. - Continue((IpProtocol, &'frame [u8])), + Continue(Ipv6HopByHopRepr<'frame>, IpProtocol, &'frame [u8]), /// Discard the packet and maybe send back an ICMPv6 packet. Discard(Option>), } @@ -18,6 +23,17 @@ impl Default for HopByHopResponse<'_> { } } +/// Enum used for the process_routing function. +#[allow(clippy::large_enum_variant)] +pub(crate) enum RoutingResponse<'frame> { + /// Continue processing the IP packet. + Continue(IpProtocol, &'frame [u8]), + /// Forward the packet based on the information from the routing header. + Forward(Packet<'frame>), + /// There was an error processing the routing header, discard the packet. + Discard, +} + impl InterfaceInner { /// Return the IPv6 address that is a candidate source address for the given destination /// address, based on RFC 6724. @@ -150,7 +166,9 @@ impl InterfaceInner { IpCidr::Ipv6(cidr) if cidr.address() != Ipv6Address::LOOPBACK => { // Take the lower order 24 bits of the IPv6 address and // append those bits to FF02:0:0:0:0:1:FF00::/104. - addr.as_bytes()[14..] == cidr.address().as_bytes()[14..] + addr.as_bytes()[..14] + == Ipv6Address::new(0xFF02, 0, 0, 0, 0, 1, 0xFF00, 0).as_bytes()[..14] + && addr.as_bytes()[14..] == cidr.address().as_bytes()[14..] } _ => false, } @@ -171,8 +189,10 @@ impl InterfaceInner { sockets: &mut SocketSet, meta: PacketMeta, ipv6_packet: &Ipv6Packet<&'frame [u8]>, + previous_hop: Option<&HardwareAddress>, + multicast_queue: &mut PacketBuffer<'_, MulticastMetadata>, ) -> Option> { - let ipv6_repr = check!(Ipv6Repr::parse(ipv6_packet)); + let mut ipv6_repr = check!(Ipv6Repr::parse(ipv6_packet)); if !ipv6_repr.src_addr.is_unicast() { // Discard packets with non-unicast source addresses. @@ -180,21 +200,80 @@ impl InterfaceInner { return None; } - let (next_header, ip_payload) = if ipv6_repr.next_header == IpProtocol::HopByHop { + let (hbh, next_header, ip_payload) = if ipv6_repr.next_header == IpProtocol::HopByHop { match self.process_hopbyhop(ipv6_repr, ipv6_packet.payload()) { HopByHopResponse::Discard(e) => return e, - HopByHopResponse::Continue(next) => next, + HopByHopResponse::Continue(hbh, next_header, payload) => { + (Some(hbh), next_header, payload) + } } } else { - (ipv6_repr.next_header, ipv6_packet.payload()) + (None, ipv6_repr.next_header, ipv6_packet.payload()) }; + // Forward if not for us if !self.has_ip_addr(ipv6_repr.dst_addr) && !self.has_multicast_group(ipv6_repr.dst_addr) && !ipv6_repr.dst_addr.is_loopback() { - net_trace!("packet IP address not for this interface"); - return None; + #[cfg(not(feature = "proto-rpl"))] + { + net_trace!("packet IP address not for this interface"); + return None; + } + + #[cfg(feature = "proto-rpl")] + { + ipv6_repr.next_header = next_header; + if let Some(hbh) = &hbh { + ipv6_repr.payload_len -= 2 + hbh.buffer_len(); + } + return self.forward(ipv6_repr, hbh, None, ip_payload); + } + } + + // Disallow list of forwardable multicast packets + let should_forward_multicast = match ipv6_repr.dst_addr.into() { + #[cfg(feature = "proto-ipv6")] + IpAddress::Ipv6(Ipv6Address::LINK_LOCAL_ALL_NODES) => false, + #[cfg(feature = "proto-rpl")] + IpAddress::Ipv6(Ipv6Address::LINK_LOCAL_ALL_RPL_NODES) => false, + _ => true, + }; + // if for us and multicast, process further and schedule forwarding + if should_forward_multicast && ipv6_repr.dst_addr.is_multicast() { + // Construct forwarding packet if possible + let forwarding_packet = self.forward(ipv6_repr, hbh, None, ip_payload); + // Lookup hardware addresses to which we would like to forward the multicast packet + let haddrs = + self.lookup_hardware_addr_multicast(&ipv6_repr.dst_addr.into(), previous_hop); + + // Schedule forwarding and process further if possible + match (&forwarding_packet, haddrs) { + #[cfg(feature = "proto-ipv6")] + (Some(Packet::Ipv6(forwarding_packet)), Ok(haddrs)) => { + if !haddrs.is_empty() { + let _ = self + .schedule_multicast_packet( + meta, + forwarding_packet, + haddrs, + multicast_queue, + ) + .inspect_err(|err| { + net_trace!( + "Could not schedule multicast packets with reason {:?}", + err + ); + }); + } + } + #[cfg(feature = "proto-ipv4")] + (Some(Packet::Ipv4(_)), Ok(_haddrs)) => { + unimplemented!() + } + _ => {} + } } #[cfg(feature = "socket-raw")] @@ -217,30 +296,54 @@ impl InterfaceInner { ipv6_repr: Ipv6Repr, ip_payload: &'frame [u8], ) -> HopByHopResponse<'frame> { - let param_problem = || { - let payload_len = - icmp_reply_payload_len(ip_payload.len(), IPV6_MIN_MTU, ipv6_repr.buffer_len()); - self.icmpv6_reply( - ipv6_repr, - Icmpv6Repr::ParamProblem { - reason: Icmpv6ParamProblem::UnrecognizedOption, - pointer: ipv6_repr.buffer_len() as u32, - header: ipv6_repr, - data: &ip_payload[0..payload_len], - }, - ) - }; - let ext_hdr = check!(Ipv6ExtHeader::new_checked(ip_payload)); let ext_repr = check!(Ipv6ExtHeaderRepr::parse(&ext_hdr)); let hbh_hdr = check!(Ipv6HopByHopHeader::new_checked(ext_repr.data)); - let hbh_repr = check!(Ipv6HopByHopRepr::parse(&hbh_hdr)); + let mut hbh_repr = check!(Ipv6HopByHopRepr::parse(&hbh_hdr)); - for opt_repr in &hbh_repr.options { + for opt_repr in &mut hbh_repr.options { match opt_repr { Ipv6OptionRepr::Pad1 | Ipv6OptionRepr::PadN(_) => (), #[cfg(feature = "proto-rpl")] - Ipv6OptionRepr::Rpl(_) => {} + Ipv6OptionRepr::Rpl(hbh) if self.rpl.dodag.is_some() => { + match self.process_rpl_hopbyhop(*hbh) { + Ok(mut hbh) => { + if self.rpl.is_root { + hbh.down = true; + } else { + #[cfg(feature = "rpl-mop-2")] + if matches!( + self.rpl.mode_of_operation, + crate::iface::RplModeOfOperation::StoringMode + ) { + hbh.down = self + .rpl + .dodag + .as_ref() + .unwrap() + .relations + .find_next_hop(ipv6_repr.dst_addr) + .is_some(); + } + } + + hbh.sender_rank = self.rpl.dodag.as_ref().unwrap().rank.raw_value(); + // FIXME: really update the RPL Hop-by-Hop. When forwarding, + // we need to update the RPL Hop-by-Hop header. + *opt_repr = Ipv6OptionRepr::Rpl(hbh); + } + Err(_) => { + // TODO: check if we need to silently drop the packet or if we need to send + // back to the original sender (global/local repair). + return HopByHopResponse::Discard(None); + } + } + } + + Ipv6OptionRepr::Rpl(_) => { + // If we are not part of a RPL network, we should silently drop the packet. + return HopByHopResponse::Discard(None); + } Ipv6OptionRepr::Unknown { type_, .. } => { match Ipv6OptionFailureType::from(*type_) { @@ -249,12 +352,20 @@ impl InterfaceInner { return HopByHopResponse::Discard(None); } Ipv6OptionFailureType::DiscardSendAll => { - return HopByHopResponse::Discard(param_problem()); + return HopByHopResponse::Discard(self.icmpv6_problem( + ipv6_repr, + ip_payload, + Icmpv6ParamProblem::UnrecognizedOption, + )); } Ipv6OptionFailureType::DiscardSendUnicast if !ipv6_repr.dst_addr.is_multicast() => { - return HopByHopResponse::Discard(param_problem()); + return HopByHopResponse::Discard(self.icmpv6_problem( + ipv6_repr, + ip_payload, + Icmpv6ParamProblem::UnrecognizedOption, + )); } _ => unreachable!(), } @@ -262,10 +373,11 @@ impl InterfaceInner { } } - HopByHopResponse::Continue(( + HopByHopResponse::Continue( + hbh_repr, ext_repr.next_header, &ip_payload[ext_repr.header_len() + ext_repr.data.len()..], - )) + ) } /// Given the next header value forward the payload onto the correct process @@ -294,22 +406,19 @@ impl InterfaceInner { #[cfg(feature = "socket-tcp")] IpProtocol::Tcp => self.process_tcp(sockets, ipv6_repr.into(), ip_payload), + #[cfg(feature = "proto-ipv6-routing")] + IpProtocol::Ipv6Route => { + self.process_routing(sockets, meta, ipv6_repr, handled_by_raw_socket, ip_payload) + } + #[cfg(feature = "socket-raw")] _ if handled_by_raw_socket => None, - _ => { - // Send back as much of the original payload as we can. - let payload_len = - icmp_reply_payload_len(ip_payload.len(), IPV6_MIN_MTU, ipv6_repr.buffer_len()); - let icmp_reply_repr = Icmpv6Repr::ParamProblem { - reason: Icmpv6ParamProblem::UnrecognizedNxtHdr, - // The offending packet is after the IPv6 header. - pointer: ipv6_repr.buffer_len() as u32, - header: ipv6_repr, - data: &ip_payload[0..payload_len], - }; - self.icmpv6_reply(ipv6_repr, icmp_reply_repr) - } + _ => self.icmpv6_problem( + ipv6_repr, + ip_payload, + Icmpv6ParamProblem::UnrecognizedNxtHdr, + ), } } @@ -324,7 +433,7 @@ impl InterfaceInner { &ip_repr.src_addr, &ip_repr.dst_addr, &icmp_packet, - &self.caps.checksum, + &self.checksum_caps(), )); #[cfg(feature = "socket-icmp")] @@ -373,6 +482,9 @@ impl InterfaceInner { Medium::Ip => None, }, + #[cfg(feature = "proto-rpl")] + Icmpv6Repr::Rpl(rpl) => self.process_rpl(ip_repr, rpl), + // Don't report an error if a packet with unknown type // has been handled by an ICMP socket #[cfg(feature = "socket-icmp")] @@ -446,6 +558,51 @@ impl InterfaceInner { } } + #[cfg(feature = "proto-ipv6-routing")] + pub(super) fn process_routing<'frame>( + &mut self, + sockets: &mut SocketSet, + meta: PacketMeta, + ipv6_repr: Ipv6Repr, + handled_by_raw_socket: bool, + ip_payload: &'frame [u8], + ) -> Option> { + let ext_hdr = check!(Ipv6ExtHeader::new_checked(ip_payload)); + + let routing_header = check!(Ipv6RoutingHeader::new_checked(ext_hdr.payload())); + let routing_repr = check!(Ipv6RoutingRepr::parse(&routing_header)); + + let (next_header, payload) = match routing_repr { + Ipv6RoutingRepr::Type2 { .. } => { + // TODO: we should respond with an ICMPv6 unknown protocol message. + net_debug!("IPv6 Type2 routing header not supported yet, dropping packet."); + return None; + } + #[cfg(not(feature = "proto-rpl"))] + Ipv6RoutingRepr::Rpl { .. } => { + net_debug!("RPL routing header not supported, dropping packet."); + return None; + } + #[cfg(feature = "proto-rpl")] + Ipv6RoutingRepr::Rpl(routing) => { + match self.process_source_routing(ipv6_repr, &ext_hdr, routing, ip_payload) { + RoutingResponse::Discard => return None, + RoutingResponse::Forward(packet) => return Some(packet), + RoutingResponse::Continue(next_header, payload) => (next_header, payload), + } + } + }; + + self.process_nxt_hdr( + sockets, + meta, + ipv6_repr, + next_header, + handled_by_raw_socket, + payload, + ) + } + pub(super) fn icmpv6_reply<'frame, 'icmp: 'frame>( &self, ipv6_repr: Ipv6Repr, @@ -472,4 +629,80 @@ impl InterfaceInner { IpPayload::Icmpv6(icmp_repr), )) } + + #[cfg(feature = "proto-rpl")] + pub(super) fn forward<'frame>( + &self, + mut ipv6_repr: Ipv6Repr, + mut _hop_by_hop: Option>, + mut _routing: Option, + payload: &'frame [u8], + ) -> Option> { + net_trace!("forwarding packet"); + + if ipv6_repr.hop_limit <= 1 { + net_trace!("hop limit reached 0, dropping packet"); + // FIXME: we should transmit an ICMPv6 Time Exceeded message, as defined + // in RFC 2460. However, this is not trivial with the current state of + // smoltcp. When sending this message back, as much as possible of the + // original message should be transmitted back. This is after updating the + // addresses in the source routing headers. At this time, we only update + // the parsed list of addresses, not the `ip_payload` buffer. It is this + // buffer we would use when sending back the ICMPv6 message. And since we + // can't update that buffer here, we can't update the source routing header + // and it would send back an incorrect header. The standard does not + // specify if we SHOULD or MUST transmit an ICMPv6 message. + return None; + } + + ipv6_repr.hop_limit -= 1; + + let mut p = PacketV6::new(ipv6_repr, IpPayload::Raw(payload)); + + if let Some(hbh) = _hop_by_hop { + p.add_hop_by_hop(hbh); + } else { + #[cfg(feature = "proto-rpl")] + if p.header().dst_addr.is_unicast() && self.rpl.dodag.is_some() { + let mut options = heapless::Vec::new(); + options + .push(Ipv6OptionRepr::Rpl(RplHopByHopRepr { + down: self.rpl.is_root, + rank_error: false, + forwarding_error: false, + instance_id: self.rpl.dodag.as_ref().unwrap().instance_id, + sender_rank: self.rpl.dodag.as_ref().unwrap().rank.raw_value(), + })) + .unwrap(); + + let hbh = Ipv6HopByHopRepr { options }; + p.add_hop_by_hop(hbh); + } + } + + if let Some(routing) = _routing { + p.add_routing(routing); + } + + Some(Packet::Ipv6(p)) + } + + fn icmpv6_problem<'frame>( + &self, + ipv6_repr: Ipv6Repr, + ip_payload: &'frame [u8], + reason: Icmpv6ParamProblem, + ) -> Option> { + let payload_len = + icmp_reply_payload_len(ip_payload.len(), IPV6_MIN_MTU, ipv6_repr.buffer_len()); + self.icmpv6_reply( + ipv6_repr, + Icmpv6Repr::ParamProblem { + reason, + pointer: ipv6_repr.buffer_len() as u32, + header: ipv6_repr, + data: &ip_payload[0..payload_len], + }, + ) + } } diff --git a/src/iface/interface/mod.rs b/src/iface/interface/mod.rs index 00f46d07d..31d668833 100644 --- a/src/iface/interface/mod.rs +++ b/src/iface/interface/mod.rs @@ -17,6 +17,9 @@ mod ipv6; #[cfg(feature = "proto-sixlowpan")] mod sixlowpan; +#[cfg(feature = "proto-rpl")] +mod rpl; + #[cfg(feature = "proto-igmp")] mod igmp; #[cfg(feature = "socket-tcp")] @@ -24,13 +27,14 @@ mod tcp; #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] mod udp; -#[cfg(feature = "proto-igmp")] -pub use igmp::MulticastError; +mod multicast; +use super::multicast::MulticastMetadata; use super::packet::*; use core::result::Result; use heapless::{LinearMap, Vec}; +use managed::ManagedSlice; #[cfg(feature = "_proto-fragmentation")] use super::fragmentation::FragKey; @@ -41,8 +45,10 @@ use super::fragmentation::{Fragmenter, FragmentsBuffer}; #[cfg(any(feature = "medium-ethernet", feature = "medium-ieee802154"))] use super::neighbor::{Answer as NeighborAnswer, Cache as NeighborCache}; use super::socket_set::SocketSet; +#[cfg(feature = "proto-rpl")] +use super::RplConfig; use crate::config::{ - IFACE_MAX_ADDR_COUNT, IFACE_MAX_MULTICAST_GROUP_COUNT, + IFACE_MAX_ADDR_COUNT, IFACE_MAX_MULTICAST_DUPLICATION_COUNT, IFACE_MAX_MULTICAST_GROUP_COUNT, IFACE_MAX_SIXLOWPAN_ADDRESS_CONTEXT_COUNT, }; use crate::iface::Routes; @@ -50,6 +56,7 @@ use crate::phy::PacketMeta; use crate::phy::{ChecksumCapabilities, Device, DeviceCapabilities, Medium, RxToken, TxToken}; use crate::rand::Rand; use crate::socket::*; +use crate::storage::{PacketBuffer, PacketMetadata}; use crate::time::{Duration, Instant}; use crate::wire::*; @@ -76,10 +83,11 @@ use check; /// The network interface logically owns a number of other data structures; to avoid /// a dependency on heap allocation, it instead owns a `BorrowMut<[T]>`, which can be /// a `&mut [T]`, or `Vec` if a heap is available. -pub struct Interface { +pub struct Interface<'a> { pub(crate) inner: InterfaceInner, fragments: FragmentsBuffer, fragmenter: Fragmenter, + multicast_queue: PacketBuffer<'a, MulticastMetadata>, } /// The device independent part of an Ethernet network interface. @@ -117,6 +125,13 @@ pub struct InterfaceInner { /// When to report for (all or) the next multicast group membership via IGMP #[cfg(feature = "proto-igmp")] igmp_report_state: IgmpReportState, + + current_frame: Option, + + #[cfg(feature = "proto-rpl")] + rpl: super::Rpl, + #[cfg(feature = "rpl-mop-3")] + rpl_targets_multicast: heapless::Vec, } /// Configuration structure used for creating a network interface. @@ -141,6 +156,9 @@ pub struct Config { /// **NOTE**: we use the same PAN ID for destination and source. #[cfg(feature = "medium-ieee802154")] pub pan_id: Option, + + #[cfg(feature = "proto-rpl")] + pub rpl_config: Option, } impl Config { @@ -150,19 +168,29 @@ impl Config { hardware_addr, #[cfg(feature = "medium-ieee802154")] pan_id: None, + #[cfg(feature = "proto-rpl")] + rpl_config: None, } } } -impl Interface { +impl<'a> Interface<'a> { /// Create a network interface using the previously provided configuration. /// /// # Panics /// This function panics if the [`Config::hardware_address`] does not match /// the medium of the device. - pub fn new(config: Config, device: &mut D, now: Instant) -> Self + pub fn new( + config: Config, + device: &mut D, + metadata_storage: MS, + payload_storage: PS, + now: Instant, + ) -> Self where D: Device + ?Sized, + MS: Into>>, + PS: Into>, { let caps = device.capabilities(); assert_eq!( @@ -216,6 +244,8 @@ impl Interface { reassembly_timeout: Duration::from_secs(60), }, fragmenter: Fragmenter::new(), + multicast_queue: PacketBuffer::new(metadata_storage, payload_storage), + inner: InterfaceInner { now, caps, @@ -241,6 +271,13 @@ impl Interface { #[cfg(feature = "proto-sixlowpan")] sixlowpan_address_context: Vec::new(), rand, + + current_frame: None, + + #[cfg(feature = "proto-rpl")] + rpl: super::Rpl::new(config.rpl_config.unwrap_or_default(), now), + #[cfg(feature = "rpl-mop-3")] + rpl_targets_multicast: Default::default(), }, } } @@ -248,7 +285,14 @@ impl Interface { /// Get the socket context. /// /// The context is needed for some socket methods. - pub fn context(&mut self) -> &mut InterfaceInner { + pub fn context(&self) -> &InterfaceInner { + &self.inner + } + + /// Get the socket context. + /// + /// The context is needed for some socket methods. + pub fn context_mut(&mut self) -> &mut InterfaceInner { &mut self.inner } @@ -395,6 +439,183 @@ impl Interface { self.fragments.reassembly_timeout = timeout; } + /// Add an address to a list of subscribed multicast IP addresses. + /// + /// Returns `Ok(announce_sent)` if the address was added successfully, where `announce_sent` + /// indicates whether an initial immediate announcement has been sent. + pub fn join_multicast_group>( + &mut self, + device: &mut D, + addr: T, + timestamp: Instant, + ) -> Result + where + D: Device + ?Sized, + { + self.inner.now = timestamp; + + match addr.into() { + #[cfg(all(feature = "proto-ipv4", feature = "proto-igmp"))] + IpAddress::Ipv4(addr) => { + let is_not_new = self + .inner + .ipv4_multicast_groups + .insert(addr, ()) + .map_err(|_| MulticastError::GroupTableFull)? + .is_some(); + if is_not_new { + Ok(false) + } else if let Some(pkt) = self.inner.igmp_report_packet(IgmpVersion::Version2, addr) + { + // 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, + None, + &mut self.fragmenter, + &mut self.multicast_queue, + ) + .unwrap(); + + Ok(true) + } else { + Ok(false) + } + } + #[cfg(all(feature = "proto-ipv6", feature = "rpl-mop-3"))] + IpAddress::Ipv6(addr) => { + // Check if the multicast address is present in the current multicast targets + if !self.inner.rpl_targets_multicast.contains(&addr) { + // Try to add the multicast target, otherwise abort + self.inner + .rpl_targets_multicast + .push(addr) + .map_err(|_err| MulticastError::GroupTableFull)?; + + // Schedule a new DAO for transmission if part of a dodag + match &mut self.inner.rpl.dodag { + Some(dodag) => { + if let Some(parent) = &dodag.parent { + dodag + .schedule_dao( + self.inner.rpl.mode_of_operation, + &[], + &[addr], + *parent, + self.inner.now, + false, + ) + .map_err(|_err| MulticastError::Exhausted)?; + } + + Ok(true) + } + None => Ok(false), + } + } else { + Ok(false) + } + } + // Multicast is not implemented/enabled for other address families + #[allow(unreachable_patterns)] + _ => Err(MulticastError::Unaddressable), + } + } + + /// Remove an address from the subscribed multicast IP addresses. + /// + /// Returns `Ok(leave_sent)` if the address was removed successfully, where `leave_sent` + /// indicates whether an immediate leave packet has been sent. + pub fn leave_multicast_group>( + &mut self, + device: &mut D, + addr: T, + timestamp: Instant, + ) -> Result + where + D: Device + ?Sized, + { + self.inner.now = timestamp; + + match addr.into() { + #[cfg(all(feature = "proto-ipv4", feature = "proto-igmp"))] + IpAddress::Ipv4(addr) => { + let was_not_present = self.inner.ipv4_multicast_groups.remove(&addr).is_none(); + if was_not_present { + Ok(false) + } else if let Some(pkt) = self.inner.igmp_leave_packet(addr) { + // 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, + None, + &mut self.fragmenter, + &mut self.multicast_queue, + ) + .unwrap(); + + Ok(true) + } else { + Ok(false) + } + } + #[cfg(all(feature = "proto-ipv6", feature = "rpl-mop-3"))] + IpAddress::Ipv6(addr) => { + if self.inner.rpl_targets_multicast.contains(&addr) { + // Try to add the multicast target, otherwise abort + self.inner + .rpl_targets_multicast + .retain(|multicast_group| multicast_group == &addr); + + // Schedule a new DAO for transmission if part of a dodag + match &mut self.inner.rpl.dodag { + Some(dodag) => { + if let Some(parent) = &dodag.parent { + dodag + .schedule_dao( + self.inner.rpl.mode_of_operation, + &[], + &[addr], + *parent, + self.inner.now, + true, + ) + .map_err(|_err| MulticastError::Exhausted)?; + } + + Ok(true) + } + None => Ok(false), + } + } else { + Ok(false) + } + } + // Multicast is not implemented/enabled for other address families + #[allow(unreachable_patterns)] + _ => Err(MulticastError::Unaddressable), + } + } + + /// Check whether the interface listens to given destination multicast IP address. + pub fn has_multicast_group>(&self, addr: T) -> bool { + self.inner.has_multicast_group(addr) + } + /// Transmit packets queued in the given sockets, and receive packets queued /// in the device. /// @@ -415,6 +636,9 @@ impl Interface { #[cfg(feature = "_proto-fragmentation")] self.fragments.assembler.remove_expired(timestamp); + // Poll multicast queue and dispatch if possible + self.poll_multicast(device); + match self.inner.caps.medium { #[cfg(feature = "medium-ieee802154")] Medium::Ieee802154 => @@ -434,6 +658,9 @@ impl Interface { } } + #[cfg(feature = "proto-rpl")] + self.poll_rpl(device); + let mut readiness_may_have_changed = false; loop { @@ -472,22 +699,29 @@ impl Interface { return Some(Instant::from_millis(0)); } + let poll_at_multicast = self.poll_at_multicast(); + + #[cfg(feature = "proto-rpl")] + let poll_at_rpl = self.poll_at_rpl(); + let inner = &mut self.inner; + let poll_at = sockets.items().filter_map(move |item| { + let socket_poll_at = item.socket.poll_at(inner); + match item + .meta + .poll_at(socket_poll_at, |ip_addr| inner.has_neighbor(&ip_addr)) + { + PollAt::Ingress => None, + PollAt::Time(instant) => Some(instant), + PollAt::Now => Some(Instant::from_millis(0)), + } + }); - sockets - .items() - .filter_map(move |item| { - let socket_poll_at = item.socket.poll_at(inner); - match item - .meta - .poll_at(socket_poll_at, |ip_addr| inner.has_neighbor(&ip_addr)) - { - PollAt::Ingress => None, - PollAt::Time(instant) => Some(instant), - PollAt::Now => Some(Instant::from_millis(0)), - } - }) - .min() + #[cfg(feature = "proto-rpl")] + let poll_at = poll_at.chain(core::iter::once(poll_at_rpl)); + let poll_at = poll_at.chain(poll_at_multicast); + + poll_at.min() } /// Return an _advisory wait time_ for calling [poll] the next time. @@ -527,25 +761,35 @@ impl Interface { rx_meta, frame, &mut self.fragments, + &mut self.multicast_queue, ) { - if let Err(err) = - self.inner.dispatch(tx_token, packet, &mut self.fragmenter) - { + if let Err(err) = self.inner.dispatch( + tx_token, + packet, + &mut self.fragmenter, + &mut self.multicast_queue, + ) { net_debug!("Failed to send response: {:?}", err); } } } #[cfg(feature = "medium-ip")] Medium::Ip => { - if let Some(packet) = - self.inner - .process_ip(sockets, rx_meta, frame, &mut self.fragments) - { + if let Some(packet) = self.inner.process_ip( + sockets, + rx_meta, + frame, + None, + &mut self.fragments, + &mut self.multicast_queue, + ) { if let Err(err) = self.inner.dispatch_ip( tx_token, PacketMeta::default(), packet, + None, &mut self.fragmenter, + &mut self.multicast_queue, ) { net_debug!("Failed to send response: {:?}", err); } @@ -558,12 +802,22 @@ impl Interface { rx_meta, frame, &mut self.fragments, + &mut self.multicast_queue, ) { + let frame = Ieee802154Frame::new_checked(&*frame).ok(); + let src_addr = frame + .as_ref() + .and_then(|frame| Ieee802154Repr::parse(frame).ok()) + .and_then(|repr| repr.src_addr) + .map(HardwareAddress::Ieee802154); + if let Err(err) = self.inner.dispatch_ip( tx_token, PacketMeta::default(), packet, + src_addr.as_ref(), &mut self.fragmenter, + &mut self.multicast_queue, ) { net_debug!("Failed to send response: {:?}", err); } @@ -606,7 +860,14 @@ impl Interface { })?; inner - .dispatch_ip(t, meta, response, &mut self.fragmenter) + .dispatch_ip( + t, + meta, + response, + None, + &mut self.fragmenter, + &mut self.multicast_queue, + ) .map_err(EgressError::Dispatch)?; emitted_any = true; @@ -775,7 +1036,9 @@ impl InterfaceInner { #[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), + IpAddress::Ipv6(addr) if self.has_solicited_node(addr) => true, + #[cfg(all(feature = "proto-ipv6", feature = "rpl-mop-3"))] + IpAddress::Ipv6(addr) if self.rpl_targets_multicast.contains(&addr) => true, #[allow(unreachable_patterns)] _ => false, } @@ -787,7 +1050,9 @@ impl InterfaceInner { sockets: &mut SocketSet, meta: PacketMeta, ip_payload: &'frame [u8], + previous_hop: Option<&HardwareAddress>, frag: &'frame mut FragmentsBuffer, + multicast_queue: &mut PacketBuffer<'_, MulticastMetadata>, ) -> Option> { match IpVersion::of_packet(ip_payload) { #[cfg(feature = "proto-ipv4")] @@ -799,7 +1064,7 @@ impl InterfaceInner { #[cfg(feature = "proto-ipv6")] Ok(IpVersion::Ipv6) => { let ipv6_packet = check!(Ipv6Packet::new_checked(ip_payload)); - self.process_ipv6(sockets, meta, &ipv6_packet) + self.process_ipv6(sockets, meta, &ipv6_packet, previous_hop, multicast_queue) } // Drop all other traffic. _ => None, @@ -845,6 +1110,7 @@ impl InterfaceInner { tx_token: Tx, packet: EthernetPacket, frag: &mut Fragmenter, + multicast_queue: &mut PacketBuffer<'_, MulticastMetadata>, ) -> Result<(), DispatchError> where Tx: TxToken, @@ -867,9 +1133,14 @@ impl InterfaceInner { arp_repr.emit(&mut packet); }) } - EthernetPacket::Ip(packet) => { - self.dispatch_ip(tx_token, PacketMeta::default(), packet, frag) - } + EthernetPacket::Ip(packet) => self.dispatch_ip( + tx_token, + PacketMeta::default(), + packet, + None, + frag, + multicast_queue, + ), } } @@ -890,6 +1161,21 @@ impl InterfaceInner { } fn has_neighbor(&self, addr: &IpAddress) -> bool { + if addr.is_multicast() { + #[cfg(feature = "proto-rpl")] + { + if let Some(dodag) = &self.rpl.dodag { + return dodag + .relations + .iter() + .any(|rel| &IpAddress::Ipv6(rel.destination()) == addr) + || dodag.parent.is_some(); + } + } + // FIXME: Do something useful here + return true; + } + match self.route(addr, self.now) { Some(_routed_addr) => match self.caps.medium { #[cfg(feature = "medium-ethernet")] @@ -903,146 +1189,277 @@ impl InterfaceInner { } } + /// Lookup the hardware address when the destination is broadcast + fn lookup_hardware_addr_broadcast( + &mut self, + dst_addr: &IpAddress, + ) -> Result, DispatchError> + { + debug_assert!(dst_addr.is_broadcast()); + let hardware_addr = match self.caps.medium { + #[cfg(feature = "medium-ethernet")] + Medium::Ethernet => HardwareAddress::Ethernet(EthernetAddress::BROADCAST), + #[cfg(feature = "medium-ieee802154")] + Medium::Ieee802154 => HardwareAddress::Ieee802154(Ieee802154Address::BROADCAST), + #[cfg(feature = "medium-ip")] + Medium::Ip => unreachable!(), + }; + + Ok(heapless::Vec::from_iter(core::iter::once(hardware_addr))) + } + + /// Lookup the hardware address when the destination is multicast + fn lookup_hardware_addr_multicast( + &mut self, + dst_addr: &IpAddress, + previous_hop: Option<&HardwareAddress>, + ) -> Result, DispatchError> + { + debug_assert!(dst_addr.is_multicast()); + + let b = dst_addr.as_bytes(); + let hardware_addresses = match *dst_addr { + #[cfg(feature = "proto-ipv4")] + IpAddress::Ipv4(_addr) => match self.caps.medium { + #[cfg(feature = "medium-ethernet")] + Medium::Ethernet => { + heapless::Vec::from_iter(core::iter::once(HardwareAddress::Ethernet( + EthernetAddress::from_bytes(&[0x01, 0x00, 0x5e, b[1] & 0x7F, b[2], b[3]]), + ))) + } + #[cfg(feature = "medium-ieee802154")] + Medium::Ieee802154 => unreachable!(), + #[cfg(feature = "medium-ip")] + Medium::Ip => unreachable!(), + }, + #[cfg(feature = "proto-ipv6")] + IpAddress::Ipv6(addr) => match self.caps.medium { + #[cfg(feature = "medium-ethernet")] + Medium::Ethernet => { + heapless::Vec::from_iter(core::iter::once(HardwareAddress::Ethernet( + EthernetAddress::from_bytes(&[0x33, 0x33, b[12], b[13], b[14], b[15]]), + ))) + } + #[cfg(feature = "medium-ieee802154")] + Medium::Ieee802154 => { + match addr { + // Handle well known multicast groups + Ipv6Address::LINK_LOCAL_ALL_RPL_NODES => { + heapless::Vec::from_iter(core::iter::once(HardwareAddress::Ieee802154( + Ieee802154Address::BROADCAST, + ))) + } + Ipv6Address::LINK_LOCAL_ALL_NODES | Ipv6Address::LINK_LOCAL_ALL_ROUTERS => { + heapless::Vec::from_iter(core::iter::once(HardwareAddress::Ieee802154( + Ieee802154Address::BROADCAST, + ))) + } + // Handle the joined multicast groups + _ => { + #[cfg(feature = "rpl-mop-3")] + // If in a DODAG, filter out the previous hop and compile a list of the remaining canditates + if let Some(dodag) = &self.rpl.dodag { + let parent = dodag.parent.iter().copied(); + let next_hops = dodag.relations.find_next_hop(addr); + let downwards = next_hops + .iter() + .flat_map(|hops| hops.iter()) + .map(|hop| hop.ip); + let hardware_addrs = parent + .chain(downwards) + .flat_map(|hop| { + match self.neighbor_cache.lookup(&hop.into(), self.now) { + NeighborAnswer::Found(haddr) => Some(haddr), + NeighborAnswer::NotFound => None, + NeighborAnswer::RateLimited => None, + } + }) + .filter(|haddr| Some(haddr) != previous_hop); + + heapless::Vec::from_iter(hardware_addrs) + } else { + // Not sure if this is correct + heapless::Vec::from_iter(core::iter::once( + HardwareAddress::Ieee802154(Ieee802154Address::BROADCAST), + )) + } + #[cfg(not(feature = "rpl-mop-3"))] + { + heapless::Vec::from_iter(core::iter::once( + HardwareAddress::Ieee802154(Ieee802154Address::BROADCAST), + )) + } + } + } + } + #[cfg(feature = "medium-ip")] + Medium::Ip => unreachable!(), + }, + }; + + Ok(hardware_addresses) + } + #[cfg(any(feature = "medium-ethernet", feature = "medium-ieee802154"))] fn lookup_hardware_addr( &mut self, tx_token: Tx, + previous_hop: Option<&HardwareAddress>, src_addr: &IpAddress, dst_addr: &IpAddress, - fragmenter: &mut Fragmenter, - ) -> Result<(HardwareAddress, Tx), DispatchError> + // previous_hop: Option, + #[allow(unused)] fragmenter: &mut Fragmenter, + ) -> Result< + ( + heapless::Vec, + Tx, + ), + DispatchError, + > where Tx: TxToken, { if self.is_broadcast(dst_addr) { - let hardware_addr = match self.caps.medium { - #[cfg(feature = "medium-ethernet")] - Medium::Ethernet => HardwareAddress::Ethernet(EthernetAddress::BROADCAST), - #[cfg(feature = "medium-ieee802154")] - Medium::Ieee802154 => HardwareAddress::Ieee802154(Ieee802154Address::BROADCAST), - #[cfg(feature = "medium-ip")] - Medium::Ip => unreachable!(), - }; - - return Ok((hardware_addr, tx_token)); + return Ok((self.lookup_hardware_addr_broadcast(dst_addr)?, tx_token)); } if dst_addr.is_multicast() { - let b = dst_addr.as_bytes(); - let hardware_addr = match *dst_addr { - #[cfg(feature = "proto-ipv4")] - IpAddress::Ipv4(_addr) => match self.caps.medium { - #[cfg(feature = "medium-ethernet")] - Medium::Ethernet => HardwareAddress::Ethernet(EthernetAddress::from_bytes(&[ - 0x01, - 0x00, - 0x5e, - b[1] & 0x7F, - b[2], - b[3], - ])), - #[cfg(feature = "medium-ieee802154")] - Medium::Ieee802154 => unreachable!(), - #[cfg(feature = "medium-ip")] - Medium::Ip => unreachable!(), - }, - #[cfg(feature = "proto-ipv6")] - IpAddress::Ipv6(_addr) => match self.caps.medium { - #[cfg(feature = "medium-ethernet")] - Medium::Ethernet => HardwareAddress::Ethernet(EthernetAddress::from_bytes(&[ - 0x33, 0x33, b[12], b[13], b[14], b[15], - ])), - #[cfg(feature = "medium-ieee802154")] - Medium::Ieee802154 => { - // Not sure if this is correct - HardwareAddress::Ieee802154(Ieee802154Address::BROADCAST) - } - #[cfg(feature = "medium-ip")] - Medium::Ip => unreachable!(), - }, - }; - - return Ok((hardware_addr, tx_token)); + return Ok(( + self.lookup_hardware_addr_multicast(dst_addr, previous_hop)?, + tx_token, + )); } let dst_addr = self .route(dst_addr, self.now) .ok_or(DispatchError::NoRoute)?; - match self.neighbor_cache.lookup(&dst_addr, self.now) { - NeighborAnswer::Found(hardware_addr) => return Ok((hardware_addr, tx_token)), - NeighborAnswer::RateLimited => return Err(DispatchError::NeighborPending), - _ => (), // XXX - } + #[cfg(feature = "proto-rpl")] + let dst_addr = if let IpAddress::Ipv6(dst_addr) = dst_addr { + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop3"))] + if let Some(dodag) = &self.rpl.dodag { + if let Some(next_hop) = dodag + .relations + .find_next_hop(dst_addr) + .and_then(|hop| hop.first()) + // In unicast it is not possible to have multiple next_hops per destination + { + if next_hop.ip == self.ipv6_addr().unwrap() { + dst_addr.into() + } else { + net_trace!("next hops {:?}", next_hop); + next_hop.ip.into() + } + } else if let Some(parent) = dodag.parent { + parent.into() + } else { + dst_addr.into() + } + } else { + dst_addr.into() + } - match (src_addr, dst_addr) { - #[cfg(all(feature = "medium-ethernet", feature = "proto-ipv4"))] - (&IpAddress::Ipv4(src_addr), IpAddress::Ipv4(dst_addr)) - if matches!(self.caps.medium, Medium::Ethernet) => - { - net_debug!( - "address {} not in neighbor cache, sending ARP request", - dst_addr - ); - let src_hardware_addr = self.hardware_addr.ethernet_or_panic(); - - let arp_repr = ArpRepr::EthernetIpv4 { - operation: ArpOperation::Request, - source_hardware_addr: src_hardware_addr, - source_protocol_addr: src_addr, - target_hardware_addr: EthernetAddress::BROADCAST, - target_protocol_addr: dst_addr, - }; + #[cfg(not(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop3")))] + dst_addr.into() + } else { + dst_addr + }; - if let Err(e) = - self.dispatch_ethernet(tx_token, arp_repr.buffer_len(), |mut frame| { - frame.set_dst_addr(EthernetAddress::BROADCAST); - frame.set_ethertype(EthernetProtocol::Arp); + match self.neighbor_cache.lookup(&dst_addr, self.now) { + NeighborAnswer::Found(hardware_addr) => { + return Ok(( + heapless::Vec::from_iter(core::iter::once(hardware_addr)), + tx_token, + )) + } + NeighborAnswer::RateLimited => { + net_debug!("neighbor {} pending", dst_addr); + return Err(DispatchError::NeighborPending); + } + NeighborAnswer::NotFound => match (src_addr, dst_addr) { + #[cfg(all(feature = "medium-ethernet", feature = "proto-ipv4"))] + (&IpAddress::Ipv4(src_addr), IpAddress::Ipv4(dst_addr)) => { + net_debug!( + "address {} not in neighbor cache, sending ARP request", + dst_addr + ); + let src_hardware_addr = self.hardware_addr.ethernet_or_panic(); + + let arp_repr = ArpRepr::EthernetIpv4 { + operation: ArpOperation::Request, + source_hardware_addr: src_hardware_addr, + source_protocol_addr: src_addr, + target_hardware_addr: EthernetAddress::BROADCAST, + target_protocol_addr: dst_addr, + }; + + if let Err(e) = + self.dispatch_ethernet(tx_token, arp_repr.buffer_len(), |mut frame| { + frame.set_dst_addr(EthernetAddress::BROADCAST); + frame.set_ethertype(EthernetProtocol::Arp); + + arp_repr.emit(&mut ArpPacket::new_unchecked(frame.payload_mut())) + }) + { + net_debug!("Failed to dispatch ARP request: {:?}", e); + return Err(DispatchError::NeighborPending); + } + } - arp_repr.emit(&mut ArpPacket::new_unchecked(frame.payload_mut())) - }) - { - net_debug!("Failed to dispatch ARP request: {:?}", e); - return Err(DispatchError::NeighborPending); + #[cfg(all(feature = "proto-ipv6", feature = "proto-rpl"))] + (&IpAddress::Ipv6(_), IpAddress::Ipv6(_)) => { + if let Some(dodag) = self.rpl.dodag.as_ref() { + if let Some(parent) = dodag.parent { + if let NeighborAnswer::Found(hardware_addr) = + self.neighbor_cache.lookup(&parent.into(), self.now) + { + return Ok(( + heapless::Vec::from_iter(core::iter::once(hardware_addr)), + tx_token, + )); + } + } + } } - } - #[cfg(feature = "proto-ipv6")] - (&IpAddress::Ipv6(src_addr), IpAddress::Ipv6(dst_addr)) => { - net_debug!( - "address {} not in neighbor cache, sending Neighbor Solicitation", - dst_addr - ); - - let solicit = Icmpv6Repr::Ndisc(NdiscRepr::NeighborSolicit { - target_addr: dst_addr, - lladdr: Some(self.hardware_addr.into()), - }); - - let packet = Packet::new_ipv6( - Ipv6Repr { - src_addr, - dst_addr: dst_addr.solicited_node(), - next_header: IpProtocol::Icmpv6, - payload_len: solicit.buffer_len(), - hop_limit: 0xff, - }, - IpPayload::Icmpv6(solicit), - ); - - if let Err(e) = - self.dispatch_ip(tx_token, PacketMeta::default(), packet, fragmenter) - { - net_debug!("Failed to dispatch NDISC solicit: {:?}", e); - return Err(DispatchError::NeighborPending); + #[cfg(all(feature = "proto-ipv6", not(feature = "proto-rpl")))] + (&IpAddress::Ipv6(src_addr), IpAddress::Ipv6(dst_addr)) => { + net_debug!( + "address {} not in neighbor cache, sending Neighbor Solicitation", + dst_addr + ); + + let solicit = Icmpv6Repr::Ndisc(NdiscRepr::NeighborSolicit { + target_addr: dst_addr, + lladdr: Some(self.hardware_addr.into()), + }); + + let packet = Packet::new_ipv6( + Ipv6Repr { + src_addr, + dst_addr: dst_addr.solicited_node(), + next_header: IpProtocol::Icmpv6, + payload_len: solicit.buffer_len(), + hop_limit: 0xff, + }, + IpPayload::Icmpv6(solicit), + ); + + if let Err(e) = + self.dispatch_ip(tx_token, Default::default(), packet, fragmenter) + { + net_debug!("Failed to dispatch NDISC solicit: {:?}", e); + return Err(DispatchError::NeighborPending); + } } - } - #[allow(unreachable_patterns)] - _ => (), + #[allow(unreachable_patterns)] + _ => (), + }, } // The request got dispatched, limit the rate on the cache. + net_debug!("request dispatched, limiting rate on cache"); self.neighbor_cache.limit_rate(self.now); Err(DispatchError::NeighborPending) } @@ -1052,6 +1469,31 @@ impl InterfaceInner { self.neighbor_cache.flush() } + /// Convenience method for scheduling a multicast packet for later transmission + fn schedule_multicast_packet( + &self, + meta: PacketMeta, + packet: &PacketV6<'_>, + ll_addrs: heapless::Vec, + multicast_queue: &mut PacketBuffer<'_, MulticastMetadata>, + ) -> Result<(), DispatchError> { + let buffer = multicast_queue + .enqueue( + packet.payload().buffer_len(), + MulticastMetadata::new(meta, packet, ll_addrs), + ) + .map_err(|_err| DispatchError::Exhausted)?; + packet + .payload() + .emit(&(*packet.header()).into(), buffer, &self.caps); + + Ok(()) + } + + /// Transmit an IP packet or schedule it into multiple transmissions when + /// fragmentation is needed or retransmissions with multicast + /// + /// If the hardware address is already known, use this one, otherwise do a lookup fn dispatch_ip( &mut self, // NOTE(unused_mut): tx_token isn't always mutated, depending on @@ -1059,6 +1501,92 @@ impl InterfaceInner { #[allow(unused_mut)] mut tx_token: Tx, meta: PacketMeta, packet: Packet, + previous_hop: Option<&HardwareAddress>, + frag: &mut Fragmenter, + multicast_queue: &mut PacketBuffer<'_, MulticastMetadata>, + ) -> Result<(), DispatchError> { + let (hardware_addr, tx_token) = self.handle_hardware_addr_lookup( + &packet, + meta, + previous_hop, + frag, + multicast_queue, + tx_token, + )?; + + self.transmit_ip(tx_token, meta, packet, hardware_addr, frag) + } + + fn handle_hardware_addr_lookup( + &mut self, + packet: &Packet, + meta: PacketMeta, + previous_hop: Option<&HardwareAddress>, + frag: &mut Fragmenter, + multicast_queue: &mut PacketBuffer<'_, MulticastMetadata>, + tx_token: Tx, + ) -> Result<(HardwareAddress, Tx), DispatchError> + where + Tx: TxToken, + { + let (mut addr, tx_token) = match self.caps.medium { + medium + if matches_cfg!([feature = "medium-ethernet"] medium, Medium::Ethernet) + || matches_cfg!( + [feature = "medium-ieee802154"] + medium, + Medium::Ieee802154 + ) => + { + self.lookup_hardware_addr( + tx_token, + previous_hop, + &packet.ip_repr().src_addr(), + &packet.ip_repr().dst_addr(), + frag, + )? + } + #[cfg(feature = "medium-ethernet")] + _ => ( + heapless::Vec::from_iter(core::iter::once(HardwareAddress::Ethernet( + EthernetAddress([0; 6]), + ))), + tx_token, + ), + #[cfg(all(not(feature = "medium-ethernet"), feature = "medium-ieee802154"))] + _ => ( + heapless::Vec::from_iter(core::iter::once(HardwareAddress::Ieee802154( + Ieee802154Address::BROADCAST, + ))), + tx_token, + ), + }; + let first_addr = addr.pop().ok_or(DispatchError::NoRoute)?; + + if !addr.is_empty() { + match packet { + #[cfg(feature = "proto-ipv4")] + Packet::Ipv4(_) => unimplemented!(), + #[cfg(feature = "proto-ipv6")] + Packet::Ipv6(packet) => { + if !addr.is_empty() { + self.schedule_multicast_packet(meta, packet, addr, multicast_queue)?; + } + } + } + } + + Ok((first_addr, tx_token)) + } + + fn transmit_ip( + &mut self, + // NOTE(unused_mut): tx_token isn't always mutated, depending on + // the feature set that is used. + #[allow(unused_mut)] mut tx_token: Tx, + meta: PacketMeta, + packet: Packet, + hardware_addr: HardwareAddress, frag: &mut Fragmenter, ) -> Result<(), DispatchError> { let mut ip_repr = packet.ip_repr(); @@ -1068,15 +1596,21 @@ impl InterfaceInner { #[cfg(feature = "medium-ieee802154")] if matches!(self.caps.medium, Medium::Ieee802154) { - let (addr, tx_token) = self.lookup_hardware_addr( + // Schedule the remaining multicast transmissions + let packet = match packet { + Packet::Ipv6(packet) => packet, + #[allow(unreachable_patterns)] + _ => unreachable!(), + }; + + self.dispatch_ieee802154( + hardware_addr.ieee802154_or_panic(), tx_token, - &ip_repr.src_addr(), - &ip_repr.dst_addr(), + meta, + packet, frag, - )?; - let addr = addr.ieee802154_or_panic(); + ); - self.dispatch_ieee802154(addr, tx_token, meta, packet, frag); return Ok(()); } @@ -1098,18 +1632,8 @@ impl InterfaceInner { // If the medium is Ethernet, then we need to retrieve the destination hardware address. #[cfg(feature = "medium-ethernet")] - let (dst_hardware_addr, mut tx_token) = match self.caps.medium { - Medium::Ethernet => { - match self.lookup_hardware_addr( - tx_token, - &ip_repr.src_addr(), - &ip_repr.dst_addr(), - frag, - )? { - (HardwareAddress::Ethernet(addr), tx_token) => (addr, tx_token), - (_, _) => unreachable!(), - } - } + let (hardware_addr, mut tx_token) = match self.caps.medium { + Medium::Ethernet => (hardware_addr.ethernet_or_panic(), tx_token), _ => (EthernetAddress([0; 6]), tx_token), }; @@ -1120,7 +1644,7 @@ impl InterfaceInner { let src_addr = self.hardware_addr.ethernet_or_panic(); frame.set_src_addr(src_addr); - frame.set_dst_addr(dst_hardware_addr); + frame.set_dst_addr(hardware_addr); match repr.version() { #[cfg(feature = "proto-ipv4")] @@ -1137,7 +1661,7 @@ impl InterfaceInner { repr.emit(&mut tx_buffer, &self.caps.checksum); let payload = &mut tx_buffer[repr.header_len()..]; - packet.emit_payload(repr, payload, &caps) + packet.emit_payload(payload, &caps) }; let total_ip_len = ip_repr.buffer_len(); @@ -1167,7 +1691,7 @@ impl InterfaceInner { #[cfg(feature = "medium-ethernet")] { - frag.ipv4.dst_hardware_addr = dst_hardware_addr; + frag.ipv4.dst_hardware_addr = hardware_addr; } // Save the total packet len (without the Ethernet header, but with the first @@ -1263,4 +1787,34 @@ enum DispatchError { /// the neighbor for it yet. Discovery has been initiated, dispatch /// should be retried later. NeighborPending, + /// When we cannot immediatly dispatch a packet and need to wait for the + /// underlying physical layer to process its current tasks, a packet may + /// need to be stored somewhere. If this storage buffer is full, we cannot + /// schedule it for later transmission. + Exhausted, +} + +/// Error type for `join_multicast_group`, `leave_multicast_group`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MulticastError { + /// The hardware device transmit buffer is full. Try again later. + Exhausted, + /// The table of joined multicast groups is already full. + GroupTableFull, + /// The addresstype is unsupported + Unaddressable, +} + +impl core::fmt::Display for MulticastError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match self { + MulticastError::Exhausted => write!(f, "Exhausted"), + MulticastError::GroupTableFull => write!(f, "GroupTableFull"), + MulticastError::Unaddressable => write!(f, "Unaddressable"), + } + } } + +#[cfg(feature = "std")] +impl std::error::Error for MulticastError {} diff --git a/src/iface/interface/multicast.rs b/src/iface/interface/multicast.rs new file mode 100644 index 000000000..dbf3e1d34 --- /dev/null +++ b/src/iface/interface/multicast.rs @@ -0,0 +1,80 @@ +use crate::{phy::Device, time::Instant}; + +use super::{Interface, IpPayload, Packet}; + +impl Interface<'_> { + /// Poll the multicast queue and dispatch the next multicast packet if available + pub(super) fn poll_multicast(&mut self, device: &mut D) -> bool + where + D: Device + ?Sized, + { + // Dequeue empty multicast packets + self.flush_multicast_queue(); + + // If we did not find any still active multicast packets, we can stop here + let Ok((meta, payload)) = self.multicast_queue.peek_mut() else { + return true; + }; + // If this panics, something went horibly wrong while checking for a valid multicast packet + let next_ll_addr = meta.pop_next_ll_addr().unwrap(); + + // Rehydrate the multicast packet from the queue + let Ok(packet) = IpPayload::parse_unchecked( + payload, + meta.payload_type(), + meta.header(), + &self.inner.checksum_caps(), + ) + .inspect_err(|_err| net_trace!("Parsing of queued packet has failed, dropping")) else { + return false; + }; + + // Try to acquire a tx_token + let Some(tx_token) = device.transmit(self.inner.now) else { + return false; // Device is busy, retry later + }; + + let metadata = meta.meta(); + let header = *meta.header(); + let _ = self + .inner + .transmit_ip( + tx_token, + metadata, + Packet::new_ipv6(header, packet), + next_ll_addr, + &mut self.fragmenter, + ) + .inspect_err(|err| { + net_trace!( + "Failed to transmit scheduled multicast transmission with reason {:?}", + err + ) + }); + + true + } + + /// Request to poll again asap if there are still packets to be transmitted in the queue + pub(super) fn poll_at_multicast(&mut self) -> Option { + if !self.multicast_queue.is_empty() { + Some(self.inner.now) + } else { + None + } + } + + /// Remove empty multicast packets from the multicast queue + fn flush_multicast_queue(&mut self) { + // We may get an error if the queue is empty, but then flushing was succesful + let _ = self.multicast_queue.dequeue_with( + |meta, _packet| { + if meta.finished() { + Ok(()) + } else { + Err(123) + } + }, + ); + } +} diff --git a/src/iface/interface/rpl.rs b/src/iface/interface/rpl.rs new file mode 100644 index 000000000..f250cc64c --- /dev/null +++ b/src/iface/interface/rpl.rs @@ -0,0 +1,1270 @@ +use super::*; +use crate::iface::interface::ipv6::RoutingResponse; + +#[cfg(feature = "rpl-mop-1")] +use crate::wire::Ipv6RoutingRepr; + +#[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] +use crate::wire::{Ipv6HopByHopRepr, Ipv6OptionRepr, RplDao, RplDaoAck}; + +use crate::iface::rpl::*; +use heapless::Vec; + +impl Interface<'_> { + pub(super) fn poll_rpl(&mut self, device: &mut D) -> bool + where + D: Device + ?Sized, + { + fn transmit( + ctx: &mut InterfaceInner, + device: &mut D, + packet: Packet, + previous_hop: Option<&HardwareAddress>, + fragmenter: &mut Fragmenter, + multicast_queue: &mut PacketBuffer<'_, MulticastMetadata>, + ) -> bool + where + D: Device + ?Sized, + { + let Some(tx_token) = device.transmit(ctx.now) else { + return false; + }; + + match ctx.dispatch_ip( + tx_token, + PacketMeta::default(), + packet, + previous_hop, + fragmenter, + multicast_queue, + ) { + Ok(()) => true, + Err(e) => { + net_debug!("failed to send packet: {:?}", e); + false + } + } + } + + let Interface { + inner: ctx, + fragmenter, + multicast_queue, + .. + } = self; + + // When we are not the root and we are not part of any DODAG, make sure to transmit + // a DODAG Information Solicitation (DIS) message. Only transmit this message when + // the DIS timer is expired. + if !ctx.rpl.is_root && ctx.rpl.dodag.is_none() && ctx.now >= ctx.rpl.dis_expiration { + net_trace!("transmitting RPL DIS"); + ctx.rpl.dis_expiration = ctx.now + Duration::from_secs(60); + + let dis = RplRepr::DodagInformationSolicitation(RplDis { + options: Default::default(), + }); + let icmp_rpl = Icmpv6Repr::Rpl(dis); + + let ipv6_repr = Ipv6Repr { + src_addr: ctx.ipv6_addr().unwrap(), + dst_addr: Ipv6Address::LINK_LOCAL_ALL_RPL_NODES, + next_header: IpProtocol::Icmpv6, + payload_len: icmp_rpl.buffer_len(), + hop_limit: 64, + }; + + return transmit( + ctx, + device, + Packet::new_ipv6(ipv6_repr, IpPayload::Icmpv6(icmp_rpl)), + None, + fragmenter, + multicast_queue, + ); + } + + let our_addr = ctx.ipv6_addr().unwrap(); + let Some(dodag) = &mut ctx.rpl.dodag else { + return false; + }; + + flush_relations(ctx.rpl.mode_of_operation, dodag, ctx.rpl.is_root, ctx.now); + + // Schedule a DAO before the route will expire. + if let Some(parent_address) = dodag.parent { + let parent = dodag.parent_set.find(&parent_address).unwrap(); + + // If we did not hear from our parent for some time, + // remove our parent. Ideally, we should check if we could find another parent. + if parent.last_heard < ctx.now - dodag.dio_timer.max_expiration() * 2 { + // dodag.remove_parent( + // ctx.rpl.mode_of_operation, + // our_addr, + // &ctx.rpl.of, + // ctx.now, + // &mut ctx.rand, + // ); + dodag.remove_parent(); + dodag.find_new_parent( + ctx.rpl.mode_of_operation, + &[our_addr], // FIXME: what about multiple unicast targets + { + #[cfg(feature = "rpl-mop-3")] + { + &ctx.rpl_targets_multicast + } + #[cfg(not(feature = "rpl-mop-3"))] + &[] + }, + &ctx.rpl.of, + ctx.now, + &mut ctx.rand, + ); + + net_trace!("transmitting DIO (INFINITE rank)"); + let mut options = heapless::Vec::new(); + options.push(ctx.rpl.dodag_configuration()).unwrap(); + + let icmp = Icmpv6Repr::Rpl(ctx.rpl.dodag_information_object(options)); + + let ipv6_repr = Ipv6Repr { + src_addr: ctx.ipv6_addr().unwrap(), + dst_addr: Ipv6Address::LINK_LOCAL_ALL_RPL_NODES, + next_header: IpProtocol::Icmpv6, + payload_len: icmp.buffer_len(), + hop_limit: 64, + }; + + ctx.rpl.dodag = None; + ctx.rpl.dis_expiration = ctx.now + Duration::from_secs(5); + + return transmit( + ctx, + device, + Packet::new_ipv6(ipv6_repr, IpPayload::Icmpv6(icmp)), + None, + fragmenter, + multicast_queue, + ); + } + + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + if dodag.dao_expiration <= ctx.now { + let _ = dodag + .schedule_dao( + ctx.rpl.mode_of_operation, + &[our_addr], + { + #[cfg(feature = "rpl-mop-3")] + { + &ctx.rpl_targets_multicast[..] + } + #[cfg(not(feature = "rpl-mop-3"))] + &[] + }, + parent_address, + ctx.now, + false, + ) + .inspect_err(|err| net_trace!("Could not transmit DAO with reason: {}", err)); + } + } + + // Transmit any DAO-ACK that are queued. + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + if !dodag.dao_acks.is_empty() { + // Transmit all the DAO-ACKs that are still queued. + net_trace!("transmit DAO-ACK"); + + #[allow(unused_mut)] + let (mut dst_addr, sequence) = dodag.dao_acks.pop().unwrap(); + let rpl_instance_id = dodag.instance_id; + let icmp = Icmpv6Repr::Rpl(RplRepr::DestinationAdvertisementObjectAck(RplDaoAck { + rpl_instance_id, + sequence, + status: 0, + dodag_id: if rpl_instance_id.is_local() { + Some(dodag.id) + } else { + None + }, + })); + + let mut p = PacketV6::new( + Ipv6Repr { + src_addr: ctx.ipv6_addr().unwrap(), + dst_addr, + next_header: IpProtocol::Icmpv6, + payload_len: icmp.buffer_len(), + hop_limit: 64, + }, + IpPayload::Icmpv6(icmp), + ); + + // A DAO-ACK always goes down. In MOP1, both Hop-by-Hop option and source + // routing header MAY be included. However, a source routing header must always + // be included when it is going down. + #[cfg(feature = "rpl-mop-1")] + if matches!(ctx.rpl.mode_of_operation, ModeOfOperation::NonStoringMode) + && ctx.rpl.is_root + { + net_trace!("creating source routing header to {}", dst_addr); + if let Some((source_route, new_dst_addr)) = + create_source_routing_header(ctx, our_addr, dst_addr) + { + p.header_mut().dst_addr = new_dst_addr; + p.add_routing(source_route); + return transmit( + ctx, + device, + Packet::Ipv6(p), + None, + fragmenter, + multicast_queue, + ); + } + }; + + let mut options = heapless::Vec::new(); + options + .push(Ipv6OptionRepr::Rpl(RplHopByHopRepr { + down: true, + rank_error: false, + forwarding_error: false, + instance_id: ctx.rpl.dodag.as_ref().unwrap().instance_id, + sender_rank: ctx.rpl.dodag.as_ref().unwrap().rank.raw_value(), + })) + .unwrap(); + p.add_hop_by_hop(Ipv6HopByHopRepr { options }); + return transmit( + ctx, + device, + Packet::Ipv6(p), + None, + fragmenter, + multicast_queue, + ); + } + + // Transmit any DAO that are queued. + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + if !dodag.daos.is_empty() { + // Remove DAOs that have been transmitted 3 times and did not get acknowledged. + // TODO: we should be able to remove the parent when it was actually not acknowledged + // after 3 times. This means that there is no valid path to the parent. + dodag.daos.retain(|dao| { + (!dao.is_no_path && dao.sent_count < 4) || (dao.is_no_path && dao.sent_count == 0) + }); + + // Go over each queued DAO and check if they need to be transmitted. + dodag.daos.iter_mut().for_each(|dao| { + if !dao.needs_sending { + let Some(next_tx) = dao.next_tx else { + let next_tx = dodag.dio_timer.min_expiration(); + // Add a random noise offset between 0ms up to 128ms + let noise = Duration::from_millis((ctx.rand.rand_u16() % 0x4) as u64) * 32; + // We always want the multicast DAO to come later than + // similary scheduled unicast DAOs, so we introduce here + // a small offset such that the unicast DAO always + // arrives first if scheduled at the same time. + let multicast_bias = if dao.has_multicast_target() { + Duration::from_millis(128) + } else { + Duration::from_micros(0) + }; + + dao.next_tx = Some(ctx.now + next_tx + multicast_bias + noise); + return; + }; + + if next_tx < ctx.now { + dao.needs_sending = true; + } + } + }); + + if let Some(dao) = dodag.daos.iter_mut().find(|dao| dao.needs_sending) { + dao.next_tx = Some(ctx.now + Duration::from_secs(60)); + dao.sent_count += 1; + dao.needs_sending = false; + let dst_addr = dao.to; + + let icmp = Icmpv6Repr::Rpl(dao.as_rpl_dao_repr()); + + let mut options = heapless::Vec::new(); + options + .push(Ipv6OptionRepr::Rpl(RplHopByHopRepr { + down: ctx.rpl.is_root, + rank_error: false, + forwarding_error: false, + instance_id: dodag.instance_id, + sender_rank: dao.rank.raw_value(), + })) + .unwrap(); + + let hbh = Ipv6HopByHopRepr { options }; + + let mut p = PacketV6::new( + Ipv6Repr { + src_addr: our_addr, + dst_addr, + next_header: IpProtocol::Icmpv6, + payload_len: icmp.buffer_len(), + hop_limit: 64, + }, + IpPayload::Icmpv6(icmp), + ); + p.add_hop_by_hop(hbh); + + net_trace!("transmitting DAO"); + return transmit( + ctx, + device, + Packet::Ipv6(p), + None, + fragmenter, + multicast_queue, + ); + } + } + + // When we are part of a DODAG, we should check if our DIO Trickle timer + // expired. If it expires, a DODAG Information Object (DIO) should be + // transmitted. + if (ctx.rpl.is_root || dodag.parent.is_some()) + && dodag.dio_timer.poll(ctx.now, &mut ctx.rand) + { + let mut options = heapless::Vec::new(); + options.push(ctx.rpl.dodag_configuration()).unwrap(); + + let icmp = Icmpv6Repr::Rpl(ctx.rpl.dodag_information_object(options)); + + let ipv6_repr = Ipv6Repr { + src_addr: our_addr, + dst_addr: Ipv6Address::LINK_LOCAL_ALL_RPL_NODES, + next_header: IpProtocol::Icmpv6, + payload_len: icmp.buffer_len(), + hop_limit: 64, + }; + + return transmit( + ctx, + device, + Packet::new_ipv6(ipv6_repr, IpPayload::Icmpv6(icmp)), + None, + fragmenter, + multicast_queue, + ); + } + + false + } + + pub(super) fn poll_at_rpl(&mut self) -> Instant { + let ctx = self.context(); + + if let Some(dodag) = &ctx.rpl.dodag { + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + if !dodag.daos.is_empty() || !dodag.dao_acks.is_empty() { + return Instant::from_millis(0); + } + + dodag.dio_timer.poll_at() + } else { + ctx.rpl.dis_expiration + } + } +} + +/// Flush old relations. When a relation was removed, we increment the DTSN, which will trigger +/// DAO's from our children. +fn flush_relations( + mode_of_operation: ModeOfOperation, + dodag: &mut Dodag, + _is_root: bool, + now: Instant, +) { + // Remove stale relations and increment the DTSN when we actually removed + // a relation. This means that a node forgot to retransmit a DAO on time. + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + if dodag.relations.flush(now) + && match mode_of_operation { + #[cfg(feature = "rpl-mop-1")] + ModeOfOperation::NonStoringMode if _is_root => true, + #[cfg(feature = "rpl-mop-2")] + ModeOfOperation::StoringMode => true, + #[cfg(feature = "rpl-mop-3")] + ModeOfOperation::StoringModeWithMulticast => true, + _ => false, + } + //&& dodag.dtsn_incremented_at < dodag.dio_timer.next_expiration() + { + net_trace!("incrementing DTSN"); + // FIXME: maybe this is not needed and we always increment the DTSN when we removed a + // relation from the relation table. + //dodag.dtsn_incremented_at = dodag.dio_timer.next_expiration(); + dodag.dtsn.increment(); + } +} + +impl InterfaceInner { + /// Get a reference to the RPL configuration. + pub fn rpl(&self) -> &crate::iface::rpl::Rpl { + &self.rpl + } + + /// Process an incoming RPL packet. + pub(super) fn process_rpl<'output, 'payload: 'output>( + &mut self, + ip_repr: Ipv6Repr, + repr: RplRepr<'payload>, + ) -> Option> { + match repr { + RplRepr::DodagInformationSolicitation(dis) => self.process_rpl_dis(ip_repr, dis), + RplRepr::DodagInformationObject(dio) => self.process_rpl_dio(ip_repr, dio), + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + RplRepr::DestinationAdvertisementObject(dao) => self.process_rpl_dao(ip_repr, dao), + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + RplRepr::DestinationAdvertisementObjectAck(dao_ack) => { + self.process_rpl_dao_ack(ip_repr, dao_ack) + } + #[allow(unreachable_patterns)] + _ => { + net_trace!("packet not supported in curent MOP"); + None + } + } + } + + /// Process an incoming RPL DIS packet. + pub(super) fn process_rpl_dis<'output, 'payload: 'output>( + &mut self, + ip_repr: Ipv6Repr, + dis: RplDis<'payload>, + ) -> Option> { + // We cannot handle a DIS when we are not part of any DODAG. + let dodag = self.rpl.dodag.as_mut()?; + + if let Some(frame) = self.current_frame.as_ref() { + self.neighbor_cache.fill_with_expiration( + ip_repr.src_addr.into(), + frame.src_addr.unwrap().into(), + self.now + dodag.dio_timer.max_expiration() * 2, + ); + } + + // Options that are expected: Pad1, PadN, Solicited Information. + for opt in dis.options { + // RFC6550 section 8.3: + // The solicited information option is used for filtering incoming DIS + // packets. This option will contain predicates, which we need to match on. + // When we match all to requested predicates, then we answer with a DIO, + // otherwise we just drop the packet. + if let RplOptionRepr::SolicitedInformation(info) = opt { + if (info.version_predicate && dodag.version_number != info.version_number) + || (info.dodag_id_predicate && dodag.id != info.dodag_id) + || (info.instance_id_predicate && dodag.instance_id != info.rpl_instance_id) + { + net_trace!("predicates did not match, dropping packet"); + return None; + } + } + } + + // When receiving a unicast DIS message, we should respond with a unicast DIO, + // containing the DODAG Information option, without resetting the Trickle timer. + if ip_repr.dst_addr.is_unicast() { + net_trace!("unicast DIS, sending unicast DIO"); + + let mut options = Vec::new(); + _ = options.push(self.rpl.dodag_configuration()); + + let dio = Icmpv6Repr::Rpl(self.rpl.dodag_information_object(options)); + + Some(Packet::new_ipv6( + Ipv6Repr { + src_addr: self.ipv6_addr().unwrap(), + dst_addr: ip_repr.src_addr, + next_header: IpProtocol::Icmpv6, + payload_len: dio.buffer_len(), + hop_limit: 64, + }, + IpPayload::Icmpv6(dio), + )) + } else { + net_trace!("received DIS, resetting trickle timer"); + + // Resest the trickle timer (section 8.3) + dodag.dio_timer.hear_inconsistency(self.now, &mut self.rand); + + None + } + } + + /// Process an incoming RPL DIO packet. + pub(super) fn process_rpl_dio<'output, 'payload: 'output>( + &mut self, + ip_repr: Ipv6Repr, + dio: RplDio<'payload>, + ) -> Option> { + if let Some(frame) = self.current_frame.as_ref() { + if let Some(dodag) = self.rpl.dodag.as_ref() { + self.neighbor_cache.fill_with_expiration( + ip_repr.src_addr.into(), + frame.src_addr.unwrap().into(), + self.now + dodag.dio_timer.max_expiration() * 2, + ); + } else { + self.neighbor_cache.fill( + ip_repr.src_addr.into(), + frame.src_addr.unwrap().into(), + self.now, + ); + } + } + + let mut dodag_configuration = None; + + // Options that are expected: Pad1, PadN, DAG Metric Container, Routing Information, DODAG + // Configuration and Prefix Information. + for opt in dio.options { + match opt { + RplOptionRepr::DagMetricContainer => { + // NOTE(thvdveld): We don't support DAG Metric containers yet. They contain + // information about node, link or path metrics specified in RFC6551. The + net_trace!("Dag Metric Container Option not yet supported"); + } + RplOptionRepr::RouteInformation { .. } => { + // The root of a DODAG is responsible for setting the option values. + + // NOTE: RIOT and Contiki-NG don't implement the handling of the route + // information option. smoltcp does not handle prefic information + // packets, neither does it handle the route information packets from + // RFC4191. Therefore, the infrastructure is not in place for handling + // this option in RPL. This is considered future work! + net_trace!("Route Information Option not yet supported"); + } + // The root of a DODAG is responsible for setting the option values. + // This information is propagated down the DODAG unchanged. + RplOptionRepr::PrefixInformation { .. } => { + // FIXME(thvdveld): handle a prefix information option. + net_trace!("Prefix Information Option not yet supported"); + } + // The dodag configuration option contains information about trickle timer, + // default route lifetime, objective code point, etc. + RplOptionRepr::DodagConfiguration( + conf @ RplDodagConfiguration { + objective_code_point, + .. + }, + ) => { + // If we are not part of a network, and the OCP is not the same as + // ours, then we don't accept the DIO packet. + if self.rpl.dodag.is_none() + && objective_code_point != self.rpl.of.objective_code_point() + { + net_trace!("dropping packet, OCP is not compatible"); + return None; + } + + dodag_configuration = Some(conf); + } + _ => {} + } + } + + let sender_rank = Rank::new(dio.rank, self.rpl.of.min_hop_rank_increase()); + + // Accept DIO if not part of DODAG + // =============================== + // If we are not part of a DODAG, check the MOP and OCP. If they are the same as + // ours, we copy the fields of the DIO and the DODAG Configuration. If we cannot + // check the OCP (because the DODAG Configuration option is missing), then we + // transmit a unicast DIS to the sender of the DIO we received. The sender MUST + // respond with a unicast DIO with the option present. + if !self.rpl.is_root + && self.rpl.dodag.is_none() + && ModeOfOperation::from(dio.mode_of_operation) == self.rpl.mode_of_operation + && sender_rank != Rank::INFINITE + { + let Some(dodag_conf) = dodag_configuration else { + // Send a unicast DIS. + net_trace!("sending unicast DIS (to ask for DODAG Conf. option)"); + + let icmp = Icmpv6Repr::Rpl(RplRepr::DodagInformationSolicitation(RplDis { + options: Default::default(), + })); + + return Some(Packet::new_ipv6( + Ipv6Repr { + src_addr: self.ipv6_addr().unwrap(), + dst_addr: ip_repr.src_addr, + next_header: IpProtocol::Icmpv6, + payload_len: icmp.buffer_len(), + hop_limit: 64, + }, + IpPayload::Icmpv6(icmp), + )); + }; + + net_trace!( + "accepting new RPL conf (grounded={} pref={} version={} InstanceID={:?} DODAGID={})", + dio.grounded, + dio.dodag_preference, + dio.version_number, + dio.rpl_instance_id, + dio.dodag_id + ); + + self.rpl + .of + .set_min_hop_rank_increase(dodag_conf.minimum_hop_rank_increase); + self.rpl + .of + .set_max_rank_increase(dodag_conf.max_rank_increase); + + let dodag = Dodag { + instance_id: dio.rpl_instance_id, + id: dio.dodag_id, + version_number: dio.version_number, + preference: dio.dodag_preference, + rank: Rank::INFINITE, + dio_timer: TrickleTimer::new( + dodag_conf.dio_interval_min as u32, + dodag_conf.dio_interval_min as u32 + dodag_conf.dio_interval_doublings as u32, + dodag_conf.dio_redundancy_constant as usize, + ), + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + dao_expiration: crate::time::Instant::ZERO, + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + dao_seq_number: RplSequenceCounter::default(), + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + dao_acks: Default::default(), + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + daos: Default::default(), + parent: None, + without_parent: Some(self.now), + authentication_enabled: dodag_conf.authentication_enabled, + path_control_size: dodag_conf.path_control_size, + dtsn: RplSequenceCounter::default(), + dtsn_incremented_at: self.now, + default_lifetime: dodag_conf.default_lifetime, + lifetime_unit: dodag_conf.lifetime_unit, + grounded: dio.grounded, + parent_set: Default::default(), + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + relations: Default::default(), + }; + + self.rpl.dodag = Some(dodag); + } + + // The sender rank might be updated by the configuration option. + let sender_rank = Rank::new(dio.rank, self.rpl.of.min_hop_rank_increase()); + + let our_addr = self.ipv6_addr().unwrap(); + let dodag = self.rpl.dodag.as_mut()?; + + // Check DIO validity + // ================== + // We check if we can accept the DIO message: + // 1. The RPL instance is the same as our RPL instance. + // 2. The DODAG ID must be the same as our DODAG ID. + // 3. The version number must be the same or higher than ours. + // 4. The Mode of Operation must be the same as our Mode of Operation. + // 5. The Objective Function must be the same as our Ojbective ObjectiveFunction, + // which we already checked. + if dio.rpl_instance_id != dodag.instance_id + || dio.dodag_id != dodag.id + || dio.version_number < dodag.version_number + || ModeOfOperation::from(dio.mode_of_operation) != self.rpl.mode_of_operation + { + net_trace!( + "dropping DIO packet (different INSTANCE ID/DODAG ID/MOP/lower Version Number)" + ); + return None; + } + + // Global repair + // ============= + // If the Version number is higher than ours, we need to clear our parent set, + // remove our parent and reset our rank. + // + // When we are the root, we change the version number to one higher than the + // received one. Then we reset the Trickle timer, such that the information is + // propagated in the network. + if dio.version_number > dodag.version_number { + net_trace!("version number higher than ours"); + + if self.rpl.is_root { + net_trace!("(root) using new version number + 1"); + + dodag.version_number = dio.version_number; + dodag.version_number.increment(); + + net_trace!("(root) resetting Trickle timer"); + // Reset the trickle timer. + dodag.dio_timer.hear_inconsistency(self.now, &mut self.rand); + return None; + } else { + net_trace!("resetting parent set, resetting rank, removing parent"); + + dodag.version_number = dio.version_number; + + // Clear the parent set, . + dodag.parent_set.clear(); + + // We do NOT send a No-path DAO. + // let _ = dodag.remove_parent( + // self.rpl.mode_of_operation, + // // our_addr, + // &self.rpl.of, + // self.now, + // &mut self.rand, + // ); + let _ = dodag.remove_parent(); + dodag.find_new_parent( + self.rpl.mode_of_operation, + &[our_addr], // FIXME + { + #[cfg(feature = "rpl-mop-3")] + { + &self.rpl_targets_multicast[..] + } + #[cfg(not(feature = "rpl-mop-3"))] + &[][..] + }, + &self.rpl.of, + self.now, + &mut self.rand, + ); + + let dio = Icmpv6Repr::Rpl(self.rpl.dodag_information_object(Default::default())); + + // Transmit a DIO with INFINITE rank, but with an updated Version number. + // Everyone knows they have to leave the network and form a new one. + return Some(Packet::new_ipv6( + Ipv6Repr { + src_addr: self.ipv6_addr().unwrap(), + dst_addr: Ipv6Address::LINK_LOCAL_ALL_RPL_NODES, + next_header: IpProtocol::Icmpv6, + payload_len: dio.buffer_len(), + hop_limit: 64, + }, + IpPayload::Icmpv6(dio), + )); + } + } + + if Some(ip_repr.src_addr) == dodag.parent { + // If our parent transmits a DIO with an infinite rank, than it means that our + // parent is leaving the network. Thus we should deselect it as our parent. + // If there is no parent in the parent set, we also detach from the network by + // sending a DIO with an infinite rank. + if Rank::new(dio.rank, self.rpl.of.min_hop_rank_increase()) == Rank::INFINITE { + net_trace!("parent leaving, removing parent"); + + // Don't need to send a no-path DOA when parent is leaving. + // let _ = dodag.remove_parent( + // self.rpl.mode_of_operation, + // // our_addr, + // &self.rpl.of, + // self.now, + // &mut self.rand, + // ); + let _ = dodag.remove_parent(); + dodag.find_new_parent( + self.rpl.mode_of_operation, + &[our_addr], // FIXME + { + #[cfg(feature = "rpl-mop-3")] + { + &self.rpl_targets_multicast[..] + } + #[cfg(not(feature = "rpl-mop-3"))] + &[] + }, + &self.rpl.of, + self.now, + &mut self.rand, + ); + + if dodag.parent.is_some() { + dodag.dio_timer.hear_inconsistency(self.now, &mut self.rand); + } else { + net_trace!("no potential parents, leaving network"); + + // DIO with INFINITE rank. + let dio = + Icmpv6Repr::Rpl(self.rpl.dodag_information_object(Default::default())); + + return Some(Packet::new_ipv6( + Ipv6Repr { + src_addr: self.ipv6_addr().unwrap(), + dst_addr: Ipv6Address::LINK_LOCAL_ALL_RPL_NODES, + next_header: IpProtocol::Icmpv6, + payload_len: dio.buffer_len(), + hop_limit: 64, + }, + IpPayload::Icmpv6(dio), + )); + } + } else { + // Update the time we last heard our parent. + let Some(parent) = dodag.parent_set.find_mut(&dodag.parent.unwrap()) else { + unreachable!(); + }; + + parent.last_heard = self.now; + + // RFC 6550 section 9.6: + // If a node hears one of its parents increase the DTSN, the node MUST + // schedule a DAO. In non-storing mode, a node should increment its own DTSN. + if dio.dtsn > parent.dtsn { + #[cfg(any( + feature = "rpl-mop-1", + feature = "rpl-mop-2", + feature = "rpl-mop-3" + ))] + { + net_trace!("DTSN increased, scheduling DAO"); + dodag.dao_expiration = self.now; + } + + #[cfg(feature = "rpl-mop-1")] + if matches!(self.rpl.mode_of_operation, ModeOfOperation::NonStoringMode) { + dodag.dtsn.increment(); + } + } + + // When we are not the root, we hear a consistency when the DIO message is from + // our parent and is valid. The validity of the message should be checked when we + // reach this line. + dodag.dio_timer.hear_consistency(); + + return None; + } + } + + // Add node to parent set + // ====================== + // If the rank is smaller than ours, the instance id and the mode of operation is + // the same as ours,, we can add the sender to our parent set. + if sender_rank < dodag.rank && !self.rpl.is_root { + if let Err(parent) = dodag.parent_set.add(Parent::new( + ip_repr.src_addr, + sender_rank, + dio.version_number, + dio.dtsn, + dodag.id, + self.now, + )) { + net_trace!("failed to add {} to parent set", parent.address); + } + + // Select parent + // ============= + // Send a no-path DAO to our old parent. + // Select and schedule DAO to new parent. + dodag.find_new_parent( + self.rpl.mode_of_operation, + &[our_addr], + { + #[cfg(feature = "rpl-mop-3")] + { + &self.rpl_targets_multicast[..] + } + #[cfg(not(feature = "rpl-mop-3"))] + &[] + }, + &self.rpl.of, + self.now, + &mut self.rand, + ); + } + + // Trickle Consistency + // =================== + // We should increment the Trickle timer counter for a valid DIO message, + // when we are the root, and the rank that is advertised in the DIO message is + // not infinite (so we received a valid DIO from a child). + if self.rpl.is_root && sender_rank != Rank::INFINITE { + dodag.dio_timer.hear_consistency(); + } + + None + } + + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + pub(super) fn process_rpl_dao<'output, 'payload: 'output>( + &mut self, + ip_repr: Ipv6Repr, + dao: RplDao<'payload>, + ) -> Option> { + let our_addr = self.ipv6_addr().unwrap(); + let dodag = self.rpl.dodag.as_mut()?; + + // Check validity of the DAO + if dodag.instance_id != dao.rpl_instance_id && Some(dodag.id) != dao.dodag_id { + net_trace!("dropping DAO, wrong DODAG ID/INSTANCE ID"); + return None; + } + + #[cfg(feature = "rpl-mop-0")] + if matches!( + self.rpl.mode_of_operation, + ModeOfOperation::NoDownwardRoutesMaintained + ) { + net_trace!("dropping DAO, MOP0 does not support it"); + return None; + } + + #[cfg(feature = "rpl-mop-1")] + if matches!(self.rpl.mode_of_operation, ModeOfOperation::NonStoringMode) + && !self.rpl.is_root + { + net_trace!("dropping DAO, MOP1 and not root"); + return None; + } + + let mut targets: Vec = Vec::new(); + + // Expected options: Pad1, PadN, RPL Target, Transit Information, RPL Target Descriptor. + for opt in &dao.options { + match opt { + RplOptionRepr::RplTarget(target) => { + // FIXME: we only take care of IPv6 addresses. + // However, a RPL target can also be a prefix or a multicast group. + // When receiving such a message, it might break our implementation. + targets + .push(Ipv6Address::from_bytes(&target.prefix)) + .unwrap(); + } + RplOptionRepr::TransitInformation(transit) => { + if transit.path_lifetime == 0 { + // Remove all targets from the relation list since we received a NO-PATH + // DAO. + for target in &targets { + net_trace!("remove {} relation (NO-PATH)", target); + dodag + .relations + .remove_hop_from_relation(*target, ip_repr.src_addr); + } + } else { + let next_hop = match self.rpl.mode_of_operation { + #[cfg(feature = "rpl-mop-0")] + ModeOfOperation::NoDownwardRoutesMaintained => unreachable!(), + #[cfg(feature = "rpl-mop-1")] + ModeOfOperation::NonStoringMode => transit.parent_address.unwrap(), + #[cfg(feature = "rpl-mop-2")] + ModeOfOperation::StoringMode => ip_repr.src_addr, + #[cfg(feature = "rpl-mop-3")] + ModeOfOperation::StoringModeWithMulticast => ip_repr.src_addr, + #[allow(unreachable_patterns)] + _ => unreachable!(), + }; + + for target in &targets { + net_trace!("adding {} => {} relation", target, next_hop); + let _ = dodag + .relations + .add_relation( + *target, + &[next_hop], + self.now, + crate::time::Duration::from_secs( + transit.path_lifetime as u64 * dodag.lifetime_unit as u64, + ), + ) + .inspect_err(|err| { + net_trace!( + "Could not add a relation to the dodag with reason {}", + err + ) + }); + } + + targets.clear(); + } + } + _ => {} + } + } + + net_trace!("RPL relations:"); + for relation in dodag.relations.iter() { + net_trace!(" {}", relation); + } + + // Schedule a DAO-ACK if an ACK is requested. + if dao.expect_ack + && dodag + .dao_acks + .push((ip_repr.src_addr, dao.sequence)) + .is_err() + { + net_trace!("unable to schedule DAO-ACK for {}", dao.sequence); + } + + // Transmit a DAO to our parent if we are not the root. + if !self.rpl.is_root { + let icmp = dodag.destination_advertisement_object(dao.options); + + let mut options = heapless::Vec::new(); + options + .push(Ipv6OptionRepr::Rpl(RplHopByHopRepr { + down: false, + rank_error: false, + forwarding_error: false, + instance_id: dodag.instance_id, + sender_rank: dodag.rank.raw_value(), + })) + .unwrap(); + + let hbh = Ipv6HopByHopRepr { options }; + + let mut p = PacketV6::new( + Ipv6Repr { + src_addr: our_addr, + dst_addr: dodag.parent.unwrap(), + next_header: IpProtocol::Icmpv6, + payload_len: icmp.buffer_len(), + hop_limit: 64, + }, + IpPayload::Icmpv6(Icmpv6Repr::Rpl(icmp)), + ); + p.add_hop_by_hop(hbh); + + return Some(Packet::Ipv6(p)); + } + + None + } + + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + pub(super) fn process_rpl_dao_ack<'output>( + &mut self, + ip_repr: Ipv6Repr, + dao_ack: RplDaoAck, + ) -> Option> { + let RplDaoAck { + rpl_instance_id, + sequence, + status, + dodag_id, + } = dao_ack; + + let dodag = self.rpl.dodag.as_mut()?; + + if rpl_instance_id == dodag.instance_id + && (dodag_id == Some(dodag.id) || dodag_id.is_none()) + { + dodag + .daos + .retain(|dao| !(dao.to == ip_repr.src_addr && dao.sequence == sequence)); + + if status == 0 { + net_trace!("DAO {} acknowledged", sequence); + } else { + // FIXME: the node should do something correct here. + net_trace!("ACK status was {}", status); + } + } + + None + } + + pub(super) fn process_rpl_hopbyhop( + &mut self, + mut hbh: RplHopByHopRepr, + ) -> Result { + let sender_rank = Rank::new(hbh.sender_rank, self.rpl.of.min_hop_rank_increase()); + + // Check for inconsistencies (see 11.2.2.2), which are: + // - If the packet is going down, and the sender rank is higher or equal as ours. + // - If the packet is going up, and the sender rank is lower or equal as ours. + // + // NOTE: the standard says that one rank error is not a critical error and that the packet + // can continue traveling through the DODAG. When the bit is set and another inconsistency + // is detected, the packet should be dropped. One case this might help is when the DODAG + // is moving to a new Version number. However, the standard does not define when a new + // Version number should be used. Therefore, we immediately drop the packet when a Rank + // error is detected, or when the bit was already set. + let rank = self + .rpl + .dodag + .as_ref() + .map(|dodag| dodag.rank) + .unwrap_or(Rank::new(u16::MAX, 1)); + if hbh.rank_error || (hbh.down && rank <= sender_rank) || (!hbh.down && rank >= sender_rank) + { + net_trace!("RPL HBH: inconsistency detected, resetting trickle timer, dropping packet"); + hbh.rank_error = true; + self.rpl + .dodag + .as_mut() + .unwrap() + .dio_timer + .hear_inconsistency(self.now, &mut self.rand); + return Err(Error); + } + + Ok(hbh) + } + + pub(super) fn process_source_routing<'frame>( + &self, + mut ipv6_repr: Ipv6Repr, + ext_hdr: &Ipv6ExtHeader<&'frame [u8]>, + routing: Ipv6SourceRoutingRepr, + payload: &'frame [u8], + ) -> RoutingResponse<'frame> { + let Ipv6SourceRoutingRepr { + mut segments_left, + cmpr_i, + cmpr_e, + pad, + mut addresses, + } = routing; + + for addr in addresses.iter_mut() { + addr.0[..cmpr_e as usize] + .copy_from_slice(&ipv6_repr.src_addr.as_bytes()[..cmpr_e as usize]); + } + + // Calculate the number of addresses left to visit. + let n = (((ext_hdr.header_len() as usize * 8) - pad as usize - (16 - cmpr_e as usize)) + / (16 - cmpr_i as usize)) + + 1; + + if segments_left == 0 { + // We can process the next header. + RoutingResponse::Continue( + ext_hdr.next_header(), + &payload[ext_hdr.payload().len() + 2..], + ) + } else if segments_left as usize > n { + todo!( + "We should send an ICMP Parameter Problem, Code 0, \ + to the source address, pointing to the segments left \ + field, and discard the packet." + ); + } else { + // Decrement the segments left by 1. + segments_left -= 1; + + // Compute i, the index of the next address to be visited in the address + // vector, by substracting segments left from n. + let i = addresses.len() - segments_left as usize; + + let address = addresses[i - 1]; + net_debug!("The next address: {}", address); + + // If Addresses[i] or the Destination address is mutlicast, we discard the + // packet. + + if address.is_multicast() || ipv6_repr.dst_addr.is_multicast() { + net_trace!("Dropping packet, destination address is multicast"); + return RoutingResponse::Discard; + } + + let tmp_addr = ipv6_repr.dst_addr; + ipv6_repr.dst_addr = address; + addresses[i - 1] = tmp_addr; + + if ipv6_repr.hop_limit <= 1 { + net_trace!("hop limit reached 0, dropping packet"); + // FIXME: we should transmit an ICMPv6 Time Exceeded message, as defined + // in RFC 2460. However, this is not trivial with the current state of + // smoltcp. When sending this message back, as much as possible of the + // original message should be transmitted back. This is after updating the + // addresses in the source routing headers. At this time, we only update + // the parsed list of addresses, not the `ip_payload` buffer. It is this + // buffer we would use when sending back the ICMPv6 message. And since we + // can't update that buffer here, we can't update the source routing header + // and it would send back an incorrect header. The standard does not + // specify if we SHOULD or MUST transmit an ICMPv6 message. + RoutingResponse::Discard + } else { + let payload = &payload[ext_hdr.payload().len() + 2..]; + + ipv6_repr.next_header = ext_hdr.next_header(); + ipv6_repr.hop_limit -= 1; + ipv6_repr.payload_len = payload.len(); + + let mut p = PacketV6::new(ipv6_repr, IpPayload::Raw(payload)); + p.add_routing(Ipv6RoutingRepr::Rpl(Ipv6SourceRoutingRepr { + segments_left, + cmpr_i, + cmpr_e, + pad, + addresses, + })); + + RoutingResponse::Forward(Packet::Ipv6(p)) + } + } + } +} + +/// Create a source routing header based on RPL relation information. +#[cfg(feature = "rpl-mop-1")] +pub(crate) fn create_source_routing_header( + ctx: &super::InterfaceInner, + our_addr: Ipv6Address, + dst_addr: Ipv6Address, +) -> Option<(Ipv6RoutingRepr, Ipv6Address)> { + let Some(dodag) = &ctx.rpl.dodag else { + unreachable!() + }; + + let mut route = Vec::::new(); + _ = route.push(dst_addr); + + let mut next = dst_addr; + + loop { + let next_hop = dodag.relations.find_next_hop(next); + if let Some(next_hop) = next_hop.and_then(|hop| hop.first()) { + // We only support unicast in SRH + net_trace!(" via {}", next_hop); + if next_hop.ip == our_addr { + break; + } + + if route.push(next_hop.ip).is_err() { + net_trace!("could not add hop to route buffer"); + return None; + } + + next = next_hop.ip; + } else { + net_trace!("no route found, last next hop is {}", next); + return None; + } + } + + let segments_left = route.len() - 1; + + if segments_left == 0 { + net_trace!("no source routing needed, node is neighbor"); + None + } else { + // Create the route list for the source routing header + let mut addresses = Vec::new(); + for addr in route[..segments_left].iter().rev() { + _ = addresses.push(*addr); + } + + Some(( + Ipv6RoutingRepr::Rpl(Ipv6SourceRoutingRepr { + segments_left: segments_left as u8, + cmpr_i: 0, + cmpr_e: 0, + pad: 0, + addresses, + }), + route[segments_left], + )) + } +} diff --git a/src/iface/interface/sixlowpan.rs b/src/iface/interface/sixlowpan.rs index 3caa446f5..6a75610d1 100644 --- a/src/iface/interface/sixlowpan.rs +++ b/src/iface/interface/sixlowpan.rs @@ -5,7 +5,7 @@ use crate::wire::Result; // TODO: lower. Should be (6lowpan mtu) - (min 6lowpan header size) + (max ipv6 header size) pub(crate) const MAX_DECOMPRESSED_LEN: usize = 1500; -impl Interface { +impl Interface<'_> { /// Process fragments that still need to be sent for 6LoWPAN packets. /// /// This function returns a boolean value indicating whether any packets were @@ -65,6 +65,7 @@ impl InterfaceInner { ieee802154_repr: &Ieee802154Repr, payload: &'payload [u8], f: &'output mut FragmentsBuffer, + multicast_queue: &mut PacketBuffer<'_, MulticastMetadata>, ) -> Option> { let payload = match check!(SixlowpanPacket::dispatch(payload)) { #[cfg(not(feature = "proto-sixlowpan-fragmentation"))] @@ -99,7 +100,14 @@ impl InterfaceInner { } }; - self.process_ipv6(sockets, meta, &check!(Ipv6Packet::new_checked(payload))) + let packet = check!(Ipv6Packet::new_checked(payload)); + self.process_ipv6( + sockets, + meta, + &packet, + ieee802154_repr.src_addr.map(|addr| addr.into()).as_ref(), + multicast_queue, + ) } #[cfg(feature = "proto-sixlowpan-fragmentation")] @@ -289,20 +297,33 @@ impl InterfaceInner { &mut self, mut tx_token: Tx, meta: PacketMeta, - packet: Packet, + mut packet: PacketV6, ieee_repr: Ieee802154Repr, frag: &mut Fragmenter, ) { - let packet = match packet { - #[cfg(feature = "proto-ipv4")] - Packet::Ipv4(_) => unreachable!(), - Packet::Ipv6(packet) => packet, - }; + #[cfg(feature = "proto-rpl")] + if packet.header().dst_addr.is_unicast() + && self.rpl.dodag.is_some() + && packet.hop_by_hop().is_none() + && packet.routing().is_none() + { + let mut options = heapless::Vec::new(); + options + .push(Ipv6OptionRepr::Rpl(RplHopByHopRepr { + down: self.rpl.is_root, + rank_error: false, + forwarding_error: false, + instance_id: self.rpl.dodag.as_ref().unwrap().instance_id, + sender_rank: self.rpl.dodag.as_ref().unwrap().rank.raw_value(), + })) + .unwrap(); + + let hbh = Ipv6HopByHopRepr { options }; + packet.add_hop_by_hop(hbh); + } - // First we calculate the size we are going to need. If the size is bigger than the MTU, - // then we use fragmentation. - let (total_size, compressed_size, uncompressed_size) = - Self::compressed_packet_size(&packet, &ieee_repr); + let sixlowpan_packet = PacketSixlowpan::new(&packet, &ieee_repr); + let total_size = sixlowpan_packet.buffer_len(); let ieee_len = ieee_repr.buffer_len(); @@ -329,14 +350,9 @@ impl InterfaceInner { return; } - let payload_length = packet.header.payload_len; + let payload_length = packet.header().payload_len; - Self::ipv6_to_sixlowpan( - &self.checksum_caps(), - packet, - &ieee_repr, - &mut pkt.buffer[..], - ); + sixlowpan_packet.emit(&mut pkt.buffer[..], &self.checksum_caps()); pkt.sixlowpan.ll_dst_addr = ieee_repr.dst_addr.unwrap(); pkt.sixlowpan.ll_src_addr = ieee_repr.src_addr.unwrap(); @@ -368,7 +384,7 @@ impl InterfaceInner { // // [RFC 4944 § 5.3]: https://datatracker.ietf.org/doc/html/rfc4944#section-5.3 - let header_diff = uncompressed_size - compressed_size; + let header_diff = sixlowpan_packet.header_diff(); let frag1_size = (125 - ieee_len - frag1.buffer_len() + header_diff) / 8 * 8 - header_diff; @@ -409,241 +425,11 @@ impl InterfaceInner { ieee_repr.emit(&mut ieee_packet); tx_buf = &mut tx_buf[ieee_len..]; - Self::ipv6_to_sixlowpan(&self.checksum_caps(), packet, &ieee_repr, tx_buf); + sixlowpan_packet.emit(tx_buf, &self.checksum_caps()); }); } } - fn ipv6_to_sixlowpan( - checksum_caps: &ChecksumCapabilities, - mut packet: PacketV6, - ieee_repr: &Ieee802154Repr, - mut buffer: &mut [u8], - ) { - let last_header = packet.payload.as_sixlowpan_next_header(); - let next_header = last_header; - - #[cfg(feature = "proto-ipv6-hbh")] - let next_header = if packet.hop_by_hop.is_some() { - SixlowpanNextHeader::Compressed - } else { - next_header - }; - - #[cfg(feature = "proto-ipv6-routing")] - let next_header = if packet.routing.is_some() { - SixlowpanNextHeader::Compressed - } else { - next_header - }; - - let iphc_repr = SixlowpanIphcRepr { - src_addr: packet.header.src_addr, - ll_src_addr: ieee_repr.src_addr, - dst_addr: packet.header.dst_addr, - ll_dst_addr: ieee_repr.dst_addr, - next_header, - hop_limit: packet.header.hop_limit, - ecn: None, - dscp: None, - flow_label: None, - }; - - iphc_repr.emit(&mut SixlowpanIphcPacket::new_unchecked( - &mut buffer[..iphc_repr.buffer_len()], - )); - buffer = &mut buffer[iphc_repr.buffer_len()..]; - - // Emit the Hop-by-Hop header - #[cfg(feature = "proto-ipv6-hbh")] - if let Some(hbh) = packet.hop_by_hop { - #[allow(unused)] - let next_header = last_header; - - #[cfg(feature = "proto-ipv6-routing")] - let next_header = if packet.routing.is_some() { - SixlowpanNextHeader::Compressed - } else { - last_header - }; - - let ext_hdr = SixlowpanExtHeaderRepr { - ext_header_id: SixlowpanExtHeaderId::HopByHopHeader, - next_header, - length: hbh.options.iter().map(|o| o.buffer_len()).sum::() as u8, - }; - ext_hdr.emit(&mut SixlowpanExtHeaderPacket::new_unchecked( - &mut buffer[..ext_hdr.buffer_len()], - )); - buffer = &mut buffer[ext_hdr.buffer_len()..]; - - for opt in &hbh.options { - opt.emit(&mut Ipv6Option::new_unchecked( - &mut buffer[..opt.buffer_len()], - )); - - buffer = &mut buffer[opt.buffer_len()..]; - } - } - - // Emit the Routing header - #[cfg(feature = "proto-ipv6-routing")] - if let Some(routing) = &packet.routing { - let ext_hdr = SixlowpanExtHeaderRepr { - ext_header_id: SixlowpanExtHeaderId::RoutingHeader, - next_header, - length: routing.buffer_len() as u8, - }; - ext_hdr.emit(&mut SixlowpanExtHeaderPacket::new_unchecked( - &mut buffer[..ext_hdr.buffer_len()], - )); - buffer = &mut buffer[ext_hdr.buffer_len()..]; - - routing.emit(&mut Ipv6RoutingHeader::new_unchecked( - &mut buffer[..routing.buffer_len()], - )); - buffer = &mut buffer[routing.buffer_len()..]; - } - - match &mut packet.payload { - IpPayload::Icmpv6(icmp_repr) => { - icmp_repr.emit( - &packet.header.src_addr, - &packet.header.dst_addr, - &mut Icmpv6Packet::new_unchecked(&mut buffer[..icmp_repr.buffer_len()]), - checksum_caps, - ); - } - #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] - IpPayload::Udp(udp_repr, payload) => { - let udp_repr = SixlowpanUdpNhcRepr(*udp_repr); - udp_repr.emit( - &mut SixlowpanUdpNhcPacket::new_unchecked( - &mut buffer[..udp_repr.header_len() + payload.len()], - ), - &iphc_repr.src_addr, - &iphc_repr.dst_addr, - payload.len(), - |buf| buf.copy_from_slice(payload), - checksum_caps, - ); - } - #[cfg(feature = "socket-tcp")] - IpPayload::Tcp(tcp_repr) => { - tcp_repr.emit( - &mut TcpPacket::new_unchecked(&mut buffer[..tcp_repr.buffer_len()]), - &packet.header.src_addr.into(), - &packet.header.dst_addr.into(), - checksum_caps, - ); - } - #[cfg(feature = "socket-raw")] - IpPayload::Raw(_raw) => todo!(), - - #[allow(unreachable_patterns)] - _ => unreachable!(), - } - } - - /// Calculates three sizes: - /// - total size: the size of a compressed IPv6 packet - /// - compressed header size: the size of the compressed headers - /// - uncompressed header size: the size of the headers that are not compressed - /// They are returned as a tuple in the same order. - fn compressed_packet_size( - packet: &PacketV6, - ieee_repr: &Ieee802154Repr, - ) -> (usize, usize, usize) { - let last_header = packet.payload.as_sixlowpan_next_header(); - let next_header = last_header; - - #[cfg(feature = "proto-ipv6-hbh")] - let next_header = if packet.hop_by_hop.is_some() { - SixlowpanNextHeader::Compressed - } else { - next_header - }; - - #[cfg(feature = "proto-ipv6-routing")] - let next_header = if packet.routing.is_some() { - SixlowpanNextHeader::Compressed - } else { - next_header - }; - - let iphc = SixlowpanIphcRepr { - src_addr: packet.header.src_addr, - ll_src_addr: ieee_repr.src_addr, - dst_addr: packet.header.dst_addr, - ll_dst_addr: ieee_repr.dst_addr, - next_header, - hop_limit: packet.header.hop_limit, - ecn: None, - dscp: None, - flow_label: None, - }; - - let mut total_size = iphc.buffer_len(); - let mut compressed_hdr_size = iphc.buffer_len(); - let mut uncompressed_hdr_size = packet.header.buffer_len(); - - // Add the hop-by-hop to the sizes. - #[cfg(feature = "proto-ipv6-hbh")] - if let Some(hbh) = &packet.hop_by_hop { - #[allow(unused)] - let next_header = last_header; - - #[cfg(feature = "proto-ipv6-routing")] - let next_header = if packet.routing.is_some() { - SixlowpanNextHeader::Compressed - } else { - last_header - }; - - let options_size = hbh.options.iter().map(|o| o.buffer_len()).sum::(); - - let ext_hdr = SixlowpanExtHeaderRepr { - ext_header_id: SixlowpanExtHeaderId::HopByHopHeader, - next_header, - length: hbh.buffer_len() as u8 + options_size as u8, - }; - - total_size += ext_hdr.buffer_len() + options_size; - compressed_hdr_size += ext_hdr.buffer_len() + options_size; - uncompressed_hdr_size += hbh.buffer_len() + options_size; - } - - // Add the routing header to the sizes. - #[cfg(feature = "proto-ipv6-routing")] - if let Some(routing) = &packet.routing { - let ext_hdr = SixlowpanExtHeaderRepr { - ext_header_id: SixlowpanExtHeaderId::RoutingHeader, - next_header, - length: routing.buffer_len() as u8, - }; - total_size += ext_hdr.buffer_len() + routing.buffer_len(); - compressed_hdr_size += ext_hdr.buffer_len() + routing.buffer_len(); - uncompressed_hdr_size += routing.buffer_len(); - } - - match packet.payload { - #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] - IpPayload::Udp(udp_hdr, payload) => { - uncompressed_hdr_size += udp_hdr.header_len(); - - let udp_hdr = SixlowpanUdpNhcRepr(udp_hdr); - compressed_hdr_size += udp_hdr.header_len(); - - total_size += udp_hdr.header_len() + payload.len(); - } - _ => { - total_size += packet.header.payload_len; - } - } - - (total_size, compressed_hdr_size, uncompressed_hdr_size) - } - #[cfg(feature = "proto-sixlowpan-fragmentation")] pub(super) fn dispatch_sixlowpan_frag( &mut self, @@ -768,6 +554,242 @@ fn decompress_udp( Ok(()) } +struct PacketSixlowpan<'p> { + iphc: SixlowpanIphcRepr, + #[cfg(feature = "proto-ipv6-hbh")] + hbh: Option<(SixlowpanExtHeaderRepr, &'p [Ipv6OptionRepr<'p>])>, + #[cfg(feature = "proto-ipv6-routing")] + routing: Option<(SixlowpanExtHeaderRepr, &'p Ipv6RoutingRepr)>, + payload: SixlowpanPayload<'p>, + + header_diff: usize, +} + +enum SixlowpanPayload<'p> { + Icmpv6(&'p Icmpv6Repr<'p>), + #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] + Udp(UdpRepr, &'p [u8], Option), + #[cfg(feature = "proto-rpl")] + Raw(&'p [u8]), +} + +impl<'p> PacketSixlowpan<'p> { + /// Create a 6LoWPAN compressed representation packet from an IPv6 representation. + fn new(packet: &'p PacketV6<'_>, ieee_repr: &Ieee802154Repr) -> Self { + let mut compressed = 0; + let mut uncompressed = 0; + + let iphc = SixlowpanIphcRepr { + src_addr: packet.header().src_addr, + ll_src_addr: ieee_repr.src_addr, + dst_addr: packet.header().dst_addr, + ll_dst_addr: ieee_repr.dst_addr, + next_header: packet.header().next_header.into(), + hop_limit: packet.header().hop_limit, + ecn: None, + dscp: None, + flow_label: None, + }; + compressed += iphc.buffer_len(); + uncompressed += packet.header().buffer_len(); + + let mut last_header = packet.header().next_header; + + #[cfg(feature = "proto-ipv6-hbh")] + let hbh = if let Some((next_header, hbh)) = packet.hop_by_hop() { + let ext_hdr = SixlowpanExtHeaderRepr { + ext_header_id: SixlowpanExtHeaderId::HopByHopHeader, + next_header: next_header.into(), + length: hbh.options.iter().map(|o| o.buffer_len() as u8).sum(), + }; + + compressed += ext_hdr.buffer_len(); + uncompressed += hbh.buffer_len(); + + last_header = next_header; + Some((ext_hdr, &hbh.options[..])) + } else { + None + }; + + #[cfg(feature = "proto-ipv6-routing")] + let routing = if let Some((next_header, routing)) = packet.routing() { + let ext_hdr = SixlowpanExtHeaderRepr { + ext_header_id: SixlowpanExtHeaderId::RoutingHeader, + next_header: next_header.into(), + length: routing.buffer_len() as u8, + }; + + compressed += ext_hdr.buffer_len() + routing.buffer_len(); + uncompressed += routing.buffer_len(); + + last_header = next_header; + Some((ext_hdr, routing)) + } else { + None + }; + + let payload = match packet.payload() { + IpPayload::Icmpv6(icmp_repr) => SixlowpanPayload::Icmpv6(icmp_repr), + #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] + IpPayload::Udp(udp_repr, payload) => { + compressed += SixlowpanUdpNhcRepr(*udp_repr).header_len(); + uncompressed += udp_repr.header_len(); + + SixlowpanPayload::Udp(*udp_repr, payload, None) + } + #[cfg(feature = "proto-rpl")] + IpPayload::Raw(raw) => { + match last_header { + IpProtocol::Udp => { + // TODO: remove unwrap + let udp_packet = UdpPacket::new_checked(raw).unwrap(); + let udp_repr = UdpRepr::parse( + &udp_packet, + &packet.header().src_addr.into(), + &packet.header().dst_addr.into(), + &ChecksumCapabilities::ignored(), + ) + .unwrap(); + + compressed += SixlowpanUdpNhcRepr(udp_repr).header_len(); + uncompressed += udp_repr.header_len(); + + SixlowpanPayload::Udp( + udp_repr, + udp_packet.payload(), + Some(udp_packet.checksum()), + ) + } + // Any other protocol does not need compression. + _ => SixlowpanPayload::Raw(raw), + } + } + _ => unreachable!(), + }; + + PacketSixlowpan { + iphc, + #[cfg(feature = "proto-ipv6-hbh")] + hbh, + #[cfg(feature = "proto-ipv6-routing")] + routing, + payload, + + header_diff: uncompressed - compressed, + } + } + + /// Return the required length for the underlying buffer when emitting the packet. + fn buffer_len(&self) -> usize { + let mut len = 0; + + len += self.iphc.buffer_len(); + + #[cfg(feature = "proto-ipv6-hbh")] + if let Some((ext_hdr, hbh)) = &self.hbh { + len += ext_hdr.buffer_len(); + len += hbh.iter().map(|o| o.buffer_len()).sum::(); + } + + #[cfg(feature = "proto-ipv6-routing")] + if let Some((ext_hdr, routing)) = &self.routing { + len += ext_hdr.buffer_len() + routing.buffer_len(); + } + + match self.payload { + SixlowpanPayload::Icmpv6(icmp_repr) => len + icmp_repr.buffer_len(), + #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] + SixlowpanPayload::Udp(udp_repr, payload, _) => { + len + SixlowpanUdpNhcRepr(udp_repr).header_len() + payload.len() + } + #[cfg(feature = "proto-rpl")] + SixlowpanPayload::Raw(payload) => len + payload.len(), + } + } + + /// Return the difference between the compressed and uncompressed header sizes. + fn header_diff(&self) -> usize { + self.header_diff + } + + /// Emit the packet into the given buffer. + fn emit(&self, mut buffer: &mut [u8], caps: &ChecksumCapabilities) { + let mut checksum_dst_addr = self.iphc.dst_addr; + + self.iphc.emit(&mut SixlowpanIphcPacket::new_unchecked( + &mut buffer[..self.iphc.buffer_len()], + )); + + buffer = &mut buffer[self.iphc.buffer_len()..]; + + #[cfg(feature = "proto-ipv6-hbh")] + if let Some((ext_hdr, hbh)) = &self.hbh { + ext_hdr.emit(&mut SixlowpanExtHeaderPacket::new_unchecked( + &mut buffer[..ext_hdr.buffer_len()], + )); + buffer = &mut buffer[ext_hdr.buffer_len()..]; + + for opt in hbh.iter() { + opt.emit(&mut Ipv6Option::new_unchecked( + &mut buffer[..opt.buffer_len()], + )); + buffer = &mut buffer[opt.buffer_len()..]; + } + } + + #[cfg(feature = "proto-ipv6-routing")] + if let Some((ext_hdr, routing)) = &self.routing { + if let Ipv6RoutingRepr::Rpl(Ipv6SourceRoutingRepr { addresses, .. }) = routing { + checksum_dst_addr = *addresses.last().unwrap(); + } + + ext_hdr.emit(&mut SixlowpanExtHeaderPacket::new_unchecked( + &mut buffer[..ext_hdr.buffer_len()], + )); + buffer = &mut buffer[ext_hdr.buffer_len()..]; + + routing.emit(&mut Ipv6RoutingHeader::new_unchecked( + &mut buffer[..routing.buffer_len()], + )); + buffer = &mut buffer[routing.buffer_len()..]; + } + + match self.payload { + SixlowpanPayload::Icmpv6(icmp_repr) => icmp_repr.emit( + &self.iphc.src_addr, + &checksum_dst_addr, + &mut Icmpv6Packet::new_unchecked(&mut buffer[..icmp_repr.buffer_len()]), + caps, + ), + #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] + SixlowpanPayload::Udp(udp_repr, payload, checksum) => { + let udp = SixlowpanUdpNhcRepr(udp_repr); + let mut udp_packet = SixlowpanUdpNhcPacket::new_unchecked( + &mut buffer[..udp.header_len() + payload.len()], + ); + udp.emit( + &mut udp_packet, + &self.iphc.src_addr, + &checksum_dst_addr, + payload.len(), + |buf| buf.copy_from_slice(payload), + caps, + ); + + if let Some(checksum) = checksum { + // FIXME: The extra if is probably the result of the existence of a bug in reading the checksum from a packet. This happened while forwarding a UDP packet through multicast where the forwarded checksum suddenly got 0. + if checksum != 0 { + udp_packet.set_checksum(checksum); + } + } + } + #[cfg(feature = "proto-rpl")] + SixlowpanPayload::Raw(payload) => buffer[..payload.len()].copy_from_slice(payload), + } + } +} + #[cfg(test)] #[cfg(all(feature = "proto-rpl", feature = "proto-ipv6-hbh"))] mod tests { @@ -817,6 +839,7 @@ mod tests { } #[test] + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] fn test_sixlowpan_compress_hop_by_hop_with_icmpv6() { let ieee_repr = Ieee802154Repr { frame_type: Ieee802154FrameType::Data, @@ -832,8 +855,18 @@ mod tests { src_addr: Some(Ieee802154Address::Extended([0, 3, 0, 3, 0, 3, 0, 3])), }; - let mut ip_packet = PacketV6 { - header: Ipv6Repr { + let dao = Icmpv6Repr::Rpl(RplRepr::DestinationAdvertisementObject(RplDao { + rpl_instance_id: RplInstanceId::Global(30), + expect_ack: false, + sequence: 241.into(), + dodag_id: Some(Ipv6Address::from_bytes(&[ + 253, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 1, 0, 1, 0, 1, + ])), + options: heapless::Vec::new(), + })); + + let ip_packet = PacketV6::new( + Ipv6Repr { src_addr: Ipv6Address::from_bytes(&[ 253, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 3, 0, 3, 0, 3, ]), @@ -841,35 +874,17 @@ mod tests { 253, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 1, 0, 1, 0, 1, ]), next_header: IpProtocol::Icmpv6, - payload_len: 66, + payload_len: dao.buffer_len(), hop_limit: 64, }, - #[cfg(feature = "proto-ipv6-hbh")] - hop_by_hop: None, - #[cfg(feature = "proto-ipv6-fragmentation")] - fragment: None, - #[cfg(feature = "proto-ipv6-routing")] - routing: None, - payload: IpPayload::Icmpv6(Icmpv6Repr::Rpl(RplRepr::DestinationAdvertisementObject { - rpl_instance_id: RplInstanceId::Global(30), - expect_ack: false, - sequence: 241, - dodag_id: Some(Ipv6Address::from_bytes(&[ - 253, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 1, 0, 1, 0, 1, - ])), - options: &[], - })), - }; + IpPayload::Icmpv6(dao), + ); - let (total_size, _, _) = InterfaceInner::compressed_packet_size(&mut ip_packet, &ieee_repr); + let sixlowpan_packet = PacketSixlowpan::new(&ip_packet, &ieee_repr); + let total_size = sixlowpan_packet.buffer_len(); let mut buffer = vec![0u8; total_size]; - InterfaceInner::ipv6_to_sixlowpan( - &ChecksumCapabilities::default(), - ip_packet, - &ieee_repr, - &mut buffer[..total_size], - ); + sixlowpan_packet.emit(&mut buffer[..total_size], &ChecksumCapabilities::default()); let result = [ 0x7e, 0x0, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x3, 0x0, 0x3, 0x0, 0x3, 0x0, @@ -885,6 +900,7 @@ mod tests { } #[test] + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] fn test_sixlowpan_compress_hop_by_hop_with_udp() { let ieee_repr = Ieee802154Repr { frame_type: Ieee802154FrameType::Data, @@ -915,45 +931,54 @@ mod tests { })) .unwrap(); - let mut ip_packet = PacketV6 { - header: Ipv6Repr { + let mut options = heapless::Vec::new(); + options + .push(RplOptionRepr::RplTarget(RplTarget { + prefix_length: 128, + prefix: heapless::Vec::from_slice(addr.as_bytes()).unwrap(), + })) + .unwrap(); + options + .push(RplOptionRepr::TransitInformation(RplTransitInformation { + external: false, + path_control: 0, + path_sequence: 0, + path_lifetime: 30, + parent_address: Some(parent_address), + })) + .unwrap(); + + let icmp = Icmpv6Repr::Rpl(RplRepr::DestinationAdvertisementObject(RplDao { + rpl_instance_id: RplInstanceId::Global(30), + expect_ack: false, + sequence: 241.into(), + dodag_id: Some(Ipv6Address::from_bytes(&[ + 253, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 1, 0, 1, 0, 1, + ])), + options, + })); + + let mut ip_packet = PacketV6::new( + Ipv6Repr { src_addr: addr, dst_addr: parent_address, next_header: IpProtocol::Icmpv6, - payload_len: 66, + payload_len: icmp.buffer_len(), hop_limit: 64, }, - #[cfg(feature = "proto-ipv6-hbh")] - hop_by_hop: Some(Ipv6HopByHopRepr { - options: hbh_options, - }), - #[cfg(feature = "proto-ipv6-fragmentation")] - fragment: None, - #[cfg(feature = "proto-ipv6-routing")] - routing: None, - payload: IpPayload::Icmpv6(Icmpv6Repr::Rpl(RplRepr::DestinationAdvertisementObject { - rpl_instance_id: RplInstanceId::Global(30), - expect_ack: false, - sequence: 241, - dodag_id: Some(Ipv6Address::from_bytes(&[ - 253, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 1, 0, 1, 0, 1, - ])), - options: &[ - 5, 18, 0, 128, 253, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 3, 0, 3, 0, 3, 6, 20, 0, 0, - 0, 30, 253, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 1, 0, 1, 0, 1, - ], - })), - }; + IpPayload::Icmpv6(icmp), + ); - let (total_size, _, _) = InterfaceInner::compressed_packet_size(&mut ip_packet, &ieee_repr); + #[cfg(feature = "proto-rpl")] + ip_packet.add_hop_by_hop(Ipv6HopByHopRepr { + options: hbh_options, + }); + + let sixlowpan_packet = PacketSixlowpan::new(&ip_packet, &ieee_repr); + let total_size = sixlowpan_packet.buffer_len(); let mut buffer = vec![0u8; total_size]; - InterfaceInner::ipv6_to_sixlowpan( - &ChecksumCapabilities::default(), - ip_packet, - &ieee_repr, - &mut buffer[..total_size], - ); + sixlowpan_packet.emit(&mut buffer[..total_size], &ChecksumCapabilities::default()); let result = [ 0x7e, 0x0, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x3, 0x0, 0x3, 0x0, 0x3, 0x0, diff --git a/src/iface/interface/tests/ipv4.rs b/src/iface/interface/tests/ipv4.rs index 316a9d58a..eb465851d 100644 --- a/src/iface/interface/tests/ipv4.rs +++ b/src/iface/interface/tests/ipv4.rs @@ -41,6 +41,7 @@ fn test_any_ip_accept_arp(#[case] medium: Medium) { PacketMeta::default(), ETHERNET_FRAME_ARP(buffer.as_mut()), &mut iface.fragments, + &mut PacketBuffer::new(vec![], vec![]), ) .is_none()); @@ -54,6 +55,7 @@ fn test_any_ip_accept_arp(#[case] medium: Medium) { PacketMeta::default(), ETHERNET_FRAME_ARP(buffer.as_mut()), &mut iface.fragments, + &mut PacketBuffer::new(vec![], vec![]), ) .is_some()); } @@ -438,7 +440,8 @@ fn test_handle_valid_arp_request(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), frame.into_inner(), - &mut iface.fragments + &mut iface.fragments, + &mut PacketBuffer::new(vec![], vec![]), ), Some(EthernetPacket::Arp(ArpRepr::EthernetIpv4 { operation: ArpOperation::Reply, @@ -453,11 +456,15 @@ fn test_handle_valid_arp_request(#[case] medium: Medium) { assert_eq!( iface.inner.lookup_hardware_addr( MockTxToken, + None, &IpAddress::Ipv4(local_ip_addr), &IpAddress::Ipv4(remote_ip_addr), &mut iface.fragmenter, ), - Ok((HardwareAddress::Ethernet(remote_hw_addr), MockTxToken)) + Ok(( + heapless::Vec::from_iter(core::iter::once(HardwareAddress::Ethernet(remote_hw_addr))), + MockTxToken + )) ); } @@ -493,7 +500,8 @@ fn test_handle_other_arp_request(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), frame.into_inner(), - &mut iface.fragments + &mut iface.fragments, + &mut PacketBuffer::new(vec![], vec![]), ), None ); @@ -502,6 +510,7 @@ fn test_handle_other_arp_request(#[case] medium: Medium) { assert_eq!( iface.inner.lookup_hardware_addr( MockTxToken, + None, &IpAddress::Ipv4(Ipv4Address([0x7f, 0x00, 0x00, 0x01])), &IpAddress::Ipv4(remote_ip_addr), &mut iface.fragmenter, @@ -546,7 +555,8 @@ fn test_arp_flush_after_update_ip(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), frame.into_inner(), - &mut iface.fragments + &mut iface.fragments, + &mut PacketBuffer::new(vec![], vec![]), ), Some(EthernetPacket::Arp(ArpRepr::EthernetIpv4 { operation: ArpOperation::Reply, @@ -561,11 +571,15 @@ fn test_arp_flush_after_update_ip(#[case] medium: Medium) { assert_eq!( iface.inner.lookup_hardware_addr( MockTxToken, + None, &IpAddress::Ipv4(local_ip_addr), &IpAddress::Ipv4(remote_ip_addr), &mut iface.fragmenter, ), - Ok((HardwareAddress::Ethernet(remote_hw_addr), MockTxToken)) + Ok(( + heapless::Vec::from_iter(core::iter::once(HardwareAddress::Ethernet(remote_hw_addr))), + MockTxToken + )) ); // Update IP addrs to trigger ARP cache flush diff --git a/src/iface/interface/tests/ipv6.rs b/src/iface/interface/tests/ipv6.rs index f7debda9f..4a37c2bcb 100644 --- a/src/iface/interface/tests/ipv6.rs +++ b/src/iface/interface/tests/ipv6.rs @@ -51,7 +51,9 @@ fn multicast_source_address(#[case] medium: Medium) { iface.inner.process_ipv6( &mut sockets, PacketMeta::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + None, + &mut PacketBuffer::new(vec![], vec![]), ), response ); @@ -99,7 +101,9 @@ fn hop_by_hop_skip_with_icmp(#[case] medium: Medium) { iface.inner.process_ipv6( &mut sockets, PacketMeta::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + None, + &mut PacketBuffer::new(vec![], vec![]), ), response ); @@ -134,7 +138,9 @@ fn hop_by_hop_discard_with_icmp(#[case] medium: Medium) { iface.inner.process_ipv6( &mut sockets, PacketMeta::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + None, + &mut PacketBuffer::new(vec![], vec![]), ), response ); @@ -188,7 +194,9 @@ fn hop_by_hop_discard_param_problem(#[case] medium: Medium) { iface.inner.process_ipv6( &mut sockets, PacketMeta::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + None, + &mut PacketBuffer::new(vec![], vec![]), ), response ); @@ -245,7 +253,9 @@ fn hop_by_hop_discard_with_multicast(#[case] medium: Medium) { iface.inner.process_ipv6( &mut sockets, PacketMeta::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + None, + &mut PacketBuffer::new(vec![], vec![]), ), response ); @@ -304,7 +314,9 @@ fn imcp_empty_echo_request(#[case] medium: Medium) { iface.inner.process_ipv6( &mut sockets, PacketMeta::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + None, + &mut PacketBuffer::new(vec![], vec![]), ), response ); @@ -364,7 +376,9 @@ fn icmp_echo_request(#[case] medium: Medium) { iface.inner.process_ipv6( &mut sockets, PacketMeta::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + None, + &mut PacketBuffer::new(vec![], vec![]), ), response ); @@ -411,12 +425,15 @@ fn icmp_echo_reply_as_input(#[case] medium: Medium) { iface.inner.process_ipv6( &mut sockets, PacketMeta::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + None, + &mut PacketBuffer::new(vec![], vec![]), ), response ); } +#[cfg(not(feature = "proto-rpl"))] #[rstest] #[case::ip(Medium::Ip)] #[cfg(feature = "medium-ip")] @@ -465,6 +482,7 @@ fn unknown_proto_with_multicast_dst_address(#[case] medium: Medium) { ); } +#[cfg(not(feature = "proto-rpl"))] #[rstest] #[case::ip(Medium::Ip)] #[cfg(feature = "medium-ip")] @@ -552,7 +570,9 @@ fn ndsic_neighbor_advertisement_ethernet(#[case] medium: Medium) { iface.inner.process_ipv6( &mut sockets, PacketMeta::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + None, + &mut PacketBuffer::new(vec![], vec![]), ), response ); @@ -608,7 +628,9 @@ fn ndsic_neighbor_advertisement_ethernet_multicast_addr(#[case] medium: Medium) iface.inner.process_ipv6( &mut sockets, PacketMeta::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + None, + &mut PacketBuffer::new(vec![], vec![]), ), response ); @@ -660,7 +682,9 @@ fn ndsic_neighbor_advertisement_ieee802154(#[case] medium: Medium) { iface.inner.process_ipv6( &mut sockets, PacketMeta::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + None, + &mut PacketBuffer::new(vec![], vec![]), ), response ); @@ -733,7 +757,8 @@ fn test_handle_valid_ndisc_request(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), frame.into_inner(), - &mut iface.fragments + &mut iface.fragments, + &mut PacketBuffer::new(vec![], vec![]), ), Some(EthernetPacket::Ip(Packet::new_ipv6( ipv6_expected, @@ -745,11 +770,15 @@ fn test_handle_valid_ndisc_request(#[case] medium: Medium) { assert_eq!( iface.inner.lookup_hardware_addr( MockTxToken, + None, &IpAddress::Ipv6(local_ip_addr), &IpAddress::Ipv6(remote_ip_addr), &mut iface.fragmenter, ), - Ok((HardwareAddress::Ethernet(remote_hw_addr), MockTxToken)) + Ok(( + heapless::Vec::from_iter(core::iter::once(HardwareAddress::Ethernet(remote_hw_addr))), + MockTxToken + )) ); } diff --git a/src/iface/interface/tests/mod.rs b/src/iface/interface/tests/mod.rs index a0a45e274..7a177a595 100644 --- a/src/iface/interface/tests/mod.rs +++ b/src/iface/interface/tests/mod.rs @@ -2,6 +2,13 @@ mod ipv4; #[cfg(feature = "proto-ipv6")] mod ipv6; +#[cfg(all( + feature = "rpl-mop-0", + feature = "rpl-mop-1", + feature = "rpl-mop-2", + feature = "rpl-mop-3", +))] +mod rpl; #[cfg(feature = "proto-sixlowpan")] mod sixlowpan; @@ -58,7 +65,15 @@ impl TxToken for MockTxToken { fn test_new_panic() { let mut device = Loopback::new(Medium::Ethernet); let config = Config::new(HardwareAddress::Ip); - Interface::new(config, &mut device, Instant::ZERO); + let mut meta = []; + let mut payload = []; + Interface::new( + config, + &mut device, + &mut meta[..], + &mut payload[..], + Instant::ZERO, + ); } #[rstest] diff --git a/src/iface/interface/tests/rpl.rs b/src/iface/interface/tests/rpl.rs new file mode 100644 index 000000000..7540472ae --- /dev/null +++ b/src/iface/interface/tests/rpl.rs @@ -0,0 +1,551 @@ +use super::*; + +use crate::iface::packet::*; + +use crate::iface::RplModeOfOperation; + +#[rstest] +#[case::mop0(RplModeOfOperation::NoDownwardRoutesMaintained)] +#[cfg(feature = "rpl-mop-0")] +#[case::mop1(RplModeOfOperation::NonStoringMode)] +#[cfg(feature = "rpl-mop-1")] +#[case::mop2(RplModeOfOperation::StoringMode)] +#[cfg(feature = "rpl-mop-2")] +#[case::mop3(RplModeOfOperation::StoringModeWithMulticast)] +#[cfg(feature = "rpl-mop-3")] +fn unicast_dis(#[case] mop: RplModeOfOperation) { + use crate::iface::rpl::{Dodag, Rank, RplInstanceId}; + + let (mut iface, _, _) = setup(Medium::Ieee802154); + iface.inner.rpl.is_root = true; + iface.inner.rpl.mode_of_operation = mop; + iface.inner.rpl.dodag = Some(Dodag { + instance_id: RplInstanceId::Local(30), + id: Default::default(), + version_number: Default::default(), + preference: 0, + rank: Rank::ROOT, + dio_timer: Default::default(), + dao_expiration: Instant::now(), + dao_seq_number: Default::default(), + dao_acks: Default::default(), + daos: Default::default(), + parent: Default::default(), + without_parent: Default::default(), + authentication_enabled: Default::default(), + path_control_size: Default::default(), + dtsn: Default::default(), + dtsn_incremented_at: Instant::now(), + default_lifetime: Default::default(), + lifetime_unit: Default::default(), + grounded: false, + parent_set: Default::default(), + relations: Default::default(), + }); + + let addr = Ipv6Address::from_parts(&[0xfe80, 0, 0, 0, 0, 0, 0, 2]); + + let response = iface.inner.process_rpl_dis( + Ipv6Repr { + src_addr: addr, + dst_addr: iface.ipv6_addr().unwrap(), + next_header: IpProtocol::Icmpv6, + payload_len: 0, // does not matter + hop_limit: 0, // does not matter + }, + RplDis { + options: Default::default(), + }, + ); + + let mut dio_options = heapless::Vec::new(); + dio_options + .push(RplOptionRepr::DodagConfiguration(RplDodagConfiguration { + authentication_enabled: false, + path_control_size: 0, + dio_interval_doublings: 8, + dio_interval_min: 12, + dio_redundancy_constant: 10, + max_rank_increase: 0, + minimum_hop_rank_increase: 256, + objective_code_point: 0, + default_lifetime: 0, + lifetime_unit: 0, + })) + .unwrap(); + + let icmp = Icmpv6Repr::Rpl(RplRepr::DodagInformationObject(RplDio { + rpl_instance_id: RplInstanceId::Local(30), + version_number: Default::default(), + rank: Rank::ROOT.raw_value(), + grounded: false, + mode_of_operation: mop.into(), + dodag_preference: 0, + dtsn: Default::default(), + dodag_id: Default::default(), + options: dio_options, + })); + + let expected = Some(Packet::Ipv6(PacketV6::new( + Ipv6Repr { + src_addr: iface.ipv6_addr().unwrap(), + dst_addr: addr, + next_header: IpProtocol::Icmpv6, + payload_len: icmp.buffer_len(), + hop_limit: 64, + }, + IpPayload::Icmpv6(icmp), + ))); + + assert_eq!(response, expected,); +} + +#[rstest] +#[case::mop0(RplModeOfOperation::NoDownwardRoutesMaintained)] +#[cfg(feature = "rpl-mop-0")] +#[case::mop1(RplModeOfOperation::NonStoringMode)] +#[cfg(feature = "rpl-mop-1")] +#[case::mop2(RplModeOfOperation::StoringMode)] +#[cfg(feature = "rpl-mop-2")] +#[case::mop3(RplModeOfOperation::StoringModeWithMulticast)] +#[cfg(feature = "rpl-mop-3")] +fn dio_without_configuration(#[case] mop: RplModeOfOperation) { + use crate::iface::rpl::{Rank, RplInstanceId}; + + let (mut iface, _, _) = setup(Medium::Ieee802154); + iface.inner.rpl.mode_of_operation = mop; + + let ll_addr = Ieee802154Address::Extended([0, 0, 0, 0, 0, 0, 0, 2]); + let addr = ll_addr.as_link_local_address().unwrap(); + + let response = iface.inner.process_rpl_dio( + Ipv6Repr { + src_addr: addr, + dst_addr: Ipv6Address::LINK_LOCAL_ALL_RPL_NODES, + next_header: IpProtocol::Icmpv6, + payload_len: 0, // does not matter + hop_limit: 0, // does not matter + }, + RplDio { + rpl_instance_id: RplInstanceId::Local(30), + version_number: Default::default(), + rank: Rank::ROOT.raw_value(), + grounded: false, + mode_of_operation: mop.into(), + dodag_preference: 0, + dtsn: Default::default(), + dodag_id: Default::default(), + options: Default::default(), + }, + ); + + let icmp = Icmpv6Repr::Rpl(RplRepr::DodagInformationSolicitation(RplDis { + options: Default::default(), + })); + + let expected = Some(Packet::Ipv6(PacketV6::new( + Ipv6Repr { + src_addr: iface.ipv6_addr().unwrap(), + dst_addr: addr, + next_header: IpProtocol::Icmpv6, + payload_len: icmp.buffer_len(), + hop_limit: 64, + }, + IpPayload::Icmpv6(icmp), + ))); + + assert_eq!(response, expected,); +} + +#[rstest] +#[case::mop0(RplModeOfOperation::NoDownwardRoutesMaintained)] +#[cfg(feature = "rpl-mop-0")] +#[case::mop1(RplModeOfOperation::NonStoringMode)] +#[cfg(feature = "rpl-mop-1")] +#[case::mop2(RplModeOfOperation::StoringMode)] +#[cfg(feature = "rpl-mop-2")] +#[case::mop3(RplModeOfOperation::StoringModeWithMulticast)] +#[cfg(feature = "rpl-mop-3")] +fn dio_with_increased_version_number(#[case] mop: RplModeOfOperation) { + use crate::iface::rpl::{Dodag, ObjectiveFunction0, Parent, ParentSet, Rank, RplInstanceId}; + + let (mut iface, _, _) = setup(Medium::Ieee802154); + + let ll_addr = Ieee802154Address::Extended([0, 0, 0, 0, 0, 0, 0, 1]); + let addr = ll_addr.as_link_local_address().unwrap(); + + let now = Instant::now(); + let mut set = ParentSet::default(); + let _ = set.add(Parent::new( + addr, + Rank::ROOT, + Default::default(), + RplSequenceCounter::from(240), + Default::default(), + now, + )); + + // Setting a dodag configuration with parent + iface.inner.rpl.mode_of_operation = mop; + iface.inner.rpl.of = ObjectiveFunction0::default(); + iface.inner.rpl.is_root = false; + iface.inner.rpl.dodag = Some(Dodag { + instance_id: RplInstanceId::Local(30), + id: Default::default(), + version_number: Default::default(), + preference: 0, + rank: Rank::new(1024, 16), + dio_timer: Default::default(), + dao_expiration: Instant::now(), + dao_seq_number: Default::default(), + dao_acks: Default::default(), + daos: Default::default(), + parent: Some(addr), + without_parent: Default::default(), + authentication_enabled: Default::default(), + path_control_size: Default::default(), + dtsn: Default::default(), + dtsn_incremented_at: Instant::now(), + default_lifetime: Default::default(), + lifetime_unit: Default::default(), + grounded: false, + parent_set: set, + relations: Default::default(), + }); + let old_version_number = iface.inner.rpl.dodag.as_ref().unwrap().version_number; + + // Check if the parameters are set correctly + assert_eq!(old_version_number, RplSequenceCounter::from(240)); + assert!(!iface + .inner + .rpl + .dodag + .as_ref() + .unwrap() + .parent_set + .is_empty()); + + // Receiving DIO with increased version number from node from another dodag + let response = iface.inner.process_rpl_dio( + Ipv6Repr { + src_addr: addr, + dst_addr: Ipv6Address::LINK_LOCAL_ALL_RPL_NODES, + next_header: IpProtocol::Icmpv6, + payload_len: 0, // does not matter + hop_limit: 0, // does not matter + }, + RplDio { + rpl_instance_id: RplInstanceId::Local(31), + version_number: RplSequenceCounter::from(242), + rank: Rank::new(16, 16).raw_value(), + grounded: false, + mode_of_operation: mop.into(), + dodag_preference: 0, + dtsn: Default::default(), + dodag_id: Default::default(), + options: Default::default(), + }, + ); + + // The version number should stay the same + assert_eq!( + iface.inner.rpl.dodag.as_ref().unwrap().version_number, + RplSequenceCounter::from(240) + ); + + // The instance id should stay the same + assert_eq!( + iface.inner.rpl.dodag.as_ref().unwrap().instance_id, + RplInstanceId::Local(30) + ); + + // The parent should remain the same + assert_eq!(iface.inner.rpl.dodag.as_ref().unwrap().parent, Some(addr)); + + // The parent set should remain the same + assert!(!iface + .inner + .rpl + .dodag + .as_ref() + .unwrap() + .parent_set + .is_empty()); + + // Response should be None + assert_eq!(response, None); + + // Upon receving a DIO with a lesser DODAG Version Number value the node cannot select the sender as a parent + let ll_addr2 = Ieee802154Address::Extended([0, 0, 0, 0, 0, 0, 0, 3]); + let addr2 = ll_addr2.as_link_local_address().unwrap(); + + let response = iface.inner.process_rpl_dio( + Ipv6Repr { + src_addr: addr2, + dst_addr: Ipv6Address::LINK_LOCAL_ALL_RPL_NODES, + next_header: IpProtocol::Icmpv6, + payload_len: 0, // does not matter + hop_limit: 0, // does not matter + }, + RplDio { + rpl_instance_id: RplInstanceId::Local(30), + version_number: RplSequenceCounter::from(239), + rank: Rank::new(16, 16).raw_value(), + grounded: false, + mode_of_operation: mop.into(), + dodag_preference: 0, + dtsn: Default::default(), + dodag_id: Default::default(), + options: Default::default(), + }, + ); + + // Response should be None + assert_eq!(response, None); + + // The parent should remain the same + assert_eq!(iface.inner.rpl.dodag.as_ref().unwrap().parent, Some(addr)); + + // Receiving DIO with increased version number from root which is also parent + let response = iface.inner.process_rpl_dio( + Ipv6Repr { + src_addr: addr, + dst_addr: Ipv6Address::LINK_LOCAL_ALL_RPL_NODES, + next_header: IpProtocol::Icmpv6, + payload_len: 0, // does not matter + hop_limit: 0, // does not matter + }, + RplDio { + rpl_instance_id: RplInstanceId::Local(30), + version_number: RplSequenceCounter::from(241), + rank: Rank::ROOT.raw_value(), + grounded: false, + mode_of_operation: mop.into(), + dodag_preference: 0, + dtsn: Default::default(), + dodag_id: Default::default(), + options: Default::default(), + }, + ); + + // The version number should be increased + assert_eq!( + iface.inner.rpl.dodag.as_ref().unwrap().version_number, + RplSequenceCounter::from(241) + ); + + // The parent should be removed + assert_eq!(iface.inner.rpl.dodag.as_ref().unwrap().parent, None); + + // The parent set should be empty + assert!(iface + .inner + .rpl + .dodag + .as_ref() + .unwrap() + .parent_set + .is_empty()); + + let icmp = Icmpv6Repr::Rpl(RplRepr::DodagInformationObject(RplDio { + rpl_instance_id: RplInstanceId::Local(30), + version_number: RplSequenceCounter::from(241), + rank: Rank::INFINITE.raw_value(), + grounded: false, + mode_of_operation: mop.into(), + dodag_preference: 0, + dtsn: Default::default(), + dodag_id: Default::default(), + options: heapless::Vec::new(), + })); + + let expected = Some(Packet::Ipv6(PacketV6::new( + Ipv6Repr { + src_addr: iface.ipv6_addr().unwrap(), + dst_addr: Ipv6Address::LINK_LOCAL_ALL_RPL_NODES, + next_header: IpProtocol::Icmpv6, + payload_len: icmp.buffer_len(), + hop_limit: 64, + }, + IpPayload::Icmpv6(icmp), + ))); + + // DIO with infinite rank is sent with the new version number so the nodes + // know they have to leave the network + assert_eq!(response, expected,); +} + +#[rstest] +fn packet_forwarding_with_multicast() { + use crate::iface::rpl::{Dodag, ObjectiveFunction0, Parent, ParentSet, Rank}; + + const MULTICAST_GROUP: Ipv6Address = Ipv6Address::new(0xff02, 0, 0, 0, 0, 0, 0, 3); + const MULTICAST_HOP: Ipv6Address = Ipv6Address::new(0xfd00, 0, 0, 0, 0, 0, 0, 2); + const MULTICAST_HOP_LL: HardwareAddress = + HardwareAddress::Ieee802154(Ieee802154Address::Extended([0, 0, 0, 0, 0, 0, 0, 2])); + + let (mut iface, _, _) = setup(Medium::Ieee802154); + + let ll_addr = Ieee802154Address::Extended([0, 0, 0, 0, 0, 0, 0, 1]); + let addr = ll_addr.as_link_local_address().unwrap(); + + let now = Instant::now(); + let mut set = ParentSet::default(); + let _ = set.add(Parent::new( + addr, + Rank::ROOT, + Default::default(), + RplSequenceCounter::from(240), + Default::default(), + now, + )); + + // Setting a dodag configuration with parent + iface.inner.rpl.mode_of_operation = RplModeOfOperation::StoringModeWithMulticast; + iface.inner.rpl.of = ObjectiveFunction0::default(); + iface.inner.rpl.is_root = false; + iface.inner.rpl.dodag = Some(Dodag { + instance_id: RplInstanceId::Local(30), + id: Default::default(), + version_number: Default::default(), + preference: 0, + rank: Rank::new(1024, 16), + dio_timer: Default::default(), + dao_expiration: Instant::now(), + dao_seq_number: Default::default(), + dao_acks: Default::default(), + daos: Default::default(), + parent: Some(addr), + without_parent: Default::default(), + authentication_enabled: Default::default(), + path_control_size: Default::default(), + dtsn: Default::default(), + dtsn_incremented_at: Instant::now(), + default_lifetime: Default::default(), + lifetime_unit: Default::default(), + grounded: false, + parent_set: set, + relations: Default::default(), + }); + iface + .inner + .neighbor_cache + .fill(addr.into(), ll_addr.into(), Instant::from_secs(10 * 60)); + iface.inner.neighbor_cache.fill( + MULTICAST_HOP.into(), + MULTICAST_HOP_LL, + Instant::from_secs(10 * 60), + ); + + let _response = iface.inner.process_rpl_dao( + Ipv6Repr { + src_addr: MULTICAST_HOP, + dst_addr: Ipv6Address::new(0xfd00, 0, 0, 0, 0, 0, 0, 1), + next_header: IpProtocol::Icmpv6, + payload_len: 0, // does not matter + hop_limit: 0xff, // does not matter + }, + RplDao { + rpl_instance_id: RplInstanceId::Local(30), + expect_ack: false, + sequence: RplSequenceCounter::new(42), + dodag_id: Default::default(), + options: heapless::Vec::from_iter([ + RplOptionRepr::RplTarget(RplTarget { + prefix_length: 64, + prefix: heapless::Vec::from_slice(MULTICAST_GROUP.as_bytes()).unwrap(), + }), + RplOptionRepr::TransitInformation(RplTransitInformation { + external: false, + path_control: 0, + path_sequence: 0, + path_lifetime: 0xff, + parent_address: Some(Ipv6Address::new(0xfd00, 0, 0, 0, 0, 0, 0, 1)), + }), + ]), + }, + ); + let _response = iface.inner.process_rpl_dao( + Ipv6Repr { + src_addr: MULTICAST_HOP, + dst_addr: Ipv6Address::new(0xfd00, 0, 0, 0, 0, 0, 0, 1), + next_header: IpProtocol::Icmpv6, + payload_len: 0, // does not matter + hop_limit: 0xff, // does not matter + }, + RplDao { + rpl_instance_id: RplInstanceId::Local(30), + expect_ack: false, + sequence: RplSequenceCounter::new(42), + dodag_id: Default::default(), + options: heapless::Vec::from_iter([ + RplOptionRepr::RplTarget(RplTarget { + prefix_length: 64, + prefix: heapless::Vec::from_slice( + Ipv6Address::new(0xfd00, 0, 0, 0, 0, 0, 0, 123).as_bytes(), // Just some other random child + ) + .unwrap(), + }), + RplOptionRepr::TransitInformation(RplTransitInformation { + external: false, + path_control: 0, + path_sequence: 0, + path_lifetime: 0xff, + parent_address: Some(Ipv6Address::new(0xfd00, 0, 0, 0, 0, 0, 0, 1)), + }), + ]), + }, + ); + + let dodag = iface.inner.rpl.dodag.as_ref().unwrap(); + assert!( + dodag + .relations + .iter() + .any(|rel| rel.is_multicast() + && rel.next_hop().iter().any(|hop| hop.ip == MULTICAST_HOP)), + "There should now be a relation with a multicast address added" + ); + + // Lookup haddrs if originating from this node + let haddrs = iface + .inner + .lookup_hardware_addr_multicast(&MULTICAST_GROUP.into(), None) + .unwrap(); + let expected_haddrs: heapless::Vec<_, { IFACE_MAX_MULTICAST_DUPLICATION_COUNT }> = + heapless::Vec::from_slice(&[ll_addr.into(), MULTICAST_HOP_LL]).unwrap(); + assert_eq!( + haddrs, expected_haddrs, + "If originating from this mote, the multicast packet should be forwarded up and down" + ); + + // Lookup haddrs if originating from the parent + let haddrs = iface + .inner + .lookup_hardware_addr_multicast(&MULTICAST_GROUP.into(), Some(&ll_addr.into())) + .unwrap(); + let expected_haddrs: heapless::Vec<_, { IFACE_MAX_MULTICAST_DUPLICATION_COUNT }> = + heapless::Vec::from_slice(&[MULTICAST_HOP_LL]).unwrap(); + assert_eq!( + haddrs, expected_haddrs, + "If originating from the parent, the multicast packet should only forward the packet down" + ); + + // Lookup haddrs if originating from one of the children + let haddrs = iface + .inner + .lookup_hardware_addr_multicast(&MULTICAST_GROUP.into(), Some(&MULTICAST_HOP_LL)) + .unwrap(); + let expected_haddrs: heapless::Vec<_, { IFACE_MAX_MULTICAST_DUPLICATION_COUNT }> = + heapless::Vec::from_slice(&[ll_addr.into()]).unwrap(); + assert_eq!(haddrs, expected_haddrs, "If originating from one of the children, the multicast packet should be forwarded up and to the other interested children"); + + // Lookup haddrs of all local rpl motes, coming from this mote + let haddrs = iface + .inner + .lookup_hardware_addr_multicast(&Ipv6Address::LINK_LOCAL_ALL_RPL_NODES.into(), None) + .unwrap(); + let expected_haddrs: heapless::Vec<_, { IFACE_MAX_MULTICAST_DUPLICATION_COUNT }> = + heapless::Vec::from_slice(&[Ieee802154Address::BROADCAST.into()]).unwrap(); + assert_eq!(haddrs, expected_haddrs); +} diff --git a/src/iface/interface/tests/sixlowpan.rs b/src/iface/interface/tests/sixlowpan.rs index 9dbdfaae0..dbfd3b925 100644 --- a/src/iface/interface/tests/sixlowpan.rs +++ b/src/iface/interface/tests/sixlowpan.rs @@ -18,7 +18,8 @@ fn ieee802154_wrong_pan_id(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), &data[..], - &mut iface.fragments + &mut iface.fragments, + &mut PacketBuffer::new(vec![], vec![]), ), response, ); @@ -59,12 +60,13 @@ fn icmp_echo_request(#[case] medium: Medium) { )); let (mut iface, mut sockets, _device) = setup(medium); - iface.update_ip_addrs(|ips| { - ips.push(IpCidr::Ipv6(Ipv6Cidr::new( - Ipv6Address::from_parts(&[0xfe80, 0, 0, 0, 0x180b, 0x4242, 0x4242, 0x4242]), - 10, - ))) - .unwrap(); + iface.update_ip_addrs(|addrs| { + addrs + .push(IpCidr::Ipv6(Ipv6Cidr::new( + Ipv6Address::from_parts(&[0xfe80, 0, 0, 0, 0x180b, 0x4242, 0x4242, 0x4242]), + 10, + ))) + .unwrap(); }); assert_eq!( @@ -72,7 +74,8 @@ fn icmp_echo_request(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), &data[..], - &mut iface.fragments + &mut iface.fragments, + &mut PacketBuffer::new(vec![], vec![]), ), response, ); @@ -84,12 +87,13 @@ fn test_echo_request_sixlowpan_128_bytes() { use crate::phy::Checksum; let (mut iface, mut sockets, mut device) = setup(Medium::Ieee802154); - iface.update_ip_addrs(|ips| { - ips.push(IpCidr::Ipv6(Ipv6Cidr::new( - Ipv6Address::new(0xfe80, 0x0, 0x0, 0x0, 0x92fc, 0x48c2, 0xa441, 0xfc76), - 10, - ))) - .unwrap(); + iface.update_ip_addrs(|addrs| { + addrs + .push(IpCidr::Ipv6(Ipv6Cidr::new( + Ipv6Address::new(0xfe80, 0x0, 0x0, 0x0, 0x92fc, 0x48c2, 0xa441, 0xfc76), + 10, + ))) + .unwrap(); }); // TODO: modify the example, such that we can also test if the checksum is correctly // computed. @@ -174,7 +178,8 @@ fn test_echo_request_sixlowpan_128_bytes() { PacketMeta::default(), &ieee802154_repr, &request_first_part_packet.into_inner()[..], - &mut iface.fragments + &mut iface.fragments, + &mut PacketBuffer::new(vec![], vec![]), ), None ); @@ -194,37 +199,46 @@ fn test_echo_request_sixlowpan_128_bytes() { 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, ]; - let result = iface.inner.process_sixlowpan( - &mut sockets, - PacketMeta::default(), - &ieee802154_repr, - &request_second_part, - &mut iface.fragments, - ); + let result = match iface + .inner + .process_sixlowpan( + &mut sockets, + PacketMeta::default(), + &ieee802154_repr, + &request_second_part, + &mut iface.fragments, + &mut PacketBuffer::new(vec![], vec![]), + ) + .unwrap() + { + Packet::Ipv6(packet) => Some(packet), + #[allow(unreachable_patterns)] + _ => unreachable!(), + }; - assert_eq!( - result, - Some(Packet::new_ipv6( - Ipv6Repr { - src_addr: Ipv6Address([ - 0xfe, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x92, 0xfc, 0x48, 0xc2, 0xa4, 0x41, - 0xfc, 0x76, - ]), - dst_addr: Ipv6Address([ - 0xfe, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x42, 0x42, 0x42, 0x42, 0x42, - 0xb, 0x1a, - ]), - next_header: IpProtocol::Icmpv6, - payload_len: 136, - hop_limit: 64, - }, - IpPayload::Icmpv6(Icmpv6Repr::EchoReply { - ident: 39, - seq_no: 2, - data, - }) - )) - ); + let icmp = Icmpv6Repr::EchoReply { + ident: 39, + seq_no: 2, + data, + }; + let expected = Some(PacketV6::new( + Ipv6Repr { + src_addr: Ipv6Address([ + 0xfe, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x92, 0xfc, 0x48, 0xc2, 0xa4, 0x41, 0xfc, + 0x76, + ]), + dst_addr: Ipv6Address([ + 0xfe, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x42, 0x42, 0x42, 0x42, 0x42, 0xb, + 0x1a, + ]), + next_header: IpProtocol::Icmpv6, + payload_len: 136, + hop_limit: 64, + }, + IpPayload::Icmpv6(icmp), + )); + + assert_eq!(result, expected); iface.inner.neighbor_cache.fill( IpAddress::Ipv6(Ipv6Address([ @@ -298,12 +312,13 @@ fn test_sixlowpan_udp_with_fragmentation() { }; let (mut iface, mut sockets, mut device) = setup(Medium::Ieee802154); - iface.update_ip_addrs(|ips| { - ips.push(IpCidr::Ipv6(Ipv6Cidr::new( - Ipv6Address::new(0xfe80, 0x0, 0x0, 0x0, 0x92fc, 0x48c2, 0xa441, 0xfc76), - 10, - ))) - .unwrap(); + iface.update_ip_addrs(|addrs| { + addrs + .push(IpCidr::Ipv6(Ipv6Cidr::new( + Ipv6Address::new(0xfe80, 0x0, 0x0, 0x0, 0x92fc, 0x48c2, 0xa441, 0xfc76), + 10, + ))) + .unwrap(); }); iface.inner.caps.checksum.udp = Checksum::None; @@ -335,7 +350,8 @@ fn test_sixlowpan_udp_with_fragmentation() { PacketMeta::default(), &ieee802154_repr, udp_first_part, - &mut iface.fragments + &mut iface.fragments, + &mut PacketBuffer::new(vec![], vec![]), ), None ); @@ -355,7 +371,8 @@ fn test_sixlowpan_udp_with_fragmentation() { PacketMeta::default(), &ieee802154_repr, udp_second_part, - &mut iface.fragments + &mut iface.fragments, + &mut PacketBuffer::new(vec![], vec![]), ), None ); @@ -379,27 +396,27 @@ In at rhoncus tortor. Cras blandit tellus diam, varius vestibulum nibh commodo n )) ); + let udp = UdpRepr { + src_port: 1234, + dst_port: 1234, + }; + let packet = PacketV6::new( + Ipv6Repr { + src_addr: Ipv6Address::default(), + dst_addr: Ipv6Address::default(), + next_header: IpProtocol::Udp, + payload_len: udp.header_len() + udp_data.len(), + hop_limit: 64, + }, + IpPayload::Udp(udp, udp_data), + ); + let tx_token = device.transmit(Instant::now()).unwrap(); iface.inner.dispatch_ieee802154( Ieee802154Address::default(), tx_token, PacketMeta::default(), - Packet::new_ipv6( - Ipv6Repr { - src_addr: Ipv6Address::default(), - dst_addr: Ipv6Address::default(), - next_header: IpProtocol::Udp, - payload_len: udp_data.len(), - hop_limit: 64, - }, - IpPayload::Udp( - UdpRepr { - src_port: 1234, - dst_port: 1234, - }, - udp_data, - ), - ), + packet, &mut iface.fragmenter, ); @@ -409,7 +426,7 @@ In at rhoncus tortor. Cras blandit tellus diam, varius vestibulum nibh commodo n device.queue.pop_front().unwrap(), &[ 0x41, 0xcc, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, - 0x2, 0x2, 0x2, 0x2, 0xc0, 0xb4, 0x5, 0x4e, 0x7e, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x2, 0x2, 0x2, 0x2, 0xc0, 0xbc, 0x5, 0x4e, 0x7e, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x4, 0xd2, 0x4, 0xd2, 0x0, 0x0, 0x4c, 0x6f, 0x72, 0x65, 0x6d, 0x20, 0x69, 0x70, 0x73, 0x75, 0x6d, 0x20, 0x64, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x73, 0x69, 0x74, 0x20, 0x61, 0x6d, 0x65, 0x74, 0x2c, 0x20, @@ -424,7 +441,7 @@ In at rhoncus tortor. Cras blandit tellus diam, varius vestibulum nibh commodo n device.queue.pop_front().unwrap(), &[ 0x41, 0xcc, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, - 0x2, 0x2, 0x2, 0x2, 0xe0, 0xb4, 0x5, 0x4e, 0xf, 0x6f, 0x72, 0x74, 0x6f, 0x72, 0x2e, + 0x2, 0x2, 0x2, 0x2, 0xe0, 0xbc, 0x5, 0x4e, 0xf, 0x6f, 0x72, 0x74, 0x6f, 0x72, 0x2e, 0x20, 0x43, 0x72, 0x61, 0x73, 0x20, 0x62, 0x6c, 0x61, 0x6e, 0x64, 0x69, 0x74, 0x20, 0x74, 0x65, 0x6c, 0x6c, 0x75, 0x73, 0x20, 0x64, 0x69, 0x61, 0x6d, 0x2c, 0x20, 0x76, 0x61, 0x72, 0x69, 0x75, 0x73, 0x20, 0x76, 0x65, 0x73, 0x74, 0x69, 0x62, 0x75, 0x6c, diff --git a/src/iface/mod.rs b/src/iface/mod.rs index 3076088a0..5fb0170fb 100644 --- a/src/iface/mod.rs +++ b/src/iface/mod.rs @@ -6,6 +6,7 @@ provides lookup and caching of hardware addresses, and handles management packet mod fragmentation; mod interface; +mod multicast; #[cfg(any(feature = "medium-ethernet", feature = "medium-ieee802154"))] mod neighbor; mod route; @@ -22,3 +23,14 @@ pub use self::interface::{Config, Interface, InterfaceInner as Context}; pub use self::route::{Route, RouteTableFull, Routes}; pub use self::socket_set::{SocketHandle, SocketSet, SocketStorage}; + +#[cfg(feature = "proto-rpl")] +pub use self::rpl::{ + Config as RplConfig, ModeOfOperation as RplModeOfOperation, RootConfig as RplRootConfig, + RplInstanceId, TrickleTimer, +}; + +#[cfg(feature = "proto-rpl")] +use self::rpl::Rpl; + +pub use multicast::MulticastMetadata; diff --git a/src/iface/multicast.rs b/src/iface/multicast.rs new file mode 100644 index 000000000..f211f5dcf --- /dev/null +++ b/src/iface/multicast.rs @@ -0,0 +1,50 @@ +use crate::{ + config::IFACE_MAX_MULTICAST_DUPLICATION_COUNT, + phy::PacketMeta, + wire::{HardwareAddress, Ipv6Repr}, +}; + +use super::packet::{IpPayloadType, PacketV6}; + +#[derive(Debug, Clone)] +pub struct MulticastMetadata { + ll_send_to: heapless::Vec, + packet_metadata: PacketMeta, + header: Ipv6Repr, + ip_payload_type: IpPayloadType, +} + +impl MulticastMetadata { + pub(crate) fn new( + packet_metadata: PacketMeta, + packet: &PacketV6<'_>, + ll_send_to: heapless::Vec, + ) -> Self { + Self { + packet_metadata, + ll_send_to, + header: *packet.header(), + ip_payload_type: packet.payload().payload_type(), + } + } + + pub fn finished(&self) -> bool { + self.ll_send_to.is_empty() + } + + pub fn pop_next_ll_addr(&mut self) -> Option { + self.ll_send_to.pop() + } + + pub fn header(&self) -> &Ipv6Repr { + &self.header + } + + pub fn meta(&self) -> PacketMeta { + self.packet_metadata + } + + pub(crate) fn payload_type(&self) -> IpPayloadType { + self.ip_payload_type.clone() + } +} diff --git a/src/iface/packet.rs b/src/iface/packet.rs index b586d75e8..cb0afed08 100644 --- a/src/iface/packet.rs +++ b/src/iface/packet.rs @@ -1,4 +1,4 @@ -use crate::phy::DeviceCapabilities; +use crate::phy::{ChecksumCapabilities, DeviceCapabilities}; use crate::wire::*; #[allow(clippy::large_enum_variant)] @@ -13,6 +13,7 @@ pub(crate) enum EthernetPacket<'a> { #[derive(Debug, PartialEq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[allow(clippy::large_enum_variant)] pub(crate) enum Packet<'p> { #[cfg(feature = "proto-ipv4")] Ipv4(PacketV4<'p>), @@ -70,13 +71,310 @@ impl<'p> Packet<'p> { } } - pub(crate) fn emit_payload( - &self, - _ip_repr: &IpRepr, - payload: &mut [u8], - caps: &DeviceCapabilities, - ) { - match self.payload() { + pub(crate) fn emit_payload(&self, payload: &mut [u8], caps: &DeviceCapabilities) { + self.payload().emit(&self.ip_repr(), payload, caps); + } +} + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg(feature = "proto-ipv4")] +pub(crate) struct PacketV4<'p> { + header: Ipv4Repr, + payload: IpPayload<'p>, +} + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg(feature = "proto-ipv6")] +pub(crate) struct PacketV6<'p> { + header: Ipv6Repr, + #[cfg(feature = "proto-ipv6-hbh")] + hop_by_hop: Option<(IpProtocol, Ipv6HopByHopRepr<'p>)>, + #[cfg(feature = "proto-ipv6-fragmentation")] + fragment: Option<(IpProtocol, Ipv6FragmentRepr)>, + #[cfg(feature = "proto-ipv6-routing")] + routing: Option<(IpProtocol, Ipv6RoutingRepr)>, + payload: IpPayload<'p>, +} + +#[cfg(feature = "proto-ipv6")] +impl<'p> PacketV6<'p> { + pub(crate) fn new(header: Ipv6Repr, payload: IpPayload<'p>) -> Self { + Self { + header, + #[cfg(feature = "proto-ipv6-hbh")] + hop_by_hop: None, + #[cfg(feature = "proto-ipv6-fragmentation")] + fragment: None, + #[cfg(feature = "proto-ipv6-routing")] + routing: None, + payload, + } + } + + pub(crate) fn header(&self) -> &Ipv6Repr { + &self.header + } + + pub(crate) fn header_mut(&mut self) -> &mut Ipv6Repr { + &mut self.header + } + + #[cfg(feature = "proto-ipv6-hbh")] + pub(crate) fn hop_by_hop(&self) -> Option<(IpProtocol, &Ipv6HopByHopRepr<'p>)> { + #[cfg(feature = "proto-ipv6-hbh")] + { + self.hop_by_hop + .as_ref() + .map(|(next_header, repr)| (*next_header, repr)) + } + #[cfg(not(feature = "proto-ipv6-hbh"))] + { + None + } + } + + #[cfg(feature = "proto-ipv6-hbh")] + pub(crate) fn add_hop_by_hop(&mut self, repr: Ipv6HopByHopRepr<'p>) { + self.header.payload_len += 2 + repr.buffer_len(); + let next_header = self.header.next_header; + self.header.next_header = IpProtocol::HopByHop; + self.hop_by_hop = Some((next_header, repr)); + } + + #[cfg(feature = "proto-ipv6-routing")] + pub(crate) fn routing(&self) -> Option<(IpProtocol, &Ipv6RoutingRepr)> { + #[cfg(feature = "proto-ipv6-routing")] + { + self.routing + .as_ref() + .map(|(next_header, repr)| (*next_header, repr)) + } + #[cfg(not(feature = "proto-ipv6-routing"))] + { + None + } + } + + #[cfg(feature = "proto-ipv6-routing")] + pub(crate) fn add_routing(&mut self, repr: Ipv6RoutingRepr) { + self.header.payload_len += 2 + repr.buffer_len(); + let mut next_header = self.header.next_header; + + if let Some((hbh_next_header, _)) = &mut self.hop_by_hop { + next_header = *hbh_next_header; + *hbh_next_header = IpProtocol::Ipv6Route; + } else { + self.header.next_header = IpProtocol::Ipv6Route; + } + + self.routing = Some((next_header, repr)); + } + + pub(crate) fn payload(&self) -> &IpPayload<'p> { + &self.payload + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum IpPayloadType { + #[cfg(feature = "proto-ipv4")] + Icmpv4, + #[cfg(feature = "proto-igmp")] + Igmp, + #[cfg(feature = "proto-ipv6")] + Icmpv6, + #[cfg(any(feature = "socket-raw", feature = "proto-rpl"))] + Raw, + #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] + Udp, + #[cfg(feature = "socket-tcp")] + Tcp, + #[cfg(feature = "socket-dhcpv4")] + Dhcpv4, +} + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub(crate) enum IpPayload<'p> { + #[cfg(feature = "proto-ipv4")] + Icmpv4(Icmpv4Repr<'p>), + #[cfg(feature = "proto-igmp")] + Igmp(IgmpRepr), + #[cfg(feature = "proto-ipv6")] + Icmpv6(Icmpv6Repr<'p>), + #[cfg(any(feature = "socket-raw", feature = "proto-rpl"))] + Raw(&'p [u8]), + #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] + Udp(UdpRepr, &'p [u8]), + #[cfg(feature = "socket-tcp")] + Tcp(TcpRepr<'p>), + #[cfg(feature = "socket-dhcpv4")] + Dhcpv4(UdpRepr, DhcpRepr<'p>), +} + +impl<'a> IpPayload<'a> { + pub fn buffer_len(&self) -> usize { + match self { + #[cfg(feature = "proto-ipv4")] + Self::Icmpv4(repr) => repr.buffer_len(), + #[cfg(feature = "proto-igmp")] + Self::Igmp(repr) => repr.buffer_len(), + #[cfg(feature = "proto-ipv6")] + Self::Icmpv6(repr) => repr.buffer_len(), + #[cfg(any(feature = "socket-raw", feature = "proto-rpl"))] + Self::Raw(repr) => repr.len(), + #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] + Self::Udp(repr, data) => repr.header_len() + data.len(), + #[cfg(feature = "socket-tcp")] + Self::Tcp(repr) => repr.buffer_len(), + #[cfg(feature = "socket-dhcpv4")] + Self::Dhcpv4(repr, data) => repr.header_len() + data.buffer_len(), + } + } + + pub fn parse_unchecked( + buffer: &'a [u8], + payload_type: IpPayloadType, + header: &Ipv6Repr, + checksum_caps: &ChecksumCapabilities, + ) -> crate::wire::Result { + match payload_type { + #[cfg(feature = "proto-ipv4")] + IpPayloadType::Icmpv4 => Ok(Self::Icmpv4( + Icmpv4Repr::parse(&Icmpv4Packet::new_unchecked(buffer), checksum_caps) + .map_err(|_| crate::wire::Error)?, + )), + #[cfg(feature = "proto-igmp")] + IpPayloadType::Igmp => Ok(Self::Igmp(IgmpRepr::parse(&IgmpPacket::new_unchecked( + buffer, + ))?)), + #[cfg(feature = "proto-ipv6")] + IpPayloadType::Icmpv6 => Ok(Self::Icmpv6(Icmpv6Repr::parse( + &header.src_addr, + &header.dst_addr, + &Icmpv6Packet::new_unchecked(buffer), + checksum_caps, + )?)), + #[cfg(any(feature = "socket-raw", feature = "proto-rpl"))] + IpPayloadType::Raw => Ok(Self::Raw(buffer)), + #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] + IpPayloadType::Udp => { + let packet = &UdpPacket::new_unchecked(buffer); + let repr = UdpRepr::parse( + packet, + &header.src_addr.into(), + &header.dst_addr.into(), + checksum_caps, + )?; + + Ok(Self::Udp(repr, packet.payload())) + } + #[cfg(feature = "socket-tcp")] + IpPayloadType::Tcp => Ok(Self::Tcp(TcpRepr::parse( + &TcpPacket::new_unchecked(buffer), + &header.src_addr.into(), + &header.dst_addr.into(), + checksum_caps, + )?)), + #[cfg(feature = "socket-dhcpv4")] + IpPayloadType::Dhcpv4 => { + // FIXME: actually use the DHCP repr + let packet = &UdpPacket::new_unchecked(buffer); + let repr = UdpRepr::parse( + packet, + &header.src_addr.into(), + &header.dst_addr.into(), + checksum_caps, + )?; + + Ok(Self::Udp(repr, packet.payload())) + } + } + } + + pub fn parse( + buffer: &'a [u8], + payload_type: IpPayloadType, + header: &Ipv6Repr, + checksum_caps: &ChecksumCapabilities, + ) -> crate::wire::Result { + match payload_type { + #[cfg(feature = "proto-ipv4")] + IpPayloadType::Icmpv4 => Ok(Self::Icmpv4( + Icmpv4Repr::parse(&Icmpv4Packet::new_checked(buffer)?, checksum_caps) + .map_err(|_| crate::wire::Error)?, + )), + #[cfg(feature = "proto-igmp")] + IpPayloadType::Igmp => Ok(Self::Igmp(IgmpRepr::parse(&IgmpPacket::new_checked( + buffer, + )?)?)), + #[cfg(feature = "proto-ipv6")] + IpPayloadType::Icmpv6 => Ok(Self::Icmpv6(Icmpv6Repr::parse( + &header.src_addr, + &header.dst_addr, + &Icmpv6Packet::new_checked(buffer)?, + checksum_caps, + )?)), + #[cfg(any(feature = "socket-raw", feature = "proto-rpl"))] + IpPayloadType::Raw => Ok(Self::Raw(buffer)), + #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] + IpPayloadType::Udp => { + let packet = &UdpPacket::new_checked(buffer)?; + let repr = UdpRepr::parse( + packet, + &header.src_addr.into(), + &header.dst_addr.into(), + checksum_caps, + )?; + + Ok(Self::Udp(repr, packet.payload())) + } + #[cfg(feature = "socket-tcp")] + IpPayloadType::Tcp => Ok(Self::Tcp(TcpRepr::parse( + &TcpPacket::new_checked(buffer)?, + &header.src_addr.into(), + &header.dst_addr.into(), + checksum_caps, + )?)), + #[cfg(feature = "socket-dhcpv4")] + IpPayloadType::Dhcpv4 => { + // FIXME and actually use the DHCP representation + let packet = &UdpPacket::new_checked(buffer)?; + let repr = UdpRepr::parse( + packet, + &header.src_addr.into(), + &header.dst_addr.into(), + checksum_caps, + )?; + + Ok(Self::Udp(repr, packet.payload())) + } + } + } + + pub fn payload_type(&self) -> IpPayloadType { + match self { + #[cfg(feature = "proto-ipv4")] + Self::Icmpv4(_) => IpPayloadType::Icmpv4, + #[cfg(feature = "proto-igmp")] + Self::Igmp(_) => IpPayloadType::Igmp, + #[cfg(feature = "proto-ipv6")] + Self::Icmpv6(_) => IpPayloadType::Icmpv6, + #[cfg(any(feature = "socket-raw", feature = "proto-rpl"))] + Self::Raw(_) => IpPayloadType::Raw, + #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] + Self::Udp(_, _) => IpPayloadType::Udp, + #[cfg(feature = "socket-tcp")] + Self::Tcp(_) => IpPayloadType::Tcp, + #[cfg(feature = "socket-dhcpv4")] + Self::Dhcpv4(_, _) => IpPayloadType::Dhcpv4, + } + } + + pub(crate) fn emit(&self, header: &IpRepr, payload: &mut [u8], caps: &DeviceCapabilities) { + match self { #[cfg(feature = "proto-ipv4")] IpPayload::Icmpv4(icmpv4_repr) => { icmpv4_repr.emit(&mut Icmpv4Packet::new_unchecked(payload), &caps.checksum) @@ -85,7 +383,7 @@ impl<'p> Packet<'p> { IpPayload::Igmp(igmp_repr) => igmp_repr.emit(&mut IgmpPacket::new_unchecked(payload)), #[cfg(feature = "proto-ipv6")] IpPayload::Icmpv6(icmpv6_repr) => { - let ipv6_repr = match _ip_repr { + let ipv6_repr = match header { #[cfg(feature = "proto-ipv4")] IpRepr::Ipv4(_) => unreachable!(), IpRepr::Ipv6(repr) => repr, @@ -98,13 +396,13 @@ impl<'p> Packet<'p> { &caps.checksum, ) } - #[cfg(feature = "socket-raw")] + #[cfg(any(feature = "socket-raw", feature = "proto-rpl"))] IpPayload::Raw(raw_packet) => payload.copy_from_slice(raw_packet), #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] IpPayload::Udp(udp_repr, inner_payload) => udp_repr.emit( &mut UdpPacket::new_unchecked(payload), - &_ip_repr.src_addr(), - &_ip_repr.dst_addr(), + &header.src_addr(), + &header.dst_addr(), inner_payload.len(), |buf| buf.copy_from_slice(inner_payload), &caps.checksum, @@ -120,7 +418,7 @@ impl<'p> Packet<'p> { // I'm really not happy about this "solution" but I don't know what else to do. if let Some(max_burst_size) = caps.max_burst_size { let mut max_segment_size = caps.max_transmission_unit; - max_segment_size -= _ip_repr.header_len(); + max_segment_size -= header.header_len(); max_segment_size -= tcp_repr.header_len(); let max_window_size = max_burst_size * max_segment_size; @@ -131,16 +429,16 @@ impl<'p> Packet<'p> { tcp_repr.emit( &mut TcpPacket::new_unchecked(payload), - &_ip_repr.src_addr(), - &_ip_repr.dst_addr(), + &header.src_addr(), + &header.dst_addr(), &caps.checksum, ); } #[cfg(feature = "socket-dhcpv4")] IpPayload::Dhcpv4(udp_repr, dhcp_repr) => udp_repr.emit( &mut UdpPacket::new_unchecked(payload), - &_ip_repr.src_addr(), - &_ip_repr.dst_addr(), + &header.src_addr(), + &header.dst_addr(), dhcp_repr.buffer_len(), |buf| dhcp_repr.emit(&mut DhcpPacket::new_unchecked(buf)).unwrap(), &caps.checksum, @@ -149,69 +447,6 @@ impl<'p> Packet<'p> { } } -#[derive(Debug, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg(feature = "proto-ipv4")] -pub(crate) struct PacketV4<'p> { - header: Ipv4Repr, - payload: IpPayload<'p>, -} - -#[derive(Debug, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg(feature = "proto-ipv6")] -pub(crate) struct PacketV6<'p> { - pub(crate) header: Ipv6Repr, - #[cfg(feature = "proto-ipv6-hbh")] - pub(crate) hop_by_hop: Option>, - #[cfg(feature = "proto-ipv6-fragmentation")] - pub(crate) fragment: Option, - #[cfg(feature = "proto-ipv6-routing")] - pub(crate) routing: Option>, - pub(crate) payload: IpPayload<'p>, -} - -#[derive(Debug, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub(crate) enum IpPayload<'p> { - #[cfg(feature = "proto-ipv4")] - Icmpv4(Icmpv4Repr<'p>), - #[cfg(feature = "proto-igmp")] - Igmp(IgmpRepr), - #[cfg(feature = "proto-ipv6")] - Icmpv6(Icmpv6Repr<'p>), - #[cfg(feature = "socket-raw")] - Raw(&'p [u8]), - #[cfg(any(feature = "socket-udp", feature = "socket-dns"))] - Udp(UdpRepr, &'p [u8]), - #[cfg(feature = "socket-tcp")] - Tcp(TcpRepr<'p>), - #[cfg(feature = "socket-dhcpv4")] - Dhcpv4(UdpRepr, DhcpRepr<'p>), -} - -impl<'p> IpPayload<'p> { - #[cfg(feature = "proto-sixlowpan")] - pub(crate) fn as_sixlowpan_next_header(&self) -> SixlowpanNextHeader { - match self { - #[cfg(feature = "proto-ipv4")] - Self::Icmpv4(_) => unreachable!(), - #[cfg(feature = "socket-dhcpv4")] - Self::Dhcpv4(..) => unreachable!(), - #[cfg(feature = "proto-ipv6")] - Self::Icmpv6(_) => SixlowpanNextHeader::Uncompressed(IpProtocol::Icmpv6), - #[cfg(feature = "proto-igmp")] - Self::Igmp(_) => unreachable!(), - #[cfg(feature = "socket-tcp")] - Self::Tcp(_) => SixlowpanNextHeader::Uncompressed(IpProtocol::Tcp), - #[cfg(feature = "socket-udp")] - Self::Udp(..) => SixlowpanNextHeader::Compressed, - #[cfg(feature = "socket-raw")] - Self::Raw(_) => todo!(), - } - } -} - #[cfg(any(feature = "proto-ipv4", feature = "proto-ipv6"))] pub(crate) fn icmp_reply_payload_len(len: usize, mtu: usize, header_len: usize) -> usize { // Send back as much of the original payload as will fit within diff --git a/src/iface/rpl/consts.rs b/src/iface/rpl/consts.rs index 70a66138f..1d9514a30 100644 --- a/src/iface/rpl/consts.rs +++ b/src/iface/rpl/consts.rs @@ -1,8 +1,8 @@ -pub const SEQUENCE_WINDOW: u8 = 16; +pub(crate) const DEFAULT_MIN_HOP_RANK_INCREASE: u16 = 256; -pub const DEFAULT_MIN_HOP_RANK_INCREASE: u16 = 256; - -pub const DEFAULT_DIO_INTERVAL_MIN: u32 = 12; -pub const DEFAULT_DIO_REDUNDANCY_CONSTANT: usize = 10; +pub(crate) const DEFAULT_DIO_INTERVAL_MIN: u32 = 12; +pub(crate) const DEFAULT_DIO_REDUNDANCY_CONSTANT: usize = 10; /// This is 20 in the standard, but in Contiki they use: -pub const DEFAULT_DIO_INTERVAL_DOUBLINGS: u32 = 8; +pub(crate) const DEFAULT_DIO_INTERVAL_DOUBLINGS: u32 = 8; + +pub(crate) const DEFAULT_RPL_INSTANCE_ID: u8 = 0x1e; diff --git a/src/iface/rpl/mod.rs b/src/iface/rpl/mod.rs index 69aa9ae77..4f1fe28fd 100644 --- a/src/iface/rpl/mod.rs +++ b/src/iface/rpl/mod.rs @@ -1,9 +1,747 @@ #![allow(unused)] mod consts; -mod lollipop; mod of0; mod parents; mod rank; mod relations; mod trickle; + +use crate::config::RPL_MAX_OPTIONS; +use crate::rand::Rand; +use crate::time::{Duration, Instant}; +use crate::wire::{ + Icmpv6Repr, Ipv6Address, RplDao, RplDio, RplDodagConfiguration, RplOptionRepr, RplRepr, + RplSequenceCounter, RplTarget, RplTransitInformation, +}; + +pub(crate) use of0::{ObjectiveFunction, ObjectiveFunction0}; +pub(crate) use parents::{Parent, ParentSet}; +pub(crate) use rank::Rank; +pub(crate) use relations::Relations; + +pub use crate::wire::RplInstanceId; +pub use trickle::TrickleTimer; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModeOfOperation { + NoDownwardRoutesMaintained, + #[cfg(feature = "rpl-mop-1")] + NonStoringMode, + #[cfg(feature = "rpl-mop-2")] + StoringMode, + #[cfg(feature = "rpl-mop-3")] + StoringModeWithMulticast, +} + +#[cfg(feature = "std")] +impl core::fmt::Display for ModeOfOperation { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ModeOfOperation::NoDownwardRoutesMaintained => write!(f, "mop0"), + #[cfg(feature = "rpl-mop-1")] + ModeOfOperation::NonStoringMode => write!(f, "mop1"), + #[cfg(feature = "rpl-mop-2")] + ModeOfOperation::StoringMode => write!(f, "mop1"), + #[cfg(feature = "rpl-mop-3")] + ModeOfOperation::StoringModeWithMulticast => write!(f, "mop3"), + } + } +} + +impl From for ModeOfOperation { + fn from(value: crate::wire::rpl::ModeOfOperation) -> Self { + use crate::wire::rpl::ModeOfOperation as WireMop; + match value { + WireMop::NoDownwardRoutesMaintained => Self::NoDownwardRoutesMaintained, + #[cfg(feature = "rpl-mop-1")] + WireMop::NonStoringMode => Self::NonStoringMode, + #[cfg(feature = "rpl-mop-2")] + WireMop::StoringModeWithoutMulticast => Self::StoringMode, + #[cfg(feature = "rpl-mop-3")] + WireMop::StoringModeWithMulticast => Self::StoringModeWithMulticast, + + _ => unreachable!(), + } + } +} + +impl From for crate::wire::rpl::ModeOfOperation { + fn from(value: ModeOfOperation) -> Self { + use crate::wire::rpl::ModeOfOperation as WireMop; + + match value { + ModeOfOperation::NoDownwardRoutesMaintained => WireMop::NoDownwardRoutesMaintained, + #[cfg(feature = "rpl-mop-1")] + ModeOfOperation::NonStoringMode => WireMop::NonStoringMode, + #[cfg(feature = "rpl-mop-2")] + ModeOfOperation::StoringMode => WireMop::StoringModeWithoutMulticast, + #[cfg(feature = "rpl-mop-3")] + ModeOfOperation::StoringModeWithMulticast => WireMop::StoringModeWithMulticast, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Config { + pub mode_of_operation: ModeOfOperation, + pub root: Option, +} + +impl Default for Config { + fn default() -> Self { + // TODO: Make some kind of leaf mode + Self { + mode_of_operation: ModeOfOperation::NoDownwardRoutesMaintained, + root: None, + } + } +} + +impl Config { + /// Create a new RPL configuration. + pub fn new(mode_of_operation: ModeOfOperation) -> Self { + Self { + mode_of_operation, + root: None, + } + } + + /// Add RPL root configuration to this config. + pub fn add_root_config(mut self, root_config: RootConfig) -> Self { + self.root = Some(root_config); + self + } + + fn is_root(&self) -> bool { + self.root.is_some() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RootConfig { + pub instance_id: RplInstanceId, + pub dodag_id: Ipv6Address, + pub preference: u8, + pub dio_timer: TrickleTimer, +} + +impl RootConfig { + /// Create a new RPL Root configuration. + pub fn new(instance_id: RplInstanceId, dodag_id: Ipv6Address) -> Self { + Self { + instance_id, + dodag_id, + preference: 0, + dio_timer: Default::default(), + } + } + + /// Set the administrative preference of the DODAG. + pub fn with_preference(mut self, preference: u8) -> Self { + self.preference = preference; + self + } + + /// Set the DIO timer to use by the RPL implementation. + pub fn with_dio_timer(mut self, timer: TrickleTimer) -> Self { + self.dio_timer = timer; + self + } +} + +pub struct Rpl { + pub(crate) is_root: bool, + pub(crate) mode_of_operation: ModeOfOperation, + pub(crate) of: ObjectiveFunction0, + + pub(crate) dis_expiration: Instant, + + pub(crate) dodag: Option, +} + +pub struct Dodag { + pub(crate) instance_id: RplInstanceId, + pub(crate) id: Ipv6Address, + pub(crate) version_number: RplSequenceCounter, + pub(crate) preference: u8, + + pub(crate) rank: Rank, + + pub(crate) dio_timer: TrickleTimer, + + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + pub(crate) dao_expiration: Instant, + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + pub(crate) dao_seq_number: RplSequenceCounter, + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + pub(crate) dao_acks: heapless::Vec<(Ipv6Address, RplSequenceCounter), 16>, + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + pub(crate) daos: heapless::Vec, + + pub(crate) parent: Option, + pub(crate) without_parent: Option, + + pub(crate) authentication_enabled: bool, + pub(crate) path_control_size: u8, + + pub(crate) dtsn: RplSequenceCounter, + pub(crate) dtsn_incremented_at: Instant, + pub(crate) default_lifetime: u8, + pub(crate) lifetime_unit: u16, + pub(crate) grounded: bool, + + pub(crate) parent_set: ParentSet, + + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + pub(crate) relations: Relations, +} + +#[derive(Debug)] +pub(crate) struct Dao { + pub needs_sending: bool, + pub next_tx: Option, + pub sent_count: u8, + pub to: Ipv6Address, + pub targets: heapless::Vec, + pub parent: Option, + pub sequence: RplSequenceCounter, + pub is_no_path: bool, + pub lifetime: u8, + pub rank: Rank, + + pub instance_id: RplInstanceId, + pub dodag_id: Option, +} + +impl Dao { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + to: Ipv6Address, + targets: &[Ipv6Address; RPL_MAX_OPTIONS - 1], + parent: Option, + sequence: RplSequenceCounter, + lifetime: u8, + instance_id: RplInstanceId, + dodag_id: Option, + rank: Rank, + ) -> Self { + Dao { + needs_sending: false, + next_tx: None, + sent_count: 0, + to, + targets: heapless::Vec::from_slice(targets).unwrap(), // Length check in types + parent, + sequence, + lifetime, + is_no_path: false, + instance_id, + dodag_id, + rank, + } + } + + pub(crate) fn no_path( + to: Ipv6Address, + targets: heapless::Vec, + sequence: RplSequenceCounter, + instance_id: RplInstanceId, + dodag_id: Option, + rank: Rank, + ) -> Self { + Dao { + needs_sending: true, + next_tx: None, + sent_count: 0, + to, + targets, + parent: None, + sequence, + lifetime: 0, + is_no_path: true, + instance_id, + dodag_id, + rank, + } + } + + pub(crate) fn as_rpl_dao_repr<'dao>(&mut self) -> RplRepr<'dao> { + let mut options = heapless::Vec::new(); + for target in &self.targets { + options + .push(RplOptionRepr::RplTarget(RplTarget { + prefix_length: 64, // TODO: get the prefix length from the address. + prefix: heapless::Vec::from_slice(target.as_bytes()).unwrap(), + })) + .unwrap(); + } + options + .push(RplOptionRepr::TransitInformation(RplTransitInformation { + external: false, + path_control: 0, + path_sequence: 0, + path_lifetime: self.lifetime, + parent_address: self.parent, + })) + .unwrap(); + + RplRepr::DestinationAdvertisementObject(RplDao { + rpl_instance_id: self.instance_id, + expect_ack: self.lifetime != 0, + sequence: self.sequence, + dodag_id: self.dodag_id, + options, + }) + } + + pub(crate) fn has_multicast_target(&mut self) -> bool { + self.targets.iter().any(|target| target.is_multicast()) + } +} + +impl Rpl { + pub fn new(config: Config, now: Instant) -> Self { + Self { + is_root: config.is_root(), + mode_of_operation: config.mode_of_operation, + of: Default::default(), + dis_expiration: now + Duration::from_secs(5), + dodag: if let Some(root) = config.root { + Some(Dodag { + instance_id: root.instance_id, + id: root.dodag_id, + version_number: RplSequenceCounter::default(), + preference: root.preference, + rank: Rank::ROOT, + dio_timer: root.dio_timer, + #[cfg(any( + feature = "rpl-mop-1", + feature = "rpl-mop-2", + feature = "rpl-mop-3" + ))] + dao_expiration: now, + #[cfg(any( + feature = "rpl-mop-1", + feature = "rpl-mop-2", + feature = "rpl-mop-3" + ))] + dao_seq_number: RplSequenceCounter::default(), + #[cfg(any( + feature = "rpl-mop-1", + feature = "rpl-mop-2", + feature = "rpl-mop-3" + ))] + dao_acks: Default::default(), + #[cfg(any( + feature = "rpl-mop-1", + feature = "rpl-mop-2", + feature = "rpl-mop-3" + ))] + daos: Default::default(), + parent: None, + without_parent: None, + authentication_enabled: false, + path_control_size: 0, + dtsn: RplSequenceCounter::default(), + dtsn_incremented_at: now, + default_lifetime: 30, + lifetime_unit: 60, + grounded: false, + parent_set: Default::default(), + #[cfg(any( + feature = "rpl-mop-1", + feature = "rpl-mop-2", + feature = "rpl-mop-3" + ))] + relations: Default::default(), + }) + } else { + None + }, + } + } + + pub fn parent(&self) -> Option<&Ipv6Address> { + if let Some(dodag) = &self.dodag { + dodag.parent.as_ref() + } else { + None + } + } + + pub fn is_root(&self) -> bool { + self.is_root + } + + pub fn instance(&self) -> Option<&RplInstanceId> { + if let Some(dodag) = &self.dodag { + Some(&dodag.instance_id) + } else { + None + } + } + + pub fn dodag(&self) -> Option<&Dodag> { + self.dodag.as_ref() + } + + pub(crate) fn has_parent(&self) -> bool { + if let Some(dodag) = &self.dodag { + return dodag.parent.is_some(); + } + + false + } + + /// ## Panics + /// This function will panic if the node is not part of a DODAG. + pub(crate) fn dodag_configuration<'o>(&self) -> RplOptionRepr<'o> { + let dodag = self.dodag.as_ref().unwrap(); + + // FIXME: I think we need to convert from seconds to something else, not sure what. + let dio_interval_doublings = dodag.dio_timer.i_max as u8 - dodag.dio_timer.i_min as u8; + + RplOptionRepr::DodagConfiguration(RplDodagConfiguration { + authentication_enabled: dodag.authentication_enabled, + path_control_size: dodag.path_control_size, + dio_interval_doublings, + dio_interval_min: dodag.dio_timer.i_min as u8, + dio_redundancy_constant: dodag.dio_timer.k as u8, + max_rank_increase: self.of.max_rank_increase(), + minimum_hop_rank_increase: self.of.min_hop_rank_increase(), + objective_code_point: self.of.objective_code_point(), + default_lifetime: dodag.default_lifetime, + lifetime_unit: dodag.lifetime_unit, + }) + } + + /// ## Panics + /// This function will panic if the node is not part of a DODAG. + pub(crate) fn dodag_information_object<'o>( + &self, + options: heapless::Vec, 2>, + ) -> RplRepr<'o> { + let dodag = self.dodag.as_ref().unwrap(); + + RplRepr::DodagInformationObject(RplDio { + rpl_instance_id: dodag.instance_id, + version_number: dodag.version_number, + rank: dodag.rank.raw_value(), + grounded: dodag.grounded, + mode_of_operation: self.mode_of_operation.into(), + dodag_preference: dodag.preference, + dtsn: dodag.dtsn, + dodag_id: dodag.id, + options, + }) + } +} + +impl Dodag { + pub fn id(&self) -> &Ipv6Address { + &self.id + } + /// ## Panics + /// This function will panic if the DODAG does not have a parent selected. + // pub(crate) fn remove_parent( + pub(crate) fn remove_parent( + &mut self, + // mop: ModeOfOperation, + // our_addr: Ipv6Address, + // of: &OF, + // now: Instant, + // rand: &mut Rand, + ) -> Ipv6Address { + let old_parent = self.parent.unwrap(); + + self.parent = None; + self.parent_set.remove(&old_parent); + + // FIXME: Probably not a good idea to have a recursive loop in function calls + // self.find_new_parent(mop, our_addr, of, now, rand); + + old_parent + } + + /// ## Panics + /// This function will panic if the DODAG does not have a parent selected. + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + pub(crate) fn remove_parent_with_no_path( + &mut self, + mop: ModeOfOperation, + // our_addr: Ipv6Address, + targets: &[Ipv6Address], + targets_multicast: &[Ipv6Address], + of: &OF, + now: Instant, + rand: &mut Rand, + ) { + // let old_parent = self.remove_parent(mop, our_addr, of, now, rand); + let old_parent = self.remove_parent(); + + #[cfg(any(feature = "rpl-mop-2", feature = "rpl-mop-3"))] + { + for targets in targets.chunks(RPL_MAX_OPTIONS - 1) { + self.daos + .push(Dao::no_path( + old_parent, + heapless::Vec::from_slice(targets).unwrap(), + self.dao_seq_number, + self.instance_id, + Some(self.id), + self.rank, + )) + .unwrap(); + self.dao_seq_number.increment(); + } + + #[cfg(feature = "rpl-mop-3")] + for targets in targets_multicast.chunks(RPL_MAX_OPTIONS - 1) { + self.daos + .push(Dao::no_path( + old_parent, + heapless::Vec::from_slice(targets).unwrap(), + self.dao_seq_number, + self.instance_id, + Some(self.id), + self.rank, + )) + .unwrap(); + self.dao_seq_number.increment(); + } + } + } + + pub(crate) fn find_new_parent( + &mut self, + mop: ModeOfOperation, + targets: &[Ipv6Address], + targets_multicast: &[Ipv6Address], + of: &OF, + now: Instant, + rand: &mut Rand, + ) { + // Remove expired parents from the parent set. + self.parent_set + .purge(now, self.dio_timer.max_expiration() * 2); + + let old_parent = self.parent; + + if let Some(parent) = of.preferred_parent(&self.parent_set) { + // Send a NO-PATH DAO in MOP 2 when we already had a parent. + #[cfg(any(feature = "rpl-mop-2", feature = "rpl-mop-3"))] + if let Some(old_parent) = old_parent { + let is_mop2 = { + #[cfg(feature = "rpl-mop-2")] + { + matches!(mop, ModeOfOperation::StoringMode) + } + #[cfg(not(feature = "rpl-mop-2"))] + false + }; + let is_mop3 = { + #[cfg(feature = "rpl-mop-3")] + { + matches!(mop, ModeOfOperation::StoringModeWithMulticast) + } + #[cfg(not(feature = "rpl-mop-3"))] + false + }; + if (is_mop2 || is_mop3) && old_parent != parent { + net_trace!( + "scheduling NO-PATH DAO for {:?} and {:?} to {}", + targets, + targets_multicast, + old_parent + ); + self.remove_parent_with_no_path( + mop, + // our_addr, + targets, + targets_multicast, + of, + now, + rand, + ) + } + } + + // Schedule a DAO when we didn't have a parent yet, or when the new parent is different + // from our old parent. + if old_parent.is_none() || old_parent != Some(parent) { + self.dio_timer.hear_inconsistency(now, rand); + self.parent = Some(parent); + self.without_parent = None; + self.rank = of.rank(self.rank, self.parent_set.find(&parent).unwrap().rank); + + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + if !matches!(mop, ModeOfOperation::NoDownwardRoutesMaintained) { + self.schedule_dao(mop, targets, targets_multicast, parent, now, false); + } + } + } else { + self.without_parent = Some(now); + self.rank = Rank::INFINITE; + } + } + + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + pub(crate) fn schedule_dao( + &mut self, + mop: ModeOfOperation, + targets: &[Ipv6Address], + targets_multicast: &[Ipv6Address], + parent: Ipv6Address, + now: Instant, + is_no_path: bool, + ) -> Result<(), DodagTransmissionError> { + use heapless::LinearMap; + + #[cfg(feature = "rpl-mop-1")] + if matches!(mop, ModeOfOperation::NonStoringMode) { + net_trace!("scheduling DAO: {} is parent of {:?}", parent, targets); + for targets in targets.chunks(RPL_MAX_OPTIONS - 1) { + self.daos + .push(if is_no_path { + Dao::no_path( + self.id, + targets.try_into().unwrap(), // Checks in the types + self.dao_seq_number, + self.instance_id, + Some(self.id), + self.rank, + ) + } else { + Dao::new( + self.id, + targets.try_into().unwrap(), // Checks in the types + Some(parent), + self.dao_seq_number, + self.default_lifetime, + self.instance_id, + Some(self.id), + self.rank, + ) + }) + .map_err(|_err| DodagTransmissionError::DaoExhausted); + self.dao_seq_number.increment(); + } + } + + #[cfg(all(feature = "rpl-mop-2", feature = "rpl-mop-3"))] + if matches!( + mop, + ModeOfOperation::StoringMode | ModeOfOperation::StoringModeWithMulticast + ) { + net_trace!("scheduling DAO: {} is parent of {:?}", parent, targets); + for targets in targets.chunks(RPL_MAX_OPTIONS - 1) { + self.daos + .push(if is_no_path { + Dao::no_path( + parent, + targets.try_into().unwrap(), // Checks in the types + self.dao_seq_number, + self.instance_id, + Some(self.id), + self.rank, + ) + } else { + Dao::new( + parent, + targets.try_into().unwrap(), // Checks in the types + None, + self.dao_seq_number, + self.default_lifetime, + self.instance_id, + Some(self.id), + self.rank, + ) + }) + .unwrap(); + + self.dao_seq_number.increment(); + } + + // If we are in MOP3, we also send a DOA with our subscribed multicast addresses. + #[cfg(feature = "rpl-mop-3")] + { + net_trace!("scheduling multicast DAO"); + for targets in targets_multicast.chunks(RPL_MAX_OPTIONS - 1) { + self.daos + .push(if is_no_path { + Dao::no_path( + parent, + targets.try_into().unwrap(), // Checks in the types + self.dao_seq_number, + self.instance_id, + Some(self.id), + self.rank, + ) + } else { + Dao::new( + parent, + targets.try_into().unwrap(), // Checks in the types + None, + self.dao_seq_number, + self.default_lifetime, + self.instance_id, + Some(self.id), + self.rank, + ) + }) + .unwrap(); + + self.dao_seq_number.increment(); + } + } + } + + let exp = (self.lifetime_unit as u64 * self.default_lifetime as u64) + .checked_sub(2 * 60) + .unwrap_or(2 * 60); + self.dao_expiration = now + Duration::from_secs(exp); + + Ok(()) + } + + /// ## Panics + /// This function will panic if the node is not part of a DODAG. + #[cfg(any(feature = "rpl-mop-1", feature = "rpl-mop-2", feature = "rpl-mop-3"))] + pub(crate) fn destination_advertisement_object<'o>( + &mut self, + options: heapless::Vec, 2>, + ) -> RplRepr<'o> { + let sequence = self.dao_seq_number; + self.dao_seq_number.increment(); + RplRepr::DestinationAdvertisementObject(RplDao { + rpl_instance_id: self.instance_id, + expect_ack: true, + sequence, + dodag_id: Some(self.id), + options, + }) + } +} + +#[derive(Debug, Clone)] +pub enum DodagTransmissionError { + DaoExhausted, +} + +impl core::fmt::Display for DodagTransmissionError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::DaoExhausted => write!(f, "DAO buffer is exhausted"), + } + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for DodagTransmissionError { + fn format(&self, f: defmt::Formatter<'_>) { + match self { + Self::DaoExhausted => defmt::write!(f, "DAO buffer is exhausted"), + } + } +} diff --git a/src/iface/rpl/of0.rs b/src/iface/rpl/of0.rs index 99e4d1f36..eaafdec16 100644 --- a/src/iface/rpl/of0.rs +++ b/src/iface/rpl/of0.rs @@ -1,16 +1,39 @@ use super::parents::*; use super::rank::Rank; -pub struct ObjectiveFunction0; +use crate::wire::Ipv6Address; -pub(crate) trait ObjectiveFunction { +pub(crate) trait ObjectiveFunction: Default { const OCP: u16; /// Return the new calculated Rank, based on information from the parent. - fn rank(current_rank: Rank, parent_rank: Rank) -> Rank; + fn rank(&self, current_rank: Rank, parent_rank: Rank) -> Rank; /// Return the preferred parent from a given parent set. - fn preferred_parent(parent_set: &ParentSet) -> Option<&Parent>; + fn preferred_parent(&self, parent_set: &ParentSet) -> Option; + + fn objective_code_point(&self) -> u16; + + /// Return the MaxRankIncrease value of an Objective Function. + fn max_rank_increase(&self) -> u16; + /// Set the MaxRankIncrease value of an Objective Function. + fn set_max_rank_increase(&mut self, max_rank_increase: u16); + + /// Return the MinHopRankIncrease value of an Objective Function. + fn min_hop_rank_increase(&self) -> u16; + /// Set the MinHopRankIncrease value of an Objective Function. + fn set_min_hop_rank_increase(&mut self, min_hop_rank_increase: u16); +} + +pub struct ObjectiveFunction0 { + max_rank_increase: u16, + min_hop_rank_increase: u16, +} + +impl Default for ObjectiveFunction0 { + fn default() -> Self { + Self::new(Self::MIN_HOP_RANK_INCREASE, Self::MAX_RANK_INCREASE) + } } impl ObjectiveFunction0 { @@ -20,54 +43,92 @@ impl ObjectiveFunction0 { const RANK_FACTOR: u16 = 1; const RANK_STEP: u16 = 3; - fn rank_increase(parent_rank: Rank) -> u16 { - (Self::RANK_FACTOR * Self::RANK_STEP + Self::RANK_STRETCH) - * parent_rank.min_hop_rank_increase + const MIN_HOP_RANK_INCREASE: u16 = 256; + + // We use a value of 0 for the maximum rank increase, since the OF0 RFC does not define one. + // This value is application specific and limits how deep a RPL DODAG network will be. + // 0 means that the depth of the tree is not limited. + // Contiki-NG uses a value of 7. + const MAX_RANK_INCREASE: u16 = 0; + + pub(crate) fn new(min_hop_rank_increase: u16, max_rank_increase: u16) -> Self { + Self { + min_hop_rank_increase, + max_rank_increase, + } + } + + fn rank_increase(&self, parent_rank: Rank) -> u16 { + (Self::RANK_FACTOR * Self::RANK_STEP + Self::RANK_STRETCH) * self.min_hop_rank_increase } } impl ObjectiveFunction for ObjectiveFunction0 { const OCP: u16 = 0; - fn rank(_: Rank, parent_rank: Rank) -> Rank { + fn rank(&self, _: Rank, parent_rank: Rank) -> Rank { assert_ne!(parent_rank, Rank::INFINITE); Rank::new( - parent_rank.value + Self::rank_increase(parent_rank), + parent_rank.value + self.rank_increase(parent_rank), parent_rank.min_hop_rank_increase, ) } - fn preferred_parent(parent_set: &ParentSet) -> Option<&Parent> { + fn preferred_parent(&self, parent_set: &ParentSet) -> Option { + let mut pref_addr = None; let mut pref_parent: Option<&Parent> = None; - for (_, parent) in parent_set.parents() { - if pref_parent.is_none() || parent.rank() < pref_parent.unwrap().rank() { + for parent in parent_set.parents() { + if pref_parent.is_none() || parent.rank < pref_parent.unwrap().rank { pref_parent = Some(parent); + pref_addr = Some(parent.address); } } - pref_parent + pref_addr + } + + fn objective_code_point(&self) -> u16 { + Self::OCP + } + + fn max_rank_increase(&self) -> u16 { + self.max_rank_increase + } + + fn min_hop_rank_increase(&self) -> u16 { + self.min_hop_rank_increase + } + + fn set_max_rank_increase(&mut self, max_rank_increase: u16) { + self.max_rank_increase = max_rank_increase; + } + + fn set_min_hop_rank_increase(&mut self, min_hop_rank_increase: u16) { + self.min_hop_rank_increase = min_hop_rank_increase; } } #[cfg(test)] mod tests { use crate::iface::rpl::consts::DEFAULT_MIN_HOP_RANK_INCREASE; + use crate::time::Instant; use super::*; #[test] fn rank_increase() { + let of = ObjectiveFunction0::default(); // 256 (root) + 3 * 256 assert_eq!( - ObjectiveFunction0::rank(Rank::INFINITE, Rank::ROOT), + of.rank(Rank::INFINITE, Rank::ROOT), Rank::new(256 + 3 * 256, DEFAULT_MIN_HOP_RANK_INCREASE) ); // 1024 + 3 * 256 assert_eq!( - ObjectiveFunction0::rank( + of.rank( Rank::INFINITE, Rank::new(1024, DEFAULT_MIN_HOP_RANK_INCREASE) ), @@ -78,18 +139,14 @@ mod tests { #[test] #[should_panic] fn rank_increase_infinite() { - assert_eq!( - ObjectiveFunction0::rank(Rank::INFINITE, Rank::INFINITE), - Rank::INFINITE - ); + let of = ObjectiveFunction0::default(); + assert_eq!(of.rank(Rank::INFINITE, Rank::INFINITE), Rank::INFINITE); } #[test] fn empty_set() { - assert_eq!( - ObjectiveFunction0::preferred_parent(&ParentSet::default()), - None - ); + let of = ObjectiveFunction0::default(); + assert_eq!(of.preferred_parent(&ParentSet::default()), None); } #[test] @@ -98,32 +155,28 @@ mod tests { let mut parents = ParentSet::default(); - parents.add( - Ipv6Address::default(), - Parent::new(0, Rank::ROOT, Default::default(), Ipv6Address::default()), - ); + parents.add(Parent::new( + Default::default(), + Rank::ROOT, + Default::default(), + Default::default(), + Default::default(), + Instant::now(), + )); let mut address = Ipv6Address::default(); address.0[15] = 1; - parents.add( + parents.add(Parent::new( address, - Parent::new( - 0, - Rank::new(1024, DEFAULT_MIN_HOP_RANK_INCREASE), - Default::default(), - Ipv6Address::default(), - ), - ); - - assert_eq!( - ObjectiveFunction0::preferred_parent(&parents), - Some(&Parent::new( - 0, - Rank::ROOT, - Default::default(), - Ipv6Address::default(), - )) - ); + Rank::new(1024, DEFAULT_MIN_HOP_RANK_INCREASE), + Default::default(), + Default::default(), + Default::default(), + Instant::now(), + )); + + let of = ObjectiveFunction0::default(); + assert_eq!(of.preferred_parent(&parents), Some(Ipv6Address::default())); } } diff --git a/src/iface/rpl/parents.rs b/src/iface/rpl/parents.rs index 70d5a5e88..280664756 100644 --- a/src/iface/rpl/parents.rs +++ b/src/iface/rpl/parents.rs @@ -1,81 +1,123 @@ -use crate::wire::Ipv6Address; +use crate::time::{Duration, Instant}; +use crate::wire::{Ipv6Address, RplSequenceCounter}; -use super::{lollipop::SequenceCounter, rank::Rank}; +use super::rank::Rank; use crate::config::RPL_PARENTS_BUFFER_COUNT; #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) struct Parent { - rank: Rank, - preference: u8, - version_number: SequenceCounter, - dodag_id: Ipv6Address, + pub address: Ipv6Address, + pub dodag_id: Ipv6Address, + pub rank: Rank, + pub version_number: RplSequenceCounter, + pub dtsn: RplSequenceCounter, + pub last_heard: Instant, } impl Parent { /// Create a new parent. pub(crate) fn new( - preference: u8, + address: Ipv6Address, rank: Rank, - version_number: SequenceCounter, + version_number: RplSequenceCounter, + dtsn: RplSequenceCounter, dodag_id: Ipv6Address, + last_heard: Instant, ) -> Self { Self { + address, rank, - preference, version_number, + dtsn, dodag_id, + last_heard, } } - - /// Return the Rank of the parent. - pub(crate) fn rank(&self) -> &Rank { - &self.rank - } } #[derive(Debug, Default)] pub(crate) struct ParentSet { - parents: heapless::LinearMap, + parents: heapless::Vec, } impl ParentSet { /// Add a new parent to the parent set. The Rank of the new parent should be lower than the /// Rank of the node that holds this parent set. - pub(crate) fn add(&mut self, address: Ipv6Address, parent: Parent) { - if let Some(p) = self.parents.get_mut(&address) { + pub(crate) fn add(&mut self, parent: Parent) -> Result<(), Parent> { + if let Some(p) = self.find_mut(&parent.address) { *p = parent; - } else if let Err(p) = self.parents.insert(address, parent) { - if let Some((w_a, w_p)) = self.worst_parent() { - if w_p.rank.dag_rank() > parent.rank.dag_rank() { - self.parents.remove(&w_a.clone()).unwrap(); - self.parents.insert(address, parent).unwrap(); - } else { - net_debug!("could not add {} to parent set, buffer is full", address); + } else { + match self.parents.push(parent) { + Ok(_) => net_trace!("added {} to parent set", parent.address), + Err(e) => { + if let Some(worst_parent) = self.worst_parent() { + if worst_parent.rank.dag_rank() > parent.rank.dag_rank() { + *worst_parent = parent; + net_trace!("added {} to parent set", parent.address); + } else { + return Err(parent); + } + } else { + unreachable!() + } } + } + } + + Ok(()) + } + + pub(crate) fn remove(&mut self, address: &Ipv6Address) { + if let Some(i) = self.parents.iter().enumerate().find_map(|(i, p)| { + if p.address == *address { + Some(i) } else { - unreachable!() + None } + }) { + self.parents.remove(i); } } + pub(crate) fn is_empty(&self) -> bool { + self.parents.is_empty() + } + + pub(crate) fn clear(&mut self) { + self.parents.clear(); + } + /// Find a parent based on its address. pub(crate) fn find(&self, address: &Ipv6Address) -> Option<&Parent> { - self.parents.get(address) + self.parents.iter().find(|p| p.address == *address) } /// Find a mutable parent based on its address. pub(crate) fn find_mut(&mut self, address: &Ipv6Address) -> Option<&mut Parent> { - self.parents.get_mut(address) + self.parents.iter_mut().find(|p| p.address == *address) } /// Return a slice to the parent set. - pub(crate) fn parents(&self) -> impl Iterator { + pub(crate) fn parents(&self) -> impl Iterator { self.parents.iter() } /// Find the worst parent that is currently in the parent set. - fn worst_parent(&self) -> Option<(&Ipv6Address, &Parent)> { - self.parents.iter().max_by_key(|(k, v)| v.rank.dag_rank()) + fn worst_parent(&mut self) -> Option<&mut Parent> { + self.parents.iter_mut().max_by_key(|p| p.rank.dag_rank()) + } + + pub(crate) fn purge(&mut self, now: Instant, expiration: Duration) { + let mut keys = heapless::Vec::::new(); + for (i, p) in self.parents.iter().enumerate() { + if p.last_heard + expiration < now { + keys.push(i); + } + } + + for k in keys { + self.parents.remove(k); + } } } @@ -85,25 +127,33 @@ mod tests { #[test] fn add_parent() { + let now = Instant::now(); let mut set = ParentSet::default(); - set.add( + set.add(Parent::new( Default::default(), - Parent::new(0, Rank::ROOT, Default::default(), Default::default()), - ); + Rank::ROOT, + Default::default(), + Default::default(), + Default::default(), + now, + )); assert_eq!( set.find(&Default::default()), Some(&Parent::new( - 0, + Default::default(), Rank::ROOT, Default::default(), - Default::default() + Default::default(), + Default::default(), + now, )) ); } #[test] fn add_more_parents() { + let now = Instant::now(); use super::super::consts::DEFAULT_MIN_HOP_RANK_INCREASE; let mut set = ParentSet::default(); @@ -114,23 +164,24 @@ mod tests { address.0[15] = i as u8; last_address = address; - set.add( + set.add(Parent::new( address, - Parent::new( - 0, - Rank::new(256 * i, DEFAULT_MIN_HOP_RANK_INCREASE), - Default::default(), - address, - ), - ); + Rank::new(256 * i, DEFAULT_MIN_HOP_RANK_INCREASE), + Default::default(), + Default::default(), + address, + now, + )); assert_eq!( set.find(&address), Some(&Parent::new( - 0, + address, Rank::new(256 * i, DEFAULT_MIN_HOP_RANK_INCREASE), Default::default(), + Default::default(), address, + now, )) ); } @@ -139,36 +190,36 @@ mod tests { // set. let mut address = Ipv6Address::default(); address.0[15] = 8; - set.add( + set.add(Parent::new( address, - Parent::new( - 0, - Rank::new(256 * 8, DEFAULT_MIN_HOP_RANK_INCREASE), - Default::default(), - address, - ), - ); + Rank::new(256 * 8, DEFAULT_MIN_HOP_RANK_INCREASE), + Default::default(), + Default::default(), + address, + now, + )); assert_eq!(set.find(&address), None); /// This Parent has a better rank than the last one in the set. let mut address = Ipv6Address::default(); address.0[15] = 9; - set.add( + set.add(Parent::new( address, - Parent::new( - 0, - Rank::new(0, DEFAULT_MIN_HOP_RANK_INCREASE), - Default::default(), - address, - ), - ); + Rank::new(0, DEFAULT_MIN_HOP_RANK_INCREASE), + Default::default(), + Default::default(), + address, + now, + )); assert_eq!( set.find(&address), Some(&Parent::new( - 0, + address, Rank::new(0, DEFAULT_MIN_HOP_RANK_INCREASE), Default::default(), - address + Default::default(), + address, + now, )) ); assert_eq!(set.find(&last_address), None); diff --git a/src/iface/rpl/relations.rs b/src/iface/rpl/relations.rs index da02a3cf9..f636830f9 100644 --- a/src/iface/rpl/relations.rs +++ b/src/iface/rpl/relations.rs @@ -1,13 +1,297 @@ -use crate::time::Instant; +use crate::time::{Duration, Instant}; use crate::wire::Ipv6Address; -use crate::config::RPL_RELATIONS_BUFFER_COUNT; +use crate::config::{RPL_MAX_NEXT_HOP_PER_DESTINATION, RPL_RELATIONS_BUFFER_COUNT}; #[derive(Debug)] -pub struct Relation { +pub enum RelationError { + NextHopExhausted, + ToFewNextHops, + RelationTypeNotSupported, +} + +#[cfg(feature = "std")] +impl std::error::Error for RelationError {} + +impl core::fmt::Display for RelationError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RelationError::NextHopExhausted => write!(f, "Next hop exhausted"), + RelationError::ToFewNextHops => write!(f, "Expected at least 1 next hop"), + RelationError::RelationTypeNotSupported => { + write!(f, "The type of destination is not supported as a relation") + } + } + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for RelationError { + fn format(&self, f: defmt::Formatter<'_>) { + match self { + RelationError::NextHopExhausted => defmt::write!(f, "Next hop exhausted"), + RelationError::ToFewNextHops => defmt::write!(f, "Expected at least 1 next hop"), + RelationError::RelationTypeNotSupported => { + defmt::write!(f, "The type of destination is not supported as a relation") + } + } + } +} + +#[derive(Debug)] +pub struct UnicastRelation { destination: Ipv6Address, - next_hop: Ipv6Address, - expiration: Instant, + next_hop: [RelationHop; 1], +} + +impl core::fmt::Display for UnicastRelation { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{} via [{}] (expires at {})", + self.destination, + self.next_hop[0], + self.next_hop[0].added + self.next_hop[0].lifetime + ) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for UnicastRelation { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "{} via [{}] (expires at {})", + self.destination, + self.next_hop[0], + self.next_hop[0].added + self.next_hop[0].lifetime + ) + } +} + +#[cfg(feature = "rpl-mop-3")] +#[derive(Debug)] +pub struct MulticastRelation { + destination: Ipv6Address, + next_hops: heapless::Vec, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct RelationHop { + pub ip: Ipv6Address, + pub added: Instant, + pub lifetime: Duration, +} + +impl RelationHop { + pub fn expires_at(&self) -> Instant { + self.added + self.lifetime + } + + pub fn has_expired(&self, now: Instant) -> bool { + self.expires_at() <= now + } +} + +impl core::fmt::Display for RelationHop { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{} (expires at {})", self.ip, self.added + self.lifetime) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for RelationHop { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "{} (expires at {})", self.ip, self.added + self.lifetime) + } +} + +#[cfg(feature = "rpl-mop-3")] +impl MulticastRelation { + /// Insert a next hop for this relation. If the next hop already exists, if + /// will return Ok(true) otherwise Ok(false) + fn insert_next_hop( + &mut self, + ip: Ipv6Address, + added: Instant, + lifetime: Duration, + ) -> Result { + if let Some(next_hop) = self.next_hops.iter_mut().find(|hop| hop.ip == ip) { + next_hop.added = added; + next_hop.lifetime = lifetime; + + Ok(true) + } else { + self.next_hops + .push(RelationHop { + ip, + added, + lifetime, + }) + .map_err(|_err| RelationError::NextHopExhausted)?; + Ok(false) + } + } + + /// Removes the next_hop from this relation + pub fn remove_next_hop(&mut self, ip: Ipv6Address) { + self.next_hops.retain(|next_hop| next_hop.ip == ip); + } +} + +#[cfg(feature = "rpl-mop-3")] +impl core::fmt::Display for MulticastRelation { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{} via [", self.destination)?; + + for hop in &self.next_hops { + write!(f, "{},", hop)?; + } + + write!(f, "]")?; + + Ok(()) + } +} + +#[cfg(all(feature = "defmt", feature = "rpl-mop-3"))] +impl defmt::Format for MulticastRelation { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "{} via [", self.destination); + + for hop in &self.next_hops { + defmt::write!(f, "{},", hop); + } + + defmt::write!(f, "]"); + } +} + +#[derive(Debug)] +pub enum Relation { + Unicast(UnicastRelation), + #[cfg(feature = "rpl-mop-3")] + Multicast(MulticastRelation), +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Relation { + fn format(&self, fmt: defmt::Formatter) { + match self { + Self::Unicast(rel) => rel.format(fmt), + #[cfg(feature = "rpl-mop-3")] + Self::Multicast(rel) => rel.format(fmt), + } + } +} + +impl Relation { + pub fn new( + destination: Ipv6Address, + next_hops: &[Ipv6Address], + now: Instant, + lifetime: Duration, + ) -> Result { + if destination.is_multicast() { + #[cfg(feature = "rpl-mop-3")] + { + Ok(Self::Multicast(MulticastRelation { + destination, + next_hops: heapless::Vec::from_iter(next_hops.iter().map(|hop| RelationHop { + ip: *hop, + added: now, + lifetime, + })), + })) + } + #[cfg(not(feature = "rpl-mop-3"))] + Err(RelationError::RelationTypeNotSupported) + } else { + if next_hops.len() > 1 { + return Err(RelationError::NextHopExhausted); + } + Ok(Self::Unicast(UnicastRelation { + destination, + next_hop: [RelationHop { + ip: next_hops[0], + added: now, + lifetime, + }], + })) + } + } + + pub fn destination(&self) -> Ipv6Address { + match self { + Self::Unicast(rel) => rel.destination, + #[cfg(feature = "rpl-mop-3")] + Self::Multicast(rel) => rel.destination, + } + } + + /// Insert a next hop for the given relation. If this is a unicast relation, + /// the previous will be overwritten and if it is a multicast relation it + /// will add an extra hop if the hop does not already exist. If there already + /// exists a hop in the multicast relation, the lifetime related metadata + /// will be updated. + pub fn insert_next_hop( + &mut self, + ip: Ipv6Address, + added: Instant, + lifetime: Duration, + ) -> Result { + match self { + Self::Unicast(rel) => { + let next_hop = &mut rel.next_hop[0]; + next_hop.ip = ip; + next_hop.added = added; + next_hop.lifetime = lifetime; + Ok(true) + } + #[cfg(feature = "rpl-mop-3")] + Self::Multicast(rel) => rel.insert_next_hop(ip, added, lifetime), + } + } + + pub fn next_hop(&self) -> &[RelationHop] { + match self { + Self::Unicast(rel) => &rel.next_hop, + #[cfg(feature = "rpl-mop-3")] + Self::Multicast(rel) => &rel.next_hops, + } + } + + /// A relation has expired if all its possible hops have expired + pub fn has_expired(&self, now: Instant) -> bool { + match self { + Self::Unicast(rel) => rel.next_hop.iter().all(|hop| hop.has_expired(now)), + #[cfg(feature = "rpl-mop-3")] + Self::Multicast(rel) => rel.next_hops.iter().all(|hop| hop.has_expired(now)), + } + } + + pub fn is_multicast(&self) -> bool { + #[cfg(feature = "rpl-mop-3")] + { + matches!(self, Self::Multicast(_)) + } + #[cfg(not(feature = "rpl-mop-3"))] + false + } + + pub fn is_unicast(&self) -> bool { + matches!(self, Self::Unicast(_)) + } +} + +impl core::fmt::Display for Relation { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Unicast(rel) => rel.fmt(f), + #[cfg(feature = "rpl-mop-3")] + Self::Multicast(rel) => rel.fmt(f), + } + } } #[derive(Default, Debug)] @@ -21,39 +305,67 @@ impl Relations { pub fn add_relation( &mut self, destination: Ipv6Address, - next_hop: Ipv6Address, - expiration: Instant, - ) { + next_hops: &[Ipv6Address], + now: Instant, + lifetime: Duration, + ) -> Result<(), RelationError> { if let Some(r) = self .relations .iter_mut() - .find(|r| r.destination == destination) + .find(|r| r.destination() == destination) { - r.next_hop = next_hop; - r.expiration = expiration; - } else { - let relation = Relation { + net_trace!( + "Updating old relation information for destination: {} with hops: {:?}", destination, - next_hop, - expiration, - }; + next_hops + ); + for next_hop in next_hops { + r.insert_next_hop(*next_hop, now, lifetime)?; + } + } else { + let relation = Relation::new(destination, next_hops, now, lifetime)?; if let Err(e) = self.relations.push(relation) { - net_debug!("Unable to add relation, buffer is full"); + net_trace!("unable to add relation, buffer is full"); } } + + Ok(()) } /// Remove all relation entries for a specific destination. - pub fn remove_relation(&mut self, destination: Ipv6Address) { - self.relations.retain(|r| r.destination != destination) + pub fn remove_hop_from_relation(&mut self, destination: Ipv6Address, hop: Ipv6Address) { + self.relations.retain_mut(|r| match r { + Relation::Unicast(r) => !(r.destination == destination && r.next_hop[0].ip == hop), + Relation::Multicast(r) => { + // Do nothing if the destination is not correct + if r.destination != destination { + return true; + } + + let pos = r.next_hops.iter().position(|h| h.ip == hop); + + // Remove if position exists + let Some(pos) = pos else { + return true; // Does not exist, so do nothing + }; + r.next_hops.swap_remove(pos); + + // Remove relation if still it has no more hops + !r.next_hops.is_empty() + } + }) } /// Return the next hop for a specific IPv6 address, if there is one. - pub fn find_next_hop(&mut self, destination: Ipv6Address) -> Option { + pub fn find_next_hop(&self, destination: Ipv6Address) -> Option<&[RelationHop]> { self.relations.iter().find_map(|r| { - if r.destination == destination { - Some(r.next_hop) + if r.destination() == destination { + match r { + Relation::Unicast(r) => Some(&r.next_hop[..]), + #[cfg(feature = "rpl-mop-3")] + Relation::Multicast(r) => Some(&r.next_hops), + } } else { None } @@ -61,8 +373,39 @@ impl Relations { } /// Purge expired relations. - pub fn purge(&mut self, now: Instant) { - self.relations.retain(|r| r.expiration > now) + /// + /// Returns `true` when a relation was actually removed. + pub fn flush(&mut self, now: Instant) -> bool { + let len = self.relations.len(); + self.relations.retain_mut(|r| { + // First flush all relations if it is a multicast relation + let has_expired = match r { + Relation::Unicast(rel) => rel.next_hop[0].has_expired(now), + #[cfg(feature = "rpl-mop-3")] + Relation::Multicast(rel) => { + rel.next_hops.retain(|hop| { + if hop.has_expired(now) { + net_trace!("Removing {} hop (expired)", hop); + false + } else { + true + } + }); + rel.next_hops.is_empty() + } + }; + + if has_expired { + net_trace!("Removing {} (destination)", r.destination()); + } + + !has_expired + }); + self.relations.len() != len + } + + pub fn iter(&self) -> impl Iterator { + self.relations.iter() } } @@ -86,7 +429,12 @@ mod tests { let addrs = addresses(2); let mut relations = Relations::default(); - relations.add_relation(addrs[0], addrs[1], Instant::now()); + relations.add_relation( + addrs[0], + &[addrs[1]], + Instant::now(), + Duration::from_secs(60 * 30), + ); assert_eq!(relations.relations.len(), 1); } @@ -98,7 +446,7 @@ mod tests { // The size of the buffer should still be RPL_RELATIONS_BUFFER_COUNT. let mut relations = Relations::default(); for a in addrs { - relations.add_relation(a, a, Instant::now()); + relations.add_relation(a, &[a], Instant::now(), Duration::from_secs(60 * 30)); } assert_eq!(relations.relations.len(), RPL_RELATIONS_BUFFER_COUNT); @@ -109,13 +457,26 @@ mod tests { let addrs = addresses(3); let mut relations = Relations::default(); - relations.add_relation(addrs[0], addrs[1], Instant::now()); + relations.add_relation( + addrs[0], + &[addrs[1]], + Instant::now(), + Duration::from_secs(60 * 30), + ); assert_eq!(relations.relations.len(), 1); - relations.add_relation(addrs[0], addrs[2], Instant::now()); + relations.add_relation( + addrs[0], + &[addrs[2]], + Instant::now(), + Duration::from_secs(60 * 30), + ); assert_eq!(relations.relations.len(), 1); - assert_eq!(relations.find_next_hop(addrs[0]), Some(addrs[2])); + assert_eq!( + relations.find_next_hop(addrs[0]).map(|hop| hop[0].ip), + Some(addrs[2]) + ); } #[test] @@ -123,16 +484,32 @@ mod tests { let addrs = addresses(3); let mut relations = Relations::default(); - relations.add_relation(addrs[0], addrs[1], Instant::now()); + relations.add_relation( + addrs[0], + &[addrs[1]], + Instant::now(), + Duration::from_secs(60 * 30), + ); assert_eq!(relations.relations.len(), 1); - assert_eq!(relations.find_next_hop(addrs[0]), Some(addrs[1])); + assert_eq!( + relations.find_next_hop(addrs[0]).map(|hop| hop[0].ip), + Some(addrs[1]) + ); - relations.add_relation(addrs[0], addrs[2], Instant::now()); + relations.add_relation( + addrs[0], + &[addrs[2]], + Instant::now(), + Duration::from_secs(60 * 30), + ); assert_eq!(relations.relations.len(), 1); - assert_eq!(relations.find_next_hop(addrs[0]), Some(addrs[2])); + assert_eq!( + relations.find_next_hop(addrs[0]).map(|hop| hop[0].ip), + Some(addrs[2]) + ); // Find the next hop of a destination not in the buffer. - assert_eq!(relations.find_next_hop(addrs[1]), None); + assert_eq!(relations.find_next_hop(addrs[1]).map(|hop| hop[0].ip), None); } #[test] @@ -140,10 +517,15 @@ mod tests { let addrs = addresses(2); let mut relations = Relations::default(); - relations.add_relation(addrs[0], addrs[1], Instant::now()); + relations.add_relation( + addrs[0], + &[addrs[1]], + Instant::now(), + Duration::from_secs(60 * 30), + ); assert_eq!(relations.relations.len(), 1); - relations.remove_relation(addrs[0]); + relations.remove_hop_from_relation(addrs[0], addrs[1]); assert!(relations.relations.is_empty()); } @@ -152,11 +534,16 @@ mod tests { let addrs = addresses(2); let mut relations = Relations::default(); - relations.add_relation(addrs[0], addrs[1], Instant::now() - Duration::from_secs(1)); + relations.add_relation( + addrs[0], + &[addrs[1]], + Instant::now() - Duration::from_secs(60 * 30 + 1), + Duration::from_secs(60 * 30), + ); assert_eq!(relations.relations.len(), 1); - relations.purge(Instant::now()); + relations.flush(Instant::now()); assert!(relations.relations.is_empty()); } } diff --git a/src/iface/rpl/trickle.rs b/src/iface/rpl/trickle.rs index a5b3b9793..37e97a755 100644 --- a/src/iface/rpl/trickle.rs +++ b/src/iface/rpl/trickle.rs @@ -14,12 +14,12 @@ use crate::{ time::{Duration, Instant}, }; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub(crate) struct TrickleTimer { - i_min: u32, - i_max: u32, - k: usize, +pub struct TrickleTimer { + pub(crate) i_min: u32, + pub(crate) i_max: u32, + pub(crate) k: usize, i: Duration, t: Duration, @@ -28,7 +28,7 @@ pub(crate) struct TrickleTimer { counter: usize, } -impl TrickleTimer { +impl Default for TrickleTimer { /// Creat a new Trickle timer using the default values. /// /// **NOTE**: the standard defines I as a random value between [Imin, Imax]. However, this @@ -39,7 +39,7 @@ impl TrickleTimer { /// don't use the default values from the standard, but the values from the _Enhanced Trickle /// Algorithm for Low-Power and Lossy Networks_ from Baraq Ghaleb et al. This is also what the /// Contiki Trickle timer does. - pub(crate) fn default(now: Instant, rand: &mut Rand) -> Self { + fn default() -> Self { use super::consts::{ DEFAULT_DIO_INTERVAL_DOUBLINGS, DEFAULT_DIO_INTERVAL_MIN, DEFAULT_DIO_REDUNDANCY_CONSTANT, @@ -49,13 +49,13 @@ impl TrickleTimer { DEFAULT_DIO_INTERVAL_MIN, DEFAULT_DIO_INTERVAL_MIN + DEFAULT_DIO_INTERVAL_DOUBLINGS, DEFAULT_DIO_REDUNDANCY_CONSTANT, - now, - rand, ) } +} +impl TrickleTimer { /// Create a new Trickle timer. - pub(crate) fn new(i_min: u32, i_max: u32, k: usize, now: Instant, rand: &mut Rand) -> Self { + pub(crate) fn new(i_min: u32, i_max: u32, k: usize) -> Self { let mut timer = Self { i_min, i_max, @@ -68,17 +68,19 @@ impl TrickleTimer { }; timer.i = Duration::from_millis(2u32.pow(timer.i_min) as u64); - timer.i_exp = now + timer.i; timer.counter = 0; - timer.set_t(now, rand); - timer } /// Poll the Trickle timer. Returns `true` when the Trickle timer signals that a message can be /// transmitted. This happens when the Trickle timer expires. pub(crate) fn poll(&mut self, now: Instant, rand: &mut Rand) -> bool { + if self.i_exp == Instant::ZERO { + self.i_exp = now + self.i; + self.set_t(now, rand); + } + let can_transmit = self.can_transmit() && self.t_expired(now); if can_transmit { @@ -98,9 +100,13 @@ impl TrickleTimer { self.t_exp.min(self.i_exp) } + pub(crate) fn next_expiration(&self) -> Instant { + self.t_exp + } + /// Signal the Trickle timer that a consistency has been heard, and thus increasing it's /// counter. - pub(crate) fn hear_consistent(&mut self) { + pub(crate) fn hear_consistency(&mut self) { self.counter += 1; } @@ -138,6 +144,18 @@ impl TrickleTimer { self.set_t(now, rand); } + pub(crate) fn interval_doublings(&self) -> u8 { + todo!(); + } + + pub(crate) fn interval_min(&self) -> u8 { + todo!(); + } + + pub(crate) fn redundancy_constant(&self) -> u8 { + todo!(); + } + pub(crate) const fn max_expiration(&self) -> Duration { Duration::from_millis(2u32.pow(self.i_max) as u64) } @@ -174,7 +192,7 @@ mod tests { fn trickle_timer_intervals() { let mut rand = Rand::new(1234); let mut now = Instant::ZERO; - let mut trickle = TrickleTimer::default(now, &mut rand); + let mut trickle = TrickleTimer::default(); let mut previous_i = trickle.i; @@ -199,11 +217,11 @@ mod tests { } #[test] + #[allow(clippy::field_reassign_with_default)] fn trickle_timer_hear_inconsistency() { let mut rand = Rand::new(1234); let mut now = Instant::ZERO; - let mut trickle = TrickleTimer::default(now, &mut rand); - + let mut trickle = TrickleTimer::default(); trickle.counter = 1; while now <= Instant::from_secs(10_000) { @@ -233,17 +251,17 @@ mod tests { } #[test] + #[allow(clippy::field_reassign_with_default)] fn trickle_timer_hear_consistency() { let mut rand = Rand::new(1234); let mut now = Instant::ZERO; - let mut trickle = TrickleTimer::default(now, &mut rand); - + let mut trickle = TrickleTimer::default(); trickle.counter = 1; let mut transmit_counter = 0; while now <= Instant::from_secs(10_000) { - trickle.hear_consistent(); + trickle.hear_consistency(); if trickle.poll(now, &mut rand) { transmit_counter += 1; diff --git a/src/lib.rs b/src/lib.rs index 040ff5749..e81643c6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,10 +142,13 @@ pub mod config { pub const IFACE_MAX_ROUTE_COUNT: usize = 4; pub const IFACE_MAX_SIXLOWPAN_ADDRESS_CONTEXT_COUNT: usize = 4; pub const IFACE_NEIGHBOR_CACHE_COUNT: usize = 3; + pub const IFACE_MAX_MULTICAST_DUPLICATION_COUNT: usize = 16; pub const REASSEMBLY_BUFFER_COUNT: usize = 4; 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 RPL_MAX_OPTIONS: usize = 2; + pub const RPL_MAX_NEXT_HOP_PER_DESTINATION: usize = 4; pub const IPV6_HBH_MAX_OPTIONS: usize = 2; } diff --git a/src/macros.rs b/src/macros.rs index e899d24ec..e31fd6708 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -167,3 +167,46 @@ macro_rules! set { NetworkEndian::write_u32(&mut $buffer.as_mut()[$field], $value); }}; } + +macro_rules! matches_cfg { + ([$($sel:tt)*] $expression:expr, $pattern:pat $(if $guard:expr)? $(,)?) => { + { + #[cfg($($sel)*)] + { + matches!($expression, $pattern $(if $guard)?) + } + #[cfg(not($($sel)*))] + { + false + } + } + }; +} + +macro_rules! cfg_match { + (($expression:expr) { + $($(cfg[$($sel:tt)*])? ($pattern:pat) => $arm_expr:block)* + }) => { + match $expression { + $( + $(#[cfg($($sel)*)])? + $pattern => $arm_expr, + )* + } + }; +} + +macro_rules! cfg_or { + ([$($sel:tt)*] $expression:expr, $or_other:expr) => { + { + #[cfg($($sel)*)] + { + $expression + } + #[cfg(not($($sel)*))] + { + $or_other + } + } + }; +} diff --git a/src/socket/icmp.rs b/src/socket/icmp.rs index 85a34b1f1..49eee8343 100644 --- a/src/socket/icmp.rs +++ b/src/socket/icmp.rs @@ -722,7 +722,7 @@ mod test_ipv4 { #[cfg(feature = "medium-ethernet")] fn test_send_dispatch(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(0), buffer(1)); let checksum = ChecksumCapabilities::default(); @@ -778,7 +778,7 @@ mod test_ipv4 { #[cfg(feature = "medium-ethernet")] fn test_set_hop_limit_v4(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut s = socket(buffer(0), buffer(1)); let checksum = ChecksumCapabilities::default(); @@ -816,7 +816,7 @@ mod test_ipv4 { #[cfg(feature = "medium-ethernet")] fn test_recv_process(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(1), buffer(1)); assert_eq!(socket.bind(Endpoint::Ident(0x1234)), Ok(())); @@ -847,7 +847,7 @@ mod test_ipv4 { #[cfg(feature = "medium-ethernet")] fn test_accept_bad_id(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(1), buffer(1)); assert_eq!(socket.bind(Endpoint::Ident(0x1234)), Ok(())); @@ -872,7 +872,7 @@ mod test_ipv4 { #[cfg(feature = "medium-ethernet")] fn test_accepts_udp(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(1), buffer(1)); assert_eq!(socket.bind(Endpoint::Udp(LOCAL_END_V4.into())), Ok(())); @@ -985,7 +985,7 @@ mod test_ipv6 { #[cfg(feature = "medium-ethernet")] fn test_send_dispatch(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(0), buffer(1)); let checksum = ChecksumCapabilities::default(); @@ -1016,7 +1016,7 @@ mod test_ipv6 { assert_eq!( socket.dispatch(cx, |_, (ip_repr, icmp_repr)| { assert_eq!(ip_repr, LOCAL_IPV6_REPR.into()); - assert_eq!(icmp_repr, ECHOV6_REPR.into()); + assert_eq!(icmp_repr, ECHOV6_REPR.clone().into()); Err(()) }), Err(()) @@ -1027,7 +1027,7 @@ mod test_ipv6 { assert_eq!( socket.dispatch(cx, |_, (ip_repr, icmp_repr)| { assert_eq!(ip_repr, LOCAL_IPV6_REPR.into()); - assert_eq!(icmp_repr, ECHOV6_REPR.into()); + assert_eq!(icmp_repr, ECHOV6_REPR.clone().into()); Ok::<_, ()>(()) }), Ok(()) @@ -1041,7 +1041,7 @@ mod test_ipv6 { #[cfg(feature = "medium-ethernet")] fn test_set_hop_limit(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut s = socket(buffer(0), buffer(1)); let checksum = ChecksumCapabilities::default(); @@ -1079,7 +1079,7 @@ mod test_ipv6 { #[cfg(feature = "medium-ethernet")] fn test_recv_process(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(1), buffer(1)); assert_eq!(socket.bind(Endpoint::Ident(0x1234)), Ok(())); @@ -1110,7 +1110,7 @@ mod test_ipv6 { #[cfg(feature = "medium-ethernet")] fn test_truncated_recv_slice(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(1), buffer(1)); assert_eq!(socket.bind(Endpoint::Ident(0x1234)), Ok(())); @@ -1141,7 +1141,7 @@ mod test_ipv6 { #[cfg(feature = "medium-ethernet")] fn test_accept_bad_id(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(1), buffer(1)); assert_eq!(socket.bind(Endpoint::Ident(0x1234)), Ok(())); @@ -1166,7 +1166,7 @@ mod test_ipv6 { #[cfg(feature = "medium-ethernet")] fn test_accepts_udp(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(1), buffer(1)); assert_eq!(socket.bind(Endpoint::Udp(LOCAL_END_V6.into())), Ok(())); diff --git a/src/socket/raw.rs b/src/socket/raw.rs index bb3a204ad..b9978f418 100644 --- a/src/socket/raw.rs +++ b/src/socket/raw.rs @@ -568,21 +568,18 @@ mod test { #[cfg(feature = "medium-ieee802154")] fn test_send_dispatch(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let mut cx = iface.context(); + let cx = iface.context_mut(); let mut socket = $socket(buffer(0), buffer(1)); assert!(socket.can_send()); - assert_eq!( - socket.dispatch(&mut cx, |_, _| unreachable!()), - Ok::<_, ()>(()) - ); + assert_eq!(socket.dispatch(cx, |_, _| unreachable!()), Ok::<_, ()>(())); assert_eq!(socket.send_slice(&$packet[..]), Ok(())); assert_eq!(socket.send_slice(b""), Err(SendError::BufferFull)); assert!(!socket.can_send()); assert_eq!( - socket.dispatch(&mut cx, |_, (ip_repr, ip_payload)| { + socket.dispatch(cx, |_, (ip_repr, ip_payload)| { assert_eq!(ip_repr, $hdr); assert_eq!(ip_payload, &$payload); Err(()) @@ -592,7 +589,7 @@ mod test { assert!(!socket.can_send()); assert_eq!( - socket.dispatch(&mut cx, |_, (ip_repr, ip_payload)| { + socket.dispatch(cx, |_, (ip_repr, ip_payload)| { assert_eq!(ip_repr, $hdr); assert_eq!(ip_payload, &$payload); Ok::<_, ()>(()) @@ -611,11 +608,11 @@ mod test { #[cfg(feature = "medium-ieee802154")] fn test_recv_truncated_slice(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let mut cx = iface.context(); + let cx = iface.context_mut(); let mut socket = $socket(buffer(1), buffer(0)); assert!(socket.accepts(&$hdr)); - socket.process(&mut cx, &$hdr, &$payload); + socket.process(cx, &$hdr, &$payload); let mut slice = [0; 4]; assert_eq!(socket.recv_slice(&mut slice[..]), Err(RecvError::Truncated)); @@ -630,14 +627,14 @@ mod test { #[cfg(feature = "medium-ieee802154")] fn test_recv_truncated_packet(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let mut cx = iface.context(); + let cx = iface.context_mut(); let mut socket = $socket(buffer(1), buffer(0)); let mut buffer = vec![0; 128]; buffer[..$packet.len()].copy_from_slice(&$packet[..]); assert!(socket.accepts(&$hdr)); - socket.process(&mut cx, &$hdr, &buffer); + socket.process(cx, &$hdr, &buffer); } #[rstest] @@ -649,11 +646,11 @@ mod test { #[cfg(feature = "medium-ieee802154")] fn test_peek_truncated_slice(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let mut cx = iface.context(); + let cx = iface.context_mut(); let mut socket = $socket(buffer(1), buffer(0)); assert!(socket.accepts(&$hdr)); - socket.process(&mut cx, &$hdr, &$payload); + socket.process(cx, &$hdr, &$payload); let mut slice = [0; 4]; assert_eq!(socket.peek_slice(&mut slice[..]), Err(RecvError::Truncated)); @@ -692,7 +689,7 @@ mod test { #[cfg(feature = "proto-ipv4")] { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = ipv4_locals::socket(buffer(0), buffer(2)); let mut wrong_version = ipv4_locals::PACKET_BYTES; @@ -710,7 +707,7 @@ mod test { #[cfg(feature = "proto-ipv6")] { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = ipv6_locals::socket(buffer(0), buffer(2)); let mut wrong_version = ipv6_locals::PACKET_BYTES; @@ -738,7 +735,7 @@ mod test { #[cfg(feature = "proto-ipv4")] { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = ipv4_locals::socket(buffer(1), buffer(0)); assert!(!socket.can_recv()); @@ -758,7 +755,7 @@ mod test { #[cfg(feature = "proto-ipv6")] { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = ipv6_locals::socket(buffer(1), buffer(0)); assert!(!socket.can_recv()); @@ -784,7 +781,7 @@ mod test { #[cfg(feature = "proto-ipv4")] { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = ipv4_locals::socket(buffer(1), buffer(0)); let mut cksumd_packet = ipv4_locals::PACKET_BYTES; @@ -803,7 +800,7 @@ mod test { #[cfg(feature = "proto-ipv6")] { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = ipv6_locals::socket(buffer(1), buffer(0)); assert_eq!(socket.peek(), Err(RecvError::Exhausted)); diff --git a/src/socket/tcp.rs b/src/socket/tcp.rs index 75b47d065..469e75030 100644 --- a/src/socket/tcp.rs +++ b/src/socket/tcp.rs @@ -873,7 +873,7 @@ impl<'a> Socket<'a> { /// # let mut iface: Interface = todo!(); /// # /// socket.connect( - /// iface.context(), + /// iface.context_mut(), /// (IpAddress::v4(10, 0, 0, 1), 80), /// get_ephemeral_port() /// ).unwrap(); diff --git a/src/socket/udp.rs b/src/socket/udp.rs index 82eebf26d..9ed7b7e86 100644 --- a/src/socket/udp.rs +++ b/src/socket/udp.rs @@ -733,7 +733,7 @@ mod test { #[cfg(feature = "medium-ieee802154")] fn test_send_dispatch(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(0), buffer(1)); assert_eq!(socket.bind(LOCAL_END), Ok(())); @@ -783,7 +783,7 @@ mod test { #[cfg(feature = "medium-ieee802154")] fn test_recv_process(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(1), buffer(0)); @@ -824,7 +824,7 @@ mod test { #[cfg(feature = "medium-ieee802154")] fn test_peek_process(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(1), buffer(0)); @@ -853,7 +853,7 @@ mod test { #[cfg(feature = "medium-ieee802154")] fn test_recv_truncated_slice(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(1), buffer(0)); @@ -881,7 +881,7 @@ mod test { #[cfg(feature = "medium-ieee802154")] fn test_peek_truncated_slice(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(1), buffer(0)); @@ -910,7 +910,7 @@ mod test { #[cfg(feature = "medium-ieee802154")] fn test_set_hop_limit(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut s = socket(buffer(0), buffer(1)); @@ -945,7 +945,7 @@ mod test { #[cfg(feature = "medium-ieee802154")] fn test_doesnt_accept_wrong_port(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut socket = socket(buffer(1), buffer(0)); @@ -966,7 +966,7 @@ mod test { #[cfg(feature = "medium-ieee802154")] fn test_doesnt_accept_wrong_ip(#[case] medium: Medium) { let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); let mut port_bound_socket = socket(buffer(1), buffer(0)); assert_eq!(port_bound_socket.bind(LOCAL_PORT), Ok(())); @@ -1004,7 +1004,7 @@ mod test { let mut socket = socket(recv_buffer, buffer(0)); let (mut iface, _, _) = setup(medium); - let cx = iface.context(); + let cx = iface.context_mut(); assert_eq!(socket.bind(LOCAL_PORT), Ok(())); diff --git a/src/storage/packet_buffer.rs b/src/storage/packet_buffer.rs index 28119fa10..de255a271 100644 --- a/src/storage/packet_buffer.rs +++ b/src/storage/packet_buffer.rs @@ -229,6 +229,19 @@ impl<'a, H> PacketBuffer<'a, H> { } } + pub fn peek_mut(&mut self) -> Result<(&mut H, &mut [u8]), Empty> { + self.dequeue_padding(); + + if let Some(metadata) = self.metadata_ring.get_allocated_mut(0, 1).first_mut() { + Ok(( + metadata.header.as_mut().unwrap(), + self.payload_ring.get_allocated_mut(0, metadata.size), + )) + } else { + Err(Empty) + } + } + /// Return the maximum number packets that can be stored. pub fn packet_capacity(&self) -> usize { self.metadata_ring.capacity() diff --git a/src/storage/ring_buffer.rs b/src/storage/ring_buffer.rs index 7d461b68c..8951058e8 100644 --- a/src/storage/ring_buffer.rs +++ b/src/storage/ring_buffer.rs @@ -369,6 +369,29 @@ impl<'a, T: 'a> RingBuffer<'a, T> { &self.storage[start_at..start_at + size] } + /// Return the largest contiguous slice of allocated buffer elements starting + /// at the given offset past the first allocated element, and up to the given size. + #[must_use] + pub fn get_allocated_mut(&mut self, offset: usize, mut size: usize) -> &mut [T] { + let start_at = self.get_idx(offset); + // We can't read past the end of the allocated data. + if offset > self.length { + return &mut []; + } + // We can't read more than we have allocated. + let clamped_length = self.length - offset; + if size > clamped_length { + size = clamped_length + } + // We can't contiguously dequeue past the end of the storage. + let until_end = self.capacity() - start_at; + if size > until_end { + size = until_end + } + + &mut self.storage[start_at..start_at + size] + } + /// Read as many elements from allocated buffer elements into the given slice /// starting at the given offset past the first allocated element, and return /// the amount read. diff --git a/src/tests.rs b/src/tests.rs index ec026ab64..1e3ade402 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,7 +1,7 @@ use crate::iface::*; use crate::wire::*; -pub(crate) fn setup<'a>(medium: Medium) -> (Interface, SocketSet<'a>, TestingDevice) { +pub(crate) fn setup<'a>(medium: Medium) -> (Interface<'static>, SocketSet<'a>, TestingDevice) { let mut device = TestingDevice::new(medium); let config = Config::new(match medium { @@ -17,7 +17,33 @@ pub(crate) fn setup<'a>(medium: Medium) -> (Interface, SocketSet<'a>, TestingDev ])), }); - let mut iface = Interface::new(config, &mut device, Instant::ZERO); + #[cfg(feature = "proto-rpl")] + let config = Config { + rpl_config: Some(RplConfig::new( + RplModeOfOperation::NoDownwardRoutesMaintained, + )), + ..config + }; + + #[cfg(feature = "rpl-mop-1")] + let config = Config { + rpl_config: Some(RplConfig::new(RplModeOfOperation::NonStoringMode)), + ..config + }; + + #[cfg(feature = "rpl-mop-2")] + let config = Config { + rpl_config: Some(RplConfig::new(RplModeOfOperation::StoringMode)), + ..config + }; + + #[cfg(feature = "rpl-mop-3")] + let config = Config { + rpl_config: Some(RplConfig::new(RplModeOfOperation::StoringModeWithMulticast)), + ..config + }; + + let mut iface = Interface::new(config, &mut device, &mut [][..], &mut [][..], Instant::ZERO); #[cfg(feature = "proto-ipv4")] { diff --git a/src/wire/icmpv6.rs b/src/wire/icmpv6.rs index 2a9e79d45..2a1381f69 100644 --- a/src/wire/icmpv6.rs +++ b/src/wire/icmpv6.rs @@ -568,7 +568,7 @@ impl> AsRef<[u8]> for Packet { } /// A high-level representation of an Internet Control Message Protocol version 6 packet header. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[non_exhaustive] pub enum Repr<'a> { diff --git a/src/wire/ipv6hbh.rs b/src/wire/ipv6hbh.rs index 9fa33a3af..895c67e4f 100644 --- a/src/wire/ipv6hbh.rs +++ b/src/wire/ipv6hbh.rs @@ -66,7 +66,7 @@ pub struct Repr<'a> { impl<'a> Repr<'a> { /// Parse an IPv6 Hop-by-Hop Header and return a high-level representation. - pub fn parse(header: &'a Header<&'a T>) -> Result> + pub fn parse(header: &Header<&'a T>) -> Result> where T: AsRef<[u8]> + ?Sized, { diff --git a/src/wire/ipv6routing.rs b/src/wire/ipv6routing.rs index a2b91e9ce..7ab81981b 100644 --- a/src/wire/ipv6routing.rs +++ b/src/wire/ipv6routing.rs @@ -321,14 +321,10 @@ impl + AsMut<[u8]>> Header { data[field::PAD] = value << 4; } - /// Set address data - /// - /// # Panics - /// This function may panic if this header is not the RPL Source Routing Header routing type. - pub fn set_addresses(&mut self, value: &[u8]) { + /// Return a pointer to the addresses buffer. + pub fn addresses_mut(&mut self) -> &mut [u8] { let data = self.buffer.as_mut(); - let addresses = &mut data[field::ADDRESSES..]; - addresses.copy_from_slice(value); + &mut data[field::ADDRESSES..] } } @@ -345,34 +341,39 @@ impl<'a, T: AsRef<[u8]> + ?Sized> fmt::Display for Header<&'a T> { } /// A high-level representation of an IPv6 Routing Header. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[non_exhaustive] -pub enum Repr<'a> { +#[allow(clippy::large_enum_variant)] +pub enum Repr { Type2 { /// Number of route segments remaining. segments_left: u8, /// The home address of the destination mobile node. home_address: Address, }, - Rpl { - /// Number of route segments remaining. - segments_left: u8, - /// Number of prefix octets from each segment, except the last segment, that are elided. - cmpr_i: u8, - /// Number of prefix octets from the last segment that are elided. - cmpr_e: u8, - /// Number of octets that are used for padding after `address[n]` at the end of the - /// RPL Source Route Header. - pad: u8, - /// Vector of addresses, numbered 1 to `n`. - addresses: &'a [u8], - }, + Rpl(SourceRoutingRepr), } -impl<'a> Repr<'a> { +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SourceRoutingRepr { + /// Number of route segments remaining. + pub segments_left: u8, + /// Number of prefix octets from each segment, except the last segment, that are elided. + pub cmpr_i: u8, + /// Number of prefix octets from the last segment that are elided. + pub cmpr_e: u8, + /// Number of octets that are used for padding after `address[n]` at the end of the + /// RPL Source Route Header. + pub pad: u8, + /// Vector of addresses, numbered 1 to `n`. + pub addresses: heapless::Vec, +} + +impl Repr { /// Parse an IPv6 Routing Header and return a high-level representation. - pub fn parse(header: &'a Header<&'a T>) -> Result> + pub fn parse(header: &Header<&T>) -> Result where T: AsRef<[u8]> + ?Sized, { @@ -382,13 +383,40 @@ impl<'a> Repr<'a> { segments_left: header.segments_left(), home_address: header.home_address(), }), - Type::Rpl => Ok(Repr::Rpl { - segments_left: header.segments_left(), - cmpr_i: header.cmpr_i(), - cmpr_e: header.cmpr_e(), - pad: header.pad(), - addresses: header.addresses(), - }), + Type::Rpl => { + let mut addresses = heapless::Vec::new(); + + let cmpr_e = header.cmpr_e(); + let cmp_i = header.cmpr_i(); + let pad = header.pad(); + + let mut addr_iterator = header.addresses() + [..header.addresses().len() - pad as usize] + .chunks_exact(16 - cmpr_e as usize); + + for addr_raw in addr_iterator.by_ref() { + let mut buffer = [0u8; 16]; + buffer[cmpr_e as usize..].copy_from_slice(addr_raw); + addresses.push(Address::from_bytes(&buffer)).unwrap(); + } + + let last_addr = addr_iterator.remainder(); + + if !last_addr.is_empty() { + let mut buffer = [0u8; 16]; + buffer[cmp_i as usize..] + .copy_from_slice(&last_addr[..last_addr.len() - pad as usize]); + addresses.push(Address::from_bytes(&buffer)).unwrap(); + } + + Ok(Repr::Rpl(SourceRoutingRepr { + segments_left: header.segments_left(), + cmpr_i: header.cmpr_i(), + cmpr_e: header.cmpr_e(), + pad: header.pad(), + addresses, + })) + } _ => Err(Error), } @@ -396,11 +424,34 @@ impl<'a> Repr<'a> { /// Return the length, in bytes, of a header that will be emitted from this high-level /// representation. - pub const fn buffer_len(&self) -> usize { + pub fn buffer_len(&self) -> usize { match self { // Routing Type + Segments Left + Reserved + Home Address Repr::Type2 { home_address, .. } => 2 + 4 + home_address.as_bytes().len(), - Repr::Rpl { addresses, .. } => 2 + 4 + addresses.len(), + Repr::Rpl(SourceRoutingRepr { addresses, .. }) => { + // Compute the length of the common prefix for every address on the route. + let mut common_prefix = 0; + + if addresses.len() > 1 { + 'outer: for i in 0..16 { + for addr in addresses.iter() { + if addr.as_bytes()[i] != addresses[0].as_bytes()[i] { + break 'outer; + } + } + common_prefix += 1; + } + } + + let mut len = 2 + 4 + addresses.len() * 16 - common_prefix * addresses.len(); + + // Add the padding: + if (len + 2) % 8 != 0 { + len += 8 - ((len + 2) % 8); + } + + len + } } } @@ -416,26 +467,55 @@ impl<'a> Repr<'a> { header.clear_reserved(); header.set_home_address(home_address); } - Repr::Rpl { + Repr::Rpl(SourceRoutingRepr { segments_left, - cmpr_i, - cmpr_e, - pad, - addresses, - } => { + ref addresses, + .. + }) => { header.set_routing_type(Type::Rpl); header.set_segments_left(segments_left); - header.set_cmpr_i(cmpr_i); - header.set_cmpr_e(cmpr_e); - header.set_pad(pad); header.clear_reserved(); - header.set_addresses(addresses); + + // Compute the length of the common prefix for every address on the route. + let mut common_prefix = 0; + + if addresses.len() > 1 { + 'outer: for i in 0..16 { + for addr in addresses.iter() { + if addr.as_bytes()[i] != addresses[0].as_bytes()[i] { + break 'outer; + } + } + common_prefix += 1; + } + } + + // Calculate the padding for the last address: + let len = 2 + 4 + addresses.len() * 16 - common_prefix * addresses.len(); + let pad = if (len + 2) % 8 != 0 { + 8 - (len + 2) % 8 + } else { + 0 + }; + + header.set_cmpr_i(common_prefix as u8); + header.set_cmpr_e(common_prefix as u8); + header.set_pad(pad as u8); + + let mut addrs_buf = header.addresses_mut(); + + for addr in addresses { + addrs_buf[..16 - common_prefix] + .copy_from_slice(&addr.as_bytes()[common_prefix..]); + addrs_buf = &mut addrs_buf[16 - common_prefix..]; + } + addrs_buf.fill(0); } } } } -impl<'a> fmt::Display for Repr<'a> { +impl fmt::Display for Repr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Repr::Type2 { @@ -450,13 +530,13 @@ impl<'a> fmt::Display for Repr<'a> { home_address ) } - Repr::Rpl { + Repr::Rpl(SourceRoutingRepr { segments_left, cmpr_i, cmpr_e, pad, .. - } => { + }) => { write!( f, "IPv6 Routing type={} seg_left={} cmpr_i={} cmpr_e={} pad={}", @@ -487,38 +567,51 @@ mod test { home_address: Address::LOOPBACK, }; - // A Source Routing Header with full IPv6 addresses in bytes - static BYTES_SRH_FULL: [u8; 38] = [ - 0x3, 0x2, 0x0, 0x0, 0x0, 0x0, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x2, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x3, 0x1, + // A Source Routing Header with elided IPv6 addresses in bytes + static BYTES_SRH_ELIDED: [u8; 54] = [ + 0x03, 0x06, 0x99, 0x60, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x04, 0x00, + 0x04, 0x00, 0x04, 0x00, 0x04, 0x05, 0x00, 0x05, 0x00, 0x05, 0x00, 0x05, 0x06, 0x00, 0x06, + 0x00, 0x06, 0x00, 0x06, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x08, 0x00, 0x08, 0x00, + 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; - // A representation of a Source Routing Header with full IPv6 addresses - static REPR_SRH_FULL: Repr = Repr::Rpl { - segments_left: 2, - cmpr_i: 0, - cmpr_e: 0, - pad: 0, - addresses: &[ - 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xfd, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x1, - ], - }; + // A representation of a Source Routing Header with elided IPv6 addresses + fn repr_srh_elided() -> Repr { + Repr::Rpl(SourceRoutingRepr { + segments_left: 6, + cmpr_i: 9, + cmpr_e: 9, + pad: 6, + addresses: heapless::Vec::from_slice(&[ + Address::new(0, 0, 0, 0, 3, 3, 3, 3), + Address::new(0, 0, 0, 0, 4, 4, 4, 4), + Address::new(0, 0, 0, 0, 5, 5, 5, 5), + Address::new(0, 0, 0, 0, 6, 6, 6, 6), + Address::new(0, 0, 0, 0, 7, 7, 7, 7), + Address::new(0, 0, 0, 0, 8, 8, 8, 8), + ]) + .unwrap(), + }) + } - // A Source Routing Header with elided IPv6 addresses in bytes - static BYTES_SRH_ELIDED: [u8; 14] = [ - 0x3, 0x2, 0xfe, 0x50, 0x0, 0x0, 0x2, 0x3, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + static BYTES_SRH_VERY_ELIDED: [u8; 14] = [ + 0x03, 0x02, 0xff, 0x60, 0x00, 0x00, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; // A representation of a Source Routing Header with elided IPv6 addresses - static REPR_SRH_ELIDED: Repr = Repr::Rpl { - segments_left: 2, - cmpr_i: 15, - cmpr_e: 14, - pad: 5, - addresses: &[0x2, 0x3, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0], - }; + fn repr_srh_very_elided() -> Repr { + Repr::Rpl(SourceRoutingRepr { + segments_left: 2, + cmpr_i: 15, + cmpr_e: 15, + pad: 6, + addresses: heapless::Vec::from_slice(&[ + Address::new(0, 0, 0, 0, 0, 0, 0, 3), + Address::new(0, 0, 0, 0, 0, 0, 0, 4), + ]) + .unwrap(), + }) + } #[test] fn test_check_len() { @@ -527,24 +620,18 @@ mod test { Err(Error), Header::new_unchecked(&BYTES_TYPE2[..3]).check_len() ); - assert_eq!( - Err(Error), - Header::new_unchecked(&BYTES_SRH_FULL[..3]).check_len() - ); assert_eq!( Err(Error), Header::new_unchecked(&BYTES_SRH_ELIDED[..3]).check_len() ); // valid - assert_eq!(Ok(()), Header::new_unchecked(&BYTES_TYPE2[..]).check_len()); - assert_eq!( - Ok(()), - Header::new_unchecked(&BYTES_SRH_FULL[..]).check_len() - ); - assert_eq!( - Ok(()), - Header::new_unchecked(&BYTES_SRH_ELIDED[..]).check_len() - ); + assert!(Header::new_unchecked(&BYTES_TYPE2[..]).check_len().is_ok()); + assert!(Header::new_unchecked(&BYTES_SRH_ELIDED[..]) + .check_len() + .is_ok()); + assert!(Header::new_unchecked(&BYTES_SRH_VERY_ELIDED[..]) + .check_len() + .is_ok()); } #[test] @@ -554,15 +641,15 @@ mod test { assert_eq!(header.segments_left(), 1); assert_eq!(header.home_address(), Address::LOOPBACK); - let header = Header::new_unchecked(&BYTES_SRH_FULL[..]); + let header = Header::new_unchecked(&BYTES_SRH_ELIDED[..]); assert_eq!(header.routing_type(), Type::Rpl); - assert_eq!(header.segments_left(), 2); - assert_eq!(header.addresses(), &BYTES_SRH_FULL[6..]); + assert_eq!(header.segments_left(), 6); + assert_eq!(header.addresses(), &BYTES_SRH_ELIDED[6..]); - let header = Header::new_unchecked(&BYTES_SRH_ELIDED[..]); + let header = Header::new_unchecked(&BYTES_SRH_VERY_ELIDED[..]); assert_eq!(header.routing_type(), Type::Rpl); assert_eq!(header.segments_left(), 2); - assert_eq!(header.addresses(), &BYTES_SRH_ELIDED[6..]); + assert_eq!(header.addresses(), &BYTES_SRH_VERY_ELIDED[6..]); } #[test] @@ -571,13 +658,13 @@ mod test { let repr = Repr::parse(&header).unwrap(); assert_eq!(repr, REPR_TYPE2); - let header = Header::new_checked(&BYTES_SRH_FULL[..]).unwrap(); + let header = Header::new_checked(&BYTES_SRH_ELIDED[..]).unwrap(); let repr = Repr::parse(&header).unwrap(); - assert_eq!(repr, REPR_SRH_FULL); + assert_eq!(repr, repr_srh_elided()); - let header = Header::new_checked(&BYTES_SRH_ELIDED[..]).unwrap(); + let header = Header::new_checked(&BYTES_SRH_VERY_ELIDED[..]).unwrap(); let repr = Repr::parse(&header).unwrap(); - assert_eq!(repr, REPR_SRH_ELIDED); + assert_eq!(repr, repr_srh_very_elided()); } #[test] @@ -587,21 +674,21 @@ mod test { REPR_TYPE2.emit(&mut header); assert_eq!(header.into_inner(), &BYTES_TYPE2[..]); - let mut bytes = [0xFFu8; 38]; + let mut bytes = [0xFFu8; 54]; let mut header = Header::new_unchecked(&mut bytes[..]); - REPR_SRH_FULL.emit(&mut header); - assert_eq!(header.into_inner(), &BYTES_SRH_FULL[..]); + repr_srh_elided().emit(&mut header); + assert_eq!(header.into_inner(), &BYTES_SRH_ELIDED[..]); - let mut bytes = [0xFFu8; 14]; + let mut bytes = [0u8; 14]; let mut header = Header::new_unchecked(&mut bytes[..]); - REPR_SRH_ELIDED.emit(&mut header); - assert_eq!(header.into_inner(), &BYTES_SRH_ELIDED[..]); + repr_srh_very_elided().emit(&mut header); + assert_eq!(header.into_inner(), &BYTES_SRH_VERY_ELIDED[..]); } #[test] fn test_buffer_len() { assert_eq!(REPR_TYPE2.buffer_len(), 22); - assert_eq!(REPR_SRH_FULL.buffer_len(), 38); - assert_eq!(REPR_SRH_ELIDED.buffer_len(), 14); + assert_eq!(repr_srh_elided().buffer_len(), 54); + assert_eq!(repr_srh_very_elided().buffer_len(), 14); } } diff --git a/src/wire/mod.rs b/src/wire/mod.rs index e73e2305f..410c5fe70 100644 --- a/src/wire/mod.rs +++ b/src/wire/mod.rs @@ -123,7 +123,7 @@ mod ndisc; ))] mod ndiscoption; #[cfg(feature = "proto-rpl")] -mod rpl; +pub(crate) mod rpl; #[cfg(all(feature = "proto-sixlowpan", feature = "medium-ieee802154"))] mod sixlowpan; mod tcp; @@ -154,9 +154,18 @@ pub use self::arp::{ #[cfg(feature = "proto-rpl")] pub use self::rpl::{ - data::HopByHopOption as RplHopByHopRepr, data::Packet as RplHopByHopPacket, - options::Packet as RplOptionPacket, options::Repr as RplOptionRepr, - InstanceId as RplInstanceId, Repr as RplRepr, + hbh::HopByHopOption as RplHopByHopRepr, + hbh::Packet as RplHopByHopPacket, + options::{ + DodagConfiguration as RplDodagConfiguration, Packet as RplOptionPacket, + PrefixInformation as RplPrefixInformation, Repr as RplOptionRepr, + RouteInformation as RplRouteInformation, RplTarget, + SolicitedInformation as RplSolicitedInformation, + TransitInformation as RplTransitInformation, + }, + DestinationAdvertisementObject as RplDao, DestinationAdvertisementObjectAck as RplDaoAck, + DodagInformationObject as RplDio, DodagInformationSolicitation as RplDis, + InstanceId as RplInstanceId, Repr as RplRepr, SequenceCounter as RplSequenceCounter, }; #[cfg(all(feature = "proto-sixlowpan", feature = "medium-ieee802154"))] @@ -215,7 +224,8 @@ pub use self::ipv6hbh::{Header as Ipv6HopByHopHeader, Repr as Ipv6HopByHopRepr}; #[cfg(feature = "proto-ipv6")] pub use self::ipv6routing::{ - Header as Ipv6RoutingHeader, Repr as Ipv6RoutingRepr, Type as Ipv6RoutingType, + Header as Ipv6RoutingHeader, Repr as Ipv6RoutingRepr, + SourceRoutingRepr as Ipv6SourceRoutingRepr, Type as Ipv6RoutingType, }; #[cfg(feature = "proto-ipv4")] diff --git a/src/wire/rpl.rs b/src/wire/rpl.rs deleted file mode 100644 index a8b6fb582..000000000 --- a/src/wire/rpl.rs +++ /dev/null @@ -1,2723 +0,0 @@ -//! Implementation of the RPL packet formats. See [RFC 6550 § 6]. -//! -//! [RFC 6550 § 6]: https://datatracker.ietf.org/doc/html/rfc6550#section-6 - -use byteorder::{ByteOrder, NetworkEndian}; - -use super::{Error, Result}; -use crate::wire::icmpv6::Packet; -use crate::wire::ipv6::Address; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[repr(u8)] -pub enum InstanceId { - Global(u8), - Local(u8), -} - -impl From for InstanceId { - fn from(val: u8) -> Self { - const MASK: u8 = 0b0111_1111; - - if ((val >> 7) & 0xb1) == 0b0 { - Self::Global(val & MASK) - } else { - Self::Local(val & MASK) - } - } -} - -impl From for u8 { - fn from(val: InstanceId) -> Self { - match val { - InstanceId::Global(val) => 0b0000_0000 | val, - InstanceId::Local(val) => 0b1000_0000 | val, - } - } -} - -impl InstanceId { - /// Return the real part of the ID. - pub fn id(&self) -> u8 { - match self { - Self::Global(val) => *val, - Self::Local(val) => *val, - } - } - - /// Returns `true` when the DODAG ID is the destination address of the IPv6 packet. - #[inline] - pub fn dodag_is_destination(&self) -> bool { - match self { - Self::Global(_) => false, - Self::Local(val) => ((val >> 6) & 0b1) == 0b1, - } - } - - /// Returns `true` when the DODAG ID is the source address of the IPv6 packet. - /// - /// *NOTE*: this only makes sense when using a local RPL Instance ID and the packet is not a - /// RPL control message. - #[inline] - pub fn dodag_is_source(&self) -> bool { - !self.dodag_is_destination() - } -} - -mod field { - use crate::wire::field::*; - - pub const RPL_INSTANCE_ID: usize = 4; - - // DODAG information solicitation fields (DIS) - pub const DIS_FLAGS: usize = 4; - pub const DIS_RESERVED: usize = 5; - - // DODAG information object fields (DIO) - pub const DIO_VERSION_NUMBER: usize = 5; - pub const DIO_RANK: Field = 6..8; - pub const DIO_GROUNDED: usize = 8; - pub const DIO_MOP: usize = 8; - pub const DIO_PRF: usize = 8; - pub const DIO_DTSN: usize = 9; - //pub const DIO_FLAGS: usize = 10; - //pub const DIO_RESERVED: usize = 11; - pub const DIO_DODAG_ID: Field = 12..12 + 16; - - // Destination advertisement object (DAO) - pub const DAO_K: usize = 5; - pub const DAO_D: usize = 5; - //pub const DAO_FLAGS: usize = 5; - //pub const DAO_RESERVED: usize = 6; - pub const DAO_SEQUENCE: usize = 7; - pub const DAO_DODAG_ID: Field = 8..8 + 16; - - // Destination advertisement object ack (DAO-ACK) - pub const DAO_ACK_D: usize = 5; - //pub const DAO_ACK_RESERVED: usize = 5; - pub const DAO_ACK_SEQUENCE: usize = 6; - pub const DAO_ACK_STATUS: usize = 7; - pub const DAO_ACK_DODAG_ID: Field = 8..8 + 16; -} - -enum_with_unknown! { - /// RPL Control Message subtypes. - pub enum RplControlMessage(u8) { - DodagInformationSolicitation = 0x00, - DodagInformationObject = 0x01, - DestinationAdvertisementObject = 0x02, - DestinationAdvertisementObjectAck = 0x03, - SecureDodagInformationSolicitation = 0x80, - SecureDodagInformationObject = 0x81, - SecureDestinationAdvertisementObject = 0x82, - SecureDestinationAdvertisementObjectAck = 0x83, - ConsistencyCheck = 0x8a, - } -} - -impl core::fmt::Display for RplControlMessage { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - RplControlMessage::DodagInformationSolicitation => { - write!(f, "DODAG information solicitation (DIS)") - } - RplControlMessage::DodagInformationObject => { - write!(f, "DODAG information object (DIO)") - } - RplControlMessage::DestinationAdvertisementObject => { - write!(f, "destination advertisement object (DAO)") - } - RplControlMessage::DestinationAdvertisementObjectAck => write!( - f, - "destination advertisement object acknowledgement (DAO-ACK)" - ), - RplControlMessage::SecureDodagInformationSolicitation => { - write!(f, "secure DODAG information solicitation (DIS)") - } - RplControlMessage::SecureDodagInformationObject => { - write!(f, "secure DODAG information object (DIO)") - } - RplControlMessage::SecureDestinationAdvertisementObject => { - write!(f, "secure destination advertisement object (DAO)") - } - RplControlMessage::SecureDestinationAdvertisementObjectAck => write!( - f, - "secure destination advertisement object acknowledgement (DAO-ACK)" - ), - RplControlMessage::ConsistencyCheck => write!(f, "consistency check (CC)"), - RplControlMessage::Unknown(id) => write!(f, "{}", id), - } - } -} - -impl> Packet { - /// Return the RPL instance ID. - #[inline] - pub fn rpl_instance_id(&self) -> InstanceId { - get!(self.buffer, into: InstanceId, field: field::RPL_INSTANCE_ID) - } -} - -impl<'p, T: AsRef<[u8]> + ?Sized> Packet<&'p T> { - /// Return a pointer to the options. - pub fn options(&self) -> Result<&'p [u8]> { - let len = self.buffer.as_ref().len(); - match RplControlMessage::from(self.msg_code()) { - RplControlMessage::DodagInformationSolicitation if len < field::DIS_RESERVED + 1 => { - return Err(Error) - } - RplControlMessage::DodagInformationObject if len < field::DIO_DODAG_ID.end => { - return Err(Error) - } - RplControlMessage::DestinationAdvertisementObject - if self.dao_dodag_id_present() && len < field::DAO_DODAG_ID.end => - { - return Err(Error) - } - RplControlMessage::DestinationAdvertisementObject if len < field::DAO_SEQUENCE + 1 => { - return Err(Error) - } - RplControlMessage::DestinationAdvertisementObjectAck - if self.dao_ack_dodag_id_present() && len < field::DAO_ACK_DODAG_ID.end => - { - return Err(Error) - } - RplControlMessage::DestinationAdvertisementObjectAck - if len < field::DAO_ACK_STATUS + 1 => - { - return Err(Error) - } - RplControlMessage::SecureDodagInformationSolicitation - | RplControlMessage::SecureDodagInformationObject - | RplControlMessage::SecureDestinationAdvertisementObject - | RplControlMessage::SecureDestinationAdvertisementObjectAck - | RplControlMessage::ConsistencyCheck => return Err(Error), - RplControlMessage::Unknown(_) => return Err(Error), - _ => {} - } - - let buffer = &self.buffer.as_ref(); - Ok(match RplControlMessage::from(self.msg_code()) { - RplControlMessage::DodagInformationSolicitation => &buffer[field::DIS_RESERVED + 1..], - RplControlMessage::DodagInformationObject => &buffer[field::DIO_DODAG_ID.end..], - RplControlMessage::DestinationAdvertisementObject if self.dao_dodag_id_present() => { - &buffer[field::DAO_DODAG_ID.end..] - } - RplControlMessage::DestinationAdvertisementObject => &buffer[field::DAO_SEQUENCE + 1..], - RplControlMessage::DestinationAdvertisementObjectAck - if self.dao_ack_dodag_id_present() => - { - &buffer[field::DAO_ACK_DODAG_ID.end..] - } - RplControlMessage::DestinationAdvertisementObjectAck => { - &buffer[field::DAO_ACK_STATUS + 1..] - } - RplControlMessage::SecureDodagInformationSolicitation - | RplControlMessage::SecureDodagInformationObject - | RplControlMessage::SecureDestinationAdvertisementObject - | RplControlMessage::SecureDestinationAdvertisementObjectAck - | RplControlMessage::ConsistencyCheck => unreachable!(), - RplControlMessage::Unknown(_) => unreachable!(), - }) - } -} - -impl + AsMut<[u8]>> Packet { - /// Set the RPL Instance ID field. - #[inline] - pub fn set_rpl_instance_id(&mut self, value: u8) { - set!(self.buffer, value, field: field::RPL_INSTANCE_ID) - } -} - -impl<'p, T: AsRef<[u8]> + AsMut<[u8]> + ?Sized> Packet<&'p mut T> { - /// Return a pointer to the options. - pub fn options_mut(&mut self) -> &mut [u8] { - match RplControlMessage::from(self.msg_code()) { - RplControlMessage::DodagInformationSolicitation => { - &mut self.buffer.as_mut()[field::DIS_RESERVED + 1..] - } - RplControlMessage::DodagInformationObject => { - &mut self.buffer.as_mut()[field::DIO_DODAG_ID.end..] - } - RplControlMessage::DestinationAdvertisementObject => { - if self.dao_dodag_id_present() { - &mut self.buffer.as_mut()[field::DAO_DODAG_ID.end..] - } else { - &mut self.buffer.as_mut()[field::DAO_SEQUENCE + 1..] - } - } - RplControlMessage::DestinationAdvertisementObjectAck => { - if self.dao_ack_dodag_id_present() { - &mut self.buffer.as_mut()[field::DAO_ACK_DODAG_ID.end..] - } else { - &mut self.buffer.as_mut()[field::DAO_ACK_STATUS + 1..] - } - } - RplControlMessage::SecureDodagInformationSolicitation - | RplControlMessage::SecureDodagInformationObject - | RplControlMessage::SecureDestinationAdvertisementObject - | RplControlMessage::SecureDestinationAdvertisementObjectAck - | RplControlMessage::ConsistencyCheck => todo!("Secure messages not supported"), - RplControlMessage::Unknown(_) => todo!(), - } - } -} - -/// Getters for the DODAG information solicitation (DIS) message. -/// -/// ```txt -/// 0 1 2 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Flags | Reserved | Option(s)... -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// ``` -impl> Packet { - /// Return the DIS flags field. - #[inline] - pub fn dis_flags(&self) -> u8 { - get!(self.buffer, field: field::DIS_FLAGS) - } - - /// Return the DIS reserved field. - #[inline] - pub fn dis_reserved(&self) -> u8 { - get!(self.buffer, field: field::DIS_RESERVED) - } -} - -/// Setters for the DODAG information solicitation (DIS) message. -impl + AsMut<[u8]>> Packet { - /// Clear the DIS flags field. - pub fn clear_dis_flags(&mut self) { - self.buffer.as_mut()[field::DIS_FLAGS] = 0; - } - - /// Clear the DIS rserved field. - pub fn clear_dis_reserved(&mut self) { - self.buffer.as_mut()[field::DIS_RESERVED] = 0; - } -} - -enum_with_unknown! { - pub enum ModeOfOperation(u8) { - NoDownwardRoutesMaintained = 0x00, - NonStoringMode = 0x01, - StoringModeWithoutMulticast = 0x02, - StoringModeWithMulticast = 0x03, - } -} - -impl Default for ModeOfOperation { - fn default() -> Self { - Self::StoringModeWithoutMulticast - } -} - -/// Getters for the DODAG information object (DIO) message. -/// -/// ```txt -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | RPLInstanceID |Version Number | Rank | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// |G|0| MOP | Prf | DTSN | Flags | Reserved | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | | -/// + + -/// | | -/// + DODAGID + -/// | | -/// + + -/// | | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Option(s)... -/// +-+-+-+-+-+-+-+-+ -/// ``` -impl> Packet { - /// Return the Version Number field. - #[inline] - pub fn dio_version_number(&self) -> u8 { - get!(self.buffer, field: field::DIO_VERSION_NUMBER) - } - - /// Return the Rank field. - #[inline] - pub fn dio_rank(&self) -> u16 { - get!(self.buffer, u16, field: field::DIO_RANK) - } - - /// Return the value of the Grounded flag. - #[inline] - pub fn dio_grounded(&self) -> bool { - get!(self.buffer, bool, field: field::DIO_GROUNDED, shift: 7, mask: 0b01) - } - - /// Return the mode of operation field. - #[inline] - pub fn dio_mode_of_operation(&self) -> ModeOfOperation { - get!(self.buffer, into: ModeOfOperation, field: field::DIO_MOP, shift: 3, mask: 0b111) - } - - /// Return the DODAG preference field. - #[inline] - pub fn dio_dodag_preference(&self) -> u8 { - get!(self.buffer, field: field::DIO_PRF, mask: 0b111) - } - - /// Return the destination advertisement trigger sequence number. - #[inline] - pub fn dio_dest_adv_trigger_seq_number(&self) -> u8 { - get!(self.buffer, field: field::DIO_DTSN) - } - - /// Return the DODAG id, which is an IPv6 address. - #[inline] - pub fn dio_dodag_id(&self) -> Address { - get!( - self.buffer, - into: Address, - fun: from_bytes, - field: field::DIO_DODAG_ID - ) - } -} - -/// Setters for the DODAG information object (DIO) message. -impl + AsMut<[u8]>> Packet { - /// Set the Version Number field. - #[inline] - pub fn set_dio_version_number(&mut self, value: u8) { - set!(self.buffer, value, field: field::DIO_VERSION_NUMBER) - } - - /// Set the Rank field. - #[inline] - pub fn set_dio_rank(&mut self, value: u16) { - set!(self.buffer, value, u16, field: field::DIO_RANK) - } - - /// Set the value of the Grounded flag. - #[inline] - pub fn set_dio_grounded(&mut self, value: bool) { - set!(self.buffer, value, bool, field: field::DIO_GROUNDED, shift: 7, mask: 0b01) - } - - /// Set the mode of operation field. - #[inline] - pub fn set_dio_mode_of_operation(&mut self, mode: ModeOfOperation) { - let raw = (self.buffer.as_ref()[field::DIO_MOP] & !(0b111 << 3)) | (u8::from(mode) << 3); - self.buffer.as_mut()[field::DIO_MOP] = raw; - } - - /// Set the DODAG preference field. - #[inline] - pub fn set_dio_dodag_preference(&mut self, value: u8) { - set!(self.buffer, value, field: field::DIO_PRF, mask: 0b111) - } - - /// Set the destination advertisement trigger sequence number. - #[inline] - pub fn set_dio_dest_adv_trigger_seq_number(&mut self, value: u8) { - set!(self.buffer, value, field: field::DIO_DTSN) - } - - /// Set the DODAG id, which is an IPv6 address. - #[inline] - pub fn set_dio_dodag_id(&mut self, address: Address) { - set!(self.buffer, address: address, field: field::DIO_DODAG_ID) - } -} - -/// Getters for the Destination Advertisement Object (DAO) message. -/// -/// ```txt -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | RPLInstanceID |K|D| Flags | Reserved | DAOSequence | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | | -/// + + -/// | | -/// + DODAGID* + -/// | | -/// + + -/// | | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Option(s)... -/// +-+-+-+-+-+-+-+-+ -/// ``` -impl> Packet { - /// Returns the Expect DAO-ACK flag. - #[inline] - pub fn dao_ack_request(&self) -> bool { - get!(self.buffer, bool, field: field::DAO_K, shift: 7, mask: 0b1) - } - - /// Returns the flag indicating that the DODAG ID is present or not. - #[inline] - pub fn dao_dodag_id_present(&self) -> bool { - get!(self.buffer, bool, field: field::DAO_D, shift: 6, mask: 0b1) - } - - /// Returns the DODAG sequence flag. - #[inline] - pub fn dao_dodag_sequence(&self) -> u8 { - get!(self.buffer, field: field::DAO_SEQUENCE) - } - - /// Returns the DODAG ID, an IPv6 address, when it is present. - #[inline] - pub fn dao_dodag_id(&self) -> Option
{ - if self.dao_dodag_id_present() { - Some(Address::from_bytes( - &self.buffer.as_ref()[field::DAO_DODAG_ID], - )) - } else { - None - } - } -} - -/// Setters for the Destination Advertisement Object (DAO) message. -impl + AsMut<[u8]>> Packet { - /// Set the Expect DAO-ACK flag. - #[inline] - pub fn set_dao_ack_request(&mut self, value: bool) { - set!(self.buffer, value, bool, field: field::DAO_K, shift: 7, mask: 0b1,) - } - - /// Set the flag indicating that the DODAG ID is present or not. - #[inline] - pub fn set_dao_dodag_id_present(&mut self, value: bool) { - set!(self.buffer, value, bool, field: field::DAO_D, shift: 6, mask: 0b1) - } - - /// Set the DODAG sequence flag. - #[inline] - pub fn set_dao_dodag_sequence(&mut self, value: u8) { - set!(self.buffer, value, field: field::DAO_SEQUENCE) - } - - /// Set the DODAG ID. - #[inline] - pub fn set_dao_dodag_id(&mut self, address: Option
) { - match address { - Some(address) => { - self.buffer.as_mut()[field::DAO_DODAG_ID].copy_from_slice(address.as_bytes()); - self.set_dao_dodag_id_present(true); - } - None => { - self.set_dao_dodag_id_present(false); - } - } - } -} - -/// Getters for the Destination Advertisement Object acknowledgement (DAO-ACK) message. -/// -/// ```txt -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | RPLInstanceID |D| Reserved | DAOSequence | Status | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | | -/// + + -/// | | -/// + DODAGID* + -/// | | -/// + + -/// | | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Option(s)... -/// +-+-+-+-+-+-+-+-+ -/// ``` -impl> Packet { - /// Returns the flag indicating that the DODAG ID is present or not. - #[inline] - pub fn dao_ack_dodag_id_present(&self) -> bool { - get!(self.buffer, bool, field: field::DAO_ACK_D, shift: 7, mask: 0b1) - } - - /// Return the DODAG sequence number. - #[inline] - pub fn dao_ack_sequence(&self) -> u8 { - get!(self.buffer, field: field::DAO_ACK_SEQUENCE) - } - - /// Return the DOA status field. - #[inline] - pub fn dao_ack_status(&self) -> u8 { - get!(self.buffer, field: field::DAO_ACK_STATUS) - } - - /// Returns the DODAG ID, an IPv6 address, when it is present. - #[inline] - pub fn dao_ack_dodag_id(&self) -> Option
{ - if self.dao_ack_dodag_id_present() { - Some(Address::from_bytes( - &self.buffer.as_ref()[field::DAO_ACK_DODAG_ID], - )) - } else { - None - } - } -} - -/// Setters for the Destination Advertisement Object acknowledgement (DAO-ACK) message. -impl + AsMut<[u8]>> Packet { - /// Set the flag indicating that the DODAG ID is present or not. - #[inline] - pub fn set_dao_ack_dodag_id_present(&mut self, value: bool) { - set!(self.buffer, value, bool, field: field::DAO_ACK_D, shift: 7, mask: 0b1) - } - - /// Set the DODAG sequence number. - #[inline] - pub fn set_dao_ack_sequence(&mut self, value: u8) { - set!(self.buffer, value, field: field::DAO_ACK_SEQUENCE) - } - - /// Set the DOA status field. - #[inline] - pub fn set_dao_ack_status(&mut self, value: u8) { - set!(self.buffer, value, field: field::DAO_ACK_STATUS) - } - - /// Set the DODAG ID. - #[inline] - pub fn set_dao_ack_dodag_id(&mut self, address: Option
) { - match address { - Some(address) => { - self.buffer.as_mut()[field::DAO_ACK_DODAG_ID].copy_from_slice(address.as_bytes()); - self.set_dao_ack_dodag_id_present(true); - } - None => { - self.set_dao_ack_dodag_id_present(false); - } - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Repr<'p> { - DodagInformationSolicitation { - options: &'p [u8], - }, - DodagInformationObject { - rpl_instance_id: InstanceId, - version_number: u8, - rank: u16, - grounded: bool, - mode_of_operation: ModeOfOperation, - dodag_preference: u8, - dtsn: u8, - dodag_id: Address, - options: &'p [u8], - }, - DestinationAdvertisementObject { - rpl_instance_id: InstanceId, - expect_ack: bool, - sequence: u8, - dodag_id: Option
, - options: &'p [u8], - }, - DestinationAdvertisementObjectAck { - rpl_instance_id: InstanceId, - sequence: u8, - status: u8, - dodag_id: Option
, - }, -} - -impl core::fmt::Display for Repr<'_> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Repr::DodagInformationSolicitation { .. } => { - write!(f, "DIS")?; - } - Repr::DodagInformationObject { - rpl_instance_id, - version_number, - rank, - grounded, - mode_of_operation, - dodag_preference, - dtsn, - dodag_id, - .. - } => { - write!( - f, - "DIO \ - IID={rpl_instance_id:?} \ - V={version_number} \ - R={rank} \ - G={grounded} \ - MOP={mode_of_operation:?} \ - Pref={dodag_preference} \ - DTSN={dtsn} \ - DODAGID={dodag_id}" - )?; - } - Repr::DestinationAdvertisementObject { - rpl_instance_id, - expect_ack, - sequence, - dodag_id, - .. - } => { - write!( - f, - "DAO \ - IID={rpl_instance_id:?} \ - Ack={expect_ack} \ - Seq={sequence} \ - DODAGID={dodag_id:?}", - )?; - } - Repr::DestinationAdvertisementObjectAck { - rpl_instance_id, - sequence, - status, - dodag_id, - .. - } => { - write!( - f, - "DAO-ACK \ - IID={rpl_instance_id:?} \ - Seq={sequence} \ - Status={status} \ - DODAGID={dodag_id:?}", - )?; - } - }; - - Ok(()) - } -} - -impl<'p> Repr<'p> { - pub fn set_options(&mut self, options: &'p [u8]) { - let opts = match self { - Repr::DodagInformationSolicitation { options } => options, - Repr::DodagInformationObject { options, .. } => options, - Repr::DestinationAdvertisementObject { options, .. } => options, - Repr::DestinationAdvertisementObjectAck { .. } => unreachable!(), - }; - - *opts = options; - } - - pub fn parse + ?Sized>(packet: &Packet<&'p T>) -> Result { - packet.check_len()?; - - let options = packet.options()?; - match RplControlMessage::from(packet.msg_code()) { - RplControlMessage::DodagInformationSolicitation => { - Ok(Repr::DodagInformationSolicitation { options }) - } - RplControlMessage::DodagInformationObject => Ok(Repr::DodagInformationObject { - rpl_instance_id: packet.rpl_instance_id(), - version_number: packet.dio_version_number(), - rank: packet.dio_rank(), - grounded: packet.dio_grounded(), - mode_of_operation: packet.dio_mode_of_operation(), - dodag_preference: packet.dio_dodag_preference(), - dtsn: packet.dio_dest_adv_trigger_seq_number(), - dodag_id: packet.dio_dodag_id(), - options, - }), - RplControlMessage::DestinationAdvertisementObject => { - Ok(Repr::DestinationAdvertisementObject { - rpl_instance_id: packet.rpl_instance_id(), - expect_ack: packet.dao_ack_request(), - sequence: packet.dao_dodag_sequence(), - dodag_id: packet.dao_dodag_id(), - options, - }) - } - RplControlMessage::DestinationAdvertisementObjectAck => { - Ok(Repr::DestinationAdvertisementObjectAck { - rpl_instance_id: packet.rpl_instance_id(), - sequence: packet.dao_ack_sequence(), - status: packet.dao_ack_status(), - dodag_id: packet.dao_ack_dodag_id(), - }) - } - RplControlMessage::SecureDodagInformationSolicitation - | RplControlMessage::SecureDodagInformationObject - | RplControlMessage::SecureDestinationAdvertisementObject - | RplControlMessage::SecureDestinationAdvertisementObjectAck - | RplControlMessage::ConsistencyCheck => Err(Error), - RplControlMessage::Unknown(_) => Err(Error), - } - } - - pub fn buffer_len(&self) -> usize { - let mut len = 4 + match self { - Repr::DodagInformationSolicitation { .. } => 2, - Repr::DodagInformationObject { .. } => 24, - Repr::DestinationAdvertisementObject { dodag_id, .. } => { - if dodag_id.is_some() { - 20 - } else { - 4 - } - } - Repr::DestinationAdvertisementObjectAck { dodag_id, .. } => { - if dodag_id.is_some() { - 20 - } else { - 4 - } - } - }; - - let opts = match self { - Repr::DodagInformationSolicitation { options } => &options[..], - Repr::DodagInformationObject { options, .. } => &options[..], - Repr::DestinationAdvertisementObject { options, .. } => &options[..], - Repr::DestinationAdvertisementObjectAck { .. } => &[], - }; - - len += opts.len(); - - len - } - - pub fn emit + AsMut<[u8]> + ?Sized>(&self, packet: &mut Packet<&mut T>) { - packet.set_msg_type(crate::wire::icmpv6::Message::RplControl); - - match self { - Repr::DodagInformationSolicitation { .. } => { - packet.set_msg_code(RplControlMessage::DodagInformationSolicitation.into()); - packet.clear_dis_flags(); - packet.clear_dis_reserved(); - } - Repr::DodagInformationObject { - rpl_instance_id, - version_number, - rank, - grounded, - mode_of_operation, - dodag_preference, - dtsn, - dodag_id, - .. - } => { - packet.set_msg_code(RplControlMessage::DodagInformationObject.into()); - packet.set_rpl_instance_id((*rpl_instance_id).into()); - packet.set_dio_version_number(*version_number); - packet.set_dio_rank(*rank); - packet.set_dio_grounded(*grounded); - packet.set_dio_mode_of_operation(*mode_of_operation); - packet.set_dio_dodag_preference(*dodag_preference); - packet.set_dio_dest_adv_trigger_seq_number(*dtsn); - packet.set_dio_dodag_id(*dodag_id); - } - Repr::DestinationAdvertisementObject { - rpl_instance_id, - expect_ack, - sequence, - dodag_id, - .. - } => { - packet.set_msg_code(RplControlMessage::DestinationAdvertisementObject.into()); - packet.set_rpl_instance_id((*rpl_instance_id).into()); - packet.set_dao_ack_request(*expect_ack); - packet.set_dao_dodag_sequence(*sequence); - packet.set_dao_dodag_id(*dodag_id); - } - Repr::DestinationAdvertisementObjectAck { - rpl_instance_id, - sequence, - status, - dodag_id, - .. - } => { - packet.set_msg_code(RplControlMessage::DestinationAdvertisementObjectAck.into()); - packet.set_rpl_instance_id((*rpl_instance_id).into()); - packet.set_dao_ack_sequence(*sequence); - packet.set_dao_ack_status(*status); - packet.set_dao_ack_dodag_id(*dodag_id); - } - } - - let options = match self { - Repr::DodagInformationSolicitation { options } => &options[..], - Repr::DodagInformationObject { options, .. } => &options[..], - Repr::DestinationAdvertisementObject { options, .. } => &options[..], - Repr::DestinationAdvertisementObjectAck { .. } => &[], - }; - - packet.options_mut().copy_from_slice(options); - } -} - -pub mod options { - use byteorder::{ByteOrder, NetworkEndian}; - - use super::{Error, InstanceId, Result}; - use crate::wire::ipv6::Address; - - /// A read/write wrapper around a RPL Control Message Option. - #[derive(Debug, Clone)] - pub struct Packet> { - buffer: T, - } - - enum_with_unknown! { - pub enum OptionType(u8) { - Pad1 = 0x00, - PadN = 0x01, - DagMetricContainer = 0x02, - RouteInformation = 0x03, - DodagConfiguration = 0x04, - RplTarget = 0x05, - TransitInformation = 0x06, - SolicitedInformation = 0x07, - PrefixInformation = 0x08, - RplTargetDescriptor = 0x09, - } - } - - impl From<&Repr<'_>> for OptionType { - fn from(repr: &Repr) -> Self { - match repr { - Repr::Pad1 => Self::Pad1, - Repr::PadN(_) => Self::PadN, - Repr::DagMetricContainer => Self::DagMetricContainer, - Repr::RouteInformation { .. } => Self::RouteInformation, - Repr::DodagConfiguration { .. } => Self::DodagConfiguration, - Repr::RplTarget { .. } => Self::RplTarget, - Repr::TransitInformation { .. } => Self::TransitInformation, - Repr::SolicitedInformation { .. } => Self::SolicitedInformation, - Repr::PrefixInformation { .. } => Self::PrefixInformation, - Repr::RplTargetDescriptor { .. } => Self::RplTargetDescriptor, - } - } - } - - mod field { - use crate::wire::field::*; - - // Generic fields. - pub const TYPE: usize = 0; - pub const LENGTH: usize = 1; - - pub const PADN: Rest = 2..; - - // Route Information fields. - pub const ROUTE_INFO_PREFIX_LENGTH: usize = 2; - pub const ROUTE_INFO_RESERVED: usize = 3; - pub const ROUTE_INFO_PREFERENCE: usize = 3; - pub const ROUTE_INFO_LIFETIME: Field = 4..9; - - // DODAG Configuration fields. - pub const DODAG_CONF_FLAGS: usize = 2; - pub const DODAG_CONF_AUTHENTICATION_ENABLED: usize = 2; - pub const DODAG_CONF_PATH_CONTROL_SIZE: usize = 2; - pub const DODAG_CONF_DIO_INTERVAL_DOUBLINGS: usize = 3; - pub const DODAG_CONF_DIO_INTERVAL_MINIMUM: usize = 4; - pub const DODAG_CONF_DIO_REDUNDANCY_CONSTANT: usize = 5; - pub const DODAG_CONF_DIO_MAX_RANK_INCREASE: Field = 6..8; - pub const DODAG_CONF_MIN_HOP_RANK_INCREASE: Field = 8..10; - pub const DODAG_CONF_OBJECTIVE_CODE_POINT: Field = 10..12; - pub const DODAG_CONF_DEFAULT_LIFETIME: usize = 13; - pub const DODAG_CONF_LIFETIME_UNIT: Field = 14..16; - - // RPL Target fields. - pub const RPL_TARGET_FLAGS: usize = 2; - pub const RPL_TARGET_PREFIX_LENGTH: usize = 3; - - // Transit Information fields. - pub const TRANSIT_INFO_FLAGS: usize = 2; - pub const TRANSIT_INFO_EXTERNAL: usize = 2; - pub const TRANSIT_INFO_PATH_CONTROL: usize = 3; - pub const TRANSIT_INFO_PATH_SEQUENCE: usize = 4; - pub const TRANSIT_INFO_PATH_LIFETIME: usize = 5; - pub const TRANSIT_INFO_PARENT_ADDRESS: Field = 6..6 + 16; - - // Solicited Information fields. - pub const SOLICITED_INFO_RPL_INSTANCE_ID: usize = 2; - pub const SOLICITED_INFO_FLAGS: usize = 3; - pub const SOLICITED_INFO_VERSION_PREDICATE: usize = 3; - pub const SOLICITED_INFO_INSTANCE_ID_PREDICATE: usize = 3; - pub const SOLICITED_INFO_DODAG_ID_PREDICATE: usize = 3; - pub const SOLICITED_INFO_DODAG_ID: Field = 4..20; - pub const SOLICITED_INFO_VERSION_NUMBER: usize = 20; - - // Prefix Information fields. - pub const PREFIX_INFO_PREFIX_LENGTH: usize = 2; - pub const PREFIX_INFO_RESERVED1: usize = 3; - pub const PREFIX_INFO_ON_LINK: usize = 3; - pub const PREFIX_INFO_AUTONOMOUS_CONF: usize = 3; - pub const PREFIX_INFO_ROUTER_ADDRESS_FLAG: usize = 3; - pub const PREFIX_INFO_VALID_LIFETIME: Field = 4..8; - pub const PREFIX_INFO_PREFERRED_LIFETIME: Field = 8..12; - pub const PREFIX_INFO_RESERVED2: Field = 12..16; - pub const PREFIX_INFO_PREFIX: Field = 16..16 + 16; - - // RPL Target Descriptor fields. - pub const TARGET_DESCRIPTOR: Field = 2..6; - } - - /// Getters for the RPL Control Message Options. - impl> Packet { - /// Imbue a raw octet buffer with RPL Control Message Option structure. - #[inline] - pub fn new_unchecked(buffer: T) -> Self { - Packet { buffer } - } - - #[inline] - pub fn new_checked(buffer: T) -> Result { - if buffer.as_ref().is_empty() { - return Err(Error); - } - - Ok(Packet { buffer }) - } - - /// Return the type field. - #[inline] - pub fn option_type(&self) -> OptionType { - OptionType::from(self.buffer.as_ref()[field::TYPE]) - } - - /// Return the length field. - #[inline] - pub fn option_length(&self) -> u8 { - get!(self.buffer, field: field::LENGTH) - } - } - - impl<'p, T: AsRef<[u8]> + ?Sized> Packet<&'p T> { - /// Return a pointer to the next option. - #[inline] - pub fn next_option(&self) -> Option<&'p [u8]> { - if !self.buffer.as_ref().is_empty() { - match self.option_type() { - OptionType::Pad1 => Some(&self.buffer.as_ref()[1..]), - OptionType::Unknown(_) => unreachable!(), - _ => { - let len = self.option_length(); - Some(&self.buffer.as_ref()[2 + len as usize..]) - } - } - } else { - None - } - } - } - - impl + AsMut<[u8]>> Packet { - /// Set the Option Type field. - #[inline] - pub fn set_option_type(&mut self, option_type: OptionType) { - self.buffer.as_mut()[field::TYPE] = option_type.into(); - } - - /// Set the Option Length field. - #[inline] - pub fn set_option_length(&mut self, length: u8) { - self.buffer.as_mut()[field::LENGTH] = length; - } - } - - impl + AsMut<[u8]>> Packet { - #[inline] - pub fn clear_padn(&mut self, size: u8) { - for b in &mut self.buffer.as_mut()[field::PADN][..size as usize] { - *b = 0; - } - } - } - - /// Getters for the DAG Metric Container Option Message. - - /// Getters for the Route Information Option Message. - /// - /// ```txt - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Type = 0x03 | Option Length | Prefix Length |Resvd|Prf|Resvd| - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Route Lifetime | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | | - /// . Prefix (Variable Length) . - /// . . - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// ``` - impl> Packet { - /// Return the Prefix Length field. - #[inline] - pub fn prefix_length(&self) -> u8 { - get!(self.buffer, field: field::ROUTE_INFO_PREFIX_LENGTH) - } - - /// Return the Route Preference field. - #[inline] - pub fn route_preference(&self) -> u8 { - (self.buffer.as_ref()[field::ROUTE_INFO_PREFERENCE] & 0b0001_1000) >> 3 - } - - /// Return the Route Lifetime field. - #[inline] - pub fn route_lifetime(&self) -> u32 { - get!(self.buffer, u32, field: field::ROUTE_INFO_LIFETIME) - } - } - - impl<'p, T: AsRef<[u8]> + ?Sized> Packet<&'p T> { - /// Return the Prefix field. - #[inline] - pub fn prefix(&self) -> &'p [u8] { - let option_len = self.option_length(); - &self.buffer.as_ref()[field::ROUTE_INFO_LIFETIME.end..] - [..option_len as usize - field::ROUTE_INFO_LIFETIME.end] - } - } - - /// Setters for the Route Information Option Message. - impl + AsMut<[u8]>> Packet { - /// Set the Prefix Length field. - #[inline] - pub fn set_route_info_prefix_length(&mut self, value: u8) { - set!(self.buffer, value, field: field::ROUTE_INFO_PREFIX_LENGTH) - } - - /// Set the Route Preference field. - #[inline] - pub fn set_route_info_route_preference(&mut self, _value: u8) { - todo!(); - } - - /// Set the Route Lifetime field. - #[inline] - pub fn set_route_info_route_lifetime(&mut self, value: u32) { - set!(self.buffer, value, u32, field: field::ROUTE_INFO_LIFETIME) - } - - /// Set the prefix field. - #[inline] - pub fn set_route_info_prefix(&mut self, _prefix: &[u8]) { - todo!(); - } - - /// Clear the reserved field. - #[inline] - pub fn clear_route_info_reserved(&mut self) { - self.buffer.as_mut()[field::ROUTE_INFO_RESERVED] = 0; - } - } - - /// Getters for the DODAG Configuration Option Message. - /// - /// ```txt - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Type = 0x04 |Opt Length = 14| Flags |A| PCS | DIOIntDoubl. | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | DIOIntMin. | DIORedun. | MaxRankIncrease | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | MinHopRankIncrease | OCP | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Reserved | Def. Lifetime | Lifetime Unit | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// ``` - impl> Packet { - /// Return the Authentication Enabled field. - #[inline] - pub fn authentication_enabled(&self) -> bool { - get!( - self.buffer, - bool, - field: field::DODAG_CONF_AUTHENTICATION_ENABLED, - shift: 3, - mask: 0b1 - ) - } - - /// Return the Path Control Size field. - #[inline] - pub fn path_control_size(&self) -> u8 { - get!(self.buffer, field: field::DODAG_CONF_PATH_CONTROL_SIZE, mask: 0b111) - } - - /// Return the DIO Interval Doublings field. - #[inline] - pub fn dio_interval_doublings(&self) -> u8 { - get!(self.buffer, field: field::DODAG_CONF_DIO_INTERVAL_DOUBLINGS) - } - - /// Return the DIO Interval Minimum field. - #[inline] - pub fn dio_interval_minimum(&self) -> u8 { - get!(self.buffer, field: field::DODAG_CONF_DIO_INTERVAL_MINIMUM) - } - - /// Return the DIO Redundancy Constant field. - #[inline] - pub fn dio_redundancy_constant(&self) -> u8 { - get!( - self.buffer, - field: field::DODAG_CONF_DIO_REDUNDANCY_CONSTANT - ) - } - - /// Return the Max Rank Increase field. - #[inline] - pub fn max_rank_increase(&self) -> u16 { - get!( - self.buffer, - u16, - field: field::DODAG_CONF_DIO_MAX_RANK_INCREASE - ) - } - - /// Return the Minimum Hop Rank Increase field. - #[inline] - pub fn minimum_hop_rank_increase(&self) -> u16 { - get!( - self.buffer, - u16, - field: field::DODAG_CONF_MIN_HOP_RANK_INCREASE - ) - } - - /// Return the Objective Code Point field. - #[inline] - pub fn objective_code_point(&self) -> u16 { - get!( - self.buffer, - u16, - field: field::DODAG_CONF_OBJECTIVE_CODE_POINT - ) - } - - /// Return the Default Lifetime field. - #[inline] - pub fn default_lifetime(&self) -> u8 { - get!(self.buffer, field: field::DODAG_CONF_DEFAULT_LIFETIME) - } - - /// Return the Lifetime Unit field. - #[inline] - pub fn lifetime_unit(&self) -> u16 { - get!(self.buffer, u16, field: field::DODAG_CONF_LIFETIME_UNIT) - } - } - - /// Getters for the DODAG Configuration Option Message. - impl + AsMut<[u8]>> Packet { - /// Clear the Flags field. - #[inline] - pub fn clear_dodag_conf_flags(&mut self) { - self.buffer.as_mut()[field::DODAG_CONF_FLAGS] = 0; - } - - /// Set the Authentication Enabled field. - #[inline] - pub fn set_dodag_conf_authentication_enabled(&mut self, value: bool) { - set!( - self.buffer, - value, - bool, - field: field::DODAG_CONF_AUTHENTICATION_ENABLED, - shift: 3, - mask: 0b1 - ) - } - - /// Set the Path Control Size field. - #[inline] - pub fn set_dodag_conf_path_control_size(&mut self, value: u8) { - set!( - self.buffer, - value, - field: field::DODAG_CONF_PATH_CONTROL_SIZE, - mask: 0b111 - ) - } - - /// Set the DIO Interval Doublings field. - #[inline] - pub fn set_dodag_conf_dio_interval_doublings(&mut self, value: u8) { - set!( - self.buffer, - value, - field: field::DODAG_CONF_DIO_INTERVAL_DOUBLINGS - ) - } - - /// Set the DIO Interval Minimum field. - #[inline] - pub fn set_dodag_conf_dio_interval_minimum(&mut self, value: u8) { - set!( - self.buffer, - value, - field: field::DODAG_CONF_DIO_INTERVAL_MINIMUM - ) - } - - /// Set the DIO Redundancy Constant field. - #[inline] - pub fn set_dodag_conf_dio_redundancy_constant(&mut self, value: u8) { - set!( - self.buffer, - value, - field: field::DODAG_CONF_DIO_REDUNDANCY_CONSTANT - ) - } - - /// Set the Max Rank Increase field. - #[inline] - pub fn set_dodag_conf_max_rank_increase(&mut self, value: u16) { - set!( - self.buffer, - value, - u16, - field: field::DODAG_CONF_DIO_MAX_RANK_INCREASE - ) - } - - /// Set the Minimum Hop Rank Increase field. - #[inline] - pub fn set_dodag_conf_minimum_hop_rank_increase(&mut self, value: u16) { - set!( - self.buffer, - value, - u16, - field: field::DODAG_CONF_MIN_HOP_RANK_INCREASE - ) - } - - /// Set the Objective Code Point field. - #[inline] - pub fn set_dodag_conf_objective_code_point(&mut self, value: u16) { - set!( - self.buffer, - value, - u16, - field: field::DODAG_CONF_OBJECTIVE_CODE_POINT - ) - } - - /// Set the Default Lifetime field. - #[inline] - pub fn set_dodag_conf_default_lifetime(&mut self, value: u8) { - set!( - self.buffer, - value, - field: field::DODAG_CONF_DEFAULT_LIFETIME - ) - } - - /// Set the Lifetime Unit field. - #[inline] - pub fn set_dodag_conf_lifetime_unit(&mut self, value: u16) { - set!( - self.buffer, - value, - u16, - field: field::DODAG_CONF_LIFETIME_UNIT - ) - } - } - - /// Getters for the RPL Target Option Message. - /// - /// ```txt - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Type = 0x05 | Option Length | Flags | Prefix Length | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | | - /// + + - /// | Target Prefix (Variable Length) | - /// . . - /// . . - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// ``` - impl> Packet { - /// Return the Target Prefix Length field. - pub fn target_prefix_length(&self) -> u8 { - get!(self.buffer, field: field::RPL_TARGET_PREFIX_LENGTH) - } - } - - impl<'p, T: AsRef<[u8]> + ?Sized> Packet<&'p T> { - /// Return the Target Prefix field. - #[inline] - pub fn target_prefix(&self) -> &'p [u8] { - let option_len = self.option_length(); - &self.buffer.as_ref()[field::RPL_TARGET_PREFIX_LENGTH + 1..] - [..option_len as usize - field::RPL_TARGET_PREFIX_LENGTH + 1] - } - } - - /// Setters for the RPL Target Option Message. - impl + AsMut<[u8]>> Packet { - /// Clear the Flags field. - #[inline] - pub fn clear_rpl_target_flags(&mut self) { - self.buffer.as_mut()[field::RPL_TARGET_FLAGS] = 0; - } - - /// Set the Target Prefix Length field. - #[inline] - pub fn set_rpl_target_prefix_length(&mut self, value: u8) { - set!(self.buffer, value, field: field::RPL_TARGET_PREFIX_LENGTH) - } - - /// Set the Target Prefix field. - #[inline] - pub fn set_rpl_target_prefix(&mut self, prefix: &[u8]) { - self.buffer.as_mut()[field::RPL_TARGET_PREFIX_LENGTH + 1..][..prefix.len()] - .copy_from_slice(prefix); - } - } - - /// Getters for the Transit Information Option Message. - /// - /// ```txt - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Type = 0x06 | Option Length |E| Flags | Path Control | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Path Sequence | Path Lifetime | | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + - /// | | - /// + + - /// | | - /// + Parent Address* + - /// | | - /// + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// ``` - impl> Packet { - /// Return the External flag. - #[inline] - pub fn is_external(&self) -> bool { - get!( - self.buffer, - bool, - field: field::TRANSIT_INFO_EXTERNAL, - shift: 7, - mask: 0b1, - ) - } - - /// Return the Path Control field. - #[inline] - pub fn path_control(&self) -> u8 { - get!(self.buffer, field: field::TRANSIT_INFO_PATH_CONTROL) - } - - /// Return the Path Sequence field. - #[inline] - pub fn path_sequence(&self) -> u8 { - get!(self.buffer, field: field::TRANSIT_INFO_PATH_SEQUENCE) - } - - /// Return the Path Lifetime field. - #[inline] - pub fn path_lifetime(&self) -> u8 { - get!(self.buffer, field: field::TRANSIT_INFO_PATH_LIFETIME) - } - - /// Return the Parent Address field. - #[inline] - pub fn parent_address(&self) -> Option
{ - if self.option_length() > 5 { - Some(Address::from_bytes( - &self.buffer.as_ref()[field::TRANSIT_INFO_PARENT_ADDRESS], - )) - } else { - None - } - } - } - - /// Setters for the Transit Information Option Message. - impl + AsMut<[u8]>> Packet { - /// Clear the Flags field. - #[inline] - pub fn clear_transit_info_flags(&mut self) { - self.buffer.as_mut()[field::TRANSIT_INFO_FLAGS] = 0; - } - - /// Set the External flag. - #[inline] - pub fn set_transit_info_is_external(&mut self, value: bool) { - set!( - self.buffer, - value, - bool, - field: field::TRANSIT_INFO_EXTERNAL, - shift: 7, - mask: 0b1 - ) - } - - /// Set the Path Control field. - #[inline] - pub fn set_transit_info_path_control(&mut self, value: u8) { - set!(self.buffer, value, field: field::TRANSIT_INFO_PATH_CONTROL) - } - - /// Set the Path Sequence field. - #[inline] - pub fn set_transit_info_path_sequence(&mut self, value: u8) { - set!(self.buffer, value, field: field::TRANSIT_INFO_PATH_SEQUENCE) - } - - /// Set the Path Lifetime field. - #[inline] - pub fn set_transit_info_path_lifetime(&mut self, value: u8) { - set!(self.buffer, value, field: field::TRANSIT_INFO_PATH_LIFETIME) - } - - /// Set the Parent Address field. - #[inline] - pub fn set_transit_info_parent_address(&mut self, address: Address) { - self.buffer.as_mut()[field::TRANSIT_INFO_PARENT_ADDRESS] - .copy_from_slice(address.as_bytes()); - } - } - - /// Getters for the Solicited Information Option Message. - /// - /// ```txt - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Type = 0x07 |Opt Length = 19| RPLInstanceID |V|I|D| Flags | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | | - /// + + - /// | | - /// + DODAGID + - /// | | - /// + + - /// | | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// |Version Number | - /// +-+-+-+-+-+-+-+-+ - /// ``` - impl> Packet { - /// Return the RPL Instance ID field. - #[inline] - pub fn rpl_instance_id(&self) -> u8 { - get!(self.buffer, field: field::SOLICITED_INFO_RPL_INSTANCE_ID) - } - - /// Return the Version Predicate flag. - #[inline] - pub fn version_predicate(&self) -> bool { - get!( - self.buffer, - bool, - field: field::SOLICITED_INFO_VERSION_PREDICATE, - shift: 7, - mask: 0b1, - ) - } - - /// Return the Instance ID Predicate flag. - #[inline] - pub fn instance_id_predicate(&self) -> bool { - get!( - self.buffer, - bool, - field: field::SOLICITED_INFO_INSTANCE_ID_PREDICATE, - shift: 6, - mask: 0b1, - ) - } - - /// Return the DODAG Predicate ID flag. - #[inline] - pub fn dodag_id_predicate(&self) -> bool { - get!( - self.buffer, - bool, - field: field::SOLICITED_INFO_DODAG_ID_PREDICATE, - shift: 5, - mask: 0b1, - ) - } - - /// Return the DODAG ID field. - #[inline] - pub fn dodag_id(&self) -> Address { - get!( - self.buffer, - into: Address, - fun: from_bytes, - field: field::SOLICITED_INFO_DODAG_ID - ) - } - - /// Return the Version Number field. - #[inline] - pub fn version_number(&self) -> u8 { - get!(self.buffer, field: field::SOLICITED_INFO_VERSION_NUMBER) - } - } - - /// Setters for the Solicited Information Option Message. - impl + AsMut<[u8]>> Packet { - /// Clear the Flags field. - #[inline] - pub fn clear_solicited_info_flags(&mut self) { - self.buffer.as_mut()[field::SOLICITED_INFO_FLAGS] = 0; - } - - /// Set the RPL Instance ID field. - #[inline] - pub fn set_solicited_info_rpl_instance_id(&mut self, value: u8) { - set!( - self.buffer, - value, - field: field::SOLICITED_INFO_RPL_INSTANCE_ID - ) - } - - /// Set the Version Predicate flag. - #[inline] - pub fn set_solicited_info_version_predicate(&mut self, value: bool) { - set!( - self.buffer, - value, - bool, - field: field::SOLICITED_INFO_VERSION_PREDICATE, - shift: 7, - mask: 0b1 - ) - } - - /// Set the Instance ID Predicate flag. - #[inline] - pub fn set_solicited_info_instance_id_predicate(&mut self, value: bool) { - set!( - self.buffer, - value, - bool, - field: field::SOLICITED_INFO_INSTANCE_ID_PREDICATE, - shift: 6, - mask: 0b1 - ) - } - - /// Set the DODAG Predicate ID flag. - #[inline] - pub fn set_solicited_info_dodag_id_predicate(&mut self, value: bool) { - set!( - self.buffer, - value, - bool, - field: field::SOLICITED_INFO_DODAG_ID_PREDICATE, - shift: 5, - mask: 0b1 - ) - } - - /// Set the DODAG ID field. - #[inline] - pub fn set_solicited_info_dodag_id(&mut self, address: Address) { - set!( - self.buffer, - address: address, - field: field::SOLICITED_INFO_DODAG_ID - ) - } - - /// Set the Version Number field. - #[inline] - pub fn set_solicited_info_version_number(&mut self, value: u8) { - set!( - self.buffer, - value, - field: field::SOLICITED_INFO_VERSION_NUMBER - ) - } - } - - /// Getters for the Prefix Information Option Message. - /// - /// ```txt - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Type = 0x08 |Opt Length = 30| Prefix Length |L|A|R|Reserved1| - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Valid Lifetime | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Preferred Lifetime | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Reserved2 | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | | - /// + + - /// | | - /// + Prefix + - /// | | - /// + + - /// | | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// ``` - impl> Packet { - /// Return the Prefix Length field. - #[inline] - pub fn prefix_info_prefix_length(&self) -> u8 { - get!(self.buffer, field: field::PREFIX_INFO_PREFIX_LENGTH) - } - - /// Return the On-Link flag. - #[inline] - pub fn on_link(&self) -> bool { - get!( - self.buffer, - bool, - field: field::PREFIX_INFO_ON_LINK, - shift: 7, - mask: 0b1, - ) - } - - /// Return the Autonomous Address-Configuration flag. - #[inline] - pub fn autonomous_address_configuration(&self) -> bool { - get!( - self.buffer, - bool, - field: field::PREFIX_INFO_AUTONOMOUS_CONF, - shift: 6, - mask: 0b1, - ) - } - - /// Return the Router Address flag. - #[inline] - pub fn router_address(&self) -> bool { - get!( - self.buffer, - bool, - field: field::PREFIX_INFO_ROUTER_ADDRESS_FLAG, - shift: 5, - mask: 0b1, - ) - } - - /// Return the Valid Lifetime field. - #[inline] - pub fn valid_lifetime(&self) -> u32 { - get!(self.buffer, u32, field: field::PREFIX_INFO_VALID_LIFETIME) - } - - /// Return the Preferred Lifetime field. - #[inline] - pub fn preferred_lifetime(&self) -> u32 { - get!( - self.buffer, - u32, - field: field::PREFIX_INFO_PREFERRED_LIFETIME - ) - } - } - - impl<'p, T: AsRef<[u8]> + ?Sized> Packet<&'p T> { - /// Return the Prefix field. - #[inline] - pub fn destination_prefix(&self) -> &'p [u8] { - &self.buffer.as_ref()[field::PREFIX_INFO_PREFIX] - } - } - - /// Setters for the Prefix Information Option Message. - impl + AsMut<[u8]>> Packet { - /// Clear the reserved fields. - #[inline] - pub fn clear_prefix_info_reserved(&mut self) { - self.buffer.as_mut()[field::PREFIX_INFO_RESERVED1] = 0; - self.buffer.as_mut()[field::PREFIX_INFO_RESERVED2].copy_from_slice(&[0; 4]); - } - - /// Set the Prefix Length field. - #[inline] - pub fn set_prefix_info_prefix_length(&mut self, value: u8) { - set!(self.buffer, value, field: field::PREFIX_INFO_PREFIX_LENGTH) - } - - /// Set the On-Link flag. - #[inline] - pub fn set_prefix_info_on_link(&mut self, value: bool) { - set!(self.buffer, value, bool, field: field::PREFIX_INFO_ON_LINK, shift: 7, mask: 0b1) - } - - /// Set the Autonomous Address-Configuration flag. - #[inline] - pub fn set_prefix_info_autonomous_address_configuration(&mut self, value: bool) { - set!( - self.buffer, - value, - bool, - field: field::PREFIX_INFO_AUTONOMOUS_CONF, - shift: 6, - mask: 0b1 - ) - } - - /// Set the Router Address flag. - #[inline] - pub fn set_prefix_info_router_address(&mut self, value: bool) { - set!( - self.buffer, - value, - bool, - field: field::PREFIX_INFO_ROUTER_ADDRESS_FLAG, - shift: 5, - mask: 0b1 - ) - } - - /// Set the Valid Lifetime field. - #[inline] - pub fn set_prefix_info_valid_lifetime(&mut self, value: u32) { - set!( - self.buffer, - value, - u32, - field: field::PREFIX_INFO_VALID_LIFETIME - ) - } - - /// Set the Preferred Lifetime field. - #[inline] - pub fn set_prefix_info_preferred_lifetime(&mut self, value: u32) { - set!( - self.buffer, - value, - u32, - field: field::PREFIX_INFO_PREFERRED_LIFETIME - ) - } - - /// Set the Prefix field. - #[inline] - pub fn set_prefix_info_destination_prefix(&mut self, prefix: &[u8]) { - self.buffer.as_mut()[field::PREFIX_INFO_PREFIX].copy_from_slice(prefix); - } - } - - /// Getters for the RPL Target Descriptor Option Message. - /// - /// ```txt - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Type = 0x09 |Opt Length = 4 | Descriptor - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// Descriptor (cont.) | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// ``` - impl> Packet { - /// Return the Descriptor field. - #[inline] - pub fn descriptor(&self) -> u32 { - get!(self.buffer, u32, field: field::TARGET_DESCRIPTOR) - } - } - - /// Setters for the RPL Target Descriptor Option Message. - impl + AsMut<[u8]>> Packet { - /// Set the Descriptor field. - #[inline] - pub fn set_rpl_target_descriptor_descriptor(&mut self, value: u32) { - set!(self.buffer, value, u32, field: field::TARGET_DESCRIPTOR) - } - } - - #[derive(Debug, PartialEq, Eq, Clone, Copy)] - #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub enum Repr<'p> { - Pad1, - PadN(u8), - DagMetricContainer, - RouteInformation { - prefix_length: u8, - preference: u8, - lifetime: u32, - prefix: &'p [u8], - }, - DodagConfiguration { - authentication_enabled: bool, - path_control_size: u8, - dio_interval_doublings: u8, - dio_interval_min: u8, - dio_redundancy_constant: u8, - max_rank_increase: u16, - minimum_hop_rank_increase: u16, - objective_code_point: u16, - default_lifetime: u8, - lifetime_unit: u16, - }, - RplTarget { - prefix_length: u8, - prefix: crate::wire::Ipv6Address, // FIXME: this is not the correct type, because the - // field can be an IPv6 address, a prefix or a - // multicast group. - }, - TransitInformation { - external: bool, - path_control: u8, - path_sequence: u8, - path_lifetime: u8, - parent_address: Option
, - }, - SolicitedInformation { - rpl_instance_id: InstanceId, - version_predicate: bool, - instance_id_predicate: bool, - dodag_id_predicate: bool, - dodag_id: Address, - version_number: u8, - }, - PrefixInformation { - prefix_length: u8, - on_link: bool, - autonomous_address_configuration: bool, - router_address: bool, - valid_lifetime: u32, - preferred_lifetime: u32, - destination_prefix: &'p [u8], - }, - RplTargetDescriptor { - descriptor: u32, - }, - } - - impl core::fmt::Display for Repr<'_> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Repr::Pad1 => write!(f, "Pad1"), - Repr::PadN(n) => write!(f, "PadN({n})"), - Repr::DagMetricContainer => todo!(), - Repr::RouteInformation { - prefix_length, - preference, - lifetime, - prefix, - } => { - write!( - f, - "ROUTE INFO \ - PrefixLength={prefix_length} \ - Preference={preference} \ - Lifetime={lifetime} \ - Prefix={prefix:0x?}" - ) - } - Repr::DodagConfiguration { - dio_interval_doublings, - dio_interval_min, - dio_redundancy_constant, - max_rank_increase, - minimum_hop_rank_increase, - objective_code_point, - default_lifetime, - lifetime_unit, - .. - } => { - write!( - f, - "DODAG CONF \ - IntD={dio_interval_doublings} \ - IntMin={dio_interval_min} \ - RedCst={dio_redundancy_constant} \ - MaxRankIncr={max_rank_increase} \ - MinHopRankIncr={minimum_hop_rank_increase} \ - OCP={objective_code_point} \ - DefaultLifetime={default_lifetime} \ - LifeUnit={lifetime_unit}" - ) - } - Repr::RplTarget { - prefix_length, - prefix, - } => { - write!( - f, - "RPL Target \ - PrefixLength={prefix_length} \ - Prefix={prefix:0x?}" - ) - } - Repr::TransitInformation { - external, - path_control, - path_sequence, - path_lifetime, - parent_address, - } => { - write!( - f, - "Transit Info \ - External={external} \ - PathCtrl={path_control} \ - PathSqnc={path_sequence} \ - PathLifetime={path_lifetime} \ - Parent={parent_address:0x?}" - ) - } - Repr::SolicitedInformation { - rpl_instance_id, - version_predicate, - instance_id_predicate, - dodag_id_predicate, - dodag_id, - version_number, - } => { - write!( - f, - "Solicited Info \ - I={instance_id_predicate} \ - IID={rpl_instance_id:0x?} \ - D={dodag_id_predicate} \ - DODAGID={dodag_id} \ - V={version_predicate} \ - Version={version_number}" - ) - } - Repr::PrefixInformation { - prefix_length, - on_link, - autonomous_address_configuration, - router_address, - valid_lifetime, - preferred_lifetime, - destination_prefix, - } => { - write!( - f, - "Prefix Info \ - PrefixLength={prefix_length} \ - L={on_link} A={autonomous_address_configuration} R={router_address} \ - Valid={valid_lifetime} \ - Preferred={preferred_lifetime} \ - Prefix={destination_prefix:0x?}" - ) - } - Repr::RplTargetDescriptor { .. } => write!(f, "Target Descriptor"), - } - } - } - - impl<'p> Repr<'p> { - pub fn parse + ?Sized>(packet: &Packet<&'p T>) -> Result { - match packet.option_type() { - OptionType::Pad1 => Ok(Repr::Pad1), - OptionType::PadN => Ok(Repr::PadN(packet.option_length())), - OptionType::DagMetricContainer => todo!(), - OptionType::RouteInformation => Ok(Repr::RouteInformation { - prefix_length: packet.prefix_length(), - preference: packet.route_preference(), - lifetime: packet.route_lifetime(), - prefix: packet.prefix(), - }), - OptionType::DodagConfiguration => Ok(Repr::DodagConfiguration { - authentication_enabled: packet.authentication_enabled(), - path_control_size: packet.path_control_size(), - dio_interval_doublings: packet.dio_interval_doublings(), - dio_interval_min: packet.dio_interval_minimum(), - dio_redundancy_constant: packet.dio_redundancy_constant(), - max_rank_increase: packet.max_rank_increase(), - minimum_hop_rank_increase: packet.minimum_hop_rank_increase(), - objective_code_point: packet.objective_code_point(), - default_lifetime: packet.default_lifetime(), - lifetime_unit: packet.lifetime_unit(), - }), - OptionType::RplTarget => Ok(Repr::RplTarget { - prefix_length: packet.target_prefix_length(), - prefix: crate::wire::Ipv6Address::from_bytes(packet.target_prefix()), - }), - OptionType::TransitInformation => Ok(Repr::TransitInformation { - external: packet.is_external(), - path_control: packet.path_control(), - path_sequence: packet.path_sequence(), - path_lifetime: packet.path_lifetime(), - parent_address: packet.parent_address(), - }), - OptionType::SolicitedInformation => Ok(Repr::SolicitedInformation { - rpl_instance_id: InstanceId::from(packet.rpl_instance_id()), - version_predicate: packet.version_predicate(), - instance_id_predicate: packet.instance_id_predicate(), - dodag_id_predicate: packet.dodag_id_predicate(), - dodag_id: packet.dodag_id(), - version_number: packet.version_number(), - }), - OptionType::PrefixInformation => Ok(Repr::PrefixInformation { - prefix_length: packet.prefix_info_prefix_length(), - on_link: packet.on_link(), - autonomous_address_configuration: packet.autonomous_address_configuration(), - router_address: packet.router_address(), - valid_lifetime: packet.valid_lifetime(), - preferred_lifetime: packet.preferred_lifetime(), - destination_prefix: packet.destination_prefix(), - }), - OptionType::RplTargetDescriptor => Ok(Repr::RplTargetDescriptor { - descriptor: packet.descriptor(), - }), - OptionType::Unknown(_) => Err(Error), - } - } - - pub fn buffer_len(&self) -> usize { - match self { - Repr::Pad1 => 1, - Repr::PadN(size) => 2 + *size as usize, - Repr::DagMetricContainer => todo!(), - Repr::RouteInformation { prefix, .. } => 2 + 6 + prefix.len(), - Repr::DodagConfiguration { .. } => 2 + 14, - Repr::RplTarget { prefix, .. } => 2 + 2 + prefix.0.len(), - Repr::TransitInformation { parent_address, .. } => { - 2 + 4 + if parent_address.is_some() { 16 } else { 0 } - } - Repr::SolicitedInformation { .. } => 2 + 2 + 16 + 1, - Repr::PrefixInformation { .. } => 32, - Repr::RplTargetDescriptor { .. } => 2 + 4, - } - } - - pub fn emit + AsMut<[u8]> + ?Sized>(&self, packet: &mut Packet<&'p mut T>) { - let mut option_length = self.buffer_len() as u8; - - packet.set_option_type(self.into()); - - if !matches!(self, Repr::Pad1) { - option_length -= 2; - packet.set_option_length(option_length); - } - - match self { - Repr::Pad1 => {} - Repr::PadN(size) => { - packet.clear_padn(*size); - } - Repr::DagMetricContainer => { - unimplemented!(); - } - Repr::RouteInformation { - prefix_length, - preference, - lifetime, - prefix, - } => { - packet.clear_route_info_reserved(); - packet.set_route_info_prefix_length(*prefix_length); - packet.set_route_info_route_preference(*preference); - packet.set_route_info_route_lifetime(*lifetime); - packet.set_route_info_prefix(prefix); - } - Repr::DodagConfiguration { - authentication_enabled, - path_control_size, - dio_interval_doublings, - dio_interval_min, - dio_redundancy_constant, - max_rank_increase, - minimum_hop_rank_increase, - objective_code_point, - default_lifetime, - lifetime_unit, - } => { - packet.clear_dodag_conf_flags(); - packet.set_dodag_conf_authentication_enabled(*authentication_enabled); - packet.set_dodag_conf_path_control_size(*path_control_size); - packet.set_dodag_conf_dio_interval_doublings(*dio_interval_doublings); - packet.set_dodag_conf_dio_interval_minimum(*dio_interval_min); - packet.set_dodag_conf_dio_redundancy_constant(*dio_redundancy_constant); - packet.set_dodag_conf_max_rank_increase(*max_rank_increase); - packet.set_dodag_conf_minimum_hop_rank_increase(*minimum_hop_rank_increase); - packet.set_dodag_conf_objective_code_point(*objective_code_point); - packet.set_dodag_conf_default_lifetime(*default_lifetime); - packet.set_dodag_conf_lifetime_unit(*lifetime_unit); - } - Repr::RplTarget { - prefix_length, - prefix, - } => { - packet.clear_rpl_target_flags(); - packet.set_rpl_target_prefix_length(*prefix_length); - packet.set_rpl_target_prefix(prefix.as_bytes()); - } - Repr::TransitInformation { - external, - path_control, - path_sequence, - path_lifetime, - parent_address, - } => { - packet.clear_transit_info_flags(); - packet.set_transit_info_is_external(*external); - packet.set_transit_info_path_control(*path_control); - packet.set_transit_info_path_sequence(*path_sequence); - packet.set_transit_info_path_lifetime(*path_lifetime); - - if let Some(address) = parent_address { - packet.set_transit_info_parent_address(*address); - } - } - Repr::SolicitedInformation { - rpl_instance_id, - version_predicate, - instance_id_predicate, - dodag_id_predicate, - dodag_id, - version_number, - } => { - packet.clear_solicited_info_flags(); - packet.set_solicited_info_rpl_instance_id((*rpl_instance_id).into()); - packet.set_solicited_info_version_predicate(*version_predicate); - packet.set_solicited_info_instance_id_predicate(*instance_id_predicate); - packet.set_solicited_info_dodag_id_predicate(*dodag_id_predicate); - packet.set_solicited_info_version_number(*version_number); - packet.set_solicited_info_dodag_id(*dodag_id); - } - Repr::PrefixInformation { - prefix_length, - on_link, - autonomous_address_configuration, - router_address, - valid_lifetime, - preferred_lifetime, - destination_prefix, - } => { - packet.clear_prefix_info_reserved(); - packet.set_prefix_info_prefix_length(*prefix_length); - packet.set_prefix_info_on_link(*on_link); - packet.set_prefix_info_autonomous_address_configuration( - *autonomous_address_configuration, - ); - packet.set_prefix_info_router_address(*router_address); - packet.set_prefix_info_valid_lifetime(*valid_lifetime); - packet.set_prefix_info_preferred_lifetime(*preferred_lifetime); - packet.set_prefix_info_destination_prefix(destination_prefix); - } - Repr::RplTargetDescriptor { descriptor } => { - packet.set_rpl_target_descriptor_descriptor(*descriptor); - } - } - } - } -} - -pub mod data { - use super::{InstanceId, Result}; - use byteorder::{ByteOrder, NetworkEndian}; - - mod field { - use crate::wire::field::*; - - pub const FLAGS: usize = 0; - pub const INSTANCE_ID: usize = 1; - pub const SENDER_RANK: Field = 2..4; - } - - /// A read/write wrapper around a RPL Packet Information send with - /// an IPv6 Hop-by-Hop option, defined in RFC6553. - /// ```txt - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Option Type | Opt Data Len | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// |O|R|F|0|0|0|0|0| RPLInstanceID | SenderRank | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | (sub-TLVs) | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// ``` - #[derive(Debug, PartialEq, Eq, Clone, Copy)] - pub struct Packet> { - buffer: T, - } - - impl> Packet { - #[inline] - pub fn new_unchecked(buffer: T) -> Self { - Self { buffer } - } - - #[inline] - pub fn new_checked(buffer: T) -> Result { - let packet = Self::new_unchecked(buffer); - packet.check_len()?; - Ok(packet) - } - - #[inline] - pub fn check_len(&self) -> Result<()> { - if self.buffer.as_ref().len() == 4 { - Ok(()) - } else { - Err(crate::wire::Error) - } - } - - #[inline] - pub fn is_down(&self) -> bool { - get!(self.buffer, bool, field: field::FLAGS, shift: 7, mask: 0b1) - } - - #[inline] - pub fn has_rank_error(&self) -> bool { - get!(self.buffer, bool, field: field::FLAGS, shift: 6, mask: 0b1) - } - - #[inline] - pub fn has_forwarding_error(&self) -> bool { - get!(self.buffer, bool, field: field::FLAGS, shift: 5, mask: 0b1) - } - - #[inline] - pub fn rpl_instance_id(&self) -> InstanceId { - get!(self.buffer, into: InstanceId, field: field::INSTANCE_ID) - } - - #[inline] - pub fn sender_rank(&self) -> u16 { - get!(self.buffer, u16, field: field::SENDER_RANK) - } - } - - impl + AsMut<[u8]>> Packet { - #[inline] - pub fn set_is_down(&mut self, value: bool) { - set!(self.buffer, value, bool, field: field::FLAGS, shift: 7, mask: 0b1) - } - - #[inline] - pub fn set_has_rank_error(&mut self, value: bool) { - set!(self.buffer, value, bool, field: field::FLAGS, shift: 6, mask: 0b1) - } - - #[inline] - pub fn set_has_forwarding_error(&mut self, value: bool) { - set!(self.buffer, value, bool, field: field::FLAGS, shift: 5, mask: 0b1) - } - - #[inline] - pub fn set_rpl_instance_id(&mut self, value: u8) { - set!(self.buffer, value, field: field::INSTANCE_ID) - } - - #[inline] - pub fn set_sender_rank(&mut self, value: u16) { - set!(self.buffer, value, u16, field: field::SENDER_RANK) - } - } - - /// A high-level representation of an IPv6 Extension Header Option. - #[derive(Debug, PartialEq, Eq, Clone, Copy)] - #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub struct HopByHopOption { - pub down: bool, - pub rank_error: bool, - pub forwarding_error: bool, - pub instance_id: InstanceId, - pub sender_rank: u16, - } - - impl HopByHopOption { - /// Parse an IPv6 Extension Header Option and return a high-level representation. - pub fn parse(opt: &Packet<&T>) -> Self - where - T: AsRef<[u8]> + ?Sized, - { - Self { - down: opt.is_down(), - rank_error: opt.has_rank_error(), - forwarding_error: opt.has_forwarding_error(), - instance_id: opt.rpl_instance_id(), - sender_rank: opt.sender_rank(), - } - } - - /// Return the length of a header that will be emitted from this high-level representation. - pub const fn buffer_len(&self) -> usize { - 4 - } - - /// Emit a high-level representation into an IPv6 Extension Header Option. - pub fn emit + AsMut<[u8]> + ?Sized>(&self, opt: &mut Packet<&mut T>) { - opt.set_is_down(self.down); - opt.set_has_rank_error(self.rank_error); - opt.set_has_forwarding_error(self.forwarding_error); - opt.set_rpl_instance_id(self.instance_id.into()); - opt.set_sender_rank(self.sender_rank); - } - } - - impl core::fmt::Display for HopByHopOption { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!( - f, - "down={} rank_error={} forw_error={} IID={:?} sender_rank={}", - self.down, - self.rank_error, - self.forwarding_error, - self.instance_id, - self.sender_rank - ) - } - } -} - -#[cfg(test)] -mod tests { - use super::options::{Packet as OptionPacket, Repr as OptionRepr}; - use super::Repr as RplRepr; - use super::*; - use crate::phy::ChecksumCapabilities; - use crate::wire::{icmpv6::*, *}; - - #[test] - fn dis_packet() { - let data = [0x7a, 0x3b, 0x3a, 0x1a, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00]; - - let ll_src_address = - Ieee802154Address::Extended([0x9e, 0xd3, 0xa2, 0x9c, 0x57, 0x1a, 0x4f, 0xe4]); - let ll_dst_address = Ieee802154Address::Short([0xff, 0xff]); - - let packet = SixlowpanIphcPacket::new_checked(&data).unwrap(); - let repr = - SixlowpanIphcRepr::parse(&packet, Some(ll_src_address), Some(ll_dst_address), &[]) - .unwrap(); - - let icmp_repr = match repr.next_header { - SixlowpanNextHeader::Uncompressed(IpProtocol::Icmpv6) => { - let icmp_packet = Icmpv6Packet::new_checked(packet.payload()).unwrap(); - match Icmpv6Repr::parse( - &repr.src_addr, - &repr.dst_addr, - &icmp_packet, - &ChecksumCapabilities::ignored(), - ) { - Ok(icmp @ Icmpv6Repr::Rpl(RplRepr::DodagInformationSolicitation { .. })) => { - icmp - } - _ => unreachable!(), - } - } - _ => unreachable!(), - }; - - // We also try to emit the packet: - let mut buffer = vec![0u8; repr.buffer_len() + icmp_repr.buffer_len()]; - repr.emit(&mut SixlowpanIphcPacket::new_unchecked( - &mut buffer[..repr.buffer_len()], - )); - icmp_repr.emit( - &repr.src_addr.into(), - &repr.dst_addr.into(), - &mut Icmpv6Packet::new_unchecked( - &mut buffer[repr.buffer_len()..][..icmp_repr.buffer_len()], - ), - &ChecksumCapabilities::ignored(), - ); - - assert_eq!(&data[..], &buffer[..]); - } - - /// Parsing of DIO packets. - #[test] - fn dio_packet() { - let data = [ - 0x9b, 0x01, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x80, 0x08, 0xf0, 0x00, 0x00, 0xfd, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, - 0x04, 0x0e, 0x00, 0x08, 0x0c, 0x00, 0x04, 0x00, 0x00, 0x80, 0x00, 0x01, 0x00, 0x1e, - 0x00, 0x3c, 0x08, 0x1e, 0x40, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]; - - let addr = Address::from_bytes(&[ - 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x01, - ]); - - let dest_prefix = [ - 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - ]; - - let packet = Packet::new_checked(&data[..]).unwrap(); - assert_eq!(packet.msg_type(), Message::RplControl); - assert_eq!( - RplControlMessage::from(packet.msg_code()), - RplControlMessage::DodagInformationObject - ); - - let mut dio_repr = RplRepr::parse(&packet).unwrap(); - match dio_repr { - RplRepr::DodagInformationObject { - rpl_instance_id, - version_number, - rank, - grounded, - mode_of_operation, - dodag_preference, - dtsn, - dodag_id, - .. - } => { - assert_eq!(rpl_instance_id, InstanceId::from(0)); - assert_eq!(version_number, 240); - assert_eq!(rank, 128); - assert!(!grounded); - assert_eq!(mode_of_operation, ModeOfOperation::NonStoringMode); - assert_eq!(dodag_preference, 0); - assert_eq!(dtsn, 240); - assert_eq!(dodag_id, addr); - } - _ => unreachable!(), - } - - let option = OptionPacket::new_unchecked(packet.options().unwrap()); - let dodag_conf_option = OptionRepr::parse(&option).unwrap(); - match dodag_conf_option { - OptionRepr::DodagConfiguration { - authentication_enabled, - path_control_size, - dio_interval_doublings, - dio_interval_min, - dio_redundancy_constant, - max_rank_increase, - minimum_hop_rank_increase, - objective_code_point, - default_lifetime, - lifetime_unit, - } => { - assert!(!authentication_enabled); - assert_eq!(path_control_size, 0); - assert_eq!(dio_interval_doublings, 8); - assert_eq!(dio_interval_min, 12); - assert_eq!(dio_redundancy_constant, 0); - assert_eq!(max_rank_increase, 1024); - assert_eq!(minimum_hop_rank_increase, 128); - assert_eq!(objective_code_point, 1); - assert_eq!(default_lifetime, 30); - assert_eq!(lifetime_unit, 60); - } - _ => unreachable!(), - } - - let option = OptionPacket::new_unchecked(option.next_option().unwrap()); - let prefix_info_option = OptionRepr::parse(&option).unwrap(); - match prefix_info_option { - OptionRepr::PrefixInformation { - prefix_length, - on_link, - autonomous_address_configuration, - valid_lifetime, - preferred_lifetime, - destination_prefix, - .. - } => { - assert_eq!(prefix_length, 64); - assert!(!on_link); - assert!(autonomous_address_configuration); - assert_eq!(valid_lifetime, u32::MAX); - assert_eq!(preferred_lifetime, u32::MAX); - assert_eq!(destination_prefix, &dest_prefix[..]); - } - _ => unreachable!(), - } - - let mut options_buffer = - vec![0u8; dodag_conf_option.buffer_len() + prefix_info_option.buffer_len()]; - - dodag_conf_option.emit(&mut OptionPacket::new_unchecked( - &mut options_buffer[..dodag_conf_option.buffer_len()], - )); - prefix_info_option.emit(&mut OptionPacket::new_unchecked( - &mut options_buffer[dodag_conf_option.buffer_len()..] - [..prefix_info_option.buffer_len()], - )); - - dio_repr.set_options(&options_buffer[..]); - - let mut buffer = vec![0u8; dio_repr.buffer_len()]; - dio_repr.emit(&mut Packet::new_unchecked(&mut buffer[..])); - - assert_eq!(&data[..], &buffer[..]); - } - - /// Parsing of DAO packets. - #[test] - fn dao_packet() { - let data = [ - 0x9b, 0x02, 0x00, 0x00, 0x00, 0x80, 0x00, 0xf1, 0x05, 0x12, 0x00, 0x80, 0xfd, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, - 0x06, 0x14, 0x00, 0x00, 0x00, 0x1e, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x02, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, - ]; - - let target_prefix = [ - 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x02, 0x00, 0x02, - 0x00, 0x02, - ]; - - let parent_addr = Address::from_bytes(&[ - 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x01, - ]); - - let packet = Packet::new_checked(&data[..]).unwrap(); - let mut dao_repr = RplRepr::parse(&packet).unwrap(); - match dao_repr { - RplRepr::DestinationAdvertisementObject { - rpl_instance_id, - expect_ack, - sequence, - dodag_id, - .. - } => { - assert_eq!(rpl_instance_id, InstanceId::from(0)); - assert!(expect_ack); - assert_eq!(sequence, 241); - assert_eq!(dodag_id, None); - } - _ => unreachable!(), - } - - let option = OptionPacket::new_unchecked(packet.options().unwrap()); - - let rpl_target_option = OptionRepr::parse(&option).unwrap(); - match rpl_target_option { - OptionRepr::RplTarget { - prefix_length, - prefix, - } => { - assert_eq!(prefix_length, 128); - assert_eq!(prefix.as_bytes(), &target_prefix[..]); - } - _ => unreachable!(), - } - - let option = OptionPacket::new_unchecked(option.next_option().unwrap()); - let transit_info_option = OptionRepr::parse(&option).unwrap(); - match transit_info_option { - OptionRepr::TransitInformation { - external, - path_control, - path_sequence, - path_lifetime, - parent_address, - } => { - assert!(!external); - assert_eq!(path_control, 0); - assert_eq!(path_sequence, 0); - assert_eq!(path_lifetime, 30); - assert_eq!(parent_address, Some(parent_addr)); - } - _ => unreachable!(), - } - - let mut options_buffer = - vec![0u8; rpl_target_option.buffer_len() + transit_info_option.buffer_len()]; - - rpl_target_option.emit(&mut OptionPacket::new_unchecked( - &mut options_buffer[..rpl_target_option.buffer_len()], - )); - transit_info_option.emit(&mut OptionPacket::new_unchecked( - &mut options_buffer[rpl_target_option.buffer_len()..] - [..transit_info_option.buffer_len()], - )); - - dao_repr.set_options(&options_buffer[..]); - - let mut buffer = vec![0u8; dao_repr.buffer_len()]; - dao_repr.emit(&mut Packet::new_unchecked(&mut buffer[..])); - - assert_eq!(&data[..], &buffer[..]); - } - - /// Parsing of DAO-ACK packets. - #[test] - fn dao_ack_packet() { - let data = [0x9b, 0x03, 0x00, 0x00, 0x00, 0x00, 0xf1, 0x00]; - - let packet = Packet::new_checked(&data[..]).unwrap(); - let dao_ack_repr = RplRepr::parse(&packet).unwrap(); - match dao_ack_repr { - RplRepr::DestinationAdvertisementObjectAck { - rpl_instance_id, - sequence, - status, - dodag_id, - .. - } => { - assert_eq!(rpl_instance_id, InstanceId::from(0)); - assert_eq!(sequence, 241); - assert_eq!(status, 0); - assert_eq!(dodag_id, None); - } - _ => unreachable!(), - } - - let mut buffer = vec![0u8; dao_ack_repr.buffer_len()]; - dao_ack_repr.emit(&mut Packet::new_unchecked(&mut buffer[..])); - - assert_eq!(&data[..], &buffer[..]); - - let data = [ - 0x9b, 0x03, 0x0, 0x0, 0x1e, 0x80, 0xf0, 0x00, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - ]; - - let packet = Packet::new_checked(&data[..]).unwrap(); - let dao_ack_repr = RplRepr::parse(&packet).unwrap(); - match dao_ack_repr { - RplRepr::DestinationAdvertisementObjectAck { - rpl_instance_id, - sequence, - status, - dodag_id, - .. - } => { - assert_eq!(rpl_instance_id, InstanceId::from(30)); - assert_eq!(sequence, 240); - assert_eq!(status, 0x0); - assert_eq!( - dodag_id, - Some(Ipv6Address([ - 254, 128, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1 - ])) - ); - } - _ => unreachable!(), - } - - let mut buffer = vec![0u8; dao_ack_repr.buffer_len()]; - dao_ack_repr.emit(&mut Packet::new_unchecked(&mut buffer[..])); - - assert_eq!(&data[..], &buffer[..]); - } -} diff --git a/src/wire/rpl/hbh.rs b/src/wire/rpl/hbh.rs new file mode 100644 index 000000000..e042dfa95 --- /dev/null +++ b/src/wire/rpl/hbh.rs @@ -0,0 +1,177 @@ +use super::{InstanceId, Result}; +use byteorder::{ByteOrder, NetworkEndian}; + +mod field { + use crate::wire::field::*; + + pub const FLAGS: usize = 0; + pub const INSTANCE_ID: usize = 1; + pub const SENDER_RANK: Field = 2..4; +} + +/// A read/write wrapper around a RPL Packet Information send with +/// an IPv6 Hop-by-Hop option, defined in RFC6553. +/// ```txt +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Option Type | Opt Data Len | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// |O|R|F|0|0|0|0|0| RPLInstanceID | SenderRank | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | (sub-TLVs) | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct Packet> { + buffer: T, +} + +impl> Packet { + /// Create a raw octet buffer with a RPL Hop-by-Hop option structure. + #[inline] + pub fn new_unchecked(buffer: T) -> Self { + Self { buffer } + } + + /// Shorthand for a combination of [new_unchecked] and [check_len]. + /// + /// [new_unchecked]: #method.new_unchecked + /// [check_len]: #method.check_len + #[inline] + pub fn new_checked(buffer: T) -> Result { + let packet = Self::new_unchecked(buffer); + packet.check_len()?; + Ok(packet) + } + + /// Ensure that no accessor method will panic if called. + /// Returns `Err(Error)` if the buffer is too short. + #[inline] + pub fn check_len(&self) -> Result<()> { + if self.buffer.as_ref().len() == 4 { + Ok(()) + } else { + Err(crate::wire::Error) + } + } + + /// Consume the packet, returning the underlying buffer. + #[inline] + pub fn into_inner(self) -> T { + self.buffer + } + + /// Return the Down field. + #[inline] + pub fn is_down(&self) -> bool { + get!(self.buffer, bool, field: field::FLAGS, shift: 7, mask: 0b1) + } + + /// Return the Rank-Error field. + #[inline] + pub fn has_rank_error(&self) -> bool { + get!(self.buffer, bool, field: field::FLAGS, shift: 6, mask: 0b1) + } + + /// Return the Forwarding-Error field. + #[inline] + pub fn has_forwarding_error(&self) -> bool { + get!(self.buffer, bool, field: field::FLAGS, shift: 5, mask: 0b1) + } + + /// Return the Instance ID field. + #[inline] + pub fn rpl_instance_id(&self) -> InstanceId { + get!(self.buffer, into: InstanceId, field: field::INSTANCE_ID) + } + + /// Return the Sender Rank field. + #[inline] + pub fn sender_rank(&self) -> u16 { + get!(self.buffer, u16, field: field::SENDER_RANK) + } +} + +impl + AsMut<[u8]>> Packet { + /// Set the Down field. + #[inline] + pub fn set_is_down(&mut self, value: bool) { + set!(self.buffer, value, bool, field: field::FLAGS, shift: 7, mask: 0b1) + } + + /// Set the Rank-Error field. + #[inline] + pub fn set_has_rank_error(&mut self, value: bool) { + set!(self.buffer, value, bool, field: field::FLAGS, shift: 6, mask: 0b1) + } + + /// Set the Forwarding-Error field. + #[inline] + pub fn set_has_forwarding_error(&mut self, value: bool) { + set!(self.buffer, value, bool, field: field::FLAGS, shift: 5, mask: 0b1) + } + + /// Set the Instance ID field. + #[inline] + pub fn set_rpl_instance_id(&mut self, value: u8) { + set!(self.buffer, value, field: field::INSTANCE_ID) + } + + /// Set the Sender Rank field. + #[inline] + pub fn set_sender_rank(&mut self, value: u16) { + set!(self.buffer, value, u16, field: field::SENDER_RANK) + } +} + +/// A high-level representation of an RPL Hop-by-Hop Option. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct HopByHopOption { + pub down: bool, + pub rank_error: bool, + pub forwarding_error: bool, + pub instance_id: InstanceId, + pub sender_rank: u16, +} + +impl HopByHopOption { + /// Parse an RPL Hop-by-Hop Option and return a high-level representation. + pub fn parse(opt: &Packet<&T>) -> Self + where + T: AsRef<[u8]> + ?Sized, + { + Self { + down: opt.is_down(), + rank_error: opt.has_rank_error(), + forwarding_error: opt.has_forwarding_error(), + instance_id: opt.rpl_instance_id(), + sender_rank: opt.sender_rank(), + } + } + + /// Return the length of a header that will be emitted from this high-level representation. + pub const fn buffer_len(&self) -> usize { + 4 + } + + /// Emit a high-level representation into an RPL Hop-by-Hop Option. + pub fn emit + AsMut<[u8]> + ?Sized>(&self, opt: &mut Packet<&mut T>) { + opt.set_is_down(self.down); + opt.set_has_rank_error(self.rank_error); + opt.set_has_forwarding_error(self.forwarding_error); + opt.set_rpl_instance_id(self.instance_id.into()); + opt.set_sender_rank(self.sender_rank); + } +} + +impl core::fmt::Display for HopByHopOption { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "down={} rank_error={} forw_error={} IID={:?} sender_rank={}", + self.down, self.rank_error, self.forwarding_error, self.instance_id, self.sender_rank + ) + } +} diff --git a/src/wire/rpl/instance_id.rs b/src/wire/rpl/instance_id.rs new file mode 100644 index 000000000..833531390 --- /dev/null +++ b/src/wire/rpl/instance_id.rs @@ -0,0 +1,66 @@ +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum InstanceId { + Global(u8), + Local(u8), +} + +impl From for InstanceId { + fn from(val: u8) -> Self { + const MASK: u8 = 0b0111_1111; + + if ((val >> 7) & 0xb1) == 0b0 { + Self::Global(val & MASK) + } else { + Self::Local(val & MASK) + } + } +} + +impl From for u8 { + fn from(val: InstanceId) -> Self { + match val { + InstanceId::Global(val) => 0b0000_0000 | val, + InstanceId::Local(val) => 0b1000_0000 | val, + } + } +} + +impl InstanceId { + /// Return the real part of the ID. + pub fn id(&self) -> u8 { + match self { + Self::Global(val) => *val, + Self::Local(val) => *val, + } + } + + /// Returns `true` when the DODAG ID is the destination address of the IPv6 packet. + #[inline] + pub fn dodag_is_destination(&self) -> bool { + match self { + Self::Global(_) => false, + Self::Local(val) => ((val >> 6) & 0b1) == 0b1, + } + } + + /// Returns `true` when the DODAG ID is the source address of the IPv6 packet. + /// + /// *NOTE*: this only makes sence when using a local RPL Instance ID and the packet is not a + /// RPL control message. + #[inline] + pub fn dodag_is_source(&self) -> bool { + !self.dodag_is_destination() + } + + #[inline] + pub fn is_local(&self) -> bool { + matches!(self, InstanceId::Local(_)) + } + + #[inline] + pub fn is_global(&self) -> bool { + matches!(self, InstanceId::Global(_)) + } +} diff --git a/src/wire/rpl/mod.rs b/src/wire/rpl/mod.rs new file mode 100644 index 000000000..635528cbb --- /dev/null +++ b/src/wire/rpl/mod.rs @@ -0,0 +1,1186 @@ +//! Implementation of the RPL packet formats. See [RFC 6550 § 6]. +//! +//! [RFC 6550 § 6]: https://datatracker.ietf.org/doc/html/rfc6550#section-6 + +use byteorder::{ByteOrder, NetworkEndian}; + +use super::{Error, Result}; +use crate::wire::icmpv6::Packet; +use crate::wire::ipv6::Address; + +pub mod hbh; +pub mod instance_id; +pub mod options; +pub mod sequence_counter; + +pub use instance_id::InstanceId; +pub use sequence_counter::SequenceCounter; + +mod field { + use crate::wire::field::*; + + pub const RPL_INSTANCE_ID: usize = 4; + + // DODAG information solicitation fields (DIS) + pub const DIS_FLAGS: usize = 4; + pub const DIS_RESERVED: usize = 5; + + // DODAG information object fields (DIO) + pub const DIO_VERSION_NUMBER: usize = 5; + pub const DIO_RANK: Field = 6..8; + pub const DIO_GROUNDED: usize = 8; + pub const DIO_MOP: usize = 8; + pub const DIO_PRF: usize = 8; + pub const DIO_DTSN: usize = 9; + //pub const DIO_FLAGS: usize = 10; + //pub const DIO_RESERVED: usize = 11; + pub const DIO_DODAG_ID: Field = 12..12 + 16; + + // Destination advertisement object (DAO) + pub const DAO_K: usize = 5; + pub const DAO_D: usize = 5; + //pub const DAO_FLAGS: usize = 5; + //pub const DAO_RESERVED: usize = 6; + pub const DAO_SEQUENCE: usize = 7; + pub const DAO_DODAG_ID: Field = 8..8 + 16; + + // Destination advertisement object ack (DAO-ACK) + pub const DAO_ACK_D: usize = 5; + //pub const DAO_ACK_RESERVED: usize = 5; + pub const DAO_ACK_SEQUENCE: usize = 6; + pub const DAO_ACK_STATUS: usize = 7; + pub const DAO_ACK_DODAG_ID: Field = 8..8 + 16; +} + +enum_with_unknown! { + /// RPL Control Message subtypes. + pub enum RplControlMessage(u8) { + DodagInformationSolicitation = 0x00, + DodagInformationObject = 0x01, + DestinationAdvertisementObject = 0x02, + DestinationAdvertisementObjectAck = 0x03, + SecureDodagInformationSolicitation = 0x80, + SecureDodagInformationObject = 0x81, + SecureDestinationAdvertisementObject = 0x82, + SecureDestinationAdvertisementObjectAck = 0x83, + ConsistencyCheck = 0x8a, + } +} + +impl core::fmt::Display for RplControlMessage { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RplControlMessage::DodagInformationSolicitation => { + write!(f, "DODAG information solicitation (DIS)") + } + RplControlMessage::DodagInformationObject => { + write!(f, "DODAG information object (DIO)") + } + RplControlMessage::DestinationAdvertisementObject => { + write!(f, "destination advertisement object (DAO)") + } + RplControlMessage::DestinationAdvertisementObjectAck => write!( + f, + "destination advertisement object acknowledgement (DAO-ACK)" + ), + RplControlMessage::SecureDodagInformationSolicitation => { + write!(f, "secure DODAG information solicitation (DIS)") + } + RplControlMessage::SecureDodagInformationObject => { + write!(f, "secure DODAG information object (DIO)") + } + RplControlMessage::SecureDestinationAdvertisementObject => { + write!(f, "secure destination advertisement object (DAO)") + } + RplControlMessage::SecureDestinationAdvertisementObjectAck => write!( + f, + "secure destination advertisement object acknowledgement (DAO-ACK)" + ), + RplControlMessage::ConsistencyCheck => write!(f, "consistency check (CC)"), + RplControlMessage::Unknown(id) => write!(f, "{}", id), + } + } +} + +impl> Packet { + /// Return the RPL instance ID. + #[inline] + pub fn rpl_instance_id(&self) -> InstanceId { + get!(self.buffer, into: InstanceId, field: field::RPL_INSTANCE_ID) + } +} + +impl<'p, T: AsRef<[u8]> + ?Sized> Packet<&'p T> { + /// Return a pointer to the options. + pub fn options(&self) -> Result<&'p [u8]> { + let len = self.buffer.as_ref().len(); + match RplControlMessage::from(self.msg_code()) { + RplControlMessage::DodagInformationSolicitation if len < field::DIS_RESERVED + 1 => { + return Err(Error) + } + RplControlMessage::DodagInformationObject if len < field::DIO_DODAG_ID.end => { + return Err(Error) + } + RplControlMessage::DestinationAdvertisementObject + if self.dao_dodag_id_present() && len < field::DAO_DODAG_ID.end => + { + return Err(Error) + } + RplControlMessage::DestinationAdvertisementObject if len < field::DAO_SEQUENCE + 1 => { + return Err(Error) + } + RplControlMessage::DestinationAdvertisementObjectAck + if self.dao_ack_dodag_id_present() && len < field::DAO_ACK_DODAG_ID.end => + { + return Err(Error) + } + RplControlMessage::DestinationAdvertisementObjectAck + if len < field::DAO_ACK_STATUS + 1 => + { + return Err(Error) + } + RplControlMessage::SecureDodagInformationSolicitation + | RplControlMessage::SecureDodagInformationObject + | RplControlMessage::SecureDestinationAdvertisementObject + | RplControlMessage::SecureDestinationAdvertisementObjectAck + | RplControlMessage::ConsistencyCheck => return Err(Error), + RplControlMessage::Unknown(_) => return Err(Error), + _ => {} + } + + let buffer = &self.buffer.as_ref(); + Ok(match RplControlMessage::from(self.msg_code()) { + RplControlMessage::DodagInformationSolicitation => &buffer[field::DIS_RESERVED + 1..], + RplControlMessage::DodagInformationObject => &buffer[field::DIO_DODAG_ID.end..], + RplControlMessage::DestinationAdvertisementObject if self.dao_dodag_id_present() => { + &buffer[field::DAO_DODAG_ID.end..] + } + RplControlMessage::DestinationAdvertisementObject => &buffer[field::DAO_SEQUENCE + 1..], + RplControlMessage::DestinationAdvertisementObjectAck + if self.dao_ack_dodag_id_present() => + { + &buffer[field::DAO_ACK_DODAG_ID.end..] + } + RplControlMessage::DestinationAdvertisementObjectAck => { + &buffer[field::DAO_ACK_STATUS + 1..] + } + RplControlMessage::SecureDodagInformationSolicitation + | RplControlMessage::SecureDodagInformationObject + | RplControlMessage::SecureDestinationAdvertisementObject + | RplControlMessage::SecureDestinationAdvertisementObjectAck + | RplControlMessage::ConsistencyCheck => unreachable!(), + RplControlMessage::Unknown(_) => unreachable!(), + }) + } +} + +impl + AsMut<[u8]>> Packet { + /// Set the RPL Instance ID field. + #[inline] + pub fn set_rpl_instance_id(&mut self, value: u8) { + set!(self.buffer, value, field: field::RPL_INSTANCE_ID) + } +} + +impl<'p, T: AsRef<[u8]> + AsMut<[u8]> + ?Sized> Packet<&'p mut T> { + /// Return a pointer to the options. + pub fn options_mut(&mut self) -> &mut [u8] { + match RplControlMessage::from(self.msg_code()) { + RplControlMessage::DodagInformationSolicitation => { + &mut self.buffer.as_mut()[field::DIS_RESERVED + 1..] + } + RplControlMessage::DodagInformationObject => { + &mut self.buffer.as_mut()[field::DIO_DODAG_ID.end..] + } + RplControlMessage::DestinationAdvertisementObject => { + if self.dao_dodag_id_present() { + &mut self.buffer.as_mut()[field::DAO_DODAG_ID.end..] + } else { + &mut self.buffer.as_mut()[field::DAO_SEQUENCE + 1..] + } + } + RplControlMessage::DestinationAdvertisementObjectAck => { + if self.dao_ack_dodag_id_present() { + &mut self.buffer.as_mut()[field::DAO_ACK_DODAG_ID.end..] + } else { + &mut self.buffer.as_mut()[field::DAO_ACK_STATUS + 1..] + } + } + RplControlMessage::SecureDodagInformationSolicitation + | RplControlMessage::SecureDodagInformationObject + | RplControlMessage::SecureDestinationAdvertisementObject + | RplControlMessage::SecureDestinationAdvertisementObjectAck + | RplControlMessage::ConsistencyCheck => todo!("Secure messages not supported"), + RplControlMessage::Unknown(_) => todo!(), + } + } +} + +/// Getters for the DODAG information solicitation (DIS) message. +/// +/// ```txt +/// 0 1 2 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Flags | Reserved | Option(s)... +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +impl> Packet { + /// Return the DIS flags field. + #[inline] + pub fn dis_flags(&self) -> u8 { + get!(self.buffer, field: field::DIS_FLAGS) + } + + /// Return the DIS reserved field. + #[inline] + pub fn dis_reserved(&self) -> u8 { + get!(self.buffer, field: field::DIS_RESERVED) + } +} + +/// Setters for the DODAG information solicitation (DIS) message. +impl + AsMut<[u8]>> Packet { + /// Clear the DIS flags field. + pub fn clear_dis_flags(&mut self) { + self.buffer.as_mut()[field::DIS_FLAGS] = 0; + } + + /// Clear the DIS rserved field. + pub fn clear_dis_reserved(&mut self) { + self.buffer.as_mut()[field::DIS_RESERVED] = 0; + } +} + +enum_with_unknown! { + pub enum ModeOfOperation(u8) { + NoDownwardRoutesMaintained = 0x00, + NonStoringMode = 0x01, + StoringModeWithoutMulticast = 0x02, + StoringModeWithMulticast = 0x03, + } +} + +impl Default for ModeOfOperation { + fn default() -> Self { + Self::StoringModeWithoutMulticast + } +} + +/// Getters for the DODAG information object (DIO) message. +/// +/// ```txt +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | RPLInstanceID |Version Number | Rank | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// |G|0| MOP | Prf | DTSN | Flags | Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + DODAGID + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Option(s)... +/// +-+-+-+-+-+-+-+-+ +/// ``` +impl> Packet { + /// Return the Version Number field. + #[inline] + pub fn dio_version_number(&self) -> u8 { + get!(self.buffer, field: field::DIO_VERSION_NUMBER) + } + + /// Return the Rank field. + #[inline] + pub fn dio_rank(&self) -> u16 { + get!(self.buffer, u16, field: field::DIO_RANK) + } + + /// Return the value of the Grounded flag. + #[inline] + pub fn dio_grounded(&self) -> bool { + get!(self.buffer, bool, field: field::DIO_GROUNDED, shift: 7, mask: 0b01) + } + + /// Return the mode of operation field. + #[inline] + pub fn dio_mode_of_operation(&self) -> ModeOfOperation { + get!(self.buffer, into: ModeOfOperation, field: field::DIO_MOP, shift: 3, mask: 0b111) + } + + /// Return the DODAG preference field. + #[inline] + pub fn dio_dodag_preference(&self) -> u8 { + get!(self.buffer, field: field::DIO_PRF, mask: 0b111) + } + + /// Return the destination advertisement trigger sequence number. + #[inline] + pub fn dio_dest_adv_trigger_seq_number(&self) -> u8 { + get!(self.buffer, field: field::DIO_DTSN) + } + + /// Return the DODAG id, which is an IPv6 address. + #[inline] + pub fn dio_dodag_id(&self) -> Address { + get!( + self.buffer, + into: Address, + fun: from_bytes, + field: field::DIO_DODAG_ID + ) + } +} + +/// Setters for the DODAG information object (DIO) message. +impl + AsMut<[u8]>> Packet { + /// Set the Version Number field. + #[inline] + pub fn set_dio_version_number(&mut self, value: u8) { + set!(self.buffer, value, field: field::DIO_VERSION_NUMBER) + } + + /// Set the Rank field. + #[inline] + pub fn set_dio_rank(&mut self, value: u16) { + set!(self.buffer, value, u16, field: field::DIO_RANK) + } + + /// Set the value of the Grounded flag. + #[inline] + pub fn set_dio_grounded(&mut self, value: bool) { + set!(self.buffer, value, bool, field: field::DIO_GROUNDED, shift: 7, mask: 0b01) + } + + /// Set the mode of operation field. + #[inline] + pub fn set_dio_mode_of_operation(&mut self, mode: ModeOfOperation) { + let raw = (self.buffer.as_ref()[field::DIO_MOP] & !(0b111 << 3)) | (u8::from(mode) << 3); + self.buffer.as_mut()[field::DIO_MOP] = raw; + } + + /// Set the DODAG preference field. + #[inline] + pub fn set_dio_dodag_preference(&mut self, value: u8) { + set!(self.buffer, value, field: field::DIO_PRF, mask: 0b111) + } + + /// Set the destination advertisement trigger sequence number. + #[inline] + pub fn set_dio_dest_adv_trigger_seq_number(&mut self, value: u8) { + set!(self.buffer, value, field: field::DIO_DTSN) + } + + /// Set the DODAG id, which is an IPv6 address. + #[inline] + pub fn set_dio_dodag_id(&mut self, address: Address) { + set!(self.buffer, address: address, field: field::DIO_DODAG_ID) + } +} + +/// Getters for the Destination Advertisement Object (DAO) message. +/// +/// ```txt +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | RPLInstanceID |K|D| Flags | Reserved | DAOSequence | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + DODAGID* + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Option(s)... +/// +-+-+-+-+-+-+-+-+ +/// ``` +impl> Packet { + /// Returns the Expect DAO-ACK flag. + #[inline] + pub fn dao_ack_request(&self) -> bool { + get!(self.buffer, bool, field: field::DAO_K, shift: 7, mask: 0b1) + } + + /// Returns the flag indicating that the DODAG ID is present or not. + #[inline] + pub fn dao_dodag_id_present(&self) -> bool { + get!(self.buffer, bool, field: field::DAO_D, shift: 6, mask: 0b1) + } + + /// Returns the DODAG sequence flag. + #[inline] + pub fn dao_dodag_sequence(&self) -> u8 { + get!(self.buffer, field: field::DAO_SEQUENCE) + } + + /// Returns the DODAG ID, an IPv6 address, when it is present. + #[inline] + pub fn dao_dodag_id(&self) -> Option
{ + if self.dao_dodag_id_present() { + Some(Address::from_bytes( + &self.buffer.as_ref()[field::DAO_DODAG_ID], + )) + } else { + None + } + } +} + +/// Setters for the Destination Advertisement Object (DAO) message. +impl + AsMut<[u8]>> Packet { + /// Set the Expect DAO-ACK flag. + #[inline] + pub fn set_dao_ack_request(&mut self, value: bool) { + set!(self.buffer, value, bool, field: field::DAO_K, shift: 7, mask: 0b1,) + } + + /// Set the flag indicating that the DODAG ID is present or not. + #[inline] + pub fn set_dao_dodag_id_present(&mut self, value: bool) { + set!(self.buffer, value, bool, field: field::DAO_D, shift: 6, mask: 0b1) + } + + /// Set the DODAG sequence flag. + #[inline] + pub fn set_dao_dodag_sequence(&mut self, value: u8) { + set!(self.buffer, value, field: field::DAO_SEQUENCE) + } + + /// Set the DODAG ID. + #[inline] + pub fn set_dao_dodag_id(&mut self, address: Option
) { + match address { + Some(address) => { + self.buffer.as_mut()[field::DAO_DODAG_ID].copy_from_slice(address.as_bytes()); + self.set_dao_dodag_id_present(true); + } + None => { + self.set_dao_dodag_id_present(false); + } + } + } +} + +/// Getters for the Destination Advertisement Object acknowledgement (DAO-ACK) message. +/// +/// ```txt +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | RPLInstanceID |D| Reserved | DAOSequence | Status | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + DODAGID* + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Option(s)... +/// +-+-+-+-+-+-+-+-+ +/// ``` +impl> Packet { + /// Returns the flag indicating that the DODAG ID is present or not. + #[inline] + pub fn dao_ack_dodag_id_present(&self) -> bool { + get!(self.buffer, bool, field: field::DAO_ACK_D, shift: 7, mask: 0b1) + } + + /// Return the DODAG sequence number. + #[inline] + pub fn dao_ack_sequence(&self) -> u8 { + get!(self.buffer, field: field::DAO_ACK_SEQUENCE) + } + + /// Return the DOA status field. + #[inline] + pub fn dao_ack_status(&self) -> u8 { + get!(self.buffer, field: field::DAO_ACK_STATUS) + } + + /// Returns the DODAG ID, an IPv6 address, when it is present. + #[inline] + pub fn dao_ack_dodag_id(&self) -> Option
{ + if self.dao_ack_dodag_id_present() { + Some(Address::from_bytes( + &self.buffer.as_ref()[field::DAO_ACK_DODAG_ID], + )) + } else { + None + } + } +} + +/// Setters for the Destination Advertisement Object acknowledgement (DAO-ACK) message. +impl + AsMut<[u8]>> Packet { + /// Set the flag indicating that the DODAG ID is present or not. + #[inline] + pub fn set_dao_ack_dodag_id_present(&mut self, value: bool) { + set!(self.buffer, value, bool, field: field::DAO_ACK_D, shift: 7, mask: 0b1) + } + + /// Set the DODAG sequence number. + #[inline] + pub fn set_dao_ack_sequence(&mut self, value: u8) { + set!(self.buffer, value, field: field::DAO_ACK_SEQUENCE) + } + + /// Set the DOA status field. + #[inline] + pub fn set_dao_ack_status(&mut self, value: u8) { + set!(self.buffer, value, field: field::DAO_ACK_STATUS) + } + + /// Set the DODAG ID. + #[inline] + pub fn set_dao_ack_dodag_id(&mut self, address: Option
) { + match address { + Some(address) => { + self.buffer.as_mut()[field::DAO_ACK_DODAG_ID].copy_from_slice(address.as_bytes()); + self.set_dao_ack_dodag_id_present(true); + } + None => { + self.set_dao_ack_dodag_id_present(false); + } + } + } +} + +type RplOptions<'p> = heapless::Vec, { crate::config::RPL_MAX_OPTIONS }>; + +/// A high-level representation of a RPL control packet. +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Repr<'p> { + DodagInformationSolicitation(DodagInformationSolicitation<'p>), + DodagInformationObject(DodagInformationObject<'p>), + DestinationAdvertisementObject(DestinationAdvertisementObject<'p>), + DestinationAdvertisementObjectAck(DestinationAdvertisementObjectAck), +} + +/// A high-level representation of a RPL DODAG Information Solicitation (DIS). +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DodagInformationSolicitation<'p> { + pub options: RplOptions<'p>, +} + +/// A high-level representation of a RPL DODAG Information Object (DIO). +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DodagInformationObject<'p> { + pub rpl_instance_id: InstanceId, + pub version_number: SequenceCounter, + pub rank: u16, + pub grounded: bool, + pub mode_of_operation: ModeOfOperation, + pub dodag_preference: u8, + pub dtsn: SequenceCounter, + pub dodag_id: Address, + pub options: RplOptions<'p>, +} + +/// A high-level representation of a RPL Destination Advertisement Object (DAO). +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DestinationAdvertisementObject<'p> { + pub rpl_instance_id: InstanceId, + pub expect_ack: bool, + pub sequence: SequenceCounter, + pub dodag_id: Option
, + pub options: RplOptions<'p>, +} + +/// A high-level representation of a RPL Destination Advertisement Object Acknowledgement +/// (DAO-ACK). +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DestinationAdvertisementObjectAck { + pub rpl_instance_id: InstanceId, + pub sequence: SequenceCounter, + pub status: u8, + pub dodag_id: Option
, +} + +impl core::fmt::Display for Repr<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Repr::DodagInformationSolicitation { .. } => { + write!(f, "DIS")?; + } + Repr::DodagInformationObject(DodagInformationObject { + rpl_instance_id, + version_number, + rank, + grounded, + mode_of_operation, + dodag_preference, + dtsn, + dodag_id, + .. + }) => { + write!( + f, + "{rpl_instance_id:?} V={version_number} R={rank} G={grounded} \ + MOP={mode_of_operation:?} Pref={dodag_preference} \ + DTSN={dtsn} DODAGID={dodag_id}" + )?; + } + Repr::DestinationAdvertisementObject(DestinationAdvertisementObject { + rpl_instance_id, + expect_ack, + sequence, + dodag_id, + .. + }) => { + write!( + f, + "DAO IID={rpl_instance_id:?} Ack={expect_ack} Seq={sequence} \ + DODAGID={dodag_id:?}" + )?; + } + Repr::DestinationAdvertisementObjectAck(DestinationAdvertisementObjectAck { + rpl_instance_id, + sequence, + status, + dodag_id, + .. + }) => { + write!( + f, + "DAO-ACK IID={rpl_instance_id:?} Seq={sequence} Status={status} \ + DODAGID={dodag_id:?}" + )?; + } + }; + + Ok(()) + } +} + +impl<'p> Repr<'p> { + pub fn set_options(&mut self, options: RplOptions<'p>) { + let opts = match self { + Repr::DodagInformationSolicitation(DodagInformationSolicitation { options }) => options, + Repr::DodagInformationObject(DodagInformationObject { options, .. }) => options, + Repr::DestinationAdvertisementObject(DestinationAdvertisementObject { + options, + .. + }) => options, + Repr::DestinationAdvertisementObjectAck { .. } => unreachable!(), + }; + + *opts = options; + } + + /// Parse a RPL packet and return a high-level representation. + pub fn parse + ?Sized>(packet: &Packet<&'p T>) -> Result { + packet.check_len()?; + + let mut options = heapless::Vec::new(); + + let iter = options::OptionsIterator::new(packet.options()?); + for opt in iter { + let opt = opt?; + options.push(opt).unwrap(); + } + + match RplControlMessage::from(packet.msg_code()) { + RplControlMessage::DodagInformationSolicitation => Ok( + Repr::DodagInformationSolicitation(DodagInformationSolicitation { options }), + ), + RplControlMessage::DodagInformationObject => { + Ok(Repr::DodagInformationObject(DodagInformationObject { + rpl_instance_id: packet.rpl_instance_id(), + version_number: packet.dio_version_number().into(), + rank: packet.dio_rank(), + grounded: packet.dio_grounded(), + mode_of_operation: packet.dio_mode_of_operation(), + dodag_preference: packet.dio_dodag_preference(), + dtsn: packet.dio_dest_adv_trigger_seq_number().into(), + dodag_id: packet.dio_dodag_id(), + options, + })) + } + RplControlMessage::DestinationAdvertisementObject => Ok( + Repr::DestinationAdvertisementObject(DestinationAdvertisementObject { + rpl_instance_id: packet.rpl_instance_id(), + expect_ack: packet.dao_ack_request(), + sequence: packet.dao_dodag_sequence().into(), + dodag_id: packet.dao_dodag_id(), + options, + }), + ), + RplControlMessage::DestinationAdvertisementObjectAck => Ok( + Repr::DestinationAdvertisementObjectAck(DestinationAdvertisementObjectAck { + rpl_instance_id: packet.rpl_instance_id(), + sequence: packet.dao_ack_sequence().into(), + status: packet.dao_ack_status(), + dodag_id: packet.dao_ack_dodag_id(), + }), + ), + RplControlMessage::SecureDodagInformationSolicitation + | RplControlMessage::SecureDodagInformationObject + | RplControlMessage::SecureDestinationAdvertisementObject + | RplControlMessage::SecureDestinationAdvertisementObjectAck + | RplControlMessage::ConsistencyCheck => Err(Error), + RplControlMessage::Unknown(_) => Err(Error), + } + } + + /// Return the length of a header that will be emitted from this high-level representation. + /// The length also contains the lengths of the emitted options. + pub fn buffer_len(&self) -> usize { + let mut len = 4 + match self { + Repr::DodagInformationSolicitation { .. } => 2, + Repr::DodagInformationObject { .. } => 24, + Repr::DestinationAdvertisementObject(DestinationAdvertisementObject { + dodag_id, + .. + }) => { + if dodag_id.is_some() { + 20 + } else { + 4 + } + } + Repr::DestinationAdvertisementObjectAck(DestinationAdvertisementObjectAck { + dodag_id, + .. + }) => { + if dodag_id.is_some() { + 20 + } else { + 4 + } + } + }; + + let opts = match self { + Repr::DodagInformationSolicitation(DodagInformationSolicitation { options }) => { + &options[..] + } + Repr::DodagInformationObject(DodagInformationObject { options, .. }) => &options[..], + Repr::DestinationAdvertisementObject(DestinationAdvertisementObject { + options, + .. + }) => &options[..], + Repr::DestinationAdvertisementObjectAck(DestinationAdvertisementObjectAck { + .. + }) => &[], + }; + + len += opts.iter().map(|o| o.buffer_len()).sum::(); + + len + } + + /// Emit a high-level representation into an RPL packet. This also emits the options the + /// high-level representation contains. + pub fn emit + AsMut<[u8]> + ?Sized>(&self, packet: &mut Packet<&mut T>) { + packet.set_msg_type(crate::wire::icmpv6::Message::RplControl); + + match self { + Repr::DodagInformationSolicitation { .. } => { + packet.set_msg_code(RplControlMessage::DodagInformationSolicitation.into()); + packet.clear_dis_flags(); + packet.clear_dis_reserved(); + } + Repr::DodagInformationObject(DodagInformationObject { + rpl_instance_id, + version_number, + rank, + grounded, + mode_of_operation, + dodag_preference, + dtsn, + dodag_id, + .. + }) => { + packet.set_msg_code(RplControlMessage::DodagInformationObject.into()); + packet.set_rpl_instance_id((*rpl_instance_id).into()); + packet.set_dio_version_number(version_number.value()); + packet.set_dio_rank(*rank); + packet.set_dio_grounded(*grounded); + packet.set_dio_mode_of_operation(*mode_of_operation); + packet.set_dio_dodag_preference(*dodag_preference); + packet.set_dio_dest_adv_trigger_seq_number(dtsn.value()); + packet.set_dio_dodag_id(*dodag_id); + } + Repr::DestinationAdvertisementObject(DestinationAdvertisementObject { + rpl_instance_id, + expect_ack, + sequence, + dodag_id, + .. + }) => { + packet.set_msg_code(RplControlMessage::DestinationAdvertisementObject.into()); + packet.set_rpl_instance_id((*rpl_instance_id).into()); + packet.set_dao_ack_request(*expect_ack); + packet.set_dao_dodag_sequence(sequence.value()); + packet.set_dao_dodag_id(*dodag_id); + } + Repr::DestinationAdvertisementObjectAck(DestinationAdvertisementObjectAck { + rpl_instance_id, + sequence, + status, + dodag_id, + .. + }) => { + packet.set_msg_code(RplControlMessage::DestinationAdvertisementObjectAck.into()); + packet.set_rpl_instance_id((*rpl_instance_id).into()); + packet.set_dao_ack_sequence(sequence.value()); + packet.set_dao_ack_status(*status); + packet.set_dao_ack_dodag_id(*dodag_id); + } + } + + let options = match self { + Repr::DodagInformationSolicitation(DodagInformationSolicitation { options }) => { + &options[..] + } + Repr::DodagInformationObject(DodagInformationObject { options, .. }) => &options[..], + Repr::DestinationAdvertisementObject(DestinationAdvertisementObject { + options, + .. + }) => &options[..], + Repr::DestinationAdvertisementObjectAck { .. } => &[], + }; + + let mut buffer = packet.options_mut(); + for opt in options { + let len = opt.buffer_len(); + opt.emit(&mut options::Packet::new_unchecked(&mut buffer[..len])); + buffer = &mut buffer[len..]; + } + } +} + +#[cfg(test)] +mod tests { + use super::options::{ + DodagConfiguration, Packet as OptionPacket, PrefixInformation, Repr as OptionRepr, + }; + use super::Repr as RplRepr; + use super::*; + use crate::phy::ChecksumCapabilities; + use crate::wire::rpl::options::TransitInformation; + use crate::wire::{icmpv6::*, *}; + + #[test] + fn dis_packet() { + let data = [0x7a, 0x3b, 0x3a, 0x1a, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00]; + + let ll_src_address = + Ieee802154Address::Extended([0x9e, 0xd3, 0xa2, 0x9c, 0x57, 0x1a, 0x4f, 0xe4]); + let ll_dst_address = Ieee802154Address::Short([0xff, 0xff]); + + let packet = SixlowpanIphcPacket::new_checked(&data).unwrap(); + let repr = + SixlowpanIphcRepr::parse(&packet, Some(ll_src_address), Some(ll_dst_address), &[]) + .unwrap(); + + let icmp_repr = match repr.next_header { + SixlowpanNextHeader::Uncompressed(IpProtocol::Icmpv6) => { + let icmp_packet = Icmpv6Packet::new_checked(packet.payload()).unwrap(); + match Icmpv6Repr::parse( + &repr.src_addr, + &repr.dst_addr, + &icmp_packet, + &ChecksumCapabilities::ignored(), + ) { + Ok(icmp @ Icmpv6Repr::Rpl(RplRepr::DodagInformationSolicitation { .. })) => { + icmp + } + _ => unreachable!(), + } + } + _ => unreachable!(), + }; + + // We also try to emit the packet: + let mut buffer = vec![0u8; repr.buffer_len() + icmp_repr.buffer_len()]; + repr.emit(&mut SixlowpanIphcPacket::new_unchecked( + &mut buffer[..repr.buffer_len()], + )); + icmp_repr.emit( + &repr.src_addr, + &repr.dst_addr, + &mut Icmpv6Packet::new_unchecked( + &mut buffer[repr.buffer_len()..][..icmp_repr.buffer_len()], + ), + &ChecksumCapabilities::ignored(), + ); + + assert_eq!(&data[..], &buffer[..]); + } + + /// Parsing of DIO packets. + #[test] + fn dio_packet() { + let data = [ + 0x9b, 0x01, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x80, 0x08, 0xf0, 0x00, 0x00, 0xfd, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x04, 0x0e, 0x00, 0x08, 0x0c, 0x00, 0x04, 0x00, 0x00, 0x80, 0x00, 0x01, 0x00, 0x1e, + 0x00, 0x3c, 0x08, 0x1e, 0x40, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + let addr = Address::from_bytes(&[ + 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x01, + ]); + + let dest_prefix = [ + 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ]; + + let packet = Packet::new_checked(&data[..]).unwrap(); + assert_eq!(packet.msg_type(), Message::RplControl); + assert_eq!( + RplControlMessage::from(packet.msg_code()), + RplControlMessage::DodagInformationObject + ); + + let mut dio_repr = RplRepr::parse(&packet).unwrap(); + match dio_repr { + RplRepr::DodagInformationObject(DodagInformationObject { + rpl_instance_id, + version_number, + rank, + grounded, + mode_of_operation, + dodag_preference, + dtsn, + dodag_id, + .. + }) => { + assert_eq!(rpl_instance_id, InstanceId::from(0)); + assert_eq!(version_number, 240.into()); + assert_eq!(rank, 128); + assert!(!grounded); + assert_eq!(mode_of_operation, ModeOfOperation::NonStoringMode); + assert_eq!(dodag_preference, 0); + assert_eq!(dtsn, 240.into()); + assert_eq!(dodag_id, addr); + } + _ => unreachable!(), + } + + let option = OptionPacket::new_unchecked(packet.options().unwrap()); + let dodag_conf_option = OptionRepr::parse(&option).unwrap(); + match dodag_conf_option { + OptionRepr::DodagConfiguration(DodagConfiguration { + authentication_enabled, + path_control_size, + dio_interval_doublings, + dio_interval_min, + dio_redundancy_constant, + max_rank_increase, + minimum_hop_rank_increase, + objective_code_point, + default_lifetime, + lifetime_unit, + }) => { + assert!(!authentication_enabled); + assert_eq!(path_control_size, 0); + assert_eq!(dio_interval_doublings, 8); + assert_eq!(dio_interval_min, 12); + assert_eq!(dio_redundancy_constant, 0); + assert_eq!(max_rank_increase, 1024); + assert_eq!(minimum_hop_rank_increase, 128); + assert_eq!(objective_code_point, 1); + assert_eq!(default_lifetime, 30); + assert_eq!(lifetime_unit, 60); + } + _ => unreachable!(), + } + + let option = OptionPacket::new_unchecked(option.next_option().unwrap()); + let prefix_info_option = OptionRepr::parse(&option).unwrap(); + match prefix_info_option { + OptionRepr::PrefixInformation(PrefixInformation { + prefix_length, + on_link, + autonomous_address_configuration, + valid_lifetime, + preferred_lifetime, + destination_prefix, + .. + }) => { + assert_eq!(prefix_length, 64); + assert!(!on_link); + assert!(autonomous_address_configuration); + assert_eq!(valid_lifetime, u32::MAX); + assert_eq!(preferred_lifetime, u32::MAX); + assert_eq!(destination_prefix, &dest_prefix[..]); + } + _ => unreachable!(), + } + + let mut options = heapless::Vec::new(); + options.push(dodag_conf_option).unwrap(); + options.push(prefix_info_option).unwrap(); + dio_repr.set_options(options); + + let mut buffer = vec![0u8; dio_repr.buffer_len()]; + dio_repr.emit(&mut Packet::new_unchecked(&mut buffer[..])); + + assert_eq!(&data[..], &buffer[..]); + } + + /// Parsing of DAO packets. + #[test] + fn dao_packet() { + let data = [ + 0x9b, 0x02, 0x00, 0x00, 0x00, 0x80, 0x00, 0xf1, 0x05, 0x12, 0x00, 0x80, 0xfd, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, + 0x06, 0x14, 0x00, 0x00, 0x00, 0x1e, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + ]; + + let target_prefix = [ + 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x02, 0x00, 0x02, + 0x00, 0x02, + ]; + + let parent_addr = Address::from_bytes(&[ + 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x01, + ]); + + let packet = Packet::new_checked(&data[..]).unwrap(); + let mut dao_repr = RplRepr::parse(&packet).unwrap(); + match dao_repr { + RplRepr::DestinationAdvertisementObject(DestinationAdvertisementObject { + rpl_instance_id, + expect_ack, + sequence, + dodag_id, + .. + }) => { + assert_eq!(rpl_instance_id, InstanceId::from(0)); + assert!(expect_ack); + assert_eq!(sequence, 241.into()); + assert_eq!(dodag_id, None); + } + _ => unreachable!(), + } + + let option = OptionPacket::new_unchecked(packet.options().unwrap()); + + let rpl_target_option = OptionRepr::parse(&option).unwrap(); + match rpl_target_option { + OptionRepr::RplTarget(RplTarget { + prefix_length, + ref prefix, + }) => { + assert_eq!(prefix_length, 128); + assert_eq!(prefix, &target_prefix[..]); + } + _ => unreachable!(), + } + + let option = OptionPacket::new_unchecked(option.next_option().unwrap()); + let transit_info_option = OptionRepr::parse(&option).unwrap(); + match transit_info_option { + OptionRepr::TransitInformation(TransitInformation { + external, + path_control, + path_sequence, + path_lifetime, + parent_address, + }) => { + assert!(!external); + assert_eq!(path_control, 0); + assert_eq!(path_sequence, 0); + assert_eq!(path_lifetime, 30); + assert_eq!(parent_address, Some(parent_addr)); + } + _ => unreachable!(), + } + + let mut options = heapless::Vec::new(); + options.push(rpl_target_option).unwrap(); + options.push(transit_info_option).unwrap(); + dao_repr.set_options(options); + + let mut buffer = vec![0u8; dao_repr.buffer_len()]; + dao_repr.emit(&mut Packet::new_unchecked(&mut buffer[..])); + + assert_eq!(&data[..], &buffer[..]); + } + + /// Parsing of DAO-ACK packets. + #[test] + fn dao_ack_packet() { + let data = [0x9b, 0x03, 0x00, 0x00, 0x00, 0x00, 0xf1, 0x00]; + + let packet = Packet::new_checked(&data[..]).unwrap(); + let dao_ack_repr = RplRepr::parse(&packet).unwrap(); + match dao_ack_repr { + RplRepr::DestinationAdvertisementObjectAck(DestinationAdvertisementObjectAck { + rpl_instance_id, + sequence, + status, + dodag_id, + .. + }) => { + assert_eq!(rpl_instance_id, InstanceId::from(0)); + assert_eq!(sequence, 241.into()); + assert_eq!(status, 0); + assert_eq!(dodag_id, None); + } + _ => unreachable!(), + } + + let mut buffer = vec![0u8; dao_ack_repr.buffer_len()]; + dao_ack_repr.emit(&mut Packet::new_unchecked(&mut buffer[..])); + + assert_eq!(&data[..], &buffer[..]); + + let data = [ + 0x9b, 0x03, 0x0, 0x0, 0x1e, 0x80, 0xf0, 0x00, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + + let packet = Packet::new_checked(&data[..]).unwrap(); + let dao_ack_repr = RplRepr::parse(&packet).unwrap(); + match dao_ack_repr { + RplRepr::DestinationAdvertisementObjectAck(DestinationAdvertisementObjectAck { + rpl_instance_id, + sequence, + status, + dodag_id, + .. + }) => { + assert_eq!(rpl_instance_id, InstanceId::from(30)); + assert_eq!(sequence, 240.into()); + assert_eq!(status, 0x0); + assert_eq!( + dodag_id, + Some(Ipv6Address([ + 254, 128, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1 + ])) + ); + } + _ => unreachable!(), + } + + let mut buffer = vec![0u8; dao_ack_repr.buffer_len()]; + dao_ack_repr.emit(&mut Packet::new_unchecked(&mut buffer[..])); + + assert_eq!(&data[..], &buffer[..]); + } +} diff --git a/src/wire/rpl/options.rs b/src/wire/rpl/options.rs new file mode 100644 index 000000000..e4564909e --- /dev/null +++ b/src/wire/rpl/options.rs @@ -0,0 +1,1440 @@ +use byteorder::{ByteOrder, NetworkEndian}; + +use super::{Error, InstanceId, Result, SequenceCounter}; +use crate::wire::ipv6::Address; + +/// A read/write wrapper around a RPL Control Message Option. +#[derive(Debug, Clone)] +pub struct Packet> { + buffer: T, +} + +enum_with_unknown! { + pub enum OptionType(u8) { + Pad1 = 0x00, + PadN = 0x01, + DagMetricContainer = 0x02, + RouteInformation = 0x03, + DodagConfiguration = 0x04, + RplTarget = 0x05, + TransitInformation = 0x06, + SolicitedInformation = 0x07, + PrefixInformation = 0x08, + RplTargetDescriptor = 0x09, + } +} + +impl From<&Repr<'_>> for OptionType { + fn from(repr: &Repr) -> Self { + match repr { + Repr::Pad1 => Self::Pad1, + Repr::PadN(_) => Self::PadN, + Repr::DagMetricContainer => Self::DagMetricContainer, + Repr::RouteInformation { .. } => Self::RouteInformation, + Repr::DodagConfiguration { .. } => Self::DodagConfiguration, + Repr::RplTarget { .. } => Self::RplTarget, + Repr::TransitInformation { .. } => Self::TransitInformation, + Repr::SolicitedInformation { .. } => Self::SolicitedInformation, + Repr::PrefixInformation { .. } => Self::PrefixInformation, + Repr::RplTargetDescriptor { .. } => Self::RplTargetDescriptor, + } + } +} + +mod field { + use crate::wire::field::*; + + // Generic fields. + pub const TYPE: usize = 0; + pub const LENGTH: usize = 1; + + pub const PADN: Rest = 2..; + + // Route Information fields. + pub const ROUTE_INFO_PREFIX_LENGTH: usize = 2; + pub const ROUTE_INFO_RESERVED: usize = 3; + pub const ROUTE_INFO_PREFERENCE: usize = 3; + pub const ROUTE_INFO_LIFETIME: Field = 4..9; + + // DODAG Configuration fields. + pub const DODAG_CONF_FLAGS: usize = 2; + pub const DODAG_CONF_AUTHENTICATION_ENABLED: usize = 2; + pub const DODAG_CONF_PATH_CONTROL_SIZE: usize = 2; + pub const DODAG_CONF_DIO_INTERVAL_DOUBLINGS: usize = 3; + pub const DODAG_CONF_DIO_INTERVAL_MINIMUM: usize = 4; + pub const DODAG_CONF_DIO_REDUNDANCY_CONSTANT: usize = 5; + pub const DODAG_CONF_DIO_MAX_RANK_INCREASE: Field = 6..8; + pub const DODAG_CONF_MIN_HOP_RANK_INCREASE: Field = 8..10; + pub const DODAG_CONF_OBJECTIVE_CODE_POINT: Field = 10..12; + pub const DODAG_CONF_DEFAULT_LIFETIME: usize = 13; + pub const DODAG_CONF_LIFETIME_UNIT: Field = 14..16; + + // RPL Target fields. + pub const RPL_TARGET_FLAGS: usize = 2; + pub const RPL_TARGET_PREFIX_LENGTH: usize = 3; + + // Transit Information fields. + pub const TRANSIT_INFO_FLAGS: usize = 2; + pub const TRANSIT_INFO_EXTERNAL: usize = 2; + pub const TRANSIT_INFO_PATH_CONTROL: usize = 3; + pub const TRANSIT_INFO_PATH_SEQUENCE: usize = 4; + pub const TRANSIT_INFO_PATH_LIFETIME: usize = 5; + pub const TRANSIT_INFO_PARENT_ADDRESS: Field = 6..6 + 16; + + // Solicited Information fields. + pub const SOLICITED_INFO_RPL_INSTANCE_ID: usize = 2; + pub const SOLICITED_INFO_FLAGS: usize = 3; + pub const SOLICITED_INFO_VERSION_PREDICATE: usize = 3; + pub const SOLICITED_INFO_INSTANCE_ID_PREDICATE: usize = 3; + pub const SOLICITED_INFO_DODAG_ID_PREDICATE: usize = 3; + pub const SOLICITED_INFO_DODAG_ID: Field = 4..20; + pub const SOLICITED_INFO_VERSION_NUMBER: usize = 20; + + // Prefix Information fields. + pub const PREFIX_INFO_PREFIX_LENGTH: usize = 2; + pub const PREFIX_INFO_RESERVED1: usize = 3; + pub const PREFIX_INFO_ON_LINK: usize = 3; + pub const PREFIX_INFO_AUTONOMOUS_CONF: usize = 3; + pub const PREFIX_INFO_ROUTER_ADDRESS_FLAG: usize = 3; + pub const PREFIX_INFO_VALID_LIFETIME: Field = 4..8; + pub const PREFIX_INFO_PREFERRED_LIFETIME: Field = 8..12; + pub const PREFIX_INFO_RESERVED2: Field = 12..16; + pub const PREFIX_INFO_PREFIX: Field = 16..16 + 16; + + // RPL Target Descriptor fields. + pub const TARGET_DESCRIPTOR: Field = 2..6; +} + +/// Getters for the RPL Control Message Options. +impl> Packet { + /// Create a raw octet buffer with RPL Control Message Option structure. + #[inline] + pub fn new_unchecked(buffer: T) -> Self { + Self { buffer } + } + + /// Shorthand for a combination of [new_checked] and [check_len]. + /// + /// [new_unchecked]: #method.new_unchecked + /// [check_len]: #method.check_len + #[inline] + pub fn new_checked(buffer: T) -> Result { + let packet = Self::new_unchecked(buffer); + packet.check_len()?; + Ok(packet) + } + + /// Ensure that no accessor method will panic if called. + /// Returns `Err(Error)` if the buffer is too short. + #[inline] + pub fn check_len(&self) -> Result<()> { + if self.buffer.as_ref().is_empty() { + return Err(Error); + } + + Ok(()) + } + + /// Return the type field. + #[inline] + pub fn option_type(&self) -> OptionType { + OptionType::from(self.buffer.as_ref()[field::TYPE]) + } + + /// Return the length field. + #[inline] + pub fn option_length(&self) -> u8 { + get!(self.buffer, field: field::LENGTH) + } +} + +impl<'p, T: AsRef<[u8]> + ?Sized> Packet<&'p T> { + /// Return a pointer to the next option. + #[inline] + pub fn next_option(&self) -> Option<&'p [u8]> { + if !self.buffer.as_ref().is_empty() { + match self.option_type() { + OptionType::Pad1 => Some(&self.buffer.as_ref()[1..]), + OptionType::Unknown(_) => unreachable!(), + _ => { + let len = self.option_length(); + Some(&self.buffer.as_ref()[2 + len as usize..]) + } + } + } else { + None + } + } +} + +impl + AsMut<[u8]>> Packet { + /// Set the Option Type field. + #[inline] + pub fn set_option_type(&mut self, option_type: OptionType) { + self.buffer.as_mut()[field::TYPE] = option_type.into(); + } + + /// Set the Option Length field. + #[inline] + pub fn set_option_length(&mut self, length: u8) { + self.buffer.as_mut()[field::LENGTH] = length; + } +} + +impl + AsMut<[u8]>> Packet { + #[inline] + pub fn clear_padn(&mut self, size: u8) { + for b in &mut self.buffer.as_mut()[field::PADN][..size as usize] { + *b = 0; + } + } +} + +/// Getters for the Route Information Option Message. +/// +/// ```txt +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type = 0x03 | Option Length | Prefix Length |Resvd|Prf|Resvd| +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Route Lifetime | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// . Prefix (Variable Length) . +/// . . +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +impl> Packet { + /// Return the Prefix Length field. + #[inline] + pub fn prefix_length(&self) -> u8 { + get!(self.buffer, field: field::ROUTE_INFO_PREFIX_LENGTH) + } + + /// Return the Route Preference field. + #[inline] + pub fn route_preference(&self) -> u8 { + (self.buffer.as_ref()[field::ROUTE_INFO_PREFERENCE] & 0b0001_1000) >> 3 + } + + /// Return the Route Lifetime field. + #[inline] + pub fn route_lifetime(&self) -> u32 { + get!(self.buffer, u32, field: field::ROUTE_INFO_LIFETIME) + } +} + +impl<'p, T: AsRef<[u8]> + ?Sized> Packet<&'p T> { + /// Return the Prefix field. + #[inline] + pub fn prefix(&self) -> &'p [u8] { + let option_len = self.option_length(); + &self.buffer.as_ref()[field::ROUTE_INFO_LIFETIME.end..] + [..option_len as usize - field::ROUTE_INFO_LIFETIME.end] + } +} + +/// Setters for the Route Information Option Message. +impl + AsMut<[u8]>> Packet { + /// Set the Prefix Length field. + #[inline] + pub fn set_route_info_prefix_length(&mut self, value: u8) { + set!(self.buffer, value, field: field::ROUTE_INFO_PREFIX_LENGTH) + } + + /// Set the Route Preference field. + #[inline] + pub fn set_route_info_route_preference(&mut self, _value: u8) { + todo!(); + } + + /// Set the Route Lifetime field. + #[inline] + pub fn set_route_info_route_lifetime(&mut self, value: u32) { + set!(self.buffer, value, u32, field: field::ROUTE_INFO_LIFETIME) + } + + /// Set the prefix field. + #[inline] + pub fn set_route_info_prefix(&mut self, _prefix: &[u8]) { + todo!(); + } + + /// Clear the reserved field. + #[inline] + pub fn clear_route_info_reserved(&mut self) { + self.buffer.as_mut()[field::ROUTE_INFO_RESERVED] = 0; + } +} + +/// Getters for the DODAG Configuration Option Message. +/// +/// ```txt +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type = 0x04 |Opt Length = 14| Flags |A| PCS | DIOIntDoubl. | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | DIOIntMin. | DIORedun. | MaxRankIncrease | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | MinHopRankIncrease | OCP | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reserved | Def. Lifetime | Lifetime Unit | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +impl> Packet { + /// Return the Authentication Enabled field. + #[inline] + pub fn authentication_enabled(&self) -> bool { + get!( + self.buffer, + bool, + field: field::DODAG_CONF_AUTHENTICATION_ENABLED, + shift: 3, + mask: 0b1 + ) + } + + /// Return the Path Control Size field. + #[inline] + pub fn path_control_size(&self) -> u8 { + get!(self.buffer, field: field::DODAG_CONF_PATH_CONTROL_SIZE, mask: 0b111) + } + + /// Return the DIO Interval Doublings field. + #[inline] + pub fn dio_interval_doublings(&self) -> u8 { + get!(self.buffer, field: field::DODAG_CONF_DIO_INTERVAL_DOUBLINGS) + } + + /// Return the DIO Interval Minimum field. + #[inline] + pub fn dio_interval_minimum(&self) -> u8 { + get!(self.buffer, field: field::DODAG_CONF_DIO_INTERVAL_MINIMUM) + } + + /// Return the DIO Redundancy Constant field. + #[inline] + pub fn dio_redundancy_constant(&self) -> u8 { + get!( + self.buffer, + field: field::DODAG_CONF_DIO_REDUNDANCY_CONSTANT + ) + } + + /// Return the Max Rank Increase field. + #[inline] + pub fn max_rank_increase(&self) -> u16 { + get!( + self.buffer, + u16, + field: field::DODAG_CONF_DIO_MAX_RANK_INCREASE + ) + } + + /// Return the Minimum Hop Rank Increase field. + #[inline] + pub fn minimum_hop_rank_increase(&self) -> u16 { + get!( + self.buffer, + u16, + field: field::DODAG_CONF_MIN_HOP_RANK_INCREASE + ) + } + + /// Return the Objective Code Point field. + #[inline] + pub fn objective_code_point(&self) -> u16 { + get!( + self.buffer, + u16, + field: field::DODAG_CONF_OBJECTIVE_CODE_POINT + ) + } + + /// Return the Default Lifetime field. + #[inline] + pub fn default_lifetime(&self) -> u8 { + get!(self.buffer, field: field::DODAG_CONF_DEFAULT_LIFETIME) + } + + /// Return the Lifetime Unit field. + #[inline] + pub fn lifetime_unit(&self) -> u16 { + get!(self.buffer, u16, field: field::DODAG_CONF_LIFETIME_UNIT) + } +} + +/// Getters for the DODAG Configuration Option Message. +impl + AsMut<[u8]>> Packet { + /// Clear the Flags field. + #[inline] + pub fn clear_dodag_conf_flags(&mut self) { + self.buffer.as_mut()[field::DODAG_CONF_FLAGS] = 0; + } + + /// Set the Authentication Enabled field. + #[inline] + pub fn set_dodag_conf_authentication_enabled(&mut self, value: bool) { + set!( + self.buffer, + value, + bool, + field: field::DODAG_CONF_AUTHENTICATION_ENABLED, + shift: 3, + mask: 0b1 + ) + } + + /// Set the Path Control Size field. + #[inline] + pub fn set_dodag_conf_path_control_size(&mut self, value: u8) { + set!( + self.buffer, + value, + field: field::DODAG_CONF_PATH_CONTROL_SIZE, + mask: 0b111 + ) + } + + /// Set the DIO Interval Doublings field. + #[inline] + pub fn set_dodag_conf_dio_interval_doublings(&mut self, value: u8) { + set!( + self.buffer, + value, + field: field::DODAG_CONF_DIO_INTERVAL_DOUBLINGS + ) + } + + /// Set the DIO Interval Minimum field. + #[inline] + pub fn set_dodag_conf_dio_interval_minimum(&mut self, value: u8) { + set!( + self.buffer, + value, + field: field::DODAG_CONF_DIO_INTERVAL_MINIMUM + ) + } + + /// Set the DIO Redundancy Constant field. + #[inline] + pub fn set_dodag_conf_dio_redundancy_constant(&mut self, value: u8) { + set!( + self.buffer, + value, + field: field::DODAG_CONF_DIO_REDUNDANCY_CONSTANT + ) + } + + /// Set the Max Rank Increase field. + #[inline] + pub fn set_dodag_conf_max_rank_increase(&mut self, value: u16) { + set!( + self.buffer, + value, + u16, + field: field::DODAG_CONF_DIO_MAX_RANK_INCREASE + ) + } + + /// Set the Minimum Hop Rank Increase field. + #[inline] + pub fn set_dodag_conf_minimum_hop_rank_increase(&mut self, value: u16) { + set!( + self.buffer, + value, + u16, + field: field::DODAG_CONF_MIN_HOP_RANK_INCREASE + ) + } + + /// Set the Objective Code Point field. + #[inline] + pub fn set_dodag_conf_objective_code_point(&mut self, value: u16) { + set!( + self.buffer, + value, + u16, + field: field::DODAG_CONF_OBJECTIVE_CODE_POINT + ) + } + + /// Set the Default Lifetime field. + #[inline] + pub fn set_dodag_conf_default_lifetime(&mut self, value: u8) { + set!( + self.buffer, + value, + field: field::DODAG_CONF_DEFAULT_LIFETIME + ) + } + + /// Set the Lifetime Unit field. + #[inline] + pub fn set_dodag_conf_lifetime_unit(&mut self, value: u16) { + set!( + self.buffer, + value, + u16, + field: field::DODAG_CONF_LIFETIME_UNIT + ) + } +} + +/// Getters for the RPL Target Option Message. +/// +/// ```txt +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type = 0x05 | Option Length | Flags | Prefix Length | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | Target Prefix (Variable Length) | +/// . . +/// . . +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +impl> Packet { + /// Return the Target Prefix Length field. + pub fn target_prefix_length(&self) -> u8 { + get!(self.buffer, field: field::RPL_TARGET_PREFIX_LENGTH) + } +} + +impl<'p, T: AsRef<[u8]> + ?Sized> Packet<&'p T> { + /// Return the Target Prefix field. + #[inline] + pub fn target_prefix(&self) -> &'p [u8] { + let option_len = self.option_length(); + &self.buffer.as_ref()[field::RPL_TARGET_PREFIX_LENGTH + 1..] + [..option_len as usize - field::RPL_TARGET_PREFIX_LENGTH + 1] + } +} + +/// Setters for the RPL Target Option Message. +impl + AsMut<[u8]>> Packet { + /// Clear the Flags field. + #[inline] + pub fn clear_rpl_target_flags(&mut self) { + self.buffer.as_mut()[field::RPL_TARGET_FLAGS] = 0; + } + + /// Set the Target Prefix Length field. + #[inline] + pub fn set_rpl_target_prefix_length(&mut self, value: u8) { + set!(self.buffer, value, field: field::RPL_TARGET_PREFIX_LENGTH) + } + + /// Set the Target Prefix field. + #[inline] + pub fn set_rpl_target_prefix(&mut self, prefix: &[u8]) { + self.buffer.as_mut()[field::RPL_TARGET_PREFIX_LENGTH + 1..][..prefix.len()] + .copy_from_slice(prefix); + } +} + +/// Getters for the Transit Information Option Message. +/// +/// ```txt +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type = 0x06 | Option Length |E| Flags | Path Control | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Path Sequence | Path Lifetime | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +/// | | +/// + + +/// | | +/// + Parent Address* + +/// | | +/// + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +impl> Packet { + /// Return the External flag. + #[inline] + pub fn is_external(&self) -> bool { + get!( + self.buffer, + bool, + field: field::TRANSIT_INFO_EXTERNAL, + shift: 7, + mask: 0b1, + ) + } + + /// Return the Path Control field. + #[inline] + pub fn path_control(&self) -> u8 { + get!(self.buffer, field: field::TRANSIT_INFO_PATH_CONTROL) + } + + /// Return the Path Sequence field. + #[inline] + pub fn path_sequence(&self) -> u8 { + get!(self.buffer, field: field::TRANSIT_INFO_PATH_SEQUENCE) + } + + /// Return the Path Lifetime field. + #[inline] + pub fn path_lifetime(&self) -> u8 { + get!(self.buffer, field: field::TRANSIT_INFO_PATH_LIFETIME) + } + + /// Return the Parent Address field. + #[inline] + pub fn parent_address(&self) -> Option
{ + if self.option_length() > 5 { + Some(Address::from_bytes( + &self.buffer.as_ref()[field::TRANSIT_INFO_PARENT_ADDRESS], + )) + } else { + None + } + } +} + +/// Setters for the Transit Information Option Message. +impl + AsMut<[u8]>> Packet { + /// Clear the Flags field. + #[inline] + pub fn clear_transit_info_flags(&mut self) { + self.buffer.as_mut()[field::TRANSIT_INFO_FLAGS] = 0; + } + + /// Set the External flag. + #[inline] + pub fn set_transit_info_is_external(&mut self, value: bool) { + set!( + self.buffer, + value, + bool, + field: field::TRANSIT_INFO_EXTERNAL, + shift: 7, + mask: 0b1 + ) + } + + /// Set the Path Control field. + #[inline] + pub fn set_transit_info_path_control(&mut self, value: u8) { + set!(self.buffer, value, field: field::TRANSIT_INFO_PATH_CONTROL) + } + + /// Set the Path Sequence field. + #[inline] + pub fn set_transit_info_path_sequence(&mut self, value: u8) { + set!(self.buffer, value, field: field::TRANSIT_INFO_PATH_SEQUENCE) + } + + /// Set the Path Lifetime field. + #[inline] + pub fn set_transit_info_path_lifetime(&mut self, value: u8) { + set!(self.buffer, value, field: field::TRANSIT_INFO_PATH_LIFETIME) + } + + /// Set the Parent Address field. + #[inline] + pub fn set_transit_info_parent_address(&mut self, address: Address) { + self.buffer.as_mut()[field::TRANSIT_INFO_PARENT_ADDRESS] + .copy_from_slice(address.as_bytes()); + } +} + +/// Getters for the Solicited Information Option Message. +/// +/// ```txt +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type = 0x07 |Opt Length = 19| RPLInstanceID |V|I|D| Flags | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + DODAGID + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// |Version Number | +/// +-+-+-+-+-+-+-+-+ +/// ``` +impl> Packet { + /// Return the RPL Instance ID field. + #[inline] + pub fn rpl_instance_id(&self) -> u8 { + get!(self.buffer, field: field::SOLICITED_INFO_RPL_INSTANCE_ID) + } + + /// Return the Version Predicate flag. + #[inline] + pub fn version_predicate(&self) -> bool { + get!( + self.buffer, + bool, + field: field::SOLICITED_INFO_VERSION_PREDICATE, + shift: 7, + mask: 0b1, + ) + } + + /// Return the Instance ID Predicate flag. + #[inline] + pub fn instance_id_predicate(&self) -> bool { + get!( + self.buffer, + bool, + field: field::SOLICITED_INFO_INSTANCE_ID_PREDICATE, + shift: 6, + mask: 0b1, + ) + } + + /// Return the DODAG Predicate ID flag. + #[inline] + pub fn dodag_id_predicate(&self) -> bool { + get!( + self.buffer, + bool, + field: field::SOLICITED_INFO_DODAG_ID_PREDICATE, + shift: 5, + mask: 0b1, + ) + } + + /// Return the DODAG ID field. + #[inline] + pub fn dodag_id(&self) -> Address { + get!( + self.buffer, + into: Address, + fun: from_bytes, + field: field::SOLICITED_INFO_DODAG_ID + ) + } + + /// Return the Version Number field. + #[inline] + pub fn version_number(&self) -> u8 { + get!(self.buffer, field: field::SOLICITED_INFO_VERSION_NUMBER) + } +} + +/// Setters for the Solicited Information Option Message. +impl + AsMut<[u8]>> Packet { + /// Clear the Flags field. + #[inline] + pub fn clear_solicited_info_flags(&mut self) { + self.buffer.as_mut()[field::SOLICITED_INFO_FLAGS] = 0; + } + + /// Set the RPL Instance ID field. + #[inline] + pub fn set_solicited_info_rpl_instance_id(&mut self, value: u8) { + set!( + self.buffer, + value, + field: field::SOLICITED_INFO_RPL_INSTANCE_ID + ) + } + + /// Set the Version Predicate flag. + #[inline] + pub fn set_solicited_info_version_predicate(&mut self, value: bool) { + set!( + self.buffer, + value, + bool, + field: field::SOLICITED_INFO_VERSION_PREDICATE, + shift: 7, + mask: 0b1 + ) + } + + /// Set the Instance ID Predicate flag. + #[inline] + pub fn set_solicited_info_instance_id_predicate(&mut self, value: bool) { + set!( + self.buffer, + value, + bool, + field: field::SOLICITED_INFO_INSTANCE_ID_PREDICATE, + shift: 6, + mask: 0b1 + ) + } + + /// Set the DODAG Predicate ID flag. + #[inline] + pub fn set_solicited_info_dodag_id_predicate(&mut self, value: bool) { + set!( + self.buffer, + value, + bool, + field: field::SOLICITED_INFO_DODAG_ID_PREDICATE, + shift: 5, + mask: 0b1 + ) + } + + /// Set the DODAG ID field. + #[inline] + pub fn set_solicited_info_dodag_id(&mut self, address: Address) { + set!( + self.buffer, + address: address, + field: field::SOLICITED_INFO_DODAG_ID + ) + } + + /// Set the Version Number field. + #[inline] + pub fn set_solicited_info_version_number(&mut self, value: u8) { + set!( + self.buffer, + value, + field: field::SOLICITED_INFO_VERSION_NUMBER + ) + } +} + +/// Getters for the Prefix Information Option Message. +/// +/// ```txt +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type = 0x08 |Opt Length = 30| Prefix Length |L|A|R|Reserved1| +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Valid Lifetime | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Preferred Lifetime | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reserved2 | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + Prefix + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +impl> Packet { + /// Return the Prefix Length field. + #[inline] + pub fn prefix_info_prefix_length(&self) -> u8 { + get!(self.buffer, field: field::PREFIX_INFO_PREFIX_LENGTH) + } + + /// Return the On-Link flag. + #[inline] + pub fn on_link(&self) -> bool { + get!( + self.buffer, + bool, + field: field::PREFIX_INFO_ON_LINK, + shift: 7, + mask: 0b1, + ) + } + + /// Return the Autonomous Address-Configuration flag. + #[inline] + pub fn autonomous_address_configuration(&self) -> bool { + get!( + self.buffer, + bool, + field: field::PREFIX_INFO_AUTONOMOUS_CONF, + shift: 6, + mask: 0b1, + ) + } + + /// Return the Router Address flag. + #[inline] + pub fn router_address(&self) -> bool { + get!( + self.buffer, + bool, + field: field::PREFIX_INFO_ROUTER_ADDRESS_FLAG, + shift: 5, + mask: 0b1, + ) + } + + /// Return the Valid Lifetime field. + #[inline] + pub fn valid_lifetime(&self) -> u32 { + get!(self.buffer, u32, field: field::PREFIX_INFO_VALID_LIFETIME) + } + + /// Return the Preferred Lifetime field. + #[inline] + pub fn preferred_lifetime(&self) -> u32 { + get!( + self.buffer, + u32, + field: field::PREFIX_INFO_PREFERRED_LIFETIME + ) + } +} + +impl<'p, T: AsRef<[u8]> + ?Sized> Packet<&'p T> { + /// Return the Prefix field. + #[inline] + pub fn destination_prefix(&self) -> &'p [u8] { + &self.buffer.as_ref()[field::PREFIX_INFO_PREFIX] + } +} + +/// Setters for the Prefix Information Option Message. +impl + AsMut<[u8]>> Packet { + /// Clear the reserved fields. + #[inline] + pub fn clear_prefix_info_reserved(&mut self) { + self.buffer.as_mut()[field::PREFIX_INFO_RESERVED1] = 0; + self.buffer.as_mut()[field::PREFIX_INFO_RESERVED2].copy_from_slice(&[0; 4]); + } + + /// Set the Prefix Length field. + #[inline] + pub fn set_prefix_info_prefix_length(&mut self, value: u8) { + set!(self.buffer, value, field: field::PREFIX_INFO_PREFIX_LENGTH) + } + + /// Set the On-Link flag. + #[inline] + pub fn set_prefix_info_on_link(&mut self, value: bool) { + set!(self.buffer, value, bool, field: field::PREFIX_INFO_ON_LINK, shift: 7, mask: 0b1) + } + + /// Set the Autonomous Address-Configuration flag. + #[inline] + pub fn set_prefix_info_autonomous_address_configuration(&mut self, value: bool) { + set!( + self.buffer, + value, + bool, + field: field::PREFIX_INFO_AUTONOMOUS_CONF, + shift: 6, + mask: 0b1 + ) + } + + /// Set the Router Address flag. + #[inline] + pub fn set_prefix_info_router_address(&mut self, value: bool) { + set!( + self.buffer, + value, + bool, + field: field::PREFIX_INFO_ROUTER_ADDRESS_FLAG, + shift: 5, + mask: 0b1 + ) + } + + /// Set the Valid Lifetime field. + #[inline] + pub fn set_prefix_info_valid_lifetime(&mut self, value: u32) { + set!( + self.buffer, + value, + u32, + field: field::PREFIX_INFO_VALID_LIFETIME + ) + } + + /// Set the Preferred Lifetime field. + #[inline] + pub fn set_prefix_info_preferred_lifetime(&mut self, value: u32) { + set!( + self.buffer, + value, + u32, + field: field::PREFIX_INFO_PREFERRED_LIFETIME + ) + } + + /// Set the Prefix field. + #[inline] + pub fn set_prefix_info_destination_prefix(&mut self, prefix: &[u8]) { + self.buffer.as_mut()[field::PREFIX_INFO_PREFIX].copy_from_slice(prefix); + } +} + +/// Getters for the RPL Target Descriptor Option Message. +/// +/// ```txt +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type = 0x09 |Opt Length = 4 | Descriptor +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// Descriptor (cont.) | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +impl> Packet { + /// Return the Descriptor field. + #[inline] + pub fn descriptor(&self) -> u32 { + get!(self.buffer, u32, field: field::TARGET_DESCRIPTOR) + } +} + +/// Setters for the RPL Target Descriptor Option Message. +impl + AsMut<[u8]>> Packet { + /// Set the Descriptor field. + #[inline] + pub fn set_rpl_target_descriptor_descriptor(&mut self, value: u32) { + set!(self.buffer, value, u32, field: field::TARGET_DESCRIPTOR) + } +} + +/// A high-level representation of a RPL Option. +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Repr<'p> { + Pad1, + PadN(u8), + DagMetricContainer, + RouteInformation(RouteInformation<'p>), + DodagConfiguration(DodagConfiguration), + RplTarget(RplTarget), + TransitInformation(TransitInformation), + SolicitedInformation(SolicitedInformation), + PrefixInformation(PrefixInformation<'p>), + RplTargetDescriptor(u32), +} + +/// A high-level representation of a RPL Route Option. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct RouteInformation<'p> { + pub prefix_length: u8, + pub preference: u8, + pub lifetime: u32, + pub prefix: &'p [u8], +} + +/// A high-level representation of a RPL DODAG Configuration Option. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DodagConfiguration { + pub authentication_enabled: bool, + pub path_control_size: u8, + pub dio_interval_doublings: u8, + pub dio_interval_min: u8, + pub dio_redundancy_constant: u8, + pub max_rank_increase: u16, + pub minimum_hop_rank_increase: u16, + pub objective_code_point: u16, + pub default_lifetime: u8, + pub lifetime_unit: u16, +} + +/// A high-level representation of a RPL Target Option. +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct RplTarget { + pub prefix_length: u8, + pub prefix: heapless::Vec, +} + +/// A high-level representation of a RPL Transit Information Option. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct TransitInformation { + pub external: bool, + pub path_control: u8, + pub path_sequence: u8, + pub path_lifetime: u8, + pub parent_address: Option
, +} + +/// A high-level representation of a RPL Solicited Information Option. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SolicitedInformation { + pub rpl_instance_id: InstanceId, + pub version_predicate: bool, + pub instance_id_predicate: bool, + pub dodag_id_predicate: bool, + pub dodag_id: Address, + pub version_number: SequenceCounter, +} + +/// A high-level representation of a RPL Prefix Information Option. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PrefixInformation<'p> { + pub prefix_length: u8, + pub on_link: bool, + pub autonomous_address_configuration: bool, + pub router_address: bool, + pub valid_lifetime: u32, + pub preferred_lifetime: u32, + pub destination_prefix: &'p [u8], +} + +impl core::fmt::Display for Repr<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Repr::Pad1 => write!(f, "Pad1"), + Repr::PadN(n) => write!(f, "PadN({n})"), + Repr::DagMetricContainer => todo!(), + Repr::RouteInformation(RouteInformation { + prefix_length, + preference, + lifetime, + prefix, + }) => { + write!( + f, + "ROUTE INFO PrefixLength={prefix_length} Preference={preference} \ + Lifetime={lifetime} Prefix={prefix:0x?}" + ) + } + Repr::DodagConfiguration(DodagConfiguration { + dio_interval_doublings, + dio_interval_min, + dio_redundancy_constant, + max_rank_increase, + minimum_hop_rank_increase, + objective_code_point, + default_lifetime, + lifetime_unit, + .. + }) => { + write!( + f, + "DODAG CONF IntD={dio_interval_doublings} IntMin={dio_interval_min} \ + RedCst={dio_redundancy_constant} MaxRankIncr={max_rank_increase} \ + MinHopRankIncr={minimum_hop_rank_increase} OCP={objective_code_point} \ + DefaultLifetime={default_lifetime} LifeUnit={lifetime_unit}" + ) + } + Repr::RplTarget(RplTarget { + prefix_length, + prefix, + }) => { + write!( + f, + "RPL Target PrefixLength={prefix_length} Prefix={prefix:0x?}" + ) + } + Repr::TransitInformation(TransitInformation { + external, + path_control, + path_sequence, + path_lifetime, + parent_address, + }) => { + write!( + f, + "Transit Info External={external} PathCtrl={path_control} \ + PathSqnc={path_sequence} PathLifetime={path_lifetime} \ + Parent={parent_address:0x?}" + ) + } + Repr::SolicitedInformation(SolicitedInformation { + rpl_instance_id, + version_predicate, + instance_id_predicate, + dodag_id_predicate, + dodag_id, + version_number, + }) => { + write!( + f, + "Solicited Info I={instance_id_predicate} IID={rpl_instance_id:0x?} \ + D={dodag_id_predicate} DODAGID={dodag_id} V={version_predicate} \ + Version={version_number}" + ) + } + Repr::PrefixInformation(PrefixInformation { + prefix_length, + on_link, + autonomous_address_configuration, + router_address, + valid_lifetime, + preferred_lifetime, + destination_prefix, + }) => { + write!( + f, + "Prefix Info PrefixLength={prefix_length} L={on_link} \ + A={autonomous_address_configuration} R={router_address} \ + Valid={valid_lifetime} Prefered={preferred_lifetime} \ + Prefix={destination_prefix:0x?}" + ) + } + Repr::RplTargetDescriptor(_) => write!(f, "Target Descriptor"), + } + } +} + +impl<'p> Repr<'p> { + /// Parse a RPL Option and return a high-level representation. + pub fn parse + ?Sized>(packet: &Packet<&'p T>) -> Result { + match packet.option_type() { + OptionType::Pad1 => Ok(Repr::Pad1), + OptionType::PadN => Ok(Repr::PadN(packet.option_length())), + OptionType::DagMetricContainer => todo!(), + OptionType::RouteInformation => Ok(Repr::RouteInformation(RouteInformation { + prefix_length: packet.prefix_length(), + preference: packet.route_preference(), + lifetime: packet.route_lifetime(), + prefix: packet.prefix(), + })), + OptionType::DodagConfiguration => Ok(Repr::DodagConfiguration(DodagConfiguration { + authentication_enabled: packet.authentication_enabled(), + path_control_size: packet.path_control_size(), + dio_interval_doublings: packet.dio_interval_doublings(), + dio_interval_min: packet.dio_interval_minimum(), + dio_redundancy_constant: packet.dio_redundancy_constant(), + max_rank_increase: packet.max_rank_increase(), + minimum_hop_rank_increase: packet.minimum_hop_rank_increase(), + objective_code_point: packet.objective_code_point(), + default_lifetime: packet.default_lifetime(), + lifetime_unit: packet.lifetime_unit(), + })), + OptionType::RplTarget => Ok(Repr::RplTarget(RplTarget { + prefix_length: packet.target_prefix_length(), + prefix: heapless::Vec::from_slice(packet.target_prefix()).map_err(|_| Error)?, + })), + OptionType::TransitInformation => Ok(Repr::TransitInformation(TransitInformation { + external: packet.is_external(), + path_control: packet.path_control(), + path_sequence: packet.path_sequence(), + path_lifetime: packet.path_lifetime(), + parent_address: packet.parent_address(), + })), + OptionType::SolicitedInformation => { + Ok(Repr::SolicitedInformation(SolicitedInformation { + rpl_instance_id: InstanceId::from(packet.rpl_instance_id()), + version_predicate: packet.version_predicate(), + instance_id_predicate: packet.instance_id_predicate(), + dodag_id_predicate: packet.dodag_id_predicate(), + dodag_id: packet.dodag_id(), + version_number: packet.version_number().into(), + })) + } + OptionType::PrefixInformation => Ok(Repr::PrefixInformation(PrefixInformation { + prefix_length: packet.prefix_info_prefix_length(), + on_link: packet.on_link(), + autonomous_address_configuration: packet.autonomous_address_configuration(), + router_address: packet.router_address(), + valid_lifetime: packet.valid_lifetime(), + preferred_lifetime: packet.preferred_lifetime(), + destination_prefix: packet.destination_prefix(), + })), + OptionType::RplTargetDescriptor => Ok(Repr::RplTargetDescriptor(packet.descriptor())), + OptionType::Unknown(_) => Err(Error), + } + } + + /// Return the length of an option that will be emitted from this high-level representation. + pub fn buffer_len(&self) -> usize { + match self { + Repr::Pad1 => 1, + Repr::PadN(size) => 2 + *size as usize, + Repr::DagMetricContainer => todo!(), + Repr::RouteInformation(RouteInformation { prefix, .. }) => 2 + 6 + prefix.len(), + Repr::DodagConfiguration { .. } => 2 + 14, + Repr::RplTarget(RplTarget { prefix, .. }) => 2 + 2 + prefix.len(), + Repr::TransitInformation(TransitInformation { parent_address, .. }) => { + 2 + 4 + if parent_address.is_some() { 16 } else { 0 } + } + Repr::SolicitedInformation { .. } => 2 + 2 + 16 + 1, + Repr::PrefixInformation { .. } => 32, + Repr::RplTargetDescriptor { .. } => 2 + 4, + } + } + + /// Emit a high-level representation into an RPL Option packet. + pub fn emit + AsMut<[u8]> + ?Sized>(&self, packet: &mut Packet<&'p mut T>) { + let mut option_length = self.buffer_len() as u8; + + packet.set_option_type(self.into()); + + if !matches!(self, Repr::Pad1) { + option_length -= 2; + packet.set_option_length(option_length); + } + + match self { + Repr::Pad1 => {} + Repr::PadN(size) => { + packet.clear_padn(*size); + } + Repr::DagMetricContainer => { + unimplemented!(); + } + Repr::RouteInformation(RouteInformation { + prefix_length, + preference, + lifetime, + prefix, + }) => { + packet.clear_route_info_reserved(); + packet.set_route_info_prefix_length(*prefix_length); + packet.set_route_info_route_preference(*preference); + packet.set_route_info_route_lifetime(*lifetime); + packet.set_route_info_prefix(prefix); + } + Repr::DodagConfiguration(DodagConfiguration { + authentication_enabled, + path_control_size, + dio_interval_doublings, + dio_interval_min, + dio_redundancy_constant, + max_rank_increase, + minimum_hop_rank_increase, + objective_code_point, + default_lifetime, + lifetime_unit, + }) => { + packet.clear_dodag_conf_flags(); + packet.set_dodag_conf_authentication_enabled(*authentication_enabled); + packet.set_dodag_conf_path_control_size(*path_control_size); + packet.set_dodag_conf_dio_interval_doublings(*dio_interval_doublings); + packet.set_dodag_conf_dio_interval_minimum(*dio_interval_min); + packet.set_dodag_conf_dio_redundancy_constant(*dio_redundancy_constant); + packet.set_dodag_conf_max_rank_increase(*max_rank_increase); + packet.set_dodag_conf_minimum_hop_rank_increase(*minimum_hop_rank_increase); + packet.set_dodag_conf_objective_code_point(*objective_code_point); + packet.set_dodag_conf_default_lifetime(*default_lifetime); + packet.set_dodag_conf_lifetime_unit(*lifetime_unit); + } + Repr::RplTarget(RplTarget { + prefix_length, + prefix, + }) => { + packet.clear_rpl_target_flags(); + packet.set_rpl_target_prefix_length(*prefix_length); + packet.set_rpl_target_prefix(prefix); + } + Repr::TransitInformation(TransitInformation { + external, + path_control, + path_sequence, + path_lifetime, + parent_address, + }) => { + packet.clear_transit_info_flags(); + packet.set_transit_info_is_external(*external); + packet.set_transit_info_path_control(*path_control); + packet.set_transit_info_path_sequence(*path_sequence); + packet.set_transit_info_path_lifetime(*path_lifetime); + + if let Some(address) = parent_address { + packet.set_transit_info_parent_address(*address); + } + } + Repr::SolicitedInformation(SolicitedInformation { + rpl_instance_id, + version_predicate, + instance_id_predicate, + dodag_id_predicate, + dodag_id, + version_number, + }) => { + packet.clear_solicited_info_flags(); + packet.set_solicited_info_rpl_instance_id((*rpl_instance_id).into()); + packet.set_solicited_info_version_predicate(*version_predicate); + packet.set_solicited_info_instance_id_predicate(*instance_id_predicate); + packet.set_solicited_info_dodag_id_predicate(*dodag_id_predicate); + packet.set_solicited_info_version_number(version_number.value()); + packet.set_solicited_info_dodag_id(*dodag_id); + } + Repr::PrefixInformation(PrefixInformation { + prefix_length, + on_link, + autonomous_address_configuration, + router_address, + valid_lifetime, + preferred_lifetime, + destination_prefix, + }) => { + packet.clear_prefix_info_reserved(); + packet.set_prefix_info_prefix_length(*prefix_length); + packet.set_prefix_info_on_link(*on_link); + packet.set_prefix_info_autonomous_address_configuration( + *autonomous_address_configuration, + ); + packet.set_prefix_info_router_address(*router_address); + packet.set_prefix_info_valid_lifetime(*valid_lifetime); + packet.set_prefix_info_preferred_lifetime(*preferred_lifetime); + packet.set_prefix_info_destination_prefix(destination_prefix); + } + Repr::RplTargetDescriptor(descriptor) => { + packet.set_rpl_target_descriptor_descriptor(*descriptor); + } + } + } +} + +/// An Iterator for RPL options. +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct OptionsIterator<'a> { + pos: usize, + length: usize, + data: &'a [u8], + hit_error: bool, +} + +impl<'a> OptionsIterator<'a> { + /// Create a new `OptionsIterator`, used to iterate over the + /// options contained in a RPL header. + pub fn new(data: &'a [u8]) -> Self { + let length = data.len(); + Self { + pos: 0, + hit_error: false, + length, + data, + } + } +} + +impl<'a> Iterator for OptionsIterator<'a> { + type Item = Result>; + + fn next(&mut self) -> Option { + if self.pos < self.length && !self.hit_error { + // If we still have data to parse and we have not previously + // hit an error, attempt to parse the next option. + match Packet::new_checked(&self.data[self.pos..]) { + Ok(hdr) => match Repr::parse(&hdr) { + Ok(repr) => { + self.pos += repr.buffer_len(); + Some(Ok(repr)) + } + Err(e) => { + self.hit_error = true; + Some(Err(e)) + } + }, + Err(e) => { + self.hit_error = true; + Some(Err(e)) + } + } + } else { + // If we failed to parse a previous option or hit the end of the + // buffer, we do not continue to iterate. + None + } + } +} diff --git a/src/iface/rpl/lollipop.rs b/src/wire/rpl/sequence_counter.rs similarity index 88% rename from src/iface/rpl/lollipop.rs rename to src/wire/rpl/sequence_counter.rs index 4785c7725..cc8420024 100644 --- a/src/iface/rpl/lollipop.rs +++ b/src/wire/rpl/sequence_counter.rs @@ -1,12 +1,13 @@ -//! Implementation of sequence counters defined in [RFC 6550 § 7.2]. Values from 128 and greater -//! are used as a linear sequence to indicate a restart and bootstrap the counter. Values less than -//! or equal to 127 are used as a circular sequence number space of size 128. When operating in the -//! circular region, if sequence numbers are detected to be too far apart, then they are not -//! comparable. -//! -//! [RFC 6550 § 7.2]: https://datatracker.ietf.org/doc/html/rfc6550#section-7.2 - -#[derive(Debug, Clone, Copy)] +pub(crate) const SEQUENCE_WINDOW: u8 = 16; + +/// Implementation of sequence counters defined in [RFC 6550 § 7.2]. Values from 128 and greater +/// are used as a linear sequence to indicate a restart and bootstrap the counter. Values less than +/// or equal to 127 are used as a circular sequence number space of size 128. When operating in the +/// circular region, if sequence numbers are detected to be too far apart, then they are not +/// comparable. +/// +/// [RFC 6550 § 7.2]: https://datatracker.ietf.org/doc/html/rfc6550#section-7.2 +#[derive(Debug, Clone, Copy, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct SequenceCounter(u8); @@ -18,6 +19,18 @@ impl Default for SequenceCounter { } } +impl core::fmt::Display for SequenceCounter { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.value()) + } +} + +impl From for SequenceCounter { + fn from(value: u8) -> Self { + Self(value) + } +} + impl SequenceCounter { /// Create a new sequence counter. /// @@ -61,7 +74,7 @@ impl PartialEq for SequenceCounter { } else { let result = if a > b { a - b } else { b - a }; - if result <= super::consts::SEQUENCE_WINDOW as usize { + if result <= SEQUENCE_WINDOW as usize { // RFC1982 a == b } else { @@ -74,7 +87,6 @@ impl PartialEq for SequenceCounter { impl PartialOrd for SequenceCounter { fn partial_cmp(&self, other: &Self) -> Option { - use super::consts::SEQUENCE_WINDOW; use core::cmp::Ordering; let a = self.value() as usize; diff --git a/src/wire/sixlowpan/mod.rs b/src/wire/sixlowpan/mod.rs index 03a5218a0..64c600c7a 100644 --- a/src/wire/sixlowpan/mod.rs +++ b/src/wire/sixlowpan/mod.rs @@ -230,6 +230,15 @@ impl defmt::Format for NextHeader { } } +impl From for NextHeader { + fn from(protocol: IpProtocol) -> Self { + match protocol { + IpProtocol::Udp | IpProtocol::HopByHop | IpProtocol::Ipv6Route => Self::Compressed, + p => Self::Uncompressed(p), + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/wire/sixlowpan/nhc.rs b/src/wire/sixlowpan/nhc.rs index 85e422a49..b85fbf8c7 100644 --- a/src/wire/sixlowpan/nhc.rs +++ b/src/wire/sixlowpan/nhc.rs @@ -198,6 +198,10 @@ impl> ExtHeaderPacket { _ => unreachable!(), } } + + pub fn header_len(&self) -> usize { + 2 + self.next_header_size() + } } impl<'a, T: AsRef<[u8]> + ?Sized> ExtHeaderPacket<&'a T> { @@ -316,7 +320,7 @@ impl ExtHeaderRepr { mod tests { use super::*; - use crate::wire::{Ipv6RoutingHeader, Ipv6RoutingRepr}; + //use crate::wire::{Ipv6RoutingHeader, Ipv6RoutingRepr}; #[cfg(feature = "proto-rpl")] use crate::wire::{ @@ -326,11 +330,11 @@ mod tests { #[cfg(feature = "proto-rpl")] const RPL_HOP_BY_HOP_PACKET: [u8; 9] = [0xe0, 0x3a, 0x06, 0x63, 0x04, 0x00, 0x1e, 0x03, 0x00]; - const ROUTING_SR_PACKET: [u8; 32] = [ - 0xe3, 0x1e, 0x03, 0x03, 0x99, 0x30, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x05, 0x00, 0x05, - 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, - 0x00, 0x00, - ]; + //const ROUTING_SR_PACKET: [u8; 32] = [ + //0xe3, 0x1e, 0x03, 0x03, 0x99, 0x30, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x05, 0x00, 0x05, + //0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, + //0x00, 0x00, + //]; #[test] #[cfg(feature = "proto-rpl")] @@ -392,58 +396,58 @@ mod tests { assert_eq!(&buffer[..], RPL_HOP_BY_HOP_PACKET); } - #[test] - fn test_source_routing_deconstruct() { - let header = ExtHeaderPacket::new_checked(&ROUTING_SR_PACKET).unwrap(); - assert_eq!(header.next_header(), NextHeader::Compressed); - assert_eq!(header.extension_header_id(), ExtHeaderId::RoutingHeader); - - let routing_hdr = Ipv6RoutingHeader::new_checked(header.payload()).unwrap(); - let repr = Ipv6RoutingRepr::parse(&routing_hdr).unwrap(); - assert_eq!( - repr, - Ipv6RoutingRepr::Rpl { - segments_left: 3, - cmpr_i: 9, - cmpr_e: 9, - pad: 3, - addresses: &[ - 0x05, 0x00, 0x05, 0x00, 0x05, 0x00, 0x05, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, - 0x06, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00 - ], - } - ); - } - - #[test] - fn test_source_routing_emit() { - let routing_hdr = Ipv6RoutingRepr::Rpl { - segments_left: 3, - cmpr_i: 9, - cmpr_e: 9, - pad: 3, - addresses: &[ - 0x05, 0x00, 0x05, 0x00, 0x05, 0x00, 0x05, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, - 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, - ], - }; - - let ext_hdr = ExtHeaderRepr { - ext_header_id: ExtHeaderId::RoutingHeader, - next_header: NextHeader::Compressed, - length: routing_hdr.buffer_len() as u8, - }; - - let mut buffer = vec![0u8; ext_hdr.buffer_len() + routing_hdr.buffer_len()]; - ext_hdr.emit(&mut ExtHeaderPacket::new_unchecked( - &mut buffer[..ext_hdr.buffer_len()], - )); - routing_hdr.emit(&mut Ipv6RoutingHeader::new_unchecked( - &mut buffer[ext_hdr.buffer_len()..], - )); - - assert_eq!(&buffer[..], ROUTING_SR_PACKET); - } + //#[test] + //fn test_source_routing_deconstruct() { + //let header = ExtHeaderPacket::new_checked(&ROUTING_SR_PACKET).unwrap(); + //assert_eq!(header.next_header(), NextHeader::Compressed); + //assert_eq!(header.extension_header_id(), ExtHeaderId::RoutingHeader); + + //let routing_hdr = Ipv6RoutingHeader::new_checked(header.payload()).unwrap(); + //let repr = Ipv6RoutingRepr::parse(&routing_hdr).unwrap(); + //assert_eq!( + //repr, + //Ipv6RoutingRepr::Rpl { + //segments_left: 3, + //cmpr_i: 9, + //cmpr_e: 9, + //pad: 3, + //addresses: &[ + //0x05, 0x00, 0x05, 0x00, 0x05, 0x00, 0x05, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, + //0x06, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00 + //], + //} + //); + //} + + //#[test] + //fn test_source_routing_emit() { + //let routing_hdr = Ipv6RoutingRepr::Rpl { + //segments_left: 3, + //cmpr_i: 9, + //cmpr_e: 9, + //pad: 3, + //addresses: &[ + //0x05, 0x00, 0x05, 0x00, 0x05, 0x00, 0x05, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, + //0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, + //], + //}; + + //let ext_hdr = ExtHeaderRepr { + //ext_header_id: ExtHeaderId::RoutingHeader, + //next_header: NextHeader::Compressed, + //length: routing_hdr.buffer_len() as u8, + //}; + + //let mut buffer = vec![0u8; ext_hdr.buffer_len() + routing_hdr.buffer_len()]; + //ext_hdr.emit(&mut ExtHeaderPacket::new_unchecked( + //&mut buffer[..ext_hdr.buffer_len()], + //)); + //routing_hdr.emit(&mut Ipv6RoutingHeader::new_unchecked( + //&mut buffer[ext_hdr.buffer_len()..], + //)); + + //assert_eq!(&buffer[..], ROUTING_SR_PACKET); + //} } /// A read/write wrapper around a 6LoWPAN_NHC UDP frame. @@ -673,7 +677,7 @@ impl + AsMut<[u8]>> UdpNhcPacket { }; } - fn set_checksum(&mut self, checksum: u16) { + pub fn set_checksum(&mut self, checksum: u16) { self.set_checksum_field(0b0); let idx = 1 + self.ports_size(); let data = self.buffer.as_mut(); diff --git a/tests/rpl.rs b/tests/rpl.rs new file mode 100644 index 000000000..cbda14ed7 --- /dev/null +++ b/tests/rpl.rs @@ -0,0 +1,824 @@ +use rstest::rstest; + +use smoltcp::iface::RplConfig; +use smoltcp::iface::RplModeOfOperation; +use smoltcp::iface::RplRootConfig; +use smoltcp::time::*; +use smoltcp::wire::{Icmpv6Repr, Ipv6Address, RplDio, RplInstanceId, RplOptionRepr, RplRepr}; + +mod sim; + +const ONE_HOUR: Duration = Duration::from_secs(60 * 60); + +fn init() { + let _ = env_logger::builder().is_test(true).try_init(); +} + +/// A RPL root node only. We count the amount of DIO's it transmits. For our Trickle implementation, +/// this should be around 10 for 1 hour. Changing the Trickle parameters will make this test fail. +/// This is valid for all modes of operation. +#[rstest] +#[case::mop0(RplModeOfOperation::NoDownwardRoutesMaintained)] +#[case::mop1(RplModeOfOperation::NonStoringMode)] +#[case::mop2(RplModeOfOperation::StoringMode)] +#[case::mop3(RplModeOfOperation::StoringModeWithMulticast)] +fn root_node_only(#[case] mop: RplModeOfOperation) { + let mut sim = sim::NetworkSim::new(); + sim.create_node(RplConfig::new(mop).add_root_config(RplRootConfig::new( + RplInstanceId::from(30), + Ipv6Address::default(), + ))); + + sim.init(); + sim.run(Duration::from_millis(500), ONE_HOUR, None); + + assert!(!sim.msgs().is_empty()); + + // In 1 hour, a root node will transmit around 10 messages. + let dio_count = sim.msgs().iter().filter(|m| m.is_dio()).count(); + assert!(dio_count == 9 || dio_count == 10 || dio_count == 11); + + // There should only be DIO's. + for msg in sim.msgs() { + assert!(msg.is_dio()); + } +} + +/// A RPL normal node that is out of range of any DODAG. The normal node +/// should transmit DIS messages, soliciting for a DODAG. These messages are transmitted every 60 +/// seconds. In hour, 60 DIS messages should be transmitted. +#[rstest] +#[case::mop0(RplModeOfOperation::NoDownwardRoutesMaintained)] +#[case::mop1(RplModeOfOperation::NonStoringMode)] +#[case::mop2(RplModeOfOperation::StoringMode)] +#[case::mop3(RplModeOfOperation::StoringModeWithMulticast)] +fn normal_node_without_dodag(#[case] mop: RplModeOfOperation) { + let mut sim = sim::NetworkSim::new(); + sim.create_node(RplConfig::new(mop)); + + sim.init(); + sim.run(Duration::from_millis(500), ONE_HOUR, None); + + assert!(!sim.msgs().is_empty()); + + // In 1 hour, around 60 DIS messages are transmitted by 1 node. + let dis_count = sim.msgs().iter().filter(|m| m.is_dis()).count(); + assert!(dis_count == 59 || dis_count == 60 || dis_count == 61); + + // There should only be DIS messages. + for msg in sim.msgs() { + assert!(msg.is_dis()); + } +} + +/// A RPL root node and a normal node in range of the root node. +/// In all mode of operations, DIOs should be transmitted. +/// For MOP1, MOP2 and MOP3, DAOs and DAO-ACKs should be transmitted. +/// We run the simulation for 15 minutes. During this period, around 7 DIOs should be transmitted +/// by each node (root and normal node). In MOP1, MOP2 and MOP3, the normal node should transmit 1 +/// DAO and the root 1 DAO-ACK. By default, DAOs require an ACK in smoltcp, unless one of the nodes +/// has joined a multicast group. Then there should be an extra DAO for the multicast group to +/// which the node is subscribed +#[rstest] +#[case::mop0(RplModeOfOperation::NoDownwardRoutesMaintained, None)] +#[case::mop1(RplModeOfOperation::NonStoringMode, None)] +#[case::mop2(RplModeOfOperation::StoringMode, None)] +#[case::mop3(RplModeOfOperation::StoringModeWithMulticast, None)] +#[case::mop3_multicast(RplModeOfOperation::StoringModeWithMulticast, Some(Ipv6Address::from_parts(&[0xff02, 0, 0, 0, 0, 0, 0, 3])))] +fn root_and_normal_node( + #[case] mop: RplModeOfOperation, + #[case] multicast_group: Option, +) { + init(); + + let mut sim = sim::topology(sim::NetworkSim::new(), mop, 1, 1); + if let Some(multicast_group) = multicast_group { + let last_child = sim.nodes_mut().last_mut().unwrap(); + last_child + .interface + .join_multicast_group(&mut last_child.device, multicast_group, Instant::ZERO) + .expect("last_child should be able to join the multicast group"); + } + + // let mut pcap_file = None; + let mut pcap_file = Some( + sim::PcapFile::new(std::path::Path::new(&format!( + "sim_logs/root_and_normal_node-{}-{}.pcap", + match mop { + RplModeOfOperation::NoDownwardRoutesMaintained => "mop0", + RplModeOfOperation::NonStoringMode => "mop1", + RplModeOfOperation::StoringMode => "mop2", + RplModeOfOperation::StoringModeWithMulticast => "mop3", + }, + if multicast_group.is_some() { + "with-multicast" + } else { + "no-multicast" + } + ))) + .unwrap(), + ); + sim.init(); + sim.run( + Duration::from_millis(500), + Duration::from_secs(60 * 15), + pcap_file.as_mut(), + ); + + assert!(!sim.msgs().is_empty()); + + let dio_count = sim.msgs().iter().filter(|m| m.is_dio()).count(); + + assert!(dio_count > 12 && dio_count < 17); + + match mop { + RplModeOfOperation::NonStoringMode + | RplModeOfOperation::StoringMode + | RplModeOfOperation::StoringModeWithMulticast => { + let dao_count = sim.msgs().iter().filter(|m| m.is_dao()).count(); + let dao_ack_count = sim.msgs().iter().filter(|m| m.is_dao_ack()).count(); + + assert_eq!(dao_count, if multicast_group.is_some() { 2 } else { 1 }); + assert_eq!(dao_ack_count, dao_count); + } + _ => (), + } + + for msg in sim.msgs() { + match mop { + // In MOP0, all messages should be DIOs. A node only transmits its first DIS after 5 + // seconds. The first DIO from the root is transmitted after 2 - 4 seconds after the + // start. Therefore, there should never be a DIS in the messages. + RplModeOfOperation::NoDownwardRoutesMaintained => assert!(msg.is_dio()), + // In MOP1, MOP2, MOP3, DAOs and DAO-ACKs are also transmitted. + RplModeOfOperation::NonStoringMode + | RplModeOfOperation::StoringMode + | RplModeOfOperation::StoringModeWithMulticast => { + assert!(msg.is_dio() || msg.is_dao() || msg.is_dao_ack()) + } + } + } +} + +#[rstest] +#[case::mop0(RplModeOfOperation::NoDownwardRoutesMaintained, None)] +#[case::mop1(RplModeOfOperation::NonStoringMode, None)] +#[case::mop2(RplModeOfOperation::StoringMode, None)] +#[case::mop3(RplModeOfOperation::StoringModeWithMulticast, None)] +#[case::mop3_multicast(RplModeOfOperation::StoringModeWithMulticast, Some(Ipv6Address::from_parts(&[0xff02, 0, 0, 0, 0, 0, 0, 3])))] +fn root_and_normal_node_moved_out_of_range( + #[case] mop: RplModeOfOperation, + #[case] multicast_group: Option, +) { + let mut sim = sim::topology(sim::NetworkSim::new(), mop, 1, 1); + if let Some(multicast_group) = multicast_group { + let last_child = sim.nodes_mut().last_mut().unwrap(); + last_child + .interface + .join_multicast_group(&mut last_child.device, multicast_group, Instant::ZERO) + .expect("last_child should be able to join the multicast group"); + } + + // Setup pcap file for multicast + let mut pcap_file = if multicast_group.is_some() { + use std::path::Path; + Some(sim::PcapFile::new(Path::new(&format!("sim_logs/multicast-{mop}.pcap"))).unwrap()) + } else { + None + }; + sim.init(); + sim.run(Duration::from_millis(100), ONE_HOUR, pcap_file.as_mut()); + + assert!(!sim.msgs().is_empty()); + + // We check that a node is connect to the DODAG, meaning there should be no DIS messages. + for msg in sim.msgs() { + match mop { + // In MOP0, all messages should be DIOs. A node only transmits its first DIS after 5 + // seconds. The first DIO from the root is transmitted after 2 - 4 seconds after the + // start. Therefore, there should never be a DIS in the messages. + RplModeOfOperation::NoDownwardRoutesMaintained => assert!(msg.is_dio()), + // In MOP1, MOP2, MOP3, DAOs and DAO-ACKs are also transmitted. + RplModeOfOperation::NonStoringMode + | RplModeOfOperation::StoringMode + | RplModeOfOperation::StoringModeWithMulticast => { + assert!(msg.is_dio() || msg.is_dao() || msg.is_dao_ack()) + } + } + } + + sim.clear_msgs(); + + // Move the node far from the root node. + sim.nodes_mut()[1].set_position(sim::Position((1000., 0.))); + + sim.run(Duration::from_millis(400), ONE_HOUR, pcap_file.as_mut()); + + match mop { + RplModeOfOperation::NonStoringMode | RplModeOfOperation::StoringMode => { + let dao_count = sim.msgs().iter().filter(|m| m.is_dao()).count(); + assert!(dao_count < 5); + } + _ => {} + } + + // When a node leaves a DODAG, it multicasts an INFINITE rank DIO. + let infinite_rank_dio_count = sim + .msgs() + .iter() + .filter(|m| { + if m.is_dio() { + let icmp = m.icmp().unwrap(); + let Icmpv6Repr::Rpl(RplRepr::DodagInformationObject(dio)) = icmp else { + return false; + }; + dio.rank == 0xffff + } else { + false + } + }) + .count(); + + assert!(infinite_rank_dio_count == 1); + + for msg in sim.msgs() { + match mop { + // There should be no DAO or DAO-ACK, however, it should containt DIS's. + RplModeOfOperation::NoDownwardRoutesMaintained => { + assert!(msg.is_dio() || msg.is_dis()) + } + RplModeOfOperation::NonStoringMode + | RplModeOfOperation::StoringMode + | RplModeOfOperation::StoringModeWithMulticast => { + assert!(msg.is_dio() || msg.is_dis() || msg.is_dao()); + } + } + } + + sim.clear_msgs(); + + // Move the node back in range of the root node. + sim.nodes_mut()[1].set_position(sim::Position((100., 0.))); + + sim.run(Duration::from_millis(100), ONE_HOUR, pcap_file.as_mut()); + + // NOTE: in rare cases, I don't know why, 2 DIS messages are transmitted instead of just 1. + let dis_count = sim.msgs().iter().filter(|m| m.is_dis()).count(); + assert!(dis_count < 3); + + for msg in sim.msgs() { + match mop { + // In MOP0, all messages should be DIOs. A node only transmits its first DIS after 5 + // seconds. The first DIO from the root is transmitted after 2 - 4 seconds after the + // start. Therefore, there should never be a DIS in the messages. + RplModeOfOperation::NoDownwardRoutesMaintained => { + assert!(msg.is_dio() || msg.is_dis()) + } + // In MOP1, MOP2, MOP3, DAOs and DAO-ACKs are also transmitted. + RplModeOfOperation::NonStoringMode + | RplModeOfOperation::StoringMode + | RplModeOfOperation::StoringModeWithMulticast => { + assert!(msg.is_dis() || msg.is_dio() || msg.is_dao() || msg.is_dao_ack()) + } + } + } +} + +#[rstest] +#[case::mop0(RplModeOfOperation::NoDownwardRoutesMaintained, None)] +//#[case::mop1(RplModeOfOperation::NonStoringMode)] +#[case::mop2(RplModeOfOperation::StoringMode, None)] +#[case::mop3(RplModeOfOperation::StoringModeWithMulticast, None)] +#[case::mop3_multicast(RplModeOfOperation::StoringModeWithMulticast, Some(Ipv6Address::from_parts(&[0xff02, 0, 0, 0, 0, 0, 0, 3])))] +fn message_forwarding_to_root( + #[case] mop: RplModeOfOperation, + #[case] multicast_group: Option, +) { + let mut sim = sim::topology(sim::NetworkSim::new(), mop, 1, 2); + if let Some(multicast_group) = multicast_group { + let last_child = sim.nodes_mut().last_mut().unwrap(); + last_child + .interface + .join_multicast_group(&mut last_child.device, multicast_group, Instant::ZERO) + .expect("last_child should be able to join the multicast group"); + } + + let dst_addr = sim.nodes()[0].ip_address; + sim::udp_receiver_node(&mut sim.nodes_mut()[0], 1234); + sim::udp_sender_node(&mut sim.nodes_mut()[2], 1234, dst_addr); + + sim.init(); + // let mut pcap_file = None; + let mut pcap_file = Some( + sim::PcapFile::new(std::path::Path::new(&format!( + "sim_logs/message-forwarding-to-root-{}-{}.pcap", + match mop { + RplModeOfOperation::NoDownwardRoutesMaintained => "mop0", + RplModeOfOperation::NonStoringMode => "mop1", + RplModeOfOperation::StoringMode => "mop2", + RplModeOfOperation::StoringModeWithMulticast => "mop3", + }, + if multicast_group.is_some() { + "with-multicast" + } else { + "no-multicast" + } + ))) + .unwrap(), + ); + sim.run(Duration::from_millis(500), ONE_HOUR, pcap_file.as_mut()); + + assert!(!sim.msgs().is_empty()); + + let dio_count = sim.msgs().iter().filter(|m| m.is_dio()).count(); + assert!(dio_count > 27 && dio_count < 33); + + // We transmit a message every 60 seconds. We simulate for 1 hour, so the node will transmit + // 59 messages. The node is not in range of the destination (which is the root). There is one + // node inbetween that has to forward it. Thus, it is forwarding 59 messages. + let udp_count = sim.msgs().iter().filter(|m| m.is_udp()).count(); + assert!((118..=120).contains(&udp_count)); + + for msg in sim.msgs() { + match mop { + RplModeOfOperation::NoDownwardRoutesMaintained => { + assert!(msg.is_dis() || msg.is_dio() || msg.is_udp()) + } + // In MOP1, MOP2, MOP3, DAOs and DAO-ACKs are also transmitted. + RplModeOfOperation::NonStoringMode + | RplModeOfOperation::StoringMode + | RplModeOfOperation::StoringModeWithMulticast => { + assert!( + msg.is_dis() + || msg.is_dio() + || msg.is_dao() + || msg.is_dao_ack() + || msg.is_udp() + ) + } + } + } +} + +#[rstest] +#[case::mop0(RplModeOfOperation::NoDownwardRoutesMaintained, None)] +//#[case::mop1(RplModeOfOperation::NonStoringMode)] +#[case::mop2(RplModeOfOperation::StoringMode, None)] +#[case::mop3(RplModeOfOperation::StoringModeWithMulticast, None)] +#[case::mop3_multicast(RplModeOfOperation::StoringModeWithMulticast, Some(Ipv6Address::from_parts(&[0xff02, 0, 0, 0, 0, 0, 0, 3])))] +fn message_forwarding_up_and_down( + #[case] mop: RplModeOfOperation, + #[case] multicast_group: Option, +) { + init(); + + let mut sim = sim::topology(sim::NetworkSim::new(), mop, 2, 2); + if let Some(multicast_group) = multicast_group { + let last_child = &mut sim.nodes_mut()[4]; + last_child + .interface + .join_multicast_group(&mut last_child.device, multicast_group, Instant::ZERO) + .expect("last_child should be able to join the multicast group"); + } + + let dst_addr = sim.nodes()[3].ip_address; + sim::udp_receiver_node(&mut sim.nodes_mut()[3], 1234); + sim::udp_sender_node(&mut sim.nodes_mut()[4], 1234, dst_addr); + + sim.init(); + let mut pcap_file = Some( + sim::PcapFile::new(std::path::Path::new(&format!( + "sim_logs/message_forwarding_up_and_down-{}-{}.pcap", + match mop { + RplModeOfOperation::NoDownwardRoutesMaintained => "mop0", + RplModeOfOperation::NonStoringMode => "mop1", + RplModeOfOperation::StoringMode => "mop2", + RplModeOfOperation::StoringModeWithMulticast => "mop3", + }, + if multicast_group.is_some() { + "with-multicast" + } else { + "no-multicast" + } + ))) + .unwrap(), + ); + sim.run( + Duration::from_millis(500), + Duration::from_secs(60 * 15), + pcap_file.as_mut(), + ); + + assert!(!sim.msgs().is_empty()); + + let dio_count = sim.msgs().iter().filter(|m| m.is_dio()).count(); + assert!((30..=40).contains(&dio_count)); + + // We transmit a message every 60 seconds. We simulate for 1 hour, so the node will transmit + // 59 messages. The node is not in range of the destination (which is the root). There is one + // node inbetween that has to forward it. Thus, it is forwarding 59 messages. + let udp_count = sim.msgs().iter().filter(|m| m.is_udp()).count(); + match mop { + RplModeOfOperation::NoDownwardRoutesMaintained => { + assert!((28..=30).contains(&udp_count)); + } + RplModeOfOperation::NonStoringMode + | RplModeOfOperation::StoringMode + | RplModeOfOperation::StoringModeWithMulticast => { + assert!((52..=60).contains(&udp_count)); + } + } + + for msg in sim.msgs() { + match mop { + RplModeOfOperation::NoDownwardRoutesMaintained => { + assert!(msg.is_dis() || msg.is_dio() || msg.is_udp()) + } + // In MOP1, MOP2, MOP3, DAOs and DAO-ACKs are also transmitted. + RplModeOfOperation::NonStoringMode + | RplModeOfOperation::StoringMode + | RplModeOfOperation::StoringModeWithMulticast => { + assert!( + msg.is_dis() + || msg.is_dio() + || msg.is_dao() + || msg.is_dao_ack() + || msg.is_udp() + ) + } + } + } + + // All UDP, DAO, DAO-ACK packets should have a HBH or a source routing header + sim.msgs() + .iter() + .filter(|m| m.is_udp() || m.is_dao() || m.is_dao_ack()) + .for_each(|m| assert!(m.has_hbh() || m.has_routing())); + + let dao_ack_packets_with_routing = sim + .msgs() + .iter() + .filter(|m| m.is_dao_ack() && m.has_routing()) + .count(); + let dao_ack_packets_without_routing = sim + .msgs() + .iter() + .filter(|m| m.is_dao_ack() && !m.has_routing()) + .count(); + + match mop { + RplModeOfOperation::NonStoringMode => { + assert!(dao_ack_packets_with_routing == 4,); + assert!(dao_ack_packets_without_routing == 2,); + } + RplModeOfOperation::StoringMode => { + assert!(dao_ack_packets_with_routing == 0,); + assert!(dao_ack_packets_without_routing == 6,); + } + RplModeOfOperation::StoringModeWithMulticast if multicast_group.is_none() => { + assert_eq!(dao_ack_packets_with_routing, 0,); + assert_eq!(dao_ack_packets_without_routing, 6,); + } + RplModeOfOperation::StoringModeWithMulticast if multicast_group.is_some() => { + assert_eq!(dao_ack_packets_with_routing, 0,); + assert_eq!(dao_ack_packets_without_routing, 6 + 2,); // 1x joining multicast generates 2 DAOs + } + _ => { + assert!(dao_ack_packets_with_routing == 0,); + assert!(dao_ack_packets_without_routing == 0,); + } + } +} + +#[rstest] +#[case::one(&[4])] +#[case::two(&[4, 2])] +#[case::three(&[4, 2, 3])] +fn forward_multicast_up_and_down(#[case] multicast_receivers: &[usize]) { + init(); + + const MULTICAST_GROUP: Ipv6Address = Ipv6Address::new(0xff02, 0, 0, 0, 0, 0, 0, 3); + let mut sim = sim::topology( + sim::NetworkSim::new(), + RplModeOfOperation::StoringModeWithMulticast, + 2, + 2, + ); + // Subscribe to multicast group + for receiver in multicast_receivers { + let node = &mut sim.nodes_mut()[*receiver]; + node.interface + .join_multicast_group(&mut node.device, MULTICAST_GROUP, Instant::ZERO) + .expect("node should be able to join the multicast group"); + + sim::udp_receiver_node(node, 1234); + } + + // Setup UDP sender + sim::udp_sender_node(&mut sim.nodes_mut()[4], 1234, MULTICAST_GROUP); + + let mut pcap_file = Some( + sim::PcapFile::new(std::path::Path::new(&format!( + "sim_logs/forward_multicast_up_and_down{}.pcap", + multicast_receivers + .iter() + .map(|id| id.to_string()) + .fold(String::new(), |a, b| a + "-" + &b), + ))) + .unwrap(), + ); + + sim.init(); + sim.run( + Duration::from_millis(500), + Duration::from_secs(60 * 5), + pcap_file.as_mut(), + ); + + assert!(!sim.msgs().is_empty()); +} + +#[rstest] +#[case::root_one(&[4], 0)] +#[case::child_one(&[4], 4)] +#[case::root_two(&[4, 2], 0)] +#[case::child_two(&[4, 2], 4)] +#[case::root_three(&[4, 2, 3], 0)] +#[case::child_three(&[4, 2, 3], 4)] +fn forward_multicast_staged_initialization( + #[case] multicast_receivers: &[usize], + #[case] multicast_sender: usize, +) { + init(); + + const MULTICAST_GROUP: Ipv6Address = Ipv6Address::new(0xff02, 0, 0, 0, 0, 0, 0, 3); + let mut sim = sim::topology( + sim::NetworkSim::new(), + RplModeOfOperation::StoringModeWithMulticast, + 2, + 2, + ); + // Subscribe to multicast group + for receiver in multicast_receivers { + let node = &mut sim.nodes_mut()[*receiver]; + node.interface + .join_multicast_group(&mut node.device, MULTICAST_GROUP, Instant::ZERO) + .expect("node should be able to join the multicast group"); + + sim::udp_receiver_node(node, 1234); + } + + // Setup UDP sender + sim::udp_sender_node( + &mut sim.nodes_mut()[multicast_sender], + 1234, + MULTICAST_GROUP, + ); + + let mut pcap_file = Some( + sim::PcapFile::new(std::path::Path::new(&format!( + "sim_logs/forward_multicast_staged_init_r{}-s{}.pcap", + multicast_receivers + .iter() + .map(|id| id.to_string()) + .fold(String::new(), |a, b| a + "-" + &b), + multicast_sender, + ))) + .unwrap(), + ); + + let nodes_len = sim.nodes().len(); + for node in 0..nodes_len { + let node = &mut sim.nodes_mut()[node]; + node.init(); + + // Run for a while + sim.run( + Duration::from_millis(500), + Duration::from_secs(60 * 5), + pcap_file.as_mut(), + ); + sim.clear_msgs(); + } + + // At the end run with the entire network up + sim.init(); + sim.run( + Duration::from_millis(500), + Duration::from_secs(60 * 5), + pcap_file.as_mut(), + ); + + assert!(!sim.msgs().is_empty()); +} + +#[rstest] +#[case::mop0(RplModeOfOperation::NoDownwardRoutesMaintained, None)] +//#[case::mop1(RplModeOfOperation::NonStoringMode)] +#[case::mop2(RplModeOfOperation::StoringMode, None)] +#[case::mop3(RplModeOfOperation::StoringModeWithMulticast, None)] +#[case::mop3_multicast( + RplModeOfOperation::StoringModeWithMulticast, + Some(Ipv6Address::new(0xff02, 0, 0, 0, 0, 0, 0, 3)) +)] +fn normal_node_change_parent( + #[case] mop: RplModeOfOperation, + #[case] multicast_group: Option, +) { + init(); + + let mut sim = sim::topology(sim::NetworkSim::new(), mop, 1, 3); + if let Some(multicast_group) = multicast_group { + let last_child = sim.nodes_mut().last_mut().unwrap(); + last_child + .interface + .join_multicast_group(&mut last_child.device, multicast_group, Instant::ZERO) + .expect("last_child should be able to join the multicast group"); + } + + sim.init(); + sim.run( + Duration::from_millis(500), + Duration::from_secs(60 * 5), + None, + ); + + assert!(!sim.msgs().is_empty()); + + // Move the the second node such that it is also in the range of a node with smaller rank. + sim.nodes_mut()[3].set_position(sim::Position((150., -50.))); + sim.clear_msgs(); + + sim.run(Duration::from_millis(500), ONE_HOUR, None); + + // Counter for sent NO-PATH DAOs + let mut no_path_dao_count = 0; + // Counter for not acknowledged NO-PATH DAOs + let mut dao_no_ack_req_count = 0; + // Counter for DIOs for the node that changed the parent + // This node should reset its Trickle Timer + let mut dio_count = 0; + + for msg in sim.msgs() { + if msg.is_dao() { + let icmp = msg.icmp().unwrap(); + let Icmpv6Repr::Rpl(RplRepr::DestinationAdvertisementObject(dao)) = icmp else { + break; + }; + dao_no_ack_req_count += !dao.expect_ack as usize; + no_path_dao_count += dao + .options + .iter() + .filter(|opt| { + if let RplOptionRepr::TransitInformation(o) = opt { + o.path_lifetime == 0 + } else { + false + } + }) + .count(); + } + if msg.is_dio() && msg.from.0 == 3 { + dio_count += 1; + } + } + + match mop { + // In MOP 2 when a nodes leaves it's parent it should send a NO-PATH DAO + RplModeOfOperation::StoringMode => { + // The node sends a NO-PATH DAO to the parent that forwards it to its own parent + // until it reaches the root, that is why there will be 3 NO-PATH DAOs sent + assert_eq!(no_path_dao_count, 4); + // NO-PATH DAO should have the ack request flag set to false only when it is sent + // to the old parent + assert_eq!(dao_no_ack_req_count, 2); + assert!(dio_count > 9 && dio_count < 12); + } + // In MOP 1 and MOP 0 there should be no NO-PATH DAOs sent + RplModeOfOperation::NonStoringMode | RplModeOfOperation::NoDownwardRoutesMaintained => { + assert!(no_path_dao_count == 0,); + // By default all DAOs are acknowledged with the exception of the NO-PATH DAO + // destined to the old parent + assert!(dao_no_ack_req_count == 0,); + assert!(dio_count > 9 && dio_count < 12); + } + _ => {} + } +} + +// When a parent leaves the network, its children nodes should also leave the DODAG +// if there are no alternate parents they can choose from. +#[rstest] +#[case::mop0(RplModeOfOperation::NoDownwardRoutesMaintained)] +//#[case::mop1(RplModeOfOperation::NonStoringMode)] +#[case::mop2(RplModeOfOperation::StoringMode)] +#[case::mop3(RplModeOfOperation::StoringModeWithMulticast)] +fn parent_leaves_network_no_other_parent(#[case] mop: RplModeOfOperation) { + let mut sim = sim::topology(sim::NetworkSim::new(), mop, 4, 2); + sim.init(); + sim.run(Duration::from_millis(500), ONE_HOUR, None); + + // Parent leaves network, child node does not have an alternative parent. + // The child node should send INFINITE_RANK DIO and after that only send DIS messages + // since it is unable to connect back to the tree + sim.nodes_mut()[1].set_position(sim::Position((300., 300.))); + + sim.clear_msgs(); + + sim.run(Duration::from_millis(500), ONE_HOUR, None); + + let no_parent_node_msgs: Vec<_> = sim.msgs().iter().filter(|m| m.from.0 == 5).collect(); + + let infinite_dio_msgs = no_parent_node_msgs + .iter() + .filter(|m| { + let icmp = m.icmp().unwrap(); + let Icmpv6Repr::Rpl(RplRepr::DodagInformationObject(dio)) = icmp else { + return false; + }; + dio.rank == 65535 + }) + .count(); + let dis_msgs = no_parent_node_msgs.iter().filter(|m| m.is_dis()).count(); + + assert_eq!(infinite_dio_msgs, 1); + assert!(dis_msgs > 0 && dis_msgs < 62); +} + +// In MOP 2 the DTSN is incremented when a parent does not hear anymore from one of its children. +#[rstest] +#[case::mop2(RplModeOfOperation::StoringMode)] +#[case::mop3(RplModeOfOperation::StoringModeWithMulticast)] +fn dtsn_incremented_when_child_leaves_network(#[case] mop: RplModeOfOperation) { + use std::collections::HashMap; + + let mut sim = sim::topology(sim::NetworkSim::new(), mop, 1, 5); + sim.nodes_mut()[4].set_position(sim::Position((200., 100.))); + sim.nodes_mut()[5].set_position(sim::Position((-100., 0.))); + + sim.init(); + sim.run(Duration::from_millis(500), ONE_HOUR, None); + + // One node is moved out of the range of its parent. + sim.nodes_mut()[4].set_position(sim::Position((500., 500.))); + + sim.clear_msgs(); + + sim.run(Duration::from_millis(500), ONE_HOUR, None); + + // Keep track of when was the first DIO with increased DTSN sent + let mut dio_at = Instant::ZERO; + let mut time_set = false; + + // The parent will not hear anymore from the child and will increment DTSN. + // All the nodes that had the missing child in the relations table will increment DTSN. + let node_ids_with_dtsn_incremented: Vec = sim + .nodes_mut() + .iter() + .filter_map(|n| if n.id != 5 { Some(n.id) } else { None }) + .collect(); + + let dios: HashMap = sim + .msgs() + .iter() + .filter_map(|msg| { + if let Some(Icmpv6Repr::Rpl(RplRepr::DodagInformationObject(dio))) = msg.icmp() { + if msg.from.0 == 2 && dio.dtsn.value() == 241 && !time_set { + dio_at = msg.at; + time_set = true; + } + Some((msg.from.0, dio)) + } else { + None + } + }) + .collect(); + + if dios.is_empty() { + panic!("No DIO messages found"); + } + + dios.iter() + .filter(|(_, v)| v.dtsn.value() == 241) + .for_each(|(k, _)| assert!(node_ids_with_dtsn_incremented.contains(k))); + + // The nodes that did not have the missing child in the relations table will not increase + // the DTSN even if they hear a DIO with increased DTSN from parent. + dios.iter() + .filter(|(k, _)| **k == 4 || **k == 5) + .for_each(|(_, v)| assert_eq!(v.dtsn.value(), 240)); + + // The remaining children will send DAOs to renew paths when hearing a DIO + // with incremented DTSN from their preferred parent + let dao_at = sim.msgs().iter().find_map(|m| { + if m.from.0 == 3 && m.is_dao() && m.at.gt(&dio_at) { + return Some(m.at); + } + None + }); + + if dao_at.is_some() { + println!("dao_at {} and dio_at {dio_at}", dao_at.unwrap()); + assert!(dao_at.unwrap() - dio_at < Duration::from_secs(6)); + } +} diff --git a/tests/sim/message.rs b/tests/sim/message.rs new file mode 100644 index 000000000..ea373d731 --- /dev/null +++ b/tests/sim/message.rs @@ -0,0 +1,209 @@ +use super::Position; +use smoltcp::phy::ChecksumCapabilities; +use smoltcp::time::*; +use smoltcp::wire::*; + +#[derive(Debug, Clone)] +pub struct Message { + pub at: Instant, + pub to: Ieee802154Address, + pub from: (usize, Position), + pub data: Vec, +} + +impl Message { + pub fn is_broadcast(&self) -> bool { + self.to == Ieee802154Address::BROADCAST + } + + pub fn udp(&self) -> Option { + let ieee802154 = Ieee802154Frame::new_checked(&self.data).unwrap(); + let lowpan = SixlowpanIphcPacket::new_checked(ieee802154.payload().unwrap()).unwrap(); + let src_addr = lowpan + .src_addr() + .unwrap() + .resolve(ieee802154.src_addr(), &[]) + .unwrap(); + let dst_addr = lowpan + .dst_addr() + .unwrap() + .resolve(ieee802154.src_addr(), &[]) + .unwrap(); + + let mut payload = lowpan.payload(); + let mut next_hdr = lowpan.next_header(); + loop { + match next_hdr { + SixlowpanNextHeader::Compressed => { + match SixlowpanNhcPacket::dispatch(payload).unwrap() { + SixlowpanNhcPacket::ExtHeader => { + let ext_hdr = SixlowpanExtHeaderPacket::new_checked(payload).unwrap(); + next_hdr = ext_hdr.next_header(); + payload = &payload[ext_hdr.header_len() + ext_hdr.payload().len()..]; + continue; + } + SixlowpanNhcPacket::UdpHeader => { + let udp = SixlowpanUdpNhcPacket::new_checked(payload).unwrap(); + return Some( + SixlowpanUdpNhcRepr::parse( + &udp, + &src_addr, + &dst_addr, + &ChecksumCapabilities::ignored(), + ) + .unwrap(), + ); + } + } + } + SixlowpanNextHeader::Uncompressed(IpProtocol::Icmpv6) => return None, + _ => unreachable!(), + }; + } + } + + pub fn icmp(&self) -> Option> { + let ieee802154 = Ieee802154Frame::new_checked(&self.data).unwrap(); + let lowpan = + SixlowpanIphcPacket::new_checked(ieee802154.payload().ok_or(Error).unwrap()).unwrap(); + let src_addr = lowpan + .src_addr() + .unwrap() + .resolve(ieee802154.src_addr(), &[]) + .unwrap(); + let dst_addr = lowpan + .dst_addr() + .unwrap() + .resolve(ieee802154.src_addr(), &[]) + .unwrap(); + + let mut payload = lowpan.payload(); + let mut next_hdr = lowpan.next_header(); + loop { + match next_hdr { + SixlowpanNextHeader::Compressed => { + match SixlowpanNhcPacket::dispatch(payload).unwrap() { + SixlowpanNhcPacket::ExtHeader => { + let ext_hdr = SixlowpanExtHeaderPacket::new_checked(payload).unwrap(); + next_hdr = ext_hdr.next_header(); + payload = &payload[ext_hdr.header_len() + ext_hdr.payload().len()..]; + continue; + } + SixlowpanNhcPacket::UdpHeader => return None, + } + } + SixlowpanNextHeader::Uncompressed(IpProtocol::Icmpv6) => { + let icmp = Icmpv6Packet::new_checked(payload).unwrap(); + + return Some( + Icmpv6Repr::parse( + &src_addr, + &dst_addr, + &icmp, + &ChecksumCapabilities::ignored(), + ) + .unwrap(), + ); + } + _ => unreachable!(), + }; + } + } + + pub fn has_routing(&self) -> bool { + let ieee802154 = Ieee802154Frame::new_checked(&self.data).unwrap(); + let lowpan = + SixlowpanIphcPacket::new_checked(ieee802154.payload().ok_or(Error).unwrap()).unwrap(); + + let mut payload = lowpan.payload(); + let mut next_hdr = lowpan.next_header(); + + loop { + match next_hdr { + SixlowpanNextHeader::Compressed => match SixlowpanNhcPacket::dispatch(payload) + .unwrap() + { + SixlowpanNhcPacket::ExtHeader => { + let ext_hdr = SixlowpanExtHeaderPacket::new_checked(payload).unwrap(); + if ext_hdr.extension_header_id() == SixlowpanExtHeaderId::RoutingHeader { + return true; + } + next_hdr = ext_hdr.next_header(); + payload = &payload[ext_hdr.header_len() + ext_hdr.payload().len()..]; + continue; + } + SixlowpanNhcPacket::UdpHeader => return false, + }, + SixlowpanNextHeader::Uncompressed(IpProtocol::Icmpv6) => { + return false; + } + _ => unreachable!(), + }; + } + } + + pub fn has_hbh(&self) -> bool { + let ieee802154 = Ieee802154Frame::new_checked(&self.data).unwrap(); + let lowpan = SixlowpanIphcPacket::new_checked(ieee802154.payload().unwrap()).unwrap(); + + let mut payload = lowpan.payload(); + let mut next_hdr = lowpan.next_header(); + + loop { + match next_hdr { + SixlowpanNextHeader::Compressed => match SixlowpanNhcPacket::dispatch(payload) + .unwrap() + { + SixlowpanNhcPacket::ExtHeader => { + let ext_hdr = SixlowpanExtHeaderPacket::new_checked(payload).unwrap(); + if ext_hdr.extension_header_id() == SixlowpanExtHeaderId::HopByHopHeader { + return true; + } + next_hdr = ext_hdr.next_header(); + payload = &payload[ext_hdr.header_len() + ext_hdr.payload().len()..]; + continue; + } + SixlowpanNhcPacket::UdpHeader => return false, + }, + SixlowpanNextHeader::Uncompressed(IpProtocol::Icmpv6) => { + return false; + } + _ => unreachable!(), + }; + } + } + + pub fn is_udp(&self) -> bool { + matches!(self.udp(), Some(SixlowpanUdpNhcRepr(_))) + } + + pub fn is_dis(&self) -> bool { + matches!( + self.icmp(), + Some(Icmpv6Repr::Rpl(RplRepr::DodagInformationSolicitation(_))) + ) + } + + pub fn is_dio(&self) -> bool { + matches!( + self.icmp(), + Some(Icmpv6Repr::Rpl(RplRepr::DodagInformationObject(_))) + ) + } + + pub fn is_dao(&self) -> bool { + matches!( + self.icmp(), + Some(Icmpv6Repr::Rpl(RplRepr::DestinationAdvertisementObject(_))) + ) + } + + pub fn is_dao_ack(&self) -> bool { + matches!( + self.icmp(), + Some(Icmpv6Repr::Rpl(RplRepr::DestinationAdvertisementObjectAck( + _ + ))) + ) + } +} diff --git a/tests/sim/mod.rs b/tests/sim/mod.rs new file mode 100644 index 000000000..ad5a82bf6 --- /dev/null +++ b/tests/sim/mod.rs @@ -0,0 +1,340 @@ +use std::fs::File; + +use smoltcp::iface::*; +use smoltcp::phy::{PcapLinkType, PcapSink}; +use smoltcp::time::*; +use smoltcp::wire::*; + +mod message; +mod node; + +use message::Message; +use node::*; + +const TRANSMIT_SPEED: f32 = 250_000. / 8.; + +#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] +pub struct Position(pub (f32, f32)); + +impl Position { + pub fn distance(&self, other: &Self) -> f32 { + ((other.0 .0 - self.0 .0).powf(2.0) + (other.0 .1 - self.0 .1).powf(2.0)).sqrt() + } + + pub fn x(&self) -> f32 { + self.0 .0 + } + + pub fn y(&self) -> f32 { + self.0 .1 + } +} + +impl From<(f32, f32)> for Position { + fn from(pos: (f32, f32)) -> Self { + Position(pos) + } +} + +pub fn topology( + mut sim: NetworkSim, + mop: RplModeOfOperation, + nodes: usize, + levels: usize, +) -> NetworkSim { + let pos = Position((0., 0.)); + let root = sim.create_node(RplConfig::new(mop).add_root_config(RplRootConfig::new( + RplInstanceId::from(30), + Ipv6Address::default(), + ))); + root.set_position(pos); + + let interval = (360. / 180. * std::f64::consts::PI / nodes as f64) as f32; + for level in 0..levels { + for node in 0..nodes { + let node_p = ( + pos.x() + 100. * f32::cos(interval * node as f32) * (level + 1) as f32, + pos.y() + 100. * f32::sin(interval * node as f32) * (level + 1) as f32, + ); + let node = sim.create_node(RplConfig::new(mop)); + node.set_position(node_p.into()); + } + } + + sim +} + +pub fn udp_receiver_node(node: &mut Node, port: u16) { + node.set_init(Box::new(|s| { + let udp_rx_buffer = smoltcp::socket::udp::PacketBuffer::new( + vec![smoltcp::socket::udp::PacketMetadata::EMPTY], + vec![0; 1280], + ); + let udp_tx_buffer = smoltcp::socket::udp::PacketBuffer::new( + vec![smoltcp::socket::udp::PacketMetadata::EMPTY], + vec![0; 1280], + ); + let udp_socket = smoltcp::socket::udp::Socket::new(udp_rx_buffer, udp_tx_buffer); + + vec![s.add(udp_socket)] + })); + + node.set_application(Box::new(move |_, sockets, handles, _| { + let socket = sockets.get_mut::(handles[0]); + if !socket.is_open() { + socket.bind(port).unwrap(); + } + })); +} + +pub fn udp_sender_node(node: &mut Node, port: u16, addr: Ipv6Address) { + node.set_init(Box::new(|s| { + let udp_rx_buffer = smoltcp::socket::udp::PacketBuffer::new( + vec![smoltcp::socket::udp::PacketMetadata::EMPTY], + vec![0; 1280], + ); + let udp_tx_buffer = smoltcp::socket::udp::PacketBuffer::new( + vec![smoltcp::socket::udp::PacketMetadata::EMPTY], + vec![0; 1280], + ); + let udp_socket = smoltcp::socket::udp::Socket::new(udp_rx_buffer, udp_tx_buffer); + + vec![s.add(udp_socket)] + })); + + node.set_application(Box::new( + move |instant, sockets, handles, last_transmitted| { + let socket = sockets.get_mut::(handles[0]); + if !socket.is_open() { + socket.bind(port).unwrap(); + } + + if socket.can_send() && instant - *last_transmitted >= Duration::from_secs(60) { + if let Ok(()) = socket.send_slice( + b"Hello World", + smoltcp::wire::IpEndpoint { + addr: addr.into(), + port, + }, + ) { + *last_transmitted = instant; + } + } + }, + )); +} + +pub struct NetworkSim { + nodes: Vec, + messages: Vec, + now: Instant, +} + +impl Default for NetworkSim { + fn default() -> Self { + Self::new() + } +} + +impl NetworkSim { + /// Create a new network simulation. + pub fn new() -> Self { + Self { + nodes: vec![], + messages: vec![], + now: Instant::ZERO, + } + } + + /// Create a new node. + pub fn create_node(&mut self, rpl: smoltcp::iface::RplConfig) -> &mut Node { + let id = self.nodes.len(); + let node = Node::new(id, rpl); + + self.nodes.push(node); + + &mut self.nodes[id] + } + + /// Get a reference to the nodes. + pub fn nodes(&self) -> &[Node] { + &self.nodes + } + + /// Get a mutable reference to the nodes. + pub fn nodes_mut(&mut self) -> &mut [Node] { + &mut self.nodes + } + + /// Get a reference to the transmitted messages. + pub fn msgs(&self) -> &[Message] { + &self.messages + } + + /// Clear all transmitted messages. + pub fn clear_msgs(&mut self) { + self.messages.clear(); + } + + /// Search for a node with a specific IEEE address and PAN ID. + fn get_node_from_ieee_mut(&mut self, destination: Ieee802154Address) -> Option<&mut Node> { + self.nodes + .iter_mut() + .find(|node| node.ieee_address == destination) + } + + /// Initialize the simulation. This is a shortcut to initialize all the nodes individually. + /// Nodes need to be initialized to be enabled, otherwise they will not show up in the simulation. + pub fn init(&mut self) { + for node in &mut self.nodes { + node.init(); + } + } + + /// Run the simluation for a specific duration with a specified step. + /// *NOTE*: the simulation uses the step as a maximum step. If a smoltcp interface needs to be + /// polled more often, then the simulation will do so. + pub fn run(&mut self, step: Duration, duration: Duration, pcap_file: Option<&mut PcapFile>) { + let start = self.now; + while self.now < start + duration { + let (new_step, _, _) = self.on_tick(self.now, step); + + if new_step == Duration::ZERO { + self.now += Duration::from_millis(1); + } else if new_step <= step { + self.now += new_step; + } else { + self.now += step; + } + } + + if let Some(file) = pcap_file { + file.append_messages(self) + } + } + + /// Run the simulation. + pub fn on_tick( + &mut self, + now: Instant, + mut step: Duration, + ) -> (Duration, Vec, Vec) { + for node in &mut self.nodes { + if node.enabled { + if let Some(application) = &node.application { + application( + now, + &mut node.sockets, + &mut node.socket_handles, + &mut node.last_transmitted, + ); + } + } + } + + // Check for messages that need to be send between nodes. + let mut unicast_msgs = vec![]; + let mut broadcast_msgs: Vec = vec![]; + + for node in &mut self.nodes { + if node.is_sending && node.sent_at < Instant::now() - Duration::from_millis(100) { + node.is_sending = false; + } + } + + for node in &mut self.nodes { + if node.enabled { + if let Some(msg) = node.peek_tx_message() { + let delta = + Duration::from_secs((msg.data.len() as f32 / TRANSMIT_SPEED) as u64); + + if now >= msg.at + delta { + let msg = node.tx_message().unwrap(); + + if msg.is_broadcast() { + node.is_sending = true; + node.sent_at = Instant::now(); + broadcast_msgs.push(msg.clone()); + self.messages.push(msg); + } else { + unicast_msgs.push(msg.clone()); + self.messages.push(msg); + } + } + } + } + } + + // Distribute all the broadcast messages. + for msg in &broadcast_msgs { + for node in self.nodes.iter_mut() { + if node.enabled + && node.id != msg.from.0 + && node.position.distance(&msg.from.1) < node.range + { + node.rx_message(msg.clone()); + } + } + } + + // Check if messages can arrive at their destination. + for msg in &unicast_msgs { + let to_node = self.get_node_from_ieee_mut(msg.to).unwrap(); + + if to_node.enabled && to_node.position.distance(&msg.from.1) < to_node.range { + to_node.rx_message(msg.clone()); + } + } + + // Poll the interfaces of the nodes. + for node in &mut self.nodes { + if node.enabled { + let Node { + device, + interface, + sockets, + next_poll, + .. + } = node; + + if next_poll.unwrap_or_else(|| now) <= now { + interface.poll(now, device, sockets); + } + + if let Some(new_step) = interface.poll_delay(now, sockets) { + step = step.min(new_step); + } + } + } + + (step, broadcast_msgs, unicast_msgs) + } +} + +#[allow(unused)] +/// Helper for writing messages from the simulator to a PCAP file +pub struct PcapFile { + file: File, +} + +#[allow(unused)] +impl PcapFile { + pub fn new(path: &std::path::Path) -> std::io::Result { + let parent = path.parent(); + if let Some(parent) = parent { + if !parent.exists() { + std::fs::create_dir_all(parent)?; + } + } + let mut file = std::fs::File::create(path)?; + PcapSink::global_header(&mut file, PcapLinkType::Ieee802154WithoutFcs); + + Ok(Self { file }) + } + + pub fn append_messages(&mut self, sim: &NetworkSim) { + for msg in &sim.messages { + PcapSink::packet(&mut self.file, msg.at, &msg.data); + } + } +} diff --git a/tests/sim/node.rs b/tests/sim/node.rs new file mode 100644 index 000000000..682d81b92 --- /dev/null +++ b/tests/sim/node.rs @@ -0,0 +1,252 @@ +use super::Message; +use super::Position; +use smoltcp::iface::*; +use smoltcp::storage::PacketMetadata; +use smoltcp::time::*; +use smoltcp::wire::*; +use std::collections::VecDeque; +use std::fmt::Display; + +type InitFn = Box) -> Vec + Send + Sync + 'static>; + +type AppFn = Box< + dyn Fn(Instant, &mut SocketSet<'static>, &mut Vec, &mut Instant) + + Send + + Sync + + 'static, +>; + +pub struct Node { + pub id: usize, + pub range: f32, + pub position: Position, + pub enabled: bool, + pub is_sending: bool, + pub parent_changed: bool, + pub previous_parent: Option, + pub sent_at: Instant, + pub ieee_address: Ieee802154Address, + pub ip_address: Ipv6Address, + pub pan_id: Ieee802154Pan, + pub device: NodeDevice, + pub last_transmitted: Instant, + pub interface: Interface<'static>, + pub sockets: SocketSet<'static>, + pub socket_handles: Vec, + pub init: Option, + pub application: Option, + pub next_poll: Option, +} + +impl Display for Node { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Node[{}] with {}", self.id, self.device)?; + Ok(()) + } +} + +impl Node { + /// Create a new node. + pub fn new(id: usize, mut rpl: RplConfig) -> Self { + let mut device = NodeDevice::new(id, Position::from((0., 0.))); + + let ieee_address = Ieee802154Address::Extended((id as u64 + 1).to_be_bytes()); + let ipv6_address = ieee_address.as_link_local_address().unwrap(); + + let rpl = if let Some(ref mut root) = rpl.root { + root.dodag_id = ipv6_address; + rpl + } else { + rpl + }; + + let mut config = Config::new(ieee_address.into()); + config.pan_id = Some(Ieee802154Pan(0xbeef)); + config.rpl_config = Some(rpl); + config.random_seed = Instant::now().total_micros() as u64; + + let mut interface = Interface::<'static>::new( + config, + &mut device, + vec![PacketMetadata::EMPTY; 16], + vec![0; 2048], + Instant::ZERO, + ); + interface.update_ip_addrs(|addresses| { + addresses + .push(IpCidr::Ipv6(Ipv6Cidr::new(ipv6_address, 10))) + .unwrap(); + }); + + Self { + id, + range: 101., + position: Position::from((0., 0.)), + enabled: false, + is_sending: false, + parent_changed: false, + previous_parent: None, + sent_at: Instant::now(), + ieee_address, + ip_address: ipv6_address, + pan_id: Ieee802154Pan(0xbeef), + device, + interface, + sockets: SocketSet::new(vec![]), + socket_handles: vec![], + init: None, + application: None, + next_poll: Some(Instant::ZERO), + last_transmitted: Instant::ZERO, + } + } + + /// Initializes the node + pub fn init(&mut self) { + self.enabled = true; + if let Some(init) = &self.init { + let handles = init(&mut self.sockets); + self.socket_handles = handles; + } + } + + /// Set the position of the node. + pub fn set_position(&mut self, position: Position) { + self.position = position; + self.device.position = position; + } + + /// Accept a message that was send to this node. + pub(crate) fn rx_message(&mut self, msg: Message) { + self.device.rx_queue.push_back(msg); + } + + /// Peek a message that needs to be send. + pub(crate) fn peek_tx_message(&mut self) -> Option<&Message> { + self.device.tx_queue.front() + } + + /// Get a message that needs to be send. + pub(crate) fn tx_message(&mut self) -> Option { + self.device.tx_queue.pop_front() + } + + pub fn set_init(&mut self, init: InitFn) { + self.init = Some(init); + } + + pub fn set_application(&mut self, application: AppFn) { + self.application = Some(application); + } +} + +pub struct NodeDevice { + pub id: usize, + pub position: Position, + pub rx_queue: VecDeque, + pub tx_queue: VecDeque, +} + +impl Display for NodeDevice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "NodeDevice[{}] at ({}, {})", + self.id, + self.position.x(), + self.position.y() + )?; + + Ok(()) + } +} + +impl NodeDevice { + pub fn new(id: usize, position: Position) -> Self { + Self { + id, + position, + rx_queue: Default::default(), + tx_queue: Default::default(), + } + } +} + +impl smoltcp::phy::Device for NodeDevice { + type RxToken<'a> = RxToken where Self: 'a; + type TxToken<'a> = TxToken<'a> where Self: 'a; + + fn receive(&mut self, timestamp: Instant) -> Option<(RxToken, TxToken)> { + if let Some(data) = self.rx_queue.pop_front() { + Some(( + RxToken { buffer: data.data }, + TxToken { + buffer: &mut self.tx_queue, + node_id: self.id, + position: self.position, + timestamp, + }, + )) + } else { + None + } + } + + fn transmit(&mut self, timestamp: Instant) -> Option { + Some(TxToken { + buffer: &mut self.tx_queue, + node_id: self.id, + position: self.position, + timestamp, + }) + } + + fn capabilities(&self) -> smoltcp::phy::DeviceCapabilities { + let mut caps = smoltcp::phy::DeviceCapabilities::default(); + caps.medium = smoltcp::phy::Medium::Ieee802154; + caps.max_transmission_unit = 125; + caps + } +} + +pub struct RxToken { + buffer: Vec, +} + +impl smoltcp::phy::RxToken for RxToken { + fn consume(mut self, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + f(&mut self.buffer) + } +} + +pub struct TxToken<'v> { + buffer: &'v mut VecDeque, + node_id: usize, + position: Position, + timestamp: Instant, +} + +impl<'v> smoltcp::phy::TxToken for TxToken<'v> { + fn consume(self, len: usize, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + let mut buffer = vec![0; len]; + let r = f(&mut buffer); + + let packet = Ieee802154Frame::new_unchecked(&buffer); + let repr = Ieee802154Repr::parse(&packet).unwrap(); + + self.buffer.push_back(Message { + at: self.timestamp, + to: repr.dst_addr.unwrap(), + from: (self.node_id, self.position), + data: buffer, + }); + + r + } +}