]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add regular Lua bindings for the DNS packet overlay
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 26 Sep 2022 15:25:51 +0000 (17:25 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 10 Oct 2022 07:57:07 +0000 (09:57 +0200)
.github/actions/spell-check/expect.txt
pdns/dnsdist-lua.cc
pdns/dnsdist-lua.hh
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-dnsparser.cc
pdns/dnsdistdist/dnsdist-lua-bindings-dnsparser.cc [new file with mode: 0644]
pdns/dnsdistdist/docs/reference/dnsparser.rst [new file with mode: 0644]
pdns/dnsdistdist/docs/reference/index.rst
regression-tests.dnsdist/test_DNSParser.py [new file with mode: 0644]

index 98f0e13a484de11dbf1d185d430d930efe4d581d..d7c949e157505363f1ac5f96b323fcebf595f560 100644 (file)
@@ -1,4 +1,4 @@
-aaaarecord
+aaarecord
 aae
 aaldering
 ababd
@@ -404,6 +404,7 @@ dnsname
 dnsnameset
 dnsop
 dnspacket
+dnsparser
 dnspcap
 dnsquestion
 DNSR
index 96f1431f5a64b6de6c1772bb5c5ef545833c7c71..d942d560b8ea237d005e11463205768864582f66 100644 (file)
@@ -3050,6 +3050,7 @@ vector<std::function<void(void)>> setupLua(LuaContext& luaCtx, bool client, bool
   setupLuaConfig(luaCtx, client, configCheck);
   setupLuaBindings(luaCtx, client);
   setupLuaBindingsDNSCrypt(luaCtx, client);
+  setupLuaBindingsDNSParser(luaCtx);
   setupLuaBindingsDNSQuestion(luaCtx);
   setupLuaBindingsKVS(luaCtx, client);
   setupLuaBindingsNetwork(luaCtx, client);
index 968ff39592f7bf51993329b20233343a511b7e34..54049fa07b337d0ac4f17ba8b0c616bf41489988 100644 (file)
@@ -146,6 +146,7 @@ vector<std::function<void(void)>> setupLua(LuaContext& luaCtx, bool client, bool
 void setupLuaActions(LuaContext& luaCtx);
 void setupLuaBindings(LuaContext& luaCtx, bool client);
 void setupLuaBindingsDNSCrypt(LuaContext& luaCtx, bool client);
+void setupLuaBindingsDNSParser(LuaContext& luaCtx);
 void setupLuaBindingsDNSQuestion(LuaContext& luaCtx);
 void setupLuaBindingsKVS(LuaContext& luaCtx, bool client);
 void setupLuaBindingsNetwork(LuaContext& luaCtx, bool client);
index 58dc706400cba5271e2915b84a10c72e7dec4677..8ada6aa9daa59cb9ea042ef65908cf6c5b5c9c8c 100644 (file)
@@ -151,6 +151,7 @@ dnsdist_SOURCES = \
        dnsdist-lbpolicies.cc dnsdist-lbpolicies.hh \
        dnsdist-lua-actions.cc \
        dnsdist-lua-bindings-dnscrypt.cc \
+       dnsdist-lua-bindings-dnsparser.cc \
        dnsdist-lua-bindings-dnsquestion.cc \
        dnsdist-lua-bindings-kvs.cc \
        dnsdist-lua-bindings-network.cc \
index c678a513ecfe4ad81f521b2f0932986ced09435e..00800b3f7c06f7459cd406aec87407deaf0f5718 100644 (file)
@@ -34,11 +34,10 @@ DNSPacketOverlay::DNSPacketOverlay(const std::string_view& packet)
   uint64_t numRecords = ntohs(d_header.ancount) + ntohs(d_header.nscount) + ntohs(d_header.arcount);
   d_records.reserve(numRecords);
 
-  try
-  {
+  try {
     PacketReader reader(pdns_string_view(reinterpret_cast<const char*>(packet.data()), packet.size()));
 
-    for (uint16_t n = 0; n < ntohs(d_header.qdcount) ; ++n) {
+    for (uint16_t n = 0; n < ntohs(d_header.qdcount); ++n) {
       reader.xfrName(d_qname);
       reader.xfrType(d_qtype);
       reader.xfrType(d_qclass);
@@ -65,4 +64,3 @@ DNSPacketOverlay::DNSPacketOverlay(const std::string_view& packet)
   }
 }
 }
-
diff --git a/pdns/dnsdistdist/dnsdist-lua-bindings-dnsparser.cc b/pdns/dnsdistdist/dnsdist-lua-bindings-dnsparser.cc
new file mode 100644 (file)
index 0000000..dc8f3c2
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#include "dnsdist.hh"
+#include "dnsdist-dnsparser.hh"
+#include "dnsdist-lua.hh"
+
+void setupLuaBindingsDNSParser(LuaContext& luaCtx)
+{
+#ifndef DISABLE_DNSPACKET_BINDINGS
+  luaCtx.writeFunction("newDNSPacketOverlay", [](const std::string& packet) {
+    dnsdist::DNSPacketOverlay dpo(packet);
+    return dpo;
+  });
+
+  luaCtx.registerMember<DNSName(dnsdist::DNSPacketOverlay::*)>(std::string("qname"), [](const dnsdist::DNSPacketOverlay& overlay) { return overlay.d_qname; });
+  luaCtx.registerMember<uint16_t(dnsdist::DNSPacketOverlay::*)>(std::string("qtype"), [](const dnsdist::DNSPacketOverlay& overlay) { return overlay.d_qtype; });
+  luaCtx.registerMember<uint16_t(dnsdist::DNSPacketOverlay::*)>(std::string("qclass"), [](const dnsdist::DNSPacketOverlay& overlay) { return overlay.d_qclass; });
+  luaCtx.registerMember<dnsheader(dnsdist::DNSPacketOverlay::*)>(std::string("dh"), [](const dnsdist::DNSPacketOverlay& overlay) { return overlay.d_header; });
+
+  luaCtx.registerFunction<uint16_t (dnsdist::DNSPacketOverlay::*)(uint8_t) const>("getRecordsCountInSection", [](const dnsdist::DNSPacketOverlay& overlay, uint8_t section) -> uint16_t {
+    if (section > 3) {
+      return 0;
+    }
+    uint16_t count = 0;
+    for (const auto& record : overlay.d_records) {
+      if (record.d_place == section) {
+        count++;
+      }
+    }
+
+    return count;
+  });
+
+  luaCtx.registerFunction<dnsdist::DNSPacketOverlay::Record (dnsdist::DNSPacketOverlay::*)(size_t) const>("getRecord", [](const dnsdist::DNSPacketOverlay& overlay, size_t idx) {
+    return overlay.d_records.at(idx);
+  });
+
+  luaCtx.registerMember<DNSName(dnsdist::DNSPacketOverlay::Record::*)>(std::string("name"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_name; });
+  luaCtx.registerMember<uint16_t(dnsdist::DNSPacketOverlay::Record::*)>(std::string("type"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_type; });
+  luaCtx.registerMember<uint16_t(dnsdist::DNSPacketOverlay::Record::*)>(std::string("class"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_class; });
+  luaCtx.registerMember<uint32_t(dnsdist::DNSPacketOverlay::Record::*)>(std::string("ttl"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_ttl; });
+  luaCtx.registerMember<uint8_t(dnsdist::DNSPacketOverlay::Record::*)>(std::string("place"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_place; });
+  luaCtx.registerMember<uint16_t(dnsdist::DNSPacketOverlay::Record::*)>(std::string("contentLength"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_contentLength; });
+  luaCtx.registerMember<uint16_t(dnsdist::DNSPacketOverlay::Record::*)>(std::string("contentOffset"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_contentOffset; });
+
+#endif /* DISABLE_DNSPACKET_BINDINGS */
+}
diff --git a/pdns/dnsdistdist/docs/reference/dnsparser.rst b/pdns/dnsdistdist/docs/reference/dnsparser.rst
new file mode 100644 (file)
index 0000000..41bb396
--- /dev/null
@@ -0,0 +1,123 @@
+DNS Parser
+==========
+
+Since 1.8.0, dnsdist contains a limited DNS parser class that can be used to inspect
+the content of DNS queries and responses in Lua.
+
+The first step is to get the content of the DNS payload into a Lua string,
+for example using :meth:`DNSQuestion:getContent`, or :meth:`DNSResponse:getContent`,
+and then to create a :class:`DNSPacketOverlay` object:
+
+.. code-block:: lua
+
+  function dumpPacket(dq)
+    local packet = dq:getContent()
+    local overlay = newDNSPacketOverlay(packet)
+    print(overlay.qname)
+    print(overlay.qtype)
+    print(overlay.qclass)
+    local count = overlay:getRecordsCountInSection(1)
+    print(count)
+    for idx=0, count-1 do
+      local record = overlay:getRecord(idx)
+      print(record.name)
+      print(record.type)
+      print(record.class)
+      print(record.ttl)
+      print(record.place)
+      print(record.contentLength)
+      print(record.contentOffset)
+    end
+    return DNSAction.None
+  end
+
+  addAction(AllRule(), LuaAction(dumpPacket))
+
+
+.. function:: newDNSPacketOverlay(packet) -> DNSPacketOverlay
+
+  .. versionadded:: 1.8.0
+
+  Returns a DNSPacketOverlay
+
+  :param str packet: The DNS payload
+
+.. class:: DNSPacketOverlay
+
+  .. versionadded:: 1.8.0
+
+  The DNSPacketOverlay object has several attributes, all of them read-only:
+
+  .. attribute:: DNSPacketOverlay.qname
+
+    The qname of this packet, as a :ref:`DNSName`.
+
+  .. attribute:: DNSPacketOverlay.qtype
+
+    The type of the query in this packet.
+
+  .. attribute:: DNSPacketOverlay.qclass
+
+    The class of the query in this packet.
+
+  .. attribute:: DNSPacketOverlay.dh
+
+  It also supports the following methods:
+
+  .. method:: DNSPacketOverlay:getRecordsCountInSection(section) -> int
+
+    Returns the number of records in the ANSWER (1), AUTHORITY (2) and
+    ADDITIONAL (3) section of this packet. The number of records in the
+    QUESTION (0) is always set to 0, look at the dnsheader if you need
+    the actual qdcount.
+
+    :param int section: The section, see above
+
+  .. method:: DNSPacketOverlay:getRecord(idx) -> DNSRecord
+
+    Get the record at the requested position. The records in the
+    QUESTION sections are not taken into account, so the first record
+    in the answer section would be at position 0.
+
+    :param int idx: The position of the requested record
+
+
+.. _DNSRecord:
+
+DNSRecord object
+==================
+
+.. class:: DNSRecord
+
+  .. versionadded:: 1.8.0
+
+  This object represents an unparsed DNS record, as returned by the :ref:`DNSPacketOverlay` class. It has several attributes, all of them read-only:
+
+  .. attribute:: DNSRecord.name
+
+    The name of this record, as a :ref:`DNSName`.
+
+  .. attribute:: DNSRecord.type
+
+    The type of this record.
+
+  .. attribute:: DNSRecord.class
+
+    The class of this record.
+
+  .. attribute:: DNSRecord.ttl
+
+    The TTL of this record.
+
+  .. attribute:: DNSRecord.place
+
+    The place (section) of this record.
+
+  .. attribute:: DNSRecord.contentLength
+
+    The length, in bytes, of the rdata content of this record.
+
+  .. attribute:: DNSRecord.contentOffset
+
+    The offset since the beginning of the DNS payload, in bytes, at which the
+    rdata content of this record starts.
index e30237e83a5562fd5932db5e744fcc333ce3f2fc..2d53990a77ae081388161d41b8544c58e5ace9a4 100755 (executable)
@@ -16,6 +16,7 @@ These chapters contain extensive information on all functions and object availab
   dq
   ebpf
   dnscrypt
+  dnsparser
   protobuf
   dnstap
   carbon
diff --git a/regression-tests.dnsdist/test_DNSParser.py b/regression-tests.dnsdist/test_DNSParser.py
new file mode 100644 (file)
index 0000000..1a14821
--- /dev/null
@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+import unittest
+import dns
+from dnsdisttests import DNSDistTest
+
+class TestDNSParser(DNSDistTest):
+
+    _verboseMode = True
+    _config_template = """
+  function checkQueryPacket(dq)
+    local packet = dq:getContent()
+    if #packet ~= 41 then
+      return DNSAction.Spoof, #packet..".invalid.query.size."
+    end
+
+    local overlay = newDNSPacketOverlay(packet)
+    if overlay.qname:toString() ~= "powerdns.com." then
+      return DNSAction.Spoof, overlay.qname:toString().."invalid.query.qname."
+    end
+    if overlay.qtype ~= DNSQType.A then
+      return DNSAction.Spoof, overlay.qtype..".invalid.query.qtype."
+    end
+    if overlay.qclass ~= DNSClass.IN then
+      return DNSAction.Spoof, overlay.qclass..".invalid.query.qclass."
+    end
+    local count = overlay:getRecordsCountInSection(0)
+    if count ~= 0 then
+      return DNSAction.Spoof, count..".invalid.query.count.in.q."
+    end
+    count = overlay:getRecordsCountInSection(1)
+    if count ~= 0 then
+      return DNSAction.Spoof, count..".invalid.query.count.in.a."
+    end
+    count = overlay:getRecordsCountInSection(2)
+    if count ~= 0 then
+      return DNSAction.Spoof, count..".invalid.query.count.in.auth."
+    end
+    count = overlay:getRecordsCountInSection(3)
+    -- for OPT
+    if count ~= 1 then
+      return DNSAction.Spoof, count..".invalid.query.count.in.add."
+    end
+    return DNSAction.None
+  end
+
+  function checkResponsePacket(dq)
+    local packet = dq:getContent()
+    if #packet ~= 57 then
+      print(#packet..".invalid.size.")
+      return DNSResponseAction.ServFail
+    end
+
+    local overlay = newDNSPacketOverlay(packet)
+    if overlay.qname:toString() ~= "powerdns.com." then
+      print(overlay.qname:toString().."invalid.qname.")
+      return DNSResponseAction.ServFail
+    end
+    if overlay.qtype ~= DNSQType.A then
+      print(overlay.qtype..".invalid.qtype.")
+      return DNSResponseAction.ServFail
+    end
+    if overlay.qclass ~= DNSClass.IN then
+      print(overlay.qclass..".invalid.qclass.")
+      return DNSResponseAction.ServFail
+    end
+    local count = overlay:getRecordsCountInSection(0)
+    if count ~= 0 then
+      print(count..".invalid.count.in.q.")
+      return DNSResponseAction.ServFail
+    end
+    count = overlay:getRecordsCountInSection(1)
+    if count ~= 1 then
+      print(count..".invalid.count.in.a.")
+      return DNSResponseAction.ServFail
+    end
+    count = overlay:getRecordsCountInSection(2)
+    if count ~= 0 then
+      print(count..".invalid.count.in.auth.")
+      return DNSResponseAction.ServFail
+    end
+    count = overlay:getRecordsCountInSection(3)
+    -- for OPT
+    if count ~= 1 then
+      print(count..".invalid.count.in.add.")
+      return DNSResponseAction.ServFail
+    end
+    local record = overlay:getRecord(0)
+    if record.name:toString() ~= "powerdns.com." then
+      print(record.name:toString()..".invalid.name.")
+      return DNSResponseAction.ServFail
+    end
+    if record.type ~= DNSQType.A then
+      print(record.type..".invalid.type.")
+      return DNSResponseAction.ServFail
+    end
+    if record.class ~= DNSClass.IN then
+      print(record.class..".invalid.class.")
+      return DNSResponseAction.ServFail
+    end
+    if record.ttl ~= 3600 then
+      print(record.ttl..".invalid.ttl.")
+      return DNSResponseAction.ServFail
+    end
+    if record.place ~= 1 then
+      print(record.place..".invalid.place.")
+      return DNSResponseAction.ServFail
+    end
+    if record.contentLength ~= 4 then
+      print(record.contentLength..".invalid.contentLength.")
+      return DNSResponseAction.ServFail
+    end
+    if record.contentOffset ~= 42 then
+      print(record.contentOffset..".invalid.contentOffset.")
+      return DNSResponseAction.ServFail
+    end
+    return DNSAction.None
+  end
+
+  addAction(AllRule(), LuaAction(checkQueryPacket))
+  addResponseAction(AllRule(), LuaResponseAction(checkResponsePacket))
+  newServer{address="127.0.0.1:%s"}
+    """
+
+    def testQuestionAndResponse(self):
+        """
+        DNS Parser: basic checks
+        """
+        name = 'powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN', use_edns=True)
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '192.0.2.1')
+        response.answer.append(rrset)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (receivedQuery, receivedResponse) = sender(query, response)
+            print(receivedResponse)
+            self.assertTrue(receivedQuery)
+            self.assertTrue(receivedResponse)
+            receivedQuery.id = query.id
+            self.assertEqual(query, receivedQuery)
+            self.assertEqual(receivedResponse, response)