From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox-firewall] nftables: move protocol match rendering into the lib
Date: Wed, 23 Sep 2026 12:00:20 +0200 [thread overview]
Message-ID: <bf2de1de-a487-4212-b0cc-9f14545be1d1@proxmox.com> (raw)
In-Reply-To: <20260827131524.1411472-1-h.laimer@proxmox.com>
On 8/27/26 3:15 PM, Hannes Laimer wrote:
> The statements selecting a transport protocol, its ports or an ICMP type
> and code were generated in proxmox-firewall, although which statements
> select a protocol is nftables knowledge and does not depend on the
> firewall config. Move the rendering into proxmox-nftables, working on
> plain expressions, and add an entry point taking the firewall's protocol
> type to the config extension. The firewall keeps its IP family pinning
> on top.
>
> No functional change intended.
>
> Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
> ---
> spiritually depends/based on [1], but apply order doesnt really matter,
> and they are both good on their own
>
> [1] https://lore.proxmox.com/pve-devel/20260827115803.1331245-1-h.laimer@proxmox.com/
>
> proxmox-firewall/src/rule.rs | 166 +++----------------------------
> proxmox-nftables/src/lib.rs | 1 +
> proxmox-nftables/src/protocol.rs | 79 +++++++++++++++
> 3 files changed, 93 insertions(+), 153 deletions(-)
> create mode 100644 proxmox-nftables/src/protocol.rs
>
> diff --git a/proxmox-firewall/src/rule.rs b/proxmox-firewall/src/rule.rs
> index 048c00e..ea63a08 100644
> --- a/proxmox-firewall/src/rule.rs
> +++ b/proxmox-firewall/src/rule.rs
> @@ -6,6 +6,7 @@ use proxmox_log as log;
> use proxmox_nftables::{
> Expression, Statement,
> expression::{Ct, IpFamily, Meta, Payload, Prefix},
> + protocol,
> statement::{Log, LogLevel, Match, Operator},
> types::{AddRule, ChainPart, SetName, TableFamily, TablePart},
> };
> @@ -19,9 +20,7 @@ use proxmox_ve_config::{
> ipset::{Ipfilter, IpsetName, RuleIpsetName},
> log::LogRateLimit,
> rule::{Direction, Kind, RuleGroup, Verdict as ConfigVerdict},
> - rule_match::{
> - Icmp, Icmpv6, IpAddrMatch, IpMatch, Ports, Protocol, RuleMatch, Sctp, Tcp, Udp,
> - },
> + rule_match::{IpAddrMatch, IpMatch, Protocol, RuleMatch},
> },
> },
> guest::types::Vmid,
> @@ -596,162 +595,23 @@ impl ToNftRules for IpMatch {
> }
> }
>
> -fn handle_protocol(rules: &mut [NftRule], _env: &NftRuleEnv, name: &str) -> Result<(), Error> {
> - for rule in rules.iter_mut() {
> - rule.push(Match::new_eq(Meta::new("l4proto"), Expression::from(name)).into());
> - }
> -
> - Ok(())
> -}
> -
> impl ToNftRules for Protocol {
> - fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
> - log::trace!("adding protocol: {self:?}");
> -
> - match self {
> - Protocol::Tcp(tcp) => tcp.to_nft_rules(rules, env),
> - Protocol::Udp(udp) => udp.to_nft_rules(rules, env),
> - Protocol::Dccp(ports) => {
> - handle_protocol(rules, env, "dccp")?;
> - ports.to_nft_rules(rules, env)
> - }
> - Protocol::UdpLite(ports) => {
> - handle_protocol(rules, env, "udplite")?;
> - ports.to_nft_rules(rules, env)
> - }
> - Protocol::Sctp(sctp) => sctp.to_nft_rules(rules, env),
> - Protocol::Icmp(icmp) => icmp.to_nft_rules(rules, env),
> - Protocol::Icmpv6(icmpv6) => icmpv6.to_nft_rules(rules, env),
> - Protocol::Named(name) => handle_protocol(rules, env, name),
> - Protocol::Numeric(id) => {
> - for rule in rules.iter_mut() {
> - rule.push(Match::new_eq(Meta::new("l4proto"), Expression::from(*id)).into());
> - }
> -
> - Ok(())
> - }
> - }
> - }
> -}
> -
> -impl ToNftRules for Tcp {
> - fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
> - handle_protocol(rules, env, "tcp")?;
> - self.ports().to_nft_rules(rules, env)
> - }
> -}
> -
> -impl ToNftRules for Udp {
> - fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
> - handle_protocol(rules, env, "udp")?;
> - self.ports().to_nft_rules(rules, env)
> - }
> -}
> -
> -impl ToNftRules for Sctp {
> - fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
> - handle_protocol(rules, env, "sctp")?;
> - self.ports().to_nft_rules(rules, env)
> - }
> -}
> -
> -impl ToNftRules for Icmp {
> fn to_nft_rules(&self, rules: &mut Vec<NftRule>, _env: &NftRuleEnv) -> Result<(), Error> {
> - for rule in rules.iter_mut() {
> - if matches!(rule.family(), Some(Family::V4) | None) {
> - if let Some(icmp_type) = self.ty() {
> - rule.push(
> - Match::new_eq(Payload::field("icmp", "type"), Expression::from(icmp_type))
> - .into(),
> - );
> - }
> -
> - if let Some(icmp_code) = self.code() {
> - rule.push(
> - Match::new_eq(Payload::field("icmp", "code"), Expression::from(icmp_code))
> - .into(),
> - );
> - }
> -
> - if self.code().is_none() && self.ty().is_none() {
> - rule.push(Match::new_eq(Meta::new("l4proto"), Expression::from("icmp")).into());
> - }
> -
> - rule.set_family(Family::V4);
> - }
> - }
> -
> - Ok(())
> - }
> -}
> -
> -impl ToNftRules for Icmpv6 {
> - fn to_nft_rules(&self, rules: &mut Vec<NftRule>, _env: &NftRuleEnv) -> Result<(), Error> {
> - log::trace!("applying icmpv6: {self:?}");
> + log::trace!("adding protocol: {self:?}");
>
> + let family = self.family();
> for rule in rules.iter_mut() {
> - if matches!(rule.family(), Some(Family::V6) | None) {
> - if let Some(icmp_type) = self.ty() {
> - rule.push(
> - Match::new_eq(
> - Payload::field("icmpv6", "type"),
> - Expression::from(icmp_type),
> - )
> - .into(),
> - );
> - }
> -
> - if let Some(icmp_code) = self.code() {
> - rule.push(
> - Match::new_eq(
> - Payload::field("icmpv6", "code"),
> - Expression::from(icmp_code),
> - )
> - .into(),
> - );
> - }
> -
> - if self.code().is_none() && self.ty().is_none() {
> - rule.push(
> - Match::new_eq(Meta::new("l4proto"), Expression::from("icmpv6")).into(),
> - );
> - }
> -
> - rule.set_family(Family::V6);
> + if family
> + .zip(rule.family())
> + .is_some_and(|(wanted, pinned)| wanted != pinned)
> + {
> + continue;
> }
> - }
> -
> - Ok(())
> - }
> -}
> -
> -impl ToNftRules for Ports {
> - fn to_nft_rules(&self, rules: &mut Vec<NftRule>, _env: &NftRuleEnv) -> Result<(), Error> {
> - log::trace!("applying ports: {self:?}");
> -
> - for rule in rules {
> - if let Some(sport) = self.sport() {
> - log::trace!("applying sport: {sport:?}");
> -
> - rule.push(
> - Match::new_eq(
> - Expression::from(Payload::field("th", "sport")),
> - Expression::from(sport),
> - )
> - .into(),
> - )
> + for statement in protocol::matches(self) {
> + rule.push(statement);
> }
nit: could be extend?
generally the whole function reads a bit awkward to me, but I cant think of anything
better on the spot.
> -
> - if let Some(dport) = self.dport() {
> - log::trace!("applying dport: {dport:?}");
> -
> - rule.push(
> - Match::new_eq(
> - Expression::from(Payload::field("th", "dport")),
> - Expression::from(dport),
> - )
> - .into(),
> - )
> + if let Some(family) = family {
> + rule.set_family(family);
> }
> }
>
> diff --git a/proxmox-nftables/src/lib.rs b/proxmox-nftables/src/lib.rs
> index 2003e1b..c54905b 100644
> --- a/proxmox-nftables/src/lib.rs
> +++ b/proxmox-nftables/src/lib.rs
> @@ -2,6 +2,7 @@ pub mod client;
> pub mod command;
> pub mod expression;
> pub mod helper;
> +pub mod protocol;
> pub mod statement;
> pub mod types;
>
> diff --git a/proxmox-nftables/src/protocol.rs b/proxmox-nftables/src/protocol.rs
> new file mode 100644
> index 0000000..fcd657f
> --- /dev/null
> +++ b/proxmox-nftables/src/protocol.rs
> @@ -0,0 +1,79 @@
> +//! Rendering of a transport protocol match into the statements that select it, which are
> +//! `meta l4proto` for the protocol, the transport-header ports, and the ICMP type and code.
> +//! The statements carry no IP family test of their own, so they work unchanged in the inet
> +//! and bridge families. An ICMP match still only applies to its own IP family, which is left
> +//! to the caller.
> +
> +#[cfg(feature = "config-ext")]
> +use proxmox_ve_config::firewall::types::rule_match::{Ports, Protocol};
> +
> +use crate::expression::{Meta, Payload};
> +use crate::statement::Match;
> +use crate::{Expression, Statement};
> +
> +/// Selects a protocol by name or number.
> +pub fn l4proto(protocol: impl Into<Expression>) -> Statement {
> + Match::new_eq(Meta::new("l4proto"), protocol.into()).into()
> +}
> +
> +/// Selects the transport-header source and destination ports that are given.
> +pub fn ports(sport: Option<Expression>, dport: Option<Expression>) -> Vec<Statement> {
> + let mut statements = Vec::new();
> + if let Some(sport) = sport {
> + statements.push(Match::new_eq(Payload::field("th", "sport"), sport).into());
> + }
> + if let Some(dport) = dport {
> + statements.push(Match::new_eq(Payload::field("th", "dport"), dport).into());
> + }
> + statements
> +}
> +
> +/// Selects an ICMP flavour by type and code. Matching either already implies the protocol, so
> +/// only a bare match needs the explicit `l4proto` test.
> +pub fn icmp(protocol: &str, ty: Option<Expression>, code: Option<Expression>) -> Vec<Statement> {
> + if ty.is_none() && code.is_none() {
> + return vec![l4proto(protocol)];
> + }
nit: is it necessary to short circuit rather than just do this and have the two if statements below?
let mut statements = vec![l4proto(protocol)];
iirc, nftables simplifies this when creating the rule anyway, so it shouldn't be less efficient in the
ruleset:
$ nft 'add rule inet test test-chain meta l4proto icmpv6 icmpv6 code 4'
$ nft 'list chain inet test test-chain'
chain test-chain {
icmpv6 code 4
}
> + let mut statements = Vec::new();
> + if let Some(ty) = ty {
> + statements.push(Match::new_eq(Payload::field(protocol, "type"), ty).into());
> + }
> + if let Some(code) = code {
> + statements.push(Match::new_eq(Payload::field(protocol, "code"), code).into());
> + }
> + statements
> +}
> +
> +/// The statements selecting `protocol`, in evaluation order.
> +#[cfg(feature = "config-ext")]
> +pub fn matches(protocol: &Protocol) -> Vec<Statement> {
> + match protocol {
> + Protocol::Tcp(tcp) => with_ports("tcp", tcp.ports()),
> + Protocol::Udp(udp) => with_ports("udp", udp.ports()),
> + Protocol::Sctp(sctp) => with_ports("sctp", sctp.ports()),
> + Protocol::Dccp(config) => with_ports("dccp", config),
> + Protocol::UdpLite(config) => with_ports("udplite", config),
> + Protocol::Icmp(config) => icmp(
> + "icmp",
> + config.ty().map(Expression::from),
> + config.code().map(Expression::from),
> + ),
> + Protocol::Icmpv6(config) => icmp(
> + "icmpv6",
> + config.ty().map(Expression::from),
> + config.code().map(Expression::from),
> + ),
> + Protocol::Named(name) => vec![l4proto(name.as_str())],
> + Protocol::Numeric(id) => vec![l4proto(*id)],
> + }
> +}
> +
> +#[cfg(feature = "config-ext")]
> +fn with_ports(protocol: &str, config: &Ports) -> Vec<Statement> {
> + let mut statements = vec![l4proto(protocol)];
> + statements.extend(ports(
> + config.sport().map(Expression::from),
> + config.dport().map(Expression::from),
> + ));
> + statements
> +}
next prev parent reply other threads:[~2026-09-23 10:00 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-27 13:15 [PATCH proxmox-firewall] nftables: move protocol match rendering into the lib Hannes Laimer
2026-09-23 10:00 ` Stefan Hanreich [this message]
2026-09-23 11:18 ` Hannes Laimer
2026-09-23 19:15 ` applied: " Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=bf2de1de-a487-4212-b0cc-9f14545be1d1@proxmox.com \
--to=s.hanreich@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox