fix(relay): seed gateways before join notifications
The relay used one accepted-peer handshake order for clients and gateways: register the peer, notify existing peers, send the welcome, then send catch-up events to the new peer. That is correct for a client joining an existing gateway because the gateway must learn the client's MAC before the client can send frames. The same ordering is risky for a gateway joining a room that already has clients. Existing clients could learn that the gateway is available before the gateway has received the existing client MAC table. Reverse that part of the sequence for gateways: send the gateway welcome, send existing peer catch-up events to the gateway, then notify clients that the gateway joined. Keep client joins on the old ordering, and keep handshake-failure cleanup quiet for gateway welcome failures because clients have not been notified yet. Test Plan: - cargo fmt --check - cargo test -p lanparty-relay - cargo clippy -p lanparty-relay --all-targets -- -D warnings - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check - git diff --cached --check Refs: MVP gateway lifecycle ordering
This commit is contained in:
@@ -153,9 +153,11 @@ Unknown unicast from a client is forwarded only to the gateway port; unknown
|
||||
unicast from the gateway is dropped instead of flooded to every remote client.
|
||||
When a peer joins or leaves, the relay sends a reliable lifecycle control event
|
||||
to peers that are still present in the room. Newly joined peers also receive
|
||||
`PeerJoined` events for peers that were already present. Accepted joins notify
|
||||
existing peers before the joining peer receives its welcome, so gateways can
|
||||
seed client MAC state before a freshly accepted client starts sending frames.
|
||||
`PeerJoined` events for peers that were already present. When a client joins,
|
||||
the relay notifies existing peers before the client receives its welcome, so
|
||||
gateways can seed client MAC state before that client starts sending frames.
|
||||
When a gateway joins, the relay gives the gateway the current client list
|
||||
before notifying clients that the gateway is available.
|
||||
|
||||
### MVP Trust Model
|
||||
|
||||
|
||||
@@ -76,6 +76,28 @@ enum PeerControlOutcome {
|
||||
Close(PeerClose),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum AcceptedHandshakeStep {
|
||||
NotifyJoinedPeerToExisting,
|
||||
SendWelcome,
|
||||
NotifyExistingPeersToJoined,
|
||||
}
|
||||
|
||||
const CLIENT_HANDSHAKE_STEPS: &[AcceptedHandshakeStep] = &[
|
||||
// Existing peers, especially the gateway, need the new client's MAC before
|
||||
// the client can receive its welcome and send Ethernet datagrams.
|
||||
AcceptedHandshakeStep::NotifyJoinedPeerToExisting,
|
||||
AcceptedHandshakeStep::SendWelcome,
|
||||
AcceptedHandshakeStep::NotifyExistingPeersToJoined,
|
||||
];
|
||||
const GATEWAY_HANDSHAKE_STEPS: &[AcceptedHandshakeStep] = &[
|
||||
AcceptedHandshakeStep::SendWelcome,
|
||||
// A newly joined gateway needs the current client MAC table before clients
|
||||
// learn that a gateway is available and send first traffic.
|
||||
AcceptedHandshakeStep::NotifyExistingPeersToJoined,
|
||||
AcceptedHandshakeStep::NotifyJoinedPeerToExisting,
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct PeerClose {
|
||||
reason: DisconnectReason,
|
||||
@@ -274,33 +296,45 @@ async fn accept_control_handshake(
|
||||
.context("failed to read relay control hello")?;
|
||||
|
||||
let (accepted, response) = build_handshake_response(rooms, connection, frame.as_slice()).await;
|
||||
if let Some(accepted) = &accepted {
|
||||
register_peer(sessions, accepted, connection.clone()).await;
|
||||
// Seed existing peers before the joining peer can send Ethernet datagrams.
|
||||
notify_peer_joined(sessions, accepted).await;
|
||||
}
|
||||
let Some(accepted) = accepted else {
|
||||
send_control_message(&mut send, &response).await?;
|
||||
|
||||
if let Err(error) = send_control_message(&mut send, &response).await {
|
||||
if let Some(accepted) = &accepted {
|
||||
let leave =
|
||||
leave_peer(rooms, sessions, &accepted.room, accepted.peer.peer_id()).await?;
|
||||
notify_peer_left(
|
||||
sessions,
|
||||
&accepted.room,
|
||||
leave.peer().peer_id(),
|
||||
DisconnectReason::Normal,
|
||||
)
|
||||
.await;
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
register_peer(sessions, &accepted, connection.clone()).await;
|
||||
for step in accepted_handshake_steps(accepted.peer.role()) {
|
||||
match step {
|
||||
AcceptedHandshakeStep::NotifyJoinedPeerToExisting => {
|
||||
notify_peer_joined(sessions, &accepted).await;
|
||||
}
|
||||
AcceptedHandshakeStep::SendWelcome => {
|
||||
if let Err(error) = send_control_message(&mut send, &response).await {
|
||||
cleanup_accepted_handshake(
|
||||
rooms,
|
||||
sessions,
|
||||
&accepted,
|
||||
accepted.peer.role() == Role::Client,
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
AcceptedHandshakeStep::NotifyExistingPeersToJoined => {
|
||||
notify_existing_peers_to_joined_peer(rooms, connection, &accepted).await;
|
||||
}
|
||||
}
|
||||
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
if let Some(accepted) = &accepted {
|
||||
notify_existing_peers_to_joined_peer(rooms, connection, accepted).await;
|
||||
}
|
||||
Ok(Some(accepted))
|
||||
}
|
||||
|
||||
Ok(accepted)
|
||||
const fn accepted_handshake_steps(role: Role) -> &'static [AcceptedHandshakeStep] {
|
||||
match role {
|
||||
Role::Client => CLIENT_HANDSHAKE_STEPS,
|
||||
Role::Gateway => GATEWAY_HANDSHAKE_STEPS,
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_peer(
|
||||
@@ -319,6 +353,26 @@ async fn register_peer(
|
||||
);
|
||||
}
|
||||
|
||||
async fn cleanup_accepted_handshake(
|
||||
rooms: &Arc<Mutex<RoomRegistry>>,
|
||||
sessions: &Arc<Mutex<HashMap<PeerKey, PeerSession>>>,
|
||||
accepted: &AcceptedPeer,
|
||||
notify_leave: bool,
|
||||
) -> Result<()> {
|
||||
let leave = leave_peer(rooms, sessions, &accepted.room, accepted.peer.peer_id()).await?;
|
||||
if notify_leave {
|
||||
notify_peer_left(
|
||||
sessions,
|
||||
&accepted.room,
|
||||
leave.peer().peer_id(),
|
||||
DisconnectReason::Normal,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_peer_io(
|
||||
rooms: &Arc<Mutex<RoomRegistry>>,
|
||||
sessions: &Arc<Mutex<HashMap<PeerKey, PeerSession>>>,
|
||||
@@ -1143,6 +1197,26 @@ mod tests {
|
||||
assert!(reason.contains("malformed datagrams"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orders_handshake_notifications_by_joining_role() {
|
||||
assert_eq!(
|
||||
accepted_handshake_steps(Role::Client),
|
||||
&[
|
||||
AcceptedHandshakeStep::NotifyJoinedPeerToExisting,
|
||||
AcceptedHandshakeStep::SendWelcome,
|
||||
AcceptedHandshakeStep::NotifyExistingPeersToJoined,
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
accepted_handshake_steps(Role::Gateway),
|
||||
&[
|
||||
AcceptedHandshakeStep::SendWelcome,
|
||||
AcceptedHandshakeStep::NotifyExistingPeersToJoined,
|
||||
AcceptedHandshakeStep::NotifyJoinedPeerToExisting,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn formats_malformed_datagram_log_line() {
|
||||
let rooms = Arc::new(Mutex::new(RoomRegistry::default()));
|
||||
|
||||
Reference in New Issue
Block a user