hints.c 17 KB
Newer Older
1
/*  Copyright (C) 2014-2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
2

Marek Vavruša's avatar
Marek Vavruša committed
3 4 5 6
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
7

Marek Vavruša's avatar
Marek Vavruša committed
8 9 10 11
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
12

Marek Vavruša's avatar
Marek Vavruša committed
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/>.
Marek Vavruša's avatar
Marek Vavruša committed
15
 */
16

17
/**
18
 * @file hints.c
19 20 21 22 23
 * @brief Constructed zone cut from the hosts-like file, see @zonecut.h
 *
 * The module provides an override for queried address records.
 */

24 25 26
#include <libknot/packet/pkt.h>
#include <libknot/descriptor.h>
#include <libknot/rrtype/aaaa.h>
27 28
#include <ccan/json/json.h>
#include <ucw/mempool.h>
29
#include <contrib/cleanup.h>
30

31
#include "daemon/engine.h"
32
#include "lib/zonecut.h"
33 34
#include "lib/module.h"
#include "lib/layer.h"
35

36
/* Defaults */
37
#define VERBOSE_MSG(qry, fmt...) QRVERBOSE(qry, "hint",  fmt)
38
#define ERR_MSG(fmt, ...) kr_log_error("[     ][hint] " fmt, ## __VA_ARGS__)
39

40 41 42 43 44
struct hints_data {
	struct kr_zonecut hints;
	struct kr_zonecut reverse_hints;
};

45 46 47 48 49 50 51 52 53
/** Useful for returning from module properties. */
static char * bool2jsonstr(bool val)
{
	char *result = NULL;
	if (-1 == asprintf(&result, "{ \"result\": %s }", val ? "true" : "false"))
		result = NULL;
	return result;
}

54
static int put_answer(knot_pkt_t *pkt, knot_rrset_t *rr)
55
{
56 57 58 59
	int ret = 0;
	if (!knot_rrset_empty(rr)) {
		/* Update packet question */
		if (!knot_dname_is_equal(knot_pkt_qname(pkt), rr->owner)) {
60
			kr_pkt_recycle(pkt);
61 62 63 64 65 66
			knot_pkt_put_question(pkt, rr->owner, rr->rclass, rr->type);
		}
		/* Append to packet */
		ret = knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, rr, KNOT_PF_FREE);
	} else {
		ret = kr_error(ENOENT);
67
	}
68 69 70 71 72 73
	/* Clear RR if failed */
	if (ret != 0) {
		knot_rrset_clear(rr, &pkt->mm);
	}
	return ret;
}
74

75 76
static int satisfy_reverse(struct kr_zonecut *hints, knot_pkt_t *pkt, struct kr_query *qry)
{
77 78 79 80
	/* Find a matching name */
	pack_t *addr_set = kr_zonecut_find(hints, qry->sname);
	if (!addr_set || addr_set->len == 0) {
		return kr_error(ENOENT);
81
	}
82 83 84 85 86
	knot_dname_t *qname = knot_dname_copy(qry->sname, &pkt->mm);
	knot_rrset_t rr;
	knot_rrset_init(&rr, qname, KNOT_RRTYPE_PTR, KNOT_CLASS_IN);

	/* Append address records from hints */
87
	uint8_t *addr = pack_last(*addr_set);
88 89 90 91
	if (addr != NULL) {
		size_t len = pack_obj_len(addr);
		void *addr_val = pack_obj_val(addr);
		knot_rrset_add_rdata(&rr, addr_val, len, 0, &pkt->mm);
92
	}
93 94

	return put_answer(pkt, &rr);
95 96 97 98 99 100 101 102 103
}

static int satisfy_forward(struct kr_zonecut *hints, knot_pkt_t *pkt, struct kr_query *qry)
{
	/* Find a matching name */
	pack_t *addr_set = kr_zonecut_find(hints, qry->sname);
	if (!addr_set || addr_set->len == 0) {
		return kr_error(ENOENT);
	}
104
	knot_dname_t *qname = knot_dname_copy(qry->sname, &pkt->mm);
105
	knot_rrset_t rr;
106 107
	knot_rrset_init(&rr, qname, qry->stype, qry->sclass);
	size_t family_len = sizeof(struct in_addr);
108 109 110 111
	if (rr.type == KNOT_RRTYPE_AAAA) {
		family_len = sizeof(struct in6_addr);
	}

112
	/* Append address records from hints */
113 114 115 116 117
	uint8_t *addr = pack_head(*addr_set);
	while (addr != pack_tail(*addr_set)) {
		size_t len = pack_obj_len(addr);
		void *addr_val = pack_obj_val(addr);
		if (len == family_len) {
118
			knot_rrset_add_rdata(&rr, addr_val, len, 0, &pkt->mm);
119 120 121 122
		}
		addr = pack_obj_next(addr);
	}

123
	return put_answer(pkt, &rr);
124 125
}

126
static int query(kr_layer_t *ctx, knot_pkt_t *pkt)
127
{
128
	struct kr_query *qry = ctx->req->current_query;
129
	if (!qry || ctx->state & (KR_STATE_FAIL)) {
130 131
		return ctx->state;
	}
132

133
	struct kr_module *module = ctx->api->data;
134 135
	struct hints_data *data = module->data;
	if (!data) { /* No valid file. */
136 137
		return ctx->state;
	}
Vladimír Čunát's avatar
Vladimír Čunát committed
138 139
	/* FIXME: putting directly into packet breaks ordering in case the hint
	 * is applied after a CNAME jump. */
140 141 142
	switch(qry->stype) {
	case KNOT_RRTYPE_A:
	case KNOT_RRTYPE_AAAA: /* Find forward record hints */
143
		if (satisfy_forward(&data->hints, pkt, qry) != 0)
144 145 146
			return ctx->state;
		break;
	case KNOT_RRTYPE_PTR: /* Find PTR record */
147
		if (satisfy_reverse(&data->reverse_hints, pkt, qry) != 0)
148 149 150 151
			return ctx->state;
		break;
	default:
		return ctx->state; /* Ignore */
152 153
	}

154
	VERBOSE_MSG(qry, "<= answered from hints\n");
155
	qry->flags &= ~QUERY_DNSSEC_WANT; /* Never authenticated */
156 157
	qry->flags |= QUERY_CACHED|QUERY_NO_MINIMIZE;
	pkt->parsed = pkt->size;
158
	knot_wire_set_qr(pkt->wire);
159
	return KR_STATE_DONE;
160
}
161

162 163 164
static int parse_addr_str(struct sockaddr_storage *sa, const char *addr)
{
	int family = strchr(addr, ':') ? AF_INET6 : AF_INET;
165 166 167 168 169 170 171
	memset(sa, 0, sizeof(struct sockaddr_storage));
	sa->ss_family = family;
	char *addr_bytes = (char *)kr_inaddr((struct sockaddr *)sa);
	if (inet_pton(family, addr, addr_bytes) < 1) {
		return kr_error(EILSEQ);
	}
	return 0;
172
}
173

174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
/** @warning _NOT_ thread-safe; returns a pointer to static data! */
static const knot_rdata_t * addr2rdata(const char *addr) {
	/* Parse address string */
	struct sockaddr_storage ss;
	if (parse_addr_str(&ss, addr) != 0) {
		return NULL;
	}

	/* Build RDATA */
	static knot_rdata_t rdata_arr[RDATA_ARR_MAX];
	size_t addr_len = kr_inaddr_len((struct sockaddr *)&ss);
	const uint8_t *raw_addr = (const uint8_t *)kr_inaddr((struct sockaddr *)&ss);
	knot_rdata_init(rdata_arr, addr_len, raw_addr, 0);
	return rdata_arr;
}

190
/** @warning _NOT_ thread-safe; returns a pointer to static data! */
191
static const knot_dname_t * raw_addr2reverse(const uint8_t *raw_addr, int family)
192
{
193 194 195 196 197
	#define REV_MAXLEN (4*16 + 16 /* the suffix, terminator, etc. */)
	char reverse_addr[REV_MAXLEN];
	static knot_dname_t dname[REV_MAXLEN];
	#undef REV_MAXLEN

198
	if (family == AF_INET) {
199 200 201
		snprintf(reverse_addr, sizeof(reverse_addr),
			 "%d.%d.%d.%d.in-addr.arpa.",
		         raw_addr[3], raw_addr[2], raw_addr[1], raw_addr[0]);
202
	} else if (family == AF_INET6) {
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
		char *ra_it = reverse_addr;
		for (int i = 15; i >= 0; --i) {
			ssize_t free_space = reverse_addr + sizeof(reverse_addr) - ra_it;
			int written = snprintf(ra_it, free_space, "%x.%x.",
						raw_addr[i] & 0x0f, raw_addr[i] >> 4);
			if (written >= free_space) {
				assert(false);
				return NULL;
			}
			ra_it += written;
		}
		ssize_t free_space = reverse_addr + sizeof(reverse_addr) - ra_it;
		if (snprintf(ra_it, free_space, "ip6.arpa.") >= free_space) {
			return NULL;
		}
218
	} else {
219
		return NULL;
220 221 222 223 224 225 226 227 228
	}
	
	if (!knot_dname_from_str(dname, reverse_addr, sizeof(dname))) {
		return NULL;
	}
	return dname;
}

/** @warning _NOT_ thread-safe; returns a pointer to static data! */
229
static const knot_dname_t * addr2reverse(const char *addr)
230 231 232 233 234 235
{
	/* Parse address string */
	struct sockaddr_storage ss;
	if (parse_addr_str(&ss, addr) != 0) {
		return NULL;
	}
236 237 238 239
	const struct sockaddr *sa = (const struct sockaddr *)&ss;
	const uint8_t *raw_addr = (const uint8_t *)kr_inaddr(sa);
	int family = kr_inaddr_family(sa);
	return raw_addr2reverse(raw_addr, family);
240 241
}

242 243 244 245 246 247
static int add_pair(struct kr_zonecut *hints, const char *name, const char *addr)
{
	/* Build key */
	knot_dname_t key[KNOT_DNAME_MAXLEN];
	if (!knot_dname_from_str(key, name, sizeof(key))) {
		return kr_error(EINVAL);
248
	}
249 250 251 252
	int ret = knot_dname_to_lower(key);
	if (ret) {
		return ret;
	}
253 254 255 256
	const knot_rdata_t *rdata = addr2rdata(addr);
	if (!rdata) {
		return kr_error(EINVAL);
	}
257

258 259 260
	return kr_zonecut_add(hints, key, rdata);
}

261 262 263 264 265 266 267 268 269 270 271
static int add_reverse_pair(struct kr_zonecut *hints, const char *name, const char *addr)
{
	const knot_dname_t *key = addr2reverse(addr);

	if (key == NULL) {
		return kr_error(EINVAL);
	}

	knot_dname_t ptr_name[KNOT_DNAME_MAXLEN];
	if (!knot_dname_from_str(ptr_name, name, sizeof(ptr_name))) {
		return kr_error(EINVAL);
Vladimír Čunát's avatar
Vladimír Čunát committed
272
	}
273 274 275 276 277 278 279 280

	/* Build RDATA */
	knot_rdata_t rdata[RDATA_ARR_MAX];
	knot_rdata_init(rdata, knot_dname_size(ptr_name), ptr_name, 0);

	return kr_zonecut_add(hints, key, rdata);
}

Vladimír Čunát's avatar
Vladimír Čunát committed
281 282 283 284
/** For a given name, remove either one address or all of them (if == NULL).
 *
 * Also remove the corresponding reverse records.
 */
285
static int del_pair(struct hints_data *data, const char *name, const char *addr)
286 287 288 289
{
	/* Build key */
	knot_dname_t key[KNOT_DNAME_MAXLEN];
	if (!knot_dname_from_str(key, name, sizeof(key))) {
290 291
		return kr_error(EINVAL);
	}
292 293
	knot_rdata_t ptr_rdata[RDATA_ARR_MAX];
	knot_rdata_init(ptr_rdata, knot_dname_size(key), key, 0);
294

295 296 297 298 299
        if (addr) {
		/* Remove the pair. */
		const knot_rdata_t *rdata = addr2rdata(addr);
		if (!rdata) {
			return kr_error(EINVAL);
Vladimír Čunát's avatar
Vladimír Čunát committed
300
		}
301
		const knot_dname_t *reverse_key = addr2reverse(addr);
Vladimír Čunát's avatar
Vladimír Čunát committed
302 303
		kr_zonecut_del(&data->reverse_hints, reverse_key, ptr_rdata);
		return kr_zonecut_del(&data->hints, key, rdata);
304
	} else {
305 306 307 308 309 310
		/* Find a matching name */
		pack_t *addr_set = kr_zonecut_find(&data->hints, key);
		if (!addr_set || addr_set->len == 0) {
			return kr_error(ENOENT);
		}

Vladimír Čunát's avatar
Vladimír Čunát committed
311
		/* Remove address records in hints from reverse_hints. */
312 313 314
		uint8_t *addr = pack_head(*addr_set);
		while (addr != pack_tail(*addr_set)) {
			void *addr_val = pack_obj_val(addr);
315 316 317
			int family = pack_obj_len(addr) == kr_family_len(AF_INET)
					? AF_INET : AF_INET6;
			const knot_dname_t *reverse_key = raw_addr2reverse(addr_val, family);
318 319 320 321 322 323
			if (reverse_key != NULL) {
				kr_zonecut_del(&data->reverse_hints, reverse_key, ptr_rdata);
			}
			addr = pack_obj_next(addr);
		}
		
324
		/* Remove the whole name. */
325
		return kr_zonecut_del_all(&data->hints, key);
326
	}
327
}
328

329
static int load_file(struct kr_module *module, const char *path)
330
{
331 332 333 334 335 336 337 338 339 340
	auto_fclose FILE *fp = fopen(path, "r");
	if (fp == NULL) {
		ERR_MSG("reading '%s' failed: %s\n", path, strerror(errno));
		return kr_error(errno);
	} else {
		VERBOSE_MSG(NULL, "reading '%s'\n", path);
	}

	/* Load file to map */
	struct hints_data *data = module->data;
341
	size_t line_len = 0;
342
	size_t count = 0;
343
	size_t line_count = 0;
344 345
	auto_free char *line = NULL;

Vladimír Čunát's avatar
Vladimír Čunát committed
346
	while (getline(&line, &line_len, fp) > 0) {
347
		++line_count;
348
		char *saveptr = NULL;
349 350
		const char *addr = strtok_r(line, " \t\n", &saveptr);
		if (addr == NULL || strchr(addr, '#') || strlen(addr) == 0) {
351 352
			continue;
		}
353 354 355 356 357 358 359 360 361 362
		const char *canonical_name = strtok_r(NULL, " \t\n", &saveptr);
		if (canonical_name == NULL) {
			ERR_MSG("%s:%zu: invalid syntax\n", path, line_count);
			continue;
		}
		/* Since the last added PTR records takes preference,
		 * we add canonical name as the last one. */
		const char *name_tok;
		while ((name_tok = strtok_r(NULL, " \t\n", &saveptr)) != NULL) {
			if (add_pair(&data->hints, name_tok, addr) == 0) {
363
				count += 1;
364
			}
365 366 367 368
			add_reverse_pair(&data->reverse_hints, name_tok, addr);
		}
		if (add_pair(&data->hints, canonical_name, addr) == 0) {
			count += 1;
369
		}
370
		add_reverse_pair(&data->reverse_hints, canonical_name, addr);
371 372
	}

373
	VERBOSE_MSG(NULL, "loaded %zu hints\n", count);
374 375 376
	return kr_ok();
}

377 378 379 380 381 382 383 384
static char* hint_add_hosts(void *env, struct kr_module *module, const char *args)
{
	if (!args)
		args = "/etc/hosts";
	int err = load_file(module, args);
	return bool2jsonstr(err == kr_ok());
}

385 386 387 388 389 390 391 392 393
/**
 * Set name => address hint.
 *
 * Input:  { name, address }
 * Output: { result: bool }
 *
 */
static char* hint_set(void *env, struct kr_module *module, const char *args)
{
394
	struct hints_data *data = module->data;
395 396
	if (!args)
		return NULL;
397
	auto_free char *args_copy = strdup(args);
Vladimír Čunát's avatar
Vladimír Čunát committed
398 399
	if (!args_copy)
		return NULL;
400 401 402 403 404

	int ret = -1;
	char *addr = strchr(args_copy, ' ');
	if (addr) {
		*addr = '\0';
Vladimír Čunát's avatar
Vladimír Čunát committed
405 406 407 408 409 410 411
		++addr;
		ret = add_reverse_pair(&data->reverse_hints, args_copy, addr);
		if (ret) {
			del_pair(data, args_copy, addr);
		} else {
			ret = add_pair(&data->hints, args_copy, addr);
		}
412 413
	}

414
	return bool2jsonstr(ret == 0);
415 416
}

417 418
static char* hint_del(void *env, struct kr_module *module, const char *args)
{
419
	struct hints_data *data = module->data;
Vladimír Čunát's avatar
Vladimír Čunát committed
420 421
	if (!args)
		return NULL;
422
	auto_free char *args_copy = strdup(args);
Vladimír Čunát's avatar
Vladimír Čunát committed
423 424
	if (!args_copy)
		return NULL;
425 426 427 428 429 430 431

	int ret = -1;
	char *addr = strchr(args_copy, ' ');
	if (addr) {
		*addr = '\0';
		++addr;
	}
432
	ret = del_pair(data, args_copy, addr);
433

434
	return bool2jsonstr(ret == 0);
435 436
}

437 438 439
/** @internal Pack address list into JSON array. */
static JsonNode *pack_addrs(pack_t *pack)
{
440
	char buf[INET6_ADDRSTRLEN];
441 442 443 444 445 446 447 448 449 450 451 452 453 454
	JsonNode *root = json_mkarray();
	uint8_t *addr = pack_head(*pack);
	while (addr != pack_tail(*pack)) {
		size_t len = pack_obj_len(addr);
		int family = len == sizeof(struct in_addr) ? AF_INET : AF_INET6;
		if (!inet_ntop(family, pack_obj_val(addr), buf, sizeof(buf))) {
			break;
		}
		json_append_element(root, json_mkstring(buf));
		addr = pack_obj_next(addr);
	}
	return root;
}

455
static char* pack_hints(struct kr_zonecut *hints);
456
/**
457
 * Retrieve address hints, either for given name or for all names.
458 459
 *
 * Input:  name
460
 * Output: NULL or "{ address1, address2, ... }"
461 462 463
 */
static char* hint_get(void *env, struct kr_module *module, const char *args)
{
464
	struct kr_zonecut *hints = &((struct hints_data *) module->data)->hints;
465 466 467 468
	if (!hints) {
		assert(false);
		return NULL;
	}
469 470 471 472 473

	if (!args) {
		return pack_hints(hints);
	}

474 475 476 477 478 479 480 481 482
	knot_dname_t key[KNOT_DNAME_MAXLEN];
	pack_t *pack = NULL;
	if (knot_dname_from_str(key, args, sizeof(key))) {
		pack = kr_zonecut_find(hints, key);
	}
	if (!pack || pack->len == 0) {
		return NULL;
	}

483 484 485 486 487
	char *result = NULL;
	JsonNode *root = pack_addrs(pack);
	if (root) {
		result = json_encode(root);
		json_delete(root);
488
	}
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
	return result;
}

/** Retrieve hint list. */
static int pack_hint(const char *k, void *v, void *baton)
{
	char nsname_str[KNOT_DNAME_MAXLEN] = {'\0'};
	knot_dname_to_str(nsname_str, (const uint8_t *)k, sizeof(nsname_str));
	JsonNode *root_node = baton;
	JsonNode *addr_list = pack_addrs((pack_t *)v);
	if (!addr_list) {
		return kr_error(ENOMEM);
	}
	json_append_member(root_node, nsname_str, addr_list);
	return kr_ok();
}
505

506 507 508 509 510 511 512 513 514 515 516
/** @internal Pack all hints into serialized JSON. */
static char* pack_hints(struct kr_zonecut *hints) {
	char *result = NULL;
	JsonNode *root_node = json_mkobject();
	if (map_walk(&hints->nsset, pack_hint, root_node) == 0) {
		result = json_encode(root_node);
	}
	json_delete(root_node);
	return result;
}

517 518 519 520 521 522 523 524 525 526 527 528
static void unpack_hint(struct kr_zonecut *root_hints, JsonNode *table, const char *name)
{
	JsonNode *node = NULL;
	json_foreach(node, table) {
		switch(node->tag) {
		case JSON_STRING: add_pair(root_hints, name ? name : node->key, node->string_); break;
		case JSON_ARRAY: unpack_hint(root_hints, node, name ? name : node->key); break;
		default: continue;
		}
	}
}

529 530 531 532 533 534 535 536 537 538 539
/**
 * Get/set root hints set.
 *
 * Input:  { name: [addr_list], ... }
 * Output: current list
 *
 */
static char* hint_root(void *env, struct kr_module *module, const char *args)
{
	struct engine *engine = env;
	struct kr_context *ctx = &engine->resolver;
540
	struct kr_zonecut *root_hints = &ctx->root_hints;
541
	/* Replace root hints if parameter is set */
Vladimír Čunát's avatar
Vladimír Čunát committed
542
	if (args && args[0] != '\0') {
543
		JsonNode *root_node = json_decode(args);
544 545
		kr_zonecut_set(root_hints, (const uint8_t *)"");
		unpack_hint(root_hints, root_node, NULL);
546
		json_delete(root_node);
547 548
	}
	/* Return current root hints */
549
	return pack_hints(root_hints);
550 551
}

552 553 554
/*
 * Module implementation.
 */
555

556
KR_EXPORT
557
const kr_layer_api_t *hints_layer(struct kr_module *module)
558
{
559
	static kr_layer_api_t _layer = {
560
		.produce = &query,
561
	};
562 563
	/* Store module reference */
	_layer.data = module;
564
	return &_layer;
565 566
}

567 568

/** Basic initialization: get a memory pool, etc. */
569
KR_EXPORT
570
int hints_init(struct kr_module *module)
571
{
572 573 574 575 576 577 578 579 580 581 582
	/* Create pool and copy itself */
	knot_mm_t _pool = {
		.ctx = mp_new(4096),
		.alloc = (knot_mm_alloc_t) mp_alloc
	};
	knot_mm_t *pool = mm_alloc(&_pool, sizeof(*pool));
	if (!pool) {
		return kr_error(ENOMEM);
	}
	memcpy(pool, &_pool, sizeof(*pool));

583 584
	struct hints_data *data = mm_alloc(pool, sizeof(struct hints_data));
	if (!data) {
585 586 587
		mp_delete(pool->ctx);
		return kr_error(ENOMEM);
	}
588 589 590
	kr_zonecut_init(&data->hints, (const uint8_t *)(""), pool);
	kr_zonecut_init(&data->reverse_hints, (const uint8_t *)(""), pool);
	module->data = data;
591 592

	return kr_ok();
593
}
594

Vladimír Čunát's avatar
Vladimír Čunát committed
595
/** Release all resources. */
596 597 598 599 600 601 602 603 604 605 606 607 608
KR_EXPORT
int hints_deinit(struct kr_module *module)
{
	struct hints_data *data = module->data;
	if (data) {
		kr_zonecut_deinit(&data->hints);
		kr_zonecut_deinit(&data->reverse_hints);
		mp_delete(data->hints.pool->ctx);
		module->data = NULL;
	}
	return kr_ok();
}

609 610 611 612
/** Drop all hints, and load a hosts file if any was specified.
 *
 * It seems slightly strange to drop all, but keep doing that for now.
 */
613
KR_EXPORT
614
int hints_config(struct kr_module *module, const char *conf)
615
{
616
	hints_deinit(module);
617 618 619
	int err = hints_init(module);
	if (err != kr_ok()) {
		return err;
620
	}
621 622 623 624 625

	if (conf && conf[0]) {
		return load_file(module, conf);
	}
	return kr_ok();
626
}
627

628
KR_EXPORT
629 630 631 632
struct kr_prop *hints_props(void)
{
	static struct kr_prop prop_list[] = {
	    { &hint_set,    "set", "Set {name, address} hint.", },
633
	    { &hint_del,    "del", "Delete one {name, address} hint or all addresses for the name.", },
634
	    { &hint_get,    "get", "Retrieve hint for given name.", },
635
	    { &hint_add_hosts, "add_hosts", "Load a file with hosts-like formatting and add contents into hints.", },
636
	    { &hint_root,   "root", "Replace root hints set (empty value to return current list).", },
637 638 639 640 641
	    { NULL, NULL, NULL }
	};
	return prop_list;
}

Marek Vavruša's avatar
Marek Vavruša committed
642
KR_MODULE_EXPORT(hints);
643 644

#undef VERBOSE_MSG