Commit Graph
446 Commits
Author SHA1 Message Date
ddidderr 4f4aa74672 docs(security): record final review and retained risks
Reassess the Fable ignored-finding ledger against the LAN-party threat model, move the newly bounded findings to fixed, and state the remaining S33, anonymous-membership, unrar, local-race, and elevated-script decisions precisely.

Add the 2026-09-12 scan report with dependency results, independent follow-up review, validation evidence, and platform limits; update architecture limits to match the implementation.

Test Plan:
- just fmt
- just clippy
- just test
- just frontend-test
- just build-fixture
- just peer-cli-build
- just mdns-vendor-test
- just security-audit
- git diff --check
2026-09-12 13:49:41 +02:00
ddidderr 6622ae21f3 style: format final security fixes
Record the repository formatter's deterministic layout for the catalog-preflight helper and handshake-window assertion.

Test Plan:
- just fmt
- just clippy
- just test
- git diff --check
2026-09-12 13:44:27 +02:00
ddidderr 3dfb98f249 fix(peer): retain Stream Install egress inactivity
Keep the catalog-sized absolute provider deadline and independently reset a ten-minute no-progress deadline after each successful frame write. Large valid games can run past ten minutes, while stalled producers, readers, and FIN still release their permits promptly.

Test Plan:
- just test
- just clippy
- long-total stalled-provider and slow-reader regressions
- git diff --check
2026-09-12 13:44:00 +02:00
ddidderr 88c31485e1 fix(peer): rate-limit half-open handshakes by IP
Before TLS and application leases exist, admit at most eight handshake attempts per observed IP during one three-second handshake window. Bound the spoofed-origin ledger at 256 entries while retaining s2n's default port and global handshake controls.

Test Plan:
- just test
- just clippy
- per-origin window and spoofed-origin ledger tests
- git diff --check
2026-09-12 13:41:51 +02:00
ddidderr 07c51a10e9 fix(peer): preserve monitor backoff per root
Track the last attempted configured root separately from the last successful snapshot. A root switch clears backoff once; repeated budget failures on that same root now retain exponential delay.

Test Plan:
- just test
- just clippy
- repeated failing-root backoff regression
- git diff --check
2026-09-12 13:39:47 +02:00
ddidderr f0a6d5f7ca fix(peer): size Stream Install egress deadlines
Give a provider ten setup minutes plus its catalog streamed bytes at 1 MiB/s instead of one fixed ten-minute absolute window. Large healthy games can complete while the sender still has a finite nonrenewable deadline through FIN.

Test Plan:
- just test
- just clippy
- large-provider timeout regression
- git diff --check
2026-09-12 13:36:10 +02:00
ddidderr 9b6f611db0 fix(peer): bound public readiness sentinel reads
Reuse the stable 64 KiB non-link version.ini reader in anonymous transfer admission and install recovery. A stale Ready snapshot can no longer turn a locally enlarged sentinel into repeated unbounded request-path reads or diagnostic content.

Test Plan:
- just test
- just clippy
- oversized public-admission sentinel regression
- git diff --check
2026-09-12 13:35:54 +02:00
ddidderr 3450d27742 fix(peer): bound aggregate launch-settings reads
Charge every SmartSteamEmu.ini chunk to one 64 MiB aggregate budget and the existing ten-minute scan clock. All candidate reads still finish before the first write, so exhaustion cannot leave partial settings or a completion marker.

Test Plan:
- just test
- just clippy
- aggregate-read zero-partial-write regression
- git diff --check
2026-09-12 13:35:36 +02:00
ddidderr eda006c66d fix(peer): isolate response buffer budgets by IP
Acquire one 8 MiB endpoint-IP budget before the shared 32 MiB control-response pool. Same-origin maximal-prefix waiters now stay out of the fair global semaphore queue, so they cannot head-of-line block small responses from other LAN hosts.

Test Plan:
- just test
- just clippy
- cross-origin semaphore ordering regression
- git diff --check
2026-09-12 13:34:42 +02:00
ddidderr 0c26aba9e9 fix(peer): bound Stream Install receive retries
Share one catalog-derived deadline across at most four distinct endpoint-IP attempts: ten minutes plus two catalog copies at 1 MiB/s. Keep the ten-minute inactivity timer, but renew it only after a nonempty catalog-checked file chunk is written to staging.

Cancellation, rollback, integrity quarantine, and public exhaustion states retain their existing semantics.

Test Plan:
- just test
- just clippy
- shared-deadline, source-IP budget, metadata-inactivity, and useful-byte regressions
- git diff --check
2026-09-12 13:23:55 +02:00
ddidderr a687e0c2b8 fix(peer): preserve control capacity across origins
Set the per-origin control-stream share to eight against the sixteen-slot global pool, preventing one observed host from occupying every decoded control task.

Test Plan:
- just test
- just clippy
- exact admission-limit regression
- git diff --check
2026-09-12 13:22:51 +02:00
ddidderr a5c3a21142 fix(peer): cap aggregate chunk retry time
Start one twenty-minute retry deadline only after an initial chunk failure, retain it across authenticated source and IP changes, and give each in-flight retry the earlier of that deadline and its ten-minute attempt limit. Expiry remains a non-quarantining transport failure.

Test Plan:
- just test
- just clippy
- three paused-time retry-deadline regressions
- git diff --check
2026-09-12 13:22:33 +02:00
ddidderr 0e72de4e7a fix(catalog): validate package roots before preflight
Require each selected package root to be a regular non-link directory before reading its bounded version.ini, and omit package-controlled observed contents from mismatch diagnostics. Full-selection preflight still completes before archive work or publication.

Test Plan:
- just test
- just clippy
- symlink-root and diagnostic-redaction regressions
- git diff --check
2026-09-12 13:22:13 +02:00
ddidderr 8f8072ffb6 build(security): add locked dependency audit gate
Add one just recipe that scans Cargo.lock with the current RustSec database and deno.lock with Deno's advisory service.

Test Plan:
- just security-audit (no known vulnerabilities; seven RustSec informational warnings)
- just --fmt --check
- git diff --check
2026-09-12 13:15:28 +02:00
ddidderr 67ea355599 fix(tauri): bind elevated scripts to catalog authority
Preserve required UAC elevation for game_setup.cmd, game_start.cmd, and server_start.cmd through a fixed-role elevated launcher worker. The worker reloads and matches embedded catalog authority, verifies the exact script from a no-follow locked handle, resolves System32 cmd.exe, and transfers path locks into the command process.

Setup still waits for completion; game and server return after the verified handoff while their command process retains the locks. Unmanifested, changed, reparse-backed, markerless, and streamed-only scripts fail closed.

Test Plan:
- just test
- just clippy
- just frontend-test
- just build-fixture
- nine Linux-visible authority/parser/digest tests
- Windows-only lock-transfer test added but not run (no Windows target/runtime available)
- git diff --check
2026-09-12 13:10:36 +02:00
ddidderr 42cf98ecec fix(peer): charge discovery quota to packet origin
Carry the mDNS response source independently of its advertised A or AAAA target. Active and cooling candidates now charge the observed host, so rotating peer IDs, ports, and target addresses cannot escape the eight-candidate origin budget.

Test Plan:
- just test
- just clippy
- rotating-advertised-target regression
- git diff --check
2026-09-12 13:09:06 +02:00
ddidderr 4d5881d6b0 fix(mdns): retain origin and bound daemon state
Vendor the pinned mdns-sd 0.21.1 source so response records retain their observed source IP. Bound unauthenticated cache records to 1024 globally and 128 per source, deduplicate and cap timers at 4096, and poll at least once per second for expiry cleanup.

Expose packet provenance through lanspread-mdns, drop originless resolutions, and add a dedicated vendor test recipe while keeping third-party sources outside workspace formatting and Clippy.

Test Plan:
- just mdns-vendor-test (106 tests passed with socket access)
- just test
- just fmt
- just clippy
- cache/per-source, exact-refresh, timer-cap, and serde tests
- git diff --check
2026-09-12 13:08:26 +02:00
ddidderr 49f8eef7b5 style(peer): format security hardening
Record the deterministic nightly rustfmt output for the newly added event-channel and retry code after the repository-wide formatter gate was repaired.

Test Plan:
- just fmt
- just clippy
- git diff --check
2026-09-12 13:07:14 +02:00
ddidderr f6af9b9420 fix(peer): bound local monitor snapshots
Filter roots through the catalog before retaining them, cap entries, games, and poll time, and preserve the previous complete snapshot on failure. Repeated failures back off exponentially and a root change resets retry state.

Test Plan:
- just test
- just clippy
- focused catalog-filter, budget-preservation, and backoff tests
- git diff --check
2026-09-12 13:06:32 +02:00
ddidderr 37420e26ed fix(peer): coalesce full UI view publications
Keep one queued and one replaceable pending snapshot for remote-library and Call-to-Play views while preserving lifecycle-event FIFO delivery. Generation barriers and fenced drains prevent stale nonempty views from crossing disable or acknowledgement boundaries.

Test Plan:
- just test
- just clippy
- focused burst, lifecycle ordering, fence, repeated-barrier, and stale-view tests
- independent ordering review
- git diff --check
2026-09-12 13:06:10 +02:00
ddidderr 470e119515 fix(peer): preflight launch settings mutations
Bound recursive setting discovery and INI reads, and finish every scan/read before the first write. Budget exhaustion can no longer leave partial setting changes or a misleading completion marker.

Test Plan:
- just test
- just clippy
- focused traversal, oversized INI, and zero-partial-write tests
- git diff --check
2026-09-12 13:00:31 +02:00
ddidderr 43f78a3d01 fix(peer): preflight legacy migration work
Bound legacy traversal by entry count and cooperative elapsed time, and complete the read-only plan before mutating any source or destination. Reuse the catalog-sized legacy-index read ceiling.

Test Plan:
- just test
- just clippy
- focused preflight and oversized migration tests
- git diff --check
2026-09-12 13:00:23 +02:00
ddidderr cd50f78854 fix(peer): bound local library ingestion
Cap index reads, games, archive fingerprints, selected-root entries, recursive depth and bytes, scan time, and version.ini reads. Failed rescans retain the prior complete index and revision instead of publishing partial state.

Test Plan:
- just test
- just clippy
- focused oversize index, traversal, and prior-snapshot tests
- git diff --check
2026-09-12 13:00:04 +02:00
ddidderr 01d92b1922 refactor(peer): split bulk request dispatch
Move file-chunk and Stream Install branches into focused helpers so the new admission path remains within the workspace's strict Clippy limits without changing request behavior.

Test Plan:
- just clippy
- git diff --check
2026-09-12 12:59:05 +02:00
ddidderr b3174deabf fix(peer): deduplicate chunk retries by source IP
Track both authenticated peer IDs and endpoint IPs for each failed chunk. Rotating keys or ports at one host no longer grants another automatic ten-minute transfer attempt, while the existing eight-attempt ceiling remains.

Test Plan:
- just test
- focused same-IP Sybil and distinct-source selection tests
- git diff --check
2026-09-12 12:53:54 +02:00
ddidderr e96f322aad fix(peer): enforce per-origin server admission
Bound each observed remote IP to eight connections, sixteen control streams, eight bulk transfers, and one Stream Install provider. Drop-owned leases release on normal completion or unwind, and per-origin saturation cannot consume another resource class.

Test Plan:
- just test
- focused port rotation, distinct-origin, drop/unwind, class isolation, and provider saturation tests
- git diff --check
2026-09-12 12:52:55 +02:00
ddidderr dfc858d0e2 fix(peer): bound aggregate control response buffering
Read the four-byte response length explicitly, acquire bytes from one 32 MiB connector-wide budget before buffering, and require exact body plus EOF. One-way requests now check EOF without allocating an 8 MiB decoder.

Test Plan:
- just test
- focused complete, oversized, truncated, trailing, one-way, and shared-budget tests
- git diff --check
2026-09-12 12:41:39 +02:00
ddidderr 145610c0a1 fix(peer): bound authenticated identities per endpoint IP
Allow at most eight committed peer identities at one pinned QUIC endpoint IP after Hello authentication. Reject excess keys before address-owner eviction, while removals release capacity and other LAN hosts remain admissible.

Test Plan:
- just test
- focused port-rotation, ninth-key, address-owner, other-IP, and release tests
- git diff --check
2026-09-12 12:41:20 +02:00
ddidderr 06cd8b93ed fix(peer): enforce absolute transfer send deadlines
Use one ten-minute authority window for all raw chunk writes and FIN, and one window for Stream Install producer waits, frames, and FIN. Successful incremental writes can no longer renew a public bulk slot indefinitely.

Test Plan:
- just test
- focused paused-time cumulative-write regressions
- git diff --check
2026-09-12 12:36:48 +02:00
ddidderr 24f573d82a fix(build): exclude generated reports from formatting
Keep the generated security report and archived audit artifacts outside the maintained Markdown formatter set, and run Tombi offline. This makes the repository formatter deterministic without rewriting generated evidence.

Test Plan:
- just fmt
- just --fmt --check
- git diff --check
2026-09-12 12:36:26 +02:00
ddidderr ff962801ae fix(peer): cap public control and extractor concurrency
Limit server-wide decoded control requests to 16 and gate Stream Install providers behind a separate two-slot semaphore. The provider permit remains held through producer cleanup, bounding concurrent unrar work while ordinary bulk transfers retain their own capacity.

Test Plan:
- just test (passed after rerunning one transient ETXTBSY failure)
- just fmt (Rust formatting passed; repository's generated security report still triggers pre-existing rumdl violations)
- git diff --check
2026-09-12 12:32:12 +02:00
ddidderr 078294ac83 fix(frontend): bound Call-to-Play projection work
A valid remote view could make the renderer reduce and mount thousands of calls
every second. A single long chat also re-sorted its growing message array after
every append during every reduction.

Keep only the highest-ranked 128 nominations and latest 256 messages for UI
projection. Events are sorted once before folding, so chat messages remain in
stable order without repeated growing-array sorts. Backend author history and
creator authority are unchanged.

Test Plan:
- `just frontend-test` -- passed; 94 frontend tests passed.
- `just fmt` -- TypeScript formatting completed; the recipe then hit the
  pre-existing generated security-report Markdown-lint failures.
- `git diff --cached --check` -- passed.
2026-09-12 12:28:25 +02:00
ddidderr 30663fea34 fix(peer): bound remote Call-to-Play state
Wire-level author limits still allowed one peer to publish thousands of creator
roots and a Sybil set to retain hundreds of thousands of aggregate events.
Those valid snapshots were repeatedly projected for the desktop.

Limit one author to 128 creator roots and all retained remote history to 16,384
events. Over-budget revisions replace that author's slice with an empty
watermark, which frees memory and prevents liveness from pulling the rejected
snapshot every five seconds. Local author capacity remains independent.

Test Plan:
- `just test` -- passed outside the sandbox; 498 peer tests and all workspace
  targets passed.
- `just fmt` -- Rust formatting completed; the recipe then hit the pre-existing
  generated security-report Markdown-lint failures.
- `git diff --cached --check` -- passed.
2026-09-12 12:26:55 +02:00
ddidderr 0189622085 fix(peer): bound aggregate remote library state
Per-peer library limits still allowed 64 authenticated identities to retain a
large aggregate map and repeatedly clone it for UI publication.

Cap retained remote availability at 16,384 rows. A peer whose new projection
would exceed the budget stays authenticated with an empty library slice, and
its accepted revision is retained so liveness does not repeatedly pull the same
over-budget snapshot.

Test Plan:
- `just test` -- passed outside the sandbox; 496 peer tests and all workspace
  targets passed.
- `just fmt` -- Rust formatting completed; the recipe then hit the pre-existing
  generated security-report Markdown-lint failures.
- `git diff --cached --check` -- passed.
2026-09-12 12:23:42 +02:00
ddidderr c892838d72 fix(peer): bound per-chunk retry source fanout
A failed catalog chunk could try every authenticated peer identity. Since each
source owns a fresh transfer deadline, one host could use Sybil identities to
turn a single unavailable chunk into a multi-hour retry sequence.

Count the initial source and cap one chunk at eight distinct peer identities.
Integrity quarantine, transport classification, cancellation, and explicit
user retries retain their existing behavior.

Test Plan:
- `just test` -- passed outside the sandbox; 495 peer tests and all workspace
  targets passed.
- `just fmt` -- Rust formatting completed; the recipe then hit the pre-existing
  generated `security-report/report.md` Markdown-lint failures.
- `git diff --cached --check` -- passed.
2026-09-12 12:21:36 +02:00
ddidderr 7de373afb6 fix(peer): sanitize legacy library index migration
The selected game root could supply an unbounded legacy library index whose
revision was copied verbatim into app-owned state. A revision of `u64::MAX`
made every later checked revision advance fail even after the original root was
removed.

Read only a bounded regular, non-link file whose identity stays stable across
the read. Deserialize it before publication, preserve its cached game data, and
reset its stale revision authority to zero. Existing app-owned state still wins
without reading the legacy source, and invalid input remains in place for
recovery.

Test Plan:
- `just test` -- passed outside the sandbox; 494 peer tests and all workspace
  targets passed.
- The initial sandboxed `just test` reached an unrelated Unix-socket permission
  denial, then passed unchanged with the required socket permission.
- `git diff --cached --check` -- passed.
2026-09-12 12:19:50 +02:00
ddidderr 0ed04f64e9 fix(tauri): remove unused frontend shell-open authority
The main webview retained `shell:allow-open` even though the frontend never
uses the shell plugin directly. File-manager opening already flows through the
`open_game_files` command, which checks catalog membership and containment.

Remove only the frontend permission. The Rust shell plugin and dependency stay
available for the checked backend open operation and bundled unrar sidecar.

Test Plan:
- `just build-fixture` -- passed, including the frontend build and Tauri
  capability validation.
- `git diff --cached --check` -- passed.
2026-09-12 12:16:00 +02:00
ddidderr 74c5412232 fix(build): pass peer CLI inputs as shell data
Peer CLI scenarios, container names, and macvlan overrides were rendered into
shell source by Just before execution. Quoting the rendered text did not stop
command substitution or quote breaking.

Enable positional arguments for the variadic scenario recipe and consume all
caller-controlled values through exported shell variables. This preserves the
existing recipe interface, including multiple scenario arguments and network
overrides, while keeping their contents out of shell syntax.

Test Plan:
- `just --fmt --check` -- passed.
- `just --dry-run --no-deps peer-cli-tests S1 '$(printf SCENARIO_PROBE)' 'space value'` -- rendered only `"$@"`.
- `just --dry-run --no-deps peer-cli-run '$(printf NAME_PROBE)'` -- rendered only quoted `$NAME` uses.
- `just --dry-run --no-deps LANSPREAD_NET='$(printf NET_PROBE)' peer-cli-net` -- rendered only quoted network variables.
- `git diff --cached --check` -- passed.
2026-09-12 12:14:18 +02:00
ddidderr 91cac53848 docs: record NET-05, Codex [7] and Codex [12] as fixed
The ignore file listed these three findings as deliberately not fixed. A
cross-review of the three parallel fix branches showed that the second
branch had small, safe fixes for each, and they have now been ported onto
this branch (justfile `set export`, fallible `setup_done_path`, bounded
wire collection deserialization). Remove their "not fixed" rationales
and move them to the fixed row of the summary table so the file again
accounts for all 35 findings accurately.

Claude-Session: https://claude.ai/code/session_01QRkCv4a4GqkajyamxmbSuA
2026-09-12 11:17:32 +02:00
ddidderr 0fd368b6fa fix(proto): apply catalog component rules to wire game IDs
Follow-up to EXP2-SEC-04 (wire game IDs). The wire validator rejected
separators, control characters and the `.`/`..` pseudo-components and
otherwise relied on the catalog lookup that follows every request. That
lookup is real, but it leaves a class of IDs on the wire that the catalog
itself would never publish: Windows device names (`CON`, `nul.txt`,
`com1`), trailing dots or spaces, and the Windows-reserved characters
`< > : " | ? *`. Rejecting them at the protocol boundary means a
filesystem-backed handler can never see one, whichever consumer is added
next.

`validate_game_id` now also calls
`lanspread_db::content_manifest::validate_portable_component`, the same
function the catalog uses to admit game IDs and that `path_validation.rs`
already reuses. Reusing it rather than copying the device-name table
(the parallel branch grew three private copies) guarantees the wire rule
can neither over-match nor drift: an ID the catalog accepts always
encodes. The existing `InvalidPathComponent` error variant is reused so
callers and logs are unchanged.

Tests add the newly rejected forms and a positive list of catalog-valid
IDs that must keep encoding: embedded dots and spaces (`game..v1
(final)`), `console.txt`, `com10` and a non-ASCII name. The fixture
catalogs and the peer test suite, which encode many request IDs, pass
unchanged.

Test plan:
- `cargo test -p lanspread-proto`: 24 passed.
- `cargo test -p lanspread-peer -p lanspread-peer-cli`: 491 + 15 + 21
  passed.
- `cargo clippy -p lanspread-proto --all-targets -- -D warnings`: clean.

Claude-Session: https://claude.ai/code/session_01QRkCv4a4GqkajyamxmbSuA
2026-09-12 11:17:12 +02:00
ddidderr 4cef5154cf fix(proto): bound wire collections during deserialization
Gemini audit finding NET-05 ("unbounded collection deserialization before
semantic validation", Low). `LibrarySnapshot::games` and
`CallToPlayAuthorSnapshot::events` were plain `Vec`s. Their semantic
validators reject more than 4,096 elements, but serde had already built
the complete vector by then, so a peer could make a receiver allocate up
to a full 8 MiB response frame's worth of parsed elements before the
limit was applied. The 64 KiB request bound already covers inbound
requests; this closes the same gap for responses a client accepts from a
peer it connected to.

Both fields now deserialize through a bounded visitor that keeps at most
`maximum + 1` elements and drains the remainder as `IgnoredAny` without
allocating. Keeping exactly one element past the limit is deliberate: the
existing `validate` methods still observe `len() > maximum` and report
`TooManyItems`, and `Response::decode` keeps leaving that judgement to
the caller so an invalid Call-to-Play domain does not discard a valid
library (and vice versa). Frames at or below the limit are byte-for-byte
unchanged, and the encoder still refuses to produce oversize frames.

Tests build oversize JSON by hand (the encoder cannot) and check that a
sequence four times the limit decodes to limit + 1 elements, that the
semantic validator then reports `TooManyItems`, and that a sequence
exactly at the limit is untouched. The existing domain-isolation test
asserts the truncated length as well.

Ported from the parallel security branch (lanspread2 commit 5139ec1),
which bundled it with the request frame bound this branch already has.

Test plan:
- `cargo test -p lanspread-proto`: 24 passed.
- `cargo clippy -p lanspread-proto --all-targets -- -D warnings`: clean.

Claude-Session: https://claude.ai/code/session_01QRkCv4a4GqkajyamxmbSuA
2026-09-12 11:16:12 +02:00
ddidderr 0a38dfbb19 fix(peer): reject unsafe game IDs in state marker paths
Scanner finding #12 ("state marker path escape"). The per-game state
helpers in `state_paths.rs` joined a raw game ID below
`<state_dir>/games/`. The public `setup_done_path` was therefore usable
with an absolute or parent-containing ID by an embedding caller, and the
legacy migration discovered IDs from directory names in the user's games
folder and joined them unconditionally. Every shipping caller today
validates its ID or takes it from the catalog, so this was a footgun
rather than an exploited hole, but the fix is small and removes the
reliance on every future caller remembering the rule.

Add `validate_game_state_id`, which rejects separators and NUL and then
delegates to `lanspread_db::content_manifest::validate_portable_component`
(the catalog's own rules: no `.`/`..`, no trailing dot or space, no control
or Windows-reserved characters, no Windows device names). Reusing the
catalog validator rather than a private copy guarantees that any ID the
catalog can publish is accepted here and that the two cannot drift apart.

`setup_done_path` now returns `eyre::Result<PathBuf>`; it is the only
state path the embedding application calls with an ID that may originate
from UI input. `launch_settings_applied_path` leaves the public API and
becomes `pub(crate)`; the two public launch-settings entry points
(`apply_launch_settings_once`, `mark_launch_settings_applied`) validate
the ID before any filesystem work. `game_state_dir` carries a
`debug_assert!` documenting the contract for internal callers without
turning a bad ID into a release-build panic; the migration test suite
exercises that assertion in debug builds.

Behaviour changes:
- Legacy migration logs a warning, counts a failure and leaves the legacy
  marker in place for a games-folder directory whose name is not a
  portable game ID, instead of creating state below it. A new test covers
  a trailing-dot directory name.
- The Windows launcher ignores a run request whose ID `setup_done_path`
  rejects, with a warning, mirroring the existing invalid-ID early return.

Tests cover catalog-valid IDs that must remain accepted (embedded dots,
spaces, `console.txt`, `com10`, non-ASCII) and unsafe IDs that must be
rejected (empty, `.`, `..`, separators, NUL, trailing dot or space,
device names, `a:b`).

This ports the fallible API from the parallel security branch (lanspread2
commits 4146a0e and 9f26c63) onto the validator this branch already
exports from `lanspread-db`.

Test plan:
- `cargo test -p lanspread-peer --lib`: 491 passed.
- `just clippy`: clean.
- On Windows, launch a game with a valid ID and confirm the setup marker
  is still written under `<app-data>/games/<id>/setup_done`.

Claude-Session: https://claude.ai/code/session_01QRkCv4a4GqkajyamxmbSuA
2026-09-12 11:14:44 +02:00
ddidderr 63aa4bc77c fix(peer): serve manifests from the validated cache only
Follow-up to the scanner finding #2 fix ("rejected bulk requests can
populate the persistent manifest cache"). The previous commit reordered
admission so the compact content index and local readiness are checked
before any manifest body is loaded. That relied on ordering alone: the
loader still called `CatalogBundle::manifest`, which reads and parses
the artifact from disk on a cache miss, and nothing in the test suite
proved that a rejected request leaves the cache untouched.

Switch the outbound admission loader to `cached_manifest`, which never
performs filesystem I/O. This is safe because every game that can pass
`can_serve_game` is catalog-eligible and its manifest was primed by
`prime_library_manifests` before the library revision that advertises it
became visible (server startup in `services/server.rs`, every library
scan publication in `handlers.rs`). A cache miss therefore means a
publication-ordering bug rather than a legitimate serve, and failing the
request closed with a logged error is the right outcome.

The admission tests now assert that a request with the wrong content
identity leaves `cached_manifest("game")` erroring, for both catalog
chunks and Stream Install, and prime the manifest explicitly before the
accepted-request assertions, mirroring what the server does. The
architecture document describes the cache-only serving path.

Behaviour visible to peers is unchanged for valid requests. Rejected
requests no longer cause a disk read under the admission lock.

This layers the `cached_manifest` switch and no-load assertions from the
parallel security branch (lanspread2 commit 4e0419a) onto the identity
gate introduced in 55fa494.

Test plan:
- `cargo test -p lanspread-peer --lib`: 488 passed.
- `just test`, `just clippy`, `cargo +nightly fmt --check` at the end of
  the series.

Claude-Session: https://claude.ai/code/session_01QRkCv4a4GqkajyamxmbSuA
2026-09-12 11:12:14 +02:00
ddidderr 098b8e9161 fix(build): keep catalog arguments out of shell source
The Just recipes rendered LANSPREAD_GAMES_DIR, LANSPREAD_UNRAR, and recipe
parameters directly into shell command text. Double quotes around those
interpolations protected whitespace but still allowed command substitution and
other shell expansion before the called tool received its arguments.

Export Just variables and recipe parameters, then expand them only as quoted
shell parameters at execution time. Fixed repository paths remain Just
interpolations, while package roots, game IDs, output directories, and the
selected unrar executable are now passed as data. This preserves the existing
recipe interface and supports paths containing spaces or literal shell syntax.

Test Plan:
- `just --fmt --check` -- passed.
- dry-run with a command-substitution-shaped path stayed literal.
  -- rendered a quoted shell variable and did not execute the substitution.
- `git diff --cached --check` -- passed.
2026-09-12 11:10:21 +02:00
ddidderr e86cfc83a3 docs: record security findings deliberately left unfixed
Companion to the fix commits on this branch. Lists every finding from
the Gemini audit and the Codex scan that was not fixed or only partly
fixed, with the reasoning for each, anchored in what lanspread is: a
LAN-party launcher with an operator-published, BLAKE3-verified catalog
and intentionally anonymous requesters.

Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg
2026-09-02 22:39:40 +02:00
ddidderr 1ae752f2a5 docs(peer): describe the new discovery, hint, framing and unpack bounds
Bring ARCHITECTURE.md in line with the security fixes on this branch:
unicast-only mDNS candidates and the per-source-IP candidate budget,
source-IP binding of change hints, the split 64 KiB request / 8 MiB
response frame caps, and the `-ol-` plus link-audit rule for ordinary
`.eti` extraction.

Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg
2026-09-02 22:39:40 +02:00
ddidderr 55fa4941bc fix(peer): gate bulk requests on the compact index before loading manifests
Security audit finding Codex #2 ("rejected bulk requests can populate
the persistent manifest cache"). `admit_outbound_transfer` loaded the
full catalog manifest for the requested game ID first and only then
compared the content ID and checked whether the game is locally
serveable. Because manifests are cached for the life of the process,
one anonymous LAN client could make a node parse and retain the entire
catalog's manifest corpus with requests for games it does not even
have, and every such request paid a disk read under the admission lock.

The catalog already exposes a compact content index that answers
identity and streamed-install support without I/O. Admission now
checks that index and in-memory local readiness first; only requests
that pass both load the manifest. Accepted requests behave exactly as
before, including the streamed-install support check.

Test plan: `just test`. Manual: with two peer-cli containers, chunk
downloads and Stream Install still complete; a request naming an
unknown game ID or wrong content ID is declined with the same log
message as before.

Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg
2026-09-02 22:37:40 +02:00
ddidderr a86a2d1a0a fix(tauri): strip cmd.exe metacharacters from launch usernames
Security audit finding SEC-IPC-01 (parameter part). The username is
passed to game_setup/game_start/server_start batch scripts as a quoted
`cmd.exe` argument. Quoting protects the launcher's own command line,
but batch scripts expand `%~4` textually into their own statements, so
a name such as `foo & calc` would run `calc` from `set NAME=%~4`. Since
the setup script runs elevated, that matters even though the value is
the local user's own input.

`sanitize_username` previously removed control characters, `"` and
`%`; it now also removes `& | < > ^`. Spaces, punctuation such as `!`
and non-ASCII letters remain allowed so ordinary gamer tags are not
mangled. The audit's stricter `[A-Za-z0-9_-]` allowlist was rejected
for that reason.

The elevated execution of catalog scripts itself is intentional: the
shared games need administrator setup and the archives that carry the
scripts are BLAKE3-verified against the bundled catalog.

Test plan: `just test` (extended sanitizer test).

Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg
2026-09-02 22:37:40 +02:00
ddidderr 43b69a0f87 fix(frontend): scope Call-to-Play event keys by author
Security audit finding Codex #14 ("cross-author event-ID collisions can
suppress Call-to-Play entries"). The Rust side guarantees that an event
nonce is unique within one authenticated author's history and preserves
`author_id` on every projected event, but the frontend reducer
deduplicated the merged view with `new Map(events.map(e => [e.id, e]))`
and tracked chat messages by `event.id` alone. A peer could therefore
publish, say, a Respond event reusing the nonce of another user's
Create event and make that call vanish from every viewer, or shadow
other users' chat messages.

`eventKeyOf` now builds `author_id + NUL + id` and is used for view
deduplication, event ordering ties, message deduplication and the
message id that CtpChat uses as its React key. Nomination ids
(`call_id`) were already creator scoped and are unchanged.

Test plan: `just frontend-test`. The new test feeds a Create from
Alice and a Respond from Bob sharing one nonce and expects both to
apply, then two same-nonce messages from different authors and expects
two distinct messages.

Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg
2026-09-02 22:37:40 +02:00
ddidderr f9c64d7c18 fix(tauri): enable a Content Security Policy for the webview
Security audit finding SEC-IPC-02. `tauri.conf.json` set `"csp": null`,
which disables Tauri's CSP injection entirely. The audit found no
script-injection route in the frontend, so this is defense in depth:
should an XSS ever land through peer-supplied text, a CSP stops it from
loading remote scripts, exfiltrating over fetch/WebSocket or framing the
app, and confines IPC to Tauri's own channel.

Production policy (`csp`):
- default/script-src 'self': only the bundled Vite output runs. Tauri
  adds hashes for the init scripts it injects.
- style-src 'self' 'unsafe-inline' plus fonts.googleapis.com: React
  inline `style` props and the Bebas Neue stylesheet that index.html
  already links.
- font-src 'self' data: fonts.gstatic.com: the font files behind that
  stylesheet.
- img-src 'self' data: asset: http://asset.localhost: thumbnails arrive
  as base64 data URLs from get_game_thumbnail.
- connect-src ipc: http://ipc.localhost: Tauri does not append these
  itself; without them every `invoke` would be blocked.
- object-src/frame-src/form-action 'none', base-uri 'none'.

Development policy (`devCsp`): Vite's dev server injects the React
refresh preamble as an inline script and needs eval and a WebSocket to
localhost:1420 for HMR, so `just run` uses a permissive policy that
still forbids frames, plugins and form submission.

Verification here was limited to a static check: the production bundle
built by `deno task build` contains only external module scripts and
stylesheets, and `cargo tauri` parses the new config. The policy has
not been exercised in a running webview on this machine; if the app
shows a blank window or missing fonts/thumbnails after this change,
the WebView console will name the blocked directive.

Test plan: `just run` (dev) and `just build` then launch the binary;
confirm the library renders, thumbnails and the display font load,
IPC-backed actions (settings, log windows, Call to Play) work, and the
webview console shows no CSP violations.

Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg
2026-09-02 22:37:40 +02:00