write PCAP handler (#46)

Motivation:

Especially with TLS but also without, in real production environments it
can be handy to be able to write pcap files from NIO directly.

Modifications:

add a ChannelHandler that can write a PCAP trace from what's going on in
the ChannelPipeline.

Result:

easier debugging in production
This commit is contained in:
Johannes Weiss 2019-04-12 15:08:10 +01:00 committed by GitHub
parent aad5c1ca6a
commit 96e8335180
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1461 additions and 1 deletions

View File

@ -19,6 +19,7 @@ var targets: [PackageDescription.Target] = [
.target(name: "NIOExtras", dependencies: ["NIO"]),
.target(name: "NIOHTTPCompression", dependencies: ["NIO", "NIOHTTP1", "CNIOExtrasZlib"]),
.target(name: "HTTPServerWithQuiescingDemo", dependencies: ["NIOExtras", "NIOHTTP1"]),
.target(name: "NIOWritePCAPDemo", dependencies: ["NIO", "NIOExtras", "NIOHTTP1"]),
.target(name: "CNIOExtrasZlib",
dependencies: [],
linkerSettings: [
@ -32,6 +33,7 @@ let package = Package(
name: "swift-nio-extras",
products: [
.executable(name: "HTTPServerWithQuiescingDemo", targets: ["HTTPServerWithQuiescingDemo"]),
.executable(name: "NIOWritePCAPDemo", targets: ["NIOWritePCAPDemo"]),
.library(name: "NIOExtras", targets: ["NIOExtras"]),
.library(name: "NIOHTTPCompression", targets: ["NIOHTTPCompression"]),
],

View File

@ -52,6 +52,10 @@ private final class HTTPHandler: ChannelInboundHandler {
}
}
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
print(error)
}
}
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
@ -70,6 +74,7 @@ signal(SIGINT, SIG_IGN)
signalSource.resume()
do {
let serverChannel = try ServerBootstrap(group: group)
.serverChannelOption(ChannelOptions.backlog, value: 256)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)

View File

@ -0,0 +1,645 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2019 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
//
//===----------------------------------------------------------------------===//
#if os(macOS) || os(tvOS) || os(iOS) || os(watchOS)
import Darwin
#else
import Glibc
#endif
import Dispatch
import NIO
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: Int?
var sequenceNumber: Int
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.
///
/// You will be able to open the `.pcap` file in for example [Wireshark](https://www.wireshark.org) or
/// [`tcpdump`](http://www.tcpdump.org). Using `NIOWritePCAPHandler` to write your `.pcap` files can be useful for
/// example when your real network traffic is TLS protected (so `tcpdump`/Wireshark can't read it directly), or if you
/// don't have enough privileges on the running host to dump the network traffic.
///
/// `NIOWritePCAPHandler` will also work with Unix Domain Sockets in which case it will still synthesize a TCP packet
/// capture with local address `111.111.111.111` (port `1111`) and remote address `222.222.222.222` (port `2222`).
public class NIOWritePCAPHandler {
public enum Mode {
case client
case server
}
private enum CloseState {
case notClosing
case closedInitiatorLocal
case closedInitiatorRemote
}
private let fileSink: (ByteBuffer) -> Void
private let mode: Mode
private let maxPayloadSize = Int(UInt16.max - 40 /* needs to fit into the IPv4 header which adds 40 */)
private var buffer: ByteBuffer!
private var readInboundBytes = 0
private var writtenOutboundBytes = 0
private var closeState = CloseState.notClosing
private static let fakeLocalAddress = try! SocketAddress(ipAddress: "111.111.111.111", port: 1111)
private static let fakeRemoteAddress = try! SocketAddress(ipAddress: "222.222.222.222", port: 2222)
private var localAddress: SocketAddress?
private var remoteAddress: SocketAddress?
public static var pcapFileHeader: ByteBuffer {
var buffer = ByteBufferAllocator().buffer(capacity: 24)
buffer.writePCAPHeader()
return buffer
}
/// Initialize a `NIOWritePCAPHandler`.
///
/// - parameters:
/// - fakeLocalAddress: Allows you to optionally override the local address to be different from the real one.
/// - fakeRemoteAddress: Allows you to optionally override the remote address to be different from the real one.
/// - fileSink: The `fileSink` closure is called every time a new chunk of the `.pcap` file is ready to be
/// written to disk or elsewhere. See `NIOSynchronizedFileSink` for a convenient way to write to
/// disk.
public init(mode: Mode,
fakeLocalAddress: SocketAddress? = nil,
fakeRemoteAddress: SocketAddress? = nil,
fileSink: @escaping (ByteBuffer) -> Void) {
self.fileSink = fileSink
self.mode = mode
if let fakeLocalAddress = fakeLocalAddress {
self.localAddress = fakeLocalAddress
}
if let fakeRemoteAddress = fakeRemoteAddress {
self.remoteAddress = fakeRemoteAddress
}
}
private func writeBuffer(_ buffer: ByteBuffer) {
self.fileSink(buffer)
}
private func localAddress(context: ChannelHandlerContext) -> SocketAddress {
if let localAddress = self.localAddress {
return localAddress
} else {
let localAddress = context.channel.localAddress ?? NIOWritePCAPHandler.fakeLocalAddress
self.localAddress = localAddress
return localAddress
}
}
private func remoteAddress(context: ChannelHandlerContext) -> SocketAddress {
if let remoteAddress = self.remoteAddress {
return remoteAddress
} else {
let remoteAddress = context.channel.remoteAddress ?? NIOWritePCAPHandler.fakeRemoteAddress
self.remoteAddress = remoteAddress
return remoteAddress
}
}
private func clientAddress(context: ChannelHandlerContext) -> SocketAddress {
switch self.mode {
case .client:
return self.localAddress(context: context)
case .server:
return self.remoteAddress(context: context)
}
}
private func serverAddress(context: ChannelHandlerContext) -> SocketAddress {
switch self.mode {
case .client:
return self.remoteAddress(context: context)
case .server:
return self.localAddress(context: context)
}
}
private func takeSensiblySizedPayload(buffer: inout ByteBuffer) -> ByteBuffer? {
guard buffer.readableBytes > 0 else {
return nil
}
return buffer.readSlice(length: min(buffer.readableBytes, self.maxPayloadSize))
}
}
extension NIOWritePCAPHandler: ChannelDuplexHandler {
public typealias InboundIn = ByteBuffer
public typealias InboundOut = ByteBuffer
public typealias OutboundIn = IOData
public typealias OutboundOut = IOData
public func handlerAdded(context: ChannelHandlerContext) {
self.buffer = context.channel.allocator.buffer(capacity: 256)
}
public func channelActive(context: ChannelHandlerContext) {
self.buffer.clear()
self.readInboundBytes = 1
self.writtenOutboundBytes = 1
do {
let clientAddress = self.clientAddress(context: context)
let serverAddress = self.serverAddress(context: context)
try self.buffer.writePCAPRecord(.init(payloadLength: 0,
src: clientAddress,
dst: serverAddress,
tcp: TCPHeader(flags: [.syn],
ackNumber: nil,
sequenceNumber: 0,
srcPort: .init(clientAddress.port!),
dstPort: .init(serverAddress.port!))))
try self.buffer.writePCAPRecord(.init(payloadLength: 0,
src: serverAddress,
dst: clientAddress,
tcp: TCPHeader(flags: [.syn, .ack],
ackNumber: 1,
sequenceNumber: 0,
srcPort: .init(serverAddress.port!),
dstPort: .init(clientAddress.port!))))
try self.buffer.writePCAPRecord(.init(payloadLength: 0,
src: clientAddress,
dst: serverAddress,
tcp: TCPHeader(flags: [.ack],
ackNumber: 1,
sequenceNumber: 1,
srcPort: .init(clientAddress.port!),
dstPort: .init(serverAddress.port!))))
self.writeBuffer(self.buffer)
} catch {
context.fireErrorCaught(error)
}
context.fireChannelActive()
}
public func channelInactive(context: ChannelHandlerContext) {
let didLocalInitiateTheClose: Bool
switch self.closeState {
case .closedInitiatorLocal:
didLocalInitiateTheClose = true
case .closedInitiatorRemote:
didLocalInitiateTheClose = false
case .notClosing:
self.closeState = .closedInitiatorRemote
didLocalInitiateTheClose = false
}
self.buffer.clear()
do {
let closeInitiatorAddress = didLocalInitiateTheClose ? self.localAddress(context: context) : self.remoteAddress(context: context)
let closeRecipientAddress = didLocalInitiateTheClose ? self.remoteAddress(context: context) : self.localAddress(context: context)
let initiatorSeq = didLocalInitiateTheClose ? self.writtenOutboundBytes : self.readInboundBytes
let recipientSeq = didLocalInitiateTheClose ? self.readInboundBytes : self.writtenOutboundBytes
// terminate the connection cleanly
try self.buffer.writePCAPRecord(.init(payloadLength: 0,
src: closeInitiatorAddress,
dst: closeRecipientAddress,
tcp: TCPHeader(flags: [.fin],
ackNumber: nil,
sequenceNumber: initiatorSeq,
srcPort: .init(closeInitiatorAddress.port!),
dstPort: .init(closeRecipientAddress.port!))))
try self.buffer.writePCAPRecord(.init(payloadLength: 0,
src: closeRecipientAddress,
dst: closeInitiatorAddress,
tcp: TCPHeader(flags: [.ack, .fin],
ackNumber: initiatorSeq + 1,
sequenceNumber: recipientSeq,
srcPort: .init(closeRecipientAddress.port!),
dstPort: .init(closeInitiatorAddress.port!))))
try self.buffer.writePCAPRecord(.init(payloadLength: 0,
src: closeInitiatorAddress,
dst: closeRecipientAddress,
tcp: TCPHeader(flags: [.ack],
ackNumber: recipientSeq + 1,
sequenceNumber: initiatorSeq + 1,
srcPort: .init(closeInitiatorAddress.port!),
dstPort: .init(closeRecipientAddress.port!))))
self.writeBuffer(self.buffer)
} catch {
context.fireErrorCaught(error)
}
context.fireChannelInactive()
}
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
defer {
context.fireChannelRead(data)
}
guard self.closeState == .notClosing else {
return
}
let data = self.unwrapInboundIn(data)
guard data.readableBytes > 0 else {
return
}
self.buffer.clear()
do {
var data = data
while var payloadToSend = self.takeSensiblySizedPayload(buffer: &data) {
try self.buffer.writePCAPRecord(.init(payloadLength: payloadToSend.readableBytes,
src: self.remoteAddress(context: context),
dst: self.localAddress(context: context),
tcp: TCPHeader(flags: [],
ackNumber: nil,
sequenceNumber: self.readInboundBytes,
srcPort: .init(self.remoteAddress(context: context).port!),
dstPort: .init(self.localAddress(context: context).port!))))
self.readInboundBytes += payloadToSend.readableBytes
self.buffer.writeBuffer(&payloadToSend)
}
assert(data.readableBytes == 0)
self.writeBuffer(self.buffer)
} catch {
context.fireErrorCaught(error)
}
}
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
defer {
context.write(data, promise: promise)
}
guard self.closeState == .notClosing else {
return
}
let data = self.unwrapInboundIn(data)
self.buffer.clear()
do {
var data = data
while var payloadToSend = self.takeSensiblySizedPayload(buffer: &data) {
try self.buffer.writePCAPRecord(.init(payloadLength: payloadToSend.readableBytes,
src: self.localAddress(context: context),
dst: self.remoteAddress(context: context),
tcp: TCPHeader(flags: [],
ackNumber: nil,
sequenceNumber: self.writtenOutboundBytes,
srcPort: .init(self.localAddress(context: context).port!),
dstPort: .init(self.remoteAddress(context: context).port!))))
self.writtenOutboundBytes += payloadToSend.readableBytes
self.buffer.writeBuffer(&payloadToSend)
}
self.writeBuffer(self.buffer)
} catch {
context.fireErrorCaught(error)
}
}
public func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
if let event = event as? ChannelEvent {
if event == .inputClosed {
switch self.closeState {
case .closedInitiatorLocal:
() // fair enough, we already closed locally
case .closedInitiatorRemote:
() // that's odd but okay
case .notClosing:
self.closeState = .closedInitiatorRemote
}
}
}
context.fireUserInboundEventTriggered(event)
}
public func close(context: ChannelHandlerContext, mode: CloseMode, promise: EventLoopPromise<Void>?) {
switch self.closeState {
case .closedInitiatorLocal:
() // weird, this looks like a double-close
case .closedInitiatorRemote:
() // fair enough, already closed I guess
case .notClosing:
self.closeState = .closedInitiatorLocal
}
context.close(mode: mode, promise: promise)
}
}
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(.init(record.tcp.sequenceNumber), as: UInt32.self) // seq no
self.writeInteger(.init(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.
///
/// A `SynchronizedFileSink` is thread-safe so can be used from any thread/`EventLoop`. After use, you
/// _must_ call `syncClose` on the `SynchronizedFileSink` to shut it and all the associated resources down. Failing
/// to do so triggers undefined behaviour.
public class SynchronizedFileSink {
private let fileHandle: NIOFileHandle
private let workQueue: DispatchQueue
private let writesGroup = DispatchGroup()
private let errorHandler: (Swift.Error) -> Void
private var state: State = .running /* protected by `workQueue` */
public enum FileWritingMode {
case appendToExistingPCAPFile
case createNewPCAPFile
}
public struct Error: Swift.Error {
public var errorCode: Int
internal enum ErrorCode: Int {
case cannotOpenFileError = 1
case cannotWriteToFileError
}
}
private enum State {
case running
case error(Swift.Error)
}
/// Creates a `SynchronizedFileSink` for writing to a `.pcap` file at `path`.
///
/// Typically, after you created a `SynchronizedFileSink`, you will hand `myFileSink.write` to
/// `NIOWritePCAPHandler`'s constructor so `NIOPCAPHandler` can write `.pcap` files. Example:
///
/// ```swift
/// let fileSink = try NIOWritePCAPHandler.SynchronizedFileSink.fileSinkWritingToFile(path: "test.pcap",
/// errorHandler: { error in
/// print("ERROR: \(error)")
/// })
/// defer {
/// try fileSink.syncClose()
/// }
/// // [...]
/// channel.pipeline.addHandler(NIOWritePCAPHandler(mode: .server, fileSink: fileSink.write))
/// ```
///
/// - parameters:
/// - path: The path of the `.pcap` file to write.
/// - fileWritingMode: Whether to append to an existing `.pcap` file or to create a new `.pcap` file. If you
/// choose to append to an existing `.pcap` file, the file header does not get written.
/// - errorHandler: Invoked when an unrecoverable error has occured. In this event you may log the error and
/// you must then `syncClose` the `SynchronizedFileSink`. When `errorHandler` has been
/// called, no further writes will be attempted and `errorHandler` will also not be called
/// again.
public static func fileSinkWritingToFile(path: String,
fileWritingMode: FileWritingMode = .createNewPCAPFile,
errorHandler: @escaping (Swift.Error) -> Void) throws -> SynchronizedFileSink {
let oflag: CInt = fileWritingMode == FileWritingMode.createNewPCAPFile ? (O_TRUNC | O_CREAT) : O_APPEND
let fd = try path.withCString { pathPtr -> CInt in
let fd = open(pathPtr, O_WRONLY | oflag, 0o600)
guard fd >= 0 else {
throw SynchronizedFileSink.Error(errorCode: Error.ErrorCode.cannotOpenFileError.rawValue)
}
return fd
}
if fileWritingMode == .createNewPCAPFile {
let writeOk = NIOWritePCAPHandler.pcapFileHeader.withUnsafeReadableBytes { ptr in
return sysWrite(fd, ptr.baseAddress, ptr.count) == ptr.count
}
guard writeOk else {
throw SynchronizedFileSink.Error(errorCode: Error.ErrorCode.cannotWriteToFileError.rawValue)
}
}
return SynchronizedFileSink(fileHandle: NIOFileHandle(descriptor: fd),
errorHandler: errorHandler)
}
private init(fileHandle: NIOFileHandle,
errorHandler: @escaping (Swift.Error) -> Void) {
self.fileHandle = fileHandle
self.workQueue = DispatchQueue(label: "io.swiftnio.extras.WritePCAPHandler.SynchronizedFileSink.workQueue")
self.errorHandler = errorHandler
}
/// Synchronously close this `SynchronizedFileSink` and any associated resources.
///
/// After use, it is mandatory to close a `SynchronizedFileSink` exactly once. `syncClose` may be called from
/// any thread but not from an `EventLoop` as it will block.
public func syncClose() throws {
self.writesGroup.wait()
try self.workQueue.sync {
try self.fileHandle.close()
}
}
public func write(buffer: ByteBuffer) {
self.workQueue.async(group: self.writesGroup) {
guard case .running = self.state else {
return
}
do {
try self.fileHandle.withUnsafeFileDescriptor { fd in
var buffer = buffer
while buffer.readableBytes > 0 {
try buffer.readWithUnsafeReadableBytes { dataPtr in
let r = sysWrite(fd, dataPtr.baseAddress, dataPtr.count)
assert(r != 0, "write returned 0 but we tried to write \(dataPtr.count) bytes")
guard r > 0 else {
throw Error.init(errorCode: Error.ErrorCode.cannotWriteToFileError.rawValue)
}
return r
}
}
}
} catch {
self.state = .error(error)
self.errorHandler(error)
}
}
}
}
}

View File

@ -0,0 +1,84 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIO
import NIOExtras
import NIOHTTP1
class SendSimpleRequestHandler: ChannelInboundHandler {
typealias InboundIn = HTTPClientResponsePart
typealias OutboundOut = HTTPClientRequestPart
private let allDonePromise: EventLoopPromise<ByteBuffer>
init(allDonePromise: EventLoopPromise<ByteBuffer>) {
self.allDonePromise = allDonePromise
}
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
if case .body(let body) = self.unwrapInboundIn(data) {
self.allDonePromise.succeed(body)
}
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
self.allDonePromise.fail(error)
context.close(promise: nil)
}
func channelActive(context: ChannelHandlerContext) {
let headers = HTTPHeaders([("host", "httpbin.org"),
("accept", "application/json")])
context.write(self.wrapOutboundOut(.head(.init(version: .init(major: 1, minor: 1),
method: .GET,
uri: "/delay/0.2",
headers: headers))), promise: nil)
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
}
}
guard let outputFile = CommandLine.arguments.dropFirst().first else {
print("Usage: \(CommandLine.arguments[0]) OUTPUT.pcap")
exit(0)
}
let fileSink = try NIOWritePCAPHandler.SynchronizedFileSink.fileSinkWritingToFile(path: outputFile) { error in
print("ERROR: \(error)")
exit(1)
}
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
defer {
try! group.syncShutdownGracefully()
}
let allDonePromise = group.next().makePromise(of: ByteBuffer.self)
let connection = try ClientBootstrap(group: group.next())
.channelInitializer { channel in
return channel.pipeline.addHandler(NIOWritePCAPHandler(mode: .client, fileSink: fileSink.write)).flatMap {
channel.pipeline.addHTTPClientHandlers()
}.flatMap {
channel.pipeline.addHandler(SendSimpleRequestHandler(allDonePromise: allDonePromise))
}
}
.connect(host: "httpbin.org", port: 80)
.wait()
let bytesReceived = try allDonePromise.futureResult.wait()
print("# Success!", String(decoding: bytesReceived.readableBytesView, as: Unicode.UTF8.self), separator: "\n")
try connection.close().wait()
try fileSink.syncClose()
print("# Your pcap file should have been written to '\(outputFile)'")
print("#")
print("# You can view \(outputFile) with")
print("# - Wireshark")
print("# - tcpdump -r '\(outputFile)'")

View File

@ -36,5 +36,6 @@ import XCTest
testCase(LineBasedFrameDecoderTest.allTests),
testCase(QuiescingHelperTest.allTests),
testCase(RequestResponseHandlerTest.allTests),
testCase(WritePCAPHandlerTest.allTests),
])
#endif

View File

@ -0,0 +1,43 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
//
// WritePCAPHandlerTest+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 WritePCAPHandlerTest {
static var allTests : [(String, (WritePCAPHandlerTest) -> () throws -> Void)] {
return [
("testConnectIssuesThreePacketsForIPv4", testConnectIssuesThreePacketsForIPv4),
("testConnectIssuesThreePacketsForIPv6", testConnectIssuesThreePacketsForIPv6),
("testAcceptConnectionFromRemote", testAcceptConnectionFromRemote),
("testCloseOriginatingFromLocal", testCloseOriginatingFromLocal),
("testCloseOriginatingFromRemote", testCloseOriginatingFromRemote),
("testInboundData", testInboundData),
("testOutboundData", testOutboundData),
("testOversizedInboundDataComesAsTwoPacketsIPv4", testOversizedInboundDataComesAsTwoPacketsIPv4),
("testOversizedInboundDataComesAsTwoPacketsIPv6", testOversizedInboundDataComesAsTwoPacketsIPv6),
("testOversizedOutboundDataComesAsTwoPacketsIPv4", testOversizedOutboundDataComesAsTwoPacketsIPv4),
("testOversizedOutboundDataComesAsTwoPacketsIPv6", testOversizedOutboundDataComesAsTwoPacketsIPv6),
]
}
}

View File

@ -0,0 +1,680 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2019 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 Foundation
import XCTest
import NIO
@testable import NIOExtras
class WritePCAPHandlerTest: XCTestCase {
private var accumulatedPackets: [ByteBuffer]!
private var channel: EmbeddedChannel!
private var scratchBuffer: ByteBuffer!
private var testAddressA: SocketAddress.IPv6Address!
private var _mode: NIOWritePCAPHandler.Mode = .client
var mode: NIOWritePCAPHandler.Mode {
get {
return self._mode
}
set {
self.channel = EmbeddedChannel(handler: NIOWritePCAPHandler(mode: newValue,
fakeLocalAddress: nil,
fakeRemoteAddress: nil,
fileSink: {
self.accumulatedPackets.append($0)
}))
self._mode = newValue
}
}
override func setUp() {
self.accumulatedPackets = []
self.channel = EmbeddedChannel(handler: NIOWritePCAPHandler(mode: .client,
fakeLocalAddress: nil,
fakeRemoteAddress: nil,
fileSink: {
self.accumulatedPackets.append($0)
}))
self.scratchBuffer = self.channel.allocator.buffer(capacity: 128)
}
override func tearDown() {
self.accumulatedPackets = nil
self.channel = nil
self.scratchBuffer = nil
}
func assertEqual(expectedAddress: SocketAddress?,
actualIPv4Address: in_addr,
actualPort: UInt16,
file: StaticString = #file,
line: UInt = #line) {
guard let port = expectedAddress?.port else {
XCTFail("expected address nil or has no port", file: file, line: line)
return
}
switch expectedAddress {
case .some(.v4(let expectedAddress)):
XCTAssertEqual(expectedAddress.address.sin_addr.s_addr,
actualIPv4Address.s_addr,
"IP addresses don't match",
file: file, line: line)
XCTAssertEqual(port, Int(actualPort), "ports don't match", file: file, line: line)
default:
XCTFail("expected address not an IPv4 address", file: file, line: line)
}
}
func assertEqual(expectedAddress: SocketAddress?,
actualIPv6Address: in6_addr,
actualPort: UInt16,
file: StaticString = #file,
line: UInt = #line) {
guard let port = expectedAddress?.port else {
XCTFail("expected address nil or has no port", file: file, line: line)
return
}
switch expectedAddress {
case .some(.v6(let expectedAddress)):
var actualIPv6Address = actualIPv6Address
var expectedAddress = expectedAddress.address
withUnsafeBytes(of: &actualIPv6Address) { actualAddressBytes in
withUnsafeBytes(of: &expectedAddress.sin6_addr) { expectedAddressBytes in
XCTAssertEqual(actualAddressBytes.count, expectedAddressBytes.count)
}
}
XCTAssertEqual(port, Int(actualPort), "ports don't match", file: file, line: line)
default:
XCTFail("expected address not an IPv4 address", file: file, line: line)
}
}
func testConnectIssuesThreePacketsForIPv4() {
XCTAssertEqual([], self.accumulatedPackets)
self.channel.localAddress = try! SocketAddress(ipAddress: "255.255.255.254", port: Int(UInt16.max) - 1)
XCTAssertNoThrow(try self.channel.connect(to: .init(ipAddress: "1.2.3.4", port: 5678)).wait())
XCTAssertNoThrow(try self.channel.throwIfErrorCaught())
XCTAssertEqual(1, self.accumulatedPackets.count) // the WritePCAPHandler will batch all into one write
var buffer = self.accumulatedPackets.first
let records = [buffer?.readPCAPRecord() /* SYN */,
buffer?.readPCAPRecord() /* SYN+ACK */,
buffer?.readPCAPRecord() /* ACK */]
var ipPackets: [TCPIPv4Packet] = []
for var record in records {
XCTAssertNotNil(record) // we must have been able to parse a record
XCTAssertGreaterThan(record?.payload.readableBytes ?? -1, 0) // there must be some TCP/IP packet in there
XCTAssertEqual(2, record?.pcapProtocolID) // 2 is IPv4
if let ipPacket = record?.payload.readTCPIPv4() {
ipPackets.append(ipPacket)
XCTAssertEqual(0, ipPacket.tcpPayload.readableBytes)
XCTAssertEqual(40, ipPacket.wholeIPPacketLength) // in IPv4 it's payload + IP + TCP header
}
}
XCTAssertEqual(3, ipPackets.count)
// SYN, local should be source, remote is destination
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv4Address: ipPackets[0].src,
actualPort: ipPackets[0].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv4Address: ipPackets[0].dst,
actualPort: ipPackets[0].tcpHeader.dstPort)
XCTAssertEqual([.syn], ipPackets[0].tcpHeader.flags)
// SYN+ACK, local should be destination, remote should be source
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv4Address: ipPackets[1].src,
actualPort: ipPackets[1].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv4Address: ipPackets[1].dst,
actualPort: ipPackets[1].tcpHeader.dstPort)
XCTAssertEqual([.syn, .ack], ipPackets[1].tcpHeader.flags)
// ACK
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv4Address: ipPackets[0].src,
actualPort: ipPackets[0].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv4Address: ipPackets[0].dst,
actualPort: ipPackets[0].tcpHeader.dstPort)
XCTAssertEqual([.ack], ipPackets[2].tcpHeader.flags)
XCTAssertEqual(0, buffer?.readableBytes) // there shouldn't be anything else left
}
func testConnectIssuesThreePacketsForIPv6() {
XCTAssertEqual([], self.accumulatedPackets)
self.channel.localAddress = try! SocketAddress(ipAddress: "1:2:3:4:5:6:7:8", port: Int(UInt16.max) - 1)
XCTAssertNoThrow(try self.channel.connect(to: .init(ipAddress: "::1", port: 5678)).wait())
XCTAssertNoThrow(try self.channel.throwIfErrorCaught())
XCTAssertEqual(1, self.accumulatedPackets.count) // the WritePCAPHandler will batch all into one write
var buffer = self.accumulatedPackets.first
let records = [buffer?.readPCAPRecord() /* SYN */,
buffer?.readPCAPRecord() /* SYN+ACK */,
buffer?.readPCAPRecord() /* ACK */]
var ipPackets: [TCPIPv6Packet] = []
for var record in records {
XCTAssertNotNil(record) // we must have been able to parse a record
XCTAssertGreaterThan(record?.payload.readableBytes ?? -1, 0) // there must be some TCP/IP packet in there
XCTAssertEqual(24, record?.pcapProtocolID) // 24 is IPv6
if let ipPacket = record?.payload.readTCPIPv6() {
ipPackets.append(ipPacket)
XCTAssertEqual(0, ipPacket.tcpPayload.readableBytes)
XCTAssertEqual(20, ipPacket.payloadLength) // in IPv6 it's just the payload, ie. payload + TCP header
}
}
XCTAssertEqual(3, ipPackets.count)
// SYN, local should be source, remote is destination
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv6Address: ipPackets[0].src,
actualPort: ipPackets[0].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv6Address: ipPackets[0].dst,
actualPort: ipPackets[0].tcpHeader.dstPort)
XCTAssertEqual([.syn], ipPackets[0].tcpHeader.flags)
// SYN+ACK, local should be destination, remote should be source
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv6Address: ipPackets[1].src,
actualPort: ipPackets[1].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv6Address: ipPackets[1].dst,
actualPort: ipPackets[1].tcpHeader.dstPort)
XCTAssertEqual([.syn, .ack], ipPackets[1].tcpHeader.flags)
// ACK
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv6Address: ipPackets[0].src,
actualPort: ipPackets[0].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv6Address: ipPackets[0].dst,
actualPort: ipPackets[0].tcpHeader.dstPort)
XCTAssertEqual([.ack], ipPackets[2].tcpHeader.flags)
XCTAssertEqual(0, buffer?.readableBytes) // there shouldn't be anything else left
}
func testAcceptConnectionFromRemote() {
self.mode = .server
XCTAssertEqual([], self.accumulatedPackets)
self.channel.remoteAddress = try! SocketAddress(ipAddress: "1.2.3.4", port: 5678)
self.channel.localAddress = try! SocketAddress(ipAddress: "255.255.255.254", port: Int(UInt16.max) - 1)
channel.pipeline.fireChannelActive()
XCTAssertNoThrow(try self.channel.throwIfErrorCaught())
XCTAssertEqual(1, self.accumulatedPackets.count) // the WritePCAPHandler will batch all into one write
var buffer = self.accumulatedPackets.first
let records = [buffer?.readPCAPRecord() /* SYN */,
buffer?.readPCAPRecord() /* SYN+ACK */,
buffer?.readPCAPRecord() /* ACK */]
var ipPackets: [TCPIPv4Packet] = []
for var record in records {
XCTAssertNotNil(record) // we must have been able to parse a record
XCTAssertGreaterThan(record?.payload.readableBytes ?? -1, 0) // there must be some TCP/IP packet in there
XCTAssertEqual(2, record?.pcapProtocolID) // 2 is IPv4
if let ipPacket = record?.payload.readTCPIPv4() {
ipPackets.append(ipPacket)
XCTAssertEqual(0, ipPacket.tcpPayload.readableBytes)
}
}
XCTAssertEqual(3, ipPackets.count)
// SYN, local should be dst, remote is src
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv4Address: ipPackets[0].src,
actualPort: ipPackets[0].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv4Address: ipPackets[0].dst,
actualPort: ipPackets[0].tcpHeader.dstPort)
XCTAssertEqual([.syn], ipPackets[0].tcpHeader.flags)
// SYN+ACK, local should be src, remote should be dst
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv4Address: ipPackets[1].src,
actualPort: ipPackets[1].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv4Address: ipPackets[1].dst,
actualPort: ipPackets[1].tcpHeader.dstPort)
XCTAssertEqual([.syn, .ack], ipPackets[1].tcpHeader.flags)
// ACK
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv4Address: ipPackets[0].src,
actualPort: ipPackets[0].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv4Address: ipPackets[0].dst,
actualPort: ipPackets[0].tcpHeader.dstPort)
XCTAssertEqual([.ack], ipPackets[2].tcpHeader.flags)
XCTAssertEqual(0, buffer?.readableBytes) // there shouldn't be anything else left
}
func testCloseOriginatingFromLocal() {
self.channel.localAddress = try! SocketAddress(ipAddress: "1.1.1.1", port: 1)
self.channel.remoteAddress = try! SocketAddress(ipAddress: "2.2.2.2", port: 2)
XCTAssertNoThrow(try self.channel.close().wait())
XCTAssertEqual(1, self.accumulatedPackets.count) // we're batching again.
var buffer = self.accumulatedPackets.first
let records = [buffer?.readPCAPRecord() /* FIN */,
buffer?.readPCAPRecord() /* FIN+ACK */,
buffer?.readPCAPRecord() /* ACK */]
XCTAssertEqual(0, buffer?.readableBytes) // nothing left
var ipPackets: [TCPIPv4Packet] = []
for var record in records {
XCTAssertNotNil(record) // we must have been able to parse a record
XCTAssertGreaterThan(record?.payload.readableBytes ?? -1, 0) // there must be some TCP/IP packet in there
if let ipPacket = record?.payload.readTCPIPv4() {
ipPackets.append(ipPacket)
XCTAssertEqual(0, ipPacket.tcpPayload.readableBytes)
}
}
// FIN, local should be source, remote is destination
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv4Address: ipPackets[0].src,
actualPort: ipPackets[0].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv4Address: ipPackets[0].dst,
actualPort: ipPackets[0].tcpHeader.dstPort)
XCTAssertEqual([.fin], ipPackets[0].tcpHeader.flags)
// FIN+ACK, local should be destination, remote should be source
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv4Address: ipPackets[1].src,
actualPort: ipPackets[1].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv4Address: ipPackets[1].dst,
actualPort: ipPackets[1].tcpHeader.dstPort)
XCTAssertEqual([.fin, .ack], ipPackets[1].tcpHeader.flags)
// ACK
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv4Address: ipPackets[0].src,
actualPort: ipPackets[0].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv4Address: ipPackets[0].dst,
actualPort: ipPackets[0].tcpHeader.dstPort)
XCTAssertEqual([.ack], ipPackets[2].tcpHeader.flags)
}
func testCloseOriginatingFromRemote() {
self.channel.localAddress = try! SocketAddress(ipAddress: "1.1.1.1", port: 1)
self.channel.remoteAddress = try! SocketAddress(ipAddress: "2.2.2.2", port: 2)
self.channel.pipeline.fireChannelInactive()
XCTAssertEqual(1, self.accumulatedPackets.count) // we're batching again.
var buffer = self.accumulatedPackets.first
let records = [buffer?.readPCAPRecord() /* FIN */,
buffer?.readPCAPRecord() /* FIN+ACK */,
buffer?.readPCAPRecord() /* ACK */]
XCTAssertEqual(0, buffer?.readableBytes) // nothing left
var ipPackets: [TCPIPv4Packet] = []
for var record in records {
XCTAssertNotNil(record) // we must have been able to parse a record
XCTAssertGreaterThan(record?.payload.readableBytes ?? -1, 0) // there must be some TCP/IP packet in there
if let ipPacket = record?.payload.readTCPIPv4() {
ipPackets.append(ipPacket)
XCTAssertEqual(0, ipPacket.tcpPayload.readableBytes)
}
}
// FIN, local should be dst, remote is src
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv4Address: ipPackets[0].src,
actualPort: ipPackets[0].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv4Address: ipPackets[0].dst,
actualPort: ipPackets[0].tcpHeader.dstPort)
XCTAssertEqual([.fin], ipPackets[0].tcpHeader.flags)
// FIN+ACK, local should be src, remote should be dst
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv4Address: ipPackets[1].src,
actualPort: ipPackets[1].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv4Address: ipPackets[1].dst,
actualPort: ipPackets[1].tcpHeader.dstPort)
XCTAssertEqual([.fin, .ack], ipPackets[1].tcpHeader.flags)
// ACK
self.assertEqual(expectedAddress: self.channel?.remoteAddress,
actualIPv4Address: ipPackets[0].src,
actualPort: ipPackets[0].tcpHeader.srcPort)
self.assertEqual(expectedAddress: self.channel?.localAddress,
actualIPv4Address: ipPackets[0].dst,
actualPort: ipPackets[0].tcpHeader.dstPort)
XCTAssertEqual([.ack], ipPackets[2].tcpHeader.flags)
}
func testInboundData() {
self.channel.localAddress = try! SocketAddress(ipAddress: "1.2.3.4", port: 1111)
self.channel.remoteAddress = try! SocketAddress(ipAddress: "9.8.7.6", port: 2222)
self.scratchBuffer.writeStaticString("hello")
XCTAssertNoThrow(try self.channel.writeInbound(self.scratchBuffer))
XCTAssertEqual(1, self.accumulatedPackets.count)
guard var packetBytes = self.accumulatedPackets.first else {
XCTFail("couldn't read bytes of first packet")
return
}
guard var payload = packetBytes.readPCAPRecord() else {
XCTFail("couldn't read payload from PCAP record")
return
}
XCTAssertEqual(0, packetBytes.readableBytes) // check nothing is left over
guard let tcpIPPacket = payload.payload.readTCPIPv4() else {
XCTFail("couldn't read TCP/IPv4 packet")
return
}
XCTAssertEqual(1111, tcpIPPacket.tcpHeader.dstPort)
XCTAssertEqual(2222, tcpIPPacket.tcpHeader.srcPort)
XCTAssertEqual("hello", String(decoding: tcpIPPacket.tcpPayload.readableBytesView, as: Unicode.UTF8.self))
}
func testOutboundData() {
self.channel.localAddress = try! SocketAddress(ipAddress: "1.2.3.4", port: 1111)
self.channel.remoteAddress = try! SocketAddress(ipAddress: "9.8.7.6", port: 2222)
self.scratchBuffer.writeStaticString("hello")
XCTAssertNoThrow(try self.channel.writeOutbound(self.scratchBuffer))
XCTAssertEqual(1, self.accumulatedPackets.count)
guard var packetBytes = self.accumulatedPackets.first else {
XCTFail("couldn't read bytes of first packet")
return
}
guard var payload = packetBytes.readPCAPRecord() else {
XCTFail("couldn't read payload from PCAP record")
return
}
XCTAssertEqual(0, packetBytes.readableBytes) // check nothing is left over
guard let tcpIPPacket = payload.payload.readTCPIPv4() else {
XCTFail("couldn't read TCP/IPv4 packet")
return
}
XCTAssertEqual(2222, tcpIPPacket.tcpHeader.dstPort)
XCTAssertEqual(1111, tcpIPPacket.tcpHeader.srcPort)
XCTAssertEqual("hello", String(decoding: tcpIPPacket.tcpPayload.readableBytesView, as: Unicode.UTF8.self))
}
func testOversizedInboundDataComesAsTwoPacketsIPv4() {
self.channel.localAddress = try! SocketAddress(ipAddress: "1.2.3.4", port: 1111)
self.channel.remoteAddress = try! SocketAddress(ipAddress: "9.8.7.6", port: 2222)
let expectedData = String(repeating: "X", count: Int(UInt16.max) * 2 - 300)
self.scratchBuffer.writeString(expectedData)
XCTAssertNoThrow(try self.channel.writeInbound(self.scratchBuffer))
XCTAssertEqual(1, self.accumulatedPackets.count)
guard var packetBytes = self.accumulatedPackets.first else {
XCTFail("couldn't read bytes of first packet")
return
}
guard var payload1 = packetBytes.readPCAPRecord(), var payload2 = packetBytes.readPCAPRecord() else {
XCTFail("couldn't read payloads from PCAP record")
return
}
XCTAssertEqual(0, packetBytes.readableBytes) // check nothing is left over
guard let tcpIPPacket1 = payload1.payload.readTCPIPv4(), let tcpIPPacket2 = payload2.payload.readTCPIPv4() else {
XCTFail("couldn't read TCP/IPv4 packets")
return
}
XCTAssertEqual(1111, tcpIPPacket1.tcpHeader.dstPort)
XCTAssertEqual(2222, tcpIPPacket1.tcpHeader.srcPort)
XCTAssertEqual(1111, tcpIPPacket2.tcpHeader.dstPort)
XCTAssertEqual(2222, tcpIPPacket2.tcpHeader.srcPort)
let actualData = String(decoding: tcpIPPacket1.tcpPayload.readableBytesView, as: Unicode.UTF8.self) +
String(decoding: tcpIPPacket2.tcpPayload.readableBytesView, as: Unicode.UTF8.self)
XCTAssertEqual(expectedData, actualData)
}
func testOversizedInboundDataComesAsTwoPacketsIPv6() {
self.channel.localAddress = try! SocketAddress(ipAddress: "::1", port: 1111)
self.channel.remoteAddress = try! SocketAddress(ipAddress: "::2", port: 2222)
let expectedData = String(repeating: "X", count: Int(UInt16.max) * 2 - 300)
self.scratchBuffer.writeString(expectedData)
XCTAssertNoThrow(try self.channel.writeInbound(self.scratchBuffer))
XCTAssertEqual(1, self.accumulatedPackets.count)
guard var packetBytes = self.accumulatedPackets.first else {
XCTFail("couldn't read bytes of first packet")
return
}
guard var payload1 = packetBytes.readPCAPRecord(), var payload2 = packetBytes.readPCAPRecord() else {
XCTFail("couldn't read payloads from PCAP record")
return
}
XCTAssertEqual(0, packetBytes.readableBytes) // check nothing is left over
guard let tcpIPPacket1 = payload1.payload.readTCPIPv6(), let tcpIPPacket2 = payload2.payload.readTCPIPv6() else {
XCTFail("couldn't read TCP/IPv6 packets")
return
}
XCTAssertEqual(1111, tcpIPPacket1.tcpHeader.dstPort)
XCTAssertEqual(2222, tcpIPPacket1.tcpHeader.srcPort)
XCTAssertEqual(1111, tcpIPPacket2.tcpHeader.dstPort)
XCTAssertEqual(2222, tcpIPPacket2.tcpHeader.srcPort)
let actualData = String(decoding: tcpIPPacket1.tcpPayload.readableBytesView, as: Unicode.UTF8.self) +
String(decoding: tcpIPPacket2.tcpPayload.readableBytesView, as: Unicode.UTF8.self)
XCTAssertEqual(expectedData, actualData)
}
func testOversizedOutboundDataComesAsTwoPacketsIPv4() {
self.channel.localAddress = try! SocketAddress(ipAddress: "1.2.3.4", port: 1111)
self.channel.remoteAddress = try! SocketAddress(ipAddress: "9.8.7.6", port: 2222)
let expectedData = String(repeating: "X", count: Int(UInt16.max) * 2 - 300)
self.scratchBuffer.writeString(expectedData)
XCTAssertNoThrow(try self.channel.writeOutbound(self.scratchBuffer))
XCTAssertEqual(1, self.accumulatedPackets.count)
guard var packetBytes = self.accumulatedPackets.first else {
XCTFail("couldn't read bytes of first packet")
return
}
guard var payload1 = packetBytes.readPCAPRecord(), var payload2 = packetBytes.readPCAPRecord() else {
XCTFail("couldn't read payloads from PCAP record")
return
}
XCTAssertEqual(0, packetBytes.readableBytes) // check nothing is left over
guard let tcpIPPacket1 = payload1.payload.readTCPIPv4(), let tcpIPPacket2 = payload2.payload.readTCPIPv4() else {
XCTFail("couldn't read TCP/IPv4 packets")
return
}
XCTAssertEqual(2222, tcpIPPacket1.tcpHeader.dstPort)
XCTAssertEqual(1111, tcpIPPacket1.tcpHeader.srcPort)
XCTAssertEqual(2222, tcpIPPacket2.tcpHeader.dstPort)
XCTAssertEqual(1111, tcpIPPacket2.tcpHeader.srcPort)
let actualData = String(decoding: tcpIPPacket1.tcpPayload.readableBytesView, as: Unicode.UTF8.self) +
String(decoding: tcpIPPacket2.tcpPayload.readableBytesView, as: Unicode.UTF8.self)
XCTAssertEqual(expectedData, actualData)
}
func testOversizedOutboundDataComesAsTwoPacketsIPv6() {
self.channel.localAddress = try! SocketAddress(ipAddress: "::1", port: 1111)
self.channel.remoteAddress = try! SocketAddress(ipAddress: "::2", port: 2222)
let expectedData = String(repeating: "X", count: Int(UInt16.max) * 2 - 300)
self.scratchBuffer.writeString(expectedData)
XCTAssertNoThrow(try self.channel.writeOutbound(self.scratchBuffer))
XCTAssertEqual(1, self.accumulatedPackets.count)
guard var packetBytes = self.accumulatedPackets.first else {
XCTFail("couldn't read bytes of first packet")
return
}
guard var payload1 = packetBytes.readPCAPRecord(), var payload2 = packetBytes.readPCAPRecord() else {
XCTFail("couldn't read payloads from PCAP record")
return
}
XCTAssertEqual(0, packetBytes.readableBytes) // check nothing is left over
guard let tcpIPPacket1 = payload1.payload.readTCPIPv6(), let tcpIPPacket2 = payload2.payload.readTCPIPv6() else {
XCTFail("couldn't read TCP/IPv6 packets")
return
}
XCTAssertEqual(2222, tcpIPPacket1.tcpHeader.dstPort)
XCTAssertEqual(1111, tcpIPPacket1.tcpHeader.srcPort)
XCTAssertEqual(2222, tcpIPPacket2.tcpHeader.dstPort)
XCTAssertEqual(1111, tcpIPPacket2.tcpHeader.srcPort)
let actualData = String(decoding: tcpIPPacket1.tcpPayload.readableBytesView, as: Unicode.UTF8.self) +
String(decoding: tcpIPPacket2.tcpPayload.readableBytesView, as: Unicode.UTF8.self)
XCTAssertEqual(expectedData, actualData)
}
}
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() -> 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
(flagsAndFriends & (0xf << 12)) == (0x5 << 12) /* check that the data offset is right */ else {
self = saveSelf
return nil
}
return TCPHeader(flags: .init(rawValue: UInt8(flagsAndFriends & 0xfff)),
ackNumber: ackNo == 0 ? nil : Int(ackNo),
sequenceNumber: Int(seqNo),
srcPort: srcPort,
dstPort: dstPort)
}
// read & parse a TCP/IPv4 packet, containing everything belonging to it (including payload)
mutating func readTCPIPv4() -> TCPIPv4Packet? {
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),
version == 0x45,
innerProtocolID == 6, // TCP is 6
var payload = self.readSlice(length: Int(ipv4WholeLength - 20)),
let tcp = payload.readTCPHeader() else {
self = saveSelf
return nil
}
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() -> 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)
versionAndFancyStuff >> 28 == 6, // IPv_6_
innerProtocolID == 6, /* TCP is 6 */
var srcAddrBuffer = self.readSlice(length: MemoryLayout<in6_addr>.size),
var dstAddrBuffer = self.readSlice(length: MemoryLayout<in6_addr>.size),
var payload = self.readSlice(length: Int(payloadLength)),
let tcp = payload.readTCPHeader() else {
self = saveSelf
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: -1, 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)
}
}

View File

@ -18,7 +18,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
function replace_acceptable_years() {
# this needs to replace all acceptable forms with 'YEARS'
sed 's/2017-201[89]/YEARS/g'
sed -e 's/2017-201[89]/YEARS/' -e 's/2019/YEARS/'
}
printf "=> Checking linux tests... "