Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support UDP multicast. #618

Merged
merged 2 commits into from Sep 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions Package.swift
Expand Up @@ -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",
Expand All @@ -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"]),
Expand Down
19 changes: 18 additions & 1 deletion Sources/NIO/Channel.swift
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this also take the family as parameter (just like we did for notAMulticastAddress) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could, but it would really need both the channel address family and the provided address family to make much sense. Should we do that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah... I think that would be too much :) Ship it !


/// The address family of the provided multicast group join is not valid for this `Channel`.
case badInterfaceAddressFamily
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as above


/// 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) {
Expand Down
12 changes: 11 additions & 1 deletion Sources/NIO/Interfaces.swift
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
}

Expand All @@ -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
}
}
91 changes: 91 additions & 0 deletions Sources/NIO/MulticastChannel.swift
@@ -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
}
}
26 changes: 26 additions & 0 deletions Sources/NIO/SocketAddresses.swift
Expand Up @@ -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 }
}
}
}

120 changes: 120 additions & 0 deletions Sources/NIO/SocketChannel.swift
Expand Up @@ -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
}
}
}