1use embassy_net::udp::{PacketMetadata, UdpSocket};
16use embassy_net::{IpAddress, Ipv4Address, Stack};
17use embassy_time::{Duration, Instant};
18
19use super::AP_IP;
20
21const SERVER_PORT: u16 = 67;
23const CLIENT_PORT: u16 = 68;
25
26const POOL_START: u8 = 10;
29const POOL_SIZE: usize = 8;
32const LEASE_SECS: u64 = 3600;
34const LEASE: Duration = Duration::from_secs(LEASE_SECS);
36const NETMASK: [u8; 4] = [255, 255, 255, 0];
39
40const BOOTREQUEST: u8 = 1;
42const BOOTREPLY: u8 = 2;
44const HTYPE_ETHERNET: u8 = 1;
46const HLEN_ETHERNET: u8 = 6;
48const MAGIC_COOKIE: u32 = 0x6382_5363;
50const FLAG_BROADCAST: u16 = 0x8000;
52
53const OPT_PAD: u8 = 0;
55const OPT_SUBNET_MASK: u8 = 1;
57const OPT_ROUTER: u8 = 3;
59const OPT_DNS: u8 = 6;
61const OPT_REQUESTED_IP: u8 = 50;
63const OPT_LEASE_TIME: u8 = 51;
65const OPT_MESSAGE_TYPE: u8 = 53;
67const OPT_SERVER_ID: u8 = 54;
69const OPT_END: u8 = 255;
71
72const MSG_DISCOVER: u8 = 1;
74const MSG_OFFER: u8 = 2;
76const MSG_REQUEST: u8 = 3;
78const MSG_DECLINE: u8 = 4;
80const MSG_ACK: u8 = 5;
82const MSG_NAK: u8 = 6;
84const MSG_RELEASE: u8 = 7;
86const MSG_INFORM: u8 = 8;
88
89struct Reader<'a> {
91 buf: &'a [u8],
92}
93
94impl<'a> Reader<'a> {
95 fn new(buf: &'a [u8]) -> Self {
97 Self { buf }
98 }
99
100 fn take(&mut self, len: usize) -> Option<&'a [u8]> {
102 let (head, tail) = self.buf.split_at_checked(len)?;
103 self.buf = tail;
104 Some(head)
105 }
106
107 fn u8(&mut self) -> Option<u8> {
109 self.take(1)?.first().copied()
110 }
111
112 fn u16(&mut self) -> Option<u16> {
114 let bytes: [u8; 2] = self.take(2)?.try_into().ok()?;
115 Some(u16::from_be_bytes(bytes))
116 }
117
118 fn u32(&mut self) -> Option<u32> {
120 let bytes: [u8; 4] = self.take(4)?.try_into().ok()?;
121 Some(u32::from_be_bytes(bytes))
122 }
123
124 fn ipv4(&mut self) -> Option<Ipv4Address> {
126 let [a, b, c, d]: [u8; 4] = self.take(4)?.try_into().ok()?;
127 Some(Ipv4Address::new(a, b, c, d))
128 }
129}
130
131struct Writer<'a> {
133 buf: &'a mut [u8],
134 pos: usize,
135}
136
137impl<'a> Writer<'a> {
138 fn new(buf: &'a mut [u8]) -> Self {
140 Self { buf, pos: 0 }
141 }
142
143 fn put(&mut self, bytes: &[u8]) -> Option<()> {
145 let end = self.pos.checked_add(bytes.len())?;
146 self.buf.get_mut(self.pos..end)?.copy_from_slice(bytes);
147 self.pos = end;
148 Some(())
149 }
150
151 fn zeros(&mut self, len: usize) -> Option<()> {
153 let end = self.pos.checked_add(len)?;
154 self.buf.get_mut(self.pos..end)?.fill(0);
155 self.pos = end;
156 Some(())
157 }
158
159 fn u8(&mut self, value: u8) -> Option<()> {
161 self.put(&[value])
162 }
163
164 fn u16(&mut self, value: u16) -> Option<()> {
166 self.put(&value.to_be_bytes())
167 }
168
169 fn u32(&mut self, value: u32) -> Option<()> {
171 self.put(&value.to_be_bytes())
172 }
173
174 fn ipv4(&mut self, value: Ipv4Address) -> Option<()> {
176 self.put(&value.octets())
177 }
178
179 fn opt(&mut self, kind: u8, data: &[u8]) -> Option<()> {
181 self.u8(kind)?;
182 self.u8(u8::try_from(data.len()).ok()?)?;
183 self.put(data)
184 }
185}
186
187struct ClientMsg {
189 xid: u32,
190 flags: u16,
191 mac: [u8; 6],
192 ciaddr: Ipv4Address,
193 giaddr: Ipv4Address,
194 msg_type: u8,
195 requested_ip: Option<Ipv4Address>,
196 server_id: Option<Ipv4Address>,
197}
198
199fn parse_client(buf: &[u8]) -> Option<ClientMsg> {
202 let mut r = Reader::new(buf);
203 let op = r.u8()?;
204 let htype = r.u8()?;
205 let hlen = r.u8()?;
206 let _hops = r.u8()?;
207 let xid = r.u32()?;
208 let _secs = r.u16()?;
209 let flags = r.u16()?;
210 let ciaddr = r.ipv4()?;
211 let _yiaddr = r.ipv4()?;
212 let _siaddr = r.ipv4()?;
213 let giaddr = r.ipv4()?;
214 let hw_addr = r.take(16)?;
215 let _sname = r.take(64)?;
216 let _file = r.take(128)?;
217 let cookie = r.u32()?;
218 if op != BOOTREQUEST
219 || htype != HTYPE_ETHERNET
220 || hlen != HLEN_ETHERNET
221 || cookie != MAGIC_COOKIE
222 {
223 return None;
224 }
225 let (mac, _padding) = hw_addr.split_at(usize::from(HLEN_ETHERNET));
226 let mac = mac.try_into().ok()?;
227
228 let mut msg_type = 0u8;
229 let mut requested_ip = None;
230 let mut server_id = None;
231 while let Some(kind) = r.u8() {
232 if kind == OPT_END {
233 break;
234 }
235 if kind == OPT_PAD {
236 continue;
237 }
238 let len = usize::from(r.u8()?);
240 let data = r.take(len)?;
241 match kind {
242 OPT_MESSAGE_TYPE => {
243 if let Some(value) = data.first() {
244 msg_type = *value;
245 }
246 }
247 OPT_REQUESTED_IP => {
248 if let Ok([a, b, c, d]) = <[u8; 4]>::try_from(data) {
249 requested_ip = Some(Ipv4Address::new(a, b, c, d));
250 }
251 }
252 OPT_SERVER_ID => {
253 if let Ok([a, b, c, d]) = <[u8; 4]>::try_from(data) {
254 server_id = Some(Ipv4Address::new(a, b, c, d));
255 }
256 }
257 _ => {}
258 }
259 }
260 if msg_type == 0 {
261 return None;
262 }
263
264 Some(ClientMsg {
265 xid,
266 flags,
267 mac,
268 ciaddr,
269 giaddr,
270 msg_type,
271 requested_ip,
272 server_id,
273 })
274}
275
276#[derive(Clone, Copy)]
278struct Lease {
279 mac: [u8; 6],
280 expires: Instant,
281 occupied: bool,
282}
283
284struct Server {
286 leases: [Lease; POOL_SIZE],
287}
288
289struct Action {
291 kind: u8,
292 yiaddr: Ipv4Address,
293 ciaddr: Ipv4Address,
294 with_config: bool,
295}
296
297impl Server {
298 const fn new() -> Self {
300 Self {
301 leases: [Lease {
302 mac: [0; 6],
303 expires: Instant::from_nanos(0),
304 occupied: false,
305 }; POOL_SIZE],
306 }
307 }
308
309 fn pool_addr(slot: usize) -> Option<Ipv4Address> {
311 if slot >= POOL_SIZE {
312 return None;
313 }
314 let offset = u8::try_from(slot).ok()?;
315 let octet = POOL_START.checked_add(offset)?;
316 let [a, b, c, _] = AP_IP.octets();
317 Some(Ipv4Address::new(a, b, c, octet))
318 }
319
320 fn slot_of_addr(ip: Ipv4Address) -> Option<usize> {
322 let [a, b, c, d] = ip.octets();
323 let [ap_a, ap_b, ap_c, _] = AP_IP.octets();
324 if [a, b, c] != [ap_a, ap_b, ap_c] {
325 return None;
326 }
327 let slot = d.checked_sub(POOL_START)?;
328 if usize::from(slot) < POOL_SIZE {
329 Some(usize::from(slot))
330 } else {
331 None
332 }
333 }
334
335 fn active_slot(&self, mac: [u8; 6], now: Instant) -> Option<usize> {
337 self.leases
338 .iter()
339 .position(|lease| lease.occupied && lease.mac == mac && lease.expires > now)
340 }
341
342 fn refresh(&mut self, slot: usize, now: Instant) {
344 if let Some(lease) = self.leases.get_mut(slot) {
345 lease.expires = now.saturating_add(LEASE);
346 }
347 }
348
349 fn assign(&mut self, mac: [u8; 6], now: Instant) -> Option<Ipv4Address> {
352 for (i, lease) in self.leases.iter_mut().enumerate() {
353 if lease.occupied && lease.mac == mac && lease.expires > now {
354 lease.expires = now.saturating_add(LEASE);
355 return Self::pool_addr(i);
356 }
357 }
358 for (i, lease) in self.leases.iter_mut().enumerate() {
359 if !lease.occupied || lease.expires <= now {
360 *lease = Lease {
361 mac,
362 expires: now.saturating_add(LEASE),
363 occupied: true,
364 };
365 return Self::pool_addr(i);
366 }
367 }
368 None
369 }
370
371 fn bind_addr(&mut self, mac: [u8; 6], ip: Ipv4Address, now: Instant) -> bool {
373 let Some(slot) = Self::slot_of_addr(ip) else {
374 return false;
375 };
376 let Some(lease) = self.leases.get_mut(slot) else {
377 return false;
378 };
379 let active = lease.occupied && lease.expires > now;
380 if active && lease.mac != mac {
381 return false;
382 }
383 if active {
384 lease.expires = now.saturating_add(LEASE);
385 } else {
386 *lease = Lease {
387 mac,
388 expires: now.saturating_add(LEASE),
389 occupied: true,
390 };
391 }
392 true
393 }
394
395 fn free(&mut self, slot: usize) {
397 if let Some(lease) = self.leases.get_mut(slot) {
398 lease.occupied = false;
399 }
400 }
401
402 fn handle(&mut self, msg: &ClientMsg, now: Instant) -> Option<Action> {
404 match msg.msg_type {
405 MSG_DISCOVER => {
406 let Some(ip) = self.assign(msg.mac, now) else {
407 log::warn!(
408 "DHCP: lease pool exhausted — ignoring DISCOVER from {:02X?}",
409 msg.mac
410 );
411 return None;
412 };
413 log::info!("DHCP OFFER {ip} for {:02X?}", msg.mac);
414 Some(Action {
415 kind: MSG_OFFER,
416 yiaddr: ip,
417 ciaddr: Ipv4Address::UNSPECIFIED,
418 with_config: true,
419 })
420 }
421 MSG_REQUEST => self.handle_request(msg, now),
422 MSG_DECLINE => {
423 if let Some(ip) = msg.requested_ip
424 && let Some(slot) = Self::slot_of_addr(ip)
425 {
426 let owned = self
427 .leases
428 .get(slot)
429 .is_some_and(|lease| lease.occupied && lease.mac == msg.mac);
430 if owned {
431 self.free(slot);
432 log::warn!("DHCP DECLINE: {ip} released, client {:02X?}", msg.mac);
433 } else {
434 log::warn!(
435 "DHCP DECLINE for {ip} ignored — not leased to {:02X?}",
436 msg.mac
437 );
438 }
439 }
440 None
441 }
442 MSG_RELEASE => {
443 if let Some(sid) = msg.server_id
444 && sid != AP_IP
445 {
446 return None;
447 }
448 if let Some(slot) = self.active_slot(msg.mac, now) {
449 self.free(slot);
450 log::info!("DHCP RELEASE for {:02X?}", msg.mac);
451 }
452 None
453 }
454 MSG_INFORM => Some(Action {
455 kind: MSG_ACK,
456 yiaddr: Ipv4Address::UNSPECIFIED,
457 ciaddr: msg.ciaddr,
458 with_config: false,
459 }),
460 _ => None,
461 }
462 }
463
464 fn handle_request(&mut self, msg: &ClientMsg, now: Instant) -> Option<Action> {
467 if let Some(sid) = msg.server_id
469 && sid != AP_IP
470 {
471 return None;
472 }
473 let requested = if let Some(ip) = msg.requested_ip {
476 Some(ip)
477 } else if msg.ciaddr.is_unspecified() {
478 None
479 } else {
480 Some(msg.ciaddr)
481 };
482
483 let (kind, yiaddr) = match (self.active_slot(msg.mac, now), requested) {
484 (Some(slot), Some(req)) => {
485 if Self::pool_addr(slot) == Some(req) {
486 self.refresh(slot, now);
487 (MSG_ACK, req)
488 } else {
489 (MSG_NAK, Ipv4Address::UNSPECIFIED)
490 }
491 }
492 (Some(_) | None, None) => (MSG_NAK, Ipv4Address::UNSPECIFIED),
493 (None, Some(req)) => {
494 if self.bind_addr(msg.mac, req, now) {
495 (MSG_ACK, req)
496 } else {
497 (MSG_NAK, Ipv4Address::UNSPECIFIED)
498 }
499 }
500 };
501 if kind == MSG_NAK {
502 log::warn!("DHCP NAK for {:02X?} (requested {requested:?})", msg.mac);
503 } else {
504 log::info!("DHCP ACK {yiaddr} for {:02X?}", msg.mac);
505 }
506 Some(Action {
507 kind,
508 yiaddr,
509 ciaddr: Ipv4Address::UNSPECIFIED,
510 with_config: kind == MSG_ACK,
511 })
512 }
513}
514
515fn build_reply(buf: &mut [u8], msg: &ClientMsg, action: &Action) -> Option<usize> {
517 buf.fill(0);
518 let mut w = Writer::new(buf);
519 w.u8(BOOTREPLY)?;
520 w.u8(HTYPE_ETHERNET)?;
521 w.u8(HLEN_ETHERNET)?;
522 w.u8(0)?;
523 w.u32(msg.xid)?;
524 w.u16(0)?;
525 w.u16(msg.flags)?;
526 w.ipv4(action.ciaddr)?;
527 w.ipv4(action.yiaddr)?;
528 w.ipv4(Ipv4Address::UNSPECIFIED)?;
529 w.ipv4(msg.giaddr)?;
530 w.put(&msg.mac)?;
531 w.zeros(10)?;
532 w.zeros(64)?;
533 w.zeros(128)?;
534 w.u32(MAGIC_COOKIE)?;
535
536 w.opt(OPT_MESSAGE_TYPE, &[action.kind])?;
537 w.opt(OPT_SERVER_ID, &AP_IP.octets())?;
538 if action.with_config {
539 let lease = u32::try_from(LEASE_SECS).ok()?;
540 w.opt(OPT_LEASE_TIME, &lease.to_be_bytes())?;
541 w.opt(OPT_SUBNET_MASK, &NETMASK)?;
542 w.opt(OPT_ROUTER, &AP_IP.octets())?;
543 w.opt(OPT_DNS, &AP_IP.octets())?;
544 }
545 w.u8(OPT_END)?;
546 Some(w.pos)
547}
548
549#[embassy_executor::task]
552pub async fn task(stack: Stack<'static>) {
553 let mut rx_meta = [PacketMetadata::EMPTY; 4];
554 let mut tx_meta = [PacketMetadata::EMPTY; 4];
555 let mut rx_ring = [0u8; 1024];
556 let mut tx_ring = [0u8; 512];
557 let mut socket = UdpSocket::new(
558 stack,
559 &mut rx_meta,
560 &mut rx_ring,
561 &mut tx_meta,
562 &mut tx_ring,
563 );
564 if let Err(err) = socket.bind(SERVER_PORT) {
565 log::error!("DHCP server: bind({SERVER_PORT}) failed: {err:?}");
566 return;
567 }
568 let [net0, net1, net2, _] = AP_IP.octets();
569 let base = Ipv4Address::new(net0, net1, net2, POOL_START);
570 log::info!("DHCP server up: {POOL_SIZE} leases from {base}");
571
572 let mut server = Server::new();
573 let mut pkt = [0u8; 1024];
574 let mut reply = [0u8; 512];
575 loop {
576 let (len, meta) = match socket.recv_from(&mut pkt).await {
577 Ok(received) => received,
578 Err(err) => {
579 log::warn!("DHCP: receive failed: {err:?}");
580 continue;
581 }
582 };
583 let IpAddress::Ipv4(sender) = meta.endpoint.addr;
585 log::debug!("DHCP rx {len}B from {sender}:{}", meta.endpoint.port);
586 let Some(data) = pkt.get(..len) else {
587 continue;
588 };
589 let Some(msg) = parse_client(data) else {
590 log::debug!("DHCP: ignored {len}B from {sender} (parse)");
591 continue;
592 };
593 let Some(action) = server.handle(&msg, Instant::now()) else {
594 continue;
595 };
596 let Some(used) = build_reply(&mut reply, &msg, &action) else {
597 continue;
598 };
599 let Some(packet) = reply.get(..used) else {
600 continue;
601 };
602
603 let dst = if msg.flags & FLAG_BROADCAST == 0 && !sender.is_unspecified() {
605 if action.yiaddr.is_unspecified() {
606 sender
607 } else {
608 action.yiaddr
609 }
610 } else {
611 Ipv4Address::BROADCAST
612 };
613 if let Err(err) = socket.send_to(packet, (dst, CLIENT_PORT)).await {
614 log::warn!("DHCP: send to {dst} failed: {err:?}");
615 }
616 }
617}