diff --git a/virtweb_backend/Cargo.lock b/virtweb_backend/Cargo.lock index 83c763a..1451da3 100644 --- a/virtweb_backend/Cargo.lock +++ b/virtweb_backend/Cargo.lock @@ -1618,6 +1618,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "ntapi" version = "0.4.1" @@ -2659,6 +2671,7 @@ dependencies = [ "light-openid", "log", "mime_guess", + "nix", "num", "quick-xml", "rand", diff --git a/virtweb_backend/Cargo.toml b/virtweb_backend/Cargo.toml index a5e7125..c671c7d 100644 --- a/virtweb_backend/Cargo.toml +++ b/virtweb_backend/Cargo.toml @@ -44,3 +44,4 @@ num = "0.4.1" rust-embed = { version = "8.1.0" } mime_guess = "2.0.4" dotenvy = "0.15.7" +nix = { version = "0.27.1", features = ["net"] } \ No newline at end of file diff --git a/virtweb_backend/src/app_config.rs b/virtweb_backend/src/app_config.rs index d76b4f5..77f74ab 100644 --- a/virtweb_backend/src/app_config.rs +++ b/virtweb_backend/src/app_config.rs @@ -261,7 +261,7 @@ impl AppConfig { } pub fn net_nat_path(&self, name: &NetworkName) -> PathBuf { - self.nat_path().join(format!("nat-{}.json", name.0)) + self.nat_path().join(name.nat_file_name()) } pub fn net_filter_definition_path(&self, name: &NetworkFilterName) -> PathBuf { diff --git a/virtweb_backend/src/constants.rs b/virtweb_backend/src/constants.rs index 3ad8bee..f026cc8 100644 --- a/virtweb_backend/src/constants.rs +++ b/virtweb_backend/src/constants.rs @@ -83,3 +83,6 @@ pub const NETWORK_CHAINS: [&str; 8] = ["root", "mac", "stp", "vlan", "arp", "rar /// Directory where nat rules are stored, inside storage directory pub const STORAGE_NAT_DIR: &str = "nat"; + +/// Environment variable that is set to run VirtWeb in NAT configuration mode +pub const NAT_MODE_ENV_VAR_NAME: &str = "NAT_MODE"; diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index 1dbeeb9..9c71db7 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -5,8 +5,9 @@ use crate::constants::{DISK_NAME_MAX_LEN, DISK_NAME_MIN_LEN, DISK_SIZE_MAX, DISK use crate::controllers::{HttpResult, LibVirtReq}; use crate::extractors::local_auth_extractor::LocalAuthEnabled; use crate::libvirt_rest_structures::hypervisor::HypervisorInfo; +use crate::utils::net_utils; use actix_web::{HttpResponse, Responder}; -use sysinfo::{NetworksExt, System, SystemExt}; +use sysinfo::{System, SystemExt}; #[derive(serde::Serialize)] struct StaticConfig { @@ -137,14 +138,5 @@ pub async fn number_vcpus() -> HttpResult { } pub async fn networks_list() -> HttpResult { - let mut system = System::new(); - system.refresh_networks_list(); - - Ok(HttpResponse::Ok().json( - system - .networks() - .iter() - .map(|n| n.0.to_string()) - .collect::>(), - )) + Ok(HttpResponse::Ok().json(net_utils::net_list())) } diff --git a/virtweb_backend/src/libvirt_rest_structures/net.rs b/virtweb_backend/src/libvirt_rest_structures/net.rs index dd7d92a..5a08ffd 100644 --- a/virtweb_backend/src/libvirt_rest_structures/net.rs +++ b/virtweb_backend/src/libvirt_rest_structures/net.rs @@ -61,6 +61,13 @@ pub struct IPV6Config { #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct NetworkName(pub String); +impl NetworkName { + /// Get the name of the file that will store the NAT configuration of this network + pub fn nat_file_name(&self) -> String { + format!("nat-{}.json", self.0) + } +} + impl NetworkName { pub fn is_valid(&self) -> bool { regex!("^[a-zA-Z0-9]+$").is_match(&self.0) diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 96d6907..5d940b0 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -27,12 +27,19 @@ use virtweb_backend::controllers::{ }; use virtweb_backend::libvirt_client::LibVirtClient; use virtweb_backend::middlewares::auth_middleware::AuthChecker; +use virtweb_backend::nat::nat_conf_mode; use virtweb_backend::utils::files_utils; #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + // Run in NAT configuration mode, if requested + if std::env::var(constants::NAT_MODE_ENV_VAR_NAME).is_ok() { + nat_conf_mode::sub_main().await.unwrap(); + return Ok(()); + } + // Load additional config from file, if requested AppConfig::parse_env_file().unwrap(); diff --git a/virtweb_backend/src/nat/mod.rs b/virtweb_backend/src/nat/mod.rs index 5aa8739..d604f47 100644 --- a/virtweb_backend/src/nat/mod.rs +++ b/virtweb_backend/src/nat/mod.rs @@ -1,2 +1,3 @@ +pub mod nat_conf_mode; pub mod nat_definition; pub mod nat_lib; diff --git a/virtweb_backend/src/nat/nat_conf_mode.rs b/virtweb_backend/src/nat/nat_conf_mode.rs new file mode 100644 index 0000000..0e0d30c --- /dev/null +++ b/virtweb_backend/src/nat/nat_conf_mode.rs @@ -0,0 +1,79 @@ +use crate::constants; +use crate::libvirt_rest_structures::net::NetworkName; +use crate::nat::nat_definition::NetNat; +use crate::utils::net_utils; +use clap::Parser; +use std::collections::HashMap; +use std::net::IpAddr; +use std::path::{Path, PathBuf}; + +/// VirtWeb NAT configuration mode. This executable should never be executed manually +#[derive(Parser, Debug, Clone)] +#[clap(author, version, about, long_about = None)] +struct NatArgs { + /// Storage directory + #[clap(short, long)] + storage: String, + + /// Network name + #[clap(short, long)] + network_name: String, + + /// Operation + #[clap(short, long)] + operation: String, + + /// Sub operation + #[clap(long)] + sub_operation: String, +} + +impl NatArgs { + pub fn network_file(&self) -> PathBuf { + let network_name = NetworkName(self.network_name.to_string()); + Path::new(&self.storage) + .join(constants::STORAGE_NAT_DIR) + .join(network_name.nat_file_name()) + } +} + +/// NAT sub main function +pub async fn sub_main() -> anyhow::Result<()> { + let args = NatArgs::parse(); + + if !args.network_file().exists() { + log::warn!("Cannot do anything for the network, because the NAT configuration file does not exixsts!"); + return Ok(()); + } + + let conf_json = std::fs::read_to_string(args.network_file())?; + let conf: NetNat = serde_json::from_str(&conf_json)?; + + let ips = net_utils::net_list_and_ips()?; + + match (args.operation.as_str(), args.sub_operation.as_str()) { + ("started", "begin") => network_started_begin(&conf, &ips).await?, + ("stopped", "end") => network_stopped_end(&conf, &ips).await?, + _ => log::debug!( + "Operation {} - {} not supported", + args.operation, + args.sub_operation + ), + } + + Ok(()) +} + +pub async fn network_started_begin( + conf: &NetNat, + ips: &HashMap>, +) -> anyhow::Result<()> { + todo!() +} + +pub async fn network_stopped_end( + conf: &NetNat, + ips: &HashMap>, +) -> anyhow::Result<()> { + todo!() +} diff --git a/virtweb_backend/src/utils/net_utils.rs b/virtweb_backend/src/utils/net_utils.rs index 1ada2e0..bb90879 100644 --- a/virtweb_backend/src/utils/net_utils.rs +++ b/virtweb_backend/src/utils/net_utils.rs @@ -1,5 +1,8 @@ +use nix::sys::socket::{AddressFamily, SockaddrLike}; +use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::str::FromStr; +use sysinfo::{NetworksExt, System, SystemExt}; pub fn extract_ipv4(ip: IpAddr) -> Ipv4Addr { match ip { @@ -51,6 +54,88 @@ pub fn is_net_interface_name_valid>(int: D) -> bool { lazy_regex::regex!("^[a-zA-Z0-9]+$").is_match(int.as_ref()) } +/// Get the list of available network interfaces +pub fn net_list() -> Vec { + let mut system = System::new(); + system.refresh_networks_list(); + + system + .networks() + .iter() + .map(|n| n.0.to_string()) + .collect::>() +} + +/// Get the list of available network interfaces associated with their IP address +pub fn net_list_and_ips() -> anyhow::Result>> { + let addrs = nix::ifaddrs::getifaddrs().unwrap(); + + let mut res = HashMap::new(); + + for ifaddr in addrs { + let address = match ifaddr.address { + Some(address) => address, + None => { + log::debug!( + "Interface {} has an unsupported address family", + ifaddr.interface_name + ); + continue; + } + }; + + let addr_str = match address.family() { + Some(AddressFamily::Inet) => { + let address = address.to_string(); + address + .split_once(':') + .map(|a| a.0) + .unwrap_or(&address) + .to_string() + } + Some(AddressFamily::Inet6) => { + let address = address.to_string(); + let address = address + .split_once(']') + .map(|a| a.0) + .unwrap_or(&address) + .to_string(); + + let address = address + .split_once('%') + .map(|a| a.0) + .unwrap_or(&address) + .to_string(); + + address.strip_prefix('[').unwrap_or(&address).to_string() + } + _ => { + log::debug!( + "Interface {} has an unsupported address family {:?}", + ifaddr.interface_name, + address.family() + ); + continue; + } + }; + + log::debug!( + "Process ip {addr_str} for interface {}", + ifaddr.interface_name + ); + + let ip = IpAddr::from_str(&addr_str)?; + + if !res.contains_key(&ifaddr.interface_name) { + res.insert(ifaddr.interface_name.to_string(), Vec::with_capacity(1)); + } + + res.get_mut(&ifaddr.interface_name).unwrap().push(ip); + } + + Ok(res) +} + #[cfg(test)] mod tests { use crate::utils::net_utils::{