Add `NWParameters` configurator to bootstraps (#230)

Allow users to provide a closure taking an `NWParameters` to customise
them before they're used to create `NWConnection`s.

### Motivation:

`NWParameters` are currently created using the provided TLS and UDP
options, and then passed over to new `NWConnection`s. However, there are
more ways in which `NWParameters` can be customised, so this new API
provides a way for users to do this.

### Modifications:

Introduce new `configureNWParameters` methods to the existing bootstraps
to allow configuring a closure for customising `NWParameters`.

### Result:

Users can now customise the `NWParameters` used to create new
`NWConnection`s.
This commit is contained in:
Gus Cairo 2025-04-16 14:33:56 +01:00 committed by GitHub
parent 3d21b85af4
commit cd1e89816d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 339 additions and 46 deletions

View File

@ -47,6 +47,7 @@ public final class NIOTSDatagramBootstrap {
private var qos: DispatchQoS? private var qos: DispatchQoS?
private var udpOptions: NWProtocolUDP.Options = .init() private var udpOptions: NWProtocolUDP.Options = .init()
private var tlsOptions: NWProtocolTLS.Options? private var tlsOptions: NWProtocolTLS.Options?
private var nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?
/// Create a `NIOTSDatagramConnectionBootstrap` on the `EventLoopGroup` `group`. /// Create a `NIOTSDatagramConnectionBootstrap` on the `EventLoopGroup` `group`.
/// ///
@ -133,6 +134,14 @@ public final class NIOTSDatagramBootstrap {
return self return self
} }
/// Customise the `NWParameters` to be used when creating the connection.
public func configureNWParameters(
_ configurator: @Sendable @escaping (NWParameters) -> Void
) -> Self {
self.nwParametersConfigurator = configurator
return self
}
/// Specify the `host` and `port` to connect to for the UDP `Channel` that will be established. /// Specify the `host` and `port` to connect to for the UDP `Channel` that will be established.
/// ///
/// - parameters: /// - parameters:
@ -188,7 +197,8 @@ public final class NIOTSDatagramBootstrap {
eventLoop: self.group.next() as! NIOTSEventLoop, eventLoop: self.group.next() as! NIOTSEventLoop,
qos: self.qos, qos: self.qos,
udpOptions: self.udpOptions, udpOptions: self.udpOptions,
tlsOptions: self.tlsOptions tlsOptions: self.tlsOptions,
nwParametersConfigurator: self.nwParametersConfigurator
) )
let initializer = self.channelInitializer ?? { @Sendable _ in conn.eventLoop.makeSucceededFuture(()) } let initializer = self.channelInitializer ?? { @Sendable _ in conn.eventLoop.makeSucceededFuture(()) }

View File

@ -140,8 +140,12 @@ internal final class NIOTSDatagramChannel: StateManagedNWConnectionChannel {
internal var allowLocalEndpointReuse = false internal var allowLocalEndpointReuse = false
internal var multipathServiceType: NWParameters.MultipathServiceType = .disabled internal var multipathServiceType: NWParameters.MultipathServiceType = .disabled
internal let nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?
var parameters: NWParameters { var parameters: NWParameters {
NWParameters(dtls: self.tlsOptions, udp: self.udpOptions) let parameters = NWParameters(dtls: self.tlsOptions, udp: self.udpOptions)
self.nwParametersConfigurator?(parameters)
return parameters
} }
var _inboundStreamOpen: Bool { var _inboundStreamOpen: Bool {
@ -182,7 +186,8 @@ internal final class NIOTSDatagramChannel: StateManagedNWConnectionChannel {
minimumIncompleteReceiveLength: Int = 1, minimumIncompleteReceiveLength: Int = 1,
maximumReceiveLength: Int = 8192, maximumReceiveLength: Int = 8192,
udpOptions: NWProtocolUDP.Options, udpOptions: NWProtocolUDP.Options,
tlsOptions: NWProtocolTLS.Options? tlsOptions: NWProtocolTLS.Options?,
nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?
) { ) {
self.tsEventLoop = eventLoop self.tsEventLoop = eventLoop
self.closePromise = eventLoop.makePromise() self.closePromise = eventLoop.makePromise()
@ -192,6 +197,7 @@ internal final class NIOTSDatagramChannel: StateManagedNWConnectionChannel {
self.connectionQueue = eventLoop.channelQueue(label: "nio.nioTransportServices.connectionchannel", qos: qos) self.connectionQueue = eventLoop.channelQueue(label: "nio.nioTransportServices.connectionchannel", qos: qos)
self.udpOptions = udpOptions self.udpOptions = udpOptions
self.tlsOptions = tlsOptions self.tlsOptions = tlsOptions
self.nwParametersConfigurator = nwParametersConfigurator
// Must come last, as it requires self to be completely initialized. // Must come last, as it requires self to be completely initialized.
self._pipeline = ChannelPipeline(channel: self) self._pipeline = ChannelPipeline(channel: self)
@ -206,7 +212,8 @@ internal final class NIOTSDatagramChannel: StateManagedNWConnectionChannel {
minimumIncompleteReceiveLength: Int = 1, minimumIncompleteReceiveLength: Int = 1,
maximumReceiveLength: Int = 8192, maximumReceiveLength: Int = 8192,
udpOptions: NWProtocolUDP.Options, udpOptions: NWProtocolUDP.Options,
tlsOptions: NWProtocolTLS.Options? tlsOptions: NWProtocolTLS.Options?,
nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?
) { ) {
self.init( self.init(
eventLoop: eventLoop, eventLoop: eventLoop,
@ -215,7 +222,8 @@ internal final class NIOTSDatagramChannel: StateManagedNWConnectionChannel {
minimumIncompleteReceiveLength: minimumIncompleteReceiveLength, minimumIncompleteReceiveLength: minimumIncompleteReceiveLength,
maximumReceiveLength: maximumReceiveLength, maximumReceiveLength: maximumReceiveLength,
udpOptions: udpOptions, udpOptions: udpOptions,
tlsOptions: tlsOptions tlsOptions: tlsOptions,
nwParametersConfigurator: nwParametersConfigurator
) )
self.connection = connection self.connection = connection
} }

View File

@ -64,8 +64,12 @@ public final class NIOTSDatagramListenerBootstrap {
private var serverQoS: DispatchQoS? private var serverQoS: DispatchQoS?
private var childQoS: DispatchQoS? private var childQoS: DispatchQoS?
private var udpOptions: NWProtocolUDP.Options = .init() private var udpOptions: NWProtocolUDP.Options = .init()
private var childUDPOptions: NWProtocolUDP.Options = .init()
private var tlsOptions: NWProtocolTLS.Options? private var tlsOptions: NWProtocolTLS.Options?
private var childTLSOptions: NWProtocolTLS.Options?
private var bindTimeout: TimeAmount? private var bindTimeout: TimeAmount?
private var nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?
private var childNWParametersConfigurator: (@Sendable (NWParameters) -> Void)?
/// Create a ``NIOTSListenerBootstrap`` for the `EventLoopGroup` `group`. /// Create a ``NIOTSListenerBootstrap`` for the `EventLoopGroup` `group`.
/// ///
@ -224,18 +228,46 @@ public final class NIOTSDatagramListenerBootstrap {
return self return self
} }
/// Specifies the TCP options to use on the child `Channel`s. /// Specifies the TCP options to use on the listener.
public func udpOptions(_ options: NWProtocolUDP.Options) -> Self { public func udpOptions(_ options: NWProtocolUDP.Options) -> Self {
self.udpOptions = options self.udpOptions = options
return self return self
} }
/// Specifies the TLS options to use on the child `Channel`s. /// Specifies the TCP options to use on the child `Channel`s.
public func childUDPOptions(_ options: NWProtocolUDP.Options) -> Self {
self.childUDPOptions = options
return self
}
/// Specifies the TLS options to use on the listener.
public func tlsOptions(_ options: NWProtocolTLS.Options) -> Self { public func tlsOptions(_ options: NWProtocolTLS.Options) -> Self {
self.tlsOptions = options self.tlsOptions = options
return self return self
} }
/// Specifies the TLS options to use on the child `Channel`s.
public func childTLSOptions(_ options: NWProtocolTLS.Options) -> Self {
self.childTLSOptions = options
return self
}
/// Customise the `NWParameters` to be used when creating the `NWConnection` for the listener.
public func configureNWParameters(
_ configurator: @Sendable @escaping (NWParameters) -> Void
) -> Self {
self.nwParametersConfigurator = configurator
return self
}
/// Customise the `NWParameters` to be used when creating the `NWConnection`s for the child `Channel`s.
public func configureChildNWParameters(
_ configurator: @Sendable @escaping (NWParameters) -> Void
) -> Self {
self.childNWParametersConfigurator = configurator
return self
}
/// Bind the `NIOTSListenerChannel` to `host` and `port`. /// Bind the `NIOTSListenerChannel` to `host` and `port`.
/// ///
/// - parameters: /// - parameters:
@ -327,10 +359,12 @@ public final class NIOTSDatagramListenerBootstrap {
qos: self.serverQoS, qos: self.serverQoS,
udpOptions: self.udpOptions, udpOptions: self.udpOptions,
tlsOptions: self.tlsOptions, tlsOptions: self.tlsOptions,
nwParametersConfigurator: self.nwParametersConfigurator,
childLoopGroup: self.childGroup, childLoopGroup: self.childGroup,
childChannelQoS: self.childQoS, childChannelQoS: self.childQoS,
childUDPOptions: self.udpOptions, childUDPOptions: self.childUDPOptions,
childTLSOptions: self.tlsOptions childTLSOptions: self.childTLSOptions,
childNWParametersConfigurator: self.childNWParametersConfigurator
) )
} else { } else {
serverChannel = NIOTSDatagramListenerChannel( serverChannel = NIOTSDatagramListenerChannel(
@ -338,10 +372,12 @@ public final class NIOTSDatagramListenerBootstrap {
qos: self.serverQoS, qos: self.serverQoS,
udpOptions: self.udpOptions, udpOptions: self.udpOptions,
tlsOptions: self.tlsOptions, tlsOptions: self.tlsOptions,
nwParametersConfigurator: self.nwParametersConfigurator,
childLoopGroup: self.childGroup, childLoopGroup: self.childGroup,
childChannelQoS: self.childQoS, childChannelQoS: self.childQoS,
childUDPOptions: self.udpOptions, childUDPOptions: self.childUDPOptions,
childTLSOptions: self.tlsOptions childTLSOptions: self.childTLSOptions,
childNWParametersConfigurator: self.childNWParametersConfigurator
) )
} }

View File

@ -81,19 +81,23 @@ internal final class NIOTSDatagramListenerChannel: StateManagedListenerChannel<N
qos: DispatchQoS? = nil, qos: DispatchQoS? = nil,
udpOptions: NWProtocolUDP.Options, udpOptions: NWProtocolUDP.Options,
tlsOptions: NWProtocolTLS.Options?, tlsOptions: NWProtocolTLS.Options?,
nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?,
childLoopGroup: EventLoopGroup, childLoopGroup: EventLoopGroup,
childChannelQoS: DispatchQoS?, childChannelQoS: DispatchQoS?,
childUDPOptions: NWProtocolUDP.Options, childUDPOptions: NWProtocolUDP.Options,
childTLSOptions: NWProtocolTLS.Options? childTLSOptions: NWProtocolTLS.Options?,
childNWParametersConfigurator: (@Sendable (NWParameters) -> Void)?
) { ) {
self.init( self.init(
eventLoop: eventLoop, eventLoop: eventLoop,
protocolOptions: .udp(udpOptions), protocolOptions: .udp(udpOptions),
tlsOptions: tlsOptions, tlsOptions: tlsOptions,
nwParametersConfigurator: nwParametersConfigurator,
childLoopGroup: childLoopGroup, childLoopGroup: childLoopGroup,
childChannelQoS: childChannelQoS, childChannelQoS: childChannelQoS,
childProtocolOptions: .udp(childUDPOptions), childProtocolOptions: .udp(childUDPOptions),
childTLSOptions: childTLSOptions childTLSOptions: childTLSOptions,
childNWParametersConfigurator: childNWParametersConfigurator
) )
} }
@ -104,20 +108,24 @@ internal final class NIOTSDatagramListenerChannel: StateManagedListenerChannel<N
qos: DispatchQoS? = nil, qos: DispatchQoS? = nil,
udpOptions: NWProtocolUDP.Options, udpOptions: NWProtocolUDP.Options,
tlsOptions: NWProtocolTLS.Options?, tlsOptions: NWProtocolTLS.Options?,
nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?,
childLoopGroup: EventLoopGroup, childLoopGroup: EventLoopGroup,
childChannelQoS: DispatchQoS?, childChannelQoS: DispatchQoS?,
childUDPOptions: NWProtocolUDP.Options, childUDPOptions: NWProtocolUDP.Options,
childTLSOptions: NWProtocolTLS.Options? childTLSOptions: NWProtocolTLS.Options?,
childNWParametersConfigurator: (@Sendable (NWParameters) -> Void)?
) { ) {
self.init( self.init(
wrapping: listener, wrapping: listener,
eventLoop: eventLoop, eventLoop: eventLoop,
protocolOptions: .udp(udpOptions), protocolOptions: .udp(udpOptions),
tlsOptions: tlsOptions, tlsOptions: tlsOptions,
nwParametersConfigurator: nwParametersConfigurator,
childLoopGroup: childLoopGroup, childLoopGroup: childLoopGroup,
childChannelQoS: childChannelQoS, childChannelQoS: childChannelQoS,
childProtocolOptions: .udp(childUDPOptions), childProtocolOptions: .udp(childUDPOptions),
childTLSOptions: childTLSOptions childTLSOptions: childTLSOptions,
childNWParametersConfigurator: childNWParametersConfigurator
) )
} }
@ -132,7 +140,8 @@ internal final class NIOTSDatagramListenerChannel: StateManagedListenerChannel<N
on: self.childLoopGroup.next() as! NIOTSEventLoop, on: self.childLoopGroup.next() as! NIOTSEventLoop,
parent: self, parent: self,
udpOptions: self.childUDPOptions, udpOptions: self.childUDPOptions,
tlsOptions: self.childTLSOptions tlsOptions: self.childTLSOptions,
nwParametersConfigurator: self.childNWParametersConfigurator
) )
self.pipeline.fireChannelRead(newChannel) self.pipeline.fireChannelRead(newChannel)

View File

@ -62,6 +62,7 @@ public final class NIOTSConnectionBootstrap {
private var tcpOptions: NWProtocolTCP.Options = .init() private var tcpOptions: NWProtocolTCP.Options = .init()
private var tlsOptions: NWProtocolTLS.Options? private var tlsOptions: NWProtocolTLS.Options?
private var protocolHandlers: (@Sendable () -> [ChannelHandler])? = nil private var protocolHandlers: (@Sendable () -> [ChannelHandler])? = nil
private var nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?
/// Create a `NIOTSConnectionBootstrap` on the `EventLoopGroup` `group`. /// Create a `NIOTSConnectionBootstrap` on the `EventLoopGroup` `group`.
/// ///
@ -165,6 +166,14 @@ public final class NIOTSConnectionBootstrap {
self.channelOption(NIOTSChannelOptions.multipathServiceType, value: type) self.channelOption(NIOTSChannelOptions.multipathServiceType, value: type)
} }
/// Customise the `NWParameters` to be used when creating the connection.
public func configureNWParameters(
_ configurator: @Sendable @escaping (NWParameters) -> Void
) -> Self {
self.nwParametersConfigurator = configurator
return self
}
/// Specify the `host` and `port` to connect to for the TCP `Channel` that will be established. /// Specify the `host` and `port` to connect to for the TCP `Channel` that will be established.
/// ///
/// - parameters: /// - parameters:
@ -243,14 +252,16 @@ public final class NIOTSConnectionBootstrap {
wrapping: newConnection, wrapping: newConnection,
on: self.group.next() as! NIOTSEventLoop, on: self.group.next() as! NIOTSEventLoop,
tcpOptions: self.tcpOptions, tcpOptions: self.tcpOptions,
tlsOptions: self.tlsOptions tlsOptions: self.tlsOptions,
nwParametersConfigurator: self.nwParametersConfigurator
) )
} else { } else {
conn = NIOTSConnectionChannel( conn = NIOTSConnectionChannel(
eventLoop: self.group.next() as! NIOTSEventLoop, eventLoop: self.group.next() as! NIOTSEventLoop,
qos: self.qos, qos: self.qos,
tcpOptions: self.tcpOptions, tcpOptions: self.tcpOptions,
tlsOptions: self.tlsOptions tlsOptions: self.tlsOptions,
nwParametersConfigurator: self.nwParametersConfigurator
) )
} }
let initializer = self.channelInitializer let initializer = self.channelInitializer
@ -437,14 +448,16 @@ extension NIOTSConnectionBootstrap {
wrapping: newConnection, wrapping: newConnection,
on: self.group.next() as! NIOTSEventLoop, on: self.group.next() as! NIOTSEventLoop,
tcpOptions: self.tcpOptions, tcpOptions: self.tcpOptions,
tlsOptions: self.tlsOptions tlsOptions: self.tlsOptions,
nwParametersConfigurator: self.nwParametersConfigurator
) )
} else { } else {
connectionChannel = NIOTSConnectionChannel( connectionChannel = NIOTSConnectionChannel(
eventLoop: self.group.next() as! NIOTSEventLoop, eventLoop: self.group.next() as! NIOTSEventLoop,
qos: self.qos, qos: self.qos,
tcpOptions: self.tcpOptions, tcpOptions: self.tcpOptions,
tlsOptions: self.tlsOptions tlsOptions: self.tlsOptions,
nwParametersConfigurator: self.nwParametersConfigurator
) )
} }
let initializer = self.channelInitializer let initializer = self.channelInitializer

View File

@ -164,8 +164,12 @@ internal final class NIOTSConnectionChannel: StateManagedNWConnectionChannel {
/// An `EventLoopPromise` that will be succeeded or failed when a connection attempt succeeds or fails. /// An `EventLoopPromise` that will be succeeded or failed when a connection attempt succeeds or fails.
internal var connectPromise: EventLoopPromise<Void>? internal var connectPromise: EventLoopPromise<Void>?
internal let nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?
internal var parameters: NWParameters { internal var parameters: NWParameters {
NWParameters(tls: self.tlsOptions, tcp: self.tcpOptions) let parameters = NWParameters(tls: self.tlsOptions, tcp: self.tcpOptions)
self.nwParametersConfigurator?(parameters)
return parameters
} }
/// The TCP options for this connection. /// The TCP options for this connection.
@ -242,7 +246,8 @@ internal final class NIOTSConnectionChannel: StateManagedNWConnectionChannel {
minimumIncompleteReceiveLength: Int = 1, minimumIncompleteReceiveLength: Int = 1,
maximumReceiveLength: Int = 8192, maximumReceiveLength: Int = 8192,
tcpOptions: NWProtocolTCP.Options, tcpOptions: NWProtocolTCP.Options,
tlsOptions: NWProtocolTLS.Options? tlsOptions: NWProtocolTLS.Options?,
nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?
) { ) {
self.tsEventLoop = eventLoop self.tsEventLoop = eventLoop
self.closePromise = eventLoop.makePromise() self.closePromise = eventLoop.makePromise()
@ -252,6 +257,7 @@ internal final class NIOTSConnectionChannel: StateManagedNWConnectionChannel {
self.connectionQueue = eventLoop.channelQueue(label: "nio.nioTransportServices.connectionchannel", qos: qos) self.connectionQueue = eventLoop.channelQueue(label: "nio.nioTransportServices.connectionchannel", qos: qos)
self.tcpOptions = tcpOptions self.tcpOptions = tcpOptions
self.tlsOptions = tlsOptions self.tlsOptions = tlsOptions
self.nwParametersConfigurator = nwParametersConfigurator
// Must come last, as it requires self to be completely initialized. // Must come last, as it requires self to be completely initialized.
self._pipeline = ChannelPipeline(channel: self) self._pipeline = ChannelPipeline(channel: self)
@ -266,7 +272,8 @@ internal final class NIOTSConnectionChannel: StateManagedNWConnectionChannel {
minimumIncompleteReceiveLength: Int = 1, minimumIncompleteReceiveLength: Int = 1,
maximumReceiveLength: Int = 8192, maximumReceiveLength: Int = 8192,
tcpOptions: NWProtocolTCP.Options, tcpOptions: NWProtocolTCP.Options,
tlsOptions: NWProtocolTLS.Options? tlsOptions: NWProtocolTLS.Options?,
nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?
) { ) {
self.init( self.init(
eventLoop: eventLoop, eventLoop: eventLoop,
@ -275,7 +282,8 @@ internal final class NIOTSConnectionChannel: StateManagedNWConnectionChannel {
minimumIncompleteReceiveLength: minimumIncompleteReceiveLength, minimumIncompleteReceiveLength: minimumIncompleteReceiveLength,
maximumReceiveLength: maximumReceiveLength, maximumReceiveLength: maximumReceiveLength,
tcpOptions: tcpOptions, tcpOptions: tcpOptions,
tlsOptions: tlsOptions tlsOptions: tlsOptions,
nwParametersConfigurator: nwParametersConfigurator
) )
self.connection = connection self.connection = connection
} }

View File

@ -64,8 +64,12 @@ public final class NIOTSListenerBootstrap {
private var serverQoS: DispatchQoS? private var serverQoS: DispatchQoS?
private var childQoS: DispatchQoS? private var childQoS: DispatchQoS?
private var tcpOptions: NWProtocolTCP.Options = .init() private var tcpOptions: NWProtocolTCP.Options = .init()
private var childTCPOptions: NWProtocolTCP.Options = .init()
private var tlsOptions: NWProtocolTLS.Options? private var tlsOptions: NWProtocolTLS.Options?
private var childTLSOptions: NWProtocolTLS.Options?
private var bindTimeout: TimeAmount? private var bindTimeout: TimeAmount?
private var nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?
private var childNWParametersConfigurator: (@Sendable (NWParameters) -> Void)?
/// Create a ``NIOTSListenerBootstrap`` for the `EventLoopGroup` `group`. /// Create a ``NIOTSListenerBootstrap`` for the `EventLoopGroup` `group`.
/// ///
@ -227,18 +231,46 @@ public final class NIOTSListenerBootstrap {
return self return self
} }
/// Specifies the TCP options to use on the child `Channel`s. /// Specifies the TCP options to use on the listener.
public func tcpOptions(_ options: NWProtocolTCP.Options) -> Self { public func tcpOptions(_ options: NWProtocolTCP.Options) -> Self {
self.tcpOptions = options self.tcpOptions = options
return self return self
} }
/// Specifies the TLS options to use on the child `Channel`s. /// Specifies the TCP options to use on the child `Channel`s.
public func childTCPOptions(_ options: NWProtocolTCP.Options) -> Self {
self.childTCPOptions = options
return self
}
/// Specifies the TLS options to use on the listener.
public func tlsOptions(_ options: NWProtocolTLS.Options) -> Self { public func tlsOptions(_ options: NWProtocolTLS.Options) -> Self {
self.tlsOptions = options self.tlsOptions = options
return self return self
} }
/// Specifies the TLS options to use on the child `Channel`s.
public func childTLSOptions(_ options: NWProtocolTLS.Options) -> Self {
self.childTLSOptions = options
return self
}
/// Customise the `NWParameters` to be used when creating the `NWConnection` for the listener.
public func configureNWParameters(
_ configurator: @Sendable @escaping (NWParameters) -> Void
) -> Self {
self.nwParametersConfigurator = configurator
return self
}
/// Customise the `NWParameters` to be used when creating the `NWConnection`s for the child `Channel`s.
public func configureChildNWParameters(
_ configurator: @Sendable @escaping (NWParameters) -> Void
) -> Self {
self.childNWParametersConfigurator = configurator
return self
}
/// Specifies a type of Multipath service to use for this listener, instead of the default /// Specifies a type of Multipath service to use for this listener, instead of the default
/// service type for the event loop. /// service type for the event loop.
/// ///
@ -337,10 +369,12 @@ public final class NIOTSListenerBootstrap {
qos: self.serverQoS, qos: self.serverQoS,
tcpOptions: self.tcpOptions, tcpOptions: self.tcpOptions,
tlsOptions: self.tlsOptions, tlsOptions: self.tlsOptions,
nwParametersConfigurator: self.nwParametersConfigurator,
childLoopGroup: self.childGroup, childLoopGroup: self.childGroup,
childChannelQoS: self.childQoS, childChannelQoS: self.childQoS,
childTCPOptions: self.tcpOptions, childTCPOptions: self.childTCPOptions,
childTLSOptions: self.tlsOptions childTLSOptions: self.childTLSOptions,
childNWParametersConfigurator: self.childNWParametersConfigurator
) )
} else { } else {
serverChannel = NIOTSListenerChannel( serverChannel = NIOTSListenerChannel(
@ -348,10 +382,12 @@ public final class NIOTSListenerBootstrap {
qos: self.serverQoS, qos: self.serverQoS,
tcpOptions: self.tcpOptions, tcpOptions: self.tcpOptions,
tlsOptions: self.tlsOptions, tlsOptions: self.tlsOptions,
nwParametersConfigurator: self.nwParametersConfigurator,
childLoopGroup: self.childGroup, childLoopGroup: self.childGroup,
childChannelQoS: self.childQoS, childChannelQoS: self.childQoS,
childTCPOptions: self.tcpOptions, childTCPOptions: self.childTCPOptions,
childTLSOptions: self.tlsOptions childTLSOptions: self.childTLSOptions,
childNWParametersConfigurator: self.childNWParametersConfigurator
) )
} }
@ -558,10 +594,12 @@ extension NIOTSListenerBootstrap {
qos: self.serverQoS, qos: self.serverQoS,
tcpOptions: self.tcpOptions, tcpOptions: self.tcpOptions,
tlsOptions: self.tlsOptions, tlsOptions: self.tlsOptions,
nwParametersConfigurator: self.nwParametersConfigurator,
childLoopGroup: self.childGroup, childLoopGroup: self.childGroup,
childChannelQoS: self.childQoS, childChannelQoS: self.childQoS,
childTCPOptions: self.tcpOptions, childTCPOptions: self.tcpOptions,
childTLSOptions: self.tlsOptions childTLSOptions: self.tlsOptions,
childNWParametersConfigurator: self.nwParametersConfigurator
) )
} else { } else {
serverChannel = NIOTSListenerChannel( serverChannel = NIOTSListenerChannel(
@ -569,10 +607,12 @@ extension NIOTSListenerBootstrap {
qos: self.serverQoS, qos: self.serverQoS,
tcpOptions: self.tcpOptions, tcpOptions: self.tcpOptions,
tlsOptions: self.tlsOptions, tlsOptions: self.tlsOptions,
nwParametersConfigurator: self.nwParametersConfigurator,
childLoopGroup: self.childGroup, childLoopGroup: self.childGroup,
childChannelQoS: self.childQoS, childChannelQoS: self.childQoS,
childTCPOptions: self.tcpOptions, childTCPOptions: self.tcpOptions,
childTLSOptions: self.tlsOptions childTLSOptions: self.tlsOptions,
childNWParametersConfigurator: self.nwParametersConfigurator
) )
} }

View File

@ -81,19 +81,23 @@ internal final class NIOTSListenerChannel: StateManagedListenerChannel<NIOTSConn
qos: DispatchQoS? = nil, qos: DispatchQoS? = nil,
tcpOptions: NWProtocolTCP.Options, tcpOptions: NWProtocolTCP.Options,
tlsOptions: NWProtocolTLS.Options?, tlsOptions: NWProtocolTLS.Options?,
nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?,
childLoopGroup: EventLoopGroup, childLoopGroup: EventLoopGroup,
childChannelQoS: DispatchQoS?, childChannelQoS: DispatchQoS?,
childTCPOptions: NWProtocolTCP.Options, childTCPOptions: NWProtocolTCP.Options,
childTLSOptions: NWProtocolTLS.Options? childTLSOptions: NWProtocolTLS.Options?,
childNWParametersConfigurator: (@Sendable (NWParameters) -> Void)?
) { ) {
self.init( self.init(
eventLoop: eventLoop, eventLoop: eventLoop,
protocolOptions: .tcp(tcpOptions), protocolOptions: .tcp(tcpOptions),
tlsOptions: tlsOptions, tlsOptions: tlsOptions,
nwParametersConfigurator: nwParametersConfigurator,
childLoopGroup: childLoopGroup, childLoopGroup: childLoopGroup,
childChannelQoS: childChannelQoS, childChannelQoS: childChannelQoS,
childProtocolOptions: .tcp(childTCPOptions), childProtocolOptions: .tcp(childTCPOptions),
childTLSOptions: childTLSOptions childTLSOptions: childTLSOptions,
childNWParametersConfigurator: childNWParametersConfigurator
) )
} }
@ -104,10 +108,12 @@ internal final class NIOTSListenerChannel: StateManagedListenerChannel<NIOTSConn
qos: DispatchQoS? = nil, qos: DispatchQoS? = nil,
tcpOptions: NWProtocolTCP.Options, tcpOptions: NWProtocolTCP.Options,
tlsOptions: NWProtocolTLS.Options?, tlsOptions: NWProtocolTLS.Options?,
nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?,
childLoopGroup: EventLoopGroup, childLoopGroup: EventLoopGroup,
childChannelQoS: DispatchQoS?, childChannelQoS: DispatchQoS?,
childTCPOptions: NWProtocolTCP.Options, childTCPOptions: NWProtocolTCP.Options,
childTLSOptions: NWProtocolTLS.Options? childTLSOptions: NWProtocolTLS.Options?,
childNWParametersConfigurator: (@Sendable (NWParameters) -> Void)?
) { ) {
self.init( self.init(
wrapping: listener, wrapping: listener,
@ -115,10 +121,12 @@ internal final class NIOTSListenerChannel: StateManagedListenerChannel<NIOTSConn
qos: qos, qos: qos,
protocolOptions: .tcp(tcpOptions), protocolOptions: .tcp(tcpOptions),
tlsOptions: tlsOptions, tlsOptions: tlsOptions,
nwParametersConfigurator: nwParametersConfigurator,
childLoopGroup: childLoopGroup, childLoopGroup: childLoopGroup,
childChannelQoS: childChannelQoS, childChannelQoS: childChannelQoS,
childProtocolOptions: .tcp(childTCPOptions), childProtocolOptions: .tcp(childTCPOptions),
childTLSOptions: childTLSOptions childTLSOptions: childTLSOptions,
childNWParametersConfigurator: childNWParametersConfigurator
) )
} }
@ -134,7 +142,8 @@ internal final class NIOTSListenerChannel: StateManagedListenerChannel<NIOTSConn
parent: self, parent: self,
qos: self.childChannelQoS, qos: self.childChannelQoS,
tcpOptions: self.childTCPOptions, tcpOptions: self.childTCPOptions,
tlsOptions: self.childTLSOptions tlsOptions: self.childTLSOptions,
nwParametersConfigurator: self.childNWParametersConfigurator
) )
self.pipeline.fireChannelRead(newChannel) self.pipeline.fireChannelRead(newChannel)

View File

@ -67,6 +67,9 @@ internal class StateManagedListenerChannel<ChildChannel: StateManagedChannel>: S
/// The TLS options for this listener. /// The TLS options for this listener.
internal let tlsOptions: NWProtocolTLS.Options? internal let tlsOptions: NWProtocolTLS.Options?
/// A customization point for this listener's `NWParameters`.
internal let nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?
/// The `DispatchQueue` that socket events for this connection will be dispatched onto. /// The `DispatchQueue` that socket events for this connection will be dispatched onto.
internal let connectionQueue: DispatchQueue internal let connectionQueue: DispatchQueue
@ -113,6 +116,9 @@ internal class StateManagedListenerChannel<ChildChannel: StateManagedChannel>: S
/// The TLS options to use for child channels. /// The TLS options to use for child channels.
internal let childTLSOptions: NWProtocolTLS.Options? internal let childTLSOptions: NWProtocolTLS.Options?
/// A customization point for each child's `NWParameters`.
internal let childNWParametersConfigurator: (@Sendable (NWParameters) -> Void)?
/// The cache of the local and remote socket addresses. Must be accessed using _addressCacheLock. /// The cache of the local and remote socket addresses. Must be accessed using _addressCacheLock.
internal var addressCache = AddressCache(local: nil, remote: nil) internal var addressCache = AddressCache(local: nil, remote: nil)
@ -130,20 +136,24 @@ internal class StateManagedListenerChannel<ChildChannel: StateManagedChannel>: S
qos: DispatchQoS? = nil, qos: DispatchQoS? = nil,
protocolOptions: ProtocolOptions, protocolOptions: ProtocolOptions,
tlsOptions: NWProtocolTLS.Options?, tlsOptions: NWProtocolTLS.Options?,
nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?,
childLoopGroup: EventLoopGroup, childLoopGroup: EventLoopGroup,
childChannelQoS: DispatchQoS?, childChannelQoS: DispatchQoS?,
childProtocolOptions: ProtocolOptions, childProtocolOptions: ProtocolOptions,
childTLSOptions: NWProtocolTLS.Options? childTLSOptions: NWProtocolTLS.Options?,
childNWParametersConfigurator: (@Sendable (NWParameters) -> Void)?
) { ) {
self.tsEventLoop = eventLoop self.tsEventLoop = eventLoop
self.closePromise = eventLoop.makePromise() self.closePromise = eventLoop.makePromise()
self.connectionQueue = eventLoop.channelQueue(label: "nio.transportservices.listenerchannel", qos: qos) self.connectionQueue = eventLoop.channelQueue(label: "nio.transportservices.listenerchannel", qos: qos)
self.protocolOptions = protocolOptions self.protocolOptions = protocolOptions
self.tlsOptions = tlsOptions self.tlsOptions = tlsOptions
self.nwParametersConfigurator = nwParametersConfigurator
self.childLoopGroup = childLoopGroup self.childLoopGroup = childLoopGroup
self.childChannelQoS = childChannelQoS self.childChannelQoS = childChannelQoS
self.childProtocolOptions = childProtocolOptions self.childProtocolOptions = childProtocolOptions
self.childTLSOptions = childTLSOptions self.childTLSOptions = childTLSOptions
self.childNWParametersConfigurator = childNWParametersConfigurator
// Must come last, as it requires self to be completely initialized. // Must come last, as it requires self to be completely initialized.
self._pipeline = ChannelPipeline(channel: self) self._pipeline = ChannelPipeline(channel: self)
@ -155,20 +165,24 @@ internal class StateManagedListenerChannel<ChildChannel: StateManagedChannel>: S
qos: DispatchQoS? = nil, qos: DispatchQoS? = nil,
protocolOptions: ProtocolOptions, protocolOptions: ProtocolOptions,
tlsOptions: NWProtocolTLS.Options?, tlsOptions: NWProtocolTLS.Options?,
nwParametersConfigurator: (@Sendable (NWParameters) -> Void)?,
childLoopGroup: EventLoopGroup, childLoopGroup: EventLoopGroup,
childChannelQoS: DispatchQoS?, childChannelQoS: DispatchQoS?,
childProtocolOptions: ProtocolOptions, childProtocolOptions: ProtocolOptions,
childTLSOptions: NWProtocolTLS.Options? childTLSOptions: NWProtocolTLS.Options?,
childNWParametersConfigurator: (@Sendable (NWParameters) -> Void)?
) { ) {
self.init( self.init(
eventLoop: eventLoop, eventLoop: eventLoop,
qos: qos, qos: qos,
protocolOptions: protocolOptions, protocolOptions: protocolOptions,
tlsOptions: tlsOptions, tlsOptions: tlsOptions,
nwParametersConfigurator: nwParametersConfigurator,
childLoopGroup: childLoopGroup, childLoopGroup: childLoopGroup,
childChannelQoS: childChannelQoS, childChannelQoS: childChannelQoS,
childProtocolOptions: childProtocolOptions, childProtocolOptions: childProtocolOptions,
childTLSOptions: childTLSOptions childTLSOptions: childTLSOptions,
childNWParametersConfigurator: childNWParametersConfigurator
) )
self.nwListener = listener self.nwListener = listener
} }
@ -398,6 +412,8 @@ extension StateManagedListenerChannel {
parameters.multipathServiceType = self.multipathServiceType parameters.multipathServiceType = self.multipathServiceType
self.nwParametersConfigurator?(parameters)
let listener: NWListener let listener: NWListener
do { do {
listener = try NWListener(using: parameters) listener = try NWListener(using: parameters)

View File

@ -83,6 +83,8 @@ internal protocol StateManagedNWConnectionChannel: StateManagedChannel where Act
var multipathServiceType: NWParameters.MultipathServiceType { get } var multipathServiceType: NWParameters.MultipathServiceType { get }
var nwParametersConfigurator: (@Sendable (NWParameters) -> Void)? { get }
func setChannelSpecificOption0<Option: ChannelOption>(option: Option, value: Option.Value) throws func setChannelSpecificOption0<Option: ChannelOption>(option: Option, value: Option.Value) throws
func getChannelSpecificOption0<Option: ChannelOption>(option: Option) throws -> Option.Value func getChannelSpecificOption0<Option: ChannelOption>(option: Option) throws -> Option.Value
@ -242,6 +244,7 @@ extension StateManagedNWConnectionChannel {
connection.betterPathUpdateHandler = self.betterPathHandler connection.betterPathUpdateHandler = self.betterPathHandler
connection.viabilityUpdateHandler = self.viabilityUpdateHandler connection.viabilityUpdateHandler = self.viabilityUpdateHandler
connection.pathUpdateHandler = self.pathChangedHandler(newPath:) connection.pathUpdateHandler = self.pathChangedHandler(newPath:)
self.nwParametersConfigurator?(connection.parameters)
connection.start(queue: self.connectionQueue) connection.start(queue: self.connectionQueue)
} }

View File

@ -371,6 +371,49 @@ final class NIOTSBootstrapTests: XCTestCase {
XCTAssertEqual(try listenerChannel.getOption(NIOTSChannelOptions.multipathServiceType).wait(), .handover) XCTAssertEqual(try listenerChannel.getOption(NIOTSChannelOptions.multipathServiceType).wait(), .handover)
XCTAssertEqual(try connectionChannel.getOption(NIOTSChannelOptions.multipathServiceType).wait(), .handover) XCTAssertEqual(try connectionChannel.getOption(NIOTSChannelOptions.multipathServiceType).wait(), .handover)
} }
func testNWParametersConfigurator() async throws {
try await withEventLoopGroup { group in
let configuratorServerListenerCounter = NIOLockedValueBox(0)
let configuratorServerConnectionCounter = NIOLockedValueBox(0)
let configuratorClientConnectionCounter = NIOLockedValueBox(0)
let waitForConnectionHandler = WaitForConnectionHandler(
connectionPromise: group.next().makePromise()
)
let listenerChannel = try await NIOTSListenerBootstrap(group: group)
.childChannelInitializer { connectionChannel in
connectionChannel.eventLoop.makeCompletedFuture {
try connectionChannel.pipeline.syncOperations.addHandler(waitForConnectionHandler)
}
}
.configureNWParameters { _ in
configuratorServerListenerCounter.withLockedValue { $0 += 1 }
}
.configureChildNWParameters { _ in
configuratorServerConnectionCounter.withLockedValue { $0 += 1 }
}
.bind(host: "localhost", port: 0)
.get()
let connectionChannel: Channel = try await NIOTSConnectionBootstrap(group: group)
.configureNWParameters { _ in
configuratorClientConnectionCounter.withLockedValue { $0 += 1 }
}
.connect(to: listenerChannel.localAddress!)
.get()
// Wait for the server to activate the connection channel to the client.
try await waitForConnectionHandler.connectionPromise.futureResult.get()
try await listenerChannel.close().get()
try await connectionChannel.close().get()
XCTAssertEqual(1, configuratorServerListenerCounter.withLockedValue { $0 })
XCTAssertEqual(1, configuratorServerConnectionCounter.withLockedValue { $0 })
XCTAssertEqual(1, configuratorClientConnectionCounter.withLockedValue { $0 })
}
}
} }
extension Channel { extension Channel {

View File

@ -37,13 +37,14 @@ final class NIOTSChannelMetadataTests: XCTestCase {
}.wait() }.wait()
} }
func testThowsIfCalledOnANonInitializedChannel() { func testThrowsIfCalledOnANonInitializedChannel() {
let eventLoopGroup = NIOTSEventLoopGroup() let eventLoopGroup = NIOTSEventLoopGroup()
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
let channel = NIOTSConnectionChannel( let channel = NIOTSConnectionChannel(
eventLoop: eventLoopGroup.next() as! NIOTSEventLoop, eventLoop: eventLoopGroup.next() as! NIOTSEventLoop,
tcpOptions: .init(), tcpOptions: .init(),
tlsOptions: .init() tlsOptions: .init(),
nwParametersConfigurator: nil
) )
XCTAssertThrowsError(try channel.getMetadata(definition: NWProtocolTLS.definition).wait()) { error in XCTAssertThrowsError(try channel.getMetadata(definition: NWProtocolTLS.definition).wait()) { error in
XCTAssertTrue(error is NIOTSConnectionNotInitialized, "unexpected error \(error)") XCTAssertTrue(error is NIOTSConnectionNotInitialized, "unexpected error \(error)")

View File

@ -272,11 +272,15 @@ class NIOTSConnectionChannelTests: XCTestCase {
} }
func testSettingTCPOptionsWholesale() throws { func testSettingTCPOptionsWholesale() throws {
let tcpOptions = NWProtocolTCP.Options() let listenerTCPOptions = NWProtocolTCP.Options()
tcpOptions.disableAckStretching = true listenerTCPOptions.disableAckStretching = true
let connectionTCPOptions = NWProtocolTCP.Options()
connectionTCPOptions.disableAckStretching = true
let listener = try NIOTSListenerBootstrap(group: self.group) let listener = try NIOTSListenerBootstrap(group: self.group)
.tcpOptions(tcpOptions) .tcpOptions(listenerTCPOptions)
.childTCPOptions(connectionTCPOptions)
.serverChannelInitializer { channel in .serverChannelInitializer { channel in
channel.getOption(ChannelOptions.socket(IPPROTO_TCP, TCP_SENDMOREACKS)).map { value in channel.getOption(ChannelOptions.socket(IPPROTO_TCP, TCP_SENDMOREACKS)).map { value in
XCTAssertEqual(value, 1) XCTAssertEqual(value, 1)
@ -293,7 +297,7 @@ class NIOTSConnectionChannelTests: XCTestCase {
} }
let connection = try NIOTSConnectionBootstrap(group: self.group) let connection = try NIOTSConnectionBootstrap(group: self.group)
.tcpOptions(tcpOptions) .tcpOptions(connectionTCPOptions)
.channelInitializer { channel in .channelInitializer { channel in
channel.getOption(ChannelOptions.socket(IPPROTO_TCP, TCP_SENDMOREACKS)).map { value in channel.getOption(ChannelOptions.socket(IPPROTO_TCP, TCP_SENDMOREACKS)).map { value in
XCTAssertEqual(value, 1) XCTAssertEqual(value, 1)

View File

@ -18,6 +18,7 @@ import Network
import NIOCore import NIOCore
import NIOTransportServices import NIOTransportServices
import Foundation import Foundation
import NIOConcurrencyHelpers
extension Channel { extension Channel {
func wait<T: Sendable>(for type: T.Type, count: Int) throws -> [T] { func wait<T: Sendable>(for type: T.Type, count: Int) throws -> [T] {
@ -232,6 +233,54 @@ final class NIOTSDatagramConnectionChannelTests: XCTestCase {
XCTAssertNoThrow(try connection.close().wait()) XCTAssertNoThrow(try connection.close().wait())
} }
func testNWParametersConfigurator() async throws {
try await withEventLoopGroup { group in
let configuratorServerListenerCounter = NIOLockedValueBox(0)
let configuratorServerConnectionCounter = NIOLockedValueBox(0)
let configuratorClientConnectionCounter = NIOLockedValueBox(0)
let waitForConnectionHandler = WaitForConnectionHandler(
connectionPromise: group.next().makePromise()
)
let listenerChannel = try await NIOTSDatagramListenerBootstrap(group: group)
.childChannelInitializer { connectionChannel in
connectionChannel.eventLoop.makeCompletedFuture {
try connectionChannel.pipeline.syncOperations.addHandler(waitForConnectionHandler)
}
}
.configureNWParameters { _ in
configuratorServerListenerCounter.withLockedValue { $0 += 1 }
}
.configureChildNWParameters { _ in
configuratorServerConnectionCounter.withLockedValue { $0 += 1 }
}
.bind(host: "localhost", port: 0)
.get()
let connectionChannel: Channel = try await NIOTSDatagramBootstrap(group: group)
.configureNWParameters { _ in
configuratorClientConnectionCounter.withLockedValue { $0 += 1 }
}
.connect(to: listenerChannel.localAddress!)
.get()
// Need to write something so the server can activate the connection channel: this is UDP,
// so there is no handshaking that happens and thus the server cannot know that the
// connection has been established and the channel can be activated until we receive something.
try await connectionChannel.writeAndFlush(ByteBuffer(bytes: [42]))
// Wait for the server to activate the connection channel to the client.
try await waitForConnectionHandler.connectionPromise.futureResult.get()
try await listenerChannel.close().get()
try await connectionChannel.close().get()
XCTAssertEqual(1, configuratorServerListenerCounter.withLockedValue { $0 })
XCTAssertEqual(1, configuratorServerConnectionCounter.withLockedValue { $0 })
XCTAssertEqual(1, configuratorClientConnectionCounter.withLockedValue { $0 })
}
}
func testCanExtractTheConnection() throws { func testCanExtractTheConnection() throws {
guard #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) else { guard #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) else {
throw XCTSkip("Option not available") throw XCTSkip("Option not available")

View File

@ -0,0 +1,44 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2025 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
#if canImport(Network)
import NIOCore
import NIOTransportServices
func withEventLoopGroup(_ test: (EventLoopGroup) async throws -> Void) async rethrows {
let group = NIOTSEventLoopGroup()
do {
try await test(group)
try? await group.shutdownGracefully()
} catch {
try? await group.shutdownGracefully()
throw error
}
}
final class WaitForConnectionHandler: ChannelInboundHandler, Sendable {
typealias InboundIn = Never
let connectionPromise: EventLoopPromise<Void>
init(connectionPromise: EventLoopPromise<Void>) {
self.connectionPromise = connectionPromise
}
func channelActive(context: ChannelHandlerContext) {
self.connectionPromise.succeed()
context.fireChannelActive()
}
}
#endif