From 5f0176cbf1290916582d2c8c8a5224680a0356d3 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Tue, 22 Nov 2022 15:02:28 +0100 Subject: [PATCH 01/12] Add `RawSocketBootsrap` --- Sources/NIOCore/BSDSocketAPI.swift | 7 + Sources/NIOPosix/BSDSocketAPICommon.swift | 8 + Sources/NIOPosix/RawSocketBootstrap.swift | 197 ++++++++++++++++++ Sources/NIOPosix/Socket.swift | 2 +- .../NIOPosixTests/DatagramChannelTests.swift | 4 +- .../RawSocketBootstrapTests.swift | 60 ++++++ 6 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 Sources/NIOPosix/RawSocketBootstrap.swift create mode 100644 Tests/NIOPosixTests/RawSocketBootstrapTests.swift diff --git a/Sources/NIOCore/BSDSocketAPI.swift b/Sources/NIOCore/BSDSocketAPI.swift index 37764986b05..23cf4d02301 100644 --- a/Sources/NIOCore/BSDSocketAPI.swift +++ b/Sources/NIOCore/BSDSocketAPI.swift @@ -267,6 +267,13 @@ extension NIOBSDSocket.Option { /// Control multicast time-to-live. public static let ip_multicast_ttl: NIOBSDSocket.Option = NIOBSDSocket.Option(rawValue: IP_MULTICAST_TTL) + + /// The IPv4 layer generates an IP header when sending a packet + /// unless the ``ip_hdrincl`` socket option is enabled on the socket. + /// When it is enabled, the packet must contain an IP header. For + /// receiving, the IP header is always included in the packet. + public static let ip_hdrincl: NIOBSDSocket.Option = + NIOBSDSocket.Option(rawValue: IP_HDRINCL) } // IPv6 Options diff --git a/Sources/NIOPosix/BSDSocketAPICommon.swift b/Sources/NIOPosix/BSDSocketAPICommon.swift index 01c9219828d..087ee30a711 100644 --- a/Sources/NIOPosix/BSDSocketAPICommon.swift +++ b/Sources/NIOPosix/BSDSocketAPICommon.swift @@ -83,6 +83,14 @@ extension NIOBSDSocket.SocketType { internal static let stream: NIOBSDSocket.SocketType = NIOBSDSocket.SocketType(rawValue: SOCK_STREAM) #endif + + #if os(Linux) + internal static let raw: NIOBSDSocket.SocketType = + NIOBSDSocket.SocketType(rawValue: CInt(SOCK_RAW.rawValue)) + #else + internal static let raw: NIOBSDSocket.SocketType = + NIOBSDSocket.SocketType(rawValue: SOCK_RAW) + #endif } // IPv4 Options diff --git a/Sources/NIOPosix/RawSocketBootstrap.swift b/Sources/NIOPosix/RawSocketBootstrap.swift new file mode 100644 index 00000000000..5ea6ea891cc --- /dev/null +++ b/Sources/NIOPosix/RawSocketBootstrap.swift @@ -0,0 +1,197 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2022 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 NIOCore + +/// A `RawSocketBootstrap` is an easy way to interact with IP based protocols other then TCP and UDP. +/// +/// Example: +/// +/// ```swift +/// let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) +/// defer { +/// try! group.syncShutdownGracefully() +/// } +/// let bootstrap = RawSocketBootstrap(group: group) +/// .channelInitializer { channel in +/// channel.pipeline.addHandler(MyChannelHandler()) +/// } +/// let channel = try! bootstrap.bind(host: "127.0.0.1", ipProtocol: .icmp).wait() +/// /* the Channel is now ready to send/receive IP packets */ +/// +/// try channel.closeFuture.wait() // Wait until the channel un-binds. +/// ``` +/// +/// The `Channel` will operate on `AddressedEnvelope` as inbound and outbound messages. +public final class RawSocketBootstrap { + + private let group: EventLoopGroup + private var channelInitializer: Optional + @usableFromInline + internal var _channelOptions: ChannelOptions.Storage + + /// Create a `RawSocketBootstrap` on the `EventLoopGroup` `group`. + /// + /// The `EventLoopGroup` `group` must be compatible, otherwise the program will crash. `RawSocketBootstrap` is + /// compatible only with `MultiThreadedEventLoopGroup` as well as the `EventLoop`s returned by + /// `MultiThreadedEventLoopGroup.next`. See `init(validatingGroup:)` for a fallible initializer for + /// situations where it's impossible to tell ahead of time if the `EventLoopGroup` is compatible or not. + /// + /// - parameters: + /// - group: The `EventLoopGroup` to use. + public convenience init(group: EventLoopGroup) { + guard NIOOnSocketsBootstraps.isCompatible(group: group) else { + preconditionFailure("RawSocketBootstrap is only compatible with MultiThreadedEventLoopGroup and " + + "SelectableEventLoop. You tried constructing one with \(group) which is incompatible.") + } + self.init(validatingGroup: group)! + } + + /// Create a `RawSocketBootstrap` on the `EventLoopGroup` `group`, validating that `group` is compatible. + /// + /// - parameters: + /// - group: The `EventLoopGroup` to use. + public init?(validatingGroup group: EventLoopGroup) { + guard NIOOnSocketsBootstraps.isCompatible(group: group) else { + return nil + } + self._channelOptions = ChannelOptions.Storage() + self.group = group + self.channelInitializer = nil + } + + /// Initialize the bound `Channel` with `initializer`. The most common task in initializer is to add + /// `ChannelHandler`s to the `ChannelPipeline`. + /// + /// - parameters: + /// - handler: A closure that initializes the provided `Channel`. + public func channelInitializer(_ handler: @escaping @Sendable (Channel) -> EventLoopFuture) -> Self { + self.channelInitializer = handler + return self + } + + /// Specifies a `ChannelOption` to be applied to the `Channel`. + /// + /// - parameters: + /// - option: The option to be applied. + /// - value: The value for the option. + @inlinable + public func channelOption(_ option: Option, value: Option.Value) -> Self { + self._channelOptions.append(key: option, value: value) + return self + } + + /// Bind the `Channel` to `host`. + /// All packets or errors matching the `ipProtocol` specified are passed to the resulting `Channel`. + /// + /// - parameters: + /// - host: The host to bind on. + /// - ipProtocol: The IP protocol used in the IP protocol/nextHeader field. + public func bind(host: String, ipProtocol: NIOIPProtocol) -> EventLoopFuture { + return bind0(ipProtocol: ipProtocol) { + return try SocketAddress.makeAddressResolvingHost(host, port: 0) + } + } + + private func bind0(ipProtocol: NIOIPProtocol, _ makeSocketAddress: () throws -> SocketAddress) -> EventLoopFuture { + let address: SocketAddress + do { + address = try makeSocketAddress() + } catch { + return group.next().makeFailedFuture(error) + } + precondition(address.port == nil || address.port == 0, "port must be 0 or not set") + func makeChannel(_ eventLoop: SelectableEventLoop) throws -> DatagramChannel { + return try DatagramChannel(eventLoop: eventLoop, + protocolFamily: address.protocol, + protocolSubtype: .init(ipProtocol), + socketType: .raw) + } + return withNewChannel(makeChannel: makeChannel) { (eventLoop, channel) in + channel.register().flatMap { + channel.bind(to: address) + } + } + } + + /// Connect the `Channel` to `host`. + /// + /// - parameters: + /// - host: The host to connect to. + /// - ipProtocol: The IP protocol used in the IP protocol/nextHeader field. + public func connect(host: String, ipProtocol: NIOIPProtocol) -> EventLoopFuture { + return connect0(ipProtocol: ipProtocol) { + return try SocketAddress.makeAddressResolvingHost(host, port: 0) + } + } + + private func connect0(ipProtocol: NIOIPProtocol, _ makeSocketAddress: () throws -> SocketAddress) -> EventLoopFuture { + let address: SocketAddress + do { + address = try makeSocketAddress() + } catch { + return group.next().makeFailedFuture(error) + } + func makeChannel(_ eventLoop: SelectableEventLoop) throws -> DatagramChannel { + return try DatagramChannel(eventLoop: eventLoop, + protocolFamily: address.protocol, + protocolSubtype: .init(ipProtocol), + socketType: .raw) + } + return withNewChannel(makeChannel: makeChannel) { (eventLoop, channel) in + channel.register().flatMap { + channel.connect(to: address) + } + } + } + + private func withNewChannel(makeChannel: (_ eventLoop: SelectableEventLoop) throws -> DatagramChannel, _ bringup: @escaping (EventLoop, DatagramChannel) -> EventLoopFuture) -> EventLoopFuture { + let eventLoop = self.group.next() + let channelInitializer = self.channelInitializer ?? { _ in eventLoop.makeSucceededFuture(()) } + let channelOptions = self._channelOptions + + let channel: DatagramChannel + do { + channel = try makeChannel(eventLoop as! SelectableEventLoop) + } catch { + return eventLoop.makeFailedFuture(error) + } + + func setupChannel() -> EventLoopFuture { + eventLoop.assertInEventLoop() + return channelOptions.applyAllChannelOptions(to: channel).flatMap { + channelInitializer(channel) + }.flatMap { + eventLoop.assertInEventLoop() + return bringup(eventLoop, channel) + }.map { + channel + }.flatMapError { error in + eventLoop.makeFailedFuture(error) + } + } + + if eventLoop.inEventLoop { + return setupChannel() + } else { + return eventLoop.flatSubmit { + setupChannel() + } + } + } +} + +#if swift(>=5.6) +@available(*, unavailable) +extension RawSocketBootstrap: Sendable {} +#endif diff --git a/Sources/NIOPosix/Socket.swift b/Sources/NIOPosix/Socket.swift index da9f4a0e58e..e55cab83520 100644 --- a/Sources/NIOPosix/Socket.swift +++ b/Sources/NIOPosix/Socket.swift @@ -32,7 +32,7 @@ typealias IOVector = iovec /// - parameters: /// - protocolFamily: The protocol family to use (usually `AF_INET6` or `AF_INET`). /// - type: The type of the socket to create. - /// - protocolSubtype: The subtype of the protocol, corresponding to the `protocol` + /// - protocolSubtype: The subtype of the protocol, corresponding to the `protocolSubtype` /// argument to the socket syscall. Defaults to 0. /// - setNonBlocking: Set non-blocking mode on the socket. /// - throws: An `IOError` if creation of the socket failed. diff --git a/Tests/NIOPosixTests/DatagramChannelTests.swift b/Tests/NIOPosixTests/DatagramChannelTests.swift index a0d3941f80c..8c1b8e4ead0 100644 --- a/Tests/NIOPosixTests/DatagramChannelTests.swift +++ b/Tests/NIOPosixTests/DatagramChannelTests.swift @@ -17,7 +17,7 @@ import NIOCore @testable import NIOPosix import XCTest -private extension Channel { +extension Channel { func waitForDatagrams(count: Int) throws -> [AddressedEnvelope] { return try self.pipeline.context(name: "ByteReadRecorder").flatMap { context in if let future = (context.handler as? DatagramReadRecorder)?.notifyForDatagrams(count) { @@ -47,7 +47,7 @@ private extension Channel { /// A class that records datagrams received and forwards them on. /// /// Used extensively in tests to validate messaging expectations. -private class DatagramReadRecorder: ChannelInboundHandler { +final class DatagramReadRecorder: ChannelInboundHandler { typealias InboundIn = AddressedEnvelope typealias InboundOut = AddressedEnvelope diff --git a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift new file mode 100644 index 00000000000..656d91a6657 --- /dev/null +++ b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2022 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 XCTest +import NIOCore +import NIOPosix + +#if canImport(Darwin) +import Darwin +#endif + +extension NIOIPProtocol { + static let reservedForTesting = Self(rawValue: 253) +} + +final class RawSocketBootstrapTests: XCTestCase { + func testWriteAndRead() throws { + #if canImport(Darwin) + try XCTSkipIf(geteuid() != 0, "Raw Socket API requires root privileges on Darwin") + #endif + + let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) } + let channel = try RawSocketBootstrap(group: elg) + .channelInitializer { + $0.pipeline.addHandler(DatagramReadRecorder(), name: "ByteReadRecorder") + } + .bind(host: "127.0.0.1", ipProtocol: .reservedForTesting).wait() + defer { XCTAssertNoThrow(try channel.close().wait()) } + try channel.configureForRecvMmsg(messageCount: 10) + let expectedMessages = (0..<10).map { "Hello World \($0)" } + for message in expectedMessages { + _ = try channel.write(AddressedEnvelope( + remoteAddress: SocketAddress(ipAddress: "127.0.0.1", port: 0), + data: ByteBuffer(string: message) + )) + } + channel.flush() + + let receivedMessages = Set(try channel.waitForDatagrams(count: 10).map(\.data).map { buffer in + String( + decoding: buffer.readableBytesView.dropFirst(4 * 5), // skip the IPv4 header + as: UTF8.self + ) + }) + + XCTAssertEqual(receivedMessages, Set(expectedMessages)) + } +} From 2decd8dff1b2ffa9852387d30d6b92327759efc9 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Tue, 22 Nov 2022 22:32:12 +0100 Subject: [PATCH 02/12] Add more tests --- .../RawSocketBootstrapTests.swift | 148 ++++++++++++++++-- 1 file changed, 139 insertions(+), 9 deletions(-) diff --git a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift index 656d91a6657..ec8336af8ad 100644 --- a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift +++ b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift @@ -18,17 +18,100 @@ import NIOPosix #if canImport(Darwin) import Darwin +#elseif canImport(Glibc) +import Glibc #endif extension NIOIPProtocol { static let reservedForTesting = Self(rawValue: 253) } +func XCTSkipIfUserHasNotEnoughRightsForRawSocketAPI(file: StaticString = #filePath, line: UInt = #line) throws { + try XCTSkipIf(geteuid() != 0, "Raw Socket API requires root privileges", file: file, line: line) +} + +struct IPv4Address: Hashable { + var rawValue: UInt32 +} + +extension IPv4Address { + init(_ v1: UInt8, _ v2: UInt8, _ v3: UInt8, _ v4: UInt8) { + rawValue = UInt32(v1) << 24 | UInt32(v2) << 16 | UInt32(v3) << 8 | UInt32(v4) + } +} + +struct IPv4Header { + static let size: Int = 20 + + private let versionAndIhl: UInt8 + var version: UInt8 { + versionAndIhl >> 4 + } + var internetHeaderLength: UInt8 { + versionAndIhl & 0b0000_1111 + } + private let dscpAndEcn: UInt8 + var differentiatedServicesCodePoint: UInt8 { + dscpAndEcn >> 2 + } + var explicitCongestionNotification: UInt8 { + dscpAndEcn & 0b0000_0011 + } + let totalLength: UInt16 + let identification: UInt16 + let flagsAndFragmentOffset: UInt16 + var flags: UInt8 { + UInt8(flagsAndFragmentOffset >> 13) + } + var fragmentOffset: UInt16 { + flagsAndFragmentOffset & 0b0001_1111_1111_1111 + } + let timeToLive: UInt8 + let `protocol`: NIOIPProtocol + let headerChecksum: UInt16 + let sourceIpAdress: IPv4Address + let destinationIpAddress: IPv4Address + + init?(buffer: inout ByteBuffer) { + guard let ( + versionAndIhl, + dscpAndEcn, + totalLength, + identification, + flagsAndFragmentOffset, + timeToLive, + `protocol`, + headerChecksum, + sourceIpAdress, + destinationIpAddress + ) = buffer.readMultipleIntegers(as: ( + UInt8, + UInt8, + UInt16, + UInt16, + UInt16, + UInt8, + UInt8, + UInt16, + UInt32, + UInt32 + ).self) else { return nil } + self.versionAndIhl = versionAndIhl + self.dscpAndEcn = dscpAndEcn + self.totalLength = totalLength + self.identification = identification + self.flagsAndFragmentOffset = flagsAndFragmentOffset + self.timeToLive = timeToLive + self.`protocol` = .init(rawValue: `protocol`) + self.headerChecksum = headerChecksum + self.sourceIpAdress = .init(rawValue: sourceIpAdress) + self.destinationIpAddress = .init(rawValue: destinationIpAddress) + } +} + final class RawSocketBootstrapTests: XCTestCase { - func testWriteAndRead() throws { - #if canImport(Darwin) - try XCTSkipIf(geteuid() != 0, "Raw Socket API requires root privileges on Darwin") - #endif + func testBindWithRecevMmsg() throws { + try XCTSkipIfUserHasNotEnoughRightsForRawSocketAPI() let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) } @@ -48,13 +131,60 @@ final class RawSocketBootstrapTests: XCTestCase { } channel.flush() - let receivedMessages = Set(try channel.waitForDatagrams(count: 10).map(\.data).map { buffer in - String( - decoding: buffer.readableBytesView.dropFirst(4 * 5), // skip the IPv4 header - as: UTF8.self - ) + let receivedMessages = Set(try channel.waitForDatagrams(count: 10).map { envelop -> String in + var data = envelop.data + let header = try XCTUnwrap(IPv4Header(buffer: &data)) + XCTAssertEqual(header.version, 4) + XCTAssertEqual(header.protocol, .reservedForTesting) + XCTAssertEqual(Int(header.totalLength), IPv4Header.size + data.readableBytes) + XCTAssertEqual(header.sourceIpAdress, .init(127, 0, 0, 1)) + XCTAssertEqual(header.destinationIpAddress, .init(127, 0, 0, 1)) + return String(buffer: data) + }) + + XCTAssertEqual(receivedMessages, Set(expectedMessages)) + } + + func testConnect() throws { + try XCTSkipIfUserHasNotEnoughRightsForRawSocketAPI() + + let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) } + let readChannel = try RawSocketBootstrap(group: elg) + .channelInitializer { + $0.pipeline.addHandler(DatagramReadRecorder(), name: "ByteReadRecorder") + } + .bind(host: "127.0.0.1", ipProtocol: .reservedForTesting).wait() + defer { XCTAssertNoThrow(try readChannel.close().wait()) } + + let writeChannel = try RawSocketBootstrap(group: elg) + .channelInitializer { + $0.pipeline.addHandler(DatagramReadRecorder(), name: "ByteReadRecorder") + } + .bind(host: "127.0.0.1", ipProtocol: .reservedForTesting).wait() + defer { XCTAssertNoThrow(try writeChannel.close().wait()) } + + let expectedMessages = (0..<10).map { "Hello World \($0)" } + for message in expectedMessages { + _ = try writeChannel.write(AddressedEnvelope( + remoteAddress: SocketAddress(ipAddress: "127.0.0.1", port: 0), + data: ByteBuffer(string: message) + )) + } + writeChannel.flush() + + let receivedMessages = Set(try readChannel.waitForDatagrams(count: 10).map { envelop -> String in + var data = envelop.data + let header = try XCTUnwrap(IPv4Header(buffer: &data)) + XCTAssertEqual(header.version, 4) + XCTAssertEqual(header.protocol, .reservedForTesting) + XCTAssertEqual(Int(header.totalLength), IPv4Header.size + data.readableBytes) + XCTAssertEqual(header.sourceIpAdress, .init(127, 0, 0, 1)) + XCTAssertEqual(header.destinationIpAddress, .init(127, 0, 0, 1)) + return String(buffer: data) }) XCTAssertEqual(receivedMessages, Set(expectedMessages)) } + } From 5b2dfd6d9fa53faa412fb006f01efebe59af446b Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Wed, 23 Nov 2022 22:46:28 +0100 Subject: [PATCH 03/12] Add `IPv4Header` and tests for `.ip_hdrincl` --- Sources/NIOCore/ChannelOption.swift | 5 + Tests/NIOPosixTests/IPv4Header.swift | 277 ++++++++++++++++++ .../RawSocketBootstrapTests.swift | 164 +++++------ 3 files changed, 363 insertions(+), 83 deletions(-) create mode 100644 Tests/NIOPosixTests/IPv4Header.swift diff --git a/Sources/NIOCore/ChannelOption.swift b/Sources/NIOCore/ChannelOption.swift index f79e126a455..826ef021499 100644 --- a/Sources/NIOCore/ChannelOption.swift +++ b/Sources/NIOCore/ChannelOption.swift @@ -276,6 +276,11 @@ public struct ChannelOptions { public static let socketOption = { (name: NIOBSDSocket.Option) -> Types.SocketOption in .init(level: .socket, name: name) } + + /// - seealso: `SocketOption`. + public static let ipOption = { (name: NIOBSDSocket.Option) -> Types.SocketOption in + .init(level: .ip, name: name) + } /// - seealso: `SocketOption`. public static let tcpOption = { (name: NIOBSDSocket.Option) -> Types.SocketOption in diff --git a/Tests/NIOPosixTests/IPv4Header.swift b/Tests/NIOPosixTests/IPv4Header.swift new file mode 100644 index 00000000000..b1d5c1a0170 --- /dev/null +++ b/Tests/NIOPosixTests/IPv4Header.swift @@ -0,0 +1,277 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2022 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 NIOCore + +struct IPv4Address: Hashable { + var rawValue: UInt32 +} + +extension IPv4Address { + init(_ v1: UInt8, _ v2: UInt8, _ v3: UInt8, _ v4: UInt8) { + rawValue = UInt32(v1) << 24 | UInt32(v2) << 16 | UInt32(v3) << 8 | UInt32(v4) + } +} + +extension IPv4Address: CustomStringConvertible { + var description: String { + let v1 = rawValue >> 24 + let v2 = rawValue >> 16 & 0b1111_1111 + let v3 = rawValue >> 8 & 0b1111_1111 + let v4 = rawValue & 0b1111_1111 + return "\(v1).\(v2).\(v3).\(v4)" + } +} + +struct IPv4Header: Hashable { + static let size: Int = 20 + + private var versionAndIhl: UInt8 + var version: UInt8 { + get { + versionAndIhl >> 4 + } + set { + precondition(newValue & 0b1111_0000 == 0) + versionAndIhl = newValue << 4 | (0b0000_1111 & versionAndIhl) + assert(newValue == version, "\(newValue) != \(version) \(versionAndIhl)") + } + } + var internetHeaderLength: UInt8 { + get { + versionAndIhl & 0b0000_1111 + } + set { + precondition(newValue & 0b1111_0000 == 0) + versionAndIhl = newValue | (0b1111_0000 & versionAndIhl) + assert(newValue == internetHeaderLength) + } + } + private var dscpAndEcn: UInt8 + var differentiatedServicesCodePoint: UInt8 { + get { + dscpAndEcn >> 2 + } + set { + precondition(newValue & 0b0000_0011 == 0) + dscpAndEcn = newValue << 2 | (0b0000_0011 & dscpAndEcn) + assert(newValue == differentiatedServicesCodePoint) + } + } + var explicitCongestionNotification: UInt8 { + get { + dscpAndEcn & 0b0000_0011 + } + set { + precondition(newValue & 0b0000_0011 == 0) + dscpAndEcn = newValue | (0b1111_1100 & dscpAndEcn) + assert(newValue == explicitCongestionNotification) + } + } + var totalLength: UInt16 + var identification: UInt16 + private var flagsAndFragmentOffset: UInt16 + var flags: UInt8 { + get { + UInt8(flagsAndFragmentOffset >> 13) + } + set { + precondition(newValue & 0b0000_0111 == 0) + flagsAndFragmentOffset = UInt16(newValue) << 13 | (0b0001_1111_1111_1111 & flagsAndFragmentOffset) + assert(newValue == flags) + } + } + var fragmentOffset: UInt16 { + get { + flagsAndFragmentOffset & 0b0001_1111_1111_1111 + } + set { + precondition(newValue & 0b1110_0000_0000_0000 == 0) + flagsAndFragmentOffset = newValue | (0b1110_0000_0000_0000 & flagsAndFragmentOffset) + assert(newValue == fragmentOffset) + } + } + var timeToLive: UInt8 + var `protocol`: NIOIPProtocol + var headerChecksum: UInt16 + var sourceIpAddress: IPv4Address + var destinationIpAddress: IPv4Address + + init?(buffer: inout ByteBuffer) { + #if canImport(Darwin) + guard + let versionAndIhl: UInt8 = buffer.readInteger(), + let dscpAndEcn: UInt8 = buffer.readInteger(), + // On BSD, the total length is in host byte order + let totalLength: UInt16 = buffer.readInteger(endianness: .host), + let identification: UInt16 = buffer.readInteger(), + // fragmentOffset is in host byte order as well but it is always zero in our tests + // and fragmentOffset is 13 bits in size so we can't just use readInteger(endianness: .host) + let flagsAndFragmentOffset: UInt16 = buffer.readInteger(), + let timeToLive: UInt8 = buffer.readInteger(), + let `protocol`: UInt8 = buffer.readInteger(), + let headerChecksum: UInt16 = buffer.readInteger(), + let sourceIpAddress: UInt32 = buffer.readInteger(), + let destinationIpAddress: UInt32 = buffer.readInteger() + else { return nil } + #elseif os(Linux) + guard let ( + versionAndIhl, + dscpAndEcn, + totalLength, + identification, + flagsAndFragmentOffset, + timeToLive, + `protocol`, + headerChecksum, + sourceIpAddress, + destinationIpAddress + ) = buffer.readMultipleIntegers(as: ( + UInt8, + UInt8, + UInt16, + UInt16, + UInt16, + UInt8, + UInt8, + UInt16, + UInt32, + UInt32 + ).self) else { return nil } + #endif + self.versionAndIhl = versionAndIhl + self.dscpAndEcn = dscpAndEcn + self.totalLength = totalLength + self.identification = identification + self.flagsAndFragmentOffset = flagsAndFragmentOffset + self.timeToLive = timeToLive + self.`protocol` = .init(rawValue: `protocol`) + self.headerChecksum = headerChecksum + self.sourceIpAddress = .init(rawValue: sourceIpAddress) + self.destinationIpAddress = .init(rawValue: destinationIpAddress) + } + init() { + self.versionAndIhl = 0 + self.dscpAndEcn = 0 + self.totalLength = 0 + self.identification = 0 + self.flagsAndFragmentOffset = 0 + self.timeToLive = 0 + self.`protocol` = .init(rawValue: 0) + self.headerChecksum = 0 + self.sourceIpAddress = .init(rawValue: 0) + self.destinationIpAddress = .init(rawValue: 0) + } + + mutating func setChecksum() { + self.headerChecksum = ~[ + UInt16(versionAndIhl) << 8 | UInt16(dscpAndEcn), + totalLength, + identification, + flagsAndFragmentOffset, + UInt16(timeToLive) << 8 | UInt16(`protocol`.rawValue), + UInt16(sourceIpAddress.rawValue >> 16), + UInt16(sourceIpAddress.rawValue & 0b0000_0000_0000_0000_1111_1111_1111_1111), + UInt16(destinationIpAddress.rawValue >> 16), + UInt16(destinationIpAddress.rawValue & 0b0000_0000_0000_0000_1111_1111_1111_1111), + ].reduce(UInt16(0), onesComplementAdd) + + assert(isValidChecksum()) + } + + func isValidChecksum() -> Bool { + let sum = ~[ + UInt16(versionAndIhl) << 8 | UInt16(dscpAndEcn), + totalLength, + identification, + flagsAndFragmentOffset, + UInt16(timeToLive) << 8 | UInt16(`protocol`.rawValue), + headerChecksum, + UInt16(sourceIpAddress.rawValue >> 16), + UInt16(sourceIpAddress.rawValue & 0b0000_0000_0000_0000_1111_1111_1111_1111), + UInt16(destinationIpAddress.rawValue >> 16), + UInt16(destinationIpAddress.rawValue & 0b0000_0000_0000_0000_1111_1111_1111_1111), + ].reduce(UInt16(0), onesComplementAdd) + return sum == 0 + } + + func write(to buffer: inout ByteBuffer) { + assert({ + var buffer = ByteBuffer() + self._write(to: &buffer) + let newValue = Self(buffer: &buffer) + return self == newValue + }()) + self._write(to: &buffer) + } + + private func _write(to buffer: inout ByteBuffer) { + #if canImport(Darwin) + buffer.writeInteger(versionAndIhl) + buffer.writeInteger(dscpAndEcn) + // On BSD, the total length needs to be in host byte order + buffer.writeInteger(totalLength, endianness: .host) + buffer.writeInteger(identification) + // fragmentOffset needs to be in host byte order as well but it is always zero in our tests + // and fragmentOffset is 13 bits in size so we can't just use writeInteger(endianness: .host) + buffer.writeInteger(flagsAndFragmentOffset) + buffer.writeInteger(timeToLive) + buffer.writeInteger(`protocol`.rawValue) + buffer.writeInteger(headerChecksum) + buffer.writeInteger(sourceIpAddress.rawValue) + buffer.writeInteger(destinationIpAddress.rawValue) + #elseif os(Linux) + buffer.writeMultipleIntegers( + versionAndIhl, + dscpAndEcn, + totalLength, + identification, + flagsAndFragmentOffset, + timeToLive, + `protocol`.rawValue, + headerChecksum, + sourceIpAddress.rawValue, + destinationIpAddress.rawValue + ) + #endif + } +} + +private func onesComplementAdd(lhs: Integer, rhs: Integer) -> Integer { + var (sum, overflowed) = lhs.addingReportingOverflow(rhs) + if overflowed { + sum &+= 1 + } + return sum +} + +extension IPv4Header: CustomStringConvertible { + var description: String { + """ + Version: \(version) + Header Length: \(internetHeaderLength * 4) bytes + Differentiated Services: \(String(differentiatedServicesCodePoint, radix: 2)) + Explicit Congestion Notification: \(String(explicitCongestionNotification, radix: 2)) + Total Length: \(totalLength) bytes + Identification: \(identification) + Flags: \(String(flags, radix: 2)) + Fragment Offset: \(fragmentOffset) Bytes + Time to Live: \(timeToLive) + Protocol: \(`protocol`) + Header Checksum: \(headerChecksum) (\(isValidChecksum() ? "valid" : "*not* valid")) + Source IP Address: \(sourceIpAddress) + Destination IP Address: \(destinationIpAddress) + """ + } +} diff --git a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift index ec8336af8ad..4b05a9a6ddb 100644 --- a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift +++ b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift @@ -30,85 +30,6 @@ func XCTSkipIfUserHasNotEnoughRightsForRawSocketAPI(file: StaticString = #filePa try XCTSkipIf(geteuid() != 0, "Raw Socket API requires root privileges", file: file, line: line) } -struct IPv4Address: Hashable { - var rawValue: UInt32 -} - -extension IPv4Address { - init(_ v1: UInt8, _ v2: UInt8, _ v3: UInt8, _ v4: UInt8) { - rawValue = UInt32(v1) << 24 | UInt32(v2) << 16 | UInt32(v3) << 8 | UInt32(v4) - } -} - -struct IPv4Header { - static let size: Int = 20 - - private let versionAndIhl: UInt8 - var version: UInt8 { - versionAndIhl >> 4 - } - var internetHeaderLength: UInt8 { - versionAndIhl & 0b0000_1111 - } - private let dscpAndEcn: UInt8 - var differentiatedServicesCodePoint: UInt8 { - dscpAndEcn >> 2 - } - var explicitCongestionNotification: UInt8 { - dscpAndEcn & 0b0000_0011 - } - let totalLength: UInt16 - let identification: UInt16 - let flagsAndFragmentOffset: UInt16 - var flags: UInt8 { - UInt8(flagsAndFragmentOffset >> 13) - } - var fragmentOffset: UInt16 { - flagsAndFragmentOffset & 0b0001_1111_1111_1111 - } - let timeToLive: UInt8 - let `protocol`: NIOIPProtocol - let headerChecksum: UInt16 - let sourceIpAdress: IPv4Address - let destinationIpAddress: IPv4Address - - init?(buffer: inout ByteBuffer) { - guard let ( - versionAndIhl, - dscpAndEcn, - totalLength, - identification, - flagsAndFragmentOffset, - timeToLive, - `protocol`, - headerChecksum, - sourceIpAdress, - destinationIpAddress - ) = buffer.readMultipleIntegers(as: ( - UInt8, - UInt8, - UInt16, - UInt16, - UInt16, - UInt8, - UInt8, - UInt16, - UInt32, - UInt32 - ).self) else { return nil } - self.versionAndIhl = versionAndIhl - self.dscpAndEcn = dscpAndEcn - self.totalLength = totalLength - self.identification = identification - self.flagsAndFragmentOffset = flagsAndFragmentOffset - self.timeToLive = timeToLive - self.`protocol` = .init(rawValue: `protocol`) - self.headerChecksum = headerChecksum - self.sourceIpAdress = .init(rawValue: sourceIpAdress) - self.destinationIpAddress = .init(rawValue: destinationIpAddress) - } -} - final class RawSocketBootstrapTests: XCTestCase { func testBindWithRecevMmsg() throws { try XCTSkipIfUserHasNotEnoughRightsForRawSocketAPI() @@ -122,7 +43,7 @@ final class RawSocketBootstrapTests: XCTestCase { .bind(host: "127.0.0.1", ipProtocol: .reservedForTesting).wait() defer { XCTAssertNoThrow(try channel.close().wait()) } try channel.configureForRecvMmsg(messageCount: 10) - let expectedMessages = (0..<10).map { "Hello World \($0)" } + let expectedMessages = (1...10).map { "Hello World \($0)" } for message in expectedMessages { _ = try channel.write(AddressedEnvelope( remoteAddress: SocketAddress(ipAddress: "127.0.0.1", port: 0), @@ -136,8 +57,19 @@ final class RawSocketBootstrapTests: XCTestCase { let header = try XCTUnwrap(IPv4Header(buffer: &data)) XCTAssertEqual(header.version, 4) XCTAssertEqual(header.protocol, .reservedForTesting) + #if canImport(Darwin) + // On BSD the IP header will only contain the size of the ip packet body not the header. + // This is known bug which can't be fixed without breaking old apps which already workaround the issue + // like we do. + XCTAssertEqual(Int(header.totalLength), data.readableBytes) + // On BSD the checksum is always zero + XCTAssertEqual(header.headerChecksum, 0) + #elseif os(Linux) XCTAssertEqual(Int(header.totalLength), IPv4Header.size + data.readableBytes) - XCTAssertEqual(header.sourceIpAdress, .init(127, 0, 0, 1)) + XCTAssertTrue(header.isValidChecksum(), "\(header)") + #endif + + XCTAssertEqual(header.sourceIpAddress, .init(127, 0, 0, 1)) XCTAssertEqual(header.destinationIpAddress, .init(127, 0, 0, 1)) return String(buffer: data) }) @@ -164,7 +96,7 @@ final class RawSocketBootstrapTests: XCTestCase { .bind(host: "127.0.0.1", ipProtocol: .reservedForTesting).wait() defer { XCTAssertNoThrow(try writeChannel.close().wait()) } - let expectedMessages = (0..<10).map { "Hello World \($0)" } + let expectedMessages = (1...10).map { "Hello World \($0)" } for message in expectedMessages { _ = try writeChannel.write(AddressedEnvelope( remoteAddress: SocketAddress(ipAddress: "127.0.0.1", port: 0), @@ -178,8 +110,19 @@ final class RawSocketBootstrapTests: XCTestCase { let header = try XCTUnwrap(IPv4Header(buffer: &data)) XCTAssertEqual(header.version, 4) XCTAssertEqual(header.protocol, .reservedForTesting) + #if canImport(Darwin) + // On BSD the IP header will only contain the size of the ip packet body not the header. + // This is known bug which can't be fixed without breaking old apps which already workaround the issue + // like we do. + XCTAssertEqual(Int(header.totalLength), data.readableBytes) + // On BSD the checksum is always zero + XCTAssertEqual(header.headerChecksum, 0) + #elseif os(Linux) XCTAssertEqual(Int(header.totalLength), IPv4Header.size + data.readableBytes) - XCTAssertEqual(header.sourceIpAdress, .init(127, 0, 0, 1)) + XCTAssertTrue(header.isValidChecksum(), "\(header)") + #endif + XCTAssertTrue(header.isValidChecksum()) + XCTAssertEqual(header.sourceIpAddress, .init(127, 0, 0, 1)) XCTAssertEqual(header.destinationIpAddress, .init(127, 0, 0, 1)) return String(buffer: data) }) @@ -187,4 +130,59 @@ final class RawSocketBootstrapTests: XCTestCase { XCTAssertEqual(receivedMessages, Set(expectedMessages)) } + func testIpHdrincl() throws { + try XCTSkipIfUserHasNotEnoughRightsForRawSocketAPI() + + let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) } + let channel = try RawSocketBootstrap(group: elg) + .channelOption(ChannelOptions.ipOption(.ip_hdrincl), value: 1) + .channelInitializer { + $0.pipeline.addHandler(DatagramReadRecorder(), name: "ByteReadRecorder") + } + .bind(host: "127.0.0.1", ipProtocol: .reservedForTesting).wait() + defer { XCTAssertNoThrow(try channel.close().wait()) } + try channel.configureForRecvMmsg(messageCount: 10) + let expectedMessages = (1...10).map { "Hello World \($0)" } + for message in expectedMessages.map(ByteBuffer.init(string:)) { + var packet = ByteBuffer() + var header = IPv4Header() + header.version = 4 + header.internetHeaderLength = 5 + header.totalLength = UInt16(IPv4Header.size + message.readableBytes) + header.protocol = .reservedForTesting + header.timeToLive = 64 + header.destinationIpAddress = .init(127, 0, 0, 1) + header.sourceIpAddress = .init(127, 0, 0, 1) + header.setChecksum() + header.write(to: &packet) + packet.writeImmutableBuffer(message) + _ = try channel.write(AddressedEnvelope( + remoteAddress: SocketAddress(ipAddress: "127.0.0.1", port: 0), + data: packet + )) + } + channel.flush() + + let receivedMessages = Set(try channel.waitForDatagrams(count: 10).map { envelop -> String in + var data = envelop.data + let header = try XCTUnwrap(IPv4Header(buffer: &data)) + XCTAssertEqual(header.version, 4) + XCTAssertEqual(header.protocol, .reservedForTesting) + #if canImport(Darwin) + // On BSD the IP header will only contain the size of the ip packet body not the header. + // This is known bug which can't be fixed without breaking old apps which already workaround the issue + // like we do. + XCTAssertEqual(Int(header.totalLength), data.readableBytes) + #elseif os(Linux) + XCTAssertEqual(Int(header.totalLength), IPv4Header.size + data.readableBytes) + #endif + XCTAssertTrue(header.isValidChecksum()) + XCTAssertEqual(header.sourceIpAddress, .init(127, 0, 0, 1)) + XCTAssertEqual(header.destinationIpAddress, .init(127, 0, 0, 1)) + return String(buffer: data) + }) + + XCTAssertEqual(receivedMessages, Set(expectedMessages)) + } } From bb5c1b4c8ad855294d6a35fb4bee5eda7ad4c44d Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Wed, 23 Nov 2022 22:49:13 +0100 Subject: [PATCH 04/12] `generate_linux_tests.rb` --- Tests/LinuxMain.swift | 1 + .../RawSocketBootstrapTests+XCTest.swift | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 Tests/NIOPosixTests/RawSocketBootstrapTests+XCTest.swift diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index bd01f79936e..d5daf0920a4 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -120,6 +120,7 @@ class LinuxMainRunner { testCase(PendingDatagramWritesManagerTests.allTests), testCase(PipeChannelTest.allTests), testCase(PriorityQueueTest.allTests), + testCase(RawSocketBootstrapTests.allTests), testCase(SALChannelTest.allTests), testCase(SALEventLoopTests.allTests), testCase(SNIHandlerTest.allTests), diff --git a/Tests/NIOPosixTests/RawSocketBootstrapTests+XCTest.swift b/Tests/NIOPosixTests/RawSocketBootstrapTests+XCTest.swift new file mode 100644 index 00000000000..28d1fa049c5 --- /dev/null +++ b/Tests/NIOPosixTests/RawSocketBootstrapTests+XCTest.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2022 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 +// +//===----------------------------------------------------------------------===// +// +// RawSocketBootstrapTests+XCTest.swift +// +import XCTest + +/// +/// NOTE: This file was generated by generate_linux_tests.rb +/// +/// Do NOT edit this file directly as it will be regenerated automatically when needed. +/// + +extension RawSocketBootstrapTests { + + @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") + static var allTests : [(String, (RawSocketBootstrapTests) -> () throws -> Void)] { + return [ + ("testBindWithRecevMmsg", testBindWithRecevMmsg), + ("testConnect", testConnect), + ("testIpHdrincl", testIpHdrincl), + ] + } +} + From 26e5c148a8c37adff74762da9933f2c5eae04059 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Thu, 24 Nov 2022 11:32:07 +0100 Subject: [PATCH 05/12] Check if we have enough rights for the raw sockets API by trying to actually create a socket and skipping if we fail because of `EPERM` --- .../RawSocketBootstrapTests.swift | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift index 4b05a9a6ddb..2082527a13e 100644 --- a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift +++ b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift @@ -14,20 +14,24 @@ import XCTest import NIOCore -import NIOPosix - -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#endif +@testable import NIOPosix extension NIOIPProtocol { static let reservedForTesting = Self(rawValue: 253) } +// lazily try's to create a raw socket and caches the error if it fails +private let cachedRawSocketAPICheck = Result { + let socket = try Socket(protocolFamily: .inet, type: .raw, protocolSubtype: .init(NIOIPProtocol.reservedForTesting), setNonBlocking: true) + try socket.close() +} + func XCTSkipIfUserHasNotEnoughRightsForRawSocketAPI(file: StaticString = #filePath, line: UInt = #line) throws { - try XCTSkipIf(geteuid() != 0, "Raw Socket API requires root privileges", file: file, line: line) + do { + try cachedRawSocketAPICheck.get() + } catch let error as IOError where error.errnoCode == EPERM { + throw XCTSkip("Raw Socket API requires higher privileges: \(error)", file: file, line: line) + } } final class RawSocketBootstrapTests: XCTestCase { From 330226c073d80858b224268ef1562a69c69fdf61 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Thu, 24 Nov 2022 11:46:41 +0100 Subject: [PATCH 06/12] `generate_linux_tests.rb` --- Tests/NIOPosixTests/RawSocketBootstrapTests+XCTest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/NIOPosixTests/RawSocketBootstrapTests+XCTest.swift b/Tests/NIOPosixTests/RawSocketBootstrapTests+XCTest.swift index 28d1fa049c5..186cd6fc58d 100644 --- a/Tests/NIOPosixTests/RawSocketBootstrapTests+XCTest.swift +++ b/Tests/NIOPosixTests/RawSocketBootstrapTests+XCTest.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2022 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information From be58e9e5b0f4ac0acbc2d4a998cbdc8d984929ca Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Thu, 24 Nov 2022 14:18:50 +0100 Subject: [PATCH 07/12] Make capitalization consistent --- Tests/NIOPosixTests/IPv4Header.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/NIOPosixTests/IPv4Header.swift b/Tests/NIOPosixTests/IPv4Header.swift index b1d5c1a0170..afdb4d1e8fe 100644 --- a/Tests/NIOPosixTests/IPv4Header.swift +++ b/Tests/NIOPosixTests/IPv4Header.swift @@ -266,7 +266,7 @@ extension IPv4Header: CustomStringConvertible { Total Length: \(totalLength) bytes Identification: \(identification) Flags: \(String(flags, radix: 2)) - Fragment Offset: \(fragmentOffset) Bytes + Fragment Offset: \(fragmentOffset) bytes Time to Live: \(timeToLive) Protocol: \(`protocol`) Header Checksum: \(headerChecksum) (\(isValidChecksum() ? "valid" : "*not* valid")) From 20ed3802642a475b760beb33d8a121430f0224a6 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Mon, 28 Nov 2022 10:48:24 +0100 Subject: [PATCH 08/12] Use `write`/`read` methods on `ByteBuffer` --- Tests/NIOPosixTests/IPv4Header.swift | 193 +++++++++++------- .../RawSocketBootstrapTests.swift | 8 +- 2 files changed, 120 insertions(+), 81 deletions(-) diff --git a/Tests/NIOPosixTests/IPv4Header.swift b/Tests/NIOPosixTests/IPv4Header.swift index afdb4d1e8fe..06f87053907 100644 --- a/Tests/NIOPosixTests/IPv4Header.swift +++ b/Tests/NIOPosixTests/IPv4Header.swift @@ -37,7 +37,7 @@ extension IPv4Address: CustomStringConvertible { struct IPv4Header: Hashable { static let size: Int = 20 - private var versionAndIhl: UInt8 + fileprivate var versionAndIhl: UInt8 var version: UInt8 { get { versionAndIhl >> 4 @@ -58,7 +58,7 @@ struct IPv4Header: Hashable { assert(newValue == internetHeaderLength) } } - private var dscpAndEcn: UInt8 + fileprivate var dscpAndEcn: UInt8 var differentiatedServicesCodePoint: UInt8 { get { dscpAndEcn >> 2 @@ -81,7 +81,7 @@ struct IPv4Header: Hashable { } var totalLength: UInt16 var identification: UInt16 - private var flagsAndFragmentOffset: UInt16 + fileprivate var flagsAndFragmentOffset: UInt16 var flags: UInt8 { get { UInt8(flagsAndFragmentOffset >> 13) @@ -108,59 +108,30 @@ struct IPv4Header: Hashable { var sourceIpAddress: IPv4Address var destinationIpAddress: IPv4Address - init?(buffer: inout ByteBuffer) { - #if canImport(Darwin) - guard - let versionAndIhl: UInt8 = buffer.readInteger(), - let dscpAndEcn: UInt8 = buffer.readInteger(), - // On BSD, the total length is in host byte order - let totalLength: UInt16 = buffer.readInteger(endianness: .host), - let identification: UInt16 = buffer.readInteger(), - // fragmentOffset is in host byte order as well but it is always zero in our tests - // and fragmentOffset is 13 bits in size so we can't just use readInteger(endianness: .host) - let flagsAndFragmentOffset: UInt16 = buffer.readInteger(), - let timeToLive: UInt8 = buffer.readInteger(), - let `protocol`: UInt8 = buffer.readInteger(), - let headerChecksum: UInt16 = buffer.readInteger(), - let sourceIpAddress: UInt32 = buffer.readInteger(), - let destinationIpAddress: UInt32 = buffer.readInteger() - else { return nil } - #elseif os(Linux) - guard let ( - versionAndIhl, - dscpAndEcn, - totalLength, - identification, - flagsAndFragmentOffset, - timeToLive, - `protocol`, - headerChecksum, - sourceIpAddress, - destinationIpAddress - ) = buffer.readMultipleIntegers(as: ( - UInt8, - UInt8, - UInt16, - UInt16, - UInt16, - UInt8, - UInt8, - UInt16, - UInt32, - UInt32 - ).self) else { return nil } - #endif + fileprivate init( + versionAndIhl: UInt8, + dscpAndEcn: UInt8, + totalLength: UInt16, + identification: UInt16, + flagsAndFragmentOffset: UInt16, + timeToLive: UInt8, + `protocol`: NIOIPProtocol, + headerChecksum: UInt16, + sourceIpAddress: IPv4Address, + destinationIpAddress: IPv4Address + ) { self.versionAndIhl = versionAndIhl self.dscpAndEcn = dscpAndEcn self.totalLength = totalLength self.identification = identification self.flagsAndFragmentOffset = flagsAndFragmentOffset self.timeToLive = timeToLive - self.`protocol` = .init(rawValue: `protocol`) + self.`protocol` = `protocol` self.headerChecksum = headerChecksum - self.sourceIpAddress = .init(rawValue: sourceIpAddress) - self.destinationIpAddress = .init(rawValue: destinationIpAddress) + self.sourceIpAddress = sourceIpAddress + self.destinationIpAddress = destinationIpAddress } + init() { self.versionAndIhl = 0 self.dscpAndEcn = 0 @@ -205,44 +176,112 @@ struct IPv4Header: Hashable { ].reduce(UInt16(0), onesComplementAdd) return sum == 0 } - - func write(to buffer: inout ByteBuffer) { - assert({ - var buffer = ByteBuffer() - self._write(to: &buffer) - let newValue = Self(buffer: &buffer) - return self == newValue - }()) - self._write(to: &buffer) - } - - private func _write(to buffer: inout ByteBuffer) { +} + +extension ByteBuffer { + @discardableResult + mutating func readIPv4Header() -> IPv4Header? { #if canImport(Darwin) - buffer.writeInteger(versionAndIhl) - buffer.writeInteger(dscpAndEcn) - // On BSD, the total length needs to be in host byte order - buffer.writeInteger(totalLength, endianness: .host) - buffer.writeInteger(identification) - // fragmentOffset needs to be in host byte order as well but it is always zero in our tests - // and fragmentOffset is 13 bits in size so we can't just use writeInteger(endianness: .host) - buffer.writeInteger(flagsAndFragmentOffset) - buffer.writeInteger(timeToLive) - buffer.writeInteger(`protocol`.rawValue) - buffer.writeInteger(headerChecksum) - buffer.writeInteger(sourceIpAddress.rawValue) - buffer.writeInteger(destinationIpAddress.rawValue) + var initialState = self + guard + let versionAndIhl: UInt8 = self.readInteger(), + let dscpAndEcn: UInt8 = self.readInteger(), + // On BSD, the total length is in host byte order + let totalLength: UInt16 = self.readInteger(endianness: .host), + let identification: UInt16 = self.readInteger(), + // fragmentOffset is in host byte order as well but it is always zero in our tests + // and fragmentOffset is 13 bits in size so we can't just use readInteger(endianness: .host) + let flagsAndFragmentOffset: UInt16 = self.readInteger(), + let timeToLive: UInt8 = self.readInteger(), + let `protocol`: UInt8 = self.readInteger(), + let headerChecksum: UInt16 = self.readInteger(), + let sourceIpAddress: UInt32 = self.readInteger(), + let destinationIpAddress: UInt32 = self.readInteger() + else { + self = initialState + return nil + } #elseif os(Linux) - buffer.writeMultipleIntegers( + guard let ( versionAndIhl, dscpAndEcn, totalLength, identification, flagsAndFragmentOffset, timeToLive, - `protocol`.rawValue, + `protocol`, headerChecksum, - sourceIpAddress.rawValue, - destinationIpAddress.rawValue + sourceIpAddress, + destinationIpAddress + ) = self.readMultipleIntegers(as: ( + UInt8, + UInt8, + UInt16, + UInt16, + UInt16, + UInt8, + UInt8, + UInt16, + UInt32, + UInt32 + ).self) else { return nil } + #endif + return .init( + versionAndIhl: versionAndIhl, + dscpAndEcn: dscpAndEcn, + totalLength: totalLength, + identification: identification, + flagsAndFragmentOffset: flagsAndFragmentOffset, + timeToLive: timeToLive, + protocol: .init(rawValue: `protocol`), + headerChecksum: headerChecksum, + sourceIpAddress: .init(rawValue: sourceIpAddress), + destinationIpAddress: .init(rawValue: destinationIpAddress) + ) + } +} + +extension ByteBuffer { + @discardableResult + mutating func writeIPv4Header(_ header: IPv4Header) -> Int { + assert({ + var buffer = ByteBuffer() + buffer._writeIPv4Header(header: header) + let newValue = Self(buffer: buffer) + return self == newValue + }()) + return self._writeIPv4Header(header: header) + } + + @discardableResult + private mutating func _writeIPv4Header(header: IPv4Header) -> Int { + #if canImport(Darwin) + let firstPart = self.writeInteger(header.versionAndIhl) + + self.writeInteger(header.dscpAndEcn) + + // On BSD, the total length needs to be in host byte order + self.writeInteger(header.totalLength, endianness: .host) + let secondPart = self.writeInteger(header.identification) + + // fragmentOffset needs to be in host byte order as well but it is always zero in our tests + // and fragmentOffset is 13 bits in size so we can't just use writeInteger(endianness: .host) + self.writeInteger(header.flagsAndFragmentOffset) + + self.writeInteger(header.timeToLive) + let thirdPart = self.writeInteger(header.`protocol`.rawValue) + + self.writeInteger(header.headerChecksum) + + self.writeInteger(header.sourceIpAddress.rawValue) + let forthPart = self.writeInteger(header.destinationIpAddress.rawValue) + return firstPart + secondPart + thirdPart + forthPart + #elseif os(Linux) + return self.writeMultipleIntegers( + header.versionAndIhl, + header.dscpAndEcn, + header.totalLength, + header.identification, + header.flagsAndFragmentOffset, + header.timeToLive, + header.`protocol`.rawValue, + header.headerChecksum, + header.sourceIpAddress.rawValue, + header.destinationIpAddress.rawValue ) #endif } diff --git a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift index 2082527a13e..859b6dd2664 100644 --- a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift +++ b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift @@ -58,7 +58,7 @@ final class RawSocketBootstrapTests: XCTestCase { let receivedMessages = Set(try channel.waitForDatagrams(count: 10).map { envelop -> String in var data = envelop.data - let header = try XCTUnwrap(IPv4Header(buffer: &data)) + let header = try XCTUnwrap(data.readIPv4Header()) XCTAssertEqual(header.version, 4) XCTAssertEqual(header.protocol, .reservedForTesting) #if canImport(Darwin) @@ -111,7 +111,7 @@ final class RawSocketBootstrapTests: XCTestCase { let receivedMessages = Set(try readChannel.waitForDatagrams(count: 10).map { envelop -> String in var data = envelop.data - let header = try XCTUnwrap(IPv4Header(buffer: &data)) + let header = try XCTUnwrap(data.readIPv4Header()) XCTAssertEqual(header.version, 4) XCTAssertEqual(header.protocol, .reservedForTesting) #if canImport(Darwin) @@ -159,7 +159,7 @@ final class RawSocketBootstrapTests: XCTestCase { header.destinationIpAddress = .init(127, 0, 0, 1) header.sourceIpAddress = .init(127, 0, 0, 1) header.setChecksum() - header.write(to: &packet) + packet.writeIPv4Header(header) packet.writeImmutableBuffer(message) _ = try channel.write(AddressedEnvelope( remoteAddress: SocketAddress(ipAddress: "127.0.0.1", port: 0), @@ -170,7 +170,7 @@ final class RawSocketBootstrapTests: XCTestCase { let receivedMessages = Set(try channel.waitForDatagrams(count: 10).map { envelop -> String in var data = envelop.data - let header = try XCTUnwrap(IPv4Header(buffer: &data)) + let header = try XCTUnwrap(data.readIPv4Header()) XCTAssertEqual(header.version, 4) XCTAssertEqual(header.protocol, .reservedForTesting) #if canImport(Darwin) From 981d0b88b242aa1aa5ed14385699dde39c95cfa1 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Mon, 28 Nov 2022 10:49:36 +0100 Subject: [PATCH 09/12] Add `NIO` prefix to `RawSocketBootstrap` --- Sources/NIOPosix/RawSocketBootstrap.swift | 4 ++-- Tests/NIOPosixTests/RawSocketBootstrapTests.swift | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/NIOPosix/RawSocketBootstrap.swift b/Sources/NIOPosix/RawSocketBootstrap.swift index 5ea6ea891cc..2ab196403f7 100644 --- a/Sources/NIOPosix/RawSocketBootstrap.swift +++ b/Sources/NIOPosix/RawSocketBootstrap.swift @@ -33,7 +33,7 @@ import NIOCore /// ``` /// /// The `Channel` will operate on `AddressedEnvelope` as inbound and outbound messages. -public final class RawSocketBootstrap { +public final class NIORawSocketBootstrap { private let group: EventLoopGroup private var channelInitializer: Optional @@ -193,5 +193,5 @@ public final class RawSocketBootstrap { #if swift(>=5.6) @available(*, unavailable) -extension RawSocketBootstrap: Sendable {} +extension NIORawSocketBootstrap: Sendable {} #endif diff --git a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift index 859b6dd2664..78cd4914ac4 100644 --- a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift +++ b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift @@ -40,7 +40,7 @@ final class RawSocketBootstrapTests: XCTestCase { let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) } - let channel = try RawSocketBootstrap(group: elg) + let channel = try NIORawSocketBootstrap(group: elg) .channelInitializer { $0.pipeline.addHandler(DatagramReadRecorder(), name: "ByteReadRecorder") } @@ -86,14 +86,14 @@ final class RawSocketBootstrapTests: XCTestCase { let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) } - let readChannel = try RawSocketBootstrap(group: elg) + let readChannel = try NIORawSocketBootstrap(group: elg) .channelInitializer { $0.pipeline.addHandler(DatagramReadRecorder(), name: "ByteReadRecorder") } .bind(host: "127.0.0.1", ipProtocol: .reservedForTesting).wait() defer { XCTAssertNoThrow(try readChannel.close().wait()) } - let writeChannel = try RawSocketBootstrap(group: elg) + let writeChannel = try NIORawSocketBootstrap(group: elg) .channelInitializer { $0.pipeline.addHandler(DatagramReadRecorder(), name: "ByteReadRecorder") } @@ -139,7 +139,7 @@ final class RawSocketBootstrapTests: XCTestCase { let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) } - let channel = try RawSocketBootstrap(group: elg) + let channel = try NIORawSocketBootstrap(group: elg) .channelOption(ChannelOptions.ipOption(.ip_hdrincl), value: 1) .channelInitializer { $0.pipeline.addHandler(DatagramReadRecorder(), name: "ByteReadRecorder") From fabaf9b804afd085ff5d2c2f84ca8d2764e7b762 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Mon, 28 Nov 2022 18:20:10 +0100 Subject: [PATCH 10/12] Fix review comments --- Tests/NIOPosixTests/IPv4Header.swift | 117 +++++++++++------- .../RawSocketBootstrapTests.swift | 39 +----- 2 files changed, 80 insertions(+), 76 deletions(-) diff --git a/Tests/NIOPosixTests/IPv4Header.swift b/Tests/NIOPosixTests/IPv4Header.swift index 06f87053907..9168bec634e 100644 --- a/Tests/NIOPosixTests/IPv4Header.swift +++ b/Tests/NIOPosixTests/IPv4Header.swift @@ -144,45 +144,15 @@ struct IPv4Header: Hashable { self.sourceIpAddress = .init(rawValue: 0) self.destinationIpAddress = .init(rawValue: 0) } - - mutating func setChecksum() { - self.headerChecksum = ~[ - UInt16(versionAndIhl) << 8 | UInt16(dscpAndEcn), - totalLength, - identification, - flagsAndFragmentOffset, - UInt16(timeToLive) << 8 | UInt16(`protocol`.rawValue), - UInt16(sourceIpAddress.rawValue >> 16), - UInt16(sourceIpAddress.rawValue & 0b0000_0000_0000_0000_1111_1111_1111_1111), - UInt16(destinationIpAddress.rawValue >> 16), - UInt16(destinationIpAddress.rawValue & 0b0000_0000_0000_0000_1111_1111_1111_1111), - ].reduce(UInt16(0), onesComplementAdd) - - assert(isValidChecksum()) - } - - func isValidChecksum() -> Bool { - let sum = ~[ - UInt16(versionAndIhl) << 8 | UInt16(dscpAndEcn), - totalLength, - identification, - flagsAndFragmentOffset, - UInt16(timeToLive) << 8 | UInt16(`protocol`.rawValue), - headerChecksum, - UInt16(sourceIpAddress.rawValue >> 16), - UInt16(sourceIpAddress.rawValue & 0b0000_0000_0000_0000_1111_1111_1111_1111), - UInt16(destinationIpAddress.rawValue >> 16), - UInt16(destinationIpAddress.rawValue & 0b0000_0000_0000_0000_1111_1111_1111_1111), - ].reduce(UInt16(0), onesComplementAdd) - return sum == 0 - } } + + extension ByteBuffer { @discardableResult mutating func readIPv4Header() -> IPv4Header? { #if canImport(Darwin) - var initialState = self + let initialState = self guard let versionAndIhl: UInt8 = self.readInteger(), let dscpAndEcn: UInt8 = self.readInteger(), @@ -247,8 +217,8 @@ extension ByteBuffer { assert({ var buffer = ByteBuffer() buffer._writeIPv4Header(header: header) - let newValue = Self(buffer: buffer) - return self == newValue + let writtenHeader = buffer.readIPv4Header() + return header == writtenHeader }()) return self._writeIPv4Header(header: header) } @@ -256,20 +226,20 @@ extension ByteBuffer { @discardableResult private mutating func _writeIPv4Header(header: IPv4Header) -> Int { #if canImport(Darwin) - let firstPart = self.writeInteger(header.versionAndIhl) + + return self.writeInteger(header.versionAndIhl) + self.writeInteger(header.dscpAndEcn) + // On BSD, the total length needs to be in host byte order - self.writeInteger(header.totalLength, endianness: .host) - let secondPart = self.writeInteger(header.identification) + + self.writeInteger(header.totalLength, endianness: .host) + + self.writeInteger(header.identification) + // fragmentOffset needs to be in host byte order as well but it is always zero in our tests // and fragmentOffset is 13 bits in size so we can't just use writeInteger(endianness: .host) self.writeInteger(header.flagsAndFragmentOffset) + - self.writeInteger(header.timeToLive) - let thirdPart = self.writeInteger(header.`protocol`.rawValue) + + self.writeInteger(header.timeToLive) + + self.writeInteger(header.`protocol`.rawValue) + self.writeInteger(header.headerChecksum) + - self.writeInteger(header.sourceIpAddress.rawValue) - let forthPart = self.writeInteger(header.destinationIpAddress.rawValue) - return firstPart + secondPart + thirdPart + forthPart + self.writeInteger(header.sourceIpAddress.rawValue) + + self.writeInteger(header.destinationIpAddress.rawValue) + #elseif os(Linux) return self.writeMultipleIntegers( header.versionAndIhl, @@ -287,6 +257,45 @@ extension ByteBuffer { } } +extension IPv4Header { + func computeChecksum() -> UInt16 { + let checksum = ~[ + UInt16(versionAndIhl) << 8 | UInt16(dscpAndEcn), + totalLength, + identification, + flagsAndFragmentOffset, + UInt16(timeToLive) << 8 | UInt16(`protocol`.rawValue), + UInt16(sourceIpAddress.rawValue >> 16), + UInt16(sourceIpAddress.rawValue & 0b0000_0000_0000_0000_1111_1111_1111_1111), + UInt16(destinationIpAddress.rawValue >> 16), + UInt16(destinationIpAddress.rawValue & 0b0000_0000_0000_0000_1111_1111_1111_1111), + ].reduce(UInt16(0), onesComplementAdd) + assert(isValidChecksum(checksum)) + return checksum + } + mutating func setChecksum() { + self.headerChecksum = computeChecksum() + } + func isValidChecksum(_ headerChecksum: UInt16) -> Bool { + let sum = ~[ + UInt16(versionAndIhl) << 8 | UInt16(dscpAndEcn), + totalLength, + identification, + flagsAndFragmentOffset, + UInt16(timeToLive) << 8 | UInt16(`protocol`.rawValue), + headerChecksum, + UInt16(sourceIpAddress.rawValue >> 16), + UInt16(sourceIpAddress.rawValue & 0b0000_0000_0000_0000_1111_1111_1111_1111), + UInt16(destinationIpAddress.rawValue >> 16), + UInt16(destinationIpAddress.rawValue & 0b0000_0000_0000_0000_1111_1111_1111_1111), + ].reduce(UInt16(0), onesComplementAdd) + return sum == 0 + } + func isValidChecksum() -> Bool { + isValidChecksum(headerChecksum) + } +} + private func onesComplementAdd(lhs: Integer, rhs: Integer) -> Integer { var (sum, overflowed) = lhs.addingReportingOverflow(rhs) if overflowed { @@ -295,6 +304,28 @@ private func onesComplementAdd(lhs: Integer, rhs: In return sum } +extension IPv4Header { + var platformIndependentTotalLengthForReceivedPacketFromRawSocket: UInt16 { + #if canImport(Darwin) + // On BSD the IP header will only contain the size of the ip packet body, not the header. + // This is known bug which can't be fixed without breaking old apps which already workaround the issue + // like e.g. we do now too. + return totalLength + 20 + #elseif os(Linux) + return totalLength + #endif + } + var platformIndependentChecksumForReceivedPacketFromRawSocket: UInt16 { + #if canImport(Darwin) + // On BSD the checksum is always zero and we need to compute it + precondition(headerChecksum == 0) + return computeChecksum() + #elseif os(Linux) + return headerChecksum + #endif + } +} + extension IPv4Header: CustomStringConvertible { var description: String { """ diff --git a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift index 78cd4914ac4..bcf2d606c14 100644 --- a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift +++ b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift @@ -61,18 +61,8 @@ final class RawSocketBootstrapTests: XCTestCase { let header = try XCTUnwrap(data.readIPv4Header()) XCTAssertEqual(header.version, 4) XCTAssertEqual(header.protocol, .reservedForTesting) - #if canImport(Darwin) - // On BSD the IP header will only contain the size of the ip packet body not the header. - // This is known bug which can't be fixed without breaking old apps which already workaround the issue - // like we do. - XCTAssertEqual(Int(header.totalLength), data.readableBytes) - // On BSD the checksum is always zero - XCTAssertEqual(header.headerChecksum, 0) - #elseif os(Linux) - XCTAssertEqual(Int(header.totalLength), IPv4Header.size + data.readableBytes) - XCTAssertTrue(header.isValidChecksum(), "\(header)") - #endif - + XCTAssertEqual(Int(header.platformIndependentTotalLengthForReceivedPacketFromRawSocket), IPv4Header.size + data.readableBytes) + XCTAssertTrue(header.isValidChecksum(header.platformIndependentChecksumForReceivedPacketFromRawSocket), "\(header)") XCTAssertEqual(header.sourceIpAddress, .init(127, 0, 0, 1)) XCTAssertEqual(header.destinationIpAddress, .init(127, 0, 0, 1)) return String(buffer: data) @@ -114,18 +104,8 @@ final class RawSocketBootstrapTests: XCTestCase { let header = try XCTUnwrap(data.readIPv4Header()) XCTAssertEqual(header.version, 4) XCTAssertEqual(header.protocol, .reservedForTesting) - #if canImport(Darwin) - // On BSD the IP header will only contain the size of the ip packet body not the header. - // This is known bug which can't be fixed without breaking old apps which already workaround the issue - // like we do. - XCTAssertEqual(Int(header.totalLength), data.readableBytes) - // On BSD the checksum is always zero - XCTAssertEqual(header.headerChecksum, 0) - #elseif os(Linux) - XCTAssertEqual(Int(header.totalLength), IPv4Header.size + data.readableBytes) - XCTAssertTrue(header.isValidChecksum(), "\(header)") - #endif - XCTAssertTrue(header.isValidChecksum()) + XCTAssertEqual(Int(header.platformIndependentTotalLengthForReceivedPacketFromRawSocket), IPv4Header.size + data.readableBytes) + XCTAssertTrue(header.isValidChecksum(header.platformIndependentChecksumForReceivedPacketFromRawSocket), "\(header)") XCTAssertEqual(header.sourceIpAddress, .init(127, 0, 0, 1)) XCTAssertEqual(header.destinationIpAddress, .init(127, 0, 0, 1)) return String(buffer: data) @@ -173,15 +153,8 @@ final class RawSocketBootstrapTests: XCTestCase { let header = try XCTUnwrap(data.readIPv4Header()) XCTAssertEqual(header.version, 4) XCTAssertEqual(header.protocol, .reservedForTesting) - #if canImport(Darwin) - // On BSD the IP header will only contain the size of the ip packet body not the header. - // This is known bug which can't be fixed without breaking old apps which already workaround the issue - // like we do. - XCTAssertEqual(Int(header.totalLength), data.readableBytes) - #elseif os(Linux) - XCTAssertEqual(Int(header.totalLength), IPv4Header.size + data.readableBytes) - #endif - XCTAssertTrue(header.isValidChecksum()) + XCTAssertEqual(Int(header.platformIndependentTotalLengthForReceivedPacketFromRawSocket), IPv4Header.size + data.readableBytes) + XCTAssertTrue(header.isValidChecksum(header.platformIndependentChecksumForReceivedPacketFromRawSocket), "\(header)") XCTAssertEqual(header.sourceIpAddress, .init(127, 0, 0, 1)) XCTAssertEqual(header.destinationIpAddress, .init(127, 0, 0, 1)) return String(buffer: data) From 1ac14d225b8a8b50f486a1117029d0d313066545 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Wed, 30 Nov 2022 14:38:18 +0100 Subject: [PATCH 11/12] Add `readIPv4HeaderFromOSRawSocket` and `writeIPv4HeaderToOSRawSocket` --- Tests/NIOPosixTests/IPv4Header.swift | 91 ++++++++++--------- .../RawSocketBootstrapTests.swift | 13 ++- 2 files changed, 55 insertions(+), 49 deletions(-) diff --git a/Tests/NIOPosixTests/IPv4Header.swift b/Tests/NIOPosixTests/IPv4Header.swift index 9168bec634e..4aec012b292 100644 --- a/Tests/NIOPosixTests/IPv4Header.swift +++ b/Tests/NIOPosixTests/IPv4Header.swift @@ -146,32 +146,21 @@ struct IPv4Header: Hashable { } } +extension FixedWidthInteger { + func convertEndianness(to endianness: Endianness) -> Self { + switch endianness { + case .little: + return self.littleEndian + case .big: + return self.bigEndian + } + } +} + extension ByteBuffer { - @discardableResult mutating func readIPv4Header() -> IPv4Header? { - #if canImport(Darwin) - let initialState = self - guard - let versionAndIhl: UInt8 = self.readInteger(), - let dscpAndEcn: UInt8 = self.readInteger(), - // On BSD, the total length is in host byte order - let totalLength: UInt16 = self.readInteger(endianness: .host), - let identification: UInt16 = self.readInteger(), - // fragmentOffset is in host byte order as well but it is always zero in our tests - // and fragmentOffset is 13 bits in size so we can't just use readInteger(endianness: .host) - let flagsAndFragmentOffset: UInt16 = self.readInteger(), - let timeToLive: UInt8 = self.readInteger(), - let `protocol`: UInt8 = self.readInteger(), - let headerChecksum: UInt16 = self.readInteger(), - let sourceIpAddress: UInt32 = self.readInteger(), - let destinationIpAddress: UInt32 = self.readInteger() - else { - self = initialState - return nil - } - #elseif os(Linux) guard let ( versionAndIhl, dscpAndEcn, @@ -195,7 +184,6 @@ extension ByteBuffer { UInt32, UInt32 ).self) else { return nil } - #endif return .init( versionAndIhl: versionAndIhl, dscpAndEcn: dscpAndEcn, @@ -209,6 +197,19 @@ extension ByteBuffer { destinationIpAddress: .init(rawValue: destinationIpAddress) ) } + + mutating func readIPv4HeaderFromOSRawSocket() -> IPv4Header? { + #if canImport(Darwin) + guard var header = self.readIPv4Header() else { return nil } + // On BSD, the total length is in host byte order + header.totalLength = header.totalLength.convertEndianness(to: .big) + // TODO: fragmentOffset is in host byte order as well but it is always zero in our tests + // and fragmentOffset is 13 bits in size so we can't just use readInteger(endianness: .host) + return header + #else + return self.readIPv4Header() + #endif + } } extension ByteBuffer { @@ -216,31 +217,15 @@ extension ByteBuffer { mutating func writeIPv4Header(_ header: IPv4Header) -> Int { assert({ var buffer = ByteBuffer() - buffer._writeIPv4Header(header: header) + buffer._writeIPv4Header(header) let writtenHeader = buffer.readIPv4Header() return header == writtenHeader }()) - return self._writeIPv4Header(header: header) + return self._writeIPv4Header(header) } @discardableResult - private mutating func _writeIPv4Header(header: IPv4Header) -> Int { - #if canImport(Darwin) - return self.writeInteger(header.versionAndIhl) + - self.writeInteger(header.dscpAndEcn) + - // On BSD, the total length needs to be in host byte order - self.writeInteger(header.totalLength, endianness: .host) + - self.writeInteger(header.identification) + - // fragmentOffset needs to be in host byte order as well but it is always zero in our tests - // and fragmentOffset is 13 bits in size so we can't just use writeInteger(endianness: .host) - self.writeInteger(header.flagsAndFragmentOffset) + - self.writeInteger(header.timeToLive) + - self.writeInteger(header.`protocol`.rawValue) + - self.writeInteger(header.headerChecksum) + - self.writeInteger(header.sourceIpAddress.rawValue) + - self.writeInteger(header.destinationIpAddress.rawValue) - - #elseif os(Linux) + private mutating func _writeIPv4Header(_ header: IPv4Header) -> Int { return self.writeMultipleIntegers( header.versionAndIhl, header.dscpAndEcn, @@ -253,7 +238,29 @@ extension ByteBuffer { header.sourceIpAddress.rawValue, header.destinationIpAddress.rawValue ) + } + + @discardableResult + mutating func writeIPv4HeaderToOSRawSocket(_ header: IPv4Header) -> Int { + assert({ + var buffer = ByteBuffer() + buffer._writeIPv4HeaderToOSRawSocket(header) + let writtenHeader = buffer.readIPv4HeaderFromOSRawSocket() + return header == writtenHeader + }()) + return self._writeIPv4HeaderToOSRawSocket(header) + } + + @discardableResult + private mutating func _writeIPv4HeaderToOSRawSocket(_ header: IPv4Header) -> Int { + #if canImport(Darwin) + var header = header + // On BSD, the total length needs to be in host byte order + header.totalLength = header.totalLength.convertEndianness(to: .big) + // TODO: fragmentOffset needs to be in host byte order as well but it is always zero in our tests + // and fragmentOffset is 13 bits in size so we can't just use writeInteger(endianness: .host) #endif + return self._writeIPv4Header(header) } } diff --git a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift index bcf2d606c14..ff721482692 100644 --- a/Tests/NIOPosixTests/RawSocketBootstrapTests.swift +++ b/Tests/NIOPosixTests/RawSocketBootstrapTests.swift @@ -58,7 +58,7 @@ final class RawSocketBootstrapTests: XCTestCase { let receivedMessages = Set(try channel.waitForDatagrams(count: 10).map { envelop -> String in var data = envelop.data - let header = try XCTUnwrap(data.readIPv4Header()) + let header = try XCTUnwrap(data.readIPv4HeaderFromOSRawSocket()) XCTAssertEqual(header.version, 4) XCTAssertEqual(header.protocol, .reservedForTesting) XCTAssertEqual(Int(header.platformIndependentTotalLengthForReceivedPacketFromRawSocket), IPv4Header.size + data.readableBytes) @@ -101,7 +101,7 @@ final class RawSocketBootstrapTests: XCTestCase { let receivedMessages = Set(try readChannel.waitForDatagrams(count: 10).map { envelop -> String in var data = envelop.data - let header = try XCTUnwrap(data.readIPv4Header()) + let header = try XCTUnwrap(data.readIPv4HeaderFromOSRawSocket()) XCTAssertEqual(header.version, 4) XCTAssertEqual(header.protocol, .reservedForTesting) XCTAssertEqual(Int(header.platformIndependentTotalLengthForReceivedPacketFromRawSocket), IPv4Header.size + data.readableBytes) @@ -139,18 +139,17 @@ final class RawSocketBootstrapTests: XCTestCase { header.destinationIpAddress = .init(127, 0, 0, 1) header.sourceIpAddress = .init(127, 0, 0, 1) header.setChecksum() - packet.writeIPv4Header(header) + packet.writeIPv4HeaderToOSRawSocket(header) packet.writeImmutableBuffer(message) - _ = try channel.write(AddressedEnvelope( + try channel.writeAndFlush(AddressedEnvelope( remoteAddress: SocketAddress(ipAddress: "127.0.0.1", port: 0), data: packet - )) + )).wait() } - channel.flush() let receivedMessages = Set(try channel.waitForDatagrams(count: 10).map { envelop -> String in var data = envelop.data - let header = try XCTUnwrap(data.readIPv4Header()) + let header = try XCTUnwrap(data.readIPv4HeaderFromOSRawSocket()) XCTAssertEqual(header.version, 4) XCTAssertEqual(header.protocol, .reservedForTesting) XCTAssertEqual(Int(header.platformIndependentTotalLengthForReceivedPacketFromRawSocket), IPv4Header.size + data.readableBytes) From 1ad4dc496488dc9c95c07df1a9527588400f49fc Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Wed, 30 Nov 2022 17:23:27 +0100 Subject: [PATCH 12/12] Add `read/writeIPv4HeaderFromBSDRawSocket` --- Tests/NIOPosixTests/IPv4Header.swift | 29 +++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/Tests/NIOPosixTests/IPv4Header.swift b/Tests/NIOPosixTests/IPv4Header.swift index 4aec012b292..1bda7fa53ed 100644 --- a/Tests/NIOPosixTests/IPv4Header.swift +++ b/Tests/NIOPosixTests/IPv4Header.swift @@ -198,14 +198,18 @@ extension ByteBuffer { ) } - mutating func readIPv4HeaderFromOSRawSocket() -> IPv4Header? { - #if canImport(Darwin) + mutating func readIPv4HeaderFromBSDRawSocket() -> IPv4Header? { guard var header = self.readIPv4Header() else { return nil } // On BSD, the total length is in host byte order header.totalLength = header.totalLength.convertEndianness(to: .big) // TODO: fragmentOffset is in host byte order as well but it is always zero in our tests // and fragmentOffset is 13 bits in size so we can't just use readInteger(endianness: .host) return header + } + + mutating func readIPv4HeaderFromOSRawSocket() -> IPv4Header? { + #if canImport(Darwin) + return self.readIPv4HeaderFromBSDRawSocket() #else return self.readIPv4Header() #endif @@ -241,27 +245,34 @@ extension ByteBuffer { } @discardableResult - mutating func writeIPv4HeaderToOSRawSocket(_ header: IPv4Header) -> Int { + mutating func writeIPv4HeaderToBSDRawSocket(_ header: IPv4Header) -> Int { assert({ var buffer = ByteBuffer() - buffer._writeIPv4HeaderToOSRawSocket(header) - let writtenHeader = buffer.readIPv4HeaderFromOSRawSocket() + buffer._writeIPv4HeaderToBSDRawSocket(header) + let writtenHeader = buffer.readIPv4HeaderFromBSDRawSocket() return header == writtenHeader }()) - return self._writeIPv4HeaderToOSRawSocket(header) + return self._writeIPv4HeaderToBSDRawSocket(header) } @discardableResult - private mutating func _writeIPv4HeaderToOSRawSocket(_ header: IPv4Header) -> Int { - #if canImport(Darwin) + private mutating func _writeIPv4HeaderToBSDRawSocket(_ header: IPv4Header) -> Int { var header = header // On BSD, the total length needs to be in host byte order header.totalLength = header.totalLength.convertEndianness(to: .big) // TODO: fragmentOffset needs to be in host byte order as well but it is always zero in our tests // and fragmentOffset is 13 bits in size so we can't just use writeInteger(endianness: .host) - #endif return self._writeIPv4Header(header) } + + @discardableResult + mutating func writeIPv4HeaderToOSRawSocket(_ header: IPv4Header) -> Int { + #if canImport(Darwin) + self.writeIPv4HeaderToBSDRawSocket(header) + #else + self.writeIPv4Header(header) + #endif + } } extension IPv4Header {