feat(relay): notify peers when rooms change
The relay now sends a reliable PeerJoined control event to peers that were already present in the room after a new peer completes the hello/welcome handshake. Events are sent on one-frame unidirectional QUIC streams, reusing the existing control codec without keeping the room/session lock across I/O. Delivery is best-effort for this first lifecycle slice: a notification failure is logged, but the newly accepted peer remains joined. PeerLeft delivery and client-side event consumption remain separate follow-up work. 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,6 +84,7 @@ Public relay binary and relay-owned room state:
|
|||||||
- one gateway per room, duplicate client MAC rejection, and room limits
|
- one gateway per room, duplicate client MAC rejection, and room limits
|
||||||
- stable effective room MTU chosen before Ethernet datagrams flow
|
- stable effective room MTU chosen before Ethernet datagrams flow
|
||||||
- live Ethernet datagram forwarding with no ingress reflection
|
- live Ethernet datagram forwarding with no ingress reflection
|
||||||
|
- reliable `PeerJoined` notifications to existing room peers
|
||||||
- L2 safety filters for jumbo, switch-control, DHCP-server, and IPv6-RA frames
|
- L2 safety filters for jumbo, switch-control, DHCP-server, and IPv6-RA frames
|
||||||
- client broadcast/multicast, unknown-unicast, and total bandwidth limiting
|
- client broadcast/multicast, unknown-unicast, and total bandwidth limiting
|
||||||
- malformed peer datagram disconnect threshold
|
- malformed peer datagram disconnect threshold
|
||||||
@@ -111,6 +112,8 @@ certificate handling remains future work. Ethernet forwarding decisions are
|
|||||||
logged with room, peer, MAC, ethertype, action, drop reason, and target count.
|
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
|
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.
|
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.
|
||||||
|
|
||||||
## Gateway
|
## Gateway
|
||||||
|
|
||||||
|
|||||||
@@ -242,6 +242,10 @@ async fn accept_control_handshake(
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(accepted) = &accepted {
|
||||||
|
notify_peer_joined(sessions, accepted).await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(accepted)
|
Ok(accepted)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,6 +431,50 @@ async fn collect_target_sessions(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn notify_peer_joined(
|
||||||
|
sessions: &Arc<Mutex<HashMap<PeerKey, PeerSession>>>,
|
||||||
|
accepted: &AcceptedPeer,
|
||||||
|
) {
|
||||||
|
let target_sessions =
|
||||||
|
collect_room_sessions_except(sessions, &accepted.room, accepted.peer.peer_id()).await;
|
||||||
|
let message = ControlMessage::PeerJoined(accepted.peer.clone());
|
||||||
|
|
||||||
|
for target in target_sessions {
|
||||||
|
if let Err(error) = send_control_event(&target.connection, &message).await {
|
||||||
|
eprintln!(
|
||||||
|
"failed to notify room {} about peer {} joining: {error:#}",
|
||||||
|
accepted.room,
|
||||||
|
accepted.peer.peer_id()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn collect_room_sessions_except(
|
||||||
|
sessions: &Arc<Mutex<HashMap<PeerKey, PeerSession>>>,
|
||||||
|
room: &RoomCode,
|
||||||
|
excluded_peer_id: u32,
|
||||||
|
) -> Vec<PeerSession> {
|
||||||
|
let sessions = sessions.lock().await;
|
||||||
|
|
||||||
|
sessions
|
||||||
|
.iter()
|
||||||
|
.filter(|(key, _)| key.room == *room && key.peer_id != excluded_peer_id)
|
||||||
|
.map(|(_, session)| session.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_control_event(
|
||||||
|
connection: &quinn::Connection,
|
||||||
|
message: &ControlMessage,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut send = connection
|
||||||
|
.open_uni()
|
||||||
|
.await
|
||||||
|
.context("failed to open relay control event stream")?;
|
||||||
|
send_control_message(&mut send, message).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn build_handshake_response(
|
async fn build_handshake_response(
|
||||||
rooms: &Arc<Mutex<RoomRegistry>>,
|
rooms: &Arc<Mutex<RoomRegistry>>,
|
||||||
connection: &quinn::Connection,
|
connection: &quinn::Connection,
|
||||||
@@ -516,12 +564,12 @@ fn reject_codec_error(error: ControlCodecError) -> Reject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn send_control_message(send: &mut SendStream, message: &ControlMessage) -> Result<()> {
|
async fn send_control_message(send: &mut SendStream, message: &ControlMessage) -> Result<()> {
|
||||||
let response = encode_control_message(message).context("failed to encode control response")?;
|
let frame = encode_control_message(message).context("failed to encode control message")?;
|
||||||
send.write_all(&response)
|
send.write_all(&frame)
|
||||||
.await
|
.await
|
||||||
.context("failed to write control response")?;
|
.context("failed to write control message")?;
|
||||||
send.finish()
|
send.finish()
|
||||||
.map_err(|error| anyhow!("failed to finish control response stream: {error}"))?;
|
.map_err(|error| anyhow!("failed to finish control message stream: {error}"))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -799,6 +847,13 @@ mod tests {
|
|||||||
let second_mac = client_mac(2);
|
let second_mac = client_mac(2);
|
||||||
let first_welcome = welcome_for_client(&first_connection, first_mac).await;
|
let first_welcome = welcome_for_client(&first_connection, first_mac).await;
|
||||||
let second_welcome = welcome_for_client(&second_connection, second_mac).await;
|
let second_welcome = welcome_for_client(&second_connection, second_mac).await;
|
||||||
|
let event = read_control_event(&first_connection).await;
|
||||||
|
let ControlMessage::PeerJoined(peer) = event else {
|
||||||
|
panic!("expected peer joined event");
|
||||||
|
};
|
||||||
|
assert_eq!(peer.peer_id(), second_welcome.peer_id());
|
||||||
|
assert_eq!(peer.role(), Role::Client);
|
||||||
|
assert_eq!(peer.mac(), Some(second_mac));
|
||||||
|
|
||||||
let ethernet = ethernet_frame(second_mac, first_mac);
|
let ethernet = ethernet_frame(second_mac, first_mac);
|
||||||
let datagram = encode_datagram(
|
let datagram = encode_datagram(
|
||||||
@@ -890,6 +945,16 @@ mod tests {
|
|||||||
Ok(decode_control_frame(&response)?)
|
Ok(decode_control_frame(&response)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn read_control_event(connection: &quinn::Connection) -> ControlMessage {
|
||||||
|
let mut recv = tokio::time::timeout(Duration::from_secs(5), connection.accept_uni())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let frame = recv.read_to_end(MAX_CONTROL_FRAME_LEN).await.unwrap();
|
||||||
|
|
||||||
|
decode_control_frame(&frame).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
async fn welcome_for_client(connection: &quinn::Connection, mac: MacAddr) -> ServerWelcome {
|
async fn welcome_for_client(connection: &quinn::Connection, mac: MacAddr) -> ServerWelcome {
|
||||||
let hello = EndpointHello::client(RoomCode::new("TESTROOM").unwrap(), mac, 1400).unwrap();
|
let hello = EndpointHello::client(RoomCode::new("TESTROOM").unwrap(), mac, 1400).unwrap();
|
||||||
let response = request_control_message(connection, ControlMessage::Hello(hello))
|
let response = request_control_message(connection, ControlMessage::Hello(hello))
|
||||||
|
|||||||
Reference in New Issue
Block a user