Hello eBPF: A Packet Logger in Pure Java using TC and XDP Hooks (13)

Welcome back to my series on ebpf. In the last post, I told you about writing eBPF applications in pure Java using my new Java compiler plugin. This week, we’re extending the XDP example from last week (and Hello eBPF: XDP-based Packet Filter (9)) to also capture outgoing packets using a Traffic Control (TC) classifier.

Before we go into the details, first, the demo of the PacketLogger:

The logger captures the incoming and outgoing IP packets with their IP address, their protocol (TCP, UDP, OTHER), the TCP/UDP port, and the packet length. But before I show you how I implemented all this in Java, here is a short introduction to the Linux network stack:

Linux Network Stack

When a packet comes over the internet into our system, it first enters via the network devices. Our XDP hook runs on the network device driver to check the incoming packets as early as possible. The packet is then passed onto the Linux network stack and is checked by the TC classifiers:

Foreshadowing for next week: For a firewall, we want to check the incoming packets as early as possible to reduce the load of incoming packets on the system, off-loading the checking from the CPU to the network device where possible. It’s not as important for outgoing packets as the CPU generates the packages.

For our example, we chose to log the incoming packets at the XDP and outgoing packets at the TC levels. We can’t use XDP on outgoing packets, because it is ingress only. To learn more about the differences between TC and XDP hooks, I recommend reading Hangbin Liu’s article tc/BPF and XDP/BPF.

TC can be used for many more:

TC can be used for a number of use cases, all of them having to do with the manipulation of traffic. TC is for example used to implement QoS (Quality of Service) allowing latency sensitive traffic like VoIP (Voice over IP) to be processed ahead of lets say web traffic. It can also drop packets to simulate packet-loss, add latency to simulate distant clients or apply bandwidth limitations for applications or users, to name a few.

TC allows an admin to filter traffic using a hierarchical model of qdiscs (Queuing DISCipline). A root qdisc is attached to a network interface with certain actions. This qdisc can also have child qdiscs which will be used over the root if their filter matches the traffic. This program type allows us to implement such a filter in eBPF.

ebPF-Docs

Now to it’s usage in Java:

TC and XDP Interface

The TC interface is similar to the XDP interface (as seen in Hello eBPF: Write your eBPF application in Pure Java (12)):

public interface TCHook {

    @BPFFunction(section = "tc")
    @NotUsableInJava
    default sk_action tcHandleIngress(Ptr<__sk_buff> packet) {
        return sk_action.SK_PASS;
    }

    @BPFFunction(section = "tc")
    @NotUsableInJava
    default sk_action tcHandleEgress(Ptr<__sk_buff> packet) {
        return sk_action.SK_PASS;
    }
}

Instances of sk_buff/__sk_buff (the latter is the stable version) are allocated when the packet first enters the network stack, both from the direction of the application and the direction of the network device:

@Type(
      noCCodeGeneration = true,
      cType = "struct __sk_buff"
  )
  @NotUsableInJava
  public static class __sk_buff extends Struct {
    public @Unsigned int len;

    public @Unsigned int pkt_type;

    public @Unsigned int mark;

    public @Unsigned int queue_mapping;

    public @Unsigned int protocol;

    public @Unsigned int vlan_present;
    
    // ...
}

The fields are populated during the packet’s passage through the network stack, so not all fields are accessible in every hook. In the TC hook, we can only access a subset:

And many more fields, more in ebpf-docs.

The most important fields for our use case are the data and data_end fields because they point to the same data as the equally named fields in the XDP context.

Now to the implementation:

Implementation in Java

We first define the stored packet info and the ring buffer:

@Type
enum PacketDirection implements Enum<PacketDirection> {
    INCOMING, OUTGOING
}

@Type
enum Protocol implements Enum<Protocol> {
    TCP, UDP, OTHER
}

@Type
static class PacketInfo {
    PacketDirection direction;
    Protocol protocol;
    IPAddress source;
    IPAddress destination;
    int port;
    int length;
}

@BPFMapDefinition(maxEntries = 4096 * 256)
BPFRingBuffer<PacketInfo> packetLog;

We then implement the XDPHook and TCHook interfaces:

@Override
public xdp_action xdpHandlePacket(Ptr<xdp_md> ctx) {
    handlePacket(PacketDirection.INCOMING,
            Ptr.voidPointer(ctx.val().data),
            Ptr.voidPointer(ctx.val().data_end));
    return xdp_action.XDP_PASS;
}

@Override
public sk_action tcHandleEgress(Ptr<__sk_buff> packet) {
    handlePacket(PacketDirection.OUTGOING,
            Ptr.voidPointer(packet.val().data),
            Ptr.voidPointer(packet.val().data_end));
    return sk_action.SK_PASS;
}

Both call the same handlePacket method, making the implementation far simpler. The method first allocates a PacketInfo instance on the stack parses the packet, collects all the info, and then stores the PacketInfo instance in the ring buffer:

@BPFFunction
@AlwaysInline
void handlePacket(PacketDirection direction, Ptr<?> start, Ptr<?> end) {
    PacketInfo info = new PacketInfo();
    info.direction = direction;
    if (parsePacket(start, end, Ptr.of(info))) {
        var ptr = packetLog.reserve();
        if (ptr != null) {
            ptr.set(info);
            packetLog.submit(ptr);
        }
    }
}

To attach our hooks to all non-internal/loopback interfaces and print the packet log, we just call the appropriate methods in the user-land:

public static void main(String[] args) throws InterruptedException {
    try (PacketLogger program = BPFProgram.load(PacketLogger.class)) {

        // set the ring buffer call back
        program.packetLog.setCallback((info) -> {
            if (info.direction == PacketDirection.INCOMING) {
                System.out.print("Incoming from " +
                        NetworkUtil.intToIpAddress(info.source.ipv4)
                                .getHostAddress());
            } else {
                System.out.print("Outgoing to   " +
                        NetworkUtil.intToIpAddress(info.destination.ipv4)
                                .getHostAddress());
            }
            System.out.printf(" protocol %s port %5d length %d%n",
                    info.protocol, info.port, info.length);
        });

        // attach the XDP and TC hooks
        program.xdpAttach();
        program.tcAttachEgress();

        // consume the packet log
        while (true) {
            program.consumeAndThrow();
            Thread.sleep(500);
        }
    }
}

I will spare you the code for parsing the packets themselves; if you’re interested, look at the code on GitHub. Parsing the packets, especially IPv6 packets, is more complicated than one might think, so the parsing code is a Java adaptation of the sample code from the xdp-project.

Conclusion

TC and XDP hooks allow us to monitor incoming and outgoing packets easily. With hello-ebpf, it is possible to write all this in pure Java, defining all data structures once and using them both in user and kernel land. Of course, all of this is a work in progress, and I had found a few bugs while implementing the largest program yet with my eBPF library. The resulting C code has around 150 lines of code (excluding blank lines).

But this is only the beginning. Join me next week when I extend the PacketLogger into a proper Firewall written purely in Java.

This article is part of my work in the SapMachine team at SAP, making profiling and debugging easier for everyone. Thanks to Dylan Reimerink for answering all my ebpf-related questions; the whole hello-ebpf project would have been much harder without your support.

Author

  • Johannes Bechberger

    Johannes Bechberger is a JVM developer working on profilers and their underlying technology in the SapMachine team at SAP. This includes improvements to async-profiler and its ecosystem, a website to view the different JFR event types, and improvements to the FirefoxProfiler, making it usable in the Java world. He started at SAP in 2022 after two years of research studies at the KIT in the field of Java security analyses. His work today is comprised of many open-source contributions and his blog, where he writes regularly on in-depth profiling and debugging topics, and of working on his JEP Candidate 435 to add a new profiling API to the OpenJDK.

    View all posts

New posts like these come out at least every two weeks, to get notified about new posts, follow me on Twitter, Mastodon, or LinkedIn, or join the newsletter: