From 5e349338e418727a3b462d3e641cba2ea0c708e6 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Fri, 22 May 2026 09:37:46 +0200 Subject: [PATCH] test(relay): cover ARP-shaped session frames The MVP pass criteria include ARP from the Windows TAP side to a LAN host. The real-session relay integration tests proved generic Ethernet forwarding, but that did not explicitly exercise ARP-shaped traffic. Add a real client/relay/gateway session test that sends an ARP request-shaped broadcast frame from the client to the gateway and an ARP reply-shaped unicast frame from the gateway back to the client. The test keeps the proof at the L2 boundary while staying independent of a real Windows TAP adapter or LAN host. Test Plan: - cargo fmt --check - cargo test -p lanparty-relay \ bridges_arp_frames_between_client_and_gateway_sessions -- --nocapture - cargo test -p lanparty-relay - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: MVP ARP pass condition --- crates/lanparty-relay/src/server.rs | 166 +++++++++++++++++++++++++++- 1 file changed, 164 insertions(+), 2 deletions(-) diff --git a/crates/lanparty-relay/src/server.rs b/crates/lanparty-relay/src/server.rs index 03d1130..f0bc50f 100644 --- a/crates/lanparty-relay/src/server.rs +++ b/crates/lanparty-relay/src/server.rs @@ -1020,6 +1020,10 @@ mod tests { use super::*; + const ETHERTYPE_ARP: u16 = 0x0806; + const ARP_REQUEST: u16 = 1; + const ARP_REPLY: u16 = 2; + #[tokio::test] async fn binds_quic_endpoint_on_configured_address() { let config = RelayConfig::new( @@ -1632,6 +1636,133 @@ mod tests { assert!(sessions.lock().await.is_empty()); } + #[tokio::test] + async fn bridges_arp_frames_between_client_and_gateway_sessions() { + let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM); + let rooms = Arc::clone(&server.rooms); + let sessions = Arc::clone(&server.sessions); + let server_addr = server.local_addr().unwrap(); + let server_task = tokio::spawn(async move { + let handles = server.accept_many_for_test(2).await.unwrap(); + let mut accepted = Vec::with_capacity(handles.len()); + + for handle in handles { + accepted.push(handle.await.unwrap().unwrap().unwrap()); + } + + server.shutdown("test complete").await; + accepted + }); + let cert_der = certificate.as_ref().to_vec(); + let room = RoomCode::new("TESTROOM").unwrap(); + let client_mac = client_mac(1); + let gateway = connect_gateway( + GatewayConfig::new( + server_addr, + "lanparty-relay.local", + cert_der.clone(), + room.clone(), + "eth0", + 1400, + ) + .unwrap(), + ) + .await + .unwrap(); + let client = connect_client( + ClientSessionConfig::new( + server_addr, + "lanparty-relay.local", + cert_der, + room, + client_mac, + 1400, + ) + .unwrap(), + ) + .await + .unwrap(); + + let ControlMessage::PeerJoined(peer) = + tokio::time::timeout(Duration::from_secs(5), gateway.recv_control_event()) + .await + .unwrap() + .unwrap() + else { + panic!("expected gateway to observe client join"); + }; + assert_eq!(peer.peer_id(), client.welcome().peer_id()); + assert_eq!(peer.role(), Role::Client); + assert_eq!(peer.mac(), Some(client_mac)); + + let ControlMessage::PeerJoined(peer) = + tokio::time::timeout(Duration::from_secs(5), client.recv_control_event()) + .await + .unwrap() + .unwrap() + else { + panic!("expected client to receive gateway catch-up event"); + }; + assert_eq!(peer.peer_id(), gateway.welcome().peer_id()); + assert_eq!(peer.role(), Role::Gateway); + + let arp_request = arp_frame( + MacAddr::BROADCAST, + client_mac, + ARP_REQUEST, + Ipv4Addr::new(10, 73, 42, 51), + MacAddr::ZERO, + Ipv4Addr::new(10, 73, 42, 1), + ); + let request_header = EthernetFrame::parse(&arp_request).unwrap(); + assert_eq!(request_header.ethertype_or_len(), ETHERTYPE_ARP); + assert!(request_header.is_broadcast()); + assert_eq!( + client + .relay_io() + .send_ethernet_with_outcome(&arp_request) + .unwrap(), + lanparty_client_core::ClientSendOutcome::Sent + ); + let received = tokio::time::timeout(Duration::from_secs(5), gateway.recv_ethernet()) + .await + .unwrap() + .unwrap(); + assert_eq!(received.source_peer_id(), client.welcome().peer_id()); + assert_eq!(received.payload(), arp_request.as_slice()); + + let arp_reply = arp_frame( + client_mac, + gateway_mac(), + ARP_REPLY, + Ipv4Addr::new(10, 73, 42, 1), + client_mac, + Ipv4Addr::new(10, 73, 42, 51), + ); + let reply_header = EthernetFrame::parse(&arp_reply).unwrap(); + assert_eq!(reply_header.ethertype_or_len(), ETHERTYPE_ARP); + assert_eq!(reply_header.destination(), client_mac); + gateway.send_ethernet(&arp_reply).unwrap(); + let received = + tokio::time::timeout(Duration::from_secs(5), client.relay_io().recv_ethernet()) + .await + .unwrap() + .unwrap(); + assert_eq!(received.source_peer_id(), gateway.welcome().peer_id()); + assert_eq!(received.payload(), arp_reply.as_slice()); + + client.shutdown("test client done").await; + gateway.shutdown("test gateway done").await; + + let accepted = tokio::time::timeout(Duration::from_secs(5), server_task) + .await + .unwrap() + .unwrap(); + assert_eq!(accepted.len(), 2); + assert_eq!(rooms.lock().await.room_count(), 0); + assert!(sessions.lock().await.is_empty()); + } + #[tokio::test] async fn reconnects_gateway_while_client_stays_joined() { let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM); @@ -2028,11 +2159,42 @@ mod tests { } fn ethernet_frame(destination: MacAddr, source: MacAddr) -> Vec { + ethernet_frame_with_payload(destination, source, 0x0800, b"payload") + } + + fn arp_frame( + destination: MacAddr, + source: MacAddr, + operation: u16, + sender_ip: Ipv4Addr, + target_mac: MacAddr, + target_ip: Ipv4Addr, + ) -> Vec { + let mut payload = Vec::with_capacity(28); + payload.extend_from_slice(&1_u16.to_be_bytes()); + payload.extend_from_slice(&0x0800_u16.to_be_bytes()); + payload.push(6); + payload.push(4); + payload.extend_from_slice(&operation.to_be_bytes()); + payload.extend_from_slice(&source.octets()); + payload.extend_from_slice(&sender_ip.octets()); + payload.extend_from_slice(&target_mac.octets()); + payload.extend_from_slice(&target_ip.octets()); + + ethernet_frame_with_payload(destination, source, ETHERTYPE_ARP, &payload) + } + + fn ethernet_frame_with_payload( + destination: MacAddr, + source: MacAddr, + ethertype: u16, + payload: &[u8], + ) -> Vec { let mut frame = Vec::new(); frame.extend_from_slice(&destination.octets()); frame.extend_from_slice(&source.octets()); - frame.extend_from_slice(&0x0800_u16.to_be_bytes()); - frame.extend_from_slice(b"payload"); + frame.extend_from_slice(ðertype.to_be_bytes()); + frame.extend_from_slice(payload); frame }