248 Views
March 31, 26
スライド概要
Building a WireGuard Data Plane with eBPF and bpf_crypto eBPF Meetup Tokyo #6, 2026 Naoya Tezuka DISCLAIMER This is a personal technical exploration and does not relate to any product, service, or internal project at my company. All content is based on publicly available upstream Linux kernel source code and documentation. No confidential information is disclosed.
Who ● Naoya Tezuka ○ github: NT-marlowe ○ X InformationRes5 ● SWE ● debug ChromeOS / Android kernel
Agenda ● Motivation & bpf_crypto API Overview ● Ideal Architecture ● The Blocker: No AEAD ● Compromises for PoC ● PoC Demo ● Takeaways & Future
What is WireGuard? ● Modern VPN protocol, in-kernel since Linux 5.6 ● Uses ChaCha20Poly1305 AEAD for data encryption 2 ● ○ ChaCha20: symmetric stream cipher ○ Poly1305 MAC to verify the integrity of data within VPN tunnel Simple and easy to use, minimal codebase [1] https://www.wireguard.com/trademark-policy/ [2] https://www.wireguard.com/protocol/
What is WireGuard? ● Modern VPN protocol, in-kernel since Linux 5.6 ● Uses ChaCha20Poly1305 AEAD for data encryption 2 ● ○ ChaCha20: symmetric stream cipher ○ Poly1305 MAC to verify the integrity of data within VPN tunnel Simple and easy to use, minimal codebase …Looks like a good use case of bpf_crypto 🤔 [1] https://www.wireguard.com/trademark-policy/ [2] https://www.wireguard.com/protocol/
What is bpf_crypto? ● eBPF kfuncs that expose the kernel's crypto API to BPF programs ○ Encrypt/decrypt packets directly in TC/XDP hooks — no user-space round-trip ● Key design: sleepable/non-sleepable separation 1 ○ Context creation (heavy, allocates memory) → sleepable SYSCALL program 2 ○ Encrypt/decrypt (fast, per-packet) → non-sleepable TC/XDP program ○ Connected via BPF map (kptr) [1] "Sleepable BPF programs": https://lwn.net/Articles/825415/ [2] https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_SYSCALL/
bpf_crypto API Overview ● Introduced in Linux 6.10 (by Vadim Fedorenko, Meta) 1 ● Create crypto ctx via SYSCALL programs, and acquire/release it [1] https://lwn.net/Articles/1049463/
bpf_crypto API Overview ● Introduced in Linux 6.10 (by Vadim Fedorenko, Meta) 1 ● Uses bpf_dynptr for input/output buffers [1] https://lwn.net/Articles/1049463/
Why eBPF for WireGuard Data Plane? ● Explore bpf_crypto's real-world capability ○ Usage examples are limited to kernel selftest 1 ○ AF_XDP-based implementations exist 2 , but they move crypto to user-space. Our goal: crypto stays in-kernel via bpf_crypto ● Programmable data plane ○ wireguard.ko is monolithic ○ eBPF allows per-packet decisions: selective encryption, custom routing, integration with observability ● Learn bpf_crypto by building [1] https://cocalc.com/github/torvalds/linux/blob/master/tools/testing/selftests/bpf/progs/crypto_bench.c [2] https://cndp.io/guide/sample_app_ug/WireGuard.html
Ideal Architecture
Ideal Architecture ● Layer 1 User-space Control Plane Noise handshake Curve25519, derives symmetric session keys. ● Layer 2: eBPF SYSCALL program (sleepable) Receives keys, calls bpf_crypto_ctx_create(), stores crypto context in BPF Map. ● Layer 3: eBPF XDP/TC Ingress Parses incoming WireGuard UDP packets, decrypts payload via bpf_crypto_decrypt(). ● Layer 4: eBPF TC Egress Encrypts outgoing packets via bpf_crypto_encrypt(), adds WireGuard/UDP/IP headers.
The Blocker: AEAD Not Supported ● WireGuard requires: ChaCha20Poly1305 AEAD ● bpf_crypto provides: skcipher only (unauthenticated stream/block ciphers) ○ ● We only have crypto/bpf_crypto_skcipher.c 1 No AEAD patch exists on LKML as of March 2026. ○ Confirmed with a committer of bpf_crypto. [1] See https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/crypto .
What We Compromised for the PoC Dropped ● Poly1305 authentication AEAD ● WireGuard protocol framing ● Key rotation ● WireGuard client compatibility Kept ● In-kernel encryption/decryption via bpf_crypto ● TC-based packet interception ● Symmetric key provisioning via SYSCALL program ● Real packet processing
What We Compromised for the PoC No key rotation No authentication No authentication
PoC Setup
PoC Env ● Linux kernel 6.12+ with bpf_crypto support ○ ● Go 1.24 ○ ● tested on 6.17.014-generic amd64 use ebpf-go for userspace implementation Clang (for BPF compilation)
Plaintext / Ciphertext Comparison
13:01:39.433769 IP 10.0.0.1 > 10.0.0.2: ICMP echo request, id 25785, seq 1, length 64
0x0000: 4500 0054 463f 4000 4001 e067 0a00 0001 E..TF?@[email protected]....
0x0010: 0a00 0002 0800 1147 64b9 0001 a323 ba69 .......Gd....#.i
0x0020: 0000 0000 5f9e 0600 0000 0000 1011 1213 ...._...........
0x0030: 1415 1617 1819 1a1b 1c1d 1e1f 2021 2223 .............!"#
0x0040: 2425 2627 2829 2a2b 2c2d 2e2f 3031 3233 $%&'()*+,-./0123
0x0050: 3435 3637
plaintext
------------------------------------------------------------------------------------12:59:50.716750 IP 10.0.0.1 > 10.0.0.2: ICMP type-#36, length 64
0x0000: 4500 0054 21fa 4000 4001 04ad 0a00 0001 E..T!.@.@.......
0x0010: 0a00 0002 24a3 e2a0 7ccd 1c1f 4b9c 652c ....$...|...K.e,
0x0020: c01d 33cc 2528 1a64 7b50 5a66 70fb 43e5 ..3.%(.d{PZfp.C.
0x0030: e764 fdcb 69e9 9913 1554 a427 b1dd 7bed .d..i....T.'..{.
0x0040: 4581 093c 0c86 3b68 671d 0b01 31ea 158b E..<..;hg...1...
0x0050: afe3 f913
ciphertext
IP header stays cleartext — routing still works
13:01:39.433769 IP 10.0.0.1 > 10.0.0.2: ICMP echo request, id 25785, seq 1, length 64
0x0000: 4500 0054 463f 4000 4001 e067 0a00 0001 E..TF?@[email protected]....
0x0010: 0a00 0002 0800 1147 64b9 0001 a323 ba69 .......Gd....#.i
0x0020: 0000 0000 5f9e 0600 0000 0000 1011 1213 ...._...........
0x0030: 1415 1617 1819 1a1b 1c1d 1e1f 2021 2223 .............!"#
0x0040: 2425 2627 2829 2a2b 2c2d 2e2f 3031 3233 $%&'()*+,-./0123
0x0050: 3435 3637
plaintext
-------------------------------------------------------------------------------------12:59:50.716750 IP 10.0.0.1 > 10.0.0.2: ICMP type-#36, length 64
0x0000: 4500 0054 21fa 4000 4001 04ad 0a00 0001 E..T!.@.@.......
0x0010: 0a00 0002 24a3 e2a0 7ccd 1c1f 4b9c 652c ....$...|...K.e,
0x0020: c01d 33cc 2528 1a64 7b50 5a66 70fb 43e5 ..3.%(.d{PZfp.C.
0x0030: e764 fdcb 69e9 9913 1554 a427 b1dd 7bed .d..i....T.'..{.
0x0040: 4581 093c 0c86 3b68 671d 0b01 31ea 158b E..<..;hg...1...
0x0050: afe3 f913
ciphertext
tcpdump can't even identify the packet type
13:01:39.433769 IP 10.0.0.1 > 10.0.0.2: ICMP echo request, id 25785, seq 1, length 64
0x0000: 4500 0054 463f 4000 4001 e067 0a00 0001 E..TF?@[email protected]....
0x0010: 0a00 0002 0800 1147 64b9 0001 a323 ba69 .......Gd....#.i
0x0020: 0000 0000 5f9e 0600 0000 0000 1011 1213 ...._...........
0x0030: 1415 1617 1819 1a1b 1c1d 1e1f 2021 2223 .............!"#
0x0040: 2425 2627 2829 2a2b 2c2d 2e2f 3031 3233 $%&'()*+,-./0123
0x0050: 3435 3637
plaintext
-------------------------------------------------------------------------------------12:59:50.716750 IP 10.0.0.1 > 10.0.0.2: ICMP type-#36, length 64
0x0000: 4500 0054 21fa 4000 4001 04ad 0a00 0001 E..T!.@.@.......
0x0010: 0a00 0002 24a3 e2a0 7ccd 1c1f 4b9c 652c ....$...|...K.e,
0x0020: c01d 33cc 2528 1a64 7b50 5a66 70fb 43e5 ..3.%(.d{PZfp.C.
0x0030: e764 fdcb 69e9 9913 1554 a427 b1dd 7bed .d..i....T.'..{.
0x0040: 4581 093c 0c86 3b68 671d 0b01 31ea 158b E..<..;hg...1...
0x0050: afe3 f913
ciphertext
Predictable padding → indistinguishable from random
13:01:39.433769 IP 10.0.0.1 > 10.0.0.2: ICMP echo request, id 25785, seq 1, length 64
0x0000: 4500 0054 463f 4000 4001 e067 0a00 0001 E..TF?@[email protected]....
0x0010: 0a00 0002 0800 1147 64b9 0001 a323 ba69 .......Gd....#.i
0x0020: 0000 0000 5f9e 0600 0000 0000 1011 1213 ...._...........
0x0030: 1415 1617 1819 1a1b 1c1d 1e1f 2021 2223 .............!"#
0x0040: 2425 2627 2829 2a2b 2c2d 2e2f 3031 3233 $%&'()*+,-./0123
0x0050: 3435 3637
plaintext
-------------------------------------------------------------------------------------12:59:50.716750 IP 10.0.0.1 > 10.0.0.2: ICMP type-#36, length 64
0x0000: 4500 0054 21fa 4000 4001 04ad 0a00 0001 E..T!.@.@.......
0x0010: 0a00 0002 24a3 e2a0 7ccd 1c1f 4b9c 652c ....$...|...K.e,
0x0020: c01d 33cc 2528 1a64 7b50 5a66 70fb 43e5 ..3.%(.d{PZfp.C.
0x0030: e764 fdcb 69e9 9913 1554 a427 b1dd 7bed .d..i....T.'..{.
0x0040: 4581 093c 0c86 3b68 671d 0b01 31ea 158b E..<..;hg...1...
0x0050: afe3 f913
ciphertext
Technical Challenge 1 — What cipher can you actually use? ● `bpf_crypto_ctx_create(type="skcipher", algo="ctr(aes)")` → returns EINVAL ○ Why? bpf_crypto internally calls crypto_alloc_lskcipher() — the newer "linear" skcipher API ● ○ ctr(aes) is listed in /proc/crypto as skcipher, but has no lskcipher implementation ○ /proc/crypto lies to you. Only ecb(aes) and cbc(aes) actually work. ○ This is completely undocumented We ended up on cbc(aes) — IV-based block chaining, avoids ECB's deterministic-block weakness ● Also: IV must live in map memory, not on the stack — verifier rejects frame pointer for bpf_dynptr_from_mem [1] https://cocalc.com/github/torvalds/linux/blob/master/tools/testing/selftests/bpf/progs/crypto_bench.c
Technical Challenge 2 — The verifier battle ● // ❌ Verifier sees payload_len could be 0, then negative ● payload_len = skb→len - 34; ● payload_len & 15u; ● // ✅ One mask to rule them all ● payload_len = (skb→len - HDR_LEN & 0x7f0; ● `0x7f0` does 3 things in 1 instruction: ● Aligns to 16 bytes (low 4 bits cleared) ● Caps at 2032 (only bits 410 survive) ● Forces non-negative (result always 0, 2032 ● Takeaway: bound with AND, not with if-else. The verifier trusts bitmasks completely. [1] https://cocalc.com/github/torvalds/linux/blob/master/tools/testing/selftests/bpf/progs/crypto_bench.c
Technical Challenge 2 — The verifier battle (by Claude) Prompt Claude Code to compile the BPF object and test-load with bpftool to see verifier output without going through the full Go loader: [1] https://cocalc.com/github/torvalds/linux/blob/master/tools/testing/selftests/bpf/progs/crypto_bench.c
Technical Challenge 3 — The scratch-bounce pattern ● You'd expect: create a dynptr pointing at skb payload offset 34, encrypt in-place ● You can't. bpf_dynptr_from_skb = whole packet, bpf_dynptr_from_mem = map/stack only ○ admitted by commiter 2 ● skb → load_bytes → scratch.src → encrypt → scratch.dst → store_bytes → skb ● Two full memcpy per packet, per direction — this is the biggest performance cost [1] https://cocalc.com/github/torvalds/linux/blob/master/tools/testing/selftests/bpf/progs/crypto_bench.c [2] https://lore.kernel.org/bpf/[email protected]/T/
AEAD was designed for but never implemented? ● kernel/bpf/crypto.c [1] ○ @authsize: The length of authentication tag used by algorithm. [1] https://github.com/torvalds/linux/blob/2d1373e4246da3b58e1df058374ed6b101804e07/kernel/bpf/crypto.c
What Would bpf_crypto_aead Unlock? ● Full ChaCha20Poly1305 in TC/XDP WireGuard-compatible data plane becomes feasible ● The framework is ready ○ authsize field, setauthsize callback, pluggable crypto type registration all exist. ● We still have technical challenges for production-ready quality 😭 ○ Key management ○ Nonce management: atomically increment packet counter ○ wireguard packet framing within eBPF program
Takeaways & Future ● bpf_crypto kfuncs work for symmetric stream encryption of real packets in TC programs. ● AEAD is the missing piece for security-critical protocols like WireGuard. ● The gap between "designed for" and "implemented" is an opportunity for upstream contribution. ○ “Feel free to add them, btw.ˮ
Thank You / Q&A Code: https://github.com/NT-marlowe/wireguard-bpf-poc
Appendix