nantevo-fwd guide
nantevo-fwd (ForWarDer · FireWall DNS) is a standalone DNS-to-DoH secure forwarder written in Rust. It accepts plaintext DNS on UDP port 53 from devices that can't speak DoH themselves — servers, printers, SCADA and OT equipment, IoT, legacy hardware — and forwards each device's queries through its own authenticated Nantevo DoH upstream, with local threat filtering applied before anything leaves your network.
What nantevo-fwd is
A single statically-linked binary with no runtime dependencies. It maps each source IP on your network to a unique Nantevo DoH upstream, which gives you per-device attribution on hardware that has never heard of DoH — static IPs and DHCP reservations are the identity mechanism.
Three things happen to every query before it leaves your network:
- Authentication mapping — the source IP is matched to its configured client mapping. Unmapped sources get
SERVFAIL, immediately (see runtime behavior). - Local filtering — the query is checked against three locally-loaded threat intelligence layers. Blocked domains answer
NXDOMAINwithout ever being forwarded. - Privacy hardening — ECS stripping, EDNS padding (RFC 8467), randomized query IDs, suppressed user agent, and HTTP/2 frame padding are applied to the outgoing DoH request.
Requirements
| Item | Detail |
|---|---|
| Platforms | FreeBSD amd64 · Linux amd64 / aarch64 · macOS arm64 / amd64 (development only) · any Rust target |
| Memory | ~300 MB at full blocklist load; comfortable on a Raspberry Pi 4 |
| Network | Binds UDP :53 for client queries; HTTPS (443) outbound to your Nantevo DoH endpoints |
| Privileges | Runs as an unprivileged user; on Linux, CAP_NET_BIND_SERVICE grants the port-53 bind |
| Bootstrap | One plaintext DNS query at startup to resolve its own DoH endpoint — the only plaintext query it ever makes. Auditable and logged. |
| Configuration | A single TOML file, passed with --config |
Deployment
FreeBSD — rc.d service
nantevo-fwd ships with a complete rc.d service script for FreeBSD 13+ and 15. It starts at boot, restarts on failure, and logs to syslog — the same deployment model used by the Nantevo cloud resolver nodes.
# /etc/rc.conf
nantevo_fwd_enable="YES"
service nantevo-fwd start
FreeBSD — BastilleBSD jail
For kernel-enforced process isolation, deploy inside a BastilleBSD jail with a dedicated IP and separate network namespace. Zero overhead versus bare metal.
bastille create fwd-01 15.0-RELEASE 10.0.0.53
bastille template fwd-01 nantevo/fwd-template
Linux — systemd service
A systemd unit file is included for any modern distribution (Ubuntu, Debian, RHEL, Alpine, and others). Tested on amd64 and aarch64.
systemctl enable nantevo-fwd
systemctl start nantevo-fwd
# follow logs
journalctl -u nantevo-fwd -f
ARM single-board computers
Tested in production on Raspberry Pi 4 (64-bit Raspberry Pi OS). The ~300 MB footprint leaves ample headroom for other services — an ideal branch-office or home-lab edge deployment for network-wide IoT and legacy coverage.
./nantevo-fwd --config fwd.toml
macOS — development and testing
Compiles and runs on Apple Silicon and Intel Macs for configuration testing and client-mapping validation before pushing to FreeBSD or Linux nodes. Not a recommended production platform.
cargo build --release
sudo ./target/release/nantevo-fwd --config fwd.toml
Client mapping
Attribution is driven by source IP. Each device you want individually attributed needs a stable address:
- Servers — static IPs map directly to unique Nantevo DoH upstreams. No changes to server configuration.
- IoT / OT fleets — DHCP reservations give each device a predictable IP and therefore a unique authenticated identity.
- Everything else — devices without a mapping are refused (
SERVFAIL) and surfaced in the Unknown Devices dashboard view so you can enroll or investigate them.
# conceptually: one source IP → one authenticated upstream
client 10.17.89.53 → nantevo-endpoint-A
client 10.17.89.18 → nantevo-endpoint-B
10.17.89.99 → SERVFAIL (not enrolled)
fwd.toml configuration reference — including the client-mapping syntax — is distributed with your onboarding bundle rather than published here, because mappings embed your private per-client endpoints and ClientIDs.Local threat intelligence
Three independent blocking layers are enforced locally, before any query is forwarded upstream. Your devices stay protected even during the forwarding hop.
| Layer | Source | Scale | Action |
|---|---|---|---|
| Domain blocklist (TIF) | HaGeZi Threat Intelligence Feed — malware, C2, phishing, tracking. Refreshes every 24h. | ~1.18M domains | Block — answers NXDOMAIN |
| IP reputation (DROP + XBL) | Spamhaus DROP (hijacked netblocks) + XBL DQS (real-time botnet / compromised-host lookup). Blocks responses resolving to known-malicious infrastructure. | ~15.16M IPv4 (58,231 exact + 1,714 CIDR ranges, +96 IPv6 subnets) | Block |
| NRD observe (Stamus) | Newly Registered Domain tracking for DGA and phishing patterns. | ~2.29M domains | Flag only — never blocked. Feeds RoCi's async analysis. |
Threat events are ranked by severity: Critical (XBL — botnet / compromised hosts) → High (DROP — hijacked netblocks) → Medium (TIF — domain blocklist) → Policy (Nantevo upstream policy block).
Safe search enforcement
Optional DNS-level safe search rewrites resolution for Google, YouTube, Bing, DuckDuckGo, Yandex, Qwant, and Pixabay to their safe-search endpoints. Configured once on the forwarder (safe_search: strict or moderate), it applies to every device on the network — including those that can't be individually configured.
Runtime behavior
Fail closed
Any source IP without a client mapping receives SERVFAIL. There is no silent forwarding and no fallback to an unprotected resolver — a device that isn't enrolled cannot resolve DNS through the forwarder, period. Unmapped attempts are not forwarded and not logged as queries; they appear only in the Unknown Devices view.
Caching and prefetch
Each enrolled client gets an independent cache with configurable TTL bounds (e.g. max_ttl: 3600). Prefetching refreshes popular records before expiry to keep hit rates high — warm-cache responses return in under 5 ms. Per-client cache hit rates and latency distributions are visible on the Fleet Overview.
DNSSEC
Three validation modes, selectable per deployment: off, opportunistic (validate where supported, pass through unsigned records), and strict (validate everything, fail closed with SERVFAIL on validation errors). RRSIG consistency checks run locally before responses are served from cache.
Privacy hardening on the upstream hop
- ECS stripping (
strip_ecs: true) — upstream and authoritative servers never see the client's subnet. - EDNS padding per RFC 8467, plus HTTP/2 multiplexing with frame padding — defeats traffic analysis on packet size and timing.
- Randomized query IDs, suppressed User-Agent,
Cache-Control: no-store— prevents cross-connection correlation and intermediary caching. - DNS rebinding protection — responses resolving public names to RFC 1918, loopback, or link-local addresses are blocked.
Dashboard & metrics
The read-only dashboard is embedded in the binary at /dashboard — no separate web server or external dependencies. It provides:
- Fleet Overview — enrolled clients, per-device query counts, block rates, average latency, health.
- Threat Events — severity-ordered with an acknowledge workflow, filterable by source (XBL / DROP / TIF / Cloud policy).
- Protection — live blocklist status and DNS protection settings.
- Query Log — most recent queries across all clients with status (
FORWARD/CACHE/BLOCK), DNSSEC validation indicator, and latency. - Unknown Devices — every source IP that attempted a query without a mapping.
Every tracked metric is also exported natively to Prometheus at /metrics:
nantevo_queries_total{client="mixpost"}
nantevo_blocked_total{src="tif"}
nantevo_latency_p95{client="conduit"}
Pointing the network at it
Set your DHCP server's DNS option to the host running nantevo-fwd. Every device on the network — regardless of OS, age, or capability — immediately routes through the forwarder with no client-side changes.
# ISC DHCP / dnsmasq
option domain-name-servers 10.22.22.1;
# nantevo-fwd listens on :53