OpenVPN Data Channel Offload (DCO)
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-dcoLog 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.5If 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
- CBC or compression “just for compatibility”. A single
--data-ciphersentry 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. - OpenVPN 2.6 + in-tree
ovpn. The Linux 6.16ovpnmodule speaks the 2.7 netlink API. On 2.6 you still want the olderovpn-dcoout-of-tree module (or upgrade userspace). Mixing them yields “kernel support missing” even though a DCO module is loaded. - 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.
- 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
- On a 6.16+ lab host, run
modinfo ovpnandopenvpn --version. Confirm the 2.7 +ovpnpairing. - 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 andtopCPU foropenvpn. - Intentionally add a DCO-incompatible option (e.g. force a CBC cipher). Confirm the log shows DCO disabled, then remove it.
- Add a host route
viaa client VPN IP and traceroute from the server VRF/namespace to prove DCO is using the system FIB.
Sources
- OpenVPN DCO: The Definitive Guide (OpenVPN blog; updated 2026-10-08)
- OpenVPN
README.dco.md(upstream limitations and enablement) - Community wiki: DataChannelOffload
- ovpn-backports (out-of-tree
ovpnfor older kernels)