use anyhow::{Context, Result, anyhow}; use clap::ValueEnum; use log::info; use smoltcp::wire::EthernetAddress; use std::net::IpAddr; use std::net::Ipv4Addr; use std::os::unix::io::{AsRawFd, RawFd}; use std::os::unix::net::UnixDatagram; use std::str::FromStr; use std::sync::mpsc::{SyncSender, sync_channel}; use vmnet::mode::Mode; use vmnet::parameters::{Parameter, ParameterKind}; use vmnet::port_forwarding::{AddressFamily, Protocol}; use vmnet::{Batch, Events, Options}; #[derive(ValueEnum, Clone, Debug)] pub enum NetType { /// Shared network /// /// Uses NAT-translation to give guests access to the global network Nat, /// Host network /// /// Guests will be able to talk only to the host without access to global network Host, } pub struct Host { interface: vmnet::Interface, new_packets_rx: UnixDatagram, callback_can_continue_tx: SyncSender<()>, pub gateway_ip: smoltcp::wire::Ipv4Address, pub gateway_mac: EthernetAddress, pub max_packet_size: u64, pub read_max_packets: u64, finalized: bool, } impl Host { pub fn new(vm_net_type: NetType, enable_isolation: bool) -> Result { // Initialize a vmnet.framework NAT or Host interface with isolation enabled let mut interface = vmnet::Interface::new( match vm_net_type { NetType::Nat => Mode::Shared(Default::default()), NetType::Host => Mode::Host(Default::default()), }, Options { enable_isolation: Some(enable_isolation), ..Default::default() }, ) .context("failed to initialize vmnet interface")?; // Retrieve first IP (gateway) used for this interface let Some(Parameter::StartAddress(start_address)) = interface.parameters().get(ParameterKind::StartAddress) else { return Err(anyhow!( "failed to retrieve vmnet's interface start address" )); }; let start_address = Ipv4Addr::from_str(&start_address) .context("failed to parse vmnet's interface start address")?; // Retrieve last IP used for this interface and calculate the prefix let Some(Parameter::EndAddress(end_address)) = interface.parameters().get(ParameterKind::EndAddress) else { return Err(anyhow!("failed to retrieve vmnet's interface end address")); }; let end_address = Ipv4Addr::from_str(&end_address) .context("failed to parse vmnet's interface end address")?; let Some(prefix) = Self::ipv4_range_prefix(start_address, end_address) else { return Err(anyhow!( "failed to resolve vmnet's interface: prefix ambiguity for {}–{}", start_address, end_address )); }; // Figure out the gateway's interface MAC address let Some(gateway_mac) = Self::interface_mac_for_ip(start_address, prefix) else { return Err(anyhow!( "failed to resolve vmnet's interface: no interface found with {}/{} CIDR", start_address, prefix )); }; let gateway_mac = EthernetAddress(gateway_mac.octets()); // Retrieve max packet size for this interface let Some(Parameter::MaxPacketSize(max_packet_size)) = interface.parameters().get(ParameterKind::MaxPacketSize) else { return Err(anyhow!( "failed to retrieve vmnet's interface max packet size" )); }; // Retrieve read max packets for this interface let Some(Parameter::ReadMaxPackets(read_max_packets)) = interface.parameters().get(ParameterKind::ReadMaxPackets) else { return Err(anyhow!( "failed to retrieve vmnet's interface read max packets" )); }; // Set up a socketpair() to emulate polling of the vmnet interface let (new_packets_tx, new_packets_rx) = UnixDatagram::pair()?; new_packets_rx.set_nonblocking(true)?; let (callback_can_continue_tx, callback_can_continue_rx) = sync_channel(0); interface .set_event_callback(Events::PACKETS_AVAILABLE, move |_mask, _params| { // Send a dummy datagram to make the other end of socketpair() readable // and ignore the error as this merely a signalling channel to wake up // the poller new_packets_tx.send(&[0; 1]).ok(); // Wait for the permission to continue to avoid // wasting CPU cycles or in case of termination, // to unblock this Block[1] and allow // vmnet.framework to terminate // // [1]: https://en.wikipedia.org/wiki/Blocks_(C_language_extension) callback_can_continue_rx.recv().unwrap(); }) .context("failed to set vmnet interface's event callback")?; Ok(Host { interface, new_packets_rx, callback_can_continue_tx, gateway_ip: start_address, gateway_mac, max_packet_size, read_max_packets, finalized: false, }) } fn interface_mac_for_ip(ip: Ipv4Addr, prefix: u8) -> Option { for iface in pnet_datalink::interfaces() { if iface .ips .iter() .any(|network| network.ip() == IpAddr::V4(ip) && network.prefix() == prefix) && let Some(mac) = iface.mac { return Some(mac); } } None } fn ipv4_range_prefix(start_address: Ipv4Addr, end_address: Ipv4Addr) -> Option { let start_address = start_address.to_bits(); let end_address = end_address.to_bits(); if start_address > end_address { return None; } Some((start_address ^ end_address).leading_zeros() as u8) } } impl Host { pub fn port_forwarding_add_rule( &mut self, external_port: u16, internal_addr: Ipv4Addr, internal_port: u16, ) -> Result<()> { let details = format!( "external_port={external_port}, internal_addr={internal_addr}, internal_port={internal_port}" ); self.interface .port_forwarding_rule_add( AddressFamily::Ipv4, Protocol::Tcp, external_port, internal_addr.into(), internal_port, ) .map(|_| info!("added port forwarding rule {details}")) .map_err(|err| anyhow!("failed to add port forwarding rule {details}: {err}")) } pub fn port_forwarding_remove_rule(&mut self, external_port: u16) -> Result<()> { let details = format!("external_port={external_port}"); self.interface .port_forwarding_rule_remove(AddressFamily::Ipv4, Protocol::Tcp, external_port) .map(|_| info!("removed port forwarding rule {details}")) .map_err(|err| anyhow!("failed to remove port forwarding rule {details}: {err}")) } pub fn read(&mut self, batch: &mut Batch, bufs: &mut [Vec]) -> vmnet::Result { // Dequeue dummy datagram from the socket (if any) // to free up buffer space and reduce false-positives // when polling let mut buf_to_be_discarded: [u8; 1] = [0; 1]; let _ = self.new_packets_rx.recv(&mut buf_to_be_discarded); let result = self.interface.read_batch(batch, bufs); if let Err(vmnet::Error::VmnetReadNothing) = result { // We've emptied everything, unlock the callback // so that it will be able to pick up new events let _ = self.callback_can_continue_tx.send(()); } result } pub fn write(&mut self, buf: &[u8]) -> vmnet::Result { self.interface.write(buf) } pub fn finalize(&mut self) -> Result<()> { // First make sure our callback won't be scheduled again after it finishes self.interface .clear_event_callback() .context("failed to clear vmnet interface's event callback")?; // Now let the callback finish let _ = self.callback_can_continue_tx.send(()); self.interface .finalize() .context("failed to finalize vmnet's interface")?; self.finalized = true; Ok(()) } } impl Drop for Host { fn drop(&mut self) { if !self.finalized { let _ = self.finalize(); } } } impl AsRawFd for Host { fn as_raw_fd(&self) -> RawFd { self.new_packets_rx.as_raw_fd() } } #[cfg(test)] mod tests { use super::Host; use std::net::Ipv4Addr; #[test] fn ipv4_range_prefix_for_class_c_subnet() { assert_eq!( Host::ipv4_range_prefix( Ipv4Addr::new(192, 168, 64, 0), Ipv4Addr::new(192, 168, 64, 255), ), Some(24) ); } #[test] fn ipv4_range_prefix_for_single_address() { assert_eq!( Host::ipv4_range_prefix(Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 0, 1)), Some(32) ); } #[test] fn ipv4_range_prefix_for_class_c_usable_range() { assert_eq!( Host::ipv4_range_prefix( Ipv4Addr::new(192, 168, 64, 1), Ipv4Addr::new(192, 168, 64, 254), ), Some(24) ); } #[test] fn ipv4_range_prefix_for_containing_class_c_subnet() { assert_eq!( Host::ipv4_range_prefix( Ipv4Addr::new(192, 168, 64, 2), Ipv4Addr::new(192, 168, 64, 254), ), Some(24) ); } #[test] fn ipv4_range_prefix_rejects_inverted_range() { assert_eq!( Host::ipv4_range_prefix(Ipv4Addr::new(10, 0, 0, 2), Ipv4Addr::new(10, 0, 0, 1)), None ); } }