Index: configure.ac ================================================================== --- configure.ac +++ configure.ac @@ -1488,10 +1488,11 @@ ]) AC_CHECK_HEADER(netinet/tcp.h, [ AC_DEFINE(OF_HAVE_NETINET_TCP_H, 1, [Whether we have netinet/tcp.h]) ]) + AC_CHECK_HEADERS(linux/mptcp.h) AC_CHECK_HEADER(netinet/sctp.h, [ AC_DEFINE(OF_HAVE_SCTP, 1, [Whether we have SCTP]) AC_DEFINE(OF_HAVE_NETINET_SCTP_H, 1, [Whether we have netinet/sctp.h]) AC_SUBST(USE_SRCS_SCTP, '${SRCS_SCTP}') Index: src/OFHTTPClient.m ================================================================== --- src/OFHTTPClient.m +++ src/OFHTTPClient.m @@ -730,10 +730,11 @@ OFNumber *IRIPort; [_client close]; sock = [OFTCPSocket socket]; + sock.usesMPTCP = true; if ([IRI.scheme caseInsensitiveCompare: @"https"] == OFOrderedSame) port = 443; else Index: src/OFHTTPServer.m ================================================================== --- src/OFHTTPServer.m +++ src/OFHTTPServer.m @@ -864,10 +864,11 @@ if (_listeningSocket != nil) @throw [OFAlreadyOpenException exceptionWithObject: self]; _listeningSocket = [[OFTCPSocket alloc] init]; + _listeningSocket.usesMPTCP = true; address = [_listeningSocket bindToHost: _host port: _port]; _port = OFSocketAddressIPPort(&address); [_listeningSocket listen]; #ifdef OF_HAVE_THREADS Index: src/OFTCPSocket.h ================================================================== --- src/OFTCPSocket.h +++ src/OFTCPSocket.h @@ -87,11 +87,12 @@ OFString *_Nullable _SOCKS5Host; uint16_t _SOCKS5Port; #ifdef OF_WII uint16_t _port; #endif - OF_RESERVE_IVARS(OFTCPSocket, 4) + uintptr_t _flags; /* Change to a smaller type on ABI bump */ + OF_RESERVE_IVARS(OFTCPSocket, 3) } #ifdef OF_HAVE_CLASS_PROPERTIES @property (class, nullable, copy, nonatomic) OFString *SOCKS5Host; @property (class, nonatomic) uint16_t SOCKS5Port; @@ -120,10 +121,21 @@ * @throw OFSetOptionFailedException The option could not be set */ @property (nonatomic) bool canDelaySendingSegments; #endif +/** + * @brief Whether the socket uses MPTCP. + * + * If you want to use MPTCP, set this to true before connecting or binding. + * After connecting or binding, this returns whether MPTCP was used. + * + * @note After connecting, this method may return `false` even when MPTCP was + * used. This is an OS limitation. + */ +@property (nonatomic) bool usesMPTCP; + /** * @brief The host to use as a SOCKS5 proxy. */ @property OF_NULLABLE_PROPERTY (copy, nonatomic) OFString *SOCKS5Host; Index: src/OFTCPSocket.m ================================================================== --- src/OFTCPSocket.m +++ src/OFTCPSocket.m @@ -50,16 +50,52 @@ #import "OFBindIPSocketFailedException.h" #import "OFGetOptionFailedException.h" #import "OFNotImplementedException.h" #import "OFNotOpenException.h" #import "OFSetOptionFailedException.h" + +#ifdef HAVE_LINUX_MPTCP_H +# include +#endif + +#if defined(OF_MACOS) || defined(OF_IOS) +# ifndef AF_MULTIPATH +# define AF_MULTIPATH 39 +# endif +#endif + +enum { + flagUseMPTCP = 1, + flagMapIPv4 = 2, + flagUseConnectX = 4 +}; static const OFRunLoopMode connectRunLoopMode = @"OFTCPSocketConnectRunLoopMode"; static OFString *defaultSOCKS5Host = nil; static uint16_t defaultSOCKS5Port = 1080; + +#if defined(OF_LINUX) && defined(IPPROTO_MPTCP) +static OFSocketAddress +mapIPv4(const OFSocketAddress *IPv4Address) +{ + OFSocketAddress IPv6Address = { + .family = OFSocketAddressFamilyIPv6, + .length = sizeof(struct sockaddr_in6) + }; + + IPv6Address.sockaddr.in6.sin6_family = AF_INET6; + IPv6Address.sockaddr.in6.sin6_port = IPv4Address->sockaddr.in.sin_port; + memcpy(&IPv6Address.sockaddr.in6.sin6_addr.s6_addr[12], + &IPv4Address->sockaddr.in.sin_addr.s_addr, 4); + IPv6Address.sockaddr.in6.sin6_addr.s6_addr[10] = 0xFF; + IPv6Address.sockaddr.in6.sin6_addr.s6_addr[11] = 0xFF; + + return IPv6Address; +} +#endif @interface OFTCPSocket () @end @interface OFTCPSocketConnectDelegate: OFObject @@ -144,15 +180,44 @@ #endif if (_socket != OFInvalidSocketHandle) @throw [OFAlreadyOpenException exceptionWithObject: self]; - if ((_socket = socket( - ((struct sockaddr *)&address->sockaddr)->sa_family, - SOCK_STREAM | SOCK_CLOEXEC, 0)) == OFInvalidSocketHandle) { - *errNo = _OFSocketErrNo(); - return false; +#if defined(OF_LINUX) && defined(IPPROTO_MPTCP) + if (_flags & flagUseMPTCP) { + /* + * For MPTCP sockets, we always use AF_INET6, so that IPv4 and + * IPv6 can both be used for a single connection. + */ + _socket = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, + IPPROTO_MPTCP); + + if (_socket != OFInvalidSocketHandle && + address->family == OFSocketAddressFamilyIPv4) + _flags |= flagMapIPv4; + else + _flags &= ~flagMapIPv4; + } +#elif (defined(OF_MACOS) || defined(OF_IOS)) && defined(SAE_ASSOCID_ANY) + if (_flags & flagUseMPTCP) { + _socket = socket(AF_MULTIPATH, SOCK_STREAM | SOCK_CLOEXEC, + IPPROTO_TCP); + + if (_socket != OFInvalidSocketHandle) + _flags |= flagUseConnectX; + else + _flags &= ~flagUseConnectX; + } +#endif + + if (_socket == OFInvalidSocketHandle) { + if ((_socket = socket( + ((struct sockaddr *)&address->sockaddr)->sa_family, + SOCK_STREAM | SOCK_CLOEXEC, 0)) == OFInvalidSocketHandle) { + *errNo = _OFSocketErrNo(); + return false; + } } #if SOCK_CLOEXEC == 0 && defined(HAVE_FCNTL) && defined(FD_CLOEXEC) if ((flags = fcntl(_socket, F_GETFD, 0)) != -1) fcntl(_socket, F_SETFD, flags | FD_CLOEXEC); @@ -162,19 +227,52 @@ } - (bool)of_connectSocketToAddress: (const OFSocketAddress *)address errNo: (int *)errNo { - if (_socket == OFInvalidSocketHandle) +#if defined(OF_LINUX) && defined(IPPROTO_MPTCP) + OFSocketAddress mappedIPv4; +#endif + + if (_socket == OFInvalidSocketHandle) { @throw [OFNotOpenException exceptionWithObject: self]; + } - /* Cast needed for AmigaOS, where the argument is declared non-const */ - if (connect(_socket, (struct sockaddr *)&address->sockaddr, - address->length) != 0) { - *errNo = _OFSocketErrNo(); - return false; +#if defined(OF_LINUX) && defined(IPPROTO_MPTCP) + if (_flags & flagMapIPv4) { + /* + * For MPTCP sockets, we always use AF_INET6, so that IPv4 and + * IPv6 can both be used for a single connection. + */ + mappedIPv4 = mapIPv4(address); + address = &mappedIPv4; } +#endif + +#if (defined(OF_MACOS) || defined(OF_IOS)) && defined(SAE_ASSOCID_ANY) + if (_flags & flagUseConnectX) { + sa_endpoints_t endpoints = { + .sae_dstaddr = (struct sockaddr *)&address->sockaddr, + .sae_dstaddrlen = address->length + }; + + if (connectx(_socket, &endpoints, SAE_ASSOCID_ANY, 0, NULL, 0, + NULL, NULL) != 0) { + *errNo = _OFSocketErrNo(); + return false; + } + } else +#endif + /* + * Cast needed for AmigaOS, where the argument is declared + * non-const. + */ + if (connect(_socket, (struct sockaddr *)&address->sockaddr, + address->length) != 0) { + *errNo = _OFSocketErrNo(); + return false; + } return true; } - (void)of_closeSocket @@ -346,18 +444,34 @@ addressFamily: OFSocketAddressFamilyAny]; address = *(OFSocketAddress *)[socketAddresses itemAtIndex: 0]; OFSocketAddressSetIPPort(&address, port); - if ((_socket = socket( - ((struct sockaddr *)&address.sockaddr)->sa_family, - SOCK_STREAM | SOCK_CLOEXEC, 0)) == OFInvalidSocketHandle) - @throw [OFBindIPSocketFailedException - exceptionWithHost: host - port: port - socket: self - errNo: _OFSocketErrNo()]; +#if defined(OF_LINUX) && defined(IPPROTO_MPTCP) + if (_flags & flagUseMPTCP) { + /* + * For MPTCP sockets, we always use AF_INET6, so that IPv4 and + * IPv6 can both be used for a single connection. + */ + _socket = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, + IPPROTO_MPTCP); + + if (_socket != OFInvalidSocketHandle && + address.family == OFSocketAddressFamilyIPv4) + address = mapIPv4(&address); + } +#endif + + if (_socket == OFInvalidSocketHandle) + if ((_socket = socket( + ((struct sockaddr *)&address.sockaddr)->sa_family, + SOCK_STREAM | SOCK_CLOEXEC, 0)) == OFInvalidSocketHandle) + @throw [OFBindIPSocketFailedException + exceptionWithHost: host + port: port + socket: self + errNo: _OFSocketErrNo()]; _canBlock = true; #if SOCK_CLOEXEC == 0 && defined(HAVE_FCNTL) && defined(FD_CLOEXEC) if ((flags = fcntl(_socket, F_GETFD, 0)) != -1) @@ -509,10 +623,31 @@ errNo: _OFSocketErrNo()]; return !v; } #endif + +- (void)setUsesMPTCP: (bool)usesMPTCP +{ + if (usesMPTCP) + _flags |= flagUseMPTCP; + else + _flags &= ~flagUseMPTCP; +} + +- (bool)usesMPTCP +{ +#if defined(OF_LINUX) && defined(SOL_MPTCP) && defined(MPTCP_INFO) + struct mptcp_info info; + socklen_t infoLen = (socklen_t)sizeof(info); + + if (getsockopt(_socket, SOL_MPTCP, MPTCP_INFO, &info, &infoLen) != -1) + return true; +#endif + + return false; +} - (void)close { #ifdef OF_WII _port = 0; Index: tests/OFHTTPClientTests.m ================================================================== --- tests/OFHTTPClientTests.m +++ tests/OFHTTPClientTests.m @@ -148,10 +148,11 @@ char buffer[5]; [_condition lock]; listener = [OFTCPSocket socket]; + listener.usesMPTCP = true; address = [listener bindToHost: @"127.0.0.1" port: 0]; _port = OFSocketAddressIPPort(&address); [listener listen]; [_condition signal];