Hello,

I would like to propose a patch that  adds initial support for Multipath TCP 
for wget.

This patch provides the "--mptcp" command line option that enables Multipath 
TCP when set.

Multipath TCP is a TCP extension defined in RFC8684 that is supported by recent 
Linux kernels (5.13+).
Multipath TCP allows a TCP connection to use different paths (e.g. Wi-Fi and 
cellular on smartphones, IPv4 and IPv6, ...).
Multipath TCP is enabled by default on recent Linux distributions (Debian, 
Ubuntu, Redhat,...)

If running on a kernel that does not support Multipath TCP, wget will just 
behave normally.
Another possibility would be to always enable MultipathTCP by default on Linux.

Use ./configure with --enable-mptcp to enable Multipath TCP support.

This patch modifies the following files:
* configure.ac
* src/connect.c
* src/connect.h
* src/init.c
* src/main.c
* src/options.h
* testenv/Makefile.am
* testenv/Test-mptcp-enabled.py
* testenv/conf/was_mptcp_enabled.py
* testenv/server/http/http_server.py

I appreciate your feedback,
Bastien Wiaux

diff --git a/configure.ac b/configure.ac
index aff89c1b..c635662c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -205,6 +205,17 @@ AS_IF([test "x$ENABLE_DEBUG" = xyes],
   []
 )
 
+dnl MPTCP: Support for mptcp
+AC_ARG_ENABLE([mptcp],
+  [AS_HELP_STRING([--enable-mptcp], [enable support for mptcp])],
+  [ENABLE_MPTCP=$enableval],
+  [ENABLE_MPTCP=no])
+
+AS_IF([test "x$ENABLE_MPTCP" = xyes],
+  [AC_DEFINE([ENABLE_MPTCP], [1], [Define if MPTCP support is enabled.])],
+  []
+)
+
 dnl Valgrind-tests: Should test suite be run under valgrind?
 AC_ARG_ENABLE(valgrind-tests,
   [AS_HELP_STRING([--enable-valgrind-tests], [enable using Valgrind for tests])],
@@ -1015,6 +1026,7 @@ AC_MSG_NOTICE([Summary of build options:
   OPIE:              $ENABLE_OPIE
   POSIX xattr:       $ENABLE_XATTR
   Debugging:         $ENABLE_DEBUG
+  MPTCP:             $ENABLE_MPTCP
   Assertions:        $ENABLE_ASSERTION
   Valgrind:          $VALGRIND_INFO
   Metalink:          $with_metalink
diff --git a/src/connect.c b/src/connect.c
index 780de480..0b004661 100644
--- a/src/connect.c
+++ b/src/connect.c
@@ -299,7 +299,16 @@ connect_to_ip (const ip_address *ip, int port, const char *print)
   sockaddr_set_data (sa, ip, port);
 
   /* Create the socket of the family appropriate for the address.  */
-  sock = socket (sa->sa_family, SOCK_STREAM, 0);
+  if (opt.mptcp){
+    sock = socket (sa->sa_family, SOCK_STREAM, IPPROTO_MPTCP);
+    if (sock == -1){
+      logprintf (LOG_VERBOSE, _("MPTCP not supported, opening classic socket.\n"));
+      sock = socket (sa->sa_family, SOCK_STREAM, 0);
+    }
+  } else {
+    sock = socket (sa->sa_family, SOCK_STREAM, 0);
+  }
+
   if (sock < 0)
     goto err;
 
@@ -461,7 +470,12 @@ bind_local (const ip_address *bind_address, int *port)
   void *setopt_ptr = (void *)&setopt_val;
   socklen_t setopt_size = sizeof (setopt_val);
 
-  sock = socket (bind_address->family, SOCK_STREAM, 0);
+  if (opt.mptcp){
+    sock = socket (bind_address->family, SOCK_STREAM, IPPROTO_MPTCP);
+  } else {
+    sock = socket (bind_address->family, SOCK_STREAM, 0);
+  }
+
   if (sock < 0)
     return -1;
 
@@ -1053,6 +1067,19 @@ fd_close (int fd)
   if (transport_map)
     info = hash_table_get (transport_map, (void *)(intptr_t) fd);
 
+  if (opt.mptcp){
+    struct mptcp_info inf;
+    socklen_t optlen;
+    int ret;
+      optlen = sizeof(inf);
+      if(!getsockopt(fd, SOL_MPTCP, MPTCP_INFO, &inf, &optlen)) {
+        DEBUGP (("Multipath TCP was ENABLED on this connection\n"));
+      }
+      else {
+        DEBUGP (("Multipath TCP was DISABLED on this connection\n"));
+      }
+  }
+
   if (info && info->imp->closer)
     info->imp->closer (fd, info->ctx);
   else
diff --git a/src/connect.h b/src/connect.h
index d03a1708..e50fafe7 100644
--- a/src/connect.h
+++ b/src/connect.h
@@ -86,4 +86,38 @@ int select_fd_nb (int, double, int);
 #define select_fd_nb select_fd
 #endif
 
+#ifdef ENABLE_MPTCP
+#ifndef IPPROTO_MPTCP
+#define IPPROTO_MPTCP 262
+#endif
+#include <linux/types.h>
+#ifndef SOL_MPTCP
+#define SOL_MPTCP 284
+#endif
+
+#ifndef MPTCP_INFO
+struct mptcp_info {
+	__u8	mptcpi_subflows;
+	__u8	mptcpi_add_addr_signal;
+	__u8	mptcpi_add_addr_accepted;
+	__u8	mptcpi_subflows_max;
+	__u8	mptcpi_add_addr_signal_max;
+	__u8	mptcpi_add_addr_accepted_max;
+	__u32	mptcpi_flags;
+	__u32	mptcpi_token;
+	__u64	mptcpi_write_seq;
+	__u64	mptcpi_snd_una;
+	__u64	mptcpi_rcv_nxt;
+	__u8	mptcpi_local_addr_used;
+	__u8	mptcpi_local_addr_max;
+	__u8	mptcpi_csum_enabled;
+};
+
+
+#define MPTCP_INFO		1
+#define MPTCP_TCPINFO		2
+#define MPTCP_SUBFLOW_ADDRS	3
+#endif
+#endif
+
 #endif /* CONNECT_H */
diff --git a/src/init.c b/src/init.c
index fbe09974..80a99a72 100644
--- a/src/init.c
+++ b/src/init.c
@@ -261,6 +261,7 @@ static const struct {
 #endif
   { "method",           &opt.method,            cmd_string_uppercase },
   { "mirror",           NULL,                   cmd_spec_mirror },
+  { "mptcp",            &opt.mptcp,             cmd_boolean },
   { "netrc",            &opt.netrc,             cmd_boolean },
   { "noclobber",        &opt.noclobber,         cmd_boolean },
   { "noconfig",         &opt.noconfig,          cmd_boolean },
diff --git a/src/main.c b/src/main.c
index d1c3c3e7..5b352916 100644
--- a/src/main.c
+++ b/src/main.c
@@ -301,6 +301,7 @@ static struct cmdline_option option_data[] =
     IF_SSL ( "crl-file", 0, OPT_VALUE, "crlfile", -1 )
     { "cut-dirs", 0, OPT_VALUE, "cutdirs", -1 },
     { "debug", 'd', OPT_BOOLEAN, "debug", -1 },
+    { "mptcp", 0, OPT_BOOLEAN, "mptcp", -1 },
     { "default-page", 0, OPT_VALUE, "defaultpage", -1 },
     { "delete-after", 0, OPT_BOOLEAN, "deleteafter", -1 },
     { "directories", 0, OPT_BOOLEAN, "dirstruct", -1 },
@@ -611,6 +612,10 @@ Logging and input file:\n"),
     N_("\
   -d,  --debug                     print lots of debugging information\n"),
 #endif
+#ifdef ENABLE_MPTCP
+    N_("\
+  --mptcp                     Use Multipath TCP\n"),
+#endif
 #ifdef USE_WATT32
     N_("\
        --wdebug                    print Watt-32 debug output\n"),
@@ -1597,6 +1602,15 @@ main (int argc, char **argv)
     }
 #endif
 
+#ifndef ENABLE_MPTCP
+  if (opt.mptcp)
+    {
+      fprintf (stderr, _("MPTCP support not compiled in. "
+                         "Ignoring --mptcp flag.\n"));
+      opt.mptcp = false;
+    }
+#endif
+
   /* All user options have now been processed, so it's now safe to do
      interoption dependency checks. */
 
diff --git a/src/options.h b/src/options.h
index f9c38cde..f3c14674 100644
--- a/src/options.h
+++ b/src/options.h
@@ -182,6 +182,8 @@ struct options
 
   bool debug;                   /* Debugging on/off */
 
+  bool mptcp;                   /* MPTCP on/off */
+
 #ifdef USE_WATT32
   bool wdebug;                  /* Watt-32 tcp/ip debugging on/off */
 #endif
diff --git a/testenv/Makefile.am b/testenv/Makefile.am
index b34dcf3d..edf25ec4 100644
--- a/testenv/Makefile.am
+++ b/testenv/Makefile.am
@@ -54,6 +54,7 @@ DEFAULT_TESTS = \
   Test--https.py                                  \
   Test--https-crl.py                              \
   Test-missing-scheme-retval.py                   \
+  Test-mptcp-enabled.py                           \
   Test-O.py                                       \
   Test-pinnedpubkey-der-https.py                  \
   Test-pinnedpubkey-der-no-check-https.py         \
diff --git a/testenv/Test-mptcp-enabled.py b/testenv/Test-mptcp-enabled.py
new file mode 100755
index 00000000..b5bc10de
--- /dev/null
+++ b/testenv/Test-mptcp-enabled.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+from sys import exit
+from test.http_test import HTTPTest
+from test.base_test import HTTP, HTTPS
+from misc.wget_file import WgetFile
+
+"""
+    This is a Prototype Test File.
+    Ideally this File should be copied and edited to write new tests.
+"""
+############# File Definitions ###############################################
+File1 = "Would you like some Tea?"
+
+A_File = WgetFile ("File1", File1)
+
+WGET_OPTIONS = "--mptcp --debug"    
+WGET_URLS = [["File1"]]
+
+Servers = [HTTP, HTTPS]
+
+Files = [[A_File]]
+Existing_Files = []
+
+ExpectedReturnCode = 0
+ExpectedDownloadedFiles = [A_File] 
+
+################ Pre and Post Test Hooks #####################################
+pre_test = {
+    "ServerFiles"       : Files,
+    "LocalFiles"        : Existing_Files
+}
+test_options = {
+    "WgetCommands"      : WGET_OPTIONS,
+    "Urls"              : WGET_URLS
+}
+post_test = {
+    "ExpectedFiles"     : ExpectedDownloadedFiles,
+    "ExpectedRetcode"   : ExpectedReturnCode,
+    "MPTCPEnabled"      : 0 
+}
+
+err = HTTPTest (
+                pre_hook=pre_test,
+                test_params=test_options,
+                post_hook=post_test,
+                protocols=Servers
+).begin ()
+
+exit (err)
diff --git a/testenv/conf/was_mptcp_enabled.py b/testenv/conf/was_mptcp_enabled.py
new file mode 100644
index 00000000..c8483249
--- /dev/null
+++ b/testenv/conf/was_mptcp_enabled.py
@@ -0,0 +1,79 @@
+from conf import hook
+import socket
+import errno
+import os
+from exc.test_failed import TestFailed
+
+try:
+    IPPROTO_MPTCP = socket.IPPROTO_MPTCP
+except AttributeError:
+    IPPROTO_MPTCP = 262
+
+
+try:
+    SOL_MPTCP = socket.SOL_MPTCP
+except AttributeError:
+    SOL_MPTCP = 284
+
+try: 
+    MPTCP_INFO = socket.MPTCP_INFO
+except AttributeError:
+    MPTCP_INFO = 1
+
+try: 
+    MPTCP_TCPINFO = socket.MPTCP_TCPINFO
+except AttributeError:
+    MPTCP_TCPINFO = 2
+
+try:
+    import ctypes
+
+    # distinct socklen_t type
+    class socklen_t(ctypes.c_uint32):
+        pass
+
+    class mptcp_subflow_data(ctypes.Structure):
+        _fields_ = (
+            ('size_subflow_data', ctypes.c_uint32), # this structure size
+            ('num_subflows', ctypes.c_uint32),      # must be 0
+            ('size_kernel', ctypes.c_uint32),       # must be 0
+            ('size_user', ctypes.c_uint32),         # element size in data[]
+        )
+
+        def __init__(self, size_user=0):
+            super().__init__(size_user=size_user)
+            self.size_subflow_data = ctypes.sizeof(self)
+
+    getsockopt = ctypes.CDLL(None, use_errno=True).getsockopt
+    getsockopt.restype = ctypes.c_int
+    getsockopt.argtypes = (
+        ctypes.c_int,      # sockfd
+        ctypes.c_int,      # level
+        ctypes.c_int,      # optname
+        ctypes.c_void_p,   # optval
+        ctypes.POINTER(socklen_t), # optlen
+    )
+
+except (ImportError, TypeError, AttributeError):
+    getsockopt = None
+
+@hook()
+class MPTCPEnabled:
+    def __init__(self, hook_arg):
+        self.hook_arg = hook_arg
+
+    def __call__(self, test_obj):
+        for serv in test_obj.servers:
+            sock = serv.server_inst.socket
+
+            # cfr. https://github.com/mptcp-apps/mptcplib/blob/main/mptcplib/_mptcplib_linux.py
+            if getsockopt == None:
+                raise TestFailed("The getsockopt operation is not supported on Host OS.")
+            optval = ctypes.c_bool()
+            optlen = socklen_t(ctypes.sizeof(ctypes.c_bool))
+            if getsockopt(  sock.fileno(), SOL_MPTCP, MPTCP_INFO, 
+                            ctypes.byref(optval), ctypes.byref(optlen) ) == -1:
+                err_no = ctypes.get_errno()
+                if err_no == errno.EOPNOTSUPP:
+                    raise TestFailed("MPTCP not supported")
+                raise TestFailed(OSError( err_no, os.strerror(err_no) ).strerror)
\ No newline at end of file
diff --git a/testenv/server/http/http_server.py b/testenv/server/http/http_server.py
index 2cc82fb9..116d3553 100644
--- a/testenv/server/http/http_server.py
+++ b/testenv/server/http/http_server.py
@@ -9,6 +9,10 @@ import threading
 import socket
 import os
 
+try:
+    IPPROTO_MPTCP = socket.IPPROTO_MPTCP
+except AttributeError:
+    IPPROTO_MPTCP = 262
 
 class StoppableHTTPServer(HTTPServer):
     """ This class extends the HTTPServer class from default http.server library
@@ -19,6 +23,18 @@ class StoppableHTTPServer(HTTPServer):
 
     request_headers = list()
 
+    def __init__(self, address, handler):
+        
+        BaseServer.__init__(self, address, handler)
+        
+        try:
+            self.socket = socket.socket(self.address_family, self.socket_type, IPPROTO_MPTCP)
+        except OSError:
+            self.socket = socket.socket(self.address_family, self.socket_type)
+
+        self.server_bind()
+        self.server_activate()
+
     """ Define methods for configuring the Server. """
 
     def server_conf(self, filelist, conf_dict):
@@ -47,8 +63,14 @@ class HTTPSServer(StoppableHTTPServer):
                                                os.getenv('srcdir', '.'),
                                                'certs',
                                                'server-key.pem'))
+        
+        try:
+            sock = socket.socket(self.address_family, self.socket_type, IPPROTO_MPTCP)
+        except OSError:
+            sock = socket.socket(self.address_family, self.socket_type)
+
         self.socket = ssl.wrap_socket(
-            sock=socket.socket(self.address_family, self.socket_type),
+            sock = sock,
             certfile=CERTFILE,
             keyfile=KEYFILE,
             server_side=True

Reply via email to