From bd14f087b6003f2d0a853e43abe450cd0ee2cc28 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Tue, 12 Jun 2018 16:24:32 +0200 Subject: [PATCH] dnsdist: Add SetECSAction to set an arbitrary outgoing ECS value --- pdns/dnsdist-lua-actions.cc | 50 +++++++++++- pdns/dnsdist-tcp.cc | 2 +- pdns/dnsdist.cc | 2 +- pdns/dnsdist.hh | 2 + pdns/dnsdistdist/docs/rules-actions.rst | 13 +++ pdns/ednssubnet.cc | 2 +- .../test_EdnsClientSubnet.py | 80 +++++++++++++++++++ 7 files changed, 147 insertions(+), 4 deletions(-) diff --git a/pdns/dnsdist-lua-actions.cc b/pdns/dnsdist-lua-actions.cc index 7df6aa8df..a4292d4be 100644 --- a/pdns/dnsdist-lua-actions.cc +++ b/pdns/dnsdist-lua-actions.cc @@ -161,7 +161,7 @@ DNSAction::Action TeeAction::operator()(DNSQuestion* dq, string* ruleresult) con query.reserve(dq->size); query.assign((char*) dq->dh, len); - if (!handleEDNSClientSubnet((char*) query.c_str(), query.capacity(), dq->qname->wirelength(), &len, &ednsAdded, &ecsAdded, *dq->remote, dq->ecsOverride, dq->ecsPrefixLength)) { + if (!handleEDNSClientSubnet(const_cast(query.c_str()), query.capacity(), dq->qname->wirelength(), &len, &ednsAdded, &ecsAdded, dq->ecsSet ? dq->ecs.getNetwork() : *dq->remote, dq->ecsOverride, dq->ecsSet ? dq->ecs.getBits() : dq->ecsPrefixLength)) { return DNSAction::Action::None; } @@ -648,6 +648,47 @@ public: } }; +class SetECSAction : public DNSAction +{ +public: + SetECSAction(Netmask v4): d_v4(v4), d_hasV6(false) + { + } + + SetECSAction(Netmask v4, Netmask v6): d_v4(v4), d_v6(v6), d_hasV6(true) + { + } + + DNSAction::Action operator()(DNSQuestion* dq, string* ruleresult) const override + { + dq->ecsSet = true; + + if (d_hasV6) { + dq->ecs = dq->remote->isIPv4() ? d_v4 : d_v6; + } + else { + dq->ecs = d_v4; + } + + return Action::None; + } + + string toString() const override + { + string result = "set ECS to " + d_v4.toString(); + if (d_hasV6) { + result += " / " + d_v6.toString(); + } + return result; + } + +private: + Netmask d_v4; + Netmask d_v6; + bool d_hasV6; +}; + + class DnstapLogAction : public DNSAction, public boost::noncopyable { public: @@ -1166,6 +1207,13 @@ void setupLuaActions() return std::shared_ptr(new DisableECSAction()); }); + g_lua.writeFunction("SetECSAction", [](const std::string v4, boost::optional v6) { + if (v6) { + return std::shared_ptr(new SetECSAction(Netmask(v4), Netmask(*v6))); + } + return std::shared_ptr(new SetECSAction(Netmask(v4))); + }); + g_lua.writeFunction("SNMPTrapAction", [](boost::optional reason) { #ifdef HAVE_NET_SNMP return std::shared_ptr(new SNMPTrapAction(reason ? *reason : "")); diff --git a/pdns/dnsdist-tcp.cc b/pdns/dnsdist-tcp.cc index ab2bda916..fbef832a4 100644 --- a/pdns/dnsdist-tcp.cc +++ b/pdns/dnsdist-tcp.cc @@ -405,7 +405,7 @@ void* tcpClientThread(int pipefd) if (dq.useECS && ((ds && ds->useECS) || (!ds && serverPool->getECS()))) { uint16_t newLen = dq.len; - if (!handleEDNSClientSubnet(query, dq.size, consumed, &newLen, &ednsAdded, &ecsAdded, ci.remote, dq.ecsOverride, dq.ecsPrefixLength)) { + if (!handleEDNSClientSubnet(query, dq.size, consumed, &newLen, &ednsAdded, &ecsAdded, dq.ecsSet ? dq.ecs.getNetwork() : ci.remote, dq.ecsOverride, dq.ecsSet ? dq.ecs.getBits() : dq.ecsPrefixLength)) { vinfolog("Dropping query from %s because we couldn't insert the ECS value", ci.remote.toStringWithPort()); goto drop; } diff --git a/pdns/dnsdist.cc b/pdns/dnsdist.cc index eb2873c62..d71582363 100644 --- a/pdns/dnsdist.cc +++ b/pdns/dnsdist.cc @@ -1336,7 +1336,7 @@ static void processUDPQuery(ClientState& cs, LocalHolders& holders, const struct bool ednsAdded = false; bool ecsAdded = false; if (dq.useECS && ((ss && ss->useECS) || (!ss && serverPool->getECS()))) { - if (!handleEDNSClientSubnet(query, dq.size, consumed, &dq.len, &(ednsAdded), &(ecsAdded), remote, dq.ecsOverride, dq.ecsPrefixLength)) { + if (!handleEDNSClientSubnet(query, dq.size, consumed, &dq.len, &(ednsAdded), &(ecsAdded), dq.ecsSet ? dq.ecs.getNetwork() : remote, dq.ecsOverride, dq.ecsSet ? dq.ecs.getBits() : dq.ecsPrefixLength)) { vinfolog("Dropping query from %s because we couldn't insert the ECS value", remote.toStringWithPort()); return; } diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index cc30c09fd..6698a870f 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -65,6 +65,7 @@ struct DNSQuestion #ifdef HAVE_PROTOBUF boost::optional uniqueId; #endif + Netmask ecs; const DNSName* qname; const uint16_t qtype; const uint16_t qclass; @@ -82,6 +83,7 @@ struct DNSQuestion bool ecsOverride; bool useECS{true}; bool addXPF{true}; + bool ecsSet{false}; }; struct DNSResponse : DNSQuestion diff --git a/pdns/dnsdistdist/docs/rules-actions.rst b/pdns/dnsdistdist/docs/rules-actions.rst index 58f71ecea..491524c85 100644 --- a/pdns/dnsdistdist/docs/rules-actions.rst +++ b/pdns/dnsdistdist/docs/rules-actions.rst @@ -926,6 +926,19 @@ The following actions exist. :param string alterFunction: Name of a function to modify the contents of the logs before sending :param bool includeCNAME: Whether or not to parse and export CNAMEs. Default false +.. function:: SetECSAction(v4 [, v6]) + + .. versionadded:: 1.3.1 + + Set the ECS prefix and prefix length sent to backends to an arbitrary value. + If both IPv4 and IPv6 masks are supplied the IPv4 one will be used for IPv4 clients + and the IPv6 one for IPv6 clients. Otherwise the first mask is used for both, and + can actually be an IPv6 mask. + Subsequent rules are processed after this rule. + + :param string v4: The IPv4 netmask, for example "192.0.2.1/32" + :param string v6: The IPv6 netmask, if any + .. function:: SkipCacheAction() Don't lookup the cache for this query, don't store the answer. diff --git a/pdns/ednssubnet.cc b/pdns/ednssubnet.cc index 4826f602f..cf78ecf8a 100644 --- a/pdns/ednssubnet.cc +++ b/pdns/ednssubnet.cc @@ -98,7 +98,7 @@ string makeEDNSSubnetOptsString(const EDNSSubnetOpts& eso) ComboAddress src=eso.source.getNetwork(); src.truncate(esow.sourceMask); - + if(family == htons(1)) ret.append((const char*) &src.sin4.sin_addr.s_addr, octetsout); else diff --git a/regression-tests.dnsdist/test_EdnsClientSubnet.py b/regression-tests.dnsdist/test_EdnsClientSubnet.py index 1b7f4d090..6adb863ed 100644 --- a/regression-tests.dnsdist/test_EdnsClientSubnet.py +++ b/regression-tests.dnsdist/test_EdnsClientSubnet.py @@ -886,3 +886,83 @@ class TestECSPrefixLengthSetByRuleOrLua(DNSDistTest): receivedQuery.id = expectedQuery.id self.checkQueryEDNSWithECS(expectedQuery, receivedQuery) self.checkResponseNoEDNS(expectedResponse, receivedResponse) + +class TestECSPrefixSetByRule(DNSDistTest): + """ + dnsdist is configured to set the EDNS0 Client Subnet + option for incoming queries to the actual source IP, + but we override it for some queries via SetECSAction(). + """ + + _config_template = """ + setECSOverride(false) + setECSSourcePrefixV4(32) + setECSSourcePrefixV6(128) + newServer{address="127.0.0.1:%s", useClientSubnet=true} + addAction(makeRule("setecsaction.ecsrules.tests.powerdns.com."), SetECSAction("192.0.2.1/32")) + """ + + def testWithRegularECS(self): + """ + ECS Prefix: not set + """ + name = 'notsetecsaction.ecsrules.tests.powerdns.com.' + ecso = clientsubnetoption.ClientSubnetOption('127.0.0.1', 32) + query = dns.message.make_query(name, 'A', 'IN') + expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, options=[ecso], payload=512) + response = dns.message.make_response(query) + response.use_edns(edns=True, payload=4096, options=[ecso]) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1') + response.answer.append(rrset) + expectedResponse = dns.message.make_response(query) + expectedResponse.answer.append(rrset) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = expectedQuery.id + self.checkQueryEDNSWithECS(expectedQuery, receivedQuery) + self.checkResponseNoEDNS(expectedResponse, receivedResponse) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = expectedQuery.id + self.checkQueryEDNSWithECS(expectedQuery, receivedQuery) + self.checkResponseNoEDNS(expectedResponse, receivedResponse) + + def testWithECSSetByRule(self): + """ + ECS Prefix: set with SetECSAction + """ + name = 'setecsaction.ecsrules.tests.powerdns.com.' + ecso = clientsubnetoption.ClientSubnetOption('192.0.2.1', 32) + query = dns.message.make_query(name, 'A', 'IN') + expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, options=[ecso], payload=512) + response = dns.message.make_response(expectedQuery) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1') + response.answer.append(rrset) + expectedResponse = dns.message.make_response(query) + expectedResponse.answer.append(rrset) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = expectedQuery.id + self.checkQueryEDNSWithECS(expectedQuery, receivedQuery) + self.checkResponseNoEDNS(expectedResponse, receivedResponse) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = expectedQuery.id + self.checkQueryEDNSWithECS(expectedQuery, receivedQuery) + self.checkResponseNoEDNS(expectedResponse, receivedResponse) -- 2.40.0