feat(cli): v3.2 multi-hop — per-hop cert, cell padding, 3-hop, CIDR whitelist

Closes the v3.1 unlinkability gap and resists volume/timing correlation:

1) Per-hop client cert (identity-unlinkable hops). [[client.circuit.hops]]
   now accepts {addr, cert_path, key_path, [server_name]} per hop — each
   hop sees a different CN, so a relay and an exit cannot correlate the
   same client by certificate. Old flat `hops = ["ip:port"]` form still
   parses (serde untagged enum) and falls back to [pki] cert/key.
   `aura provision-client --circuit-hops N` mints N fresh UUIDv4 certs.

2) Cell padding. CellPaddingConn wrapper pads every outgoing packet to a
   fixed size (default 1280 bytes; `cell_size = N` configurable) before
   it hits the inner AEAD. Format: u16_be(real_len) || pkt || zero_pad.
   On-wire sizes become constant -> defeats volume/timing fingerprints.
   Opt-in via [client.circuit] cell_padding = true and the mirror
   [server] cell_padding_for_circuit_clients = true.

3) 3-hop support. dial_circuit now accepts N >= 2 hops; iterative
   ExtendBridge nests N-1 forwarders and N handshakes. Client owns the
   full chain via CircuitConnection (forwarders abort on drop).
   New integration test multihop_v3_2_three_hops_end_to_end runs three
   in-process actors (A relay -> B relay -> C exit) on loopback and
   verifies peer_id == C's CN.

4) CIDR whitelist. [server.relay] allow_extend_to entries accept
   "10.0.0.0/24" (subnet, any port), "10.0.0.0/24:443" (subnet + port),
   "[2001:db8::/32]:443" (IPv6 with port), as well as exact IP:port.
   Empty list keeps the v3.1 open-relay (warn).

19 new tests; workspace 276 passed (+19), clippy -D warnings clean, fmt clean.
257 baseline tests untouched; all v2 / v3.1 / LE configs work as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xah30
2026-05-27 20:07:12 +03:00
parent f26ed7fce0
commit 9b98004424
13 changed files with 1768 additions and 298 deletions
+40 -11
View File
@@ -127,17 +127,46 @@ enabled = false
mean_interval_ms = 500
jitter = 0.5
# v3.1 multi-hop / onion routing: dial through an entry-relay before reaching the exit-server.
# When `enabled = true`, the client opens an OUTER Aura UDP connection to `hops[0]` (the relay),
# sends one ExtendBridge envelope describing `hops[1]` (the exit), waits for CircuitReady, and
# then runs an INNER Aura handshake addressed to the exit through that relay — two AEAD layers
# per packet, the exit knows the client's CN but not the source IP, the relay knows the source
# IP but not the destination nor a single plaintext byte. Exactly two hops are required in
# v3.1; configure the relay-server with [server.relay] enabled = true and
# allow_extend_to = ["<this client's exit IP:port>"].
# v3.1 / v3.2 multi-hop / onion routing: dial through 1 or 2 intermediate hops before reaching
# the exit-server. When `enabled = true`, the client opens an OUTER Aura UDP connection to
# `hops[0]` (the entry-relay), sends one ExtendBridge envelope describing the next hop, waits for
# CircuitReady, then either dials the exit directly (2-hop) or repeats the ExtendBridge dance
# through a middle relay (3-hop). The innermost handshake authenticates the EXIT's cert opaquely
# — every relay sees only the next-hop address and AEAD ciphertext.
#
# Omitting the section (or `enabled = false`) keeps the v2 single-hop dial path intact —
# [client] server_addr / [transport] order rules apply as before.
# v3.2 adds:
# * per-hop client certificates (the entry-relay and the exit see DIFFERENT CNs — they cannot
# link the two handshakes by identity), and
# * cell padding (every packet is padded to a constant `cell_size` bytes before sending — the
# exit MUST also enable `[server] cell_padding_for_circuit_clients = true` to decode), and
# * 3-hop support (just add a third [[client.circuit.hops]] table).
#
# Omitting the section (or `enabled = false`) keeps the v2 single-hop dial path intact.
#
# --- v3.1 FLAT FORM (back-compat) — every hop uses the [pki] cert/key above (NOT unlinkable):
# [client.circuit]
# enabled = true
# hops = ["198.51.100.5:443", "203.0.113.10:443"] # [entry_relay, exit_server] — literal IP:port
# hops = ["198.51.100.5:443", "203.0.113.10:443"]
#
# --- v3.2 PER-HOP FORM — each hop has its own cert/key (identity-unlinkable):
# [client.circuit]
# enabled = true
# cell_padding = true
# cell_size = 1280
#
# [[client.circuit.hops]]
# addr = "198.51.100.5:443"
# cert_path = "~/.config/aura/circuit/entry.crt"
# key_path = "~/.config/aura/circuit/entry.key"
#
# [[client.circuit.hops]] # OPTIONAL middle hop for a 3-hop circuit
# addr = "198.51.100.99:443"
# cert_path = "~/.config/aura/circuit/middle.crt"
# key_path = "~/.config/aura/circuit/middle.key"
#
# [[client.circuit.hops]]
# addr = "203.0.113.10:443"
# cert_path = "~/.config/aura/circuit/exit.crt"
# key_path = "~/.config/aura/circuit/exit.key"
#
# Generate per-hop certs in one command: `aura provision-client --circuit-hops 3 ...`
+23 -6
View File
@@ -151,11 +151,28 @@ jitter = 0.5
# Omitting the whole [server.relay] section (or `enabled = false`) keeps the v2 behaviour intact.
# [server.relay]
# enabled = true
# Whitelist of allowed downstream exit addresses. ONLY literal IP:port entries; DNS resolution
# is NOT performed in v3.1 (unparsable entries are logged at WARN and skipped). An empty list
# turns this server into an OPEN relay accepting any downstream — dangerous; the runtime logs
# a WARN on each accepted bridge.
# Whitelist of allowed downstream destinations. v3.2 accepts three entry formats:
# * "IP:port" — exact literal SocketAddr (the v3.1 form).
# * "10.0.0.0/24" — bare CIDR; matches ANY port at any IP in the subnet.
# * "10.0.0.0/24:443" — CIDR with explicit port; matches that port on any IP in the subnet.
# * "[2001:db8::/32]:443" — square-bracket IPv6 CIDR with port.
# * "2001:db8::/32" — bare IPv6 CIDR (any port).
# Unparseable entries are logged at WARN and skipped. An empty list turns this server into an
# OPEN relay accepting any downstream — dangerous; the runtime logs a WARN on each accepted bridge.
# allow_extend_to = [
# "198.51.100.5:443", # the exit you operate
# "203.0.113.10:443",
# "198.51.100.5:443", # the exit you operate (exact)
# "203.0.113.0/24", # a whole /24 of trusted exits (any port)
# "10.0.0.0/16:443", # a /16 of relays on port 443 only
# ]
#
# v3.2 cell padding: opt-in. The relay itself does NOT decode cells — it just forwards bytes.
# These knobs are documented here for symmetry; the actual decode happens on the EXIT (see
# [server] cell_padding_for_circuit_clients below).
# cell_padding = false
# cell_size = 1280
# v3.2 EXIT-side cell padding. When an exit-server serves cell-padded circuit clients (i.e. the
# clients have `[client.circuit] cell_padding = true`), add the following field to the [server]
# block at the top of this file so the inner-handshake session's recv decodes the constant-size
# cells and the send re-pads on the way back. Defaults to `false` for v3.1 compatibility.
# cell_padding_for_circuit_clients = true