diff --git a/netlink-packet-route/src/rtnl/tc/constants.rs b/netlink-packet-route/src/rtnl/tc/constants.rs new file mode 100644 index 00000000..fd89f9c9 --- /dev/null +++ b/netlink-packet-route/src/rtnl/tc/constants.rs @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT + +/// Handles +pub const TC_H_MAJ_MASK: u32 = 0xFFFF0000; +pub const TC_H_MIN_MASK: u32 = 0x0000FFFF; + +#[macro_export] +macro_rules! TC_H_MAKE { + ($maj: expr, $min: expr) => { + ($maj & TC_H_MAJ_MASK) | ($min & TC_H_MIN_MASK) + }; +} + +pub const TC_H_UNSPEC: u32 = 0; +pub const TC_H_ROOT: u32 = 0xFFFFFFFF; +pub const TC_H_INGRESS: u32 = 0xFFFFFFF1; +pub const TC_H_CLSACT: u32 = TC_H_INGRESS; + +pub const TC_H_MIN_PRIORITY: u32 = 0xFFE0; +pub const TC_H_MIN_INGRESS: u32 = 0xFFF2; +pub const TC_H_MIN_EGRESS: u32 = 0xFFF3; + +/// U32 filters +pub const TCA_U32_UNSPEC: u16 = 0; +pub const TCA_U32_CLASSID: u16 = 1; +pub const TCA_U32_HASH: u16 = 2; +pub const TCA_U32_LINK: u16 = 3; +pub const TCA_U32_DIVISOR: u16 = 4; +pub const TCA_U32_SEL: u16 = 5; +pub const TCA_U32_POLICE: u16 = 6; +pub const TCA_U32_ACT: u16 = 7; +pub const TCA_U32_INDEV: u16 = 8; +pub const TCA_U32_PCNT: u16 = 9; +pub const TCA_U32_MARK: u16 = 10; +pub const TCA_U32_FLAGS: u16 = 11; +pub const TCA_U32_PAD: u16 = 12; +pub const TCA_U32_MAX: u16 = TCA_U32_PAD; + +/// U32 Flags +pub const TC_U32_TERMINAL: u8 = 1; +pub const TC_U32_OFFSET: u8 = 2; +pub const TC_U32_VAROFFSET: u8 = 4; +pub const TC_U32_EAT: u8 = 8; +pub const TC_U32_MAXDEPTH: u8 = 8; + +/// Action attributes +pub const TCA_ACT_UNSPEC: u16 = 0; +pub const TCA_ACT_KIND: u16 = 1; +pub const TCA_ACT_OPTIONS: u16 = 2; +pub const TCA_ACT_INDEX: u16 = 3; +pub const TCA_ACT_STATS: u16 = 4; +pub const TCA_ACT_PAD: u16 = 5; +pub const TCA_ACT_COOKIE: u16 = 6; + +//TODO(wllenyj): Why not subtract 1? See `linux/pkt_cls.h` for original definition. +pub const TCA_ACT_MAX: u16 = 7; +pub const TCA_OLD_COMPAT: u16 = TCA_ACT_MAX + 1; +pub const TCA_ACT_MAX_PRIO: u16 = 32; +pub const TCA_ACT_BIND: u16 = 1; +pub const TCA_ACT_NOBIND: u16 = 0; +pub const TCA_ACT_UNBIND: u16 = 1; +pub const TCA_ACT_NOUNBIND: u16 = 0; +pub const TCA_ACT_REPLACE: u16 = 1; +pub const TCA_ACT_NOREPLACE: u16 = 0; + +pub const TC_ACT_UNSPEC: i32 = -1; +pub const TC_ACT_OK: i32 = 0; +pub const TC_ACT_RECLASSIFY: i32 = 1; +pub const TC_ACT_SHOT: i32 = 2; +pub const TC_ACT_PIPE: i32 = 3; +pub const TC_ACT_STOLEN: i32 = 4; +pub const TC_ACT_QUEUED: i32 = 5; +pub const TC_ACT_REPEAT: i32 = 6; +pub const TC_ACT_REDIRECT: i32 = 7; +pub const TC_ACT_TRAP: i32 = 8; + +pub const TC_ACT_VALUE_MAX: i32 = TC_ACT_TRAP; + +pub const TC_ACT_JUMP: i32 = 0x10000000; + +pub const TCA_ACT_TAB: u16 = 1; // TCA_ROOT_TAB +pub const TCAA_MAX: u16 = 1; + +/// Mirred action attr +pub const TCA_MIRRED_UNSPEC: u16 = 0; +pub const TCA_MIRRED_TM: u16 = 1; +pub const TCA_MIRRED_PARMS: u16 = 2; +pub const TCA_MIRRED_PAD: u16 = 3; +pub const TCA_MIRRED_MAX: u16 = TCA_MIRRED_PAD; + +pub const TCA_EGRESS_REDIR: i32 = 1; /* packet redirect to EGRESS */ +pub const TCA_EGRESS_MIRROR: i32 = 2; /* mirror packet to EGRESS */ +pub const TCA_INGRESS_REDIR: i32 = 3; /* packet redirect to INGRESS */ +pub const TCA_INGRESS_MIRROR: i32 = 4; /* mirror packet to INGRESS */ diff --git a/netlink-packet-route/src/rtnl/tc/message.rs b/netlink-packet-route/src/rtnl/tc/message.rs index da212a7e..ee0a993f 100644 --- a/netlink-packet-route/src/rtnl/tc/message.rs +++ b/netlink-packet-route/src/rtnl/tc/message.rs @@ -3,8 +3,14 @@ use anyhow::Context; use crate::{ - nlas::tc::Nla, - traits::{Emitable, Parseable}, + constants::*, + nlas::{ + tc::{Nla, Stats, Stats2, StatsBuffer, TcOpt}, + DefaultNla, + NlasIterator, + }, + parsers::{parse_string, parse_u8}, + traits::{Emitable, Parseable, ParseableParametrized}, DecodeError, TcMessageBuffer, TC_HEADER_LEN, @@ -24,6 +30,17 @@ impl TcMessage { pub fn from_parts(header: TcHeader, nlas: Vec) -> Self { TcMessage { header, nlas } } + + /// Create a new `TcMessage` with the given index + pub fn with_index(index: i32) -> Self { + Self { + header: TcHeader { + index, + ..Default::default() + }, + nlas: Vec::new(), + } + } } #[derive(Debug, PartialEq, Eq, Clone, Default)] @@ -90,8 +107,52 @@ impl<'a, T: AsRef<[u8]> + 'a> Parseable> for TcMessage { impl<'a, T: AsRef<[u8]> + 'a> Parseable> for Vec { fn parse(buf: &TcMessageBuffer<&'a T>) -> Result { let mut nlas = vec![]; + let mut kind = String::new(); + for nla_buf in buf.nlas() { - nlas.push(Nla::parse(&nla_buf?)?); + let buf = nla_buf.context("invalid tc nla")?; + let payload = buf.value(); + let nla = match buf.kind() { + TCA_UNSPEC => Nla::Unspec(payload.to_vec()), + TCA_KIND => { + kind = parse_string(payload).context("invalid TCA_KIND")?; + Nla::Kind(kind.clone()) + } + TCA_OPTIONS => { + let mut nlas = vec![]; + for nla in NlasIterator::new(payload) { + let nla = nla.context("invalid TCA_OPTIONS")?; + nlas.push( + TcOpt::parse_with_param(&nla, &kind) + .context("failed to parse TCA_OPTIONS")?, + ) + } + Nla::Options(nlas) + } + TCA_STATS => Nla::Stats( + Stats::parse(&StatsBuffer::new_checked(payload).context("invalid TCA_STATS")?) + .context("failed to parse TCA_STATS")?, + ), + TCA_XSTATS => Nla::XStats(payload.to_vec()), + TCA_RATE => Nla::Rate(payload.to_vec()), + TCA_FCNT => Nla::Fcnt(payload.to_vec()), + TCA_STATS2 => { + let mut nlas = vec![]; + for nla in NlasIterator::new(payload) { + let nla = nla.context("invalid TCA_STATS2")?; + nlas.push(Stats2::parse(&nla).context("failed to parse TCA_STATS2")?); + } + Nla::Stats2(nlas) + } + TCA_STAB => Nla::Stab(payload.to_vec()), + TCA_CHAIN => Nla::Chain(payload.to_vec()), + TCA_HW_OFFLOAD => { + Nla::HwOffload(parse_u8(payload).context("failed to parse TCA_HW_OFFLOAD")?) + } + _ => Nla::Other(DefaultNla::parse(&buf).context("failed to parse tc nla")?), + }; + + nlas.push(nla); } Ok(nlas) } diff --git a/netlink-packet-route/src/rtnl/tc/mod.rs b/netlink-packet-route/src/rtnl/tc/mod.rs index 1febf7a9..eec2e384 100644 --- a/netlink-packet-route/src/rtnl/tc/mod.rs +++ b/netlink-packet-route/src/rtnl/tc/mod.rs @@ -1,7 +1,11 @@ // SPDX-License-Identifier: MIT mod buffer; +pub mod constants; mod message; pub mod nlas; pub use self::{buffer::*, message::*, nlas::*}; + +#[cfg(test)] +mod test; diff --git a/netlink-packet-route/src/rtnl/tc/nlas/mod.rs b/netlink-packet-route/src/rtnl/tc/nlas/mod.rs index 6ae7b551..e9ed44f6 100644 --- a/netlink-packet-route/src/rtnl/tc/nlas/mod.rs +++ b/netlink-packet-route/src/rtnl/tc/nlas/mod.rs @@ -9,10 +9,15 @@ pub use self::stats_queue::*; mod stats_basic; pub use self::stats_basic::*; +mod options; +pub use self::options::*; + +mod qdisc; +pub use self::qdisc::*; + use crate::{ constants::*, - nlas::{self, DefaultNla, NlaBuffer, NlasIterator}, - parsers::{parse_string, parse_u8}, + nlas::{self, DefaultNla, NlaBuffer}, traits::{Emitable, Parseable}, DecodeError, }; @@ -23,9 +28,9 @@ pub enum Nla { Unspec(Vec), /// Name of queueing discipline Kind(String), - /// Qdisc-specific options follow - Options(Vec), - /// Qdisc statistics + /// Options follow + Options(Vec), + /// Statistics Stats(Stats), /// Module-specific statistics XStats(Vec), @@ -45,20 +50,15 @@ impl nlas::Nla for Nla { use self::Nla::*; match *self { // Vec - Unspec(ref bytes) - | Options(ref bytes) - | XStats(ref bytes) - | Rate(ref bytes) - | Fcnt(ref bytes) - | Stab(ref bytes) - | Chain(ref bytes) => bytes.len(), + Unspec(ref bytes) | XStats(ref bytes) | Rate(ref bytes) | Fcnt(ref bytes) + | Stab(ref bytes) | Chain(ref bytes) => bytes.len(), HwOffload(_) => 1, Stats2(ref thing) => thing.as_slice().buffer_len(), Stats(_) => STATS_LEN, Kind(ref string) => string.as_bytes().len() + 1, - + Options(ref opt) => opt.as_slice().buffer_len(), // Defaults - Other(ref attr) => attr.value_len(), + Other(ref attr) => attr.value_len(), } } @@ -68,7 +68,6 @@ impl nlas::Nla for Nla { match *self { // Vec Unspec(ref bytes) - | Options(ref bytes) | XStats(ref bytes) | Rate(ref bytes) | Fcnt(ref bytes) @@ -83,6 +82,7 @@ impl nlas::Nla for Nla { buffer[..string.as_bytes().len()].copy_from_slice(string.as_bytes()); buffer[string.as_bytes().len()] = 0; } + Options(ref opt) => opt.as_slice().emit(buffer), // Default Other(ref attr) => attr.emit_value(buffer), @@ -108,32 +108,6 @@ impl nlas::Nla for Nla { } } -impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> for Nla { - fn parse(buf: &NlaBuffer<&'a T>) -> Result { - let payload = buf.value(); - Ok(match buf.kind() { - TCA_UNSPEC => Self::Unspec(payload.to_vec()), - TCA_KIND => Self::Kind(parse_string(payload)?), - TCA_OPTIONS => Self::Options(payload.to_vec()), - TCA_STATS => Self::Stats(Stats::parse(&StatsBuffer::new_checked(payload)?)?), - TCA_XSTATS => Self::XStats(payload.to_vec()), - TCA_RATE => Self::Rate(payload.to_vec()), - TCA_FCNT => Self::Fcnt(payload.to_vec()), - TCA_STATS2 => { - let mut nlas = vec![]; - for nla in NlasIterator::new(payload) { - nlas.push(Stats2::parse(&(nla?))?); - } - Self::Stats2(nlas) - } - TCA_STAB => Self::Stab(payload.to_vec()), - TCA_CHAIN => Self::Chain(payload.to_vec()), - TCA_HW_OFFLOAD => Self::HwOffload(parse_u8(payload)?), - _ => Self::Other(DefaultNla::parse(buf)?), - }) - } -} - #[derive(Debug, PartialEq, Eq, Clone)] pub enum Stats2 { StatsApp(Vec), diff --git a/netlink-packet-route/src/rtnl/tc/nlas/options.rs b/netlink-packet-route/src/rtnl/tc/nlas/options.rs new file mode 100644 index 00000000..51efc09d --- /dev/null +++ b/netlink-packet-route/src/rtnl/tc/nlas/options.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +use crate::{ + nlas::{self, DefaultNla, NlaBuffer}, + tc::ingress, + traits::{Parseable, ParseableParametrized}, + DecodeError, +}; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum TcOpt { + // Qdisc specific options + Ingress, + // Other options + Other(DefaultNla), +} + +impl nlas::Nla for TcOpt { + fn value_len(&self) -> usize { + match self { + Self::Ingress => 0, + Self::Other(o) => o.value_len(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + Self::Ingress => unreachable!(), + Self::Other(o) => o.emit_value(buffer), + } + } + + fn kind(&self) -> u16 { + match self { + Self::Ingress => unreachable!(), + Self::Other(o) => o.kind(), + } + } +} + +impl<'a, T, S> ParseableParametrized, S> for TcOpt +where + T: AsRef<[u8]> + ?Sized, + S: AsRef, +{ + fn parse_with_param(buf: &NlaBuffer<&'a T>, kind: S) -> Result { + Ok(match kind.as_ref() { + ingress::KIND => TcOpt::Ingress, + _ => Self::Other(DefaultNla::parse(buf)?), + }) + } +} diff --git a/netlink-packet-route/src/rtnl/tc/nlas/qdisc/mod.rs b/netlink-packet-route/src/rtnl/tc/nlas/qdisc/mod.rs new file mode 100644 index 00000000..962fd567 --- /dev/null +++ b/netlink-packet-route/src/rtnl/tc/nlas/qdisc/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT + +pub mod ingress { + pub const KIND: &str = "ingress"; +} diff --git a/netlink-packet-route/src/rtnl/tc/nlas/qdisc/prio.rs b/netlink-packet-route/src/rtnl/tc/nlas/qdisc/prio.rs new file mode 100644 index 00000000..f25c4213 --- /dev/null +++ b/netlink-packet-route/src/rtnl/tc/nlas/qdisc/prio.rs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +//#[derive(Debug, PartialEq, Eq, Clone)] +//pub enum Qdisc { +// Prio(Prio), +// Ingress, +//} +// +//pub const TC_PRIO_MAX: usize = 15; +//#[derive(Debug, PartialEq, Eq, Clone)] +//pub struct Prio { +// // Number of bands +// bands: i32, +// // Map: logical priority -> PRIO band +// priomap: [u8; TC_PRIO_MAX + 1], +//} diff --git a/netlink-packet-route/src/rtnl/tc/test.rs b/netlink-packet-route/src/rtnl/tc/test.rs new file mode 100644 index 00000000..482cb7a5 --- /dev/null +++ b/netlink-packet-route/src/rtnl/tc/test.rs @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT + +#![cfg(test)] + +use crate::{ + constants::*, + nlas::NlasIterator, + tc::{ingress, Nla, Stats, Stats2, StatsBuffer, TC_HEADER_LEN}, + traits::{Emitable, Parseable}, + TcHeader, + TcMessage, + TcMessageBuffer, +}; + +#[rustfmt::skip] + static QDISC_INGRESS_PACKET: [u8; 136] = [ + 0, // family + 0, 0, 0, // pad1 + pad2 + 84, 0, 0, 0, // Interface index = 84 + 0, 0, 255, 255, // handle: 0xffff0000 + 241, 255, 255, 255, // parent: 0xfffffff1 + 1, 0, 0, 0, // info: refcnt: 1 + + // nlas + 12, 0, // length + 1, 0, // type: TCA_KIND + 105, 110, 103, 114, 101, 115, 115, 0, // ingress\0 + + 4, 0, // length + 2, 0, // type: TCA_OPTIONS + + 5, 0, // length + 12, 0,// type: TCA_HW_OFFLOAD + 0, // data: 0 + 0, 0, 0,// padding + + 48, 0, // length + 7, 0, // type: TCA_STATS2 + 20, 0, // length + 1, 0, // type: TCA_STATS_BASIC + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 24, 0, + 3, 0, // type: TCA_STATS_QUEUE + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + + 44, 0, // length + 3, 0, // type: TCA_STATS + 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, 0, 0, 0, 0, 0, 0, 0, 0 + ]; + +#[test] +fn tc_packet_header_read() { + let packet = TcMessageBuffer::new(QDISC_INGRESS_PACKET); + assert_eq!(packet.family(), 0); + assert_eq!(packet.index(), 84); + assert_eq!(packet.handle(), 0xffff0000); + assert_eq!(packet.parent(), 0xfffffff1); + assert_eq!(packet.info(), 1); +} + +#[test] +fn tc_packet_header_build() { + let mut buf = vec![0xff; TC_HEADER_LEN]; + { + let mut packet = TcMessageBuffer::new(&mut buf); + packet.set_family(0); + packet.set_pad1(0); + packet.set_pad2(0); + packet.set_index(84); + packet.set_handle(0xffff0000); + packet.set_parent(0xfffffff1); + packet.set_info(1); + } + assert_eq!(&buf[..], &QDISC_INGRESS_PACKET[0..TC_HEADER_LEN]); +} + +#[test] +fn tc_packet_nlas_read() { + let packet = TcMessageBuffer::new(&QDISC_INGRESS_PACKET[..]); + assert_eq!(packet.nlas().count(), 5); + let mut nlas = packet.nlas(); + + let nla = nlas.next().unwrap().unwrap(); + nla.check_buffer_length().unwrap(); + assert_eq!(nla.length(), 12); + assert_eq!(nla.kind(), TCA_KIND); + assert_eq!(nla.value(), "ingress\0".as_bytes()); + + let nla = nlas.next().unwrap().unwrap(); + nla.check_buffer_length().unwrap(); + assert_eq!(nla.length(), 4); + assert_eq!(nla.kind(), TCA_OPTIONS); + assert_eq!(nla.value(), []); + + let nla = nlas.next().unwrap().unwrap(); + nla.check_buffer_length().unwrap(); + assert_eq!(nla.length(), 5); + assert_eq!(nla.kind(), TCA_HW_OFFLOAD); + assert_eq!(nla.value(), [0]); + + let nla = nlas.next().unwrap().unwrap(); + nla.check_buffer_length().unwrap(); + assert_eq!(nla.length(), 48); + assert_eq!(nla.kind(), TCA_STATS2); + + let mut stats2_iter = NlasIterator::new(nla.value()); + let stats2_nla = stats2_iter.next().unwrap().unwrap(); + stats2_nla.check_buffer_length().unwrap(); + assert_eq!(stats2_nla.length(), 20); + assert_eq!(stats2_nla.kind(), TCA_STATS_BASIC); + assert_eq!(stats2_nla.value(), [0; 16]); + let s2 = Stats2::parse(&stats2_nla).unwrap(); + assert!(matches!(s2, Stats2::StatsBasic(_))); + + let stats2_nla = stats2_iter.next().unwrap().unwrap(); + stats2_nla.check_buffer_length().unwrap(); + assert_eq!(stats2_nla.length(), 24); + assert_eq!(stats2_nla.kind(), TCA_STATS_QUEUE); + assert_eq!(stats2_nla.value(), [0; 20]); + let s2 = Stats2::parse(&stats2_nla).unwrap(); + assert!(matches!(s2, Stats2::StatsQueue(_))); + + let nla = nlas.next().unwrap().unwrap(); + nla.check_buffer_length().unwrap(); + assert_eq!(nla.length(), 44); + assert_eq!(nla.kind(), TCA_STATS); + assert_eq!(nla.value(), [0; 40]); + let s = Stats::parse(&StatsBuffer::new(nla.value())).unwrap(); + assert_eq!(s.packets, 0); + assert_eq!(s.backlog, 0); +} + +#[test] +fn tc_qdisc_ingress_emit() { + let mut header = TcHeader::default(); + header.index = 84; + header.handle = 0xffff0000; + header.parent = 0xfffffff1; + header.info = 1; + + let nlas = vec![Nla::Kind(ingress::KIND.into()), Nla::Options(vec![])]; + + let msg = TcMessage::from_parts(header, nlas); + let mut buf = vec![0; 36]; + assert_eq!(msg.buffer_len(), 36); + msg.emit(&mut buf[..]); + assert_eq!(&buf, &QDISC_INGRESS_PACKET[..36]); +} + +#[test] +fn tc_qdisc_ingress_read() { + let packet = TcMessageBuffer::new_checked(&QDISC_INGRESS_PACKET).unwrap(); + + let msg = TcMessage::parse(&packet).unwrap(); + assert_eq!(msg.header.index, 84); + assert_eq!(msg.nlas.len(), 5); + + let mut iter = msg.nlas.iter(); + + let nla = iter.next().unwrap(); + assert_eq!(nla, &Nla::Kind(String::from(ingress::KIND))); + + let nla = iter.next().unwrap(); + assert_eq!(nla, &Nla::Options(vec![])); + + let nla = iter.next().unwrap(); + assert_eq!(nla, &Nla::HwOffload(0)); +} diff --git a/rtnetlink/examples/add_tc_qdisc_ingress.rs b/rtnetlink/examples/add_tc_qdisc_ingress.rs new file mode 100644 index 00000000..ffb02130 --- /dev/null +++ b/rtnetlink/examples/add_tc_qdisc_ingress.rs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +use std::env; + +use rtnetlink::new_connection; + +#[tokio::main] +async fn main() -> Result<(), ()> { + env_logger::init(); + let args: Vec = env::args().collect(); + if args.len() != 2 { + usage(); + return Ok(()); + } + + let index: u32 = args[1].parse().unwrap_or_else(|_| { + eprintln!("invalid index"); + std::process::exit(1); + }); + + let (connection, handle, _) = new_connection().unwrap(); + tokio::spawn(connection); + + if let Err(e) = handle.qdisc().add(index as i32).ingress().execute().await { + eprintln!("{}", e); + } + + Ok(()) +} + +fn usage() { + eprintln!( + "usage: + cargo run --example add_tc_qdisc_ingress -- + +Note that you need to run this program as root. Instead of running cargo as root, +build the example normally: + + cd rtnetlink ; cargo build --example add_tc_qdisc_ingress + +Then find the binary in the target directory: + + cd ../target/debug/example ; sudo ./add_tc_qdisc_ingress " + ); +} diff --git a/rtnetlink/src/traffic_control/add_qdisc.rs b/rtnetlink/src/traffic_control/add_qdisc.rs new file mode 100644 index 00000000..d19d8c7c --- /dev/null +++ b/rtnetlink/src/traffic_control/add_qdisc.rs @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MIT + +use futures::stream::StreamExt; + +use crate::{ + packet::{ + tc::{constants::*, nlas}, + NetlinkMessage, + RtnlMessage, + TcMessage, + NLM_F_ACK, + NLM_F_REQUEST, + TC_H_MAKE, + }, + try_nl, + Error, + Handle, +}; + +pub struct QDiscNewRequest { + handle: Handle, + message: TcMessage, + flags: u16, +} + +impl QDiscNewRequest { + pub(crate) fn new(handle: Handle, message: TcMessage, flags: u16) -> Self { + Self { + handle, + message, + flags: NLM_F_REQUEST | flags, + } + } + + /// Execute the request + pub async fn execute(self) -> Result<(), Error> { + let Self { + mut handle, + message, + flags, + } = self; + + let mut req = NetlinkMessage::from(RtnlMessage::NewQueueDiscipline(message)); + req.header.flags = NLM_F_ACK | flags; + + let mut response = handle.request(req)?; + while let Some(message) = response.next().await { + try_nl!(message); + } + Ok(()) + } + + /// Set handle, + pub fn handle(mut self, maj: u16, min: u16) -> Self { + self.message.header.handle = TC_H_MAKE!((maj as u32) << 16, min as u32); + self + } + + /// Set parent to root. + pub fn root(mut self) -> Self { + assert_eq!(self.message.header.parent, TC_H_UNSPEC); + self.message.header.parent = TC_H_ROOT; + self + } + + /// Set parent + pub fn parent(mut self, parent: u32) -> Self { + assert_eq!(self.message.header.parent, TC_H_UNSPEC); + self.message.header.parent = parent; + self + } + + /// New a ingress qdisc + pub fn ingress(mut self) -> Self { + assert_eq!(self.message.header.parent, TC_H_UNSPEC); + self.message.header.parent = TC_H_INGRESS; + self.message.header.handle = 0xffff0000; + self.message + .nlas + .push(nlas::Nla::Kind("ingress".to_string())); + self + } +} + +#[cfg(test)] +mod test { + use std::{fs::File, os::unix::io::AsRawFd, path::Path}; + + use futures::stream::TryStreamExt; + use nix::sched::{setns, CloneFlags}; + use tokio::runtime::Runtime; + + use super::*; + use crate::{ + new_connection, + packet::{ + rtnl::tc::nlas::Nla::{HwOffload, Kind}, + LinkMessage, + AF_UNSPEC, + }, + NetworkNamespace, + NETNS_PATH, + SELF_NS_PATH, + }; + + const TEST_NS: &str = "netlink_test_qdisc_ns"; + const TEST_DUMMY: &str = "test_dummy"; + + struct Netns { + path: String, + _cur: File, + last: File, + } + + impl Netns { + async fn new(path: &str) -> Self { + // record current ns + let last = File::open(Path::new(SELF_NS_PATH)).unwrap(); + + // create new ns + NetworkNamespace::add(path.to_string()).await.unwrap(); + + // entry new ns + let ns_path = Path::new(NETNS_PATH); + let file = File::open(ns_path.join(path)).unwrap(); + setns(file.as_raw_fd(), CloneFlags::CLONE_NEWNET).unwrap(); + + Self { + path: path.to_string(), + _cur: file, + last, + } + } + } + impl Drop for Netns { + fn drop(&mut self) { + println!("exit ns: {}", self.path); + setns(self.last.as_raw_fd(), CloneFlags::CLONE_NEWNET).unwrap(); + + let ns_path = Path::new(NETNS_PATH).join(&self.path); + nix::mount::umount2(&ns_path, nix::mount::MntFlags::MNT_DETACH).unwrap(); + nix::unistd::unlink(&ns_path).unwrap(); + // _cur File will be closed auto + // Since there is no async drop, NetworkNamespace::del cannot be called + // here. Dummy interface will be deleted automatically after netns is + // deleted. + } + } + + async fn setup_env() -> (Handle, LinkMessage, Netns) { + let netns = Netns::new(TEST_NS).await; + + // Notice: The Handle can only be created after the setns, so that the + // Handle is the connection within the new ns. + let (connection, handle, _) = new_connection().unwrap(); + tokio::spawn(connection); + handle + .link() + .add() + .dummy(TEST_DUMMY.to_string()) + .execute() + .await + .unwrap(); + let mut links = handle + .link() + .get() + .match_name(TEST_DUMMY.to_string()) + .execute(); + let link = links.try_next().await.unwrap(); + (handle, link.unwrap(), netns) + } + + async fn test_async_new_qdisc() { + let (handle, test_link, _netns) = setup_env().await; + handle + .qdisc() + .add(test_link.header.index as i32) + .ingress() + .execute() + .await + .unwrap(); + let mut qdiscs_iter = handle + .qdisc() + .get() + .index(test_link.header.index as i32) + .ingress() + .execute(); + + let mut found = false; + while let Some(nl_msg) = qdiscs_iter.try_next().await.unwrap() { + if nl_msg.header.index == test_link.header.index as i32 + && nl_msg.header.handle == 0xffff0000 + { + assert_eq!(nl_msg.header.family, AF_UNSPEC as u8); + assert_eq!(nl_msg.header.handle, 0xffff0000); + assert_eq!(nl_msg.header.parent, TC_H_INGRESS); + assert_eq!(nl_msg.header.info, 1); // refcount + assert_eq!(nl_msg.nlas[0], Kind("ingress".to_string())); + assert_eq!(nl_msg.nlas[2], HwOffload(0)); + found = true; + break; + } + } + if !found { + panic!("not found dev:{} qdisc.", test_link.header.index); + } + } + + #[test] + fn test_new_qdisc() { + Runtime::new().unwrap().block_on(test_async_new_qdisc()); + } +} diff --git a/rtnetlink/src/traffic_control/del_qdisc.rs b/rtnetlink/src/traffic_control/del_qdisc.rs new file mode 100644 index 00000000..26125549 --- /dev/null +++ b/rtnetlink/src/traffic_control/del_qdisc.rs @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +use futures::StreamExt; + +use crate::{ + packet::{NetlinkMessage, RtnlMessage, TcMessage, NLM_F_ACK, NLM_F_REQUEST}, + try_nl, + Error, + Handle, +}; + +pub struct QDiscDelRequest { + handle: Handle, + message: TcMessage, +} + +impl QDiscDelRequest { + pub(crate) fn new(handle: Handle, message: TcMessage) -> Self { + QDiscDelRequest { handle, message } + } + + // Execute the request + pub async fn execute(self) -> Result<(), Error> { + let QDiscDelRequest { + mut handle, + message, + } = self; + + let mut req = NetlinkMessage::from(RtnlMessage::DelQueueDiscipline(message)); + req.header.flags = NLM_F_REQUEST | NLM_F_ACK; + + let mut response = handle.request(req)?; + while let Some(message) = response.next().await { + try_nl!(message) + } + Ok(()) + } + + /// Return a mutable reference to the request + pub fn message_mut(&mut self) -> &mut TcMessage { + &mut self.message + } +} diff --git a/rtnetlink/src/traffic_control/get.rs b/rtnetlink/src/traffic_control/get.rs index 48b7cdb9..f0673054 100644 --- a/rtnetlink/src/traffic_control/get.rs +++ b/rtnetlink/src/traffic_control/get.rs @@ -7,7 +7,7 @@ use futures::{ }; use crate::{ - packet::{NetlinkMessage, RtnlMessage, TcMessage, NLM_F_DUMP, NLM_F_REQUEST}, + packet::{tc::constants::*, NetlinkMessage, RtnlMessage, TcMessage, NLM_F_DUMP, NLM_F_REQUEST}, try_rtnl, Error, Handle, @@ -43,6 +43,18 @@ impl QDiscGetRequest { Err(e) => Either::Right(future::err::(e).into_stream()), } } + + pub fn index(mut self, index: i32) -> Self { + self.message.header.index = index; + self + } + + /// Get ingress qdisc + pub fn ingress(mut self) -> Self { + assert_eq!(self.message.header.parent, TC_H_UNSPEC); + self.message.header.parent = TC_H_INGRESS; + self + } } pub struct TrafficClassGetRequest { diff --git a/rtnetlink/src/traffic_control/handle.rs b/rtnetlink/src/traffic_control/handle.rs index d5a89526..27bcfd56 100644 --- a/rtnetlink/src/traffic_control/handle.rs +++ b/rtnetlink/src/traffic_control/handle.rs @@ -1,12 +1,18 @@ // SPDX-License-Identifier: MIT use super::{ + QDiscDelRequest, QDiscGetRequest, + QDiscNewRequest, TrafficChainGetRequest, TrafficClassGetRequest, TrafficFilterGetRequest, }; -use crate::Handle; + +use crate::{ + packet::{TcMessage, NLM_F_CREATE, NLM_F_EXCL, NLM_F_REPLACE}, + Handle, +}; pub struct QDiscHandle(Handle); @@ -19,6 +25,41 @@ impl QDiscHandle { pub fn get(&mut self) -> QDiscGetRequest { QDiscGetRequest::new(self.0.clone()) } + + /// Create a new qdisc, don't replace if the object already exists. + /// ( equivalent to `tc qdisc add dev STRING`) + pub fn add(&mut self, index: i32) -> QDiscNewRequest { + let msg = TcMessage::with_index(index); + QDiscNewRequest::new(self.0.clone(), msg, NLM_F_EXCL | NLM_F_CREATE) + } + + /// Change the qdisc, the handle cannot be changed and neither can the parent. + /// In other words, change cannot move a node. + /// ( equivalent to `tc qdisc change dev STRING`) + pub fn change(&mut self, index: i32) -> QDiscNewRequest { + let msg = TcMessage::with_index(index); + QDiscNewRequest::new(self.0.clone(), msg, 0) + } + + /// Replace existing matching qdisc, create qdisc if it doesn't already exist. + /// ( equivalent to `tc qdisc replace dev STRING`) + pub fn replace(&mut self, index: i32) -> QDiscNewRequest { + let msg = TcMessage::with_index(index); + QDiscNewRequest::new(self.0.clone(), msg.into(), NLM_F_CREATE | NLM_F_REPLACE) + } + + /// Performs a replace where the node must exist already. + /// ( equivalent to `tc qdisc link dev STRING`) + pub fn link(&mut self, index: i32) -> QDiscNewRequest { + let msg = TcMessage::with_index(index); + QDiscNewRequest::new(self.0.clone(), msg, NLM_F_REPLACE) + } + + /// Delete the qdisc ( equivalent to `tc qdisc del dev STRING`) + pub fn del(&mut self, index: i32) -> QDiscDelRequest { + let msg = TcMessage::with_index(index); + QDiscDelRequest::new(self.0.clone(), msg) + } } pub struct TrafficClassHandle { diff --git a/rtnetlink/src/traffic_control/mod.rs b/rtnetlink/src/traffic_control/mod.rs index 28586219..58a90306 100644 --- a/rtnetlink/src/traffic_control/mod.rs +++ b/rtnetlink/src/traffic_control/mod.rs @@ -6,5 +6,11 @@ pub use self::handle::*; mod get; pub use self::get::*; +mod add_qdisc; +pub use self::add_qdisc::*; + +mod del_qdisc; +pub use self::del_qdisc::*; + #[cfg(test)] mod test;