From 55baa1f2ccf02267949efc8e96e96c55043f573e Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Mon, 6 Jun 2016 10:34:37 +0200 Subject: [PATCH] dnsdist: Filter on opcode, records count/type, trailing data * Add `OpcodeRule()` to filter on opcode + DNSOpcode.* Lua values * Add `TrailingDataRule()` to filter queries with trailing data * Add `RecordsCountRule(section, minCount, maxCount)` to match on the number of records in a given section * Add `RecordsTypeCountRule(section, type, minCount, maxCount)` to match on the number of records of type `type` in a given section * Add DNSSection.* Lua values * Add DNSClass.* Lua values --- pdns/README-dnsdist.md | 14 + pdns/dnsdist-lua.cc | 58 +++- pdns/dnsdistconf.lua | 14 +- pdns/dnsparser.cc | 113 +++++++ pdns/dnsparser.hh | 2 + pdns/dnsrulactions.hh | 139 +++++++++ regression-tests.dnsdist/dnsdisttests.py | 24 +- regression-tests.dnsdist/test_Advanced.py | 72 ++++- regression-tests.dnsdist/test_Basics.py | 4 +- regression-tests.dnsdist/test_Carbon.py | 3 +- regression-tests.dnsdist/test_CheckConfig.py | 5 +- .../test_EdnsClientSubnet.py | 1 - regression-tests.dnsdist/test_RecordsCount.py | 286 ++++++++++++++++++ regression-tests.dnsdist/test_Trailing.py | 73 +++++ 14 files changed, 770 insertions(+), 38 deletions(-) create mode 100644 regression-tests.dnsdist/test_RecordsCount.py create mode 100644 regression-tests.dnsdist/test_Trailing.py diff --git a/pdns/README-dnsdist.md b/pdns/README-dnsdist.md index 110d35662..83cc56d17 100644 --- a/pdns/README-dnsdist.md +++ b/pdns/README-dnsdist.md @@ -332,6 +332,10 @@ Rules have selectors and actions. Current selectors are: * RE2Rule on query name (optional) * Packet requests DNSSEC processing * Query received over UDP or TCP + * Opcode (OpcodeRule) + * Number of entries in a given section (RecordsCountRule) + * Number of entries of a specific type in a given section (RecordsTypeCountRule) + * Presence of trailing data (TrailingDataRule) Special rules are: @@ -383,13 +387,17 @@ A DNS rule can be: * a MaxQPSRule * a NetmaskGroupRule * a NotRule + * an OpcodeRule * an OrRule * a QClassRule * a QTypeRule * a RegexRule * a RE2Rule + * a RecordsCountRule + * a RecordsTypeCountRule * a SuffixMatchNodeRule * a TCPRule + * a TrailingDataRule Some specific actions do not stop the processing when they match, contrary to all other actions: @@ -1072,11 +1080,16 @@ instantiate a server with additional parameters * `NetmaskGroupRule()`: matches traffic from the specified network range * `NotRule()`: matches if the sub-rule does not match * `OrRule()`: matches if at least one of the sub-rules matches + * `OpcodeRule()`: matches queries with the specified opcode * `QClassRule(qclass)`: matches queries with the specified qclass (numeric) * `QTypeRule(qtype)`: matches queries with the specified qtype * `RegexRule(regex)`: matches the query name against the supplied regex + * `RecordsCountRule(section, minCount, maxCount)`: matches if there is at least `minCount` and at most `maxCount` records in the `section` section + * `RecordsTypeCountRule(section, type, minCount, maxCount)`: matches if there is at least `minCount` and at most `maxCount` records of type `type` in the `section` section + * `RE2Rule(regex)`: matches the query name against the supplied regex using the RE2 engine * `SuffixMatchNodeRule(smn, [quiet-bool])`: matches based on a group of domain suffixes for rapid testing of membership. Pass `true` as second parameter to prevent listing of all domains matched. * `TCPRule(tcp)`: matches question received over TCP if `tcp` is true, over UDP otherwise + * `TrailingDataRule()`: matches if the query has trailing data * Rule management related: * `getAction(num)`: returns the Action associate with rule 'num'. * `showRules()`: show all defined rules (Pool, Block, QPS, addAnyTCRule) @@ -1192,6 +1205,7 @@ instantiate a server with additional parameters * member `dh`: DNSHeader * member `len`: the question length * member `localaddr`: ComboAddress of the local bind this question was received on + * member `opcode`: the question opcode * member `qname`: DNSName of this question * member `qclass`: QClass (as an unsigned integer) of this question * member `qtype`: QType (as an unsigned integer) of this question diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index 615491815..45b0ef071 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -138,13 +138,49 @@ vector> setupLua(bool client, const std::string& confi {"Delay", (int)DNSAction::Action::Delay}} ); - g_lua.writeVariable("DNSResponseAction", std::unordered_map{ + g_lua.writeVariable("DNSResponseAction", std::unordered_map{ {"None",(int)DNSResponseAction::Action::None}} ); + g_lua.writeVariable("DNSClass", std::unordered_map{ + {"IN", QClass::IN }, + {"CHAOS", QClass::CHAOS }, + {"NONE", QClass::NONE }, + {"ANY", QClass::ANY } + }); + + g_lua.writeVariable("DNSOpcode", std::unordered_map{ + {"Query", Opcode::Query }, + {"IQuery", Opcode::IQuery }, + {"Status", Opcode::Status }, + {"Notify", Opcode::Notify }, + {"Update", Opcode::Update } + }); + + g_lua.writeVariable("DNSSection", std::unordered_map{ + {"Question", 0 }, + {"Answer", 1 }, + {"Authority", 2 }, + {"Additional",3 } + }); + + vector > rcodes = {{"NOERROR", RCode::NoError }, + {"FORMERR", RCode::FormErr }, + {"SERVFAIL", RCode::ServFail }, + {"NXDOMAIN", RCode::NXDomain }, + {"NOTIMP", RCode::NotImp }, + {"REFUSED", RCode::Refused }, + {"YXDOMAIN", RCode::YXDomain }, + {"YXRRSET", RCode::YXRRSet }, + {"NXRRSET", RCode::NXRRSet }, + {"NOTAUTH", RCode::NotAuth }, + {"NOTZONE", RCode::NotZone } + }; vector > dd; for(const auto& n : QType::names) dd.push_back({n.first, n.second}); + for(const auto& n : rcodes) + dd.push_back({n.first, n.second}); g_lua.writeVariable("dnsdist", dd); g_lua.writeFunction("newServer", @@ -778,10 +814,13 @@ vector> setupLua(bool client, const std::string& confi } return std::shared_ptr(new QTypeRule(qtype)); }); - g_lua.writeFunction("QClassRule", [](int c) { + g_lua.writeFunction("QClassRule", [](int c) { return std::shared_ptr(new QClassRule(c)); }); + g_lua.writeFunction("OpcodeRule", [](uint8_t code) { + return std::shared_ptr(new OpcodeRule(code)); + }); g_lua.writeFunction("AndRule", [](vector > >a) { return std::shared_ptr(new AndRule(a)); @@ -803,7 +842,19 @@ vector> setupLua(bool client, const std::string& confi return std::shared_ptr(new NotRule(rule)); }); - g_lua.writeFunction("addAction", [](luadnsrule_t var, std::shared_ptr ea) + g_lua.writeFunction("RecordsCountRule", [](uint8_t section, uint16_t minCount, uint16_t maxCount) { + return std::shared_ptr(new RecordsCountRule(section, minCount, maxCount)); + }); + + g_lua.writeFunction("RecordsTypeCountRule", [](uint8_t section, uint16_t type, uint16_t minCount, uint16_t maxCount) { + return std::shared_ptr(new RecordsTypeCountRule(section, type, minCount, maxCount)); + }); + + g_lua.writeFunction("TrailingDataRule", []() { + return std::shared_ptr(new TrailingDataRule()); + }); + + g_lua.writeFunction("addAction", [](luadnsrule_t var, std::shared_ptr ea) { setLuaSideEffect(); auto rule=makeRule(var); @@ -1310,6 +1361,7 @@ vector> setupLua(bool client, const std::string& confi /* DNSDist DNSQuestion */ g_lua.registerMember("dh", &DNSQuestion::dh); g_lua.registerMember("len", [](const DNSQuestion& dq) -> uint16_t { return dq.len; }, [](DNSQuestion& dq, uint16_t newlen) { dq.len = newlen; }); + g_lua.registerMember("opcode", [](const DNSQuestion& dq) -> uint8_t { return dq.dh->opcode; }, [](DNSQuestion& dq, uint8_t newOpcode) { (void) newOpcode; }); g_lua.registerMember("size", [](const DNSQuestion& dq) -> size_t { return dq.size; }, [](DNSQuestion& dq, size_t newSize) { (void) newSize; }); g_lua.registerMember("tcp", [](const DNSQuestion& dq) -> bool { return dq.tcp; }, [](DNSQuestion& dq, bool newTcp) { (void) newTcp; }); g_lua.registerMember("skipCache", [](const DNSQuestion& dq) -> bool { return dq.skipCache; }, [](DNSQuestion& dq, bool newSkipCache) { dq.skipCache = newSkipCache; }); diff --git a/pdns/dnsdistconf.lua b/pdns/dnsdistconf.lua index be86a0477..5b9bedd01 100644 --- a/pdns/dnsdistconf.lua +++ b/pdns/dnsdistconf.lua @@ -1,4 +1,3 @@ - -- listen for console connection with the given secret key controlSocket("0.0.0.0") setKey("MXNeLFWHUe4363BBKrY06cAsH8NWNb+Se2eXU5+Bb74=") @@ -167,10 +166,10 @@ end -- addAction(AndRule({QTypeRule("ANY"), TCPRule(true)}), TCAction()) -- return 'not implemented' for qtype != A over UDP --- addAction(AndRule({NotRule(QTypeRule("A")), TCPRule(false)}), RCodeAction(4)) +-- addAction(AndRule({NotRule(QTypeRule("A")), TCPRule(false)}), RCodeAction(dnsdist.NOTIMPL)) -- return 'not implemented' for qtype == A OR received over UDP --- addAction(OrRule({QTypeRule("A"), TCPRule(false)}), RCodeAction(4)) +-- addAction(OrRule({QTypeRule("A"), TCPRule(false)}), RCodeAction(dnsdist.NOTIMPL)) -- log all queries to a 'dndist.log' file, in text-mode (not binary) -- addAction(AllRule(), LogAction("dnsdist.log", false)) @@ -180,9 +179,16 @@ end -- drop all queries for the CHAOS class -- addAction(QClassRule(3), DropAction()) +-- addAction(QClassRule(DNSClass.CHAOS), DropAction()) + +-- drop all queries with the UPDATE opcode +-- addAction(OpcodeRule(DNSOpcode.Update), DropAction()) + +-- refuse all queries not having exactly one question +-- addAction(NotRule(RecordsCountRule(DNSSection.Question, 1, 1)), RCodeAction(dnsdist.REFUSED)) -- return 'refused' for domains matching the regex evil[0-9]{4,}.powerdns.com$ --- addAction(RegexRule("evil[0-9]{4,}\\.powerdns\\.com$"), RCodeAction(5)) +-- addAction(RegexRule("evil[0-9]{4,}\\.powerdns\\.com$"), RCodeAction(dnsdist.REFUSED)) -- spoof responses for A, AAAA and ANY for spoof.powerdns.com. -- A queries will get 192.0.2.1, AAAA 2001:DB8::1 and ANY both diff --git a/pdns/dnsparser.cc b/pdns/dnsparser.cc index 493d82e88..e47721d70 100644 --- a/pdns/dnsparser.cc +++ b/pdns/dnsparser.cc @@ -617,6 +617,10 @@ public: tmp = htonl(tmp); memcpy(d_packet + d_offset-4, (const char*)&tmp, sizeof(tmp)); } + uint32_t getOffset() const + { + return d_offset; + } private: void moveOffset(uint16_t by) { @@ -713,3 +717,112 @@ uint32_t getDNSPacketMinTTL(const char* packet, size_t length) } return result; } + +uint32_t getDNSPacketLength(const char* packet, size_t length) +{ + uint32_t result = length; + if(length < sizeof(dnsheader)) { + return result; + } + try + { + const dnsheader* dh = (const dnsheader*) packet; + DNSPacketMangler dpm(const_cast(packet), length); + + const uint16_t qdcount = ntohs(dh->qdcount); + for(size_t n = 0; n < qdcount; ++n) { + dpm.skipLabel(); + dpm.skipBytes(4); // qtype, qclass + } + const size_t numrecords = ntohs(dh->ancount) + ntohs(dh->nscount) + ntohs(dh->arcount); + for(size_t n = 0; n < numrecords; ++n) { + dpm.skipLabel(); + + /* const uint16_t dnstype */ dpm.get16BitInt(); + /* uint16_t dnsclass */ dpm.get16BitInt(); + /* const uint32_t ttl */ dpm.get32BitInt(); + dpm.skipRData(); + } + result = dpm.getOffset(); + } + catch(...) + { + } + return result; +} + +uint16_t getRecordsOfTypeCount(const char* packet, size_t length, uint8_t section, uint16_t type) +{ + uint16_t result = 0; + if(length < sizeof(dnsheader)) { + return result; + } + try + { + const dnsheader* dh = (const dnsheader*) packet; + DNSPacketMangler dpm(const_cast(packet), length); + + const uint16_t qdcount = ntohs(dh->qdcount); + for(size_t n = 0; n < qdcount; ++n) { + dpm.skipLabel(); + if (section == 0) { + uint16_t dnstype = dpm.get16BitInt(); + if (dnstype == type) { + result++; + } + dpm.skipBytes(2); // qclass + } else { + dpm.skipBytes(4); // qtype, qclass + } + } + const uint16_t ancount = ntohs(dh->ancount); + for(size_t n = 0; n < ancount; ++n) { + dpm.skipLabel(); + if (section == 1) { + uint16_t dnstype = dpm.get16BitInt(); + if (dnstype == type) { + result++; + } + dpm.skipBytes(2); // qclass + } else { + dpm.skipBytes(4); // qtype, qclass + } + /* const uint32_t ttl */ dpm.get32BitInt(); + dpm.skipRData(); + } + const uint16_t nscount = ntohs(dh->nscount); + for(size_t n = 0; n < nscount; ++n) { + dpm.skipLabel(); + if (section == 2) { + uint16_t dnstype = dpm.get16BitInt(); + if (dnstype == type) { + result++; + } + dpm.skipBytes(2); // qclass + } else { + dpm.skipBytes(4); // qtype, qclass + } + /* const uint32_t ttl */ dpm.get32BitInt(); + dpm.skipRData(); + } + const uint16_t arcount = ntohs(dh->arcount); + for(size_t n = 0; n < arcount; ++n) { + dpm.skipLabel(); + if (section == 3) { + uint16_t dnstype = dpm.get16BitInt(); + if (dnstype == type) { + result++; + } + dpm.skipBytes(2); // qclass + } else { + dpm.skipBytes(4); // qtype, qclass + } + /* const uint32_t ttl */ dpm.get32BitInt(); + dpm.skipRData(); + } + } + catch(...) + { + } + return result; +} diff --git a/pdns/dnsparser.hh b/pdns/dnsparser.hh index c54b456f9..05117bf5d 100644 --- a/pdns/dnsparser.hh +++ b/pdns/dnsparser.hh @@ -381,6 +381,8 @@ string simpleCompress(const string& label, const string& root=""); void ageDNSPacket(char* packet, size_t length, uint32_t seconds); void ageDNSPacket(std::string& packet, uint32_t seconds); uint32_t getDNSPacketMinTTL(const char* packet, size_t length); +uint32_t getDNSPacketLength(const char* packet, size_t length); +uint16_t getRecordsOfTypeCount(const char* packet, size_t length, uint8_t section, uint16_t type); template std::shared_ptr getRR(const DNSRecord& dr) diff --git a/pdns/dnsrulactions.hh b/pdns/dnsrulactions.hh index 5f60730d2..899e7d64b 100644 --- a/pdns/dnsrulactions.hh +++ b/pdns/dnsrulactions.hh @@ -6,6 +6,7 @@ #include "lock.hh" #include "remote_logger.hh" #include "dnsdist-protobuf.hh" +#include "dnsparser.hh" class MaxQPSIPRule : public DNSRule { @@ -310,6 +311,23 @@ private: uint16_t d_qclass; }; +class OpcodeRule : public DNSRule +{ +public: + OpcodeRule(uint8_t opcode) : d_opcode(opcode) + { + } + bool matches(const DNSQuestion* dq) const override + { + return d_opcode == dq->dh->opcode; + } + string toString() const override + { + return "opcode=="+d_opcode; + } +private: + uint8_t d_opcode; +}; class TCPRule : public DNSRule { @@ -348,6 +366,127 @@ private: shared_ptr d_rule; }; +class RecordsCountRule : public DNSRule +{ +public: + RecordsCountRule(uint8_t section, uint16_t minCount, uint16_t maxCount): d_minCount(minCount), d_maxCount(maxCount), d_section(section) + { + } + bool matches(const DNSQuestion* dq) const override + { + uint16_t count = 0; + switch(d_section) { + case 0: + count = ntohs(dq->dh->qdcount); + break; + case 1: + count = ntohs(dq->dh->ancount); + break; + case 2: + count = ntohs(dq->dh->nscount); + break; + case 3: + count = ntohs(dq->dh->arcount); + break; + } + return count >= d_minCount && count <= d_maxCount; + } + string toString() const override + { + string section; + switch(d_section) { + case 0: + section = "QD"; + break; + case 1: + section = "AN"; + break; + case 2: + section = "NS"; + break; + case 3: + section = "AR"; + break; + } + return std::to_string(d_minCount) + " <= records in " + section + " <= "+ std::to_string(d_maxCount); + } +private: + uint16_t d_minCount; + uint16_t d_maxCount; + uint8_t d_section; +}; + +class RecordsTypeCountRule : public DNSRule +{ +public: + RecordsTypeCountRule(uint8_t section, uint16_t type, uint16_t minCount, uint16_t maxCount): d_type(type), d_minCount(minCount), d_maxCount(maxCount), d_section(section) + { + } + bool matches(const DNSQuestion* dq) const override + { + uint16_t count = 0; + switch(d_section) { + case 0: + count = ntohs(dq->dh->qdcount); + break; + case 1: + count = ntohs(dq->dh->ancount); + break; + case 2: + count = ntohs(dq->dh->nscount); + break; + case 3: + count = ntohs(dq->dh->arcount); + break; + } + if (count < d_minCount || count > d_maxCount) { + return false; + } + count = getRecordsOfTypeCount(reinterpret_cast(dq->dh), dq->len, d_section, d_type); + return count >= d_minCount && count <= d_maxCount; + } + string toString() const override + { + string section; + switch(d_section) { + case 0: + section = "QD"; + break; + case 1: + section = "AN"; + break; + case 2: + section = "NS"; + break; + case 3: + section = "AR"; + break; + } + return std::to_string(d_minCount) + " <= " + QType(d_type).getName() + " records in " + section + " <= "+ std::to_string(d_maxCount); + } +private: + uint16_t d_type; + uint16_t d_minCount; + uint16_t d_maxCount; + uint8_t d_section; +}; + +class TrailingDataRule : public DNSRule +{ +public: + TrailingDataRule() + { + } + bool matches(const DNSQuestion* dq) const override + { + uint16_t length = getDNSPacketLength(reinterpret_cast(dq->dh), dq->len); + return length < dq->len; + } + string toString() const override + { + return "trailing data"; + } +}; class DropAction : public DNSAction { diff --git a/regression-tests.dnsdist/dnsdisttests.py b/regression-tests.dnsdist/dnsdisttests.py index a9ef2be97..8184eed4a 100644 --- a/regression-tests.dnsdist/dnsdisttests.py +++ b/regression-tests.dnsdist/dnsdisttests.py @@ -144,14 +144,15 @@ class DNSDistTest(unittest.TestCase): return response @classmethod - def UDPResponder(cls, port): + def UDPResponder(cls, port, ignoreTrailing=False): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sock.bind(("127.0.0.1", port)) while True: data, addr = sock.recvfrom(4096) - request = dns.message.from_wire(data) + request = dns.message.from_wire(data, ignore_trailing=ignoreTrailing) response = cls._getResponse(request) + if not response: continue @@ -161,7 +162,7 @@ class DNSDistTest(unittest.TestCase): sock.close() @classmethod - def TCPResponder(cls, port): + def TCPResponder(cls, port, ignoreTrailing=False): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) try: @@ -177,8 +178,9 @@ class DNSDistTest(unittest.TestCase): data = conn.recv(2) (datalen,) = struct.unpack("!H", data) data = conn.recv(datalen) - request = dns.message.from_wire(data) + request = dns.message.from_wire(data, ignore_trailing=ignoreTrailing) response = cls._getResponse(request) + if not response: continue @@ -189,7 +191,7 @@ class DNSDistTest(unittest.TestCase): sock.close() @classmethod - def sendUDPQuery(cls, query, response, useQueue=True, timeout=2.0): + def sendUDPQuery(cls, query, response, useQueue=True, timeout=2.0, rawQuery=False): if useQueue: cls._toResponderQueue.put(response, True, timeout) @@ -197,7 +199,9 @@ class DNSDistTest(unittest.TestCase): cls._sock.settimeout(timeout) try: - cls._sock.send(query.to_wire()) + if not rawQuery: + query = query.to_wire() + cls._sock.send(query) data = cls._sock.recv(4096) except socket.timeout: data = None @@ -214,7 +218,7 @@ class DNSDistTest(unittest.TestCase): return (receivedQuery, message) @classmethod - def sendTCPQuery(cls, query, response, useQueue=True, timeout=2.0): + def sendTCPQuery(cls, query, response, useQueue=True, timeout=2.0, rawQuery=False): if useQueue: cls._toResponderQueue.put(response, True, timeout) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -224,7 +228,11 @@ class DNSDistTest(unittest.TestCase): sock.connect(("127.0.0.1", cls._dnsDistPort)) try: - wire = query.to_wire() + if not rawQuery: + wire = query.to_wire() + else: + wire = query + sock.send(struct.pack("!H", len(wire))) sock.send(wire) data = sock.recv(2) diff --git a/regression-tests.dnsdist/test_Advanced.py b/regression-tests.dnsdist/test_Advanced.py index bd2e7849f..e2542b3bf 100644 --- a/regression-tests.dnsdist/test_Advanced.py +++ b/regression-tests.dnsdist/test_Advanced.py @@ -502,7 +502,7 @@ class TestAdvancedTruncateAnyAndTCP(DNSDistTest): class TestAdvancedAndNot(DNSDistTest): _config_template = """ - addAction(AndRule({NotRule(QTypeRule("A")), TCPRule(false)}), RCodeAction(4)) + addAction(AndRule({NotRule(QTypeRule("A")), TCPRule(false)}), RCodeAction(dnsdist.NOTIMP)) newServer{address="127.0.0.1:%s"} """ def testAOverUDPReturnsNotImplementedCanary(self): @@ -574,7 +574,7 @@ class TestAdvancedAndNot(DNSDistTest): class TestAdvancedOr(DNSDistTest): _config_template = """ - addAction(OrRule({QTypeRule("A"), TCPRule(false)}), RCodeAction(4)) + addAction(OrRule({QTypeRule("A"), TCPRule(false)}), RCodeAction(dnsdist.NOTIMP)) newServer{address="127.0.0.1:%s"} """ def testAAAAOverUDPReturnsNotImplemented(self): @@ -709,7 +709,7 @@ class TestAdvancedQClass(DNSDistTest): _config_template = """ newServer{address="127.0.0.1:%s"} - addAction(QClassRule(3), DropAction()) + addAction(QClassRule(DNSClass.CHAOS), DropAction()) """ def testAdvancedQClassChaosDrop(self): """ @@ -718,17 +718,10 @@ class TestAdvancedQClass(DNSDistTest): """ name = 'qclasschaos.advanced.tests.powerdns.com.' query = dns.message.make_query(name, 'TXT', 'CHAOS') - response = dns.message.make_response(query) - rrset = dns.rrset.from_text(name, - 3600, - dns.rdataclass.CH, - dns.rdatatype.TXT, - 'hop') - response.answer.append(rrset) - (_, receivedResponse) = self.sendUDPQuery(query, response) + (_, receivedResponse) = self.sendUDPQuery(query, response=None) self.assertEquals(receivedResponse, None) - (_, receivedResponse) = self.sendTCPQuery(query, response) + (_, receivedResponse) = self.sendTCPQuery(query, response=None) self.assertEquals(receivedResponse, None) def testAdvancedQClassINAllow(self): @@ -760,6 +753,55 @@ class TestAdvancedQClass(DNSDistTest): self.assertEquals(query, receivedQuery) self.assertEquals(response, receivedResponse) +class TestAdvancedOpcode(DNSDistTest): + + _config_template = """ + newServer{address="127.0.0.1:%s"} + addAction(OpcodeRule(DNSOpcode.Notify), DropAction()) + """ + def testAdvancedOpcodeNotifyDrop(self): + """ + Advanced: Drop Opcode NOTIFY + + """ + name = 'opcodenotify.advanced.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + query.set_opcode(dns.opcode.NOTIFY) + + (_, receivedResponse) = self.sendUDPQuery(query, response=None) + self.assertEquals(receivedResponse, None) + (_, receivedResponse) = self.sendTCPQuery(query, response=None) + self.assertEquals(receivedResponse, None) + + def testAdvancedOpcodeUpdateINAllow(self): + """ + Advanced: Allow Opcode UPDATE + + """ + name = 'opcodeupdate.advanced.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + query.set_opcode(dns.opcode.UPDATE) + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1') + response.answer.append(rrset) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) class TestAdvancedNonTerminalRule(DNSDistTest): @@ -857,7 +899,6 @@ class TestAdvancedRestoreFlagsOnSelfResponse(DNSDistTest): query = dns.message.make_query(name, 'A', 'IN') # dnsdist set RA = RD for spoofed responses query.flags &= ~dns.flags.RD - expectedQuery = dns.message.make_query(name, 'A', 'IN') response = dns.message.make_response(query) rrset = dns.rrset.from_text(name, @@ -927,7 +968,7 @@ class TestAdvancedQPSNone(DNSDistTest): _config_template = """ addQPSLimit("qpsnone.advanced.tests.powerdns.com", 100) - addAction(AllRule(), RCodeAction(5)) + addAction(AllRule(), RCodeAction(dnsdist.REFUSED)) newServer{address="127.0.0.1:%s"} """ @@ -955,7 +996,7 @@ class TestAdvancedNMGRule(DNSDistTest): _config_template = """ allowed = newNMG() allowed:addMask("192.0.2.1/32") - addAction(NotRule(NetmaskGroupRule(allowed)), RCodeAction(5)) + addAction(NotRule(NetmaskGroupRule(allowed)), RCodeAction(dnsdist.REFUSED)) newServer{address="127.0.0.1:%s"} """ @@ -976,3 +1017,4 @@ class TestAdvancedNMGRule(DNSDistTest): (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) self.assertEquals(receivedResponse, expectedResponse) + diff --git a/regression-tests.dnsdist/test_Basics.py b/regression-tests.dnsdist/test_Basics.py index 5dcc6fb79..6973dfd78 100644 --- a/regression-tests.dnsdist/test_Basics.py +++ b/regression-tests.dnsdist/test_Basics.py @@ -10,10 +10,10 @@ class TestBasics(DNSDistTest): newServer{address="127.0.0.1:%s"} truncateTC(true) addAnyTCRule() - addAction(RegexRule("evil[0-9]{4,}\\\\.regex\\\\.tests\\\\.powerdns\\\\.com$"), RCodeAction(5)) + addAction(RegexRule("evil[0-9]{4,}\\\\.regex\\\\.tests\\\\.powerdns\\\\.com$"), RCodeAction(dnsdist.REFUSED)) mySMN = newSuffixMatchNode() mySMN:add(newDNSName("nameAndQtype.tests.powerdns.com.")) - addAction(AndRule{SuffixMatchNodeRule(mySMN), QTypeRule("TXT")}, RCodeAction(4)) + addAction(AndRule{SuffixMatchNodeRule(mySMN), QTypeRule("TXT")}, RCodeAction(dnsdist.NOTIMP)) addAction(makeRule("drop.test.powerdns.com."), DropAction()) block=newDNSName("powerdns.org.") function blockFilter(dq) diff --git a/regression-tests.dnsdist/test_Carbon.py b/regression-tests.dnsdist/test_Carbon.py index c3634333d..09b13daf3 100644 --- a/regression-tests.dnsdist/test_Carbon.py +++ b/regression-tests.dnsdist/test_Carbon.py @@ -2,9 +2,8 @@ import Queue import threading import socket +import sys import time -import unittest -import dns from dnsdisttests import DNSDistTest class TestCarbon(DNSDistTest): diff --git a/regression-tests.dnsdist/test_CheckConfig.py b/regression-tests.dnsdist/test_CheckConfig.py index eaed2bc84..709a0ceaa 100644 --- a/regression-tests.dnsdist/test_CheckConfig.py +++ b/regression-tests.dnsdist/test_CheckConfig.py @@ -2,7 +2,6 @@ import unittest import os import subprocess -import sys import time class TestCheckConfig(unittest.TestCase): @@ -35,10 +34,10 @@ class TestCheckConfig(unittest.TestCase): newServer{address="127.0.0.1:53"} truncateTC(true) addAnyTCRule() - addAction(RegexRule("evil[0-9]{4,}\\\\.regex\\\\.tests\\\\.powerdns\\\\.com$"), RCodeAction(5)) + addAction(RegexRule("evil[0-9]{4,}\\\\.regex\\\\.tests\\\\.powerdns\\\\.com$"), RCodeAction(dnsdist.REFUSED)) mySMN = newSuffixMatchNode() mySMN:add(newDNSName("nameAndQtype.tests.powerdns.com.")) - addAction(AndRule{SuffixMatchNodeRule(mySMN), QTypeRule("TXT")}, RCodeAction(4)) + addAction(AndRule{SuffixMatchNodeRule(mySMN), QTypeRule("TXT")}, RCodeAction(dnsdist.NOTIMP)) addAction(makeRule("drop.test.powerdns.com."), DropAction()) block=newDNSName("powerdns.org.") function blockFilter(dq) diff --git a/regression-tests.dnsdist/test_EdnsClientSubnet.py b/regression-tests.dnsdist/test_EdnsClientSubnet.py index 959d4996b..273a7dee5 100644 --- a/regression-tests.dnsdist/test_EdnsClientSubnet.py +++ b/regression-tests.dnsdist/test_EdnsClientSubnet.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -import unittest import dns import clientsubnetoption import cookiesoption diff --git a/regression-tests.dnsdist/test_RecordsCount.py b/regression-tests.dnsdist/test_RecordsCount.py new file mode 100644 index 000000000..1193fdd41 --- /dev/null +++ b/regression-tests.dnsdist/test_RecordsCount.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python +import copy +import os +import dns +from dnsdisttests import DNSDistTest + +class TestRecordsCountOnlyOneAR(DNSDistTest): + + _config_template = """ + addAction(NotRule(RecordsCountRule(DNSSection.Additional, 1, 1)), RCodeAction(dnsdist.REFUSED)) + newServer{address="127.0.0.1:%s"} + """ + + def testRecordsCountRefuseEmptyAR(self): + """ + RecordsCount: Refuse arcount == 0 + + Send a query to "refuseemptyar.recordscount.tests.powerdns.com.", + check that we are getting a REFUSED response. + """ + name = 'refuseemptyar.recordscount.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + expectedResponse = dns.message.make_response(query) + expectedResponse.set_rcode(dns.rcode.REFUSED) + + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + + def testRecordsCountAllowOneAR(self): + """ + RecordsCount: Allow arcount == 1 + + Send a query to "allowonear.recordscount.tests.powerdns.com.", + check that we are getting a valid response. + """ + name = 'allowonear.recordscount.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN', use_edns=True) + response = dns.message.make_response(query) + response.answer.append(dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1')) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + def testRecordsCountRefuseTwoAR(self): + """ + RecordsCount: Refuse arcount > 1 + + Send a query to "refusetwoar.recordscount.tests.powerdns.com.", + check that we are getting a REFUSED response. + """ + name = 'refusetwoar.recordscount.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN', use_edns=True) + query.additional.append(dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1')) + expectedResponse = dns.message.make_response(query) + expectedResponse.set_rcode(dns.rcode.REFUSED) + + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + +class TestRecordsCountMoreThanOneLessThanFour(DNSDistTest): + + _config_template = """ + addAction(RecordsCountRule(DNSSection.Answer, 2, 3), AllowAction()) + addAction(AllRule(), RCodeAction(dnsdist.REFUSED)) + newServer{address="127.0.0.1:%s"} + """ + + def testRecordsCountRefuseOneAN(self): + """ + RecordsCount: Refuse ancount == 0 + + Send a query to "refusenoan.recordscount.tests.powerdns.com.", + check that we are getting a REFUSED response. + """ + name = 'refusenoan.recordscount.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + expectedResponse = dns.message.make_response(query) + expectedResponse.set_rcode(dns.rcode.REFUSED) + + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + + def testRecordsCountAllowTwoAN(self): + """ + RecordsCount: Allow ancount == 2 + + Send a query to "allowtwoan.recordscount.tests.powerdns.com.", + check that we are getting a valid response. + """ + name = 'allowtwoan.recordscount.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN', use_edns=True) + rrset = dns.rrset.from_text_list(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + ['127.0.0.1', '127.0.0.2']) + query.answer.append(rrset) + response = dns.message.make_response(query) + response.answer.append(rrset) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + def testRecordsCountRefuseFourAN(self): + """ + RecordsCount: Refuse ancount > 3 + + Send a query to "refusefouran.recordscount.tests.powerdns.com.", + check that we are getting a REFUSED response. + """ + name = 'refusefouran.recordscount.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN', use_edns=True) + rrset = dns.rrset.from_text_list(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4']) + query.answer.append(rrset) + + expectedResponse = dns.message.make_response(query) + expectedResponse.set_rcode(dns.rcode.REFUSED) + expectedResponse.answer.append(rrset) + + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + +class TestRecordsCountNothingInNS(DNSDistTest): + + _config_template = """ + addAction(RecordsCountRule(DNSSection.Authority, 0, 0), AllowAction()) + addAction(AllRule(), RCodeAction(dnsdist.REFUSED)) + newServer{address="127.0.0.1:%s"} + """ + + def testRecordsCountRefuseNS(self): + """ + RecordsCount: Refuse nscount != 0 + + Send a query to "refusens.recordscount.tests.powerdns.com.", + check that we are getting a REFUSED response. + """ + name = 'refusens.recordscount.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.NS, + 'ns.tests.powerdns.com.') + query.authority.append(rrset) + expectedResponse = dns.message.make_response(query) + expectedResponse.set_rcode(dns.rcode.REFUSED) + expectedResponse.authority.append(rrset) + + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + + + def testRecordsCountAllowEmptyNS(self): + """ + RecordsCount: Allow nscount == 0 + + Send a query to "allowns.recordscount.tests.powerdns.com.", + check that we are getting a valid response. + """ + name = 'allowns.recordscount.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + response.answer.append(dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1')) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + +class TestRecordsCountNoOPTInAR(DNSDistTest): + + _config_template = """ + addAction(NotRule(RecordsTypeCountRule(DNSSection.Additional, dnsdist.OPT, 0, 0)), RCodeAction(dnsdist.REFUSED)) + newServer{address="127.0.0.1:%s"} + """ + + def testRecordsCountRefuseOPTinAR(self): + """ + RecordsTypeCount: Refuse OPT in AR + + Send a query to "refuseoptinar.recordscount.tests.powerdns.com.", + check that we are getting a REFUSED response. + """ + name = 'refuseoptinar.recordscount.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN', use_edns=True) + expectedResponse = dns.message.make_response(query) + expectedResponse.set_rcode(dns.rcode.REFUSED) + + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + + def testRecordsCountAllowNoOPTInAR(self): + """ + RecordsTypeCount: Allow no OPT in AR + + Send a query to "allownooptinar.recordscount.tests.powerdns.com.", + check that we are getting a valid response. + """ + name = 'allowwnooptinar.recordscount.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + response.answer.append(dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1')) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) diff --git a/regression-tests.dnsdist/test_Trailing.py b/regression-tests.dnsdist/test_Trailing.py new file mode 100644 index 000000000..c874ede2c --- /dev/null +++ b/regression-tests.dnsdist/test_Trailing.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +import threading +import dns +from dnsdisttests import DNSDistTest + +class TestTrailing(DNSDistTest): + + # this test suite uses a different responder port + # because, contrary to the other ones, its + # responders allow trailing data and we don't want + # to mix things up. + _testServerPort = 5360 + _config_template = """ + newServer{address="127.0.0.1:%s"} + addAction(AndRule({QTypeRule(dnsdist.AAAA), TrailingDataRule()}), DropAction()) + """ + @classmethod + def startResponders(cls): + print("Launching responders..") + + cls._UDPResponder = threading.Thread(name='UDP Responder', target=cls.UDPResponder, args=[cls._testServerPort, True]) + cls._UDPResponder.setDaemon(True) + cls._UDPResponder.start() + cls._TCPResponder = threading.Thread(name='TCP Responder', target=cls.TCPResponder, args=[cls._testServerPort, True]) + cls._TCPResponder.setDaemon(True) + cls._TCPResponder.start() + + def testTrailingAllowed(self): + """ + Trailing: Allowed + + """ + name = 'allowed.trailing.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1') + response.answer.append(rrset) + + raw = query.to_wire() + raw = raw + 'A'* 20 + (receivedQuery, receivedResponse) = self.sendUDPQuery(raw, response, rawQuery=True) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(raw, response, rawQuery=True) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + def testTrailingDropped(self): + """ + Trailing: dropped + + """ + name = 'dropped.trailing.tests.powerdns.com.' + query = dns.message.make_query(name, 'AAAA', 'IN') + + raw = query.to_wire() + raw = raw + 'A'* 20 + + (_, receivedResponse) = self.sendUDPQuery(raw, response=None, rawQuery=True) + self.assertEquals(receivedResponse, None) + (_, receivedResponse) = self.sendTCPQuery(raw, response=None, rawQuery=True) + self.assertEquals(receivedResponse, None) -- 2.50.0