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.
This commit is contained in:
Aayush Atharva 2020-09-11 19:36:16 +05:30 committed by GitHub
parent 8c1db6ccb8
commit d889397c1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1144 additions and 0 deletions

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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:
* <ol>
* <li> magic_number </li>
* <li> version_major </li>
* <li> version_minor </li>
* <li> thiszone </li>
* <li> sigfigs </li>
* <li> snaplen </li>
* <li> network </li>
* </ol>
*/
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);
}
}

View File

@ -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;
/**
* <p> {@link PcapWriteHandler} captures {@link ByteBuf} from {@link SocketChannel} / {@link ServerChannel}
* or {@link DatagramPacket} and writes it into Pcap {@link OutputStream}. </p>
*
* <p>
* Things to keep in mind when using {@link PcapWriteHandler} with TCP:
*
* <ul>
* <li> Whenever {@link ChannelInboundHandlerAdapter#channelActive(ChannelHandlerContext)} is called,
* a fake TCP 3-way handshake (SYN, SYN+ACK, ACK) is simulated as new connection in Pcap. </li>
*
* <li> 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. </li>
*
* <li> Whenever {@link ChannelInboundHandlerAdapter#exceptionCaught(ChannelHandlerContext, Throwable)}
* is called, a fake TCP RST is sent to simulate connection Reset in Pcap. </li>
*
* <li> ACK is sent each time data is send / received. </li>
*
* <li> Zero Length Data Packets can cause TCP Double ACK error in Wireshark. To tackle this,
* set {@code captureZeroByte} to {@code false}. </li>
* </ul>
* </p>
*/
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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -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;

View File

@ -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<DatagramPacket>() {
@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());
}
}