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.
This commit is contained in:
@@ -20,6 +20,7 @@ use lanspread_proto::{
|
|||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
|
|
||||||
pub const MAX_AUTHENTICATED_PEERS: usize = 64;
|
pub const MAX_AUTHENTICATED_PEERS: usize = 64;
|
||||||
|
const MAX_TOTAL_REMOTE_LIBRARY_GAMES: usize = 16_384;
|
||||||
|
|
||||||
/// Monotonic identity for one successfully authenticated endpoint observation.
|
/// Monotonic identity for one successfully authenticated endpoint observation.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
@@ -380,9 +381,23 @@ impl PeerGameDB {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let incoming_library = library
|
let incoming_library = library.as_ref().map(|snapshot| {
|
||||||
.as_ref()
|
let games = library_map(snapshot);
|
||||||
.map(|snapshot| (snapshot.revision, library_map(snapshot)));
|
let current_count = self
|
||||||
|
.peers
|
||||||
|
.values()
|
||||||
|
.map(|peer| peer.games.len())
|
||||||
|
.sum::<usize>();
|
||||||
|
let replaced_count = self
|
||||||
|
.peers
|
||||||
|
.get(&endpoint.peer_id)
|
||||||
|
.map_or(0, |peer| peer.games.len());
|
||||||
|
let projected_count = current_count
|
||||||
|
.saturating_sub(replaced_count)
|
||||||
|
.saturating_add(games.len());
|
||||||
|
let admitted = (projected_count <= MAX_TOTAL_REMOTE_LIBRARY_GAMES).then_some(games);
|
||||||
|
(snapshot.revision, admitted)
|
||||||
|
});
|
||||||
if let Some(peer) = self.peers.get_mut(&peer_id) {
|
if let Some(peer) = self.peers.get_mut(&peer_id) {
|
||||||
let previous_endpoint =
|
let previous_endpoint =
|
||||||
(peer.addr != addr).then(|| PeerEndpoint::new(peer.peer_id, peer.addr));
|
(peer.addr != addr).then(|| PeerEndpoint::new(peer.peer_id, peer.addr));
|
||||||
@@ -401,6 +416,7 @@ impl PeerGameDB {
|
|||||||
.library_revision
|
.library_revision
|
||||||
.is_none_or(|current| revision > current) =>
|
.is_none_or(|current| revision > current) =>
|
||||||
{
|
{
|
||||||
|
let games = games.unwrap_or_default();
|
||||||
let changed = peer.library_revision != Some(revision) || peer.games != games;
|
let changed = peer.library_revision != Some(revision) || peer.games != games;
|
||||||
peer.library_revision = Some(revision);
|
peer.library_revision = Some(revision);
|
||||||
peer.games = games;
|
peer.games = games;
|
||||||
@@ -446,7 +462,9 @@ impl PeerGameDB {
|
|||||||
runtime_session_id,
|
runtime_session_id,
|
||||||
library_revision: incoming_library.as_ref().map(|(revision, _)| *revision),
|
library_revision: incoming_library.as_ref().map(|(revision, _)| *revision),
|
||||||
call_to_play_revision: None,
|
call_to_play_revision: None,
|
||||||
games: incoming_library.map_or_else(HashMap::new, |(_, games)| games),
|
games: incoming_library
|
||||||
|
.and_then(|(_, games)| games)
|
||||||
|
.unwrap_or_default(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.addr_index.insert(addr, peer_id);
|
self.addr_index.insert(addr, peer_id);
|
||||||
@@ -745,6 +763,18 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn large_library(revision: u64, seed: u8) -> LibrarySnapshot {
|
||||||
|
LibrarySnapshot {
|
||||||
|
revision,
|
||||||
|
games: (0..lanspread_proto::MAX_LIBRARY_GAMES)
|
||||||
|
.map(|index| GameAvailability {
|
||||||
|
game_id: format!("game-{seed}-{index:04}"),
|
||||||
|
content_id: ContentId::from_bytes([seed; 32]),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn commit(
|
fn commit(
|
||||||
db: &mut PeerGameDB,
|
db: &mut PeerGameDB,
|
||||||
endpoint: PeerEndpoint,
|
endpoint: PeerEndpoint,
|
||||||
@@ -785,6 +815,51 @@ mod tests {
|
|||||||
assert_eq!(db.peer_endpoint(&endpoint.peer_id), Some(endpoint));
|
assert_eq!(db.peer_endpoint(&endpoint.peer_id), Some(endpoint));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregate_remote_library_budget_drops_only_the_oversized_projection() {
|
||||||
|
let mut db = PeerGameDB::new();
|
||||||
|
for seed in 1..=4 {
|
||||||
|
commit(
|
||||||
|
&mut db,
|
||||||
|
endpoint(seed, 12_000 + u16::from(seed)),
|
||||||
|
session(seed),
|
||||||
|
large_library(u64::from(seed), seed),
|
||||||
|
Some(0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let fifth = endpoint(5, 12_005);
|
||||||
|
let ticket = db
|
||||||
|
.begin_candidate_negotiation(fifth)
|
||||||
|
.expect("fifth candidate should reserve");
|
||||||
|
let upsert = db
|
||||||
|
.commit_authenticated_snapshot(fifth, ticket, session(5), Some(large_library(5, 5)))
|
||||||
|
.expect("fifth snapshot should still authenticate")
|
||||||
|
.expect("fifth ticket should remain current");
|
||||||
|
|
||||||
|
assert!(upsert.library_changed);
|
||||||
|
assert_eq!(
|
||||||
|
db.peer_snapshots()
|
||||||
|
.iter()
|
||||||
|
.find(|peer| peer.peer_id == fifth.peer_id)
|
||||||
|
.expect("fifth peer should be retained")
|
||||||
|
.game_count,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
db.revision_snapshot(&fifth.peer_id)
|
||||||
|
.expect("fifth peer should have a revision watermark")
|
||||||
|
.library_revision,
|
||||||
|
Some(5),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
db.peer_snapshots()
|
||||||
|
.iter()
|
||||||
|
.map(|peer| peer.game_count)
|
||||||
|
.sum::<usize>(),
|
||||||
|
MAX_TOTAL_REMOTE_LIBRARY_GAMES
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_remote_peers_returns_peer_id_sorted_retirements() {
|
fn clear_remote_peers_returns_peer_id_sorted_retirements() {
|
||||||
let mut db = PeerGameDB::new();
|
let mut db = PeerGameDB::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user