Skip to main content

stackforge_core/layer/dhcp/
builder.rs

1use std::net::Ipv4Addr;
2
3use crate::layer::dhcp::options::{DhcpOption, code, msg_type, serialize_options};
4use crate::layer::field::MacAddress;
5
6/// DHCP magic cookie: 99.130.83.99
7const MAGIC_COOKIE: [u8; 4] = [99, 130, 83, 99];
8
9/// BOOTP op codes.
10const BOOTREQUEST: u8 = 1;
11const BOOTREPLY: u8 = 2;
12
13/// Builder for constructing DHCP packets.
14pub struct DhcpBuilder {
15    op: u8,
16    htype: u8,
17    hlen: u8,
18    hops: u8,
19    xid: u32,
20    secs: u16,
21    flags: u16,
22    ciaddr: Ipv4Addr,
23    yiaddr: Ipv4Addr,
24    siaddr: Ipv4Addr,
25    giaddr: Ipv4Addr,
26    chaddr: [u8; 16],
27    sname: [u8; 64],
28    file: [u8; 128],
29    options: Vec<DhcpOption>,
30}
31
32impl Default for DhcpBuilder {
33    fn default() -> Self {
34        Self {
35            op: BOOTREQUEST,
36            htype: 1, // Ethernet
37            hlen: 6,
38            hops: 0,
39            xid: 0,
40            secs: 0,
41            flags: 0,
42            ciaddr: Ipv4Addr::UNSPECIFIED,
43            yiaddr: Ipv4Addr::UNSPECIFIED,
44            siaddr: Ipv4Addr::UNSPECIFIED,
45            giaddr: Ipv4Addr::UNSPECIFIED,
46            chaddr: [0u8; 16],
47            sname: [0u8; 64],
48            file: [0u8; 128],
49            options: Vec::new(),
50        }
51    }
52}
53
54impl DhcpBuilder {
55    #[must_use]
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Create a DHCP Discover message.
61    #[must_use]
62    pub fn discover(client_mac: MacAddress, xid: u32) -> Self {
63        let mut b = Self::new()
64            .op(BOOTREQUEST)
65            .xid(xid)
66            .chaddr_mac(client_mac)
67            .flags(0x8000); // Broadcast flag
68        b.options.push(DhcpOption::message_type(msg_type::DISCOVER));
69        b
70    }
71
72    /// Create a DHCP Offer message.
73    #[must_use]
74    pub fn offer(
75        xid: u32,
76        client_mac: MacAddress,
77        offered_ip: Ipv4Addr,
78        server_ip: Ipv4Addr,
79    ) -> Self {
80        let mut b = Self::new()
81            .op(BOOTREPLY)
82            .xid(xid)
83            .yiaddr(offered_ip)
84            .siaddr(server_ip)
85            .chaddr_mac(client_mac);
86        b.options.push(DhcpOption::message_type(msg_type::OFFER));
87        b.options.push(DhcpOption::server_id(server_ip));
88        b
89    }
90
91    /// Create a DHCP Request message.
92    #[must_use]
93    pub fn request(
94        client_mac: MacAddress,
95        xid: u32,
96        requested_ip: Ipv4Addr,
97        server_ip: Ipv4Addr,
98    ) -> Self {
99        let mut b = Self::new()
100            .op(BOOTREQUEST)
101            .xid(xid)
102            .chaddr_mac(client_mac)
103            .flags(0x8000);
104        b.options.push(DhcpOption::message_type(msg_type::REQUEST));
105        b.options.push(DhcpOption::new(
106            code::REQUESTED_IP,
107            requested_ip.octets().to_vec(),
108        ));
109        b.options.push(DhcpOption::server_id(server_ip));
110        b
111    }
112
113    /// Create a DHCP ACK message.
114    #[must_use]
115    pub fn ack(
116        xid: u32,
117        client_mac: MacAddress,
118        assigned_ip: Ipv4Addr,
119        server_ip: Ipv4Addr,
120    ) -> Self {
121        let mut b = Self::new()
122            .op(BOOTREPLY)
123            .xid(xid)
124            .yiaddr(assigned_ip)
125            .siaddr(server_ip)
126            .chaddr_mac(client_mac);
127        b.options.push(DhcpOption::message_type(msg_type::ACK));
128        b.options.push(DhcpOption::server_id(server_ip));
129        b
130    }
131
132    /// Create a DHCP NAK message.
133    #[must_use]
134    pub fn nak(xid: u32, client_mac: MacAddress, server_ip: Ipv4Addr) -> Self {
135        let mut b = Self::new().op(BOOTREPLY).xid(xid).chaddr_mac(client_mac);
136        b.options.push(DhcpOption::message_type(msg_type::NAK));
137        b.options.push(DhcpOption::server_id(server_ip));
138        b
139    }
140
141    #[must_use]
142    pub fn op(mut self, op: u8) -> Self {
143        self.op = op;
144        self
145    }
146
147    #[must_use]
148    pub fn xid(mut self, xid: u32) -> Self {
149        self.xid = xid;
150        self
151    }
152
153    #[must_use]
154    pub fn flags(mut self, flags: u16) -> Self {
155        self.flags = flags;
156        self
157    }
158
159    #[must_use]
160    pub fn ciaddr(mut self, ip: Ipv4Addr) -> Self {
161        self.ciaddr = ip;
162        self
163    }
164
165    #[must_use]
166    pub fn yiaddr(mut self, ip: Ipv4Addr) -> Self {
167        self.yiaddr = ip;
168        self
169    }
170
171    #[must_use]
172    pub fn siaddr(mut self, ip: Ipv4Addr) -> Self {
173        self.siaddr = ip;
174        self
175    }
176
177    #[must_use]
178    pub fn giaddr(mut self, ip: Ipv4Addr) -> Self {
179        self.giaddr = ip;
180        self
181    }
182
183    #[must_use]
184    pub fn chaddr_mac(mut self, mac: MacAddress) -> Self {
185        self.chaddr[0..6].copy_from_slice(&mac.0);
186        self
187    }
188
189    /// Add a DHCP option.
190    #[must_use]
191    pub fn option(mut self, opt: DhcpOption) -> Self {
192        self.options.push(opt);
193        self
194    }
195
196    /// Add a lease time option.
197    #[must_use]
198    pub fn lease_time(self, seconds: u32) -> Self {
199        self.option(DhcpOption::lease_time(seconds))
200    }
201
202    /// Add a subnet mask option.
203    #[must_use]
204    pub fn subnet_mask(self, mask: Ipv4Addr) -> Self {
205        self.option(DhcpOption::subnet_mask(mask))
206    }
207
208    /// Add a router option.
209    #[must_use]
210    pub fn router(self, ip: Ipv4Addr) -> Self {
211        self.option(DhcpOption::router(ip))
212    }
213
214    /// Add a DNS servers option.
215    #[must_use]
216    pub fn dns(self, servers: &[Ipv4Addr]) -> Self {
217        self.option(DhcpOption::dns(servers))
218    }
219
220    /// Add a domain name option.
221    #[must_use]
222    pub fn domain_name(self, name: &str) -> Self {
223        self.option(DhcpOption::domain_name(name))
224    }
225
226    /// Build the DHCP packet bytes (BOOTP header + options).
227    ///
228    /// This produces the UDP payload — the caller must wrap it in
229    /// UDP(sport=67, dport=68) / IP / Ethernet.
230    #[must_use]
231    pub fn build(&self) -> Vec<u8> {
232        let opts_bytes = serialize_options(&self.options);
233        let mut out = Vec::with_capacity(240 + opts_bytes.len());
234
235        // BOOTP fixed header (236 bytes)
236        out.push(self.op);
237        out.push(self.htype);
238        out.push(self.hlen);
239        out.push(self.hops);
240        out.extend_from_slice(&self.xid.to_be_bytes());
241        out.extend_from_slice(&self.secs.to_be_bytes());
242        out.extend_from_slice(&self.flags.to_be_bytes());
243        out.extend_from_slice(&self.ciaddr.octets());
244        out.extend_from_slice(&self.yiaddr.octets());
245        out.extend_from_slice(&self.siaddr.octets());
246        out.extend_from_slice(&self.giaddr.octets());
247        out.extend_from_slice(&self.chaddr);
248        out.extend_from_slice(&self.sname);
249        out.extend_from_slice(&self.file);
250
251        // Magic cookie
252        out.extend_from_slice(&MAGIC_COOKIE);
253
254        // Options
255        out.extend_from_slice(&opts_bytes);
256
257        out
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_discover_build() {
267        let mac = MacAddress::new([0x00, 0x11, 0x22, 0x33, 0x44, 0x55]);
268        let pkt = DhcpBuilder::discover(mac, 0x12345678).build();
269
270        assert_eq!(pkt[0], BOOTREQUEST); // op
271        assert_eq!(pkt[1], 1); // htype (ethernet)
272        assert_eq!(pkt[2], 6); // hlen
273        // xid
274        assert_eq!(&pkt[4..8], &[0x12, 0x34, 0x56, 0x78]);
275        // flags (broadcast)
276        assert_eq!(&pkt[10..12], &[0x80, 0x00]);
277        // chaddr
278        assert_eq!(&pkt[28..34], &[0x00, 0x11, 0x22, 0x33, 0x44, 0x55]);
279        // magic cookie at offset 236
280        assert_eq!(&pkt[236..240], &[99, 130, 83, 99]);
281        // First option should be message type = DISCOVER
282        assert_eq!(pkt[240], code::MESSAGE_TYPE);
283        assert_eq!(pkt[241], 1); // length
284        assert_eq!(pkt[242], msg_type::DISCOVER);
285    }
286
287    #[test]
288    fn test_offer_build() {
289        let mac = MacAddress::new([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
290        let pkt = DhcpBuilder::offer(
291            0xaabbccdd,
292            mac,
293            Ipv4Addr::new(192, 168, 1, 100),
294            Ipv4Addr::new(192, 168, 1, 1),
295        )
296        .lease_time(3600)
297        .subnet_mask(Ipv4Addr::new(255, 255, 255, 0))
298        .build();
299
300        assert_eq!(pkt[0], BOOTREPLY);
301        // yiaddr = offered IP
302        assert_eq!(&pkt[16..20], &[192, 168, 1, 100]);
303        // siaddr = server IP
304        assert_eq!(&pkt[20..24], &[192, 168, 1, 1]);
305    }
306
307    #[test]
308    fn test_ack_build() {
309        let mac = MacAddress::new([0x00, 0x11, 0x22, 0x33, 0x44, 0x55]);
310        let pkt = DhcpBuilder::ack(
311            0x11223344,
312            mac,
313            Ipv4Addr::new(10, 0, 0, 50),
314            Ipv4Addr::new(10, 0, 0, 1),
315        )
316        .build();
317
318        assert_eq!(pkt[0], BOOTREPLY);
319        assert_eq!(&pkt[16..20], &[10, 0, 0, 50]); // yiaddr
320    }
321}