This release keeps Numa resolving for users in France after Quad9 leaves on 24 November. It adds -c/--config, list URLs in allowlist and authoritative = false for a proxy TLD that is a real domain. It also hardens recursive mode and the Windows service, and picks up three RustSec fixes.
⚠️ Check before upgrading
A config file you name must exist. A path that did not exist used to start Numa on defaults without a warning. Now the server and the CLI stop with "config file not found". Check the path in your service unit or Docker command. (#451)
Override batches are all or nothing. POST /overrides and /overrides/environment used to store the items before the first invalid one. Now one invalid item rejects the whole batch. (#466)
DNSSEC shows as off in forward and odoh mode. Validation only runs in recursive mode. The dashboard, /health and numa dnssec on now say so instead of reporting DNSSEC on. A monitor that checks /health for dnssec sees the change. (#463)
🌍 Quad9 leaves France
Quad9 stops serving users in France on 24 November 2026. Numa's built-in DoH fallback was Quad9 alone. Numa uses it in auto mode when the recursive probe fails, and in forward mode when it finds no system DNS. The fallback is now Quad9, then Cloudflare (https://1.1.1.1/dns-query). Cloudflare only sees the queries that Quad9 failed. If you set address, nothing changes. (#477)
⚙️ Config
-c FILE/--config FILEselects the config file, before or after the subcommand. Thanks @id4vip (#450, #451)- Unknown keys in
numa.tomlare logged with their path. Azones = [...]pasted under[proxy]gets a hint to move it above the first section. (#457) - Parse errors name the file and show the line and column, instead of a debug dump of the whole config. (#456)
✅ Allowlist URLs
allowlist accepts list URLs and paths next to plain domains. Numa fetches, caches and refreshes them with the block lists. A list that someone else maintains cannot undo a manual block from the dashboard. Thanks @id4vip (#467, #469)
🏷️ Your own domain as the proxy TLD
With authoritative = false under [proxy], Numa answers its .tld services and sends every other name under the TLD to forwarding rules and the upstream. Before, Numa returned NXDOMAIN for the apex, MX and hosts that live elsewhere. The default stays true. Thanks @id4vip (#467, #468, #473)
📝 Zones
- TXT records work in zones, so a DKIM key is a plain
value = "v=DKIM1; ...". MX values split on any whitespace. Thanks @bcookatpcsd (#224, #454) - A zone CNAME target written as
nas.home.lan.orNAS.home.lannow reaches the local record instead of going upstream. Overrides get the same fix. (#455) - numa.toml shows the inline
zonesarray and where it has to go. (#458)
🔒 Recursive mode
Recursive mode validates an answer before it is cached or shared with waiting queries. A refresh resolves the name again instead of reusing its own cache entry. AD bits from nameservers are cleared. NODATA answers with an SOA are cached, so a removed record is replaced. (#476)
🪟 Windows
- The service reports a failure when serve stops on an error, so the SCM restart policy runs. Before, a boot-time bind error left the machine without DNS. (#470)
- The UDP listener keeps running after per-datagram receive errors. (#476)
numa installsets the owner and ACL of%PROGRAMDATA%\numato SYSTEM and Administrators. (#476)
Run numa install again after you upgrade on Windows to apply the restart flag and the new permissions.
⚡ Performance
- The Linux release binaries and the Docker image (musl) use mimalloc. On a 2 vCPU x86_64 VPS this gave 2.1-2.5x the cached queries per second, and 1.8-1.9x on a Pi 4. macOS, Windows and glibc builds are unchanged. Thanks @Guara92 (#394, #395)
- Blocklist checks look at the allowlist only after a block hit, and skip empty sets. Against v0.24.1 on an Apple Silicon laptop, a blocked lookup is 31% faster and a not-blocked lookup 56% faster. (#469, #474)
- Blocklist reloads no longer allocate and drop extra copies of the domain table. (#443, #445)
Also notable
- Dependency updates for RustSec advisories: rustls (#452), rustls-webpki and webpki roots (#472), ctutils (#478)
- The retired KSK-2017 root trust anchor is removed. The root rollover completed on 11 October, so behaviour does not change. (#449)
- On hosts where systemctl cannot reach the system bus, its errors no longer fill the daemon log on every dashboard poll. (#461)
- numa.toml has one commented
[upstream]block per mode. README covers upgrading, calling Numa by path under sudo, and-c. (#446, #453, #460, #462)
All changes
What's Changed
- ci(docker): capture stderr in the alpine smoke test by @razvandimescu in #441
- perf(blocklist): size the parse set up front and stop copying it on load by @razvandimescu in #443
- perf(blocklist): grow the parse set instead of reserving per line by @razvandimescu in #445
- docs(readme): document upgrading and calling numa by path under sudo by @razvandimescu in #446
- chore(dnssec): drop the retired KSK-2017 root trust anchor by @razvandimescu in #449
- refactor(ctx): build the blocked sinkhole response in one place by @razvandimescu in #447
- test(svcb): pin ech SvcParam delivery through the rewrite paths by @razvandimescu in #381
- fix(deps): bump rustls to 0.23.46 for RUSTSEC-2026-0346..0349 by @razvandimescu in #452
- feat(cli): add -c/--config to pick the config file by @razvandimescu in #451
- docs(readme): mention -c next to config path and config edit by @razvandimescu in #453
- perf(alloc): use mimalloc on musl targets by @razvandimescu in #395
- feat(zones): accept TXT records, split MX values on any whitespace by @razvandimescu in #454
- docs(zones): show the inline zones array and where it has to go by @razvandimescu in #458
- fix(cli): print errors with Display, name the config file on parse errors by @razvandimescu in #456
- fix(ctx): chase CNAME targets written with a trailing dot or capitals by @razvandimescu in #455
- feat(config): warn on unknown config keys by @razvandimescu in #457
- docs(readme): drop /etc/numa from the uninstall leftovers by @razvandimescu in #460
- fix(system_dns): keep systemctl errors out of the daemon log by @razvandimescu in #461
- docs(config): give each upstream mode its own block in numa.toml by @razvandimescu in #462
- test: share the zone() fixture and drop a duplicated TXT case by @razvandimescu in #465
- refactor: trim what the changes since 0.24.1 left behind by @razvandimescu in #464
- fix(deps): bump rustls-webpki + webpki roots for the new rustsec advisories by @razvandimescu in #472
- fix(windows): restart the service when serve fails by @razvandimescu in #470
- fix(dnssec): don't report DNSSEC on in forward or odoh mode by @razvandimescu in #463
- fix(api): store an override batch only when every item is valid by @razvandimescu in #466
- perf(blocking): skip lookups in empty sets by @razvandimescu in #474
- feat(blocking): accept list URLs and paths in allowlist by @razvandimescu in #469
- fix: recursive refresh and coalescing, windows udp recv and install permissions by @razvandimescu in #476
- feat(proxy): add authoritative = false for a tld that is a real domain by @razvandimescu in #468
- fix(deps): bump ctutils for RUSTSEC-2026-0385 by @razvandimescu in #478
- fix(upstream): try cloudflare after quad9 in the built-in DoH fallback by @razvandimescu in #477
Full Changelog: v0.24.1...v0.25.0