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, mpls, nat, pedit, police, sample, skbmod, 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)]
pub struct MplsAction {
m_action: i32,
label: Option<u32>,
tc: Option<u8>,
ttl: Option<u8>,
bos: Option<u8>,
proto: Option<u16>,
action: i32,
}
impl MplsAction {
fn with(m_action: i32) -> Self {
Self {
m_action,
label: None,
tc: None,
ttl: None,
bos: None,
proto: None,
action: action::TC_ACT_PIPE,
}
}
pub fn pop(proto: u16) -> Self {
let mut a = Self::with(mpls::TCA_MPLS_ACT_POP);
a.proto = Some(proto);
a
}
pub fn push(label: u32) -> Self {
let mut a = Self::with(mpls::TCA_MPLS_ACT_PUSH);
a.label = Some(label);
a
}
pub fn modify() -> Self {
Self::with(mpls::TCA_MPLS_ACT_MODIFY)
}
pub fn dec_ttl() -> Self {
Self::with(mpls::TCA_MPLS_ACT_DEC_TTL)
}
pub fn label(mut self, label: u32) -> Self {
self.label = Some(label);
self
}
pub fn tc(mut self, tc: u8) -> Self {
self.tc = Some(tc);
self
}
pub fn ttl(mut self, ttl: u8) -> Self {
self.ttl = Some(ttl);
self
}
pub fn bos(mut self, bos: u8) -> Self {
self.bos = Some(bos);
self
}
pub fn protocol(mut self, proto: u16) -> Self {
self.proto = Some(proto);
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut op: Option<i32> = None;
let mut label = None;
let mut tc = None;
let mut ttl = None;
let mut bos = None;
let mut proto = None;
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"pop" => {
op = Some(mpls::TCA_MPLS_ACT_POP);
i += 1;
}
"push" => {
op = Some(mpls::TCA_MPLS_ACT_PUSH);
i += 1;
}
"modify" => {
op = Some(mpls::TCA_MPLS_ACT_MODIFY);
i += 1;
}
"dec_ttl" => {
op = Some(mpls::TCA_MPLS_ACT_DEC_TTL);
i += 1;
}
"label" => {
let s = action_need_value(params, i, "mpls", key)?;
let v = action_parse_u32("mpls", "label", s)?;
if v > 0xF_FFFF {
return Err(Error::InvalidMessage(format!(
"mpls: label `{v}` out of range (0–1048575)"
)));
}
label = Some(v);
i += 2;
}
"tc" => {
let s = action_need_value(params, i, "mpls", key)?;
let v = action_parse_u32("mpls", "tc", s)?;
if v > 7 {
return Err(Error::InvalidMessage(format!(
"mpls: tc `{v}` out of range (0–7)"
)));
}
tc = Some(v as u8);
i += 2;
}
"ttl" => {
let s = action_need_value(params, i, "mpls", key)?;
let v = action_parse_u32("mpls", "ttl", s)?;
if v > 255 {
return Err(Error::InvalidMessage(format!(
"mpls: ttl `{v}` out of range (0–255)"
)));
}
ttl = Some(v as u8);
i += 2;
}
"bos" => {
let s = action_need_value(params, i, "mpls", key)?;
let v = action_parse_u32("mpls", "bos", s)?;
if v > 1 {
return Err(Error::InvalidMessage(format!(
"mpls: bos `{v}` out of range (0–1)"
)));
}
bos = Some(v as u8);
i += 2;
}
"protocol" => {
let s = action_need_value(params, i, "mpls", key)?;
proto = Some(parse_ethertype("mpls", s)?);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"mpls: unknown token `{other}` (recognised: pop, push, modify, dec_ttl, label <n>, tc <n>, ttl <n>, bos <n>, protocol <ethertype>)"
)));
}
}
}
let m_action = op.ok_or_else(|| {
Error::InvalidMessage(
"mpls: missing operation (pop, push, modify, or dec_ttl)".to_string(),
)
})?;
if m_action == mpls::TCA_MPLS_ACT_PUSH && label.is_none() {
return Err(Error::InvalidMessage(
"mpls: push requires a `label`".to_string(),
));
}
Ok(Self {
m_action,
label,
tc,
ttl,
bos,
proto,
action: action::TC_ACT_PIPE,
})
}
}
impl ActionConfig for MplsAction {
fn kind(&self) -> &'static str {
"mpls"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let parms = mpls::TcMpls::new(self.m_action, self.action);
builder.append_attr(mpls::TCA_MPLS_PARMS, parms.as_bytes());
if let Some(proto) = self.proto {
builder.append_attr(mpls::TCA_MPLS_PROTO, &proto.to_be_bytes());
}
if let Some(label) = self.label {
builder.append_attr_u32(mpls::TCA_MPLS_LABEL, label);
}
if let Some(tc) = self.tc {
builder.append_attr(mpls::TCA_MPLS_TC, &[tc]);
}
if let Some(ttl) = self.ttl {
builder.append_attr(mpls::TCA_MPLS_TTL, &[ttl]);
}
if let Some(bos) = self.bos {
builder.append_attr(mpls::TCA_MPLS_BOS, &[bos]);
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct SkbmodAction {
dmac: Option<[u8; 6]>,
smac: Option<[u8; 6]>,
etype: Option<u16>,
swap_mac: bool,
action: i32,
}
impl SkbmodAction {
pub fn new() -> Self {
Self {
action: action::TC_ACT_PIPE,
..Default::default()
}
}
pub fn set_dmac(mut self, dmac: [u8; 6]) -> Self {
self.dmac = Some(dmac);
self
}
pub fn set_smac(mut self, smac: [u8; 6]) -> Self {
self.smac = Some(smac);
self
}
pub fn set_etype(mut self, etype: u16) -> Self {
self.etype = Some(etype);
self
}
pub fn swap_mac(mut self) -> Self {
self.swap_mac = true;
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut a = Self::new();
let mut any = false;
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"set" => {
let what = action_need_value(params, i, "skbmod", "set")?;
let val = params.get(i + 2).copied().ok_or_else(|| {
Error::InvalidMessage(format!("skbmod: `set {what}` requires a value"))
})?;
match what {
"smac" => {
a.smac = Some(crate::util::addr::parse_mac(val).map_err(|_| {
Error::InvalidMessage(format!("skbmod: invalid smac `{val}`"))
})?);
}
"dmac" => {
a.dmac = Some(crate::util::addr::parse_mac(val).map_err(|_| {
Error::InvalidMessage(format!("skbmod: invalid dmac `{val}`"))
})?);
}
"etype" => {
a.etype = Some(parse_ethertype("skbmod", val)?);
}
other => {
return Err(Error::InvalidMessage(format!(
"skbmod: unknown `set` target `{other}` (expected smac, dmac, etype)"
)));
}
}
any = true;
i += 3;
}
"swap" => {
let what = action_need_value(params, i, "skbmod", "swap")?;
if what != "mac" {
return Err(Error::InvalidMessage(format!(
"skbmod: unknown `swap` target `{what}` (expected mac)"
)));
}
a.swap_mac = true;
any = true;
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"skbmod: unknown token `{other}` (recognised: set smac|dmac|etype <v>, swap mac)"
)));
}
}
}
if !any {
return Err(Error::InvalidMessage(
"skbmod: at least one operation required (set smac|dmac|etype, swap mac)"
.to_string(),
));
}
Ok(a)
}
}
impl ActionConfig for SkbmodAction {
fn kind(&self) -> &'static str {
"skbmod"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let mut flags = 0u64;
if self.dmac.is_some() {
flags |= skbmod::SKBMOD_F_DMAC;
}
if self.smac.is_some() {
flags |= skbmod::SKBMOD_F_SMAC;
}
if self.etype.is_some() {
flags |= skbmod::SKBMOD_F_ETYPE;
}
if self.swap_mac {
flags |= skbmod::SKBMOD_F_SWAPMAC;
}
let parms = skbmod::TcSkbmod::new(flags, self.action);
builder.append_attr(skbmod::TCA_SKBMOD_PARMS, parms.as_bytes());
if let Some(dmac) = self.dmac {
builder.append_attr(skbmod::TCA_SKBMOD_DMAC, &dmac);
}
if let Some(smac) = self.smac {
builder.append_attr(skbmod::TCA_SKBMOD_SMAC, &smac);
}
if let Some(etype) = self.etype {
builder.append_attr(skbmod::TCA_SKBMOD_ETYPE, &etype.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, Default)]
pub struct CtinfoAction {
dscp_mask: Option<u32>,
dscp_statemask: Option<u32>,
cpmark_mask: Option<u32>,
zone: Option<u16>,
action: i32,
}
impl CtinfoAction {
pub fn new() -> Self {
Self {
action: action::TC_ACT_PIPE,
..Default::default()
}
}
pub fn dscp(mut self, mask: u32, statemask: u32) -> Self {
self.dscp_mask = Some(mask);
self.dscp_statemask = Some(statemask);
self
}
pub fn cpmark(mut self, mask: u32) -> Self {
self.cpmark_mask = Some(mask);
self
}
pub fn zone(mut self, zone: u16) -> Self {
self.zone = Some(zone);
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut a = Self::new();
let mut any = false;
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"dscp" => {
let s = action_need_value(params, i, "ctinfo", "dscp")?;
let (mask_s, state_s) = match s.split_once('/') {
Some((m, st)) => (m, Some(st)),
None => (s, None),
};
let mask = ctinfo_parse_u32("dscp mask", mask_s)?;
let statemask = match state_s {
Some(st) => ctinfo_parse_u32("dscp statemask", st)?,
None => 0,
};
a = a.dscp(mask, statemask);
any = true;
i += 2;
}
"cpmark" => {
let next_is_mask = params
.get(i + 1)
.is_some_and(|s| ctinfo_parse_u32("cpmark mask", s).is_ok());
if next_is_mask {
let mask = ctinfo_parse_u32("cpmark mask", params[i + 1])?;
a = a.cpmark(mask);
i += 2;
} else {
a = a.cpmark(0xffff_ffff);
i += 1;
}
any = true;
}
"zone" => {
let s = action_need_value(params, i, "ctinfo", "zone")?;
let z = ctinfo_parse_u32("zone", s)?;
if z > u16::MAX as u32 {
return Err(Error::InvalidMessage(format!(
"ctinfo: zone `{z}` out of range (0–65535)"
)));
}
a = a.zone(z as u16);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"ctinfo: unknown token `{other}` (recognised: dscp <mask>[/<statemask>], cpmark [<mask>], zone <0–65535>)"
)));
}
}
}
if !any {
return Err(Error::InvalidMessage(
"ctinfo: at least one of `dscp`/`cpmark` is required".to_string(),
));
}
Ok(a)
}
}
impl ActionConfig for CtinfoAction {
fn kind(&self) -> &'static str {
"ctinfo"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::action::ctinfo;
let parms = ctinfo::TcCtinfo::new(self.action);
builder.append_attr(ctinfo::TCA_CTINFO_ACT, parms.as_bytes());
if let Some(zone) = self.zone {
builder.append_attr(ctinfo::TCA_CTINFO_ZONE, &zone.to_ne_bytes());
}
if let Some(mask) = self.dscp_mask {
builder.append_attr_u32(ctinfo::TCA_CTINFO_PARMS_DSCP_MASK, mask);
}
if let Some(statemask) = self.dscp_statemask {
builder.append_attr_u32(ctinfo::TCA_CTINFO_PARMS_DSCP_STATEMASK, statemask);
}
if let Some(mask) = self.cpmark_mask {
builder.append_attr_u32(ctinfo::TCA_CTINFO_PARMS_CPMARK_MASK, mask);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum IfeMeta {
Mark,
Prio,
TcIndex,
}
#[derive(Debug, Clone)]
struct IfeMetaEntry {
kind: IfeMeta,
value: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct IfeAction {
encode: bool,
dmac: Option<[u8; 6]>,
smac: Option<[u8; 6]>,
etype: Option<u16>,
metadata: Vec<IfeMetaEntry>,
action: i32,
}
impl IfeAction {
fn new(encode: bool) -> Self {
Self {
encode,
dmac: None,
smac: None,
etype: None,
metadata: Vec::new(),
action: action::TC_ACT_PIPE,
}
}
pub fn encode() -> Self {
Self::new(true)
}
pub fn decode() -> Self {
Self::new(false)
}
pub fn allow(mut self, meta: IfeMeta) -> Self {
self.metadata.push(IfeMetaEntry {
kind: meta,
value: None,
});
self
}
pub fn use_value(mut self, meta: IfeMeta, value: u32) -> Self {
self.metadata.push(IfeMetaEntry {
kind: meta,
value: Some(value),
});
self
}
pub fn dst(mut self, mac: [u8; 6]) -> Self {
self.dmac = Some(mac);
self
}
pub fn src(mut self, mac: [u8; 6]) -> Self {
self.smac = Some(mac);
self
}
pub fn etype(mut self, etype: u16) -> Self {
self.etype = Some(etype);
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn build(self) -> Self {
self
}
fn meta_from_str(kind: &str, s: &str) -> Result<IfeMeta> {
match s {
"mark" => Ok(IfeMeta::Mark),
"prio" => Ok(IfeMeta::Prio),
"tcindex" => Ok(IfeMeta::TcIndex),
other => Err(Error::InvalidMessage(format!(
"{kind}: unknown metadata `{other}` (expected mark, prio, tcindex)"
))),
}
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut a = match params.first() {
Some(&"encode") => Self::encode(),
Some(&"decode") => Self::decode(),
_ => {
return Err(Error::InvalidMessage(
"ife: first token must be `encode` or `decode`".to_string(),
));
}
};
let mut i = 1;
while i < params.len() {
let key = params[i];
match key {
"allow" => {
let s = action_need_value(params, i, "ife", "allow")?;
a = a.allow(Self::meta_from_str("ife", s)?);
i += 2;
}
"use" => {
let what = action_need_value(params, i, "ife", "use")?;
let meta = Self::meta_from_str("ife", what)?;
let val_s = params.get(i + 2).copied().ok_or_else(|| {
Error::InvalidMessage(format!("ife: `use {what}` requires a value"))
})?;
let val = action_parse_u32("ife", "use value", val_s)?;
a = a.use_value(meta, val);
i += 3;
}
"dst" => {
let s = action_need_value(params, i, "ife", "dst")?;
a = a.dst(crate::util::addr::parse_mac(s).map_err(|_| {
Error::InvalidMessage(format!("ife: invalid dst MAC `{s}`"))
})?);
i += 2;
}
"src" => {
let s = action_need_value(params, i, "ife", "src")?;
a = a.src(crate::util::addr::parse_mac(s).map_err(|_| {
Error::InvalidMessage(format!("ife: invalid src MAC `{s}`"))
})?);
i += 2;
}
"type" => {
let s = action_need_value(params, i, "ife", "type")?;
a = a.etype(parse_ethertype("ife", s)?);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"ife: unknown token `{other}` (recognised: encode|decode, allow <meta>, use <meta> <val>, dst/src <MAC>, type <ethertype>)"
)));
}
}
}
Ok(a)
}
}
impl ActionConfig for IfeAction {
fn kind(&self) -> &'static str {
"ife"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::action::ife;
let flags = if self.encode { ife::IFE_ENCODE } else { 0 };
let parms = ife::TcIfe::new(flags, self.action);
builder.append_attr(ife::TCA_IFE_PARMS, parms.as_bytes());
if let Some(dmac) = self.dmac {
builder.append_attr(ife::TCA_IFE_DMAC, &dmac);
}
if let Some(smac) = self.smac {
builder.append_attr(ife::TCA_IFE_SMAC, &smac);
}
if let Some(etype) = self.etype {
builder.append_attr(ife::TCA_IFE_TYPE, &etype.to_be_bytes());
}
if !self.metadata.is_empty() {
let lst = builder.nest_start(ife::TCA_IFE_METALST);
for entry in &self.metadata {
let attr = match entry.kind {
IfeMeta::Mark => ife::IFE_META_SKBMARK,
IfeMeta::Prio => ife::IFE_META_PRIO,
IfeMeta::TcIndex => ife::IFE_META_TCINDEX,
};
match entry.value {
None => builder.append_attr(attr, &[]),
Some(v) => match entry.kind {
IfeMeta::TcIndex => builder.append_attr(attr, &(v as u16).to_be_bytes()),
_ => builder.append_attr(attr, &v.to_be_bytes()),
},
}
}
builder.nest_end(lst);
}
Ok(())
}
}
#[derive(Debug, Clone)]
struct GateEntry {
open: bool,
interval_ns: u32,
ipv: Option<i32>,
max_octets: Option<i32>,
}
#[derive(Debug, Clone, Default)]
pub struct GateAction {
priority: Option<i32>,
base_time: Option<u64>,
cycle_time: Option<u64>,
cycle_time_ext: Option<u64>,
clockid: Option<i32>,
entries: Vec<GateEntry>,
action: i32,
}
impl GateAction {
pub fn new() -> Self {
Self {
action: action::TC_ACT_PIPE,
..Default::default()
}
}
pub fn priority(mut self, prio: i32) -> Self {
self.priority = Some(prio);
self
}
pub fn base_time(mut self, t: std::time::Duration) -> Self {
self.base_time = Some(t.as_nanos() as u64);
self
}
pub fn cycle_time(mut self, t: std::time::Duration) -> Self {
self.cycle_time = Some(t.as_nanos() as u64);
self
}
pub fn cycle_time_ext(mut self, t: std::time::Duration) -> Self {
self.cycle_time_ext = Some(t.as_nanos() as u64);
self
}
pub fn clockid(mut self, id: i32) -> Self {
self.clockid = Some(id);
self
}
pub fn sched_entry(
mut self,
open: bool,
interval: std::time::Duration,
ipv: Option<i32>,
max_octets: Option<i32>,
) -> Self {
self.entries.push(GateEntry {
open,
interval_ns: interval.as_nanos() as u32,
ipv,
max_octets,
});
self
}
pub fn action(mut self, action: i32) -> Self {
self.action = action;
self
}
pub fn build(self) -> Self {
self
}
fn clockid_from_str(s: &str) -> Result<i32> {
let bare = s.strip_prefix("CLOCK_").unwrap_or(s);
Ok(match bare {
"REALTIME" => 0,
"MONOTONIC" => 1,
"BOOTTIME" => 7,
"TAI" => 11,
other => other.parse::<i32>().map_err(|_| {
Error::InvalidMessage(format!(
"gate: unknown clockid `{other}` (expected TAI/REALTIME/MONOTONIC/BOOTTIME or a number)"
))
})?,
})
}
fn is_keyword(tok: &str) -> bool {
matches!(
tok,
"priority"
| "base-time"
| "cycle-time"
| "cycle-time-ext"
| "clockid"
| "sched-entry"
| "pass"
| "ok"
| "drop"
| "shot"
| "continue"
| "pipe"
| "reclassify"
)
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut a = Self::new();
let mut i = 0;
let parse_ns = |key: &str, s: &str| -> Result<u64> {
crate::util::parse::get_time(s)
.map(|d| d.as_nanos() as u64)
.map_err(|_| {
Error::InvalidMessage(format!(
"gate: invalid {key} `{s}` (expected a tc-style time)"
))
})
};
while i < params.len() {
let key = params[i];
match key {
"priority" => {
let s = action_need_value(params, i, "gate", "priority")?;
a.priority = Some(s.parse::<i32>().map_err(|_| {
Error::InvalidMessage(format!("gate: invalid priority `{s}`"))
})?);
i += 2;
}
"base-time" => {
let s = action_need_value(params, i, "gate", "base-time")?;
a.base_time = Some(parse_ns("base-time", s)?);
i += 2;
}
"cycle-time" => {
let s = action_need_value(params, i, "gate", "cycle-time")?;
a.cycle_time = Some(parse_ns("cycle-time", s)?);
i += 2;
}
"cycle-time-ext" => {
let s = action_need_value(params, i, "gate", "cycle-time-ext")?;
a.cycle_time_ext = Some(parse_ns("cycle-time-ext", s)?);
i += 2;
}
"clockid" => {
let s = action_need_value(params, i, "gate", "clockid")?;
a.clockid = Some(Self::clockid_from_str(s)?);
i += 2;
}
"sched-entry" => {
let state = action_need_value(params, i, "gate", "sched-entry")?;
let open = match state {
"open" => true,
"close" => false,
other => {
return Err(Error::InvalidMessage(format!(
"gate: sched-entry state `{other}` (expected open or close)"
)));
}
};
let interval_s = params.get(i + 2).copied().ok_or_else(|| {
Error::InvalidMessage(
"gate: `sched-entry` requires an interval".to_string(),
)
})?;
let interval_ns = parse_ns("sched-entry interval", interval_s)?;
let interval_ns: u32 = interval_ns.try_into().map_err(|_| {
Error::InvalidMessage(format!(
"gate: sched-entry interval `{interval_s}` exceeds u32 nanoseconds"
))
})?;
i += 3;
let mut ipv = None;
let mut max_octets = None;
if let Some(tok) = params.get(i)
&& !Self::is_keyword(tok)
{
ipv = Some(tok.parse::<i32>().map_err(|_| {
Error::InvalidMessage(format!("gate: invalid sched-entry ipv `{tok}`"))
})?);
i += 1;
if let Some(tok2) = params.get(i)
&& !Self::is_keyword(tok2)
{
max_octets = Some(tok2.parse::<i32>().map_err(|_| {
Error::InvalidMessage(format!(
"gate: invalid sched-entry max-octets `{tok2}`"
))
})?);
i += 1;
}
}
a.entries.push(GateEntry {
open,
interval_ns,
ipv,
max_octets,
});
}
other => {
return Err(Error::InvalidMessage(format!(
"gate: unknown token `{other}` (recognised: priority, base-time, cycle-time, cycle-time-ext, clockid, sched-entry open|close <interval> [ipv [max-octets]])"
)));
}
}
}
Ok(a)
}
}
impl ActionConfig for GateAction {
fn kind(&self) -> &'static str {
"gate"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::action::gate;
let parms = gate::TcGate::new(self.action);
builder.append_attr(gate::TCA_GATE_PARMS, parms.as_bytes());
if let Some(prio) = self.priority {
builder.append_attr_u32(gate::TCA_GATE_PRIORITY, prio as u32);
}
if let Some(t) = self.base_time {
builder.append_attr(gate::TCA_GATE_BASE_TIME, &t.to_ne_bytes());
}
if let Some(t) = self.cycle_time {
builder.append_attr(gate::TCA_GATE_CYCLE_TIME, &t.to_ne_bytes());
}
if let Some(t) = self.cycle_time_ext {
builder.append_attr(gate::TCA_GATE_CYCLE_TIME_EXT, &t.to_ne_bytes());
}
if let Some(id) = self.clockid {
builder.append_attr_u32(gate::TCA_GATE_CLOCKID, id as u32);
}
if !self.entries.is_empty() {
let list = builder.nest_start(gate::TCA_GATE_ENTRY_LIST);
for (idx, e) in self.entries.iter().enumerate() {
let one = builder.nest_start(gate::TCA_GATE_ONE_ENTRY);
builder.append_attr_u32(gate::TCA_GATE_ENTRY_INDEX, idx as u32);
if e.open {
builder.append_attr(gate::TCA_GATE_ENTRY_GATE, &[]);
}
builder.append_attr_u32(gate::TCA_GATE_ENTRY_INTERVAL, e.interval_ns);
if let Some(ipv) = e.ipv {
builder.append_attr_u32(gate::TCA_GATE_ENTRY_IPV, ipv as u32);
}
if let Some(mo) = e.max_octets {
builder.append_attr_u32(gate::TCA_GATE_ENTRY_MAX_OCTETS, mo as u32);
}
builder.nest_end(one);
}
builder.nest_end(list);
}
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> {
let mut a = Self::new();
let mut any = false;
let mut i = 0;
while i < params.len() {
match params[i] {
"ex" => {
i += 1;
}
"munge" => {
let (next, consumed) = a.parse_one_munge(¶ms[i + 1..])?;
a = next;
i += 1 + consumed;
any = true;
}
"pass" | "ok" | "drop" | "shot" | "pipe" | "reclassify" | "stolen" | "continue" => {
a.action = parse_gact_verdict(params[i])?;
i += 1;
}
other => {
return Err(Error::InvalidMessage(format!(
"pedit: unexpected token `{other}` (expected `munge` or a control \
verdict like pipe/pass/drop)"
)));
}
}
}
if !any {
return Err(Error::InvalidMessage(
"pedit: requires at least one `munge <field> set <value>` spec".to_string(),
));
}
Ok(a)
}
fn parse_one_munge(self, rest: &[&str]) -> Result<(Self, usize)> {
let layer = rest.first().copied().ok_or_else(|| {
Error::InvalidMessage("pedit: `munge` requires a field spec".to_string())
})?;
if layer == "offset" {
return self.parse_raw_munge(rest);
}
let field = rest.get(1).copied().ok_or_else(|| {
Error::InvalidMessage(format!("pedit: `munge {layer}` requires a field name"))
})?;
let op = rest.get(2).copied().ok_or_else(|| {
Error::InvalidMessage(format!("pedit: `munge {layer} {field}` requires an op"))
})?;
let value = rest.get(3).copied().ok_or_else(|| {
Error::InvalidMessage(format!(
"pedit: `munge {layer} {field} {op}` requires a value"
))
})?;
if op != "set" {
return Err(Error::InvalidMessage(format!(
"pedit: op `{op}` on `{layer} {field}` is not modelled by the typed builder — \
use `munge offset <off> {op} <val>` or PeditAction::set_raw / add_raw"
)));
}
let a = match (layer, field) {
("ip", "src") => self.set_ipv4_src(pedit_ipv4(layer, field, value)?),
("ip", "dst") => self.set_ipv4_dst(pedit_ipv4(layer, field, value)?),
("ip", "ttl") => self.set_ipv4_ttl(pedit_u8(layer, field, value)?),
("ip", "tos" | "dsfield") => self.set_ipv4_tos(pedit_u8(layer, field, value)?),
("tcp", "sport") => self.set_tcp_sport(pedit_u16(layer, field, value)?),
("tcp", "dport") => self.set_tcp_dport(pedit_u16(layer, field, value)?),
("udp", "sport") => self.set_udp_sport(pedit_u16(layer, field, value)?),
("udp", "dport") => self.set_udp_dport(pedit_u16(layer, field, value)?),
("eth", "src") => self.set_eth_src(pedit_mac(layer, field, value)?),
("eth", "dst") => self.set_eth_dst(pedit_mac(layer, field, value)?),
_ => {
return Err(Error::InvalidMessage(format!(
"pedit: `{layer} {field}` is not modelled by the typed builder \
(modelled: ip src|dst|ttl|tos, tcp/udp sport|dport, eth src|dst) — \
use `munge offset <off> set <val> [retain <mask>]` for raw edits"
)));
}
};
Ok((a, 4))
}
fn parse_raw_munge(self, rest: &[&str]) -> Result<(Self, usize)> {
let off = rest.get(1).copied().ok_or_else(|| {
Error::InvalidMessage("pedit: `munge offset` requires an offset".to_string())
})?;
let off = pedit_u32("offset", "offset", off)?;
let mut idx = 2;
if matches!(rest.get(idx).copied(), Some("u8" | "u16" | "u32")) {
idx += 1;
}
let op = rest.get(idx).copied().ok_or_else(|| {
Error::InvalidMessage("pedit: `munge offset` requires `set` or `add`".to_string())
})?;
let value = rest.get(idx + 1).copied().ok_or_else(|| {
Error::InvalidMessage(format!("pedit: `munge offset {off} {op}` requires a value"))
})?;
let val = pedit_u32("offset", "value", value)?;
idx += 2;
let mask = if matches!(rest.get(idx).copied(), Some("retain")) {
let m = rest.get(idx + 1).copied().ok_or_else(|| {
Error::InvalidMessage("pedit: `retain` requires a mask value".to_string())
})?;
idx += 2;
pedit_u32("offset", "retain", m)?
} else {
0
};
let htype = pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_NETWORK;
let a = match op {
"set" => self.set_raw(htype, off, val, mask),
"add" => self.add_raw(htype, off, val, mask),
other => {
return Err(Error::InvalidMessage(format!(
"pedit: op `{other}` on `offset` is not modelled (expected set or add)"
)));
}
};
Ok((a, idx))
}
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_ne_bytes([input[0], input[1]]) as usize;
let attr_type = u16::from_ne_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 ctinfo_parse_u32(what: &str, s: &str) -> Result<u32> {
let parsed = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
u32::from_str_radix(hex, 16)
} else {
s.parse::<u32>()
};
parsed.map_err(|_| {
Error::InvalidMessage(format!(
"ctinfo: invalid {what} `{s}` (expected 0x-hex or decimal u32)"
))
})
}
fn parse_ethertype(kind: &str, s: &str) -> Result<u16> {
match s {
"ipv4" | "ip" => Ok(0x0800),
"ipv6" | "ip6" => Ok(0x86DD),
"mpls_uc" => Ok(0x8847),
"mpls_mc" => Ok(0x8848),
_ => {
let parsed = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
u16::from_str_radix(hex, 16)
} else {
s.parse::<u16>()
};
parsed.map_err(|_| {
Error::InvalidMessage(format!(
"{kind}: invalid protocol `{s}` (expected ipv4, ipv6, mpls_uc, mpls_mc, 0xNN, or decimal u16)"
))
})
}
}
}
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 pedit_u32(layer: &str, field: &str, s: &str) -> Result<u32> {
let parsed = match s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
Some(hex) => u32::from_str_radix(hex, 16),
None => s.parse::<u32>(),
};
parsed.map_err(|_| {
Error::InvalidMessage(format!(
"pedit: invalid {layer} {field} value `{s}` (expected integer or 0x-hex)"
))
})
}
fn pedit_u8(layer: &str, field: &str, s: &str) -> Result<u8> {
let v = pedit_u32(layer, field, s)?;
u8::try_from(v).map_err(|_| {
Error::InvalidMessage(format!(
"pedit: {layer} {field} value `{s}` out of range for u8 (0..=255)"
))
})
}
fn pedit_u16(layer: &str, field: &str, s: &str) -> Result<u16> {
let v = pedit_u32(layer, field, s)?;
u16::try_from(v).map_err(|_| {
Error::InvalidMessage(format!(
"pedit: {layer} {field} value `{s}` out of range for u16 (0..=65535)"
))
})
}
fn pedit_ipv4(layer: &str, field: &str, s: &str) -> Result<Ipv4Addr> {
s.parse::<Ipv4Addr>().map_err(|_| {
Error::InvalidMessage(format!("pedit: invalid {layer} {field} IPv4 address `{s}`"))
})
}
fn pedit_mac(layer: &str, field: &str, s: &str) -> Result<[u8; 6]> {
crate::util::addr::parse_mac(s).map_err(|_| {
Error::InvalidMessage(format!("pedit: invalid {layer} {field} MAC address `{s}`"))
})
}
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_ne_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_ne_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_ne_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_ne_bytes([frame[4], frame[5]]);
assert_eq!(nlmsg_type, NlMsgType::RTM_GETACTION);
let flags = u16::from_ne_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 mpls_parse_params_push() {
let a =
MplsAction::parse_params(&["push", "label", "100", "ttl", "64", "tc", "3"]).unwrap();
let b = MplsAction::push(100).ttl(64).tc(3).build();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn mpls_parse_params_pop_protocol_names() {
let a = MplsAction::parse_params(&["pop", "protocol", "ipv4"]).unwrap();
assert_eq!(a.proto, Some(0x0800));
let b = MplsAction::parse_params(&["pop", "protocol", "0x8847"]).unwrap();
assert_eq!(b.proto, Some(0x8847));
}
#[test]
fn mpls_parse_params_strict() {
assert!(MplsAction::parse_params(&["push"]).is_err());
assert!(MplsAction::parse_params(&["label", "5"]).is_err());
assert!(MplsAction::parse_params(&["push", "label", "2000000"]).is_err());
assert!(MplsAction::parse_params(&["modify", "tc", "9"]).is_err());
assert!(MplsAction::parse_params(&["pop", "protocol", "nope"]).is_err());
assert!(MplsAction::parse_params(&["bogus"]).is_err());
}
#[test]
fn skbmod_parse_params_set_and_swap() {
let a = SkbmodAction::parse_params(&["set", "dmac", "02:00:00:00:00:01", "swap", "mac"])
.unwrap();
let b = SkbmodAction::new()
.set_dmac([0x02, 0, 0, 0, 0, 1])
.swap_mac()
.build();
assert_eq!(write_options_bytes(&a), write_options_bytes(&b));
}
#[test]
fn skbmod_parse_params_strict() {
assert!(SkbmodAction::parse_params(&[]).is_err());
assert!(SkbmodAction::parse_params(&["set", "smac", "zz"]).is_err());
assert!(SkbmodAction::parse_params(&["set", "bogus", "1"]).is_err());
assert!(SkbmodAction::parse_params(&["swap", "ip"]).is_err());
assert!(SkbmodAction::parse_params(&["nope"]).is_err());
}
#[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 ctinfo_parse_params_dscp_with_statemask() {
let a = CtinfoAction::parse_params(&["dscp", "0xfc/0x01", "zone", "5"]).unwrap();
assert_eq!(ActionConfig::kind(&a), "ctinfo");
assert_eq!(a.dscp_mask, Some(0xfc));
assert_eq!(a.dscp_statemask, Some(0x01));
assert_eq!(a.zone, Some(5));
let a2 = CtinfoAction::parse_params(&["dscp", "0xfc"]).unwrap();
assert_eq!(a2.dscp_mask, Some(0xfc));
assert_eq!(a2.dscp_statemask, Some(0));
}
#[test]
fn ctinfo_parse_params_cpmark_optional_mask() {
let a = CtinfoAction::parse_params(&["cpmark", "zone", "1"]).unwrap();
assert_eq!(a.cpmark_mask, Some(0xffff_ffff));
assert_eq!(a.zone, Some(1));
let a2 = CtinfoAction::parse_params(&["cpmark", "0x00ff"]).unwrap();
assert_eq!(a2.cpmark_mask, Some(0x00ff));
}
#[test]
fn ctinfo_parse_params_requires_dscp_or_cpmark() {
let err = CtinfoAction::parse_params(&["zone", "1"]).unwrap_err();
assert!(
err.to_string().contains("at least one of `dscp`/`cpmark`"),
"got: {err}"
);
}
#[test]
fn ctinfo_parse_params_strict_errors() {
assert!(
CtinfoAction::parse_params(&["nonsense"])
.unwrap_err()
.to_string()
.contains("ctinfo: unknown token")
);
assert!(
CtinfoAction::parse_params(&["dscp", "nothex"])
.unwrap_err()
.to_string()
.contains("invalid dscp mask")
);
assert!(
CtinfoAction::parse_params(&["zone", "70000"])
.unwrap_err()
.to_string()
.contains("out of range")
);
}
#[test]
fn ife_parse_params_encode_allow_and_use() {
let a = IfeAction::parse_params(&[
"encode",
"allow",
"mark",
"use",
"prio",
"7",
"dst",
"02:00:00:00:00:01",
"type",
"0xed3e",
])
.unwrap();
assert_eq!(ActionConfig::kind(&a), "ife");
assert_eq!(a.metadata.len(), 2);
assert_eq!(a.metadata[0].kind, IfeMeta::Mark);
assert_eq!(a.metadata[0].value, None); assert_eq!(a.metadata[1].kind, IfeMeta::Prio);
assert_eq!(a.metadata[1].value, Some(7)); assert_eq!(a.etype, Some(0xed3e));
assert!(a.dmac.is_some());
assert!(a.encode);
}
#[test]
fn ife_parse_params_decode_mode() {
let a = IfeAction::parse_params(&["decode", "allow", "tcindex"]).unwrap();
assert!(!a.encode);
assert_eq!(a.metadata[0].kind, IfeMeta::TcIndex);
}
#[test]
fn ife_parse_params_mode_required_first() {
let err = IfeAction::parse_params(&["allow", "mark"]).unwrap_err();
assert!(
err.to_string()
.contains("first token must be `encode` or `decode`")
);
}
#[test]
fn ife_parse_params_strict_errors() {
assert!(
IfeAction::parse_params(&["encode", "allow", "bogusmeta"])
.unwrap_err()
.to_string()
.contains("unknown metadata")
);
assert!(
IfeAction::parse_params(&["encode", "use", "mark"])
.unwrap_err()
.to_string()
.contains("requires a value")
);
assert!(
IfeAction::parse_params(&["encode", "nonsense"])
.unwrap_err()
.to_string()
.contains("ife: unknown token")
);
}
#[test]
fn gate_parse_params_schedule() {
let a = GateAction::parse_params(&[
"priority",
"0",
"cycle-time",
"2ms",
"clockid",
"TAI",
"sched-entry",
"open",
"1ms",
"sched-entry",
"close",
"1ms",
])
.unwrap();
assert_eq!(ActionConfig::kind(&a), "gate");
assert_eq!(a.priority, Some(0));
assert_eq!(a.cycle_time, Some(2_000_000));
assert_eq!(a.clockid, Some(11)); assert_eq!(a.entries.len(), 2);
assert!(a.entries[0].open);
assert_eq!(a.entries[0].interval_ns, 1_000_000);
assert!(!a.entries[1].open);
}
#[test]
fn gate_parse_params_entry_optional_ipv_max_octets() {
let a = GateAction::parse_params(&[
"sched-entry",
"open",
"300us",
"2",
"1500",
"sched-entry",
"close",
"300us",
])
.unwrap();
assert_eq!(a.entries.len(), 2);
assert_eq!(a.entries[0].ipv, Some(2));
assert_eq!(a.entries[0].max_octets, Some(1500));
assert_eq!(a.entries[1].ipv, None);
}
#[test]
fn gate_parse_params_clockid_names() {
assert_eq!(
GateAction::parse_params(&["clockid", "CLOCK_REALTIME"])
.unwrap()
.clockid,
Some(0)
);
assert_eq!(
GateAction::parse_params(&["clockid", "MONOTONIC"])
.unwrap()
.clockid,
Some(1)
);
}
#[test]
fn gate_parse_params_strict_errors() {
assert!(
GateAction::parse_params(&["nonsense"])
.unwrap_err()
.to_string()
.contains("gate: unknown token")
);
assert!(
GateAction::parse_params(&["sched-entry", "halfopen", "1ms"])
.unwrap_err()
.to_string()
.contains("expected open or close")
);
assert!(
GateAction::parse_params(&["cycle-time", "notatime"])
.unwrap_err()
.to_string()
.contains("invalid cycle-time")
);
}
#[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_ip_src_matches_builder() {
let parsed = PeditAction::parse_params(&["munge", "ip", "src", "set", "10.0.0.1"]).unwrap();
let built = PeditAction::new().set_ipv4_src("10.0.0.1".parse().unwrap());
assert_eq!(write_options_bytes(&parsed), write_options_bytes(&built));
}
#[test]
fn pedit_parse_multi_munge_and_control_action() {
let parsed = PeditAction::parse_params(&[
"munge",
"ip",
"dst",
"set",
"192.168.1.2",
"munge",
"tcp",
"dport",
"set",
"8080",
"pipe",
])
.unwrap();
let built = PeditAction::new()
.set_ipv4_dst("192.168.1.2".parse().unwrap())
.set_tcp_dport(8080)
.action(action::TC_ACT_PIPE);
assert_eq!(write_options_bytes(&parsed), write_options_bytes(&built));
}
#[test]
fn pedit_parse_eth_and_ttl_and_drop() {
let parsed = PeditAction::parse_params(&[
"munge",
"eth",
"dst",
"set",
"aa:bb:cc:dd:ee:ff",
"munge",
"ip",
"ttl",
"set",
"64",
"drop",
])
.unwrap();
let built = PeditAction::new()
.set_eth_dst([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff])
.set_ipv4_ttl(64)
.action(action::TC_ACT_SHOT);
assert_eq!(write_options_bytes(&parsed), write_options_bytes(&built));
}
#[test]
fn pedit_parse_raw_offset_with_size_and_retain() {
let parsed = PeditAction::parse_params(&[
"munge",
"offset",
"12",
"u32",
"set",
"0x0a000001",
"retain",
"0xffffffff",
])
.unwrap();
let built = PeditAction::new().set_raw(
pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_NETWORK,
12,
0x0a00_0001,
0xffff_ffff,
);
assert_eq!(write_options_bytes(&parsed), write_options_bytes(&built));
}
#[test]
fn pedit_parse_raw_offset_add_defaults_mask_zero() {
let parsed = PeditAction::parse_params(&["munge", "offset", "8", "add", "1"]).unwrap();
let built = PeditAction::new().add_raw(pedit::TCA_PEDIT_KEY_EX_HDR_TYPE_NETWORK, 8, 1, 0);
assert_eq!(write_options_bytes(&parsed), write_options_bytes(&built));
}
#[test]
fn pedit_parse_rejects_empty() {
let err = PeditAction::parse_params(&[]).unwrap_err();
assert!(err.to_string().contains("pedit:"), "{err}");
assert!(err.to_string().contains("at least one"), "{err}");
}
#[test]
fn pedit_parse_rejects_unmodelled_layer_field() {
let err = PeditAction::parse_params(&["munge", "ip6", "src", "set", "::1"]).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("not modelled"), "{msg}");
assert!(
msg.contains("offset"),
"points at the raw escape hatch: {msg}"
);
}
#[test]
fn pedit_parse_rejects_unmodelled_layered_op() {
let err = PeditAction::parse_params(&["munge", "ip", "ttl", "add", "1"]).unwrap_err();
assert!(err.to_string().contains("not modelled"), "{err}");
}
#[test]
fn pedit_parse_rejects_garbage_value() {
let err =
PeditAction::parse_params(&["munge", "tcp", "dport", "set", "notaport"]).unwrap_err();
assert!(err.to_string().contains("pedit:"), "{err}");
let err2 = PeditAction::parse_params(&["munge", "ip", "ttl", "set", "999"]).unwrap_err();
assert!(err2.to_string().contains("out of range"), "{err2}");
}
#[test]
fn pedit_parse_rejects_unknown_leading_token() {
let err = PeditAction::parse_params(&["frobnicate"]).unwrap_err();
assert!(err.to_string().contains("unexpected token"), "{err}");
}
}