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
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);
}
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.
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);
}
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);
}
Attach the BPF probes
Call AddProbe() to attach all UDP monitoring probes: 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
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
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:
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
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);
Call cleanup function
The cleanup function detaches all kprobes: This detaches probes from:
ip6_datagram_connect
ip4_datagram_connect
udp_recvmsg
udp_sendmsg
udp_destruct_sock
udpv6_sendmsg
udpv6_recvmsg
kretprobe__udpv6_recvmsg
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:
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