Response Policy Zones in Unbound

We are incredibly happy to introduce Unbound 1.10. This release features RPZ, a mechanism that makes it possible to define your local policies in a standardized way, and load your policies from external sources.

Response Policy Zones in Unbound

By Ralph Dolmans

Unbound, for a long time already, has support for local-zones and local-data. This makes it possible to give a custom answer back for certain domain names. Unbound also contains the respip module which makes it possible to rewrite answers containing certain IP addresses. Although these options are heavily used, they are Unbound specific. If you operate resolvers from multiple vendors you have to maintain your policies for multiple configurations, which all will have their own syntax. Using the Unbound specific configuration also makes it challenging to consume policies from external sources. You will have to fetch the external policies in the offered format, and reformat it in such a way that Unbound will understand it. You then have to keep this list up-to-date, for example using unbound-control. There is, however, a policy format that will work on different resolver implementations, and that has capabilities to be directly transferred and loaded from external sources: Response Policy Zones (RPZ).

RPZ policies are formatted in DNS zone files. This makes it possible to easily consume and keep them to up-to-date by using DNS zone transfers. Something that Unbound is already capable of doing for its auth-zone feature.

Each policy in the policy zone consist of a trigger and an action. The trigger describes when the policy should be applied. The action describes what action should be taken if the policy needs to be applied. Each trigger and action combination is defined as a Resource Record (RR) in the policy zone. The owner of the RR states the trigger, the type and RDATA state the action.

The latest RPZ draft describes five different policy triggers. We decided to only implement two of these triggers in our initial RPZ release: the QNAME trigger, and the Response IP Address trigger.

QNAME trigger

A policy with the QNAME trigger will be applied when the target domain name in the query (the query name, or QNAME) matches the trigger name. The trigger name is the part of the owner of the record before the origin of the zone. For example, if we have this record in the rpz.nlnetlabs.nl zone:

$ORIGIN rpz.nlnetlabs.nl.
example.com.rpz.nlnetlabs.nl.    TXT  "trigger for example.com"

then Unbound will add a policy for queries for example.com. Only exact matches for example.com will be triggered. If we want to have a policy for example.com and all of its subdomains we can do that by adding a wildcard record:

$ORIGIN rpz.nlnetlabs.nl.
example.com.rpz.nlnetlabs.nl.    TXT  "trigger for example.com"
*.example.com.rpz.nlnetlabs.nl.  TXT  "trigger for *.example.com"

RPZ actions

The action that will be applied for above example is the Local Data action. This means that queries for example.com for the TXT type will be answered with our newly created record. Queries for types that do not exist in our policy zones will result in a NODATA answer.

$ drill txt example.com
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 14642
;; flags: qr aa rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 
;; QUESTION SECTION:
;; example.com. IN TXT

;; ANSWER SECTION:
example.com. 3600 IN TXT "trigger for example.com"



$ drill aaaa example.com @127.0.0.54
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 4713
;; flags: qr aa rd ra ; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0 
;; QUESTION SECTION:
;; example.com. IN AAAA

;; ANSWER SECTION:

Other RPZ actions that are supported by Unbound are: the NXDOMAIN, NODATA, PASSTHRU, and DROP actions. All of these actions are defined by having a CNAME to a specific name. A policy for the NXDOMAIN action is created by having a CNAME to the root:

$ORIGIN rpz.nlnetlabs.nl.
example.com.rpz.nlnetlabs.nl.    CNAME .

The NXDOMAIN action will, as the name suggest, answer with an NXDOMAIN when triggered:

$ drill aaaa example.com
;; ->>HEADER<<- opcode: QUERY, rcode: NXDOMAIN, id: 14754
;; flags: qr aa rd ra ; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0 
;; QUESTION SECTION:
;; example.com. IN AAAA

;; ANSWER SECTION:

The CNAME targets for the other RPZ actions are:

+----------+---------------------+
|  Action  |  RR type and RDATA  |
+----------+---------------------+
| NXDOMAIN | CNAME .             |
| NODATA   | CNAME *.            |
| PASSTHRU | CNAME rpz-passthru. |
| DROP     | CNAME rpz-drop.     |
+----------+---------------------+

The NODATA action is self explanatory. The DROP action will simply ignore (drop) the query. The PASSTHRU action makes it possible to exclude a domain, or IP address, from your policies. If the PASSTHRU action is triggered no other policy from any of the available policy zones will be applied:

$ORIGIN rpz.nlnetlabs.nl.
*.example.com.rpz.nlnetlabs.nl.   TXT "local data policy"
www.example.com.rpz.nlnetlabs.nl. CNAME rpz-passthru.

Queries for all subdomains of example.com will now be answered with an NXDOMAIN, except for queries for www.example.com, these will be resolved normally.

$ drill txt withpolicy.example.com
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 62993
;; flags: qr aa rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 
;; QUESTION SECTION:
;; withpolicy.example.com. IN TXT

;; ANSWER SECTION:
withpolicy.example.com. 3600 IN TXT "local data policy"



$ drill txt www.example.com
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 42053
;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 
;; QUESTION SECTION:
;; www.example.com. IN TXT

;; ANSWER SECTION:
www.example.com. 86400 IN TXT "v=spf1 -all"

Response IP Address trigger

As said before, the other RPZ trigger supported by Unbound is the Response IP Address trigger. This trigger makes it possible to apply the same RPZ actions as mentioned above, but triggered based on the IPv4 or IPv6 address in the answer section of the answer. The IP address to trigger on is again part of the owner of the policy records. The IP address is encoded in reverse form and prepended with the prefix length to use. This all is prepended to the rpz-ip label, which will be placed right under the apex of the zone. So, a trigger for addresses in the 192.0.2.0/24 block will be encoded as:

$ORIGIN rpz.nlnetlabs.nl.
24.0.2.0.192.rpz-ip.rpz.nlnetlabs.nl. [...]

IPv6 addresses can also be used in RPZ policies. In that case the zz label can be used to replace the longest set of zeros. A trigger for addresses in the 2001:DB8::/32 block will be encoded as:

$ORIGIN rpz.nlnetlabs.nl.
32.zz.db8.2001.rpz-ip.rpz.nlnetlabs.nl. [...]

We can now, for example, replace an address by applying one specified in a policy containing a Local Data action. The IPv4 address for example.com is currently 93.184.216.34, we can change that to 192.0.2.1:

$ORIGIN rpz.nlnetlabs.nl.
32.34.216.184.93.rpz-ip.rpz.nlnetlabs.nl. A 192.0.2.1

---

$ drill example.com
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 13670
;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 
;; QUESTION SECTION:
;; example.com. IN A

;; ANSWER SECTION:
example.com. 3600 IN A 192.0.2.1

RPZ in Unbound

Now that we have a good understanding of how RPZ works we can have a look at how to load these policy zones into Unbound.

The RPZ implementation in Unbound depends on the respip module, this module needs therefore to be loaded using module-config. Each policy zone is configured in Unbound using the rpz clause. A minimal configuration with a single policy zone can look like:

server:
  module-config: "respip validator iterator"

rpz:
  name: rpz.nlnetlabs.nl
  zonefile: rpz.nlnetlabs.nl

In above example the policy zone will be loaded from file. It is also possible to load the zone using DNS zone transfers. Both AXFR and IXFR is supported, all additions and deletion in the zone will be picked up by Unbound and reflected in the local policies. Transferring the policy using a DNS zone transfer is as easy as specifying the server to get the zone from:

server:
  module-config: "respip validator iterator"

rpz:
  name: rpz.nlnetlabs.nl
  master: <ip address of server to transfer from>
  zonefile: rpz.nlnetlabs.nl

The zone will now be transferred from the configured address and saved to a zonefile on disk.

It is possible to have more than one policy zone in Unbound. Having multiple policy zones is as simple as having multiple rpz clauses:

server:
  module-config: "respip validator iterator"

rpz:
  name: rpz.nlnetlabs.nl
  zonefile: rpz.nlnetlabs.nl

rpz:
  name: rpz2.nlnetlabs.nl
  zonefile: rpz2.nlnetlabs.nl

The policy zones will be applied in the configured order. In our example, Unbound will only look at the rpz2.nlnetlabs.nl policies if there is no match in the rpz.nlnetlabs.nl zone. If there is no match in any of the configured zones Unbound will continue to resolve the domain by sending upstream queries. Note that a PASSTHRU action is considered a match, having that action in the first zone will therefore stop Unbound from looking further at other policy zones.

Unbound has the possibility to override the actions that will be used for policies in a zone that matches the zone’s triggers. This can be done using the rpz-action-override configuration option. The possible values for the option are: nxdomain, nodata, passthru, drop, disabled, and cname. The first four options of this list will do the same as the RPZ actions with the same name.

The cname override option will make it possible to apply a local data action using a CNAME for all matching triggers in the policy zone. The CNAME to use in the answer can be configured using the rpz-cname-override configuration option. Using these overrides are nice if you use an external feed to get a list of triggers, but would like to redirect all your users to your own domain:

RPZ zone (rpz.nlnetlabs.nl):

$ORIGIN rpz.nlnetlabs.nl.
drop.example.com.rpz.nlnetlabs.nl. CNAME rpz-drop.
32.34.216.184.93.rpz-ip.rpz.nlnetlabs.nl. A 192.0.2.1

---

Unbound config:
server:
  module-config: "respip validator iterator"

rpz:
  name: rpz.nlnetlabs.nl
  zonefile: rpz.nlnetlabs.nl
  rpz-action-override: cname
  rpz-cname-override: "example.nl."

---

Example queries:
$ drill drop.example.com
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 14547
;; flags: qr aa rd ra ; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0 
;; QUESTION SECTION:
;; drop.example.com. IN A

;; ANSWER SECTION:
drop.example.com. 3600 IN CNAME example.nl.
example.nl. 3600 IN A 94.198.159.35

$ drill example.com
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 31187
;; flags: qr rd ra ; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0 
;; QUESTION SECTION:
;; example.com. IN A

;; ANSWER SECTION:
example.com. 3600 IN CNAME example.nl.
example.nl. 3568 IN A 94.198.159.35

The disabled option will stop Unbound from applying any of the actions in the zone. This, combined with the rpz-log option, is a nice way to test what would happen to your traffic when a policy will be enabled, without directly impacting your users. The difference between disabled and passthru is that disabled is not considered to be a valid match and will therefore not stop Unbound from looking at the next configured policy zone.

When rpz-log is set to yes, Unbound will log all applied actions for a policy zone. With rpz-log enabled you can specify a name for the log using rpz-log-name, this way you can easily find all matches for a specific zone. It is also possible to get statistics per applied RPZ action using unbound-control stats. This requires the extended-statistics to be enabled.

Unbound’s RPZ implementation works together with the tags functionality. This makes is possible to enable (some of) the policy zones only for a set of the users. To do this the tags need to be defined using define-tag, the correct tags need to be matched with the client IP addresses using access-control-tag, and the tags need to be specified for the policy zones for which they apply.

server:
  module-config: "respip validator iterator"
  define-tag: "malware social"
  access-control-tag 127.0.0.10/32 "social"
  access-control-tag 127.0.0.20/32 "social malware"
  access-control-tag 127.0.0.30/32 "malware"

rpz:
  name: malware.rpz.example.com
  zonefile: malware.rpz.example.com
  tags: "malware"

rpz:
  name: social.rpz.example.com
  zonefile: social.rpz.example.com
  tags: "social"

Queries from 127.0.0.1 will not be filtered. For queries coming from 127.0.0.10 only the policies from the social.rpz.example.com zone will be used, for 127.0.0.30 only the policies from the malware.rpz.example.com zone will be used, and queries originated from 127.0.0.20 will be subjected to the policies from both zones.

Tack så mycket

We think Response Policy Zones is a valuable addition to the feature set of Unbound and it had been on our roadmap for some time already. A while ago, SUNET expressed an immediate need for RPZ and offered to fund the development.

In addition to funding, we worked with SUNET throughout the development process. SUNET maintains and develops Sweden’s university data network and offers a range of online services for research and education. This allowed us to refine our RPZ implementation along the way and thoroughly test it in an operational environment before making it available to the wider community. These factors were the primary drivers for us to prioritize RPZ on our roadmap.

Our gratitude goes out to SUNET for this collaboration. This is an excellent way to develop industry relevant open source software in a sustainable way. We are certainly open to similar collaborations in the future.