Split-Horizon DNS

Split-horizon (split-brain) DNS with BIND views. Different answers for internal vs external clients.

What Split-Horizon DNS Does

Split-horizon (split-brain) DNS returns different answers for the same query depending on who’s asking. Internal clients get private IPs; external clients get public IPs. This lets you use the same domain name everywhere without exposing internal infrastructure.

BIND Views — The Implementation

Two-view split-horizon — internal vs external
acl "internal" { 10.50.0.0/16; 172.16.0.0/12; 127.0.0.0/8; };

view "internal" {
    match-clients { internal; };
    recursion yes;
    allow-query { internal; };

    zone "inside.domusdigitalis.dev" IN {
        type master;
        file "inside.domusdigitalis.dev.internal.zone";
    };

    zone "1.50.10.in-addr.arpa" IN {
        type master;
        file "10.50.1.internal.rev";
    };
};

view "external" {
    match-clients { any; };
    recursion no;
    allow-query { any; };

    zone "inside.domusdigitalis.dev" IN {
        type master;
        file "inside.domusdigitalis.dev.external.zone";
    };
};

Views are evaluated in order — first match wins. internal is listed first so trusted clients match before any. External view disables recursion (authoritative-only for the internet).

Zone Files for Each View

Internal zone file — private IPs, full records
; inside.domusdigitalis.dev.internal.zone
$TTL 3600
@ IN SOA ns1.inside.domusdigitalis.dev. admin.domusdigitalis.dev. (
    2026041001 3600 900 604800 86400
)
@ IN NS  ns1.inside.domusdigitalis.dev.
@ IN NS  ns2.inside.domusdigitalis.dev.

ns1     IN A  10.50.1.2
ns2     IN A  10.50.1.3
ise-01  IN A  10.50.1.20
dc01    IN A  10.50.1.50
vault   IN A  10.50.1.60
nas     IN A  10.50.1.70

Internal zone has all hosts, all records, full AD service discovery SRV records.

External zone file — public IPs, minimal records
; example.com.external.zone
$TTL 3600
@ IN SOA ns1.example.com. admin.example.com. (
    2026041001 3600 900 604800 86400
)
@ IN NS  ns1.example.com.
@ IN NS  ns2.example.com.

@    IN A     198.51.100.10
www  IN A     198.51.100.10
@    IN MX 10 mail.example.com.
mail IN A     198.51.100.11

External zone exposes only what must be publicly reachable. No internal hostnames, no SRV records, no reverse zones.

View Ordering Rules

Order matters — first match wins
# CORRECT — specific before general
view "internal" { match-clients { internal; }; ... };
view "external" { match-clients { any; }; ... };

# WRONG — any matches everything, internal view is never reached
view "external" { match-clients { any; }; ... };
view "internal" { match-clients { internal; }; ... };
Every zone must appear in every view (or use match-destinations)
# If a zone exists in one view, it must exist in all views
# or clients in the other view get REFUSED for that zone

BIND requires consistency across views. A zone in the internal view but missing from the external view means external queries for that zone return REFUSED rather than NXDOMAIN.

Testing Split-Horizon

Query from internal — should get private IPs
dig @10.50.1.90 inside.domusdigitalis.dev A +short
Query from external — should get public IPs
# From an external machine or using a public DNS
dig @public-ns.example.com example.com A +short
Simulate external query from internal network
dig @8.8.8.8 example.com A +short

If your domain is publicly delegated, querying public DNS shows the external view’s answer.

Verify which view a client matches
sudo rndc status

BIND’s query log (when debug is enabled) shows which view served each query. Enable temporarily with rndc trace 1.

VyOS Dual-BIND and Split-Horizon

Considerations for the VyOS HA pair
# Both vyos-01 and vyos-02 need identical view definitions
# Internal view: same zone data (master/slave replication)
# External view: only if these servers face the internet

# On vyos-01 (master)
view "internal" {
    match-clients { internal; };
    zone "inside.domusdigitalis.dev" { type master; file "inside.domusdigitalis.dev.internal.zone";
        allow-transfer { 10.50.1.3; }; };
};

# On vyos-02 (slave)
view "internal" {
    match-clients { internal; };
    zone "inside.domusdigitalis.dev" { type slave; masters { 10.50.1.2; };
        file "slaves/inside.domusdigitalis.dev.internal.zone"; };
};

When using views with master/slave replication, the zone transfer happens within the context of the view. Both servers must have matching view definitions.

Common Pitfalls

  • Forgot to put all zones in all views: clients in the missing view get REFUSED, not NXDOMAIN

  • ACL ordering: any before a specific ACL means the specific ACL is never reached

  • Zone transfers across views: slave must define the zone in the same view as the master

  • Recursion in external view: never enable — you become an open resolver for the internet

  • Different serials: each view has its own zone file with its own serial — increment independently

See Also

  • BIND — named.conf view configuration

  • Zones — zone file management per view

  • Authoritative — master/slave with views