Split-Horizon DNS Misconfiguration
Embed This Widget
Add the script tag and a data attribute to embed this widget.
Embed via iframe for maximum compatibility.
<iframe src="https://ipfyi.com/iframe/entity//" width="420" height="400" frameborder="0" style="border:0;border-radius:10px;max-width:100%" loading="lazy"></iframe>
Paste this URL in WordPress, Medium, or any oEmbed-compatible platform.
https://ipfyi.com/entity//
Add a dynamic SVG badge to your README or docs.
[](https://ipfyi.com/entity//)
Use the native HTML custom element.
A split-horizon (split-brain) DNS setup is intended to return private IP addresses to internal clients and public IPs to external clients for the same hostname. Due to misconfiguration, the zones are out of sync — internal clients receive the public IP (routing through the internet for internal services) or external clients receive RFC 1918 private addresses that are unreachable from the internet, causing connection failures on both sides.
Symptoms
- ⚠ Internal clients connect to services via the internet (hairpin NAT) instead of the direct internal path
- ⚠ VPN-connected clients cannot reach internal services by hostname, even though split-tunnel routes are configured correctly
- ⚠ External users get NXDOMAIN or RFC 1918 addresses for a public-facing hostname
- ⚠ The same hostname resolves to different IPs depending on which machine runs the query
- ⚠ Internal DNS resolver returns public IPs for an internal service (e.g., app.corp.com → 203.0.113.10 instead of 10.0.1.50)
- ⚠ After a DNS zone update, internal and external views diverge — one view was updated but the other was not
Possible Root Causes
- • The internal DNS view's match-clients ACL does not cover all internal subnets or VPN tunnel address ranges, so some internal clients fall through to the external view
- • Zone records were updated in the external view but not the internal view (or vice versa), causing the two views to diverge
- • VPN clients are not receiving or not honoring the VPN-pushed DNS server, so they continue using the public resolver and receive external-view responses
- • Internal clients are configured with a public DNS resolver (8.8.8.8) rather than the internal server, bypassing the split-horizon entirely
- • A new subnet was added (cloud environment, new VLAN, new VPN pool) without updating the internal-nets ACL on the DNS server
Diagnosis Steps
Step 1 — Identify which view a client is using
# Query from inside the network (internal view)
dig A app.corp.com @<internal-dns-server>
# Query from outside the network (external view / public resolver)
dig A app.corp.com @8.8.8.8
dig A app.corp.com @1.1.1.1
# Compare the two results — they should return different IPs
# Internal: 10.x.x.x / 172.16.x.x / 192.168.x.x
# External: a public routable IP
Step 2 — Verify which DNS server internal clients actually use
# Linux
resolvectl status | grep "DNS Servers"
cat /etc/resolv.conf
# macOS
scutil --dns | grep nameserver
# Windows
ipconfig /all | findstr "DNS Servers"
If internal clients are using a public resolver (8.8.8.8) instead of the internal one, split-horizon cannot work — all clients will see the public view.
Step 3 — Check zone configuration on the internal DNS server
For BIND 9:
# List all views and their match conditions
grep -A 5 'view "internal"' /etc/bind/named.conf
grep -A 5 'view "external"' /etc/bind/named.conf
# Verify zone file for the internal view
named-checkzone corp.com /etc/bind/zones/internal/db.corp.com
# Verify the match-clients ACL covers the internal subnets
grep -A 10 'acl "internal-nets"' /etc/bind/named.conf
Step 4 — Test from inside a VPN tunnel
VPN clients often fail split-horizon because the VPN push does not override the client's stub resolver configuration:
# On VPN-connected client — what resolver is active?
resolvectl status | grep -A 5 tun0 # Linux
scutil --dns | head -30 # macOS
# Does the VPN-assigned DNS server respond correctly?
dig A app.corp.com @<vpn-pushed-dns>
If the VPN-pushed DNS server is correct but the client is not using it, the issue is in VPN client DNS push configuration (push "dhcp-option DNS 10.0.0.53" in OpenVPN, or DNS setting in WireGuard Peer section).
Step 5 — Confirm zone serial numbers are in sync
Different serials between views means updates have been applied to one view only:
# Check SOA serial for internal view
dig SOA corp.com @<internal-dns>
# Check SOA serial for external view
dig SOA corp.com @<external-dns>
# Or directly on the nameserver
rndc zonestatus corp.com IN internal
rndc zonestatus corp.com IN external
Step 6 — Test hairpin NAT (the common symptom)
If internal clients are hitting the public IP of an internal service:
# traceroute from internal client to the service hostname
traceroute app.corp.com
# Does the path go out to the internet and back?
# Internal direct: 10.0.0.1 → 10.0.1.50 (1-2 hops)
# Hairpin (broken): 10.0.0.1 → 203.0.113.10 → NAT back in → 10.0.1.50 (many hops)
Solution
Fix 1 — Update match-clients ACL to include all internal ranges
# /etc/bind/named.conf or named.conf.local
acl "internal-nets" {
10.0.0.0/8;
172.16.0.0/12;
192.168.0.0/16;
10.8.0.0/24; # Add VPN pool
localhost;
localnets;
};
view "internal" {
match-clients { internal-nets; };
# ...
};
view "external" {
match-clients { any; };
# ...
};
# Validate and reload
sudo named-checkconf
sudo rndc reload
Fix 2 — Synchronize zone records across views
After any DNS record change, update BOTH views and increment the SOA serial in each:
# Update internal zone file
sudo vim /etc/bind/zones/internal/db.corp.com
# Update external zone file
sudo vim /etc/bind/zones/external/db.corp.com
# Reload both zones
sudo rndc reload corp.com IN internal
sudo rndc reload corp.com IN external
# Verify serials match (or internal > external for recently updated)
dig SOA corp.com @<internal-ns>
dig SOA corp.com @<external-ns>
Fix 3 — Fix VPN DNS push
For OpenVPN:
# server.conf — push DNS to all clients
push "dhcp-option DNS 10.0.0.53"
push "dhcp-option DNS 10.0.0.54"
For WireGuard (client config):
[Interface]
DNS = 10.0.0.53
[Peer]
# ...
Fix 4 — Enforce internal DNS on internal clients (DHCP option 6)
# ISC DHCP — /etc/dhcp/dhcpd.conf
subnet 10.0.0.0 netmask 255.255.255.0 {
option domain-name-servers 10.0.0.53, 10.0.0.54;
}
Prevention
- Maintain a single source-of-truth zone file for each domain and generate internal/external views from it programmatically — manual dual-maintenance is error-prone
- Add automated testing after every DNS change: verify that the same hostname resolves to the internal IP from a host inside each internal subnet and to the public IP from an external resolver
- Document all internal subnets and VPN pools in the DNS server's ACL comments; update the ACL whenever a new network segment is provisioned
- Monitor for hairpin NAT traffic at the perimeter firewall — internal-to-internal traffic that leaves and re-enters the network indicates split-horizon failure
- Use a configuration management tool (Ansible, Terraform) to apply DNS changes to both views atomically, preventing one-view-only updates