From: Remi Gacogne Date: Mon, 11 Feb 2019 15:16:29 +0000 (+0100) Subject: rec: Add an option to not override custom RPZ types with the default policy X-Git-Tag: auth-4.2.0-beta1~14^2~1 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=d122dac077b1acdd0e42b62b4107d31c6cc360f6;p=thirdparty%2Fpdns.git rec: Add an option to not override custom RPZ types with the default policy --- diff --git a/pdns/filterpo.cc b/pdns/filterpo.cc index dc7fc5e375..c3585ce031 100644 --- a/pdns/filterpo.cc +++ b/pdns/filterpo.cc @@ -210,18 +210,18 @@ void DNSFilterEngine::Zone::addResponseTrigger(const Netmask& nm, Policy&& pol) d_postpolAddr.insert(nm).second=std::move(pol); } -void DNSFilterEngine::Zone::addQNameTrigger(const DNSName& n, Policy&& pol) +void DNSFilterEngine::Zone::addQNameTrigger(const DNSName& n, Policy&& pol, bool ignoreDuplicate) { auto it = d_qpolName.find(n); if (it != d_qpolName.end()) { auto& existingPol = it->second; - if (pol.d_kind != PolicyKind::Custom) { + if (pol.d_kind != PolicyKind::Custom && !ignoreDuplicate) { throw std::runtime_error("Adding a QName-based filter policy of kind " + getKindToString(pol.d_kind) + " but a policy of kind " + getKindToString(existingPol.d_kind) + " already exists for the following QName: " + n.toLogString()); } - if (existingPol.d_kind != PolicyKind::Custom) { + if (existingPol.d_kind != PolicyKind::Custom && ignoreDuplicate) { throw std::runtime_error("Adding a QName-based filter policy of kind " + getKindToString(existingPol.d_kind) + " but there was already an existing policy for the following QName: " + n.toLogString()); } diff --git a/pdns/filterpo.hh b/pdns/filterpo.hh index 22900cb37d..3d5aea5d86 100644 --- a/pdns/filterpo.hh +++ b/pdns/filterpo.hh @@ -158,7 +158,7 @@ public: void dump(FILE * fp) const; void addClientTrigger(const Netmask& nm, Policy&& pol); - void addQNameTrigger(const DNSName& nm, Policy&& pol); + void addQNameTrigger(const DNSName& nm, Policy&& pol, bool ignoreDuplicate=false); void addNSTrigger(const DNSName& dn, Policy&& pol); void addNSIPTrigger(const Netmask& nm, Policy&& pol); void addResponseTrigger(const Netmask& nm, Policy&& pol); diff --git a/pdns/rec-lua-conf.cc b/pdns/rec-lua-conf.cc index a94c992a2f..ea1d56ed6c 100644 --- a/pdns/rec-lua-conf.cc +++ b/pdns/rec-lua-conf.cc @@ -51,31 +51,36 @@ typename C::value_type::second_type constGet(const C& c, const std::string& name return iter->second; } +typedef std::unordered_map > rpzOptions_t; -static void parseRPZParameters(const std::unordered_map >& have, std::string& polName, boost::optional& defpol, uint32_t& maxTTL, size_t& zoneSizeHint) +static void parseRPZParameters(rpzOptions_t& have, std::string& polName, boost::optional& defpol, bool& defpolOverrideLocal, uint32_t& maxTTL, size_t& zoneSizeHint) { if(have.count("policyName")) { - polName = boost::get(constGet(have, "policyName")); + polName = boost::get(have["policyName"]); } if(have.count("defpol")) { defpol=DNSFilterEngine::Policy(); - defpol->d_kind = (DNSFilterEngine::PolicyKind)boost::get(constGet(have, "defpol")); + defpol->d_kind = (DNSFilterEngine::PolicyKind)boost::get(have["defpol"]); defpol->d_name = std::make_shared(polName); if(defpol->d_kind == DNSFilterEngine::PolicyKind::Custom) { defpol->d_custom.push_back(DNSRecordContent::mastermake(QType::CNAME, QClass::IN, - boost::get(constGet(have,"defcontent")))); + boost::get(have["defcontent"]))); if(have.count("defttl")) - defpol->d_ttl = static_cast(boost::get(constGet(have, "defttl"))); + defpol->d_ttl = static_cast(boost::get(have["defttl"])); else defpol->d_ttl = -1; // get it from the zone } + + if (have.count("defpolOverrideLocalData")) { + defpolOverrideLocal = boost::get(have["defpolOverrideLocalData"]); + } } if(have.count("maxTTL")) { - maxTTL = boost::get(constGet(have, "maxTTL")); + maxTTL = boost::get(have["maxTTL"]); } if(have.count("zoneSizeHint")) { - zoneSizeHint = static_cast(boost::get(constGet(have, "zoneSizeHint"))); + zoneSizeHint = static_cast(boost::get(have["zoneSizeHint"])); } } @@ -186,23 +191,24 @@ void loadRecursorLuaConfig(const std::string& fname, luaConfigDelayedThreads& de }; Lua.writeVariable("Policy", pmap); - Lua.writeFunction("rpzFile", [&lci](const string& filename, const boost::optional>>& options) { + Lua.writeFunction("rpzFile", [&lci](const string& filename, boost::optional options) { try { boost::optional defpol; + bool defpolOverrideLocal = true; std::string polName("rpzFile"); std::shared_ptr zone = std::make_shared(); uint32_t maxTTL = std::numeric_limits::max(); if(options) { auto& have = *options; size_t zoneSizeHint = 0; - parseRPZParameters(have, polName, defpol, maxTTL, zoneSizeHint); + parseRPZParameters(have, polName, defpol, defpolOverrideLocal, maxTTL, zoneSizeHint); if (zoneSizeHint > 0) { zone->reserve(zoneSizeHint); } } g_log<setName(polName); - loadRPZFromFile(filename, zone, defpol, maxTTL); + loadRPZFromFile(filename, zone, defpol, defpolOverrideLocal, maxTTL); lci.dfe.addZone(zone); g_log< > >& masters_, const string& zoneName, const boost::optional>>& options) { + Lua.writeFunction("rpzMaster", [&lci, &delayedThreads](const boost::variant > >& masters_, const string& zoneName, boost::optional options) { boost::optional defpol; + bool defpolOverrideLocal = true; std::shared_ptr zone = std::make_shared(); TSIGTriplet tt; uint32_t refresh=0; @@ -242,40 +249,40 @@ void loadRecursorLuaConfig(const std::string& fname, luaConfigDelayedThreads& de if (options) { auto& have = *options; size_t zoneSizeHint = 0; - parseRPZParameters(have, polName, defpol, maxTTL, zoneSizeHint); + parseRPZParameters(have, polName, defpol, defpolOverrideLocal, maxTTL, zoneSizeHint); if (zoneSizeHint > 0) { zone->reserve(zoneSizeHint); } if(have.count("tsigname")) { - tt.name=DNSName(toLower(boost::get(constGet(have, "tsigname")))); - tt.algo=DNSName(toLower(boost::get(constGet(have, "tsigalgo")))); - if(B64Decode(boost::get(constGet(have, "tsigsecret")), tt.secret)) + tt.name=DNSName(toLower(boost::get(have["tsigname"]))); + tt.algo=DNSName(toLower(boost::get(have[ "tsigalgo"]))); + if(B64Decode(boost::get(have[ "tsigsecret"]), tt.secret)) throw std::runtime_error("TSIG secret is not valid Base-64 encoded"); } if(have.count("refresh")) { - refresh = boost::get(constGet(have,"refresh")); + refresh = boost::get(have["refresh"]); } if(have.count("maxReceivedMBytes")) { - maxReceivedXFRMBytes = static_cast(boost::get(constGet(have,"maxReceivedMBytes"))); + maxReceivedXFRMBytes = static_cast(boost::get(have["maxReceivedMBytes"])); } if(have.count("localAddress")) { - localAddress = ComboAddress(boost::get(constGet(have,"localAddress"))); + localAddress = ComboAddress(boost::get(have["localAddress"])); } if(have.count("axfrTimeout")) { - axfrTimeout = static_cast(boost::get(constGet(have, "axfrTimeout"))); + axfrTimeout = static_cast(boost::get(have["axfrTimeout"])); } if(have.count("seedFile")) { - seedFile = boost::get(constGet(have, "seedFile")); + seedFile = boost::get(have["seedFile"]); } if(have.count("dumpFile")) { - dumpFile = boost::get(constGet(have, "dumpFile")); + dumpFile = boost::get(have["dumpFile"]); } } @@ -297,7 +304,7 @@ void loadRecursorLuaConfig(const std::string& fname, luaConfigDelayedThreads& de if (!seedFile.empty()) { g_log<getDomain() != domain) { throw PDNSException("The RPZ zone " + zoneName + " loaded from the seed file (" + zone->getDomain().toString() + ") does not match the one passed in parameter (" + domain.toString() + ")"); @@ -321,7 +328,7 @@ void loadRecursorLuaConfig(const std::string& fname, luaConfigDelayedThreads& de exit(1); // FIXME proper exit code? } - delayedThreads.rpzMasterThreads.push_back(std::make_tuple(masters, defpol, maxTTL, zoneIdx, tt, maxReceivedXFRMBytes, localAddress, axfrTimeout, sr, dumpFile)); + delayedThreads.rpzMasterThreads.push_back(std::make_tuple(masters, defpol, defpolOverrideLocal, maxTTL, zoneIdx, tt, maxReceivedXFRMBytes, localAddress, axfrTimeout, sr, dumpFile)); }); typedef vector > > > > argvec_t; @@ -518,7 +525,7 @@ void startLuaConfigDelayedThreads(const luaConfigDelayedThreads& delayedThreads, { for (const auto& rpzMaster : delayedThreads.rpzMasterThreads) { try { - std::thread t(RPZIXFRTracker, std::get<0>(rpzMaster), std::get<1>(rpzMaster), std::get<2>(rpzMaster), std::get<3>(rpzMaster), std::get<4>(rpzMaster), std::get<5>(rpzMaster) * 1024 * 1024, std::get<6>(rpzMaster), std::get<7>(rpzMaster), std::get<8>(rpzMaster), std::get<9>(rpzMaster), generation); + std::thread t(RPZIXFRTracker, std::get<0>(rpzMaster), std::get<1>(rpzMaster), std::get<2>(rpzMaster), std::get<3>(rpzMaster), std::get<4>(rpzMaster), std::get<5>(rpzMaster), std::get<6>(rpzMaster) * 1024 * 1024, std::get<7>(rpzMaster), std::get<8>(rpzMaster), std::get<9>(rpzMaster), std::get<10>(rpzMaster), generation); t.detach(); } catch(const std::exception& e) { diff --git a/pdns/rec-lua-conf.hh b/pdns/rec-lua-conf.hh index 6ce1430768..4323bd0c6c 100644 --- a/pdns/rec-lua-conf.hh +++ b/pdns/rec-lua-conf.hh @@ -69,7 +69,7 @@ extern GlobalStateHolder g_luaconfs; struct luaConfigDelayedThreads { - std::vector, boost::optional, uint32_t, size_t, TSIGTriplet, size_t, ComboAddress, uint16_t, std::shared_ptr, std::string> > rpzMasterThreads; + std::vector, boost::optional, bool, uint32_t, size_t, TSIGTriplet, size_t, ComboAddress, uint16_t, std::shared_ptr, std::string> > rpzMasterThreads; }; void loadRecursorLuaConfig(const std::string& fname, luaConfigDelayedThreads& delayedThreads); diff --git a/pdns/recursordist/docs/lua-config/rpz.rst b/pdns/recursordist/docs/lua-config/rpz.rst index 6f6d832a94..eba8cb8f0c 100644 --- a/pdns/recursordist/docs/lua-config/rpz.rst +++ b/pdns/recursordist/docs/lua-config/rpz.rst @@ -18,16 +18,31 @@ An RPZ can be loaded from file or slaved from a master. To load from file, use f .. code-block:: Lua - rpzFile("dblfilename", {defpol=Policy.Custom, defcontent="badserver.example.com"}) + rpzFile("dblfilename") To slave from a master and start IXFR to get updates, use for example: .. code-block:: Lua - rpzMaster("192.0.2.4", "policy.rpz", {defpol=Policy.Drop}) + rpzMaster("192.0.2.4", "policy.rpz") In this example, 'policy.rpz' denotes the name of the zone to query for. +The action to be taken on a match is defined by the zone itself, but in some cases it might be interesting to be able to override it, and always apply the same action +regardless of the one specified in the RPZ zone. To load from file and override the default action with a custom CNAME to badserver.example.com., use for example: + +.. code-block:: Lua + + rpzFile("dblfilename", {defpol=Policy.Custom, defcontent="badserver.example.com"}) + +To instead drop all queries matching a rule, while slaving from a master: + +.. code-block:: Lua + + rpzMaster("192.0.2.4", "policy.rpz", {defpol=Policy.Drop}) + +Note that since 4.2.0, it is possible for the override policy specified via 'defpol' to no longer applied to local data entries present in the zone by setting the 'defpolOverrideLocalData' parameter to false. + As of version 4.2.0, the first parameter of :func:`rpzMaster` can be a list of addresses for failover: rpzMaster({"192.0.2.4","192.0.2.5:5301"}, "policy.rpz", {defpol=Policy.Drop}) @@ -61,13 +76,20 @@ RPZ settings These options can be set in the ``settings`` of both :func:`rpzMaster` and :func:`rpzFile`. +defcontent +^^^^^^^^^^ +CNAME field to return in case of defpol=Policy.Custom + defpol ^^^^^^ Default policy: `Policy.Custom`_, `Policy.Drop`_, `Policy.NXDOMAIN`_, `Policy.NODATA`_, `Policy.Truncate`_, `Policy.NoAction`_. -defcontent -^^^^^^^^^^ -CNAME field to return in case of defpol=Policy.Custom +defpolOverrideLocalData +^^^^^^^^^^^^^^^^^^^^^^^ +.. versionadded:: 4.2.0 + Before 4.2.0 local data entries are always overridden by the default policy. + +Whether local data entries should be overridden by the default policy. Default is true. defttl ^^^^^^ diff --git a/pdns/rpzloader.cc b/pdns/rpzloader.cc index 8dc17311c5..116cc9f300 100644 --- a/pdns/rpzloader.cc +++ b/pdns/rpzloader.cc @@ -60,7 +60,7 @@ static Netmask makeNetmaskFromRPZ(const DNSName& name) return Netmask(v6); } -void RPZRecordToPolicy(const DNSRecord& dr, std::shared_ptr zone, bool addOrRemove, boost::optional defpol, uint32_t maxTTL) +static void RPZRecordToPolicy(const DNSRecord& dr, std::shared_ptr zone, bool addOrRemove, boost::optional defpol, bool defpolOverrideLocal, uint32_t maxTTL) { static const DNSName drop("rpz-drop."), truncate("rpz-tcp-only."), noaction("rpz-passthru."); static const DNSName rpzClientIP("rpz-client-ip"), rpzIP("rpz-ip"), @@ -68,6 +68,7 @@ void RPZRecordToPolicy(const DNSRecord& dr, std::shared_ptrgetTarget(); if(defpol) { pol=*defpol; + defpolApplied = true; } else if(crcTarget.isRoot()) { // cerr<<"Wants NXDOMAIN for "<d_ttl < 0) { + if (!defpolApplied || defpol->d_ttl < 0) { pol.d_ttl = static_cast(std::min(maxTTL, dr.d_ttl)); } else { pol.d_ttl = static_cast(std::min(maxTTL, static_cast(pol.d_ttl))); @@ -169,14 +172,19 @@ void RPZRecordToPolicy(const DNSRecord& dr, std::shared_ptrrmNSIPTrigger(nm, std::move(pol)); } else { - if(addOrRemove) - zone->addQNameTrigger(dr.d_name, std::move(pol)); - else + if(addOrRemove) { + /* if we did override the existing policy with the default policy, + we might turn two A or AAAA into a CNAME, which would trigger + an exception. Let's just ignore it. */ + zone->addQNameTrigger(dr.d_name, std::move(pol), defpolApplied); + } + else { zone->rmQNameTrigger(dr.d_name, std::move(pol)); + } } } -static shared_ptr loadRPZFromServer(const ComboAddress& master, const DNSName& zoneName, std::shared_ptr zone, boost::optional defpol, uint32_t maxTTL, const TSIGTriplet& tt, size_t maxReceivedBytes, const ComboAddress& localAddress, uint16_t axfrTimeout) +static shared_ptr loadRPZFromServer(const ComboAddress& master, const DNSName& zoneName, std::shared_ptr zone, boost::optional defpol, bool defpolOverrideLocal, uint32_t maxTTL, const TSIGTriplet& tt, size_t maxReceivedBytes, const ComboAddress& localAddress, uint16_t axfrTimeout) { g_log< loadRPZFromServer(const ComboAddress& master continue; } - RPZRecordToPolicy(dr, zone, true, defpol, maxTTL); + RPZRecordToPolicy(dr, zone, true, defpol, defpolOverrideLocal, maxTTL); nrecords++; } axfrNow = time(nullptr); @@ -223,7 +231,7 @@ static shared_ptr loadRPZFromServer(const ComboAddress& master } // this function is silent - you do the logging -std::shared_ptr loadRPZFromFile(const std::string& fname, std::shared_ptr zone, boost::optional defpol, uint32_t maxTTL) +std::shared_ptr loadRPZFromFile(const std::string& fname, std::shared_ptr zone, boost::optional defpol, bool defpolOverrideLocal, uint32_t maxTTL) { shared_ptr sr = nullptr; ZoneParserTNG zpt(fname); @@ -244,7 +252,7 @@ std::shared_ptr loadRPZFromFile(const std::string& fname, std: } else { dr.d_name=dr.d_name.makeRelative(domain); - RPZRecordToPolicy(dr, zone, true, defpol, maxTTL); + RPZRecordToPolicy(dr, zone, true, defpol, defpolOverrideLocal, maxTTL); } } catch(const PDNSException& pe) { @@ -338,7 +346,7 @@ static bool dumpZoneToDisk(const DNSName& zoneName, const std::shared_ptr& masters, boost::optional defpol, uint32_t maxTTL, size_t zoneIdx, const TSIGTriplet& tt, size_t maxReceivedBytes, const ComboAddress& localAddress, const uint16_t axfrTimeout, std::shared_ptr sr, std::string dumpZoneFileName, uint64_t configGeneration) +void RPZIXFRTracker(const std::vector& masters, boost::optional defpol, bool defpolOverrideLocal, uint32_t maxTTL, size_t zoneIdx, const TSIGTriplet& tt, size_t maxReceivedBytes, const ComboAddress& localAddress, const uint16_t axfrTimeout, std::shared_ptr sr, std::string dumpZoneFileName, uint64_t configGeneration) { setThreadName("pdns-r/RPZIXFR"); bool isPreloaded = sr != nullptr; @@ -360,7 +368,7 @@ void RPZIXFRTracker(const std::vector& masters, boost::optional newZone = std::make_shared(*oldZone); for (const auto& master : masters) { try { - sr = loadRPZFromServer(master, zoneName, newZone, defpol, maxTTL, tt, maxReceivedBytes, localAddress, axfrTimeout); + sr = loadRPZFromServer(master, zoneName, newZone, defpol, defpolOverrideLocal, maxTTL, tt, maxReceivedBytes, localAddress, axfrTimeout); if(refresh == 0) { refresh = sr->d_st.refresh; } @@ -473,7 +481,7 @@ void RPZIXFRTracker(const std::vector& masters, boost::optional& masters, boost::optional loadRPZFromFile(const std::string& fname, std::shared_ptr zone, boost::optional defpol, uint32_t maxTTL); -void RPZRecordToPolicy(const DNSRecord& dr, std::shared_ptr zone, bool addOrRemove, boost::optional defpol, uint32_t maxTTL); -void RPZIXFRTracker(const std::vector& masters, boost::optional defpol, uint32_t maxTTL, size_t zoneIdx, const TSIGTriplet& tt, size_t maxReceivedBytes, const ComboAddress& localAddress, const uint16_t axfrTimeout, shared_ptr sr, std::string dumpZoneFileName, uint64_t configGeneration); +std::shared_ptr loadRPZFromFile(const std::string& fname, std::shared_ptr zone, boost::optional defpol, bool defpolOverrideLocal, uint32_t maxTTL); +void RPZIXFRTracker(const std::vector& masters, boost::optional defpol, bool defpolOverrideLocal, uint32_t maxTTL, size_t zoneIdx, const TSIGTriplet& tt, size_t maxReceivedBytes, const ComboAddress& localAddress, const uint16_t axfrTimeout, shared_ptr sr, std::string dumpZoneFileName, uint64_t configGeneration); struct rpzStats { diff --git a/regression-tests.recursor-dnssec/test_RPZ.py b/regression-tests.recursor-dnssec/test_RPZ.py index 39f8e410c4..0ed6e08ba0 100644 --- a/regression-tests.recursor-dnssec/test_RPZ.py +++ b/regression-tests.recursor-dnssec/test_RPZ.py @@ -176,19 +176,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' @@ -213,21 +201,6 @@ 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) @classmethod def setUpClass(cls): @@ -283,6 +256,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): + 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 +291,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 +370,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 +435,192 @@ e 3600 IN A 192.0.2.42 # check non-custom policies self.checkTruncated('tc.example.') self.checkDropped('drop.example.') + +class RPZFileRecursorTest(RPZRecursorTest): + """ + This test makes sure that we correctly load RPZ zones from a file + """ + + _confdir = 'RPZFile' + _wsPort = 8042 + _wsTimeout = 2 + _wsPassword = 'secretpassword' + _apiKey = 'secretapikey' + _lua_config_file = """ + rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." }) + """ % (_confdir) + _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) + + @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' + _wsPort = 8042 + _wsTimeout = 2 + _wsPassword = 'secretpassword' + _apiKey = 'secretapikey' + _lua_config_file = """ + rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction }) + """ % (_confdir) + _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) + + @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' + _wsPort = 8042 + _wsTimeout = 2 + _wsPassword = 'secretpassword' + _apiKey = 'secretapikey' + _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 +webserver=yes +webserver-port=%d +webserver-address=127.0.0.1 +webserver-password=%s +api-key=%s +""" % (_confdir, _wsPort, _wsPassword, _apiKey) + + @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 polic + 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.')