Unbound-compatible DNS server — REST API, XDP kernel-bypass, no restart for config changes.
Most existing unbound.conf files parse without error — non-standard directives are ignored gracefully, and unsupported tuning knobs are preserved but inert (not applied) — see Unbound compatibility. Runbound adds a live REST API, AF_XDP kernel-bypass, and a browser dashboard on top.
Prior art. DNS-over-XDP is not new — Knot DNS has had an authoritative XDP mode since 3.0 (2020), and the Knot project ships
kxdpgun, an XDP DNS load generator. Runbound's contribution is the combination: a drop-in Unbound-compatible resolver with the XDP fast path on the cache/serve hot path, a live REST API (change config with no restart), and a single static musl binary — not XDP on its own.
The built-in web dashboard (no nginx, no external tooling) — live metrics, DNSSEC validation status, cache, and blocklist management, over HTTPS with a downloadable local CA:

Overview — live QPS, cache, latency, and DNSSEC validation counters

Feeds — curated blocklist presets, added and refreshed from the UI/API
More dashboard views — DNS · Blacklist · Subnets · Logs · Protection · Users · System · Settings · Account

DNS — local records & zones, live (no restart)

Blacklist — block domains (NXDOMAIN / REFUSED)

Subnets — split-horizon answers & per-subnet filtering

Protection — DDoS abuse rules, bans, ICMP XDP responder

Users — RBAC roles & per-user zone scoping

System — runtime, sync/slaves, anycast, full backup/restore

Settings — DNSSEC validation, Sovereign/Forward mode, local CA, DoT/DoH/DoQ
A drop-in Unbound-compatible DNS server with an XDP kernel-bypass fast path, a live REST API and an embedded dashboard. Everything below is built in — no plugins, single static binary.
| Feature | Notes |
|---|---|
| Drop-in Unbound config | parses unbound.conf-style syntax, incl. include: / include-toplevel: (glob) |
| Forwarding resolver | DoT-capable upstreams, query racing, per-upstream health probes |
| Sovereign recursion | iterative from the root, opt-in (#202) — no third-party resolver sees your queries; DNSSEC-validated, anti-SSRF |
| Split-horizon DNS | per-subnet answers |
| serve-stale (RFC 8767) | answer from expired cache on upstream SERVFAIL |
| Feature | Notes |
|---|---|
| DNSSEC validation | under full-recursion with dnssec-validation: yes — Bogus → SERVFAIL, AD bit |
| Authoritative DNSSEC signing | online & zero-touch — per-zone KSK+ZSK (ECDSAP256), NSEC3 authenticated denial of existence, DS surfaced via API (#201) |
| Encrypted DNS server | DoT (853) / DoH (443/RFC 8484 GET+POST) / DoQ (853) — self-signed leaf signed by a downloadable local CA (import once), or import your own; live, no restart |
| Automatic TLS | built-in ACME / Let’s Encrypt |
| DDoS abuse engine | per-client rate-limit + tarpit + bans, escalation gated to verified sources (connection-based transports TCP/DoT/DoH/DoQ, or UDP carrying a valid DNS cookie — anti-spoof); bans dropped at the XDP/kernel layer (IPv4 and IPv6 — #228); enforced on both datapaths |
| RBAC | read / dns / operator / admin API roles |
| Privacy by default | client-IP redaction, configurable retention (GDPR) |
| Tamper-evident audit log | per-entry HMAC-SHA256, SIEM-ready JSON; actor-attributed (every admin/user action), searchable in the WebUI |
| Feature | Notes |
|---|---|
| AF_XDP kernel-bypass | near-zero-syscall hot path; ~9.85 M qps single-link X710 (line-bound), ~19.4 M qps dual-link sustained flood (NIC-truth, ~24 % host CPU; 20.3 M peak under ramp) — see Performance |
| SIMD / ASM wire responder | shared by the fast and slow paths |
| Multi-NIC + IRQ/CPU auto-pinning | governor control, NIC-queue auto-sizing |
| XDP ICMP echo responder | rate-limited, with auto-ban |
| Static binary | musl, no runtime dependencies |
| Feature | Notes |
|---|---|
| Live REST API | add/block domains, zones, config — no restart |
| Embedded browser dashboard | no nginx needed |
| Block-list feed subscriptions | managed via the API |
| Real-time stats | Prometheus /metrics (queries, cache, XDP, DDoS/abuse, listener saturation, upstreams) + SSE stream |
| Master/slave replication | REST relay (HMAC + TLS-pinned) and AXFR (RFC 5936) / IXFR (RFC 1995) *¹ |
| Anycast deployment | BGP route announcement via a supervised exabgp process, health-driven route withdrawal |
| Multi-user API | per-user zone isolation |
| Webhook notifications | Slack / Discord / ntfy |
| Hot backup / restore | via the API |
| White-label UI branding | name, logo, accent colour, favicon + About-tab info via a dedicated branding.conf (#25) |
*¹ Runbound ships both REST API-driven replication and standard AXFR (RFC 5936) / IXFR (RFC 1995) zone transfers. IXFR is answered as a full AXFR (no incremental transfer). AXFR requires explicit ACL configuration — see docs/configuration.md.
Runbound is graded A by DNS-OARC's Check My DNS, an
independent DNS conformance test — 100% on Transport, Basic DNS and DNS
Features (IPv6 and TCP transport, EDNS, and well-randomised query IDs (Runbound, via
getrandom) + source ports (OS ephemeral selection, RFC 6056) for off-path spoofing
resistance). The capture below is from a production network where Runbound
is the only resolver — every client query goes through it, day in and day out — and
the end-to-end report is a straight A, including client IPv6 and RPKI-valid transit to the
authoritative servers it queries.

Check My DNS (DNS-OARC) — grade A, 100% across Transport / Basic DNS / DNS Features
- Linux x86_64 or arm64
- systemd
- Port 53 available (stop
unbound,bind9,dnsmasqorsystemd-resolvedfirst if running) curlorwget- Root access (
sudo)
curl -fsSL https://raw.githubusercontent.com/redlemonbe/Runbound/main/install.sh | sudo bashThat's it. The script:
- Downloads the latest binary for your architecture
- Creates a
runboundsystem user - Writes a default config to
/etc/runbound/runbound.conf - Generates a random API key in
/etc/runbound/env - Installs and starts the systemd service
See docs/INSTALL.md for every option (--uninstall, --purge, --help), integrity verification (SHA256 + minisign), file locations and troubleshooting.
At the end you'll see:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Version: runbound <version>
API key: a1b2c3d4... ← save this
Config: /etc/runbound/runbound.conf
Logs: journalctl -u runbound -f
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Save the API key — you'll need it to use the dashboard and the REST API.
# DNS is responding
dig @127.0.0.1 google.com
# Service status
sudo systemctl status runbound
# API is up
curl -s http://127.0.0.1:8080/api/stats \
-H "Authorization: Bearer YOUR_API_KEY" | python3 -m json.toolIf port 53 is already taken:
# Check what's using port 53
sudo ss -tlnp | grep :53
# Common culprits
sudo systemctl stop unbound # Unbound
sudo systemctl stop bind9 # BIND9
sudo systemctl stop dnsmasq # dnsmasq
sudo systemctl disable systemd-resolved && sudo systemctl stop systemd-resolved # systemd-resolvedThen re-run the install command.
# Remove Runbound, keep your config and data
curl -fsSL https://raw.githubusercontent.com/redlemonbe/Runbound/main/install.sh | sudo bash -s -- --uninstall
# Remove everything: config, data, and the runbound user/group
curl -fsSL https://raw.githubusercontent.com/redlemonbe/Runbound/main/install.sh | sudo bash -s -- --purge--uninstall keeps your config and data in /etc/runbound and /var/lib/runbound; --purge deletes them (and the runbound system user/group) as well.
Runbound embeds the dashboard — no nginx needed. Enable it in your config:
server:
ui-enabled: yes
ui-port: 8091
Restart the service, then open https://YOUR_SERVER_IP:8091.
On first access your browser will warn about the self-signed certificate. Download the Runbound CA at https://YOUR_SERVER_IP:8091/webui/ca.crt and install it once — no more warnings on any device on your network.
Enter your API key and click Connect. Full setup guide: docs/web-ui.md.
TOKEN="your-api-key"
# Add a local DNS entry — live, no restart
curl -s -X POST http://localhost:8080/api/dns \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"nas.home.","type":"A","value":"192.168.1.10","ttl":300}'
# Block a domain
curl -s -X POST http://localhost:8080/api/blacklist \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"domain":"ads.example.com"}'
# Subscribe to a block-list feed (auto-refreshed)
curl -s -X POST http://localhost:8080/api/feeds \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"urlhaus","url":"https://urlhaus.abuse.ch/downloads/hostfile/"}'Benchmarked under a documented, reproducible methodology — the truth is the receiver
NIC hardware counters (tx_packets), warm cache, governor pinned, flow-control off,
never the generator's self-report. Full per-run reports: docs/benchmark/.
Current campaign (2026-07-03, dnsmark 1.0 + dnsperf 2.14.0): AMD
Threadripper PRO 5995WX receiver, dual Xeon E5-2690 v2 generator, direct 10 GbE DACs (Intel
X710/i40e + X520/ixgbe), 100k-name real corpus. Every throughput figure is cross-checked
against the receiver NIC tx_packets (agreement 0.1–1.0 %).
| Runbound 0.9.0 (measured 2026-07-03) | Served (receiver NIC) | Host CPU (128 c) | Limited by |
|---|---|---|---|
xdp: yes — dual-link (X710 + X520) |
~19.4 M qps (flood) / 20.3 M (ramp) | ~24.4 % | 99 % of the aggregate 20 G link — link-bound (CPU not saturated) |
xdp: yes — single link (X710) |
~9.85 M qps | ~10.1 % | 10 G link (103 B responses → line-bound) |
xdp: yes — single link (X520) |
~9.81 M qps | ~8.2 % | 10 G link (response direction) |
xdp: no — kernel slow path (X710) |
~2.86 M qps | ~17.7 % | kernel-UDP RX path |
xdp: no — kernel slow path (X520) |
~2.18 M qps | ~17.1 % | kernel-UDP RX path |
These figures are cache-hit / hot-path throughput (answers served from cache or local
zones), not recursion under cache miss — a different workload. In no run did Runbound reach
its own CPU ceiling; the fast path is bounded by the 10 G link (103-byte responses cap a
single link at ~9.85 M/s), the slow path by the kernel-UDP RX path — never by Runbound's own
code. Same-rig kernel-UDP reference resolvers, same cache-hit workload: unbound 1.22.0
~1.91 M (X710), BIND 9.20.23 ~1.49 M (X710, and it livelocks into SERVFAIL under the
X520 flood). Runbound's slow path is ~1.5× unbound / ~1.9× BIND and, unlike BIND, does not
livelock; its fast path is ~5× on the same rig, at lower host CPU. Fast-path wire latency
p50 31 µs (X710) / 34 µs (X520); kernel slow-path cache-hit latency (tcpdump) p50
24.6 µs (X710). These two latencies are not directly comparable — the fast-path figure is generator-side (server + link RTT, via dnsmark --wire-latency), the slow-path figure is server-only (via tcpdump dns.time). Recursion and DNSSEC are fully in-house. Full context, with every counter and caveat:
docs/benchmark/INDEX.md.
The fast path is self-configuring: huge pages are self-provisioned and NIC
queues scale to the CPU automatically (AF_XDP ring sizes use a fixed default;
queues kept at the driver default on bus-bound Xeon v2 + X520). It is designed for
linear scaling — SO_REUSEPORT, lock-free config hot-swap (ArcSwap), per-core affinity, and
SSE4.2 CRC32c + SIMD on the lookup path — though core scaling beyond the 2×10G link ceiling is
not yet demonstrated (every run so far is link-bound at ≤24 % CPU).
An eBPF XDP program attaches to the NIC at startup. UDP/53 packets for local zones and cache hits are answered in user space at driver level — near-zero syscalls on the hot path (a sendto wakeup only when the TX ring drains under sustained load). All other queries pass through to the normal resolver via XDP_PASS.
Negative answers are cached: recursor/forwarder NXDOMAIN / NODATA are stored per (name, type) with an RFC 2308 SOA-derived TTL and served straight from the fast-path cache on a repeat (#166/#210); Bogus answers are never cached. Huge pages and NIC queue counts are configured automatically at startup — see docs/xdp.md.
# Verify XDP is active
journalctl -u runbound | grep XDPDisable without editing config: RUNBOUND_DISABLE_XDP=1 — useful if the host becomes unreachable after an XDP attach. Details: docs/xdp.md.
Full index: docs/index.md
Quick links: Quick Start · Configuration · REST API · XDP · Internals · Systemd · Security Audit · Building & Verifying · Security Policy · Threat Model
CI (.github/workflows/ci.yml) runs on every push to main and every pull request — build, clippy and tests must all be green.
Build prerequisites (Debian/Ubuntu): sudo apt install clang libbpf-dev mold musl-tools (the CI installs the same). Full list: docs/BUILD.md.
cargo build --release # xdp is in the default feature set
cargo clippy --all-targets -- -D warnings # must be warning-free
cargo test # all tests must passPull requests welcome. By submitting a PR you agree to the CLA.
Not affiliated with the NLnet Labs Unbound project.
Bitcoin — 3FP8hkkiu4kwCD1PDFgAv2oq1ZTyXwy3yy
Ethereum — 0xB5eEAf89edA4204Aa9305B068b37A93439cBb680
AGPL v3 — see LICENSE. Commercial license available for organizations that need to deploy without AGPL obligations: COMMERCIAL_LICENSE.md.

