xref: /netbsd-src/external/mpl/bind/dist/lib/dns/resconf.c (revision bcda20f65a8566e103791ec395f7f499ef322704)
1 /*	$NetBSD: resconf.c,v 1.2 2025/01/26 16:25:24 christos Exp $	*/
2 
3 /*
4  * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
5  *
6  * SPDX-License-Identifier: MPL-2.0
7  *
8  * This Source Code Form is subject to the terms of the Mozilla Public
9  * License, v. 2.0. If a copy of the MPL was not distributed with this
10  * file, you can obtain one at https://mozilla.org/MPL/2.0/.
11  *
12  * See the COPYRIGHT file distributed with this work for additional
13  * information regarding copyright ownership.
14  */
15 
16 /*! \file resconf.c */
17 
18 /**
19  * Module for parsing resolv.conf files (largely derived from lwconfig.c).
20  *
21  *    irs_resconf_load() opens the file filename and parses it to initialize
22  *    the configuration structure.
23  *
24  * \section lwconfig_return Return Values
25  *
26  *    irs_resconf_load() returns #IRS_R_SUCCESS if it successfully read and
27  *    parsed filename. It returns a non-0 error code if filename could not be
28  *    opened or contained incorrect resolver statements.
29  *
30  * \section lwconfig_see See Also
31  *
32  *    stdio(3), \link resolver resolver \endlink
33  *
34  * \section files Files
35  *
36  *    /etc/resolv.conf
37  */
38 
39 #include <ctype.h>
40 #include <errno.h>
41 #include <inttypes.h>
42 #include <netdb.h>
43 #include <stdio.h>
44 #include <stdlib.h>
45 #include <string.h>
46 #include <sys/socket.h>
47 #include <sys/types.h>
48 
49 #include <isc/magic.h>
50 #include <isc/mem.h>
51 #include <isc/netaddr.h>
52 #include <isc/sockaddr.h>
53 #include <isc/util.h>
54 
55 #include <irs/resconf.h>
56 
57 #define IRS_RESCONF_MAGIC    ISC_MAGIC('R', 'E', 'S', 'c')
58 #define IRS_RESCONF_VALID(c) ISC_MAGIC_VALID(c, IRS_RESCONF_MAGIC)
59 
60 /*!
61  * protocol constants
62  */
63 
64 #if !defined(NS_INADDRSZ)
65 #define NS_INADDRSZ 4
66 #endif /* if !defined(NS_INADDRSZ) */
67 
68 #if !defined(NS_IN6ADDRSZ)
69 #define NS_IN6ADDRSZ 16
70 #endif /* if !defined(NS_IN6ADDRSZ) */
71 
72 /*!
73  * resolv.conf parameters
74  */
75 
76 #define RESCONFMAXNAMESERVERS 3U   /*%< max 3 "nameserver" entries */
77 #define RESCONFMAXSEARCH      8U   /*%< max 8 domains in "search" entry */
78 #define RESCONFMAXLINELEN     256U /*%< max size of a line */
79 #define RESCONFMAXSORTLIST    10U  /*%< max 10 */
80 
81 #define CHECK(op)                            \
82 	do {                                 \
83 		result = (op);               \
84 		if (result != ISC_R_SUCCESS) \
85 			goto cleanup;        \
86 	} while (0)
87 
88 /*!
89  * configuration data structure
90  */
91 
92 struct irs_resconf {
93 	/*
94 	 * The configuration data is a thread-specific object, and does not
95 	 * need to be locked.
96 	 */
97 	unsigned int magic;
98 	isc_mem_t *mctx;
99 
100 	isc_sockaddrlist_t nameservers;
101 	unsigned int numns; /*%< number of configured servers */
102 
103 	char *domainname;
104 	char *search[RESCONFMAXSEARCH];
105 	uint8_t searchnxt; /*%< index for next free slot */
106 
107 	irs_resconf_searchlist_t searchlist;
108 
109 	struct {
110 		isc_netaddr_t addr;
111 		/*% mask has a non-zero 'family' if set */
112 		isc_netaddr_t mask;
113 	} sortlist[RESCONFMAXSORTLIST];
114 	uint8_t sortlistnxt;
115 
116 	/*%< non-zero if 'options debug' set */
117 	uint8_t resdebug;
118 	/*%< set to n in 'options ndots:n' */
119 	uint8_t ndots;
120 	/*%< set to n in 'options attempts:n' */
121 	uint8_t attempts;
122 	/*%< set to n in 'options timeout:n' */
123 	uint8_t timeout;
124 };
125 
126 static isc_result_t
127 resconf_parsenameserver(irs_resconf_t *conf, FILE *fp);
128 static isc_result_t
129 resconf_parsedomain(irs_resconf_t *conf, FILE *fp);
130 static isc_result_t
131 resconf_parsesearch(irs_resconf_t *conf, FILE *fp);
132 static isc_result_t
133 resconf_parsesortlist(irs_resconf_t *conf, FILE *fp);
134 static isc_result_t
135 resconf_parseoption(irs_resconf_t *ctx, FILE *fp);
136 
137 /*!
138  * Eat characters from FP until EOL or EOF. Returns EOF or '\n'
139  */
140 static int
141 eatline(FILE *fp) {
142 	int ch;
143 
144 	ch = fgetc(fp);
145 	while (ch != '\n' && ch != EOF) {
146 		ch = fgetc(fp);
147 	}
148 
149 	return ch;
150 }
151 
152 /*!
153  * Eats white space up to next newline or non-whitespace character (of
154  * EOF). Returns the last character read. Comments are considered white
155  * space.
156  */
157 static int
158 eatwhite(FILE *fp) {
159 	int ch;
160 
161 	ch = fgetc(fp);
162 	while (ch != '\n' && ch != EOF && isspace((unsigned char)ch)) {
163 		ch = fgetc(fp);
164 	}
165 
166 	if (ch == ';' || ch == '#') {
167 		ch = eatline(fp);
168 	}
169 
170 	return ch;
171 }
172 
173 /*!
174  * Skip over any leading whitespace and then read in the next sequence of
175  * non-whitespace characters. In this context newline is not considered
176  * whitespace. Returns EOF on end-of-file, or the character
177  * that caused the reading to stop.
178  */
179 static int
180 getword(FILE *fp, char *buffer, size_t size) {
181 	char *p = NULL;
182 	int ch;
183 
184 	REQUIRE(buffer != NULL);
185 	REQUIRE(size > 0U);
186 
187 	p = buffer;
188 	*p = '\0';
189 
190 	ch = eatwhite(fp);
191 
192 	if (ch == EOF) {
193 		return EOF;
194 	}
195 
196 	do {
197 		*p = '\0';
198 
199 		if (ch == EOF || isspace((unsigned char)ch)) {
200 			break;
201 		} else if ((size_t)(p - buffer) == size - 1) {
202 			return EOF; /* Not enough space. */
203 		}
204 
205 		*p++ = (char)ch;
206 		ch = fgetc(fp);
207 	} while (1);
208 
209 	return ch;
210 }
211 
212 static isc_result_t
213 add_server(isc_mem_t *mctx, const char *address_str,
214 	   isc_sockaddrlist_t *nameservers) {
215 	int error;
216 	isc_sockaddr_t *address = NULL;
217 	struct addrinfo hints, *res;
218 	isc_result_t result = ISC_R_SUCCESS;
219 
220 	res = NULL;
221 	memset(&hints, 0, sizeof(hints));
222 	hints.ai_family = AF_UNSPEC;
223 	hints.ai_socktype = SOCK_DGRAM;
224 	hints.ai_protocol = IPPROTO_UDP;
225 	hints.ai_flags = AI_NUMERICHOST;
226 	error = getaddrinfo(address_str, "53", &hints, &res);
227 	if (error != 0) {
228 		return ISC_R_BADADDRESSFORM;
229 	}
230 
231 	address = isc_mem_get(mctx, sizeof(*address));
232 	if (res->ai_addrlen > sizeof(address->type)) {
233 		isc_mem_put(mctx, address, sizeof(*address));
234 		result = ISC_R_RANGE;
235 		goto cleanup;
236 	}
237 
238 	if (res->ai_family == AF_INET) {
239 		struct in_addr *v4;
240 		unsigned char zeroaddress[] = { 0, 0, 0, 0 };
241 		unsigned char loopaddress[] = { 127, 0, 0, 1 };
242 
243 		/* XXX: special case: treat all-0 IPv4 address as loopback */
244 		v4 = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
245 		if (memcmp(v4, zeroaddress, 4) == 0) {
246 			memmove(v4, loopaddress, 4);
247 		}
248 		memmove(&address->type.sin, res->ai_addr, res->ai_addrlen);
249 	} else if (res->ai_family == AF_INET6) {
250 		memmove(&address->type.sin6, res->ai_addr, res->ai_addrlen);
251 	} else {
252 		isc_mem_put(mctx, address, sizeof(*address));
253 		UNEXPECTED_ERROR("ai_family (%d) not INET nor INET6",
254 				 res->ai_family);
255 		result = ISC_R_UNEXPECTED;
256 		goto cleanup;
257 	}
258 	address->length = (unsigned int)res->ai_addrlen;
259 
260 	ISC_LINK_INIT(address, link);
261 	ISC_LIST_APPEND(*nameservers, address, link);
262 
263 cleanup:
264 	freeaddrinfo(res);
265 
266 	return result;
267 }
268 
269 static isc_result_t
270 create_addr(const char *buffer, isc_netaddr_t *addr, int convert_zero) {
271 	struct in_addr v4;
272 	struct in6_addr v6;
273 
274 	if (inet_pton(AF_INET, buffer, &v4) == 1) {
275 		if (convert_zero) {
276 			unsigned char zeroaddress[] = { 0, 0, 0, 0 };
277 			unsigned char loopaddress[] = { 127, 0, 0, 1 };
278 			if (memcmp(&v4, zeroaddress, 4) == 0) {
279 				memmove(&v4, loopaddress, 4);
280 			}
281 		}
282 		addr->family = AF_INET;
283 		memmove(&addr->type.in, &v4, NS_INADDRSZ);
284 		addr->zone = 0;
285 	} else if (inet_pton(AF_INET6, buffer, &v6) == 1) {
286 		addr->family = AF_INET6;
287 		memmove(&addr->type.in6, &v6, NS_IN6ADDRSZ);
288 		addr->zone = 0;
289 	} else {
290 		return ISC_R_BADADDRESSFORM; /* Unrecognised format. */
291 	}
292 
293 	return ISC_R_SUCCESS;
294 }
295 
296 static isc_result_t
297 resconf_parsenameserver(irs_resconf_t *conf, FILE *fp) {
298 	char word[RESCONFMAXLINELEN];
299 	int cp;
300 	isc_result_t result;
301 
302 	cp = getword(fp, word, sizeof(word));
303 	if (strlen(word) == 0U) {
304 		return ISC_R_UNEXPECTEDEND; /* Nothing on line. */
305 	} else if (cp == ' ' || cp == '\t') {
306 		cp = eatwhite(fp);
307 	}
308 
309 	if (cp != EOF && cp != '\n') {
310 		return ISC_R_UNEXPECTEDTOKEN; /* Extra junk on line. */
311 	}
312 
313 	if (conf->numns == RESCONFMAXNAMESERVERS) {
314 		return ISC_R_SUCCESS;
315 	}
316 
317 	result = add_server(conf->mctx, word, &conf->nameservers);
318 	if (result != ISC_R_SUCCESS) {
319 		return result;
320 	}
321 	conf->numns++;
322 
323 	return ISC_R_SUCCESS;
324 }
325 
326 static isc_result_t
327 resconf_parsedomain(irs_resconf_t *conf, FILE *fp) {
328 	char word[RESCONFMAXLINELEN];
329 	int res;
330 	unsigned int i;
331 
332 	res = getword(fp, word, sizeof(word));
333 	if (strlen(word) == 0U) {
334 		return ISC_R_UNEXPECTEDEND; /* Nothing else on line. */
335 	} else if (res == ' ' || res == '\t') {
336 		res = eatwhite(fp);
337 	}
338 
339 	if (res != EOF && res != '\n') {
340 		return ISC_R_UNEXPECTEDTOKEN; /* Extra junk on line. */
341 	}
342 
343 	if (conf->domainname != NULL) {
344 		isc_mem_free(conf->mctx, conf->domainname);
345 	}
346 
347 	/*
348 	 * Search and domain are mutually exclusive.
349 	 */
350 	for (i = 0; i < RESCONFMAXSEARCH; i++) {
351 		if (conf->search[i] != NULL) {
352 			isc_mem_free(conf->mctx, conf->search[i]);
353 			conf->search[i] = NULL;
354 		}
355 	}
356 	conf->searchnxt = 0;
357 
358 	conf->domainname = isc_mem_strdup(conf->mctx, word);
359 
360 	return ISC_R_SUCCESS;
361 }
362 
363 static isc_result_t
364 resconf_parsesearch(irs_resconf_t *conf, FILE *fp) {
365 	int delim;
366 	unsigned int idx;
367 	char word[RESCONFMAXLINELEN];
368 
369 	if (conf->domainname != NULL) {
370 		/*
371 		 * Search and domain are mutually exclusive.
372 		 */
373 		isc_mem_free(conf->mctx, conf->domainname);
374 		conf->domainname = NULL;
375 	}
376 
377 	/*
378 	 * Remove any previous search definitions.
379 	 */
380 	for (idx = 0; idx < RESCONFMAXSEARCH; idx++) {
381 		if (conf->search[idx] != NULL) {
382 			isc_mem_free(conf->mctx, conf->search[idx]);
383 			conf->search[idx] = NULL;
384 		}
385 	}
386 	conf->searchnxt = 0;
387 
388 	delim = getword(fp, word, sizeof(word));
389 	if (strlen(word) == 0U) {
390 		return ISC_R_UNEXPECTEDEND; /* Nothing else on line. */
391 	}
392 
393 	idx = 0;
394 	while (strlen(word) > 0U) {
395 		if (conf->searchnxt == RESCONFMAXSEARCH) {
396 			goto ignore; /* Too many domains. */
397 		}
398 
399 		INSIST(idx < sizeof(conf->search) / sizeof(conf->search[0]));
400 		conf->search[idx] = isc_mem_strdup(conf->mctx, word);
401 		idx++;
402 		conf->searchnxt++;
403 
404 	ignore:
405 		if (delim == EOF || delim == '\n') {
406 			break;
407 		} else {
408 			delim = getword(fp, word, sizeof(word));
409 		}
410 	}
411 
412 	return ISC_R_SUCCESS;
413 }
414 
415 static isc_result_t
416 resconf_parsesortlist(irs_resconf_t *conf, FILE *fp) {
417 	int delim, res;
418 	unsigned int idx;
419 	char word[RESCONFMAXLINELEN];
420 	char *p;
421 
422 	delim = getword(fp, word, sizeof(word));
423 	if (strlen(word) == 0U) {
424 		return ISC_R_UNEXPECTEDEND; /* Empty line after keyword. */
425 	}
426 
427 	while (strlen(word) > 0U) {
428 		if (conf->sortlistnxt == RESCONFMAXSORTLIST) {
429 			return ISC_R_QUOTA; /* Too many values. */
430 		}
431 
432 		p = strchr(word, '/');
433 		if (p != NULL) {
434 			*p++ = '\0';
435 		}
436 
437 		idx = conf->sortlistnxt;
438 		INSIST(idx <
439 		       sizeof(conf->sortlist) / sizeof(conf->sortlist[0]));
440 		res = create_addr(word, &conf->sortlist[idx].addr, 1);
441 		if (res != ISC_R_SUCCESS) {
442 			return res;
443 		}
444 
445 		if (p != NULL) {
446 			res = create_addr(p, &conf->sortlist[idx].mask, 0);
447 			if (res != ISC_R_SUCCESS) {
448 				return res;
449 			}
450 		} else {
451 			/*
452 			 * Make up a mask. (XXX: is this correct?)
453 			 */
454 			conf->sortlist[idx].mask = conf->sortlist[idx].addr;
455 			memset(&conf->sortlist[idx].mask.type, 0xff,
456 			       sizeof(conf->sortlist[idx].mask.type));
457 		}
458 
459 		conf->sortlistnxt++;
460 
461 		if (delim == EOF || delim == '\n') {
462 			break;
463 		} else {
464 			delim = getword(fp, word, sizeof(word));
465 		}
466 	}
467 
468 	return ISC_R_SUCCESS;
469 }
470 
471 static isc_result_t
472 resconf_optionnumber(const char *word, uint8_t *number) {
473 	char *p;
474 	long n;
475 
476 	n = strtol(word, &p, 10);
477 	if (*p != '\0') { /* Bad string. */
478 		return ISC_R_UNEXPECTEDTOKEN;
479 	}
480 	if (n < 0 || n > 0xff) { /* Out of range. */
481 		return ISC_R_RANGE;
482 	}
483 	*number = n;
484 	return ISC_R_SUCCESS;
485 }
486 
487 static isc_result_t
488 resconf_parseoption(irs_resconf_t *conf, FILE *fp) {
489 	int delim;
490 	isc_result_t result = ISC_R_SUCCESS;
491 	char word[RESCONFMAXLINELEN];
492 
493 	delim = getword(fp, word, sizeof(word));
494 	if (strlen(word) == 0U) {
495 		return ISC_R_UNEXPECTEDEND; /* Empty line after keyword. */
496 	}
497 
498 	while (strlen(word) > 0U) {
499 		if (strcmp("debug", word) == 0) {
500 			conf->resdebug = 1;
501 		} else if (strncmp("ndots:", word, 6) == 0) {
502 			CHECK(resconf_optionnumber(word + 6, &conf->ndots));
503 		} else if (strncmp("attempts:", word, 9) == 0) {
504 			CHECK(resconf_optionnumber(word + 9, &conf->attempts));
505 		} else if (strncmp("timeout:", word, 8) == 0) {
506 			CHECK(resconf_optionnumber(word + 8, &conf->timeout));
507 		}
508 
509 		if (delim == EOF || delim == '\n') {
510 			break;
511 		} else {
512 			delim = getword(fp, word, sizeof(word));
513 		}
514 	}
515 
516 cleanup:
517 	return result;
518 }
519 
520 static isc_result_t
521 add_search(irs_resconf_t *conf, char *domain) {
522 	irs_resconf_search_t *entry;
523 
524 	entry = isc_mem_get(conf->mctx, sizeof(*entry));
525 
526 	entry->domain = domain;
527 	ISC_LINK_INIT(entry, link);
528 	ISC_LIST_APPEND(conf->searchlist, entry, link);
529 
530 	return ISC_R_SUCCESS;
531 }
532 
533 /*% parses a file and fills in the data structure. */
534 isc_result_t
535 irs_resconf_load(isc_mem_t *mctx, const char *filename, irs_resconf_t **confp) {
536 	FILE *fp = NULL;
537 	char word[256];
538 	isc_result_t rval, ret = ISC_R_SUCCESS;
539 	irs_resconf_t *conf;
540 	unsigned int i;
541 	int stopchar;
542 
543 	REQUIRE(mctx != NULL);
544 	REQUIRE(filename != NULL);
545 	REQUIRE(strlen(filename) > 0U);
546 	REQUIRE(confp != NULL && *confp == NULL);
547 
548 	conf = isc_mem_get(mctx, sizeof(*conf));
549 
550 	conf->mctx = mctx;
551 	ISC_LIST_INIT(conf->nameservers);
552 	ISC_LIST_INIT(conf->searchlist);
553 	conf->numns = 0;
554 	conf->domainname = NULL;
555 	conf->searchnxt = 0;
556 	conf->sortlistnxt = 0;
557 	conf->resdebug = 0;
558 	conf->ndots = 1;
559 	conf->attempts = 3;
560 	conf->timeout = 0;
561 	for (i = 0; i < RESCONFMAXSEARCH; i++) {
562 		conf->search[i] = NULL;
563 	}
564 
565 	errno = 0;
566 	if ((fp = fopen(filename, "r")) != NULL) {
567 		do {
568 			stopchar = getword(fp, word, sizeof(word));
569 			if (stopchar == EOF) {
570 				rval = ISC_R_SUCCESS;
571 				POST(rval);
572 				break;
573 			}
574 
575 			if (strlen(word) == 0U) {
576 				rval = ISC_R_SUCCESS;
577 			} else if (strcmp(word, "nameserver") == 0) {
578 				rval = resconf_parsenameserver(conf, fp);
579 			} else if (strcmp(word, "domain") == 0) {
580 				rval = resconf_parsedomain(conf, fp);
581 			} else if (strcmp(word, "search") == 0) {
582 				rval = resconf_parsesearch(conf, fp);
583 			} else if (strcmp(word, "sortlist") == 0) {
584 				rval = resconf_parsesortlist(conf, fp);
585 			} else if (strcmp(word, "options") == 0) {
586 				rval = resconf_parseoption(conf, fp);
587 			} else {
588 				/* unrecognised word. Ignore entire line */
589 				rval = ISC_R_SUCCESS;
590 				stopchar = eatline(fp);
591 				if (stopchar == EOF) {
592 					break;
593 				}
594 			}
595 			if (ret == ISC_R_SUCCESS && rval != ISC_R_SUCCESS) {
596 				ret = rval;
597 			}
598 		} while (1);
599 
600 		fclose(fp);
601 	} else {
602 		switch (errno) {
603 		case ENOENT:
604 			break;
605 		default:
606 			isc_mem_put(mctx, conf, sizeof(*conf));
607 			return ISC_R_INVALIDFILE;
608 		}
609 	}
610 
611 	if (ret != ISC_R_SUCCESS) {
612 		goto error;
613 	}
614 
615 	/*
616 	 * Construct unified search list from domain or configured
617 	 * search list
618 	 */
619 	if (conf->domainname != NULL) {
620 		ret = add_search(conf, conf->domainname);
621 	} else if (conf->searchnxt > 0) {
622 		for (i = 0; i < conf->searchnxt; i++) {
623 			ret = add_search(conf, conf->search[i]);
624 			if (ret != ISC_R_SUCCESS) {
625 				break;
626 			}
627 		}
628 	}
629 
630 	/* If we don't find a nameserver fall back to localhost */
631 	if (conf->numns == 0U) {
632 		INSIST(ISC_LIST_EMPTY(conf->nameservers));
633 
634 		/* XXX: should we catch errors? */
635 		(void)add_server(conf->mctx, "::1", &conf->nameservers);
636 		(void)add_server(conf->mctx, "127.0.0.1", &conf->nameservers);
637 	}
638 
639 error:
640 	conf->magic = IRS_RESCONF_MAGIC;
641 
642 	if (ret != ISC_R_SUCCESS) {
643 		irs_resconf_destroy(&conf);
644 	} else {
645 		if (fp == NULL) {
646 			ret = ISC_R_FILENOTFOUND;
647 		}
648 		*confp = conf;
649 	}
650 
651 	return ret;
652 }
653 
654 void
655 irs_resconf_destroy(irs_resconf_t **confp) {
656 	irs_resconf_t *conf;
657 	isc_sockaddr_t *address;
658 	irs_resconf_search_t *searchentry;
659 	unsigned int i;
660 
661 	REQUIRE(confp != NULL);
662 	conf = *confp;
663 	*confp = NULL;
664 	REQUIRE(IRS_RESCONF_VALID(conf));
665 
666 	while ((searchentry = ISC_LIST_HEAD(conf->searchlist)) != NULL) {
667 		ISC_LIST_UNLINK(conf->searchlist, searchentry, link);
668 		isc_mem_put(conf->mctx, searchentry, sizeof(*searchentry));
669 	}
670 
671 	while ((address = ISC_LIST_HEAD(conf->nameservers)) != NULL) {
672 		ISC_LIST_UNLINK(conf->nameservers, address, link);
673 		isc_mem_put(conf->mctx, address, sizeof(*address));
674 	}
675 
676 	if (conf->domainname != NULL) {
677 		isc_mem_free(conf->mctx, conf->domainname);
678 	}
679 
680 	for (i = 0; i < RESCONFMAXSEARCH; i++) {
681 		if (conf->search[i] != NULL) {
682 			isc_mem_free(conf->mctx, conf->search[i]);
683 		}
684 	}
685 
686 	isc_mem_put(conf->mctx, conf, sizeof(*conf));
687 }
688 
689 isc_sockaddrlist_t *
690 irs_resconf_getnameservers(irs_resconf_t *conf) {
691 	REQUIRE(IRS_RESCONF_VALID(conf));
692 
693 	return &conf->nameservers;
694 }
695 
696 irs_resconf_searchlist_t *
697 irs_resconf_getsearchlist(irs_resconf_t *conf) {
698 	REQUIRE(IRS_RESCONF_VALID(conf));
699 
700 	return &conf->searchlist;
701 }
702 
703 unsigned int
704 irs_resconf_getndots(irs_resconf_t *conf) {
705 	REQUIRE(IRS_RESCONF_VALID(conf));
706 
707 	return (unsigned int)conf->ndots;
708 }
709 
710 unsigned int
711 irs_resconf_getattempts(irs_resconf_t *conf) {
712 	REQUIRE(IRS_RESCONF_VALID(conf));
713 
714 	return (unsigned int)conf->attempts;
715 }
716 
717 unsigned int
718 irs_resconf_gettimeout(irs_resconf_t *conf) {
719 	REQUIRE(IRS_RESCONF_VALID(conf));
720 
721 	return (unsigned int)conf->timeout;
722 }
723