pcapsql_core/protocol/
dhcp.rs

1//! DHCP protocol parser.
2//!
3//! Parses DHCP (Dynamic Host Configuration Protocol) messages used for
4//! network configuration. Matches on UDP ports 67/68 and the DHCP magic cookie.
5
6use compact_str::CompactString;
7use smallvec::SmallVec;
8
9use super::{FieldValue, ParseContext, ParseResult, Protocol};
10use crate::schema::{DataKind, FieldDescriptor};
11
12/// DHCP server port.
13pub const DHCP_SERVER_PORT: u16 = 67;
14
15/// DHCP client port.
16pub const DHCP_CLIENT_PORT: u16 = 68;
17
18/// DHCP magic cookie (0x63825363).
19const DHCP_MAGIC_COOKIE: [u8; 4] = [0x63, 0x82, 0x53, 0x63];
20
21/// DHCP minimum header size (without options).
22const DHCP_MIN_HEADER_SIZE: usize = 236;
23
24/// DHCP protocol parser.
25#[derive(Debug, Clone, Copy)]
26pub struct DhcpProtocol;
27
28impl Protocol for DhcpProtocol {
29    fn name(&self) -> &'static str {
30        "dhcp"
31    }
32
33    fn display_name(&self) -> &'static str {
34        "DHCP"
35    }
36
37    fn can_parse(&self, context: &ParseContext) -> Option<u32> {
38        // Check for DHCP ports in either direction
39        let src_port = context.hint("src_port");
40        let dst_port = context.hint("dst_port");
41
42        let is_dhcp_port = |p: u64| p == DHCP_SERVER_PORT as u64 || p == DHCP_CLIENT_PORT as u64;
43
44        match (src_port, dst_port) {
45            (Some(p), _) if is_dhcp_port(p) => Some(100),
46            (_, Some(p)) if is_dhcp_port(p) => Some(100),
47            _ => None,
48        }
49    }
50
51    fn parse<'a>(&self, data: &'a [u8], _context: &ParseContext) -> ParseResult<'a> {
52        // DHCP header is at least 236 bytes
53        if data.len() < DHCP_MIN_HEADER_SIZE {
54            return ParseResult::error("DHCP header too short".to_string(), data);
55        }
56
57        // Check for DHCP magic cookie at offset 236
58        if data.len() >= 240 && data[236..240] != DHCP_MAGIC_COOKIE {
59            return ParseResult::error(
60                "DHCP magic cookie not found (might be BOOTP)".to_string(),
61                data,
62            );
63        }
64
65        let mut fields = SmallVec::new();
66
67        // Parse fixed header fields
68        // Op (1 byte): 1 = BOOTREQUEST, 2 = BOOTREPLY
69        let op = data[0];
70        fields.push(("op", FieldValue::UInt8(op)));
71
72        // Hardware type (1 byte): 1 = Ethernet
73        let htype = data[1];
74        fields.push(("htype", FieldValue::UInt8(htype)));
75
76        // Hardware address length (1 byte)
77        let hlen = data[2];
78        fields.push(("hlen", FieldValue::UInt8(hlen)));
79
80        // Hops (1 byte)
81        let hops = data[3];
82        fields.push(("hops", FieldValue::UInt8(hops)));
83
84        // Transaction ID (4 bytes)
85        let xid = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
86        fields.push(("xid", FieldValue::UInt32(xid)));
87
88        // Seconds elapsed (2 bytes)
89        let secs = u16::from_be_bytes([data[8], data[9]]);
90        fields.push(("secs", FieldValue::UInt16(secs)));
91
92        // Flags (2 bytes)
93        let flags = u16::from_be_bytes([data[10], data[11]]);
94        fields.push(("flags", FieldValue::UInt16(flags)));
95
96        // Client IP address (4 bytes)
97        let ciaddr = format_ip(&data[12..16]);
98        fields.push((
99            "ciaddr",
100            FieldValue::OwnedString(CompactString::new(ciaddr)),
101        ));
102
103        // Your (client) IP address (4 bytes)
104        let yiaddr = format_ip(&data[16..20]);
105        fields.push((
106            "yiaddr",
107            FieldValue::OwnedString(CompactString::new(yiaddr)),
108        ));
109
110        // Server IP address (4 bytes)
111        let siaddr = format_ip(&data[20..24]);
112        fields.push((
113            "siaddr",
114            FieldValue::OwnedString(CompactString::new(siaddr)),
115        ));
116
117        // Gateway/relay IP address (4 bytes)
118        let giaddr = format_ip(&data[24..28]);
119        fields.push((
120            "giaddr",
121            FieldValue::OwnedString(CompactString::new(giaddr)),
122        ));
123
124        // Client hardware address (16 bytes, but only first hlen are valid)
125        let chaddr_len = (hlen as usize).min(16);
126        let chaddr = format_mac(&data[28..28 + chaddr_len]);
127        fields.push((
128            "chaddr",
129            FieldValue::OwnedString(CompactString::new(chaddr)),
130        ));
131
132        // Skip server host name (64 bytes) and boot file name (128 bytes)
133        // These are at offsets 44 and 108 respectively
134
135        // Parse options (starting at offset 240, after magic cookie)
136        if data.len() > 240 {
137            parse_dhcp_options(&data[240..], &mut fields);
138        }
139
140        // No child protocols after DHCP
141        ParseResult::success(fields, &[], SmallVec::new())
142    }
143
144    fn schema_fields(&self) -> Vec<FieldDescriptor> {
145        vec![
146            FieldDescriptor::new("dhcp.op", DataKind::UInt8).set_nullable(true),
147            FieldDescriptor::new("dhcp.htype", DataKind::UInt8).set_nullable(true),
148            FieldDescriptor::new("dhcp.hlen", DataKind::UInt8).set_nullable(true),
149            FieldDescriptor::new("dhcp.hops", DataKind::UInt8).set_nullable(true),
150            FieldDescriptor::new("dhcp.xid", DataKind::UInt32).set_nullable(true),
151            FieldDescriptor::new("dhcp.secs", DataKind::UInt16).set_nullable(true),
152            FieldDescriptor::new("dhcp.flags", DataKind::UInt16).set_nullable(true),
153            FieldDescriptor::new("dhcp.ciaddr", DataKind::String).set_nullable(true),
154            FieldDescriptor::new("dhcp.yiaddr", DataKind::String).set_nullable(true),
155            FieldDescriptor::new("dhcp.siaddr", DataKind::String).set_nullable(true),
156            FieldDescriptor::new("dhcp.giaddr", DataKind::String).set_nullable(true),
157            FieldDescriptor::new("dhcp.chaddr", DataKind::String).set_nullable(true),
158            FieldDescriptor::new("dhcp.message_type", DataKind::UInt8).set_nullable(true),
159            FieldDescriptor::new("dhcp.server_id", DataKind::String).set_nullable(true),
160            FieldDescriptor::new("dhcp.lease_time", DataKind::UInt32).set_nullable(true),
161            FieldDescriptor::new("dhcp.subnet_mask", DataKind::String).set_nullable(true),
162            FieldDescriptor::new("dhcp.router", DataKind::String).set_nullable(true),
163            FieldDescriptor::new("dhcp.dns_servers", DataKind::String).set_nullable(true),
164        ]
165    }
166
167    fn child_protocols(&self) -> &[&'static str] {
168        &[]
169    }
170
171    fn dependencies(&self) -> &'static [&'static str] {
172        &["udp"]
173    }
174}
175
176/// Format an IP address from 4 bytes.
177fn format_ip(bytes: &[u8]) -> String {
178    if bytes.len() >= 4 {
179        format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3])
180    } else {
181        "0.0.0.0".to_string()
182    }
183}
184
185/// Format a MAC address from bytes.
186fn format_mac(bytes: &[u8]) -> String {
187    bytes
188        .iter()
189        .map(|b| format!("{b:02x}"))
190        .collect::<Vec<_>>()
191        .join(":")
192}
193
194/// Parse DHCP options and add relevant fields.
195fn parse_dhcp_options(data: &[u8], fields: &mut SmallVec<[(&'static str, FieldValue); 16]>) {
196    let mut offset = 0;
197
198    while offset < data.len() {
199        let option_code = data[offset];
200
201        // Option 0: Pad
202        if option_code == 0 {
203            offset += 1;
204            continue;
205        }
206
207        // Option 255: End
208        if option_code == 255 {
209            break;
210        }
211
212        // Other options have length byte
213        if offset + 1 >= data.len() {
214            break;
215        }
216        let option_len = data[offset + 1] as usize;
217
218        if offset + 2 + option_len > data.len() {
219            break;
220        }
221
222        let option_data = &data[offset + 2..offset + 2 + option_len];
223
224        match option_code {
225            // Option 1: Subnet Mask
226            1 if option_len == 4 => {
227                fields.push((
228                    "subnet_mask",
229                    FieldValue::OwnedString(CompactString::new(format_ip(option_data))),
230                ));
231            }
232            // Option 3: Router
233            3 if option_len >= 4 => {
234                // Can have multiple routers, just use first one
235                fields.push((
236                    "router",
237                    FieldValue::OwnedString(CompactString::new(format_ip(&option_data[..4]))),
238                ));
239            }
240            // Option 6: DNS Servers
241            6 if option_len >= 4 => {
242                let dns_servers: Vec<String> = option_data
243                    .chunks(4)
244                    .filter(|chunk| chunk.len() == 4)
245                    .map(format_ip)
246                    .collect();
247                fields.push((
248                    "dns_servers",
249                    FieldValue::OwnedString(CompactString::new(dns_servers.join(","))),
250                ));
251            }
252            // Option 51: IP Address Lease Time
253            51 if option_len == 4 => {
254                let lease_time = u32::from_be_bytes([
255                    option_data[0],
256                    option_data[1],
257                    option_data[2],
258                    option_data[3],
259                ]);
260                fields.push(("lease_time", FieldValue::UInt32(lease_time)));
261            }
262            // Option 53: DHCP Message Type
263            53 if option_len == 1 => {
264                fields.push(("message_type", FieldValue::UInt8(option_data[0])));
265            }
266            // Option 54: Server Identifier
267            54 if option_len == 4 => {
268                fields.push((
269                    "server_id",
270                    FieldValue::OwnedString(CompactString::new(format_ip(option_data))),
271                ));
272            }
273            _ => {}
274        }
275
276        offset += 2 + option_len;
277    }
278}
279
280/// DHCP message types.
281#[allow(dead_code)]
282pub mod message_type {
283    pub const DISCOVER: u8 = 1;
284    pub const OFFER: u8 = 2;
285    pub const REQUEST: u8 = 3;
286    pub const DECLINE: u8 = 4;
287    pub const ACK: u8 = 5;
288    pub const NAK: u8 = 6;
289    pub const RELEASE: u8 = 7;
290    pub const INFORM: u8 = 8;
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    /// Create a minimal DHCP Discover packet.
298    fn create_dhcp_discover() -> Vec<u8> {
299        let mut packet = vec![0u8; 300];
300
301        // Op: BOOTREQUEST (1)
302        packet[0] = 1;
303        // Hardware type: Ethernet (1)
304        packet[1] = 1;
305        // Hardware address length: 6
306        packet[2] = 6;
307        // Hops: 0
308        packet[3] = 0;
309        // Transaction ID
310        packet[4..8].copy_from_slice(&0x12345678u32.to_be_bytes());
311        // Seconds: 0
312        packet[8..10].copy_from_slice(&0u16.to_be_bytes());
313        // Flags: Broadcast (0x8000)
314        packet[10..12].copy_from_slice(&0x8000u16.to_be_bytes());
315        // Client IP: 0.0.0.0
316        // Your IP: 0.0.0.0
317        // Server IP: 0.0.0.0
318        // Gateway IP: 0.0.0.0
319        // Client MAC: 00:11:22:33:44:55
320        packet[28..34].copy_from_slice(&[0x00, 0x11, 0x22, 0x33, 0x44, 0x55]);
321
322        // Magic cookie at offset 236
323        packet[236..240].copy_from_slice(&DHCP_MAGIC_COOKIE);
324
325        // Options start at 240
326        // Option 53: DHCP Message Type = Discover (1)
327        packet[240] = 53;
328        packet[241] = 1;
329        packet[242] = message_type::DISCOVER;
330
331        // Option 255: End
332        packet[243] = 255;
333
334        packet
335    }
336
337    /// Create a DHCP Offer packet with common options.
338    fn create_dhcp_offer() -> Vec<u8> {
339        let mut packet = vec![0u8; 350];
340
341        // Op: BOOTREPLY (2)
342        packet[0] = 2;
343        // Hardware type: Ethernet (1)
344        packet[1] = 1;
345        // Hardware address length: 6
346        packet[2] = 6;
347        // Hops: 0
348        packet[3] = 0;
349        // Transaction ID
350        packet[4..8].copy_from_slice(&0xABCDEF01u32.to_be_bytes());
351        // Seconds: 0
352        packet[8..10].copy_from_slice(&0u16.to_be_bytes());
353        // Flags: 0
354        packet[10..12].copy_from_slice(&0u16.to_be_bytes());
355        // Client IP: 0.0.0.0
356        // Your IP: 192.168.1.100
357        packet[16..20].copy_from_slice(&[192, 168, 1, 100]);
358        // Server IP: 192.168.1.1
359        packet[20..24].copy_from_slice(&[192, 168, 1, 1]);
360        // Gateway IP: 0.0.0.0
361        // Client MAC: 00:11:22:33:44:55
362        packet[28..34].copy_from_slice(&[0x00, 0x11, 0x22, 0x33, 0x44, 0x55]);
363
364        // Magic cookie at offset 236
365        packet[236..240].copy_from_slice(&DHCP_MAGIC_COOKIE);
366
367        let mut opt_offset = 240;
368
369        // Option 53: DHCP Message Type = Offer (2)
370        packet[opt_offset] = 53;
371        packet[opt_offset + 1] = 1;
372        packet[opt_offset + 2] = message_type::OFFER;
373        opt_offset += 3;
374
375        // Option 54: Server Identifier = 192.168.1.1
376        packet[opt_offset] = 54;
377        packet[opt_offset + 1] = 4;
378        packet[opt_offset + 2..opt_offset + 6].copy_from_slice(&[192, 168, 1, 1]);
379        opt_offset += 6;
380
381        // Option 51: Lease Time = 86400 seconds (1 day)
382        packet[opt_offset] = 51;
383        packet[opt_offset + 1] = 4;
384        packet[opt_offset + 2..opt_offset + 6].copy_from_slice(&86400u32.to_be_bytes());
385        opt_offset += 6;
386
387        // Option 1: Subnet Mask = 255.255.255.0
388        packet[opt_offset] = 1;
389        packet[opt_offset + 1] = 4;
390        packet[opt_offset + 2..opt_offset + 6].copy_from_slice(&[255, 255, 255, 0]);
391        opt_offset += 6;
392
393        // Option 3: Router = 192.168.1.1
394        packet[opt_offset] = 3;
395        packet[opt_offset + 1] = 4;
396        packet[opt_offset + 2..opt_offset + 6].copy_from_slice(&[192, 168, 1, 1]);
397        opt_offset += 6;
398
399        // Option 6: DNS Servers = 8.8.8.8, 8.8.4.4
400        packet[opt_offset] = 6;
401        packet[opt_offset + 1] = 8;
402        packet[opt_offset + 2..opt_offset + 6].copy_from_slice(&[8, 8, 8, 8]);
403        packet[opt_offset + 6..opt_offset + 10].copy_from_slice(&[8, 8, 4, 4]);
404        opt_offset += 10;
405
406        // Option 255: End
407        packet[opt_offset] = 255;
408
409        packet
410    }
411
412    /// Create a DHCP ACK packet.
413    fn create_dhcp_ack() -> Vec<u8> {
414        let mut packet = create_dhcp_offer();
415        // Change message type to ACK
416        packet[242] = message_type::ACK;
417        packet
418    }
419
420    #[test]
421    fn test_can_parse_dhcp() {
422        let parser = DhcpProtocol;
423
424        // Without hint
425        let ctx1 = ParseContext::new(1);
426        assert!(parser.can_parse(&ctx1).is_none());
427
428        // With dst_port 67 (server)
429        let mut ctx2 = ParseContext::new(1);
430        ctx2.insert_hint("dst_port", 67);
431        assert!(parser.can_parse(&ctx2).is_some());
432
433        // With src_port 68 (client)
434        let mut ctx3 = ParseContext::new(1);
435        ctx3.insert_hint("src_port", 68);
436        assert!(parser.can_parse(&ctx3).is_some());
437
438        // With different port
439        let mut ctx4 = ParseContext::new(1);
440        ctx4.insert_hint("dst_port", 80);
441        assert!(parser.can_parse(&ctx4).is_none());
442    }
443
444    #[test]
445    fn test_parse_dhcp_discover() {
446        let packet = create_dhcp_discover();
447
448        let parser = DhcpProtocol;
449        let mut context = ParseContext::new(1);
450        context.insert_hint("dst_port", 67);
451
452        let result = parser.parse(&packet, &context);
453
454        assert!(result.is_ok());
455        assert_eq!(result.get("op"), Some(&FieldValue::UInt8(1)));
456        assert_eq!(result.get("htype"), Some(&FieldValue::UInt8(1)));
457        assert_eq!(result.get("hlen"), Some(&FieldValue::UInt8(6)));
458        assert_eq!(result.get("xid"), Some(&FieldValue::UInt32(0x12345678)));
459        assert_eq!(result.get("flags"), Some(&FieldValue::UInt16(0x8000)));
460        assert_eq!(
461            result.get("chaddr"),
462            Some(&FieldValue::OwnedString(CompactString::new(
463                "00:11:22:33:44:55"
464            )))
465        );
466        assert_eq!(result.get("message_type"), Some(&FieldValue::UInt8(1)));
467    }
468
469    #[test]
470    fn test_parse_dhcp_offer() {
471        let packet = create_dhcp_offer();
472
473        let parser = DhcpProtocol;
474        let mut context = ParseContext::new(1);
475        context.insert_hint("src_port", 67);
476
477        let result = parser.parse(&packet, &context);
478
479        assert!(result.is_ok());
480        assert_eq!(result.get("op"), Some(&FieldValue::UInt8(2)));
481        assert_eq!(result.get("xid"), Some(&FieldValue::UInt32(0xABCDEF01)));
482        assert_eq!(
483            result.get("yiaddr"),
484            Some(&FieldValue::OwnedString(CompactString::new(
485                "192.168.1.100"
486            )))
487        );
488        assert_eq!(
489            result.get("siaddr"),
490            Some(&FieldValue::OwnedString(CompactString::new("192.168.1.1")))
491        );
492        assert_eq!(result.get("message_type"), Some(&FieldValue::UInt8(2)));
493        assert_eq!(
494            result.get("server_id"),
495            Some(&FieldValue::OwnedString(CompactString::new("192.168.1.1")))
496        );
497    }
498
499    #[test]
500    fn test_parse_dhcp_ack() {
501        let packet = create_dhcp_ack();
502
503        let parser = DhcpProtocol;
504        let mut context = ParseContext::new(1);
505        context.insert_hint("src_port", 67);
506
507        let result = parser.parse(&packet, &context);
508
509        assert!(result.is_ok());
510        assert_eq!(result.get("message_type"), Some(&FieldValue::UInt8(5)));
511    }
512
513    #[test]
514    fn test_parse_dhcp_options() {
515        let packet = create_dhcp_offer();
516
517        let parser = DhcpProtocol;
518        let mut context = ParseContext::new(1);
519        context.insert_hint("src_port", 67);
520
521        let result = parser.parse(&packet, &context);
522
523        assert!(result.is_ok());
524        assert_eq!(result.get("lease_time"), Some(&FieldValue::UInt32(86400)));
525        assert_eq!(
526            result.get("subnet_mask"),
527            Some(&FieldValue::OwnedString(CompactString::new(
528                "255.255.255.0"
529            )))
530        );
531        assert_eq!(
532            result.get("router"),
533            Some(&FieldValue::OwnedString(CompactString::new("192.168.1.1")))
534        );
535        assert_eq!(
536            result.get("dns_servers"),
537            Some(&FieldValue::OwnedString(CompactString::new(
538                "8.8.8.8,8.8.4.4"
539            )))
540        );
541    }
542
543    #[test]
544    fn test_dhcp_schema_fields() {
545        let parser = DhcpProtocol;
546        let fields = parser.schema_fields();
547
548        assert!(!fields.is_empty());
549
550        let field_names: Vec<&str> = fields.iter().map(|f| f.name).collect();
551        assert!(field_names.contains(&"dhcp.op"));
552        assert!(field_names.contains(&"dhcp.xid"));
553        assert!(field_names.contains(&"dhcp.message_type"));
554        assert!(field_names.contains(&"dhcp.lease_time"));
555    }
556
557    #[test]
558    fn test_dhcp_too_short() {
559        let short_packet = vec![0u8; 100]; // Too short for DHCP
560
561        let parser = DhcpProtocol;
562        let mut context = ParseContext::new(1);
563        context.insert_hint("dst_port", 67);
564
565        let result = parser.parse(&short_packet, &context);
566
567        assert!(!result.is_ok());
568        assert!(result.error.is_some());
569    }
570}