Packet capturing in Java with Pcap4j

Advertisement

Advertisement

Overview

This tutorial will cover packet capturing in the Java programming language using the pcap4j library.

Other packet capturing tutorials

I have a few other packet capturing tutorials in other languages as well. If you want to learn how to use the underlying pcap library in C, refer to Using libpcap in C. If you have an interest in Go, you can read Packet Capture, Injection, and Analysis with Gopacket or watch my talk from GopherCon 2016: Packet Capture, Analysis, and Injection with Go on YouTube.

Prerequisites

Java JDK 1.8

This example was tested with Java JDK 8. For instructions on installing the JDK on Windows, refer to my tutorial on Installing JDK 1.8 and 9 in Windows.

On Debian and Ubuntu based distributions, you can instal JDK 8 easily with:

sudo apt install openjdk-8-jdk

On Mac and other platforms, I recommend using the Oracle JDK installer from Oracle's JDK download page.

WinPcap or libpcap

You need a packet capturing library installed. On Linux and Mac, libpcap is typically included by default, and if not, most distributions provide a package for easy installation. Absolute worst case, you will need to build it yourself from source from http://www.tcpdump.org. If using Windows, you need to install WinPcap which provides a simple installer.

Pcap4j library

We will use the pcap4j Java library to hook in to the native libpcap or WinPcap. In this tutorial we will use Maven and the Maven Central Repository to take care of our Java dependencies, so you don't have to worry about downloading anything manually. Maven will take care of everything. If you are unfamiliar with Maven or don't have it installed yet, I recommend you first read my tutorial on Maven Basics for Java Developers.

If you prefer to download the JAR and manage the dependencies yourself, you can download the latest JAR from the pcap4j GitHub release page.

Setting up the project with maven

Generate project structure using maven-archetype-quickstart

I will use Maven to generate a skeleton project that is ready to go. Generate a quickstart project with the following command:

# This is all one line
mvn archetype:generate -DgroupId=com.github.username -DartifactId=pcap -Dversion=1.0.0 -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

This will create a pcap directory with a pom.xml file and a src directory.

Add the pcap4j dependency in pom.xml

Inside the created directory, edit the pom.xml file and add a new dependency entry for pcap4j. It should look similar to this:

<!-- pom.xml -->
<project>
    ...
    <dependencies>
        ...
        <dependency>
            <groupId>org.pcap4j</groupId>
            <artifactId>pcap4j-core</artifactId>
            <version>1.7.3</version>
            <type>jar</type>
        </dependency>
        ...
    </dependencies>
    ...
</project>

Now we are ready to start writing source code. Inside the src/main/java directory, drill down the directories until you get to the App.java file. We can start editing in that file. It should already look something like this:

// App.java
package com.github.username;

public class App
{
    public static void main(String[] args)
    {
        System.out.println("Hello World!");
    }
}

Listing networking devices

Let's modify the App.java file a bit and put some code in the main() function that will list the network interfaces and ask the user to choose one. Update the code to look more like this:

// App.java
package com.github.username;

import java.io.IOException;
import org.pcap4j.core.PcapNetworkInterface;
import org.pcap4j.util.NifSelector;

public class App
{
    public static void main(String[] args)
    {
        // The class that will store the network device
        // we want to use for capturing.
        PcapNetworkInterface device = null;

        // Pcap4j comes with a convenient method for listing
        // and choosing a network interface from the terminal
        try {
            // List the network devices available with a prompt
            device = new NifSelector().selectNetworkInterface();
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("You chose: " + device);
    }
}

Now that we have some code we actually want to run and test, let's set up Maven to package our app in to an executable JAR file with dependencies that is ready for us to run.

Packaging and running the application

To make packaging and running the application easier, we should package the application as a JAR with all the dependencies embedded. Modify the pom.xml in the root directory of the project and add a section for build and add the following plugin configurations to your existing pom.xml. While we're at it, let's tell the compiler plugin to target Java version 1.8.

<!-- pom.xml -->
<project>
    ...
    <build>
        <plugins>
            <!-- Specify to the compiler we want Java 1.8 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>

            <!-- Tell the JAR plugin which class is the main class -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.github.username.App</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

            <!-- Embed dependencies inside the final JAR -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.1.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <finalName>uber-${project.artifactId}-${project.version}</finalName>
                </configuration>
            </plugin>
        </plugins>
    </build>
    ...
</project>

After adding those plugins, we can navigate to the root project directory with the pom.xml and run:

# Generate the uber jar in the target directory
mvn package

# Launch the jar
java -jar target/uber-pcap-1.0.0.jar

# You might need to elevate your privileges with sudo
sudo java -jar target/uber-pcap-1.0.0.jar

When you run the example it will print out all available devices and you must enter a number to select a device

Some minor refactoring

Before we move on and make this program more complex, let's break out the code from the main() method in to a separate method like this

// App.java
package com.github.username;

import java.io.IOException;
import org.pcap4j.core.PcapNetworkInterface;
import org.pcap4j.util.NifSelector;

public class App {

    static PcapNetworkInterface getNetworkDevice() {
        PcapNetworkInterface device = null;
        try {
            device = new NifSelector().selectNetworkInterface();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return device;
    }

    public static void main(String[] args) {
        PcapNetworkInterface device = getNetworkDevice();
        System.out.println("You chose: " + device);
    }
}

Getting the device list yourself

If you want to get the list of devices yourself, instead of using the selector tool provided, call the native Pcaps.findallDevs() method. Refer to this code snippet taken from the NifSelector class.

// Instead of using NifSelector, you can get the
// list of network devices from the system

List<PcapNetworkInterface> allDevs = null;

try {
    allDevs = Pcaps.findAllDevs();
} catch (PcapNativeException e) {
    throw new IOException(e.getMessage());
}

if (allDevs == null || allDevs.isEmpty()) {
    throw new IOException("No NIF to capture.");
}

Getting a handle and capturing live packets

Once you have selected a device, you can create call openLive() to create a handle that can be used to listen for packets. In this next code snippet, we will modify the main() function to actually open the device that was chosen.

We will do a few things here, first we will open the device with openLive() and then we will create a PacketListener that defines how to handle the packets when they are received. The last step is to tell the open device to loop and process packets using the listener we defined. The listener we create that processes the packets simply prints out the packet information. You will need to add a few extra imports for this code. Also note the additional throws on the main function, done to simplify the example.

// App.java
package com.github.username;

import java.io.IOException;

import org.pcap4j.core.NotOpenException;
import org.pcap4j.core.PacketListener;
import org.pcap4j.core.PcapHandle;
import org.pcap4j.core.PcapNativeException;
import org.pcap4j.core.PcapNetworkInterface;
import org.pcap4j.core.PcapNetworkInterface.PromiscuousMode;
import org.pcap4j.packet.Packet;
import org.pcap4j.util.NifSelector;

public class App {

    static PcapNetworkInterface getNetworkDevice() {
        PcapNetworkInterface device = null;
        try {
            device = new NifSelector().selectNetworkInterface();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return device;
    }

    public static void main(String[] args) throws PcapNativeException, NotOpenException {
        // The code we had before
        PcapNetworkInterface device = getNetworkDevice();
        System.out.println("You chose: " + device);

        // New code below here
        if (device == null) {
            System.out.println("No device chosen.");
            System.exit(1);
        }

        // Open the device and get a handle
        int snapshotLength = 65536; // in bytes  
        int readTimeout = 50; // in milliseconds                  
        final PcapHandle handle;
        handle = device.openLive(snapshotLength, PromiscuousMode.PROMISCUOUS, readTimeout);

        // Create a listener that defines what to do with the received packets
        PacketListener listener = new PacketListener() {
            @Override
            public void gotPacket(Packet packet) {
                // Override the default gotPacket() function and process packet
                System.out.println(handle.getTimestamp());
                System.out.println(packet);
            }
        };

        // Tell the handle to loop using the listener we created
        try {
            int maxPackets = 50;
            handle.loop(maxPackets, listener);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Cleanup when complete
        handle.close();
    }

}

Capturing with BPF filter

You can implement filters using the Berkeley Packet Filter (BPF) syntax. Read more about BPF at https://en.wikipedia.org/wiki/Berkeley_Packet_Filter. After opening a device (or a pcap file, covered later) and getting a handle, you can set the filter on the handle using setFilter(). Be sure to set the filter after the handle is created, but before you begin capturing. The setFilter() method takes care of compiling and setting the filter in one step, something that is normally two separate steps in C.

// import org.pcap4j.core.BpfProgram.BpfCompileMode;

// Attach a filter to the handle after creating it
final PcapHandle handle = device.openLive(SNAPLEN, PromiscuousMode.PROMISCUOUS, READ_TIMEOUT);

// Set a filter to only listen for tcp packets on port 80 (HTTP)
String filter = "tcp port 80";
handle.setFilter(filter, BpfCompileMode.OPTIMIZE);

// Continue with rest of code

Looking deeper at the packet classes

Now that we know how to capture packets and process them, it's worth taking a moment to look at the AbstractPacket class that provides the base functionality for all packet types including the TcpPacket class.

Refer to the official documentation of the AbstractPacket class at https://kaitoy.github.io/pcap4j/javadoc/latest/en/org/pcap4j/packet/AbstractPacket.html, but I will point out a few noteworthy methods.

// If we had any kind of packet object, we could use:
packet.getHeader()
packet.getPayload()
packet.length()
packet.getRawData()
packet.toHexString()

The above reference is for a generic packet object. There is also a more specific TcpPacket as well as many other types of packets.

Writing a pcap file

To write a pcap file, you must create a PcapDumper object, obtained from a handle by calling handle.dumpOpen(fileName).

// import org.pcap4j.core.PcapDumper;
PcapDumper dumper = handle.dumpOpen("dump.pcap");

// Write packets as needed
try {
    dumper.dump(packet, handle.getTimestamp());
} catch (NotOpenException e) {
    e.printStackTrace();
}

// Be sure to close it when done
dumper.close();

Reading a pcap file

Reading a pcap file is very similar to reading from a regular device. The only difference is that you call a static method, Pcaps.openOffline(), instead of handle.openLive(). Once you have the handle, you treat it exactly the same way as a handle to a live device.

// import org.pcap4j.core.Pcaps;

PcapHandle handle;
try {
    handle = Pcaps.openOffline("dump.pcap", TimestampPrecision.NANO);
} catch (PcapNativeException e) {
    handle = Pcaps.openOffline("dump.pcap");
}

Get stats about a handle

Using a handle that we already have, we can get statistics about how many packets were captured and dropped using the PcapStat class obtained from handle.getStats().

// import org.pcap4j.core.PcapStat;
// import com.sun.jna.Platform;

PcapStat stats = handle.getStats();
System.out.println("Packets received: " + stats.getNumPacketsReceived());
System.out.println("Packets dropped: " + stats.getNumPacketsDropped());
System.out.println("Packets dropped by interface: " + stats.getNumPacketsDroppedByIf());
// Supported by WinPcap only
if (Platform.isWindows()) {
    System.out.println("Packets captured: " + stats.getNumPacketsCaptured());
}

Full code example

This example contains all of the code above, except for the example of reading a pcap file. It will open a live device, dump some packets using a filter to listen on TCP port 80, write the packets to a file, and print out statistics at the end.

// App.java
package com.github.username;

import java.io.IOException;

import com.sun.jna.Platform;

import org.pcap4j.core.NotOpenException;
import org.pcap4j.core.PacketListener;
import org.pcap4j.core.PcapDumper;
import org.pcap4j.core.PcapHandle;
import org.pcap4j.core.PcapNativeException;
import org.pcap4j.core.PcapNetworkInterface;
import org.pcap4j.core.PcapStat;
import org.pcap4j.core.BpfProgram.BpfCompileMode;
import org.pcap4j.core.PcapNetworkInterface.PromiscuousMode;
import org.pcap4j.packet.Packet;
import org.pcap4j.util.NifSelector;

public class App {

    static PcapNetworkInterface getNetworkDevice() {
        PcapNetworkInterface device = null;
        try {
            device = new NifSelector().selectNetworkInterface();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return device;
    }

    public static void main(String[] args) throws PcapNativeException, NotOpenException {
        // The code we had before
        PcapNetworkInterface device = getNetworkDevice();
        System.out.println("You chose: " + device);

        // New code below here
        if (device == null) {
            System.out.println("No device chosen.");
            System.exit(1);
        }

        // Open the device and get a handle
        int snapshotLength = 65536; // in bytes  
        int readTimeout = 50; // in milliseconds                  
        final PcapHandle handle;
        handle = device.openLive(snapshotLength, PromiscuousMode.PROMISCUOUS, readTimeout);
        PcapDumper dumper = handle.dumpOpen("out.pcap");

        // Set a filter to only listen for tcp packets on port 80 (HTTP)
        String filter = "tcp port 80";
        handle.setFilter(filter, BpfCompileMode.OPTIMIZE);

        // Create a listener that defines what to do with the received packets
        PacketListener listener = new PacketListener() {
            @Override
            public void gotPacket(Packet packet) {
                // Print packet information to screen
                System.out.println(handle.getTimestamp());
                System.out.println(packet);

                // Dump packets to file
                try {
                    dumper.dump(packet, handle.getTimestamp());
                } catch (NotOpenException e) {
                    e.printStackTrace();
                }
            }
        };

        // Tell the handle to loop using the listener we created
        try {
            int maxPackets = 50;
            handle.loop(maxPackets, listener);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Print out handle statistics
        PcapStat stats = handle.getStats();
        System.out.println("Packets received: " + stats.getNumPacketsReceived());
        System.out.println("Packets dropped: " + stats.getNumPacketsDropped());
        System.out.println("Packets dropped by interface: " + stats.getNumPacketsDroppedByIf());
        // Supported by WinPcap only
        if (Platform.isWindows()) {
            System.out.println("Packets captured: " +stats.getNumPacketsCaptured());
        }

        // Cleanup when complete
        dumper.close();
        handle.close();
    }
}

Further reading

Advertisement

Advertisement