edge_dhcp/
server.rs

1use core::fmt::Debug;
2
3use super::*;
4
5#[derive(Clone, Debug)]
6#[cfg_attr(feature = "defmt", derive(defmt::Format))]
7pub struct Lease {
8    mac: [u8; 16],
9    expires: u64,
10}
11
12#[derive(Clone, Debug)]
13#[cfg_attr(feature = "defmt", derive(defmt::Format))]
14pub enum Action<'a> {
15    Discover(Option<Ipv4Addr>, &'a [u8; 16]),
16    Request(Ipv4Addr, &'a [u8; 16]),
17    Release(Ipv4Addr, &'a [u8; 16]),
18    Decline(Ipv4Addr, &'a [u8; 16]),
19}
20
21#[derive(Clone, Debug)]
22#[cfg_attr(feature = "defmt", derive(defmt::Format))]
23#[non_exhaustive]
24pub struct ServerOptions<'a> {
25    pub ip: Ipv4Addr,
26    pub gateways: &'a [Ipv4Addr],
27    pub subnet: Option<Ipv4Addr>,
28    pub dns: &'a [Ipv4Addr],
29    pub captive_url: Option<&'a str>,
30    pub lease_duration_secs: u32,
31}
32
33impl<'a> ServerOptions<'a> {
34    pub fn new(ip: Ipv4Addr, gw_buf: Option<&'a mut [Ipv4Addr; 1]>) -> Self {
35        let gateways = if let Some(gw_buf) = gw_buf {
36            gw_buf[0] = ip;
37            gw_buf.as_slice()
38        } else {
39            &[]
40        };
41
42        Self {
43            ip,
44            gateways,
45            subnet: Some(Ipv4Addr::new(255, 255, 255, 0)),
46            dns: &[],
47            captive_url: None,
48            lease_duration_secs: 7200,
49        }
50    }
51
52    pub fn process<'o>(&self, request: &'o Packet<'o>) -> Option<Action<'o>> {
53        if request.reply {
54            return None;
55        }
56
57        let message_type = request.options.iter().find_map(|option| {
58            if let DhcpOption::MessageType(message_type) = option {
59                Some(message_type)
60            } else {
61                None
62            }
63        });
64
65        let message_type = if let Some(message_type) = message_type {
66            message_type
67        } else {
68            warn!(
69                "Ignoring DHCP request, no message type found: {:?}",
70                request
71            );
72            return None;
73        };
74
75        let server_identifier = request.options.iter().find_map(|option| {
76            if let DhcpOption::ServerIdentifier(ip) = option {
77                Some(ip)
78            } else {
79                None
80            }
81        });
82
83        if server_identifier.is_some() && server_identifier != Some(self.ip) {
84            warn!(
85                "Ignoring {} request, not addressed to this server: {:?}",
86                message_type, request
87            );
88            return None;
89        }
90
91        debug!("Received {} request: {:?}", message_type, request);
92        match message_type {
93            MessageType::Discover => Some(Action::Discover(
94                request.options.requested_ip(),
95                &request.chaddr,
96            )),
97            MessageType::Request => {
98                let requested_ip = request.options.requested_ip().or_else(|| {
99                    if request.ciaddr.is_unspecified() {
100                        None
101                    } else {
102                        Some(request.ciaddr)
103                    }
104                })?;
105
106                Some(Action::Request(requested_ip, &request.chaddr))
107            }
108            MessageType::Release if server_identifier == Some(self.ip) => {
109                Some(Action::Release(request.yiaddr, &request.chaddr))
110            }
111            MessageType::Decline if server_identifier == Some(self.ip) => {
112                Some(Action::Decline(request.yiaddr, &request.chaddr))
113            }
114            _ => None,
115        }
116    }
117
118    pub fn offer(
119        &self,
120        request: &Packet,
121        yiaddr: Ipv4Addr,
122        opt_buf: &'a mut [DhcpOption<'a>],
123    ) -> Packet<'a> {
124        self.reply(request, MessageType::Offer, Some(yiaddr), opt_buf)
125    }
126
127    pub fn ack_nak(
128        &self,
129        request: &Packet,
130        ip: Option<Ipv4Addr>,
131        opt_buf: &'a mut [DhcpOption<'a>],
132    ) -> Packet<'a> {
133        self.reply(
134            request,
135            if ip.is_some() {
136                MessageType::Ack
137            } else {
138                MessageType::Nak
139            },
140            ip,
141            opt_buf,
142        )
143    }
144
145    fn reply(
146        &self,
147        request: &Packet,
148        message_type: MessageType,
149        ip: Option<Ipv4Addr>,
150        buf: &'a mut [DhcpOption<'a>],
151    ) -> Packet<'a> {
152        let reply = request.new_reply(
153            ip,
154            request.options.reply(
155                message_type,
156                self.ip,
157                self.lease_duration_secs as _,
158                self.gateways,
159                self.subnet,
160                self.dns,
161                self.captive_url,
162                buf,
163            ),
164        );
165
166        debug!("Sending {} reply: {:?}", message_type, reply);
167
168        reply
169    }
170}
171
172/// A simple DHCP server.
173/// The server is unaware of the IP/UDP transport layer and operates purely in terms of packets
174/// represented as Rust slices.
175#[derive(Clone, Debug)]
176#[cfg_attr(feature = "defmt", derive(defmt::Format))]
177pub struct Server<F, const N: usize> {
178    pub now: F,
179    pub range_start: Ipv4Addr,
180    pub range_end: Ipv4Addr,
181    pub leases: heapless::LinearMap<Ipv4Addr, Lease, N>,
182}
183
184impl<F, const N: usize> Server<F, N>
185where
186    F: FnMut() -> u64,
187{
188    /// Create a new DHCP server.
189    ///
190    /// # Arguments
191    /// - `now`: A closure that returns the current time in seconds since some epoch.
192    /// - `ip`: The IP address of the server.
193    pub const fn new(now: F, ip: Ipv4Addr) -> Self {
194        let octets = ip.octets();
195
196        Self {
197            now,
198            range_start: Ipv4Addr::new(octets[0], octets[1], octets[2], 50),
199            range_end: Ipv4Addr::new(octets[0], octets[1], octets[2], 200),
200            leases: heapless::LinearMap::new(),
201        }
202    }
203
204    pub fn handle_request<'o>(
205        &mut self,
206        opt_buf: &'o mut [DhcpOption<'o>],
207        server_options: &'o ServerOptions,
208        request: &Packet,
209    ) -> Option<Packet<'o>> {
210        server_options
211            .process(request)
212            .and_then(|action| match action {
213                Action::Discover(requested_ip, mac) => {
214                    let ip = requested_ip
215                        .and_then(|ip| self.is_available(mac, ip).then_some(ip))
216                        .or_else(|| self.current_lease(mac))
217                        .or_else(|| self.available());
218
219                    ip.map(|ip| server_options.offer(request, ip, opt_buf))
220                }
221                Action::Request(ip, mac) => {
222                    let now = (self.now)();
223
224                    let ip = (self.is_available(mac, ip)
225                        && self.add_lease(
226                            ip,
227                            request.chaddr,
228                            now + server_options.lease_duration_secs as u64,
229                        ))
230                    .then_some(ip);
231
232                    Some(server_options.ack_nak(request, ip, opt_buf))
233                }
234                Action::Release(_ip, mac) | Action::Decline(_ip, mac) => {
235                    self.remove_lease(mac);
236
237                    None
238                }
239            })
240    }
241
242    fn is_available(&mut self, mac: &[u8; 16], addr: Ipv4Addr) -> bool {
243        let pos: u32 = addr.into();
244
245        let start: u32 = self.range_start.into();
246        let end: u32 = self.range_end.into();
247
248        pos >= start
249            && pos <= end
250            && match self.leases.get(&addr) {
251                Some(lease) => lease.mac == *mac || (self.now)() > lease.expires,
252                None => true,
253            }
254    }
255
256    fn available(&mut self) -> Option<Ipv4Addr> {
257        let start: u32 = self.range_start.into();
258        let end: u32 = self.range_end.into();
259
260        for pos in start..end + 1 {
261            let addr = pos.into();
262
263            if !self.leases.contains_key(&addr) {
264                return Some(addr);
265            }
266        }
267
268        if let Some(addr) = self
269            .leases
270            .iter()
271            .find_map(|(addr, lease)| ((self.now)() > lease.expires).then_some(*addr))
272        {
273            self.leases.remove(&addr);
274
275            Some(addr)
276        } else {
277            None
278        }
279    }
280
281    fn current_lease(&self, mac: &[u8; 16]) -> Option<Ipv4Addr> {
282        self.leases
283            .iter()
284            .find_map(|(addr, lease)| (lease.mac == *mac).then_some(*addr))
285    }
286
287    fn add_lease(&mut self, addr: Ipv4Addr, mac: [u8; 16], expires: u64) -> bool {
288        self.remove_lease(&mac);
289
290        self.leases.insert(addr, Lease { mac, expires }).is_ok()
291    }
292
293    fn remove_lease(&mut self, mac: &[u8; 16]) -> bool {
294        if let Some(addr) = self.current_lease(mac) {
295            self.leases.remove(&addr);
296
297            true
298        } else {
299            false
300        }
301    }
302}
303
304#[cfg(feature = "io")]
305impl<const N: usize> Server<fn() -> u64, N> {
306    /// Create a new DHCP server using `embassy-time::Instant::now` as the currtent time epoch provider.
307    ///
308    /// # Arguments
309    /// - `ip`: The IP address of the server.
310    pub const fn new_with_et(ip: Ipv4Addr) -> Self {
311        Self::new(|| embassy_time::Instant::now().as_secs(), ip)
312    }
313}