use winnow::{binary::le_u16, prelude::*, token::take};
use crate::netlink::{
parse::{FromNetlink, PResult, parse_string_from_bytes},
types::tc::TcMsg,
};
mod attr_ids {
pub const TCA_KIND: u16 = 1;
pub const TCA_OPTIONS: u16 = 2;
pub const TCA_STATS: u16 = 3;
pub const TCA_XSTATS: u16 = 4;
pub const TCA_STATS2: u16 = 7;
pub const TCA_CHAIN: u16 = 11;
pub const TCA_HW_OFFLOAD: u16 = 12;
pub const TCA_INGRESS_BLOCK: u16 = 13;
pub const TCA_EGRESS_BLOCK: u16 = 14;
}
mod stats2_ids {
pub const TCA_STATS_BASIC: u16 = 1;
pub const TCA_STATS_RATE_EST: u16 = 2;
pub const TCA_STATS_QUEUE: u16 = 3;
pub const TCA_STATS_APP: u16 = 4;
pub const TCA_STATS_BASIC_HW: u16 = 7;
pub const TCA_STATS_PKT64: u16 = 8;
}
#[derive(Debug, Clone, Default)]
pub struct TcMessage {
pub(crate) header: TcMsg,
pub(crate) kind: Option<String>,
pub(crate) options: Option<Vec<u8>>,
pub(crate) chain: Option<u32>,
pub(crate) hw_offload: Option<u8>,
pub(crate) ingress_block: Option<u32>,
pub(crate) egress_block: Option<u32>,
pub(crate) stats_basic: Option<TcStatsBasic>,
pub(crate) stats_queue: Option<TcStatsQueue>,
pub(crate) stats_rate_est: Option<TcStatsRateEst>,
pub(crate) xstats: Option<Vec<u8>>,
pub(crate) name: Option<String>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TcStatsBasic {
pub bytes: u64,
pub packets: u64,
}
impl TcStatsBasic {
pub fn delta(&self, previous: &Self) -> TcStatsBasic {
TcStatsBasic {
bytes: self.bytes.saturating_sub(previous.bytes),
packets: self.packets.saturating_sub(previous.packets),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TcStatsQueue {
pub qlen: u32,
pub backlog: u32,
pub drops: u32,
pub requeues: u32,
pub overlimits: u32,
}
impl TcStatsQueue {
pub fn delta(&self, previous: &Self) -> TcStatsQueue {
TcStatsQueue {
qlen: self.qlen, backlog: self.backlog, drops: self.drops.saturating_sub(previous.drops),
requeues: self.requeues.saturating_sub(previous.requeues),
overlimits: self.overlimits.saturating_sub(previous.overlimits),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TcStatsRateEst {
pub bps: u32,
pub pps: u32,
}
impl TcMessage {
pub fn new() -> Self {
Self::default()
}
pub fn ifindex(&self) -> u32 {
self.header.tcm_ifindex as u32
}
pub fn handle(&self) -> crate::TcHandle {
crate::TcHandle::from_raw(self.header.tcm_handle)
}
pub fn handle_raw(&self) -> u32 {
self.header.tcm_handle
}
pub fn parent(&self) -> crate::TcHandle {
crate::TcHandle::from_raw(self.header.tcm_parent)
}
pub fn parent_raw(&self) -> u32 {
self.header.tcm_parent
}
pub fn info(&self) -> u32 {
self.header.tcm_info
}
pub fn protocol(&self) -> u16 {
(self.header.tcm_info >> 16) as u16
}
pub fn priority(&self) -> u16 {
(self.header.tcm_info & 0xFFFF) as u16
}
pub fn kind(&self) -> Option<&str> {
self.kind.as_deref()
}
pub fn raw_options(&self) -> Option<&[u8]> {
self.options.as_deref()
}
pub fn chain(&self) -> Option<u32> {
self.chain
}
pub fn hw_offload(&self) -> Option<u8> {
self.hw_offload
}
pub fn ingress_block(&self) -> Option<u32> {
self.ingress_block
}
pub fn egress_block(&self) -> Option<u32> {
self.egress_block
}
pub fn stats_basic(&self) -> Option<&TcStatsBasic> {
self.stats_basic.as_ref()
}
pub fn stats_queue(&self) -> Option<&TcStatsQueue> {
self.stats_queue.as_ref()
}
pub fn stats_rate_est(&self) -> Option<&TcStatsRateEst> {
self.stats_rate_est.as_ref()
}
pub fn xstats(&self) -> Option<&[u8]> {
self.xstats.as_deref()
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn name_or<'a>(&'a self, fallback: &'a str) -> &'a str {
self.name.as_deref().unwrap_or(fallback)
}
pub fn bytes(&self) -> u64 {
self.stats_basic.map(|s| s.bytes).unwrap_or(0)
}
pub fn packets(&self) -> u64 {
self.stats_basic.map(|s| s.packets).unwrap_or(0)
}
pub fn drops(&self) -> u32 {
self.stats_queue.map(|s| s.drops).unwrap_or(0)
}
pub fn overlimits(&self) -> u32 {
self.stats_queue.map(|s| s.overlimits).unwrap_or(0)
}
pub fn requeues(&self) -> u32 {
self.stats_queue.map(|s| s.requeues).unwrap_or(0)
}
pub fn qlen(&self) -> u32 {
self.stats_queue.map(|s| s.qlen).unwrap_or(0)
}
pub fn backlog(&self) -> u32 {
self.stats_queue.map(|s| s.backlog).unwrap_or(0)
}
pub fn bps(&self) -> u32 {
self.stats_rate_est.map(|s| s.bps).unwrap_or(0)
}
pub fn pps(&self) -> u32 {
self.stats_rate_est.map(|s| s.pps).unwrap_or(0)
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn resolve_name(mut self) -> Self {
if let Ok(name) = crate::util::ifname::index_to_name(self.ifindex()) {
self.name = Some(name);
}
self
}
pub fn resolve_name_mut(&mut self) {
if let Ok(name) = crate::util::ifname::index_to_name(self.ifindex()) {
self.name = Some(name);
}
}
pub fn options(&self) -> Option<crate::netlink::tc_options::QdiscOptions> {
crate::netlink::tc_options::parse_qdisc_options(self)
}
#[inline]
pub fn is_netem(&self) -> bool {
self.kind() == Some("netem")
}
#[inline]
pub fn is_root(&self) -> bool {
self.header.tcm_parent == crate::netlink::types::tc::tc_handle::ROOT
}
#[inline]
pub fn is_ingress(&self) -> bool {
self.header.tcm_parent == crate::netlink::types::tc::tc_handle::INGRESS
|| self.kind() == Some("ingress")
}
#[inline]
pub fn is_clsact(&self) -> bool {
self.header.tcm_parent == crate::netlink::types::tc::tc_handle::CLSACT
|| self.kind() == Some("clsact")
}
#[inline]
pub fn is_class(&self) -> bool {
use crate::netlink::types::tc::tc_handle;
let parent = self.header.tcm_parent;
parent != tc_handle::ROOT
&& parent != tc_handle::INGRESS
&& parent != tc_handle::CLSACT
&& parent != tc_handle::UNSPEC
&& tc_handle::minor(self.header.tcm_handle) != 0
}
#[inline]
pub fn is_filter(&self) -> bool {
self.header.tcm_info != 0
}
#[inline]
pub fn filter_protocol(&self) -> Option<u16> {
if self.is_filter() {
Some((self.header.tcm_info & 0xFFFF) as u16)
} else {
None
}
}
#[inline]
pub fn filter_priority(&self) -> Option<u16> {
if self.is_filter() {
Some((self.header.tcm_info >> 16) as u16)
} else {
None
}
}
#[inline]
pub fn handle_str(&self) -> String {
crate::netlink::types::tc::tc_handle::format(self.header.tcm_handle)
}
#[inline]
pub fn parent_str(&self) -> String {
crate::netlink::types::tc::tc_handle::format(self.header.tcm_parent)
}
pub fn bpf_info(&self) -> Option<BpfInfo> {
if self.kind() != Some("bpf") {
return None;
}
let options = self.options.as_deref()?;
use crate::netlink::types::tc::filter::bpf;
let mut info = BpfInfo {
id: None,
name: None,
tag: None,
direct_action: false,
classid: None,
};
let mut pos = 0;
while pos + 4 <= options.len() {
let len = u16::from_ne_bytes([options[pos], options[pos + 1]]) as usize;
let attr_type = u16::from_ne_bytes([options[pos + 2], options[pos + 3]]) & 0x3FFF;
if len < 4 || pos + len > options.len() {
break;
}
let payload = &options[pos + 4..pos + len];
match attr_type {
bpf::TCA_BPF_ID if payload.len() >= 4 => {
info.id = Some(u32::from_ne_bytes([
payload[0], payload[1], payload[2], payload[3],
]));
}
bpf::TCA_BPF_NAME => {
let name = std::str::from_utf8(payload)
.ok()
.map(|s| s.trim_end_matches('\0').to_string());
info.name = name;
}
bpf::TCA_BPF_TAG if payload.len() >= 8 => {
let mut tag = [0u8; 8];
tag.copy_from_slice(&payload[..8]);
info.tag = Some(tag);
}
bpf::TCA_BPF_FLAGS if payload.len() >= 4 => {
let flags =
u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
info.direct_action = (flags & bpf::TCA_BPF_FLAG_ACT_DIRECT) != 0;
}
bpf::TCA_BPF_CLASSID if payload.len() >= 4 => {
info.classid = Some(u32::from_ne_bytes([
payload[0], payload[1], payload[2], payload[3],
]));
}
_ => {}
}
pos += (len + 3) & !3;
}
Some(info)
}
}
#[derive(Debug, Clone)]
pub struct BpfInfo {
pub id: Option<u32>,
pub name: Option<String>,
pub tag: Option<[u8; 8]>,
pub direct_action: bool,
pub classid: Option<u32>,
}
impl BpfInfo {
pub fn tag_hex(&self) -> Option<String> {
self.tag
.map(|t| t.iter().map(|b| format!("{b:02x}")).collect())
}
}
impl FromNetlink for TcMessage {
fn write_dump_header(buf: &mut Vec<u8>) {
let header = TcMsg::new();
buf.extend_from_slice(header.as_bytes());
}
fn parse(input: &mut &[u8]) -> PResult<Self> {
if input.len() < TcMsg::SIZE {
return Err(winnow::error::ErrMode::Cut(
winnow::error::ContextError::new(),
));
}
let header_bytes: &[u8] = take(TcMsg::SIZE).parse_next(input)?;
let header = *TcMsg::from_bytes(header_bytes)
.map_err(|_| winnow::error::ErrMode::Cut(winnow::error::ContextError::new()))?;
let mut msg = TcMessage {
header,
..Default::default()
};
while !input.is_empty() && input.len() >= 4 {
let len = le_u16.parse_next(input)? as usize;
let attr_type = le_u16.parse_next(input)?;
if len < 4 {
break;
}
let payload_len = len.saturating_sub(4);
if input.len() < payload_len {
break;
}
let attr_data: &[u8] = take(payload_len).parse_next(input)?;
let aligned = (len + 3) & !3;
let padding = aligned.saturating_sub(len);
if input.len() >= padding {
let _: &[u8] = take(padding).parse_next(input)?;
}
match attr_type & 0x3FFF {
attr_ids::TCA_KIND => {
msg.kind = Some(parse_string_from_bytes(attr_data));
}
attr_ids::TCA_OPTIONS => {
msg.options = Some(attr_data.to_vec());
}
attr_ids::TCA_XSTATS => {
msg.xstats = Some(attr_data.to_vec());
}
attr_ids::TCA_CHAIN if attr_data.len() >= 4 => {
msg.chain = Some(u32::from_ne_bytes(attr_data[..4].try_into().unwrap()));
}
attr_ids::TCA_HW_OFFLOAD if !attr_data.is_empty() => {
msg.hw_offload = Some(attr_data[0]);
}
attr_ids::TCA_INGRESS_BLOCK if attr_data.len() >= 4 => {
msg.ingress_block =
Some(u32::from_ne_bytes(attr_data[..4].try_into().unwrap()));
}
attr_ids::TCA_EGRESS_BLOCK if attr_data.len() >= 4 => {
msg.egress_block = Some(u32::from_ne_bytes(attr_data[..4].try_into().unwrap()));
}
attr_ids::TCA_STATS2 => {
parse_stats2(&mut msg, attr_data);
}
attr_ids::TCA_STATS => {
parse_legacy_stats(&mut msg, attr_data);
}
_ => {} }
}
Ok(msg)
}
}
fn parse_stats2(msg: &mut TcMessage, data: &[u8]) {
let mut input = data;
while !input.is_empty() && input.len() >= 4 {
let len = u16::from_ne_bytes(input[..2].try_into().unwrap()) as usize;
let attr_type = u16::from_ne_bytes(input[2..4].try_into().unwrap());
if len < 4 || input.len() < len {
break;
}
let payload = &input[4..len];
match attr_type & 0x3FFF {
stats2_ids::TCA_STATS_BASIC | stats2_ids::TCA_STATS_BASIC_HW
if payload.len() >= 12 => {
let bytes = u64::from_ne_bytes(payload[..8].try_into().unwrap());
let packets = u32::from_ne_bytes(payload[8..12].try_into().unwrap());
msg.stats_basic = Some(TcStatsBasic {
bytes,
packets: packets as u64,
});
}
stats2_ids::TCA_STATS_PKT64
if payload.len() >= 8 => {
let packets = u64::from_ne_bytes(payload[..8].try_into().unwrap());
if let Some(ref mut stats) = msg.stats_basic {
stats.packets = packets;
} else {
msg.stats_basic = Some(TcStatsBasic { bytes: 0, packets });
}
}
stats2_ids::TCA_STATS_QUEUE
if payload.len() >= 20 => {
msg.stats_queue = Some(TcStatsQueue {
qlen: u32::from_ne_bytes(payload[0..4].try_into().unwrap()),
backlog: u32::from_ne_bytes(payload[4..8].try_into().unwrap()),
drops: u32::from_ne_bytes(payload[8..12].try_into().unwrap()),
requeues: u32::from_ne_bytes(payload[12..16].try_into().unwrap()),
overlimits: u32::from_ne_bytes(payload[16..20].try_into().unwrap()),
});
}
stats2_ids::TCA_STATS_RATE_EST
if payload.len() >= 8 => {
msg.stats_rate_est = Some(TcStatsRateEst {
bps: u32::from_ne_bytes(payload[0..4].try_into().unwrap()),
pps: u32::from_ne_bytes(payload[4..8].try_into().unwrap()),
});
}
stats2_ids::TCA_STATS_APP
if msg.xstats.is_none() => {
msg.xstats = Some(payload.to_vec());
}
_ => {}
}
let aligned = (len + 3) & !3;
if input.len() <= aligned {
break;
}
input = &input[aligned..];
}
}
fn parse_legacy_stats(msg: &mut TcMessage, data: &[u8]) {
if data.len() >= 36 {
let bytes = u64::from_ne_bytes(data[0..8].try_into().unwrap());
let packets = u32::from_ne_bytes(data[8..12].try_into().unwrap());
let drops = u32::from_ne_bytes(data[12..16].try_into().unwrap());
let overlimits = u32::from_ne_bytes(data[16..20].try_into().unwrap());
let bps = u32::from_ne_bytes(data[20..24].try_into().unwrap());
let pps = u32::from_ne_bytes(data[24..28].try_into().unwrap());
let qlen = u32::from_ne_bytes(data[28..32].try_into().unwrap());
let backlog = u32::from_ne_bytes(data[32..36].try_into().unwrap());
msg.stats_basic = Some(TcStatsBasic {
bytes,
packets: packets as u64,
});
msg.stats_queue = Some(TcStatsQueue {
qlen,
backlog,
drops,
requeues: 0,
overlimits,
});
msg.stats_rate_est = Some(TcStatsRateEst { bps, pps });
}
}
pub type QdiscMessage = TcMessage;
pub type ClassMessage = TcMessage;
pub type FilterMessage = TcMessage;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tc_message_default() {
let msg = TcMessage::new();
assert_eq!(msg.ifindex(), 0);
assert_eq!(msg.handle(), crate::TcHandle::UNSPEC);
assert_eq!(msg.parent(), crate::TcHandle::UNSPEC);
assert!(msg.kind().is_none());
}
#[test]
fn test_filter_protocol_priority() {
let mut msg = TcMessage::new();
msg.header.tcm_info = (0x0800 << 16) | 100;
assert_eq!(msg.protocol(), 0x0800);
assert_eq!(msg.priority(), 100);
}
}