├── README.md ├── .gitignore ├── Sources ├── Dribble │ ├── Errors.swift │ ├── TurnChannels.swift │ ├── Heplers.swift │ ├── TurnClient.swift │ ├── StunClient.swift │ ├── StunInboundHandler.swift │ ├── TurnAllocation.swift │ └── STUN.swift └── CLIExample │ └── CLI.swift ├── Coturn └── docker-compose.yml ├── Package.resolved ├── Package.swift ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ ├── Dribble.xcscheme │ ├── CLIExample.xcscheme │ └── Dribble-Package.xcscheme └── Tests └── DribbleTests └── DribbleTests.swift /README.md: -------------------------------------------------------------------------------- 1 | # Dribble 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .vscode/ 9 | -------------------------------------------------------------------------------- /Sources/Dribble/Errors.swift: -------------------------------------------------------------------------------- 1 | enum StunClientError: Error { 2 | case queryFailed 3 | } 4 | 5 | enum TurnClientError: Error { 6 | case createPermissionFailure 7 | } 8 | 9 | enum TurnChannelError: Error { 10 | case operationUnsupported 11 | } 12 | -------------------------------------------------------------------------------- /Coturn/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | 3 | services: 4 | coturn: 5 | image: bloodhunterd/coturn 6 | environment: 7 | REALM: example.com 8 | SECRET: password 9 | hostname: coturn 10 | ports: 11 | - '3478:3478' 12 | - '5349:5349' 13 | volumes: 14 | - ./cert.pem:/etc/ssl/private/cert.pem:ro 15 | - ./key.pem:/etc/ssl/private/key.pem:ro 16 | - ./dhparams.pem:/etc/ssl/private/dhparams.pem:ro 17 | - ./turndb:/var/lib/turn/turndb 18 | -------------------------------------------------------------------------------- /Sources/Dribble/TurnChannels.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | 3 | struct TurnChannelData { 4 | // 0x4000 through 0x4FFF 5 | let channelNumber: ChannelNumber 6 | var length: UInt16 { 7 | UInt16(applicationData.readableBytes) 8 | } 9 | var applicationData: ByteBuffer 10 | } 11 | 12 | public struct ChannelNumber: RawRepresentable { 13 | public let rawValue: UInt16 14 | 15 | public init(rawValue: UInt16) { 16 | self.rawValue = rawValue 17 | } 18 | 19 | public init() { 20 | self.rawValue = .random(in: 0x4000 ... 0x4FFF) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Dribble/Heplers.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | 3 | final class EnvelopToByteBufferConverter: ChannelInboundHandler { 4 | public typealias InboundIn = AddressedEnvelope 5 | public typealias InboundOut = ByteBuffer 6 | public typealias ErrorHandler = ((Error) -> ())? 7 | 8 | private let errorHandler: ErrorHandler 9 | 10 | init(errorHandler: ErrorHandler) { 11 | self.errorHandler = errorHandler 12 | } 13 | 14 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 15 | let envelope = self.unwrapInboundIn(data) 16 | let byteBuffer = envelope.data 17 | context.fireChannelRead(self.wrapInboundOut(byteBuffer)) 18 | } 19 | 20 | public func errorCaught(context: ChannelHandlerContext, error: Error) { 21 | errorHandler?(error) 22 | context.close(promise: nil) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Dribble/TurnClient.swift: -------------------------------------------------------------------------------- 1 | import NIOConcurrencyHelpers 2 | import _NIOConcurrency 3 | import NIO 4 | 5 | public final class TurnClient: StunClient { 6 | public func requestAllocation() async throws -> TurnAllocation { 7 | let message = try await sendMessage(.allocationRequest()) 8 | 9 | guard let relayedAddressAttribute = message.attributes.first(where: { attribute in 10 | return attribute.type == StunAttributeType.xorRelayedAddress.rawValue 11 | }) else { 12 | throw StunClientError.queryFailed 13 | } 14 | 15 | switch try relayedAddressAttribute.resolve(forTransaction: message.header.transactionId) { 16 | case .xorRelayedAddress(let address): 17 | return TurnAllocation( 18 | ourAddress: address, 19 | client: self 20 | ) 21 | default: 22 | throw StunClientError.queryFailed 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-atomics", 6 | "repositoryURL": "https://github.com/apple/swift-atomics.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "cd142fd2f64be2100422d658e7411e39489da985", 10 | "version": "1.2.0" 11 | } 12 | }, 13 | { 14 | "package": "swift-collections", 15 | "repositoryURL": "https://github.com/apple/swift-collections.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "a902f1823a7ff3c9ab2fba0f992396b948eda307", 19 | "version": "1.0.5" 20 | } 21 | }, 22 | { 23 | "package": "swift-crypto", 24 | "repositoryURL": "https://github.com/apple/swift-crypto.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "d9825fa541df64b1a7b182178d61b9a82730d01f", 28 | "version": "2.1.0" 29 | } 30 | }, 31 | { 32 | "package": "swift-nio", 33 | "repositoryURL": "https://github.com/apple/swift-nio.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "54c85cb26308b89846d4671f23954dce088da2b0", 37 | "version": "2.60.0" 38 | } 39 | } 40 | ] 41 | }, 42 | "version": 1 43 | } 44 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Dribble", 8 | platforms: [ 9 | .macOS(.v12), 10 | .iOS(.v15), 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "Dribble", 16 | targets: ["Dribble"]), 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.60.0"), 20 | .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"3.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "Dribble", 27 | dependencies: [ 28 | .product(name: "NIO", package: "swift-nio"), 29 | .product(name: "_NIOConcurrency", package: "swift-nio"), 30 | .product(name: "NIOFoundationCompat", package: "swift-nio"), 31 | .product(name: "Crypto", package: "swift-crypto"), 32 | ]), 33 | .executableTarget(name: "CLIExample", dependencies: [ 34 | "Dribble" 35 | ]), 36 | .testTarget( 37 | name: "DribbleTests", 38 | dependencies: ["Dribble"]), 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /Sources/Dribble/StunClient.swift: -------------------------------------------------------------------------------- 1 | import NIOConcurrencyHelpers 2 | import _NIOConcurrency 3 | import NIO 4 | 5 | public class StunClient { 6 | let channel: Channel 7 | let sender: StunMessageSender 8 | 9 | internal required init(channel: Channel, sender: StunMessageSender) { 10 | self.channel = channel 11 | self.sender = sender 12 | } 13 | 14 | public static func connect(to address: SocketAddress) async throws -> Self { 15 | let sender = StunInboundHandler(remoteAddress: address) 16 | let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) 17 | let channel = try await DatagramBootstrap(group: elg) 18 | .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 19 | .channelInitializer { channel in 20 | channel.pipeline.addHandlers( 21 | EnvelopToByteBufferConverter { _ in 22 | _ = channel.close() 23 | }, 24 | ByteToMessageHandler(StunParser()), 25 | sender 26 | ) 27 | }.bind( 28 | host: address.protocol == .inet ? "0.0.0.0" : "::", 29 | port: 0 30 | ).get() 31 | 32 | return Self.init(channel: channel, sender: sender) 33 | } 34 | 35 | internal func sendMessage(_ message: StunMessage) async throws -> StunMessage { 36 | try await sender.sendMessage(message, on: channel) 37 | } 38 | 39 | public func requestBinding(addressFamily: AddressFamily) async throws -> SocketAddress { 40 | let message = try await sendMessage(.bindingRequest(with: addressFamily)) 41 | 42 | guard let addressAttribute = message.attributes.first(where: { attribute in 43 | switch attribute.type { 44 | case StunAttributeType.mappedAddress.rawValue: 45 | return true 46 | case StunAttributeType.xorMappedAddress.rawValue: 47 | return true 48 | default: 49 | return false 50 | } 51 | }) else { 52 | throw StunClientError.queryFailed 53 | } 54 | 55 | switch try addressAttribute.resolve(forTransaction: message.header.transactionId) { 56 | case .mappedAddress(let address), .xorMappedAddress(let address): 57 | return address 58 | default: 59 | throw StunClientError.queryFailed 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/CLIExample/CLI.swift: -------------------------------------------------------------------------------- 1 | import Dribble 2 | import NIO 3 | 4 | final class EnvelopToByteBufferConverter: ChannelInboundHandler { 5 | public typealias InboundIn = AddressedEnvelope 6 | public typealias InboundOut = ByteBuffer 7 | public typealias ErrorHandler = ((Error) -> ())? 8 | 9 | private let errorHandler: ErrorHandler 10 | 11 | init(errorHandler: ErrorHandler) { 12 | self.errorHandler = errorHandler 13 | } 14 | 15 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 16 | let envelope = self.unwrapInboundIn(data) 17 | let byteBuffer = envelope.data 18 | context.fireChannelRead(self.wrapInboundOut(byteBuffer)) 19 | } 20 | 21 | public func errorCaught(context: ChannelHandlerContext, error: Error) { 22 | errorHandler?(error) 23 | context.close(promise: nil) 24 | } 25 | } 26 | 27 | struct PrintHandler: ByteToMessageDecoder { 28 | typealias InboundOut = Never 29 | 30 | func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState { 31 | let string = buffer.readString(length: buffer.readableBytes) ?? "error" 32 | print(string) 33 | 34 | return .continue 35 | } 36 | 37 | func decodeLast(context: ChannelHandlerContext, buffer: inout ByteBuffer, seenEOF: Bool) throws -> DecodingState { 38 | try decode(context: context, buffer: &buffer) 39 | } 40 | } 41 | 42 | @main 43 | struct CLI { 44 | static func main() async throws { 45 | let client = try await TurnClient.connect( 46 | to: SocketAddress.makeAddressResolvingHost("10.211.55.4", port: 3478) 47 | ) 48 | 49 | let myAddress = try await client.requestBinding(addressFamily: .ipv4) 50 | let allocation = try await client.requestAllocation() 51 | 52 | // Normally the other client should also find their address 53 | // We can skip that here, because it's the same address 54 | 55 | let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) 56 | let proxyTargettedChannel = try await DatagramBootstrap(group: elg) 57 | .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 58 | .channelInitializer { channel in 59 | channel.eventLoop.makeSucceededVoidFuture() 60 | }.bind( 61 | host: myAddress.protocol == .inet ? "0.0.0.0" : "::", 62 | port: 0 63 | ).get() 64 | 65 | var theirAddress = myAddress 66 | theirAddress.port = proxyTargettedChannel.localAddress?.port 67 | let allocationChannel = try await allocation.createChannel(for: theirAddress) 68 | try await allocationChannel.pipeline.addHandler(ByteToMessageHandler(PrintHandler())) 69 | try await proxyTargettedChannel.pipeline.addHandlers( 70 | EnvelopToByteBufferConverter { _ in }, 71 | ByteToMessageHandler(PrintHandler()) 72 | ) 73 | 74 | try await proxyTargettedChannel.writeAndFlush( 75 | AddressedEnvelope( 76 | remoteAddress: allocation.ourAddress, 77 | data: ByteBuffer(string: "Hello") 78 | ) 79 | ) 80 | 81 | try await allocationChannel.writeAndFlush(ByteBuffer(string: "Test")) 82 | 83 | sleep(5) 84 | try await proxyTargettedChannel.close() 85 | try await allocationChannel.close() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/Dribble.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Sources/Dribble/StunInboundHandler.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | import _NIOConcurrency 3 | 4 | protocol StunMessageSender { 5 | func sendMessage(_ message: StunMessage, on channel: Channel) async throws -> StunMessage 6 | func registerTurnAllocationChannel(_ channel: TurnAllocationChannel, theirAddress: SocketAddress) async throws 7 | } 8 | 9 | final class StunInboundHandler: ChannelInboundHandler, StunMessageSender { 10 | public typealias InboundIn = StunMessage 11 | public typealias OutboundOut = AddressedEnvelope 12 | 13 | let remoteAddress: SocketAddress 14 | var queries = [StunTransactionId: EventLoopPromise]() 15 | var allocations = [(SocketAddress, Channel)]() 16 | 17 | init(remoteAddress: SocketAddress) { 18 | self.remoteAddress = remoteAddress 19 | } 20 | 21 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 22 | let message = self.unwrapInboundIn(data) 23 | 24 | if message.header.type == .dataIndication { 25 | do { 26 | if 27 | let data = message.attributes.first(where: { $0.stunType == .data }), 28 | let origin = message.attributes.first(where: { $0.stunType == .xorPeerAddress }), 29 | case .data(let buffer) = try data.resolve(forTransaction: message.header.transactionId), 30 | case .xorPeerAddress(let address) = try origin.resolve(forTransaction: message.header.transactionId) 31 | { 32 | let allocation = allocations.first(where: { allocation in 33 | switch (allocation.0, address) { 34 | case (.v4(let lhs), .v4(let rhs)): 35 | return lhs.address.sin_addr.s_addr == rhs.address.sin_addr.s_addr 36 | case (.v6(let lhs), .v6(let rhs)): 37 | #if swift(>=5.5) && os(Linux) 38 | return lhs.address.sin6_addr.__in6_u.__u6_addr32 == rhs.address.sin6_addr.__in6_u.__u6_addr32 39 | #else 40 | return lhs.address.sin6_addr.__u6_addr.__u6_addr32 == rhs.address.sin6_addr.__u6_addr.__u6_addr32 41 | #endif 42 | case (.v4, _), (.v6, _), (.unixDomainSocket, _), 43 | (_, .v4), (_, .v6), (_, .unixDomainSocket): 44 | return false 45 | } 46 | }) 47 | allocation?.1.pipeline.fireChannelRead(NIOAny(buffer)) 48 | } 49 | } catch { 50 | print(error) 51 | } 52 | } else if let query = queries.removeValue(forKey: message.header.transactionId) { 53 | query.succeed(message) 54 | } 55 | } 56 | 57 | public func errorCaught(context: ChannelHandlerContext, error: Error) { 58 | for query in queries.values { 59 | query.fail(error) 60 | } 61 | 62 | queries.removeAll() 63 | allocations.removeAll() 64 | context.close(promise: nil) 65 | } 66 | 67 | func registerTurnAllocationChannel(_ channel: TurnAllocationChannel, theirAddress: SocketAddress) async throws { 68 | allocations.append((theirAddress, channel)) 69 | } 70 | 71 | func sendMessage(_ message: StunMessage, on channel: Channel) async throws -> StunMessage { 72 | var data = ByteBuffer() 73 | data.writeStunMessage(message) 74 | let promise = channel.eventLoop.makePromise(of: StunMessage.self) 75 | self.queries[message.header.transactionId] = promise 76 | return try await channel.writeAndFlush( 77 | AddressedEnvelope( 78 | remoteAddress: remoteAddress, 79 | data: data 80 | ) 81 | ).flatMap { 82 | promise.futureResult 83 | }.get() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/CLIExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 69 | 75 | 76 | 77 | 78 | 84 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /Tests/DribbleTests/DribbleTests.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | import XCTest 3 | @testable import Dribble 4 | import NIOPosix 5 | 6 | final class DribbleTests: XCTestCase { 7 | func testExample() throws { 8 | let remoteAddress = try SocketAddress.makeAddressResolvingHost("stun.l.google.com", port: 19302) 9 | let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) 10 | let server = try DatagramBootstrap(group: elg) 11 | .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 12 | .channelInitializer { channel in 13 | channel.pipeline.addHandlers([ 14 | EnvelopToByteBufferConverter { _ in 15 | _ = channel.close() 16 | }, 17 | ByteToMessageHandler(StunParser()), 18 | 19 | StunInboundHandler(errorHandler: { error in 20 | XCTFail() 21 | }, attributesHandler: { message in 22 | for attribute in message.attributes { 23 | var value = attribute.value 24 | guard let type = StunAttributeType(rawValue: attribute.type) else { 25 | continue 26 | } 27 | let attribute = try! ResolvedStunAttribute( 28 | type: type, 29 | transactionId: message.header.transactionId, 30 | buffer: &value 31 | ) 32 | print(attribute) 33 | } 34 | print(message) 35 | }) 36 | ]) 37 | }.bind(host: remoteAddress.protocol == .inet ? "0.0.0.0" : "::", port: 14135).wait() 38 | 39 | do { 40 | let message = StunMessage.bindingRequest(with: .ipv6) 41 | var buffer = ByteBuffer() 42 | buffer.writeStunMessage(message) 43 | let envelope = AddressedEnvelope(remoteAddress: remoteAddress, data: buffer) 44 | try server.writeAndFlush(envelope).wait() 45 | } 46 | 47 | sleep(3) 48 | 49 | do { 50 | let message = StunMessage.allocationRequest() 51 | var buffer = ByteBuffer() 52 | buffer.writeStunMessage(message) 53 | let envelope = AddressedEnvelope(remoteAddress: remoteAddress, data: buffer) 54 | try server.writeAndFlush(envelope).wait() 55 | } 56 | 57 | try server.close().wait() 58 | try elg.syncShutdownGracefully() 59 | } 60 | } 61 | 62 | final class EnvelopToByteBufferConverter: ChannelInboundHandler { 63 | public typealias InboundIn = AddressedEnvelope 64 | public typealias InboundOut = ByteBuffer 65 | public typealias ErrorHandler = ((Error) -> ())? 66 | 67 | private let errorHandler: ErrorHandler 68 | 69 | init(errorHandler: ErrorHandler) { 70 | self.errorHandler = errorHandler 71 | } 72 | 73 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 74 | let envelope = self.unwrapInboundIn(data) 75 | let byteBuffer = envelope.data 76 | context.fireChannelRead(self.wrapInboundOut(byteBuffer)) 77 | } 78 | 79 | public func errorCaught(context: ChannelHandlerContext, error: Error) { 80 | errorHandler?(error) 81 | context.close(promise: nil) 82 | } 83 | } 84 | 85 | final class StunInboundHandler: ChannelInboundHandler { 86 | public typealias InboundIn = StunMessage 87 | public typealias OutboundOut = AddressedEnvelope 88 | public typealias ErrorHandler = ((Error) -> ())? 89 | 90 | private let errorHandler: ErrorHandler 91 | private let attributesHandler: (StunMessage) -> () 92 | 93 | init(errorHandler: ErrorHandler, attributesHandler: @escaping (StunMessage) -> ()) { 94 | self.errorHandler = errorHandler 95 | self.attributesHandler = attributesHandler 96 | } 97 | 98 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 99 | attributesHandler(self.unwrapInboundIn(data)) 100 | } 101 | 102 | public func errorCaught(context: ChannelHandlerContext, error: Error) { 103 | errorHandler?(error) 104 | context.close(promise: nil) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/Dribble-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 81 | 82 | 88 | 89 | 90 | 91 | 97 | 98 | 104 | 105 | 106 | 107 | 109 | 110 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /Sources/Dribble/TurnAllocation.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | 3 | public struct TurnAllocation { 4 | public let ourAddress: SocketAddress 5 | internal let client: TurnClient 6 | 7 | public func createChannel(for theirAddress: SocketAddress) async throws -> Channel { 8 | let transactionId = StunTransactionId() 9 | var xorPeerAddress = ByteBuffer() 10 | xorPeerAddress.writeSocketAddress(theirAddress, xor: true) 11 | let response = try await client.sendMessage( 12 | StunMessage( 13 | type: .createPermission, 14 | transactionId: transactionId, 15 | attributes: [ 16 | .init( 17 | type: .xorPeerAddress, 18 | value: xorPeerAddress 19 | ) 20 | ] 21 | ) 22 | ) 23 | 24 | guard response.header.type == .createPermissionSuccess else { 25 | throw TurnClientError.createPermissionFailure 26 | } 27 | 28 | let channel = TurnAllocationChannel( 29 | client: client, 30 | allocationAddress: theirAddress 31 | ) 32 | 33 | try await client.sender.registerTurnAllocationChannel(channel, theirAddress: theirAddress) 34 | 35 | return channel 36 | } 37 | } 38 | 39 | final class TurnAllocationChannel: Channel, ChannelCore { 40 | internal let client: TurnClient 41 | internal let allocationAddress: SocketAddress 42 | public let allocator: ByteBufferAllocator 43 | private var _pipeline: ChannelPipeline! 44 | 45 | init(client: TurnClient, allocationAddress: SocketAddress) { 46 | self.client = client 47 | self.allocationAddress = allocationAddress 48 | self.allocator = client.channel.allocator 49 | self._pipeline = ChannelPipeline(channel: self) 50 | } 51 | 52 | public var closeFuture: EventLoopFuture { 53 | client.channel.closeFuture 54 | } 55 | 56 | public var pipeline: ChannelPipeline { 57 | _pipeline 58 | } 59 | 60 | public var localAddress: SocketAddress? { parent?.localAddress } 61 | public var remoteAddress: SocketAddress? { parent?.remoteAddress } 62 | 63 | public var parent: Channel? { 64 | client.channel 65 | } 66 | 67 | public let isWritable = true 68 | public let isActive = true 69 | 70 | public var _channelCore: ChannelCore { self } 71 | 72 | public func localAddress0() throws -> SocketAddress { 73 | try client.channel._channelCore.localAddress0() 74 | } 75 | 76 | public func remoteAddress0() throws -> SocketAddress { 77 | try client.channel._channelCore.remoteAddress0() 78 | } 79 | 80 | public func setOption