diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..8af59dd --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +RUST_TEST_THREADS = "1" diff --git a/src/handle.rs b/src/handle.rs index 08de353..1272236 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -7,6 +7,7 @@ use netlink_proto::{sys::SocketAddr, ConnectionHandle}; use crate::{ AddressHandle, Error, LinkHandle, NeighbourHandle, RouteHandle, RuleHandle, + TrafficActionHandle, }; #[cfg(not(target_os = "freebsd"))] use crate::{ @@ -98,4 +99,11 @@ impl Handle { pub fn traffic_chain(&self, ifindex: i32) -> TrafficChainHandle { TrafficChainHandle::new(self.clone(), ifindex) } + + /// Create a new handle, specifically for traffic control action requests + /// (equivalent to `tc actions list action ` commands) + #[cfg(not(target_os = "freebsd"))] + pub fn traffic_action(&self) -> TrafficActionHandle { + TrafficActionHandle::new(self.clone()) + } } diff --git a/src/traffic_control/add_action.rs b/src/traffic_control/add_action.rs new file mode 100644 index 0000000..eafc303 --- /dev/null +++ b/src/traffic_control/add_action.rs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT + +use futures::future::Either; +use futures::{future, FutureExt, StreamExt, TryStream}; +use netlink_packet_core::{ + NetlinkMessage, NLM_F_ACK, NLM_F_EXCL, NLM_F_REQUEST, +}; +use netlink_packet_route::tc::{ + TcAction, TcActionMessage, TcActionMessageAttribute, +}; +use netlink_packet_route::RouteNetlinkMessage; +use nix::libc::RTM_NEWACTION; + +use crate::{try_rtnl, Error, Handle}; + +/// A request to add a new traffic control action +#[derive(Debug, Clone)] +#[must_use = "builder"] +pub struct TrafficActionNewRequest { + handle: Handle, + message: TcActionMessage, +} + +impl TrafficActionNewRequest { + pub(crate) fn new(handle: Handle) -> Self { + Self { + handle, + message: TcActionMessage::default(), + } + } + + /// Specifies the action to add + pub fn action(mut self, action: TcAction) -> Self { + self.message + .attributes + .push(TcActionMessageAttribute::Actions(vec![action])); + self + } + + /// Execute the request + #[must_use = "builder"] + pub fn execute( + self, + ) -> impl TryStream { + let Self { + mut handle, + message, + } = self; + + let mut req = NetlinkMessage::from( + RouteNetlinkMessage::NewTrafficAction(message), + ); + req.header.message_type = RTM_NEWACTION; + req.header.flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_EXCL; + + match handle.request(req) { + Ok(response) => Either::Left(response.map(move |msg| { + Ok(try_rtnl!(msg, RouteNetlinkMessage::NewTrafficAction)) + })), + Err(err) => Either::Right( + future::err::(err).into_stream(), + ), + } + } +} diff --git a/src/traffic_control/del_action.rs b/src/traffic_control/del_action.rs new file mode 100644 index 0000000..e45176d --- /dev/null +++ b/src/traffic_control/del_action.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT + +use futures::StreamExt; +use netlink_packet_core::{NetlinkMessage, NLM_F_ACK, NLM_F_REQUEST}; +use netlink_packet_route::tc::{ + TcAction, TcActionMessage, TcActionMessageAttribute, +}; +use netlink_packet_route::RouteNetlinkMessage; + +use crate::{try_nl, Error, Handle}; + +/// A request to delete a traffic control action +#[must_use] +pub struct TrafficActionDelRequest { + handle: Handle, + message: TcActionMessage, +} + +impl TrafficActionDelRequest { + pub(crate) fn new(handle: Handle) -> Self { + TrafficActionDelRequest { + handle, + message: TcActionMessage::default(), + } + } + + /// Specifies the action to delete + pub fn action(mut self, action: TcAction) -> Self { + self.message + .attributes + .push(TcActionMessageAttribute::Actions(vec![action])); + self + } + + /// Execute the request + pub async fn execute(self) -> Result<(), Error> { + let TrafficActionDelRequest { + mut handle, + message, + } = self; + + let mut req = NetlinkMessage::from( + RouteNetlinkMessage::DelTrafficAction(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 TcActionMessage { + &mut self.message + } +} diff --git a/src/traffic_control/get.rs b/src/traffic_control/get.rs index 01cb262..fc80990 100644 --- a/src/traffic_control/get.rs +++ b/src/traffic_control/get.rs @@ -1,14 +1,17 @@ // SPDX-License-Identifier: MIT - use futures::{ future::{self, Either}, stream::{StreamExt, TryStream}, FutureExt, }; use netlink_packet_core::{NetlinkMessage, NLM_F_DUMP, NLM_F_REQUEST}; +use netlink_packet_route::tc::{ + TcAction, TcActionAttribute, TcActionMessage, TcActionMessageAttribute, + TcActionMessageFlags, TcActionMessageFlagsWithSelector, +}; use netlink_packet_route::{ tc::{TcHandle, TcMessage}, - RouteNetlinkMessage, + AddressFamily, RouteNetlinkMessage, }; use crate::{try_rtnl, Error, Handle}; @@ -168,3 +171,101 @@ impl TrafficChainGetRequest { } } } + +/// Request to retrieve traffic actions from the kernel. +/// Equivalent to +/// +/// ```bash +/// tc actions list action $action_type +/// ``` +#[derive(Debug, Clone)] +#[must_use] +pub struct TrafficActionGetRequest { + handle: Handle, + message: TcActionMessage, +} + +/// The kind of traffic action. +/// +/// This is a list of known traffic actions. +/// If the kernel returns an unknown action, it will be represented as +/// `Other(String)`. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum TrafficActionKind { + /// Used for mirroring and redirecting packets. + Mirror, + /// Used for network address translation. + Nat, + /// Other action type not yet directly supported by this library. + Other(String), +} + +impl> From for TrafficActionKind { + fn from(kind: T) -> Self { + match kind.as_ref() { + "mirred" => TrafficActionKind::Mirror, + "nat" => TrafficActionKind::Nat, + _ => TrafficActionKind::Other(kind.as_ref().into()), + } + } +} + +impl From for String { + fn from(kind: TrafficActionKind) -> Self { + match kind { + TrafficActionKind::Mirror => "mirred".into(), + TrafficActionKind::Nat => "nat".into(), + TrafficActionKind::Other(kind) => kind, + } + } +} + +impl TrafficActionGetRequest { + pub(crate) fn new(handle: Handle) -> Self { + let mut message = TcActionMessage::default(); + message.header.family = AddressFamily::Unspec; + let flags = TcActionMessageAttribute::Flags( + TcActionMessageFlagsWithSelector::new( + TcActionMessageFlags::LargeDump, + ), + ); + message.attributes.push(flags); + Self { handle, message } + } + + /// Specify the kind of the action to retrieve. + pub fn kind>(mut self, kind: T) -> Self { + let mut tc_action = TcAction::default(); + tc_action + .attributes + .push(TcActionAttribute::Kind(String::from(kind.into()))); + let acts = TcActionMessageAttribute::Actions(vec![tc_action]); + self.message.attributes.push(acts); + self + } + + /// Execute the request + #[must_use] + pub fn execute( + self, + ) -> impl TryStream { + let Self { + mut handle, + message, + } = self; + + let mut req = NetlinkMessage::from( + RouteNetlinkMessage::GetTrafficAction(message), + ); + req.header.flags = NLM_F_REQUEST | NLM_F_DUMP; + + match handle.request(req) { + Ok(response) => Either::Left(response.map(move |msg| { + Ok(try_rtnl!(msg, RouteNetlinkMessage::GetTrafficAction)) + })), + Err(e) => Either::Right( + future::err::(e).into_stream(), + ), + } + } +} diff --git a/src/traffic_control/handle.rs b/src/traffic_control/handle.rs index 3d0b24a..cc4e90f 100644 --- a/src/traffic_control/handle.rs +++ b/src/traffic_control/handle.rs @@ -1,14 +1,18 @@ // SPDX-License-Identifier: MIT +use netlink_packet_core::{NLM_F_CREATE, NLM_F_EXCL, NLM_F_REPLACE}; +use netlink_packet_route::tc::TcMessage; + +use crate::{ + Handle, TrafficActionDelRequest, TrafficActionGetRequest, + TrafficActionNewRequest, +}; + use super::{ QDiscDelRequest, QDiscGetRequest, QDiscNewRequest, TrafficChainGetRequest, TrafficClassGetRequest, TrafficFilterGetRequest, TrafficFilterNewRequest, }; -use crate::Handle; -use netlink_packet_core::{NLM_F_CREATE, NLM_F_EXCL, NLM_F_REPLACE}; -use netlink_packet_route::tc::TcMessage; - pub struct QDiscHandle(Handle); impl QDiscHandle { @@ -134,3 +138,46 @@ impl TrafficChainHandle { TrafficChainGetRequest::new(self.handle.clone(), self.ifindex) } } + +#[non_exhaustive] +pub struct TrafficActionHandle { + handle: Handle, +} + +impl TrafficActionHandle { + /// Create a new traffic action manipulation handle. + pub fn new(handle: Handle) -> Self { + TrafficActionHandle { handle } + } + + /// Retrieve the list of actions. + /// + /// Equivalent to + /// + /// ```bash + /// tc action list actions $act_type + /// ``` + pub fn get(&mut self) -> TrafficActionGetRequest { + TrafficActionGetRequest::new(self.handle.clone()) + } + + /// Add a new action. + /// + /// Equivalent to + /// ```bash + /// tc action add action $act_type $act_options index $index + /// ``` + pub fn add(&mut self) -> TrafficActionNewRequest { + TrafficActionNewRequest::new(self.handle.clone()) + } + + /// Delete an action. + /// + /// Equivalent to + /// ```bash + /// tc action del action $act_type index $index + /// ``` + pub fn del(&mut self) -> TrafficActionDelRequest { + TrafficActionDelRequest::new(self.handle.clone()) + } +} diff --git a/src/traffic_control/mod.rs b/src/traffic_control/mod.rs index 132b473..84f6a0c 100644 --- a/src/traffic_control/mod.rs +++ b/src/traffic_control/mod.rs @@ -1,19 +1,31 @@ // SPDX-License-Identifier: MIT -mod handle; +//! Traffic control manipulation utilities. +//! See [`tc`]. +//! +//! [`tc`]: https://man7.org/linux/man-pages/man8/tc.8.html + +pub use self::add_action::*; +pub use self::add_filter::*; +pub use self::add_qdisc::*; +pub use self::del_action::*; +pub use self::del_qdisc::*; +pub use self::get::*; pub use self::handle::*; +mod handle; + mod get; -pub use self::get::*; mod add_qdisc; -pub use self::add_qdisc::*; mod del_qdisc; -pub use self::del_qdisc::*; mod add_filter; -pub use self::add_filter::*; + +mod del_action; + +mod add_action; #[cfg(test)] mod test; diff --git a/src/traffic_control/test.rs b/src/traffic_control/test.rs index 02c95f5..acdb405 100644 --- a/src/traffic_control/test.rs +++ b/src/traffic_control/test.rs @@ -1,16 +1,27 @@ // SPDX-License-Identifier: MIT +use std::net::Ipv4Addr; use std::process::Command; use futures::stream::TryStreamExt; use netlink_packet_core::ErrorMessage; +use netlink_packet_route::tc::TcActionType::Stolen; +use netlink_packet_route::tc::TcMirrorActionType::EgressRedir; +use netlink_packet_route::tc::{ + TcAction, TcActionAttribute, TcActionMessage, TcActionMessageAttribute, + TcActionMirrorOption, TcActionNatOption, TcActionOption, TcActionType, + TcMirror, TcNat, TcNatFlags, +}; use netlink_packet_route::{ tc::{TcAttribute, TcMessage}, AddressFamily, }; use tokio::runtime::Runtime; -use crate::{new_connection, Error::NetlinkError}; +use crate::{ + new_connection, Error::NetlinkError, TrafficActionGetRequest, + TrafficActionKind, TrafficActionNewRequest, +}; static TEST_DUMMY_NIC: &str = "netlink-test"; @@ -297,3 +308,434 @@ fn test_get_traffic_classes_filters_and_chains() { assert_eq!(chains[0].attributes[0], TcAttribute::Chain(0),); } } + +async fn _get_actions(kind: TrafficActionKind) -> Vec { + let (connection, handle, _) = new_connection().unwrap(); + tokio::spawn(connection); + let req: TrafficActionGetRequest = handle.traffic_action().get().kind(kind); + let mut actions_iter = req.execute(); + let mut actions = Vec::new(); + while let Some(nl_msg) = actions_iter.try_next().await.unwrap() { + actions.push(nl_msg); + } + actions +} + +fn _flush_test_mirred_action() { + let output = Command::new("tc") + .args(["actions", "flush", "action", "mirred"]) + .output() + .expect("failed to run tc command"); + if !output.status.success() { + eprintln!("Failed to flush mirred actions: {output:?}"); + } + assert!(output.status.success()); +} + +fn _flush_test_nat_action() { + let output = Command::new("tc") + .args(["actions", "flush", "action", "nat"]) + .output() + .expect("failed to run tc command"); + if !output.status.success() { + eprintln!("Failed to flush nat actions: {output:?}"); + } + assert!(output.status.success()); +} + +fn _add_test_mirred_action(index: u32) { + let output = Command::new("tc") + .args([ + "actions", + "add", + "action", + "mirred", + "egress", + "redirect", + "dev", + TEST_DUMMY_NIC, + "index", + format!("{index}").as_str(), + ]) + .output() + .expect("failed to run tc command"); + if !output.status.success() { + eprintln!("Failed to add test mirred action: {output:?}"); + } + assert!(output.status.success()); +} + +fn _add_test_nat_action(index: u32) { + let output = Command::new("tc") + .args([ + "actions", + "add", + "action", + "nat", + "ingress", + "1.2.3.4/32", + "5.6.7.8", + "index", + format!("{index}").as_str(), + ]) + .output() + .expect("failed to run tc command"); + if !output.status.success() { + eprintln!("Failed to add test mirred action: {output:?}"); + } + assert!(output.status.success()); +} + +#[test] +#[cfg_attr(not(feature = "test_as_root"), ignore)] +fn test_get_mirred_actions() { + async fn _test() { + _add_test_dummy_interface(); + // NOTE: this is sketchy if tests run concurrently + _flush_test_mirred_action(); + let index = 1; + _add_test_mirred_action(index); + let actions = _get_actions(TrafficActionKind::Mirror).await; + assert_eq!(actions.len(), 1); + let action = &actions[0]; + assert_eq!(action.header.family, AddressFamily::Unspec); + // Find one and only one `RootCount` attribute and assert that its value + // is 1 + let root_counts: Vec<_> = action + .attributes + .iter() + .filter_map(|attr| match attr { + TcActionMessageAttribute::RootCount(count) => Some(count), + _ => None, + }) + .collect(); + assert_eq!(root_counts.len(), 1); + assert_eq!(root_counts[0], &1); + // Find one and only one `Actions` attribute and assert that it has a + // length of 1 + let action_lists: Vec<_> = action + .attributes + .iter() + .filter_map(|attr| match attr { + TcActionMessageAttribute::Actions(actions) => Some(actions), + _ => None, + }) + .collect(); + assert_eq!(action_lists.len(), 1); + assert_eq!(action_lists[0].len(), 1); + let action = &action_lists[0][0]; + assert_eq!(action.tab, 0); + assert!(action + .attributes + .contains(&TcActionAttribute::Kind("mirred".to_string()))); + let Some(options) = + action.attributes.iter().find_map(|attr| match attr { + TcActionAttribute::Options(options) => Some(options), + _ => None, + }) + else { + eprintln!("{action:?}"); + panic!("No options attribute found in action"); + }; + // Assert that we find only mirror options. + let mirror_options: Vec<_> = options + .iter() + .map(|option| match option { + TcActionOption::Mirror(opt) => opt, + unexpected => { + eprintln!("{unexpected:?}"); + panic!("Unexpected option type found in mirror action"); + } + }) + .collect(); + assert!(!mirror_options.is_empty()); + let Some(mirror_params) = + mirror_options.iter().find_map(|opt| match opt { + TcActionMirrorOption::Parms(params) => Some(params), + _ => None, + }) + else { + eprintln!("{action:?}"); + panic!("No mirror params found in action"); + }; + assert_eq!( + mirror_params.ifindex, + _get_test_dummy_interface_index() as u32 + ); + assert_eq!(mirror_params.eaction, EgressRedir); + assert_eq!(mirror_params.generic.action, Stolen); + assert_eq!(mirror_params.generic.bindcnt, 0); + assert_eq!(mirror_params.generic.index, 1); + assert_eq!(mirror_params.generic.refcnt, 1); + } + Runtime::new().unwrap().block_on(_test()); +} + +/// NOTE: I consider this test to be overly complex in its structure. +/// It seems like we have an API usability problem. +/// I needed to do a fairly large amount of destructuring to get some +/// fairly basic data. +#[test] +#[cfg_attr(not(feature = "test_as_root"), ignore)] +fn test_add_mirror_action() { + async fn _test() { + _add_test_dummy_interface(); + _flush_test_mirred_action(); + let (connection, handle, _) = new_connection().unwrap(); + tokio::spawn(connection); + let index = 99; + let mut tc_action = TcAction::default(); + tc_action + .attributes + .push(TcActionAttribute::Kind("mirred".to_string())); + let mut mirror_options = TcMirror::default(); + mirror_options.generic.index = index; + mirror_options.generic.action = Stolen; + mirror_options.ifindex = _get_test_dummy_interface_index() as u32; + mirror_options.eaction = EgressRedir; + tc_action.attributes.push(TcActionAttribute::Options(vec![ + TcActionOption::Mirror(TcActionMirrorOption::Parms(mirror_options)), + ])); + let req: TrafficActionNewRequest = + handle.traffic_action().add().action(tc_action); + let mut resp = req.execute(); + if let Some(msg) = resp.try_next().await.unwrap() { + eprintln!("{:?}", msg); + panic!("Unexpected response message"); + }; + let resp = _get_actions(TrafficActionKind::Mirror).await; + assert_eq!(resp.len(), 1); + let mut checked_interior_props = false; + resp[0].attributes.iter().for_each(|attr| { + if let TcActionMessageAttribute::Actions(acts) = attr { + acts.iter().for_each(|act| { + act.attributes.iter().for_each(|act_attr| { + if let TcActionAttribute::Options(opts) = act_attr { + opts.iter().for_each(|opt| { + if let TcActionOption::Mirror(mirror) = opt { + match mirror { + TcActionMirrorOption::Tm(_) => {} + TcActionMirrorOption::Parms(parms) => { + assert_eq!( + parms.ifindex, + _get_test_dummy_interface_index( + ) + as u32 + ); + assert_eq!( + parms.generic.index, + index + ); + assert_eq!(parms.generic.refcnt, 1); + assert_eq!( + parms.generic.bindcnt, + 0 + ); + checked_interior_props = true; + } + _ => {} + } + } + }) + } + }) + }) + } + }); + assert!(checked_interior_props); + } + Runtime::new().unwrap().block_on(_test()); +} + +#[test] +#[cfg_attr(not(feature = "test_as_root"), ignore)] +fn test_del_mirror_action() { + async fn _test() { + _flush_test_mirred_action(); + _add_test_dummy_interface(); + _add_test_mirred_action(99); + let (connection, handle, _) = new_connection().unwrap(); + tokio::spawn(connection); + let mirrors = _get_actions(TrafficActionKind::Mirror).await; + assert_eq!(mirrors.len(), 1); + let mut tc_action = TcAction::default(); + tc_action + .attributes + .push(TcActionAttribute::Kind("mirred".to_string())); + tc_action.attributes.push(TcActionAttribute::Index(99)); + let req = handle.traffic_action().del().action(tc_action); + req.execute().await.unwrap(); + let mirrors = _get_actions(TrafficActionKind::Mirror).await; + assert!(mirrors.is_empty()); + } + Runtime::new().unwrap().block_on(_test()); +} + +#[test] +#[cfg_attr(not(feature = "test_as_root"), ignore)] +fn test_get_nat_actions() { + async fn _test() { + _add_test_dummy_interface(); + // NOTE: this is sketchy if tests run concurrently + _flush_test_nat_action(); + let index = 99; + _add_test_nat_action(index); + let actions = _get_actions(TrafficActionKind::Nat).await; + assert_eq!(actions.len(), 1); + let action = &actions[0]; + assert_eq!(action.header.family, AddressFamily::Unspec); + // Find one and only one `RootCount` attribute and assert that its value + // is 1 + let root_counts: Vec<_> = action + .attributes + .iter() + .filter_map(|attr| match attr { + TcActionMessageAttribute::RootCount(count) => Some(count), + _ => None, + }) + .collect(); + assert_eq!(root_counts.len(), 1); + assert_eq!(root_counts[0], &1); + // Find one and only one `Actions` attribute and assert that it has a + // length of 1 + let action_lists: Vec<_> = action + .attributes + .iter() + .filter_map(|attr| match attr { + TcActionMessageAttribute::Actions(actions) => Some(actions), + _ => None, + }) + .collect(); + assert_eq!(action_lists.len(), 1); + assert_eq!(action_lists[0].len(), 1); + let action = &action_lists[0][0]; + assert_eq!(action.tab, 0); + assert!(action + .attributes + .contains(&TcActionAttribute::Kind("nat".to_string()))); + let Some(options) = + action.attributes.iter().find_map(|attr| match attr { + TcActionAttribute::Options(options) => Some(options), + _ => None, + }) + else { + eprintln!("{action:?}"); + panic!("No options attribute found in action"); + }; + // Assert that we find only nat options. + let nat_options: Vec<_> = options + .iter() + .map(|option| match option { + TcActionOption::Nat(opt) => opt, + unexpected => { + eprintln!("{unexpected:?}"); + panic!("Unexpected option type found in nat action"); + } + }) + .collect(); + assert!(!nat_options.is_empty()); + let Some(nat_params) = nat_options.iter().find_map(|opt| match opt { + TcActionNatOption::Parms(params) => Some(params), + _ => None, + }) else { + eprintln!("{action:?}"); + panic!("No mirror params found in action"); + }; + assert_eq!(nat_params.generic.index, index); + assert_eq!(nat_params.generic.action, TcActionType::Ok); + assert_eq!(nat_params.generic.refcnt, 1); + assert_eq!(nat_params.generic.bindcnt, 0); + assert_eq!(nat_params.old_addr, Ipv4Addr::new(1, 2, 3, 4)); + assert_eq!(nat_params.new_addr, Ipv4Addr::new(5, 6, 7, 8)); + assert_eq!(nat_params.mask, Ipv4Addr::new(255, 255, 255, 255)); + assert_eq!(nat_params.flags, TcNatFlags::empty()); + } + Runtime::new().unwrap().block_on(_test()); +} + +/// NOTE: I consider this test to be overly complex in its structure. +/// It seems like we have an API usability problem. +/// I needed to do a fairly large amount of destructuring to get some +/// fairly basic data. +#[test] +#[cfg_attr(not(feature = "test_as_root"), ignore)] +fn test_add_nat_action() { + async fn _test() { + _add_test_dummy_interface(); + _flush_test_nat_action(); + let (connection, handle, _) = new_connection().unwrap(); + tokio::spawn(connection); + let index = 99; + let mut tc_action = TcAction::default(); + tc_action + .attributes + .push(TcActionAttribute::Kind("nat".to_string())); + let mut nat_options = TcNat::default(); + nat_options.generic.index = index; + nat_options.generic.action = Stolen; + nat_options.old_addr = Ipv4Addr::new(1, 2, 3, 4); + nat_options.new_addr = Ipv4Addr::new(5, 6, 7, 8); + nat_options.mask = Ipv4Addr::new(255, 255, 255, 255); + tc_action.attributes.push(TcActionAttribute::Options(vec![ + TcActionOption::Nat(TcActionNatOption::Parms(nat_options)), + ])); + let req: TrafficActionNewRequest = + handle.traffic_action().add().action(tc_action); + let mut resp = req.execute(); + if let Some(msg) = resp.try_next().await.unwrap() { + eprintln!("{:?}", msg); + panic!("Unexpected response message"); + }; + let resp = _get_actions(TrafficActionKind::Nat).await; + assert_eq!(resp.len(), 1); + let mut checked_interior_props = false; + resp[0].attributes.iter().for_each(|attr| { + if let TcActionMessageAttribute::Actions(acts) = attr { + acts.iter().for_each(|act| { + act.attributes.iter().for_each(|act_attr| { + if let TcActionAttribute::Options(opts) = act_attr { + opts.iter().for_each(|opt| { + if let TcActionOption::Nat( + TcActionNatOption::Parms(parms), + ) = opt + { + assert_eq!(parms.generic.index, index); + assert_eq!(parms.generic.refcnt, 1); + assert_eq!(parms.generic.bindcnt, 0); + assert_eq!(parms.generic.action, Stolen); + checked_interior_props = true; + } + }) + } + }) + }) + } + }); + assert!(checked_interior_props); + } + Runtime::new().unwrap().block_on(_test()); +} + +#[test] +#[cfg_attr(not(feature = "test_as_root"), ignore)] +fn test_del_nat_action() { + async fn _test() { + _flush_test_nat_action(); + _add_test_dummy_interface(); + _add_test_nat_action(99); + let (connection, handle, _) = new_connection().unwrap(); + tokio::spawn(connection); + let nats = _get_actions(TrafficActionKind::Nat).await; + assert_eq!(nats.len(), 1); + let mut tc_action = TcAction::default(); + tc_action + .attributes + .push(TcActionAttribute::Kind("nat".to_string())); + tc_action.attributes.push(TcActionAttribute::Index(99)); + let req = handle.traffic_action().del().action(tc_action); + req.execute().await.unwrap(); + let nats = _get_actions(TrafficActionKind::Nat).await; + assert!(nats.is_empty()); + } + Runtime::new().unwrap().block_on(_test()); +}