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:
@@ -1020,6 +1020,10 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
const ETHERTYPE_ARP: u16 = 0x0806;
|
||||||
|
const ARP_REQUEST: u16 = 1;
|
||||||
|
const ARP_REPLY: u16 = 2;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn binds_quic_endpoint_on_configured_address() {
|
async fn binds_quic_endpoint_on_configured_address() {
|
||||||
let config = RelayConfig::new(
|
let config = RelayConfig::new(
|
||||||
@@ -1632,6 +1636,133 @@ mod tests {
|
|||||||
assert!(sessions.lock().await.is_empty());
|
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]
|
#[tokio::test]
|
||||||
async fn reconnects_gateway_while_client_stays_joined() {
|
async fn reconnects_gateway_while_client_stays_joined() {
|
||||||
let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM);
|
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> {
|
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();
|
let mut frame = Vec::new();
|
||||||
frame.extend_from_slice(&destination.octets());
|
frame.extend_from_slice(&destination.octets());
|
||||||
frame.extend_from_slice(&source.octets());
|
frame.extend_from_slice(&source.octets());
|
||||||
frame.extend_from_slice(&0x0800_u16.to_be_bytes());
|
frame.extend_from_slice(ðertype.to_be_bytes());
|
||||||
frame.extend_from_slice(b"payload");
|
frame.extend_from_slice(payload);
|
||||||
frame
|
frame
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user