Skip to main content

stackforge_core/layer/dhcp/
mod.rs

1pub mod builder;
2pub mod options;
3
4use std::net::Ipv4Addr;
5
6use crate::layer::field::{FieldError, FieldValue, MacAddress};
7use crate::layer::{Layer, LayerIndex, LayerKind};
8
9use self::options::{DhcpOption, code, parse_options};
10
11pub use builder::DhcpBuilder;
12
13/// DHCP magic cookie bytes.
14const MAGIC_COOKIE: [u8; 4] = [99, 130, 83, 99];
15
16/// Minimum DHCP packet size: 240 (BOOTP header) + 4 (magic cookie) + 3 (minimum option).
17pub const DHCP_MIN_HEADER_LEN: usize = 240;
18
19/// DHCP uses UDP ports 67 (server) and 68 (client).
20pub const DHCP_SERVER_PORT: u16 = 67;
21pub const DHCP_CLIENT_PORT: u16 = 68;
22
23/// Field names for Python field access.
24pub const DHCP_FIELD_NAMES: &[&str] = &[
25    "op",
26    "htype",
27    "hlen",
28    "hops",
29    "xid",
30    "secs",
31    "flags",
32    "ciaddr",
33    "yiaddr",
34    "siaddr",
35    "giaddr",
36    "chaddr",
37    "msg_type",
38    "server_id",
39    "requested_ip",
40    "lease_time",
41    "subnet_mask",
42    "router",
43    "dns",
44];
45
46fn short(need: usize, have: usize) -> FieldError {
47    FieldError::BufferTooShort {
48        offset: 0,
49        need,
50        have,
51    }
52}
53
54/// DHCP layer — a zero-copy view into a parsed DHCP packet.
55#[derive(Debug, Clone)]
56pub struct DhcpLayer {
57    pub index: LayerIndex,
58}
59
60impl DhcpLayer {
61    #[must_use]
62    pub fn new(index: LayerIndex) -> Self {
63        Self { index }
64    }
65
66    fn data<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
67        &buf[self.index.start..self.index.end]
68    }
69
70    fn check<'a>(&self, buf: &'a [u8], need: usize) -> Result<&'a [u8], FieldError> {
71        let d = self.data(buf);
72        if d.len() < need {
73            Err(short(need, d.len()))
74        } else {
75            Ok(d)
76        }
77    }
78
79    /// BOOTP op code: 1 = request, 2 = reply.
80    pub fn op(&self, buf: &[u8]) -> Result<u8, FieldError> {
81        Ok(self.check(buf, 1)?[0])
82    }
83
84    /// Hardware type (1 = Ethernet).
85    pub fn htype(&self, buf: &[u8]) -> Result<u8, FieldError> {
86        Ok(self.check(buf, 2)?[1])
87    }
88
89    /// Hardware address length.
90    pub fn hlen(&self, buf: &[u8]) -> Result<u8, FieldError> {
91        Ok(self.check(buf, 3)?[2])
92    }
93
94    /// Hops.
95    pub fn hops(&self, buf: &[u8]) -> Result<u8, FieldError> {
96        Ok(self.check(buf, 4)?[3])
97    }
98
99    /// Transaction ID.
100    pub fn xid(&self, buf: &[u8]) -> Result<u32, FieldError> {
101        let d = self.check(buf, 8)?;
102        Ok(u32::from_be_bytes([d[4], d[5], d[6], d[7]]))
103    }
104
105    /// Seconds elapsed.
106    pub fn secs(&self, buf: &[u8]) -> Result<u16, FieldError> {
107        let d = self.check(buf, 10)?;
108        Ok(u16::from_be_bytes([d[8], d[9]]))
109    }
110
111    /// Flags.
112    pub fn flags(&self, buf: &[u8]) -> Result<u16, FieldError> {
113        let d = self.check(buf, 12)?;
114        Ok(u16::from_be_bytes([d[10], d[11]]))
115    }
116
117    /// Client IP address.
118    pub fn ciaddr(&self, buf: &[u8]) -> Result<Ipv4Addr, FieldError> {
119        let d = self.check(buf, 16)?;
120        Ok(Ipv4Addr::new(d[12], d[13], d[14], d[15]))
121    }
122
123    /// Your (client) IP address.
124    pub fn yiaddr(&self, buf: &[u8]) -> Result<Ipv4Addr, FieldError> {
125        let d = self.check(buf, 20)?;
126        Ok(Ipv4Addr::new(d[16], d[17], d[18], d[19]))
127    }
128
129    /// Server IP address.
130    pub fn siaddr(&self, buf: &[u8]) -> Result<Ipv4Addr, FieldError> {
131        let d = self.check(buf, 24)?;
132        Ok(Ipv4Addr::new(d[20], d[21], d[22], d[23]))
133    }
134
135    /// Gateway IP address.
136    pub fn giaddr(&self, buf: &[u8]) -> Result<Ipv4Addr, FieldError> {
137        let d = self.check(buf, 28)?;
138        Ok(Ipv4Addr::new(d[24], d[25], d[26], d[27]))
139    }
140
141    /// Client hardware address (first 6 bytes for Ethernet).
142    pub fn chaddr(&self, buf: &[u8]) -> Result<[u8; 6], FieldError> {
143        let d = self.check(buf, 34)?;
144        let mut mac = [0u8; 6];
145        mac.copy_from_slice(&d[28..34]);
146        Ok(mac)
147    }
148
149    /// Parse all DHCP options.
150    pub fn options(&self, buf: &[u8]) -> Vec<DhcpOption> {
151        let d = self.data(buf);
152        if d.len() < 240 {
153            return Vec::new();
154        }
155        if d[236..240] != MAGIC_COOKIE {
156            return Vec::new();
157        }
158        parse_options(&d[240..])
159    }
160
161    /// Get a specific option by code.
162    pub fn get_option(&self, buf: &[u8], opt_code: u8) -> Option<DhcpOption> {
163        self.options(buf).into_iter().find(|o| o.code == opt_code)
164    }
165
166    /// Get the DHCP message type.
167    pub fn msg_type(&self, buf: &[u8]) -> Option<u8> {
168        self.get_option(buf, code::MESSAGE_TYPE)
169            .and_then(|o| o.as_message_type())
170    }
171
172    /// Get the server identifier.
173    pub fn server_id(&self, buf: &[u8]) -> Option<Ipv4Addr> {
174        self.get_option(buf, code::SERVER_ID)
175            .and_then(|o| o.as_ipv4())
176    }
177
178    /// Get the requested IP address.
179    pub fn requested_ip(&self, buf: &[u8]) -> Option<Ipv4Addr> {
180        self.get_option(buf, code::REQUESTED_IP)
181            .and_then(|o| o.as_ipv4())
182    }
183
184    /// Get the lease time option value.
185    pub fn lease_time(&self, buf: &[u8]) -> Option<u32> {
186        self.get_option(buf, code::LEASE_TIME).and_then(|o| {
187            if o.data.len() >= 4 {
188                Some(u32::from_be_bytes([
189                    o.data[0], o.data[1], o.data[2], o.data[3],
190                ]))
191            } else {
192                None
193            }
194        })
195    }
196
197    /// Get the subnet mask option.
198    pub fn subnet_mask(&self, buf: &[u8]) -> Option<Ipv4Addr> {
199        self.get_option(buf, code::SUBNET_MASK)
200            .and_then(|o| o.as_ipv4())
201    }
202
203    /// Get the router option.
204    pub fn router(&self, buf: &[u8]) -> Option<Ipv4Addr> {
205        self.get_option(buf, code::ROUTER).and_then(|o| o.as_ipv4())
206    }
207
208    /// Get the DNS servers option.
209    pub fn dns(&self, buf: &[u8]) -> Vec<Ipv4Addr> {
210        self.get_option(buf, code::DNS)
211            .map(|o| {
212                o.data
213                    .chunks_exact(4)
214                    .map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3]))
215                    .collect()
216            })
217            .unwrap_or_default()
218    }
219
220    /// Check if this is a DHCP request (BOOTP op=1).
221    pub fn is_request(&self, buf: &[u8]) -> bool {
222        self.op(buf).is_ok_and(|op| op == 1)
223    }
224
225    /// Check if this is a DHCP reply (BOOTP op=2).
226    pub fn is_reply(&self, buf: &[u8]) -> bool {
227        self.op(buf).is_ok_and(|op| op == 2)
228    }
229
230    /// Set the op field.
231    pub fn set_op(&self, buf: &mut [u8], val: u8) -> Result<(), FieldError> {
232        let start = self.index.start;
233        let d = &mut buf[start..self.index.end];
234        if d.is_empty() {
235            return Err(short(1, 0));
236        }
237        d[0] = val;
238        Ok(())
239    }
240
241    /// Set the xid field.
242    pub fn set_xid(&self, buf: &mut [u8], val: u32) -> Result<(), FieldError> {
243        let start = self.index.start;
244        let d = &mut buf[start..self.index.end];
245        if d.len() < 8 {
246            return Err(short(8, d.len()));
247        }
248        d[4..8].copy_from_slice(&val.to_be_bytes());
249        Ok(())
250    }
251
252    /// Set the flags field.
253    pub fn set_flags(&self, buf: &mut [u8], val: u16) -> Result<(), FieldError> {
254        let start = self.index.start;
255        let d = &mut buf[start..self.index.end];
256        if d.len() < 12 {
257            return Err(short(12, d.len()));
258        }
259        d[10..12].copy_from_slice(&val.to_be_bytes());
260        Ok(())
261    }
262
263    /// Get field value by name.
264    pub fn get_field(&self, buf: &[u8], name: &str) -> Option<Result<FieldValue, FieldError>> {
265        match name {
266            "op" => Some(self.op(buf).map(|v| FieldValue::U8(v))),
267            "htype" => Some(self.htype(buf).map(|v| FieldValue::U8(v))),
268            "hlen" => Some(self.hlen(buf).map(|v| FieldValue::U8(v))),
269            "hops" => Some(self.hops(buf).map(|v| FieldValue::U8(v))),
270            "xid" => Some(self.xid(buf).map(|v| FieldValue::U32(v))),
271            "secs" => Some(self.secs(buf).map(|v| FieldValue::U16(v))),
272            "flags" => Some(self.flags(buf).map(|v| FieldValue::U16(v))),
273            "ciaddr" => Some(self.ciaddr(buf).map(|v| FieldValue::Ipv4(v))),
274            "yiaddr" => Some(self.yiaddr(buf).map(|v| FieldValue::Ipv4(v))),
275            "siaddr" => Some(self.siaddr(buf).map(|v| FieldValue::Ipv4(v))),
276            "giaddr" => Some(self.giaddr(buf).map(|v| FieldValue::Ipv4(v))),
277            "chaddr" => Some(
278                self.chaddr(buf)
279                    .map(|mac| FieldValue::Mac(MacAddress::new(mac))),
280            ),
281            "msg_type" => Some(Ok(match self.msg_type(buf) {
282                Some(v) => FieldValue::U8(v),
283                None => FieldValue::U8(0),
284            })),
285            "server_id" => Some(Ok(match self.server_id(buf) {
286                Some(v) => FieldValue::Ipv4(v),
287                None => FieldValue::Ipv4(Ipv4Addr::UNSPECIFIED),
288            })),
289            "requested_ip" => Some(Ok(match self.requested_ip(buf) {
290                Some(v) => FieldValue::Ipv4(v),
291                None => FieldValue::Ipv4(Ipv4Addr::UNSPECIFIED),
292            })),
293            "lease_time" => Some(Ok(FieldValue::U32(self.lease_time(buf).unwrap_or(0)))),
294            "subnet_mask" => Some(Ok(match self.subnet_mask(buf) {
295                Some(v) => FieldValue::Ipv4(v),
296                None => FieldValue::Ipv4(Ipv4Addr::UNSPECIFIED),
297            })),
298            "router" => Some(Ok(match self.router(buf) {
299                Some(v) => FieldValue::Ipv4(v),
300                None => FieldValue::Ipv4(Ipv4Addr::UNSPECIFIED),
301            })),
302            "dns" => {
303                let servers = self.dns(buf);
304                if servers.is_empty() {
305                    Some(Ok(FieldValue::Str(String::new())))
306                } else {
307                    let s = servers
308                        .iter()
309                        .map(|ip| ip.to_string())
310                        .collect::<Vec<_>>()
311                        .join(",");
312                    Some(Ok(FieldValue::Str(s)))
313                }
314            },
315            _ => None,
316        }
317    }
318
319    /// Set field value by name.
320    pub fn set_field(
321        &self,
322        buf: &mut [u8],
323        name: &str,
324        value: FieldValue,
325    ) -> Option<Result<(), FieldError>> {
326        match name {
327            "op" => {
328                if let FieldValue::U8(v) = value {
329                    Some(self.set_op(buf, v))
330                } else {
331                    Some(Err(FieldError::InvalidValue(format!(
332                        "op: expected U8, got {value:?}"
333                    ))))
334                }
335            },
336            "xid" => {
337                if let FieldValue::U32(v) = value {
338                    Some(self.set_xid(buf, v))
339                } else {
340                    Some(Err(FieldError::InvalidValue(format!(
341                        "xid: expected U32, got {value:?}"
342                    ))))
343                }
344            },
345            "flags" => {
346                if let FieldValue::U16(v) = value {
347                    Some(self.set_flags(buf, v))
348                } else {
349                    Some(Err(FieldError::InvalidValue(format!(
350                        "flags: expected U16, got {value:?}"
351                    ))))
352                }
353            },
354            _ => None,
355        }
356    }
357
358    /// Field names for this layer.
359    pub fn field_names(&self) -> &'static [&'static str] {
360        DHCP_FIELD_NAMES
361    }
362}
363
364impl Layer for DhcpLayer {
365    fn kind(&self) -> LayerKind {
366        LayerKind::Dhcp
367    }
368
369    fn summary(&self, buf: &[u8]) -> String {
370        let msg = match self.msg_type(buf) {
371            Some(1) => "Discover",
372            Some(2) => "Offer",
373            Some(3) => "Request",
374            Some(4) => "Decline",
375            Some(5) => "ACK",
376            Some(6) => "NAK",
377            Some(7) => "Release",
378            Some(8) => "Inform",
379            _ => "Unknown",
380        };
381        format!("DHCP {msg}")
382    }
383
384    fn header_len(&self, buf: &[u8]) -> usize {
385        let d = self.data(buf);
386        d.len()
387    }
388
389    fn field_names(&self) -> &'static [&'static str] {
390        DHCP_FIELD_NAMES
391    }
392}
393
394/// Check if a UDP payload looks like a DHCP packet.
395#[must_use]
396pub fn is_dhcp_payload(data: &[u8]) -> bool {
397    if data.len() < 240 {
398        return false;
399    }
400    data[236..240] == MAGIC_COOKIE
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use crate::layer::field::MacAddress;
407
408    #[test]
409    fn test_parse_discover() {
410        let mac = MacAddress::new([0x00, 0x11, 0x22, 0x33, 0x44, 0x55]);
411        let data = DhcpBuilder::discover(mac, 0xaabbccdd).build();
412
413        let layer = DhcpLayer::new(LayerIndex {
414            kind: LayerKind::Dhcp,
415            start: 0,
416            end: data.len(),
417        });
418
419        assert_eq!(layer.op(&data).unwrap(), 1);
420        assert_eq!(layer.xid(&data).unwrap(), 0xaabbccdd);
421        assert_eq!(
422            layer.chaddr(&data).unwrap(),
423            [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]
424        );
425        assert_eq!(layer.msg_type(&data), Some(options::msg_type::DISCOVER));
426        assert_eq!(layer.summary(&data), "DHCP Discover");
427    }
428
429    #[test]
430    fn test_parse_offer() {
431        let mac = MacAddress::new([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
432        let data = DhcpBuilder::offer(
433            0x12345678,
434            mac,
435            Ipv4Addr::new(192, 168, 1, 100),
436            Ipv4Addr::new(192, 168, 1, 1),
437        )
438        .lease_time(7200)
439        .subnet_mask(Ipv4Addr::new(255, 255, 255, 0))
440        .router(Ipv4Addr::new(192, 168, 1, 1))
441        .dns(&[Ipv4Addr::new(8, 8, 8, 8)])
442        .build();
443
444        let layer = DhcpLayer::new(LayerIndex {
445            kind: LayerKind::Dhcp,
446            start: 0,
447            end: data.len(),
448        });
449
450        assert_eq!(layer.op(&data).unwrap(), 2);
451        assert_eq!(
452            layer.yiaddr(&data).unwrap(),
453            Ipv4Addr::new(192, 168, 1, 100)
454        );
455        assert_eq!(layer.siaddr(&data).unwrap(), Ipv4Addr::new(192, 168, 1, 1));
456        assert_eq!(layer.msg_type(&data), Some(options::msg_type::OFFER));
457        assert_eq!(layer.server_id(&data), Some(Ipv4Addr::new(192, 168, 1, 1)));
458        assert_eq!(layer.summary(&data), "DHCP Offer");
459    }
460
461    #[test]
462    fn test_is_dhcp_payload() {
463        let mac = MacAddress::new([0x00; 6]);
464        let data = DhcpBuilder::discover(mac, 1).build();
465        assert!(is_dhcp_payload(&data));
466        assert!(!is_dhcp_payload(&[0u8; 10]));
467    }
468}