diff --git a/Cargo.toml b/Cargo.toml index dec77b6..80451e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,4 @@ libc = "0.2.82" serde = "1.0.120" serde_derive = "1.0.120" text_io = "0.1.8" -nix = {version = "0.27.1", features = ["socket", "fs"]} +nix = {version = "0.27.1", features = ["socket", "fs"]} \ No newline at end of file diff --git a/docs/krunvm-changevm.1.txt b/docs/krunvm-changevm.1.txt index 4f14e90..e00838d 100644 --- a/docs/krunvm-changevm.1.txt +++ b/docs/krunvm-changevm.1.txt @@ -61,6 +61,8 @@ host visible in the guest. An empty string ("") tells krunvm to not set a working directory explicitly, letting libkrun decide which one should be set. +*--net* _NETWORK_MODE_:: + Configures the network connection mode. Supported modes are either PASST or TSI. SEE ALSO -------- diff --git a/docs/krunvm-config.1.txt b/docs/krunvm-config.1.txt index 168ed91..ae3b416 100644 --- a/docs/krunvm-config.1.txt +++ b/docs/krunvm-config.1.txt @@ -34,6 +34,9 @@ OPTIONS Sets the default mount of RAM, in MiB, that will be configured for newly created microVMs. +*--net* _NETWORK_MODE_:: + Sets the default network connection mode, that will be configured for + newly created microVMs. Supported modes are PASST or TSI. SEE ALSO -------- diff --git a/docs/krunvm-create.1.txt b/docs/krunvm-create.1.txt index d63f3f8..488bcb4 100644 --- a/docs/krunvm-create.1.txt +++ b/docs/krunvm-create.1.txt @@ -53,6 +53,8 @@ host visible in the guest. An empty string ("") tells krunvm to not set a working directory explicitly, letting libkrun decide which one should be set. +*--net* _NETWORK_MODE_:: + Set the network connection mode. Supported modes are either PASST or TSI. SEE ALSO -------- diff --git a/docs/krunvm.1.txt b/docs/krunvm.1.txt index 8a956e8..e04ea82 100644 --- a/docs/krunvm.1.txt +++ b/docs/krunvm.1.txt @@ -29,10 +29,15 @@ microVM and exposing ports from the guest to the host (and the networks connected to it). Networking to the guest running in the microVM is provided by -libkrun's TSI (Transparent Socket Impersonation), enabling a seamless -experience that doesn't require network bridges nor other explicit -network configuration. +either libkrun's TSI (Transparent Socket Impersonation) or PASST. +TSI enables a seamless experience that doesn't require network bridges nor other explicit +network configuration. It only supports impersonating AF_INET SOCK_DGRAM and SOCK_STREAM sockets. +This implies it's not possible to communicate outside the VM with raw sockets. + +PASST uses virtio-net guest device and sends all traffic to a passt subprocess. +Support of network protocols is therefore dependent on what passt supports. +Note that currently you need to run a DHCP client in the guest to get an IP address. GLOBAL OPTIONS -------------- diff --git a/src/bindings.rs b/src/bindings.rs index 41dc52c..b8676e3 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -13,6 +13,7 @@ extern "C" { pub fn krun_set_mapped_volumes(ctx: u32, mapped_volumes: *const *const c_char) -> i32; pub fn krun_set_port_map(ctx: u32, port_map: *const *const c_char) -> i32; pub fn krun_set_workdir(ctx: u32, workdir_path: *const c_char) -> i32; + pub fn krun_set_passt_fd(ctx: u32, fd: c_int) -> i32; pub fn krun_set_exec( ctx: u32, exec_path: *const c_char, diff --git a/src/commands/changevm.rs b/src/commands/changevm.rs index 5cd365b..9b7e9d7 100644 --- a/src/commands/changevm.rs +++ b/src/commands/changevm.rs @@ -4,8 +4,8 @@ use clap::Args; use std::collections::HashMap; +use crate::config::{KrunvmConfig, NetworkMode}; use crate::utils::{path_pairs_to_hash_map, port_pairs_to_hash_map, PathPair, PortPair}; -use crate::{KrunvmConfig, APP_NAME}; use super::list::printvm; @@ -46,6 +46,10 @@ pub struct ChangeVmCmd { /// Port(s) in format "host_port:guest_port" to be exposed to the host #[arg(long = "port")] ports: Vec, + + /// Set the network connection mode for the microVM + #[arg(long)] + net: Option, } impl ChangeVmCmd { @@ -130,12 +134,17 @@ impl ChangeVmCmd { cfg_changed = true; } + if let Some(network_mode) = self.net { + vmcfg.network_mode = network_mode; + cfg_changed = true; + } + println!(); printvm(vmcfg); println!(); if cfg_changed { - confy::store(APP_NAME, &cfg).unwrap(); + crate::config::save(cfg).unwrap(); } } } diff --git a/src/commands/config.rs b/src/commands/config.rs index ab09d34..1e3b372 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -1,7 +1,7 @@ // Copyright 2021 Red Hat, Inc. // SPDX-License-Identifier: Apache-2.0 -use crate::{KrunvmConfig, APP_NAME}; +use crate::config::{KrunvmConfig, NetworkMode}; use clap::Args; /// Configure global values @@ -18,6 +18,10 @@ pub struct ConfigCmd { /// DNS server to use in the microVM #[arg(long)] dns: Option, + + /// Default network connection mode to use + #[arg(long)] + net: Option, } impl ConfigCmd { @@ -47,11 +51,18 @@ impl ConfigCmd { cfg_changed = true; } + if let Some(network_mode) = self.net { + if network_mode != cfg.default_network_mode { + cfg.default_network_mode = network_mode; + cfg_changed = true; + } + } + if cfg_changed { - confy::store(APP_NAME, &cfg).unwrap(); + crate::config::save(cfg).unwrap(); } - println!("Global configuration:"); + println!("Global config:"); println!( "Default number of CPUs for newly created VMs: {}", cfg.default_cpus diff --git a/src/commands/create.rs b/src/commands/create.rs index 4040983..60d7ae0 100644 --- a/src/commands/create.rs +++ b/src/commands/create.rs @@ -1,6 +1,8 @@ // Copyright 2021 Red Hat, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::config::{KrunvmConfig, NetworkMode, VmConfig}; +use crate::APP_NAME; use clap::Args; use std::fs; use std::io::Write; @@ -12,8 +14,6 @@ use crate::utils::{ get_buildah_args, mount_container, path_pairs_to_hash_map, port_pairs_to_hash_map, umount_container, BuildahCommand, PathPair, PortPair, }; -use crate::{KrunvmConfig, VmConfig, APP_NAME}; - #[cfg(target_os = "macos")] const KRUNVM_ROSETTA_FILE: &str = ".krunvm-rosetta"; @@ -51,6 +51,10 @@ pub struct CreateCmd { #[arg(long = "port")] ports: Vec, + /// Network connection mode to use + #[arg(long)] + net: Option, + /// Create a x86_64 microVM even on an Aarch64 host #[arg(short, long)] #[cfg(target_os = "macos")] @@ -68,6 +72,7 @@ impl CreateCmd { let mapped_ports = port_pairs_to_hash_map(self.ports); let image = self.image; let name = self.name; + let network_mode = self.net.unwrap_or_else(|| cfg.default_network_mode.clone()); if let Some(ref name) = name { if cfg.vmconfig_map.contains_key(name) { @@ -160,6 +165,7 @@ https://threedots.ovh/blog/2022/06/quick-look-at-rosetta-on-linux/ workdir: workdir.to_string(), mapped_volumes, mapped_ports, + network_mode, }; let rootfs = mount_container(cfg, &vmcfg).unwrap(); diff --git a/src/commands/delete.rs b/src/commands/delete.rs index 2f4c54a..d027a92 100644 --- a/src/commands/delete.rs +++ b/src/commands/delete.rs @@ -1,7 +1,8 @@ // Copyright 2021 Red Hat, Inc. // SPDX-License-Identifier: Apache-2.0 -use crate::{KrunvmConfig, APP_NAME}; +use crate::config; +use crate::config::KrunvmConfig; use clap::Args; use crate::utils::{remove_container, umount_container}; @@ -26,6 +27,6 @@ impl DeleteCmd { umount_container(cfg, &vmcfg).unwrap(); remove_container(cfg, &vmcfg).unwrap(); - confy::store(APP_NAME, &cfg).unwrap(); + config::save(cfg).unwrap() } } diff --git a/src/commands/list.rs b/src/commands/list.rs index 791943c..7ca03cf 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,7 +1,7 @@ // Copyright 2021 Red Hat, Inc. // SPDX-License-Identifier: Apache-2.0 -use crate::{KrunvmConfig, VmConfig}; +use crate::config::{KrunvmConfig, VmConfig}; use clap::Args; /// List microVMs @@ -33,6 +33,7 @@ pub fn printvm(vm: &VmConfig) { println!(" DNS server: {}", vm.dns); println!(" Buildah container: {}", vm.container); println!(" Workdir: {}", vm.workdir); + println!(" Network mode: {:?}", vm.network_mode); println!(" Mapped volumes: {:?}", vm.mapped_volumes); println!(" Mapped ports: {:?}", vm.mapped_ports); } diff --git a/src/commands/start.rs b/src/commands/start.rs index 2d67423..11641a5 100644 --- a/src/commands/start.rs +++ b/src/commands/start.rs @@ -2,18 +2,29 @@ // SPDX-License-Identifier: Apache-2.0 use clap::Args; -use libc::c_char; +use libc::{c_char, c_int}; +use nix::errno::Errno; +use nix::sys::socket::{socketpair, AddressFamily, SockFlag, SockType}; +use std::collections::HashMap; use std::ffi::CString; + use std::fs::File; #[cfg(target_os = "linux")] use std::io::{Error, ErrorKind}; + +use std::os::fd::{IntoRawFd, OwnedFd}; use std::os::unix::io::AsRawFd; + #[cfg(target_os = "macos")] use std::path::Path; +use std::process::Stdio; + +use nix::fcntl::{fcntl, FcntlArg, FdFlag}; use crate::bindings; +use crate::bindings::krun_set_passt_fd; +use crate::config::{KrunvmConfig, NetworkMode, VmConfig}; use crate::utils::{mount_container, umount_container}; -use crate::{KrunvmConfig, VmConfig}; #[derive(Args, Debug)] /// Start an existing microVM @@ -36,6 +47,49 @@ pub struct StartCmd { mem: Option, // TODO: implement or remove this } +fn start_passt(mapped_ports: &HashMap) -> Result { + let (passt_fd, krun_fd) = socketpair( + AddressFamily::Unix, + SockType::Stream, + None, + SockFlag::empty(), + ) + .map_err(|e| { + eprint!("Failed to create socket pair for passt: {e}"); + })?; + + if let Err(e) = fcntl(krun_fd.as_raw_fd(), FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC)) { + eprint!("Failed to set FD_CLOEXEC: {e}"); + } + + let mut cmd = std::process::Command::new("passt"); + cmd.arg("-q") + .arg("-f") + .arg("-F") + .arg(passt_fd.as_raw_fd().to_string()); + + if !mapped_ports.is_empty() { + let comma_separated_ports = mapped_ports + .iter() + .map(|(host_port, guest_port)| format!("{}:{}", host_port, guest_port)) + .collect::>() + .join(","); + + cmd.arg("-t").arg(comma_separated_ports); + } + + cmd.stdout(Stdio::null()) + .stderr(Stdio::null()) + .stdin(Stdio::null()); + + if let Err(e) = cmd.spawn() { + eprintln!("Failed to start passt: {e}"); + return Err(()); + } + + Ok(krun_fd) +} + impl StartCmd { pub fn run(self, cfg: &KrunvmConfig) { let vmcfg = match cfg.vmconfig_map.get(&self.name) { @@ -152,10 +206,29 @@ unsafe fn exec_vm(vmcfg: &VmConfig, rootfs: &str, cmd: Option<&str>, args: Vec { + let ret = bindings::krun_set_port_map(ctx, ps.as_ptr()); + if ret < 0 { + println!("Error setting VM port map"); + std::process::exit(-1); + } + } + NetworkMode::Passt => { + let Ok(passt_fd) = start_passt(&vmcfg.mapped_ports) else { + std::process::exit(-1); + }; + let ret = krun_set_passt_fd(ctx, passt_fd.into_raw_fd() as c_int); + if ret < 0 { + let errno = Errno::from_i32(-ret); + if errno == Errno::ENOTSUP { + println!("Failed to set passt fd: your libkrun build does not support virtio-net/passt mode."); + } else { + println!("Failed to set passt fd: {}", errno); + } + std::process::exit(-1); + } + } } if !vmcfg.workdir.is_empty() { diff --git a/src/config/migrate.rs b/src/config/migrate.rs new file mode 100644 index 0000000..91a5956 --- /dev/null +++ b/src/config/migrate.rs @@ -0,0 +1,138 @@ +use crate::config::{v1, v2}; +use confy::ConfyError; + +pub fn migrate_and_load_impl( + load_v2: impl FnOnce() -> Result, + load_v1: impl FnOnce() -> Result, + save_v2: impl FnOnce(&v2::KrunvmConfig) -> Result<(), ()>, +) -> Result { + fn check_version(got: u8, expected: u8) -> Result<(), ()> { + if expected != got { + eprintln!( + "Invalid config version number {} expected {}", + got, expected + ); + Err(()) + } else { + Ok(()) + } + } + + let v2_load_err = match load_v2() { + Ok(conf) => { + check_version(conf.version, 2)?; + return Ok(conf); + } + Err(e) => e, + }; + + let v1_load_err = match load_v1() { + Ok(cfg) => { + check_version(cfg.version, 1)?; + let v2_config = cfg.into(); + save_v2(&v2_config)?; + return Ok(v2_config); + } + Err(e) => e, + }; + + eprintln!("Failed to load config: "); + eprintln!("Tried to load as as v2 config, got error: {v2_load_err}"); + eprintln!("Tried to load as as v1 config, got error: {v1_load_err}"); + Err(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::NetworkMode; + use std::collections::HashMap; + + #[test] + fn load_without_migrate() { + let cfg = v2::KrunvmConfig { + default_dns: "8.8.8.8".into(), + ..v2::KrunvmConfig::default() + }; + + let returned_cfg = migrate_and_load_impl( + || Ok(cfg.clone()), + || panic!("Loading v1 should not be attempted"), + |_| panic!("Migration should not occur"), + ) + .unwrap(); + assert_eq!(returned_cfg, cfg); + } + + #[test] + fn load_migrating_to_v2() { + let v1_vms = [( + "fedora".to_string(), + v1::VmConfig { + name: "fedora".to_string(), + mapped_ports: Default::default(), + cpus: 2, + dns: "1.1.1.1".to_string(), + mapped_volumes: Default::default(), + workdir: "/".to_string(), + container: "fedora".to_string(), + mem: 8192, + }, + )]; + + let v1_cfg = v1::KrunvmConfig { + default_dns: "8.8.8.8".into(), + vmconfig_map: HashMap::from(v1_vms), + ..v1::KrunvmConfig::default() + }; + + let result_v2_vms = [( + "fedora".to_string(), + v2::VmConfig { + name: "fedora".to_string(), + mapped_ports: Default::default(), + cpus: 2, + dns: "1.1.1.1".to_string(), + mapped_volumes: Default::default(), + workdir: "/".to_string(), + container: "fedora".to_string(), + mem: 8192, + network_mode: NetworkMode::Tsi, + }, + )]; + + let result_v2_cfg = v2::KrunvmConfig { + default_dns: "8.8.8.8".into(), + vmconfig_map: HashMap::from(result_v2_vms), + default_network_mode: NetworkMode::Tsi, + ..v2::KrunvmConfig::default() + }; + + let mut load_v2_called = false; + let mut load_v1_called = false; + let mut save_called = false; + + let returned_cfg = migrate_and_load_impl( + || { + load_v2_called = true; + Err(ConfyError::BadConfigDirectoryStr) + }, + || { + load_v1_called = true; + Ok(v1_cfg) + }, + |migrated| { + save_called = true; + assert_eq!(migrated, &result_v2_cfg); + Ok(()) + }, + ) + .unwrap(); + + assert!(save_called, "Save must be called"); + assert!(load_v2_called, "Load v2 must be called"); + assert!(save_called, "Load v1 must be called"); + + assert_eq!(returned_cfg, result_v2_cfg); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..f50873c --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,20 @@ +mod migrate; +mod v1; +mod v2; + +use crate::APP_NAME; + +use crate::config::migrate::migrate_and_load_impl; +pub use v2::{KrunvmConfig, NetworkMode, VmConfig}; + +pub fn save(cfg: &KrunvmConfig) -> Result<(), ()> { + confy::store(APP_NAME, cfg).map_err(|e| eprintln!("Failed to load config: {e}")) +} + +pub fn load() -> Result { + migrate_and_load_impl( + || confy::load::(APP_NAME), + || confy::load::(APP_NAME), + save, + ) +} diff --git a/src/config/v1.rs b/src/config/v1.rs new file mode 100644 index 0000000..877f034 --- /dev/null +++ b/src/config/v1.rs @@ -0,0 +1,37 @@ +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Serialize, Deserialize)] +pub struct KrunvmConfig { + pub version: u8, + pub default_cpus: u32, + pub default_mem: u32, + pub default_dns: String, + pub storage_volume: String, + pub vmconfig_map: HashMap, +} + +impl Default for KrunvmConfig { + fn default() -> KrunvmConfig { + KrunvmConfig { + version: 1, + default_cpus: 2, + default_mem: 1024, + default_dns: "1.1.1.1".to_string(), + storage_volume: String::new(), + vmconfig_map: HashMap::new(), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VmConfig { + pub name: String, + pub cpus: u32, + pub mem: u32, + pub container: String, + pub workdir: String, + pub dns: String, + pub mapped_volumes: HashMap, + pub mapped_ports: HashMap, +} diff --git a/src/config/v2.rs b/src/config/v2.rs new file mode 100644 index 0000000..c48e3d7 --- /dev/null +++ b/src/config/v2.rs @@ -0,0 +1,98 @@ +use crate::config::v1; + +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::str::FromStr; + +#[derive(Clone, Serialize, Deserialize, Debug, Default, Eq, PartialEq)] +pub enum NetworkMode { + #[default] + Tsi, + Passt, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct KrunvmConfig { + pub version: u8, + pub default_cpus: u32, + pub default_mem: u32, + pub default_dns: String, + pub default_network_mode: NetworkMode, + pub storage_volume: String, + pub vmconfig_map: HashMap, +} + +impl Default for KrunvmConfig { + fn default() -> KrunvmConfig { + KrunvmConfig { + version: 2, + default_cpus: 2, + default_mem: 1024, + default_dns: "1.1.1.1".to_string(), + default_network_mode: NetworkMode::default(), + storage_volume: String::new(), + vmconfig_map: HashMap::new(), + } + } +} + +impl From for KrunvmConfig { + fn from(old: v1::KrunvmConfig) -> Self { + KrunvmConfig { + version: 2, + default_cpus: old.default_cpus, + default_mem: old.default_mem, + default_dns: old.default_dns, + default_network_mode: NetworkMode::default(), + storage_volume: old.storage_volume, + vmconfig_map: old + .vmconfig_map + .into_iter() + .map(|(key, value)| (key, value.into())) + .collect(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct VmConfig { + pub name: String, + pub cpus: u32, + pub mem: u32, + pub container: String, + pub workdir: String, + pub dns: String, + pub network_mode: NetworkMode, + pub mapped_volumes: HashMap, + pub mapped_ports: HashMap, +} + +impl From for VmConfig { + fn from(old: v1::VmConfig) -> Self { + VmConfig { + name: old.name, + cpus: old.cpus, + mem: old.mem, + container: old.container, + workdir: old.workdir, + dns: old.dns, + mapped_volumes: old.mapped_volumes, + mapped_ports: old.mapped_ports, + network_mode: NetworkMode::default(), + } + } +} + +impl FromStr for NetworkMode { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("tsi") { + Ok(NetworkMode::Tsi) + } else if s.eq_ignore_ascii_case("passt") { + Ok(NetworkMode::Passt) + } else { + Err("Invalid network mode") + } + } +} diff --git a/src/main.rs b/src/main.rs index 0531136..c35d619 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,60 +1,26 @@ // Copyright 2021 Red Hat, Inc. // SPDX-License-Identifier: Apache-2.0 -use std::collections::HashMap; #[cfg(target_os = "macos")] use std::fs::File; #[cfg(target_os = "macos")] use std::io::{self, Read, Write}; use crate::commands::{ChangeVmCmd, ConfigCmd, CreateCmd, DeleteCmd, ListCmd, StartCmd}; +#[cfg(target_os = "macos")] +use crate::config::KrunvmConfig; use clap::{Parser, Subcommand}; -use serde_derive::{Deserialize, Serialize}; #[cfg(target_os = "macos")] use text_io::read; #[allow(unused)] mod bindings; mod commands; +mod config; mod utils; const APP_NAME: &str = "krunvm"; -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct VmConfig { - name: String, - cpus: u32, - mem: u32, - container: String, - workdir: String, - dns: String, - mapped_volumes: HashMap, - mapped_ports: HashMap, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct KrunvmConfig { - version: u8, - default_cpus: u32, - default_mem: u32, - default_dns: String, - storage_volume: String, - vmconfig_map: HashMap, -} - -impl Default for KrunvmConfig { - fn default() -> KrunvmConfig { - KrunvmConfig { - version: 1, - default_cpus: 2, - default_mem: 1024, - default_dns: "1.1.1.1".to_string(), - storage_volume: String::new(), - vmconfig_map: HashMap::new(), - } - } -} - #[cfg(target_os = "macos")] fn check_case_sensitivity(volume: &str) -> Result { let first_path = format!("{}/krunvm_test", volume); @@ -167,7 +133,7 @@ enum Command { } fn main() { - let mut cfg: KrunvmConfig = confy::load(APP_NAME).unwrap(); + let mut cfg = config::load().unwrap(); let cli_args = Cli::parse(); #[cfg(target_os = "macos")] diff --git a/src/utils.rs b/src/utils.rs index 8b3d5b2..003f621 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,12 +1,13 @@ // Copyright 2021 Red Hat, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::APP_NAME; use std::collections::HashMap; use std::path::Path; use std::process::Command; use std::str::FromStr; -use crate::{KrunvmConfig, VmConfig, APP_NAME}; +use crate::config::{KrunvmConfig, VmConfig}; pub enum BuildahCommand { From,