]> granicus.if.org Git - pdns/commitdiff
dnsdist: Add SetECSAction to set an arbitrary outgoing ECS value
authorRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 12 Jun 2018 14:24:32 +0000 (16:24 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 12 Jun 2018 14:24:33 +0000 (16:24 +0200)
pdns/dnsdist-lua-actions.cc
pdns/dnsdist-tcp.cc
pdns/dnsdist.cc
pdns/dnsdist.hh
pdns/dnsdistdist/docs/rules-actions.rst
pdns/ednssubnet.cc
regression-tests.dnsdist/test_EdnsClientSubnet.py

index 7df6aa8df6a63f599b65edc83e77043a8eefa192..a4292d4bead949d03b17090844df15793906d932 100644 (file)
@@ -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<char*>(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<DNSAction>(new DisableECSAction());
     });
 
+  g_lua.writeFunction("SetECSAction", [](const std::string v4, boost::optional<std::string> v6) {
+      if (v6) {
+        return std::shared_ptr<DNSAction>(new SetECSAction(Netmask(v4), Netmask(*v6)));
+      }
+      return std::shared_ptr<DNSAction>(new SetECSAction(Netmask(v4)));
+    });
+
   g_lua.writeFunction("SNMPTrapAction", [](boost::optional<std::string> reason) {
 #ifdef HAVE_NET_SNMP
       return std::shared_ptr<DNSAction>(new SNMPTrapAction(reason ? *reason : ""));
index ab2bda916c06247f67da007c8eaa893dd1a15393..fbef832a47d73de9abf1a6de0dc310979cc02e79 100644 (file)
@@ -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;
           }
index eb2873c624d6343825be380da2bc3aeb54f86a7c..d71582363a4ca3bf9249f524b48674af29714a26 100644 (file)
@@ -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;
       }
index cc30c09fd0173b8b5473f9e03debdca3b7f73f58..6698a870ff21bcab32683b18c58ce6aed987d9b4 100644 (file)
@@ -65,6 +65,7 @@ struct DNSQuestion
 #ifdef HAVE_PROTOBUF
   boost::optional<boost::uuids::uuid> 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
index 58f71ecea4b3f046e593362c2e897052a556a05a..491524c85b67032aa1c4a6ad042f51c07dede029 100644 (file)
@@ -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.
index 4826f602f2e98a98d9866ef3a35ed739f1ec7e96..cf78ecf8a7e618394b13cacfeca473b0cf2bea59 100644 (file)
@@ -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
index 1b7f4d090d2559c0176f12376d7e35a833808ab7..6adb863ed012c16a06ec8a2cfcdf4ad1945652f5 100644 (file)
@@ -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)