]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
socket: support binding to interface *AND* IP
authorOrgad Shaneh <orgad.shaneh@audiocodes.com>
Fri, 17 May 2024 11:44:44 +0000 (14:44 +0300)
committerDaniel Stenberg <daniel@haxx.se>
Tue, 4 Jun 2024 21:47:54 +0000 (23:47 +0200)
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

.github/workflows/proselint.yml
docs/libcurl/opts/CURLOPT_INTERFACE.md
lib/cf-socket.c
lib/cf-socket.h
lib/setopt.c
lib/urldata.h
tests/data/Makefile.inc
tests/data/test1663 [new file with mode: 0644]
tests/unit/Makefile.inc
tests/unit/unit1663.c [new file with mode: 0644]

index b8526674d576270f76a1f3ba51bbacb5dcbbd472..19ebba4a908ce218f4bffa29f5581259c2003630 100644 (file)
@@ -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
index f79a43078eb65299771afe30ed3db91cec532171..c29db3e8182fea8ed135dec4df5dd7c701fe20b4 100644 (file)
@@ -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!<name>` - Interface name
+* `host!<name>` - IP address or hostname
+* `ifhost!<interface>!<host>` - 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
 
index 22827d37538569b8e3b0049a23b99e4e8c51bf53..3b95a6e35ac525e4bdea773b1e5f264dcd1e62ae 100644 (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,
+    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
-                        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;
-      }
+                      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(!is_interface) {
+    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;
     }
   }
index 38a4e5511dac4ba699a9a58c78a81cc3e749e834..6040058b0955d194502686ab08d20a3ba1c03442 100644 (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'.
index 4e4da969fd6a14299151985343a2b1b3164a67f3..7b05bd82132f6ad00c2db95d6ea8ccab112b8b53 100644 (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:
index f55ba5027d673faf18729b6a61ff171048fc8934..15eb1f8364865216f420aca541d74a2df4c678b1 100644 (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 */
index 6352b44656bf9e2ab8bdaa53e5abd8d84da18a82..d1e7e4ce773143df274430ccba4d9113b4018acc 100644 (file)
@@ -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 (file)
index 0000000..160bfde
--- /dev/null
@@ -0,0 +1,23 @@
+<testcase>
+<info>
+<keywords>
+unittest
+interface
+bind
+</keywords>
+</info>
+
+#
+# Client-side
+<client>
+<server>
+none
+</server>
+<features>
+unittest
+</features>
+<name>
+unit tests for interface option parsing
+</name>
+</client>
+</testcase>
index 1e48aadf918f9631af95daf67ed2511a3905faba..c402f803509c8a96a220750cf2808944047208b9 100644 (file)
@@ -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 (file)
index 0000000..f4801fe
--- /dev/null
@@ -0,0 +1,98 @@
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) 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.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 <netinet/in.h>
+#endif
+#ifdef HAVE_NETINET_IN6_H
+#include <netinet/in6.h>
+#endif
+
+#include <curl/curl.h>
+
+#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