Releases: redlemonbe/Runbound
Releases · redlemonbe/Runbound
Release list
v0.9.3
Fixed
- CPU over-subscription on VMs / containers (self-inflicted latency).
cpu::physical_cores()
read host-wide/sysCPU topology, which is NOT namespaced: a Proxmox VM with vCPU hotplug
slots (or an LXC cpuset) advertises far morecpuNentries than the process may run on. On a
2-vCPU VM Runbound therefore spawned 64 tokio workers + 63 kernel-loop threads, saturating
the box (load ~14, ~193% CPU at idle) and adding a ~200 ms scheduling stall to every
slow-path UDP answer (blocks, CHAOS, and each cache-miss recursion) — cache hits on the fast
path stayed at ~1 ms, so the effective cache-hit rate and average latency looked "dirty".
physical_cores()now intersects the sysfs topology with the CPU-affinity mask
(sched_getaffinity, the same source asnproc), so worker/thread counts match the real
budget. Physical-only selection is preserved (cores are only dropped, never an SMT sibling
added — the ASM/XDP hot path stays HT-free). No-op on bare-metal.
v0.9.2
Added
- Live-editable DNS rate limit —
rate-limit/rate-limit-burstare changeable at runtime
viaPATCH /api/configand the WebUI Protection tab ("DNS Rate Limit" card): applied live to the
XDP fast path + kernel slow path (no restart) and persisted torunbound.conf.rate-limit-burst
is now honoured atserver:level (it was silently ignored — the DNS burst was hard-coded to
rps*2) and survives config regeneration;GET /api/configreports both live. Per-node (a local
capacity policy, not replicated to slaves). - QNAME minimisation (RFC 9156 ) for the sovereign full-recursion resolver.
Intermediate authoritative servers (root, TLD) are now probed with only the next
label toward the target (as a QTYPE A query), so they never see the full query
name; the complete name and type are sent only to the zone's own authoritative
server. On by default underresolution: full-recursion(like Unbound), toggled
withqname-minimisation: yes|no. Relaxed variant: any anomaly on a probe
(NXDOMAIN on an empty non-terminal, an unexpected answer, or a mute server) falls
back to the full name at that delegation point, so it never breaks an
otherwise-resolvable name; deep flat names stop being probed label-by-label after
10 labels. The two previously-duplicated descent loops (resolve_once/
resolve_message) are unified into one engine. DNSSEC validation (incl. the
DS-at-parent anchoring) and the infrastructure cache are unaffected —
verified live (Secure/Bogus/Insecure) and by packet capture showing the root
receiving only the TLD. - The WebUI header now shows the running version, between the name and the status dot.
Security
- Double-pass adversarial audit :
GET /api/audit/tail
is gated to admins (was readable by any authenticated key in multi-user); backup import writes
secret files0600; the forward negative cache checks the authority SOA is in-bailiwick for the
qname;parse_nsecslices the NSEC type bitmap at the bytes consumed, not the presentation length. - Auto-ban hardening on every path. Loopback/unspecified can no longer be banned (a
PUT /blacklist/127.0.0.1self-DoS that also persisted); relayed and manual bans are now permanent
on both ban systems (XDP/kernel fast path and the slow-path/DoT/DoH/DoQ enforcer), fixing a
silent 24h lapse on slaves; bot-defense bans also reachicmp_statsso they enforce on the
kernel-UDP path underxdp: no; IPv6 bans propagate to the fast-path map; the in-memory ban and
login-rate maps are capped.
Fixed
- Alert rules with different windows now each count in their own sliding window; a single shared
counter previously let a short-window rule reset the count a long-window rule needed, silently
disabling it. create_userwrote a duplicate entry tousers.json./api/metrics: therunbound_cache_hit_rateHELP said "0.0 to 1.0" but the value is a percentage.- Startup fails fast with an actionable message when the config base directory is not writable
(was an obscure tokio "cannot drop a runtime" panic); the API/UI runtimes are leaked at creation to
remove that async-drop panic class. - API documentation (
docs/api.md) resynced with the handlers across 21 endpoints — response
schemas (dns/lookup,cache/stats, upstream probe), real OpenMetrics family names, the
PATCH /api/config"is persisted" note, and 11 missing/api/systemfields. - WebUI idle auto-logout restored to 5 minutes (it had drifted to 30).
- Backup restore/import now applies live (hot-reload) instead of asking for a service
restart — Runbound never restarts. The restore re-reads the config, republishes local
zones (resyncing the XDP cache), refreshes alert rules, and re-applies the resolution
mode + QNAME-minimisation toggles; both restore paths share oneapply_config_hot_reload
helper (restore_handlerpreviously discarded the reloaded config). Verified live: a
restored zone reappears with no restart. - Persisted-cache load hardened against an
Instant-overflow panic on a corrupt cache
file (checked add; PENT-1 from the offensive pentest — docs/security-audit/). - Compact denial of existence (RFC 9824 / Cloudflare "black lies", #232): under
resolution: full-recursion+dnssec-validation: yes, a non-existent name in a
Cloudflare-hosted zone is now presented to the client as NXDOMAIN instead of
NOERROR/NODATA. A validated NSEC that matches the qname and whose type bitmap carries
the NXNAME pseudo-type (128) translates NOERROR → NXDOMAIN (fail-closed: only on a
Secure verdict). Genuine NODATA and classic NXDOMAIN are unchanged. NSEC for now;
NSEC3 compact denial to follow. - IPv6 on the slow path (#233): with
xdp: no, an IPv6interface:failed to bind
(:::53is invalid and the kernel-loop socket was hard-coded to IPv4), which took
down ALL of DNS. Bind addresses are now bracketed ([::]:53), the socket domain
follows the address (IPv6-only, so[::]:53coexists with an IPv4 wildcard bind and
the ACL/rate-limiter sees the real source), and a secondary interface that cannot bind
(e.g. a fixed IPv6 whose prefix went away) is skipped with a warning instead of
failing the whole service — the primary DNS stays up.
Changed
- A manual ban via
PUT /api/alerts/blocked/:ipis now permanent on both ban systems (matching
the documented "permanent, no expiry"). Ban/blacklist endpoints return{blocked:false,reason}/
{blacklisted:false,reason}for a protected (loopback/unspecified) target instead of a false
success. - WebUI About page: removed the external Links section and the issue hyperlink; the
community-contributor credit is now plain text (no link).
v0.9.1
Added
- IPv6 bans are now enforced at the XDP fast path. A new
icmp_banned_v6
BPF map (16-byte key) plus gated lookups on the main and VLAN-tagged IPv6
datapaths drop banned IPv6 sources at kernel-bypass speed, not just in the
userspace slow path. The IPv4 ban path is unchanged. - Unbound
include:/include-toplevel:directives are now honoured (with glob
support), so a split configuration — e.g. Debian's
include: "/etc/unbound/unbound.conf.d/*.conf"— loads correctly instead of being
silently dropped. Guard-railed: bounded nesting depth (8), file count (256) and
cumulative size (16 MiB), glob only in the final path component, and refusal to
escape the config file's directory.
Fixed
- "Top Domains" stayed empty at low QPS: per-domain counters sat in
thread-local buffers because the only flush trigger was count-based (every 512
calls), which never fires at residential/LAN rates. Added a time-based flush
(≤ 1 s) so the dashboard converges at any QPS; the multi-MQPS path is unchanged. - The validating recursor had no infrastructure cache: every cache miss
re-walked from the root, re-fetching zone-cut NS sets and the whole DNSSEC chain
(root/TLD DNSKEY+DS) each time — ~70 % of miss traffic hit the root servers and
each miss cost 325 ms–1.3 s. Added a TTL-honouring, bounded (LRU) zone-cut +
validated-DNSKEY cache: repeated misses under the same parent now collapse from
~240 ms to ~55 ms. Fail-closed validation is preserved (DNSKEYs cached only after
a Secure result, bounded by RRSIG expiry); reviewed by a two-model cross-audit. PUT /api/policies/:namereturned 422 when the request body omittedname; it
now takes the name from the path as documented (and aPOSTwithout a name gives
a clear 400 "name is required" instead of a 422).- The
fuzz-feature library build was broken (webhooksreferenced
crate::feeds::{SsrfSafeDnsResolver, is_private_ip}, butfeedsis not exposed to
the cargo-fuzz lib). Extracted a standalonesrc/ssrf.rs— the single source of
truth for the SSRF address filter and connection-time DNS guard — that
recursor_wire,feedsandwebhooksdelegate to. Restores
cargo build --features fuzzand the weekly fuzz workflow. cargo auditwas not clean: bumpedanyhow1.0.102 → 1.0.103 (RUSTSEC-2026-0190),
documented the two remainingunmaintainedadvisories (paste,rustls-pemfile)
inaudit.toml, and added acargo auditCI job so the badge is verified on every
push.- Test harness: three
rbac_*API tests were flaky-failing on some machines because
they shared a fixedBASE_DIR(/tmp/runbound-test) that could be owned by another
user, making/api/dnsreturn 500. Each test process now uses its own writable temp
dir. Full suite: 464 passing / 0 failing.
Docs
- Documented
GET /api/dns/:idand theGET /api/alerts/rulesalias./api/help
now maps 1:1 to the router and every one of the 82 endpoints is covered by
docs/api.md. - Pre-publication accuracy pass: build prerequisites (clang/libbpf-dev/mold/musl-tools)
in README + BUILD.md; fixed a dead audit-report link; version markers 0.9.0 → 0.9.1
in the API/quick-start/internals/sync examples; the audit log is described as
per-entry HMAC-SHA256 + periodic checkpoints (not a running chain); the sovereign
recursor is config-gated (resolution: full-recursion), not feature-gated; and
corrected stale source comments (recursor_wirebuild_query emits EDNS DO=1,
wire_bridgenotes hickory is a dev-only oracle).include:documented in
docs/unbound-migration.md.
Security
- M1 — the WebUI login always runs the argon2 verify, even when the username is
wrong, so response time no longer leaks whether a username is valid. - M2 — the validating recursor reads UDP responses until one matches the query's
transaction id and question; a single spoofed or stray datagram from the queried
server no longer aborts resolution (still bounded by the query timeout). - M3 — the systemd unit now sets
MemoryDenyWriteExecute=true(W^X). Verified
compatible: AF_XDP UMEM is read/write only, eBPF loads via thebpf()syscall, and
the crypto is pure-Rust. - M4 — the WebUI escapes the record id interpolated into the delete handlers.
- M5 —
tls-cert-bundleis Unbound's outbound CA bundle, not the DoT/DoH server
certificate; it is now warned about and ignored instead of being loaded as the cert. - Documented in
docs/xdp.mdthat the XDP blacklist fast-block matches the raw QNAME
case-sensitively — a case-varied (0x20) query falls through to the slow path, which
still blocks it (no bypass).