Recursive DNS on GL-MT3000: Unbound behind AdGuard Home
A guide to running a fully recursive DNS stack on the GL-MT3000 travel router. AdGuard Home handles filtering and client-facing DNS on port 53; Unbound runs as a recursive resolver on port 5335, querying root servers directly instead of forwarding to public providers like 1.1.1.1 or 8.8.8.8. No data leaves to third-party resolvers.
The GL-MT3000 runs OpenWrt (aarch64_cortex-a53) with GL.iNet’s firmware layer on top. The OpenWrt unbound-daemon package is available in opkg, but the GL.iNet init script manages Unbound via UCI (OpenWrt’s Unified Configuration Interface) and generates its own config - it does not use /etc/unbound/unbound.conf. This guide works around that by disabling the UCI-managed service and running Unbound directly under a custom procd init script.
Prerequisites
Section titled “Prerequisites”- GL-MT3000 with AdGuard Home already running (enabled via GL.iNet admin panel -> Applications -> AdGuard Home)
- SSH access enabled (LuCI -> System -> Administration -> SSH Access, or GL.iNet admin panel -> System -> Advanced Settings)
- An SSH key added in LuCI -> System -> Administration -> SSH Keys (recommended over password auth)
Part 1: install Unbound
Section titled “Part 1: install Unbound”SSH into the router and install the daemon package:
opkg updateopkg install unbound-daemonFetch the root hints file (list of DNS root servers) and bootstrap the DNSSEC trust anchor:
curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.rootunbound-anchor -a /etc/unbound/root.keyThe unbound-anchor command creates or updates root.key, which Unbound needs for DNSSEC validation (auto-trust-anchor-file in the config). Without it, DNSSEC will fail on first start.
Part 2: configure Unbound
Section titled “Part 2: configure Unbound”Create /etc/unbound/unbound.conf. The key differences from a standard Unbound config:
interface: 127.0.0.1+port: 5335- loopback only, AdGuard Home is the public-facing resolveraccess-control: 0.0.0.0/0 refuse- only localhost (AdGuard Home) can query Unbounduse-caps-for-id: no- 0x20 encoding causes resolution failures with authoritative servers that don’t preserve query name casing; disabled for reliabilitymodule-config: "validator iterator"- DNSSEC validation enabled- Cache sized for standalone use (no upstream cache layer)
so-sndbuf/so-rcvbufomitted - require sysctl tuning not present on stock OpenWrt- Local zones for
lan,gl-mt3000, and the192.168.8.0/24reverse zone pulled from the UCI-generated config
# Standalone recursive resolver - upstream for AdGuard Home on :5335
server: do-daemonize: no username: "" chroot: "" directory: "/etc/unbound" pidfile: "/var/run/unbound.pid" use-syslog: yes verbosity: 1 log-queries: no log-replies: no log-servfail: yes log-time-ascii: yes
interface: 127.0.0.1 port: 5335
do-ip4: yes do-udp: yes do-tcp: yes prefer-ip6: no do-ip6: no
access-control: 127.0.0.1/32 allow access-control: 0.0.0.0/0 refuse
# DNSSEC harden-glue: yes harden-dnssec-stripped: yes harden-below-nxdomain: yes harden-algo-downgrade: yes qname-minimisation: yes aggressive-nsec: yes val-clean-additional: yes val-permissive-mode: no module-config: "validator iterator" auto-trust-anchor-file: /etc/unbound/root.key
# Privacy & hardening use-caps-for-id: no rrset-roundrobin: yes hide-identity: yes hide-version: yes minimal-responses: yes
private-address: 10.0.0.0/8 private-address: 100.64.0.0/10 private-address: 169.254.0.0/16 private-address: 172.16.0.0/12 private-address: 192.168.0.0/16 private-address: fc00::/7 private-address: fe80::/10
# Local zones (update if your LAN subnet changes) domain-insecure: lan private-domain: lan local-zone: lan static local-data: "lan. 7200 IN SOA localhost. nobody.invalid. 1 3600 1200 9600 300" local-data: "lan. 7200 IN NS localhost."
domain-insecure: gl-mt3000 private-domain: gl-mt3000 local-zone: gl-mt3000 static local-data: "gl-mt3000. 7200 IN SOA localhost. nobody.invalid. 1 3600 1200 9600 300" local-data: "gl-mt3000. 7200 IN NS localhost." local-data: "gl-mt3000. 300 IN A 192.168.8.1"
domain-insecure: 8.168.192.in-addr.arpa local-zone: 8.168.192.in-addr.arpa static local-data: "8.168.192.in-addr.arpa. 7200 IN SOA localhost. nobody.invalid. 1 3600 1200 9600 300" local-data: "8.168.192.in-addr.arpa. 7200 IN NS localhost." local-data-ptr: "192.168.8.1 300 gl-mt3000"
local-zone: local always_nxdomain
# Cache - sized for standalone use on MT-3000 (512MB RAM total) # No upstream cache layer; Unbound sees all queries from AGH key-cache-size: 8m msg-cache-size: 16m rrset-cache-size: 32m cache-min-ttl: 0 cache-max-ttl: 72000 cache-max-negative-ttl: 60 val-bogus-ttl: 300 infra-host-ttl: 900
prefetch: yes prefetch-key: yes
# Serve stale entries while refreshing - useful on a router where # upstream hiccups affect all devices on the network serve-expired: yes serve-expired-ttl: 86400 serve-expired-client-timeout: 1800
# Performance - MT-3000 has 2 cores (Filogic 820 / MT7981B, dual-core Cortex-A53) num-threads: 2 msg-cache-slabs: 4 rrset-cache-slabs: 4 infra-cache-slabs: 4 key-cache-slabs: 4 ratelimit-slabs: 4 ip-ratelimit-slabs: 4
edns-buffer-size: 1232 so-reuseport: yes jostle-timeout: 200 outgoing-range: 4096 outgoing-port-permit: 10240-65535 num-queries-per-thread: 1024 target-fetch-policy: "3 2 1 0 0"
extended-statistics: yes statistics-interval: 0 statistics-cumulative: yes
tls-cert-bundle: /etc/ssl/certs/ca-certificates.crt root-hints: /etc/unbound/root.hintsTransfer it to the router:
scp unbound.conf root@192.168.8.1:/etc/unbound/unbound.confPart 3: Procd init script
Section titled “Part 3: Procd init script”The UCI-managed unbound service must be disabled before starting the custom one - both try to bind port 5335 and will conflict.
service unbound disableservice unbound stopCreate the procd init script:
cat > /etc/init.d/unbound-custom << 'EOF'#!/bin/sh /etc/rc.common
START=18STOP=50USE_PROCD=1PROG=/usr/sbin/unbound
start_service() { procd_open_instance "unbound" procd_set_param command $PROG -d -c /etc/unbound/unbound.conf procd_set_param respawn procd_close_instance}EOF
chmod +x /etc/init.d/unbound-customservice unbound-custom enableservice unbound-custom startVerify it is listening:
netstat -lunp | grep 5335Expected output:
udp 0 0 127.0.0.1:5335 0.0.0.0:* <PID>/unboundudp 0 0 127.0.0.1:5335 0.0.0.0:* <PID>/unboundThe duplicate line is normal - one entry per thread (num-threads: 2).
Part 4: point AdGuard Home at Unbound
Section titled “Part 4: point AdGuard Home at Unbound”In the AdGuard Home web UI:
- Settings -> DNS settings -> Upstream DNS servers
- Replace all existing entries (
1.1.1.1,8.8.8.8,9.9.9.9) with:127.0.0.1:5335 - Switch from Parallel requests to Load-balancing (irrelevant with a single upstream, but cleaner)
- Click Apply, then Test upstreams
A green confirmation means AdGuard Home can reach Unbound and resolve through it. All DNS queries from devices on the network now recurse against root servers via Unbound, with AdGuard Home handling filtering and logging in front.
Troubleshooting
Section titled “Troubleshooting”Crash loop on service start
Run Unbound directly to see the error:
unbound -c /etc/unbound/unbound.conf -d 2>&1Common causes:
root.hintsmissing - fetch it:curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.root- Port already in use - check if the UCI service is still running:
netstat -lunp | grep 5335 - Config syntax error - install
unbound-checkconf(opkg install unbound-checkconf) and rununbound-checkconf /etc/unbound/unbound.conf
Logs
With use-syslog: yes, Unbound logs to the system log:
logread -f | grep unboundTest resolution directly
From the router:
nslookup google.com 127.0.0.1# ornslookup -port=5335 google.com 127.0.0.1Persistence after firmware upgrade
Section titled “Persistence after firmware upgrade”GL.iNet firmware upgrades preserve a curated set of config files from /etc/ when “Keep Settings” is enabled, but custom files outside the default backup list (like /etc/unbound/ and /etc/init.d/unbound-custom) may not survive. Add them to /etc/sysupgrade.conf to ensure persistence, and back up regardless:
# from your local machinescp root@192.168.8.1:/etc/unbound/unbound.conf ./unbound.confscp root@192.168.8.1:/etc/init.d/unbound-custom ./unbound-customAfter an upgrade, the UCI unbound service may re-enable itself. Check with service unbound status and disable again if needed.
Verification
Section titled “Verification”- Unbound is listening on loopback only -
netstat -lunp | grep 5335prints two127.0.0.1:5335udprows, one per thread (num-threads: 2). - It answers directly -
nslookup -port=5335 google.com 127.0.0.1returns an answer from Unbound itself, not from a forwarder. - AdGuard Home reaches it - Test upstreams in Settings -> DNS settings is green with
127.0.0.1:5335as the only upstream. - Clients recurse through the pair -
nslookup google.com 127.0.0.1resolves on port 53 with no public resolver left in the upstream list.
Gotchas and lessons learned
Section titled “Gotchas and lessons learned”- The OpenWrt
unboundpackage’s init script generates its own config into/var/lib/unbound/unbound.confand never reads/etc/unbound/unbound.conf, so starting it with your config crash-loops - disable it and run the custom procd script instead (Part 1, Part 3). - Both init scripts bind port 5335, so the UCI service has to be stopped before the custom one starts (Part 3).
do-daemonize: nois required under procd - a forking Unbound reads as a crash (Part 2).- DNSSEC validation fails on first start without
root.key;unbound-anchorcreates it (Part 1). use-caps-for-id: yes(0x20 encoding) breaks resolution against authoritative servers that do not preserve query-name casing, which is why the config disables it (Part 2).so-sndbufandso-rcvbufare omitted because the sysctl tuning they need is not present on stock OpenWrt (Part 2).- A public fallback upstream reintroduces a third-party resolver whenever Unbound is unreachable (Part 4).
- Default Dropbear ships no
sftp-server, so a modernscpfails untilopenssh-sftp-serveris installed (Prerequisites). - Custom files outside GL.iNet’s backup list do not survive a firmware upgrade, and the UCI
unboundservice can re-enable itself afterwards (Persistence after firmware upgrade).
File reference
Section titled “File reference”| Path | What it holds |
|---|---|
/etc/unbound/unbound.conf | The resolver config, transferred with scp from your working copy |
/etc/unbound/root.hints | Root server list, fetched from internic.net |
/etc/unbound/root.key | DNSSEC trust anchor, created by unbound-anchor |
/etc/init.d/unbound-custom | procd init script that runs Unbound against that config |
/etc/init.d/unbound | The UCI-generated init script this guide disables with service unbound disable |
/var/lib/unbound/unbound.conf | Where the UCI script generates its own config; not read by the custom service |
/etc/sysupgrade.conf | Add the custom files here so a firmware upgrade keeps them |
| AdGuard Home upstream list | 127.0.0.1:5335 as the single upstream (Settings -> DNS settings) |