2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
4 * originally authored by Norbert Sendetzky
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.
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.
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.
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.
23 #include "exceptions.hh"
24 #include "ldapbackend.hh"
29 * Types which aren't active are currently not supported by PDNS
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
84 "PdnsRecordOrdername",
87 bool LdapBackend::list(const ZoneName
& target
, domainid_t domain_id
, bool /* include_disabled */)
91 d_qname
= target
.operator const DNSName
&();
93 d_results_cache
.clear();
95 return (this->*d_list_fcnt
)(target
, domain_id
);
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");
101 catch (LDAPNoConnection
& lnc
) {
102 g_log
<< Logger::Warning
<< d_myname
<< " Connection to LDAP lost, trying to reconnect" << endl
;
104 this->list(target
, domain_id
);
106 throw PDNSException("Failed to reconnect to LDAP server");
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
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");
120 bool LdapBackend::list_simple(const ZoneName
& target
, domainid_t
/* domain_id */)
126 dn
= getArg("basedn");
127 qesc
= toLower(d_pldap
->escape(target
.toStringRootDot()));
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))
135 if (d_result
.count("dn") && !d_result
["dn"].empty()) {
136 if (!mustDo("basedn-axfr-override")) {
137 dn
= d_result
["dn"][0];
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
)));
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
);
155 bool LdapBackend::list_strict(const ZoneName
& target
, domainid_t domain_id
)
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
162 return list_simple(target
, domain_id
);
165 void LdapBackend::lookup(const QType
& qtype
, const DNSName
& qname
, domainid_t zoneid
, DNSPacket
* dnspkt
)
171 d_results_cache
.clear();
174 g_log
.log("Query: '" + qname
.toStringRootDot() + "|" + qtype
.toString() + "'", Logger::Error
);
176 (this->*d_lookup_fcnt
)(qtype
, qname
, dnspkt
, zoneid
);
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");
182 catch (LDAPNoConnection
& lnc
) {
183 g_log
<< Logger::Warning
<< d_myname
<< " Connection to LDAP lost, trying to reconnect" << endl
;
185 this->lookup(qtype
, qname
, zoneid
, dnspkt
);
187 throw PDNSException("Failed to reconnect to LDAP server");
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
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");
199 void LdapBackend::lookup_simple(const QType
& qtype
, const DNSName
& qname
, DNSPacket
* /* dnspkt */, domainid_t
/* zoneid */)
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
};
205 qesc
= toLower(d_pldap
->escape(qname
.toStringRootDot()));
206 filter
= "associatedDomain=" + qesc
;
208 if (qtype
.getCode() != QType::ANY
) {
209 attr
= qtype
.toString() + "Record";
210 filter
= "&(" + filter
+ ")(" + attr
+ "=*)";
211 attronly
[0] = attr
.c_str();
212 attributes
= attronly
;
215 filter
= strbind(":target:", filter
, getArg("filter-lookup"));
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
);
221 void LdapBackend::lookup_strict(const QType
& qtype
, const DNSName
& qname
, DNSPacket
* /* dnspkt */, domainid_t
/* zoneid */)
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
};
229 qesc
= toLower(d_pldap
->escape(qname
.toStringRootDot()));
230 stringtok(parts
, qesc
, ".");
233 if (parts
.size() == 6 && len
> 13 && qesc
.substr(len
- 13, 13) == ".in-addr.arpa") // IPv4 reverse lookups
235 filter
= "aRecord=" + ptr2ip4(parts
);
236 attronly
[0] = "associatedDomain";
237 attributes
= attronly
;
239 else if (parts
.size() == 34 && len
> 9 && (qesc
.substr(len
- 9, 9) == ".ip6.arpa")) // IPv6 reverse lookups
241 filter
= "aAAARecord=" + ptr2ip6(parts
);
242 attronly
[0] = "associatedDomain";
243 attributes
= attronly
;
245 else // IPv4 and IPv6 lookups
247 filter
= "associatedDomain=" + qesc
;
250 if (qtype
.getCode() != QType::ANY
) {
251 attr
= qtype
.toString() + "Record";
252 filter
= "&(" + filter
+ ")(" + attr
+ "=*)";
253 attronly
[0] = attr
.c_str();
254 attributes
= attronly
;
257 filter
= strbind(":target:", filter
, getArg("filter-lookup"));
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
);
263 void LdapBackend::lookup_tree(const QType
& qtype
, const DNSName
& qname
, DNSPacket
* /* dnspkt */, domainid_t
/* zoneid */)
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
;
270 qesc
= toLower(d_pldap
->escape(qname
.toStringRootDot()));
271 filter
= "associatedDomain=" + qesc
;
273 if (qtype
.getCode() != QType::ANY
) {
274 attr
= qtype
.toString() + "Record";
275 filter
= "&(" + filter
+ ")(" + attr
+ "=*)";
276 attronly
[0] = attr
.c_str();
277 attributes
= attronly
;
280 filter
= strbind(":target:", filter
, getArg("filter-lookup"));
282 stringtok(parts
, toLower(qname
.toString()), ".");
283 for (auto i
= parts
.crbegin(); i
!= parts
.crend(); i
++) {
284 dn
= "dc=" + *i
+ "," + dn
;
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
);
291 bool LdapBackend::get(DNSResourceRecord
& rr
)
293 if (d_results_cache
.empty()) {
294 while (d_results_cache
.empty()) {
295 bool exhausted
= false;
296 bool valid_entry_found
= false;
298 while (!valid_entry_found
&& !exhausted
) {
300 exhausted
= !d_search
->getNext(d_result
, true);
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");
309 // All entries are valid here
310 valid_entry_found
= true;
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;
325 DNSResult result_template
;
326 result_template
.ttl
= d_default_ttl
;
327 result_template
.lastmod
= 0;
328 this->extract_common_attributes(result_template
);
330 std::vector
<std::string
> associatedDomains
;
332 if (d_result
.count("associatedDomain")) {
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.
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
);
349 // This was a lookup in strict mode, so we add the reverse lookup
350 // information manually.
351 d_result
["pTRRecord"] = d_result
["associatedDomain"];
356 for (const auto& domain
: associatedDomains
)
357 this->extract_entry_results(DNSName(domain
), result_template
, QType(uint16_t(QType::ANY
)));
360 this->extract_entry_results(d_qname
, result_template
, QType(uint16_t(QType::ANY
)));
364 if (d_results_cache
.empty())
368 DNSResult result
= d_results_cache
.back();
369 d_results_cache
.pop_back();
370 rr
.qtype
= result
.qtype
;
371 rr
.qname
= result
.qname
;
373 rr
.last_modified
= 0;
374 rr
.content
= result
.value
;
375 rr
.auth
= result
.auth
;
377 g_log
<< Logger::Debug
<< d_myname
<< " Record = qname: " << rr
.qname
<< ", qtype: " << (rr
.qtype
).toString() << ", ttl: " << rr
.ttl
<< ", content: " << rr
.content
<< endl
;
381 bool LdapBackend::getDomainInfo(const ZoneName
& domain
, DomainInfo
& info
, bool /* getSerial */)
385 PowerLDAP::sentry_t result
;
386 const char* attronly
[] = {
389 "PdnsDomainNotifiedSerial",
390 "PdnsDomainLastCheck",
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
)) {
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");
407 catch (LDAPNoConnection
& lnc
) {
408 g_log
<< Logger::Warning
<< d_myname
<< " Connection to LDAP lost, trying to reconnect" << endl
;
410 this->getDomainInfo(domain
, info
);
412 throw PDNSException("Failed to reconnect to LDAP server");
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
418 catch (std::exception
& e
) {
419 throw DBException("STL exception");
422 if (result
.count("sOARecord") && !result
["sOARecord"].empty()) {
424 fillSOAData(result
["sOARecord"][0], sd
);
426 if (result
.count("PdnsDomainId") && !result
["PdnsDomainId"].empty())
427 info
.id
= static_cast<domainid_t
>(std::stoll(result
["PdnsDomainId"][0]));
429 info
.id
= UnknownDomainID
;
431 info
.serial
= sd
.serial
;
434 if (result
.count("PdnsDomainLastCheck") && !result
["PdnsDomainLastCheck"].empty())
435 pdns::checked_stoi_into(info
.last_check
, result
["PdnsDomainLastCheck"][0]);
439 if (result
.count("PdnsDomainNotifiedSerial") && !result
["PdnsDomainNotifiedSerial"].empty())
440 pdns::checked_stoi_into(info
.notified_serial
, result
["PdnsDomainNotifiedSerial"][0]);
442 info
.notified_serial
= 0;
444 if (result
.count("PdnsDomainMaster") && !result
["PdnsDomainMaster"].empty()) {
445 for (const auto& m
: result
["PdnsDomainMaster"])
446 info
.primaries
.emplace_back(m
, 53);
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
;
456 info
.kind
= DomainInfo::Native
;
459 info
.kind
= DomainInfo::Native
;