[pve-devel] [PATCH proxmox-firewall 09/37] config: firewall: add types for rules
Max Carrara
m.carrara at proxmox.com
Wed Apr 3 12:46:56 CEST 2024
On Tue Apr 2, 2024 at 7:16 PM CEST, Stefan Hanreich wrote:
> 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 at proxmox.com>
> Signed-off-by: Stefan Hanreich <s.hanreich at 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> {
Hmm, since this is only used below, IMO it's fine that this returns a
tuple like that on `Ok` - but should functions like that be used in
multiple places, it might be beneficial to use a type alias or even a
tuple struct for readability's sake.
> + 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!
Should maaaybe note that it must be sorted for binary search, not just
for any reason. :P
> +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!
Same here.
> +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!
And here too.
> +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!
As well as here.
> +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"))
> + }
> + );
> + }
> +}
More information about the pve-devel
mailing list