/* MIT License * * Copyright (c) 1998 Massachusetts Institute of Technology * Copyright (c) 2007 Daniel Stenberg * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * SPDX-License-Identifier: MIT */ #include "ares_private.h" #ifdef HAVE_SYS_PARAM_H # include <sys/param.h> #endif #ifdef HAVE_NETINET_IN_H # include <netinet/in.h> #endif #ifdef HAVE_NETDB_H # include <netdb.h> #endif #ifdef HAVE_ARPA_INET_H # include <arpa/inet.h> #endif #if defined(ANDROID) || defined(__ANDROID__) # include <sys/system_properties.h> # include "ares_android.h" /* From the Bionic sources */ # define DNS_PROP_NAME_PREFIX "net.dns" # define MAX_DNS_PROPERTIES 8 #endif #if defined(CARES_USE_LIBRESOLV) # include <resolv.h> #endif #if defined(USE_WINSOCK) && defined(HAVE_IPHLPAPI_H) # include <iphlpapi.h> #endif #include "ares_inet_net_pton.h" static unsigned char ip_natural_mask(const struct ares_addr *addr) { const unsigned char *ptr = NULL; /* This is an odd one. If a raw ipv4 address is specified, then we take * what is called a natural mask, which means we look at the first octet * of the ip address and for values 0-127 we assume it is a class A (/8), * for values 128-191 we assume it is a class B (/16), and for 192-223 * we assume it is a class C (/24). 223-239 is Class D which and 240-255 is * Class E, however, there is no pre-defined mask for this, so we'll use * /24 as well as that's what the old code did. * * For IPv6, we'll use /64. */ if (addr->family == AF_INET6) { return 64; } ptr = (const unsigned char *)&addr->addr.addr4; if (*ptr < 128) { return 8; } if (*ptr < 192) { return 16; } return 24; } static ares_bool_t sortlist_append(struct apattern **sortlist, size_t *nsort, const struct apattern *pat) { struct apattern *newsort; newsort = ares_realloc(*sortlist, (*nsort + 1) * sizeof(*newsort)); if (newsort == NULL) { return ARES_FALSE; /* LCOV_EXCL_LINE: OutOfMemory */ } *sortlist = newsort; memcpy(&(*sortlist)[*nsort], pat, sizeof(**sortlist)); (*nsort)++; return ARES_TRUE; } static ares_status_t parse_sort(ares_buf_t *buf, struct apattern *pat) { ares_status_t status; const unsigned char ip_charset[] = "ABCDEFabcdef0123456789.:"; char ipaddr[INET6_ADDRSTRLEN] = ""; size_t addrlen; memset(pat, 0, sizeof(*pat)); /* Consume any leading whitespace */ ares_buf_consume_whitespace(buf, ARES_TRUE); /* If no length, just ignore, return ENOTFOUND as an indicator */ if (ares_buf_len(buf) == 0) { return ARES_ENOTFOUND; } ares_buf_tag(buf); /* Consume ip address */ if (ares_buf_consume_charset(buf, ip_charset, sizeof(ip_charset) - 1) == 0) { return ARES_EBADSTR; } /* Fetch ip address */ status = ares_buf_tag_fetch_string(buf, ipaddr, sizeof(ipaddr)); if (status != ARES_SUCCESS) { return status; } /* Parse it to make sure its valid */ pat->addr.family = AF_UNSPEC; if (ares_dns_pton(ipaddr, &pat->addr, &addrlen) == NULL) { return ARES_EBADSTR; } /* See if there is a subnet mask */ if (ares_buf_begins_with(buf, (const unsigned char *)"/", 1)) { char maskstr[16]; const unsigned char ipv4_charset[] = "0123456789."; /* Consume / */ ares_buf_consume(buf, 1); ares_buf_tag(buf); /* Consume mask */ if (ares_buf_consume_charset(buf, ipv4_charset, sizeof(ipv4_charset) - 1) == 0) { return ARES_EBADSTR; } /* Fetch mask */ status = ares_buf_tag_fetch_string(buf, maskstr, sizeof(maskstr)); if (status != ARES_SUCCESS) { return status; } if (ares_str_isnum(maskstr)) { /* Numeric mask */ int mask = atoi(maskstr); if (mask < 0 || mask > 128) { return ARES_EBADSTR; } if (pat->addr.family == AF_INET && mask > 32) { return ARES_EBADSTR; } pat->mask = (unsigned char)mask; } else { /* Ipv4 subnet style mask */ struct ares_addr maskaddr; const unsigned char *ptr; memset(&maskaddr, 0, sizeof(maskaddr)); maskaddr.family = AF_INET; if (ares_dns_pton(maskstr, &maskaddr, &addrlen) == NULL) { return ARES_EBADSTR; } ptr = (const unsigned char *)&maskaddr.addr.addr4; pat->mask = (unsigned char)(ares_count_bits_u8(ptr[0]) + ares_count_bits_u8(ptr[1]) + ares_count_bits_u8(ptr[2]) + ares_count_bits_u8(ptr[3])); } } else { pat->mask = ip_natural_mask(&pat->addr); } /* Consume any trailing whitespace */ ares_buf_consume_whitespace(buf, ARES_TRUE); /* If we have any trailing bytes other than whitespace, its a parse failure */ if (ares_buf_len(buf) != 0) { return ARES_EBADSTR; } return ARES_SUCCESS; } ares_status_t ares_parse_sortlist(struct apattern **sortlist, size_t *nsort, const char *str) { ares_buf_t *buf = NULL; ares_status_t status = ARES_SUCCESS; ares_array_t *arr = NULL; size_t num = 0; size_t i; if (sortlist == NULL || nsort == NULL || str == NULL) { return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */ } if (*sortlist != NULL) { ares_free(*sortlist); } *sortlist = NULL; *nsort = 0; buf = ares_buf_create_const((const unsigned char *)str, ares_strlen(str)); if (buf == NULL) { status = ARES_ENOMEM; goto done; } /* Split on space or semicolon */ status = ares_buf_split(buf, (const unsigned char *)" ;", 2, ARES_BUF_SPLIT_NONE, 0, &arr); if (status != ARES_SUCCESS) { goto done; } num = ares_array_len(arr); for (i = 0; i < num; i++) { ares_buf_t **bufptr = ares_array_at(arr, i); ares_buf_t *entry = *bufptr; struct apattern pat; status = parse_sort(entry, &pat); if (status != ARES_SUCCESS && status != ARES_ENOTFOUND) { goto done; } if (status != ARES_SUCCESS) { continue; } if (!sortlist_append(sortlist, nsort, &pat)) { status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ goto done; /* LCOV_EXCL_LINE: OutOfMemory */ } } status = ARES_SUCCESS; done: ares_buf_destroy(buf); ares_array_destroy(arr); if (status != ARES_SUCCESS) { ares_free(*sortlist); *sortlist = NULL; *nsort = 0; } return status; } static ares_status_t config_search(ares_sysconfig_t *sysconfig, const char *str, size_t max_domains) { if (sysconfig->domains && sysconfig->ndomains > 0) { /* if we already have some domains present, free them first */ ares_strsplit_free(sysconfig->domains, sysconfig->ndomains); sysconfig->domains = NULL; sysconfig->ndomains = 0; } sysconfig->domains = ares_strsplit(str, ", ", &sysconfig->ndomains); if (sysconfig->domains == NULL) { return ARES_ENOMEM; } /* Truncate if necessary */ if (max_domains && sysconfig->ndomains > max_domains) { size_t i; for (i = max_domains; i < sysconfig->ndomains; i++) { ares_free(sysconfig->domains[i]); sysconfig->domains[i] = NULL; } sysconfig->ndomains = max_domains; } return ARES_SUCCESS; } static ares_status_t buf_fetch_string(ares_buf_t *buf, char *str, size_t str_len) { ares_status_t status; ares_buf_tag(buf); ares_buf_consume(buf, ares_buf_len(buf)); status = ares_buf_tag_fetch_string(buf, str, str_len); return status; } static ares_status_t config_lookup(ares_sysconfig_t *sysconfig, ares_buf_t *buf, const char *separators) { ares_status_t status; char lookupstr[32]; size_t lookupstr_cnt = 0; char **lookups = NULL; size_t num = 0; size_t i; size_t separators_len = ares_strlen(separators); status = ares_buf_split_str(buf, (const unsigned char *)separators, separators_len, ARES_BUF_SPLIT_TRIM, 0, &lookups, &num); if (status != ARES_SUCCESS) { goto done; } for (i = 0; i < num; i++) { const char *value = lookups[i]; char ch; if (ares_strcaseeq(value, "dns") || ares_strcaseeq(value, "bind") || ares_strcaseeq(value, "resolv") || ares_strcaseeq(value, "resolve")) { ch = 'b'; } else if (ares_strcaseeq(value, "files") || ares_strcaseeq(value, "file") || ares_strcaseeq(value, "local")) { ch = 'f'; } else { continue; } /* Look for a duplicate and ignore */ if (memchr(lookupstr, ch, lookupstr_cnt) == NULL) { lookupstr[lookupstr_cnt++] = ch; } } if (lookupstr_cnt) { lookupstr[lookupstr_cnt] = 0; ares_free(sysconfig->lookups); sysconfig->lookups = ares_strdup(lookupstr); if (sysconfig->lookups == NULL) { status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ goto done; /* LCOV_EXCL_LINE: OutOfMemory */ } } status = ARES_SUCCESS; done: if (status != ARES_ENOMEM) { status = ARES_SUCCESS; } ares_free_array(lookups, num, ares_free); return status; } static ares_status_t process_option(ares_sysconfig_t *sysconfig, ares_buf_t *option) { char **kv = NULL; size_t num = 0; const char *key; const char *val; unsigned int valint = 0; ares_status_t status; /* Split on : */ status = ares_buf_split_str(option, (const unsigned char *)":", 1, ARES_BUF_SPLIT_TRIM, 2, &kv, &num); if (status != ARES_SUCCESS) { goto done; } if (num < 1) { status = ARES_EBADSTR; goto done; } key = kv[0]; if (num == 2) { val = kv[1]; valint = (unsigned int)strtoul(val, NULL, 10); } if (ares_streq(key, "ndots")) { sysconfig->ndots = valint; } else if (ares_streq(key, "retrans") || ares_streq(key, "timeout")) { if (valint == 0) { return ARES_EFORMERR; } sysconfig->timeout_ms = valint * 1000; } else if (ares_streq(key, "retry") || ares_streq(key, "attempts")) { if (valint == 0) { return ARES_EFORMERR; } sysconfig->tries = valint; } else if (ares_streq(key, "rotate")) { sysconfig->rotate = ARES_TRUE; } else if (ares_streq(key, "use-vc") || ares_streq(key, "usevc")) { sysconfig->usevc = ARES_TRUE; } done: ares_free_array(kv, num, ares_free); return status; } ares_status_t ares_sysconfig_set_options(ares_sysconfig_t *sysconfig, const char *str) { ares_buf_t *buf = NULL; ares_array_t *options = NULL; size_t num; size_t i; ares_status_t status; buf = ares_buf_create_const((const unsigned char *)str, ares_strlen(str)); if (buf == NULL) { return ARES_ENOMEM; } status = ares_buf_split(buf, (const unsigned char *)" \t", 2, ARES_BUF_SPLIT_TRIM, 0, &options); if (status != ARES_SUCCESS) { goto done; } num = ares_array_len(options); for (i = 0; i < num; i++) { ares_buf_t **bufptr = ares_array_at(options, i); ares_buf_t *valbuf = *bufptr; status = process_option(sysconfig, valbuf); /* Out of memory is the only fatal condition */ if (status == ARES_ENOMEM) { goto done; /* LCOV_EXCL_LINE: OutOfMemory */ } } status = ARES_SUCCESS; done: ares_array_destroy(options); ares_buf_destroy(buf); return status; } ares_status_t ares_init_by_environment(ares_sysconfig_t *sysconfig) { const char *localdomain; const char *res_options; ares_status_t status; localdomain = getenv("LOCALDOMAIN"); if (localdomain) { char *temp = ares_strdup(localdomain); if (temp == NULL) { return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ } status = config_search(sysconfig, temp, 1); ares_free(temp); if (status != ARES_SUCCESS) { return status; } } res_options = getenv("RES_OPTIONS"); if (res_options) { status = ares_sysconfig_set_options(sysconfig, res_options); if (status != ARES_SUCCESS) { return status; } } return ARES_SUCCESS; } /* Configuration Files: * /etc/resolv.conf * - All Unix-like systems * - Comments start with ; or # * - Lines have a keyword followed by a value that is interpreted specific * to the keyword: * - Keywords: * - nameserver - IP address of nameserver with optional port (using a : * prefix). If using an ipv6 address and specifying a port, the ipv6 * address must be encapsulated in brackets. For link-local ipv6 * addresses, the interface can also be specified with a % prefix. e.g.: * "nameserver [fe80::1]:1234%iface" * This keyword may be specified multiple times. * - search - whitespace separated list of domains * - domain - obsolete, same as search except only a single domain * - lookup / hostresorder - local, bind, file, files * - sortlist - whitespace separated ip-address/netmask pairs * - options - options controlling resolver variables * - ndots:n - set ndots option * - timeout:n (retrans:n) - timeout per query attempt in seconds * - attempts:n (retry:n) - number of times resolver will send query * - rotate - round-robin selection of name servers * - use-vc / usevc - force tcp * /etc/nsswitch.conf * - Modern Linux, FreeBSD, HP-UX, Solaris * - Search order set via: * "hosts: files dns mdns4_minimal mdns4" * - files is /etc/hosts * - dns is dns * - mdns4_minimal does mdns only if ending in .local * - mdns4 does not limit to domains ending in .local * /etc/netsvc.conf * - AIX * - Search order set via: * "hosts = local , bind" * - bind is dns * - local is /etc/hosts * /etc/svc.conf * - Tru64 * - Same format as /etc/netsvc.conf * /etc/host.conf * - Early FreeBSD, Early Linux * - Not worth supporting, format varied based on system, FreeBSD used * just a line per search order, Linux used "order " and a comma * delimited list of "bind" and "hosts" */ /* This function will only return ARES_SUCCESS or ARES_ENOMEM. Any other * conditions are ignored. Users may mess up config files, but we want to * process anything we can. */ ares_status_t ares_sysconfig_parse_resolv_line(const ares_channel_t *channel, ares_sysconfig_t *sysconfig, ares_buf_t *line) { char option[32]; char value[512]; ares_status_t status = ARES_SUCCESS; /* Ignore lines beginning with a comment */ if (ares_buf_begins_with(line, (const unsigned char *)"#", 1) || ares_buf_begins_with(line, (const unsigned char *)";", 1)) { return ARES_SUCCESS; } ares_buf_tag(line); /* Shouldn't be possible, but if it happens, ignore the line. */ if (ares_buf_consume_nonwhitespace(line) == 0) { return ARES_SUCCESS; } status = ares_buf_tag_fetch_string(line, option, sizeof(option)); if (status != ARES_SUCCESS) { return ARES_SUCCESS; } ares_buf_consume_whitespace(line, ARES_TRUE); status = buf_fetch_string(line, value, sizeof(value)); if (status != ARES_SUCCESS) { return ARES_SUCCESS; } ares_str_trim(value); if (*value == 0) { return ARES_SUCCESS; } /* At this point we have a string option and a string value, both trimmed * of leading and trailing whitespace. Lets try to evaluate them */ if (ares_streq(option, "domain")) { /* Domain is legacy, don't overwrite an existing config set by search */ if (sysconfig->domains == NULL) { status = config_search(sysconfig, value, 1); } } else if (ares_streq(option, "lookup") || ares_streq(option, "hostresorder")) { ares_buf_tag_rollback(line); status = config_lookup(sysconfig, line, " \t"); } else if (ares_streq(option, "search")) { status = config_search(sysconfig, value, 0); } else if (ares_streq(option, "nameserver")) { status = ares_sconfig_append_fromstr(channel, &sysconfig->sconfig, value, ARES_TRUE); } else if (ares_streq(option, "sortlist")) { /* Ignore all failures except ENOMEM. If the sysadmin set a bad * sortlist, just ignore the sortlist, don't cause an inoperable * channel */ status = ares_parse_sortlist(&sysconfig->sortlist, &sysconfig->nsortlist, value); if (status != ARES_ENOMEM) { status = ARES_SUCCESS; } } else if (ares_streq(option, "options")) { status = ares_sysconfig_set_options(sysconfig, value); } return status; } /* This function will only return ARES_SUCCESS or ARES_ENOMEM. Any other * conditions are ignored. Users may mess up config files, but we want to * process anything we can. */ static ares_status_t parse_nsswitch_line(const ares_channel_t *channel, ares_sysconfig_t *sysconfig, ares_buf_t *line) { char option[32]; ares_status_t status = ARES_SUCCESS; ares_array_t *sects = NULL; ares_buf_t **bufptr; ares_buf_t *buf; (void)channel; /* Ignore lines beginning with a comment */ if (ares_buf_begins_with(line, (const unsigned char *)"#", 1)) { return ARES_SUCCESS; } /* database : values (space delimited) */ status = ares_buf_split(line, (const unsigned char *)":", 1, ARES_BUF_SPLIT_TRIM, 2, §s); if (status != ARES_SUCCESS || ares_array_len(sects) != 2) { goto done; } bufptr = ares_array_at(sects, 0); buf = *bufptr; status = buf_fetch_string(buf, option, sizeof(option)); if (status != ARES_SUCCESS) { goto done; } /* Only support "hosts:" */ if (!ares_streq(option, "hosts")) { goto done; } /* Values are space separated */ bufptr = ares_array_at(sects, 1); buf = *bufptr; status = config_lookup(sysconfig, buf, " \t"); done: ares_array_destroy(sects); if (status != ARES_ENOMEM) { status = ARES_SUCCESS; } return status; } /* This function will only return ARES_SUCCESS or ARES_ENOMEM. Any other * conditions are ignored. Users may mess up config files, but we want to * process anything we can. */ static ares_status_t parse_svcconf_line(const ares_channel_t *channel, ares_sysconfig_t *sysconfig, ares_buf_t *line) { char option[32]; ares_buf_t **bufptr; ares_buf_t *buf; ares_status_t status = ARES_SUCCESS; ares_array_t *sects = NULL; (void)channel; /* Ignore lines beginning with a comment */ if (ares_buf_begins_with(line, (const unsigned char *)"#", 1)) { return ARES_SUCCESS; } /* database = values (comma delimited)*/ status = ares_buf_split(line, (const unsigned char *)"=", 1, ARES_BUF_SPLIT_TRIM, 2, §s); if (status != ARES_SUCCESS || ares_array_len(sects) != 2) { goto done; } bufptr = ares_array_at(sects, 0); buf = *bufptr; status = buf_fetch_string(buf, option, sizeof(option)); if (status != ARES_SUCCESS) { goto done; } /* Only support "hosts=" */ if (!ares_streq(option, "hosts")) { goto done; } /* Values are comma separated */ bufptr = ares_array_at(sects, 1); buf = *bufptr; status = config_lookup(sysconfig, buf, ","); done: ares_array_destroy(sects); if (status != ARES_ENOMEM) { status = ARES_SUCCESS; } return status; } ares_status_t ares_sysconfig_process_buf(const ares_channel_t *channel, ares_sysconfig_t *sysconfig, ares_buf_t *buf, ares_sysconfig_line_cb_t cb) { ares_array_t *lines = NULL; size_t num; size_t i; ares_status_t status; status = ares_buf_split(buf, (const unsigned char *)"\n", 1, ARES_BUF_SPLIT_TRIM, 0, &lines); if (status != ARES_SUCCESS) { goto done; } num = ares_array_len(lines); for (i = 0; i < num; i++) { ares_buf_t **bufptr = ares_array_at(lines, i); ares_buf_t *line = *bufptr; status = cb(channel, sysconfig, line); if (status != ARES_SUCCESS) { goto done; } } done: ares_array_destroy(lines); return status; } /* Should only return: * ARES_ENOTFOUND - file not found * ARES_EFILE - error reading file (perms) * ARES_ENOMEM - out of memory * ARES_SUCCESS - file processed, doesn't necessarily mean it was a good * file, but we're not erroring out if we can't parse * something (or anything at all) */ static ares_status_t process_config_lines(const ares_channel_t *channel, const char *filename, ares_sysconfig_t *sysconfig, ares_sysconfig_line_cb_t cb) { ares_status_t status = ARES_SUCCESS; ares_buf_t *buf = NULL; buf = ares_buf_create(); if (buf == NULL) { status = ARES_ENOMEM; goto done; } status = ares_buf_load_file(filename, buf); if (status != ARES_SUCCESS) { goto done; } status = ares_sysconfig_process_buf(channel, sysconfig, buf, cb); done: ares_buf_destroy(buf); return status; } ares_status_t ares_init_sysconfig_files(const ares_channel_t *channel, ares_sysconfig_t *sysconfig, ares_bool_t process_resolvconf) { ares_status_t status = ARES_SUCCESS; /* Resolv.conf */ if (process_resolvconf) { status = process_config_lines(channel, (channel->resolvconf_path != NULL) ? channel->resolvconf_path : PATH_RESOLV_CONF, sysconfig, ares_sysconfig_parse_resolv_line); if (status != ARES_SUCCESS && status != ARES_ENOTFOUND) { goto done; } } /* Nsswitch.conf */ status = process_config_lines(channel, "/etc/nsswitch.conf", sysconfig, parse_nsswitch_line); if (status != ARES_SUCCESS && status != ARES_ENOTFOUND) { goto done; } /* netsvc.conf */ status = process_config_lines(channel, "/etc/netsvc.conf", sysconfig, parse_svcconf_line); if (status != ARES_SUCCESS && status != ARES_ENOTFOUND) { goto done; } /* svc.conf */ status = process_config_lines(channel, "/etc/svc.conf", sysconfig, parse_svcconf_line); if (status != ARES_SUCCESS && status != ARES_ENOTFOUND) { goto done; } status = ARES_SUCCESS; done: return status; }