From d889397c1ee82d9194550aedde433dd4d801e48b Mon Sep 17 00:00:00 2001
From: Aayush Atharva
Date: Fri, 11 Sep 2020 19:36:16 +0530
Subject: [PATCH] Add PcapWriteHandler Support (#10498)
Motivation:
Write TCP and UDP packets into Pcap `OutputStream` which helps a lot in debugging.
Modification:
Added TCP and UDP Pcap writer.
Result:
New handler can write packets into an `OutputStream`, e.g. a file that can be opened with Wireshark.
Fixes #10385.
---
.../io/netty/handler/pcap/EthernetPacket.java | 81 +++
.../java/io/netty/handler/pcap/IPPacket.java | 111 ++++
.../io/netty/handler/pcap/PcapHeaders.java | 67 +++
.../netty/handler/pcap/PcapWriteHandler.java | 523 ++++++++++++++++++
.../io/netty/handler/pcap/PcapWriter.java | 80 +++
.../java/io/netty/handler/pcap/TCPPacket.java | 82 +++
.../java/io/netty/handler/pcap/UDPPacket.java | 43 ++
.../io/netty/handler/pcap/package-info.java | 20 +
.../handler/pcap/PcapWriteHandlerTest.java | 137 +++++
9 files changed, 1144 insertions(+)
create mode 100644 handler/src/main/java/io/netty/handler/pcap/EthernetPacket.java
create mode 100644 handler/src/main/java/io/netty/handler/pcap/IPPacket.java
create mode 100644 handler/src/main/java/io/netty/handler/pcap/PcapHeaders.java
create mode 100644 handler/src/main/java/io/netty/handler/pcap/PcapWriteHandler.java
create mode 100644 handler/src/main/java/io/netty/handler/pcap/PcapWriter.java
create mode 100644 handler/src/main/java/io/netty/handler/pcap/TCPPacket.java
create mode 100644 handler/src/main/java/io/netty/handler/pcap/UDPPacket.java
create mode 100644 handler/src/main/java/io/netty/handler/pcap/package-info.java
create mode 100644 handler/src/test/java/io/netty/handler/pcap/PcapWriteHandlerTest.java
diff --git a/handler/src/main/java/io/netty/handler/pcap/EthernetPacket.java b/handler/src/main/java/io/netty/handler/pcap/EthernetPacket.java
new file mode 100644
index 0000000000..eaa7cd5830
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/pcap/EthernetPacket.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.handler.pcap;
+
+import io.netty.buffer.ByteBuf;
+
+final class EthernetPacket {
+
+ /**
+ * MAC Address: 00:00:5E:00:53:00
+ */
+ private static final byte[] DUMMY_SOURCE_MAC_ADDRESS = new byte[]{0, 0, 94, 0, 83, 0};
+
+ /**
+ * MAC Address: 00:00:5E:00:53:FF
+ */
+ private static final byte[] DUMMY_DESTINATION_MAC_ADDRESS = new byte[]{0, 0, 94, 0, 83, -1};
+
+ /**
+ * IPv4
+ */
+ private static final int V4 = 0x0800;
+
+ /**
+ * IPv6
+ */
+ private static final int V6 = 0x86dd;
+
+ private EthernetPacket() {
+ // Prevent outside initialization
+ }
+
+ /**
+ * Write IPv4 Ethernet Packet. It uses a dummy MAC address for both source and destination.
+ *
+ * @param byteBuf ByteBuf where Ethernet Packet data will be set
+ * @param payload Payload of IPv4
+ */
+ static void writeIPv4(ByteBuf byteBuf, ByteBuf payload) {
+ EthernetPacket.writePacket(byteBuf, payload, DUMMY_SOURCE_MAC_ADDRESS, DUMMY_DESTINATION_MAC_ADDRESS, V4);
+ }
+
+ /**
+ * Write IPv6 Ethernet Packet. It uses a dummy MAC address for both source and destination.
+ *
+ * @param byteBuf ByteBuf where Ethernet Packet data will be set
+ * @param payload Payload of IPv6
+ */
+ static void writeIPv6(ByteBuf byteBuf, ByteBuf payload) {
+ EthernetPacket.writePacket(byteBuf, payload, DUMMY_SOURCE_MAC_ADDRESS, DUMMY_DESTINATION_MAC_ADDRESS, V6);
+ }
+
+ /**
+ * Write IPv6 Ethernet Packet
+ *
+ * @param byteBuf ByteBuf where Ethernet Packet data will be set
+ * @param payload Payload of IPv6
+ * @param srcAddress Source MAC Address
+ * @param dstAddress Destination MAC Address
+ * @param type Type of Frame
+ */
+ private static void writePacket(ByteBuf byteBuf, ByteBuf payload, byte[] srcAddress, byte[] dstAddress, int type) {
+ byteBuf.writeBytes(dstAddress); // Destination MAC Address
+ byteBuf.writeBytes(srcAddress); // Source MAC Address
+ byteBuf.writeShort(type); // Frame Type (IPv4 or IPv6)
+ byteBuf.writeBytes(payload); // Payload of L3
+ }
+}
diff --git a/handler/src/main/java/io/netty/handler/pcap/IPPacket.java b/handler/src/main/java/io/netty/handler/pcap/IPPacket.java
new file mode 100644
index 0000000000..a6f6b3bd94
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/pcap/IPPacket.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.handler.pcap;
+
+import io.netty.buffer.ByteBuf;
+
+final class IPPacket {
+
+ private static final byte MAX_TTL = (byte) 255;
+ private static final short V4_HEADER_SIZE = 20;
+ private static final byte TCP = 6 & 0xff;
+ private static final byte UDP = 17 & 0xff;
+
+ /**
+ * Version + Traffic class + Flow label
+ */
+ private static final int IPV6_VERSION_TRAFFIC_FLOW = 60000000;
+
+ private IPPacket() {
+ // Prevent outside initialization
+ }
+
+ /**
+ * Write IPv4 Packet for UDP Packet
+ *
+ * @param byteBuf ByteBuf where IP Packet data will be set
+ * @param payload Payload of UDP
+ * @param srcAddress Source IPv4 Address
+ * @param dstAddress Destination IPv4 Address
+ */
+ static void writeUDPv4(ByteBuf byteBuf, ByteBuf payload, int srcAddress, int dstAddress) {
+ writePacketv4(byteBuf, payload, UDP, srcAddress, dstAddress);
+ }
+
+ /**
+ * Write IPv6 Packet for UDP Packet
+ *
+ * @param byteBuf ByteBuf where IP Packet data will be set
+ * @param payload Payload of UDP
+ * @param srcAddress Source IPv6 Address
+ * @param dstAddress Destination IPv6 Address
+ */
+ static void writeUDPv6(ByteBuf byteBuf, ByteBuf payload, byte[] srcAddress, byte[] dstAddress) {
+ writePacketv6(byteBuf, payload, UDP, srcAddress, dstAddress);
+ }
+
+ /**
+ * Write IPv4 Packet for TCP Packet
+ *
+ * @param byteBuf ByteBuf where IP Packet data will be set
+ * @param payload Payload of TCP
+ * @param srcAddress Source IPv4 Address
+ * @param dstAddress Destination IPv4 Address
+ */
+ static void writeTCPv4(ByteBuf byteBuf, ByteBuf payload, int srcAddress, int dstAddress) {
+ writePacketv4(byteBuf, payload, TCP, srcAddress, dstAddress);
+ }
+
+ /**
+ * Write IPv6 Packet for TCP Packet
+ *
+ * @param byteBuf ByteBuf where IP Packet data will be set
+ * @param payload Payload of TCP
+ * @param srcAddress Source IPv6 Address
+ * @param dstAddress Destination IPv6 Address
+ */
+ static void writeTCPv6(ByteBuf byteBuf, ByteBuf payload, byte[] srcAddress, byte[] dstAddress) {
+ writePacketv6(byteBuf, payload, TCP, srcAddress, dstAddress);
+ }
+
+ private static void writePacketv4(ByteBuf byteBuf, ByteBuf payload, int protocol, int srcAddress,
+ int dstAddress) {
+
+ byteBuf.writeByte(0x45); // Version + IHL
+ byteBuf.writeByte(0x00); // DSCP
+ byteBuf.writeShort(V4_HEADER_SIZE + payload.readableBytes()); // Length
+ byteBuf.writeShort(0x0000); // Identification
+ byteBuf.writeShort(0x0000); // Fragment
+ byteBuf.writeByte(MAX_TTL); // TTL
+ byteBuf.writeByte(protocol); // Protocol
+ byteBuf.writeShort(0); // Checksum
+ byteBuf.writeInt(srcAddress); // Source IPv4 Address
+ byteBuf.writeInt(dstAddress); // Destination IPv4 Address
+ byteBuf.writeBytes(payload); // Payload of L4
+ }
+
+ private static void writePacketv6(ByteBuf byteBuf, ByteBuf payload, int protocol, byte[] srcAddress,
+ byte[] dstAddress) {
+
+ byteBuf.writeInt(IPV6_VERSION_TRAFFIC_FLOW); // Version + Traffic class + Flow label
+ byteBuf.writeShort(payload.readableBytes()); // Payload length
+ byteBuf.writeByte(protocol & 0xff); // Next header
+ byteBuf.writeByte(MAX_TTL); // Hop limit
+ byteBuf.writeBytes(srcAddress); // Source IPv6 Address
+ byteBuf.writeBytes(dstAddress); // Destination IPv6 Address
+ byteBuf.writeBytes(payload); // Payload of L4
+ }
+}
diff --git a/handler/src/main/java/io/netty/handler/pcap/PcapHeaders.java b/handler/src/main/java/io/netty/handler/pcap/PcapHeaders.java
new file mode 100644
index 0000000000..96eacd27bf
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/pcap/PcapHeaders.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.handler.pcap;
+
+import io.netty.buffer.ByteBuf;
+
+import java.util.concurrent.TimeUnit;
+
+final class PcapHeaders {
+
+ /**
+ * Pcap Global Header built from:
+ *
+ * - magic_number
+ * - version_major
+ * - version_minor
+ * - thiszone
+ * - sigfigs
+ * - snaplen
+ * - network
+ *
+ */
+ private static final byte[] GLOBAL_HEADER = new byte[]{-95, -78, -61, -44, 0, 2, 0, 4, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 1};
+
+ private PcapHeaders() {
+ // Prevent outside initialization
+ }
+
+ /**
+ * Write Pcap Global Header
+ *
+ * @param byteBuf byteBuf ByteBuf where we'll write header data
+ */
+ public static void writeGlobalHeader(ByteBuf byteBuf) {
+ byteBuf.writeBytes(GLOBAL_HEADER);
+ }
+
+ /**
+ * Write Pcap Packet Header
+ *
+ * @param byteBuf ByteBuf where we'll write header data
+ * @param ts_sec timestamp seconds
+ * @param ts_usec timestamp microseconds
+ * @param incl_len number of octets of packet saved in file
+ * @param orig_len actual length of packet
+ */
+ static void writePacketHeader(ByteBuf byteBuf, int ts_sec, int ts_usec, int incl_len, int orig_len) {
+ byteBuf.writeInt(ts_sec);
+ byteBuf.writeInt(ts_usec);
+ byteBuf.writeInt(incl_len);
+ byteBuf.writeInt(orig_len);
+ }
+}
diff --git a/handler/src/main/java/io/netty/handler/pcap/PcapWriteHandler.java b/handler/src/main/java/io/netty/handler/pcap/PcapWriteHandler.java
new file mode 100644
index 0000000000..5c5e270651
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/pcap/PcapWriteHandler.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.handler.pcap;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.channel.ChannelDuplexHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.ServerChannel;
+import io.netty.channel.socket.DatagramChannel;
+import io.netty.channel.socket.DatagramPacket;
+import io.netty.channel.socket.ServerSocketChannel;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.util.NetUtil;
+import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+
+/**
+ * {@link PcapWriteHandler} captures {@link ByteBuf} from {@link SocketChannel} / {@link ServerChannel}
+ * or {@link DatagramPacket} and writes it into Pcap {@link OutputStream}.
+ *
+ *
+ * Things to keep in mind when using {@link PcapWriteHandler} with TCP:
+ *
+ *
+ * - Whenever {@link ChannelInboundHandlerAdapter#channelActive(ChannelHandlerContext)} is called,
+ * a fake TCP 3-way handshake (SYN, SYN+ACK, ACK) is simulated as new connection in Pcap.
+ *
+ * - Whenever {@link ChannelInboundHandlerAdapter#handlerRemoved(ChannelHandlerContext)} is called,
+ * a fake TCP 3-way handshake (FIN+ACK, FIN+ACK, ACK) is simulated as connection shutdown in Pcap.
+ *
+ * - Whenever {@link ChannelInboundHandlerAdapter#exceptionCaught(ChannelHandlerContext, Throwable)}
+ * is called, a fake TCP RST is sent to simulate connection Reset in Pcap.
+ *
+ * - ACK is sent each time data is send / received.
+ *
+ * - Zero Length Data Packets can cause TCP Double ACK error in Wireshark. To tackle this,
+ * set {@code captureZeroByte} to {@code false}.
+ *
+ *
+ */
+public final class PcapWriteHandler extends ChannelDuplexHandler {
+
+ private final InternalLogger logger = InternalLoggerFactory.getInstance(PcapWriteHandler.class);
+
+ /**
+ * {@link PcapWriter} Instance
+ */
+ private PcapWriter pCapWriter;
+
+ /**
+ * {@link OutputStream} where we'll write Pcap data.
+ */
+ private final OutputStream outputStream;
+
+ /**
+ * {@code true} if we want to capture packets with zero bytes else {@code false}.
+ */
+ private final boolean captureZeroByte;
+
+ /**
+ * {@code true} if we want to write Pcap Global Header on initialization of
+ * {@link PcapWriter} else {@code false}.
+ */
+ private final boolean writePcapGlobalHeader;
+
+ /**
+ * TCP Sender Segment Number.
+ * It'll start with 1 and keep incrementing with number of bytes read/sent.
+ */
+ private int sendSegmentNumber = 1;
+
+ /**
+ * TCP Receiver Segment Number.
+ * It'll start with 1 and keep incrementing with number of bytes read/sent.
+ */
+ private int receiveSegmentNumber = 1;
+
+ /**
+ * Source Address
+ */
+ private InetSocketAddress srcAddr;
+
+ /**
+ * Destination Address
+ */
+ private InetSocketAddress dstAddr;
+
+ /**
+ * Create new {@link PcapWriteHandler} Instance.
+ * {@code captureZeroByte} is set to {@code false} and
+ * {@code writePcapGlobalHeader} is set to {@code true}.
+ *
+ * @param outputStream OutputStream where Pcap data will be written
+ * @throws NullPointerException If {@link OutputStream} is {@code null} then we'll throw an
+ * {@link NullPointerException}
+ */
+ public PcapWriteHandler(OutputStream outputStream) {
+ this(outputStream, false, true);
+ }
+
+ /**
+ * Create new {@link PcapWriteHandler} Instance
+ *
+ * @param outputStream OutputStream where Pcap data will be written
+ * @param captureZeroByte Set to {@code true} to enable capturing packets with empty (0 bytes) payload.
+ * Otherwise, if set to {@code false}, empty packets will be filtered out.
+ * @param writePcapGlobalHeader Set to {@code true} to write Pcap Global Header on initialization.
+ * Otherwise, if set to {@code false}, Pcap Global Header will not be written
+ * on initialization. This could when writing Pcap data on a existing file where
+ * Pcap Global Header is already present.
+ * @throws NullPointerException If {@link OutputStream} is {@code null} then we'll throw an
+ * {@link NullPointerException}
+ */
+ public PcapWriteHandler(OutputStream outputStream, boolean captureZeroByte, boolean writePcapGlobalHeader) {
+ this.outputStream = ObjectUtil.checkNotNull(outputStream, "OutputStream");
+ this.captureZeroByte = captureZeroByte;
+ this.writePcapGlobalHeader = writePcapGlobalHeader;
+ }
+
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) throws Exception {
+
+ ByteBufAllocator byteBufAllocator = ctx.alloc();
+
+ /*
+ * If `writePcapGlobalHeader` is `true`, we'll write Pcap Global Header.
+ */
+ if (writePcapGlobalHeader) {
+
+ ByteBuf byteBuf = byteBufAllocator.buffer();
+ try {
+ this.pCapWriter = new PcapWriter(this.outputStream, byteBuf);
+ } catch (IOException ex) {
+ ctx.channel().close();
+ ctx.fireExceptionCaught(ex);
+ logger.error("Caught Exception While Initializing PcapWriter, Closing Channel.", ex);
+ } finally {
+ byteBuf.release();
+ }
+ } else {
+ this.pCapWriter = new PcapWriter(this.outputStream);
+ }
+
+ // If Channel belongs to `SocketChannel` then we're handling TCP.
+ if (ctx.channel() instanceof SocketChannel) {
+
+ // Capture correct `localAddress` and `remoteAddress`
+ if (ctx.channel().parent() instanceof ServerSocketChannel) {
+ srcAddr = (InetSocketAddress) ctx.channel().remoteAddress();
+ dstAddr = (InetSocketAddress) ctx.channel().localAddress();
+ } else {
+ srcAddr = (InetSocketAddress) ctx.channel().localAddress();
+ dstAddr = (InetSocketAddress) ctx.channel().remoteAddress();
+ }
+
+ logger.debug("Initiating Fake TCP 3-Way Handshake");
+
+ ByteBuf tcpBuf = byteBufAllocator.buffer();
+
+ try {
+ // Write SYN with Normal Source and Destination Address
+ TCPPacket.writePacket(tcpBuf, null, 0, 0, srcAddr.getPort(), dstAddr.getPort(), TCPPacket.TCPFlag.SYN);
+ completeTCPWrite(srcAddr, dstAddr, tcpBuf, byteBufAllocator, ctx);
+
+ // Write SYN+ACK with Reversed Source and Destination Address
+ TCPPacket.writePacket(tcpBuf, null, 0, 1, dstAddr.getPort(), srcAddr.getPort(), TCPPacket.TCPFlag.SYN,
+ TCPPacket.TCPFlag.ACK);
+ completeTCPWrite(dstAddr, srcAddr, tcpBuf, byteBufAllocator, ctx);
+
+ // Write ACK with Normal Source and Destination Address
+ TCPPacket.writePacket(tcpBuf, null, 1, 1, srcAddr.getPort(), dstAddr.getPort(), TCPPacket.TCPFlag.ACK);
+ completeTCPWrite(srcAddr, dstAddr, tcpBuf, byteBufAllocator, ctx);
+ } finally {
+ tcpBuf.release();
+ }
+
+ logger.debug("Finished Fake TCP 3-Way Handshake");
+ } else if (ctx.channel() instanceof DatagramChannel) {
+ DatagramChannel datagramChannel = (DatagramChannel) ctx.channel();
+
+ // If `DatagramChannel` is connected then we can get
+ // `localAddress` and `remoteAddress` from Channel.
+ if (datagramChannel.isConnected()) {
+ srcAddr = (InetSocketAddress) ctx.channel().localAddress();
+ dstAddr = (InetSocketAddress) ctx.channel().remoteAddress();
+ }
+ }
+
+ super.channelActive(ctx);
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ if (ctx.channel() instanceof SocketChannel) {
+ handleTCP(ctx, msg, false);
+ } else if (ctx.channel() instanceof DatagramChannel) {
+ handleUDP(ctx, msg);
+ } else {
+ logger.debug("Discarding Pcap Write for Unknown Channel Type: {}", ctx.channel());
+ }
+ super.channelRead(ctx, msg);
+ }
+
+ @Override
+ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+ if (ctx.channel() instanceof SocketChannel) {
+ handleTCP(ctx, msg, true);
+ } else if (ctx.channel() instanceof DatagramChannel) {
+ handleUDP(ctx, msg);
+ } else {
+ logger.debug("Discarding Pcap Write for Unknown Channel Type: {}", ctx.channel());
+ }
+ super.write(ctx, msg, promise);
+ }
+
+ /**
+ * Handle TCP L4
+ *
+ * @param ctx {@link ChannelHandlerContext} for {@link ByteBuf} allocation and
+ * {@code fireExceptionCaught}
+ * @param msg {@link Object} must be {@link ByteBuf} else it'll be discarded
+ * @param isWriteOperation Set {@code true} if we have to process packet when packets are being sent out
+ * else set {@code false}
+ */
+ private void handleTCP(ChannelHandlerContext ctx, Object msg, boolean isWriteOperation) {
+ if (msg instanceof ByteBuf) {
+
+ // If bytes are 0 and `captureZeroByte` is false, we won't capture this.
+ if (((ByteBuf) msg).readableBytes() == 0 && !captureZeroByte) {
+ logger.debug("Discarding Zero Byte TCP Packet. isWriteOperation {}", isWriteOperation);
+ return;
+ }
+
+ ByteBufAllocator byteBufAllocator = ctx.alloc();
+ ByteBuf packet = ((ByteBuf) msg).duplicate();
+ ByteBuf tcpBuf = byteBufAllocator.buffer();
+ int bytes = packet.readableBytes();
+
+ try {
+ if (isWriteOperation) {
+ TCPPacket.writePacket(tcpBuf, packet, sendSegmentNumber, receiveSegmentNumber, srcAddr.getPort(),
+ dstAddr.getPort(), TCPPacket.TCPFlag.ACK);
+ completeTCPWrite(srcAddr, dstAddr, tcpBuf, byteBufAllocator, ctx);
+ logTCP(true, bytes, sendSegmentNumber, receiveSegmentNumber, srcAddr, dstAddr, false);
+
+ sendSegmentNumber += bytes;
+
+ TCPPacket.writePacket(tcpBuf, null, receiveSegmentNumber, sendSegmentNumber, dstAddr.getPort(),
+ srcAddr.getPort(), TCPPacket.TCPFlag.ACK);
+ completeTCPWrite(dstAddr, srcAddr, tcpBuf, byteBufAllocator, ctx);
+ logTCP(true, bytes, sendSegmentNumber, receiveSegmentNumber, dstAddr, srcAddr, true);
+ } else {
+ TCPPacket.writePacket(tcpBuf, packet, receiveSegmentNumber, sendSegmentNumber, dstAddr.getPort(),
+ srcAddr.getPort(), TCPPacket.TCPFlag.ACK);
+ completeTCPWrite(dstAddr, srcAddr, tcpBuf, byteBufAllocator, ctx);
+ logTCP(false, bytes, receiveSegmentNumber, sendSegmentNumber, dstAddr, srcAddr, false);
+
+ receiveSegmentNumber += bytes;
+
+ TCPPacket.writePacket(tcpBuf, null, sendSegmentNumber, receiveSegmentNumber, srcAddr.getPort(),
+ dstAddr.getPort(), TCPPacket.TCPFlag.ACK);
+ completeTCPWrite(srcAddr, dstAddr, tcpBuf, byteBufAllocator, ctx);
+ logTCP(false, bytes, sendSegmentNumber, receiveSegmentNumber, srcAddr, dstAddr, true);
+ }
+ } finally {
+ tcpBuf.release();
+ }
+ } else {
+ logger.debug("Discarding Pcap Write for TCP Object: {}", msg);
+ }
+ }
+
+ /**
+ * Write TCP/IP L3 and L2 here.
+ *
+ * @param srcAddr {@link InetSocketAddress} Source Address of this Packet
+ * @param dstAddr {@link InetSocketAddress} Destination Address of this Packet
+ * @param tcpBuf {@link ByteBuf} containing TCP L4 Data
+ * @param byteBufAllocator {@link ByteBufAllocator} for allocating bytes for TCP/IP L3 and L2 data.
+ * @param ctx {@link ChannelHandlerContext} for {@code fireExceptionCaught}
+ */
+ private void completeTCPWrite(InetSocketAddress srcAddr, InetSocketAddress dstAddr, ByteBuf tcpBuf,
+ ByteBufAllocator byteBufAllocator, ChannelHandlerContext ctx) {
+
+ ByteBuf ipBuf = byteBufAllocator.buffer();
+ ByteBuf ethernetBuf = byteBufAllocator.buffer();
+ ByteBuf pcap = byteBufAllocator.buffer();
+
+ try {
+ if (srcAddr.getAddress() instanceof Inet4Address && dstAddr.getAddress() instanceof Inet4Address) {
+ IPPacket.writeTCPv4(ipBuf, tcpBuf,
+ NetUtil.ipv4AddressToInt((Inet4Address) srcAddr.getAddress()),
+ NetUtil.ipv4AddressToInt((Inet4Address) dstAddr.getAddress()));
+
+ EthernetPacket.writeIPv4(ethernetBuf, ipBuf);
+ } else if (srcAddr.getAddress() instanceof Inet6Address && dstAddr.getAddress() instanceof Inet6Address) {
+ IPPacket.writeTCPv6(ipBuf, tcpBuf,
+ srcAddr.getAddress().getAddress(),
+ dstAddr.getAddress().getAddress());
+
+ EthernetPacket.writeIPv6(ethernetBuf, ipBuf);
+ } else {
+ logger.error("Source and Destination IP Address versions are not same. Source Address: {}, " +
+ "Destination Address: {}", srcAddr.getAddress(), dstAddr.getAddress());
+ return;
+ }
+
+ // Write Packet into Pcap
+ pCapWriter.writePacket(pcap, ethernetBuf);
+ } catch (IOException ex) {
+ logger.error("Caught Exception While Writing Packet into Pcap", ex);
+ ctx.fireExceptionCaught(ex);
+ } finally {
+ ipBuf.release();
+ ethernetBuf.release();
+ pcap.release();
+ }
+ }
+
+ /**
+ * Logger for TCP
+ */
+ private void logTCP(boolean isWriteOperation, int bytes, int sendSegmentNumber, int receiveSegmentNumber,
+ InetSocketAddress srcAddr, InetSocketAddress dstAddr, boolean ackOnly) {
+ // If `ackOnly` is `true` when we don't need to write any data so we'll not
+ // log number of bytes being written and mark the operation as "TCP ACK".
+ if (logger.isDebugEnabled()) {
+ if (ackOnly) {
+ logger.debug("Writing TCP ACK, isWriteOperation {}, Segment Number {}, Ack Number {}, Src Addr {}, "
+ + "Dst Addr {}", isWriteOperation, sendSegmentNumber, receiveSegmentNumber, dstAddr, srcAddr);
+ } else {
+ logger.debug("Writing TCP Data of {} Bytes, isWriteOperation {}, Segment Number {}, Ack Number {}, " +
+ "Src Addr {}, Dst Addr {}", bytes, isWriteOperation, sendSegmentNumber,
+ receiveSegmentNumber, srcAddr, dstAddr);
+ }
+ }
+ }
+
+ /**
+ * Handle UDP l4
+ *
+ * @param ctx {@link ChannelHandlerContext} for {@code localAddress} / {@code remoteAddress},
+ * {@link ByteBuf} allocation and {@code fireExceptionCaught}
+ * @param msg {@link DatagramPacket} or {@link DatagramChannel}
+ */
+ private void handleUDP(ChannelHandlerContext ctx, Object msg) {
+ ByteBuf udpBuf = ctx.alloc().buffer();
+
+ try {
+ if (msg instanceof DatagramPacket) {
+
+ // If bytes are 0 and `captureZeroByte` is false, we won't capture this.
+ if (((DatagramPacket) msg).content().readableBytes() == 0 && !captureZeroByte) {
+ logger.debug("Discarding Zero Byte UDP Packet");
+ return;
+ }
+
+ DatagramPacket datagramPacket = ((DatagramPacket) msg).duplicate();
+ InetSocketAddress srcAddr = datagramPacket.sender();
+ InetSocketAddress dstAddr = datagramPacket.recipient();
+
+ // If `datagramPacket.sender()` is `null` then DatagramPacket is initialized
+ // `sender` (local) address. In this case, we'll get source address from Channel.
+ if (srcAddr == null) {
+ srcAddr = (InetSocketAddress) ctx.channel().localAddress();
+ }
+
+ logger.debug("Writing UDP Data of {} Bytes, Src Addr {}, Dst Addr {}",
+ datagramPacket.content().readableBytes(), srcAddr, dstAddr);
+
+ UDPPacket.writePacket(udpBuf, datagramPacket.content(), srcAddr.getPort(), dstAddr.getPort());
+ completeUDPWrite(srcAddr, dstAddr, udpBuf, ctx.alloc(), ctx);
+ } else if (msg instanceof ByteBuf && ((DatagramChannel) ctx.channel()).isConnected()) {
+
+ // If bytes are 0 and `captureZeroByte` is false, we won't capture this.
+ if (((ByteBuf) msg).readableBytes() == 0 && !captureZeroByte) {
+ logger.debug("Discarding Zero Byte UDP Packet");
+ return;
+ }
+
+ ByteBuf byteBuf = ((ByteBuf) msg).duplicate();
+
+ logger.debug("Writing UDP Data of {} Bytes, Src Addr {}, Dst Addr {}",
+ byteBuf.readableBytes(), srcAddr, dstAddr);
+
+ UDPPacket.writePacket(udpBuf, byteBuf, srcAddr.getPort(), dstAddr.getPort());
+ completeUDPWrite(srcAddr, dstAddr, udpBuf, ctx.alloc(), ctx);
+ } else {
+ logger.debug("Discarding Pcap Write for UDP Object: {}", msg);
+ }
+ } finally {
+ udpBuf.release();
+ }
+ }
+
+ /**
+ * Write UDP/IP L3 and L2 here.
+ *
+ * @param srcAddr {@link InetSocketAddress} Source Address of this Packet
+ * @param dstAddr {@link InetSocketAddress} Destination Address of this Packet
+ * @param udpBuf {@link ByteBuf} containing UDP L4 Data
+ * @param byteBufAllocator {@link ByteBufAllocator} for allocating bytes for UDP/IP L3 and L2 data.
+ * @param ctx {@link ChannelHandlerContext} for {@code fireExceptionCaught}
+ */
+ private void completeUDPWrite(InetSocketAddress srcAddr, InetSocketAddress dstAddr, ByteBuf udpBuf,
+ ByteBufAllocator byteBufAllocator, ChannelHandlerContext ctx) {
+
+ ByteBuf ipBuf = byteBufAllocator.buffer();
+ ByteBuf ethernetBuf = byteBufAllocator.buffer();
+ ByteBuf pcap = byteBufAllocator.buffer();
+
+ try {
+ if (srcAddr.getAddress() instanceof Inet4Address && dstAddr.getAddress() instanceof Inet4Address) {
+ IPPacket.writeUDPv4(ipBuf, udpBuf,
+ NetUtil.ipv4AddressToInt((Inet4Address) srcAddr.getAddress()),
+ NetUtil.ipv4AddressToInt((Inet4Address) dstAddr.getAddress()));
+
+ EthernetPacket.writeIPv4(ethernetBuf, ipBuf);
+ } else if (srcAddr.getAddress() instanceof Inet6Address && dstAddr.getAddress() instanceof Inet6Address) {
+ IPPacket.writeUDPv6(ipBuf, udpBuf,
+ srcAddr.getAddress().getAddress(),
+ dstAddr.getAddress().getAddress());
+
+ EthernetPacket.writeIPv6(ethernetBuf, ipBuf);
+ } else {
+ logger.error("Source and Destination IP Address versions are not same. Source Address: {}, " +
+ "Destination Address: {}", srcAddr.getAddress(), dstAddr.getAddress());
+ return;
+ }
+
+ // Write Packet into Pcap
+ pCapWriter.writePacket(pcap, ethernetBuf);
+ } catch (IOException ex) {
+ logger.error("Caught Exception While Writing Packet into Pcap", ex);
+ ctx.fireExceptionCaught(ex);
+ } finally {
+ ipBuf.release();
+ ethernetBuf.release();
+ pcap.release();
+ }
+ }
+
+ @Override
+ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+
+ // If `isTCP` is true, then we'll simulate a `FIN` flow.
+ if (ctx.channel() instanceof SocketChannel) {
+ logger.debug("Starting Fake TCP FIN+ACK Flow to close connection");
+
+ ByteBufAllocator byteBufAllocator = ctx.alloc();
+ ByteBuf tcpBuf = byteBufAllocator.buffer();
+
+ try {
+ // Write FIN+ACK with Normal Source and Destination Address
+ TCPPacket.writePacket(tcpBuf, null, sendSegmentNumber, receiveSegmentNumber, srcAddr.getPort(),
+ dstAddr.getPort(), TCPPacket.TCPFlag.FIN, TCPPacket.TCPFlag.ACK);
+ completeTCPWrite(srcAddr, dstAddr, tcpBuf, byteBufAllocator, ctx);
+
+ // Write FIN+ACK with Reversed Source and Destination Address
+ TCPPacket.writePacket(tcpBuf, null, receiveSegmentNumber, sendSegmentNumber, dstAddr.getPort(),
+ srcAddr.getPort(), TCPPacket.TCPFlag.FIN, TCPPacket.TCPFlag.ACK);
+ completeTCPWrite(dstAddr, srcAddr, tcpBuf, byteBufAllocator, ctx);
+
+ // Write ACK with Normal Source and Destination Address
+ TCPPacket.writePacket(tcpBuf, null, sendSegmentNumber + 1, receiveSegmentNumber + 1,
+ srcAddr.getPort(), dstAddr.getPort(), TCPPacket.TCPFlag.ACK);
+ completeTCPWrite(srcAddr, dstAddr, tcpBuf, byteBufAllocator, ctx);
+ } finally {
+ tcpBuf.release();
+ }
+
+ logger.debug("Finished Fake TCP FIN+ACK Flow to close connection");
+ }
+
+ this.pCapWriter.close();
+ super.handlerRemoved(ctx);
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+
+ if (ctx.channel() instanceof SocketChannel) {
+ ByteBuf tcpBuf = ctx.alloc().buffer();
+
+ try {
+ // Write RST with Normal Source and Destination Address
+ TCPPacket.writePacket(tcpBuf, null, sendSegmentNumber, receiveSegmentNumber, srcAddr.getPort(),
+ dstAddr.getPort(), TCPPacket.TCPFlag.RST, TCPPacket.TCPFlag.ACK);
+ completeTCPWrite(srcAddr, dstAddr, tcpBuf, ctx.alloc(), ctx);
+ } finally {
+ tcpBuf.release();
+ }
+
+ logger.debug("Sent Fake TCP RST to close connection");
+ }
+
+ this.pCapWriter.close();
+ ctx.fireExceptionCaught(cause);
+ }
+}
diff --git a/handler/src/main/java/io/netty/handler/pcap/PcapWriter.java b/handler/src/main/java/io/netty/handler/pcap/PcapWriter.java
new file mode 100644
index 0000000000..bae06b27b3
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/pcap/PcapWriter.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.handler.pcap;
+
+import io.netty.buffer.ByteBuf;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.TimeUnit;
+
+final class PcapWriter implements Closeable {
+
+ /**
+ * {@link OutputStream} where we'll write Pcap data.
+ */
+ private final OutputStream outputStream;
+
+ /**
+ * This uses {@link OutputStream} for writing Pcap.
+ * Pcap Global Header is not written on construction.
+ */
+ PcapWriter(OutputStream outputStream) {
+ this.outputStream = outputStream;
+ }
+
+ /**
+ * This uses {@link OutputStream} for writing Pcap.
+ * Pcap Global Header is also written on construction.
+ *
+ * @throws IOException If {@link OutputStream#write(byte[])} throws an exception
+ */
+ PcapWriter(OutputStream outputStream, ByteBuf byteBuf) throws IOException {
+ this.outputStream = outputStream;
+
+ PcapHeaders.writeGlobalHeader(byteBuf);
+ byteBuf.readBytes(outputStream, byteBuf.readableBytes());
+ }
+
+ /**
+ * Write Packet in Pcap OutputStream.
+ *
+ * @param packetHeaderBuf Packer Header {@link ByteBuf}
+ * @param packet Packet
+ * @throws IOException If {@link OutputStream#write(byte[])} throws an exception
+ */
+ void writePacket(ByteBuf packetHeaderBuf, ByteBuf packet) throws IOException {
+ long currentTime = System.currentTimeMillis();
+
+ PcapHeaders.writePacketHeader(
+ packetHeaderBuf,
+ (int) TimeUnit.MILLISECONDS.toSeconds(currentTime),
+ (int) TimeUnit.MILLISECONDS.toMicros(currentTime) % 1000000,
+ packet.readableBytes(),
+ packet.readableBytes()
+ );
+
+ packetHeaderBuf.readBytes(outputStream, packetHeaderBuf.readableBytes());
+ packet.readBytes(outputStream, packet.readableBytes());
+ }
+
+ @Override
+ public void close() throws IOException {
+ outputStream.flush();
+ outputStream.close();
+ }
+}
diff --git a/handler/src/main/java/io/netty/handler/pcap/TCPPacket.java b/handler/src/main/java/io/netty/handler/pcap/TCPPacket.java
new file mode 100644
index 0000000000..7916b8e9e6
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/pcap/TCPPacket.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.handler.pcap;
+
+import io.netty.buffer.ByteBuf;
+
+final class TCPPacket {
+
+ /**
+ * Data Offset + Reserved Bits.
+ */
+ private static final short OFFSET = 0x5000;
+
+ private TCPPacket() {
+ // Prevent outside initialization
+ }
+
+ /**
+ * Write TCP Packet
+ *
+ * @param byteBuf ByteBuf where Packet data will be set
+ * @param payload Payload of this Packet
+ * @param srcPort Source Port
+ * @param dstPort Destination Port
+ */
+ static void writePacket(ByteBuf byteBuf, ByteBuf payload, int segmentNumber, int ackNumber, int srcPort,
+ int dstPort, TCPFlag... tcpFlags) {
+
+ byteBuf.writeShort(srcPort); // Source Port
+ byteBuf.writeShort(dstPort); // Destination Port
+ byteBuf.writeInt(segmentNumber); // Segment Number
+ byteBuf.writeInt(ackNumber); // Acknowledgment Number
+ byteBuf.writeShort(OFFSET | TCPFlag.getFlag(tcpFlags)); // Flags
+ byteBuf.writeShort(65535); // Window Size
+ byteBuf.writeShort(0x0001); // Checksum
+ byteBuf.writeShort(0); // Urgent Pointer
+
+ if (payload != null) {
+ byteBuf.writeBytes(payload); // Payload of Data
+ }
+ }
+
+ enum TCPFlag {
+ FIN(1),
+ SYN(1 << 1),
+ RST(1 << 2),
+ PSH(1 << 3),
+ ACK(1 << 4),
+ URG(1 << 5),
+ ECE(1 << 6),
+ CWR(1 << 7);
+
+ private final int value;
+
+ TCPFlag(int value) {
+ this.value = value;
+ }
+
+ static int getFlag(TCPFlag... tcpFlags) {
+ int flags = 0;
+
+ for (TCPFlag tcpFlag : tcpFlags) {
+ flags |= tcpFlag.value;
+ }
+
+ return flags;
+ }
+ }
+}
diff --git a/handler/src/main/java/io/netty/handler/pcap/UDPPacket.java b/handler/src/main/java/io/netty/handler/pcap/UDPPacket.java
new file mode 100644
index 0000000000..de24e81bbe
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/pcap/UDPPacket.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.handler.pcap;
+
+import io.netty.buffer.ByteBuf;
+
+final class UDPPacket {
+
+ private static final short UDP_HEADER_SIZE = 8;
+
+ private UDPPacket() {
+ // Prevent outside initialization
+ }
+
+ /**
+ * Write UDP Packet
+ *
+ * @param byteBuf ByteBuf where Packet data will be set
+ * @param payload Payload of this Packet
+ * @param srcPort Source Port
+ * @param dstPort Destination Port
+ */
+ static void writePacket(ByteBuf byteBuf, ByteBuf payload, int srcPort, int dstPort) {
+ byteBuf.writeShort(srcPort); // Source Port
+ byteBuf.writeShort(dstPort); // Destination Port
+ byteBuf.writeShort(UDP_HEADER_SIZE + payload.readableBytes()); // UDP Header Length + Payload Length
+ byteBuf.writeShort(0x0001); // Checksum
+ byteBuf.writeBytes(payload); // Payload of Data
+ }
+}
diff --git a/handler/src/main/java/io/netty/handler/pcap/package-info.java b/handler/src/main/java/io/netty/handler/pcap/package-info.java
new file mode 100644
index 0000000000..dc324777d7
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/pcap/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Capture data and write into Pcap format which helps in troubleshooting.
+ */
+package io.netty.handler.pcap;
diff --git a/handler/src/test/java/io/netty/handler/pcap/PcapWriteHandlerTest.java b/handler/src/test/java/io/netty/handler/pcap/PcapWriteHandlerTest.java
new file mode 100644
index 0000000000..643d2c1259
--- /dev/null
+++ b/handler/src/test/java/io/netty/handler/pcap/PcapWriteHandlerTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.handler.pcap;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufOutputStream;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.DatagramPacket;
+import io.netty.channel.socket.nio.NioDatagramChannel;
+import io.netty.util.CharsetUtil;
+import io.netty.util.NetUtil;
+import org.junit.Test;
+
+import java.net.Inet4Address;
+import java.net.InetSocketAddress;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class PcapWriteHandlerTest {
+
+ @Test
+ public void udpV4() throws InterruptedException {
+
+ ByteBuf byteBuf = Unpooled.buffer();
+
+ InetSocketAddress srvAddr = new InetSocketAddress("127.0.0.1", 62001);
+ InetSocketAddress cltAddr = new InetSocketAddress("127.0.0.1", 62002);
+
+ NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup(2);
+
+ // We'll bootstrap a UDP Server to avoid "Network Unreachable errors" when sending UDP Packet.
+ Bootstrap server = new Bootstrap()
+ .group(eventLoopGroup)
+ .channel(NioDatagramChannel.class)
+ .handler(new SimpleChannelInboundHandler() {
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) {
+ // Discard
+ }
+ });
+
+ ChannelFuture channelFutureServer = server.bind(srvAddr).sync();
+ assertTrue(channelFutureServer.isSuccess());
+
+ // We'll bootstrap a UDP Client for sending UDP Packets to UDP Server.
+ Bootstrap client = new Bootstrap()
+ .group(eventLoopGroup)
+ .channel(NioDatagramChannel.class)
+ .handler(new PcapWriteHandler(new ByteBufOutputStream(byteBuf)));
+
+ ChannelFuture channelFutureClient = client.connect(srvAddr, cltAddr).sync();
+ assertTrue(channelFutureClient.isSuccess());
+ assertTrue(channelFutureClient.channel().writeAndFlush(Unpooled.wrappedBuffer("Meow".getBytes()))
+ .sync().isSuccess());
+ assertTrue(eventLoopGroup.shutdownGracefully().sync().isSuccess());
+
+ // Verify Pcap Global Headers
+ assertEquals(0xa1b2c3d4, byteBuf.readInt()); // magic_number
+ assertEquals(2, byteBuf.readShort()); // version_major
+ assertEquals(4, byteBuf.readShort()); // version_minor
+ assertEquals(0, byteBuf.readInt()); // thiszone
+ assertEquals(0, byteBuf.readInt()); // sigfigs
+ assertEquals(0xffff, byteBuf.readInt()); // snaplen
+ assertEquals(1, byteBuf.readInt()); // network
+
+ // Verify Pcap Packet Header
+ byteBuf.readInt(); // Just read, we don't care about timestamps for now
+ byteBuf.readInt(); // Just read, we don't care about timestamps for now
+ assertEquals(46, byteBuf.readInt()); // Length of Packet Saved In Pcap
+ assertEquals(46, byteBuf.readInt()); // Actual Length of Packet
+
+ // -------------------------------------------- Verify Packet --------------------------------------------
+ // Verify Ethernet Packet
+ ByteBuf ethernetPacket = byteBuf.readBytes(46);
+ ByteBuf dstMac = ethernetPacket.readBytes(6);
+ ByteBuf srcMac = ethernetPacket.readBytes(6);
+ assertArrayEquals(new byte[]{0, 0, 94, 0, 83, -1}, ByteBufUtil.getBytes(dstMac));
+ assertArrayEquals(new byte[]{0, 0, 94, 0, 83, 0}, ByteBufUtil.getBytes(srcMac));
+ assertEquals(0x0800, ethernetPacket.readShort());
+
+ // Verify IPv4 Packet
+ ByteBuf ipv4Packet = ethernetPacket.readBytes(32);
+ assertEquals(0x45, ipv4Packet.readByte()); // Version + IHL
+ assertEquals(0x00, ipv4Packet.readByte()); // DSCP
+ assertEquals(32, ipv4Packet.readShort()); // Length
+ assertEquals(0x0000, ipv4Packet.readShort()); // Identification
+ assertEquals(0x0000, ipv4Packet.readShort()); // Fragment
+ assertEquals((byte) 0xff, ipv4Packet.readByte()); // TTL
+ assertEquals((byte) 17, ipv4Packet.readByte()); // Protocol
+ assertEquals(0, ipv4Packet.readShort()); // Checksum
+ // Source IPv4 Address
+ assertEquals(NetUtil.ipv4AddressToInt((Inet4Address) srvAddr.getAddress()), ipv4Packet.readInt());
+ // Destination IPv4 Address
+ assertEquals(NetUtil.ipv4AddressToInt((Inet4Address) cltAddr.getAddress()), ipv4Packet.readInt());
+
+ // Verify UDP Packet
+ ByteBuf udpPacket = ipv4Packet.readBytes(12);
+ assertEquals(cltAddr.getPort() & 0xffff, udpPacket.readUnsignedShort()); // Source Port
+ assertEquals(srvAddr.getPort() & 0xffff, udpPacket.readUnsignedShort()); // Destination Port
+ assertEquals(12, udpPacket.readShort()); // Length
+ assertEquals(0x0001, udpPacket.readShort()); // Checksum
+
+ // Verify Payload
+ ByteBuf payload = udpPacket.readBytes(4);
+ assertArrayEquals("Meow".getBytes(CharsetUtil.UTF_8), ByteBufUtil.getBytes(payload)); // Payload
+
+ // Release all ByteBuf
+ assertTrue(dstMac.release());
+ assertTrue(srcMac.release());
+ assertTrue(payload.release());
+ assertTrue(byteBuf.release());
+ assertTrue(ethernetPacket.release());
+ assertTrue(ipv4Packet.release());
+ assertTrue(udpPacket.release());
+ }
+}