1 /*
2  * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
3  *
4  * SPDX-License-Identifier: MPL-2.0 and ISC
5  *
6  * This Source Code Form is subject to the terms of the Mozilla Public
7  * License, v. 2.0. If a copy of the MPL was not distributed with this
8  * file, you can obtain one at https://mozilla.org/MPL/2.0/.
9  */
10 
11 /*
12  * Copyright (C) 2002 Stichting NLnet, Netherlands, stichting@nlnet.nl.
13  *
14  * The development of Dynamically Loadable Zones (DLZ) for Bind 9 was
15  * conceived and contributed by Rob Butler.
16  *
17  * Permission to use, copy, modify, and distribute this software for any purpose
18  * with or without fee is hereby granted, provided that the above copyright
19  * notice and this permission notice appear in all copies.
20  *
21  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
22  * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
23  * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
24  * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
25  * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
26  * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
27  * PERFORMANCE OF THIS SOFTWARE.
28  */
29 
30 /*
31  * BIND 9 DLZ MySQL module with support for dynamic DNS (DDNS)
32  *
33  * Adapted from code contributed by Marty Lee, Maui Systems Ltd.
34  *
35  * See README for database schema and usage details.
36  */
37 
38 #include <ifaddrs.h>
39 #include <inttypes.h>
40 #include <netdb.h>
41 #include <netinet/in.h>
42 #include <pthread.h>
43 #include <stdarg.h>
44 #include <stdbool.h>
45 #include <stdio.h>
46 #include <stdlib.h>
47 #include <string.h>
48 #include <sys/socket.h>
49 #include <sys/types.h>
50 #include <unistd.h>
51 
52 #include <mysql/errmsg.h>
53 #include <mysql/mysql.h>
54 
55 #include <dlz_list.h>
56 #include <dlz_minimal.h>
57 #include <dlz_pthread.h>
58 
59 #if !defined(LIBMARIADB) && MYSQL_VERSION_ID >= 80000
60 typedef bool my_bool;
61 #endif /* !defined(LIBMARIADB) && MYSQL_VERSION_ID >= 80000 */
62 
63 /*
64  * The SQL queries that will be used for lookups and updates are defined
65  * here.  They will be processed into queries by the build_query()
66  * function.
67  *
68  * NOTE: Despite appearances, these do NOT use printf-style formatting.
69  * "%s", with no modifiers, is the only supported directive.
70  */
71 
72 /*
73  * Get the NS RRset for a zone
74  * Arguments: zone-name
75  */
76 #define Q_GETNS                                                         \
77 	"SELECT d.data FROM ZoneData d, Zones z "                       \
78 	"WHERE UPPER(d.type) = 'NS' AND LOWER(z.domain) = LOWER('%s') " \
79 	"AND z.id = d.zone_id"
80 
81 /*
82  * Get a list of zones (ignoring writable or not)
83  * Arguments: (none)
84  */
85 #define Q_GETZONES "SELECT LOWER(domain), serial FROM Zones"
86 
87 /*
88  * Find a specific zone
89  * Arguments: zone-name
90  */
91 #define Q_FINDZONE "SELECT id FROM Zones WHERE LOWER(domain) = LOWER('%s')"
92 
93 /*
94  * Get SOA data from zone apex
95  * Arguments: zone-name
96  */
97 #define Q_GETSOA                                                            \
98 	"SELECT host, admin, serial, refresh, retry, expire, minimum, ttl " \
99 	"FROM Zones WHERE LOWER(domain) = LOWER('%s')"
100 
101 /*
102  * Get other data from zone apex
103  * Arguments: zone-name, zone-name (repeated)
104  */
105 #define Q_GETAPEX                                                   \
106 	"SELECT d.type, d.data, d.ttl FROM ZoneData d, Zones z "    \
107 	"WHERE LOWER(z.domain) = LOWER('%s') AND z.id = d.zone_id " \
108 	"AND LOWER(d.name) IN (LOWER('%s'), '', '@') "              \
109 	"ORDER BY UPPER(d.type) ASC"
110 
111 /*
112  * Get data from non-apex nodes
113  * Arguments: zone-name, node-name (relative to zone name)
114  */
115 #define Q_GETNODE                                                   \
116 	"SELECT d.type, d.data, d.ttl FROM ZoneData d, Zones z "    \
117 	"WHERE LOWER(z.domain) = LOWER('%s') AND z.id = d.zone_id " \
118 	"AND LOWER(d.name) = LOWER('%s') "                          \
119 	"ORDER BY UPPER(d.type) ASC"
120 
121 /*
122  * Get all data from a zone, for AXFR
123  * Arguments: zone-name
124  */
125 #define Q_GETALL                                                         \
126 	"SELECT d.name, d.type, d.data, d.ttl FROM ZoneData d, Zones z " \
127 	"WHERE LOWER(z.domain) = LOWER('%s') AND z.id = d.zone_id"
128 
129 /*
130  * Get SOA serial number for a zone.
131  * Arguments: zone-name
132  */
133 #define Q_GETSERIAL "SELECT serial FROM Zones WHERE domain = '%s'"
134 
135 /*
136  * Determine whether a zone is writeable, and if so, retrieve zone_id
137  * Arguments: zone-name
138  */
139 #define Q_WRITEABLE                   \
140 	"SELECT id FROM Zones WHERE " \
141 	"LOWER(domain) = LOWER('%s') AND writeable = 1"
142 
143 /*
144  * Insert data into zone (other than SOA)
145  * Arguments: zone-id (from Q_WRITEABLE), node-name (relative to zone-name),
146  * 	      rrtype, rdata text, TTL (in text format)
147  */
148 #define I_DATA                                                   \
149 	"INSERT INTO ZoneData (zone_id, name, type, data, ttl) " \
150 	"VALUES (%s, LOWER('%s'), UPPER('%s'), '%s', %s)"
151 
152 /*
153  * Update SOA serial number for a zone
154  * Arguments: new serial number (in text format), zone-id (from Q_WRITEABLE)
155  */
156 #define U_SERIAL "UPDATE Zones SET serial = %s WHERE id = %s"
157 
158 /*
159  * Delete a specific record (non-SOA) from a zone
160  *
161  * Arguments: node-name (relative to zone-name), zone-id (from Q_WRITEABLE),
162  * 	      rrtype, rdata text, TTL (in text format).
163  */
164 #define D_RECORD                                                       \
165 	"DELETE FROM ZoneData WHERE zone_id = %s AND "                 \
166 	"LOWER(name) = LOWER('%s') AND UPPER(type) = UPPER('%s') AND " \
167 	"data = '%s' AND ttl = %s"
168 
169 /*
170  * Delete an entire rrset from a zone
171  * Arguments: node-name (relative to zone-name), zone-id (from Q_WRITEABLE),
172  * 	      rrtype.
173  */
174 #define D_RRSET                                        \
175 	"DELETE FROM ZoneData WHERE zone_id = %s AND " \
176 	"LOWER(name) = LOWER('%s') AND UPPER(type) = UPPER('%s')"
177 
178 /*
179  * Number of concurrent database connections we support
180  * - equivalent to maxmium number of concurrent transactions
181  *   that can be 'in-flight' + 1
182  */
183 #define MAX_DBI 16
184 
185 typedef struct mysql_record {
186 	char zone[255];
187 	char name[100];
188 	char type[10];
189 	char data[200];
190 	char ttl[10];
191 } mysql_record_t;
192 
193 typedef struct mysql_instance {
194 	int id;
195 	MYSQL *sock;
196 	int connected;
197 	dlz_mutex_t mutex;
198 } mysql_instance_t;
199 
200 typedef struct mysql_transaction mysql_transaction_t;
201 struct mysql_transaction {
202 	char *zone;
203 	char *zone_id;
204 	mysql_instance_t *dbi;
205 	mysql_transaction_t *next;
206 };
207 
208 typedef struct mysql_data {
209 	int debug;
210 
211 	/*
212 	 * Database connection details
213 	 */
214 	char *db_name;
215 	char *db_host;
216 	char *db_user;
217 	char *db_pass;
218 
219 	/*
220 	 * Database structures
221 	 */
222 	mysql_instance_t db[MAX_DBI];
223 
224 	/*
225 	 * Transactions
226 	 */
227 	mysql_transaction_t *transactions;
228 
229 	/*
230 	 * Mutex for transactions
231 	 */
232 	dlz_mutex_t tx_mutex;
233 
234 	/* Helper functions from the dlz_dlopen driver */
235 	log_t *log;
236 	dns_sdlz_putrr_t *putrr;
237 	dns_sdlz_putnamedrr_t *putnamedrr;
238 	dns_dlz_writeablezone_t *writeable_zone;
239 } mysql_data_t;
240 
241 typedef struct mysql_arg mysql_arg_t;
242 typedef DLZ_LIST(mysql_arg_t) mysql_arglist_t;
243 struct mysql_arg {
244 	char *arg;
245 	DLZ_LINK(mysql_arg_t) link;
246 };
247 
248 static const char *modname = "dlz_mysqldyn";
249 
250 /*
251  * Local functions
252  */
253 static bool
db_connect(mysql_data_t * state,mysql_instance_t * dbi)254 db_connect(mysql_data_t *state, mysql_instance_t *dbi) {
255 	MYSQL *conn;
256 	/*
257 	 * Make sure this thread has been through 'init'
258 	 */
259 	mysql_thread_init();
260 
261 	if (dbi->connected) {
262 		return (true);
263 	}
264 
265 	if (state->log != NULL) {
266 		state->log(ISC_LOG_INFO, "%s: init connection %d ", modname,
267 			   dbi->id);
268 	}
269 
270 	conn = mysql_real_connect(dbi->sock, state->db_host, state->db_user,
271 				  state->db_pass, state->db_name, 0, NULL,
272 				  CLIENT_REMEMBER_OPTIONS);
273 	if (conn == NULL) {
274 		if (state->log != NULL) {
275 			state->log(ISC_LOG_ERROR,
276 				   "%s: database connection failed: %s",
277 				   modname, mysql_error(dbi->sock));
278 		}
279 
280 		dlz_mutex_unlock(&dbi->mutex);
281 		return (false);
282 	}
283 
284 	dbi->connected = 1;
285 	return (true);
286 }
287 
288 static mysql_instance_t *
get_dbi(mysql_data_t * state)289 get_dbi(mysql_data_t *state) {
290 	int i;
291 
292 	/*
293 	 * Find an available dbi
294 	 */
295 	for (i = 0; i < MAX_DBI; i++) {
296 		if (dlz_mutex_trylock(&state->db[i].mutex) == 0) {
297 			break;
298 		}
299 	}
300 
301 	if (i == MAX_DBI) {
302 		if (state->debug && state->log != NULL) {
303 			state->log(ISC_LOG_ERROR,
304 				   "%s: No available connections", modname);
305 		}
306 		return (NULL);
307 	}
308 	return (&state->db[i]);
309 }
310 
311 /*
312  * Allocate memory and store an escaped, sanitized version
313  * of string 'original'
314  */
315 static char *
sanitize(mysql_instance_t * dbi,const char * original)316 sanitize(mysql_instance_t *dbi, const char *original) {
317 	char *s;
318 
319 	if (original == NULL) {
320 		return (NULL);
321 	}
322 
323 	s = (char *)malloc((strlen(original) * 2) + 1);
324 	if (s != NULL) {
325 		memset(s, 0, (strlen(original) * 2) + 1);
326 
327 		mysql_real_escape_string(dbi->sock, s, original,
328 					 strlen(original));
329 	}
330 
331 	return (s);
332 }
333 
334 /*
335  * Append the string pointed to by 's' to the argument list 'arglist',
336  * and add the string length to the running total pointed to by 'len'.
337  */
338 static isc_result_t
additem(mysql_arglist_t * arglist,char ** s,size_t * len)339 additem(mysql_arglist_t *arglist, char **s, size_t *len) {
340 	mysql_arg_t *item;
341 
342 	item = malloc(sizeof(*item));
343 	if (item == NULL) {
344 		return (ISC_R_NOMEMORY);
345 	}
346 
347 	DLZ_LINK_INIT(item, link);
348 	item->arg = *s;
349 	*len += strlen(*s);
350 	DLZ_LIST_APPEND(*arglist, item, link);
351 	*s = NULL;
352 
353 	return (ISC_R_SUCCESS);
354 }
355 
356 /*
357  * Construct a query string using a variable number of arguments, and
358  * save it into newly allocated memory.
359  *
360  * NOTE: 'command' resembles a printf-style format string, but ONLY
361  * supports the "%s" directive with no modifiers of any kind.
362  *
363  * If 'dbi' is NULL, we attempt to get a temporary database connection;
364  * otherwise we use the existing one.
365  */
366 static char *
build_query(mysql_data_t * state,mysql_instance_t * dbi,const char * command,...)367 build_query(mysql_data_t *state, mysql_instance_t *dbi, const char *command,
368 	    ...) {
369 	isc_result_t result;
370 	bool localdbi = false;
371 	mysql_arglist_t arglist;
372 	mysql_arg_t *item;
373 	char *p, *q, *tmp = NULL, *querystr = NULL;
374 	char *query = NULL;
375 	size_t len = 0;
376 	va_list ap1;
377 
378 	/* Get a DB instance if needed */
379 	if (dbi == NULL) {
380 		dbi = get_dbi(state);
381 		if (dbi == NULL) {
382 			return (NULL);
383 		}
384 		localdbi = true;
385 	}
386 
387 	/* Make sure this instance is connected */
388 	if (!db_connect(state, dbi)) {
389 		goto fail;
390 	}
391 
392 	va_start(ap1, command);
393 	DLZ_LIST_INIT(arglist);
394 	q = querystr = strdup(command);
395 	if (querystr == NULL) {
396 		goto fail;
397 	}
398 
399 	for (;;) {
400 		if (*q == '\0') {
401 			break;
402 		}
403 
404 		p = strstr(q, "%s");
405 		if (p != NULL) {
406 			*p = '\0';
407 			tmp = strdup(q);
408 			if (tmp == NULL) {
409 				goto fail;
410 			}
411 
412 			result = additem(&arglist, &tmp, &len);
413 			if (result != ISC_R_SUCCESS) {
414 				goto fail;
415 			}
416 
417 			tmp = sanitize(dbi, va_arg(ap1, const char *));
418 			if (tmp == NULL) {
419 				goto fail;
420 			}
421 
422 			result = additem(&arglist, &tmp, &len);
423 			if (result != ISC_R_SUCCESS) {
424 				goto fail;
425 			}
426 
427 			q = p + 2;
428 		} else {
429 			tmp = strdup(q);
430 			if (tmp == NULL) {
431 				goto fail;
432 			}
433 
434 			result = additem(&arglist, &tmp, &len);
435 			if (result != ISC_R_SUCCESS) {
436 				goto fail;
437 			}
438 
439 			break;
440 		}
441 	}
442 
443 	if (len == 0) {
444 		goto fail;
445 	}
446 
447 	query = malloc(len + 1);
448 	if (query == NULL) {
449 		goto fail;
450 	}
451 
452 	*query = '\0';
453 	for (item = DLZ_LIST_HEAD(arglist); item != NULL;
454 	     item = DLZ_LIST_NEXT(item, link))
455 	{
456 		if (item->arg != NULL) {
457 			strcat(query, item->arg);
458 		}
459 	}
460 
461 fail:
462 	va_end(ap1);
463 
464 	for (item = DLZ_LIST_HEAD(arglist); item != NULL;
465 	     item = DLZ_LIST_NEXT(item, link))
466 	{
467 		if (item->arg != NULL) {
468 			free(item->arg);
469 		}
470 		free(item);
471 	}
472 
473 	if (tmp != NULL) {
474 		free(tmp);
475 	}
476 	if (querystr != NULL) {
477 		free(querystr);
478 	}
479 
480 	if (dbi != NULL && localdbi) {
481 		dlz_mutex_unlock(&dbi->mutex);
482 	}
483 
484 	return (query);
485 }
486 
487 /* Does this name end in a dot? */
488 static bool
isrelative(const char * s)489 isrelative(const char *s) {
490 	if (s == NULL || s[strlen(s) - 1] == '.') {
491 		return (false);
492 	}
493 	return (true);
494 }
495 
496 /* Return a dot if 's' doesn't already end with one */
497 static inline const char *
dot(const char * s)498 dot(const char *s) {
499 	return (isrelative(s) ? "." : "");
500 }
501 
502 /*
503  * Generate a full hostname from a (presumably relative) name 'name'
504  * and a zone name 'zone'; store the result in 'dest' (which must have
505  * enough space).
506  */
507 static void
fqhn(const char * name,const char * zone,char * dest)508 fqhn(const char *name, const char *zone, char *dest) {
509 	if (dest == NULL) {
510 		return;
511 	}
512 
513 	if (strlen(name) == 0 || strcmp(name, "@") == 0) {
514 		sprintf(dest, "%s%s", zone, dot(zone));
515 	} else {
516 		if (isrelative(name)) {
517 			sprintf(dest, "%s.%s%s", name, zone, dot(zone));
518 		} else {
519 			strcpy(dest, name);
520 		}
521 	}
522 }
523 
524 /*
525  * Names are stored in relative form in ZoneData; this function
526  * removes labels matching 'zone' from the end of 'name'.
527  */
528 static char *
relname(const char * name,const char * zone)529 relname(const char *name, const char *zone) {
530 	size_t nlen, zlen;
531 	const char *p;
532 	char *new;
533 
534 	new = (char *)malloc(strlen(name) + 1);
535 	if (new == NULL) {
536 		return (NULL);
537 	}
538 
539 	nlen = strlen(name);
540 	zlen = strlen(zone);
541 
542 	if (nlen < zlen) {
543 		strcpy(new, name);
544 		return (new);
545 	} else if (nlen == zlen || strcasecmp(name, zone) == 0) {
546 		strcpy(new, "@");
547 		return (new);
548 	}
549 
550 	p = name + nlen - zlen;
551 	if (strcasecmp(p, zone) != 0 &&
552 	    (zone[zlen - 1] != '.' || strncasecmp(p, zone, zlen - 1) != 0))
553 	{
554 		strcpy(new, name);
555 		return (new);
556 	}
557 
558 	strncpy(new, name, nlen - zlen);
559 	new[nlen - zlen - 1] = '\0';
560 	return (new);
561 }
562 
563 static isc_result_t
validate_txn(mysql_data_t * state,mysql_transaction_t * txn)564 validate_txn(mysql_data_t *state, mysql_transaction_t *txn) {
565 	isc_result_t result = ISC_R_FAILURE;
566 	mysql_transaction_t *txp;
567 
568 	dlz_mutex_lock(&state->tx_mutex);
569 	for (txp = state->transactions; txp != NULL; txp = txp->next) {
570 		if (txn == txp) {
571 			result = ISC_R_SUCCESS;
572 			break;
573 		}
574 	}
575 	dlz_mutex_unlock(&state->tx_mutex);
576 
577 	if (result != ISC_R_SUCCESS && state->log != NULL) {
578 		state->log(ISC_LOG_ERROR, "%s: invalid txn %x", modname, txn);
579 	}
580 
581 	return (result);
582 }
583 
584 static isc_result_t
db_execute(mysql_data_t * state,mysql_instance_t * dbi,const char * query)585 db_execute(mysql_data_t *state, mysql_instance_t *dbi, const char *query) {
586 	int ret;
587 
588 	/* Make sure this instance is connected.  */
589 	if (!db_connect(state, dbi)) {
590 		return (ISC_R_FAILURE);
591 	}
592 
593 	ret = mysql_real_query(dbi->sock, query, strlen(query));
594 	if (ret != 0) {
595 		if (state->debug && state->log != NULL) {
596 			state->log(ISC_LOG_ERROR, "%s: query '%s' failed: %s",
597 				   modname, query, mysql_error(dbi->sock));
598 		}
599 		return (ISC_R_FAILURE);
600 	}
601 
602 	if (state->debug && state->log != NULL) {
603 		state->log(ISC_LOG_INFO, "%s: execute(%d) %s", modname, dbi->id,
604 			   query);
605 	}
606 
607 	return (ISC_R_SUCCESS);
608 }
609 
610 static MYSQL_RES *
db_query(mysql_data_t * state,mysql_instance_t * dbi,const char * query)611 db_query(mysql_data_t *state, mysql_instance_t *dbi, const char *query) {
612 	isc_result_t result;
613 	bool localdbi = false;
614 	MYSQL_RES *res = NULL;
615 
616 	if (query == NULL) {
617 		return (NULL);
618 	}
619 
620 	/* Get a DB instance if needed */
621 	if (dbi == NULL) {
622 		dbi = get_dbi(state);
623 		if (dbi == NULL) {
624 			return (NULL);
625 		}
626 		localdbi = true;
627 	}
628 
629 	/* Make sure this instance is connected */
630 	if (!db_connect(state, dbi)) {
631 		goto fail;
632 	}
633 
634 	result = db_execute(state, dbi, query);
635 	if (result != ISC_R_SUCCESS) {
636 		goto fail;
637 	}
638 
639 	res = mysql_store_result(dbi->sock);
640 	if (res == NULL) {
641 		if (state->log != NULL) {
642 			state->log(ISC_LOG_ERROR,
643 				   "%s: unable to store result: %s", modname,
644 				   mysql_error(dbi->sock));
645 		}
646 		goto fail;
647 	}
648 
649 	if (state->debug && state->log != NULL) {
650 		state->log(ISC_LOG_INFO, "%s: query(%d) returned %d rows",
651 			   modname, dbi->id, mysql_num_rows(res));
652 	}
653 
654 fail:
655 	if (dbi != NULL && localdbi) {
656 		dlz_mutex_unlock(&dbi->mutex);
657 	}
658 	return (res);
659 }
660 
661 /*
662  * Generate a DNS NOTIFY packet:
663  * 12 bytes header
664  * Question (1)
665  * 	strlen(zone) +2
666  *  2 bytes qtype
667  *  2 bytes qclass
668  *
669  * -> 18 bytes + strlen(zone)
670  *
671  * N.B. Need to be mindful of byte ordering; using htons to map 16bit
672  * values to the 'on the wire' packet values.
673  */
674 static unsigned char *
make_notify(const char * zone,int * packetlen)675 make_notify(const char *zone, int *packetlen) {
676 	int i, j;
677 	unsigned char *packet = (unsigned char *)malloc(strlen(zone) + 18);
678 
679 	if (packet == NULL) {
680 		return (NULL);
681 	}
682 
683 	*packetlen = strlen(zone) + 18;
684 	memset(packet, 0, *packetlen);
685 
686 	/* Random query ID */
687 	i = rand();
688 	packet[0] = htons(i) & 0xff;
689 	packet[1] = htons(i) >> 8;
690 
691 	/* Flags (OpCode '4' in bits 14-11), Auth Answer set in bit 10 */
692 	i = 0x2400;
693 	packet[2] = htons(i) & 0xff;
694 	packet[3] = htons(i) >> 8;
695 
696 	/* QD Count */
697 	i = 0x1;
698 	packet[4] = htons(i) & 0xff;
699 	packet[5] = htons(i) >> 8;
700 
701 	/* Question */
702 	packet[12] = '.';
703 	memmove(&packet[13], zone, strlen(zone));
704 	packet[13 + strlen(zone)] = 0;
705 
706 	/* Make the question into labels */
707 	j = 12;
708 	while (packet[j]) {
709 		for (i = j + 1; packet[i] != '\0' && packet[i] != '.'; i++)
710 			;
711 		packet[j] = i - j - 1;
712 		j = i;
713 	}
714 
715 	/* Question type */
716 	i = 6;
717 	packet[j + 1] = htons(i) & 0xff;
718 	packet[j + 2] = htons(i) >> 8;
719 
720 	/* Queston class */
721 	i = 1;
722 	packet[j + 3] = htons(i) & 0xff;
723 	packet[j + 4] = htons(i) >> 8;
724 
725 	return (packet);
726 }
727 
728 static void
send_notify(struct sockaddr_in * addr,const unsigned char * p,const int plen)729 send_notify(struct sockaddr_in *addr, const unsigned char *p, const int plen) {
730 	int s;
731 
732 	addr->sin_family = AF_INET;
733 	addr->sin_port = htons(53);
734 
735 	if ((s = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
736 		return;
737 	}
738 
739 	sendto(s, p, plen, 0, (struct sockaddr *)addr, sizeof(*addr));
740 	close(s);
741 	return;
742 }
743 
744 /*
745  * Generate and send a DNS NOTIFY packet
746  */
747 static void
notify(mysql_data_t * state,const char * zone,int sn)748 notify(mysql_data_t *state, const char *zone, int sn) {
749 	MYSQL_RES *res;
750 	MYSQL_ROW row;
751 	char *query;
752 	unsigned char *packet;
753 	int packetlen;
754 	struct ifaddrs *ifap, *ifa;
755 	char zaddr[INET_ADDRSTRLEN];
756 	void *addrp = NULL;
757 
758 	/* Get the name servers from the NS rrset */
759 	query = build_query(state, NULL, Q_GETNS, zone);
760 	res = db_query(state, NULL, query);
761 	free(query);
762 	if (res == NULL) {
763 		return;
764 	}
765 
766 	/* Create a DNS NOTIFY packet */
767 	packet = make_notify(zone, &packetlen);
768 	if (packet == NULL) {
769 		mysql_free_result(res);
770 		return;
771 	}
772 
773 	/* Get a list of our own addresses */
774 	if (getifaddrs(&ifap) < 0) {
775 		ifap = NULL;
776 	}
777 
778 	/* Tell each nameserver of the update */
779 	while ((row = mysql_fetch_row(res)) != NULL) {
780 		bool local = false;
781 		struct hostent *h;
782 		struct sockaddr_in addr, *sin;
783 
784 		/*
785 		 * Put nameserver rdata through gethostbyname as it
786 		 * might be an IP address or a hostname. (XXX: switch
787 		 * this to inet_pton/getaddrinfo.)
788 		 */
789 		h = gethostbyname(row[0]);
790 		if (h == NULL) {
791 			continue;
792 		}
793 
794 		memmove(&addr.sin_addr, h->h_addr, h->h_length);
795 		addrp = &addr.sin_addr;
796 
797 		/* Get the address for the nameserver into a string */
798 		inet_ntop(AF_INET, addrp, zaddr, INET_ADDRSTRLEN);
799 		for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) {
800 			char ifaddr[INET_ADDRSTRLEN];
801 
802 			if (ifa->ifa_addr->sa_family != AF_INET) {
803 				continue;
804 			}
805 
806 			/* Get local address into a string */
807 			sin = (struct sockaddr_in *)ifa->ifa_addr;
808 			addrp = &sin->sin_addr;
809 			inet_ntop(AF_INET, addrp, ifaddr, INET_ADDRSTRLEN);
810 
811 			/* See if nameserver address matches this one */
812 			if (strcmp(ifaddr, zaddr) == 0) {
813 				local = true;
814 			}
815 		}
816 
817 		if (!local) {
818 			if (state->log != NULL) {
819 				state->log(ISC_LOG_INFO,
820 					   "%s: notify %s zone %s serial %d",
821 					   modname, row[0], zone, sn);
822 			}
823 			send_notify(&addr, packet, packetlen);
824 		}
825 	}
826 
827 	mysql_free_result(res);
828 	free(packet);
829 	if (ifap != NULL) {
830 		freeifaddrs(ifap);
831 	}
832 }
833 
834 /*
835  * Constructs a mysql_record_t structure from 'rdatastr', to be
836  * used in the dlz_{add,sub,del}rdataset functions below.
837  */
838 static mysql_record_t *
makerecord(mysql_data_t * state,const char * name,const char * rdatastr)839 makerecord(mysql_data_t *state, const char *name, const char *rdatastr) {
840 	mysql_record_t *new_record;
841 	char *real_name = NULL, *dclass = NULL, *type = NULL;
842 	char *data = NULL, *ttlstr = NULL, *buf = NULL;
843 	char *saveptr = NULL;
844 	dns_ttl_t ttlvalue;
845 
846 	new_record = (mysql_record_t *)malloc(sizeof(mysql_record_t));
847 
848 	if (new_record == NULL) {
849 		if (state->log != NULL) {
850 			state->log(ISC_LOG_ERROR,
851 				   "%s: makerecord - unable to malloc",
852 				   modname);
853 		}
854 		return (NULL);
855 	}
856 
857 	buf = strdup(rdatastr);
858 	if (buf == NULL) {
859 		if (state->log != NULL) {
860 			state->log(ISC_LOG_ERROR,
861 				   "%s: makerecord - unable to malloc",
862 				   modname);
863 		}
864 		free(new_record);
865 		return (NULL);
866 	}
867 
868 	/*
869 	 * The format is:
870 	 * FULLNAME\tTTL\tDCLASS\tTYPE\tDATA
871 	 *
872 	 * The DATA field is space separated, and is in the data format
873 	 * for the type used by dig
874 	 */
875 	real_name = strtok_r(buf, "\t", &saveptr);
876 	if (real_name == NULL) {
877 		goto error;
878 	}
879 
880 	ttlstr = strtok_r(NULL, "\t", &saveptr);
881 	if (ttlstr == NULL || sscanf(ttlstr, "%d", &ttlvalue) != 1) {
882 		goto error;
883 	}
884 
885 	dclass = strtok_r(NULL, "\t", &saveptr);
886 	if (dclass == NULL) {
887 		goto error;
888 	}
889 
890 	type = strtok_r(NULL, "\t", &saveptr);
891 	if (type == NULL) {
892 		goto error;
893 	}
894 
895 	data = strtok_r(NULL, "\t", &saveptr);
896 	if (data == NULL) {
897 		goto error;
898 	}
899 
900 	strcpy(new_record->name, name);
901 	strcpy(new_record->type, type);
902 	strcpy(new_record->data, data);
903 	sprintf(new_record->ttl, "%d", ttlvalue);
904 
905 	free(buf);
906 	return (new_record);
907 
908 error:
909 	free(buf);
910 	free(new_record);
911 	return (NULL);
912 }
913 
914 /*
915  * Remember a helper function from the bind9 dlz_dlopen driver
916  */
917 static void
b9_add_helper(mysql_data_t * state,const char * helper_name,void * ptr)918 b9_add_helper(mysql_data_t *state, const char *helper_name, void *ptr) {
919 	if (strcmp(helper_name, "log") == 0) {
920 		state->log = (log_t *)ptr;
921 	}
922 	if (strcmp(helper_name, "putrr") == 0) {
923 		state->putrr = (dns_sdlz_putrr_t *)ptr;
924 	}
925 	if (strcmp(helper_name, "putnamedrr") == 0) {
926 		state->putnamedrr = (dns_sdlz_putnamedrr_t *)ptr;
927 	}
928 	if (strcmp(helper_name, "writeable_zone") == 0) {
929 		state->writeable_zone = (dns_dlz_writeablezone_t *)ptr;
930 	}
931 }
932 
933 /*
934  * DLZ API functions
935  */
936 
937 /*
938  * Return the version of the API
939  */
940 int
dlz_version(unsigned int * flags)941 dlz_version(unsigned int *flags) {
942 	UNUSED(flags);
943 	*flags |= DNS_SDLZFLAG_THREADSAFE;
944 	return (DLZ_DLOPEN_VERSION);
945 }
946 
947 /*
948  * Called to initialize the driver
949  */
950 isc_result_t
dlz_create(const char * dlzname,unsigned int argc,char * argv[],void ** dbdata,...)951 dlz_create(const char *dlzname, unsigned int argc, char *argv[], void **dbdata,
952 	   ...) {
953 	mysql_data_t *state;
954 	const char *helper_name;
955 	va_list ap;
956 	int n;
957 
958 	UNUSED(dlzname);
959 
960 	state = calloc(1, sizeof(mysql_data_t));
961 	if (state == NULL) {
962 		return (ISC_R_NOMEMORY);
963 	}
964 
965 	dlz_mutex_init(&state->tx_mutex, NULL);
966 	state->transactions = NULL;
967 
968 	/* Fill in the helper functions */
969 	va_start(ap, dbdata);
970 	while ((helper_name = va_arg(ap, const char *)) != NULL) {
971 		b9_add_helper(state, helper_name, va_arg(ap, void *));
972 	}
973 	va_end(ap);
974 
975 	if (state->log != NULL) {
976 		state->log(ISC_LOG_INFO, "loading %s module", modname);
977 	}
978 
979 	if ((argc < 2) || (argc > 6)) {
980 		if (state->log != NULL) {
981 			state->log(ISC_LOG_ERROR,
982 				   "%s: missing args <dbname> "
983 				   "[<dbhost> [<user> <pass>]]",
984 				   modname);
985 		}
986 		dlz_destroy(state);
987 		return (ISC_R_FAILURE);
988 	}
989 
990 	state->db_name = strdup(argv[1]);
991 	if (argc > 2) {
992 		state->db_host = strdup(argv[2]);
993 		if (argc > 4) {
994 			state->db_user = strdup(argv[3]);
995 			state->db_pass = strdup(argv[4]);
996 		} else {
997 			state->db_user = strdup("bind");
998 			state->db_pass = strdup("");
999 		}
1000 	} else {
1001 		state->db_host = strdup("localhost");
1002 		state->db_user = strdup("bind");
1003 		state->db_pass = strdup("");
1004 	}
1005 
1006 	if (state->log != NULL) {
1007 		state->log(ISC_LOG_INFO, "%s: DB=%s, Host=%s, User=%s", modname,
1008 			   state->db_name, state->db_host, state->db_user);
1009 	}
1010 
1011 	/*
1012 	 * Assign the 'state' to dbdata so we get it in our callbacks
1013 	 */
1014 
1015 	dlz_mutex_lock(&state->tx_mutex);
1016 
1017 	/*
1018 	 * Populate DB instances
1019 	 */
1020 	if (mysql_thread_safe()) {
1021 		for (n = 0; n < MAX_DBI; n++) {
1022 			dlz_mutex_init(&state->db[n].mutex, NULL);
1023 			dlz_mutex_lock(&state->db[n].mutex);
1024 			state->db[n].id = n;
1025 			state->db[n].connected = 0;
1026 			state->db[n].sock = mysql_init(NULL);
1027 			mysql_options(state->db[n].sock,
1028 				      MYSQL_READ_DEFAULT_GROUP, modname);
1029 			mysql_options(state->db[n].sock, MYSQL_OPT_RECONNECT,
1030 				      &(my_bool){ 1 });
1031 			dlz_mutex_unlock(&state->db[n].mutex);
1032 		}
1033 
1034 		*dbdata = state;
1035 		dlz_mutex_unlock(&state->tx_mutex);
1036 		return (ISC_R_SUCCESS);
1037 	}
1038 
1039 	free(state->db_name);
1040 	free(state->db_host);
1041 	free(state->db_user);
1042 	free(state->db_pass);
1043 	dlz_mutex_destroy(&state->tx_mutex);
1044 	free(state);
1045 	return (ISC_R_FAILURE);
1046 }
1047 
1048 /*
1049  * Shut down the backend
1050  */
1051 void
dlz_destroy(void * dbdata)1052 dlz_destroy(void *dbdata) {
1053 	mysql_data_t *state = (mysql_data_t *)dbdata;
1054 	int i;
1055 
1056 	if (state->debug && state->log != NULL) {
1057 		state->log(ISC_LOG_INFO, "%s: shutting down", modname);
1058 	}
1059 
1060 	for (i = 0; i < MAX_DBI; i++) {
1061 		if (state->db[i].sock) {
1062 			mysql_close(state->db[i].sock);
1063 			state->db[i].sock = NULL;
1064 		}
1065 	}
1066 	free(state->db_name);
1067 	free(state->db_host);
1068 	free(state->db_user);
1069 	free(state->db_pass);
1070 	dlz_mutex_destroy(&state->tx_mutex);
1071 	free(state);
1072 }
1073 
1074 /*
1075  * See if we handle a given zone
1076  */
1077 isc_result_t
dlz_findzonedb(void * dbdata,const char * name,dns_clientinfomethods_t * methods,dns_clientinfo_t * clientinfo)1078 dlz_findzonedb(void *dbdata, const char *name, dns_clientinfomethods_t *methods,
1079 	       dns_clientinfo_t *clientinfo) {
1080 	isc_result_t result = ISC_R_SUCCESS;
1081 	mysql_data_t *state = (mysql_data_t *)dbdata;
1082 	MYSQL_RES *res;
1083 	char *query;
1084 
1085 	/* Query the Zones table to see if this zone is present */
1086 	query = build_query(state, NULL, Q_FINDZONE, name);
1087 
1088 	if (query == NULL) {
1089 		return (ISC_R_NOMEMORY);
1090 	}
1091 
1092 	res = db_query(state, NULL, query);
1093 	if (res == NULL) {
1094 		return (ISC_R_FAILURE);
1095 	}
1096 
1097 	if (mysql_num_rows(res) == 0) {
1098 		result = ISC_R_NOTFOUND;
1099 	}
1100 
1101 	mysql_free_result(res);
1102 	return (result);
1103 }
1104 
1105 /*
1106  * Perform a database lookup
1107  */
1108 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)1109 dlz_lookup(const char *zone, const char *name, void *dbdata,
1110 	   dns_sdlzlookup_t *lookup, dns_clientinfomethods_t *methods,
1111 	   dns_clientinfo_t *clientinfo) {
1112 	isc_result_t result;
1113 	mysql_data_t *state = (mysql_data_t *)dbdata;
1114 	bool found = false;
1115 	char *real_name;
1116 	MYSQL_RES *res;
1117 	MYSQL_ROW row;
1118 	char *query;
1119 	mysql_transaction_t *txn = NULL;
1120 	mysql_instance_t *dbi = NULL;
1121 
1122 	if (state->putrr == NULL) {
1123 		if (state->log != NULL) {
1124 			state->log(ISC_LOG_ERROR, "%s: dlz_lookup - no putrr",
1125 				   modname);
1126 		}
1127 		return (ISC_R_NOTIMPLEMENTED);
1128 	}
1129 
1130 	/* Are we okay to try to find the txn version?  */
1131 	if (clientinfo != NULL && clientinfo->version >= 2) {
1132 		txn = (mysql_transaction_t *)clientinfo->dbversion;
1133 		if (txn != NULL && validate_txn(state, txn) == ISC_R_SUCCESS) {
1134 			dbi = txn->dbi;
1135 		}
1136 		if (dbi != NULL) {
1137 			state->log(ISC_LOG_DEBUG(1),
1138 				   "%s: lookup in live transaction %p, DBI %p",
1139 				   modname, txn, dbi);
1140 		}
1141 	}
1142 
1143 	if (strcmp(name, "@") == 0) {
1144 		real_name = (char *)malloc(strlen(zone) + 1);
1145 		if (real_name == NULL) {
1146 			return (ISC_R_NOMEMORY);
1147 		}
1148 		strcpy(real_name, zone);
1149 	} else {
1150 		real_name = (char *)malloc(strlen(name) + 1);
1151 		if (real_name == NULL) {
1152 			return (ISC_R_NOMEMORY);
1153 		}
1154 		strcpy(real_name, name);
1155 	}
1156 
1157 	if (strcmp(real_name, zone) == 0) {
1158 		/*
1159 		 * Get the Zones table data for use in the SOA:
1160 		 * zone admin serial refresh retry expire min
1161 		 */
1162 		query = build_query(state, dbi, Q_GETSOA, zone);
1163 		if (query == NULL) {
1164 			free(real_name);
1165 			return (ISC_R_NOMEMORY);
1166 		}
1167 
1168 		res = db_query(state, dbi, query);
1169 		free(query);
1170 
1171 		if (res == NULL) {
1172 			free(real_name);
1173 			return (ISC_R_NOTFOUND);
1174 		}
1175 
1176 		while ((row = mysql_fetch_row(res)) != NULL) {
1177 			char host[1024], admin[1024], data[4096];
1178 			int ttl;
1179 
1180 			sscanf(row[7], "%d", &ttl);
1181 			fqhn(row[0], zone, host);
1182 			fqhn(row[1], zone, admin);
1183 
1184 			/* zone admin serial refresh retry expire min */
1185 			snprintf(data, sizeof(data), "%s%s %s%s %s %s %s %s %s",
1186 				 host, dot(host), admin, dot(admin), row[2],
1187 				 row[3], row[4], row[5], row[6]);
1188 
1189 			result = state->putrr(lookup, "soa", ttl, data);
1190 			if (result != ISC_R_SUCCESS) {
1191 				free(real_name);
1192 				mysql_free_result(res);
1193 				return (result);
1194 			}
1195 		}
1196 
1197 		mysql_free_result(res);
1198 
1199 		/*
1200 		 *  Now we'll get the rest of the apex data
1201 		 */
1202 		query = build_query(state, dbi, Q_GETAPEX, zone, real_name);
1203 	} else {
1204 		query = build_query(state, dbi, Q_GETNODE, zone, real_name);
1205 	}
1206 
1207 	res = db_query(state, dbi, query);
1208 	free(query);
1209 
1210 	if (res == NULL) {
1211 		free(real_name);
1212 		return (ISC_R_NOTFOUND);
1213 	}
1214 
1215 	while ((row = mysql_fetch_row(res)) != NULL) {
1216 		int ttl;
1217 		sscanf(row[2], "%d", &ttl);
1218 		result = state->putrr(lookup, row[0], ttl, row[1]);
1219 		if (result != ISC_R_SUCCESS) {
1220 			free(real_name);
1221 			mysql_free_result(res);
1222 			return (result);
1223 		}
1224 
1225 		found = true;
1226 	}
1227 
1228 	if (state->debug && state->log != NULL) {
1229 		state->log(ISC_LOG_INFO, "%s: dlz_lookup %s/%s/%s - (%d rows)",
1230 			   modname, name, real_name, zone, mysql_num_rows(res));
1231 	}
1232 
1233 	mysql_free_result(res);
1234 	free(real_name);
1235 
1236 	if (!found) {
1237 		return (ISC_R_NOTFOUND);
1238 	}
1239 
1240 	return (ISC_R_SUCCESS);
1241 }
1242 
1243 /*
1244  * See if a zone transfer is allowed
1245  */
1246 isc_result_t
dlz_allowzonexfr(void * dbdata,const char * name,const char * client)1247 dlz_allowzonexfr(void *dbdata, const char *name, const char *client) {
1248 	mysql_data_t *state = (mysql_data_t *)dbdata;
1249 
1250 	if (state->debug && state->log != NULL) {
1251 		state->log(ISC_LOG_INFO, "dlz_allowzonexfr: %s %s", name,
1252 			   client);
1253 	}
1254 
1255 	/* Just say yes for all our zones */
1256 	return (dlz_findzonedb(dbdata, name, NULL, NULL));
1257 }
1258 
1259 /*
1260  * Perform a zone transfer
1261  */
1262 isc_result_t
dlz_allnodes(const char * zone,void * dbdata,dns_sdlzallnodes_t * allnodes)1263 dlz_allnodes(const char *zone, void *dbdata, dns_sdlzallnodes_t *allnodes) {
1264 	isc_result_t result = ISC_R_SUCCESS;
1265 	mysql_data_t *state = (mysql_data_t *)dbdata;
1266 	MYSQL_RES *res;
1267 	MYSQL_ROW row;
1268 	char *query;
1269 
1270 	UNUSED(zone);
1271 
1272 	if (state->debug && (state->log != NULL)) {
1273 		state->log(ISC_LOG_INFO, "dlz_allnodes: %s", zone);
1274 	}
1275 
1276 	if (state->putnamedrr == NULL) {
1277 		return (ISC_R_NOTIMPLEMENTED);
1278 	}
1279 
1280 	/*
1281 	 * Get all the ZoneData for this zone
1282 	 */
1283 	query = build_query(state, NULL, Q_GETALL, zone);
1284 	if (query == NULL) {
1285 		return (ISC_R_NOMEMORY);
1286 	}
1287 
1288 	res = db_query(state, NULL, query);
1289 	free(query);
1290 	if (res == NULL) {
1291 		return (ISC_R_NOTFOUND);
1292 	}
1293 
1294 	while ((row = mysql_fetch_row(res)) != NULL) {
1295 		char hostname[1024];
1296 		int ttl;
1297 
1298 		sscanf(row[3], "%d", &ttl);
1299 		fqhn(row[0], zone, hostname);
1300 		result = state->putnamedrr(allnodes, hostname, row[1], ttl,
1301 					   row[2]);
1302 		if (result != ISC_R_SUCCESS) {
1303 			break;
1304 		}
1305 	}
1306 
1307 	mysql_free_result(res);
1308 	return (result);
1309 }
1310 
1311 /*
1312  * Start a transaction
1313  */
1314 isc_result_t
dlz_newversion(const char * zone,void * dbdata,void ** versionp)1315 dlz_newversion(const char *zone, void *dbdata, void **versionp) {
1316 	isc_result_t result = ISC_R_FAILURE;
1317 	mysql_data_t *state = (mysql_data_t *)dbdata;
1318 	MYSQL_RES *res;
1319 	MYSQL_ROW row;
1320 	char *query;
1321 	char zone_id[16];
1322 	mysql_transaction_t *txn = NULL, *newtx = NULL;
1323 
1324 	/*
1325 	 * Check Zone is writable
1326 	 */
1327 	query = build_query(state, NULL, Q_WRITEABLE, zone);
1328 	if (query == NULL) {
1329 		return (ISC_R_NOMEMORY);
1330 	}
1331 
1332 	res = db_query(state, NULL, query);
1333 	free(query);
1334 	if (res == NULL) {
1335 		return (ISC_R_FAILURE);
1336 	}
1337 
1338 	if ((row = mysql_fetch_row(res)) == NULL) {
1339 		mysql_free_result(res);
1340 		return (ISC_R_FAILURE);
1341 	}
1342 
1343 	strcpy(zone_id, row[0]);
1344 	mysql_free_result(res);
1345 
1346 	/*
1347 	 * See if we already have a transaction for this zone
1348 	 */
1349 	dlz_mutex_lock(&state->tx_mutex);
1350 	for (txn = state->transactions; txn != NULL; txn = txn->next) {
1351 		if (strcmp(txn->zone, zone) == 0) {
1352 			if (state->log != NULL) {
1353 				state->log(ISC_LOG_ERROR,
1354 					   "%s: transaction already "
1355 					   "started for zone %s",
1356 					   modname, zone);
1357 			}
1358 			dlz_mutex_unlock(&state->tx_mutex);
1359 			return (ISC_R_FAILURE);
1360 		}
1361 	}
1362 
1363 	/*
1364 	 * Create new transaction
1365 	 */
1366 	newtx = (mysql_transaction_t *)calloc(1, sizeof(mysql_transaction_t));
1367 	if (newtx == NULL) {
1368 		result = ISC_R_NOMEMORY;
1369 		goto cleanup;
1370 	}
1371 	newtx->zone = strdup(zone);
1372 	if (newtx->zone == NULL) {
1373 		result = ISC_R_NOMEMORY;
1374 		goto cleanup;
1375 	}
1376 	newtx->zone_id = strdup(zone_id);
1377 	if (newtx->zone_id == NULL) {
1378 		result = ISC_R_NOMEMORY;
1379 		goto cleanup;
1380 	}
1381 	newtx->dbi = get_dbi(state);
1382 	newtx->next = NULL;
1383 
1384 	if (newtx->dbi == NULL) {
1385 		result = ISC_R_FAILURE;
1386 		goto cleanup;
1387 	}
1388 
1389 	result = db_execute(state, newtx->dbi, "START TRANSACTION");
1390 	if (result != ISC_R_SUCCESS) {
1391 		dlz_mutex_unlock(&newtx->dbi->mutex);
1392 		goto cleanup;
1393 	}
1394 
1395 	/*
1396 	 * Add this tx to front of list
1397 	 */
1398 	newtx->next = state->transactions;
1399 	state->transactions = newtx;
1400 
1401 	if (state->debug && (state->log != NULL)) {
1402 		state->log(ISC_LOG_INFO, "%s: New tx %x", modname, newtx);
1403 	}
1404 
1405 cleanup:
1406 	dlz_mutex_unlock(&state->tx_mutex);
1407 	if (result == ISC_R_SUCCESS) {
1408 		*versionp = (void *)newtx;
1409 	} else {
1410 		dlz_mutex_unlock(&state->tx_mutex);
1411 		if (newtx != NULL) {
1412 			if (newtx->zone != NULL) {
1413 				free(newtx->zone);
1414 			}
1415 			if (newtx->zone != NULL) {
1416 				free(newtx->zone_id);
1417 			}
1418 			free(newtx);
1419 		}
1420 	}
1421 
1422 	return (result);
1423 }
1424 
1425 /*
1426  * End a transaction
1427  */
1428 void
dlz_closeversion(const char * zone,bool commit,void * dbdata,void ** versionp)1429 dlz_closeversion(const char *zone, bool commit, void *dbdata, void **versionp) {
1430 	isc_result_t result;
1431 	mysql_data_t *state = (mysql_data_t *)dbdata;
1432 	mysql_transaction_t *txn = (mysql_transaction_t *)*versionp;
1433 	mysql_transaction_t *txp;
1434 	char *query;
1435 	MYSQL_RES *res;
1436 	MYSQL_ROW row;
1437 
1438 	/*
1439 	 * Find the transaction
1440 	 */
1441 	dlz_mutex_lock(&state->tx_mutex);
1442 	if (state->transactions == txn) {
1443 		/* Tx is first in list; remove it. */
1444 		state->transactions = txn->next;
1445 	} else {
1446 		txp = state->transactions;
1447 		while (txp != NULL) {
1448 			if (txp->next != NULL) {
1449 				if (txp->next == txn) {
1450 					txp->next = txn->next;
1451 					break;
1452 				}
1453 			}
1454 			if (txp == txn) {
1455 				txp = txn->next;
1456 				break;
1457 			}
1458 			txp = txp->next;
1459 		}
1460 	}
1461 
1462 	/*
1463 	 * Tidy up
1464 	 */
1465 	dlz_mutex_unlock(&state->tx_mutex);
1466 	*versionp = NULL;
1467 
1468 	if (commit) {
1469 		int oldsn = 0, newsn = 0;
1470 
1471 		/*
1472 		 * Find out the serial number of the zone out with the
1473 		 * transaction so we can see if it has incremented or not
1474 		 */
1475 		query = build_query(state, txn->dbi, Q_GETSERIAL, zone);
1476 		if (query == NULL && state->log != NULL) {
1477 			state->log(ISC_LOG_ERROR,
1478 				   "%s: unable to commit transaction %x "
1479 				   "on zone %s: no memory",
1480 				   modname, txn, zone);
1481 			return;
1482 		}
1483 
1484 		res = db_query(state, txn->dbi, query);
1485 		if (res != NULL) {
1486 			while ((row = mysql_fetch_row(res)) != NULL) {
1487 				sscanf(row[0], "%d", &oldsn);
1488 			}
1489 			mysql_free_result(res);
1490 		}
1491 
1492 		/*
1493 		 * Commit the transaction to the database
1494 		 */
1495 		result = db_execute(state, txn->dbi, "COMMIT");
1496 		if (result != ISC_R_SUCCESS && state->log != NULL) {
1497 			state->log(ISC_LOG_INFO,
1498 				   "%s: (%x) commit transaction on zone %s",
1499 				   modname, txn, zone);
1500 			return;
1501 		}
1502 
1503 		if (state->debug && state->log != NULL) {
1504 			state->log(ISC_LOG_INFO,
1505 				   "%s: (%x) committing transaction "
1506 				   "on zone %s",
1507 				   modname, txn, zone);
1508 		}
1509 
1510 		/*
1511 		 * Now get the serial number again
1512 		 */
1513 		query = build_query(state, txn->dbi, Q_GETSERIAL, zone);
1514 		res = db_query(state, txn->dbi, query);
1515 		free(query);
1516 
1517 		if (res != NULL) {
1518 			while ((row = mysql_fetch_row(res)) != NULL) {
1519 				sscanf(row[0], "%d", &newsn);
1520 			}
1521 			mysql_free_result(res);
1522 		}
1523 
1524 		/*
1525 		 * Look to see if serial numbers have changed
1526 		 */
1527 		if (newsn > oldsn) {
1528 			notify(state, zone, newsn);
1529 		}
1530 	} else {
1531 		result = db_execute(state, txn->dbi, "ROLLBACK");
1532 		if (state->debug && (state->log != NULL)) {
1533 			state->log(ISC_LOG_INFO,
1534 				   "%s: (%x) roll back transaction on zone %s",
1535 				   modname, txn, zone);
1536 		}
1537 	}
1538 
1539 	/*
1540 	 * Unlock the mutex for this txn
1541 	 */
1542 	dlz_mutex_unlock(&txn->dbi->mutex);
1543 
1544 	/*
1545 	 * Free up other structures
1546 	 */
1547 	free(txn->zone);
1548 	free(txn->zone_id);
1549 	free(txn);
1550 }
1551 
1552 /*
1553  * Configure a writeable zone
1554  */
1555 #if DLZ_DLOPEN_VERSION < 3
1556 isc_result_t
dlz_configure(dns_view_t * view,void * dbdata)1557 dlz_configure(dns_view_t *view, void *dbdata)
1558 #else  /* DLZ_DLOPEN_VERSION >= 3 */
1559 isc_result_t
1560 dlz_configure(dns_view_t *view, dns_dlzdb_t *dlzdb, void *dbdata)
1561 #endif /* DLZ_DLOPEN_VERSION */
1562 {
1563 	mysql_data_t *state = (mysql_data_t *)dbdata;
1564 	isc_result_t result;
1565 	MYSQL_RES *res;
1566 	MYSQL_ROW row;
1567 	int count;
1568 
1569 	/*
1570 	 * Seed PRNG (used by Notify code)
1571 	 */
1572 	srand(getpid());
1573 
1574 	if (state->debug && state->log != NULL) {
1575 		state->log(ISC_LOG_INFO, "%s: dlz_confgure", modname);
1576 	}
1577 
1578 	if (state->writeable_zone == NULL) {
1579 		if (state->log != NULL) {
1580 			state->log(ISC_LOG_ERROR,
1581 				   "%s: no writeable_zone method available",
1582 				   modname);
1583 		}
1584 		return (ISC_R_FAILURE);
1585 	}
1586 
1587 	/*
1588 	 * Get a list of Zones (ignore writeable column at this point)
1589 	 */
1590 	res = db_query(state, NULL, Q_GETZONES);
1591 	if (res == NULL) {
1592 		return (ISC_R_FAILURE);
1593 	}
1594 
1595 	count = 0;
1596 	while ((row = mysql_fetch_row(res)) != NULL) {
1597 		int sn;
1598 		sscanf(row[1], "%d", &sn);
1599 		notify(state, row[0], sn);
1600 		result = state->writeable_zone(view,
1601 #if DLZ_DLOPEN_VERSION >= 3
1602 					       dlzdb,
1603 #endif /* if DLZ_DLOPEN_VERSION >= 3 */
1604 					       row[0]);
1605 		if (result != ISC_R_SUCCESS) {
1606 			if (state->log != NULL) {
1607 				state->log(ISC_LOG_ERROR,
1608 					   "%s: failed to configure zone %s",
1609 					   modname, row[0]);
1610 			}
1611 			mysql_free_result(res);
1612 			return (result);
1613 		}
1614 		count++;
1615 	}
1616 	mysql_free_result(res);
1617 
1618 	if (state->debug && state->log != NULL) {
1619 		state->log(ISC_LOG_INFO, "%s: configured %d zones", modname,
1620 			   count);
1621 	}
1622 	return (ISC_R_SUCCESS);
1623 }
1624 
1625 /*
1626  * Authorize a zone update
1627  */
1628 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)1629 dlz_ssumatch(const char *signer, const char *name, const char *tcpaddr,
1630 	     const char *type, const char *key, uint32_t keydatalen,
1631 	     unsigned char *keydata, void *dbdata) {
1632 	mysql_data_t *state = (mysql_data_t *)dbdata;
1633 
1634 	UNUSED(tcpaddr);
1635 	UNUSED(type);
1636 	UNUSED(keydatalen);
1637 	UNUSED(keydata);
1638 	UNUSED(key);
1639 
1640 	if (state->debug && state->log != NULL) {
1641 		state->log(ISC_LOG_INFO, "%s: allowing update of %s by key %s",
1642 			   modname, name, signer);
1643 	}
1644 	return (true);
1645 }
1646 
1647 isc_result_t
dlz_addrdataset(const char * name,const char * rdatastr,void * dbdata,void * version)1648 dlz_addrdataset(const char *name, const char *rdatastr, void *dbdata,
1649 		void *version) {
1650 	mysql_data_t *state = (mysql_data_t *)dbdata;
1651 	mysql_transaction_t *txn = (mysql_transaction_t *)version;
1652 	char *new_name, *query;
1653 	mysql_record_t *record;
1654 	isc_result_t result;
1655 
1656 	if (txn == NULL) {
1657 		return (ISC_R_FAILURE);
1658 	}
1659 
1660 	new_name = relname(name, txn->zone);
1661 	if (new_name == NULL) {
1662 		return (ISC_R_NOMEMORY);
1663 	}
1664 
1665 	if (state->debug && (state->log != NULL)) {
1666 		state->log(ISC_LOG_INFO, "%s: add (%x) %s (as %s) %s", modname,
1667 			   version, name, new_name, rdatastr);
1668 	}
1669 
1670 	record = makerecord(state, new_name, rdatastr);
1671 	free(new_name);
1672 	if (record == NULL) {
1673 		return (ISC_R_FAILURE);
1674 	}
1675 
1676 	/* Write out data to database */
1677 	if (strcasecmp(record->type, "SOA") != 0) {
1678 		query = build_query(state, txn->dbi, I_DATA, txn->zone_id,
1679 				    record->name, record->type, record->data,
1680 				    record->ttl);
1681 		if (query == NULL) {
1682 			result = ISC_R_FAILURE;
1683 			goto cleanup;
1684 		}
1685 		result = db_execute(state, txn->dbi, query);
1686 		free(query);
1687 	} else {
1688 		/*
1689 		 * This is an SOA record, so we update: it must exist,
1690 		 * or we wouldn't have gotten this far.
1691 		 * SOA: zone admin serial refresh retry expire min
1692 		 */
1693 		char sn[32];
1694 		sscanf(record->data, "%*s %*s %31s %*s %*s %*s %*s", sn);
1695 		query = build_query(state, txn->dbi, U_SERIAL, sn,
1696 				    txn->zone_id);
1697 		if (query == NULL) {
1698 			result = ISC_R_FAILURE;
1699 			goto cleanup;
1700 		}
1701 		result = db_execute(state, txn->dbi, query);
1702 		free(query);
1703 	}
1704 
1705 cleanup:
1706 	free(record);
1707 	return (result);
1708 }
1709 
1710 isc_result_t
dlz_subrdataset(const char * name,const char * rdatastr,void * dbdata,void * version)1711 dlz_subrdataset(const char *name, const char *rdatastr, void *dbdata,
1712 		void *version) {
1713 	mysql_data_t *state = (mysql_data_t *)dbdata;
1714 	mysql_transaction_t *txn = (mysql_transaction_t *)version;
1715 	char *new_name, *query;
1716 	mysql_record_t *record;
1717 	isc_result_t result;
1718 
1719 	if (txn == NULL) {
1720 		return (ISC_R_FAILURE);
1721 	}
1722 
1723 	new_name = relname(name, txn->zone);
1724 	if (new_name == NULL) {
1725 		return (ISC_R_NOMEMORY);
1726 	}
1727 
1728 	if (state->debug && (state->log != NULL)) {
1729 		state->log(ISC_LOG_INFO, "%s: sub (%x) %s %s", modname, version,
1730 			   name, rdatastr);
1731 	}
1732 
1733 	record = makerecord(state, new_name, rdatastr);
1734 	free(new_name);
1735 	if (record == NULL) {
1736 		return (ISC_R_FAILURE);
1737 	}
1738 	/*
1739 	 * If 'type' isn't 'SOA', delete the records
1740 	 */
1741 	if (strcasecmp(record->type, "SOA") == 0) {
1742 		result = ISC_R_SUCCESS;
1743 	} else {
1744 		query = build_query(state, txn->dbi, D_RECORD, txn->zone_id,
1745 				    record->name, record->type, record->data,
1746 				    record->ttl);
1747 		if (query == NULL) {
1748 			result = ISC_R_NOMEMORY;
1749 			goto cleanup;
1750 		}
1751 
1752 		result = db_execute(state, txn->dbi, query);
1753 		free(query);
1754 	}
1755 
1756 cleanup:
1757 	free(record);
1758 	return (result);
1759 }
1760 
1761 isc_result_t
dlz_delrdataset(const char * name,const char * type,void * dbdata,void * version)1762 dlz_delrdataset(const char *name, const char *type, void *dbdata,
1763 		void *version) {
1764 	mysql_data_t *state = (mysql_data_t *)dbdata;
1765 	mysql_transaction_t *txn = (mysql_transaction_t *)version;
1766 	char *new_name, *query;
1767 	isc_result_t result;
1768 
1769 	if (txn == NULL) {
1770 		return (ISC_R_FAILURE);
1771 	}
1772 
1773 	new_name = relname(name, txn->zone);
1774 	if (new_name == NULL) {
1775 		return (ISC_R_NOMEMORY);
1776 	}
1777 
1778 	if (state->debug && (state->log != NULL)) {
1779 		state->log(ISC_LOG_INFO, "%s: del (%x) %s %s", modname, version,
1780 			   name, type);
1781 	}
1782 
1783 	query = build_query(state, txn->dbi, D_RRSET, txn->zone_id, new_name,
1784 			    type);
1785 	if (query == NULL) {
1786 		result = ISC_R_NOMEMORY;
1787 		goto cleanup;
1788 	}
1789 
1790 	result = db_execute(state, txn->dbi, query);
1791 	free(query);
1792 
1793 cleanup:
1794 	free(new_name);
1795 	return (result);
1796 }
1797