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