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}