edge_dhcp/
client.rs

1use rand_core::RngCore;
2
3use super::*;
4
5/// A simple DHCP client.
6/// The client is unaware of the IP/UDP transport layer and operates purely in terms of packets
7/// represented as Rust slices.
8///
9/// As such, the client can generate all BOOTP requests and parse BOOTP replies.
10pub struct Client<T> {
11    pub rng: T,
12    pub mac: [u8; 6],
13}
14
15impl<T> Client<T>
16where
17    T: RngCore,
18{
19    pub const fn new(rng: T, mac: [u8; 6]) -> Self {
20        Self { rng, mac }
21    }
22
23    pub fn discover<'o>(
24        &mut self,
25        opt_buf: &'o mut [DhcpOption<'o>],
26        secs: u16,
27        ip: Option<Ipv4Addr>,
28    ) -> (Packet<'o>, u32) {
29        self.bootp_request(secs, None, true, Options::discover(ip, opt_buf))
30    }
31
32    pub fn request<'o>(
33        &mut self,
34        opt_buf: &'o mut [DhcpOption<'o>],
35        secs: u16,
36        ip: Ipv4Addr,
37        broadcast: bool,
38    ) -> (Packet<'o>, u32) {
39        self.bootp_request(secs, None, broadcast, Options::request(ip, opt_buf))
40    }
41
42    pub fn release<'o>(
43        &mut self,
44        opt_buf: &'o mut [DhcpOption<'o>],
45        secs: u16,
46        ip: Ipv4Addr,
47    ) -> Packet<'o> {
48        self.bootp_request(secs, Some(ip), false, Options::release(opt_buf))
49            .0
50    }
51
52    pub fn decline<'o>(
53        &mut self,
54        opt_buf: &'o mut [DhcpOption<'o>],
55        secs: u16,
56        ip: Ipv4Addr,
57    ) -> Packet<'o> {
58        self.bootp_request(secs, Some(ip), false, Options::decline(opt_buf))
59            .0
60    }
61
62    pub fn is_offer(&self, reply: &Packet<'_>, xid: u32) -> bool {
63        self.is_bootp_reply_for_us(reply, xid, Some(&[MessageType::Offer]))
64    }
65
66    pub fn is_ack(&self, reply: &Packet<'_>, xid: u32) -> bool {
67        self.is_bootp_reply_for_us(reply, xid, Some(&[MessageType::Ack]))
68    }
69
70    pub fn is_nak(&self, reply: &Packet<'_>, xid: u32) -> bool {
71        self.is_bootp_reply_for_us(reply, xid, Some(&[MessageType::Nak]))
72    }
73
74    #[allow(clippy::too_many_arguments)]
75    pub fn bootp_request<'o>(
76        &mut self,
77        secs: u16,
78        ip: Option<Ipv4Addr>,
79        broadcast: bool,
80        options: Options<'o>,
81    ) -> (Packet<'o>, u32) {
82        let xid = self.rng.next_u32();
83
84        (
85            Packet::new_request(self.mac, xid, secs, ip, broadcast, options),
86            xid,
87        )
88    }
89
90    pub fn is_bootp_reply_for_us(
91        &self,
92        reply: &Packet<'_>,
93        xid: u32,
94        expected_message_types: Option<&[MessageType]>,
95    ) -> bool {
96        if reply.reply && reply.is_for_us(&self.mac, xid) {
97            if let Some(expected_message_types) = expected_message_types {
98                let mt = reply.options.iter().find_map(|option| {
99                    if let DhcpOption::MessageType(mt) = option {
100                        Some(mt)
101                    } else {
102                        None
103                    }
104                });
105
106                expected_message_types.iter().any(|emt| mt == Some(*emt))
107            } else {
108                true
109            }
110        } else {
111            false
112        }
113    }
114}