From defa8471096e6585b61909c7cf692916a121804c Mon Sep 17 00:00:00 2001 From: Brad House Date: Fri, 13 Sep 2024 11:06:40 -0400 Subject: [PATCH] link-local scope --- README.md | 2 ++ src/lib/include/ares_str.h | 1 + src/lib/str/ares_str.c | 18 +++++++++++++- src/lib/util/ares_uri.c | 50 ++++++++++++++++++++++++++++++++------ src/lib/util/ares_uri.h | 3 ++- test/ares-test-fuzz-name.c | 29 ++++++++++++++++++++++ test/ares-test-fuzz.c | 2 +- test/ares-test-internal.cc | 1 + 8 files changed, 95 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 401906047f..6566c9fe6a 100644 --- a/README.md +++ b/README.md @@ -154,3 +154,5 @@ See [Features](FEATURES.md) IPv6 address sorting as used by `ares_getaddrinfo()`. - [RFC7413](https://datatracker.ietf.org/doc/html/rfc7413). TCP FastOpen (TFO) for 0-RTT TCP Connection Resumption. +- [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986). + Uniform Resource Identifier (URI). Used for server configuration. diff --git a/src/lib/include/ares_str.h b/src/lib/include/ares_str.h index d598bd12a8..12554ca4d0 100644 --- a/src/lib/include/ares_str.h +++ b/src/lib/include/ares_str.h @@ -44,6 +44,7 @@ CARES_EXTERN size_t ares_strlen(const char *str); CARES_EXTERN size_t ares_strcpy(char *dest, const char *src, size_t dest_size); CARES_EXTERN ares_bool_t ares_str_isnum(const char *str); +CARES_EXTERN ares_bool_t ares_str_isalnum(const char *str); CARES_EXTERN void ares_str_ltrim(char *str); CARES_EXTERN void ares_str_rtrim(char *str); diff --git a/src/lib/str/ares_str.c b/src/lib/str/ares_str.c index b3f642536c..17ccfb3d99 100644 --- a/src/lib/str/ares_str.c +++ b/src/lib/str/ares_str.c @@ -101,7 +101,23 @@ ares_bool_t ares_str_isnum(const char *str) } for (i = 0; str[i] != 0; i++) { - if (str[i] < '0' || str[i] > '9') { + if (!ares_isdigit(str[i])) { + return ARES_FALSE; + } + } + return ARES_TRUE; +} + +ares_bool_t ares_str_isalnum(const char *str) +{ + size_t i; + + if (str == NULL || *str == 0) { + return ARES_FALSE; + } + + for (i = 0; str[i] != 0; i++) { + if (!ares_isdigit(str[i]) && !ares_isalpha(str[i])) { return ARES_FALSE; } } diff --git a/src/lib/util/ares_uri.c b/src/lib/util/ares_uri.c index 5bb10a9ee9..605fbd21ba 100644 --- a/src/lib/util/ares_uri.c +++ b/src/lib/util/ares_uri.c @@ -416,16 +416,43 @@ ares_status_t ares_uri_set_host(ares_uri_t *uri, const char *host) { struct ares_addr addr; size_t addrlen; + char hoststr[256]; + char *ll_scope; - if (uri == NULL || ares_strlen(host) == 0) { + if (uri == NULL || ares_strlen(host) == 0 || + ares_strlen(host) >= sizeof(hoststr)) { return ARES_EFORMERR; } + ares_strcpy(hoststr, host, sizeof(hoststr)); + + /* Look for '%' which could be a link-local scope for ipv6 addresses and + * parse it off */ + ll_scope = strchr(hoststr, '%'); + if (ll_scope != NULL) { + *ll_scope = 0; + ll_scope++; + if (!ares_str_isalnum(ll_scope)) { + return ARES_EBADNAME; + } + } + /* If its an IP address, normalize it */ memset(&addr, 0, sizeof(addr)); addr.family = AF_UNSPEC; - if (ares_dns_pton(host, &addr, &addrlen) != NULL) { - ares_inet_ntop(addr.family, &addr.addr, uri->host, sizeof(uri->host)); + if (ares_dns_pton(hoststr, &addr, &addrlen) != NULL) { + char ipaddr[256]; + ares_inet_ntop(addr.family, &addr.addr, ipaddr, sizeof(ipaddr)); + /* Only IPv6 is allowed to have a scope */ + if (ll_scope != NULL && addr.family != AF_INET6) { + return ARES_EBADNAME; + } + + if (ll_scope != NULL) { + snprintf(uri->host, sizeof(uri->host), "%s%%%s", ipaddr, ll_scope); + } else { + ares_strcpy(uri->host, ipaddr, sizeof(uri->host)); + } return ARES_SUCCESS; } @@ -647,8 +674,6 @@ static ares_status_t ares_uri_write_scheme(ares_uri_t *uri, ares_buf_t *buf) static ares_status_t ares_uri_write_authority(ares_uri_t *uri, ares_buf_t *buf) { ares_status_t status; - struct ares_addr addr; - size_t addrlen; ares_bool_t is_ipv6 = ARES_FALSE; if (ares_strlen(uri->username)) { @@ -678,10 +703,19 @@ static ares_status_t ares_uri_write_authority(ares_uri_t *uri, ares_buf_t *buf) } /* We need to write ipv6 addresses with [ ] */ - memset(&addr, 0, sizeof(addr)); - addr.family = AF_INET6; - if (ares_dns_pton(uri->host, &addr, &addrlen) != NULL) { + if (strchr(uri->host, '%') != NULL) { + /* If we have a % in the name, it must be ipv6 link local scope, so we + * don't need to check anything else */ is_ipv6 = ARES_TRUE; + } else { + /* Parse the host to see if it is an ipv6 address */ + struct ares_addr addr; + size_t addrlen; + memset(&addr, 0, sizeof(addr)); + addr.family = AF_INET6; + if (ares_dns_pton(uri->host, &addr, &addrlen) != NULL) { + is_ipv6 = ARES_TRUE; + } } if (is_ipv6) { diff --git a/src/lib/util/ares_uri.h b/src/lib/util/ares_uri.h index 5d088f022e..398a93bf5e 100644 --- a/src/lib/util/ares_uri.h +++ b/src/lib/util/ares_uri.h @@ -113,7 +113,8 @@ const char *ares_uri_get_password(ares_uri_t *uri); */ ares_status_t ares_uri_set_host(ares_uri_t *uri, const char *host); -/*! Retrieve the currently configured host (or ip address). +/*! Retrieve the currently configured host (or ip address). IPv6 addresses + * May include a link-local scope (e.g. fe80::b542:84df:1719:65e3%en0). * * \param[in] uri Initialized URI object * \return string containing host, maybe NULL if not set. diff --git a/test/ares-test-fuzz-name.c b/test/ares-test-fuzz-name.c index f32d347b75..fece38c903 100644 --- a/test/ares-test-fuzz-name.c +++ b/test/ares-test-fuzz-name.c @@ -33,6 +33,11 @@ int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size); +/* Fuzzing on a query name isn't very useful as its already fuzzed as part + * of the normal fuzzing operations. So we'll disable this by default and + * instead use this same fuzzer to validate our URI scheme parsers accessed + * via ares_set_servers_csv() */ +#ifdef USE_LEGACY_FUZZERS /* Entrypoint for Clang's libfuzzer, exercising query creation. */ int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size) { @@ -48,3 +53,27 @@ int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size) free(name); return 0; } + +#else + +int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size) +{ + ares_channel_t *channel = NULL; + char *csv; + + ares_library_init(ARES_LIB_INIT_ALL); + ares_init(&channel); + + /* Need to null-term data */ + csv = malloc(size + 1); + memcpy(csv, data, size); + csv[size] = '\0'; + ares_set_servers_csv(channel, csv); + free(csv); + + ares_destroy(channel); + ares_library_cleanup(); + + return 0; +} +#endif diff --git a/test/ares-test-fuzz.c b/test/ares-test-fuzz.c index 38d720bbaf..6c5fc75306 100644 --- a/test/ares-test-fuzz.c +++ b/test/ares-test-fuzz.c @@ -31,7 +31,7 @@ int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size); -#ifdef USE_LEGACY_PARSERS +#ifdef USE_LEGACY_FUZZERS /* This implementation calls the legacy c-ares parsers, which historically * all used different logic and parsing. As of c-ares 1.21.0 these are diff --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc index ba8e72985b..32b68e3605 100644 --- a/test/ares-test-internal.cc +++ b/test/ares-test-internal.cc @@ -366,6 +366,7 @@ TEST_F(LibraryTest, URI) { { ARES_TRUE, "https://www.example.com?key=%41%61%32%2D%2E%5f%7e%2F%3F%21%24%27%28%29%2a%2C%3b%3a%40", "https://www.example.com?key=Aa2-._~/?!$'()*,;:@" }, { ARES_TRUE, "dns+tls://192.168.1.1:53", NULL }, { ARES_TRUE, "dns+tls://[fe80::1]:53", NULL }, + { ARES_TRUE, "dns://[fe80::b542:84df:1719:65e3%en0]", NULL }, { ARES_TRUE, "dns+tls://[fe80:00::00:1]:53", "dns+tls://[fe80::1]:53" }, { ARES_TRUE, "d.n+s-tls://www.example.com", NULL }, { ARES_FALSE, "dns*tls://www.example.com", NULL },