Add support for SO_REUSEADDR and SO_REUSEPORT. (#1)

Motivation:

These are commonly-set socket options for channels, and we should
support them when using Transport Services as well.

Modifications:

Pass the socket options through to allowLocalEndpointReuse.

Result:

SO_REUSEADDR and SO_REUSEPORT will be available.
This commit is contained in:
Cory Benfield 2018-08-08 17:22:02 +01:00 committed by GitHub
parent 6475881aea
commit 955d1b91e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 125 additions and 4 deletions

View File

@ -189,6 +189,12 @@ internal final class NIOTSConnectionChannel {
/// An object to keep track of pending writes and manage our backpressure signaling.
private var backpressureManager = BackpressureManager()
/// The value of SO_REUSEADDR.
private var reuseAddress = false
/// The value of SO_REUSEPORT.
private var reusePort = false
/// Create a `NIOTSConnectionChannel` on a given `NIOTSEventLoop`.
///
/// Note that `NIOTSConnectionChannel` objects cannot be created on arbitrary loops types.
@ -284,7 +290,16 @@ extension NIOTSConnectionChannel: Channel {
self.options.supportRemoteHalfClosure = value as! Bool
case _ as SocketOption:
let optionValue = option as! SocketOption
try self.tcpOptions.applyChannelOption(option: optionValue, value: value as! SocketOptionValue)
// SO_REUSEADDR and SO_REUSEPORT are handled here.
switch optionValue.value {
case (SOL_SOCKET, SO_REUSEADDR):
self.reuseAddress = (value as! SocketOptionValue) != Int32(0)
case (SOL_SOCKET, SO_REUSEPORT):
self.reusePort = (value as! SocketOptionValue) != Int32(0)
default:
try self.tcpOptions.applyChannelOption(option: optionValue, value: value as! SocketOptionValue)
}
case _ as WriteBufferWaterMarkOption:
if self.backpressureManager.writabilityChanges(whenUpdatingWaterMarks: value as! WriteBufferWaterMark) {
self.pipeline.fireChannelWritabilityChanged()
@ -318,7 +333,16 @@ extension NIOTSConnectionChannel: Channel {
return self.options.supportRemoteHalfClosure as! T.OptionType
case _ as SocketOption:
let optionValue = option as! SocketOption
return try self.tcpOptions.valueFor(socketOption: optionValue) as! T.OptionType
// SO_REUSEADDR and SO_REUSEPORT are handled here.
switch optionValue.value {
case (SOL_SOCKET, SO_REUSEADDR):
return Int32(self.reuseAddress ? 1 : 0) as! T.OptionType
case (SOL_SOCKET, SO_REUSEPORT):
return Int32(self.reusePort ? 1 : 0) as! T.OptionType
default:
return try self.tcpOptions.valueFor(socketOption: optionValue) as! T.OptionType
}
case _ as WriteBufferWaterMarkOption:
return self.backpressureManager.waterMarks as! T.OptionType
default:
@ -396,6 +420,11 @@ extension NIOTSConnectionChannel: StateManagedChannel {
self.connectPromise = promise
let parameters = NWParameters(tls: self.tlsOptions, tcp: self.tcpOptions)
// Network.framework munges REUSEADDR and REUSEPORT together, so we turn this on if we need
// either.
parameters.allowLocalEndpointReuse = self.reuseAddress || self.reusePort
let connection = NWConnection(to: target, using: parameters)
connection.stateUpdateHandler = self.stateUpdateHandler(newState:)
connection.betterPathUpdateHandler = self.betterPathHandler

View File

@ -67,6 +67,12 @@ internal final class NIOTSListenerChannel {
/// Whether autoRead is enabled for this channel.
private var autoRead: Bool = true
/// The value of SO_REUSEADDR.
private var reuseAddress = false
/// The value of SO_REUSEPORT.
private var reusePort = false
/// Create a `NIOTSListenerChannel` on a given `NIOTSEventLoop`.
///
/// Note that `NIOTSListenerChannel` objects cannot be created on arbitrary loops types.
@ -148,7 +154,16 @@ extension NIOTSListenerChannel: Channel {
}
case _ as SocketOption:
let optionValue = option as! SocketOption
try self.tcpOptions.applyChannelOption(option: optionValue, value: value as! SocketOptionValue)
// SO_REUSEADDR and SO_REUSEPORT are handled here.
switch optionValue.value {
case (SOL_SOCKET, SO_REUSEADDR):
self.reuseAddress = (value as! SocketOptionValue) != Int32(0)
case (SOL_SOCKET, SO_REUSEPORT):
self.reusePort = (value as! SocketOptionValue) != Int32(0)
default:
try self.tcpOptions.applyChannelOption(option: optionValue, value: value as! SocketOptionValue)
}
default:
fatalError("option \(option) not supported")
}
@ -176,7 +191,16 @@ extension NIOTSListenerChannel: Channel {
return autoRead as! T.OptionType
case _ as SocketOption:
let optionValue = option as! SocketOption
return try self.tcpOptions.valueFor(socketOption: optionValue) as! T.OptionType
// SO_REUSEADDR and SO_REUSEPORT are handled here.
switch optionValue.value {
case (SOL_SOCKET, SO_REUSEADDR):
return Int32(self.reuseAddress ? 1 : 0) as! T.OptionType
case (SOL_SOCKET, SO_REUSEPORT):
return Int32(self.reusePort ? 1 : 0) as! T.OptionType
default:
return try self.tcpOptions.valueFor(socketOption: optionValue) as! T.OptionType
}
default:
fatalError("option \(option) not supported")
}
@ -247,6 +271,10 @@ extension NIOTSListenerChannel: StateManagedChannel {
parameters.requiredInterface = interface
}
// Network.framework munges REUSEADDR and REUSEPORT together, so we turn this on if we need
// either.
parameters.allowLocalEndpointReuse = self.reuseAddress || self.reusePort
let listener: NWListener
do {
listener = try NWListener(using: parameters)

View File

@ -425,4 +425,42 @@ class NIOTSConnectionChannelTests: XCTestCase {
XCTAssertTrue(connection.isWritable)
}.wait()
}
func testSettingGettingReuseaddr() throws {
let listener = try NIOTSListenerBootstrap(group: self.group).bind(host: "localhost", port: 0).wait()
defer {
XCTAssertNoThrow(try listener.close().wait())
}
let connection = try NIOTSConnectionBootstrap(group: self.group)
.connect(to: listener.localAddress!)
.wait()
defer {
XCTAssertNoThrow(try connection.close().wait())
}
XCTAssertEqual(0, try connection.getOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR)).wait())
XCTAssertNoThrow(try connection.setOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR), value: 5).wait())
XCTAssertEqual(1, try connection.getOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR)).wait())
XCTAssertNoThrow(try connection.setOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR), value: 0).wait())
XCTAssertEqual(0, try connection.getOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR)).wait())
}
func testSettingGettingReuseport() throws {
let listener = try NIOTSListenerBootstrap(group: self.group).bind(host: "localhost", port: 0).wait()
defer {
XCTAssertNoThrow(try listener.close().wait())
}
let connection = try NIOTSConnectionBootstrap(group: self.group)
.connect(to: listener.localAddress!)
.wait()
defer {
XCTAssertNoThrow(try connection.close().wait())
}
XCTAssertEqual(0, try connection.getOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEPORT)).wait())
XCTAssertNoThrow(try connection.setOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEPORT), value: 5).wait())
XCTAssertEqual(1, try connection.getOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEPORT)).wait())
XCTAssertNoThrow(try connection.setOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEPORT), value: 0).wait())
XCTAssertEqual(0, try connection.getOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEPORT)).wait())
}
}

View File

@ -112,4 +112,30 @@ class NIOTSListenerChannelTests: XCTestCase {
XCTAssertEqual(bindRecordingHandler.endpointTargets, [endpoint])
}.wait()
}
func testSettingGettingReuseaddr() throws {
let listener = try NIOTSListenerBootstrap(group: self.group).bind(host: "localhost", port: 0).wait()
defer {
XCTAssertNoThrow(try listener.close().wait())
}
XCTAssertEqual(0, try listener.getOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR)).wait())
XCTAssertNoThrow(try listener.setOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR), value: 5).wait())
XCTAssertEqual(1, try listener.getOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR)).wait())
XCTAssertNoThrow(try listener.setOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR), value: 0).wait())
XCTAssertEqual(0, try listener.getOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR)).wait())
}
func testSettingGettingReuseport() throws {
let listener = try NIOTSListenerBootstrap(group: self.group).bind(host: "localhost", port: 0).wait()
defer {
XCTAssertNoThrow(try listener.close().wait())
}
XCTAssertEqual(0, try listener.getOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEPORT)).wait())
XCTAssertNoThrow(try listener.setOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEPORT), value: 5).wait())
XCTAssertEqual(1, try listener.getOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEPORT)).wait())
XCTAssertNoThrow(try listener.setOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEPORT), value: 0).wait())
XCTAssertEqual(0, try listener.getOption(option: ChannelOptions.socket(SOL_SOCKET, SO_REUSEPORT)).wait())
}
}