diff --git a/.github/workflows/proselint.yml b/.github/workflows/proselint.yml index b8526674d5..19ebba4a90 100644 --- a/.github/workflows/proselint.yml +++ b/.github/workflows/proselint.yml @@ -51,7 +51,7 @@ jobs: JSON - name: check prose - run: a=`git ls-files '*.md' | grep -Ev '(docs/CHECKSRC.md|docs/DISTROS.md)'` && proselint $a README + run: git ls-files '*.md' | grep -Ev 'CHECKSRC.md|DISTROS.md|CURLOPT_INTERFACE.md' | xargs proselint README # This is for CHECKSRC and files with aggressive exclamation mark needs - name: create second proselint config @@ -68,4 +68,4 @@ jobs: JSON - name: check special prose - run: a=docs/CHECKSRC.md && proselint $a + run: proselint docs/CHECKSRC.md docs/libcurl/opts/CURLOPT_INTERFACE.md diff --git a/docs/libcurl/opts/CURLOPT_INTERFACE.md b/docs/libcurl/opts/CURLOPT_INTERFACE.md index f79a43078e..c29db3e818 100644 --- a/docs/libcurl/opts/CURLOPT_INTERFACE.md +++ b/docs/libcurl/opts/CURLOPT_INTERFACE.md @@ -28,15 +28,16 @@ CURLcode curl_easy_setopt(CURL *handle, CURLOPT_INTERFACE, char *interface); Pass a char pointer as parameter. This sets the *interface* name to use as outgoing network interface. The name can be an interface name, an IP address, -or a hostname. +or a hostname. If you prefer one of these, you can use the following special +prefixes: -If the parameter starts with "if!" then it is treated only as an interface -name. If the parameter starts with "host!" it is treated as either an IP -address or a hostname. +* `if!` - Interface name +* `host!` - IP address or hostname +* `ifhost!!` - Interface name and IP address or hostname -If "if!" is specified but the parameter does not match an existing interface, -*CURLE_INTERFACE_FAILED* is returned from the libcurl function used to perform -the transfer. +If `if!` or `ifhost!` is specified but the parameter does not match an existing +interface, *CURLE_INTERFACE_FAILED* is returned from the libcurl function used +to perform the transfer. libcurl does not support using network interface names for this option on Windows. @@ -74,7 +75,9 @@ int main(void) # AVAILABILITY -The "if!" and "host!" syntax was added in 7.24.0. +The `if!` and `host!` syntax was added in 7.24.0. + +The `ifhost!` syntax was added in 8.9.0. # RETURN VALUE diff --git a/lib/cf-socket.c b/lib/cf-socket.c index 22827d3753..3b95a6e35a 100644 --- a/lib/cf-socket.c +++ b/lib/cf-socket.c @@ -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. + * + * - can be either an interface name or a host. + * if! - interface name. + * host! - host name. + * ifhost!! - 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; } } diff --git a/lib/cf-socket.h b/lib/cf-socket.h index 38a4e5511d..6040058b09 100644 --- a/lib/cf-socket.h +++ b/lib/cf-socket.h @@ -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'. diff --git a/lib/setopt.c b/lib/setopt.c index 4e4da969fd..7b05bd8213 100644 --- a/lib/setopt.c +++ b/lib/setopt.c @@ -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: diff --git a/lib/urldata.h b/lib/urldata.h index f55ba5027d..15eb1f8364 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -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 */ diff --git a/tests/data/Makefile.inc b/tests/data/Makefile.inc index 6352b44656..d1e7e4ce77 100644 --- a/tests/data/Makefile.inc +++ b/tests/data/Makefile.inc @@ -211,7 +211,7 @@ test1620 test1621 \ test1630 test1631 test1632 test1633 test1634 test1635 \ \ test1650 test1651 test1652 test1653 test1654 test1655 \ -test1660 test1661 test1662 \ +test1660 test1661 test1662 test1663 \ \ test1670 test1671 \ \ diff --git a/tests/data/test1663 b/tests/data/test1663 new file mode 100644 index 0000000000..160bfde3f3 --- /dev/null +++ b/tests/data/test1663 @@ -0,0 +1,23 @@ + + + +unittest +interface +bind + + + +# +# Client-side + + +none + + +unittest + + +unit tests for interface option parsing + + + diff --git a/tests/unit/Makefile.inc b/tests/unit/Makefile.inc index 1e48aadf91..c402f80350 100644 --- a/tests/unit/Makefile.inc +++ b/tests/unit/Makefile.inc @@ -37,7 +37,7 @@ UNITPROGS = unit1300 unit1302 unit1303 unit1304 unit1305 unit1307 \ unit1608 unit1609 unit1610 unit1611 unit1612 unit1614 unit1615 unit1616 \ unit1620 unit1621 \ unit1650 unit1651 unit1652 unit1653 unit1654 unit1655 \ - unit1660 unit1661 \ + unit1660 unit1661 unit1663 \ unit2600 unit2601 unit2602 unit2603 unit2604 \ unit3200 \ unit3205 @@ -126,6 +126,8 @@ unit1660_SOURCES = unit1660.c $(UNITFILES) unit1661_SOURCES = unit1661.c $(UNITFILES) +unit1663_SOURCES = unit1663.c $(UNITFILES) + unit2600_SOURCES = unit2600.c $(UNITFILES) unit2601_SOURCES = unit2601.c $(UNITFILES) diff --git a/tests/unit/unit1663.c b/tests/unit/unit1663.c new file mode 100644 index 0000000000..f4801fe5cc --- /dev/null +++ b/tests/unit/unit1663.c @@ -0,0 +1,98 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , 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.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. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ +#include "curlcheck.h" + +#ifdef HAVE_NETINET_IN_H +#include +#endif +#ifdef HAVE_NETINET_IN6_H +#include +#endif + +#include + +#include "cf-socket.h" + +#include "memdebug.h" /* LAST include file */ + +static CURLcode unit_setup(void) +{ + CURLcode res = CURLE_OK; + global_init(CURL_GLOBAL_ALL); + return res; +} + +static void unit_stop(void) +{ + curl_global_cleanup(); +} + +static void test_parse( + const char *input, + const char *exp_dev, + const char *exp_iface, + const char *exp_host, + CURLcode exp_rc) +{ + char *dev = NULL; + char *iface = NULL; + char *host = NULL; + CURLcode rc = Curl_parse_interface( + input, strlen(input), &dev, &iface, &host); + fail_unless(rc == exp_rc, "Curl_parse_interface() failed"); + + fail_unless(!!exp_dev == !!dev, "dev expectation failed."); + fail_unless(!!exp_iface == !!iface, "iface expectation failed"); + fail_unless(!!exp_host == !!host, "host expectation failed"); + + if(!unitfail) { + fail_unless(!exp_dev || strcmp(dev, exp_dev) == 0, + "dev should be equal to exp_dev"); + fail_unless(!exp_iface || strcmp(iface, exp_iface) == 0, + "iface should be equal to exp_iface"); + fail_unless(!exp_host || strcmp(host, exp_host) == 0, + "host should be equal to exp_host"); + } + + free(dev); + free(iface); + free(host); +} + +UNITTEST_START +{ + test_parse("dev", "dev", NULL, NULL, CURLE_OK); + test_parse("if!eth0", NULL, "eth0", NULL, CURLE_OK); + test_parse("host!myname", NULL, NULL, "myname", CURLE_OK); + test_parse("ifhost!eth0!myname", NULL, "eth0", "myname", CURLE_OK); + test_parse("", NULL, NULL, NULL, CURLE_BAD_FUNCTION_ARGUMENT); + test_parse("!", "!", NULL, NULL, CURLE_OK); + test_parse("if!", NULL, NULL, NULL, CURLE_BAD_FUNCTION_ARGUMENT); + test_parse("if!eth0!blubb", NULL, "eth0!blubb", NULL, CURLE_OK); + test_parse("host!", NULL, NULL, NULL, CURLE_BAD_FUNCTION_ARGUMENT); + test_parse("ifhost!", NULL, NULL, NULL, CURLE_BAD_FUNCTION_ARGUMENT); + test_parse("ifhost!eth0", NULL, NULL, NULL, CURLE_BAD_FUNCTION_ARGUMENT); + test_parse("ifhost!eth0!", NULL, NULL, NULL, CURLE_BAD_FUNCTION_ARGUMENT); +} +UNITTEST_STOP