Files
lanspread/crates/lanspread-peer-cli
ddidderr 84cbabfeba fix(install): skip and reject links when extracting .eti archives
Security audit findings EXP2-SEC-01, SEC-IPC-04 and Codex #3 (symlink
redirection through externally extracted archives).

The ordinary install path hands every root `.eti` archive to an external
`unrar x` process and promotes the resulting staging directory to
`local/` as soon as the extractor exits 0. Nothing inspected what unrar
materialised. A symbolic link (or, on Windows, a junction) inside an
archive would survive promotion and later redirect the launch-time
settings rewrite in `apply_launch_settings_once`, the uninstall path,
or any script the game ships. The archives themselves are BLAKE3
verified against the bundled catalog before they can be installed, so a
hostile link would have to be published by the catalog operator; this
is defense in depth rather than a live remote exploit.

Two independent layers now guard promotion:

- Both `unrar` invocations (Tauri sidecar and peer-cli external
  unpacker) pass `-ol-`, which makes unrar 7.x skip symbolic-link
  entries entirely. The bundled 7.10 sidecar was checked against a
  fixture archive.
- `install_inner`/`update_inner` walk the staging tree without
  following links and refuse to promote it if any entry is a symlink or
  (on Windows) carries the reparse-point attribute. The normal rollback
  then removes staging and clears the install intent.

The audit's `-sl-` suggestion does not exist in unrar; `-sl<size>` is a
size filter. Post-extraction digest verification of every extracted
file remains out of scope for the ordinary path; Stream Install already
verifies each output entry.

Test plan: `just test` (new unix test installs with a fake unpacker
that plants a symlink and asserts install fails, `local/` is absent and
the intent is cleared; the peer-cli controlled-unrar test checks the
new argument position). Manual: install a fixture game via peer-cli.

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

lanspread-peer-cli

Scriptable peer harness for automated LAN-spread tests. The binary starts the core peer runtime without the Tauri GUI, reads one JSON command per stdin line, and writes JSONL events, results, and errors to stdout.

Running

just peer-cli-build
just peer-cli-image
just peer-cli-tests
just peer-cli-run alpha

Useful flags:

  • --games-dir PATH stores local archives and installs.
  • --state-dir PATH stores the generated peer identity.
  • --identity-file PATH loads one existing peer identity strictly. Missing or invalid files fail startup without repair, quarantine, or fallback.
  • --catalog-db PATH and --manifests-dir PATH select one coherent catalog authority profile.
  • --fixture GAME_ID seeds a tiny archive that the fixture unpacker can install. The selected profile must already authorize that exact fixture.

Fixture Game Directories

fixtures/fixture-alpha, fixtures/fixture-bravo, and fixtures/fixture-charlie are ready-to-use game directories for local CLI smoke tests. Point --games-dir at one of them to start a peer with several catalog-backed fake games. Each game includes version.ini and a real RAR archive renamed to .eti; fixture-alpha and fixture-bravo share ggoo, while fixture-bravo and fixture-charlie share cnc4.

The checked-in catalogs/default profile authorizes the normal alpha, bravo, charlie, and persona packages. catalogs/solid and catalogs/multi are separate authorities because their cnctw packages intentionally contain different bytes and extracted layouts. catalogs/unknown is a source-only cod2 profile used to prove that another peer's honest catalog cannot extend the client's local catalog.

Regenerate and verify the profiles with:

just fixture-catalogs
just fixture-catalogs-check

Both commands use the Rust catalog publisher. Dynamic sparse and many-file acceptance packages use the same test-only generator through just fixture-download-only-catalog or just fixture-catalog; the Python scenario runner never derives hashes or catalog rows itself. Production artifacts are a separate corpus and are checked by just catalog-check-production.

Commands

Every command is a JSON object with cmd or command; id is optional and is echoed back on the result or error line.

{"id":"s1","cmd":"status"}
{"id":"p1","cmd":"wait-peers","count":1,"timeout_ms":5000}
{"id":"c1","cmd":"connect","peer_id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","addr":"127.0.0.1:34567"}
{"id":"g1","cmd":"list-games"}
{"id":"d1","cmd":"download","game_id":"fixture-one","install":true}
{"id":"i1","cmd":"install","game_id":"fixture-one"}
{"id":"u1","cmd":"uninstall","game_id":"fixture-one"}
{"id":"q1","cmd":"shutdown"}

connect requires the target's peer_id and addr from the same local-peer-ready event. Address-only connects are rejected because the peer ID is the TLS identity pin, not descriptive metadata.

The status result includes receiver-side active_operations and sender-side active_outbound_transfers counts by game ID, which the scenario runner uses to verify transfer lifecycle cleanup.