diff --git a/.gitignore b/.gitignore index a0bdd6c..bd6329a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ target vendor/ tags *.swp +AGENTS.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..0575d90 --- /dev/null +++ b/TODO.md @@ -0,0 +1,65 @@ +# TODO + +## Bugs found with nlmon captured packets (fixed) + +- `EthtoolPauseStatAttr::Tx` emitted `ETHTOOL_A_PAUSE_STAT_RX_FRAMES` + instead of `ETHTOOL_A_PAUSE_STAT_TX_FRAMES` (`src/pause/attr.rs`). +- `EthtoolLinkModeAttr::Ours`/`Peer`, `EthtoolTsInfoAttr` bitsets and + `EthtoolFecAttr::Modes`/`Stats` were emitted without the + `NLA_F_NESTED` flag the kernel sets. +- Feature verbose bitset parsing dropped index 0 and name-less bits and + ignored `ETHTOOL_A_BITSET_SIZE`/`ETHTOOL_A_BITSET_NOMASK`, so the + kernel reply could not be emitted back byte for byte. +- Compact bitsets (`ETHTOOL_FLAG_COMPACT_BITSETS`, used by `ethtool -k`) + failed to parse with "No ETHTOOL_A_BITSET_BITS NLA found". +- FEC stats failed to parse when the driver has no corrected bits stats + (empty `ETHTOOL_A_FEC_STAT_CORR_BITS`) or reports per lane values. +- Emitting parsed bitsets panicked with `todo!()` for feature, link mode, + timestamping and FEC messages. + +## unit tests + +- Module EEPROM (`ethtool -m`) reply parsing has no captured-packet + test. Requires SFP hardware which none of the virtual NICs + (virtio-net, veth, netdevsim) provide. +- `EthtoolTsInfoAttr::TxTypes`/`RxFilters` parse paths are not covered, + virtio-net, veth and netdevsim do not report tx types/rx filters. +- Non-empty `EthtoolLinkModeAttr::Ours`/`Peer` bitsets are not covered, + none of the available virtual NICs reports supported link modes. + +## API review + +- Review whether public types need `#[non_exhaustive]`. Candidates: + `EthtoolCmd` (kernel keeps adding commands), `EthtoolBitsetValue` + (two UAPI forms today). Attribute enums already have + `Other(DefaultNla)` catch-alls. `EthtoolBit`/`EthtoolBitset` have + public fields users need to construct for set requests, adding + `#[non_exhaustive]` there would block struct literals. + +## attributes kept as `DefaultNla` + +- `link_mode`: `ETHTOOL_A_LINKMODES_RATE_MATCHING` (10). +- `rings`: `ETHTOOL_A_RINGS_RX_PUSH` (14), + `ETHTOOL_A_RINGS_TX_PUSH_BUF_LEN` (15), + `ETHTOOL_A_RINGS_TX_PUSH_BUF_LEN_MAX` (16), + `ETHTOOL_A_RINGS_HDS_THRESH` (17), + `ETHTOOL_A_RINGS_HDS_THRESH_MAX` (18). +- `pause`: `ETHTOOL_A_PAUSE_STATS_SRC` (6) and + `ETHTOOL_A_PAUSE_STAT_TX_PAUSE_STORM_EVENTS` (4). +- `tsinfo`: `ETHTOOL_A_TSINFO_HWTSTAMP_PROVIDER` (7), + `ETHTOOL_A_TSINFO_HWTSTAMP_SOURCE` (8), + `ETHTOOL_A_TSINFO_HWTSTAMP_PHYINDEX` (9). +- `EthtoolModuleEEPROMAttr` only parses `HEADER` and `DATA`, + `OFFSET`/`LENGTH`/`PAGE`/`BANK`/`I2C_ADDRESS` fall through to + `DefaultNla` (they are only used in requests today). + +## missing support + +- `EthtoolMessage` parses a subset of ethtool commands only: `STATS_GET`, + `MODULE_GET`, `EEE_GET`, `PHY_GET`, `TSCONFIG_GET`, ... return + "Unsupported ethtool reply command". +- Compact bitsets keep the raw `value`/`mask` words, there is no bit + name table to resolve the enabled features from a compact reply. +- Unknown attributes nested inside a bitset are dropped with a warning, + a later kernel addition would break byte-exact round-trip of parsed + messages. diff --git a/src/bitset.rs b/src/bitset.rs new file mode 100644 index 0000000..eaa18ee --- /dev/null +++ b/src/bitset.rs @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: MIT + +use log::warn; +use netlink_packet_core::{ + emit_u32, parse_string, parse_u32, DecodeError, Emitable, ErrorContext, + Nla, NlasIterator, NLA_F_NESTED, +}; + +const ETHTOOL_A_BITSET_NOMASK: u16 = 1; +const ETHTOOL_A_BITSET_SIZE: u16 = 2; +const ETHTOOL_A_BITSET_BITS: u16 = 3; +const ETHTOOL_A_BITSET_VALUE: u16 = 4; +const ETHTOOL_A_BITSET_MASK: u16 = 5; + +const ETHTOOL_A_BITSET_BITS_BIT: u16 = 1; + +const ETHTOOL_A_BITSET_BIT_INDEX: u16 = 1; +const ETHTOOL_A_BITSET_BIT_NAME: u16 = 2; +const ETHTOOL_A_BITSET_BIT_VALUE: u16 = 3; + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct EthtoolBit { + pub index: u32, + pub name: String, + pub value: bool, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum EthtoolBitsetValue { + /// Verbose (non compact) form: list of bits with names. + Bits(Vec), + /// Compact form (`ETHTOOL_FLAG_COMPACT_BITSETS`): value bitmap words + /// and, unless `EthtoolBitset::nomask` is set, mask bitmap words. + Compact { value: Vec, mask: Vec }, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct EthtoolBitset { + /// Whether `ETHTOOL_A_BITSET_NOMASK` flag was present, which means + /// this bitset has no mask. + pub nomask: bool, + /// Number of bits in this bitset (`ETHTOOL_A_BITSET_SIZE`). + pub size: u32, + pub value: EthtoolBitsetValue, +} + +impl EthtoolBitset { + pub(crate) fn parse(payload: &[u8]) -> Result { + let mut nomask = false; + let mut size = 0u32; + let mut bits = None; + let mut value = None; + let mut mask = None; + let error_msg = "failed to parse bitset attributes"; + for nla in NlasIterator::new(payload) { + let nla = &nla.context(error_msg)?; + match nla.kind() { + ETHTOOL_A_BITSET_NOMASK => nomask = true, + ETHTOOL_A_BITSET_SIZE => { + size = parse_u32(nla.value()) + .context("Invalid ETHTOOL_A_BITSET_SIZE value")? + } + ETHTOOL_A_BITSET_BITS => { + bits = Some(parse_bits(nla.value())?); + } + ETHTOOL_A_BITSET_VALUE => { + value = Some(parse_u32_words(nla.value())?); + } + ETHTOOL_A_BITSET_MASK => { + mask = Some(parse_u32_words(nla.value())?); + } + kind => { + warn!( + "Unknown ETHTOOL_A_BITSET_* attribute {kind} {:?}", + nla.value() + ); + } + } + } + let value = match (bits, value) { + (Some(mut bits), _) => { + if nomask { + // For bitsets without mask, only set bits are listed, + // hence any listed bit is enabled. + for bit in bits.iter_mut() { + bit.value = true; + } + } + EthtoolBitsetValue::Bits(bits) + } + (None, Some(value)) => EthtoolBitsetValue::Compact { + value, + mask: mask.unwrap_or_default(), + }, + (None, None) => { + return Err("Invalid bitset: neither BITS nor VALUE".into()) + } + }; + Ok(Self { + nomask, + size, + value, + }) + } +} + +fn parse_bits(raw: &[u8]) -> Result, DecodeError> { + let error_msg = "Failed to parse ETHTOOL_A_BITSET_BITS attributes"; + let mut bits = Vec::new(); + for bit_nla in NlasIterator::new(raw) { + let bit_nla = &bit_nla.context(error_msg)?; + match bit_nla.kind() { + ETHTOOL_A_BITSET_BITS_BIT => { + bits.push(parse_bit(bit_nla.value())?); + } + kind => { + warn!( + "Unknown ETHTOOL_A_BITSET_BITS attribute {kind} {:?}", + bit_nla.value() + ); + } + } + } + Ok(bits) +} + +fn parse_bit(raw: &[u8]) -> Result { + let error_msg = "Failed to parse ETHTOOL_A_BITSET_BITS_BIT attributes"; + let mut bit = EthtoolBit::default(); + let mut index_found = false; + for nla in NlasIterator::new(raw) { + let nla = &nla.context(error_msg)?; + let payload = nla.value(); + match nla.kind() { + ETHTOOL_A_BITSET_BIT_INDEX => { + bit.index = parse_u32(payload) + .context("Invalid ETHTOOL_A_BITSET_BIT_INDEX value")?; + index_found = true; + } + ETHTOOL_A_BITSET_BIT_NAME => { + bit.name = parse_string(payload) + .context("Invalid ETHTOOL_A_BITSET_BIT_NAME value")?; + } + ETHTOOL_A_BITSET_BIT_VALUE => bit.value = true, + kind => { + warn!( + "Unknown ETHTOOL_A_BITSET_BITS_BIT attribute {kind} {:?}", + nla.value() + ); + } + } + } + if !index_found { + return Err("Missing ETHTOOL_A_BITSET_BIT_INDEX".into()); + } + Ok(bit) +} + +fn parse_u32_words(raw: &[u8]) -> Result, DecodeError> { + if !raw.len().is_multiple_of(4) { + return Err(format!( + "Invalid bitmap length {}, not multiple of 4", + raw.len() + ) + .into()); + } + Ok(raw + .as_chunks::<4>() + .0 + .iter() + .map(|w| u32::from_ne_bytes(*w)) + .collect()) +} + +const fn align4(len: usize) -> usize { + (len + 3) & !3 +} + +const fn nla_len(value_len: usize) -> usize { + align4(value_len) + 4 +} + +struct BitsetFlag(u16); + +impl Nla for BitsetFlag { + fn value_len(&self) -> usize { + 0 + } + + fn kind(&self) -> u16 { + self.0 + } + + fn emit_value(&self, _buffer: &mut [u8]) {} +} + +struct BitsetU32(u16, u32); + +impl Nla for BitsetU32 { + fn value_len(&self) -> usize { + 4 + } + + fn kind(&self) -> u16 { + self.0 + } + + fn emit_value(&self, buffer: &mut [u8]) { + emit_u32(buffer, self.1).unwrap(); + } +} + +struct BitsetStr<'a>(u16, &'a str); + +impl Nla for BitsetStr<'_> { + fn value_len(&self) -> usize { + self.1.len() + 1 + } + + fn kind(&self) -> u16 { + self.0 + } + + fn emit_value(&self, buffer: &mut [u8]) { + buffer[..self.1.len()].copy_from_slice(self.1.as_bytes()); + buffer[self.1.len()] = 0; + } +} + +struct BitsetWords<'a>(u16, &'a [u32]); + +impl Nla for BitsetWords<'_> { + fn value_len(&self) -> usize { + self.1.len() * 4 + } + + fn kind(&self) -> u16 { + self.0 + } + + fn emit_value(&self, buffer: &mut [u8]) { + for (i, word) in self.1.iter().enumerate() { + emit_u32(&mut buffer[i * 4..], *word).unwrap(); + } + } +} + +struct BitsetBitNla<'a>(&'a EthtoolBit, bool); + +impl Nla for BitsetBitNla<'_> { + fn value_len(&self) -> usize { + let mut len = nla_len(4); // index + len += nla_len(self.0.name.len() + 1); // name + if self.1 && self.0.value { + len += nla_len(0); // value flag, only for masked bitsets + } + len + } + + fn kind(&self) -> u16 { + ETHTOOL_A_BITSET_BITS_BIT | NLA_F_NESTED + } + + fn emit_value(&self, buffer: &mut [u8]) { + let index = BitsetU32(ETHTOOL_A_BITSET_BIT_INDEX, self.0.index); + let name = BitsetStr(ETHTOOL_A_BITSET_BIT_NAME, self.0.name.as_str()); + let mut offset = 0; + index.emit(&mut buffer[offset..]); + offset += index.buffer_len(); + name.emit(&mut buffer[offset..]); + offset += name.buffer_len(); + if self.1 && self.0.value { + let value = BitsetFlag(ETHTOOL_A_BITSET_BIT_VALUE); + value.emit(&mut buffer[offset..]); + } + } +} + +struct BitsetBitsNla<'a>(&'a [EthtoolBit], bool); + +impl Nla for BitsetBitsNla<'_> { + fn value_len(&self) -> usize { + self.0 + .iter() + .map(|bit| nla_len(BitsetBitNla(bit, self.1).value_len())) + .sum() + } + + fn kind(&self) -> u16 { + ETHTOOL_A_BITSET_BITS | NLA_F_NESTED + } + + fn emit_value(&self, buffer: &mut [u8]) { + let mut offset = 0; + for bit in self.0 { + let nla = BitsetBitNla(bit, self.1); + nla.emit(&mut buffer[offset..]); + offset += nla.buffer_len(); + } + } +} + +impl Emitable for EthtoolBitset { + fn buffer_len(&self) -> usize { + let mut len = 0; + if self.nomask { + len += nla_len(0); + } + len += nla_len(4); + match &self.value { + EthtoolBitsetValue::Bits(bits) => { + len += nla_len(BitsetBitsNla(bits, !self.nomask).value_len()); + } + EthtoolBitsetValue::Compact { value, mask } => { + len += nla_len(value.len() * 4); + if !self.nomask { + len += nla_len(mask.len() * 4); + } + } + } + len + } + + fn emit(&self, buffer: &mut [u8]) { + let mut offset = 0; + if self.nomask { + let nla = BitsetFlag(ETHTOOL_A_BITSET_NOMASK); + nla.emit(&mut buffer[offset..]); + offset += nla.buffer_len(); + } + let size = BitsetU32(ETHTOOL_A_BITSET_SIZE, self.size); + size.emit(&mut buffer[offset..]); + offset += size.buffer_len(); + match &self.value { + EthtoolBitsetValue::Bits(bits) => { + let nla = BitsetBitsNla(bits, !self.nomask); + nla.emit(&mut buffer[offset..]); + } + EthtoolBitsetValue::Compact { value, mask } => { + let nla = BitsetWords(ETHTOOL_A_BITSET_VALUE, value); + nla.emit(&mut buffer[offset..]); + offset += nla.buffer_len(); + if !self.nomask { + let nla = BitsetWords(ETHTOOL_A_BITSET_MASK, mask); + nla.emit(&mut buffer[offset..]); + } + } + } + } +} diff --git a/src/bitset_util.rs b/src/bitset_util.rs deleted file mode 100644 index a9644f7..0000000 --- a/src/bitset_util.rs +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: MIT - -use log::warn; -use netlink_packet_core::{ - parse_string, parse_u32, DecodeError, ErrorContext, NlasIterator, -}; - -const ETHTOOL_A_BITSET_BITS: u16 = 3; - -const ETHTOOL_A_BITSET_BITS_BIT: u16 = 1; - -const ETHTOOL_A_BITSET_BIT_INDEX: u16 = 1; -const ETHTOOL_A_BITSET_BIT_NAME: u16 = 2; -const ETHTOOL_A_BITSET_BIT_VALUE: u16 = 3; - -#[derive(Debug, PartialEq, Eq, Clone, Default)] -pub(crate) struct EthtoolBitSet { - pub(crate) index: u32, - pub(crate) name: String, - pub(crate) value: bool, -} - -pub(crate) fn parse_bitset_bits_nlas( - raw: &[u8], -) -> Result, DecodeError> { - let error_msg = "failed to parse mode bit sets"; - for nla in NlasIterator::new(raw) { - let nla = &nla.context(error_msg)?; - if nla.kind() == ETHTOOL_A_BITSET_BITS { - return parse_bitset_bits_nla(nla.value()); - } - } - Err("No ETHTOOL_A_BITSET_BITS NLA found".into()) -} - -pub(crate) fn parse_bitset_bits_string_nlas( - raw: &[u8], -) -> Result, DecodeError> { - let error_msg = "failed to parse mode bit sets"; - for nla in NlasIterator::new(raw) { - let nla = &nla.context(error_msg)?; - if nla.kind() == ETHTOOL_A_BITSET_BITS { - let bits = parse_bitset_bits_nla(nla.value())?; - - return Ok(bits - .into_iter() - .filter_map(|b| if b.value { Some(b.name) } else { None }) - .collect::>()); - } - } - Err("No ETHTOOL_A_BITSET_BITS NLA found".into()) -} - -fn parse_bitset_bits_nla( - raw: &[u8], -) -> Result, DecodeError> { - let mut bit_sets = Vec::new(); - let error_msg = "Failed to parse ETHTOOL_A_BITSET_BITS attributes"; - for bit_nla in NlasIterator::new(raw) { - let bit_nla = &bit_nla.context(error_msg)?; - match bit_nla.kind() { - ETHTOOL_A_BITSET_BITS_BIT => { - let error_msg = - "Failed to parse ETHTOOL_A_BITSET_BITS_BIT attributes"; - let mut bit_set = EthtoolBitSet::default(); - let nlas = NlasIterator::new(bit_nla.value()); - for nla in nlas { - let nla = &nla.context(error_msg)?; - let payload = nla.value(); - match nla.kind() { - ETHTOOL_A_BITSET_BIT_INDEX => { - bit_set.index = - parse_u32(payload).context(format!( - "Invalid ETHTOOL_A_BITSET_BIT_INDEX \ - value {payload:?}" - ))?; - } - ETHTOOL_A_BITSET_BIT_VALUE => { - bit_set.value = true; - } - ETHTOOL_A_BITSET_BIT_NAME => { - bit_set.name = parse_string(payload).context( - "Invald ETHTOOL_A_BITSET_BIT_NAME value", - )?; - } - _ => { - warn!( - "Unknown ETHTOOL_A_BITSET_BITS_BIT {} {:?}", - nla.kind(), - nla.value(), - ); - } - } - } - bit_sets.push(bit_set); - } - _ => { - warn!( - "Unknown ETHTOOL_A_BITSET_BITS kind {}, {:?}", - bit_nla.kind(), - bit_nla.value() - ); - } - }; - } - Ok(bit_sets) -} diff --git a/src/feature/attr.rs b/src/feature/attr.rs index 97e94fc..9ca7235 100644 --- a/src/feature/attr.rs +++ b/src/feature/attr.rs @@ -1,12 +1,11 @@ // SPDX-License-Identifier: MIT -use log::warn; use netlink_packet_core::{ - parse_string, parse_u32, DecodeError, DefaultNla, Emitable, ErrorContext, - Nla, NlaBuffer, NlasIterator, Parseable, NLA_F_NESTED, + DecodeError, DefaultNla, Emitable, ErrorContext, Nla, NlaBuffer, + NlasIterator, Parseable, NLA_F_NESTED, }; -use crate::{EthtoolAttr, EthtoolHeader}; +use crate::{bitset::EthtoolBitset, EthtoolAttr, EthtoolHeader}; const ETHTOOL_A_FEATURES_HEADER: u16 = 1; const ETHTOOL_A_FEATURES_HW: u16 = 2; // User changable features @@ -14,46 +13,13 @@ const ETHTOOL_A_FEATURES_WANTED: u16 = 3; // User requested fatures const ETHTOOL_A_FEATURES_ACTIVE: u16 = 4; // Active features const ETHTOOL_A_FEATURES_NOCHANGE: u16 = 5; -const ETHTOOL_A_BITSET_BITS: u16 = 3; - -const ETHTOOL_A_BITSET_BITS_BIT: u16 = 1; - -const ETHTOOL_A_BITSET_BIT_INDEX: u16 = 1; -const ETHTOOL_A_BITSET_BIT_NAME: u16 = 2; -const ETHTOOL_A_BITSET_BIT_VALUE: u16 = 3; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct EthtoolFeatureBit { - pub index: u32, - pub name: String, - pub value: bool, -} - -impl EthtoolFeatureBit { - fn new(has_mask: bool) -> Self { - Self { - index: 0, - name: "".into(), - value: !has_mask, - } - } -} - -fn feature_bits_len(_feature_bits: &[EthtoolFeatureBit]) -> usize { - todo!("Does not support changing ethtool feature yet") -} - -fn feature_bits_emit(_feature_bits: &[EthtoolFeatureBit], _buffer: &mut [u8]) { - todo!("Does not support changing ethtool feature yet") -} - #[derive(Debug, PartialEq, Eq, Clone)] pub enum EthtoolFeatureAttr { Header(Vec), - Hw(Vec), - Wanted(Vec), - Active(Vec), - NoChange(Vec), + Hw(EthtoolBitset), + Wanted(EthtoolBitset), + Active(EthtoolBitset), + NoChange(EthtoolBitset), Other(DefaultNla), } @@ -61,12 +27,10 @@ impl Nla for EthtoolFeatureAttr { fn value_len(&self) -> usize { match self { Self::Header(hdrs) => hdrs.as_slice().buffer_len(), - Self::Hw(feature_bits) - | Self::Wanted(feature_bits) - | Self::Active(feature_bits) - | Self::NoChange(feature_bits) => { - feature_bits_len(feature_bits.as_slice()) - } + Self::Hw(bitset) + | Self::Wanted(bitset) + | Self::Active(bitset) + | Self::NoChange(bitset) => bitset.buffer_len(), Self::Other(attr) => attr.value_len(), } } @@ -85,12 +49,10 @@ impl Nla for EthtoolFeatureAttr { fn emit_value(&self, buffer: &mut [u8]) { match self { Self::Header(ref nlas) => nlas.as_slice().emit(buffer), - Self::Hw(feature_bits) - | Self::Wanted(feature_bits) - | Self::Active(feature_bits) - | Self::NoChange(feature_bits) => { - feature_bits_emit(feature_bits.as_slice(), buffer) - } + Self::Hw(bitset) + | Self::Wanted(bitset) + | Self::Active(bitset) + | Self::NoChange(bitset) => bitset.emit(buffer), Self::Other(ref attr) => attr.emit_value(buffer), } } @@ -113,28 +75,23 @@ impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> } Self::Header(nlas) } - ETHTOOL_A_FEATURES_HW => { - Self::Hw(parse_bitset_bits_string_nlas( - payload, - true, /* ETHTOOL_A_FEATURES_HW is using mask */ - )?) - } + ETHTOOL_A_FEATURES_HW => Self::Hw( + EthtoolBitset::parse(payload) + .context("failed to parse ETHTOOL_A_FEATURES_HW bitset")?, + ), ETHTOOL_A_FEATURES_WANTED => { - Self::Wanted(parse_bitset_bits_string_nlas( - payload, - false, /* ETHTOOL_A_FEATURES_WANTED does not use mask */ + Self::Wanted(EthtoolBitset::parse(payload).context( + "failed to parse ETHTOOL_A_FEATURES_WANTED bitset", )?) } ETHTOOL_A_FEATURES_ACTIVE => { - Self::Active(parse_bitset_bits_string_nlas( - payload, - false, /* ETHTOOL_A_FEATURES_ACTIVE does not use mask */ + Self::Active(EthtoolBitset::parse(payload).context( + "failed to parse ETHTOOL_A_FEATURES_ACTIVE bitset", )?) } ETHTOOL_A_FEATURES_NOCHANGE => { - Self::NoChange(parse_bitset_bits_string_nlas( - payload, - false, /* ETHTOOL_A_FEATURES_NOCHANGE does not use mask */ + Self::NoChange(EthtoolBitset::parse(payload).context( + "failed to parse ETHTOOL_A_FEATURES_NOCHANGE bitset", )?) } _ => Self::Other( @@ -144,82 +101,6 @@ impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> } } -fn parse_bitset_bits_string_nlas( - raw: &[u8], - has_mask: bool, -) -> Result, DecodeError> { - let error_msg = "failed to parse feature bit sets"; - for nla in NlasIterator::new(raw) { - let nla = &nla.context(error_msg)?; - if nla.kind() == ETHTOOL_A_BITSET_BITS { - return parse_bitset_bits_nla(nla.value(), has_mask); - } - } - Err("No ETHTOOL_A_BITSET_BITS NLA found".into()) -} - -fn parse_bitset_bits_nla( - raw: &[u8], - has_mask: bool, -) -> Result, DecodeError> { - let mut feature_bits = Vec::new(); - let error_msg = "Failed to parse ETHTOOL_A_BITSET_BITS attributes"; - for bit_nla in NlasIterator::new(raw) { - let bit_nla = &bit_nla.context(error_msg)?; - match bit_nla.kind() { - ETHTOOL_A_BITSET_BITS_BIT => { - let error_msg = - "Failed to parse ETHTOOL_A_BITSET_BITS_BIT attributes"; - let nlas = NlasIterator::new(bit_nla.value()); - let mut cur_bit_info = EthtoolFeatureBit::new(has_mask); - for nla in nlas { - let nla = &nla.context(error_msg)?; - let payload = nla.value(); - match nla.kind() { - ETHTOOL_A_BITSET_BIT_INDEX => { - if cur_bit_info.index != 0 - && !&cur_bit_info.name.is_empty() - { - feature_bits.push(cur_bit_info); - cur_bit_info = EthtoolFeatureBit::new(has_mask); - } - cur_bit_info.index = parse_u32(payload).context( - "Invald ETHTOOL_A_BITSET_BIT_INDEX value", - )?; - } - ETHTOOL_A_BITSET_BIT_NAME => { - cur_bit_info.name = parse_string(payload).context( - "Invald ETHTOOL_A_BITSET_BIT_NAME value", - )?; - } - ETHTOOL_A_BITSET_BIT_VALUE => { - cur_bit_info.value = true; - } - _ => { - warn!( - "Unknown ETHTOOL_A_BITSET_BITS_BIT {} {:?}", - nla.kind(), - nla.value(), - ); - } - } - } - if cur_bit_info.index != 0 && !&cur_bit_info.name.is_empty() { - feature_bits.push(cur_bit_info); - } - } - _ => { - warn!( - "Unknown ETHTOOL_A_BITSET_BITS kind {}, {:?}", - bit_nla.kind(), - bit_nla.value() - ); - } - }; - } - Ok(feature_bits) -} - pub(crate) fn parse_feature_nlas( buffer: &[u8], ) -> Result, DecodeError> { diff --git a/src/feature/mod.rs b/src/feature/mod.rs index 018a709..0295635 100644 --- a/src/feature/mod.rs +++ b/src/feature/mod.rs @@ -5,6 +5,6 @@ mod get; mod handle; pub(crate) use attr::parse_feature_nlas; -pub use attr::{EthtoolFeatureAttr, EthtoolFeatureBit}; +pub use attr::EthtoolFeatureAttr; pub use get::EthtoolFeatureGetRequest; pub use handle::EthtoolFeatureHandle; diff --git a/src/fec/attr.rs b/src/fec/attr.rs index c97d99c..d502105 100644 --- a/src/fec/attr.rs +++ b/src/fec/attr.rs @@ -1,15 +1,11 @@ // SPDX-License-Identifier: MIT use netlink_packet_core::{ - emit_u64, parse_u32, parse_u64, parse_u8, DecodeError, DefaultNla, - Emitable, ErrorContext, Nla, NlaBuffer, NlasIterator, Parseable, - NLA_F_NESTED, + emit_u32, emit_u64, parse_u32, parse_u8, DecodeError, DefaultNla, Emitable, + ErrorContext, Nla, NlaBuffer, NlasIterator, Parseable, NLA_F_NESTED, }; -use crate::{ - bitset_util::{parse_bitset_bits_nlas, EthtoolBitSet}, - EthtoolAttr, EthtoolHeader, -}; +use crate::{bitset::EthtoolBitset, EthtoolAttr, EthtoolHeader}; const ETHTOOL_A_FEC_HEADER: u16 = 1; const ETHTOOL_A_FEC_MODES: u16 = 2; @@ -27,7 +23,7 @@ const ETHTOOL_LINK_MODE_FEC_LLRS_BIT: u32 = 74; pub enum EthtoolFecAttr { Header(Vec), /// Configured modes - Modes(Vec), + Modes(EthtoolBitset), /// FEC mode auto selection /// Request the driver to choose FEC mode based on SFP module parameters. /// This does not mean autonegotiation. @@ -42,7 +38,7 @@ impl Nla for EthtoolFecAttr { fn value_len(&self) -> usize { match self { Self::Header(hdrs) => hdrs.as_slice().buffer_len(), - Self::Modes(_) => todo!("Does not support emitting EthtoolFecAttr"), + Self::Modes(bitset) => bitset.buffer_len(), Self::Auto(_) => 1, Self::Active(_) => 4, Self::Stats(s) => s.as_slice().buffer_len(), @@ -53,10 +49,10 @@ impl Nla for EthtoolFecAttr { fn kind(&self) -> u16 { match self { Self::Header(_) => ETHTOOL_A_FEC_HEADER | NLA_F_NESTED, - Self::Modes(_) => ETHTOOL_A_FEC_MODES, + Self::Modes(_) => ETHTOOL_A_FEC_MODES | NLA_F_NESTED, Self::Auto(_) => ETHTOOL_A_FEC_AUTO, Self::Active(_) => ETHTOOL_A_FEC_ACTIVE, - Self::Stats(_) => ETHTOOL_A_FEC_STATS, + Self::Stats(_) => ETHTOOL_A_FEC_STATS | NLA_F_NESTED, Self::Other(attr) => attr.kind(), } } @@ -65,7 +61,12 @@ impl Nla for EthtoolFecAttr { match self { Self::Header(ref nlas) => nlas.as_slice().emit(buffer), Self::Other(ref attr) => attr.emit_value(buffer), - _ => todo!("Does not support changing ethtool fec yet"), + Self::Modes(bitset) => bitset.emit(buffer), + Self::Auto(value) => buffer[0] = *value as u8, + Self::Active(value) => { + emit_u32(buffer, u32::from(value.clone())).unwrap() + } + Self::Stats(ref nlas) => nlas.as_slice().emit(buffer), } } } @@ -88,12 +89,9 @@ impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> Self::Header(nlas) } ETHTOOL_A_FEC_MODES => { - let bits = parse_bitset_bits_nlas(payload).context(format!( - "Invalid ETHTOOL_A_FEC_TIMESTAMPING {payload:?}" - ))?; - Self::Modes( - bits.into_iter().map(EthtoolFecMode::from).collect(), - ) + Self::Modes(EthtoolBitset::parse(payload).context(format!( + "Invalid ETHTOOL_A_FEC_MODES {payload:?}" + ))?) } ETHTOOL_A_FEC_AUTO => Self::Auto( parse_u8(payload).context(format!( @@ -166,18 +164,6 @@ impl std::fmt::Display for EthtoolFecMode { } } -impl From for EthtoolFecMode { - fn from(b: EthtoolBitSet) -> Self { - match b.index { - ETHTOOL_LINK_MODE_FEC_NONE_BIT => Self::None, - ETHTOOL_LINK_MODE_FEC_RS_BIT => Self::Rs, - ETHTOOL_LINK_MODE_FEC_BASER_BIT => Self::Baser, - ETHTOOL_LINK_MODE_FEC_LLRS_BIT => Self::Llrs, - _ => Self::Other(b.index, b.name), - } - } -} - impl From for EthtoolFecMode { fn from(d: u32) -> Self { match d { @@ -208,17 +194,22 @@ const ETHTOOL_A_FEC_STAT_CORR_BITS: u16 = 4; #[derive(Debug, PartialEq, Eq, Clone)] pub enum EthtoolFecStat { - Corrected(u64), - Uncorrected(u64), - CorrectBits(u64), + /// Corrected blocks, total and per lane. + Corrected(Vec), + /// Uncorrectable blocks, total and per lane. + Uncorrected(Vec), + /// Corrected bits, total and per lane. + CorrectBits(Vec), Other(DefaultNla), } impl Nla for EthtoolFecStat { fn value_len(&self) -> usize { match self { + Self::Corrected(values) + | Self::Uncorrected(values) + | Self::CorrectBits(values) => values.len() * 8, Self::Other(attr) => attr.value_len(), - _ => 8, } } @@ -233,14 +224,34 @@ impl Nla for EthtoolFecStat { fn emit_value(&self, buffer: &mut [u8]) { match self { - Self::Corrected(v) - | Self::Uncorrected(v) - | Self::CorrectBits(v) => emit_u64(buffer, *v).unwrap(), + Self::Corrected(values) + | Self::Uncorrected(values) + | Self::CorrectBits(values) => { + for (i, value) in values.iter().enumerate() { + emit_u64(&mut buffer[i * 8..], *value).unwrap(); + } + } Self::Other(attr) => attr.emit_value(buffer), } } } +fn parse_u64_values(payload: &[u8]) -> Result, DecodeError> { + if !payload.len().is_multiple_of(8) { + return Err(format!( + "Invalid u64 array length {}, not multiple of 8", + payload.len() + ) + .into()); + } + Ok(payload + .as_chunks::<8>() + .0 + .iter() + .map(|c| u64::from_ne_bytes(*c)) + .collect()) +} + impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> for EthtoolFecStat { @@ -248,19 +259,19 @@ impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> let payload = buf.value(); Ok(match buf.kind() { ETHTOOL_A_FEC_STAT_CORRECTED => { - Self::Corrected(parse_u64(payload).context(format!( + Self::Corrected(parse_u64_values(payload).context(format!( "Invalid ETHTOOL_A_FEC_STAT_CORRECTED {payload:?}" ))?) } ETHTOOL_A_FEC_STAT_UNCORR => { - Self::Uncorrected(parse_u64(payload).context(format!( - "Invalid ETHTOOL_A_FEC_STAT_UNCORR {payload:?}" - ))?) + Self::Uncorrected(parse_u64_values(payload).context( + format!("Invalid ETHTOOL_A_FEC_STAT_UNCORR {payload:?}"), + )?) } ETHTOOL_A_FEC_STAT_CORR_BITS => { - Self::CorrectBits(parse_u64(payload).context(format!( - "Invalid ETHTOOL_A_FEC_STAT_CORR_BITS {payload:?}" - ))?) + Self::CorrectBits(parse_u64_values(payload).context( + format!("Invalid ETHTOOL_A_FEC_STAT_CORR_BITS {payload:?}"), + )?) } _ => Self::Other( DefaultNla::parse(buf).context("invalid NLA (unknown kind)")?, diff --git a/src/lib.rs b/src/lib.rs index f50e18f..8cf2fd2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -mod bitset_util; +mod bitset; mod channel; mod coalesce; mod connection; @@ -15,12 +15,17 @@ mod macros; mod message; mod pause; mod ring; +// test data are using hard coded little endian byte order, not for big-endian +#[cfg(not(target_endian = "big"))] +#[cfg(test)] +mod tests; mod tsinfo; pub use self::fec::{ EthtoolFecAttr, EthtoolFecGetRequest, EthtoolFecHandle, EthtoolFecMode, EthtoolFecStat, }; +pub use bitset::{EthtoolBit, EthtoolBitset, EthtoolBitsetValue}; pub use channel::{ EthtoolChannelAttr, EthtoolChannelGetRequest, EthtoolChannelHandle, EthtoolChannelSetRequest, @@ -38,8 +43,7 @@ pub use eeprom::{ }; pub use error::EthtoolError; pub use feature::{ - EthtoolFeatureAttr, EthtoolFeatureBit, EthtoolFeatureGetRequest, - EthtoolFeatureHandle, + EthtoolFeatureAttr, EthtoolFeatureGetRequest, EthtoolFeatureHandle, }; pub use handle::EthtoolHandle; pub use header::EthtoolHeader; diff --git a/src/link_mode/attr.rs b/src/link_mode/attr.rs index 03dd955..af22832 100644 --- a/src/link_mode/attr.rs +++ b/src/link_mode/attr.rs @@ -1,13 +1,11 @@ // SPDX-License-Identifier: MIT use netlink_packet_core::{ - parse_u32, parse_u8, DecodeError, DefaultNla, Emitable, ErrorContext, Nla, - NlaBuffer, NlasIterator, Parseable, NLA_F_NESTED, + emit_u32, parse_u32, parse_u8, DecodeError, DefaultNla, Emitable, + ErrorContext, Nla, NlaBuffer, NlasIterator, Parseable, NLA_F_NESTED, }; -use crate::{ - bitset_util::parse_bitset_bits_string_nlas, EthtoolAttr, EthtoolHeader, -}; +use crate::{bitset::EthtoolBitset, EthtoolAttr, EthtoolHeader}; const ETHTOOL_A_LINKMODES_HEADER: u16 = 1; const ETHTOOL_A_LINKMODES_AUTONEG: u16 = 2; @@ -57,8 +55,8 @@ impl From for u8 { pub enum EthtoolLinkModeAttr { Header(Vec), Autoneg(bool), - Ours(Vec), - Peer(Vec), + Ours(EthtoolBitset), + Peer(EthtoolBitset), Speed(u32), Duplex(EthtoolLinkModeDuplex), ControllerSubordinateCfg(u8), @@ -75,12 +73,7 @@ impl Nla for EthtoolLinkModeAttr { | Self::Duplex(_) | Self::ControllerSubordinateCfg(_) | Self::ControllerSubordinateState(_) => 1, - Self::Ours(_) => { - todo!("Does not support changing ethtool link mode yet") - } - Self::Peer(_) => { - todo!("Does not support changing ethtool link mode yet") - } + Self::Ours(bitset) | Self::Peer(bitset) => bitset.buffer_len(), Self::Speed(_) | Self::Lanes(_) => 4, Self::Other(attr) => attr.value_len(), } @@ -90,8 +83,8 @@ impl Nla for EthtoolLinkModeAttr { match self { Self::Header(_) => ETHTOOL_A_LINKMODES_HEADER | NLA_F_NESTED, Self::Autoneg(_) => ETHTOOL_A_LINKMODES_AUTONEG, - Self::Ours(_) => ETHTOOL_A_LINKMODES_OURS, - Self::Peer(_) => ETHTOOL_A_LINKMODES_PEER, + Self::Ours(_) => ETHTOOL_A_LINKMODES_OURS | NLA_F_NESTED, + Self::Peer(_) => ETHTOOL_A_LINKMODES_PEER | NLA_F_NESTED, Self::Speed(_) => ETHTOOL_A_LINKMODES_SPEED, Self::Duplex(_) => ETHTOOL_A_LINKMODES_DUPLEX, Self::ControllerSubordinateCfg(_) => { @@ -109,7 +102,14 @@ impl Nla for EthtoolLinkModeAttr { match self { Self::Header(ref nlas) => nlas.as_slice().emit(buffer), Self::Other(ref attr) => attr.emit_value(buffer), - _ => todo!("Does not support changing ethtool link mode yet"), + Self::Autoneg(value) => buffer[0] = *value as u8, + Self::Duplex(value) => buffer[0] = u8::from(*value), + Self::ControllerSubordinateCfg(value) + | Self::ControllerSubordinateState(value) => buffer[0] = *value, + Self::Speed(value) | Self::Lanes(value) => { + emit_u32(buffer, *value).unwrap() + } + Self::Ours(bitset) | Self::Peer(bitset) => bitset.emit(buffer), } } } @@ -137,12 +137,14 @@ impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> == 1, ), - ETHTOOL_A_LINKMODES_OURS => { - Self::Ours(parse_bitset_bits_string_nlas(payload)?) - } - ETHTOOL_A_LINKMODES_PEER => { - Self::Peer(parse_bitset_bits_string_nlas(payload)?) - } + ETHTOOL_A_LINKMODES_OURS => Self::Ours( + EthtoolBitset::parse(payload) + .context("Invalid ETHTOOL_A_LINKMODES_OURS value")?, + ), + ETHTOOL_A_LINKMODES_PEER => Self::Peer( + EthtoolBitset::parse(payload) + .context("Invalid ETHTOOL_A_LINKMODES_PEER value")?, + ), ETHTOOL_A_LINKMODES_SPEED => Self::Speed( parse_u32(payload) .context("Invalid ETHTOOL_A_LINKMODES_SPEED value")?, diff --git a/src/pause/attr.rs b/src/pause/attr.rs index b1e6b3f..516505c 100644 --- a/src/pause/attr.rs +++ b/src/pause/attr.rs @@ -34,7 +34,7 @@ impl Nla for EthtoolPauseStatAttr { fn kind(&self) -> u16 { match self { Self::Rx(_) => ETHTOOL_A_PAUSE_STAT_RX_FRAMES, - Self::Tx(_) => ETHTOOL_A_PAUSE_STAT_RX_FRAMES, + Self::Tx(_) => ETHTOOL_A_PAUSE_STAT_TX_FRAMES, Self::Other(attr) => attr.kind(), } } diff --git a/tests/channels.rs b/src/tests/channels.rs similarity index 64% rename from tests/channels.rs rename to src/tests/channels.rs index 023df5d..065fa21 100644 --- a/tests/channels.rs +++ b/src/tests/channels.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT -use ethtool::{ +use super::parse_and_round_trip; +use crate::{ EthtoolAttr, EthtoolChannelAttr, EthtoolCmd, EthtoolHeader, EthtoolMessage, }; use netlink_packet_core::{Emitable, ParseableParametrized}; @@ -131,3 +132,61 @@ fn test_channels_set_combined() { assert_eq!(expected, raw,); } +#[test] +fn test_parsing_channels_netdevsim() { + let raw: Vec = vec![ + 0x12, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0xa3, 0x00, 0x00, 0x00, 0x09, 0x00, 0x02, 0x00, 0x73, 0x69, 0x6d, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, + ]; + + // ethtool -l sim0 after ethtool -L sim0 combined 1 + let msg = parse_and_round_trip(&raw); + + assert_eq!( + msg, + EthtoolMessage { + cmd: EthtoolCmd::ChannelGetReply, + nlas: vec![ + EthtoolAttr::Channel(EthtoolChannelAttr::Header(vec![ + EthtoolHeader::DevIndex(163), + EthtoolHeader::DevName("sim0".to_string()), + ])), + EthtoolAttr::Channel(EthtoolChannelAttr::CombinedMax(1)), + EthtoolAttr::Channel(EthtoolChannelAttr::CombinedCount(1)), + ], + } + ); +} + +#[test] +fn test_parsing_channels_veth() { + let raw: Vec = vec![ + 0x12, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x02, 0x00, 0x76, 0x65, 0x74, 0x68, + 0x30, 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x03, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x08, 0x00, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, + ]; + + // ethtool -l veth0 + let msg = parse_and_round_trip(&raw); + + assert_eq!( + msg, + EthtoolMessage { + cmd: EthtoolCmd::ChannelGetReply, + nlas: vec![ + EthtoolAttr::Channel(EthtoolChannelAttr::Header(vec![ + EthtoolHeader::DevIndex(256), + EthtoolHeader::DevName("veth0".to_string()), + ])), + EthtoolAttr::Channel(EthtoolChannelAttr::RxMax(16)), + EthtoolAttr::Channel(EthtoolChannelAttr::RxCount(1)), + EthtoolAttr::Channel(EthtoolChannelAttr::TxMax(16)), + EthtoolAttr::Channel(EthtoolChannelAttr::TxCount(1)), + ], + } + ); +} diff --git a/src/tests/coalesce.rs b/src/tests/coalesce.rs new file mode 100644 index 0000000..453930d --- /dev/null +++ b/src/tests/coalesce.rs @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT + +// Generated from kernel emitted ethtool netlink packets captured with an +// nlmon device (`modprobe nlmon`, `ip link add nl0 type nlmon`, +// `ip link set nl0 up`, `tcpdump -i nl0 -w capture.pcap`) on Linux 7.2 and +// inlined here. The 16 bytes netlink header is stripped, so the data starts +// at the generic netlink header exactly like the buffers parsed by +// `EthtoolMessage::parse_with_param()`. Each test verifies the captured +// packet is parsed into the expected attributes and emitted back byte for +// byte. + +use super::parse_and_round_trip; +use crate::{ + EthtoolAttr, EthtoolCmd, EthtoolCoalesceAttr, EthtoolHeader, EthtoolMessage, +}; + +#[test] +fn test_parsing_coalesce_netdevsim() { + let raw: Vec = vec![ + 0x14, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0xa3, 0x00, 0x00, 0x00, 0x09, 0x00, 0x02, 0x00, 0x73, 0x69, 0x6d, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x03, 0x00, 0x05, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x06, 0x00, 0x14, 0x00, 0x00, 0x00, 0x08, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0a, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0b, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x0c, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0d, 0x00, + 0xe8, 0x03, 0x00, 0x00, 0x08, 0x00, 0x0e, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x0f, 0x00, 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x10, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x08, 0x00, 0x11, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x12, 0x00, 0xd0, 0x07, 0x00, 0x00, 0x08, 0x00, 0x13, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x08, 0x00, 0x14, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x15, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x16, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x17, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x19, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1c, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + // ethtool -c sim0 after configuring all coalesce parameters + let msg = parse_and_round_trip(&raw); + + assert_eq!( + msg, + EthtoolMessage { + cmd: EthtoolCmd::CoalesceGetReply, + nlas: vec![ + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::Header(vec![ + EthtoolHeader::DevIndex(163), + EthtoolHeader::DevName("sim0".to_string()), + ])), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RxUsecs(10)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RxMaxFrames(5)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RxUsecsIrq(0)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RxMaxFramesIrq(0)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxUsecs(20)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxMaxFrames(6)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxUsecsIrq(0)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxMaxFramesIrq(0)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::StatsBlockUsecs( + 100 + )), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::UseAdaptiveRx(true)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::UseAdaptiveTx(true)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::PktRateLow(1000)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RxUsecsLow(1)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RxMaxFramesLow(2)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxUsecsLow(3)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxMaxFramesLow(4)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::PktRateHigh(2000)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RxUsecsHigh(5)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RxMaxFramesHigh(6)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxUsecsHigh(7)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxMaxFramesHigh(8)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RateSampleInterval( + 9 + )), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::UseCqeModeTx(false)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::UseCqeModeRx(false)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxAggrMaxBytes(0)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxAggrMaxFrames(0)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxAggrTimeUsecs(0)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RxCqeFrames(0)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RxCqeNsecs(0)), + ], + } + ); +} + +#[test] +fn test_parsing_coalesce_virtio() { + let raw: Vec = vec![ + 0x14, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x00, 0x65, 0x6e, 0x70, 0x32, + 0x73, 0x30, 0x00, 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + // ethtool -c enp2s0 + let msg = parse_and_round_trip(&raw); + + assert_eq!( + msg, + EthtoolMessage { + cmd: EthtoolCmd::CoalesceGetReply, + nlas: vec![ + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::Header(vec![ + EthtoolHeader::DevIndex(2), + EthtoolHeader::DevName("enp2s0".to_string()), + ])), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RxUsecs(0)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::RxMaxFrames(1)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxUsecs(0)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::TxMaxFrames(1)), + EthtoolAttr::Coalesce(EthtoolCoalesceAttr::UseAdaptiveRx( + false + )), + ], + } + ); +} diff --git a/src/tests/features.rs b/src/tests/features.rs new file mode 100644 index 0000000..0f23466 --- /dev/null +++ b/src/tests/features.rs @@ -0,0 +1,465 @@ +// SPDX-License-Identifier: MIT + +// Generated from kernel emitted ethtool netlink packets captured with an +// nlmon device (`modprobe nlmon`, `ip link add nl0 type nlmon`, +// `ip link set nl0 up`, `tcpdump -i nl0 -w capture.pcap`) on Linux 7.2 and +// inlined here. The 16 bytes netlink header is stripped, so the data starts +// at the generic netlink header exactly like the buffers parsed by +// `EthtoolMessage::parse_with_param()`. Each test verifies the captured +// packet is parsed into the expected attributes and emitted back byte for +// byte. + +use super::parse_and_round_trip; +use crate::{ + EthtoolAttr, EthtoolBit, EthtoolBitset, EthtoolBitsetValue, EthtoolCmd, + EthtoolFeatureAttr, EthtoolHeader, +}; + +#[test] +fn test_parsing_features_compact_bitsets() { + let raw: Vec = vec![ + 0x0b, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0xa3, 0x00, 0x00, 0x00, 0x09, 0x00, 0x02, 0x00, 0x73, 0x69, 0x6d, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x02, 0x80, 0x08, 0x00, 0x02, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x04, 0x00, 0x49, 0xca, 0x09, 0x80, + 0x00, 0x86, 0x1e, 0x0a, 0x0c, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x1c, 0x00, 0x03, 0x80, 0x04, 0x00, 0x01, 0x00, + 0x08, 0x00, 0x02, 0x00, 0x40, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x04, 0x00, + 0x49, 0xca, 0x01, 0x80, 0x00, 0x80, 0x1c, 0x00, 0x1c, 0x00, 0x04, 0x80, + 0x04, 0x00, 0x01, 0x00, 0x08, 0x00, 0x02, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x04, 0x00, 0x69, 0xca, 0x01, 0x80, 0x00, 0x80, 0x1c, 0x04, + 0x1c, 0x00, 0x05, 0x80, 0x04, 0x00, 0x01, 0x00, 0x08, 0x00, 0x02, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + // ethtool -k sim0 (CLI sets ETHTOOL_FLAG_COMPACT_BITSETS) + let msg = parse_and_round_trip(&raw); + + assert_eq!(msg.cmd, EthtoolCmd::FeatureGetReply); + assert_eq!(msg.nlas.len(), 5); + assert_eq!( + msg.nlas[0], + EthtoolAttr::Feature(EthtoolFeatureAttr::Header(vec![ + EthtoolHeader::DevIndex(163), + EthtoolHeader::DevName("sim0".to_string()), + ])) + ); + assert_eq!( + msg.nlas[1], + EthtoolAttr::Feature(EthtoolFeatureAttr::Hw(EthtoolBitset { + nomask: false, + size: 64, + value: EthtoolBitsetValue::Compact { + value: vec![2148125257, 169772544], + mask: vec![4294967295, 4294967295], + }, + })) + ); + assert_eq!( + msg.nlas[2], + EthtoolAttr::Feature(EthtoolFeatureAttr::Wanted(EthtoolBitset { + nomask: true, + size: 64, + value: EthtoolBitsetValue::Compact { + value: vec![2147600969, 1867776], + mask: vec![], + }, + })) + ); + assert_eq!( + msg.nlas[3], + EthtoolAttr::Feature(EthtoolFeatureAttr::Active(EthtoolBitset { + nomask: true, + size: 64, + value: EthtoolBitsetValue::Compact { + value: vec![2147601001, 68976640], + mask: vec![], + }, + })) + ); + assert_eq!( + msg.nlas[4], + EthtoolAttr::Feature(EthtoolFeatureAttr::NoChange(EthtoolBitset { + nomask: true, + size: 64, + value: EthtoolBitsetValue::Compact { + value: vec![1024, 0], + mask: vec![], + }, + })) + ); +} + +#[test] +fn test_parsing_features_string_bitsets() { + let raw: Vec = vec![ + 0x0b, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x00, 0x65, 0x6e, 0x70, 0x32, + 0x73, 0x30, 0x00, 0x00, 0xe4, 0x08, 0x02, 0x80, 0x08, 0x00, 0x02, 0x00, + 0x40, 0x00, 0x00, 0x00, 0xd8, 0x08, 0x03, 0x80, 0x28, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x73, 0x63, 0x61, 0x74, 0x74, 0x65, 0x72, 0x2d, 0x67, + 0x61, 0x74, 0x68, 0x65, 0x72, 0x00, 0x00, 0x00, 0x04, 0x00, 0x03, 0x00, + 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x15, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x63, 0x68, 0x65, 0x63, 0x6b, + 0x73, 0x75, 0x6d, 0x2d, 0x69, 0x70, 0x76, 0x34, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x2d, + 0x69, 0x70, 0x2d, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x00, 0x00, + 0x04, 0x00, 0x03, 0x00, 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x15, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x2d, 0x69, 0x70, 0x76, 0x36, + 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00, 0x68, 0x69, 0x67, 0x68, + 0x64, 0x6d, 0x61, 0x00, 0x2c, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x73, + 0x63, 0x61, 0x74, 0x74, 0x65, 0x72, 0x2d, 0x67, 0x61, 0x74, 0x68, 0x65, + 0x72, 0x2d, 0x66, 0x72, 0x61, 0x67, 0x6c, 0x69, 0x73, 0x74, 0x00, 0x00, + 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x76, 0x6c, 0x61, 0x6e, 0x2d, + 0x68, 0x77, 0x2d, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x00, 0x00, 0x00, + 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x15, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x76, 0x6c, 0x61, 0x6e, 0x2d, + 0x68, 0x77, 0x2d, 0x70, 0x61, 0x72, 0x73, 0x65, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x13, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x76, 0x6c, 0x61, 0x6e, 0x2d, + 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, + 0x76, 0x6c, 0x61, 0x6e, 0x2d, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, + 0x67, 0x65, 0x64, 0x00, 0x2c, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x67, + 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, + 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x03, 0x00, + 0x14, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x67, + 0x72, 0x6f, 0x00, 0x00, 0x04, 0x00, 0x03, 0x00, 0x18, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x00, + 0x72, 0x78, 0x2d, 0x6c, 0x72, 0x6f, 0x00, 0x00, 0x28, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x74, 0x63, 0x70, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, + 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x03, 0x00, + 0x20, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x11, 0x00, 0x00, 0x00, + 0x12, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x67, 0x73, 0x6f, 0x2d, 0x72, + 0x6f, 0x62, 0x75, 0x73, 0x74, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x74, 0x63, 0x70, 0x2d, 0x65, 0x63, 0x6e, 0x2d, 0x73, + 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, + 0x04, 0x00, 0x03, 0x00, 0x34, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x21, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x74, + 0x63, 0x70, 0x2d, 0x6d, 0x61, 0x6e, 0x67, 0x6c, 0x65, 0x69, 0x64, 0x2d, + 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x03, 0x00, 0x2c, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x19, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x74, 0x63, 0x70, 0x36, 0x2d, 0x73, 0x65, 0x67, 0x6d, + 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x03, 0x00, 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x19, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x66, + 0x63, 0x6f, 0x65, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x16, 0x00, 0x00, 0x00, 0x18, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x67, 0x72, 0x65, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, + 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x2c, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x17, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x67, 0x72, 0x65, 0x2d, 0x63, 0x73, 0x75, 0x6d, 0x2d, + 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x69, + 0x70, 0x78, 0x69, 0x70, 0x34, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x28, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x19, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x69, 0x70, 0x78, 0x69, 0x70, 0x36, 0x2d, 0x73, 0x65, + 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, + 0x2c, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x1a, 0x00, 0x00, 0x00, + 0x1c, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x75, 0x64, 0x70, 0x5f, 0x74, + 0x6e, 0x6c, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x03, 0x00, 0x34, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x21, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x75, 0x64, 0x70, 0x5f, 0x74, 0x6e, 0x6c, 0x2d, 0x63, + 0x73, 0x75, 0x6d, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x03, 0x00, + 0x20, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x13, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x67, 0x73, 0x6f, 0x2d, 0x70, + 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x00, 0x00, 0x30, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x23, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x2d, 0x72, 0x65, + 0x6d, 0x63, 0x73, 0x75, 0x6d, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x28, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x19, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x73, 0x63, 0x74, 0x70, 0x2d, 0x73, 0x65, 0x67, 0x6d, + 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x1f, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x65, 0x73, 0x70, 0x2d, 0x73, + 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, + 0x14, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x21, 0x00, 0x00, 0x00, 0x18, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x75, 0x64, 0x70, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, + 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x03, 0x00, + 0x1c, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x67, 0x73, 0x6f, 0x2d, 0x6c, + 0x69, 0x73, 0x74, 0x00, 0x2c, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x74, + 0x63, 0x70, 0x2d, 0x61, 0x63, 0x63, 0x65, 0x63, 0x6e, 0x2d, 0x73, 0x65, + 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, + 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x24, 0x00, 0x00, 0x00, + 0x19, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x63, 0x68, 0x65, 0x63, 0x6b, + 0x73, 0x75, 0x6d, 0x2d, 0x66, 0x63, 0x6f, 0x65, 0x2d, 0x63, 0x72, 0x63, + 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x25, 0x00, 0x00, 0x00, 0x15, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x2d, 0x73, 0x63, 0x74, 0x70, + 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x15, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x6e, + 0x74, 0x75, 0x70, 0x6c, 0x65, 0x2d, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x68, + 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x1c, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x28, 0x00, 0x00, 0x00, 0x10, 0x00, 0x02, 0x00, + 0x72, 0x78, 0x2d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x00, + 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x29, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x6e, 0x6f, 0x63, 0x61, 0x63, + 0x68, 0x65, 0x2d, 0x63, 0x6f, 0x70, 0x79, 0x00, 0x04, 0x00, 0x03, 0x00, + 0x1c, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x2a, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x02, 0x00, 0x6c, 0x6f, 0x6f, 0x70, 0x62, 0x61, 0x63, 0x6b, + 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x66, + 0x63, 0x73, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x61, + 0x6c, 0x6c, 0x00, 0x00, 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x76, + 0x6c, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x67, 0x2d, 0x68, 0x77, 0x2d, + 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x00, 0x00, 0x28, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x02, 0x00, + 0x72, 0x78, 0x2d, 0x76, 0x6c, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x67, + 0x2d, 0x68, 0x77, 0x2d, 0x70, 0x61, 0x72, 0x73, 0x65, 0x00, 0x00, 0x00, + 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x2f, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x76, 0x6c, 0x61, 0x6e, 0x2d, + 0x73, 0x74, 0x61, 0x67, 0x2d, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x00, + 0x20, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x13, 0x00, 0x02, 0x00, 0x6c, 0x32, 0x2d, 0x66, 0x77, 0x64, 0x2d, 0x6f, + 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x31, 0x00, 0x00, 0x00, 0x12, 0x00, 0x02, 0x00, + 0x68, 0x77, 0x2d, 0x74, 0x63, 0x2d, 0x6f, 0x66, 0x66, 0x6c, 0x6f, 0x61, + 0x64, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x32, 0x00, 0x00, 0x00, 0x13, 0x00, 0x02, 0x00, 0x65, 0x73, 0x70, 0x2d, + 0x68, 0x77, 0x2d, 0x6f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x00, 0x00, + 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x33, 0x00, 0x00, 0x00, + 0x1b, 0x00, 0x02, 0x00, 0x65, 0x73, 0x70, 0x2d, 0x74, 0x78, 0x2d, 0x63, + 0x73, 0x75, 0x6d, 0x2d, 0x68, 0x77, 0x2d, 0x6f, 0x66, 0x66, 0x6c, 0x6f, + 0x61, 0x64, 0x00, 0x00, 0x2c, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x34, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x75, + 0x64, 0x70, 0x5f, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x2d, 0x70, 0x6f, + 0x72, 0x74, 0x2d, 0x6f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x00, 0x00, + 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x35, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x02, 0x00, 0x74, 0x6c, 0x73, 0x2d, 0x68, 0x77, 0x2d, 0x74, + 0x78, 0x2d, 0x6f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x00, 0x00, 0x00, + 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x36, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x02, 0x00, 0x74, 0x6c, 0x73, 0x2d, 0x68, 0x77, 0x2d, 0x72, + 0x78, 0x2d, 0x6f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x37, 0x00, 0x00, 0x00, + 0x0e, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x67, 0x72, 0x6f, 0x2d, 0x68, + 0x77, 0x00, 0x00, 0x00, 0x04, 0x00, 0x03, 0x00, 0x14, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x38, 0x00, 0x00, 0x00, 0x05, 0x00, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x10, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x67, + 0x72, 0x6f, 0x2d, 0x6c, 0x69, 0x73, 0x74, 0x00, 0x04, 0x00, 0x03, 0x00, + 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x02, 0x00, 0x6d, 0x61, 0x63, 0x73, 0x65, 0x63, 0x2d, 0x68, + 0x77, 0x2d, 0x6f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x00, 0x00, 0x00, + 0x2c, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x3b, 0x00, 0x00, 0x00, + 0x1a, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x75, 0x64, 0x70, 0x2d, 0x67, + 0x72, 0x6f, 0x2d, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, + 0x67, 0x00, 0x00, 0x00, 0x04, 0x00, 0x03, 0x00, 0x24, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x18, 0x00, 0x02, 0x00, + 0x68, 0x73, 0x72, 0x2d, 0x74, 0x61, 0x67, 0x2d, 0x69, 0x6e, 0x73, 0x2d, + 0x6f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x00, 0x24, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x17, 0x00, 0x02, 0x00, + 0x68, 0x73, 0x72, 0x2d, 0x74, 0x61, 0x67, 0x2d, 0x72, 0x6d, 0x2d, 0x6f, + 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, + 0x68, 0x73, 0x72, 0x2d, 0x66, 0x77, 0x64, 0x2d, 0x6f, 0x66, 0x66, 0x6c, + 0x6f, 0x61, 0x64, 0x00, 0x20, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x3f, 0x00, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, 0x68, 0x73, 0x72, 0x2d, + 0x64, 0x75, 0x70, 0x2d, 0x6f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x00, + 0xac, 0x01, 0x03, 0x80, 0x04, 0x00, 0x01, 0x00, 0x08, 0x00, 0x02, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x9c, 0x01, 0x03, 0x80, 0x24, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x73, 0x63, 0x61, 0x74, 0x74, 0x65, 0x72, 0x2d, 0x67, + 0x61, 0x74, 0x68, 0x65, 0x72, 0x00, 0x00, 0x00, 0x28, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x2d, + 0x69, 0x70, 0x2d, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x00, 0x00, + 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x1c, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x67, 0x65, 0x6e, 0x65, 0x72, + 0x69, 0x63, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x67, + 0x72, 0x6f, 0x00, 0x00, 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x74, + 0x63, 0x70, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x00, 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x12, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x74, + 0x63, 0x70, 0x2d, 0x65, 0x63, 0x6e, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, + 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x28, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x19, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x74, 0x63, 0x70, 0x36, 0x2d, 0x73, 0x65, 0x67, 0x6d, + 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x1a, 0x00, 0x00, 0x00, + 0x1c, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x75, 0x64, 0x70, 0x5f, 0x74, + 0x6e, 0x6c, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x00, 0x30, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x1b, 0x00, 0x00, 0x00, 0x21, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x75, + 0x64, 0x70, 0x5f, 0x74, 0x6e, 0x6c, 0x2d, 0x63, 0x73, 0x75, 0x6d, 0x2d, + 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x18, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x75, + 0x64, 0x70, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x00, 0x1c, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x37, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x67, + 0x72, 0x6f, 0x2d, 0x68, 0x77, 0x00, 0x00, 0x00, 0x20, 0x02, 0x04, 0x80, + 0x04, 0x00, 0x01, 0x00, 0x08, 0x00, 0x02, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x10, 0x02, 0x03, 0x80, 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x73, + 0x63, 0x61, 0x74, 0x74, 0x65, 0x72, 0x2d, 0x67, 0x61, 0x74, 0x68, 0x65, + 0x72, 0x00, 0x00, 0x00, 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x2d, 0x69, 0x70, 0x2d, 0x67, + 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x05, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00, + 0x68, 0x69, 0x67, 0x68, 0x64, 0x6d, 0x61, 0x00, 0x20, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x09, 0x00, 0x00, 0x00, 0x13, 0x00, 0x02, 0x00, + 0x72, 0x78, 0x2d, 0x76, 0x6c, 0x61, 0x6e, 0x2d, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x00, 0x00, 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x67, + 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, + 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x18, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x00, + 0x72, 0x78, 0x2d, 0x67, 0x72, 0x6f, 0x00, 0x00, 0x24, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x74, 0x63, 0x70, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, + 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x20, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x67, 0x73, 0x6f, 0x2d, 0x72, 0x6f, 0x62, 0x75, 0x73, + 0x74, 0x00, 0x00, 0x00, 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x12, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x74, + 0x63, 0x70, 0x2d, 0x65, 0x63, 0x6e, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, + 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x28, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x19, 0x00, 0x02, 0x00, + 0x74, 0x78, 0x2d, 0x74, 0x63, 0x70, 0x36, 0x2d, 0x73, 0x65, 0x67, 0x6d, + 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x1a, 0x00, 0x00, 0x00, + 0x1c, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x75, 0x64, 0x70, 0x5f, 0x74, + 0x6e, 0x6c, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x00, 0x30, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x1b, 0x00, 0x00, 0x00, 0x21, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x75, + 0x64, 0x70, 0x5f, 0x74, 0x6e, 0x6c, 0x2d, 0x63, 0x73, 0x75, 0x6d, 0x2d, + 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x18, 0x00, 0x02, 0x00, 0x74, 0x78, 0x2d, 0x75, + 0x64, 0x70, 0x2d, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x00, 0x1c, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x10, 0x00, 0x02, 0x00, 0x72, 0x78, 0x2d, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x00, 0x1c, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x37, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x02, 0x00, + 0x72, 0x78, 0x2d, 0x67, 0x72, 0x6f, 0x2d, 0x68, 0x77, 0x00, 0x00, 0x00, + 0x34, 0x00, 0x05, 0x80, 0x04, 0x00, 0x01, 0x00, 0x08, 0x00, 0x02, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x24, 0x00, 0x03, 0x80, 0x20, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, + 0x76, 0x6c, 0x61, 0x6e, 0x2d, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, + 0x67, 0x65, 0x64, 0x00, + ]; + + // FEATURES_GET_REPLY for enp2s0 without COMPACT flag + let msg = parse_and_round_trip(&raw); + + assert_eq!(msg.cmd, EthtoolCmd::FeatureGetReply); + assert_eq!(msg.nlas.len(), 5); + assert_eq!( + msg.nlas[0], + EthtoolAttr::Feature(EthtoolFeatureAttr::Header(vec![ + EthtoolHeader::DevIndex(2), + EthtoolHeader::DevName("enp2s0".to_string()), + ])) + ); + + let hw = match &msg.nlas[1] { + EthtoolAttr::Feature(EthtoolFeatureAttr::Hw(bitset)) => bitset, + other => panic!("Expected HW features, got {other:?}"), + }; + assert!(!hw.nomask); + assert_eq!(hw.size, 64); + let bits = match &hw.value { + EthtoolBitsetValue::Bits(bits) => bits, + other => panic!("Expected verbose bitset, got {other:?}"), + }; + assert_eq!(bits.len(), 64); + assert_eq!( + bits[0], + EthtoolBit { + index: 0, + name: "tx-scatter-gather".to_string(), + value: true, + } + ); + // Kernel emits bits with empty name for features without a name, + // they must not be filtered out. + assert_eq!( + bits[2], + EthtoolBit { + index: 2, + name: String::new(), + value: false, + } + ); + assert!(bits.iter().any(|bit| bit.name.is_empty())); + + let wanted = match &msg.nlas[2] { + EthtoolAttr::Feature(EthtoolFeatureAttr::Wanted(bitset)) => bitset, + other => panic!("Expected wanted features, got {other:?}"), + }; + assert!(wanted.nomask); + assert_eq!(wanted.size, 64); + let bits = match &wanted.value { + EthtoolBitsetValue::Bits(bits) => bits, + other => panic!("Expected verbose bitset, got {other:?}"), + }; + assert_eq!(bits.len(), 11); + assert_eq!( + bits[0], + EthtoolBit { + index: 0, + name: "tx-scatter-gather".to_string(), + value: true, + } + ); + + let active = match &msg.nlas[3] { + EthtoolAttr::Feature(EthtoolFeatureAttr::Active(bitset)) => bitset, + other => panic!("Expected active features, got {other:?}"), + }; + assert_eq!( + match &active.value { + EthtoolBitsetValue::Bits(bits) => bits.len(), + other => panic!("Expected verbose bitset, got {other:?}"), + }, + 15 + ); + + let nochange = match &msg.nlas[4] { + EthtoolAttr::Feature(EthtoolFeatureAttr::NoChange(bitset)) => bitset, + other => panic!("Expected nochange features, got {other:?}"), + }; + assert_eq!( + nochange.value, + EthtoolBitsetValue::Bits(vec![EthtoolBit { + index: 10, + name: "vlan-challenged".to_string(), + value: true, + }]) + ); +} diff --git a/src/tests/fec.rs b/src/tests/fec.rs new file mode 100644 index 0000000..241cf53 --- /dev/null +++ b/src/tests/fec.rs @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT + +// Generated from kernel emitted ethtool netlink packets captured with an +// nlmon device (`modprobe nlmon`, `ip link add nl0 type nlmon`, +// `ip link set nl0 up`, `tcpdump -i nl0 -w capture.pcap`) on Linux 7.2 and +// inlined here. The 16 bytes netlink header is stripped, so the data starts +// at the generic netlink header exactly like the buffers parsed by +// `EthtoolMessage::parse_with_param()`. Each test verifies the captured +// packet is parsed into the expected attributes and emitted back byte for +// byte. + +use super::parse_and_round_trip; +use crate::{ + EthtoolAttr, EthtoolBit, EthtoolBitset, EthtoolBitsetValue, EthtoolCmd, + EthtoolFecAttr, EthtoolFecMode, EthtoolFecStat, EthtoolHeader, + EthtoolMessage, +}; +use netlink_packet_core::{Nla, NLA_F_NESTED}; + +#[test] +fn test_parsing_fec_netdevsim() { + let raw: Vec = vec![ + 0x1e, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0xa3, 0x00, 0x00, 0x00, 0x09, 0x00, 0x02, 0x00, 0x73, 0x69, 0x6d, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x02, 0x80, 0x04, 0x00, 0x01, 0x00, + 0x08, 0x00, 0x02, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x18, 0x00, 0x03, 0x80, + 0x14, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x32, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x02, 0x00, 0x52, 0x53, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x32, 0x00, 0x00, 0x00, + ]; + + // ethtool --show-fec sim0 after ethtool --set-fec sim0 encoding rs + let msg = parse_and_round_trip(&raw); + + assert_eq!( + msg, + EthtoolMessage { + cmd: EthtoolCmd::FecGetReply, + nlas: vec![ + EthtoolAttr::Fec(EthtoolFecAttr::Header(vec![ + EthtoolHeader::DevIndex(163), + EthtoolHeader::DevName("sim0".to_string()), + ])), + EthtoolAttr::Fec(EthtoolFecAttr::Modes(EthtoolBitset { + nomask: true, + size: 125, + value: EthtoolBitsetValue::Bits(vec![EthtoolBit { + index: 50, + name: "RS".to_string(), + value: true, + }]), + })), + EthtoolAttr::Fec(EthtoolFecAttr::Auto(false)), + EthtoolAttr::Fec(EthtoolFecAttr::Active(EthtoolFecMode::Rs)), + ], + } + ); +} + +#[test] +fn test_parsing_fec_baser() { + let raw: Vec = vec![ + 0x1e, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0xa4, 0x00, 0x00, 0x00, 0x09, 0x00, 0x02, 0x00, 0x73, 0x69, 0x6d, 0x31, + 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x02, 0x80, 0x04, 0x00, 0x01, 0x00, + 0x08, 0x00, 0x02, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x03, 0x80, + 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x33, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x02, 0x00, 0x42, 0x41, 0x53, 0x45, 0x52, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, + ]; + + // ethtool --show-fec sim1 after ethtool --set-fec sim1 encoding baser + let msg = parse_and_round_trip(&raw); + + assert_eq!( + msg, + EthtoolMessage { + cmd: EthtoolCmd::FecGetReply, + nlas: vec![ + EthtoolAttr::Fec(EthtoolFecAttr::Header(vec![ + EthtoolHeader::DevIndex(164), + EthtoolHeader::DevName("sim1".to_string()), + ])), + EthtoolAttr::Fec(EthtoolFecAttr::Modes(EthtoolBitset { + nomask: true, + size: 125, + value: EthtoolBitsetValue::Bits(vec![EthtoolBit { + index: 51, + name: "BASER".to_string(), + value: true, + }]), + })), + EthtoolAttr::Fec(EthtoolFecAttr::Auto(false)), + EthtoolAttr::Fec(EthtoolFecAttr::Active(EthtoolFecMode::Baser)), + ], + } + ); +} + +#[test] +fn test_parsing_fec_stats_netdevsim() { + let raw: Vec = vec![ + 0x1e, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0xa3, 0x00, 0x00, 0x00, 0x09, 0x00, 0x02, 0x00, 0x73, 0x69, 0x6d, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x02, 0x80, 0x04, 0x00, 0x01, 0x00, + 0x08, 0x00, 0x02, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x18, 0x00, 0x03, 0x80, + 0x14, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x32, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x02, 0x00, 0x52, 0x53, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x32, 0x00, 0x00, 0x00, + 0xbc, 0x00, 0x05, 0x80, 0x0c, 0x00, 0x02, 0x00, 0x7b, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0x40, 0x00, 0x05, 0x80, + 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0xbd, 0x01, 0x00, 0x00, + 0x24, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1c, 0x00, 0x05, 0x80, 0x08, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x40, 0x00, 0x05, 0x80, 0x08, 0x00, 0x02, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x03, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x05, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + // ethtool --show-fec --include-statistics sim0 + let msg = parse_and_round_trip(&raw); + + assert_eq!(msg.cmd, EthtoolCmd::FecGetReply); + assert_eq!(msg.nlas.len(), 5); + assert_eq!( + msg.nlas[0], + EthtoolAttr::Fec(EthtoolFecAttr::Header(vec![ + EthtoolHeader::DevIndex(163), + EthtoolHeader::DevName("sim0".to_string()), + ])) + ); + assert_eq!(msg.nlas[2], EthtoolAttr::Fec(EthtoolFecAttr::Auto(false))); + assert_eq!( + msg.nlas[3], + EthtoolAttr::Fec(EthtoolFecAttr::Active(EthtoolFecMode::Rs)) + ); + let stats = match &msg.nlas[4] { + EthtoolAttr::Fec(EthtoolFecAttr::Stats(stats)) => stats, + other => panic!("Expected FEC stats, got {other:?}"), + }; + // The driver has no corrected bits stats hence the kernel emits + // ETHTOOL_A_FEC_STAT_CORR_BITS with an empty payload. Followed by the + // per lane stats histogram. + assert_eq!(stats.len(), 6); + assert_eq!(stats[0], EthtoolFecStat::Corrected(vec![123])); + assert_eq!(stats[1], EthtoolFecStat::Uncorrected(vec![4])); + assert_eq!(stats[2], EthtoolFecStat::CorrectBits(vec![])); + // ETHTOOL_A_FEC_STAT_HIST per lane histogram attributes + for stat in &stats[3..] { + match stat { + EthtoolFecStat::Other(nla) => { + assert_eq!(nla.kind(), 5 | NLA_F_NESTED) + } + other => panic!("Expected FEC stats hist, got {other:?}"), + } + } +} diff --git a/src/tests/link_modes.rs b/src/tests/link_modes.rs new file mode 100644 index 0000000..eee701e --- /dev/null +++ b/src/tests/link_modes.rs @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT + +// Generated from kernel emitted ethtool netlink packets captured with an +// nlmon device (`modprobe nlmon`, `ip link add nl0 type nlmon`, +// `ip link set nl0 up`, `tcpdump -i nl0 -w capture.pcap`) on Linux 7.2 and +// inlined here. The 16 bytes netlink header is stripped, so the data starts +// at the generic netlink header exactly like the buffers parsed by +// `EthtoolMessage::parse_with_param()`. Each test verifies the captured +// packet is parsed into the expected attributes and emitted back byte for +// byte. + +use super::parse_and_round_trip; +use crate::{ + EthtoolAttr, EthtoolBitset, EthtoolBitsetValue, EthtoolCmd, EthtoolHeader, + EthtoolLinkModeAttr, EthtoolLinkModeDuplex, EthtoolMessage, +}; +use netlink_packet_core::DefaultNla; + +#[test] +fn test_parsing_link_modes_virtio() { + let raw: Vec = vec![ + 0x04, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x00, 0x65, 0x6e, 0x70, 0x32, + 0x73, 0x30, 0x00, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x03, 0x80, 0x08, 0x00, 0x02, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x03, 0x80, 0x08, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x05, 0x00, 0x06, 0x00, 0xff, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0a, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + // ethtool enp2s0 + let msg = parse_and_round_trip(&raw); + + assert_eq!( + msg, + EthtoolMessage { + cmd: EthtoolCmd::LinkModeGetReply, + nlas: vec![ + EthtoolAttr::LinkMode(EthtoolLinkModeAttr::Header(vec![ + EthtoolHeader::DevIndex(2), + EthtoolHeader::DevName("enp2s0".to_string()), + ])), + EthtoolAttr::LinkMode(EthtoolLinkModeAttr::Autoneg(false)), + EthtoolAttr::LinkMode(EthtoolLinkModeAttr::Ours( + EthtoolBitset { + nomask: false, + size: 125, + value: EthtoolBitsetValue::Bits(vec![]), + } + )), + EthtoolAttr::LinkMode(EthtoolLinkModeAttr::Speed(u32::MAX)), + EthtoolAttr::LinkMode(EthtoolLinkModeAttr::Duplex( + EthtoolLinkModeDuplex::Unknown + )), + EthtoolAttr::LinkMode(EthtoolLinkModeAttr::Other( + DefaultNla::new(10, vec![0]) + )), + ], + } + ); +} + +#[test] +fn test_parsing_link_modes_veth() { + let raw: Vec = vec![ + 0x04, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x02, 0x00, 0x76, 0x65, 0x74, 0x68, + 0x30, 0x00, 0x00, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x03, 0x80, 0x08, 0x00, 0x02, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x03, 0x80, 0x08, 0x00, 0x05, 0x00, 0x10, 0x27, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0a, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + // ethtool veth0 + let msg = parse_and_round_trip(&raw); + + assert_eq!( + msg, + EthtoolMessage { + cmd: EthtoolCmd::LinkModeGetReply, + nlas: vec![ + EthtoolAttr::LinkMode(EthtoolLinkModeAttr::Header(vec![ + EthtoolHeader::DevIndex(256), + EthtoolHeader::DevName("veth0".to_string()), + ])), + EthtoolAttr::LinkMode(EthtoolLinkModeAttr::Autoneg(false)), + EthtoolAttr::LinkMode(EthtoolLinkModeAttr::Ours( + EthtoolBitset { + nomask: false, + size: 125, + value: EthtoolBitsetValue::Bits(vec![]), + } + )), + EthtoolAttr::LinkMode(EthtoolLinkModeAttr::Speed(10000)), + EthtoolAttr::LinkMode(EthtoolLinkModeAttr::Duplex( + EthtoolLinkModeDuplex::Full + )), + EthtoolAttr::LinkMode(EthtoolLinkModeAttr::Other( + DefaultNla::new(10, vec![0]) + )), + ], + } + ); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..f8fd56e --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +mod channels; +mod coalesce; +mod features; +mod fec; +mod link_modes; +mod pause; +mod rings; +mod tsinfo; + +use crate::EthtoolMessage; +use netlink_packet_core::{Emitable, ParseableParametrized}; +use netlink_packet_generic::GenlHeader; + +/// Parse a captured generic netlink payload (starting at the generic netlink +/// header, the 16 bytes netlink header having been stripped) and verify it +/// can be emitted back byte for byte. +fn parse_and_round_trip(raw: &[u8]) -> EthtoolMessage { + let header = GenlHeader::parse(raw).unwrap(); + let msg = + EthtoolMessage::parse_with_param(&raw[header.buffer_len()..], header) + .unwrap(); + let mut buffer = vec![0; msg.buffer_len() + header.buffer_len()]; + header.emit(&mut buffer); + msg.emit(&mut buffer[header.buffer_len()..]); + assert_eq!(buffer, raw); + msg +} diff --git a/src/tests/pause.rs b/src/tests/pause.rs new file mode 100644 index 0000000..f1ec9dc --- /dev/null +++ b/src/tests/pause.rs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +// Generated from kernel emitted ethtool netlink packets captured with an +// nlmon device (`modprobe nlmon`, `ip link add nl0 type nlmon`, +// `ip link set nl0 up`, `tcpdump -i nl0 -w capture.pcap`) on Linux 7.2 and +// inlined here. The 16 bytes netlink header is stripped, so the data starts +// at the generic netlink header exactly like the buffers parsed by +// `EthtoolMessage::parse_with_param()`. Each test verifies the captured +// packet is parsed into the expected attributes and emitted back byte for +// byte. + +use super::parse_and_round_trip; +use crate::{ + EthtoolAttr, EthtoolCmd, EthtoolHeader, EthtoolMessage, EthtoolPauseAttr, + EthtoolPauseStatAttr, +}; +use netlink_packet_core::DefaultNla; + +#[test] +fn test_parsing_pause_netdevsim() { + let raw: Vec = vec![ + 0x16, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0xa3, 0x00, 0x00, 0x00, 0x09, 0x00, 0x02, 0x00, 0x73, 0x69, 0x6d, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x01, 0x00, 0x00, 0x00, + ]; + + // ethtool -a sim0 after ethtool -A sim0 autoneg off rx on tx on + let msg = parse_and_round_trip(&raw); + + assert_eq!( + msg, + EthtoolMessage { + cmd: EthtoolCmd::PauseGetReply, + nlas: vec![ + EthtoolAttr::Pause(EthtoolPauseAttr::Header(vec![ + EthtoolHeader::DevIndex(163), + EthtoolHeader::DevName("sim0".to_string()), + ])), + EthtoolAttr::Pause(EthtoolPauseAttr::AutoNeg(false)), + EthtoolAttr::Pause(EthtoolPauseAttr::Rx(true)), + EthtoolAttr::Pause(EthtoolPauseAttr::Tx(true)), + ], + } + ); +} + +#[test] +fn test_parsing_pause_stats_netdevsim() { + let raw: Vec = vec![ + 0x16, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0xa3, 0x00, 0x00, 0x00, 0x09, 0x00, 0x02, 0x00, 0x73, 0x69, 0x6d, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1c, 0x00, 0x05, 0x80, 0x0c, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + // ethtool -a --include-statistics sim0 + let msg = parse_and_round_trip(&raw); + + assert_eq!( + msg, + EthtoolMessage { + cmd: EthtoolCmd::PauseGetReply, + nlas: vec![ + EthtoolAttr::Pause(EthtoolPauseAttr::Header(vec![ + EthtoolHeader::DevIndex(163), + EthtoolHeader::DevName("sim0".to_string()), + ])), + EthtoolAttr::Pause(EthtoolPauseAttr::AutoNeg(false)), + EthtoolAttr::Pause(EthtoolPauseAttr::Rx(true)), + EthtoolAttr::Pause(EthtoolPauseAttr::Tx(true)), + // ETHTOOL_A_PAUSE_STATS_SRC (6) + EthtoolAttr::Pause(EthtoolPauseAttr::Other(DefaultNla::new( + 6, + vec![0, 0, 0, 0] + ))), + EthtoolAttr::Pause(EthtoolPauseAttr::Stats(vec![ + EthtoolPauseStatAttr::Tx(2), + EthtoolPauseStatAttr::Rx(1), + ])), + ], + } + ); +} diff --git a/src/tests/rings.rs b/src/tests/rings.rs new file mode 100644 index 0000000..8672353 --- /dev/null +++ b/src/tests/rings.rs @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT + +// Generated from kernel emitted ethtool netlink packets captured with an +// nlmon device (`modprobe nlmon`, `ip link add nl0 type nlmon`, +// `ip link set nl0 up`, `tcpdump -i nl0 -w capture.pcap`) on Linux 7.2 and +// inlined here. The 16 bytes netlink header is stripped, so the data starts +// at the generic netlink header exactly like the buffers parsed by +// `EthtoolMessage::parse_with_param()`. Each test verifies the captured +// packet is parsed into the expected attributes and emitted back byte for +// byte. + +use super::parse_and_round_trip; +use crate::{ + EthtoolAttr, EthtoolCmd, EthtoolHeader, EthtoolMessage, EthtoolRingAttr, +}; +use netlink_packet_core::DefaultNla; + +#[test] +fn test_parsing_rings_netdevsim() { + let raw: Vec = vec![ + 0x10, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0xa3, 0x00, 0x00, 0x00, 0x09, 0x00, 0x02, 0x00, 0x73, 0x69, 0x6d, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x10, 0x00, 0x00, + 0x08, 0x00, 0x06, 0x00, 0x00, 0x02, 0x00, 0x00, 0x08, 0x00, 0x03, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x07, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x04, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x08, 0x00, 0x05, 0x00, 0x00, 0x10, 0x00, 0x00, + 0x08, 0x00, 0x09, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x0b, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x11, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x12, 0x00, 0x00, 0x04, 0x00, 0x00, + ]; + + // ethtool -g sim0 after ethtool -G sim0 rx 512 tx 512 rx-mini 8 rx-jumbo 16 + let msg = parse_and_round_trip(&raw); + + assert_eq!( + msg, + EthtoolMessage { + cmd: EthtoolCmd::RingGetReply, + nlas: vec![ + EthtoolAttr::Ring(EthtoolRingAttr::Header(vec![ + EthtoolHeader::DevIndex(163), + EthtoolHeader::DevName("sim0".to_string()), + ])), + EthtoolAttr::Ring(EthtoolRingAttr::RxMax(4096)), + EthtoolAttr::Ring(EthtoolRingAttr::Rx(512)), + EthtoolAttr::Ring(EthtoolRingAttr::RxMiniMax(4096)), + EthtoolAttr::Ring(EthtoolRingAttr::RxMini(8)), + EthtoolAttr::Ring(EthtoolRingAttr::RxJumboMax(4096)), + EthtoolAttr::Ring(EthtoolRingAttr::RxJumbo(16)), + EthtoolAttr::Ring(EthtoolRingAttr::TxMax(4096)), + EthtoolAttr::Ring(EthtoolRingAttr::Tx(512)), + EthtoolAttr::Ring(EthtoolRingAttr::TcpDataSplit(2)), + EthtoolAttr::Ring(EthtoolRingAttr::TxPush(false)), + // ETHTOOL_A_RINGS_RX_PUSH (14) + EthtoolAttr::Ring(EthtoolRingAttr::Other(DefaultNla::new( + 14, + vec![0] + ))), + // ETHTOOL_A_RINGS_HDS_THRESH (17) + EthtoolAttr::Ring(EthtoolRingAttr::Other(DefaultNla::new( + 17, + vec![0, 0, 0, 0] + ))), + // ETHTOOL_A_RINGS_HDS_THRESH_MAX (18) + EthtoolAttr::Ring(EthtoolRingAttr::Other(DefaultNla::new( + 18, + vec![0, 4, 0, 0] + ))), + ], + } + ); +} + +#[test] +fn test_parsing_rings_virtio() { + let raw: Vec = vec![ + 0x10, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x00, 0x65, 0x6e, 0x70, 0x32, + 0x73, 0x30, 0x00, 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x08, 0x00, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x08, 0x00, 0x05, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x08, 0x00, 0x09, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0e, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + // ethtool -g enp2s0 + let msg = parse_and_round_trip(&raw); + + assert_eq!( + msg, + EthtoolMessage { + cmd: EthtoolCmd::RingGetReply, + nlas: vec![ + EthtoolAttr::Ring(EthtoolRingAttr::Header(vec![ + EthtoolHeader::DevIndex(2), + EthtoolHeader::DevName("enp2s0".to_string()), + ])), + EthtoolAttr::Ring(EthtoolRingAttr::RxMax(256)), + EthtoolAttr::Ring(EthtoolRingAttr::Rx(256)), + EthtoolAttr::Ring(EthtoolRingAttr::TxMax(256)), + EthtoolAttr::Ring(EthtoolRingAttr::Tx(256)), + EthtoolAttr::Ring(EthtoolRingAttr::TxPush(false)), + // ETHTOOL_A_RINGS_RX_PUSH (14) + EthtoolAttr::Ring(EthtoolRingAttr::Other(DefaultNla::new( + 14, + vec![0] + ))), + ], + } + ); +} diff --git a/src/tests/tsinfo.rs b/src/tests/tsinfo.rs new file mode 100644 index 0000000..396bb7d --- /dev/null +++ b/src/tests/tsinfo.rs @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT + +// Generated from kernel emitted ethtool netlink packets captured with an +// nlmon device (`modprobe nlmon`, `ip link add nl0 type nlmon`, +// `ip link set nl0 up`, `tcpdump -i nl0 -w capture.pcap`) on Linux 7.2 and +// inlined here. The 16 bytes netlink header is stripped, so the data starts +// at the generic netlink header exactly like the buffers parsed by +// `EthtoolMessage::parse_with_param()`. Each test verifies the captured +// packet is parsed into the expected attributes and emitted back byte for +// byte. + +use super::parse_and_round_trip; +use crate::{ + EthtoolAttr, EthtoolBit, EthtoolBitset, EthtoolBitsetValue, EthtoolCmd, + EthtoolHeader, EthtoolTsInfoAttr, +}; +use netlink_packet_core::{DefaultNla, NLA_F_NESTED}; + +#[test] +fn test_parsing_tsinfo_virtio() { + let raw: Vec = vec![ + 0x1a, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x00, 0x65, 0x6e, 0x70, 0x32, + 0x73, 0x30, 0x00, 0x00, 0x84, 0x00, 0x02, 0x80, 0x04, 0x00, 0x01, 0x00, + 0x08, 0x00, 0x02, 0x00, 0x13, 0x00, 0x00, 0x00, 0x74, 0x00, 0x03, 0x80, + 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x02, 0x00, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, + 0x2d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x00, 0x00, 0x00, + 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x15, 0x00, 0x02, 0x00, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, + 0x2d, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x00, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x1a, 0x00, 0x02, 0x00, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, + 0x2d, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x2d, 0x63, 0x6c, 0x6f, 0x63, + 0x6b, 0x00, 0x00, 0x00, + ]; + + // ethtool -T enp2s0 + let msg = parse_and_round_trip(&raw); + + assert_eq!(msg.cmd, EthtoolCmd::TsInfoGetReply); + assert_eq!(msg.nlas.len(), 2); + assert_eq!( + msg.nlas[0], + EthtoolAttr::TsInfo(EthtoolTsInfoAttr::Header(vec![ + EthtoolHeader::DevIndex(2), + EthtoolHeader::DevName("enp2s0".to_string()), + ])) + ); + assert_eq!( + msg.nlas[1], + EthtoolAttr::TsInfo(EthtoolTsInfoAttr::Timestamping(EthtoolBitset { + nomask: true, + size: 19, + value: EthtoolBitsetValue::Bits(vec![ + EthtoolBit { + index: 1, + name: "software-transmit".to_string(), + value: true, + }, + EthtoolBit { + index: 3, + name: "software-receive".to_string(), + value: true, + }, + EthtoolBit { + index: 4, + name: "software-system-clock".to_string(), + value: true, + }, + ]), + })) + ); +} + +#[test] +fn test_parsing_tsinfo_netdevsim() { + let raw: Vec = vec![ + 0x1a, 0x01, 0x00, 0x00, 0x18, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, + 0xa3, 0x00, 0x00, 0x00, 0x09, 0x00, 0x02, 0x00, 0x73, 0x69, 0x6d, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x02, 0x80, 0x04, 0x00, 0x01, 0x00, + 0x08, 0x00, 0x02, 0x00, 0x13, 0x00, 0x00, 0x00, 0x50, 0x00, 0x03, 0x80, + 0x24, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x15, 0x00, 0x02, 0x00, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, + 0x2d, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x00, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x1a, 0x00, 0x02, 0x00, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, + 0x2d, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x2d, 0x63, 0x6c, 0x6f, 0x63, + 0x6b, 0x00, 0x00, 0x00, 0x08, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x07, 0x80, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, + 0x01, 0x00, 0x00, 0x00, + ]; + + // ethtool -T sim0 + let msg = parse_and_round_trip(&raw); + + assert_eq!(msg.cmd, EthtoolCmd::TsInfoGetReply); + assert_eq!(msg.nlas.len(), 5); + assert_eq!( + msg.nlas[0], + EthtoolAttr::TsInfo(EthtoolTsInfoAttr::Header(vec![ + EthtoolHeader::DevIndex(163), + EthtoolHeader::DevName("sim0".to_string()), + ])) + ); + assert_eq!( + msg.nlas[1], + EthtoolAttr::TsInfo(EthtoolTsInfoAttr::Timestamping(EthtoolBitset { + nomask: true, + size: 19, + value: EthtoolBitsetValue::Bits(vec![ + EthtoolBit { + index: 3, + name: "software-receive".to_string(), + value: true, + }, + EthtoolBit { + index: 4, + name: "software-system-clock".to_string(), + value: true, + }, + ]), + })) + ); + assert_eq!( + msg.nlas[2], + EthtoolAttr::TsInfo(EthtoolTsInfoAttr::PhcIndex(0)) + ); + // Unknown/newer kernel attributes (ETHTOOL_A_TSINFO_HWTSTAMP_PROVIDER + // and ETHTOOL_A_TSINFO_HWTSTAMP_SOURCE) are kept as raw attributes. + assert_eq!( + msg.nlas[3], + EthtoolAttr::TsInfo(EthtoolTsInfoAttr::Other(DefaultNla::new( + 7 | NLA_F_NESTED, + vec![8, 0, 1, 0, 0, 0, 0, 0, 8, 0, 2, 0, 0, 0, 0, 0,] + ))) + ); + assert_eq!( + msg.nlas[4], + EthtoolAttr::TsInfo(EthtoolTsInfoAttr::Other(DefaultNla::new( + 8, + vec![1, 0, 0, 0] + ))) + ); +} diff --git a/src/tsinfo/attr.rs b/src/tsinfo/attr.rs index f213441..75765aa 100644 --- a/src/tsinfo/attr.rs +++ b/src/tsinfo/attr.rs @@ -1,13 +1,11 @@ // SPDX-License-Identifier: MIT use netlink_packet_core::{ - parse_u32, DecodeError, DefaultNla, Emitable, ErrorContext, Nla, NlaBuffer, - NlasIterator, Parseable, NLA_F_NESTED, + emit_u32, parse_u32, DecodeError, DefaultNla, Emitable, ErrorContext, Nla, + NlaBuffer, NlasIterator, Parseable, NLA_F_NESTED, }; -use crate::{ - bitset_util::parse_bitset_bits_string_nlas, EthtoolAttr, EthtoolHeader, -}; +use crate::{bitset::EthtoolBitset, EthtoolAttr, EthtoolHeader}; const ETHTOOL_A_TSINFO_HEADER: u16 = 1; const ETHTOOL_A_TSINFO_TIMESTAMPING: u16 = 2; @@ -18,9 +16,9 @@ const ETHTOOL_A_TSINFO_PHC_INDEX: u16 = 5; #[derive(Debug, PartialEq, Eq, Clone)] pub enum EthtoolTsInfoAttr { Header(Vec), - Timestamping(Vec), - TxTypes(Vec), - RxFilters(Vec), + Timestamping(EthtoolBitset), + TxTypes(EthtoolBitset), + RxFilters(EthtoolBitset), PhcIndex(u32), Other(DefaultNla), } @@ -29,10 +27,10 @@ impl Nla for EthtoolTsInfoAttr { fn value_len(&self) -> usize { match self { Self::Header(hdrs) => hdrs.as_slice().buffer_len(), - Self::Timestamping(_) - | Self::PhcIndex(_) - | Self::TxTypes(_) - | Self::RxFilters(_) => 4, + Self::Timestamping(bitset) + | Self::TxTypes(bitset) + | Self::RxFilters(bitset) => bitset.buffer_len(), + Self::PhcIndex(_) => 4, Self::Other(attr) => attr.value_len(), } } @@ -40,9 +38,11 @@ impl Nla for EthtoolTsInfoAttr { fn kind(&self) -> u16 { match self { Self::Header(_) => ETHTOOL_A_TSINFO_HEADER | NLA_F_NESTED, - Self::Timestamping(_) => ETHTOOL_A_TSINFO_TIMESTAMPING, - Self::TxTypes(_) => ETHTOOL_A_TSINFO_TX_TYPES, - Self::RxFilters(_) => ETHTOOL_A_TSINFO_RX_FILTERS, + Self::Timestamping(_) => { + ETHTOOL_A_TSINFO_TIMESTAMPING | NLA_F_NESTED + } + Self::TxTypes(_) => ETHTOOL_A_TSINFO_TX_TYPES | NLA_F_NESTED, + Self::RxFilters(_) => ETHTOOL_A_TSINFO_RX_FILTERS | NLA_F_NESTED, Self::PhcIndex(_) => ETHTOOL_A_TSINFO_PHC_INDEX, Self::Other(attr) => attr.kind(), } @@ -52,7 +52,10 @@ impl Nla for EthtoolTsInfoAttr { match self { Self::Header(ref nlas) => nlas.as_slice().emit(buffer), Self::Other(ref attr) => attr.emit_value(buffer), - _ => todo!("Does not support changing ethtool ts info yet"), + Self::Timestamping(bitset) + | Self::TxTypes(bitset) + | Self::RxFilters(bitset) => bitset.emit(buffer), + Self::PhcIndex(value) => emit_u32(buffer, *value).unwrap(), } } } @@ -75,15 +78,15 @@ impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> Self::Header(nlas) } ETHTOOL_A_TSINFO_TIMESTAMPING => Self::Timestamping( - parse_bitset_bits_string_nlas(payload) + EthtoolBitset::parse(payload) .context("Invalid ETHTOOL_A_TSINFO_TIMESTAMPING value")?, ), ETHTOOL_A_TSINFO_TX_TYPES => Self::TxTypes( - parse_bitset_bits_string_nlas(payload) + EthtoolBitset::parse(payload) .context("Invalid ETHTOOL_A_TSINFO_TX_TYPES value")?, ), ETHTOOL_A_TSINFO_RX_FILTERS => Self::RxFilters( - parse_bitset_bits_string_nlas(payload) + EthtoolBitset::parse(payload) .context("Invalid ETHTOOL_A_TSINFO_RX_FILTERS value")?, ), ETHTOOL_A_TSINFO_PHC_INDEX => Self::PhcIndex( diff --git a/tests/dump_link_modes.rs b/tests/dump_link_modes.rs index 4d9b34f..5d7260e 100644 --- a/tests/dump_link_modes.rs +++ b/tests/dump_link_modes.rs @@ -26,7 +26,7 @@ async fn dump_link_modes() { } assert!(!msgs.is_empty()); let ethtool_msg = &msgs[0].payload; - println!("ethtool_msg {:?}", ðtool_msg); + println!("ethtool_msg {ethtool_msg:?}"); assert!(ethtool_msg.cmd == ethtool::EthtoolCmd::LinkModeGetReply); assert!(ethtool_msg.nlas.len() > 1);