use std::net::Ipv4Addr;
use super::{
Connection,
builder::MessageBuilder,
connection::dump_request,
error::{Error, Result},
message::{NLM_F_ACK, NLM_F_CREATE, NLM_F_REQUEST, NlMsgType},
protocol::Route,
types::tc::{
TCA_ACT_TAB, TcMsg,
action::{
self, TCA_ACT_INDEX, TCA_ACT_KIND, TCA_ACT_OPTIONS, TcGen, connmark, csum, ct, gact,
mirred, nat, pedit, police, sample, tunnel_key, vlan,
},
},
};
pub trait ActionConfig: Send + Sync {
fn kind(&self) -> &'static str;
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()>;
}
#[derive(Debug, Clone)]
pub struct GactAction {
action: i32,
prob_type: u16,
prob_val: u16,
prob_action: i32,
}
impl GactAction {
pub fn new(action: i32) -> Self {
Self {
action,
prob_type: gact::PGACT_NONE,
prob_val: 0,
prob_action: action::TC_ACT_OK,
}
}
pub fn drop() -> Self {
Self::new(action::TC_ACT_SHOT)
}
pub fn pass() -> Self {
Self::new(action::TC_ACT_OK)
}
pub fn pipe() -> Self {
Self::new(action::TC_ACT_PIPE)
}
pub fn reclassify() -> Self {
Self::new(action::TC_ACT_RECLASSIFY)
}
pub fn stolen() -> Self {
Self::new(action::TC_ACT_STOLEN)
}
pub fn goto_chain(chain: u32) -> Self {
use super::types::tc::action::tc_act_goto_chain;
Self::new(tc_act_goto_chain(chain))
}
pub fn random(mut self, percent: u16, action: i32) -> Self {
self.prob_type = gact::PGACT_NETRAND;
self.prob_val = ((percent as u32 * 65535) / 100).min(65535) as u16;
self.prob_action = action;
self
}
pub fn random_drop(self, percent: u16) -> Self {
self.random(percent, action::TC_ACT_SHOT)
}
pub fn deterministic(mut self, one_in_n: u16, action: i32) -> Self {
self.prob_type = gact::PGACT_DETERM;
self.prob_val = one_in_n;
self.prob_action = action;
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut act = Self::pass();
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"pass" | "ok" => {
act = Self::new(action::TC_ACT_OK);
i += 1;
}
"drop" | "shot" => {
act = Self::new(action::TC_ACT_SHOT);
i += 1;
}
"pipe" => {
act = Self::new(action::TC_ACT_PIPE);
i += 1;
}
"reclassify" => {
act = Self::new(action::TC_ACT_RECLASSIFY);
i += 1;
}
"stolen" => {
act = Self::new(action::TC_ACT_STOLEN);
i += 1;
}
"continue" => {
act = Self::new(action::TC_ACT_UNSPEC);
i += 1;
}
"goto_chain" => {
let s = action_need_value(params, i, "gact", key)?;
let chain = action_parse_u32("gact", "goto_chain", s)?;
act = Self::goto_chain(chain);
i += 2;
}
"random" => {
let kind = action_need_value(params, i, "gact", key)?;
let verdict_s = params.get(i + 2).copied().ok_or_else(|| {
Error::InvalidMessage(
"gact: `random <kind>` requires <verdict>".to_string(),
)
})?;
let val_s = params.get(i + 3).copied().ok_or_else(|| {
Error::InvalidMessage(
"gact: `random <kind> <verdict>` requires <value>".to_string(),
)
})?;
let verdict = parse_gact_verdict(verdict_s)?;
let val: u16 = val_s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"gact: invalid random value `{val_s}` (expected u16)"
))
})?;
act = match kind {
"determ" => act.deterministic(val, verdict),
"netrand" => act.random(val, verdict),
other => {
return Err(Error::InvalidMessage(format!(
"gact: unknown random kind `{other}` (expected `determ` or `netrand`)"
)));
}
};
i += 4;
}
other => {
return Err(Error::InvalidMessage(format!(
"gact: unknown token `{other}` (recognised: pass/drop/pipe/reclassify/stolen/continue, goto_chain <n>, random determ|netrand <verdict> <val>)"
)));
}
}
}
Ok(act)
}
pub fn build(self) -> Self {
self
}
}
impl ActionConfig for GactAction {
fn kind(&self) -> &'static str {
"gact"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let parms = gact::TcGact::new(self.action);
builder.append_attr(gact::TCA_GACT_PARMS, parms.as_bytes());
if self.prob_type != gact::PGACT_NONE {
let prob = gact::TcGactP::new(self.prob_type, self.prob_val, self.prob_action);
builder.append_attr(gact::TCA_GACT_PROB, prob.as_bytes());
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct MirredAction {
eaction: i32,
ifindex: u32,
action: i32,
}
impl MirredAction {
fn new_with_ifindex(eaction: i32, ifindex: u32) -> Self {
let action = if eaction == mirred::TCA_EGRESS_REDIR || eaction == mirred::TCA_INGRESS_REDIR
{
action::TC_ACT_STOLEN
} else {
action::TC_ACT_PIPE
};
Self {
eaction,
ifindex,
action,
}
}
pub fn redirect_by_index(ifindex: u32) -> Self {
Self::new_with_ifindex(mirred::TCA_EGRESS_REDIR, ifindex)
}
pub fn mirror_by_index(ifindex: u32) -> Self {
Self::new_with_ifindex(mirred::TCA_EGRESS_MIRROR, ifindex)
}
pub fn ingress_redirect_by_index(ifindex: u32) -> Self {
Self::new_with_ifindex(mirred::TCA_INGRESS_REDIR, ifindex)
}
pub fn ingress_mirror_by_index(ifindex: u32) -> Self {
Self::new_with_ifindex(mirred::TCA_INGRESS_MIRROR, ifindex)
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn pipe(mut self) -> Self {
self.action = action::TC_ACT_PIPE;
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut direction_ingress = false;
let mut op_mirror = false;
let mut ifindex: Option<u32> = None;
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"egress" => {
direction_ingress = false;
i += 1;
}
"ingress" => {
direction_ingress = true;
i += 1;
}
"redirect" => {
op_mirror = false;
i += 1;
}
"mirror" => {
op_mirror = true;
i += 1;
}
"dev" => {
let s = action_need_value(params, i, "mirred", key)?;
if ifindex.is_some() {
return Err(Error::InvalidMessage(
"mirred: `dev` and `ifindex` are mutually exclusive".to_string(),
));
}
let idx = crate::util::get_ifindex(s).map_err(|e| {
Error::InvalidMessage(format!("mirred: dev `{s}` not found: {e}"))
})?;
ifindex = Some(idx);
i += 2;
}
"ifindex" => {
let s = action_need_value(params, i, "mirred", key)?;
if ifindex.is_some() {
return Err(Error::InvalidMessage(
"mirred: `dev` and `ifindex` are mutually exclusive".to_string(),
));
}
ifindex = Some(action_parse_u32("mirred", "ifindex", s)?);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"mirred: unknown token `{other}` (recognised: egress/ingress, redirect/mirror, dev <ifname>, ifindex <n>)"
)));
}
}
}
let idx = ifindex.ok_or_else(|| {
Error::InvalidMessage(
"mirred: target interface required (use `dev <ifname>` or `ifindex <n>`)"
.to_string(),
)
})?;
Ok(match (direction_ingress, op_mirror) {
(false, false) => Self::redirect_by_index(idx),
(false, true) => Self::mirror_by_index(idx),
(true, false) => Self::ingress_redirect_by_index(idx),
(true, true) => Self::ingress_mirror_by_index(idx),
})
}
pub fn build(self) -> Self {
self
}
}
impl ActionConfig for MirredAction {
fn kind(&self) -> &'static str {
"mirred"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let parms = mirred::TcMirred::new(self.eaction, self.ifindex, self.action);
builder.append_attr(mirred::TCA_MIRRED_PARMS, parms.as_bytes());
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct PoliceAction {
rate: u64,
peakrate: Option<u64>,
burst: u32,
mtu: u32,
conform_action: i32,
exceed_action: i32,
avrate: Option<u32>,
}
impl Default for PoliceAction {
fn default() -> Self {
Self::new()
}
}
impl PoliceAction {
pub fn new() -> Self {
Self {
rate: 0,
peakrate: None,
burst: 0,
mtu: 2047,
conform_action: action::TC_ACT_OK,
exceed_action: action::TC_ACT_SHOT,
avrate: None,
}
}
pub fn rate(mut self, bytes_per_sec: u64) -> Self {
self.rate = bytes_per_sec;
self
}
pub fn rate_bps(mut self, bits_per_sec: u64) -> Self {
self.rate = bits_per_sec / 8;
self
}
pub fn peakrate(mut self, bytes_per_sec: u64) -> Self {
self.peakrate = Some(bytes_per_sec);
self
}
pub fn burst(mut self, bytes: u32) -> Self {
self.burst = bytes;
self
}
pub fn mtu(mut self, mtu: u32) -> Self {
self.mtu = mtu;
self
}
pub fn conform(mut self, action: i32) -> Self {
self.conform_action = action;
self
}
pub fn conform_pass(mut self) -> Self {
self.conform_action = action::TC_ACT_OK;
self
}
pub fn conform_pipe(mut self) -> Self {
self.conform_action = action::TC_ACT_PIPE;
self
}
pub fn exceed(mut self, action: i32) -> Self {
self.exceed_action = action;
self
}
pub fn exceed_drop(mut self) -> Self {
self.exceed_action = action::TC_ACT_SHOT;
self
}
pub fn exceed_reclassify(mut self) -> Self {
self.exceed_action = action::TC_ACT_RECLASSIFY;
self
}
pub fn exceed_pipe(mut self) -> Self {
self.exceed_action = action::TC_ACT_PIPE;
self
}
pub fn avrate(mut self, bytes_per_sec: u32) -> Self {
self.avrate = Some(bytes_per_sec);
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut act = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"rate" => {
let s = action_need_value(params, i, "police", key)?;
let r = crate::util::Rate::parse(s).map_err(|_| {
Error::InvalidMessage(format!(
"police: invalid rate `{s}` (expected tc-style rate like `1mbit`)"
))
})?;
act = act.rate(r.as_bytes_per_sec());
i += 2;
}
"peakrate" => {
let s = action_need_value(params, i, "police", key)?;
let r = crate::util::Rate::parse(s).map_err(|_| {
Error::InvalidMessage(format!(
"police: invalid peakrate `{s}` (expected tc-style rate)"
))
})?;
act = act.peakrate(r.as_bytes_per_sec());
i += 2;
}
"burst" | "buffer" | "maxburst" => {
let s = action_need_value(params, i, "police", key)?;
let bytes = crate::util::parse::get_size(s).map_err(|_| {
Error::InvalidMessage(format!(
"police: invalid burst `{s}` (expected tc-style size like `32k`)"
))
})?;
if bytes > u32::MAX as u64 {
return Err(Error::InvalidMessage(format!(
"police: burst `{s}` exceeds u32::MAX bytes"
)));
}
act = act.burst(bytes as u32);
i += 2;
}
"mtu" => {
let s = action_need_value(params, i, "police", key)?;
let bytes = crate::util::parse::get_size(s).map_err(|_| {
Error::InvalidMessage(format!(
"police: invalid mtu `{s}` (expected tc-style size)"
))
})?;
if bytes > u32::MAX as u64 {
return Err(Error::InvalidMessage(format!(
"police: mtu `{s}` exceeds u32::MAX bytes"
)));
}
act = act.mtu(bytes as u32);
i += 2;
}
"avrate" => {
let s = action_need_value(params, i, "police", key)?;
let r = crate::util::Rate::parse(s).map_err(|_| {
Error::InvalidMessage(format!(
"police: invalid avrate `{s}` (expected tc-style rate)"
))
})?;
let bps = r.as_bytes_per_sec();
if bps > u32::MAX as u64 {
return Err(Error::InvalidMessage(format!(
"police: avrate `{s}` exceeds u32::MAX bytes/sec"
)));
}
act = act.avrate(bps as u32);
i += 2;
}
"conform-exceed" => {
let s = action_need_value(params, i, "police", key)?;
let (conform_s, exceed_s) = s.split_once('/').ok_or_else(|| {
Error::InvalidMessage(format!(
"police: `conform-exceed` requires `<conform>/<exceed>` (got `{s}`)"
))
})?;
let conform = parse_gact_verdict(conform_s).map_err(|e| {
Error::InvalidMessage(e.to_string().replace("gact:", "police:"))
})?;
let exceed = parse_gact_verdict(exceed_s).map_err(|e| {
Error::InvalidMessage(e.to_string().replace("gact:", "police:"))
})?;
act = act.conform(conform).exceed(exceed);
i += 2;
}
"conform" => {
let s = action_need_value(params, i, "police", key)?;
let v = parse_gact_verdict(s).map_err(|e| {
Error::InvalidMessage(e.to_string().replace("gact:", "police:"))
})?;
act = act.conform(v);
i += 2;
}
"exceed" => {
let s = action_need_value(params, i, "police", key)?;
let v = parse_gact_verdict(s).map_err(|e| {
Error::InvalidMessage(e.to_string().replace("gact:", "police:"))
})?;
act = act.exceed(v);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"police: unknown token `{other}` (recognised: rate, burst/buffer/maxburst, peakrate, mtu, avrate, conform-exceed <c>/<e>, conform, exceed)"
)));
}
}
}
Ok(act)
}
pub fn build(self) -> Self {
self
}
}
impl ActionConfig for PoliceAction {
fn kind(&self) -> &'static str {
"police"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::TcRateSpec;
let peakrate = match self.peakrate {
Some(pr) => TcRateSpec::new(pr.min(u32::MAX as u64) as u32),
None => TcRateSpec::default(),
};
let parms = police::TcPolice {
rate: TcRateSpec::new(self.rate.min(u32::MAX as u64) as u32),
burst: self.burst,
mtu: self.mtu,
action: self.exceed_action,
peakrate,
..Default::default()
};
builder.append_attr(police::TCA_POLICE_TBF, parms.as_bytes());
if self.rate > u32::MAX as u64 {
builder.append_attr(police::TCA_POLICE_RATE64, &self.rate.to_ne_bytes());
}
if let Some(pr) = self.peakrate
&& pr > u32::MAX as u64
{
builder.append_attr(police::TCA_POLICE_PEAKRATE64, &pr.to_ne_bytes());
}
builder.append_attr_u32(police::TCA_POLICE_RESULT, self.conform_action as u32);
if let Some(avrate) = self.avrate {
builder.append_attr_u32(police::TCA_POLICE_AVRATE, avrate);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct VlanAction {
v_action: i32,
vlan_id: Option<u16>,
vlan_prio: Option<u8>,
vlan_proto: u16,
action: i32,
}
impl VlanAction {
pub fn pop() -> Self {
Self {
v_action: vlan::TCA_VLAN_ACT_POP,
vlan_id: None,
vlan_prio: None,
vlan_proto: vlan::ETH_P_8021Q,
action: action::TC_ACT_PIPE,
}
}
pub fn push(vlan_id: u16) -> Self {
Self {
v_action: vlan::TCA_VLAN_ACT_PUSH,
vlan_id: Some(vlan_id),
vlan_prio: None,
vlan_proto: vlan::ETH_P_8021Q,
action: action::TC_ACT_PIPE,
}
}
pub fn modify(vlan_id: u16) -> Self {
Self {
v_action: vlan::TCA_VLAN_ACT_MODIFY,
vlan_id: Some(vlan_id),
vlan_prio: None,
vlan_proto: vlan::ETH_P_8021Q,
action: action::TC_ACT_PIPE,
}
}
pub fn priority(mut self, prio: u8) -> Self {
self.vlan_prio = Some(prio);
self
}
pub fn qinq(mut self) -> Self {
self.vlan_proto = vlan::ETH_P_8021AD;
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut op: Option<&str> = None;
let mut vlan_id: Option<u16> = None;
let mut priority: Option<u8> = None;
let mut qinq = false;
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"pop" => {
op = Some("pop");
i += 1;
}
"push" => {
op = Some("push");
let s = action_need_value(params, i, "vlan", key)?;
let id = action_parse_u32("vlan", "push id", s)?;
if id > 4095 {
return Err(Error::InvalidMessage(format!(
"vlan: VLAN ID `{id}` out of range (0–4095)"
)));
}
vlan_id = Some(id as u16);
i += 2;
}
"modify" => {
op = Some("modify");
let s = action_need_value(params, i, "vlan", key)?;
let id = action_parse_u32("vlan", "modify id", s)?;
if id > 4095 {
return Err(Error::InvalidMessage(format!(
"vlan: VLAN ID `{id}` out of range (0–4095)"
)));
}
vlan_id = Some(id as u16);
i += 2;
}
"priority" => {
let s = action_need_value(params, i, "vlan", key)?;
let p = action_parse_u32("vlan", "priority", s)?;
if p > 7 {
return Err(Error::InvalidMessage(format!(
"vlan: priority `{p}` out of range (0–7)"
)));
}
priority = Some(p as u8);
i += 2;
}
"protocol" => {
let s = action_need_value(params, i, "vlan", key)?;
match s {
"802.1q" => qinq = false,
"802.1ad" => qinq = true,
other => {
return Err(Error::InvalidMessage(format!(
"vlan: unknown protocol `{other}` (expected `802.1q` or `802.1ad`)"
)));
}
}
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"vlan: unknown token `{other}` (recognised: pop, push <id>, modify <id>, priority <p>, protocol 802.1q|802.1ad)"
)));
}
}
}
let mut act = match op {
Some("pop") => Self::pop(),
Some("push") => {
let id = vlan_id.expect("push always sets vlan_id");
Self::push(id)
}
Some("modify") => {
let id = vlan_id.expect("modify always sets vlan_id");
Self::modify(id)
}
_ => {
return Err(Error::InvalidMessage(
"vlan: missing operation (pop, push <id>, or modify <id>)".to_string(),
));
}
};
if let Some(p) = priority {
act = act.priority(p);
}
if qinq {
act = act.qinq();
}
Ok(act)
}
pub fn build(self) -> Self {
self
}
}
impl ActionConfig for VlanAction {
fn kind(&self) -> &'static str {
"vlan"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let parms = vlan::TcVlan::new(self.v_action, self.action);
builder.append_attr(vlan::TCA_VLAN_PARMS, parms.as_bytes());
if let Some(id) = self.vlan_id {
builder.append_attr(vlan::TCA_VLAN_PUSH_VLAN_ID, &id.to_ne_bytes());
}
if let Some(prio) = self.vlan_prio {
builder.append_attr(vlan::TCA_VLAN_PUSH_VLAN_PRIORITY, &[prio]);
}
builder.append_attr(
vlan::TCA_VLAN_PUSH_VLAN_PROTOCOL,
&self.vlan_proto.to_be_bytes(),
);
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct SkbeditAction {
queue_mapping: Option<u16>,
priority: Option<u32>,
mark: Option<u32>,
mark_mask: Option<u32>,
action: i32,
}
mod skbedit {
pub const TCA_SKBEDIT_PARMS: u16 = 2;
pub const TCA_SKBEDIT_PRIORITY: u16 = 3;
pub const TCA_SKBEDIT_QUEUE_MAPPING: u16 = 4;
pub const TCA_SKBEDIT_MARK: u16 = 5;
pub const TCA_SKBEDIT_MASK: u16 = 8;
}
impl SkbeditAction {
pub fn new() -> Self {
Self {
action: action::TC_ACT_PIPE,
..Default::default()
}
}
pub fn queue_mapping(mut self, queue: u16) -> Self {
self.queue_mapping = Some(queue);
self
}
pub fn priority(mut self, prio: u32) -> Self {
self.priority = Some(prio);
self
}
pub fn mark(mut self, mark: u32) -> Self {
self.mark = Some(mark);
self
}
pub fn mark_with_mask(mut self, mark: u32, mask: u32) -> Self {
self.mark = Some(mark);
self.mark_mask = Some(mask);
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut act = Self::new();
let mut mark: Option<u32> = None;
let mut mask: Option<u32> = None;
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"priority" => {
let s = action_need_value(params, i, "skbedit", key)?;
act = act.priority(action_parse_u32("skbedit", "priority", s)?);
i += 2;
}
"mark" => {
let s = action_need_value(params, i, "skbedit", key)?;
mark = Some(action_parse_u32("skbedit", "mark", s)?);
i += 2;
}
"mask" => {
let s = action_need_value(params, i, "skbedit", key)?;
mask = Some(action_parse_u32("skbedit", "mask", s)?);
i += 2;
}
"queue_mapping" => {
let s = action_need_value(params, i, "skbedit", key)?;
let q = action_parse_u32("skbedit", "queue_mapping", s)?;
if q > u16::MAX as u32 {
return Err(Error::InvalidMessage(format!(
"skbedit: queue_mapping `{q}` out of range (0–65535)"
)));
}
act = act.queue_mapping(q as u16);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"skbedit: unknown token `{other}` (recognised: priority, mark, mask, queue_mapping)"
)));
}
}
}
match (mark, mask) {
(Some(m), Some(mk)) => act = act.mark_with_mask(m, mk),
(Some(m), None) => act = act.mark(m),
(None, Some(_)) => {
return Err(Error::InvalidMessage(
"skbedit: `mask` requires a `mark` value".to_string(),
));
}
(None, None) => {}
}
Ok(act)
}
pub fn build(self) -> Self {
self
}
}
impl ActionConfig for SkbeditAction {
fn kind(&self) -> &'static str {
"skbedit"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let parms = TcGen::new(self.action);
builder.append_attr(skbedit::TCA_SKBEDIT_PARMS, parms.as_bytes());
if let Some(queue) = self.queue_mapping {
builder.append_attr(skbedit::TCA_SKBEDIT_QUEUE_MAPPING, &queue.to_ne_bytes());
}
if let Some(prio) = self.priority {
builder.append_attr(skbedit::TCA_SKBEDIT_PRIORITY, &prio.to_ne_bytes());
}
if let Some(mark) = self.mark {
builder.append_attr(skbedit::TCA_SKBEDIT_MARK, &mark.to_ne_bytes());
}
if let Some(mask) = self.mark_mask {
builder.append_attr(skbedit::TCA_SKBEDIT_MASK, &mask.to_ne_bytes());
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct NatAction {
old_addr: Ipv4Addr,
new_addr: Ipv4Addr,
prefix_len: u8,
egress: bool,
action: i32,
}
impl NatAction {
pub fn snat(old_addr: Ipv4Addr, new_addr: Ipv4Addr) -> Self {
Self {
old_addr,
new_addr,
prefix_len: 32,
egress: true,
action: action::TC_ACT_OK,
}
}
pub fn dnat(old_addr: Ipv4Addr, new_addr: Ipv4Addr) -> Self {
Self {
old_addr,
new_addr,
prefix_len: 32,
egress: false,
action: action::TC_ACT_OK,
}
}
pub fn prefix(mut self, prefix_len: u8) -> Self {
self.prefix_len = prefix_len.min(32);
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn pipe(mut self) -> Self {
self.action = action::TC_ACT_PIPE;
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
if params.len() < 3 {
return Err(Error::InvalidMessage(
"nat: requires `ingress|egress <oldaddr[/prefix]> <newaddr>`".to_string(),
));
}
let direction = params[0];
let old_with_prefix = params[1];
let new_addr_s = params[2];
if params.len() > 3 {
return Err(Error::InvalidMessage(format!(
"nat: unexpected token `{}` after `{} {} {}` (only the 3 positional args are supported)",
params[3], direction, old_with_prefix, new_addr_s
)));
}
let (old_addr_s, prefix) = match old_with_prefix.split_once('/') {
Some((a, p)) => {
let pl: u8 = p.parse().map_err(|_| {
Error::InvalidMessage(format!(
"nat: invalid old-address prefix `{p}` (expected 0–32)"
))
})?;
if pl > 32 {
return Err(Error::InvalidMessage(format!(
"nat: prefix `{pl}` out of range (0–32)"
)));
}
(a, pl)
}
None => (old_with_prefix, 32),
};
let old_addr: Ipv4Addr = old_addr_s.parse().map_err(|_| {
Error::InvalidMessage(format!("nat: invalid old address `{old_addr_s}`"))
})?;
let new_addr: Ipv4Addr = new_addr_s.parse().map_err(|_| {
Error::InvalidMessage(format!("nat: invalid new address `{new_addr_s}`"))
})?;
let mut act = match direction {
"egress" => Self::snat(old_addr, new_addr),
"ingress" => Self::dnat(old_addr, new_addr),
other => {
return Err(Error::InvalidMessage(format!(
"nat: direction `{other}` (expected `ingress` or `egress`)"
)));
}
};
if prefix != 32 {
act = act.prefix(prefix);
}
Ok(act)
}
pub fn build(self) -> Self {
self
}
fn prefix_to_mask(prefix_len: u8) -> u32 {
if prefix_len == 0 {
0
} else if prefix_len >= 32 {
0xFFFFFFFF
} else {
!((1u32 << (32 - prefix_len)) - 1)
}
}
}
impl ActionConfig for NatAction {
fn kind(&self) -> &'static str {
"nat"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let mask = Self::prefix_to_mask(self.prefix_len);
let flags = if self.egress {
nat::TCA_NAT_FLAG_EGRESS
} else {
0
};
let parms = nat::TcNat::new(
u32::from_be_bytes(self.old_addr.octets()),
u32::from_be_bytes(self.new_addr.octets()),
u32::from_be_bytes(mask.to_be_bytes()),
flags,
self.action,
);
builder.append_attr(nat::TCA_NAT_PARMS, parms.as_bytes());
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct TunnelKeyAction {
t_action: i32,
src_ipv4: Option<Ipv4Addr>,
dst_ipv4: Option<Ipv4Addr>,
src_ipv6: Option<std::net::Ipv6Addr>,
dst_ipv6: Option<std::net::Ipv6Addr>,
key_id: Option<u32>,
dst_port: Option<u16>,
tos: Option<u8>,
ttl: Option<u8>,
no_csum: bool,
no_frag: bool,
action: i32,
}
impl TunnelKeyAction {
pub fn set() -> Self {
Self {
t_action: tunnel_key::TCA_TUNNEL_KEY_ACT_SET,
src_ipv4: None,
dst_ipv4: None,
src_ipv6: None,
dst_ipv6: None,
key_id: None,
dst_port: None,
tos: None,
ttl: None,
no_csum: false,
no_frag: false,
action: action::TC_ACT_PIPE,
}
}
pub fn release() -> Self {
Self {
t_action: tunnel_key::TCA_TUNNEL_KEY_ACT_RELEASE,
src_ipv4: None,
dst_ipv4: None,
src_ipv6: None,
dst_ipv6: None,
key_id: None,
dst_port: None,
tos: None,
ttl: None,
no_csum: false,
no_frag: false,
action: action::TC_ACT_PIPE,
}
}
pub fn src(mut self, addr: Ipv4Addr) -> Self {
self.src_ipv4 = Some(addr);
self.src_ipv6 = None;
self
}
pub fn dst(mut self, addr: Ipv4Addr) -> Self {
self.dst_ipv4 = Some(addr);
self.dst_ipv6 = None;
self
}
pub fn src6(mut self, addr: std::net::Ipv6Addr) -> Self {
self.src_ipv6 = Some(addr);
self.src_ipv4 = None;
self
}
pub fn dst6(mut self, addr: std::net::Ipv6Addr) -> Self {
self.dst_ipv6 = Some(addr);
self.dst_ipv4 = None;
self
}
pub fn key_id(mut self, id: u32) -> Self {
self.key_id = Some(id);
self
}
pub fn dst_port(mut self, port: u16) -> Self {
self.dst_port = Some(port);
self
}
pub fn tos(mut self, tos: u8) -> Self {
self.tos = Some(tos);
self
}
pub fn ttl(mut self, ttl: u8) -> Self {
self.ttl = Some(ttl);
self
}
pub fn no_csum(mut self) -> Self {
self.no_csum = true;
self
}
pub fn no_frag(mut self) -> Self {
self.no_frag = true;
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut op: Option<&str> = None;
for &t in params {
if matches!(t, "set" | "release") {
if op.is_some() {
return Err(Error::InvalidMessage(
"tunnel_key: only one of `set` / `release` may be specified".to_string(),
));
}
op = Some(t);
}
}
let mut act = match op {
Some("set") => Self::set(),
Some("release") => Self::release(),
_ => {
return Err(Error::InvalidMessage(
"tunnel_key: missing operation (`set` or `release`)".to_string(),
));
}
};
let is_set = matches!(op, Some("set"));
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"set" | "release" => i += 1,
"src" => {
let s = action_need_value(params, i, "tunnel_key", key)?;
let addr: Ipv4Addr = s.parse().map_err(|_| {
Error::InvalidMessage(format!("tunnel_key: invalid src `{s}`"))
})?;
if !is_set {
return Err(Error::InvalidMessage(
"tunnel_key: src/dst/id/etc. only valid with `set`".to_string(),
));
}
act = act.src(addr);
i += 2;
}
"dst" => {
let s = action_need_value(params, i, "tunnel_key", key)?;
let addr: Ipv4Addr = s.parse().map_err(|_| {
Error::InvalidMessage(format!("tunnel_key: invalid dst `{s}`"))
})?;
if !is_set {
return Err(Error::InvalidMessage(
"tunnel_key: src/dst/id/etc. only valid with `set`".to_string(),
));
}
act = act.dst(addr);
i += 2;
}
"src6" => {
let s = action_need_value(params, i, "tunnel_key", key)?;
let addr: std::net::Ipv6Addr = s.parse().map_err(|_| {
Error::InvalidMessage(format!("tunnel_key: invalid src6 `{s}`"))
})?;
if !is_set {
return Err(Error::InvalidMessage(
"tunnel_key: src/dst/id/etc. only valid with `set`".to_string(),
));
}
act = act.src6(addr);
i += 2;
}
"dst6" => {
let s = action_need_value(params, i, "tunnel_key", key)?;
let addr: std::net::Ipv6Addr = s.parse().map_err(|_| {
Error::InvalidMessage(format!("tunnel_key: invalid dst6 `{s}`"))
})?;
if !is_set {
return Err(Error::InvalidMessage(
"tunnel_key: src/dst/id/etc. only valid with `set`".to_string(),
));
}
act = act.dst6(addr);
i += 2;
}
"id" => {
let s = action_need_value(params, i, "tunnel_key", key)?;
let id = action_parse_u32("tunnel_key", "id", s)?;
if !is_set {
return Err(Error::InvalidMessage(
"tunnel_key: src/dst/id/etc. only valid with `set`".to_string(),
));
}
act = act.key_id(id);
i += 2;
}
"dst_port" => {
let s = action_need_value(params, i, "tunnel_key", key)?;
let port = action_parse_u32("tunnel_key", "dst_port", s)?;
if port > u16::MAX as u32 {
return Err(Error::InvalidMessage(format!(
"tunnel_key: dst_port `{port}` out of range (0–65535)"
)));
}
if !is_set {
return Err(Error::InvalidMessage(
"tunnel_key: src/dst/id/etc. only valid with `set`".to_string(),
));
}
act = act.dst_port(port as u16);
i += 2;
}
"tos" => {
let s = action_need_value(params, i, "tunnel_key", key)?;
let v = action_parse_u32("tunnel_key", "tos", s)?;
if v > u8::MAX as u32 {
return Err(Error::InvalidMessage(format!(
"tunnel_key: tos `{v}` out of range (0–255)"
)));
}
if !is_set {
return Err(Error::InvalidMessage(
"tunnel_key: src/dst/id/etc. only valid with `set`".to_string(),
));
}
act = act.tos(v as u8);
i += 2;
}
"ttl" => {
let s = action_need_value(params, i, "tunnel_key", key)?;
let v = action_parse_u32("tunnel_key", "ttl", s)?;
if v > u8::MAX as u32 {
return Err(Error::InvalidMessage(format!(
"tunnel_key: ttl `{v}` out of range (0–255)"
)));
}
if !is_set {
return Err(Error::InvalidMessage(
"tunnel_key: src/dst/id/etc. only valid with `set`".to_string(),
));
}
act = act.ttl(v as u8);
i += 2;
}
"no_csum" => {
if !is_set {
return Err(Error::InvalidMessage(
"tunnel_key: no_csum only valid with `set`".to_string(),
));
}
act = act.no_csum();
i += 1;
}
"no_frag" => {
if !is_set {
return Err(Error::InvalidMessage(
"tunnel_key: no_frag only valid with `set`".to_string(),
));
}
act = act.no_frag();
i += 1;
}
other => {
return Err(Error::InvalidMessage(format!(
"tunnel_key: unknown token `{other}` (recognised: set/release, src/dst[6] <addr>, id <vni>, dst_port <port>, tos/ttl <0-255>, no_csum, no_frag)"
)));
}
}
}
Ok(act)
}
pub fn build(self) -> Self {
self
}
}
impl ActionConfig for TunnelKeyAction {
fn kind(&self) -> &'static str {
"tunnel_key"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let parms = tunnel_key::TcTunnelKey::new(self.t_action, self.action);
builder.append_attr(tunnel_key::TCA_TUNNEL_KEY_PARMS, parms.as_bytes());
if self.t_action == tunnel_key::TCA_TUNNEL_KEY_ACT_SET {
if let Some(addr) = self.src_ipv4 {
builder.append_attr(tunnel_key::TCA_TUNNEL_KEY_ENC_IPV4_SRC, &addr.octets());
}
if let Some(addr) = self.dst_ipv4 {
builder.append_attr(tunnel_key::TCA_TUNNEL_KEY_ENC_IPV4_DST, &addr.octets());
}
if let Some(addr) = self.src_ipv6 {
builder.append_attr(tunnel_key::TCA_TUNNEL_KEY_ENC_IPV6_SRC, &addr.octets());
}
if let Some(addr) = self.dst_ipv6 {
builder.append_attr(tunnel_key::TCA_TUNNEL_KEY_ENC_IPV6_DST, &addr.octets());
}
if let Some(id) = self.key_id {
let id_be64 = (id as u64).to_be_bytes();
builder.append_attr(tunnel_key::TCA_TUNNEL_KEY_ENC_KEY_ID, &id_be64);
}
if let Some(port) = self.dst_port {
builder.append_attr_u16_be(tunnel_key::TCA_TUNNEL_KEY_ENC_DST_PORT, port);
}
if let Some(tos) = self.tos {
builder.append_attr_u8(tunnel_key::TCA_TUNNEL_KEY_ENC_TOS, tos);
}
if let Some(ttl) = self.ttl {
builder.append_attr_u8(tunnel_key::TCA_TUNNEL_KEY_ENC_TTL, ttl);
}
builder.append_attr_u8(
tunnel_key::TCA_TUNNEL_KEY_NO_CSUM,
if self.no_csum { 1 } else { 0 },
);
if self.no_frag {
builder.append_attr_empty(tunnel_key::TCA_TUNNEL_KEY_NO_FRAG);
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ConnmarkAction {
zone: u16,
action: i32,
}
impl ConnmarkAction {
pub fn new() -> Self {
Self {
zone: 0,
action: action::TC_ACT_PIPE,
}
}
pub fn with_zone(zone: u16) -> Self {
Self {
zone,
action: action::TC_ACT_PIPE,
}
}
pub fn zone(mut self, zone: u16) -> Self {
self.zone = zone;
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut act = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"zone" => {
let s = action_need_value(params, i, "connmark", key)?;
let z = action_parse_u32("connmark", "zone", s)?;
if z > u16::MAX as u32 {
return Err(Error::InvalidMessage(format!(
"connmark: zone `{z}` out of range (0–65535)"
)));
}
act = act.zone(z as u16);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"connmark: unknown token `{other}` (recognised: zone <0–65535>)"
)));
}
}
}
Ok(act)
}
pub fn build(self) -> Self {
self
}
}
impl Default for ConnmarkAction {
fn default() -> Self {
Self::new()
}
}
impl ActionConfig for ConnmarkAction {
fn kind(&self) -> &'static str {
"connmark"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let parms = connmark::TcConnmark::new(self.zone, self.action);
builder.append_attr(connmark::TCA_CONNMARK_PARMS, parms.as_bytes());
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct CsumAction {
update_flags: u32,
action: i32,
}
impl CsumAction {
pub fn new() -> Self {
Self {
update_flags: 0,
action: action::TC_ACT_OK,
}
}
pub fn iph(mut self) -> Self {
self.update_flags |= csum::TCA_CSUM_UPDATE_FLAG_IPV4HDR;
self
}
pub fn icmp(mut self) -> Self {
self.update_flags |= csum::TCA_CSUM_UPDATE_FLAG_ICMP;
self
}
pub fn igmp(mut self) -> Self {
self.update_flags |= csum::TCA_CSUM_UPDATE_FLAG_IGMP;
self
}
pub fn tcp(mut self) -> Self {
self.update_flags |= csum::TCA_CSUM_UPDATE_FLAG_TCP;
self
}
pub fn udp(mut self) -> Self {
self.update_flags |= csum::TCA_CSUM_UPDATE_FLAG_UDP;
self
}
pub fn udplite(mut self) -> Self {
self.update_flags |= csum::TCA_CSUM_UPDATE_FLAG_UDPLITE;
self
}
pub fn sctp(mut self) -> Self {
self.update_flags |= csum::TCA_CSUM_UPDATE_FLAG_SCTP;
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut act = Self::new();
for &t in params {
act = match t {
"iph" => act.iph(),
"icmp" => act.icmp(),
"igmp" => act.igmp(),
"tcp" => act.tcp(),
"udp" => act.udp(),
"udplite" => act.udplite(),
"sctp" => act.sctp(),
other => {
return Err(Error::InvalidMessage(format!(
"csum: unknown checksum kind `{other}` (recognised: iph/icmp/igmp/tcp/udp/udplite/sctp)"
)));
}
};
}
Ok(act)
}
pub fn build(self) -> Self {
self
}
}
impl Default for CsumAction {
fn default() -> Self {
Self::new()
}
}
impl ActionConfig for CsumAction {
fn kind(&self) -> &'static str {
"csum"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let parms = csum::TcCsum::new(self.update_flags, self.action);
builder.append_attr(csum::TCA_CSUM_PARMS, parms.as_bytes());
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct SampleAction {
rate: u32,
group: u32,
trunc_size: Option<u32>,
action: i32,
}
impl SampleAction {
pub fn new(rate: u32, group: u32) -> Self {
Self {
rate,
group,
trunc_size: None,
action: action::TC_ACT_PIPE,
}
}
pub fn trunc(mut self, size: u32) -> Self {
self.trunc_size = Some(size);
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut rate: Option<u32> = None;
let mut group: Option<u32> = None;
let mut trunc: Option<u32> = None;
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"rate" => {
let s = action_need_value(params, i, "sample", key)?;
rate = Some(action_parse_u32("sample", "rate", s)?);
i += 2;
}
"group" => {
let s = action_need_value(params, i, "sample", key)?;
group = Some(action_parse_u32("sample", "group", s)?);
i += 2;
}
"trunc" => {
let s = action_need_value(params, i, "sample", key)?;
trunc = Some(action_parse_u32("sample", "trunc", s)?);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"sample: unknown token `{other}` (recognised: rate <N>, group <G>, trunc <bytes>)"
)));
}
}
}
let rate =
rate.ok_or_else(|| Error::InvalidMessage("sample: `rate <N>` required".to_string()))?;
let group = group
.ok_or_else(|| Error::InvalidMessage("sample: `group <G>` required".to_string()))?;
let mut act = Self::new(rate, group);
if let Some(t) = trunc {
act = act.trunc(t);
}
Ok(act)
}
pub fn build(self) -> Self {
self
}
}
impl ActionConfig for SampleAction {
fn kind(&self) -> &'static str {
"sample"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let parms = sample::TcSample::new(self.action);
builder.append_attr(sample::TCA_SAMPLE_PARMS, parms.as_bytes());
builder.append_attr_u32(sample::TCA_SAMPLE_RATE, self.rate);
builder.append_attr_u32(sample::TCA_SAMPLE_PSAMPLE_GROUP, self.group);
if let Some(trunc) = self.trunc_size {
builder.append_attr_u32(sample::TCA_SAMPLE_TRUNC_SIZE, trunc);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct CtAction {
ct_action: u16,
zone: Option<u16>,
mark: Option<u32>,
mark_mask: Option<u32>,
labels: Option<[u8; 16]>,
labels_mask: Option<[u8; 16]>,
nat_ipv4_min: Option<Ipv4Addr>,
nat_ipv4_max: Option<Ipv4Addr>,
nat_ipv6_min: Option<std::net::Ipv6Addr>,
nat_ipv6_max: Option<std::net::Ipv6Addr>,
nat_port_min: Option<u16>,
nat_port_max: Option<u16>,
helper_name: Option<String>,
helper_family: Option<u8>,
helper_proto: Option<u8>,
action: i32,
}
impl Default for CtAction {
fn default() -> Self {
Self::new()
}
}
impl CtAction {
pub fn new() -> Self {
Self {
ct_action: 0,
zone: None,
mark: None,
mark_mask: None,
labels: None,
labels_mask: None,
nat_ipv4_min: None,
nat_ipv4_max: None,
nat_ipv6_min: None,
nat_ipv6_max: None,
nat_port_min: None,
nat_port_max: None,
helper_name: None,
helper_family: None,
helper_proto: None,
action: action::TC_ACT_PIPE,
}
}
pub fn commit() -> Self {
let mut s = Self::new();
s.ct_action = ct::TCA_CT_ACT_COMMIT;
s
}
pub fn clear() -> Self {
let mut s = Self::new();
s.ct_action = ct::TCA_CT_ACT_CLEAR;
s
}
pub fn force(mut self) -> Self {
self.ct_action |= ct::TCA_CT_ACT_FORCE;
self
}
pub fn zone(mut self, zone: u16) -> Self {
self.zone = Some(zone);
self
}
pub fn mark(mut self, mark: u32, mask: u32) -> Self {
self.mark = Some(mark);
self.mark_mask = Some(mask);
self
}
pub fn labels(mut self, labels: [u8; 16], mask: [u8; 16]) -> Self {
self.labels = Some(labels);
self.labels_mask = Some(mask);
self
}
pub fn nat_src(mut self, min: Ipv4Addr, max: Ipv4Addr) -> Self {
self.ct_action |= ct::TCA_CT_ACT_NAT | ct::TCA_CT_ACT_NAT_SRC;
self.nat_ipv4_min = Some(min);
self.nat_ipv4_max = Some(max);
self
}
pub fn nat_src_single(mut self, addr: Ipv4Addr) -> Self {
self.ct_action |= ct::TCA_CT_ACT_NAT | ct::TCA_CT_ACT_NAT_SRC;
self.nat_ipv4_min = Some(addr);
self.nat_ipv4_max = Some(addr);
self
}
pub fn nat_dst(mut self, min: Ipv4Addr, max: Ipv4Addr) -> Self {
self.ct_action |= ct::TCA_CT_ACT_NAT | ct::TCA_CT_ACT_NAT_DST;
self.nat_ipv4_min = Some(min);
self.nat_ipv4_max = Some(max);
self
}
pub fn nat_dst_single(mut self, addr: Ipv4Addr) -> Self {
self.ct_action |= ct::TCA_CT_ACT_NAT | ct::TCA_CT_ACT_NAT_DST;
self.nat_ipv4_min = Some(addr);
self.nat_ipv4_max = Some(addr);
self
}
pub fn nat_src6(mut self, min: std::net::Ipv6Addr, max: std::net::Ipv6Addr) -> Self {
self.ct_action |= ct::TCA_CT_ACT_NAT | ct::TCA_CT_ACT_NAT_SRC;
self.nat_ipv6_min = Some(min);
self.nat_ipv6_max = Some(max);
self
}
pub fn nat_dst6(mut self, min: std::net::Ipv6Addr, max: std::net::Ipv6Addr) -> Self {
self.ct_action |= ct::TCA_CT_ACT_NAT | ct::TCA_CT_ACT_NAT_DST;
self.nat_ipv6_min = Some(min);
self.nat_ipv6_max = Some(max);
self
}
pub fn nat_port_range(mut self, min: u16, max: u16) -> Self {
self.nat_port_min = Some(min);
self.nat_port_max = Some(max);
self
}
pub fn helper(mut self, name: &str, family: u8, proto: u8) -> Self {
self.helper_name = Some(name.to_string());
self.helper_family = Some(family);
self.helper_proto = Some(proto);
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let op = params.iter().copied().find_map(|t| match t {
"commit" => Some("commit"),
"clear" => Some("clear"),
_ => None,
});
let mut act = match op {
Some("commit") => Self::commit(),
Some("clear") => Self::clear(),
_ => Self::new(),
};
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"commit" | "clear" => i += 1, "force" => {
act = act.force();
i += 1;
}
"zone" => {
let s = action_need_value(params, i, "ct", key)?;
let z = action_parse_u32("ct", "zone", s)?;
if z > u16::MAX as u32 {
return Err(Error::InvalidMessage(format!(
"ct: zone `{z}` out of range (0–65535)"
)));
}
act = act.zone(z as u16);
i += 2;
}
"mark" => {
let mark_s = action_need_value(params, i, "ct", key)?;
let mask_s = params.get(i + 2).copied().ok_or_else(|| {
Error::InvalidMessage("ct: `mark` requires `<value> <mask>`".to_string())
})?;
let m = action_parse_u32("ct", "mark", mark_s)?;
let mk = action_parse_u32("ct", "mark mask", mask_s)?;
act = act.mark(m, mk);
i += 3;
}
"nat" => {
let dir = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage(
"ct: `nat` requires `src|dst <addr>` or `src|dst <min>-<max>`"
.to_string(),
)
})?;
let range_s = params.get(i + 2).copied().ok_or_else(|| {
Error::InvalidMessage(format!(
"ct: `nat {dir}` requires an address or `<min>-<max>` range"
))
})?;
let (min, max) = parse_ipv4_range_or_single(range_s)?;
act = match dir {
"src" => act.nat_src(min, max),
"dst" => act.nat_dst(min, max),
other => {
return Err(Error::InvalidMessage(format!(
"ct: `nat` direction must be `src` or `dst` (got `{other}`)"
)));
}
};
i += 3;
}
other => {
return Err(Error::InvalidMessage(format!(
"ct: unknown token `{other}` (recognised: commit, clear, force, zone, mark <v> <m>, nat src|dst <addr>|<min>-<max>)"
)));
}
}
}
Ok(act)
}
pub fn build(self) -> Self {
self
}
}
impl ActionConfig for CtAction {
fn kind(&self) -> &'static str {
"ct"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let parms = ct::TcCt::new(self.action);
builder.append_attr(ct::TCA_CT_PARMS, parms.as_bytes());
if self.ct_action != 0 {
builder.append_attr(ct::TCA_CT_ACTION, &self.ct_action.to_ne_bytes());
}
if let Some(zone) = self.zone {
builder.append_attr(ct::TCA_CT_ZONE, &zone.to_ne_bytes());
}
if let Some(mark) = self.mark {
builder.append_attr(ct::TCA_CT_MARK, &mark.to_ne_bytes());
}
if let Some(mask) = self.mark_mask {
builder.append_attr(ct::TCA_CT_MARK_MASK, &mask.to_ne_bytes());
}
if let Some(labels) = &self.labels {
builder.append_attr(ct::TCA_CT_LABELS, labels);
}
if let Some(mask) = &self.labels_mask {
builder.append_attr(ct::TCA_CT_LABELS_MASK, mask);
}
if let Some(addr) = self.nat_ipv4_min {
builder.append_attr(ct::TCA_CT_NAT_IPV4_MIN, &addr.octets());
}
if let Some(addr) = self.nat_ipv4_max {
builder.append_attr(ct::TCA_CT_NAT_IPV4_MAX, &addr.octets());
}
if let Some(addr) = self.nat_ipv6_min {
builder.append_attr(ct::TCA_CT_NAT_IPV6_MIN, &addr.octets());
}
if let Some(addr) = self.nat_ipv6_max {
builder.append_attr(ct::TCA_CT_NAT_IPV6_MAX, &addr.octets());
}
if let Some(port) = self.nat_port_min {
builder.append_attr(ct::TCA_CT_NAT_PORT_MIN, &port.to_be_bytes());
}
if let Some(port) = self.nat_port_max {
builder.append_attr(ct::TCA_CT_NAT_PORT_MAX, &port.to_be_bytes());
}
if let Some(name) = &self.helper_name {
builder.append_attr_str(ct::TCA_CT_HELPER_NAME, name);
}
if let Some(family) = self.helper_family {
builder.append_attr(ct::TCA_CT_HELPER_FAMILY, &[family]);
}
if let Some(proto) = self.helper_proto {
builder.append_attr(ct::TCA_CT_HELPER_PROTO, &[proto]);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct PeditAction {
keys: Vec<PeditKey>,
action: i32,
}
#[derive(Debug, Clone)]
struct PeditKey {
htype: u16,
cmd: u16,
mask: u32,
val: u32,
off: u32,
}
impl Default for PeditAction {
fn default() -> Self {
Self::new()
}
}
impl PeditAction {
pub fn new() -> Self {
Self {
keys: Vec::new(),
action: action::TC_ACT_PIPE,
}
}
pub fn set_ipv4_src(mut self, addr: Ipv4Addr) -> Self {
self.keys.push(PeditKey {
htype: pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_IP4,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask: 0,
val: u32::from_be_bytes(addr.octets()),
off: 12, });
self
}
pub fn set_ipv4_dst(mut self, addr: Ipv4Addr) -> Self {
self.keys.push(PeditKey {
htype: pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_IP4,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask: 0,
val: u32::from_be_bytes(addr.octets()),
off: 16, });
self
}
pub fn set_ipv4_tos(mut self, tos: u8) -> Self {
self.keys.push(PeditKey {
htype: pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_IP4,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask: 0x00ff_ffff,
val: (tos as u32) << 24,
off: 0, });
self
}
pub fn set_ipv4_ttl(mut self, ttl: u8) -> Self {
self.keys.push(PeditKey {
htype: pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_IP4,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask: 0xffff_00ff,
val: (ttl as u32) << 8,
off: 8, });
self
}
pub fn set_tcp_sport(mut self, port: u16) -> Self {
self.keys.push(PeditKey {
htype: pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_TCP,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask: 0x0000_ffff,
val: (port as u32) << 16,
off: 0, });
self
}
pub fn set_tcp_dport(mut self, port: u16) -> Self {
self.keys.push(PeditKey {
htype: pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_TCP,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask: 0xffff_0000,
val: port as u32,
off: 0, });
self
}
pub fn set_udp_sport(mut self, port: u16) -> Self {
self.keys.push(PeditKey {
htype: pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_UDP,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask: 0x0000_ffff,
val: (port as u32) << 16,
off: 0,
});
self
}
pub fn set_udp_dport(mut self, port: u16) -> Self {
self.keys.push(PeditKey {
htype: pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_UDP,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask: 0xffff_0000,
val: port as u32,
off: 0,
});
self
}
pub fn set_eth_src(mut self, mac: [u8; 6]) -> Self {
self.keys.push(PeditKey {
htype: pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_ETH,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask: 0,
val: u32::from_be_bytes([mac[0], mac[1], mac[2], mac[3]]),
off: 6, });
self.keys.push(PeditKey {
htype: pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_ETH,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask: 0x0000_ffff,
val: ((mac[4] as u32) << 24) | ((mac[5] as u32) << 16),
off: 10,
});
self
}
pub fn set_eth_dst(mut self, mac: [u8; 6]) -> Self {
self.keys.push(PeditKey {
htype: pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_ETH,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask: 0,
val: u32::from_be_bytes([mac[0], mac[1], mac[2], mac[3]]),
off: 0, });
self.keys.push(PeditKey {
htype: pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_ETH,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask: 0x0000_ffff,
val: ((mac[4] as u32) << 24) | ((mac[5] as u32) << 16),
off: 4,
});
self
}
pub fn set_raw(mut self, htype: u16, off: u32, val: u32, mask: u32) -> Self {
self.keys.push(PeditKey {
htype,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_SET,
mask,
val,
off,
});
self
}
pub fn add_raw(mut self, htype: u16, off: u32, val: u32, mask: u32) -> Self {
self.keys.push(PeditKey {
htype,
cmd: pedit::TCA_PEDIT_KEY_EX_CMD_ADD,
mask,
val,
off,
});
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn parse_params(_params: &[&str]) -> Result<Self> {
Err(Error::InvalidMessage(
"pedit: not yet typed-modelled — use the typed builder \
(PeditAction::set_ipv4_src / set_tcp_dport / etc.) \
instead of the tc(8) DSL. Future work per Plan 139 §10."
.to_string(),
))
}
pub fn build(self) -> Self {
self
}
}
impl ActionConfig for PeditAction {
fn kind(&self) -> &'static str {
"pedit"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
if self.keys.is_empty() {
return Err(Error::InvalidMessage(
"pedit requires at least one key".into(),
));
}
let sel = pedit::TcPeditSel::new(self.action, self.keys.len() as u8);
let mut parms_data = Vec::new();
parms_data.extend_from_slice(sel.as_bytes());
for key in &self.keys {
let k = pedit::TcPeditKey::new(key.mask, key.val, key.off);
parms_data.extend_from_slice(k.as_bytes());
}
builder.append_attr(pedit::TCA_PEDIT_PARMS_EX, &parms_data);
let keys_ex_token = builder.nest_start(pedit::TCA_PEDIT_KEYS_EX);
for key in &self.keys {
let key_ex_token = builder.nest_start(pedit::TCA_PEDIT_KEY_EX);
builder.append_attr(pedit::TCA_PEDIT_KEY_EX_HTYPE, &key.htype.to_ne_bytes());
builder.append_attr(pedit::TCA_PEDIT_KEY_EX_CMD, &key.cmd.to_ne_bytes());
builder.nest_end(key_ex_token);
}
builder.nest_end(keys_ex_token);
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct ActionList {
actions: Vec<Box<dyn ActionConfigDyn>>,
}
pub trait ActionConfigDyn: Send + Sync + std::fmt::Debug {
fn kind(&self) -> &'static str;
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()>;
fn clone_box(&self) -> Box<dyn ActionConfigDyn>;
}
impl<T: ActionConfig + Clone + std::fmt::Debug + 'static> ActionConfigDyn for T {
fn kind(&self) -> &'static str {
ActionConfig::kind(self)
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
ActionConfig::write_options(self, builder)
}
fn clone_box(&self) -> Box<dyn ActionConfigDyn> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn ActionConfigDyn> {
fn clone(&self) -> Self {
self.clone_box()
}
}
impl ActionList {
pub fn new() -> Self {
Self::default()
}
pub fn with<T: ActionConfig + Clone + std::fmt::Debug + 'static>(mut self, action: T) -> Self {
self.actions.push(Box::new(action));
self
}
pub fn is_empty(&self) -> bool {
self.actions.is_empty()
}
pub fn len(&self) -> usize {
self.actions.len()
}
pub fn write_to(&self, builder: &mut MessageBuilder) -> Result<()> {
for (i, action) in self.actions.iter().enumerate() {
let act_token = builder.nest_start((i + 1) as u16);
builder.append_attr_str(action::TCA_ACT_KIND, action.kind());
let opt_token = builder.nest_start(action::TCA_ACT_OPTIONS);
action.write_options(builder)?;
builder.nest_end(opt_token);
builder.nest_end(act_token);
}
Ok(())
}
pub fn build(self) -> Self {
self
}
}
#[derive(Debug, Clone)]
#[must_use = "builders do nothing unless used"]
pub struct BpfAction {
fd: i32,
name: Option<String>,
action: i32,
}
impl BpfAction {
pub fn from_fd(fd: i32) -> Self {
Self {
fd,
name: None,
action: action::TC_ACT_PIPE,
}
}
pub fn from_pinned(path: impl AsRef<std::path::Path>) -> Result<Self> {
use std::os::unix::io::IntoRawFd;
let file = std::fs::File::open(path.as_ref())
.map_err(|e| Error::InvalidMessage(format!("open BPF pin: {e}")))?;
Ok(Self::from_fd(file.into_raw_fd()))
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn pipe(mut self) -> Self {
self.action = action::TC_ACT_PIPE;
self
}
pub fn ok(mut self) -> Self {
self.action = action::TC_ACT_OK;
self
}
#[allow(clippy::should_implement_trait)]
pub fn drop(mut self) -> Self {
self.action = action::TC_ACT_SHOT;
self
}
pub fn verdict(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut source: Option<Self> = None;
let mut name: Option<String> = None;
let mut verdict: Option<i32> = None;
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"pinned" => {
let s = action_need_value(params, i, "bpf", key)?;
if source.is_some() {
return Err(Error::InvalidMessage(
"bpf: only one of `pinned` / `fd` may be specified".to_string(),
));
}
source = Some(Self::from_pinned(s)?);
i += 2;
}
"fd" => {
let s = action_need_value(params, i, "bpf", key)?;
if source.is_some() {
return Err(Error::InvalidMessage(
"bpf: only one of `pinned` / `fd` may be specified".to_string(),
));
}
let fd_u = action_parse_u32("bpf", "fd", s)?;
source = Some(Self::from_fd(fd_u as i32));
i += 2;
}
"name" => {
let s = action_need_value(params, i, "bpf", key)?;
name = Some(s.to_string());
i += 2;
}
"verdict" => {
let s = action_need_value(params, i, "bpf", key)?;
verdict = Some(parse_gact_verdict(s).map_err(|e| {
Error::InvalidMessage(e.to_string().replace("gact:", "bpf:"))
})?);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"bpf: unknown token `{other}` (recognised: pinned <path>, fd <n>, name <text>, verdict <kw>)"
)));
}
}
}
let mut act = source.ok_or_else(|| {
Error::InvalidMessage(
"bpf: program source required (`pinned <path>` or `fd <n>`)".to_string(),
)
})?;
if let Some(n) = name {
act = act.name(n);
}
if let Some(v) = verdict {
act = act.verdict(v);
}
Ok(act)
}
pub fn build(self) -> Self {
self
}
}
impl ActionConfig for BpfAction {
fn kind(&self) -> &'static str {
"bpf"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::action::bpf_act;
let parms = bpf_act::TcActBpf::new(self.action);
builder.append_attr(bpf_act::TCA_ACT_BPF_PARMS, parms.as_bytes());
builder.append_attr_u32(bpf_act::TCA_ACT_BPF_FD, self.fd as u32);
if let Some(ref name) = self.name {
builder.append_attr_str(bpf_act::TCA_ACT_BPF_NAME, name);
}
Ok(())
}
}
#[derive(Debug, Clone)]
#[must_use = "builders do nothing unless used"]
pub struct SimpleAction {
sdata: String,
action: i32,
}
impl SimpleAction {
pub fn new(sdata: impl Into<String>) -> Self {
Self {
sdata: sdata.into(),
action: action::TC_ACT_PIPE,
}
}
pub fn ok(mut self) -> Self {
self.action = action::TC_ACT_OK;
self
}
#[allow(clippy::should_implement_trait)]
pub fn drop(mut self) -> Self {
self.action = action::TC_ACT_SHOT;
self
}
pub fn verdict(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut sdata: Option<&str> = None;
let mut verdict: Option<i32> = None;
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"sdata" => {
let s = action_need_value(params, i, "simple", key)?;
sdata = Some(s);
i += 2;
}
"verdict" => {
let s = action_need_value(params, i, "simple", key)?;
verdict = Some(parse_gact_verdict(s).map_err(|e| {
Error::InvalidMessage(e.to_string().replace("gact:", "simple:"))
})?);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"simple: unknown token `{other}` (recognised: sdata <text>, verdict <kw>)"
)));
}
}
}
let sdata = sdata
.ok_or_else(|| Error::InvalidMessage("simple: `sdata <text>` required".to_string()))?;
let mut act = Self::new(sdata);
if let Some(v) = verdict {
act = act.verdict(v);
}
Ok(act)
}
pub fn build(self) -> Self {
self
}
}
impl ActionConfig for SimpleAction {
fn kind(&self) -> &'static str {
"simple"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::action::simple_act;
let parms = simple_act::TcDefact::new(self.action);
builder.append_attr(simple_act::TCA_DEF_PARMS, parms.as_bytes());
let mut bytes = self.sdata.clone().into_bytes();
bytes.push(0);
builder.append_attr(simple_act::TCA_DEF_DATA, &bytes);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ActionMessage {
pub kind: String,
pub index: u32,
pub options_raw: Vec<u8>,
}
impl Connection<Route> {
#[tracing::instrument(level = "debug", skip_all, fields(method = "add_action", kind = %action.kind()))]
pub async fn add_action<A: ActionConfig>(&self, action: A) -> Result<()> {
let mut b = MessageBuilder::new(
NlMsgType::RTM_NEWACTION,
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE,
);
b.append(&TcMsg::default());
let tab = b.nest_start(TCA_ACT_TAB);
let act = b.nest_start(1);
b.append_attr(TCA_ACT_KIND, action.kind().as_bytes());
let opts = b.nest_start(TCA_ACT_OPTIONS);
action.write_options(&mut b)?;
b.nest_end(opts);
b.nest_end(act);
b.nest_end(tab);
self.send_ack(b).await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "del_action", kind = %kind, index))]
pub async fn del_action(&self, kind: &str, index: u32) -> Result<()> {
let mut b = MessageBuilder::new(NlMsgType::RTM_DELACTION, NLM_F_REQUEST | NLM_F_ACK);
b.append(&TcMsg::default());
let tab = b.nest_start(TCA_ACT_TAB);
let act = b.nest_start(1);
b.append_attr(TCA_ACT_KIND, kind.as_bytes());
b.append_attr_u32(TCA_ACT_INDEX, index);
b.nest_end(act);
b.nest_end(tab);
self.send_ack(b).await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "get_action", kind = %kind, index))]
pub async fn get_action(&self, kind: &str, index: u32) -> Result<Option<ActionMessage>> {
let mut b = MessageBuilder::new(NlMsgType::RTM_GETACTION, NLM_F_REQUEST);
b.append(&TcMsg::default());
let tab = b.nest_start(TCA_ACT_TAB);
let act = b.nest_start(1);
b.append_attr(TCA_ACT_KIND, kind.as_bytes());
b.append_attr_u32(TCA_ACT_INDEX, index);
b.nest_end(act);
b.nest_end(tab);
match self.send_request(b).await {
Ok(response) => {
let msgs = parse_action_messages(&response);
Ok(msgs.into_iter().next())
}
Err(e) if e.is_not_found() => Ok(None),
Err(e) => Err(e),
}
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "dump_actions", kind = %kind))]
pub async fn dump_actions(&self, kind: &str) -> Result<Vec<ActionMessage>> {
let mut b = dump_request(NlMsgType::RTM_GETACTION);
b.append(&TcMsg::default());
let tab = b.nest_start(TCA_ACT_TAB);
let act = b.nest_start(1);
if !kind.is_empty() {
b.append_attr(TCA_ACT_KIND, kind.as_bytes());
}
b.nest_end(act);
b.nest_end(tab);
let responses = self.send_dump(b).await?;
let mut actions = Vec::new();
for response in responses {
actions.extend(parse_action_messages(&response));
}
Ok(actions)
}
}
fn action_attr_slice(msg: &[u8]) -> Option<&[u8]> {
const NLMSG_HDRLEN: usize = 16;
let tcmsg_size = std::mem::size_of::<TcMsg>();
let start = NLMSG_HDRLEN + tcmsg_size;
if msg.len() < start {
return None;
}
Some(&msg[start..])
}
fn next_nla(input: &[u8]) -> Option<(u16, &[u8], &[u8])> {
use super::attr::NLA_TYPE_MASK;
if input.len() < 4 {
return None;
}
let len = u16::from_le_bytes([input[0], input[1]]) as usize;
let attr_type = u16::from_le_bytes([input[2], input[3]]) & NLA_TYPE_MASK;
if len < 4 || len > input.len() {
return None;
}
let payload = &input[4..len];
let aligned = (len + 3) & !3;
let rest = if aligned <= input.len() {
&input[aligned..]
} else {
&[]
};
Some((attr_type, payload, rest))
}
fn action_need_value<'a>(params: &[&'a str], i: usize, kind: &str, key: &str) -> Result<&'a str> {
params
.get(i + 1)
.copied()
.ok_or_else(|| Error::InvalidMessage(format!("{kind}: `{key}` requires a value")))
}
fn action_parse_u32(kind: &str, key: &str, s: &str) -> Result<u32> {
s.parse::<u32>().map_err(|_| {
Error::InvalidMessage(format!(
"{kind}: invalid {key} `{s}` (expected unsigned integer)"
))
})
}
fn parse_ipv4_range_or_single(s: &str) -> Result<(Ipv4Addr, Ipv4Addr)> {
if let Some((min_s, max_s)) = s.split_once('-') {
let min: Ipv4Addr = min_s.parse().map_err(|_| {
Error::InvalidMessage(format!("ct: invalid min address `{min_s}` in range"))
})?;
let max: Ipv4Addr = max_s.parse().map_err(|_| {
Error::InvalidMessage(format!("ct: invalid max address `{max_s}` in range"))
})?;
Ok((min, max))
} else {
let addr: Ipv4Addr = s
.parse()
.map_err(|_| Error::InvalidMessage(format!("ct: invalid address `{s}`")))?;
Ok((addr, addr))
}
}
fn parse_gact_verdict(s: &str) -> Result<i32> {
Ok(match s {
"pass" | "ok" => action::TC_ACT_OK,
"drop" | "shot" => action::TC_ACT_SHOT,
"pipe" => action::TC_ACT_PIPE,
"reclassify" => action::TC_ACT_RECLASSIFY,
"stolen" => action::TC_ACT_STOLEN,
"continue" => action::TC_ACT_UNSPEC,
other => {
return Err(Error::InvalidMessage(format!(
"gact: unknown verdict `{other}` (expected pass/drop/pipe/reclassify/stolen/continue)"
)));
}
})
}
fn parse_action_messages(msg: &[u8]) -> Vec<ActionMessage> {
let mut out = Vec::new();
let Some(attrs) = action_attr_slice(msg) else {
return out;
};
let mut input = attrs;
while let Some((attr_type, payload, rest)) = next_nla(input) {
input = rest;
if attr_type != TCA_ACT_TAB {
continue;
}
let mut slot_input = payload;
while let Some((_slot_id, slot_payload, slot_rest)) = next_nla(slot_input) {
slot_input = slot_rest;
if let Some(action) = parse_one_action(slot_payload) {
out.push(action);
}
}
}
out
}
fn parse_one_action(slot: &[u8]) -> Option<ActionMessage> {
let mut kind: Option<String> = None;
let mut index: u32 = 0;
let mut options_raw: Vec<u8> = Vec::new();
let mut input = slot;
while let Some((attr_type, payload, rest)) = next_nla(input) {
input = rest;
match attr_type {
TCA_ACT_KIND => {
let bytes = payload.split(|&b| b == 0).next().unwrap_or(payload);
kind = Some(String::from_utf8_lossy(bytes).into_owned());
}
TCA_ACT_INDEX if payload.len() >= 4 => {
index = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
}
TCA_ACT_OPTIONS => {
options_raw = payload.to_vec();
if index == 0
&& let Some((_kind_attr, parms_payload, _)) = next_nla(payload)
&& parms_payload.len() >= 4
{
index = u32::from_le_bytes([
parms_payload[0],
parms_payload[1],
parms_payload[2],
parms_payload[3],
]);
}
}
_ => {}
}
}
Some(ActionMessage {
kind: kind?,
index,
options_raw,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gact_action() {
let drop = GactAction::drop();
assert_eq!(drop.action, action::TC_ACT_SHOT);
assert_eq!(ActionConfig::kind(&drop), "gact");
let pass = GactAction::pass();
assert_eq!(pass.action, action::TC_ACT_OK);
let random = GactAction::new(action::TC_ACT_OK).random_drop(10).build();
assert_eq!(random.prob_type, gact::PGACT_NETRAND);
}
#[test]
fn test_police_action() {
let police = PoliceAction::new()
.rate(1_000_000)
.burst(32 * 1024)
.exceed_drop()
.build();
assert_eq!(police.rate, 1_000_000);
assert_eq!(police.burst, 32 * 1024);
assert_eq!(police.exceed_action, action::TC_ACT_SHOT);
assert_eq!(ActionConfig::kind(&police), "police");
}
#[test]
fn test_vlan_action() {
let pop = VlanAction::pop();
assert_eq!(pop.v_action, vlan::TCA_VLAN_ACT_POP);
assert_eq!(ActionConfig::kind(&pop), "vlan");
let push = VlanAction::push(100).priority(3).build();
assert_eq!(push.v_action, vlan::TCA_VLAN_ACT_PUSH);
assert_eq!(push.vlan_id, Some(100));
assert_eq!(push.vlan_prio, Some(3));
}
#[test]
fn test_skbedit_action() {
let edit = SkbeditAction::new().priority(7).mark(100).build();
assert_eq!(edit.priority, Some(7));
assert_eq!(edit.mark, Some(100));
assert_eq!(ActionConfig::kind(&edit), "skbedit");
}
#[test]
fn test_action_list() {
let list = ActionList::new().with(GactAction::drop()).build();
assert_eq!(list.len(), 1);
assert!(!list.is_empty());
}
#[test]
fn test_nat_action() {
let snat = NatAction::snat(Ipv4Addr::new(10, 0, 0, 0), Ipv4Addr::new(192, 168, 1, 1))
.prefix(8)
.build();
assert_eq!(snat.old_addr, Ipv4Addr::new(10, 0, 0, 0));
assert_eq!(snat.new_addr, Ipv4Addr::new(192, 168, 1, 1));
assert_eq!(snat.prefix_len, 8);
assert!(snat.egress);
assert_eq!(ActionConfig::kind(&snat), "nat");
let dnat = NatAction::dnat(Ipv4Addr::new(192, 168, 1, 1), Ipv4Addr::new(10, 0, 0, 1));
assert!(!dnat.egress);
}
#[test]
fn test_nat_prefix_to_mask() {
assert_eq!(NatAction::prefix_to_mask(0), 0);
assert_eq!(NatAction::prefix_to_mask(8), 0xFF000000);
assert_eq!(NatAction::prefix_to_mask(16), 0xFFFF0000);
assert_eq!(NatAction::prefix_to_mask(24), 0xFFFFFF00);
assert_eq!(NatAction::prefix_to_mask(32), 0xFFFFFFFF);
}
#[test]
fn test_tunnel_key_action() {
let set = TunnelKeyAction::set()
.src(Ipv4Addr::new(192, 168, 1, 1))
.dst(Ipv4Addr::new(192, 168, 1, 2))
.key_id(100)
.dst_port(4789)
.ttl(64)
.no_csum()
.build();
assert_eq!(set.t_action, tunnel_key::TCA_TUNNEL_KEY_ACT_SET);
assert_eq!(set.src_ipv4, Some(Ipv4Addr::new(192, 168, 1, 1)));
assert_eq!(set.dst_ipv4, Some(Ipv4Addr::new(192, 168, 1, 2)));
assert_eq!(set.key_id, Some(100));
assert_eq!(set.dst_port, Some(4789));
assert_eq!(set.ttl, Some(64));
assert!(set.no_csum);
assert_eq!(ActionConfig::kind(&set), "tunnel_key");
let release = TunnelKeyAction::release();
assert_eq!(release.t_action, tunnel_key::TCA_TUNNEL_KEY_ACT_RELEASE);
}
#[test]
fn test_connmark_action() {
let mark = ConnmarkAction::new();
assert_eq!(mark.zone, 0);
assert_eq!(ActionConfig::kind(&mark), "connmark");
let mark_zone = ConnmarkAction::with_zone(5);
assert_eq!(mark_zone.zone, 5);
let mark_custom = ConnmarkAction::new()
.zone(10)
.action(action::TC_ACT_OK)
.build();
assert_eq!(mark_custom.zone, 10);
assert_eq!(mark_custom.action, action::TC_ACT_OK);
}
#[test]
fn test_csum_action() {
let csum_action = CsumAction::new().iph().tcp().udp().build();
assert_eq!(
csum_action.update_flags,
csum::TCA_CSUM_UPDATE_FLAG_IPV4HDR
| csum::TCA_CSUM_UPDATE_FLAG_TCP
| csum::TCA_CSUM_UPDATE_FLAG_UDP
);
assert_eq!(ActionConfig::kind(&csum_action), "csum");
let all = CsumAction::new()
.iph()
.icmp()
.igmp()
.tcp()
.udp()
.udplite()
.sctp()
.build();
assert_eq!(all.update_flags, 0x7F); }
#[test]
fn test_sample_action() {
let sample = SampleAction::new(100, 5);
assert_eq!(sample.rate, 100);
assert_eq!(sample.group, 5);
assert_eq!(sample.trunc_size, None);
assert_eq!(ActionConfig::kind(&sample), "sample");
let sample_trunc = SampleAction::new(50, 10).trunc(128).build();
assert_eq!(sample_trunc.rate, 50);
assert_eq!(sample_trunc.group, 10);
assert_eq!(sample_trunc.trunc_size, Some(128));
}
#[test]
fn test_ct_action() {
let ct = CtAction::new();
assert_eq!(ct.ct_action, 0);
assert_eq!(ActionConfig::kind(&ct), "ct");
let commit = CtAction::commit();
assert_eq!(commit.ct_action, ct::TCA_CT_ACT_COMMIT);
let force = CtAction::commit().force();
assert_eq!(
force.ct_action,
ct::TCA_CT_ACT_COMMIT | ct::TCA_CT_ACT_FORCE
);
let clear = CtAction::clear();
assert_eq!(clear.ct_action, ct::TCA_CT_ACT_CLEAR);
let snat = CtAction::commit()
.nat_src_single(Ipv4Addr::new(10, 0, 0, 1))
.zone(1)
.mark(0x100, 0xffffffff)
.build();
assert!(snat.ct_action & ct::TCA_CT_ACT_NAT != 0);
assert!(snat.ct_action & ct::TCA_CT_ACT_NAT_SRC != 0);
assert_eq!(snat.zone, Some(1));
assert_eq!(snat.mark, Some(0x100));
assert_eq!(snat.nat_ipv4_min, Some(Ipv4Addr::new(10, 0, 0, 1)));
}
#[test]
fn test_pedit_action() {
let pedit = PeditAction::new()
.set_ipv4_src(Ipv4Addr::new(10, 0, 0, 1))
.build();
assert_eq!(pedit.keys.len(), 1);
assert_eq!(ActionConfig::kind(&pedit), "pedit");
let pedit_multi = PeditAction::new()
.set_ipv4_src(Ipv4Addr::new(10, 0, 0, 1))
.set_ipv4_dst(Ipv4Addr::new(10, 0, 0, 2))
.set_tcp_dport(8080)
.build();
assert_eq!(pedit_multi.keys.len(), 3);
let pedit_eth = PeditAction::new()
.set_eth_src([0x00, 0x11, 0x22, 0x33, 0x44, 0x55])
.build();
assert_eq!(pedit_eth.keys.len(), 2);
}
#[test]
fn test_bpf_action_default_pipe() {
let bpf = BpfAction::from_fd(42);
assert_eq!(bpf.action, action::TC_ACT_PIPE);
assert_eq!(bpf.fd, 42);
assert!(bpf.name.is_none());
assert_eq!(ActionConfig::kind(&bpf), "bpf");
}
#[test]
fn test_bpf_action_verdict_helpers() {
assert_eq!(BpfAction::from_fd(1).ok().action, action::TC_ACT_OK);
assert_eq!(BpfAction::from_fd(1).drop().action, action::TC_ACT_SHOT);
assert_eq!(BpfAction::from_fd(1).pipe().action, action::TC_ACT_PIPE);
assert_eq!(BpfAction::from_fd(1).verdict(7).action, 7);
}
#[test]
fn test_bpf_action_with_name() {
let bpf = BpfAction::from_fd(10).name("my_bpf").build();
assert_eq!(bpf.name.as_deref(), Some("my_bpf"));
}
#[test]
fn test_bpf_action_from_pinned_missing_path() {
let result = BpfAction::from_pinned("/nonexistent/path/that/should/not/exist");
assert!(result.is_err());
}
#[test]
fn test_bpf_action_writes_attrs() {
let bpf = BpfAction::from_fd(42).name("trace").build();
let mut builder = crate::netlink::builder::MessageBuilder::new(0, 0);
let start = builder.len();
ActionConfig::write_options(&bpf, &mut builder).unwrap();
let end = builder.len();
assert!(end > start);
}
#[test]
fn test_simple_action_default_pipe() {
let s = SimpleAction::new("trace");
assert_eq!(s.action, action::TC_ACT_PIPE);
assert_eq!(s.sdata, "trace");
assert_eq!(ActionConfig::kind(&s), "simple");
}
#[test]
fn test_simple_action_verdicts() {
assert_eq!(SimpleAction::new("x").ok().action, action::TC_ACT_OK);
assert_eq!(SimpleAction::new("x").drop().action, action::TC_ACT_SHOT);
assert_eq!(SimpleAction::new("x").verdict(99).action, 99);
}
#[test]
fn test_simple_action_writes_sdata_with_nul() {
let s = SimpleAction::new("hi").build();
let mut builder = crate::netlink::builder::MessageBuilder::new(0, 0);
let start = builder.len();
ActionConfig::write_options(&s, &mut builder).unwrap();
let bytes = &builder.as_bytes()[start..];
assert!(bytes.windows(3).any(|w| w == b"hi\0"));
}
fn build_add_action_frame<A: ActionConfig>(action: A) -> Vec<u8> {
let mut b = MessageBuilder::new(
NlMsgType::RTM_NEWACTION,
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE,
);
b.append(&TcMsg::default());
let tab = b.nest_start(TCA_ACT_TAB);
let act = b.nest_start(1);
b.append_attr(TCA_ACT_KIND, action.kind().as_bytes());
let opts = b.nest_start(TCA_ACT_OPTIONS);
action.write_options(&mut b).unwrap();
b.nest_end(opts);
b.nest_end(act);
b.nest_end(tab);
b.finish()
}
#[test]
fn add_action_gact_drop_roundtrips_through_parser() {
let frame = build_add_action_frame(GactAction::drop());
let parsed = parse_action_messages(&frame);
assert_eq!(parsed.len(), 1, "exactly one action slot");
assert_eq!(parsed[0].kind, "gact");
assert!(!parsed[0].options_raw.is_empty());
}
#[test]
fn add_action_mirred_roundtrips() {
let action = MirredAction::redirect_by_index(7);
let frame = build_add_action_frame(action);
let parsed = parse_action_messages(&frame);
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].kind, "mirred");
}
#[test]
fn add_action_police_roundtrips() {
let action = PoliceAction::new()
.rate(1_000_000)
.burst(32 * 1024)
.exceed_drop()
.build();
let frame = build_add_action_frame(action);
let parsed = parse_action_messages(&frame);
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].kind, "police");
}
#[test]
fn add_action_vlan_pop_roundtrips() {
let frame = build_add_action_frame(VlanAction::pop());
let parsed = parse_action_messages(&frame);
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].kind, "vlan");
}
#[test]
fn add_action_skbedit_roundtrips() {
let frame = build_add_action_frame(SkbeditAction::new().mark(0x42).build());
let parsed = parse_action_messages(&frame);
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].kind, "skbedit");
}
#[test]
fn del_action_emits_kind_plus_index_at_slot_level() {
let mut b = MessageBuilder::new(NlMsgType::RTM_DELACTION, NLM_F_REQUEST | NLM_F_ACK);
b.append(&TcMsg::default());
let tab = b.nest_start(TCA_ACT_TAB);
let act = b.nest_start(1);
b.append_attr(TCA_ACT_KIND, b"gact");
b.append_attr_u32(TCA_ACT_INDEX, 42);
b.nest_end(act);
b.nest_end(tab);
let frame = b.finish();
let parsed = parse_action_messages(&frame);
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].kind, "gact");
assert_eq!(parsed[0].index, 42);
assert!(parsed[0].options_raw.is_empty(), "del has no options");
let nlmsg_type = u16::from_le_bytes([frame[4], frame[5]]);
assert_eq!(nlmsg_type, NlMsgType::RTM_DELACTION);
}
#[test]
fn get_action_request_uses_request_only_flags() {
let mut b = MessageBuilder::new(NlMsgType::RTM_GETACTION, NLM_F_REQUEST);
b.append(&TcMsg::default());
let tab = b.nest_start(TCA_ACT_TAB);
let act = b.nest_start(1);
b.append_attr(TCA_ACT_KIND, b"mirred");
b.append_attr_u32(TCA_ACT_INDEX, 7);
b.nest_end(act);
b.nest_end(tab);
let frame = b.finish();
let nlmsg_type = u16::from_le_bytes([frame[4], frame[5]]);
assert_eq!(nlmsg_type, NlMsgType::RTM_GETACTION);
let flags = u16::from_le_bytes([frame[6], frame[7]]);
const NLM_F_DUMP: u16 = 0x300;
assert_eq!(flags & NLM_F_DUMP, 0);
assert_eq!(flags & NLM_F_ACK, 0);
assert!(flags & NLM_F_REQUEST != 0);
}
#[test]
fn parse_action_messages_handles_two_slots() {
let mut b = MessageBuilder::new(NlMsgType::RTM_NEWACTION, 0);
b.append(&TcMsg::default());
let tab = b.nest_start(TCA_ACT_TAB);
let act1 = b.nest_start(1);
b.append_attr(TCA_ACT_KIND, b"gact");
b.append_attr_u32(TCA_ACT_INDEX, 1);
b.nest_end(act1);
let act2 = b.nest_start(2);
b.append_attr(TCA_ACT_KIND, b"mirred");
b.append_attr_u32(TCA_ACT_INDEX, 2);
b.nest_end(act2);
b.nest_end(tab);
let frame = b.finish();
let parsed = parse_action_messages(&frame);
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0].kind, "gact");
assert_eq!(parsed[0].index, 1);
assert_eq!(parsed[1].kind, "mirred");
assert_eq!(parsed[1].index, 2);
}
#[test]
fn parse_action_messages_skips_truncated_input() {
let parsed = parse_action_messages(&[0u8; 8]);
assert!(parsed.is_empty());
}
fn write_options_bytes<A: ActionConfig>(a: &A) -> Vec<u8> {
let mut b = MessageBuilder::new(0, 0);
let start = b.len();
a.write_options(&mut b).unwrap();
b.as_bytes()[start..].to_vec()
}
#[test]
fn gact_parse_params_default_yields_pass() {
let a = GactAction::parse_params(&[]).unwrap();
assert_eq!(
write_options_bytes(&a),
write_options_bytes(&GactAction::pass())
);
}
#[test]
fn gact_parse_params_drop_alias_shot() {
let a = GactAction::parse_params(&["drop"]).unwrap();
let b = GactAction::parse_params(&["shot"]).unwrap();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
assert_eq!(
write_options_bytes(&a),
write_options_bytes(&GactAction::drop())
);
}
#[test]
fn gact_parse_params_pass_alias_ok() {
assert_eq!(
write_options_bytes(&GactAction::parse_params(&["pass"]).unwrap()),
write_options_bytes(&GactAction::parse_params(&["ok"]).unwrap()),
);
}
#[test]
fn gact_parse_params_goto_chain() {
let a = GactAction::parse_params(&["goto_chain", "5"]).unwrap();
let b = GactAction::goto_chain(5);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn gact_parse_params_random_determ() {
let a = GactAction::parse_params(&["pass", "random", "determ", "drop", "10"]).unwrap();
let b = GactAction::pass().deterministic(10, action::TC_ACT_SHOT);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn gact_parse_params_random_netrand_percent() {
let a = GactAction::parse_params(&["pass", "random", "netrand", "drop", "25"]).unwrap();
let b = GactAction::pass().random(25, action::TC_ACT_SHOT);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn gact_parse_params_unknown_token_errors() {
let err = GactAction::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("gact: unknown token"));
}
#[test]
fn gact_parse_params_random_unknown_kind_errors() {
let err = GactAction::parse_params(&["pass", "random", "wat", "drop", "10"]).unwrap_err();
assert!(err.to_string().contains("unknown random kind"));
}
#[test]
fn mirred_parse_params_egress_redirect_by_ifindex() {
let a = MirredAction::parse_params(&["egress", "redirect", "ifindex", "7"]).unwrap();
assert_eq!(
write_options_bytes(&a),
write_options_bytes(&MirredAction::redirect_by_index(7)),
);
}
#[test]
fn mirred_parse_params_ingress_mirror_by_ifindex() {
let a = MirredAction::parse_params(&["ingress", "mirror", "ifindex", "11"]).unwrap();
assert_eq!(
write_options_bytes(&a),
write_options_bytes(&MirredAction::ingress_mirror_by_index(11)),
);
}
#[test]
fn mirred_parse_params_token_order_independent() {
let a = MirredAction::parse_params(&["ifindex", "3", "ingress", "redirect"]).unwrap();
let b = MirredAction::parse_params(&["ingress", "redirect", "ifindex", "3"]).unwrap();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn mirred_parse_params_dev_and_ifindex_mutually_exclusive() {
let err = MirredAction::parse_params(&["ifindex", "1", "ifindex", "2"]).unwrap_err();
assert!(err.to_string().contains("mutually exclusive"));
}
#[test]
fn mirred_parse_params_missing_ifindex_errors() {
let err = MirredAction::parse_params(&["egress", "redirect"]).unwrap_err();
assert!(err.to_string().contains("target interface required"));
}
#[test]
fn vlan_parse_params_pop() {
let a = VlanAction::parse_params(&["pop"]).unwrap();
assert_eq!(
write_options_bytes(&a),
write_options_bytes(&VlanAction::pop())
);
}
#[test]
fn vlan_parse_params_push_with_priority() {
let a = VlanAction::parse_params(&["push", "100", "priority", "3"]).unwrap();
let b = VlanAction::push(100).priority(3);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn vlan_parse_params_modify_qinq() {
let a = VlanAction::parse_params(&["modify", "200", "protocol", "802.1ad"]).unwrap();
let b = VlanAction::modify(200).qinq();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn vlan_parse_params_id_out_of_range_errors() {
let err = VlanAction::parse_params(&["push", "5000"]).unwrap_err();
assert!(err.to_string().contains("out of range"));
}
#[test]
fn vlan_parse_params_missing_op_errors() {
let err = VlanAction::parse_params(&["priority", "3"]).unwrap_err();
assert!(err.to_string().contains("missing operation"));
}
#[test]
fn vlan_parse_params_unknown_protocol_errors() {
let err = VlanAction::parse_params(&["pop", "protocol", "wat"]).unwrap_err();
assert!(err.to_string().contains("unknown protocol"));
}
#[test]
fn skbedit_parse_params_priority() {
let a = SkbeditAction::parse_params(&["priority", "7"]).unwrap();
let b = SkbeditAction::new().priority(7).build();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn skbedit_parse_params_mark() {
let a = SkbeditAction::parse_params(&["mark", "42"]).unwrap();
let b = SkbeditAction::new().mark(42).build();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn skbedit_parse_params_mark_with_mask_any_order() {
let a = SkbeditAction::parse_params(&["mark", "1", "mask", "0xff"]).unwrap_err();
assert!(a.to_string().contains("expected unsigned integer"));
let b = SkbeditAction::parse_params(&["mark", "1", "mask", "255"]).unwrap();
let c = SkbeditAction::parse_params(&["mask", "255", "mark", "1"]).unwrap();
assert_eq!(write_options_bytes(&b), write_options_bytes(&c));
}
#[test]
fn skbedit_parse_params_mask_without_mark_errors() {
let err = SkbeditAction::parse_params(&["mask", "255"]).unwrap_err();
assert!(err.to_string().contains("`mask` requires a `mark`"));
}
#[test]
fn skbedit_parse_params_queue_mapping_out_of_range() {
let err = SkbeditAction::parse_params(&["queue_mapping", "100000"]).unwrap_err();
assert!(err.to_string().contains("out of range"));
}
#[test]
fn connmark_parse_params_default_zone_zero() {
let a = ConnmarkAction::parse_params(&[]).unwrap();
assert_eq!(
write_options_bytes(&a),
write_options_bytes(&ConnmarkAction::new()),
);
}
#[test]
fn connmark_parse_params_with_zone() {
let a = ConnmarkAction::parse_params(&["zone", "5"]).unwrap();
let b = ConnmarkAction::with_zone(5);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn connmark_parse_params_zone_out_of_range_errors() {
let err = ConnmarkAction::parse_params(&["zone", "100000"]).unwrap_err();
assert!(err.to_string().contains("out of range"));
}
#[test]
fn connmark_parse_params_unknown_token_errors() {
let err = ConnmarkAction::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("connmark: unknown token"));
}
#[test]
fn csum_parse_params_iph_only() {
let a = CsumAction::parse_params(&["iph"]).unwrap();
assert_eq!(
write_options_bytes(&a),
write_options_bytes(&CsumAction::new().iph())
);
}
#[test]
fn csum_parse_params_combo_any_order_idempotent() {
let a = CsumAction::parse_params(&["iph", "tcp", "udp"]).unwrap();
let b = CsumAction::parse_params(&["udp", "iph", "tcp"]).unwrap();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
let c = CsumAction::parse_params(&["iph", "iph", "tcp", "tcp", "udp"]).unwrap();
assert_eq!(write_options_bytes(&a), write_options_bytes(&c));
}
#[test]
fn csum_parse_params_all_kinds() {
let a = CsumAction::parse_params(&["iph", "icmp", "igmp", "tcp", "udp", "udplite", "sctp"])
.unwrap();
let b = CsumAction::new()
.iph()
.icmp()
.igmp()
.tcp()
.udp()
.udplite()
.sctp();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn csum_parse_params_unknown_kind_errors() {
let err = CsumAction::parse_params(&["iph", "wat"]).unwrap_err();
assert!(err.to_string().contains("unknown checksum kind"));
}
#[test]
fn sample_parse_params_required_rate_and_group() {
let a = SampleAction::parse_params(&["rate", "100", "group", "5"]).unwrap();
assert_eq!(
write_options_bytes(&a),
write_options_bytes(&SampleAction::new(100, 5))
);
}
#[test]
fn sample_parse_params_with_trunc_any_order() {
let a = SampleAction::parse_params(&["trunc", "128", "rate", "100", "group", "5"]).unwrap();
let b = SampleAction::new(100, 5).trunc(128);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn sample_parse_params_missing_rate_errors() {
let err = SampleAction::parse_params(&["group", "5"]).unwrap_err();
assert!(err.to_string().contains("`rate <N>` required"));
}
#[test]
fn sample_parse_params_missing_group_errors() {
let err = SampleAction::parse_params(&["rate", "100"]).unwrap_err();
assert!(err.to_string().contains("`group <G>` required"));
}
#[test]
fn tunnel_key_parse_params_set_full_v4() {
let a = TunnelKeyAction::parse_params(&[
"set", "src", "10.0.0.1", "dst", "10.0.0.2", "id", "100", "dst_port", "4789",
])
.unwrap();
let b = TunnelKeyAction::set()
.src("10.0.0.1".parse().unwrap())
.dst("10.0.0.2".parse().unwrap())
.key_id(100)
.dst_port(4789);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn tunnel_key_parse_params_release_emits_no_metadata() {
let a = TunnelKeyAction::parse_params(&["release"]).unwrap();
let b = TunnelKeyAction::release();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn tunnel_key_parse_params_set_with_no_csum_no_frag() {
let a = TunnelKeyAction::parse_params(&[
"set", "dst", "10.0.0.2", "id", "1", "no_csum", "no_frag",
])
.unwrap();
let b = TunnelKeyAction::set()
.dst("10.0.0.2".parse().unwrap())
.key_id(1)
.no_csum()
.no_frag();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn tunnel_key_parse_params_release_rejects_set_modifiers() {
let err = TunnelKeyAction::parse_params(&["release", "src", "10.0.0.1"]).unwrap_err();
assert!(err.to_string().contains("only valid with `set`"));
}
#[test]
fn tunnel_key_parse_params_missing_op_errors() {
let err = TunnelKeyAction::parse_params(&["src", "10.0.0.1"]).unwrap_err();
assert!(err.to_string().contains("missing operation"));
}
#[test]
fn tunnel_key_parse_params_invalid_dst_port_errors() {
let err = TunnelKeyAction::parse_params(&["set", "dst_port", "70000"]).unwrap_err();
assert!(err.to_string().contains("dst_port"));
}
#[test]
fn nat_parse_params_egress_snat_full_addr() {
let a = NatAction::parse_params(&["egress", "10.0.0.1", "192.168.0.1"]).unwrap();
let b = NatAction::snat("10.0.0.1".parse().unwrap(), "192.168.0.1".parse().unwrap());
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn nat_parse_params_ingress_dnat_with_prefix() {
let a = NatAction::parse_params(&["ingress", "10.0.0.0/24", "192.168.0.0"]).unwrap();
let b =
NatAction::dnat("10.0.0.0".parse().unwrap(), "192.168.0.0".parse().unwrap()).prefix(24);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn nat_parse_params_short_input_errors() {
let err = NatAction::parse_params(&["egress", "10.0.0.1"]).unwrap_err();
assert!(err.to_string().contains("requires"));
}
#[test]
fn nat_parse_params_unknown_direction_errors() {
let err = NatAction::parse_params(&["wat", "10.0.0.1", "192.168.0.1"]).unwrap_err();
assert!(err.to_string().contains("direction"));
}
#[test]
fn nat_parse_params_invalid_prefix_errors() {
let err = NatAction::parse_params(&["egress", "10.0.0.0/40", "192.168.0.0"]).unwrap_err();
assert!(err.to_string().contains("out of range"));
}
#[test]
fn simple_parse_params_sdata_required() {
let err = SimpleAction::parse_params(&[]).unwrap_err();
assert!(err.to_string().contains("`sdata <text>` required"));
}
#[test]
fn simple_parse_params_sdata_only() {
let a = SimpleAction::parse_params(&["sdata", "matched-port-80"]).unwrap();
let b = SimpleAction::new("matched-port-80");
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn simple_parse_params_with_verdict() {
let a = SimpleAction::parse_params(&["sdata", "trace", "verdict", "drop"]).unwrap();
let b = SimpleAction::new("trace").drop();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn simple_parse_params_unknown_verdict_rebrands_error_prefix() {
let err = SimpleAction::parse_params(&["sdata", "x", "verdict", "wat"]).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("simple:"),
"must rebrand gact: → simple: ({msg})"
);
assert!(!msg.contains("gact:"));
}
#[test]
fn bpf_parse_params_fd_basic() {
let a = BpfAction::parse_params(&["fd", "42"]).unwrap();
let b = BpfAction::from_fd(42);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn bpf_parse_params_fd_with_name() {
let a = BpfAction::parse_params(&["fd", "7", "name", "trace"]).unwrap();
let b = BpfAction::from_fd(7).name("trace");
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn bpf_parse_params_fd_with_verdict() {
let a = BpfAction::parse_params(&["fd", "1", "verdict", "drop"]).unwrap();
let b = BpfAction::from_fd(1).verdict(action::TC_ACT_SHOT);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn bpf_parse_params_pinned_missing_path_errors() {
let err = BpfAction::parse_params(&["pinned", "/nonexistent/xyzzy"]).unwrap_err();
assert!(err.to_string().contains("open BPF pin"));
}
#[test]
fn bpf_parse_params_no_source_errors() {
let err = BpfAction::parse_params(&["name", "x"]).unwrap_err();
assert!(err.to_string().contains("program source required"));
}
#[test]
fn bpf_parse_params_pinned_and_fd_mutually_exclusive() {
let err = BpfAction::parse_params(&["fd", "1", "fd", "2"]).unwrap_err();
assert!(err.to_string().contains("only one of"));
}
#[test]
fn police_parse_params_rate_and_burst() {
let a = PoliceAction::parse_params(&["rate", "1mbit", "burst", "32k"]).unwrap();
let b = PoliceAction::new()
.rate(crate::util::Rate::mbit(1).as_bytes_per_sec())
.burst(32 * 1024);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn police_parse_params_conform_exceed_pair() {
let a = PoliceAction::parse_params(&[
"rate",
"1mbit",
"burst",
"32k",
"conform-exceed",
"pass/drop",
])
.unwrap();
let b = PoliceAction::new()
.rate(crate::util::Rate::mbit(1).as_bytes_per_sec())
.burst(32 * 1024)
.conform(action::TC_ACT_OK)
.exceed(action::TC_ACT_SHOT);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn police_parse_params_individual_conform_exceed() {
let a = PoliceAction::parse_params(&[
"rate",
"1mbit",
"burst",
"32k",
"conform",
"pipe",
"exceed",
"reclassify",
])
.unwrap();
let b = PoliceAction::new()
.rate(crate::util::Rate::mbit(1).as_bytes_per_sec())
.burst(32 * 1024)
.conform(action::TC_ACT_PIPE)
.exceed(action::TC_ACT_RECLASSIFY);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn police_parse_params_invalid_rate_errors() {
let err = PoliceAction::parse_params(&["rate", "wat", "burst", "32k"]).unwrap_err();
assert!(err.to_string().contains("invalid rate"));
}
#[test]
fn police_parse_params_conform_exceed_missing_slash_errors() {
let err = PoliceAction::parse_params(&["conform-exceed", "passdrop"]).unwrap_err();
assert!(err.to_string().contains("requires `<conform>/<exceed>`"));
}
#[test]
fn police_parse_params_conform_exceed_unknown_verdict_rebrands() {
let err = PoliceAction::parse_params(&["conform-exceed", "wat/drop"]).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("police:"), "rebranded prefix expected: {msg}");
assert!(!msg.contains("gact:"));
}
#[test]
fn ct_parse_params_commit_with_zone_and_mark() {
let a = CtAction::parse_params(&["commit", "zone", "5", "mark", "0", "0"]).unwrap();
let b = CtAction::commit().zone(5).mark(0, 0);
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn ct_parse_params_clear_with_force() {
let a = CtAction::parse_params(&["clear", "force"]).unwrap();
let b = CtAction::clear().force();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn ct_parse_params_nat_src_single_addr() {
let a = CtAction::parse_params(&["commit", "nat", "src", "10.0.0.1"]).unwrap();
let b = CtAction::commit().nat_src_single("10.0.0.1".parse().unwrap());
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn ct_parse_params_nat_dst_address_range() {
let a = CtAction::parse_params(&["commit", "nat", "dst", "10.0.0.10-10.0.0.20"]).unwrap();
let b =
CtAction::commit().nat_dst("10.0.0.10".parse().unwrap(), "10.0.0.20".parse().unwrap());
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn ct_parse_params_zone_out_of_range_errors() {
let err = CtAction::parse_params(&["zone", "100000"]).unwrap_err();
assert!(err.to_string().contains("out of range"));
}
#[test]
fn ct_parse_params_mark_requires_two_values() {
let err = CtAction::parse_params(&["mark", "1"]).unwrap_err();
assert!(err.to_string().contains("`<value> <mask>`"));
}
#[test]
fn ct_parse_params_nat_unknown_direction_errors() {
let err = CtAction::parse_params(&["nat", "wat", "10.0.0.1"]).unwrap_err();
assert!(err.to_string().contains("must be `src` or `dst`"));
}
#[test]
fn pedit_parse_params_always_rejects_with_clear_message() {
let err = PeditAction::parse_params(&["munge", "ip", "src", "set", "1.2.3.4"]).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("pedit:"), "kind-prefixed: {msg}");
assert!(
msg.contains("not yet typed-modelled"),
"stub message: {msg}"
);
assert!(msg.contains("Plan 139"), "points at the plan: {msg}");
}
#[test]
fn pedit_parse_params_empty_input_also_rejects() {
let err = PeditAction::parse_params(&[]).unwrap_err();
assert!(err.to_string().contains("not yet typed-modelled"));
}
}