]> git.ipfire.org Git - thirdparty/pdns.git/blob - modules/ldapbackend/native.cc
Merge pull request #15791 from miodvallat/udon
[thirdparty/pdns.git] / 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 if (target.isPartOf(DNSName("in-addr.arpa")) || target.isPartOf(DNSName("ip6.arpa"))) {
158 g_log << Logger::Warning << d_myname << " Request for reverse zone AXFR, but this is not supported in strict mode" << endl;
159 return false; // AXFR isn't supported in strict mode. Use simple mode and additional PTR records
160 }
161
162 return list_simple(target, domain_id);
163 }
164
165 void LdapBackend::lookup(const QType& qtype, const DNSName& qname, domainid_t zoneid, DNSPacket* dnspkt)
166 {
167 try {
168 d_in_list = false;
169 d_qname = qname;
170 d_qtype = qtype;
171 d_results_cache.clear();
172
173 if (d_qlog) {
174 g_log.log("Query: '" + qname.toStringRootDot() + "|" + qtype.toString() + "'", Logger::Error);
175 }
176 (this->*d_lookup_fcnt)(qtype, qname, dnspkt, zoneid);
177 }
178 catch (LDAPTimeout& lt) {
179 g_log << Logger::Warning << d_myname << " Unable to search LDAP directory: " << lt.what() << endl;
180 throw DBException("LDAP server timeout");
181 }
182 catch (LDAPNoConnection& lnc) {
183 g_log << Logger::Warning << d_myname << " Connection to LDAP lost, trying to reconnect" << endl;
184 if (reconnect())
185 this->lookup(qtype, qname, zoneid, dnspkt);
186 else
187 throw PDNSException("Failed to reconnect to LDAP server");
188 }
189 catch (LDAPException& le) {
190 g_log << Logger::Error << d_myname << " Unable to search LDAP directory: " << le.what() << endl;
191 throw PDNSException("LDAP server unreachable"); // try to reconnect to another server
192 }
193 catch (std::exception& e) {
194 g_log << Logger::Error << d_myname << " Caught STL exception for qname " << qname << ": " << e.what() << endl;
195 throw DBException("STL exception");
196 }
197 }
198
199 void LdapBackend::lookup_simple(const QType& qtype, const DNSName& qname, DNSPacket* /* dnspkt */, domainid_t /* zoneid */)
200 {
201 string filter, attr, qesc;
202 const char** attributes = ldap_attrany + 1; // skip associatedDomain
203 const char* attronly[] = {NULL, "dNSTTL", "modifyTimestamp", "PdnsRecordTTL", "PdnsRecordAuth", "PdnsRecordOrdername", NULL};
204
205 qesc = toLower(d_pldap->escape(qname.toStringRootDot()));
206 filter = "associatedDomain=" + qesc;
207
208 if (qtype.getCode() != QType::ANY) {
209 attr = qtype.toString() + "Record";
210 filter = "&(" + filter + ")(" + attr + "=*)";
211 attronly[0] = attr.c_str();
212 attributes = attronly;
213 }
214
215 filter = strbind(":target:", filter, getArg("filter-lookup"));
216
217 g_log << Logger::Debug << d_myname << " Search = basedn: " << getArg("basedn") << ", filter: " << filter << ", qtype: " << qtype.toString() << endl;
218 d_search = d_pldap->search(getArg("basedn"), LDAP_SCOPE_SUBTREE, filter, attributes);
219 }
220
221 void LdapBackend::lookup_strict(const QType& qtype, const DNSName& qname, DNSPacket* /* dnspkt */, domainid_t /* zoneid */)
222 {
223 int len;
224 vector<string> parts;
225 string filter, attr, qesc;
226 const char** attributes = ldap_attrany + 1; // skip associatedDomain
227 const char* attronly[] = {NULL, "dNSTTL", "modifyTimestamp", "PdnsRecordTTL", "PdnsRecordAuth", "PdnsRecordOrdername", NULL};
228
229 qesc = toLower(d_pldap->escape(qname.toStringRootDot()));
230 stringtok(parts, qesc, ".");
231 len = qesc.length();
232
233 if (parts.size() == 6 && len > 13 && qesc.substr(len - 13, 13) == ".in-addr.arpa") // IPv4 reverse lookups
234 {
235 filter = "aRecord=" + ptr2ip4(parts);
236 attronly[0] = "associatedDomain";
237 attributes = attronly;
238 }
239 else if (parts.size() == 34 && len > 9 && (qesc.substr(len - 9, 9) == ".ip6.arpa")) // IPv6 reverse lookups
240 {
241 filter = "aAAARecord=" + ptr2ip6(parts);
242 attronly[0] = "associatedDomain";
243 attributes = attronly;
244 }
245 else // IPv4 and IPv6 lookups
246 {
247 filter = "associatedDomain=" + qesc;
248 }
249
250 if (qtype.getCode() != QType::ANY) {
251 attr = qtype.toString() + "Record";
252 filter = "&(" + filter + ")(" + attr + "=*)";
253 attronly[0] = attr.c_str();
254 attributes = attronly;
255 }
256
257 filter = strbind(":target:", filter, getArg("filter-lookup"));
258
259 g_log << Logger::Debug << d_myname << " Search = basedn: " << getArg("basedn") << ", filter: " << filter << ", qtype: " << qtype.toString() << endl;
260 d_search = d_pldap->search(getArg("basedn"), LDAP_SCOPE_SUBTREE, filter, attributes);
261 }
262
263 void LdapBackend::lookup_tree(const QType& qtype, const DNSName& qname, DNSPacket* /* dnspkt */, domainid_t /* zoneid */)
264 {
265 string filter, attr, qesc, dn;
266 const char** attributes = ldap_attrany + 1; // skip associatedDomain
267 const char* attronly[] = {NULL, "dNSTTL", "modifyTimestamp", "PdnsRecordTTL", "PdnsRecordAuth", "PdnsRecordOrdername", NULL};
268 vector<string> parts;
269
270 qesc = toLower(d_pldap->escape(qname.toStringRootDot()));
271 filter = "associatedDomain=" + qesc;
272
273 if (qtype.getCode() != QType::ANY) {
274 attr = qtype.toString() + "Record";
275 filter = "&(" + filter + ")(" + attr + "=*)";
276 attronly[0] = attr.c_str();
277 attributes = attronly;
278 }
279
280 filter = strbind(":target:", filter, getArg("filter-lookup"));
281
282 stringtok(parts, toLower(qname.toString()), ".");
283 for (auto i = parts.crbegin(); i != parts.crend(); i++) {
284 dn = "dc=" + *i + "," + dn;
285 }
286
287 g_log << Logger::Debug << d_myname << " Search = basedn: " << dn + getArg("basedn") << ", filter: " << filter << ", qtype: " << qtype.toString() << endl;
288 d_search = d_pldap->search(dn + getArg("basedn"), LDAP_SCOPE_BASE, filter, attributes);
289 }
290
291 bool LdapBackend::get(DNSResourceRecord& rr)
292 {
293 if (d_results_cache.empty()) {
294 while (d_results_cache.empty()) {
295 bool exhausted = false;
296 bool valid_entry_found = false;
297
298 while (!valid_entry_found && !exhausted) {
299 try {
300 exhausted = !d_search->getNext(d_result, true);
301 }
302 catch (LDAPException& le) {
303 g_log << Logger::Error << d_myname << " Failed to get next result: " << le.what() << endl;
304 throw PDNSException("Get next result impossible");
305 }
306
307 if (!exhausted) {
308 if (!d_in_list) {
309 // All entries are valid here
310 valid_entry_found = true;
311 }
312 else {
313 // If we're called after list() then the entry *must* contain
314 // associatedDomain, otherwise let's just skip it
315 if (d_result.count("associatedDomain"))
316 valid_entry_found = true;
317 }
318 }
319 }
320
321 if (exhausted) {
322 break;
323 }
324
325 DNSResult result_template;
326 result_template.ttl = d_default_ttl;
327 result_template.lastmod = 0;
328 this->extract_common_attributes(result_template);
329
330 std::vector<std::string> associatedDomains;
331
332 if (d_result.count("associatedDomain")) {
333 if (d_in_list) {
334 // We can have more than one associatedDomain in the entry, so for each of them we have to check
335 // that they are indeed under the domain we've been asked to list (nothing enforces this, so you
336 // can have one associatedDomain set to "host.first-domain.com" and another one set to
337 // "host.second-domain.com"). Better not return the latter I guess :)
338 // We also have to generate one DNSResult per DNS-relevant attribute. As we've asked only for them
339 // and the others above we've already cleaned it's just a matter of iterating over them.
340
341 unsigned int axfrqlen = d_qname.toStringRootDot().length();
342 for (auto i = d_result["associatedDomain"].begin(); i != d_result["associatedDomain"].end(); ++i) {
343 // Sanity checks: is this associatedDomain attribute under the requested domain?
344 if (i->size() >= axfrqlen && i->substr(i->size() - axfrqlen, axfrqlen) == d_qname.toStringRootDot())
345 associatedDomains.push_back(*i);
346 }
347 }
348 else {
349 // This was a lookup in strict mode, so we add the reverse lookup
350 // information manually.
351 d_result["pTRRecord"] = d_result["associatedDomain"];
352 }
353 }
354
355 if (d_in_list) {
356 for (const auto& domain : associatedDomains)
357 this->extract_entry_results(DNSName(domain), result_template, QType(uint16_t(QType::ANY)));
358 }
359 else {
360 this->extract_entry_results(d_qname, result_template, QType(uint16_t(QType::ANY)));
361 }
362 }
363
364 if (d_results_cache.empty())
365 return false;
366 }
367
368 DNSResult result = d_results_cache.back();
369 d_results_cache.pop_back();
370 rr.qtype = result.qtype;
371 rr.qname = result.qname;
372 rr.ttl = result.ttl;
373 rr.last_modified = 0;
374 rr.content = result.value;
375 rr.auth = result.auth;
376
377 g_log << Logger::Debug << d_myname << " Record = qname: " << rr.qname << ", qtype: " << (rr.qtype).toString() << ", ttl: " << rr.ttl << ", content: " << rr.content << endl;
378 return true;
379 }
380
381 bool LdapBackend::getDomainInfo(const ZoneName& domain, DomainInfo& info, bool /* getSerial */)
382 {
383 string filter;
384 SOAData sd;
385 PowerLDAP::sentry_t result;
386 const char* attronly[] = {
387 "sOARecord",
388 "PdnsDomainId",
389 "PdnsDomainNotifiedSerial",
390 "PdnsDomainLastCheck",
391 "PdnsDomainMaster",
392 "PdnsDomainType",
393 NULL};
394
395 try {
396 // search for SOARecord of domain
397 filter = "(&(associatedDomain=" + toLower(d_pldap->escape(domain.toStringRootDot())) + ")(SOARecord=*))";
398 d_search = d_pldap->search(getArg("basedn"), LDAP_SCOPE_SUBTREE, filter, attronly);
399 if (!d_search->getNext(result)) {
400 return false;
401 }
402 }
403 catch (LDAPTimeout& lt) {
404 g_log << Logger::Warning << d_myname << " Unable to search LDAP directory: " << lt.what() << endl;
405 throw DBException("LDAP server timeout");
406 }
407 catch (LDAPNoConnection& lnc) {
408 g_log << Logger::Warning << d_myname << " Connection to LDAP lost, trying to reconnect" << endl;
409 if (reconnect())
410 this->getDomainInfo(domain, info);
411 else
412 throw PDNSException("Failed to reconnect to LDAP server");
413 }
414 catch (LDAPException& le) {
415 g_log << Logger::Error << d_myname << " Unable to search LDAP directory: " << le.what() << endl;
416 throw PDNSException("LDAP server unreachable"); // try to reconnect to another server
417 }
418 catch (std::exception& e) {
419 throw DBException("STL exception");
420 }
421
422 if (result.count("sOARecord") && !result["sOARecord"].empty()) {
423 sd.serial = 0;
424 fillSOAData(result["sOARecord"][0], sd);
425
426 if (result.count("PdnsDomainId") && !result["PdnsDomainId"].empty())
427 info.id = static_cast<domainid_t>(std::stoll(result["PdnsDomainId"][0]));
428 else
429 info.id = UnknownDomainID;
430
431 info.serial = sd.serial;
432 info.zone = domain;
433
434 if (result.count("PdnsDomainLastCheck") && !result["PdnsDomainLastCheck"].empty())
435 pdns::checked_stoi_into(info.last_check, result["PdnsDomainLastCheck"][0]);
436 else
437 info.last_check = 0;
438
439 if (result.count("PdnsDomainNotifiedSerial") && !result["PdnsDomainNotifiedSerial"].empty())
440 pdns::checked_stoi_into(info.notified_serial, result["PdnsDomainNotifiedSerial"][0]);
441 else
442 info.notified_serial = 0;
443
444 if (result.count("PdnsDomainMaster") && !result["PdnsDomainMaster"].empty()) {
445 for (const auto& m : result["PdnsDomainMaster"])
446 info.primaries.emplace_back(m, 53);
447 }
448
449 if (result.count("PdnsDomainType") && !result["PdnsDomainType"].empty()) {
450 string kind = result["PdnsDomainType"][0];
451 if (kind == "master")
452 info.kind = DomainInfo::Primary;
453 else if (kind == "slave")
454 info.kind = DomainInfo::Secondary;
455 else
456 info.kind = DomainInfo::Native;
457 }
458 else {
459 info.kind = DomainInfo::Native;
460 }
461
462 info.backend = this;
463 return true;
464 }
465
466 return false;
467 }