summaryrefslogtreecommitdiffstats
path: root/contrib/dlz/modules/ldap
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:59:48 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:59:48 +0000
commit3b9b6d0b8e7f798023c9d109c490449d528fde80 (patch)
tree2e1c188dd7b8d7475cd163de9ae02c428343669b /contrib/dlz/modules/ldap
parentInitial commit. (diff)
downloadbind9-3b9b6d0b8e7f798023c9d109c490449d528fde80.tar.xz
bind9-3b9b6d0b8e7f798023c9d109c490449d528fde80.zip
Adding upstream version 1:9.18.19.upstream/1%9.18.19upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'contrib/dlz/modules/ldap')
-rw-r--r--contrib/dlz/modules/ldap/Makefile46
-rw-r--r--contrib/dlz/modules/ldap/dlz_ldap_dynamic.c1190
-rw-r--r--contrib/dlz/modules/ldap/testing/README10
-rw-r--r--contrib/dlz/modules/ldap/testing/dlz.schema192
-rw-r--r--contrib/dlz/modules/ldap/testing/example.ldif192
-rw-r--r--contrib/dlz/modules/ldap/testing/named.conf43
-rw-r--r--contrib/dlz/modules/ldap/testing/slapd.conf44
7 files changed, 1717 insertions, 0 deletions
diff --git a/contrib/dlz/modules/ldap/Makefile b/contrib/dlz/modules/ldap/Makefile
new file mode 100644
index 0000000..d9b6ff5
--- /dev/null
+++ b/contrib/dlz/modules/ldap/Makefile
@@ -0,0 +1,46 @@
+# Copyright Internet Systems Consortium, Inc. ("ISC")
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+
+# Copyright (C) Stichting NLnet, Netherlands, stichting@nlnet.nl.
+#
+# The development of Dynamically Loadable Zones (DLZ) for Bind 9 was
+# conceived and contributed by Rob Butler.
+#
+# SPDX-License-Identifier: ISC and MPL-2.0
+#
+# Permission to use, copy, modify, and distribute this software for any purpose
+# with or without fee is hereby granted, provided that the above copyright
+# notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND STICHTING NLNET DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL STICHTING NLNET BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+# OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+prefix = /usr
+libdir = $(prefix)/lib/bind9
+
+CFLAGS += -fPIC -g -I../include
+LDAP_LIBS=-lldap
+
+all: dlz_ldap_dynamic.so
+
+dlz_dbi.o: ../common/dlz_dbi.c
+ $(CC) $(CFLAGS) -c ../common/dlz_dbi.c
+
+dlz_ldap_dynamic.so: dlz_ldap_dynamic.c dlz_dbi.o
+ $(CC) $(CFLAGS) -shared -o dlz_ldap_dynamic.so \
+ dlz_ldap_dynamic.c dlz_dbi.o $(LDAP_LIBS)
+
+clean:
+ rm -f dlz_ldap_dynamic.so *.o
+
+install: dlz_ldap_dynamic.so
+ mkdir -p $(DESTDIR)$(libdir)
+ install dlz_ldap_dynamic.so $(DESTDIR)$(libdir)
diff --git a/contrib/dlz/modules/ldap/dlz_ldap_dynamic.c b/contrib/dlz/modules/ldap/dlz_ldap_dynamic.c
new file mode 100644
index 0000000..ce1c50c
--- /dev/null
+++ b/contrib/dlz/modules/ldap/dlz_ldap_dynamic.c
@@ -0,0 +1,1190 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0 and ISC
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+/*
+ * Copyright (C) 2002 Stichting NLnet, Netherlands, stichting@nlnet.nl.
+ *
+ * The development of Dynamically Loadable Zones (DLZ) for Bind 9 was
+ * conceived and contributed by Rob Butler.
+ *
+ * Permission to use, copy, modify, and distribute this software for any purpose
+ * with or without fee is hereby granted, provided that the above copyright
+ * notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+ * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ * PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This provides the externally loadable ldap DLZ module, without
+ * update support
+ */
+
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <dlz_dbi.h>
+#include <dlz_list.h>
+#include <dlz_minimal.h>
+#include <dlz_pthread.h>
+
+/*
+ * Need older API functions from ldap.h.
+ */
+#define LDAP_DEPRECATED 1
+
+#include <ldap.h>
+
+#define SIMPLE "simple"
+#define KRB41 "krb41"
+#define KRB42 "krb42"
+#define V2 "v2"
+#define V3 "v3"
+
+#define dbc_search_limit 30
+#define ALLNODES 1
+#define ALLOWXFR 2
+#define AUTHORITY 3
+#define FINDZONE 4
+#define LOOKUP 5
+
+/*%
+ * Structure to hold everything needed by this "instance" of the LDAP
+ * driver remember, the driver code is only loaded once, but may have
+ * many separate instances.
+ */
+typedef struct {
+ db_list_t *db; /*%< handle to a list of DB */
+ int method; /*%< security authentication
+ * method */
+ char *user; /*%< who is authenticating */
+ char *cred; /*%< password for simple
+ * authentication method */
+ int protocol; /*%< LDAP communication
+ * protocol version */
+ char *hosts; /*%< LDAP server hosts */
+
+ /* Helper functions from the dlz_dlopen driver */
+ log_t *log;
+ dns_sdlz_putrr_t *putrr;
+ dns_sdlz_putnamedrr_t *putnamedrr;
+ dns_dlz_writeablezone_t *writeable_zone;
+} ldap_instance_t;
+
+/* forward references */
+
+#if DLZ_DLOPEN_VERSION < 3
+isc_result_t
+dlz_findzonedb(void *dbdata, const char *name);
+#else /* if DLZ_DLOPEN_VERSION < 3 */
+isc_result_t
+dlz_findzonedb(void *dbdata, const char *name, dns_clientinfomethods_t *methods,
+ dns_clientinfo_t *clientinfo);
+#endif /* if DLZ_DLOPEN_VERSION < 3 */
+
+void
+dlz_destroy(void *dbdata);
+
+static void
+b9_add_helper(ldap_instance_t *db, const char *helper_name, void *ptr);
+
+/*
+ * Private methods
+ */
+
+/*% checks that the LDAP URL parameters make sense */
+static isc_result_t
+dlz_ldap_checkURL(ldap_instance_t *db, char *URL, int attrCnt,
+ const char *msg) {
+ isc_result_t result = ISC_R_SUCCESS;
+ int ldap_result;
+ LDAPURLDesc *ldap_url = NULL;
+
+ if (!ldap_is_ldap_url(URL)) {
+ db->log(ISC_LOG_ERROR, "%s query is not a valid LDAP URL", msg);
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ ldap_result = ldap_url_parse(URL, &ldap_url);
+ if (ldap_result != LDAP_SUCCESS || ldap_url == NULL) {
+ db->log(ISC_LOG_ERROR, "parsing %s query failed", msg);
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ if (ldap_count_values(ldap_url->lud_attrs) < attrCnt) {
+ db->log(ISC_LOG_ERROR,
+ "%s query must specify at least "
+ "%d attributes to return",
+ msg, attrCnt);
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ if (ldap_url->lud_host != NULL) {
+ db->log(ISC_LOG_ERROR, "%s query must not specify a host", msg);
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ if (ldap_url->lud_port != 389) {
+ db->log(ISC_LOG_ERROR, "%s query must not specify a port", msg);
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ if (ldap_url->lud_dn == NULL || strlen(ldap_url->lud_dn) < 1) {
+ db->log(ISC_LOG_ERROR, "%s query must specify a search base",
+ msg);
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ if (ldap_url->lud_exts != NULL || ldap_url->lud_crit_exts != 0) {
+ db->log(ISC_LOG_ERROR,
+ "%s uses extensions. "
+ "The driver does not support LDAP extensions.",
+ msg);
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+cleanup:
+ if (ldap_url != NULL) {
+ ldap_free_urldesc(ldap_url);
+ }
+
+ return (result);
+}
+
+/*% Connects / reconnects to LDAP server */
+static isc_result_t
+dlz_ldap_connect(ldap_instance_t *dbi, dbinstance_t *dbc) {
+ isc_result_t result;
+ int ldap_result;
+
+ /* if we have a connection, get ride of it. */
+ if (dbc->dbconn != NULL) {
+ ldap_unbind_s((LDAP *)dbc->dbconn);
+ dbc->dbconn = NULL;
+ }
+
+ /* now connect / reconnect. */
+
+ /* initialize. */
+ dbc->dbconn = ldap_init(dbi->hosts, LDAP_PORT);
+ if (dbc->dbconn == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+
+ /* set protocol version. */
+ ldap_result = ldap_set_option((LDAP *)dbc->dbconn,
+ LDAP_OPT_PROTOCOL_VERSION,
+ &(dbi->protocol));
+ if (ldap_result != LDAP_SUCCESS) {
+ result = ISC_R_NOPERM;
+ goto cleanup;
+ }
+
+ /* "bind" to server. i.e. send username / pass */
+ ldap_result = ldap_bind_s((LDAP *)dbc->dbconn, dbi->user, dbi->cred,
+ dbi->method);
+ if (ldap_result != LDAP_SUCCESS) {
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ return (ISC_R_SUCCESS);
+
+cleanup:
+
+ /* cleanup if failure. */
+ if (dbc->dbconn != NULL) {
+ ldap_unbind_s((LDAP *)dbc->dbconn);
+ dbc->dbconn = NULL;
+ }
+
+ return (result);
+}
+
+/*%
+ * Properly cleans up a list of database instances.
+ * This function is only used when the driver is compiled for
+ * multithreaded operation.
+ */
+static void
+dlz_ldap_destroy_dblist(db_list_t *dblist) {
+ dbinstance_t *ndbi = NULL;
+ dbinstance_t *dbi = NULL;
+
+ /* get the first DBI in the list */
+ ndbi = DLZ_LIST_HEAD(*dblist);
+
+ /* loop through the list */
+ while (ndbi != NULL) {
+ dbi = ndbi;
+ /* get the next DBI in the list */
+ ndbi = DLZ_LIST_NEXT(dbi, link);
+ /* release DB connection */
+ if (dbi->dbconn != NULL) {
+ ldap_unbind_s((LDAP *)dbi->dbconn);
+ }
+ /* release all memory that comprised a DBI */
+ destroy_dbinstance(dbi);
+ }
+ /* release memory for the list structure */
+ free(dblist);
+}
+
+/*%
+ * Loops through the list of DB instances, attempting to lock
+ * on the mutex. If successful, the DBI is reserved for use
+ * and the thread can perform queries against the database.
+ * If the lock fails, the next one in the list is tried.
+ * looping continues until a lock is obtained, or until
+ * the list has been searched dbc_search_limit times.
+ * This function is only used when the driver is compiled for
+ * multithreaded operation.
+ */
+static dbinstance_t *
+dlz_ldap_find_avail_conn(ldap_instance_t *ldap) {
+ dbinstance_t *dbi = NULL;
+ dbinstance_t *head;
+ int count = 0;
+
+ /* get top of list */
+ head = dbi = DLZ_LIST_HEAD(*ldap->db);
+
+ /* loop through list */
+ while (count < dbc_search_limit) {
+ /* try to lock on the mutex */
+ if (dlz_mutex_trylock(&dbi->lock) == 0) {
+ return (dbi); /* success, return the DBI for use. */
+ }
+ /* not successful, keep trying */
+ dbi = DLZ_LIST_NEXT(dbi, link);
+
+ /* check to see if we have gone to the top of the list. */
+ if (dbi == NULL) {
+ count++;
+ dbi = head;
+ }
+ }
+
+ ldap->log(ISC_LOG_INFO,
+ "LDAP driver unable to find available connection "
+ "after searching %d times",
+ count);
+ return (NULL);
+}
+
+static isc_result_t
+dlz_ldap_process_results(ldap_instance_t *db, LDAP *dbc, LDAPMessage *msg,
+ char **attrs, void *ptr, bool allnodes) {
+ isc_result_t result = ISC_R_SUCCESS;
+ int i = 0;
+ int j;
+ int len;
+ char *attribute = NULL;
+ LDAPMessage *entry;
+ char *endp = NULL;
+ char *host = NULL;
+ char *type = NULL;
+ char *data = NULL;
+ char **vals = NULL;
+ int ttl;
+
+ /* get the first entry to process */
+ entry = ldap_first_entry(dbc, msg);
+ if (entry == NULL) {
+ db->log(ISC_LOG_INFO, "LDAP no entries to process.");
+ return (ISC_R_FAILURE);
+ }
+
+ /* loop through all entries returned */
+ while (entry != NULL) {
+ /* reset for this loop */
+ ttl = 0;
+ len = 0;
+ i = 0;
+ attribute = attrs[i];
+
+ /* determine how much space we need for data string */
+ for (j = 0; attrs[j] != NULL; j++) {
+ /* get the list of values for this attribute. */
+ vals = ldap_get_values(dbc, entry, attrs[j]);
+ /* skip empty attributes. */
+ if (vals == NULL || ldap_count_values(vals) < 1) {
+ continue;
+ }
+ /*
+ * we only use the first value. this driver
+ * does not support multi-valued attributes.
+ */
+ len = len + strlen(vals[0]) + 1;
+ /* free vals for next loop */
+ ldap_value_free(vals);
+ }
+
+ /* allocate memory for data string */
+ data = malloc(len + 1);
+ if (data == NULL) {
+ db->log(ISC_LOG_ERROR, "LDAP driver unable to allocate "
+ "memory "
+ "while processing results");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ /*
+ * Make sure data is null termed at the beginning so
+ * we can check if any data was stored to it later.
+ */
+ data[0] = '\0';
+
+ /* reset j to re-use below */
+ j = 0;
+
+ /* loop through the attributes in the order specified. */
+ while (attribute != NULL) {
+ /* get the list of values for this attribute. */
+ vals = ldap_get_values(dbc, entry, attribute);
+
+ /* skip empty attributes. */
+ if (vals == NULL || vals[0] == NULL) {
+ /* increment attribute pointer */
+ attribute = attrs[++i];
+ /* start loop over */
+ continue;
+ }
+
+ /*
+ * j initially = 0. Increment j each time we
+ * set a field that way next loop will set
+ * next field.
+ */
+ switch (j) {
+ case 0:
+ j++;
+ /*
+ * convert text to int, make sure it
+ * worked right
+ */
+ ttl = strtol(vals[0], &endp, 10);
+ if (*endp != '\0' || ttl < 0) {
+ db->log(ISC_LOG_ERROR, "LDAP driver "
+ "ttl must "
+ "be a positive "
+ "number");
+ goto cleanup;
+ }
+ break;
+ case 1:
+ j++;
+ type = strdup(vals[0]);
+ break;
+ case 2:
+ j++;
+ if (allnodes) {
+ host = strdup(vals[0]);
+ } else {
+ strcpy(data, vals[0]);
+ }
+ break;
+ case 3:
+ j++;
+ if (allnodes) {
+ strcpy(data, vals[0]);
+ } else {
+ strcat(data, " ");
+ strcat(data, vals[0]);
+ }
+ break;
+ default:
+ strcat(data, " ");
+ strcat(data, vals[0]);
+ break;
+ }
+
+ /* free values */
+ ldap_value_free(vals);
+ vals = NULL;
+
+ /* increment attribute pointer */
+ attribute = attrs[++i];
+ }
+
+ if (type == NULL) {
+ db->log(ISC_LOG_ERROR, "LDAP driver unable to retrieve "
+ "DNS type");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ if (strlen(data) < 1) {
+ db->log(ISC_LOG_ERROR, "LDAP driver unable to retrieve "
+ "DNS data");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ if (allnodes && host != NULL) {
+ dns_sdlzallnodes_t *an = (dns_sdlzallnodes_t *)ptr;
+ if (strcasecmp(host, "~") == 0) {
+ result = db->putnamedrr(an, "*", type, ttl,
+ data);
+ } else {
+ result = db->putnamedrr(an, host, type, ttl,
+ data);
+ }
+ if (result != ISC_R_SUCCESS) {
+ db->log(ISC_LOG_ERROR,
+ "ldap_dynamic: putnamedrr failed "
+ "for \"%s %s %u %s\" (%d)",
+ host, type, ttl, data, result);
+ }
+ } else {
+ dns_sdlzlookup_t *lookup = (dns_sdlzlookup_t *)ptr;
+ result = db->putrr(lookup, type, ttl, data);
+ if (result != ISC_R_SUCCESS) {
+ db->log(ISC_LOG_ERROR,
+ "ldap_dynamic: putrr failed "
+ "for \"%s %u %s\" (%s)",
+ type, ttl, data, result);
+ }
+ }
+
+ if (result != ISC_R_SUCCESS) {
+ db->log(ISC_LOG_ERROR, "LDAP driver failed "
+ "while sending data to BIND.");
+ goto cleanup;
+ }
+
+ /* free memory for type, data and host for next loop */
+ free(type);
+ type = NULL;
+
+ free(data);
+ data = NULL;
+
+ if (host != NULL) {
+ free(host);
+ host = NULL;
+ }
+
+ /* get the next entry to process */
+ entry = ldap_next_entry(dbc, entry);
+ }
+
+cleanup:
+ /* de-allocate memory */
+ if (vals != NULL) {
+ ldap_value_free(vals);
+ }
+ if (host != NULL) {
+ free(host);
+ }
+ if (type != NULL) {
+ free(type);
+ }
+ if (data != NULL) {
+ free(data);
+ }
+
+ return (result);
+}
+
+/*%
+ * This function is the real core of the driver. Zone, record
+ * and client strings are passed in (or NULL is passed if the
+ * string is not available). The type of query we want to run
+ * is indicated by the query flag, and the dbdata object is passed
+ * passed in to. dbdata really holds either:
+ * 1) a list of database instances (in multithreaded mode) OR
+ * 2) a single database instance (in single threaded mode)
+ * The function will construct the query and obtain an available
+ * database instance (DBI). It will then run the query and hopefully
+ * obtain a result set.
+ */
+static isc_result_t
+dlz_ldap_get_results(const char *zone, const char *record, const char *client,
+ unsigned int query, void *dbdata, void *ptr) {
+ isc_result_t result;
+ ldap_instance_t *db = (ldap_instance_t *)dbdata;
+ dbinstance_t *dbi = NULL;
+ char *querystring = NULL;
+ LDAPURLDesc *ldap_url = NULL;
+ int ldap_result = 0;
+ LDAPMessage *ldap_msg = NULL;
+ int i;
+ int entries;
+
+ /* get db instance / connection */
+ /* find an available DBI from the list */
+ dbi = dlz_ldap_find_avail_conn(db);
+
+ /* if DBI is null, can't do anything else */
+ if (dbi == NULL) {
+ return (ISC_R_FAILURE);
+ }
+
+ /* set fields */
+ if (zone != NULL) {
+ dbi->zone = strdup(zone);
+ if (dbi->zone == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+ } else {
+ dbi->zone = NULL;
+ }
+
+ if (record != NULL) {
+ dbi->record = strdup(record);
+ if (dbi->record == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+ } else {
+ dbi->record = NULL;
+ }
+
+ if (client != NULL) {
+ dbi->client = strdup(client);
+ if (dbi->client == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+ } else {
+ dbi->client = NULL;
+ }
+
+ /* what type of query are we going to run? */
+ switch (query) {
+ case ALLNODES:
+ /*
+ * if the query was not passed in from the config file
+ * then we can't run it. return not_implemented, so
+ * it's like the code for that operation was never
+ * built into the driver.... AHHH flexibility!!!
+ */
+ if (dbi->allnodes_q == NULL) {
+ result = ISC_R_NOTIMPLEMENTED;
+ goto cleanup;
+ } else {
+ querystring = build_querystring(dbi->allnodes_q);
+ }
+ break;
+ case ALLOWXFR:
+ /* same as comments as ALLNODES */
+ if (dbi->allowxfr_q == NULL) {
+ result = ISC_R_NOTIMPLEMENTED;
+ goto cleanup;
+ } else {
+ querystring = build_querystring(dbi->allowxfr_q);
+ }
+ break;
+ case AUTHORITY:
+ /* same as comments as ALLNODES */
+ if (dbi->authority_q == NULL) {
+ result = ISC_R_NOTIMPLEMENTED;
+ goto cleanup;
+ } else {
+ querystring = build_querystring(dbi->authority_q);
+ }
+ break;
+ case FINDZONE:
+ /* this is required. It's the whole point of DLZ! */
+ if (dbi->findzone_q == NULL) {
+ db->log(ISC_LOG_DEBUG(2), "No query specified for "
+ "findzone. "
+ "Findzone requires a query");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ } else {
+ querystring = build_querystring(dbi->findzone_q);
+ }
+ break;
+ case LOOKUP:
+ /* this is required. It's also a major point of DLZ! */
+ if (dbi->lookup_q == NULL) {
+ db->log(ISC_LOG_DEBUG(2), "No query specified for "
+ "lookup. "
+ "Lookup requires a query");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ } else {
+ querystring = build_querystring(dbi->lookup_q);
+ }
+ break;
+ default:
+ /*
+ * this should never happen. If it does, the code is
+ * screwed up!
+ */
+ db->log(ISC_LOG_ERROR, "Incorrect query flag passed to "
+ "dlz_ldap_get_results");
+ result = ISC_R_UNEXPECTED;
+ goto cleanup;
+ }
+
+ /* if the querystring is null, Bummer, outta RAM. UPGRADE TIME!!! */
+ if (querystring == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+
+ /*
+ * output the full query string during debug so we can see
+ * what lame error the query has.
+ */
+ db->log(ISC_LOG_DEBUG(1), "Query String: %s", querystring);
+
+ /* break URL down into it's component parts, if error cleanup */
+ ldap_result = ldap_url_parse(querystring, &ldap_url);
+ if (ldap_result != LDAP_SUCCESS || ldap_url == NULL) {
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ for (i = 0; i < 3; i++) {
+ /*
+ * dbi->dbconn may be null if trying to reconnect on a
+ * previous query failed.
+ */
+ if (dbi->dbconn == NULL) {
+ db->log(ISC_LOG_INFO, "LDAP driver attempting to "
+ "re-connect");
+
+ result = dlz_ldap_connect((ldap_instance_t *)dbdata,
+ dbi);
+ if (result != ISC_R_SUCCESS) {
+ result = ISC_R_FAILURE;
+ continue;
+ }
+ }
+
+ /* perform ldap search synchronously */
+ ldap_result =
+ ldap_search_s((LDAP *)dbi->dbconn, ldap_url->lud_dn,
+ ldap_url->lud_scope, ldap_url->lud_filter,
+ ldap_url->lud_attrs, 0, &ldap_msg);
+
+ /*
+ * check return code. No such object is ok, just
+ * didn't find what we wanted
+ */
+ switch (ldap_result) {
+ case LDAP_NO_SUCH_OBJECT:
+ db->log(ISC_LOG_DEBUG(1), "No object found matching "
+ "query requirements");
+ result = ISC_R_NOTFOUND;
+ goto cleanup;
+ break;
+ case LDAP_SUCCESS: /* on success do nothing */
+ result = ISC_R_SUCCESS;
+ i = 3;
+ break;
+ case LDAP_SERVER_DOWN:
+ db->log(ISC_LOG_INFO, "LDAP driver attempting to "
+ "re-connect");
+ result = dlz_ldap_connect((ldap_instance_t *)dbdata,
+ dbi);
+ if (result != ISC_R_SUCCESS) {
+ result = ISC_R_FAILURE;
+ }
+ break;
+ default:
+ /*
+ * other errors not ok. Log error message and
+ * get out
+ */
+ db->log(ISC_LOG_ERROR, "LDAP error: %s",
+ ldap_err2string(ldap_result));
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ break;
+ }
+ }
+
+ if (result != ISC_R_SUCCESS) {
+ goto cleanup;
+ }
+
+ switch (query) {
+ case ALLNODES:
+ result = dlz_ldap_process_results(db, (LDAP *)dbi->dbconn,
+ ldap_msg, ldap_url->lud_attrs,
+ ptr, true);
+ break;
+ case AUTHORITY:
+ case LOOKUP:
+ result = dlz_ldap_process_results(db, (LDAP *)dbi->dbconn,
+ ldap_msg, ldap_url->lud_attrs,
+ ptr, false);
+ break;
+ case ALLOWXFR:
+ entries = ldap_count_entries((LDAP *)dbi->dbconn, ldap_msg);
+ if (entries == 0) {
+ result = ISC_R_NOPERM;
+ } else if (entries > 0) {
+ result = ISC_R_SUCCESS;
+ } else {
+ result = ISC_R_FAILURE;
+ }
+ break;
+ case FINDZONE:
+ entries = ldap_count_entries((LDAP *)dbi->dbconn, ldap_msg);
+ if (entries == 0) {
+ result = ISC_R_NOTFOUND;
+ } else if (entries > 0) {
+ result = ISC_R_SUCCESS;
+ } else {
+ result = ISC_R_FAILURE;
+ }
+ break;
+ default:
+ /*
+ * this should never happen. If it does, the code is
+ * screwed up!
+ */
+ db->log(ISC_LOG_ERROR, "Incorrect query flag passed to "
+ "dlz_ldap_get_results");
+ result = ISC_R_UNEXPECTED;
+ }
+
+cleanup:
+ /* it's always good to cleanup after yourself */
+
+ /* if we retrieved results, free them */
+ if (ldap_msg != NULL) {
+ ldap_msgfree(ldap_msg);
+ }
+
+ if (ldap_url != NULL) {
+ ldap_free_urldesc(ldap_url);
+ }
+
+ /* cleanup */
+ if (dbi->zone != NULL) {
+ free(dbi->zone);
+ }
+ if (dbi->record != NULL) {
+ free(dbi->record);
+ }
+ if (dbi->client != NULL) {
+ free(dbi->client);
+ }
+ dbi->zone = dbi->record = dbi->client = NULL;
+
+ /* release the lock so another thread can use this dbi */
+ (void)dlz_mutex_unlock(&dbi->lock);
+
+ /* release query string */
+ if (querystring != NULL) {
+ free(querystring);
+ }
+
+ /* return result */
+ return (result);
+}
+
+/*
+ * DLZ methods
+ */
+isc_result_t
+dlz_allowzonexfr(void *dbdata, const char *name, const char *client) {
+ isc_result_t result;
+
+ /* check to see if we are authoritative for the zone first */
+#if DLZ_DLOPEN_VERSION < 3
+ result = dlz_findzonedb(dbdata, name);
+#else /* if DLZ_DLOPEN_VERSION < 3 */
+ result = dlz_findzonedb(dbdata, name, NULL, NULL);
+#endif /* if DLZ_DLOPEN_VERSION < 3 */
+ if (result != ISC_R_SUCCESS) {
+ return (result);
+ }
+
+ /* get all the zone data */
+ result = dlz_ldap_get_results(name, NULL, client, ALLOWXFR, dbdata,
+ NULL);
+ return (result);
+}
+
+isc_result_t
+dlz_allnodes(const char *zone, void *dbdata, dns_sdlzallnodes_t *allnodes) {
+ return (dlz_ldap_get_results(zone, NULL, NULL, ALLNODES, dbdata,
+ allnodes));
+}
+
+isc_result_t
+dlz_authority(const char *zone, void *dbdata, dns_sdlzlookup_t *lookup) {
+ return (dlz_ldap_get_results(zone, NULL, NULL, AUTHORITY, dbdata,
+ lookup));
+}
+
+#if DLZ_DLOPEN_VERSION < 3
+isc_result_t
+dlz_findzonedb(void *dbdata, const char *name)
+#else /* if DLZ_DLOPEN_VERSION < 3 */
+isc_result_t
+dlz_findzonedb(void *dbdata, const char *name, dns_clientinfomethods_t *methods,
+ dns_clientinfo_t *clientinfo)
+#endif /* if DLZ_DLOPEN_VERSION < 3 */
+{
+#if DLZ_DLOPEN_VERSION >= 3
+ UNUSED(methods);
+ UNUSED(clientinfo);
+#endif /* if DLZ_DLOPEN_VERSION >= 3 */
+ return (dlz_ldap_get_results(name, NULL, NULL, FINDZONE, dbdata, NULL));
+}
+
+#if DLZ_DLOPEN_VERSION == 1
+isc_result_t
+dlz_lookup(const char *zone, const char *name, void *dbdata,
+ dns_sdlzlookup_t *lookup)
+#else /* if DLZ_DLOPEN_VERSION == 1 */
+isc_result_t
+dlz_lookup(const char *zone, const char *name, void *dbdata,
+ dns_sdlzlookup_t *lookup, dns_clientinfomethods_t *methods,
+ dns_clientinfo_t *clientinfo)
+#endif /* if DLZ_DLOPEN_VERSION == 1 */
+{
+ isc_result_t result;
+
+#if DLZ_DLOPEN_VERSION >= 2
+ UNUSED(methods);
+ UNUSED(clientinfo);
+#endif /* if DLZ_DLOPEN_VERSION >= 2 */
+
+ if (strcmp(name, "*") == 0) {
+ result = dlz_ldap_get_results(zone, "~", NULL, LOOKUP, dbdata,
+ lookup);
+ } else {
+ result = dlz_ldap_get_results(zone, name, NULL, LOOKUP, dbdata,
+ lookup);
+ }
+ return (result);
+}
+
+isc_result_t
+dlz_create(const char *dlzname, unsigned int argc, char *argv[], void **dbdata,
+ ...) {
+ isc_result_t result = ISC_R_FAILURE;
+ ldap_instance_t *ldap = NULL;
+ dbinstance_t *dbi = NULL;
+ const char *helper_name = NULL;
+ int protocol, method, dbcount, i;
+ char *endp = NULL;
+ va_list ap;
+
+ UNUSED(dlzname);
+
+ /* allocate memory for LDAP instance */
+ ldap = calloc(1, sizeof(ldap_instance_t));
+ if (ldap == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+ memset(ldap, 0, sizeof(ldap_instance_t));
+
+ /* Fill in the helper functions */
+ va_start(ap, dbdata);
+ while ((helper_name = va_arg(ap, const char *)) != NULL) {
+ b9_add_helper(ldap, helper_name, va_arg(ap, void *));
+ }
+ va_end(ap);
+
+ /* if debugging, let user know we are multithreaded. */
+ ldap->log(ISC_LOG_DEBUG(1), "LDAP driver running multithreaded");
+
+ if (argc < 9) {
+ ldap->log(ISC_LOG_ERROR, "LDAP driver requires at least "
+ "8 command line args.");
+ goto cleanup;
+ }
+
+ /* no more than 13 arg's should be passed to the driver */
+ if (argc > 12) {
+ ldap->log(ISC_LOG_ERROR, "LDAP driver cannot accept more than "
+ "11 command line args.");
+ goto cleanup;
+ }
+
+ /* determine protocol version. */
+ if (strncasecmp(argv[2], V2, strlen(V2)) == 0) {
+ protocol = 2;
+ } else if (strncasecmp(argv[2], V3, strlen(V3)) == 0) {
+ protocol = 3;
+ } else {
+ ldap->log(ISC_LOG_ERROR,
+ "LDAP driver protocol must be either %s or %s", V2,
+ V3);
+ goto cleanup;
+ }
+
+ /* determine connection method. */
+ if (strncasecmp(argv[3], SIMPLE, strlen(SIMPLE)) == 0) {
+ method = LDAP_AUTH_SIMPLE;
+ } else if (strncasecmp(argv[3], KRB41, strlen(KRB41)) == 0) {
+ method = LDAP_AUTH_KRBV41;
+ } else if (strncasecmp(argv[3], KRB42, strlen(KRB42)) == 0) {
+ method = LDAP_AUTH_KRBV42;
+ } else {
+ ldap->log(ISC_LOG_ERROR,
+ "LDAP driver authentication method must be "
+ "one of %s, %s or %s",
+ SIMPLE, KRB41, KRB42);
+ goto cleanup;
+ }
+
+ /* check how many db connections we should create */
+ dbcount = strtol(argv[1], &endp, 10);
+ if (*endp != '\0' || dbcount < 0) {
+ ldap->log(ISC_LOG_ERROR, "LDAP driver database connection "
+ "count "
+ "must be positive.");
+ goto cleanup;
+ }
+
+ /* check that LDAP URL parameters make sense */
+ switch (argc) {
+ case 12:
+ result = dlz_ldap_checkURL(ldap, argv[11], 0,
+ "allow zone transfer");
+ if (result != ISC_R_SUCCESS) {
+ goto cleanup;
+ }
+ FALLTHROUGH;
+ case 11:
+ result = dlz_ldap_checkURL(ldap, argv[10], 3, "all nodes");
+ if (result != ISC_R_SUCCESS) {
+ goto cleanup;
+ }
+ FALLTHROUGH;
+ case 10:
+ if (strlen(argv[9]) > 0) {
+ result = dlz_ldap_checkURL(ldap, argv[9], 3,
+ "authority");
+ if (result != ISC_R_SUCCESS) {
+ goto cleanup;
+ }
+ }
+ FALLTHROUGH;
+ case 9:
+ result = dlz_ldap_checkURL(ldap, argv[8], 3, "lookup");
+ if (result != ISC_R_SUCCESS) {
+ goto cleanup;
+ }
+ result = dlz_ldap_checkURL(ldap, argv[7], 0, "find zone");
+ if (result != ISC_R_SUCCESS) {
+ goto cleanup;
+ }
+ break;
+ default:
+ /* not really needed, should shut up compiler. */
+ result = ISC_R_FAILURE;
+ }
+
+ /* store info needed to automatically re-connect. */
+ ldap->protocol = protocol;
+ ldap->method = method;
+ ldap->hosts = strdup(argv[6]);
+ if (ldap->hosts == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+ ldap->user = strdup(argv[4]);
+ if (ldap->user == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+ ldap->cred = strdup(argv[5]);
+ if (ldap->cred == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+
+ /* allocate memory for database connection list */
+ ldap->db = calloc(1, sizeof(db_list_t));
+ if (ldap->db == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+
+ /* initialize DB connection list */
+ DLZ_LIST_INIT(*(ldap->db));
+
+ /*
+ * create the appropriate number of database instances (DBI)
+ * append each new DBI to the end of the list
+ */
+ for (i = 0; i < dbcount; i++) {
+ /* how many queries were passed in from config file? */
+ switch (argc) {
+ case 9:
+ result = build_dbinstance(NULL, NULL, NULL, argv[7],
+ argv[8], NULL, &dbi,
+ ldap->log);
+ break;
+ case 10:
+ result = build_dbinstance(NULL, NULL, argv[9], argv[7],
+ argv[8], NULL, &dbi,
+ ldap->log);
+ break;
+ case 11:
+ result = build_dbinstance(argv[10], NULL, argv[9],
+ argv[7], argv[8], NULL, &dbi,
+ ldap->log);
+ break;
+ case 12:
+ result = build_dbinstance(argv[10], argv[11], argv[9],
+ argv[7], argv[8], NULL, &dbi,
+ ldap->log);
+ break;
+ default:
+ /* not really needed, should shut up compiler. */
+ result = ISC_R_FAILURE;
+ }
+
+ if (result == ISC_R_SUCCESS) {
+ ldap->log(ISC_LOG_DEBUG(2), "LDAP driver created "
+ "database instance "
+ "object.");
+ } else { /* unsuccessful?, log err msg and cleanup. */
+ ldap->log(ISC_LOG_ERROR, "LDAP driver could not create "
+ "database instance object.");
+ goto cleanup;
+ }
+
+ /* when multithreaded, build a list of DBI's */
+ DLZ_LINK_INIT(dbi, link);
+ DLZ_LIST_APPEND(*(ldap->db), dbi, link);
+ /* attempt to connect */
+ result = dlz_ldap_connect(ldap, dbi);
+
+ /*
+ * if db connection cannot be created, log err msg and
+ * cleanup.
+ */
+ switch (result) {
+ /* success, do nothing */
+ case ISC_R_SUCCESS:
+ break;
+ /*
+ * no memory means ldap_init could not
+ * allocate memory
+ */
+ case ISC_R_NOMEMORY:
+ ldap->log(ISC_LOG_ERROR,
+ "LDAP driver could not allocate memory "
+ "for connection number %u",
+ i + 1);
+ goto cleanup;
+ /*
+ * no perm means ldap_set_option could not set
+ * protocol version
+ */
+ case ISC_R_NOPERM:
+ ldap->log(ISC_LOG_ERROR, "LDAP driver could not "
+ "set protocol version.");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ /* failure means couldn't connect to ldap server */
+ case ISC_R_FAILURE:
+ ldap->log(ISC_LOG_ERROR,
+ "LDAP driver could not bind "
+ "connection number %u to server.",
+ i + 1);
+ goto cleanup;
+ /*
+ * default should never happen. If it does,
+ * major errors.
+ */
+ default:
+ ldap->log(ISC_LOG_ERROR, "dlz_create() failed (%d)",
+ result);
+ result = ISC_R_UNEXPECTED;
+ goto cleanup;
+ }
+
+ /* set DBI = null for next loop through. */
+ dbi = NULL;
+ }
+
+ /* set dbdata to the ldap_instance we created. */
+ *dbdata = ldap;
+
+ return (ISC_R_SUCCESS);
+
+cleanup:
+ dlz_destroy(ldap);
+
+ return (result);
+}
+
+void
+dlz_destroy(void *dbdata) {
+ if (dbdata != NULL) {
+ ldap_instance_t *db = (ldap_instance_t *)dbdata;
+ /* cleanup the list of DBI's */
+ if (db->db != NULL) {
+ dlz_ldap_destroy_dblist((db_list_t *)(db->db));
+ }
+
+ if (db->hosts != NULL) {
+ free(db->hosts);
+ }
+ if (db->user != NULL) {
+ free(db->user);
+ }
+ if (db->cred != NULL) {
+ free(db->cred);
+ }
+ free(dbdata);
+ }
+}
+
+/*
+ * Return the version of the API
+ */
+int
+dlz_version(unsigned int *flags) {
+ *flags |= DNS_SDLZFLAG_RELATIVERDATA | DNS_SDLZFLAG_THREADSAFE;
+ return (DLZ_DLOPEN_VERSION);
+}
+
+/*
+ * Register a helper function from the bind9 dlz_dlopen driver
+ */
+static void
+b9_add_helper(ldap_instance_t *db, const char *helper_name, void *ptr) {
+ if (strcmp(helper_name, "log") == 0) {
+ db->log = (log_t *)ptr;
+ }
+ if (strcmp(helper_name, "putrr") == 0) {
+ db->putrr = (dns_sdlz_putrr_t *)ptr;
+ }
+ if (strcmp(helper_name, "putnamedrr") == 0) {
+ db->putnamedrr = (dns_sdlz_putnamedrr_t *)ptr;
+ }
+ if (strcmp(helper_name, "writeable_zone") == 0) {
+ db->writeable_zone = (dns_dlz_writeablezone_t *)ptr;
+ }
+}
diff --git a/contrib/dlz/modules/ldap/testing/README b/contrib/dlz/modules/ldap/testing/README
new file mode 100644
index 0000000..69b1381
--- /dev/null
+++ b/contrib/dlz/modules/ldap/testing/README
@@ -0,0 +1,10 @@
+These files were used for testing on Ubuntu Linux using OpenLDAP.
+
+- Move aside /etc/ldap/slapd.d
+- Move slapd.conf to /etc/ldap
+- Move dlz.schema to /etc/ldap/schema/dlz.schema
+- Run "/etc/init.d/slapd restart"
+- Run "ldapadd -x -f example.ldif -D 'cn=Manager,o=bind-dlz' -w secret"
+
+LDAP server is now loaded with example.com data from the file example.ldif
+
diff --git a/contrib/dlz/modules/ldap/testing/dlz.schema b/contrib/dlz/modules/ldap/testing/dlz.schema
new file mode 100644
index 0000000..d0f0086
--- /dev/null
+++ b/contrib/dlz/modules/ldap/testing/dlz.schema
@@ -0,0 +1,192 @@
+#
+#
+# 1.3.6.1.4.1.18420.1.1.X is reserved for attribute types declared by the DLZ project.
+# 1.3.6.1.4.1.18420.1.2.X is reserved for object classes declared by the DLZ project.
+# 1.3.6.1.4.1.18420.1.3.X is reserved for PRIVATE extensions to the DLZ attribute
+# types and object classes that may be needed by end users
+# to add security, etc. Attributes and object classes using
+# this OID MUST NOT be published outside of an organization
+# except to offer them for consideration to become part of the
+# standard attributes and object classes published by the DLZ project.
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.10
+ NAME 'dlzZoneName'
+ DESC 'DNS zone name - domain name not including host name'
+ SUP name
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.20
+ NAME 'dlzHostName'
+ DESC 'Host portion of a domain name'
+ SUP name
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.30
+ NAME 'dlzData'
+ DESC 'Data for the resource record'
+ SUP name
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.40
+ NAME 'dlzType'
+ DESC 'DNS record type - A, SOA, NS, MX, etc...'
+ SUP name
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.50
+ NAME 'dlzSerial'
+ DESC 'SOA record serial number'
+ EQUALITY integerMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.60
+ NAME 'dlzRefresh'
+ DESC 'SOA record refresh time in seconds'
+ EQUALITY integerMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.70
+ NAME 'dlzRetry'
+ DESC 'SOA retry time in seconds'
+ EQUALITY integerMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.80
+ NAME 'dlzExpire'
+ DESC 'SOA expire time in seconds'
+ EQUALITY integerMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.90
+ NAME 'dlzMinimum'
+ DESC 'SOA minimum time in seconds'
+ EQUALITY integerMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.100
+ NAME 'dlzAdminEmail'
+ DESC 'E-mail address of person responsible for this zone - @ should be replaced with . (period)'
+ SUP name
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.110
+ NAME 'dlzPrimaryNS'
+ DESC 'Primary name server for this zone - should be host name not IP address'
+ SUP name
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.120
+ NAME 'dlzIPAddr'
+ DESC 'IP address - IPV4 should be in dot notation xxx.xxx.xxx.xxx IPV6 should be in colon notation xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx'
+ EQUALITY caseExactIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{40}
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.130
+ NAME 'dlzCName'
+ DESC 'DNS cname'
+ SUP name
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.140
+ NAME 'dlzPreference'
+ DESC 'DNS MX record preference. Lower numbers have higher preference'
+ EQUALITY integerMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.150
+ NAME 'dlzTTL'
+ DESC 'DNS time to live - how long this record can be cached by caching DNS servers'
+ EQUALITY integerMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.18420.1.1.160
+ NAME 'dlzRecordID'
+ DESC 'Unique ID for each DLZ resource record'
+ SUP name
+ SINGLE-VALUE )
+
+#------------------------------------------------------------------------------
+# Object class definitions
+#------------------------------------------------------------------------------
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.10
+ NAME 'dlzZone'
+ DESC 'Zone name portion of a domain name'
+ SUP top STRUCTURAL
+ MUST ( objectclass $ dlzZoneName ) )
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.20
+ NAME 'dlzHost'
+ DESC 'Host name portion of a domain name'
+ SUP top STRUCTURAL
+ MUST ( objectclass $ dlzHostName ) MAY ( description ) )
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.30
+ NAME 'dlzAbstractRecord'
+ DESC 'Data common to all DNS record types'
+ SUP top ABSTRACT
+ MUST ( objectclass $ dlzRecordID $ dlzHostName $ dlzType $ dlzTTL ) MAY ( description ) )
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.40
+ NAME 'dlzGenericRecord'
+ DESC 'Generic DNS record - useful when a specific object class has not been defined for a DNS record'
+ SUP dlzAbstractRecord STRUCTURAL
+ MUST ( dlzData ) )
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.50
+ NAME 'dlzARecord'
+ DESC 'DNS A record'
+ SUP dlzAbstractrecord STRUCTURAL
+ MUST ( dlzIPAddr ) )
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.60
+ NAME 'dlzNSRecord'
+ DESC 'DNS NS record'
+ SUP dlzGenericRecord STRUCTURAL )
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.70
+ NAME 'dlzMXRecord'
+ DESC 'DNS MX record'
+ SUP dlzGenericRecord STRUCTURAL
+ MUST ( dlzPreference ) )
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.80
+ NAME 'dlzSOARecord'
+ DESC 'DNS SOA record'
+ SUP dlzAbstractRecord STRUCTURAL
+ MUST ( dlzSerial $ dlzRefresh $ dlzRetry
+ $ dlzExpire $ dlzMinimum $ dlzAdminEmail $ dlzPrimaryNS ) )
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.90
+ NAME 'dlzTextRecord'
+ DESC 'Text data with spaces should be wrapped in double quotes'
+ SUP dlzGenericRecord STRUCTURAL )
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.100
+ NAME 'dlzPTRRecord'
+ DESC 'DNS PTR record'
+ SUP dlzGenericRecord STRUCTURAL )
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.110
+ NAME 'dlzCNameRecord'
+ DESC 'DNS CName record'
+ SUP dlzGenericRecord STRUCTURAL )
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.120
+ NAME 'dlzXFR'
+ DESC 'Host allowed to perform zone transfer'
+ SUP top STRUCTURAL
+ MUST ( objectclass $ dlzRecordID $ dlzIPAddr ) )
+
+objectclass ( 1.3.6.1.4.1.18420.1.2.130
+ NAME 'dlzDNameRecord'
+ DESC 'DNS DName record'
+ SUP dlzGenericRecord STRUCTURAL )
diff --git a/contrib/dlz/modules/ldap/testing/example.ldif b/contrib/dlz/modules/ldap/testing/example.ldif
new file mode 100644
index 0000000..fff1793
--- /dev/null
+++ b/contrib/dlz/modules/ldap/testing/example.ldif
@@ -0,0 +1,192 @@
+# server suffix - o=bind-dlz
+
+dn: o=bind-dlz
+objectclass: organization
+o: bind-dlz
+
+dn: ou=dns,o=bind-dlz
+objectclass: organizationalUnit
+ou: dns
+
+dn: dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzZone
+dlzZoneName: example.com
+
+dn: dlzHostName=@,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzHost
+dlzHostName: @
+
+dn: dlzHostName=www,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzHost
+dlzHostName: www
+
+dn: dlzHostName=mail,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzHost
+dlzHostName: mail
+
+dn: dlzHostName=backup,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzHost
+dlzHostName: backup
+
+dn: dlzHostName=ns1,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzHost
+dlzHostName: ns1
+
+dn: dlzHostName=ns2,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzHost
+dlzHostName: ns2
+
+dn: dlzHostName=~,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzHost
+dlzHostName: ~
+
+dn: dlzHostName=cname,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzHost
+dlzHostName: cname
+
+dn: dlzHostName=dname,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzHost
+dlzHostName: dname
+
+dn: dlzRecordID=1,dlzHostName=@,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzGenericRecord
+dlzRecordID: 1
+dlzHostName: @
+dlzType: txt
+dlzData: "this is a text record"
+dlzTTL: 10
+
+dn: dlzRecordID=2,dlzHostName=www,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzARecord
+dlzRecordID: 2
+dlzHostName: www
+dlzType: a
+dlzIPAddr: 192.168.0.1
+dlzTTL: 10
+
+dn: dlzRecordID=3,dlzHostName=mail,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzARecord
+dlzRecordID: 3
+dlzHostName: mail
+dlzType: a
+dlzIPAddr: 192.168.0.2
+dlzTTL: 10
+
+dn: dlzRecordID=4,dlzHostName=backup,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzARecord
+dlzRecordID: 4
+dlzHostName: backup
+dlzType: a
+dlzIPAddr: 192.168.0.3
+dlzTTL: 10
+
+dn: dlzRecordID=5,dlzHostName=@,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzMXRecord
+dlzRecordID: 5
+dlzHostName: @
+dlzType: mx
+dlzData: mail
+dlzPreference: 20
+dlzTTL: 10
+
+dn: dlzRecordID=6,dlzHostName=@,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzMXRecord
+dlzRecordID: 6
+dlzHostName: @
+dlzType: mx
+dlzData: backup
+dlzPreference: 40
+dlzTTL: 10
+
+dn: dlzRecordID=7,dlzHostName=www,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzMXRecord
+dlzRecordID: 7
+dlzHostName: www
+dlzType: mx
+dlzData: backup
+dlzPreference: 40
+dlzTTL: 10
+
+dn: dlzRecordID=8,dlzHostName=www,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzMXRecord
+dlzRecordID: 8
+dlzHostName: www
+dlzType: mx
+dlzData: mail
+dlzPreference: 20
+dlzTTL: 10
+
+dn: dlzRecordID=9,dlzHostName=ns1,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzARecord
+dlzRecordID: 9
+dlzHostName: ns1
+dlzType: a
+dlzIPAddr: 192.168.0.4
+dlzTTL: 10
+
+dn: dlzRecordID=10,dlzHostName=ns2,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzARecord
+dlzRecordID: 10
+dlzHostName: ns2
+dlzType: a
+dlzIPAddr: 192.168.0.5
+dlzTTL: 10
+
+dn: dlzRecordID=11,dlzHostName=@,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzSOARecord
+dlzRecordID: 11
+dlzHostName: @
+dlzType: soa
+dlzSerial: 2
+dlzRefresh: 2800
+dlzRetry: 7200
+dlzExpire: 604800
+dlzMinimum: 86400
+dlzAdminEmail: root.example.com.
+dlzPrimaryns: ns1.example.com.
+dlzTTL: 10
+
+dn: dlzRecordID=12,dlzHostName=@,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzNSRecord
+dlzRecordID: 12
+dlzHostName: @
+dlzType: ns
+dlzData: ns1.example.com.
+dlzTTL: 10
+
+dn: dlzRecordID=13,dlzHostName=@,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzNSRecord
+dlzRecordID: 13
+dlzHostName: @
+dlzType: ns
+dlzData: ns2
+dlzTTL: 10
+
+dn: dlzRecordID=14,dlzHostName=~,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzARecord
+dlzRecordID: 14
+dlzHostName: ~
+dlzType: a
+dlzIPAddr: 192.168.0.250
+dlzTTL: 10
+
+dn: dlzRecordID=15,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzXFR
+dlzRecordID: 15
+dlzIPAddr: 127.0.0.1
+
+dn: dlzRecordID=16,dlzHostName=cname,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzCNameRecord
+dlzRecordID: 16
+dlzHostName: cname
+dlzType: cname
+dlzData: www
+dlzTTL: 10
+
+dn: dlzRecordID=17,dlzHostName=dname,dlzZoneName=example.com,ou=dns,o=bind-dlz
+objectclass: dlzDNameRecord
+dlzRecordID: 17
+dlzHostName: dname
+dlzType: dname
+dlzData: example.net.
+dlzTTL: 10
diff --git a/contrib/dlz/modules/ldap/testing/named.conf b/contrib/dlz/modules/ldap/testing/named.conf
new file mode 100644
index 0000000..3f8378b
--- /dev/null
+++ b/contrib/dlz/modules/ldap/testing/named.conf
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+controls { };
+
+options {
+ directory ".";
+ port 5300;
+ pid-file "named.pid";
+ session-keyfile "session.key";
+ listen-on { any; };
+ listen-on-v6 { none; };
+ recursion no;
+};
+
+key rndc_key {
+ secret "1234abcd8765";
+ algorithm hmac-md5;
+};
+
+controls {
+ inet 127.0.0.1 port 9953 allow { any; } keys { rndc_key; };
+};
+
+dlz "test" {
+ database "dlopen ../dlz_ldap_dynamic.so 2
+ v3 simple {cn=Manager,o=bind-dlz} {secret} {127.0.0.1}
+ ldap:///dlzZoneName=$zone$,ou=dns,o=bind-dlz???objectclass=dlzZone
+ ldap:///dlzHostName=$record$,dlzZoneName=$zone$,ou=dns,o=bind-dlz?dlzTTL,dlzType,dlzPreference,dlzData,dlzIPAddr?sub?(&(objectclass=dlzAbstractRecord)(!(dlzType=soa)))
+ ldap:///dlzHostName=@,dlzZoneName=$zone$,ou=dns,o=bind-dlz?dlzTTL,dlzType,dlzData,dlzPrimaryNS,dlzAdminEmail,dlzSerial,dlzRefresh,dlzRetry,dlzExpire,dlzMinimum?sub?(&(objectclass=dlzAbstractRecord)(dlzType=soa))
+ ldap:///dlzZoneName=$zone$,ou=dns,o=bind-dlz?dlzTTL,dlzType,dlzHostName,dlzPreference,dlzData,dlzIPAddr,dlzPrimaryNS,dlzAdminEmail,dlzSerial,dlzRefresh,dlzRetry,dlzExpire,dlzMinimum?sub?(&(objectclass=dlzAbstractRecord)(!(dlzType=soa)))
+ ldap:///dlzZoneName=$zone$,ou=dns,o=bind-dlz??sub?(&(objectclass=dlzXFR)(dlzIPAddr=$client$))";
+};
diff --git a/contrib/dlz/modules/ldap/testing/slapd.conf b/contrib/dlz/modules/ldap/testing/slapd.conf
new file mode 100644
index 0000000..d4a6287
--- /dev/null
+++ b/contrib/dlz/modules/ldap/testing/slapd.conf
@@ -0,0 +1,44 @@
+# this is the full path to the core.schema
+include /etc/ldap/schema/core.schema
+
+# this is the full path to the dlz.schema
+include /etc/ldap/schema/dlz.schema
+
+# these files hold the slapd process ID and program args when
+# slapd is started.
+pidfile /var/run/slapd/slapd.pid
+argsfile /var/run/slapd/slapd.args
+
+modulepath /usr/lib/ldap
+moduleload back_hdb
+
+# this allows ldap version 2 connections. You should comment
+# it out if you don't need ldap version 2.
+allow bind_v2
+
+# this sets up the Berkeley DB database backend for LDAP to use.
+database hdb
+
+# This is the root of the LDAP server. You still need to add
+# an entry to this location via a LDIF file, or you won't be
+# able to add anything else into the LDAP server.
+suffix "o=bind-dlz"
+
+# this is the "username" you have to use when connecting to the
+# ldap server to make updates. Type the whole thing exactly
+# as you see it as a parameter to ldapadd.
+rootdn "cn=Manager,o=bind-dlz"
+
+# this is the "password" you have to use when connecting to the
+# ldap server to make updates.
+rootpw secret
+
+# this is the directory that the LDAP server will create the
+# Berkeley DB backend in.
+directory /var/lib/ldap
+
+# this just adds some indexing to the LDAP server.
+# probably should have more to better optimize DLZ LDAP searches.
+index cn,sn,uid pres,eq
+index objectClass eq
+