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. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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 local Call to Play change -> cheap invalidation hint to known peers
-> each peer pulls the author's current state over pinned TLS -> 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 ## 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 envelopes, canonical opaque payloads, nonce caches, replay semantics, inbound
client-certificate plumbing, and connect-back authority state. 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 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 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 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 - version-bound ALPN and per-installation server certificates instead of the
repository-wide `cert.pem`/`key.pem`; repository-wide `cert.pem`/`key.pem`;
- mDNS candidate-only semantics and useful incompatible-version telemetry; - mDNS candidate-only semantics and useful incompatible-version telemetry;
- responder-owned pull snapshots plus bounded change hints instead of inbound - responder-owned pull snapshots, revision-bearing `Pong`, and bounded change
state-bearing `Hello`, pushed `LibraryDelta`, and pushed/relayed hints instead of inbound state-bearing `Hello`, pushed `LibraryDelta`, and
`CallToPlayEvents`; pushed/relayed `CallToPlayEvents`;
- cryptographic `content_id` in game availability and catalog-driven chunk - cryptographic `content_id` in game availability and catalog-driven chunk
requests; requests;
- canonical forward-slash catalog paths; - 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-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::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. | | `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. | | `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. | | 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. | | `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; CLI dial rejects the wrong key at the expected address;
- a forged mDNS record or inbound hint cannot create/rebind/remove peer state, - 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; 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 - an honest multi-source download swarms automatically and commits only the
catalog bytes; catalog bytes;
- one bad source is quarantined and another source completes the chunk; - one bad source is quarantined and another source completes the chunk;