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