1/*	$NetBSD: resconf.c,v 1.8 2024/02/21 22:52: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
92struct 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
104	char *domainname;
105	char *search[RESCONFMAXSEARCH];
106	uint8_t searchnxt; /*%< index for next free slot
107			    * */
108
109	irs_resconf_searchlist_t searchlist;
110
111	struct {
112		isc_netaddr_t addr;
113		/*% mask has a non-zero 'family' if set */
114		isc_netaddr_t mask;
115	} sortlist[RESCONFMAXSORTLIST];
116	uint8_t sortlistnxt;
117
118	/*%< non-zero if 'options debug' set */
119	uint8_t resdebug;
120	/*%< set to n in 'options ndots:n' */
121	uint8_t ndots;
122	/*%< set to n in 'options attempts:n' */
123	uint8_t attempts;
124	/*%< set to n in 'options timeout:n' */
125	uint8_t timeout;
126};
127
128static isc_result_t
129resconf_parsenameserver(irs_resconf_t *conf, FILE *fp);
130static isc_result_t
131resconf_parsedomain(irs_resconf_t *conf, FILE *fp);
132static isc_result_t
133resconf_parsesearch(irs_resconf_t *conf, FILE *fp);
134static isc_result_t
135resconf_parsesortlist(irs_resconf_t *conf, FILE *fp);
136static isc_result_t
137resconf_parseoption(irs_resconf_t *ctx, FILE *fp);
138
139/*!
140 * Eat characters from FP until EOL or EOF. Returns EOF or '\n'
141 */
142static int
143eatline(FILE *fp) {
144	int ch;
145
146	ch = fgetc(fp);
147	while (ch != '\n' && ch != EOF) {
148		ch = fgetc(fp);
149	}
150
151	return (ch);
152}
153
154/*!
155 * Eats white space up to next newline or non-whitespace character (of
156 * EOF). Returns the last character read. Comments are considered white
157 * space.
158 */
159static int
160eatwhite(FILE *fp) {
161	int ch;
162
163	ch = fgetc(fp);
164	while (ch != '\n' && ch != EOF && isspace((unsigned char)ch)) {
165		ch = fgetc(fp);
166	}
167
168	if (ch == ';' || ch == '#') {
169		ch = eatline(fp);
170	}
171
172	return (ch);
173}
174
175/*!
176 * Skip over any leading whitespace and then read in the next sequence of
177 * non-whitespace characters. In this context newline is not considered
178 * whitespace. Returns EOF on end-of-file, or the character
179 * that caused the reading to stop.
180 */
181static int
182getword(FILE *fp, char *buffer, size_t size) {
183	char *p = NULL;
184	int ch;
185
186	REQUIRE(buffer != NULL);
187	REQUIRE(size > 0U);
188
189	p = buffer;
190	*p = '\0';
191
192	ch = eatwhite(fp);
193
194	if (ch == EOF) {
195		return (EOF);
196	}
197
198	do {
199		*p = '\0';
200
201		if (ch == EOF || isspace((unsigned char)ch)) {
202			break;
203		} else if ((size_t)(p - buffer) == size - 1) {
204			return (EOF); /* Not enough space. */
205		}
206
207		*p++ = (char)ch;
208		ch = fgetc(fp);
209	} while (1);
210
211	return (ch);
212}
213
214static isc_result_t
215add_server(isc_mem_t *mctx, const char *address_str,
216	   isc_sockaddrlist_t *nameservers) {
217	int error;
218	isc_sockaddr_t *address = NULL;
219	struct addrinfo hints, *res;
220	isc_result_t result = ISC_R_SUCCESS;
221
222	res = NULL;
223	memset(&hints, 0, sizeof(hints));
224	hints.ai_family = AF_UNSPEC;
225	hints.ai_socktype = SOCK_DGRAM;
226	hints.ai_protocol = IPPROTO_UDP;
227	hints.ai_flags = AI_NUMERICHOST;
228	error = getaddrinfo(address_str, "53", &hints, &res);
229	if (error != 0) {
230		return (ISC_R_BADADDRESSFORM);
231	}
232
233	/* XXX: special case: treat all-0 IPv4 address as loopback */
234	if (res->ai_family == AF_INET) {
235		struct in_addr *v4;
236		unsigned char zeroaddress[] = { 0, 0, 0, 0 };
237		unsigned char loopaddress[] = { 127, 0, 0, 1 };
238
239		v4 = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
240		if (memcmp(v4, zeroaddress, 4) == 0) {
241			memmove(v4, loopaddress, 4);
242		}
243	}
244
245	address = isc_mem_get(mctx, sizeof(*address));
246	if (res->ai_addrlen > sizeof(address->type)) {
247		isc_mem_put(mctx, address, sizeof(*address));
248		result = ISC_R_RANGE;
249		goto cleanup;
250	}
251	address->length = (unsigned int)res->ai_addrlen;
252	memmove(&address->type.ss, res->ai_addr, res->ai_addrlen);
253	ISC_LINK_INIT(address, link);
254	ISC_LIST_APPEND(*nameservers, address, link);
255
256cleanup:
257	freeaddrinfo(res);
258
259	return (result);
260}
261
262static isc_result_t
263create_addr(const char *buffer, isc_netaddr_t *addr, int convert_zero) {
264	struct in_addr v4;
265	struct in6_addr v6;
266
267	if (inet_pton(AF_INET, buffer, &v4) == 1) {
268		if (convert_zero) {
269			unsigned char zeroaddress[] = { 0, 0, 0, 0 };
270			unsigned char loopaddress[] = { 127, 0, 0, 1 };
271			if (memcmp(&v4, zeroaddress, 4) == 0) {
272				memmove(&v4, loopaddress, 4);
273			}
274		}
275		addr->family = AF_INET;
276		memmove(&addr->type.in, &v4, NS_INADDRSZ);
277		addr->zone = 0;
278	} else if (inet_pton(AF_INET6, buffer, &v6) == 1) {
279		addr->family = AF_INET6;
280		memmove(&addr->type.in6, &v6, NS_IN6ADDRSZ);
281		addr->zone = 0;
282	} else {
283		return (ISC_R_BADADDRESSFORM); /* Unrecognised format. */
284	}
285
286	return (ISC_R_SUCCESS);
287}
288
289static isc_result_t
290resconf_parsenameserver(irs_resconf_t *conf, FILE *fp) {
291	char word[RESCONFMAXLINELEN];
292	int cp;
293	isc_result_t result;
294
295	cp = getword(fp, word, sizeof(word));
296	if (strlen(word) == 0U) {
297		return (ISC_R_UNEXPECTEDEND); /* Nothing on line. */
298	} else if (cp == ' ' || cp == '\t') {
299		cp = eatwhite(fp);
300	}
301
302	if (cp != EOF && cp != '\n') {
303		return (ISC_R_UNEXPECTEDTOKEN); /* Extra junk on line. */
304	}
305
306	if (conf->numns == RESCONFMAXNAMESERVERS) {
307		return (ISC_R_SUCCESS);
308	}
309
310	result = add_server(conf->mctx, word, &conf->nameservers);
311	if (result != ISC_R_SUCCESS) {
312		return (result);
313	}
314	conf->numns++;
315
316	return (ISC_R_SUCCESS);
317}
318
319static isc_result_t
320resconf_parsedomain(irs_resconf_t *conf, FILE *fp) {
321	char word[RESCONFMAXLINELEN];
322	int res;
323	unsigned int i;
324
325	res = getword(fp, word, sizeof(word));
326	if (strlen(word) == 0U) {
327		return (ISC_R_UNEXPECTEDEND); /* Nothing else on line. */
328	} else if (res == ' ' || res == '\t') {
329		res = eatwhite(fp);
330	}
331
332	if (res != EOF && res != '\n') {
333		return (ISC_R_UNEXPECTEDTOKEN); /* Extra junk on line. */
334	}
335
336	if (conf->domainname != NULL) {
337		isc_mem_free(conf->mctx, conf->domainname);
338	}
339
340	/*
341	 * Search and domain are mutually exclusive.
342	 */
343	for (i = 0; i < RESCONFMAXSEARCH; i++) {
344		if (conf->search[i] != NULL) {
345			isc_mem_free(conf->mctx, conf->search[i]);
346			conf->search[i] = NULL;
347		}
348	}
349	conf->searchnxt = 0;
350
351	conf->domainname = isc_mem_strdup(conf->mctx, word);
352
353	return (ISC_R_SUCCESS);
354}
355
356static isc_result_t
357resconf_parsesearch(irs_resconf_t *conf, FILE *fp) {
358	int delim;
359	unsigned int idx;
360	char word[RESCONFMAXLINELEN];
361
362	if (conf->domainname != NULL) {
363		/*
364		 * Search and domain are mutually exclusive.
365		 */
366		isc_mem_free(conf->mctx, conf->domainname);
367		conf->domainname = NULL;
368	}
369
370	/*
371	 * Remove any previous search definitions.
372	 */
373	for (idx = 0; idx < RESCONFMAXSEARCH; idx++) {
374		if (conf->search[idx] != NULL) {
375			isc_mem_free(conf->mctx, conf->search[idx]);
376			conf->search[idx] = NULL;
377		}
378	}
379	conf->searchnxt = 0;
380
381	delim = getword(fp, word, sizeof(word));
382	if (strlen(word) == 0U) {
383		return (ISC_R_UNEXPECTEDEND); /* Nothing else on line. */
384	}
385
386	idx = 0;
387	while (strlen(word) > 0U) {
388		if (conf->searchnxt == RESCONFMAXSEARCH) {
389			goto ignore; /* Too many domains. */
390		}
391
392		INSIST(idx < sizeof(conf->search) / sizeof(conf->search[0]));
393		conf->search[idx] = isc_mem_strdup(conf->mctx, word);
394		idx++;
395		conf->searchnxt++;
396
397	ignore:
398		if (delim == EOF || delim == '\n') {
399			break;
400		} else {
401			delim = getword(fp, word, sizeof(word));
402		}
403	}
404
405	return (ISC_R_SUCCESS);
406}
407
408static isc_result_t
409resconf_parsesortlist(irs_resconf_t *conf, FILE *fp) {
410	int delim, res;
411	unsigned int idx;
412	char word[RESCONFMAXLINELEN];
413	char *p;
414
415	delim = getword(fp, word, sizeof(word));
416	if (strlen(word) == 0U) {
417		return (ISC_R_UNEXPECTEDEND); /* Empty line after keyword. */
418	}
419
420	while (strlen(word) > 0U) {
421		if (conf->sortlistnxt == RESCONFMAXSORTLIST) {
422			return (ISC_R_QUOTA); /* Too many values. */
423		}
424
425		p = strchr(word, '/');
426		if (p != NULL) {
427			*p++ = '\0';
428		}
429
430		idx = conf->sortlistnxt;
431		INSIST(idx <
432		       sizeof(conf->sortlist) / sizeof(conf->sortlist[0]));
433		res = create_addr(word, &conf->sortlist[idx].addr, 1);
434		if (res != ISC_R_SUCCESS) {
435			return (res);
436		}
437
438		if (p != NULL) {
439			res = create_addr(p, &conf->sortlist[idx].mask, 0);
440			if (res != ISC_R_SUCCESS) {
441				return (res);
442			}
443		} else {
444			/*
445			 * Make up a mask. (XXX: is this correct?)
446			 */
447			conf->sortlist[idx].mask = conf->sortlist[idx].addr;
448			memset(&conf->sortlist[idx].mask.type, 0xff,
449			       sizeof(conf->sortlist[idx].mask.type));
450		}
451
452		conf->sortlistnxt++;
453
454		if (delim == EOF || delim == '\n') {
455			break;
456		} else {
457			delim = getword(fp, word, sizeof(word));
458		}
459	}
460
461	return (ISC_R_SUCCESS);
462}
463
464static isc_result_t
465resconf_optionnumber(const char *word, uint8_t *number) {
466	char *p;
467	long n;
468
469	n = strtol(word, &p, 10);
470	if (*p != '\0') { /* Bad string. */
471		return (ISC_R_UNEXPECTEDTOKEN);
472	}
473	if (n < 0 || n > 0xff) { /* Out of range. */
474		return (ISC_R_RANGE);
475	}
476	*number = n;
477	return (ISC_R_SUCCESS);
478}
479
480static isc_result_t
481resconf_parseoption(irs_resconf_t *conf, FILE *fp) {
482	int delim;
483	isc_result_t result = ISC_R_SUCCESS;
484	char word[RESCONFMAXLINELEN];
485
486	delim = getword(fp, word, sizeof(word));
487	if (strlen(word) == 0U) {
488		return (ISC_R_UNEXPECTEDEND); /* Empty line after keyword. */
489	}
490
491	while (strlen(word) > 0U) {
492		if (strcmp("debug", word) == 0) {
493			conf->resdebug = 1;
494		} else if (strncmp("ndots:", word, 6) == 0) {
495			CHECK(resconf_optionnumber(word + 6, &conf->ndots));
496		} else if (strncmp("attempts:", word, 9) == 0) {
497			CHECK(resconf_optionnumber(word + 9, &conf->attempts));
498		} else if (strncmp("timeout:", word, 8) == 0) {
499			CHECK(resconf_optionnumber(word + 8, &conf->timeout));
500		}
501
502		if (delim == EOF || delim == '\n') {
503			break;
504		} else {
505			delim = getword(fp, word, sizeof(word));
506		}
507	}
508
509cleanup:
510	return (result);
511}
512
513static isc_result_t
514add_search(irs_resconf_t *conf, char *domain) {
515	irs_resconf_search_t *entry;
516
517	entry = isc_mem_get(conf->mctx, sizeof(*entry));
518
519	entry->domain = domain;
520	ISC_LINK_INIT(entry, link);
521	ISC_LIST_APPEND(conf->searchlist, entry, link);
522
523	return (ISC_R_SUCCESS);
524}
525
526/*% parses a file and fills in the data structure. */
527isc_result_t
528irs_resconf_load(isc_mem_t *mctx, const char *filename, irs_resconf_t **confp) {
529	FILE *fp = NULL;
530	char word[256];
531	isc_result_t rval, ret = ISC_R_SUCCESS;
532	irs_resconf_t *conf;
533	unsigned int i;
534	int stopchar;
535
536	REQUIRE(mctx != NULL);
537	REQUIRE(filename != NULL);
538	REQUIRE(strlen(filename) > 0U);
539	REQUIRE(confp != NULL && *confp == NULL);
540
541	conf = isc_mem_get(mctx, sizeof(*conf));
542
543	conf->mctx = mctx;
544	ISC_LIST_INIT(conf->nameservers);
545	ISC_LIST_INIT(conf->searchlist);
546	conf->numns = 0;
547	conf->domainname = NULL;
548	conf->searchnxt = 0;
549	conf->sortlistnxt = 0;
550	conf->resdebug = 0;
551	conf->ndots = 1;
552	conf->attempts = 3;
553	conf->timeout = 0;
554	for (i = 0; i < RESCONFMAXSEARCH; i++) {
555		conf->search[i] = NULL;
556	}
557
558	errno = 0;
559	if ((fp = fopen(filename, "r")) != NULL) {
560		do {
561			stopchar = getword(fp, word, sizeof(word));
562			if (stopchar == EOF) {
563				rval = ISC_R_SUCCESS;
564				POST(rval);
565				break;
566			}
567
568			if (strlen(word) == 0U) {
569				rval = ISC_R_SUCCESS;
570			} else if (strcmp(word, "nameserver") == 0) {
571				rval = resconf_parsenameserver(conf, fp);
572			} else if (strcmp(word, "domain") == 0) {
573				rval = resconf_parsedomain(conf, fp);
574			} else if (strcmp(word, "search") == 0) {
575				rval = resconf_parsesearch(conf, fp);
576			} else if (strcmp(word, "sortlist") == 0) {
577				rval = resconf_parsesortlist(conf, fp);
578			} else if (strcmp(word, "options") == 0) {
579				rval = resconf_parseoption(conf, fp);
580			} else {
581				/* unrecognised word. Ignore entire line */
582				rval = ISC_R_SUCCESS;
583				stopchar = eatline(fp);
584				if (stopchar == EOF) {
585					break;
586				}
587			}
588			if (ret == ISC_R_SUCCESS && rval != ISC_R_SUCCESS) {
589				ret = rval;
590			}
591		} while (1);
592
593		fclose(fp);
594	} else {
595		switch (errno) {
596		case ENOENT:
597			break;
598		default:
599			isc_mem_put(mctx, conf, sizeof(*conf));
600			return (ISC_R_INVALIDFILE);
601		}
602	}
603
604	if (ret != ISC_R_SUCCESS) {
605		goto error;
606	}
607
608	/*
609	 * Construct unified search list from domain or configured
610	 * search list
611	 */
612	if (conf->domainname != NULL) {
613		ret = add_search(conf, conf->domainname);
614	} else if (conf->searchnxt > 0) {
615		for (i = 0; i < conf->searchnxt; i++) {
616			ret = add_search(conf, conf->search[i]);
617			if (ret != ISC_R_SUCCESS) {
618				break;
619			}
620		}
621	}
622
623	/* If we don't find a nameserver fall back to localhost */
624	if (conf->numns == 0U) {
625		INSIST(ISC_LIST_EMPTY(conf->nameservers));
626
627		/* XXX: should we catch errors? */
628		(void)add_server(conf->mctx, "::1", &conf->nameservers);
629		(void)add_server(conf->mctx, "127.0.0.1", &conf->nameservers);
630	}
631
632error:
633	conf->magic = IRS_RESCONF_MAGIC;
634
635	if (ret != ISC_R_SUCCESS) {
636		irs_resconf_destroy(&conf);
637	} else {
638		if (fp == NULL) {
639			ret = ISC_R_FILENOTFOUND;
640		}
641		*confp = conf;
642	}
643
644	return (ret);
645}
646
647void
648irs_resconf_destroy(irs_resconf_t **confp) {
649	irs_resconf_t *conf;
650	isc_sockaddr_t *address;
651	irs_resconf_search_t *searchentry;
652	unsigned int i;
653
654	REQUIRE(confp != NULL);
655	conf = *confp;
656	*confp = NULL;
657	REQUIRE(IRS_RESCONF_VALID(conf));
658
659	while ((searchentry = ISC_LIST_HEAD(conf->searchlist)) != NULL) {
660		ISC_LIST_UNLINK(conf->searchlist, searchentry, link);
661		isc_mem_put(conf->mctx, searchentry, sizeof(*searchentry));
662	}
663
664	while ((address = ISC_LIST_HEAD(conf->nameservers)) != NULL) {
665		ISC_LIST_UNLINK(conf->nameservers, address, link);
666		isc_mem_put(conf->mctx, address, sizeof(*address));
667	}
668
669	if (conf->domainname != NULL) {
670		isc_mem_free(conf->mctx, conf->domainname);
671	}
672
673	for (i = 0; i < RESCONFMAXSEARCH; i++) {
674		if (conf->search[i] != NULL) {
675			isc_mem_free(conf->mctx, conf->search[i]);
676		}
677	}
678
679	isc_mem_put(conf->mctx, conf, sizeof(*conf));
680}
681
682isc_sockaddrlist_t *
683irs_resconf_getnameservers(irs_resconf_t *conf) {
684	REQUIRE(IRS_RESCONF_VALID(conf));
685
686	return (&conf->nameservers);
687}
688
689irs_resconf_searchlist_t *
690irs_resconf_getsearchlist(irs_resconf_t *conf) {
691	REQUIRE(IRS_RESCONF_VALID(conf));
692
693	return (&conf->searchlist);
694}
695
696unsigned int
697irs_resconf_getndots(irs_resconf_t *conf) {
698	REQUIRE(IRS_RESCONF_VALID(conf));
699
700	return ((unsigned int)conf->ndots);
701}
702
703unsigned int
704irs_resconf_getattempts(irs_resconf_t *conf) {
705	REQUIRE(IRS_RESCONF_VALID(conf));
706
707	return ((unsigned int)conf->attempts);
708}
709
710unsigned int
711irs_resconf_gettimeout(irs_resconf_t *conf) {
712	REQUIRE(IRS_RESCONF_VALID(conf));
713
714	return ((unsigned int)conf->timeout);
715}
716