From 5bc5a219047c7476149948cbf1a0c2736398ee17 Mon Sep 17 00:00:00 2001 From: Nikolay Edigaryev Date: Mon, 17 Aug 2026 16:57:41 +0100 Subject: [PATCH] Reject foreign DHCP replies before policy fallback --- lib/proxy/host.rs | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/lib/proxy/host.rs b/lib/proxy/host.rs index 2e9f7cc..b0be52c 100644 --- a/lib/proxy/host.rs +++ b/lib/proxy/host.rs @@ -8,6 +8,13 @@ use dhcproto::v4::Opcode; use smoltcp::phy::ChecksumCapabilities; use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, Ipv4Repr, UdpPacket}; +/// DhcpResponseDisposition distinguishes non-DHCP traffic from allowed and rejected DHCP replies. +enum DhcpResponseDisposition { + NotDhcp, + Allow, + Reject, +} + impl Proxy<'_> { pub(crate) fn process_frame_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Result<()> { if self.allowed_from_host(frame).is_none() { @@ -55,13 +62,14 @@ impl Proxy<'_> { } pub(super) fn allowed_from_host_ipv4(&mut self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> Option<()> { - // Backwards compatibility with Softnet consumers that only use stateless rules - if self.flows.is_none() { - return Some(()); + match self.dhcp_response_disposition(ipv4_pkt) { + DhcpResponseDisposition::NotDhcp => { /* Fall through to generic policy */ } + DhcpResponseDisposition::Allow => return Some(()), + DhcpResponseDisposition::Reject => return None, } - // DHCP is required to maintain the VM's lease and must bypass user-specified rules - if self.is_allowed_dhcp_response(ipv4_pkt) { + // Backwards compatibility with Softnet consumers that only use stateless rules + if self.flows.is_none() { return Some(()); } @@ -119,8 +127,9 @@ impl Proxy<'_> { _ => return, }; - if !self.is_allowed_dhcp_response(&ipv4_pkt) { - return; + match self.dhcp_response_disposition(&ipv4_pkt) { + DhcpResponseDisposition::Allow => { /* Continue snooping */ } + DhcpResponseDisposition::NotDhcp | DhcpResponseDisposition::Reject => return, } let udp_pkt = match UdpPacket::new_checked(ipv4_pkt.payload()) { @@ -137,30 +146,34 @@ impl Proxy<'_> { } } - fn is_allowed_dhcp_response(&self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> bool { + fn dhcp_response_disposition(&self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> DhcpResponseDisposition { if ipv4_pkt.src_addr() != self.host.gateway_ip || ipv4_pkt.next_header() != smoltcp::wire::IpProtocol::Udp { - return false; + return DhcpResponseDisposition::NotDhcp; } let Ok(udp_pkt) = UdpPacket::new_checked(ipv4_pkt.payload()) else { - return false; + return DhcpResponseDisposition::NotDhcp; }; // Require the standard DHCP server and client ports if !udp_pkt.is_dhcp_response() { - return false; + return DhcpResponseDisposition::NotDhcp; } // 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; + return DhcpResponseDisposition::Reject; }; - message_matches_bootp_client(&message, Opcode::BootReply, self.vm_mac_address.0) + if message_matches_bootp_client(&message, Opcode::BootReply, self.vm_mac_address.0) { + DhcpResponseDisposition::Allow + } else { + DhcpResponseDisposition::Reject + } } }