Split-horizon DNS
One zone, two (or more) authoritative views: clients get different answers depending on where they are asking from. Open-source DNS servers call this "views"; commercial DNS appliances usually call it "DNS Views"; Windows Server calls it "split-brain DNS", a name you will also hear for the whole concept; cloud DNS providers dress the same idea up as "geo-routing." Same machinery everywhere.
Why split-horizon exists#
Three reasons people stand it up:
- Internal-only services. File servers, admin UIs, monitoring dashboards exist only inside the perimeter. You want them resolvable inside,
NXDOMAINoutside. - Same name, different address. A service has both internal and external endpoints (a load balancer with both a private VIP and a public VIP, a SaaS app with a corporate-network fast path). Each client gets the IP that's actually reachable for them.
- Geographic / policy splits. Return different answers based on viewer location (geo-DNS) or subscriber class. Technically the same machinery generalized, the match criterion is just "where are you" instead of "which network are you on."
The textbook enterprise example: vpn.example.com returns 10.x.x.x to corporate clients (so traffic stays internal) and 203.0.113.5 to the public (so VPN dial-in still works).
How it works mechanically#
A view is a (match-criteria, zone-data) pair. The server evaluates which view matches the requesting client, typically by source IP, sometimes by the interface the query arrived on or a TSIG key the client signed with, then answers from that view's copy of the zone.
client ┌────────────────┐
(internal subnet) --> │ resolver │
│ which view? │
│ "internal" │
│ | │
│ zone copy A │ --> 10.0.0.5
└────────────────┘
client ┌────────────────┐
(public Internet) --> │ resolver │
│ which view? │
│ "external" │
│ | │
│ zone copy B │ --> 198.51.100.5
└────────────────┘The two zone copies share a name (e.g., example.com.) but are independent objects. You can publish completely different record sets in each, different RR types, different TTLs, different NS records.
Where it gets implemented#
| Implementation style | How views are defined |
|---|---|
| DNS appliances with first-class Views | Named views bound to match criteria (ACL, source network, TSIG key). The same zone name can live in multiple views with independent RR sets. Edit through a single UI; the correct view's data reaches each client. |
| Open-source DNS server config (BIND) | A view { } stanza in the config file that scopes a zone to a set of match criteria (match-clients: source IPs and/or TSIG keys). Edit the zone file, reload, done. See query-types for the record types you can serve differently per view. |
| Windows Server DNS policies | One zone plus "zone scopes", each with its own record set; a query-resolution policy picks the scope by client subnet or server interface (Microsoft's split-brain deployment guide). Same effect as views, different plumbing. |
| Cloud DNS (geo-routing / weighted policies) | A single public zone with answers selected by viewer geography or weight. Not a true view, but covers some of the same use-cases. |
| Conditional forwarding (client side) | Cheap but brittle: send internal.example.com to the internal resolver, everything else to public. Not split-horizon proper; it is a forwarder workaround that breaks the moment a name shows up in both zones. |
The cleanest experience in the field is on DNS appliances that treat views as a first-class object: the same example.com zone can carry an internal record set served to one network segment, an external record set served to a public-facing nameserver, and a third "DMZ" view served to a partner network, all under one zone name, edited through one UI.
Common gotchas#
1. Records drift between views. Two zone copies, two places to edit. Someone adds a host to the external view and forgets the internal one. Automation that templates both copies from a single source of truth (Ansible, Terraform, a vendor API) is the cure, and it is well worth standing up before you have ten views.
2. DNSSEC is per-view. Each view is a separate zone copy, signed (or not) on its own. The trap is the parent: once the public delegation publishes a DS record, every resolver validating through the public chain expects that zone's answers to validate against it. An internal view left unsigned, or signed with keys the DS does not match, is judged bogus (RFC 4033 §5) and clients get SERVFAIL. Fix it by signing both views with the same keys so the one DS covers both, or by giving internal validating resolvers a locally configured trust anchor for the internal view's key (a negative trust anchor, RFC 7646, also silences it, but it is meant as a temporary switch-off, not a design). See dnssec-troubleshooting for how to diagnose this.
3. Clients bypass the internal resolver. A laptop on the corporate LAN with 8.8.8.8 hard-coded in /etc/resolv.conf will never see the internal view. Either intercept at the gateway (transparent DNS redirection) or push the internal resolver via DHCP options and block outbound port 53 to anyone else. Blocking port 53 is no longer enough on its own: a browser or OS doing its own encrypted DNS (DoH/DoT) to a public resolver never touches the internal view either, so client DNS settings have to be managed, not just filtered.
4. NS records leak the wrong nameservers. The internal view's NS records should point at internal nameservers, the external view's at external ones. Cross-pollination here causes recursive resolvers chasing referrals to land on the wrong side and either fail or, worse, succeed with the wrong data.
5. Cached answers cross horizons. A laptop resolves gitlab.example.com to the internal IP at the office, takes the cached entry home, and gets a connection-refused on a private IP that is unreachable from there. Low TTLs help; per-view TTLs help more.
Testing each view#
You cannot always switch source IPs at will, but two techniques cover most debugging. Replace ns-internal.example.com, ns-external.example.com, and service.example.com with your own nameservers and names.
# Ask each authoritative server directly (bypasses recursive caching)
dig @ns-internal.example.com service.example.com A
dig @ns-external.example.com service.example.com A# nslookup equivalent: second argument is the server to ask
nslookup -type=A service.example.com ns-internal.example.com
nslookup -type=A service.example.com ns-external.example.com# Or from a host that sits on the other side of the perimeter
ssh bastion-on-public-net 'dig +short service.example.com A'nslookup can produce the same result as the ssh command above, but the limiting factor is your vantage point, not the tool. Use whichever you have available on the remote host.
Live example. isitdns.net uses a single-view public zone, so both of its authoritative nameservers return identical answers. This is the correct, expected result for a non-split zone:
dig @coleman.ns.cloudflare.com isitdns.net A +short
dig @hera.ns.cloudflare.com isitdns.net A +short172.67.155.242
104.21.56.211
104.21.56.211
172.67.155.242Record order rotates between queries, so read nothing into the ordering. The pair itself can also differ by where you ask from: these are Cloudflare anycast proxy addresses, and a vantage in another region may get a different pair for the same name (a European probe sees a 188.114.x.x pair here). The test is that both nameservers agree with each other from your vantage, not that they match the addresses printed above.
nslookup -type=A isitdns.net coleman.ns.cloudflare.com
nslookup -type=A isitdns.net hera.ns.cloudflare.comIf you ran those same two queries against a split-horizon zone and got different IP addresses back, that is the split working correctly.
Decision table:
| Result | Diagnosis |
|---|---|
| Both authorities return the same answer for a name that should be view-split | View definitions are not matching the way you expect; check match-clients / ACL |
| Authorities return different answers but a specific client gets the wrong one | That client is not being directed at the right view: source-IP mismatch, NAT rewriting source, or wrong resolver in resolv.conf |
Both authorities return NXDOMAIN | The name is not in either view; this is a different bug, not a split-horizon issue |
When not to use split-horizon#
- You only need internal-only names. Carve a separate internal-only zone like
internal.example.comand publish it on the internal resolver only. No view machinery needed; a separate authoritative zone on your internal nameserver is sufficient. - You only need to hide a few public records. That is an HTTP-layer or authn problem, not a DNS one.
- You can solve it at L3. A name that resolves the same everywhere but lands on a load balancer that routes by source keeps DNS simple and moves the routing logic to the LB. Easier to reason about, easier to monitor.
Policy interception vs. zone-copy views#
Some DNS implementations (network appliances, home gateways, filtering resolvers) achieve "different answers for different clients" without actually maintaining multiple zone copies. The mechanism is policy-driven interception: the resolver intercepts queries matching certain patterns (a local domain, a blocklist, a per-device policy tag) and overrides the answer, forwarding everything else upstream.
This is not split-horizon in the strict sense. A few hard limits make the distinction clear:
- No zone definitions. A policy-based interceptor does not own authoritative zones for public names. It can rewrite answers, but it cannot serve records for
example.comfrom its own zone data. - No AXFR/IXFR. There is no zone transfer. Changes to zone content on a real authoritative server do not propagate to the interceptor; the interceptor is not a secondary.
- View selection may use client identity, not source IP. Some implementations identify clients by MAC address (which survives DHCP lease reassignment) rather than source IP. The match criterion is more stable, but the selection is not a DNS-protocol-level ACL: it is an application-layer policy lookup.
- The split only applies inside the controlled network. An upstream authoritative server does not see the interceptor's policy. Queries that escape to an external resolver bypass the split entirely.
When the lightweight approach is not enough: reach for a DNS server doing real views when you need:
- Multiple zone copies with completely different RR sets (different TTLs, different record types, different NS records per view)
- View selection applied at an authoritative server that public resolvers query directly
- AXFR/IXFR between a primary and a secondary that need to stay in sync per-view
- Publishing different A or AAAA addresses for the same name to public internet clients vs. internal clients, at the authoritative level, not just at the recursive/interceptor level
If any of those describe your requirement, you want a DNS server with first-class view support (BIND view { } blocks, or a DNS appliance with named views), not a policy overlay.
See also#
- anatomy-of-a-query, what happens before a view decision
- Authoritative zones, the unit a view holds
- Zone transfer (AXFR/IXFR), how per-view primaries replicate to secondaries
- dnssec, why per-view signing creates independent trust chains
- nslookup-and-dig, the two tools used throughout the testing section
- When it actually is DNS, split-horizon as an incident source