OpenVPN Data Channel Offload (DCO)

Authors

K19G

grok-bot

Updated

October 31, 2026

OpenVPN Data Channel Offload (DCO)

Thesis

Classic OpenVPN runs the control plane and the data plane in userspace. Every encrypted packet crosses the kernel↔︎userspace boundary twice. That copy-and-context-switch tax is why OpenVPN used to lose throughput races against kernel-native VPNs. Data Channel Offload (DCO) moves encryption, decryption, and encapsulation into a Linux kernel module (ovpn since Linux 6.16; earlier out-of-tree ovpn-dco / ovpn-dco-v2). Userspace OpenVPN keeps TLS, authentication, and option negotiation; the kernel keeps the hot path. The security model does not change — only where the ciphertext is processed. On modern hardware, published UDP benchmarks regularly show roughly 2× legacy OpenVPN throughput, and independent tests put DCO in the same ballpark as other high-performance VPN designs.

Mental model

  Before DCO (userspace data path)          With DCO (kernel data path)

  NIC ──► kernel ──► openvpn(userspace)     NIC ──► kernel ovpn module
              │  encrypt / decrypt                     │  encrypt / decrypt
              │  encapsulate                           │  encapsulate
              ▼                                        ▼
         kernel ──► NIC                            NIC (no userspace hop)

  Control plane (TLS, auth, PUSH) stays in userspace either way.
Piece Role
Control plane TLS handshake, certificates, --push / NCP, peer lifecycle — always userspace
Data plane AEAD encrypt/decrypt, tunnel encapsulation, peer forwarding — kernel with DCO
ovpn module In-tree since Linux 6.16; needs OpenVPN 2.7+
ovpn-dco / backports Out-of-tree path for kernels < 6.16 or OpenVPN 2.6.x
--disable-dco Force the legacy userspace data path (benchmarks, unsupported options)

Cipher and topology constraints (by design). DCO is L3 / dev tun only. Supported data-channel ciphers are AES-GCM-128/192/256 and ChaCha20-Poly1305. Compression framing is out; servers use --topology subnet. Older CBC-only peers force a non-DCO fallback. Routing to clients uses the host routing table (a client VPN IP as nexthop) rather than OpenVPN’s internal iroute machinery — see the official README.dco.md.

Version pairing that matters in 2026:

Kernel / userspace Module name Notes
Linux ≥ 6.16 + OpenVPN ≥ 2.7 ovpn (in-tree) Preferred; no DKMS on stock distro kernels
Older kernel or OpenVPN 2.6 ovpn-dco via ovpn-backports / distro DKMS Still fine; package names differ by distro
Windows ovpn-dco-win Ships with official Windows builds since 2.6; multi-core workers improved further in 2026

Worked examples

Case 1: Confirm the kernel side before you touch configs

On a host that already runs Linux 6.16+ with OpenVPN 2.7+, you do not install a third-party module. Check what you have:

# dco-probe.sh — inventory for OpenVPN DCO (read-only; no packages installed here)
uname -r
modinfo ovpn 2>/dev/null | head -5 || echo "ovpn module metadata not present"
lsmod | grep -E '^ovpn' || echo "ovpn not loaded yet (normal until openvpn starts)"
openvpn --version 2>/dev/null | head -3 || echo "openvpn binary not on PATH"

Expected shape on a DCO-ready box (versions will differ):

$ uname -r
6.16.3-1-amd64
$ modinfo ovpn | head -3
filename:       /lib/modules/6.16.3-1-amd64/kernel/drivers/net/ovpn/ovpn.ko
description:    OpenVPN data channel offload
license:        GPL

If modinfo ovpn is missing on an older kernel, use the ovpn-backports project or your distro’s ovpn-dkms / kmod-ovpn package — not covered as an install lab in this chapter (no package installs in the book environment).

Case 2: Let OpenVPN auto-enable DCO (and force it off for A/B)

OpenVPN auto-detects DCO when the module is present and the config is compatible. You only need an explicit flag to disable it.

# Minimal server sketch — AEAD + subnet topology so DCO can engage
# openvpn-server-dco.conf
port 1194
proto udp
dev tun
topology subnet
server 10.8.0.0 255.255.255.0
dh none
tls-groups X25519:secp256r1
data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305
persist-key
keepalive 10 60
verb 3
# certificates omitted — use your existing PKI
# Run with DCO (default when available):
#   openvpn --config openvpn-server-dco.conf
# Force userspace data path for a throughput comparison:
#   openvpn --config openvpn-server-dco.conf --disable-dco

Log lines to look for:

Message Meaning
DCO / ovpn enabled (wording varies by version) Kernel data path is live
Note: Kernel support for ovpn-dco missing, disabling data channel offload. Module absent or userspace too old for in-tree ovpn
Warning that an option disabled DCO Config used a feature DCO cannot implement (compression, unsupported cipher, …)

Antonio Quartulli’s workstation numbers (Ryzen 9 9950X, 25 Gbps NIC, iperf3 UDP, published via the OpenVPN DCO guide, Feb 2026): legacy userspace ≈ 3.61 Gbps; DCO ≈ 7.15 Gbps — about 2× on the same box. Third-party reports (IPFire, consumer VPN vendors, router vendors) show similar order-of-magnitude gains; treat any single percentage as hardware-specific.

Case 3: One-sided DCO still helps

You do not need DCO on both ends for a win. A DCO server alone cuts server CPU and raises aggregate capacity; both ends DCO maximizes the path. Clients that speak OpenVPN 2.6+/Connect 3.4+ with AEAD are enough for the control plane to negotiate a DCO-capable data channel when the server supports it.

  laptop (no DCO) ──TLS──► gateway (DCO / ovpn)
       ▲                         │
       │   ciphertext still      │  encrypt/decrypt + forward
       └──── encrypted tunnel ───┘  stays in kernel on the gateway

Case 4: Routing mental shift under DCO

With DCO, “iroutes” become ordinary host routes whose nexthop is the client’s VPN address. Client-to-client is no longer a special userspace hairpin: packets hit the tunnel interface and the system routing table decides the next peer.

# Example: send 192.0.2.0/24 to the client that owns 10.8.0.5
# ip route add 192.0.2.0/24 via 10.8.0.5

If you still need a blackhole when that client is offline, add an explicit companion route that only exists while the peer is connected — DCO will not invent OpenVPN’s old internal routing table for you.

The trap

  1. CBC or compression “just for compatibility”. A single --data-ciphers entry that forces CBC, or leftover --compress, silently (or with a warning) drops you back to userspace. You keep the old config working and wonder why the benchmark looks unchanged.
  2. OpenVPN 2.6 + in-tree ovpn. The Linux 6.16 ovpn module speaks the 2.7 netlink API. On 2.6 you still want the older ovpn-dco out-of-tree module (or upgrade userspace). Mixing them yields “kernel support missing” even though a DCO module is loaded.
  3. Assuming DCO changes cryptography. It does not. TLS, certificates, and cipher selection stay in userspace; DCO only relocates AEAD packet processing. Threat modeling stays the same; capacity planning does not.
  4. Comparing protocols by blog headlines alone. Marketing posts crown a winner every quarter. Measure your NIC, CPU crypto offload, and MTU. DCO removes OpenVPN’s historical userspace tax; it does not erase the need for sound AEAD choice, MSS clamping, and CPU affinity on multi-gigabit paths.

The boring rule

Ship OpenVPN ≥ 2.7 on Linux ≥ 6.16 with AES-GCM or ChaCha20-Poly1305, --topology subnet, and no compression. Let DCO auto-enable. Keep --disable-dco in your lab toolkit for A/B tests. On older kernels, use the matching out-of-tree module — do not assume the in-tree ovpn name. Verify with modinfo / logs before you trust a speed claim.

Try this

  1. On a 6.16+ lab host, run modinfo ovpn and openvpn --version. Confirm the 2.7 + ovpn pairing.
  2. Bring up a two-namespace UDP tunnel (reuse your existing PKI). Capture a short iperf3 run with DCO on, then the same run with --disable-dco. Record throughput and top CPU for openvpn.
  3. Intentionally add a DCO-incompatible option (e.g. force a CBC cipher). Confirm the log shows DCO disabled, then remove it.
  4. Add a host route via a client VPN IP and traceroute from the server VRF/namespace to prove DCO is using the system FIB.

Sources