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
This commit is contained in:
2026-05-22 09:08:37 +02:00
parent 91a781cd7b
commit 8f09fea6f3
+63 -4
View File
@@ -914,6 +914,18 @@ mod tests {
.send_datagram(Bytes::from(misdirected_response)) .send_datagram(Bytes::from(misdirected_response))
.unwrap(); .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( let response = encode_datagram(
FrameType::Ethernet, FrameType::Ethernet,
7, 7,
@@ -938,7 +950,7 @@ mod tests {
let ControlMessage::Stats(stats) = stats_message else { let ControlMessage::Stats(stats) = stats_message else {
panic!("expected client stats event"); 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(); stats_received_tx.send(()).unwrap();
let mut disconnect_recv = connection.accept_uni().await.unwrap(); let mut disconnect_recv = connection.accept_uni().await.unwrap();
@@ -1028,6 +1040,20 @@ mod tests {
misdirected_unicast_ethernet_frame().as_slice() 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 = let received =
tokio::time::timeout(Duration::from_secs(5), relay_io.recv_ethernet_outcome()) tokio::time::timeout(Duration::from_secs(5), relay_io.recv_ethernet_outcome())
.await .await
@@ -1107,14 +1133,20 @@ mod tests {
.unwrap(), .unwrap(),
ClientSendOutcome::Dropped(DropReason::VlanTaggedFrame) 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(); let stats = relay_io.stats_snapshot();
assert_eq!(stats.ethernet_frames_tx(), 1); 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_tx(), 0);
assert_eq!(stats.broadcast_frames_rx(), 0); assert_eq!(stats.broadcast_frames_rx(), 0);
assert_eq!(stats.datagrams_tx(), 1); assert_eq!(stats.datagrams_tx(), 1);
assert_eq!(stats.datagrams_rx(), 3); assert_eq!(stats.datagrams_rx(), 4);
assert_eq!(stats.dropped_frames(), 10); assert_eq!(stats.dropped_frames(), 11);
assert_eq!(stats.malformed_frames(), 1); assert_eq!(stats.malformed_frames(), 1);
assert_eq!(client.stats_snapshot(), stats); assert_eq!(client.stats_snapshot(), stats);
@@ -1265,6 +1297,33 @@ mod tests {
) )
} }
fn lan_dhcpv4_server_reply_to_client() -> Vec<u8> {
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<u8> {
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<u8> {
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( fn ethernet_frame_with_headers(
destination: MacAddr, destination: MacAddr,
source: MacAddr, source: MacAddr,