leaf is an authoritative DNS server for nip.io-style hostnames, implemented in Rust and hardened for internet-facing deployment.
It serves deterministic A records from encoded IPv4 names inside a configured zone, for example:
1-2-3-4.dev.example.com->1.2.3.4api.10-11-12-13.dev.example.com->10.11.12.131.2.3.4.dev.example.com->1.2.3.4
leaf is currently serving xip.kali.st in production, including names such as:
172-16-15-103.xip.kali.st->172.16.15.103
Official public webpage: https://kali.st
- Authoritative-only DNS behavior for one or more configured zones.
- UDP and TCP listeners.
- Correct negative responses with SOA authority section for
NXDOMAINandNODATA. - Apex authoritative
SOAandNSrecords. - No recursion (
RA=0). - Global and per-IP query rate limiting.
- Per-IP + qname invalid-query throttling for repeated bad lookups.
- Global and per-IP TCP connection caps.
- TCP idle/read/write timeouts.
- UDP and TCP request size bounds.
- Structured operational logs for startup and dropped requests.
- Optional per-query success logs for UDP/TCP.
leaf currently supports authoritative answers for A records derived from hostname-encoded IPv4 values.
Response policy:
OPCODE != QUERY->NOTIMPQDCOUNT != 1->FORMERR- Out-of-zone names ->
REFUSEDand non-authoritative (AA=0) - In-zone
ANY->REFUSED - In-zone existing encoded name +
A->NOERRORwith oneAanswer,AA=1 - In-zone existing encoded name + non-
A->NOERRORwith empty answer and SOA in authority (NODATA) - In-zone non-existing encoded name ->
NXDOMAINwith SOA in authority - Apex
SOA->NOERRORwith SOA answer - Apex
NS->NOERRORwith NS answer
src/main.rs: runtime orchestration, UDP/TCP loops, timeouts, logging, limit enforcement.src/config.rs: CLI/env/TOML parsing, precedence merge, and validation.src/dns.rs: authoritative DNS response logic.src/limits.rs: query and TCP connection limiter implementations.tests/e2e.rs: black-box end-to-end tests over real UDP/TCP sockets..gitlab-ci.yml: CI pipeline for lint, checks, tests, extended tests, release artifact build.Containerfile: multi-stage image build for Podman..containerignore: trimmed container build context.PRODUCTION_READINESS.md: go-live checklist and operational guidance.
- Rust toolchain (stable for local dev).
- Linux/macOS shell environment for examples below.
- For CI parity with this repo pipeline, nightly rust is used in GitLab jobs.
- Podman (optional, for containerized deployment).
cargo buildRelease build:
cargo build --releaseMinimum required configuration:
LEAF_ZONES=dev.example.com cargo runThis starts the server on 0.0.0.0:5300 by default.
LEAF_ZONE remains supported as a single-zone shorthand.
Example with explicit bind and TTL:
LEAF_ZONES=dev.example.com,prod.example.com \
LEAF_LISTEN=127.0.0.1:5300 \
LEAF_TTL=60 \
cargo runleaf supports file-based config from:
--config /path/to/leaf.toml, orLEAF_CONFIG=/path/to/leaf.toml, or- auto-load
./leaf.tomlif present
Precedence is:
- CLI flags
- Environment variables
- TOML file
Use leaf.example.toml as the template.
For zone selection:
--zonecan be provided multiple times or as a comma-separated list.LEAF_ZONESaccepts comma-separated zones.LEAF_ZONEis still supported for a single zone.- In TOML,
zones = ["dev.example.com", "prod.example.com"]is preferred; legacyzone = "..."remains supported.
Recommended leaf.toml layout:
zones = ["dev.example.com", "prod.example.com"]
listen = "0.0.0.0:5300"
[dns]
ttl = 60
# zone_ns = "ns1.dev.example.com"
# zone_hostmaster = "hostmaster.dev.example.com"
[soa]
serial = 1
refresh = 300
retry = 60
expire = 86400
minimum = 60
[limits]
global_qps_limit = 5000
per_ip_qps_limit = 200
per_ip_invalid_qname_qps_limit = 20
limiter_max_tracked_ips = 10000
invalid_qname_limiter_max_tracked_keys = 50000
tcp_max_connections = 1024
tcp_max_connections_per_ip = 64
tcp_idle_timeout_ms = 10000
tcp_read_timeout_ms = 3000
tcp_write_timeout_ms = 3000
max_tcp_frame_bytes = 4096
max_udp_request_bytes = 1232
[logging]
query_log_enabled = falseNotes:
- Top-level flat keys are still accepted for backward compatibility.
- If
dns.zone_ns/dns.zone_hostmasterare omitted, defaults are derived per zone (ns1.<zone>,hostmaster.<zone>). - Set
[logging].query_log_enabled = true(orLEAF_LOG_QUERIES=true) to enable per-query log events. - Per-query events are emitted at
infolevel.
Build image:
podman build -t leaf:latest -f Containerfile .Run on high port (works well for rootless local validation):
podman run --rm --name leaf \
-e LEAF_ZONES=dev.example.com,prod.example.com \
-p 5300:5300/udp \
-p 5300:5300/tcp \
leaf:latestRun on public DNS port 53 (rootful Podman recommended):
sudo podman run -d --name leaf --restart=always \
--read-only \
--cap-drop=all \
--cap-add=NET_BIND_SERVICE \
-e LEAF_ZONES=dev.example.com,prod.example.com \
-e LEAF_LISTEN=0.0.0.0:53 \
-e LEAF_CONFIG=/etc/leaf/leaf.toml \
-v ./leaf.toml:/etc/leaf/leaf.toml:ro \
-p 53:53/udp \
-p 53:53/tcp \
leaf:latestNotes:
- Rootless Podman usually cannot bind low ports like
53without host tuning. - On Hetzner, allow inbound
53/udpand53/tcpin host and cloud firewall policy.
All options are available via CLI flags and environment variables. For TOML, you can use either flat top-level keys (legacy) or the structured layout shown above.
| Variable | Default | Description |
|---|---|---|
LEAF_CONFIG |
none | Path to TOML config file (same as --config) |
LEAF_ZONES |
required unless LEAF_ZONE is set |
Comma-separated authoritative zones (for example dev.example.com,prod.example.com) |
LEAF_ZONE |
optional | Backward-compatible single-zone shortcut |
LEAF_LISTEN |
0.0.0.0:5300 |
Bind address and port for UDP+TCP |
LEAF_TTL |
60 |
TTL for positive answers |
LEAF_ZONE_NS |
ns1.<zone> |
Zone apex NS target |
LEAF_ZONE_HOSTMASTER |
hostmaster.<zone> |
SOA RNAME-like mailbox domain |
LEAF_SOA_SERIAL |
1 |
SOA serial |
LEAF_SOA_REFRESH |
300 |
SOA refresh |
LEAF_SOA_RETRY |
60 |
SOA retry |
LEAF_SOA_EXPIRE |
86400 |
SOA expire |
LEAF_SOA_MINIMUM |
60 |
SOA minimum TTL, used in negative authority responses |
LEAF_GLOBAL_QPS_LIMIT |
5000 |
Global fixed-window query cap (1s window) |
LEAF_PER_IP_QPS_LIMIT |
200 |
Per-IP fixed-window query cap (1s window) |
LEAF_PER_IP_INVALID_QNAME_QPS_LIMIT |
20 |
Per-IP + qname fixed-window cap for invalid responses (NXDOMAIN/REFUSED/FORMERR) |
LEAF_LIMITER_MAX_TRACKED_IPS |
10000 |
Max distinct IPs tracked per limiter window |
LEAF_INVALID_QNAME_LIMITER_MAX_TRACKED_KEYS |
50000 |
Max distinct ip+qname keys tracked in invalid-query limiter window |
LEAF_TCP_MAX_CONNECTIONS |
1024 |
Global concurrent TCP connection cap |
LEAF_TCP_MAX_CONNECTIONS_PER_IP |
64 |
Per-IP concurrent TCP connection cap |
LEAF_TCP_IDLE_TIMEOUT_MS |
10000 |
Timeout waiting for next frame prefix |
LEAF_TCP_READ_TIMEOUT_MS |
3000 |
Timeout while reading frame payload |
LEAF_TCP_WRITE_TIMEOUT_MS |
3000 |
Timeout writing framed response |
LEAF_MAX_TCP_FRAME_BYTES |
4096 |
Max accepted incoming TCP DNS frame length |
LEAF_MAX_UDP_REQUEST_BYTES |
1232 |
Max accepted incoming UDP DNS payload |
LEAF_LOG_QUERIES |
false |
Emit per-query success logs (event=udp_query/event=tcp_query) without client IP or qname |
TOML key mapping in structured layout:
LEAF_ZONES->zones = ["..."]LEAF_LISTEN->listen = "ip:port"LEAF_TTL->[dns] ttl = ...LEAF_ZONE_NS->[dns] zone_ns = "..."LEAF_ZONE_HOSTMASTER->[dns] zone_hostmaster = "..."LEAF_SOA_*->[soa] ...LEAF_GLOBAL_QPS_LIMIT,LEAF_PER_IP_QPS_LIMIT,LEAF_PER_IP_INVALID_QNAME_QPS_LIMIT->[limits] ...LEAF_LIMITER_MAX_TRACKED_IPS,LEAF_INVALID_QNAME_LIMITER_MAX_TRACKED_KEYS->[limits] ...LEAF_TCP_*,LEAF_MAX_TCP_FRAME_BYTES,LEAF_MAX_UDP_REQUEST_BYTES->[limits] ...LEAF_LOG_QUERIES->[logging] query_log_enabled = ...(legacy top-levellog_queries = ...is also accepted)
# Positive A lookup
dig @127.0.0.1 -p 5300 1-2-3-4.dev.example.com A +norecurse
# Apex SOA
dig @127.0.0.1 -p 5300 dev.example.com SOA +norecurse
# Apex NS
dig @127.0.0.1 -p 5300 dev.example.com NS +norecurse
# NXDOMAIN with SOA authority
dig @127.0.0.1 -p 5300 nope.dev.example.com A +norecurseleaf logs to stderr as structured key/value lines.
Always logged:
- startup events (
event=startup) - dropped/blocked traffic (
event=udp_drop,event=tcp_drop) - TCP handler failures (
event=tcp_connection_error)
Optional per-query success logging:
- Set
LEAF_LOG_QUERIES=trueor[logging] query_log_enabled = true. - Emits one structured event per answered UDP request with
event=udp_query. - Emits one structured event per answered TCP request with
event=tcp_query. - Query logs intentionally omit client IP and full qname for data minimization.
- Startup events are
info, dropped/invalid traffic iswarn, and handler failures areerror.
For Podman:
sudo podman logs -f leafRun all checks locally:
cargo fmt --all -- --check
cargo check --all-targets --all-features --locked
cargo clippy --all-targets --all-features --locked -- -D warnings
cargo test --locked
cargo test --all-targets --all-features --release --lockedTest coverage currently includes:
- Unit tests for parser/config/limiter and DNS logic.
- End-to-end integration tests that spawn the real
leafbinary and query it over UDP and TCP. - E2E protocol matrix coverage for apex records, positive A synthesis, NXDOMAIN/NODATA,
ANYrefusal, out-of-zone refusal, non-QUERYopcode handling, and multi-questionFORMERR. - E2E coverage for structured TOML startup and invalid-query throttling behavior.
The GitLab pipeline (.gitlab-ci.yml) contains:
cargo_fmtcargo_checkcargo_clippycargo_testcargo_test_extendedreleasejob (tag-only) that builds Linuxamd64+arm64binaries and publishes versioned tarballs:dist/leaf-amd64-linux-${TAG}.tar.gzdist/leaf-arm64-linux-${TAG}.tar.gzdist/SHA256SUMScontainer_releasejob (tag-only) that builds and publishes multi-arch (amd64,arm64) images to Quay
Quay publish job requires these CI/CD variables:
QUAY_USERNAMEQUAY_PASSWORD
Image destination defaults to:
quay.io/cloudflavor/leaf:${GIT_COMMIT_TAG}(fallback to${CI_COMMIT_TAG}in GitLab)quay.io/cloudflavor/leaf:latest
Both tags are published as a multi-arch manifest list.
Local pipeline emulation with opal:
opal run --no-tuiUse PRODUCTION_READINESS.md as the deployment checklist before public cutover.
Minimum production expectations:
- Run as non-root.
- Bind port 53 via
CAP_NET_BIND_SERVICEinstead of root. - Expose only
53/udpand53/tcp. - Validate behavior from external networks using
dig. - Verify limiter and timeout behavior under load before full delegation.
- Shared TTL/SOA/limit settings across all configured zones.
- IPv4
Asynthesis only. - No recursive resolution.
- No DNSSEC implementation.
- No built-in metrics endpoint yet.
Apache-2.0. See LICENSE.