dora is a DHCP server written in Rust using tokio. It is built on the dhcproto library. Lease and management state is stored in PostgreSQL (split services) or in an embedded redb file (dora-embedded, for routers and other single hosts — see docs/embedded.md). The goal of dora is to provide a complete, performant, and correct implementation of DHCPv4, and eventually DHCPv6. Dora supports duplicate address detection, ping, binding multiple interfaces, static addresses, client classes, DDNS (new!), metrics and leases HTTP API see example.yaml for all options.
It is, however, in development and may contain bugs. We hope to build a community around this project. To that end, PRs, issues, and constructive comments are welcome.
You can see all the options available by looking through example.yaml. dora will parse equivalent JSON or YAML formats of the schema.
If started on non-default dhcp port, it is assumed this is for testing, and dora will unicast any response back rather than following the RFC.
see example.yaml for all available options.
The split services store their state in PostgreSQL. Their SQL queries are
checked against the database at compile time by sqlx, but a checked-in
offline query cache (the workspace-root .sqlx/ directory) means you do not
need a database to build — the build sets SQLX_OFFLINE=true (see
.env/CI). You only need a running Postgres to run those services or the
Postgres part of the test suite. dora-embedded needs no database at all.
To build, just use cargo (below). To run against a local Postgres, create a
database and point DATABASE_URL at it, then apply the schema once with
dora-migrate (the services no longer migrate on startup):
# example: a local dev database
createdb dora # or: psql -c 'CREATE DATABASE dora;'
export DATABASE_URL=postgres://user:pass@localhost/dora
DATABASE_URL (or -d/--database-url) is the connection string dora uses. To
work on the queries with compile-time checking against a live DB, install
sqlx-cli (with the postgres feature),
unset SQLX_OFFLINE, run sqlx migrate run, and regenerate the offline cache
with cargo sqlx prepare after changing a query.
Use standard cargo subcommands to build (with the --release flag for no debug symbols):
cargo build
dora is split into one binary per service, all in crates/bins:
| binary | role |
|---|---|
dora-v4 |
DHCPv4 server |
dora-v6 |
DHCPv6 server |
dora-api |
management/observability HTTP API |
dora-migrate |
run-once database schema migrator |
dora-embedded |
v4 + v6 + API in one process on an embedded redb file (no PostgreSQL) |
The services share the same CLI flags (config path, database URL, listen addresses, …). By default a server binds privileged ports, which may require sudo. Run help for any of them:
cargo run --bin dora-v4 -- --help
For an architecture overview (crates, services, deployment) see docs/architecture.md
To build and run dora in docker see docs/docker.md
To deploy dora on Kubernetes/K3s with Cilium see docs/kubernetes_deploy.md
dora requires a config file to start. See example.yaml for all available options.
Use DORA_LOG env var for adjusting log level and which targets, see here for more options.
(assuming you have a Postgres reachable via DATABASE_URL)
First apply the database schema (the services no longer migrate on startup —
dora-migrate owns the schema), then run a service. To bring up the v4 server
bound to the default v4 addr (0.0.0.0:67) with a particular config:
# once, to create/upgrade the schema
cargo run --bin dora-migrate -- -d postgres://user:pass@localhost/dora
# then the v4 server (dora-v6 / dora-api start the same way)
cargo run --bin dora-v4 -- -c path/to/config.json -d postgres://user:pass@localhost/dora
No Postgres: dora-embedded runs the v4 server, the v6 server (if configured)
and the management API together on one redb file. The store is created and
upgraded on start, so there is no migrate step. The DATABASE_URL scheme
selects the backend: redb:///abs/path (three slashes) here, postgres://
for the split services.
cargo run -p embedded --bin dora-embedded -- -c path/to/config.yaml -d redb:///var/lib/dora/dora.redb
See docs/embedded.md for durability (--redb-sync),
flash-wear and backup guidance.
cargo build
optional: use --release flag for optimized binary without debug symbols
cargo build builds the whole workspace; the service binaries will be present at
target/{debug,release}/{dora-v4,dora-v6,dora-api,dora-migrate,dora-embedded}
For an dora-embedded binary that does not link the Postgres client at all,
build the package on its own (cargo unifies features across a workspace-wide
build):
cargo build --release -p embedded --bin dora-embedded
There is a project called cross that does most of the heavy lifting and will build everything in a container, this is the first thing to try. Note that you will need either docker or podman, so we recommend that you install docker if you have not yet done so.
cargo install cross
# build each service you need; dora-v4 shown, swap in dora-v6/dora-api/dora-migrate
cross build --target armv7-unknown-linux-gnueabihf --bin dora-v4 --release
# the embedded single-process server for a router (no Postgres client linked)
cross build --target aarch64-unknown-linux-gnu -p embedded --bin dora-embedded --release
Note Remember to pass --release to cross if you want an optimized version of the binary
You can compile for the musl target also, although it will not have jemallocator:
cross build --target armv7-unknown-linux-musleabihf --bin dora-v4 --release
If that works, you should have a dora-v4 binary in target/armv7-unknown-linux-gnueabihf/release/dora-v4 or target/armv7-unknown-linux-musleabihf/release/dora-v4
Firstly, you need the ARM toolchain from rustup:
rustup target add armv7-unknown-linux-gnueabihf
Notice that .cargo/config.toml has an entry for replacing the linker when cross compiling to ARM:
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
This means arm-linux-gnueabihf-gcc must be available on the system and will be used as the linker. Once you have it installed, you can produce an ARMv7 binary using:
TARGET_CC=arm-linux-gnueabihf-gcc TARGET_AR=arm-linux-gnueabihf-gcc-ar cargo build --target=armv7-unknown-linux-gnueabihf --bin dora-v4
Every service shares the same options; run cargo run --bin dora-v4 -- --help
(or dora-v6 / dora-api / dora-migrate / dora-embedded) to see them. Each
flag also has an environment-variable form (see example.yaml
for config options). -d / --database-url / DATABASE_URL selects the store
by URL scheme (postgres://… or redb:///…); dora-embedded adds
--redb-sync and --redb-flush-ms.
dora uses the tracing library for stdout logs.
There is a tool included in the workspace called dora-cfg, you can run it with:
cargo run --bin dora-cfg -- <args>
It will pretty-print the internal dora config representation as well as parse the wire format so hex encoded values are human-readable.
dora serves a JSON management API. By default it binds to 127.0.0.1:3333
(override with --external-api / EXTERNAL_API). The full contract is the
OpenAPI 3.1 document in docs/openapi.yaml, also served at
GET /openapi.json.
Public (unauthenticated): GET /health, GET /ready, GET /openapi.json.
Everything else requires a Bearer token (DORA_API_TOKEN) or verified mTLS client
certificate. Trusted local development can explicitly opt out by setting
DORA_API_ALLOW_UNAUTHENTICATED=true. Current
endpoints:
GET /health
GET /ready
GET /openapi.json
GET /v1/server
GET /v1/metrics (also /v1/metrics/summary, /v1/metrics/prometheus)
GET /metrics, /metrics-text (Prometheus scrape, authenticated)
GET /v1/leases/v4 (pagination, filters, sort)
GET /v1/leases/v6
GET /v1/reservations/v4
GET /v1/reservations/v6
GET /v1/config (structured, secrets redacted)
Every response carries an X-Request-ID header; errors use the envelope
{ "error": { "code", "message", "request_id" } }.
❯ curl -s 127.0.0.1:3333/v1/leases/v4 | jq
{
"meta": { "limit": 100, "offset": 0, "total": 1, "count": 1, "filters": {}, "sort": ["ip"] },
"items": [
{
"family": "v4",
"state": "leased",
"ip": "192.168.5.2",
"network": "192.168.5.0/24",
"client_id": "c08fd9962fc1",
"expires_at": "2025-04-06T18:22:21+00:00",
"source": "database"
}
]
}-
see dhcproto for protocol level support
In synthetic tests with perfdhcp I was able to get to around 5000 leases/sec, but dora was nowhere near close to consuming available CPU. dora keeps no leases in memory at the moment. It relies totally on the database in order to determine which is the next IP to allocate within a range. The db workload is fairly write-heavy, so throughput is bounded by the database and its round-trip latency.
We could go much faster by keeping leases in memory and appending to the db like more traditional DHCP implementations, but this is a trade-off for complexity. I've experimented with the bitmap from roaring-rs and it seems pretty fast, although we'd need logic to reload the database into memory again on startup and be able to evict entries after lease expiration. Additional complexity we don't care for at the moment. There may be other ways to squeeze more performance out without having to go down this road.
dhcpm is a tool built in rust that that will mock dhcp requests and is highly useful for testing dhcp in an isolated manner.
perfdhcp can be used to test dora, include giaddr, the subnet select option or the relay agent link selection opt, you can use this as a starting point:
perfdhcp is a component of kea-admin so you'll need to install it to get the binary:
Ubuntu/Debian:
sudo apt-get install kea-admin
sudo perfdhcp -4 -N 9900 -L 9903 -r 1 -xi -t 1 -o 118,C0A80001 -R 100 127.0.0.1
This will start perfdhcp using dhcpv4, send messages to 127.0.0.1:9900, listen on port 9903 at a rate of 1/sec, and using 100 different devices. It includes the subnet select opt (118) with C0A80001 as a hex encoded value of the integer of 192.168.0.1. dora must be listening on 9900 and have a config with ranges to allocate on the 192.168.0.1 network.
See PI setup
If you find a bug, or see something that doesn't look right, please open an issue and let us know. We welcome any and all constructive feedback.
We're still actively working on this. Some of the things we'd like to add in the future include: DDNS updates, stateful DHCPv6, HA & Client classification.
