Skip to main content

firewall_objects/service/
icmp.rs

1//! ICMP version/type helpers.
2
3use std::fmt;
4
5/// Identify whether an ICMP definition targets IPv4 or IPv6.
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub enum IcmpVersion {
9    V4,
10    V6,
11}
12
13/// Parsed ICMP tuple of version, type, and optional code.
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct IcmpSpec {
17    pub version: IcmpVersion,
18    pub ty: u8,
19    pub code: Option<u8>,
20}
21
22impl IcmpSpec {
23    pub const fn new(version: IcmpVersion, ty: u8, code: Option<u8>) -> Self {
24        Self { version, ty, code }
25    }
26}
27
28impl fmt::Display for IcmpSpec {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        let prefix = match self.version {
31            IcmpVersion::V4 => "icmp",
32            IcmpVersion::V6 => "icmpv6",
33        };
34
35        match self.code {
36            Some(code) => write!(f, "{prefix}/{ty}:{code}", ty = self.ty),
37            None => write!(f, "{prefix}/{ty}", ty = self.ty),
38        }
39    }
40}
41
42/// Parse an ICMP specification string using the provided IP version.
43///
44/// Accepts numeric type/code (`"8:0"`) or a limited set of aliases such as
45/// `"echo-request"`, `"echo-reply"`, `"time-exceeded"`, and `"packet-too-big"`.
46pub fn parse_descriptor(value: &str, version: IcmpVersion) -> Result<IcmpSpec, String> {
47    let trimmed = value.trim();
48
49    if trimmed.is_empty() {
50        return Err("ICMP definition cannot be empty".into());
51    }
52
53    let (type_token, code_token) = trimmed
54        .split_once(':')
55        .map(|(ty, code)| (ty.trim(), Some(code.trim())))
56        .unwrap_or_else(|| (trimmed, None));
57
58    if type_token.is_empty() {
59        return Err("ICMP type cannot be empty".into());
60    }
61
62    let ty = parse_type_token(type_token, version)?;
63    let code = match code_token {
64        Some(raw) if !raw.is_empty() => Some(
65            raw.parse::<u8>()
66                .map_err(|_| "invalid ICMP code; expected 0-255".to_string())?,
67        ),
68        Some(_) => {
69            return Err("ICMP code cannot be empty".into());
70        }
71        None => None,
72    };
73
74    Ok(IcmpSpec::new(version, ty, code))
75}
76
77fn parse_type_token(token: &str, version: IcmpVersion) -> Result<u8, String> {
78    if let Ok(num) = token.parse::<u8>() {
79        return Ok(num);
80    }
81
82    resolve_alias(token, version).ok_or_else(|| {
83        format!(
84            "unknown ICMP {} type '{token}'",
85            match version {
86                IcmpVersion::V4 => "v4",
87                IcmpVersion::V6 => "v6",
88            }
89        )
90    })
91}
92
93fn resolve_alias(token: &str, version: IcmpVersion) -> Option<u8> {
94    let key = token.trim().to_ascii_lowercase();
95    let ty = match version {
96        IcmpVersion::V4 => match key.as_str() {
97            "echo" | "echo-request" => 8,
98            "echo-reply" => 0,
99            "destination-unreachable" => 3,
100            "source-quench" => 4,
101            "redirect" => 5,
102            "router-advertisement" => 9,
103            "router-solicitation" => 10,
104            "time-exceeded" => 11,
105            "parameter-problem" => 12,
106            "timestamp" => 13,
107            "timestamp-reply" => 14,
108            "address-mask-request" => 17,
109            "address-mask-reply" => 18,
110            _ => return None,
111        },
112        IcmpVersion::V6 => match key.as_str() {
113            "echo" | "echo-request" => 128,
114            "echo-reply" => 129,
115            "destination-unreachable" => 1,
116            "packet-too-big" => 2,
117            "time-exceeded" => 3,
118            "parameter-problem" => 4,
119            "router-solicitation" => 133,
120            "router-advertisement" => 134,
121            "neighbor-solicitation" => 135,
122            "neighbor-advertisement" => 136,
123            "redirect" | "redirect-message" => 137,
124            _ => return None,
125        },
126    };
127
128    Some(ty)
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn parses_numeric_type_and_code() {
137        let spec = parse_descriptor("8:0", IcmpVersion::V4).unwrap();
138        assert_eq!(spec.ty, 8);
139        assert_eq!(spec.code, Some(0));
140    }
141
142    #[test]
143    fn parses_aliases() {
144        let echo = parse_descriptor("echo-request", IcmpVersion::V4).unwrap();
145        assert_eq!(echo.ty, 8);
146
147        let v6 = parse_descriptor("packet-too-big", IcmpVersion::V6).unwrap();
148        assert_eq!(v6.ty, 2);
149    }
150
151    #[test]
152    fn rejects_unknown_alias() {
153        let err = parse_descriptor("bogus", IcmpVersion::V4).unwrap_err();
154        assert!(err.contains("unknown"));
155    }
156}