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:
parent
8d430f2d85
commit
f9552beffa
|
@ -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.
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -31,6 +31,10 @@ extension MulticastTest {
|
|||
("testCanJoinBasicMulticastGroupIPv6", testCanJoinBasicMulticastGroupIPv6),
|
||||
("testCanLeaveAnIPv4MulticastGroup", testCanLeaveAnIPv4MulticastGroup),
|
||||
("testCanLeaveAnIPv6MulticastGroup", testCanLeaveAnIPv6MulticastGroup),
|
||||
("testCanJoinBasicMulticastGroupIPv4WithDevice", testCanJoinBasicMulticastGroupIPv4WithDevice),
|
||||
("testCanJoinBasicMulticastGroupIPv6WithDevice", testCanJoinBasicMulticastGroupIPv6WithDevice),
|
||||
("testCanLeaveAnIPv4MulticastGroupWithDevice", testCanLeaveAnIPv4MulticastGroupWithDevice),
|
||||
("testCanLeaveAnIPv6MulticastGroupWithDevice", testCanLeaveAnIPv6MulticastGroupWithDevice),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ extension UtilitiesTest {
|
|||
return [
|
||||
("testCoreCountWorks", testCoreCountWorks),
|
||||
("testEnumeratingInterfaces", testEnumeratingInterfaces),
|
||||
("testEnumeratingDevices", testEnumeratingDevices),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue