diff --git a/virtweb_backend/src/actors/libvirt_actor.rs b/virtweb_backend/src/actors/libvirt_actor.rs index 75c488f..4a3eaa8 100644 --- a/virtweb_backend/src/actors/libvirt_actor.rs +++ b/virtweb_backend/src/actors/libvirt_actor.rs @@ -7,6 +7,7 @@ use crate::libvirt_rest_structures::hypervisor::*; use crate::libvirt_rest_structures::net::*; use crate::libvirt_rest_structures::nw_filter::{NetworkFilter, NetworkFilterName}; use crate::libvirt_rest_structures::vm::*; +use crate::nat::nat_lib; use actix::{Actor, Context, Handler, Message}; use image::ImageOutputFormat; use std::io::Cursor; @@ -395,6 +396,9 @@ impl Handler for LibVirtActor { let network = Network::define_xml(&self.m, &network_xml)?; let uuid = XMLUuid::parse_from_str(&network.get_uuid_string()?)?; + // Save NAT definition + nat_lib::save_nat_def(&msg.0)?; + // Save a copy of the source definition msg.0.uuid = Some(uuid); let json = serde_json::to_string(&msg.0)?; @@ -464,9 +468,12 @@ impl Handler for LibVirtActor { fn handle(&mut self, msg: DeleteNetwork, _ctx: &mut Self::Context) -> Self::Result { log::debug!("Delete network: {}\n", msg.0.as_string()); let network = Network::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; - let network_name = network.get_name()?; + let network_name = NetworkName(network.get_name()?); network.undefine()?; + // Remove NAT definition, if any + nat_lib::remove_nat_def(&network_name)?; + // Remove backup definition let backup_definition = AppConfig::get().net_definition_path(&network_name); if backup_definition.exists() { diff --git a/virtweb_backend/src/app_config.rs b/virtweb_backend/src/app_config.rs index 6ac8ab5..d76b4f5 100644 --- a/virtweb_backend/src/app_config.rs +++ b/virtweb_backend/src/app_config.rs @@ -1,4 +1,6 @@ +use crate::constants; use crate::libvirt_lib_structures::XMLUuid; +use crate::libvirt_rest_structures::net::NetworkName; use crate::libvirt_rest_structures::nw_filter::NetworkFilterName; use clap::Parser; use std::net::IpAddr; @@ -250,8 +252,16 @@ impl AppConfig { self.definitions_path().join(format!("vm-{name}.json")) } - pub fn net_definition_path(&self, name: &str) -> PathBuf { - self.definitions_path().join(format!("net-{name}.json")) + pub fn net_definition_path(&self, name: &NetworkName) -> PathBuf { + self.definitions_path().join(format!("net-{}.json", name.0)) + } + + pub fn nat_path(&self) -> PathBuf { + self.storage_path().join(constants::STORAGE_NAT_DIR) + } + + pub fn net_nat_path(&self, name: &NetworkName) -> PathBuf { + self.nat_path().join(format!("nat-{}.json", name.0)) } 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 9f22a79..4ba506d 100644 --- a/virtweb_backend/src/constants.rs +++ b/virtweb_backend/src/constants.rs @@ -77,3 +77,6 @@ pub const BUILTIN_NETWORK_FILTER_RULES: [&str; 24] = [ /// List of valid network chains pub const NETWORK_CHAINS: [&str; 8] = ["root", "mac", "stp", "vlan", "arp", "rarp", "ipv4", "ipv6"]; + +/// Directory where nat rules are stored, inside storage directory +pub const STORAGE_NAT_DIR: &str = "nat"; diff --git a/virtweb_backend/src/lib.rs b/virtweb_backend/src/lib.rs index 3d733aa..356a5e3 100644 --- a/virtweb_backend/src/lib.rs +++ b/virtweb_backend/src/lib.rs @@ -7,4 +7,5 @@ pub mod libvirt_client; pub mod libvirt_lib_structures; pub mod libvirt_rest_structures; pub mod middlewares; +pub mod nat; pub mod utils; diff --git a/virtweb_backend/src/libvirt_rest_structures/net.rs b/virtweb_backend/src/libvirt_rest_structures/net.rs index 7094cef..e7ebe5f 100644 --- a/virtweb_backend/src/libvirt_rest_structures/net.rs +++ b/virtweb_backend/src/libvirt_rest_structures/net.rs @@ -1,6 +1,8 @@ use crate::libvirt_lib_structures::network::*; use crate::libvirt_lib_structures::XMLUuid; use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; +use crate::nat::nat_definition::Nat; +use crate::nat::nat_lib; use crate::utils::net_utils::{extract_ipv4, extract_ipv6}; use ipnetwork::{Ipv4Network, Ipv6Network}; use lazy_regex::regex; @@ -21,57 +23,68 @@ pub struct DHCPv4HostReservation { #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct IPv4DHCPConfig { - start: Ipv4Addr, - end: Ipv4Addr, - hosts: Vec, + pub start: Ipv4Addr, + pub end: Ipv4Addr, + pub hosts: Vec, } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct IPV4Config { - bridge_address: Ipv4Addr, - prefix: u32, - dhcp: Option, + pub bridge_address: Ipv4Addr, + pub prefix: u32, + pub dhcp: Option, + pub nat: Option>>, } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct DHCPv6HostReservation { - name: String, - ip: Ipv6Addr, + pub name: String, + pub ip: Ipv6Addr, } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct IPv6DHCPConfig { - start: Ipv6Addr, - end: Ipv6Addr, - hosts: Vec, + pub start: Ipv6Addr, + pub end: Ipv6Addr, + pub hosts: Vec, } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct IPV6Config { - bridge_address: Ipv6Addr, - prefix: u32, - dhcp: Option, + pub bridge_address: Ipv6Addr, + pub prefix: u32, + pub dhcp: Option, + pub nat: Option>>, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct NetworkName(pub String); + +impl NetworkName { + pub fn is_valid(&self) -> bool { + regex!("^[a-zA-Z0-9]+$").is_match(&self.0) + } } /// Network configuration #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct NetworkInfo { - pub name: String, + pub name: NetworkName, pub uuid: Option, - title: Option, - description: Option, - forward_mode: NetworkForwardMode, - device: Option, - bridge_name: Option, - dns_server: Option, - domain: Option, - ip_v4: Option, - ip_v6: Option, + pub title: Option, + pub description: Option, + pub forward_mode: NetworkForwardMode, + pub device: Option, + pub bridge_name: Option, + pub dns_server: Option, + pub domain: Option, + pub ip_v4: Option, + pub ip_v6: Option, } impl NetworkInfo { pub fn as_virt_network(&self) -> anyhow::Result { - if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { + if !self.name.is_valid() { return Err(StructureExtraction("network name is invalid!").into()); } @@ -160,7 +173,7 @@ impl NetworkInfo { } Ok(NetworkXML { - name: self.name.to_string(), + name: self.name.0.to_string(), uuid: self.uuid, title: self.title.clone(), description: self.description.clone(), @@ -183,8 +196,12 @@ impl NetworkInfo { } pub fn from_xml(xml: NetworkXML) -> anyhow::Result { + let name = NetworkName(xml.name); + + let nat = nat_lib::load_nat_def(&name)?; + Ok(Self { - name: xml.name, + name, uuid: xml.uuid, title: xml.title, description: xml.description, @@ -227,6 +244,7 @@ impl NetworkInfo { }) .collect(), }), + nat: nat.ipv4, }), ip_v6: xml .ips @@ -252,7 +270,25 @@ impl NetworkInfo { }) .collect(), }), + nat: nat.ipv6, }), }) } + + /// Check if at least one NAT definition was specified on this interface + pub fn has_nat_def(&self) -> bool { + if let Some(ipv4) = &self.ip_v4 { + if ipv4.nat.is_some() { + return true; + } + } + + if let Some(ipv6) = &self.ip_v6 { + if ipv6.nat.is_some() { + return true; + } + } + + false + } } diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index e50b230..96d6907 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -41,6 +41,7 @@ async fn main() -> std::io::Result<()> { files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_path()).unwrap(); files_utils::set_file_permission(AppConfig::get().vnc_sockets_path(), 0o777).unwrap(); files_utils::create_directory_if_missing(AppConfig::get().disks_storage_path()).unwrap(); + files_utils::create_directory_if_missing(AppConfig::get().nat_path()).unwrap(); files_utils::create_directory_if_missing(AppConfig::get().definitions_path()).unwrap(); let conn = Data::new(LibVirtClient( diff --git a/virtweb_backend/src/nat/mod.rs b/virtweb_backend/src/nat/mod.rs new file mode 100644 index 0000000..5aa8739 --- /dev/null +++ b/virtweb_backend/src/nat/mod.rs @@ -0,0 +1,2 @@ +pub mod nat_definition; +pub mod nat_lib; diff --git a/virtweb_backend/src/nat/nat_definition.rs b/virtweb_backend/src/nat/nat_definition.rs new file mode 100644 index 0000000..f5f65b4 --- /dev/null +++ b/virtweb_backend/src/nat/nat_definition.rs @@ -0,0 +1,39 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum NatSource { + Interface { name: String }, + Ip { ip: IPv }, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum NatProtocol { + TCP, + UDP, + Both, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum NatHostPort { + Single { port: u16 }, + Range { start: u16, end: u16 }, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Nat { + pub protocol: NatProtocol, + pub host_addr: NatSource, + pub host_port: NatHostPort, + pub guest_addr: IPv, + pub guest_port: u16, + pub comment: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +pub struct NetNat { + pub interface: String, + pub ipv4: Option>>, + pub ipv6: Option>>, +} diff --git a/virtweb_backend/src/nat/nat_lib.rs b/virtweb_backend/src/nat/nat_lib.rs new file mode 100644 index 0000000..8ca6233 --- /dev/null +++ b/virtweb_backend/src/nat/nat_lib.rs @@ -0,0 +1,61 @@ +use crate::app_config::AppConfig; +use crate::libvirt_rest_structures::net::{NetworkInfo, NetworkName}; +use crate::nat::nat_definition::NetNat; + +#[derive(thiserror::Error, Debug)] +enum NatLibError { + #[error("Could not save nat definition, because network bridge name was not specified!")] + MissingNetworkBridgeName, +} + +/// Save nat definition +pub fn save_nat_def(net: &NetworkInfo) -> anyhow::Result<()> { + let nat = match net.has_nat_def() { + true => NetNat { + interface: net + .bridge_name + .as_ref() + .ok_or(NatLibError::MissingNetworkBridgeName)? + .to_string(), + ipv4: net + .ip_v4 + .as_ref() + .map(|i| i.nat.clone()) + .unwrap_or_default(), + ipv6: net + .ip_v6 + .as_ref() + .map(|i| i.nat.clone()) + .unwrap_or_default(), + }, + false => NetNat::default(), + }; + + let json = serde_json::to_string(&nat)?; + + std::fs::write(AppConfig::get().net_nat_path(&net.name), json)?; + + Ok(()) +} + +/// Remove nat definition, if existing +pub fn remove_nat_def(name: &NetworkName) -> anyhow::Result<()> { + let nat_file = AppConfig::get().net_nat_path(name); + if nat_file.exists() { + std::fs::remove_file(nat_file)?; + } + + Ok(()) +} + +/// Load nat definition, if available +pub fn load_nat_def(name: &NetworkName) -> anyhow::Result { + let nat_file = AppConfig::get().net_nat_path(name); + if !nat_file.exists() { + return Ok(NetNat::default()); + } + + let file = std::fs::read_to_string(nat_file)?; + + Ok(serde_json::from_str(&file)?) +}