1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
|
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
import NIO
import XCTest
final class SocketOptionProviderTest: XCTestCase {
var group: MultiThreadedEventLoopGroup!
var serverChannel: Channel!
var clientChannel: Channel!
var ipv4DatagramChannel: Channel!
var ipv6DatagramChannel: Channel?
struct CastError: Error { }
private func convertedChannel(file: StaticString = #file, line: UInt = #line) throws -> SocketOptionProvider {
guard let provider = self.clientChannel as? SocketOptionProvider else {
XCTFail("Unable to cast \(String(describing: self.clientChannel)) to SocketOptionProvider", file: (file), line: line)
throw CastError()
}
return provider
}
private func ipv4MulticastProvider(file: StaticString = #file, line: UInt = #line) throws -> SocketOptionProvider {
guard let provider = self.ipv4DatagramChannel as? SocketOptionProvider else {
XCTFail("Unable to cast \(String(describing: self.ipv4DatagramChannel)) to SocketOptionProvider", file: (file), line: line)
throw CastError()
}
return provider
}
private func ipv6MulticastProvider(file: StaticString = #file, line: UInt = #line) throws -> SocketOptionProvider? {
guard let ipv6Channel = self.ipv6DatagramChannel else {
return nil
}
guard let provider = ipv6Channel as? SocketOptionProvider else {
XCTFail("Unable to cast \(ipv6Channel)) to SocketOptionChannel", file: (file), line: line)
throw CastError()
}
return provider
}
override func setUp() {
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
self.serverChannel = try? assertNoThrowWithValue(ServerBootstrap(group: group).bind(host: "127.0.0.1", port: 0).wait())
self.clientChannel = try? assertNoThrowWithValue(ClientBootstrap(group: group).connect(to: serverChannel.localAddress!).wait())
// We need to join these multicast groups on the loopback interface to work around issues with rapidly joining and leaving
// many multicast groups. On some OSes, if we do that on a public interface, we can build up a kernel backlog of IGMP
// joins/leaves that may eventually lead to an ENOMEM and a spurious test failure. As joining/leaving groups on loopback
// interfaces does not require IGMP joins/leaves, forcing these joins onto the loopback interface saves us from this
// 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.enumerateDevices().filter {
$0.address == v4LoopbackAddress
}.first)!
// Only run the setup if the loopback interface supports multicast
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), device: v4LoopbackInterface).map { channel }
}.wait()
)
}
// 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.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), device: v6LoopbackInterface).map { channel }
}.wait()
}
}
override func tearDown() {
XCTAssertNoThrow(try ipv6DatagramChannel?.close().wait())
XCTAssertNoThrow(try ipv4DatagramChannel?.close().wait())
XCTAssertNoThrow(try clientChannel.close().wait())
XCTAssertNoThrow(try serverChannel.close().wait())
XCTAssertNoThrow(try group.syncShutdownGracefully())
}
func testSettingAndGettingComplexSocketOption() throws {
let provider = try assertNoThrowWithValue(self.convertedChannel())
let newTimeout = timeval(tv_sec: 5, tv_usec: 0)
let retrievedTimeout = try assertNoThrowWithValue(provider.unsafeSetSocketOption(level: .socket, name: .so_rcvtimeo, value: newTimeout).flatMap {
provider.unsafeGetSocketOption(level: .socket, name: .so_rcvtimeo) as EventLoopFuture<timeval>
}.wait())
XCTAssertEqual(retrievedTimeout.tv_sec, newTimeout.tv_sec)
XCTAssertEqual(retrievedTimeout.tv_usec, newTimeout.tv_usec)
}
func testObtainingDefaultValueOfComplexSocketOption() throws {
let provider = try assertNoThrowWithValue(self.convertedChannel())
let retrievedTimeout: timeval = try assertNoThrowWithValue(provider.unsafeGetSocketOption(level: .socket, name: .so_rcvtimeo).wait())
XCTAssertEqual(retrievedTimeout.tv_sec, 0)
XCTAssertEqual(retrievedTimeout.tv_usec, 0)
}
func testSettingAndGettingSimpleSocketOption() throws {
let provider = try assertNoThrowWithValue(self.convertedChannel())
let newReuseAddr = 1 as CInt
let retrievedReuseAddr = try assertNoThrowWithValue(provider.unsafeSetSocketOption(level: .socket, name: .so_reuseaddr, value: newReuseAddr).flatMap {
provider.unsafeGetSocketOption(level: .socket, name: .so_reuseaddr) as EventLoopFuture<CInt>
}.wait())
XCTAssertNotEqual(retrievedReuseAddr, 0)
}
func testObtainingDefaultValueOfSimpleSocketOption() throws {
let provider = try assertNoThrowWithValue(self.convertedChannel())
let reuseAddr: CInt = try assertNoThrowWithValue(provider.unsafeGetSocketOption(level: .socket, name: .so_reuseaddr).wait())
XCTAssertEqual(reuseAddr, 0)
}
func testPassingInvalidSizeToSetComplexSocketOptionFails() throws {
// You'll notice that there are no other size mismatch tests in this file. The reason for that is that
// setsockopt is pretty dumb, and getsockopt is dumber. Specifically, setsockopt checks only that the length
// of the option value is *at least as large* as the expected struct (which is why this test will actually
// work), and getsockopt will happily return without error even in the buffer is too small. Either way,
// we just abandon the other tests: this is sufficient to prove that the error path works.
let provider = try assertNoThrowWithValue(self.convertedChannel())
XCTAssertThrowsError(try provider.unsafeSetSocketOption(level: .socket, name: .so_rcvtimeo, value: 1).wait()) { error in
XCTAssertEqual(EINVAL, (error as? IOError)?.errnoCode)
}
}
// MARK: Tests for the safe helper functions.
func testLinger() throws {
let newLingerValue = linger(l_onoff: 1, l_linger: 64)
let provider = try self.convertedChannel()
XCTAssertNoThrow(try provider.setSoLinger(newLingerValue).flatMap {
provider.getSoLinger()
}.map {
XCTAssertEqual($0.l_linger, newLingerValue.l_linger)
XCTAssertEqual($0.l_onoff, newLingerValue.l_onoff)
}.wait())
}
func testSoIpMulticastIf() throws {
guard let channel = self.ipv4DatagramChannel else {
// no multicast support
return
}
let provider = try assertNoThrowWithValue(self.ipv4MulticastProvider())
let address: in_addr
switch channel.localAddress {
case .some(.v4(let addr)):
address = addr.address.sin_addr
default:
XCTFail("Local address must be IPv4, but is \(channel.localAddress.debugDescription)")
return
}
XCTAssertNoThrow(try provider.setIPMulticastIF(address).flatMap {
provider.getIPMulticastIF()
}.map {
XCTAssertEqual($0.s_addr, address.s_addr)
}.wait())
}
func testIpMulticastTtl() throws {
guard self.ipv4DatagramChannel != nil else {
// alas, no multicast, let's skip.
return
}
let provider = try assertNoThrowWithValue(self.ipv4MulticastProvider())
XCTAssertNoThrow(try provider.setIPMulticastTTL(6).flatMap {
provider.getIPMulticastTTL()
}.map {
XCTAssertEqual($0, 6)
}.wait())
}
func testIpMulticastLoop() throws {
guard self.ipv4DatagramChannel != nil else {
// alas, no multicast, let's skip.
return
}
let provider = try assertNoThrowWithValue(self.ipv4MulticastProvider())
XCTAssertNoThrow(try provider.setIPMulticastLoop(1).flatMap {
provider.getIPMulticastLoop()
}.map {
XCTAssertNotEqual($0, 0)
}.wait())
}
func testIpv6MulticastIf() throws {
guard let provider = try assertNoThrowWithValue(self.ipv6MulticastProvider()) else {
// Skip on systems without IPv6.
return
}
guard self.ipv6DatagramChannel != nil else {
// alas, no multicast, let's skip.
return
}
// 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.enumerateDevices().filter({ $0.address == loopbackAddress }).first) else {
XCTFail("Could not find index of loopback address")
return
}
XCTAssertNoThrow(try provider.setIPv6MulticastIF(CUnsignedInt(loopbackInterface.interfaceIndex)).flatMap {
provider.getIPv6MulticastIF()
}.map {
XCTAssertEqual($0, CUnsignedInt(loopbackInterface.interfaceIndex))
}.wait())
}
func testIPv6MulticastHops() throws {
guard let provider = try assertNoThrowWithValue(self.ipv6MulticastProvider()) else {
// Skip on systems without IPv6.
return
}
guard self.ipv6DatagramChannel != nil else {
// alas, no multicast, let's skip.
return
}
XCTAssertNoThrow(try provider.setIPv6MulticastHops(6).flatMap {
provider.getIPv6MulticastHops()
}.map {
XCTAssertEqual($0, 6)
}.wait())
}
func testIPv6MulticastLoop() throws {
guard let provider = try assertNoThrowWithValue(self.ipv6MulticastProvider()) else {
// Skip on systems without IPv6.
return
}
guard self.ipv6DatagramChannel != nil else {
// alas, no multicast, let's skip.
return
}
XCTAssertNoThrow(try provider.setIPv6MulticastLoop(1).flatMap {
provider.getIPv6MulticastLoop()
}.map {
XCTAssertNotEqual($0, 0)
}.wait())
}
func testTCPInfo() throws {
// This test only runs on Linux, FreeBSD, and Android.
#if os(Linux) || os(FreeBSD) || os(Android)
let channel = self.clientChannel! as! SocketOptionProvider
let tcpInfo = try assertNoThrowWithValue(channel.getTCPInfo().wait())
// We just need to soundness check something here to ensure that the data is vaguely reasonable.
XCTAssertEqual(tcpInfo.tcpi_state, UInt8(TCP_ESTABLISHED))
#endif
}
func testTCPConnectionInfo() throws {
// This test only runs on Darwin.
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
let channel = self.clientChannel! as! SocketOptionProvider
let tcpConnectionInfo = try assertNoThrowWithValue(channel.getTCPConnectionInfo().wait())
#if os(macOS) // deliberately only on macOS
// We just need to soundness check something here to ensure that the data is vaguely reasonable.
XCTAssertEqual(tcpConnectionInfo.tcpi_state, UInt8(TSI_S_ESTABLISHED))
#endif
#endif
}
}
|