diff --git a/Cargo.toml b/Cargo.toml index a25aa39e..a346ddb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,5 +27,6 @@ netlink-packet-utils = { version = "0.5.2" } name = "dump_packet_links" [dev-dependencies] +hex = "0.4.3" netlink-sys = { version = "0.8.5" } pretty_assertions = "0.7.2" diff --git a/src/message.rs b/src/message.rs index 3529f07a..d1b20809 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT use anyhow::Context; -use netlink_packet_utils::{ - DecodeError, Emitable, Parseable, ParseableParametrized, -}; - use netlink_packet_core::{ NetlinkDeserializable, NetlinkHeader, NetlinkPayload, NetlinkSerializable, }; +use netlink_packet_utils::{ + DecodeError, Emitable, Parseable, ParseableParametrized, +}; +use crate::tc::{TcActionMessage, TcActionMessageBuffer}; use crate::{ address::{AddressHeader, AddressMessage, AddressMessageBuffer}, link::{LinkMessage, LinkMessageBuffer}, @@ -46,9 +46,9 @@ const RTM_GETTCLASS: u16 = 42; const RTM_NEWTFILTER: u16 = 44; const RTM_DELTFILTER: u16 = 45; const RTM_GETTFILTER: u16 = 46; -// const RTM_NEWACTION: u16 = 48; -// const RTM_DELACTION: u16 = 49; -// const RTM_GETACTION: u16 = 50; +const RTM_NEWACTION: u16 = 48; +const RTM_DELACTION: u16 = 49; +const RTM_GETACTION: u16 = 50; const RTM_NEWPREFIX: u16 = 52; // const RTM_GETMULTICAST: u16 = 58; // const RTM_GETANYCAST: u16 = 62; @@ -291,6 +291,21 @@ impl<'a, T: AsRef<[u8]> + ?Sized> } } + RTM_NEWACTION | RTM_DELACTION | RTM_GETACTION => { + let err = "invalid tc action message"; + let msg = TcActionMessage::parse( + &TcActionMessageBuffer::new_checked(&buf.inner()) + .context(err)?, + ) + .context(err)?; + match message_type { + RTM_NEWACTION => RouteNetlinkMessage::NewTrafficAction(msg), + RTM_DELACTION => RouteNetlinkMessage::DelTrafficAction(msg), + RTM_GETACTION => RouteNetlinkMessage::GetTrafficAction(msg), + _ => unreachable!(), + } + } + // ND ID Messages RTM_NEWNSID | RTM_GETNSID | RTM_DELNSID => { let err = "invalid nsid message"; @@ -348,6 +363,9 @@ pub enum RouteNetlinkMessage { NewTrafficFilter(TcMessage), DelTrafficFilter(TcMessage), GetTrafficFilter(TcMessage), + NewTrafficAction(TcActionMessage), + DelTrafficAction(TcActionMessage), + GetTrafficAction(TcActionMessage), NewTrafficChain(TcMessage), DelTrafficChain(TcMessage), GetTrafficChain(TcMessage), @@ -460,6 +478,18 @@ impl RouteNetlinkMessage { matches!(self, RouteNetlinkMessage::GetTrafficFilter(_)) } + pub fn is_new_action(&self) -> bool { + matches!(self, RouteNetlinkMessage::NewTrafficAction(_)) + } + + pub fn is_del_action(&self) -> bool { + matches!(self, RouteNetlinkMessage::DelTrafficAction(_)) + } + + pub fn is_get_action(&self) -> bool { + matches!(self, RouteNetlinkMessage::GetTrafficAction(_)) + } + pub fn is_new_chain(&self) -> bool { matches!(self, RouteNetlinkMessage::NewTrafficChain(_)) } @@ -528,6 +558,9 @@ impl RouteNetlinkMessage { NewTrafficFilter(_) => RTM_NEWTFILTER, DelTrafficFilter(_) => RTM_DELTFILTER, GetTrafficFilter(_) => RTM_GETTFILTER, + NewTrafficAction(_) => RTM_NEWACTION, + DelTrafficAction(_) => RTM_DELACTION, + GetTrafficAction(_) => RTM_GETACTION, NewTrafficChain(_) => RTM_NEWCHAIN, DelTrafficChain(_) => RTM_DELCHAIN, GetTrafficChain(_) => RTM_GETCHAIN, @@ -598,7 +631,12 @@ impl Emitable for RouteNetlinkMessage { | NewRule(ref msg) | DelRule(ref msg) | GetRule(ref msg) - => msg.buffer_len() + => msg.buffer_len(), + + | NewTrafficAction(ref msg) + | DelTrafficAction(ref msg) + | GetTrafficAction(ref msg) + => msg.buffer_len(), } } @@ -658,7 +696,12 @@ impl Emitable for RouteNetlinkMessage { | NewRule(ref msg) | DelRule(ref msg) | GetRule(ref msg) - => msg.emit(buffer) + => msg.emit(buffer), + + | NewTrafficAction(ref msg) + | DelTrafficAction(ref msg) + | GetTrafficAction(ref msg) + => msg.emit(buffer), } } } diff --git a/src/tc/actions/action.rs b/src/tc/actions/action.rs index 153a0e62..81203188 100644 --- a/src/tc/actions/action.rs +++ b/src/tc/actions/action.rs @@ -2,7 +2,7 @@ use anyhow::Context; use byteorder::{ByteOrder, NativeEndian}; - +use netlink_packet_utils::nla::NLA_F_NESTED; use netlink_packet_utils::{ nla::{DefaultNla, Nla, NlaBuffer, NlasIterator}, parsers::{parse_string, parse_u32}, @@ -10,17 +10,24 @@ use netlink_packet_utils::{ DecodeError, }; +use crate::tc::TcStats2; + use super::{ TcActionMirror, TcActionMirrorOption, TcActionNat, TcActionNatOption, }; -use crate::tc::TcStats2; +/// TODO: determine when and why to use this as opposed to the buffer's `kind`. const TCA_ACT_TAB: u16 = 1; +/// [`TcAction`] is a netlink message attribute that describes a [tc-action]. +/// +/// [tc-action]: https://man7.org/linux/man-pages/man8/tc-actions.8.html #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub struct TcAction { + /// Table id. Corresponds to the `Kind` of the action. pub tab: u16, + /// Attributes of the action. pub attributes: Vec, } @@ -39,7 +46,7 @@ impl Nla for TcAction { } fn emit_value(&self, buffer: &mut [u8]) { - self.attributes.as_slice().emit(buffer) + self.attributes.as_slice().emit(buffer); } fn kind(&self) -> u16 { @@ -49,62 +56,53 @@ impl Nla for TcAction { impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> for TcAction { fn parse(buf: &NlaBuffer<&'a T>) -> Result { - let mut attributes = vec![]; - let mut kind = String::new(); - - for iter in NlasIterator::new(buf.value()) { - let buf = iter.context("invalid action nla")?; - let payload = buf.value(); - attributes.push(match buf.kind() { - TCA_ACT_KIND => { - kind = parse_string(payload) - .context("failed to parse TCA_ACT_KIND")?; - TcActionAttribute::Kind(kind.clone()) - } - TCA_ACT_OPTIONS => { - let mut nlas = vec![]; - for nla in NlasIterator::new(payload) { - let nla = nla.context("invalid TCA_ACT_OPTIONS")?; - nlas.push( - TcActionOption::parse_with_param(&nla, &kind) - .context(format!( - "failed to parse TCA_ACT_OPTIONS \ - for kind {kind}" - ))?, + // We need to find the `Kind` attribute before we can parse the others, + // as kind is used in calls to parse_with_param for the other + // attributes. + // Messages of this type which do not specify [`Kind`], or which specify + // `Kind` more than once are malformed and should be rejected. + // We cannot ensure that `Kind` will be the first attribute in the + // `attributes` `Vec` (although it usually is). + // As a result, we need to determine `Kind` first, then parse the rest + // of the attributes. + let kind = match NlasIterator::new(buf.value()) + .filter_map(|nla| { + let nla = match nla { + Ok(nla) => nla, + Err(e) => { + return Some( + Err(e).context("failed to parse action nla"), ) } - TcActionAttribute::Options(nlas) + }; + match nla.kind() { + TCA_ACT_KIND => Some( + parse_string(nla.value()) + .context("failed to parse TCA_ACT_KIND"), + ), + _ => None, } - TCA_ACT_INDEX => TcActionAttribute::Index( - parse_u32(payload) - .context("failed to parse TCA_ACT_INDEX")?, - ), - TCA_ACT_STATS => { - let mut nlas = vec![]; - for nla in NlasIterator::new(payload) { - let nla = nla.context("invalid TCA_ACT_STATS")?; - nlas.push( - TcStats2::parse_with_param(&nla, &kind).context( - format!( - "failed to parse TCA_ACT_STATS for \ - kind {kind}", - ), - )?, - ); - } - TcActionAttribute::Stats(nlas) + }) + .collect::, _>>() + { + Ok(kinds) => { + if kinds.is_empty() { + return Err(DecodeError::from("Missing TCA_ACT_KIND")); } - TCA_ACT_COOKIE => TcActionAttribute::Cookie(payload.to_vec()), - TCA_ACT_IN_HW_COUNT => TcActionAttribute::InHwCount( - parse_u32(payload) - .context("failed to parse TCA_ACT_IN_HW_COUNT")?, - ), - _ => TcActionAttribute::Other( - DefaultNla::parse(&buf) - .context("failed to parse action nla")?, - ), - }); - } + if kinds.len() > 1 { + return Err(DecodeError::from("Duplicate TCA_ACT_KIND")); + } + kinds[0].clone() + } + Err(e) => return Err(DecodeError::from(e.to_string())), + }; + + let attributes = NlasIterator::new(buf.value()) + .map(|nla| { + TcActionAttribute::parse_with_param(&nla?, kind.as_str()) + }) + .collect::, _>>()?; + Ok(Self { tab: buf.kind(), attributes, @@ -123,15 +121,55 @@ const TCA_ACT_COOKIE: u16 = 6; // const TCA_ACT_USED_HW_STATS: u16 = 9; const TCA_ACT_IN_HW_COUNT: u16 = 10; +/// Attributes of a traffic control action. #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub enum TcActionAttribute { + /// The [`Kind`] (general type or class) of the action (e.g. "mirred", + /// "nat"). + /// + /// [`Kind`]: #variant.Kind Kind(String), + /// Parameters of the action. Options(Vec), + /// Index of the action. + /// + /// This is used to identify the action in the kernel. + /// Each action [`Kind`] has a unique table of actions. + /// That is, each action [`Kind`] has its own set of [`Index`] values. + /// + /// If [`Index`] is zero on action creation, + /// the kernel will assign a unique index to the new action. + /// The combination of [`Kind`] and [`Index`] can then be used to identify + /// and interact with the action in the future. + /// + /// For example, one action can be used by multiple different filters by + /// referencing the action's [`Index`] when creating that filter. + /// Such multiply referenced actions will aggregate their statistics. + /// + /// The kernel will reject attempts to delete an action if it is in use by + /// a filter. + /// Remove all referencing filters before deleting the action. + /// + /// [`Kind`]: #variant.Kind + /// [`Index`]: #variant.Index Index(u32), + /// Statistics about the action (e.g., number of bytes and or packets + /// processed). Stats(Vec), + /// [`Cookie`] is an attribute which _is not interpreted by the kernel at + /// all_ and may be used to store up to 16 bytes of arbitrary data on + /// an action in the kernel. + /// Userspace processes may then use this data to store additional + /// information about the action or to correlate actions with other + /// data. + /// + /// [`Cookie`]: #variant.Cookie Cookie(Vec), + /// Number of times the action has been installed in hardware. InHwCount(u32), + /// Other attributes unknown at the time of writing or not yet supported by + /// this library. Other(DefaultNla), } @@ -156,7 +194,7 @@ impl Nla for TcActionAttribute { } Self::Options(opt) => opt.as_slice().emit(buffer), Self::Index(value) | Self::InHwCount(value) => { - NativeEndian::write_u32(buffer, *value) + NativeEndian::write_u32(buffer, *value); } Self::Stats(s) => s.as_slice().emit(buffer), Self::Other(attr) => attr.emit_value(buffer), @@ -165,7 +203,17 @@ impl Nla for TcActionAttribute { fn kind(&self) -> u16 { match self { Self::Kind(_) => TCA_ACT_KIND, - Self::Options(_) => TCA_ACT_OPTIONS, + Self::Options(opts) => { + // NOTE: the kernel simply doesn't consistently use the nested + // flag based on captured messages. + // This is heuristically trying to match the kernel's behavior + // but may not be correct. + if opts.len() == 1 { + TCA_ACT_OPTIONS | NLA_F_NESTED + } else { + TCA_ACT_OPTIONS + } + } Self::Index(_) => TCA_ACT_INDEX, Self::Stats(_) => TCA_ACT_STATS, Self::Cookie(_) => TCA_ACT_COOKIE, @@ -175,11 +223,78 @@ impl Nla for TcActionAttribute { } } +impl<'a, T, P> ParseableParametrized, P> for TcActionAttribute +where + T: AsRef<[u8]> + ?Sized, + P: AsRef, +{ + fn parse_with_param( + buf: &NlaBuffer<&'a T>, + kind: P, + ) -> Result { + Ok(match buf.kind() { + TCA_ACT_KIND => { + let buf_value = buf.value(); + TcActionAttribute::Kind( + parse_string(buf_value) + .context("failed to parse TCA_ACT_KIND")?, + ) + } + TCA_ACT_OPTIONS => TcActionAttribute::Options( + NlasIterator::new(buf.value()) + .map(|nla| { + let nla = nla.context("invalid TCA_ACT_OPTIONS")?; + TcActionOption::parse_with_param(&nla, kind.as_ref()) + .context("failed to parse TCA_ACT_OPTIONS") + }) + .collect::, _>>()?, + ), + TCA_ACT_INDEX => TcActionAttribute::Index( + parse_u32(buf.value()) + .context("failed to parse TCA_ACT_INDEX")?, + ), + TCA_ACT_STATS => TcActionAttribute::Stats( + NlasIterator::new(buf.value()) + .map(|nla| { + let nla = nla.context("invalid TCA_ACT_STATS")?; + TcStats2::parse_with_param(&nla, kind.as_ref()) + .context("failed to parse TCA_ACT_STATS") + }) + .collect::, _>>()?, + ), + TCA_ACT_COOKIE => TcActionAttribute::Cookie(buf.value().to_vec()), + TCA_ACT_IN_HW_COUNT => TcActionAttribute::InHwCount( + parse_u32(buf.value()) + .context("failed to parse TCA_ACT_IN_HW_COUNT")?, + ), + _ => TcActionAttribute::Other( + DefaultNla::parse(buf).context("failed to parse action nla")?, + ), + }) + } +} + +/// `TcActionOption` is a netlink message attribute that describes an option of +/// a [tc-actions] action. +/// +/// This enum is non-exhaustive as new action types may be added to the kernel +/// at any time. +/// Only a small subset of possible actions are currently supported. +/// +/// [tc-actions]: https://man7.org/linux/man-pages/man8/tc-actions.8.html #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub enum TcActionOption { + /// Mirror options. + /// + /// These options can be used to mirror (copy) or redirect frames / packets + /// to another network interface. Mirror(TcActionMirrorOption), + /// NAT options. + /// + /// These options type can be used to perform network address translation. Nat(TcActionNatOption), + /// Other action types not yet supported by this library. Other(DefaultNla), } @@ -235,14 +350,71 @@ where } } -// `define tc_gen` in `linux/pkt_cls.h` +/// Generic traffic control action parameters. +/// +/// This structure is used to describe attributes common to all traffic control +/// actions. +/// +/// See [`#define tc_gen` in `linux/pkt_cls.h`][`tc_gen`]. +/// +/// [`tc_gen`]: https://elixir.bootlin.com/linux/v6.8.9/source/include/uapi/linux/pkt_cls.h#L179 #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] #[non_exhaustive] pub struct TcActionGeneric { + /// The [`index`] of the action is a unique identifier used to track + /// actions installed in the kernel. + /// + /// Each action type (e.g. [`mirror`] or [`nat`]) has its own independent + /// [`index`] space. + /// If you assign the [`index`] field to `0` when creating an action, the + /// kernel will assign a unique [`index`] to the new action. + /// + /// [`mirror`]: struct.TcActionMirror.html + /// [`nat`]: struct.TcActionNat.html + /// [`index`]: #structfield.index pub index: u32, + /// NOTE: I cannot find any documentation on this field nor any place + /// where it is used in iproute2 or the Linux kernel. + /// The [`capab`] field is part of the [`#define tc_gen`] in the kernel, + /// and that `#define` is used in many places, + /// but I don't see any place using the [`capab`] field in any way. + /// I may be looking in the wrong place or missing something. + /// + /// [`#define tc_gen`]: https://elixir.bootlin.com/linux/v6.8.9/source/include/uapi/linux/pkt_cls.h#L179 + /// [`capab`]: #structfield.capab pub capab: u32, + /// Action type. pub action: TcActionType, + /// Reference count of this action. + /// + /// This refers to the number of times this action is referenced within the + /// kernel. + /// Actions are cleaned up (deleted) when [`refcnt`] reaches 0. + /// + /// If you create an action on its own (i.e., not associated with a + /// filter), the [`refcnt`] will be 1. + /// If that action is then associated with a filter, the [`refcnt`] will be + /// 2. + /// If you then delete that filter, the [`refcnt`] will be 1 and the action + /// will remain until you explicitly delete it (which is only possible + /// when the [`refcnt`] is 1 and the [`bindcnt`] is 0). + /// + /// If you were to create an action indirectly (e.g., as part of creating a + /// filter) then the [`refcnt`] will still be 1 (along with the + /// [`bindcnt`]). + /// If you then create another filter that references the same action, the + /// [`refcnt`] will be 2 (along with the [`bindcnt`]). + /// + /// If you then deleted both of those actions, + /// the [`refcnt`] would be 0 and the action would be removed from the + /// kernel. + /// + /// [`refcnt`]: #structfield.refcnt + /// [`bindcnt`]: #structfield.bindcnt pub refcnt: i32, + /// Bind count of this action. + /// + /// The number of filters that reference (bind to) this action. pub bindcnt: i32, } @@ -296,20 +468,55 @@ const TC_ACT_REPEAT: i32 = 6; const TC_ACT_REDIRECT: i32 = 7; const TC_ACT_TRAP: i32 = 8; +/// Generic traffic control action types. +/// +/// These are the possible "outcomes" for a packet after an action is applied to +/// it. +/// +/// This enum is non-exhaustive as new action types may be added to the kernel +/// at any time. #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] #[non_exhaustive] pub enum TcActionType { + /// No specific outcome specified (i.e., take the default for that action). #[default] Unspec, + /// Terminates packet processing and allows the packet to proceed. Ok, + /// Terminates packet processing and restart packet classification. Reclassify, + /// Drop the packet. Shot, + /// Pipe the packet to the next action (if any). Pipe, + /// The packet is removed from this processing pipeline and returned to + /// another. + /// This happens, for example, when using the "mirred" redirect action. Stolen, + /// Queue the packet for later processing. Queued, + /// Repeat the action. + /// + /// > TODO: confirm this. I have not used this action before and its + /// > semantics are unclear. Repeat, + /// Redirect the packet. + /// + /// > TODO: confirm semantics of this action. It is unclear how + /// > [`Redirect`] differs from [`Stolen`]. + /// + /// [`Stolen`]: #variant.Stolen + /// [`Redirect`]: #variant.Redirect Redirect, + /// Transition packet processing from the hardware to software. + /// + /// If this action is encountered by in software, it is equivalent to + /// [`Shot`]. + /// + /// [`Shot`]: #variant.Shot Trap, + /// Other action types not known at the time of writing or not yet + /// supported by this library. Other(i32), } diff --git a/src/tc/actions/header.rs b/src/tc/actions/header.rs new file mode 100644 index 00000000..2bc85938 --- /dev/null +++ b/src/tc/actions/header.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_utils::nla::{NlaBuffer, NlasIterator}; +use netlink_packet_utils::{DecodeError, Emitable, Parseable}; + +use crate::AddressFamily; + +const TCA_HEADER_LEN: usize = 4; + +buffer!(TcActionMessageBuffer(TCA_HEADER_LEN) { + family: (u8, 0), + pad1: (u8, 1), + pad2: (u16, 2..TCA_HEADER_LEN), + payload: (slice, TCA_HEADER_LEN..), +}); + +impl<'a, T: AsRef<[u8]> + ?Sized> TcActionMessageBuffer<&'a T> { + /// Returns an iterator over the attributes of a `TcActionMessageBuffer`. + pub fn attributes( + &self, + ) -> impl Iterator, DecodeError>> { + NlasIterator::new(self.payload()) + } +} + +/// Header for a traffic control action message. +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct TcActionMessageHeader { + /// Address family (usually `AddressFamily::Unspec`). + pub family: AddressFamily, +} + +impl Emitable for TcActionMessageHeader { + fn buffer_len(&self) -> usize { + TCA_HEADER_LEN + } + + fn emit(&self, buffer: &mut [u8]) { + let mut packet = TcActionMessageBuffer::new(buffer); + packet.set_family(self.family.into()); + } +} + +impl> Parseable> + for TcActionMessageHeader +{ + fn parse(buf: &TcActionMessageBuffer) -> Result { + Ok(TcActionMessageHeader { + family: buf.family().into(), + }) + } +} diff --git a/src/tc/actions/message.rs b/src/tc/actions/message.rs new file mode 100644 index 00000000..5f3185d9 --- /dev/null +++ b/src/tc/actions/message.rs @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: MIT + +use anyhow::Context; +use netlink_packet_utils::nla::{DefaultNla, NlaBuffer}; +use netlink_packet_utils::nla::{Nla, NlasIterator}; +use netlink_packet_utils::{DecodeError, Emitable, Parseable}; + +use crate::tc::actions::{TcActionMessageBuffer, TcActionMessageHeader}; +use crate::tc::TcAction; + +/// Message to describe [tc-actions] +/// +/// [tc-actions]: https://man7.org/linux/man-pages/man8/tc-actions.8.html +#[derive(Debug, PartialEq, Eq, Clone, Default)] +#[non_exhaustive] +pub struct TcActionMessage { + /// Header of the message. + pub header: TcActionMessageHeader, + /// Attributes of the message. + pub attributes: Vec, +} + +const TCA_ACT_FLAG_LARGE_DUMP_ON: u32 = 1 << 0; +const TCA_ACT_FLAG_TERSE_DUMP: u32 = 1 << 1; + +bitflags! { + /// Flags to configure action dumps (list operations). + #[derive(Debug, PartialEq, Eq, Clone, Copy, Default, PartialOrd, Ord, Hash)] + #[non_exhaustive] + pub struct TcActionMessageFlags: u32 { + /// From `iproute2`'s [`rtnetlink.h`] + /// + /// If set, this flag enables more than TCA_ACT_MAX_PRIO actions in a single + /// actions listing operation. + /// + /// > TCA_ACT_FLAG_LARGE_DUMP_ON user->kernel to request for larger than + /// TCA_ACT_MAX_PRIO actions in a dump. + /// All dump responses will contain the number of actions being dumped + /// stored in for user app's consumption in TCA_ROOT_COUNT + /// + /// [`rtnetlink.h`]: https://github.com/iproute2/iproute2/blob/89210b9ec1c445ae963d181b5816d12a0cdafbb6/include/uapi/linux/rtnetlink.h#L803-L806 + const LargeDump = TCA_ACT_FLAG_LARGE_DUMP_ON; + /// If set, this flag restricts an action dump to only include essential + /// details. + /// + /// From `iproute2`'s [`rtnetlink.h`]: + /// + /// > TCA_ACT_FLAG_TERSE_DUMP user->kernel to request terse (brief) dump + /// that only includes essential action info (kind, index, etc.) + /// + /// [`rtnetlink.h`]: https://github.com/iproute2/iproute2/blob/89210b9ec1c445ae963d181b5816d12a0cdafbb6/include/uapi/linux/rtnetlink.h#L808-L809 + const TerseDump = TCA_ACT_FLAG_TERSE_DUMP; + const _ = !0; + } +} + +/// [`TcActionMessageFlagsWithSelector`] sets the [`TcActionMessageFlags`] which +/// are to be included in an operation, based on the accompanying [`flags`] and +/// [`selector`] fields. +/// +/// [`flags`]: #structfield.flags +/// [`selector`]: #structfield.selector +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, PartialOrd, Ord, Hash)] +pub struct TcActionMessageFlagsWithSelector { + /// A bitmask of [`TcActionMessageFlags`] to be associated with an + /// operation. + pub flags: TcActionMessageFlags, + /// A bitmask to determine which flags are to be included in an operation. + /// + /// Any flags which are set in the [`flags`] field but which are not set in + /// the [`selector`] field will be ignored. + /// + /// [`flags`]: #structfield.flags + /// [`selector`]: #structfield.selector + pub selector: TcActionMessageFlags, +} + +impl Nla for TcActionMessageFlagsWithSelector { + fn value_len(&self) -> usize { + 8 + } + + fn kind(&self) -> u16 { + TCA_ROOT_FLAGS + } + + fn emit_value(&self, buffer: &mut [u8]) { + buffer[..4].copy_from_slice(&self.flags.bits().to_ne_bytes()); + buffer[4..8].copy_from_slice(&self.selector.bits().to_ne_bytes()); + } +} + +impl TcActionMessageFlagsWithSelector { + /// Create a new [`TcActionMessageFlagsWithSelector`] with the given + /// [`flags`]. + /// The [`selector`] field is set to the same value as [`flags`] (i.e., none + /// of the [`flags`] will be ignored). + /// + /// [`flags`]: #structfield.flags + /// [`selector`]: #structfield.selector + #[must_use] + pub fn new(flags: TcActionMessageFlags) -> Self { + Self { + flags, + selector: flags, + } + } + + /// Create a new [`TcActionMessageFlagsWithSelector`] with the given + /// [`flags`] and [`selector`]. + /// + /// [`flags`]: #structfield.flags + /// [`selector`]: #structfield.selector + #[must_use] + pub fn new_with_selector( + flags: TcActionMessageFlags, + selector: TcActionMessageFlags, + ) -> Self { + Self { flags, selector } + } +} + +impl<'a, T: AsRef<[u8]> + 'a + ?Sized> Parseable> + for TcActionMessageFlagsWithSelector +{ + fn parse(buf: &NlaBuffer<&'a T>) -> Result { + let value = buf.value(); + if value.len() != 8 { + return Err(DecodeError::from("invalid length")); + } + let flags = TcActionMessageFlags::from_bits(u32::from_ne_bytes( + value[0..4].try_into().context("invalid length")?, + )) + .ok_or_else(|| DecodeError::from("invalid flags"))?; + let selector = TcActionMessageFlags::from_bits(u32::from_ne_bytes( + value[4..].try_into().context("invalid length")?, + )) + .ok_or_else(|| DecodeError::from("invalid flags selector"))?; + Ok(Self::new_with_selector(flags, selector)) + } +} + +const TCA_ACT_TAB: u16 = 1; +const TCA_ROOT_FLAGS: u16 = 2; +const TCA_ROOT_COUNT: u16 = 3; +const TCA_ROOT_TIME_DELTA: u16 = 4; +const TCA_ROOT_EXT_WARN_MSG: u16 = 5; + +/// This enum is used to represent the different types of attributes that can be +/// part of a `TcActionMessage`. +/// +/// This enum is non-exhaustive, additional variants may be added in the future. +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub enum TcActionMessageAttribute { + /// Collection of `TcActions`. + Actions(Vec), + /// Flags to configure action dumps (list operations). + Flags(TcActionMessageFlagsWithSelector), + /// Number of actions being dumped. + RootCount(u32), + /// Time delta. + RootTimeDelta(u32), + /// Extended warning message. + RootExtWarnMsg(String), + /// Other attributes unknown at the time of writing. + Other(DefaultNla), +} + +impl<'a, T: AsRef<[u8]> + 'a + ?Sized> Parseable> + for TcActionMessageAttribute +{ + fn parse(buf: &NlaBuffer<&'a T>) -> Result { + Ok(match buf.kind() { + TCA_ACT_TAB => { + let actions = NlasIterator::new(buf.value()) + .map(|nla| TcAction::parse(&nla?)) + .collect::, _>>()?; + Self::Actions(actions) + } + TCA_ROOT_FLAGS => { + Self::Flags(TcActionMessageFlagsWithSelector::parse(buf)?) + } + TCA_ROOT_COUNT => { + let count = u32::from_ne_bytes( + buf.value().try_into().context("invalid length")?, + ); + Self::RootCount(count) + } + TCA_ROOT_TIME_DELTA => { + let delta = u32::from_be_bytes( + buf.value().try_into().context("invalid length")?, + ); + Self::RootTimeDelta(delta) + } + TCA_ROOT_EXT_WARN_MSG => { + let msg = String::from_utf8(buf.value().to_vec()) + .context("invalid utf8")?; + Self::RootExtWarnMsg(msg) + } + _ => Self::Other(DefaultNla::parse(buf)?), + }) + } +} + +impl Nla for TcActionMessageAttribute { + fn value_len(&self) -> usize { + match self { + Self::Actions(actions) => actions.as_slice().buffer_len(), + Self::Flags(_) => 8, + Self::RootCount(_) => 4, + Self::RootTimeDelta(_) => 4, + Self::RootExtWarnMsg(msg) => msg.len(), + Self::Other(nla) => nla.value_len(), + } + } + + fn kind(&self) -> u16 { + match self { + Self::Actions(_) => TCA_ACT_TAB, + Self::Flags(_) => TCA_ROOT_FLAGS, + Self::RootCount(_) => TCA_ROOT_COUNT, + Self::RootTimeDelta(_) => TCA_ROOT_TIME_DELTA, + Self::RootExtWarnMsg(_) => TCA_ROOT_EXT_WARN_MSG, + Self::Other(nla) => nla.kind(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + Self::Actions(actions) => actions.as_slice().emit(buffer), + Self::Flags(flags) => { + flags.emit_value(buffer); + } + Self::RootCount(count) => { + buffer.copy_from_slice(&count.to_ne_bytes()); + } + Self::RootTimeDelta(delta) => { + buffer.copy_from_slice(&delta.to_be_bytes()); + } + Self::RootExtWarnMsg(msg) => buffer.copy_from_slice(msg.as_bytes()), + Self::Other(nla) => nla.emit_value(buffer), + } + } +} + +impl<'a, T: AsRef<[u8]> + 'a + ?Sized> Parseable> + for TcActionMessage +{ + fn parse(buf: &TcActionMessageBuffer<&'a T>) -> Result { + let attrs: Result, DecodeError> = buf + .attributes() + .map(|attr| TcActionMessageAttribute::parse(&attr?)) + .collect::, _>>(); + + Ok(Self { + header: TcActionMessageHeader::parse(buf) + .context("failed to parse tc message header")?, + attributes: attrs?, + }) + } +} + +impl Emitable for TcActionMessage { + 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/tc/actions/mod.rs b/src/tc/actions/mod.rs index 6102aaec..e426ee30 100644 --- a/src/tc/actions/mod.rs +++ b/src/tc/actions/mod.rs @@ -1,17 +1,28 @@ // SPDX-License-Identifier: MIT -mod action; -mod mirror; -mod nat; -mod nat_flag; +pub use nat_flag::TcNatFlags; pub use self::action::{ TcAction, TcActionAttribute, TcActionGeneric, TcActionGenericBuffer, TcActionOption, TcActionType, }; +pub use self::header::{TcActionMessageBuffer, TcActionMessageHeader}; +pub use self::message::{ + TcActionMessage, TcActionMessageAttribute, TcActionMessageFlags, + TcActionMessageFlagsWithSelector, +}; pub use self::mirror::{ TcActionMirror, TcActionMirrorOption, TcMirror, TcMirrorActionType, TcMirrorBuffer, }; pub use self::nat::{TcActionNat, TcActionNatOption, TcNat, TcNatBuffer}; -pub use nat_flag::TcNatFlags; + +mod action; +mod header; +mod message; +mod mirror; +mod nat; +mod nat_flag; + +#[cfg(test)] +pub mod tests; diff --git a/src/tc/actions/nat.rs b/src/tc/actions/nat.rs index 0c44ef4c..a46cc528 100644 --- a/src/tc/actions/nat.rs +++ b/src/tc/actions/nat.rs @@ -29,7 +29,7 @@ impl TcActionNat { #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub enum TcActionNatOption { - /// > TODO: document this after we make it something better than `Vec` + /// TODO: document this after we make it something better than `Vec` Tm(Vec), /// Parameters for the nat action. Parms(TcNat), diff --git a/src/tc/actions/tests/action.rs b/src/tc/actions/tests/action.rs new file mode 100644 index 00000000..2d75f432 --- /dev/null +++ b/src/tc/actions/tests/action.rs @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT + +use crate::tc::{ + TcAction, TcActionAttribute, TcActionGeneric, TcActionGenericBuffer, + TcActionType, TcStats2, TcStatsBasic, +}; +use netlink_packet_utils::nla::NlaBuffer; +use netlink_packet_utils::{Emitable, Parseable}; + +#[test] +fn tc_action_generic_parse_back() { + let orig = TcActionGeneric { + index: 1, + capab: 2, + action: TcActionType::Reclassify, + refcnt: 3, + bindcnt: 4, + }; + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcActionGeneric::parse( + &TcActionGenericBuffer::new_checked(buffer).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); +} + +#[test] +fn tc_action_parse_back_minimal() { + let orig = TcAction { + tab: 1, + attributes: vec![TcActionAttribute::Kind("example".into())], + }; + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = + TcAction::parse(&NlaBuffer::new_checked(buffer.as_slice()).unwrap()) + .unwrap(); + assert_eq!(orig, parsed); +} + +#[test] +fn tc_action_parse_back_example() { + let orig = TcAction { + tab: 1, + attributes: vec![ + TcActionAttribute::Kind("example".into()), + TcActionAttribute::Index(1), + TcActionAttribute::Cookie(vec![1, 2, 3, 4, 5, 6, 7, 8]), + TcActionAttribute::InHwCount(99), + TcActionAttribute::Stats(vec![ + TcStats2::Basic(TcStatsBasic { + bytes: 1, + packets: 2, + }), + TcStats2::BasicHw(TcStatsBasic { + bytes: 3, + packets: 4, + }), + ]), + TcActionAttribute::Options(vec![]), + ], + }; + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = + TcAction::parse(&NlaBuffer::new_checked(buffer.as_slice()).unwrap()) + .unwrap(); + assert_eq!(orig, parsed); +} diff --git a/src/tc/actions/tests/header.rs b/src/tc/actions/tests/header.rs new file mode 100644 index 00000000..e074bfef --- /dev/null +++ b/src/tc/actions/tests/header.rs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT + +use crate::tc::actions::{TcActionMessageBuffer, TcActionMessageHeader}; +use crate::AddressFamily; +use netlink_packet_utils::{Emitable, Parseable}; + +#[test] +fn tc_action_message_header_parse_back_all_known_families() { + for family in [ + AddressFamily::Unspec, + // AddressFamily::Local, // `Local` and `Unix` overlap! + AddressFamily::Unix, + AddressFamily::Inet, + AddressFamily::Ax25, + AddressFamily::Ipx, + AddressFamily::Appletalk, + AddressFamily::Netrom, + AddressFamily::Bridge, + AddressFamily::Atmpvc, + AddressFamily::X25, + AddressFamily::Inet6, + AddressFamily::Rose, + AddressFamily::Decnet, + AddressFamily::Netbeui, + AddressFamily::Security, + AddressFamily::Key, + // AddressFamily::Route, // `Route` and `Netlink` overlap! + AddressFamily::Netlink, + AddressFamily::Packet, + AddressFamily::Ash, + AddressFamily::Econet, + AddressFamily::Atmsvc, + AddressFamily::Rds, + AddressFamily::Sna, + AddressFamily::Irda, + AddressFamily::Pppox, + AddressFamily::Wanpipe, + AddressFamily::Llc, + AddressFamily::Ib, + AddressFamily::Mpls, + AddressFamily::Can, + AddressFamily::Tipc, + AddressFamily::Bluetooth, + AddressFamily::Iucv, + AddressFamily::Rxrpc, + AddressFamily::Isdn, + AddressFamily::Phonet, + AddressFamily::Ieee802154, + AddressFamily::Caif, + AddressFamily::Alg, + AddressFamily::Nfc, + AddressFamily::Vsock, + AddressFamily::Kcm, + AddressFamily::Qipcrtr, + AddressFamily::Smc, + AddressFamily::Xdp, + AddressFamily::Mctp, + ] { + let orig = TcActionMessageHeader { family }; + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcActionMessageHeader::parse( + &TcActionMessageBuffer::new_checked(&buffer).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); + } +} + +#[test] +fn tc_action_message_header_parse_back_other() { + let orig = TcActionMessageHeader { + family: AddressFamily::Other(99), + }; + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcActionMessageHeader::parse( + &TcActionMessageBuffer::new_checked(&buffer).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); +} diff --git a/src/tc/actions/tests/message.rs b/src/tc/actions/tests/message.rs new file mode 100644 index 00000000..47031710 --- /dev/null +++ b/src/tc/actions/tests/message.rs @@ -0,0 +1,357 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_utils::nla::{DefaultNla, NlaBuffer}; +use netlink_packet_utils::{Emitable, Parseable}; + +use crate::tc::actions::message::TcActionMessageAttribute::{ + Actions, Flags, RootCount, RootExtWarnMsg, RootTimeDelta, +}; +use crate::tc::actions::message::{ + TcActionMessage, TcActionMessageAttribute, TcActionMessageFlags, + TcActionMessageFlagsWithSelector, +}; +use crate::tc::actions::{TcActionMessageBuffer, TcActionMessageHeader}; +use crate::tc::TcAction; +use crate::tc::TcActionAttribute::{Cookie, Index, Kind}; +use crate::AddressFamily; + +mod mirror { + use netlink_packet_utils::nla::DefaultNla; + use netlink_packet_utils::Parseable; + + use crate::tc::actions::message::TcActionMessage; + use crate::tc::actions::message::TcActionMessageAttribute::{ + Actions, RootCount, + }; + use crate::tc::actions::{TcActionMessageBuffer, TcActionMessageHeader}; + use crate::tc::TcActionAttribute::{ + InHwCount, Kind, Options, Other, Stats, + }; + use crate::tc::TcActionMirrorOption::{Parms, Tm}; + use crate::tc::TcActionOption::Mirror; + use crate::tc::TcActionType::{Pipe, Stolen}; + use crate::tc::TcMirrorActionType::{EgressRedir, IngressMirror}; + use crate::tc::TcStats2::{Basic, BasicHw, Queue}; + use crate::tc::{ + TcAction, TcActionGeneric, TcMirror, TcStatsBasic, TcStatsQueue, + }; + use crate::AddressFamily; + + /// Captured `TcActionMessage` examples used for testing. + mod message { + /// Request + /// ```bash + /// tc actions add action mirred egress redirect dev lo index 1 + /// ``` + pub(super) const CREATE1: &str = "0000000038000100340001000b0001006d69727265640000240002802000020001000000000000000400000000000000000000000100000001000000"; + /// Request + /// ```bash + /// tc actions add action mirred ingress mirror dev lo index 2 + /// ``` + pub(super) const CREATE2: &str = "0000000038000100340001000b0001006d69727265640000240002802000020002000000000000000300000000000000000000000400000001000000"; + /// Response + /// ```bash + /// tc actions list action mirred + /// ``` + pub(super) const LIST: &str = "00000000080003000200000064010100b00000000b0001006d6972726564000044000400140001000000000000000000000000000000000014000700000000000000000000000000000000001800030000000000000000000000000000000000000000000c000900000000000300000008000a0000000000480002002000020001000000000000000400000001000000000000000100000001000000240001000000000000000000000000000000000000000000000000000000000000000000b00001000b0001006d6972726564000044000400140001000000000000000000000000000000000014000700000000000000000000000000000000001800030000000000000000000000000000000000000000000c000900000000000300000008000a0000000000480002002000020002000000000000000300000001000000000000000400000001000000240001000000000000000000000000000000000000000000000000000000000000000000"; + } + + #[test] + fn parse_message1_create() { + let expected = TcActionMessage { + header: TcActionMessageHeader { + family: AddressFamily::Unspec, + }, + attributes: vec![Actions(vec![TcAction { + tab: 1, + attributes: vec![ + Kind("mirred".into()), + Options(vec![Mirror(Parms(TcMirror { + generic: TcActionGeneric { + index: 1, + capab: 0, + action: Stolen, + refcnt: 0, + bindcnt: 0, + }, + eaction: EgressRedir, + ifindex: 1, + }))]), + ], + }])], + }; + + let buf = hex::decode(message::CREATE1).unwrap(); + let parsed = TcActionMessage::parse( + &TcActionMessageBuffer::new_checked(&buf).unwrap(), + ) + .unwrap(); + assert_eq!(parsed, expected); + } + + #[test] + fn parse_message2_create() { + let expected = TcActionMessage { + header: TcActionMessageHeader { + family: AddressFamily::Unspec, + }, + attributes: vec![Actions(vec![TcAction { + tab: 1, + attributes: vec![ + Kind("mirred".into()), + Options(vec![Mirror(Parms(TcMirror { + generic: TcActionGeneric { + index: 2, + capab: 0, + action: Pipe, + refcnt: 0, + bindcnt: 0, + }, + eaction: IngressMirror, + ifindex: 1, + }))]), + ], + }])], + }; + + let buf = hex::decode(message::CREATE2).unwrap(); + let parsed = TcActionMessage::parse( + &TcActionMessageBuffer::new_checked(&buf).unwrap(), + ) + .unwrap(); + assert_eq!(parsed, expected); + } + + #[test] + fn parse_message3_list() { + let expected = TcActionMessage { + header: TcActionMessageHeader { + family: AddressFamily::Unspec, + }, + attributes: vec![ + RootCount(2), + Actions(vec![ + TcAction { + tab: 0, + attributes: vec![ + Kind("mirred".into()), + Stats(vec![ + Basic(TcStatsBasic { + bytes: 0, + packets: 0, + }), + BasicHw(TcStatsBasic { + bytes: 0, + packets: 0, + }), + Queue(TcStatsQueue { + qlen: 0, + backlog: 0, + drops: 0, + requeues: 0, + overlimits: 0, + }), + ]), + Other(DefaultNla::new( + 9, + vec![0, 0, 0, 0, 3, 0, 0, 0], + )), + InHwCount(0), + Options(vec![ + Mirror(Parms(TcMirror { + generic: TcActionGeneric { + index: 1, + capab: 0, + action: Stolen, + refcnt: 1, + bindcnt: 0, + }, + eaction: EgressRedir, + ifindex: 1, + })), + Mirror(Tm(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ])), + ]), + ], + }, + TcAction { + tab: 1, + attributes: vec![ + Kind("mirred".into()), + Stats(vec![ + Basic(TcStatsBasic { + bytes: 0, + packets: 0, + }), + BasicHw(TcStatsBasic { + bytes: 0, + packets: 0, + }), + Queue(TcStatsQueue { + qlen: 0, + backlog: 0, + drops: 0, + requeues: 0, + overlimits: 0, + }), + ]), + Other(DefaultNla::new( + 9, + vec![0, 0, 0, 0, 3, 0, 0, 0], + )), + InHwCount(0), + Options(vec![ + Mirror(Parms(TcMirror { + generic: TcActionGeneric { + index: 2, + capab: 0, + action: Pipe, + refcnt: 1, + bindcnt: 0, + }, + eaction: IngressMirror, + ifindex: 1, + })), + Mirror(Tm(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ])), + ]), + ], + }, + ]), + ], + }; + let buf = hex::decode(message::LIST).unwrap(); + let parsed = TcActionMessage::parse( + &TcActionMessageBuffer::new_checked(&buf).unwrap(), + ) + .unwrap(); + assert_eq!(parsed, expected); + } +} + +#[test] +fn tc_action_message_attribute_parse_back_blank_actions() { + let orig = Actions(vec![]); + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcActionMessageAttribute::parse( + &NlaBuffer::new_checked(buffer.as_slice()).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); +} + +#[test] +fn tc_action_message_attribute_parse_back_example_action() { + let orig = Actions(vec![TcAction { + tab: 9999, + attributes: vec![Kind("example".into())], + }]); + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcActionMessageAttribute::parse( + &NlaBuffer::new_checked(buffer.as_slice()).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); +} + +#[test] +fn tc_action_message_attribute_parse_back_multiple_example_action() { + let orig = Actions(vec![ + TcAction { + tab: 1111, + attributes: vec![Kind("example1".into())], + }, + TcAction { + tab: 2222, + attributes: vec![ + Kind("example2".into()), + Index(42), + Cookie(vec![1, 2, 3, 4, 5, 6]), + ], + }, + ]); + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcActionMessageAttribute::parse( + &NlaBuffer::new_checked(buffer.as_slice()).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); +} + +#[test] +fn tc_action_message_flags_parse_back_default() { + let orig = TcActionMessageFlagsWithSelector::default(); + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcActionMessageFlagsWithSelector::parse( + &NlaBuffer::new_checked(buffer.as_slice()).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); +} + +#[test] +fn tc_action_message_flags_parse_back_example_value() { + let orig = TcActionMessageFlagsWithSelector { + flags: TcActionMessageFlags::LargeDump + | TcActionMessageFlags::TerseDump, + selector: TcActionMessageFlags::LargeDump, + }; + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcActionMessageFlagsWithSelector::parse( + &NlaBuffer::new_checked(buffer.as_slice()).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); +} + +#[test] +fn tc_action_message_parse_back_default() { + let orig = TcActionMessage::default(); + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcActionMessage::parse( + &TcActionMessageBuffer::new_checked(buffer.as_slice()).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); +} + +#[test] +fn tc_action_message_parse_back_example_value() { + let orig = TcActionMessage { + header: TcActionMessageHeader { + family: AddressFamily::Alg, + }, + attributes: vec![ + Flags(TcActionMessageFlagsWithSelector { + flags: TcActionMessageFlags::LargeDump, + selector: TcActionMessageFlags::LargeDump, + }), + RootCount(42), + RootTimeDelta(43), + RootExtWarnMsg("hello".to_string()), + TcActionMessageAttribute::Other(DefaultNla::new( + 99, + vec![1, 2, 3, 4], + )), + ], + }; + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcActionMessage::parse( + &TcActionMessageBuffer::new_checked(buffer.as_slice()).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); +} diff --git a/src/tc/actions/tests/mirror.rs b/src/tc/actions/tests/mirror.rs new file mode 100644 index 00000000..74343239 --- /dev/null +++ b/src/tc/actions/tests/mirror.rs @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_utils::nla::NlaBuffer; +use netlink_packet_utils::{Emitable, Parseable}; + +use crate::tc::{ + TcActionGeneric, TcActionGenericBuffer, TcActionMirrorOption, TcActionType, + TcMirror, TcMirrorActionType, TcMirrorBuffer, +}; + +#[test] +fn tc_action_generic_parse_back() { + let orig = TcActionGeneric { + index: 1, + capab: 2, + action: TcActionType::Reclassify, + refcnt: 3, + bindcnt: 4, + }; + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcActionGeneric::parse( + &TcActionGenericBuffer::new_checked(buffer).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); +} + +#[test] +fn tc_mirror_default_parse_back() { + let orig = TcMirror { + generic: Default::default(), + eaction: Default::default(), + ifindex: 111, + }; + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcMirror::parse( + &TcMirrorBuffer::new_checked(buffer.as_slice()).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); +} + +#[test] +fn tc_mirror_example_parse_back() { + let orig = TcMirror { + generic: TcActionGeneric { + index: 1, + capab: 2, + action: TcActionType::Ok, + refcnt: 3, + bindcnt: 4, + }, + eaction: TcMirrorActionType::IngressMirror, + ifindex: 99, + }; + let mut buffer = vec![0; orig.buffer_len()]; + orig.emit(&mut buffer); + let parsed = TcMirror::parse( + &TcMirrorBuffer::new_checked(buffer.as_slice()).unwrap(), + ) + .unwrap(); + assert_eq!(orig, parsed); +} + +#[test] +fn tc_mirror_tm_default_parse_back() { + let mirror_option = TcActionMirrorOption::Tm(vec![]); + let mut buffer = vec![0; mirror_option.buffer_len()]; + mirror_option.emit(&mut buffer); + let nla_buf = NlaBuffer::new_checked(&buffer).unwrap(); + let parsed = TcActionMirrorOption::parse(&nla_buf).unwrap(); + assert_eq!(mirror_option, parsed); +} + +#[test] +fn tc_mirror_tm_example_parse_back() { + let mirror_option = TcActionMirrorOption::Tm(vec![1, 2, 3]); + let mut buffer = vec![0; mirror_option.buffer_len()]; + mirror_option.emit(&mut buffer); + let nla_buf = NlaBuffer::new_checked(&buffer).unwrap(); + let parsed = TcActionMirrorOption::parse(&nla_buf).unwrap(); + assert_eq!(mirror_option, parsed); +} diff --git a/src/tc/actions/tests/mod.rs b/src/tc/actions/tests/mod.rs new file mode 100644 index 00000000..e36825fe --- /dev/null +++ b/src/tc/actions/tests/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pub mod action; +pub mod header; +pub mod message; +pub mod mirror; +pub mod nat; diff --git a/src/tc/actions/tests/nat.rs b/src/tc/actions/tests/nat.rs new file mode 100644 index 00000000..e1eec778 --- /dev/null +++ b/src/tc/actions/tests/nat.rs @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: MIT + +use std::net::Ipv4Addr; + +use netlink_packet_utils::nla::NlaBuffer; +use netlink_packet_utils::{Emitable, Parseable}; + +use crate::tc::actions::message::TcActionMessage; +use crate::tc::actions::message::TcActionMessageAttribute::Actions; +use crate::tc::actions::{TcActionMessageBuffer, TcActionMessageHeader}; +use crate::tc::TcActionAttribute::{InHwCount, Kind, Options, Stats}; +use crate::tc::TcActionNatOption::{Parms, Tm}; +use crate::tc::TcActionOption::Nat; +use crate::tc::TcStats2::{Basic, BasicHw, Queue}; +use crate::tc::{ + TcAction, TcActionGeneric, TcActionNatOption, TcActionType, TcNat, + TcNatFlags, TcStatsBasic, TcStatsQueue, +}; +use crate::AddressFamily; + +/// Capture of request for +/// +/// ```bash +/// tc actions add action nat ingress 1.2.3.4/32 5.6.7.0 index 1 +/// ``` +const TC_ACTION_NAT_EXAMPLE1: &str = "000000003c00010038000100080001006e6174002c0002802800010001000000000000000000000000000000000000000102030405060700ffffffff00000000"; + +fn tc_action_message_nat_example1() -> TcActionMessage { + TcActionMessage { + header: TcActionMessageHeader { + family: AddressFamily::Unspec, + }, + attributes: vec![Actions(vec![TcAction { + tab: 1, + attributes: vec![ + Kind("nat".into()), + Options(vec![Nat(TcActionNatOption::Parms(TcNat { + generic: TcActionGeneric { + index: 1, + capab: 0, + action: TcActionType::Ok, + refcnt: 0, + bindcnt: 0, + }, + old_addr: Ipv4Addr::new(1, 2, 3, 4), + new_addr: Ipv4Addr::new(5, 6, 7, 0), + mask: Ipv4Addr::new(255, 255, 255, 255), + flags: TcNatFlags::empty(), + }))]), + ], + }])], + } +} + +#[test] +fn parse_tc_action_nat_example1() { + let buf = hex::decode(TC_ACTION_NAT_EXAMPLE1).unwrap(); + let parsed = TcActionMessage::parse( + &TcActionMessageBuffer::new_checked(&buf).unwrap(), + ) + .unwrap(); + assert_eq!(parsed, tc_action_message_nat_example1()); +} + +#[test] +fn emit_tc_action_nat_example1() { + let example = tc_action_message_nat_example1(); + let mut buf = vec![0; example.buffer_len()]; + example.emit(&mut buf); + assert_eq!(buf, hex::decode(TC_ACTION_NAT_EXAMPLE1).unwrap()); +} + +/// Capture of request for +/// +/// ```bash +/// tc actions add action nat ingress 1.2.3.0/24 5.6.7.9 index 2 +/// ``` +const TC_ACTION_NAT_EXAMPLE2: &str = "000000003c00010038000100080001006e6174002c0002802800010002000000000000000000000000000000000000000102030005060709ffffff0000000000"; + +fn tc_action_message_nat_example2() -> TcActionMessage { + TcActionMessage { + header: TcActionMessageHeader { + family: AddressFamily::Unspec, + }, + attributes: vec![Actions(vec![TcAction { + tab: 1, + attributes: vec![ + Kind("nat".into()), + Options(vec![Nat(TcActionNatOption::Parms(TcNat { + generic: TcActionGeneric { + index: 2, + capab: 0, + action: TcActionType::Ok, + refcnt: 0, + bindcnt: 0, + }, + old_addr: Ipv4Addr::new(1, 2, 3, 0), + new_addr: Ipv4Addr::new(5, 6, 7, 9), + mask: Ipv4Addr::new(255, 255, 255, 0), + flags: TcNatFlags::empty(), + }))]), + ], + }])], + } +} + +#[test] +fn parse_tc_action_nat_example2() { + let buf = hex::decode(TC_ACTION_NAT_EXAMPLE2).unwrap(); + let parsed = TcActionMessage::parse( + &TcActionMessageBuffer::new_checked(&buf).unwrap(), + ) + .unwrap(); + assert_eq!(parsed, tc_action_message_nat_example2()); +} + +#[test] +fn emit_tc_action_nat_example2() { + let example = tc_action_message_nat_example2(); + let mut buf = vec![0; example.buffer_len()]; + example.emit(&mut buf); + assert_eq!(buf, hex::decode(TC_ACTION_NAT_EXAMPLE2).unwrap()); +} + +/// Capture of request for +/// +/// ```bash +/// tc actions add action nat egress 2.3.4.0/24 5.6.7.9 index 3 +/// ``` +const TC_ACTION_NAT_EXAMPLE3: &str = "000000003c00010038000100080001006e6174002c0002802800010003000000000000000000000000000000000000000203040005060709ffffff0001000000"; + +fn tc_action_message_nat_example3() -> TcActionMessage { + TcActionMessage { + header: TcActionMessageHeader { + family: AddressFamily::Unspec, + }, + attributes: vec![Actions(vec![TcAction { + tab: 1, + attributes: vec![ + Kind("nat".into()), + Options(vec![Nat(TcActionNatOption::Parms(TcNat { + generic: TcActionGeneric { + index: 3, + capab: 0, + action: TcActionType::Ok, + refcnt: 0, + bindcnt: 0, + }, + old_addr: Ipv4Addr::new(2, 3, 4, 0), + new_addr: Ipv4Addr::new(5, 6, 7, 9), + mask: Ipv4Addr::new(255, 255, 255, 0), + flags: TcNatFlags::Egress, + }))]), + ], + }])], + } +} + +#[test] +fn parse_tc_action_nat_example3() { + let buf = hex::decode(TC_ACTION_NAT_EXAMPLE3).unwrap(); + let parsed = TcActionMessage::parse( + &TcActionMessageBuffer::new_checked(&buf).unwrap(), + ) + .unwrap(); + assert_eq!(parsed, tc_action_message_nat_example3()); +} + +#[test] +fn emit_tc_action_nat_example3() { + let example = tc_action_message_nat_example3(); + let mut buf = vec![0x00; example.buffer_len()]; + example.emit(&mut buf); + assert_eq!(buf, hex::decode(TC_ACTION_NAT_EXAMPLE3).unwrap()); +} + +const TC_ACTION_NAT_OPTION_PARAMS_EXAMPLES: [TcActionNatOption; 2] = [ + TcActionNatOption::Parms(TcNat { + flags: TcNatFlags::empty(), + generic: TcActionGeneric { + action: TcActionType::Reclassify, + bindcnt: 1, + capab: 2, + index: 3, + refcnt: 4, + }, + mask: Ipv4Addr::BROADCAST, + new_addr: Ipv4Addr::new(1, 2, 3, 4), + old_addr: Ipv4Addr::new(5, 6, 7, 8), + }), + TcActionNatOption::Parms(TcNat { + flags: TcNatFlags::empty(), + generic: TcActionGeneric { + action: TcActionType::Pipe, + bindcnt: 5, + capab: 6, + index: 7, + refcnt: 8, + }, + mask: Ipv4Addr::new(255, 255, 255, 254), + new_addr: Ipv4Addr::new(2, 1, 255, 0), + old_addr: Ipv4Addr::new(7, 2, 88, 44), + }), +]; + +#[test] +fn tc_action_nat_option_parse_back_example_params() { + for example in TC_ACTION_NAT_OPTION_PARAMS_EXAMPLES { + let mut buffer = vec![0; example.buffer_len()]; + example.emit(&mut buffer); + let parsed = TcActionNatOption::parse( + &NlaBuffer::new_checked(buffer.as_slice()).unwrap(), + ) + .unwrap(); + assert_eq!(example, parsed); + } +} + +#[test] +fn tc_action_nat_option_emit_uses_whole_buffer() { + for example in TC_ACTION_NAT_OPTION_PARAMS_EXAMPLES { + let mut buffer1 = vec![0x00; example.buffer_len()]; + let mut buffer2 = vec![0xff; example.buffer_len()]; + example.emit(&mut buffer1); + example.emit(&mut buffer2); + assert_eq!(buffer1, buffer2); + } +} + +fn tc_action_nat_option_tm_examples() -> [TcActionNatOption; 4] { + [ + TcActionNatOption::Tm(vec![]), + TcActionNatOption::Tm(vec![1]), + TcActionNatOption::Tm(vec![1, 2, 3, 4]), + TcActionNatOption::Tm(vec![99; 10]), + ] +} + +#[test] +fn tc_action_nat_option_parse_back_example_tm() { + for example in tc_action_nat_option_tm_examples().iter() { + let mut buffer = vec![0; example.buffer_len()]; + example.emit(&mut buffer); + let parsed = TcActionNatOption::parse( + &NlaBuffer::new_checked(buffer.as_slice()).unwrap(), + ) + .unwrap(); + assert_eq!(example, &parsed); + } +} + +#[test] +fn tc_action_nat_option_emit_tm_uses_whole_buffer() { + for example in tc_action_nat_option_tm_examples().iter() { + let mut buffer1 = vec![0x00; example.buffer_len()]; + let mut buffer2 = vec![0xff; example.buffer_len()]; + example.emit(&mut buffer1); + example.emit(&mut buffer2); + assert_eq!(buffer1, buffer2); + } +} + +/// Setup: +/// +/// ```bash +/// tc actions flush action nat +/// tc actions add action nat ingress 192.0.2.1/32 203.0.113.1 index 1 +/// ``` +/// +/// Then capture netlink response message of this command: +/// +/// ```bash +/// tc -statistics actions get action nat index 1 +/// ``` +/// +/// Raw packet modification: +/// * cooked header removed (16 bytes). +/// * rtnetlink header removed (16 bytes). +#[test] +fn test_get_filter_nat() { + const RAW: &str = "00000000ac000100a8000100080001006e617400440004001400010000000000000000000000000000000000140007000000000000000000000000000000000018000300000000000000000000000000000000000000000008000a000000000050000200280001000100000000000000000000000100000000000000c0000201cb007101ffffffff00000000240002000000000000000000000000000000000000000000000000000000000000000000"; + let raw = hex::decode(RAW).unwrap(); + + let expected = TcActionMessage { + header: TcActionMessageHeader { + family: AddressFamily::Unspec, + }, + attributes: vec![Actions(vec![TcAction { + tab: 1, + attributes: vec![ + Kind("nat".into()), + Stats(vec![ + Basic(TcStatsBasic { + bytes: 0, + packets: 0, + }), + BasicHw(TcStatsBasic { + bytes: 0, + packets: 0, + }), + Queue(TcStatsQueue { + qlen: 0, + backlog: 0, + drops: 0, + requeues: 0, + overlimits: 0, + }), + ]), + InHwCount(0), + Options(vec![ + Nat(Parms(TcNat { + generic: TcActionGeneric { + index: 1, + capab: 0, + action: TcActionType::Ok, + refcnt: 1, + bindcnt: 0, + }, + old_addr: [192, 0, 2, 1].into(), + new_addr: [203, 0, 113, 1].into(), + mask: Ipv4Addr::BROADCAST, + flags: TcNatFlags::empty(), + })), + Nat(Tm(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ])), + ]), + ], + }])], + }; + + assert_eq!( + expected, + TcActionMessage::parse(&TcActionMessageBuffer::new(&raw)).unwrap() + ); + + let mut buf = vec![0; expected.buffer_len()]; + + expected.emit(&mut buf); + + assert_eq!(buf, raw); +} diff --git a/src/tc/mod.rs b/src/tc/mod.rs index 56608bd2..a083b968 100644 --- a/src/tc/mod.rs +++ b/src/tc/mod.rs @@ -11,9 +11,11 @@ mod stats; pub use self::actions::{ TcAction, TcActionAttribute, TcActionGeneric, TcActionGenericBuffer, - TcActionMirror, TcActionMirrorOption, TcActionNat, TcActionNatOption, - TcActionOption, TcActionType, TcMirror, TcMirrorActionType, TcMirrorBuffer, - TcNat, TcNatBuffer, TcNatFlags, + TcActionMessage, TcActionMessageAttribute, TcActionMessageBuffer, + TcActionMessageFlags, TcActionMessageFlagsWithSelector, TcActionMirror, + TcActionMirrorOption, TcActionNat, TcActionNatOption, TcActionOption, + TcActionType, TcMirror, TcMirrorActionType, TcMirrorBuffer, TcNat, + TcNatBuffer, TcNatFlags, }; pub use self::attribute::TcAttribute; pub use self::filters::{ diff --git a/src/tc/tests/action_nat.rs b/src/tc/tests/action_nat.rs deleted file mode 100644 index 172993b2..00000000 --- a/src/tc/tests/action_nat.rs +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-License-Identifier: MIT - -use std::net::Ipv4Addr; - -use netlink_packet_utils::{Emitable, Parseable}; - -use crate::{ - tc::{ - filters::{TcU32OptionFlags, TcU32SelectorFlags}, - TcAction, TcActionAttribute, TcActionGeneric, TcActionNatOption, - TcActionOption, TcActionType, TcAttribute, TcFilterU32Option, TcHandle, - TcHeader, TcMessage, TcMessageBuffer, TcNat, TcNatFlags, TcOption, - TcStats2, TcStatsBasic, TcStatsQueue, TcU32Key, TcU32Selector, - }, - AddressFamily, -}; - -// Setup: -// ip link add dummy1 type dummy -// ip link set dummy1 up -// tc qdisc add dev dummy1 root handle 1: prio bands 4 -// tc filter add dev dummy1 parent ffff: \ -// protocol ip prio 10 u32 \ -// match ip dst 192.0.2.1/32 \ -// action nat ingress 192.0.2.1/32 203.0.113.1 -// -// Capture nlmon of this command: -// -// tc -s filter show dev dummy1 -// -// Raw packet modification: -// * rtnetlink header removed. -#[test] -fn test_get_filter_nat() { - let raw = vec![ - 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x80, - 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x04, 0x00, 0x08, 0x00, 0x01, 0x00, - 0x75, 0x33, 0x32, 0x00, 0x08, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x02, 0x00, 0x24, 0x00, 0x05, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x02, 0x02, 0x10, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x80, - 0x08, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x00, 0x00, 0xac, 0x00, 0x07, 0x00, - 0xa8, 0x00, 0x01, 0x00, 0x08, 0x00, 0x01, 0x00, 0x6e, 0x61, 0x74, 0x00, - 0x44, 0x00, 0x04, 0x00, 0x14, 0x00, 0x01, 0x00, 0x62, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x14, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x03, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0a, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x02, 0x00, 0x28, 0x00, 0x01, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x02, 0x02, - 0xcb, 0x00, 0x71, 0x01, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x24, 0x00, 0x02, 0x00, 0x87, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x78, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x78, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x1c, 0x00, 0x09, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ]; - - let expected = TcMessage { - header: TcHeader { - family: AddressFamily::Unspec, - index: 53, - handle: TcHandle { - major: 0x8000, - minor: 0x800, - }, - parent: TcHandle { major: 1, minor: 0 }, - info: 262152, // TODO(Gris Ge) - }, - attributes: vec![ - TcAttribute::Kind("u32".to_string()), - TcAttribute::Chain(0), - TcAttribute::Options(vec![ - TcOption::U32(TcFilterU32Option::Selector(TcU32Selector { - flags: TcU32SelectorFlags::Terminal, - offshift: 0, - nkeys: 1, - offmask: 0, - off: 0, - offoff: 0, - hoff: 0, - hmask: 0, - keys: vec![TcU32Key { - mask: 0xffffffff, - val: u32::from_ne_bytes( - Ipv4Addr::new(192, 0, 2, 2).octets(), - ), - off: 16, - offmask: 0, - }], - })), - TcOption::U32(TcFilterU32Option::Hash(u32::from_be(0x80))), - TcOption::U32(TcFilterU32Option::Flags( - TcU32OptionFlags::NotInHw, - )), - TcOption::U32(TcFilterU32Option::Action(vec![TcAction { - tab: 1, - attributes: vec![ - TcActionAttribute::Kind("nat".to_string()), - TcActionAttribute::Stats(vec![ - TcStats2::Basic(TcStatsBasic { - bytes: 98, - packets: 1, - }), - TcStats2::BasicHw(TcStatsBasic { - bytes: 0, - packets: 0, - }), - TcStats2::Queue(TcStatsQueue { - qlen: 0, - backlog: 0, - drops: 0, - requeues: 0, - overlimits: 0, - }), - ]), - TcActionAttribute::InHwCount(0), - TcActionAttribute::Options(vec![ - TcActionOption::Nat(TcActionNatOption::Parms( - TcNat { - generic: TcActionGeneric { - index: 1, - capab: 0, - action: TcActionType::Ok, - refcnt: 1, - bindcnt: 1, - }, - old_addr: Ipv4Addr::new(192, 0, 2, 2), - new_addr: Ipv4Addr::new(203, 0, 113, 1), - mask: Ipv4Addr::new(255, 255, 255, 255), - flags: TcNatFlags::empty(), - }, - )), - TcActionOption::Nat(TcActionNatOption::Tm(vec![ - 135, 20, 0, 0, 0, 0, 0, 0, 120, 7, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 120, 7, 0, 0, 0, - 0, 0, 0, - ])), - ]), - ], - }])), - TcOption::U32(TcFilterU32Option::Pnct(vec![ - 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, - 0, 0, 0, 0, - ])), - ]), - ], - }; - - assert_eq!( - expected, - TcMessage::parse(&TcMessageBuffer::new(&raw)).unwrap() - ); - - let mut buf = vec![0; expected.buffer_len()]; - - expected.emit(&mut buf); - - assert_eq!(buf, raw); -} diff --git a/src/tc/tests/mod.rs b/src/tc/tests/mod.rs index 62cb2237..05058a46 100644 --- a/src/tc/tests/mod.rs +++ b/src/tc/tests/mod.rs @@ -1,7 +1,5 @@ // SPDX-License-Identifier: MIT -#[cfg(test)] -mod action_nat; #[cfg(test)] mod filter_matchall; #[cfg(test)]