From 87c605c4dad580a60d198e71bc49997822ca89d1 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Tue, 5 Jan 2016 10:27:54 +0100 Subject: [PATCH] dnsdist: Add sending CNAME in spoofed responses - Add addDomainCNAMESpoof() and SpoofCNAMEAction() - Check that we have enough space in the buffer to write the response - Implement the first part of #3064 --- pdns/README-dnsdist.md | 5 +- pdns/dnsdist-lua.cc | 27 +++++++- pdns/dnsdist-tcp.cc | 9 ++- pdns/dnsdist.cc | 2 +- pdns/dnsdist.hh | 2 +- pdns/dnsrulactions.hh | 83 +++++++++++++++-------- regression-tests.dnsdist/dnsdisttests.py | 43 ++++++++---- regression-tests.dnsdist/test_Advanced.py | 28 +++++++- 8 files changed, 147 insertions(+), 52 deletions(-) diff --git a/pdns/README-dnsdist.md b/pdns/README-dnsdist.md index c9fe5329ec..0fe2196e13 100644 --- a/pdns/README-dnsdist.md +++ b/pdns/README-dnsdist.md @@ -280,6 +280,7 @@ Rules can be added via: * addDisableValidationRule(DNS rule) * addDomainBlock(domain) * addDomainSpoof(domain, IPv4[, IPv6]) + * addDomainCNAMESpoof(domain, CNAME) * addLuaAction(DNS rule, lua function) * addNoRecurseRule(DNS rule) * addPoolRule(DNS rule, destination pool) @@ -783,11 +784,13 @@ instantiate a server with additional parameters * `QPSPoolAction()`: set the packet into the specified pool only if it does not exceed the specified QPS limits * `QPSAction()`: drop these packets if the QPS limits are exceeded * `RCodeAction()`: reply immediatly by turning the query into a response with the specified rcode - * `SpoofAction()`: forge a response with the specified IPv4 (for an A query). If you specify both an IPv4 and an IPv6, IPv4 will be used for A and IPv6 for an AAAA. + * `SpoofAction()`: forge a response with the specified IPv4 (for an A query). If you specify both an IPv4 and an IPv6, IPv4 will be used for A and IPv6 for an AAAA + * `SpoofCNAMEAction()`: forge a response with the specified CNAME value * `TCAction()`: create answer to query with TC and RD bits set, to move to TCP/IP * Specialist rule generators * addAnyTCRule(): generate TC=1 answers to ANY queries, moving them to TCP * addDomainSpoof(domain, ip[, ip6]): generate answers for A queries using the ip parameter. If ip6 is supplied, generate answers for AAAA queries too + * addDomainCNAMESpoof(domain, cname): generate CNAME answers for queries using the specified value * addDisableValidationRule(domain): set the CD flags to 1 for all queries matching the specified domain * addNoRecurseRule(domain): clear the RD flag for all queries matching the specified domain * setDNSSECPool(): move queries requesting DNSSEC processing to this pool diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index 672597945a..d4474f5d2e 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -15,13 +15,13 @@ static vector>* g_launchWork; class LuaAction : public DNSAction { public: - typedef std::function(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, int len)> func_t; + typedef std::function(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t len, uint16_t bufferSize)> func_t; LuaAction(LuaAction::func_t func) : d_func(func) {} - Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const override + Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const override { - auto ret = d_func(remote, qname, qtype, dh, len); + auto ret = d_func(remote, qname, qtype, dh, len, bufferSize); if(ruleresult) *ruleresult=std::get<1>(ret); return (Action)std::get<0>(ret); @@ -437,6 +437,10 @@ vector> setupLua(bool client, const std::string& confi return std::shared_ptr(new SpoofAction(ComboAddress(a))); }); + g_lua.writeFunction("SpoofCNAMEAction", [](const string& a) { + return std::shared_ptr(new SpoofAction(a)); + }); + g_lua.writeFunction("addDomainSpoof", [](const std::string& domain, const std::string& ip, boost::optional ip6) { setLuaSideEffect(); SuffixMatchNode smn; @@ -461,6 +465,23 @@ vector> setupLua(bool client, const std::string& confi }); + g_lua.writeFunction("addDomainCNAMESpoof", [](const std::string& domain, const std::string& cname) { + setLuaSideEffect(); + SuffixMatchNode smn; + try + { + smn.add(DNSName(domain)); + } + catch(std::exception& e) { + g_outputBuffer="Error parsing parameters: "+string(e.what()); + return; + } + g_rulactions.modify([&smn,&cname](decltype(g_rulactions)::value_type& rulactions) { + rulactions.push_back({ + std::make_shared(smn), + std::make_shared(cname) }); + }); + }); g_lua.writeFunction("DropAction", []() { return std::shared_ptr(new DropAction); diff --git a/pdns/dnsdist-tcp.cc b/pdns/dnsdist-tcp.cc index ee25798cb8..4c30629c95 100644 --- a/pdns/dnsdist-tcp.cc +++ b/pdns/dnsdist-tcp.cc @@ -158,10 +158,13 @@ void* tcpClientThread(int pipefd) break; } - char queryBuffer[qlen]; + /* if the query is small, allocate a bit more + memory to be able to spoof the content, + or to add ECS without allocating a new buffer */ + size_t querySize = qlen <= 4096 ? qlen + 512 : qlen; + char queryBuffer[querySize]; const char* query = queryBuffer; uint16_t queryLen = qlen; - size_t querySize = qlen; readn2WithTimeout(ci.fd, queryBuffer, queryLen, g_tcpRecvTimeout); #ifdef HAVE_DNSCRYPT std::shared_ptr dnsCryptQuery = 0; @@ -232,7 +235,7 @@ void* tcpClientThread(int pipefd) DNSAction::Action action=DNSAction::Action::None; for(const auto& lr : *localRulactions) { if(lr.first->matches(ci.remote, qname, qtype, dh, queryLen)) { - action=(*lr.second)(ci.remote, qname, qtype, dh, queryLen, &ruleresult); + action=(*lr.second)(ci.remote, qname, qtype, dh, queryLen, querySize, &ruleresult); if(action != DNSAction::Action::None) { lr.first->d_matches++; break; diff --git a/pdns/dnsdist.cc b/pdns/dnsdist.cc index 43835eafa6..be5633962d 100644 --- a/pdns/dnsdist.cc +++ b/pdns/dnsdist.cc @@ -611,7 +611,7 @@ try for(const auto& lr : *localRulactions) { if(lr.first->matches(remote, qname, qtype, dh, len)) { - action=(*lr.second)(remote, qname, qtype, dh, len, &ruleresult); + action=(*lr.second)(remote, qname, qtype, dh, len, querySize, &ruleresult); if(action != DNSAction::Action::None) { lr.first->d_matches++; break; diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index 400d4b959a..09c014608d 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -366,7 +366,7 @@ class DNSAction { public: enum class Action { Drop, Nxdomain, Spoof, Allow, HeaderModify, Pool, Delay, None}; - virtual Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const =0; + virtual Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const =0; virtual string toString() const = 0; }; diff --git a/pdns/dnsrulactions.hh b/pdns/dnsrulactions.hh index 91a05926c9..893720377d 100644 --- a/pdns/dnsrulactions.hh +++ b/pdns/dnsrulactions.hh @@ -214,7 +214,7 @@ private: class DropAction : public DNSAction { public: - DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const override + DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const override { return Action::Drop; } @@ -230,7 +230,7 @@ class QPSAction : public DNSAction public: QPSAction(int limit) : d_qps(limit, limit) {} - DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const override + DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const override { if(d_qps.check()) return Action::Allow; @@ -250,7 +250,7 @@ class DelayAction : public DNSAction public: DelayAction(int msec) : d_msec(msec) {} - DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const override + DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const override { *ruleresult=std::to_string(d_msec); return Action::Delay; @@ -268,7 +268,7 @@ class PoolAction : public DNSAction { public: PoolAction(const std::string& pool) : d_pool(pool) {} - DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const override + DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const override { *ruleresult=d_pool; return Action::Pool; @@ -287,7 +287,7 @@ class QPSPoolAction : public DNSAction { public: QPSPoolAction(unsigned int limit, const std::string& pool) : d_qps(limit, limit), d_pool(pool) {} - DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const override + DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const override { if(d_qps.check()) { *ruleresult=d_pool; @@ -310,7 +310,7 @@ class RCodeAction : public DNSAction { public: RCodeAction(int rcode) : d_rcode(rcode) {} - DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const override + DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const override { dh->rcode = d_rcode; dh->qr = true; // for good measure @@ -328,7 +328,7 @@ private: class TCAction : public DNSAction { public: - DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const override + DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const override { dh->tc = true; dh->qr = true; // for good measure @@ -345,58 +345,81 @@ class SpoofAction : public DNSAction public: SpoofAction(const ComboAddress& a) : d_a(a) { d_aaaa.sin4.sin_family = 0;} SpoofAction(const ComboAddress& a, const ComboAddress& aaaa) : d_a(a), d_aaaa(aaaa) {} - DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const override + SpoofAction(const string& cname): d_cname(cname) { d_a.sin4.sin_family = 0; d_aaaa.sin4.sin_family = 0; } + DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const override { - if((qtype == QType::A && d_a.sin4.sin_family == 0) || - (qtype == QType::AAAA && d_aaaa.sin4.sin_family == 0) || (qtype != QType::A && qtype != QType::AAAA)) + if(d_cname.empty() && + ((qtype == QType::A && d_a.sin4.sin_family == 0) || + (qtype == QType::AAAA && d_aaaa.sin4.sin_family == 0) || (qtype != QType::A && qtype != QType::AAAA))) return Action::None; + unsigned int consumed=0; + uint8_t rdatalen = 0; + if (!d_cname.empty()) { + qtype = QType::CNAME; + rdatalen = d_cname.wirelength(); + } else { + rdatalen = qtype == QType::A ? 4 : 16; + } + + const unsigned char recordstart[]={0xc0, 0x0c, // compressed name + 0, (unsigned char) qtype, + 0, QClass::IN, // IN + 0, 0, 0, 60, // TTL + 0, rdatalen}; + + DNSName ignore((char*)dh, len, sizeof(dnsheader), false, 0, 0, &consumed); + + if (bufferSize < (sizeof(dnsheader) + consumed + 4 + sizeof(recordstart) + rdatalen)) { + return Action::None; + } + dh->qr = true; // for good measure dh->ra = dh->rd; // for good measure dh->ad = false; dh->ancount = htons(1); - dh->arcount = 0; // for now, forget about your EDNS, we're marching over it - unsigned int consumed=0; - - DNSName ignore((char*)dh, len, sizeof(dnsheader), false, 0, 0, &consumed); + dh->arcount = 0; // for now, forget about your EDNS, we're marching over it char* dest = ((char*)dh) +sizeof(dnsheader) + consumed + 4; - uint8_t addrlen = qtype == QType::A ? 4 : 16; - - const unsigned char recordstart[]={0xc0, 0x0c, // compressed name - 0, (unsigned char) qtype, - 0, 1, // IN - 0, 0, 0, 60, // TTL - 0, addrlen}; memcpy(dest, recordstart, sizeof(recordstart)); if(qtype==QType::A) memcpy(dest+sizeof(recordstart), &d_a.sin4.sin_addr.s_addr, 4); - else + else if (qtype==QType::AAAA) memcpy(dest+sizeof(recordstart), &d_aaaa.sin6.sin6_addr.s6_addr, 16); - len = (dest + sizeof(recordstart) + addrlen) - (char*)dh; + else if (qtype==QType::CNAME) { + string wireData = d_cname.toDNSString(); + memcpy(dest+sizeof(recordstart), wireData.c_str(), wireData.length()); + } + len = (dest + sizeof(recordstart) + rdatalen) - (char*)dh; return Action::HeaderModify; } string toString() const override { string ret; - if(d_a.sin4.sin_family) - ret="spoof in "+d_a.toString(); - if(d_aaaa.sin6.sin6_family) { + if(!d_cname.empty()) { if(!ret.empty()) ret += ", "; - ret+="spoof in "+d_aaaa.toString(); + ret+="spoof in "+d_cname.toString(); + } else { + if(d_a.sin4.sin_family) + ret="spoof in "+d_a.toString(); + if(d_aaaa.sin6.sin6_family) { + if(!ret.empty()) ret += ", "; + ret+="spoof in "+d_aaaa.toString(); + } } return ret; } private: ComboAddress d_a; ComboAddress d_aaaa; + DNSName d_cname; }; class NoRecurseAction : public DNSAction { public: - DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const override + DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const override { dh->rd = false; return Action::HeaderModify; @@ -426,7 +449,7 @@ public: if(d_fp) fclose(d_fp); } - DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const override + DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const override { if(!d_fp) { vinfolog("Packet from %s for %s %s with id %d", remote.toStringWithPort(), qname.toString(), QType(qtype).getName(), dh->id); @@ -450,7 +473,7 @@ private: class DisableValidationAction : public DNSAction { public: - DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, string* ruleresult) const override + DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, uint16_t& len, uint16_t bufferSize, string* ruleresult) const override { dh->cd = true; return Action::HeaderModify; diff --git a/regression-tests.dnsdist/dnsdisttests.py b/regression-tests.dnsdist/dnsdisttests.py index e8b7e6484e..81be25024c 100644 --- a/regression-tests.dnsdist/dnsdisttests.py +++ b/regression-tests.dnsdist/dnsdisttests.py @@ -141,12 +141,22 @@ class DNSDistTest(unittest.TestCase): else: # unexpected query, or health check response = dns.message.make_response(request) - rrset = dns.rrset.from_text(request.question[0].name, - 3600, - request.question[0].rdclass, - request.question[0].rdtype, - '127.0.0.1') - response.answer.append(rrset) + if request.question[0].rdclass == dns.rdataclass.IN: + if request.question[0].rdtype == dns.rdatatype.A: + rrset = dns.rrset.from_text(request.question[0].name, + 3600, + request.question[0].rdclass, + request.question[0].rdtype, + '127.0.0.1') + elif request.question[0].rdtype == dns.rdatatype.AAAA: + rrset = dns.rrset.from_text(request.question[0].name, + 3600, + request.question[0].rdclass, + request.question[0].rdtype, + '::1') + if rrset: + response.answer.append(rrset) + sock.sendto(response.to_wire(), addr) sock.close() @@ -178,12 +188,21 @@ class DNSDistTest(unittest.TestCase): else: # unexpected query, or health check response = dns.message.make_response(request) - rrset = dns.rrset.from_text(request.question[0].name, - 3600, - request.question[0].rdclass, - request.question[0].rdtype, - '127.0.0.1') - response.answer.append(rrset) + if request.question[0].rdclass == dns.rdataclass.IN: + if request.question[0].rdtype == dns.rdatatype.A: + rrset = dns.rrset.from_text(request.question[0].name, + 3600, + request.question[0].rdclass, + request.question[0].rdtype, + '127.0.0.1') + elif request.question[0].rdtype == dns.rdatatype.AAAA: + rrset = dns.rrset.from_text(request.question[0].name, + 3600, + request.question[0].rdclass, + request.question[0].rdtype, + '::1') + if rrset: + response.answer.append(rrset) wire = response.to_wire() conn.send(struct.pack("!H", len(wire))) diff --git a/regression-tests.dnsdist/test_Advanced.py b/regression-tests.dnsdist/test_Advanced.py index 662999b189..2bf52f8c0d 100644 --- a/regression-tests.dnsdist/test_Advanced.py +++ b/regression-tests.dnsdist/test_Advanced.py @@ -203,6 +203,7 @@ class TestAdvancedSpoof(DNSDistTest): _config_template = """ addDomainSpoof("spoof.tests.powerdns.com.", "192.0.2.1", "2001:DB8::1") + addDomainCNAMESpoof("cnamespoof.tests.powerdns.com.", "cname.tests.powerdns.com.") newServer{address="127.0.0.1:%s"} """ @@ -260,6 +261,32 @@ class TestAdvancedSpoof(DNSDistTest): receivedResponse.id = expectedResponse.id self.assertEquals(expectedResponse, receivedResponse) + def testSpoofCNAME(self): + """ + Send an A query for "cnamespoof.tests.powerdns.com.", + check that dnsdist sends a spoofed result. + """ + name = 'cnamespoof.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + # dnsdist set RA = RD for spoofed responses + query.flags &= ~dns.flags.RD + expectedResponse = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.CNAME, + 'cname.tests.powerdns.com.') + expectedResponse.answer.append(rrset) + + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False, timeout=2.0) + self.assertTrue(receivedResponse) + receivedResponse.id = expectedResponse.id + self.assertEquals(expectedResponse, receivedResponse) + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False, timeout=2.0) + self.assertTrue(receivedResponse) + receivedResponse.id = expectedResponse.id + self.assertEquals(expectedResponse, receivedResponse) class TestAdvancedPoolRouting(DNSDistTest): @@ -424,7 +451,6 @@ class TestAdvancedRoundRobinLBOneDown(DNSDistTest): self.assertEquals(query, receivedQuery) self.assertEquals(response, receivedResponse) - print(TestAdvancedRoundRobinLB._responsesCounter) total = 0 for key in TestAdvancedRoundRobinLB._responsesCounter: value = TestAdvancedRoundRobinLB._responsesCounter[key] -- 2.47.2