DHCP and Split-Horizon DNS - Local Name Resolution on the LAN

Status: Active
Last Updated: 2026-08-26
Category: Networking - Phase 2: Services
Prerequisites: dns-fundamentals, dns-explained, ip-addressing-subnets
Time: 2 hours
Tags: dnsmasq, dhcp, split-horizon, dns, lan, overrides

Summary

Run dnsmasq on the LAN to serve DHCP leases and split-horizon DNS: internal clients resolve *.fogserv.cloud straight to local RFC1918 addresses while the public records keep pointing at Cloudflare for outside visitors. No hairpin NAT, no CDN round-trips from inside your own network.

๐ŸŽฏ What You'll Learn

By the end of this article, you'll be able to:


Table of Contents

  1. Context / Why This Matters
  2. Implementation / Core Content
  3. Practical Examples
  4. Troubleshooting & Common Pitfalls
  5. Next Steps / Ops Actions

Context / Why This Matters

All public services live behind Cloudflare (cloudflare-dns) at edge IPs. When a LAN client resolves grafana.fogserv.cloud it gets the Cloudflare edge โ€” traffic leaves the house and comes back. Split-horizon DNS fixes this by answering A grafana.fogserv.cloud โ†’ 192.168.20.x internally, keeping traffic on the LAN (faster, works when the WAN is down, no CDN limits). It pairs naturally with reverse proxies that already hold valid certs via DNS-01 (letsencrypt-automation), so internal HTTPS still validates.

Implementation / Core Content

Install dnsmasq

sudo apt install dnsmasq
sudo systemctl disable --now systemd-resolved   # free port 53
sudo rm /etc/resolv.conf && echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf

/etc/dnsmasq.d/main.conf

# ---- DHCP ----
interface=ens18
bind-interfaces
domain=fogserv.cloud
dhcp-range=192.168.20.100,192.168.20.199,12h
dhcp-option=option:router,192.168.20.1
dhcp-option=option:dns-server,192.168.20.2    # this host

# Static reservations
dhcp-host=b8:27:eb:aa:bb:cc,rpi-cluster01,192.168.20.30,infinite
dhcp-host=52:54:00:11:22:33,truenas,192.168.20.40,infinite

# Don't hand out addresses to unknown MACs on sensitive segments (optional)
# dhcp-ignore=tag:!known

# ---- DNS: split horizon ----
# Local answers first; everything else forwarded upstream
local=/fogserv.cloud/
address=/grafana.fogserv.cloud/192.168.20.10
address=/kuma.fogserv.cloud/192.168.20.10
address=/git.fogserv.cloud/192.168.20.11
cname=media.fogserv.cloud,jellyfin.lan

# Upstream resolvers for everything else
server=1.1.1.1
server=9.9.9.9

# Hygiene
no-resolv
cache-size=2000
log-queries          # enable temporarily when debugging; noisy otherwise
no-hosts             # we manage names here, not in /etc/hosts
dnsmasq --test                 # syntax check
sudo systemctl restart dnsmasq
ss -lunp | grep :53            # confirm listening

Pointing clients at it

Two options:

  1. Router hands out the resolver via its own DHCP WAN/LAN settings โ€” set DNS to 192.168.20.2. Simplest, catches every device.
  2. dnsmasq is the DHCP server (config above) โ€” it already tells clients itself.

Never run two DHCP servers on one L2 segment; pick either the router or dnsmasq.

Why not override the whole zone?

address=/fogserv.cloud/192.168.20.10 would answer every subdomain with one IP. That breaks per-service routing unless one proxy fronts them all with host-based rules. Prefer explicit per-name address= lines so each service maps to its own proxy/back-end.

Practical Examples

Verify split-horizon behavior

dig +short grafana.fogserv.cloud @192.168.20.2
# 192.168.20.10   <- internal view

dig +short grafana.fogserv.cloud @1.1.1.1
# 104.21.x.y      <- public Cloudflare edge

Confirm a DHCP lease

cat /var/lib/misc/dnsmasq.leases
# 1724640000 b8:27:eb:aa:bb:cc 192.168.20.30 rpi-cluster01 ff:...

Test from a laptop

curl -sI https://grafana.fogserv.cloud | head -1
# HTTP/2 200 โ€” served over LAN; cert still valid because it was issued publicly

Quick ad-blocking variant

Add conf-file=/etc/dnsmasq.d/blocklist where blocklist contains thousands of address=/doubleclick.net/ lines โ€” same mechanism as the service overrides.

Troubleshooting & Common Pitfalls

Problem Cause Fix
Port 53 already in use systemd-resolved stub listener Disable resolved or set DNSStubListener=no, restart
Clients get IPs but no internet Wrong router option / upstream dead Check dhcp-option=option:router; test dig @1.1.1.1 from host
Double DHCP on segment Router DHCP left enabled Disable router DHCP or drop dnsmasq's dhcp-range
Internal HTTPS shows wrong cert Wildcard missing / SNI mismatch Issue wildcard via DNS-01; see letsencrypt article
DNS loop: dnsmasq forwards to itself Upstream = own IP no-resolv plus explicit external server= lines
New device name not resolving Lease table vs address= mismatch Names come from config/leases; add explicit address= lines
Changes don't apply Forgot restart or syntax error dnsmasq --test then systemctl restart dnsmasq

Next Steps / Ops Actions

Sources & Related

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse