Skip to main content
RunBook Academy

OPNsenseXVIII · DNS and UnboundSplit-horizon and overrides

Host overrides and split-horizon DNS

Intermediate⏱ ~12 minunbound-controldrillhost

What you'll learn

  • Explain when split-horizon DNS is the right answer and when it is the wrong one
  • Configure Unbound host overrides for LAN clients
  • Avoid the common failure modes of overrides (wildcards, PTR records, DHCP integration)
  • Compare host overrides with NAT reflection and know which to choose
  • Document overrides so they survive personnel change

Prerequisites

Verified against OPNsense 25.x · FreeBSD 14.x · PF (FreeBSD packet filter) FreeBSD 14.x · Unbound 1.20+ · Kea DHCP OPNsense 25.x plugin · WireGuard in-kernel + OPNsense plugin · strongSwan (IPsec plugin) OPNsense 25.x plugin · OpenVPN 2.6.x · Suricata 7.x · 2026-08-14

Not yet marked complete on this device.

A LAN host asking the firewall for app.example.com and getting the public IP address, then trying to reach that IP from inside the network, is the most common split-horizon failure. The public IP routes through the WAN; the LAN host has no path to the WAN-routable address that the firewall itself serves; the connection times out. The fix is split-horizon DNS: the firewall returns the private IP to LAN clients and the public IP to the world, so that internal clients reach the service on the internal network and external clients reach it through the firewall’s port-forward.

OPNsense implements split-horizon through Unbound host overrides — records the operator adds to the resolver that say “for clients asking this firewall, return this address”. This lesson covers when overrides are the right answer, how to configure them correctly, and the alternatives that are sometimes the better choice.

When split-horizon is the right answer

Split-horizon is the right answer when the same name resolves to different IPs depending on the client’s network, and the difference is intentional. The canonical production example:

Public DNS: app.example.com → 203.0.113.50
LAN DNS:    app.example.com → 10.0.0.50

External clients reach 203.0.113.50 (the firewall’s public IP, DNAT’d to the internal server). LAN clients reach 10.0.0.50 (the server directly, no NAT, no firewall traversal). Both answers are correct for the client making the query.

The decision tree:

SituationRight answer
Internal clients should reach the service on the internal IPHost override — return the internal IP to LAN queries
Internal clients should reach the service through the public IP (loop through the firewall)NAT reflection / hairpin — return the public IP, route back through the firewall
Internal clients should not reach the service at allFirewall rule — drop outbound traffic to the service’s IP from the LAN
Both internal and external clients should reach the public IPNo override — return the public IP to everyone, expect internal clients to hairpin

The split-horizon override is the answer when the operator has decided that internal clients should take the short path to the internal service. NAT reflection is the answer when the operator has decided that internal clients should take the same path as external clients (and accept the round-trip cost and the fragility of NAT).

How host overrides work in OPNsense

OPNsense adds the override to Unbound’s local-data table. The resolver matches the query against the local-data before walking the recursion path, so the override answer is returned without any outbound query. The record is served from cache indefinitely; no upstream can override it.

The configuration page is Services → Unbound DNS → Overrides. The fields:

FieldMeaning
HostThe host part of the name (no domain)
DomainThe domain part of the name (e.g. example.com)
TypeA (IPv4), AAAA (IPv6), or MX
IPThe address to return
DescriptionA free-text note for the operator

A wildcard override (Host = *) catches every name in the domain. The wildcard is the answer for split-horizon patterns where many internal subdomains exist and the operator wants every one of them to resolve to the same internal IP.

Verifying an override is live

The override applies immediately on Apply. The verification is to query the firewall’s resolver directly.

Read-only / Safedrill override
$ drill app.example.com @192.0.2.1
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 41287
;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; app.example.com.   IN  A
;; ANSWER SECTION:
app.example.com.    3600   IN  A   10.0.0.50
;; Query time: 1 msec
;; SERVER: 192.0.2.1#53(192.0.2.1)

Illustrative output

The same query against an external resolver (drill @8.8.8.8 app.example.com) returns the public IP. The split-horizon is in effect: the firewall’s resolver returns the internal answer, the public DNS returns the external answer, and the client sees only the answer from the resolver it asks.

DHCP integration: registering lease names

OPNsense can register DHCP lease hostnames in Unbound automatically. The setting is Register ISC DHCP4 Leases under General. With this enabled, every DHCP lease that includes a hostname becomes a host override in Unbound.

The discipline: this is convenient for small networks and a management burden for large ones. The DHCP lease table becomes the source of truth for some names, and the operator has to look in two places (DHCP leases and Unbound overrides) to find every override. The operational choice is to pick one source of truth and stick to it:

Source of truthWhen to use
Unbound overrides onlyStatic internal services, servers with fixed IPs
DHCP leases onlyDynamic clients, BYOD networks
Both, with overrides for static and leases for dynamicMost common pattern; the discipline is “which table is this name in?”

The reverse — a PTR record for the IP — is generated automatically for non-wildcard overrides and for DHCP leases that include a hostname. The PTR lets reverse lookups (drill -x 10.0.0.50) return a useful name.

Overrides versus NAT reflection

The alternative to host overrides is NAT reflection (also called NAT hairpin). NAT reflection rewrites the source IP of an internal client so the firewall can DNAT the response back to the same internal host.

The two approaches solve the same problem with different trade-offs:

AspectHost overridesNAT reflection
Internal client pathDirect to internal IPThrough firewall, NAT’d
LatencyLower (one hop)Higher (firewall round-trip)
TLS with certificate validationWorks (no MITM)Often breaks (cert is for public name, client reaches internal IP)
Logging visibilityFirewall log shows internal-to-internalFirewall log shows the hairpin
Configuration surfaceUnbound overridesNAT rules + reflection rule + matching firewall rule

The operational rule: prefer host overrides for services that have TLS with certificate validation (which is most production services in 2026), and NAT reflection for services where the operator specifically wants internal clients to take the same path as external ones.

The certificate problem with NAT reflection is decisive for most production cases. When a LAN client connects to app.example.com and the public DNS returns the public IP, the client presents a SNI for app.example.com and expects a certificate for that name. With NAT reflection, the firewall rewrites the destination to the internal IP, and the internal server presents a certificate for app.example.com (assuming it is configured to). With a host override, the LAN client connects directly to the internal IP and the server presents its certificate. The certificate is correct either way — but the client validates the certificate against the name it asked for, not the IP it connected to, and the certificate’s SAN includes app.example.com in either case. Where NAT reflection breaks is when the certificate’s SAN only lists the public name and the client does strict hostname validation that compares the CN to the destination IP — most modern clients do not, but some hardened configurations do.

Documenting overrides

The override page is the firewall’s source of truth for split-horizon. Every override should have a description that names:

  • The service the override exists for.
  • The IP the override returns and why.
  • The public-IP counterpart (if any).
  • The change record that introduced it.

The discipline is the same as for any other configuration that becomes invisible when it is correct: without a description, the next operator who looks at the override does not know whether it is load-bearing or a leftover from a test.

Read-only / Safelist local data
$ unbound-control -c /var/unbound/unbound.conf list_local_data | grep -E '^app' | head -3
app.example.com. 3600 IN A 10.0.0.50
api.example.com. 3600 IN A 10.0.0.51
gitlab.example.com. 3600 IN A 10.0.0.52

Illustrative output

Summary

  • Split-horizon DNS returns different answers for the same name depending on the client’s network. The right answer for most production cases is the host override.
  • OPNsense implements overrides through Unbound’s local-data table. The override is matched before any outbound query and is cached permanently until the configuration changes.
  • Wildcard overrides are for closed internal zones only. Zones with a public face need explicit per-host overrides.
  • Compare overrides with NAT reflection: overrides win for TLS-validated services; NAT reflection wins when the operator specifically wants internal clients to take the same path as external ones.
  • Every override gets a description naming the service, the IP returned, the public-IP counterpart, and the change record.
  • mDNS is not a substitute for host overrides across VLANs.

Knowledge check · 4 questions

  1. Q1. A LAN host asks the firewall for app.example.com. The firewall returns the public IP (203.0.113.50). The LAN host times out trying to reach the public IP. The fix is:

  2. Q2. A wildcard host override (*.corp.internal → 10.0.0.50) returns 10.0.0.50 for every name in corp.internal, including names that are supposed to be reached through public DNS.

  3. Q3. Which of the following are good reasons to prefer a host override over NAT reflection? Select all that apply.

  4. Q4. A host override exists for app.example.com → 10.0.0.50 but the override description is blank. Six months later a new operator looks at the override table. What is the most likely problem?

Passing score: 75%. Answers are checked in this browser.