Skip to content

DNS plane

The DNS plane is one of the two halves of The Gateway’s dataplane (the other is the splitting / routing engine). Every DNS query from every LAN device is intercepted by the integrated resolver — either by being the network’s dnsmasq-advertised resolver, or by an iptables DNAT redirect for devices that try to talk to 8.8.8.8 directly.

Capability Notes
Recursive resolver Forwarding to upstream resolvers of your choice. upstream_mode: plain / doh / dot.
Per-device filter rules Match by source IP, subnet, interface, device, or device group. Wildcard, suffix, exact, or regex.
Allowlist / blocklist Curated and custom lists; mergeable; per-rule precedence.
Domain rewrite Force a domain to NXDOMAIN or to a specific A / AAAA record.
Custom zones Local A / AAAA / CNAME / MX / SOA / NS records (e.g. *.lan for LAN service discovery).
Upstream rotation off / round-robin / random over a pool of upstream resolvers. Per-query when on.
ECS stripping Strip EDNS Client Subnet on outbound queries — privacy by default, always on.
DNS leak prevention iptables DNAT for hard-coded resolvers; nftables blocks for DoH / DoT bypass.
DoH / DoT block DoH by SNI on TCP/443 against known providers; DoT by dropping TCP/853.
Telemetry preset blocks Curated lists for common OS / app telemetry endpoints, organised by category. One-toggle enable/disable.
Cache with stats Global hit / miss / eviction counters. Per-upstream RTT.
Pi-hole integration Optional: front an existing Pi-hole v5 or v6 instance. Auto-detects API version, pushes blocklist adds/removes from the gateway side.

A filter rule lives in /api/v1/dns/filter-rules: pattern + match-type + action, scoped by source. action ∈ {block, allow, rewrite}, match_type ∈ {exact, wildcard, regex, contains, suffix}, source_type ∈ {all, ip, subnet, interface, device, device_group}. Rules are checked in ascending priority order; the first matching rule wins, so lower number = wins.

Terminal window
# Block ads only for the kids device group
curl -X POST https://gateway.lan:8080/api/v1/dns/filter-rules \
-H "Authorization: Bearer $API_KEY" -d '{
"name":"block-ads-kids","priority":110,"enabled":true,
"action":"block","match_type":"wildcard","pattern":"*.doubleclick.net",
"source_type":"device_group","source_value":"kids"
}'

To rewrite a specific domain to a fixed answer for every client, use the simpler /api/v1/dns/rewrites endpoint (no priority, no source scope):

Terminal window
curl -X POST https://gateway.lan:8080/api/v1/dns/rewrites \
-H "Authorization: Bearer $API_KEY" -d '{
"domain":"mail.corp.example","answer":"10.20.0.5"
}'

For per-device-group rewrites, use /dns/filter-rules with action: rewrite instead. A query is matched against filter rules in priority order; the first matching rule wins.

You can host arbitrary local zones in the integrated resolver. A zone (/api/v1/dns/zones) is just a name + enabled flag; records (/api/v1/dns/records) are a separate resource that reference the zone by name.

Terminal window
# 1. Create the zone
curl -X POST https://gateway.lan:8080/api/v1/dns/zones \
-H "Authorization: Bearer $API_KEY" -d '{"zone":"lan","enabled":true}'
# 2. Add records (one per call)
curl -X POST https://gateway.lan:8080/api/v1/dns/records \
-H "Authorization: Bearer $API_KEY" -d '{
"zone":"lan", "name":"nas", "type":"A", "value":"10.0.0.5", "ttl":300, "enabled":true
}'
curl -X POST https://gateway.lan:8080/api/v1/dns/records \
-H "Authorization: Bearer $API_KEY" -d '{
"zone":"lan", "name":"pihole", "type":"CNAME", "value":"gateway.lan", "ttl":300, "enabled":true
}'

Useful for hostname-based service discovery on the LAN without standing up dnsmasq or BIND. The integrated resolver synthesises SOA + NS automatically — no need to define them per zone.

The integrated resolver is the only allowed DNS path. Three layers enforce this:

  1. DHCP hands out the gateway IP as the DNS server.
  2. iptables DNAT rewrites any UDP/53 destined for any other IP back to the gateway.
  3. nftables drop rules block DoH (443/tcp to known DoH providers by SNI) and DoT (853/tcp to anywhere).

A device that hard-codes 1.1.1.1 still gets its queries answered — by The Gateway’s resolver, with filtering applied. A device that uses Firefox’s DoH gets blocked.

If you already run a Pi-hole and want to keep its blocklist UI as the authoritative source for “what gets blocked at the network”, the daemon can front it. Set dns.backend: pihole in the bootstrap config and provide the Pi-hole admin URL plus credentials:

dns:
backend: pihole
pihole:
url: "http://pihole.lan"
password: "REDACTED" # Pi-hole v6+ web password (preferred)
api_key: "" # Pi-hole v5 only — WEBPASSWORD from setupVars.conf
sync_blocklist: true # allow the daemon to push add/remove

The client auto-detects v5 vs v6 — v6 uses a session token (POST /api/auth, then SID header on subsequent calls); v5 uses &auth=<key> in the query string. You only need to populate the credential field that matches your install.

What the gateway does, from the daemon side:

Operation API surface Effect
Read status GET /api/v1/dns/pihole/status Connected? Version, gravity size, queries / blocked today
Enable backend POST /api/v1/dns/pihole/enable Re-enables Pi-hole if disabled
Disable backend POST /api/v1/dns/pihole/disable Pauses Pi-hole filtering
Add to blocklist (internal, triggered by filter-rule changes) Pushes new deny entries into Pi-hole’s exact-match list
Remove from blocklist (internal, triggered by filter-rule changes) Removes deny entries; tolerates 404 (already gone)

Push operations are gateway → Pi-hole only — the daemon does not poll Pi-hole’s UI for changes and merge them back. If an operator adds a block in the Pi-hole UI, it stays there as Pi-hole’s local state; the gateway is unaware of it. Treat the gateway’s filter-rules table as authoritative for what it will push, and Pi-hole’s UI as authoritative for everything Pi-hole adds on its own (gravity, adlists, manual entries).

Any non-2xx HTTP response (other than 401 which triggers re-auth, and 404 on delete which means “already gone”) surfaces as an error on the operator’s call — silent push failures were a previous source of state drift and are now treated as real failures.

Method Path Purpose
GET /api/v1/dns/status Resolver health, upstream RTTs
GET /api/v1/dns/cache Cache entries
POST /api/v1/dns/cache/flush Flush cache (optional pattern filter)
GET /api/v1/dns/log Query log
GET /api/v1/dns/clients Per-client query counters
GET/POST/PUT/DELETE /api/v1/dns/filter-rules Filter rule CRUD
GET/POST/PUT/DELETE /api/v1/dns/zones Custom zones
GET/POST/PUT/DELETE /api/v1/dns/records Per-zone records
GET/POST/DELETE /api/v1/dns/rewrites NXDOMAIN / A-rewrite rules
GET/POST/DELETE /api/v1/dns/allowlist Per-source allow entries
POST/DELETE /api/v1/dns/blocklist Per-source block entries
GET/POST/PUT/DELETE /api/v1/dns/forwarders Upstream-resolver pool
GET/POST/PUT/DELETE /api/v1/dns/presets Curated preset blocklists
GET/PUT /api/v1/dns/rotation Upstream rotation mode
GET /api/v1/dns/pihole/status Pi-hole backend status (if configured)
POST /api/v1/dns/pihole/enable Re-enable Pi-hole filtering
POST /api/v1/dns/pihole/disable Pause Pi-hole filtering

After the resolver is healthy and serving the LAN:

  1. Confirm devices actually use it. Open the DNS log and filter by device IP — a device with zero queries is going around the resolver. See leak-prevention.
  2. Enable telemetry-preset blocks for the OS/app categories present on the LAN (Windows, macOS, iOS, smart TVs). One toggle each on the Telemetry page.
  3. Pair DoH/DoT blocking with the resolver. Filtering only works if devices can’t bypass via Firefox TRR or hard-coded 1.1.1.1:853. See Privacy → system-wide DNS protections.
  4. Set per-device filter rules for device groups that need stricter posture (kids, guests) — same matching engine, narrower scope.