1 /* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
2
3 This program is free software: you can redistribute it and/or modify
4 it under the terms of the GNU General Public License as published by
5 the Free Software Foundation, either version 3 of the License, or
6 (at your option) any later version.
7
8 This program is distributed in the hope that it will be useful,
9 but WITHOUT ANY WARRANTY; without even the implied warranty of
10 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License
14 along with this program. If not, see <https://www.gnu.org/licenses/>.
15 */
16
17 #include "contrib/ctype.h"
18 #include "contrib/macros.h"
19 #include "contrib/net.h"
20 #include "contrib/sockaddr.h"
21 #include "contrib/wire_ctx.h"
22 #include "knot/include/module.h"
23
24 #define MOD_NET "\x07""network"
25 #define MOD_ORIGIN "\x06""origin"
26 #define MOD_PREFIX "\x06""prefix"
27 #define MOD_TTL "\x03""ttl"
28 #define MOD_TYPE "\x04""type"
29 #define MOD_SHORT "\x0d""reverse-short"
30
31 /*! \brief Supported answer synthesis template types. */
32 enum synth_template_type {
33 SYNTH_NULL = 0,
34 SYNTH_FORWARD = 1,
35 SYNTH_REVERSE = 2
36 };
37
38 static const knot_lookup_t synthetic_types[] = {
39 { SYNTH_FORWARD, "forward" },
40 { SYNTH_REVERSE, "reverse" },
41 { 0, NULL }
42 };
43
check_prefix(knotd_conf_check_args_t * args)44 int check_prefix(knotd_conf_check_args_t *args)
45 {
46 if (strchr((const char *)args->data, '.') != NULL) {
47 args->err_str = "dot '.' is not allowed";
48 return KNOT_EINVAL;
49 }
50
51 return KNOT_EOK;
52 }
53
54 const yp_item_t synth_record_conf[] = {
55 { MOD_TYPE, YP_TOPT, YP_VOPT = { synthetic_types, SYNTH_NULL } },
56 { MOD_PREFIX, YP_TSTR, YP_VSTR = { "" }, YP_FNONE, { check_prefix } },
57 { MOD_ORIGIN, YP_TDNAME, YP_VNONE },
58 { MOD_TTL, YP_TINT, YP_VINT = { 0, UINT32_MAX, 3600, YP_STIME } },
59 { MOD_NET, YP_TNET, YP_VNONE, YP_FMULTI },
60 { MOD_SHORT, YP_TBOOL, YP_VBOOL = { true } },
61 { NULL }
62 };
63
synth_record_conf_check(knotd_conf_check_args_t * args)64 int synth_record_conf_check(knotd_conf_check_args_t *args)
65 {
66 // Check type.
67 knotd_conf_t type = knotd_conf_check_item(args, MOD_TYPE);
68 if (type.count == 0) {
69 args->err_str = "no synthesis type specified";
70 return KNOT_EINVAL;
71 }
72
73 // Check origin.
74 knotd_conf_t origin = knotd_conf_check_item(args, MOD_ORIGIN);
75 if (origin.count == 0 && type.single.option == SYNTH_REVERSE) {
76 args->err_str = "no origin specified";
77 return KNOT_EINVAL;
78 }
79 if (origin.count != 0 && type.single.option == SYNTH_FORWARD) {
80 args->err_str = "origin not allowed with forward type";
81 return KNOT_EINVAL;
82 }
83
84 // Check network subnet.
85 knotd_conf_t net = knotd_conf_check_item(args, MOD_NET);
86 if (net.count == 0) {
87 args->err_str = "no network subnet specified";
88 return KNOT_EINVAL;
89 }
90 knotd_conf_free(&net);
91
92 // Check reverse-short parameter is only for reverse synthrecord.
93 knotd_conf_t reverse_short = knotd_conf_check_item(args, MOD_SHORT);
94 if (reverse_short.count != 0 && type.single.option == SYNTH_FORWARD) {
95 args->err_str = "reverse-short not allowed with forward type";
96 return KNOT_EINVAL;
97 }
98
99 return KNOT_EOK;
100 }
101
102 #define ARPA_ZONE_LABELS 2
103 #define IPV4_ADDR_LABELS 4
104 #define IPV6_ADDR_LABELS 32
105 #define IPV4_ARPA_DNAME (uint8_t *)"\x07""in-addr""\x04""arpa"
106 #define IPV6_ARPA_DNAME (uint8_t *)"\x03""ip6""\x04""arpa"
107 #define IPV4_ARPA_LEN 14
108 #define IPV6_ARPA_LEN 10
109
110 /*!
111 * \brief Synthetic response template.
112 */
113 typedef struct {
114 struct sockaddr_storage addr;
115 struct sockaddr_storage addr_max;
116 int addr_mask;
117 } synth_templ_addr_t;
118
119 typedef struct {
120 enum synth_template_type type;
121 char *prefix;
122 size_t prefix_len;
123 char *zone;
124 size_t zone_len;
125 uint32_t ttl;
126 size_t addr_count;
127 synth_templ_addr_t *addr;
128 bool reverse_short;
129 } synth_template_t;
130
131 typedef union {
132 uint32_t b32;
133 uint8_t b4[4];
134 } addr_block_t;
135
136 /*! \brief Write one IPV4 address block without redundant leading zeros. */
block_write(addr_block_t * block,char * addr_str)137 static unsigned block_write(addr_block_t *block, char *addr_str)
138 {
139 unsigned len = 0;
140
141 if (block->b4[0] != '0') {
142 addr_str[len++] = block->b4[0];
143 }
144 if (len > 0 || block->b4[1] != '0') {
145 addr_str[len++] = block->b4[1];
146 }
147 if (len > 0 || block->b4[2] != '0') {
148 addr_str[len++] = block->b4[2];
149 }
150 addr_str[len++] = block->b4[3];
151
152 return len;
153 }
154
155 /*! \brief Substitute all occurrences of given character. */
str_subst(char * str,size_t len,char from,char to)156 static void str_subst(char *str, size_t len, char from, char to)
157 {
158 for (int i = 0; i < len; ++i) {
159 if (str[i] == from) {
160 str[i] = to;
161 }
162 }
163 }
164
165 /*! \brief Separator character for address family. */
str_separator(int addr_family)166 static char str_separator(int addr_family)
167 {
168 return (addr_family == AF_INET6) ? ':' : '.';
169 }
170
171 /*! \brief Return true if query type is satisfied with provided address family. */
query_satisfied_by_family(uint16_t qtype,int family)172 static bool query_satisfied_by_family(uint16_t qtype, int family)
173 {
174 switch (qtype) {
175 case KNOT_RRTYPE_A: return family == AF_INET;
176 case KNOT_RRTYPE_AAAA: return family == AF_INET6;
177 case KNOT_RRTYPE_ANY: return true;
178 default: return false;
179 }
180 }
181
182 /*! \brief Parse address from reverse query QNAME and return address family. */
reverse_addr_parse(knotd_qdata_t * qdata,const synth_template_t * tpl,char * addr_str,int * addr_family,bool * parent)183 static int reverse_addr_parse(knotd_qdata_t *qdata, const synth_template_t *tpl,
184 char *addr_str, int *addr_family, bool *parent)
185 {
186 /* QNAME required format is [address].[subnet/zone]
187 * f.e. [1.0...0].[h.g.f.e.0.0.0.0.d.c.b.a.ip6.arpa] represents
188 * [abcd:0:efgh::1] */
189 const knot_dname_t *label = qdata->name; // uncompressed name
190
191 static const char ipv4_zero[] = "0.0.0.0";
192
193 bool can_ipv4 = true;
194 bool can_ipv6 = true;
195 unsigned labels = 0;
196
197 uint8_t buf4[16], *buf4_end = buf4 + sizeof(buf4), *buf4_pos = buf4_end;
198 uint8_t buf6[32], *buf6_end = buf6 + sizeof(buf6), *buf6_pos = buf6_end;
199
200 for ( ; labels < IPV6_ADDR_LABELS; labels++) {
201 if (unlikely(*label == 0)) {
202 return KNOT_EINVAL;
203 }
204 if (label[1] == 'i') {
205 break;
206 }
207 if (labels < IPV4_ADDR_LABELS) {
208 switch (*label) {
209 case 1:
210 assert(buf4 + 1 < buf4_pos && buf6 < buf6_pos);
211 *--buf6_pos = label[1];
212 *--buf4_pos = label[1];
213 *--buf4_pos = '.';
214 break;
215 case 2:
216 case 3:
217 assert(buf4 + *label < buf4_pos);
218 can_ipv6 = false;
219 buf4_pos -= *label;
220 memcpy(buf4_pos, label + 1, *label);
221 *--buf4_pos = '.';
222 break;
223 default:
224 return KNOT_EINVAL;
225 }
226 } else {
227 can_ipv4 = false;
228 if (!can_ipv6 || *label != 1) {
229 return KNOT_EINVAL;
230 }
231 assert(buf6 < buf6_pos);
232 *--buf6_pos = label[1];
233
234 }
235 label += *label + sizeof(*label);
236 }
237
238 if (can_ipv4 && knot_dname_is_equal(label, IPV4_ARPA_DNAME)) {
239 *addr_family = AF_INET;
240 *parent = (labels < IPV4_ADDR_LABELS);
241 int buf4_overweight = (buf4_end - buf4_pos) - (2 * labels);
242 assert(buf4_overweight >= 0);
243 memcpy(addr_str + buf4_overweight, ipv4_zero, sizeof(ipv4_zero));
244 if (labels > 0) {
245 buf4_pos++; // skip leading '.'
246 memcpy(addr_str, buf4_pos, buf4_end - buf4_pos);
247 }
248 return KNOT_EOK;
249 } else if (can_ipv6 && knot_dname_is_equal(label, IPV6_ARPA_DNAME)) {
250 *addr_family = AF_INET6;
251 *parent = (labels < IPV6_ADDR_LABELS);
252
253 addr_block_t blocks[8] = { { 0 } };
254 int compr_start = -1, compr_end = -1;
255
256 unsigned buf6_len = buf6_end - buf6_pos;
257 memcpy(blocks, buf6_pos, buf6_len);
258 memset(((uint8_t *)blocks) + buf6_len, 0x30, sizeof(blocks) - buf6_len);
259
260 for (int i = 0; i < 8; i++) {
261 addr_block_t *block = &blocks[i];
262
263 /* The Unicode string MUST NOT contain "--" in the third and fourth
264 character positions and MUST NOT start or end with a "-".
265 So we will not compress first, second, and last address blocks
266 for simplicity. And we will not compress a single block.
267
268 i: 0 1 2 3 4 5 6 7
269 label block: H:G:F:E:D:C:B:A
270 address block: A B C D E F G H
271 compressibles: 0 0 0 0 0
272 0 0 0 0
273 0 0 0
274 0 0
275 */
276 // Check for trailing zero dual-blocks.
277 if (tpl->reverse_short && i > 1 && i < 6 &&
278 block[0].b32 == 0x30303030UL && block[1].b32 == 0x30303030UL) {
279 if (compr_start == -1) {
280 compr_start = i;
281 }
282 } else {
283 if (compr_start != -1 && compr_end == -1) {
284 compr_end = i;
285 }
286 }
287 }
288
289 // Write address blocks.
290 unsigned addr_len = 0;
291 for (int i = 0; i < 8; i++) {
292 if (compr_start == -1 || i < compr_start || i > compr_end) {
293 // Write regular address block.
294 if (tpl->reverse_short) {
295 addr_len += block_write(&blocks[i], addr_str + addr_len);
296 } else {
297 assert(sizeof(blocks[i]) == 4);
298 memcpy(addr_str + addr_len, &blocks[i], 4);
299 addr_len += 4;
300 }
301 // Write separator
302 if (i < 7) {
303 addr_str[addr_len++] = ':';
304 }
305 } else if (compr_start != -1 && compr_end == i) {
306 // Write compression double colon.
307 addr_str[addr_len++] = ':';
308 }
309 }
310 addr_str[addr_len] = '\0';
311
312 return KNOT_EOK;
313 }
314
315 return KNOT_EINVAL;
316 }
317
forward_addr_parse(knotd_qdata_t * qdata,const synth_template_t * tpl,char * addr_str,int * addr_family)318 static int forward_addr_parse(knotd_qdata_t *qdata, const synth_template_t *tpl,
319 char *addr_str, int *addr_family)
320 {
321 const knot_dname_t *label = qdata->name;
322
323 // Check for prefix mismatch.
324 if (label[0] <= tpl->prefix_len ||
325 memcmp(label + 1, tpl->prefix, tpl->prefix_len) != 0) {
326 return KNOT_EINVAL;
327 }
328
329 // Copy address part.
330 unsigned addr_len = label[0] - tpl->prefix_len;
331 memcpy(addr_str, label + 1 + tpl->prefix_len, addr_len);
332 addr_str[addr_len] = '\0';
333
334 // Determine address family.
335 unsigned hyphen_cnt = 0;
336 const char *ch = addr_str;
337 while (hyphen_cnt < 4 && ch < addr_str + addr_len) {
338 if (*ch == '-') {
339 hyphen_cnt++;
340 if (*++ch == '-') { // Check for shortened IPv6 notation.
341 hyphen_cnt = 4;
342 break;
343 }
344 }
345 ch++;
346 }
347 // Valid IPv4 address looks like A-B-C-D.
348 *addr_family = (hyphen_cnt == 3) ? AF_INET : AF_INET6;
349
350 // Restore correct address format.
351 const char sep = str_separator(*addr_family);
352 str_subst(addr_str, addr_len, '-', sep);
353
354 return KNOT_EOK;
355 }
356
addr_parse(knotd_qdata_t * qdata,const synth_template_t * tpl,char * addr_str,int * addr_family,bool * parent)357 static int addr_parse(knotd_qdata_t *qdata, const synth_template_t *tpl, char *addr_str,
358 int *addr_family, bool *parent)
359 {
360 switch (tpl->type) {
361 case SYNTH_REVERSE: return reverse_addr_parse(qdata, tpl, addr_str, addr_family, parent);
362 case SYNTH_FORWARD: return forward_addr_parse(qdata, tpl, addr_str, addr_family);
363 default: return KNOT_EINVAL;
364 }
365 }
366
synth_ptrname(uint8_t * out,const char * addr_str,const synth_template_t * tpl,int addr_family)367 static knot_dname_t *synth_ptrname(uint8_t *out, const char *addr_str,
368 const synth_template_t *tpl, int addr_family)
369 {
370 knot_dname_txt_storage_t ptrname;
371 int addr_len = strlen(addr_str);
372 const char sep = str_separator(addr_family);
373
374 // PTR right-hand value is [prefix][address][zone]
375 wire_ctx_t ctx = wire_ctx_init((uint8_t *)ptrname, sizeof(ptrname));
376 wire_ctx_write(&ctx, tpl->prefix, tpl->prefix_len);
377 wire_ctx_write(&ctx, addr_str, addr_len);
378 wire_ctx_write_u8(&ctx, '.');
379 wire_ctx_write(&ctx, tpl->zone, tpl->zone_len);
380 wire_ctx_write_u8(&ctx, '\0');
381 if (ctx.error != KNOT_EOK) {
382 return NULL;
383 }
384
385 // Substitute address separator by '-'.
386 str_subst(ptrname + tpl->prefix_len, addr_len, sep, '-');
387
388 // Convert to domain name.
389 return knot_dname_from_str(out, ptrname, KNOT_DNAME_MAXLEN);
390 }
391
reverse_rr(char * addr_str,const synth_template_t * tpl,knot_pkt_t * pkt,knot_rrset_t * rr,int addr_family)392 static int reverse_rr(char *addr_str, const synth_template_t *tpl, knot_pkt_t *pkt,
393 knot_rrset_t *rr, int addr_family)
394 {
395 // Synthesize PTR record data.
396 knot_dname_storage_t ptrname;
397 if (synth_ptrname(ptrname, addr_str, tpl, addr_family) == NULL) {
398 return KNOT_EINVAL;
399 }
400
401 rr->type = KNOT_RRTYPE_PTR;
402 knot_rrset_add_rdata(rr, ptrname, knot_dname_size(ptrname), &pkt->mm);
403
404 return KNOT_EOK;
405 }
406
forward_rr(char * addr_str,const synth_template_t * tpl,knot_pkt_t * pkt,knot_rrset_t * rr,int addr_family)407 static int forward_rr(char *addr_str, const synth_template_t *tpl, knot_pkt_t *pkt,
408 knot_rrset_t *rr, int addr_family)
409 {
410 struct sockaddr_storage query_addr;
411 sockaddr_set(&query_addr, addr_family, addr_str, 0);
412
413 // Specify address type and data.
414 if (addr_family == AF_INET6) {
415 rr->type = KNOT_RRTYPE_AAAA;
416 const struct sockaddr_in6* ip = (const struct sockaddr_in6*)&query_addr;
417 knot_rrset_add_rdata(rr, (const uint8_t *)&ip->sin6_addr,
418 sizeof(struct in6_addr), &pkt->mm);
419 } else if (addr_family == AF_INET) {
420 rr->type = KNOT_RRTYPE_A;
421 const struct sockaddr_in* ip = (const struct sockaddr_in*)&query_addr;
422 knot_rrset_add_rdata(rr, (const uint8_t *)&ip->sin_addr,
423 sizeof(struct in_addr), &pkt->mm);
424 } else {
425 return KNOT_EINVAL;
426 }
427
428 return KNOT_EOK;
429 }
430
synth_rr(char * addr_str,const synth_template_t * tpl,knot_pkt_t * pkt,knotd_qdata_t * qdata,int addr_family)431 static knot_rrset_t *synth_rr(char *addr_str, const synth_template_t *tpl, knot_pkt_t *pkt,
432 knotd_qdata_t *qdata, int addr_family)
433 {
434 knot_rrset_t *rr = knot_rrset_new(qdata->name, 0, KNOT_CLASS_IN, tpl->ttl,
435 &pkt->mm);
436 if (rr == NULL) {
437 return NULL;
438 }
439
440 // Fill in the specific data.
441 int ret = KNOT_ERROR;
442 switch (tpl->type) {
443 case SYNTH_REVERSE: ret = reverse_rr(addr_str, tpl, pkt, rr, addr_family); break;
444 case SYNTH_FORWARD: ret = forward_rr(addr_str, tpl, pkt, rr, addr_family); break;
445 default: break;
446 }
447
448 if (ret != KNOT_EOK) {
449 knot_rrset_free(rr, &pkt->mm);
450 return NULL;
451 }
452
453 return rr;
454 }
455
456 /*! \brief Check if query fits the template requirements. */
template_match(knotd_in_state_t state,const synth_template_t * tpl,knot_pkt_t * pkt,knotd_qdata_t * qdata)457 static knotd_in_state_t template_match(knotd_in_state_t state, const synth_template_t *tpl,
458 knot_pkt_t *pkt, knotd_qdata_t *qdata)
459 {
460 int provided_af = AF_UNSPEC;
461 struct sockaddr_storage query_addr;
462 char addr_str[SOCKADDR_STRLEN];
463 assert(SOCKADDR_STRLEN > KNOT_DNAME_MAXLABELLEN);
464 bool parent = false; // querying empty-non-terminal being (possibly indirect) parent of synthesized name
465
466 // Parse address from query name.
467 if (addr_parse(qdata, tpl, addr_str, &provided_af, &parent) != KNOT_EOK ||
468 sockaddr_set(&query_addr, provided_af, addr_str, 0) != KNOT_EOK) {
469 return state;
470 }
471
472 // Try all available addresses.
473 int i;
474 for (i = 0; i < tpl->addr_count; i++) {
475 if (tpl->addr[i].addr_max.ss_family == AF_UNSPEC) {
476 if (sockaddr_net_match(&query_addr, &tpl->addr[i].addr,
477 tpl->addr[i].addr_mask)) {
478 break;
479 }
480 } else {
481 if (sockaddr_range_match(&query_addr, &tpl->addr[i].addr,
482 &tpl->addr[i].addr_max)) {
483 break;
484 }
485 }
486 }
487 if (i >= tpl->addr_count) {
488 return state;
489 }
490
491 // Check if the request is for an available query type.
492 uint16_t qtype = knot_pkt_qtype(qdata->query);
493 switch (tpl->type) {
494 case SYNTH_FORWARD:
495 assert(!parent);
496 if (!query_satisfied_by_family(qtype, provided_af)) {
497 qdata->rcode = KNOT_RCODE_NOERROR;
498 return KNOTD_IN_STATE_NODATA;
499 }
500 break;
501 case SYNTH_REVERSE:
502 if (parent || (qtype != KNOT_RRTYPE_PTR && qtype != KNOT_RRTYPE_ANY)) {
503 qdata->rcode = KNOT_RCODE_NOERROR;
504 return KNOTD_IN_STATE_NODATA;
505 }
506 break;
507 default:
508 return state;
509 }
510
511 // Synthesize record from template.
512 knot_rrset_t *rr = synth_rr(addr_str, tpl, pkt, qdata, provided_af);
513 if (rr == NULL) {
514 qdata->rcode = KNOT_RCODE_SERVFAIL;
515 return KNOTD_IN_STATE_ERROR;
516 }
517
518 // Insert synthetic response into packet.
519 if (knot_pkt_put(pkt, 0, rr, KNOT_PF_FREE) != KNOT_EOK) {
520 return KNOTD_IN_STATE_ERROR;
521 }
522
523 // Authoritative response.
524 knot_wire_set_aa(pkt->wire);
525
526 return KNOTD_IN_STATE_HIT;
527 }
528
solve_synth_record(knotd_in_state_t state,knot_pkt_t * pkt,knotd_qdata_t * qdata,knotd_mod_t * mod)529 static knotd_in_state_t solve_synth_record(knotd_in_state_t state, knot_pkt_t *pkt,
530 knotd_qdata_t *qdata, knotd_mod_t *mod)
531 {
532 assert(pkt && qdata && mod);
533
534 // Applicable when search in zone fails.
535 if (state != KNOTD_IN_STATE_MISS) {
536 return state;
537 }
538
539 // Check if template fits.
540 return template_match(state, knotd_mod_ctx(mod), pkt, qdata);
541 }
542
synth_record_load(knotd_mod_t * mod)543 int synth_record_load(knotd_mod_t *mod)
544 {
545 // Create synthesis template.
546 synth_template_t *tpl = calloc(1, sizeof(*tpl));
547 if (tpl == NULL) {
548 return KNOT_ENOMEM;
549 }
550
551 // Set type.
552 knotd_conf_t conf = knotd_conf_mod(mod, MOD_TYPE);
553 tpl->type = conf.single.option;
554
555 /* Set prefix. */
556 conf = knotd_conf_mod(mod, MOD_PREFIX);
557 tpl->prefix = strdup(conf.single.string);
558 tpl->prefix_len = strlen(tpl->prefix);
559
560 // Set origin if generating reverse record.
561 if (tpl->type == SYNTH_REVERSE) {
562 conf = knotd_conf_mod(mod, MOD_ORIGIN);
563 tpl->zone = knot_dname_to_str_alloc(conf.single.dname);
564 if (tpl->zone == NULL) {
565 free(tpl->prefix);
566 free(tpl);
567 return KNOT_ENOMEM;
568 }
569 tpl->zone_len = strlen(tpl->zone);
570 }
571
572 // Set ttl.
573 conf = knotd_conf_mod(mod, MOD_TTL);
574 tpl->ttl = conf.single.integer;
575
576 // Set address.
577 conf = knotd_conf_mod(mod, MOD_NET);
578 tpl->addr_count = conf.count;
579 tpl->addr = calloc(conf.count, sizeof(*tpl->addr));
580 if (tpl->addr == NULL) {
581 knotd_conf_free(&conf);
582 free(tpl->zone);
583 free(tpl->prefix);
584 free(tpl);
585 return KNOT_ENOMEM;
586 }
587 for (size_t i = 0; i < conf.count; i++) {
588 tpl->addr[i].addr = conf.multi[i].addr;
589 tpl->addr[i].addr_max = conf.multi[i].addr_max;
590 tpl->addr[i].addr_mask = conf.multi[i].addr_mask;
591 }
592 knotd_conf_free(&conf);
593
594 // Set address shortening.
595 if (tpl->type == SYNTH_REVERSE) {
596 conf = knotd_conf_mod(mod, MOD_SHORT);
597 tpl->reverse_short = conf.single.boolean;
598 }
599
600 knotd_mod_ctx_set(mod, tpl);
601
602 return knotd_mod_in_hook(mod, KNOTD_STAGE_ANSWER, solve_synth_record);
603 }
604
synth_record_unload(knotd_mod_t * mod)605 void synth_record_unload(knotd_mod_t *mod)
606 {
607 synth_template_t *tpl = knotd_mod_ctx(mod);
608
609 free(tpl->addr);
610 free(tpl->zone);
611 free(tpl->prefix);
612 free(tpl);
613 }
614
615 KNOTD_MOD_API(synthrecord, KNOTD_MOD_FLAG_SCOPE_ZONE,
616 synth_record_load, synth_record_unload, synth_record_conf,
617 synth_record_conf_check);
618