From 068512c9f030cb0224109697d0ae579167bdd92f Mon Sep 17 00:00:00 2001 From: Aperence Date: Fri, 23 Aug 2024 10:01:03 +0200 Subject: [PATCH 1/3] Add an option to enable Multipath TCP on clients Multipath TCP (MPTCP) is a TCP extension allowing to enhance the reliability of the network by using multiple interfaces. This extension provides a seamless handover between interfaces in case of deterioration of the connection on the original one. In the context of iOS and Mac OS X, it could be really interesting to leverage the capabilities of MPTCP as they could benefit from their multiple interfaces (ethernet + Wi-fi for Mac OS X, Wi-fi + cellular for iOS). This contribution introduces patches to HTTPClient.Configuration and establishment of the Bootstraps. A supplementary field "enableMultipath" was added to the configuration, allowing to request the use of MPTCP. This flag is then used when creating the channels to configure the client. Note that in the future, it might also be potentially interesting to offer more precise configuration options for MPTCP on MacOS, as the Network framework allows also to select a type of service, instead of just offering the option to create MPTCP connections. Currently, when enabling MPTCP, only the Handover mode is used. --- .../HTTPConnectionPool+Factory.swift | 4 ++++ Sources/AsyncHTTPClient/HTTPClient.swift | 5 +++++ Tests/AsyncHTTPClientTests/HTTPClientTests.swift | 16 ++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift index 1461a6620..3a0011d5e 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift @@ -322,6 +322,7 @@ extension HTTPConnectionPool.ConnectionFactory { if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop) { return tsBootstrap .channelOption(NIOTSChannelOptions.waitForActivity, value: self.clientConfiguration.networkFrameworkWaitForConnectivity) + .channelOption(NIOTSChannelOptions.multipathServiceType, value: self.clientConfiguration.enableMultipath ? .handover : .disabled) .connectTimeout(deadline - NIODeadline.now()) .channelInitializer { channel in do { @@ -338,6 +339,7 @@ extension HTTPConnectionPool.ConnectionFactory { if let nioBootstrap = ClientBootstrap(validatingGroup: eventLoop) { return nioBootstrap .connectTimeout(deadline - NIODeadline.now()) + .enableMPTCP(clientConfiguration.enableMultipath) } preconditionFailure("No matching bootstrap found") @@ -415,6 +417,7 @@ extension HTTPConnectionPool.ConnectionFactory { tsBootstrap .channelOption(NIOTSChannelOptions.waitForActivity, value: self.clientConfiguration.networkFrameworkWaitForConnectivity) + .channelOption(NIOTSChannelOptions.multipathServiceType, value: self.clientConfiguration.enableMultipath ? .handover : .disabled) .connectTimeout(deadline - NIODeadline.now()) .tlsOptions(options) .channelInitializer { channel in @@ -443,6 +446,7 @@ extension HTTPConnectionPool.ConnectionFactory { let bootstrap = ClientBootstrap(group: eventLoop) .connectTimeout(deadline - NIODeadline.now()) + .enableMPTCP(clientConfiguration.enableMultipath) .channelInitializer { channel in sslContextFuture.flatMap { sslContext -> EventLoopFuture in do { diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index c52263318..096fb9387 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -738,6 +738,10 @@ public class HTTPClient { } } + /// Whether ``HTTPClient`` will use Multipath TCP or not + /// By default, don't use it + public var enableMultipath: Bool + public init( tlsConfiguration: TLSConfiguration? = nil, redirectConfiguration: RedirectConfiguration? = nil, @@ -755,6 +759,7 @@ public class HTTPClient { self.decompression = decompression self.httpVersion = .automatic self.networkFrameworkWaitForConnectivity = true + self.enableMultipath = false } public init(tlsConfiguration: TLSConfiguration? = nil, diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index ac0aee068..f5dc7b98c 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -3590,6 +3590,22 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass { XCTAssertEqual(.ok, response.status) } + func testClientWithMultipath() throws { + do { + var conf = HTTPClient.Configuration() + conf.enableMultipath = true + let client = HTTPClient(configuration: conf) + defer { + XCTAssertNoThrow(try client.shutdown().wait()) + } + let response = try client.get(url: self.defaultHTTPBinURLPrefix + "get").wait() + XCTAssertEqual(.ok, response.status) + }catch let error as IOError where error.errnoCode == EINVAL{ + // some old Linux kernels don't support MPTCP, skip this test in this case + throw XCTSkip() + } + } + func testSingletonClientWorks() throws { let response = try HTTPClient.shared.get(url: self.defaultHTTPBinURLPrefix + "get").wait() XCTAssertEqual(.ok, response.status) From a74dea1c750e2e5a2998f8cb2527a4caaa1ce19e Mon Sep 17 00:00:00 2001 From: Anthony Doeraene <78789735+Aperence@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:36:59 +0200 Subject: [PATCH 2/3] Update Tests/AsyncHTTPClientTests/HTTPClientTests.swift Co-authored-by: Cory Benfield --- Tests/AsyncHTTPClientTests/HTTPClientTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index f5dc7b98c..b54077290 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -3600,7 +3600,7 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass { } let response = try client.get(url: self.defaultHTTPBinURLPrefix + "get").wait() XCTAssertEqual(.ok, response.status) - }catch let error as IOError where error.errnoCode == EINVAL{ + } catch let error as IOError where error.errnoCode == EINVAL { // some old Linux kernels don't support MPTCP, skip this test in this case throw XCTSkip() } From 6f0c01bf095c844756cad2f450d71c901c7ccdde Mon Sep 17 00:00:00 2001 From: Aperence Date: Wed, 18 Sep 2024 20:35:29 +0200 Subject: [PATCH 3/3] Added checks for other type of errors --- Tests/AsyncHTTPClientTests/HTTPClientTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index b54077290..0c259c1bd 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -3600,8 +3600,10 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass { } let response = try client.get(url: self.defaultHTTPBinURLPrefix + "get").wait() XCTAssertEqual(.ok, response.status) - } catch let error as IOError where error.errnoCode == EINVAL { + } catch let error as IOError where error.errnoCode == EINVAL || error.errnoCode == EPROTONOSUPPORT || error.errnoCode == ENOPROTOOPT { // some old Linux kernels don't support MPTCP, skip this test in this case + // see https://www.mptcp.dev/implementation.html for details about each type + // of error throw XCTSkip() } }