From 8f09fea6f35fc747d982bf978cc3a36f16426f60 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Fri, 22 May 2026 09:08:37 +0200 Subject: [PATCH] test(client): cover DHCP reply policy at relay boundary The MVP depends on the Windows TAP adapter receiving DHCP from the real LAN, while still preventing a remote client from pretending to be a DHCP server. The lower-level safety classifiers already encode that direction split, but the client relay boundary did not test both sides together. Extend the client-core QUIC session test with a LAN-side DHCPv4 server reply that must be accepted toward TAP, and a remote-client DHCPv4 server reply that must be dropped before relay send. This keeps the critical DHCP path covered at the same layer that records client stats and feeds the Windows TAP frame pump. Test Plan: - cargo fmt - cargo test -p lanparty-client-core - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings Refs: MVP DHCP tunnel acceptance --- crates/lanparty-client-core/src/lib.rs | 67 ++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/crates/lanparty-client-core/src/lib.rs b/crates/lanparty-client-core/src/lib.rs index edde7e1..ab1768f 100644 --- a/crates/lanparty-client-core/src/lib.rs +++ b/crates/lanparty-client-core/src/lib.rs @@ -914,6 +914,18 @@ mod tests { .send_datagram(Bytes::from(misdirected_response)) .unwrap(); + let dhcp_response = encode_datagram( + FrameType::Ethernet, + 7, + 1, + 0, + &lan_dhcpv4_server_reply_to_client(), + ) + .unwrap(); + connection + .send_datagram(Bytes::from(dhcp_response)) + .unwrap(); + let response = encode_datagram( FrameType::Ethernet, 7, @@ -938,7 +950,7 @@ mod tests { let ControlMessage::Stats(stats) = stats_message else { panic!("expected client stats event"); }; - assert_eq!(stats, TunnelStats::new(1, 3, 1, 3, 10, 1)); + assert_eq!(stats, TunnelStats::new(1, 4, 1, 4, 11, 1)); stats_received_tx.send(()).unwrap(); let mut disconnect_recv = connection.accept_uni().await.unwrap(); @@ -1028,6 +1040,20 @@ mod tests { misdirected_unicast_ethernet_frame().as_slice() ); + let received = + tokio::time::timeout(Duration::from_secs(5), relay_io.recv_ethernet_outcome()) + .await + .unwrap() + .unwrap(); + let ClientReceiveOutcome::Accepted(received) = received else { + panic!("expected accepted LAN DHCP server reply"); + }; + assert_eq!(received.source_peer_id(), 1); + assert_eq!( + received.payload(), + lan_dhcpv4_server_reply_to_client().as_slice() + ); + let received = tokio::time::timeout(Duration::from_secs(5), relay_io.recv_ethernet_outcome()) .await @@ -1107,14 +1133,20 @@ mod tests { .unwrap(), ClientSendOutcome::Dropped(DropReason::VlanTaggedFrame) ); + assert_eq!( + relay_io + .send_ethernet_with_outcome(&remote_dhcpv4_server_reply()) + .unwrap(), + ClientSendOutcome::Dropped(DropReason::DhcpServerReply) + ); let stats = relay_io.stats_snapshot(); assert_eq!(stats.ethernet_frames_tx(), 1); - assert_eq!(stats.ethernet_frames_rx(), 3); + assert_eq!(stats.ethernet_frames_rx(), 4); assert_eq!(stats.broadcast_frames_tx(), 0); assert_eq!(stats.broadcast_frames_rx(), 0); assert_eq!(stats.datagrams_tx(), 1); - assert_eq!(stats.datagrams_rx(), 3); - assert_eq!(stats.dropped_frames(), 10); + assert_eq!(stats.datagrams_rx(), 4); + assert_eq!(stats.dropped_frames(), 11); assert_eq!(stats.malformed_frames(), 1); assert_eq!(client.stats_snapshot(), stats); @@ -1265,6 +1297,33 @@ mod tests { ) } + fn lan_dhcpv4_server_reply_to_client() -> Vec { + ethernet_frame_with_headers( + MacAddr::new([0x02, 0, 0, 0, 0, 1]), + MacAddr::new([0x0a, 0, 0, 0, 0, 2]), + lanparty_proto::ETHERTYPE_IPV4, + &ipv4_udp_payload(67, 68), + ) + } + + fn remote_dhcpv4_server_reply() -> Vec { + ethernet_frame_with_headers( + MacAddr::BROADCAST, + MacAddr::new([0x02, 0, 0, 0, 0, 1]), + lanparty_proto::ETHERTYPE_IPV4, + &ipv4_udp_payload(67, 68), + ) + } + + fn ipv4_udp_payload(source_port: u16, destination_port: u16) -> Vec { + let mut packet = vec![0; 28]; + packet[0] = 0x45; + packet[9] = 17; + packet[20..22].copy_from_slice(&source_port.to_be_bytes()); + packet[22..24].copy_from_slice(&destination_port.to_be_bytes()); + packet + } + fn ethernet_frame_with_headers( destination: MacAddr, source: MacAddr,