pcapsql_core/protocol/
ipsec.rs

1//! IPsec (ESP and AH) protocol parser.
2//!
3//! IPsec provides security services at the IP layer including:
4//! - ESP (Encapsulating Security Payload): Encryption and authentication
5//! - AH (Authentication Header): Authentication only
6//!
7//! RFC 4303: IP Encapsulating Security Payload (ESP)
8//! RFC 4302: IP Authentication Header (AH)
9//!
10//! # Encrypted Payload Limitations
11//!
12//! **Important:** ESP payloads are encrypted and cannot be parsed without the
13//! corresponding Security Association (SA) and decryption keys. This parser
14//! extracts only the unencrypted header fields:
15//!
16//! ## ESP Limitations
17//!
18//! The ESP header format (RFC 4303) places the Next Header field in the
19//! encrypted trailer, making it inaccessible without decryption:
20//!
21//! ```text
22//! +---------------+---------------+---------------+---------------+
23//! |                Security Parameters Index (SPI)                | <- Cleartext
24//! +---------------+---------------+---------------+---------------+
25//! |                      Sequence Number                          | <- Cleartext
26//! +---------------+---------------+---------------+---------------+
27//! |                    Payload Data (variable)                    | <- ENCRYPTED
28//! +               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
29//! |               |     Padding (0-255 bytes)                     | <- ENCRYPTED
30//! +-+-+-+-+-+-+-+-+               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
31//! |                               |  Pad Length   |  Next Header  | <- ENCRYPTED
32//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
33//! |         Integrity Check Value (ICV) (variable)                | <- Authentication
34//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
35//! ```
36//!
37//! As a result, for ESP packets this parser:
38//! - Extracts SPI and Sequence Number (cleartext header)
39//! - Cannot determine the encapsulated protocol (Next Header is encrypted)
40//! - Cannot parse inner payloads (data is encrypted)
41//! - Cannot verify the ICV without the SA keys
42//!
43//! ## AH Differences
44//!
45//! Unlike ESP, the AH header (RFC 4302) does NOT encrypt the payload:
46//!
47//! ```text
48//! +---------------+---------------+---------------+---------------+
49//! |  Next Header  |  Payload Len  |          RESERVED             | <- Cleartext
50//! +---------------+---------------+---------------+---------------+
51//! |                Security Parameters Index (SPI)                | <- Cleartext
52//! +---------------+---------------+---------------+---------------+
53//! |                    Sequence Number Field                      | <- Cleartext
54//! +---------------+---------------+---------------+---------------+
55//! |                 Integrity Check Value (ICV)                   | <- Authentication
56//! |                         (variable)                            |
57//! +---------------+---------------+---------------+---------------+
58//! |                    IP Payload (NOT encrypted)                 | <- Cleartext
59//! +---------------+---------------+---------------+---------------+
60//! ```
61//!
62//! For AH packets this parser:
63//! - Extracts all header fields including Next Header
64//! - Can determine the encapsulated protocol
65//! - Payload is accessible for further parsing
66//! - Cannot verify the ICV without the SA keys
67//!
68//! ## Decryption Requirements
69//!
70//! To decrypt ESP payloads, you would need:
71//! 1. The Security Association (SA) for this SPI
72//! 2. The encryption algorithm (AES-CBC, AES-GCM, etc.)
73//! 3. The encryption key
74//! 4. The IV (typically prepended to the encrypted payload)
75//!
76//! This information is typically exchanged via IKE (Internet Key Exchange)
77//! and is not available from packet capture alone.
78//!
79//! ## Practical Implications
80//!
81//! When querying IPsec traffic:
82//! - `ipsec.spi` - Available for both ESP and AH
83//! - `ipsec.sequence` - Available for both ESP and AH
84//! - `ipsec.protocol` - Returns "ESP" or "AH"
85//! - `ipsec.ah_next_header` - Only available for AH packets
86//! - Inner protocol fields - Only parseable for AH packets
87//!
88//! For ESP traffic analysis without decryption, focus on:
89//! - Traffic flow analysis (source/destination IPs)
90//! - SPI values (can identify security associations)
91//! - Sequence numbers (can detect replay attacks or packet loss)
92//! - Packet timing and sizes
93
94use smallvec::SmallVec;
95
96use super::ipv6::next_header;
97use super::{FieldValue, ParseContext, ParseResult, PayloadMode, Protocol};
98use crate::schema::{DataKind, FieldDescriptor};
99
100/// IPsec protocol parser (handles both ESP and AH).
101#[derive(Debug, Clone, Copy)]
102pub struct IpsecProtocol;
103
104impl Protocol for IpsecProtocol {
105    fn name(&self) -> &'static str {
106        "ipsec"
107    }
108
109    fn display_name(&self) -> &'static str {
110        "IPsec"
111    }
112
113    fn can_parse(&self, context: &ParseContext) -> Option<u32> {
114        // Match when IP protocol hint equals ESP (50) or AH (51)
115        match context.hint("ip_protocol") {
116            Some(proto) if proto == next_header::ESP as u64 => Some(100),
117            Some(proto) if proto == next_header::AH as u64 => Some(100),
118            _ => None,
119        }
120    }
121
122    fn parse<'a>(&self, data: &'a [u8], context: &ParseContext) -> ParseResult<'a> {
123        // Determine if this is ESP or AH based on the context hint
124        let is_esp = match context.hint("ip_protocol") {
125            Some(proto) => proto == next_header::ESP as u64,
126            None => return ParseResult::error("IPsec: missing ip_protocol hint".to_string(), data),
127        };
128
129        if is_esp {
130            self.parse_esp(data)
131        } else {
132            self.parse_ah(data)
133        }
134    }
135
136    fn schema_fields(&self) -> Vec<FieldDescriptor> {
137        vec![
138            FieldDescriptor::new("ipsec.protocol", DataKind::String).set_nullable(true),
139            FieldDescriptor::new("ipsec.spi", DataKind::UInt32).set_nullable(true),
140            FieldDescriptor::new("ipsec.sequence", DataKind::UInt32).set_nullable(true),
141            FieldDescriptor::new("ipsec.ah_next_header", DataKind::UInt8).set_nullable(true),
142            FieldDescriptor::new("ipsec.ah_icv_length", DataKind::UInt8).set_nullable(true),
143        ]
144    }
145
146    fn child_protocols(&self) -> &[&'static str] {
147        // ESP payload is encrypted, AH can have any IP protocol
148        &[]
149    }
150
151    fn payload_mode(&self) -> PayloadMode {
152        // ESP payload is encrypted, so we can't parse further
153        // AH payload could be parsed, but we treat IPsec as terminal for simplicity
154        PayloadMode::None
155    }
156
157    fn dependencies(&self) -> &'static [&'static str] {
158        &["ipv4", "ipv6"] // IPsec (ESP/AH) runs over IPv4/IPv6
159    }
160}
161
162impl IpsecProtocol {
163    /// Parse ESP (Encapsulating Security Payload) header.
164    ///
165    /// ESP Header:
166    /// ```text
167    ///  0                   1                   2                   3
168    ///  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
169    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
170    /// |               Security Parameters Index (SPI)                 |
171    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
172    /// |                      Sequence Number                          |
173    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
174    /// |                    Payload Data (encrypted)                   |
175    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
176    /// ```
177    fn parse_esp<'a>(&self, data: &'a [u8]) -> ParseResult<'a> {
178        // ESP header minimum is 8 bytes (SPI + Sequence Number)
179        if data.len() < 8 {
180            return ParseResult::error("ESP header too short".to_string(), data);
181        }
182
183        let mut fields = SmallVec::new();
184
185        fields.push(("protocol", FieldValue::Str("ESP")));
186
187        // Bytes 0-3: Security Parameters Index (SPI)
188        let spi = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
189        fields.push(("spi", FieldValue::UInt32(spi)));
190
191        // Bytes 4-7: Sequence Number
192        let sequence = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
193        fields.push(("sequence", FieldValue::UInt32(sequence)));
194
195        // The rest is encrypted payload, we can't parse further
196        // Note: The actual payload starts at byte 8, but it's encrypted
197        // The trailer (padding, pad length, next header) and ICV are at the end
198        // but we can't determine their location without decryption
199
200        // Return with no child hints since ESP payload is encrypted
201        ParseResult::success(fields, &data[8..], SmallVec::new())
202    }
203
204    /// Parse AH (Authentication Header).
205    ///
206    /// AH Header:
207    /// ```text
208    ///  0                   1                   2                   3
209    ///  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
210    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
211    /// |  Next Header  |  Payload Len  |          RESERVED             |
212    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
213    /// |                 Security Parameters Index (SPI)               |
214    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
215    /// |                    Sequence Number Field                      |
216    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
217    /// |                    ICV (variable length)                      |
218    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
219    /// ```
220    fn parse_ah<'a>(&self, data: &'a [u8]) -> ParseResult<'a> {
221        // AH header minimum is 12 bytes (without ICV)
222        if data.len() < 12 {
223            return ParseResult::error("AH header too short".to_string(), data);
224        }
225
226        let mut fields = SmallVec::new();
227
228        fields.push(("protocol", FieldValue::Str("AH")));
229
230        // Byte 0: Next Header
231        let next_header = data[0];
232        fields.push(("ah_next_header", FieldValue::UInt8(next_header)));
233
234        // Byte 1: Payload Length (in 32-bit words, minus 2)
235        // Total AH length = (payload_len + 2) * 4 bytes
236        let payload_len = data[1];
237        let ah_length = ((payload_len as usize) + 2) * 4;
238
239        // Calculate ICV length: AH length - 12 bytes (fixed header)
240        let icv_length = if ah_length > 12 {
241            (ah_length - 12) as u8
242        } else {
243            0
244        };
245        fields.push(("ah_icv_length", FieldValue::UInt8(icv_length)));
246
247        // Bytes 2-3: Reserved
248
249        // Bytes 4-7: SPI
250        let spi = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
251        fields.push(("spi", FieldValue::UInt32(spi)));
252
253        // Bytes 8-11: Sequence Number
254        let sequence = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
255        fields.push(("sequence", FieldValue::UInt32(sequence)));
256
257        // ICV follows (variable length based on payload_len)
258        // Payload starts after AH header
259
260        if data.len() < ah_length {
261            return ParseResult::error("AH: data too short for declared length".to_string(), data);
262        }
263
264        // Set up child hints for the next protocol (based on next_header)
265        let mut child_hints = SmallVec::new();
266        child_hints.push(("ip_protocol", next_header as u64));
267
268        ParseResult::success(fields, &data[ah_length..], child_hints)
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    /// Create an ESP header.
277    fn create_esp_header(spi: u32, sequence: u32) -> Vec<u8> {
278        let mut header = Vec::new();
279        header.extend_from_slice(&spi.to_be_bytes());
280        header.extend_from_slice(&sequence.to_be_bytes());
281        header
282    }
283
284    /// Create an AH header.
285    fn create_ah_header(next_header: u8, spi: u32, sequence: u32, icv_len: usize) -> Vec<u8> {
286        let mut header = Vec::new();
287
288        // Next Header
289        header.push(next_header);
290
291        // Payload Length (in 32-bit words minus 2)
292        // AH length = (payload_len + 2) * 4
293        // ICV is after 12-byte fixed header
294        // So: payload_len = (12 + icv_len) / 4 - 2
295        let ah_length = 12 + icv_len;
296        let payload_len = (ah_length / 4) - 2;
297        header.push(payload_len as u8);
298
299        // Reserved
300        header.extend_from_slice(&[0x00, 0x00]);
301
302        // SPI
303        header.extend_from_slice(&spi.to_be_bytes());
304
305        // Sequence Number
306        header.extend_from_slice(&sequence.to_be_bytes());
307
308        // ICV (variable length)
309        header.extend(vec![0u8; icv_len]);
310
311        header
312    }
313
314    // Test 1: can_parse with ESP (protocol 50)
315    #[test]
316    fn test_can_parse_with_esp_protocol() {
317        let parser = IpsecProtocol;
318
319        // Without hint
320        let ctx1 = ParseContext::new(1);
321        assert!(parser.can_parse(&ctx1).is_none());
322
323        // With wrong protocol
324        let mut ctx2 = ParseContext::new(1);
325        ctx2.insert_hint("ip_protocol", 6); // TCP
326        assert!(parser.can_parse(&ctx2).is_none());
327
328        // With ESP protocol
329        let mut ctx3 = ParseContext::new(1);
330        ctx3.insert_hint("ip_protocol", 50);
331        assert!(parser.can_parse(&ctx3).is_some());
332        assert_eq!(parser.can_parse(&ctx3), Some(100));
333    }
334
335    // Test 2: can_parse with AH (protocol 51)
336    #[test]
337    fn test_can_parse_with_ah_protocol() {
338        let parser = IpsecProtocol;
339
340        let mut context = ParseContext::new(1);
341        context.insert_hint("ip_protocol", 51);
342
343        assert!(parser.can_parse(&context).is_some());
344        assert_eq!(parser.can_parse(&context), Some(100));
345    }
346
347    // Test 3: ESP SPI and sequence extraction
348    #[test]
349    fn test_esp_spi_and_sequence_extraction() {
350        let parser = IpsecProtocol;
351        let mut context = ParseContext::new(1);
352        context.insert_hint("ip_protocol", 50); // ESP
353
354        let header = create_esp_header(0x12345678, 0xABCDEF01);
355        let result = parser.parse(&header, &context);
356
357        assert!(result.is_ok());
358        assert_eq!(result.get("protocol"), Some(&FieldValue::Str("ESP")));
359        assert_eq!(result.get("spi"), Some(&FieldValue::UInt32(0x12345678)));
360        assert_eq!(
361            result.get("sequence"),
362            Some(&FieldValue::UInt32(0xABCDEF01))
363        );
364    }
365
366    // Test 4: AH header parsing
367    #[test]
368    fn test_ah_header_parsing() {
369        let parser = IpsecProtocol;
370        let mut context = ParseContext::new(1);
371        context.insert_hint("ip_protocol", 51); // AH
372
373        // AH with 12-byte ICV (HMAC-SHA-256-128)
374        let header = create_ah_header(6, 0x87654321, 0x00000001, 12);
375        let result = parser.parse(&header, &context);
376
377        assert!(result.is_ok());
378        assert_eq!(result.get("protocol"), Some(&FieldValue::Str("AH")));
379        assert_eq!(result.get("spi"), Some(&FieldValue::UInt32(0x87654321)));
380        assert_eq!(result.get("sequence"), Some(&FieldValue::UInt32(1)));
381    }
382
383    // Test 5: AH next header field
384    #[test]
385    fn test_ah_next_header_field() {
386        let parser = IpsecProtocol;
387        let mut context = ParseContext::new(1);
388        context.insert_hint("ip_protocol", 51); // AH
389
390        // Test different next headers
391        let test_cases = [
392            (6u8, "TCP"),
393            (17u8, "UDP"),
394            (1u8, "ICMP"),
395            (50u8, "ESP"), // AH can encapsulate ESP
396        ];
397
398        for (next_header, _name) in test_cases {
399            let header = create_ah_header(next_header, 0x1234, 0x5678, 12);
400            let result = parser.parse(&header, &context);
401
402            assert!(result.is_ok());
403            assert_eq!(
404                result.get("ah_next_header"),
405                Some(&FieldValue::UInt8(next_header))
406            );
407            assert_eq!(result.hint("ip_protocol"), Some(next_header as u64));
408        }
409    }
410
411    // Test 6: Protocol field ("ESP" vs "AH")
412    #[test]
413    fn test_protocol_field() {
414        let parser = IpsecProtocol;
415
416        // ESP
417        let mut ctx_esp = ParseContext::new(1);
418        ctx_esp.insert_hint("ip_protocol", 50);
419        let esp_header = create_esp_header(0x1234, 0x5678);
420        let result_esp = parser.parse(&esp_header, &ctx_esp);
421        assert!(result_esp.is_ok());
422        assert_eq!(result_esp.get("protocol"), Some(&FieldValue::Str("ESP")));
423
424        // AH
425        let mut ctx_ah = ParseContext::new(1);
426        ctx_ah.insert_hint("ip_protocol", 51);
427        let ah_header = create_ah_header(6, 0x1234, 0x5678, 12);
428        let result_ah = parser.parse(&ah_header, &ctx_ah);
429        assert!(result_ah.is_ok());
430        assert_eq!(result_ah.get("protocol"), Some(&FieldValue::Str("AH")));
431    }
432
433    // Test 7: ESP too short
434    #[test]
435    fn test_esp_too_short() {
436        let parser = IpsecProtocol;
437        let mut context = ParseContext::new(1);
438        context.insert_hint("ip_protocol", 50);
439
440        let short_header = [0x00, 0x00, 0x00, 0x01]; // Only 4 bytes
441        let result = parser.parse(&short_header, &context);
442
443        assert!(!result.is_ok());
444        assert!(result.error.is_some());
445    }
446
447    // Test 8: AH too short
448    #[test]
449    fn test_ah_too_short() {
450        let parser = IpsecProtocol;
451        let mut context = ParseContext::new(1);
452        context.insert_hint("ip_protocol", 51);
453
454        let short_header = [0x06, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]; // Only 8 bytes
455        let result = parser.parse(&short_header, &context);
456
457        assert!(!result.is_ok());
458        assert!(result.error.is_some());
459    }
460
461    // Test 9: AH ICV length derivation
462    #[test]
463    fn test_ah_icv_length_derivation() {
464        let parser = IpsecProtocol;
465        let mut context = ParseContext::new(1);
466        context.insert_hint("ip_protocol", 51);
467
468        // Test different ICV lengths
469        let test_icv_lengths = [12usize, 16, 20, 32];
470
471        for icv_len in test_icv_lengths {
472            let header = create_ah_header(6, 0x1234, 0x5678, icv_len);
473            let result = parser.parse(&header, &context);
474
475            assert!(result.is_ok());
476            assert_eq!(
477                result.get("ah_icv_length"),
478                Some(&FieldValue::UInt8(icv_len as u8))
479            );
480        }
481    }
482
483    // Test 10: Schema fields
484    #[test]
485    fn test_ipsec_schema_fields() {
486        let parser = IpsecProtocol;
487        let fields = parser.schema_fields();
488
489        assert!(!fields.is_empty());
490        let field_names: Vec<&str> = fields.iter().map(|f| f.name).collect();
491        assert!(field_names.contains(&"ipsec.protocol"));
492        assert!(field_names.contains(&"ipsec.spi"));
493        assert!(field_names.contains(&"ipsec.sequence"));
494        assert!(field_names.contains(&"ipsec.ah_next_header"));
495        assert!(field_names.contains(&"ipsec.ah_icv_length"));
496    }
497
498    // Test 11: ESP with payload
499    #[test]
500    fn test_esp_with_payload() {
501        let parser = IpsecProtocol;
502        let mut context = ParseContext::new(1);
503        context.insert_hint("ip_protocol", 50);
504
505        let mut data = create_esp_header(0x1234, 0x5678);
506        // Add encrypted payload
507        data.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
508
509        let result = parser.parse(&data, &context);
510
511        assert!(result.is_ok());
512        assert_eq!(result.remaining.len(), 4); // Encrypted payload
513    }
514
515    // Test 12: AH with following payload
516    #[test]
517    fn test_ah_with_following_payload() {
518        let parser = IpsecProtocol;
519        let mut context = ParseContext::new(1);
520        context.insert_hint("ip_protocol", 51);
521
522        let mut data = create_ah_header(6, 0x1234, 0x5678, 12);
523        // Add payload (e.g., TCP header start)
524        data.extend_from_slice(&[0x00, 0x50, 0x01, 0xBB, 0x00, 0x00, 0x00, 0x00]);
525
526        let result = parser.parse(&data, &context);
527
528        assert!(result.is_ok());
529        assert_eq!(result.remaining.len(), 8); // TCP header
530        assert_eq!(result.hint("ip_protocol"), Some(6u64)); // TCP
531    }
532}