4 minutes
dnsmasq & WireGuard
DNS Caching for a WireGuard Hub with dnsmasq
TL;DR: Running dnsmasq on a WireGuard hub caches DNS results for all connected peers, reducing latency and upstream load — but getting it to play nicely with Ubuntu’s resolvconf integration requires a non-obvious fix.
Background
When building a WireGuard hub on Ubuntu 20.04, I wanted to improve DNS performance by adding a local caching resolver. The idea: instead of every peer’s DNS query going straight to an upstream server, the hub itself runs dnsmasq to cache responses — speeding up repeat lookups across all connected clients.
The catch is that Ubuntu (since 12.04) no longer installs dnsmasq by default. It relies on systemd-resolved to manage DNS resolution. On a server, I prefer to sidestep this entirely: read the upstream DNS addresses from the VPS or cloud provider’s network config, stop systemd-resolved, and let dnsmasq take full control.
Architecture
The goal is to run dnsmasq exclusively on the WireGuard interface (wg0) — caching and forwarding DNS for VPN peers without touching the host’s own resolution:
VPN Peer → wg0 (192.168.14.11) → dnsmasq cache → Upstream DNS (192.168.15.1)
dnsmasq Configuration
The config is intentionally minimal and scoped to wg0 only:
# /etc/dnsmasq.conf
# Do not read upstream servers from /etc/resolv.conf
no-resolv
# Do not use /etc/hosts for name resolution
no-hosts
# Cache up to 10,000 DNS records in memory
cache-size=10000
# Query upstreams in order, not in parallel
strict-order
# Upstream DNS server
server=192.168.15.1
# Bind only to the WireGuard interface address
listen-address=192.168.14.11
# Bind strictly to listed interfaces only
bind-interfaces
interface=wg0
except-interface=lo
Note:
bind-interfacesis used instead ofbind-dynamicto strictly restrict dnsmasq towg0. Combined withno-resolvandno-hosts, dnsmasq becomes a pure forwarding cache for WireGuard peers — it has no visibility into the host’s local DNS.
The Problem: resolv.conf Gets Hijacked
After starting dnsmasq, a strange symptom appeared: local DNS resolution on the host broke entirely.
Inspecting /etc/resolv.conf revealed the cause:
| State | resolv.conf nameserver |
|---|---|
| Before dnsmasq starts | 127.0.0.53 (systemd-resolved stub) |
| After dnsmasq starts | 127.0.0.1 ← but dnsmasq isn’t listening here! |
Something was rewriting resolv.conf to point at 127.0.0.1 even though dnsmasq was only bound to 192.168.14.11 on wg0.
Tracing the Root Cause
The culprit is a resolvconf integration script at /etc/resolvconf/update.d/dnsmasq. It cannot be removed (system integrity), and it provides no configuration option to disable itself. The solution has to come from elsewhere.
Looking at the dnsmasq init script at /etc/init.d/dnsmasq, there’s a start_resolvconf() function that runs on every start:
start_resolvconf() {
# If "lo" is listed in DNSMASQ_EXCEPT, exit early — skip resolvconf.
# This is the only clean exit point we can use.
for interface in $DNSMASQ_EXCEPT; do
[ $interface = lo ] && return
done
# Also skipped if DNS is fully disabled (port=0) — not useful here.
if grep -qs '^port=0' /etc/dnsmasq.conf; then
return
fi
# This line rewrites /etc/resolv.conf to nameserver 127.0.0.1
if [ -x /sbin/resolvconf ] ; then
echo "nameserver 127.0.0.1" | /sbin/resolvconf -a lo.$NAME
fi
return 0
}
There is exactly one early-exit path: if $DNSMASQ_EXCEPT contains lo, the function returns before touching resolvconf. The port=0 branch isn’t usable — we need DNS enabled. That for loop is our only lever.
The Fix
Set DNSMASQ_EXCEPT=lo in /etc/default/dnsmasq. This satisfies the early-return condition in start_resolvconf() and prevents any rewrite of /etc/resolv.conf.
Important:
IGNORE_RESOLVCONF=yesalso exists in the init script, but setting it alone is not sufficient to prevent the resolvconf rewrite in the current Ubuntu init logic. Both variables are needed together.
Since dnsmasq is started and stopped via WireGuard’s PostUp/PreDown hooks, the /etc/default/dnsmasq file is written dynamically by the startup script:
#!/bin/bash
# /etc/wireguard/scripts/wg0-postup.sh
# Write /etc/default/dnsmasq before starting.
# DNSMASQ_EXCEPT=lo triggers the early-exit in start_resolvconf(),
# preventing resolvconf from rewriting /etc/resolv.conf.
# IGNORE_RESOLVCONF=yes is an additional guard; neither alone is enough.
echo "IGNORE_RESOLVCONF=yes" > /etc/default/dnsmasq
echo "DNSMASQ_EXCEPT=lo" >> /etc/default/dnsmasq
# Start dnsmasq — binds to wg0 (192.168.14.11) only
/etc/init.d/dnsmasq start
exit 0
With this in place:
- dnsmasq binds exclusively to
wg0and caches up to 10,000 records - DNS queries from VPN peers are served from cache or forwarded to
192.168.15.1 - The host’s
/etc/resolv.confis left completely untouched
Update — August 7, 2021
Jordan Whited published a post on WireGuard Endpoint Discovery and NAT Traversal using DNS-SD. In the Doubling Down on WireGuard section, he proposes using SRV records to expose WireGuard peer information via DNS Service Discovery.
The approach uses CoreDNS — a plugin-based DNS server written in Go — with a custom plugin that answers DNS-SD queries with WireGuard peer metadata. This is a compelling alternative architecture for dynamic peer discovery, eliminating the need for a central controller in mesh topologies where peers need to find each other’s endpoints automatically.
Based on the original write-up at sskaje.me.
Photo by Ferhat Deniz Fors on Unsplash.