diff --git a/crates/lanspread-peer/src/services/transfer.rs b/crates/lanspread-peer/src/services/transfer.rs index faeb9f5..13d9f56 100644 --- a/crates/lanspread-peer/src/services/transfer.rs +++ b/crates/lanspread-peer/src/services/transfer.rs @@ -62,6 +62,30 @@ fn control_codec() -> LengthDelimitedCodec { .new_codec() } +/// Cheap identity gate backed by the compact content index: no manifest body +/// is read or retained for a game ID or content ID this catalog does not +/// publish. +fn content_identity_matches( + ctx: &PeerCtx, + game_id: &str, + content_id: ContentId, + request_label: &str, +) -> bool { + match ctx.catalog.content_identity(game_id) { + Some(identity) if identity.content_id == content_id => true, + Some(_) => { + log::warn!( + "Declining {request_label} for {game_id}: requested content {content_id} does not match the local catalog" + ); + false + } + None => { + log::warn!("Declining {request_label} for unknown catalog game {game_id}"); + false + } + } +} + fn load_expected_catalog_manifest( ctx: &PeerCtx, game_id: &str, @@ -255,6 +279,12 @@ struct AdmittedOutboundTransfer { /// Validates content identity before readiness checks, filesystem opens, /// transfer registration, or provider work. `SetGameDir` holds the same /// admission barrier while draining the prior directory epoch. +/// +/// Ordering matters for cost: the compact content index answers identity +/// and streamed-install support without touching disk, and local readiness +/// is an in-memory lookup. Only a request that passes both is allowed to +/// load (and thereby cache) the full catalog manifest, so anonymous requests +/// for games this node does not serve cannot populate the manifest cache. async fn admit_outbound_transfer( ctx: &PeerCtx, game_id: &str, @@ -273,16 +303,13 @@ async fn admit_outbound_transfer( offset, length, } => { - let manifest = load_expected_catalog_manifest(ctx, game_id)?; - if manifest.content_id() != content_id { - log::warn!( - "Declining catalog chunk for {game_id}: requested content {content_id} does not match the local catalog" - ); + if !content_identity_matches(ctx, game_id, content_id, "catalog chunk") { return None; } if !can_serve_game(ctx, &game_dir, game_id).await { return None; } + let manifest = load_expected_catalog_manifest(ctx, game_id)?; let authorized_entry = authorize_catalog_file_request( &manifest, game_id, @@ -306,16 +333,13 @@ async fn admit_outbound_transfer( AdmittedOutboundPayload::CatalogFile { file } } OutboundTransferRequest::StreamInstall { content_id } => { - let manifest = load_expected_catalog_manifest(ctx, game_id)?; - if manifest.content_id() != content_id { - log::warn!( - "Declining StreamInstall for {game_id}: requested content {content_id} does not match the local catalog" - ); + if !content_identity_matches(ctx, game_id, content_id, "StreamInstall") + || !can_serve_game(ctx, &game_dir, game_id).await + { return None; } - if !can_serve_game(ctx, &game_dir, game_id).await - || !manifest.supports_streamed_install() - { + let manifest = load_expected_catalog_manifest(ctx, game_id)?; + if !manifest.supports_streamed_install() { return None; } AdmittedOutboundPayload::StreamInstall { game_dir, manifest }