1use smallvec::SmallVec;
4
5use super::{FieldValue, ParseContext, ParseResult, Protocol};
6use crate::schema::{DataKind, FieldDescriptor};
7
8pub const IP_PROTO_ICMP: u8 = 1;
10
11pub mod icmp_type {
13 pub const ECHO_REPLY: u8 = 0;
14 pub const DESTINATION_UNREACHABLE: u8 = 3;
15 pub const SOURCE_QUENCH: u8 = 4;
16 pub const REDIRECT: u8 = 5;
17 pub const ECHO_REQUEST: u8 = 8;
18 pub const TIME_EXCEEDED: u8 = 11;
19 pub const PARAMETER_PROBLEM: u8 = 12;
20 pub const TIMESTAMP_REQUEST: u8 = 13;
21 pub const TIMESTAMP_REPLY: u8 = 14;
22}
23
24#[derive(Debug, Clone, Copy)]
26pub struct IcmpProtocol;
27
28impl Protocol for IcmpProtocol {
29 fn name(&self) -> &'static str {
30 "icmp"
31 }
32
33 fn display_name(&self) -> &'static str {
34 "ICMP"
35 }
36
37 fn can_parse(&self, context: &ParseContext) -> Option<u32> {
38 match context.hint("ip_protocol") {
39 Some(proto) if proto == IP_PROTO_ICMP as u64 => Some(100),
40 _ => None,
41 }
42 }
43
44 fn parse<'a>(&self, data: &'a [u8], _context: &ParseContext) -> ParseResult<'a> {
45 if data.len() < 8 {
47 return ParseResult::error(
48 format!("ICMP header too short: {} bytes", data.len()),
49 data,
50 );
51 }
52
53 let mut fields = SmallVec::new();
54
55 let icmp_type = data[0];
56 let icmp_code = data[1];
57 let checksum = u16::from_be_bytes([data[2], data[3]]);
58
59 fields.push(("type", FieldValue::UInt8(icmp_type)));
60 fields.push(("code", FieldValue::UInt8(icmp_code)));
61 fields.push(("checksum", FieldValue::UInt16(checksum)));
62
63 match icmp_type {
65 icmp_type::ECHO_REQUEST | icmp_type::ECHO_REPLY => {
66 let identifier = u16::from_be_bytes([data[4], data[5]]);
67 let sequence = u16::from_be_bytes([data[6], data[7]]);
68 fields.push(("identifier", FieldValue::UInt16(identifier)));
69 fields.push(("sequence", FieldValue::UInt16(sequence)));
70 }
71 icmp_type::DESTINATION_UNREACHABLE => {
72 if icmp_code == 4 && data.len() >= 8 {
74 let mtu = u16::from_be_bytes([data[6], data[7]]);
75 fields.push(("next_hop_mtu", FieldValue::UInt16(mtu)));
76 }
77 }
78 icmp_type::REDIRECT => {
79 if data.len() >= 8 {
80 fields.push(("gateway", FieldValue::ipv4(&data[4..8])));
81 }
82 }
83 _ => {
84 let rest = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
86 fields.push(("rest_of_header", FieldValue::UInt32(rest)));
87 }
88 }
89
90 let type_name = match icmp_type {
92 icmp_type::ECHO_REPLY => "Echo Reply",
93 icmp_type::DESTINATION_UNREACHABLE => "Destination Unreachable",
94 icmp_type::SOURCE_QUENCH => "Source Quench",
95 icmp_type::REDIRECT => "Redirect",
96 icmp_type::ECHO_REQUEST => "Echo Request",
97 icmp_type::TIME_EXCEEDED => "Time Exceeded",
98 icmp_type::PARAMETER_PROBLEM => "Parameter Problem",
99 icmp_type::TIMESTAMP_REQUEST => "Timestamp Request",
100 icmp_type::TIMESTAMP_REPLY => "Timestamp Reply",
101 _ => "Unknown",
102 };
103 fields.push(("type_name", FieldValue::Str(type_name)));
104
105 ParseResult::success(fields, &data[8..], SmallVec::new())
107 }
108
109 fn schema_fields(&self) -> Vec<FieldDescriptor> {
110 vec![
111 FieldDescriptor::new("icmp.type", DataKind::UInt8).set_nullable(true),
112 FieldDescriptor::new("icmp.code", DataKind::UInt8).set_nullable(true),
113 FieldDescriptor::new("icmp.checksum", DataKind::UInt16).set_nullable(true),
114 FieldDescriptor::new("icmp.type_name", DataKind::String).set_nullable(true),
115 FieldDescriptor::new("icmp.identifier", DataKind::UInt16).set_nullable(true),
116 FieldDescriptor::new("icmp.sequence", DataKind::UInt16).set_nullable(true),
117 FieldDescriptor::new("icmp.next_hop_mtu", DataKind::UInt16).set_nullable(true),
118 FieldDescriptor::new("icmp.gateway", DataKind::String).set_nullable(true),
119 ]
120 }
121
122 fn dependencies(&self) -> &'static [&'static str] {
123 &["ipv4"]
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_parse_icmp_echo_request() {
133 let header = [
135 0x08, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, ];
141
142 let parser = IcmpProtocol;
143 let mut context = ParseContext::new(1);
144 context.insert_hint("ip_protocol", 1);
145
146 let result = parser.parse(&header, &context);
147
148 assert!(result.is_ok());
149 assert_eq!(result.get("type"), Some(&FieldValue::UInt8(8)));
150 assert_eq!(result.get("identifier"), Some(&FieldValue::UInt16(1)));
151 assert_eq!(result.get("sequence"), Some(&FieldValue::UInt16(2)));
152 assert_eq!(
153 result.get("type_name"),
154 Some(&FieldValue::Str("Echo Request"))
155 );
156 }
157
158 #[test]
159 fn test_parse_icmp_echo_reply() {
160 let header = [
161 0x00, 0x00, 0xab, 0xcd, 0x12, 0x34, 0x00, 0x0a, ];
167
168 let parser = IcmpProtocol;
169 let mut context = ParseContext::new(1);
170 context.insert_hint("ip_protocol", 1);
171
172 let result = parser.parse(&header, &context);
173
174 assert!(result.is_ok());
175 assert_eq!(
176 result.get("type"),
177 Some(&FieldValue::UInt8(icmp_type::ECHO_REPLY))
178 );
179 assert_eq!(result.get("identifier"), Some(&FieldValue::UInt16(0x1234)));
180 assert_eq!(result.get("sequence"), Some(&FieldValue::UInt16(10)));
181 assert_eq!(
182 result.get("type_name"),
183 Some(&FieldValue::Str("Echo Reply"))
184 );
185 }
186
187 #[test]
188 fn test_parse_icmp_destination_unreachable() {
189 let header = [
190 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
195
196 let parser = IcmpProtocol;
197 let mut context = ParseContext::new(1);
198 context.insert_hint("ip_protocol", 1);
199
200 let result = parser.parse(&header, &context);
201
202 assert!(result.is_ok());
203 assert_eq!(
204 result.get("type"),
205 Some(&FieldValue::UInt8(icmp_type::DESTINATION_UNREACHABLE))
206 );
207 assert_eq!(result.get("code"), Some(&FieldValue::UInt8(1)));
208 assert_eq!(
209 result.get("type_name"),
210 Some(&FieldValue::Str("Destination Unreachable"))
211 );
212 }
213
214 #[test]
215 fn test_parse_icmp_time_exceeded() {
216 let header = [
217 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
222
223 let parser = IcmpProtocol;
224 let mut context = ParseContext::new(1);
225 context.insert_hint("ip_protocol", 1);
226
227 let result = parser.parse(&header, &context);
228
229 assert!(result.is_ok());
230 assert_eq!(
231 result.get("type"),
232 Some(&FieldValue::UInt8(icmp_type::TIME_EXCEEDED))
233 );
234 assert_eq!(
235 result.get("type_name"),
236 Some(&FieldValue::Str("Time Exceeded"))
237 );
238 }
239
240 #[test]
241 fn test_can_parse_icmp() {
242 let parser = IcmpProtocol;
243
244 let ctx1 = ParseContext::new(1);
246 assert!(parser.can_parse(&ctx1).is_none());
247
248 let mut ctx2 = ParseContext::new(1);
250 ctx2.insert_hint("ip_protocol", 6);
251 assert!(parser.can_parse(&ctx2).is_none());
252
253 let mut ctx3 = ParseContext::new(1);
255 ctx3.insert_hint("ip_protocol", 1);
256 assert!(parser.can_parse(&ctx3).is_some());
257 }
258
259 #[test]
260 fn test_parse_icmp_too_short() {
261 let short_header = [0x08, 0x00, 0x00]; let parser = IcmpProtocol;
264 let mut context = ParseContext::new(1);
265 context.insert_hint("ip_protocol", 1);
266
267 let result = parser.parse(&short_header, &context);
268
269 assert!(!result.is_ok());
270 assert!(result.error.is_some());
271 }
272
273 #[test]
274 fn test_icmp_with_payload() {
275 let packet = [
276 0x08, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0xde, 0xad, 0xbe, 0xef,
283 ];
284
285 let parser = IcmpProtocol;
286 let mut context = ParseContext::new(1);
287 context.insert_hint("ip_protocol", 1);
288
289 let result = parser.parse(&packet, &context);
290
291 assert!(result.is_ok());
292 assert_eq!(result.remaining.len(), 4); }
294}