leasehund 0.5.2

A lightweight, embedded-friendly DHCP server implementation for Rust no_std environments
Documentation

Dall-E generated leasehund image

Leasehund

A lightweight, embedded-friendly DHCP server implementation for Rust no_std environments.

Overview

Leasehund provides a minimal DHCP server implementation designed for embedded systems and resource-constrained environments. Built on top of the Embassy async runtime, it supports the core DHCP functionality needed for automatic IP address assignment in local networks.

Features

  • No-std compatible: Designed for embedded systems without heap allocation
  • Embassy integration: Built on top of Embassy async runtime and networking stack
  • Configurable IP pools: Define custom IP address ranges for client assignment
  • Lease expiry: Expired leases are automatically reclaimed before each allocation
  • IP reservation: Offered IPs are reserved to prevent duplicate offers
  • Multiple DNS servers: Support for up to N DNS servers (compile-time const generic)
  • Optional router configuration: Router/gateway can be disabled if not needed
  • Builder pattern: Fluent API for easy configuration
  • Memory efficient: Uses heapless data structures with compile-time size limits

Quick Start

Add this to your Cargo.toml:

[dependencies]
leasehund = "0.5"

Usage

#![no_std]
#![no_main]

use core::net::Ipv4Addr;
use leasehund::DhcpServer;
use embassy_net::Stack;

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    // Initialize your embassy network stack here
    let stack = /* ... your network stack initialization ... */;

    let mut server = DhcpServer::<32, 4>::new(
        Ipv4Addr::new(192, 168, 1, 1),    // Server IP
        Ipv4Addr::new(255, 255, 255, 0),  // Subnet mask
        Ipv4Addr::new(192, 168, 1, 1),    // Router/Gateway
        Ipv4Addr::new(8, 8, 8, 8),        // DNS server
        Ipv4Addr::new(192, 168, 1, 100),  // IP pool start
        Ipv4Addr::new(192, 168, 1, 200),  // IP pool end
    );

    // Run the DHCP server (this will loop forever)
    server.run(stack).await;
}

Configuration

Basic Configuration

The DHCP server requires the following configuration parameters:

Parameter Description Example
server_ip IP address of the DHCP server 192.168.1.1
subnet_mask Network subnet mask 255.255.255.0
router Default gateway IP address 192.168.1.1
dns_server DNS server IP address 8.8.8.8
ip_pool_start First IP in the assignable range 192.168.1.100
ip_pool_end Last IP in the assignable range 192.168.1.200

Advanced Configuration (Builder Pattern)

Use the builder API for multiple DNS servers and custom options:

use core::net::Ipv4Addr;
use leasehund::{DhcpConfigBuilder, DhcpServer};

let config = DhcpConfigBuilder::<4>::new()
    .server_ip(Ipv4Addr::new(10, 0, 1, 1))
    .subnet_mask(Ipv4Addr::new(255, 255, 0, 0))
    .router(Ipv4Addr::new(10, 0, 1, 1))
    .add_dns_server(Ipv4Addr::new(1, 1, 1, 1))
    .add_dns_server(Ipv4Addr::new(1, 0, 0, 1))
    .ip_pool(Ipv4Addr::new(10, 0, 100, 1), Ipv4Addr::new(10, 0, 199, 254))
    .lease_time(7200) // 2 hours
    .build();

let server: DhcpServer<32, 4> = DhcpServer::with_config(config);

Note: The builder starts with no DNS servers. Use .add_dns_server() to add them. The maximum number of concurrent leases and DNS servers are compile-time constants set via const generics (e.g., DhcpServer::<32, 4>).

Supported DHCP Messages

Message Type Description Server Response
DISCOVER Client broadcast to find DHCP servers OFFER with available IP
REQUEST Client request for specific IP address ACK confirming lease
RELEASE Client releasing IP address Lease removal (no response)

DHCP Options Supported

The server automatically includes these standard DHCP options in responses:

  • Option 1: Subnet Mask
  • Option 3: Router (Default Gateway)
  • Option 6: Domain Name Server (DNS)
  • Option 51: IP Address Lease Time
  • Option 53: DHCP Message Type
  • Option 54: Server Identifier

Advanced Usage

In case you need to handle lease/release events of each new client you can use the lease_one method:

use core::net::Ipv4Addr;
use leasehund::{DhcpServer, DhcpConfigBuilder, DHCPServerSocket, DHCPServerBuffers, TransactionEvent};
use embassy_net::Stack;

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    // Initialize your embassy network stack here
    let stack = /* ... your network stack initialization ... */;

    let config = DhcpConfigBuilder::<4>::new()
        .server_ip(Ipv4Addr::new(10, 0, 1, 1))
        .subnet_mask(Ipv4Addr::new(255, 255, 0, 0))
        .router(Ipv4Addr::new(10, 0, 1, 1))
        .add_dns_server(Ipv4Addr::new(1, 1, 1, 1))
        .add_dns_server(Ipv4Addr::new(1, 0, 0, 1))
        .add_dns_server(Ipv4Addr::new(8, 8, 8, 8))
        .ip_pool(
            Ipv4Addr::new(10, 0, 100, 1),
            Ipv4Addr::new(10, 0, 199, 254)
        )
        .lease_time(7200)
        .build();

    let mut server: DhcpServer<32, 4> = DhcpServer::with_config(config);
    let mut buffers = DHCPServerBuffers::new();
    let mut socket = DHCPServerSocket::new(stack, &mut buffers);
    loop {
        let Ok(event) = server.lease_one(&mut socket).await else {
            continue;
        };

        match event {
            TransactionEvent::Leased(ip, mac) => {
                info!("Leased IP: {} to MAC: {:02x?}", ip, mac);
            }
            TransactionEvent::Released(ip, mac) => {
                info!("Released IP: {} from MAC: {:02x?}", ip, mac);
            }
        }
    }
}

Event Callbacks

If you want the convenience of run() but still need to react to lease events, use run_with_callback():

use core::net::Ipv4Addr;
use leasehund::{DhcpServer, DhcpConfigBuilder, TransactionEvent};
use embassy_net::Stack;

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let stack = /* ... your network stack initialization ... */;

    let config = DhcpConfigBuilder::<4>::new()
        .server_ip(Ipv4Addr::new(10, 0, 1, 1))
        .subnet_mask(Ipv4Addr::new(255, 255, 0, 0))
        .no_router()
        .add_dns_server(Ipv4Addr::new(8, 8, 8, 8))
        .ip_pool(Ipv4Addr::new(10, 0, 100, 1), Ipv4Addr::new(10, 0, 199, 254))
        .lease_time(7200)
        .build();

    let mut server: DhcpServer<32, 4> = DhcpServer::with_config(config);

    server.run_with_callback(stack, |event| {
        match event {
            TransactionEvent::Leased(ip, mac) => {
                info!("New lease: {} -> {:02x?}", ip, mac);
            }
            TransactionEvent::Released(ip, mac) => {
                info!("Lease released: {} -> {:02x?}", ip, mac);
            }
        }
    }).await;
}

This is ideal for logging, metrics, or triggering side effects (e.g., opening a firewall pinhole) when a host gets a DHCP lease.

Protocol Compliance

Leasehund is compliant with RFC 2131 and RFC 2132. All DHCP packets include and check the required DHCP magic cookie (0x63825363, see RFC 2132 section 2) for strict standards compliance.

Architecture

Memory Usage

The server uses fixed-size data structures to ensure predictable memory usage:

  • Lease Storage: FnvIndexMap with maximum entries set by const generic (e.g., DhcpServer::<32, 4>)
  • Packet Buffers: 1KB RX/TX buffers for UDP socket
  • Response Packets: Maximum 576 bytes per DHCP response

Network Protocol

  • Listen Port: UDP 67 (standard DHCP server port)
  • Client Port: UDP 68 (standard DHCP client port)
  • Broadcast: All responses sent as broadcast packets for maximum compatibility
  • Packet Format: RFC 2131 compliant DHCP packet structure

Examples

Simple Home Network

let server = DhcpServer::<32, 4>::new(
    Ipv4Addr::new(192, 168, 1, 1),    // Router IP
    Ipv4Addr::new(255, 255, 255, 0),  // /24 network
    Ipv4Addr::new(192, 168, 1, 1),    // Gateway
    Ipv4Addr::new(1, 1, 1, 1),        // Cloudflare DNS
    Ipv4Addr::new(192, 168, 1, 100),  // Pool start
    Ipv4Addr::new(192, 168, 1, 199),  // Pool end (100 addresses)
);

Corporate Network

let server = DhcpServer::<32, 4>::new(
    Ipv4Addr::new(10, 0, 1, 1),       // Server IP
    Ipv4Addr::new(255, 255, 0, 0),    // /16 network
    Ipv4Addr::new(10, 0, 1, 1),       // Gateway
    Ipv4Addr::new(10, 0, 1, 2),       // Internal DNS
    Ipv4Addr::new(10, 0, 100, 1),     // Large pool start
    Ipv4Addr::new(10, 0, 199, 254),   // Large pool end
);

Limitations

  • IPv4 Only: IPv6 is not supported
  • Lease Time: Configurable at runtime via DhcpConfig/DhcpConfigBuilder (default 24 hours)
  • Sizing: Maximum clients and DNS servers are compile-time constants set via const generics
  • Basic Options: Limited to essential DHCP options
  • No Relay: DHCP relay functionality not implemented

Requirements

  • Rust: Edition 2024 or later
  • Embassy: Compatible with Embassy async runtime
  • no_std: Fully compatible with no_std environments
  • Memory: Approximately 2KB RAM for lease storage and buffers

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development Setup

git clone https://github.com/rttfd/leasehund.git
cd leasehund
make ci

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Built with Embassy - Modern embedded framework for Rust
  • Uses smoltcp for network protocol implementation
  • Inspired by the need for lightweight DHCP servers in embedded IoT applications

Leasehund - Because every good network needs a reliable dog to fetch IP addresses!