Skip to content
Merged
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
27 changes: 26 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ libc = "0.2"
log = "0.4"
mac_address = "1.1"
merge = "0.2"
nix = { version = "0.31", features = ["mman", "pthread", "signal"] }
nix = { version = "0.31", features = ["mman", "pthread", "signal", "net", "ioctl", "poll"] }
nohash = "0.2"
rftrace = { version = "0.3", optional = true }
rftrace-frontend = { version = "0.3", optional = true }
Expand All @@ -63,7 +63,6 @@ tempfile = "3.27"
thiserror = "2.0.18"
time = "0.3"
toml = "1"
tun-tap = { version = "0.1.3", default-features = false }
uhyve-interface = { version = "0.2.0", path = "uhyve-interface", features = ["std"] }
virtio-bindings = "~0.2.7"
vm-fdt = "0.3"
Expand All @@ -73,11 +72,16 @@ tar-no-std = { version = "0.4", features = ["alloc"] }
async-channel = "2.5.0"
futures-lite = "2.6.1"
event-listener = "5.4.1"
bitflags = "2.11"
virtio-queue = "0.17"
zerocopy = { version = "0.8", features = ["derive"] }

[target.'cfg(target_os = "linux")'.dependencies]
kvm-bindings = "0.14"
kvm-ioctls = "0.24"
landlock = "0.4.4"
mac_address = "1.1"
tun-tap = { version = "0.1", default-features = false }
vmm-sys-util = "0.15"

[target.'cfg(target_os = "macos")'.dependencies]
Expand All @@ -92,7 +96,6 @@ memory_addresses = { version = "0.3", default-features = false, features = [
] }

[target.'cfg(target_arch = "aarch64")'.dependencies]
bitflags = "2.11"
memory_addresses = { version = "0.3", default-features = false, features = [
"aarch64",
] }
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,31 @@ For more options, the default values, and the corresponding environment variable
uhyve --help
```

### Networking

> [!NOTE]
> TAP network support is currently only available on Linux.

To use tap networking, you need to create a tap device first and connect it to a suitable network interface (needs root). The following script creates a tap device and bridge interface for guest-host networking:

```sh
ip link add bridge0 type bridge
ip link set bridge0 up
ip addr add 10.0.5.2/24 brd + dev bridge0
ip tuntap add tap10 mode tap one_queue
ip link set dev tap10 up
ip link set tap10 master bridge0
```

Configure the IP address and gateway of your Hermit image via `HERMIT_IP=10.0.5.3` and `HERMIT_GATEWAY=10.0.5.2`. Run the image via:

```sh
uhyve --net=tap:tap10 path/to/image
```

The guest can reach the host at `10.0.5.2` and the guest is available on `10.0.5.3`


### Contributing

If you are interested in contributing to Uhyve, make sure to check out the [Uhyve wiki][uhyve-wiki]!
Expand Down
1 change: 1 addition & 0 deletions benches/benches/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod complete_binary;
pub mod network;
pub mod vm;
246 changes: 246 additions & 0 deletions benches/benches/network.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
use std::{
io::{Read, Write},
net::{Shutdown, TcpListener, TcpStream},
path::PathBuf,
thread,
time::Instant,
};

use byte_unit::{Byte, Unit};
use criterion::{Criterion, criterion_group, measurement::Measurement};
use log::debug;
use regex::Regex;
#[cfg(target_os = "linux")]
use uhyvelib::params::FileSandboxMode;
use uhyvelib::{
params::{NetworkMode, Output, Params},
vm::UhyveVm,
};

use crate::common::{
BuildMode, HERMIT_GATEWAY, HERMIT_IP, build_hermit_bin, check_result_and_print_output,
};

const TOTAL_BYTES: u64 = 1024 * 1024 * 1024;

/// Custom struct for throughput measurements in criterion. Must be used in connection with `iter_custom`
pub struct ThroughputMeasurement;

impl Measurement for ThroughputMeasurement {
type Intermediate = ();
type Value = u64;

fn start(&self) -> Self::Intermediate {}

fn end(&self, _i: Self::Intermediate) -> Self::Value {
unreachable!("This measurement uses iter_custom")
}

fn add(&self, v1: &Self::Value, v2: &Self::Value) -> Self::Value {
*v1 + *v2
}

fn zero(&self) -> Self::Value {
0
}

fn to_f64(&self, value: &Self::Value) -> f64 {
*value as f64
}

fn formatter(&self) -> &dyn criterion::measurement::ValueFormatter {
&ThroughputFormatter
}
}

struct ThroughputFormatter;

impl criterion::measurement::ValueFormatter for ThroughputFormatter {
fn scale_values(&self, typical_value: f64, values: &mut [f64]) -> &'static str {
let (factor, unitstr) = match typical_value {
0.0..1000.0 => (1.0, "bits/s"),
1000.0..1000000.0 => (1000.0, "Kbits/s"),
1000000.0..1000000000.0 => (1000000.0, "Mbits/s"),
1000000000.0.. => (1000000000.0, "Gbits/s"),
_ => unreachable!("Negative Throughput???"),
};
values.iter_mut().for_each(|v| *v /= factor);
unitstr
}

fn scale_throughputs(
&self,
_typical_value: f64,
_throughput: &criterion::Throughput,
_throughputs: &mut [f64],
) -> &'static str {
"bits/s"
}

fn scale_for_machines(&self, _values: &mut [f64]) -> &'static str {
"bits/s"
}
}

fn network_receive_bench(kernel_path: PathBuf) -> u64 {
let params = Params {
cpu_count: 1.try_into().unwrap(),
memory_size: Byte::from_u64_with_unit(64, Unit::MiB)
.unwrap()
.try_into()
.unwrap(),
output: Output::Buffer,
stats: true,
aslr: false,
#[cfg(target_os = "linux")]
file_isolation: FileSandboxMode::None,
network: Some(NetworkMode::Tap {
name: "tap10".to_string(),
}),
kernel_args: vec![
"--".to_owned(),
"testname=receive_bench".to_owned(),
"test_argument=".to_owned(),
],
..Default::default()
};

let t = thread::spawn(move || {
let mut hermit_ip = String::from(HERMIT_IP);
hermit_ip.push_str(":9975");
let mut stream = TcpStream::connect(hermit_ip).unwrap();

let buf = vec![123u8; 64 * 1024]; // Bytes without meaning
let mut sent: u64 = 0;

let start = Instant::now();

while sent < TOTAL_BYTES {
let remaining = (TOTAL_BYTES - sent) as usize;
let to_send = remaining.min(buf.len());
stream.write_all(&buf[..to_send]).unwrap();
sent += to_send as u64;
}

stream.shutdown(Shutdown::Write).unwrap();
let elapsed = start.elapsed();
let secs = elapsed.as_secs_f64();

debug!("Sent {sent} bytes in {secs:.3} s");
let mbit = (sent as f64 * 8.0) / (secs * 1_000_000.0);
debug!("Throughput (sending): {mbit:.2} Mbit/s");
});

let res = UhyveVm::new(kernel_path.clone(), params).unwrap().run(None);

check_result_and_print_output(&res, 0);

let re =
Regex::new(r"(?m)^Throughput \(receiving\):\s*([0-9]+(?:\.[0-9]+)?)\s+Mbit/s").unwrap();

let caps = re.captures(res.output.as_ref().unwrap()).unwrap();
let throughput: f64 = caps[1].parse().expect("invalid number");

t.join().unwrap();
(throughput * 1000000.0) as u64
}

fn network_send_bench(kernel_path: PathBuf) -> u64 {
let params = Params {
cpu_count: 1.try_into().unwrap(),
memory_size: Byte::from_u64_with_unit(64, Unit::MiB)
.unwrap()
.try_into()
.unwrap(),
output: Output::Buffer,
stats: true,
aslr: false,
#[cfg(target_os = "linux")]
file_isolation: FileSandboxMode::None,
network: Some(NetworkMode::Tap {
name: "tap10".to_string(),
}),
kernel_args: vec![
"--".to_owned(),
"testname=send_bench".to_owned(),
format!("test_argument={HERMIT_GATEWAY}:9975/{TOTAL_BYTES}").to_owned(),
],
..Default::default()
};

let t = thread::spawn(move || {
let listener = TcpListener::bind(HERMIT_GATEWAY.to_string() + ":9975").unwrap();
debug!("socket bound");
let (mut stream, peer) = listener.accept().unwrap();
debug!("Got connection from {}", peer);

stream.set_nodelay(true).unwrap();

let mut buf = vec![0u8; 8192];
let mut received: u64 = 0;

let start = Instant::now();
loop {
let n = stream.read(&mut buf).unwrap();
if n == 0 {
// connection terminated
break;
}
received += n as u64;
}

let elapsed = start.elapsed();
let secs = elapsed.as_secs_f64();

debug!("Received {received} bytes in {secs:.3} s");
let mbit = (received as f64 * 8.0) / (secs * 1_000_000.0);
debug!("Throughput (receiving): {mbit:.2} Mbit/s");
});

let res = UhyveVm::new(kernel_path.clone(), params).unwrap().run(None);

check_result_and_print_output(&res, 0);

let re = Regex::new(r"(?m)^Throughput \(sending\):\s*([0-9]+(?:\.[0-9]+)?)\s+Mbit/s").unwrap();

let caps = re.captures(res.output.as_ref().unwrap()).unwrap();
let throughput: f64 = caps[1].parse().expect("invalid number");

t.join().unwrap();
(throughput * 1000000.0) as u64
}

pub fn network_receive_throughput(c: &mut Criterion<ThroughputMeasurement>) {
env_logger::try_init().ok();
let kernel_path = build_hermit_bin("network_test", BuildMode::Release);
c.bench_function("network_receive_throughput", |b| {
b.iter_custom(|iters| {
let mut total: u64 = 0;
for _ in 0..iters {
total += network_receive_bench(kernel_path.clone());
}
total / iters
});
});
}

pub fn network_send_throughput(c: &mut Criterion<ThroughputMeasurement>) {
env_logger::try_init().ok();
let kernel_path = build_hermit_bin("network_test", BuildMode::Release);

c.bench_function("network_send_throughput", |b| {
b.iter_custom(|iters| {
let mut total: u64 = 0;
for _ in 0..iters {
total += network_send_bench(kernel_path.clone());
}
total / iters
});
});
}

criterion_group!(
name = network_benchmark_group;
config = Criterion::default().with_measurement(ThroughputMeasurement).sample_size(10);
targets = network_receive_throughput, network_send_throughput
);
Loading