diff --git a/CHANGES b/CHANGES
index 8d4af0c7e7..5e9f168f9d 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,9 @@
+4116. [bug] Fix a bug in RPZ that could cause some policy
+ zones that did not specifically require
+ recursion to be treated as if they did;
+ consequently, setting qname-wait-recurse no; was
+ sometimes ineffective. [RT #39229]
+
4115. [func] "rndc -r" now prints the result code (e.g.,
ISC_R_SUCCESS, ISC_R_TIMEOUT, etc) after
running the requested command. [RT #38913]
diff --git a/bin/tests/system/README b/bin/tests/system/README
index 69a3937ac8..0ed340e632 100644
--- a/bin/tests/system/README
+++ b/bin/tests/system/README
@@ -19,6 +19,7 @@ involving a different DNS setup. They are:
(not a complete resolver test suite)
rrl/ query rate limiting
rpz/ Tests of response policy zone (RPZ) rewriting
+ rpzrecurse/ Another set of RPZ tests to check recursion behavior
stub/ Tests of stub zone functionality
unknown/ Unknown type and class tests
upforwd/ Update forwarding tests
diff --git a/bin/tests/system/conf.sh.in b/bin/tests/system/conf.sh.in
index 1b206caecf..d0f6bbcf17 100644
--- a/bin/tests/system/conf.sh.in
+++ b/bin/tests/system/conf.sh.in
@@ -72,8 +72,9 @@ SUBDIRS="acl additional allow_query addzone autosign builtin
legacy limits logfileconfig lwresd
masterfile masterformat metadata mkeys
notify nslookup nsupdate pending pipelined @PKCS11_TEST@
- reclimit redirect resolver rndc rpz rrl rrchecker rrsetorder
- rsabigexponent runtime sit sfcache smartsign sortlist spf
+ reclimit redirect resolver rndc rpz rpzrecurse
+ rrl rrchecker rrsetorder rsabigexponent runtime
+ sit sfcache smartsign sortlist spf
staticstub statistics stub tcp tkey tsig tsiggss unknown
upforwd verify views wildcard xfer xferquota zero zonechecks"
diff --git a/bin/tests/system/rpz/tests.sh b/bin/tests/system/rpz/tests.sh
index 52aaf633bb..4baa30a399 100644
--- a/bin/tests/system/rpz/tests.sh
+++ b/bin/tests/system/rpz/tests.sh
@@ -635,7 +635,7 @@ for i in 1 2 3 4 5; do
nsd $ns5 delete '*.example.com.policy1.' example.com.policy1.
done
-echo "I:checking checking that going from a empty policy zone works"
+echo "I:checking that going from a empty policy zone works"
nsd $ns5 add '*.x.servfail.policy2.' x.servfail.policy2.
sleep 1
$RNDCCMD $ns7 reload policy2
diff --git a/bin/tests/system/rpzrecurse/.gitignore b/bin/tests/system/rpzrecurse/.gitignore
new file mode 100644
index 0000000000..6154e06877
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/.gitignore
@@ -0,0 +1,2 @@
+/rpz
+/rpz.o
diff --git a/bin/tests/system/rpzrecurse/README b/bin/tests/system/rpzrecurse/README
new file mode 100644
index 0000000000..6312006b68
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/README
@@ -0,0 +1,113 @@
+These tests check RPZ recursion behavior (including skipping
+recursion when appropriate).
+
+The general structure of the tests is:
+
+* The resolver (ns2) with an unqualified view containing the policy
+ zones, the response-policy statement, and a root hint zone
+
+* The auth server that contains two authoritative zones, l1.l0 and
+ l2.l1.l0, both delegated to itself. l2.l1.l0 specifies a non-existent
+ zone data file and so will generate SERVFAILs for any queries to it.
+
+The l2.l1.l0 zone was chosen to generate SERVFAIL responses because RPZ
+evaluation will use that error response whenever it encounters it during
+processing, thus making it a binary indicator for whether or not
+recursion was attempted. This also allows us to not worry about having
+to craft 'ip', 'nsdname', and 'nsip' rules that matched the queries.
+
+Each test is intended to be fed a number of queries constructed as
+qXX.l2.l1.l0, where XX is the 1-based query sequence number (e.g. the
+first query of each test is q01.l2.l1.l0).
+
+For all the tests the triggers are constructed as follows:
+client-ip - match 127.0.0.1/32
+ip - match 255.255.255.255/32 (does not matter due to SERVFAIL)
+nsdname - match does.not.matter (and it doesn't)
+nsip - match 255.255.255.255/32 (also does not matter)
+qname - match qXX.l2.l1.l0, where XX is the query sequence number that
+is intended to be matched by this qname rule.
+
+Here's the detail on the test cases:
+
+Group 1 - testing skipping recursion for a single policy zone with only
+records that allow recursion to be skipped
+
+Test 1a:
+ 1 policy zone containing 1 'client-ip' trigger
+ 1 query, expected to skip recursion
+
+Test 1b:
+ 1 policy zone containing 1 'qname' trigger (q01)
+ 2 queries, q01 is expected to skip recursion, q02 is expected to
+ recurse
+
+Test 1c:
+ 1 policy zone containing both a 'client-ip' and 'qname' trigger (q02)
+ 1 query, expected to skip recursion
+
+Group 2 - testing skipping recursion with multiple policy zones when all
+zones have only trigger types eligible to skip recursion with
+
+Test 2a:
+ 32 policy zones, each containing 1 'qname' trigger (qNN, where NN is
+ the zone's sequence 1-based sequence number formatted to 2 digits,
+ so each of the first 32 queries should match a different zone)
+ 33 queries, the first 32 of which are expected to skip recursion
+ while the 33rd is expected to recurse
+
+Group 3 - Testing interaction of triggers that require recursion when in
+a single zone, both alone and with triggers that allow recursion to be
+skipped
+
+Test 3a:
+ 1 policy zone containing 1 'ip' trigger
+ 1 query, expected to recurse
+
+Test 3b:
+ 1 policy zone containing 1 'nsdname' trigger
+ 1 query, expected to recurse
+
+Test 3c:
+ 1 policy zone containing 1 'nsip' trigger
+ 1 query, expected to recurse
+
+Test 3d:
+ 1 policy zone containing 1 'ip' trigger and 1 'qname' trigger (q02)
+ 2 queries, the first should not recurse and the second should recurse
+
+Test 3e:
+ 1 policy zone containing 1 'nsdname' trigger and 1 'qname' trigger
+ (q02)
+ 2 queries, the first should not recurse and the second should recurse
+
+Test 3f:
+ 1 policy zone containing 1 'nsip' trigger and 1 'qname' trigger (q02)
+ 2 queries, the first should not recurse and the second should recurse
+
+Group 4 - contains 32 subtests designed to verify that recursion is
+skippable for only the appropriate zones based on the order specified in
+the 'response-policy' statement
+
+Tests 4aa to 4bf:
+ 32 policy zones per test, one of which is configured with 1 'ip'
+ trigger and one 'qname' trigger while the others are configured
+ only with 1 'qname' trigger. The zone with both triggers starts
+ listed first and is moved backwards by one position with each
+ test. The 'qname' triggers in the zones are structured so that
+ the zones are tested starting with the first zone and the 'ip'
+ trigger is tested before the 'qname' trigger for that zone.
+ 33 queries per test, where the number expected to skip recursion
+ matches the test sequence number: e.g. 1 skip for 4aa, 26 skips
+ for 4az, and 32 skips for 4bf
+
+Group 5 - This test verifies that the "pivot" policy zone for whether or
+not recursion can be skipped is the first listed zone with applicable
+trigger types rather than a later listed zone.
+
+Test 5a:
+ 5 policy zones, the 1st, 3rd, and 5th configured with 1 'qname'
+ trigger each (q01, q04, and q06, respectively), the 2nd and 4th
+ each configured with an 'ip' and 'qname' trigger (q02 and q05,
+ respectively for the 'qname' triggers
+ 6 queries, of which only q01 and q02 are expected to skip recursion
diff --git a/bin/tests/system/rpzrecurse/clean.sh b/bin/tests/system/rpzrecurse/clean.sh
new file mode 100644
index 0000000000..e8cf5aa414
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/clean.sh
@@ -0,0 +1,24 @@
+# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+# Clean up after rpz tests.
+
+rm -f dig.out.*
+rm -f ns2/named.conf
+rm -f ns2/*.local
+rm -f ns2/*.queries
+rm -f ns2/named.*.conf
+rm -f ns*/named.lock
+rm -f ns*/named.memstats
+rm -f ns*/named.run
diff --git a/bin/tests/system/rpzrecurse/ns1/db.l0 b/bin/tests/system/rpzrecurse/ns1/db.l0
new file mode 100644
index 0000000000..3fd996fde5
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/ns1/db.l0
@@ -0,0 +1,6 @@
+$TTL 60
+@ IN SOA root.ns ns 1996072700 3600 1800 86400 60
+ NS ns
+ns A 10.53.0.1
+l1 NS ns.l1
+ns.l1 A 10.53.0.1
diff --git a/bin/tests/system/rpzrecurse/ns1/db.l1.l0 b/bin/tests/system/rpzrecurse/ns1/db.l1.l0
new file mode 100644
index 0000000000..cb3c4323dd
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/ns1/db.l1.l0
@@ -0,0 +1,6 @@
+$TTL 60
+@ IN SOA root.ns ns 1996072700 3600 1800 86400 60
+ NS ns
+ns A 10.53.0.1
+l2 NS ns.l2
+ns.l2 A 10.53.0.1
diff --git a/bin/tests/system/rpzrecurse/ns1/named.conf b/bin/tests/system/rpzrecurse/ns1/named.conf
new file mode 100644
index 0000000000..4de9d81610
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/ns1/named.conf
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+ * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ * PERFORMANCE OF THIS SOFTWARE.
+ */
+
+options {
+ query-source address 10.53.0.1;
+ notify-source 10.53.0.1;
+ transfer-source 10.53.0.1;
+ port 5300;
+ pid-file "named.pid";
+ listen-on { 10.53.0.1; };
+ listen-on-v6 { none; };
+ recursion no;
+};
+
+zone "." {
+ type master;
+ file "root.db";
+};
+
+zone "l0" {
+ type master;
+ file "db.l0";
+};
+
+zone "l1.l0" {
+ type master;
+ file "db.l1.l0";
+};
+
+zone "l2.l1.l0" {
+ type master;
+ file "does-not-exist";
+};
diff --git a/bin/tests/system/rpzrecurse/ns1/root.db b/bin/tests/system/rpzrecurse/ns1/root.db
new file mode 100644
index 0000000000..79b7f48768
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/ns1/root.db
@@ -0,0 +1,27 @@
+; Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+;
+; Permission to use, copy, modify, and/or distribute this software for any
+; purpose with or without fee is hereby granted, provided that the above
+; copyright notice and this permission notice appear in all copies.
+;
+; THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+; REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+; AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+; INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+; LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+; OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+; PERFORMANCE OF THIS SOFTWARE.
+
+$TTL 300
+. IN SOA muks.isc.org. a.root.servers.nil. (
+ 2010 ; serial
+ 600 ; refresh
+ 600 ; retry
+ 1200 ; expire
+ 600 ; minimum
+ )
+. NS a.root-servers.nil.
+a.root-servers.nil. A 10.53.0.1
+
+l0. NS ns.l0.
+ns.l0. A 10.53.0.1
diff --git a/bin/tests/system/rpzrecurse/ns2/.gitignore b/bin/tests/system/rpzrecurse/ns2/.gitignore
new file mode 100644
index 0000000000..db4f2c3d45
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/ns2/.gitignore
@@ -0,0 +1,3 @@
+/*.local
+/*.queries
+/*.conf
diff --git a/bin/tests/system/rpzrecurse/ns2/named.conf.default b/bin/tests/system/rpzrecurse/ns2/named.conf.default
new file mode 100644
index 0000000000..b92af5b0cd
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/ns2/named.conf.default
@@ -0,0 +1,9 @@
+# common configuration
+include "named.conf.header";
+
+view "recursive" {
+ zone "." {
+ type hint;
+ file "root.hint";
+ };
+};
diff --git a/bin/tests/system/rpzrecurse/ns2/named.conf.header b/bin/tests/system/rpzrecurse/ns2/named.conf.header
new file mode 100644
index 0000000000..7a78a1a617
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/ns2/named.conf.header
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+ * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ * PERFORMANCE OF THIS SOFTWARE.
+ */
+
+controls { /* empty */ };
+
+options {
+ query-source address 10.53.0.2;
+ notify-source 10.53.0.2;
+ transfer-source 10.53.0.2;
+ port 5300;
+ pid-file "named.pid";
+ listen-on { 10.53.0.2; };
+ listen-on-v6 { none; };
+ recursion yes;
+ querylog yes;
+};
diff --git a/bin/tests/system/rpzrecurse/ns2/root.hint b/bin/tests/system/rpzrecurse/ns2/root.hint
new file mode 100644
index 0000000000..5f9268ef6f
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/ns2/root.hint
@@ -0,0 +1,17 @@
+; Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+;
+; Permission to use, copy, modify, and/or distribute this software for any
+; purpose with or without fee is hereby granted, provided that the above
+; copyright notice and this permission notice appear in all copies.
+;
+; THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+; REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+; AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+; INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+; LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+; OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+; PERFORMANCE OF THIS SOFTWARE.
+
+$TTL 999999
+. IN NS a.root-servers.nil.
+a.root-servers.nil. IN A 10.53.0.1
diff --git a/bin/tests/system/rpzrecurse/prereq.sh b/bin/tests/system/rpzrecurse/prereq.sh
new file mode 100644
index 0000000000..1ce466e097
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/prereq.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+#
+# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+SYSTEMTESTTOP=..
+. $SYSTEMTESTTOP/conf.sh
+
+ret=0
+../rpz/rpz nsdname || ret=1
+../rpz/rpz nsip || ret=1
+
+if [ $ret != 0 ]; then
+ echo "I:This test requires NSIP AND NSDNAME support in RPZ." >&2
+ exit 1
+fi
+
+exec $SHELL ../testcrypto.sh
diff --git a/bin/tests/system/rpzrecurse/setup.sh b/bin/tests/system/rpzrecurse/setup.sh
new file mode 100644
index 0000000000..100464bf1e
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/setup.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+#
+# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+SYSTEMTESTTOP=..
+. $SYSTEMTESTTOP/conf.sh
+
+perl testgen.pl
+cp -f ns2/named.conf.default ns2/named.conf
diff --git a/bin/tests/system/rpzrecurse/testgen.pl b/bin/tests/system/rpzrecurse/testgen.pl
new file mode 100755
index 0000000000..6062a24f42
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/testgen.pl
@@ -0,0 +1,299 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+my $boilerplate_header = <<'EOB';
+# common configuration
+include "named.conf.header";
+
+view "recursive" {
+ zone "." {
+ type hint;
+ file "root.hint";
+ };
+
+ # policy configuration to be tested
+ response-policy {
+EOB
+
+my $boilerplate_middle = <<'EOB';
+ } qname-wait-recurse no;
+
+ # policy zones to be tested
+EOB
+
+my $boilerplate_end = <<'EOB';
+};
+EOB
+
+my $policy_zone_header = <<'EOH';
+$TTL 60
+@ IN SOA root.ns ns 1996072700 3600 1800 86400 60
+ NS ns
+ns A 127.0.0.1
+EOH
+
+sub policy_client_ip {
+ return "32.1.0.0.127.rpz-client-ip CNAME .\n";
+}
+
+sub policy_qname {
+ my $query_nbr = shift;
+ return sprintf "q%02d.l2.l1.l0 CNAME .\n", $query_nbr;
+}
+
+sub policy_ip {
+ return "32.255.255.255.255.rpz-ip CNAME .\n";
+}
+
+sub policy_nsdname {
+ return "does.not.matter.rpz-nsdname CNAME .\n";
+}
+
+sub policy_nsip {
+ return "32.255.255.255.255.rpz-ip CNAME .\n";
+}
+
+my %static_triggers = (
+ 'client-ip' => \&policy_client_ip,
+ 'ip' => \&policy_ip,
+ 'nsdname' => \&policy_nsdname,
+ 'nsip' => \&policy_nsip,
+);
+
+sub mkconf {
+ my $case_id = shift;
+ my $n_queries = shift;
+
+ { # generate the query list
+ my $query_list_filename = "ns2/$case_id.queries";
+ my $query_list_fh;
+
+ open $query_list_fh, ">$query_list_filename" or die;
+
+ for( my $i = 1; $i <= $n_queries; $i++ ) {
+ print $query_list_fh sprintf "q%02d.l2.l1.l0\n", $i;
+ }
+ }
+
+ my @zones;
+
+ { # generate the conf file
+ my $conf_filename = "ns2/named.$case_id.conf";
+
+ my $conf_fh;
+
+ open $conf_fh, ">$conf_filename" or die;
+
+ print $conf_fh $boilerplate_header;
+
+ my $zone_seq = 0;
+
+ @zones = map {
+ [
+ sprintf( "$case_id.%02d.policy.local", $zone_seq++ ),
+ $_,
+ ];
+ } @_;
+
+ print $conf_fh map { qq{ zone "$_->[0]";\n} } @zones;
+
+ print $conf_fh $boilerplate_middle;
+
+ print $conf_fh map { qq{ zone "$_->[0]" { type master; file "db.$_->[0]"; };\n} } @zones;
+
+ print $conf_fh $boilerplate_end;
+ }
+
+ # generate the policy zone contents
+ foreach my $policy_zone_info( @zones ) {
+ my $policy_zone_name = $policy_zone_info->[0];
+ my $policy_zone_contents = $policy_zone_info->[1];
+
+ my $policy_zone_filename = "ns2/db.$policy_zone_name";
+ my $policy_zone_fh;
+
+ open $policy_zone_fh, ">$policy_zone_filename" or die;
+
+ print $policy_zone_fh $policy_zone_header;
+
+ foreach my $trigger( @$policy_zone_contents ) {
+ if( exists $static_triggers{$trigger} ) {
+ # matches a trigger type with a static value
+ print $policy_zone_fh $static_triggers{$trigger}->();
+ }
+ else {
+ # a qname trigger, where what was specified is the query number it should match
+ print $policy_zone_fh policy_qname( $trigger );
+ }
+ }
+ }
+}
+
+mkconf(
+ '1a',
+ 1,
+ [ 'client-ip' ],
+);
+
+mkconf(
+ '1b',
+ 2,
+ [ 1 ],
+);
+
+mkconf(
+ '1c',
+ 1,
+ [ 'client-ip', 2 ],
+);
+
+mkconf(
+ '2a',
+ 33,
+ map { [ $_ ]; } 1 .. 32
+);
+
+mkconf(
+ '3a',
+ 1,
+ [ 'ip' ],
+);
+
+mkconf(
+ '3b',
+ 1,
+ [ 'nsdname' ],
+);
+
+mkconf(
+ '3c',
+ 1,
+ [ 'nsip' ],
+);
+
+mkconf(
+ '3d',
+ 2,
+ [ 'ip', 1 ]
+);
+
+mkconf(
+ '3e',
+ 2,
+ [ 'nsdname', 1 ]
+);
+
+mkconf(
+ '3f',
+ 2,
+ [ 'nsip', 1 ]
+);
+
+{
+ my $seq_code = 'aa';
+ my $seq_nbr = 0;
+
+ while( $seq_nbr < 32 ) {
+
+ mkconf(
+ "4$seq_code",
+ 33,
+ ( map { [ $_ ]; } 1 .. $seq_nbr ),
+ [ 'ip', $seq_nbr + 2 ],
+ ( map { [ $_ + 2 ]; } ($seq_nbr + 1) .. 31 ),
+ );
+
+ $seq_code++;
+ $seq_nbr++;
+ }
+}
+
+mkconf(
+ '5a',
+ 6,
+ [ 1 ],
+ [ 2, 'ip' ],
+ [ 4 ],
+ [ 5, 'ip' ],
+ [ 6 ],
+);
+
+__END__
+
+0x01 - has client-ip
+ 32.1.0.0.127.rpz-client-ip CNAME .
+0x02 - has qname
+ qX.l2.l1.l0 CNAME .
+0x10 - has ip
+ 32.255.255.255.255.rpz-ip CNAME .
+0x20 - has nsdname
+ does.not.matter.rpz-nsdname CNAME .
+0x40 - has nsip
+ 32.255.255.255.255.rpz-nsip CNAME .
+
+$case.$seq.policy.local
+
+case 1a = 0x01
+ .q01 = (00,0x01)=-r
+case 1b = 0x02
+ .q01 = (00,0x02)=-r
+ .q02 = (--,----)=+r
+case 1c = 0x03
+ .q01 = (00,0x01)=-r
+
+case 2a = 0x03{32}
+ .q01 = (00,0x02)=-r
+ .q02 = (01,0x02)=-r
+ ...
+ .q31 = (30,0x02)=-r
+ .q32 = (31,0x02)=-r
+ .q33 = (--,----)=+r
+
+case 3a = 0x10
+ .q01 = (00,0x10)=+r
+case 3b = 0x20
+ .q01 = (00,0x20)=+r
+case 3c = 0x40
+ .q01 = (00,0x40)=+r
+case 3d = 0x12
+ .q01 = (00,0x10)=+r
+ .q02 = (00,0x02)=-r
+case 3e = 0x22
+ .q01 = (00,0x20)=+r
+ .q02 = (00,0x02)=-r
+case 3f = 0x42
+ .q01 = (00,0x40)=+r
+ .q02 = (00,0x02)=-r
+
+case 4aa = 0x12,0x02{31}
+ .q01 = (00,0x10)=+r
+ .q02 = (00,0x02)=-r
+ .q03 = (01,0x02)=+r
+ ...
+ .q32 = (30,0x02)=+r
+ .q33 = (31,0x02)=+r
+case 4__ = 0x02{n(1->30)},0x12,0x02{31-n}
+ .q01 = (00,0x02)=-r
+ ...
+ .q(n+1) = (n,0x10)=+r
+ .q(n+2) = (n,0x02)=-r
+ ...
+ .q33 = (31,0x02)=+r
+case 4bf = 0x02{31},0x12
+ .q01 = (00,0x02)=-r
+ .q02 = (01,0x02)=-r
+ ...
+ .q31 = (30,0x02)=-r
+ .q32 = (31,0x10)=+r
+ .q33 = (31,0x02)=-r
+
+case 5a = 0x02,0x12,0x02,0x12,0x02
+ .q01 = (00,0x02)=-r
+ .q02 = (01,0x02)=-r
+ .q03 = (01,0x10)=+r
+ .q04 = (02,0x02)=+r
+ .q05 = (03,0x02)=+r
+ .q06 = (04,0x02)=+r
+
diff --git a/bin/tests/system/rpzrecurse/tests.sh b/bin/tests/system/rpzrecurse/tests.sh
new file mode 100644
index 0000000000..0e6cb384ab
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/tests.sh
@@ -0,0 +1,168 @@
+#!/bin/sh
+#
+# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+SYSTEMTESTTOP=..
+. $SYSTEMTESTTOP/conf.sh
+
+status=0
+t=0
+
+# $1 = test name (such as 1a, 1b, etc. for which named.$1.conf exists)
+run_server() {
+ TESTNAME=$1
+
+ echo "I:stopping resolver"
+ $PERL $SYSTEMTESTTOP/stop.pl . ns2
+
+ sleep 1
+
+ echo "I:starting resolver using named.$TESTNAME.conf"
+ cp -f ns2/named.$TESTNAME.conf ns2/named.conf
+ $PERL $SYSTEMTESTTOP/start.pl --noclean --restart . ns2
+}
+
+run_query() {
+ TESTNAME=$1
+ LINE=$2
+
+ NAME=`tail -n +"$LINE" ns2/$TESTNAME.queries | head -n 1`
+ $DIG $DIGOPTS $NAME a @10.53.0.2 -p 5300 -b 127.0.0.1 > dig.out.${t}
+ grep "status: SERVFAIL" dig.out.${t} > /dev/null 2>&1 && return 1
+ return 0
+}
+
+# $1 = test name (such as 1a, 1b, etc. for which $1.queries exists)
+# $2 = line number in query file to test (the name to query is taken from this line)
+expect_norecurse() {
+ TESTNAME=$1
+ LINE=$2
+
+ NAME=`tail -n +"$LINE" ns2/$TESTNAME.queries | head -n 1`
+ t=`expr $t + 1`
+ echo "I:testing $NAME doesn't recurse (${t})"
+ run_query $TESTNAME $LINE || {
+ echo "I:test ${t} failed"
+ status=1
+ }
+}
+
+# $1 = test name (such as 1a, 1b, etc. for which $1.queries exists)
+# $2 = line number in query file to test (the name to query is taken from this line)
+expect_recurse() {
+ TESTNAME=$1
+ LINE=$2
+
+ NAME=`tail -n +"$LINE" ns2/$TESTNAME.queries | head -n 1`
+ t=`expr $t + 1`
+ echo "I:testing $NAME recurses (${t})"
+ run_query $TESTNAME $LINE && {
+ echo "I:test $t failed"
+ status=1
+ }
+}
+
+t=`expr $t + 1`
+echo "I:testing that l1.l0 exists without RPZ (${t})"
+$DIG $DIGOPTS l1.l0 ns @10.53.0.2 -p 5300 > dig.out.${t}
+grep "status: NOERROR" dig.out.${t} > /dev/null 2>&1 || {
+ echo "I:test $t failed"
+ status=1
+}
+
+t=`expr $t + 1`
+echo "I:testing that l2.l1.l0 returns SERVFAIL without RPZ (${t})"
+$DIG $DIGOPTS l2.l1.l0 ns @10.53.0.2 -p 5300 > dig.out.${t}
+grep "status: SERVFAIL" dig.out.${t} > /dev/null 2>&1 || {
+ echo "I:test $t failed"
+ status=1
+}
+
+# Group 1
+run_server 1a
+expect_norecurse 1a 1
+run_server 1b
+expect_norecurse 1b 1
+expect_recurse 1b 2
+run_server 1c
+expect_norecurse 1c 1
+
+# Group 2
+run_server 2a
+for n in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
+do
+ expect_norecurse 2a $n
+done
+expect_recurse 2a 33
+
+# Group 3
+run_server 3a
+expect_recurse 3a 1
+run_server 3b
+expect_recurse 3b 1
+run_server 3c
+expect_recurse 3c 1
+run_server 3d
+expect_norecurse 3d 1
+expect_recurse 3d 2
+run_server 3e
+expect_norecurse 3e 1
+expect_recurse 3e 2
+run_server 3f
+expect_norecurse 3f 1
+expect_recurse 3f 2
+
+# Group 4
+testlist="aa ap bf"
+values="1 16 32"
+# Uncomment the following to test every skip value instead of
+# only a sample of values
+#
+#testlist="aa ab ac ad ae af ag ah ai aj ak al am an ao ap \
+# aq ar as at au av aw ax ay az ba bb bc bd be bf"
+#values="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \
+# 21 22 23 24 25 26 27 28 29 30 31 32"
+set -- $values
+for n in $testlist; do
+ run_server 4$n
+ ni=$1
+ t=`expr $t + 1`
+ echo "I:testing that ${ni} of 33 queries skip recursion (${t})"
+ c=0
+ for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \
+ 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
+ do
+ run_query 4$n $i
+ c=`expr $c + $?`
+ done
+ skipped=`expr 33 - $c`
+ if [ $skipped != $ni ]; then
+ echo "I:test $t failed (actual=$skipped, expected=$ni)"
+ status=1
+ fi
+ shift
+done
+
+# Group 5
+run_server 5a
+expect_norecurse 5a 1
+expect_norecurse 5a 2
+expect_recurse 5a 3
+expect_recurse 5a 4
+expect_recurse 5a 5
+expect_recurse 5a 6
+
+echo "I:exit status: $status"
+exit $status
diff --git a/doc/arm/Bv9ARM-book.xml b/doc/arm/Bv9ARM-book.xml
index 528e2df95d..4490125c95 100644
--- a/doc/arm/Bv9ARM-book.xml
+++ b/doc/arm/Bv9ARM-book.xml
@@ -9950,7 +9950,7 @@ deny-answer-aliases { "example.net"; };
DISABLED actions) must be chosen.
Triggers or the records that encode them are chosen for the
rewriting in the following order:
-
+
Choose the triggered record in the zone that appears
first in the response-policy option.
@@ -9967,7 +9967,7 @@ deny-answer-aliases { "example.net"; };
prefer the IP or NSIP trigger that matches
the smallest IP address.
-
+
diff --git a/doc/arm/notes.xml b/doc/arm/notes.xml
index 2dea1edc92..57e46ff811 100644
--- a/doc/arm/notes.xml
+++ b/doc/arm/notes.xml
@@ -620,6 +620,17 @@
dig +short. [RT #39291]
+
+
+ A bug in the RPZ implementation could cause some policy
+ zones that did not specifically require recursion to be
+ treated as if they did; consequently, setting
+ qname-wait-recurse no; was
+ sometimes ineffective. This has been corrected.
+ In most configurations, behavioral changes due to this
+ fix will not be noticeable. [RT #39229]
+
+
diff --git a/lib/dns/rpz.c b/lib/dns/rpz.c
index bec8330d51..7f9bdd5887 100644
--- a/lib/dns/rpz.c
+++ b/lib/dns/rpz.c
@@ -55,7 +55,7 @@
*
* Each leaf indicates that an IP address is listed in the IP address or the
* name server IP address policy sub-zone (or both) of the corresponding
- * response response zone. The policy data such as a CNAME or an A record
+ * response policy zone. The policy data such as a CNAME or an A record
* is kept in the policy zone. After an IP address has been found in a radix
* tree, the node in the policy zone's database is found by converting
* the IP address to a domain name in a canonical form.
@@ -133,11 +133,6 @@ struct dns_rpz_cidr_node {
dns_rpz_addr_zbits_t sum;
};
-/*
- * The data in a RBT node has two pairs of bits for policy zones.
- * One pair is for the corresponding name of the node such as example.com
- * and the other pair is for a wildcard child such as *.example.com.
- */
/*
* A pair of arrays of bits flagging the existence of
* QNAME and NSDNAME policy triggers.
@@ -148,6 +143,11 @@ struct dns_rpz_nm_zbits {
dns_rpz_zbits_t ns;
};
+/*
+ * The data in a RBT node has two pairs of bits for policy zones.
+ * One pair is for the corresponding name of the node such as example.com
+ * and the other pair is for a wildcard child such as *.example.com.
+ */
typedef struct dns_rpz_nm_data dns_rpz_nm_data_t;
struct dns_rpz_nm_data {
dns_rpz_nm_zbits_t set;
@@ -259,11 +259,15 @@ dns_rpz_policy2str(dns_rpz_policy_t policy) {
return (str);
}
+/*
+ * Return the bit number of the highest set bit in 'zbit'.
+ * (for example, 0x01 returns 0, 0xFF returns 7, etc.)
+ */
static int
zbit_to_num(dns_rpz_zbits_t zbit) {
dns_rpz_num_t rpz_num;
- INSIST(zbit != 0);
+ REQUIRE(zbit != 0);
rpz_num = 0;
#if DNS_RPZ_MAX_ZONES > 32
if ((zbit & 0xffffffff00000000L) != 0) {
@@ -376,30 +380,172 @@ set_sum_pair(dns_rpz_cidr_node_t *cnode) {
static void
fix_qname_skip_recurse(dns_rpz_zones_t *rpzs) {
- dns_rpz_zbits_t zbits;
+ dns_rpz_zbits_t mask;
+
+ /* qname_wait_recurse and qname_skip_recurse are used to
+ * implement the "qname-wait-recurse" config option.
+ *
+ * By default, "qname-wait-recurse" is yes, so no
+ * processing happens without recursion. In this case,
+ * qname_wait_recurse is true, and qname_skip_recurse
+ * (a bit field indicating which policy zones can be
+ * processed without recursion) is set to all 0's by
+ * fix_qname_skip_recurse().
+ *
+ * When "qname-wait-recurse" is no, qname_skip_recurse may be
+ * set to a non-zero value by fix_qname_skip_recurse(). The mask
+ * has to have bits set for for the policy zones for which
+ * processing may continue without recursion, and bits cleared
+ * for the rest.
+ *
+ * (1) The ARM says:
+ *
+ * The "qname-wait-recurse no" option overrides that default
+ * behavior when recursion cannot change a non-error
+ * response. The option does not affect QNAME or client-IP
+ * triggers in policy zones listed after other zones
+ * containing IP, NSIP and NSDNAME triggers, because those may
+ * depend on the A, AAAA, and NS records that would be found
+ * during recursive resolution.
+ *
+ * Let's consider the following:
+ *
+ * zbits_req = (rpzs->have.ipv4 | rpzs->have.ipv6 |
+ * rpzs->have.nsdname |
+ * rpzs->have.nsipv4 | rpzs->have.nsipv6);
+ *
+ * zbits_req now contains bits set for zones which require
+ * recursion.
+ *
+ * But going by the description in the ARM, if the first policy
+ * zone requires recursion, then all zones after that (higher
+ * order bits) have to wait as well. If the Nth zone requires
+ * recursion, then (N+1)th zone onwards all need to wait.
+ *
+ * So mapping this, examples:
+ *
+ * zbits_req = 0b000 mask = 0xffffffff (no zones have to wait for
+ * recursion)
+ * zbits_req = 0b001 mask = 0x00000000 (all zones have to wait)
+ * zbits_req = 0b010 mask = 0x00000001 (the first zone doesn't have to
+ * wait, second zone onwards need
+ * to wait)
+ * zbits_req = 0b011 mask = 0x00000000 (all zones have to wait)
+ * zbits_req = 0b100 mask = 0x00000011 (the 1st and 2nd zones don't
+ * have to wait, third zone
+ * onwards need to wait)
+ *
+ * More generally, we have to count the number of trailing 0
+ * bits in zbits_req and only these can be processed without
+ * recursion. All the rest need to wait.
+ *
+ * (2) The ARM says that "qname-wait-recurse no" option
+ * overrides the default behavior when recursion cannot change a
+ * non-error response. So, in the order of listing of policy
+ * zones, within the first policy zone where recursion may be
+ * required, we should first allow CLIENT-IP and QNAME policy
+ * records to be attempted without recursion.
+ */
/*
* Get a mask covering all policy zones that are not subordinate to
* other policy zones containing triggers that require that the
* qname be resolved before they can be checked.
*/
- if (rpzs->p.qname_wait_recurse) {
- zbits = 0;
- } else {
- zbits = (rpzs->have.ipv4 || rpzs->have.ipv6 ||
- rpzs->have.nsdname ||
- rpzs->have.nsipv4 || rpzs->have.nsipv6);
- if (zbits == 0) {
- zbits = DNS_RPZ_ALL_ZBITS;
- } else {
- zbits = DNS_RPZ_ZMASK(zbit_to_num(zbits));
- }
- }
- rpzs->have.qname_skip_recurse = zbits;
-
rpzs->have.client_ip = rpzs->have.client_ipv4 | rpzs->have.client_ipv6;
rpzs->have.ip = rpzs->have.ipv4 | rpzs->have.ipv6;
rpzs->have.nsip = rpzs->have.nsipv4 | rpzs->have.nsipv6;
+
+ if (rpzs->p.qname_wait_recurse) {
+ mask = 0;
+ } else {
+ dns_rpz_zbits_t zbits_req;
+ dns_rpz_zbits_t zbits_notreq;
+ dns_rpz_zbits_t mask2;
+ dns_rpz_zbits_t req_mask;
+
+ /*
+ * Get the masks of zones with policies that
+ * do/don't require recursion
+ */
+
+ zbits_req = (rpzs->have.ipv4 | rpzs->have.ipv6 |
+ rpzs->have.nsdname |
+ rpzs->have.nsipv4 | rpzs->have.nsipv6);
+ zbits_notreq = (rpzs->have.client_ip | rpzs->have.qname);
+
+ if (zbits_req == 0) {
+ mask = DNS_RPZ_ALL_ZBITS;
+ goto set;
+ }
+
+ /*
+ * req_mask is a mask covering used bits in
+ * zbits_req. (For instance, 0b1 => 0b1, 0b101 => 0b111,
+ * 0b11010101 => 0b11111111).
+ */
+ req_mask = zbits_req;
+ req_mask |= req_mask >> 1;
+ req_mask |= req_mask >> 2;
+ req_mask |= req_mask >> 4;
+ req_mask |= req_mask >> 8;
+ req_mask |= req_mask >> 16;
+#if DNS_RPZ_MAX_ZONES > 32
+ req_mask |= req_mask >> 32;
+#endif
+
+ /*
+ * There's no point in skipping recursion for a later
+ * zone if it is required in a previous zone.
+ */
+ if ((zbits_notreq & req_mask) == 0) {
+ mask = 0;
+ goto set;
+ }
+
+ /*
+ * This bit arithmetic creates a mask of zones in which
+ * it is okay to skip recursion. After the first zone
+ * that has to wait for recursion, all the others have
+ * to wait as well, so we want to create a mask in which
+ * all the trailing zeroes in zbits_req are are 1, and
+ * more significant bits are 0. (For instance,
+ * 0x0700 => 0x00ff, 0x0007 => 0x0000)
+ */
+ mask = ~(zbits_req | -zbits_req);
+
+ /*
+ * As mentioned in (2) above, the zone corresponding to
+ * the least significant zero could have its CLIENT-IP
+ * and QNAME policies checked before recursion, if it
+ * has any of those policies. So if it does, we
+ * can set its 0 to 1.
+ *
+ * Locate the least significant 0 bit in the mask (for
+ * instance, 0xff => 0x100)...
+ */
+ mask2 = (mask << 1) & ~mask;
+
+ /*
+ * Also set the bit for zone 0, because if it's in
+ * zbits_notreq then it's definitely okay to attempt to
+ * skip recursion for zone 0...
+ */
+ mask2 |= 1;
+
+ /* Clear any bits *not* in zbits_notreq... */
+ mask2 &= zbits_notreq;
+
+ /* And merge the result into the skip-recursion mask */
+ mask |= mask2;
+ }
+
+ set:
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_RPZ, DNS_LOGMODULE_RBTDB,
+ DNS_RPZ_DEBUG_QUIET,
+ "computed RPZ qname_skip_recurse mask=0x%llx",
+ (isc_uint64_t) mask);
+ rpzs->have.qname_skip_recurse = mask;
}
static void