wireforge_app/dhcp/
parser.rs1use 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#[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}