From 296545fe63961e295bb8640278555dd91c0352e4 Mon Sep 17 00:00:00 2001 From: John-Paul <190630760+simonisjp@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:30:37 +0000 Subject: [PATCH] Put --expose port forwards on the vmnet network configuration Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- lib/host.rs | 24 ------------------------ lib/manager.rs | 9 +++++++-- lib/manager_test.rs | 23 +++++++++++++++++++++++ lib/network.rs | 32 ++++++++++++++++++++++++++++++++ lib/proxy/mod.rs | 15 ++++----------- 7 files changed, 71 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9efaffe..b74464b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2909,9 +2909,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vmnet" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf185ef14ad566190dfcc44151e1e97864f5528ce478ac497910bc9f4bfd58d" +checksum = "971ab27d91ac0f6573c40436b4b3bca046bfd9ee84efa5e2c416be50847d936b" dependencies = [ "bitflags 2.13.2", "block2", @@ -2931,9 +2931,9 @@ dependencies = [ [[package]] name = "vmnet-derive" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c326cc79bf3790485d9c4def530d9b35d2fceb6cd7b189a12d1476fd2206b9f" +checksum = "5f420509568a7eccdf21fe64b5a7635f4f8b0e871d7d735df406ebbc24df99c1" dependencies = [ "darling", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 2d20a02..8b02c22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ tokio-util = { version = "0.7.19", features = ["join-map", "codec"] } bytes = "1" futures-util = { version = "0.3", default-features = false, features = ["std"] } dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" } -vmnet = "0.9.0" +vmnet = "0.10.0" clap = { version = "4", features = ["derive"] } mac_address = "1" privdrop = "0" diff --git a/lib/host.rs b/lib/host.rs index 0c555c8..5e7d63a 100644 --- a/lib/host.rs +++ b/lib/host.rs @@ -1,12 +1,10 @@ use crate::network::Network; use anyhow::{Context, Result, anyhow}; -use log::info; use std::net::Ipv4Addr; use std::sync::Arc; use std::sync::mpsc::{SyncSender, sync_channel}; use tokio::sync::Notify; use vmnet::parameters::{Parameter, ParameterKind}; -use vmnet::port_forwarding::{AddressFamily, Protocol}; use vmnet::{Batch, Events, Options}; pub struct Host { @@ -84,28 +82,6 @@ impl Host { } 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 async fn read(&mut self, batch: &mut Batch, bufs: &mut [Vec]) -> vmnet::Result { loop { // One callback can cover several batches; stay ready until an empty read diff --git a/lib/manager.rs b/lib/manager.rs index 68541e2..c0bc359 100644 --- a/lib/manager.rs +++ b/lib/manager.rs @@ -295,7 +295,13 @@ impl Manager { .iter() .map(|(_, config, vm_ip, _)| (config.vm_mac_address, *vm_ip)) .collect(); - let network = Network::new(vm_net_type, subnet, &reservations)?; + let port_forwardings: Vec<_> = configured + .iter() + .flat_map(|(_, config, vm_ip, _)| { + config.exposed_ports.iter().map(|port| (*vm_ip, *port)) + }) + .collect(); + let network = Network::new(vm_net_type, subnet, &reservations, &port_forwardings)?; let mut proxies = Vec::with_capacity(configured.len()); // Finish the entire batch before giving a proxy to any VM task @@ -307,7 +313,6 @@ impl Manager { &network, config.allow, config.block, - config.exposed_ports, ) .with_context(|| format!("failed to initialize VM at index {index} ({vm_id})"))?; proxies.push(proxy); diff --git a/lib/manager_test.rs b/lib/manager_test.rs index 6b75587..28d64a3 100644 --- a/lib/manager_test.rs +++ b/lib/manager_test.rs @@ -766,6 +766,29 @@ async fn native_vm_disconnect_ends_run() { timeout(TIMEOUT, manager.shutdown()).await.unwrap().unwrap(); } +#[tokio::test] +#[serial_test::serial] +async fn native_batch_exposes_ports_for_every_vm() { + let mut manager = native_manager(None); + let (socket, _peer) = UnixDatagram::pair().unwrap(); + let mut first = vm_config(&socket, "02:00:00:00:00:02"); + first.exposed_ports = vec!["52222:22".parse().unwrap()]; + let mut second = vm_config(&socket, "02:00:00:00:00:03"); + second.exposed_ports = vec!["52223:22".parse().unwrap(), "58080:80".parse().unwrap()]; + drop(socket); + + // Every VM's forwards are added to the shared network before it starts + timeout( + TIMEOUT, + manager.initialize(NetType::Nat, vec![first, second]), + ) + .await + .unwrap() + .unwrap(); + + timeout(TIMEOUT, manager.shutdown()).await.unwrap().unwrap(); +} + #[tokio::test] #[serial_test::serial] async fn native_control_disconnect_keeps_vms_running() { diff --git a/lib/network.rs b/lib/network.rs index 8784320..49c97ff 100644 --- a/lib/network.rs +++ b/lib/network.rs @@ -1,10 +1,13 @@ +use crate::proxy::ExposedPort; use anyhow::{Context, Result}; use clap::ValueEnum; use ipnet::Ipv4Net; +use log::info; use mac_address::MacAddress; use std::net::Ipv4Addr; use vmnet::NetworkConfiguration; use vmnet::network::Mode; +use vmnet::port_forwarding::{AddressFamily, Protocol}; // A /29 has six usable addresses, including the gateway pub(crate) const MAX_VMS: usize = 5; @@ -35,6 +38,7 @@ impl Network { net_type: NetType, subnet: Ipv4Net, reservations: &[(MacAddress, Ipv4Addr)], + port_forwardings: &[(Ipv4Addr, ExposedPort)], ) -> Result { // Create vmnet's network configuration let mut configuration = NetworkConfiguration::new(match net_type { @@ -62,10 +66,38 @@ impl Network { })?; } + // Forward exposed ports to the reserved VM addresses, which vmnet only + // accepts on the configuration, before the network is created + let port_forwardings = port_forwardings + .iter() + .map(|(vm_ip, port)| { + let details = format!( + "external_port={}, internal_addr={vm_ip}, internal_port={}", + port.external_port, port.internal_port + ); + + configuration + .add_port_forwarding_rule( + AddressFamily::Ipv4, + Protocol::Tcp, + port.external_port, + (*vm_ip).into(), + port.internal_port, + ) + .with_context(|| format!("failed to add port forwarding rule {details}"))?; + + Ok(details) + }) + .collect::>>()?; + // Create vmnet's network let handle = vmnet::Network::new(&configuration) .with_context(|| format!("failed to create vmnet network {subnet}"))?; + for details in port_forwardings { + info!("added port forwarding rule {details}"); + } + Ok(Self { handle, subnet, diff --git a/lib/proxy/mod.rs b/lib/proxy/mod.rs index af2f14c..bb8f9b7 100644 --- a/lib/proxy/mod.rs +++ b/lib/proxy/mod.rs @@ -9,7 +9,7 @@ use crate::dhcp_server::DhcpServer; use crate::host::Host; use crate::network::Network; use crate::vm::VM; -use anyhow::{Context, Result}; +use anyhow::Result; pub use exposed_port::ExposedPort; use flows::{FlowTable, PendingFlow}; use ipnet::Ipv4Net; @@ -62,14 +62,13 @@ impl Proxy { network: &Network, allow: Vec, block: Vec, - exposed_ports: Vec, ) -> Result { let vm_mac_address = EthernetAddress(vm_mac_address.bytes()); let allow = normalize_rules(allow); let block = normalize_rules(block); // Create this VM's vmnet interface on the shared network - let mut host = Host::new( + let host = Host::new( network, vm_ip, !allow.contains(&Rule::Stateless(Target::Prefix(Ipv4Net::default()))), @@ -89,12 +88,6 @@ impl Proxy { host.gateway_ip, ); - // Install port forwardings for the VM's reserved IP address - for port in exposed_ports { - host.port_forwarding_add_rule(port.external_port, host.vm_ip, port.internal_port) - .context("failed to configure port forwarding")?; - } - Ok(Proxy { vm, host, @@ -443,7 +436,8 @@ mod tests { let (_, vm_ip, subnet) = crate::subnet_finder::find_available_subnet(29).unwrap(); let vm_mac_address = MacAddress::from_str("02:00:00:00:00:02").unwrap(); let network = - crate::network::Network::new(NetType::Nat, subnet, &[(vm_mac_address, vm_ip)]).unwrap(); + crate::network::Network::new(NetType::Nat, subnet, &[(vm_mac_address, vm_ip)], &[]) + .unwrap(); let proxy = Proxy::new( VM::new(vm_fd.as_raw_fd()).unwrap(), vm_mac_address, @@ -457,7 +451,6 @@ mod tests { .into_iter() .map(|value| value.parse().unwrap()) .collect(), - Vec::new(), ) .unwrap();