summaryrefslogtreecommitdiffstats
path: root/contrib/dlz/modules/mysql
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--contrib/dlz/modules/mysql/Makefile46
-rw-r--r--contrib/dlz/modules/mysql/dlz_mysql_dynamic.c1071
-rw-r--r--contrib/dlz/modules/mysql/testing/README7
-rw-r--r--contrib/dlz/modules/mysql/testing/dlz.data12
-rw-r--r--contrib/dlz/modules/mysql/testing/dlz.schema30
-rw-r--r--contrib/dlz/modules/mysql/testing/named.conf46
-rw-r--r--contrib/dlz/modules/mysqldyn/Makefile46
-rw-r--r--contrib/dlz/modules/mysqldyn/README87
-rw-r--r--contrib/dlz/modules/mysqldyn/dlz_mysqldyn_mod.c1800
-rw-r--r--contrib/dlz/modules/mysqldyn/testing/README11
-rw-r--r--contrib/dlz/modules/mysqldyn/testing/dlz.data18
-rw-r--r--contrib/dlz/modules/mysqldyn/testing/dlz.schema31
-rw-r--r--contrib/dlz/modules/mysqldyn/testing/named.conf39
13 files changed, 3244 insertions, 0 deletions
diff --git a/contrib/dlz/modules/mysql/Makefile b/contrib/dlz/modules/mysql/Makefile
new file mode 100644
index 0000000..5d0904c
--- /dev/null
+++ b/contrib/dlz/modules/mysql/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 $(shell mysql_config --cflags)
+MYSQL_LIBS=$(shell mysql_config --libs)
+
+all: dlz_mysql_dynamic.so
+
+dlz_dbi.o: ../common/dlz_dbi.c
+ $(CC) $(CFLAGS) -c ../common/dlz_dbi.c
+
+dlz_mysql_dynamic.so: dlz_mysql_dynamic.c dlz_dbi.o
+ $(CC) $(CFLAGS) -shared -o dlz_mysql_dynamic.so \
+ dlz_mysql_dynamic.c dlz_dbi.o $(MYSQL_LIBS)
+
+clean:
+ rm -f dlz_mysql_dynamic.so *.o
+
+install: dlz_mysql_dynamic.so
+ mkdir -p $(DESTDIR)$(libdir)
+ install dlz_mysql_dynamic.so $(DESTDIR)$(libdir)
diff --git a/contrib/dlz/modules/mysql/dlz_mysql_dynamic.c b/contrib/dlz/modules/mysql/dlz_mysql_dynamic.c
new file mode 100644
index 0000000..a6e0ebf
--- /dev/null
+++ b/contrib/dlz/modules/mysql/dlz_mysql_dynamic.c
@@ -0,0 +1,1071 @@
+/*
+ * 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 MySQL DLZ module, without
+ * update support
+ */
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <mysql/mysql.h>
+
+#include <dlz_dbi.h>
+#include <dlz_list.h>
+#include <dlz_minimal.h>
+#include <dlz_pthread.h>
+
+#if !defined(MARIADB_BASE_VERSION) && MYSQL_VERSION_ID >= 80000
+typedef bool my_bool;
+#endif /* !defined(MARIADB_BASE_VERSION) && MYSQL_VERSION_ID >= 80000 */
+
+#define dbc_search_limit 30
+#define ALLNODES 1
+#define ALLOWXFR 2
+#define AUTHORITY 3
+#define FINDZONE 4
+#define COUNTZONE 5
+#define LOOKUP 6
+
+#define safeGet(in) in == NULL ? "" : in
+
+/*%
+ * Structure to hold everything needed by this "instance" of the MySQL
+ * module remember, the module code is only loaded once, but may have
+ * many separate instances.
+ */
+typedef struct {
+ db_list_t *db; /*%< handle to a list of DB */
+ int dbcount;
+
+ unsigned int flags;
+ char *dbname;
+ char *host;
+ char *user;
+ char *pass;
+ char *socket;
+ int port;
+
+ /* 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;
+} mysql_instance_t;
+
+/* forward references */
+isc_result_t
+dlz_findzonedb(void *dbdata, const char *name, dns_clientinfomethods_t *methods,
+ dns_clientinfo_t *clientinfo);
+
+void
+dlz_destroy(void *dbdata);
+
+static void
+b9_add_helper(mysql_instance_t *db, const char *helper_name, void *ptr);
+
+/*
+ * Private methods
+ */
+
+static void
+dlz_mysql_destroy(dbinstance_t *db) {
+ /* release DB connection */
+ if (db->dbconn != NULL) {
+ mysql_close((MYSQL *)db->dbconn);
+ }
+
+ /* destroy DB instance */
+ destroy_dbinstance(db);
+}
+
+/*%
+ * Properly cleans up a list of database instances.
+ * This function is only used when the module is compiled for
+ * multithreaded operation.
+ */
+static void
+dlz_mysql_destroy_dblist(db_list_t *dblist) {
+ dbinstance_t *ndbi = NULL;
+ dbinstance_t *dbi = NULL;
+
+ ndbi = DLZ_LIST_HEAD(*dblist);
+ while (ndbi != NULL) {
+ dbi = ndbi;
+ ndbi = DLZ_LIST_NEXT(dbi, link);
+
+ dlz_mysql_destroy(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 module is compiled for
+ * multithreaded operation.
+ */
+static dbinstance_t *
+mysql_find_avail_conn(mysql_instance_t *mysql) {
+ dbinstance_t *dbi = NULL, *head;
+ int count = 0;
+
+ /* get top of list */
+ head = dbi = DLZ_LIST_HEAD(*(mysql->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;
+ }
+ }
+
+ mysql->log(ISC_LOG_INFO,
+ "MySQL module unable to find available connection "
+ "after searching %d times",
+ count);
+ return (NULL);
+}
+
+/*%
+ * Allocates memory for a new string, and then constructs the new
+ * string by "escaping" the input string. The new string is
+ * safe to be used in queries. This is necessary because we cannot
+ * be sure of what types of strings are passed to us, and we don't
+ * want special characters in the string causing problems.
+ */
+static char *
+mysqldrv_escape_string(MYSQL *mysql, const char *instr) {
+ char *outstr;
+ unsigned int len;
+
+ if (instr == NULL) {
+ return (NULL);
+ }
+
+ len = strlen(instr);
+ outstr = malloc((2 * len * sizeof(char)) + 1);
+ if (outstr == NULL) {
+ return (NULL);
+ }
+
+ mysql_real_escape_string(mysql, outstr, instr, len);
+
+ return (outstr);
+}
+
+/*%
+ * This function is the real core of the module. 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 a single database instance.
+ * The function will construct and run the query, hopefully getting
+ * a result set.
+ */
+static isc_result_t
+mysql_get_resultset(const char *zone, const char *record, const char *client,
+ unsigned int query, void *dbdata, MYSQL_RES **rs) {
+ isc_result_t result;
+ dbinstance_t *dbi = NULL;
+ mysql_instance_t *db = (mysql_instance_t *)dbdata;
+ char *querystring = NULL;
+ unsigned int i = 0;
+ unsigned int j = 0;
+ int qres = 0;
+
+ /* find an available DBI from the list */
+ dbi = mysql_find_avail_conn(db);
+
+ if (dbi == NULL) {
+ return (ISC_R_FAILURE);
+ }
+
+ /* what type of query are we going to run? */
+ switch (query) {
+ case ALLNODES:
+ if (dbi->allnodes_q == NULL) {
+ result = ISC_R_NOTIMPLEMENTED;
+ goto cleanup;
+ }
+ break;
+ case ALLOWXFR:
+ if (dbi->allowxfr_q == NULL) {
+ result = ISC_R_NOTIMPLEMENTED;
+ goto cleanup;
+ }
+ break;
+ case AUTHORITY:
+ if (dbi->authority_q == NULL) {
+ result = ISC_R_NOTIMPLEMENTED;
+ goto cleanup;
+ }
+ break;
+ case FINDZONE:
+ 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;
+ }
+ break;
+ case COUNTZONE:
+ if (dbi->countzone_q == NULL) {
+ result = ISC_R_NOTIMPLEMENTED;
+ goto cleanup;
+ }
+ break;
+ case LOOKUP:
+ 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;
+ }
+ break;
+ default:
+ db->log(ISC_LOG_ERROR, "Incorrect query flag passed to "
+ "mysql_get_resultset");
+ result = ISC_R_UNEXPECTED;
+ goto cleanup;
+ }
+
+ if (zone != NULL) {
+ if (dbi->zone != NULL) {
+ free(dbi->zone);
+ }
+
+ dbi->zone = mysqldrv_escape_string((MYSQL *)dbi->dbconn, zone);
+ if (dbi->zone == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+ } else {
+ dbi->zone = NULL;
+ }
+
+ if (record != NULL) {
+ if (dbi->record != NULL) {
+ free(dbi->record);
+ }
+
+ dbi->record = mysqldrv_escape_string((MYSQL *)dbi->dbconn,
+ record);
+ if (dbi->record == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+ } else {
+ dbi->record = NULL;
+ }
+
+ if (client != NULL) {
+ if (dbi->client != NULL) {
+ free(dbi->client);
+ }
+
+ dbi->client = mysqldrv_escape_string((MYSQL *)dbi->dbconn,
+ client);
+ if (dbi->client == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+ } else {
+ dbi->client = NULL;
+ }
+
+ /*
+ * what type of query are we going to run? this time we build
+ * the actual query to run.
+ */
+ switch (query) {
+ case ALLNODES:
+ querystring = build_querystring(dbi->allnodes_q);
+ break;
+ case ALLOWXFR:
+ querystring = build_querystring(dbi->allowxfr_q);
+ break;
+ case AUTHORITY:
+ querystring = build_querystring(dbi->authority_q);
+ break;
+ case FINDZONE:
+ querystring = build_querystring(dbi->findzone_q);
+ break;
+ case COUNTZONE:
+ querystring = build_querystring(dbi->countzone_q);
+ break;
+ case LOOKUP:
+ querystring = build_querystring(dbi->lookup_q);
+ break;
+ default:
+ db->log(ISC_LOG_ERROR, "Incorrect query flag passed to "
+ "mysql_get_resultset");
+ result = ISC_R_UNEXPECTED;
+ goto cleanup;
+ }
+
+ if (querystring == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+
+ /* output the full query string when debugging */
+ db->log(ISC_LOG_DEBUG(1), "\nQuery String: %s\n", querystring);
+
+ /* attempt query up to 3 times. */
+ for (i = 0; i < 3; i++) {
+ qres = mysql_query((MYSQL *)dbi->dbconn, querystring);
+ if (qres == 0) {
+ break;
+ }
+ for (j = 0; j < 4; j++) {
+ if (mysql_ping((MYSQL *)dbi->dbconn) == 0) {
+ break;
+ }
+ }
+ }
+
+ if (qres == 0) {
+ result = ISC_R_SUCCESS;
+ if (query != COUNTZONE) {
+ *rs = mysql_store_result((MYSQL *)dbi->dbconn);
+ if (*rs == NULL) {
+ result = ISC_R_FAILURE;
+ }
+ }
+ } else {
+ result = ISC_R_FAILURE;
+ }
+
+cleanup:
+ if (dbi->zone != NULL) {
+ free(dbi->zone);
+ dbi->zone = NULL;
+ }
+ if (dbi->record != NULL) {
+ free(dbi->record);
+ dbi->record = NULL;
+ }
+ if (dbi->client != NULL) {
+ free(dbi->client);
+ dbi->client = NULL;
+ }
+
+ /* release the lock so another thread can use this dbi */
+ (void)dlz_mutex_unlock(&dbi->lock);
+
+ if (querystring != NULL) {
+ free(querystring);
+ }
+
+ return (result);
+}
+
+/*%
+ * The processing of result sets for lookup and authority are
+ * exactly the same. So that functionality has been moved
+ * into this function to minimize code.
+ */
+static isc_result_t
+mysql_process_rs(mysql_instance_t *db, dns_sdlzlookup_t *lookup,
+ MYSQL_RES *rs) {
+ isc_result_t result = ISC_R_NOTFOUND;
+ MYSQL_ROW row;
+ unsigned int fields;
+ unsigned int j;
+ char *tmpString;
+ char *endp;
+ int ttl;
+
+ fields = mysql_num_fields(rs); /* how many columns in result set */
+ row = mysql_fetch_row(rs); /* get a row from the result set */
+ while (row != NULL) {
+ unsigned int len = 0;
+
+ switch (fields) {
+ case 1:
+ /*
+ * one column in rs, it's the data field. use
+ * default type of A record, and default TTL
+ * of 86400
+ */
+ result = db->putrr(lookup, "a", 86400, safeGet(row[0]));
+ break;
+ case 2:
+ /*
+ * two columns, data field, and data type.
+ * use default TTL of 86400.
+ */
+ result = db->putrr(lookup, safeGet(row[0]), 86400,
+ safeGet(row[1]));
+ break;
+ case 3:
+ /*
+ * three columns, all data no defaults.
+ * convert text to int, make sure it worked
+ * right.
+ */
+ ttl = strtol(safeGet(row[0]), &endp, 10);
+ if (*endp != '\0' || ttl < 0) {
+ db->log(ISC_LOG_ERROR, "MySQL module ttl must "
+ "be "
+ "a positive number");
+ return (ISC_R_FAILURE);
+ }
+
+ result = db->putrr(lookup, safeGet(row[1]), ttl,
+ safeGet(row[2]));
+ break;
+ default:
+ /*
+ * more than 3 fields, concatenate the last
+ * ones together. figure out how long to make
+ * string.
+ */
+ for (j = 2; j < fields; j++) {
+ len += strlen(safeGet(row[j])) + 1;
+ }
+
+ /*
+ * allocate string memory, allow for NULL to
+ * term string
+ */
+ tmpString = malloc(len + 1);
+ if (tmpString == NULL) {
+ db->log(ISC_LOG_ERROR, "MySQL module unable to "
+ "allocate "
+ "memory for temporary "
+ "string");
+ mysql_free_result(rs);
+ return (ISC_R_FAILURE);
+ }
+
+ strcpy(tmpString, safeGet(row[2]));
+ for (j = 3; j < fields; j++) {
+ strcat(tmpString, " ");
+ strcat(tmpString, safeGet(row[j]));
+ }
+
+ ttl = strtol(safeGet(row[0]), &endp, 10);
+ if (*endp != '\0' || ttl < 0) {
+ db->log(ISC_LOG_ERROR, "MySQL module ttl must "
+ "be "
+ "a positive number");
+ free(tmpString);
+ return (ISC_R_FAILURE);
+ }
+
+ result = db->putrr(lookup, safeGet(row[1]), ttl,
+ tmpString);
+ free(tmpString);
+ }
+
+ if (result != ISC_R_SUCCESS) {
+ mysql_free_result(rs);
+ db->log(ISC_LOG_ERROR, "putrr returned error: %d",
+ result);
+ return (ISC_R_FAILURE);
+ }
+
+ row = mysql_fetch_row(rs);
+ }
+
+ mysql_free_result(rs);
+ return (result);
+}
+
+/*
+ * DLZ methods
+ */
+
+/*% determine if the zone is supported by (in) the database */
+isc_result_t
+dlz_findzonedb(void *dbdata, const char *name, dns_clientinfomethods_t *methods,
+ dns_clientinfo_t *clientinfo) {
+ isc_result_t result;
+ MYSQL_RES *rs = NULL;
+ my_ulonglong rows;
+ mysql_instance_t *db = (mysql_instance_t *)dbdata;
+
+ UNUSED(methods);
+ UNUSED(clientinfo);
+
+ result = mysql_get_resultset(name, NULL, NULL, FINDZONE, dbdata, &rs);
+ if (result != ISC_R_SUCCESS || rs == NULL) {
+ if (rs != NULL) {
+ mysql_free_result(rs);
+ }
+
+ db->log(ISC_LOG_ERROR, "MySQL module unable to return "
+ "result set for findzone query");
+
+ return (ISC_R_FAILURE);
+ }
+
+ /*
+ * if we returned any rows, the zone is supported.
+ */
+ rows = mysql_num_rows(rs);
+ mysql_free_result(rs);
+ if (rows > 0) {
+ mysql_get_resultset(name, NULL, NULL, COUNTZONE, dbdata, NULL);
+ return (ISC_R_SUCCESS);
+ }
+
+ return (ISC_R_NOTFOUND);
+}
+
+/*% Determine if the client is allowed to perform a zone transfer */
+isc_result_t
+dlz_allowzonexfr(void *dbdata, const char *name, const char *client) {
+ isc_result_t result;
+ mysql_instance_t *db = (mysql_instance_t *)dbdata;
+ MYSQL_RES *rs = NULL;
+ my_ulonglong rows;
+
+ /* first check if the zone is supported by the database. */
+ result = dlz_findzonedb(dbdata, name, NULL, NULL);
+ if (result != ISC_R_SUCCESS) {
+ return (ISC_R_NOTFOUND);
+ }
+
+ /*
+ * if we get to this point we know the zone is supported by
+ * the database the only questions now are is the zone
+ * transfer is allowed for this client and did the config file
+ * have an allow zone xfr query.
+ */
+ result = mysql_get_resultset(name, NULL, client, ALLOWXFR, dbdata, &rs);
+ if (result == ISC_R_NOTIMPLEMENTED) {
+ return (result);
+ }
+
+ if (result != ISC_R_SUCCESS || rs == NULL) {
+ if (rs != NULL) {
+ mysql_free_result(rs);
+ }
+ db->log(ISC_LOG_ERROR, "MySQL module unable to return "
+ "result set for allow xfr query");
+ return (ISC_R_FAILURE);
+ }
+
+ /*
+ * count how many rows in result set; if we returned any,
+ * zone xfr is allowed.
+ */
+ rows = mysql_num_rows(rs);
+ mysql_free_result(rs);
+ if (rows > 0) {
+ return (ISC_R_SUCCESS);
+ }
+
+ return (ISC_R_NOPERM);
+}
+
+/*%
+ * If the client is allowed to perform a zone transfer, the next order of
+ * business is to get all the nodes in the zone, so bind can respond to the
+ * query.
+ */
+isc_result_t
+dlz_allnodes(const char *zone, void *dbdata, dns_sdlzallnodes_t *allnodes) {
+ isc_result_t result;
+ mysql_instance_t *db = (mysql_instance_t *)dbdata;
+ MYSQL_RES *rs = NULL;
+ MYSQL_ROW row;
+ unsigned int fields;
+ unsigned int j;
+ char *tmpString;
+ char *endp;
+ int ttl;
+
+ result = mysql_get_resultset(zone, NULL, NULL, ALLNODES, dbdata, &rs);
+ if (result == ISC_R_NOTIMPLEMENTED) {
+ return (result);
+ }
+
+ /* if we didn't get a result set, log an err msg. */
+ if (result != ISC_R_SUCCESS) {
+ db->log(ISC_LOG_ERROR, "MySQL module unable to return "
+ "result set for all nodes query");
+ goto cleanup;
+ }
+
+ result = ISC_R_NOTFOUND;
+
+ fields = mysql_num_fields(rs); /* how many columns in result set */
+ row = mysql_fetch_row(rs); /* get a row from the result set */
+ while (row != NULL) {
+ if (fields < 4) {
+ db->log(ISC_LOG_ERROR, "MySQL module too few fields "
+ "returned "
+ "by all nodes query");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ ttl = strtol(safeGet(row[0]), &endp, 10);
+ if (*endp != '\0' || ttl < 0) {
+ db->log(ISC_LOG_ERROR, "MySQL module ttl must be "
+ "a positive number");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ if (fields == 4) {
+ result = db->putnamedrr(allnodes, safeGet(row[2]),
+ safeGet(row[1]), ttl,
+ safeGet(row[3]));
+ } else {
+ unsigned int len = 0;
+
+ /*
+ * more than 4 fields, concatenate the last
+ * ones together.
+ */
+ for (j = 3; j < fields; j++) {
+ len += strlen(safeGet(row[j])) + 1;
+ }
+
+ tmpString = malloc(len + 1);
+ if (tmpString == NULL) {
+ db->log(ISC_LOG_ERROR, "MySQL module unable to "
+ "allocate "
+ "memory for temporary "
+ "string");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ strcpy(tmpString, safeGet(row[3]));
+ for (j = 4; j < fields; j++) {
+ strcat(tmpString, " ");
+ strcat(tmpString, safeGet(row[j]));
+ }
+
+ result = db->putnamedrr(allnodes, safeGet(row[2]),
+ safeGet(row[1]), ttl,
+ tmpString);
+ free(tmpString);
+ }
+
+ if (result != ISC_R_SUCCESS) {
+ db->log(ISC_LOG_ERROR, "putnamedrr returned error: %s",
+ result);
+ result = ISC_R_FAILURE;
+ break;
+ }
+
+ row = mysql_fetch_row(rs);
+ }
+
+cleanup:
+ if (rs != NULL) {
+ mysql_free_result(rs);
+ }
+
+ return (result);
+}
+
+/*%
+ * If the lookup function does not return SOA or NS records for the zone,
+ * use this function to get that information for named.
+ */
+isc_result_t
+dlz_authority(const char *zone, void *dbdata, dns_sdlzlookup_t *lookup) {
+ isc_result_t result;
+ MYSQL_RES *rs = NULL;
+ mysql_instance_t *db = (mysql_instance_t *)dbdata;
+
+ result = mysql_get_resultset(zone, NULL, NULL, AUTHORITY, dbdata, &rs);
+ if (result == ISC_R_NOTIMPLEMENTED) {
+ return (result);
+ }
+
+ if (result != ISC_R_SUCCESS) {
+ if (rs != NULL) {
+ mysql_free_result(rs);
+ }
+ db->log(ISC_LOG_ERROR, "MySQL module unable to return "
+ "result set for authority query");
+ return (ISC_R_FAILURE);
+ }
+
+ /*
+ * lookup and authority result sets are processed in the same
+ * manner: mysql_process_rs does the job for both functions.
+ */
+ return (mysql_process_rs(db, lookup, rs));
+}
+
+/*% If zone is supported, lookup up a (or multiple) record(s) in it */
+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) {
+ isc_result_t result;
+ MYSQL_RES *rs = NULL;
+ mysql_instance_t *db = (mysql_instance_t *)dbdata;
+
+ UNUSED(methods);
+ UNUSED(clientinfo);
+
+ result = mysql_get_resultset(zone, name, NULL, LOOKUP, dbdata, &rs);
+
+ /* if we didn't get a result set, log an err msg. */
+ if (result != ISC_R_SUCCESS) {
+ if (rs != NULL) {
+ mysql_free_result(rs);
+ }
+ db->log(ISC_LOG_ERROR, "MySQL module unable to return "
+ "result set for lookup query");
+ return (ISC_R_FAILURE);
+ }
+
+ /*
+ * lookup and authority result sets are processed in the same
+ * manner: mysql_process_rs does the job for both functions.
+ */
+ return (mysql_process_rs(db, lookup, rs));
+}
+
+/*%
+ * Create an instance of the module.
+ */
+isc_result_t
+dlz_create(const char *dlzname, unsigned int argc, char *argv[], void **dbdata,
+ ...) {
+ isc_result_t result = ISC_R_FAILURE;
+ mysql_instance_t *mysql = NULL;
+ dbinstance_t *dbi = NULL;
+ MYSQL *dbc;
+ char *tmp = NULL;
+ char *endp = NULL;
+ const char *helper_name;
+ int dbcount, i, j;
+ va_list ap;
+
+ UNUSED(dlzname);
+
+ /* allocate memory for MySQL instance */
+ mysql = calloc(1, sizeof(mysql_instance_t));
+ if (mysql == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+ memset(mysql, 0, sizeof(mysql_instance_t));
+
+ /* Fill in the helper functions */
+ va_start(ap, dbdata);
+ while ((helper_name = va_arg(ap, const char *)) != NULL) {
+ b9_add_helper(mysql, helper_name, va_arg(ap, void *));
+ }
+ va_end(ap);
+
+ /* if debugging, let user know we are multithreaded. */
+ mysql->log(ISC_LOG_DEBUG(1), "MySQL module running multithreaded");
+
+ /* verify we have at least 4 arg's passed to the module */
+ if (argc < 4) {
+ mysql->log(ISC_LOG_ERROR, "MySQL module requires "
+ "at least 4 command line args.");
+ return (ISC_R_FAILURE);
+ }
+
+ /* no more than 8 arg's should be passed to the module */
+ if (argc > 8) {
+ mysql->log(ISC_LOG_ERROR, "MySQL module cannot accept "
+ "more than 7 command line args.");
+ return (ISC_R_FAILURE);
+ }
+
+ /* get db name - required */
+ mysql->dbname = get_parameter_value(argv[1], "dbname=");
+ if (mysql->dbname == NULL) {
+ mysql->log(ISC_LOG_ERROR, "MySQL module requires a dbname "
+ "parameter.");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ /* get db port. Not required, but must be > 0 if specified */
+ tmp = get_parameter_value(argv[1], "port=");
+ if (tmp == NULL) {
+ mysql->port = 0;
+ } else {
+ mysql->port = strtol(tmp, &endp, 10);
+ if (*endp != '\0' || mysql->port < 0) {
+ mysql->log(ISC_LOG_ERROR, "Mysql module: port "
+ "must be a positive number.");
+ free(tmp);
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+ free(tmp);
+ }
+
+ mysql->host = get_parameter_value(argv[1], "host=");
+ mysql->user = get_parameter_value(argv[1], "user=");
+ mysql->pass = get_parameter_value(argv[1], "pass=");
+ mysql->socket = get_parameter_value(argv[1], "socket=");
+
+ mysql->flags = CLIENT_REMEMBER_OPTIONS;
+
+ tmp = get_parameter_value(argv[1], "compress=");
+ if (tmp != NULL) {
+ if (strcasecmp(tmp, "true") == 0) {
+ mysql->flags |= CLIENT_COMPRESS;
+ }
+ free(tmp);
+ }
+
+ tmp = get_parameter_value(argv[1], "ssl=");
+ if (tmp != NULL) {
+ if (strcasecmp(tmp, "true") == 0) {
+ mysql->flags |= CLIENT_SSL;
+ }
+ free(tmp);
+ }
+
+ tmp = get_parameter_value(argv[1], "space=");
+ if (tmp != NULL) {
+ if (strcasecmp(tmp, "ignore") == 0) {
+ mysql->flags |= CLIENT_IGNORE_SPACE;
+ }
+ free(tmp);
+ }
+
+ /* multithreaded build can have multiple DB connections */
+ tmp = get_parameter_value(argv[1], "threads=");
+ if (tmp == NULL) {
+ dbcount = 1;
+ } else {
+ dbcount = strtol(tmp, &endp, 10);
+ if (*endp != '\0' || dbcount < 1) {
+ mysql->log(ISC_LOG_ERROR, "MySQL database connection "
+ "count "
+ "must be positive.");
+ free(tmp);
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+ free(tmp);
+ }
+
+ /* allocate memory for database connection list */
+ mysql->db = calloc(1, sizeof(db_list_t));
+ if (mysql->db == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+
+ /* initialize DB connection list */
+ DLZ_LIST_INIT(*(mysql->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++) {
+ switch (argc) {
+ case 4:
+ result = build_dbinstance(NULL, NULL, NULL, argv[2],
+ argv[3], NULL, &dbi,
+ mysql->log);
+ break;
+ case 5:
+ result = build_dbinstance(NULL, NULL, argv[4], argv[2],
+ argv[3], NULL, &dbi,
+ mysql->log);
+ break;
+ case 6:
+ result = build_dbinstance(argv[5], NULL, argv[4],
+ argv[2], argv[3], NULL, &dbi,
+ mysql->log);
+ break;
+ case 7:
+ result = build_dbinstance(argv[5], argv[6], argv[4],
+ argv[2], argv[3], NULL, &dbi,
+ mysql->log);
+ break;
+ case 8:
+ result = build_dbinstance(argv[5], argv[6], argv[4],
+ argv[2], argv[3], argv[7],
+ &dbi, mysql->log);
+ break;
+ default:
+ result = ISC_R_FAILURE;
+ }
+
+ if (result != ISC_R_SUCCESS) {
+ mysql->log(ISC_LOG_ERROR, "MySQL module could not "
+ "create "
+ "database instance object.");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ /* when multithreaded, build a list of DBI's */
+ DLZ_LINK_INIT(dbi, link);
+ DLZ_LIST_APPEND(*(mysql->db), dbi, link);
+
+ /* create and set db connection */
+ dbi->dbconn = mysql_init(NULL);
+ if (dbi->dbconn == NULL) {
+ mysql->log(ISC_LOG_ERROR, "MySQL module could not "
+ "allocate "
+ "memory for database "
+ "connection");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ dbc = NULL;
+
+ /* enable automatic reconnection. */
+ if (mysql_options((MYSQL *)dbi->dbconn, MYSQL_OPT_RECONNECT,
+ &(my_bool){ 1 }) != 0)
+ {
+ mysql->log(ISC_LOG_WARNING, "MySQL module failed to "
+ "set "
+ "MYSQL_OPT_RECONNECT "
+ "option, continuing");
+ }
+
+ for (j = 0; dbc == NULL && j < 4; j++) {
+ dbc = mysql_real_connect(
+ (MYSQL *)dbi->dbconn, mysql->host, mysql->user,
+ mysql->pass, mysql->dbname, mysql->port,
+ mysql->socket, mysql->flags);
+ if (dbc == NULL) {
+ mysql->log(ISC_LOG_ERROR,
+ "MySQL connection failed: %s",
+ mysql_error((MYSQL *)dbi->dbconn));
+ }
+ }
+
+ if (dbc == NULL) {
+ mysql->log(ISC_LOG_ERROR, "MySQL module failed to "
+ "create "
+ "database connection after 4 "
+ "attempts");
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ /* set DBI = null for next loop through. */
+ dbi = NULL;
+ }
+
+ *dbdata = mysql;
+
+ return (ISC_R_SUCCESS);
+
+cleanup:
+ dlz_destroy(mysql);
+
+ return (result);
+}
+
+/*%
+ * Destroy the module.
+ */
+void
+dlz_destroy(void *dbdata) {
+ mysql_instance_t *db = (mysql_instance_t *)dbdata;
+
+ /* cleanup the list of DBI's */
+ if (db->db != NULL) {
+ dlz_mysql_destroy_dblist((db_list_t *)(db->db));
+ }
+
+ if (db->dbname != NULL) {
+ free(db->dbname);
+ }
+ if (db->host != NULL) {
+ free(db->host);
+ }
+ if (db->user != NULL) {
+ free(db->user);
+ }
+ if (db->pass != NULL) {
+ free(db->pass);
+ }
+ if (db->socket != NULL) {
+ free(db->socket);
+ }
+}
+
+/*
+ * Return the version of the API
+ */
+int
+dlz_version(unsigned int *flags) {
+ *flags |= (DNS_SDLZFLAG_RELATIVEOWNER | 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(mysql_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/mysql/testing/README b/contrib/dlz/modules/mysql/testing/README
new file mode 100644
index 0000000..a4b87bb
--- /dev/null
+++ b/contrib/dlz/modules/mysql/testing/README
@@ -0,0 +1,7 @@
+These files were used for testing on Ubuntu Linux using MySQL
+
+- Install MySQL: sudo apt-get install mysql-server
+- Run "mysql --user=USER --password=PASSWORD < dlz.schema" to set up database
+- Run "mysql --user=USER --password=PASSWORD < dlz.data" to populate it
+- update named.conf with correct USER and PASSWORD
+
diff --git a/contrib/dlz/modules/mysql/testing/dlz.data b/contrib/dlz/modules/mysql/testing/dlz.data
new file mode 100644
index 0000000..cef3e13
--- /dev/null
+++ b/contrib/dlz/modules/mysql/testing/dlz.data
@@ -0,0 +1,12 @@
+use BindDB;
+INSERT INTO `records` (`id`, `zone`, `ttl`, `type`, `host`, `mx_priority`, `data`, `primary_ns`, `resp_contact`, `serial`, `refresh`, `retry`, `expire`, `minimum`) VALUES
+(1, 'example.com', 86400, 'SOA', '@', NULL, NULL, 'ns1.example.com.', 'info.example.com.', 2011043001, 10800, 7200, 604800, 86400),
+(2, 'example.com', 86400, 'NS', '@', NULL, 'ns1.example.com.', NULL, NULL, NULL, NULL, NULL, NULL, NULL),
+(3, 'example.com', 86400, 'NS', '@', NULL, 'ns2.example.com.', NULL, NULL, NULL, NULL, NULL, NULL, NULL),
+(4, 'example.com', 86400, 'MX', '@', 10, 'mail.example.com.', NULL, NULL, NULL, NULL, NULL, NULL, NULL),
+(5, 'example.com', 86400, 'A', '@', NULL, '192.168.0.2', NULL, NULL, NULL, NULL, NULL, NULL, NULL),
+(6, 'example.com', 86400, 'CNAME', 'www', NULL, '@', NULL, NULL, NULL, NULL, NULL, NULL, NULL),
+(7, 'example.com', 86400, 'A', 'ns1', NULL, '192.168.0.111', NULL, NULL, NULL, NULL, NULL, NULL, NULL),
+(8, 'example.com', 86400, 'A', 'ns2', NULL, '192.168.0.222', NULL, NULL, NULL, NULL, NULL, NULL, NULL),
+(9, 'example.com', 86400, 'A', 'mail', NULL, '192.168.0.3', NULL, NULL, NULL, NULL, NULL, NULL, NULL),
+(10, 'example.com', 86400, 'TXT', '@', NULL, 'v=spf1 ip:192.168.0.3 ~all', NULL, NULL, NULL, NULL, NULL, NULL, NULL)
diff --git a/contrib/dlz/modules/mysql/testing/dlz.schema b/contrib/dlz/modules/mysql/testing/dlz.schema
new file mode 100644
index 0000000..f20b59e
--- /dev/null
+++ b/contrib/dlz/modules/mysql/testing/dlz.schema
@@ -0,0 +1,30 @@
+CREATE DATABASE `BindDB` DEFAULT CHARACTER SET latin1;
+USE `BindDB`;
+
+CREATE TABLE IF NOT EXISTS `records` (
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `zone` varchar(255) NOT NULL,
+ `ttl` int(11) NOT NULL DEFAULT '86400',
+ `type` varchar(255) NOT NULL,
+ `host` varchar(255) NOT NULL DEFAULT '@',
+ `mx_priority` int(11) DEFAULT NULL,
+ `data` text,
+ `primary_ns` varchar(255) DEFAULT NULL,
+ `resp_contact` varchar(255) DEFAULT NULL,
+ `serial` bigint(20) DEFAULT NULL,
+ `refresh` int(11) DEFAULT NULL,
+ `retry` int(11) DEFAULT NULL,
+ `expire` int(11) DEFAULT NULL,
+ `minimum` int(11) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `type` (`type`),
+ KEY `host` (`host`),
+ KEY `zone` (`zone`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+CREATE TABLE IF NOT EXISTS `xfr` (
+ `zone` varchar(255) NOT NULL,
+ `client` varchar(255) NOT NULL,
+ KEY `zone` (`zone`),
+ KEY `client` (`client`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
diff --git a/contrib/dlz/modules/mysql/testing/named.conf b/contrib/dlz/modules/mysql/testing/named.conf
new file mode 100644
index 0000000..1152143
--- /dev/null
+++ b/contrib/dlz/modules/mysql/testing/named.conf
@@ -0,0 +1,46 @@
+/*
+ * 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_mysql_dynamic.so
+ {
+ host=127.0.0.1 port=3306 socket=/tmp/mysql.sock
+ dbname=BindDB user=USER pass=PASSWORD threads=2
+ }
+ {SELECT zone FROM records WHERE zone = '$zone$'}
+ {SELECT ttl, type, mx_priority, IF(type = 'TXT', CONCAT('\"',data,'\"'), data) AS data FROM records WHERE zone = '$zone$' AND host = '$record$' AND type <> 'SOA' AND type <> 'NS'}
+ {SELECT ttl, type, data, primary_ns, resp_contact, serial, refresh, retry, expire, minimum FROM records WHERE zone = '$zone$' AND (type = 'SOA' OR type='NS')}
+ {SELECT ttl, type, host, mx_priority, IF(type = 'TXT', CONCAT('\"',data,'\"'), data) AS data, resp_contact, serial, refresh, retry, expire, minimum FROM records WHERE zone = '$zone$' AND type <> 'SOA' AND type <> 'NS'}
+ {SELECT zone FROM xfr where zone='$zone$' AND client = '$client$'}";
+};
diff --git a/contrib/dlz/modules/mysqldyn/Makefile b/contrib/dlz/modules/mysqldyn/Makefile
new file mode 100644
index 0000000..f78cd2f
--- /dev/null
+++ b/contrib/dlz/modules/mysqldyn/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 $(shell mysql_config --cflags)
+MYSQL_LIBS=$(shell mysql_config --libs)
+
+all: dlz_mysqldyn_mod.so
+
+dlz_dbi.o: ../common/dlz_dbi.c
+ $(CC) $(CFLAGS) -c ../common/dlz_dbi.c
+
+dlz_mysqldyn_mod.so: dlz_mysqldyn_mod.c dlz_dbi.o
+ $(CC) $(CFLAGS) -shared -o dlz_mysqldyn_mod.so \
+ dlz_mysqldyn_mod.c dlz_dbi.o $(MYSQL_LIBS)
+
+clean:
+ rm -f dlz_mysqldyn_mod.so *.o
+
+install: dlz_mysqldyn_mod.so
+ mkdir -p $(DESTDIR)$(libdir)
+ install dlz_mysqldyn_mod.so $(DESTDIR)$(libdir)
diff --git a/contrib/dlz/modules/mysqldyn/README b/contrib/dlz/modules/mysqldyn/README
new file mode 100644
index 0000000..7f93c24
--- /dev/null
+++ b/contrib/dlz/modules/mysqldyn/README
@@ -0,0 +1,87 @@
+<!--
+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.
+-->
+
+BIND 9 DLZ MySQL module with support for dynamic DNS (DDNS)
+
+Adapted from code contributed by Marty Lee, Maui Systems Ltd.
+
+This is a dynamically loadable zone (DLZ) plugin that uses a fixed-
+schema MySQL database for back-end storage. It allows zone data
+to be updated via dynamic DNS updates, and sends DNS NOTIFY packets
+to other name servers when appropriate.
+
+The database for this module uses the following schema:
+
+ CREATE TABLE `Zones` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `domain` varchar(128) NOT NULL DEFAULT '',
+ `host` varchar(128) NOT NULL DEFAULT '',
+ `admin` varchar(128) NOT NULL DEFAULT '',
+ `serial` int(11) NOT NULL DEFAULT '1',
+ `expire` int(11) NOT NULL DEFAULT '86400',
+ `refresh` int(11) NOT NULL DEFAULT '86400',
+ `retry` int(11) NOT NULL DEFAULT '86400',
+ `minimum` int(11) NOT NULL DEFAULT '86400',
+ `ttl` int(11) NOT NULL DEFAULT '86400',
+ `writeable` tinyint(1) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`),
+ KEY `domain_idx` (`domain`)
+ );
+
+ CREATE TABLE `ZoneData` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `zone_id` int(11) NOT NULL,
+ `name` varchar(128) NOT NULL DEFAULT '',
+ `type` varchar(16) NOT NULL DEFAULT '',
+ `ttl` int(11) NOT NULL DEFAULT '86400',
+ `data` varchar(128) NOT NULL DEFAULT '',
+ PRIMARY KEY (`id`),
+ KEY `zone_idx` (`zone_id`),
+ KEY `name_idx` (`zone_id`, `name`),
+ KEY `type_idx` (`type`)
+ );
+
+'Zones' contains information about specific zones:
+ - domain: the zone name
+ - admin: the zone administrator
+ - serial, expire, reresh, retry, minimum: values in the SOA record
+ - ttl: default zone TTL
+ - writeable: set to true if the zone can be updated via DDNS
+
+'ZoneData' contains the individual records within the zone:
+ - zone_id: the 'id' from the corresponding record in Zones
+ - name: domain name, relative to the zone apex. (Data at the zone
+ apex itself may use a blank name or "@".)
+ - type: the RR type, expressed as text
+ - ttl: the record's TTL
+ - data: the records rdata, expressed as text.
+
+To configure this module in named.conf:
+
+dlz "mysqldlz" {
+ database "dlopen <path to>/dlz_mysqldyn_mod.so <dbname> [dbhost [dbuser [dbpass]]]";
+};
diff --git a/contrib/dlz/modules/mysqldyn/dlz_mysqldyn_mod.c b/contrib/dlz/modules/mysqldyn/dlz_mysqldyn_mod.c
new file mode 100644
index 0000000..bdd0bcc
--- /dev/null
+++ b/contrib/dlz/modules/mysqldyn/dlz_mysqldyn_mod.c
@@ -0,0 +1,1800 @@
+/*
+ * 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.
+ */
+
+/*
+ * BIND 9 DLZ MySQL module with support for dynamic DNS (DDNS)
+ *
+ * Adapted from code contributed by Marty Lee, Maui Systems Ltd.
+ *
+ * See README for database schema and usage details.
+ */
+
+#include <ifaddrs.h>
+#include <inttypes.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <pthread.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <mysql/errmsg.h>
+#include <mysql/mysql.h>
+
+#include <dlz_list.h>
+#include <dlz_minimal.h>
+#include <dlz_pthread.h>
+
+#if !defined(MARIADB_BASE_VERSION) && MYSQL_VERSION_ID >= 80000
+typedef bool my_bool;
+#endif /* !defined(MARIADB_BASE_VERSION) && MYSQL_VERSION_ID >= 80000 */
+
+/*
+ * The SQL queries that will be used for lookups and updates are defined
+ * here. They will be processed into queries by the build_query()
+ * function.
+ *
+ * NOTE: Despite appearances, these do NOT use printf-style formatting.
+ * "%s", with no modifiers, is the only supported directive.
+ */
+
+/*
+ * Get the NS RRset for a zone
+ * Arguments: zone-name
+ */
+#define Q_GETNS \
+ "SELECT d.data FROM ZoneData d, Zones z " \
+ "WHERE UPPER(d.type) = 'NS' AND LOWER(z.domain) = LOWER('%s') " \
+ "AND z.id = d.zone_id"
+
+/*
+ * Get a list of zones (ignoring writable or not)
+ * Arguments: (none)
+ */
+#define Q_GETZONES "SELECT LOWER(domain), serial FROM Zones"
+
+/*
+ * Find a specific zone
+ * Arguments: zone-name
+ */
+#define Q_FINDZONE "SELECT id FROM Zones WHERE LOWER(domain) = LOWER('%s')"
+
+/*
+ * Get SOA data from zone apex
+ * Arguments: zone-name
+ */
+#define Q_GETSOA \
+ "SELECT host, admin, serial, refresh, retry, expire, minimum, ttl " \
+ "FROM Zones WHERE LOWER(domain) = LOWER('%s')"
+
+/*
+ * Get other data from zone apex
+ * Arguments: zone-name, zone-name (repeated)
+ */
+#define Q_GETAPEX \
+ "SELECT d.type, d.data, d.ttl FROM ZoneData d, Zones z " \
+ "WHERE LOWER(z.domain) = LOWER('%s') AND z.id = d.zone_id " \
+ "AND LOWER(d.name) IN (LOWER('%s'), '', '@') " \
+ "ORDER BY UPPER(d.type) ASC"
+
+/*
+ * Get data from non-apex nodes
+ * Arguments: zone-name, node-name (relative to zone name)
+ */
+#define Q_GETNODE \
+ "SELECT d.type, d.data, d.ttl FROM ZoneData d, Zones z " \
+ "WHERE LOWER(z.domain) = LOWER('%s') AND z.id = d.zone_id " \
+ "AND LOWER(d.name) = LOWER('%s') " \
+ "ORDER BY UPPER(d.type) ASC"
+
+/*
+ * Get all data from a zone, for AXFR
+ * Arguments: zone-name
+ */
+#define Q_GETALL \
+ "SELECT d.name, d.type, d.data, d.ttl FROM ZoneData d, Zones z " \
+ "WHERE LOWER(z.domain) = LOWER('%s') AND z.id = d.zone_id"
+
+/*
+ * Get SOA serial number for a zone.
+ * Arguments: zone-name
+ */
+#define Q_GETSERIAL "SELECT serial FROM Zones WHERE domain = '%s'"
+
+/*
+ * Determine whether a zone is writeable, and if so, retrieve zone_id
+ * Arguments: zone-name
+ */
+#define Q_WRITEABLE \
+ "SELECT id FROM Zones WHERE " \
+ "LOWER(domain) = LOWER('%s') AND writeable = 1"
+
+/*
+ * Insert data into zone (other than SOA)
+ * Arguments: zone-id (from Q_WRITEABLE), node-name (relative to zone-name),
+ * rrtype, rdata text, TTL (in text format)
+ */
+#define I_DATA \
+ "INSERT INTO ZoneData (zone_id, name, type, data, ttl) " \
+ "VALUES (%s, LOWER('%s'), UPPER('%s'), '%s', %s)"
+
+/*
+ * Update SOA serial number for a zone
+ * Arguments: new serial number (in text format), zone-id (from Q_WRITEABLE)
+ */
+#define U_SERIAL "UPDATE Zones SET serial = %s WHERE id = %s"
+
+/*
+ * Delete a specific record (non-SOA) from a zone
+ *
+ * Arguments: node-name (relative to zone-name), zone-id (from Q_WRITEABLE),
+ * rrtype, rdata text, TTL (in text format).
+ */
+#define D_RECORD \
+ "DELETE FROM ZoneData WHERE zone_id = %s AND " \
+ "LOWER(name) = LOWER('%s') AND UPPER(type) = UPPER('%s') AND " \
+ "data = '%s' AND ttl = %s"
+
+/*
+ * Delete an entire rrset from a zone
+ * Arguments: node-name (relative to zone-name), zone-id (from Q_WRITEABLE),
+ * rrtype.
+ */
+#define D_RRSET \
+ "DELETE FROM ZoneData WHERE zone_id = %s AND " \
+ "LOWER(name) = LOWER('%s') AND UPPER(type) = UPPER('%s')"
+
+/*
+ * Number of concurrent database connections we support
+ * - equivalent to maxmium number of concurrent transactions
+ * that can be 'in-flight' + 1
+ */
+#define MAX_DBI 16
+
+typedef struct mysql_record {
+ char zone[255];
+ char name[100];
+ char type[10];
+ char data[200];
+ char ttl[10];
+} mysql_record_t;
+
+typedef struct mysql_instance {
+ int id;
+ MYSQL *sock;
+ int connected;
+ dlz_mutex_t mutex;
+} mysql_instance_t;
+
+typedef struct mysql_transaction mysql_transaction_t;
+struct mysql_transaction {
+ char *zone;
+ char *zone_id;
+ mysql_instance_t *dbi;
+ mysql_transaction_t *next;
+};
+
+typedef struct mysql_data {
+ int debug;
+
+ /*
+ * Database connection details
+ */
+ char *db_name;
+ char *db_host;
+ char *db_user;
+ char *db_pass;
+
+ /*
+ * Database structures
+ */
+ mysql_instance_t db[MAX_DBI];
+
+ /*
+ * Transactions
+ */
+ mysql_transaction_t *transactions;
+
+ /*
+ * Mutex for transactions
+ */
+ dlz_mutex_t tx_mutex;
+
+ /* 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;
+} mysql_data_t;
+
+typedef struct mysql_arg mysql_arg_t;
+typedef DLZ_LIST(mysql_arg_t) mysql_arglist_t;
+struct mysql_arg {
+ char *arg;
+ DLZ_LINK(mysql_arg_t) link;
+};
+
+static const char *modname = "dlz_mysqldyn";
+
+/*
+ * Local functions
+ */
+static bool
+db_connect(mysql_data_t *state, mysql_instance_t *dbi) {
+ MYSQL *conn;
+ /*
+ * Make sure this thread has been through 'init'
+ */
+ mysql_thread_init();
+
+ if (dbi->connected) {
+ return (true);
+ }
+
+ if (state->log != NULL) {
+ state->log(ISC_LOG_INFO, "%s: init connection %d ", modname,
+ dbi->id);
+ }
+
+ conn = mysql_real_connect(dbi->sock, state->db_host, state->db_user,
+ state->db_pass, state->db_name, 0, NULL,
+ CLIENT_REMEMBER_OPTIONS);
+ if (conn == NULL) {
+ if (state->log != NULL) {
+ state->log(ISC_LOG_ERROR,
+ "%s: database connection failed: %s",
+ modname, mysql_error(dbi->sock));
+ }
+
+ dlz_mutex_unlock(&dbi->mutex);
+ return (false);
+ }
+
+ dbi->connected = 1;
+ return (true);
+}
+
+static mysql_instance_t *
+get_dbi(mysql_data_t *state) {
+ int i;
+
+ /*
+ * Find an available dbi
+ */
+ for (i = 0; i < MAX_DBI; i++) {
+ if (dlz_mutex_trylock(&state->db[i].mutex) == 0) {
+ break;
+ }
+ }
+
+ if (i == MAX_DBI) {
+ if (state->debug && state->log != NULL) {
+ state->log(ISC_LOG_ERROR,
+ "%s: No available connections", modname);
+ }
+ return (NULL);
+ }
+ return (&state->db[i]);
+}
+
+/*
+ * Allocate memory and store an escaped, sanitized version
+ * of string 'original'
+ */
+static char *
+sanitize(mysql_instance_t *dbi, const char *original) {
+ char *s;
+
+ if (original == NULL) {
+ return (NULL);
+ }
+
+ s = (char *)malloc((strlen(original) * 2) + 1);
+ if (s != NULL) {
+ memset(s, 0, (strlen(original) * 2) + 1);
+
+ mysql_real_escape_string(dbi->sock, s, original,
+ strlen(original));
+ }
+
+ return (s);
+}
+
+/*
+ * Append the string pointed to by 's' to the argument list 'arglist',
+ * and add the string length to the running total pointed to by 'len'.
+ */
+static isc_result_t
+additem(mysql_arglist_t *arglist, char **s, size_t *len) {
+ mysql_arg_t *item;
+
+ item = malloc(sizeof(*item));
+ if (item == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+
+ DLZ_LINK_INIT(item, link);
+ item->arg = *s;
+ *len += strlen(*s);
+ DLZ_LIST_APPEND(*arglist, item, link);
+ *s = NULL;
+
+ return (ISC_R_SUCCESS);
+}
+
+/*
+ * Construct a query string using a variable number of arguments, and
+ * save it into newly allocated memory.
+ *
+ * NOTE: 'command' resembles a printf-style format string, but ONLY
+ * supports the "%s" directive with no modifiers of any kind.
+ *
+ * If 'dbi' is NULL, we attempt to get a temporary database connection;
+ * otherwise we use the existing one.
+ */
+static char *
+build_query(mysql_data_t *state, mysql_instance_t *dbi, const char *command,
+ ...) {
+ isc_result_t result;
+ bool localdbi = false;
+ mysql_arglist_t arglist;
+ mysql_arg_t *item;
+ char *p, *q, *tmp = NULL, *querystr = NULL;
+ char *query = NULL;
+ size_t len = 0;
+ va_list ap1;
+
+ /* Get a DB instance if needed */
+ if (dbi == NULL) {
+ dbi = get_dbi(state);
+ if (dbi == NULL) {
+ return (NULL);
+ }
+ localdbi = true;
+ }
+
+ /* Make sure this instance is connected */
+ if (!db_connect(state, dbi)) {
+ goto fail;
+ }
+
+ va_start(ap1, command);
+ DLZ_LIST_INIT(arglist);
+ q = querystr = strdup(command);
+ if (querystr == NULL) {
+ goto fail;
+ }
+
+ for (;;) {
+ if (*q == '\0') {
+ break;
+ }
+
+ p = strstr(q, "%s");
+ if (p != NULL) {
+ *p = '\0';
+ tmp = strdup(q);
+ if (tmp == NULL) {
+ goto fail;
+ }
+
+ result = additem(&arglist, &tmp, &len);
+ if (result != ISC_R_SUCCESS) {
+ goto fail;
+ }
+
+ tmp = sanitize(dbi, va_arg(ap1, const char *));
+ if (tmp == NULL) {
+ goto fail;
+ }
+
+ result = additem(&arglist, &tmp, &len);
+ if (result != ISC_R_SUCCESS) {
+ goto fail;
+ }
+
+ q = p + 2;
+ } else {
+ tmp = strdup(q);
+ if (tmp == NULL) {
+ goto fail;
+ }
+
+ result = additem(&arglist, &tmp, &len);
+ if (result != ISC_R_SUCCESS) {
+ goto fail;
+ }
+
+ break;
+ }
+ }
+
+ if (len == 0) {
+ goto fail;
+ }
+
+ query = malloc(len + 1);
+ if (query == NULL) {
+ goto fail;
+ }
+
+ *query = '\0';
+ for (item = DLZ_LIST_HEAD(arglist); item != NULL;
+ item = DLZ_LIST_NEXT(item, link))
+ {
+ if (item->arg != NULL) {
+ strcat(query, item->arg);
+ }
+ }
+
+fail:
+ va_end(ap1);
+
+ while ((item = DLZ_LIST_HEAD(arglist)) != NULL) {
+ DLZ_LIST_UNLINK(arglist, item, link);
+ if (item->arg != NULL) {
+ free(item->arg);
+ }
+ free(item);
+ }
+
+ if (tmp != NULL) {
+ free(tmp);
+ }
+ if (querystr != NULL) {
+ free(querystr);
+ }
+
+ if (dbi != NULL && localdbi) {
+ dlz_mutex_unlock(&dbi->mutex);
+ }
+
+ return (query);
+}
+
+/* Does this name end in a dot? */
+static bool
+isrelative(const char *s) {
+ if (s == NULL || s[strlen(s) - 1] == '.') {
+ return (false);
+ }
+ return (true);
+}
+
+/* Return a dot if 's' doesn't already end with one */
+static const char *
+dot(const char *s) {
+ return (isrelative(s) ? "." : "");
+}
+
+/*
+ * Generate a full hostname from a (presumably relative) name 'name'
+ * and a zone name 'zone'; store the result in 'dest' (which must have
+ * enough space).
+ */
+static void
+fqhn(const char *name, const char *zone, char *dest) {
+ if (dest == NULL) {
+ return;
+ }
+
+ if (strlen(name) == 0 || strcmp(name, "@") == 0) {
+ sprintf(dest, "%s%s", zone, dot(zone));
+ } else {
+ if (isrelative(name)) {
+ sprintf(dest, "%s.%s%s", name, zone, dot(zone));
+ } else {
+ strcpy(dest, name);
+ }
+ }
+}
+
+/*
+ * Names are stored in relative form in ZoneData; this function
+ * removes labels matching 'zone' from the end of 'name'.
+ */
+static char *
+relname(const char *name, const char *zone) {
+ size_t nlen, zlen;
+ const char *p;
+ char *new;
+
+ new = (char *)malloc(strlen(name) + 1);
+ if (new == NULL) {
+ return (NULL);
+ }
+
+ nlen = strlen(name);
+ zlen = strlen(zone);
+
+ if (nlen < zlen) {
+ strcpy(new, name);
+ return (new);
+ } else if (nlen == zlen || strcasecmp(name, zone) == 0) {
+ strcpy(new, "@");
+ return (new);
+ }
+
+ p = name + nlen - zlen;
+ if (strcasecmp(p, zone) != 0 &&
+ (zone[zlen - 1] != '.' || strncasecmp(p, zone, zlen - 1) != 0))
+ {
+ strcpy(new, name);
+ return (new);
+ }
+
+ strncpy(new, name, nlen - zlen);
+ new[nlen - zlen - 1] = '\0';
+ return (new);
+}
+
+static isc_result_t
+validate_txn(mysql_data_t *state, mysql_transaction_t *txn) {
+ isc_result_t result = ISC_R_FAILURE;
+ mysql_transaction_t *txp;
+
+ dlz_mutex_lock(&state->tx_mutex);
+ for (txp = state->transactions; txp != NULL; txp = txp->next) {
+ if (txn == txp) {
+ result = ISC_R_SUCCESS;
+ break;
+ }
+ }
+ dlz_mutex_unlock(&state->tx_mutex);
+
+ if (result != ISC_R_SUCCESS && state->log != NULL) {
+ state->log(ISC_LOG_ERROR, "%s: invalid txn %x", modname, txn);
+ }
+
+ return (result);
+}
+
+static isc_result_t
+db_execute(mysql_data_t *state, mysql_instance_t *dbi, const char *query) {
+ int ret;
+
+ /* Make sure this instance is connected. */
+ if (!db_connect(state, dbi)) {
+ return (ISC_R_FAILURE);
+ }
+
+ ret = mysql_real_query(dbi->sock, query, strlen(query));
+ if (ret != 0) {
+ if (state->debug && state->log != NULL) {
+ state->log(ISC_LOG_ERROR, "%s: query '%s' failed: %s",
+ modname, query, mysql_error(dbi->sock));
+ }
+ return (ISC_R_FAILURE);
+ }
+
+ if (state->debug && state->log != NULL) {
+ state->log(ISC_LOG_INFO, "%s: execute(%d) %s", modname, dbi->id,
+ query);
+ }
+
+ return (ISC_R_SUCCESS);
+}
+
+static MYSQL_RES *
+db_query(mysql_data_t *state, mysql_instance_t *dbi, const char *query) {
+ isc_result_t result;
+ bool localdbi = false;
+ MYSQL_RES *res = NULL;
+
+ if (query == NULL) {
+ return (NULL);
+ }
+
+ /* Get a DB instance if needed */
+ if (dbi == NULL) {
+ dbi = get_dbi(state);
+ if (dbi == NULL) {
+ return (NULL);
+ }
+ localdbi = true;
+ }
+
+ /* Make sure this instance is connected */
+ if (!db_connect(state, dbi)) {
+ goto fail;
+ }
+
+ result = db_execute(state, dbi, query);
+ if (result != ISC_R_SUCCESS) {
+ goto fail;
+ }
+
+ res = mysql_store_result(dbi->sock);
+ if (res == NULL) {
+ if (state->log != NULL) {
+ state->log(ISC_LOG_ERROR,
+ "%s: unable to store result: %s", modname,
+ mysql_error(dbi->sock));
+ }
+ goto fail;
+ }
+
+ if (state->debug && state->log != NULL) {
+ state->log(ISC_LOG_INFO, "%s: query(%d) returned %d rows",
+ modname, dbi->id, mysql_num_rows(res));
+ }
+
+fail:
+ if (dbi != NULL && localdbi) {
+ dlz_mutex_unlock(&dbi->mutex);
+ }
+ return (res);
+}
+
+/*
+ * Generate a DNS NOTIFY packet:
+ * 12 bytes header
+ * Question (1)
+ * strlen(zone) +2
+ * 2 bytes qtype
+ * 2 bytes qclass
+ *
+ * -> 18 bytes + strlen(zone)
+ *
+ * N.B. Need to be mindful of byte ordering; using htons to map 16bit
+ * values to the 'on the wire' packet values.
+ */
+static unsigned char *
+make_notify(const char *zone, int *packetlen) {
+ int i, j;
+ unsigned char *packet = (unsigned char *)malloc(strlen(zone) + 18);
+
+ if (packet == NULL) {
+ return (NULL);
+ }
+
+ *packetlen = strlen(zone) + 18;
+ memset(packet, 0, *packetlen);
+
+ /* Random query ID */
+ i = rand();
+ packet[0] = htons(i) & 0xff;
+ packet[1] = htons(i) >> 8;
+
+ /* Flags (OpCode '4' in bits 14-11), Auth Answer set in bit 10 */
+ i = 0x2400;
+ packet[2] = htons(i) & 0xff;
+ packet[3] = htons(i) >> 8;
+
+ /* QD Count */
+ i = 0x1;
+ packet[4] = htons(i) & 0xff;
+ packet[5] = htons(i) >> 8;
+
+ /* Question */
+ packet[12] = '.';
+ memmove(&packet[13], zone, strlen(zone));
+ packet[13 + strlen(zone)] = 0;
+
+ /* Make the question into labels */
+ j = 12;
+ while (packet[j]) {
+ for (i = j + 1; packet[i] != '\0' && packet[i] != '.'; i++) {
+ ;
+ }
+ packet[j] = i - j - 1;
+ j = i;
+ }
+
+ /* Question type */
+ i = 6;
+ packet[j + 1] = htons(i) & 0xff;
+ packet[j + 2] = htons(i) >> 8;
+
+ /* Queston class */
+ i = 1;
+ packet[j + 3] = htons(i) & 0xff;
+ packet[j + 4] = htons(i) >> 8;
+
+ return (packet);
+}
+
+static void
+send_notify(struct sockaddr_in *addr, const unsigned char *p, const int plen) {
+ int s;
+
+ addr->sin_family = AF_INET;
+ addr->sin_port = htons(53);
+
+ if ((s = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
+ return;
+ }
+
+ sendto(s, p, plen, 0, (struct sockaddr *)addr, sizeof(*addr));
+ close(s);
+ return;
+}
+
+/*
+ * Generate and send a DNS NOTIFY packet
+ */
+static void
+notify(mysql_data_t *state, const char *zone, int sn) {
+ MYSQL_RES *res;
+ MYSQL_ROW row;
+ char *query;
+ unsigned char *packet;
+ int packetlen;
+ struct ifaddrs *ifap, *ifa;
+ char zaddr[INET_ADDRSTRLEN];
+ void *addrp = NULL;
+
+ /* Get the name servers from the NS rrset */
+ query = build_query(state, NULL, Q_GETNS, zone);
+ res = db_query(state, NULL, query);
+ free(query);
+ if (res == NULL) {
+ return;
+ }
+
+ /* Create a DNS NOTIFY packet */
+ packet = make_notify(zone, &packetlen);
+ if (packet == NULL) {
+ mysql_free_result(res);
+ return;
+ }
+
+ /* Get a list of our own addresses */
+ if (getifaddrs(&ifap) < 0) {
+ ifap = NULL;
+ }
+
+ /* Tell each nameserver of the update */
+ while ((row = mysql_fetch_row(res)) != NULL) {
+ bool local = false;
+ struct hostent *h;
+ struct sockaddr_in addr, *sin;
+
+ /*
+ * Put nameserver rdata through gethostbyname as it
+ * might be an IP address or a hostname. (XXX: switch
+ * this to inet_pton/getaddrinfo.)
+ */
+ h = gethostbyname(row[0]);
+ if (h == NULL) {
+ continue;
+ }
+
+ memmove(&addr.sin_addr, h->h_addr, h->h_length);
+ addrp = &addr.sin_addr;
+
+ /* Get the address for the nameserver into a string */
+ inet_ntop(AF_INET, addrp, zaddr, INET_ADDRSTRLEN);
+ for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) {
+ char ifaddr[INET_ADDRSTRLEN];
+
+ if (ifa->ifa_addr->sa_family != AF_INET) {
+ continue;
+ }
+
+ /* Get local address into a string */
+ sin = (struct sockaddr_in *)ifa->ifa_addr;
+ addrp = &sin->sin_addr;
+ inet_ntop(AF_INET, addrp, ifaddr, INET_ADDRSTRLEN);
+
+ /* See if nameserver address matches this one */
+ if (strcmp(ifaddr, zaddr) == 0) {
+ local = true;
+ }
+ }
+
+ if (!local) {
+ if (state->log != NULL) {
+ state->log(ISC_LOG_INFO,
+ "%s: notify %s zone %s serial %d",
+ modname, row[0], zone, sn);
+ }
+ send_notify(&addr, packet, packetlen);
+ }
+ }
+
+ mysql_free_result(res);
+ free(packet);
+ if (ifap != NULL) {
+ freeifaddrs(ifap);
+ }
+}
+
+/*
+ * Constructs a mysql_record_t structure from 'rdatastr', to be
+ * used in the dlz_{add,sub,del}rdataset functions below.
+ */
+static mysql_record_t *
+makerecord(mysql_data_t *state, const char *name, const char *rdatastr) {
+ mysql_record_t *new_record;
+ char *real_name = NULL, *dclass = NULL, *type = NULL;
+ char *data = NULL, *ttlstr = NULL, *buf = NULL;
+ char *saveptr = NULL;
+ dns_ttl_t ttlvalue;
+
+ new_record = (mysql_record_t *)malloc(sizeof(mysql_record_t));
+
+ if (new_record == NULL) {
+ if (state->log != NULL) {
+ state->log(ISC_LOG_ERROR,
+ "%s: makerecord - unable to malloc",
+ modname);
+ }
+ return (NULL);
+ }
+
+ buf = strdup(rdatastr);
+ if (buf == NULL) {
+ if (state->log != NULL) {
+ state->log(ISC_LOG_ERROR,
+ "%s: makerecord - unable to malloc",
+ modname);
+ }
+ free(new_record);
+ return (NULL);
+ }
+
+ /*
+ * The format is:
+ * FULLNAME\tTTL\tDCLASS\tTYPE\tDATA
+ *
+ * The DATA field is space separated, and is in the data format
+ * for the type used by dig
+ */
+ real_name = strtok_r(buf, "\t", &saveptr);
+ if (real_name == NULL) {
+ goto error;
+ }
+
+ ttlstr = strtok_r(NULL, "\t", &saveptr);
+ if (ttlstr == NULL || sscanf(ttlstr, "%d", &ttlvalue) != 1) {
+ goto error;
+ }
+
+ dclass = strtok_r(NULL, "\t", &saveptr);
+ if (dclass == NULL) {
+ goto error;
+ }
+
+ type = strtok_r(NULL, "\t", &saveptr);
+ if (type == NULL) {
+ goto error;
+ }
+
+ data = strtok_r(NULL, "\t", &saveptr);
+ if (data == NULL) {
+ goto error;
+ }
+
+ strcpy(new_record->name, name);
+ strcpy(new_record->type, type);
+ strcpy(new_record->data, data);
+ sprintf(new_record->ttl, "%d", ttlvalue);
+
+ free(buf);
+ return (new_record);
+
+error:
+ free(buf);
+ free(new_record);
+ return (NULL);
+}
+
+/*
+ * Remember a helper function from the bind9 dlz_dlopen driver
+ */
+static void
+b9_add_helper(mysql_data_t *state, const char *helper_name, void *ptr) {
+ if (strcmp(helper_name, "log") == 0) {
+ state->log = (log_t *)ptr;
+ }
+ if (strcmp(helper_name, "putrr") == 0) {
+ state->putrr = (dns_sdlz_putrr_t *)ptr;
+ }
+ if (strcmp(helper_name, "putnamedrr") == 0) {
+ state->putnamedrr = (dns_sdlz_putnamedrr_t *)ptr;
+ }
+ if (strcmp(helper_name, "writeable_zone") == 0) {
+ state->writeable_zone = (dns_dlz_writeablezone_t *)ptr;
+ }
+}
+
+/*
+ * DLZ API functions
+ */
+
+/*
+ * Return the version of the API
+ */
+int
+dlz_version(unsigned int *flags) {
+ UNUSED(flags);
+ *flags |= DNS_SDLZFLAG_THREADSAFE;
+ return (DLZ_DLOPEN_VERSION);
+}
+
+/*
+ * Called to initialize the driver
+ */
+isc_result_t
+dlz_create(const char *dlzname, unsigned int argc, char *argv[], void **dbdata,
+ ...) {
+ mysql_data_t *state;
+ const char *helper_name;
+ va_list ap;
+ int n;
+
+ UNUSED(dlzname);
+
+ state = calloc(1, sizeof(mysql_data_t));
+ if (state == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+
+ dlz_mutex_init(&state->tx_mutex, NULL);
+ state->transactions = NULL;
+
+ /* Fill in the helper functions */
+ va_start(ap, dbdata);
+ while ((helper_name = va_arg(ap, const char *)) != NULL) {
+ b9_add_helper(state, helper_name, va_arg(ap, void *));
+ }
+ va_end(ap);
+
+ if (state->log != NULL) {
+ state->log(ISC_LOG_INFO, "loading %s module", modname);
+ }
+
+ if ((argc < 2) || (argc > 6)) {
+ if (state->log != NULL) {
+ state->log(ISC_LOG_ERROR,
+ "%s: missing args <dbname> "
+ "[<dbhost> [<user> <pass>]]",
+ modname);
+ }
+ dlz_destroy(state);
+ return (ISC_R_FAILURE);
+ }
+
+ state->db_name = strdup(argv[1]);
+ if (argc > 2) {
+ state->db_host = strdup(argv[2]);
+ if (argc > 4) {
+ state->db_user = strdup(argv[3]);
+ state->db_pass = strdup(argv[4]);
+ } else {
+ state->db_user = strdup("bind");
+ state->db_pass = strdup("");
+ }
+ } else {
+ state->db_host = strdup("localhost");
+ state->db_user = strdup("bind");
+ state->db_pass = strdup("");
+ }
+
+ if (state->log != NULL) {
+ state->log(ISC_LOG_INFO, "%s: DB=%s, Host=%s, User=%s", modname,
+ state->db_name, state->db_host, state->db_user);
+ }
+
+ /*
+ * Assign the 'state' to dbdata so we get it in our callbacks
+ */
+
+ dlz_mutex_lock(&state->tx_mutex);
+
+ /*
+ * Populate DB instances
+ */
+ if (mysql_thread_safe()) {
+ for (n = 0; n < MAX_DBI; n++) {
+ dlz_mutex_init(&state->db[n].mutex, NULL);
+ dlz_mutex_lock(&state->db[n].mutex);
+ state->db[n].id = n;
+ state->db[n].connected = 0;
+ state->db[n].sock = mysql_init(NULL);
+ mysql_options(state->db[n].sock,
+ MYSQL_READ_DEFAULT_GROUP, modname);
+ mysql_options(state->db[n].sock, MYSQL_OPT_RECONNECT,
+ &(my_bool){ 1 });
+ dlz_mutex_unlock(&state->db[n].mutex);
+ }
+
+ *dbdata = state;
+ dlz_mutex_unlock(&state->tx_mutex);
+ return (ISC_R_SUCCESS);
+ }
+
+ free(state->db_name);
+ free(state->db_host);
+ free(state->db_user);
+ free(state->db_pass);
+ dlz_mutex_destroy(&state->tx_mutex);
+ free(state);
+ return (ISC_R_FAILURE);
+}
+
+/*
+ * Shut down the backend
+ */
+void
+dlz_destroy(void *dbdata) {
+ mysql_data_t *state = (mysql_data_t *)dbdata;
+ int i;
+
+ if (state->debug && state->log != NULL) {
+ state->log(ISC_LOG_INFO, "%s: shutting down", modname);
+ }
+
+ for (i = 0; i < MAX_DBI; i++) {
+ if (state->db[i].sock) {
+ mysql_close(state->db[i].sock);
+ state->db[i].sock = NULL;
+ }
+ }
+ free(state->db_name);
+ free(state->db_host);
+ free(state->db_user);
+ free(state->db_pass);
+ dlz_mutex_destroy(&state->tx_mutex);
+ free(state);
+}
+
+/*
+ * See if we handle a given zone
+ */
+isc_result_t
+dlz_findzonedb(void *dbdata, const char *name, dns_clientinfomethods_t *methods,
+ dns_clientinfo_t *clientinfo) {
+ UNUSED(methods);
+ UNUSED(clientinfo);
+ isc_result_t result = ISC_R_SUCCESS;
+ mysql_data_t *state = (mysql_data_t *)dbdata;
+ MYSQL_RES *res;
+ char *query;
+
+ /* Query the Zones table to see if this zone is present */
+ query = build_query(state, NULL, Q_FINDZONE, name);
+
+ if (query == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+
+ res = db_query(state, NULL, query);
+ if (res == NULL) {
+ return (ISC_R_FAILURE);
+ }
+
+ if (mysql_num_rows(res) == 0) {
+ result = ISC_R_NOTFOUND;
+ }
+
+ mysql_free_result(res);
+ return (result);
+}
+
+/*
+ * Perform a database lookup
+ */
+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) {
+ UNUSED(methods);
+ UNUSED(clientinfo);
+ isc_result_t result;
+ mysql_data_t *state = (mysql_data_t *)dbdata;
+ bool found = false;
+ char *real_name;
+ MYSQL_RES *res;
+ MYSQL_ROW row;
+ char *query;
+ mysql_transaction_t *txn = NULL;
+ mysql_instance_t *dbi = NULL;
+
+ if (state->putrr == NULL) {
+ if (state->log != NULL) {
+ state->log(ISC_LOG_ERROR, "%s: dlz_lookup - no putrr",
+ modname);
+ }
+ return (ISC_R_NOTIMPLEMENTED);
+ }
+
+ /* Are we okay to try to find the txn version? */
+ if (clientinfo != NULL && clientinfo->version >= 2) {
+ txn = (mysql_transaction_t *)clientinfo->dbversion;
+ if (txn != NULL && validate_txn(state, txn) == ISC_R_SUCCESS) {
+ dbi = txn->dbi;
+ }
+ if (dbi != NULL) {
+ state->log(ISC_LOG_DEBUG(1),
+ "%s: lookup in live transaction %p, DBI %p",
+ modname, txn, dbi);
+ }
+ }
+
+ if (strcmp(name, "@") == 0) {
+ real_name = (char *)malloc(strlen(zone) + 1);
+ if (real_name == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+ strcpy(real_name, zone);
+ } else {
+ real_name = (char *)malloc(strlen(name) + 1);
+ if (real_name == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+ strcpy(real_name, name);
+ }
+
+ if (strcmp(real_name, zone) == 0) {
+ /*
+ * Get the Zones table data for use in the SOA:
+ * zone admin serial refresh retry expire min
+ */
+ query = build_query(state, dbi, Q_GETSOA, zone);
+ if (query == NULL) {
+ free(real_name);
+ return (ISC_R_NOMEMORY);
+ }
+
+ res = db_query(state, dbi, query);
+ free(query);
+
+ if (res == NULL) {
+ free(real_name);
+ return (ISC_R_NOTFOUND);
+ }
+
+ while ((row = mysql_fetch_row(res)) != NULL) {
+ char host[1024], admin[1024], data[4096];
+ int ttl;
+
+ sscanf(row[7], "%d", &ttl);
+ fqhn(row[0], zone, host);
+ fqhn(row[1], zone, admin);
+
+ /* zone admin serial refresh retry expire min */
+ snprintf(data, sizeof(data), "%s%s %s%s %s %s %s %s %s",
+ host, dot(host), admin, dot(admin), row[2],
+ row[3], row[4], row[5], row[6]);
+
+ result = state->putrr(lookup, "soa", ttl, data);
+ if (result != ISC_R_SUCCESS) {
+ free(real_name);
+ mysql_free_result(res);
+ return (result);
+ }
+ }
+
+ mysql_free_result(res);
+
+ /*
+ * Now we'll get the rest of the apex data
+ */
+ query = build_query(state, dbi, Q_GETAPEX, zone, real_name);
+ } else {
+ query = build_query(state, dbi, Q_GETNODE, zone, real_name);
+ }
+
+ res = db_query(state, dbi, query);
+ free(query);
+
+ if (res == NULL) {
+ free(real_name);
+ return (ISC_R_NOTFOUND);
+ }
+
+ while ((row = mysql_fetch_row(res)) != NULL) {
+ int ttl;
+ sscanf(row[2], "%d", &ttl);
+ result = state->putrr(lookup, row[0], ttl, row[1]);
+ if (result != ISC_R_SUCCESS) {
+ free(real_name);
+ mysql_free_result(res);
+ return (result);
+ }
+
+ found = true;
+ }
+
+ if (state->debug && state->log != NULL) {
+ state->log(ISC_LOG_INFO, "%s: dlz_lookup %s/%s/%s - (%d rows)",
+ modname, name, real_name, zone, mysql_num_rows(res));
+ }
+
+ mysql_free_result(res);
+ free(real_name);
+
+ if (!found) {
+ return (ISC_R_NOTFOUND);
+ }
+
+ return (ISC_R_SUCCESS);
+}
+
+/*
+ * See if a zone transfer is allowed
+ */
+isc_result_t
+dlz_allowzonexfr(void *dbdata, const char *name, const char *client) {
+ mysql_data_t *state = (mysql_data_t *)dbdata;
+
+ if (state->debug && state->log != NULL) {
+ state->log(ISC_LOG_INFO, "dlz_allowzonexfr: %s %s", name,
+ client);
+ }
+
+ /* Just say yes for all our zones */
+ return (dlz_findzonedb(dbdata, name, NULL, NULL));
+}
+
+/*
+ * Perform a zone transfer
+ */
+isc_result_t
+dlz_allnodes(const char *zone, void *dbdata, dns_sdlzallnodes_t *allnodes) {
+ isc_result_t result = ISC_R_SUCCESS;
+ mysql_data_t *state = (mysql_data_t *)dbdata;
+ MYSQL_RES *res;
+ MYSQL_ROW row;
+ char *query;
+
+ UNUSED(zone);
+
+ if (state->debug && (state->log != NULL)) {
+ state->log(ISC_LOG_INFO, "dlz_allnodes: %s", zone);
+ }
+
+ if (state->putnamedrr == NULL) {
+ return (ISC_R_NOTIMPLEMENTED);
+ }
+
+ /*
+ * Get all the ZoneData for this zone
+ */
+ query = build_query(state, NULL, Q_GETALL, zone);
+ if (query == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+
+ res = db_query(state, NULL, query);
+ free(query);
+ if (res == NULL) {
+ return (ISC_R_NOTFOUND);
+ }
+
+ while ((row = mysql_fetch_row(res)) != NULL) {
+ char hostname[1024];
+ int ttl;
+
+ sscanf(row[3], "%d", &ttl);
+ fqhn(row[0], zone, hostname);
+ result = state->putnamedrr(allnodes, hostname, row[1], ttl,
+ row[2]);
+ if (result != ISC_R_SUCCESS) {
+ break;
+ }
+ }
+
+ mysql_free_result(res);
+ return (result);
+}
+
+/*
+ * Start a transaction
+ */
+isc_result_t
+dlz_newversion(const char *zone, void *dbdata, void **versionp) {
+ isc_result_t result = ISC_R_FAILURE;
+ mysql_data_t *state = (mysql_data_t *)dbdata;
+ MYSQL_RES *res;
+ MYSQL_ROW row;
+ char *query;
+ char zone_id[16];
+ mysql_transaction_t *txn = NULL, *newtx = NULL;
+
+ /*
+ * Check Zone is writable
+ */
+ query = build_query(state, NULL, Q_WRITEABLE, zone);
+ if (query == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+
+ res = db_query(state, NULL, query);
+ free(query);
+ if (res == NULL) {
+ return (ISC_R_FAILURE);
+ }
+
+ if ((row = mysql_fetch_row(res)) == NULL) {
+ mysql_free_result(res);
+ return (ISC_R_FAILURE);
+ }
+
+ strcpy(zone_id, row[0]);
+ mysql_free_result(res);
+
+ /*
+ * See if we already have a transaction for this zone
+ */
+ dlz_mutex_lock(&state->tx_mutex);
+ for (txn = state->transactions; txn != NULL; txn = txn->next) {
+ if (strcmp(txn->zone, zone) == 0) {
+ if (state->log != NULL) {
+ state->log(ISC_LOG_ERROR,
+ "%s: transaction already "
+ "started for zone %s",
+ modname, zone);
+ }
+ dlz_mutex_unlock(&state->tx_mutex);
+ return (ISC_R_FAILURE);
+ }
+ }
+
+ /*
+ * Create new transaction
+ */
+ newtx = (mysql_transaction_t *)calloc(1, sizeof(mysql_transaction_t));
+ if (newtx == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+ newtx->zone = strdup(zone);
+ if (newtx->zone == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+ newtx->zone_id = strdup(zone_id);
+ if (newtx->zone_id == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+ newtx->dbi = get_dbi(state);
+ newtx->next = NULL;
+
+ if (newtx->dbi == NULL) {
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+
+ result = db_execute(state, newtx->dbi, "START TRANSACTION");
+ if (result != ISC_R_SUCCESS) {
+ dlz_mutex_unlock(&newtx->dbi->mutex);
+ goto cleanup;
+ }
+
+ /*
+ * Add this tx to front of list
+ */
+ newtx->next = state->transactions;
+ state->transactions = newtx;
+
+ if (state->debug && (state->log != NULL)) {
+ state->log(ISC_LOG_INFO, "%s: New tx %x", modname, newtx);
+ }
+
+cleanup:
+ dlz_mutex_unlock(&state->tx_mutex);
+ if (result == ISC_R_SUCCESS) {
+ *versionp = (void *)newtx;
+ } else {
+ dlz_mutex_unlock(&state->tx_mutex);
+ if (newtx != NULL) {
+ if (newtx->zone != NULL) {
+ free(newtx->zone);
+ }
+ if (newtx->zone != NULL) {
+ free(newtx->zone_id);
+ }
+ free(newtx);
+ }
+ }
+
+ return (result);
+}
+
+/*
+ * End a transaction
+ */
+void
+dlz_closeversion(const char *zone, bool commit, void *dbdata, void **versionp) {
+ isc_result_t result;
+ mysql_data_t *state = (mysql_data_t *)dbdata;
+ mysql_transaction_t *txn = (mysql_transaction_t *)*versionp;
+ mysql_transaction_t *txp;
+ char *query;
+ MYSQL_RES *res;
+ MYSQL_ROW row;
+
+ /*
+ * Find the transaction
+ */
+ dlz_mutex_lock(&state->tx_mutex);
+ if (state->transactions == txn) {
+ /* Tx is first in list; remove it. */
+ state->transactions = txn->next;
+ } else {
+ txp = state->transactions;
+ while (txp != NULL) {
+ if (txp->next != NULL) {
+ if (txp->next == txn) {
+ txp->next = txn->next;
+ break;
+ }
+ }
+ if (txp == txn) {
+ txp = txn->next;
+ break;
+ }
+ txp = txp->next;
+ }
+ }
+
+ /*
+ * Tidy up
+ */
+ dlz_mutex_unlock(&state->tx_mutex);
+ *versionp = NULL;
+
+ if (commit) {
+ int oldsn = 0, newsn = 0;
+
+ /*
+ * Find out the serial number of the zone out with the
+ * transaction so we can see if it has incremented or not
+ */
+ query = build_query(state, txn->dbi, Q_GETSERIAL, zone);
+ if (query == NULL && state->log != NULL) {
+ state->log(ISC_LOG_ERROR,
+ "%s: unable to commit transaction %x "
+ "on zone %s: no memory",
+ modname, txn, zone);
+ return;
+ }
+
+ res = db_query(state, txn->dbi, query);
+ if (res != NULL) {
+ while ((row = mysql_fetch_row(res)) != NULL) {
+ sscanf(row[0], "%d", &oldsn);
+ }
+ mysql_free_result(res);
+ }
+
+ /*
+ * Commit the transaction to the database
+ */
+ result = db_execute(state, txn->dbi, "COMMIT");
+ if (result != ISC_R_SUCCESS && state->log != NULL) {
+ state->log(ISC_LOG_INFO,
+ "%s: (%x) commit transaction on zone %s",
+ modname, txn, zone);
+ return;
+ }
+
+ if (state->debug && state->log != NULL) {
+ state->log(ISC_LOG_INFO,
+ "%s: (%x) committing transaction "
+ "on zone %s",
+ modname, txn, zone);
+ }
+
+ /*
+ * Now get the serial number again
+ */
+ query = build_query(state, txn->dbi, Q_GETSERIAL, zone);
+ res = db_query(state, txn->dbi, query);
+ free(query);
+
+ if (res != NULL) {
+ while ((row = mysql_fetch_row(res)) != NULL) {
+ sscanf(row[0], "%d", &newsn);
+ }
+ mysql_free_result(res);
+ }
+
+ /*
+ * Look to see if serial numbers have changed
+ */
+ if (newsn > oldsn) {
+ notify(state, zone, newsn);
+ }
+ } else {
+ result = db_execute(state, txn->dbi, "ROLLBACK");
+ if (state->debug && (state->log != NULL)) {
+ state->log(ISC_LOG_INFO,
+ "%s: (%x) roll back transaction on zone %s",
+ modname, txn, zone);
+ }
+ }
+
+ /*
+ * Unlock the mutex for this txn
+ */
+ dlz_mutex_unlock(&txn->dbi->mutex);
+
+ /*
+ * Free up other structures
+ */
+ free(txn->zone);
+ free(txn->zone_id);
+ free(txn);
+}
+
+/*
+ * Configure a writeable zone
+ */
+#if DLZ_DLOPEN_VERSION < 3
+isc_result_t
+dlz_configure(dns_view_t *view, void *dbdata)
+#else /* DLZ_DLOPEN_VERSION >= 3 */
+isc_result_t
+dlz_configure(dns_view_t *view, dns_dlzdb_t *dlzdb, void *dbdata)
+#endif /* DLZ_DLOPEN_VERSION */
+{
+ mysql_data_t *state = (mysql_data_t *)dbdata;
+ isc_result_t result;
+ MYSQL_RES *res;
+ MYSQL_ROW row;
+ int count;
+
+ /*
+ * Seed PRNG (used by Notify code)
+ */
+ srand(getpid());
+
+ if (state->debug && state->log != NULL) {
+ state->log(ISC_LOG_INFO, "%s: dlz_confgure", modname);
+ }
+
+ if (state->writeable_zone == NULL) {
+ if (state->log != NULL) {
+ state->log(ISC_LOG_ERROR,
+ "%s: no writeable_zone method available",
+ modname);
+ }
+ return (ISC_R_FAILURE);
+ }
+
+ /*
+ * Get a list of Zones (ignore writeable column at this point)
+ */
+ res = db_query(state, NULL, Q_GETZONES);
+ if (res == NULL) {
+ return (ISC_R_FAILURE);
+ }
+
+ count = 0;
+ while ((row = mysql_fetch_row(res)) != NULL) {
+ int sn;
+ sscanf(row[1], "%d", &sn);
+ notify(state, row[0], sn);
+ result = state->writeable_zone(view,
+#if DLZ_DLOPEN_VERSION >= 3
+ dlzdb,
+#endif /* if DLZ_DLOPEN_VERSION >= 3 */
+ row[0]);
+ if (result != ISC_R_SUCCESS) {
+ if (state->log != NULL) {
+ state->log(ISC_LOG_ERROR,
+ "%s: failed to configure zone %s",
+ modname, row[0]);
+ }
+ mysql_free_result(res);
+ return (result);
+ }
+ count++;
+ }
+ mysql_free_result(res);
+
+ if (state->debug && state->log != NULL) {
+ state->log(ISC_LOG_INFO, "%s: configured %d zones", modname,
+ count);
+ }
+ return (ISC_R_SUCCESS);
+}
+
+/*
+ * Authorize a zone update
+ */
+bool
+dlz_ssumatch(const char *signer, const char *name, const char *tcpaddr,
+ const char *type, const char *key, uint32_t keydatalen,
+ unsigned char *keydata, void *dbdata) {
+ mysql_data_t *state = (mysql_data_t *)dbdata;
+
+ UNUSED(tcpaddr);
+ UNUSED(type);
+ UNUSED(keydatalen);
+ UNUSED(keydata);
+ UNUSED(key);
+
+ if (state->debug && state->log != NULL) {
+ state->log(ISC_LOG_INFO, "%s: allowing update of %s by key %s",
+ modname, name, signer);
+ }
+ return (true);
+}
+
+isc_result_t
+dlz_addrdataset(const char *name, const char *rdatastr, void *dbdata,
+ void *version) {
+ mysql_data_t *state = (mysql_data_t *)dbdata;
+ mysql_transaction_t *txn = (mysql_transaction_t *)version;
+ char *new_name, *query;
+ mysql_record_t *record;
+ isc_result_t result;
+
+ if (txn == NULL) {
+ return (ISC_R_FAILURE);
+ }
+
+ new_name = relname(name, txn->zone);
+ if (new_name == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+
+ if (state->debug && (state->log != NULL)) {
+ state->log(ISC_LOG_INFO, "%s: add (%x) %s (as %s) %s", modname,
+ version, name, new_name, rdatastr);
+ }
+
+ record = makerecord(state, new_name, rdatastr);
+ free(new_name);
+ if (record == NULL) {
+ return (ISC_R_FAILURE);
+ }
+
+ /* Write out data to database */
+ if (strcasecmp(record->type, "SOA") != 0) {
+ query = build_query(state, txn->dbi, I_DATA, txn->zone_id,
+ record->name, record->type, record->data,
+ record->ttl);
+ if (query == NULL) {
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+ result = db_execute(state, txn->dbi, query);
+ free(query);
+ } else {
+ /*
+ * This is an SOA record, so we update: it must exist,
+ * or we wouldn't have gotten this far.
+ * SOA: zone admin serial refresh retry expire min
+ */
+ char sn[32];
+ sscanf(record->data, "%*s %*s %31s %*s %*s %*s %*s", sn);
+ query = build_query(state, txn->dbi, U_SERIAL, sn,
+ txn->zone_id);
+ if (query == NULL) {
+ result = ISC_R_FAILURE;
+ goto cleanup;
+ }
+ result = db_execute(state, txn->dbi, query);
+ free(query);
+ }
+
+cleanup:
+ free(record);
+ return (result);
+}
+
+isc_result_t
+dlz_subrdataset(const char *name, const char *rdatastr, void *dbdata,
+ void *version) {
+ mysql_data_t *state = (mysql_data_t *)dbdata;
+ mysql_transaction_t *txn = (mysql_transaction_t *)version;
+ char *new_name, *query;
+ mysql_record_t *record;
+ isc_result_t result;
+
+ if (txn == NULL) {
+ return (ISC_R_FAILURE);
+ }
+
+ new_name = relname(name, txn->zone);
+ if (new_name == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+
+ if (state->debug && (state->log != NULL)) {
+ state->log(ISC_LOG_INFO, "%s: sub (%x) %s %s", modname, version,
+ name, rdatastr);
+ }
+
+ record = makerecord(state, new_name, rdatastr);
+ free(new_name);
+ if (record == NULL) {
+ return (ISC_R_FAILURE);
+ }
+ /*
+ * If 'type' isn't 'SOA', delete the records
+ */
+ if (strcasecmp(record->type, "SOA") == 0) {
+ result = ISC_R_SUCCESS;
+ } else {
+ query = build_query(state, txn->dbi, D_RECORD, txn->zone_id,
+ record->name, record->type, record->data,
+ record->ttl);
+ if (query == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+
+ result = db_execute(state, txn->dbi, query);
+ free(query);
+ }
+
+cleanup:
+ free(record);
+ return (result);
+}
+
+isc_result_t
+dlz_delrdataset(const char *name, const char *type, void *dbdata,
+ void *version) {
+ mysql_data_t *state = (mysql_data_t *)dbdata;
+ mysql_transaction_t *txn = (mysql_transaction_t *)version;
+ char *new_name, *query;
+ isc_result_t result;
+
+ if (txn == NULL) {
+ return (ISC_R_FAILURE);
+ }
+
+ new_name = relname(name, txn->zone);
+ if (new_name == NULL) {
+ return (ISC_R_NOMEMORY);
+ }
+
+ if (state->debug && (state->log != NULL)) {
+ state->log(ISC_LOG_INFO, "%s: del (%x) %s %s", modname, version,
+ name, type);
+ }
+
+ query = build_query(state, txn->dbi, D_RRSET, txn->zone_id, new_name,
+ type);
+ if (query == NULL) {
+ result = ISC_R_NOMEMORY;
+ goto cleanup;
+ }
+
+ result = db_execute(state, txn->dbi, query);
+ free(query);
+
+cleanup:
+ free(new_name);
+ return (result);
+}
diff --git a/contrib/dlz/modules/mysqldyn/testing/README b/contrib/dlz/modules/mysqldyn/testing/README
new file mode 100644
index 0000000..862ec6f
--- /dev/null
+++ b/contrib/dlz/modules/mysqldyn/testing/README
@@ -0,0 +1,11 @@
+These files were used for testing on Ubuntu Linux using MySQL
+
+To set up a test server:
+- Install MySQL: sudo apt-get install mysql-server
+- Run "mysql --user=USER --password=PASSWORD < dlz.schema" to set up database
+- Run "mysql --user=USER --password=PASSWORD < dlz.data" to populate it
+- Update named.conf with correct USER and PASSWORD
+- Generate a TSIG key: "ddns-confgen -qz example.com"
+
+To query the database, use "dig -p 5300 @localhost"
+To send dynamic updates, use "nsupdate -p 5300 -k ddns.key"
diff --git a/contrib/dlz/modules/mysqldyn/testing/dlz.data b/contrib/dlz/modules/mysqldyn/testing/dlz.data
new file mode 100644
index 0000000..068ad7a
--- /dev/null
+++ b/contrib/dlz/modules/mysqldyn/testing/dlz.data
@@ -0,0 +1,18 @@
+use BindDB;
+insert into `Zones`
+ ( `id`, `domain`, `host`, `admin`, `serial`, `expire`,
+ `refresh`, `retry`, `minimum`, `ttl`, `writeable`) VALUES
+ (1, 'example.com', '@', 'info', 2014040100, 10800,
+ 7200, 604800, 86400, 86400, 1);
+
+insert into `ZoneData`
+ (`id`, `zone_id`, `name`, `type`, `data`) VALUES
+ ('', 1, '@', 'NS', 'ns1.example.com.'),
+ ('', 1, '@', 'NS', 'ns2.example.com.'),
+ ('', 1, '@', 'MX', '10 mail.example.com.'),
+ ('', 1, '@', 'A', '192.168.0.2'),
+ ('', 1, '@', 'TXT', '"v=spf1 ip:192.168.0.3 ~all"'),
+ ('', 1, 'www', 'CNAME', 'example.com.'),
+ ('', 1, 'mail', 'A', '192.168.0.3'),
+ ('', 1, 'ns1', 'A', '192.168.1.111'),
+ ('', 1, 'ns2', 'A', '192.168.1.222');
diff --git a/contrib/dlz/modules/mysqldyn/testing/dlz.schema b/contrib/dlz/modules/mysqldyn/testing/dlz.schema
new file mode 100644
index 0000000..a28f912
--- /dev/null
+++ b/contrib/dlz/modules/mysqldyn/testing/dlz.schema
@@ -0,0 +1,31 @@
+CREATE DATABASE `BindDB` DEFAULT CHARACTER SET latin1;
+USE `BindDB`;
+
+CREATE TABLE `ZoneData` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `zone_id` int(11) NOT NULL,
+ `name` varchar(128) NOT NULL DEFAULT '',
+ `type` varchar(16) NOT NULL DEFAULT '',
+ `data` varchar(128) NOT NULL DEFAULT '',
+ `ttl` int(11) NOT NULL DEFAULT '86400',
+ PRIMARY KEY (`id`),
+ KEY `zone_idx` (`zone_id`),
+ KEY `name_idx` (`zone_id`, `name`),
+ KEY `type_idx` (`type`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+CREATE TABLE `Zones` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `domain` varchar(128) NOT NULL DEFAULT '',
+ `host` varchar(128) NOT NULL DEFAULT '',
+ `admin` varchar(128) NOT NULL DEFAULT '',
+ `serial` int(11) NOT NULL DEFAULT '1',
+ `expire` int(11) NOT NULL DEFAULT '86400',
+ `refresh` int(11) NOT NULL DEFAULT '86400',
+ `retry` int(11) NOT NULL DEFAULT '86400',
+ `minimum` int(11) NOT NULL DEFAULT '86400',
+ `ttl` int(11) NOT NULL DEFAULT '86400',
+ `writeable` tinyint(1) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`),
+ KEY `domain_idx` (`domain`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
diff --git a/contrib/dlz/modules/mysqldyn/testing/named.conf b/contrib/dlz/modules/mysqldyn/testing/named.conf
new file mode 100644
index 0000000..8131d9c
--- /dev/null
+++ b/contrib/dlz/modules/mysqldyn/testing/named.conf
@@ -0,0 +1,39 @@
+/*
+ * 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;
+};
+
+include "ddns.key";
+
+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_mysqldyn_mod.so BindDB localhost root password";
+};