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:
parent
8c1db6ccb8
commit
d889397c1e
@ -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
|
||||||
|
}
|
||||||
|
}
|
111
handler/src/main/java/io/netty/handler/pcap/IPPacket.java
Normal file
111
handler/src/main/java/io/netty/handler/pcap/IPPacket.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
67
handler/src/main/java/io/netty/handler/pcap/PcapHeaders.java
Normal file
67
handler/src/main/java/io/netty/handler/pcap/PcapHeaders.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
80
handler/src/main/java/io/netty/handler/pcap/PcapWriter.java
Normal file
80
handler/src/main/java/io/netty/handler/pcap/PcapWriter.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
82
handler/src/main/java/io/netty/handler/pcap/TCPPacket.java
Normal file
82
handler/src/main/java/io/netty/handler/pcap/TCPPacket.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
handler/src/main/java/io/netty/handler/pcap/UDPPacket.java
Normal file
43
handler/src/main/java/io/netty/handler/pcap/UDPPacket.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user