diff --git a/Cargo.toml b/Cargo.toml index 29bb2a4bb7..07edf02d49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,10 @@ members=["framework", "test/srv6-sighup-flow", "test/srv6-inject", "test/tcp-checksum", - "test/icmpv6", "test/mtu-too-big", - "test/transform-error" + "test/transform-error", + "test/mtu-too-big", + "test/ndp-router-advertisement" # "test/delay-test", # "test/chain-test", # "test/shutdown-test", diff --git a/examples.sh b/examples.sh index 46f0b73ddf..f0c491b9f6 100644 --- a/examples.sh +++ b/examples.sh @@ -12,9 +12,9 @@ export examples=( test/srv6-sighup-flow test/srv6-inject test/tcp-checksum - test/icmpv6 test/mtu-too-big test/transform-error + test/ndp-router-advertisement # test/delay-test # test/chain-test # test/shutdown-test diff --git a/framework/src/headers/ip/v6/icmp/mod.rs b/framework/src/headers/ip/v6/icmp/mod.rs index 8b5d986229..115adec45d 100644 --- a/framework/src/headers/ip/v6/icmp/mod.rs +++ b/framework/src/headers/ip/v6/icmp/mod.rs @@ -1,4 +1,7 @@ +pub use self::ndp::*; +pub use self::ndp_options::*; pub use self::packet_too_big::*; +pub use self::router_advertisement::*; use super::{EndOffset, Ipv6VarHeader}; use headers::CalcChecksums; use num::FromPrimitive; @@ -6,7 +9,10 @@ use std::default::Default; use std::fmt; use std::marker::PhantomData; +mod ndp; +mod ndp_options; mod packet_too_big; +mod router_advertisement; /* ICMPv6 messages are contained in IPv6 packets. The IPv6 packet contains an IPv6 header followed by the @@ -103,7 +109,7 @@ where #[inline] fn offset(&self) -> usize { - // ICMPv6 Header(Type + Code + Checksum) is always 4 bytes: (8 + 8 + 16) / 8 = 4 + // Standard ICMPv6 Header(Type + Code + Checksum) is 4 bytes: (8 + 8 + 16) / 8 = 4 4 } diff --git a/framework/src/headers/ip/v6/icmp/ndp.rs b/framework/src/headers/ip/v6/icmp/ndp.rs new file mode 100644 index 0000000000..4f5c8bf323 --- /dev/null +++ b/framework/src/headers/ip/v6/icmp/ndp.rs @@ -0,0 +1,5 @@ +use headers::EndOffset; + +/// Marker trait for all types of NdpMessages +/// Ensures all implementers support EndOffset and Sized +pub trait NdpMessageContents: EndOffset + Sized {} diff --git a/framework/src/headers/ip/v6/icmp/ndp_options.rs b/framework/src/headers/ip/v6/icmp/ndp_options.rs new file mode 100644 index 0000000000..9af1c373df --- /dev/null +++ b/framework/src/headers/ip/v6/icmp/ndp_options.rs @@ -0,0 +1,254 @@ +use std::fmt; +use std::slice; + +use num::FromPrimitive; + +use headers::ip::v6::icmp::ndp::NdpMessageContents; +use headers::mac::MacAddress; + +use log::warn; + +#[derive(FromPrimitive, Debug, PartialEq, Hash, Eq, Clone, Copy)] +#[repr(u8)] +pub enum NdpOptionType { + SourceLinkLayerAddress = 1, + TargetLinkLayerAddress = 2, + PrefixInformation = 3, + RedirectHeader = 4, + MTU = 5, + Undefined, +} + +impl fmt::Display for NdpOptionType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + NdpOptionType::SourceLinkLayerAddress => write!(f, "Source Link Layer Address"), + NdpOptionType::TargetLinkLayerAddress => write!(f, "Target Link Layer Address"), + NdpOptionType::PrefixInformation => write!(f, "Prefix Information"), + NdpOptionType::RedirectHeader => write!(f, "Redirect Header"), + NdpOptionType::MTU => write!(f, "MTU"), + NdpOptionType::Undefined => write!(f, "Undefined"), + } + } +} + +pub trait NdpOption { + fn get_type() -> NdpOptionType; + // Size will be used to makes sure we dont go beyond the memory we own when doing memory + // overlay in option_scan. this is equal to the size of the option struct + fn get_size() -> u8; +} + +/** + MTU NDP Option type + + 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 | Length | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | MTU | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Fields: + + Type 5 + + Length 1 + + Reserved This field is unused. It MUST be initialized to + zero by the sender and MUST be ignored by the + receiver. + + MTU 32-bit unsigned integer. The recommended MTU for + the link. + + Description + The MTU option is used in Router Advertisement + messages to ensure that all nodes on a link use the + same MTU value in those cases where the link MTU is + not well known. + + This option MUST be silently ignored for other + Neighbor Discovery messages. + + In configurations in which heterogeneous + technologies are bridged together, the maximum + supported MTU may differ from one segment to + another. If the bridges do not generate ICMP + Packet Too Big messages, communicating nodes will + be unable to use Path MTU to dynamically determine + the appropriate MTU on a per-neighbor basis. In + such cases, routers can be configured to use the + MTU option to specify the maximum MTU value that is + supported by all segments. +*/ +#[derive(Debug)] +#[repr(C, packed)] +pub struct MtuOption { + option_type: u8, + option_length: u8, + reserved: u16, + mtu: u32, +} + +impl MtuOption { + /// Retrieves the value of the mtu in the option payload in little endian + pub fn get_mtu(&self) -> u32 { + u32::from_be(self.mtu) + } + + /// Gets the reserved value from the option payload in little endian + pub fn get_reserved(&self) -> u16 { + u16::from_be(self.reserved) + } +} + +impl NdpOption for MtuOption { + fn get_type() -> NdpOptionType { + NdpOptionType::MTU + } + + fn get_size() -> u8 { + // option_type + option_length + reserved + mtu + // (8 + 8 + 16 + 32) / 8 = 6 + 6 + } +} + +/** + 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 | Length | Link-Layer Address ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Fields: + + Type + 1 for Source Link-layer Address + 2 for Target Link-layer Address + Length The length of the option (including the type and + length fields) in units of 8 octets. For example, + the length for IEEE 802 addresses is 1 + [IPv6-ETHER]. + + Link-Layer Address + The variable length link-layer address. + + The content and format of this field (including + byte and bit ordering) is expected to be specified + in specific documents that describe how IPv6 + operates over different link layers. For instance, + [IPv6-ETHER]. + + Description + The Source Link-Layer Address option contains the + link-layer address of the sender of the packet. It + is used in the Neighbor Solicitation, Router + Solicitation, and Router Advertisement packets. + + The Target Link-Layer Address option contains the + link-layer address of the target. It is used in + Neighbor Advertisement and Redirect packets. + + These options MUST be silently ignored for other + Neighbor Discovery messages. +*/ +#[derive(Debug)] +#[repr(C, packed)] +pub struct LinkLayerAddressOption { + option_type: u8, + option_length: u8, + addr: MacAddress, +} + +impl NdpOption for LinkLayerAddressOption { + fn get_type() -> NdpOptionType { + NdpOptionType::SourceLinkLayerAddress + } + + fn get_size() -> u8 { + // option_type + option_length + mac_address + // (8 + 8 + 48) / 8 = 8 + 8 + } +} + +/// Provides functions to implementors that allow the implementor to scan their NDP message for +/// NDP options. The type of options available are specific to the type of NDP Message +/// Ensures that implementors support NdpMessageContents, which further enforces EndOffset + Sized +/// which we require to do parsing +pub trait NdpOptions: NdpMessageContents { + /// Attempts to lookup the source link layer address from the underlying NDP Message + fn get_source_link_layer_address_option(&self, payload_length: u16) -> Option<&MacAddress> { + let opt = self.option_scan::(payload_length); + opt.map(|sll| &sll.addr) + } + + /// Attempts to lookup the mtu option from the underlying NDP Message + fn get_mtu_option(&self, payload_length: u16) -> Option { + let mtu_option = self + .option_scan::(payload_length) + .map(|mtu| mtu.get_mtu()); + mtu_option + } + + /// Searches through a packet for an option, returns the first it finds or none + /// The underlying type is determine by implementing the NdpOption trait which returns the NDP Option Type + fn option_scan(&self, payload_length: u16) -> Option<&T> { + unsafe { + // the start of the options we offset from is the start of the ndp message + let opt_start = (self as *const Self) as *const u8; + let mut payload_offset = self.offset(); + let requested_type = T::get_type(); + + // make sure we do not scan beyond the end of the packet + while (payload_offset as u16) < payload_length { + // we want to pull the first two bytes off, option type and option length + let seek_to = opt_start.add(payload_offset); + let option_meta = slice::from_raw_parts(seek_to, 2); + + let cur_type_option: Option = FromPrimitive::from_u8(option_meta[0]); + + let cur_type: NdpOptionType = match cur_type_option { + Some(cur_type_option) => cur_type_option, + None => NdpOptionType::Undefined, + }; + + // second option is the length in octets (8 byte chunks). So a length of 1 means 8 bytes + let cur_size = option_meta[1] * 8; + + if cur_size == 0 { + // Don't know how we will be here, but it is an error condition as we will + // go into an infinite loop and blow the stack if we skip, so exit None + warn!("Option length is set to zero for option type {}", cur_type); + return None; + } else if (payload_offset + cur_size as usize) > (payload_length as usize) { + // we need to protect ourselves in case the option_length goes past the payload length + warn!("Option length exceeds the payload length for option type {} option_length {}", cur_type, cur_size); + return None; + } + + if cur_type != requested_type { + // the current type does not match the requested type, so skip to next + payload_offset = payload_offset + (cur_size as usize); + } else { + // we have a winner! + // advance the pointer to the current offset + let cur_start = opt_start.add(payload_offset); + + // This check makes sure that our struct doesnt exceed the payload length + // and overlay memory that we dont own + if (payload_offset + T::get_size() as usize) > (payload_length as usize) { + return None; + } + + let found_opt = cur_start as *const T; + return Some(&(*found_opt)); + } + } + None + } + } +} diff --git a/framework/src/headers/ip/v6/icmp/router_advertisement.rs b/framework/src/headers/ip/v6/icmp/router_advertisement.rs new file mode 100644 index 0000000000..36f4c585bb --- /dev/null +++ b/framework/src/headers/ip/v6/icmp/router_advertisement.rs @@ -0,0 +1,285 @@ +use std::default::Default; +use std::fmt; +use std::marker::PhantomData; + +use headers::ip::v6::icmp::ndp::NdpMessageContents; +use headers::ip::v6::icmp::ndp_options::*; +use headers::{CalcChecksums, EndOffset, Ipv6VarHeader}; +use utils::*; + +use super::{IcmpMessageType, Icmpv6Header}; + +/* + ICMPv6 messages are contained in IPv6 packets. The IPv6 packet contains an IPv6 header followed by the + payload which contains the ICMPv6 message. + + From (https://tools.ietf.org/html/rfc4861) + The ICMPv6 Router Advertisement Messages have the following general format: + + 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 | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cur Hop Limit |M|O| Reserved | Router Lifetime | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Reachable Time | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Retrans Timer | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Options ... + +-+-+-+-+-+-+-+-+-+-+-+- + + ICMP Fields: + + Type 134 + + Code 0 + + Checksum The ICMP checksum. See [ICMPv6]. + + Cur Hop Limit 8-bit unsigned integer. The default value that + should be placed in the Hop Count field of the IP + header for outgoing IP packets. A value of zero + means unspecified (by this router). + + M 1-bit "Managed address configuration" flag. When + set, it indicates that addresses are available via + Dynamic Host Configuration Protocol [DHCPv6]. + + If the M flag is set, the O flag is redundant and + can be ignored because DHCPv6 will return all + available configuration information. + + O 1-bit "Other configuration" flag. When set, it + indicates that other configuration information is + available via DHCPv6. Examples of such information + are DNS-related information or information on other + servers within the network. + + Note: If neither M nor O flags are set, this indicates that no + information is available via DHCPv6. + + Reserved A 6-bit unused field. It MUST be initialized to + zero by the sender and MUST be ignored by the + receiver. + + Router Lifetime + 16-bit unsigned integer. The lifetime associated + with the default router in units of seconds. The + field can contain values up to 65535 and receivers + should handle any value, while the sending rules in + Section 6 limit the lifetime to 9000 seconds. A + Lifetime of 0 indicates that the router is not a + default router and SHOULD NOT appear on the default + router list. The Router Lifetime applies only to + the router's usefulness as a default router; it + does not apply to information contained in other + message fields or options. Options that need time + limits for their information include their own + lifetime fields. + + Reachable Time 32-bit unsigned integer. The time, in + milliseconds, that a node assumes a neighbor is + reachable after having received a reachability + confirmation. Used by the Neighbor Unreachability + Detection algorithm (see Section 7.3). A value of + zero means unspecified (by this router). + + Retrans Timer 32-bit unsigned integer. The time, in + milliseconds, between retransmitted Neighbor + Solicitation messages. Used by address resolution + and the Neighbor Unreachability Detection algorithm + (see Sections 7.2 and 7.3). A value of zero means + unspecified (by this router). + + Possible options: + + Source link-layer address + The link-layer address of the interface from which + the Router Advertisement is sent. Only used on + link layers that have addresses. A router MAY omit + this option in order to enable inbound load sharing + across multiple link-layer addresses. + + MTU SHOULD be sent on links that have a variable MTU + (as specified in the document that describes how to + run IP over the particular link type). MAY be sent + on other links. + + Prefix Information + These options specify the prefixes that are on-link + and/or are used for stateless address + autoconfiguration. A router SHOULD include all its + on-link prefixes (except the link-local prefix) so + that multihomed hosts have complete prefix + information about on-link destinations for the + links to which they attach. If complete + information is lacking, a host with multiple + interfaces may not be able to choose the correct + outgoing interface when sending traffic to its + neighbors. + Future versions of this protocol may define new option types. + Receivers MUST silently ignore any options they do not recognize + and continue processing the message. + +*/ + +const MANAGED_CFG_ADDR_POS: u8 = 0; +const OTHER_CFG_POS: u8 = 1; + +#[derive(Debug)] +#[repr(C, packed)] +pub struct Icmpv6RouterAdvertisement +where + T: Ipv6VarHeader, +{ + icmp: Icmpv6Header, + current_hop_limit: u8, + reserved_flags: u8, + router_lifetime: u16, + reachable_time: u32, + retrans_timer: u32, + options: u8, + _parent: PhantomData, +} + +impl Default for Icmpv6RouterAdvertisement +where + T: Ipv6VarHeader, +{ + fn default() -> Icmpv6RouterAdvertisement { + Icmpv6RouterAdvertisement { + icmp: Icmpv6Header { + msg_type: IcmpMessageType::RouterAdvertisement as u8, + ..Default::default() + }, + current_hop_limit: 0, + reserved_flags: 0, + router_lifetime: 0, + reachable_time: 0, + retrans_timer: 0, + options: 0, + _parent: PhantomData, + } + } +} + +impl fmt::Display for Icmpv6RouterAdvertisement +where + T: Ipv6VarHeader, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "msg_type: {} code: {} checksum: {}, current_hop_limit {}, reserved_flags {}, router_lifetime {}, reachable_time {}, retrans_timers {}", + self.msg_type().unwrap(), + self.code(), + self.checksum(), + self.current_hop_limit(), + self.reserved_flags(), + self.router_lifetime(), + self.reachable_time(), + self.retrans_timer(), + ) + } +} + +impl EndOffset for Icmpv6RouterAdvertisement +where + T: Ipv6VarHeader, +{ + type PreviousHeader = T; + + #[inline] + fn offset(&self) -> usize { + // Router Advertisement static contents follows + // Type (1), Code (1), Checksum (2) = 4 + // Hop Limit (1), Flags+Reserved (1), Lifetime (2) = 4 + // Reachable Time (4), Retrans Timer (4) = 16 bytes total before possible Options + 16 + } + + #[inline] + fn size() -> usize { + // ICMPv6 Header is always 8 bytes so size = offset + 16 + } + + #[inline] + fn payload_size(&self, hint: usize) -> usize { + // There is no payload size in the ICMPv6 header + hint - self.offset() + } + + #[inline] + fn check_correct(&self, _prev: &T) -> bool { + true + } +} + +/// Marker trait for ndp message contents +impl NdpMessageContents for Icmpv6RouterAdvertisement {} + +/// Gives us option parsing super powers!! Well, adds option parsing to a router advertisement +impl NdpOptions for Icmpv6RouterAdvertisement {} + +impl Icmpv6RouterAdvertisement +where + T: Ipv6VarHeader, +{ + #[inline] + pub fn new() -> Self { + Default::default() + } + + #[inline] + pub fn msg_type(&self) -> Option { + self.icmp.msg_type() + } + + #[inline] + pub fn code(&self) -> u8 { + self.icmp.code() + } + + #[inline] + pub fn checksum(&self) -> u16 { + self.icmp.checksum() + } + + #[inline] + pub fn current_hop_limit(&self) -> u8 { + self.current_hop_limit + } + + #[inline] + pub fn reserved_flags(&self) -> u8 { + self.reserved_flags + } + + #[inline] + pub fn managed_addr_cfg(&self) -> bool { + get_bit(self.reserved_flags, MANAGED_CFG_ADDR_POS) + } + + #[inline] + pub fn other_cfg(&self) -> bool { + get_bit(self.reserved_flags, OTHER_CFG_POS) + } + + #[inline] + pub fn router_lifetime(&self) -> u16 { + u16::from_be(self.router_lifetime) + } + + #[inline] + pub fn reachable_time(&self) -> u32 { + u32::from_be(self.reachable_time) + } + + #[inline] + pub fn retrans_timer(&self) -> u32 { + u32::from_be(self.retrans_timer) + } +} diff --git a/framework/src/tests/mod.rs b/framework/src/tests/mod.rs index 921c83640f..38a48fdb29 100644 --- a/framework/src/tests/mod.rs +++ b/framework/src/tests/mod.rs @@ -132,7 +132,7 @@ pub const V6_BYTES: [u8; 62] = [ ]; #[rustfmt::skip] -pub const ICMP_RTR_ADV_BYTES: [u8;136] = [ +pub const ICMP_ROUTER_ADVERTISEMENT_BYTES: [u8;118] = [ // --- Ethernet Header --- // Destination MAC 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, @@ -144,7 +144,7 @@ pub const ICMP_RTR_ADV_BYTES: [u8;136] = [ // Version, Traffic Class, Flow Label 0x60, 0x00, 0x00, 0x00, // Payload Length - 0x00, 0x58, + 0x00, 0x40, // Next Header(ICMPV6=58) 0x3a, // Hop Limit, @@ -164,23 +164,155 @@ pub const ICMP_RTR_ADV_BYTES: [u8;136] = [ // Curr hop limit 0x40, // Flags - 0x40, + 0x58, // Router lifetime - 0x0e, 0x10, + 0x07, 0x08, // Reachable time - 0x00, + 0x00,0x00, 0x08, 0x07, // Retrans timer - 0x00, + 0x00,0x00, 0x05, 0xdc, + // ICMPv6 Option(MTU) + 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x05, 0xdc, + // ICMPv6 Option(Source link-layer address) + 0x01, 0x01, 0xc2, 0x00, 0x54, 0xf5, 0x00, 0x00, // ICMPv6 Option(Prefix Information) 0x03, 0x04, 0x40, 0xc0, 0x00, 0x00, 0x09, 0x3e, 0x00, 0x00, 0x09, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x26, 0x07, 0xfc, 0xc8, 0xf1, 0x42, 0xb0, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub const ICMP_ROUTER_ADVERTISEMENT_BYTES_NO_LINK_LAYER_ADDRESS: [u8;110] = [ + // --- Ethernet Header --- + // Destination MAC + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // Source MAC + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + // EtherType(IPv6) + 0x86, 0xDD, + // --- IPv6 Header --- + // Version, Traffic Class, Flow Label + 0x60, 0x00, 0x00, 0x00, + // Payload Length + 0x00, 0x38, + // Next Header(ICMPV6=58) + 0x3a, + // Hop Limit, + 0xff, + // Source Address + 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4, 0xf0, 0x45, 0xff, 0xfe, 0x0c, 0x66, 0x4b, + // Destination Address + 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // ---ICMPv6 Header--- + // Type + 0x86, + // Code + 0x00, + // Checksum + 0xf5, 0x0c, + // --ICMPv6 Payload-- + // Curr hop limit + 0x40, + // Flags + 0x58, + // Router lifetime + 0x07, 0x08, + // Reachable time + 0x00,0x00, 0x08, 0x07, + // Retrans timer + 0x00,0x00, 0x05, 0xdc, // ICMPv6 Option(MTU) 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x05, 0xdc, - // ICMPv6 Option(Source link-layer address) - 0x01, 0x01, 0x70, 0x3a, 0xcb, 0x1b, 0xf9, 0x7a, - // ICMPv6 Option(Recursive DNS Server) - 0x19, 0x03, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x26, 0x07, 0xfc, 0xc8, 0xf1, 0x42, 0xb0, 0xf0, - 0xd4, 0xf0, 0x45, 0xff, 0xfe, 0x0c, 0x66, 0x4b + // ICMPv6 Option(Prefix Information) + 0x03, 0x04, 0x40, 0xc0, 0x00, 0x00, 0x09, 0x3e, 0x00, 0x00, 0x09, 0x3e, 0x00, 0x00, 0x00, 0x00, + 0x26, 0x07, 0xfc, 0xc8, 0xf1, 0x42, 0xb0, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub const ICMP_ROUTER_ADVERTISEMENT_BYTES_INVALID_OPTION_LENGTH: [u8;78] = [ + // --- Ethernet Header --- + // Destination MAC + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // Source MAC + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + // EtherType(IPv6) + 0x86, 0xDD, + // --- IPv6 Header --- + // Version, Traffic Class, Flow Label + 0x60, 0x00, 0x00, 0x00, + // Payload Length + 0x00, 0x18, + // Next Header(ICMPV6=58) + 0x3a, + // Hop Limit, + 0xff, + // Source Address + 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4, 0xf0, 0x45, 0xff, 0xfe, 0x0c, 0x66, 0x4b, + // Destination Address + 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // ---ICMPv6 Header--- + // Type + 0x86, + // Code + 0x00, + // Checksum + 0xf5, 0x0c, + // --ICMPv6 Payload-- + // Curr hop limit + 0x40, + // Flags + 0x58, + // Router lifetime + 0x07, 0x08, + // Reachable time + 0x00,0x00, 0x08, 0x07, + // Retrans timer + 0x00,0x00, 0x05, 0xdc, + // ICMPv6 Option(MTU) with Invalid Option Length + 0x05, 0x08, 0x00, 0x00, 0x00, 0x00, 0x05, 0xdc, +]; + +#[rustfmt::skip] +pub const ICMP_ROUTER_ADVERTISEMENT_BYTES_INVALID_OPTION_TYPE: [u8;78] = [ + // --- Ethernet Header --- + // Destination MAC + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // Source MAC + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + // EtherType(IPv6) + 0x86, 0xDD, + // --- IPv6 Header --- + // Version, Traffic Class, Flow Label + 0x60, 0x00, 0x00, 0x00, + // Payload Length + 0x00, 0x18, + // Next Header(ICMPV6=58) + 0x3a, + // Hop Limit, + 0xff, + // Source Address + 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4, 0xf0, 0x45, 0xff, 0xfe, 0x0c, 0x66, 0x4b, + // Destination Address + 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // ---ICMPv6 Header--- + // Type + 0x86, + // Code + 0x00, + // Checksum + 0xf5, 0x0c, + // --ICMPv6 Payload-- + // Curr hop limit + 0x40, + // Flags + 0x58, + // Router lifetime + 0x07, 0x08, + // Reachable time + 0x00,0x00, 0x08, 0x07, + // Retrans timer + 0x00,0x00, 0x05, 0xdc, + // ICMPv6 Option(MTU) with Invalid Option Length + 0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0x05, 0xdc, ]; #[rustfmt::skip] diff --git a/framework/tests/packets.rs b/framework/tests/packets.rs index 205e68ca28..424d08d7dd 100644 --- a/framework/tests/packets.rs +++ b/framework/tests/packets.rs @@ -24,9 +24,9 @@ fn packet_from_bytes(bytes: &[u8]) -> Packet { } #[test] -fn icmpv6_from_bytes() { +fn ndp_router_advertisement_from_bytes() { dpdk_test! { - let pkt = packet_from_bytes(&ICMP_RTR_ADV_BYTES); + let pkt = packet_from_bytes(&ICMP_ROUTER_ADVERTISEMENT_BYTES); // Check Ethernet header let epkt = pkt.parse_header::(); { @@ -38,6 +38,8 @@ fn icmpv6_from_bytes() { // Check IPv6 header let v6pkt = epkt.parse_header::(); + let &v6 = v6pkt.get_header(); + let payload_len = v6.payload_len(); { let v6 = v6pkt.get_header(); let src = Ipv6Addr::from_str("fe80::d4f0:45ff:fe0c:664b").unwrap(); @@ -45,7 +47,7 @@ fn icmpv6_from_bytes() { assert_eq!(v6.version(), 6); assert_eq!(v6.traffic_class(), 0); assert_eq!(v6.flow_label(), 0); - assert_eq!(v6.payload_len(), 88); + assert_eq!(payload_len, 64); assert_eq!(v6.next_header().unwrap(), NextHeader::Icmp); assert_eq!(v6.hop_limit(), 255); assert_eq!(Ipv6Addr::from(v6.src()), src); @@ -53,12 +55,173 @@ fn icmpv6_from_bytes() { } //Check Icmp header - let icmp_pkt = v6pkt.parse_header::>(); + let icmp_pkt = v6pkt.parse_header::>(); { let icmpv6h = icmp_pkt.get_header(); assert_eq!(icmpv6h.msg_type().unwrap(), IcmpMessageType::RouterAdvertisement); assert_eq!(icmpv6h.checksum(), 0xf50c); assert_eq!(icmpv6h.code(), 0); + assert_eq!(icmpv6h.current_hop_limit(), 64); + assert_eq!(icmpv6h.managed_addr_cfg(), false); + assert_eq!(icmpv6h.other_cfg(), true); + assert_eq!(icmpv6h.router_lifetime(), 1800); + assert_eq!(icmpv6h.reachable_time(), 2055); + assert_eq!(icmpv6h.retrans_timer(), 1500); + assert_eq!( + format!("{:X?}", icmpv6h.get_source_link_layer_address_option(payload_len).unwrap()), + format!("{:X?}", MacAddress::from_str("c2:00:54:f5:00:00").unwrap()) + ); + assert_eq!(icmpv6h.get_mtu_option(payload_len).unwrap(), 1500); + } + } +} + +#[test] +fn ndp_router_advertisement_from_bytes_no_link_layer_address() { + dpdk_test! { + let pkt = packet_from_bytes(&ICMP_ROUTER_ADVERTISEMENT_BYTES_NO_LINK_LAYER_ADDRESS ); + // Check Ethernet header + let epkt = pkt.parse_header::(); + { + let eth = epkt.get_header(); + assert_eq!(eth.dst().addr, MacAddress::new(0, 0, 0, 0, 0, 1).addr); + assert_eq!(eth.src().addr, MacAddress::new(0, 0, 0, 0, 0, 2).addr); + assert_eq!(eth.etype(), Some(EtherType::IPv6)); + } + + // Check IPv6 header + let v6pkt = epkt.parse_header::(); + let &v6 = v6pkt.get_header(); + let payload_len = v6.payload_len(); + { + let v6 = v6pkt.get_header(); + let src = Ipv6Addr::from_str("fe80::d4f0:45ff:fe0c:664b").unwrap(); + let dst = Ipv6Addr::from_str("ff02::1").unwrap(); + assert_eq!(v6.version(), 6); + assert_eq!(v6.traffic_class(), 0); + assert_eq!(v6.flow_label(), 0); + assert_eq!(v6.payload_len(), 56); + assert_eq!(v6.next_header().unwrap(), NextHeader::Icmp); + assert_eq!(v6.hop_limit(), 255); + assert_eq!(Ipv6Addr::from(v6.src()), src); + assert_eq!(Ipv6Addr::from(v6.dst()), dst); + } + + //Check Icmp header + let icmp_pkt = v6pkt.parse_header::>(); + { + let icmpv6h = icmp_pkt.get_header(); + assert_eq!(icmpv6h.msg_type().unwrap(), IcmpMessageType::RouterAdvertisement); + assert_eq!(icmpv6h.checksum(), 0xf50c); + assert_eq!(icmpv6h.code(), 0); + assert_eq!(icmpv6h.current_hop_limit(), 64); + assert_eq!(icmpv6h.managed_addr_cfg(), false); + assert_eq!(icmpv6h.other_cfg(), true); + assert_eq!(icmpv6h.router_lifetime(), 1800); + assert_eq!(icmpv6h.reachable_time(), 2055); + assert_eq!(icmpv6h.retrans_timer(), 1500); + assert_eq!(icmpv6h.get_source_link_layer_address_option(payload_len).is_some(), false); + assert_eq!(icmpv6h.get_mtu_option(payload_len).unwrap(), 1500); + } + } +} + +#[test] +fn ndp_router_advertisement_from_bytes_invalid_option_length() { + dpdk_test! { + let pkt = packet_from_bytes(&ICMP_ROUTER_ADVERTISEMENT_BYTES_INVALID_OPTION_LENGTH ); + // Check Ethernet header + let epkt = pkt.parse_header::(); + { + let eth = epkt.get_header(); + assert_eq!(eth.dst().addr, MacAddress::new(0, 0, 0, 0, 0, 1).addr); + assert_eq!(eth.src().addr, MacAddress::new(0, 0, 0, 0, 0, 2).addr); + assert_eq!(eth.etype(), Some(EtherType::IPv6)); + } + + // Check IPv6 header + let v6pkt = epkt.parse_header::(); + let &v6 = v6pkt.get_header(); + let payload_len = v6.payload_len(); + { + let v6 = v6pkt.get_header(); + let src = Ipv6Addr::from_str("fe80::d4f0:45ff:fe0c:664b").unwrap(); + let dst = Ipv6Addr::from_str("ff02::1").unwrap(); + assert_eq!(v6.version(), 6); + assert_eq!(v6.traffic_class(), 0); + assert_eq!(v6.flow_label(), 0); + assert_eq!(v6.payload_len(), 24); + assert_eq!(v6.next_header().unwrap(), NextHeader::Icmp); + assert_eq!(v6.hop_limit(), 255); + assert_eq!(Ipv6Addr::from(v6.src()), src); + assert_eq!(Ipv6Addr::from(v6.dst()), dst); + } + + //Check Icmp header + let icmp_pkt = v6pkt.parse_header::>(); + { + let icmpv6h = icmp_pkt.get_header(); + assert_eq!(icmpv6h.msg_type().unwrap(), IcmpMessageType::RouterAdvertisement); + assert_eq!(icmpv6h.checksum(), 0xf50c); + assert_eq!(icmpv6h.code(), 0); + assert_eq!(icmpv6h.current_hop_limit(), 64); + assert_eq!(icmpv6h.managed_addr_cfg(), false); + assert_eq!(icmpv6h.other_cfg(), true); + assert_eq!(icmpv6h.router_lifetime(), 1800); + assert_eq!(icmpv6h.reachable_time(), 2055); + assert_eq!(icmpv6h.retrans_timer(), 1500); + assert_eq!(icmpv6h.get_source_link_layer_address_option(payload_len).is_some(), false); + assert_eq!(icmpv6h.get_mtu_option(payload_len).is_some(), false); + } + } +} + +#[test] +fn ndp_router_advertisement_from_bytes_invalid_option_type() { + dpdk_test! { + let pkt = packet_from_bytes(&ICMP_ROUTER_ADVERTISEMENT_BYTES_INVALID_OPTION_TYPE ); + // Check Ethernet header + let epkt = pkt.parse_header::(); + { + let eth = epkt.get_header(); + assert_eq!(eth.dst().addr, MacAddress::new(0, 0, 0, 0, 0, 1).addr); + assert_eq!(eth.src().addr, MacAddress::new(0, 0, 0, 0, 0, 2).addr); + assert_eq!(eth.etype(), Some(EtherType::IPv6)); + } + + // Check IPv6 header + let v6pkt = epkt.parse_header::(); + let &v6 = v6pkt.get_header(); + let payload_len = v6.payload_len(); + { + let v6 = v6pkt.get_header(); + let src = Ipv6Addr::from_str("fe80::d4f0:45ff:fe0c:664b").unwrap(); + let dst = Ipv6Addr::from_str("ff02::1").unwrap(); + assert_eq!(v6.version(), 6); + assert_eq!(v6.traffic_class(), 0); + assert_eq!(v6.flow_label(), 0); + assert_eq!(v6.payload_len(), 24); + assert_eq!(v6.next_header().unwrap(), NextHeader::Icmp); + assert_eq!(v6.hop_limit(), 255); + assert_eq!(Ipv6Addr::from(v6.src()), src); + assert_eq!(Ipv6Addr::from(v6.dst()), dst); + } + + //Check Icmp header + let icmp_pkt = v6pkt.parse_header::>(); + { + let icmpv6h = icmp_pkt.get_header(); + assert_eq!(icmpv6h.msg_type().unwrap(), IcmpMessageType::RouterAdvertisement); + assert_eq!(icmpv6h.checksum(), 0xf50c); + assert_eq!(icmpv6h.code(), 0); + assert_eq!(icmpv6h.current_hop_limit(), 64); + assert_eq!(icmpv6h.managed_addr_cfg(), false); + assert_eq!(icmpv6h.other_cfg(), true); + assert_eq!(icmpv6h.router_lifetime(), 1800); + assert_eq!(icmpv6h.reachable_time(), 2055); + assert_eq!(icmpv6h.retrans_timer(), 1500); + assert_eq!(icmpv6h.get_source_link_layer_address_option(payload_len).is_some(), false); + assert_eq!(icmpv6h.get_mtu_option(payload_len).is_some(), false); } } } diff --git a/test/icmpv6/check.sh b/test/icmpv6/check.sh deleted file mode 100755 index 8d929d7c05..0000000000 --- a/test/icmpv6/check.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -TEST_NAME=icmpv6 - -C='\033[1;34m' -NC='\033[0m' - -echo -e "${C}RUNNING: $TEST_NAME${NC}" - -PORT_OPTIONS2="dpdk:eth_pcap0,rx_pcap=data/icmpv6.pcap,tx_pcap=data/out.pcap" - -../../build.sh run $TEST_NAME -p $PORT_OPTIONS2 -c 1 --dur 1 diff --git a/test/icmpv6/data/icmpv6.pcap b/test/icmpv6/data/icmpv6.pcap deleted file mode 100644 index 41142e9b1a..0000000000 Binary files a/test/icmpv6/data/icmpv6.pcap and /dev/null differ diff --git a/test/icmpv6/src/main.rs b/test/icmpv6/src/main.rs index 7c32cdf2cd..74570c4176 100644 --- a/test/icmpv6/src/main.rs +++ b/test/icmpv6/src/main.rs @@ -13,6 +13,7 @@ use std::process; use std::sync::Arc; use std::thread; use std::time::Duration; + mod nf; fn test(ports: Vec, sched: &mut S) diff --git a/test/icmpv6/src/nf.rs b/test/icmpv6/src/nf.rs deleted file mode 100644 index ca77c45aed..0000000000 --- a/test/icmpv6/src/nf.rs +++ /dev/null @@ -1,82 +0,0 @@ -use colored::*; -use netbricks::headers::*; -use netbricks::operators::*; -use std::default::Default; -use std::net::Ipv6Addr; - -struct Meta { - src_ip: Ipv6Addr, - dst_ip: Ipv6Addr, -} - -impl Default for Meta { - fn default() -> Meta { - Meta { - src_ip: Ipv6Addr::UNSPECIFIED, - dst_ip: Ipv6Addr::UNSPECIFIED, - } - } -} - -#[inline] -fn icmp_v6_nf>(parent: T) -> CompositionBatch { - println!( - "{}", - format!("Tests ICMPv6 messages for msg_type, code and checksum").white() - ); - parent - .parse::() - .metadata(box |pkt| Meta { - src_ip: pkt.get_header().src(), - dst_ip: pkt.get_header().dst(), - }) - .parse::>() - .transform(box |pkt| { - let segment_length = pkt.segment_length(Protocol::Icmp); - let src = pkt.read_metadata().src_ip; - let dst = pkt.read_metadata().dst_ip; - - let icmpv6 = pkt.get_mut_header(); - println!( - "{}", - format!( - " Msg Type: {:X?} | Code: {} | Checksum: {:X?}", - icmpv6.msg_type().unwrap(), - icmpv6.code(), - icmpv6.checksum() - ) - .purple() - ); - - assert_eq!( - format!("{:X?}", icmpv6.msg_type().unwrap()), - format!("{:X?}", IcmpMessageType::RouterAdvertisement) - ); - assert_eq!(format!("{:X?}", icmpv6.code()), format!("{:X?}", 0)); - assert_eq!( - format!("{:X?}", icmpv6.checksum()), - format!("{:X?}", 0xf50c) - ); - - let prev_checksum = icmpv6.checksum(); - icmpv6.set_checksum(0); - icmpv6.update_v6_checksum(segment_length, src, dst, Protocol::Icmp); - - assert_eq!( - format!("{:X?}", prev_checksum), - format!("{:X?}", icmpv6.checksum()) - ); - }) - .compose() -} - -pub fn icmp_nf>(parent: T) -> CompositionBatch { - let pipeline = parent - .parse::() - .filter(box |pkt| match pkt.get_header().etype() { - Some(EtherType::IPv6) => true, - _ => false, - }); - - icmp_v6_nf(pipeline) -} diff --git a/test/icmpv6/Cargo.toml b/test/ndp-router-advertisement/Cargo.toml similarity index 87% rename from test/icmpv6/Cargo.toml rename to test/ndp-router-advertisement/Cargo.toml index a784b132e0..cfee6633bc 100644 --- a/test/icmpv6/Cargo.toml +++ b/test/ndp-router-advertisement/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "icmpv6" +name = "ndp-router-advertisement" version = "0.1.0" authors = ["William of Ockham "] diff --git a/test/ndp-router-advertisement/check.sh b/test/ndp-router-advertisement/check.sh new file mode 100755 index 0000000000..d5543ecefb --- /dev/null +++ b/test/ndp-router-advertisement/check.sh @@ -0,0 +1,22 @@ +#!/bin/bash +TEST_NAME=ndp-router-advertisement + +C='\033[1;34m' +NC='\033[0m' + +echo -e "${C}RUNNING: $TEST_NAME${NC}" + +PORT_OPTIONS="dpdk:eth_pcap0,rx_pcap=data/ndp_router_advertisement.pcap,tx_pcap=/tmp/out.pcap" + +../../build.sh run $TEST_NAME -p $PORT_OPTIONS -c 1 --dur 1 +tcpdump -tner /tmp/out.pcap | tee /dev/tty | diff - data/expect_ndp_router_advertisement.out + +TEST_ROUTER_ADVERTISEMENT=$? + +echo ---- +if [[ $TEST_ROUTER_ADVERTISEMENT != 0 ]]; then + echo "FAIL: Router Advertisement Test - $TEST_ROUTER_ADVERTISEMENT" + exit 1 +else + echo "PASS" +fi \ No newline at end of file diff --git a/test/ndp-router-advertisement/data/expect_ndp_router_advertisement.out b/test/ndp-router-advertisement/data/expect_ndp_router_advertisement.out new file mode 100644 index 0000000000..f91b908f85 --- /dev/null +++ b/test/ndp-router-advertisement/data/expect_ndp_router_advertisement.out @@ -0,0 +1 @@ +c2:00:54:f5:00:00 > 33:33:00:00:00:01, ethertype IPv6 (0x86dd), length 118: fe80::c000:54ff:fef5:0 > ff02::1: ICMP6, router advertisement, length 64 diff --git a/test/ndp-router-advertisement/data/ndp_router_advertisement.pcap b/test/ndp-router-advertisement/data/ndp_router_advertisement.pcap new file mode 100644 index 0000000000..b6d9f6b5ac Binary files /dev/null and b/test/ndp-router-advertisement/data/ndp_router_advertisement.pcap differ diff --git a/test/ndp-router-advertisement/data/out.pcap b/test/ndp-router-advertisement/data/out.pcap new file mode 100644 index 0000000000..d0d53ff19f Binary files /dev/null and b/test/ndp-router-advertisement/data/out.pcap differ diff --git a/test/ndp-router-advertisement/src/main.rs b/test/ndp-router-advertisement/src/main.rs new file mode 100644 index 0000000000..59c269d54b --- /dev/null +++ b/test/ndp-router-advertisement/src/main.rs @@ -0,0 +1,78 @@ +#![feature(box_syntax)] +#![feature(asm)] +extern crate colored; +extern crate netbricks; +use self::nf::*; +use netbricks::config::{basic_opts, read_matches}; +use netbricks::interface::*; +use netbricks::operators::*; +use netbricks::scheduler::*; +use std::env; +use std::fmt::Display; +use std::process; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +mod nf; + +fn test(ports: Vec, sched: &mut S) +where + T: PacketRx + PacketTx + Display + Clone + 'static, + S: Scheduler + Sized, +{ + println!("Receiving started"); + + let pipelines: Vec<_> = ports + .iter() + .map(|port| ndp_nf(ReceiveBatch::new(port.clone())).send(port.clone())) + .collect(); + println!("Running {} pipelines", pipelines.len()); + for pipeline in pipelines { + sched.add_task(pipeline).unwrap(); + } +} + +fn main() { + let mut opts = basic_opts(); + opts.optopt( + "", + "dur", + "Test duration", + "If this option is set to a nonzero value, then the \ + test will just loop after 2 seconds", + ); + + let args: Vec = env::args().collect(); + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => panic!(f.to_string()), + }; + + let configuration = read_matches(&matches, &opts); + + let test_duration: u64 = matches + .opt_str("dur") + .unwrap_or_else(|| String::from("0")) + .parse() + .expect("Could not parse test duration"); + + match initialize_system(&configuration) { + Ok(mut context) => { + context.start_schedulers(); + context.add_pipeline_to_run(Arc::new(move |p, s: &mut StandaloneScheduler| test(p, s))); + context.execute(); + + if test_duration != 0 { + thread::sleep(Duration::from_secs(test_duration)); + } else { + loop { + thread::sleep(Duration::from_secs(2)); + } + } + } + Err(ref e) => { + println!("Error: {}", e); + process::exit(1); + } + } +} diff --git a/test/ndp-router-advertisement/src/nf.rs b/test/ndp-router-advertisement/src/nf.rs new file mode 100644 index 0000000000..7b627ee865 --- /dev/null +++ b/test/ndp-router-advertisement/src/nf.rs @@ -0,0 +1,111 @@ +use colored::*; +use netbricks::headers::*; +use netbricks::operators::*; +use std::str::FromStr; + +struct Meta { + payload_len: u16, +} + +pub fn ndp_nf>(parent: T) -> CompositionBatch { + let pipeline = parent + .parse::() + .filter(box |pkt| match pkt.get_header().etype() { + Some(EtherType::IPv6) => true, + _ => false, + }); + + ndp_router_advertisement_nf(pipeline) +} + +#[inline] +fn ndp_router_advertisement_nf>( + parent: T, +) -> CompositionBatch { + println!( + "{}", + format!("Tests ICMPv6 messages for msg_type, code and checksum").white() + ); + parent + .parse::() + .metadata(box |pkt| Meta { + payload_len: pkt.get_header().payload_len(), + }) + .parse::>() + .transform(box |pkt| { + let payload_len = pkt.read_metadata().payload_len; + let dl = pkt.data_len(); + let router_advertisement = pkt.get_mut_header(); + + println!("payload_len {}; data_len {}", payload_len, dl); + + println!( + "{}", + format!( + " Msg Type: {:X?} | Code: {} | Checksum: {:X?}", + router_advertisement.msg_type().unwrap(), + router_advertisement.code(), + router_advertisement.checksum() + ) + .purple() + ); + + assert_eq!( + format!("{:X?}", router_advertisement.msg_type().unwrap()), + format!("{:X?}", IcmpMessageType::RouterAdvertisement) + ); + assert_eq!( + format!("{:X?}", router_advertisement.code()), + format!("{:X?}", 0) + ); + assert_eq!( + format!("{:X?}", router_advertisement.checksum()), + format!("{:X?}", 0xbff2) + ); + assert_eq!( + format!("{:X?}", router_advertisement.current_hop_limit()), + format!("{:X?}", 64) + ); + assert_eq!( + format!("{:X?}", router_advertisement.managed_addr_cfg()), + format!("{:X?}", true) + ); + assert_eq!( + format!("{:X?}", router_advertisement.other_cfg()), + format!("{:X?}", true) + ); + assert_eq!( + format!("{:X?}", router_advertisement.router_lifetime()), + format!("{:X?}", 1800) + ); + assert_eq!( + format!("{:X?}", router_advertisement.reachable_time()), + format!("{:X?}", 600) + ); + assert_eq!( + format!("{:X?}", router_advertisement.retrans_timer()), + format!("{:X?}", 500) + ); + + assert_eq!(format!("{:X?}", payload_len), format!("{:X?}", 64)); + + assert_eq!( + format!( + "{:X?}", + router_advertisement + .get_source_link_layer_address_option(payload_len) + .unwrap() + ), + format!("{:X?}", MacAddress::from_str("c2:00:54:f5:00:00").unwrap()) + ); + + assert_eq!( + format!( + "{:X?}", + router_advertisement.get_mtu_option(payload_len).unwrap() + ), + format!("{:X?}", 1500) + ); + }) + .compose() +}