mirror of
https://github.com/apple/swift-nio-extras.git
synced 2025-05-14 08:52:42 +08:00
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:
parent
aad5c1ca6a
commit
96e8335180
@ -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"]),
|
||||
],
|
||||
|
@ -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)
|
||||
|
645
Sources/NIOExtras/WritePCAPHandler.swift
Normal file
645
Sources/NIOExtras/WritePCAPHandler.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
84
Sources/NIOWritePCAPDemo/main.swift
Normal file
84
Sources/NIOWritePCAPDemo/main.swift
Normal 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)'")
|
@ -36,5 +36,6 @@ import XCTest
|
||||
testCase(LineBasedFrameDecoderTest.allTests),
|
||||
testCase(QuiescingHelperTest.allTests),
|
||||
testCase(RequestResponseHandlerTest.allTests),
|
||||
testCase(WritePCAPHandlerTest.allTests),
|
||||
])
|
||||
#endif
|
||||
|
43
Tests/NIOExtrasTests/WritePCAPHandlerTest+XCTest.swift
Normal file
43
Tests/NIOExtrasTests/WritePCAPHandlerTest+XCTest.swift
Normal 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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
680
Tests/NIOExtrasTests/WritePCAPHandlerTest.swift
Normal file
680
Tests/NIOExtrasTests/WritePCAPHandlerTest.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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... "
|
||||
|
Loading…
x
Reference in New Issue
Block a user