Following a Packet: The User Plane in the volte.io ePDG

Part 3 of our ePDG series. The control plane is done, the bearer is up. Now what actually happens to a packet as it crosses the ePDG? A walkthrough of the kernel-and-userspace data path, in both directions.

← Back to News The volte.io ePDG user plane: an ESP-encrypted packet from the UE is decrypted by the Linux kernel (or the NIC), policy-routed, and re-encapsulated into a GTP-U tunnel toward the PGW, with userspace kept entirely out of the data path

Part 1 covered authentication. Part 2 covered bearer establishment. By the end of those exchanges, the UE has a tunnel, an IP address, and somewhere to register. It can now use the network.

This article is about what happens during that use – the moment a SIP REGISTER, an RTP packet, or a TCP segment crosses the ePDG. The architecture is interesting because the ePDG sits at the boundary between two very different worlds: an IPsec ESP tunnel terminated on one side, a GTP-U tunnel originating on the other. Translating between them – fast and at scale – is where the design choices in the volte.io ePDG do most of their work.

The Headline for Operators

Once the bearer is up, the userspace ePDG process is not in the data path. Packets flow through the Linux kernel, with hardware offload where available. Userspace handles only signaling – IKE messages, GTP-C messages, AAA Diameter exchanges. This is what allows a single host to comfortably push gigabits of VoWiFi traffic, and what lets the control plane and the data plane scale independently in Kubernetes.

1. The Two Tunnels Meeting at the ePDG

Before following a packet, it helps to picture what the ePDG looks like from a packet’s perspective.

SWu – facing the public internet

An IKEv2-negotiated IPsec tunnel between the UE and the ePDG. Packets arrive as ESP, encrypted with AES-GCM or AES-CBC, often UDP-encapsulated for NAT traversal on port 4500.

S2b – facing the mobile core

A GTP-U tunnel between the ePDG and the PGW. Packets travel as plain UDP on port 2152, with each packet carrying a GTP header that names the F-TEID identifying the subscriber’s session.

The job in the middle

Take an inner IP packet from one tunnel and re-encapsulate it for the other. Uplink: decrypt ESP, encapsulate in GTP-U. Downlink: decapsulate GTP-U, encrypt in ESP.

Where the work runs

The volte.io ePDG does both directions in the kernel, using subsystems that the Linux community has spent over two decades optimizing.

ePDG user-plane architecture: an ESP/IPsec tunnel from the UE on the SWu interface and a GTP-U tunnel to the PGW on the S2b interface meet in the Linux kernel; the userspace ePDG handles only signaling and is not in the data path
The two tunnels meeting at the ePDG. Userspace owns the signaling; the kernel owns every byte of user-plane traffic.

2. The Uplink Path: From the UE to the PGW

A subscriber places a SIP REGISTER. The handset’s IP stack hands the packet to its IPsec implementation, which wraps it in ESP, which UDP-encapsulates it on port 4500, which sends it out the WiFi interface and across the internet to the ePDG’s public IP address.

What happens when that UDP packet arrives at the ePDG host?

2.1 Reception and ESP Decryption

The packet hits the host’s NIC. If the NIC supports IPsec inline offload – as the NVIDIA/Mellanox ConnectX cards we use for our IMS deployments do – the NIC recognizes the UDP-encapsulated ESP, looks up the matching Security Association in its hardware table, decrypts the payload, verifies integrity, and presents the cleartext inner IP packet to the kernel as if it had arrived in the clear.

If the NIC does not offload, the same work happens in software via the kernel’s XFRM subsystem. AES-NI on any modern x86 CPU makes this fast enough for most deployments – typically a few cycles per byte for AES-GCM, dominated more by memory bandwidth than CPU.

Either way, the cleartext packet that emerges has the UE’s allocated IP address as its source, and some destination – for our SIP REGISTER, that destination is the P-CSCF address the UE learned during IKE_AUTH.

2.2 Routing Decision

Now the kernel asks the same question it asks of every packet: where does this go?

This is where the architecture gets interesting. The cleartext packet’s destination is the P-CSCF, which sits inside the operator’s IMS network. It cannot just be routed through the host’s main routing table, because the host has no direct connectivity to the IMS – only the PGW does.

The volte.io ePDG handles this with policy-based routing. The packet has just emerged from the UE’s IPsec tunnel; the kernel marks it with metadata that identifies which tunnel it came from. A policy rule directs all packets carrying that mark into a per-subscriber routing table whose default route points into a GTP-U tunnel.

The relevant Linux primitives are:

The result is that the kernel’s standard forwarding path – the same path used for any other routed traffic – moves the packet from “just decrypted from ESP” to “about to be encapsulated in GTP-U” without any userspace involvement.

2.3 GTP-U Encapsulation

The GTP-U device receives the packet, prepends an 8-byte GTP-U header containing the F-TEID the PGW expects (learned during the Create Session Response in Part 2), and hands the result to the kernel’s UDP stack with the destination set to the PGW’s GTP-U endpoint on port 2152.

The kernel sends the UDP packet out through whatever interface routes to the PGW – typically a private interface into the operator’s mobile core. The PGW receives it, strips the GTP-U header, looks up the session by F-TEID, and routes the inner IP packet onward to its destination – for our SIP REGISTER, that means delivering it to the P-CSCF.

Uplink data path through the volte.io ePDG: an ESP packet from the UE is received by the NIC, decrypted by the kernel XFRM subsystem or NIC offload, policy-routed, encapsulated by the kernel GTP-U module, and forwarded as UDP on port 2152 to the PGW
Uplink: NIC → kernel XFRM → policy routing → kernel GTP-U → NIC. Userspace is not involved.

2.4 What the ePDG Userspace Did During All This

Nothing.

That is the architectural point. The userspace Erlang process that handles IKE signaling, that manages session state, that talks to Redis and Prometheus – it was not involved in any of the steps above. The packet went NIC → kernel XFRM → routing → kernel GTP-U → NIC, with userspace only ever having programmed the state up front. This is what allows the data path to scale linearly with hardware, independent of how many control-plane events the ePDG is handling at the same time.

3. The Downlink Path: From the PGW to the UE

The reply to the SIP REGISTER eventually comes back through the same path in reverse – a 200 OK from the IMS, routed through the PGW, encapsulated in GTP-U, and arriving at the ePDG.

3.1 GTP-U Reception and Decapsulation

The packet arrives on the ePDG’s mobile-core-facing interface as UDP on port 2152. The kernel’s GTP-U module looks up the F-TEID in the GTP header, identifies the session, and delivers the decapsulated inner IP packet – a packet whose destination is the UE’s allocated IP address – to the kernel’s IP layer.

3.2 The Routing Trick

This is where careful policy configuration matters. The kernel’s main routing table, asked where to send a packet destined for the UE, would not have a useful answer – the UE’s IP address is in the operator’s address pool, not in any subnet directly attached to the host. Without help, the packet would either be dropped or sent out the default route, which would be wrong.

The help comes from a routing rule that catches packets emerging from a GTP-U device and directs them into a routing table whose default route points into the matching IPsec SA. The kernel sees a packet whose destination matches the outbound XFRM policy installed at the end of the IKE_AUTH exchange, and the policy directs the packet through the outbound Security Association.

3.3 ESP Encryption and NAT Traversal

The XFRM subsystem (or the NIC’s hardware offload, if available) encrypts the packet with the outbound key, prepends the ESP header with the next sequence number, computes the integrity check value, and produces an ESP packet ready to send.

For NAT-traversed sessions, the ESP packet is then UDP-encapsulated on port 4500 per RFC 3948. The destination is the public IP and port from which the UE’s IKE messages arrived – which may well be a NAT’d address, where the operator’s network sees the carrier-grade NAT or the home router’s external interface, not the UE’s internal address.

The packet leaves the public interface, traverses the internet, and arrives at the UE, where the handset’s IPsec stack reverses the process.

Downlink data path through the volte.io ePDG: a GTP-U packet from the PGW is decapsulated by the kernel GTP-U module, policy-routed into the matching IPsec Security Association, ESP-encrypted by XFRM or NIC offload, UDP-encapsulated for NAT traversal on port 4500, and sent back to the UE
Downlink: kernel GTP-U decapsulation → policy routing → XFRM encryption → NAT-T UDP encapsulation → out to the UE.

3.4 Sequence Numbers and the Replay Window

One detail worth noting on the downlink: ESP includes a 32-bit sequence number that increments with every packet. The receiver maintains a replay window – typically 64 packets wide – and rejects any packet whose sequence number has already been seen, or whose sequence number is too far ahead of the window’s leading edge.

This works fine for a stable, in-order link. It can cause unexpected packet drops on a link with significant reordering, which Wi-Fi sometimes is. The volte.io ePDG configures a wide replay window (1024 packets) on its outbound SAs by default, and exposes the parameter for tuning. This is not a frequent operational concern, but when it does come up – typically with mobile devices on flaky public Wi-Fi – having the parameter exposed avoids a frustrating diagnostic exercise.

4. Why Kernel XFRM and Not Userspace ESP

It is worth addressing a question that comes up regularly in design discussions: why kernel XFRM rather than a userspace ESP implementation, the way some ePDG implementations handle it?

The kernel XFRM subsystem has been in mainline Linux since 2002. It has been used by every Linux-based VPN product, every IPsec gateway, and most Android phones for over two decades. It supports hardware offload through a clean abstraction. It handles edge cases – fragment handling, MTU discovery, NAT-T keepalives, replay protection across SA rekeying – that any userspace implementation would need to reinvent.

A userspace ESP implementation, by contrast, requires every packet to traverse the kernel-userspace boundary at least twice (once for ingress, once for egress), with the associated context switches and memory copies. It does not benefit from hardware offload unless the NIC exposes a userspace-friendly interface. And it has to reimplement, slowly and probably with more bugs, what the kernel already does well.

There are reasons to choose userspace ESP – primarily flexibility for specialized environments where the kernel’s IPsec is not available – but for a Linux-based ePDG running in Kubernetes on commodity or accelerated hardware, kernel XFRM is the correct choice. It is the same conclusion every serious Linux-based IPsec product has reached, and we see no reason to disagree.

What This Means in Practice

Choosing kernel XFRM is not just an aesthetic preference. It means twenty-plus years of bug fixes, hardware-offload paths from every major NIC vendor, and a data plane that survives kernel upgrades and security patches without the ePDG team having to rebuild crypto code. It is also the difference between a deployment that needs a small fleet of pods to handle a metro’s VoWiFi traffic, and one that needs a rack.

5. The Same Picture, in Numbers

To make the architecture concrete, here is what a single uplink RTP packet (a 20-millisecond audio frame, around 200 bytes) costs in compute on a modern x86 server:

The total budget is on the order of 1000 CPU cycles per packet, or roughly 300 nanoseconds on a 3 GHz core. With one core dedicated to softirq processing, that comfortably handles several million packets per second – far more than a single ePDG instance is ever likely to see in practice. With NIC offload of the ESP step, the cycle budget per packet drops further, and CPU stops being the bottleneck before the NIC does.

This is what lets a VoWiFi deployment actually run on Kubernetes. The data plane is in the kernel; the control plane is in pods. The two scale independently, and the data plane scales linearly with the hardware available to it.

Coming Up in Part 4

The data path is fast, the control plane is clean, and a single ePDG instance can handle the traffic of a small operator. The remaining question – and the subject of Part 4 – is how this scales horizontally across multiple ePDG replicas.

We will look at what Redis-backed shared state actually does (and, importantly, does not do) for an IKE-based gateway. We will examine LoadBalancer behavior and session affinity, and what happens when a UE’s session needs to be reconstructed elsewhere. We will discuss Connection Draining during rolling upgrades, and the operational primitives – health probes, observability, graceful shutdown – that turn a single working pod into a fleet that can be operated like any other Kubernetes workload.

Why This Matters Commercially

Every byte that crosses an ePDG is a billable VoWiFi minute, a SIP REGISTER, or an RTP frame the subscriber will hear. The cost of moving those bytes – in CPU, in hardware, in operator pain – is set by the data-path architecture. By keeping userspace out of the fast path and letting the kernel and NIC do what they do best, the volte.io ePDG turns VoWiFi capacity into a question of how much hardware to provision, not how much engineering to throw at the next traffic peak.

Ready to Transform Your Network?

Contact us today and discover how volte.io can power your communications infrastructure.

Contact Us