Install
# Debian / Ubuntu
sudo apt install dnsmasq
# On systems where systemd-resolved listens on :53, you'll conflict; disable resolved's stub:
sudo nano /etc/systemd/resolved.conf
# DNSStubListener=no
sudo systemctl restart systemd-resolved
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
Or just run dnsmasq on the router / dedicated DHCP box, not on a desktop with resolved (see that tutorial).
The minimum-viable config
# /etc/dnsmasq.conf
# DNS: upstream servers for queries dnsmasq can't answer locally
server=1.1.1.1
server=9.9.9.9
# Don't read /etc/resolv.conf (we set servers explicitly)
no-resolv
# Bind only to the LAN interface, never the WAN
interface=eth0
bind-interfaces
# Cache size (entries)
cache-size=10000
# Don't forward names without a dot (single-label names like "router")
domain-needed
# Don't forward reverse lookups for RFC1918 addresses upstream
bogus-priv
# Local domain
domain=lab.example.com
local=/lab.example.com/
expand-hosts
# DHCP — range + lease time
dhcp-range=192.168.1.100,192.168.1.200,12h
# DHCP options pushed to clients
dhcp-option=3,192.168.1.1 # default gateway
dhcp-option=6,192.168.1.1 # DNS = this dnsmasq itself
dhcp-option=15,lab.example.com # domain name
# Log DHCP transactions (for debugging)
log-dhcp
log-facility=/var/log/dnsmasq.log
sudo systemctl restart dnsmasq
sudo systemctl status dnsmasq
journalctl -u dnsmasq -f
DHCP reservations: stable IPs for specific MACs
# In dnsmasq.conf or /etc/dnsmasq.d/reservations.conf
# Reserve an IP for a specific MAC + assign a hostname
dhcp-host=aa:bb:cc:dd:ee:ff,192.168.1.10,nas,infinite
dhcp-host=11:22:33:44:55:66,192.168.1.20,printer,7d
# Reserve by hostname only (IP from pool, no specific IP)
dhcp-host=laptop-amir
The hostname assigned here also becomes resolvable in the local domain — nas.lab.example.com auto-resolves to 192.168.1.10 as long as expand-hosts + domain= are set.
Local DNS records
Three ways to define local names:
- /etc/hosts — dnsmasq reads it by default. Add static names:
# /etc/hosts 192.168.1.10 nas nas.lab.example.com 192.168.1.30 proxmox proxmox.lab.example.com - address= directive — wildcard / pattern resolution:
address=/.lab.example.com/192.168.1.30 # every *.lab.example.com -> .30 address=/ads.googletagservices.com/0.0.0.0 # null-route (basic ad block) - host-record — explicit A + PTR pairs with options for TTL:
host-record=internal.lab.example.com,192.168.1.50,3600
Conditional forwarding: query specific zones to specific resolvers
# Send all .corp queries to the corporate DNS server
server=/corp.example.com/10.0.5.1
# Send all .lab queries locally; never forward upstream
local=/lab.example.com/
# Send DNS-over-TLS via stubby / cloudflared / unbound on localhost
server=127.0.0.1#5053
The server=/zone/<ip> directive overrides the default upstream for that zone only.
DNS-level ad blocking
# Drop a hosts-format blocklist in
# /etc/dnsmasq.d/blocklist.conf
# (e.g. from someonewhocares.org/hosts/)
addn-hosts=/etc/dnsmasq.d/blocklist.hosts
Or use Pi-hole, which is essentially dnsmasq's FTL fork with a web UI for managing blocklists. For homelab setups where you want both DHCP + DNS + blocking from one box, Pi-hole is the polished package; for "just DNS + DHCP for my LAN," plain dnsmasq is lighter.
PXE booting
dnsmasq's TFTP + PXE support makes it the easiest way to net-boot machines for diskless OS installs:
enable-tftp
tftp-root=/srv/tftp
# Hand out PXE info
dhcp-boot=pxelinux.0
dhcp-match=set:efi-x86_64,option:client-arch,7
dhcp-boot=tag:efi-x86_64,bootnetx64.efi
Drop the bootloader binaries in /srv/tftp/; install media (Debian / Ubuntu / iPXE) in the right paths; the BIOS-PXE handshake works. Used to flash dozens of identical homelab nodes without USB keys.
The "captive portal" hijack pattern
# Redirect all DNS queries to a specific IP (e.g. a landing page)
address=/#/10.0.5.10
Match-all (/#/) forces every DNS query to resolve to one IP. Useful for the captive-portal "before you log in, every site goes to the portal" trick. Don't deploy on a regular network — it's wholesale DNS hijacking.
IPv6 RA / DHCPv6
# IPv6 stateless auto-config (RA only, clients pick their own addresses)
dhcp-range=fd00:abcd:1234::,ra-only
# Stateful DHCPv6 (dnsmasq assigns specific IPv6 addresses)
dhcp-range=fd00:abcd:1234::100,fd00:abcd:1234::200,64,12h
# Advertise this router as the IPv6 default
ra-param=eth0,10,300
# Push DNS option to clients
dhcp-option=option6:dns-server,fd00:abcd:1234::1
Reload + inspect
# Send HUP to reload (re-reads /etc/hosts, blocklists, leases)
sudo systemctl reload dnsmasq
# Active leases
cat /var/lib/misc/dnsmasq.leases
# Format: expiry-epoch MAC IP hostname client-id
# Stats
sudo killall -USR1 dnsmasq
journalctl -u dnsmasq | tail -50 # prints cache stats
When dnsmasq isn't the right tool
- Authoritative DNS for public zones. Use PowerDNS (see that tutorial) or Knot DNS — dnsmasq isn't authoritative.
- Multi-VLAN / enterprise DHCP with elaborate policies. ISC Kea (the modern replacement for ISC DHCP) has classes, hooks, and HA. dnsmasq is single-server.
- Recursive resolver with DNSSEC validation. dnsmasq can validate DNSSEC but Unbound is the standard recursive resolver; better caching strategies for high-traffic.
- Very large LANs (1000+ DHCP clients). dnsmasq handles a few hundred fine; beyond that, dedicated DHCP servers scale better.
Worth knowing
- The one binary really does everything. Each feature is a flag; you can run a DNS-only or DHCP-only dnsmasq, no need to enable both.
- Configs in /etc/dnsmasq.d/ — drop separate files; the main config has
conf-dir=/etc/dnsmasq.dby default. Cleaner than one big file. - The leases file is the truth. Restoring dnsmasq state across boxes means copying
/var/lib/misc/dnsmasq.leases; otherwise newly-booted clients get fresh leases.