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

PowerDNS / pdns / 18575936356

16 Oct 2025 07:14AM UTC coverage: 59.969% (-13.0%) from 72.964%
18575936356

push

github

web-flow
Merge pull request #16265 from rgacogne/warn-release-workflows

Warn about workflows that needs to be backported to release branches

68886 of 181932 branches covered (37.86%)

Branch coverage included in aggregate %.

149660 of 182501 relevant lines covered (82.01%)

6578565.1 hits per line

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

29.55
/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
      return this->list(target, domain_id);
105
    }
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;
161
    return false; // AXFR isn't supported in strict mode. Use simple mode and additional PTR records
162
  }
×
163

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

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

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

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

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

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

417✔
218
  filter = strbind(":target:", filter, getArg("filter-lookup"));
417✔
219

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

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

416✔
232
  qesc = toLower(d_pldap->escape(qname.toStringRootDot()));
233
  stringtok(parts, qesc, ".");
416!
234
  len = qesc.length();
2!
235

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

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

416✔
260
  filter = strbind(":target:", filter, getArg("filter-lookup"));
416✔
261

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

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

×
273
  qesc = toLower(d_pldap->escape(qname.toStringRootDot()));
414✔
274
  filter = "associatedDomain=" + qesc;
196✔
275

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

414!
283
  filter = strbind(":target:", filter, getArg("filter-lookup"));
2,867✔
284

2,453!
285
  stringtok(parts, toLower(qname.toString()), ".");
2,453✔
286
  for (auto i = parts.crbegin(); i != parts.crend(); i++) {
×
287
    dn = "dc=" + *i + "," + dn;
414✔
288
  }
414✔
289

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

2,076✔
294
bool LdapBackend::get(DNSResourceRecord& rr)
2,003✔
295
{
1,625!
296
  if (d_results_cache.empty()) {
1,625!
297
    while (d_results_cache.empty()) {
×
298
      bool exhausted = false;
3,250✔
299
      bool valid_entry_found = false;
1,625✔
300

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

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

1,247✔
324
      if (exhausted) {
×
325
        break;
378✔
326
      }
378✔
327

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

378✔
333
      std::vector<std::string> associatedDomains;
3!
334

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

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

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

1,625!
367
    if (d_results_cache.empty())
×
368
      return false;
829✔
369
  }
829✔
370

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

2,076!
380
  g_log << Logger::Debug << d_myname << " Record = qname: " << rr.qname << ", qtype: " << (rr.qtype).toString() << ", ttl: " << rr.ttl << ", content: " << rr.content << endl;
381
  return true;
382
}
×
383

384
void LdapBackend::lookupEnd()
×
385
{
×
386
  d_results_cache.clear();
×
387
}
×
388

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

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

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

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

439
    info.serial = sd.serial;
×
440
    info.zone = domain;
441

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

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

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

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

470
    info.backend = this;
471
    return true;
472
  }
473

474
  return false;
475
}
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