From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Cc: Stefan Hanreich <s.hanreich@proxmox.com>,
Wolfgang Bumiller <w.bumiller@proxmox.com>
Subject: [pve-devel] [PATCH proxmox-firewall 09/37] config: firewall: add types for rules
Date: Tue, 2 Apr 2024 19:16:01 +0200 [thread overview]
Message-ID: <20240402171629.536804-10-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20240402171629.536804-1-s.hanreich@proxmox.com>
Additionally we implement FromStr for all rule types and parts, which
can be used for parsing firewall config rules. Initial rule parsing
works by parsing the different options into a HashMap and only then
de-serializing a struct from the parsed options.
This intermediate step makes rule parsing a lot easier, since we can
reuse the deserialization logic from serde. Also, we can split the
parsing/deserialization logic from the validation logic.
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/firewall/parse.rs | 185 ++++
proxmox-ve-config/src/firewall/types/mod.rs | 3 +
proxmox-ve-config/src/firewall/types/rule.rs | 412 ++++++++
.../src/firewall/types/rule_match.rs | 953 ++++++++++++++++++
4 files changed, 1553 insertions(+)
create mode 100644 proxmox-ve-config/src/firewall/types/rule.rs
create mode 100644 proxmox-ve-config/src/firewall/types/rule_match.rs
diff --git a/proxmox-ve-config/src/firewall/parse.rs b/proxmox-ve-config/src/firewall/parse.rs
index 669623b..227e045 100644
--- a/proxmox-ve-config/src/firewall/parse.rs
+++ b/proxmox-ve-config/src/firewall/parse.rs
@@ -1,3 +1,5 @@
+use std::fmt;
+
use anyhow::{bail, format_err, Error};
/// Parses out a "name" which can be alphanumeric and include dashes.
@@ -78,3 +80,186 @@ pub fn parse_bool(value: &str) -> Result<bool, Error> {
},
)
}
+
+/// `&str` deserializer which also accepts an `Option`.
+///
+/// Serde's `StringDeserializer` does not.
+#[derive(Clone, Copy, Debug)]
+pub struct SomeStrDeserializer<'a, E>(serde::de::value::StrDeserializer<'a, E>);
+
+impl<'de, 'a, E> serde::de::Deserializer<'de> for SomeStrDeserializer<'a, E>
+where
+ E: serde::de::Error,
+{
+ type Error = E;
+
+ fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ self.0.deserialize_any(visitor)
+ }
+
+ fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ visitor.visit_some(self.0)
+ }
+
+ fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ self.0.deserialize_str(visitor)
+ }
+
+ fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ self.0.deserialize_string(visitor)
+ }
+
+ fn deserialize_enum<V>(
+ self,
+ _name: &str,
+ _variants: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ visitor.visit_enum(self.0)
+ }
+
+ serde::forward_to_deserialize_any! {
+ bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
+ bytes byte_buf unit unit_struct newtype_struct seq tuple
+ tuple_struct map struct identifier ignored_any
+ }
+}
+
+/// `&str` wrapper which implements `IntoDeserializer` via `SomeStrDeserializer`.
+#[derive(Clone, Debug)]
+pub struct SomeStr<'a>(pub &'a str);
+
+impl<'a> From<&'a str> for SomeStr<'a> {
+ fn from(s: &'a str) -> Self {
+ Self(s)
+ }
+}
+
+impl<'de, 'a, E> serde::de::IntoDeserializer<'de, E> for SomeStr<'a>
+where
+ E: serde::de::Error,
+{
+ type Deserializer = SomeStrDeserializer<'a, E>;
+
+ fn into_deserializer(self) -> Self::Deserializer {
+ SomeStrDeserializer(self.0.into_deserializer())
+ }
+}
+
+/// `String` deserializer which also accepts an `Option`.
+///
+/// Serde's `StringDeserializer` does not.
+#[derive(Clone, Debug)]
+pub struct SomeStringDeserializer<E>(serde::de::value::StringDeserializer<E>);
+
+impl<'de, E> serde::de::Deserializer<'de> for SomeStringDeserializer<E>
+where
+ E: serde::de::Error,
+{
+ type Error = E;
+
+ fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ self.0.deserialize_any(visitor)
+ }
+
+ fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ visitor.visit_some(self.0)
+ }
+
+ fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ self.0.deserialize_str(visitor)
+ }
+
+ fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ self.0.deserialize_string(visitor)
+ }
+
+ fn deserialize_enum<V>(
+ self,
+ _name: &str,
+ _variants: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ visitor.visit_enum(self.0)
+ }
+
+ serde::forward_to_deserialize_any! {
+ bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
+ bytes byte_buf unit unit_struct newtype_struct seq tuple
+ tuple_struct map struct identifier ignored_any
+ }
+}
+
+/// `&str` wrapper which implements `IntoDeserializer` via `SomeStringDeserializer`.
+#[derive(Clone, Debug)]
+pub struct SomeString(pub String);
+
+impl From<&str> for SomeString {
+ fn from(s: &str) -> Self {
+ Self::from(s.to_string())
+ }
+}
+
+impl From<String> for SomeString {
+ fn from(s: String) -> Self {
+ Self(s)
+ }
+}
+
+impl<'de, E> serde::de::IntoDeserializer<'de, E> for SomeString
+where
+ E: serde::de::Error,
+{
+ type Deserializer = SomeStringDeserializer<E>;
+
+ fn into_deserializer(self) -> Self::Deserializer {
+ SomeStringDeserializer(self.0.into_deserializer())
+ }
+}
+
+#[derive(Debug)]
+pub struct SerdeStringError(String);
+
+impl std::error::Error for SerdeStringError {}
+
+impl fmt::Display for SerdeStringError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str(&self.0)
+ }
+}
+
+impl serde::de::Error for SerdeStringError {
+ fn custom<T: fmt::Display>(msg: T) -> Self {
+ Self(msg.to_string())
+ }
+}
diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs
index 5833787..b4a6b12 100644
--- a/proxmox-ve-config/src/firewall/types/mod.rs
+++ b/proxmox-ve-config/src/firewall/types/mod.rs
@@ -3,7 +3,10 @@ pub mod alias;
pub mod ipset;
pub mod log;
pub mod port;
+pub mod rule;
+pub mod rule_match;
pub use address::Cidr;
pub use alias::Alias;
pub use ipset::Ipset;
+pub use rule::Rule;
diff --git a/proxmox-ve-config/src/firewall/types/rule.rs b/proxmox-ve-config/src/firewall/types/rule.rs
new file mode 100644
index 0000000..20deb3a
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/types/rule.rs
@@ -0,0 +1,412 @@
+use core::fmt::Display;
+use std::fmt;
+use std::str::FromStr;
+
+use anyhow::{bail, ensure, format_err, Error};
+
+use crate::firewall::parse::match_name;
+use crate::firewall::types::rule_match::RuleMatch;
+use crate::firewall::types::rule_match::RuleOptions;
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub enum Direction {
+ #[default]
+ In,
+ Out,
+}
+
+impl std::str::FromStr for Direction {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Error> {
+ for (name, dir) in [("IN", Direction::In), ("OUT", Direction::Out)] {
+ if s.eq_ignore_ascii_case(name) {
+ return Ok(dir);
+ }
+ }
+
+ bail!("invalid direction: {s:?}, expect 'IN' or 'OUT'");
+ }
+}
+
+serde_plain::derive_deserialize_from_fromstr!(Direction, "valid packet direction");
+
+impl fmt::Display for Direction {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Direction::In => f.write_str("in"),
+ Direction::Out => f.write_str("out"),
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub enum Verdict {
+ Accept,
+ Reject,
+ #[default]
+ Drop,
+}
+
+impl std::str::FromStr for Verdict {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Error> {
+ for (name, verdict) in [
+ ("ACCEPT", Verdict::Accept),
+ ("REJECT", Verdict::Reject),
+ ("DROP", Verdict::Drop),
+ ] {
+ if s.eq_ignore_ascii_case(name) {
+ return Ok(verdict);
+ }
+ }
+ bail!("invalid verdict {s:?}, expected one of 'ACCEPT', 'REJECT' or 'DROP'");
+ }
+}
+
+impl Display for Verdict {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let string = match self {
+ Verdict::Accept => "ACCEPT",
+ Verdict::Drop => "DROP",
+ Verdict::Reject => "REJECT",
+ };
+
+ write!(f, "{string}")
+ }
+}
+
+serde_plain::derive_deserialize_from_fromstr!(Verdict, "valid verdict");
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Rule {
+ pub(crate) disabled: bool,
+ pub(crate) kind: Kind,
+ pub(crate) comment: Option<String>,
+}
+
+impl std::ops::Deref for Rule {
+ type Target = Kind;
+
+ fn deref(&self) -> &Self::Target {
+ &self.kind
+ }
+}
+
+impl std::ops::DerefMut for Rule {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.kind
+ }
+}
+
+impl FromStr for Rule {
+ type Err = Error;
+
+ fn from_str(input: &str) -> Result<Self, Self::Err> {
+ if input.contains(['\n', '\r']) {
+ bail!("rule must not contain any newlines!");
+ }
+
+ let (line, comment) = match input.rsplit_once('#') {
+ Some((line, comment)) if !comment.is_empty() => (line.trim(), Some(comment.trim())),
+ _ => (input.trim(), None),
+ };
+
+ let (disabled, line) = match line.strip_prefix('|') {
+ Some(line) => (true, line.trim_start()),
+ None => (false, line),
+ };
+
+ // todo: case insensitive?
+ let kind = if line.starts_with("GROUP") {
+ Kind::from(line.parse::<RuleGroup>()?)
+ } else {
+ Kind::from(line.parse::<RuleMatch>()?)
+ };
+
+ Ok(Self {
+ disabled,
+ comment: comment.map(str::to_string),
+ kind,
+ })
+ }
+}
+
+impl Rule {
+ pub fn iface(&self) -> Option<&str> {
+ match &self.kind {
+ Kind::Group(group) => group.iface(),
+ Kind::Match(rule) => rule.iface(),
+ }
+ }
+
+ pub fn disabled(&self) -> bool {
+ self.disabled
+ }
+
+ pub fn kind(&self) -> &Kind {
+ &self.kind
+ }
+
+ pub fn comment(&self) -> Option<&str> {
+ self.comment.as_deref()
+ }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum Kind {
+ Group(RuleGroup),
+ Match(RuleMatch),
+}
+
+impl Kind {
+ pub fn is_group(&self) -> bool {
+ matches!(self, Kind::Group(_))
+ }
+
+ pub fn is_match(&self) -> bool {
+ matches!(self, Kind::Match(_))
+ }
+}
+
+impl From<RuleGroup> for Kind {
+ fn from(value: RuleGroup) -> Self {
+ Kind::Group(value)
+ }
+}
+
+impl From<RuleMatch> for Kind {
+ fn from(value: RuleMatch) -> Self {
+ Kind::Match(value)
+ }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct RuleGroup {
+ pub(crate) group: String,
+ pub(crate) iface: Option<String>,
+}
+
+impl RuleGroup {
+ pub(crate) fn from_options(group: String, options: RuleOptions) -> Result<Self, Error> {
+ ensure!(
+ options.proto.is_none()
+ && options.dport.is_none()
+ && options.sport.is_none()
+ && options.dest.is_none()
+ && options.source.is_none()
+ && options.log.is_none()
+ && options.icmp_type.is_none(),
+ "only interface parameter is permitted for group rules"
+ );
+
+ Ok(Self {
+ group,
+ iface: options.iface,
+ })
+ }
+
+ pub fn group(&self) -> &str {
+ &self.group
+ }
+
+ pub fn iface(&self) -> Option<&str> {
+ self.iface.as_deref()
+ }
+}
+
+impl FromStr for RuleGroup {
+ type Err = Error;
+
+ fn from_str(input: &str) -> Result<Self, Self::Err> {
+ let (keyword, rest) = match_name(input)
+ .ok_or_else(|| format_err!("expected a leading keyword in rule group"))?;
+
+ if !keyword.eq_ignore_ascii_case("group") {
+ bail!("Expected keyword GROUP")
+ }
+
+ let (name, rest) =
+ match_name(rest.trim()).ok_or_else(|| format_err!("expected a name for rule group"))?;
+
+ let options = rest.trim_start().parse()?;
+
+ Self::from_options(name.to_string(), options)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::firewall::types::{
+ address::{IpEntry, IpList},
+ alias::{AliasName, AliasScope},
+ ipset::{IpsetName, IpsetScope},
+ log::LogLevel,
+ rule_match::{Icmp, IcmpCode, IpAddrMatch, IpMatch, Ports, Protocol, Udp},
+ Cidr,
+ };
+
+ use super::*;
+
+ #[test]
+ fn test_parse_rule() {
+ let mut rule: Rule = "|GROUP tgr -i eth0 # acomm".parse().expect("valid rule");
+
+ assert_eq!(
+ rule,
+ Rule {
+ disabled: true,
+ comment: Some("acomm".to_string()),
+ kind: Kind::Group(RuleGroup {
+ group: "tgr".to_string(),
+ iface: Some("eth0".to_string()),
+ }),
+ },
+ );
+
+ rule = "IN ACCEPT -p udp -dport 33 -sport 22 -log warning"
+ .parse()
+ .expect("valid rule");
+
+ assert_eq!(
+ rule,
+ Rule {
+ disabled: false,
+ comment: None,
+ kind: Kind::Match(RuleMatch {
+ dir: Direction::In,
+ verdict: Verdict::Accept,
+ proto: Some(Udp::new(Ports::from_u16(22, 33)).into()),
+ log: Some(LogLevel::Warning),
+ ..Default::default()
+ }),
+ }
+ );
+
+ rule = "IN ACCEPT --proto udp -i eth0".parse().expect("valid rule");
+
+ assert_eq!(
+ rule,
+ Rule {
+ disabled: false,
+ comment: None,
+ kind: Kind::Match(RuleMatch {
+ dir: Direction::In,
+ verdict: Verdict::Accept,
+ proto: Some(Udp::new(Ports::new(None, None)).into()),
+ iface: Some("eth0".to_string()),
+ ..Default::default()
+ }),
+ }
+ );
+
+ rule = " OUT DROP \
+ -source 10.0.0.0/24 -dest 20.0.0.0-20.255.255.255,192.168.0.0/16 \
+ -p icmp -log nolog -icmp-type port-unreachable "
+ .parse()
+ .expect("valid rule");
+
+ assert_eq!(
+ rule,
+ Rule {
+ disabled: false,
+ comment: None,
+ kind: Kind::Match(RuleMatch {
+ dir: Direction::Out,
+ verdict: Verdict::Drop,
+ ip: IpMatch::new(
+ IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 24).unwrap())),
+ IpAddrMatch::Ip(
+ IpList::new(vec![
+ IpEntry::Range([20, 0, 0, 0].into(), [20, 255, 255, 255].into()),
+ IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 0], 16).unwrap()),
+ ])
+ .unwrap()
+ ),
+ )
+ .ok(),
+ proto: Some(Protocol::Icmp(Icmp::new_code(IcmpCode::Named(
+ "port-unreachable"
+ )))),
+ log: Some(LogLevel::Nolog),
+ ..Default::default()
+ }),
+ }
+ );
+
+ rule = "IN BGP(ACCEPT) --log crit --iface eth0"
+ .parse()
+ .expect("valid rule");
+
+ assert_eq!(
+ rule,
+ Rule {
+ disabled: false,
+ comment: None,
+ kind: Kind::Match(RuleMatch {
+ dir: Direction::In,
+ verdict: Verdict::Accept,
+ log: Some(LogLevel::Critical),
+ fw_macro: Some("BGP".to_string()),
+ iface: Some("eth0".to_string()),
+ ..Default::default()
+ }),
+ }
+ );
+
+ rule = "IN ACCEPT --source dc/test --dest +dc/test"
+ .parse()
+ .expect("valid rule");
+
+ assert_eq!(
+ rule,
+ Rule {
+ disabled: false,
+ comment: None,
+ kind: Kind::Match(RuleMatch {
+ dir: Direction::In,
+ verdict: Verdict::Accept,
+ ip: Some(
+ IpMatch::new(
+ IpAddrMatch::Alias(AliasName::new(AliasScope::Datacenter, "test")),
+ IpAddrMatch::Set(IpsetName::new(IpsetScope::Datacenter, "test"),),
+ )
+ .unwrap()
+ ),
+ ..Default::default()
+ }),
+ }
+ );
+
+ rule = "IN REJECT".parse().expect("valid rule");
+
+ assert_eq!(
+ rule,
+ Rule {
+ disabled: false,
+ comment: None,
+ kind: Kind::Match(RuleMatch {
+ dir: Direction::In,
+ verdict: Verdict::Reject,
+ ..Default::default()
+ }),
+ }
+ );
+
+ "IN DROP ---log crit"
+ .parse::<Rule>()
+ .expect_err("too many dashes in option");
+
+ "IN DROP --log --iface eth0"
+ .parse::<Rule>()
+ .expect_err("no value for option");
+
+ "IN DROP --log crit --iface"
+ .parse::<Rule>()
+ .expect_err("no value for option");
+ }
+}
diff --git a/proxmox-ve-config/src/firewall/types/rule_match.rs b/proxmox-ve-config/src/firewall/types/rule_match.rs
new file mode 100644
index 0000000..ae5345c
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/types/rule_match.rs
@@ -0,0 +1,953 @@
+use std::collections::HashMap;
+use std::fmt;
+use std::str::FromStr;
+
+use serde::Deserialize;
+
+use anyhow::{bail, format_err, Error};
+use serde::de::IntoDeserializer;
+
+use crate::firewall::parse::{match_name, match_non_whitespace, SomeStr};
+use crate::firewall::types::address::{Family, IpList};
+use crate::firewall::types::alias::AliasName;
+use crate::firewall::types::ipset::IpsetName;
+use crate::firewall::types::log::LogLevel;
+use crate::firewall::types::port::PortList;
+use crate::firewall::types::rule::{Direction, Verdict};
+
+#[derive(Clone, Debug, Default, Deserialize)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+#[serde(deny_unknown_fields, rename_all = "kebab-case")]
+pub(crate) struct RuleOptions {
+ #[serde(alias = "p")]
+ pub(crate) proto: Option<String>,
+
+ pub(crate) dport: Option<String>,
+ pub(crate) sport: Option<String>,
+
+ pub(crate) dest: Option<String>,
+ pub(crate) source: Option<String>,
+
+ #[serde(alias = "i")]
+ pub(crate) iface: Option<String>,
+
+ pub(crate) log: Option<LogLevel>,
+ pub(crate) icmp_type: Option<String>,
+}
+
+impl FromStr for RuleOptions {
+ type Err = Error;
+
+ fn from_str(mut line: &str) -> Result<Self, Self::Err> {
+ let mut options = HashMap::new();
+
+ loop {
+ line = line.trim_start();
+
+ if line.is_empty() {
+ break;
+ }
+
+ line = line
+ .strip_prefix('-')
+ .ok_or_else(|| format_err!("expected an option starting with '-'"))?;
+
+ // second dash is optional
+ line = line.strip_prefix('-').unwrap_or(line);
+
+ let param;
+ (param, line) = match_name(line)
+ .ok_or_else(|| format_err!("expected a parameter name after '-'"))?;
+
+ let value;
+ (value, line) = match_non_whitespace(line.trim_start())
+ .ok_or_else(|| format_err!("expected a value for {param:?}"))?;
+
+ if options.insert(param, SomeStr(value)).is_some() {
+ bail!("duplicate option in rule: {param}")
+ }
+ }
+
+ Ok(RuleOptions::deserialize(IntoDeserializer::<
+ '_,
+ crate::firewall::parse::SerdeStringError,
+ >::into_deserializer(
+ options
+ ))?)
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct RuleMatch {
+ pub(crate) dir: Direction,
+ pub(crate) verdict: Verdict,
+ pub(crate) fw_macro: Option<String>,
+
+ pub(crate) iface: Option<String>,
+ pub(crate) log: Option<LogLevel>,
+ pub(crate) ip: Option<IpMatch>,
+ pub(crate) proto: Option<Protocol>,
+}
+
+impl RuleMatch {
+ pub(crate) fn from_options(
+ dir: Direction,
+ verdict: Verdict,
+ fw_macro: impl Into<Option<String>>,
+ options: RuleOptions,
+ ) -> Result<Self, Error> {
+ if options.dport.is_some() && options.icmp_type.is_some() {
+ bail!("dport and icmp-type are mutually exclusive");
+ }
+
+ let ip = IpMatch::from_options(&options)?;
+ let proto = Protocol::from_options(&options)?;
+
+ // todo: check protocol & IP Version compatibility
+
+ Ok(Self {
+ dir,
+ verdict,
+ fw_macro: fw_macro.into(),
+ iface: options.iface,
+ log: options.log,
+ ip,
+ proto,
+ })
+ }
+
+ pub fn direction(&self) -> Direction {
+ self.dir
+ }
+
+ pub fn iface(&self) -> Option<&str> {
+ self.iface.as_deref()
+ }
+
+ pub fn verdict(&self) -> Verdict {
+ self.verdict
+ }
+
+ pub fn fw_macro(&self) -> Option<&str> {
+ self.fw_macro.as_deref()
+ }
+
+ pub fn log(&self) -> Option<LogLevel> {
+ self.log
+ }
+
+ pub fn ip(&self) -> Option<&IpMatch> {
+ self.ip.as_ref()
+ }
+
+ pub fn proto(&self) -> Option<&Protocol> {
+ self.proto.as_ref()
+ }
+}
+
+/// Returns `(Macro name, Verdict, RestOfTheLine)`.
+fn parse_action(line: &str) -> Result<(Option<&str>, Verdict, &str), Error> {
+ let (verdict, line) =
+ match_name(line).ok_or_else(|| format_err!("expected a verdict or macro name"))?;
+
+ Ok(if let Some(line) = line.strip_prefix('(') {
+ // <macro>(<verdict>)
+
+ let macro_name = verdict;
+ let (verdict, line) = match_name(line).ok_or_else(|| format_err!("expected a verdict"))?;
+ let line = line
+ .strip_prefix(')')
+ .ok_or_else(|| format_err!("expected closing ')' after verdict"))?;
+
+ let verdict: Verdict = verdict.parse()?;
+
+ (Some(macro_name), verdict, line.trim_start())
+ } else {
+ (None, verdict.parse()?, line.trim_start())
+ })
+}
+
+impl FromStr for RuleMatch {
+ type Err = Error;
+
+ fn from_str(line: &str) -> Result<Self, Self::Err> {
+ let (dir, rest) = match_name(line).ok_or_else(|| format_err!("expected a direction"))?;
+
+ let direction: Direction = dir.parse()?;
+
+ let (fw_macro, verdict, rest) = parse_action(rest.trim_start())?;
+
+ let options: RuleOptions = rest.trim_start().parse()?;
+
+ Self::from_options(direction, verdict, fw_macro.map(str::to_string), options)
+ }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct IpMatch {
+ pub(crate) src: Option<IpAddrMatch>,
+ pub(crate) dst: Option<IpAddrMatch>,
+}
+
+impl IpMatch {
+ pub fn new(
+ src: impl Into<Option<IpAddrMatch>>,
+ dst: impl Into<Option<IpAddrMatch>>,
+ ) -> Result<Self, Error> {
+ let source = src.into();
+ let dest = dst.into();
+
+ if source.is_none() && dest.is_none() {
+ bail!("either src or dst must be set")
+ }
+
+ if let (Some(src), Some(dst)) = (&source, &dest) {
+ if src.family() != dst.family() {
+ bail!("src and dst family must be equal")
+ }
+ }
+
+ let ip_match = Self {
+ src: source,
+ dst: dest,
+ };
+
+ Ok(ip_match)
+ }
+
+ fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
+ let src = options
+ .source
+ .as_ref()
+ .map(|elem| elem.parse::<IpAddrMatch>())
+ .transpose()?;
+
+ let dst = options
+ .dest
+ .as_ref()
+ .map(|elem| elem.parse::<IpAddrMatch>())
+ .transpose()?;
+
+ Ok(IpMatch::new(src, dst).ok())
+ }
+
+ pub fn src(&self) -> Option<&IpAddrMatch> {
+ self.src.as_ref()
+ }
+
+ pub fn dst(&self) -> Option<&IpAddrMatch> {
+ self.dst.as_ref()
+ }
+}
+
+#[derive(Clone, Debug, Deserialize)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum IpAddrMatch {
+ Ip(IpList),
+ Set(IpsetName),
+ Alias(AliasName),
+}
+
+impl IpAddrMatch {
+ pub fn family(&self) -> Option<Family> {
+ if let IpAddrMatch::Ip(list) = self {
+ return Some(list.family());
+ }
+
+ None
+ }
+}
+
+impl FromStr for IpAddrMatch {
+ type Err = Error;
+
+ fn from_str(value: &str) -> Result<Self, Error> {
+ if value.is_empty() {
+ bail!("empty IP specification");
+ }
+
+ if let Ok(ip_list) = value.parse() {
+ return Ok(IpAddrMatch::Ip(ip_list));
+ }
+
+ if let Ok(ipset) = value.parse() {
+ return Ok(IpAddrMatch::Set(ipset));
+ }
+
+ if let Ok(name) = value.parse() {
+ return Ok(IpAddrMatch::Alias(name));
+ }
+
+ bail!("invalid IP specification: {value}")
+ }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum Protocol {
+ Dccp(Ports),
+ Sctp(Sctp),
+ Tcp(Tcp),
+ Udp(Udp),
+ UdpLite(Ports),
+ Icmp(Icmp),
+ Icmpv6(Icmpv6),
+ Named(String),
+ Numeric(u8),
+}
+
+impl Protocol {
+ pub(crate) fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
+ let proto = match options.proto.as_deref() {
+ Some(p) => p,
+ None => return Ok(None),
+ };
+
+ Ok(Some(match proto {
+ "dccp" | "33" => Protocol::Dccp(Ports::from_options(options)?),
+ "sctp" | "132" => Protocol::Sctp(Sctp::from_options(options)?),
+ "tcp" | "6" => Protocol::Tcp(Tcp::from_options(options)?),
+ "udp" | "17" => Protocol::Udp(Udp::from_options(options)?),
+ "udplite" | "136" => Protocol::UdpLite(Ports::from_options(options)?),
+ "icmp" | "1" => Protocol::Icmp(Icmp::from_options(options)?),
+ "ipv6-icmp" | "icmpv6" | "58" => Protocol::Icmpv6(Icmpv6::from_options(options)?),
+ other => match other.parse::<u8>() {
+ Ok(num) => Protocol::Numeric(num),
+ Err(_) => Protocol::Named(other.to_string()),
+ },
+ }))
+ }
+
+ pub fn family(&self) -> Option<Family> {
+ match self {
+ Self::Icmp(_) => Some(Family::V4),
+ Self::Icmpv6(_) => Some(Family::V6),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Udp {
+ ports: Ports,
+}
+
+impl Udp {
+ fn from_options(options: &RuleOptions) -> Result<Self, Error> {
+ Ok(Self {
+ ports: Ports::from_options(options)?,
+ })
+ }
+
+ pub fn new(ports: Ports) -> Self {
+ Self { ports }
+ }
+
+ pub fn ports(&self) -> &Ports {
+ &self.ports
+ }
+}
+
+impl From<Udp> for Protocol {
+ fn from(value: Udp) -> Self {
+ Protocol::Udp(value)
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Ports {
+ sport: Option<PortList>,
+ dport: Option<PortList>,
+}
+
+impl Ports {
+ pub fn new(sport: impl Into<Option<PortList>>, dport: impl Into<Option<PortList>>) -> Self {
+ Self {
+ sport: sport.into(),
+ dport: dport.into(),
+ }
+ }
+
+ fn from_options(options: &RuleOptions) -> Result<Self, Error> {
+ Ok(Self {
+ sport: options.sport.as_deref().map(|s| s.parse()).transpose()?,
+ dport: options.dport.as_deref().map(|s| s.parse()).transpose()?,
+ })
+ }
+
+ pub fn from_u16(sport: impl Into<Option<u16>>, dport: impl Into<Option<u16>>) -> Self {
+ Self::new(
+ sport.into().map(PortList::from),
+ dport.into().map(PortList::from),
+ )
+ }
+
+ pub fn sport(&self) -> Option<&PortList> {
+ self.sport.as_ref()
+ }
+
+ pub fn dport(&self) -> Option<&PortList> {
+ self.dport.as_ref()
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Tcp {
+ ports: Ports,
+}
+
+impl Tcp {
+ pub fn new(ports: Ports) -> Self {
+ Self { ports }
+ }
+
+ fn from_options(options: &RuleOptions) -> Result<Self, Error> {
+ Ok(Self {
+ ports: Ports::from_options(options)?,
+ })
+ }
+
+ pub fn ports(&self) -> &Ports {
+ &self.ports
+ }
+}
+
+impl From<Tcp> for Protocol {
+ fn from(value: Tcp) -> Self {
+ Protocol::Tcp(value)
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Sctp {
+ ports: Ports,
+}
+
+impl Sctp {
+ fn from_options(options: &RuleOptions) -> Result<Self, Error> {
+ Ok(Self {
+ ports: Ports::from_options(options)?,
+ })
+ }
+
+ pub fn ports(&self) -> &Ports {
+ &self.ports
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Icmp {
+ ty: Option<IcmpType>,
+ code: Option<IcmpCode>,
+}
+
+impl Icmp {
+ pub fn new_ty(ty: IcmpType) -> Self {
+ Self {
+ ty: Some(ty),
+ ..Default::default()
+ }
+ }
+
+ pub fn new_code(code: IcmpCode) -> Self {
+ Self {
+ code: Some(code),
+ ..Default::default()
+ }
+ }
+
+ fn from_options(options: &RuleOptions) -> Result<Self, Error> {
+ if let Some(ty) = &options.icmp_type {
+ return ty.parse();
+ }
+
+ Ok(Self::default())
+ }
+
+ pub fn ty(&self) -> Option<&IcmpType> {
+ self.ty.as_ref()
+ }
+
+ pub fn code(&self) -> Option<&IcmpCode> {
+ self.code.as_ref()
+ }
+}
+
+impl FromStr for Icmp {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut this = Self::default();
+
+ if let Ok(ty) = s.parse() {
+ this.ty = Some(ty);
+ return Ok(this);
+ }
+
+ if let Ok(code) = s.parse() {
+ this.code = Some(code);
+ return Ok(this);
+ }
+
+ bail!("supplied string is neither a valid icmp type nor code");
+ }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum IcmpType {
+ Numeric(u8),
+ Named(&'static str),
+}
+
+// MUST BE SORTED!
+const ICMP_TYPES: &[(&str, u8)] = &[
+ ("address-mask-reply", 18),
+ ("address-mask-request", 17),
+ ("destination-unreachable", 3),
+ ("echo-reply", 0),
+ ("echo-request", 8),
+ ("info-reply", 16),
+ ("info-request", 15),
+ ("parameter-problem", 12),
+ ("redirect", 5),
+ ("router-advertisement", 9),
+ ("router-solicitation", 10),
+ ("source-quench", 4),
+ ("time-exceeded", 11),
+ ("timestamp-reply", 14),
+ ("timestamp-request", 13),
+];
+
+impl std::str::FromStr for IcmpType {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Error> {
+ if let Ok(ty) = s.trim().parse::<u8>() {
+ return Ok(Self::Numeric(ty));
+ }
+
+ if let Ok(index) = ICMP_TYPES.binary_search_by(|v| v.0.cmp(s)) {
+ return Ok(Self::Named(ICMP_TYPES[index].0));
+ }
+
+ bail!("{s:?} is not a valid icmp type");
+ }
+}
+
+impl fmt::Display for IcmpType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ IcmpType::Numeric(ty) => write!(f, "{ty}"),
+ IcmpType::Named(ty) => write!(f, "{ty}"),
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum IcmpCode {
+ Numeric(u8),
+ Named(&'static str),
+}
+
+// MUST BE SORTED!
+const ICMP_CODES: &[(&str, u8)] = &[
+ ("admin-prohibited", 13),
+ ("host-prohibited", 10),
+ ("host-unreachable", 1),
+ ("net-prohibited", 9),
+ ("net-unreachable", 0),
+ ("port-unreachable", 3),
+ ("prot-unreachable", 2),
+];
+
+impl std::str::FromStr for IcmpCode {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Error> {
+ if let Ok(code) = s.trim().parse::<u8>() {
+ return Ok(Self::Numeric(code));
+ }
+
+ if let Ok(index) = ICMP_CODES.binary_search_by(|v| v.0.cmp(s)) {
+ return Ok(Self::Named(ICMP_CODES[index].0));
+ }
+
+ bail!("{s:?} is not a valid icmp code");
+ }
+}
+
+impl fmt::Display for IcmpCode {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ IcmpCode::Numeric(code) => write!(f, "{code}"),
+ IcmpCode::Named(code) => write!(f, "{code}"),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Icmpv6 {
+ pub ty: Option<Icmpv6Type>,
+ pub code: Option<Icmpv6Code>,
+}
+
+impl Icmpv6 {
+ pub fn new_ty(ty: Icmpv6Type) -> Self {
+ Self {
+ ty: Some(ty),
+ ..Default::default()
+ }
+ }
+
+ pub fn new_code(code: Icmpv6Code) -> Self {
+ Self {
+ code: Some(code),
+ ..Default::default()
+ }
+ }
+
+ fn from_options(options: &RuleOptions) -> Result<Self, Error> {
+ if let Some(ty) = &options.icmp_type {
+ return ty.parse();
+ }
+
+ Ok(Self::default())
+ }
+
+ pub fn ty(&self) -> Option<&Icmpv6Type> {
+ self.ty.as_ref()
+ }
+
+ pub fn code(&self) -> Option<&Icmpv6Code> {
+ self.code.as_ref()
+ }
+}
+
+impl FromStr for Icmpv6 {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut this = Self::default();
+
+ if let Ok(ty) = s.parse() {
+ this.ty = Some(ty);
+ return Ok(this);
+ }
+
+ if let Ok(code) = s.parse() {
+ this.code = Some(code);
+ return Ok(this);
+ }
+
+ bail!("supplied string is neither a valid icmpv6 type nor code");
+ }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum Icmpv6Type {
+ Numeric(u8),
+ Named(&'static str),
+}
+
+// MUST BE SORTED!
+const ICMPV6_TYPES: &[(&str, u8)] = &[
+ ("destination-unreachable", 1),
+ ("echo-reply", 129),
+ ("echo-request", 128),
+ ("ind-neighbor-advert", 142),
+ ("ind-neighbor-solicit", 141),
+ ("mld-listener-done", 132),
+ ("mld-listener-query", 130),
+ ("mld-listener-reduction", 132),
+ ("mld-listener-report", 131),
+ ("mld2-listener-report", 143),
+ ("nd-neighbor-advert", 136),
+ ("nd-neighbor-solicit", 135),
+ ("nd-redirect", 137),
+ ("nd-router-advert", 134),
+ ("nd-router-solicit", 133),
+ ("packet-too-big", 2),
+ ("parameter-problem", 4),
+ ("router-renumbering", 138),
+ ("time-exceeded", 3),
+];
+
+impl std::str::FromStr for Icmpv6Type {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Error> {
+ if let Ok(ty) = s.trim().parse::<u8>() {
+ return Ok(Self::Numeric(ty));
+ }
+
+ if let Ok(index) = ICMPV6_TYPES.binary_search_by(|v| v.0.cmp(s)) {
+ return Ok(Self::Named(ICMPV6_TYPES[index].0));
+ }
+
+ bail!("{s:?} is not a valid icmpv6 type");
+ }
+}
+
+impl fmt::Display for Icmpv6Type {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Icmpv6Type::Numeric(ty) => write!(f, "{ty}"),
+ Icmpv6Type::Named(ty) => write!(f, "{ty}"),
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum Icmpv6Code {
+ Numeric(u8),
+ Named(&'static str),
+}
+
+// MUST BE SORTED!
+const ICMPV6_CODES: &[(&str, u8)] = &[
+ ("addr-unreachable", 3),
+ ("admin-prohibited", 1),
+ ("no-route", 0),
+ ("policy-fail", 5),
+ ("port-unreachable", 4),
+ ("reject-route", 6),
+];
+
+impl std::str::FromStr for Icmpv6Code {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Error> {
+ if let Ok(code) = s.trim().parse::<u8>() {
+ return Ok(Self::Numeric(code));
+ }
+
+ if let Ok(index) = ICMPV6_CODES.binary_search_by(|v| v.0.cmp(s)) {
+ return Ok(Self::Named(ICMPV6_CODES[index].0));
+ }
+
+ bail!("{s:?} is not a valid icmpv6 code");
+ }
+}
+
+impl fmt::Display for Icmpv6Code {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Icmpv6Code::Numeric(code) => write!(f, "{code}"),
+ Icmpv6Code::Named(code) => write!(f, "{code}"),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::firewall::types::Cidr;
+
+ use super::*;
+
+ #[test]
+ fn test_parse_action() {
+ assert_eq!(parse_action("REJECT").unwrap(), (None, Verdict::Reject, ""));
+
+ assert_eq!(
+ parse_action("SSH(ACCEPT) qweasd").unwrap(),
+ (Some("SSH"), Verdict::Accept, "qweasd")
+ );
+ }
+
+ #[test]
+ fn test_parse_ip_addr_match() {
+ for input in [
+ "10.0.0.0/8",
+ "10.0.0.0/8,192.168.0.0-192.168.255.255,172.16.0.1",
+ "dc/test",
+ "+guest/proxmox",
+ ] {
+ input.parse::<IpAddrMatch>().expect("valid ip match");
+ }
+
+ for input in [
+ "10.0.0.0/",
+ "10.0.0.0/8,192.168.256.0-192.168.255.255,172.16.0.1",
+ "dcc/test",
+ "+guest/",
+ "",
+ ] {
+ input.parse::<IpAddrMatch>().expect_err("invalid ip match");
+ }
+ }
+
+ #[test]
+ fn test_parse_options() {
+ let mut options: RuleOptions =
+ "-p udp --sport 123 --dport 234 -source 127.0.0.1 --dest 127.0.0.1 -i ens1 --log crit"
+ .parse()
+ .expect("valid option string");
+
+ assert_eq!(
+ options,
+ RuleOptions {
+ proto: Some("udp".to_string()),
+ sport: Some("123".to_string()),
+ dport: Some("234".to_string()),
+ source: Some("127.0.0.1".to_string()),
+ dest: Some("127.0.0.1".to_string()),
+ iface: Some("ens1".to_string()),
+ log: Some(LogLevel::Critical),
+ icmp_type: None,
+ }
+ );
+
+ options = "".parse().expect("valid option string");
+
+ assert_eq!(options, RuleOptions::default(),);
+ }
+
+ #[test]
+ fn test_construct_ip_match() {
+ IpMatch::new(
+ IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
+ IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
+ )
+ .expect("valid ip match");
+
+ IpMatch::new(
+ IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
+ IpAddrMatch::Ip(IpList::from(Cidr::new_v6([0x0000; 8], 8).unwrap())),
+ )
+ .expect_err("cannot mix ip families");
+
+ IpMatch::new(None, None).expect_err("at least one ip must be set");
+ }
+
+ #[test]
+ fn test_from_options() {
+ let mut options = RuleOptions {
+ proto: Some("tcp".to_string()),
+ sport: Some("123".to_string()),
+ dport: Some("234".to_string()),
+ source: Some("192.168.0.1".to_string()),
+ dest: Some("10.0.0.1".to_string()),
+ iface: Some("eth123".to_string()),
+ log: Some(LogLevel::Error),
+ ..Default::default()
+ };
+
+ assert_eq!(
+ Protocol::from_options(&options).unwrap().unwrap(),
+ Protocol::Tcp(Tcp::new(Ports::from_u16(123, 234))),
+ );
+
+ assert_eq!(
+ IpMatch::from_options(&options).unwrap().unwrap(),
+ IpMatch::new(
+ IpAddrMatch::Ip(IpList::from(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),),
+ IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 1], 32).unwrap()),)
+ )
+ .unwrap(),
+ );
+
+ options = RuleOptions::default();
+
+ assert_eq!(Protocol::from_options(&options).unwrap(), None,);
+
+ assert_eq!(IpMatch::from_options(&options).unwrap(), None,);
+
+ options = RuleOptions {
+ proto: Some("tcp".to_string()),
+ sport: Some("qwe".to_string()),
+ source: Some("qwe".to_string()),
+ ..Default::default()
+ };
+
+ Protocol::from_options(&options).expect_err("invalid source port");
+
+ IpMatch::from_options(&options).expect_err("invalid source address");
+
+ options = RuleOptions {
+ icmp_type: Some("port-unreachable".to_string()),
+ dport: Some("123".to_string()),
+ ..Default::default()
+ };
+
+ RuleMatch::from_options(Direction::In, Verdict::Drop, None, options)
+ .expect_err("cannot mix dport and icmp-type");
+ }
+
+ #[test]
+ fn test_parse_icmp() {
+ let mut icmp: Icmp = "info-request".parse().expect("valid icmp type");
+
+ assert_eq!(
+ icmp,
+ Icmp {
+ ty: Some(IcmpType::Named("info-request")),
+ code: None
+ }
+ );
+
+ icmp = "12".parse().expect("valid icmp type");
+
+ assert_eq!(
+ icmp,
+ Icmp {
+ ty: Some(IcmpType::Numeric(12)),
+ code: None
+ }
+ );
+
+ icmp = "port-unreachable".parse().expect("valid icmp code");
+
+ assert_eq!(
+ icmp,
+ Icmp {
+ ty: None,
+ code: Some(IcmpCode::Named("port-unreachable"))
+ }
+ );
+ }
+
+ #[test]
+ fn test_parse_icmp6() {
+ let mut icmp: Icmpv6 = "echo-reply".parse().expect("valid icmpv6 type");
+
+ assert_eq!(
+ icmp,
+ Icmpv6 {
+ ty: Some(Icmpv6Type::Named("echo-reply")),
+ code: None
+ }
+ );
+
+ icmp = "12".parse().expect("valid icmpv6 type");
+
+ assert_eq!(
+ icmp,
+ Icmpv6 {
+ ty: Some(Icmpv6Type::Numeric(12)),
+ code: None
+ }
+ );
+
+ icmp = "admin-prohibited".parse().expect("valid icmpv6 code");
+
+ assert_eq!(
+ icmp,
+ Icmpv6 {
+ ty: None,
+ code: Some(Icmpv6Code::Named("admin-prohibited"))
+ }
+ );
+ }
+}
--
2.39.2
next prev parent reply other threads:[~2024-04-02 17:17 UTC|newest]
Thread overview: 67+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-04-02 17:15 [pve-devel] [RFC container/firewall/manager/proxmox-firewall/qemu-server 00/37] proxmox firewall nftables implementation Stefan Hanreich
2024-04-02 17:15 ` [pve-devel] [PATCH proxmox-firewall 01/37] config: add proxmox-ve-config crate Stefan Hanreich
2024-04-02 17:15 ` [pve-devel] [PATCH proxmox-firewall 02/37] config: firewall: add types for ip addresses Stefan Hanreich
2024-04-03 10:46 ` Max Carrara
2024-04-09 8:26 ` Stefan Hanreich
2024-04-02 17:15 ` [pve-devel] [PATCH proxmox-firewall 03/37] config: firewall: add types for ports Stefan Hanreich
2024-04-02 17:15 ` [pve-devel] [PATCH proxmox-firewall 04/37] config: firewall: add types for log level and rate limit Stefan Hanreich
2024-04-02 17:15 ` [pve-devel] [PATCH proxmox-firewall 05/37] config: firewall: add types for aliases Stefan Hanreich
2024-04-02 17:15 ` [pve-devel] [PATCH proxmox-firewall 06/37] config: host: add helpers for host network configuration Stefan Hanreich
2024-04-03 10:46 ` Max Carrara
2024-04-09 8:32 ` Stefan Hanreich
2024-04-09 14:20 ` Lukas Wagner
2024-04-02 17:15 ` [pve-devel] [PATCH proxmox-firewall 07/37] config: guest: add helpers for parsing guest network config Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 08/37] config: firewall: add types for ipsets Stefan Hanreich
2024-04-02 17:16 ` Stefan Hanreich [this message]
2024-04-03 10:46 ` [pve-devel] [PATCH proxmox-firewall 09/37] config: firewall: add types for rules Max Carrara
2024-04-09 8:36 ` Stefan Hanreich
2024-04-09 14:55 ` Lukas Wagner
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 10/37] config: firewall: add types for security groups Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 11/37] config: firewall: add generic parser for firewall configs Stefan Hanreich
2024-04-03 10:47 ` Max Carrara
2024-04-09 8:38 ` Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 12/37] config: firewall: add cluster-specific config + option types Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 13/37] config: firewall: add host specific " Stefan Hanreich
2024-04-03 10:47 ` Max Carrara
2024-04-09 8:55 ` Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 14/37] config: firewall: add guest-specific " Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 15/37] config: firewall: add firewall macros Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 16/37] config: firewall: add conntrack helper types Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 17/37] nftables: add crate for libnftables bindings Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 18/37] nftables: add helpers Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 19/37] nftables: expression: add types Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 20/37] nftables: expression: implement conversion traits for firewall config Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 21/37] nftables: statement: add types Stefan Hanreich
2024-04-03 10:47 ` Max Carrara
2024-04-09 8:58 ` Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 22/37] nftables: statement: add conversion traits for config types Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 23/37] nftables: commands: add types Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 24/37] nftables: types: add conversion traits Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 25/37] nftables: add libnftables bindings Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 26/37] firewall: add firewall crate Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 27/37] firewall: add base ruleset Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 28/37] firewall: add config loader Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 29/37] firewall: add rule generation logic Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 30/37] firewall: add object " Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 31/37] firewall: add ruleset " Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 32/37] firewall: add proxmox-firewall binary Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH proxmox-firewall 33/37] firewall: add files for debian packaging Stefan Hanreich
2024-04-03 13:14 ` Fabian Grünbichler
2024-04-09 8:56 ` Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH qemu-server 34/37] firewall: add handling for new nft firewall Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH pve-container 35/37] " Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH pve-firewall 36/37] add configuration option for new nftables firewall Stefan Hanreich
2024-04-02 17:16 ` [pve-devel] [PATCH pve-manager 37/37] firewall: expose " Stefan Hanreich
2024-04-02 20:47 ` [pve-devel] [RFC container/firewall/manager/proxmox-firewall/qemu-server 00/37] proxmox firewall nftables implementation Laurent GUERBY
2024-04-03 7:33 ` Stefan Hanreich
[not found] ` <mailman.54.1712122640.450.pve-devel@lists.proxmox.com>
2024-04-03 7:52 ` Stefan Hanreich
2024-04-03 12:26 ` Stefan Hanreich
[not found] ` <mailman.56.1712124362.450.pve-devel@lists.proxmox.com>
2024-04-03 8:15 ` Stefan Hanreich
[not found] ` <mailman.77.1712145853.450.pve-devel@lists.proxmox.com>
2024-04-03 12:25 ` Stefan Hanreich
[not found] ` <mailman.78.1712149473.450.pve-devel@lists.proxmox.com>
2024-04-03 13:08 ` Stefan Hanreich
2024-04-03 10:46 ` Max Carrara
2024-04-09 9:21 ` Stefan Hanreich
2024-04-10 10:25 ` Lukas Wagner
2024-04-11 5:21 ` Stefan Hanreich
2024-04-11 7:34 ` Thomas Lamprecht
2024-04-11 7:55 ` Stefan Hanreich
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=20240402171629.536804-10-s.hanreich@proxmox.com \
--to=s.hanreich@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
--cc=w.bumiller@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