pcapsql_core/protocol/
icmp.rs

1//! ICMP protocol parser.
2
3use smallvec::SmallVec;
4
5use super::{FieldValue, ParseContext, ParseResult, Protocol};
6use crate::schema::{DataKind, FieldDescriptor};
7
8/// IP protocol number for ICMP.
9pub const IP_PROTO_ICMP: u8 = 1;
10
11/// ICMP type constants.
12pub 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/// ICMP protocol parser.
25#[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        // ICMP header is at least 8 bytes
46        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        // Type-specific fields (bytes 4-7)
64        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                // Next-hop MTU for "fragmentation needed" (code 4)
73                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                // Store the 4 bytes as a generic field
85                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        // Add type name for convenience
91        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        // ICMP doesn't have child protocols typically
106        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        // ICMP Echo Request
134        let header = [
135            0x08, // Type: Echo Request
136            0x00, // Code: 0
137            0x00, 0x00, // Checksum
138            0x00, 0x01, // Identifier: 1
139            0x00, 0x02, // Sequence: 2
140        ];
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, // Type: Echo Reply
162            0x00, // Code: 0
163            0xab, 0xcd, // Checksum
164            0x12, 0x34, // Identifier: 0x1234
165            0x00, 0x0a, // Sequence: 10
166        ];
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, // Type: Destination Unreachable
191            0x01, // Code: Host Unreachable
192            0x00, 0x00, // Checksum
193            0x00, 0x00, 0x00, 0x00, // Unused
194        ];
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, // Type: Time Exceeded
218            0x00, // Code: TTL exceeded
219            0x00, 0x00, // Checksum
220            0x00, 0x00, 0x00, 0x00, // Unused
221        ];
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        // Without hint
245        let ctx1 = ParseContext::new(1);
246        assert!(parser.can_parse(&ctx1).is_none());
247
248        // With TCP protocol
249        let mut ctx2 = ParseContext::new(1);
250        ctx2.insert_hint("ip_protocol", 6);
251        assert!(parser.can_parse(&ctx2).is_none());
252
253        // With ICMP protocol
254        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]; // Only 3 bytes
262
263        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, // Type: Echo Request
277            0x00, // Code: 0
278            0x00, 0x00, // Checksum
279            0x00, 0x01, // Identifier
280            0x00, 0x01, // Sequence
281            // Payload data
282            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); // Payload bytes
293    }
294}