Skip to main content

wireforge_app/dhcp/
parser.rs

1//! DHCPv4 message parser (RFC 2131/2132).
2
3use core::net::Ipv4Addr;
4
5use super::types::{DhcpMessageType, DhcpOption};
6use wireforge_core::util::{read_u16be, read_u32be};
7
8const DHCP_MAGIC_COOKIE: u32 = 0x63825363;
9pub const DHCP_MIN_LEN: usize = 240;
10
11/// Zero-copy DHCP packet parser.
12#[derive(Debug, Clone)]
13pub struct DhcpPacket<'a> {
14    buf: &'a [u8],
15}
16
17impl<'a> DhcpPacket<'a> {
18    pub fn new(buf: &'a [u8]) -> Option<Self> {
19        if buf.len() < DHCP_MIN_LEN { return None; }
20        if read_u32be(&buf[236..240]) != DHCP_MAGIC_COOKIE { return None; }
21        Some(Self { buf })
22    }
23
24    #[inline] pub fn op(&self) -> u8 { self.buf[0] }
25    #[inline] pub fn htype(&self) -> u8 { self.buf[1] }
26    #[inline] pub fn hlen(&self) -> u8 { self.buf[2] }
27    #[inline] pub fn hops(&self) -> u8 { self.buf[3] }
28    #[inline] pub fn xid(&self) -> u32 { read_u32be(&self.buf[4..8]) }
29    #[inline] pub fn secs(&self) -> u16 { read_u16be(&self.buf[8..10]) }
30    #[inline] pub fn flags(&self) -> u16 { read_u16be(&self.buf[10..12]) }
31    #[inline] pub fn broadcast(&self) -> bool { self.flags() & 0x8000 != 0 }
32    #[inline] pub fn ciaddr(&self) -> Ipv4Addr { ip4(&self.buf[12..16]) }
33    #[inline] pub fn yiaddr(&self) -> Ipv4Addr { ip4(&self.buf[16..20]) }
34    #[inline] pub fn siaddr(&self) -> Ipv4Addr { ip4(&self.buf[20..24]) }
35    #[inline] pub fn giaddr(&self) -> Ipv4Addr { ip4(&self.buf[24..28]) }
36    #[inline] pub fn chaddr(&self) -> &[u8] { &self.buf[28..44] }
37    #[inline] pub fn sname(&self) -> &[u8] { &self.buf[44..108] }
38    #[inline] pub fn file(&self) -> &[u8] { &self.buf[108..236] }
39    #[inline] pub fn magic_cookie(&self) -> u32 { read_u32be(&self.buf[236..240]) }
40
41    pub fn options(&self) -> DhcpOptionIter<'a> {
42        let buf = self.buf;
43        DhcpOptionIter {
44            buf,
45            pos: 240,
46            file_buf: &buf[108..236],
47            sname_buf: &buf[44..108],
48            overload: self._find_overload(),
49            phase: 0,
50        }
51    }
52
53    fn _find_overload(&self) -> u8 {
54        let mut pos = 240usize;
55        while pos + 2 <= self.buf.len() {
56            let code = self.buf[pos];
57            if code == 0xff { break; }
58            if code == 0 { pos += 1; continue; }
59            if pos + 1 >= self.buf.len() { break; }
60            let len = self.buf[pos + 1] as usize;
61            if code == 52 && len >= 1 && pos + 2 < self.buf.len() {
62                return self.buf[pos + 2];
63            }
64            pos += 2 + len;
65        }
66        0
67    }
68
69    pub fn message_type(&self) -> Option<DhcpMessageType> {
70        for opt in self.options() {
71            if let DhcpOption::MessageType(mt) = opt { return Some(mt); }
72            if matches!(opt, DhcpOption::End) { break; }
73        }
74        None
75    }
76}
77
78fn ip4(b: &[u8]) -> Ipv4Addr { Ipv4Addr::new(b[0], b[1], b[2], b[3]) }
79
80pub struct DhcpOptionIter<'a> {
81    buf: &'a [u8],
82    pos: usize,
83    file_buf: &'a [u8],
84    sname_buf: &'a [u8],
85    overload: u8,
86    phase: u8,
87}
88
89impl<'a> Iterator for DhcpOptionIter<'a> {
90    type Item = DhcpOption;
91
92    fn next(&mut self) -> Option<Self::Item> {
93        loop {
94            let data = match self.phase {
95                0 => {
96                    if self.pos >= self.buf.len() { self.phase = 1; self.pos = 0; continue; }
97                    &self.buf[self.pos..]
98                }
99                1 => {
100                    if self.overload & 1 == 0 || self.pos >= self.file_buf.len() { self.phase = 2; self.pos = 0; continue; }
101                    &self.file_buf[self.pos..]
102                }
103                2 => {
104                    if self.overload & 2 == 0 || self.pos >= self.sname_buf.len() { return None; }
105                    &self.sname_buf[self.pos..]
106                }
107                _ => return None,
108            };
109            if data.is_empty() { return None; }
110            let code = data[0];
111            if code == 0xff { return None; }
112            if code == 0 { self.pos += 1; continue; }
113            if data.len() < 2 { return None; }
114            let len = data[1] as usize;
115            if data.len() < 2 + len { self.pos += 2 + data.len().saturating_sub(2); continue; }
116            let val = &data[2..2 + len];
117            let opt = parse_option(code, val);
118            self.pos += 2 + len;
119            return Some(opt);
120        }
121    }
122}
123
124fn parse_option(code: u8, data: &[u8]) -> DhcpOption {
125    match code {
126        1 => DhcpOption::SubnetMask(ip4(&data[..4.min(data.len())])),
127        3 => DhcpOption::Router(parse_ip_list(data)),
128        6 => DhcpOption::DnsServer(parse_ip_list(data)),
129        12 => DhcpOption::HostName(String::from_utf8_lossy(data).into_owned()),
130        15 => DhcpOption::DomainName(String::from_utf8_lossy(data).into_owned()),
131        28 => DhcpOption::BroadcastAddr(ip4(&data[..4.min(data.len())])),
132        50 => DhcpOption::RequestedIp(ip4(&data[..4.min(data.len())])),
133        51 => DhcpOption::LeaseTime(read_u32be(&data[..4.min(data.len())])),
134        53 => DhcpOption::MessageType(DhcpMessageType::from(data.first().copied().unwrap_or(0))),
135        54 => DhcpOption::ServerId(ip4(&data[..4.min(data.len())])),
136        55 => DhcpOption::ParameterList(data.to_vec()),
137        58 => DhcpOption::RenewalTime(read_u32be(&data[..4.min(data.len())])),
138        59 => DhcpOption::RebindingTime(read_u32be(&data[..4.min(data.len())])),
139        _ => DhcpOption::Unknown { code, data: data.to_vec() },
140    }
141}
142
143fn parse_ip_list(data: &[u8]) -> Vec<Ipv4Addr> {
144    data.chunks(4).filter(|c| c.len() == 4).map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3])).collect()
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::dhcp::builder::DhcpPacketBuilder;
151
152    #[test]
153    fn parse_dhcp_discover() {
154        let data = DhcpPacketBuilder::discover()
155            .xid(0x12345678)
156            .chaddr(&[0x00, 0x11, 0x22, 0x33, 0x44, 0x55])
157            .build();
158
159        let pkt = DhcpPacket::new(&data).unwrap();
160        assert_eq!(pkt.op(), 1);
161        assert_eq!(pkt.xid(), 0x12345678);
162        assert_eq!(pkt.magic_cookie(), 0x63825363);
163        assert_eq!(pkt.message_type(), Some(DhcpMessageType::Discover));
164    }
165
166    #[test]
167    fn parse_dhcp_too_short() {
168        assert!(DhcpPacket::new(&[]).is_none());
169        assert!(DhcpPacket::new(&[0u8; 239]).is_none());
170    }
171
172    #[test]
173    fn parse_dhcp_bad_cookie() {
174        let mut data = vec![0u8; 240];
175        data[236..240].copy_from_slice(&0xDEADBEEFu32.to_be_bytes());
176        assert!(DhcpPacket::new(&data).is_none());
177    }
178
179    #[test]
180    fn dhcp_offer_with_options() {
181        let server_ip = [192u8, 168, 1, 1];
182        let client_ip = [192u8, 168, 1, 100];
183        let data = DhcpPacketBuilder::offer()
184            .xid(0x42)
185            .yiaddr(client_ip)
186            .siaddr(server_ip)
187            .add_option(&DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)))
188            .add_option(&DhcpOption::LeaseTime(86400))
189            .add_option(&DhcpOption::ServerId(Ipv4Addr::new(192, 168, 1, 1)))
190            .build();
191
192        let pkt = DhcpPacket::new(&data).unwrap();
193        assert_eq!(pkt.op(), 2);
194        assert_eq!(pkt.message_type(), Some(DhcpMessageType::Offer));
195        assert_eq!(pkt.yiaddr(), Ipv4Addr::new(192, 168, 1, 100));
196
197        let opts: Vec<_> = pkt.options().collect();
198        assert!(opts.iter().any(|o| matches!(o, DhcpOption::SubnetMask(_))));
199        assert!(opts.iter().any(|o| matches!(o, DhcpOption::LeaseTime(86400))));
200    }
201}