/*
* Copyright (C) 2014 Maui Systems Ltd, Scotland, contact@maui-systems.co.uk.
*
* 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 MAUI SYSTEMS LTD DISCLAIMS ALL
* WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL MAUI SYSTEMS LTD 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.
*/
/*
* Copyright (C) 2011,2014 Internet Systems Consortium, Inc. ("ISC")
*
* 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.
*/
/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
#include <unistd.h>
#include <inttypes.h>
#include <pthread.h>
#include <netdb.h>
#include <ifaddrs.h>
#include <dlz_minimal.h>
#include <dlz_list.h>
#include <dlz_pthread.h>
/*
* 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)
*/
/*
* 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 \
"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 \
"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 \
"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')"
#ifdef WIN32
#elif defined(_REENTRANT)
#else
#endif
/*
* Number of concurrent database connections we support
* - equivalent to maxmium number of concurrent transactions
* that can be 'in-flight' + 1
*/
typedef struct mysql_record {
typedef struct mysql_instance {
int id;
int connected;
struct mysql_transaction {
char *zone;
char *zone_id;
};
typedef struct mysql_data {
int debug;
/*
* Database connection details
*/
char *db_name;
char *db_host;
char *db_user;
char *db_pass;
/*
* Database structures
*/
/*
* Transactions
*/
/*
* Mutex for transactions
*/
/* Helper functions from the dlz_dlopen driver */
} mysql_data_t;
struct mysql_arg {
char *arg;
};
/*
* Local functions
*/
static isc_boolean_t
/*
* Make sure this thread has been through 'init'
*/
return (ISC_TRUE);
"%s: database connection failed: %s",
return (ISC_FALSE);
}
return (ISC_TRUE);
}
static mysql_instance_t *
int i;
/*
* Find an available dbi
*/
for (i = 0; i < MAX_DBI; i++) {
break;
}
if (i == MAX_DBI) {
"%s: No available connections", modname);
return (NULL);
}
}
/*
* Allocate memory and store an escaped, sanitized version
* of string 'original'
*/
static char *
char *s;
return (NULL);
if (s != NULL) {
}
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
return (ISC_R_NOMEMORY);
*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 *
const char *command, ...)
{
/* Get a DB instance if needed */
return (NULL);
}
/* Make sure this instance is connected */
goto fail;
goto fail;
for (;;) {
if (*q == '\0')
break;
p = strstr(q, "%s");
if (p != NULL) {
*p = '\0';
goto fail;
if (result != ISC_R_SUCCESS)
goto fail;
goto fail;
if (result != ISC_R_SUCCESS)
goto fail;
q = p + 2;
} else {
goto fail;
if (result != ISC_R_SUCCESS)
goto fail;
break;
}
}
if (len == 0)
goto fail;
goto fail;
*query = '\0';
fail:
{
}
return (query);
}
/* Does this name end in a dot? */
static isc_boolean_t
isrelative(const char *s) {
return (ISC_FALSE);
return (ISC_TRUE);
}
/* Return a dot if 's' doesn't already end with one */
static inline const char *
dot(const char *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
return;
else {
if (isrelative(name))
else
}
}
/*
* Names are stored in relative form in ZoneData; this function
* removes labels matching 'zone' from the end of 'name'.
*/
static char *
const char *p;
char *new;
return (NULL);
return (new);
return (new);
}
if (strcasecmp(p, zone) != 0 &&
{
return (new);
}
return (new);
}
static isc_result_t
break;
}
}
return (result);
}
static isc_result_t
int ret;
/* Make sure this instance is connected. */
return (ISC_R_FAILURE);
if (ret != 0) {
"%s: query '%s' failed: %s",
return (ISC_R_FAILURE);
}
return (ISC_R_SUCCESS);
}
static MYSQL_RES *
return (NULL);
/* Get a DB instance if needed */
return (NULL);
}
/* Make sure this instance is connected */
goto fail;
if (result != ISC_R_SUCCESS)
goto fail;
"%s: unable to store result: %s",
goto fail;
}
"%s: query(%d) returned %d rows",
fail:
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 *
int i, j;
return (NULL);
/* Random query ID */
i = rand();
/* Flags (OpCode '4' in bits 14-11), Auth Answer set in bit 10 */
i = 0x2400;
/* QD Count */
i = 0x1;
/* Question */
/* Make the question into labels */
j = 12;
while (packet[j]) {
packet[j] = i - j - 1;
j = i;
}
/* Question type */
i = 6;
/* Queston class */
i = 1;
return (packet);
}
static void
int s;
return;
close(s);
return;
}
/*
* Generate and send a DNS NOTIFY packet
*/
static void
char *query;
unsigned char *packet;
int packetlen;
/* Get the name servers from the NS rrset */
return;
/* Create a DNS NOTIFY packet */
return;
}
/* Get a list of our own addresses */
if (getifaddrs(&ifap) < 0)
/* Tell each nameserver of the update */
struct hostent *h;
/*
* 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;
/* Get the address for the nameserver into a string */
continue;
/* Get local address into a string */
/* See if nameserver address matches this one */
}
if (!local) {
"%s: notify %s zone %s serial %d",
}
}
}
/*
* Constructs a mysql_record_t structure from 'rdatastr', to be
* used in the dlz_{add,sub,del}rdataset functions below.
*/
static mysql_record_t *
new_record = (mysql_record_t *)
malloc(sizeof(mysql_record_t));
if (new_record == NULL) {
"%s: makerecord - unable to malloc",
modname);
return (NULL);
}
"%s: makerecord - unable to malloc",
modname);
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
*/
goto error;
goto error;
goto error;
goto error;
goto error;
return (new_record);
return (NULL);
}
/*
* Remember a helper function from the bind9 dlz_dlopen driver
*/
static void
}
/*
* DLZ API functions
*/
/*
* Return the version of the API
*/
int
return (DLZ_DLOPEN_VERSION);
}
/*
* Called to initialize the driver
*/
void **dbdata, ...)
{
const char *helper_name;
int n;
return (ISC_R_NOMEMORY);
/* Fill in the helper functions */
"%s: missing args <dbname> "
"[<dbhost> [<user> <pass>]]", modname);
return (ISC_R_FAILURE);
}
if (argc > 2) {
if (argc > 4) {
} else {
}
} else {
}
"%s: DB=%s, Host=%s, User=%s",
/*
* Assign the 'state' to dbdata so we get it in our callbacks
*/
/*
* Populate DB instances
*/
if (mysql_thread_safe()) {
for (n = 0; n < MAX_DBI; n++) {
modname);
}
return (ISC_R_SUCCESS);
}
return (ISC_R_FAILURE);
}
/*
* Shut down the backend
*/
void
int i;
for (i = 0; i < MAX_DBI; i++) {
}
}
}
/*
* See if we handle a given zone
*/
{
char *query;
/* Query the Zones table to see if this zone is present */
return (ISC_R_NOMEMORY);
return (ISC_R_FAILURE);
if (mysql_num_rows(res) == 0)
return (result);
}
/*
* Perform a database lookup
*/
{
char *real_name;
char *query;
"%s: dlz_lookup - no putrr", modname);
return (ISC_R_NOTIMPLEMENTED);
}
/* Are we okay to try to find the txn version? */
if (clientinfo != NULL &&
"%s: lookup in live transaction %p, DBI %p",
}
}
return (ISC_R_NOMEMORY);
} else {
return (ISC_R_NOMEMORY);
}
/*
* Get the Zones table data for use in the SOA:
* zone admin serial refresh retry expire min
*/
return (ISC_R_NOMEMORY);
}
return (ISC_R_NOTFOUND);
}
int ttl;
/* zone admin serial refresh retry expire min */
if (result != ISC_R_SUCCESS) {
return (result);
}
}
/*
* Now we'll get the rest of the apex data
*/
} else
return (ISC_R_NOTFOUND);
}
int ttl;
if (result != ISC_R_SUCCESS) {
return (result);
}
}
"%s: dlz_lookup %s/%s/%s - (%d rows)",
if (!found)
return (ISC_R_NOTFOUND);
return (ISC_R_SUCCESS);
}
/*
* See if a zone transfer is allowed
*/
/* Just say yes for all our zones */
}
/*
* Perform a zone transfer
*/
char *query;
return (ISC_R_NOTIMPLEMENTED);
/*
* Get all the ZoneData for this zone
*/
return (ISC_R_NOMEMORY);
return (ISC_R_NOTFOUND);
int ttl;
if (result != ISC_R_SUCCESS)
break;
}
return (result);
}
/*
* Start a transaction
*/
char *query;
/*
* Check Zone is writable
*/
return (ISC_R_NOMEMORY);
return (ISC_R_FAILURE);
return (ISC_R_FAILURE);
}
/*
* See if we already have a transaction for this zone
*/
"%s: transaction already "
return (ISC_R_FAILURE);
}
}
/*
* Create new transaction
*/
newtx = (mysql_transaction_t *)
malloc(sizeof(mysql_transaction_t));
goto cleanup;
}
if (result != ISC_R_SUCCESS) {
goto cleanup;
}
/*
* Add this tx to front of list
*/
if (result == ISC_R_SUCCESS) {
} else {
}
return (result);
}
/*
* End a transaction
*/
void
{
char *query;
/*
* Find the transaction
*/
/* Tx is first in list; remove it. */
} else {
break;
}
}
break;
}
}
}
/*
* Tidy up
*/
if (commit) {
/*
* Find out the serial number of the zone out with the
* transaction so we can see if it has incremented or not
*/
"%s: unable to commit transaction %x "
"on zone %s: no memory",
return;
}
}
/*
* Commit the transaction to the database
*/
"%s: (%x) commit transaction on zone %s",
return;
}
"%s: (%x) committing transaction "
"on zone %s",
/*
* Now get the serial number again
*/
}
/*
* Look to see if serial numbers have changed
*/
} else {
"%s: (%x) roll back transaction on zone %s",
}
/*
* Unlock the mutex for this txn
*/
/*
* Free up other structures
*/
}
/*
* Configure a writeable zone
*/
#if DLZ_DLOPEN_VERSION < 3
#else /* DLZ_DLOPEN_VERSION >= 3 */
#endif /* DLZ_DLOPEN_VERSION */
{
int count;
/*
* Seed PRNG (used by Notify code)
*/
"%s: no writeable_zone method available",
modname);
return (ISC_R_FAILURE);
}
/*
* Get a list of Zones (ignore writeable column at this point)
*/
return (ISC_R_FAILURE);
count = 0;
int sn;
#if DLZ_DLOPEN_VERSION >= 3
#endif
row[0]);
if (result != ISC_R_SUCCESS) {
"%s: failed to configure zone %s",
return (result);
}
count++;
}
return (ISC_R_SUCCESS);
}
/*
* Authorize a zone update
*/
{
"%s: allowing update of %s by key %s",
return (ISC_TRUE);
}
{
return (ISC_R_FAILURE);
return (ISC_R_NOMEMORY);
return (ISC_R_FAILURE);
/* Write out data to database */
goto cleanup;
}
} 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
*/
goto cleanup;
}
}
return (result);
}
{
return (ISC_R_FAILURE);
return (ISC_R_NOMEMORY);
return (ISC_R_FAILURE);
/*
* If 'type' isn't 'SOA', delete the records
*/
else {
goto cleanup;
}
}
return (result);
}
{
return (ISC_R_FAILURE);
return (ISC_R_NOMEMORY);
goto cleanup;
}
return (result);
}