]> git.ipfire.org Git - thirdparty/pdns.git/blobdiff - regression-tests.recursor-dnssec/test_RPZ.py
Make sure we can install unsigned packages.
[thirdparty/pdns.git] / regression-tests.recursor-dnssec / test_RPZ.py
index 39f8e410c4168b40ffcfd5f980b5a5bc0aaa462b..8341a8cd5a593ba2d254a66b591bc2835cd71629 100644 (file)
@@ -52,7 +52,8 @@ class RPZServer(object):
         elif message.question[0].rdtype == dns.rdatatype.IXFR:
             oldSerial = message.authority[0][0].serial
 
-            if oldSerial != self._currentSerial:
+            # special case for the 9th update, which might get skipped
+            if oldSerial != self._currentSerial and self._currentSerial != 9:
                 print('Received an IXFR query with an unexpected serial %d, expected %d' % (oldSerial, self._currentSerial))
                 return (None, self._currentSerial)
 
@@ -118,6 +119,29 @@ class RPZServer(object):
                     dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'),
                     dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial)
                     ]
+            elif newSerial == 8:
+                # this one is a bit special too, we are answering with a full AXFR and the new zone is empty
+                records = [
+                    dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
+                    dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial)
+                    ]
+            elif newSerial == 9:
+                # IXFR inserting a duplicate, we should not crash and skip it
+                records = [
+                    dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
+                    dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial),
+                    dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
+                    dns.rrset.from_text('dup.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-passthru.'),
+                    dns.rrset.from_text('dup.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-passthru.'),
+                    dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial)
+                    ]
+            elif newSerial == 10:
+                # full AXFR to make sure we are removing the duplicate, adding a record, to the that the update was correctly applied
+                records = [
+                    dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
+                    dns.rrset.from_text('f.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
+                    dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial)
+                    ]
 
         response.answer = records
         return (newSerial, response)
@@ -176,19 +200,7 @@ class RPZServer(object):
                 print('Error in RPZ socket: %s' % str(e))
                 sock.close()
 
-rpzServerPort = 4250
-rpzServer = RPZServer(rpzServerPort)
-
 class RPZRecursorTest(RecursorTest):
-    """
-    This test makes sure that we correctly update RPZ zones via AXFR then IXFR
-    """
-
-    global rpzServerPort
-    _lua_config_file = """
-    -- The first server is a bogus one, to test that we correctly fail over to the second one
-    rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1 })
-    """ % (rpzServerPort)
     _wsPort = 8042
     _wsTimeout = 2
     _wsPassword = 'secretpassword'
@@ -212,22 +224,8 @@ webserver-port=%d
 webserver-address=127.0.0.1
 webserver-password=%s
 api-key=%s
+log-rpz-changes=yes
 """ % (_confdir, _wsPort, _wsPassword, _apiKey)
-    _xfrDone = 0
-
-    @classmethod
-    def generateRecursorConfig(cls, confdir):
-        authzonepath = os.path.join(confdir, 'example.zone')
-        with open(authzonepath, 'w') as authzone:
-            authzone.write("""$ORIGIN example.
-@ 3600 IN SOA {soa}
-a 3600 IN A 192.0.2.42
-b 3600 IN A 192.0.2.42
-c 3600 IN A 192.0.2.42
-d 3600 IN A 192.0.2.42
-e 3600 IN A 192.0.2.42
-""".format(soa=cls._SOA))
-        super(RPZRecursorTest, cls).generateRecursorConfig(confdir)
 
     @classmethod
     def setUpClass(cls):
@@ -245,7 +243,7 @@ e 3600 IN A 192.0.2.42
     def tearDownClass(cls):
         cls.tearDownRecursor()
 
-    def checkBlocked(self, name, shouldBeBlocked=True, adQuery=False):
+    def checkBlocked(self, name, shouldBeBlocked=True, adQuery=False, singleCheck=False):
         query = dns.message.make_query(name, 'A', want_dnssec=True)
         query.flags |= dns.flags.CD
         if adQuery:
@@ -261,9 +259,11 @@ e 3600 IN A 192.0.2.42
                 expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.42')
 
             self.assertRRsetInAnswer(res, expected)
+            if singleCheck:
+                break
 
-    def checkNotBlocked(self, name, adQuery=False):
-        self.checkBlocked(name, False, adQuery)
+    def checkNotBlocked(self, name, adQuery=False, singleCheck=False):
+        self.checkBlocked(name, False, adQuery, singleCheck)
 
     def checkCustom(self, qname, qtype, expected):
         query = dns.message.make_query(qname, qtype, want_dnssec=True)
@@ -283,6 +283,16 @@ e 3600 IN A 192.0.2.42
             self.assertRcodeEqual(res, dns.rcode.NOERROR)
             self.assertEqual(len(res.answer), 0)
 
+    def checkNXD(self, qname, qtype='A'):
+        query = dns.message.make_query(qname, qtype, want_dnssec=True)
+        query.flags |= dns.flags.CD
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            res = sender(query)
+            self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
+            self.assertEqual(len(res.answer), 0)
+            self.assertEqual(len(res.authority), 1)
+
     def checkTruncated(self, qname, qtype='A'):
         query = dns.message.make_query(qname, qtype, want_dnssec=True)
         query.flags |= dns.flags.CD
@@ -308,6 +318,66 @@ e 3600 IN A 192.0.2.42
             res = sender(query)
             self.assertEqual(res, None)
 
+    def checkRPZStats(self, serial, recordsCount, fullXFRCount, totalXFRCount):
+        headers = {'x-api-key': self._apiKey}
+        url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/rpzstatistics'
+        r = requests.get(url, headers=headers, timeout=self._wsTimeout)
+        self.assertTrue(r)
+        self.assertEquals(r.status_code, 200)
+        self.assertTrue(r.json())
+        content = r.json()
+        self.assertIn('zone.rpz.', content)
+        zone = content['zone.rpz.']
+        for key in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']:
+            self.assertIn(key, zone)
+
+        self.assertEquals(zone['serial'], serial)
+        self.assertEquals(zone['records'], recordsCount)
+        self.assertEquals(zone['transfers_full'], fullXFRCount)
+        self.assertEquals(zone['transfers_success'], totalXFRCount)
+
+rpzServerPort = 4250
+rpzServer = RPZServer(rpzServerPort)
+
+class RPZXFRRecursorTest(RPZRecursorTest):
+    """
+    This test makes sure that we correctly update RPZ zones via AXFR then IXFR
+    """
+
+    global rpzServerPort
+    _lua_config_file = """
+    -- The first server is a bogus one, to test that we correctly fail over to the second one
+    rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1 })
+    """ % (rpzServerPort)
+    _confdir = 'RPZXFR'
+    _wsPort = 8042
+    _wsTimeout = 2
+    _wsPassword = 'secretpassword'
+    _apiKey = 'secretapikey'
+    _config_template = """
+auth-zones=example=configs/%s/example.zone
+webserver=yes
+webserver-port=%d
+webserver-address=127.0.0.1
+webserver-password=%s
+api-key=%s
+""" % (_confdir, _wsPort, _wsPassword, _apiKey)
+    _xfrDone = 0
+
+    @classmethod
+    def generateRecursorConfig(cls, confdir):
+        authzonepath = os.path.join(confdir, 'example.zone')
+        with open(authzonepath, 'w') as authzone:
+            authzone.write("""$ORIGIN example.
+@ 3600 IN SOA {soa}
+a 3600 IN A 192.0.2.42
+b 3600 IN A 192.0.2.42
+c 3600 IN A 192.0.2.42
+d 3600 IN A 192.0.2.42
+e 3600 IN A 192.0.2.42
+""".format(soa=cls._SOA))
+        super(RPZRecursorTest, cls).generateRecursorConfig(confdir)
+
     def waitUntilCorrectSerialIsLoaded(self, serial, timeout=5):
         global rpzServer
 
@@ -327,24 +397,6 @@ e 3600 IN A 192.0.2.42
 
         raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout, serial, currentSerial))
 
-    def checkRPZStats(self, serial, recordsCount, fullXFRCount, totalXFRCount):
-        headers = {'x-api-key': self._apiKey}
-        url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/rpzstatistics'
-        r = requests.get(url, headers=headers, timeout=self._wsTimeout)
-        self.assertTrue(r)
-        self.assertEquals(r.status_code, 200)
-        self.assertTrue(r.json())
-        content = r.json()
-        self.assertIn('zone.rpz.', content)
-        zone = content['zone.rpz.']
-        for key in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']:
-            self.assertIn(key, zone)
-
-        self.assertEquals(zone['serial'], serial)
-        self.assertEquals(zone['records'], recordsCount)
-        self.assertEquals(zone['transfers_full'], fullXFRCount)
-        self.assertEquals(zone['transfers_success'], totalXFRCount)
-
     def testRPZ(self):
         # first zone, only a should be blocked
         self.waitUntilCorrectSerialIsLoaded(1)
@@ -410,3 +462,374 @@ e 3600 IN A 192.0.2.42
         # check non-custom policies
         self.checkTruncated('tc.example.')
         self.checkDropped('drop.example.')
+
+        # eighth zone, all entries should be gone
+        self.waitUntilCorrectSerialIsLoaded(8)
+        self.checkRPZStats(8, 0, 3, self._xfrDone)
+        self.checkNotBlocked('a.example.')
+        self.checkNotBlocked('b.example.')
+        self.checkNotBlocked('c.example.')
+        self.checkNotBlocked('d.example.')
+        self.checkNotBlocked('e.example.')
+        self.checkNXD('f.example.')
+        self.checkNXD('tc.example.')
+        self.checkNXD('drop.example.')
+
+        # 9th zone is a duplicate, it might get skipped
+        global rpzServer
+        rpzServer.moveToSerial(9)
+        time.sleep(3)
+        self.waitUntilCorrectSerialIsLoaded(10)
+        self.checkRPZStats(10, 1, 4, self._xfrDone)
+        self.checkNotBlocked('a.example.')
+        self.checkNotBlocked('b.example.')
+        self.checkNotBlocked('c.example.')
+        self.checkNotBlocked('d.example.')
+        self.checkNotBlocked('e.example.')
+        self.checkBlocked('f.example.')
+        self.checkNXD('tc.example.')
+        self.checkNXD('drop.example.')
+
+class RPZFileRecursorTest(RPZRecursorTest):
+    """
+    This test makes sure that we correctly load RPZ zones from a file
+    """
+
+    _confdir = 'RPZFile'
+    _lua_config_file = """
+    rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." })
+    """ % (_confdir)
+    _config_template = """
+auth-zones=example=configs/%s/example.zone
+""" % (_confdir)
+
+    @classmethod
+    def generateRecursorConfig(cls, confdir):
+        authzonepath = os.path.join(confdir, 'example.zone')
+        with open(authzonepath, 'w') as authzone:
+            authzone.write("""$ORIGIN example.
+@ 3600 IN SOA {soa}
+a 3600 IN A 192.0.2.42
+b 3600 IN A 192.0.2.42
+c 3600 IN A 192.0.2.42
+d 3600 IN A 192.0.2.42
+e 3600 IN A 192.0.2.42
+z 3600 IN A 192.0.2.42
+""".format(soa=cls._SOA))
+
+        rpzFilePath = os.path.join(confdir, 'zone.rpz')
+        with open(rpzFilePath, 'w') as rpzZone:
+            rpzZone.write("""$ORIGIN zone.rpz.
+@ 3600 IN SOA {soa}
+a.example.zone.rpz. 60 IN A 192.0.2.42
+a.example.zone.rpz. 60 IN A 192.0.2.43
+a.example.zone.rpz. 60 IN TXT "some text"
+drop.example.zone.rpz. 60 IN CNAME rpz-drop.
+z.example.zone.rpz. 60 IN A 192.0.2.1
+tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
+""".format(soa=cls._SOA))
+        super(RPZFileRecursorTest, cls).generateRecursorConfig(confdir)
+
+    def testRPZ(self):
+        self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43'))
+        self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"'))
+        self.checkBlocked('z.example.')
+        self.checkNotBlocked('b.example.')
+        self.checkNotBlocked('c.example.')
+        self.checkNotBlocked('d.example.')
+        self.checkNotBlocked('e.example.')
+        # check that the policy is disabled for AD=1 queries
+        self.checkNotBlocked('z.example.', True)
+        # check non-custom policies
+        self.checkTruncated('tc.example.')
+        self.checkDropped('drop.example.')
+
+class RPZFileDefaultPolRecursorTest(RPZRecursorTest):
+    """
+    This test makes sure that we correctly load RPZ zones from a file with a default policy
+    """
+
+    _confdir = 'RPZFileDefaultPolicy'
+    _lua_config_file = """
+    rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction })
+    """ % (_confdir)
+    _config_template = """
+auth-zones=example=configs/%s/example.zone
+""" % (_confdir)
+
+    @classmethod
+    def generateRecursorConfig(cls, confdir):
+        authzonepath = os.path.join(confdir, 'example.zone')
+        with open(authzonepath, 'w') as authzone:
+            authzone.write("""$ORIGIN example.
+@ 3600 IN SOA {soa}
+a 3600 IN A 192.0.2.42
+b 3600 IN A 192.0.2.42
+c 3600 IN A 192.0.2.42
+d 3600 IN A 192.0.2.42
+drop 3600 IN A 192.0.2.42
+e 3600 IN A 192.0.2.42
+z 3600 IN A 192.0.2.42
+""".format(soa=cls._SOA))
+
+        rpzFilePath = os.path.join(confdir, 'zone.rpz')
+        with open(rpzFilePath, 'w') as rpzZone:
+            rpzZone.write("""$ORIGIN zone.rpz.
+@ 3600 IN SOA {soa}
+a.example.zone.rpz. 60 IN A 192.0.2.42
+drop.example.zone.rpz. 60 IN CNAME rpz-drop.
+z.example.zone.rpz. 60 IN A 192.0.2.1
+tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
+""".format(soa=cls._SOA))
+        super(RPZFileDefaultPolRecursorTest, cls).generateRecursorConfig(confdir)
+
+    def testRPZ(self):
+        # local data entries are overridden by default
+        self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42'))
+        self.checkNoData('a.example.', 'TXT')
+        # will not be blocked because the default policy overrides local data entries by default
+        self.checkNotBlocked('z.example.')
+        self.checkNotBlocked('b.example.')
+        self.checkNotBlocked('c.example.')
+        self.checkNotBlocked('d.example.')
+        self.checkNotBlocked('e.example.')
+        # check non-local policies, they should be overridden by the default policy
+        self.checkNXD('tc.example.', 'A')
+        self.checkNotBlocked('drop.example.')
+
+class RPZFileDefaultPolNotOverrideLocalRecursorTest(RPZRecursorTest):
+    """
+    This test makes sure that we correctly load RPZ zones from a file with a default policy, not overriding local data entries
+    """
+
+    _confdir = 'RPZFileDefaultPolicyNotOverrideLocal'
+    _lua_config_file = """
+    rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false })
+    """ % (_confdir)
+    _config_template = """
+auth-zones=example=configs/%s/example.zone
+""" % (_confdir)
+
+    @classmethod
+    def generateRecursorConfig(cls, confdir):
+        authzonepath = os.path.join(confdir, 'example.zone')
+        with open(authzonepath, 'w') as authzone:
+            authzone.write("""$ORIGIN example.
+@ 3600 IN SOA {soa}
+a 3600 IN A 192.0.2.42
+b 3600 IN A 192.0.2.42
+c 3600 IN A 192.0.2.42
+d 3600 IN A 192.0.2.42
+drop 3600 IN A 192.0.2.42
+e 3600 IN A 192.0.2.42
+z 3600 IN A 192.0.2.42
+""".format(soa=cls._SOA))
+
+        rpzFilePath = os.path.join(confdir, 'zone.rpz')
+        with open(rpzFilePath, 'w') as rpzZone:
+            rpzZone.write("""$ORIGIN zone.rpz.
+@ 3600 IN SOA {soa}
+a.example.zone.rpz. 60 IN A 192.0.2.42
+a.example.zone.rpz. 60 IN A 192.0.2.43
+a.example.zone.rpz. 60 IN TXT "some text"
+drop.example.zone.rpz. 60 IN CNAME rpz-drop.
+z.example.zone.rpz. 60 IN A 192.0.2.1
+tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
+""".format(soa=cls._SOA))
+        super(RPZFileDefaultPolNotOverrideLocalRecursorTest, cls).generateRecursorConfig(confdir)
+
+    def testRPZ(self):
+        # local data entries will not be overridden by the default policy
+        self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43'))
+        self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"'))
+        # will be blocked because the default policy does not override local data entries
+        self.checkBlocked('z.example.')
+        self.checkNotBlocked('b.example.')
+        self.checkNotBlocked('c.example.')
+        self.checkNotBlocked('d.example.')
+        self.checkNotBlocked('e.example.')
+        # check non-local policies, they should be overridden by the default policy
+        self.checkNXD('tc.example.', 'A')
+        self.checkNotBlocked('drop.example.')
+
+class RPZSimpleAuthServer(object):
+
+    def __init__(self, port):
+        self._serverPort = port
+        listener = threading.Thread(name='RPZ Simple Auth Listener', target=self._listener, args=[])
+        listener.setDaemon(True)
+        listener.start()
+
+    def _getAnswer(self, message):
+
+        response = dns.message.make_response(message)
+        response.flags |= dns.flags.AA
+        records = [
+            dns.rrset.from_text('nsip.delegated.example.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.42')
+        ]
+
+        response.answer = records
+        return response
+
+    def _listener(self):
+        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        try:
+            sock.bind(("127.0.0.1", self._serverPort))
+        except socket.error as e:
+            print("Error binding in the RPZ simple auth listener: %s" % str(e))
+            sys.exit(1)
+
+        while True:
+            try:
+                data, addr = sock.recvfrom(4096)
+                message = dns.message.from_wire(data)
+                if len(message.question) != 1:
+                    print('Invalid query, qdcount is %d' % (len(message.question)))
+                    break
+
+                answer = self._getAnswer(message)
+                if not answer:
+                    print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype))
+                    break
+
+                wire = answer.to_wire()
+                sock.sendto(wire, addr)
+
+            except socket.error as e:
+                print('Error in RPZ simple auth socket: %s' % str(e))
+
+rpzAuthServerPort = 4260
+rpzAuthServer = RPZSimpleAuthServer(rpzAuthServerPort)
+
+class RPZOrderingPrecedenceRecursorTest(RPZRecursorTest):
+    """
+    This test makes sure that the recursor respects the RPZ ordering precedence rules
+    """
+
+    _confdir = 'RPZOrderingPrecedence'
+    _lua_config_file = """
+    rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
+    rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
+    """ % (_confdir, _confdir)
+    _config_template = """
+auth-zones=example=configs/%s/example.zone
+forward-zones=delegated.example=127.0.0.1:%d
+""" % (_confdir, rpzAuthServerPort)
+
+    @classmethod
+    def generateRecursorConfig(cls, confdir):
+        authzonepath = os.path.join(confdir, 'example.zone')
+        with open(authzonepath, 'w') as authzone:
+            authzone.write("""$ORIGIN example.
+@ 3600 IN SOA {soa}
+sub.test 3600 IN A 192.0.2.42
+passthru-then-blocked-by-higher 3600 IN A 192.0.2.66
+passthru-then-blocked-by-same 3600 IN A 192.0.2.66
+blocked-then-passhtru-by-higher 3600 IN A 192.0.2.100
+""".format(soa=cls._SOA))
+
+        rpzFilePath = os.path.join(confdir, 'zone.rpz')
+        with open(rpzFilePath, 'w') as rpzZone:
+            rpzZone.write("""$ORIGIN zone.rpz.
+@ 3600 IN SOA {soa}
+*.test.example.zone.rpz. 60 IN CNAME rpz-passthru.
+32.66.2.0.192.rpz-ip.zone.rpz. 60 IN A 192.0.2.1
+32.100.2.0.192.rpz-ip.zone.rpz. 60 IN CNAME rpz-passthru.
+passthru-then-blocked-by-same.example.zone.rpz. 60 IN CNAME rpz-passthru.
+32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN CNAME rpz-passthru.
+""".format(soa=cls._SOA))
+
+        rpzFilePath = os.path.join(confdir, 'zone2.rpz')
+        with open(rpzFilePath, 'w') as rpzZone:
+            rpzZone.write("""$ORIGIN zone2.rpz.
+@ 3600 IN SOA {soa}
+sub.test.example.com.zone2.rpz. 60 IN CNAME .
+passthru-then-blocked-by-higher.example.zone2.rpz. 60 IN CNAME rpz-passthru.
+blocked-then-passhtru-by-higher.example.zone2.rpz. 60 IN A 192.0.2.1
+32.42.2.0.192.rpz-ip 60 IN CNAME .
+""".format(soa=cls._SOA))
+
+        super(RPZOrderingPrecedenceRecursorTest, cls).generateRecursorConfig(confdir)
+
+    def testRPZOrderingForQNameAndWhitelisting(self):
+        # we should first match on the qname (the wildcard, not on the exact name since
+        # we respect the order of the RPZ zones), see the pass-thru rule
+        # and only process RPZ rules of higher precedence.
+        # The subsequent rule on the content of the A should therefore not trigger a NXDOMAIN.
+        self.checkNotBlocked('sub.test.example.')
+
+    def testRPZOrderingWhitelistedThenBlockedByHigher(self):
+        # we should first match on the qname from the second RPZ zone,
+        # continue the resolution process, and get blocked by the content of the A record
+        # based on the first RPZ zone, whose priority is higher than the second one.
+        self.checkBlocked('passthru-then-blocked-by-higher.example.')
+
+    def testRPZOrderingWhitelistedThenBlockedBySame(self):
+        # we should first match on the qname from the first RPZ zone,
+        # continue the resolution process, and NOT get blocked by the content of the A record
+        # based on the same RPZ zone, since it's not higher.
+        self.checkCustom('passthru-then-blocked-by-same.example.', 'A', dns.rrset.from_text('passthru-then-blocked-by-same.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.66'))
+
+    def testRPZOrderBlockedThenWhitelisted(self):
+        # The qname is first blocked by the second RPZ zone
+        # Then, should the resolution process go on, the A record would be whitelisted
+        # by the first zone.
+        # This is what the RPZ specification requires, but we currently decided that we
+        # don't want to leak queries to malicious DNS servers and waste time if the qname is blacklisted.
+        # We might change our opinion at some point, though.
+        self.checkBlocked('blocked-then-passhtru-by-higher.example.')
+
+    def testRPZOrderDelegate(self):
+        # The IP of the NS we are going to contact is whitelisted (passthru) in zone 1,
+        # so even though the record (192.0.2.42) returned by the server is blacklisted
+        # by zone 2, it should not be blocked.
+        # We only test once because after that the answer is cached, so the NS is not contacted
+        # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
+        self.checkNotBlocked('nsip.delegated.example.', singleCheck=True)
+
+class RPZNSIPCustomTest(RPZRecursorTest):
+    """
+    This test makes sure that the recursor handles custom RPZ rules in a NSIP
+    """
+
+    _confdir = 'RPZNSIPCustom'
+    _lua_config_file = """
+    rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
+    rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
+    """ % (_confdir, _confdir)
+    _config_template = """
+auth-zones=example=configs/%s/example.zone
+forward-zones=delegated.example=127.0.0.1:%d
+""" % (_confdir, rpzAuthServerPort)
+
+    @classmethod
+    def generateRecursorConfig(cls, confdir):
+        authzonepath = os.path.join(confdir, 'example.zone')
+        with open(authzonepath, 'w') as authzone:
+            authzone.write("""$ORIGIN example.
+@ 3600 IN SOA {soa}
+""".format(soa=cls._SOA))
+
+        rpzFilePath = os.path.join(confdir, 'zone.rpz')
+        with open(rpzFilePath, 'w') as rpzZone:
+            rpzZone.write("""$ORIGIN zone.rpz.
+@ 3600 IN SOA {soa}
+32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN A 192.0.2.1
+""".format(soa=cls._SOA))
+
+        rpzFilePath = os.path.join(confdir, 'zone2.rpz')
+        with open(rpzFilePath, 'w') as rpzZone:
+            rpzZone.write("""$ORIGIN zone2.rpz.
+@ 3600 IN SOA {soa}
+32.1.2.0.192.rpz-ip 60 IN CNAME .
+""".format(soa=cls._SOA))
+
+        super(RPZNSIPCustomTest, cls).generateRecursorConfig(confdir)
+
+    def testRPZDelegate(self):
+        # The IP of the NS we are going to contact should result in a custom record (192.0.2.1) from zone 1,
+        # so even though the record (192.0.2.1) returned by the server is blacklisted
+        # by zone 2, it should not be blocked.
+        # We only test once because after that the answer is cached, so the NS is not contacted
+        # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
+        self.checkCustom('nsip.delegated.example.', 'A', dns.rrset.from_text('nsip.delegated.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1'))