• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PowerDNS / pdns / 17210411594

25 Aug 2025 01:33PM UTC coverage: 65.973% (+0.2%) from 65.814%
17210411594

Pull #15874

github

web-flow
Merge 6ac7c5514 into 9eeac00a7
Pull Request #15874: dnsdist: Only check the freshness of the configuration when needed

42140 of 92460 branches covered (45.58%)

Branch coverage included in aggregate %.

59 of 73 new or added lines in 19 files covered. (80.82%)

5421 existing lines in 75 files now uncovered.

128091 of 165573 relevant lines covered (77.36%)

4808876.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

39.72
/modules/ldapbackend/native.cc
1
/*
2
 * This file is part of PowerDNS or dnsdist.
3
 * Copyright -- PowerDNS.COM B.V. and its contributors
4
 * originally authored by Norbert Sendetzky
5
 *
6
 * This program is free software; you can redistribute it and/or modify
7
 * it under the terms of version 2 of the GNU General Public License as
8
 * published by the Free Software Foundation.
9
 *
10
 * In addition, for the avoidance of any doubt, permission is granted to
11
 * link this program with OpenSSL and to (re)distribute the binaries
12
 * produced as the result of such linking.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program; if not, write to the Free Software
21
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
 */
23
#include "exceptions.hh"
24
#include "ldapbackend.hh"
25
#include <cstdlib>
26

27
/*
28
 *  Known DNS RR types
29
 *  Types which aren't active are currently not supported by PDNS
30
 */
31

32
static const char* ldap_attrany[] = { // NOLINT(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
33
  "associatedDomain", // needs to be first, code below depends on this
34
  "dNSTTL",
35
  "ALIASRecord",
36
  "aRecord",
37
  "nSRecord",
38
  "cNAMERecord",
39
  "sOARecord",
40
  "pTRRecord",
41
  "hInfoRecord",
42
  "mXRecord",
43
  "tXTRecord",
44
  "rPRecord",
45
  "aFSDBRecord",
46
  //  "SigRecord",
47
  "KeyRecord",
48
  //  "gPosRecord",
49
  "aAAARecord",
50
  "lOCRecord",
51
  "sRVRecord",
52
  "nAPTRRecord",
53
  "kXRecord",
54
  "certRecord",
55
  //  "a6Record",
56
  "dNameRecord",
57
  //  "aPLRecord",
58
  "dSRecord",
59
  "sSHFPRecord",
60
  "iPSecKeyRecord",
61
  "rRSIGRecord",
62
  "nSECRecord",
63
  "dNSKeyRecord",
64
  "dHCIDRecord",
65
  "nSEC3Record",
66
  "nSEC3PARAMRecord",
67
  "tLSARecord",
68
  "cDSRecord",
69
  "cDNSKeyRecord",
70
  "openPGPKeyRecord",
71
  "SVCBRecord",
72
  "HTTPSRecord",
73
  "sPFRecord",
74
  "EUI48Record",
75
  "EUI64Record",
76
  "tKeyRecord",
77
  "uRIRecord",
78
  "cAARecord",
79
  "TYPE65226Record",
80
  "TYPE65534Record",
81
  "modifyTimestamp",
82
  "PdnsRecordTTL",
83
  "PdnsRecordAuth",
84
  "PdnsRecordOrdername",
85
  nullptr};
86

87
bool LdapBackend::list(const ZoneName& target, domainid_t domain_id, bool /* include_disabled */)
88
{
×
89
  try {
×
90
    d_in_list = true;
×
91
    d_qname = target.operator const DNSName&();
×
92
    d_qtype = QType::ANY;
×
93
    d_results_cache.clear();
×
94

95
    return (this->*d_list_fcnt)(target, domain_id);
×
96
  }
×
97
  catch (LDAPTimeout& lt) {
×
98
    g_log << Logger::Warning << d_myname << " Unable to get zone " << target << " from LDAP directory: " << lt.what() << endl;
×
99
    throw DBException("LDAP server timeout");
×
100
  }
×
101
  catch (LDAPNoConnection& lnc) {
×
102
    g_log << Logger::Warning << d_myname << " Connection to LDAP lost, trying to reconnect" << endl;
×
103
    if (reconnect())
×
104
      this->list(target, domain_id);
×
105
    else
×
106
      throw PDNSException("Failed to reconnect to LDAP server");
×
107
  }
×
108
  catch (LDAPException& le) {
×
109
    g_log << Logger::Error << d_myname << " Unable to get zone " << target << " from LDAP directory: " << le.what() << endl;
×
110
    throw PDNSException("LDAP server unreachable"); // try to reconnect to another server
×
111
  }
×
112
  catch (std::exception& e) {
×
113
    g_log << Logger::Error << d_myname << " Caught STL exception for target " << target << ": " << e.what() << endl;
×
114
    throw DBException("STL exception");
×
115
  }
×
116

117
  return false;
×
118
}
×
119

120
bool LdapBackend::list_simple(const ZoneName& target, domainid_t /* domain_id */)
121
{
×
122
  string dn;
×
123
  string filter;
×
124
  string qesc;
×
125

126
  dn = getArg("basedn");
×
127
  qesc = toLower(d_pldap->escape(target.toStringRootDot()));
×
128

129
  // search for SOARecord of target
130
  filter = strbind(":target:", "&(associatedDomain=" + qesc + ")(sOARecord=*)", getArg("filter-axfr"));
×
131
  PowerLDAP::SearchResult::Ptr search = d_pldap->search(dn, LDAP_SCOPE_SUBTREE, filter, (const char**)ldap_attrany);
×
132
  if (!search->getNext(d_result, true))
×
133
    return false;
×
134

135
  if (d_result.count("dn") && !d_result["dn"].empty()) {
×
136
    if (!mustDo("basedn-axfr-override")) {
×
137
      dn = d_result["dn"][0];
×
138
    }
×
139
  }
×
140

141
  // If we have any records associated with this entry let's parse them here
142
  DNSResult soa_result;
×
143
  soa_result.ttl = d_default_ttl;
×
144
  soa_result.lastmod = 0;
×
145
  this->extract_common_attributes(soa_result);
×
146
  this->extract_entry_results(d_qname, soa_result, QType(uint16_t(QType::ANY)));
×
147

148
  filter = strbind(":target:", "associatedDomain=*." + qesc, getArg("filter-axfr"));
×
149
  g_log << Logger::Debug << d_myname << " Search = basedn: " << dn << ", filter: " << filter << endl;
×
150
  d_search = d_pldap->search(dn, LDAP_SCOPE_SUBTREE, filter, (const char**)ldap_attrany);
×
151

152
  return true;
×
153
}
×
154

155
bool LdapBackend::list_strict(const ZoneName& target, domainid_t domain_id)
156
{
×
157
  static const DNSName inaddrarpa("in-addr.arpa");
×
158
  static const DNSName ip6arpa("ip6.arpa");
×
159
  if (target.isPartOf(inaddrarpa) || target.isPartOf(ip6arpa)) {
×
160
    g_log << Logger::Warning << d_myname << " Request for reverse zone AXFR, but this is not supported in strict mode" << endl;
×
UNCOV
161
    return false; // AXFR isn't supported in strict mode. Use simple mode and additional PTR records
×
162
  }
×
163

UNCOV
164
  return list_simple(target, domain_id);
×
UNCOV
165
}
×
166

167
void LdapBackend::lookup(const QType& qtype, const DNSName& qname, domainid_t zoneid, DNSPacket* dnspkt)
168
{
1,253✔
169
  try {
1,253✔
170
    d_in_list = false;
1,253✔
171
    d_qname = qname;
1,253✔
172
    d_qtype = qtype;
1,253✔
173
    d_results_cache.clear();
1,253✔
174

175
    if (d_qlog) {
1,253!
176
      g_log.log("Query: '" + qname.toStringRootDot() + "|" + qtype.toString() + "'", Logger::Error);
1,253✔
177
    }
1,253✔
178
    (this->*d_lookup_fcnt)(qtype, qname, dnspkt, zoneid);
1,253✔
179
  }
1,253✔
180
  catch (LDAPTimeout& lt) {
1,253✔
181
    g_log << Logger::Warning << d_myname << " Unable to search LDAP directory: " << lt.what() << endl;
×
UNCOV
182
    throw DBException("LDAP server timeout");
×
183
  }
×
184
  catch (LDAPNoConnection& lnc) {
1,253✔
185
    g_log << Logger::Warning << d_myname << " Connection to LDAP lost, trying to reconnect" << endl;
×
186
    if (reconnect())
×
187
      this->lookup(qtype, qname, zoneid, dnspkt);
×
188
    else
×
UNCOV
189
      throw PDNSException("Failed to reconnect to LDAP server");
×
190
  }
×
191
  catch (LDAPException& le) {
1,253✔
192
    g_log << Logger::Error << d_myname << " Unable to search LDAP directory: " << le.what() << endl;
×
UNCOV
193
    throw PDNSException("LDAP server unreachable"); // try to reconnect to another server
×
194
  }
×
195
  catch (std::exception& e) {
1,253✔
196
    g_log << Logger::Error << d_myname << " Caught STL exception for qname " << qname << ": " << e.what() << endl;
×
UNCOV
197
    throw DBException("STL exception");
×
UNCOV
198
  }
×
199
}
1,253✔
200

201
void LdapBackend::lookup_simple(const QType& qtype, const DNSName& qname, DNSPacket* /* dnspkt */, domainid_t /* zoneid */)
202
{
419✔
203
  string filter, attr, qesc;
419✔
204
  const char** attributes = ldap_attrany + 1; // skip associatedDomain
419✔
205
  const char* attronly[] = {NULL, "dNSTTL", "modifyTimestamp", "PdnsRecordTTL", "PdnsRecordAuth", "PdnsRecordOrdername", NULL};
419✔
206

207
  qesc = toLower(d_pldap->escape(qname.toStringRootDot()));
419✔
208
  filter = "associatedDomain=" + qesc;
419✔
209

210
  if (qtype.getCode() != QType::ANY) {
419✔
211
    attr = qtype.toString() + "Record";
197✔
212
    filter = "&(" + filter + ")(" + attr + "=*)";
197✔
213
    attronly[0] = attr.c_str();
197✔
214
    attributes = attronly;
197✔
215
  }
197✔
216

217
  filter = strbind(":target:", filter, getArg("filter-lookup"));
419✔
218

219
  g_log << Logger::Debug << d_myname << " Search = basedn: " << getArg("basedn") << ", filter: " << filter << ", qtype: " << qtype.toString() << endl;
419✔
220
  d_search = d_pldap->search(getArg("basedn"), LDAP_SCOPE_SUBTREE, filter, attributes);
419✔
221
}
419✔
222

223
void LdapBackend::lookup_strict(const QType& qtype, const DNSName& qname, DNSPacket* /* dnspkt */, domainid_t /* zoneid */)
224
{
418✔
225
  int len;
418✔
226
  vector<string> parts;
418✔
227
  string filter, attr, qesc;
418✔
228
  const char** attributes = ldap_attrany + 1; // skip associatedDomain
418✔
229
  const char* attronly[] = {NULL, "dNSTTL", "modifyTimestamp", "PdnsRecordTTL", "PdnsRecordAuth", "PdnsRecordOrdername", NULL};
418✔
230

231
  qesc = toLower(d_pldap->escape(qname.toStringRootDot()));
418✔
232
  stringtok(parts, qesc, ".");
418✔
233
  len = qesc.length();
418✔
234

235
  if (parts.size() == 6 && len > 13 && qesc.substr(len - 13, 13) == ".in-addr.arpa") // IPv4 reverse lookups
418!
236
  {
2✔
237
    filter = "aRecord=" + ptr2ip4(parts);
2✔
238
    attronly[0] = "associatedDomain";
2✔
239
    attributes = attronly;
2✔
240
  }
2✔
241
  else if (parts.size() == 34 && len > 9 && (qesc.substr(len - 9, 9) == ".ip6.arpa")) // IPv6 reverse lookups
416!
242
  {
×
243
    filter = "aAAARecord=" + ptr2ip6(parts);
×
244
    attronly[0] = "associatedDomain";
×
UNCOV
245
    attributes = attronly;
×
UNCOV
246
  }
×
247
  else // IPv4 and IPv6 lookups
416✔
248
  {
416✔
249
    filter = "associatedDomain=" + qesc;
416✔
250
  }
416✔
251

252
  if (qtype.getCode() != QType::ANY) {
418✔
253
    attr = qtype.toString() + "Record";
197✔
254
    filter = "&(" + filter + ")(" + attr + "=*)";
197✔
255
    attronly[0] = attr.c_str();
197✔
256
    attributes = attronly;
197✔
257
  }
197✔
258

259
  filter = strbind(":target:", filter, getArg("filter-lookup"));
418✔
260

261
  g_log << Logger::Debug << d_myname << " Search = basedn: " << getArg("basedn") << ", filter: " << filter << ", qtype: " << qtype.toString() << endl;
418✔
262
  d_search = d_pldap->search(getArg("basedn"), LDAP_SCOPE_SUBTREE, filter, attributes);
418✔
263
}
418✔
264

265
void LdapBackend::lookup_tree(const QType& qtype, const DNSName& qname, DNSPacket* /* dnspkt */, domainid_t /* zoneid */)
266
{
416✔
267
  string filter, attr, qesc, dn;
416✔
268
  const char** attributes = ldap_attrany + 1; // skip associatedDomain
416✔
269
  const char* attronly[] = {NULL, "dNSTTL", "modifyTimestamp", "PdnsRecordTTL", "PdnsRecordAuth", "PdnsRecordOrdername", NULL};
416✔
270
  vector<string> parts;
416✔
271

272
  qesc = toLower(d_pldap->escape(qname.toStringRootDot()));
416✔
273
  filter = "associatedDomain=" + qesc;
416✔
274

275
  if (qtype.getCode() != QType::ANY) {
416✔
276
    attr = qtype.toString() + "Record";
196✔
277
    filter = "&(" + filter + ")(" + attr + "=*)";
196✔
278
    attronly[0] = attr.c_str();
196✔
279
    attributes = attronly;
196✔
280
  }
196✔
281

282
  filter = strbind(":target:", filter, getArg("filter-lookup"));
416✔
283

284
  stringtok(parts, toLower(qname.toString()), ".");
416✔
285
  for (auto i = parts.crbegin(); i != parts.crend(); i++) {
2,877✔
286
    dn = "dc=" + *i + "," + dn;
2,461✔
287
  }
2,461✔
288

289
  g_log << Logger::Debug << d_myname << " Search = basedn: " << dn + getArg("basedn") << ", filter: " << filter << ", qtype: " << qtype.toString() << endl;
416✔
290
  d_search = d_pldap->search(dn + getArg("basedn"), LDAP_SCOPE_BASE, filter, attributes);
416✔
291
}
416✔
292

293
bool LdapBackend::get(DNSResourceRecord& rr)
294
{
2,085✔
295
  if (d_results_cache.empty()) {
2,085✔
296
    while (d_results_cache.empty()) {
2,012✔
297
      bool exhausted = false;
1,628✔
298
      bool valid_entry_found = false;
1,628✔
299

300
      while (!valid_entry_found && !exhausted) {
3,256✔
301
        try {
1,628✔
302
          exhausted = !d_search->getNext(d_result, true);
1,628✔
303
        }
1,628✔
304
        catch (LDAPException& le) {
1,628✔
305
          g_log << Logger::Error << d_myname << " Failed to get next result: " << le.what() << endl;
×
UNCOV
306
          throw PDNSException("Get next result impossible");
×
UNCOV
307
        }
×
308

309
        if (!exhausted) {
1,628✔
310
          if (!d_in_list) {
384!
311
            // All entries are valid here
312
            valid_entry_found = true;
384✔
313
          }
384✔
UNCOV
314
          else {
×
315
            // If we're called after list() then the entry *must* contain
316
            // associatedDomain, otherwise let's just skip it
317
            if (d_result.count("associatedDomain"))
×
UNCOV
318
              valid_entry_found = true;
×
UNCOV
319
          }
×
320
        }
384✔
321
      }
1,628✔
322

323
      if (exhausted) {
1,628✔
324
        break;
1,244✔
325
      }
1,244✔
326

327
      DNSResult result_template;
384✔
328
      result_template.ttl = d_default_ttl;
384✔
329
      result_template.lastmod = 0;
384✔
330
      this->extract_common_attributes(result_template);
384✔
331

332
      std::vector<std::string> associatedDomains;
384✔
333

334
      if (d_result.count("associatedDomain")) {
384✔
335
        if (d_in_list) {
3!
336
          // We can have more than one associatedDomain in the entry, so for each of them we have to check
337
          // that they are indeed under the domain we've been asked to list (nothing enforces this, so you
338
          // can have one associatedDomain set to "host.first-domain.com" and another one set to
339
          // "host.second-domain.com"). Better not return the latter I guess :)
340
          // We also have to generate one DNSResult per DNS-relevant attribute. As we've asked only for them
341
          // and the others above we've already cleaned it's just a matter of iterating over them.
342

UNCOV
343
          unsigned int axfrqlen = d_qname.toStringRootDot().length();
×
344
          for (auto i = d_result["associatedDomain"].begin(); i != d_result["associatedDomain"].end(); ++i) {
×
345
            // Sanity checks: is this associatedDomain attribute under the requested domain?
346
            if (i->size() >= axfrqlen && i->substr(i->size() - axfrqlen, axfrqlen) == d_qname.toStringRootDot())
×
347
              associatedDomains.push_back(*i);
×
UNCOV
348
          }
×
UNCOV
349
        }
×
350
        else {
3✔
351
          // This was a lookup in strict mode, so we add the reverse lookup
352
          // information manually.
353
          d_result["pTRRecord"] = d_result["associatedDomain"];
3✔
354
        }
3✔
355
      }
3✔
356

357
      if (d_in_list) {
384!
358
        for (const auto& domain : associatedDomains)
×
UNCOV
359
          this->extract_entry_results(DNSName(domain), result_template, QType(uint16_t(QType::ANY)));
×
UNCOV
360
      }
×
361
      else {
384✔
362
        this->extract_entry_results(d_qname, result_template, QType(uint16_t(QType::ANY)));
384✔
363
      }
384✔
364
    }
384✔
365

366
    if (d_results_cache.empty())
1,628✔
367
      return false;
1,244✔
368
  }
1,628✔
369

370
  DNSResult result = d_results_cache.back();
841✔
371
  d_results_cache.pop_back();
841✔
372
  rr.qtype = result.qtype;
841✔
373
  rr.qname = result.qname;
841✔
374
  rr.ttl = result.ttl;
841✔
375
  rr.last_modified = 0;
841✔
376
  rr.content = result.value;
841✔
377
  rr.auth = result.auth;
841✔
378

379
  g_log << Logger::Debug << d_myname << " Record = qname: " << rr.qname << ", qtype: " << (rr.qtype).toString() << ", ttl: " << rr.ttl << ", content: " << rr.content << endl;
841✔
380
  return true;
841✔
381
}
2,085✔
382

383
void LdapBackend::lookupEnd()
384
{
9✔
385
  d_results_cache.clear();
9✔
386
}
9✔
387

388
bool LdapBackend::getDomainInfo(const ZoneName& domain, DomainInfo& info, bool /* getSerial */)
389
{
×
390
  string filter;
×
391
  SOAData sd;
×
392
  PowerLDAP::sentry_t result;
×
393
  const char* attronly[] = {
×
UNCOV
394
    "sOARecord",
×
395
    "PdnsDomainId",
×
UNCOV
396
    "PdnsDomainNotifiedSerial",
×
397
    "PdnsDomainLastCheck",
×
398
    "PdnsDomainMaster",
×
399
    "PdnsDomainType",
×
400
    NULL};
×
401

402
  try {
×
403
    // search for SOARecord of domain
404
    filter = "(&(associatedDomain=" + toLower(d_pldap->escape(domain.toStringRootDot())) + ")(SOARecord=*))";
×
405
    d_search = d_pldap->search(getArg("basedn"), LDAP_SCOPE_SUBTREE, filter, attronly);
×
406
    if (!d_search->getNext(result)) {
×
407
      return false;
×
408
    }
×
409
  }
×
410
  catch (LDAPTimeout& lt) {
×
411
    g_log << Logger::Warning << d_myname << " Unable to search LDAP directory: " << lt.what() << endl;
×
412
    throw DBException("LDAP server timeout");
×
413
  }
×
414
  catch (LDAPNoConnection& lnc) {
×
415
    g_log << Logger::Warning << d_myname << " Connection to LDAP lost, trying to reconnect" << endl;
×
416
    if (reconnect())
×
417
      this->getDomainInfo(domain, info);
×
418
    else
×
419
      throw PDNSException("Failed to reconnect to LDAP server");
×
420
  }
×
UNCOV
421
  catch (LDAPException& le) {
×
422
    g_log << Logger::Error << d_myname << " Unable to search LDAP directory: " << le.what() << endl;
×
423
    throw PDNSException("LDAP server unreachable"); // try to reconnect to another server
×
424
  }
×
UNCOV
425
  catch (std::exception& e) {
×
426
    throw DBException("STL exception");
×
427
  }
×
428

429
  if (result.count("sOARecord") && !result["sOARecord"].empty()) {
×
UNCOV
430
    sd.serial = 0;
×
431
    fillSOAData(result["sOARecord"][0], sd);
×
432

UNCOV
433
    if (result.count("PdnsDomainId") && !result["PdnsDomainId"].empty())
×
434
      info.id = static_cast<domainid_t>(std::stoll(result["PdnsDomainId"][0]));
×
435
    else
×
436
      info.id = UnknownDomainID;
×
437

UNCOV
438
    info.serial = sd.serial;
×
439
    info.zone = domain;
×
440

441
    if (result.count("PdnsDomainLastCheck") && !result["PdnsDomainLastCheck"].empty())
×
442
      pdns::checked_stoi_into(info.last_check, result["PdnsDomainLastCheck"][0]);
×
UNCOV
443
    else
×
444
      info.last_check = 0;
×
445

446
    if (result.count("PdnsDomainNotifiedSerial") && !result["PdnsDomainNotifiedSerial"].empty())
×
447
      pdns::checked_stoi_into(info.notified_serial, result["PdnsDomainNotifiedSerial"][0]);
×
UNCOV
448
    else
×
449
      info.notified_serial = 0;
×
450

451
    if (result.count("PdnsDomainMaster") && !result["PdnsDomainMaster"].empty()) {
×
452
      for (const auto& m : result["PdnsDomainMaster"])
×
453
        info.primaries.emplace_back(m, 53);
×
454
    }
×
455

456
    if (result.count("PdnsDomainType") && !result["PdnsDomainType"].empty()) {
×
457
      string kind = result["PdnsDomainType"][0];
×
458
      if (kind == "master")
×
459
        info.kind = DomainInfo::Primary;
×
460
      else if (kind == "slave")
×
UNCOV
461
        info.kind = DomainInfo::Secondary;
×
462
      else
×
463
        info.kind = DomainInfo::Native;
×
464
    }
×
UNCOV
465
    else {
×
466
      info.kind = DomainInfo::Native;
×
467
    }
×
468

UNCOV
469
    info.backend = this;
×
UNCOV
470
    return true;
×
UNCOV
471
  }
×
472

UNCOV
473
  return false;
×
UNCOV
474
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc