From 18f5a3338ed1597190346dcc8428342b1b78ca50 Mon Sep 17 00:00:00 2001 From: Darry <163115040+genforAI@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:41:41 -0400 Subject: [PATCH] =?UTF-8?q?Require=20BOOTP=20chaddr=20match=20for=20host?= =?UTF-8?q?=E2=86=92VM=20DHCP=20responses=20(#192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Require BOOTP chaddr match for host→VM DHCP responses Mirror #191 request-path identity checks: admit/forward DHCP BootReplies only when chaddr matches the VM MAC, so foreign client replies are not written into the guest fd. Co-authored-by: Cursor * $ cargo fmt --------- Co-authored-by: genforAI Co-authored-by: Cursor Co-authored-by: Nikolay Edigaryev --- lib/proxy/host.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/lib/proxy/host.rs b/lib/proxy/host.rs index 1c33098..2e9f7cc 100644 --- a/lib/proxy/host.rs +++ b/lib/proxy/host.rs @@ -1,7 +1,10 @@ +use crate::dhcp_snooper::message_matches_bootp_client; use crate::proxy::flows::{FlowDirection, FlowMatch}; use crate::proxy::udp_packet_helper::UdpPacketHelper; use crate::proxy::{Direction, PolicyDecision, Proxy}; use anyhow::{Context, Result}; +use dhcproto::Decodable; +use dhcproto::v4::Opcode; use smoltcp::phy::ChecksumCapabilities; use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, Ipv4Repr, UdpPacket}; @@ -141,8 +144,75 @@ impl Proxy<'_> { return false; } - UdpPacket::new_checked(ipv4_pkt.payload()) - .map(|udp_pkt| udp_pkt.is_dhcp_response()) - .unwrap_or(false) + let Ok(udp_pkt) = UdpPacket::new_checked(ipv4_pkt.payload()) else { + return false; + }; + + // Require the standard DHCP server and client ports + if !udp_pkt.is_dhcp_response() { + return false; + } + + // Require the BOOTP client hardware address to match this VM + // (symmetric with is_allowed_dhcp_request / #191 on the VM→host path) + let mut decoder = dhcproto::v4::Decoder::new(udp_pkt.payload()); + let Ok(message) = dhcproto::v4::Message::decode(&mut decoder) else { + return false; + }; + + message_matches_bootp_client(&message, Opcode::BootReply, self.vm_mac_address.0) + } +} + +#[cfg(test)] +mod tests { + use crate::dhcp_snooper::message_matches_bootp_client; + use dhcproto::Decodable; + use dhcproto::v4::{DhcpOption, Message, MessageType, Opcode}; + use dhcproto::{Encodable, Encoder}; + use smoltcp::wire::Ipv4Address; + + const VM_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01]; + const OTHER_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02]; + + #[test] + fn dhcp_boot_reply_chaddr_must_match_vm() { + let own = encode_boot_reply(VM_MAC); + let foreign = encode_boot_reply(OTHER_MAC); + + let mut dec = dhcproto::v4::Decoder::new(&own); + let own_msg = Message::decode(&mut dec).unwrap(); + let mut dec = dhcproto::v4::Decoder::new(&foreign); + let foreign_msg = Message::decode(&mut dec).unwrap(); + + assert!(message_matches_bootp_client( + &own_msg, + Opcode::BootReply, + VM_MAC + )); + assert!(!message_matches_bootp_client( + &foreign_msg, + Opcode::BootReply, + VM_MAC + )); + } + + fn encode_boot_reply(chaddr: [u8; 6]) -> Vec { + let mut message = Message::new( + Ipv4Address::UNSPECIFIED, + Ipv4Address::new(192, 168, 64, 2), + Ipv4Address::UNSPECIFIED, + Ipv4Address::UNSPECIFIED, + &chaddr, + ); + message.set_opcode(Opcode::BootReply); + message + .opts_mut() + .insert(DhcpOption::MessageType(MessageType::Ack)); + message.opts_mut().insert(DhcpOption::AddressLeaseTime(600)); + + let mut encoded = Vec::new(); + message.encode(&mut Encoder::new(&mut encoded)).unwrap(); + encoded } }