#![no_std]
#![warn(missing_docs)]
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
use core::net::Ipv4Addr;
use embassy_net::Stack;
use embassy_net::udp::{PacketMetadata, UdpSocket};
use embassy_time::{Duration, Timer};
use hash32::{BuildHasherDefault, FnvHasher};
use heapless::{IndexMap, Vec};
use smoltcp::phy::PacketMeta;
pub use embassy_net::udp::RecvError;
const DHCP_SERVER_PORT: u16 = 67;
const DHCP_CLIENT_PORT: u16 = 68;
const DEFAULT_MAX_CLIENTS: usize = 32;
const DEFAULT_MAX_DNS_SERVERS: usize = 4;
const DEFAULT_LEASE_TIME: u32 = 86400; const SOCKET_BUFFER_SIZE: usize = 1024;
const OFFER_RESERVATION_MS: u64 = 60_000;
#[derive(Clone, Debug)]
pub struct DhcpConfig<const MAX_DNS: usize = DEFAULT_MAX_DNS_SERVERS> {
pub server_ip: Ipv4Addr,
pub subnet_mask: Ipv4Addr,
pub router: Option<Ipv4Addr>,
pub dns_servers: heapless::Vec<Ipv4Addr, MAX_DNS>,
pub ip_pool_start: Ipv4Addr,
pub ip_pool_end: Ipv4Addr,
pub lease_time: u32,
}
impl<const MAX_DNS: usize> Default for DhcpConfig<MAX_DNS> {
fn default() -> Self {
let mut dns_servers = heapless::Vec::new();
let _ = dns_servers.push(Ipv4Addr::new(8, 8, 8, 8));
Self {
server_ip: Ipv4Addr::new(192, 168, 1, 1),
subnet_mask: Ipv4Addr::new(255, 255, 255, 0),
router: Some(Ipv4Addr::new(192, 168, 1, 1)),
dns_servers,
ip_pool_start: Ipv4Addr::new(192, 168, 1, 100),
ip_pool_end: Ipv4Addr::new(192, 168, 1, 200),
lease_time: DEFAULT_LEASE_TIME,
}
}
}
#[derive(Clone, Debug)]
pub struct DhcpConfigBuilder<const MAX_DNS: usize = DEFAULT_MAX_DNS_SERVERS> {
config: DhcpConfig<MAX_DNS>,
}
impl<const MAX_DNS: usize> DhcpConfigBuilder<MAX_DNS> {
#[must_use]
pub fn new() -> Self {
let mut config = DhcpConfig::default();
config.dns_servers.clear();
Self { config }
}
#[must_use]
pub const fn server_ip(mut self, ip: Ipv4Addr) -> Self {
self.config.server_ip = ip;
self
}
#[must_use]
pub const fn subnet_mask(mut self, mask: Ipv4Addr) -> Self {
self.config.subnet_mask = mask;
self
}
#[must_use]
pub const fn router(mut self, router: Ipv4Addr) -> Self {
self.config.router = Some(router);
self
}
#[must_use]
pub const fn no_router(mut self) -> Self {
self.config.router = None;
self
}
#[must_use]
pub fn add_dns_server(mut self, dns: Ipv4Addr) -> Self {
let _ = self.config.dns_servers.push(dns);
self
}
#[must_use]
pub fn clear_dns_servers(mut self) -> Self {
self.config.dns_servers.clear();
self
}
#[must_use]
pub const fn ip_pool(mut self, start: Ipv4Addr, end: Ipv4Addr) -> Self {
self.config.ip_pool_start = start;
self.config.ip_pool_end = end;
self
}
#[must_use]
pub const fn lease_time(mut self, seconds: u32) -> Self {
self.config.lease_time = seconds;
self
}
#[must_use]
pub fn build(self) -> DhcpConfig<MAX_DNS> {
self.config
}
}
impl<const MAX_DNS: usize> Default for DhcpConfigBuilder<MAX_DNS> {
fn default() -> Self {
Self::new()
}
}
const DHCP_DISCOVER: u8 = 1;
const DHCP_OFFER: u8 = 2;
const DHCP_REQUEST: u8 = 3;
const DHCP_ACK: u8 = 5;
const DHCP_RELEASE: u8 = 7;
const OPTION_SUBNET_MASK: u8 = 1;
const OPTION_ROUTER: u8 = 3;
const OPTION_DNS_SERVER: u8 = 6;
const OPTION_LEASE_TIME: u8 = 51;
const OPTION_MESSAGE_TYPE: u8 = 53;
const OPTION_SERVER_ID: u8 = 54;
const OPTION_END: u8 = 255;
const DHCP_MAGIC: [u8; 4] = [0x63, 0x82, 0x53, 0x63];
#[repr(C, packed)]
#[derive(Clone, Copy)]
struct DhcpPacket {
op: u8,
htype: u8,
hlen: u8,
hops: u8,
xid: u32,
secs: u16,
flags: u16,
ciaddr: [u8; 4],
yiaddr: [u8; 4],
siaddr: [u8; 4],
giaddr: [u8; 4],
chaddr: [u8; 16],
sname: [u8; 64],
file: [u8; 128],
magic: [u8; 4],
}
impl Default for DhcpPacket {
fn default() -> Self {
Self {
op: 0,
htype: 0,
hlen: 0,
hops: 0,
xid: 0,
secs: 0,
flags: 0,
ciaddr: [0; 4],
yiaddr: [0; 4],
siaddr: [0; 4],
giaddr: [0; 4],
chaddr: [0; 16],
sname: [0; 64],
file: [0; 128],
magic: DHCP_MAGIC,
}
}
}
const FIXED_PART_SIZE: usize = core::mem::size_of::<DhcpPacket>();
const OPTIONS_SIZE: usize = 335;
const DHCP_PACKET_SIZE: usize = FIXED_PART_SIZE + OPTIONS_SIZE + 1;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
struct LeaseEntry {
ip: Ipv4Addr,
mac: [u8; 6],
expires_at: u64,
}
pub struct DHCPServerBuffers {
rx_buffer: [u8; SOCKET_BUFFER_SIZE],
tx_buffer: [u8; SOCKET_BUFFER_SIZE],
rx_meta: [PacketMetadata; 16],
tx_meta: [PacketMetadata; 16],
}
impl DHCPServerBuffers {
#[must_use]
pub const fn new() -> Self {
Self {
rx_buffer: [0; SOCKET_BUFFER_SIZE],
tx_buffer: [0; SOCKET_BUFFER_SIZE],
rx_meta: [PacketMetadata::EMPTY; 16],
tx_meta: [PacketMetadata::EMPTY; 16],
}
}
}
impl Default for DHCPServerBuffers {
fn default() -> Self {
Self::new()
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum TransactionEvent {
Leased(Ipv4Addr, [u8; 6]),
Released(Ipv4Addr, [u8; 6]),
}
pub struct DHCPServerSocket<'a> {
socket: UdpSocket<'a>,
}
impl<'a> DHCPServerSocket<'a> {
#[must_use]
pub fn new(stack: Stack<'a>, buffers: &'a mut DHCPServerBuffers) -> Self {
let mut socket = UdpSocket::new(
stack,
&mut buffers.rx_meta,
&mut buffers.rx_buffer,
&mut buffers.tx_meta,
&mut buffers.tx_buffer,
);
socket.bind(DHCP_SERVER_PORT).unwrap();
Self { socket }
}
}
pub struct DhcpServer<
const MAX_CLIENTS: usize = DEFAULT_MAX_CLIENTS,
const MAX_DNS: usize = DEFAULT_MAX_DNS_SERVERS,
> {
config: DhcpConfig<MAX_DNS>,
leases: IndexMap<[u8; 6], LeaseEntry, BuildHasherDefault<FnvHasher>, MAX_CLIENTS>,
}
impl<const MAX_CLIENTS: usize, const MAX_DNS: usize> DhcpServer<MAX_CLIENTS, MAX_DNS> {
#[must_use]
pub fn new(
server_ip: Ipv4Addr,
subnet_mask: Ipv4Addr,
router: Ipv4Addr,
dns_server: Ipv4Addr,
ip_pool_start: Ipv4Addr,
ip_pool_end: Ipv4Addr,
) -> Self {
let mut dns_servers = heapless::Vec::new();
let _ = dns_servers.push(dns_server);
let config = DhcpConfig::<MAX_DNS> {
server_ip,
subnet_mask,
router: Some(router),
dns_servers,
ip_pool_start,
ip_pool_end,
lease_time: DEFAULT_LEASE_TIME,
};
Self {
config,
leases: IndexMap::new(),
}
}
#[must_use]
pub const fn with_config(config: DhcpConfig<MAX_DNS>) -> Self {
Self {
config,
leases: IndexMap::new(),
}
}
#[must_use]
pub const fn config(&self) -> &DhcpConfig<MAX_DNS> {
&self.config
}
#[must_use]
pub fn lease_count(&self) -> usize {
self.leases.len()
}
pub fn purge_expired_leases(&mut self) {
let now = embassy_time::Instant::now().as_millis();
let mut expired: Vec<[u8; 6], MAX_CLIENTS> = Vec::new();
for (mac, entry) in &self.leases {
if entry.expires_at <= now {
let _ = expired.push(*mac);
}
}
for mac in &expired {
self.leases.remove(mac);
}
}
#[must_use]
pub fn is_pool_full(&self) -> bool {
let pool_size =
u32::from(self.config.ip_pool_end) - u32::from(self.config.ip_pool_start) + 1;
self.leases.len() >= (pool_size as usize).min(MAX_CLIENTS)
}
pub fn get_next_available_ip(&self) -> Option<Ipv4Addr> {
let start = u32::from(self.config.ip_pool_start);
let end = u32::from(self.config.ip_pool_end);
(start..=end)
.map(Ipv4Addr::from)
.find(|ip| !self.leases.values().any(|lease| lease.ip == *ip))
}
fn parse_message_type(options: &[u8]) -> Option<u8> {
let mut i = 0;
while i < options.len() {
match options[i] {
OPTION_END => break,
OPTION_MESSAGE_TYPE if i + 2 < options.len() => return Some(options[i + 2]),
_ => {
if i + 1 < options.len() {
i += options[i + 1] as usize + 2;
} else {
break;
}
}
}
}
None
}
fn add_options(&self, packet: &mut Vec<u8, DHCP_PACKET_SIZE>, msg_type: u8) {
packet
.extend_from_slice(&[OPTION_MESSAGE_TYPE, 1, msg_type])
.ok();
packet.extend_from_slice(&[OPTION_SERVER_ID, 4]).ok();
packet
.extend_from_slice(&self.config.server_ip.octets())
.ok();
packet.extend_from_slice(&[OPTION_SUBNET_MASK, 4]).ok();
packet
.extend_from_slice(&self.config.subnet_mask.octets())
.ok();
if let Some(router) = self.config.router {
packet.extend_from_slice(&[OPTION_ROUTER, 4]).ok();
packet.extend_from_slice(&router.octets()).ok();
}
if !self.config.dns_servers.is_empty() {
let dns_len = self.config.dns_servers.len() * 4;
let dns_len_u8 = u8::try_from(dns_len).unwrap_or_default();
packet
.extend_from_slice(&[OPTION_DNS_SERVER, dns_len_u8])
.ok();
for dns in &self.config.dns_servers {
packet.extend_from_slice(&dns.octets()).ok();
}
}
packet.extend_from_slice(&[OPTION_LEASE_TIME, 4]).ok();
packet
.extend_from_slice(&self.config.lease_time.to_be_bytes())
.ok();
packet.extend_from_slice(&[OPTION_END]).ok();
}
fn make_response(&mut self, req: &DhcpPacket, msg_type: u8) -> Vec<u8, DHCP_PACKET_SIZE> {
let mut resp = DhcpPacket {
op: 2, xid: req.xid,
htype: 1,
hlen: 6,
magic: DHCP_MAGIC,
..Default::default()
};
resp.chaddr[..6].copy_from_slice(&req.chaddr[..6]);
let mac = req.chaddr[..6].try_into().unwrap_or([0; 6]);
match msg_type {
DHCP_OFFER => {
if let Some(ip) = self.get_next_available_ip() {
resp.yiaddr = ip.octets();
let reservation = LeaseEntry {
ip,
mac,
expires_at: embassy_time::Instant::now().as_millis() + OFFER_RESERVATION_MS,
};
let _ = self.leases.insert(mac, reservation);
}
}
DHCP_ACK => {
if let Some(lease) = self.leases.get(&mac) {
resp.yiaddr = lease.ip.octets();
} else if let Some(ip) = self.get_next_available_ip() {
resp.yiaddr = ip.octets();
}
let ip = Ipv4Addr::from(resp.yiaddr);
let lease = LeaseEntry {
ip,
mac,
expires_at: embassy_time::Instant::now().as_millis()
+ (u64::from(self.config.lease_time) * 1000),
};
let _ = self.leases.insert(mac, lease);
}
_ => {}
}
let mut bytes = Vec::<u8, DHCP_PACKET_SIZE>::new();
let resp_bytes: [u8; FIXED_PART_SIZE] = unsafe { core::mem::transmute(resp) };
bytes.extend_from_slice(&resp_bytes).ok();
self.add_options(&mut bytes, msg_type);
bytes
}
#[allow(clippy::future_not_send)]
async fn handle_packet(
&mut self,
socket: &DHCPServerSocket<'_>,
data: &[u8],
) -> Option<TransactionEvent> {
self.purge_expired_leases();
if data.len() < FIXED_PART_SIZE {
return None;
}
let packet = unsafe { core::ptr::read_unaligned(data.as_ptr().cast::<DhcpPacket>()) };
if packet.magic != DHCP_MAGIC {
return None;
}
let options = &data[FIXED_PART_SIZE..];
let msg_type = Self::parse_message_type(options)?;
let (resp, event) = match msg_type {
DHCP_DISCOVER => (Some(self.make_response(&packet, DHCP_OFFER)), None),
DHCP_REQUEST => {
let resp = self.make_response(&packet, DHCP_ACK);
let mac: [u8; 6] = packet.chaddr[..6].try_into().unwrap_or([0; 6]);
let event = self
.leases
.get(&mac)
.map(|entry| TransactionEvent::Leased(entry.ip, mac));
(Some(resp), event)
}
DHCP_RELEASE => {
let mac: [u8; 6] = packet.chaddr[..6].try_into().unwrap_or([0; 6]);
let entry = self.leases.remove(&mac);
return entry.map(|e| TransactionEvent::Released(e.ip, e.mac));
}
_ => return None,
};
if let Some(resp) = resp {
let meta = embassy_net::udp::UdpMetadata {
endpoint: (Ipv4Addr::BROADCAST, DHCP_CLIENT_PORT).into(),
local_address: None,
meta: PacketMeta::default(),
};
let _ = socket.socket.send_to(&resp, meta).await;
}
event
}
#[allow(clippy::future_not_send)]
pub async fn lease_one(
&mut self,
socket: &mut DHCPServerSocket<'_>,
) -> Result<TransactionEvent, RecvError> {
loop {
let mut buf = [0u8; DHCP_PACKET_SIZE];
match socket.socket.recv_from(&mut buf).await {
Ok((len, _)) => {
if let Some(event) = self.handle_packet(socket, &buf[..len]).await {
socket.socket.flush().await;
return Ok(event);
}
}
Err(e) => return Err(e),
}
}
}
#[allow(clippy::future_not_send)]
pub async fn run(&mut self, stack: Stack<'_>) -> ! {
self.run_with_callback(stack, |_| {}).await
}
#[allow(clippy::future_not_send)]
pub async fn run_with_callback<F>(&mut self, stack: Stack<'_>, mut callback: F) -> !
where
F: FnMut(TransactionEvent),
{
let mut buffers = DHCPServerBuffers::new();
let mut socket = DHCPServerSocket::new(stack, &mut buffers);
loop {
let mut buf = [0u8; DHCP_PACKET_SIZE];
match socket.socket.recv_from(&mut buf).await {
Ok((len, _)) => {
if let Some(event) = self.handle_packet(&socket, &buf[..len]).await {
callback(event);
}
socket.socket.flush().await;
}
Err(_) => Timer::after(Duration::from_millis(100)).await,
}
}
}
}
#[cfg(test)]
mod tests {
use core::net::Ipv4Addr;
type TestServer = super::DhcpServer<2, 2>;
type TestConfig = super::DhcpConfig<2>;
type TestBuilder = super::DhcpConfigBuilder<2>;
#[test]
fn config_builder_basic() {
let config = TestBuilder::new()
.server_ip(Ipv4Addr::new(10, 0, 0, 1))
.subnet_mask(Ipv4Addr::new(255, 255, 255, 0))
.router(Ipv4Addr::new(10, 0, 0, 254))
.add_dns_server(Ipv4Addr::new(8, 8, 8, 8))
.ip_pool(Ipv4Addr::new(10, 0, 0, 100), Ipv4Addr::new(10, 0, 0, 200))
.lease_time(3600)
.build();
assert_eq!(config.server_ip, Ipv4Addr::new(10, 0, 0, 1));
assert_eq!(config.subnet_mask, Ipv4Addr::new(255, 255, 255, 0));
assert_eq!(config.router, Some(Ipv4Addr::new(10, 0, 0, 254)));
assert_eq!(config.dns_servers.len(), 1);
assert_eq!(config.dns_servers[0], Ipv4Addr::new(8, 8, 8, 8));
assert_eq!(config.ip_pool_start, Ipv4Addr::new(10, 0, 0, 100));
assert_eq!(config.ip_pool_end, Ipv4Addr::new(10, 0, 0, 200));
assert_eq!(config.lease_time, 3600);
}
#[test]
fn config_builder_starts_with_no_dns() {
let config = TestBuilder::new().build();
assert!(config.dns_servers.is_empty());
}
#[test]
fn config_builder_no_router() {
let config = TestBuilder::new().no_router().build();
assert_eq!(config.router, None);
}
#[test]
fn server_new() {
let server = TestServer::new(
Ipv4Addr::new(192, 168, 1, 1),
Ipv4Addr::new(255, 255, 255, 0),
Ipv4Addr::new(192, 168, 1, 1),
Ipv4Addr::new(8, 8, 4, 4),
Ipv4Addr::new(192, 168, 1, 100),
Ipv4Addr::new(192, 168, 1, 200),
);
let config = server.config();
assert_eq!(config.server_ip, Ipv4Addr::new(192, 168, 1, 1));
assert_eq!(config.dns_servers.len(), 1);
assert_eq!(config.dns_servers[0], Ipv4Addr::new(8, 8, 4, 4));
}
#[test]
fn ip_pool_full() {
let mut server = TestServer::new(
Ipv4Addr::new(10, 0, 0, 1),
Ipv4Addr::new(255, 255, 255, 0),
Ipv4Addr::new(10, 0, 0, 1),
Ipv4Addr::new(1, 1, 1, 1),
Ipv4Addr::new(10, 0, 0, 100),
Ipv4Addr::new(10, 0, 0, 101),
);
for i in 0..2 {
let mac = [0, 0, 0, 0, 0, i];
let lease = super::LeaseEntry {
ip: Ipv4Addr::new(10, 0, 0, 100 + i),
mac,
expires_at: u64::MAX,
};
let _ = server.leases.insert(mac, lease);
}
assert!(server.is_pool_full());
}
#[test]
fn config_default_values() {
let config = TestConfig::default();
assert_eq!(config.server_ip, Ipv4Addr::new(192, 168, 1, 1));
assert_eq!(config.subnet_mask, Ipv4Addr::new(255, 255, 255, 0));
assert_eq!(config.router, Some(Ipv4Addr::new(192, 168, 1, 1)));
assert_eq!(config.dns_servers.len(), 1);
assert_eq!(config.dns_servers[0], Ipv4Addr::new(8, 8, 8, 8));
assert_eq!(config.ip_pool_start, Ipv4Addr::new(192, 168, 1, 100));
assert_eq!(config.ip_pool_end, Ipv4Addr::new(192, 168, 1, 200));
assert_eq!(config.lease_time, super::DEFAULT_LEASE_TIME);
}
#[test]
fn get_next_available_ip_empty() {
let server = TestServer::new(
Ipv4Addr::new(10, 0, 0, 1),
Ipv4Addr::new(255, 255, 255, 0),
Ipv4Addr::new(10, 0, 0, 1),
Ipv4Addr::new(1, 1, 1, 1),
Ipv4Addr::new(10, 0, 0, 100),
Ipv4Addr::new(10, 0, 0, 101),
);
assert_eq!(
server.get_next_available_ip(),
Some(Ipv4Addr::new(10, 0, 0, 100))
);
}
#[test]
fn get_next_available_ip_skips_leased() {
let mut server = TestServer::new(
Ipv4Addr::new(10, 0, 0, 1),
Ipv4Addr::new(255, 255, 255, 0),
Ipv4Addr::new(10, 0, 0, 1),
Ipv4Addr::new(1, 1, 1, 1),
Ipv4Addr::new(10, 0, 0, 100),
Ipv4Addr::new(10, 0, 0, 101),
);
let mac = [0, 0, 0, 0, 0, 1];
let lease = super::LeaseEntry {
ip: Ipv4Addr::new(10, 0, 0, 100),
mac,
expires_at: u64::MAX,
};
let _ = server.leases.insert(mac, lease);
assert_eq!(
server.get_next_available_ip(),
Some(Ipv4Addr::new(10, 0, 0, 101))
);
}
#[test]
fn parse_message_type_discover() {
let options = [
super::OPTION_MESSAGE_TYPE,
1,
super::DHCP_DISCOVER,
super::OPTION_END,
];
assert_eq!(
TestServer::parse_message_type(&options),
Some(super::DHCP_DISCOVER)
);
}
#[test]
fn parse_message_type_empty() {
assert_eq!(TestServer::parse_message_type(&[]), None);
}
#[test]
fn parse_message_type_end_only() {
assert_eq!(TestServer::parse_message_type(&[super::OPTION_END]), None);
}
}