Split-Horizon DNS at Home: One Domain, Two Answers
Why your own domain should resolve to a private IP indoors and a public one outdoors — and how to make that happen cleanly

Contents
Here’s a problem that arrives the moment you self-host something with a proper hostname. You’ve got a service at nextcloud.mylab.local, or better still at cloud.example.com using a real certificate, and you want to reach it both from your sofa and from a train. From the train the name has to resolve to your public IP so traffic can find its way in through the router. From the sofa, resolving to the public IP means your packets go out to the internet and try to come straight back in — a manoeuvre called NAT hairpinning that many home routers do badly or refuse to do at all. The clean answer is to give the same name two different addresses depending on who’s asking. That is split-horizon DNS, and once you understand why you want it, the how is short.
Why one answer isn’t enough
Think about what a hostname is for: it’s a stable label so you don’t have to memorise IPs. The trouble is that the right IP for a service depends on where you’re standing. A device on your LAN should talk to the service over the local network — fast, private, never touching the internet. A device out in the world has no route to your 192.168.1.x addresses, so for it the name must resolve to your public IP, where your router is waiting to forward the connection in.
If you publish only the public IP, indoor clients suffer the hairpin route. When it works at all, the traffic leaves your LAN, hits the router’s WAN side, gets NATed back inward, and returns — which wastes the loop, sometimes breaks source-address assumptions, and often just fails silently on consumer gear. If you publish only the private IP, anyone off your network gets an address they can’t reach. Neither single answer serves both audiences, which is the whole reason split horizon exists: your internal resolver answers with the LAN IP, the public DNS answers with the WAN IP, and each client gets the address that actually works from where it is.
There’s a second, subtler win. When indoor traffic to cloud.example.com stays on the LAN, it never leaves the building. Your reverse proxy still terminates TLS with a valid public certificate — the certificate is tied to the name rather than to the IP — so you keep HTTPS everywhere while keeping internal traffic internal. That combination is exactly what makes a self-hosted setup feel like a real service rather than a pile of port numbers.
How it works in practice
You need two things: a public DNS record (in your registrar’s or DNS provider’s zone) pointing the name at your public IP, and a local override on your internal resolver pointing the same name at the LAN IP. Internal clients use your resolver — the same box running your ad-blocking sinkhole — so they pick up the override. External clients query public DNS and get the public IP. The name is identical; the answer forks on which resolver replies.
The local override goes by different names in different tools. AdGuard Home calls them DNS rewrites. Pi-hole calls them local DNS records. Under the hood, if you’re using dnsmasq (which Pi-hole is built on), it’s a one-line address or host-record directive. A typical dnsmasq snippet for a handful of internal services looks like this:
| |
That wildcard line is the quiet hero. Point *.lab.example.com at your reverse proxy and every new service you spin up is reachable internally the instant the proxy has a route for it — no per-name DNS edit each time. The reverse proxy reads the Host header and sends the request to the right container. You add services by editing the proxy, and DNS just works.
In AdGuard Home the same thing is a rewrite rule: *.lab.example.com → 192.168.1.20, entered in the Filters → DNS rewrites page. If you’d rather not run a reverse proxy at all, you point each name at the individual service’s IP — the mechanism is identical, you just list more addresses.
Your public zone, meanwhile, only needs to know about the names that should be reachable from outside:
| |
If your WAN IP is dynamic, this is where a dynamic-DNS updater earns its keep — it keeps that public A record chasing your changing address. The internal override never changes because your LAN IPs are stable.
Real domain or made-up internal name?
A fork in the road worth deciding deliberately: do you split-horizon a real domain you own, or invent an internal-only suffix like .lab or .home? Using a real domain (example.com) lets you obtain genuine public certificates via ACME DNS-01 challenges for internal-only names, so even a service that never faces the internet gets valid HTTPS with no browser warnings. That’s the setup I run and recommend. The cost is that you’re maintaining two views of the same zone and must keep the rebind exceptions straight.
An invented suffix keeps internal and external cleanly separated — nothing under .lab ever exists publicly, so there’s no zone to split. The downside is certificates: no public CA will issue for a name it can’t validate, so you’re stuck with a private CA that every device has to trust, or with unencrypted internal traffic. Avoid squatting on suffixes that might become real (.dev and .app are already public TLDs with enforced HTTPS, which will bite you). If you go invented, use something reserved-in-spirit and consistent. For most people who already own a domain, splitting the real one is less total hassle over a year.
Where split horizon meets the rest of your network
Split horizon assumes your indoor clients actually use your resolver. That’s usually true, and it’s the same assumption the sinkhole relies on, so the two reinforce each other. It gets more interesting once you segment the network. If you’ve followed the VLAN approach to home networking, a device on your guest or IoT VLAN might be pointed at a different resolver, or firewalled off from your internal services entirely. That’s usually what you want — the doorbell has no business reaching cloud.example.com — but it means “internal” is a per-VLAN idea, and you may want the override to answer only for the segments that should reach the service.
The interaction with NAT is worth spelling out, because split horizon is partly a way to avoid depending on your router’s hairpin behaviour. If you’re already fighting your ISP’s setup — say you’re stuck behind carrier-grade NAT — then external reachability is a separate battle covered in escaping double-NAT. Split horizon fixes the internal half regardless of how the external half is solved, which is why I set it up first: indoor access starts working immediately even while I’m still sorting out the route from the outside world.
Troubleshooting: the rebind trap and friends
Internal name resolves to nothing, or to the public IP. The single most common cause is DNS rebind protection. Many resolvers and routers refuse, by default, to return a private (RFC 1918) address for a public domain, because that behaviour is a classic attack pattern. Your legitimate split-horizon override looks exactly like the attack, so it gets stripped. In AdGuard Home this is the “Disable IPv6” area’s neighbour — look for a rebinding/private-address option and whitelist your domain. In dnsmasq, add rebind-domain-ok=/example.com/ (or the broader stop-dns-rebind exceptions). On some routers acting as resolver, you add the domain to a rebind exception list. Until you do this, your careful override silently returns nothing.
Works on one device, fails on another. Check what resolver each device is actually using. A phone with a hard-coded public DNS, or a browser with its own DNS-over-HTTPS turned on, bypasses your internal resolver and gets only the public answer — then hairpins or fails. This is the same bypass that defeats a sinkhole, and the fix is the same: make your resolver the only sanctioned one and, where you can, block outbound port 53 and known DoH endpoints for clients that shouldn’t wander off.
Change made, still resolving the old address. DNS caching, at three possible layers. The resolver caches, the operating system caches, and the application (browsers especially) caches. After editing an override, flush the resolver, and on the client flush the OS cache or just toggle the interface. TTLs on your public records also mean external changes take time to propagate — keep the TTL low on records you expect to edit.
TLS certificate warnings internally. If the internal name resolves fine but the browser complains about the certificate, your reverse proxy is serving the wrong certificate for that Host, or you pointed the override at the service directly instead of at the proxy that holds the cert. Send internal traffic to the same proxy that terminates TLS for the public name and the certificate matches, because it’s issued to the name, and the name is what you typed.
Wildcard override not catching a sub-subdomain. dnsmasq’s address=/.lab.example.com/ matches lab.example.com and everything beneath it; double-check you included the leading dot for the wildcard form, and remember it won’t match a sibling like otherlab.example.com.
The verdict
Split-horizon DNS is one of those setups that takes ten minutes and then quietly saves you grief for years. If you self-host anything you reach by name from both inside and outside — and if you’ve gone to the trouble of real certificates you almost certainly do — set it up. Indoor access stops depending on your router’s hairpin mercy, internal traffic stays on the LAN where it’s fast and private, and you keep valid HTTPS everywhere without contortions.
The one honest caveat is the rebind trap: budget five minutes for the “why is my override returning nothing” dance, whitelist your domain, and it’ll behave. It’s not something to run if you host nothing internal and only ever reach a single service from outside — for that, a plain public record is enough. But the moment you have two or three named services and a household that uses them from the sofa, the small amount of configuration pays for itself the first time someone stops asking you why the address “only works sometimes”.




