/* $NetBSD: resconf.c,v 1.6.2.2 2024/02/25 15:47:12 martin Exp $ */ /* * Copyright (C) Internet Systems Consortium, Inc. ("ISC") * * SPDX-License-Identifier: MPL-2.0 * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at https://mozilla.org/MPL/2.0/. * * See the COPYRIGHT file distributed with this work for additional * information regarding copyright ownership. */ /*! \file resconf.c */ /** * Module for parsing resolv.conf files (largely derived from lwconfig.c). * * irs_resconf_load() opens the file filename and parses it to initialize * the configuration structure. * * \section lwconfig_return Return Values * * irs_resconf_load() returns #IRS_R_SUCCESS if it successfully read and * parsed filename. It returns a non-0 error code if filename could not be * opened or contained incorrect resolver statements. * * \section lwconfig_see See Also * * stdio(3), \link resolver resolver \endlink * * \section files Files * * /etc/resolv.conf */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define IRS_RESCONF_MAGIC ISC_MAGIC('R', 'E', 'S', 'c') #define IRS_RESCONF_VALID(c) ISC_MAGIC_VALID(c, IRS_RESCONF_MAGIC) /*! * protocol constants */ #if !defined(NS_INADDRSZ) #define NS_INADDRSZ 4 #endif /* if !defined(NS_INADDRSZ) */ #if !defined(NS_IN6ADDRSZ) #define NS_IN6ADDRSZ 16 #endif /* if !defined(NS_IN6ADDRSZ) */ /*! * resolv.conf parameters */ #define RESCONFMAXNAMESERVERS 3U /*%< max 3 "nameserver" entries */ #define RESCONFMAXSEARCH 8U /*%< max 8 domains in "search" entry */ #define RESCONFMAXLINELEN 256U /*%< max size of a line */ #define RESCONFMAXSORTLIST 10U /*%< max 10 */ #define CHECK(op) \ do { \ result = (op); \ if (result != ISC_R_SUCCESS) \ goto cleanup; \ } while (0) /*! * configuration data structure */ struct irs_resconf { /* * The configuration data is a thread-specific object, and does not * need to be locked. */ unsigned int magic; isc_mem_t *mctx; isc_sockaddrlist_t nameservers; unsigned int numns; /*%< number of configured servers * */ char *domainname; char *search[RESCONFMAXSEARCH]; uint8_t searchnxt; /*%< index for next free slot * */ irs_resconf_searchlist_t searchlist; struct { isc_netaddr_t addr; /*% mask has a non-zero 'family' if set */ isc_netaddr_t mask; } sortlist[RESCONFMAXSORTLIST]; uint8_t sortlistnxt; /*%< non-zero if 'options debug' set */ uint8_t resdebug; /*%< set to n in 'options ndots:n' */ uint8_t ndots; /*%< set to n in 'options attempts:n' */ uint8_t attempts; /*%< set to n in 'options timeout:n' */ uint8_t timeout; }; static isc_result_t resconf_parsenameserver(irs_resconf_t *conf, FILE *fp); static isc_result_t resconf_parsedomain(irs_resconf_t *conf, FILE *fp); static isc_result_t resconf_parsesearch(irs_resconf_t *conf, FILE *fp); static isc_result_t resconf_parsesortlist(irs_resconf_t *conf, FILE *fp); static isc_result_t resconf_parseoption(irs_resconf_t *ctx, FILE *fp); /*! * Eat characters from FP until EOL or EOF. Returns EOF or '\n' */ static int eatline(FILE *fp) { int ch; ch = fgetc(fp); while (ch != '\n' && ch != EOF) { ch = fgetc(fp); } return (ch); } /*! * Eats white space up to next newline or non-whitespace character (of * EOF). Returns the last character read. Comments are considered white * space. */ static int eatwhite(FILE *fp) { int ch; ch = fgetc(fp); while (ch != '\n' && ch != EOF && isspace((unsigned char)ch)) { ch = fgetc(fp); } if (ch == ';' || ch == '#') { ch = eatline(fp); } return (ch); } /*! * Skip over any leading whitespace and then read in the next sequence of * non-whitespace characters. In this context newline is not considered * whitespace. Returns EOF on end-of-file, or the character * that caused the reading to stop. */ static int getword(FILE *fp, char *buffer, size_t size) { char *p = NULL; int ch; REQUIRE(buffer != NULL); REQUIRE(size > 0U); p = buffer; *p = '\0'; ch = eatwhite(fp); if (ch == EOF) { return (EOF); } do { *p = '\0'; if (ch == EOF || isspace((unsigned char)ch)) { break; } else if ((size_t)(p - buffer) == size - 1) { return (EOF); /* Not enough space. */ } *p++ = (char)ch; ch = fgetc(fp); } while (1); return (ch); } static isc_result_t add_server(isc_mem_t *mctx, const char *address_str, isc_sockaddrlist_t *nameservers) { int error; isc_sockaddr_t *address = NULL; struct addrinfo hints, *res; isc_result_t result = ISC_R_SUCCESS; res = NULL; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; hints.ai_protocol = IPPROTO_UDP; hints.ai_flags = AI_NUMERICHOST; error = getaddrinfo(address_str, "53", &hints, &res); if (error != 0) { return (ISC_R_BADADDRESSFORM); } /* XXX: special case: treat all-0 IPv4 address as loopback */ if (res->ai_family == AF_INET) { struct in_addr *v4; unsigned char zeroaddress[] = { 0, 0, 0, 0 }; unsigned char loopaddress[] = { 127, 0, 0, 1 }; v4 = &((struct sockaddr_in *)res->ai_addr)->sin_addr; if (memcmp(v4, zeroaddress, 4) == 0) { memmove(v4, loopaddress, 4); } } address = isc_mem_get(mctx, sizeof(*address)); if (res->ai_addrlen > sizeof(address->type)) { isc_mem_put(mctx, address, sizeof(*address)); result = ISC_R_RANGE; goto cleanup; } address->length = (unsigned int)res->ai_addrlen; memmove(&address->type.ss, res->ai_addr, res->ai_addrlen); ISC_LINK_INIT(address, link); ISC_LIST_APPEND(*nameservers, address, link); cleanup: freeaddrinfo(res); return (result); } static isc_result_t create_addr(const char *buffer, isc_netaddr_t *addr, int convert_zero) { struct in_addr v4; struct in6_addr v6; if (inet_pton(AF_INET, buffer, &v4) == 1) { if (convert_zero) { unsigned char zeroaddress[] = { 0, 0, 0, 0 }; unsigned char loopaddress[] = { 127, 0, 0, 1 }; if (memcmp(&v4, zeroaddress, 4) == 0) { memmove(&v4, loopaddress, 4); } } addr->family = AF_INET; memmove(&addr->type.in, &v4, NS_INADDRSZ); addr->zone = 0; } else if (inet_pton(AF_INET6, buffer, &v6) == 1) { addr->family = AF_INET6; memmove(&addr->type.in6, &v6, NS_IN6ADDRSZ); addr->zone = 0; } else { return (ISC_R_BADADDRESSFORM); /* Unrecognised format. */ } return (ISC_R_SUCCESS); } static isc_result_t resconf_parsenameserver(irs_resconf_t *conf, FILE *fp) { char word[RESCONFMAXLINELEN]; int cp; isc_result_t result; cp = getword(fp, word, sizeof(word)); if (strlen(word) == 0U) { return (ISC_R_UNEXPECTEDEND); /* Nothing on line. */ } else if (cp == ' ' || cp == '\t') { cp = eatwhite(fp); } if (cp != EOF && cp != '\n') { return (ISC_R_UNEXPECTEDTOKEN); /* Extra junk on line. */ } if (conf->numns == RESCONFMAXNAMESERVERS) { return (ISC_R_SUCCESS); } result = add_server(conf->mctx, word, &conf->nameservers); if (result != ISC_R_SUCCESS) { return (result); } conf->numns++; return (ISC_R_SUCCESS); } static isc_result_t resconf_parsedomain(irs_resconf_t *conf, FILE *fp) { char word[RESCONFMAXLINELEN]; int res; unsigned int i; res = getword(fp, word, sizeof(word)); if (strlen(word) == 0U) { return (ISC_R_UNEXPECTEDEND); /* Nothing else on line. */ } else if (res == ' ' || res == '\t') { res = eatwhite(fp); } if (res != EOF && res != '\n') { return (ISC_R_UNEXPECTEDTOKEN); /* Extra junk on line. */ } if (conf->domainname != NULL) { isc_mem_free(conf->mctx, conf->domainname); } /* * Search and domain are mutually exclusive. */ for (i = 0; i < RESCONFMAXSEARCH; i++) { if (conf->search[i] != NULL) { isc_mem_free(conf->mctx, conf->search[i]); conf->search[i] = NULL; } } conf->searchnxt = 0; conf->domainname = isc_mem_strdup(conf->mctx, word); return (ISC_R_SUCCESS); } static isc_result_t resconf_parsesearch(irs_resconf_t *conf, FILE *fp) { int delim; unsigned int idx; char word[RESCONFMAXLINELEN]; if (conf->domainname != NULL) { /* * Search and domain are mutually exclusive. */ isc_mem_free(conf->mctx, conf->domainname); conf->domainname = NULL; } /* * Remove any previous search definitions. */ for (idx = 0; idx < RESCONFMAXSEARCH; idx++) { if (conf->search[idx] != NULL) { isc_mem_free(conf->mctx, conf->search[idx]); conf->search[idx] = NULL; } } conf->searchnxt = 0; delim = getword(fp, word, sizeof(word)); if (strlen(word) == 0U) { return (ISC_R_UNEXPECTEDEND); /* Nothing else on line. */ } idx = 0; while (strlen(word) > 0U) { if (conf->searchnxt == RESCONFMAXSEARCH) { goto ignore; /* Too many domains. */ } INSIST(idx < sizeof(conf->search) / sizeof(conf->search[0])); conf->search[idx] = isc_mem_strdup(conf->mctx, word); idx++; conf->searchnxt++; ignore: if (delim == EOF || delim == '\n') { break; } else { delim = getword(fp, word, sizeof(word)); } } return (ISC_R_SUCCESS); } static isc_result_t resconf_parsesortlist(irs_resconf_t *conf, FILE *fp) { int delim, res; unsigned int idx; char word[RESCONFMAXLINELEN]; char *p; delim = getword(fp, word, sizeof(word)); if (strlen(word) == 0U) { return (ISC_R_UNEXPECTEDEND); /* Empty line after keyword. */ } while (strlen(word) > 0U) { if (conf->sortlistnxt == RESCONFMAXSORTLIST) { return (ISC_R_QUOTA); /* Too many values. */ } p = strchr(word, '/'); if (p != NULL) { *p++ = '\0'; } idx = conf->sortlistnxt; INSIST(idx < sizeof(conf->sortlist) / sizeof(conf->sortlist[0])); res = create_addr(word, &conf->sortlist[idx].addr, 1); if (res != ISC_R_SUCCESS) { return (res); } if (p != NULL) { res = create_addr(p, &conf->sortlist[idx].mask, 0); if (res != ISC_R_SUCCESS) { return (res); } } else { /* * Make up a mask. (XXX: is this correct?) */ conf->sortlist[idx].mask = conf->sortlist[idx].addr; memset(&conf->sortlist[idx].mask.type, 0xff, sizeof(conf->sortlist[idx].mask.type)); } conf->sortlistnxt++; if (delim == EOF || delim == '\n') { break; } else { delim = getword(fp, word, sizeof(word)); } } return (ISC_R_SUCCESS); } static isc_result_t resconf_optionnumber(const char *word, uint8_t *number) { char *p; long n; n = strtol(word, &p, 10); if (*p != '\0') { /* Bad string. */ return (ISC_R_UNEXPECTEDTOKEN); } if (n < 0 || n > 0xff) { /* Out of range. */ return (ISC_R_RANGE); } *number = n; return (ISC_R_SUCCESS); } static isc_result_t resconf_parseoption(irs_resconf_t *conf, FILE *fp) { int delim; isc_result_t result = ISC_R_SUCCESS; char word[RESCONFMAXLINELEN]; delim = getword(fp, word, sizeof(word)); if (strlen(word) == 0U) { return (ISC_R_UNEXPECTEDEND); /* Empty line after keyword. */ } while (strlen(word) > 0U) { if (strcmp("debug", word) == 0) { conf->resdebug = 1; } else if (strncmp("ndots:", word, 6) == 0) { CHECK(resconf_optionnumber(word + 6, &conf->ndots)); } else if (strncmp("attempts:", word, 9) == 0) { CHECK(resconf_optionnumber(word + 9, &conf->attempts)); } else if (strncmp("timeout:", word, 8) == 0) { CHECK(resconf_optionnumber(word + 8, &conf->timeout)); } if (delim == EOF || delim == '\n') { break; } else { delim = getword(fp, word, sizeof(word)); } } cleanup: return (result); } static isc_result_t add_search(irs_resconf_t *conf, char *domain) { irs_resconf_search_t *entry; entry = isc_mem_get(conf->mctx, sizeof(*entry)); entry->domain = domain; ISC_LINK_INIT(entry, link); ISC_LIST_APPEND(conf->searchlist, entry, link); return (ISC_R_SUCCESS); } /*% parses a file and fills in the data structure. */ isc_result_t irs_resconf_load(isc_mem_t *mctx, const char *filename, irs_resconf_t **confp) { FILE *fp = NULL; char word[256]; isc_result_t rval, ret = ISC_R_SUCCESS; irs_resconf_t *conf; unsigned int i; int stopchar; REQUIRE(mctx != NULL); REQUIRE(filename != NULL); REQUIRE(strlen(filename) > 0U); REQUIRE(confp != NULL && *confp == NULL); conf = isc_mem_get(mctx, sizeof(*conf)); conf->mctx = mctx; ISC_LIST_INIT(conf->nameservers); ISC_LIST_INIT(conf->searchlist); conf->numns = 0; conf->domainname = NULL; conf->searchnxt = 0; conf->sortlistnxt = 0; conf->resdebug = 0; conf->ndots = 1; conf->attempts = 3; conf->timeout = 0; for (i = 0; i < RESCONFMAXSEARCH; i++) { conf->search[i] = NULL; } errno = 0; if ((fp = fopen(filename, "r")) != NULL) { do { stopchar = getword(fp, word, sizeof(word)); if (stopchar == EOF) { rval = ISC_R_SUCCESS; POST(rval); break; } if (strlen(word) == 0U) { rval = ISC_R_SUCCESS; } else if (strcmp(word, "nameserver") == 0) { rval = resconf_parsenameserver(conf, fp); } else if (strcmp(word, "domain") == 0) { rval = resconf_parsedomain(conf, fp); } else if (strcmp(word, "search") == 0) { rval = resconf_parsesearch(conf, fp); } else if (strcmp(word, "sortlist") == 0) { rval = resconf_parsesortlist(conf, fp); } else if (strcmp(word, "options") == 0) { rval = resconf_parseoption(conf, fp); } else { /* unrecognised word. Ignore entire line */ rval = ISC_R_SUCCESS; stopchar = eatline(fp); if (stopchar == EOF) { break; } } if (ret == ISC_R_SUCCESS && rval != ISC_R_SUCCESS) { ret = rval; } } while (1); fclose(fp); } else { switch (errno) { case ENOENT: break; default: isc_mem_put(mctx, conf, sizeof(*conf)); return (ISC_R_INVALIDFILE); } } if (ret != ISC_R_SUCCESS) { goto error; } /* * Construct unified search list from domain or configured * search list */ if (conf->domainname != NULL) { ret = add_search(conf, conf->domainname); } else if (conf->searchnxt > 0) { for (i = 0; i < conf->searchnxt; i++) { ret = add_search(conf, conf->search[i]); if (ret != ISC_R_SUCCESS) { break; } } } /* If we don't find a nameserver fall back to localhost */ if (conf->numns == 0U) { INSIST(ISC_LIST_EMPTY(conf->nameservers)); /* XXX: should we catch errors? */ (void)add_server(conf->mctx, "::1", &conf->nameservers); (void)add_server(conf->mctx, "127.0.0.1", &conf->nameservers); } error: conf->magic = IRS_RESCONF_MAGIC; if (ret != ISC_R_SUCCESS) { irs_resconf_destroy(&conf); } else { if (fp == NULL) { ret = ISC_R_FILENOTFOUND; } *confp = conf; } return (ret); } void irs_resconf_destroy(irs_resconf_t **confp) { irs_resconf_t *conf; isc_sockaddr_t *address; irs_resconf_search_t *searchentry; unsigned int i; REQUIRE(confp != NULL); conf = *confp; *confp = NULL; REQUIRE(IRS_RESCONF_VALID(conf)); while ((searchentry = ISC_LIST_HEAD(conf->searchlist)) != NULL) { ISC_LIST_UNLINK(conf->searchlist, searchentry, link); isc_mem_put(conf->mctx, searchentry, sizeof(*searchentry)); } while ((address = ISC_LIST_HEAD(conf->nameservers)) != NULL) { ISC_LIST_UNLINK(conf->nameservers, address, link); isc_mem_put(conf->mctx, address, sizeof(*address)); } if (conf->domainname != NULL) { isc_mem_free(conf->mctx, conf->domainname); } for (i = 0; i < RESCONFMAXSEARCH; i++) { if (conf->search[i] != NULL) { isc_mem_free(conf->mctx, conf->search[i]); } } isc_mem_put(conf->mctx, conf, sizeof(*conf)); } isc_sockaddrlist_t * irs_resconf_getnameservers(irs_resconf_t *conf) { REQUIRE(IRS_RESCONF_VALID(conf)); return (&conf->nameservers); } irs_resconf_searchlist_t * irs_resconf_getsearchlist(irs_resconf_t *conf) { REQUIRE(IRS_RESCONF_VALID(conf)); return (&conf->searchlist); } unsigned int irs_resconf_getndots(irs_resconf_t *conf) { REQUIRE(IRS_RESCONF_VALID(conf)); return ((unsigned int)conf->ndots); } unsigned int irs_resconf_getattempts(irs_resconf_t *conf) { REQUIRE(IRS_RESCONF_VALID(conf)); return ((unsigned int)conf->attempts); } unsigned int irs_resconf_gettimeout(irs_resconf_t *conf) { REQUIRE(IRS_RESCONF_VALID(conf)); return ((unsigned int)conf->timeout); }