Skip to content

Commit

Permalink
Add an option to enable Multipath TCP on clients (#766)
Browse files Browse the repository at this point in the history
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.

---------

Co-authored-by: Cory Benfield <[email protected]>
  • Loading branch information
Aperence and Lukasa authored Sep 20, 2024
1 parent 10bd49c commit 15dbe6d
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<Void> in
do {
Expand Down
5 changes: 5 additions & 0 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -755,6 +759,7 @@ public class HTTPClient {
self.decompression = decompression
self.httpVersion = .automatic
self.networkFrameworkWaitForConnectivity = true
self.enableMultipath = false
}

public init(tlsConfiguration: TLSConfiguration? = nil,
Expand Down
18 changes: 18 additions & 0 deletions Tests/AsyncHTTPClientTests/HTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3590,6 +3590,24 @@ 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 || 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()
}
}

func testSingletonClientWorks() throws {
let response = try HTTPClient.shared.get(url: self.defaultHTTPBinURLPrefix + "get").wait()
XCTAssertEqual(.ok, response.status)
Expand Down

0 comments on commit 15dbe6d

Please sign in to comment.