Replace NIONetworkInterface with NIONetworkDevice. (#1622)

Motivation:

NIONetworkInterface was a useful type, but its implementation was fairly
fatally flawed in a number of ways. While we could have lived with the
awkward type construction (it's a class, which isn't the semantic we
want), the ultimate flaw is that you cannot create a NIONetworkInterface
for an interface that does not have an IP address.

That's not helpful, given that it's not uncommon for an interface to not
have an IP address!

Modifications:

- Deprecate NIONetworkInterface
- Add NIONetworkDevice
- Provide new methods on MulticastChannel that use NIONetworkDevice
  instead of NIONetworkInterface.
- Provide default implementations of new MulticastChannel protocol
  requirements.
- Implement new MulticastChannel protocol requirements.
- Update the tests.

Result:

Devices will be able to reflect network devices without IP addresses.
This commit is contained in:
Cory Benfield 2020-08-24 09:37:27 +01:00 committed by GitHub
parent 8d430f2d85
commit f9552beffa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 725 additions and 41 deletions

View File

@ -339,6 +339,7 @@ public enum ChannelError: Error {
case illegalMulticastAddress(SocketAddress)
/// Multicast is not supported on Interface
@available(*, deprecated, renamed: "NIOMulticastNotSupportedError")
case multicastNotSupported(NIONetworkInterface)
/// An operation that was inappropriate given the current `Channel` state was attempted.
@ -353,6 +354,20 @@ extension ChannelError: Equatable { }
/// The removal of a `ChannelHandler` using `ChannelPipeline.removeHandler` has been attempted more than once.
public struct NIOAttemptedToRemoveHandlerMultipleTimesError: Error {}
/// Multicast is not supported on this interface.
public struct NIOMulticastNotSupportedError: Error {
public var device: NIONetworkDevice
public init(device: NIONetworkDevice) {
self.device = device
}
}
/// Multicast has not been properly implemented on this channel.
public struct NIOMulticastNotImplementedError: Error {
public init() {}
}
/// An `Channel` related event that is passed through the `ChannelPipeline` to notify the user.
public enum ChannelEvent: Equatable {
/// `ChannelOptions.allowRemoteHalfClosure` is `true` and input portion of the `Channel` was closed.

View File

@ -38,7 +38,259 @@ private extension ifaddrs {
}
}
/// A representation of a single network device on a system.
public struct NIONetworkDevice {
private var backing: Backing
/// The name of the network device.
public var name: String {
get {
return self.backing.name
}
set {
self.uniquifyIfNeeded()
self.backing.name = newValue
}
}
/// The address associated with the given network device.
public var address: SocketAddress? {
get {
return self.backing.address
}
set {
self.uniquifyIfNeeded()
self.backing.address = newValue
}
}
/// The netmask associated with this address, if any.
public var netmask: SocketAddress? {
get {
return self.backing.netmask
}
set {
self.uniquifyIfNeeded()
self.backing.netmask = newValue
}
}
/// The broadcast address associated with this socket interface, if it has one. Some
/// interfaces do not, especially those that have a `pointToPointDestinationAddress`.
public var broadcastAddress: SocketAddress? {
get {
return self.backing.broadcastAddress
}
set {
self.uniquifyIfNeeded()
self.backing.broadcastAddress = newValue
}
}
/// The address of the peer on a point-to-point interface, if this is one. Some
/// interfaces do not have such an address: most of those have a `broadcastAddress`
/// instead.
public var pointToPointDestinationAddress: SocketAddress? {
get {
return self.backing.pointToPointDestinationAddress
}
set {
self.uniquifyIfNeeded()
self.backing.pointToPointDestinationAddress = newValue
}
}
/// If the Interface supports Multicast
public var multicastSupported: Bool {
get {
return self.backing.multicastSupported
}
set {
self.uniquifyIfNeeded()
self.backing.multicastSupported = newValue
}
}
/// The index of the interface, as provided by `if_nametoindex`.
public var interfaceIndex: Int {
get {
return self.backing.interfaceIndex
}
set {
self.uniquifyIfNeeded()
self.backing.interfaceIndex = newValue
}
}
/// Create a brand new network interface.
///
/// This constructor will fail if NIO does not understand the format of the underlying
/// socket address family. This is quite common: for example, Linux will return AF_PACKET
/// addressed interfaces on most platforms, which NIO does not currently understand.
internal init?(_ caddr: ifaddrs) {
guard let backing = Backing(caddr) else {
return nil
}
self.backing = backing
}
/// Convert a `NIONetworkInterface` to a `NIONetworkDevice`. As `NIONetworkDevice`s are a superset of `NIONetworkInterface`s,
/// it is always possible to perform this conversion.
@available(*, deprecated, message: "This is a compatibility helper, and will be removed in a future release")
public init(_ interface: NIONetworkInterface) {
self.backing = Backing(
name: interface.name,
address: interface.address,
netmask: interface.netmask,
broadcastAddress: interface.broadcastAddress,
pointToPointDestinationAddress: interface.pointToPointDestinationAddress,
multicastSupported: interface.multicastSupported,
interfaceIndex: interface.interfaceIndex
)
}
public init(name: String,
address: SocketAddress?,
netmask: SocketAddress?,
broadcastAddress: SocketAddress?,
pointToPointDestinationAddress: SocketAddress,
multicastSupported: Bool,
interfaceIndex: Int) {
self.backing = Backing(
name: name,
address: address,
netmask: netmask,
broadcastAddress: broadcastAddress,
pointToPointDestinationAddress: pointToPointDestinationAddress,
multicastSupported: multicastSupported,
interfaceIndex: interfaceIndex
)
}
private mutating func uniquifyIfNeeded() {
if !isKnownUniquelyReferenced(&self.backing) {
self.backing = Backing(copying: self.backing)
}
}
}
extension NIONetworkDevice {
fileprivate final class Backing {
/// The name of the network interface.
var name: String
/// The address associated with the given network interface.
var address: SocketAddress?
/// The netmask associated with this address, if any.
var netmask: SocketAddress?
/// The broadcast address associated with this socket interface, if it has one. Some
/// interfaces do not, especially those that have a `pointToPointDestinationAddress`.
var broadcastAddress: SocketAddress?
/// The address of the peer on a point-to-point interface, if this is one. Some
/// interfaces do not have such an address: most of those have a `broadcastAddress`
/// instead.
var pointToPointDestinationAddress: SocketAddress?
/// If the Interface supports Multicast
var multicastSupported: Bool
/// The index of the interface, as provided by `if_nametoindex`.
var interfaceIndex: Int
/// Create a brand new network interface.
///
/// This constructor will fail if NIO does not understand the format of the underlying
/// socket address family. This is quite common: for example, Linux will return AF_PACKET
/// addressed interfaces on most platforms, which NIO does not currently understand.
internal init?(_ caddr: ifaddrs) {
self.name = String(cString: caddr.ifa_name)
self.address = caddr.ifa_addr.flatMap { $0.convert() }
self.netmask = caddr.ifa_netmask.flatMap { $0.convert() }
if (caddr.ifa_flags & UInt32(IFF_BROADCAST)) != 0, let addr = caddr.broadaddr {
self.broadcastAddress = addr.convert()
self.pointToPointDestinationAddress = nil
} else if (caddr.ifa_flags & UInt32(IFF_POINTOPOINT)) != 0, let addr = caddr.dstaddr {
self.broadcastAddress = nil
self.pointToPointDestinationAddress = addr.convert()
} else {
self.broadcastAddress = nil
self.pointToPointDestinationAddress = nil
}
self.multicastSupported = (caddr.ifa_flags & UInt32(IFF_MULTICAST)) != 0
do {
self.interfaceIndex = Int(try Posix.if_nametoindex(caddr.ifa_name))
} catch {
return nil
}
}
init(copying original: Backing) {
self.name = original.name
self.address = original.address
self.netmask = original.netmask
self.broadcastAddress = original.broadcastAddress
self.pointToPointDestinationAddress = original.pointToPointDestinationAddress
self.multicastSupported = original.multicastSupported
self.interfaceIndex = original.interfaceIndex
}
init(name: String,
address: SocketAddress?,
netmask: SocketAddress?,
broadcastAddress: SocketAddress?,
pointToPointDestinationAddress: SocketAddress?,
multicastSupported: Bool,
interfaceIndex: Int) {
self.name = name
self.address = address
self.netmask = netmask
self.broadcastAddress = broadcastAddress
self.pointToPointDestinationAddress = pointToPointDestinationAddress
self.multicastSupported = multicastSupported
self.interfaceIndex = interfaceIndex
}
}
}
extension NIONetworkDevice: CustomDebugStringConvertible {
public var debugDescription: String {
let baseString = "Device \(self.name): address \(String(describing: self.address))"
let maskString = self.netmask != nil ? " netmask \(self.netmask!)" : ""
return baseString + maskString
}
}
// Sadly, as this is class-backed we cannot synthesise the implementation.
extension NIONetworkDevice: Equatable {
public static func ==(lhs: NIONetworkDevice, rhs: NIONetworkDevice) -> Bool {
return lhs.name == rhs.name &&
lhs.address == rhs.address &&
lhs.netmask == rhs.netmask &&
lhs.broadcastAddress == rhs.broadcastAddress &&
lhs.pointToPointDestinationAddress == rhs.pointToPointDestinationAddress &&
lhs.interfaceIndex == rhs.interfaceIndex
}
}
extension NIONetworkDevice: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(self.name)
hasher.combine(self.address)
hasher.combine(self.netmask)
hasher.combine(self.broadcastAddress)
hasher.combine(self.pointToPointDestinationAddress)
hasher.combine(self.interfaceIndex)
}
}
/// A representation of a single network interface on a system.
@available(*, deprecated, renamed: "NIONetworkDevice")
public final class NIONetworkInterface {
// This is a class because in almost all cases this will carry
// four structs that are backed by classes, and so will incur 4
@ -111,6 +363,7 @@ public final class NIONetworkInterface {
}
}
@available(*, deprecated, renamed: "NIONetworkDevice")
extension NIONetworkInterface: CustomDebugStringConvertible {
public var debugDescription: String {
let baseString = "Interface \(self.name): address \(self.address)"
@ -119,6 +372,7 @@ extension NIONetworkInterface: CustomDebugStringConvertible {
}
}
@available(*, deprecated, renamed: "NIONetworkDevice")
extension NIONetworkInterface: Equatable {
public static func ==(lhs: NIONetworkInterface, rhs: NIONetworkInterface) -> Bool {
return lhs.name == rhs.name &&

View File

@ -33,8 +33,19 @@ public protocol MulticastChannel: Channel {
/// - interface: The interface on which to join the given group, or `nil` to allow the kernel to choose.
/// - promise: The `EventLoopPromise` that will be notified once the operation is complete, or
/// `nil` if you are not interested in the result of the operation.
@available(*, deprecated, renamed: "joinGroup(_:device:promise:)")
func joinGroup(_ group: SocketAddress, interface: NIONetworkInterface?, promise: EventLoopPromise<Void>?)
/// Request that the `MulticastChannel` join the multicast group given by `group` on the device
/// given by `device`.
///
/// - parameters:
/// - group: The IP address corresponding to the relevant multicast group.
/// - device: The device on which to join the given group, or `nil` to allow the kernel to choose.
/// - promise: The `EventLoopPromise` that will be notified once the operation is complete, or
/// `nil` if you are not interested in the result of the operation.
func joinGroup(_ group: SocketAddress, device: NIONetworkDevice?, promise: EventLoopPromise<Void>?)
/// Request that the `MulticastChannel` leave the multicast group given by `group`.
///
/// - parameters:
@ -51,14 +62,25 @@ public protocol MulticastChannel: Channel {
/// - interface: The interface on which to leave the given group, or `nil` to allow the kernel to choose.
/// - promise: The `EventLoopPromise` that will be notified once the operation is complete, or
/// `nil` if you are not interested in the result of the operation.
@available(*, deprecated, renamed: "leaveGroup(_:device:promise:)")
func leaveGroup(_ group: SocketAddress, interface: NIONetworkInterface?, promise: EventLoopPromise<Void>?)
/// Request that the `MulticastChannel` leave the multicast group given by `group` on the device
/// given by `device`.
///
/// - parameters:
/// - group: The IP address corresponding to the relevant multicast group.
/// - device: The device on which to leave the given group, or `nil` to allow the kernel to choose.
/// - promise: The `EventLoopPromise` that will be notified once the operation is complete, or
/// `nil` if you are not interested in the result of the operation.
func leaveGroup(_ group: SocketAddress, device: NIONetworkDevice?, promise: EventLoopPromise<Void>?)
}
// MARK:- Default implementations for MulticastChannel
extension MulticastChannel {
public func joinGroup(_ group: SocketAddress, promise: EventLoopPromise<Void>?) {
self.joinGroup(group, interface: nil, promise: promise)
self.joinGroup(group, device: nil, promise: promise)
}
public func joinGroup(_ group: SocketAddress) -> EventLoopFuture<Void> {
@ -67,14 +89,21 @@ extension MulticastChannel {
return promise.futureResult
}
@available(*, deprecated, renamed: "joinGroup(_:device:)")
public func joinGroup(_ group: SocketAddress, interface: NIONetworkInterface?) -> EventLoopFuture<Void> {
let promise = self.eventLoop.makePromise(of: Void.self)
self.joinGroup(group, interface: interface, promise: promise)
return promise.futureResult
}
public func joinGroup(_ group: SocketAddress, device: NIONetworkDevice?) -> EventLoopFuture<Void> {
let promise = self.eventLoop.makePromise(of: Void.self)
self.joinGroup(group, device: device, promise: promise)
return promise.futureResult
}
public func leaveGroup(_ group: SocketAddress, promise: EventLoopPromise<Void>?) {
self.leaveGroup(group, interface: nil, promise: promise)
self.leaveGroup(group, device: nil, promise: promise)
}
public func leaveGroup(_ group: SocketAddress) -> EventLoopFuture<Void> {
@ -83,9 +112,45 @@ extension MulticastChannel {
return promise.futureResult
}
@available(*, deprecated, renamed: "leaveGroup(_:device:)")
public func leaveGroup(_ group: SocketAddress, interface: NIONetworkInterface?) -> EventLoopFuture<Void> {
let promise = self.eventLoop.makePromise(of: Void.self)
self.leaveGroup(group, interface: interface, promise: promise)
return promise.futureResult
}
public func leaveGroup(_ group: SocketAddress, device: NIONetworkDevice?) -> EventLoopFuture<Void> {
let promise = self.eventLoop.makePromise(of: Void.self)
self.leaveGroup(group, device: device, promise: promise)
return promise.futureResult
}
}
// MARK:- API Compatibility shims for MulticastChannel
extension MulticastChannel {
/// Request that the `MulticastChannel` join the multicast group given by `group` on the device
/// given by `device`.
///
/// - parameters:
/// - group: The IP address corresponding to the relevant multicast group.
/// - device: The device on which to join the given group, or `nil` to allow the kernel to choose.
/// - promise: The `EventLoopPromise` that will be notified once the operation is complete, or
/// `nil` if you are not interested in the result of the operation.
public func joinGroup(_ group: SocketAddress, device: NIONetworkDevice?, promise: EventLoopPromise<Void>?) {
// We just fail this in the default implementation. Users should override it.
promise?.fail(NIOMulticastNotImplementedError())
}
/// Request that the `MulticastChannel` leave the multicast group given by `group` on the device
/// given by `device`.
///
/// - parameters:
/// - group: The IP address corresponding to the relevant multicast group.
/// - device: The device on which to leave the given group, or `nil` to allow the kernel to choose.
/// - promise: The `EventLoopPromise` that will be notified once the operation is complete, or
/// `nil` if you are not interested in the result of the operation.
public func leaveGroup(_ group: SocketAddress, device: NIONetworkDevice?, promise: EventLoopPromise<Void>?) {
// We just fail this in the default implementation. Users should override it.
promise?.fail(NIOMulticastNotImplementedError())
}
}

View File

@ -755,22 +755,44 @@ extension DatagramChannel: MulticastChannel {
}
}
@available(*, deprecated, renamed: "joinGroup(_:device:promise:)")
func joinGroup(_ group: SocketAddress, interface: NIONetworkInterface?, promise: EventLoopPromise<Void>?) {
if eventLoop.inEventLoop {
self.performGroupOperation0(group, interface: interface, promise: promise, operation: .join)
self.performGroupOperation0(group, device: interface.map { NIONetworkDevice($0) }, promise: promise, operation: .join)
} else {
eventLoop.execute {
self.performGroupOperation0(group, interface: interface, promise: promise, operation: .join)
self.performGroupOperation0(group, device: interface.map { NIONetworkDevice($0) }, promise: promise, operation: .join)
}
}
}
@available(*, deprecated, renamed: "leaveGroup(_:device:promise:)")
func leaveGroup(_ group: SocketAddress, interface: NIONetworkInterface?, promise: EventLoopPromise<Void>?) {
if eventLoop.inEventLoop {
self.performGroupOperation0(group, interface: interface, promise: promise, operation: .leave)
self.performGroupOperation0(group, device: interface.map { NIONetworkDevice($0) }, promise: promise, operation: .leave)
} else {
eventLoop.execute {
self.performGroupOperation0(group, interface: interface, promise: promise, operation: .leave)
self.performGroupOperation0(group, device: interface.map { NIONetworkDevice($0) }, promise: promise, operation: .leave)
}
}
}
func joinGroup(_ group: SocketAddress, device: NIONetworkDevice?, promise: EventLoopPromise<Void>?) {
if eventLoop.inEventLoop {
self.performGroupOperation0(group, device: device, promise: promise, operation: .join)
} else {
eventLoop.execute {
self.performGroupOperation0(group, device: device, promise: promise, operation: .join)
}
}
}
func leaveGroup(_ group: SocketAddress, device: NIONetworkDevice?, promise: EventLoopPromise<Void>?) {
if eventLoop.inEventLoop {
self.performGroupOperation0(group, device: device, promise: promise, operation: .leave)
} else {
eventLoop.execute {
self.performGroupOperation0(group, device: device, promise: promise, operation: .leave)
}
}
}
@ -779,7 +801,7 @@ extension DatagramChannel: MulticastChannel {
///
/// Joining and leaving a multicast group ultimately corresponds to a single, carefully crafted, socket option.
private func performGroupOperation0(_ group: SocketAddress,
interface: NIONetworkInterface?,
device: NIONetworkDevice?,
promise: EventLoopPromise<Void>?,
operation: GroupOperation) {
self.eventLoop.assertInEventLoop()
@ -789,10 +811,10 @@ extension DatagramChannel: MulticastChannel {
return
}
/// Check if the interface supports multicast
if let interface = interface {
guard interface.multicastSupported else {
promise?.fail(ChannelError.multicastNotSupported(interface))
/// Check if the device supports multicast
if let device = device {
guard device.multicastSupported else {
promise?.fail(NIOMulticastNotSupportedError(device: device))
return
}
}
@ -817,7 +839,7 @@ extension DatagramChannel: MulticastChannel {
// Ok, we now have reason to believe this will actually work. We need to pass this on to the socket.
do {
switch (group, interface?.address) {
switch (group, device?.address) {
case (.unixDomainSocket, _):
preconditionFailure("Should not be reachable, UNIX sockets are never multicast addresses")
case (.v4(let groupAddress), .some(.v4(let interfaceAddress))):
@ -830,7 +852,7 @@ extension DatagramChannel: MulticastChannel {
try self.socket.setOption(level: .ip, name: operation.optionName(level: .ip), value: multicastRequest)
case (.v6(let groupAddress), .some(.v6)):
// IPv6 binding with specific target interface.
let multicastRequest = ipv6_mreq(ipv6mr_multiaddr: groupAddress.address.sin6_addr, ipv6mr_interface: UInt32(interface!.interfaceIndex))
let multicastRequest = ipv6_mreq(ipv6mr_multiaddr: groupAddress.address.sin6_addr, ipv6mr_interface: UInt32(device!.interfaceIndex))
try self.socket.setOption(level: .ipv6, name: operation.optionName(level: .ipv6), value: multicastRequest)
case (.v6(let groupAddress), .none):
// IPv6 binding with no specific interface requested.

View File

@ -100,6 +100,7 @@ public enum System {
///
/// - returns: An array of network interfaces available on this machine.
/// - throws: If an error is encountered while enumerating interfaces.
@available(*, deprecated, renamed: "enumerateDevices")
public static func enumerateInterfaces() throws -> [NIONetworkInterface] {
var interface: UnsafeMutablePointer<ifaddrs>? = nil
try Posix.getifaddrs(&interface)
@ -119,4 +120,32 @@ public enum System {
return results
}
/// A utility function that enumerates the available network devices on this machine.
///
/// This function returns values that are true for a brief snapshot in time. These results can
/// change, and the returned values will not change to reflect them. This function must be called
/// again to get new results.
///
/// - returns: An array of network devices available on this machine.
/// - throws: If an error is encountered while enumerating interfaces.
public static func enumerateDevices() throws -> [NIONetworkDevice] {
var interface: UnsafeMutablePointer<ifaddrs>? = nil
try Posix.getifaddrs(&interface)
let originalInterface = interface
defer {
freeifaddrs(originalInterface)
}
var results: [NIONetworkDevice] = []
results.reserveCapacity(12) // Arbitrary choice.
while let concreteInterface = interface {
if let nioInterface = NIONetworkDevice(concreteInterface.pointee) {
results.append(nioInterface)
}
interface = concreteInterface.pointee.ifa_next
}
return results
}
}

View File

@ -46,18 +46,18 @@ private final class ChatMessageEncoder: ChannelOutboundHandler {
// We allow users to specify the interface they want to use here.
var targetInterface: NIONetworkInterface? = nil
var targetDevice: NIONetworkDevice? = nil
if let interfaceAddress = CommandLine.arguments.dropFirst().first,
let targetAddress = try? SocketAddress(ipAddress: interfaceAddress, port: 0) {
for interface in try! System.enumerateInterfaces() {
if interface.address == targetAddress {
targetInterface = interface
for device in try! System.enumerateDevices() {
if device.address == targetAddress {
targetDevice = device
break
}
}
if targetInterface == nil {
fatalError("Could not find interface for \(interfaceAddress)")
if targetDevice == nil {
fatalError("Could not find device for \(interfaceAddress)")
}
}
@ -81,20 +81,22 @@ let datagramChannel = try datagramBootstrap
.bind(host: "0.0.0.0", port: 7654)
.flatMap { channel -> EventLoopFuture<Channel> in
let channel = channel as! MulticastChannel
return channel.joinGroup(chatMulticastGroup, interface: targetInterface).map { channel }
return channel.joinGroup(chatMulticastGroup, device: targetDevice).map { channel }
}.flatMap { channel -> EventLoopFuture<Channel> in
guard let targetInterface = targetInterface else {
guard let targetDevice = targetDevice else {
return channel.eventLoop.makeSucceededFuture(channel)
}
let provider = channel as! SocketOptionProvider
switch targetInterface.address {
case .v4(let addr):
switch targetDevice.address {
case .some(.v4(let addr)):
return provider.setIPMulticastIF(addr.address.sin_addr).map { channel }
case .v6:
return provider.setIPv6MulticastIF(CUnsignedInt(targetInterface.interfaceIndex)).map { channel }
case .unixDomainSocket:
case .some(.v6):
return provider.setIPv6MulticastIF(CUnsignedInt(targetDevice.interfaceIndex)).map { channel }
case .some(.unixDomainSocket):
preconditionFailure("Should not be possible to create a multicast socket on a unix domain socket")
case .none:
preconditionFailure("Should not be possible to create a multicast socket on an interface without an address")
}
}.wait()

View File

@ -119,7 +119,7 @@ final class DatagramChannelTests: XCTestCase {
private var supportsIPv6: Bool {
do {
let ipv6Loopback = try SocketAddress(ipAddress: "::1", port: 0)
return try System.enumerateInterfaces().contains(where: { $0.address == ipv6Loopback })
return try System.enumerateDevices().contains(where: { $0.address == ipv6Loopback })
} catch {
return false
}

View File

@ -31,6 +31,10 @@ extension MulticastTest {
("testCanJoinBasicMulticastGroupIPv6", testCanJoinBasicMulticastGroupIPv6),
("testCanLeaveAnIPv4MulticastGroup", testCanLeaveAnIPv4MulticastGroup),
("testCanLeaveAnIPv6MulticastGroup", testCanLeaveAnIPv6MulticastGroup),
("testCanJoinBasicMulticastGroupIPv4WithDevice", testCanJoinBasicMulticastGroupIPv4WithDevice),
("testCanJoinBasicMulticastGroupIPv6WithDevice", testCanJoinBasicMulticastGroupIPv6WithDevice),
("testCanLeaveAnIPv4MulticastGroupWithDevice", testCanLeaveAnIPv4MulticastGroupWithDevice),
("testCanLeaveAnIPv6MulticastGroupWithDevice", testCanLeaveAnIPv6MulticastGroupWithDevice),
]
}
}

View File

@ -49,6 +49,7 @@ final class MulticastTest: XCTestCase {
struct ReceivedDatagramError: Error { }
@available(*, deprecated)
private func interfaceForAddress(address: String) throws -> NIONetworkInterface {
let targetAddress = try SocketAddress(ipAddress: address, port: 0)
guard let interface = try System.enumerateInterfaces().lazy.filter({ $0.address == targetAddress }).first else {
@ -57,6 +58,15 @@ final class MulticastTest: XCTestCase {
return interface
}
private func deviceForAddress(address: String) throws -> NIONetworkDevice {
let targetAddress = try SocketAddress(ipAddress: address, port: 0)
guard let device = try System.enumerateDevices().lazy.filter({ $0.address == targetAddress }).first else {
throw NoSuchInterfaceError()
}
return device
}
@available(*, deprecated)
private func bindMulticastChannel(host: String, port: Int, multicastAddress: String, interface: NIONetworkInterface) -> EventLoopFuture<MulticastChannel> {
return DatagramBootstrap(group: self.group)
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
@ -84,6 +94,34 @@ final class MulticastTest: XCTestCase {
}
}
private func bindMulticastChannel(host: String, port: Int, multicastAddress: String, device: NIONetworkDevice) -> EventLoopFuture<MulticastChannel> {
return DatagramBootstrap(group: self.group)
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.bind(host: host, port: port)
.flatMap { channel in
let channel = channel as! MulticastChannel
do {
let multicastAddress = try SocketAddress(ipAddress: multicastAddress, port: channel.localAddress!.port!)
return channel.joinGroup(multicastAddress, device: device).map { channel }
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
}.flatMap { (channel: MulticastChannel) -> EventLoopFuture<MulticastChannel> in
let provider = channel as! SocketOptionProvider
switch channel.localAddress! {
case .v4:
return provider.setIPMulticastLoop(1).map { channel }
case .v6:
return provider.setIPv6MulticastLoop(1).map { channel }
case .unixDomainSocket:
preconditionFailure("Multicast is meaningless on unix domain sockets")
}
}
}
@available(*, deprecated)
private func configureSenderMulticastIf(sender: Channel, multicastInterface: NIONetworkInterface) -> EventLoopFuture<Void> {
let provider = sender as! SocketOptionProvider
@ -98,6 +136,21 @@ final class MulticastTest: XCTestCase {
}
}
private func configureSenderMulticastIf(sender: Channel, multicastDevice: NIONetworkDevice) -> EventLoopFuture<Void> {
let provider = sender as! SocketOptionProvider
switch (sender.localAddress!, multicastDevice.address) {
case (.v4, .some(.v4(let addr))):
return provider.setIPMulticastIF(addr.address.sin_addr)
case (.v6, .some(.v6)):
return provider.setIPv6MulticastIF(CUnsignedInt(multicastDevice.interfaceIndex))
default:
XCTFail("Cannot join channel bound to \(sender.localAddress!) to interface at \(String(describing: multicastDevice.address))")
return sender.eventLoop.makeFailedFuture(MulticastInterfaceMismatchError())
}
}
@available(*, deprecated)
private func leaveMulticastGroup(channel: Channel, multicastAddress: String, interface: NIONetworkInterface) -> EventLoopFuture<Void> {
let channel = channel as! MulticastChannel
@ -109,6 +162,17 @@ final class MulticastTest: XCTestCase {
}
}
private func leaveMulticastGroup(channel: Channel, multicastAddress: String, device: NIONetworkDevice) -> EventLoopFuture<Void> {
let channel = channel as! MulticastChannel
do {
let multicastAddress = try SocketAddress(ipAddress: multicastAddress, port: channel.localAddress!.port!)
return channel.leaveGroup(multicastAddress, device: device)
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
}
private func assertDatagramReaches(multicastChannel: Channel, sender: Channel, multicastAddress: SocketAddress, file: StaticString = #file, line: UInt = #line) throws {
let receivedMulticastDatagram = multicastChannel.eventLoop.makePromise(of: AddressedEnvelope<ByteBuffer>.self)
XCTAssertNoThrow(try multicastChannel.pipeline.addHandler(PromiseOnReadHandler(promise: receivedMulticastDatagram)).wait())
@ -154,6 +218,7 @@ final class MulticastTest: XCTestCase {
XCTAssertNoThrow(try timeoutPromise.futureResult.wait(), file: (file), line: line)
}
@available(*, deprecated)
func testCanJoinBasicMulticastGroupIPv4() throws {
let multicastInterface = try assertNoThrowWithValue(self.interfaceForAddress(address: "127.0.0.1"))
guard multicastInterface.multicastSupported else {
@ -163,8 +228,8 @@ final class MulticastTest: XCTestCase {
port: 0,
multicastAddress: "224.0.2.66",
interface: multicastInterface).wait()) { error in
if case .some(.multicastNotSupported(let actualInterface)) = error as? ChannelError {
XCTAssertEqual(multicastInterface, actualInterface)
if let error = error as? NIOMulticastNotSupportedError {
XCTAssertEqual(NIONetworkDevice(multicastInterface), error.device)
} else {
XCTFail("unexpected error: \(error)")
}
@ -182,8 +247,8 @@ final class MulticastTest: XCTestCase {
interface: multicastInterface).wait())
// no error, that's great
} catch {
if case .some(.multicastNotSupported(_)) = error as? ChannelError {
XCTFail("network interface (\(multicastInterface)) claims we support multicast but: \(error)")
if error is NIOMulticastNotSupportedError {
XCTFail("network interface (\(multicastInterface))) claims we support multicast but: \(error)")
} else {
XCTFail("unexpected error: \(error)")
}
@ -210,6 +275,7 @@ final class MulticastTest: XCTestCase {
try self.assertDatagramReaches(multicastChannel: listenerChannel, sender: sender, multicastAddress: multicastAddress)
}
@available(*, deprecated)
func testCanJoinBasicMulticastGroupIPv6() throws {
guard System.supportsIPv6 else {
// Skip on non-IPv6 systems
@ -224,8 +290,8 @@ final class MulticastTest: XCTestCase {
port: 0,
multicastAddress: "ff12::beeb",
interface: multicastInterface).wait()) { error in
if case .some(.multicastNotSupported(let actualInterface)) = error as? ChannelError {
XCTAssertEqual(multicastInterface, actualInterface)
if let error = error as? NIOMulticastNotSupportedError {
XCTAssertEqual(NIONetworkDevice(multicastInterface), error.device)
} else {
XCTFail("unexpected error: \(error)")
}
@ -242,8 +308,8 @@ final class MulticastTest: XCTestCase {
multicastAddress: "ff12::beeb",
interface: multicastInterface).wait())
} catch {
if case .some(.multicastNotSupported(_)) = error as? ChannelError {
XCTFail("network interface (\(multicastInterface)) claims we support multicast but: \(error)")
if error is NIOMulticastNotSupportedError {
XCTFail("network interface (\(multicastInterface))) claims we support multicast but: \(error)")
} else {
XCTFail("unexpected error: \(error)")
}
@ -269,6 +335,7 @@ final class MulticastTest: XCTestCase {
try self.assertDatagramReaches(multicastChannel: listenerChannel, sender: sender, multicastAddress: multicastAddress)
}
@available(*, deprecated)
func testCanLeaveAnIPv4MulticastGroup() throws {
let multicastInterface = try assertNoThrowWithValue(self.interfaceForAddress(address: "127.0.0.1"))
guard multicastInterface.multicastSupported else {
@ -307,6 +374,7 @@ final class MulticastTest: XCTestCase {
try self.assertDatagramDoesNotReach(multicastChannel: listenerChannel, after: .milliseconds(500), sender: sender, multicastAddress: multicastAddress)
}
@available(*, deprecated)
func testCanLeaveAnIPv6MulticastGroup() throws {
guard System.supportsIPv6 else {
// Skip on non-IPv6 systems
@ -348,4 +416,199 @@ final class MulticastTest: XCTestCase {
XCTAssertNoThrow(try leaveMulticastGroup(channel: listenerChannel, multicastAddress: "ff12::beeb", interface: multicastInterface).wait())
try self.assertDatagramDoesNotReach(multicastChannel: listenerChannel, after: .milliseconds(500), sender: sender, multicastAddress: multicastAddress)
}
func testCanJoinBasicMulticastGroupIPv4WithDevice() throws {
let multicastDevice = try assertNoThrowWithValue(self.deviceForAddress(address: "127.0.0.1"))
guard multicastDevice.multicastSupported else {
// alas, we don't support multicast, let's skip but test the right error is thrown
XCTAssertThrowsError(try self.bindMulticastChannel(host: "0.0.0.0",
port: 0,
multicastAddress: "224.0.2.66",
device: multicastDevice).wait()) { error in
if let error = error as? NIOMulticastNotSupportedError {
XCTAssertEqual(multicastDevice, error.device)
} else {
XCTFail("unexpected error: \(error)")
}
}
return
}
// We avoid the risk of interference due to our all-addresses bind by only joining this multicast
// group on the loopback.
let listenerChannel: Channel
do {
listenerChannel = try assertNoThrowWithValue(self.bindMulticastChannel(host: "0.0.0.0",
port: 0,
multicastAddress: "224.0.2.66",
device: multicastDevice).wait())
// no error, that's great
} catch {
if error is NIOMulticastNotSupportedError {
XCTFail("network interface (\(multicastDevice)) claims we support multicast but: \(error)")
} else {
XCTFail("unexpected error: \(error)")
}
return
}
defer {
XCTAssertNoThrow(try listenerChannel.close().wait())
}
let multicastAddress = try assertNoThrowWithValue(try SocketAddress(ipAddress: "224.0.2.66", port: listenerChannel.localAddress!.port!))
// Now that we've joined the group, let's send to it.
let sender = try assertNoThrowWithValue(DatagramBootstrap(group: self.group)
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.bind(host: "127.0.0.1", port: 0)
.wait()
)
defer {
XCTAssertNoThrow(try sender.close().wait())
}
XCTAssertNoThrow(try configureSenderMulticastIf(sender: sender, multicastDevice: multicastDevice).wait())
try self.assertDatagramReaches(multicastChannel: listenerChannel, sender: sender, multicastAddress: multicastAddress)
}
func testCanJoinBasicMulticastGroupIPv6WithDevice() throws {
guard System.supportsIPv6 else {
// Skip on non-IPv6 systems
return
}
let multicastDevice = try assertNoThrowWithValue(self.deviceForAddress(address: "::1"))
guard multicastDevice.multicastSupported else {
// alas, we don't support multicast, let's skip but test the right error is thrown
XCTAssertThrowsError(try self.bindMulticastChannel(host: "::1",
port: 0,
multicastAddress: "ff12::beeb",
device: multicastDevice).wait()) { error in
if let error = error as? NIOMulticastNotSupportedError {
XCTAssertEqual(multicastDevice, error.device)
} else {
XCTFail("unexpected error: \(error)")
}
}
return
}
let listenerChannel: Channel
do {
// We avoid the risk of interference due to our all-addresses bind by only joining this multicast
// group on the loopback.
listenerChannel = try assertNoThrowWithValue(self.bindMulticastChannel(host: "::1",
port: 0,
multicastAddress: "ff12::beeb",
device: multicastDevice).wait())
} catch {
if error is NIOMulticastNotSupportedError {
XCTFail("network interface (\(multicastDevice)) claims we support multicast but: \(error)")
} else {
XCTFail("unexpected error: \(error)")
}
return
}
defer {
XCTAssertNoThrow(try listenerChannel.close().wait())
}
let multicastAddress = try assertNoThrowWithValue(try SocketAddress(ipAddress: "ff12::beeb", port: listenerChannel.localAddress!.port!))
// Now that we've joined the group, let's send to it.
let sender = try assertNoThrowWithValue(DatagramBootstrap(group: self.group)
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.bind(host: "::1", port: 0)
.wait()
)
defer {
XCTAssertNoThrow(try sender.close().wait())
}
XCTAssertNoThrow(try configureSenderMulticastIf(sender: sender, multicastDevice: multicastDevice).wait())
try self.assertDatagramReaches(multicastChannel: listenerChannel, sender: sender, multicastAddress: multicastAddress)
}
func testCanLeaveAnIPv4MulticastGroupWithDevice() throws {
let multicastDevice = try assertNoThrowWithValue(self.deviceForAddress(address: "127.0.0.1"))
guard multicastDevice.multicastSupported else {
// alas, we don't support multicast, let's skip
return
}
// We avoid the risk of interference due to our all-addresses bind by only joining this multicast
// group on the loopback.
let listenerChannel = try assertNoThrowWithValue(self.bindMulticastChannel(host: "0.0.0.0",
port: 0,
multicastAddress: "224.0.2.66",
device: multicastDevice).wait())
defer {
XCTAssertNoThrow(try listenerChannel.close().wait())
}
let multicastAddress = try assertNoThrowWithValue(try SocketAddress(ipAddress: "224.0.2.66", port: listenerChannel.localAddress!.port!))
// Now that we've joined the group, let's send to it.
let sender = try assertNoThrowWithValue(DatagramBootstrap(group: self.group)
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.bind(host: "127.0.0.1", port: 0)
.wait()
)
defer {
XCTAssertNoThrow(try sender.close().wait())
}
XCTAssertNoThrow(try configureSenderMulticastIf(sender: sender, multicastDevice: multicastDevice).wait())
try self.assertDatagramReaches(multicastChannel: listenerChannel, sender: sender, multicastAddress: multicastAddress)
// Now we should *leave* the group.
XCTAssertNoThrow(try leaveMulticastGroup(channel: listenerChannel, multicastAddress: "224.0.2.66", device: multicastDevice).wait())
try self.assertDatagramDoesNotReach(multicastChannel: listenerChannel, after: .milliseconds(500), sender: sender, multicastAddress: multicastAddress)
}
func testCanLeaveAnIPv6MulticastGroupWithDevice() throws {
guard System.supportsIPv6 else {
// Skip on non-IPv6 systems
return
}
let multicastDevice = try assertNoThrowWithValue(self.deviceForAddress(address: "::1"))
guard multicastDevice.multicastSupported else {
// alas, we don't support multicast, let's skip
return
}
// We avoid the risk of interference due to our all-addresses bind by only joining this multicast
// group on the loopback.
let listenerChannel = try assertNoThrowWithValue(self.bindMulticastChannel(host: "::1",
port: 0,
multicastAddress: "ff12::beeb",
device: multicastDevice).wait())
defer {
XCTAssertNoThrow(try listenerChannel.close().wait())
}
let multicastAddress = try assertNoThrowWithValue(try SocketAddress(ipAddress: "ff12::beeb", port: listenerChannel.localAddress!.port!))
// Now that we've joined the group, let's send to it.
let sender = try assertNoThrowWithValue(DatagramBootstrap(group: self.group)
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.bind(host: "::1", port: 0)
.wait()
)
defer {
XCTAssertNoThrow(try sender.close().wait())
}
XCTAssertNoThrow(try configureSenderMulticastIf(sender: sender, multicastDevice: multicastDevice).wait())
try self.assertDatagramReaches(multicastChannel: listenerChannel, sender: sender, multicastAddress: multicastAddress)
// Now we should *leave* the group.
XCTAssertNoThrow(try leaveMulticastGroup(channel: listenerChannel, multicastAddress: "ff12::beeb", device: multicastDevice).wait())
try self.assertDatagramDoesNotReach(multicastChannel: listenerChannel, after: .milliseconds(500), sender: sender, multicastAddress: multicastAddress)
}
}

View File

@ -65,7 +65,7 @@ final class SocketOptionProviderTest: XCTestCase {
// risk.
let v4LoopbackAddress = try! assertNoThrowWithValue(SocketAddress(ipAddress: "127.0.0.1", port: 0))
let v6LoopbackAddress = try! assertNoThrowWithValue(SocketAddress(ipAddress: "::1", port: 0))
let v4LoopbackInterface = try! assertNoThrowWithValue(System.enumerateInterfaces().filter {
let v4LoopbackInterface = try! assertNoThrowWithValue(System.enumerateDevices().filter {
$0.address == v4LoopbackAddress
}.first)!
@ -73,7 +73,7 @@ final class SocketOptionProviderTest: XCTestCase {
if v4LoopbackAddress.isMulticast {
self.ipv4DatagramChannel = try? assertNoThrowWithValue(
DatagramBootstrap(group: group).bind(host: "127.0.0.1", port: 0).flatMap { channel in
return (channel as! MulticastChannel).joinGroup(try! SocketAddress(ipAddress: "224.0.2.66", port: 0), interface: v4LoopbackInterface).map { channel }
return (channel as! MulticastChannel).joinGroup(try! SocketAddress(ipAddress: "224.0.2.66", port: 0), device: v4LoopbackInterface).map { channel }
}.wait()
)
}
@ -81,9 +81,9 @@ final class SocketOptionProviderTest: XCTestCase {
// Only run the setup if the loopback interface supports multicast
if v6LoopbackAddress.isMulticast {
// The IPv6 setup is allowed to fail, some hosts don't have IPv6.
let v6LoopbackInterface = try? assertNoThrowWithValue(System.enumerateInterfaces().filter { $0.address == v6LoopbackAddress }.first)
let v6LoopbackInterface = try? assertNoThrowWithValue(System.enumerateDevices().filter { $0.address == v6LoopbackAddress }.first)
self.ipv6DatagramChannel = try? DatagramBootstrap(group: group).bind(host: "::1", port: 0).flatMap { channel in
return (channel as! MulticastChannel).joinGroup(try! SocketAddress(ipAddress: "ff12::beeb", port: 0), interface: v6LoopbackInterface).map { channel }
return (channel as! MulticastChannel).joinGroup(try! SocketAddress(ipAddress: "ff12::beeb", port: 0), device: v6LoopbackInterface).map { channel }
}.wait()
}
}
@ -222,7 +222,7 @@ final class SocketOptionProviderTest: XCTestCase {
// TODO: test this when we know what the interface indices are.
let loopbackAddress = try assertNoThrowWithValue(SocketAddress(ipAddress: "::1", port: 0))
guard let loopbackInterface = try assertNoThrowWithValue(System.enumerateInterfaces().filter({ $0.address == loopbackAddress }).first) else {
guard let loopbackInterface = try assertNoThrowWithValue(System.enumerateDevices().filter({ $0.address == loopbackAddress }).first) else {
XCTFail("Could not find index of loopback address")
return
}

View File

@ -20,7 +20,7 @@ extension System {
static var supportsIPv6: Bool {
do {
let ipv6Loopback = try SocketAddress.makeAddressResolvingHost("::1", port: 0)
return try System.enumerateInterfaces().filter { $0.address == ipv6Loopback }.first != nil
return try System.enumerateDevices().filter { $0.address == ipv6Loopback }.first != nil
} catch {
return false
}

View File

@ -29,6 +29,7 @@ extension UtilitiesTest {
return [
("testCoreCountWorks", testCoreCountWorks),
("testEnumeratingInterfaces", testEnumeratingInterfaces),
("testEnumeratingDevices", testEnumeratingDevices),
]
}
}

View File

@ -20,6 +20,7 @@ class UtilitiesTest: XCTestCase {
XCTAssertGreaterThan(System.coreCount, 0)
}
@available(*, deprecated)
func testEnumeratingInterfaces() throws {
// This is a tricky test, because we can't really assert much and expect this
// to pass on all systems. The best we can do is assume there is a loopback:
@ -47,4 +48,32 @@ class UtilitiesTest: XCTestCase {
XCTAssertTrue(ipv4LoopbackPresent || ipv6LoopbackPresent)
}
func testEnumeratingDevices() throws {
// This is a tricky test, because we can't really assert much and expect this
// to pass on all systems. The best we can do is assume there is a loopback:
// maybe an IPv4 one, maybe an IPv6 one, but there will be one. We look for
// both.
let devices = try System.enumerateDevices()
XCTAssertGreaterThan(devices.count, 0)
var ipv4LoopbackPresent = false
var ipv6LoopbackPresent = false
for device in devices {
if try device.address == SocketAddress(ipAddress: "127.0.0.1", port: 0) {
ipv4LoopbackPresent = true
XCTAssertEqual(device.netmask, try SocketAddress(ipAddress: "255.0.0.0", port: 0))
XCTAssertNil(device.broadcastAddress)
XCTAssertNil(device.pointToPointDestinationAddress)
} else if try device.address == SocketAddress(ipAddress: "::1", port: 0) {
ipv6LoopbackPresent = true
XCTAssertEqual(device.netmask, try SocketAddress(ipAddress: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", port: 0))
XCTAssertNil(device.broadcastAddress)
XCTAssertNil(device.pointToPointDestinationAddress)
}
}
XCTAssertTrue(ipv4LoopbackPresent || ipv6LoopbackPresent)
}
}