[<img alt="github" src="https://img.shields.io/badge/github-rttfd/leasehund-37a8e0?style=for-the-badge&labelColor=555555&logo=github" height="20">](https://github.com/rttfd/leasehund)
[<img alt="crates.io" src="https://img.shields.io/crates/v/leasehund.svg?style=for-the-badge&color=ff8b94&logo=rust" height="20">](https://crates.io/crates/leasehund)
[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-leasehund-bedc9c?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/leasehund)

![Dall-E generated leasehund image](https://raw.githubusercontent.com/rttfd/static/refs/heads/main/leasehund/leasehund.jpeg)

# 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`:

```toml
[dependencies]
leasehund = "0.5"
```

## Usage

```rust
#![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:

```rust
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:

```rust
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()`:

```rust
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](https://www.rfc-editor.org/rfc/rfc2131) and [RFC 2132](https://www.rfc-editor.org/rfc/rfc2132). All DHCP packets include and check the required DHCP magic cookie (0x63825363, see [RFC 2132 section 2](https://www.rfc-editor.org/rfc/rfc2132#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

```rust
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

```rust
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

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

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Acknowledgments

- Built with [Embassy](https://embassy.dev/) - Modern embedded framework for Rust
- Uses [smoltcp](https://github.com/smoltcp-rs/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!
