1use compact_str::CompactString;
7use smallvec::SmallVec;
8
9use super::{FieldValue, ParseContext, ParseResult, Protocol};
10use crate::schema::{DataKind, FieldDescriptor};
11
12pub const DHCP_SERVER_PORT: u16 = 67;
14
15pub const DHCP_CLIENT_PORT: u16 = 68;
17
18const DHCP_MAGIC_COOKIE: [u8; 4] = [0x63, 0x82, 0x53, 0x63];
20
21const DHCP_MIN_HEADER_SIZE: usize = 236;
23
24#[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 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 if data.len() < DHCP_MIN_HEADER_SIZE {
54 return ParseResult::error("DHCP header too short".to_string(), data);
55 }
56
57 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 let op = data[0];
70 fields.push(("op", FieldValue::UInt8(op)));
71
72 let htype = data[1];
74 fields.push(("htype", FieldValue::UInt8(htype)));
75
76 let hlen = data[2];
78 fields.push(("hlen", FieldValue::UInt8(hlen)));
79
80 let hops = data[3];
82 fields.push(("hops", FieldValue::UInt8(hops)));
83
84 let xid = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
86 fields.push(("xid", FieldValue::UInt32(xid)));
87
88 let secs = u16::from_be_bytes([data[8], data[9]]);
90 fields.push(("secs", FieldValue::UInt16(secs)));
91
92 let flags = u16::from_be_bytes([data[10], data[11]]);
94 fields.push(("flags", FieldValue::UInt16(flags)));
95
96 let ciaddr = format_ip(&data[12..16]);
98 fields.push((
99 "ciaddr",
100 FieldValue::OwnedString(CompactString::new(ciaddr)),
101 ));
102
103 let yiaddr = format_ip(&data[16..20]);
105 fields.push((
106 "yiaddr",
107 FieldValue::OwnedString(CompactString::new(yiaddr)),
108 ));
109
110 let siaddr = format_ip(&data[20..24]);
112 fields.push((
113 "siaddr",
114 FieldValue::OwnedString(CompactString::new(siaddr)),
115 ));
116
117 let giaddr = format_ip(&data[24..28]);
119 fields.push((
120 "giaddr",
121 FieldValue::OwnedString(CompactString::new(giaddr)),
122 ));
123
124 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 if data.len() > 240 {
137 parse_dhcp_options(&data[240..], &mut fields);
138 }
139
140 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
176fn 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
185fn format_mac(bytes: &[u8]) -> String {
187 bytes
188 .iter()
189 .map(|b| format!("{b:02x}"))
190 .collect::<Vec<_>>()
191 .join(":")
192}
193
194fn 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 if option_code == 0 {
203 offset += 1;
204 continue;
205 }
206
207 if option_code == 255 {
209 break;
210 }
211
212 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 1 if option_len == 4 => {
227 fields.push((
228 "subnet_mask",
229 FieldValue::OwnedString(CompactString::new(format_ip(option_data))),
230 ));
231 }
232 3 if option_len >= 4 => {
234 fields.push((
236 "router",
237 FieldValue::OwnedString(CompactString::new(format_ip(&option_data[..4]))),
238 ));
239 }
240 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 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 53 if option_len == 1 => {
264 fields.push(("message_type", FieldValue::UInt8(option_data[0])));
265 }
266 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#[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 fn create_dhcp_discover() -> Vec<u8> {
299 let mut packet = vec![0u8; 300];
300
301 packet[0] = 1;
303 packet[1] = 1;
305 packet[2] = 6;
307 packet[3] = 0;
309 packet[4..8].copy_from_slice(&0x12345678u32.to_be_bytes());
311 packet[8..10].copy_from_slice(&0u16.to_be_bytes());
313 packet[10..12].copy_from_slice(&0x8000u16.to_be_bytes());
315 packet[28..34].copy_from_slice(&[0x00, 0x11, 0x22, 0x33, 0x44, 0x55]);
321
322 packet[236..240].copy_from_slice(&DHCP_MAGIC_COOKIE);
324
325 packet[240] = 53;
328 packet[241] = 1;
329 packet[242] = message_type::DISCOVER;
330
331 packet[243] = 255;
333
334 packet
335 }
336
337 fn create_dhcp_offer() -> Vec<u8> {
339 let mut packet = vec![0u8; 350];
340
341 packet[0] = 2;
343 packet[1] = 1;
345 packet[2] = 6;
347 packet[3] = 0;
349 packet[4..8].copy_from_slice(&0xABCDEF01u32.to_be_bytes());
351 packet[8..10].copy_from_slice(&0u16.to_be_bytes());
353 packet[10..12].copy_from_slice(&0u16.to_be_bytes());
355 packet[16..20].copy_from_slice(&[192, 168, 1, 100]);
358 packet[20..24].copy_from_slice(&[192, 168, 1, 1]);
360 packet[28..34].copy_from_slice(&[0x00, 0x11, 0x22, 0x33, 0x44, 0x55]);
363
364 packet[236..240].copy_from_slice(&DHCP_MAGIC_COOKIE);
366
367 let mut opt_offset = 240;
368
369 packet[opt_offset] = 53;
371 packet[opt_offset + 1] = 1;
372 packet[opt_offset + 2] = message_type::OFFER;
373 opt_offset += 3;
374
375 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 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 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 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 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 packet[opt_offset] = 255;
408
409 packet
410 }
411
412 fn create_dhcp_ack() -> Vec<u8> {
414 let mut packet = create_dhcp_offer();
415 packet[242] = message_type::ACK;
417 packet
418 }
419
420 #[test]
421 fn test_can_parse_dhcp() {
422 let parser = DhcpProtocol;
423
424 let ctx1 = ParseContext::new(1);
426 assert!(parser.can_parse(&ctx1).is_none());
427
428 let mut ctx2 = ParseContext::new(1);
430 ctx2.insert_hint("dst_port", 67);
431 assert!(parser.can_parse(&ctx2).is_some());
432
433 let mut ctx3 = ParseContext::new(1);
435 ctx3.insert_hint("src_port", 68);
436 assert!(parser.can_parse(&ctx3).is_some());
437
438 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]; 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}