1 /* $NetBSD: resconf.c,v 1.4 2026/01/29 18:37:49 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 /*! 82 * configuration data structure 83 */ 84 85 struct irs_resconf { 86 /* 87 * The configuration data is a thread-specific object, and does not 88 * need to be locked. 89 */ 90 unsigned int magic; 91 isc_mem_t *mctx; 92 93 isc_sockaddrlist_t nameservers; 94 unsigned int numns; /*%< number of configured servers */ 95 96 char *domainname; 97 char *search[RESCONFMAXSEARCH]; 98 uint8_t searchnxt; /*%< index for next free slot */ 99 100 irs_resconf_searchlist_t searchlist; 101 102 struct { 103 isc_netaddr_t addr; 104 /*% mask has a non-zero 'family' if set */ 105 isc_netaddr_t mask; 106 } sortlist[RESCONFMAXSORTLIST]; 107 uint8_t sortlistnxt; 108 109 /*%< non-zero if 'options debug' set */ 110 uint8_t resdebug; 111 /*%< set to n in 'options ndots:n' */ 112 uint8_t ndots; 113 /*%< set to n in 'options attempts:n' */ 114 uint8_t attempts; 115 /*%< set to n in 'options timeout:n' */ 116 uint8_t timeout; 117 }; 118 119 static isc_result_t 120 resconf_parsenameserver(irs_resconf_t *conf, FILE *fp); 121 static isc_result_t 122 resconf_parsedomain(irs_resconf_t *conf, FILE *fp); 123 static isc_result_t 124 resconf_parsesearch(irs_resconf_t *conf, FILE *fp); 125 static isc_result_t 126 resconf_parsesortlist(irs_resconf_t *conf, FILE *fp); 127 static isc_result_t 128 resconf_parseoption(irs_resconf_t *ctx, FILE *fp); 129 130 /*! 131 * Eat characters from FP until EOL or EOF. Returns EOF or '\n' 132 */ 133 static int 134 eatline(FILE *fp) { 135 int ch; 136 137 ch = fgetc(fp); 138 while (ch != '\n' && ch != EOF) { 139 ch = fgetc(fp); 140 } 141 142 return ch; 143 } 144 145 /*! 146 * Eats white space up to next newline or non-whitespace character (of 147 * EOF). Returns the last character read. Comments are considered white 148 * space. 149 */ 150 static int 151 eatwhite(FILE *fp) { 152 int ch; 153 154 ch = fgetc(fp); 155 while (ch != '\n' && ch != EOF && isspace((unsigned char)ch)) { 156 ch = fgetc(fp); 157 } 158 159 if (ch == ';' || ch == '#') { 160 ch = eatline(fp); 161 } 162 163 return ch; 164 } 165 166 /*! 167 * Skip over any leading whitespace and then read in the next sequence of 168 * non-whitespace characters. In this context newline is not considered 169 * whitespace. Returns EOF on end-of-file, or the character 170 * that caused the reading to stop. 171 */ 172 static int 173 getword(FILE *fp, char *buffer, size_t size) { 174 char *p = NULL; 175 int ch; 176 177 REQUIRE(buffer != NULL); 178 REQUIRE(size > 0U); 179 180 p = buffer; 181 *p = '\0'; 182 183 ch = eatwhite(fp); 184 185 if (ch == EOF) { 186 return EOF; 187 } 188 189 do { 190 *p = '\0'; 191 192 if (ch == EOF || isspace((unsigned char)ch)) { 193 break; 194 } else if ((size_t)(p - buffer) == size - 1) { 195 return EOF; /* Not enough space. */ 196 } 197 198 *p++ = (char)ch; 199 ch = fgetc(fp); 200 } while (1); 201 202 return ch; 203 } 204 205 static isc_result_t 206 add_server(isc_mem_t *mctx, const char *address_str, 207 isc_sockaddrlist_t *nameservers) { 208 int error; 209 isc_sockaddr_t *address = NULL; 210 struct addrinfo hints, *res; 211 isc_result_t result = ISC_R_SUCCESS; 212 213 res = NULL; 214 memset(&hints, 0, sizeof(hints)); 215 hints.ai_family = AF_UNSPEC; 216 hints.ai_socktype = SOCK_DGRAM; 217 hints.ai_protocol = IPPROTO_UDP; 218 hints.ai_flags = AI_NUMERICHOST; 219 error = getaddrinfo(address_str, "53", &hints, &res); 220 if (error != 0) { 221 return ISC_R_BADADDRESSFORM; 222 } 223 224 address = isc_mem_get(mctx, sizeof(*address)); 225 if (res->ai_addrlen > sizeof(address->type)) { 226 isc_mem_put(mctx, address, sizeof(*address)); 227 result = ISC_R_RANGE; 228 goto cleanup; 229 } 230 231 if (res->ai_family == AF_INET) { 232 struct in_addr *v4; 233 unsigned char zeroaddress[] = { 0, 0, 0, 0 }; 234 unsigned char loopaddress[] = { 127, 0, 0, 1 }; 235 236 /* XXX: special case: treat all-0 IPv4 address as loopback */ 237 v4 = &((struct sockaddr_in *)res->ai_addr)->sin_addr; 238 if (memcmp(v4, zeroaddress, 4) == 0) { 239 memmove(v4, loopaddress, 4); 240 } 241 memmove(&address->type.sin, res->ai_addr, res->ai_addrlen); 242 } else if (res->ai_family == AF_INET6) { 243 memmove(&address->type.sin6, res->ai_addr, res->ai_addrlen); 244 } else { 245 isc_mem_put(mctx, address, sizeof(*address)); 246 UNEXPECTED_ERROR("ai_family (%d) not INET nor INET6", 247 res->ai_family); 248 result = ISC_R_UNEXPECTED; 249 goto cleanup; 250 } 251 address->length = (unsigned int)res->ai_addrlen; 252 253 ISC_LINK_INIT(address, link); 254 ISC_LIST_APPEND(*nameservers, address, link); 255 256 cleanup: 257 freeaddrinfo(res); 258 259 return result; 260 } 261 262 static isc_result_t 263 create_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 289 static isc_result_t 290 resconf_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 319 static isc_result_t 320 resconf_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 356 static isc_result_t 357 resconf_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 408 static isc_result_t 409 resconf_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 464 static isc_result_t 465 resconf_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 480 static isc_result_t 481 resconf_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 509 cleanup: 510 return result; 511 } 512 513 static isc_result_t 514 add_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. */ 527 isc_result_t 528 irs_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 if (stopchar != '\n') { 584 stopchar = eatline(fp); 585 if (stopchar == EOF) { 586 break; 587 } 588 } 589 } 590 if (ret == ISC_R_SUCCESS && rval != ISC_R_SUCCESS) { 591 ret = rval; 592 } 593 } while (1); 594 595 fclose(fp); 596 } else { 597 switch (errno) { 598 case ENOENT: 599 break; 600 default: 601 isc_mem_put(mctx, conf, sizeof(*conf)); 602 return ISC_R_INVALIDFILE; 603 } 604 } 605 606 if (ret != ISC_R_SUCCESS) { 607 goto error; 608 } 609 610 /* 611 * Construct unified search list from domain or configured 612 * search list 613 */ 614 if (conf->domainname != NULL) { 615 ret = add_search(conf, conf->domainname); 616 } else if (conf->searchnxt > 0) { 617 for (i = 0; i < conf->searchnxt; i++) { 618 ret = add_search(conf, conf->search[i]); 619 if (ret != ISC_R_SUCCESS) { 620 break; 621 } 622 } 623 } 624 625 /* If we don't find a nameserver fall back to localhost */ 626 if (conf->numns == 0U) { 627 INSIST(ISC_LIST_EMPTY(conf->nameservers)); 628 629 /* XXX: should we catch errors? */ 630 (void)add_server(conf->mctx, "::1", &conf->nameservers); 631 (void)add_server(conf->mctx, "127.0.0.1", &conf->nameservers); 632 } 633 634 error: 635 conf->magic = IRS_RESCONF_MAGIC; 636 637 if (ret != ISC_R_SUCCESS) { 638 irs_resconf_destroy(&conf); 639 } else { 640 if (fp == NULL) { 641 ret = ISC_R_FILENOTFOUND; 642 } 643 *confp = conf; 644 } 645 646 return ret; 647 } 648 649 void 650 irs_resconf_destroy(irs_resconf_t **confp) { 651 irs_resconf_t *conf; 652 isc_sockaddr_t *address; 653 irs_resconf_search_t *searchentry; 654 unsigned int i; 655 656 REQUIRE(confp != NULL); 657 conf = *confp; 658 *confp = NULL; 659 REQUIRE(IRS_RESCONF_VALID(conf)); 660 661 while ((searchentry = ISC_LIST_HEAD(conf->searchlist)) != NULL) { 662 ISC_LIST_UNLINK(conf->searchlist, searchentry, link); 663 isc_mem_put(conf->mctx, searchentry, sizeof(*searchentry)); 664 } 665 666 while ((address = ISC_LIST_HEAD(conf->nameservers)) != NULL) { 667 ISC_LIST_UNLINK(conf->nameservers, address, link); 668 isc_mem_put(conf->mctx, address, sizeof(*address)); 669 } 670 671 if (conf->domainname != NULL) { 672 isc_mem_free(conf->mctx, conf->domainname); 673 } 674 675 for (i = 0; i < RESCONFMAXSEARCH; i++) { 676 if (conf->search[i] != NULL) { 677 isc_mem_free(conf->mctx, conf->search[i]); 678 } 679 } 680 681 isc_mem_put(conf->mctx, conf, sizeof(*conf)); 682 } 683 684 isc_sockaddrlist_t * 685 irs_resconf_getnameservers(irs_resconf_t *conf) { 686 REQUIRE(IRS_RESCONF_VALID(conf)); 687 688 return &conf->nameservers; 689 } 690 691 irs_resconf_searchlist_t * 692 irs_resconf_getsearchlist(irs_resconf_t *conf) { 693 REQUIRE(IRS_RESCONF_VALID(conf)); 694 695 return &conf->searchlist; 696 } 697 698 unsigned int 699 irs_resconf_getndots(irs_resconf_t *conf) { 700 REQUIRE(IRS_RESCONF_VALID(conf)); 701 702 return (unsigned int)conf->ndots; 703 } 704 705 unsigned int 706 irs_resconf_getattempts(irs_resconf_t *conf) { 707 REQUIRE(IRS_RESCONF_VALID(conf)); 708 709 return (unsigned int)conf->attempts; 710 } 711 712 unsigned int 713 irs_resconf_gettimeout(irs_resconf_t *conf) { 714 REQUIRE(IRS_RESCONF_VALID(conf)); 715 716 return (unsigned int)conf->timeout; 717 } 718