diff --git a/README.md b/README.md index 72f7379..611db6f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/crates/lanparty-relay/src/server.rs b/crates/lanparty-relay/src/server.rs index 3f91866..2184c46 100644 --- a/crates/lanparty-relay/src/server.rs +++ b/crates/lanparty-relay/src/server.rs @@ -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>, + sessions: &Arc>>, + 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>, sessions: &Arc>>, @@ -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()));