#![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 DEFAULT_SOCKET_BUFFER_SIZE: usize = 1024;
#[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,
pub socket_buffer_size: usize,
}
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,
socket_buffer_size: DEFAULT_SOCKET_BUFFER_SIZE,
}
}
}
#[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 {
Self {
config: DhcpConfig::default(),
}
}
#[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 const fn socket_buffer_size(mut self, size: usize) -> Self {
self.config.socket_buffer_size = size;
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 END_OPTIONS_MARK_SIZE: usize = 1; const OPTIONS_SIZE: usize = 335;
const DHCP_PACKET_SIZE: usize = FIXED_PART_SIZE + OPTIONS_SIZE + END_OPTIONS_MARK_SIZE;
#[allow(dead_code)]
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
struct LeaseEntry {
ip: Ipv4Addr,
mac: [u8; 6],
lease_time: u64, }
pub struct DHCPServerBuffers {
rx_buffer: [u8; DEFAULT_SOCKET_BUFFER_SIZE],
tx_buffer: [u8; DEFAULT_SOCKET_BUFFER_SIZE],
rx_meta: [PacketMetadata; 16],
tx_meta: [PacketMetadata; 16],
}
impl DHCPServerBuffers {
#[must_use]
pub const fn new() -> Self {
Self {
rx_buffer: [0; DEFAULT_SOCKET_BUFFER_SIZE],
tx_buffer: [0; DEFAULT_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 const fn new(
server_ip: Ipv4Addr,
subnet_mask: Ipv4Addr,
router: Ipv4Addr,
_dns_server: Ipv4Addr, ip_pool_start: Ipv4Addr,
ip_pool_end: Ipv4Addr,
) -> Self {
let config = DhcpConfig::<MAX_DNS> {
server_ip,
subnet_mask,
router: Some(router),
dns_servers: heapless::Vec::new(),
ip_pool_start,
ip_pool_end,
lease_time: DEFAULT_LEASE_TIME,
socket_buffer_size: DEFAULT_SOCKET_BUFFER_SIZE,
};
Self {
config,
leases: IndexMap::new(),
}
}
#[must_use]
pub fn new_with_dns(
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,
socket_buffer_size: DEFAULT_SOCKET_BUFFER_SIZE,
};
Self {
config,
leases: IndexMap::new(),
}
}
#[must_use]
pub const fn with_config(config: DhcpConfig<MAX_DNS>) -> Self {
let leases = IndexMap::new();
Self { config, leases }
}
#[must_use]
pub const fn config(&self) -> &DhcpConfig<MAX_DNS> {
&self.config
}
#[must_use]
pub fn lease_count(&self) -> usize {
self.leases.len()
}
#[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))
}
#[allow(clippy::unused_self)]
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_count = self.config.dns_servers.len() * 4; let dns_count_u8 = u8::try_from(dns_count).unwrap_or_default();
packet
.extend_from_slice(&[OPTION_DNS_SERVER, dns_count_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();
}
}
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 lease = LeaseEntry {
ip,
mac,
lease_time: 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();
unsafe {
let resp_bytes = core::slice::from_raw_parts(
(&raw const resp).cast::<u8>(),
core::mem::size_of::<DhcpPacket>(),
);
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> {
if data.len() < core::mem::size_of::<DhcpPacket>() {
return None;
}
let packet = unsafe { &*data.as_ptr().cast::<DhcpPacket>() };
if packet.magic != DHCP_MAGIC {
return None;
}
let options = &data[core::mem::size_of::<DhcpPacket>()..];
if let Some(msg_type) = Self::parse_message_type(options) {
match msg_type {
DHCP_DISCOVER => {
let resp = self.make_response(packet, DHCP_OFFER);
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;
return None;
}
DHCP_REQUEST => {
let resp = self.make_response(packet, DHCP_ACK);
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;
let mac: [u8; 6] = packet.chaddr[..6].try_into().unwrap_or([0; 6]);
return self
.leases
.get(&mac)
.map(|entry| TransactionEvent::Leased(entry.ip, mac));
}
DHCP_RELEASE => {
let mac: [u8; 6] = packet.chaddr[..6].try_into().unwrap_or([0; 6]);
let entry = self.leases.remove(&mac);
return entry.map(|entry| TransactionEvent::Released(entry.ip, entry.mac));
}
_ => {
return None;
}
}
}
None
}
#[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<'_>) -> ! {
let mut buffers = DHCPServerBuffers::new();
let socket = DHCPServerSocket::new(stack, &mut buffers);
loop {
let mut buf = [0u8; DHCP_PACKET_SIZE];
match socket.socket.recv_from(&mut buf).await {
Ok((len, _)) => {
let _ = self.handle_packet(&socket, &buf[..len]).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 dhcp_config_builder_basic() {
let config = TestBuilder::new()
.clear_dns_servers()
.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)
.socket_buffer_size(2048)
.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);
assert_eq!(config.socket_buffer_size, 2048);
}
#[test]
fn dhcp_server_new_with_dns() {
let server = TestServer::new_with_dns(
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 dhcp_server_ip_pool_full() {
let mut server = TestServer::new_with_dns(
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,
lease_time: 123_456,
};
let _ = server.leases.insert(mac, lease);
}
assert!(server.is_pool_full());
}
#[test]
fn dhcp_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);
assert_eq!(config.socket_buffer_size, super::DEFAULT_SOCKET_BUFFER_SIZE);
}
}