Files
jarl/docs/views.md
T
abrown38andClaude Opus 5.5 0795a1001e Fix zone list filtering and BIND warning display; add a views guide
- jarl zone list showed only primary zones. zone create binds its --type
  flag (default "primary") to the same variable as list's --type, so the
  default leaked into the list filter and hid hint, secondary and forward
  zones. list now has its own variable. TestListQueries runs list
  commands against a fake server and checks the filters they send.
- named-checkconf/named-checkzone messages are classified by the tool's
  exit code: on success, everything printed is a warning. Warnings such
  as 'both "recursion no;" and "allow-recursion" active' were shown as
  errors. The CLI, jarld check and the web UI now label them as warnings
  and skip the routine "loaded serial" lines.
- The zone_outside_view error points at --adopt-zones.
- docs/views.md: step-by-step split DNS for a server that has no views
  yet, tested on a copy of a stock Ubuntu 26.04 setup. Linked from the
  README, concepts and validation docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:36:24 +00:00

5.0 KiB

Split DNS with views

Views let named answer differently depending on who asks. The usual setup for a homelab that is also authoritative on the internet has two views:

  • internal: your networks get recursion (they use the server as their resolver) and see private addresses (nas.adbraun.lol → 10.10.0.20) and internal-only zones such as reverse zones for private networks.
  • external: everyone else gets authoritative answers only. That means public addresses, no recursion, and no internal zones.

How named treats views (and why Jarl asks to move your zones)

Without any view statements, named puts every zone into an implicit default view. That's why an imported server shows no views. As soon as one view exists, named requires every zone to be inside a view. So the first view you create has to take over the existing zones, including the root hints zone . that Debian/Ubuntu configure.

Jarl does this in the same changeset that creates the view:

  • web UI: Views → New view. The box "Move the N zone(s) that are not in a view into this view" is ticked by default.
  • CLI: jarl view create NAME --adopt-zones
  • API: "adopt_zones": true

Creating the first view without it is refused before anything reaches named (zone_outside_view).

Views are matched in order: the first view whose match-clients (and match-destinations) fit the client wins. Put the specific view first and the catch-all last.

Step by step

1. Check the ACL for your networks

The internal view matches your networks. Usually that's the ACL you already use for allow-recursion:

jarl acl list
jarl acl update homelab --add localhost      # if missing; see the note below

Include localhost if the DNS server itself uses 127.0.0.1 as its resolver. Otherwise the server's own lookups land in the external view and get no recursion.

2. Create the internal view and move the existing zones into it

jarl view create internal --match-clients homelab --adopt-zones --dry-run   # look first
jarl view create internal --match-clients homelab --adopt-zones
create view internal
move zone . into view internal
move zone 0.10.10.in-addr.arpa into view internal
move zone adbraun.lol into view internal
applied (changeset 4)

Nothing changes for your clients: they match internal and get the same answers as before. The server-wide options (recursion, allow-recursion, forwarders, ...) still apply, because views inherit them.

3. Create the external view

jarl view create external --match-clients any
jarl option set recursion no --view external
jarl option set allow-recursion '{ none; }' --view external

The inherited allow-recursion { homelab; } already keeps outsiders from recursing. The explicit recursion no also stops them reading the cache, and makes the intent obvious. Setting allow-recursion { none; } as well avoids named-checkconf's warning that allow-recursion is still active in a view with recursion off. Jarl warns (security_open_resolver) if a view ever offers recursion to everyone.

4. Give the external view its zones

Different data per view (split DNS). Copy the zone, then change what should differ:

jarl zone create adbraun.lol --view external --clone-from adbraun.lol --clone-view internal
jarl record list adbraun.lol --view external
jarl record update <id> --data 203.0.113.20          # public address instead of 10.10.0.20
jarl record delete adbraun.lol nas-internal A --view external

From then on the two copies are separate zones. A record added to one isn't in the other, and the serials are separate.

The same data in both views. Use an in-view zone. The external view then serves the internal view's zone, so there is one copy to maintain:

jarl zone create example.org --view external --type in-view --in-view internal

Don't do that for zones with private data. Internal-only zones (reverse zones for 10.x / 192.168.x) simply don't get an external copy: named answers outsiders with NXDOMAIN from its built-in empty zones.

5. Check from both sides

dig @ns1.adbraun.lol nas.adbraun.lol +short            # from the LAN: 10.10.0.20
dig @<public-ip> nas.adbraun.lol +short                 # from outside: 203.0.113.20
dig @<public-ip> example.com                            # from outside: status REFUSED
jarl check

On the server, dig -b <source address> picks which view you test.

Living with views

  • Commands that name a zone that exists in several views need --view (jarl record add adbraun.lol www A ... --view external). Jarl refuses to guess.
  • jarl acme writes challenge records into every view that has the zone, so Let's Encrypt (external) and internal clients see the same thing. Use --view to limit it.
  • jarl view order internal external changes the match order, and jarl view list shows it.
  • Deleting a view is refused while it still has zones, unless you pass --cascade.
  • Zone files are stored per view: /var/lib/bind/jarl/internal/adbraun.lol.zone, /var/lib/bind/jarl/external/adbraun.lol.zone.