diff --git a/vm/devices/net/net_consomme/consomme/src/arp.rs b/vm/devices/net/net_consomme/consomme/src/arp.rs index 5468317299..71b0c5eb84 100644 --- a/vm/devices/net/net_consomme/consomme/src/arp.rs +++ b/vm/devices/net/net_consomme/consomme/src/arp.rs @@ -5,6 +5,7 @@ use super::Access; use super::Client; use super::DropReason; use crate::ChecksumState; +use crate::Ipv4Address; use crate::MIN_MTU; use smoltcp::wire::ArpOperation; use smoltcp::wire::ArpPacket; @@ -13,6 +14,11 @@ use smoltcp::wire::EthernetFrame; use smoltcp::wire::EthernetProtocol; use smoltcp::wire::EthernetRepr; +fn is_same_subnet(addr1: Ipv4Address, addr2: Ipv4Address, subnet_mask: Ipv4Address) -> bool { + let subnet_mask = subnet_mask.to_bits(); + (addr1.to_bits() & subnet_mask) == (addr2.to_bits() & subnet_mask) +} + impl Access<'_, T> { pub(crate) fn handle_arp( &mut self, @@ -20,37 +26,49 @@ impl Access<'_, T> { payload: &[u8], ) -> Result<(), DropReason> { let arp = ArpRepr::parse(&ArpPacket::new_unchecked(payload))?; - match arp { - ArpRepr::EthernetIpv4 { - operation: ArpOperation::Request, - source_hardware_addr, - source_protocol_addr, - target_hardware_addr: _, - target_protocol_addr, - } if target_protocol_addr == self.inner.state.params.gateway_ip => { - let e_repr = EthernetRepr { - src_addr: self.inner.state.params.gateway_mac, - dst_addr: frame.src_addr, - ethertype: EthernetProtocol::Arp, - }; - let arp_repr = ArpRepr::EthernetIpv4 { - operation: ArpOperation::Reply, - source_hardware_addr: self.inner.state.params.gateway_mac, - source_protocol_addr: target_protocol_addr, - target_hardware_addr: source_hardware_addr, - target_protocol_addr: source_protocol_addr, - }; - let mut buffer = [0; MIN_MTU]; - let mut response = EthernetFrame::new_unchecked(&mut buffer); - e_repr.emit(&mut response); - let mut arp_response = ArpPacket::new_unchecked(response.payload_mut()); - arp_repr.emit(&mut arp_response); - let len = e_repr.buffer_len() + arp_repr.buffer_len(); - self.client.recv(&buffer[..len], &ChecksumState::NONE); - } + let ArpRepr::EthernetIpv4 { + operation: ArpOperation::Request, + source_hardware_addr, + source_protocol_addr, + target_protocol_addr, + .. + } = arp + else { + return Err(DropReason::UnsupportedArp); + }; - _ => return Err(DropReason::UnsupportedArp), + // For any addresses in the subnet given to the guest, provide the gateway MAC address. + // This is the standard mechanism to indicate all traffic flows through the gateway, even + // local subnet traffic. + if !is_same_subnet( + self.inner.state.params.gateway_ip, + target_protocol_addr, + self.inner.state.params.net_mask, + ) { + tracing::debug!(?target_protocol_addr, gateway = %self.inner.state.params.gateway_ip, subnet_mask = %self.inner.state.params.net_mask, "Ignoring ARP request"); + return Ok(()); + } + + let mac_address = self.inner.state.params.gateway_mac; + let e_repr = EthernetRepr { + src_addr: mac_address, + dst_addr: frame.src_addr, + ethertype: EthernetProtocol::Arp, + }; + let arp_repr = ArpRepr::EthernetIpv4 { + operation: ArpOperation::Reply, + source_hardware_addr: mac_address, + source_protocol_addr: target_protocol_addr, + target_hardware_addr: source_hardware_addr, + target_protocol_addr: source_protocol_addr, }; + let mut buffer = [0; MIN_MTU]; + let mut response = EthernetFrame::new_unchecked(&mut buffer); + e_repr.emit(&mut response); + let mut arp_response = ArpPacket::new_unchecked(response.payload_mut()); + arp_repr.emit(&mut arp_response); + let len = e_repr.buffer_len() + arp_repr.buffer_len(); + self.client.recv(&buffer[..len], &ChecksumState::NONE); Ok(()) } } diff --git a/vm/devices/net/net_consomme/consomme/src/ndp.rs b/vm/devices/net/net_consomme/consomme/src/ndp.rs index b0a0084579..5d959579b9 100644 --- a/vm/devices/net/net_consomme/consomme/src/ndp.rs +++ b/vm/devices/net/net_consomme/consomme/src/ndp.rs @@ -63,6 +63,14 @@ fn rdnss_option_size(num_servers: usize) -> usize { } } +fn is_same_subnet(addr1: Ipv6Address, addr2: Ipv6Address, prefix_len: u8) -> bool { + if prefix_len == 0 { + return true; + } + let mask = u128::MAX << (128 - prefix_len); + (addr1.to_bits() & mask) == (addr2.to_bits() & mask) +} + #[derive(Debug)] pub enum NdpMessageType { RouterSolicit, @@ -317,14 +325,19 @@ impl Access<'_, T> { } } - // Only respond if the target is our link-local address - // In a stateless NAT implementation, the gateway only responds for its own - // link-local address, not for global addresses that clients autoconfigure - if target_addr != self.inner.state.params.gateway_link_local_ipv6 { + // For any addresses in the subnet given to the guest, provide the gateway MAC address. + // This is the standard mechanism to indicate all traffic flows through the gateway, even + // local subnet traffic. + if !is_same_subnet( + self.inner.state.params.gateway_link_local_ipv6, + target_addr, + self.inner.state.params.prefix_len_ipv6, + ) { tracing::debug!( target_addr = %target_addr, - our_link_local = %self.inner.state.params.gateway_link_local_ipv6, - "NS target is not our link-local address, ignoring" + gateway = %self.inner.state.params.gateway_link_local_ipv6, + prefix_len = %self.inner.state.params.prefix_len_ipv6, + "NS target is not local, ignoring" ); return Ok(()); } diff --git a/vm/devices/net/net_consomme/consomme/src/tcp.rs b/vm/devices/net/net_consomme/consomme/src/tcp.rs index 51fd341770..5efbfe9c73 100644 --- a/vm/devices/net/net_consomme/consomme/src/tcp.rs +++ b/vm/devices/net/net_consomme/consomme/src/tcp.rs @@ -245,7 +245,8 @@ impl Access<'_, T> { if let Some((socket, mut other_addr)) = result { // Check for loopback requests and replace the dest port. // This supports a guest owning both the sending and receiving ports. - if other_addr.ip().is_loopback() { + let is_loopback = other_addr.ip().is_loopback(); + if is_loopback { for (other_ft, connection) in self.inner.tcp.connections.iter() { if connection.inner.state == TcpState::Connecting && other_ft.dst.port() == *port { if let LoopbackPortInfo::ProxyForGuestPort{sending_port, guest_port} = connection.inner.loopback_port { @@ -258,6 +259,20 @@ impl Access<'_, T> { } } + // If the source IP is loopback or matches the client IP address, replace + // it with the gateway IP so that the guest's reply routes back through the + // virtual adapter instead of its own loopback interface. + let other_ip = other_addr.ip(); + match &mut other_addr { + SocketAddr::V4(v4) if is_loopback || other_ip == self.inner.state.params.client_ip => { + v4.set_ip(self.inner.state.params.gateway_ip); + } + SocketAddr::V6(v6) if is_loopback || other_ip == self.inner.state.params.client_ip_ipv6.unwrap_or(::std::net::Ipv6Addr::UNSPECIFIED) => { + v6.set_ip(self.inner.state.params.gateway_link_local_ipv6); + } + _ => {} + } + let ft = match other_addr { SocketAddr::V4(_) => FourTuple { dst: other_addr, diff --git a/vm/devices/net/net_consomme/consomme/src/udp.rs b/vm/devices/net/net_consomme/consomme/src/udp.rs index 4eef4a0dc2..5e1eea3a1d 100644 --- a/vm/devices/net/net_consomme/consomme/src/udp.rs +++ b/vm/devices/net/net_consomme/consomme/src/udp.rs @@ -162,11 +162,19 @@ impl UdpConnection { }, ) { Poll::Ready(Ok((n, src_addr))) => { + // Replace loopback source IPs with the gateway IP so + // the guest's reply routes back through the virtual + // adapter instead of its own loopback interface. let (packet_len, checksum_state) = match (dst_addr, src_addr.ip()) { (SocketAddr::V4(dst), IpAddr::V4(src_ip)) => { + let effective_src_ip: IpAddress = if src_ip.is_loopback() { + state.params.gateway_ip.into() + } else { + src_ip.into() + }; let len = build_udp_packet( &mut eth, - src_ip.into(), + effective_src_ip, (*dst.ip()).into(), src_addr.port(), dst.port(), @@ -177,9 +185,14 @@ impl UdpConnection { (len, ChecksumState::UDP4) } (SocketAddr::V6(dst), IpAddr::V6(src_ip)) => { + let effective_src_ip: IpAddress = if src_ip.is_loopback() { + state.params.gateway_link_local_ipv6.into() + } else { + src_ip.into() + }; let len = build_udp_packet( &mut eth, - src_ip.into(), + effective_src_ip, (*dst.ip()).into(), src_addr.port(), dst.port(), @@ -254,11 +267,19 @@ impl UdpListener { .recv_from(&mut eth.payload_mut()[header_offset..]) }) { Poll::Ready(Ok((n, src_addr))) => { + // Replace loopback source IPs with the gateway IP so + // the guest's reply routes back through the virtual + // adapter instead of its own loopback interface. let Some((packet_len, checksum_state)) = (match (guest_dst_ip, src_addr.ip()) { (IpAddr::V4(dst_ip), IpAddr::V4(src_ip)) => { + let effective_src_ip: IpAddress = if src_ip.is_loopback() { + state.params.gateway_ip.into() + } else { + src_ip.into() + }; let len = build_udp_packet( &mut eth, - src_ip.into(), + effective_src_ip, dst_ip.into(), src_addr.port(), self.guest_port, @@ -269,9 +290,14 @@ impl UdpListener { Some((len, ChecksumState::UDP4)) } (IpAddr::V6(dst_ip), IpAddr::V6(src_ip)) => { + let effective_src_ip: IpAddress = if src_ip.is_loopback() { + state.params.gateway_link_local_ipv6.into() + } else { + src_ip.into() + }; let len = build_udp_packet( &mut eth, - src_ip.into(), + effective_src_ip, dst_ip.into(), src_addr.port(), self.guest_port,