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
416 lines
23 KiB
Markdown
416 lines
23 KiB
Markdown
# lanspread-peer architecture
|
|
|
|
The peer uses mDNS discovery, authenticated QUIC transport, responder-owned
|
|
metadata snapshots, and catalog-authorized file transfers. Wire protocol 8 and
|
|
ALPN `lanspread/8` are the only supported wire mode; there is no legacy decode,
|
|
fallback, or compatibility shim.
|
|
|
|
## Goals (unchanged)
|
|
|
|
- Local LAN discovery via mDNS.
|
|
- QUIC + JSON messages for control, raw streams for file data.
|
|
- UI drives operations through `PeerCommand`, peers remain headless.
|
|
- Peers can appear/disappear at any time without data loss.
|
|
|
|
## Installation identity and QUIC authentication
|
|
|
|
- One installation-local Ed25519 key signs one self-issued TLS certificate.
|
|
`PeerId` is BLAKE3 over the certificate's complete canonical DER
|
|
SubjectPublicKeyInfo, encoded as 52 lowercase unpadded RFC 4648 base32
|
|
characters. It is not a UUID, display name, IP address, or wire assertion.
|
|
- The desktop app stores a strict version-1 record at `peer-identity-v1.json` in
|
|
its Tauri app-data directory. The record binds the Ed25519 certificate,
|
|
private key, SPKI-derived `PeerId`, and exact SAN. Reads and publication do
|
|
not follow symlinks or reparse points, creation is no-clobber, corrupt bytes
|
|
are quarantined before replacement, and private material is kept at mode 0600
|
|
where the platform supports Unix modes.
|
|
- Persistence failure produces a typed `Ephemeral` outcome and a fresh identity
|
|
for that runtime; it never falls back to another peer-ID scheme. The peer CLI
|
|
can instead select an existing record with `--identity-file`; that path is
|
|
loaded strictly and is never repaired, replaced, or generated implicitly.
|
|
- Identity state assumes one normal application owner for its app-data directory
|
|
and deliberately has no cross-process identity lease. Retained no-follow
|
|
handles contain path traversal and external-target mutation, and publication
|
|
does not overwrite an existing winner. A concurrent same-user writer can still
|
|
cause availability or continuity loss during quarantine; it is outside the
|
|
supported ownership model.
|
|
- QUIC uses the rustls provider with TLS 1.3 only. Every outbound operation
|
|
carries a `PeerEndpoint { peer_id, addr }`; the expected ID is encoded in an
|
|
exact SNI name, checked against the responder certificate SPKI, and the real
|
|
TLS 1.3 CertificateVerify signature is delegated to rustls. ALPN is
|
|
`lanspread/8`. Client resumption and 0-RTT are disabled; the server issues no
|
|
tickets and sends no early or half-RTT data.
|
|
|
|
## Peer lifecycle and message flow
|
|
|
|
### 1) Startup and advertise
|
|
|
|
- Start QUIC server.
|
|
- Advertise via mDNS with TXT records:
|
|
- `peer_id` (the canonical SPKI-derived ID)
|
|
- `proto_ver`
|
|
- optional `hostname`
|
|
|
|
### 2) Discovery and handshake
|
|
|
|
When a peer is discovered:
|
|
|
|
1. Parse `peer_id`, address, and `proto_ver` into a candidate `PeerEndpoint`.
|
|
Discovery is not authentication and does not add the peer to `PeerGameDB` or
|
|
emit UI membership events. Advertised addresses must be unicast with a
|
|
nonzero port; multicast, broadcast, and unspecified targets are dropped
|
|
before any handshake packet is sent. The mDNS ingress queue and active
|
|
candidate negotiations are each capped at 64, and one source IP may hold at
|
|
most 8 active-or-cooling candidates so a single host cannot fill every slot
|
|
by rotating claimed peer IDs and ports.
|
|
2. Reserve a candidate negotiation lease before queueing or awaiting work, then
|
|
establish a TLS-pinned connection to that exact endpoint. Missing or non-v8
|
|
records are rejected, and neither an ephemeral QUIC source port nor a payload
|
|
can replace the candidate listener address.
|
|
3. Send the empty `Hello` pull request. The pinned responder returns a
|
|
`HelloSnapshot` containing a `PeerStateSnapshot` with `runtime_session_id`,
|
|
`library`, and `call_to_play`: only that responder's local library and local
|
|
Call-to-Play author slice.
|
|
4. Decode the strict bounded wire shape, then validate the library and
|
|
Call-to-Play domains independently. Commit the endpoint and each valid domain
|
|
only if the candidate lease is still current for both peer ID and address.
|
|
This domain isolation lets an invalid remote Call-to-Play slice be cleared or
|
|
preserved according to its session without discarding a valid library.
|
|
5. Assign a fresh endpoint generation to every successful authentication.
|
|
Dropped, failed, or superseded work releases its candidate claims, and a late
|
|
result cannot mutate a newer generation.
|
|
|
|
### 3) Steady state
|
|
|
|
- Successful liveness probes update `last_seen` only when both the typed
|
|
endpoint and its authenticated generation are still current. Stale probes
|
|
cannot refresh or remove a reauthenticated endpoint.
|
|
- `Ping` receives a `Pong` carrying `PeerRevisions` with the runtime-session ID
|
|
and both domain revisions. A session or revision mismatch schedules a full
|
|
pinned pull, so idle liveness heals dropped change hints.
|
|
- Local library or Call-to-Play changes send cheap revision hints. Hints are
|
|
untrusted and lossy: unknown claims are ignored, a hint is honoured only when
|
|
it arrives from the IP address at which the claimed peer was authenticated,
|
|
and a known claim can only coalesce a pull from the claimed peer's already
|
|
authenticated endpoint. Requesters are anonymous, so this source binding is
|
|
what stops a third host from choosing which peers this node pulls.
|
|
- The state-sync scheduler caps its ingress and tracked peers at 64, coalesces
|
|
work for five seconds, and runs at most eight pinned pulls and eight hint
|
|
sends concurrently. Child work is drained lexically on shutdown.
|
|
- The server admits at most 64 unauthenticated handshakes, 64 established
|
|
connection scopes, and 32 control streams per connection. Each control stream
|
|
accepts one frame followed by request EOF and sends at most one response.
|
|
Inbound request frames are capped at 64 KiB (the QUIC receive window per
|
|
server stream matches it), response frames at 8 MiB, and control I/O has
|
|
ten-second deadlines.
|
|
|
|
### Call to Play replication
|
|
|
|
Call to Play is transient, responder-owned state. Each peer serves exactly one
|
|
local author snapshot:
|
|
|
|
```text
|
|
CallToPlayAuthorSnapshot { revision, display_name, events }
|
|
CallToPlayAuthorEvent { id, call_id, at, action }
|
|
```
|
|
|
|
The outer `PeerStateSnapshot` owns the runtime-session ID. A `CallId` encodes
|
|
the creator's typed `PeerId` plus a 128-bit nonce. The UI supplies only a local
|
|
intent and current display name; the peer core generates call/event nonces and
|
|
timestamps. Identity and display name are not repeated inside events. Creator
|
|
actions (`Create`, `Start`, `Cancel`, and `AddTime`) must come from the
|
|
`CallId.creator`; participant authors may contribute only participant actions.
|
|
|
|
The receiver assigns author identity from the pinned `PeerEndpoint`, validates
|
|
and normalizes the whole author slice off-lock, then commits it against that
|
|
endpoint's current generation and runtime session. For the same session, only a
|
|
higher revision replaces the slice; an equal identical snapshot is a no-op, an
|
|
equal different snapshot is a conflict, and a lower revision is stale. A new
|
|
authenticated session clears the previous slice before accepting valid new
|
|
state. Invalid same-session state preserves the last valid slice, while invalid
|
|
new-session state leaves that author absent.
|
|
|
|
Participant events are retained but hidden until the creator's direct author
|
|
slice supplies the call root. Removing a participant removes only that author's
|
|
rows; removing the creator hides the entire call. Because every snapshot is
|
|
served only by its author and attributed from the pinned responder, there is no
|
|
union merge, relay, live-event push, acknowledgement, or author claim on the
|
|
wire. The UI receives a deterministic complete replacement view.
|
|
|
|
Unresolved call history is removed as a whole after the deadline plus five
|
|
minutes; terminal history is removed as a whole after `Start` or `Cancel` plus
|
|
15 minutes. No tombstone survives pruning. Pruning a local slice advances its
|
|
revision before a snapshot or `Pong` is served, allowing peers to discover the
|
|
removal. The store caps total authors at 64, events per author at 4,096, and one
|
|
encoded author snapshot at 4 MiB, with reserved capacity for terminal actions.
|
|
|
|
### 4) Shutdown
|
|
|
|
- Cancellation stops admission, drains all lexically owned connection, stream,
|
|
state-sync, operation, and mDNS children, closes the shared endpoints, and
|
|
joins the supervisor.
|
|
- There is no `Goodbye` control message. Pinned liveness failure and stale
|
|
generation-conditional removal are authoritative for departure.
|
|
|
|
## Library sync protocol
|
|
|
|
The responder's full library slice is
|
|
`LibrarySnapshot { revision, games: Vec<GameAvailability> }`, where each sorted,
|
|
unique availability row contains only `game_id` and the catalog-derived exact
|
|
`ContentId`. A snapshot contains at most 4,096 games. It does not carry titles,
|
|
versions, paths, sizes, file lists, hashes chosen by the peer, installed state,
|
|
or another peer's availability.
|
|
|
|
The empty `Hello` request always returns the responder's current complete slice.
|
|
For the same runtime session, a higher revision atomically replaces the cached
|
|
slice; lower or equal stale work cannot roll it back. A new authenticated
|
|
runtime session resets the old domain before its candidate snapshot is
|
|
considered. `LibraryChanged(ChangeHint)` carries only an untrusted claimed
|
|
identity, runtime session, and revision. It never mutates a library directly and
|
|
has no delta payload. A pinned `Pong` revision mismatch schedules the same full
|
|
pull.
|
|
|
|
## Local library publication
|
|
|
|
- Only real local availability changes advance the revision and schedule a hint;
|
|
there is no periodic list broadcast.
|
|
- Filesystem events are gated per game ID instead of time-debounced:
|
|
- an active operation lock drops events for that game;
|
|
- a rescan already running for the ID sets a rescan-pending flag;
|
|
- the running rescan loops once more when that flag was set.
|
|
- Local library scans emit `LocalLibraryChanged` only for real library changes,
|
|
except that accepted game-directory changes can force a UI snapshot for the
|
|
new path without changing peer availability.
|
|
- Active operation mutations emit `ActiveOperationsChanged` from the mutation
|
|
path instead of riding on local library scans.
|
|
- The remote UI projection is a wholesale replacement derived from all current
|
|
authenticated per-peer slices; two peers offering the same game remain
|
|
distinct exact-content sources.
|
|
|
|
## Local game scanning: fast and low cost
|
|
|
|
### Strategy
|
|
|
|
1. Maintain a persistent revisioned on-disk index of local summaries and cheap
|
|
per-game fingerprints (root-level `version.ini` contents/mtime, root-level
|
|
`.eti` name/size/mtime, `local/` presence, and recovery state).
|
|
2. Poll a bounded non-recursive metadata snapshot once per second and update
|
|
only changed games.
|
|
3. Keep a 300-second fallback scan to recover from missed events.
|
|
|
|
### Fast-path scanning
|
|
|
|
- On startup, list only top-level game directories.
|
|
- For each game, read a cheap fingerprint:
|
|
- root-level `.eti` file names, sizes, and mtimes
|
|
- root-level `version.ini` mtime
|
|
- presence of `local/` as a directory
|
|
- If the fingerprint is unchanged, reuse the cached local summary.
|
|
- Only run a recursive scan for new or changed games.
|
|
|
|
## Local State and Recovery
|
|
|
|
Downloaded and installed are independent predicates:
|
|
|
|
- `downloaded` is true only when `<game_root>/version.ini` exists as a regular
|
|
file. The sentinel is written last through `.version.ini.tmp` and atomic
|
|
rename. The old sentinel is parked before a pending ownership set is
|
|
published; recovery restores it only when a valid baseline proves payload
|
|
mutation never started.
|
|
- `installed` is true when `<game_root>/local/` is a directory. The contents of
|
|
`local/` are user-owned and are skipped by manifests, fingerprints, and file
|
|
serving.
|
|
- Install and update transactions unpack into staging, then overwrite the first
|
|
discovered game-provided `account_name.txt` and `language.txt` files under the
|
|
staged tree from launcher settings before promoting it to `local/`.
|
|
|
|
Reserved per-game paths:
|
|
|
|
- `.version.ini.tmp` and `.version.ini.discarded` are download transaction
|
|
scratch files. Startup recovery interprets them together with the durable
|
|
ownership journal rather than sweeping them independently.
|
|
- `.local.installing/` is extraction staging.
|
|
- `.local.backup/` holds the previous install while an update or uninstall is in
|
|
flight.
|
|
- `games/<game_id>/install_intent.json` in the configured state directory is the
|
|
atomic per-game intent log. Every `games/<game_id>/` path is derived from a
|
|
game ID that satisfies the catalog's portable component rules; the public
|
|
`setup_done_path` and the launch-settings entry points validate the ID before
|
|
joining it, and legacy migration refuses a games-folder directory whose name
|
|
would not pass.
|
|
- `games/<game_id>/download_ownership/v1-<root_digest>/record.json` in that
|
|
state directory records the last committed and any pending downloader-owned
|
|
regular-file set and its exact catalog content ID. The namespace is derived
|
|
from one canonical configured games directory, and the record retains the full
|
|
lossless root identity. Same-ID state for different configured roots therefore
|
|
remains independent.
|
|
- `.lanspread_owned` inside `.local.*` directories proves Lanspread ownership
|
|
when no active intent independently proves transaction ownership.
|
|
|
|
Downloaded-file removal is not an uninstall transaction. For a catalog ID that
|
|
is a single direct child of the configured game directory, it requires a valid
|
|
ownership record and regular root-level `version.ini`, and refuses `local/`,
|
|
`.local.installing/`, or `.local.backup/`. It journals an empty pending
|
|
generation, deletes only committed downloader-owned files and the sentinel, and
|
|
keeps the game root plus every unknown file or directory.
|
|
|
|
The state layout is game-first: root namespaces live under each game ID rather
|
|
than under a global root tree. A former singleton ownership record is migrated
|
|
once into its derived namespace with copy-first durable publication. Singleton
|
|
state is not accepted as a parallel runtime format, and conflicting or split
|
|
migration evidence fails closed.
|
|
|
|
Recovery reads app-state `install_intent.json` and combines the recorded active
|
|
intent with the observed `local/`, `.local.installing/`, and `.local.backup/`
|
|
state. Intent states `Installing`, `Updating`, and `Uninstalling` prove
|
|
ownership of the corresponding reserved directories even if the marker was not
|
|
flushed before a crash. Settled state is represented by absence of the intent;
|
|
markerless `.local.*` directories are then left untouched.
|
|
|
|
Legacy `.lanspread/`, `.lanspread.json`, `.lanspread.json.tmp`,
|
|
`.softlan_game_installed`, and `local/.softlan_first_start_done` files are
|
|
handled only by the dedicated pre-start migration phase. Normal operation does
|
|
not read legacy state paths.
|
|
|
|
### Result
|
|
|
|
Most scans become O(number of game dirs), with full recursion only when needed.
|
|
|
|
## File manifests and downloads
|
|
|
|
- Tauri and the peer CLI inject one immutable `CatalogBundle`: `game.db` defines
|
|
catalog identity/version, the mandatory compact
|
|
`manifests/catalog-content-index-v1.jsonl` maps every exact catalog row to its
|
|
expected `ContentId` and Stream Install capability, and
|
|
`manifests/<game_id>.json` defines canonical paths, kinds, sizes, 128 MiB
|
|
chunks, and BLAKE3 values. The index has exact database coverage and is loaded
|
|
once; manifest bodies remain on demand and must recompute both indexed fields.
|
|
Disk body loads and full validation recheck the durable publication marker
|
|
before and after their work. Runtime peers cannot add catalog entries or
|
|
replace that byte authority.
|
|
- Authenticated remote-availability joins use the compact index only. Unknown or
|
|
mismatched `(game_id, content_id)` pairs are rejected without filesystem I/O
|
|
or manifest parsing. Outbound serving (chunk and Stream Install admission)
|
|
also gates on the compact index and in-memory local readiness first and then
|
|
reads the manifest body from the validated cache only: every publishable
|
|
game's body was primed before its library revision became visible, so an
|
|
anonymous request can neither trigger a disk read nor populate the manifest
|
|
cache. Download planning on the requesting side still loads and validates the
|
|
full body on demand.
|
|
- The local catalog artifact is converted into a `ValidatedDownloadManifest`
|
|
before any destination mutation. It contains canonical game-root-relative
|
|
paths and rejects aliases, reserved state, shape conflicts, and bounded-size
|
|
violations as one unit. Remote peers never define the storage plan.
|
|
- Source selection requires the exact catalog `(game_id, content_id)` pair from
|
|
an authenticated peer's current library slice. Every ordinary chunk request
|
|
repeats the exact `ContentId`, `CanonicalCatalogPath`, offset, and length.
|
|
There is no version-only or address-only eligibility adapter.
|
|
- A new manifest target that already exists without prior committed ownership is
|
|
rejected before the sentinel, ownership journal, or payload is mutated.
|
|
- Download mutation holds a capability handle for the direct catalog game root.
|
|
Directory components and final files are reopened relative to that handle
|
|
without following links or Windows reparse points; chunk writes and checks use
|
|
the same opened file handle.
|
|
- Downloads hash every chunk, including the buffered `version.ini`, as it is
|
|
received and require the catalog BLAKE3 value, exact length, and exact end of
|
|
stream. One absolute ten-minute application deadline covers the complete
|
|
open/request/receive/check lifecycle of each ordinary chunk.
|
|
- An integrity failure quarantines `(PeerId, content_id)` in shared memory for
|
|
the rest of that peer runtime, independent of address changes. Transport
|
|
failures remain retryable without quarantine. Each failed chunk may try every
|
|
distinct eligible peer identity once; there is no separate numeric retry cap.
|
|
- A no-transfer local shortcut is allowed only when settled ownership records
|
|
the exact expected committed content ID. A matching `version.ini` alone, or a
|
|
legacy/pre-content-ID record, is not catalog-content proof.
|
|
- A game is transferable only when its ID is in the catalog, no operation is
|
|
active for that ID, and the root-level `version.ini` sentinel exists.
|
|
- The sender admits only an exact catalog path and, for ranged requests, an
|
|
exact catalog chunk range before opening the retained no-follow file handle.
|
|
- `local/` paths are never served, even if a stale or malicious manifest request
|
|
asks for them.
|
|
- Cancelling or recovering a download removes only paths named by its durable
|
|
ownership journal and its validated pending manifest. Unknown user files,
|
|
`local/`, and install transaction metadata are preserved, so a cancelled
|
|
update of an installed game settles as local-only.
|
|
|
|
### Ordinary install extraction
|
|
|
|
- `install`/`update` extract every direct regular root `.eti` archive with the
|
|
external `unrar` unpacker into the staging directory. The archives were
|
|
BLAKE3-verified against the catalog when downloaded.
|
|
- `unrar` runs with `-ol-`, so symbolic-link entries are skipped, and the
|
|
staging tree is walked without following links before promotion. Any symlink
|
|
(or reparse point on Windows) aborts the transaction and rolls staging back,
|
|
so a link can never redirect later launch-time rewrites or uninstall.
|
|
|
|
### Streamed install integrity
|
|
|
|
- Low-disk streamed installs request archive-derived file bytes from one peer
|
|
and write them directly into the install transaction staging directory.
|
|
- A sender admits Stream Install only for a manifest with verified extracted
|
|
output and only when its direct regular root `.eti` set exactly equals the
|
|
catalog archive set. Missing or extra archives stop before the extraction
|
|
provider receives authority.
|
|
- The Stream Install request names the exact catalog `ContentId`. All
|
|
path-bearing frames, including archive names, use `CanonicalCatalogPath` and
|
|
the fallible checked decoder distinguishes malformed framing from an explicit
|
|
sender `Error` frame.
|
|
- The receiver independently requires the exact catalog archive set and the
|
|
complete extracted path/kind/size set. It hashes every regular file with
|
|
BLAKE3 while writing and verifies every expected entry before commit. Regular
|
|
files may occur only once across the entire archive set; repeated directory
|
|
entries are allowed because they carry no bytes. RAR CRC32 remains only an
|
|
early corruption signal, not an authority boundary.
|
|
- Integrity failures quarantine the source for that catalog content and retry a
|
|
fresh transaction from another eligible peer. Transport failures retry without
|
|
quarantine; local I/O and cancellation stop. Every failed attempt rolls back
|
|
before the next source begins.
|
|
- After complete catalog verification, account/language/persona settings are
|
|
rewritten in staging before promotion. The one-shot launch-settings marker is
|
|
written only after successful promotion; if it cannot be written, first play
|
|
safely retries the rewrite.
|
|
- A game without a verified extracted-file manifest neither offers nor accepts
|
|
Stream Install. There is no CRC32-only fallback.
|
|
|
|
## Catalog publication and packaging
|
|
|
|
- `lanspread-catalog-publisher` generates production manifests and their
|
|
complete compact identity index beside the canonical packages, independently
|
|
rebuilds/verifies each selected artifact, and uses a durable corpus marker so
|
|
an interrupted body/index publication fails closed. Incremental generation
|
|
requires an existing exact indexed corpus and atomically republishes the full
|
|
index after the selected bodies. After it owns the marker, it freshly
|
|
validates every unselected body/index pair before deriving that mixed index;
|
|
no pre-marker snapshot can roll another completed publication backward.
|
|
- `lanspread-fixture-catalog` is a separate test-only generator. It derives
|
|
reduced peer-CLI `game.db` files and manifests from explicitly selected
|
|
fixtures; those outputs are development and acceptance-test authority only.
|
|
- Tauri fixture builds require both the exact committed development resource map
|
|
and `LANSPREAD_USE_FIXTURE_CATALOG=1`. Every other build mode defaults to the
|
|
production resource map, and the custom production profile cannot be
|
|
downgraded to fixtures.
|
|
- Production packaging requires the complete production `game.db`/manifest
|
|
corpus to pass `check --all`. The canonical production packages and their 186
|
|
generated manifests are not present in this checkout, so that gate remains
|
|
intentionally blocked; fixture success is not production completion.
|
|
|
|
## Fault tolerance rules
|
|
|
|
- Every authenticated peer is keyed by `PeerId`, not by IP address.
|
|
- An authenticated `PeerEndpoint` retains the candidate or explicitly supplied
|
|
listener address. Payload fields and ephemeral QUIC source ports cannot
|
|
rewrite it, and no network caller can dial an address without the expected
|
|
responder ID.
|
|
- Every successful authentication assigns a fresh endpoint generation. Refresh,
|
|
liveness update, and stale removal are conditional on the exact endpoint and
|
|
generation they observed.
|
|
- Runtime-session IDs and per-domain revisions prevent delayed snapshots from
|
|
rolling state back across restart or endpoint replacement. Change hints never
|
|
bypass the pinned pull and generation checks.
|
|
- A generation-conditional authenticated departure removes that peer's library
|
|
and Call-to-Play slices and publishes complete replacement views. Ordinary
|
|
short-lived QUIC connection closure is not roster departure.
|
|
- Protocol 8 is strict and current-only. Unknown fields, noncanonical typed IDs
|
|
or paths, duplicate/unsorted library rows, oversized domains, extra control
|
|
frames, and v7 messages are rejected instead of adapted.
|