w5500_dhcp/lib.rs
1//! DHCP client for the [Wiznet W5500] SPI internet offload chip.
2//!
3//! # Warning
4//!
5//! Please review the code before use in a production environment.
6//! This code has been tested, but only with a single DHCP server.
7//!
8//! # Feature Flags
9//!
10//! All features are disabled by default.
11//!
12//! * `eh0`: Passthrough to [`w5500-hl`].
13//! * `eh1`: Passthrough to [`w5500-hl`].
14//! * `defmt`: Enable logging with `defmt`. Also a passthrough to [`w5500-hl`].
15//! * `log`: Enable logging with `log`.
16//!
17//! [`w5500-hl`]: https://crates.io/crates/w5500-hl
18//! [Wiznet W5500]: https://www.wiznet.io/product-item/w5500/
19#![cfg_attr(docsrs, feature(doc_cfg), feature(doc_auto_cfg))]
20#![cfg_attr(not(test), no_std)]
21#![forbid(unsafe_code)]
22#![warn(missing_docs)]
23
24// This mod MUST go first, so that the others see its macros.
25pub(crate) mod fmt;
26
27mod pkt;
28mod rand;
29
30use hl::{
31 io::Seek,
32 ll::{
33 net::{Eui48Addr, Ipv4Addr},
34 LinkStatus, PhyCfg, Registers, Sn, SocketInterrupt, SocketInterruptMask,
35 },
36 net::SocketAddrV4,
37 Common, Error, Udp, UdpReader,
38};
39pub use w5500_hl as hl;
40pub use w5500_hl::ll;
41
42use pkt::{send_dhcp_discover, send_dhcp_request, MsgType, PktDe};
43pub use w5500_hl::Hostname;
44
45/// DHCP destination port.
46pub const DST_PORT: u16 = 67;
47
48/// DHCP source port.
49pub const SRC_PORT: u16 = 68;
50
51/// Duration in seconds to wait for physical link-up.
52const LINK_UP_TIMEOUT_SECS: u32 = 1;
53
54/// DHCP client states.
55#[derive(Debug, PartialEq, Eq, Clone, Copy)]
56#[cfg_attr(feature = "defmt", derive(defmt::Format))]
57#[non_exhaustive] // may support rebooting and ini-reboot in the future
58#[doc(hidden)]
59pub enum State {
60 /// Initialization state, client sends DHCPDISCOVER.
61 Init,
62 /// Client waits for DHCPOFFER.
63 Selecting,
64 /// Client sends for DHCPREQUEST.
65 Requesting,
66 /// Client has a valid lease.
67 Bound,
68 /// T1 expires, client sends DHCPREQUEST to renew.
69 Renewing,
70 /// T2 expires, client sends DHCPREQUEST to rebind.
71 Rebinding,
72}
73
74/// DHCP client.
75///
76/// This requires the W5500 interrupt pin configured for a falling edge trigger.
77///
78/// # Example
79///
80/// ```no_run
81/// use rand_core::RngCore;
82/// use w5500_dhcp::{
83/// ll::{net::Eui48Addr, Sn},
84/// Client, Hostname,
85/// };
86/// # let mut w5500 = w5500_regsim::W5500::default();
87/// # let mut rng = rand_core::OsRng;
88/// # fn this_is_where_you_setup_the_w5500_int_pin_for_a_falling_edge_trigger() { }
89/// # fn monotonic_seconds() -> u32 { 0 }
90///
91/// const DHCP_SN: Sn = Sn::Sn0;
92///
93/// // locally administered MAC address
94/// const MAC_ADDRESS: Eui48Addr = Eui48Addr::new(0x02, 0x00, 0x11, 0x22, 0x33, 0x44);
95///
96/// const HOSTNAME: Hostname = Hostname::new_unwrapped("example");
97///
98/// this_is_where_you_setup_the_w5500_int_pin_for_a_falling_edge_trigger();
99///
100/// let seed: u64 = rng.next_u64();
101///
102/// let mut dhcp: Client = Client::new(DHCP_SN, seed, MAC_ADDRESS, HOSTNAME);
103///
104/// dhcp.setup_socket(&mut w5500)?;
105///
106/// let call_after_secs: u32 = dhcp.process(&mut w5500, monotonic_seconds())?;
107/// // call process again after call_after_secs, or on the next interrupt
108/// # Ok::<(), w5500_hl::Error<std::io::ErrorKind>>(())
109/// ```
110#[derive(Debug)]
111#[cfg_attr(feature = "defmt", derive(defmt::Format))]
112pub struct Client<'a> {
113 /// Socket to use for the DHCP client.
114 sn: Sn,
115 /// DHCP client state
116 state: State,
117 /// Instant of the last state transition
118 state_timeout: Option<u32>,
119 /// Timeout duration in seconds
120 timeout: u32,
121 /// Renewal duration.
122 t1: u32,
123 /// Rebinding duration.
124 t2: u32,
125 /// Lease duration.
126 lease: u32,
127 /// Time that the lease was obtained.
128 lease_monotonic_secs: u32,
129 /// DHCP server identifier option
130 server_id_option: Option<Ipv4Addr>,
131 /// Last XID
132 xid: u32,
133 /// XID generator
134 rand: rand::Rand,
135 /// Hardware (EUI-48 MAC) address
136 mac: Eui48Addr,
137 /// IP address
138 ip: Ipv4Addr,
139 /// Client hostname
140 hostname: Hostname<'a>,
141 /// DNS server
142 dns: Option<Ipv4Addr>,
143 /// (S)NTP server
144 ntp: Option<Ipv4Addr>,
145 /// Broadcast address
146 broadcast_addr: SocketAddrV4,
147 /// Source Port
148 src_port: u16,
149}
150
151impl<'a> Client<'a> {
152 /// Create a new DHCP client storage structure.
153 ///
154 /// The DHCP client can be reset by re-creating this structure.
155 ///
156 /// # Example
157 ///
158 /// ```
159 /// use rand_core::RngCore;
160 /// use w5500_dhcp::{
161 /// ll::{net::Eui48Addr, Sn},
162 /// Client, Hostname,
163 /// };
164 /// # let mut rng = rand_core::OsRng;
165 ///
166 /// const DHCP_SN: Sn = Sn::Sn0;
167 /// // locally administered MAC address
168 /// const MAC_ADDRESS: Eui48Addr = Eui48Addr::new(0x02, 0x00, 0x11, 0x22, 0x33, 0x44);
169 /// const HOSTNAME: Hostname = Hostname::new_unwrapped("example");
170 /// let seed: u64 = rng.next_u64();
171 ///
172 /// let dhcp: Client = Client::new(DHCP_SN, seed, MAC_ADDRESS, HOSTNAME);
173 /// ```
174 pub fn new(sn: Sn, seed: u64, mac: Eui48Addr, hostname: Hostname<'a>) -> Self {
175 let mut rand: rand::Rand = rand::Rand::new(seed);
176
177 Self {
178 sn,
179 state: State::Init,
180 state_timeout: None,
181 timeout: 5,
182 t1: 0,
183 t2: 0,
184 lease: 0,
185 lease_monotonic_secs: 0,
186 server_id_option: None,
187 xid: rand.next_u32(),
188 rand,
189 mac,
190 ip: Ipv4Addr::UNSPECIFIED,
191 hostname,
192 dns: None,
193 ntp: None,
194 broadcast_addr: SocketAddrV4::new(Ipv4Addr::BROADCAST, DST_PORT),
195 src_port: SRC_PORT,
196 }
197 }
198
199 /// Set the DHCP state timeout duration in seconds.
200 ///
201 /// This is the duration to wait for the DHCP server to send a reply before
202 /// resetting and starting over.
203 ///
204 /// # Example
205 ///
206 /// Set a 10 second timeout.
207 ///
208 /// ```
209 /// use rand_core::RngCore;
210 /// use w5500_dhcp::{
211 /// ll::{net::Eui48Addr, Sn},
212 /// Client, Hostname,
213 /// };
214 /// # let mut rng = rand_core::OsRng;
215 ///
216 /// const HOSTNAME: Hostname = Hostname::new_unwrapped("example");
217 /// let mut dhcp: Client = Client::new(
218 /// Sn::Sn0,
219 /// rng.next_u64(),
220 /// Eui48Addr::new(0x02, 0x00, 0x11, 0x22, 0x33, 0x44),
221 /// HOSTNAME,
222 /// );
223 /// dhcp.set_timeout_secs(10);
224 /// ```
225 pub fn set_timeout_secs(&mut self, secs: u32) {
226 self.timeout = secs;
227 }
228
229 /// Returns `true` if the DHCP client has a valid lease.
230 ///
231 /// # Example
232 ///
233 /// ```
234 /// use rand_core::RngCore;
235 /// use w5500_dhcp::{
236 /// ll::{net::Eui48Addr, Sn},
237 /// Client, Hostname,
238 /// };
239 /// # let mut rng = rand_core::OsRng;
240 ///
241 /// const HOSTNAME: Hostname = Hostname::new_unwrapped("example");
242 /// let dhcp: Client = Client::new(
243 /// Sn::Sn0,
244 /// rng.next_u64(),
245 /// Eui48Addr::new(0x02, 0x00, 0x11, 0x22, 0x33, 0x44),
246 /// HOSTNAME,
247 /// );
248 /// assert_eq!(dhcp.has_lease(), false);
249 /// ```
250 #[inline]
251 pub fn has_lease(&self) -> bool {
252 matches!(
253 self.state,
254 State::Bound | State::Rebinding | State::Renewing
255 )
256 }
257
258 /// Setup the DHCP socket interrupts.
259 ///
260 /// This should be called once during initialization.
261 ///
262 /// This only sets up the W5500 interrupts, you must configure the W5500
263 /// interrupt pin for a falling edge trigger yourself.
264 pub fn setup_socket<W5500: Registers>(&self, w5500: &mut W5500) -> Result<(), W5500::Error> {
265 let simr: u8 = w5500.simr()?;
266 w5500.set_simr(self.sn.bitmask() | simr)?;
267 const MASK: SocketInterruptMask = SocketInterruptMask::ALL_MASKED.unmask_recv();
268 w5500.set_sn_imr(self.sn, MASK)?;
269 w5500.close(self.sn)?;
270 w5500.set_sipr(&self.ip)?;
271 w5500.udp_bind(self.sn, self.src_port)
272 }
273
274 fn timeout_elapsed_secs(&self, monotonic_secs: u32) -> Option<u32> {
275 self.state_timeout.map(|to| monotonic_secs - to)
276 }
277
278 fn next_call(&self, monotonic_secs: u32) -> u32 {
279 if let Some(timeout_elapsed_secs) = self.timeout_elapsed_secs(monotonic_secs) {
280 self.timeout.saturating_sub(timeout_elapsed_secs)
281 } else {
282 let elapsed: u32 = monotonic_secs.saturating_sub(self.lease_monotonic_secs);
283 match self.state {
284 State::Bound => self.t1.saturating_sub(elapsed),
285 State::Renewing => self.t2.saturating_sub(elapsed),
286 // rebinding
287 _ => self.lease.saturating_sub(elapsed),
288 }
289 }
290 }
291
292 fn set_state_with_timeout(&mut self, state: State, monotonic_secs: u32) {
293 debug!(
294 "{:?} -> {:?} with timeout {}",
295 self.state, state, monotonic_secs
296 );
297 self.state = state;
298 self.state_timeout = Some(monotonic_secs);
299 }
300
301 fn set_state(&mut self, state: State) {
302 debug!("{:?} -> {:?} without timeout", self.state, state);
303 self.state = state;
304 self.state_timeout = None;
305 }
306
307 /// Get the leased IP address.
308 pub fn leased_ip(&self) -> Option<Ipv4Addr> {
309 if self.has_lease() {
310 Some(self.ip)
311 } else {
312 None
313 }
314 }
315
316 /// Get the DNS server provided by DHCP.
317 ///
318 /// After the client is bound this will return the IP address of the
319 /// most-preferred DNS server.
320 /// If the client is not bound, or the DHCP server did not provide this
321 /// address it will return `None`.
322 #[inline]
323 pub fn dns(&self) -> Option<Ipv4Addr> {
324 self.dns
325 }
326
327 /// Get the NTP server provided by DHCP.
328 ///
329 /// After the client is bound this will return the IP address of the
330 /// most-preferred NTP server.
331 /// If the client is not bound, or the DHCP server did not provide this
332 /// address it will return `None`.
333 #[inline]
334 pub fn ntp(&self) -> Option<Ipv4Addr> {
335 self.ntp
336 }
337
338 /// Process DHCP client events.
339 ///
340 /// This should be called in these conditions:
341 ///
342 /// 1. After power-on-reset, to start the DHCP client.
343 /// 2. A W5500 interrupt on the DHCP socket is received.
344 /// 3. After the duration indicated by the return value.
345 ///
346 /// This will clear any pending DHCP socket interrupts.
347 ///
348 /// # System Time
349 ///
350 /// You must supply a monotonic `u32` that counts the number of seconds
351 /// since system boot to this method.
352 ///
353 /// This is required for timeouts, and tracking the DHCP lease timers.
354 ///
355 /// # Return Value
356 ///
357 /// The return value is a number of seconds into the future you should
358 /// call this method.
359 /// You may call this method before that time, but nothing will happen.
360 pub fn process<W5500: Registers>(
361 &mut self,
362 w5500: &mut W5500,
363 monotonic_secs: u32,
364 ) -> Result<u32, Error<W5500::Error>> {
365 let sn_ir: SocketInterrupt = w5500.sn_ir(self.sn)?;
366 if sn_ir.any_raised() {
367 w5500.set_sn_ir(self.sn, sn_ir)?;
368 }
369
370 if self.state == State::Init {
371 let phy_cfg: PhyCfg = w5500.phycfgr()?;
372 if phy_cfg.lnk() != LinkStatus::Up {
373 debug!("Link is not up: {}", phy_cfg);
374 return Ok(LINK_UP_TIMEOUT_SECS);
375 };
376 }
377
378 fn recv<W5500: Registers>(
379 w5500: &mut W5500,
380 sn: Sn,
381 xid: u32,
382 ) -> Result<Option<PktDe<W5500>>, Error<W5500::Error>> {
383 let reader: UdpReader<W5500> = match w5500.udp_reader(sn) {
384 Ok(r) => r,
385 Err(Error::WouldBlock) => return Ok(None),
386 Err(e) => return Err(e),
387 };
388
389 debug!(
390 "RX {} B from {}",
391 reader.header().len,
392 reader.header().origin
393 );
394
395 let stream_len: u16 = reader.stream_len();
396 let header_len: u16 = reader.header().len;
397 if header_len > stream_len {
398 // this is often recoverable
399 warn!(
400 "packet may be truncated header={} > stream={}",
401 header_len, stream_len
402 );
403 return Ok(None);
404 }
405
406 let mut pkt: PktDe<W5500> = PktDe::from(reader);
407 if pkt.is_bootreply()? {
408 debug!("packet is not a bootreply");
409 return Ok(None);
410 }
411 let recv_xid: u32 = pkt.xid()?;
412 if recv_xid != xid {
413 debug!("recv xid {:08X} does not match ours {:08X}", recv_xid, xid);
414 return Ok(None);
415 }
416
417 Ok(Some(pkt))
418 }
419
420 while let Some(mut pkt) = recv(w5500, self.sn, self.xid)? {
421 debug!("{:?}", self.state);
422 match self.state {
423 State::Selecting => {
424 self.ip = pkt.yiaddr()?;
425 self.server_id_option = pkt.dhcp_server()?;
426 pkt.done()?;
427 self.request(w5500)?;
428 self.set_state_with_timeout(State::Requesting, monotonic_secs);
429 }
430 State::Requesting | State::Renewing | State::Rebinding => {
431 match pkt.msg_type()? {
432 Some(MsgType::Ack) => {
433 let subnet_mask: Option<Ipv4Addr> = pkt.subnet_mask()?;
434 let gateway: Option<Ipv4Addr> = pkt.dhcp_server()?;
435 let dns: Option<Ipv4Addr> = pkt.dns()?;
436 let ntp: Option<Ipv4Addr> = pkt.ntp()?;
437
438 let lease_time: u32 = match pkt.lease_time()? {
439 Some(x) => x,
440 None => {
441 error!("lease_time option missing");
442 return Ok(self.next_call(monotonic_secs));
443 }
444 };
445 info!("lease_time: {}", lease_time);
446 let renewal_time: u32 = match pkt.renewal_time()? {
447 Some(x) => x,
448 None => lease_time / 2,
449 };
450 info!("renewal_time: {}", renewal_time);
451 let rebinding_time: u32 = match pkt.rebinding_time()? {
452 Some(x) => x,
453 None => lease_time.saturating_mul(7) / 8,
454 };
455 info!("rebinding_time: {}", rebinding_time);
456
457 // de-rate times by 12%
458 self.t1 = renewal_time.saturating_sub(renewal_time / 8);
459 self.t2 = rebinding_time.saturating_sub(rebinding_time / 8);
460 self.lease = lease_time.saturating_sub(lease_time / 8);
461 self.lease_monotonic_secs = monotonic_secs;
462
463 pkt.done()?;
464
465 match subnet_mask {
466 Some(subnet_mask) => {
467 info!("subnet_mask: {}", subnet_mask);
468 w5500.set_subr(&subnet_mask)?;
469 }
470 None if self.state == State::Renewing => (),
471 None => {
472 error!("subnet_mask option missing");
473 return Ok(self.next_call(monotonic_secs));
474 }
475 };
476
477 match gateway {
478 Some(gateway) => {
479 info!("gateway: {}", gateway);
480 w5500.set_gar(&gateway)?;
481 }
482 None if self.state == State::Renewing => (),
483 None => {
484 error!("gateway option missing");
485 return Ok(self.next_call(monotonic_secs));
486 }
487 };
488
489 // rebinding and renewal do not need to set a new IP
490 if self.state == State::Requesting {
491 info!("dhcp.ip: {}", self.ip);
492 w5500.set_sipr(&self.ip)?;
493 }
494
495 if let Some(dns) = dns {
496 info!("DNS: {}", dns);
497 self.dns.replace(dns);
498 };
499
500 if let Some(ntp) = ntp {
501 info!("NTP: {}", ntp);
502 self.ntp.replace(ntp);
503 };
504
505 self.set_state(State::Bound);
506 }
507 Some(MsgType::Nak) => {
508 info!("request was NAK'd");
509 pkt.done()?;
510 self.discover(w5500, monotonic_secs)?;
511 }
512 Some(mt) => {
513 info!("ignoring message type {:?}", mt);
514 pkt.done()?;
515 }
516 None => {
517 error!("message type option missing");
518 pkt.done()?;
519 }
520 }
521 }
522 state => {
523 debug!("ignored IRQ in state={:?}", state);
524 pkt.done()?;
525 }
526 }
527 }
528
529 if let Some(elapsed_secs) = self.timeout_elapsed_secs(monotonic_secs) {
530 if elapsed_secs > self.timeout {
531 info!(
532 "timeout waiting for state to transition from {:?}",
533 self.state
534 );
535 self.discover(w5500, monotonic_secs)?;
536 }
537 } else {
538 match self.state {
539 State::Init => self.discover(w5500, monotonic_secs)?,
540 // states handled by IRQs and timeouts
541 State::Selecting | State::Requesting => (),
542 State::Bound | State::Renewing | State::Rebinding => {
543 let elapsed: u32 = monotonic_secs.wrapping_sub(self.lease_monotonic_secs);
544 if elapsed > self.lease {
545 info!("lease expired");
546 self.discover(w5500, monotonic_secs)?;
547 } else if elapsed > self.t2
548 && matches!(self.state, State::Bound | State::Renewing)
549 {
550 info!("t2 expired");
551 self.request(w5500)?;
552 // no need for timeout, lease expiration will handle failures
553 self.set_state(State::Rebinding);
554 } else if elapsed > self.t1 && matches!(self.state, State::Bound) {
555 info!("t1 expired");
556 self.request(w5500)?;
557 // no need for timeout, t2 expiration will handle failures
558 self.set_state(State::Renewing);
559 }
560 }
561 }
562 }
563
564 Ok(self.next_call(monotonic_secs))
565 }
566
567 fn discover<W5500: Registers>(
568 &mut self,
569 w5500: &mut W5500,
570 monotonic_secs: u32,
571 ) -> Result<(), Error<W5500::Error>> {
572 self.ip = Ipv4Addr::UNSPECIFIED;
573 self.xid = self.rand.next_u32();
574 debug!("sending DHCPDISCOVER xid={:08X}", self.xid);
575
576 w5500.set_sipr(&self.ip)?;
577 w5500.udp_bind(self.sn, self.src_port)?;
578
579 send_dhcp_discover(
580 w5500,
581 self.sn,
582 &self.mac,
583 self.hostname,
584 self.xid,
585 &self.broadcast_addr,
586 )?;
587 self.set_state_with_timeout(State::Selecting, monotonic_secs);
588 Ok(())
589 }
590
591 fn request<W5500: Registers>(&mut self, w5500: &mut W5500) -> Result<(), Error<W5500::Error>> {
592 self.xid = self.rand.next_u32();
593 debug!("sending DHCPREQUEST xid={:08X}", self.xid);
594 send_dhcp_request(
595 w5500,
596 self.sn,
597 &self.mac,
598 &self.ip,
599 self.hostname,
600 self.server_id_option.as_ref(),
601 self.xid,
602 )?;
603 Ok(())
604 }
605
606 /// Set the DHCP source port.
607 ///
608 /// Defaults to [`SRC_PORT`].
609 /// This is an interface for testing, typically the default is what you
610 /// want to use.
611 #[inline]
612 #[doc(hidden)]
613 pub fn set_src_port(&mut self, port: u16) {
614 self.src_port = port
615 }
616
617 /// Set the client broadcast address.
618 ///
619 /// Defaults to [`Ipv4Addr::BROADCAST`]:[`DST_PORT`].
620 /// This is an interface for testing, typically the default is what you
621 /// want to use.
622 #[inline]
623 #[doc(hidden)]
624 pub fn set_broadcast_addr(&mut self, addr: SocketAddrV4) {
625 self.broadcast_addr = addr;
626 }
627
628 /// DHCP client state.
629 #[inline]
630 #[doc(hidden)]
631 pub fn state(&self) -> State {
632 self.state
633 }
634
635 /// T1 time.
636 ///
637 /// This should be used only as a debug interface, and not to set timers.
638 /// DHCP timers are tracked internally by [`process`](Self::process).
639 ///
640 /// Returns `None` if the DHCP client does not have a valid lease.
641 #[inline]
642 #[doc(hidden)]
643 pub fn t1(&self) -> Option<u32> {
644 match self.state {
645 State::Init | State::Selecting | State::Requesting => None,
646 State::Bound | State::Renewing | State::Rebinding => Some(self.t1),
647 }
648 }
649
650 /// T2 time.
651 ///
652 /// This should be used only as a debug interface, and not to set timers.
653 /// DHCP timers are tracked internally by [`process`](Self::process).
654 ///
655 /// Returns `None` if the DHCP client does not have a valid lease.
656 #[inline]
657 #[doc(hidden)]
658 pub fn t2(&self) -> Option<u32> {
659 match self.state {
660 State::Init | State::Selecting | State::Requesting => None,
661 State::Bound | State::Renewing | State::Rebinding => Some(self.t2),
662 }
663 }
664
665 /// Lease time.
666 ///
667 /// This should be used only as a debug interface, and not to set timers.
668 /// DHCP timers are tracked internally by [`process`](Self::process).
669 ///
670 /// Returns `None` if the DHCP client does not have a valid lease.
671 #[inline]
672 #[doc(hidden)]
673 pub fn lease_time(&self) -> Option<u32> {
674 match self.state {
675 State::Init | State::Selecting | State::Requesting => None,
676 State::Bound | State::Renewing | State::Rebinding => Some(self.lease),
677 }
678 }
679}