From 35dec6dcfd45fdf2e2e9d2389ad8eb942b6a847e Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Mon, 5 May 2025 13:57:55 +0200 Subject: [PATCH] Document views. --- .github/actions/spell-check/expect.txt | 5 + docs/backends/bind.rst | 1 + docs/backends/generic-mysql.rst | 1 + docs/backends/generic-odbc.rst | 1 + docs/backends/generic-postgresql.rst | 1 + docs/backends/generic-sqlite3.rst | 1 + docs/backends/geoip.rst | 1 + docs/backends/ldap.rst | 1 + docs/backends/lmdb.rst | 1 + docs/backends/lua2.rst | 1 + docs/backends/pipe.rst | 1 + docs/backends/random.rst | 1 + docs/backends/remote.rst | 1 + docs/backends/tinydns.rst | 1 + docs/http-api/index.rst | 2 + docs/http-api/networks.rst | 66 ++++++ .../swagger/authoritative-api-swagger.yaml | 215 ++++++++++++++++- docs/http-api/views.rst | 79 +++++++ docs/indexTOC.rst | 1 + docs/manpages/pdnsutil.1.rst | 29 ++- docs/settings.rst | 14 ++ docs/views.rst | 223 ++++++++++++++++++ 22 files changed, 645 insertions(+), 2 deletions(-) create mode 100644 docs/http-api/networks.rst create mode 100644 docs/http-api/views.rst create mode 100644 docs/views.rst diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 763f852799..e40cc7ba62 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -86,6 +86,7 @@ backported backticks BADALG BADCOOKIE +badguys badips BADKEY BADMODE @@ -976,6 +977,7 @@ orsn Oservers otherdomain otherpool +othervariant ourname ourserial outpacket @@ -1183,6 +1185,7 @@ Roel rolltype Rosmalen roundrobin +routable rping rpms rpz @@ -1498,6 +1501,7 @@ Valkenburg Vandalon vandergaast Vandoren +variantless Vasiliy Veldhuyzen Verheijen @@ -1613,4 +1617,5 @@ zskroll ztc Zumstrull Zwane +zytrax zzyzz diff --git a/docs/backends/bind.rst b/docs/backends/bind.rst index da56f40275..6f52f88313 100644 --- a/docs/backends/bind.rst +++ b/docs/backends/bind.rst @@ -12,6 +12,7 @@ BIND zone file backend * Disabled data: No * Comments: No * Search: Yes +* Views: No * API: Read-only * Multiple instances: No * Zone caching: Yes diff --git a/docs/backends/generic-mysql.rst b/docs/backends/generic-mysql.rst index 1ced43ef65..538f8986a6 100644 --- a/docs/backends/generic-mysql.rst +++ b/docs/backends/generic-mysql.rst @@ -12,6 +12,7 @@ Generic MySQL/MariaDB backend * Disabled data: Yes * Comments: Yes * Search: Yes +* Views: No * API: Read-Write * Multiple instances: yes * Zone caching: Yes diff --git a/docs/backends/generic-odbc.rst b/docs/backends/generic-odbc.rst index 9ce42d993d..29aabbccd1 100644 --- a/docs/backends/generic-odbc.rst +++ b/docs/backends/generic-odbc.rst @@ -12,6 +12,7 @@ Generic ODBC Backend * Disabled data: Yes * Comments: Yes * Search: Yes +* Views: No * API: Read-Write * Multiple instances: yes * Zone caching: Yes diff --git a/docs/backends/generic-postgresql.rst b/docs/backends/generic-postgresql.rst index 1ebbbb7d75..76a6f84a5a 100644 --- a/docs/backends/generic-postgresql.rst +++ b/docs/backends/generic-postgresql.rst @@ -12,6 +12,7 @@ Generic PostgreSQL backend * Disabled data: Yes * Comments: Yes * Search: Yes +* Views: No * API: Read-Write * Multiple instances: yes * Zone caching: Yes diff --git a/docs/backends/generic-sqlite3.rst b/docs/backends/generic-sqlite3.rst index d0b9cd8e92..635febf922 100644 --- a/docs/backends/generic-sqlite3.rst +++ b/docs/backends/generic-sqlite3.rst @@ -12,6 +12,7 @@ Generic SQLite 3 backend * Disabled data: Yes * Comments: Yes * Search: Yes +* Views: No * API: Read-Write * Multiple instances: yes * Zone caching: Yes diff --git a/docs/backends/geoip.rst b/docs/backends/geoip.rst index 3a385e5a3b..5e1ea9b176 100644 --- a/docs/backends/geoip.rst +++ b/docs/backends/geoip.rst @@ -12,6 +12,7 @@ GeoIP backend * Disabled data: No * Comments: No * Search: No +* Views: No * API: Read-only * Multiple instances: Yes * Zone caching: Yes diff --git a/docs/backends/ldap.rst b/docs/backends/ldap.rst index 2c2cfc0d89..1334235ee0 100644 --- a/docs/backends/ldap.rst +++ b/docs/backends/ldap.rst @@ -12,6 +12,7 @@ LDAP backend * Disabled data: No * Comments: No * Search: No +* Views: No * API: Read-only * Multiple instances: Yes * Zone caching: No diff --git a/docs/backends/lmdb.rst b/docs/backends/lmdb.rst index 5a91641289..5f9106aedb 100644 --- a/docs/backends/lmdb.rst +++ b/docs/backends/lmdb.rst @@ -12,6 +12,7 @@ LMDB backend * Disabled data: Yes * Comments: No * Search: No +* Views: Yes * API: Read-Write * Multiple instances: No * Zone caching: Yes diff --git a/docs/backends/lua2.rst b/docs/backends/lua2.rst index 4b303be088..a0b7c5e9d1 100644 --- a/docs/backends/lua2.rst +++ b/docs/backends/lua2.rst @@ -12,6 +12,7 @@ Lua2 Backend * Disabled data: No * Comments: No * Search: No +* Views: No * API: Read-Write * Multiple instances: Yes * Zone caching: Yes\* diff --git a/docs/backends/pipe.rst b/docs/backends/pipe.rst index 1810721af0..b08f42c4ef 100644 --- a/docs/backends/pipe.rst +++ b/docs/backends/pipe.rst @@ -12,6 +12,7 @@ Pipe Backend * Disabled data: No * Comments: No * Search: No +* Views: No * API: Read-only * Multiple instances: Yes * Zone caching: No diff --git a/docs/backends/random.rst b/docs/backends/random.rst index 54ac8049c7..8a1961db74 100644 --- a/docs/backends/random.rst +++ b/docs/backends/random.rst @@ -15,6 +15,7 @@ Random Backend * Disabled data: No * Comments: No * Search: No +* Views: No * API: No * Multiple instances: No * Zone caching: No diff --git a/docs/backends/remote.rst b/docs/backends/remote.rst index 20c54c0f9a..7648004452 100644 --- a/docs/backends/remote.rst +++ b/docs/backends/remote.rst @@ -12,6 +12,7 @@ Remote Backend * Disabled data: No * Comments: No * Search: Yes\* +* Views: No * API: Read-Write * Multiple instances: Yes * Zone caching: Yes\* diff --git a/docs/backends/tinydns.rst b/docs/backends/tinydns.rst index 9e32c0be72..b000f13538 100644 --- a/docs/backends/tinydns.rst +++ b/docs/backends/tinydns.rst @@ -12,6 +12,7 @@ TinyDNS Backend * Disabled data: No * Comments: No * Search: No +* Views: No * API: Read-only * Multiple Instances: Yes * Zone caching: Yes diff --git a/docs/http-api/index.rst b/docs/http-api/index.rst index 2051947955..de479f1abc 100644 --- a/docs/http-api/index.rst +++ b/docs/http-api/index.rst @@ -363,6 +363,8 @@ The API exposes several endpoints and objects: server zone + views + networks cryptokey metadata tsigkey diff --git a/docs/http-api/networks.rst b/docs/http-api/networks.rst new file mode 100644 index 0000000000..a457d55503 --- /dev/null +++ b/docs/http-api/networks.rst @@ -0,0 +1,66 @@ +Networks +======== + +These endpoints allow configuration of networks, used by :doc:`../views`. + +Networks Endpoints +------------------ + +.. openapi:: swagger/authoritative-api-swagger.yaml + :paths: /servers/{server_id}/networks /servers/{server_id}/networks/{ip}/{prefixlen} + +Examples +-------- + +Listing all networks +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: http + + GET /api/v1/servers/localhost/networks HTTP/1.1 + X-API-Key: secret + +Will yield a response similar to this (several headers omitted): + +.. code-block:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"networks": [{"network":"192.168.0.0/16","view":"trusted"},{"network":"0.0.0.0/0","view":"untrusted"}]} + +Listing the view of a given network +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: http + + GET /api/v1/servers/localhost/networks/192.168.0.0/16 HTTP/1.1 + X-API-Key: secret + +Will yield a response similar to this (several headers omitted): + +.. code-block:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"networks": [{"network":"192.168.0.0/16","view":"trusted"}]} + +Setting up a network +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: http + + PUT /api/v1/servers/localhost/networks/192.168.0.0/16 HTTP/1.1 + X-API-Key: secret + Content-Type: application/json + + {"view": "trusted"} + +Will yield a response similar to this (several headers omitted): + +.. code-block:: http + + HTTP/1.1 204 No Content + + diff --git a/docs/http-api/swagger/authoritative-api-swagger.yaml b/docs/http-api/swagger/authoritative-api-swagger.yaml index b820599f7b..0be561ce52 100644 --- a/docs/http-api/swagger/authoritative-api-swagger.yaml +++ b/docs/http-api/swagger/authoritative-api-swagger.yaml @@ -1,6 +1,6 @@ swagger: '2.0' info: - version: "0.0.15" + version: "0.0.16" title: PowerDNS Authoritative HTTP API license: name: MIT @@ -939,6 +939,182 @@ paths: description: 'OK, key was deleted' <<: *commonErrors + '/servers/{server_id}/views': + get: + summary: List all views in a server + operationId: listViews + tags: + - views + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + responses: + '200': + description: An array of view names + schema: + $ref: '#/definitions/Views' + <<: *commonErrors + + '/servers/{server_id}/views/{view}': + get: + summary: List the contents of a given view + operationId: listView + tags: + - views + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: view + type: string + in: path + required: true + description: The name of the view to retrieve + responses: + '200': + description: An array of zone names + schema: + $ref: '#/definitions/View' + <<: *commonErrors + post: + summary: Adds a zone to a given view, creating it if needed + operationId: addToView + tags: + - views + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: view + type: string + in: path + required: true + description: The name of the view to update + - name: name + description: The zone to add to the view + required: true + in: body + schema: + type: string + responses: + '204': + description: 'Returns 204 No Content on success.' + <<: *commonErrors + + '/servers/{server_id}/views/{view}/{id}': + delete: + summary: Removes the given zone from the given view + operationId: deleteFromView + tags: + - views + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: view + type: string + in: path + required: true + description: The name of the view to update + - name: id + description: The zone to remove from the view + required: true + in: path + type: string + responses: + '204': + description: 'Returns 204 No Content on success.' + <<: *commonErrors + + '/servers/{server_id}/networks': + get: + summary: List all registered networks and views in a server + operationId: listNetworks + tags: + - networks + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + responses: + '200': + description: An array of networks + schema: + $ref: '#/definitions/Networks' + <<: *commonErrors + + '/servers/{server_id}/networks/{ip}/{prefixlen}': + get: + summary: Return the view associated to the given network + operationId: getNetwork + tags: + - networks + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: ip + type: string + in: path + required: true + description: The base address of the network + - name: prefixlen + type: string + in: path + required: true + description: The length of the network prefix + responses: + '200': + description: An array of networks + schema: + $ref: '#/definitions/Networks' + <<: *commonErrors + put: + summary: Sets the view associated to the given network + operationId: setNetwork + tags: + - networks + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: ip + type: string + in: path + required: true + description: The base address of the network + - name: prefixlen + type: string + in: path + required: true + description: The length of the network prefix + - name: view + type: string + required: true + description: The name of the view to use for to this network + in: body + schema: + type: string + responses: + '204': + description: 'Returns 204 No Content on success.' + <<: *commonErrors + definitions: Server: title: Server @@ -1396,3 +1572,40 @@ definitions: result: type: string description: 'A message about the result like "Flushed cache"' + + View: + title: View + properties: + zones: + type: array + items: + type: string + description: 'An array of zone names' + + Views: + title: Views + properties: + views: + type: array + items: + type: string + description: 'An array of view names' + + Network: + title: Network + properties: + network: + type: string + description: 'Network specification in human-readable form base address/prefix length' + view: + type: string + description: 'The name of the view' + + Networks: + title: Networks + properties: + networks: + type: array + items: + $ref: '#/definitions/Network' + diff --git a/docs/http-api/views.rst b/docs/http-api/views.rst new file mode 100644 index 0000000000..267892117f --- /dev/null +++ b/docs/http-api/views.rst @@ -0,0 +1,79 @@ +Views +===== + +These endpoints allow configuration of per-zone :doc:`../views`. + +Views Endpoints +--------------- + +.. openapi:: swagger/authoritative-api-swagger.yaml + :paths: /servers/{server_id}/views /servers/{server_id}/views/{view} /servers/{server_id}/views/{view}/{id} + +Examples +-------- + +Listing all views +^^^^^^^^^^^^^^^^^^ + +.. code-block:: http + + GET /api/v1/servers/localhost/views HTTP/1.1 + X-API-Key: secret + +Will yield a response similar to this (several headers omitted): + +.. code-block:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"views":["trusted","untrusted"]} + +Listing the zones of a view +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: http + + GET /api/v1/servers/localhost/views/trusted HTTP/1.1 + X-API-Key: secret + +Will yield a response similar to this (several headers omitted): + +.. code-block:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"zones":["example.com..trusted","otherdomain.com..untrusted"]} + +Creating or adding to a view +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: http + + POST /api/v1/servers/localhost/views/trusted HTTP/1.1 + X-API-Key: secret + Content-Type: application/json + + {"name":"example.org..trusted"} + +Will yield a response similar to this (several headers omitted): + +.. code-block:: http + + HTTP/1.1 204 No Content + +Deleting a view +^^^^^^^^^^^^^^^ + +.. code-block:: http + + DELETE /api/v1/servers/localhost/views/trusted HTTP/1.1 + X-API-Key: secret + +Will yield a response similar to this (several headers omitted): + +.. code-block:: http + + HTTP/1.1 204 No Content + diff --git a/docs/indexTOC.rst b/docs/indexTOC.rst index 6301756ff0..797ba54b76 100644 --- a/docs/indexTOC.rst +++ b/docs/indexTOC.rst @@ -18,6 +18,7 @@ PowerDNS Authoritative Server dnsupdate catalog tsig + views lua-records/index guides/index backends/index diff --git a/docs/manpages/pdnsutil.1.rst b/docs/manpages/pdnsutil.1.rst index a6ccf7060c..83669ca1de 100644 --- a/docs/manpages/pdnsutil.1.rst +++ b/docs/manpages/pdnsutil.1.rst @@ -31,7 +31,7 @@ Commands There are many available commands, this section splits them up into their respective uses. -DNSSEC-related Commands +DNSSEC-RELATED COMMANDS ----------------------- Several commands manipulate the DNSSEC keys and options for zones. Some @@ -433,6 +433,33 @@ zonemd-verify-file *ZONE* *FILE* Validate ZONEMD for *ZONE* read from *FILE*. +VIEWS COMMANDS +-------------- + +list-networks + + List all defined networks with their chosen views. + +set-network *NET* [*VIEW*] + + Set the *VIEW* for a the *NET* network, or delete if no *VIEW* argument. + +view-add-zone *VIEW* *ZONE..VARIANT* + + Add the given *ZONE* *VARIANT* to a *VIEW*. + +view-del-zone *VIEW* *ZONE..VARIANT* + + Remove a *ZONE* *VARIANT* from a *VIEW*. + +list-view *VIEW* + + List all within *VIEW*. + +list-views + + List all view names. + DEBUGGING TOOLS --------------- diff --git a/docs/settings.rst b/docs/settings.rst index ba2af94da1..5e998db10a 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -2018,6 +2018,18 @@ behaviour, to ``powerdns`` to just make it state setting will return a ServFail, much like Microsoft nameservers do. You can set this response to a custom value as well. +.. _setting-views: + +``views`` +--------- + +- Boolean +- Default: no + +.. versionadded:: 5.0.0 + +Enable :doc:`views`. + .. _setting-webserver: ``webserver`` @@ -2220,6 +2232,8 @@ Seconds to cache a list of all known zones. A value of 0 will disable the cache. If your backends do not respond to unknown or dynamically generated zones, it is suggested to enable :ref:`setting-consistent-backends` (default since 4.5) and leave this option at its default of `300`. +If :ref:`setting-views` are enabled, the zone cache **must** be enabled. + .. _setting-zone-metadata-cache-ttl: ``zone-metadata-cache-ttl`` diff --git a/docs/views.rst b/docs/views.rst new file mode 100644 index 0000000000..69b18e7672 --- /dev/null +++ b/docs/views.rst @@ -0,0 +1,223 @@ +Views +===== + +.. versionadded:: 5.0.0 + +Views are an experimental feature, which allows the scope of zones to be +narrowed, depending on the originating address of the query, by exposing +different `variants` of zones. + +A simple use case for this feature is to separate internal (trusted) and +external (untrusted) views of a given domain, without having to rely upon a +GeoIP-like backend. + +Requirements +------------ + +The `Views` features is currently only available in the :doc:`LMDB +` backend, and requires the zone cache to be enabled (by setting +:ref:`setting-zone-cache-refresh-interval` to a non-zero value). + +It must also be explicitly enabled using :ref:`setting-views` in the +configuration file. + +Concepts +-------- + +The first piece of the Views puzzle is the `network`. A `network`, specified as +a base address and a prefix length, is associated to a `view name`. The `view +name` in turn, will select a set of `zone variants` to be used to answer queries +for these zones, originating from this network. + +Queries originating from no configured network will be answered as in a +non-views setup, without any restriction. + +Zone Variants +^^^^^^^^^^^^^ + +A Zone Variant is a zone on its own, written as ``.``. +Variant names are made of lowercase letters, digits, underscore and dashes only. + +For example, the following variants are valid: + +- ``example.org..variant01`` +- ``example.org..1st_variant`` +- ``example.org..othervariant`` + +and a variant of the root zone would be: + +- ``..variant`` + +Zone variants can be used in any command or operation where a zone name is +expected, i.e. with :doc:`pdnsutil ` or the +:doc:`HTTP API `. + +There is no mechanism to populate a freshly-created variant from the variantless +zone contents. + +Networks +^^^^^^^^ + +Networks are set up either with :doc:`pdnsutil ` or the +:doc:`HTTP API `. + +Every network is associated to a unique view name. + +Note that in PowerDNS, unlike Bind, the order in which networks are configured +does not matter. When deciding which network to use to answer a DNS query, the +narrowest (smallest) network will always be chosen. + +Views +^^^^^ + +Views are set up either with :doc:`pdnsutil ` or the +:doc:`HTTP API `. + +Every view is associated to a list of zone variants. It can also include +regular (variantless) zones, but this is not needed as all zones which do not +appear in a view will operate as in a non-views setup. + +In other words, zones not part of a view are always implicitly available in +that view, as their variantless contents. + +Only one variant per zone may appear in a view; setting a new zone variant will +replace the previous one in the view. + +Resolution Algorithm +-------------------- + +When views are enabled, the following operations take place when processing +a DNS query: + +- the source address of the request (or the EDNS subnet option if present) is + used to check whether it matches a configured *network*. +- if so, the *view* associated to that *network* is retrieved; otherwise, + views will be bypassed. +- when searching for a given zone, if there is a specific *variant* for that + zone in the *view*, then that zone variant will be used; otherwise, + the regular variantless zone will be used. + +Configuration tweaks +-------------------- + +When views are used, the :ref:`packet-cache` will cache result results for each +view independently. If your configuration benefits from the packet cache, +you might need to multiply its capacity +(:ref:`setting-max-packet-cache-entries`) by the number of views in use. + +Examples +-------- + +Simple setup +^^^^^^^^^^^^ + +In such a setup, we want to provide three different flavours of a given zone: +one for internal (non-routable) queries, one for trusted origins, and one for +the rest of the Internet. + +Let's start by defining the specific networks:: + + pdnsutil set-network 10.0.0.0/8 internal + pdnsutil set-network 172.16.0.0/12 internal + pdnsutil set-network 192.168.0.0/16 internal + pdnsutil set-network fc00::/7 internal + + pdnsutil set-network 198.51.100.0/24 trusted + pdnsutil set-network 203.0.113.0/24 trusted + pdnsutil set-network 2001:db8::/32 trusted + +Once these commands have been run, queries originating from these particular +networks will select either the "internal" or "trusted" view, while queries +originating from other addresses will default to the unbiased view, which you +may consider an always-existing default (nameless) view. + +You can check the result of these commands with:: + + $ pdnsutil list-networks + 10.0.0.0/8 internal + 172.16.0.0/12 internal + 192.168.0.0/16 internal + 198.51.100.0/24 trusted + 203.0.113.0/24 trusted + 2001:db8::/32 trusted + fc00::/7 internal + +Since these views have not been set up yet, they are empty, causing no change of +outcome when resolving domain queries. + +Let's differentiate these views now:: + + pdnsutil view-add-zone internal example.com..internal + pdnsutil view-add-zone internal example2.com..secret + + pdnsutil view-add-zone trusted example.com..trusted + +Note that the `view-add-zone` command does not create any zone! You will need +to create these zones, like you would do for any other "regular" zone:: + + pdnsutil create-zone example.com..internal + pdnsutil create-zone example2.com..secret + pdnsutil create-zone example.com..trusted + +and then use `load-zone`, `edit-zone`, or `add-record` to add contents to these +zones. + +With these settings in place, queries for the `example.com.` zone will be +performed on the `example.com..internal` zone when originating from the internal +networks, on the `example.com..trusted` zone when originating from the trusted +network, and on the variantless, unmodified, `example.com.` zone when +originating from elsewhere; and queries for the `example2.com.` zone will be +performed on the `example2.com..secret` zone when originating from the internal +networks, and on the variantless `example2.com.` otherwise. + +Queries for all other zones will be unaffected, since no other zone is +configured in the views. + +As seen in this example, a given view may cause multiple zones to be resolved +differently. At any time, you can check which views are setup, and the details +of a given view:: + + $ pdnsutil list-views + internal + trusted + $ pdnsutil list-view internal + example.com..internal + example2.com..secret + $ pdnsutil list-view trusted + example.com..trusted + +Bind configuration adaptation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Consider the following Bind configuration, shamelessly borrowed from +https://www.zytrax.com/books/dns/ch7/view.html:: + + view "trusted" { + match-clients { 192.168.23.0/24; }; // our network + zone "example.com" { + type master; + // private zone file including local hosts + file "internal/master.example.com"; + }; + // add required zones + }; + view "badguys" { + match-clients {"any"; }; // all other hosts + zone "example.com" { + type master; + // public only hosts + file "external/master.example.com"; + }; + // add required zones + }; + +The equivalent PowerDNS setup would be:: + + pdnsutil set-network 192.168.23.0/24 trusted + pdnsutil set-network 0.0.0.0/0 badguys + + pdnsutil view-add-zone trusted master.example.com..internal + pdnsutil view-add-zone badguys master.example.com..external + + pdnsutil load-zone example.com..internal internal/master.example.com + pdnsutil load-zone example.com..external external/master.example.com -- 2.47.2