|
| 1 | +# v1.9.0-rc2 — Release Notes |
| 2 | + |
| 3 | +**Scope:** 11 commits since `v1.9.0-rc1` (2026-04-24 → 2026-04-27). |
| 4 | + |
| 5 | +The headline of RC2 is **P1-010 closed** (both halves) — the |
| 6 | +tunnel-crypto-desync class of bugs that caused fire-and-forget RPCs |
| 7 | +(task submit, send-results) to silently lose data across rekey windows. |
| 8 | +P1-009 closed as a side effect: the `test_midrekey_*` regression repros |
| 9 | +all pass cleanly now. |
| 10 | + |
| 11 | +## P1-010 — tunnel desync recovery |
| 12 | + |
| 13 | +Four commits, four layers of the fix: |
| 14 | + |
| 15 | +### 1. Salvage replay (`7f76e75`) |
| 16 | + |
| 17 | +Per-peer plaintext ring buffer in `peerCrypto.salvage`. Every |
| 18 | +encrypted send copies the plaintext into the buffer. When |
| 19 | +`handleAuthKeyExchange` (or unauth `handleKeyExchange`) installs a |
| 20 | +fresh `peerCrypto` because the peer's pubkey changed, we re-encrypt |
| 21 | +the recent plaintexts with the new key and re-send via `writeFrame`. |
| 22 | + |
| 23 | +Recovers fire-and-forget RPCs that were sent under stale crypto |
| 24 | +during the rekey window. |
| 25 | + |
| 26 | +Webhook event: `tunnel.desync_salvage` with replay count. |
| 27 | + |
| 28 | +### 2. Rekey retransmit loop (`a126a26`) |
| 29 | + |
| 30 | +Per-peer `pendingRekey` state set on `sendKeyExchangeToNode`, cleared |
| 31 | +on first successful inbound decrypt. New goroutine |
| 32 | +`rekeyRetransmitLoop` retransmits stale entries every 4 s, capped at |
| 33 | +5 retries. Closes the case where our key_exchange or peer's reply was |
| 34 | +dropped on the wire — without this, the next chance to recover was |
| 35 | +the 5-min `RelayProbeInterval`. |
| 36 | + |
| 37 | +Webhook event: `tunnel.rekey_gave_up` if a peer hits the retransmit |
| 38 | +cap (real reachability problem worth investigating). |
| 39 | + |
| 40 | +### 3. Cap salvage at 4 entries (`5de736f`) |
| 41 | + |
| 42 | +Original cap of 32 caused a replay-storm on rekey: the dataexchange |
| 43 | +retransmit layer's churn filled the buffer, and on rekey all 32 |
| 44 | +frames went out in a tight loop. The receiver's freshly-installed |
| 45 | +`peerCrypto` had `maxRecvNonce = 0`; out-of-order arrival of nonces |
| 46 | +1..32 tripped the replay-window check. 4 covers the realistic shape |
| 47 | +(task submit + send-results + a couple of dataexchange retries) |
| 48 | +without overwhelming the receiver. Memory drops from ~48 KiB / peer |
| 49 | +to ~6 KiB; ~6 MiB worst-case fleet-wide at `maxCryptoPeers = 1024`. |
| 50 | + |
| 51 | +### 4. Preserve nonce/replay state across duplicate key_exchange (`d4d5f42`) |
| 52 | + |
| 53 | +The latent companion bug: `handleAuthKeyExchange` and |
| 54 | +`handleKeyExchange` used to **unconditionally replace** `tm.crypto[N]` |
| 55 | +with a freshly-derived `peerCrypto`, even when the peer's pubkey was |
| 56 | +unchanged. Same shared secret, but reset nonce counter and empty |
| 57 | +replay bitmap. Subsequent encrypted sends used counter 1, 2, 3... |
| 58 | +while peer's `pc[us]` had a high `maxRecvNonce` from before, so |
| 59 | +peer dropped them as "outside replay window." |
| 60 | + |
| 61 | +Fix: replace only when there's no existing entry OR the pubkey |
| 62 | +actually changed. Pinned by |
| 63 | +`TestHandleKeyExchangeDuplicatePreservesCryptoState`. |
| 64 | + |
| 65 | +(This same fix was attempted earlier in the session and reverted — |
| 66 | +it was incompatible with the 32-entry salvage cap. After the cap |
| 67 | +reduction in `5de736f`, the preservation fix is safe.) |
| 68 | + |
| 69 | +## Tests added |
| 70 | + |
| 71 | +- `pkg/daemon/tunnel_desync_salvage_test.go` — 7 unit tests for the |
| 72 | + ring buffer (size + age bounds, copy-not-reference, nil-safety). |
| 73 | +- `pkg/daemon/tunnel_rekey_retransmit_test.go` — 9 unit tests for the |
| 74 | + retransmit state machine (mark, clear, stale, give-up cap). |
| 75 | +- `pkg/daemon/tunnel_dup_keyexchange_test.go` — 2 unit tests pinning |
| 76 | + duplicate-preservation and real-rekey-replacement. |
| 77 | +- `tests/integration/local/test_tunnel_desync_recovery.sh` — black-box |
| 78 | + test: establish tunnel, restart receiver, submit task immediately, |
| 79 | + assert it lands within 30 s. |
| 80 | + |
| 81 | +## Verified |
| 82 | + |
| 83 | +| Test | Result | |
| 84 | +|---|---| |
| 85 | +| `go vet ./...` | clean | |
| 86 | +| `go test -parallel 4 ./pkg/... ./tests/` | all 10 packages pass | |
| 87 | +| `test_tunnel_desync_recovery` | 7/0 | |
| 88 | +| `test_midrekey_send_file` | 5/0 (was 4/1, P1-009 repro) | |
| 89 | +| `test_midrekey_send_message` | 6/0 (was 5/1, P1-009 repro) | |
| 90 | +| `test_midrekey_task_results` | 6/0 (was 4/2, P1-009 repro) | |
| 91 | +| `test_midrekey_task_submit` | 5/0 (was 3/2, P1-009 repro) | |
| 92 | +| `test_peer_restarted_send_file` | 4/0 (was 3/1) | |
| 93 | +| `test_peer_restarted_send_message` | 5/0 (was 3/2) | |
| 94 | +| `test_sender_clean_restart_midflight` | 6/0 | |
| 95 | +| `test_chaos_packet_loss` | 9/0 | |
| 96 | +| `test_force_relay_task` | 4/0 | |
| 97 | +| Full integration suite (`btjsk44z8`, -j 8) | 225/232 — best of cycle | |
| 98 | + |
| 99 | +## Other (pre-RC2 but post-RC1) work folded in |
| 100 | + |
| 101 | +- `b4237e3` + `71e5f56` — 30 open-data network blueprints (academic, |
| 102 | + geo, health, news, finance, etc.). Already deployed to production |
| 103 | + registry as IDs 44–73. Open-join, default-allow, full inter-agent |
| 104 | + communication. |
| 105 | +- `e7d9efb` — untracked runtime artifacts that should never have been |
| 106 | + committed (test logs, results); added matching `.gitignore` rules. |
| 107 | +- `aa0cb6a`, `08b3a34`, `c843c4a`, `22092c3` — website / blog content, |
| 108 | + no protocol impact. |
| 109 | + |
| 110 | +## Backwards compatibility |
| 111 | + |
| 112 | +Wire-compatible with `v1.8.0` and `v1.9.0-rc1` daemons. No new packet |
| 113 | +types, no new fields, no version negotiation. Mixed deployments are |
| 114 | +safe. The retransmit loop and salvage replay are visible to peers as |
| 115 | +ordinary key_exchange / encrypted frames respectively. |
| 116 | + |
| 117 | +## Known limitations (unchanged from RC1) |
| 118 | + |
| 119 | +- `test_cli` — env-gated (hits production `agent-alpha`). |
| 120 | +- `test_register_identity_new_endpoint` — architectural (container |
| 121 | + PID-1 dies on `pkill`). Needs Dockerfile init wrapper. |
| 122 | +- Vouching-chain transitive walk unimplemented. |
| 123 | +- Trust links global, not per-network (design choice). |
| 124 | +- `DefaultVerdict` policy field fails OPEN on pre-v1.9 daemons — |
| 125 | + upgrade daemons before pushing configs that rely on it. |
| 126 | + |
| 127 | +## Upgrade |
| 128 | + |
| 129 | +No coordinated rollout needed. Update daemons in any order: |
| 130 | +rendezvous → agents is the recommended sequence purely for the |
| 131 | +`DefaultVerdict` ordering risk. Old daemons keep working unchanged. |
| 132 | + |
| 133 | +## Full commit log |
| 134 | + |
| 135 | +``` |
| 136 | +d4d5f42 Preserve nonce/replay state across duplicate key_exchange |
| 137 | +c843c4a Add blog post: AI agent discovery: master P2P networks in 2026 |
| 138 | +5de736f Cap desync salvage at 4 entries (was 32) to prevent replay storm |
| 139 | +22092c3 Hero: decorative boids flock behind the headline |
| 140 | +a126a26 P1-010 tunnel-state half: retransmit pending key exchanges |
| 141 | +7f76e75 P1-010: salvage in-flight plaintext on peer-initiated rekey |
| 142 | +08b3a34 Add blog post: Overlay networking: Secure AI agent communication explained |
| 143 | +aa0cb6a Add blog post: Top 6 openanp.ai Alternatives 2026 |
| 144 | +71e5f56 Open-data networks: full inter-agent communication; SHIPPED roster |
| 145 | +b4237e3 Add 30 open-data network blueprints |
| 146 | +e7d9efb Untrack runtime artifacts that should never have been committed |
| 147 | +``` |
| 148 | + |
| 149 | +`git log v1.9.0-rc1..v1.9.0-rc2 --oneline` (11 commits). |
0 commit comments