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
This commit is contained in:
2026-05-22 09:37:46 +02:00
parent 0fb4689cb2
commit 5e349338e4
+164 -2
View File
@@ -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<u8> {
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<u8> {
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<u8> {
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(&ethertype.to_be_bytes());
frame.extend_from_slice(payload);
frame
}