Skip to content

perf: cache the DISCO X25519 shared secret - #31

Open
kedimuzafer wants to merge 1 commit into
CamM2325:mainfrom
kedimuzafer:perf/disco-x25519-cache
Open

perf: cache the DISCO X25519 shared secret#31
kedimuzafer wants to merge 1 commit into
CamM2325:mainfrom
kedimuzafer:perf/disco-x25519-cache

Conversation

@kedimuzafer

Copy link
Copy Markdown

Problem

nacl_box() and nacl_box_open() both call nacl_box_beforenm() internally, which is an X25519 scalar multiplication. DISCO calls them once per packet, in each direction.

On an ESP32-S3 at 240 MHz that scalar multiplication is 30–40 ms. Timestamps from the device's own log, processing one 110-byte DISCO PONG:

I (161618) ml_wg_mgr: DISCO RX: 110 bytes via direct, disco_key=ff986233
I (161648) ml_wg_mgr: DISCO PONG from mert-1: RTT=93 ms (via direct)

30 ms between receiving the packet and finishing with it. And the periodic probe round over a 15-peer tailnet:

I (161603) ml_wg_mgr: disco_periodic_probes: 116 ms

That runs every second, on the same task that drains wg_rx_queue. The queue is 8 deep and the enqueue is non-blocking, so every WireGuard packet arriving inside those 113 ms was dropped without a trace. That was enough to keep TCP through the tunnel from ever getting going.

Change

The node's own disco secret key is fixed for its lifetime, so the shared key depends only on the peer's public key. Adds nacl_box_shared_get() — a 24-slot memo keyed on the peer public key, replaced round-robin — and routes the four DISCO call sites in ml_wg_mgr.c through it plus the existing nacl_box_afternm() / nacl_box_open_afternm() primitives, which are already exported and already take a precomputed key.

Cost: 64 bytes per slot, 1.5 KB of .bss, no allocation, no change to the wire format or to the crypto itself.

After

  • disco_periodic_probes: ~113 ms → under 1 ms per round
  • DISCO PONG handling: 30–40 ms → negligible
  • Tunnel RTT on a LAN peer: 20–235 ms (mdev 58 ms) → 6.2 ms (mdev 0.8 ms)

A miss costs exactly what the old path cost, so worst case is unchanged.

Measured together with #30; the two are independent and can land in either order.

nacl_box()/nacl_box_open() derive the shared key with nacl_box_beforenm() on
every call, which is an X25519 scalar multiplication. Measured on an
ESP32-S3 at 240 MHz that is 30-40 ms, and DISCO does one per packet in each
direction.

The node's own disco secret key never changes at runtime, so the result
depends only on the peer's public key. Adds nacl_box_shared_get(), a 24-slot
memo keyed on the peer public key, and routes the four DISCO call sites in
ml_wg_mgr.c through it plus the existing _afternm() primitives.

Effect on a 15-peer tailnet: disco_periodic_probes() went from ~113 ms per
one-second round to under a millisecond, and processing a single 110-byte
DISCO PONG from 30-40 ms to negligible. Those 113 ms rounds ran on the same
task that drains the WireGuard receive queue, so the queue overflowed behind
them and tunnelled TCP could not stay up.

64 bytes of state per slot, 1.5 KB total, no allocation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqLmUG7wJ7p9ya2Vp4A9J
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant