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: + *
    + *
  1. magic_number
  2. + *
  3. version_major
  4. + *
  5. version_minor
  6. + *
  7. thiszone
  8. + *
  9. sigfigs
  10. + *
  11. snaplen
  12. + *
  13. network
  14. + *
+ */ + 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: + * + *

+ *

+ */ +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()); + } +}