firewall_objects/service/
icmp.rs1use std::fmt;
4
5#[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#[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
42pub 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}