Monitor VPN Connections with eBPF, networking devices and blue cables on a wooden desk with plant
eBPF Use Cases
William  

Trace VPN Tunnels With eBPF at Both Network Layers

Monitor VPN connections with eBPF at the tunnel interface and the physical interface, then send small metadata records to user space for correlation. I start with process, interface, socket, and peer signals instead of capturing every packet. That tells you whether a tunnel exists, where its outer endpoint is, and which inner flows cross it. eBPF gives you kernel visibility. It does not decrypt every packet or prove that a peer is healthy because a socket exists.

Last updated: 2026-07-30

How do you monitor VPN connections with eBPF?

I split the job into three paths:

  1. Find the VPN interface, process, socket, and control-plane state.
  2. Attach eBPF programs where the traffic is visible.
  3. Correlate kernel events in a user-space agent.

Start by identifying what the host actually runs. Do not guess from a service name. A WireGuard interface can exist with no long-lived VPN process, while OpenVPN normally has both a process and a tun device.

ip -br link
ss -uap
wg show all
ip xfrm state
ip xfrm policy

ip -br link shows the interfaces and their state. ss -uap shows UDP sockets and owning processes where the kernel exposes them. wg show all reports WireGuard's control-plane view. The ip xfrm commands show IPsec state and policy.

For OpenVPN, also check the process and tunnel device. For strongSwan, look for the charon process and the XFRM state. For WireGuard, do not treat wg-quick as the data path. It configures the interface and usually exits.

The eBPF program should collect metadata, not payloads. A useful event can contain the interface index, direction, protocol, addresses, ports, packet size, process identity where available, and a timestamp. The user-space agent can add peer names, policy labels, DNS names, and inventory data later.

That is how I monitor VPN connections with eBPF without turning every host into a packet-capture appliance.

Define what counts as a VPN connection

A process start is not an active VPN connection. A UDP socket is not a successful handshake. A packet on wg0 tells you that traffic crossed the tunnel, but it does not tell you whether every configured peer is reachable.

I define connection state from several signals:

  • OpenVPN: the process exists, the tun or tap device is present, and the control socket carries traffic.
  • WireGuard: the wg interface exists, peer state is configured, outer UDP traffic appears, and inner traffic crosses the tunnel.
  • IPsec: XFRM state and policy exist, then encrypted traffic matches the expected path.
  • All VPN types: the interface stays attached, routes remain valid, and traffic follows the expected direction.

This distinction matters during incidents. A VPN can look up from the service manager while its interface has no traffic. It can also pass traffic while the control plane is failing to refresh peer state.

I keep the control-plane checks in user space. I use eBPF for packet, socket, process, and interface events. Trying to make one kernel program understand every VPN's private state creates a brittle monitor that fails after a kernel update.

Pick the hook that sees the signal you need

There is no universal VPN hook. The right attach point depends on whether you need lifecycle, underlay, tunnel, or policy information.

SignalUseful attach pointWhat you learnMain limitation
VPN process start and exitTracepoints for execve and process exitPID, command, lifecycle changesA running process does not prove an active tunnel
Encrypted outer trafficXDP or tc on the physical interfaceOuter addresses, ports, direction, packet metadataPayload remains encrypted
Inner tunnel traffictc on tun, tap, or wgInner addresses, ports, and flow directionThe view depends on the interface and attach direction
Socket activitySocket or cgroup hooksProcess and socket associationThe socket may belong to the kernel, not a user process
IPsec policy changesXFRM tracepoints or carefully chosen probesPolicy and state transitionsInternal symbols vary across kernels
Interface changesNetwork tracepoints and netlink in user spaceDevice creation, deletion, and stateInterface events do not explain packet behavior

I use tracepoints before kprobes. Tracepoints have a defined event format and usually survive internal function changes better. Function entry and exit programs using BTF can work well when the target function is exposed, but they still depend on kernel details.

Kprobes are a fallback, not a design principle. A probe attached to an internal WireGuard or XFRM function can stop loading after a kernel update, even though the VPN still works. If the event exists at a tracepoint or through netlink, use that instead.

For packet visibility, I start with tc. It works naturally with virtual tunnel devices and gives access to the socket buffer (skb) path. I use XDP on a physical network interface when I need early underlay visibility or packet filtering. XDP is usually the wrong first choice for tun0 or wg0.

WireGuard needs two views

Monitor VPN Connections with eBPF, two network devices linked by blue and yellow Ethernet cables

The WireGuard data path has an outer and an inner view. On the physical interface, eBPF sees the encrypted UDP packet and its outer endpoint. On the WireGuard interface, eBPF can see the inner packet after decryption or before encryption, depending on the direction and hook.

That gives you useful correlation:

  • Physical interface traffic shows whether the host is exchanging encrypted packets.
  • The WireGuard interface shows whether inner traffic is crossing the tunnel.
  • User-space WireGuard state maps a public key to configured peer information.
  • Process tracing matters only for user-space implementations such as wireguard-go.

Use the WireGuard tools locally to inspect control-plane state:

sudo wg show all
sudo ip -d link show type wireguard
sudo bpftool net

wg show all reports configured interfaces and peer state. Keep that output local or redact it before shipping it. WireGuard state includes sensitive identity and endpoint information.

Do not build a production monitor around private wg_* kernel functions unless you control the kernel build and accept the maintenance cost. WireGuard eBPF monitoring works better when eBPF watches the network path and a user-space collector reads WireGuard's supported control interface.

A peer with recent traffic is active in the narrow sense that traffic exists. It is not automatically healthy. Your agent should compare traffic, peer state, route state, and expected policy before raising an alert.

Read tunnel traffic at the right layer

A physical interface and a tunnel interface answer different questions.

At the physical interface, you can see the encrypted transport. For WireGuard, that usually means UDP traffic between the host and an endpoint. For OpenVPN, it may be UDP or TCP. For IPsec, it may be ESP or a control channel that negotiates the data path.

At wg0, tun0, or tap0, you can observe the inner network conversation. That may expose private addresses, DNS traffic, application ports, and other data you did not see on the underlay.

This is why I do not attach every hook by default. Attach the underlay hook when you need endpoint and transport health. Attach the tunnel hook when you need policy and flow visibility. Collecting both without a clear question creates duplicate events and more sensitive data.

If DNS matters, correlate tunnel flow events with a separate monitor for DNS traffic with eBPF. Do not force DNS parsing into the VPN program unless the kernel-side program has a clear reason to do it.

Move small records through maps

eBPF programs do not write directly into your database. They use maps and event buffers to pass state to user space.

I use maps for state that must remain available:

  • A hash map for active interface or peer state.
  • A per-CPU map for packet counters and drop counters.
  • An LRU map for flow summaries that must expire under pressure.
  • A ring buffer for discrete events such as interface changes or policy updates.

The user-space agent reads those records and adds context. It can query wg show all, inspect XFRM state, read process metadata, and attach inventory labels without making the kernel program parse command output.

Keep event records compact. Include the fields needed to identify the event and leave enrichment to user space. If the ring buffer fills, count the lost events. A monitor that silently drops its own alerts is worse than a monitor that reports its limits.

I also separate counters from alerts. Packet counters can be aggregated in the kernel. A peer endpoint change or unexpected tunnel deletion should become an event with context. Sending every packet through a ring buffer wastes work and makes the important records harder to find.

What does mount -t bpf bpf /sys/fs/bpf do?

The command mounts the BPF filesystem, commonly called bpffs, at /sys/fs/bpf. It does not load a program, attach a hook, or make a VPN observable by itself.

sudo mkdir -p /sys/fs/bpf
findmnt -t bpf

if ! mountpoint -q /sys/fs/bpf; then
 sudo mount -t bpf bpf /sys/fs/bpf
fi

findmnt /sys/fs/bpf

In mount -t bpf bpf /sys/fs/bpf, the first bpf after -t is the filesystem type. The second bpf is the source name expected by the mount interface. /sys/fs/bpf is the target directory.

The filesystem gives you a place to pin BPF programs and maps. Pinning keeps those kernel objects reachable after the loader exits, which helps a service restart without tearing down every attachment. It does not make the objects survive a reboot.

If findmnt -t bpf already shows the mount, do not mount it again. Many Linux systems mount it during boot through a systemd unit. Repeating the command is not a repair.

A mount failure inside a container often means you are in the wrong mount namespace. Mounting BPF in that namespace does not make the host's BPF objects visible. The container also needs the required BPF permissions and access to the relevant kernel interfaces.

Check the kernel before writing the monitor

Start with feature discovery. Do not compile a large CO-RE application and discover afterward that the target kernel lacks the program type, helper, or BTF data it needs.

uname -r
sudo bpftool feature probe kernel
sudo bpftool prog list
sudo bpftool map list
test -r /sys/kernel/btf/vmlinux && echo "BTF available"

uname -r identifies the running kernel. It does not tell you which BPF features are enabled. bpftool feature probe kernel reports available program types, map types, and helpers. The program and map listings show what is already loaded, which matters on a host with another observability agent.

BTF means BPF Type Format. It gives loaders type information about the running kernel. CO-RE means Compile Once – Run Everywhere. With libbpf and BTF, a program can adapt field offsets across supported kernels instead of hard-coding one kernel layout.

The toolchain I use for a maintained monitor is clang and LLVM for compilation, libbpf for loading, bpftool for inspection, and a user-space language with a clear event reader. BCC and bpftrace are good for testing a question quickly. They are not a reason to keep a pile of ad hoc scripts in production.

Use bpftrace to confirm that a lifecycle signal exists before writing C:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%d %s\n", pid, str(args->filename)); }'

That prints process execution events. Filter the output in your head or in a test shell for openvpn, charon, or wireguard-go. It proves that the tracepoint fires. It does not prove that the process owns the active VPN path.

For deeper syscall context, this eBPF system call tracing guide covers the same diagnostic method without tying it to VPN code.

Build the production program with libbpf

A production monitor should have a small kernel object and an explicit user-space loader. The loader opens the BPF object, loads it through the bpf() system call, attaches each program, reads maps, and handles shutdown.

I keep the kernel side responsible for:

  • Reading packet or tracepoint context.
  • Checking packet bounds before every field access.
  • Filtering to known interfaces, protocols, or directions.
  • Updating counters.
  • Sending compact events.
  • Returning a pass verdict unless the program is intentionally enforcing policy.

The user-space side handles:

  • Configuration.
  • Interface discovery.
  • Peer and policy enrichment.
  • Event serialization.
  • Alert rules.
  • Reloads and cleanup.
  • Export to logs or a security information and event management system.

This split makes failures easier to reason about. If the event reader stops, the VPN datapath should keep passing traffic. If an attachment fails, the loader should report the exact program and hook instead of starting with half the monitor missing.

Use pinned objects only when you need independent lifecycle management. Otherwise, let the libbpf application own its objects and detach them cleanly. A stale pinned map can make a new deployment look like it has old state, which is how an incident report gets its first misleading line.

Turn events into useful anomaly rules

Monitor VPN Connections with eBPF, rack server and network device with Ethernet cables on a shelf

A VPN anomaly rule should compare related signals. A single packet is rarely enough.

Useful rules include:

  • A tunnel interface appears without the expected service or configuration.
  • An endpoint changes outside a maintenance or roaming pattern.
  • Outer encrypted traffic continues while expected inner traffic stops.
  • Inner traffic reaches a network outside the tunnel's allowed policy.
  • A new process opens a tunnel device unexpectedly.
  • XFRM policy changes without a matching deployment event.
  • A tunnel disappears while dependent routes remain active.
  • The monitor loses events or map reads for longer than its normal collection window.

Do not alert on every endpoint change. Mobile clients, NAT, and roaming can change the outer address without indicating compromise. Correlate the change with peer state, handshake activity, process state, and the allowed network policy.

The same applies to idle tunnels. No inner packets can mean a dead tunnel, or it can mean the user has nothing to send. A rule needs context about expected use. Otherwise it reports normal silence as a failure.

For security workflows, the eBPF IDS guide is a useful companion for turning kernel events into detection decisions. The VPN monitor should feed that pipeline with normalized records rather than inventing a second alert format.

Where TinyCheck anomaly rules fit

TinyCheck anomaly rules belong in user space. They are detection logic, not eBPF hooks.

If you use TinyCheck anomaly rules alongside VPN monitoring, map your eBPF events into the fields those rules actually consume. That may include an endpoint, a destination, a protocol, a DNS name, a process, or a flow state. Keep the rule syntax out of the kernel program.

Do not copy a rule into C and ask the verifier to help you interpret it. The verifier checks memory safety and allowed operations. It does not know whether a destination is suspicious, whether a peer is approved, or whether a DNS name belongs to a normal application.

A clean pipeline looks like this:

eBPF hook
 -> compact kernel event
 -> user-space normalization
 -> peer, process, DNS, and policy enrichment
 -> TinyCheck anomaly rule
 -> alert or stored event

The rule should also say what evidence caused it to fire. Store the interface, direction, peer identity if known, and the policy that was violated. A bare “anomaly detected” line is not useful during an outage.

Respect the verifier and the packet path

The verifier is the boundary between your program and the kernel. It checks pointer bounds, helper use, control flow, map access, and other safety rules before the program loads.

For packet parsing, check the data pointer and end pointer before reading every header. After moving past an IP or transport header, check the new pointer again. The verifier does not accept “the packet is probably long enough” as an argument.

Keep loops bounded. Avoid large stack objects. Do not sleep in a packet program. Use helpers that are valid for the program type. A helper available to a tracing program may not be available to an XDP or tc program.

A program that passes the verifier can still be wrong. It may attach to the wrong direction, read encrypted headers instead of inner headers, or count segmented packets differently from the application. The verifier checks safety. It does not check your theory of the VPN.

Use bpf_printk for short debugging sessions only. It is slow and noisy under traffic. Move real diagnostics into ring-buffer events and per-CPU counters after the hook is proven.

Test attachments and failure modes

Load the program on a test host or an isolated namespace first. Generate known tunnel traffic, stop the VPN, change a route, and rotate the peer endpoint. Confirm which event appears for each action.

sudo bpftool prog list
sudo bpftool map list
sudo bpftool net
sudo tc filter show dev wg0 ingress
sudo tc filter show dev wg0 egress
sudo journalctl -k -b

bpftool prog list confirms that the program loaded. bpftool map list shows its state objects. bpftool net helps locate network attachments. The tc commands confirm whether the expected filters are attached to the tunnel device and direction.

If the program refuses to load, read the libbpf and verifier log. Do not delete maps or reload the kernel until you know whether the failure is a missing helper, a bad context access, a permission problem, or a type mismatch.

If the program loads but reports no traffic, check the interface namespace first. A VPN interface in another network namespace will not produce events from a hook attached in the host namespace. Then check direction, offload settings, and whether the traffic is taking a different route.

Generic receive offload and segmentation can also change what a packet hook sees. Packet counts at XDP, tc, the socket layer, and the application do not have to match. That is a measurement difference, not proof that one layer is broken.

Default packet programs to pass traffic. A monitoring error must not become a VPN outage. If you later add enforcement, test the drop path separately and make the policy visible in the event record.

What can eBPF not tell you about a VPN?

eBPF cannot give you the same view at every hook.

At the physical interface, encrypted VPN payloads remain opaque. You may see outer addresses, ports, packet direction, and size. You cannot read the inner destination from that hook.

At a tunnel interface, you may see decrypted inner traffic. That creates a privacy boundary you need to treat seriously. Private addresses, application ports, and DNS requests can reveal more than the outer VPN stream did.

eBPF also cannot prove peer authentication from packet activity alone. For WireGuard, read supported peer state and correlate it with traffic. For IPsec, inspect XFRM state and policy. For OpenVPN, combine process and tunnel state with socket activity.

Finally, eBPF does not remove kernel compatibility work. Tracepoints and stable contexts reduce the problem. They do not eliminate it. Internal kprobes, helper availability, BTF differences, virtual-device behavior, and permission policy still need testing.

Keep the monitor small enough to operate

The hard part is not attaching a probe. It is keeping the monitor correct after the host, kernel, VPN configuration, and network namespace change.

I keep these production rules:

  • Load only the programs needed for the current VPN type.
  • Pin maps when a separate service needs them.
  • Scope capture to known interfaces and directions.
  • Drop payload collection unless there is a documented reason.
  • Count lost ring-buffer events.
  • Record attach failures and verifier logs.
  • Restrict who can load or replace BPF programs.
  • Test with the host's SELinux, AppArmor, and lockdown settings.
  • Keep a rollback that detaches the monitor without touching VPN routes.
  • Review map growth and event volume during real traffic.

Do not ship private keys, full command lines, or raw packet data by default. A VPN monitor already handles sensitive network information. More collection does not make the diagnosis better.

If event volume becomes the problem, fix the event design before adding more CPU. Aggregate counters in per-CPU maps, filter at the hook, and enrich in user space. This guide to reducing kernel overhead using eBPF covers the same pressure from a broader tracing perspective.

Who should use this approach?

Use eBPF VPN monitoring when you need host-level visibility across several VPN implementations and can control the Linux hosts. It suits security teams, network operators, and platform engineers who need to correlate tunnel traffic with processes, routes, namespaces, and policy.

Skip it when you need decrypted application payloads, a portable monitor with no kernel privileges, or a service that must run across systems with no consistent BPF support. A packet capture, VPN export, or application-level agent may fit those cases better.

The trade-off is clear. eBPF gives you precise kernel-side signals with less copying than a full capture path, but you must understand the hook, the namespace, the verifier, and the VPN implementation. If you do not know which layer produced an event, the event is not evidence yet.

FAQ

Can eBPF monitor a VPN that runs inside a container?

Yes, if you attach in the network namespace where the VPN interface and traffic exist. A host-level program may see the physical underlay while missing the container's tunnel device. Check namespace placement before changing the program.

Should VPN flow records contain full IP addresses?

Only if the investigation needs them. Store the smallest identity that supports the rule, protect access to the records, and apply retention controls in user space. Hashing or tokenizing addresses can reduce exposure, but it also limits later investigation.

How do I handle a monitor that loses ring-buffer events?

Expose a lost-event counter beside the normal event stream. Then reduce kernel-side detail, filter earlier, or aggregate repeated traffic into maps. Do not treat a quiet stream as a healthy stream until you have checked the loss counter.

Can I monitor a VPN without changing its routing?

A passive tracepoint, socket, or packet program can observe traffic without changing routes. Keep tc and XDP programs in pass-through mode while testing. Enforcement programs are different and can alter packet handling if their return path is wrong.

Is bpftrace enough for a permanent VPN monitor?

Use bpftrace to answer a narrow question and prove that a hook fires. For a permanent service, use libbpf with explicit loading, map handling, permissions, error reporting, and cleanup. A command that is useful during an incident is not automatically an operable daemon.