diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:59:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:59:48 +0000 |
commit | 3b9b6d0b8e7f798023c9d109c490449d528fde80 (patch) | |
tree | 2e1c188dd7b8d7475cd163de9ae02c428343669b /contrib | |
parent | Initial commit. (diff) | |
download | bind9-upstream/1%9.18.19.tar.xz bind9-upstream/1%9.18.19.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')
64 files changed, 13632 insertions, 0 deletions
diff --git a/contrib/README b/contrib/README new file mode 100644 index 0000000..5f0682b --- /dev/null +++ b/contrib/README @@ -0,0 +1,31 @@ +<!-- +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. +--> + +This directory contains scripts, tools, and other useful accessories to +BIND 9. Contrib software is not supported by ISC, but reported bugs will +be fixed as time permits. + + - scripts/ + + Assorted useful scripts, including 'nanny' which monitors + named and restarts it in the event of a crash, 'zone-edit' + which enables editing of a dynamic zone, and others. + + - dlz/modules + + Dynamically linkable DLZ modules that can be configured into + named at runtime, enabling access to external data sources including + LDAP, MySQL, Berkeley DB, perl scripts, etc. + +Some links to useful software and other resources related to BIND 9 and +DNS can be found at https://www.isc.org/dns-tools-and-resources. diff --git a/contrib/dlz/example/Makefile b/contrib/dlz/example/Makefile new file mode 100644 index 0000000..5c66ba1 --- /dev/null +++ b/contrib/dlz/example/Makefile @@ -0,0 +1,27 @@ +# 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. + +# for building the dlz_example driver we don't use +# the bind9 build structure as the aim is to provide an +# example that is separable from the bind9 source tree + +# this means this Makefile is not portable, so the testsuite +# skips this test on platforms where it doesn't build + +CFLAGS += -Wall -fPIC -g + +all: dlz_example.so + +dlz_example.so: dlz_example.o + $(CC) $(CFLAGS) -shared -o dlz_example.so dlz_example.o + +clean: + rm -f dlz_example.o dlz_example.so diff --git a/contrib/dlz/example/README b/contrib/dlz/example/README new file mode 100644 index 0000000..d87812a --- /dev/null +++ b/contrib/dlz/example/README @@ -0,0 +1,256 @@ +<!-- +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. +--> + +OVERVIEW: + +DLZ (Dynamically Loadable Zones) is an extension to BIND 9 that +allows zone data to be retrieved directly from an external database. +There is no required format or schema. DLZ drivers exist for several +different database backends including PostgreSQL, MySQL, and LDAP and +can be written for any other. + +Historically, DLZ drivers had to be statically linked with the named +binary and were turned on via a configure option at compile time (for +example, "configure --with-dlz-ldap"). Currently, the drivers provided +in the BIND 9 tarball in contrib/dlz/drivers are still linked this way. + +However, as of BIND 9.8, it is also possible to link some DLZ modules +dynamically at runtime, via the DLZ "dlopen" driver, which acts as a +generic wrapper around a shared object that implements the DLZ API. The +"dlopen" driver is linked into named by default, so configure options are +no longer necessary unless using older DLZ drivers. + +When the DLZ module provides data to named, it does so in text format. +The response is converted to DNS wire format by named. This conversion, +and the lack of any internal caching, places significant limits on the +query performance of DLZ modules. Consequently, DLZ is not recommended +for use on high-volume servers. However, it can be used in a hidden +primary configuration, with secondaries retrieving zone updates via AXFR. +(Note, however, that DLZ has no built-in support for DNS notify; secondaries +are not automatically informed of changes to the zones in the database.) + +CONFIGURING DLZ: + +A DLZ database is configured with a "dlz" statement in named.conf. + + dlz example { + database "dlopen driver.so <args>"; + search yes; + }; + +This specifies a DLZ module to search when answering queries; the module +is implemented in "driver.so" and is loaded at runtime by the dlopen DLZ +driver. Multiple "dlz" statements can be specified; when answering a +query, all DLZ modules with the "search" option set to "yes" will be +checked for an answer, and the best available answer will be returned +to the client. + +The "search" option in this example can be omitted, as "yes" is the +default value. If it is set to "no", then this DLZ module is *not* +searched for best-match when a query is received. Instead, zones in +this DLZ must be separately specified in a zone statement. This can +be useful when conventional zone semantics are desired but you wish +to use a different back-end storage mechanism than the standard zone +database. For example, to use a DLZ module for an NXDOMAIN redirection +zone: + + dlz other { + database "dlopen driver.so <args>"; + search no; + }; + + zone "." { + type redirect; + dlz other; + }; + +EXAMPLE DRIVER: + +This directory contains an example of an externally-lodable DLZ module, +dlz_example.c, which demonstrates the features of the DLZ API. It sets up +a single zone, whose name is configured in named.conf. The zone can answer +queries and AXFR requests, and accept DDNS updates. + +By default, at runtime, the zone implemented by this driver will contain +an SOA, NS, and a single A record at the apex. If configured in named.conf +to use the name "example.nil", then, the zone will look like this: + + example.nil. 3600 IN SOA example.nil. hostmaster.example.nil. ( + 123 900 600 86400 3600 + ) + example.nil. 3600 IN NS example.nil. + example.nil. 1800 IN A 10.53.0.1 + +The driver is also capable of retrieving information about the querying +client, and altering its response on the basis of this information. To +demonstrate this feature, the example driver responds to queries for +"source-addr.<zonename>/TXT" with the source address of the query. +Note, however, that this record will *not* be included in AXFR or ANY +responses. (Normally, this feature would be used to alter responses in +some other fashion, e.g., by providing different address records for +a particular name depending on the network from which the query arrived.) + +DYNAMIC UPDATES AND TRANSACTIONS: + +If a DLZ module wants to implement dynamic DNS updates (DDNS), the +normal calling sequence is + - dlz_newversion (start a 'transaction') + - dlz_addrdataset (add records) + - dlz_subrdataset (remove records) + - dlz_closeversion (end a 'transaction') + +However, BIND may also query the database during the transaction +(e.g., to check prerequisites), and your DLZ might need to know whether +the lookup is against the pre-existing data, or the new data. +dlz_lookup() doesn't give you access to the 'versionp' pointer +directly, so it must be passed via 'clientinfo' structure if +it is needed. + +The dlz_example.c code has sample code to show how to get the 'versionp' +pointer from within dlz_lookup(). If it's set to NULL, we query +the standard database; if non-NULL, we query against the in-flight +data within the appropriate uncommitted transaction. + +IMPLEMENTATION NOTES: + +The minimal set of type definitions, prototypes, and macros needed +for implementing a DLZ driver is in ../modules/dlz_minimal.h. Copy this +header file into your source tree when creating an external DLZ module. + +The DLZ dlopen driver provides a set of callback functions: + + - void log(int level, const char *fmt, ...); + + Writes the specified string to the named log, at the specified + log level. Uses printf() format semantics. + + - isc_result_t putrr(dns_sdlzlookup_t *lookup, const char *type, + dns_ttl_t ttl, const char *data); + + Puts a DNS resource record into the query response, which + referenced by the opaque structure 'lookup' provided by named. + + - isc_result_t putnamedrr(dns_sdlzallnotes_t *allnodes, + const char *name, const char *type, + dns_ttl_t ttl, const char *data); + + Puts a DNS resource record into an AXFR response, which is + referenced by the opaque structure 'allnodes' provided by named. + + - isc_result_t writeable_zone(dns_view_t *view, const char *zone_name); + + Allows the DLZ module to inform named that a given zone can receive + DDNS updates. (Note: This is not currently supported for DLZ + databases that are configured as 'search no;') + +The external DLZ module can define the following functions (some of these +are mandatory, others optional). + + - int dlz_version(unsigned int *flags); + + Required for alL external DLZ modules, to indicate the version number + of the DLZ dlopen driver that this module supports. It should return + the value DLZ_DLOPEN_VERSION, which is defined in the file + contrib/dlz/modules/dlz_minimal.h and is currently 3. 'flags' is + updated to indicate capabilities of the module. In particular, if + the module is thread-safe then it sets 'flags' to include + DNS_SDLZFLAG_THREADSAFE. (Other capability flags may be added in + the future.) + + - isc_result_t dlz_create(const char *dlzname, + unsigned int argc, char *argv[], + void **dbdata, ...); + + Required for all external DLZ modules; this call initializes the + module. + + - void dlz_destroy(void *dbdata); + + Optional. If supplied, this will be called when the driver is + unloaded. + + - isc_result_t dlz_findzonedb(void *dbdata, const char *name, + dns_clientinfomethods_t *methods, + dns_clientinfo_t *clientinfo); + + Required for all external DLZ modules. This indicates whether + the DLZ module can answer for the given name. Returns ISC_R_SUCCESS + if so, and ISC_R_NOTFOUND if not. As an optimization, it can + also return ISC_R_NOMORE: this indicates that the DLZ module has + no data for the given name or for any name above it in the DNS. + This prevents named from searching for a zone cut. + + - 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); + + Required for all external DLZ modules. This carries out the database + lookup for a query. + + - isc_result_t dlz_allowzonexfr(void *dbdata, const char *name, + const char *client); + + Optional. Supply this if you want the module to support AXFR + for the specified zone and client. A return value of ISC_R_SUCCESS + means AXFR is allowed, any other value means it isn't. + + - isc_result_t dlz_allnodes(const char *zone, void *dbdata, + dns_sdlzallnodes_t *allnodes); + + Optional, but must be supplied dlz_allowzonexfr() is. This function + returns all nodes in the zone in order to perform a zone transfer. + + - isc_result_t dlz_newversion(const char *zone, void *dbdata, + void **versionp); + + Optional. Supply this if you want the module to support DDNS + updates. This function starts a transaction in the database. + + + - void dlz_closeversion(const char *zone, bool commit, + void *dbdata, void **versionp); + + Optional, but must be supplied if dlz_newversion() is. This function + closes a transaction. 'commit' indicates whether to commit the changes + to the database, or ignore them. + + - isc_result_t dlz_configure(dns_view_t *view, void *dbdata); + + Optional, but must be supplied in order to support DDNS updates. + + - bool dlz_ssumatch(const char *signer, const char *name, + const char *tcpaddr, const char *type, + const char *key, uint32_t keydatalen, + uint8_t *keydata, void *dbdata); + + Optional, but must be supplied in order to support DDNS updates. + + - isc_result_t dlz_addrdataset(const char *name, const char *rdatastr, + void *dbdata, void *version); + + Optional, but must be supplied in order to support DDNS updates. + Adds the data in 'rdatastr' to a database node. + + - isc_result_t dlz_subrdataset(const char *name, const char *rdatastr, + void *dbdata, void *version); + + Optional, but must be supplied in order to support DDNS updates. + Removes the data in 'rdatastr' from a database node. + + - isc_result_t dlz_delrdataset(const char *name, const char *rdatastr, + void *dbdata, void *version); + + Optional, but must be supplied in order to support DDNS updates. + Deletes all data matching the type specified in 'rdatastr' from + the database. diff --git a/contrib/dlz/example/dlz_example.c b/contrib/dlz/example/dlz_example.c new file mode 100644 index 0000000..8be3afb --- /dev/null +++ b/contrib/dlz/example/dlz_example.c @@ -0,0 +1,820 @@ +/* + * Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: ISC + * + * Permission to use, copy, modify, and/or 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 ISC DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL ISC 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 a very simple example of an external loadable DLZ + * driver, with update support. + */ + +#include <inttypes.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "../modules/include/dlz_minimal.h" + +#define CHECK(x) \ + do { \ + result = (x); \ + if (result != ISC_R_SUCCESS) \ + goto failure; \ + } while (0) + +/* For this simple example, use fixed sized strings */ +struct record { + char name[100]; + char type[10]; + char data[200]; + dns_ttl_t ttl; +}; + +#define MAX_RECORDS 100 + +struct dlz_example_data { + char *zone_name; + + /* An example driver doesn't need good memory management :-) */ + struct record current[MAX_RECORDS]; + struct record adds[MAX_RECORDS]; + struct record deletes[MAX_RECORDS]; + + bool transaction_started; + + /* 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; +}; + +static bool +single_valued(const char *type) { + const char *single[] = { "soa", "cname", NULL }; + int i; + + for (i = 0; single[i]; i++) { + if (strcasecmp(single[i], type) == 0) { + return (true); + } + } + return (false); +} + +/* + * Add a record to a list + */ +static isc_result_t +add_name(struct dlz_example_data *state, struct record *list, const char *name, + const char *type, dns_ttl_t ttl, const char *data) { + int i; + bool single = single_valued(type); + int first_empty = -1; + + for (i = 0; i < MAX_RECORDS; i++) { + if (first_empty == -1 && strlen(list[i].name) == 0U) { + first_empty = i; + } + if (strcasecmp(list[i].name, name) != 0) { + continue; + } + if (strcasecmp(list[i].type, type) != 0) { + continue; + } + if (!single && strcasecmp(list[i].data, data) != 0) { + continue; + } + break; + } + if (i == MAX_RECORDS && first_empty != -1) { + i = first_empty; + } + if (i == MAX_RECORDS) { + if (state->log != NULL) { + state->log(ISC_LOG_ERROR, "dlz_example: out of record " + "space"); + } + return (ISC_R_FAILURE); + } + + if (strlen(name) >= sizeof(list[i].name) || + strlen(type) >= sizeof(list[i].type) || + strlen(data) >= sizeof(list[i].data)) + { + return (ISC_R_NOSPACE); + } + + strncpy(list[i].name, name, sizeof(list[i].name)); + list[i].name[sizeof(list[i].name) - 1] = '\0'; + + strncpy(list[i].type, type, sizeof(list[i].type)); + list[i].type[sizeof(list[i].type) - 1] = '\0'; + + strncpy(list[i].data, data, sizeof(list[i].data)); + list[i].data[sizeof(list[i].data) - 1] = '\0'; + + list[i].ttl = ttl; + + return (ISC_R_SUCCESS); +} + +/* + * Delete a record from a list + */ +static isc_result_t +del_name(struct dlz_example_data *state, struct record *list, const char *name, + const char *type, dns_ttl_t ttl, const char *data) { + int i; + + UNUSED(state); + + for (i = 0; i < MAX_RECORDS; i++) { + if (strcasecmp(name, list[i].name) == 0 && + strcasecmp(type, list[i].type) == 0 && + strcasecmp(data, list[i].data) == 0 && ttl == list[i].ttl) + { + break; + } + } + if (i == MAX_RECORDS) { + return (ISC_R_NOTFOUND); + } + memset(&list[i], 0, sizeof(struct record)); + return (ISC_R_SUCCESS); +} + +static isc_result_t +fmt_address(isc_sockaddr_t *addr, char *buffer, size_t size) { + char addr_buf[100]; + const char *ret; + uint16_t port = 0; + + switch (addr->type.sa.sa_family) { + case AF_INET: + port = ntohs(addr->type.sin.sin_port); + ret = inet_ntop(AF_INET, &addr->type.sin.sin_addr, addr_buf, + sizeof(addr_buf)); + break; + case AF_INET6: + port = ntohs(addr->type.sin6.sin6_port); + ret = inet_ntop(AF_INET6, &addr->type.sin6.sin6_addr, addr_buf, + sizeof(addr_buf)); + break; + default: + return (ISC_R_FAILURE); + } + + if (ret == NULL) { + return (ISC_R_FAILURE); + } + + snprintf(buffer, size, "%s#%u", addr_buf, port); + return (ISC_R_SUCCESS); +} + +/* + * Return the version of the API + */ +int +dlz_version(unsigned int *flags) { + UNUSED(flags); + return (DLZ_DLOPEN_VERSION); +} + +/* + * Remember a helper function from the bind9 dlz_dlopen driver + */ +static void +b9_add_helper(struct dlz_example_data *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; + } +} + +/* + * Called to initialize the driver + */ +isc_result_t +dlz_create(const char *dlzname, unsigned int argc, char *argv[], void **dbdata, + ...) { + struct dlz_example_data *state; + const char *helper_name; + va_list ap; + char soa_data[200]; + const char *extra; + isc_result_t result; + int n; + + UNUSED(dlzname); + + state = calloc(1, sizeof(struct dlz_example_data)); + if (state == NULL) { + return (ISC_R_NOMEMORY); + } + + /* 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 (argc < 2 || argv[1][0] == '\0') { + if (state->log != NULL) { + state->log(ISC_LOG_ERROR, "dlz_example: please specify " + "a zone name"); + } + dlz_destroy(state); + return (ISC_R_FAILURE); + } + + /* Ensure zone name is absolute */ + state->zone_name = malloc(strlen(argv[1]) + 2); + if (state->zone_name == NULL) { + free(state); + return (ISC_R_NOMEMORY); + } + if (argv[1][strlen(argv[1]) - 1] == '.') { + strcpy(state->zone_name, argv[1]); + } else { + sprintf(state->zone_name, "%s.", argv[1]); + } + + if (strcmp(state->zone_name, ".") == 0) { + extra = ".root"; + } else { + extra = "."; + } + + n = sprintf(soa_data, "%s hostmaster%s%s 123 900 600 86400 3600", + state->zone_name, extra, state->zone_name); + + if (n < 0) { + CHECK(ISC_R_FAILURE); + } + if ((unsigned)n >= sizeof(soa_data)) { + CHECK(ISC_R_NOSPACE); + } + + add_name(state, &state->current[0], state->zone_name, "soa", 3600, + soa_data); + add_name(state, &state->current[0], state->zone_name, "ns", 3600, + state->zone_name); + add_name(state, &state->current[0], state->zone_name, "a", 1800, + "10.53.0.1"); + + if (state->log != NULL) { + state->log(ISC_LOG_INFO, "dlz_example: started for zone %s", + state->zone_name); + } + + *dbdata = state; + return (ISC_R_SUCCESS); + +failure: + free(state); + return (result); +} + +/* + * Shut down the backend + */ +void +dlz_destroy(void *dbdata) { + struct dlz_example_data *state = (struct dlz_example_data *)dbdata; + + if (state->log != NULL) { + state->log(ISC_LOG_INFO, "dlz_example: shutting down zone %s", + state->zone_name); + } + free(state->zone_name); + 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) { + struct dlz_example_data *state = (struct dlz_example_data *)dbdata; + isc_sockaddr_t *src; + char addrbuf[100]; + char absolute[1024]; + + strcpy(addrbuf, "unknown"); + if (methods != NULL && methods->sourceip != NULL && + methods->version - methods->age <= DNS_CLIENTINFOMETHODS_VERSION && + DNS_CLIENTINFOMETHODS_VERSION <= methods->version) + { + methods->sourceip(clientinfo, &src); + fmt_address(src, addrbuf, sizeof(addrbuf)); + } + state->log(ISC_LOG_INFO, "dlz_example: findzonedb connection from: %s", + addrbuf); + + state->log(ISC_LOG_INFO, + "dlz_example: dlz_findzonedb called with name '%s' " + "in zone DB '%s'", + name, state->zone_name); + + /* + * Returning ISC_R_NOTFOUND will cause the query logic to + * check the database for parent names, looking for zone cuts. + * + * Returning ISC_R_NOMORE prevents the query logic from doing + * this; it will move onto the next database after a single query. + */ + if (strcasecmp(name, "test.example.com") == 0) { + return (ISC_R_NOMORE); + } + + /* + * For example.net, only return ISC_R_NOMORE when queried + * from 10.53.0.1. + */ + if (strcasecmp(name, "test.example.net") == 0 && + strncmp(addrbuf, "10.53.0.1", 9) == 0) + { + return (ISC_R_NOMORE); + } + + if (strcasecmp(state->zone_name, name) == 0) { + return (ISC_R_SUCCESS); + } + + snprintf(absolute, sizeof(absolute), "%s.", name); + if (strcasecmp(state->zone_name, absolute) == 0) { + return (ISC_R_SUCCESS); + } + + return (ISC_R_NOTFOUND); +} + +/* + * Look up one record in the sample database. + * + * If the queryname is "source-addr", send back a TXT record containing + * the address of the client; this demonstrates the use of 'methods' + * and 'clientinfo'. + * + * If the queryname is "too-long", send back a TXT record that's too long + * to process; this should result in a SERVFAIL when queried. + */ +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; + struct dlz_example_data *state = (struct dlz_example_data *)dbdata; + bool found = false; + void *dbversion = NULL; + isc_sockaddr_t *src; + char full_name[256]; + char buf[512]; + int i; + + UNUSED(zone); + + if (state->putrr == NULL) { + return (ISC_R_NOTIMPLEMENTED); + } + + if (strcmp(name, "@") == 0) { + strncpy(full_name, state->zone_name, 255); + full_name[255] = '\0'; + } else { + snprintf(full_name, 255, "%s.%s", name, state->zone_name); + } + + /* + * If we need to know the database version (as set in + * the 'newversion' dlz function) we can pick it up from the + * clientinfo. + * + * This allows a lookup to query the correct version of the DNS + * data, if the DLZ can differentiate between versions. + * + * For example, if a new database transaction is created by + * 'newversion', the lookup should query within the same + * transaction scope if it can. + * + * If the DLZ only operates on 'live' data, then version + * wouldn't necessarily be needed. + */ + if (clientinfo != NULL && clientinfo->version >= 2) { + dbversion = clientinfo->dbversion; + if (dbversion != NULL && *(bool *)dbversion) { + state->log(ISC_LOG_INFO, "dlz_example: lookup against " + "live " + "transaction\n"); + } + } + + if (strcmp(name, "source-addr") == 0) { + char ecsbuf[DNS_ECS_FORMATSIZE] = "not supported"; + strncpy(buf, "unknown", sizeof(buf)); + if (methods != NULL && methods->sourceip != NULL && + (methods->version - methods->age <= + DNS_CLIENTINFOMETHODS_VERSION) && + DNS_CLIENTINFOMETHODS_VERSION <= methods->version) + { + methods->sourceip(clientinfo, &src); + fmt_address(src, buf, sizeof(buf)); + } + if (clientinfo != NULL && clientinfo->version >= 3) { + if (clientinfo->ecs.addr.family != AF_UNSPEC) { + dns_ecs_format(&clientinfo->ecs, ecsbuf, + sizeof(ecsbuf)); + } else { + snprintf(ecsbuf, sizeof(ecsbuf), "%s", + "not present"); + } + } + i = strlen(buf); + snprintf(buf + i, sizeof(buf) - i - 1, " ECS %s", ecsbuf); + + state->log(ISC_LOG_INFO, + "dlz_example: lookup connection from: %s", buf); + + found = true; + result = state->putrr(lookup, "TXT", 0, buf); + /* We could also generate a CNAME RR: + snprintf(buf, sizeof(buf), "%s.redirect.example.", ecsbuf); + result = state->putrr(lookup, "CNAME", 0, buf); */ + if (result != ISC_R_SUCCESS) { + return (result); + } + } + + if (strcmp(name, "too-long") == 0) { + for (i = 0; i < 511; i++) { + buf[i] = 'x'; + } + buf[i] = '\0'; + found = true; + result = state->putrr(lookup, "TXT", 0, buf); + if (result != ISC_R_SUCCESS) { + return (result); + } + } + + for (i = 0; i < MAX_RECORDS; i++) { + if (strcasecmp(state->current[i].name, full_name) == 0) { + found = true; + result = state->putrr(lookup, state->current[i].type, + state->current[i].ttl, + state->current[i].data); + if (result != ISC_R_SUCCESS) { + return (result); + } + } + } + + 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) { + UNUSED(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) { + struct dlz_example_data *state = (struct dlz_example_data *)dbdata; + int i; + + UNUSED(zone); + + if (state->putnamedrr == NULL) { + return (ISC_R_NOTIMPLEMENTED); + } + + for (i = 0; i < MAX_RECORDS; i++) { + isc_result_t result; + if (strlen(state->current[i].name) == 0U) { + continue; + } + result = state->putnamedrr(allnodes, state->current[i].name, + state->current[i].type, + state->current[i].ttl, + state->current[i].data); + if (result != ISC_R_SUCCESS) { + return (result); + } + } + + return (ISC_R_SUCCESS); +} + +/* + * Start a transaction + */ +isc_result_t +dlz_newversion(const char *zone, void *dbdata, void **versionp) { + struct dlz_example_data *state = (struct dlz_example_data *)dbdata; + + if (state->transaction_started) { + if (state->log != NULL) { + state->log(ISC_LOG_INFO, + "dlz_example: transaction already " + "started for zone %s", + zone); + } + return (ISC_R_FAILURE); + } + + state->transaction_started = true; + *versionp = (void *)&state->transaction_started; + + return (ISC_R_SUCCESS); +} + +/* + * End a transaction + */ +void +dlz_closeversion(const char *zone, bool commit, void *dbdata, void **versionp) { + struct dlz_example_data *state = (struct dlz_example_data *)dbdata; + + if (!state->transaction_started) { + if (state->log != NULL) { + state->log(ISC_LOG_INFO, + "dlz_example: transaction not " + "started for zone %s", + zone); + } + *versionp = NULL; + return; + } + + state->transaction_started = false; + + *versionp = NULL; + + if (commit) { + int i; + if (state->log != NULL) { + state->log(ISC_LOG_INFO, + "dlz_example: committing " + "transaction on zone %s", + zone); + } + for (i = 0; i < MAX_RECORDS; i++) { + if (strlen(state->deletes[i].name) > 0U) { + (void)del_name(state, &state->current[0], + state->deletes[i].name, + state->deletes[i].type, + state->deletes[i].ttl, + state->deletes[i].data); + } + } + for (i = 0; i < MAX_RECORDS; i++) { + if (strlen(state->adds[i].name) > 0U) { + (void)add_name(state, &state->current[0], + state->adds[i].name, + state->adds[i].type, + state->adds[i].ttl, + state->adds[i].data); + } + } + } else { + if (state->log != NULL) { + state->log(ISC_LOG_INFO, + "dlz_example: cancelling " + "transaction on zone %s", + zone); + } + } + memset(state->adds, 0, sizeof(state->adds)); + memset(state->deletes, 0, sizeof(state->deletes)); +} + +/* + * Configure a writeable zone + */ +isc_result_t +dlz_configure(dns_view_t *view, dns_dlzdb_t *dlzdb, void *dbdata) { + struct dlz_example_data *state = (struct dlz_example_data *)dbdata; + isc_result_t result; + + if (state->log != NULL) { + state->log(ISC_LOG_INFO, "dlz_example: starting configure"); + } + + if (state->writeable_zone == NULL) { + if (state->log != NULL) { + state->log(ISC_LOG_INFO, "dlz_example: no " + "writeable_zone method " + "available"); + } + return (ISC_R_FAILURE); + } + + result = state->writeable_zone(view, dlzdb, state->zone_name); + if (result != ISC_R_SUCCESS) { + if (state->log != NULL) { + state->log(ISC_LOG_ERROR, + "dlz_example: failed to " + "configure zone %s", + state->zone_name); + } + return (result); + } + + if (state->log != NULL) { + state->log(ISC_LOG_INFO, + "dlz_example: configured writeable " + "zone %s", + state->zone_name); + } + 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) { + struct dlz_example_data *state = (struct dlz_example_data *)dbdata; + + UNUSED(tcpaddr); + UNUSED(type); + UNUSED(key); + UNUSED(keydatalen); + UNUSED(keydata); + + if (strncmp(name, "deny.", 5) == 0) { + if (state->log != NULL) { + state->log(ISC_LOG_INFO, + "dlz_example: denying update " + "of name=%s by %s", + name, signer); + } + return (false); + } + if (state->log != NULL) { + state->log(ISC_LOG_INFO, + "dlz_example: allowing update of " + "name=%s by %s", + name, signer); + } + return (true); +} + +static isc_result_t +modrdataset(struct dlz_example_data *state, const char *name, + const char *rdatastr, struct record *list) { + char *full_name, *dclass, *type, *data, *ttlstr, *buf; + char absolute[1024]; + isc_result_t result; + char *saveptr = NULL; + + buf = strdup(rdatastr); + if (buf == NULL) { + return (ISC_R_FAILURE); + } + + /* + * 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 + */ + + full_name = strtok_r(buf, "\t", &saveptr); + if (full_name == NULL) { + goto error; + } + + ttlstr = strtok_r(NULL, "\t", &saveptr); + if (ttlstr == NULL) { + 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; + } + + if (name[strlen(name) - 1] != '.') { + snprintf(absolute, sizeof(absolute), "%s.", name); + name = absolute; + } + + result = add_name(state, list, name, type, strtoul(ttlstr, NULL, 10), + data); + free(buf); + return (result); + +error: + free(buf); + return (ISC_R_FAILURE); +} + +isc_result_t +dlz_addrdataset(const char *name, const char *rdatastr, void *dbdata, + void *version) { + struct dlz_example_data *state = (struct dlz_example_data *)dbdata; + + if (version != (void *)&state->transaction_started) { + return (ISC_R_FAILURE); + } + + if (state->log != NULL) { + state->log(ISC_LOG_INFO, "dlz_example: adding rdataset %s '%s'", + name, rdatastr); + } + + return (modrdataset(state, name, rdatastr, &state->adds[0])); +} + +isc_result_t +dlz_subrdataset(const char *name, const char *rdatastr, void *dbdata, + void *version) { + struct dlz_example_data *state = (struct dlz_example_data *)dbdata; + + if (version != (void *)&state->transaction_started) { + return (ISC_R_FAILURE); + } + + if (state->log != NULL) { + state->log(ISC_LOG_INFO, + "dlz_example: subtracting rdataset " + "%s '%s'", + name, rdatastr); + } + + return (modrdataset(state, name, rdatastr, &state->deletes[0])); +} + +isc_result_t +dlz_delrdataset(const char *name, const char *type, void *dbdata, + void *version) { + struct dlz_example_data *state = (struct dlz_example_data *)dbdata; + + if (version != (void *)&state->transaction_started) { + return (ISC_R_FAILURE); + } + + if (state->log != NULL) { + state->log(ISC_LOG_INFO, + "dlz_example: deleting rdataset %s " + "of type %s", + name, type); + } + + return (ISC_R_SUCCESS); +} diff --git a/contrib/dlz/example/named.conf b/contrib/dlz/example/named.conf new file mode 100644 index 0000000..eba464c --- /dev/null +++ b/contrib/dlz/example/named.conf @@ -0,0 +1,60 @@ +/* + * 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. + */ + +/* + * This is a sample named.conf file that uses the DLZ module defined in + * dlz_example.c. It sets up a zone 'example.nil' which can accept DDNS + * updates. + * + * By default, when run, the zone contains the following records: + * + * example.nil. 3600 IN SOA example.nil. hostmaster.example.nil. ( + * 123 900 600 86400 3600 + * ) + * example.nil. 3600 IN NS example.nil. + * example.nil. 1800 IN A 10.53.0.1 + * + * Additionally, a query for 'source-addr.example.nil/TXT' is always + * answered with the source address of the query. This is used to + * demonstrate the code that retrieves client information from the + * caller. + * + * To use this driver, "dlz_external.so" must be moved into the working + * directory for named. + */ + +options { + allow-transfer { any; }; + allow-query { any; }; + notify yes; + recursion no; +}; + +/* + * To test dynamic updates, create a DDNS key: + * + * ddns-confgen -q -z example.nil > ddns.key + * + * Then uncomment the following line: + * + * include "ddns.key"; + * + * Use "nsupdate -k ddns.key" when sending updates. (NOTE: This driver does + * not check the key that's used: as long as the update is signed by a key + * known to named, the update will be accepted. Only updates to names + * that begin with "deny." are rejected.) + */ + +dlz "example" { + database "dlopen ./dlz_example.so example.nil"; +}; diff --git a/contrib/dlz/modules/bdbhpt/Makefile b/contrib/dlz/modules/bdbhpt/Makefile new file mode 100644 index 0000000..29d6b94 --- /dev/null +++ b/contrib/dlz/modules/bdbhpt/Makefile @@ -0,0 +1,43 @@ +# 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 +BDB_LIBS=-ldb + +all: dlz_bdbhpt_dynamic.so + +dlz_bdbhpt_dynamic.so: dlz_bdbhpt_dynamic.c + $(CC) $(CFLAGS) -shared -o dlz_bdbhpt_dynamic.so \ + dlz_bdbhpt_dynamic.c $(BDB_LIBS) + +clean: + rm -f dlz_bdbhpt_dynamic.so + +install: dlz_bdbhpt_dynamic.so + mkdir -p $(DESTDIR)$(libdir) + install dlz_bdbhpt_dynamic.so $(DESTDIR)$(libdir) diff --git a/contrib/dlz/modules/bdbhpt/README.md b/contrib/dlz/modules/bdbhpt/README.md new file mode 100644 index 0000000..3a9e7a7 --- /dev/null +++ b/contrib/dlz/modules/bdbhpt/README.md @@ -0,0 +1,113 @@ +<!-- +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. +--> + +dlz-bdbhpt-dynamic +================== + +A Bind 9 Dynamically Loadable BerkeleyDB High Performance Text Driver + +Summary +------- + +This is an attempt to port the original Bind 9 DLZ bdbhpt_driver.c as +found in the Bind 9 source tree into the new DLZ dlopen driver API. +The goals of this project are as follows: + +* Provide DLZ facilities to OEM-supported Bind distributions +* Support both v1 (Bind 9.8) and v2 (Bind 9.9) of the dlopen() DLZ API + +Requirements +------------ + +You will need the following: + * Bind 9.8 or higher with the DLZ dlopen driver enabled + * BerkeleyDB libraries and header files + * A C compiler + +This distribution have been successfully installed and tested on +Ubuntu 12.04. + +Installation +------------ + +With the above requirements satisfied perform the following steps: + +1. Ensure the symlink for dlz_minimal.h points at the correct header + file matching your Bind version +2. Run: make +3. Run: sudo make install # this will install dlz_bdbhpt_dynamic.so + into /usr/lib/bind9/ +4. Add a DLZ statement similar to the example below into your + Bind configuration +5. Ensure your BerkeleyDB home-directory exists and can be written to + by the bind user +6. Use the included testing/bdbhpt-populate.pl script to provide some + data for initial testing + +Usage +----- + +Example usage is as follows: + +``` +dlz "bdbhpt_dynamic" { + database "dlopen /usr/lib/bind9/dlz_bdbhpt_dynamic.so T /var/cache/bind/dlz dnsdata.db"; +}; +``` + +The arguments for the "database" line above are as follows: + +1. dlopen - Use the dlopen DLZ driver to dynamically load our compiled + driver +2. The full path to your built dlz_bdbhpt_dynamic.so +3. Single character specifying the mode to open your BerkeleyDB + environment: + * T - Transactional Mode - Highest safety, lowest speed. + * C - Concurrent Mode - Lower safety (no rollback), higher speed. + * P - Private Mode - No interprocess communication & no locking. + Lowest safety, highest speed. +4. Directory containing your BerkeleyDB - this is where the BerkeleyDB + environment will be created. +5. Filename within this directory containing your BerkeleyDB tables. + +A copy of the above Bind configuration is included within +example/dlz.conf. + +Author +------ + +The person responsible for this is: + + Mark Goldfinch <g@g.org.nz> + +The code is maintained at: + + https://github.com/goldie80/dlz-bdbhpt-dynamic + +There is very little in the way of original code in this work, +however, original license conditions from both bdbhpt_driver.c and +dlz_example.c are maintained in the dlz_bdbhpt_dynamic.c. diff --git a/contrib/dlz/modules/bdbhpt/dlz_bdbhpt_dynamic.c b/contrib/dlz/modules/bdbhpt/dlz_bdbhpt_dynamic.c new file mode 100644 index 0000000..e84f866 --- /dev/null +++ b/contrib/dlz/modules/bdbhpt/dlz_bdbhpt_dynamic.c @@ -0,0 +1,837 @@ +/* + * 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) 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 is simply a merge of Andrew Tridgell's dlz_example.c and the + * original bdb_bdbhpt_driver.c + * + * This provides the externally loadable bdbhpt DLZ driver, without + * update support + * + */ + +#include <db.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "dlz_minimal.h" + +/* should the bdb driver use threads. */ +#define bdbhpt_threads DB_THREAD + +/* bdbhpt database names */ +#define dlz_data "dns_data" +#define dlz_zone "dns_zone" +#define dlz_xfr "dns_xfr" +#define dlz_client "dns_client" + +#define dlz_bdbhpt_dynamic_version "0.1" + +/* + * This structure contains all our DB handles and helper functions we + * inherit from the dlz_dlopen driver + * + */ +typedef struct bdbhpt_instance { + DB_ENV *dbenv; /* bdbhpt environment */ + DB *data; /* dns_data database handle */ + DB *zone; /* zone database handle */ + DB *xfr; /* zone xfr database handle */ + DB *client; /* client database handle */ + + /* 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; +} bdbhpt_instance_t; + +typedef struct bdbhpt_parsed_data { + char *host; + char *type; + int ttl; + char *data; +} bdbhpt_parsed_data_t; + +static void +b9_add_helper(struct bdbhpt_instance *db, const char *helper_name, void *ptr); + +/*% + * Reverses a string in place. + */ +static char * +bdbhpt_strrev(char *str) { + char *p1, *p2; + + if (!str || !*str) { + return (str); + } + for (p1 = str, p2 = str + strlen(str) - 1; p2 > p1; ++p1, --p2) { + *p1 ^= *p2; + *p2 ^= *p1; + *p1 ^= *p2; + } + return (str); +} + +/*% + * Parses the DBT from the Berkeley DB into a parsed_data record + * The parsed_data record should be allocated before and passed into the + * bdbhpt_parse_data function. The char (type & data) fields should not + * be "free"d as that memory is part of the DBT data field. It will be + * "free"d when the DBT is freed. + */ + +static isc_result_t +bdbhpt_parse_data(log_t *log, char *in, bdbhpt_parsed_data_t *pd) { + char *endp, *ttlStr; + char *tmp = in; + char *lastchar = (char *)&tmp[strlen(tmp)]; + + /*% + * String should be formatted as: + * replication_id + * (a space) + * host_name + * (a space) + * ttl + * (a space) + * type + * (a space) + * remaining data + * + * examples: + * + * 9191 host 10 A 127.0.0.1 + * server1_212 host 10 A 127.0.0.2 + * {xxxx-xxxx-xxxx-xxxx-xxxx} host 10 MX 20 mail.example.com + */ + + /* + * we don't need the replication id, so don't + * bother saving a pointer to it. + */ + + /* find space after replication id */ + tmp = strchr(tmp, ' '); + /* verify we found a space */ + if (tmp == NULL) { + return (ISC_R_FAILURE); + } + /* make sure it is safe to increment pointer */ + if (++tmp > lastchar) { + return (ISC_R_FAILURE); + } + + /* save pointer to host */ + pd->host = tmp; + + /* find space after host and change it to a '\0' */ + tmp = strchr(tmp, ' '); + /* verify we found a space */ + if (tmp == NULL) { + return (ISC_R_FAILURE); + } + /* change the space to a null (string terminator) */ + tmp[0] = '\0'; + /* make sure it is safe to increment pointer */ + if (++tmp > lastchar) { + return (ISC_R_FAILURE); + } + + /* save pointer to ttl string */ + ttlStr = tmp; + + /* find space after ttl and change it to a '\0' */ + tmp = strchr(tmp, ' '); + /* verify we found a space */ + if (tmp == NULL) { + return (ISC_R_FAILURE); + } + /* change the space to a null (string terminator) */ + tmp[0] = '\0'; + /* make sure it is safe to increment pointer */ + if (++tmp > lastchar) { + return (ISC_R_FAILURE); + } + + /* save pointer to dns type */ + pd->type = tmp; + + /* find space after type and change it to a '\0' */ + tmp = strchr(tmp, ' '); + /* verify we found a space */ + if (tmp == NULL) { + return (ISC_R_FAILURE); + } + /* change the space to a null (string terminator) */ + tmp[0] = '\0'; + /* make sure it is safe to increment pointer */ + if (++tmp > lastchar) { + return (ISC_R_FAILURE); + } + + /* save pointer to remainder of DNS data */ + pd->data = tmp; + + /* convert ttl string to integer */ + pd->ttl = strtol(ttlStr, &endp, 10); + if (*endp != '\0' || pd->ttl < 0) { + log(ISC_LOG_ERROR, "bdbhpt_dynamic: " + "ttl must be a positive number"); + return (ISC_R_FAILURE); + } + + /* if we get this far everything should have worked. */ + return (ISC_R_SUCCESS); +} + +/* + * See if a zone transfer is allowed + */ +isc_result_t +dlz_allowzonexfr(void *dbdata, const char *name, const char *client) { + isc_result_t result; + bdbhpt_instance_t *db = (bdbhpt_instance_t *)dbdata; + DBT key, data; + + /* check to see if we are authoritative for the zone first. */ +#if DLZ_DLOPEN_VERSION >= 3 + result = dlz_findzonedb(dbdata, name, NULL, NULL); +#else /* if DLZ_DLOPEN_VERSION >= 3 */ + result = dlz_findzonedb(dbdata, name); +#endif /* if DLZ_DLOPEN_VERSION >= 3 */ + if (result != ISC_R_SUCCESS) { + return (ISC_R_NOTFOUND); + } + + memset(&key, 0, sizeof(DBT)); + key.flags = DB_DBT_MALLOC; + key.data = strdup(name); + if (key.data == NULL) { + result = ISC_R_NOMEMORY; + goto xfr_cleanup; + } + key.size = strlen(key.data); + + memset(&data, 0, sizeof(DBT)); + data.flags = DB_DBT_MALLOC; + data.data = strdup(client); + if (data.data == NULL) { + result = ISC_R_NOMEMORY; + goto xfr_cleanup; + } + data.size = strlen(data.data); + + switch (db->client->get(db->client, NULL, &key, &data, DB_GET_BOTH)) { + case DB_NOTFOUND: + result = ISC_R_NOTFOUND; + break; + case 0: + result = ISC_R_SUCCESS; + break; + default: + result = ISC_R_FAILURE; + } + +xfr_cleanup: + /* free any memory duplicate string in the key field */ + if (key.data != NULL) { + free(key.data); + } + + /* free any memory allocated to the data field. */ + if (data.data != NULL) { + free(data.data); + } + + return (result); +} + +/*% + * Perform a zone transfer + * + * BDB does not allow a secondary index on a database that allows + * duplicates. We have a few options: + * + * 1) kill speed by having lookup method use a secondary db which + * is associated to the primary DB with the DNS data. Then have + * another secondary db for zone transfer which also points to + * the dns_data primary. NO - The point of this driver is + * lookup performance. + * + * 2) Blow up database size by storing DNS data twice. Once for + * the lookup (dns_data) database, and a second time for the zone + * transfer (dns_xfr) database. NO - That would probably require + * a larger cache to provide good performance. Also, that would + * make the DB larger on disk potentially slowing it as well. + * + * 3) Loop through the dns_xfr database with a cursor to get + * all the different hosts in a zone. Then use the zone & host + * together to lookup the data in the dns_data database. YES - + * This may slow down zone xfr's a little, but that's ok they + * don't happen as often and don't need to be as fast. We can + * also use this table when deleting a zone (The BDB driver + * is read only - the delete would be used during replication + * updates by a separate process). + */ +isc_result_t +dlz_allnodes(const char *zone, void *dbdata, dns_sdlzallnodes_t *allnodes) { + isc_result_t result = ISC_R_NOTFOUND; + bdbhpt_instance_t *db = (bdbhpt_instance_t *)dbdata; + DBC *xfr_cursor = NULL; + DBC *dns_cursor = NULL; + DBT xfr_key, xfr_data, dns_key, dns_data; + int xfr_flags; + int dns_flags; + int bdbhptres; + bdbhpt_parsed_data_t pd; + char *tmp = NULL, *tmp_zone, *tmp_zone_host = NULL; + + memset(&xfr_key, 0, sizeof(DBT)); + memset(&xfr_data, 0, sizeof(DBT)); + memset(&dns_key, 0, sizeof(DBT)); + memset(&dns_data, 0, sizeof(DBT)); + + xfr_key.data = tmp_zone = strdup(zone); + if (xfr_key.data == NULL) { + return (ISC_R_NOMEMORY); + } + + xfr_key.size = strlen(xfr_key.data); + + /* get a cursor to loop through dns_xfr table */ + if (db->xfr->cursor(db->xfr, NULL, &xfr_cursor, 0) != 0) { + result = ISC_R_FAILURE; + goto allnodes_cleanup; + } + + /* get a cursor to loop through dns_data table */ + if (db->data->cursor(db->data, NULL, &dns_cursor, 0) != 0) { + result = ISC_R_FAILURE; + goto allnodes_cleanup; + } + + xfr_flags = DB_SET; + + /* loop through xfr table for specified zone. */ + while ((bdbhptres = xfr_cursor->c_get(xfr_cursor, &xfr_key, &xfr_data, + xfr_flags)) == 0) + { + xfr_flags = DB_NEXT_DUP; + + /* +1 to allow for space between zone and host names */ + dns_key.size = xfr_data.size + xfr_key.size + 1; + + /* +1 to allow for null term at end of string. */ + dns_key.data = tmp_zone_host = malloc(dns_key.size + 1); + if (dns_key.data == NULL) { + goto allnodes_cleanup; + } + + /* + * construct search key for dns_data. + * zone_name(a space)host_name + */ + strcpy(dns_key.data, zone); + strcat(dns_key.data, " "); + strncat(dns_key.data, xfr_data.data, xfr_data.size); + + dns_flags = DB_SET; + + while ((bdbhptres = dns_cursor->c_get(dns_cursor, &dns_key, + &dns_data, dns_flags)) == + 0) + { + dns_flags = DB_NEXT_DUP; + + /* +1 to allow for null term at end of string. */ + tmp = realloc(tmp, dns_data.size + 1); + if (tmp == NULL) { + goto allnodes_cleanup; + } + + /* copy data to tmp string, and append null term. */ + strncpy(tmp, dns_data.data, dns_data.size); + tmp[dns_data.size] = '\0'; + + /* split string into dns data parts. */ + if (bdbhpt_parse_data(db->log, tmp, &pd) != + ISC_R_SUCCESS) + { + goto allnodes_cleanup; + } + result = db->putnamedrr(allnodes, pd.host, pd.type, + pd.ttl, pd.data); + if (result != ISC_R_SUCCESS) { + goto allnodes_cleanup; + } + } /* end inner while loop */ + + /* clean up memory */ + if (tmp_zone_host != NULL) { + free(tmp_zone_host); + tmp_zone_host = NULL; + } + } /* end outer while loop */ + +allnodes_cleanup: + /* free any memory */ + if (tmp != NULL) { + free(tmp); + } + + if (tmp_zone_host != NULL) { + free(tmp_zone_host); + } + + if (tmp_zone != NULL) { + free(tmp_zone); + } + + /* get rid of cursors */ + if (xfr_cursor != NULL) { + xfr_cursor->c_close(xfr_cursor); + } + + if (dns_cursor != NULL) { + dns_cursor->c_close(dns_cursor); + } + + return (result); +} + +/*% + * Performs bdbhpt cleanup. + * Used by bdbhpt_create if there is an error starting up. + * Used by bdbhpt_destroy when the driver is shutting down. + */ +static void +bdbhpt_cleanup(bdbhpt_instance_t *db) { + /* close databases */ + if (db->data != NULL) { + db->data->close(db->data, 0); + } + if (db->xfr != NULL) { + db->xfr->close(db->xfr, 0); + } + if (db->zone != NULL) { + db->zone->close(db->zone, 0); + } + if (db->client != NULL) { + db->client->close(db->client, 0); + } + + /* close environment */ + if (db->dbenv != NULL) { + db->dbenv->close(db->dbenv, 0); + } +} + +/* + * See if we handle a given zone + */ +#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 */ +{ + isc_result_t result; + bdbhpt_instance_t *db = (bdbhpt_instance_t *)dbdata; + DBT key, data; + + memset(&key, 0, sizeof(DBT)); + memset(&data, 0, sizeof(DBT)); + data.flags = DB_DBT_MALLOC; + +#if DLZ_DLOPEN_VERSION >= 3 + UNUSED(methods); + UNUSED(clientinfo); +#endif /* if DLZ_DLOPEN_VERSION >= 3 */ + + key.data = strdup(name); + + if (key.data == NULL) { + return (ISC_R_NOMEMORY); + } + + /* + * reverse string to take advantage of BDB locality of reference + * if we need further lookups because the zone doesn't match the + * first time. + */ + key.data = bdbhpt_strrev(key.data); + key.size = strlen(key.data); + + switch (db->zone->get(db->zone, NULL, &key, &data, 0)) { + case DB_NOTFOUND: + result = ISC_R_NOTFOUND; + break; + case 0: + result = ISC_R_SUCCESS; + break; + default: + result = ISC_R_FAILURE; + } + + /* free any memory duplicate string in the key field */ + if (key.data != NULL) { + free(key.data); + } + + /* free any memory allocated to the data field. */ + if (data.data != NULL) { + free(data.data); + } + + return (result); +} + +/* + * Look up one record in the database. + * + */ +#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 = ISC_R_NOTFOUND; + bdbhpt_instance_t *db = (bdbhpt_instance_t *)dbdata; + DBC *data_cursor = NULL; + DBT key, data; + int bdbhptres; + int flags; + + bdbhpt_parsed_data_t pd; + char *tmp = NULL; + char *keyStr = NULL; + +#if DLZ_DLOPEN_VERSION >= 2 + UNUSED(methods); + UNUSED(clientinfo); +#endif /* if DLZ_DLOPEN_VERSION >= 2 */ + + memset(&key, 0, sizeof(DBT)); + memset(&data, 0, sizeof(DBT)); + + key.size = strlen(zone) + strlen(name) + 1; + + /* allocate mem for key */ + key.data = keyStr = malloc((key.size + 1) * sizeof(char)); + + if (keyStr == NULL) { + return (ISC_R_NOMEMORY); + } + + strcpy(keyStr, zone); + strcat(keyStr, " "); + strcat(keyStr, name); + + /* get a cursor to loop through data */ + if (db->data->cursor(db->data, NULL, &data_cursor, 0) != 0) { + result = ISC_R_FAILURE; + goto lookup_cleanup; + } + + result = ISC_R_NOTFOUND; + + flags = DB_SET; + while ((bdbhptres = data_cursor->c_get(data_cursor, &key, &data, + flags)) == 0) + { + flags = DB_NEXT_DUP; + tmp = realloc(tmp, data.size + 1); + if (tmp == NULL) { + goto lookup_cleanup; + } + + strncpy(tmp, data.data, data.size); + tmp[data.size] = '\0'; + + if (bdbhpt_parse_data(db->log, tmp, &pd) != ISC_R_SUCCESS) { + goto lookup_cleanup; + } + + result = db->putrr(lookup, pd.type, pd.ttl, pd.data); + if (result != ISC_R_SUCCESS) { + goto lookup_cleanup; + } + } /* end while loop */ + +lookup_cleanup: + /* get rid of cursor */ + if (data_cursor != NULL) { + data_cursor->c_close(data_cursor); + } + + free(keyStr); + if (tmp != NULL) { + free(tmp); + } + + return (result); +} + +/*% + * Initialises, sets flags and then opens Berkeley databases. + */ +static isc_result_t +bdbhpt_opendb(log_t *log, DB_ENV *db_env, DBTYPE db_type, DB **db, + const char *db_name, char *db_file, int flags) { + int result; + + /* Initialise the database. */ + if ((result = db_create(db, db_env, 0)) != 0) { + log(ISC_LOG_ERROR, + "bdbhpt_dynamic: could not initialize %s database. " + "BerkeleyDB error: %s", + db_name, db_strerror(result)); + return (ISC_R_FAILURE); + } + + /* set database flags. */ + if ((result = (*db)->set_flags(*db, flags)) != 0) { + log(ISC_LOG_ERROR, + "bdbhpt_dynamic: could not set flags for %s database. " + "BerkeleyDB error: %s", + db_name, db_strerror(result)); + return (ISC_R_FAILURE); + } + + /* open the database. */ + if ((result = (*db)->open(*db, NULL, db_file, db_name, db_type, + DB_RDONLY | bdbhpt_threads, 0)) != 0) + { + log(ISC_LOG_ERROR, + "bdbhpt_dynamic: could not open %s database in %s. " + "BerkeleyDB error: %s", + db_name, db_file, db_strerror(result)); + return (ISC_R_FAILURE); + } + + return (ISC_R_SUCCESS); +} + +/* + * Called to initialize the driver + */ +isc_result_t +dlz_create(const char *dlzname, unsigned int argc, char *argv[], void **dbdata, + ...) { + isc_result_t result; + int bdbhptres; + int bdbFlags = 0; + bdbhpt_instance_t *db = NULL; + + const char *helper_name; + va_list ap; + + UNUSED(dlzname); + + /* Allocate memory for our db structures and helper functions */ + db = calloc(1, sizeof(struct bdbhpt_instance)); + if (db == NULL) { + return (ISC_R_NOMEMORY); + } + + /* Fill in the helper functions */ + va_start(ap, dbdata); + while ((helper_name = va_arg(ap, const char *)) != NULL) { + b9_add_helper(db, helper_name, va_arg(ap, void *)); + } + va_end(ap); + + /* verify we have 4 arg's passed to the driver */ + if (argc != 4) { + db->log(ISC_LOG_ERROR, + "bdbhpt_dynamic: please supply 3 command line args. " + "You supplied: %s", + argc); + return (ISC_R_FAILURE); + } + + switch ((char)*argv[1]) { + /* + * Transactional mode. Highest safety - lowest speed. + */ + case 'T': + case 't': + bdbFlags = DB_INIT_MPOOL | DB_INIT_LOCK | DB_INIT_LOG | + DB_INIT_TXN; + db->log(ISC_LOG_INFO, "bdbhpt_dynamic: using transactional " + "mode."); + break; + + /* + * Concurrent mode. Lower safety (no rollback) - + * higher speed. + */ + case 'C': + case 'c': + bdbFlags = DB_INIT_CDB | DB_INIT_MPOOL; + db->log(ISC_LOG_INFO, "bdbhpt_dynamic: using concurrent mode."); + break; + + /* + * Private mode. No inter-process communication & no locking. + * Lowest safety - highest speed. + */ + case 'P': + case 'p': + bdbFlags = DB_PRIVATE | DB_INIT_MPOOL; + db->log(ISC_LOG_INFO, "bdbhpt_dynamic: using private mode."); + break; + default: + db->log(ISC_LOG_ERROR, + "bdbhpt_dynamic: " + "operating mode must be set to P or C or T. " + "You specified '%s'", + argv[1]); + return (ISC_R_FAILURE); + } + + /* + * create bdbhpt environment + * Basically bdbhpt allocates and assigns memory to db->dbenv + */ + bdbhptres = db_env_create(&db->dbenv, 0); + if (bdbhptres != 0) { + db->log(ISC_LOG_ERROR, + "bdbhpt_dynamic: db environment could not be created. " + "BerkeleyDB error: %s", + db_strerror(bdbhptres)); + result = ISC_R_FAILURE; + goto init_cleanup; + } + + /* open bdbhpt environment */ + bdbhptres = db->dbenv->open(db->dbenv, argv[2], + bdbFlags | bdbhpt_threads | DB_CREATE, 0); + if (bdbhptres != 0) { + db->log(ISC_LOG_ERROR, + "bdbhpt_dynamic: " + "db environment at '%s' could not be opened. " + "BerkeleyDB error: %s", + argv[2], db_strerror(bdbhptres)); + result = ISC_R_FAILURE; + goto init_cleanup; + } + + /* open dlz_data database. */ + result = bdbhpt_opendb(db->log, db->dbenv, DB_UNKNOWN, &db->data, + dlz_data, argv[3], DB_DUP | DB_DUPSORT); + if (result != ISC_R_SUCCESS) { + goto init_cleanup; + } + + /* open dlz_xfr database. */ + result = bdbhpt_opendb(db->log, db->dbenv, DB_UNKNOWN, &db->xfr, + dlz_xfr, argv[3], DB_DUP | DB_DUPSORT); + if (result != ISC_R_SUCCESS) { + goto init_cleanup; + } + + /* open dlz_zone database. */ + result = bdbhpt_opendb(db->log, db->dbenv, DB_UNKNOWN, &db->zone, + dlz_zone, argv[3], 0); + if (result != ISC_R_SUCCESS) { + goto init_cleanup; + } + + /* open dlz_client database. */ + result = bdbhpt_opendb(db->log, db->dbenv, DB_UNKNOWN, &db->client, + dlz_client, argv[3], DB_DUP | DB_DUPSORT); + if (result != ISC_R_SUCCESS) { + goto init_cleanup; + } + + *dbdata = db; + + db->log(ISC_LOG_INFO, "bdbhpt_dynamic: version %s, started", + dlz_bdbhpt_dynamic_version); + return (ISC_R_SUCCESS); + +init_cleanup: + bdbhpt_cleanup(db); + return (result); +} + +/* + * Shut down the backend + */ +void +dlz_destroy(void *dbdata) { + struct bdbhpt_instance *db = (struct bdbhpt_instance *)dbdata; + + db->log(ISC_LOG_INFO, "dlz_bdbhpt_dynamic (%s): shutting down", + dlz_bdbhpt_dynamic_version); + bdbhpt_cleanup((bdbhpt_instance_t *)dbdata); + free(db); +} + +/* + * Return the version of the API + */ +int +dlz_version(unsigned int *flags) { + UNUSED(flags); + return (DLZ_DLOPEN_VERSION); +} + +/* + * Register a helper function from the bind9 dlz_dlopen driver + */ +static void +b9_add_helper(struct bdbhpt_instance *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/bdbhpt/testing/README b/contrib/dlz/modules/bdbhpt/testing/README new file mode 100644 index 0000000..aec0935 --- /dev/null +++ b/contrib/dlz/modules/bdbhpt/testing/README @@ -0,0 +1,11 @@ +These files were used for testing on Ubuntu Linux using BDB 5.1 and +BerkeleyDB 0.54 for perl. + +- Populate the database from dns-data.txt for zone example.com: + + perl bdbhpt-populate.pl \ + --bdb=test.db --input=dns-data.txt --zones=example.com + +- Run "named -g -c named.conf" + +BDB server is now loaded with example.com data from the file test.db diff --git a/contrib/dlz/modules/bdbhpt/testing/bdbhpt-populate.pl b/contrib/dlz/modules/bdbhpt/testing/bdbhpt-populate.pl new file mode 100755 index 0000000..f71f783 --- /dev/null +++ b/contrib/dlz/modules/bdbhpt/testing/bdbhpt-populate.pl @@ -0,0 +1,244 @@ +#!/usr/bin/perl -w + +# 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. + +use strict; +use BerkeleyDB; +use Getopt::Long; + +my $opt = {}; +if (!GetOptions($opt, qw/bdb|b:s input|i:s zones|z:s help|h/)) { + usage('GetOptions processing failed.'); + exit 1; +} + +if ($opt->{help}) { + usage(); + exit 0; +} + +my $db_file = $opt->{bdb}; +if (!defined $db_file || $db_file eq '') { + usage('Please specify an output BerkeleyDB filename.'); + exit 1; +} + +my $input_file = $opt->{input}; +if (!defined $input_file || $input_file eq '') { + usage('Please specify an input records file.'); + exit 1; +} + +my $zone_list = $opt->{zones}; +if (!defined $zone_list || $zone_list eq '') { + usage('Please specify a space separated list of zones'); + exit 1; +} + +my $records = []; +my $unique_names = []; +populate_records(records=>$records, input_file=>$input_file, unique_names=>$unique_names); + +my $flags = DB_CREATE; + +my $dns_data = new BerkeleyDB::Hash + -Filename => $db_file, + -Flags => $flags, + -Property => DB_DUP | DB_DUPSORT, + -Subname => "dns_data" + || die "Cannot create dns_data: $BerkeleyDB::Error"; + +my $replId = 0; +my @zones = split(/\s+/, $zone_list); +foreach my $zone (@zones) { + foreach my $r (@$records) { + my $name = $r->{name}; + my $ttl = $r->{ttl}; + my $type = $r->{type}; + my $data = $r->{data}; + + $data =~ s/\%zone\%/$zone/g; + $data =~ s/\%driver\%/bdbhpt-dynamic/g; + + my $row_name = "$zone $name"; + my $row_value = "$replId $name $ttl $type $data"; + if ($dns_data->db_put($row_name, $row_value) != 0) { + die "Cannot add record '$row_name' -> '$row_value' to dns_data: $BerkeleyDB::Error"; + } + $replId++; + } +} + +$dns_data->db_close(); + +my $dns_xfr = new BerkeleyDB::Hash + -Filename => $db_file, + -Flags => $flags, + -Property => DB_DUP | DB_DUPSORT, + -Subname => "dns_xfr" + or die "Cannot create dns_xfr: $BerkeleyDB::Error"; + +foreach my $zone (@zones) { + foreach my $name (@$unique_names) { + if ($dns_xfr->db_put($zone, $name) != 0) { + die "Cannot add record '$zone' -> '$name' to dns_xfr: $BerkeleyDB::Error"; + } + } +} + +$dns_xfr->db_close(); + +my $dns_client = new BerkeleyDB::Hash + -Filename => $db_file, + -Flags => $flags, + -Property => DB_DUP | DB_DUPSORT, + -Subname => "dns_client" + or die "Cannot create dns_client: $BerkeleyDB::Error"; + +foreach my $zone (@zones) { + my $ip = '127.0.0.1'; + if ($dns_client->db_put($zone, $ip) != 0) { + die "Cannot add record '$zone' -> '$ip' to dns_client: $BerkeleyDB::Error"; + } +} + +$dns_client->db_close(); + +my $dns_zone = new BerkeleyDB::Btree + -Filename => $db_file, + -Flags => $flags, + -Property => 0, + -Subname => "dns_zone" + or die "Cannot create dns_zone: $BerkeleyDB::Error"; + +foreach my $zone (@zones) { + my $reversed_zone = reverse($zone); + if ($dns_zone->db_put($reversed_zone, "1") != 0) { + die "Cannot add record '$reversed_zone' -> '1' to dns_zone: $BerkeleyDB::Error"; + } +}; + +$dns_zone->db_close(); + +exit 0; + +sub usage { + my ($message) = @_; + if (defined $message && $message ne '') { + print STDERR $message . "\n\n"; + } + + print STDERR "usage: $0 --bdb=<bdb-file> --input=<input-file> --zones=<zone-list>\n\n"; + print STDERR "\tbdb-file: The output BerkeleyDB file you wish to create and use with bdbhpt-dynamic\n\n"; + print STDERR "\tinput-file: The input text-file containing records to populate within your zones\n\n"; + print STDERR "\tzone-list: The space-separated list of zones you wish to create\n\n"; +} + +sub populate_records { + my (%args) = @_; + my $records = $args{records}; + my $input_file = $args{input_file}; + my $unique_names = $args{unique_names}; + + my %unique; + + open(RECORDS, $input_file) || die "unable to open $input_file: $!"; + while (<RECORDS>) { + chomp; + s/\#.*$//; + s/^\s+//; + if ($_ eq '') { + next; + } + my ($name, $ttl, $type, $data) = split(/\s+/, $_, 4); + my $record = { name=>$name, ttl=>$ttl, type=>$type, data=>$data }; + if (validate_record($record)) { + push @$records, $record; + $unique{$name} = 1; + } + } + close(RECORDS); + + foreach my $name (sort keys %unique) { + push @$unique_names, $name; + } +} + +# This could probably do more in-depth tests, but these tests are better than nothing! +sub validate_record { + my ($r) = @_; + + # http://en.wikipedia.org/wiki/List_of_DNS_record_types + my @TYPES = qw/A AAAA AFSDB APL CERT CNAME DHCID DLV DNAME DNSKEY DS HIP IPSECKEY KEY KX LOC MX NAPTR NS NSEC NSEC3 NSEC3PARAM PTR RRSIG RP SIG SOA SPF SRV SSHFP TA TKEY TLSA TSIG TXT/; + my $VALID_TYPE = {}; + foreach my $t (@TYPES) { + $VALID_TYPE->{$t} = 1; + } + + if (!defined $r->{name} || $r->{name} eq '') { + die "Record name must be set"; + } + + if (!defined $r->{ttl} || $r->{ttl} eq '') { + die "Record TTL must be set"; + } + + if ($r->{ttl} =~ /\D/ || $r->{ttl} < 0) { + die "Record TTL must be an integer 0 or greater"; + } + + if (!defined $r->{type} || $r->{type} eq '') { + die "Record type must be set"; + } + + if (!$VALID_TYPE->{$r->{type}}) { + die "Unsupported record type: $r->{type}"; + } + + # Lets do some data validation for the records which will cause bind to crash if they're wrong + if ($r->{type} eq 'SOA') { + my $soa_error = "SOA records must take the form: 'server email refresh retry expire negative_cache_ttl'"; + my ($server, $email, $version, $refresh, $retry, $expire, $negative_cache_ttl) = split(/\s+/, $r->{data}); + if (!defined $server || $server eq '') { + die "$soa_error, missing server"; + } + if (!defined $email || $email eq '') { + die "$soa_error, missing email"; + } + if (!defined $refresh || $refresh eq '') { + die "$soa_error, missing refresh"; + } + if ($refresh =~ /\D/ || $refresh <= 0) { + die "$soa_error, refresh must be an integer greater than 0"; + } + if (!defined $retry || $retry eq '') { + die "$soa_error, missing retry"; + } + if ($retry =~ /\D/ || $retry <= 0) { + die "$soa_error, retry must be an integer greater than 0"; + } + if (!defined $expire || $expire eq '') { + die "$soa_error, missing expire"; + } + if ($expire =~ /\D/ || $expire <= 0) { + die "$soa_error, expire must be an integer greater than 0"; + } + if (!defined $negative_cache_ttl || $negative_cache_ttl eq '') { + die "$soa_error, missing negative cache ttl"; + } + if ($negative_cache_ttl =~ /\D/ || $negative_cache_ttl <= 0) { + die "$soa_error, negative cache ttl must be an integer greater than 0"; + } + } + + return 1; +} diff --git a/contrib/dlz/modules/bdbhpt/testing/dns-data.txt b/contrib/dlz/modules/bdbhpt/testing/dns-data.txt new file mode 100644 index 0000000..242cd3d --- /dev/null +++ b/contrib/dlz/modules/bdbhpt/testing/dns-data.txt @@ -0,0 +1,19 @@ +# Name TTL Type Data +@ 3600 SOA ns1.%zone%. root.%zone%. 2012071700 604800 86400 2419200 10800 +@ 3600 NS ns1.%zone%. +@ 3600 MX 5 mx1.%zone%. +@ 3600 MX 10 mx2.%zone%. +@ 3600 TXT This zone brought to you by %driver%! +jabber 3600 A 127.0.0.1 +mx1 3600 A 127.0.0.2 +mx2 3600 A 127.0.0.3 +jabber 3600 A 127.0.0.4 +ns1 3600 A 127.0.0.5 +ns1 3600 AAAA ::1 +voip 3600 A 127.0.0.6 +www 3600 CNAME www1.%zone% +www1 3600 A 127.0.0.7 +_sip._udp 3600 SRV 5 0 5060 voip.%zone%. +_jabber._tcp 3600 SRV 5 0 5269 jabber.%zone%. +_xmpp-client._tcp 3600 SRV 5 0 5222 jabber.%zone%. +_xmpp-server._tcp 3600 SRV 5 0 5269 jabber.%zone%. diff --git a/contrib/dlz/modules/bdbhpt/testing/named.conf b/contrib/dlz/modules/bdbhpt/testing/named.conf new file mode 100644 index 0000000..c8b5722 --- /dev/null +++ b/contrib/dlz/modules/bdbhpt/testing/named.conf @@ -0,0 +1,37 @@ +/* + * 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 "bdbhpt_dynamic" { + database "dlopen ../dlz_bdbhpt_dynamic.so T . test.db"; +}; diff --git a/contrib/dlz/modules/common/dlz_dbi.c b/contrib/dlz/modules/common/dlz_dbi.c new file mode 100644 index 0000000..88ff632 --- /dev/null +++ b/contrib/dlz/modules/common/dlz_dbi.c @@ -0,0 +1,484 @@ +/* + * 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. + */ + +#include <ctype.h> +#include <errno.h> +#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> + +/*% + * properly destroys a querylist by de-allocating the + * memory for each query segment, and then the list itself + */ + +void +destroy_querylist(query_list_t **querylist) { + query_segment_t *tseg = NULL; + query_segment_t *nseg = NULL; + + /* if query list is null, nothing to do */ + if (*querylist == NULL) { + return; + } + + /* start at the top of the list */ + nseg = DLZ_LIST_HEAD(**querylist); + while (nseg != NULL) { /* loop, until end of list */ + tseg = nseg; + /* + * free the query segment's text string but only if it + * was really a query segment, and not a pointer to + * %zone%, or %record%, or %client% + */ + if (tseg->cmd != NULL && tseg->direct) { + free(tseg->cmd); + } + /* get the next query segment, before we destroy this one. */ + nseg = DLZ_LIST_NEXT(nseg, link); + /* deallocate this query segment. */ + free(tseg); + } + /* deallocate the query segment list */ + free(*querylist); +} + +/*% constructs a query list by parsing a string into query segments */ +isc_result_t +build_querylist(const char *query_str, char **zone, char **record, + char **client, query_list_t **querylist, unsigned int flags, + log_t log) { + isc_result_t result; + bool foundzone = false; + bool foundrecord = false; + bool foundclient = false; + char *temp_str = NULL; + char *right_str = NULL; + char *token = NULL; + query_list_t *tql; + query_segment_t *tseg = NULL; + + /* if query string is null, or zero length */ + if (query_str == NULL || strlen(query_str) < 1) { + if ((flags & REQUIRE_QUERY) == 0) { + /* we don't need it were ok. */ + return (ISC_R_SUCCESS); + } else { + /* we did need it, PROBLEM!!! */ + return (ISC_R_FAILURE); + } + } + + /* allocate memory for query list */ + tql = calloc(1, sizeof(query_list_t)); + /* couldn't allocate memory. Problem!! */ + if (tql == NULL) { + return (ISC_R_NOMEMORY); + } + + /* initialize the query segment list */ + DLZ_LIST_INIT(*tql); + + /* make a copy of query_str so we can chop it up */ + temp_str = right_str = strdup(query_str); + /* couldn't make a copy, problem!! */ + if (right_str == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + + /* loop through the string and chop it up */ + for (token = strtok_r(right_str, "$", &temp_str); token; + token = strtok_r(NULL, "$", &temp_str)) + { + /* allocate memory for tseg */ + tseg = calloc(1, sizeof(query_segment_t)); + if (tseg == NULL) { /* no memory, clean everything up. */ + result = ISC_R_NOMEMORY; + goto cleanup; + } + tseg->direct = false; + /* initialize the query segment link */ + DLZ_LINK_INIT(tseg, link); + /* append the query segment to the list */ + DLZ_LIST_APPEND(*tql, tseg, link); + + /* + * split string at the first "$". set query segment to + * left portion + */ + tseg->cmd = strdup(token); + if (tseg->cmd == NULL) { + /* no memory, clean everything up. */ + result = ISC_R_NOMEMORY; + goto cleanup; + } + /* tseg->cmd points directly to a string. */ + tseg->direct = true; + tseg->strlen = strlen(tseg->cmd); + + /* check if we encountered "$zone$" token */ + if (strcasecmp(tseg->cmd, "zone") == 0) { + /* + * we don't really need, or want the "zone" + * text, so get rid of it. + */ + free(tseg->cmd); + /* set tseg->cmd to in-direct zone string */ + tseg->cmd = (char **)zone; + tseg->strlen = 0; + /* tseg->cmd points in-directly to a string */ + tseg->direct = false; + foundzone = true; + /* check if we encountered "$record$" token */ + } else if (strcasecmp(tseg->cmd, "record") == 0) { + /* + * we don't really need, or want the "record" + * text, so get rid of it. + */ + free(tseg->cmd); + /* set tseg->cmd to in-direct record string */ + tseg->cmd = (char **)record; + tseg->strlen = 0; + /* tseg->cmd points in-directly poinsts to a string */ + tseg->direct = false; + foundrecord = true; + /* check if we encountered "$client$" token */ + } else if (strcasecmp(tseg->cmd, "client") == 0) { + /* + * we don't really need, or want the "client" + * text, so get rid of it. + */ + free(tseg->cmd); + /* set tseg->cmd to in-direct record string */ + tseg->cmd = (char **)client; + tseg->strlen = 0; + /* tseg->cmd points in-directly poinsts to a string */ + tseg->direct = false; + foundclient = true; + } + } + + /* we don't need temp_str any more */ + free(right_str); + right_str = NULL; + /* + * add checks later to verify zone and record are found if + * necessary. + */ + + /* if this query requires %client%, make sure we found it */ + if (((flags & REQUIRE_CLIENT) != 0) && (!foundclient)) { + /* Write error message to log */ + if (log != NULL) { + log(ISC_LOG_ERROR, "Required token $client$ not " + "found."); + } + result = ISC_R_FAILURE; + goto flag_fail; + } + + /* if this query requires %record%, make sure we found it */ + if (((flags & REQUIRE_RECORD) != 0) && (!foundrecord)) { + /* Write error message to log */ + if (log != NULL) { + log(ISC_LOG_ERROR, "Required token $record$ not " + "found."); + } + result = ISC_R_FAILURE; + goto flag_fail; + } + + /* if this query requires %zone%, make sure we found it */ + if (((flags & REQUIRE_ZONE) != 0) && (!foundzone)) { + /* Write error message to log */ + if (log != NULL) { + log(ISC_LOG_ERROR, "Required token $zone$ not found."); + } + result = ISC_R_FAILURE; + goto flag_fail; + } + + /* pass back the query list */ + *querylist = (query_list_t *)tql; + + /* return success */ + return (ISC_R_SUCCESS); + +cleanup: + /* get rid of temp_str */ + if (right_str != NULL) { + free(right_str); + } + +flag_fail: + /* get rid of what was build of the query list */ + destroy_querylist(&tql); + return (result); +} + +/*% + * build a query string from query segments, and dynamic segments + * dynamic segments replace where the tokens %zone%, %record%, %client% + * used to be in our queries from named.conf + */ +char * +build_querystring(query_list_t *querylist) { + query_segment_t *tseg = NULL; + unsigned int length = 0; + char *qs = NULL; + + /* start at the top of the list */ + tseg = DLZ_LIST_HEAD(*querylist); + while (tseg != NULL) { + /* + * if this is a query segment, use the + * precalculated string length + */ + if (tseg->direct) { + length += tseg->strlen; + } else { /* calculate string length for dynamic segments. */ + length += strlen(*(char **)tseg->cmd); + } + /* get the next segment */ + tseg = DLZ_LIST_NEXT(tseg, link); + } + + qs = malloc(length + 1); + if (qs == NULL) { + return (NULL); + } + + *qs = '\0'; + /* start at the top of the list again */ + tseg = DLZ_LIST_HEAD(*querylist); + while (tseg != NULL) { + if (tseg->direct) { + /* query segments */ + strcat(qs, tseg->cmd); + } else { + /* dynamic segments */ + strcat(qs, *(char **)tseg->cmd); + } + /* get the next segment */ + tseg = DLZ_LIST_NEXT(tseg, link); + } + + return (qs); +} + +/*% constructs a dbinstance (DBI) */ +isc_result_t +build_dbinstance(const char *allnodes_str, const char *allowxfr_str, + const char *authority_str, const char *findzone_str, + const char *lookup_str, const char *countzone_str, + dbinstance_t **dbi, log_t log) { + isc_result_t result; + dbinstance_t *db = NULL; + int err; + + /* allocate and zero memory for driver structure */ + db = calloc(1, sizeof(dbinstance_t)); + if (db == NULL) { + if (log != NULL) { + log(ISC_LOG_ERROR, "Could not allocate memory for " + "database instance object."); + } + return (ISC_R_NOMEMORY); + } + memset(db, 0, sizeof(dbinstance_t)); + db->dbconn = NULL; + db->client = NULL; + db->record = NULL; + db->zone = NULL; + db->query_buf = NULL; + db->allnodes_q = NULL; + db->allowxfr_q = NULL; + db->authority_q = NULL; + db->findzone_q = NULL; + db->countzone_q = NULL; + db->lookup_q = NULL; + + /* initialize the reference count mutex */ + err = dlz_mutex_init(&db->lock, NULL); + if (err == ENOMEM) { + result = ISC_R_NOMEMORY; + goto cleanup; + } else if (err != 0) { + result = ISC_R_UNEXPECTED; + goto cleanup; + } + + /* build the all nodes query list */ + result = build_querylist(allnodes_str, &db->zone, &db->record, + &db->client, &db->allnodes_q, REQUIRE_ZONE, + log); + /* if unsuccessful, log err msg and cleanup */ + if (result != ISC_R_SUCCESS) { + if (log != NULL) { + log(ISC_LOG_ERROR, "Could not build all nodes query " + "list"); + } + goto cleanup; + } + + /* build the allow zone transfer query list */ + result = build_querylist(allowxfr_str, &db->zone, &db->record, + &db->client, &db->allowxfr_q, + REQUIRE_ZONE | REQUIRE_CLIENT, log); + /* if unsuccessful, log err msg and cleanup */ + if (result != ISC_R_SUCCESS) { + if (log != NULL) { + log(ISC_LOG_ERROR, "Could not build allow xfr query " + "list"); + } + goto cleanup; + } + + /* build the authority query, query list */ + result = build_querylist(authority_str, &db->zone, &db->record, + &db->client, &db->authority_q, REQUIRE_ZONE, + log); + /* if unsuccessful, log err msg and cleanup */ + if (result != ISC_R_SUCCESS) { + if (log != NULL) { + log(ISC_LOG_ERROR, "Could not build authority query " + "list"); + } + goto cleanup; + } + + /* build findzone query, query list */ + result = build_querylist(findzone_str, &db->zone, &db->record, + &db->client, &db->findzone_q, REQUIRE_ZONE, + log); + /* if unsuccessful, log err msg and cleanup */ + if (result != ISC_R_SUCCESS) { + if (log != NULL) { + log(ISC_LOG_ERROR, "Could not build find zone query " + "list"); + } + goto cleanup; + } + + /* build countzone query, query list */ + result = build_querylist(countzone_str, &db->zone, &db->record, + &db->client, &db->countzone_q, REQUIRE_ZONE, + log); + /* if unsuccessful, log err msg and cleanup */ + if (result != ISC_R_SUCCESS) { + if (log != NULL) { + log(ISC_LOG_ERROR, "Could not build count zone query " + "list"); + } + goto cleanup; + } + + /* build lookup query, query list */ + result = build_querylist(lookup_str, &db->zone, &db->record, + &db->client, &db->lookup_q, REQUIRE_RECORD, + log); + /* if unsuccessful, log err msg and cleanup */ + if (result != ISC_R_SUCCESS) { + if (log != NULL) { + log(ISC_LOG_ERROR, "Could not build lookup query list"); + } + goto cleanup; + } + + /* pass back the db instance */ + *dbi = (dbinstance_t *)db; + + /* return success */ + return (ISC_R_SUCCESS); + +cleanup: + /* destroy whatever was build of the db instance */ + destroy_dbinstance(db); + /* return failure */ + return (ISC_R_FAILURE); +} + +void +destroy_dbinstance(dbinstance_t *dbi) { + /* destroy any query lists we created */ + destroy_querylist(&dbi->allnodes_q); + destroy_querylist(&dbi->allowxfr_q); + destroy_querylist(&dbi->authority_q); + destroy_querylist(&dbi->findzone_q); + destroy_querylist(&dbi->countzone_q); + destroy_querylist(&dbi->lookup_q); + + /* get rid of the mutex */ + (void)dlz_mutex_destroy(&dbi->lock); + + /* return, and detach the memory */ + free(dbi); +} + +char * +get_parameter_value(const char *input, const char *key) { + int keylen; + char *keystart; + char value[255]; + int i; + + if (key == NULL || input == NULL || *input == '\0') { + return (NULL); + } + + keylen = strlen(key); + + if (keylen < 1) { + return (NULL); + } + + keystart = strstr(input, key); + + if (keystart == NULL) { + return (NULL); + } + + for (i = 0; i < 255; i++) { + value[i] = keystart[keylen + i]; + if (isspace(value[i]) || value[i] == '\0') { + value[i] = '\0'; + break; + } + } + + return (strdup(value)); +} diff --git a/contrib/dlz/modules/filesystem/Makefile b/contrib/dlz/modules/filesystem/Makefile new file mode 100644 index 0000000..6c5470f --- /dev/null +++ b/contrib/dlz/modules/filesystem/Makefile @@ -0,0 +1,45 @@ +# 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 + +all: dlz_filesystem_dynamic.so + +dir.o: dir.c + $(CC) $(CFLAGS) -c dir.c + +dlz_filesystem_dynamic.so: dlz_filesystem_dynamic.c dir.o + $(CC) $(CFLAGS) -shared -o dlz_filesystem_dynamic.so \ + dlz_filesystem_dynamic.c dir.o + +clean: + rm -f dlz_filesystem_dynamic.so *.o + +install: dlz_filesystem_dynamic.so + mkdir -p $(DESTDIR)$(libdir) + install dlz_filesystem_dynamic.so $(DESTDIR)$(libdir) diff --git a/contrib/dlz/modules/filesystem/dir.c b/contrib/dlz/modules/filesystem/dir.c new file mode 100644 index 0000000..f2b3a4e --- /dev/null +++ b/contrib/dlz/modules/filesystem/dir.c @@ -0,0 +1,118 @@ +/* + * 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/. + */ + +#include "dir.h" +#include <ctype.h> +#include <errno.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "dlz_minimal.h" + +void +dir_init(dir_t *dir) { + dir->entry.name[0] = '\0'; + dir->entry.length = 0; + + dir->handle = NULL; +} + +isc_result_t +dir_open(dir_t *dir, const char *dirname) { + char *p; + isc_result_t result = ISC_R_SUCCESS; + + if (strlen(dirname) + 3 > sizeof(dir->dirname)) { + return (ISC_R_NOSPACE); + } + strcpy(dir->dirname, dirname); + + p = dir->dirname + strlen(dir->dirname); + if (dir->dirname < p && *(p - 1) != '/') { + *p++ = '/'; + } + *p++ = '*'; + *p = '\0'; + + dir->handle = opendir(dirname); + if (dir->handle == NULL) { + switch (errno) { + case ENOTDIR: + case ELOOP: + case EINVAL: + case ENAMETOOLONG: + case EBADF: + result = ISC_R_INVALIDFILE; + break; + case ENOENT: + result = ISC_R_FILENOTFOUND; + break; + case EACCES: + case EPERM: + result = ISC_R_NOPERM; + break; + case ENOMEM: + result = ISC_R_NOMEMORY; + break; + default: + result = ISC_R_UNEXPECTED; + break; + } + } + + return (result); +} + +/*! + * \brief Return previously retrieved file or get next one. + * + * Unix's dirent has + * separate open and read functions, but the Win32 and DOS interfaces open + * the dir stream and reads the first file in one operation. + */ +isc_result_t +dir_read(dir_t *dir) { + struct dirent *entry; + + entry = readdir(dir->handle); + if (entry == NULL) { + return (ISC_R_NOMORE); + } + + if (sizeof(dir->entry.name) <= strlen(entry->d_name)) { + return (ISC_R_UNEXPECTED); + } + + strcpy(dir->entry.name, entry->d_name); + + dir->entry.length = strlen(entry->d_name); + return (ISC_R_SUCCESS); +} + +/*! + * \brief Close directory stream. + */ +void +dir_close(dir_t *dir) { + (void)closedir(dir->handle); + dir->handle = NULL; +} + +/*! + * \brief Reposition directory stream at start. + */ +isc_result_t +dir_reset(dir_t *dir) { + rewinddir(dir->handle); + + return (ISC_R_SUCCESS); +} diff --git a/contrib/dlz/modules/filesystem/dir.h b/contrib/dlz/modules/filesystem/dir.h new file mode 100644 index 0000000..b9a3749 --- /dev/null +++ b/contrib/dlz/modules/filesystem/dir.h @@ -0,0 +1,45 @@ +/* + * 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/. + */ + +#pragma once + +#include <dirent.h> +#include <sys/types.h> + +#include <dlz_minimal.h> + +#define DIR_NAMEMAX 256 +#define DIR_PATHMAX 1024 + +typedef struct direntry { + char name[DIR_NAMEMAX]; + unsigned int length; +} direntry_t; + +typedef struct dir { + char dirname[DIR_PATHMAX]; + direntry_t entry; + DIR *handle; +} dir_t; + +void +dir_init(dir_t *dir); + +isc_result_t +dir_open(dir_t *dir, const char *dirname); + +isc_result_t +dir_read(dir_t *dir); + +isc_result_t +dir_reset(dir_t *dir); + +void +dir_close(dir_t *dir); diff --git a/contrib/dlz/modules/filesystem/dlz_filesystem_dynamic.c b/contrib/dlz/modules/filesystem/dlz_filesystem_dynamic.c new file mode 100644 index 0000000..3b8a8bb --- /dev/null +++ b/contrib/dlz/modules/filesystem/dlz_filesystem_dynamic.c @@ -0,0 +1,1008 @@ +/* + * 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 filesystem DLZ module, without + * update support + */ + +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> + +#include "dir.h" +#include "dlz_list.h" +#include "dlz_minimal.h" + +typedef struct config_data { + char *basedir; + int basedirsize; + char *datadir; + int datadirsize; + char *xfrdir; + int xfrdirsize; + int splitcnt; + char separator; + char pathsep; + + /* 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; +} config_data_t; + +typedef struct dir_entry dir_entry_t; + +struct dir_entry { + char dirpath[DIR_PATHMAX]; + DLZ_LINK(dir_entry_t) link; +}; + +typedef DLZ_LIST(dir_entry_t) dlist_t; + +/* forward reference */ + +static void +b9_add_helper(struct config_data *cd, const char *helper_name, void *ptr); + +/* + * Private methods + */ +static bool +is_safe(const char *input) { + unsigned int i; + unsigned int len = strlen(input); + + /* check that only allowed characters are in the domain name */ + for (i = 0; i < len; i++) { + /* '.' is allowed, but has special requirements */ + if (input[i] == '.') { + /* '.' is not allowed as first char */ + if (i == 0) { + return (false); + } + /* '..', two dots together is not allowed. */ + if (input[i - 1] == '.') { + return (false); + } + /* '.' is not allowed as last char */ + if (i == len - 1) { + return (false); + } + /* only 1 dot in ok location, continue at next char */ + continue; + } + /* '-' is allowed, continue at next char */ + if (input[i] == '-') { + continue; + } + /* 0-9 is allowed, continue at next char */ + if (input[i] >= '0' && input[i] <= '9') { + continue; + } + /* A-Z uppercase is allowed, continue at next char */ + if (input[i] >= 'A' && input[i] <= 'Z') { + continue; + } + /* a-z lowercase is allowed, continue at next char */ + if (input[i] >= 'a' && input[i] <= 'z') { + continue; + } + + /* + * colon needs to be allowed for IPV6 client + * addresses. Not dangerous in domain names, as not a + * special char. + */ + if (input[i] == ':') { + continue; + } + + /* + * '@' needs to be allowed for in zone data. Not + * dangerous in domain names, as not a special char. + */ + if (input[i] == '@') { + continue; + } + + /* + * if we reach this point we have encountered a + * disallowed char! + */ + return (false); + } + /* everything ok. */ + return (true); +} + +static isc_result_t +create_path_helper(char *out, const char *in, config_data_t *cd) { + char *tmpString; + char *tmpPtr; + int i; + + tmpString = strdup(in); + if (tmpString == NULL) { + return (ISC_R_NOMEMORY); + } + + /* + * don't forget is_safe guarantees '.' will NOT be the + * first/last char + */ + while ((tmpPtr = strrchr(tmpString, '.')) != NULL) { + i = 0; + while (tmpPtr[i + 1] != '\0') { + if (cd->splitcnt < 1) { + strcat(out, (char *)&tmpPtr[i + 1]); + } else { + strncat(out, (char *)&tmpPtr[i + 1], + cd->splitcnt); + } + strncat(out, (char *)&cd->pathsep, 1); + if (cd->splitcnt == 0) { + break; + } + if (strlen((char *)&tmpPtr[i + 1]) <= + (unsigned int)cd->splitcnt) + { + break; + } + i += cd->splitcnt; + } + tmpPtr[0] = '\0'; + } + + /* handle the "first" label properly */ + i = 0; + tmpPtr = tmpString; + while (tmpPtr[i] != '\0') { + if (cd->splitcnt < 1) { + strcat(out, (char *)&tmpPtr[i]); + } else { + strncat(out, (char *)&tmpPtr[i], cd->splitcnt); + } + strncat(out, (char *)&cd->pathsep, 1); + if (cd->splitcnt == 0) { + break; + } + if (strlen((char *)&tmpPtr[i]) <= (unsigned int)cd->splitcnt) { + break; + } + i += cd->splitcnt; + } + + free(tmpString); + return (ISC_R_SUCCESS); +} + +/*% + * Checks to make sure zone and host are safe. If safe, then + * hashes zone and host strings to build a path. If zone / host + * are not safe an error is returned. + */ + +static isc_result_t +create_path(const char *zone, const char *host, const char *client, + config_data_t *cd, char **path) { + char *tmpPath; + int pathsize; + int len; + isc_result_t result; + bool isroot = false; + + /* special case for root zone */ + if (strcmp(zone, ".") == 0) { + isroot = true; + } + + /* if the requested zone is "unsafe", return error */ + if (!isroot && !is_safe(zone)) { + return (ISC_R_FAILURE); + } + + /* if host was passed, verify that it is safe */ + if (host != NULL && !is_safe(host)) { + return (ISC_R_FAILURE); + } + + /* if client was passed, verify that it is safe */ + if (client != NULL && !is_safe(client)) { + return (ISC_R_FAILURE); + } + + /* Determine how much memory the split up string will require */ + if (host != NULL) { + len = strlen(zone) + strlen(host); + } else if (client != NULL) { + len = strlen(zone) + strlen(client); + } else { + len = strlen(zone); + } + + /* + * even though datadir and xfrdir will never be in the same + * string we only waste a few bytes by allocating for both, + * and then we are safe from buffer overruns. + */ + pathsize = len + cd->basedirsize + cd->datadirsize + cd->xfrdirsize + 4; + + /* if we are splitting names, we will need extra space. */ + if (cd->splitcnt > 0) { + pathsize += len / cd->splitcnt; + } + + tmpPath = malloc(pathsize * sizeof(char)); + if (tmpPath == NULL) { + /* write error message */ + cd->log(ISC_LOG_ERROR, "Filesystem driver unable to " + "allocate memory in create_path()."); + result = ISC_R_NOMEMORY; + goto cleanup_mem; + } + + /* + * build path string. + * start out with base directory. + */ + strcpy(tmpPath, cd->basedir); + + /* add zone name - parsed properly */ + if (!isroot) { + result = create_path_helper(tmpPath, zone, cd); + if (result != ISC_R_SUCCESS) { + goto cleanup_mem; + } + } + + /* + * When neither client or host is passed we are building a + * path to see if a zone is supported. We require that a zone + * path have the "data dir" directory contained within it so + * that we know this zone is really supported. Otherwise, + * this zone may not really be supported because we are + * supporting a delagated sub zone. + * + * Example: + * + * We are supporting long.domain.com and using a splitcnt of + * 0. the base dir is "/base-dir/" and the data dir is + * "/.datadir" We want to see if we are authoritative for + * domain.com. Path /base-dir/com/domain/.datadir since + * /base-dir/com/domain/.datadir does not exist, we are not + * authoritative for the domain "domain.com". However we are + * authoritative for the domain "long.domain.com" because the + * path /base-dir/com/domain/long/.datadir does exist! + */ + + /* if client is passed append xfr dir, otherwise append data dir */ + if (client != NULL) { + strcat(tmpPath, cd->xfrdir); + strncat(tmpPath, (char *)&cd->pathsep, 1); + strcat(tmpPath, client); + } else { + strcat(tmpPath, cd->datadir); + } + + /* if host not null, add it. */ + if (host != NULL) { + strncat(tmpPath, (char *)&cd->pathsep, 1); + result = create_path_helper(tmpPath, host, cd); + if (result != ISC_R_SUCCESS) { + goto cleanup_mem; + } + } + + /* return the path we built. */ + *path = tmpPath; + + /* return success */ + result = ISC_R_SUCCESS; + +cleanup_mem: + /* cleanup memory */ + + /* free tmpPath memory */ + if (tmpPath != NULL && result != ISC_R_SUCCESS) { + free(tmpPath); + } + + return (result); +} + +static isc_result_t +process_dir(dir_t *dir, void *passback, config_data_t *cd, dlist_t *dir_list, + unsigned int basedirlen) { + char tmp[DIR_PATHMAX + DIR_NAMEMAX]; + int astPos; + struct stat sb; + isc_result_t result = ISC_R_FAILURE; + char *endp; + char *type; + char *ttlStr; + char *data; + char host[DIR_NAMEMAX]; + char *tmpString; + char *tmpPtr; + int ttl; + int i; + int len; + dir_entry_t *direntry; + bool foundHost; + + tmp[0] = '\0'; /* set 1st byte to '\0' so strcpy works right. */ + host[0] = '\0'; + foundHost = false; + + /* copy base directory name to tmp. */ + strcpy(tmp, dir->dirname); + + /* dir->dirname will always have '*' as the last char. */ + astPos = strlen(dir->dirname) - 1; + + /* if dir_list != NULL, were are performing a zone xfr */ + if (dir_list != NULL) { + /* if splitcnt == 0, determine host from path. */ + if (cd->splitcnt == 0) { + if (strlen(tmp) - 3 > basedirlen) { + tmp[astPos - 1] = '\0'; + tmpString = (char *)&tmp[basedirlen + 1]; + /* handle filesystem's special wildcard "-" */ + if (strcmp(tmpString, "-") == 0) { + strcpy(host, "*"); + } else { + /* + * not special wildcard -- normal name + */ + while ((tmpPtr = strrchr( + tmpString, + cd->pathsep)) != NULL) + { + if ((strlen(host) + + strlen(tmpPtr + 1) + 2) > + DIR_NAMEMAX) + { + continue; + } + strcat(host, tmpPtr + 1); + strcat(host, "."); + tmpPtr[0] = '\0'; + } + if ((strlen(host) + strlen(tmpString) + + 1) <= DIR_NAMEMAX) + { + strcat(host, tmpString); + } + } + + foundHost = true; + /* set tmp again for use later */ + strcpy(tmp, dir->dirname); + } + } else { + /* + * if splitcnt != 0 determine host from + * ".host" directory entry + */ + while (dir_read(dir) == ISC_R_SUCCESS) { + if (strncasecmp(".host", dir->entry.name, 5) == + 0) + { + /* + * handle filesystem's special + * wildcard "-" + */ + if (strcmp((char *)&dir->entry.name[6], + "-") == 0) + { + strcpy(host, "*"); + } else { + strncpy(host, + (char *)&dir->entry + .name[6], + sizeof(host) - 1); + host[255] = '\0'; + } + foundHost = true; + break; + } + } + /* reset dir list for use later */ + dir_reset(dir); + } /* end of else */ + } + + while (dir_read(dir) == ISC_R_SUCCESS) { + cd->log(ISC_LOG_DEBUG(1), + "Filesystem driver Dir name:" + " '%s' Dir entry: '%s'\n", + dir->dirname, dir->entry.name); + + /* skip any entries starting with "." */ + if (dir->entry.name[0] == '.') { + continue; + } + + /* + * get rid of '*', set to NULL. Effectively trims + * string from previous loop to base directory only + * while still leaving memory for concat to be + * performed next. + */ + + tmp[astPos] = '\0'; + + /* add name to base directory name. */ + strcat(tmp, dir->entry.name); + + /* make sure we can stat entry */ + if (stat(tmp, &sb) == 0) { + /* if entry is a directory */ + if ((sb.st_mode & S_IFDIR) != 0) { + /* + * if dir list is NOT NULL, add dir to + * dir list + */ + if (dir_list != NULL) { + direntry = malloc(sizeof(dir_entry_t)); + if (direntry == NULL) { + return (ISC_R_NOMEMORY); + } + strcpy(direntry->dirpath, tmp); + DLZ_LINK_INIT(direntry, link); + DLZ_LIST_APPEND(*dir_list, direntry, + link); + result = ISC_R_SUCCESS; + } + continue; + + /* + * if entry is a file be sure we do + * not add entry to DNS results if we + * are performing a zone xfr and we + * could not find a host entry. + */ + } else if (dir_list != NULL && !foundHost) { + continue; + } + } else { /* if we cannot stat entry, skip it. */ + continue; + } + + type = dir->entry.name; + ttlStr = strchr(type, cd->separator); + if (ttlStr == NULL) { + cd->log(ISC_LOG_ERROR, + "Filesystem driver: " + "%s could not be parsed properly", + tmp); + return (ISC_R_FAILURE); + } + + /* replace separator char with NULL to split string */ + ttlStr[0] = '\0'; + /* start string after NULL of previous string */ + ttlStr = (char *)&ttlStr[1]; + + data = strchr(ttlStr, cd->separator); + if (data == NULL) { + cd->log(ISC_LOG_ERROR, + "Filesystem driver: " + "%s could not be parsed properly", + tmp); + return (ISC_R_FAILURE); + } + + /* replace separator char with NULL to split string */ + data[0] = '\0'; + + /* start string after NULL of previous string */ + data = (char *)&data[1]; + + /* replace all cd->separator chars with a space. */ + len = strlen(data); + + for (i = 0; i < len; i++) { + if (data[i] == cd->separator) { + data[i] = ' '; + } + } + + /* convert text to int, make sure it worked right */ + ttl = strtol(ttlStr, &endp, 10); + if (*endp != '\0' || ttl < 0) { + cd->log(ISC_LOG_ERROR, "Filesystem driver " + "ttl must be a positive number"); + } + + /* pass data back to Bind */ + if (dir_list == NULL) { + result = cd->putrr((dns_sdlzlookup_t *)passback, type, + ttl, data); + } else { + result = cd->putnamedrr((dns_sdlzallnodes_t *)passback, + (char *)host, type, ttl, data); + } + + /* if error, return error right away */ + if (result != ISC_R_SUCCESS) { + return (result); + } + } /* end of while loop */ + + return (result); +} + +/* + * DLZ methods + */ +isc_result_t +dlz_allowzonexfr(void *dbdata, const char *name, const char *client) { + isc_result_t result; + char *path; + struct stat sb; + config_data_t *cd; + path = NULL; + + cd = (config_data_t *)dbdata; + + if (create_path(name, NULL, client, cd, &path) != ISC_R_SUCCESS) { + return (ISC_R_NOTFOUND); + } + + if (stat(path, &sb) != 0) { + result = ISC_R_NOTFOUND; + goto complete_AXFR; + } + + if ((sb.st_mode & S_IFREG) != 0) { + result = ISC_R_SUCCESS; + goto complete_AXFR; + } + + result = ISC_R_NOTFOUND; + +complete_AXFR: + free(path); + return (result); +} + +isc_result_t +dlz_allnodes(const char *zone, void *dbdata, dns_sdlzallnodes_t *allnodes) { + isc_result_t result; + dlist_t *dir_list; + config_data_t *cd = (config_data_t *)dbdata; + char *basepath; + unsigned int basepathlen; + struct stat sb; + dir_t dir; + dir_entry_t *dir_entry; + dir_entry_t *next_de; + + basepath = NULL; + + /* allocate memory for list */ + dir_list = malloc(sizeof(dlist_t)); + if (dir_list == NULL) { + result = ISC_R_NOTFOUND; + goto complete_allnds; + } + + /* initialize list */ + DLZ_LIST_INIT(*dir_list); + + if (create_path(zone, NULL, NULL, cd, &basepath) != ISC_R_SUCCESS) { + result = ISC_R_NOTFOUND; + goto complete_allnds; + } + + /* remove path separator at end of path so stat works properly */ + basepathlen = strlen(basepath); + + if (stat(basepath, &sb) != 0) { + result = ISC_R_NOTFOUND; + goto complete_allnds; + } + + if ((sb.st_mode & S_IFDIR) == 0) { + result = ISC_R_NOTFOUND; + goto complete_allnds; + } + + /* initialize and open directory */ + dir_init(&dir); + result = dir_open(&dir, basepath); + + /* if directory open failed, return error. */ + if (result != ISC_R_SUCCESS) { + cd->log(ISC_LOG_ERROR, + "Unable to open %s directory to read entries.", + basepath); + result = ISC_R_FAILURE; + goto complete_allnds; + } + + /* process the directory */ + result = process_dir(&dir, allnodes, cd, dir_list, basepathlen); + + /* close the directory */ + dir_close(&dir); + + if (result != ISC_R_SUCCESS) { + goto complete_allnds; + } + + /* get first dir entry from list. */ + dir_entry = DLZ_LIST_HEAD(*dir_list); + while (dir_entry != NULL) { + result = dir_open(&dir, dir_entry->dirpath); + /* if directory open failed, return error. */ + if (result != ISC_R_SUCCESS) { + cd->log(ISC_LOG_ERROR, + "Unable to open %s " + "directory to read entries.", + basepath); + result = ISC_R_FAILURE; + goto complete_allnds; + } + + /* process the directory */ + result = process_dir(&dir, allnodes, cd, dir_list, basepathlen); + + /* close the directory */ + dir_close(&dir); + + if (result != ISC_R_SUCCESS) { + goto complete_allnds; + } + + dir_entry = DLZ_LIST_NEXT(dir_entry, link); + } /* end while */ + +complete_allnds: + if (dir_list != NULL) { + /* clean up entries from list. */ + dir_entry = DLZ_LIST_HEAD(*dir_list); + while (dir_entry != NULL) { + next_de = DLZ_LIST_NEXT(dir_entry, link); + free(dir_entry); + dir_entry = next_de; + } /* end while */ + free(dir_list); + } + + if (basepath != NULL) { + free(basepath); + } + + return (result); +} + +#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 */ +{ + isc_result_t result; + config_data_t *cd = (config_data_t *)dbdata; + char *path; + struct stat sb; + path = NULL; + +#if DLZ_DLOPEN_VERSION >= 3 + UNUSED(methods); + UNUSED(clientinfo); +#endif /* if DLZ_DLOPEN_VERSION >= 3 */ + + if (create_path(name, NULL, NULL, cd, &path) != ISC_R_SUCCESS) { + return (ISC_R_NOTFOUND); + } + + cd->log(ISC_LOG_DEBUG(1), + "Filesystem driver Findzone() Checking for path: '%s'\n", path); + + if (stat(path, &sb) != 0) { + result = ISC_R_NOTFOUND; + goto complete_FZ; + } + + if ((sb.st_mode & S_IFDIR) != 0) { + result = ISC_R_SUCCESS; + goto complete_FZ; + } + + result = ISC_R_NOTFOUND; + +complete_FZ: + + free(path); + return (result); +} + +#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 = ISC_R_NOTFOUND; + config_data_t *cd = (config_data_t *)dbdata; + char *path; + struct stat sb; + dir_t dir; + path = NULL; + + UNUSED(lookup); +#if DLZ_DLOPEN_VERSION >= 2 + UNUSED(methods); + UNUSED(clientinfo); +#endif /* if DLZ_DLOPEN_VERSION >= 2 */ + + if (strcmp(name, "*") == 0) { + /* + * handle filesystem's special wildcard "-" + */ + result = create_path(zone, "-", NULL, cd, &path); + } else { + result = create_path(zone, name, NULL, cd, &path); + } + + if (result != ISC_R_SUCCESS) { + return (ISC_R_NOTFOUND); + } + + /* remove path separator at end of path so stat works properly */ + path[strlen(path) - 1] = '\0'; + + cd->log(ISC_LOG_DEBUG(1), + "Filesystem driver lookup() Checking for path: '%s'\n", path); + + if (stat(path, &sb) != 0) { + result = ISC_R_NOTFOUND; + goto complete_lkup; + } + + if ((sb.st_mode & S_IFDIR) == 0) { + result = ISC_R_NOTFOUND; + goto complete_lkup; + } + + /* initialize and open directory */ + dir_init(&dir); + result = dir_open(&dir, path); + + /* if directory open failed, return error. */ + if (result != ISC_R_SUCCESS) { + cd->log(ISC_LOG_ERROR, + "Unable to open %s directory to read entries.", path); + result = ISC_R_FAILURE; + goto complete_lkup; + } + + /* process any records in the directory */ + result = process_dir(&dir, lookup, cd, NULL, 0); + + /* close the directory */ + dir_close(&dir); + +complete_lkup: + + free(path); + return (result); +} + +isc_result_t +dlz_create(const char *dlzname, unsigned int argc, char *argv[], void **dbdata, + ...) { + isc_result_t result = ISC_R_NOMEMORY; + config_data_t *cd; + char *endp; + int len; + char pathsep; + const char *helper_name; + va_list ap; + + UNUSED(dlzname); + + /* allocate memory for our config data and helper functions */ + cd = calloc(1, sizeof(config_data_t)); + if (cd == NULL) { + goto no_mem; + } + + /* zero the memory */ + memset(cd, 0, sizeof(config_data_t)); + + /* Fill in the helper functions */ + va_start(ap, dbdata); + while ((helper_name = va_arg(ap, const char *)) != NULL) { + b9_add_helper(cd, helper_name, va_arg(ap, void *)); + } + va_end(ap); + + /* we require 5 command line args. */ + if (argc != 6) { + cd->log(ISC_LOG_ERROR, "Filesystem driver requires " + "6 command line args."); + result = ISC_R_FAILURE; + goto free_cd; + } + + if (strlen(argv[5]) > 1) { + cd->log(ISC_LOG_ERROR, "Filesystem driver can only " + "accept a single character for " + "separator."); + result = ISC_R_FAILURE; + goto free_cd; + } + + /* verify base dir ends with '/' or '\' */ + len = strlen(argv[1]); + if (argv[1][len - 1] != '\\' && argv[1][len - 1] != '/') { + cd->log(ISC_LOG_ERROR, + "Base dir parameter for filesystem driver " + "should end with %s", + "either '/' or '\\' "); + result = ISC_R_FAILURE; + goto free_cd; + } + + /* determine and save path separator for later */ + if (argv[1][len - 1] == '\\') { + pathsep = '\\'; + } else { + pathsep = '/'; + } + + cd->pathsep = pathsep; + + /* get and store our base directory */ + cd->basedir = strdup(argv[1]); + if (cd->basedir == NULL) { + goto no_mem; + } + cd->basedirsize = strlen(cd->basedir); + + /* get and store our data sub-dir */ + cd->datadir = strdup(argv[2]); + if (cd->datadir == NULL) { + goto no_mem; + } + cd->datadirsize = strlen(cd->datadir); + + /* get and store our zone xfr sub-dir */ + cd->xfrdir = strdup(argv[3]); + if (cd->xfrdir == NULL) { + goto no_mem; + } + cd->xfrdirsize = strlen(cd->xfrdir); + + /* get and store our directory split count */ + cd->splitcnt = strtol(argv[4], &endp, 10); + if (*endp != '\0' || cd->splitcnt < 0) { + cd->log(ISC_LOG_ERROR, "Directory split count must be zero (0) " + "or a positive number"); + } + + /* get and store our separator character */ + cd->separator = *argv[5]; + + /* pass back config data */ + *dbdata = cd; + + /* return success */ + return (ISC_R_SUCCESS); + + /* handle no memory error */ +no_mem: + + /* write error message */ + if (cd != NULL && cd->log != NULL) { + cd->log(ISC_LOG_ERROR, "filesystem_dynamic: Filesystem driver " + "unable to " + "allocate memory for config data."); + } + +free_cd: + /* if we allocated a config data object clean it up */ + if (cd != NULL) { + dlz_destroy(cd); + } + + /* return error */ + return (result); +} + +void +dlz_destroy(void *dbdata) { + config_data_t *cd; + + cd = (config_data_t *)dbdata; + + /* + * free memory for each section of config data that was + * allocated + */ + if (cd->basedir != NULL) { + free(cd->basedir); + } + + if (cd->datadir != NULL) { + free(cd->datadir); + } + + if (cd->xfrdir != NULL) { + free(cd->xfrdir); + } + + /* free config data memory */ + free(cd); +} + +/* + * Return the version of the API + */ +int +dlz_version(unsigned int *flags) { + UNUSED(flags); + return (DLZ_DLOPEN_VERSION); +} + +/* + * Register a helper function from the bind9 dlz_dlopen driver + */ +static void +b9_add_helper(struct config_data *cd, const char *helper_name, void *ptr) { + if (strcmp(helper_name, "log") == 0) { + cd->log = (log_t *)ptr; + } + if (strcmp(helper_name, "putrr") == 0) { + cd->putrr = (dns_sdlz_putrr_t *)ptr; + } + if (strcmp(helper_name, "putnamedrr") == 0) { + cd->putnamedrr = (dns_sdlz_putnamedrr_t *)ptr; + } + if (strcmp(helper_name, "writeable_zone") == 0) { + cd->writeable_zone = (dns_dlz_writeablezone_t *)ptr; + } +} diff --git a/contrib/dlz/modules/include/dlz_dbi.h b/contrib/dlz/modules/include/dlz_dbi.h new file mode 100644 index 0000000..5181abd --- /dev/null +++ b/contrib/dlz/modules/include/dlz_dbi.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * Copyright (C) Stichting NLnet, Netherlands, stichting@nlnet.nl. + * + * SPDX-License-Identifier: MPL-2.0 and ISC + * + * 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 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. + */ + +#include <stdbool.h> + +#include <dlz_list.h> +#include <dlz_minimal.h> +#include <dlz_pthread.h> + +#pragma once + +/* + * Types + */ +#define REQUIRE_CLIENT 0x01 +#define REQUIRE_QUERY 0x02 +#define REQUIRE_RECORD 0x04 +#define REQUIRE_ZONE 0x08 + +typedef struct query_segment query_segment_t; +typedef DLZ_LIST(query_segment_t) query_list_t; +typedef struct dbinstance dbinstance_t; +typedef DLZ_LIST(dbinstance_t) db_list_t; +typedef struct driverinstance driverinstance_t; + +/*% + * a query segment is all the text between our special tokens + * special tokens are %zone%, %record%, %client% + */ +struct query_segment { + void *cmd; + unsigned int strlen; + bool direct; + DLZ_LINK(query_segment_t) link; +}; + +/*% + * a database instance contains everything we need for running + * a query against the database. Using it each separate thread + * can dynamically construct a query and execute it against the + * database. The "instance_lock" and locking code in the driver's + * make sure no two threads try to use the same DBI at a time. + */ +struct dbinstance { + void *dbconn; + query_list_t *allnodes_q; + query_list_t *allowxfr_q; + query_list_t *authority_q; + query_list_t *findzone_q; + query_list_t *lookup_q; + query_list_t *countzone_q; + char *query_buf; + char *zone; + char *record; + char *client; + dlz_mutex_t lock; + DLZ_LINK(dbinstance_t) link; +}; + +/* + * Method declarations + */ + +void +destroy_querylist(query_list_t **querylist); + +isc_result_t +build_querylist(const char *query_str, char **zone, char **record, + char **client, query_list_t **querylist, unsigned int flags, + log_t log); + +char * +build_querystring(query_list_t *querylist); + +isc_result_t +build_dbinstance(const char *allnodes_str, const char *allowxfr_str, + const char *authority_str, const char *findzone_str, + const char *lookup_str, const char *countzone_str, + dbinstance_t **dbi, log_t log); + +void +destroy_dbinstance(dbinstance_t *dbi); + +char * +get_parameter_value(const char *input, const char *key); diff --git a/contrib/dlz/modules/include/dlz_list.h b/contrib/dlz/modules/include/dlz_list.h new file mode 100644 index 0000000..077ae89 --- /dev/null +++ b/contrib/dlz/modules/include/dlz_list.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: ISC + * + * 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. + */ + +#pragma once + +#define DLZ_LIST(type) \ + struct { \ + type *head, *tail; \ + } +#define DLZ_LIST_INIT(list) \ + do { \ + (list).head = NULL; \ + (list).tail = NULL; \ + } while (0) + +#define DLZ_LINK(type) \ + struct { \ + type *prev, *next; \ + } +#define DLZ_LINK_INIT(elt, link) \ + do { \ + (elt)->link.prev = (void *)(-1); \ + (elt)->link.next = (void *)(-1); \ + } while (0) + +#define DLZ_LIST_HEAD(list) ((list).head) +#define DLZ_LIST_TAIL(list) ((list).tail) + +#define DLZ_LIST_APPEND(list, elt, link) \ + do { \ + if ((list).tail != NULL) \ + (list).tail->link.next = (elt); \ + else \ + (list).head = (elt); \ + (elt)->link.prev = (list).tail; \ + (elt)->link.next = NULL; \ + (list).tail = (elt); \ + } while (0) + +#define DLZ_LIST_PREV(elt, link) ((elt)->link.prev) +#define DLZ_LIST_NEXT(elt, link) ((elt)->link.next) + +#define DLZ_LIST_UNLINK(list, elt, link) \ + do { \ + if ((elt)->link.next != NULL) \ + (elt)->link.next->link.prev = (elt)->link.prev; \ + else \ + (list).tail = (elt)->link.prev; \ + if ((elt)->link.prev != NULL) \ + (elt)->link.prev->link.next = (elt)->link.next; \ + else \ + (list).head = (elt)->link.next; \ + (elt)->link.prev = (void *)(-1); \ + (elt)->link.next = (void *)(-1); \ + } while (0) diff --git a/contrib/dlz/modules/include/dlz_minimal.h b/contrib/dlz/modules/include/dlz_minimal.h new file mode 100644 index 0000000..0bc092a --- /dev/null +++ b/contrib/dlz/modules/include/dlz_minimal.h @@ -0,0 +1,327 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: ISC + * + * 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 header provides a minimal set of defines and typedefs needed + * for building an external DLZ module for bind9. When creating a new + * external DLZ driver, please copy this header into your own source + * tree. + */ + +#pragma once + +#include <inttypes.h> +#include <stdbool.h> +#include <stdlib.h> + +#include <arpa/inet.h> +#include <net/if.h> +#include <netinet/in.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/un.h> + +typedef unsigned int isc_result_t; +typedef uint32_t dns_ttl_t; + +/* + * Define DLZ_DLOPEN_VERSION to different values to use older versions + * of the interface + */ +#ifndef DLZ_DLOPEN_VERSION +#define DLZ_DLOPEN_VERSION 3 +#define DLZ_DLOPEN_AGE 0 +#endif /* ifndef DLZ_DLOPEN_VERSION */ + +/* return these in flags from dlz_version() */ +#define DNS_SDLZFLAG_THREADSAFE 0x00000001U +#define DNS_SDLZFLAG_RELATIVEOWNER 0x00000002U +#define DNS_SDLZFLAG_RELATIVERDATA 0x00000004U + +/* result codes */ +#define ISC_R_SUCCESS 0 +#define ISC_R_NOMEMORY 1 +#define ISC_R_NOPERM 6 +#define ISC_R_NOSPACE 19 +#define ISC_R_NOTFOUND 23 +#define ISC_R_FAILURE 25 +#define ISC_R_NOTIMPLEMENTED 27 +#define ISC_R_NOMORE 29 +#define ISC_R_INVALIDFILE 30 +#define ISC_R_UNEXPECTED 34 +#define ISC_R_FILENOTFOUND 38 + +/* log levels */ +#define ISC_LOG_INFO (-1) +#define ISC_LOG_NOTICE (-2) +#define ISC_LOG_WARNING (-3) +#define ISC_LOG_ERROR (-4) +#define ISC_LOG_CRITICAL (-5) +#define ISC_LOG_DEBUG(level) (level) + +/* other useful definitions */ +#define UNUSED(x) (void)(x) +#define DE_CONST(konst, var) \ + do { \ + union { \ + const void *k; \ + void *v; \ + } _u; \ + _u.k = konst; \ + var = _u.v; \ + } while (0) + +#if !defined(__has_attribute) +#define __has_attribute(x) 0 +#endif /* if !defined(__has_attribute) */ + +#if __GNUC__ >= 7 || __has_attribute(fallthrough) +#define FALLTHROUGH __attribute__((fallthrough)) +#else +/* clang-format off */ +#define FALLTHROUGH do {} while (0) /* FALLTHROUGH */ +/* clang-format on */ +#endif + +#ifdef __GNUC__ +#define UNREACHABLE() __builtin_unreachable() +#else +#define UNREACHABLE() abort() +#endif + +/* opaque structures */ +typedef void *dns_sdlzlookup_t; +typedef void *dns_sdlzallnodes_t; +typedef void *dns_view_t; +typedef void *dns_dlzdb_t; + +#if DLZ_DLOPEN_VERSION > 1 +/* + * Method and type definitions needed for retrieval of client info + * from the caller. + */ +typedef struct isc_sockaddr { + union { + struct sockaddr sa; + struct sockaddr_in sin; + struct sockaddr_in6 sin6; + struct sockaddr_un sunix; + } type; + unsigned int length; + void *link; +} isc_sockaddr_t; + +typedef struct isc_netaddr { + unsigned int family; + union { + struct in_addr in; + struct in6_addr in6; + char un[sizeof(((struct sockaddr_un *)0)->sun_path)]; + } type; + uint32_t zone; +} isc_netaddr_t; + +typedef struct dns_ecs { + isc_netaddr_t addr; + uint8_t source; + uint8_t scope; +} dns_ecs_t; + +#define DNS_CLIENTINFO_VERSION 3 +typedef struct dns_clientinfo { + uint16_t version; + void *data; + void *dbversion; + dns_ecs_t ecs; +} dns_clientinfo_t; + +typedef isc_result_t (*dns_clientinfo_sourceip_t)(dns_clientinfo_t *client, + isc_sockaddr_t **addrp); + +typedef isc_result_t (*dns_clientinfo_version_t)(dns_clientinfo_t *client, + void **addrp); + +#define DNS_CLIENTINFOMETHODS_VERSION 2 +#define DNS_CLIENTINFOMETHODS_AGE 1 +typedef struct dns_clientinfomethods { + uint16_t version; + uint16_t age; + dns_clientinfo_sourceip_t sourceip; +} dns_clientinfomethods_t; +#endif /* DLZ_DLOPEN_VERSION > 1 */ + +#define DNS_ECS_FORMATSIZE \ + sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:XXX.XXX.XXX.XXX%SSSSSSSSSS" \ + "/NNN/NNN") + +/* + * Method definitions for callbacks provided by the dlopen driver + */ +typedef void +log_t(int level, const char *fmt, ...); + +typedef isc_result_t +dns_sdlz_putrr_t(dns_sdlzlookup_t *lookup, const char *type, dns_ttl_t ttl, + const char *data); + +typedef isc_result_t +dns_sdlz_putnamedrr_t(dns_sdlzallnodes_t *allnodes, const char *name, + const char *type, dns_ttl_t ttl, const char *data); + +#if DLZ_DLOPEN_VERSION < 3 +typedef isc_result_t +dns_dlz_writeablezone_t(dns_view_t *view, const char *zone_name); +#else /* DLZ_DLOPEN_VERSION >= 3 */ +typedef isc_result_t +dns_dlz_writeablezone_t(dns_view_t *view, dns_dlzdb_t *dlzdb, + const char *zone_name); +#endif /* DLZ_DLOPEN_VERSION */ + +/* + * prototypes for the functions you can include in your module + */ + +/* + * dlz_version() is required for all DLZ external drivers. It should + * return DLZ_DLOPEN_VERSION. 'flags' is updated to indicate capabilities + * of the module. In particular, if the module is thread-safe then it + * sets 'flags' to include DNS_SDLZFLAG_THREADSAFE. Other capability + * flags may be added in the future. + */ +int +dlz_version(unsigned int *flags); + +/* + * dlz_create() is required for all DLZ external drivers. + */ +isc_result_t +dlz_create(const char *dlzname, unsigned int argc, char *argv[], void **dbdata, + ...); + +/* + * dlz_destroy() is optional, and will be called when the driver is + * unloaded if supplied + */ +void +dlz_destroy(void *dbdata); + +/* + * dlz_findzonedb is required for all DLZ external drivers + */ +#if DLZ_DLOPEN_VERSION < 3 +isc_result_t +dlz_findzonedb(void *dbdata, const char *name); +#else /* DLZ_DLOPEN_VERSION >= 3 */ +isc_result_t +dlz_findzonedb(void *dbdata, const char *name, dns_clientinfomethods_t *methods, + dns_clientinfo_t *clientinfo); +#endif /* DLZ_DLOPEN_VERSION */ + +/* + * dlz_lookup is required for all DLZ external drivers + */ +#if DLZ_DLOPEN_VERSION == 1 +isc_result_t +dlz_lookup(const char *zone, const char *name, void *dbdata, + dns_sdlzlookup_t *lookup); +#else /* 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 /* DLZ_DLOPEN_VERSION */ + +/* + * dlz_authority() is optional if dlz_lookup() supplies + * authority information (i.e., SOA, NS) for the dns record + */ +isc_result_t +dlz_authority(const char *zone, void *dbdata, dns_sdlzlookup_t *lookup); + +/* + * dlz_allowzonexfr() is optional, and should be supplied if you want to + * support zone transfers + */ +isc_result_t +dlz_allowzonexfr(void *dbdata, const char *name, const char *client); + +/* + * dlz_allnodes() is optional, but must be supplied if supply a + * dlz_allowzonexfr() function + */ +isc_result_t +dlz_allnodes(const char *zone, void *dbdata, dns_sdlzallnodes_t *allnodes); + +/* + * dlz_newversion() is optional. It should be supplied if you want to + * support dynamic updates. + */ +isc_result_t +dlz_newversion(const char *zone, void *dbdata, void **versionp); + +/* + * dlz_closeversion() is optional, but must be supplied if you supply a + * dlz_newversion() function + */ +void +dlz_closeversion(const char *zone, bool commit, void *dbdata, void **versionp); + +/* + * dlz_configure() is optional, but must be supplied if you want to support + * dynamic updates + */ +#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 */ + +/* + * dlz_ssumatch() is optional, but must be supplied if you want to support + * dynamic updates + */ +bool +dlz_ssumatch(const char *signer, const char *name, const char *tcpaddr, + const char *type, const char *key, uint32_t keydatalen, + uint8_t *keydata, void *dbdata); + +/* + * dlz_addrdataset() is optional, but must be supplied if you want to + * support dynamic updates + */ +isc_result_t +dlz_addrdataset(const char *name, const char *rdatastr, void *dbdata, + void *version); + +/* + * dlz_subrdataset() is optional, but must be supplied if you want to + * support dynamic updates + */ +isc_result_t +dlz_subrdataset(const char *name, const char *rdatastr, void *dbdata, + void *version); + +/* + * dlz_delrdataset() is optional, but must be supplied if you want to + * support dynamic updates + */ +isc_result_t +dlz_delrdataset(const char *name, const char *type, void *dbdata, + void *version); diff --git a/contrib/dlz/modules/include/dlz_pthread.h b/contrib/dlz/modules/include/dlz_pthread.h new file mode 100644 index 0000000..1e0b6f4 --- /dev/null +++ b/contrib/dlz/modules/include/dlz_pthread.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: ISC + * + * 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. + */ + +#pragma once + +#include <pthread.h> +#define dlz_mutex_t pthread_mutex_t +#define dlz_mutex_init pthread_mutex_init +#define dlz_mutex_destroy pthread_mutex_destroy +#define dlz_mutex_lock pthread_mutex_lock +#define dlz_mutex_trylock pthread_mutex_trylock +#define dlz_mutex_unlock pthread_mutex_unlock 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 + 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"; +}; diff --git a/contrib/dlz/modules/perl/Makefile b/contrib/dlz/modules/perl/Makefile new file mode 100644 index 0000000..ec99e00 --- /dev/null +++ b/contrib/dlz/modules/perl/Makefile @@ -0,0 +1,63 @@ +# 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. +# Copyright (C) John Eaglesham +# +# 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. + +# For building the dlz_perl_driver driver we don't use +# the bind9 build structure as the aim is to provide an +# perl_driver that is separable from the bind9 source tree + +CFLAGS += -fPIC -O2 -I../include +FLAGS_PERL ?= perl +LIBNAME = dlz_perl_driver.so + +all: $(LIBNAME) + +dlz_perl_driver.o: dlz_perl_driver.c + $(CC) $(CFLAGS) `${FLAGS_PERL} -MExtUtils::Embed -e ccopts` -c -o dlz_perl_driver.o dlz_perl_driver.c + + +dlz_perl_callback_clientinfo.c: dlz_perl_callback_clientinfo.xs + ${FLAGS_PERL} `${FLAGS_PERL} -MConfig -le 'print $$Config{privlibexp}'`/ExtUtils/xsubpp -prototypes -typemap `${FLAGS_PERL} -MConfig -le 'print $$Config{privlibexp}'`/ExtUtils/typemap dlz_perl_callback_clientinfo.xs > dlz_perl_callback_clientinfo.c + +dlz_perl_callback_clientinfo.o: dlz_perl_callback_clientinfo.c + $(CC) $(CFLAGS) `${FLAGS_PERL} -MExtUtils::Embed -e ccopts` -c -o dlz_perl_callback_clientinfo.o dlz_perl_callback_clientinfo.c + + +dlz_perl_callback.c: dlz_perl_callback.xs + ${FLAGS_PERL} `${FLAGS_PERL} -MConfig -le 'print $$Config{privlibexp}'`/ExtUtils/xsubpp -prototypes -typemap `${FLAGS_PERL} -MConfig -le 'print $$Config{privlibexp}'`/ExtUtils/typemap dlz_perl_callback.xs > dlz_perl_callback.c + +dlz_perl_callback.o: dlz_perl_callback.c + $(CC) $(CFLAGS) `${FLAGS_PERL} -MExtUtils::Embed -e ccopts` -c -o dlz_perl_callback.o dlz_perl_callback.c + + +$(LIBNAME): dlz_perl_driver.o dlz_perl_callback_clientinfo.o dlz_perl_callback.o + $(CC) $(LDFLAGS) -shared -o $(LIBNAME) dlz_perl_driver.o dlz_perl_callback_clientinfo.o dlz_perl_callback.o `${FLAGS_PERL} -MExtUtils::Embed -e ldopts` + +clean: + rm -f dlz_perl_driver.o dlz_perl_driver.so dlz_perl_callback_clientinfo.c dlz_perl_callback_clientinfo.o dlz_perl_callback.c dlz_perl_callback.o + +install: dlz_perl_driver.so + mkdir -p $(DESTDIR)$(libdir) + install dlz_perl_driver.so $(DESTDIR)$(libdir) diff --git a/contrib/dlz/modules/perl/README b/contrib/dlz/modules/perl/README new file mode 100644 index 0000000..324054a --- /dev/null +++ b/contrib/dlz/modules/perl/README @@ -0,0 +1,38 @@ +<!-- +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. +Copyright (C) John Eaglesham + +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 Perl module (bind-dlz-tools) + +Written by John Eaglesham <dns@8192.net> + +A dynamically loadable zone (DLZ) plugin embedding a Perl +interpreter in BIND, allowing Perl scripts to be written to +integrate with BIND and serve DNS data. + +More information/updates at http://bind-dlz-tools.sourceforge.net/ diff --git a/contrib/dlz/modules/perl/dlz_perl_callback.xs b/contrib/dlz/modules/perl/dlz_perl_callback.xs new file mode 100644 index 0000000..0b9f504 --- /dev/null +++ b/contrib/dlz/modules/perl/dlz_perl_callback.xs @@ -0,0 +1,88 @@ +/* + * 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) Stichting NLnet, Netherlands, stichting@nlnet.nl. + * Copyright (C) John Eaglesham + * + * 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. + */ + +#include "EXTERN.h" +#include "perl.h" +#include "XSUB.h" +#include "dlz_perl_driver.h" + +#include <dlz_minimal.h> + +/* And some XS code. */ +MODULE = DLZ_Perl PACKAGE = DLZ_Perl + +int +LOG_INFO() + CODE: + RETVAL = ISC_LOG_INFO; + OUTPUT: + RETVAL + +int +LOG_NOTICE() + CODE: + RETVAL = ISC_LOG_NOTICE; + OUTPUT: + RETVAL + +int +LOG_WARNING() + CODE: + RETVAL = ISC_LOG_WARNING; + OUTPUT: + RETVAL + +int +LOG_ERROR() + CODE: + RETVAL = ISC_LOG_ERROR; + OUTPUT: + RETVAL + +int +LOG_CRITICAL() + CODE: + RETVAL = ISC_LOG_CRITICAL; + OUTPUT: + RETVAL + + +void +log(opaque, level, msg) + IV opaque + int level + char *msg + + PREINIT: + log_t *log = (log_t *) opaque; + + CODE: + log( level, msg ); + diff --git a/contrib/dlz/modules/perl/dlz_perl_callback_clientinfo.xs b/contrib/dlz/modules/perl/dlz_perl_callback_clientinfo.xs new file mode 100644 index 0000000..22aec66 --- /dev/null +++ b/contrib/dlz/modules/perl/dlz_perl_callback_clientinfo.xs @@ -0,0 +1,94 @@ +/* + * 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) Stichting NLnet, Netherlands, stichting@nlnet.nl. + * Copyright (C) John Eaglesham + * + * 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. + */ + +#define ADDR_BUF_LEN INET6_ADDRSTRLEN + +#include "EXTERN.h" +#include "perl.h" +#include "XSUB.h" +#include "dlz_perl_driver.h" + +#include <dlz_minimal.h> + +/* And some XS code. */ +MODULE = DLZ_Perl::clientinfo PACKAGE = DLZ_Perl::clientinfo + +PROTOTYPES: DISABLE + +void +sourceip(opaque) + SV *opaque + + PREINIT: + const char *ret; + char addr_buf[ADDR_BUF_LEN]; + int port; + isc_sockaddr_t *src; + dlz_perl_clientinfo_opaque *ci; + I32 wantarray = GIMME_V; + + PPCODE: + if (!SvTRUE(opaque) || !SvIOK(opaque)) XSRETURN_EMPTY; + + /* + * Safe, because Perl guarantees that an IV (the type we + * pass into DLZ functions who pass it here) is able to + * hold a pointer. + */ + ci = (dlz_perl_clientinfo_opaque *) SvIV(opaque); + if (wantarray == G_VOID || ci->methods == NULL || + ci->methods->version - ci->methods->age < + DNS_CLIENTINFOMETHODS_VERSION) + XSRETURN_EMPTY; + + ci->methods->sourceip(ci->clientinfo, &src); + + switch (src->type.sa.sa_family) { + case AF_INET: + port = ntohs(src->type.sin.sin_port); + ret = inet_ntop(AF_INET, + &src->type.sin.sin_addr, + addr_buf, ADDR_BUF_LEN); + break; + case AF_INET6: + port = ntohs(src->type.sin6.sin6_port); + ret = inet_ntop(AF_INET6, + &src->type.sin6.sin6_addr, + addr_buf, ADDR_BUF_LEN); + break; + default: + ret = NULL; + } + + if (ret == NULL) XSRETURN_EMPTY; + + XPUSHs(sv_2mortal(newSVpv(addr_buf, strlen(addr_buf)))); + if (wantarray == G_ARRAY) XPUSHs(sv_2mortal(newSViv(port))); + diff --git a/contrib/dlz/modules/perl/dlz_perl_driver.c b/contrib/dlz/modules/perl/dlz_perl_driver.c new file mode 100644 index 0000000..b99b296 --- /dev/null +++ b/contrib/dlz/modules/perl/dlz_perl_driver.c @@ -0,0 +1,715 @@ +/* + * 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) Stichting NLnet, Netherlands, stichting@nlnet.nl. + * Copyright (C) John Eaglesham + * + * 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. + */ + +#include "dlz_perl_driver.h" +#include <EXTERN.h> +#include <perl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <dlz_minimal.h> + +#define BUF_LEN 64 /* Should be big enough, right? hah */ + +/* Enable debug logging? */ +#if 0 +#define carp(...) cd->log(ISC_LOG_INFO, __VA_ARGS__); +#else /* if 0 */ +#define carp(...) +#endif /* if 0 */ + +#ifndef MULTIPLICITY +/* This is a pretty terrible work-around for handling HUP/rndc reconfig, but + * the way BIND/DLZ handles reloads causes it to create a second back end + * before removing the first. In the case of a single global interpreter, + * serious problems arise. We can hack around this, but it's much better to do + * it properly and link against a perl compiled with multiplicity. */ +static PerlInterpreter *global_perl = NULL; +static int global_perl_dont_free = 0; +#endif /* ifndef MULTIPLICITY */ + +typedef struct config_data { + PerlInterpreter *perl; + char *perl_source; + SV *perl_class; + + /* Functions given to us by bind9 */ + log_t *log; + dns_sdlz_putrr_t *putrr; + dns_sdlz_putnamedrr_t *putnamedrr; + dns_dlz_writeablezone_t *writeable_zone; +} config_data_t; + +/* Note, this code generates warnings due to lost type qualifiers. This code + * is (almost) verbatim from perlembed, and is known to work correctly despite + * the warnings. + */ +EXTERN_C void xs_init(pTHX); +EXTERN_C void +boot_DynaLoader(pTHX_ CV *cv); +EXTERN_C void +boot_DLZ_Perl__clientinfo(pTHX_ CV *cv); +EXTERN_C void +boot_DLZ_Perl(pTHX_ CV *cv); +EXTERN_C void +xs_init(pTHX) { + const char *file = __FILE__; + dXSUB_SYS; + + /* DynaLoader is a special case */ + newXS("DynaLoader::boot_DynaLoader", boot_DynaLoader, file); + newXS("DLZ_Perl::clientinfo::bootstrap", boot_DLZ_Perl__clientinfo, + file); + newXS("DLZ_Perl::bootstrap", boot_DLZ_Perl, file); +} + +/* + * methods + */ + +/* + * remember a helper function, from the bind9 dlz_dlopen driver + */ +static void +b9_add_helper(config_data_t *state, const char *helper_name, void *ptr) { + if (strcmp(helper_name, "log") == 0) { + state->log = ptr; + } + if (strcmp(helper_name, "putrr") == 0) { + state->putrr = ptr; + } + if (strcmp(helper_name, "putnamedrr") == 0) { + state->putnamedrr = ptr; + } + if (strcmp(helper_name, "writeable_zone") == 0) { + state->writeable_zone = ptr; + } +} + +int +dlz_version(unsigned int *flags) { + UNUSED(flags); + return (DLZ_DLOPEN_VERSION); +} + +isc_result_t +dlz_allnodes(const char *zone, void *dbdata, dns_sdlzallnodes_t *allnodes) { + config_data_t *cd = (config_data_t *)dbdata; + isc_result_t retval; + int rrcount, r; + SV *record_ref; + SV **rr_name; + SV **rr_type; + SV **rr_ttl; + SV **rr_data; +#ifdef MULTIPLICITY + PerlInterpreter *my_perl = cd->perl; +#endif /* ifdef MULTIPLICITY */ + dSP; + + PERL_SET_CONTEXT(cd->perl); + ENTER; + SAVETMPS; + + PUSHMARK(SP); + XPUSHs(cd->perl_class); + XPUSHs(sv_2mortal(newSVpv(zone, 0))); + PUTBACK; + + carp("DLZ Perl: Calling allnodes for zone %s", zone); + rrcount = call_method("allnodes", G_ARRAY | G_EVAL); + carp("DLZ Perl: Call to allnodes returned rrcount of %i", rrcount); + + SPAGAIN; + + if (SvTRUE(ERRSV)) { + (void)POPs; + cd->log(ISC_LOG_ERROR, + "DLZ Perl: allnodes for zone %s died in eval: %s", zone, + SvPV_nolen(ERRSV)); + retval = ISC_R_FAILURE; + goto CLEAN_UP_AND_RETURN; + } + + if (!rrcount) { + retval = ISC_R_NOTFOUND; + goto CLEAN_UP_AND_RETURN; + } + + retval = ISC_R_SUCCESS; + r = 0; + while (r++ < rrcount) { + record_ref = POPs; + if ((!SvROK(record_ref)) || + (SvTYPE(SvRV(record_ref)) != SVt_PVAV)) + { + cd->log(ISC_LOG_ERROR, + "DLZ Perl: allnodes for zone %s " + "returned an invalid value " + "(expected array of arrayrefs)", + zone); + retval = ISC_R_FAILURE; + break; + } + + record_ref = SvRV(record_ref); + + rr_name = av_fetch((AV *)record_ref, 0, 0); + rr_type = av_fetch((AV *)record_ref, 1, 0); + rr_ttl = av_fetch((AV *)record_ref, 2, 0); + rr_data = av_fetch((AV *)record_ref, 3, 0); + + if (rr_name == NULL || rr_type == NULL || rr_ttl == NULL || + rr_data == NULL) + { + cd->log(ISC_LOG_ERROR, + "DLZ Perl: allnodes for zone %s " + "returned an array that was missing data", + zone); + retval = ISC_R_FAILURE; + break; + } + + carp("DLZ Perl: Got record %s/%s = %s", SvPV_nolen(*rr_name), + SvPV_nolen(*rr_type), SvPV_nolen(*rr_data)); + retval = cd->putnamedrr(allnodes, SvPV_nolen(*rr_name), + SvPV_nolen(*rr_type), SvIV(*rr_ttl), + SvPV_nolen(*rr_data)); + if (retval != ISC_R_SUCCESS) { + cd->log(ISC_LOG_ERROR, + "DLZ Perl: putnamedrr in allnodes " + "for zone %s failed with code %i " + "(did lookup return invalid record data?)", + zone, retval); + break; + } + } + +CLEAN_UP_AND_RETURN: + PUTBACK; + FREETMPS; + LEAVE; + + carp("DLZ Perl: Returning from allnodes, r = %i, retval = %i", r, + retval); + + return (retval); +} + +isc_result_t +dlz_allowzonexfr(void *dbdata, const char *name, const char *client) { + config_data_t *cd = (config_data_t *)dbdata; + int r; + isc_result_t retval; +#ifdef MULTIPLICITY + PerlInterpreter *my_perl = cd->perl; +#endif /* ifdef MULTIPLICITY */ + dSP; + + PERL_SET_CONTEXT(cd->perl); + ENTER; + SAVETMPS; + + PUSHMARK(SP); + XPUSHs(cd->perl_class); + XPUSHs(sv_2mortal(newSVpv(name, 0))); + XPUSHs(sv_2mortal(newSVpv(client, 0))); + PUTBACK; + + r = call_method("allowzonexfr", G_SCALAR | G_EVAL); + SPAGAIN; + + if (SvTRUE(ERRSV)) { + /* + * On error there's an undef at the top of the stack. Pop + * it away so we don't leave junk on the stack for the next + * caller. + */ + (void)POPs; + cd->log(ISC_LOG_ERROR, + "DLZ Perl: allowzonexfr died in eval: %s", + SvPV_nolen(ERRSV)); + retval = ISC_R_FAILURE; + } else if (r == 0) { + /* Client returned nothing -- zone not found. */ + retval = ISC_R_NOTFOUND; + } else if (r > 1) { + /* Once again, clean out the stack when possible. */ + while (r--) { + POPi; + } + cd->log(ISC_LOG_ERROR, "DLZ Perl: allowzonexfr returned too " + "many parameters!"); + retval = ISC_R_FAILURE; + } else { + /* + * Client returned true/false -- we're authoritative for + * the zone. + */ + r = POPi; + if (r) { + retval = ISC_R_SUCCESS; + } else { + retval = ISC_R_NOPERM; + } + } + + PUTBACK; + FREETMPS; + LEAVE; + return (retval); +} + +#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 */ +{ + config_data_t *cd = (config_data_t *)dbdata; + int r; + isc_result_t retval; +#ifdef MULTIPLICITY + PerlInterpreter *my_perl = cd->perl; +#endif /* ifdef MULTIPLICITY */ + +#if DLZ_DLOPEN_VERSION >= 3 + UNUSED(methods); + UNUSED(clientinfo); +#endif /* if DLZ_DLOPEN_VERSION >= 3 */ + + dSP; + carp("DLZ Perl: findzone looking for '%s'", name); + + PERL_SET_CONTEXT(cd->perl); + ENTER; + SAVETMPS; + + PUSHMARK(SP); + XPUSHs(cd->perl_class); + XPUSHs(sv_2mortal(newSVpv(name, 0))); + PUTBACK; + + r = call_method("findzone", G_SCALAR | G_EVAL); + SPAGAIN; + + if (SvTRUE(ERRSV)) { + /* + * On error there's an undef at the top of the stack. Pop + * it away so we don't leave junk on the stack for the next + * caller. + */ + (void)POPs; + cd->log(ISC_LOG_ERROR, "DLZ Perl: findzone died in eval: %s", + SvPV_nolen(ERRSV)); + retval = ISC_R_FAILURE; + } else if (r == 0) { + retval = ISC_R_FAILURE; + } else if (r > 1) { + /* Once again, clean out the stack when possible. */ + while (r--) { + POPi; + } + cd->log(ISC_LOG_ERROR, "DLZ Perl: findzone returned too many " + "parameters!"); + retval = ISC_R_FAILURE; + } else { + r = POPi; + if (r) { + retval = ISC_R_SUCCESS; + } else { + retval = ISC_R_NOTFOUND; + } + } + + PUTBACK; + FREETMPS; + LEAVE; + return (retval); +} + +#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 retval; + config_data_t *cd = (config_data_t *)dbdata; + int rrcount, r; + dlz_perl_clientinfo_opaque opaque; + SV *record_ref; + SV **rr_type; + SV **rr_ttl; + SV **rr_data; +#ifdef MULTIPLICITY + PerlInterpreter *my_perl = cd->perl; +#endif /* ifdef MULTIPLICITY */ + +#if DLZ_DLOPEN_VERSION >= 2 + UNUSED(methods); + UNUSED(clientinfo); +#endif /* if DLZ_DLOPEN_VERSION >= 2 */ + + dSP; + PERL_SET_CONTEXT(cd->perl); + ENTER; + SAVETMPS; + + opaque.methods = methods; + opaque.clientinfo = clientinfo; + + PUSHMARK(SP); + XPUSHs(cd->perl_class); + XPUSHs(sv_2mortal(newSVpv(name, 0))); + XPUSHs(sv_2mortal(newSVpv(zone, 0))); + XPUSHs(sv_2mortal(newSViv((IV)&opaque))); + PUTBACK; + + carp("DLZ Perl: Searching for name %s in zone %s", name, zone); + rrcount = call_method("lookup", G_ARRAY | G_EVAL); + carp("DLZ Perl: Call to lookup returned %i", rrcount); + + SPAGAIN; + + if (SvTRUE(ERRSV)) { + (void)POPs; + cd->log(ISC_LOG_ERROR, "DLZ Perl: lookup died in eval: %s", + SvPV_nolen(ERRSV)); + retval = ISC_R_FAILURE; + goto CLEAN_UP_AND_RETURN; + } + + if (!rrcount) { + retval = ISC_R_NOTFOUND; + goto CLEAN_UP_AND_RETURN; + } + + retval = ISC_R_SUCCESS; + r = 0; + while (r++ < rrcount) { + record_ref = POPs; + if ((!SvROK(record_ref)) || + (SvTYPE(SvRV(record_ref)) != SVt_PVAV)) + { + cd->log(ISC_LOG_ERROR, "DLZ Perl: lookup returned an " + "invalid value (expected array " + "of arrayrefs)!"); + retval = ISC_R_FAILURE; + break; + } + + record_ref = SvRV(record_ref); + + rr_type = av_fetch((AV *)record_ref, 0, 0); + rr_ttl = av_fetch((AV *)record_ref, 1, 0); + rr_data = av_fetch((AV *)record_ref, 2, 0); + + if (rr_type == NULL || rr_ttl == NULL || rr_data == NULL) { + cd->log(ISC_LOG_ERROR, + "DLZ Perl: lookup for record %s in " + "zone %s returned an array that was " + "missing data", + name, zone); + retval = ISC_R_FAILURE; + break; + } + + carp("DLZ Perl: Got record %s = %s", SvPV_nolen(*rr_type), + SvPV_nolen(*rr_data)); + retval = cd->putrr(lookup, SvPV_nolen(*rr_type), SvIV(*rr_ttl), + SvPV_nolen(*rr_data)); + + if (retval != ISC_R_SUCCESS) { + cd->log(ISC_LOG_ERROR, + "DLZ Perl: putrr for lookup of %s in " + "zone %s failed with code %i " + "(did lookup return invalid record data?)", + name, zone, retval); + break; + } + } + +CLEAN_UP_AND_RETURN: + PUTBACK; + FREETMPS; + LEAVE; + + carp("DLZ Perl: Returning from lookup, r = %i, retval = %i", r, retval); + + return (retval); +} + +static const char * +#ifdef MULTIPLICITY +missing_perl_method(const char *perl_class_name, PerlInterpreter *my_perl) +#else /* ifdef MULTIPLICITY */ +missing_perl_method(const char *perl_class_name) +#endif /* ifdef MULTIPLICITY */ +{ + char full_name[BUF_LEN]; + const char *methods[] = { "new", "findzone", "lookup", NULL }; + int i = 0; + + while (methods[i] != NULL) { + snprintf(full_name, BUF_LEN, "%s::%s", perl_class_name, + methods[i]); + + if (get_cv(full_name, 0) == NULL) { + return (methods[i]); + } + i++; + } + + return (NULL); +} + +isc_result_t +dlz_create(const char *dlzname, unsigned int argc, char *argv[], void **dbdata, + ...) { + config_data_t *cd; + char *perlrun[] = { (char *)"", NULL, (char *)"dlz perl", NULL }; + char *perl_class_name; + int r; + va_list ap; + const char *helper_name; + const char *missing_method_name; + char *call_argv_args = NULL; +#ifdef MULTIPLICITY + PerlInterpreter *my_perl; +#endif /* ifdef MULTIPLICITY */ + + cd = malloc(sizeof(config_data_t)); + if (cd == NULL) { + return (ISC_R_NOMEMORY); + } + + memset(cd, 0, sizeof(config_data_t)); + + /* fill in the helper functions */ + va_start(ap, dbdata); + while ((helper_name = va_arg(ap, const char *)) != NULL) { + b9_add_helper(cd, helper_name, va_arg(ap, void *)); + } + va_end(ap); + + if (argc < 2) { + cd->log(ISC_LOG_ERROR, + "DLZ Perl '%s': Missing script argument.", dlzname); + free(cd); + return (ISC_R_FAILURE); + } + + if (argc < 3) { + cd->log(ISC_LOG_ERROR, + "DLZ Perl '%s': Missing class name argument.", dlzname); + free(cd); + return (ISC_R_FAILURE); + } + perl_class_name = argv[2]; + + cd->log(ISC_LOG_INFO, "DLZ Perl '%s': Loading '%s' from location '%s'", + dlzname, perl_class_name, argv[1], argc); + +#ifndef MULTIPLICITY + if (global_perl) { + /* + * PERL_SET_CONTEXT not needed here as we're guaranteed to + * have an implicit context thanks to an undefined + * MULTIPLICITY. + */ + PL_perl_destruct_level = 1; + perl_destruct(global_perl); + perl_free(global_perl); + global_perl = NULL; + global_perl_dont_free = 1; + } +#endif /* ifndef MULTIPLICITY */ + + cd->perl = perl_alloc(); + if (cd->perl == NULL) { + free(cd); + return (ISC_R_FAILURE); + } +#ifdef MULTIPLICITY + my_perl = cd->perl; +#endif /* ifdef MULTIPLICITY */ + PERL_SET_CONTEXT(cd->perl); + + /* + * We will re-create the interpreter during an rndc reconfig, so we + * must set this variable per perlembed in order to insure we can + * clean up Perl at a later time. + */ + PL_perl_destruct_level = 1; + perl_construct(cd->perl); + PL_exit_flags |= PERL_EXIT_DESTRUCT_END; + /* Prevent crashes from clients writing to $0 */ + PL_origalen = 1; + + cd->perl_source = strdup(argv[1]); + if (cd->perl_source == NULL) { + free(cd); + return (ISC_R_NOMEMORY); + } + + perlrun[1] = cd->perl_source; + if (perl_parse(cd->perl, xs_init, 3, perlrun, (char **)NULL)) { + cd->log(ISC_LOG_ERROR, + "DLZ Perl '%s': Failed to parse Perl script, aborting", + dlzname); + goto CLEAN_UP_PERL_AND_FAIL; + } + + /* Let Perl know about our callbacks. */ + call_argv("DLZ_Perl::clientinfo::bootstrap", G_DISCARD | G_NOARGS, + &call_argv_args); + call_argv("DLZ_Perl::bootstrap", G_DISCARD | G_NOARGS, &call_argv_args); + + /* + * Run the script. We don't really need to do this since we have + * the init callback, but there's not really a downside either. + */ + if (perl_run(cd->perl)) { + cd->log(ISC_LOG_ERROR, + "DLZ Perl '%s': Script exited with an error, aborting", + dlzname); + goto CLEAN_UP_PERL_AND_FAIL; + } + +#ifdef MULTIPLICITY + if ((missing_method_name = missing_perl_method(perl_class_name, + my_perl))) +#else /* ifdef MULTIPLICITY */ + if ((missing_method_name = missing_perl_method(perl_class_name))) +#endif /* ifdef MULTIPLICITY */ + { + cd->log(ISC_LOG_ERROR, + "DLZ Perl '%s': Missing required function '%s', " + "aborting", + dlzname, missing_method_name); + goto CLEAN_UP_PERL_AND_FAIL; + } + + dSP; + ENTER; + SAVETMPS; + + PUSHMARK(SP); + XPUSHs(sv_2mortal(newSVpv(perl_class_name, 0))); + + /* Build flattened hash of config info. */ + XPUSHs(sv_2mortal(newSVpv("log_context", 0))); + XPUSHs(sv_2mortal(newSViv((IV)cd->log))); + + /* Argument to pass to new? */ + if (argc == 4) { + XPUSHs(sv_2mortal(newSVpv("argv", 0))); + XPUSHs(sv_2mortal(newSVpv(argv[3], 0))); + } + + PUTBACK; + + r = call_method("new", G_EVAL | G_SCALAR); + + SPAGAIN; + + if (r) { + cd->perl_class = SvREFCNT_inc(POPs); + } + + PUTBACK; + FREETMPS; + LEAVE; + + if (SvTRUE(ERRSV)) { + (void)POPs; + cd->log(ISC_LOG_ERROR, "DLZ Perl '%s': new died in eval: %s", + dlzname, SvPV_nolen(ERRSV)); + goto CLEAN_UP_PERL_AND_FAIL; + } + + if (!r || !sv_isobject(cd->perl_class)) { + cd->log(ISC_LOG_ERROR, + "DLZ Perl '%s': new failed to return a blessed object", + dlzname); + goto CLEAN_UP_PERL_AND_FAIL; + } + + *dbdata = cd; + +#ifndef MULTIPLICITY + global_perl = cd->perl; +#endif /* ifndef MULTIPLICITY */ + return (ISC_R_SUCCESS); + +CLEAN_UP_PERL_AND_FAIL: + PL_perl_destruct_level = 1; + perl_destruct(cd->perl); + perl_free(cd->perl); + free(cd->perl_source); + free(cd); + return (ISC_R_FAILURE); +} + +void +dlz_destroy(void *dbdata) { + config_data_t *cd = (config_data_t *)dbdata; +#ifdef MULTIPLICITY + PerlInterpreter *my_perl = cd->perl; +#endif /* ifdef MULTIPLICITY */ + + cd->log(ISC_LOG_INFO, "DLZ Perl: Unloading driver."); + +#ifndef MULTIPLICITY + if (!global_perl_dont_free) { +#endif /* ifndef MULTIPLICITY */ + PERL_SET_CONTEXT(cd->perl); + PL_perl_destruct_level = 1; + perl_destruct(cd->perl); + perl_free(cd->perl); +#ifndef MULTIPLICITY + global_perl_dont_free = 0; + global_perl = NULL; + } +#endif /* ifndef MULTIPLICITY */ + + free(cd->perl_source); + free(cd); +} diff --git a/contrib/dlz/modules/perl/dlz_perl_driver.h b/contrib/dlz/modules/perl/dlz_perl_driver.h new file mode 100644 index 0000000..71e13d5 --- /dev/null +++ b/contrib/dlz/modules/perl/dlz_perl_driver.h @@ -0,0 +1,39 @@ +/* + * 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) Stichting NLnet, Netherlands, stichting@nlnet.nl. + * Copyright (C) John Eaglesham + * + * 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. + */ + +#pragma once + +#include <dlz_minimal.h> + +/* This is the only part that differs from dlz_minimal.h. */ +typedef struct dlz_perl_clientinfo_opaque { + dns_clientinfomethods_t *methods; + dns_clientinfo_t *clientinfo; +} dlz_perl_clientinfo_opaque; diff --git a/contrib/dlz/modules/perl/testing/dlz_perl_example.pm b/contrib/dlz/modules/perl/testing/dlz_perl_example.pm new file mode 100644 index 0000000..eafa87c --- /dev/null +++ b/contrib/dlz/modules/perl/testing/dlz_perl_example.pm @@ -0,0 +1,185 @@ +# 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. +# Copyright (C) John Eaglesham +# +# 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. +package dlz_perl_example; + +use warnings; +use strict; + +use Data::Dumper; +$Data::Dumper::Sortkeys = 1; + +# Constructor. Everything after the class name can be folded into a hash of +# various options and settings. Right now only log_context and argv are +# available. +sub new { + my ( $class, %config ) = @_; + my $self = {}; + bless $self, $class; + + $self->{log} = sub { + my ( $level, $msg ) = @_; + DLZ_Perl::log( $config{log_context}, $level, $msg ); + }; + + if ( $config{argv} ) { warn "Got argv: $config{argv}\n"; } + + $self->{zones} = { + 'example.com' => { + '@' => [ + { + type => 'SOA', + ttl => 86400, + data => + 'ns1.example.com. hostmaster.example.com. 12345 172800 900 1209600 3600', + } + ], + perlrr => [ + { + type => 'A', + ttl => 444, + data => '1.1.1.1', + }, + { + type => 'A', + ttl => 444, + data => '1.1.1.2', + } + ], + perltime => [ + { + code => sub { + return ['TXT', '1', time()]; + }, + }, + ], + sourceip => [ + { + code => sub { + my ( $opaque ) = @_; + # Passing anything other than the proper opaque value, + # 0, or undef to this function will cause a crash (at + # best!). + my ( $addr, $port ) = + DLZ_Perl::clientinfo::sourceip( $opaque ); + if ( !$addr ) { $addr = $port = 'unknown'; } + return ['TXT', '1', $addr], ['TXT', '1', $port]; + }, + }, + ], + }, + }; + + $self->{log}->( + DLZ_Perl::LOG_INFO(), + 'DLZ Perl Script: Called init. Loaded zone data: ' + . Dumper( $self->{zones} ) + ); + return $self; +} + +# Do we have data for this zone? Expects a simple true or false return value. +sub findzone { + my ( $self, $zone ) = @_; + $self->{log}->( + DLZ_Perl::LOG_INFO(), + "DLZ Perl Script: Called findzone, looking for zone $zone" + ); + + return exists $self->{zones}->{$zone}; +} + +# Return the data for a given record in a given zone. The final parameter is +# an opaque value that can be passed to DLZ_Perl::clientinfo::sourceip to +# retrieve the client source IP and port. Expected return value is an array +# of array refs, with each array ref representing one record and containing +# the type, ttl, and data in that order. Data is as it appears in a zone file. +sub lookup { + my ( $self, $name, $zone, $client_info ) = @_; + $self->{log}->( + DLZ_Perl::LOG_INFO(), + "DLZ Perl Script: Called lookup, looking for record $name in zone $zone" + ); + return unless $self->{zones}->{$zone}->{$name}; + + my @results; + foreach my $rr ( @{ $self->{zones}->{$zone}->{$name} } ) { + if ( $rr->{'code'} ) { + my @r = $rr->{'code'}->( $client_info ); + if ( @r ) { + push @results, @r; + } + } else { + push @results, [$rr->{'type'}, $rr->{'ttl'}, $rr->{'data'}]; + } + } + + return @results; +} + +# Will we allow zone transfer for this client? Expects a simple true or false +# return value. +sub allowzonexfr { + my ( $self, $zone, $client ) = @_; + $self->{log}->( + DLZ_Perl::LOG_INFO(), + "DLZ Perl Script: Called allowzonexfr, looking for zone $zone for " . + "client $client" + ); + if ( $client eq '127.0.0.1' ) { return 1; } + return 0; +} + +# Note the return AoA for this method differs from lookup in that it must +# return the name of the record as well as the other data. +sub allnodes { + my ( $self, $zone ) = @_; + my @results; + $self->{log}->( + DLZ_Perl::LOG_INFO(), + "DLZ Perl Script: Called allnodes, looking for zone $zone" + ); + + foreach my $name ( keys %{ $self->{zones}->{$zone} } ) { + foreach my $rr ( @{ $self->{zones}->{$zone}->{$name} } ) { + if ( $rr->{'code'} ) { + my @r = $rr->{'code'}->(); + # The code returns an array of array refs without the name. + # This makes things easy for lookup but hard here. We must + # iterate over each array ref and inject the name into it. + foreach my $a ( @r ) { + unshift @{$a}, $name; + } + push @results, @r; + } else { + push @results, + [$name, $rr->{'type'}, $rr->{'ttl'}, $rr->{'data'}]; + } + } + } + return @results; +} + +1; diff --git a/contrib/dlz/modules/perl/testing/named.conf b/contrib/dlz/modules/perl/testing/named.conf new file mode 100644 index 0000000..293d655 --- /dev/null +++ b/contrib/dlz/modules/perl/testing/named.conf @@ -0,0 +1,26 @@ +/* + * 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. + */ + +options { + port 5300; + pid-file "named.pid"; + session-keyfile "session.key"; + listen-on { 127.0.0.1; }; + listen-on-v6 { none; }; + recursion no; + notify no; +}; + +dlz "perl zone" { + database "dlopen ../dlz_perl_driver.so dlz_perl_example.pm dlz_perl_example"; +}; diff --git a/contrib/dlz/modules/sqlite3/Makefile b/contrib/dlz/modules/sqlite3/Makefile new file mode 100644 index 0000000..1532921 --- /dev/null +++ b/contrib/dlz/modules/sqlite3/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 +SQLITE3_LIBS=-lsqlite3 + +all: dlz_sqlite3_dynamic.so + +dlz_dbi.o: ../common/dlz_dbi.c + $(CC) $(CFLAGS) -c ../common/dlz_dbi.c + +dlz_sqlite3_dynamic.so: dlz_sqlite3_dynamic.c dlz_dbi.o + $(CC) $(CFLAGS) -shared -o dlz_sqlite3_dynamic.so \ + dlz_sqlite3_dynamic.c dlz_dbi.o $(SQLITE3_LIBS) + +clean: + rm -f dlz_sqlite3_dynamic.so *.o + +install: dlz_sqlite3_dynamic.so + mkdir -p $(DESTDIR)$(libdir) + install dlz_sqlite3_dynamic.so $(DESTDIR)$(libdir) diff --git a/contrib/dlz/modules/sqlite3/dlz_sqlite3_dynamic.c b/contrib/dlz/modules/sqlite3/dlz_sqlite3_dynamic.c new file mode 100644 index 0000000..be6c6d7 --- /dev/null +++ b/contrib/dlz/modules/sqlite3/dlz_sqlite3_dynamic.c @@ -0,0 +1,1056 @@ +/* + * 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) 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 SQLitee DLZ module, without + * update support. Based in part on SQLite code contributed by Tim Tessier. + */ + +#include <sqlite3.h> +#include <stdarg.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> + +#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 SQLite3 + * 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; + + char *dbname; + + /* 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; +} sqlite3_instance_t; + +/* + * SQLite3 result set + */ +typedef struct { + char **pazResult; /* Result of the query */ + int pnRow; /* Number of result rows */ + int pnColumn; /* Number of result columns */ + int curRow; /* Current row */ + char *pzErrmsg; /* Error message */ +} sqlite3_res_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(sqlite3_instance_t *db, const char *helper_name, void *ptr); + +/* + * Private methods + */ + +static void +dlz_sqlite3_destroy(dbinstance_t *db) { + /* release DB connection */ + if (db->dbconn != NULL) { + sqlite3_close((sqlite3 *)db->dbconn); + } + sqlite3_shutdown(); + + /* 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_sqlite3_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_sqlite3_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 * +sqlite3_find_avail(sqlite3_instance_t *sqlite3) { + dbinstance_t *dbi = NULL, *head; + int count = 0; + + /* get top of list */ + head = dbi = DLZ_LIST_HEAD(*(sqlite3->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; + } + } + + sqlite3->log(ISC_LOG_INFO, + "SQLite3 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 * +escape_string(const char *instr) { + char *outstr; + char *ptr; + unsigned int len; + unsigned int tlen = 0; + unsigned int atlen = 0; + unsigned int i; + + if (instr == NULL) { + return (NULL); + } + len = strlen(instr); + atlen = (2 * len * sizeof(char)) + 1; + outstr = malloc(atlen); + if (outstr == NULL) { + return (NULL); + } + + ptr = outstr; + for (i = 0; i < len; i++) { + if (tlen > atlen || instr[i] == '\0') { + break; + } + + if (instr[i] == '\'') { + *ptr++ = '\''; + tlen++; + } + + *ptr++ = instr[i]; + tlen++; + } + *ptr = '\0'; + + 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 too. dbdata really holds a single database instance. + * The function will construct and run the query, hopefully getting + * a result set. + */ +static isc_result_t +sqlite3_get_resultset(const char *zone, const char *record, const char *client, + unsigned int query, void *dbdata, sqlite3_res_t **rsp) { + isc_result_t result; + dbinstance_t *dbi = NULL; + sqlite3_instance_t *db = (sqlite3_instance_t *)dbdata; + char *querystring = NULL; + sqlite3_res_t *rs = NULL; + int qres = 0; + + if ((query == COUNTZONE && rsp != NULL) || + (query != COUNTZONE && (rsp == NULL || *rsp != NULL))) + { + db->log(ISC_LOG_DEBUG(2), "Invalid result set pointer."); + result = ISC_R_FAILURE; + goto cleanup; + } + + /* find an available DBI from the list */ + dbi = sqlite3_find_avail(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 " + "sqlite3_get_resultset"); + result = ISC_R_UNEXPECTED; + goto cleanup; + } + + if (zone != NULL) { + if (dbi->zone != NULL) { + free(dbi->zone); + } + + dbi->zone = escape_string(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 = escape_string(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 = escape_string(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 " + "sqlite3_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); + + rs = malloc(sizeof(sqlite3_res_t)); + if (rs == NULL) { + db->log(ISC_LOG_ERROR, "Failed to allocate result set"); + result = ISC_R_NOMEMORY; + goto cleanup; + } + memset(rs, 0, sizeof(sqlite3_res_t)); + + qres = sqlite3_get_table(dbi->dbconn, querystring, &rs->pazResult, + &rs->pnRow, &rs->pnColumn, &rs->pzErrmsg); + if (qres != SQLITE_OK) { + db->log(ISC_LOG_DEBUG(1), "SQLite3 query failed; %s", + rs->pzErrmsg != NULL ? rs->pzErrmsg : "unknown error"); + sqlite3_free(rs->pzErrmsg); + rs->pzErrmsg = NULL; + result = ISC_R_FAILURE; + goto cleanup; + } + + result = ISC_R_SUCCESS; + if (query == COUNTZONE) { + sqlite3_free_table(rs->pazResult); + } + + if (rsp != NULL) { + *rsp = rs; + } + +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 char ** +dlz_sqlite3_fetch_row(sqlite3_res_t *rs) { + char **retval = NULL; + if (rs != NULL) { + if (rs->pnRow > 0 && rs->curRow < rs->pnRow) { + int index = (rs->curRow + 1) * rs->pnColumn; + retval = &rs->pazResult[index]; + rs->curRow++; + } + } + return (retval); +} + +static unsigned int +dlz_sqlite3_num_fields(sqlite3_res_t *rs) { + unsigned int retval = 0; + if (rs != NULL) { + retval = rs->pnColumn; + } + return (retval); +} + +static unsigned int +dlz_sqlite3_num_rows(sqlite3_res_t *rs) { + unsigned int retval = 0; + if (rs != NULL) { + retval = rs->pnRow; + } + return (retval); +} + +static void +dlz_sqlite3_free_result(sqlite3_res_t *rs) { + if (rs != NULL) { + sqlite3_free_table(rs->pazResult); + free(rs); + } +} + +static isc_result_t +dlz_sqlite3_process_rs(sqlite3_instance_t *db, dns_sdlzlookup_t *lookup, + sqlite3_res_t *rs) { + isc_result_t result = ISC_R_NOTFOUND; + char **row; + unsigned int fields; + unsigned int j; + char *tmpString; + char *endp; + int ttl; + + row = dlz_sqlite3_fetch_row(rs); /* get a row from the result set */ + fields = dlz_sqlite3_num_fields(rs); /* how many columns in 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, "SQLite3 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, "SQLite3 module: unable " + "to allocate " + "memory for temporary " + "string"); + dlz_sqlite3_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, "SQLite3 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) { + dlz_sqlite3_free_result(rs); + db->log(ISC_LOG_ERROR, "putrr returned error: %d", + result); + return (ISC_R_FAILURE); + } + + row = dlz_sqlite3_fetch_row(rs); + } + + dlz_sqlite3_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; + sqlite3_res_t *rs = NULL; + sqlite3_uint64 rows; + sqlite3_instance_t *db = (sqlite3_instance_t *)dbdata; + + UNUSED(methods); + UNUSED(clientinfo); + + result = sqlite3_get_resultset(name, NULL, NULL, FINDZONE, dbdata, &rs); + if (result != ISC_R_SUCCESS || rs == NULL) { + if (rs != NULL) { + dlz_sqlite3_free_result(rs); + } + + db->log(ISC_LOG_ERROR, "SQLite3 module: unable to return " + "result set for FINDZONE query"); + + return (ISC_R_FAILURE); + } + + /* + * if we returned any rows, the zone is supported. + */ + rows = dlz_sqlite3_num_rows(rs); + dlz_sqlite3_free_result(rs); + if (rows > 0) { + sqlite3_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; + sqlite3_instance_t *db = (sqlite3_instance_t *)dbdata; + sqlite3_res_t *rs = NULL; + sqlite3_uint64 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 = sqlite3_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) { + dlz_sqlite3_free_result(rs); + } + db->log(ISC_LOG_ERROR, "SQLite3 module: unable to return " + "result set for ALLOWXFR query"); + return (ISC_R_FAILURE); + } + + /* + * count how many rows in result set; if we returned any, + * zone xfr is allowed. + */ + rows = dlz_sqlite3_num_rows(rs); + dlz_sqlite3_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; + sqlite3_instance_t *db = (sqlite3_instance_t *)dbdata; + sqlite3_res_t *rs = NULL; + char **row; + unsigned int fields; + unsigned int j; + char *tmpString; + char *endp; + int ttl; + + result = sqlite3_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, "SQLite3 module: unable to return " + "result set for all nodes query"); + goto cleanup; + } + + result = ISC_R_NOTFOUND; + + fields = dlz_sqlite3_num_fields(rs); + row = dlz_sqlite3_fetch_row(rs); + while (row != NULL) { + if (fields < 4) { + db->log(ISC_LOG_ERROR, "SQLite3 module: too few fields " + "returned " + "by ALLNODES query"); + result = ISC_R_FAILURE; + goto cleanup; + } + + ttl = strtol(safeGet(row[0]), &endp, 10); + if (*endp != '\0' || ttl < 0) { + db->log(ISC_LOG_ERROR, "SQLite3 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, "SQLite3 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 = dlz_sqlite3_fetch_row(rs); + } + +cleanup: + if (rs != NULL) { + dlz_sqlite3_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; + sqlite3_res_t *rs = NULL; + sqlite3_instance_t *db = (sqlite3_instance_t *)dbdata; + + result = sqlite3_get_resultset(zone, NULL, NULL, AUTHORITY, dbdata, + &rs); + if (result == ISC_R_NOTIMPLEMENTED) { + return (result); + } + + if (result != ISC_R_SUCCESS) { + if (rs != NULL) { + dlz_sqlite3_free_result(rs); + } + db->log(ISC_LOG_ERROR, "SQLite3 module: unable to return " + "result set for AUTHORITY query"); + return (ISC_R_FAILURE); + } + + /* + * lookup and authority result sets are processed in the same + * manner: dlz_sqlite3_process_rs does the job for both functions. + */ + return (dlz_sqlite3_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; + sqlite3_res_t *rs = NULL; + sqlite3_instance_t *db = (sqlite3_instance_t *)dbdata; + + UNUSED(methods); + UNUSED(clientinfo); + + result = sqlite3_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) { + dlz_sqlite3_free_result(rs); + } + db->log(ISC_LOG_ERROR, "SQLite3 module: unable to return " + "result set for LOOKUP query"); + return (ISC_R_FAILURE); + } + + /* + * lookup and authority result sets are processed in the same + * manner: dlz_sqlite3_process_rs does the job for both functions. + */ + return (dlz_sqlite3_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; + sqlite3_instance_t *s3 = NULL; + dbinstance_t *dbi = NULL; + sqlite3 *dbc = NULL; + char *tmp = NULL; + char *endp = NULL; + const char *helper_name; + int dbcount, i, ret; + va_list ap; + + UNUSED(dlzname); + + /* allocate memory for SQLite3 instance */ + s3 = calloc(1, sizeof(sqlite3_instance_t)); + if (s3 == NULL) { + return (ISC_R_NOMEMORY); + } + memset(s3, 0, sizeof(sqlite3_instance_t)); + + /* Fill in the helper functions */ + va_start(ap, dbdata); + while ((helper_name = va_arg(ap, const char *)) != NULL) { + b9_add_helper(s3, helper_name, va_arg(ap, void *)); + } + va_end(ap); + + /* if debugging, let user know we are multithreaded. */ + s3->log(ISC_LOG_DEBUG(1), "SQLite3 module: running multithreaded"); + + /* verify we have at least 4 arg's passed to the module */ + if (argc < 4) { + s3->log(ISC_LOG_ERROR, "SQLite3 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) { + s3->log(ISC_LOG_ERROR, "SQLite3 module cannot accept " + "more than 8 command line args."); + return (ISC_R_FAILURE); + } + + /* get db name - required */ + s3->dbname = get_parameter_value(argv[1], "dbname="); + if (s3->dbname == NULL) { + s3->log(ISC_LOG_ERROR, "SQLite3 module requires a dbname " + "parameter."); + result = ISC_R_FAILURE; + goto cleanup; + } + + /* 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) { + s3->log(ISC_LOG_ERROR, "SQLite3 module: database " + "connection count " + "must be positive."); + free(tmp); + result = ISC_R_FAILURE; + goto cleanup; + } + free(tmp); + } + + /* allocate memory for database connection list */ + s3->db = calloc(1, sizeof(db_list_t)); + if (s3->db == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + + /* initialize DB connection list */ + DLZ_LIST_INIT(*(s3->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, s3->log); + break; + case 5: + result = build_dbinstance(NULL, NULL, argv[4], argv[2], + argv[3], NULL, &dbi, s3->log); + break; + case 6: + result = build_dbinstance(argv[5], NULL, argv[4], + argv[2], argv[3], NULL, &dbi, + s3->log); + break; + case 7: + result = build_dbinstance(argv[5], argv[6], argv[4], + argv[2], argv[3], NULL, &dbi, + s3->log); + break; + case 8: + result = build_dbinstance(argv[5], argv[6], argv[4], + argv[2], argv[3], argv[7], + &dbi, s3->log); + break; + default: + result = ISC_R_FAILURE; + } + + if (result != ISC_R_SUCCESS) { + s3->log(ISC_LOG_ERROR, "SQLite3 module: could not " + "create " + "database instance object."); + result = ISC_R_FAILURE; + goto cleanup; + } + + /* create and set db connection */ + ret = sqlite3_initialize(); + if (ret != SQLITE_OK) { + s3->log(ISC_LOG_ERROR, "SQLite3 module: could not " + "initialize database object."); + result = ISC_R_FAILURE; + goto cleanup; + } + + ret = sqlite3_open(s3->dbname, &dbc); + if (ret != SQLITE_OK) { + s3->log(ISC_LOG_ERROR, + "SQLite3 module: could not " + "open '%s'.", + s3->dbname); + result = ISC_R_FAILURE; + goto cleanup; + } + + /* when multithreaded, build a list of DBI's */ + DLZ_LINK_INIT(dbi, link); + DLZ_LIST_APPEND(*(s3->db), dbi, link); + + dbi->dbconn = dbc; + dbc = NULL; + /* set DBI = null for next loop through. */ + dbi = NULL; + } + + *dbdata = s3; + return (ISC_R_SUCCESS); + +cleanup: + dlz_destroy(s3); + + return (result); +} + +/*% + * Destroy the module. + */ +void +dlz_destroy(void *dbdata) { + sqlite3_instance_t *db = (sqlite3_instance_t *)dbdata; + /* cleanup the list of DBI's */ + if (db->db != NULL) { + dlz_sqlite3_destroy_dblist((db_list_t *)(db->db)); + } + + if (db->dbname != NULL) { + free(db->dbname); + } +} + +/* + * 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(sqlite3_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/sqlite3/testing/README b/contrib/dlz/modules/sqlite3/testing/README new file mode 100644 index 0000000..c7af001 --- /dev/null +++ b/contrib/dlz/modules/sqlite3/testing/README @@ -0,0 +1,10 @@ +These files were used for testing on Ubuntu Linux using SQLite3 + +- Install SQLite3: sudo apt-get install sqlite3 libsqlite3-dev +- Build sqlite3 DLZ module +- Run "sqlite3 BindDB < dlz.schema" to set up database +- Run "sqlite3 BindDB < dlz.data" to populate it +- Run "named -gc named.conf" +- Send test queries, e.g "dig @localhost -p 5300 example.com", + "dig @localhost -p 5300 axfr example.com" (AXFR should be + allowed from 127.0.0.1 only). diff --git a/contrib/dlz/modules/sqlite3/testing/dlz.data b/contrib/dlz/modules/sqlite3/testing/dlz.data new file mode 100644 index 0000000..015607f --- /dev/null +++ b/contrib/dlz/modules/sqlite3/testing/dlz.data @@ -0,0 +1,18 @@ +INSERT INTO `records` +(`zone`, `ttl`, `type`, `host`, `mx_priority`, `data`, `primary_ns`, `resp_contact`, `serial`, `refresh`, `retry`, `expire`, `minimum`) +VALUES +('example.com', 86400, 'SOA', '@', NULL, NULL, 'ns1.example.com.', 'info.example.com.', 2011043001, 10800, 7200, 604800, 86400), +('example.com', 86400, 'NS', '@', NULL, 'ns1.example.com.', NULL, NULL, NULL, NULL, NULL, NULL, NULL), +('example.com', 86400, 'NS', '@', NULL, 'ns2.example.com.', NULL, NULL, NULL, NULL, NULL, NULL, NULL), +('example.com', 86400, 'MX', '@', 10, 'mail.example.com.', NULL, NULL, NULL, NULL, NULL, NULL, NULL), +('example.com', 86400, 'A', '@', NULL, '192.168.0.2', NULL, NULL, NULL, NULL, NULL, NULL, NULL), +('example.com', 86400, 'CNAME', 'www', NULL, '@', NULL, NULL, NULL, NULL, NULL, NULL, NULL), +('example.com', 86400, 'A', 'ns1', NULL, '192.168.0.111', NULL, NULL, NULL, NULL, NULL, NULL, NULL), +('example.com', 86400, 'A', 'ns2', NULL, '192.168.0.222', NULL, NULL, NULL, NULL, NULL, NULL, NULL), +('example.com', 86400, 'A', 'mail', NULL, '192.168.0.3', NULL, NULL, NULL, NULL, NULL, NULL, NULL), +('example.com', 86400, 'TXT', '@', NULL, 'v=spf1 ip:192.168.0.3 ~all', NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +INSERT INTO `xfr` +(`zone`, `client`) +VALUES +('example.com', '127.0.0.1'); diff --git a/contrib/dlz/modules/sqlite3/testing/dlz.schema b/contrib/dlz/modules/sqlite3/testing/dlz.schema new file mode 100644 index 0000000..4cbcb34 --- /dev/null +++ b/contrib/dlz/modules/sqlite3/testing/dlz.schema @@ -0,0 +1,28 @@ +CREATE TABLE IF NOT EXISTS `records` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `zone` CHAR(255) NOT NULL, + `ttl` INT NOT NULL DEFAULT '86400', + `type` CHAR(255) NOT NULL, + `host` CHAR(255) NOT NULL DEFAULT '@', + `mx_priority` INT DEFAULT NULL, + `data` text, + `primary_ns` CHAR(255) DEFAULT NULL, + `resp_contact` CHAR(255) DEFAULT NULL, + `serial` bigint DEFAULT NULL, + `refresh` INT DEFAULT NULL, + `retry` INT DEFAULT NULL, + `expire` INT DEFAULT NULL, + `minimum` INT DEFAULT NULL +); + +CREATE INDEX IF NOT EXISTS record_type on records (type); +CREATE INDEX IF NOT EXISTS record_host on records (host); +CREATE INDEX IF NOT EXISTS record_zone on records (zone); + +CREATE TABLE IF NOT EXISTS `xfr` ( + `zone` CHAR(255) NOT NULL, + `client` CHAR(255) NOT NULL +); + +CREATE INDEX IF NOT EXISTS xfr_zone on xfr (zone); +CREATE INDEX IF NOT EXISTS xfr_client on xfr (client); diff --git a/contrib/dlz/modules/sqlite3/testing/named.conf b/contrib/dlz/modules/sqlite3/testing/named.conf new file mode 100644 index 0000000..d5ce0bb --- /dev/null +++ b/contrib/dlz/modules/sqlite3/testing/named.conf @@ -0,0 +1,45 @@ +/* + * 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_sqlite3_dynamic.so + { + dbname=BindDB threads=2 + } + {SELECT zone FROM records WHERE zone = '$zone$'} + {SELECT ttl, type, mx_priority, CASE WHEN type = 'TXT' THEN '\"' || data || '\"' ELSE data END 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, CASE WHEN type = 'TXT' THEN '\"' || data || '\"' ELSE data END 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/wildcard/Makefile b/contrib/dlz/modules/wildcard/Makefile new file mode 100644 index 0000000..087a353 --- /dev/null +++ b/contrib/dlz/modules/wildcard/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. +# Copyright (C) Vadim Goncharov, Russia, vadim_nuclight@mail.ru. +# +# 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 + +all: dlz_wildcard_dynamic.so + +dlz_dbi.o: ../common/dlz_dbi.c + $(CC) $(CFLAGS) -c ../common/dlz_dbi.c + +dlz_wildcard_dynamic.so: dlz_wildcard_dynamic.c dlz_dbi.o + $(CC) $(CFLAGS) -shared -o dlz_wildcard_dynamic.so \ + dlz_wildcard_dynamic.c dlz_dbi.o + +clean: + rm -f dlz_wildcard_dynamic.so *.o + +install: dlz_wildcard_dynamic.so + mkdir -p $(DESTDIR)$(libdir) + install dlz_wildcard_dynamic.so $(DESTDIR)$(libdir) diff --git a/contrib/dlz/modules/wildcard/README b/contrib/dlz/modules/wildcard/README new file mode 100644 index 0000000..2ec6852 --- /dev/null +++ b/contrib/dlz/modules/wildcard/README @@ -0,0 +1,59 @@ +<!-- +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. +Copyright (C) Vadim Goncharov, Russia, vadim_nuclight@mail.ru. + +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. +--> + +The "wildcard" DLZ module provides a "template" zone for domains matching +a wildcard name. For example, the following DLZ configuration would match +any zone name containing the string "example" and ending with .com, such +as "thisexample.com", "exampleofthat.com", or "anexampleoftheotherthing.com". + + dlz "test" { + database "dlopen ../dlz_wildcard_dynamic.so + *example*.com 10.53.* 1800 + @ 3600 SOA {ns3.example.nil. support.example.nil. 42 14400 7200 2592000 600} + @ 3600 NS ns3.example.nil. + @ 3600 NS ns4.example.nil. + @ 3600 NS ns8.example.nil. + @ 3600 MX {5 mail.example.nil.} + ftp 86400 A 192.0.0.1 + sql 86400 A 192.0.0.2 + tmp {} A 192.0.0.3 + www 86400 A 192.0.0.3 + www 86400 AAAA ::1 + txt 300 TXT {\"you requested $record$ in $zone$\"} + * 86400 A 192.0.0.100"; + }; + +For any zone name matching the wildcard, it would return the data from +the template. "$zone$" is replaced with zone name: i.e., the shortest +possible string of labels in the query name that matches the wildcard. +"$record$" is replaced with the remainder of the query name. In the +example above, a query for "txt.thisexample.com/TXT" would return the +string "you requested txt in thisexample.com". + +Any client whose source address matches the second wildcard ("10.53.*") +is allowed to request a zone transfer. diff --git a/contrib/dlz/modules/wildcard/dlz_wildcard_dynamic.c b/contrib/dlz/modules/wildcard/dlz_wildcard_dynamic.c new file mode 100644 index 0000000..95221a1 --- /dev/null +++ b/contrib/dlz/modules/wildcard/dlz_wildcard_dynamic.c @@ -0,0 +1,773 @@ +/* + * 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) Stichting NLnet, Netherlands, stichting@nlnet.nl. + * Copyright (C) Vadim Goncharov, Russia, vadim_nuclight@mail.ru. + * + * 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 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. + */ + +/* + * This provides the externally loadable wildcard DLZ module. + */ + +#include <ctype.h> +#include <inttypes.h> +#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> + +#define DE_CONST(konst, var) \ + do { \ + union { \ + const void *k; \ + void *v; \ + } _u; \ + _u.k = konst; \ + var = _u.v; \ + } while (0) + +/* fnmatch() return values. */ +#define FNM_NOMATCH 1 /* Match failed. */ + +/* fnmatch() flags. */ +#define FNM_NOESCAPE 0x01 /* Disable backslash escaping. */ +#define FNM_PATHNAME 0x02 /* Slash must be matched by slash. */ +#define FNM_PERIOD 0x04 /* Period must be matched by period. */ +#define FNM_LEADING_DIR 0x08 /* Ignore /<tail> after Imatch. */ +#define FNM_CASEFOLD 0x10 /* Case insensitive search. */ +#define FNM_IGNORECASE FNM_CASEFOLD +#define FNM_FILE_NAME FNM_PATHNAME + +/* + * Our data structures. + */ + +typedef struct named_rr nrr_t; +typedef DLZ_LIST(nrr_t) rr_list_t; + +typedef struct config_data { + char *zone_pattern; + char *axfr_pattern; + rr_list_t rrs_list; + char *zone; + char *record; + char *client; + + /* 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; +} config_data_t; + +struct named_rr { + char *name; + char *type; + int ttl; + query_list_t *data; + DLZ_LINK(nrr_t) link; +}; + +/* + * Forward references + */ +static int +rangematch(const char *, char, int, char **); + +static int +fnmatch(const char *pattern, const char *string, int flags); + +static void +b9_add_helper(struct config_data *cd, const char *helper_name, void *ptr); + +static const char * +shortest_match(const char *pattern, const char *string); + +isc_result_t +dlz_allnodes(const char *zone, void *dbdata, dns_sdlzallnodes_t *allnodes) { + config_data_t *cd = (config_data_t *)dbdata; + isc_result_t result; + char *querystring = NULL; + nrr_t *nrec; + int i = 0; + + DE_CONST(zone, cd->zone); + + /* Write info message to log */ + cd->log(ISC_LOG_DEBUG(1), "dlz_wildcard allnodes called for zone '%s'", + zone); + + result = ISC_R_FAILURE; + + nrec = DLZ_LIST_HEAD(cd->rrs_list); + while (nrec != NULL) { + cd->record = nrec->name; + + querystring = build_querystring(nrec->data); + + if (querystring == NULL) { + result = ISC_R_NOMEMORY; + goto done; + } + + cd->log(ISC_LOG_DEBUG(2), + "dlz_wildcard allnodes entry num %d: calling " + "putnamedrr(name=%s type=%s ttl=%d qs=%s)", + i++, nrec->name, nrec->type, nrec->ttl, querystring); + + result = cd->putnamedrr(allnodes, nrec->name, nrec->type, + nrec->ttl, querystring); + if (result != ISC_R_SUCCESS) { + goto done; + } + + nrec = DLZ_LIST_NEXT(nrec, link); + } + +done: + cd->zone = NULL; + + if (querystring != NULL) { + free(querystring); + } + + return (result); +} + +isc_result_t +dlz_allowzonexfr(void *dbdata, const char *name, const char *client) { + config_data_t *cd = (config_data_t *)dbdata; + + UNUSED(name); + + /* Write info message to log */ + cd->log(ISC_LOG_DEBUG(1), + "dlz_wildcard allowzonexfr called for client '%s'", client); + + if (fnmatch(cd->axfr_pattern, client, FNM_CASEFOLD) == 0) { + return (ISC_R_SUCCESS); + } else { + return (ISC_R_NOTFOUND); + } +} + +#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 */ +{ + config_data_t *cd = (config_data_t *)dbdata; + const char *p; + +#if DLZ_DLOPEN_VERSION >= 3 + UNUSED(methods); + UNUSED(clientinfo); +#endif /* if DLZ_DLOPEN_VERSION >= 3 */ + + p = shortest_match(cd->zone_pattern, name); + if (p == NULL) { + return (ISC_R_NOTFOUND); + } + + /* Write info message to log */ + cd->log(ISC_LOG_DEBUG(1), "dlz_wildcard findzonedb matched '%s'", p); + + return (ISC_R_SUCCESS); +} + +#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; + config_data_t *cd = (config_data_t *)dbdata; + char *querystring = NULL; + const char *p; + char *namebuf; + nrr_t *nrec; + +#if DLZ_DLOPEN_VERSION >= 2 + UNUSED(methods); + UNUSED(clientinfo); +#endif /* if DLZ_DLOPEN_VERSION >= 2 */ + + p = shortest_match(cd->zone_pattern, zone); + if (p == NULL) { + return (ISC_R_NOTFOUND); + } + + DE_CONST(name, cd->record); + DE_CONST(p, cd->zone); + + if ((p != zone) && (strcmp(name, "@") == 0 || strcmp(name, zone) == 0)) + { + size_t len = p - zone; + namebuf = malloc(len); + if (namebuf == NULL) { + return (ISC_R_NOMEMORY); + } + strncpy(namebuf, zone, len - 1); + namebuf[len - 1] = '\0'; + cd->record = namebuf; + } else if (p == zone) { + cd->record = (char *)"@"; + } + + /* Write info message to log */ + cd->log(ISC_LOG_DEBUG(1), + "dlz_wildcard_dynamic: lookup for '%s' in '%s': " + "trying '%s' in '%s'", + name, zone, cd->record, cd->zone); + + result = ISC_R_NOTFOUND; + nrec = DLZ_LIST_HEAD(cd->rrs_list); + while (nrec != NULL) { + nrr_t *next = DLZ_LIST_NEXT(nrec, link); + if (strcmp(cd->record, nrec->name) == 0) { + /* We handle authority data in dlz_authority() */ + if (strcmp(nrec->type, "SOA") == 0 || + strcmp(nrec->type, "NS") == 0) + { + nrec = next; + continue; + } + + querystring = build_querystring(nrec->data); + if (querystring == NULL) { + result = ISC_R_NOMEMORY; + goto done; + } + + result = cd->putrr(lookup, nrec->type, nrec->ttl, + querystring); + if (result != ISC_R_SUCCESS) { + goto done; + } + + result = ISC_R_SUCCESS; + + free(querystring); + querystring = NULL; + } + nrec = next; + } + +done: + cd->zone = NULL; + cd->record = NULL; + + if (querystring != NULL) { + free(querystring); + } + + return (result); +} + +isc_result_t +dlz_authority(const char *zone, void *dbdata, dns_sdlzlookup_t *lookup) { + isc_result_t result; + config_data_t *cd = (config_data_t *)dbdata; + char *querystring = NULL; + nrr_t *nrec; + const char *p; + + p = shortest_match(cd->zone_pattern, zone); + if (p == NULL) { + return (ISC_R_NOTFOUND); + } + + DE_CONST(p, cd->zone); + + /* Write info message to log */ + cd->log(ISC_LOG_DEBUG(1), "dlz_wildcard_dynamic: authority for '%s'", + zone); + + result = ISC_R_NOTFOUND; + nrec = DLZ_LIST_HEAD(cd->rrs_list); + while (nrec != NULL) { + if (strcmp("@", nrec->name) == 0) { + isc_result_t presult; + + querystring = build_querystring(nrec->data); + if (querystring == NULL) { + result = ISC_R_NOMEMORY; + goto done; + } + + presult = cd->putrr(lookup, nrec->type, nrec->ttl, + querystring); + if (presult != ISC_R_SUCCESS) { + result = presult; + goto done; + } + + result = ISC_R_SUCCESS; + + free(querystring); + querystring = NULL; + } + nrec = DLZ_LIST_NEXT(nrec, link); + } + +done: + cd->zone = NULL; + + if (querystring != NULL) { + free(querystring); + } + + return (result); +} + +static void +destroy_rrlist(config_data_t *cd) { + nrr_t *trec, *nrec; + + nrec = DLZ_LIST_HEAD(cd->rrs_list); + + while (nrec != NULL) { + trec = nrec; + + destroy_querylist(&trec->data); + + if (trec->name != NULL) { + free(trec->name); + } + if (trec->type != NULL) { + free(trec->type); + } + trec->name = trec->type = NULL; + + /* Get the next record, before we destroy this one. */ + nrec = DLZ_LIST_NEXT(nrec, link); + + free(trec); + } +} + +isc_result_t +dlz_create(const char *dlzname, unsigned int argc, char *argv[], void **dbdata, + ...) { + config_data_t *cd; + char *endp; + unsigned int i; + int def_ttl; + nrr_t *trec = NULL; + isc_result_t result; + const char *helper_name; + va_list ap; + + if (argc < 8 || argc % 4 != 0) { + return (ISC_R_FAILURE); + } + + cd = calloc(1, sizeof(config_data_t)); + if (cd == NULL) { + return (ISC_R_NOMEMORY); + } + memset(cd, 0, sizeof(config_data_t)); + + /* Fill in the helper functions */ + va_start(ap, dbdata); + while ((helper_name = va_arg(ap, const char *)) != NULL) { + b9_add_helper(cd, helper_name, va_arg(ap, void *)); + } + va_end(ap); + + /* + * Write info message to log + */ + cd->log(ISC_LOG_INFO, + "Loading '%s' using DLZ_wildcard driver. " + "Zone: %s, AXFR allowed for: %s, $TTL: %s", + dlzname, argv[1], argv[2], argv[3]); + + /* initialize the records list here to simplify cleanup */ + DLZ_LIST_INIT(cd->rrs_list); + + cd->zone_pattern = strdup(argv[1]); + cd->axfr_pattern = strdup(argv[2]); + if (cd->zone_pattern == NULL || cd->axfr_pattern == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + + def_ttl = strtol(argv[3], &endp, 10); + if (*endp != '\0' || def_ttl < 0) { + def_ttl = 3600; + cd->log(ISC_LOG_ERROR, "default TTL invalid, using 3600"); + } + + for (i = 4; i < argc; i += 4) { + result = ISC_R_NOMEMORY; + + trec = malloc(sizeof(nrr_t)); + if (trec == NULL) { + goto full_cleanup; + } + + memset(trec, 0, sizeof(nrr_t)); + + /* Initialize the record link */ + DLZ_LINK_INIT(trec, link); + /* Append the record to the list */ + DLZ_LIST_APPEND(cd->rrs_list, trec, link); + + trec->name = strdup(argv[i]); + if (trec->name == NULL) { + goto full_cleanup; + } + + trec->type = strdup(argv[i + 2]); + if (trec->type == NULL) { + goto full_cleanup; + } + + trec->ttl = strtol(argv[i + 1], &endp, 10); + if (argv[i + 1][0] == '\0' || *endp != '\0' || trec->ttl < 0) { + trec->ttl = def_ttl; + } + + result = build_querylist(argv[i + 3], &cd->zone, &cd->record, + &cd->client, &trec->data, 0, cd->log); + /* If unsuccessful, log err msg and cleanup */ + if (result != ISC_R_SUCCESS) { + cd->log(ISC_LOG_ERROR, + "Could not build RR data list at argv[%d]", + i + 3); + goto full_cleanup; + } + } + + *dbdata = cd; + + return (ISC_R_SUCCESS); + +full_cleanup: + destroy_rrlist(cd); + +cleanup: + if (cd->zone_pattern != NULL) { + free(cd->zone_pattern); + } + if (cd->axfr_pattern != NULL) { + free(cd->axfr_pattern); + } + free(cd); + + return (result); +} + +void +dlz_destroy(void *dbdata) { + config_data_t *cd = (config_data_t *)dbdata; + + /* + * Write debugging message to log + */ + cd->log(ISC_LOG_DEBUG(2), "Unloading DLZ_wildcard driver."); + + destroy_rrlist(cd); + + free(cd->zone_pattern); + free(cd->axfr_pattern); + free(cd); +} + +/* + * Return the version of the API + */ +int +dlz_version(unsigned int *flags) { + UNUSED(flags); + /* XXX: ok to set DNS_SDLZFLAG_THREADSAFE here? */ + return (DLZ_DLOPEN_VERSION); +} + +/* + * Register a helper function from the bind9 dlz_dlopen driver + */ +static void +b9_add_helper(struct config_data *cd, const char *helper_name, void *ptr) { + if (strcmp(helper_name, "log") == 0) { + cd->log = (log_t *)ptr; + } + if (strcmp(helper_name, "putrr") == 0) { + cd->putrr = (dns_sdlz_putrr_t *)ptr; + } + if (strcmp(helper_name, "putnamedrr") == 0) { + cd->putnamedrr = (dns_sdlz_putnamedrr_t *)ptr; + } + if (strcmp(helper_name, "writeable_zone") == 0) { + cd->writeable_zone = (dns_dlz_writeablezone_t *)ptr; + } +} + +static const char * +shortest_match(const char *pattern, const char *string) { + const char *p = string; + if (pattern == NULL || p == NULL || *p == '\0') { + return (NULL); + } + + p += strlen(p); + while (p-- > string) { + if (*p == '.') { + if (fnmatch(pattern, p + 1, FNM_CASEFOLD) == 0) { + return (p + 1); + } + } + } + if (fnmatch(pattern, string, FNM_CASEFOLD) == 0) { + return (string); + } + + return (NULL); +} + +/* + * The helper functions stolen from the FreeBSD kernel (sys/libkern/fnmatch.c). + * + * Why don't we use fnmatch(3) from libc? Because it is not thread-safe, and + * it is not thread-safe because it supports multibyte characters. But here, + * in BIND, we want to be thread-safe and don't need multibyte - DNS names are + * always ASCII. + */ +#define EOS '\0' + +#define RANGE_MATCH 1 +#define RANGE_NOMATCH 0 +#define RANGE_ERROR (-1) + +static int +fnmatch(const char *pattern, const char *string, int flags) { + const char *stringstart; + char *newp; + char c, test; + + for (stringstart = string;;) { + switch (c = *pattern++) { + case EOS: + if ((flags & FNM_LEADING_DIR) && *string == '/') { + return (0); + } + return (*string == EOS ? 0 : FNM_NOMATCH); + case '?': + if (*string == EOS) { + return (FNM_NOMATCH); + } + if (*string == '/' && (flags & FNM_PATHNAME)) { + return (FNM_NOMATCH); + } + if (*string == '.' && (flags & FNM_PERIOD) && + (string == stringstart || + ((flags & FNM_PATHNAME) && *(string - 1) == '/'))) + { + return (FNM_NOMATCH); + } + ++string; + break; + case '*': + c = *pattern; + /* Collapse multiple stars. */ + while (c == '*') { + c = *++pattern; + } + + if (*string == '.' && (flags & FNM_PERIOD) && + (string == stringstart || + ((flags & FNM_PATHNAME) && *(string - 1) == '/'))) + { + return (FNM_NOMATCH); + } + + /* Optimize for pattern with * at end or before /. */ + if (c == EOS) { + if (flags & FNM_PATHNAME) { + return ((flags & FNM_LEADING_DIR) || + index(string, + '/') == + NULL + ? 0 + : FNM_NOMATCH); + } else { + return (0); + } + } else if (c == '/' && flags & FNM_PATHNAME) { + if ((string = index(string, '/')) == NULL) { + return (FNM_NOMATCH); + } + break; + } + + /* General case, use recursion. */ + while ((test = *string) != EOS) { + if (!fnmatch(pattern, string, + flags & ~FNM_PERIOD)) + { + return (0); + } + if (test == '/' && flags & FNM_PATHNAME) { + break; + } + ++string; + } + return (FNM_NOMATCH); + case '[': + if (*string == EOS) { + return (FNM_NOMATCH); + } + if (*string == '/' && (flags & FNM_PATHNAME)) { + return (FNM_NOMATCH); + } + if (*string == '.' && (flags & FNM_PERIOD) && + (string == stringstart || + ((flags & FNM_PATHNAME) && *(string - 1) == '/'))) + { + return (FNM_NOMATCH); + } + + switch (rangematch(pattern, *string, flags, &newp)) { + case RANGE_ERROR: + goto norm; + case RANGE_MATCH: + pattern = newp; + break; + case RANGE_NOMATCH: + return (FNM_NOMATCH); + } + ++string; + break; + case '\\': + if (!(flags & FNM_NOESCAPE)) { + if ((c = *pattern++) == EOS) { + c = '\\'; + --pattern; + } + } + FALLTHROUGH; + default: + norm: + if (c == *string) { + } else if ((flags & FNM_CASEFOLD) && + (tolower((unsigned char)c) == + tolower((unsigned char)*string))) + { + } else { + return (FNM_NOMATCH); + } + string++; + break; + } + } + UNREACHABLE(); +} + +static int +rangematch(const char *pattern, char test, int flags, char **newp) { + int negate, ok; + char c, c2; + + /* + * A bracket expression starting with an unquoted circumflex + * character produces unspecified results (IEEE 1003.2-1992, + * 3.13.2). This implementation treats it like '!', for + * consistency with the regular expression syntax. + * J.T. Conklin (conklin@ngai.kaleida.com) + */ + if ((negate = (*pattern == '!' || *pattern == '^'))) { + ++pattern; + } + + if (flags & FNM_CASEFOLD) { + test = tolower((unsigned char)test); + } + + /* + * A right bracket shall lose its special meaning and represent + * itself in a bracket expression if it occurs first in the list. + * -- POSIX.2 2.8.3.2 + */ + ok = 0; + c = *pattern++; + do { + if (c == '\\' && !(flags & FNM_NOESCAPE)) { + c = *pattern++; + } + if (c == EOS) { + return (RANGE_ERROR); + } + + if (c == '/' && (flags & FNM_PATHNAME)) { + return (RANGE_NOMATCH); + } + + if (flags & FNM_CASEFOLD) { + c = tolower((unsigned char)c); + } + + if (*pattern == '-' && (c2 = *(pattern + 1)) != EOS && + c2 != ']') + { + pattern += 2; + if (c2 == '\\' && !(flags & FNM_NOESCAPE)) { + c2 = *pattern++; + } + if (c2 == EOS) { + return (RANGE_ERROR); + } + + if (flags & FNM_CASEFOLD) { + c2 = tolower((unsigned char)c2); + } + + if (c <= test && test <= c2) { + ok = 1; + } + } else if (c == test) { + ok = 1; + } + } while ((c = *pattern++) != ']'); + + *newp = (char *)(uintptr_t)pattern; + return (ok == negate ? RANGE_NOMATCH : RANGE_MATCH); +} diff --git a/contrib/dlz/modules/wildcard/testing/named.conf b/contrib/dlz/modules/wildcard/testing/named.conf new file mode 100644 index 0000000..cd46706 --- /dev/null +++ b/contrib/dlz/modules/wildcard/testing/named.conf @@ -0,0 +1,55 @@ +/* + * 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; }; +}; + +/* + * This will match any zone name containing the string "example" and + * ending with .com, such as "thisexample.com", "exampleofthat.com", + * or "anexampleoftheotherthing.com". + */ +dlz "test" { + database "dlopen ../dlz_wildcard_dynamic.so + *example*.com 10.53.* 1800 + @ 3600 SOA {ns3.example.nil. support.example.nil. 42 14400 7200 2592000 600} + @ 3600 NS ns3.example.nil. + @ 3600 NS ns4.example.nil. + @ 3600 NS ns8.example.nil. + @ 3600 MX {5 mail.example.nil.} + ftp 86400 A 192.0.0.1 + sql 86400 A 192.0.0.2 + tmp {} A 192.0.0.3 + www 86400 A 192.0.0.3 + www 86400 AAAA ::1 + txt 300 TXT {\"you requested $record$ in $zone$\"} + * 86400 A 192.0.0.100"; +}; diff --git a/contrib/scripts/catzhash.py b/contrib/scripts/catzhash.py new file mode 100644 index 0000000..fa0b69b --- /dev/null +++ b/contrib/scripts/catzhash.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# 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. + +# catzhash.py: generate the SHA-1 hash of a domain name in wire format. +# +# This can be used to determine the label to use in a catalog zone to +# represent the specified zone. For example, the zone +# "domain.example" can be represented in a catalog zone called +# "catalog.example" by adding the following record: +# +# 5960775ba382e7a4e09263fc06e7c00569b6a05c.zones.catalog.example. \ +# IN PTR domain.example. +# +# The label "5960775ba382e7a4e09263fc06e7c00569b6a05c" is the output of +# this script when run with the argument "domain.example". + +import sys +import hashlib +import dns.name + +if len(sys.argv) < 2: + print("Usage: %s name" % sys.argv[0]) + +NAME = dns.name.from_text(sys.argv[1]).to_wire() +print(hashlib.sha1(NAME).hexdigest()) diff --git a/contrib/scripts/check-secure-delegation.pl.in b/contrib/scripts/check-secure-delegation.pl.in new file mode 100644 index 0000000..0f38c08 --- /dev/null +++ b/contrib/scripts/check-secure-delegation.pl.in @@ -0,0 +1,116 @@ +#!@PERL@ +# +# 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. + +use warnings; +use FileHandle; +use IPC::Open2; +use POSIX qw/strftime/; + +# +# We only compare keyid / DNSSEC algorithm pairs. If this succeeds then +# the crypto will likely succeed. If it fails then the crypto will definitely +# fail. +# +$prefix = "@prefix@"; +$dig = "$prefix/bin/dig +cd +dnssec +noall +answer"; +$dsfromkey = "$prefix/sbin/dnssec-dsfromkey -1 -A -f /dev/stdin"; + +# Get "now" in a RRSIG datestamp format. +$now = strftime "%Y%m%d%H%M%S", gmtime; + +foreach $zone (@ARGV) { + my %algorithms = (); + my %dnskeygood = (); + my %dnskeyalg = (); + my %dnskey = (); + my %dsgood = (); + my %ds = (); + + # Read the DS records and extract the key id, algorithm pairs + open(DS, "$dig -t DS -q $zone|") || die("dig DS failed"); + while(<DS>) { + @words = split; + if ($words[3] eq "RRSIG" && $words[4] eq "DS") { + next if ($words[8] >= $now && $words[9] <= $now); + print "BAD SIG DATES: $_"; + } + next if ($words[3] ne "DS"); + $ds{"$words[4] $words[5]"} = 1; + $algorithms{"$words[5]"} = 1; + } + close(DS); + + # Read the RRSIG(DNSKEY) records and extract the key id, + # algorithm pairs. Set good if we have a match against the DS + # records. DNSKEY records should be before the RRSIG records. + open(DNSKEY, "$dig -t DNSKEY -q $zone|") || die("dig DNSKEY failed"); + while (<DNSKEY>) { + @words = split; + if ($words[3] eq "DNSKEY") { + $dnskeyalg{"$words[6]"} = 1; + next if (! -e "/dev/stdin"); + # get the key id ($dswords[3]). + $pid = open2(*Reader, *Writer, "$dsfromkey $zone"); + die("dsfromkey failed") if ($pid == -1); + print Writer "$_"; + close(Writer); + $line = <Reader>; + close(Reader); + @dswords = split /\s/, $line; + $dnskey{"$dswords[3] $dswords[4]"} = 1; + next; + } + next if ($words[3] ne "RRSIG" || $words[4] ne "DNSKEY"); + if ($words[8] >= $now && $words[9] <= $now) { + # If we don't have /dev/stdin then just check for the + # RRSIG otherwise check for both the DNSKEY and + # RRSIG. + $dsgood{"$words[5]"} = 1 + if (! -e "/dev/stdin" && + exists($ds{"$words[10] $words[5]"})); + $dsgood{"$words[5]"} = 1 + if (exists($ds{"$words[10] $words[5]"}) && + exists($dnskey{"$words[10] $words[5]"})); + $dnskeygood{"$words[5]"} = 1 + if (! -e "/dev/stdin"); + $dnskeygood{"$words[5]"} = 1 + if (exists($dnskey{"$words[10] $words[5]"})); + } else { + $dnskeygood{"$words[5]"} = 1; + print "BAD SIG DATES: $_"; + } + } + close(DNSKEY); + + # Do we have signatures for all DNSKEY algorithms? + foreach $alg ( keys %dnskeyalg ) { + print "Missing $zone DNSKEY RRSIG for algorithm $alg\n" + if (!exists($dnskeygood{$alg})); + } + + # Do we have a matching self signed DNSKEY for all DNSSEC algorithms + # in the DS records. + $count = 0; + foreach $alg ( keys %algorithms ) { + if (exists($dsgood{$alg})) { + print "$zone algorithm $alg good " . + "(found DS / self signed DNSKEY pair)\n"; + } else { + print "$zone algorithm $alg bad " . + "(no DS / self signed DNSKEY pair found)\n"; + } + $count++; + } + print "$zone has no secure delegation records\n" + if (! $count); +} diff --git a/contrib/scripts/check5011.pl b/contrib/scripts/check5011.pl new file mode 100644 index 0000000..814295a --- /dev/null +++ b/contrib/scripts/check5011.pl @@ -0,0 +1,210 @@ +#!/usr/bin/perl + +# 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. + +use warnings; +use strict; + +use POSIX qw(strftime); +my $now = strftime "%Y%m%d%H%M%S", gmtime; + +sub ext8601 ($) { + my $d = shift; + $d =~ s{(....)(..)(..)(..)(..)(..)} + {$1-$2-$3.$4:$5:$6+0000}; + return $d; +} + +sub getkey ($$) { + my $h = shift; + my $k = shift; + m{\s+(\d+)\s+(\d+)\s+(\d+)\s+[(]\s*$}; + $k->{flags} = $1; + $k->{protocol} = $2; + $k->{algorithm} = $3; + my $data = "("; + while (<$h>) { + s{^\s+}{}; + s{\s+$}{}; + last if m{^[)]}; + $data .= $_; + } + m{ alg = (\S+)\s*; key id = (\d+)}; + $k->{alg} = $1; + $k->{id} = $2; + $k->{data} = $data; + return $k; +} + +sub fmtkey ($) { + my $k = shift; + return sprintf "%16s tag %s", $k->{name}, $k->{id}; +} + +sub printstatus ($) { + my $a = shift; + if ($a->{removehd} ne "19700101000000") { + printf " untrusted and to be removed at %s\n", ext8601 $a->{removehd}; + } elsif ($a->{addhd} le $now) { + printf " trusted\n"; + } else { + printf " waiting for %s\n", ext8601 $a->{addhd}; + } +} + +sub digkeys ($) { + my $name = shift; + my $keys; + open my $d, "-|", qw{dig +multiline DNSKEY}, $name; + while (<$d>) { + next unless m{^([a-z0-9.-]*)\s+\d+\s+IN\s+DNSKEY\s+}; + next unless $name eq $1; + push @$keys, getkey $d, { name => $name }; + } + return $keys; +} + +my $anchor; +my $owner = "."; +while (<>) { + next unless m{^([a-z0-9.-]*)\s+KEYDATA\s+(\d+)\s+(\d+)\s+(\d+)\s+}; + my $k = getkey *ARGV, { + name => $1, + refresh => $2, + addhd => $3, + removehd => $4, + }; + if ($k->{name} eq "") { + $k->{name} = $owner; + } else { + $owner = $k->{name}; + } + $k->{name} =~ s{[.]*$}{.}; + push @{$anchor->{$k->{name}}}, $k; +} + +for my $name (keys %$anchor) { + my $keys = digkeys $name; + my $anchors = $anchor->{$name}; + for my $k (@$keys) { + if ($k->{flags} & 1) { + printf "%s %s", fmtkey $k, $k->{alg}; + } else { + # ZSK - skipping + next; + } + if ($k->{flags} & 512) { + print " revoked;"; + } + my $a; + for my $t (@$anchors) { + if ($t->{data} eq $k->{data} and + $t->{protocol} eq $k->{protocol} and + $t->{algorithm} eq $k->{algorithm}) { + $t->{matched} = 1; + $a = $t; + last; + } + } + if (not defined $a) { + print " no trust anchor\n"; + next; + } + printstatus $a; + } + for my $a (@$anchors) { + next if $a->{matched}; + printf "%s %s missing;", fmtkey $a, $a->{alg}; + printstatus $a; + } +} + +exit; + +__END__ + +=head1 NAME + +check5011 - summarize DNSSEC trust anchor status + +=head1 SYNOPSIS + +check5011 <I<managed-keys.bind>> + +=head1 DESCRIPTION + +The BIND managed-keys file contains DNSSEC trust anchors +that can be automatically updated according to RFC 5011. The +B<check5011> program reads this file and prints a summary of the +status of the trust anchors. It fetches the corresponding +DNSKEY records using B<dig> and compares them to the trust anchors. + +Each key is printed on a line with its name, its tag, and its +algorithm, followed by a summary of its status. + +=over + +=item C<trusted> + +The key is currently trusted. + +=item C<waiting for ...> + +The key is new, and B<named> is waiting for the "add hold-down" period +to pass before the key will be trusted. + +=item C<untrusted and to be removed at ...> + +The key was revoked and will be removed at the stated time. + +=item C<no trust anchor> + +The key is present in the DNS but not in the managed-keys file. + +=item C<revoked> + +The key has its revoked flag set. This is printed before the key's +trust anchor status which should normally be C<untrusted...> if +B<named> has observed the revocation. + +=item C<missing> + +There is no DNSKEY record for this trust anchor. This is printed +before the key's trust anchor status. + +=back + +By default the managed keys are stored in a file called +F<managed-keys.bind> in B<named>'s working directory. This location +can be changed with B<named>'s B<managed-keys-directory> option. If +you are using views the file may be named with the SHA256 hash of a +view name with a F<.mkeys> extension added. + +=head1 AUTHOR + +=over + +=item Written by Tony Finch <fanf2@cam.ac.uk> <dot@dotat.at> + +=item at the University of Cambridge Computing Service. + +=item You may do anything with this. It has no warranty. + +=item L<http://creativecommons.org/publicdomain/zero/1.0/> + +=back + +=head1 SEE ALSO + +dig(1), named(8) + +=cut diff --git a/contrib/scripts/nanny.pl b/contrib/scripts/nanny.pl new file mode 100644 index 0000000..02e7ed1 --- /dev/null +++ b/contrib/scripts/nanny.pl @@ -0,0 +1,49 @@ +#!/usr/bin/perl +# +# 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. + +# A simple nanny to make sure named stays running. + +$pid_file_location = '/var/run/named.pid'; +$nameserver_location = 'localhost'; +$dig_program = 'dig'; +$named_program = 'named'; + +fork() && exit(); + +for (;;) { + $pid = 0; + open(FILE, $pid_file_location) || goto restart; + $pid = <FILE>; + close(FILE); + chomp($pid); + + $res = kill 0, $pid; + + goto restart if ($res == 0); + + $dig_command = + "$dig_program +short . \@$nameserver_location > /dev/null"; + $return = system($dig_command); + goto restart if ($return == 9); + + sleep 30; + next; + + restart: + if ($pid != 0) { + kill 15, $pid; + sleep 30; + } + system ($named_program); + sleep 120; +} diff --git a/contrib/scripts/zone-edit.sh.in b/contrib/scripts/zone-edit.sh.in new file mode 100644 index 0000000..2596ef8 --- /dev/null +++ b/contrib/scripts/zone-edit.sh.in @@ -0,0 +1,152 @@ +#!/bin/sh +# +# 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. + +dir=/tmp/zone-edit.$$ +mkdir ${dir} || exit 1 +trap "/bin/rm -rf ${dir}" 0 + +prefix=@prefix@ +exec_prefix=@exec_prefix@ +bindir=@bindir@ + +dig=${bindir}/dig +checkzone=${bindir}/named-checkzone +nsupdate=${bindir}/nsupdate + +case $# in +0) echo "Usage: zone-edit <zone> [dig options] [ -- nsupdate options ]"; exit 0 ;; +esac + +# What kind of echo are we using? +try=`echo -n ""` +if test "X$try" = "X-n " +then + echo_arg="" + bsc="\\c" +else + echo_arg="-n" + bsc="" +fi + +zone="${1}" +shift +digopts= +while test $# -ne 0 +do + case "${1}" in + --) + shift + break + ;; + *) + digopts="$digopts $1" + shift + ;; + esac +done + +${dig} axfr "$zone" $digopts | +awk '$4 == "RRSIG" || $4 == "NSEC" || $4 == "NSEC3" || $4 == "NSEC3PARAM" { next; } { print; }' > ${dir}/old + +if test -s ${dir}/old +then + ${checkzone} -q -D "$zone" ${dir}/old > ${dir}/ooo +fi + +if test -s ${dir}/ooo +then + cp ${dir}/ooo ${dir}/new + while : + do + if ${VISUAL:-${EDITOR:-/bin/ed}} ${dir}/new + then + if ${checkzone} -q -D "$zone" ${dir}/new > ${dir}/nnn + then + sort ${dir}/ooo > ${dir}/s1 + sort ${dir}/nnn > ${dir}/s2 + comm -23 ${dir}/s1 ${dir}/s2 | + sed 's/^/update delete /' > ${dir}/ccc + comm -13 ${dir}/s1 ${dir}/s2 | + sed 's/^/update add /' >> ${dir}/ccc + if test -s ${dir}/ccc + then + cat ${dir}/ccc | more + while : + do + echo ${echo_arg} "Update (u), Abort (a), Redo (r), Modify (m), Display (d) : $bsc" + read ans + case "$ans" in + u) + ( + echo zone "$zone" + cat ${dir}/ccc + echo send + ) | ${nsupdate} "$@" + break 2 + ;; + a) + break 2 + ;; + d) + cat ${dir}/ccc | more + ;; + r) + cp ${dir}/ooo ${dir}/new + break + ;; + m) + break + ;; + esac + done + else + while : + do + echo ${echo_arg} "Abort (a), Redo (r), Modify (m) : $bsc" + read ans + case "$ans" in + a) + break 2 + ;; + r) + cp ${dir}/ooo ${dir}/new + break + ;; + m) + break + ;; + esac + done + fi + else + while : + do + echo ${echo_arg} "Abort (a), Redo (r), Modify (m) : $bsc" + read ans + case "$ans" in + a) + break 2 + ;; + r) + cp ${dir}/ooo ${dir}/new + break + ;; + m) + break + ;; + esac + done + fi + fi + done +fi |