Permalink
Browse files

Support UDP multicast. (#618)

Motivation:

A large number of very useful protocols are implemented using multicast
with UDP. As a result, it would be helpful to add support for joining and
leaving IP multicast groups using SwiftNIO.

Modifications:

- Defines a MulticastChannel protocol for channels that support joining and
  leaving multicast groups.
- Adds an implementation of MulticastChannel to DatagramChannel.
- Adds a interfaceIndex property to NIONetworkInterface.
- Adds if_nametoindex to the Posix enum.
- Adds a isMulticast computed property to SocketAddress
- Adds a demo multicast chat application.
- Add a number of multicast-related socket options to SocketOptionProvider.

Result:

NIO users will be able to write channels that handle multicast UDP.
  • Loading branch information...
Lukasa committed Sep 24, 2018
1 parent 79267e8 commit c73eb5769473c66d653fe641c8e74a394c594553
@@ -53,6 +53,8 @@ var targets: [PackageDescription.Target] = [
dependencies: ["NIO", "NIOHTTP1", "NIOWebSocket"]),
.target(name: "NIOPerformanceTester",
dependencies: ["NIO", "NIOHTTP1", "NIOFoundationCompat"]),
.target(name: "NIOMulticastChat",
dependencies: ["NIO"]),
.testTarget(name: "NIOTests",
dependencies: ["NIO", "NIOFoundationCompat"]),
.testTarget(name: "NIOConcurrencyHelpersTests",
@@ -77,6 +79,7 @@ let package = Package(
targets: ["NIOWebSocketServer"]),
.executable(name: "NIOPerformanceTester",
targets: ["NIOPerformanceTester"]),
.executable(name: "NIOMulticastChat", targets: ["NIOMulticastChat"]),
.library(name: "NIO", targets: ["NIO"]),
.library(name: "NIOTLS", targets: ["NIOTLS"]),
.library(name: "NIOHTTP1", targets: ["NIOHTTP1"]),
@@ -331,12 +331,29 @@ public enum ChannelError: Error {
}
/// This should be inside of `ChannelError` but we keep it separate to not break API.
// TODO: For 2.0: bring this inside of `ChannelError`
// TODO: For 2.0: bring this inside of `ChannelError`. https://github.com/apple/swift-nio/issues/620
public enum ChannelLifecycleError: Error {
/// An operation that was inappropriate given the current `Channel` state was attempted.
case inappropriateOperationForState
}
/// This should be inside of `ChannelError` but we keep it separate to not break API.
// TODO: For 2.0: bring this inside of `ChannelError`. https://github.com/apple/swift-nio/issues/620
public enum MulticastError: Error {
/// The local address of the `Channel` could not be determined.
case unknownLocalAddress
/// The address family of the multicast group was not valid for this `Channel`.
case badMulticastGroupAddressFamily
/// The address family of the provided multicast group join is not valid for this `Channel`.
case badInterfaceAddressFamily
/// An attempt was made to join a multicast group that does not correspond to a multicast
/// address.
case illegalMulticastAddress(SocketAddress)
}
extension ChannelError: Equatable {
public static func ==(lhs: ChannelError, rhs: ChannelError) -> Bool {
switch (lhs, rhs) {
@@ -60,6 +60,9 @@ public final class NIONetworkInterface {
/// instead.
public let pointToPointDestinationAddress: SocketAddress?
/// The index of the interface, as provided by `if_nametoindex`.
public let interfaceIndex: Int
/// Create a brand new network interface.
///
/// This constructor will fail if NIO does not understand the format of the underlying
@@ -88,6 +91,12 @@ public final class NIONetworkInterface {
self.broadcastAddress = nil
self.pointToPointDestinationAddress = nil
}
do {
self.interfaceIndex = Int(try Posix.if_nametoindex(caddr.ifa_name))
} catch {
return nil
}
}
}
@@ -105,6 +114,7 @@ extension NIONetworkInterface: Equatable {
lhs.address == rhs.address &&
lhs.netmask == rhs.netmask &&
lhs.broadcastAddress == rhs.broadcastAddress &&
lhs.pointToPointDestinationAddress == rhs.pointToPointDestinationAddress
lhs.pointToPointDestinationAddress == rhs.pointToPointDestinationAddress &&
lhs.interfaceIndex == rhs.interfaceIndex
}
}
@@ -0,0 +1,91 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
/// A `MulticastChannel` is a `Channel` that supports IP multicast operations: that is, a channel that can join multicast
/// groups.
///
/// - note: As with `Channel`, all operations on a `MulticastChannel` are thread-safe.
public protocol MulticastChannel: Channel {
/// Request that the `MulticastChannel` join the multicast group given by `group`.
///
/// - parameters:
/// - group: The IP address corresponding to the relevant multicast group.
/// - 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, promise: EventLoopPromise<Void>?)
/// Request that the `MulticastChannel` join the multicast group given by `group` on the interface
/// given by `interface`.
///
/// - parameters:
/// - group: The IP address corresponding to the relevant multicast group.
/// - 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.
func joinGroup(_ group: SocketAddress, interface: NIONetworkInterface?, promise: EventLoopPromise<Void>?)
/// Request that the `MulticastChannel` leave the multicast group given by `group`.
///
/// - parameters:
/// - group: The IP address corresponding to the relevant multicast group.
/// - 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, promise: EventLoopPromise<Void>?)
/// Request that the `MulticastChannel` leave the multicast group given by `group` on the interface
/// given by `interface`.
///
/// - parameters:
/// - group: The IP address corresponding to the relevant multicast group.
/// - 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.
func leaveGroup(_ group: SocketAddress, interface: NIONetworkInterface?, promise: EventLoopPromise<Void>?)
}
// MARK:- Default implementations for MulticastChannel
public extension MulticastChannel {
func joinGroup(_ group: SocketAddress, promise: EventLoopPromise<Void>?) {
self.joinGroup(group, interface: nil, promise: promise)
}
func joinGroup(_ group: SocketAddress) -> EventLoopFuture<Void> {
let promise: EventLoopPromise<Void> = self.eventLoop.newPromise()
self.joinGroup(group, promise: promise)
return promise.futureResult
}
func joinGroup(_ group: SocketAddress, interface: NIONetworkInterface?) -> EventLoopFuture<Void> {
let promise: EventLoopPromise<Void> = self.eventLoop.newPromise()
self.joinGroup(group, interface: interface, promise: promise)
return promise.futureResult
}
func leaveGroup(_ group: SocketAddress, promise: EventLoopPromise<Void>?) {
self.leaveGroup(group, interface: nil, promise: promise)
}
func leaveGroup(_ group: SocketAddress) -> EventLoopFuture<Void> {
let promise: EventLoopPromise<Void> = self.eventLoop.newPromise()
self.leaveGroup(group, promise: promise)
return promise.futureResult
}
func leaveGroup(_ group: SocketAddress, interface: NIONetworkInterface?) -> EventLoopFuture<Void> {
let promise: EventLoopPromise<Void> = self.eventLoop.newPromise()
self.leaveGroup(group, interface: interface, promise: promise)
return promise.futureResult
}
}
@@ -310,3 +310,29 @@ extension SocketAddress: Equatable {
}
}
extension SocketAddress {
/// Whether this `SocketAddress` corresponds to a multicast address.
public var isMulticast: Bool {
switch self {
case .unixDomainSocket:
// No multicast on unix sockets.
return false
case .v4(let v4Addr):
// For IPv4 a multicast address is in the range 224.0.0.0/4.
// The easy way to check if this is the case is to just mask off
// the address.
let v4WireAddress = v4Addr.address.sin_addr.s_addr
let mask = in_addr_t(0xF000_0000).bigEndian
let subnet = in_addr_t(0xE000_0000).bigEndian
return v4WireAddress & mask == subnet
case .v6(let v6Addr):
// For IPv6 a multicast address is in the range ff00::/8.
// Here we don't need a bitmask, as all the top bits are set,
// so we can just ask for equality on the top byte.
var v6WireAddress = v6Addr.address.sin6_addr
return withUnsafeBytes(of: &v6WireAddress) { $0[0] == 0xff }
}
}
}
@@ -728,3 +728,123 @@ extension DatagramChannel: CustomStringConvertible {
return "DatagramChannel { selectable = \(self.selectable), localAddress = \(self.localAddress.debugDescription), remoteAddress = \(self.remoteAddress.debugDescription) }"
}
}
extension DatagramChannel: MulticastChannel {
/// The socket options for joining and leaving multicast groups are very similar.
/// This enum allows us to write a single function to do all the work, and then
/// at the last second pull out the correct socket option name.
private enum GroupOperation {
/// Join a multicast group.
case join
/// Leave a multicast group.
case leave
/// Given a socket option level, returns the appropriate socket option name for
/// this group operation.
///
/// - parameters:
/// - level: The socket option level. Must be one of `IPPROTO_IP` or
/// `IPPROTO_IPV6`. Will trap if an invalid value is provided.
/// - returns: The socket option name to use for this group operation.
func optionName(level: CInt) -> CInt {
switch (self, level) {
case (.join, CInt(IPPROTO_IP)):
return CInt(IP_ADD_MEMBERSHIP)
case (.leave, CInt(IPPROTO_IP)):
return CInt(IP_DROP_MEMBERSHIP)
case (.join, CInt(IPPROTO_IPV6)):
return CInt(IPV6_JOIN_GROUP)
case (.leave, CInt(IPPROTO_IPV6)):
return CInt(IPV6_LEAVE_GROUP)
default:
preconditionFailure("Unexpected socket option level: \(level)")
}
}
}
public func joinGroup(_ group: SocketAddress, interface: NIONetworkInterface?, promise: EventLoopPromise<Void>?) {
if eventLoop.inEventLoop {
self.performGroupOperation0(group, interface: interface, promise: promise, operation: .join)
} else {
eventLoop.execute {
self.performGroupOperation0(group, interface: interface, promise: promise, operation: .join)
}
}
}
public func leaveGroup(_ group: SocketAddress, interface: NIONetworkInterface?, promise: EventLoopPromise<Void>?) {
if eventLoop.inEventLoop {
self.performGroupOperation0(group, interface: interface, promise: promise, operation: .leave)
} else {
eventLoop.execute {
self.performGroupOperation0(group, interface: interface, promise: promise, operation: .leave)
}
}
}
/// The implementation of `joinGroup` and `leaveGroup`.
///
/// Joining and leaving a multicast group ultimately corresponds to a single, carefully crafted, socket option.
private func performGroupOperation0(_ group: SocketAddress,
interface: NIONetworkInterface?,
promise: EventLoopPromise<Void>?,
operation: GroupOperation) {
assert(self.eventLoop.inEventLoop)
guard self.isActive else {
promise?.fail(error: ChannelLifecycleError.inappropriateOperationForState)
return
}
// We need to check that we have the appropriate address types in all cases. They all need to overlap with
// the address type of this channel, or this cannot work.
guard let localAddress = self.localAddress else {
promise?.fail(error: MulticastError.unknownLocalAddress)
return
}
guard localAddress.protocolFamily == group.protocolFamily else {
promise?.fail(error: MulticastError.badMulticastGroupAddressFamily)
return
}
// Ok, now we need to check that the group we've been asked to join is actually a multicast group.
guard group.isMulticast else {
promise?.fail(error: MulticastError.illegalMulticastAddress(group))
return
}
// 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) {
case (.unixDomainSocket, _):
preconditionFailure("Should not be reachable, UNIX sockets are never multicast addresses")
case (.v4(let groupAddress), .some(.v4(let interfaceAddress))):
// IPv4Binding with specific target interface.
let multicastRequest = ip_mreq(imr_multiaddr: groupAddress.address.sin_addr, imr_interface: interfaceAddress.address.sin_addr)
try self.socket.setOption(level: CInt(IPPROTO_IP), name: operation.optionName(level: CInt(IPPROTO_IP)), value: multicastRequest)
case (.v4(let groupAddress), .none):
// IPv4 binding without target interface.
let multicastRequest = ip_mreq(imr_multiaddr: groupAddress.address.sin_addr, imr_interface: in_addr(s_addr: INADDR_ANY))
try self.socket.setOption(level: CInt(IPPROTO_IP), name: operation.optionName(level: CInt(IPPROTO_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))
try self.socket.setOption(level: CInt(IPPROTO_IPV6), name: operation.optionName(level: CInt(IPPROTO_IPV6)), value: multicastRequest)
case (.v6(let groupAddress), .none):
// IPv6 binding with no specific interface requested.
let multicastRequest = ipv6_mreq(ipv6mr_multiaddr: groupAddress.address.sin6_addr, ipv6mr_interface: 0)
try self.socket.setOption(level: CInt(IPPROTO_IPV6), name: operation.optionName(level: CInt(IPPROTO_IPV6)), value: multicastRequest)
case (.v4, .some(.v6)), (.v6, .some(.v4)), (.v4, .some(.unixDomainSocket)), (.v6, .some(.unixDomainSocket)):
// Mismatched group and interface address: this is an error.
throw MulticastError.badInterfaceAddressFamily
}
promise?.succeed(result: ())
} catch {
promise?.fail(error: error)
return
}
}
}
Oops, something went wrong.

0 comments on commit c73eb57

Please sign in to comment.