diff --git a/docs/changelog/4.9.rst b/docs/changelog/4.9.rst index 716ed9fbe387..95cdb06b4b21 100644 --- a/docs/changelog/4.9.rst +++ b/docs/changelog/4.9.rst @@ -1,6 +1,98 @@ Changelogs for 4.9.x ==================== +.. changelog:: + :version: 4.9.15 + :released: 20th of May 2026 + + This is release 4.9.15 of the Authoritative Server. + It contains bug fixes and security fixes. + + Please review the :doc:`Upgrade Notes <../upgrading>` before upgrading from versions < 4.9.x. + + .. change:: + :tags: Bug Fixes + :pullreq: 17444 + + Fix PowerDNS Security Advisory 2026-06 for PowerDNS Authoritative Server: Multiple Issues + + .. change:: + :tags: Bug Fixes + :pullreq: 17295 + :tickets: 17284 + + use less inefficient code in web server + + .. change:: + :tags: Bug Fixes + :pullreq: 17293 + :tickets: 17240 + + harden xfr*BitInt writers + + .. change:: + :tags: Bug Fixes + :pullreq: 17260 + :tickets: 16636 + + perform axfr immediately when creating an autosecondary domain + + .. change:: + :tags: Bug Fixes + :pullreq: 17262 + :tickets: 16731 + + web: stricter control of statistics rings changes + + .. change:: + :tags: Bug Fixes + :pullreq: 17265 + :tickets: 16831 + + stricter handing of the Lua DNS update policy + + .. change:: + :tags: Bug Fixes + :pullreq: 17267 + :tickets: 17000 + + correctly delete ENT records from the API + + .. change:: + :tags: Bug Fixes + :pullreq: 17269 + :tickets: 17126 + + lua: one more bad case of createForward + + .. change:: + :tags: Bug Fixes + :pullreq: 17271 + :tickets: 17130 + + minor pdns_control bugfixes + + .. change:: + :tags: Bug Fixes + :pullreq: 17272 + :tickets: 17149 + + webserver: correctly split the basic authorization cookie + + .. change:: + :tags: Bug Fixes + :pullreq: 17274 + :tickets: 17152 + + fixes to AXFR in Bind backend + + .. change:: + :tags: Bug Fixes + :pullreq: 17276 + :tickets: 17155 + + dnsupdate handling buglet + .. changelog:: :version: 4.9.14 :released: 22th of April 2026 diff --git a/docs/changelog/5.0.rst b/docs/changelog/5.0.rst index ad7b1260deb1..fb49065026f8 100644 --- a/docs/changelog/5.0.rst +++ b/docs/changelog/5.0.rst @@ -1,6 +1,105 @@ Changelogs for 5.0.x ==================== +.. changelog:: + :version: 5.0.5 + :released: 20th of May 2026 + + This is release 5.0.5 of the Authoritative Server. + It contains bug fixes and security fixes. + + Please review the :doc:`Upgrade Notes <../upgrading>` before upgrading from versions < 4.9.x. + + .. change:: + :tags: Bug Fixes + :pullreq: 17443 + + Fix PowerDNS Security Advisory 2026-06 for PowerDNS Authoritative Server: Multiple Issues + + .. change:: + :tags: Bug Fixes + :pullreq: 17296 + :tickets: 17284 + + use less inefficient code in web server + + .. change:: + :tags: Bug Fixes + :pullreq: 17294 + :tickets: 17240 + + harden xfr*BitInt writers + + .. change:: + :tags: Bug Fixes + :pullreq: 17259 + :tickets: 16636 + + perform axfr immediately when creating an autosecondary domain + + .. change:: + :tags: Bug Fixes + :pullreq: 17261 + :tickets: 16671 + + Actually install binaries when building with meson + + .. change:: + :tags: Bug Fixes + :pullreq: 17263 + :tickets: 16731 + + web: stricter control of statistics rings changes + + .. change:: + :tags: Bug Fixes + :pullreq: 17264 + :tickets: 16831 + + stricter handing of the Lua DNS update policy + + .. change:: + :tags: Bug Fixes + :pullreq: 17266 + :tickets: 17000 + + correctly delete ENT records from the API + + .. change:: + :tags: Bug Fixes + :pullreq: 17268 + :tickets: 17126 + + lua: one more bad case of createForward + + .. change:: + :tags: Bug Fixes + :pullreq: 17270 + :tickets: 17130 + + minor pdns_control bugfixes + + .. change:: + :tags: Bug Fixes + :pullreq: 17273 + :tickets: 17149 + + webserver: correctly split the basic authorization cookie + + .. change:: + :tags: Bug Fixes + :pullreq: 17275 + :tickets: 17152 + + fixes to AXFR in Bind backend + + .. change:: + :tags: Bug Fixes + :pullreq: 17277 + :tickets: 17155 + + dnsupdate handling buglet + .. changelog:: :version: 5.0.4 :released: 22th of April 2026 diff --git a/docs/secpoll.zone b/docs/secpoll.zone index 57c05d1fde53..6c4cbbf5a4df 100644 --- a/docs/secpoll.zone +++ b/docs/secpoll.zone @@ -1,4 +1,4 @@ -@ 86400 IN SOA pdns-public-ns1.powerdns.com. peter\.van\.dijk.powerdns.com. 2026042901 10800 3600 604800 10800 +@ 86400 IN SOA pdns-public-ns1.powerdns.com. peter\.van\.dijk.powerdns.com. 2026052001 10800 3600 604800 10800 @ 3600 IN NS pdns-public-ns1.powerdns.com. @ 3600 IN NS pdns-public-ns2.powerdns.com. @@ -142,16 +142,18 @@ auth-4.9.10.security-status 60 IN TXT "3 Upgrade now auth-4.9.11.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-4.9.12.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-4.9.13.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" -auth-4.9.14.security-status 60 IN TXT "1 OK" +auth-4.9.14.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-06.html" +auth-4.9.15.security-status 60 IN TXT "1 OK" auth-5.0.0-alpha1.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-5.0.0-beta1.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-5.0.0.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-5.0.1.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-5.0.2.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-5.0.3.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" -auth-5.0.4.security-status 60 IN TXT "1 OK" +auth-5.0.4.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-06.html" +auth-5.0.5.security-status 60 IN TXT "1 OK" auth-5.1.0-alpha1.security-status 60 IN TXT "3 Superseded pre-release (known vulnerabilities)" -auth-5.1.0-beta1.security-status 60 IN TXT "1 Unsupported pre-release (no known vulnerabilities)" +auth-5.1.0-beta1.security-status 60 IN TXT "3 Unsupported pre-release (known vulnerabilities)" ; Auth Debian auth-3.4.1-2.debian.security-status 60 IN TXT "3 Upgrade now, see https://docs.powerdns.com/authoritative/appendices/EOL.html" diff --git a/docs/security-advisories/powerdns-advisory-2026-06.rst b/docs/security-advisories/powerdns-advisory-2026-06.rst new file mode 100644 index 000000000000..30ab88c40ad0 --- /dev/null +++ b/docs/security-advisories/powerdns-advisory-2026-06.rst @@ -0,0 +1,119 @@ +PowerDNS Security Advisory 2026-06: Multiple Issues +=================================================== + +Concurrency and locking defects in GSS-TSIG +------------------------------------------- + +- CVE: CVE-2026-42002 +- Date: 2026-05-06T00:00:00+00:00 +- Affects: PowerDNS Authoritative Server 4.7.0 up to and including 4.9.14 and 5.0.4 +- Not affected: PowerDNS Authoritative Server 4.9.15, 5.0.5 +- Severity: Medium +- Impact: Denial of service +- Exploit: Concurrent TKEY queries for the same key may accidentally share the same GSS-TSIG data structures and cause memory corruption or unexpected server exit. +- Risk of system compromise: None +- Solution: Upgrade to patched version or disable gss-tsig support in server configuration +- CWE: CWE-364 +- CVSS: 3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H +- Last affected: 4.9.14,5.0.4 +- First fixed: 4.9.15,5.0.5 +- Internal ID: 381 + +Multiple concurrency and locking defects in the GSS-TSIG code can lead to +memory corruption due to accidental data structure sharing, which can in turn +lead to a program crash. + +Moreover, the lack of bounds on the number of in-flight GSS-TSIG contexts can +lead to unbounded memory consumption in case of an excessive number of requests +at a given time. A limit of 1000 contexts is now enforced, and can be modified +with the "gss-max-contexts" parameter in server configuration. + +Insufficient Validation of Autoprimary SOA Queries +-------------------------------------------------- + +- CVE: CVE-2026-42001 +- Date: 2026-05-06T00:00:00+00:00 +- Affects: PowerDNS Authoritative Server 4.1.0 up to and including 4.9.14 and 5.0.4 +- Not affected: PowerDNS Authoritative Server 4.9.15, 5.0.5 +- Severity: High +- Impact: Denial of service +- Exploit: Ill-formed answer to SOA query from server operating in autosecondary mode +- Risk of system compromise: None +- Solution: Upgrade to patched version, or disable autosecondary operation +- CWE: CWE-400 +- CVSS: 3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H +- Last affected: 4.9.14,5.0.4 +- First fixed: 4.9.15,5.0.5 +- Internal ID: 467 + +Missing sanity checks of the answer to the initial SOA query, when running in +autosecondary mode and receiving a notification for a not-yet-known domain +may cause the server to crash. + +Insufficient Validation of Names During AXFR +-------------------------------------------- + +- CVE: CVE-2026-42000 +- Date: 2026-05-06T00:00:00+00:00 +- Affects: PowerDNS Authoritative Server up to and including 4.9.14 and 5.0.4 +- Not affected: PowerDNS Authoritative Server 4.9.15, 5.0.5 +- Severity: Medium +- Impact: Denial of service +- Exploit: AXFR of zone with specific contents to Bind backend +- Risk of system compromise: None +- Solution: Upgrade to patched version +- CWE: CWE-77 +- CVSS: 3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:H/A:N +- Last affected: 4.9.14,5.0.4 +- First fixed: 4.9.15,5.0.5 +- Internal ID: 474 + +Missing escaping of special characters (such as $ or @) in DNS names received +during an AXFR operation can lead to an incorrect (non-parsable) Bind backend +configuration to be written, causing this backend to fail until manual +operation is performed to fix the configuration. + +Incorrect Behaviour of Views with TCP PROXY Requests +---------------------------------------------------- + +- CVE: CVE-2026-41999 +- Date: 2026-05-06T00:00:00+00:00 +- Affects: PowerDNS Authoritative Server 5.0.0 up to and including 5.0.4 +- Not affected: PowerDNS Authoritative Server 5.0.5 +- Severity: Medium +- Impact: Information Disclosure +- Exploit: TCP query using PROXY Protocol +- Risk of system compromise: None +- Solution: Upgrade to patched version or disable views feature +- CWE: CWE-284 +- CVSS: 3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N +- Last affected: 5.0.4 +- First fixed: 5.0.5 +- Internal ID: 482 + +When using views, queries sent using TCP Proxy Protocol will select the view +according to the address of the proxy, rather than the address of the initial +query. This can lead to wrong data being returned. + +Insufficient Validation of Member Zone Data May Cause Catalog Zone Transfer to Fail +----------------------------------------------------------------------------------- + +- CVE: CVE-2026-42396 +- Date: 2026-05-06T00:00:00+00:00 +- Affects: PowerDNS Authoritative Server 4.7.0 up to and including 4.9.14 and 5.0.4 +- Not affected: PowerDNS Authoritative Server 4.9.15, 5.0.5 +- Severity: Medium +- Impact: Denial of service +- Exploit: AXFR of catalog zone with a member whose producer group option +contains a double-quote character +- Risk of system compromise: None +- Solution: Upgrade to patched version, or remove all double-quote characters from producer group names. +- CWE: CWE-94 +- CVSS: 3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H +- Last affected: 4.9.14,5.0.4 +- First fixed: 4.9.15,5.0.5 +- Internal ID: 483 + +Missing proper escaping of double-quote characters when computing labels will +cause AXFR of a catalog zone with a member whose producer group option contains +such a character to fail. diff --git a/modules/bindbackend/bindbackend2.cc b/modules/bindbackend/bindbackend2.cc index 24fb4e4fad79..b89355b7427a 100644 --- a/modules/bindbackend/bindbackend2.cc +++ b/modules/bindbackend/bindbackend2.cc @@ -337,6 +337,35 @@ static void stripDomainSuffix(string* qname, const ZoneName& zonename) } } +// Perform adequate escaping of characters which have special meaning in +// Bind zone files. +// Note that the input is supposed to be a DNSName::toString() - or any of +// its variants - so we assume \ and . have been correctly escaped by +// DNSName::appendEscapedLabel already. +static const std::string bindEscape(const std::string& name) +{ + std::string ret; + std::array ebuf{}; + + for (char letter : name) { + switch (letter) { + case '$': + case '@': + case '"': + case ';': + case '(': + case ')': + snprintf(ebuf.data(), ebuf.size(), "\\%03u", static_cast(letter)); + ret += ebuf.data(); + break; + default: + ret += letter; + break; + } + } + return ret; +} + bool Bind2Backend::feedRecord(const DNSResourceRecord& rr, const DNSName& /* ordername */, bool /* ordernameIsNSEC3 */) { if (d_transaction_id == UnknownDomainID) { @@ -345,7 +374,7 @@ bool Bind2Backend::feedRecord(const DNSResourceRecord& rr, const DNSName& /* ord string qname; if (d_transaction_qname.empty()) { - qname = rr.qname.toString(); + qname = bindEscape(rr.qname.toString()); } else if (rr.qname.isPartOf(d_transaction_qname)) { if (rr.qname == d_transaction_qname.operator const DNSName&()) { @@ -353,7 +382,7 @@ bool Bind2Backend::feedRecord(const DNSResourceRecord& rr, const DNSName& /* ord } else { DNSName relName = rr.qname.makeRelative(d_transaction_qname); - qname = relName.toStringNoDot(); + qname = bindEscape(relName.toStringNoDot()); } } else { diff --git a/pdns/auth-catalogzone.cc b/pdns/auth-catalogzone.cc index 828d9f6137a9..0c9cd10a85a6 100644 --- a/pdns/auth-catalogzone.cc +++ b/pdns/auth-catalogzone.cc @@ -147,7 +147,7 @@ void CatalogInfo::toDNSZoneRecords(const ZoneName& zone, vector& DNSZoneRecord dzr; dzr.dr.d_name = prefix; dzr.dr.d_type = QType::PTR; - dzr.dr.setContent(std::make_shared(d_zone.operator const DNSName&().toString())); + dzr.dr.setContent(std::make_shared(d_zone.operator const DNSName&())); dzrs.emplace_back(dzr); // coo property @@ -162,7 +162,7 @@ void CatalogInfo::toDNSZoneRecords(const ZoneName& zone, vector& for (const auto& group : d_group) { dzr.dr.d_name = g_groupdnsname + prefix; dzr.dr.d_type = QType::TXT; - dzr.dr.setContent(std::make_shared("\"" + group + "\"")); + dzr.dr.setContent(std::make_shared("\"" + txtEscape(group) + "\"")); dzrs.emplace_back(dzr); } } diff --git a/pdns/auth-main.cc b/pdns/auth-main.cc index cb684a63ff1b..5c9aa7c2a296 100644 --- a/pdns/auth-main.cc +++ b/pdns/auth-main.cc @@ -353,6 +353,7 @@ static void declareArguments() #ifdef ENABLE_GSS_TSIG ::arg().setSwitch("enable-gss-tsig", "Enable GSS TSIG processing") = "no"; + ::arg().set("gss-max-contexts", "The maximum number of simultaneous GSS contexts allowed") = "1000"; #endif ::arg().setSwitch("views", "Enable views (variants) of zones, for backends which support them") = "no"; @@ -628,10 +629,7 @@ static void qthread(unsigned int num) numreceived++; - accountremote = question.d_remote; - if (question.d_inner_remote) { - accountremote = *question.d_inner_remote; - } + accountremote = question.getInnerRemote(); if (accountremote.sin4.sin_family == AF_INET) { numreceived4++; @@ -786,6 +784,9 @@ static void mainthread() #endif #ifdef ENABLE_GSS_TSIG g_doGssTSIG = ::arg().mustDo("enable-gss-tsig"); + if (g_doGssTSIG) { + GssContext::s_maxGssContexts = ::arg().asNum("gss-max-contexts"); + } #endif g_views = ::arg().mustDo("views"); g_memberCatalogGroup = ::arg()["member-catalog-group"]; diff --git a/pdns/axfr-retriever.cc b/pdns/axfr-retriever.cc index b11895a39801..35193c746c33 100644 --- a/pdns/axfr-retriever.cc +++ b/pdns/axfr-retriever.cc @@ -128,48 +128,46 @@ int AXFRRetriever::getChunk(Resolver::res_t &res, vector* records, ui d_receivedBytes += (uint16_t) len; - MOADNSParser mdp(false, d_buf.data(), len); - - int err = mdp.d_header.rcode; + try { + MOADNSParser mdp(false, d_buf.data(), len); - if(err) { - throw ResolverException("AXFR chunk error: " + RCode::to_s(err)); - } + int err = mdp.d_header.rcode; + if (err != 0) { + throw ResolverException("AXFR chunk error: " + RCode::to_s(err)); + } - if(mdp.d_header.tc) { - throw ResolverException("AXFR chunk had TC bit set"); - } + if(mdp.d_header.tc) { + throw ResolverException("AXFR chunk had TC bit set"); + } - try { d_tsigVerifier.check(std::string(d_buf.data(), len), mdp); - } - catch(const std::runtime_error& re) { - throw ResolverException(re.what()); - } - if(!records) { - err = parseResult(mdp, DNSName(), 0, 0, &res); - - if (!err) { - for(const auto& answer : mdp.d_answers) { - if (answer.d_type == QType::SOA) { - d_soacount++; + if (records == nullptr) { + err = parseResult(mdp, DNSName(), 0, 0, &res); + if (err == 0) { + for(const auto& answer : mdp.d_answers) { + if (answer.d_type == QType::SOA) { + d_soacount++; + } } } } - } - else { - records->clear(); - records->reserve(mdp.d_answers.size()); + else { + records->clear(); + records->reserve(mdp.d_answers.size()); - for(auto& r: mdp.d_answers) { - if (r.d_type == QType::SOA) { - d_soacount++; - } + for(auto& r: mdp.d_answers) { + if (r.d_type == QType::SOA) { + d_soacount++; + } - records->push_back(std::move(r)); + records->push_back(std::move(r)); + } } } + catch(const std::runtime_error& re) { + throw ResolverException(re.what()); + } return true; } diff --git a/pdns/comfun.cc b/pdns/comfun.cc index 3aafd8142b25..9e7af8932d88 100644 --- a/pdns/comfun.cc +++ b/pdns/comfun.cc @@ -550,6 +550,9 @@ try } // cout<topAllocatorsString(20)<(answer); - if (!content) { - SLOG(g_log<info(Logr::Error, "TSIG record has no or invalid content (invalid packet)")); - return false; + bool gotit=false; + for(const auto & answer : mdp.d_answers) { + if(answer.d_type == QType::TSIG && answer.d_class == QType::ANY) { + // cast can fail, f.e. if d_content is an UnknownRecordContent. + auto content = getRR(answer); + if (!content) { + SLOG(g_log<info(Logr::Error, "TSIG record has no or invalid content (invalid packet)")); + return false; + } + *trc = *content; + *keyname = answer.d_name; + gotit=true; } - *trc = *content; - *keyname = answer.d_name; - gotit=true; } + if(!gotit) + return false; + + if (tsigPosOut != nullptr) { + *tsigPosOut = tsigPos; + } + + return true; } - if(!gotit) + catch (const MOADNSException&) { + // If execution has reached this routine, we can reasonably assume that + // the packet is good enough to pass the sanity checks of + // MOADNSParser::init(). But just in case it doesn't, better handle this. return false; - - if (tsigPosOut) { - *tsigPosOut = tsigPos; } - - return true; } bool DNSPacket::validateTSIG(const TSIGTriplet& tsigTriplet, const TSIGRecordContent& tsigContent, const std::string& previousMAC, const std::string& theirMAC, bool timersOnly) const { - MOADNSParser mdp(d_isQuery, d_rawpacket); - uint16_t tsigPos = mdp.getTSIGPos(); - if (tsigPos == 0) { + try { + MOADNSParser mdp(d_isQuery, d_rawpacket); + uint16_t tsigPos = mdp.getTSIGPos(); + if (tsigPos == 0) { + return false; + } + + return ::validateTSIG(d_slog, d_rawpacket, tsigPos, tsigTriplet, tsigContent, previousMAC, theirMAC, timersOnly); + } + catch (const MOADNSException&) { + // If execution has reached this routine, we can reasonably assume that + // the packet is good enough to pass the sanity checks of + // MOADNSParser::init(). But just in case it doesn't, better handle this. return false; } - - return ::validateTSIG(d_slog, d_rawpacket, tsigPos, tsigTriplet, tsigContent, previousMAC, theirMAC, timersOnly); } bool DNSPacket::getTKEYRecord(TKEYRecordContent *tr, DNSName *keyname) const { - MOADNSParser mdp(d_isQuery, d_rawpacket); - bool gotit=false; - - for(const auto & answer : mdp.d_answers) { - if (gotit) { - SLOG(g_log<info(Logr::Error, "More than one TKEY record found in query")); - return false; - } + try { + MOADNSParser mdp(d_isQuery, d_rawpacket); + bool gotit=false; - if(answer.d_type == QType::TKEY) { - // cast can fail, f.e. if d_content is an UnknownRecordContent. - auto content = getRR(answer); - if (!content) { - SLOG(g_log<info(Logr::Error, "TKEY record has no or invalid content (invalid packet)")); + for(const auto & answer : mdp.d_answers) { + if (gotit) { + SLOG(g_log<info(Logr::Error, "More than one TKEY record found in query")); return false; } - *tr = *content; - *keyname = answer.d_name; - gotit=true; + + if(answer.d_type == QType::TKEY) { + // cast can fail, f.e. if d_content is an UnknownRecordContent. + auto content = getRR(answer); + if (!content) { + SLOG(g_log<info(Logr::Error, "TKEY record has no or invalid content (invalid packet)")); + return false; + } + *tr = *content; + *keyname = answer.d_name; + gotit=true; + } } - } - return gotit; + return gotit; + } + catch (const MOADNSException&) { + // If execution has reached this routine, we can reasonably assume that + // the packet is good enough to pass the sanity checks of + // MOADNSParser::init(). But just in case it doesn't, better handle this. + return false; + } } /** This function takes data from the network, possibly received with recvfrom, and parses diff --git a/pdns/dnssecinfra.cc b/pdns/dnssecinfra.cc index fc0758d7c3b1..ceff465ce919 100644 --- a/pdns/dnssecinfra.cc +++ b/pdns/dnssecinfra.cc @@ -871,7 +871,6 @@ bool validateTSIG(Logr::log_t slog, const std::string& packet, size_t sigPos, co tsigMsg = makeTSIGMessageFromTSIGPacket(packet, sigPos, tt.name, trc, previousMAC, timersOnly, dnsHeaderOffset); if (algo == TSIG_GSS) { - GssContext gssctx(tt.name); if (!gss_verify_signature(slog, tt.name, tsigMsg, theirMAC)) { throw std::runtime_error("Signature with TSIG key '"+tt.name.toLogString()+"' failed to validate"); } diff --git a/pdns/gss_context.cc b/pdns/gss_context.cc index 8a9b985d6204..c1945b27246f 100644 --- a/pdns/gss_context.cc +++ b/pdns/gss_context.cc @@ -55,6 +55,8 @@ GssContextError GssContext::getError() { return GSS_CONTEXT_UNSUPPORTED; } #define TSIG_GSS_EXPIRE_INTERVAL 60 +unsigned int GssContext::s_maxGssContexts{1000}; + class GssCredential : boost::noncopyable { public: @@ -136,7 +138,7 @@ class GssCredential : boost::noncopyable static LockGuarded>> s_gss_accept_creds; static LockGuarded>> s_gss_init_creds; -class GssSecContext : boost::noncopyable +class GssSecContext { public: GssSecContext(std::shared_ptr cred) @@ -173,7 +175,7 @@ class GssSecContext : boost::noncopyable } d_state{GssStateInitial}; }; // GssSecContext -static LockGuarded>> s_gss_sec_context; +static LockGuarded>>> s_gss_sec_context; template static void doExpire(T& m, time_t now) @@ -189,9 +191,29 @@ static void doExpire(T& m, time_t now) } } +// Same as above, for s_gss_sec_context +template +static void doExpireL(T& m, time_t now) +{ + auto lock = m.lock(); + for (auto i = lock->begin(); i != lock->end();) { + time_t expiretime{0}; + { + auto ctx = i->second->lock(); + expiretime = ctx->d_expires; + } + if (now > expiretime) { + i = lock->erase(i); + } + else { + ++i; + } + } +} + static void expire() { - static time_t s_last_expired; + static std::atomic s_last_expired; time_t now = time(nullptr); if (now - s_last_expired < TSIG_GSS_EXPIRE_INTERVAL) { return; @@ -199,7 +221,7 @@ static void expire() s_last_expired = now; doExpire(s_gss_init_creds, now); doExpire(s_gss_accept_creds, now); - doExpire(s_gss_sec_context, now); + doExpireL(s_gss_sec_context, now); } bool GssContext::supported() { return true; } @@ -238,18 +260,56 @@ void GssContext::setLabel(const DNSName& label) auto it = lock->find(d_label); if (it != lock->end()) { d_secctx = it->second; - d_type = d_secctx->d_type; + auto ctx = d_secctx->lock(); + d_type = ctx->d_type; } } bool GssContext::expired() { - return (!d_secctx || (d_secctx->d_expires > -1 && d_secctx->d_expires < time(nullptr))); + if (!d_secctx) { + return true; + } + auto ctx = d_secctx->lock(); + return (ctx->d_expires > -1 && ctx->d_expires < time(nullptr)); } bool GssContext::valid() { - return (d_secctx && !expired() && d_secctx->d_state == GssSecContext::GssStateComplete); + if (expired()) { + return false; + } + auto ctx = d_secctx->lock(); + return ctx->d_state == GssSecContext::GssStateComplete; +} + +bool GssContext::createOrReuseContext(std::shared_ptr cred) +{ + // see if we can find a context in non-completed state + if (d_secctx) { + auto ctx = d_secctx->lock(); + if (ctx->d_state != GssSecContext::GssStateNegotiate) { + d_error = GSS_CONTEXT_INVALID; + return false; + } + } + else { + // make context + auto lock = s_gss_sec_context.lock(); + if (lock->size() == s_maxGssContexts) { + d_error = GSS_CONTEXT_LIMIT_REACHED; + d_gss_errors.push_back("Limit of concurrent GSS contexts reached"); + return false; + } + d_secctx = std::make_shared>(cred); + { + auto ctx = d_secctx->lock(); + ctx->d_state = GssSecContext::GssStateNegotiate; + ctx->d_type = d_type; + } + (*lock)[d_label] = d_secctx; + } + return true; } bool GssContext::init(const std::string& input, std::string& output) @@ -278,53 +338,45 @@ bool GssContext::init(const std::string& input, std::string& output) cred = it->second; } - // see if we can find a context in non-completed state - if (d_secctx) { - if (d_secctx->d_state != GssSecContext::GssStateNegotiate) { - d_error = GSS_CONTEXT_INVALID; - return false; - } - } - else { - // make context - auto lock = s_gss_sec_context.lock(); - d_secctx = std::make_shared(cred); - d_secctx->d_state = GssSecContext::GssStateNegotiate; - d_secctx->d_type = d_type; - (*lock)[d_label] = d_secctx; + if (!createOrReuseContext(cred)) { + return false; } recv_tok.length = input.size(); recv_tok.value = const_cast(static_cast(input.c_str())); - if (!d_peerPrincipal.empty()) { - buffer.value = const_cast(static_cast(d_peerPrincipal.c_str())); - buffer.length = d_peerPrincipal.size(); - maj = gss_import_name(&min, &buffer, (gss_OID)GSS_KRB5_NT_PRINCIPAL_NAME, &(d_secctx->d_peer_name)); - if (maj != GSS_S_COMPLETE) { - processError("gss_import_name", maj, min); - return false; + { + auto ctx = d_secctx->lock(); + + if (!d_peerPrincipal.empty()) { + buffer.value = const_cast(static_cast(d_peerPrincipal.c_str())); + buffer.length = d_peerPrincipal.size(); + maj = gss_import_name(&min, &buffer, (gss_OID)GSS_KRB5_NT_PRINCIPAL_NAME, &(ctx->d_peer_name)); + if (maj != GSS_S_COMPLETE) { + processError("gss_import_name", maj, min); + return false; + } } - } - maj = gss_init_sec_context(&min, cred->d_cred, &d_secctx->d_ctx, d_secctx->d_peer_name, GSS_C_NO_OID, GSS_C_MUTUAL_FLAG | GSS_C_REPLAY_FLAG, GSS_C_INDEFINITE, GSS_C_NO_CHANNEL_BINDINGS, &recv_tok, nullptr, &send_tok, &flags, &expires); + maj = gss_init_sec_context(&min, cred->d_cred, &ctx->d_ctx, ctx->d_peer_name, GSS_C_NO_OID, GSS_C_MUTUAL_FLAG | GSS_C_REPLAY_FLAG, GSS_C_INDEFINITE, GSS_C_NO_CHANNEL_BINDINGS, &recv_tok, nullptr, &send_tok, &flags, &expires); - if (send_tok.length > 0) { - output.assign(static_cast(send_tok.value), send_tok.length); - tmp_maj = gss_release_buffer(&tmp_min, &send_tok); - } + if (send_tok.length > 0) { + output.assign(static_cast(send_tok.value), send_tok.length); + tmp_maj = gss_release_buffer(&tmp_min, &send_tok); + } - if (maj == GSS_S_COMPLETE) { - // We do not want forever - if (expires == GSS_C_INDEFINITE) { - expires = 60; + if (maj == GSS_S_COMPLETE) { + // We do not want forever + if (expires == GSS_C_INDEFINITE) { + expires = 60; + } + ctx->d_expires = time(nullptr) + expires; + ctx->d_state = GssSecContext::GssStateComplete; + return true; + } + else if (maj != GSS_S_CONTINUE_NEEDED) { + processError("gss_init_sec_context", maj, min); } - d_secctx->d_expires = time(nullptr) + expires; - d_secctx->d_state = GssSecContext::GssStateComplete; - return true; - } - else if (maj != GSS_S_CONTINUE_NEEDED) { - processError("gss_init_sec_context", maj, min); } return (maj == GSS_S_CONTINUE_NEEDED); @@ -356,44 +408,36 @@ bool GssContext::accept(const std::string& input, std::string& output) cred = it->second; } - // see if we can find a context in non-completed state - if (d_secctx) { - if (d_secctx->d_state != GssSecContext::GssStateNegotiate) { - d_error = GSS_CONTEXT_INVALID; - return false; - } - } - else { - // make context - auto lock = s_gss_sec_context.lock(); - d_secctx = std::make_shared(cred); - d_secctx->d_state = GssSecContext::GssStateNegotiate; - d_secctx->d_type = d_type; - (*lock)[d_label] = d_secctx; + if (!createOrReuseContext(cred)) { + return false; } recv_tok.length = input.size(); recv_tok.value = const_cast(static_cast(input.c_str())); - maj = gss_accept_sec_context(&min, &d_secctx->d_ctx, cred->d_cred, &recv_tok, GSS_C_NO_CHANNEL_BINDINGS, &d_secctx->d_peer_name, nullptr, &send_tok, &flags, &expires, nullptr); + { + auto ctx = d_secctx->lock(); + maj = gss_accept_sec_context(&min, &ctx->d_ctx, cred->d_cred, &recv_tok, GSS_C_NO_CHANNEL_BINDINGS, &ctx->d_peer_name, nullptr, &send_tok, &flags, &expires, nullptr); - if (send_tok.length > 0) { - output.assign(static_cast(send_tok.value), send_tok.length); - tmp_maj = gss_release_buffer(&tmp_min, &send_tok); - } + if (send_tok.length > 0) { + output.assign(static_cast(send_tok.value), send_tok.length); + tmp_maj = gss_release_buffer(&tmp_min, &send_tok); + } - if (maj == GSS_S_COMPLETE) { - // We do not want forever - if (expires == GSS_C_INDEFINITE) { - expires = 60; + if (maj == GSS_S_COMPLETE) { + // We do not want forever + if (expires == GSS_C_INDEFINITE) { + expires = 60; + } + ctx->d_expires = time(nullptr) + expires; + ctx->d_state = GssSecContext::GssStateComplete; + return true; + } + else if (maj != GSS_S_CONTINUE_NEEDED) { + processError("gss_accept_sec_context", maj, min); } - d_secctx->d_expires = time(nullptr) + expires; - d_secctx->d_state = GssSecContext::GssStateComplete; - return true; - } - else if (maj != GSS_S_CONTINUE_NEEDED) { - processError("gss_accept_sec_context", maj, min); } + return (maj == GSS_S_CONTINUE_NEEDED); }; @@ -408,7 +452,10 @@ bool GssContext::sign(const std::string& input, std::string& output) recv_tok.length = input.size(); recv_tok.value = const_cast(static_cast(input.c_str())); - maj = gss_get_mic(&min, d_secctx->d_ctx, GSS_C_QOP_DEFAULT, &recv_tok, &send_tok); + { + auto ctx = d_secctx->lock(); + maj = gss_get_mic(&min, ctx->d_ctx, GSS_C_QOP_DEFAULT, &recv_tok, &send_tok); + } if (send_tok.length > 0) { output.assign(static_cast(send_tok.value), send_tok.length); @@ -434,7 +481,10 @@ bool GssContext::verify(const std::string& input, const std::string& signature) sign_tok.length = signature.size(); sign_tok.value = const_cast(static_cast(signature.c_str())); - maj = gss_verify_mic(&min, d_secctx->d_ctx, &recv_tok, &sign_tok, nullptr); + { + auto ctx = d_secctx->lock(); + maj = gss_verify_mic(&min, ctx->d_ctx, &recv_tok, &sign_tok, nullptr); + } if (maj != GSS_S_COMPLETE) { processError("gss_get_mic", maj, min); @@ -473,20 +523,23 @@ bool GssContext::getPeerPrincipal(std::string& name) gss_buffer_desc value; OM_uint32 maj, min; - if (d_secctx->d_peer_name != GSS_C_NO_NAME) { - maj = gss_display_name(&min, d_secctx->d_peer_name, &value, nullptr); - if (maj == GSS_S_COMPLETE && value.length > 0) { - name.assign(static_cast(value.value), value.length); - maj = gss_release_buffer(&min, &value); - return true; + { + auto ctx = d_secctx->lock(); + if (ctx->d_peer_name != GSS_C_NO_NAME) { + maj = gss_display_name(&min, ctx->d_peer_name, &value, nullptr); + if (maj == GSS_S_COMPLETE && value.length > 0) { + name.assign(static_cast(value.value), value.length); + maj = gss_release_buffer(&min, &value); + return true; + } + else { + return false; + } } else { return false; } } - else { - return false; - } } std::tuple GssContext::getCounts() diff --git a/pdns/gss_context.hh b/pdns/gss_context.hh index e19873674f5d..e2f4f28815cb 100644 --- a/pdns/gss_context.hh +++ b/pdns/gss_context.hh @@ -28,6 +28,7 @@ #include "namespaces.hh" #include "pdnsexception.hh" #include "dns.hh" +#include "lock.hh" #include "logr.hh" #ifdef ENABLE_GSS_TSIG @@ -45,7 +46,8 @@ enum GssContextError GSS_CONTEXT_NOT_INITIALIZED, GSS_CONTEXT_INVALID, GSS_CONTEXT_EXPIRED, - GSS_CONTEXT_ALREADY_INITIALIZED + GSS_CONTEXT_ALREADY_INITIALIZED, + GSS_CONTEXT_LIMIT_REACHED, }; //! GSS context types @@ -57,6 +59,7 @@ enum GssContextType }; class GssSecContext; +class GssCredential; /*! Class for representing GSS names, such as host/host.domain.com@REALM. */ @@ -194,11 +197,15 @@ public: GssContextError getError(); // getErrorStrings() { return d_gss_errors; } // cred); #endif DNSName d_label; // d_gss_errors; // d_secctx; //> d_secctx; //empty()) - throw ResolverException("SOA query to '" + remote->toLogString() + "' produced response without domain name (RCode: " + RCode::to_s(mdp.d_header.rcode) + ")"); - - if(mdp.d_answers.empty()) - throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' produced no results (RCode: " + RCode::to_s(mdp.d_header.rcode) + ")"); - - if(mdp.d_qtype != QType::SOA) - throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' returned wrong record type"); - - if(mdp.d_header.rcode != 0) - throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' returned Rcode " + RCode::to_s(mdp.d_header.rcode)); - - *theirInception = *theirExpire = 0; - bool gotSOA=false; - for(const MOADNSParser::answers_t::value_type& drc : mdp.d_answers) { - if(drc.d_type == QType::SOA && drc.d_name == *domain) { - auto src = getRR(drc); - if (src) { - *theirSerial = src->d_st.serial; - gotSOA = true; - } + bool gotSOA{false}; + try { + MOADNSParser mdp(false, (char*)buf, err); + *id=mdp.d_header.id; + *domain = mdp.d_qname; + + if(domain->empty()) { + throw ResolverException("SOA query to '" + remote->toLogString() + "' produced response without domain name (RCode: " + RCode::to_s(mdp.d_header.rcode) + ")"); + } + + if(mdp.d_answers.empty()) { + throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' produced no results (RCode: " + RCode::to_s(mdp.d_header.rcode) + ")"); + } + + if(mdp.d_qtype != QType::SOA) { + throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' returned wrong record type"); } - if(drc.d_type == QType::RRSIG && drc.d_name == *domain) { - auto rrc = getRR(drc); - if(rrc && rrc->d_type == QType::SOA) { - *theirInception= std::max(*theirInception, rrc->d_siginception); - *theirExpire = std::max(*theirExpire, rrc->d_sigexpire); + + if(mdp.d_header.rcode != 0) { + throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' returned Rcode " + RCode::to_s(mdp.d_header.rcode)); + } + + *theirInception = *theirExpire = 0; + for(const MOADNSParser::answers_t::value_type& drc : mdp.d_answers) { + if(drc.d_type == QType::SOA && drc.d_name == *domain) { + auto src = getRR(drc); + if (src) { + *theirSerial = src->d_st.serial; + gotSOA = true; + } + } + if(drc.d_type == QType::RRSIG && drc.d_name == *domain) { + auto rrc = getRR(drc); + if(rrc && rrc->d_type == QType::SOA) { + *theirInception= std::max(*theirInception, rrc->d_siginception); + *theirExpire = std::max(*theirExpire, rrc->d_sigexpire); + } } } } - if(!gotSOA) + catch (const MOADNSException& exc) { + throw ResolverException("SOA Query to '" + remote->toLogString() + "' produced ill-formed response: " + exc.what()); + } + if(!gotSOA) { throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' did not return a SOA"); + } return true; } @@ -339,6 +349,9 @@ int Resolver::resolve(const ComboAddress& to, const DNSName &domain, int type, R catch(ResolverException &re) { throw ResolverException(re.reason+" from "+to.toLogString()); } + catch (const MOADNSException& exc) { + throw ResolverException(std::string(exc.what()) + " from " + to.toLogString()); + } } int Resolver::resolve(const ComboAddress& ipport, const DNSName &domain, int type, Resolver::res_t* res) { diff --git a/pdns/stubresolver.cc b/pdns/stubresolver.cc index a6da7039d121..f095616274b6 100644 --- a/pdns/stubresolver.cc +++ b/pdns/stubresolver.cc @@ -174,22 +174,30 @@ int stubDoResolve(Logr::log_t slog, const DNSName& qname, uint16_t qtype, vector catch (...) { continue; } - MOADNSParser mdp(false, reply); - if (mdp.d_header.rcode == RCode::ServFail) { - continue; - } - for (const auto& answer : mdp.d_answers) { - if (answer.d_place == 1 && answer.d_type == qtype) { - DNSZoneRecord zrr; - zrr.dr = answer; - zrr.auth = true; - ret.push_back(std::move(zrr)); + try { + MOADNSParser mdp(false, reply); + if (mdp.d_header.rcode == RCode::ServFail) { + continue; + } + + for (const auto& answer : mdp.d_answers) { + if (answer.d_place == 1 && answer.d_type == qtype) { + DNSZoneRecord zrr; + zrr.dr = answer; + zrr.auth = true; + ret.push_back(std::move(zrr)); + } } + SLOG(g_log << Logger::Debug << logPrefix << "Question for '" << queryNameType << "' got answered by " << dest.toString() << endl, + slog->info(Logr::Debug, "stub-resolver: got an answer", "query", Logging::Loggable(qname), "type", Logging::Loggable(QType(qtype)), "resolver", Logging::Loggable(dest))); + return mdp.d_header.rcode; + } + catch (const MOADNSException& exc) { + SLOG(g_log << Logger::Debug << logPrefix << "Question for '" << queryNameType << "' got ill-formed answer from " << dest.toString() << ": " << exc.what() << endl, + slog->error(Logr::Debug, exc.what(), "stub-resolver: got an ill-formed answer", "query", Logging::Loggable(qname), "type", Logging::Loggable(QType(qtype)), "resolver", Logging::Loggable(dest))); + continue; } - SLOG(g_log << Logger::Debug << logPrefix << "Question for '" << queryNameType << "' got answered by " << dest.toString() << endl, - slog->info(Logr::Debug, "stub-resolver: got an answer", "query", Logging::Loggable(qname), "type", Logging::Loggable(QType(qtype)), "resolver", Logging::Loggable(dest))); - return mdp.d_header.rcode; } return RCode::ServFail; } diff --git a/pdns/tcpreceiver.cc b/pdns/tcpreceiver.cc index 60b33fb22e87..4b8604d2d6a7 100644 --- a/pdns/tcpreceiver.cc +++ b/pdns/tcpreceiver.cc @@ -408,7 +408,7 @@ void TCPNameserver::doConnection(int fd, Logr::log_t slog) if (packet->couldBeCached()) { std::string view{}; if (g_views) { - Netmask netmask(packet->d_remote); + Netmask netmask(packet->getInnerRemote()); view = g_zoneCache.getViewFromNetwork(&netmask); } if (PC.get(*packet, *cached, view)) { // short circuit - does the PacketCache recognize this question? diff --git a/regression-tests.auth-py/test_ProxyProtocol.py b/regression-tests.auth-py/test_ProxyProtocol.py index f438aec73025..a92d88443226 100644 --- a/regression-tests.auth-py/test_ProxyProtocol.py +++ b/regression-tests.auth-py/test_ProxyProtocol.py @@ -4,6 +4,7 @@ import socket import struct import subprocess +import unittest from authtests import AuthTest from proxyprotocol import ProxyProtocol @@ -267,3 +268,74 @@ def testAXFR(self): res = dns.message.from_wire(data) self.assertRcodeEqual(res, expectedrcode) + + +class TestProxyProtocolViews(AuthTest): + _config_template = """ +launch={backend} +proxy-protocol-from=127.0.0.1 +""" + + @classmethod + def setUpClass(cls): + super(TestProxyProtocolViews, cls).setUpClass() + + if cls._backend == "lmdb": + os.system("$PDNSUTIL --config-dir=configs/auth zone create views.test") + os.system( + "$PDNSUTIL --config-dir=configs/auth rrset replace views.test views.test SOA 'no.where no.where 1 10800 3600 604800 3600'" + ) + os.system("$PDNSUTIL --config-dir=configs/auth zone create views.test..squint") + os.system( + "$PDNSUTIL --config-dir=configs/auth rrset replace views.test..squint views.test SOA 'no.where no.where 1 10800 3600 604800 3600'" + ) + os.system("$PDNSUTIL --config-dir=configs/auth rrset add views.test..squint a.views.test A 1.2.3.4") + os.system("$PDNSUTIL --config-dir=configs/auth network set 192.168.0.0/16 squinted") + os.system("$PDNSUTIL --config-dir=configs/auth view add-zone squinted views.test..squint") + os.system("$PDNSCONTROL --socket-dir=configs/auth rediscover") + + def testViews(self): + """ + Check that views correctly operate on the PROXY header inner address + """ + + if self._backend == "lmdb": + query = dns.message.make_query("a.views.test", "A") + + queryPayload = query.to_wire() + + for task in ( + ("192.0.2.1", dns.rcode.NXDOMAIN), + ("192.168.2.53", dns.rcode.NOERROR), + ): + ip, expectedrcode = task + + ppPayload = ProxyProtocol.getPayload(False, True, False, ip, "10.1.2.3", 12345, 53, []) + + # TCP + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(2.0) + sock.connect(("127.0.0.1", self._authPort)) + + try: + sock.send(ppPayload) + sock.send(struct.pack("!H", len(queryPayload))) + sock.send(queryPayload) + data = sock.recv(2) + if data: + (datalen,) = struct.unpack("!H", data) + data = sock.recv(datalen) + except socket.timeout as e: + print("Timeout: %s" % (str(e))) + data = None + except socket.error as e: + print("Network error: %s" % (str(e))) + data = None + finally: + sock.close() + + res = None + if data: + res = dns.message.from_wire(data) + + self.assertRcodeEqual(res, expectedrcode)