feat(relay): notify peers when room members leave
The relay now sends a reliable PeerLeft control event to remaining room peers after removing the departed peer from session and room state. Normal connection closure is reported as Normal, while malformed-datagram disconnects are reported as ProtocolError. This completes the first relay-side lifecycle event pair. Delivery remains best-effort and client-side event consumption is intentionally left for a separate slice. Test Plan: - cargo fmt --check - cargo test -p lanparty-relay forwards_ethernet_datagrams_between_joined_peers \ -- --nocapture - 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 Refs: PLAN.md
This commit is contained in:
@@ -84,7 +84,7 @@ Public relay binary and relay-owned room state:
|
||||
- one gateway per room, duplicate client MAC rejection, and room limits
|
||||
- stable effective room MTU chosen before Ethernet datagrams flow
|
||||
- live Ethernet datagram forwarding with no ingress reflection
|
||||
- reliable `PeerJoined` notifications to existing room peers
|
||||
- reliable `PeerJoined`/`PeerLeft` notifications to existing room peers
|
||||
- L2 safety filters for jumbo, switch-control, DHCP-server, and IPv6-RA frames
|
||||
- client broadcast/multicast, unknown-unicast, and total bandwidth limiting
|
||||
- malformed peer datagram disconnect threshold
|
||||
@@ -112,8 +112,8 @@ certificate handling remains future work. Ethernet forwarding decisions are
|
||||
logged with room, peer, MAC, ethertype, action, drop reason, and target count.
|
||||
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, the relay sends a reliable `PeerJoined` control event to
|
||||
peers that were already present in the room.
|
||||
When a peer joins or leaves, the relay sends a reliable lifecycle control event
|
||||
to peers that are still present in the room.
|
||||
|
||||
## Gateway
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{fs, net::SocketAddr, path::Path, sync::Arc};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use bytes::Bytes;
|
||||
use lanparty_ctrl::{
|
||||
CONTROL_LENGTH_PREFIX_LEN, ControlCodecError, ControlMessage, EndpointHello,
|
||||
CONTROL_LENGTH_PREFIX_LEN, ControlCodecError, ControlMessage, DisconnectReason, EndpointHello,
|
||||
MAX_CONTROL_MESSAGE_LEN, PeerInfo, RELAY_ALPN, Reject, RejectReason, Role, RoomCode,
|
||||
ServerWelcome, decode_control_frame, encode_control_message,
|
||||
};
|
||||
@@ -65,6 +65,28 @@ enum PeerDatagramOutcome {
|
||||
Malformed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct PeerClose {
|
||||
reason: DisconnectReason,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl PeerClose {
|
||||
fn normal(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
reason: DisconnectReason::Normal,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn protocol_error(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
reason: DisconnectReason::ProtocolError,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MalformedDatagramTracker {
|
||||
count: usize,
|
||||
@@ -203,13 +225,20 @@ async fn handle_incoming_connection(
|
||||
accepted.welcome.effective_tap_mtu()
|
||||
);
|
||||
|
||||
let close_reason = run_peer_datagrams(&rooms, &sessions, &accepted, &connection).await;
|
||||
leave_peer(&rooms, &sessions, &accepted.room, accepted.peer.peer_id()).await?;
|
||||
let close = run_peer_datagrams(&rooms, &sessions, &accepted, &connection).await;
|
||||
let leave = leave_peer(&rooms, &sessions, &accepted.room, accepted.peer.peer_id()).await?;
|
||||
notify_peer_left(
|
||||
&sessions,
|
||||
&accepted.room,
|
||||
leave.peer().peer_id(),
|
||||
close.reason.clone(),
|
||||
)
|
||||
.await;
|
||||
println!(
|
||||
"peer {} left room {}: {}",
|
||||
accepted.peer.peer_id(),
|
||||
accepted.room,
|
||||
close_reason
|
||||
close.message
|
||||
);
|
||||
|
||||
Ok(Some(accepted))
|
||||
@@ -268,7 +297,7 @@ async fn run_peer_datagrams(
|
||||
sessions: &Arc<Mutex<HashMap<PeerKey, PeerSession>>>,
|
||||
accepted: &AcceptedPeer,
|
||||
connection: &quinn::Connection,
|
||||
) -> String {
|
||||
) -> PeerClose {
|
||||
let mut malformed_tracker = MalformedDatagramTracker::default();
|
||||
|
||||
loop {
|
||||
@@ -279,7 +308,7 @@ async fn run_peer_datagrams(
|
||||
Ok(PeerDatagramOutcome::Malformed) => {
|
||||
if let Some(reason) = malformed_tracker.record_malformed() {
|
||||
connection.close(0_u32.into(), reason.as_bytes());
|
||||
return reason;
|
||||
return PeerClose::protocol_error(reason);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -291,7 +320,7 @@ async fn run_peer_datagrams(
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => return error.to_string(),
|
||||
Err(error) => return PeerClose::normal(error.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,6 +479,22 @@ async fn notify_peer_joined(
|
||||
}
|
||||
}
|
||||
|
||||
async fn notify_peer_left(
|
||||
sessions: &Arc<Mutex<HashMap<PeerKey, PeerSession>>>,
|
||||
room: &RoomCode,
|
||||
peer_id: u32,
|
||||
reason: DisconnectReason,
|
||||
) {
|
||||
let target_sessions = collect_room_sessions_except(sessions, room, peer_id).await;
|
||||
let message = ControlMessage::PeerLeft { peer_id, reason };
|
||||
|
||||
for target in target_sessions {
|
||||
if let Err(error) = send_control_event(&target.connection, &message).await {
|
||||
eprintln!("failed to notify room {room} about peer {peer_id} leaving: {error:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_room_sessions_except(
|
||||
sessions: &Arc<Mutex<HashMap<PeerKey, PeerSession>>>,
|
||||
room: &RoomCode,
|
||||
@@ -579,18 +624,18 @@ async fn leave_peer(
|
||||
sessions: &Arc<Mutex<HashMap<PeerKey, PeerSession>>>,
|
||||
room: &RoomCode,
|
||||
peer_id: u32,
|
||||
) -> Result<()> {
|
||||
) -> Result<crate::LeaveResult> {
|
||||
sessions
|
||||
.lock()
|
||||
.await
|
||||
.remove(&PeerKey::new(room.clone(), peer_id));
|
||||
rooms
|
||||
let leave = rooms
|
||||
.lock()
|
||||
.await
|
||||
.leave(room, peer_id)
|
||||
.with_context(|| format!("failed to remove peer {peer_id} from room {room}"))?;
|
||||
|
||||
Ok(())
|
||||
Ok(leave)
|
||||
}
|
||||
|
||||
fn write_development_certificate(path: &Path, certificate: &CertificateDer<'_>) -> Result<()> {
|
||||
@@ -883,6 +928,13 @@ mod tests {
|
||||
assert_eq!(first_welcome.room_id(), second_welcome.room_id());
|
||||
|
||||
first_connection.close(0_u32.into(), b"test complete");
|
||||
let event = read_control_event(&second_connection).await;
|
||||
let ControlMessage::PeerLeft { peer_id, reason } = event else {
|
||||
panic!("expected peer left event");
|
||||
};
|
||||
assert_eq!(peer_id, first_welcome.peer_id());
|
||||
assert_eq!(reason, DisconnectReason::Normal);
|
||||
|
||||
second_connection.close(0_u32.into(), b"test complete");
|
||||
first_endpoint.wait_idle().await;
|
||||
second_endpoint.wait_idle().await;
|
||||
|
||||
Reference in New Issue
Block a user