socket: support binding to interface *AND* IP

Introduce new notation for CURLOPT_INTERFACE / --interface:
ifhost!<interface>!<host>

Binding to an interface doesn't set the address, and an interface can
have multiple addresses.

When binding to an address (without interface), the kernel is free to
choose the route, and it can route through any device that can access
the target address, not necessarily the one with the chosen address.

Moreover, it is possible for different interfaces to have the same IP
address, on which case we need to provide a way to be more specific.

Factor out the parsing part of interface option, and add unit tests:
1663.

Closes #13719
This commit is contained in:
Orgad Shaneh 2024-05-17 14:44:44 +03:00 committed by Daniel Stenberg
parent 23fe1a52dc
commit 3060557af7
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
10 changed files with 322 additions and 79 deletions

View file

@ -78,6 +78,7 @@
#include "multihandle.h"
#include "rand.h"
#include "share.h"
#include "strdup.h"
#include "version_win32.h"
/* The last 3 #include files should be in this order */
@ -435,6 +436,82 @@ void Curl_sndbuf_init(curl_socket_t sockfd)
}
#endif /* USE_WINSOCK */
/*
* Curl_parse_interface()
*
* This is used to parse interface argument in the following formats.
* In all the examples, `host` can be an IP address or a hostname.
*
* <iface_or_host> - can be either an interface name or a host.
* if!<iface> - interface name.
* host!<host> - host name.
* ifhost!<iface>!<host> - interface name and host name.
*
* Parameters:
*
* input [in] - input string.
* len [in] - length of the input string.
* dev [in/out] - address where a pointer to newly allocated memory
* holding the interface-or-host will be stored upon
* completion.
* iface [in/out] - address where a pointer to newly allocated memory
* holding the interface will be stored upon completion.
* host [in/out] - address where a pointer to newly allocated memory
* holding the host will be stored upon completion.
*
* Returns CURLE_OK on success.
*/
CURLcode Curl_parse_interface(const char *input, size_t len,
char **dev, char **iface, char **host)
{
static const char if_prefix[] = "if!";
static const char host_prefix[] = "host!";
static const char if_host_prefix[] = "ifhost!";
DEBUGASSERT(dev);
DEBUGASSERT(iface);
DEBUGASSERT(host);
if(strncmp(if_prefix, input, strlen(if_prefix)) == 0) {
input += strlen(if_prefix);
if(!*input)
return CURLE_BAD_FUNCTION_ARGUMENT;
*iface = Curl_memdup0(input, len - strlen(if_prefix));
return *iface ? CURLE_OK : CURLE_OUT_OF_MEMORY;
}
if(strncmp(host_prefix, input, strlen(host_prefix)) == 0) {
input += strlen(host_prefix);
if(!*input)
return CURLE_BAD_FUNCTION_ARGUMENT;
*host = Curl_memdup0(input, len - strlen(host_prefix));
return *host ? CURLE_OK : CURLE_OUT_OF_MEMORY;
}
if(strncmp(if_host_prefix, input, strlen(if_host_prefix)) == 0) {
const char *host_part;
input += strlen(if_host_prefix);
len -= strlen(if_host_prefix);
host_part = memchr(input, '!', len);
if(!host_part || !*(host_part + 1))
return CURLE_BAD_FUNCTION_ARGUMENT;
*iface = Curl_memdup0(input, host_part - input);
if(!*iface)
return CURLE_OUT_OF_MEMORY;
++host_part;
*host = Curl_memdup0(host_part, len - (host_part - input));
if(!*host) {
free(*iface);
*iface = NULL;
return CURLE_OUT_OF_MEMORY;
}
return CURLE_OK;
}
if(!*input)
return CURLE_BAD_FUNCTION_ARGUMENT;
*dev = Curl_memdup0(input, len);
return *dev ? CURLE_OK : CURLE_OUT_OF_MEMORY;
}
#ifndef CURL_DISABLE_BINDLOCAL
static CURLcode bindlocal(struct Curl_easy *data, struct connectdata *conn,
curl_socket_t sockfd, int af, unsigned int scope)
@ -453,6 +530,10 @@ static CURLcode bindlocal(struct Curl_easy *data, struct connectdata *conn,
/* how many port numbers to try to bind to, increasing one at a time */
int portnum = data->set.localportrange;
const char *dev = data->set.str[STRING_DEVICE];
const char *iface_input = data->set.str[STRING_INTERFACE];
const char *host_input = data->set.str[STRING_BINDHOST];
const char *iface = iface_input ? iface_input : dev;
const char *host = host_input ? host_input : dev;
int error;
#ifdef IP_BIND_ADDRESS_NO_PORT
int on = 1;
@ -464,81 +545,72 @@ static CURLcode bindlocal(struct Curl_easy *data, struct connectdata *conn,
/*************************************************************
* Select device to bind socket to
*************************************************************/
if(!dev && !port)
if(!iface && !host && !port)
/* no local kind of binding was requested */
return CURLE_OK;
memset(&sa, 0, sizeof(struct Curl_sockaddr_storage));
if(dev && (strlen(dev)<255) ) {
if(iface && (strlen(iface)<255) ) {
char myhost[256] = "";
int done = 0; /* -1 for error, 1 for address found */
bool is_interface = FALSE;
bool is_host = FALSE;
static const char *if_prefix = "if!";
static const char *host_prefix = "host!";
if(strncmp(if_prefix, dev, strlen(if_prefix)) == 0) {
dev += strlen(if_prefix);
is_interface = TRUE;
}
else if(strncmp(host_prefix, dev, strlen(host_prefix)) == 0) {
dev += strlen(host_prefix);
is_host = TRUE;
}
if2ip_result_t if2ip_result = IF2IP_NOT_FOUND;
/* interface */
if(!is_host) {
#ifdef SO_BINDTODEVICE
/*
* This binds the local socket to a particular interface. This will
* force even requests to other local interfaces to go out the external
* interface. Only bind to the interface when specified as interface,
* not just as a hostname or ip address.
*
* The interface might be a VRF, eg: vrf-blue, which means it cannot be
* converted to an IP address and would fail Curl_if2ip. Simply try to
* use it straight away.
*/
if(setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE,
dev, (curl_socklen_t)strlen(dev) + 1) == 0) {
/* This is often "errno 1, error: Operation not permitted" if you're
* not running as root or another suitable privileged user. If it
* succeeds it means the parameter was a valid interface and not an IP
* address. Return immediately.
*/
infof(data, "socket successfully bound to interface '%s'", dev);
/*
* This binds the local socket to a particular interface. This will
* force even requests to other local interfaces to go out the external
* interface. Only bind to the interface when specified as interface,
* not just as a hostname or ip address.
*
* The interface might be a VRF, eg: vrf-blue, which means it cannot be
* converted to an IP address and would fail Curl_if2ip. Simply try to
* use it straight away.
*/
if(setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE,
iface, (curl_socklen_t)strlen(iface) + 1) == 0) {
/* This is often "errno 1, error: Operation not permitted" if you're
* not running as root or another suitable privileged user. If it
* succeeds it means the parameter was a valid interface and not an IP
* address. Return immediately.
*/
if(!host_input) {
infof(data, "socket successfully bound to interface '%s'", iface);
return CURLE_OK;
}
#endif
switch(Curl_if2ip(af,
#ifdef USE_IPV6
scope, conn->scope_id,
#endif
dev, myhost, sizeof(myhost))) {
case IF2IP_NOT_FOUND:
if(is_interface) {
/* Do not fall back to treating it as a host name */
failf(data, "Couldn't bind to interface '%s'", dev);
return CURLE_INTERFACE_FAILED;
}
break;
case IF2IP_AF_NOT_SUPPORTED:
/* Signal the caller to try another address family if available */
return CURLE_UNSUPPORTED_PROTOCOL;
case IF2IP_FOUND:
is_interface = TRUE;
/*
* We now have the numerical IP address in the 'myhost' buffer
*/
infof(data, "Local Interface %s is ip %s using address family %i",
dev, myhost, af);
done = 1;
break;
}
}
if(!is_interface) {
#endif
if(!host_input) {
/* Discover IP from input device, then bind to it */
if2ip_result = Curl_if2ip(af,
#ifdef USE_IPV6
scope, conn->scope_id,
#endif
iface, myhost, sizeof(myhost));
}
switch(if2ip_result) {
case IF2IP_NOT_FOUND:
if(iface_input && !host_input) {
/* Do not fall back to treating it as a host name */
failf(data, "Couldn't bind to interface '%s'", iface);
return CURLE_INTERFACE_FAILED;
}
break;
case IF2IP_AF_NOT_SUPPORTED:
/* Signal the caller to try another address family if available */
return CURLE_UNSUPPORTED_PROTOCOL;
case IF2IP_FOUND:
/*
* We now have the numerical IP address in the 'myhost' buffer
*/
host = myhost;
infof(data, "Local Interface %s is ip %s using address family %i",
iface, host, af);
done = 1;
break;
}
if(!iface_input || host_input) {
/*
* This was not an interface, resolve the name as a host name
* or IP number
@ -557,7 +629,7 @@ static CURLcode bindlocal(struct Curl_easy *data, struct connectdata *conn,
conn->ip_version = CURL_IPRESOLVE_V6;
#endif
rc = Curl_resolv(data, dev, 80, FALSE, &h);
rc = Curl_resolv(data, host, 80, FALSE, &h);
if(rc == CURLRESOLV_PENDING)
(void)Curl_resolver_wait_resolv(data, &h);
conn->ip_version = ipver;
@ -566,7 +638,7 @@ static CURLcode bindlocal(struct Curl_easy *data, struct connectdata *conn,
/* convert the resolved address, sizeof myhost >= INET_ADDRSTRLEN */
Curl_printable_address(h->addr, myhost, sizeof(myhost));
infof(data, "Name '%s' family %i resolved to '%s' family %i",
dev, af, myhost, h->addr->ai_family);
host, af, myhost, h->addr->ai_family);
Curl_resolv_unlock(data, h);
if(af != h->addr->ai_family) {
/* bad IP version combo, signal the caller to try another address
@ -628,7 +700,7 @@ static CURLcode bindlocal(struct Curl_easy *data, struct connectdata *conn,
the error buffer, so the user receives this error message instead of a
generic resolve error. */
data->state.errorbuf = FALSE;
failf(data, "Couldn't bind to '%s'", dev);
failf(data, "Couldn't bind to '%s'", host);
return CURLE_INTERFACE_FAILED;
}
}

View file

@ -54,6 +54,11 @@ struct Curl_sockaddr_ex {
};
#define sa_addr _sa_ex_u.addr
/*
* Parse interface option, and return the interface name and the host part.
*/
CURLcode Curl_parse_interface(const char *input, size_t len,
char **dev, char **iface, char **host);
/*
* Create a socket based on info from 'conn' and 'ai'.

View file

@ -139,6 +139,42 @@ static CURLcode setstropt_userpwd(char *option, char **userp, char **passwdp)
return CURLE_OK;
}
static CURLcode setstropt_interface(
char *option, char **devp, char **ifacep, char **hostp)
{
char *dev = NULL;
char *iface = NULL;
char *host = NULL;
size_t len;
CURLcode result;
DEBUGASSERT(devp);
DEBUGASSERT(ifacep);
DEBUGASSERT(hostp);
/* Parse the interface details */
if(!option || !*option)
return CURLE_BAD_FUNCTION_ARGUMENT;
len = strlen(option);
if(len > 255)
return CURLE_BAD_FUNCTION_ARGUMENT;
result = Curl_parse_interface(option, len, &dev, &iface, &host);
if(result)
return result;
free(*devp);
*devp = dev;
free(*ifacep);
*ifacep = iface;
free(*hostp);
*hostp = host;
return CURLE_OK;
}
#define C_SSLVERSION_VALUE(x) (x & 0xffff)
#define C_SSLVERSION_MAX_VALUE(x) (x & 0xffff0000)
@ -1881,8 +1917,10 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
* Set what interface or address/hostname to bind the socket to when
* performing an operation and thus what from-IP your connection will use.
*/
result = Curl_setstropt(&data->set.str[STRING_DEVICE],
va_arg(param, char *));
result = setstropt_interface(va_arg(param, char *),
&data->set.str[STRING_DEVICE],
&data->set.str[STRING_INTERFACE],
&data->set.str[STRING_BINDHOST]);
break;
#ifndef CURL_DISABLE_BINDLOCAL
case CURLOPT_LOCALPORT:

View file

@ -1458,6 +1458,8 @@ enum dupstring {
STRING_CUSTOMREQUEST, /* HTTP/FTP/RTSP request/method to use */
STRING_DEFAULT_PROTOCOL, /* Protocol to use when the URL doesn't specify */
STRING_DEVICE, /* local network interface/address to use */
STRING_INTERFACE, /* local network interface to use */
STRING_BINDHOST, /* local address to use */
STRING_ENCODING, /* Accept-Encoding string */
#ifndef CURL_DISABLE_FTP
STRING_FTP_ACCOUNT, /* ftp account data */