CURLOPT_RESOLVE: Add support for multiple IP addresses per entry

This enables users to preresolve but still take advantage of happy
eyeballs and trying multiple addresses if some are not connecting.

Ref: https://github.com/curl/curl/pull/2260
This commit is contained in:
Anders Bakken 2018-01-30 16:33:51 -08:00 committed by Jay Satiro
parent e77f0e5a5a
commit 50d1b3379a
9 changed files with 349 additions and 44 deletions

View File

@ -1,5 +1,5 @@
Long: resolve
Arg: <host:port:address>
Arg: <host:port:address[,address]...>
Help: Resolve the host+port to this address
Added: 7.21.3
---
@ -16,4 +16,6 @@ is set to make curl use another IP version.
Support for providing the IP address within [brackets] was added in 7.57.0.
Support for providing multiple IP addresses per entry was added in 7.59.0.
This option can be used many times to add many host names to resolve.

View File

@ -37,10 +37,12 @@ list of \fBstruct curl_slist\fP structs properly filled in. Use
to clean up an entire list.
Each single name resolve string should be written using the format
HOST:PORT:ADDRESS where HOST is the name libcurl will try to resolve, PORT is
the port number of the service where libcurl wants to connect to the HOST and
ADDRESS is the numerical IP address. If libcurl is built to support IPv6,
ADDRESS can of course be either IPv4 or IPv6 style addressing.
HOST:PORT:ADDRESS[,ADDRESS]... where HOST is the name libcurl will try
to resolve, PORT is the port number of the service where libcurl wants
to connect to the HOST and ADDRESS is one or more numerical IP
addresses. If you specify multiple ip addresses they need to be
separated by comma. If libcurl is built to support IPv6, each of the
ADDRESS entries can of course be either IPv4 or IPv6 style addressing.
This option effectively pre-populates the DNS cache with entries for the
host+port pair so redirects and everything that operations against the
@ -57,6 +59,8 @@ by including a string in the linked list that uses the format
and port number must exactly match what was already added previously.
Support for providing the ADDRESS within [brackets] was added in 7.57.0.
Support for providing multiple IP addresses per entry was added in 7.59.0.
.SH DEFAULT
NULL
.SH PROTOCOLS

View File

@ -619,8 +619,8 @@ void Curl_persistconninfo(struct connectdata *conn)
/* retrieves ip address and port from a sockaddr structure.
note it calls Curl_inet_ntop which sets errno on fail, not SOCKERRNO. */
static bool getaddressinfo(struct sockaddr *sa, char *addr,
long *port)
bool Curl_getaddressinfo(struct sockaddr *sa, char *addr,
long *port)
{
unsigned short us_port;
struct sockaddr_in *si = NULL;
@ -700,16 +700,16 @@ void Curl_updateconninfo(struct connectdata *conn, curl_socket_t sockfd)
return;
}
if(!getaddressinfo((struct sockaddr*)&ssrem,
conn->primary_ip, &conn->primary_port)) {
if(!Curl_getaddressinfo((struct sockaddr*)&ssrem,
conn->primary_ip, &conn->primary_port)) {
failf(data, "ssrem inet_ntop() failed with errno %d: %s",
errno, Curl_strerror(conn, errno));
return;
}
memcpy(conn->ip_addr_str, conn->primary_ip, MAX_IPADR_LEN);
if(!getaddressinfo((struct sockaddr*)&ssloc,
conn->local_ip, &conn->local_port)) {
if(!Curl_getaddressinfo((struct sockaddr*)&ssloc,
conn->local_ip, &conn->local_port)) {
failf(data, "ssloc inet_ntop() failed with errno %d: %s",
errno, Curl_strerror(conn, errno));
return;
@ -1005,8 +1005,8 @@ static CURLcode singleipconnect(struct connectdata *conn,
return CURLE_OK;
/* store remote address and port used in this connection attempt */
if(!getaddressinfo((struct sockaddr*)&addr.sa_addr,
ipaddress, &port)) {
if(!Curl_getaddressinfo((struct sockaddr*)&addr.sa_addr,
ipaddress, &port)) {
/* malformed address or bug in inet_ntop, try next address */
failf(data, "sa_addr inet_ntop() failed with errno %d: %s",
errno, Curl_strerror(conn, errno));

View File

@ -77,6 +77,11 @@ void Curl_updateconninfo(struct connectdata *conn, curl_socket_t sockfd);
void Curl_persistconninfo(struct connectdata *conn);
int Curl_closesocket(struct connectdata *conn, curl_socket_t sock);
/*
* Get presentation format IP address and port from a sockaddr.
*/
bool Curl_getaddressinfo(struct sockaddr *sa, char *addr, long *port);
/*
* The Curl_sockaddr_ex structure is basically libcurl's external API
* curl_sockaddr structure with enough space available to directly hold any

View File

@ -781,7 +781,7 @@ CURLcode Curl_loadhostpairs(struct Curl_easy *data)
{
struct curl_slist *hostp;
char hostname[256];
int port;
int port = 0;
for(hostp = data->change.resolve; hostp; hostp = hostp->next) {
if(!hostp->data)
@ -819,32 +819,95 @@ CURLcode Curl_loadhostpairs(struct Curl_easy *data)
}
else {
struct Curl_dns_entry *dns;
Curl_addrinfo *addr;
Curl_addrinfo *head = NULL, *tail = NULL;
char *entry_id;
size_t entry_len;
char buffer[256];
char *address = &buffer[0];
char address[64];
char *addresses;
char *addr_begin;
char *addr_end;
char *port_ptr;
char *end_ptr;
char *host_end;
unsigned long tmp_port;
bool error = true;
if(3 != sscanf(hostp->data, "%255[^:]:%d:%255s", hostname, &port,
address)) {
host_end = strchr(hostp->data, ':');
if(!host_end ||
((host_end - hostp->data) >= (ptrdiff_t)sizeof(hostname)))
goto err;
memcpy(hostname, hostp->data, host_end - hostp->data);
hostname[host_end - hostp->data] = '\0';
port_ptr = host_end + 1;
tmp_port = strtoul(port_ptr, &end_ptr, 10);
if(end_ptr == port_ptr || tmp_port > USHRT_MAX || *end_ptr != ':')
goto err;
port = (int)tmp_port;
addresses = end_ptr + 1;
while(*end_ptr) {
size_t alen;
Curl_addrinfo *ai;
addr_begin = end_ptr + 1;
addr_end = strchr(addr_begin, ',');
if(!addr_end)
addr_end = addr_begin + strlen(addr_begin);
end_ptr = addr_end;
/* allow IP(v6) address within [brackets] */
if(*addr_begin == '[') {
if(addr_end == addr_begin || *(addr_end - 1) != ']')
goto err;
++addr_begin;
--addr_end;
}
alen = addr_end - addr_begin;
if(!alen)
continue;
if(alen >= sizeof(address))
goto err;
memcpy(address, addr_begin, alen);
address[alen] = '\0';
#ifndef ENABLE_IPV6
if(strchr(address, ':')) {
infof(data, "Ignoring resolve address '%s', missing IPv6 support.\n",
address);
continue;
}
#endif
ai = Curl_str2addr(address, port);
if(!ai) {
infof(data, "Resolve address '%s' found illegal!\n", address);
goto err;
}
if(tail) {
tail->ai_next = ai;
tail = tail->ai_next;
}
else {
head = tail = ai;
}
}
if(!head)
goto err;
error = false;
err:
if(error) {
infof(data, "Couldn't parse CURLOPT_RESOLVE entry '%s'!\n",
hostp->data);
continue;
}
/* allow IP(v6) address within [brackets] */
if(address[0] == '[') {
size_t alen = strlen(address);
if(address[alen-1] != ']')
/* it needs to also end with ] to be valid */
continue;
address[alen-1] = 0; /* zero terminate there */
address++; /* pass the open bracket */
}
addr = Curl_str2addr(address, port);
if(!addr) {
infof(data, "Address in '%s' found illegal!\n", hostp->data);
Curl_freeaddrinfo(head);
continue;
}
@ -852,10 +915,9 @@ CURLcode Curl_loadhostpairs(struct Curl_easy *data)
entry_id = create_hostcache_id(hostname, port);
/* If we can't create the entry id, fail */
if(!entry_id) {
Curl_freeaddrinfo(addr);
Curl_freeaddrinfo(head);
return CURLE_OUT_OF_MEMORY;
}
entry_len = strlen(entry_id);
if(data->share)
@ -869,7 +931,7 @@ CURLcode Curl_loadhostpairs(struct Curl_easy *data)
if(!dns) {
/* if not in the cache already, put this host in the cache */
dns = Curl_cache_addr(data, addr, hostname, port);
dns = Curl_cache_addr(data, head, hostname, port);
if(dns) {
dns->timestamp = 0; /* mark as added by CURLOPT_RESOLVE */
/* release the returned reference; the cache itself will keep the
@ -880,19 +942,19 @@ CURLcode Curl_loadhostpairs(struct Curl_easy *data)
else {
/* this is a duplicate, free it again */
infof(data, "RESOLVE %s:%d is already cached, %s not stored!\n",
hostname, port, address);
Curl_freeaddrinfo(addr);
hostname, port, addresses);
Curl_freeaddrinfo(head);
}
if(data->share)
Curl_share_unlock(data, CURL_LOCK_DATA_DNS);
if(!dns) {
Curl_freeaddrinfo(addr);
Curl_freeaddrinfo(head);
return CURLE_OUT_OF_MEMORY;
}
infof(data, "Added %s:%d:%s to DNS cache\n",
hostname, port, address);
hostname, port, addresses);
}
}
data->change.resolve = NULL; /* dealt with now */

View File

@ -172,7 +172,7 @@ test1525 test1526 test1527 test1528 test1529 test1530 test1531 test1532 \
test1533 test1534 test1535 test1536 test1537 test1538 \
test1540 \
test1550 test1551 test1552 test1553 test1554 test1555 test1556 \
test1600 test1601 test1602 test1603 test1604 test1605 test1606 \
test1600 test1601 test1602 test1603 test1604 test1605 test1606 test1607 \
\
test1700 test1701 test1702 \
\

26
tests/data/test1607 Normal file
View File

@ -0,0 +1,26 @@
<testcase>
<info>
<keywords>
unittest
CURLOPT_RESOLVE
</keywords>
</info>
#
# Client-side
<client>
<server>
none
</server>
<features>
unittest
</features>
<name>
CURLOPT_RESOLVE parsing
</name>
<tool>
unit1607
</tool>
</client>
</testcase>

View File

@ -9,7 +9,7 @@ UNITPROGS = unit1300 unit1301 unit1302 unit1303 unit1304 unit1305 unit1307 \
unit1308 unit1309 unit1323 \
unit1330 unit1394 unit1395 unit1396 unit1397 unit1398 \
unit1399 \
unit1600 unit1601 unit1602 unit1603 unit1604 unit1605 unit1606
unit1600 unit1601 unit1602 unit1603 unit1604 unit1605 unit1606 unit1607
unit1300_SOURCES = unit1300.c $(UNITFILES)
unit1300_CPPFLAGS = $(AM_CPPFLAGS)
@ -85,3 +85,6 @@ unit1605_CPPFLAGS = $(AM_CPPFLAGS)
unit1606_SOURCES = unit1606.c $(UNITFILES)
unit1606_CPPFLAGS = $(AM_CPPFLAGS)
unit1607_SOURCES = unit1607.c $(UNITFILES)
unit1607_CPPFLAGS = $(AM_CPPFLAGS)

203
tests/unit/unit1607.c Normal file
View File

@ -0,0 +1,203 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.haxx.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
***************************************************************************/
#include "curlcheck.h"
#include "urldata.h"
#include "connect.h"
#include "share.h"
#include "memdebug.h" /* LAST include file */
static struct Curl_easy *easy;
struct curl_hash *hostcache;
static CURLcode unit_setup(void)
{
int res = CURLE_OK;
global_init(CURL_GLOBAL_ALL);
easy = curl_easy_init();
if(!easy)
return CURLE_OUT_OF_MEMORY;
hostcache = Curl_global_host_cache_init();
if(!hostcache)
return CURLE_OUT_OF_MEMORY;
return res;
}
static void unit_stop(void)
{
curl_easy_cleanup(easy);
curl_global_cleanup();
}
struct testcase {
/* host:port:address[,address]... */
const char *optval;
/* lowercase host and port to retrieve the addresses from hostcache */
const char *host;
int port;
/* 0 to 9 addresses expected from hostcache */
const char *address[10];
};
/* In builds without IPv6 support CURLOPT_RESOLVE should skip over those
addresses, so we have to do that as well. */
static const char skip = 0;
#ifdef ENABLE_IPV6
#define IPV6ONLY(x) x
#else
#define IPV6ONLY(x) &skip
#endif
/* CURLOPT_RESOLVE address parsing tests */
static const struct testcase tests[] = {
/* spaces aren't allowed, for now */
{ "test.com:80:127.0.0.1, 127.0.0.2",
"test.com", 80, { NULL, }
},
{ "TEST.com:80:,,127.0.0.1,,,127.0.0.2,,,,::1,,,",
"test.com", 80, { "127.0.0.1", "127.0.0.2", IPV6ONLY("::1"), }
},
{ "test.com:80:::1,127.0.0.1",
"test.com", 80, { IPV6ONLY("::1"), "127.0.0.1", }
},
{ "test.com:80:[::1],127.0.0.1",
"test.com", 80, { IPV6ONLY("::1"), "127.0.0.1", }
},
{ "test.com:80:::1",
"test.com", 80, { IPV6ONLY("::1"), }
},
{ "test.com:80:[::1]",
"test.com", 80, { IPV6ONLY("::1"), }
},
{ "test.com:80:127.0.0.1",
"test.com", 80, { "127.0.0.1", }
},
{ "test.com:80:,127.0.0.1",
"test.com", 80, { "127.0.0.1", }
},
{ "test.com:80:127.0.0.1,",
"test.com", 80, { "127.0.0.1", }
},
{ "test.com:0:127.0.0.1",
"test.com", 0, { "127.0.0.1", }
},
};
UNITTEST_START
int i;
int testnum = sizeof(tests) / sizeof(struct testcase);
for(i = 0; i < testnum; ++i, curl_easy_reset(easy)) {
int j;
int addressnum = sizeof tests[i].address / sizeof *tests[i].address;
struct Curl_addrinfo *addr;
struct Curl_dns_entry *dns;
struct curl_slist *list;
void *entry_id;
bool problem = false;
Curl_hostcache_clean(easy, hostcache);
easy->dns.hostcache = hostcache;
easy->dns.hostcachetype = HCACHE_GLOBAL;
list = curl_slist_append(NULL, tests[i].optval);
curl_easy_setopt(easy, CURLOPT_RESOLVE, list);
Curl_loadhostpairs(easy);
entry_id = (void *)aprintf("%s:%d", tests[i].host, tests[i].port);
dns = Curl_hash_pick(easy->dns.hostcache, entry_id, strlen(entry_id) + 1);
free(entry_id);
entry_id = NULL;
addr = dns ? dns->addr : NULL;
for(j = 0; j < addressnum; ++j) {
long port = 0;
char ipaddress[MAX_IPADR_LEN] = {0};
if(!addr && !tests[i].address[j])
break;
if(tests[i].address[j] == &skip)
continue;
if(addr && !Curl_getaddressinfo(addr->ai_addr,
ipaddress, &port)) {
fprintf(stderr, "%s:%d tests[%d] failed. getaddressinfo failed.\n",
__FILE__, __LINE__, i);
problem = true;
break;
}
if(addr && !tests[i].address[j]) {
fprintf(stderr, "%s:%d tests[%d] failed. the retrieved addr "
"is %s but tests[%d].address[%d] is NULL.\n",
__FILE__, __LINE__, i, ipaddress, i, j);
problem = true;
break;
}
if(!addr && tests[i].address[j]) {
fprintf(stderr, "%s:%d tests[%d] failed. the retrieved addr "
"is NULL but tests[%d].address[%d] is %s.\n",
__FILE__, __LINE__, i, i, j, tests[i].address[j]);
problem = true;
break;
}
if(!curl_strequal(ipaddress, tests[i].address[j])) {
fprintf(stderr, "%s:%d tests[%d] failed. the retrieved addr "
"%s is not equal to tests[%d].address[%d] %s.\n",
__FILE__, __LINE__, i, ipaddress, i, j, tests[i].address[j]);
problem = true;
break;
}
if(port != tests[i].port) {
fprintf(stderr, "%s:%d tests[%d] failed. the retrieved port "
"for tests[%d].address[%d] is %ld but tests[%d].port is %d.\n",
__FILE__, __LINE__, i, i, j, port, i, tests[i].port);
problem = true;
break;
}
addr = addr->ai_next;
}
Curl_hostcache_clean(easy, easy->dns.hostcache);
curl_slist_free_all(list);
if(problem) {
unitfail++;
continue;
}
}
UNITTEST_STOP