Record the completed protocol-8 implementation, its security and lifecycle decisions, and the final local acceptance evidence. Mark protocol-7 Call to Play relay reviews as historical so they cannot be mistaken for current design. Keep production acceptance honest by recording the unavailable canonical 186-game manifest corpus, real Windows/NTFS confinement and durability proof, and representative physical-LAN evidence as external release prerequisites. Test Plan: - `just fmt` (passed) - `just test` (passed; 708 workspace tests, including peer 480 and Tauri 56) - `just clippy` (passed) - `just frontend-test` (passed; 91/91) - `just build` (passed; fixture-backed no-bundle build) - `LANSPREAD_S37_MIN_MIB_PER_S=100 just peer-cli-tests` (passed; S1-S49) - `git diff --cached --check` (passed)
36 KiB
Pragmatic LAN safety, peer identity, and content integrity
Status
The code and local test surface described by Phases 1-5 are implemented in this
checkout. The standard Rust, Tauri, and frontend gates pass, and the current
fresh-image S1-S49 Docker matrix is recorded in
organize/testing/PEER_CLI_SCENARIOS.md. This is not yet full
production-release acceptance because three external evidence gates remain:
- the canonical 186-game package corpus and production manifests are absent, so the production catalog and bundle gates remain fail-closed;
- no run on a supported Windows/NTFS system yet proves reparse-point confinement and file/directory durability; and
- no representative physical-LAN three-peer and throughput run has been recorded. Docker-host throughput is local acceptance evidence only.
This plan deliberately treats Lanspread as what it is: a desktop utility for friends and other attendees at a LAN party to discover each other, share a known game catalog at LAN speed, and coordinate a match. It is not an account system, a global untrusted file-sharing network, or a device-administration product.
The normal user journey must remain:
- Open Lanspread.
- See nearby people and their available games automatically.
- Click Download or Stream Install without approving every source.
- Let Lanspread swarm from matching peers and verify the result itself.
- Use Call to Play while those people are present.
Security mechanisms in this plan are automatic. There are no key backup dialogs, trust ceremonies, fingerprint prompts, or per-device download permissions in the normal UI.
The project still has one current wire version and no compatibility shims. The wire changes below are developed together and activated with one protocol bump, not three partially compatible protocol generations.
1. Product and architecture decisions
| Area | Decision | User-visible result |
|---|---|---|
| Filesystem safety | Validate the complete destination manifest before any mutation and confine it to one catalog game root. | A hostile peer cannot overwrite another game, local/, saves, or transaction state. |
| 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, 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. |
The resulting data flow is intentionally small:
mDNS candidate -> responder-pinned TLS -> peer-owned snapshot or file bytes
bundled content manifest -> validated local download plan
-> BLAKE3 check for every received chunk
-> version.ini commit only after complete success
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
Assume a hostile device can join the same LAN, advertise arbitrary mDNS data, send arbitrary protocol messages, occupy reused IP addresses, and run a modified Lanspread build. The attacker does not control the victim's OS, the installed Lanspread application, or its bundled catalog files.
After this plan:
- a remote description cannot make Lanspread create, truncate, or delete a path outside the requested catalog game's download-owned area;
- a selected responder must prove possession of the TLS private key whose public
key derives the expected
PeerId; - mDNS, IP addresses, display names, and inbound notification bodies never become identity authority by themselves;
- a source cannot make a download commit bytes that differ from the hashes in the victim's bundled catalog, even if that source is the only peer present;
- corrupt sources are removed from the current transfer automatically rather than presented to the user as a trust decision; and
- one peer cannot publish Call-to-Play actions as another peer or mutate another peer's author-owned state.
The following are explicit non-goals:
- A
PeerIddoes not prove a human name. Display names remain friendly labels. - The installation key is not a user account and has no promised continuity across OS reinstall, application-data deletion, or copying the application to another computer.
- Content hashes prove that bytes match the bundled catalog. They do not prove that the catalog publisher's game is benign, licensed, or malware-free.
- A hash advertised by the same peer that sends the bytes is not trusted. The expected hash must come from the local bundled catalog.
- When Local network sharing is enabled, nearby devices may browse and request shared catalog content. Per-device admission and requester blocking are not part of this product model.
- Basic frame, connection, and work limits are required, but internet-scale Sybil resistance and Byzantine convergence are not goals for a LAN-party utility.
- Peers on another protocol version do not interoperate. The UI explains the mismatch instead of adding a legacy protocol path.
3. Normative design
3.1 Confine download preparation first
This was implemented first because it fixed a live local data-loss path without depending on authentication or a wire change.
The peer core constructs a ValidatedDownloadManifest before
begin_version_ini_transaction, prepare_game_storage, directory creation,
file creation/truncation, preallocation, or cleanup. Storage functions accept
that validated type, never raw GameFileDescription values from Tauri or a
peer.
Validation is for the complete list and fails without any mutation. It must:
- require a known catalog
game_idand resolve every destination relative to exactly<games_folder>/<game_id>; - use one canonical forward-slash relative-path form and reject empty, absolute,
drive-qualified, UNC, NUL,
.,.., mixed-separator, and non-normalized paths; - reject duplicate paths, file/directory conflicts, and platform aliases such as Windows case, trailing-dot/space, device-name, and alternate-data-stream collisions;
- reject
local/,.local.*, download/install intent state, legacy state, scratch sentinels, and every other path owned by installation or recovery, while allowing the intended rootversion.ini; - require the game root to be one direct non-symlink child of the configured games directory and avoid following symlink or reparse components while opening destinations;
- enforce descriptor-count, individual-size, and aggregate-size limits; and
- require the exact root/file shape needed for a complete downloadable game,
including one regular root
version.ini.
The Tauri command supplies only the selected game_id. The peer core chooses
the complete authoritative plan. A UI-echoed file list is never authority.
After a successful complete transfer, remove download-owned files absent from
the authoritative manifest before committing version.ini. Preserve local/,
install staging/backup state, and user-owned files in all success, failure,
cancellation, and recovery paths.
Historically, this validator contained protocol-7 remote descriptions through a
narrow adapter that required and removed exactly one matching leading game_id/
component. The current protocol constructs the same validated type directly from
the bundled content manifest; remote descriptions no longer define local paths.
Required proof includes hostile descriptors placed after valid descriptors,
cross-game paths, both requested and other-game local/ sentinels, reserved
paths, duplicates and aliases, symlink/reparse destinations, oversized lists,
and stale download-owned files. Every rejection must prove zero filesystem
mutation.
3.2 Make the bundled catalog the content authority
game.db is already the application's authority for game identity and version.
Add reproducibly generated per-game companion manifest artifacts, located at
manifests/<game_id>.json (loaded on-demand when downloading or serving a
game), and package them with both the desktop application and peer-CLI fixtures.
For each supported (game_id, game_version), the manifest artifact contains:
CatalogContentManifest {
schema_version
game_id
game_version
chunk_size
files: [
{ canonical_path, kind, size, file_blake3, chunk_blake3[] }
]
streamed_install_files: [
{ canonical_path, kind, size, file_blake3 }
]
content_id
}
Entries are sorted by canonical path. content_id is BLAKE3 over a versioned,
length-delimited encoding of all preceding manifest fields and hashes, excluding
the content_id field itself; it is not the current noncryptographic
u64 manifest_hash. Golden tests freeze that encoding. The ordinary chunk size
matches Lanspread's 128 MiB transfer chunk.
The catalog publishing workflow must generate these per-game manifests from the canonical game packages, verify them by rereading the packages, and fail the application build/release if a downloadable catalog entry lacks one. Runtime peer consensus and “the only peer said this hash” are not substitutes for this artifact. If real package inputs are unavailable during development, fixture manifests may prove the code path, but the phase is not complete for production games.
Peers advertise only that they can serve a catalog content_id. A peer counts
as a source for the local catalog game only when its advertised ID exactly
matches the receiver's expected ID. The receiver builds paths, sizes, chunks,
and expected hashes entirely from its local catalog manifest. This replaces
remote manifest selection and majority-by-file-size consensus.
For ordinary downloads:
- Select every currently reachable peer advertising the expected
content_id; there is no approval prompt. - Carry
PeerEndpoint { peer_id, addr }andcontent_idthrough planning, swarming, progress, and retry. - The sender serves only an exact catalog file/range for the requested
(game_id, content_id)and applies the same canonical/reserved-path policy before opening a local file. A caller-supplied path can never exposelocal/or another local file. - Hash each chunk with BLAKE3 while receiving it and compare it before marking that chunk complete. Exact length, offset coverage, and the catalog file shape are also mandatory.
- On mismatch, invalidate that write, quarantine that
(PeerId, content_id)for the current runtime/transfer, and retry the chunk from another matching peer. Do not create durable “trust” state. - Commit
version.inionly after every catalog entry and chunk has completed successfully. Failure leaves the game non-downloadable/non-installable and preserveslocal/.
No background disk-scanning or pre-hashing of existing files is required. Chunks are verified strictly as they stream in during an active transfer.
Streamed install needs a catalog-owned extracted-file manifest because the sender controls both today's RAR CRC32 metadata and extracted bytes. The receiver accepts exactly the expected path set, sizes, and BLAKE3 values in isolated staging, then applies the documented local account/language rewrite and promotes the transaction. CRC32 may remain as an early corruption check, but it is not the security boundary. A game without a verified extracted manifest does not offer Stream Install; there is no unverified fallback or warning-through button.
Hashing is performed in the existing streaming I/O path. The acceptance gate measures end-to-end throughput on the standard LAN workload and avoids a second full read when complete chunk coverage already proves the file bytes.
3.3 Use a simple installation-local TLS identity
The identity exists to bind a live peer and its changing address to TLS. It is not exposed as a user credential.
- Generate one self-issued TLS certificate/key pair in Tauri's
app_data_dir()and store it in one versioned application file with restrictive permissions where the platform supports them. - Prefer Ed25519 if the selected s2n-quic rustls provider supports the complete responder-verification path. Otherwise use one supported P-256 TLS key. Do not add a second signing identity or a custom certificate-extension binding.
- Define
PeerIdas lowercase unpadded base32 ofBLAKE3(canonical DER SubjectPublicKeyInfo)from the actual TLS key. The same key is therefore both the identity and the TLS proof-of-possession key. - Validate on load that the private key, certificate SPKI, and derived ID agree. Never log private material.
- A valid file is reused. A missing or corrupt file is regenerated automatically (quarantining corrupt bytes best-effort) and produces at most a diagnostic log entry. If persistence is unavailable, use a fresh in-memory identity for that run and show a non-blocking diagnostic; LAN functionality should not become a repair wizard.
- The peer CLI may accept an explicit deterministic identity file/seed for repeatable tests. It does not probe keyrings or share a default container identity accidentally.
There is no OS-keyring backend, sidecar/backend reconciliation, migration intent, identity lease, encrypted backup, import, reset, clone warning, or continuity repair UI. If application data is lost, the installation simply appears as a new nearby peer. Because authorization is not attached to the old ID, nothing security-sensitive needs migration.
3.4 Pin responders and make remote state pull-only
Every outbound operation accepts a first-class endpoint:
struct PeerEndpoint {
peer_id: PeerId,
addr: SocketAddr,
}
This endpoint is carried through discovery handshake, library refresh, Call-to-Play refresh, metadata/content requests, chunk plans, retries, streamed install, healing, liveness, and direct peer-CLI operations. Delete address-derived IDs, unique-IP identity fallbacks, and address-only connects.
mDNS supplies bounded candidates containing
(peer_id, addr, protocol, revision hints). It may cause a dial, but it never
directly creates or updates authenticated peer/library/Call-to-Play state. A
candidate becomes a peer only after a successful outgoing TLS connection to its
advertised address proves the expected PeerId.
Use TLS 1.3 with a self-issued certificate. The custom client verifier must:
- parse only the selected certificate/SPKI shape;
- derive the
PeerIdfrom that SPKI and compare the full value with the endpoint's expected ID; and - perform real TLS 1.3 CertificateVerify validation under the presented key.
The load-bearing negative test presents peer A's certificate/SPKI with peer B's private key and requires the handshake to fail. Also reject a different valid peer at a reused address. Use one version-bound ALPN, disable 0-RTT, and start without TLS session resumption so every short-lived connection performs the simple full proof.
The protocol is deliberately responder-authenticated rather than wrapping every message in a signature:
- Requests that read public library/content state may be made by any LAN client while Local network sharing is enabled.
- A response is authoritative only to the initiator that connected using the
expected
PeerEndpoint; the TLS channel supplies integrity and request/ response correlation. - A state-bearing response contains only the responder's own state. It cannot vouch for third parties.
- Inbound
LibraryChangedorCallToPlayChangedmessages are untrusted hints. For a known claimed ID they schedule one coalesced, rate-limited pull from that ID's already known endpoint. Their payload never merges directly. Hints for unknown IDs are ignored and mDNS remains the discovery path. Hellobecomes a pull-oriented exchange: the initiator sends no authoritative identity or replicated state, and the pinned responder returns its own current snapshot.
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
PEER_PING_IDLE_SECS + PEER_PING_INTERVAL_SECS, because the idle threshold is
only evaluated on interval ticks; that is 50s at the current 30s/20s settings,
not one interval. Assert against the constants rather than a literal bound.
Reconciliation still adds no new timer, no mDNS payload growth, and no periodic
full-state polling, and 50s is acceptable for the rare lost-hint case.
Freshness is tracked per peer as last_revision_check, stamped only by an
exchange that actually returned that peer's current revisions: a Pong or a
completed pull. Content transfers must not stamp it. A large download is a long
run of outbound pinned exchanges that carry no revisions, and it is exactly when
reconciliation must not be postponed. Inbound activity must not stamp it either:
ping_idle_peers currently gates on last_seen, which
update_last_seen_by_addr refreshes from traffic arriving from that peer, so a
peer that keeps talking to us would suppress the very check that detects our
staleness about it. last_seen keeps its existing stale-peer pruning role and
is not reused here. 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 that address/generation. A same-ID address move is likewise committed only after pinning the new endpoint. Pings are also pinned, and a late ping result may update/remove only the same endpoint generation it probed so it cannot delete a peer that has already moved or reconnected.
Remove Goodbye. It is unnecessary for correctness and an unauthenticated
removal hint is unsafe. mDNS expiry plus responder-pinned liveness handles
departure.
3.5 Keep Call to Play direct and ephemeral
Call to Play is coordination among people currently at the party. It does not need a Byzantine replicated ledger.
Each runtime owns only its locally authored slice:
CallId { creator: PeerId, random_nonce }
CallToPlayAuthorSnapshot {
runtime_session_id
revision
display_name
events[] // actor ID is not a wire field
}
The local core creates call/event IDs, increments the revision after each
accepted local action, and sends a cheap change hint to known peers. A receiver
coalesces the hint, connects to the author's known PeerEndpoint, and pulls
that author's complete current slice. Because the responder is pinned, the
receiver assigns the author ID itself. A peer cannot put another actor ID into
the wire object.
Snapshots use replacement, not union/CRDT semantics. For each peer, permit one in-flight refresh; a newer revision for the same runtime session replaces that author's previous slice atomically. A new runtime session replaces the old session after a fresh pinned handshake. Stale concurrent results cannot overwrite the current session.
Authority rules remain simple:
Create,Start,Cancel, andAddTimeare effective only when the pinned author equalsCallId.creator.- RSVP, ready/leave, and chat actions are attributed to the pinned author. They become effective only while the referenced creator root is directly present; an author slice pulled before its creator is retained within its ordinary bound but remains hidden until that creator's direct pull arrives.
- A snapshot contains only events authored by its responder. Third-party events are rejected rather than relayed.
- Display names never grant authority.
A newly arriving peer discovers and pulls directly from every live peer, so it reconstructs calls from the people still present. If an author's peer goes away, remove that author's slice. If the creator goes away, the call disappears from the derived view. A participant who leaves naturally drops out. This is the intended session model, not data loss.
Keep the useful human-scale timers: active calls expire, unresolved expired calls may remain visible for five minutes, and Start/Cancel results may remain visible for fifteen minutes. After that, the author drops them from its current snapshot. There are no session-long tombstones, rootless terminal records, three-day history horizons, verification caches, or permanent anti-resurrection state because no third party can replay an old author's history as authority.
Retain straightforward schema and resource limits: bounded strings/chat, bounded events and encoded bytes per author, bounded total live peers, and a named control-frame maximum. Validate one author's snapshot off to the side and accept or reject it as a unit; a bad/oversized peer cannot consume another author's slice or the local author's capacity. Exact limits are set from the existing three-peer and stress fixtures, not from an internet-scale adversary model.
A malicious creator can show inconsistent versions of its own noncritical call to different peers. This plan accepts that limit rather than adding signatures, gossip, consensus, or permanent storage to a party invitation feature.
3.6 Keep the UI about games and people
Add one visible Local network sharing setting, on by default for this
LAN-sharing application. When off, stop mDNS advertisement/discovery, the QUIC
listener, outbound refresh, and serving. The setting is durable and its state is
obvious in the main UI/settings.
Do not add per-peer source prompts. Every peer with the locally expected
content_id is an eligible swarm source; verification is automatic.
Normal UI uses display names and peer count. A short PeerId suffix may disambiguate duplicate names or appear in diagnostics, but there are no New, Trusted, key-changed, backup, repair, or fingerprint-confirmation workflows.
User-facing exceptional states are concrete:
Verifying downloaded chunkswhile newly received content is checked;A source sent invalid data; retrying another nearby peerwhen recovery is in progress;No nearby peer could provide the verified catalog versionafter all matching sources fail;Nearby devices are running a different Lanspread versionwhen mDNS sees an incompatible protocol; and- a non-blocking networking diagnostic if the installation identity cannot be persisted and will change next launch.
Do not ask the user to solve a cryptographic implementation problem.
4. One protocol cutover
The coordinated cutover replaced protocol 7 with current protocol 8. There is no intermediate 8/9/10 design and no compatibility decoding.
The cutover includes:
PeerIdderived from the TLS SPKI andPeerEndpointrequired by every outbound connection;- 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, revision-bearing
Pong, and bounded change hints instead of inbound state-bearingHello, pushedLibraryDelta, and pushed/relayedCallToPlayEvents; - cryptographic
content_idin game availability and catalog-driven chunk requests; - canonical forward-slash catalog paths;
- author-owned Call-to-Play snapshots; and
- removal of
Goodbyeand payload fields that pretend to identify an authoritative sender.
Peers on another protocol remain excluded, as required by project policy. To reduce real LAN-party friction, make this one coordinated bump and surface the version mismatch rather than failing silently.
5. Code ownership
Keep the change inside existing crates unless implementation pressure proves a real reusable boundary; a new identity crate is not required by the design.
| Area | Responsibility |
|---|---|
lanspread-db / lanspread-compat |
Catalog content-manifest types and loading beside game.db. |
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, 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. |
| Tauri/frontend | Global sharing switch, verification/progress failures, incompatible-version notice, and replacement of the full derived Call-to-Play view. |
| peer CLI | Distinct deterministic identities, hostile TLS/content modes, and zero-prompt multi-peer scenarios. |
6. Implementation phases and gates
Every code phase runs just fmt, just clippy, and just test. Frontend or
Tauri phases also run just frontend-test and just build. Network/transfer
phases run focused peer-CLI scenarios during development and the unfiltered
just peer-cli-tests before completion. Manual alpha/bravo/charlie evidence
must use a freshly built image.
Phase 1 — land filesystem confinement immediately
Implement ValidatedDownloadManifest, make the UI submit only game_id, and
move all validation before transaction/storage mutation. Centralize reserved
paths and add the zero-mutation hostile tests from §3.1. Preserve current wire
bytes in this phase; it is an independent safety fix.
Gate: standard Rust/Tauri checks, hostile descriptor tests, full peer-CLI suite, and supported Windows path/reparse evidence. Linux-only results must not be reported as Windows proof.
Phase 2 — establish real catalog content authority
Add the reproducible content-manifest generator and fixture manifests. Freeze the versioned manifest/content-ID encoding with golden tests. Extend local catalog state, build download plans only from that state, implement streaming BLAKE3 checks and source quarantine, and implement verified extracted manifests for Stream Install.
Do not claim completion from test fixtures alone: production catalog packages must have independently generated manifests, and the release/build path must reject a missing manifest. Benchmark hashing at normal LAN throughput.
Phase 3 — prove and implement simple responder identity
Start with a bounded rustls/s2n-quic spike that proves self-issued certificate support, SPKI extraction, expected-ID pinning, and real TLS 1.3 CertificateVerify. The certificate-A/private-key-B negative is the go/no-go gate. Choose Ed25519 or P-256 based on that proof, using one TLS identity key.
Then add simple load-or-generate persistence, deterministic CLI identities,
PeerEndpoint, and endpoint plumbing through every outbound consumer. Separate
mDNS candidates from authenticated peer records and make liveness removal
generation-conditional. No trust database or identity UI is introduced.
Phase 4 — make the single wire cutover
Bump the current protocol once and activate all coupled wire behavior from §4:
pinned transport, catalog content_id, catalog-driven downloads, pull-only
library synchronization, bounded invalidation hints, author-owned Call-to-Play
snapshots, and no Goodbye.
This phase is not complete until:
- three fresh peers discover each other with no prompts and see post-start library changes;
- a new peer reconstructs active Call-to-Play state by pulling every live author, and creator departure removes the call;
- every metadata, chunk, retry, stream-install, healing, liveness, and direct 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
PEER_PING_IDLE_SECS + PEER_PING_INTERVAL_SECS, and neither inbound traffic nor an in-flight content transfer defers 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;
- all-bad/only-bad sources fail without committing
version.inior touchinglocal/; - a streamed path/hash/set mismatch cannot promote staging;
- an oversized Call-to-Play snapshot affects only that remote author and local publication still works; and
- protocol-7 peers are rejected while the UI receives enough information to explain the version mismatch.
Update ARCHITECTURE.md, protocol docs, and CLI documentation in the same
phase; do not leave the shared-certificate or relayed-event description behind.
Phase 5 — finish the small user-facing surface and audit
Add the global sharing switch and the concrete progress/error states from §3.6. Run a first-run test with an empty app-data directory, a normal restart, a corrupt identity file, and unwritable identity persistence; none may produce a key-management workflow or prevent the ephemeral fallback from participating for that run.
Run all standard checks, the complete peer-CLI suite, fresh three-peer manual scenarios, production builds/bundles on supported platforms, and a final audit for:
- raw remote manifests reaching storage;
- unhashed transfer completion or CRC32 presented as malicious-source proof;
- the shared repository TLS private key;
- address-only outbound connections or fabricated peer IDs;
- direct mutation from mDNS, inbound Hello, deltas, or change hints;
- relayed third-party Call-to-Play history or permanent tombstones;
- signed control envelopes, nonce/replay tables, keyring/backup/repair code, or per-peer source authorization reappearing without a new product requirement;
- silent protocol-version failure; and
- user wording that calls a peer, display name, or executable “trusted” merely because TLS or a hash check succeeded.
7. Success criteria
The plan is complete when the following statement is true from a user's point of view:
I opened Lanspread at a LAN party, immediately saw the people and games nearby, downloaded from all matching peers without approving devices, and Lanspread itself rejected any wrong data. I never had to know that it owns a TLS key.
From the implementation point of view, that experience rests on only three security boundaries: confined local paths, catalog-owned content hashes, and responder-pinned TLS. Call to Play deliberately reuses the pinned-pull model and remains ephemeral instead of becoming a second distributed security protocol.