diff --git a/virtweb_backend/src/constants.rs b/virtweb_backend/src/constants.rs index 95b95c5..060982c 100644 --- a/virtweb_backend/src/constants.rs +++ b/virtweb_backend/src/constants.rs @@ -43,3 +43,6 @@ pub const DISK_SIZE_MIN: usize = 100; /// Disk size max (MB) pub const DISK_SIZE_MAX: usize = 1000 * 1000 * 2; + +/// Network mac address default prefix +pub const NET_MAC_ADDR_PREFIX: &str = "52:54:00"; diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index 0f699e1..9849fbb 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -14,6 +14,7 @@ struct StaticConfig { local_auth_enabled: bool, oidc_auth_enabled: bool, iso_mimetypes: &'static [&'static str], + net_mac_prefix: &'static str, constraints: ServerConstraints, } @@ -42,6 +43,7 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { local_auth_enabled: *local_auth, oidc_auth_enabled: !AppConfig::get().disable_oidc, iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES, + net_mac_prefix: constants::NET_MAC_ADDR_PREFIX, constraints: ServerConstraints { iso_max_size: constants::ISO_MAX_SIZE, diff --git a/virtweb_backend/src/libvirt_lib_structures.rs b/virtweb_backend/src/libvirt_lib_structures.rs index b94daad..addadab 100644 --- a/virtweb_backend/src/libvirt_lib_structures.rs +++ b/virtweb_backend/src/libvirt_lib_structures.rs @@ -63,6 +63,13 @@ pub struct FeaturesXML { #[serde(rename = "acpi")] pub struct ACPIXML {} +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "mac")] +pub struct NetMacAddress { + #[serde(rename(serialize = "@address"))] + pub address: String, +} + #[derive(serde::Serialize, serde::Deserialize)] #[serde(rename = "source")] pub struct NetIntSourceXML { @@ -75,7 +82,7 @@ pub struct NetIntSourceXML { pub struct DomainNetInterfaceXML { #[serde(rename(serialize = "@type"))] pub r#type: String, - + pub mac: NetMacAddress, pub source: Option, } diff --git a/virtweb_backend/src/libvirt_rest_structures.rs b/virtweb_backend/src/libvirt_rest_structures.rs index f2d5534..67ba13c 100644 --- a/virtweb_backend/src/libvirt_rest_structures.rs +++ b/virtweb_backend/src/libvirt_rest_structures.rs @@ -3,10 +3,10 @@ use crate::constants; use crate::libvirt_lib_structures::{ DevicesXML, DiskBootXML, DiskDriverXML, DiskReadOnlyXML, DiskSourceXML, DiskTargetXML, DiskXML, DomainCPUTopology, DomainCPUXML, DomainInputXML, DomainMemoryXML, DomainNetInterfaceXML, - DomainVCPUXML, DomainXML, FeaturesXML, GraphicsXML, NetIntSourceXML, NetworkDHCPRangeXML, - NetworkDHCPXML, NetworkDNSForwarderXML, NetworkDNSXML, NetworkDomainXML, NetworkForwardXML, - NetworkIPXML, NetworkXML, OSLoaderXML, OSTypeXML, TPMBackendXML, TPMDeviceXML, VideoModelXML, - VideoXML, XMLUuid, ACPIXML, OSXML, + DomainVCPUXML, DomainXML, FeaturesXML, GraphicsXML, NetIntSourceXML, NetMacAddress, + NetworkDHCPRangeXML, NetworkDHCPXML, NetworkDNSForwarderXML, NetworkDNSXML, NetworkDomainXML, + NetworkForwardXML, NetworkIPXML, NetworkXML, OSLoaderXML, OSTypeXML, TPMBackendXML, + TPMDeviceXML, VideoModelXML, VideoXML, XMLUuid, ACPIXML, OSXML, }; use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; use crate::utils::disks_utils::Disk; @@ -65,9 +65,16 @@ pub enum VMArchitecture { X86_64, } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Network { + mac: String, + #[serde(flatten)] + r#type: NetworkType, +} + #[derive(serde::Serialize, serde::Deserialize)] #[serde(tag = "type")] -pub enum Network { +pub enum NetworkType { UserspaceSLIRPStack, DefinedNetwork { network: String }, // TODO : complete network types } @@ -235,12 +242,14 @@ impl VMInfo { let mut networks = vec![]; for n in self.networks { - networks.push(match n { - Network::UserspaceSLIRPStack => DomainNetInterfaceXML { + networks.push(match n.r#type { + NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML { + mac: NetMacAddress { address: n.mac }, r#type: "user".to_string(), source: None, }, - Network::DefinedNetwork { network } => DomainNetInterfaceXML { + NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML { + mac: NetMacAddress { address: n.mac }, r#type: "network".to_string(), source: Some(NetIntSourceXML { network }), }, @@ -382,14 +391,21 @@ impl VMInfo { .devices .net_interfaces .iter() - .map(|d| match d.r#type.as_str() { - "user" => Ok(Network::UserspaceSLIRPStack), - "network" => Ok(Network::DefinedNetwork { - network: d.source.as_ref().unwrap().network.to_string(), - }), - a => Err(LibVirtStructError::DomainExtraction(format!( - "Unknown network interface type: {a}! " - ))), + .map(|d| { + Ok(Network { + mac: d.mac.address.to_string(), + r#type: match d.r#type.as_str() { + "user" => NetworkType::UserspaceSLIRPStack, + "network" => NetworkType::DefinedNetwork { + network: d.source.as_ref().unwrap().network.to_string(), + }, + a => { + return Err(LibVirtStructError::DomainExtraction(format!( + "Unknown network interface type: {a}! " + ))); + } + }, + }) }) .collect::, _>>()?, diff --git a/virtweb_frontend/src/api/ServerApi.ts b/virtweb_frontend/src/api/ServerApi.ts index bba401e..07e6531 100644 --- a/virtweb_frontend/src/api/ServerApi.ts +++ b/virtweb_frontend/src/api/ServerApi.ts @@ -5,6 +5,7 @@ export interface ServerConfig { local_auth_enabled: boolean; oidc_auth_enabled: boolean; iso_mimetypes: string[]; + net_mac_prefix: string; constraints: ServerConstraints; } diff --git a/virtweb_frontend/src/api/VMApi.ts b/virtweb_frontend/src/api/VMApi.ts index 382d916..48655af 100644 --- a/virtweb_frontend/src/api/VMApi.ts +++ b/virtweb_frontend/src/api/VMApi.ts @@ -34,10 +34,12 @@ export type VMNetInterface = VMNetUserspaceSLIRPStack | VMNetDefinedNetwork; export interface VMNetUserspaceSLIRPStack { type: "UserspaceSLIRPStack"; + mac: string; } export interface VMNetDefinedNetwork { type: "DefinedNetwork"; + mac: string; network: string; } diff --git a/virtweb_frontend/src/utils/RandUtils.ts b/virtweb_frontend/src/utils/RandUtils.ts new file mode 100644 index 0000000..3c2c85c --- /dev/null +++ b/virtweb_frontend/src/utils/RandUtils.ts @@ -0,0 +1,11 @@ +/** + * Generate a random MAC address + */ +export function randomMacAddress(prefix: string | undefined): string { + let mac = "XX:XX:XX:XX:XX:XX"; + mac = prefix + mac.slice(prefix?.length); + + return mac.replace(/X/g, () => + "0123456789abcdef".charAt(Math.floor(Math.random() * 16)) + ); +} diff --git a/virtweb_frontend/src/widgets/forms/MACInput.tsx b/virtweb_frontend/src/widgets/forms/MACInput.tsx new file mode 100644 index 0000000..56bc5fa --- /dev/null +++ b/virtweb_frontend/src/widgets/forms/MACInput.tsx @@ -0,0 +1,46 @@ +import { TextInput } from "./TextInput"; + +export function MACInput(p: { + label: string; + editable: boolean; + value?: string; + onValueChange?: (newVal: string | undefined) => void; +}): React.ReactElement { + const { onValueChange, ...props } = p; + return ( + { + onValueChange?.(sanitizeMacAddress(v)); + }} + {...props} + /> + ); +} + +function sanitizeMacAddress(s: string | undefined): string | undefined { + if (s === "" || s === undefined) return s; + + const split = s.split(":"); + if (split.length > 6) split.splice(6); + + let needAnotherIteration = false; + + const res = split + .map((e) => { + if (e === "") return e; + + const num = parseInt(e, 16); + if (isNaN(num)) return "0"; + + let s = num.toString(16).padStart(2, "0"); + if (num > 0xff) { + needAnotherIteration = true; + return s.slice(0, 2) + ":" + s.slice(2); + } + + return s; + }) + .join(":"); + + return needAnotherIteration ? sanitizeMacAddress(res) : res; +} diff --git a/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx b/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx index 0b0fb7d..5774e31 100644 --- a/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx +++ b/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx @@ -14,6 +14,9 @@ import { VMInfo, VMNetInterface } from "../../api/VMApi"; import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; import { SelectInput } from "./SelectInput"; import { NetworkInfo } from "../../api/NetworksApi"; +import { randomMacAddress } from "../../utils/RandUtils"; +import { ServerApi } from "../../api/ServerApi"; +import { MACInput } from "./MACInput"; export function VMNetworksList(p: { vm: VMInfo; @@ -22,7 +25,10 @@ export function VMNetworksList(p: { networksList: NetworkInfo[]; }): React.ReactElement { const addNew = () => { - p.vm.networks.push({ type: "UserspaceSLIRPStack" }); + p.vm.networks.push({ + type: "UserspaceSLIRPStack", + mac: randomMacAddress(ServerApi.Config.net_mac_prefix), + }); p.onChange?.(); }; @@ -120,6 +126,16 @@ function NetworkInfoWidget(p: { />
+ { + p.network.mac = v!; + p.onChange?.(); + }} + /> + {p.network.type === "DefinedNetwork" && (