fix(peer): preserve advertised addresses for QUIC peers

After renewing the dev certificate, peers could complete handshakes but then
lost each other during liveness checks. Inbound QUIC streams report the client's
ephemeral source port, while the peer database is supposed to track the peer's
advertised listening address. Recording the ephemeral address created unstable
peer entries that could not be pinged later.

Resolve transport source addresses back to the unique known peer on the same IP,
and keep an existing advertised address when an inbound Hello arrives from that
peer. Goodbye events now report the stored peer address as well.

This keeps the core peer behavior in lanspread-peer; the CLI only observes the
resulting peer snapshots.

Test Plan:
- just fmt
- just test
- just clippy
- just peer-cli-build
- just peer-cli-image
- just peer-cli-alpha, just peer-cli-bravo, just peer-cli-charlie
- list-peers after the ping idle window shows advertised peer addresses with
  populated game lists instead of ephemeral-port peers disappearing

Refs: PEER_CLI_SCENARIOS.md
This commit is contained in:
2026-05-17 09:34:10 +02:00
parent 84f533aeee
commit 10a1f57183
4 changed files with 97 additions and 11 deletions
@@ -121,7 +121,7 @@ pub(super) async fn accept_inbound_hello(
return build_hello_ack(ctx).await;
}
if let Some(addr) = remote_addr {
if let Some(addr) = peer_record_addr(&ctx.peer_game_db, &hello.peer_id, remote_addr).await {
let upsert = record_remote_library(
&ctx.peer_game_db,
hello.peer_id.clone(),
@@ -147,6 +147,16 @@ pub(super) async fn accept_inbound_hello(
build_hello_ack(ctx).await
}
async fn peer_record_addr(
peer_game_db: &Arc<RwLock<PeerGameDB>>,
peer_id: &PeerId,
remote_addr: Option<SocketAddr>,
) -> Option<SocketAddr> {
let remote_addr = remote_addr?;
let db = peer_game_db.read().await;
Some(db.peer_addr(peer_id).unwrap_or(remote_addr))
}
pub(super) fn spawn_library_resync(
peer_id: Arc<String>,
local_library: Arc<RwLock<LocalLibraryState>>,
@@ -265,3 +275,33 @@ async fn select_library_update(
&library_guard,
)))
}
#[cfg(test)]
mod tests {
use std::{net::SocketAddr, sync::Arc};
use tokio::sync::RwLock;
use super::peer_record_addr;
use crate::peer_db::PeerGameDB;
fn addr(ip: [u8; 4], port: u16) -> SocketAddr {
SocketAddr::from((ip, port))
}
#[tokio::test]
async fn inbound_hello_keeps_existing_listening_addr() {
let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new()));
let advertised = addr([10, 66, 0, 2], 40000);
let transport_source = addr([10, 66, 0, 2], 52000);
peer_game_db
.write()
.await
.upsert_peer("peer".to_string(), advertised);
let record_addr =
peer_record_addr(&peer_game_db, &"peer".to_string(), Some(transport_source)).await;
assert_eq!(record_addr, Some(advertised));
}
}