diff --git a/Package.swift b/Package.swift index 07157329..8e4427a2 100644 --- a/Package.swift +++ b/Package.swift @@ -19,6 +19,7 @@ var targets: [PackageDescription.Target] = [ .target( name: "NIOExtras", dependencies: [ + "NIOPCAP", .product(name: "NIO", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOHTTP1", package: "swift-nio"), @@ -86,7 +87,7 @@ var targets: [PackageDescription.Target] = [ .testTarget( name: "NIOExtrasTests", dependencies: [ - "NIOExtras", + "NIOExtras", "NIOPCAP", .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOEmbedded", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), @@ -123,6 +124,12 @@ var targets: [PackageDescription.Target] = [ .product(name: "NIOEmbedded", package: "swift-nio"), .product(name: "NIOTestUtils", package: "swift-nio"), ]), + .target( + name: "NIOPCAP", // For now, this is not a product, ie. it's not exported and just an internal "helper module". + dependencies: [ + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + ]), ] let package = Package( diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift index 77840da0..768eb45c 100644 --- a/Package@swift-5.5.swift +++ b/Package@swift-5.5.swift @@ -19,6 +19,7 @@ var targets: [PackageDescription.Target] = [ .target( name: "NIOExtras", dependencies: [ + "NIOPCAP", .product(name: "NIO", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOHTTP1", package: "swift-nio") @@ -86,7 +87,7 @@ var targets: [PackageDescription.Target] = [ .testTarget( name: "NIOExtrasTests", dependencies: [ - "NIOExtras", + "NIOExtras", "NIOPCAP", .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOEmbedded", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), @@ -122,6 +123,12 @@ var targets: [PackageDescription.Target] = [ .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOTestUtils", package: "swift-nio"), ]), + .target( + name: "NIOPCAP", // For now, this is not a product, ie. it's not exported and just an internal "helper module". + dependencies: [ + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + ]), ] let package = Package( diff --git a/Sources/NIOExtras/WritePCAPHandler.swift b/Sources/NIOExtras/WritePCAPHandler.swift index 195a27e8..9f535b70 100644 --- a/Sources/NIOExtras/WritePCAPHandler.swift +++ b/Sources/NIOExtras/WritePCAPHandler.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2019-2021 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2019-2022 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,105 +12,18 @@ // //===----------------------------------------------------------------------===// -#if os(macOS) || os(tvOS) || os(iOS) || os(watchOS) +import Dispatch + +import NIOCore +import NIOPCAP +#if canImport(Darwin) import Darwin #else import Glibc #endif -import Dispatch - -import NIOCore let sysWrite = write -struct TCPHeader { - struct Flags: OptionSet { - var rawValue: UInt8 - - init(rawValue: UInt8) { - self.rawValue = rawValue - } - - static let fin = Flags(rawValue: 1 << 0) - static let syn = Flags(rawValue: 1 << 1) - static let rst = Flags(rawValue: 1 << 2) - static let psh = Flags(rawValue: 1 << 3) - static let ack = Flags(rawValue: 1 << 4) - static let urg = Flags(rawValue: 1 << 5) - static let ece = Flags(rawValue: 1 << 6) - static let cwr = Flags(rawValue: 1 << 7) - } - - var flags: Flags - var ackNumber: UInt32? - var sequenceNumber: UInt32 - var srcPort: UInt16 - var dstPort: UInt16 -} - -struct PCAPRecordHeader { - enum Error: Swift.Error { - case incompatibleAddressPair(SocketAddress, SocketAddress) - } - enum AddressTuple { - case v4(src: SocketAddress.IPv4Address, dst: SocketAddress.IPv4Address) - case v6(src: SocketAddress.IPv6Address, dst: SocketAddress.IPv6Address) - - var srcPort: UInt16 { - switch self { - case .v4(src: let src, dst: _): - return UInt16(bigEndian: src.address.sin_port) - case .v6(src: let src, dst: _): - return UInt16(bigEndian: src.address.sin6_port) - } - } - - var dstPort: UInt16 { - switch self { - case .v4(src: _, dst: let dst): - return UInt16(bigEndian: dst.address.sin_port) - case .v6(src: _, dst: let dst): - return UInt16(bigEndian: dst.address.sin6_port) - } - } - } - - var payloadLength: Int - var addresses: AddressTuple - var time: timeval - var tcp: TCPHeader - - init(payloadLength: Int, addresses: AddressTuple, time: timeval, tcp: TCPHeader) { - self.payloadLength = payloadLength - self.addresses = addresses - self.time = time - self.tcp = tcp - - assert(addresses.srcPort == Int(tcp.srcPort)) - assert(addresses.dstPort == Int(tcp.dstPort)) - assert(tcp.ackNumber == nil ? !tcp.flags.contains([.ack]) : tcp.flags.contains([.ack])) - } - - init(payloadLength: Int, src: SocketAddress, dst: SocketAddress, tcp: TCPHeader) throws { - let addressTuple: AddressTuple - switch (src, dst) { - case (.v4(let src), .v4(let dst)): - addressTuple = .v4(src: src, dst: dst) - case (.v6(let src), .v6(let dst)): - addressTuple = .v6(src: src, dst: dst) - default: - throw Error.incompatibleAddressPair(src, dst) - } - self = .init(payloadLength: payloadLength, addresses: addressTuple, tcp: tcp) - } - - init(payloadLength: Int, addresses: AddressTuple, tcp: TCPHeader) { - var tv = timeval() - gettimeofday(&tv, nil) - self = .init(payloadLength: payloadLength, addresses: addresses, time: tv, tcp: tcp) - } -} - /// A `ChannelHandler` that can write a [`.pcap` file](https://en.wikipedia.org/wiki/Pcap) containing the send/received /// data as synthesized TCP packet captures. /// @@ -187,7 +100,7 @@ public class NIOWritePCAPHandler: RemovableChannelHandler { /// Reusable header for `.pcap` file. public static var pcapFileHeader: ByteBuffer { var buffer = ByteBufferAllocator().buffer(capacity: 24) - buffer.writePCAPHeader() + buffer.writePCAPHeader(.default) return buffer } @@ -497,104 +410,6 @@ extension NIOWritePCAPHandler: ChannelDuplexHandler { } } -extension ByteBuffer { - mutating func writePCAPHeader() { - // guint32 magic_number; /* magic number */ - self.writeInteger(0xa1b2c3d4, endianness: .host, as: UInt32.self) - // guint16 version_major; /* major version number */ - self.writeInteger(2, endianness: .host, as: UInt16.self) - // guint16 version_minor; /* minor version number * - self.writeInteger(4, endianness: .host, as: UInt16.self) - // gint32 thiszone; /* GMT to local correction */ - self.writeInteger(0, endianness: .host, as: UInt32.self) - // guint32 sigfigs; /* accuracy of timestamps */ - self.writeInteger(0, endianness: .host, as: UInt32.self) - // guint32 snaplen; /* max length of captured packets, in octets */ - self.writeInteger(.max, endianness: .host, as: UInt32.self) - // guint32 network; /* data link type */ - self.writeInteger(0, endianness: .host, as: UInt32.self) - } - - mutating func writePCAPRecord(_ record: PCAPRecordHeader) throws { - let rawDataLength = record.payloadLength - let tcpLength = rawDataLength + 20 /* TCP header length */ - - // record - // guint32 ts_sec; /* timestamp seconds */ - self.writeInteger(.init(record.time.tv_sec), endianness: .host, as: UInt32.self) - // guint32 ts_usec; /* timestamp microseconds */ - self.writeInteger(.init(record.time.tv_usec), endianness: .host, as: UInt32.self) - // continued below ... - - switch record.addresses { - case .v4(let la, let ra): - let ipv4WholeLength = tcpLength + 20 /* IPv4 header length, included in IPv4 */ - let recordLength = ipv4WholeLength + 4 /* 32 bits for protocol id */ - - // record, continued - // guint32 incl_len; /* number of octets of packet saved in file */ - self.writeInteger(.init(recordLength), endianness: .host, as: UInt32.self) - // guint32 orig_len; /* actual length of packet */ - self.writeInteger(.init(recordLength), endianness: .host, as: UInt32.self) - - self.writeInteger(2, endianness: .host, as: UInt32.self) // IPv4 - - // IPv4 packet - self.writeInteger(0x45, as: UInt8.self) // IP version (4) & IHL (5) - self.writeInteger(0, as: UInt8.self) // DSCP - self.writeInteger(.init(ipv4WholeLength), as: UInt16.self) - - self.writeInteger(0, as: UInt16.self) // identification - self.writeInteger(0x4000 /* this set's "don't fragment" */, as: UInt16.self) // flags & fragment offset - self.writeInteger(.max /* we don't care about TTL */, as: UInt8.self) // TTL - self.writeInteger(6, as: UInt8.self) // TCP - self.writeInteger(0, as: UInt16.self) // checksum - self.writeInteger(la.address.sin_addr.s_addr, endianness: .host, as: UInt32.self) - self.writeInteger(ra.address.sin_addr.s_addr, endianness: .host, as: UInt32.self) - case .v6(let la, let ra): - let ipv6PayloadLength = tcpLength - let recordLength = ipv6PayloadLength + 4 /* 32 bits for protocol id */ + 40 /* IPv6 header length */ - - // record, continued - // guint32 incl_len; /* number of octets of packet saved in file */ - self.writeInteger(.init(recordLength), endianness: .host, as: UInt32.self) - // guint32 orig_len; /* actual length of packet */ - self.writeInteger(.init(recordLength), endianness: .host, as: UInt32.self) - - self.writeInteger(24, endianness: .host, as: UInt32.self) // IPv6 - - // IPv6 packet - self.writeInteger(/* version */ (6 << 28), as: UInt32.self) // IP version (6) & fancy stuff - self.writeInteger(.init(ipv6PayloadLength), as: UInt16.self) - self.writeInteger(6, as: UInt8.self) // TCP - self.writeInteger(.max /* we don't care about TTL */, as: UInt8.self) // hop limit (like TTL) - - var laAddress = la.address - withUnsafeBytes(of: &laAddress.sin6_addr) { ptr in - assert(ptr.count == 16) - self.writeBytes(ptr) - } - var raAddress = ra.address - withUnsafeBytes(of: &raAddress.sin6_addr) { ptr in - assert(ptr.count == 16) - self.writeBytes(ptr) - } - } - - // TCP - self.writeInteger(record.tcp.srcPort, as: UInt16.self) - self.writeInteger(record.tcp.dstPort, as: UInt16.self) - - self.writeInteger(record.tcp.sequenceNumber, as: UInt32.self) // seq no - self.writeInteger(record.tcp.ackNumber ?? 0, as: UInt32.self) // ack no - - self.writeInteger(5 << 12 | UInt16(record.tcp.flags.rawValue), as: UInt16.self) // data offset + reserved bits + fancy stuff - self.writeInteger(.max /* we don't do actual window sizes */, as: UInt16.self) // window size - self.writeInteger(0xbad /* fake */, as: UInt16.self) // checksum - self.writeInteger(0, as: UInt16.self) // urgent pointer - } -} - extension NIOWritePCAPHandler { /// A synchronised file sink that uses a `DispatchQueue` to do all the necessary write synchronously. /// diff --git a/Sources/NIOPCAP/PCAP.swift b/Sources/NIOPCAP/PCAP.swift new file mode 100644 index 00000000..ac7140a3 --- /dev/null +++ b/Sources/NIOPCAP/PCAP.swift @@ -0,0 +1,493 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2019-2023 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 +import NIOPosix + +public struct NIOPCAPParsingError: Error & Sendable { + public var problem: String +} + +public struct PCAP2Header { + public var endianness: Endianness + public let majorVersion: Int = 2 + public var minorVersion: Int + public var gmtOffset: Int + public var timestampAccuracy: Int + public var maximumSnapLength: UInt32 + public var dataLinkType: UInt32 + + public init(endianness: Endianness, + minorVersion: Int, + gmtOffset: Int, + timestampAccuracy: Int, + maximumSnapLength: UInt32, + dataLinkType: UInt32) { + self.endianness = endianness + self.minorVersion = minorVersion + self.gmtOffset = gmtOffset + self.timestampAccuracy = timestampAccuracy + self.maximumSnapLength = maximumSnapLength + self.dataLinkType = dataLinkType + } + + public static let `default`: Self = .init(endianness: .host, + minorVersion: 4, + gmtOffset: 0, + timestampAccuracy: .max, + maximumSnapLength: .max, + dataLinkType: 0) +} + +public struct PCAPReadError: Error, Hashable { + private enum ErrorKind: Hashable { + case invalidMagic + case unsupportedVersion + } + + private var errorKind: ErrorKind + + public static let invalidMagic = Self(errorKind: .invalidMagic) + public static let unsupportedVersion = Self(errorKind: .unsupportedVersion) +} + +public struct TCPHeader { + public struct Flags: OptionSet { + public var rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } + + public static let fin = Flags(rawValue: 1 << 0) + public static let syn = Flags(rawValue: 1 << 1) + public static let rst = Flags(rawValue: 1 << 2) + public static let psh = Flags(rawValue: 1 << 3) + public static let ack = Flags(rawValue: 1 << 4) + public static let urg = Flags(rawValue: 1 << 5) + public static let ece = Flags(rawValue: 1 << 6) + public static let cwr = Flags(rawValue: 1 << 7) + } + + public var flags: Flags + public var ackNumber: UInt32? + public var sequenceNumber: UInt32 + public var srcPort: UInt16 + public var dstPort: UInt16 + public var headerSizeInBytes: UInt8 { + didSet { + assert(self.headerSizeInBytes >= 20 && self.headerSizeInBytes <= 60) + } + } + + public init(flags: TCPHeader.Flags, + ackNumber: UInt32? = nil, + sequenceNumber: UInt32, + srcPort: UInt16, + dstPort: UInt16, + headerSizeInBytes: UInt8 = 20) { + self.flags = flags + self.ackNumber = ackNumber + self.sequenceNumber = sequenceNumber + self.srcPort = srcPort + self.dstPort = dstPort + self.headerSizeInBytes = headerSizeInBytes + precondition(self.headerSizeInBytes >= 20 && self.headerSizeInBytes <= 60) + } +} + +public struct PCAPRecordHeader { + public enum Error: Swift.Error { + case incompatibleAddressPair(SocketAddress, SocketAddress) + } + public enum AddressTuple { + case v4(src: SocketAddress.IPv4Address, dst: SocketAddress.IPv4Address) + case v6(src: SocketAddress.IPv6Address, dst: SocketAddress.IPv6Address) + + public var srcPort: UInt16 { + switch self { + case .v4(src: let src, dst: _): + return UInt16(bigEndian: src.address.sin_port) + case .v6(src: let src, dst: _): + return UInt16(bigEndian: src.address.sin6_port) + } + } + + public var dstPort: UInt16 { + switch self { + case .v4(src: _, dst: let dst): + return UInt16(bigEndian: dst.address.sin_port) + case .v6(src: _, dst: let dst): + return UInt16(bigEndian: dst.address.sin6_port) + } + } + } + + public var payloadLength: Int + public var addresses: AddressTuple + public var time: timeval + public var tcp: TCPHeader + + public init(payloadLength: Int, addresses: AddressTuple, time: timeval, tcp: TCPHeader) { + self.payloadLength = payloadLength + self.addresses = addresses + self.time = time + self.tcp = tcp + + assert(addresses.srcPort == Int(tcp.srcPort)) + assert(addresses.dstPort == Int(tcp.dstPort)) + assert(tcp.ackNumber == nil ? !tcp.flags.contains([.ack]) : tcp.flags.contains([.ack])) + } + + public init(payloadLength: Int, src: SocketAddress, dst: SocketAddress, tcp: TCPHeader) throws { + let addressTuple: AddressTuple + switch (src, dst) { + case (.v4(let src), .v4(let dst)): + addressTuple = .v4(src: src, dst: dst) + case (.v6(let src), .v6(let dst)): + addressTuple = .v6(src: src, dst: dst) + default: + throw Error.incompatibleAddressPair(src, dst) + } + self = .init(payloadLength: payloadLength, addresses: addressTuple, tcp: tcp) + } + + public init(payloadLength: Int, addresses: AddressTuple, tcp: TCPHeader) { + var tv = timeval() + gettimeofday(&tv, nil) + self = .init(payloadLength: payloadLength, addresses: addresses, time: tv, tcp: tcp) + } +} + +public struct PCAPRecord { + public var time: timeval + public var header: PCAPRecordHeader + public var pcapProtocolID: UInt32 + public var payload: ByteBuffer + + public init(time: timeval, header: PCAPRecordHeader, pcapProtocolID: UInt32, payload: ByteBuffer) { + self.time = time + self.header = header + self.pcapProtocolID = pcapProtocolID + self.payload = payload + } +} + +public struct TCPIPv4Packet { + public var src: in_addr + public var dst: in_addr + public var wholeIPPacketLength: Int + public var tcpHeader: TCPHeader + public var rawTCPOptions: ByteBuffer + public var tcpPayload: ByteBuffer + + public init(src: in_addr, dst: in_addr, wholeIPPacketLength: Int, tcpHeader: TCPHeader, rawTCPOptions: ByteBuffer, tcpPayload: ByteBuffer) { + self.src = src + self.dst = dst + self.wholeIPPacketLength = wholeIPPacketLength + self.tcpHeader = tcpHeader + self.rawTCPOptions = rawTCPOptions + self.tcpPayload = tcpPayload + } +} + +public struct TCPIPv6Packet { + public var src: in6_addr + public var dst: in6_addr + public var payloadLength: Int + public var tcpHeader: TCPHeader + public var tcpPayload: ByteBuffer + + public init(src: in6_addr, dst: in6_addr, payloadLength: Int, tcpHeader: TCPHeader, tcpPayload: ByteBuffer) { + self.src = src + self.dst = dst + self.payloadLength = payloadLength + self.tcpHeader = tcpHeader + self.tcpPayload = tcpPayload + } +} + +extension ByteBuffer { + // read & parse a TCP packet, containing everything belonging to it (including payload) + public mutating func readTCPHeader() throws -> TCPHeader? { + let saveSelf = self + guard let srcPort = self.readInteger(as: UInt16.self), + let dstPort = self.readInteger(as: UInt16.self), + let seqNo = self.readInteger(as: UInt32.self), // seq no + let ackNo = self.readInteger(as: UInt32.self), // ack no + let flagsAndFriends = self.readInteger(as: UInt16.self), // data offset + reserved bits + fancy stuff + let _ = self.readInteger(as: UInt16.self), // window size + let _ = self.readInteger(as: UInt16.self), // checksum + let _ = self.readInteger(as: UInt16.self) /* urgent pointer */ else { + self = saveSelf + return nil + } + let dataOffset = (flagsAndFriends & (0xf << 12)) >> 12 + guard dataOffset >= 5 && dataOffset <= 15 else { + throw NIOPCAPParsingError(problem: "illegal TCP data offset \(dataOffset)") + } + + return TCPHeader(flags: .init(rawValue: UInt8(flagsAndFriends & 0xfff)), + ackNumber: ackNo == 0 ? nil : ackNo, + sequenceNumber: seqNo, + srcPort: srcPort, + dstPort: dstPort, + headerSizeInBytes: UInt8(dataOffset) * 4) + } + + // read & parse a TCP/IPv4 packet, containing everything belonging to it (including payload) + public mutating func readTCPIPv4() throws -> TCPIPv4Packet? { + struct ParsingError: Error {} + + let saveSelf = self + guard let version = self.readInteger(as: UInt8.self), + let _ = self.readInteger(as: UInt8.self), // DSCP + let ipv4WholeLength = self.readInteger(as: UInt16.self), + let _ = self.readInteger(as: UInt16.self), // identification + let _ = self.readInteger(as: UInt16.self), // flags & fragment offset + let _ = self.readInteger(as: UInt8.self), // TTL + let innerProtocolID = self.readInteger(as: UInt8.self), // TCP + let _ = self.readInteger(as: UInt16.self), // checksum + let srcRaw = self.readInteger(endianness: .host, as: UInt32.self), + let dstRaw = self.readInteger(endianness: .host, as: UInt32.self), + var payload = self.readSlice(length: Int(ipv4WholeLength - 20)), + let tcp = try payload.readTCPHeader(), + let tcpOptions = payload.readSlice(length: Int(tcp.headerSizeInBytes - 20)) else { + self = saveSelf + return nil + } + guard version == 0x45, innerProtocolID == 6 /* TCP is 6 */ else { + throw NIOPCAPParsingError(problem: "\(version)/\(innerProtocolID) don't match IPv6") + } + + let src = in_addr(s_addr: srcRaw) + let dst = in_addr(s_addr: dstRaw) + return TCPIPv4Packet(src: src, + dst: dst, + wholeIPPacketLength: .init(ipv4WholeLength), + tcpHeader: tcp, + rawTCPOptions: tcpOptions, + tcpPayload: payload) + } + + // read & parse a TCP/IPv6 packet, containing everything belonging to it (including payload) + public mutating func readTCPIPv6() throws -> TCPIPv6Packet? { + let saveSelf = self + guard let versionAndFancyStuff = self.readInteger(as: UInt32.self), // IP version (6) & fancy stuff + let payloadLength = self.readInteger(as: UInt16.self), + let innerProtocolID = self.readInteger(as: UInt8.self), // TCP + let _ = self.readInteger(as: UInt8.self), // hop limit (like TTL) + var srcAddrBuffer = self.readSlice(length: MemoryLayout.size), + var dstAddrBuffer = self.readSlice(length: MemoryLayout.size), + var payload = self.readSlice(length: Int(payloadLength)), + let tcp = try payload.readTCPHeader() else { + self = saveSelf + return nil + } + guard versionAndFancyStuff >> 28 == 6 /* IPv_6_ */, innerProtocolID == 6 /* TCP is 6 */ else { + return nil + } + + var srcAddress = in6_addr() + var dstAddress = in6_addr() + withUnsafeMutableBytes(of: &srcAddress) { copyDestPtr in + _ = srcAddrBuffer.readWithUnsafeReadableBytes { copySrcPtr in + precondition(copyDestPtr.count == copySrcPtr.count) + copyDestPtr.copyMemory(from: copySrcPtr) + return copyDestPtr.count + } + } + withUnsafeMutableBytes(of: &dstAddress) { copyDestPtr in + _ = dstAddrBuffer.readWithUnsafeReadableBytes { copySrcPtr in + precondition(copyDestPtr.count == copySrcPtr.count) + copyDestPtr.copyMemory(from: copySrcPtr) + return copyDestPtr.count + } + } + + return TCPIPv6Packet(src: srcAddress, + dst: dstAddress, + payloadLength: .init(payloadLength), + tcpHeader: tcp, + tcpPayload: payload) + } + + // read a PCAP record, including all its payload + public mutating func readPCAPRecord(endianness: Endianness = .host) -> PCAPRecord? { + let saveSelf = self // save the buffer in case we don't have enough to parse + + guard let timeSecs = self.readInteger(endianness: endianness, as: UInt32.self), + let timeUSecs = self.readInteger(endianness: endianness, as: UInt32.self), + let lenPacket = self.readInteger(endianness: endianness, as: UInt32.self), + let lenDisk = self.readInteger(endianness: endianness, as: UInt32.self), + let pcapProtocolID = self.readInteger(endianness: endianness, as: UInt32.self), + let payload = self.readSlice(length: Int(lenDisk - 4)) else { + self = saveSelf + return nil + } + + assert(lenPacket == lenDisk, "\(lenPacket) != \(lenDisk)") + + let notImplementedAddress = try! SocketAddress(ipAddress: "9.9.9.9", port: 0xbad) + let tcp = TCPHeader(flags: [], + ackNumber: nil, + sequenceNumber: 0xbad, + srcPort: 0xbad, + dstPort: 0xbad, + headerSizeInBytes: 20) + return .init(time: timeval(tv_sec: .init(timeSecs), tv_usec: .init(timeUSecs)), + header: try! PCAPRecordHeader(payloadLength: .init(lenPacket), + src: notImplementedAddress, + dst: notImplementedAddress, + tcp: tcp), + pcapProtocolID: pcapProtocolID, + payload: payload) + } +} + +extension ByteBuffer { + mutating func readPCAP2Header() throws -> PCAP2Header? { + let save = self + guard let magic = self.readInteger(endianness: .big, as: UInt32.self) else { + self = save + return nil + } + + let wantedMagic: UInt32 = 0xa1b2c3d4 + let endianness: Endianness + switch magic { + case wantedMagic: + endianness = .big + case UInt32(bigEndian: wantedMagic): + endianness = .little + default: + throw PCAPReadError.invalidMagic + } + + guard let values = self.readMultipleIntegers(endianness: endianness, + as: (UInt16, UInt16, UInt32, UInt32, UInt32, UInt32).self) else { + self = save + return nil + } + + let (major, minor, gmtOffset, timestampAccuracy, snapLen, network) = values + + guard major == 2 else { + throw PCAPReadError.unsupportedVersion + } + + return PCAP2Header(endianness: endianness, + minorVersion: Int(minor), + gmtOffset: Int(gmtOffset), + timestampAccuracy: Int(timestampAccuracy), + maximumSnapLength: snapLen, + dataLinkType: network) + } + + public mutating func writePCAPHeader(_ pcapHeader: PCAP2Header) { + // guint32 magic_number; /* magic number */ + self.writeInteger(0xa1b2c3d4, endianness: .host, as: UInt32.self) + // guint16 version_major; /* major version number */ + self.writeInteger(UInt16(pcapHeader.majorVersion), endianness: .host, as: UInt16.self) + // guint16 version_minor; /* minor version number * + self.writeInteger(UInt16(pcapHeader.minorVersion), endianness: .host, as: UInt16.self) + // gint32 thiszone; /* GMT to local correction */ + self.writeInteger(UInt32(pcapHeader.gmtOffset), endianness: .host, as: UInt32.self) + // guint32 sigfigs; /* accuracy of timestamps */ + self.writeInteger(UInt32(truncatingIfNeeded: pcapHeader.timestampAccuracy), endianness: .host, as: UInt32.self) + // guint32 snaplen; /* max length of captured packets, in octets */ + self.writeInteger(UInt32(pcapHeader.maximumSnapLength), endianness: .host, as: UInt32.self) + // guint32 network; /* data link type */ + self.writeInteger(pcapHeader.dataLinkType, endianness: .host, as: UInt32.self) + } + + public mutating func writePCAPRecord(_ record: PCAPRecordHeader) throws { + let rawDataLength = record.payloadLength + let tcpLength = rawDataLength + 20 /* TCP header length */ + + // record + // guint32 ts_sec; /* timestamp seconds */ + self.writeInteger(.init(record.time.tv_sec), endianness: .host, as: UInt32.self) + // guint32 ts_usec; /* timestamp microseconds */ + self.writeInteger(.init(record.time.tv_usec), endianness: .host, as: UInt32.self) + // continued below ... + + switch record.addresses { + case .v4(let la, let ra): + let ipv4WholeLength = tcpLength + 20 /* IPv4 header length, included in IPv4 */ + let recordLength = ipv4WholeLength + 4 /* 32 bits for protocol id */ + + // record, continued + // guint32 incl_len; /* number of octets of packet saved in file */ + self.writeInteger(.init(recordLength), endianness: .host, as: UInt32.self) + // guint32 orig_len; /* actual length of packet */ + self.writeInteger(.init(recordLength), endianness: .host, as: UInt32.self) + + self.writeInteger(2, endianness: .host, as: UInt32.self) // IPv4 + + // IPv4 packet + self.writeInteger(0x45, as: UInt8.self) // IP version (4) & IHL (5) + self.writeInteger(0, as: UInt8.self) // DSCP + self.writeInteger(.init(ipv4WholeLength), as: UInt16.self) + + self.writeInteger(0, as: UInt16.self) // identification + self.writeInteger(0x4000 /* this set's "don't fragment" */, as: UInt16.self) // flags & fragment offset + self.writeInteger(.max /* we don't care about TTL */, as: UInt8.self) // TTL + self.writeInteger(6, as: UInt8.self) // TCP + self.writeInteger(0, as: UInt16.self) // checksum + self.writeInteger(la.address.sin_addr.s_addr, endianness: .host, as: UInt32.self) + self.writeInteger(ra.address.sin_addr.s_addr, endianness: .host, as: UInt32.self) + case .v6(let la, let ra): + let ipv6PayloadLength = tcpLength + let recordLength = ipv6PayloadLength + 4 /* 32 bits for protocol id */ + 40 /* IPv6 header length */ + + // record, continued + // guint32 incl_len; /* number of octets of packet saved in file */ + self.writeInteger(.init(recordLength), endianness: .host, as: UInt32.self) + // guint32 orig_len; /* actual length of packet */ + self.writeInteger(.init(recordLength), endianness: .host, as: UInt32.self) + + self.writeInteger(24, endianness: .host, as: UInt32.self) // IPv6 + + // IPv6 packet + self.writeInteger(/* version */ (6 << 28), as: UInt32.self) // IP version (6) & fancy stuff + self.writeInteger(.init(ipv6PayloadLength), as: UInt16.self) + self.writeInteger(6, as: UInt8.self) // TCP + self.writeInteger(.max /* we don't care about TTL */, as: UInt8.self) // hop limit (like TTL) + + var laAddress = la.address + withUnsafeBytes(of: &laAddress.sin6_addr) { ptr in + assert(ptr.count == 16) + self.writeBytes(ptr) + } + var raAddress = ra.address + withUnsafeBytes(of: &raAddress.sin6_addr) { ptr in + assert(ptr.count == 16) + self.writeBytes(ptr) + } + } + + // TCP + self.writeInteger(record.tcp.srcPort, as: UInt16.self) + self.writeInteger(record.tcp.dstPort, as: UInt16.self) + + self.writeInteger(record.tcp.sequenceNumber, as: UInt32.self) // seq no + self.writeInteger(record.tcp.ackNumber ?? 0, as: UInt32.self) // ack no + + self.writeInteger(5 << 12 | UInt16(record.tcp.flags.rawValue), as: UInt16.self) // data offset + reserved bits + fancy stuff + self.writeInteger(.max /* we don't do actual window sizes */, as: UInt16.self) // window size + self.writeInteger(0xbad /* fake */, as: UInt16.self) // checksum + self.writeInteger(0, as: UInt16.self) // urgent pointer + } +} diff --git a/Sources/NIOPCAP/PCAPDecoder.swift b/Sources/NIOPCAP/PCAPDecoder.swift new file mode 100644 index 00000000..fcd28ee4 --- /dev/null +++ b/Sources/NIOPCAP/PCAPDecoder.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2022-2023 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 + +public struct PCAPDecoder: NIOSingleStepByteToMessageDecoder { + public typealias InboundOut = PCAPRecord + + private enum State { + case header + case record(PCAP2Header) + } + + private var state = State.header + + public mutating func decode(buffer: inout ByteBuffer) throws -> PCAPRecord? { + switch self.state { + case .header: + if let header = try buffer.readPCAP2Header() { + self.state = .record(header) + } + return nil + case .record(let header): + return buffer.readPCAPRecord(endianness: header.endianness) + } + } + + public mutating func decodeLast(buffer: inout ByteBuffer, seenEOF: Bool) throws -> PCAPRecord? { + return try self.decode(buffer: &buffer) + } +} diff --git a/Tests/NIOExtrasTests/PCAPRingBufferTest.swift b/Tests/NIOExtrasTests/PCAPRingBufferTest.swift index 40343253..37d5b521 100644 --- a/Tests/NIOExtrasTests/PCAPRingBufferTest.swift +++ b/Tests/NIOExtrasTests/PCAPRingBufferTest.swift @@ -16,7 +16,7 @@ import XCTest import NIOCore import NIOEmbedded -@testable import NIOExtras +import NIOExtras class PCAPRingBufferTest: XCTestCase { private func dataForTests() -> [ByteBuffer] { diff --git a/Tests/NIOExtrasTests/WritePCAPHandlerTest.swift b/Tests/NIOExtrasTests/WritePCAPHandlerTest.swift index ab873841..604dbb63 100644 --- a/Tests/NIOExtrasTests/WritePCAPHandlerTest.swift +++ b/Tests/NIOExtrasTests/WritePCAPHandlerTest.swift @@ -17,6 +17,7 @@ import XCTest import NIOCore import NIOEmbedded +import NIOPCAP @testable import NIOExtras class WritePCAPHandlerTest: XCTestCase { @@ -690,157 +691,3 @@ class WritePCAPHandlerTest: XCTestCase { } } - -struct PCAPRecord { - var time: timeval - var header: PCAPRecordHeader - var pcapProtocolID: UInt32 - var payload: ByteBuffer -} - -struct TCPIPv4Packet { - var src: in_addr - var dst: in_addr - var wholeIPPacketLength: Int - var tcpHeader: TCPHeader - var tcpPayload: ByteBuffer -} - -struct TCPIPv6Packet { - var src: in6_addr - var dst: in6_addr - var payloadLength: Int - var tcpHeader: TCPHeader - var tcpPayload: ByteBuffer -} - -extension ByteBuffer { - // read & parse a TCP packet, containing everything belonging to it (including payload) - mutating func readTCPHeader() throws -> TCPHeader? { - struct ParsingError: Error {} - - let saveSelf = self - guard let srcPort = self.readInteger(as: UInt16.self), - let dstPort = self.readInteger(as: UInt16.self), - let seqNo = self.readInteger(as: UInt32.self), // seq no - let ackNo = self.readInteger(as: UInt32.self), // ack no - let flagsAndFriends = self.readInteger(as: UInt16.self), // data offset + reserved bits + fancy stuff - let _ = self.readInteger(as: UInt16.self), // window size - let _ = self.readInteger(as: UInt16.self), // checksum - let _ = self.readInteger(as: UInt16.self) /* urgent pointer */ else { - self = saveSelf - return nil - } - guard (flagsAndFriends & (0xf << 12)) == (0x5 << 12) /* check that the data offset is right */ else { - throw ParsingError() - } - - return TCPHeader(flags: .init(rawValue: UInt8(flagsAndFriends & 0xfff)), - ackNumber: ackNo == 0 ? nil : ackNo, - sequenceNumber: seqNo, - srcPort: srcPort, - dstPort: dstPort) - } - - // read & parse a TCP/IPv4 packet, containing everything belonging to it (including payload) - mutating func readTCPIPv4() throws -> TCPIPv4Packet? { - struct ParsingError: Error {} - - let saveSelf = self - guard let version = self.readInteger(as: UInt8.self), - let _ = self.readInteger(as: UInt8.self), // DSCP - let ipv4WholeLength = self.readInteger(as: UInt16.self), - let _ = self.readInteger(as: UInt16.self), // identification - let _ = self.readInteger(as: UInt16.self), // flags & fragment offset - let _ = self.readInteger(as: UInt8.self), // TTL - let innerProtocolID = self.readInteger(as: UInt8.self), // TCP - let _ = self.readInteger(as: UInt16.self), // checksum - let srcRaw = self.readInteger(endianness: .host, as: UInt32.self), - let dstRaw = self.readInteger(endianness: .host, as: UInt32.self), - var payload = self.readSlice(length: Int(ipv4WholeLength - 20)), - let tcp = try payload.readTCPHeader() else { - self = saveSelf - return nil - } - guard version == 0x45, innerProtocolID == 6 /* TCP is 6 */ else { - throw ParsingError() - } - - let src = in_addr(s_addr: srcRaw) - let dst = in_addr(s_addr: dstRaw) - return TCPIPv4Packet(src: src, - dst: dst, - wholeIPPacketLength: .init(ipv4WholeLength), - tcpHeader: tcp, - tcpPayload: payload) - } - - // read & parse a TCP/IPv6 packet, containing everything belonging to it (including payload) - mutating func readTCPIPv6() throws -> TCPIPv6Packet? { - let saveSelf = self - guard let versionAndFancyStuff = self.readInteger(as: UInt32.self), // IP version (6) & fancy stuff - let payloadLength = self.readInteger(as: UInt16.self), - let innerProtocolID = self.readInteger(as: UInt8.self), // TCP - let _ = self.readInteger(as: UInt8.self), // hop limit (like TTL) - var srcAddrBuffer = self.readSlice(length: MemoryLayout.size), - var dstAddrBuffer = self.readSlice(length: MemoryLayout.size), - var payload = self.readSlice(length: Int(payloadLength)), - let tcp = try payload.readTCPHeader() else { - self = saveSelf - return nil - } - guard versionAndFancyStuff >> 28 == 6 /* IPv_6_ */, innerProtocolID == 6 /* TCP is 6 */ else { - return nil - } - - var srcAddress = in6_addr() - var dstAddress = in6_addr() - withUnsafeMutableBytes(of: &srcAddress) { copyDestPtr in - _ = srcAddrBuffer.readWithUnsafeReadableBytes { copySrcPtr in - precondition(copyDestPtr.count == copySrcPtr.count) - copyDestPtr.copyMemory(from: copySrcPtr) - return copyDestPtr.count - } - } - withUnsafeMutableBytes(of: &dstAddress) { copyDestPtr in - _ = dstAddrBuffer.readWithUnsafeReadableBytes { copySrcPtr in - precondition(copyDestPtr.count == copySrcPtr.count) - copyDestPtr.copyMemory(from: copySrcPtr) - return copyDestPtr.count - } - } - - return TCPIPv6Packet(src: srcAddress, - dst: dstAddress, - payloadLength: .init(payloadLength), - tcpHeader: tcp, - tcpPayload: payload) - } - - // read a PCAP record, including all its payload - mutating func readPCAPRecord() -> PCAPRecord? { - let saveSelf = self // save the buffer in case we don't have enough to parse - - guard let timeSecs = self.readInteger(endianness: .host, as: UInt32.self), - let timeUSecs = self.readInteger(endianness: .host, as: UInt32.self), - let lenPacket = self.readInteger(endianness: .host, as: UInt32.self), - let lenDisk = self.readInteger(endianness: .host, as: UInt32.self), - let pcapProtocolID = self.readInteger(endianness: .host, as: UInt32.self), - let payload = self.readSlice(length: Int(lenDisk - 4)) else { - self = saveSelf - return nil - } - - assert(lenPacket == lenDisk, "\(lenPacket) != \(lenDisk)") - - let notImplementedAddress = try! SocketAddress(ipAddress: "9.9.9.9", port: 0xbad) - let tcp = TCPHeader(flags: [], ackNumber: nil, sequenceNumber: 0xbad, srcPort: 0xbad, dstPort: 0xbad) - return .init(time: timeval(tv_sec: .init(timeSecs), tv_usec: .init(timeUSecs)), - header: try! PCAPRecordHeader(payloadLength: .init(lenPacket), - src: notImplementedAddress, - dst: notImplementedAddress, - tcp: tcp), - pcapProtocolID: pcapProtocolID, - payload: payload) - } -} diff --git a/Tests/NIONFS3Tests/NFS3FileSystemTests+XCTest.swift b/Tests/NIONFS3Tests/NFS3FileSystemTests+XCTest.swift index a45d480d..78cacfec 100644 --- a/Tests/NIONFS3Tests/NFS3FileSystemTests+XCTest.swift +++ b/Tests/NIONFS3Tests/NFS3FileSystemTests+XCTest.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2018-2022 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2018-2023 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/NIONFS3Tests/NFS3ReplyEncoderTest+XCTest.swift b/Tests/NIONFS3Tests/NFS3ReplyEncoderTest+XCTest.swift index f2c5309d..96148cb6 100644 --- a/Tests/NIONFS3Tests/NFS3ReplyEncoderTest+XCTest.swift +++ b/Tests/NIONFS3Tests/NFS3ReplyEncoderTest+XCTest.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2018-2022 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2018-2023 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/NIONFS3Tests/NFS3RoundtripTests+XCTest.swift b/Tests/NIONFS3Tests/NFS3RoundtripTests+XCTest.swift index 8dff52ce..c23d70bc 100644 --- a/Tests/NIONFS3Tests/NFS3RoundtripTests+XCTest.swift +++ b/Tests/NIONFS3Tests/NFS3RoundtripTests+XCTest.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2018-2022 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2018-2023 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information