From: Remi Gacogne Date: Mon, 26 Sep 2022 15:25:51 +0000 (+0200) Subject: dnsdist: Add regular Lua bindings for the DNS packet overlay X-Git-Tag: dnsdist-1.8.0-rc1~283^2~1 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=3669b961dc941631cf0b34699a0ec00256ddde4e;p=thirdparty%2Fpdns.git dnsdist: Add regular Lua bindings for the DNS packet overlay --- diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 98f0e13a48..d7c949e157 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1,4 +1,4 @@ -aaaarecord +aaarecord aae aaldering ababd @@ -404,6 +404,7 @@ dnsname dnsnameset dnsop dnspacket +dnsparser dnspcap dnsquestion DNSR diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index 96f1431f5a..d942d560b8 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -3050,6 +3050,7 @@ vector> 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); diff --git a/pdns/dnsdist-lua.hh b/pdns/dnsdist-lua.hh index 968ff39592..54049fa07b 100644 --- a/pdns/dnsdist-lua.hh +++ b/pdns/dnsdist-lua.hh @@ -146,6 +146,7 @@ vector> 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); diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index 58dc706400..8ada6aa9da 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -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 \ diff --git a/pdns/dnsdistdist/dnsdist-dnsparser.cc b/pdns/dnsdistdist/dnsdist-dnsparser.cc index c678a513ec..00800b3f7c 100644 --- a/pdns/dnsdistdist/dnsdist-dnsparser.cc +++ b/pdns/dnsdistdist/dnsdist-dnsparser.cc @@ -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(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 index 0000000000..dc8f3c273d --- /dev/null +++ b/pdns/dnsdistdist/dnsdist-lua-bindings-dnsparser.cc @@ -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(std::string("qname"), [](const dnsdist::DNSPacketOverlay& overlay) { return overlay.d_qname; }); + luaCtx.registerMember(std::string("qtype"), [](const dnsdist::DNSPacketOverlay& overlay) { return overlay.d_qtype; }); + luaCtx.registerMember(std::string("qclass"), [](const dnsdist::DNSPacketOverlay& overlay) { return overlay.d_qclass; }); + luaCtx.registerMember(std::string("dh"), [](const dnsdist::DNSPacketOverlay& overlay) { return overlay.d_header; }); + + luaCtx.registerFunction("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("getRecord", [](const dnsdist::DNSPacketOverlay& overlay, size_t idx) { + return overlay.d_records.at(idx); + }); + + luaCtx.registerMember(std::string("name"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_name; }); + luaCtx.registerMember(std::string("type"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_type; }); + luaCtx.registerMember(std::string("class"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_class; }); + luaCtx.registerMember(std::string("ttl"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_ttl; }); + luaCtx.registerMember(std::string("place"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_place; }); + luaCtx.registerMember(std::string("contentLength"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_contentLength; }); + luaCtx.registerMember(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 index 0000000000..41bb396618 --- /dev/null +++ b/pdns/dnsdistdist/docs/reference/dnsparser.rst @@ -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. diff --git a/pdns/dnsdistdist/docs/reference/index.rst b/pdns/dnsdistdist/docs/reference/index.rst index e30237e83a..2d53990a77 100755 --- a/pdns/dnsdistdist/docs/reference/index.rst +++ b/pdns/dnsdistdist/docs/reference/index.rst @@ -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 index 0000000000..1a14821aab --- /dev/null +++ b/regression-tests.dnsdist/test_DNSParser.py @@ -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)