diff --git a/src/lib.rs b/src/lib.rs index 06bccbd2..51c857c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ pub mod nsid; pub mod prefix; pub mod route; pub mod rule; +pub mod stats; pub mod tc; mod message; diff --git a/src/message.rs b/src/message.rs index f9492f90..eb3315fc 100644 --- a/src/message.rs +++ b/src/message.rs @@ -14,6 +14,7 @@ use crate::{ prefix::{PrefixMessage, PrefixMessageBuffer}, route::{RouteHeader, RouteMessage, RouteMessageBuffer}, rule::{RuleMessage, RuleMessageBuffer}, + stats::{StatsMessage, StatsMessageBuffer}, tc::{TcActionMessage, TcActionMessageBuffer, TcMessage, TcMessageBuffer}, }; @@ -66,8 +67,8 @@ const RTM_SETNEIGHTBL: u16 = 67; const RTM_NEWNSID: u16 = 88; const RTM_DELNSID: u16 = 89; const RTM_GETNSID: u16 = 90; -// const RTM_NEWSTATS: u16 = 92; -// const RTM_GETSTATS: u16 = 94; +const RTM_NEWSTATS: u16 = 92; +const RTM_GETSTATS: u16 = 94; // const RTM_NEWCACHEREPORT: u16 = 96; const RTM_NEWCHAIN: u16 = 100; const RTM_DELCHAIN: u16 = 101; @@ -302,6 +303,21 @@ impl<'a, T: AsRef<[u8]> + ?Sized> } } + // Stats messages + RTM_NEWSTATS | RTM_GETSTATS => { + let err = "invalid stats message"; + let msg = StatsMessage::parse( + &StatsMessageBuffer::new_checked(&buf.inner()) + .context(err)?, + ) + .context(err)?; + match message_type { + RTM_NEWSTATS => RouteNetlinkMessage::NewStats(msg), + RTM_GETSTATS => RouteNetlinkMessage::GetStats(msg), + _ => unreachable!(), + } + } + // ND ID Messages RTM_NEWNSID | RTM_GETNSID | RTM_DELNSID => { let err = "invalid nsid message"; @@ -365,6 +381,8 @@ pub enum RouteNetlinkMessage { NewTrafficChain(TcMessage), DelTrafficChain(TcMessage), GetTrafficChain(TcMessage), + NewStats(StatsMessage), + GetStats(StatsMessage), NewNsId(NsidMessage), DelNsId(NsidMessage), GetNsId(NsidMessage), @@ -522,6 +540,14 @@ impl RouteNetlinkMessage { matches!(self, RouteNetlinkMessage::DelRule(_)) } + pub fn is_new_stats(&self) -> bool { + matches!(self, RouteNetlinkMessage::NewStats(_)) + } + + pub fn is_get_stats(&self) -> bool { + matches!(self, RouteNetlinkMessage::GetStats(_)) + } + pub fn message_type(&self) -> u16 { match self { Self::NewLink(_) => RTM_NEWLINK, @@ -561,6 +587,8 @@ impl RouteNetlinkMessage { Self::GetNsId(_) => RTM_GETNSID, Self::NewNsId(_) => RTM_NEWNSID, Self::DelNsId(_) => RTM_DELNSID, + Self::NewStats(_) => RTM_NEWSTATS, + Self::GetStats(_) => RTM_GETSTATS, Self::GetRule(_) => RTM_GETRULE, Self::NewRule(_) => RTM_NEWRULE, Self::DelRule(_) => RTM_DELRULE, @@ -613,6 +641,10 @@ impl Emitable for RouteNetlinkMessage { | Self::DelNsId(ref msg) | Self::GetNsId(ref msg) => msg.buffer_len(), + Self::NewStats(ref msg) | Self::GetStats(ref msg) => { + msg.buffer_len() + } + Self::NewRule(ref msg) | Self::DelRule(ref msg) | Self::GetRule(ref msg) => msg.buffer_len(), @@ -667,6 +699,10 @@ impl Emitable for RouteNetlinkMessage { | Self::DelNsId(ref msg) | Self::GetNsId(ref msg) => msg.emit(buffer), + Self::NewStats(ref msg) | Self::GetStats(ref msg) => { + msg.emit(buffer) + } + Self::NewRule(ref msg) | Self::DelRule(ref msg) | Self::GetRule(ref msg) => msg.emit(buffer), diff --git a/src/stats/attribute.rs b/src/stats/attribute.rs new file mode 100644 index 00000000..9f039425 --- /dev/null +++ b/src/stats/attribute.rs @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + DecodeError, DefaultNla, Emitable, ErrorContext, Nla, NlaBuffer, Parseable, +}; + +use super::xstats::{ + parse_offload_xstats_inner, AfSpecStats, LinkXstatGroup, OffloadXstat, + VecLinkXstats, +}; +use crate::link::{Stats64, Stats64Buffer}; + +const IFLA_STATS_LINK_64: u16 = 1; +const IFLA_STATS_LINK_XSTATS: u16 = 2; +const IFLA_STATS_LINK_XSTATS_SLAVE: u16 = 3; +const IFLA_STATS_LINK_OFFLOAD_XSTATS: u16 = 4; +const IFLA_STATS_AF_SPEC: u16 = 5; + +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub enum StatsAttribute { + Link64(Stats64), + LinkXstats(Vec), + LinkXstatsPort(Vec), + LinkOffloadXstats(Vec), + AfSpec(AfSpecStats), + Other(DefaultNla), +} + +impl Nla for StatsAttribute { + fn value_len(&self) -> usize { + match self { + Self::Link64(v) => v.buffer_len(), + Self::LinkXstats(v) | Self::LinkXstatsPort(v) => { + v.as_slice().buffer_len() + } + Self::LinkOffloadXstats(v) => v.as_slice().buffer_len(), + Self::AfSpec(v) => v.buffer_len(), + Self::Other(attr) => attr.value_len(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + Self::Link64(v) => v.emit(buffer), + Self::LinkXstats(v) | Self::LinkXstatsPort(v) => { + v.as_slice().emit(buffer) + } + Self::LinkOffloadXstats(v) => v.as_slice().emit(buffer), + Self::AfSpec(v) => v.emit(buffer), + Self::Other(attr) => attr.emit_value(buffer), + } + } + + fn kind(&self) -> u16 { + match self { + Self::Link64(_) => IFLA_STATS_LINK_64, + Self::LinkXstats(_) => IFLA_STATS_LINK_XSTATS, + Self::LinkXstatsPort(_) => IFLA_STATS_LINK_XSTATS_SLAVE, + Self::LinkOffloadXstats(_) => IFLA_STATS_LINK_OFFLOAD_XSTATS, + Self::AfSpec(_) => IFLA_STATS_AF_SPEC, + Self::Other(attr) => attr.kind(), + } + } +} + +impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> + for StatsAttribute +{ + fn parse(buf: &NlaBuffer<&'a T>) -> Result { + let payload = buf.value(); + Ok(match buf.kind() { + IFLA_STATS_LINK_64 => Self::Link64( + Stats64::parse(&Stats64Buffer::new(payload)) + .context("invalid IFLA_STATS_LINK_64 value")?, + ), + IFLA_STATS_LINK_XSTATS => { + let err = "invalid IFLA_STATS_LINK_XSTATS value"; + Self::LinkXstats( + VecLinkXstats::parse(&NlaBuffer::new(payload)) + .context(err)? + .0, + ) + } + IFLA_STATS_LINK_XSTATS_SLAVE => { + let err = "invalid IFLA_STATS_LINK_XSTATS_SLAVE value"; + Self::LinkXstatsPort( + VecLinkXstats::parse(&NlaBuffer::new(payload)) + .context(err)? + .0, + ) + } + IFLA_STATS_LINK_OFFLOAD_XSTATS => { + let err = "invalid IFLA_STATS_LINK_OFFLOAD_XSTATS value"; + Self::LinkOffloadXstats( + parse_offload_xstats_inner(payload).context(err)?, + ) + } + IFLA_STATS_AF_SPEC => Self::AfSpec(AfSpecStats::parse(payload)), + kind => Self::Other( + DefaultNla::parse(buf) + .context(format!("unknown IFLA_STATS type {kind}"))?, + ), + }) + } +} diff --git a/src/stats/header.rs b/src/stats/header.rs new file mode 100644 index 00000000..0e8e4069 --- /dev/null +++ b/src/stats/header.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + DecodeError, Emitable, NlaBuffer, NlasIterator, Parseable, +}; + +use crate::AddressFamily; + +const STATS_HEADER_LEN: usize = 12; + +buffer!(StatsMessageBuffer(STATS_HEADER_LEN) { + family: (u8, 0), + pad1: (u8, 1), + pad2: (u16, 2..4), + ifindex: (u32, 4..8), + filter_mask: (u32, 8..STATS_HEADER_LEN), + payload: (slice, STATS_HEADER_LEN..), +}); + +impl<'a, T: AsRef<[u8]> + ?Sized> StatsMessageBuffer<&'a T> { + pub fn attributes( + &self, + ) -> impl Iterator, DecodeError>> { + NlasIterator::new(self.payload()) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct StatsHeader { + pub family: AddressFamily, + pub ifindex: u32, + pub filter_mask: u32, +} + +impl Emitable for StatsHeader { + fn buffer_len(&self) -> usize { + STATS_HEADER_LEN + } + + fn emit(&self, buffer: &mut [u8]) { + let mut packet = StatsMessageBuffer::new(buffer); + packet.set_family(self.family.into()); + packet.set_pad1(0); + packet.set_pad2(0); + packet.set_ifindex(self.ifindex); + packet.set_filter_mask(self.filter_mask); + } +} + +impl> Parseable> for StatsHeader { + fn parse(buf: &StatsMessageBuffer) -> Result { + Ok(StatsHeader { + family: buf.family().into(), + ifindex: buf.ifindex(), + filter_mask: buf.filter_mask(), + }) + } +} diff --git a/src/stats/message.rs b/src/stats/message.rs new file mode 100644 index 00000000..c7284c14 --- /dev/null +++ b/src/stats/message.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{DecodeError, Emitable, ErrorContext, Parseable}; + +use crate::stats::{StatsAttribute, StatsHeader, StatsMessageBuffer}; + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +#[non_exhaustive] +pub struct StatsMessage { + pub header: StatsHeader, + pub attributes: Vec, +} + +impl<'a, T: AsRef<[u8]> + 'a> Parseable> + for StatsMessage +{ + fn parse(buf: &StatsMessageBuffer<&'a T>) -> Result { + Ok(Self { + header: StatsHeader::parse(buf) + .context("failed to parse stats message header")?, + attributes: Vec::::parse(buf) + .context("failed to parse stats message NLAs")?, + }) + } +} + +impl<'a, T: AsRef<[u8]> + 'a> Parseable> + for Vec +{ + fn parse(buf: &StatsMessageBuffer<&'a T>) -> Result { + let mut attributes = vec![]; + for nla_buf in buf.attributes() { + attributes.push(StatsAttribute::parse(&nla_buf?)?); + } + Ok(attributes) + } +} + +impl Emitable for StatsMessage { + fn buffer_len(&self) -> usize { + self.header.buffer_len() + self.attributes.as_slice().buffer_len() + } + + fn emit(&self, buffer: &mut [u8]) { + self.header.emit(buffer); + self.attributes + .as_slice() + .emit(&mut buffer[self.header.buffer_len()..]); + } +} diff --git a/src/stats/mod.rs b/src/stats/mod.rs new file mode 100644 index 00000000..0dd0b7ac --- /dev/null +++ b/src/stats/mod.rs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +mod attribute; +mod header; +mod message; +mod xstats; + +#[cfg(test)] +mod tests; + +pub use self::{ + attribute::StatsAttribute, + header::{StatsHeader, StatsMessageBuffer}, + message::StatsMessage, + xstats::{ + AfSpecStatEntry, AfSpecStats, Bond3adStats, BondXstat, + BridgeMcastStats, BridgeStpXstats, BridgeVlanXstats, BridgeXstat, + HwSInfo, HwStats64, LinkXstatGroup, OffloadXstat, + }, +}; diff --git a/src/stats/tests.rs b/src/stats/tests.rs new file mode 100644 index 00000000..d508c4d2 --- /dev/null +++ b/src/stats/tests.rs @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + Emitable, NetlinkMessage, NetlinkPayload, Nla, NlaBuffer, NlasIterator, + Parseable, +}; + +use crate::{ + stats::{ + BridgeMcastStats, BridgeStpXstats, BridgeVlanXstats, BridgeXstat, + HwStats64, LinkXstatGroup, OffloadXstat, StatsAttribute, StatsHeader, + StatsMessage, StatsMessageBuffer, + }, + RouteNetlinkMessage, +}; + +// --------------------------------------------------------------------------- +// nlmon capture: ip stats show dev br-8fd093ec54e9 group xstats +// -> RTM_GETSTATS request, kernel responds with RTM_NEWSTATS +// Expected: bridge LINK_XSTATS with VLAN + MCAST data +// --------------------------------------------------------------------------- +#[test] +fn test_bridge_xstats_from_nlmon() { + let raw: Vec = vec![ + 0x44, 0x01, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x28, 0x58, 0x32, 0x6a, + 0x23, 0x76, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x28, 0x01, 0x02, 0x00, 0x24, 0x01, 0x01, 0x00, + 0x2c, 0x00, 0x01, 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, 0x00, 0x00, + 0x01, 0x00, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf4, 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, 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, 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, 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, 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, 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, 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, + 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, + ]; + + let msg = NetlinkMessage::::deserialize(&raw) + .expect("failed to deserialize bridge xstats"); + + let stats = match msg.payload { + NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewStats(s)) => s, + _ => panic!("expected NewStats"), + }; + + assert_eq!(stats.header.ifindex, 3); + assert_eq!(stats.header.filter_mask, 2); + assert_eq!(stats.attributes.len(), 1); + + let attr = &stats.attributes[0]; + match attr { + StatsAttribute::LinkXstats(groups) => { + assert_eq!(groups.len(), 1); + match &groups[0] { + LinkXstatGroup::Bridge(entries) => { + assert_eq!(entries.len(), 2); + // First entry should be VLAN, second should be MCAST + match &entries[0] { + BridgeXstat::Vlan(vlan) => { + assert_eq!(vlan.vid, 1); + assert_eq!(vlan.flags, 0x27); + } + other => panic!("expected Vlan, got {:?}", other), + } + match &entries[1] { + BridgeXstat::Mcast(mcast) => { + // All zero bridge mcast stats + assert_eq!(mcast.igmp_v1queries_rx, 0); + assert_eq!(mcast.igmp_v1queries_tx, 0); + } + other => panic!("expected Mcast, got {:?}", other), + } + } + other => panic!("expected Bridge, got {:?}", other), + } + } + other => panic!("expected LinkXstats, got {:?}", other), + } +} + +// --------------------------------------------------------------------------- +// nlmon capture: ip stats show dev enp2s0 group offload +// -> RTM_NEWSTATS with IFLA_STATS_LINK_OFFLOAD_XSTATS +// Expected: HW_S_INFO containing L3_STATS (both request/used = 0) +// --------------------------------------------------------------------------- +#[test] +fn test_offload_xstats_from_nlmon() { + let raw: Vec = vec![ + 0x38, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x29, 0x58, 0x32, 0x6a, + 0x25, 0x76, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0x18, 0x00, 0x02, 0x80, + 0x14, 0x00, 0x03, 0x80, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + let msg = NetlinkMessage::::deserialize(&raw) + .expect("failed to deserialize offload xstats"); + + let stats = match msg.payload { + NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewStats(s)) => s, + _ => panic!("expected NewStats"), + }; + + assert_eq!(stats.header.ifindex, 2); + assert_eq!(stats.header.filter_mask, 8); + assert_eq!(stats.attributes.len(), 1); + + match &stats.attributes[0] { + StatsAttribute::LinkOffloadXstats(entries) => { + assert_eq!(entries.len(), 1); + match &entries[0] { + OffloadXstat::HwSInfo(info) => { + assert_eq!(info.request, Some(0)); + assert_eq!(info.used, Some(0)); + } + other => panic!("expected HwSInfo, got {:?}", other), + } + } + other => panic!("expected LinkOffloadXstats, got {:?}", other), + } +} + +#[test] +fn test_offload_xstats_parse_inner_nlas() { + // Manually verify the NLA structure of offload xstats. + // Raw offload xstats NLA payload from nlmon capture + let payload: Vec = vec![ + 0x18, 0x00, 0x02, 0x80, 0x14, 0x00, 0x03, 0x80, 0x05, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + let entries = NlasIterator::new(&payload) + .filter_map(|nla| nla.ok()) + .collect::>(); + + assert_eq!(entries.len(), 1); + // The first NLA has type 0x8002 = NLA_F_NESTED | + // IFLA_OFFLOAD_XSTATS_HW_S_INFO + assert_eq!(entries[0].kind() & !0x8000, 2); + // L3_STATS (type 3) is nested inside HW_S_INFO + let inner = NlasIterator::new(entries[0].value()) + .filter_map(|nla| nla.ok()) + .collect::>(); + assert_eq!(inner.len(), 1); + assert_eq!(inner[0].kind() & !0x8000, 3); // L3_STATS + // Inside L3_STATS, we have request and used NLAs + let l3_inner = NlasIterator::new(inner[0].value()) + .filter_map(|nla| nla.ok()) + .collect::>(); + assert_eq!(l3_inner.len(), 2); + assert_eq!(l3_inner[0].kind() & !0x8000, 1); // request + assert_eq!(l3_inner[1].kind() & !0x8000, 2); // used +} + +// --------------------------------------------------------------------------- +// nlmon capture: ip stats show dev lo +// -> RTM_NEWSTATS with all filter_mask bits set (0x1f) +// Expected: LINK_64 + LINK_OFFLOAD_XSTATS + AF_SPEC attributes +// --------------------------------------------------------------------------- +#[test] +fn test_combined_stats_from_nlmon() { + // nlmon capture: ip stats show dev lo (all filter_mask bits set) + let raw: Vec = vec![ + 0x08, 0x01, 0x00, 0x00, 0x5c, 0x00, 0x02, 0x00, 0x2b, 0x58, 0x32, 0x6a, + 0x2a, 0x76, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x1f, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x01, 0x00, 0x81, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xca, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x30, 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, 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, + 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, 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, 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, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0x18, 0x00, 0x02, 0x80, + 0x14, 0x00, 0x03, 0x80, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x05, 0x00, + ]; + assert_eq!(raw.len(), 264); + assert_eq!(raw.len(), 264); + + let msg = NetlinkMessage::::deserialize(&raw) + .expect("failed to deserialize combined stats"); + + let stats = match msg.payload { + NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewStats(s)) => s, + _ => panic!("expected NewStats"), + }; + + assert_eq!(stats.header.ifindex, 1); + assert_eq!(stats.header.filter_mask, 0x1f); + // Should have 3 attributes: LINK_64, LINK_OFFLOAD_XSTATS, AF_SPEC + assert_eq!(stats.attributes.len(), 3); + + // First attribute: LINK_64 + match &stats.attributes[0] { + StatsAttribute::Link64(s64) => { + assert_eq!(s64.rx_packets, 0x81); + assert_eq!(s64.tx_packets, 0x81); + assert_eq!(s64.rx_bytes, 0x30ca); + assert_eq!(s64.tx_bytes, 0x30ca); + } + other => panic!("expected Link64, got {:?}", other), + } + + // Second attribute: LINK_OFFLOAD_XSTATS + match &stats.attributes[1] { + StatsAttribute::LinkOffloadXstats(entries) => { + assert_eq!(entries.len(), 1); + match &entries[0] { + OffloadXstat::HwSInfo(info) => { + assert_eq!(info.request, Some(0)); + assert_eq!(info.used, Some(0)); + } + other => panic!("expected HwSInfo, got {:?}", other), + } + } + other => panic!("expected LinkOffloadXstats, got {:?}", other), + } + + // Third attribute: AF_SPEC (empty for lo) + match &stats.attributes[2] { + StatsAttribute::AfSpec(spec) => { + assert_eq!(spec.0.len(), 0); + } + other => panic!("expected AfSpec, got {:?}", other), + } +} + +// --------------------------------------------------------------------------- +// Existing tests (kept for backward compatibility) +// --------------------------------------------------------------------------- + +#[test] +fn test_bridge_xstats_vlan_roundtrip() { + let vlan = BridgeVlanXstats { + vid: 1, + flags: 0x27, + ..Default::default() + }; + let len = 4 + vlan.value_len(); + let mut buf = vec![0u8; len]; + buf[0..2].copy_from_slice(&(len as u16).to_ne_bytes()); + buf[2..4].copy_from_slice(&vlan.kind().to_ne_bytes()); + vlan.emit_value(&mut buf[4..]); + + let nla = NlaBuffer::new_checked(&buf).unwrap(); + assert_eq!(nla.kind(), 1); + let parsed_val = BridgeVlanXstats::parse(nla.value()).unwrap(); + assert_eq!(parsed_val.vid, 1); + assert_eq!(parsed_val.flags, 0x27); + assert_eq!(parsed_val.rx_bytes, 0); +} + +#[test] +fn test_bridge_xstats_construct_roundtrip() { + let mcast = BridgeMcastStats::default(); + let stp = BridgeStpXstats::default(); + let vlan = BridgeVlanXstats { + vid: 1, + flags: 0x27, + ..Default::default() + }; + + let groups = vec![LinkXstatGroup::Bridge(vec![ + BridgeXstat::Vlan(vlan), + BridgeXstat::Mcast(mcast), + BridgeXstat::Stp(stp), + ])]; + + let msg = StatsMessage { + header: StatsHeader { + family: 0u8.into(), + ifindex: 3, + filter_mask: 2, + }, + attributes: vec![StatsAttribute::LinkXstats(groups)], + }; + + let len = msg.buffer_len(); + let mut buf = vec![0; len]; + msg.emit(&mut buf); + + let parsed = + StatsMessage::parse(&StatsMessageBuffer::new_checked(&buf).unwrap()) + .expect("failed to parse stats message"); + + assert_eq!(parsed.header, msg.header); + assert_eq!(parsed.attributes, msg.attributes); +} + +#[test] +fn test_offload_xstats_hw_stats64_roundtrip() { + let hw = HwStats64 { + rx_packets: 0x12345678, + rx_errors: 1, + multicast: 42, + ..Default::default() + }; + + let len = hw.buffer_len(); + let mut buf = vec![0; len]; + hw.emit(&mut buf); + + let parsed = HwStats64::parse(&buf).unwrap(); + assert_eq!(parsed.rx_packets, 0x12345678); + assert_eq!(parsed.rx_errors, 1); + assert_eq!(parsed.multicast, 42); +} + +#[test] +fn test_stats_header_roundtrip() { + let header = StatsHeader { + family: 0u8.into(), + ifindex: 42, + filter_mask: 6, + }; + + let len = header.buffer_len(); + let mut buf = vec![0; len]; + header.emit(&mut buf); + + let parsed = StatsHeader::parse(&StatsMessageBuffer::new(&buf)) + .expect("failed to parse stats header"); + assert_eq!(parsed, header); +} diff --git a/src/stats/xstats.rs b/src/stats/xstats.rs new file mode 100644 index 00000000..d398fc6e --- /dev/null +++ b/src/stats/xstats.rs @@ -0,0 +1,760 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + DecodeError, Emitable, ErrorContext, Nla, NlaBuffer, NlasIterator, + Parseable, NLA_F_NESTED, +}; + +const LINK_XSTATS_TYPE_BRIDGE: u16 = 1; +const LINK_XSTATS_TYPE_BOND: u16 = 2; + +const BRIDGE_XSTATS_VLAN: u16 = 1; +const BRIDGE_XSTATS_MCAST: u16 = 2; +const BRIDGE_XSTATS_STP: u16 = 4; + +const BOND_XSTATS_3AD: u16 = 1; + +const IFLA_OFFLOAD_XSTATS_CPU_HIT: u16 = 1; +const IFLA_OFFLOAD_XSTATS_HW_S_INFO: u16 = 2; +const IFLA_OFFLOAD_XSTATS_L3_STATS: u16 = 3; + +// --------------------------------------------------------------------------- +// Link xstats (inside IFLA_STATS_LINK_XSTATS / _PORT) +// --------------------------------------------------------------------------- + +/// A fully parsed entry inside `IFLA_STATS_LINK_XSTATS` or +/// `IFLA_STATS_LINK_XSTATS_PORT`. The inner data is parsed at +/// construction time. +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub enum LinkXstatGroup { + Bridge(Vec), + Bond(Vec), + Other(u16, Vec), +} + +impl Nla for LinkXstatGroup { + fn value_len(&self) -> usize { + match self { + Self::Bridge(v) => v.as_slice().buffer_len(), + Self::Bond(v) => v.as_slice().buffer_len(), + Self::Other(_, d) => d.len(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + Self::Bridge(v) => v.as_slice().emit(buffer), + Self::Bond(v) => v.as_slice().emit(buffer), + Self::Other(_, d) => buffer.copy_from_slice(d), + } + } + + fn kind(&self) -> u16 { + match self { + Self::Bridge(_) => LINK_XSTATS_TYPE_BRIDGE | NLA_F_NESTED, + Self::Bond(_) => LINK_XSTATS_TYPE_BOND | NLA_F_NESTED, + Self::Other(k, _) => *k, + } + } +} + +pub(crate) struct VecLinkXstats(pub(crate) Vec); + +impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> + for VecLinkXstats +{ + fn parse(buf: &NlaBuffer<&'a T>) -> Result { + let mut groups = Vec::new(); + for nla in NlasIterator::new(buf.into_inner()) { + let nla = nla?; + let kind = nla.kind() & !NLA_F_NESTED; + let val = nla.value(); + groups.push(match kind { + LINK_XSTATS_TYPE_BRIDGE => LinkXstatGroup::Bridge( + parse_bridge_xstats(val) + .context("invalid bridge xstats in link xstats")?, + ), + LINK_XSTATS_TYPE_BOND => LinkXstatGroup::Bond( + parse_bond_xstats(val) + .context("invalid bond xstats in link xstats")?, + ), + _ => LinkXstatGroup::Other(kind, val.to_vec()), + }); + } + Ok(Self(groups)) + } +} + +fn parse_bridge_xstats( + payload: &[u8], +) -> Result, DecodeError> { + let mut result = Vec::new(); + for nla in NlasIterator::new(payload) { + let nla = nla.context("invalid NLA in bridge xstats")?; + let kind = nla.kind() & !NLA_F_NESTED; + let val = nla.value(); + result.push(match kind { + BRIDGE_XSTATS_MCAST => BridgeXstat::Mcast( + BridgeMcastStats::parse(val) + .ok_or(DecodeError::from("invalid bridge mcast stats"))?, + ), + BRIDGE_XSTATS_STP => BridgeXstat::Stp( + BridgeStpXstats::parse(val) + .ok_or(DecodeError::from("invalid bridge stp stats"))?, + ), + BRIDGE_XSTATS_VLAN => BridgeXstat::Vlan( + BridgeVlanXstats::parse(val) + .ok_or(DecodeError::from("invalid bridge vlan stats"))?, + ), + _ => BridgeXstat::Other(kind, val.to_vec()), + }); + } + Ok(result) +} + +fn parse_bond_xstats(payload: &[u8]) -> Result, DecodeError> { + let mut result = Vec::new(); + for nla in NlasIterator::new(payload) { + let nla = nla.context("invalid NLA in bond xstats")?; + let kind = nla.kind() & !NLA_F_NESTED; + let val = nla.value(); + result.push(match kind { + BOND_XSTATS_3AD => BondXstat::Threead(Bond3adStats::parse(val)), + _ => BondXstat::Other(kind, val.to_vec()), + }); + } + Ok(result) +} + +// --------------------------------------------------------------------------- +// Bridge xstats types +// --------------------------------------------------------------------------- + +/// Parsed bridge xstat value. +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub enum BridgeXstat { + Mcast(BridgeMcastStats), + Stp(BridgeStpXstats), + Vlan(BridgeVlanXstats), + Other(u16, Vec), +} + +impl Nla for BridgeXstat { + fn value_len(&self) -> usize { + match self { + Self::Mcast(v) => v.buffer_len(), + Self::Stp(v) => v.buffer_len(), + Self::Vlan(_) => 40, // raw struct bytes, no NLA wrapper + Self::Other(_, d) => d.len(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + Self::Mcast(v) => v.emit(buffer), + Self::Stp(v) => v.emit(buffer), + Self::Vlan(v) => v.emit_value(buffer), + Self::Other(_, d) => buffer.copy_from_slice(d), + } + } + + fn kind(&self) -> u16 { + match self { + Self::Mcast(_) => BRIDGE_XSTATS_MCAST, + Self::Stp(_) => BRIDGE_XSTATS_STP, + Self::Vlan(_) => BRIDGE_XSTATS_VLAN, + Self::Other(k, _) => *k, + } + } +} + +/// Parsed `struct br_mcast_stats` (240 bytes). +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct BridgeMcastStats { + pub igmp_v1queries_rx: u64, + pub igmp_v1queries_tx: u64, + pub igmp_v2queries_rx: u64, + pub igmp_v2queries_tx: u64, + pub igmp_v3queries_rx: u64, + pub igmp_v3queries_tx: u64, + pub igmp_leaves_rx: u64, + pub igmp_leaves_tx: u64, + pub igmp_v1reports_rx: u64, + pub igmp_v1reports_tx: u64, + pub igmp_v2reports_rx: u64, + pub igmp_v2reports_tx: u64, + pub igmp_v3reports_rx: u64, + pub igmp_v3reports_tx: u64, + pub igmp_parse_errors: u64, + pub mld_v1queries_rx: u64, + pub mld_v1queries_tx: u64, + pub mld_v2queries_rx: u64, + pub mld_v2queries_tx: u64, + pub mld_leaves_rx: u64, + pub mld_leaves_tx: u64, + pub mld_v1reports_rx: u64, + pub mld_v1reports_tx: u64, + pub mld_v2reports_rx: u64, + pub mld_v2reports_tx: u64, + pub mld_parse_errors: u64, + pub mcast_bytes_rx: u64, + pub mcast_bytes_tx: u64, + pub mcast_packets_rx: u64, + pub mcast_packets_tx: u64, +} + +impl BridgeMcastStats { + pub fn parse(data: &[u8]) -> Option { + if data.len() < 240 { + return None; + } + fn le64(d: &[u8]) -> u64 { + let mut b = [0u8; 8]; + b.copy_from_slice(&d[..8]); + u64::from_ne_bytes(b) + } + Some(Self { + igmp_v1queries_rx: le64(&data[0..8]), + igmp_v1queries_tx: le64(&data[8..16]), + igmp_v2queries_rx: le64(&data[16..24]), + igmp_v2queries_tx: le64(&data[24..32]), + igmp_v3queries_rx: le64(&data[32..40]), + igmp_v3queries_tx: le64(&data[40..48]), + igmp_leaves_rx: le64(&data[48..56]), + igmp_leaves_tx: le64(&data[56..64]), + igmp_v1reports_rx: le64(&data[64..72]), + igmp_v1reports_tx: le64(&data[72..80]), + igmp_v2reports_rx: le64(&data[80..88]), + igmp_v2reports_tx: le64(&data[88..96]), + igmp_v3reports_rx: le64(&data[96..104]), + igmp_v3reports_tx: le64(&data[104..112]), + igmp_parse_errors: le64(&data[112..120]), + mld_v1queries_rx: le64(&data[120..128]), + mld_v1queries_tx: le64(&data[128..136]), + mld_v2queries_rx: le64(&data[136..144]), + mld_v2queries_tx: le64(&data[144..152]), + mld_leaves_rx: le64(&data[152..160]), + mld_leaves_tx: le64(&data[160..168]), + mld_v1reports_rx: le64(&data[168..176]), + mld_v1reports_tx: le64(&data[176..184]), + mld_v2reports_rx: le64(&data[184..192]), + mld_v2reports_tx: le64(&data[192..200]), + mld_parse_errors: le64(&data[200..208]), + mcast_bytes_rx: le64(&data[208..216]), + mcast_bytes_tx: le64(&data[216..224]), + mcast_packets_rx: le64(&data[224..232]), + mcast_packets_tx: le64(&data[232..240]), + }) + } +} + +impl Emitable for BridgeMcastStats { + fn buffer_len(&self) -> usize { + 240 + } + + fn emit(&self, buffer: &mut [u8]) { + fn emit_u64(buf: &mut [u8], val: u64) { + buf.copy_from_slice(&val.to_ne_bytes()); + } + emit_u64(&mut buffer[0..8], self.igmp_v1queries_rx); + emit_u64(&mut buffer[8..16], self.igmp_v1queries_tx); + emit_u64(&mut buffer[16..24], self.igmp_v2queries_rx); + emit_u64(&mut buffer[24..32], self.igmp_v2queries_tx); + emit_u64(&mut buffer[32..40], self.igmp_v3queries_rx); + emit_u64(&mut buffer[40..48], self.igmp_v3queries_tx); + emit_u64(&mut buffer[48..56], self.igmp_leaves_rx); + emit_u64(&mut buffer[56..64], self.igmp_leaves_tx); + emit_u64(&mut buffer[64..72], self.igmp_v1reports_rx); + emit_u64(&mut buffer[72..80], self.igmp_v1reports_tx); + emit_u64(&mut buffer[80..88], self.igmp_v2reports_rx); + emit_u64(&mut buffer[88..96], self.igmp_v2reports_tx); + emit_u64(&mut buffer[96..104], self.igmp_v3reports_rx); + emit_u64(&mut buffer[104..112], self.igmp_v3reports_tx); + emit_u64(&mut buffer[112..120], self.igmp_parse_errors); + emit_u64(&mut buffer[120..128], self.mld_v1queries_rx); + emit_u64(&mut buffer[128..136], self.mld_v1queries_tx); + emit_u64(&mut buffer[136..144], self.mld_v2queries_rx); + emit_u64(&mut buffer[144..152], self.mld_v2queries_tx); + emit_u64(&mut buffer[152..160], self.mld_leaves_rx); + emit_u64(&mut buffer[160..168], self.mld_leaves_tx); + emit_u64(&mut buffer[168..176], self.mld_v1reports_rx); + emit_u64(&mut buffer[176..184], self.mld_v1reports_tx); + emit_u64(&mut buffer[184..192], self.mld_v2reports_rx); + emit_u64(&mut buffer[192..200], self.mld_v2reports_tx); + emit_u64(&mut buffer[200..208], self.mld_parse_errors); + emit_u64(&mut buffer[208..216], self.mcast_bytes_rx); + emit_u64(&mut buffer[216..224], self.mcast_bytes_tx); + emit_u64(&mut buffer[224..232], self.mcast_packets_rx); + emit_u64(&mut buffer[232..240], self.mcast_packets_tx); + } +} + +/// Parsed `struct bridge_stp_xstats` (48 bytes). +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct BridgeStpXstats { + pub transition_blk: u64, + pub transition_fwd: u64, + pub rx_bpdu: u64, + pub tx_bpdu: u64, + pub rx_tcn: u64, + pub tx_tcn: u64, +} + +impl BridgeStpXstats { + pub fn parse(data: &[u8]) -> Option { + if data.len() < 48 { + return None; + } + fn le64(d: &[u8]) -> u64 { + let mut b = [0u8; 8]; + b.copy_from_slice(&d[..8]); + u64::from_ne_bytes(b) + } + Some(Self { + transition_blk: le64(&data[0..8]), + transition_fwd: le64(&data[8..16]), + rx_bpdu: le64(&data[16..24]), + tx_bpdu: le64(&data[24..32]), + rx_tcn: le64(&data[32..40]), + tx_tcn: le64(&data[40..48]), + }) + } +} + +impl Emitable for BridgeStpXstats { + fn buffer_len(&self) -> usize { + 48 + } + + fn emit(&self, buffer: &mut [u8]) { + buffer[0..8].copy_from_slice(&self.transition_blk.to_ne_bytes()); + buffer[8..16].copy_from_slice(&self.transition_fwd.to_ne_bytes()); + buffer[16..24].copy_from_slice(&self.rx_bpdu.to_ne_bytes()); + buffer[24..32].copy_from_slice(&self.tx_bpdu.to_ne_bytes()); + buffer[32..40].copy_from_slice(&self.rx_tcn.to_ne_bytes()); + buffer[40..48].copy_from_slice(&self.tx_tcn.to_ne_bytes()); + } +} + +/// Parsed `struct bridge_vlan_xstats` (40 bytes). +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct BridgeVlanXstats { + pub rx_bytes: u64, + pub rx_packets: u64, + pub tx_bytes: u64, + pub tx_packets: u64, + pub vid: u16, + pub flags: u16, +} + +impl Nla for BridgeVlanXstats { + fn value_len(&self) -> usize { + 40 + } + + fn emit_value(&self, buffer: &mut [u8]) { + buffer[0..8].copy_from_slice(&self.rx_bytes.to_ne_bytes()); + buffer[8..16].copy_from_slice(&self.rx_packets.to_ne_bytes()); + buffer[16..24].copy_from_slice(&self.tx_bytes.to_ne_bytes()); + buffer[24..32].copy_from_slice(&self.tx_packets.to_ne_bytes()); + buffer[32..34].copy_from_slice(&self.vid.to_ne_bytes()); + buffer[34..36].copy_from_slice(&self.flags.to_ne_bytes()); + // pad2 = 0 (unused) + } + + fn kind(&self) -> u16 { + BRIDGE_XSTATS_VLAN + } +} + +impl BridgeVlanXstats { + pub fn parse(data: &[u8]) -> Option { + if data.len() < 40 { + return None; + } + fn le64(d: &[u8]) -> u64 { + let mut b = [0u8; 8]; + b.copy_from_slice(&d[..8]); + u64::from_ne_bytes(b) + } + Some(Self { + rx_bytes: le64(&data[0..8]), + rx_packets: le64(&data[8..16]), + tx_bytes: le64(&data[16..24]), + tx_packets: le64(&data[24..32]), + vid: u16::from_ne_bytes(data[32..34].try_into().unwrap()), + flags: u16::from_ne_bytes(data[34..36].try_into().unwrap()), + }) + } +} + +// --------------------------------------------------------------------------- +// Bond xstats types +// --------------------------------------------------------------------------- + +/// Parsed bond xstat value. +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub enum BondXstat { + Threead(Bond3adStats), + Other(u16, Vec), +} + +impl Nla for BondXstat { + fn value_len(&self) -> usize { + match self { + Self::Threead(v) => v.buffer_len(), + Self::Other(_, d) => d.len(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + Self::Threead(v) => v.emit(buffer), + Self::Other(_, d) => buffer.copy_from_slice(d), + } + } + + fn kind(&self) -> u16 { + match self { + Self::Threead(_) => BOND_XSTATS_3AD | NLA_F_NESTED, + Self::Other(k, _) => *k, + } + } +} + +/// Parsed `BOND_XSTATS_3AD` content. +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct Bond3adStats { + pub lacpdu_rx: Option, + pub lacpdu_tx: Option, + pub lacpdu_unknown_rx: Option, + pub lacpdu_illegal_rx: Option, + pub marker_rx: Option, + pub marker_tx: Option, + pub marker_resp_rx: Option, + pub marker_resp_tx: Option, + pub marker_unknown_rx: Option, +} + +impl Bond3adStats { + pub fn parse(payload: &[u8]) -> Self { + fn le64(d: &[u8]) -> u64 { + let mut b = [0u8; 8]; + b.copy_from_slice(&d[..8]); + u64::from_ne_bytes(b) + } + let mut stats = Bond3adStats::default(); + for nla in NlasIterator::new(payload).filter_map(|n| n.ok()) { + let val = nla.value(); + if val.len() < 8 { + continue; + } + let v = le64(val); + match nla.kind() & !NLA_F_NESTED { + 1 => stats.lacpdu_rx = Some(v), + 2 => stats.lacpdu_tx = Some(v), + 3 => stats.lacpdu_unknown_rx = Some(v), + 4 => stats.lacpdu_illegal_rx = Some(v), + 5 => stats.marker_rx = Some(v), + 6 => stats.marker_tx = Some(v), + 7 => stats.marker_resp_rx = Some(v), + 8 => stats.marker_resp_tx = Some(v), + 9 => stats.marker_unknown_rx = Some(v), + _ => {} + } + } + stats + } + + pub fn buffer_len(&self) -> usize { + let mut len = 0; + macro_rules! add { + ($f:ident, $k:expr) => { + if self.$f.is_some() { + len += 4 + 8; // NLA header + u64 + } + }; + } + add!(lacpdu_rx, 1); + add!(lacpdu_tx, 2); + add!(lacpdu_unknown_rx, 3); + add!(lacpdu_illegal_rx, 4); + add!(marker_rx, 5); + add!(marker_tx, 6); + add!(marker_resp_rx, 7); + add!(marker_resp_tx, 8); + add!(marker_unknown_rx, 9); + len + } + + pub fn emit(&self, buffer: &mut [u8]) { + let nla_len: u16 = 4 + 8; + let mut off = 0; + macro_rules! emit { + ($f:ident, $k:expr) => { + if let Some(v) = self.$f { + buffer[off..off + 2] + .copy_from_slice(&nla_len.to_ne_bytes()); + buffer[off + 2..off + 4] + .copy_from_slice(&($k as u16).to_ne_bytes()); + buffer[off + 4..off + 12].copy_from_slice(&v.to_ne_bytes()); + off += 12; + } + }; + } + emit!(lacpdu_rx, 1); + emit!(lacpdu_tx, 2); + emit!(lacpdu_unknown_rx, 3); + emit!(lacpdu_illegal_rx, 4); + emit!(marker_rx, 5); + emit!(marker_tx, 6); + emit!(marker_resp_rx, 7); + emit!(marker_resp_tx, 8); + emit!(marker_unknown_rx, 9); + let _ = off; + } +} + +// --------------------------------------------------------------------------- +// Offload xstats (inside IFLA_STATS_LINK_OFFLOAD_XSTATS) +// --------------------------------------------------------------------------- + +/// A fully parsed entry inside `IFLA_STATS_LINK_OFFLOAD_XSTATS`. +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub enum OffloadXstat { + CpuHit(HwStats64), + HwSInfo(HwSInfo), + L3Stats(HwStats64), + Other(u16, Vec), +} + +impl Nla for OffloadXstat { + fn value_len(&self) -> usize { + match self { + Self::CpuHit(v) | Self::L3Stats(v) => v.buffer_len(), + Self::HwSInfo(v) => v.buffer_len(), + Self::Other(_, d) => d.len(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + Self::CpuHit(v) | Self::L3Stats(v) => v.emit(buffer), + Self::HwSInfo(v) => v.emit(buffer), + Self::Other(_, d) => buffer.copy_from_slice(d), + } + } + + fn kind(&self) -> u16 { + match self { + Self::CpuHit(_) => IFLA_OFFLOAD_XSTATS_CPU_HIT, + Self::HwSInfo(_) => IFLA_OFFLOAD_XSTATS_HW_S_INFO | NLA_F_NESTED, + Self::L3Stats(_) => IFLA_OFFLOAD_XSTATS_L3_STATS, + Self::Other(k, _) => *k, + } + } +} + +pub(crate) fn parse_offload_xstats_inner( + payload: &[u8], +) -> Result, DecodeError> { + let mut result = Vec::new(); + for nla in NlasIterator::new(payload) { + let nla = nla.context("invalid NLA in offload xstats")?; + let kind = nla.kind() & !NLA_F_NESTED; + let val = nla.value(); + result.push(match kind { + IFLA_OFFLOAD_XSTATS_CPU_HIT => OffloadXstat::CpuHit( + HwStats64::parse(val) + .ok_or(DecodeError::from("invalid cpu hit stats"))?, + ), + IFLA_OFFLOAD_XSTATS_HW_S_INFO => { + OffloadXstat::HwSInfo(HwSInfo::parse(val)) + } + IFLA_OFFLOAD_XSTATS_L3_STATS => OffloadXstat::L3Stats( + HwStats64::parse(val) + .ok_or(DecodeError::from("invalid l3 stats"))?, + ), + _ => OffloadXstat::Other(kind, val.to_vec()), + }); + } + Ok(result) +} + +/// Parsed `struct rtnl_hw_stats64` (72 bytes). +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct HwStats64 { + pub rx_packets: u64, + pub tx_packets: u64, + pub rx_bytes: u64, + pub tx_bytes: u64, + pub rx_errors: u64, + pub tx_errors: u64, + pub rx_dropped: u64, + pub tx_dropped: u64, + pub multicast: u64, +} + +impl HwStats64 { + pub fn parse(data: &[u8]) -> Option { + if data.len() < 72 { + return None; + } + fn le64(d: &[u8]) -> u64 { + let mut b = [0u8; 8]; + b.copy_from_slice(&d[..8]); + u64::from_ne_bytes(b) + } + Some(Self { + rx_packets: le64(&data[0..8]), + tx_packets: le64(&data[8..16]), + rx_bytes: le64(&data[16..24]), + tx_bytes: le64(&data[24..32]), + rx_errors: le64(&data[32..40]), + tx_errors: le64(&data[40..48]), + rx_dropped: le64(&data[48..56]), + tx_dropped: le64(&data[56..64]), + multicast: le64(&data[64..72]), + }) + } +} + +impl Emitable for HwStats64 { + fn buffer_len(&self) -> usize { + 72 + } + + fn emit(&self, buffer: &mut [u8]) { + buffer[0..8].copy_from_slice(&self.rx_packets.to_ne_bytes()); + buffer[8..16].copy_from_slice(&self.tx_packets.to_ne_bytes()); + buffer[16..24].copy_from_slice(&self.rx_bytes.to_ne_bytes()); + buffer[24..32].copy_from_slice(&self.tx_bytes.to_ne_bytes()); + buffer[32..40].copy_from_slice(&self.rx_errors.to_ne_bytes()); + buffer[40..48].copy_from_slice(&self.tx_errors.to_ne_bytes()); + buffer[48..56].copy_from_slice(&self.rx_dropped.to_ne_bytes()); + buffer[56..64].copy_from_slice(&self.tx_dropped.to_ne_bytes()); + buffer[64..72].copy_from_slice(&self.multicast.to_ne_bytes()); + } +} + +/// Parsed HW stats info nest (`IFLA_OFFLOAD_XSTATS_HW_S_INFO`). +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct HwSInfo { + pub request: Option, + pub used: Option, +} + +impl HwSInfo { + pub fn parse(payload: &[u8]) -> Self { + let mut info = HwSInfo::default(); + for nla in NlasIterator::new(payload).filter_map(|n| n.ok()) { + let val = nla.value(); + match nla.kind() & !NLA_F_NESTED { + 1 if !val.is_empty() => info.request = Some(val[0]), + 2 if !val.is_empty() => info.used = Some(val[0]), + // L3_STATS is nested inside HW_S_INFO; descend to find + // request/used + IFLA_OFFLOAD_XSTATS_L3_STATS => { + for inner in NlasIterator::new(val).filter_map(|n| n.ok()) { + let iv = inner.value(); + match inner.kind() & !NLA_F_NESTED { + 1 if !iv.is_empty() => info.request = Some(iv[0]), + 2 if !iv.is_empty() => info.used = Some(iv[0]), + _ => {} + } + } + } + _ => {} + } + } + info + } + + pub fn buffer_len(&self) -> usize { + let mut len = 0; + if self.request.is_some() { + len += 4 + 1; // NLA header + u8 (+ padding to 4) + len += (4 - (len % 4)) % 4; + } + if self.used.is_some() { + len += 4 + 1; + len += (4 - (len % 4)) % 4; + } + len + } + + pub fn emit(&self, buffer: &mut [u8]) { + let mut off = 0; + if let Some(v) = self.request { + let padded = 4 + 4; // NLA header + 4 bytes (u8 + 3 padding) + buffer[off..off + 2] + .copy_from_slice(&(padded as u16).to_ne_bytes()); + buffer[off + 2..off + 4].copy_from_slice(&1u16.to_ne_bytes()); + buffer[off + 4] = v; + off += padded; + } + if let Some(v) = self.used { + let padded = 4 + 4; + buffer[off..off + 2] + .copy_from_slice(&(padded as u16).to_ne_bytes()); + buffer[off + 2..off + 4].copy_from_slice(&2u16.to_ne_bytes()); + buffer[off + 4] = v; + } + } +} + +// --------------------------------------------------------------------------- +// AF_SPEC stats (inside IFLA_STATS_AF_SPEC) +// --------------------------------------------------------------------------- + +/// Parsed content of IFLA_STATS_AF_SPEC. +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct AfSpecStats(pub Vec); + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct AfSpecStatEntry { + pub af_type: u16, + pub data: Vec, +} + +impl Nla for AfSpecStatEntry { + fn value_len(&self) -> usize { + self.data.len() + } + + fn emit_value(&self, buffer: &mut [u8]) { + buffer.copy_from_slice(&self.data); + } + + fn kind(&self) -> u16 { + self.af_type + } +} + +impl Emitable for AfSpecStats { + fn buffer_len(&self) -> usize { + self.0.as_slice().buffer_len() + } + + fn emit(&self, buffer: &mut [u8]) { + self.0.as_slice().emit(buffer); + } +} + +impl AfSpecStats { + pub fn parse(payload: &[u8]) -> Self { + let entries = NlasIterator::new(payload) + .filter_map(|nla| nla.ok()) + .map(|nla| AfSpecStatEntry { + af_type: nla.kind() & !NLA_F_NESTED, + data: nla.value().to_vec(), + }) + .collect(); + AfSpecStats(entries) + } +}