resconf.c revision 1.6
1/*	$NetBSD: resconf.c,v 1.6 2022/09/23 12:15:32 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#ifndef WIN32
40#include <netdb.h>
41#include <sys/socket.h>
42#include <sys/types.h>
43#endif /* ifndef WIN32 */
44
45#include <ctype.h>
46#include <errno.h>
47#include <inttypes.h>
48#include <stdio.h>
49#include <stdlib.h>
50#include <string.h>
51
52#include <isc/magic.h>
53#include <isc/mem.h>
54#include <isc/netaddr.h>
55#include <isc/sockaddr.h>
56#include <isc/util.h>
57
58#include <irs/netdb.h>
59#include <irs/resconf.h>
60
61#define IRS_RESCONF_MAGIC    ISC_MAGIC('R', 'E', 'S', 'c')
62#define IRS_RESCONF_VALID(c) ISC_MAGIC_VALID(c, IRS_RESCONF_MAGIC)
63
64/*!
65 * protocol constants
66 */
67
68#if !defined(NS_INADDRSZ)
69#define NS_INADDRSZ 4
70#endif /* if !defined(NS_INADDRSZ) */
71
72#if !defined(NS_IN6ADDRSZ)
73#define NS_IN6ADDRSZ 16
74#endif /* if !defined(NS_IN6ADDRSZ) */
75
76/*!
77 * resolv.conf parameters
78 */
79
80#define RESCONFMAXNAMESERVERS 3U   /*%< max 3 "nameserver" entries */
81#define RESCONFMAXSEARCH      8U   /*%< max 8 domains in "search" entry */
82#define RESCONFMAXLINELEN     256U /*%< max size of a line */
83#define RESCONFMAXSORTLIST    10U  /*%< max 10 */
84
85/*!
86 * configuration data structure
87 */
88
89struct irs_resconf {
90	/*
91	 * The configuration data is a thread-specific object, and does not
92	 * need to be locked.
93	 */
94	unsigned int magic;
95	isc_mem_t *mctx;
96
97	isc_sockaddrlist_t nameservers;
98	unsigned int numns; /*%< number of configured servers
99			     * */
100
101	char *domainname;
102	char *search[RESCONFMAXSEARCH];
103	uint8_t searchnxt; /*%< index for next free slot
104			    * */
105
106	irs_resconf_searchlist_t searchlist;
107
108	struct {
109		isc_netaddr_t addr;
110		/*% mask has a non-zero 'family' if set */
111		isc_netaddr_t mask;
112	} sortlist[RESCONFMAXSORTLIST];
113	uint8_t sortlistnxt;
114
115	/*%< non-zero if 'options debug' set */
116	uint8_t resdebug;
117	/*%< set to n in 'options ndots:n' */
118	uint8_t ndots;
119};
120
121static isc_result_t
122resconf_parsenameserver(irs_resconf_t *conf, FILE *fp);
123static isc_result_t
124resconf_parsedomain(irs_resconf_t *conf, FILE *fp);
125static isc_result_t
126resconf_parsesearch(irs_resconf_t *conf, FILE *fp);
127static isc_result_t
128resconf_parsesortlist(irs_resconf_t *conf, FILE *fp);
129static isc_result_t
130resconf_parseoption(irs_resconf_t *ctx, FILE *fp);
131
132#if HAVE_GET_WIN32_NAMESERVERS
133static isc_result_t
134get_win32_nameservers(irs_resconf_t *conf);
135#endif /* if HAVE_GET_WIN32_NAMESERVERS */
136
137/*!
138 * Eat characters from FP until EOL or EOF. Returns EOF or '\n'
139 */
140static int
141eatline(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 */
157static int
158eatwhite(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 */
179static int
180getword(FILE *fp, char *buffer, size_t size) {
181	int ch;
182	char *p;
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
212static isc_result_t
213add_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	/* XXX: special case: treat all-0 IPv4 address as loopback */
232	if (res->ai_family == AF_INET) {
233		struct in_addr *v4;
234		unsigned char zeroaddress[] = { 0, 0, 0, 0 };
235		unsigned char loopaddress[] = { 127, 0, 0, 1 };
236
237		v4 = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
238		if (memcmp(v4, zeroaddress, 4) == 0) {
239			memmove(v4, loopaddress, 4);
240		}
241	}
242
243	address = isc_mem_get(mctx, sizeof(*address));
244	if (res->ai_addrlen > sizeof(address->type)) {
245		isc_mem_put(mctx, address, sizeof(*address));
246		result = ISC_R_RANGE;
247		goto cleanup;
248	}
249	address->length = (unsigned int)res->ai_addrlen;
250	memmove(&address->type.ss, res->ai_addr, res->ai_addrlen);
251	ISC_LINK_INIT(address, link);
252	ISC_LIST_APPEND(*nameservers, address, link);
253
254cleanup:
255	freeaddrinfo(res);
256
257	return (result);
258}
259
260static isc_result_t
261create_addr(const char *buffer, isc_netaddr_t *addr, int convert_zero) {
262	struct in_addr v4;
263	struct in6_addr v6;
264
265	if (inet_pton(AF_INET, buffer, &v4) == 1) {
266		if (convert_zero) {
267			unsigned char zeroaddress[] = { 0, 0, 0, 0 };
268			unsigned char loopaddress[] = { 127, 0, 0, 1 };
269			if (memcmp(&v4, zeroaddress, 4) == 0) {
270				memmove(&v4, loopaddress, 4);
271			}
272		}
273		addr->family = AF_INET;
274		memmove(&addr->type.in, &v4, NS_INADDRSZ);
275		addr->zone = 0;
276	} else if (inet_pton(AF_INET6, buffer, &v6) == 1) {
277		addr->family = AF_INET6;
278		memmove(&addr->type.in6, &v6, NS_IN6ADDRSZ);
279		addr->zone = 0;
280	} else {
281		return (ISC_R_BADADDRESSFORM); /* Unrecognised format. */
282	}
283
284	return (ISC_R_SUCCESS);
285}
286
287static isc_result_t
288resconf_parsenameserver(irs_resconf_t *conf, FILE *fp) {
289	char word[RESCONFMAXLINELEN];
290	int cp;
291	isc_result_t result;
292
293	if (conf->numns == RESCONFMAXNAMESERVERS) {
294		return (ISC_R_SUCCESS);
295	}
296
297	cp = getword(fp, word, sizeof(word));
298	if (strlen(word) == 0U) {
299		return (ISC_R_UNEXPECTEDEND); /* Nothing on line. */
300	} else if (cp == ' ' || cp == '\t') {
301		cp = eatwhite(fp);
302	}
303
304	if (cp != EOF && cp != '\n') {
305		return (ISC_R_UNEXPECTEDTOKEN); /* Extra junk on line. */
306	}
307
308	result = add_server(conf->mctx, word, &conf->nameservers);
309	if (result != ISC_R_SUCCESS) {
310		return (result);
311	}
312	conf->numns++;
313
314	return (ISC_R_SUCCESS);
315}
316
317static isc_result_t
318resconf_parsedomain(irs_resconf_t *conf, FILE *fp) {
319	char word[RESCONFMAXLINELEN];
320	int res;
321	unsigned int i;
322
323	res = getword(fp, word, sizeof(word));
324	if (strlen(word) == 0U) {
325		return (ISC_R_UNEXPECTEDEND); /* Nothing else on line. */
326	} else if (res == ' ' || res == '\t') {
327		res = eatwhite(fp);
328	}
329
330	if (res != EOF && res != '\n') {
331		return (ISC_R_UNEXPECTEDTOKEN); /* Extra junk on line. */
332	}
333
334	if (conf->domainname != NULL) {
335		isc_mem_free(conf->mctx, conf->domainname);
336	}
337
338	/*
339	 * Search and domain are mutually exclusive.
340	 */
341	for (i = 0; i < RESCONFMAXSEARCH; i++) {
342		if (conf->search[i] != NULL) {
343			isc_mem_free(conf->mctx, conf->search[i]);
344			conf->search[i] = NULL;
345		}
346	}
347	conf->searchnxt = 0;
348
349	conf->domainname = isc_mem_strdup(conf->mctx, word);
350
351	return (ISC_R_SUCCESS);
352}
353
354static isc_result_t
355resconf_parsesearch(irs_resconf_t *conf, FILE *fp) {
356	int delim;
357	unsigned int idx;
358	char word[RESCONFMAXLINELEN];
359
360	if (conf->domainname != NULL) {
361		/*
362		 * Search and domain are mutually exclusive.
363		 */
364		isc_mem_free(conf->mctx, conf->domainname);
365		conf->domainname = NULL;
366	}
367
368	/*
369	 * Remove any previous search definitions.
370	 */
371	for (idx = 0; idx < RESCONFMAXSEARCH; idx++) {
372		if (conf->search[idx] != NULL) {
373			isc_mem_free(conf->mctx, conf->search[idx]);
374			conf->search[idx] = NULL;
375		}
376	}
377	conf->searchnxt = 0;
378
379	delim = getword(fp, word, sizeof(word));
380	if (strlen(word) == 0U) {
381		return (ISC_R_UNEXPECTEDEND); /* Nothing else on line. */
382	}
383
384	idx = 0;
385	while (strlen(word) > 0U) {
386		if (conf->searchnxt == RESCONFMAXSEARCH) {
387			goto ignore; /* Too many domains. */
388		}
389
390		INSIST(idx < sizeof(conf->search) / sizeof(conf->search[0]));
391		conf->search[idx] = isc_mem_strdup(conf->mctx, word);
392		idx++;
393		conf->searchnxt++;
394
395	ignore:
396		if (delim == EOF || delim == '\n') {
397			break;
398		} else {
399			delim = getword(fp, word, sizeof(word));
400		}
401	}
402
403	return (ISC_R_SUCCESS);
404}
405
406static isc_result_t
407resconf_parsesortlist(irs_resconf_t *conf, FILE *fp) {
408	int delim, res;
409	unsigned int idx;
410	char word[RESCONFMAXLINELEN];
411	char *p;
412
413	delim = getword(fp, word, sizeof(word));
414	if (strlen(word) == 0U) {
415		return (ISC_R_UNEXPECTEDEND); /* Empty line after keyword. */
416	}
417
418	while (strlen(word) > 0U) {
419		if (conf->sortlistnxt == RESCONFMAXSORTLIST) {
420			return (ISC_R_QUOTA); /* Too many values. */
421		}
422
423		p = strchr(word, '/');
424		if (p != NULL) {
425			*p++ = '\0';
426		}
427
428		idx = conf->sortlistnxt;
429		INSIST(idx <
430		       sizeof(conf->sortlist) / sizeof(conf->sortlist[0]));
431		res = create_addr(word, &conf->sortlist[idx].addr, 1);
432		if (res != ISC_R_SUCCESS) {
433			return (res);
434		}
435
436		if (p != NULL) {
437			res = create_addr(p, &conf->sortlist[idx].mask, 0);
438			if (res != ISC_R_SUCCESS) {
439				return (res);
440			}
441		} else {
442			/*
443			 * Make up a mask. (XXX: is this correct?)
444			 */
445			conf->sortlist[idx].mask = conf->sortlist[idx].addr;
446			memset(&conf->sortlist[idx].mask.type, 0xff,
447			       sizeof(conf->sortlist[idx].mask.type));
448		}
449
450		conf->sortlistnxt++;
451
452		if (delim == EOF || delim == '\n') {
453			break;
454		} else {
455			delim = getword(fp, word, sizeof(word));
456		}
457	}
458
459	return (ISC_R_SUCCESS);
460}
461
462static isc_result_t
463resconf_parseoption(irs_resconf_t *conf, FILE *fp) {
464	int delim;
465	long ndots;
466	char *p;
467	char word[RESCONFMAXLINELEN];
468
469	delim = getword(fp, word, sizeof(word));
470	if (strlen(word) == 0U) {
471		return (ISC_R_UNEXPECTEDEND); /* Empty line after keyword. */
472	}
473
474	while (strlen(word) > 0U) {
475		if (strcmp("debug", word) == 0) {
476			conf->resdebug = 1;
477		} else if (strncmp("ndots:", word, 6) == 0) {
478			ndots = strtol(word + 6, &p, 10);
479			if (*p != '\0') { /* Bad string. */
480				return (ISC_R_UNEXPECTEDTOKEN);
481			}
482			if (ndots < 0 || ndots > 0xff) { /* Out of range. */
483				return (ISC_R_RANGE);
484			}
485			conf->ndots = (uint8_t)ndots;
486		}
487
488		if (delim == EOF || delim == '\n') {
489			break;
490		} else {
491			delim = getword(fp, word, sizeof(word));
492		}
493	}
494
495	return (ISC_R_SUCCESS);
496}
497
498static isc_result_t
499add_search(irs_resconf_t *conf, char *domain) {
500	irs_resconf_search_t *entry;
501
502	entry = isc_mem_get(conf->mctx, sizeof(*entry));
503
504	entry->domain = domain;
505	ISC_LINK_INIT(entry, link);
506	ISC_LIST_APPEND(conf->searchlist, entry, link);
507
508	return (ISC_R_SUCCESS);
509}
510
511/*% parses a file and fills in the data structure. */
512isc_result_t
513irs_resconf_load(isc_mem_t *mctx, const char *filename, irs_resconf_t **confp) {
514	FILE *fp = NULL;
515	char word[256];
516	isc_result_t rval, ret = ISC_R_SUCCESS;
517	irs_resconf_t *conf;
518	unsigned int i;
519	int stopchar;
520
521	REQUIRE(mctx != NULL);
522	REQUIRE(filename != NULL);
523	REQUIRE(strlen(filename) > 0U);
524	REQUIRE(confp != NULL && *confp == NULL);
525
526	conf = isc_mem_get(mctx, sizeof(*conf));
527
528	conf->mctx = mctx;
529	ISC_LIST_INIT(conf->nameservers);
530	ISC_LIST_INIT(conf->searchlist);
531	conf->numns = 0;
532	conf->domainname = NULL;
533	conf->searchnxt = 0;
534	conf->sortlistnxt = 0;
535	conf->resdebug = 0;
536	conf->ndots = 1;
537	for (i = 0; i < RESCONFMAXSEARCH; i++) {
538		conf->search[i] = NULL;
539	}
540
541	errno = 0;
542	if ((fp = fopen(filename, "r")) != NULL) {
543		do {
544			stopchar = getword(fp, word, sizeof(word));
545			if (stopchar == EOF) {
546				rval = ISC_R_SUCCESS;
547				POST(rval);
548				break;
549			}
550
551			if (strlen(word) == 0U) {
552				rval = ISC_R_SUCCESS;
553			} else if (strcmp(word, "nameserver") == 0) {
554				rval = resconf_parsenameserver(conf, fp);
555			} else if (strcmp(word, "domain") == 0) {
556				rval = resconf_parsedomain(conf, fp);
557			} else if (strcmp(word, "search") == 0) {
558				rval = resconf_parsesearch(conf, fp);
559			} else if (strcmp(word, "sortlist") == 0) {
560				rval = resconf_parsesortlist(conf, fp);
561			} else if (strcmp(word, "options") == 0) {
562				rval = resconf_parseoption(conf, fp);
563			} else {
564				/* unrecognised word. Ignore entire line */
565				rval = ISC_R_SUCCESS;
566				stopchar = eatline(fp);
567				if (stopchar == EOF) {
568					break;
569				}
570			}
571			if (ret == ISC_R_SUCCESS && rval != ISC_R_SUCCESS) {
572				ret = rval;
573			}
574		} while (1);
575
576		fclose(fp);
577	} else {
578		switch (errno) {
579		case ENOENT:
580			break;
581		default:
582			isc_mem_put(mctx, conf, sizeof(*conf));
583			return (ISC_R_INVALIDFILE);
584		}
585	}
586
587	if (ret != ISC_R_SUCCESS) {
588		goto error;
589	}
590
591	/*
592	 * Construct unified search list from domain or configured
593	 * search list
594	 */
595	if (conf->domainname != NULL) {
596		ret = add_search(conf, conf->domainname);
597	} else if (conf->searchnxt > 0) {
598		for (i = 0; i < conf->searchnxt; i++) {
599			ret = add_search(conf, conf->search[i]);
600			if (ret != ISC_R_SUCCESS) {
601				break;
602			}
603		}
604	}
605
606#if HAVE_GET_WIN32_NAMESERVERS
607	ret = get_win32_nameservers(conf);
608	if (ret != ISC_R_SUCCESS) {
609		goto error;
610	}
611#endif /* if HAVE_GET_WIN32_NAMESERVERS */
612
613	/* If we don't find a nameserver fall back to localhost */
614	if (conf->numns == 0U) {
615		INSIST(ISC_LIST_EMPTY(conf->nameservers));
616
617		/* XXX: should we catch errors? */
618		(void)add_server(conf->mctx, "::1", &conf->nameservers);
619		(void)add_server(conf->mctx, "127.0.0.1", &conf->nameservers);
620	}
621
622error:
623	conf->magic = IRS_RESCONF_MAGIC;
624
625	if (ret != ISC_R_SUCCESS) {
626		irs_resconf_destroy(&conf);
627	} else {
628		if (fp == NULL) {
629			ret = ISC_R_FILENOTFOUND;
630		}
631		*confp = conf;
632	}
633
634	return (ret);
635}
636
637void
638irs_resconf_destroy(irs_resconf_t **confp) {
639	irs_resconf_t *conf;
640	isc_sockaddr_t *address;
641	irs_resconf_search_t *searchentry;
642	unsigned int i;
643
644	REQUIRE(confp != NULL);
645	conf = *confp;
646	*confp = NULL;
647	REQUIRE(IRS_RESCONF_VALID(conf));
648
649	while ((searchentry = ISC_LIST_HEAD(conf->searchlist)) != NULL) {
650		ISC_LIST_UNLINK(conf->searchlist, searchentry, link);
651		isc_mem_put(conf->mctx, searchentry, sizeof(*searchentry));
652	}
653
654	while ((address = ISC_LIST_HEAD(conf->nameservers)) != NULL) {
655		ISC_LIST_UNLINK(conf->nameservers, address, link);
656		isc_mem_put(conf->mctx, address, sizeof(*address));
657	}
658
659	if (conf->domainname != NULL) {
660		isc_mem_free(conf->mctx, conf->domainname);
661	}
662
663	for (i = 0; i < RESCONFMAXSEARCH; i++) {
664		if (conf->search[i] != NULL) {
665			isc_mem_free(conf->mctx, conf->search[i]);
666		}
667	}
668
669	isc_mem_put(conf->mctx, conf, sizeof(*conf));
670}
671
672isc_sockaddrlist_t *
673irs_resconf_getnameservers(irs_resconf_t *conf) {
674	REQUIRE(IRS_RESCONF_VALID(conf));
675
676	return (&conf->nameservers);
677}
678
679irs_resconf_searchlist_t *
680irs_resconf_getsearchlist(irs_resconf_t *conf) {
681	REQUIRE(IRS_RESCONF_VALID(conf));
682
683	return (&conf->searchlist);
684}
685
686unsigned int
687irs_resconf_getndots(irs_resconf_t *conf) {
688	REQUIRE(IRS_RESCONF_VALID(conf));
689
690	return ((unsigned int)conf->ndots);
691}
692