The core problem this solves is that your DNS zone data is often sensitive or optimized differently for internal vs. external consumption, and you need a way to present distinct views of the same zone to different types of clients.
Let’s see it in action with BIND 9, a common DNS server.
Imagine you have a zone file, internal.example.com.zone:
$TTL 86400
@ IN SOA ns1.internal.example.com. admin.internal.example.com. (
2023102701 ; serial
3600 ; refresh
1800 ; retry
604800 ; expire
86400 ; minimum TTL
)
IN NS ns1.internal.example.com.
ns1 IN A 192.168.1.10
server1 IN A 192.168.1.20
db-server IN A 192.168.1.30
And you want an external view, external.example.com.zone, which might hide internal IP addresses and perhaps only expose a public-facing web server:
$TTL 86400
@ IN SOA ns1.external.example.com. admin.external.example.com. (
2023102701 ; serial
3600 ; refresh
1800 ; retry
604800 ; expire
86400 ; minimum TTL
)
IN NS ns1.external.example.com.
ns1 IN A 203.0.113.10
www IN A 203.0.113.50
Here’s how you configure BIND 9 to serve these using view clauses in named.conf:
acl "internal_clients" { 192.168.1.0/24; 10.0.0.0/8; localhost; localnets; };
options {
directory "/var/cache/bind";
recursion yes;
allow-query { any; };
};
view "internal" {
match-clients { internal_clients; };
zone "internal.example.com" {
type master;
file "/etc/bind/zones/internal.example.com.zone";
};
// You might also want to serve other internal zones here.
};
view "external" {
match-clients { any; }; // Catch all other clients
zone "external.example.com" {
type master;
file "/etc/bind/zones/external.example.com.zone";
};
// You might also want to serve other external zones here.
};
In this setup:
acl "internal_clients"defines the network ranges that are considered "internal."- The
optionsblock contains general server settings. - The first
view "internal"block specifies that any client matchinginternal_clientswill see theinternal.example.comzone loaded from its corresponding file. - The second
view "external"block acts as a fallback. Any client not matchinginternal_clientswill fall into this view and see theexternal.example.comzone.
When a query for server1.internal.example.com arrives from 192.168.1.50:
- BIND checks the
viewclauses. match-clients { internal_clients; }for the "internal" view matches192.168.1.50.- The "internal" view is selected.
- BIND looks for the
internal.example.comzone within this view and serves the IP192.168.1.20.
When a query for server1.internal.example.com arrives from 203.0.113.100 (an external IP):
- BIND checks the
viewclauses. match-clients { internal_clients; }for the "internal" view does not match203.0.113.100.- BIND proceeds to the next
view. match-clients { any; }for the "external" view matches203.0.113.100.- The "external" view is selected.
- BIND looks for the
internal.example.comzone within this view. Since it’s not defined here, the query fails for that zone. If the query was forwww.external.example.com, BIND would find it and serve203.0.113.50.
The order of view blocks matters. BIND processes them sequentially and uses the first one that matches the client’s IP address. This allows for complex layering, where you might have a "super-internal" view, then a general "internal" view, and finally a default "external" view.
A common, but often overlooked, detail is how allow-query interacts with views. If allow-query is set in the global options block to a restrictive list, it applies before views are even considered. If you want views to completely control access, ensure your global allow-query is broad enough (e.g., { any; }) and then use match-clients within each view to enforce your desired access policies.
The next step is often managing different DNS record types or even entire subdomains differently for internal and external clients, which can lead you to explore response policy zones (RPZ) for more granular control.