From a21d67e679e25bc6cdb7513bab8204b470499981 Mon Sep 17 00:00:00 2001 From: Brad House Date: Tue, 11 Jul 2023 14:16:00 -0400 Subject: [PATCH] Support configuration of DNS server ports (#534) As per https://man.openbsd.org/OpenBSD-5.1/resolv.conf.5 we should support bracketed syntax for resolv.conf entries to contain an optional port number. We also need to utilize this format for configuration of MacOS DNS servers as seen when using the Viscosity OpenVPN client, where it starts a private DNS server listening on localhost on a non-standard port. Fix By: Brad House (@bradh352) --- src/lib/ares_init.c | 156 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 130 insertions(+), 26 deletions(-) diff --git a/src/lib/ares_init.c b/src/lib/ares_init.c index 3e2033cd..0bc71592 100644 --- a/src/lib/ares_init.c +++ b/src/lib/ares_init.c @@ -73,7 +73,7 @@ static int init_by_defaults(ares_channel channel); #ifndef WATT32 static int config_nameserver(struct server_state **servers, int *nservers, - char *str); + const char *str); #endif static int set_search(ares_channel channel, const char *str); static int set_options(ares_channel channel, const char *str); @@ -1313,18 +1313,28 @@ static int init_by_resolv_conf(ares_channel channel) int nscount = res_getservers(&res, addr, MAXNS); int i; for (i = 0; i < nscount; ++i) { - char str[INET6_ADDRSTRLEN]; + char ipaddr[INET6_ADDRSTRLEN] = ""; + char ipaddr_port[INET6_ADDRSTRLEN + 8]; /* [%s]:NNNNN */ + unsigned short port = 0; int config_status; sa_family_t family = addr[i].sin.sin_family; if (family == AF_INET) { - ares_inet_ntop(family, &addr[i].sin.sin_addr, str, sizeof(str)); + ares_inet_ntop(family, &addr[i].sin.sin_addr, ipaddr, sizeof(ipaddr)); + port = ntohs(addr[i].sin.sin_port); } else if (family == AF_INET6) { - ares_inet_ntop(family, &addr[i].sin6.sin6_addr, str, sizeof(str)); + ares_inet_ntop(family, &addr[i].sin6.sin6_addr, ipaddr, sizeof(ipaddr)); + port = ntohs(addr[i].sin6.sin6_port); } else { continue; } - config_status = config_nameserver(&servers, &nservers, str); + if (port) { + snprintf(ipaddr_port, sizeof(ipaddr_port), "[%s]:%u", ipaddr, port); + } else { + snprintf(ipaddr_port, sizeof(ipaddr_port), "%s", ipaddr); + } + + config_status = config_nameserver(&servers, &nservers, ipaddr_port); if (config_status != ARES_SUCCESS) { status = config_status; break; @@ -1831,8 +1841,112 @@ static int ares_ipv6_server_blacklisted(const unsigned char ipaddr[16]) return 0; } -/* Add the IPv4 or IPv6 nameservers in str (separated by commas) to the - * servers list, updating servers and nservers as required. +/* Parse address and port in these formats, either ipv4 or ipv6 addresses + * are allowed: + * ipaddr + * [ipaddr] + * [ipaddr]:port + * + * If a port is not specified, will set port to 0. + * + * Will fail if an IPv6 nameserver as detected by + * ares_ipv6_server_blacklisted() + * + * Returns an error code on failure, else ARES_SUCCESS + */ +static int parse_dnsaddrport(const char *str, size_t len, + struct ares_addr *host, unsigned short *port) +{ + char ipaddr[INET6_ADDRSTRLEN] = ""; + char ipport[6] = ""; + size_t mylen; + const char *addr_start = NULL; + const char *addr_end = NULL; + const char *port_start = NULL; + const char *port_end = NULL; + + /* Must start with [, hex digit or : */ + if (len == 0 || (*str != '[' && !isxdigit(*str) && *str != ':')) { + return ARES_EBADSTR; + } + + /* If it starts with a bracket, must end with a bracket */ + if (*str == '[') { + const char *ptr; + addr_start = str+1; + ptr = memchr(addr_start, ']', len-1); + if (ptr == NULL) { + return ARES_EBADSTR; + } + addr_end = ptr-1; + + /* Try to pull off port */ + if ((size_t)(ptr - str) < len) { + ptr++; + if (*ptr != ':') { + return ARES_EBADSTR; + } + + /* Missing port number */ + if ((size_t)(ptr - str) == len) { + return ARES_EBADSTR; + } + + port_start = ptr+1; + port_end = str+(len-1); + } + } else { + addr_start = str; + addr_end = str+(len-1); + } + + mylen = (addr_end-addr_start)+1; + /* Larger than buffer with null term */ + if (mylen+1 > sizeof(ipaddr)) { + return ARES_EBADSTR; + } + + memset(ipaddr, 0, sizeof(ipaddr)); + memcpy(ipaddr, addr_start, mylen); + + if (port_start) { + mylen = (port_end-port_start)+1; + /* Larger than buffer with null term */ + if (mylen+1 > sizeof(ipport)) { + return ARES_EBADSTR; + } + memset(ipport, 0, sizeof(ipport)); + memcpy(ipport, port_start, mylen); + } else { + snprintf(ipport, sizeof(ipport), "0"); + } + + /* Convert textual address to binary format. */ + if (ares_inet_pton(AF_INET, ipaddr, &host->addrV4) == 1) { + host->family = AF_INET; + } else if (ares_inet_pton(AF_INET6, ipaddr, &host->addrV6) == 1 + /* Silently skip blacklisted IPv6 servers. */ + && !ares_ipv6_server_blacklisted( + (const unsigned char *)&host->addrV6)) { + host->family = AF_INET6; + } else { + return ARES_EBADSTR; + } + + *port = (unsigned short)atoi(ipport); + return ARES_SUCCESS; +} + +/* Add the IPv4 or IPv6 nameservers in str (separated by commas or spaces) to + * the servers list, updating servers and nservers as required. + * + * If a nameserver is encapsulated in [ ] it may optionally include a port + * suffix, e.g.: + * [127.0.0.1]:59591 + * + * The extended format is required to support OpenBSD's resolv.conf format: + * https://man.openbsd.org/OpenBSD-5.1/resolv.conf.5 + * As well as MacOS libresolv that may include a non-default port number. * * This will silently ignore blacklisted IPv6 nameservers as detected by * ares_ipv6_server_blacklisted(). @@ -1840,16 +1954,18 @@ static int ares_ipv6_server_blacklisted(const unsigned char ipaddr[16]) * Returns an error code on failure, else ARES_SUCCESS. */ static int config_nameserver(struct server_state **servers, int *nservers, - char *str) + const char *str) { struct ares_addr host; struct server_state *newserv; - char *p, *txtaddr; + const char *p, *txtaddr; /* On Windows, there may be more than one nameserver specified in the same * registry key, so we parse input as a space or comma seperated list. */ for (p = str; p;) { + unsigned short port; + /* Skip whitespace and commas. */ while (*p && (ISSPACE(*p) || (*p == ','))) p++; @@ -1863,23 +1979,11 @@ static int config_nameserver(struct server_state **servers, int *nservers, /* Advance past this address. */ while (*p && !ISSPACE(*p) && (*p != ',')) p++; - if (*p) - /* Null terminate this address. */ - *p++ = '\0'; - else - /* Reached end of input, done when this address is processed. */ - p = NULL; - /* Convert textual address to binary format. */ - if (ares_inet_pton(AF_INET, txtaddr, &host.addrV4) == 1) - host.family = AF_INET; - else if (ares_inet_pton(AF_INET6, txtaddr, &host.addrV6) == 1 - /* Silently skip blacklisted IPv6 servers. */ - && !ares_ipv6_server_blacklisted( - (const unsigned char *)&host.addrV6)) - host.family = AF_INET6; - else + if (parse_dnsaddrport(txtaddr, p-txtaddr, &host, &port) != + ARES_SUCCESS) { continue; + } /* Resize servers state array. */ newserv = ares_realloc(*servers, (*nservers + 1) * @@ -1889,8 +1993,8 @@ static int config_nameserver(struct server_state **servers, int *nservers, /* Store address data. */ newserv[*nservers].addr.family = host.family; - newserv[*nservers].addr.udp_port = 0; - newserv[*nservers].addr.tcp_port = 0; + newserv[*nservers].addr.udp_port = htons(port); + newserv[*nservers].addr.tcp_port = htons(port); if (host.family == AF_INET) memcpy(&newserv[*nservers].addr.addrV4, &host.addrV4, sizeof(host.addrV4));