io.netty.handler.pcap.PcapWriteHandler Maven / Gradle / Ivy
/*
* 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:
*
* https://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.Channel;
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.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.concurrent.atomic.AtomicReference;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
/**
* {@link PcapWriteHandler} captures {@link ByteBuf} from {@link SocketChannel} / {@link ServerChannel}
* or {@link DatagramPacket} and writes it into Pcap {@link OutputStream}.
*
*
* Things to keep in mind when using {@link PcapWriteHandler} with TCP:
*
*
* - Whenever {@link ChannelInboundHandlerAdapter#channelActive(ChannelHandlerContext)} is called,
* a fake TCP 3-way handshake (SYN, SYN+ACK, ACK) is simulated as new connection in Pcap.
*
* - Whenever {@link ChannelInboundHandlerAdapter#handlerRemoved(ChannelHandlerContext)} is called,
* a fake TCP 3-way handshake (FIN+ACK, FIN+ACK, ACK) is simulated as connection shutdown in Pcap.
*
* - Whenever {@link ChannelInboundHandlerAdapter#exceptionCaught(ChannelHandlerContext, Throwable)}
* is called, a fake TCP RST is sent to simulate connection Reset in Pcap.
*
* - ACK is sent each time data is send / received.
*
* - Zero Length Data Packets can cause TCP Double ACK error in Wireshark. To tackle this,
* set {@code captureZeroByte} to {@code false}.
*
*
*/
public final class PcapWriteHandler extends ChannelDuplexHandler implements Closeable {
/**
* Logger for logging events
*/
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;
/**
* {@code true} if we want to synchronize on the {@link OutputStream} while writing
* else {@code false}.
*/
private final boolean sharedOutputStream;
/**
* 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;
/**
* Type of the channel this handler is registered on
*/
private ChannelType channelType;
/**
* Address of the initiator of the connection
*/
private InetSocketAddress initiatorAddr;
/**
* Address of the receiver of the connection
*/
private InetSocketAddress handlerAddr;
/**
* Set to {@code true} if this handler is registered on a server pipeline
*/
private boolean isServerPipeline;
/**
* Current of this {@link PcapWriteHandler}
*/
private final AtomicReference state = new AtomicReference(State.INIT);
/**
* 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. Call {@link #close()} to close this
* OutputStream.
* @throws NullPointerException If {@link OutputStream} is {@code null} then we'll throw an
* {@link NullPointerException}
*
* @deprecated Use {@link Builder} instead.
*/
@Deprecated
public PcapWriteHandler(OutputStream outputStream) {
this(outputStream, false, true);
}
/**
* Create new {@link PcapWriteHandler} Instance
*
* @param outputStream OutputStream where Pcap data will be written. Call {@link #close()} to close this
* OutputStream.
* @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}
*
* @deprecated Use {@link Builder} instead.
*/
@Deprecated
public PcapWriteHandler(OutputStream outputStream, boolean captureZeroByte, boolean writePcapGlobalHeader) {
this.outputStream = checkNotNull(outputStream, "OutputStream");
this.captureZeroByte = captureZeroByte;
this.writePcapGlobalHeader = writePcapGlobalHeader;
sharedOutputStream = false;
}
private PcapWriteHandler(Builder builder, OutputStream outputStream) {
this.outputStream = outputStream;
captureZeroByte = builder.captureZeroByte;
sharedOutputStream = builder.sharedOutputStream;
writePcapGlobalHeader = builder.writePcapGlobalHeader;
channelType = builder.channelType;
handlerAddr = builder.handlerAddr;
initiatorAddr = builder.initiatorAddr;
isServerPipeline = builder.isServerPipeline;
}
/**
* Writes the Pcap Global Header to the provided {@code OutputStream}
*
* @param outputStream OutputStream where Pcap data will be written.
* @throws IOException if there is an error writing to the {@code OutputStream}
*/
public static void writeGlobalHeader(OutputStream outputStream) throws IOException {
PcapHeaders.writeGlobalHeader(outputStream);
}
private void initializeIfNecessary(ChannelHandlerContext ctx) throws Exception {
// If State is not 'INIT' then it means we're already initialized so then no need to initiaize again.
if (state.get() != State.INIT) {
return;
}
pCapWriter = new PcapWriter(this);
if (channelType == null) {
// infer channel type
if (ctx.channel() instanceof SocketChannel) {
channelType = ChannelType.TCP;
// If Channel belongs to `SocketChannel` then we're handling TCP.
// Capture correct `localAddress` and `remoteAddress`
if (ctx.channel().parent() instanceof ServerSocketChannel) {
isServerPipeline = true;
initiatorAddr = (InetSocketAddress) ctx.channel().remoteAddress();
handlerAddr = getLocalAddress(ctx.channel(), initiatorAddr);
} else {
isServerPipeline = false;
handlerAddr = (InetSocketAddress) ctx.channel().remoteAddress();
initiatorAddr = getLocalAddress(ctx.channel(), handlerAddr);
}
} else if (ctx.channel() instanceof DatagramChannel) {
channelType = ChannelType.UDP;
DatagramChannel datagramChannel = (DatagramChannel) ctx.channel();
// If `DatagramChannel` is connected then we can get
// `localAddress` and `remoteAddress` from Channel.
if (datagramChannel.isConnected()) {
handlerAddr = (InetSocketAddress) ctx.channel().remoteAddress();
initiatorAddr = getLocalAddress(ctx.channel(), handlerAddr);
}
}
}
if (channelType == ChannelType.TCP) {
logger.debug("Initiating Fake TCP 3-Way Handshake");
ByteBuf tcpBuf = ctx.alloc().buffer();
try {
// Write SYN with Normal Source and Destination Address
TCPPacket.writePacket(tcpBuf, null, 0, 0,
initiatorAddr.getPort(), handlerAddr.getPort(), TCPPacket.TCPFlag.SYN);
completeTCPWrite(initiatorAddr, handlerAddr, tcpBuf, ctx.alloc(), ctx);
// Write SYN+ACK with Reversed Source and Destination Address
TCPPacket.writePacket(tcpBuf, null, 0, 1,
handlerAddr.getPort(), initiatorAddr.getPort(), TCPPacket.TCPFlag.SYN, TCPPacket.TCPFlag.ACK);
completeTCPWrite(handlerAddr, initiatorAddr, tcpBuf, ctx.alloc(), ctx);
// Write ACK with Normal Source and Destination Address
TCPPacket.writePacket(tcpBuf, null, 1, 1, initiatorAddr.getPort(),
handlerAddr.getPort(), TCPPacket.TCPFlag.ACK);
completeTCPWrite(initiatorAddr, handlerAddr, tcpBuf, ctx.alloc(), ctx);
} finally {
tcpBuf.release();
}
logger.debug("Finished Fake TCP 3-Way Handshake");
}
state.set(State.WRITING);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
initializeIfNecessary(ctx);
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// Initialize if needed
if (state.get() == State.INIT) {
initializeIfNecessary(ctx);
}
// Only write if State is STARTED
if (state.get() == State.WRITING) {
if (channelType == ChannelType.TCP) {
handleTCP(ctx, msg, false);
} else if (channelType == ChannelType.UDP) {
handleUDP(ctx, msg);
} else {
logDiscard();
}
}
super.channelRead(ctx, msg);
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
// Initialize if needed
if (state.get() == State.INIT) {
initializeIfNecessary(ctx);
}
// Only write if State is STARTED
if (state.get() == State.WRITING) {
if (channelType == ChannelType.TCP) {
handleTCP(ctx, msg, true);
} else if (channelType == ChannelType.UDP) {
handleUDP(ctx, msg);
} else {
logDiscard();
}
}
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) {
final InetSocketAddress srcAddr;
final InetSocketAddress dstAddr;
if (isServerPipeline) {
srcAddr = handlerAddr;
dstAddr = initiatorAddr;
} else {
srcAddr = initiatorAddr;
dstAddr = handlerAddr;
}
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 {
final InetSocketAddress srcAddr;
final InetSocketAddress dstAddr;
if (isServerPipeline) {
srcAddr = initiatorAddr;
dstAddr = handlerAddr;
} else {
srcAddr = handlerAddr;
dstAddr = initiatorAddr;
}
TCPPacket.writePacket(tcpBuf, packet, receiveSegmentNumber, sendSegmentNumber, srcAddr.getPort(),
dstAddr.getPort(), TCPPacket.TCPFlag.ACK);
completeTCPWrite(srcAddr, dstAddr, tcpBuf, byteBufAllocator, ctx);
logTCP(false, bytes, receiveSegmentNumber, sendSegmentNumber, srcAddr, dstAddr, false);
receiveSegmentNumber += bytes;
TCPPacket.writePacket(tcpBuf, null, sendSegmentNumber, receiveSegmentNumber, dstAddr.getPort(),
srcAddr.getPort(), TCPPacket.TCPFlag.ACK);
completeTCPWrite(dstAddr, srcAddr, tcpBuf, byteBufAllocator, ctx);
logTCP(false, bytes, sendSegmentNumber, receiveSegmentNumber, dstAddr, srcAddr, 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();
}
}
/**
* Handle UDP l4
*
* @param ctx {@link ChannelHandlerContext} for {@code localAddress} / {@code remoteAddress},
* {@link ByteBuf} allocation and {@code fireExceptionCaught}
* @param msg {@link DatagramPacket} or {@link ByteBuf}
*/
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 = getLocalAddress(ctx.channel(), dstAddr);
}
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 &&
(!(ctx.channel() instanceof DatagramChannel) || ((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(), initiatorAddr, handlerAddr);
UDPPacket.writePacket(udpBuf, byteBuf, initiatorAddr.getPort(), handlerAddr.getPort());
completeUDPWrite(initiatorAddr, handlerAddr, 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();
}
}
/**
* Get the local address of a channel. If the address is a wildcard address ({@code 0.0.0.0} or {@code ::}), and
* the address family does not match that of the {@code remote}, return the wildcard address of the {@code remote}'s
* family instead.
*
* @param ch The channel to get the local address from
* @param remote The remote address
* @return The fixed local address
*/
private static InetSocketAddress getLocalAddress(Channel ch, InetSocketAddress remote) {
InetSocketAddress local = (InetSocketAddress) ch.localAddress();
if (remote != null && local.getAddress().isAnyLocalAddress()) {
if (local.getAddress() instanceof Inet4Address && remote.getAddress() instanceof Inet6Address) {
return new InetSocketAddress(WildcardAddressHolder.wildcard6, local.getPort());
}
if (local.getAddress() instanceof Inet6Address && remote.getAddress() instanceof Inet4Address) {
return new InetSocketAddress(WildcardAddressHolder.wildcard4, local.getPort());
}
}
return local;
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// If `isTCP` is true, then we'll simulate a `FIN` flow.
if (channelType == ChannelType.TCP) {
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, initiatorAddr.getPort(),
handlerAddr.getPort(), TCPPacket.TCPFlag.FIN, TCPPacket.TCPFlag.ACK);
completeTCPWrite(initiatorAddr, handlerAddr, tcpBuf, byteBufAllocator, ctx);
// Write FIN+ACK with Reversed Source and Destination Address
TCPPacket.writePacket(tcpBuf, null, receiveSegmentNumber, sendSegmentNumber, handlerAddr.getPort(),
initiatorAddr.getPort(), TCPPacket.TCPFlag.FIN, TCPPacket.TCPFlag.ACK);
completeTCPWrite(handlerAddr, initiatorAddr, tcpBuf, byteBufAllocator, ctx);
// Write ACK with Normal Source and Destination Address
TCPPacket.writePacket(tcpBuf, null, sendSegmentNumber + 1, receiveSegmentNumber + 1,
initiatorAddr.getPort(), handlerAddr.getPort(), TCPPacket.TCPFlag.ACK);
completeTCPWrite(initiatorAddr, handlerAddr, tcpBuf, byteBufAllocator, ctx);
} finally {
tcpBuf.release();
}
logger.debug("Finished Fake TCP FIN+ACK Flow to close connection");
}
close();
super.handlerRemoved(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (channelType == ChannelType.TCP) {
ByteBuf tcpBuf = ctx.alloc().buffer();
try {
// Write RST with Normal Source and Destination Address
TCPPacket.writePacket(tcpBuf, null, sendSegmentNumber, receiveSegmentNumber, initiatorAddr.getPort(),
handlerAddr.getPort(), TCPPacket.TCPFlag.RST, TCPPacket.TCPFlag.ACK);
completeTCPWrite(initiatorAddr, handlerAddr, tcpBuf, ctx.alloc(), ctx);
} finally {
tcpBuf.release();
}
logger.debug("Sent Fake TCP RST to close connection");
}
close();
ctx.fireExceptionCaught(cause);
}
/**
* 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);
}
}
}
OutputStream outputStream() {
return outputStream;
}
boolean sharedOutputStream() {
return sharedOutputStream;
}
/**
* Returns {@code true} if the {@link PcapWriteHandler} is currently
* writing packets to the {@link OutputStream} else returns {@code false}.
*/
public boolean isWriting() {
return state.get() == State.WRITING;
}
State state() {
return state.get();
}
/**
* Pause the {@link PcapWriteHandler} from writing packets to the {@link OutputStream}.
*/
public void pause() {
if (!state.compareAndSet(State.WRITING, State.PAUSED)) {
throw new IllegalStateException("State must be 'STARTED' to pause but current state is: " + state);
}
}
/**
* Resume the {@link PcapWriteHandler} to writing packets to the {@link OutputStream}.
*/
public void resume() {
if (!state.compareAndSet(State.PAUSED, State.WRITING)) {
throw new IllegalStateException("State must be 'PAUSED' to resume but current state is: " + state);
}
}
void markClosed() {
if (state.get() != State.CLOSED) {
state.set(State.CLOSED);
}
}
// Visible for testing only.
PcapWriter pCapWriter() {
return pCapWriter;
}
private void logDiscard() {
logger.warn("Discarding pcap write because channel type is unknown. The channel this handler is registered " +
"on is not a SocketChannel or DatagramChannel, so the inference does not work. Please call " +
"forceTcpChannel or forceUdpChannel before registering the handler.");
}
@Override
public String toString() {
return "PcapWriteHandler{" +
"captureZeroByte=" + captureZeroByte +
", writePcapGlobalHeader=" + writePcapGlobalHeader +
", sharedOutputStream=" + sharedOutputStream +
", sendSegmentNumber=" + sendSegmentNumber +
", receiveSegmentNumber=" + receiveSegmentNumber +
", channelType=" + channelType +
", initiatorAddr=" + initiatorAddr +
", handlerAddr=" + handlerAddr +
", isServerPipeline=" + isServerPipeline +
", state=" + state +
'}';
}
/**
* Close {@code PcapWriter} and {@link OutputStream}.
* Note: Calling this method does not close {@link PcapWriteHandler}.
* Only Pcap Writes are closed.
*
* @throws IOException If {@link OutputStream#close()} throws an exception
*/
@Override
public void close() throws IOException {
if (state.get() == State.CLOSED) {
logger.debug("PcapWriterHandler is already closed");
} else {
pCapWriter.close();
markClosed();
logger.debug("PcapWriterHandler is now closed");
}
}
private enum ChannelType {
TCP, UDP
}
public static Builder builder() {
return new Builder();
}
/**
* Builder for {@link PcapWriteHandler}
*/
public static final class Builder {
private boolean captureZeroByte;
private boolean sharedOutputStream;
private boolean writePcapGlobalHeader = true;
private ChannelType channelType;
private InetSocketAddress initiatorAddr;
private InetSocketAddress handlerAddr;
private boolean isServerPipeline;
private Builder() {
}
/**
* 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 captureZeroByte Whether to filter out empty packets.
* @return this builder
*/
public Builder captureZeroByte(boolean captureZeroByte) {
this.captureZeroByte = captureZeroByte;
return this;
}
/**
* Set to {@code true} if multiple {@link PcapWriteHandler} instances will be
* writing to the same {@link OutputStream} concurrently, and write locking is
* required. Otherwise, if set to {@code false}, no locking will be done.
* Additionally, {@link #close} will not close the underlying {@code OutputStream}.
* Note: it is probably an error to have both {@code writePcapGlobalHeader} and
* {@code sharedOutputStream} set to {@code true} at the same time.
*
* @param sharedOutputStream Whether {@link OutputStream} is shared or not
* @return this builder
*/
public Builder sharedOutputStream(boolean sharedOutputStream) {
this.sharedOutputStream = sharedOutputStream;
return this;
}
/**
* 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.
*
* @param writePcapGlobalHeader Whether to write the pcap global header.
* @return this builder
*/
public Builder writePcapGlobalHeader(boolean writePcapGlobalHeader) {
this.writePcapGlobalHeader = writePcapGlobalHeader;
return this;
}
/**
* Force this handler to write data as if they were TCP packets, with the given connection metadata. If this
* method isn't called, we determine the metadata from the channel.
*
* @param serverAddress The address of the TCP server (handler)
* @param clientAddress The address of the TCP client (initiator)
* @param isServerPipeline Whether the handler is part of the server channel
* @return this builder
*/
public Builder forceTcpChannel(InetSocketAddress serverAddress, InetSocketAddress clientAddress,
boolean isServerPipeline) {
channelType = ChannelType.TCP;
handlerAddr = checkNotNull(serverAddress, "serverAddress");
initiatorAddr = checkNotNull(clientAddress, "clientAddress");
this.isServerPipeline = isServerPipeline;
return this;
}
/**
* Force this handler to write data as if they were UDP packets, with the given connection metadata. If this
* method isn't called, we determine the metadata from the channel.
*
* Note that even if this method is called, the address information on {@link DatagramPacket} takes precedence
* if it is present.
*
* @param localAddress The address of the UDP local
* @param remoteAddress The address of the UDP remote
* @return this builder
*/
public Builder forceUdpChannel(InetSocketAddress localAddress, InetSocketAddress remoteAddress) {
channelType = ChannelType.UDP;
handlerAddr = checkNotNull(remoteAddress, "remoteAddress");
initiatorAddr = checkNotNull(localAddress, "localAddress");
return this;
}
/**
* Build the {@link PcapWriteHandler}.
*
* @param outputStream The output stream to write the pcap data to.
* @return The handler.
*/
public PcapWriteHandler build(OutputStream outputStream) {
checkNotNull(outputStream, "outputStream");
return new PcapWriteHandler(this, outputStream);
}
}
private static final class WildcardAddressHolder {
static final InetAddress wildcard4; // 0.0.0.0
static final InetAddress wildcard6; // ::
static {
try {
wildcard4 = InetAddress.getByAddress(new byte[4]);
wildcard6 = InetAddress.getByAddress(new byte[16]);
} catch (UnknownHostException e) {
// would only happen if the byte array was of incorrect size
throw new AssertionError(e);
}
}
}
}