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