Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,11 @@ dependencies = [
"guid",
"jiff",
"libc",
"mesh",
"mesh_process",
"net_backend_resources",
"net_tap",
"netvsp_resources",
"openvmm_defs",
"openvmm_helpers",
"pal_async",
Expand Down Expand Up @@ -5356,6 +5361,7 @@ dependencies = [
"mesh_rpc",
"mesh_worker",
"net_backend_resources",
"net_tap",
"netvsp_resources",
"nvme_resources",
"openssl",
Expand Down
3 changes: 3 additions & 0 deletions openvmm/openvmm_entry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
unicycle.workspace = true

[target.'cfg(target_os = "linux")'.dependencies]
net_tap.workspace = true

[target.'cfg(windows)'.dependencies]
vmswitch.workspace = true
virt_whp.workspace = true
Expand Down
12 changes: 11 additions & 1 deletion openvmm/openvmm_entry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1850,7 +1850,17 @@ fn parse_endpoint(
}
}
EndpointConfigCli::Tap { name } => {
net_backend_resources::tap::TapHandle { name: name.clone() }.into_resource()
#[cfg(target_os = "linux")]
{
let fd = net_tap::tap::open_tap(name).context("failed to open TAP device")?;
net_backend_resources::tap::TapHandle { fd }.into_resource()
}

#[cfg(not(target_os = "linux"))]
{
let _ = name;
bail!("TAP backend is only supported on Linux")
}
}
};

Expand Down
16 changes: 14 additions & 2 deletions openvmm/openvmm_entry/src/ttrpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,10 @@ impl VmService {
}
}

#[cfg_attr(
not(any(windows, target_os = "linux")),
allow(unreachable_code, unused_variables)
)]
fn parse_nic_config(
nic: vmservice::NicConfig,
) -> anyhow::Result<(DeviceVtl, Resource<VmbusDeviceHandleKind>)> {
Expand All @@ -724,9 +728,17 @@ fn parse_nic_config(
},
}
.into_resource(),
#[cfg(unix)]
Backend::Tap(tap) => {
net_backend_resources::tap::TapHandle { name: tap.name }.into_resource()
#[cfg(target_os = "linux")]
{
let fd = net_tap::tap::open_tap(&tap.name).context("failed to open TAP device")?;
net_backend_resources::tap::TapHandle { fd }.into_resource()
}
#[cfg(not(target_os = "linux"))]
{
let _ = tap;
anyhow::bail!("TAP backend is only supported on Linux")
}
}
_ => anyhow::bail!("unsupported backend"),
};
Expand Down
8 changes: 8 additions & 0 deletions petri/burette/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@ clap = { workspace = true, features = ["derive"] }
fs-err.workspace = true
futures.workspace = true
jiff.workspace = true
mesh.workspace = true
mesh_process.workspace = true
serde = { workspace = true, features = ["std", "derive"] }
serde_json = { workspace = true, features = ["std"] }
tempfile.workspace = true
tracing.workspace = true

[target.'cfg(target_os = "linux")'.dependencies]
guid.workspace = true
libc.workspace = true
net_backend_resources.workspace = true
net_tap.workspace = true
netvsp_resources.workspace = true
virtio_resources.workspace = true
vm_resource.workspace = true
Comment on lines +36 to +42
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guid, virtio_resources, and vm_resource are already in [dependencies], but they’re repeated under [target.'cfg(target_os = "linux")'.dependencies]. This duplication is unnecessary and can be removed (or, if the intent is to make them Linux-only, they should be removed from the non-target section instead).

Suggested change
guid.workspace = true
libc.workspace = true
net_backend_resources.workspace = true
net_tap.workspace = true
netvsp_resources.workspace = true
virtio_resources.workspace = true
vm_resource.workspace = true
libc.workspace = true
net_backend_resources.workspace = true
net_tap.workspace = true
netvsp_resources.workspace = true

Copilot uses AI. Check for mistakes.

[target.'cfg(windows)'.dependencies]
windows = { workspace = true, features = [
Expand Down
241 changes: 241 additions & 0 deletions petri/burette/src/iperf_helper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! iperf3 helper subprocess for network throughput tests.
//!
//! This module implements a child process that serves iperf3 server
//! requests via mesh RPC. Both the Consomme and TAP backends use this
//! same helper — the TAP variant just does `unshare` before connecting
//! to mesh (see `run_tap_helper`).

// UNSAFETY: Calling libc functions for namespace setup (unshare) and
// network interface configuration (socket, ioctls).
#![cfg_attr(target_os = "linux", expect(unsafe_code))]

use mesh::MeshPayload;
use mesh::rpc::FailableRpc;

/// Initial message sent from parent to the helper via mesh.
#[derive(MeshPayload)]
pub struct IperfHelperInit {
pub ready: mesh::OneshotSender<Result<IperfHelperReady, String>>,
}

/// Sent from helper to parent after it's ready to serve requests.
#[derive(MeshPayload)]
pub struct IperfHelperReady {
pub requests: mesh::Sender<IperfRequest>,
}

/// Request from parent to the iperf3 helper.
#[derive(MeshPayload)]
pub enum IperfRequest {
/// Spawn iperf3 server, run until client disconnects, return JSON output.
RunIperf3(FailableRpc<Iperf3Args, String>),
/// Create a TAP device in the helper's (namespaced) network stack and
/// return the fd. Only valid when the helper was started with
/// `run_tap_helper`.
#[cfg(target_os = "linux")]
SetupTap(FailableRpc<TapConfig, std::os::fd::OwnedFd>),
/// Shut down the helper.
Stop,
}

/// Arguments for an iperf3 server invocation.
#[derive(MeshPayload)]
pub struct Iperf3Args {
pub port: u16,
pub extra_args: Vec<String>,
}

/// Configuration for creating a TAP device.
#[cfg(target_os = "linux")]
#[derive(MeshPayload)]
pub struct TapConfig {
/// TAP device name (e.g., "tap0").
pub name: String,
/// CIDR for the host side of the TAP (e.g., "192.168.100.1/24").
pub cidr: String,
}

/// Entry point for the plain iperf3 helper (no namespace).
///
/// Can be called at any point — no single-threaded requirement.
pub fn run_helper() {
if let Err(e) = mesh_process::try_run_mesh_host("burette", async |init: IperfHelperInit| {
run_helper_inner(init).await;
Ok(())
}) {
eprintln!("iperf helper failed: {e}");
std::process::exit(1);
}
}

async fn run_helper_inner(init: IperfHelperInit) {
let (req_send, req_recv) = mesh::channel();
init.ready.send(Ok(IperfHelperReady { requests: req_send }));
serve_requests(req_recv).await;
}

/// Serve iperf3 (and optionally TAP setup) requests until the channel
/// is closed or a Stop request is received.
async fn serve_requests(mut recv: mesh::Receiver<IperfRequest>) {
while let Ok(req) = recv.recv().await {
match req {
IperfRequest::RunIperf3(rpc) => {
rpc.handle_failable(async |args| {
// -s: server mode
// -1: handle one client then exit
// -J: JSON output
// -p: port
let output = std::process::Command::new("iperf3")
.args(["-s", "-1", "-J", "-p", &args.port.to_string()])
.args(&args.extra_args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.map_err(|e| format!("failed to spawn iperf3: {e}"))?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"iperf3 exited with {}: {}",
output.status,
stderr.trim()
));
Comment on lines +99 to +105
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helper treats any non-zero iperf3 server exit status as a hard error and discards stdout, but the previous in-process implementation explicitly noted that the server may exit non-zero while still producing valid JSON results. To avoid flakiness, consider parsing stdout when it’s non-empty (and log/warn on the status) instead of failing solely on !status.success().

Copilot uses AI. Check for mistakes.
}

Ok(String::from_utf8_lossy(&output.stdout).into_owned())
})
.await;
}
#[cfg(target_os = "linux")]
IperfRequest::SetupTap(rpc) => {
rpc.handle_failable(async |config| {
linux::setup_tap_device(&config.name, &config.cidr)
})
.await;
}
IperfRequest::Stop => break,
}
}
}

#[cfg(target_os = "linux")]
pub mod linux {
use anyhow::Context as _;
use std::os::fd::AsRawFd;
use std::os::fd::FromRawFd;

/// Entry point for the TAP namespace helper.
///
/// This MUST be called before any threads are spawned (before clap
/// parsing, before pal_async pool creation). The `unshare()` syscall
/// requires a single-threaded process.
pub fn run_tap_helper() {
// SAFETY: unshare() with CLONE_NEWUSER | CLONE_NEWNET is safe — it only
// affects the calling process's namespace membership.
let ret = unsafe { libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNET) };
if ret != 0 {
let err = std::io::Error::last_os_error();
eprintln!("unshare(CLONE_NEWUSER | CLONE_NEWNET) failed: {err}");
std::process::exit(1);
}

// Now join mesh and serve — same as the plain helper.
super::run_helper();
}

/// Create and configure a TAP device. Returns the TAP fd.
pub fn setup_tap_device(name: &str, cidr: &str) -> anyhow::Result<std::os::fd::OwnedFd> {
let tap_fd = net_tap::tap::open_tap(name).context("failed to create TAP device")?;
configure_tap_interface(name, cidr).context("failed to configure TAP interface")?;
Ok(tap_fd)
}

/// Bring up a TAP interface and assign an IP address using ioctls.
fn configure_tap_interface(name: &str, cidr: &str) -> anyhow::Result<()> {
let (addr_str, prefix_str) = cidr.split_once('/').context("CIDR must contain '/'")?;
let addr: std::net::Ipv4Addr = addr_str.parse().context("invalid IPv4 address")?;
let prefix_len: u32 = prefix_str.parse().context("invalid prefix length")?;
anyhow::ensure!(prefix_len <= 32, "prefix length {prefix_len} > 32");
let netmask = if prefix_len == 0 {
0u32
} else {
!0u32 << (32 - prefix_len)
};

// SAFETY: Creating an AF_INET/SOCK_DGRAM socket for ioctls.
let sock = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
anyhow::ensure!(
sock >= 0,
"socket() failed: {}",
std::io::Error::last_os_error()
);
// SAFETY: `sock` is a valid, newly created file descriptor.
let sock = unsafe { std::os::fd::OwnedFd::from_raw_fd(sock) };
let fd = sock.as_raw_fd();

let mut ifr = new_ifreq(name)?;

// SAFETY: SIOCGIFFLAGS / SIOCSIFFLAGS are standard Linux ioctls.
unsafe {
anyhow::ensure!(
libc::ioctl(fd, libc::SIOCGIFFLAGS as _, &mut ifr) == 0,
"SIOCGIFFLAGS: {}",
std::io::Error::last_os_error()
);
ifr.ifr_ifru.ifru_flags |= libc::IFF_UP as libc::c_short;
anyhow::ensure!(
libc::ioctl(fd, libc::SIOCSIFFLAGS as _, &ifr) == 0,
"SIOCSIFFLAGS: {}",
std::io::Error::last_os_error()
);
}

// SAFETY: SIOCSIFADDR writes the `ifru_addr` field of an `ifreq`.
unsafe {
ifr.ifr_ifru.ifru_addr = sockaddr_in4(addr);
anyhow::ensure!(
libc::ioctl(fd, libc::SIOCSIFADDR as _, &ifr) == 0,
"SIOCSIFADDR: {}",
std::io::Error::last_os_error()
);
}

// SAFETY: SIOCSIFNETMASK writes the `ifru_netmask` field of an `ifreq`.
unsafe {
ifr.ifr_ifru.ifru_netmask = sockaddr_in4(std::net::Ipv4Addr::from(netmask));
anyhow::ensure!(
libc::ioctl(fd, libc::SIOCSIFNETMASK as _, &ifr) == 0,
"SIOCSIFNETMASK: {}",
std::io::Error::last_os_error()
);
}

Ok(())
}

fn new_ifreq(name: &str) -> anyhow::Result<libc::ifreq> {
// SAFETY: All-zero is a valid `ifreq`.
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
let bytes = name.as_bytes();
anyhow::ensure!(
bytes.len() < libc::IF_NAMESIZE,
"interface name too long: {name:?}"
);
for (i, &b) in bytes.iter().enumerate() {
ifr.ifr_name[i] = b as libc::c_char;
}
Ok(ifr)
}

fn sockaddr_in4(addr: std::net::Ipv4Addr) -> libc::sockaddr {
// SAFETY: All-zero is a valid `sockaddr_in`.
let mut sa: libc::sockaddr_in = unsafe { std::mem::zeroed() };
sa.sin_family = libc::AF_INET as libc::sa_family_t;
sa.sin_addr.s_addr = u32::from(addr).to_be();
// SAFETY: `sockaddr_in` and `sockaddr` have compatible layout.
unsafe { std::ptr::from_ref(&sa).cast::<libc::sockaddr>().read() }
}
}
Loading
Loading