summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_quic_transport.py
Commit message (Collapse)AuthorAgeFilesLines
* feat(node): several named roots per group, and one implementation per operationChristophe Besson2026-08-181-12/+13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Stage A — a group's content is a set of named roots --------------------------------------------------- `shared_dir` becomes a list of {name, path, kind}. The name is the directory's basename, derived once at add time and *stored*: recomputing it would re-identify a whole library the day someone renames a folder on disk. Duplicate names are refused case-insensitively and no root may contain another — both compared with NFC folding, because most of these directories live on exFAT or NTFS where `Films` and `films` are one directory. Every index path carries its root name, in a one-root group as much as in a five-root one. One path shape has to be got right once; two have to be kept right for ever. **A root that goes away freezes; it never empties.** Unmounting a volume makes watchdog report every file under it as deleted, or presents an empty directory to the next scan. Acting on either propagates deletions for a whole library to every member, as though the owner had erased it. So a deletion is acted on only once its root is confirmed readable, and availability is tracked per root — one unplugged drive leaves the others serving. 12 tests, verified to fail against an indexer without the check. Events are not trusted to be complete either: ReadDirectoryChangesW drops them under load and inotify on a FUSE mount misses changes made outside it. A periodic reconciliation sweep is the only thing that recovers a missed event. MNP 0.2 → 0.3 (additive). The hub needs no change: SwarmSource carries a content hash, a node id and an endpoint — no paths, no filenames — and private groups register nothing (H7). Stage B — one implementation behind every front door ---------------------------------------------------- C1 and C6 were both "a second path into the node with its own weaker handshake". Two implementations of `revoke` with two authorization checks is that shape one size down. `meshbay_node/ops.py` holds each operation once, takes the daemon state, and knows nothing about HTTP, argv or MNP. The loopback API is one `_op(...)` line per endpoint; the MNP handlers call the same functions. test_ops.py asserts the shape rather than trusting it. Phase 14 is finished on top of it — `group list`, `gek init|rotate`, `reload` (SIGHUP), `denylist show|clear`, `file list|rm`. **No operator action requires a browser any more.** Plus `gek_rotate` and `member_unpin` as operator-signed MNP operations: rotation is the half of revocation that revocation cannot do, since the ex-member holds the current key, and the node generates the replacement with its own CSPRNG — no key material crosses the wire, which is what the C5b rule is actually about. Two bugs found by running it rather than by testing it ------------------------------------------------------ GroupIndex is keyed by **content hash**, so the same bytes at two paths are one entry — which is also why a scan reports ten files and indexes nine. Reconciliation compared paths, so it decided the second path was a missed event every 60 s, rewrote the entry and pushed an index update to every connected peer. Seen in a live node's log. `meshbay-node reload` crashed on first use with `subprocess` unimported: the module compiles fine, which is the "syntax, not names" trap already recorded for the SPA. test_cli_dispatch.py now walks every verb and refuses to let one be added to the parser without an entry there. Also corrected: protocol.py declared a second MNP_VERSION of "0.1" while the wire carried "0.2" — harmless only because nothing imported it. And _do_dir_create/_do_dir_delete referenced an undefined `filename` on their error path. 740 tests pass; QE/deploy/e2e.py passes end to end against the live deployment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(quic): GEK proof and mutual authentication — closes C6Christophe Besson2026-08-131-0/+4
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Phase 11.5.4/5/6 — finding C6, the last open critical finding. QUIC ran a JWT-only handshake: a forged or stolen token reached the node and could inject chat without ever holding the group key. It now runs the same challenge/response as WebRTC through meshbay_common.handshake — client nonce, role-bound length-prefixed transcript, GEK proof, and the node proving itself with a GEK proof plus an Ed25519 signature over the transcript (C3). 11.5.6 channel binding, resolved by spike and then by two findings the spike could not predict: * aioquic 1.3.0 exposes no RFC 5705 exporter, and the peer certificate only via a private attribute. The server reads its own certificate from disk, so no internals are touched on that side; the client's access is guarded and fails loudly if an upgrade moves it. * A RESUMED TLS session carries no certificate — aioquic does not re-send it, so there is nothing live to bind to. The anchor therefore travels with the session ticket, which is sound because the ticket is cryptographically derived from the handshake where that certificate was presented. * The anchor had to travel with the ticket rather than live on the client object: resumption constructs a fresh client, so an instance-level cache was silently useless. Caught by the resumption test, not by inspection. Both paths refuse rather than degrade. No certificate and no cached anchor means the handshake fails; it never falls back to an unbound proof, which would silently drop MitM detection (L4). QuicChunkClient gains a peer_cert_der property and constructor argument, mirroring how session_ticket is already carried by the caller. Tests: 9 quic/multi-group, full node+common suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(quic): share the unified handshake authorizationChristophe Besson2026-08-131-1/+8
| | | | | | | | | | | | | | | | | | | | | | | | | | Phase 11.5.4 — QUIC half. Findings M1, M9 on this transport. quic_server._do_handshake_sync was a second, weaker copy of the WebRTC logic: group_id was optional, so omitting it skipped the membership check entirely and fell back to the node's first group (M1); node-scoped daemon tokens were accepted as client tokens (M9); and the checks could drift from the WebRTC path independently, which is how they diverged in the first place. Authorization now comes from meshbay_common.handshake, shared with WebRTC. C6 IS STILL OPEN ON THIS TRANSPORT. There is no GEK proof here yet: a forged or stolen token still reaches the node over QUIC and can inject chat without holding the group key. What remains is the challenge/response and the mutual node proof — quic_binding() is written and unit-tested for exactly this, and 11.5.6 (whether a certificate hash is the right anchor, or an RFC 5705 exporter is reachable from aioquic) is still unproven. This commit narrows the gap to the proof itself; it does not close the finding. QUIC tests updated: default tokens are members of the test group, and clients pass group_id, since it is mandatory now. Tests: 9 quic/multi-group, full node+common suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: Phase 7 — Node v2 (multi-group, Sender Keys, 0-RTT, chat, denylist)Christophe Besson2026-08-101-2/+132
| | | | | | | | | | | | | | | | | | | | | | | | | | | Implements all 8 milestones (7.0-7.7): - 7.0: JWT carries `groups` claim; node verifies group membership at MNP handshake (QUIC + TCP+TLS). Resolves security review C2. - 7.1: QUIC 0-RTT session resumption via stored session tickets (17-21ms reconnect vs 47ms cold). - 7.2: Hub→node WebSocket signaling for NAT punch coordination (`client_incoming`/`punch_ready`) + jti denylist push. Denylist class blocks revoked users/jtis at handshake. - 7.3: Multi-group daemon — one QUIC port serves N groups with per-group GEK, shared_root, and index routing. - 7.4: HLS streaming via QUIC (STREAM_SEGMENT message type, ffmpeg segment extraction). - 7.5: Sender Keys protocol for group chat (Signal Groups approach). Each member has own sending chain key, HKDF chain ratchet, AES-256-GCM encryption, Ed25519 signing. Resolves security review C1. - 7.6: Chat store (SQLite via aiosqlite), CHAT_MESSAGE MNP wire type with peer broadcast, web UI with WebSocket push. - 7.7: Argon2id calibration CLI. First security review included (first-review.md). 109 tests, demo-v3 validated against meshbay.org production hub. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(node): add QUIC transport (MNP v2) — quic_server + quic_clientChristophe Besson2026-08-091-0/+157
QuicChunkServer/QuicChunkClient: same MNP protocol over QUIC/UDP. Enables hole-punching (Spike 4 Cone NAT validated). Uses aioquic 1.3.0. Bug found+fixed: asyncio.Event race condition in client recv loop (quic_event_received overwrote _stream_events[0] after _recv created it). Fixed with asyncio.Queue (no shared mutable state). Server uses synchronous handlers in quic_event_received (avoids ensure_future transmit timing issue). 3/3 tests. Full suite: 50/50. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>