/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed 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 net.java.sip.communicator.impl.packetlogging;
import java.io.*;
import java.util.*;
import com.google.common.collect.*;
import net.java.sip.communicator.util.*;
import org.jitsi.service.fileaccess.*;
import org.jitsi.service.packetlogging.*;
/**
* Packet Logging Service implementation dumping logs in
* pcap(tcpdump/wireshark) format file.
*
* @author Damian Minkov
*/
public class PacketLoggingServiceImpl
implements PacketLoggingService
{
/**
* Our Logger.
*/
private static final Logger logger
= Logger.getLogger(PacketLoggingServiceImpl.class);
/**
* The max size of the <tt>EvictingQueue</tt> that the saver thread
* is using.
*
* TODO this needs to be configurable eventually.
*/
private static final int EVICTING_QUEUE_MAX_SIZE = 1000;
/**
* The OutputStream we are currently writing to.
*/
private FileOutputStream outputStream = null;
/**
* The thread that queues packets and saves them to file.
*/
private SaverThread saverThread = new SaverThread();
/**
* The current configuration.
*/
private PacketLoggingConfiguration packetLoggingConfiguration = null;
/**
* The fake ethernet header we use as template.
*/
private final static byte[] fakeEthernetHeader =
new byte[]{
(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,
(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,
(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00
};
/** IEEE 802.3 EtherType for IPv4 */
private final static byte[] ipv4EtherType =
new byte[] { 0x08, 0x00 };
/** IEEE 802.3 EtherType for IPv6 */
private final static byte[] ipv6EtherType =
new byte[] { (byte)0x86, (byte)0xdd };
/**
* The fake ipv4 header we use as template.
*/
private final static byte[] ipHeaderTemplate =
new byte[]{
(byte)0x45, (byte)0x00,
(byte)0x03, (byte)0x48, (byte)0xc9, (byte)0x14,
(byte)0x00, (byte)0x00, (byte)0x35,(byte)0x11,
(byte)0x00, (byte)0x00, // check sum
(byte)0xd5, (byte)0xc0, (byte)0x3b, (byte)0x4b,//src
(byte)0xc0, (byte)0xa8, (byte)0x00, (byte)0x34 //dst
};
/**
* The fake ipv6 header we use as template.
*/
private final static byte[] ip6HeaderTemplate =
new byte[]{
(byte)0x60, (byte)0x00, (byte)0x00, (byte)0x00, // version, traffic, flowable
(byte)0x00, (byte)0x00, // length
(byte)0x11, // next header
(byte)0x40, // hop limit
(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, // src
(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, // src
(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, // src
(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, // src
(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, // dst
(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, // dst
(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, // dst
(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00 // dst
};
/**
* The fake udp header we use as template.
*/
private final static byte[] udpHeaderTemplate =
new byte[]{
(byte)0x13, (byte)0xc4,
(byte)0x13, (byte)0xc4,
(byte)0x03, (byte)0x34,
(byte)0x00, (byte)0x00// checksum
};
/**
* The fake tcp header we use as template.
*/
private final static byte[] tcpHeaderTemplate =
new byte[]{
(byte)0xb7, (byte)0x61, // src port
(byte)0x13, (byte)0xc4, // dst port
(byte)0x4f, (byte)0x20, (byte)0x37, (byte)0x3b, // seq number
(byte)0x11, (byte)0x1d, (byte)0xbc, (byte)0x54, // ack number
(byte)0x80, // header length
(byte)0x18, // flags
(byte)0x00, (byte)0x2e, // windows size
(byte)0xac, (byte)0x78, // check sum
(byte)0x00, (byte)0x00,
(byte)0x01, (byte)0x01, (byte)0x08, (byte)0x0a, // options
(byte)0x00, (byte)0x06, (byte)0xd4, (byte)0x48, // options
(byte)0x6e, (byte)0xcc, (byte)0x76, (byte)0xbd // options
};
/**
* Using this object to lock and protectd the two counters
* used for tcp seq and ack numbers.
*/
private Object tcpCounterLock = new Object();
/**
* The seq that the sender will send.
*/
private long srcCount = 1;
/**
* This is the ack number send from the sender.
*/
private long dstCount = 1;
/**
* A counter watching how much has been written to the file.
*/
private long written = 0;
/**
* All the files we can use for writing.
*/
private File[] files;
/**
* Starting the packet logger. Generating the files we can use,
* rotate any previous files and open the current file for writing.
*/
public void start()
{
saverThread.start();
}
/**
* Generates the files we will later use for writing.
* @throws Exception
*/
private void getFileNames()
throws Exception
{
int fileCount = getConfiguration().getLogfileCount();
files = new File[fileCount];
for(int i = 0; i < fileCount; i++)
{
files[i]
= PacketLoggingActivator.getFileAccessService()
.getPrivatePersistentFile(
new File(PacketLoggingActivator.LOGGING_DIR_NAME,
"jitsi" + i + ".pcap").toString(),
FileCategory.LOG);
}
}
/**
* Rotates any existing files and use the newly created first one
* for writing.
* @throws IOException
*/
private void rotateFiles()
throws IOException
{
if(outputStream != null)
{
outputStream.flush();
outputStream.close();
}
for (int i = getConfiguration().getLogfileCount() - 2; i >= 0; i--)
{
File f1 = files[i];
File f2 = files[i+1];
if (f1.exists())
{
if (f2.exists())
f2.delete();
f1.renameTo(f2);
}
}
outputStream = new FileOutputStream(files[0]);
written = 0;
createGlobalHeader();
}
/**
* Stops the packet logging.
*/
public void stop()
{
saverThread.stopRunning();
if(outputStream != null)
{
try
{
outputStream.flush();
outputStream.close();
}
catch(IOException e)
{
e.printStackTrace();
}
finally
{
outputStream = null;
}
}
}
/**
* Creates pcap file global header.
* @throws IOException
*/
private void createGlobalHeader()
throws IOException
{
/* magic number(swapped) */
outputStream.write(0xd4);
outputStream.write(0xc3);
outputStream.write(0xb2);
outputStream.write(0xa1);
/* major version number */
outputStream.write(0x02);
outputStream.write(0x00);
/* minor version number */
outputStream.write(0x04);
outputStream.write(0x00);
/* GMT to local correction */
outputStream.write(0x00);
outputStream.write(0x00);
outputStream.write(0x00);
outputStream.write(0x00);
/* accuracy of timestamps */
outputStream.write(0x00);
outputStream.write(0x00);
outputStream.write(0x00);
outputStream.write(0x00);
/* max length of captured packets, in octets */
outputStream.write(0xff);
outputStream.write(0xff);
outputStream.write(0x00);
outputStream.write(0x00);
/* data link type(ethernet) */
outputStream.write(0x01);
outputStream.write(0x00);
outputStream.write(0x00);
outputStream.write(0x00);
}
/**
* Checks is logging globally enabled for the service.
*
* @return is logging enabled.
*/
public boolean isLoggingEnabled()
{
return getConfiguration().isGlobalLoggingEnabled();
}
/**
* Checks is logging globally enabled for and is it currently available for
* the given service.
*
* @param protocol that is checked.
* @return is logging enabled.
*/
public boolean isLoggingEnabled(ProtocolName protocol)
{
PacketLoggingConfiguration cfg = getConfiguration();
if (cfg.isGlobalLoggingEnabled())
{
switch(protocol)
{
case SIP:
return cfg.isSipLoggingEnabled();
case JABBER:
return cfg.isJabberLoggingEnabled();
case RTP:
return cfg.isRTPLoggingEnabled();
case ICE4J:
return cfg.isIce4JLoggingEnabled();
case ARBITRARY:
return cfg.isArbitraryLoggingEnabled();
default:
// It may seem like it was unnecessary to invoke
// getConfiguration and isGlobalLoggingEnabled prior to checking
// that the specified protocol is supported but, actually, there
// are no other ProtocolName values.
return false;
}
}
else
return false;
}
/**
* Log a packet with all the required information.
*
* @param protocol the source protocol that logs this packet.
* @param sourceAddress the source address of the packet.
* @param sourcePort the source port of the packet.
* @param destinationAddress the destination address.
* @param destinationPort the destination port.
* @param transport the transport this packet uses.
* @param sender are we the sender of the packet or not.
* @param packetContent the packet content.
*/
public void logPacket(
ProtocolName protocol,
byte[] sourceAddress, int sourcePort,
byte[] destinationAddress, int destinationPort,
TransportName transport,
boolean sender,
byte[] packetContent)
{
this.logPacket(protocol, sourceAddress, sourcePort,
destinationAddress, destinationPort,
transport, sender,
packetContent, 0, packetContent.length);
}
/**
* Log a packet with all the required information.
*
* @param protocol the source protocol that logs this packet.
* @param sourceAddress the source address of the packet.
* @param sourcePort the source port of the packet.
* @param destinationAddress the destination address.
* @param destinationPort the destination port.
* @param transport the transport this packet uses.
* @param sender are we the sender of the packet or not.
* @param packetContent the packet content.
* @param packetOffset the packet content offset.
* @param packetLength the packet content length.
*/
public void logPacket(
ProtocolName protocol,
byte[] sourceAddress,
int sourcePort,
byte[] destinationAddress,
int destinationPort,
TransportName transport,
boolean sender,
byte[] packetContent,
int packetOffset,
int packetLength)
{
saverThread.queuePacket(
new Packet(protocol,
sourceAddress,
sourcePort,
destinationAddress,
destinationPort,
transport,
sender,
packetContent,
packetOffset,
packetLength));
}
/**
* Returns the current Packet Logging Configuration.
*
* @return the Packet Logging Configuration.
*/
public PacketLoggingConfiguration getConfiguration()
{
if(packetLoggingConfiguration == null)
packetLoggingConfiguration = new PacketLoggingConfigurationImpl();
return packetLoggingConfiguration;
}
/**
* Dump the packet to the output file stream.
*
* @param packet the packet ot save.
* @throws Exception when error occurs saving to file stream or when
* rotating files.
*/
private void savePacket(Packet packet)
throws Exception
{
// if one of the addresses is ipv4 we are using ipv4,
// local udp addresses come as 0.0.0.0.0....0.0.0 when
// ipv6 is enabled in the underlying os
boolean isIPv4 = packet.sourceAddress.length == 4
|| packet.destinationAddress.length == 4;
byte[] ipHeader;
if(isIPv4)
{
ipHeader = new byte[ipHeaderTemplate.length];
System.arraycopy(
ipHeaderTemplate, 0, ipHeader, 0, ipHeader.length);
System.arraycopy(packet.sourceAddress,
0,
ipHeader,
12,
4);
System.arraycopy(packet.destinationAddress,
0,
ipHeader,
16,
4);
}
else
{
ipHeader = new byte[ip6HeaderTemplate.length];
System.arraycopy(
ip6HeaderTemplate, 0, ipHeader, 0, ipHeader.length);
System.arraycopy(packet.sourceAddress,
0,
ipHeader,
8,
16);
System.arraycopy(packet.destinationAddress,
0,
ipHeader,
24,
16);
}
byte[] transportHeader;
short len;
if(packet.transport == TransportName.UDP)
{
byte[] udpHeader = new byte[udpHeaderTemplate.length];
transportHeader = udpHeader;
System.arraycopy(udpHeaderTemplate, 0,
udpHeader, 0, udpHeader.length);
writeShort(packet.sourcePort, udpHeader, 0);
writeShort(packet.destinationPort, udpHeader, 2);
len = (short)(packet.packetLength + udpHeader.length);
writeShort(len, udpHeader, 4);
}
else
{
transportHeader = new byte[tcpHeaderTemplate.length];
System.arraycopy(tcpHeaderTemplate, 0, transportHeader,
0, transportHeader.length);
writeShort(packet.sourcePort, transportHeader, 0);
writeShort(packet.destinationPort, transportHeader, 2);
len = (short)(packet.packetLength + transportHeader.length);
if(packet.sender)
{
long seqnum;
long acknum;
synchronized(tcpCounterLock)
{
seqnum = srcCount;
srcCount += packet.packetLength;
acknum = dstCount;
}
intToBytes((int)(seqnum & 0xffffffff),
transportHeader, 4);
intToBytes((int)(acknum & 0xffffffff),
transportHeader, 8);
}
else
{
long seqnum;
long acknum;
synchronized(tcpCounterLock)
{
seqnum = dstCount;
dstCount += packet.packetLength;
acknum = srcCount;
}
intToBytes((int)(seqnum & 0xffffffff),
transportHeader, 4);
intToBytes((int)(acknum & 0xffffffff),
transportHeader, 8);
}
}
// now set ip header total length
if(isIPv4)
{
short ipTotalLen = (short)(len + ipHeader.length);
writeShort(ipTotalLen, ipHeader, 2);
if(packet.transport == TransportName.UDP)
ipHeader[9] = (byte)0x11;
else
ipHeader[9] = (byte)0x06;
int chk2 = computeChecksum(ipHeader);
ipHeader[10] = (byte) (chk2 >> 8);
ipHeader[11] = (byte) (chk2 & 0xff);
}
else
{
writeShort(len, ipHeader, 4);
if(packet.transport == TransportName.UDP)
ipHeader[6] = (byte)0x11;
else
ipHeader[6] = (byte)0x06;
}
long current = System.currentTimeMillis();
int tsSec = (int)(current/1000);
int tsUsec = (int)((current%1000) * 1000);
int feakHeaderLen = fakeEthernetHeader.length +
(isIPv4 ? ipv4EtherType : ipv6EtherType).length +
ipHeader.length + transportHeader.length;
int inclLen = packet.packetLength + feakHeaderLen;
int origLen = inclLen;
synchronized(this)
{
// open files only if needed
if(outputStream == null)
{
getFileNames();
rotateFiles();// this one opens the file for write
}
long limit = getConfiguration().getLimit();
if((limit > 0) && (written > limit))
rotateFiles();
addInt(tsSec);
addInt(tsUsec);
addInt(inclLen);
addInt(origLen);
outputStream.write(fakeEthernetHeader);
outputStream.write(isIPv4 ? ipv4EtherType : ipv6EtherType);
outputStream.write(ipHeader);
outputStream.write(transportHeader);
outputStream.write(
packet.packetContent,
packet.packetOffset,
packet.packetLength);
outputStream.flush();
written += inclLen + 16;
}
}
/**
* Writes int to the file. Used for packet headers.
* @param d the value to write.
* @throws IOException
*/
private void addInt(int d)
throws IOException
{
outputStream.write ((d & 0xff));
outputStream.write(((d & 0xff00) >> 8));
outputStream.write(((d & 0xff0000) >> 16));
outputStream.write(((d & 0xff000000) >> 24));
}
/**
* Converts a 32-bit word representation of an IPv4 address to a
* byte array.
*
* @param address The 32-bit word representation of the IPv4 address.
* @param data The byte array in which to store the IPv4 data.
* @param offset The offset into the array where the data start.
*/
private static final void intToBytes(int address, byte[] data,
int offset)
{
data[offset] = (byte)(0xff & (address >>> 24));
data[offset + 1] = (byte)(0xff & (address >>> 16));
data[offset + 2] = (byte)(0xff & (address >>> 8));
data[offset + 3] = (byte)(0xff & address);
}
/**
* Puts the short value to the array.
* @param value value to convert to bytes.
* @param data destination data
* @param offset offset in the data
*/
private static void writeShort(int value, byte[] data, int offset)
{
data[offset] = (byte) (value >> 8);
data[offset + 1] = (byte) value;
}
/**
* Calculates checksums assuming the checksum is a 16-bit header field.
*/
private int computeChecksum(byte[] data)
{
int total = 0;
int i = 0;
// Don't Skip existing checksum cause its set to 0000
int imax = data.length - (data.length % 2);
while(i < imax)
total+=(((data[i++] & 0xff) << 8) | (data[i++] & 0xff));
if(i < data.length)
total+=((data[i] & 0xff) << 8);
// Fold to 16 bits
while((total & 0xffff0000) != 0)
total = (total & 0xffff) + (total >>> 16);
total = (~total & 0xffff);
return total;
}
/**
* The data we receive and that we will dump in a file.
*/
private static class Packet
{
/**
* The protocol logging this packet.
*/
ProtocolName protocol;
/**
* The source address of the packet.
*/
byte[] sourceAddress;
/**
* The source port of the packet.
*/
int sourcePort;
/**
* The destination address of the packet.
*/
byte[] destinationAddress;
/**
* The destination port of the packet.
*/
int destinationPort;
/**
* Is the packet a udp one.
*/
TransportName transport;
/**
* Are we sending the packet, or false if we are receiving.
*/
boolean sender;
/**
* Array containing packet content.
*/
byte[] packetContent;
/**
* The offset in the packetContent where packet content is.
*/
int packetOffset;
/**
* The length of the packet content.
*/
int packetLength;
/**
* Creates a packet with the needed data.
* @param protocol the source protocol that logs this packet.
* @param sourceAddress The source address of the packet.
* @param sourcePort The source port of the packet.
* @param destinationAddress The destination address of the packet.
* @param destinationPort The destination port of the packet.
* @param transport the transport this packet uses.
* @param sender Are we sending the packet,
* or false if we are receiving.
* @param packetContent Array containing packet content.
* @param packetOffset The offset in the packetContent
* where packet content is.
* @param packetLength The length of the packet content.
*/
private Packet(ProtocolName protocol,
byte[] sourceAddress,
int sourcePort,
byte[] destinationAddress,
int destinationPort,
TransportName transport,
boolean sender,
byte[] packetContent,
int packetOffset,
int packetLength)
{
this.protocol = protocol;
this.sourceAddress = sourceAddress;
this.sourcePort = sourcePort;
this.destinationAddress = destinationAddress;
this.destinationPort = destinationPort;
this.transport = transport;
this.sender = sender;
this.packetContent = packetContent;
this.packetOffset = packetOffset;
this.packetLength = packetLength;
}
}
/**
* Dumps packet in separate thread so we don't block
* our calling thread.
*/
private class SaverThread
extends Thread
{
/**
* start/stop indicator.
*/
private boolean stopped = true;
/**
* List of packets queued to be written in the file.
*/
private final EvictingQueue<Packet> pktsToSave
= EvictingQueue.create(EVICTING_QUEUE_MAX_SIZE);
/**
* Initializes a new <tt>SaverThread</tt>.
*/
SaverThread()
{
setName(PacketLoggingServiceImpl.class.getName() + " SaverThread");
}
/**
* Sends instant messages in separate thread so we don't block
* our calling thread.
*/
@Override
public void run()
{
stopped = false;
while(!stopped)
{
Packet pktToSave;
synchronized(this)
{
if(pktsToSave.isEmpty())
{
try
{
wait();
}
catch (InterruptedException iex)
{
}
continue;
}
pktToSave = pktsToSave.poll();
}
if(pktToSave != null)
{
try
{
savePacket(pktToSave);
}
catch(Throwable t)
{
/*
* XXX ThreadDeath must be rethrown; otherwise, the
* related Thread will not die.
*/
if (t instanceof ThreadDeath)
throw (ThreadDeath) t;
else
logger.error("Error writing packet to file", t);
}
}
}
}
/**
* Interrupts this sender so that it would no longer send messages.
*/
public synchronized void stopRunning()
{
stopped = true;
notifyAll();
}
/**
* Schedule new packet for save.
* @param packet new packet to save.
*/
public synchronized void queuePacket(Packet packet)
{
if (EVICTING_QUEUE_MAX_SIZE - pktsToSave.size() == 0)
{
logger.warn("Queue is full, packets are being evicted.");
}
pktsToSave.add(packet);
notifyAll();
}
}
}