improve PEER_AUTH_PLAN.md and re-organize files

This commit is contained in:
2026-08-09 16:08:01 +02:00
parent fe3c3c6520
commit f4a6259cf3
14 changed files with 36 additions and 71 deletions
-66
View File
@@ -1,66 +0,0 @@
# Streamed Install Next Steps
Id treat the prototype as proof of the hard part: “can we stream
archive-derived install bytes into `local/` without making the receiver a
source?” Yes. Next Id harden the pieces that decide whether this is
product-ready.
1. **Done — Move from CLI-only to real app integration**
The GUI now has an explicit “Low disk install” action in the game detail
modal for remote-only games. The Tauri backend queues that path through
`stream_install_game`, injects the shared external `unrar` stream provider,
and hands fetched file details to `StreamInstallGame` instead of the normal
download command.
2. **Done — Replace per-file `unrar p` with a final archive provider**
The shared external `unrar` stream provider now runs `unrar lt` once for the
archive metadata and one sequential `unrar p` pass per archive for payload
bytes. It frames directories, file starts, file chunks, and file ends from
the technical listing, so CLI and GUI callers use one purpose-built provider
instead of a per-file extraction loop.
3. **Done — Handle solid archives deliberately**
The provider exposes the RAR `solid` flag in `ArchiveBegin` and always uses
one sequential payload pass per archive, which is the safe path for solid
archives. S41 now verifies a real solid RAR fixture through the Docker
peer-cli flow, including local-only final state, absent root archive/sentinel,
byte count, and extracted payload SHA-256 hashes.
4. **Done — Decide the integrity model**
Streamed installs intentionally verify against sender archive metadata for
now: each file must match the RAR-advertised size and CRC32. That catches
transport corruption, truncation, and provider bugs, but does not claim
malicious-peer protection. Trusted content remains a separate catalog schema
step: add catalog-owned archive or extracted-file SHA-256 hashes, then verify
those at the receiver before commit.
5. **Done — Upgrade retry/resume semantics**
Streamed install attempts now use the same majority-validated peer set as
normal downloads, and each failed attempt rolls back its staging transaction
before trying the next peer. S42 pins the policy: retry the whole stream from
another validated peer, keep no partial files across attempts, and do not add
byte-offset resume until there is a strong reason.
6. **Done — Expand scenario coverage**
S43-S47 cover the remaining streamed-install edges: already-installed
rejection, corrupt archive rollback, sender disconnect mid-stream, receiver
cancel mid-stream, and multi-archive `.eti` roots streamed in sorted order.
The peer-cli harness now exposes `cancel-download` so cancellation scenarios
exercise the same runtime path as the GUI.
7. **Done — Clean product semantics**
The UI now keeps streamed installs in the installed visual state while making
the sharing limitation explicit: cards show `Not shareable`, and the detail
modal status shows `Installed, not shareable`. Downloaded-and-installed games
keep the normal `Installed` label.
The remaining production-readiness step is additive: move from sender-owned RAR
metadata to catalog-owned archive or extracted-file hashes, then verify those
at the receiver before committing the streamed install.
@@ -34,7 +34,7 @@ not three partially compatible protocol generations.
| Content authority | Ship BLAKE3 file and chunk hashes from the same bundled catalog authority as `game.db`. | Every eligible nearby peer is usable automatically; wrong bytes are rejected and retried elsewhere. |
| Peer identity | Use one installation-local TLS key and derive `PeerId` from that TLS public key. | Identity works silently and survives ordinary restarts when possible; users do not manage it. |
| Transport | Pin every outbound QUIC connection to the expected `PeerId`. | An address spoof or MITM cannot impersonate the peer selected as a source. |
| Control messages | Use ordinary bounded protocol messages inside TLS. Treat unauthenticated inbound change notifications only as hints that trigger a pinned pull. | No signed-envelope layer, nonce ledger, or message-signing overhead. |
| Control messages | Use ordinary bounded protocol messages inside TLS. Treat unauthenticated inbound change notifications only as hints that trigger a pinned pull, and carry current revisions on the liveness ping that already runs so a lost hint self-heals. | No signed-envelope layer, nonce ledger, or message-signing overhead. |
| Call to Play | Exchange only each peer's own session state by direct pinned pulls; do not relay third-party histories. | Calls are live LAN-party state and disappear naturally as their authors leave. |
| Privacy | Provide one global Local network sharing switch. | Participation is easy to understand; no per-peer policy matrix. |
| Protocol rollout | Make one cutover to the new current protocol. | Mixed versions are explained clearly, without maintaining legacy paths. |
@@ -50,6 +50,9 @@ bundled content manifest -> validated local download plan
local Call to Play change -> cheap invalidation hint to known peers
-> each peer pulls the author's current state over pinned TLS
pinned liveness ping -> responder's own current revisions
-> pull that one responder only on mismatch
```
## 2. Threat model and guarantees
@@ -316,6 +319,31 @@ This extra pull is one small LAN round trip and removes general signed
envelopes, canonical opaque payloads, nonce caches, replay semantics, inbound
client-certificate plumbing, and connect-back authority state.
Hints are a latency optimization, never a correctness requirement. The liveness
ping that already runs is the reconciliation channel: `Pong` carries the
responder's own `(runtime_session_id, library_revision, call_to_play_revision)`.
The initiator compares them against what it has cached for that endpoint
generation and, on any mismatch or a new session ID, schedules exactly the
coalesced pinned pull a hint would have scheduled. A hint that was dropped,
never sent, or discarded by rate limiting therefore converges within one ping
interval, with no new timer, no mDNS payload growth, and no periodic full-state
polling.
Freshness for that check must be measured from the local peer's own last pinned
exchange with the remote peer, never from inbound activity. The current
`ping_idle_peers` skips any peer whose `last_seen` was refreshed by traffic
arriving from it (`update_last_seen_by_addr`), so a peer that keeps talking to
us would suppress the very check that detects our staleness about it. Track the
last completed outbound pinned exchange separately from `last_seen`, which
continues to serve stale-peer pruning. Inbound traffic is not evidence of
freshness, for the same reason it is not evidence of identity.
Revisions on `Pong` are a staleness signal, not content authority; the pull
remains the authoritative step. A responder that inflates its revision only
causes pulls of its own state, bounded by the same coalescing and rate limits. A
responder that understates it leaves the initiator stale about that responder
alone, which it could already achieve by changing nothing.
An unproven address collision never evicts an authenticated peer. If a pinned
dial later proves that a different ID now owns the same address, atomically
replace address ownership and retire the old record only if it still names
@@ -437,9 +465,9 @@ The cutover includes:
- version-bound ALPN and per-installation server certificates instead of the
repository-wide `cert.pem`/`key.pem`;
- mDNS candidate-only semantics and useful incompatible-version telemetry;
- responder-owned pull snapshots plus bounded change hints instead of inbound
state-bearing `Hello`, pushed `LibraryDelta`, and pushed/relayed
`CallToPlayEvents`;
- responder-owned pull snapshots, revision-bearing `Pong`, and bounded change
hints instead of inbound state-bearing `Hello`, pushed `LibraryDelta`, and
pushed/relayed `CallToPlayEvents`;
- cryptographic `content_id` in game availability and catalog-driven chunk
requests;
- canonical forward-slash catalog paths;
@@ -462,7 +490,7 @@ real reusable boundary; a new identity crate is not required by the design.
| `lanspread-proto` | `PeerId`, `PeerEndpoint`, `content_id`, pull snapshots, change hints, author-owned Call-to-Play wire types, and the one protocol version. No crypto or storage logic. |
| `lanspread-peer::identity` | Simple key/certificate load-or-generate, SPKI-derived ID, and test identity injection. |
| `lanspread-peer::network` | Per-endpoint rustls client config, full responder verification, ALPN, and no address-only connect. |
| discovery/handshake/liveness | Candidate-only mDNS, pinned pulls, hint coalescing, endpoint generations, and version-mismatch reporting. |
| discovery/handshake/liveness | Candidate-only mDNS, pinned pulls, hint coalescing, revision reconciliation on ping, endpoint generations, and version-mismatch reporting. |
| `peer_db` | Authenticated endpoint/state records and exact `content_id` source lookup. |
| download/storage/stream install | Validated catalog plan, hash-as-received, source quarantine/retry, sentinel commit, and protected staging. |
| `call_to_play` | Local author slice, per-peer replacement snapshots, simple authority checks, timers, and bounds. |
@@ -529,6 +557,9 @@ This phase is not complete until:
CLI dial rejects the wrong key at the expected address;
- a forged mDNS record or inbound hint cannot create/rebind/remove peer state,
inject a library/Call-to-Play update, or bypass a pinned pull;
- a change hint that is dropped, never sent, or rate-limited away still
converges within one ping interval, and inbound traffic from a peer does not
defer that peer's revision check;
- an honest multi-source download swarms automatically and commits only the
catalog bytes;
- one bad source is quarantined and another source completes the chunk;