e2e: virtio: net: add e2e cases for virtio-net

Add e2e cases to test virtio-net connection.
6 cases are added as following:
- virtio_net_ping_test
- virtio_net_ping_test_with_mrg_rxbuf
- vhost_user_net_ping_test
- vhost_user_net_ping_test_with_mrg_rxbuf
- vhost_user_net_ncat_test_with_mrg_rxbuf_guest2host
- vhost_user_net_ncat_test_with_mrg_rxbuf_host2guest

These cases will firstly start a guest vm
with virtio-net device.
First four test following scenarios:
- regular virtio-net without mrg_rxbuf
- regular virtio-net with mrg_rxbuf
- vhost-user-net without mrg_rxbuf
- vhost-user-net with mrg_rxbuf
After vm start, it will configured network in guest,
and then do ping operation in two directions.

Last two test cases will use ncat to
transfer a random file between guest and host via
vhost-user virtio-net with MRG_RXBUF feature.

TEST=tools/run_tests --dut=vm -vv -E \
'package(e2e_tests) & (test(vhost_user_net) | test(virtio_net))'

Change-Id: I76dd1390ece75dee1d5d92b506537ca26c849615
Signed-off-by: Junnan Wu <junnan01.wu@samsung.com>
Reviewed-on: https://chromium-review.googlesource.com/c/crosvm/crosvm/+/6545141
Reviewed-by: David Stevens <stevensd@chromium.org>
Commit-Queue: David Stevens <stevensd@chromium.org>
Reviewed-by: Zihan Chen <zihanchen@google.com>
diff --git a/e2e_tests/tests/net.rs b/e2e_tests/tests/net.rs
new file mode 100644
index 0000000..3bc7ce7
--- /dev/null
+++ b/e2e_tests/tests/net.rs
@@ -0,0 +1,470 @@
+// Copyright 2025 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Testing virtio-net.
+#![cfg(any(target_os = "android", target_os = "linux"))]
+
+use std::fs::File;
+use std::io::ErrorKind;
+use std::path::Path;
+use std::process::Command;
+use std::process::Stdio;
+use std::time::Duration;
+
+use anyhow::anyhow;
+use fixture::utils::retry_with_delay;
+use fixture::vhost_user::CmdType;
+use fixture::vhost_user::Config as VuConfig;
+use fixture::vhost_user::VhostUserBackend;
+use fixture::vm::Config as VmConfig;
+use fixture::vm::TestVm;
+use tempfile::NamedTempFile;
+const VIRTIO_NET_F_MRG_RXBUF: usize = 15;
+const NCAT_RETRIES: usize = 15;
+const NCAT_RETRY_DELAY: Duration = Duration::from_millis(300);
+
+pub fn create_vu_net_config(socket: &Path, host_net_name: String, mrg_rxbuf: bool) -> VuConfig {
+    let socket_path = socket.to_str().unwrap();
+    let mut args = vec![
+        "net".to_string(),
+        "--tap-name".to_string(),
+        format!("{socket_path},{host_net_name}").to_string(),
+    ];
+    if mrg_rxbuf {
+        args.push("--mrg-rxbuf".to_string());
+    }
+    VuConfig::new(CmdType::Device, "net").extra_args(args)
+}
+
+fn create_guest_with_virtio_net_backend(
+    config: VmConfig,
+    host_ip_with_mask: String,
+    host_net_name: String,
+    mrg_rxbuf: bool,
+    vhost_user_mode: bool,
+) -> anyhow::Result<(Option<VhostUserBackend>, TestVm)> {
+    // Del crosvm_tap if exist
+    Command::new("sudo")
+        .args(["ip", "tuntap", "del", "mode", "tap", &host_net_name])
+        .output()
+        .unwrap_or_else(|_| panic!("Fail to del {}", host_net_name));
+    // Enable crosvm_tap in backend and up
+    Command::new("sudo")
+        .args([
+            "ip",
+            "tuntap",
+            "add",
+            "mode",
+            "tap",
+            "user",
+            "crosvm",
+            &host_net_name,
+        ])
+        .output()
+        .unwrap_or_else(|_| panic!("Fail to create {}", host_net_name));
+    Command::new("sudo")
+        .args([
+            "ip",
+            "addr",
+            "add",
+            &host_ip_with_mask,
+            "dev",
+            &host_net_name,
+        ])
+        .output()
+        .unwrap_or_else(|_| panic!("Fail to set {} address", host_net_name));
+    Command::new("sudo")
+        .args(["ip", "link", "set", &host_net_name, "up"])
+        .output()
+        .unwrap_or_else(|_| panic!("Fail to up {}", host_net_name));
+
+    let (vu, cfg) = if vhost_user_mode {
+        // Start a vhost-user-net backend firstly
+        let socket = NamedTempFile::new().unwrap();
+        let vu_config = create_vu_net_config(socket.path(), host_net_name, mrg_rxbuf);
+        let vu_device = VhostUserBackend::new_sudo(vu_config).unwrap();
+        (
+            Some(vu_device),
+            config
+                .extra_args(vec!["--mem".to_owned(), "512".to_owned()])
+                .with_vhost_user("net", socket.path()),
+        )
+    } else {
+        let mut extra_args = vec!["--mem".to_owned(), "512".to_owned(), "--net".to_owned()];
+        if mrg_rxbuf {
+            extra_args.push(format!("tap-name={},mrg-rxbuf", host_net_name))
+        } else {
+            extra_args.push(format!("tap-name={}", host_net_name))
+        }
+        (None, config.extra_args(extra_args))
+    };
+
+    let guest_vm = TestVm::new_sudo(cfg).expect("fail to create guest vm");
+    if vhost_user_mode {
+        Ok((vu, guest_vm))
+    } else {
+        Ok((None, guest_vm))
+    }
+}
+
+/// Configure guest virtio-net device, return virtio name
+fn network_configure_in_guest(
+    vm: &mut TestVm,
+    host_ip: String,
+    guest_ip: String,
+) -> anyhow::Result<String> {
+    // mount sysfs to check details
+    vm.exec_in_guest("mount -t sysfs sysfs /sys")
+        .expect("fail to mount sysfs in vm");
+
+    // Parse virtio device list and find out virtio-net id
+    let result = vm
+        .exec_in_guest("cat /sys/bus/virtio/devices/*/device")
+        .expect("Can't get virtio devices id");
+
+    let virtio_id_list: Vec<&str> = result.stdout.split("\n").collect();
+    let virtio_net_id = virtio_id_list.iter().position(|&x| x == "0x0001");
+    // The name of virtio-net driver is virtioX
+    let virtio_name = if let Some(id) = virtio_net_id {
+        format!("virtio{}", id)
+    } else {
+        return Err(anyhow!("fail to find virtio net driver"));
+    };
+
+    // Find the ethernet interface name in guest
+    let guest_dev = vm
+        .exec_in_guest(&format!("ls /sys/bus/virtio/devices/{}/net", virtio_name))
+        .expect("Can not find the name of virtio-net")
+        .stdout
+        .trim_end()
+        .to_string();
+
+    // set ip address in guest
+    vm.exec_in_guest(&format!("ip addr add {}/24 dev {}", guest_ip, guest_dev))
+        .expect("fail to configure net device address");
+    // up network device
+    vm.exec_in_guest(&format!("ip link set {} up", guest_dev))
+        .expect("fail to up net device");
+    // route information add
+    vm.exec_in_guest(&format!("ip route add default via {}", host_ip))
+        .expect("fail to configure net device address");
+
+    vm.exec_in_guest("ip route show")
+        .expect("fail to configure net device address");
+    Ok(virtio_name)
+}
+
+/// Check whether MRG_RXBUF feature bit is configured in guest driver
+/// vm: guest test VM
+/// virtio_name: the virtio driver name in guest (e.g. virtio0)
+fn check_driver_negotiated_features_with_mrg_rxbuf(
+    vm: &mut TestVm,
+    virtio_name: String,
+) -> anyhow::Result<bool> {
+    let binding = vm
+        .exec_in_guest(&format!(
+            "cat /sys/bus/virtio/devices/{}/features",
+            virtio_name
+        ))
+        .expect("Can not get the features of virtio-net");
+    // Find the ethernet interface name in guest
+    let features = binding.stdout.trim_end();
+
+    Ok(features.chars().nth(VIRTIO_NET_F_MRG_RXBUF).unwrap() == '1')
+}
+
+fn test_net_connection(
+    config: VmConfig,
+    host_ip: String,
+    host_net_name: String,
+    guest_ip: String,
+    mrg_rxbuf: bool,
+    vhost_user_mode: bool,
+) -> anyhow::Result<()> {
+    let host_ip_with_mask = format!("{}/24", host_ip);
+    let (_vu_device, mut vm) = create_guest_with_virtio_net_backend(
+        config,
+        host_ip_with_mask,
+        host_net_name.clone(),
+        mrg_rxbuf,
+        vhost_user_mode,
+    )
+    .expect("fail to create device and start vm");
+    let virtio_name = network_configure_in_guest(&mut vm, host_ip.clone(), guest_ip.clone())?;
+
+    assert_eq!(
+        mrg_rxbuf,
+        check_driver_negotiated_features_with_mrg_rxbuf(&mut vm, virtio_name)?
+    );
+    let packets_num = "5";
+    let host_ping_guest_result = Command::new("ping")
+        .args([&guest_ip, "-c", packets_num])
+        .output()
+        .expect("fail to ping guest")
+        .stdout;
+
+    assert!(String::from_utf8(host_ping_guest_result)
+        .unwrap()
+        .contains(&format!(
+            "{} packets transmitted, {} received",
+            packets_num, packets_num
+        )));
+    let guest_ping_host_result = vm
+        .exec_in_guest(&format!("ping {} -c {}", host_ip.clone(), packets_num))
+        .expect("fail to ping host")
+        .stdout;
+    assert!(guest_ping_host_result.contains(&format!(
+        "{} packets transmitted, {} received",
+        packets_num, packets_num
+    )));
+    Command::new("sudo")
+        .args(["ip", "link", "set", &host_net_name.clone(), "down"])
+        .output()
+        .expect("fail to set device down");
+    Ok(())
+}
+
+#[test]
+fn virtio_net_ping_test() -> anyhow::Result<()> {
+    let vm_config = VmConfig::new();
+    test_net_connection(
+        vm_config,
+        "192.168.10.1".to_owned(),
+        "crosvm_tap0".to_owned(),
+        "192.168.10.2".to_owned(),
+        false,
+        false,
+    )?;
+    Ok(())
+}
+
+#[test]
+fn virtio_net_ping_test_with_mrg_rxbuf() -> anyhow::Result<()> {
+    let vm_config = VmConfig::new();
+    test_net_connection(
+        vm_config,
+        "192.168.11.1".to_owned(),
+        "crosvm_tap1".to_owned(),
+        "192.168.11.2".to_owned(),
+        true,
+        false,
+    )?;
+    Ok(())
+}
+
+#[test]
+fn vhost_user_net_ping_test() -> anyhow::Result<()> {
+    let vm_config = VmConfig::new();
+    test_net_connection(
+        vm_config,
+        "192.168.12.1".to_owned(),
+        "crosvm_tap2".to_owned(),
+        "192.168.12.2".to_owned(),
+        false,
+        true,
+    )?;
+    Ok(())
+}
+
+#[test]
+fn vhost_user_net_ping_test_with_mrg_rxbuf() -> anyhow::Result<()> {
+    let vm_config = VmConfig::new();
+    test_net_connection(
+        vm_config,
+        "192.168.13.1".to_owned(),
+        "crosvm_tap3".to_owned(),
+        "192.168.13.2".to_owned(),
+        true,
+        true,
+    )?;
+    Ok(())
+}
+
+fn guest_to_host_ncat_test(vm: &mut TestVm, host_ip: String, port: String) -> anyhow::Result<()> {
+    let listen_port = port;
+    let listen_args = vec!["-l", &listen_port];
+    //Create a recv file in host, then ncat listen a port and re-direct to this file
+    let recv_file = File::create("/tmp/host_recv.txt")?;
+    Command::new("ncat")
+        .args(listen_args)
+        .stdout(Stdio::from(recv_file))
+        .spawn()
+        .expect("fail to spawn");
+
+    // create a random file in guest and get the md5sum value of this file
+    vm.exec_in_guest("mount -t tmpfs tmpfs /tmp")
+        .expect("fail to mount tmpfs in vm");
+
+    vm.exec_in_guest("dd if=/dev/random of=/tmp/guest_send.txt bs=1M count=10")
+        .expect("fail to generate a random file");
+
+    let md5_guest = vm
+        .exec_in_guest("md5sum /tmp/guest_send.txt | awk '{ print $1 }'")
+        .expect("fail to check md5sum")
+        .stdout;
+
+    // Transfer this file to host via virtio-net and calculate its md5sum value
+    vm.exec_in_guest(&format!(
+        "ncat {} {listen_port} < /tmp/guest_send.txt",
+        host_ip
+    ))
+    .expect("fail to send file");
+
+    let res = Command::new("md5sum")
+        .stdout(Stdio::piped())
+        .args(["/tmp/host_recv.txt"])
+        .output()?
+        .stdout;
+    let md5_host = String::from_utf8(res)?
+        .split_whitespace()
+        .next()
+        .unwrap()
+        .to_string();
+
+    assert_eq!(md5_guest.trim_end(), md5_host);
+    Ok(())
+}
+
+fn host_to_guest_ncat_test(vm: &mut TestVm, guest_ip: String, port: String) -> anyhow::Result<()> {
+    vm.exec_in_guest("mount -t tmpfs tmpfs /tmp")
+        .expect("fail to mount tmpfs in vm");
+
+    //Create a send file in host
+    Command::new("dd")
+        .args([
+            "if=/dev/random",
+            "of=/tmp/host_send.txt",
+            "bs=1M",
+            "count=10",
+        ])
+        .output()
+        .expect("fail to generate send file");
+    let res = Command::new("md5sum")
+        .stdout(Stdio::piped())
+        .args(["/tmp/host_send.txt"])
+        .output()?
+        .stdout;
+    let md5_host = String::from_utf8(res)?
+        .split_whitespace()
+        .next()
+        .unwrap()
+        .to_string();
+    let guest_listen_cmd = format!("ncat -l {} > /tmp/guest_recv.txt", port.clone());
+    let guest_cmd = vm.exec_in_guest_async(&guest_listen_cmd).unwrap();
+
+    retry_with_delay(
+        move || {
+            let send_file = File::open("/tmp/host_send.txt")?;
+            let out = Command::new("ncat")
+                .args([&guest_ip, &port])
+                .stdin(Stdio::from(send_file))
+                .output();
+            // if connection refused, it will still return Ok, then retry will exit.
+            if out.as_ref().is_ok_and(|x| {
+                String::from_utf8(x.stderr.clone()).unwrap() != "Ncat: Connection refused.\n"
+            }) {
+                out
+            } else {
+                Err(std::io::Error::new(
+                    ErrorKind::Other,
+                    "Ncat: Connection refused",
+                ))
+            }
+        },
+        NCAT_RETRIES,
+        NCAT_RETRY_DELAY,
+    )
+    .unwrap();
+
+    guest_cmd.wait_ok(vm).unwrap();
+    let md5_guest = vm
+        .exec_in_guest("md5sum /tmp/guest_recv.txt | awk '{ print $1 }'")
+        .expect("fail to check md5sum")
+        .stdout;
+    assert_eq!(md5_guest.trim_end(), md5_host);
+    Ok(())
+}
+
+fn test_ncat_guest_to_host(
+    config: VmConfig,
+    host_ip: String,
+    host_net_name: String,
+    guest_ip: String,
+    mrg_rxbuf: bool,
+    vhost_user_mode: bool,
+) -> anyhow::Result<()> {
+    let host_ip_with_mask = format!("{}/24", host_ip);
+    let (_vu_device, mut vm) = create_guest_with_virtio_net_backend(
+        config,
+        host_ip_with_mask,
+        host_net_name.clone(),
+        mrg_rxbuf,
+        vhost_user_mode,
+    )
+    .expect("fail to create device and start vm");
+    let virtio_name = network_configure_in_guest(&mut vm, host_ip.clone(), guest_ip.clone())?;
+
+    assert_eq!(
+        mrg_rxbuf,
+        check_driver_negotiated_features_with_mrg_rxbuf(&mut vm, virtio_name)?
+    );
+    guest_to_host_ncat_test(&mut vm, host_ip.clone(), "1111".to_owned())?;
+    Ok(())
+}
+
+fn test_ncat_host_to_guest(
+    config: VmConfig,
+    host_ip: String,
+    host_net_name: String,
+    guest_ip: String,
+    mrg_rxbuf: bool,
+    vhost_user_mode: bool,
+) -> anyhow::Result<()> {
+    let host_ip_with_mask = format!("{}/24", host_ip);
+    let (_vu_device, mut vm) = create_guest_with_virtio_net_backend(
+        config,
+        host_ip_with_mask,
+        host_net_name.clone(),
+        mrg_rxbuf,
+        vhost_user_mode,
+    )
+    .expect("fail to create device and start vm");
+    let virtio_name = network_configure_in_guest(&mut vm, host_ip.clone(), guest_ip.clone())?;
+
+    assert_eq!(
+        mrg_rxbuf,
+        check_driver_negotiated_features_with_mrg_rxbuf(&mut vm, virtio_name)?
+    );
+    host_to_guest_ncat_test(&mut vm, guest_ip.clone(), "1234".to_owned())?;
+
+    Ok(())
+}
+
+#[test]
+fn vhost_user_net_ncat_test_with_mrg_rxbuf_guest2host() -> anyhow::Result<()> {
+    let vm_config = VmConfig::new();
+    test_ncat_guest_to_host(
+        vm_config,
+        "192.168.14.1".to_owned(),
+        "crosvm_tap4".to_owned(),
+        "192.168.14.2".to_owned(),
+        true,
+        true,
+    )?;
+    Ok(())
+}
+
+#[test]
+fn vhost_user_net_ncat_test_with_mrg_rxbuf_host2guest() -> anyhow::Result<()> {
+    let vm_config = VmConfig::new();
+    test_ncat_host_to_guest(
+        vm_config,
+        "192.168.15.1".to_owned(),
+        "crosvm_tap5".to_owned(),
+        "192.168.15.2".to_owned(),
+        true,
+        true,
+    )?;
+    Ok(())
+}