From 61dc3490d8c4e7f46952035efda83a3bddc7f77e Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Fri, 26 Sep 2025 16:59:25 +0200 Subject: [PATCH] Handle ENT records (with no content) in pipe backend protocol. Fixes: #15027 Signed-off-by: Miod Vallat --- docs/backends/pipe.rst | 32 +++++++++------- modules/pipebackend/pipebackend.cc | 60 ++++++++++++++++++------------ modules/pipebackend/pipebackend.hh | 2 + 3 files changed, 56 insertions(+), 38 deletions(-) diff --git a/docs/backends/pipe.rst b/docs/backends/pipe.rst index beac6130a..0ff863c80 100644 --- a/docs/backends/pipe.rst +++ b/docs/backends/pipe.rst @@ -41,7 +41,7 @@ DNS-based failover with low TTLs. .. note:: Please do read the :doc:`Backend Writer's guide <../appendices/backend-writers-guide>` carefully. The PipeBackend, like all other backends, must not do any DNS thinking, but - answer all questions (INCLUDING THE ANY QUESTION) faithfully. + answer all questions (**including the ANY question**) faithfully. Specifically, the queries that the PipeBackend receives will not correspond to the queries that arrived over DNS. So, a query for an AAAA record may turn into a backend query for an ANY record. There is nothing @@ -66,7 +66,8 @@ local-ip-address field is added after the remote-ip-address, the local-ip-address refers to the IP address the question was received on. When set to 3, the real remote IP/subnet is added based on edns-subnet support (this also requires enabling :ref:`setting-edns-subnet-processing`). -When set to 4 it sends zone name in AXFR request. See also :ref:`PipeBackend Protocol ` below. +When set to 4, it will also send the zone name in AXFR requests. +See also :ref:`PipeBackend Protocol ` below. .. _setting-pipe-command: @@ -119,13 +120,15 @@ characters. Handshake ^^^^^^^^^ -PowerDNS sends out ``HELO\t1``, indicating that it wants to speak the -protocol as defined in this document, version 1. For abi-version 2 or 3, -PowerDNS sends ``HELO\t2`` or ``HELO\t3``. A PowerDNS Coprocess must -then send out a banner, prefixed by ``OK\t``, indicating it launched -successfully. If it does not support the indicated version, it should -respond with ``FAIL``, but not exit. Suggested behaviour is to try and -read a further line, and wait to be terminated. +PowerDNS sends out ``HELO\tN``, where N is the version of the protocol it +wants to speak (e.g. ``HELO\t1`` for protocol version 1, ``HELO\t2`` for +protocol version 2, etc). +A PowerDNS Coprocess must then send out a banner, prefixed by ``OK\t``, +indicating it launched successfully. +If it does not support the indicated version, it should respond with ``FAIL``, +but not exit. +Suggested behaviour is to try and read a further line, and wait to be +terminated. .. note:: Fields are separated by a tab (``\t``) character, @@ -150,8 +153,8 @@ pipe-abi-version = 2 Q qname qclass qtype id remote-ip-address local-ip-address -pipe-abi-version = 3 -~~~~~~~~~~~~~~~~~~~~ +pipe-abi-version = 3 and higher +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: @@ -193,7 +196,7 @@ AXFR-queries look like this: AXFR id zone-name The ``id`` is gathered from the answer to a SOA query. ``zone-name`` is -given in ABI version 4. +given in ABI version 4 and higher. Answers ^^^^^^^ @@ -223,6 +226,7 @@ Again, all fields are tab-separated. ``content`` is as specified in :doc:`../appendices/types`. For MX and SRV, content consists of the priority, followed by a tab, followed by the actual content. +For ENT (Empty Non-Terminal), content will be ignored and can be omitted. A sample dialogue may look like this (note that in reality, almost all queries will actually be for the ANY qtype): @@ -265,7 +269,7 @@ This is a typical zone transfer. ABI version 3 and higher ~~~~~~~~~~~~~~~~~~~~~~~~ -For abi-version 3, DATA-responses get two extra fields: +From abi-version 3 onwards, DATA-responses get two extra fields: :: @@ -284,7 +288,7 @@ which are used for delegation, and also for any glue (A, AAAA) records present for this purpose. Do note that the DS record for a secure delegation should be authoritative! -For abi-versions 1 and 2, the two new fields fall back to default +For abi-version 1 and 2, the two new fields fall back to default values. The default value for scopebits is 0. The default for auth is 1 (meaning authoritative). diff --git a/modules/pipebackend/pipebackend.cc b/modules/pipebackend/pipebackend.cc index 431b87692..e6b6d3d53 100644 --- a/modules/pipebackend/pipebackend.cc +++ b/modules/pipebackend/pipebackend.cc @@ -266,6 +266,12 @@ PipeBackend::~PipeBackend() cleanup(); } +void PipeBackend::throwTooShortDataError(const std::string& what) +{ + g_log << Logger::Error << kBackendId << " Coprocess returned incomplete or empty line in data section for query for " << d_qname << endl; + throw PDNSException("Format error communicating with coprocess in data section" + what); +} + bool PipeBackend::get(DNSResourceRecord& r) { if (d_disavow) // this query has been blocked @@ -275,9 +281,6 @@ bool PipeBackend::get(DNSResourceRecord& r) // The answer format: // DATA qname qclass qtype ttl id content - unsigned int extraFields = 0; - if (d_abiVersion >= 3) - extraFields = 2; try { launch(); @@ -300,40 +303,49 @@ bool PipeBackend::get(DNSResourceRecord& r) continue; } else if (parts[0] == "DATA") { // yay - if (parts.size() < 7 + extraFields) { - g_log << Logger::Error << kBackendId << " Coprocess returned incomplete or empty line in data section for query for " << d_qname << endl; - throw PDNSException("Format error communicating with coprocess in data section"); - // now what? + // The shortest records (ENT) require 6 fields. Other may require more + // and will have a stricter check once the record type has been + // computed. + if (parts.size() < 6 + (d_abiVersion >= 3 ? 2 : 0)) { + throwTooShortDataError(""); } if (d_abiVersion >= 3) { r.scopeMask = std::stoi(parts[1]); r.auth = (parts[2] == "1"); + parts.erase(parts.begin() + 1, parts.begin() + 3); } else { r.scopeMask = 0; r.auth = true; } - r.qname = DNSName(parts[1 + extraFields]); - r.qtype = parts[3 + extraFields]; - pdns::checked_stoi_into(r.ttl, parts[4 + extraFields]); - pdns::checked_stoi_into(r.domain_id, parts[5 + extraFields]); - - if (r.qtype.getCode() != QType::MX && r.qtype.getCode() != QType::SRV) { + r.qname = DNSName(parts[1]); + r.qtype = parts[3]; + pdns::checked_stoi_into(r.ttl, parts[4]); + pdns::checked_stoi_into(r.domain_id, parts[5]); + + switch (r.qtype.getCode()) { + case QType::ENT: + // No other data to process r.content.clear(); - for (unsigned int n = 6 + extraFields; n < parts.size(); ++n) { - if (n != 6 + extraFields) - r.content.append(1, ' '); - r.content.append(parts[n]); + break; + case QType::MX: + case QType::SRV: + if (parts.size() < 8) { + throwTooShortDataError("of MX/SRV record"); } - } - else { - if (parts.size() < 8 + extraFields) { - g_log << Logger::Error << kBackendId << " Coprocess returned incomplete MX/SRV line in data section for query for " << d_qname << endl; - throw PDNSException("Format error communicating with coprocess in data section of MX/SRV record"); + r.content = parts[6] + " " + parts[7]; + break; + default: + if (parts.size() < 7) { + throwTooShortDataError(""); } - - r.content = parts[6 + extraFields] + " " + parts[7 + extraFields]; + r.content = parts[6]; + for (std::vector::size_type pos = 7; pos < parts.size(); ++pos) { + r.content.append(1, ' '); + r.content.append(parts[pos]); + } + break; } break; } diff --git a/modules/pipebackend/pipebackend.hh b/modules/pipebackend/pipebackend.hh index dd75e6c3d..15535f495 100644 --- a/modules/pipebackend/pipebackend.hh +++ b/modules/pipebackend/pipebackend.hh @@ -61,6 +61,8 @@ public: private: void launch(); void cleanup(); + void throwTooShortDataError(const std::string& what); + std::unique_ptr d_coproc; std::unique_ptr d_regex; DNSName d_qname; -- 2.47.3