Skip to main content
The UDP Event Interceptor captures UDP socket activity using eBPF kernel probes. It tracks UDP send and receive operations, recording network statistics including bytes transferred, packet counts, and connection metadata for both IPv4 and IPv6.

Overview

The UDP tracer attaches to multiple kernel functions to track UDP activity:
  • udp_sendmsg / udpv6_sendmsg - Capture outbound UDP traffic
  • udp_recvmsg / udpv6_recvmsg - Capture inbound UDP traffic
  • ip4_datagram_connect / ip6_datagram_connect - Track socket connections
  • udp_destruct_sock - Capture socket destruction events

Event Structure

Each UDP event contains the following fields:
struct udp_event_t {
    uint16_t family;           // Address family (AF_INET or AF_INET6)
    uint32_t pid;              // Process ID
    uint32_t UserId;           // User ID
    uint64_t EventTime;        // Event timestamp (nanoseconds)
    uint16_t SPT;              // Source port
    uint16_t DPT;              // Destination port
    char task[16];             // Process/command name
    uint64_t rx_b;             // Bytes received
    uint64_t tx_b;             // Bytes transmitted
    uint32_t rxPkts;           // Packets received count
    uint32_t txPkts;           // Packets transmitted count
    char SADDR[64];            // Source IP address (string)
    char DADDR[64];            // Destination IP address (string)
};

Initializing the UDP Tracer

1

Load the shared library

Load the UDP event interceptor library:
#include <dlfcn.h>

#define SOFILE "/opt/RealTimeKql/lib/libudpEvent.so"

void *handle = dlopen(SOFILE, RTLD_LAZY);
if (!handle) {
    fprintf(stderr, "Failed to load library: %s\n", dlerror());
    exit(EXIT_FAILURE);
}
2

Resolve the AddProbe function

Get the AddProbe function:
dlerror(); // Clear errors
void (*AddProbe)() = dlsym(handle, "AddProbe");

char *err = dlerror();
if (err) {
    fprintf(stderr, "Failed to resolve AddProbe: %s\n", err);
    exit(EXIT_FAILURE);
}
Unlike TCP monitoring, UDP’s AddProbe() takes no arguments - the BPF program is embedded in the library.
3

Resolve the DequeuePerfEvent function

Get the event dequeue function:
dlerror();
struct udp_event_t (*DequeuePerfEvent)() = dlsym(handle, "DequeuePerfEvent");

err = dlerror();
if (err) {
    fprintf(stderr, "Failed to resolve DequeuePerfEvent: %s\n", err);
    exit(EXIT_FAILURE);
}
4

Resolve additional functions

Get status checking and cleanup functions:
dlerror();
unsigned (*getStatus)() = dlsym(handle, "getStatus");
err = dlerror();
if (err) {
    fprintf(stderr, "Failed to resolve getStatus: %s\n", err);
    exit(EXIT_FAILURE);
}

dlerror();
void (*cleanup)() = dlsym(handle, "cleanup");
err = dlerror();
if (err) {
    fprintf(stderr, "Failed to resolve cleanup: %s\n", err);
    exit(EXIT_FAILURE);
}
5

Attach the BPF probes

Call AddProbe() to attach all UDP monitoring probes:
AddProbe();
This internally attaches kprobes to:
  • ip6_datagram_connect
  • ip4_datagram_connect
  • udp_recvmsg (entry and return)
  • udp_sendmsg
  • udp_destruct_sock
  • udpv6_recvmsg (entry and return)
  • udpv6_sendmsg
6

Wait for initialization

Wait for all probes to attach:
while (!getStatus()) {
    puts("Waiting for tracer initialization...");
    sleep(1);
}

Complete Monitoring Example

Here’s a complete example based on the test implementation:
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <signal.h>
#include <unistd.h>
#include "common.h"

#define SOFILE "/opt/RealTimeKql/lib/libudpEvent.so"

void (*cleanup)();
void *handle = NULL;

void signalHandler(int signum) {
    printf("Interrupted by signal %u\n", signum);
    cleanup();
    if (handle && dlclose(handle)) {
        puts("Error closing handle, but continuing shutdown");
    }
    exit(signum);
}

void printEvent(struct udp_event_t *event) {
    if (!event) return;
    
    printf("---\n");
    printf("PID: %d\n", event->pid);
    printf("UID: %d\n", event->UserId);
    printf("Family: %d\n", event->family);
    printf("Bytes received: %lu\n", event->rx_b);
    printf("Bytes sent: %lu\n", event->tx_b);
    printf("Packets received: %u\n", event->rxPkts);
    printf("Packets sent: %u\n", event->txPkts);
    printf("Command: %s\n", event->task);
    printf("Source: %s:%d\n", event->SADDR, event->SPT);
    printf("Destination: %s:%d\n", event->DADDR, event->DPT);
    printf("Event time: %ld\n", event->EventTime);
    printf("---\n");
}

int main() {
    printf("UDP Event Monitor PID: %d\n", getpid());
    
    // Load library
    handle = dlopen(SOFILE, RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "Failed to load library\n");
        exit(EXIT_FAILURE);
    }
    
    // Resolve symbols
    void (*AddProbe)() = dlsym(handle, "AddProbe");
    struct udp_event_t (*DequeuePerfEvent)() = dlsym(handle, "DequeuePerfEvent");
    cleanup = dlsym(handle, "cleanup");
    unsigned (*getStatus)() = dlsym(handle, "getStatus");
    
    // Check for errors (simplified)
    if (!AddProbe || !DequeuePerfEvent || !cleanup || !getStatus) {
        fprintf(stderr, "Failed to resolve symbols\n");
        exit(EXIT_FAILURE);
    }
    
    // Setup signal handler
    signal(SIGINT, signalHandler);
    
    // Attach probes
    puts("Attaching UDP probes...");
    AddProbe();
    puts("Probes attached!");
    
    // Wait for initialization
    while (!getStatus()) {
        puts("Waiting on getStatus()...");
        sleep(1);
    }
    
    // Main event loop
    struct udp_event_t event;
    while (1) {
        event = DequeuePerfEvent();
        printEvent(&event);
    }
    
    return 0;
}

IPv4 vs IPv6 Handling

The UDP tracer seamlessly handles both IPv4 and IPv6 traffic through separate probe points:

IPv4 Probes

// Tracks IPv4 UDP connections
int kprobe_ip4_datagram_connect(struct pt_regs *ctx, struct sock *sk)

// Captures IPv4 UDP sends
int kprobe__udp_sendmsg(struct pt_regs *ctx, struct sock *sk, 
                        struct msghdr *msg, size_t len)

// Captures IPv4 UDP receives
int kprobe_udp_recvmsg(struct pt_regs *ctx, struct sock *sk)
int kretprobe__udp_recvmsg(struct pt_regs *ctx)

IPv6 Probes

// Tracks IPv6 UDP connections
int kprobe_ip6_datagram_connect(struct pt_regs *ctx, struct sock *sk)

// Captures IPv6 UDP sends
int kprobe__udpv6_sendmsg(struct pt_regs *ctx, struct sock *sk, 
                          struct msghdr *msg, size_t len)

// Captures IPv6 UDP receives
int kprobe__udpv6_recvmsg(struct pt_regs *ctx, struct sock *sk)
int kretprobe__udpv6_recvmsg(struct pt_regs *ctx)
The family field in the event structure indicates whether the traffic is IPv4 (AF_INET = 2) or IPv6 (AF_INET6 = 10).

Packet vs Byte Counting

The UDP tracer provides both packet counts and byte counts:

Byte Counting

  • rx_b: Total bytes received across all UDP packets
  • tx_b: Total bytes sent across all UDP packets
Bytes are accumulated from the actual payload size reported by the kernel:
// From kretprobe__udp_recvmsg
int ret = PT_REGS_RC(ctx);  // Return value = bytes received
if (ret > 0) {
    eventPtr->rx_b += ret;
    eventPtr->rxPkts += 1;
}

Packet Counting

  • rxPkts: Number of UDP packets received
  • txPkts: Number of UDP packets sent
Packets are counted each time send/receive operations complete:
// From kprobe__udp_sendmsg
eventPtr->tx_b += len;      // Add bytes
eventPtr->txPkts += 1;      // Increment packet count
Byte counts represent application-level payload data, not including UDP/IP headers.

Understanding Event Aggregation

UDP events are aggregated per socket. The tracer maintains state using a BPF hash map:
BPF_HASH(otherHash, uintptr_t, struct event_t);
Events are updated as traffic flows and emitted at various trigger points:
  • When udp_sendmsg is called
  • When udp_recvmsg completes
  • When the socket is destroyed (udp_destruct_sock)
For long-lived UDP sockets, you may receive multiple events as traffic accumulates. Each event represents a snapshot of cumulative statistics for that socket.

Interpreting Event Data

Process Information

  • pid: Process ID that owns the socket
  • UserId: User ID of the process (from bpf_get_current_uid_gid())
  • task: Process name (up to 16 characters)

Connection Endpoints

  • SADDR/SPT: Source IP address and port
  • DADDR/DPT: Destination IP address and port
  • family: Address family (2 = IPv4, 10 = IPv6)
For unconnected UDP sockets, address information may only be available after the first send or receive operation.

Timestamps

  • EventTime: Nanosecond timestamp adjusted for system boot time
The timestamp provides absolute wall-clock time:
// Adjustment for boot time
toConsumer.EventTime = event->EventTime + notSoLongAgo;

Example Output

When running the UDP tracer, you’ll see output like this:
---
PID: 1180210
UID: 1000
Family: 10
Bytes received: 0
Bytes sent: 32
Packets received: 0
Packets sent: 1
Command: udpTraffic.sh
Source: 2001:xxx:f0:5e:aaa:a627:f45f:9c0c:42486
Destination: 2001:xxx:f0:5e:bbb:8d6f:32ef:6180:53
Event time: 1628185427077225859
---
This shows a DNS query (destination port 53) sent over IPv6.

Event Queue Management

The tracer maintains an internal event queue with a maximum size:
#define MAXQSIZE 1024
If events are not dequeued fast enough, the tracer will shed oldest events:
if (eventDeque.size() > MAXQSIZE) {
    eventDeque.pop_front();  // Drop oldest event
    puts("Shedding UDP events..");
}
Process events promptly in your main loop to avoid event loss during high-traffic periods.

Cleanup and Shutdown

1

Setup signal handler

Register handlers for graceful shutdown:
void signalHandler(int signum) {
    printf("Caught signal %d\n", signum);
    cleanup();
    if (handle && dlclose(handle)) {
        puts("Error closing handle");
    }
    exit(signum);
}

signal(SIGINT, signalHandler);
2

Call cleanup function

The cleanup function detaches all kprobes:
cleanup();
This detaches probes from:
  • ip6_datagram_connect
  • ip4_datagram_connect
  • udp_recvmsg
  • udp_sendmsg
  • udp_destruct_sock
  • udpv6_sendmsg
  • udpv6_recvmsg
  • kretprobe__udpv6_recvmsg
3

Close library handle

Close the dynamic library:
if (dlclose(handle)) {
    fprintf(stderr, "Error closing library\n");
}

Best Practices

High-Frequency Events

UDP can generate events at very high rates. Ensure your processing loop is efficient.

Event Aggregation

Events are per-socket aggregates. Don’t assume one event per packet.

Signal Handling

Always implement signal handlers to ensure proper cleanup.

Root Privileges

eBPF requires root or CAP_BPF capabilities to load probes.

Troubleshooting

Verify the library is installed:
ls -l /opt/RealTimeKql/lib/libudpEvent.so
If not found, reinstall or update the SOFILE path.
Run with elevated privileges:
sudo ./udpEventTest
  • Verify probes attached successfully (check console output)
  • Generate UDP traffic: dig example.com or ping6 ipv6.google.com
  • Check kernel logs: sudo dmesg | tail
If you see “Shedding UDP events” messages:
  • Process events faster in your main loop
  • Consider filtering events at the BPF level
  • Increase MAXQSIZE and recompile if needed
Ensure IPv6 is enabled:
sysctl net.ipv6.conf.all.disable_ipv6
Should return 0 (enabled).

Advanced Usage

Filtering Specific Ports

Modify the BPF program to filter events by port:
// Add filtering in kprobe__udp_sendmsg
if (eventPtr->DPT != 53) {  // Only DNS traffic
    return 0;
}

Custom Event Processing

Process events based on traffic patterns:
while (1) {
    struct udp_event_t event = DequeuePerfEvent();
    
    // Only process send-heavy sockets
    if (event.txPkts > 100) {
        analyzeTraffic(&event);
    }
    
    // Detect potential DNS tunneling
    if (event.DPT == 53 && event.tx_b > 512) {
        flagSuspiciousActivity(&event);
    }
}

Next Steps

TCP Monitoring

Learn how to monitor TCP connections

Building from Source

Customize and build the interceptor

Testing

Run tests and verify functionality

API Reference

Detailed UDP API documentation