);
-
-#[cfg(test)]
-mod tests {
- use std::str::FromStr;
-
- use super::*;
-
- #[test]
- fn test_network_interface_type_display() {
- assert_eq!(NetworkInterfaceType::IoEmu.to_string(), "ioemu");
- assert_eq!(NetworkInterfaceType::Vif.to_string(), "vif");
- }
-
- #[test]
- fn test_network_interface_model_display() {
- assert_eq!(NetworkInterfaceModel::Rtl8139.to_string(), "rtl8139");
- assert_eq!(NetworkInterfaceModel::E1000.to_string(), "e1000");
- assert_eq!(
- NetworkInterfaceModel::AnySupported("model".to_string()).to_string(),
- "model"
- );
- }
-
- #[test]
- fn test_network_interface_display() {
- let network_interface = NetworkInterface {
- name: "vif0.0".to_string(),
- mac: MacAddress::from_str("00:16:3e:00:00:00").unwrap(),
- bridge: "xenbr0".to_string(),
- gateway_device: "eth0".to_string(),
- r#type: NetworkInterfaceType::IoEmu,
- model: Some(NetworkInterfaceModel::Rtl8139),
- };
- assert_eq!(
- network_interface.to_string(),
- "mac=00:16:3E:00:00:00, bridge=xenbr0, gatewaydev=eth0, type=ioemu, model=rtl8139"
- );
- }
-}
diff --git a/xenith-domain-management/src/domain/processor.rs b/xenith-domain-management/src/domain/processor.rs
deleted file mode 100644
index 82c1a8c..0000000
--- a/xenith-domain-management/src/domain/processor.rs
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
-Xenith - Xen-based security hypervisor
-Copyright (C) 2025-2026 Xenith contributors
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-//! Processor and platforms configuration structures and options for a domain.
-//!
-//!
-//!
-//! You could wonder why we don't directly specify CPUID values in the domain configuration as
-//! xl.cfg allows to do. The reason is that xl, when specifying cpuid for hypervisor leaves
-//! (0x4000xxxx major group) only the lowest 8 bits of leaf's 0x4000xx00 EAX register are processed,
-//! the rest are ignored (these 8 bits signify maximum number of hypervisor leaves).
-//!
-//! This is an important limitation, as we need to hook the hypervisor leaves to hide the fact that
-//! the CPU is running in a virtual machine. To do this, we need to specify the full leaf value.
-//!
-//! If this changes, we could add it back with the `Xend` format, as the `Libxl` format does not allow
-//! to specify the full leaf value.
-//!
-//!
-
-use std::fmt::Display;
-
-use serde::{Deserialize, Serialize};
-
-/// Represents the SMBIOS information for a domain
-#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
-pub struct SmBios {
- pub bios_vendor: Option,
- pub bios_version: Option,
- pub system_manufacturer: Option,
- pub system_product_name: Option,
- pub system_version: Option,
- pub system_serial_number: Option,
- pub baseboard_manufacturer: Option,
- pub baseboard_product_name: Option,
- pub baseboard_version: Option,
- pub baseboard_serial_number: Option,
- pub baseboard_asset_tag: Option,
- pub baseboard_location_in_chassis: Option,
- pub enclosure_manufacturer: Option,
- pub enclosure_version: Option,
- pub enclosure_serial_number: Option,
- pub enclosure_asset_tag: Option,
- pub battery_manufacturer: Option,
- pub battery_device_name: Option,
- pub oems: Option>,
-}
-
-impl Display for SmBios {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let fields = [
- ("bios_vendor", &self.bios_vendor),
- ("bios_version", &self.bios_version),
- ("system_manufacturer", &self.system_manufacturer),
- ("system_product_name", &self.system_product_name),
- ("system_version", &self.system_version),
- ("system_serial_number", &self.system_serial_number),
- ("baseboard_manufacturer", &self.baseboard_manufacturer),
- ("baseboard_product_name", &self.baseboard_product_name),
- ("baseboard_version", &self.baseboard_version),
- ("baseboard_serial_number", &self.baseboard_serial_number),
- ("baseboard_asset_tag", &self.baseboard_asset_tag),
- (
- "baseboard_location_in_chassis",
- &self.baseboard_location_in_chassis,
- ),
- ("enclosure_manufacturer", &self.enclosure_manufacturer),
- ("enclosure_version", &self.enclosure_version),
- ("enclosure_serial_number", &self.enclosure_serial_number),
- ("enclosure_asset_tag", &self.enclosure_asset_tag),
- ("battery_manufacturer", &self.battery_manufacturer),
- ("battery_device_name", &self.battery_device_name),
- ];
-
- // oem is a special case, as it is a list of "oem=value" pairs
- // for example, if oems = ["Xenith", "Xenith VM"], then the string
- // representation should be "oem=Xenith, oem=Xenith VM"
- let mut oems_str = String::new();
- for oem in self.oems.iter().flatten() {
- oems_str.push_str(&format!("oem={}, ", oem));
- }
- oems_str.pop();
- oems_str.pop();
-
- let mut smbios_str = fields
- .iter()
- .filter_map(|(name, value)| value.as_deref().map(|v| format!("{}={}", name, v)))
- .collect::>();
-
- smbios_str.push(oems_str);
-
- if smbios_str.is_empty() {
- write!(f, "")
- } else {
- write!(f, "{}", smbios_str.join(", "))
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_smbios_display() {
- let smbios = SmBios {
- bios_vendor: Some("Xenith".to_string()),
- bios_version: Some("1.0".to_string()),
- system_manufacturer: Some("Xenith".to_string()),
- system_product_name: Some("Xenith VM".to_string()),
- system_version: Some("1.0".to_string()),
- system_serial_number: Some("123".to_string()),
- baseboard_manufacturer: Some("Xenith".to_string()),
- baseboard_product_name: Some("Xenith VM".to_string()),
- baseboard_version: Some("1.0".to_string()),
- baseboard_serial_number: Some("123".to_string()),
- baseboard_asset_tag: Some("123".to_string()),
- baseboard_location_in_chassis: Some("123".to_string()),
- enclosure_manufacturer: Some("Xenith".to_string()),
- enclosure_version: Some("1.0".to_string()),
- enclosure_serial_number: Some("123".to_string()),
- enclosure_asset_tag: Some("123".to_string()),
- battery_manufacturer: Some("Xenith".to_string()),
- battery_device_name: Some("Xenith VM".to_string()),
- oems: Some(vec!["Xenith".to_string(), "Xenith VM".to_string()]),
- };
-
- assert_eq!(
- smbios.to_string(),
- "bios_vendor=Xenith, bios_version=1.0, system_manufacturer=Xenith, system_product_name=Xenith VM, system_version=1.0, system_serial_number=123, baseboard_manufacturer=Xenith, baseboard_product_name=Xenith VM, baseboard_version=1.0, baseboard_serial_number=123, baseboard_asset_tag=123, baseboard_location_in_chassis=123, enclosure_manufacturer=Xenith, enclosure_version=1.0, enclosure_serial_number=123, enclosure_asset_tag=123, battery_manufacturer=Xenith, battery_device_name=Xenith VM, oem=Xenith, oem=Xenith VM"
- );
- }
-}
diff --git a/xenith-domain-management/src/domain/time.rs b/xenith-domain-management/src/domain/time.rs
deleted file mode 100644
index fc5d431..0000000
--- a/xenith-domain-management/src/domain/time.rs
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
-Xenith - Xen-based security hypervisor
-Copyright (C) 2025-2026 Xenith contributors
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-//! Time configuration structures and options for a domain.
-
-use std::fmt::Display;
-
-use serde::{Deserialize, Serialize};
-
-/// Represents the mode of the Time Stamp Counter (TSC) for a domain
-///
-/// See `man 7 xen-tscmode` for more information.
-#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum TimeStampCounterMode {
- /// Guest rdtsc/p is executed natively when monotonicity can be guaranteed and
- /// emulated otherwise (with frequency scaled if necessary).
- ///
- /// If a HVM container in default TSC mode is created on a host that provides
- /// constant host TSC, its guest TSC frequency will be the same as the host. If
- /// it is later migrated to another host that provide constant host TSC and
- /// supports Intel VMX TSC scaling/AMD SVM TSC ratio, its guest TSC frequency
- /// will be the same before and after migration, and guest rdtsc/p will be
- /// executed natively after migration as well
- #[default]
- Default,
- /// Guest rdtsc/p is always emulated and the virtual TSC will appear to increment
- /// (kernel and user) at a fixed 1GHz rate, regardless of the pCPU HZ rate or
- /// power state. Although there is an overhead associated with emulation, this
- /// will NOT affect underlying CPU performance.
- AlwaysEmulate,
- /// Guest rdtsc/p is always executed natively (no monotonicity/frequency
- /// guarantees). Guest rdtsc/p is emulated at native frequency if unsupported by
- /// h/w, else executed natively.
- Native,
-}
-
-impl Display for TimeStampCounterMode {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- TimeStampCounterMode::Default => write!(f, "default"),
- TimeStampCounterMode::AlwaysEmulate => write!(f, "always_emulate"),
- TimeStampCounterMode::Native => write!(f, "native"),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_tsc_mode_display() {
- assert_eq!(format!("{}", TimeStampCounterMode::Default), "default");
- assert_eq!(
- format!("{}", TimeStampCounterMode::AlwaysEmulate),
- "always_emulate"
- );
- assert_eq!(format!("{}", TimeStampCounterMode::Native), "native");
- }
-}
diff --git a/xenith-domain-management/src/driver.rs b/xenith-domain-management/src/driver.rs
deleted file mode 100644
index 2e48fc8..0000000
--- a/xenith-domain-management/src/driver.rs
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
-Xenith - Xen-based security hypervisor
-Copyright (C) 2025-2026 Xenith contributors
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-mod identifier;
-mod status;
-
-pub use identifier::{DomainId, DomainIdentifier, DomainName};
-pub use status::Status;
-
-use log::{debug, info};
-use virt::connect::Connect;
-use virt::domain::Domain;
-use virt::error::clear_error_callback;
-
-use crate::configuration::Configuration;
-use crate::error::DriverError;
-
-#[derive(Debug, Clone)]
-pub struct Driver {
- connection: Connect,
- configuration: Configuration,
-}
-
-impl Driver {
- const XEN_URI: &'static str = "xen:///system";
-
- pub fn new() -> Result {
- // Initialize the libvirt connection and
- // disable printing errors to stdout
- debug!("Initializing libvirt connection");
- let connection = Connect::open(Some(Driver::XEN_URI)).map_err(DriverError::Connection)?;
-
- clear_error_callback();
-
- // Initialize the state and configuration
- let configuration = Configuration::new();
-
- debug!("Creating configuration files");
- configuration.create_configuration()?;
-
- info!("Driver initialized successfully");
- Ok(Driver {
- connection,
- configuration,
- })
- }
-
- pub fn get_connection(&self) -> &Connect {
- &self.connection
- }
-
- pub fn get_configuration(&self) -> &Configuration {
- &self.configuration
- }
-
- pub fn get_domain_by_id(&self, domain_id: DomainId) -> Result {
- let domain = Domain::lookup_by_id(&self.connection, domain_id)
- .map_err(DriverError::DomainNotFound)?;
-
- Ok(domain)
- }
-
- pub fn get_domain_by_name(&self, domain_name: &DomainName) -> Result {
- let domain = Domain::lookup_by_name(&self.connection, domain_name)
- .map_err(DriverError::DomainNotFound)?;
-
- Ok(domain)
- }
-
- pub fn get_domain(&self, domain_identifier: DomainIdentifier) -> Result {
- match domain_identifier {
- DomainIdentifier::Name(name) => self.get_domain_by_name(&name),
- DomainIdentifier::Id(id) => self.get_domain_by_id(id),
- }
- }
-
- /// Get the domain ID by its name
- /// Returns the domain ID if found, or an error if not
- /// running or not found
- ///
- /// # Arguments
- ///
- /// * `domain_name` - The name of the domain to look up
- ///
- /// # Returns
- ///
- /// * `Result` - The domain ID if found, or an error
- pub fn get_domain_id(&self, domain_name: &DomainName) -> Result {
- let domain = self.get_domain_by_name(domain_name)?;
-
- let domain_id = domain.get_id().ok_or(DriverError::DomainNotRunning)?;
- Ok(domain_id)
- }
-
- /// Get the domain name by its ID
- /// Returns the domain name if found, or an error if not
- /// running or not found
- ///
- /// # Arguments
- ///
- /// * `domain_id` - The ID of the domain to look up
- ///
- /// # Returns
- ///
- /// * `Result` - The domain name if found, or an error
- pub fn get_domain_name(&self, domain_id: DomainId) -> Result {
- let domain = self.get_domain_by_id(domain_id)?;
-
- let domain_name = domain
- .get_name()
- .map_err(|_| DriverError::DomainWithoutName)?;
-
- if domain_name.is_empty() {
- return Err(DriverError::DomainWithoutName);
- }
-
- Ok(domain_name)
- }
-
- /// Get the domain status by its identifier
- /// Returns the domain status if found, or an error if not
- /// running or not found
- ///
- /// # Arguments
- ///
- /// * `domain_id` - The ID of the domain to look up
- ///
- /// # Returns
- ///
- /// * `Result` - The domain status if found, or an error
- pub fn get_domain_status(
- &self,
- domain_identifier: DomainIdentifier,
- ) -> Result {
- let domain = self.get_domain(domain_identifier)?;
-
- let state = domain
- .get_state()
- .map_err(|_| DriverError::DomainNotRunning)?;
-
- let status = state.0;
-
- Ok(Status::from(status))
- }
-}
diff --git a/xenith-domain-management/src/driver/identifier.rs b/xenith-domain-management/src/driver/identifier.rs
deleted file mode 100644
index a63526e..0000000
--- a/xenith-domain-management/src/driver/identifier.rs
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
-Xenith - Xen-based security hypervisor
-Copyright (C) 2025-2026 Xenith contributors
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-use std::fmt::Display;
-
-use serde::{Deserialize, Serialize};
-
-/// Represents a domain name, which is a string.
-pub type DomainName = String;
-/// Represents a domain ID, which is a 32-bit unsigned integer.
-pub type DomainId = u32;
-
-/// Represents a domain identifier, which can be either a name or an ID.
-/// This is used to identify a domain in the libvirt storage.
-///
-/// This is useful for operations on domains, allowing using both
-/// domain names and IDs interchangeably.
-///
-/// **Important**: libvirt doesn't attribute a domain ID to a stopped domain.
-#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
-pub enum DomainIdentifier {
- /// Domain name
- /// This is the default variant.
- Name(DomainName),
- /// Domain ID
- Id(DomainId),
-}
-
-impl From for DomainIdentifier {
- fn from(name: DomainName) -> Self {
- DomainIdentifier::Name(name)
- }
-}
-
-impl From for DomainIdentifier {
- fn from(id: DomainId) -> Self {
- DomainIdentifier::Id(id)
- }
-}
-
-impl Default for DomainIdentifier {
- fn default() -> Self {
- DomainIdentifier::Id(0)
- }
-}
-
-impl Display for DomainIdentifier {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- DomainIdentifier::Name(name) => write!(f, "{}", name),
- DomainIdentifier::Id(id) => write!(f, "{}", id),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_domain_identifier_from_name() {
- let name = "example-domain".to_string();
- let identifier: DomainIdentifier = name.clone().into();
- assert_eq!(identifier, DomainIdentifier::Name(name));
- }
-
- #[test]
- fn test_domain_identifier_from_id() {
- let id = 42;
- let identifier: DomainIdentifier = id.into();
- assert_eq!(identifier, DomainIdentifier::Id(id));
- }
-
- #[test]
- fn test_domain_identifier_default() {
- let default_identifier = DomainIdentifier::default();
- assert_eq!(default_identifier, DomainIdentifier::Id(0));
- }
-
- #[test]
- fn test_domain_identifier_display_name() {
- let identifier = DomainIdentifier::Name("example-domain".to_string());
- assert_eq!(identifier.to_string(), "example-domain");
- }
-
- #[test]
- fn test_domain_identifier_display_id() {
- let identifier = DomainIdentifier::Id(42);
- assert_eq!(identifier.to_string(), "42");
- }
-
- #[test]
- fn test_domain_identifier_equality() {
- let id1 = DomainIdentifier::Id(1);
- let id2 = DomainIdentifier::Id(1);
- let name1 = DomainIdentifier::Name("domain".to_string());
- let name2 = DomainIdentifier::Name("domain".to_string());
- assert_eq!(id1, id2);
- assert_eq!(name1, name2);
- assert_ne!(id1, name1);
- }
-
- #[test]
- fn test_domain_identifier_ordering() {
- let id1 = DomainIdentifier::Id(1);
- let id2 = DomainIdentifier::Id(2);
- let name1 = DomainIdentifier::Name("alpha".to_string());
- let name2 = DomainIdentifier::Name("beta".to_string());
- assert!(id1 < id2);
- assert!(name1 < name2);
- }
-}
diff --git a/xenith-domain-management/src/driver/status.rs b/xenith-domain-management/src/driver/status.rs
deleted file mode 100644
index 9a7566a..0000000
--- a/xenith-domain-management/src/driver/status.rs
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
-Xenith - Xen-based security hypervisor
-Copyright (C) 2025-2026 Xenith contributors
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-use serde::{Deserialize, Serialize};
-use virt_sys::{
- VIR_DOMAIN_BLOCKED, VIR_DOMAIN_CRASHED, VIR_DOMAIN_NOSTATE, VIR_DOMAIN_PAUSED,
- VIR_DOMAIN_PMSUSPENDED, VIR_DOMAIN_RUNNING, VIR_DOMAIN_SHUTDOWN, VIR_DOMAIN_SHUTOFF,
- virDomainState,
-};
-
-/// Represents the status of a domain from libvirt.
-#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
-pub enum Status {
- #[default]
- NoState,
- Running,
- Blocked,
- Paused,
- Shutdown,
- Shutoff,
- Crashed,
- PMSuspended,
-}
-
-impl From for Status {
- fn from(state: virDomainState) -> Self {
- match state {
- VIR_DOMAIN_NOSTATE => Status::NoState,
- VIR_DOMAIN_RUNNING => Status::Running,
- VIR_DOMAIN_BLOCKED => Status::Blocked,
- VIR_DOMAIN_PAUSED => Status::Paused,
- VIR_DOMAIN_SHUTDOWN => Status::Shutdown,
- VIR_DOMAIN_SHUTOFF => Status::Shutoff,
- VIR_DOMAIN_CRASHED => Status::Crashed,
- VIR_DOMAIN_PMSUSPENDED => Status::PMSuspended,
- _ => panic!("Unsupported domain state: {}", state),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use virt_sys::{
- VIR_DOMAIN_BLOCKED, VIR_DOMAIN_CRASHED, VIR_DOMAIN_NOSTATE, VIR_DOMAIN_PAUSED,
- VIR_DOMAIN_PMSUSPENDED, VIR_DOMAIN_RUNNING, VIR_DOMAIN_SHUTDOWN, VIR_DOMAIN_SHUTOFF,
- };
-
- #[test]
- fn test_status_from_vir_domain_state() {
- assert_eq!(Status::from(VIR_DOMAIN_NOSTATE), Status::NoState);
- assert_eq!(Status::from(VIR_DOMAIN_RUNNING), Status::Running);
- assert_eq!(Status::from(VIR_DOMAIN_BLOCKED), Status::Blocked);
- assert_eq!(Status::from(VIR_DOMAIN_PAUSED), Status::Paused);
- assert_eq!(Status::from(VIR_DOMAIN_SHUTDOWN), Status::Shutdown);
- assert_eq!(Status::from(VIR_DOMAIN_SHUTOFF), Status::Shutoff);
- assert_eq!(Status::from(VIR_DOMAIN_CRASHED), Status::Crashed);
- assert_eq!(Status::from(VIR_DOMAIN_PMSUSPENDED), Status::PMSuspended);
- }
-
- #[test]
- #[should_panic(expected = "Unsupported domain state")]
- fn test_status_from_unsupported_state() {
- #[allow(unused_must_use)]
- Status::from(9999); // Invalid state
- }
-
- #[test]
- fn test_status_equality() {
- assert_eq!(Status::Running, Status::Running);
- assert_ne!(Status::Running, Status::Paused);
- }
-
- #[test]
- fn test_status_ordering() {
- assert!(Status::NoState < Status::Running);
- assert!(Status::Paused > Status::Blocked);
- }
-
- #[test]
- fn test_status_debug_format() {
- let status = Status::Running;
- assert_eq!(format!("{:?}", status), "Running");
- }
-
- #[test]
- fn test_status_default() {
- let default_status: Status = Default::default();
- assert_eq!(default_status, Status::NoState);
- }
-}
diff --git a/xenith-domain-management/src/error.rs b/xenith-domain-management/src/error.rs
deleted file mode 100644
index 68024aa..0000000
--- a/xenith-domain-management/src/error.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
-Xenith - Xen-based security hypervisor
-Copyright (C) 2025-2026 Xenith contributors
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-use std::io::Error as IoError;
-use thiserror::Error;
-use virt::error::Error as LibvirtError;
-
-#[derive(Error, Debug)]
-pub enum DriverError {
- #[error("Connection error")]
- Connection(#[source] LibvirtError),
- #[error("Configuration error")]
- Configuration(#[from] ConfigurationError),
- #[error("Domain name doesn't exist")]
- DomainNotFound(#[source] LibvirtError),
- #[error("Domain not running")]
- DomainNotRunning,
- #[error("Domain without name")]
- DomainWithoutName,
- #[error("Unknown driver error")]
- Unknown,
-}
-
-#[derive(Error, Debug)]
-pub enum ConfigurationError {
- #[error("Can't create configuration files")]
- Creation(#[source] IoError),
- #[error("Can't parse configuration files")]
- Parsing(#[source] IoError),
- #[error("Unknown configuration error")]
- Unknown,
-}
diff --git a/xenith-domain-management/src/lib.rs b/xenith-domain-management/src/lib.rs
deleted file mode 100644
index d262290..0000000
--- a/xenith-domain-management/src/lib.rs
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
-Xenith - Xen-based security hypervisor
-Copyright (C) 2025-2026 Xenith contributors
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-//! Xenith VM management library
-//!
-//! This library provides a high-level interface for managing Xenith domains.
-//! Inspired by [xen-tools](https://github.com/xen-tools/xen-tools), it
-//! aims to provide a simple and easy-to-use interface for managing domains
-//! on Xenith.
-//!
-//! ## Features
-//!
-//! - Creation : create a new VM
-//! - Deletion : delete an existing VM
-//! - Start : start a VM
-//! - Stop : stop a VM
-//! - Pause : pause a VM
-//! - Continue : continue a paused VM
-//! - Snapshot : create a snapshot of a VM state
-//! - Restore : restore a VM to a previous snapshot
-//!
-//! ## Supported operating systems
-//!
-//! We aim to support at least the following operating systems:
-//! - Debian 12 (Bookworm)
-//! - Ubuntu 24.04 (Noble Numbat)
-//! - Windows 10
-//! - Windows 11
-//!
-//! It is to note that all operating systems images are built using [Hashicorp Packer](https://developer.hashicorp.com/packer).
-//! This is to ensure that the images are built in a reproducible way and allows you to save setup time.
-
-pub mod configuration;
-pub mod domain;
-pub mod driver;
-pub mod error;
-pub mod templating;
diff --git a/xenith-domain-management/src/main.rs b/xenith-domain-management/src/main.rs
deleted file mode 100644
index 867fe96..0000000
--- a/xenith-domain-management/src/main.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-use virt::connect::Connect;
-use virt::domain::Domain;
-
-fn main() {
- let conn = match Connect::open(Some("xen:///system")) {
- Ok(c) => c,
- Err(e) => panic!("Unable to connect: {}", e),
- };
-
- let domains = conn.list_domains().unwrap();
-
- for domain in &domains {
- println!("Domain: {}", domain);
- }
-
- let domain = Domain::lookup_by_id(&conn, domains[0]).expect("Unable to lookup domain");
-
- println!("Domain name: {}", domain.get_name().unwrap());
- println!("Domain ID: {}", domain.get_id().unwrap());
-
- let xml = domain.get_xml_desc(0).unwrap();
- println!("Domain XML: \n{}", xml);
-}
diff --git a/xenith-domain-management/src/templating.rs b/xenith-domain-management/src/templating.rs
deleted file mode 100644
index b28c912..0000000
--- a/xenith-domain-management/src/templating.rs
+++ /dev/null
@@ -1,228 +0,0 @@
-/*
-Xenith - Xen-based security hypervisor
-Copyright (C) 2025-2026 Xenith contributors
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-//! Xenith domain configuration templating
-
-use crate::domain::Domain;
-
-use tera::{Context, Tera};
-
-/// Domain configuration templating
-///
-/// This struct is used to generate a domain configuration file from a [`Domain`] object
-#[derive(Debug, Clone, Default)]
-pub struct DomainTemplate {
- tera: Tera,
- context: Context,
-}
-
-impl DomainTemplate {
- pub const DEFAULT_CONFIG_TEMPLATE: &str = "templates/default.xml";
-
- /// Create a new [`Tera`] domain template with a custom template
- ///
- /// # Arguments
- ///
- /// * `domain` - The Xenith [`Domain`] to be templated
- /// * `template` - The path to the custom template file
- /// * `name` - The name of the template
- ///
- /// # Returns
- ///
- /// A [`Result`] containing the [`DomainTemplate`] if successful, or a [`tera::Error`] if not
- pub fn new_with_template(
- domain: Domain,
- template: &str,
- name: Option<&str>,
- ) -> Result {
- let mut tera = Tera::default();
- tera.add_template_file(template, name)?;
-
- // Disable autoescaping for all templates
- // This is necessary for XML templates to prevent escaping of special characters
- // such as <, >, & and /.
- tera.autoescape_on(vec![]);
-
- let mut context = Context::new();
- context.insert("domain", &domain);
-
- Ok(Self { tera, context })
- }
-
- /// Create a new default [`Tera`] domain template
- ///
- /// # Arguments
- ///
- /// * `domain` - The Xenith [`Domain`] to be templated
- ///
- /// # Returns
- ///
- /// A [`Result`] containing the [`DomainTemplate`] if successful, or a [`tera::Error`] if not
- pub fn new(domain: Domain) -> Result {
- DomainTemplate::new_with_template(domain, DomainTemplate::DEFAULT_CONFIG_TEMPLATE, None)
- }
-
- /// Render the domain configuration template
- ///
- /// # Returns
- ///
- /// A [`Result`] containing the rendered domain configuration as a [`String`] if successful, or a [`tera::Error`] if not
- pub fn render(&self) -> Result {
- self.tera
- .render(DomainTemplate::DEFAULT_CONFIG_TEMPLATE, &self.context)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::path::PathBuf;
- use std::str::FromStr;
-
- use super::*;
- use crate::domain::*;
-
- #[test]
- fn test_default_template() {
- let template_path = PathBuf::from(DomainTemplate::DEFAULT_CONFIG_TEMPLATE);
- assert!(template_path.exists(), "Template file does not exist");
- assert!(template_path.is_file(), "Template path is not a file");
- assert!(
- template_path
- .extension()
- .map(|ext| ext == "xml")
- .unwrap_or(false),
- "Template file is not an XML file"
- );
- assert!(
- template_path
- .file_name()
- .map(|name| name == "default.xml")
- .unwrap_or(false),
- "Template file is not named default.xml"
- );
- }
-
- #[test]
- fn test_domain_template() -> Result<(), tera::Error> {
- // Create a realist domain configuration
- let name = DomainName("Xenith".to_string());
- let r#type = DomainType::Hvm;
- let memory = MemoryCapacity(8000); // 8GB
- let maximum_memory = MaximumMemoryCapacity(10000); // 16GB
- let boot_devices = BootDevices(vec![
- BootDevice::HardDisk,
- BootDevice::CdRom,
- BootDevice::Network,
- ]);
- let disks = DiskDevices(vec![
- Disk {
- target: PathBuf::from("/dev/sda"),
- format: DiskFormat::Qcow2,
- access: DiskAccess::ReadWrite,
- virtual_device: "xvda".to_string(),
- },
- Disk {
- target: PathBuf::from("/dev/sdb"),
- format: DiskFormat::Raw,
- access: DiskAccess::ReadOnly,
- virtual_device: "xvdb".to_string(),
- },
- ]);
- let emulated_disk_controller = EmulatedDiskControllerType::Ahci;
- let network_interfaces = NetworkInterfaces(vec![
- NetworkInterface {
- name: "vif0.0".to_string(),
- mac: MacAddress::from_str("00:16:3e:00:00:00").unwrap(),
- bridge: "xenbr0".to_string(),
- gateway_device: "eth0".to_string(),
- r#type: NetworkInterfaceType::IoEmu,
- model: Some(NetworkInterfaceModel::Rtl8139),
- },
- NetworkInterface {
- name: "vif0.1".to_string(),
- mac: MacAddress::from_str("00:16:3e:00:00:01").unwrap(),
- bridge: "xenbr0".to_string(),
- gateway_device: "eth0".to_string(),
- r#type: NetworkInterfaceType::IoEmu,
- model: Some(NetworkInterfaceModel::Rtl8139),
- },
- ]);
- let domain_actions = DomainActions {
- on_poweroff: EventAction::Destroy,
- on_reboot: EventAction::Restart,
- on_watchdog: EventAction::Destroy,
- on_crash: EventAction::Destroy,
- on_soft_reset: EventAction::SoftReset,
- };
- let virtual_cpus = VirtualCpuNumber(4);
- let maximum_virtual_cpus = MaximumVirtualCpuNumber(8);
- let smbios = SmBios {
- bios_vendor: Some("Bios Vendor".to_string()),
- bios_version: Some("1.0.0".to_string()),
- system_manufacturer: Some("System Manufacturer".to_string()),
- system_product_name: Some("System Product Name".to_string()),
- system_version: Some("1.0".to_string()),
- system_serial_number: Some("0123456789".to_string()),
- baseboard_manufacturer: Some("Baseboard".to_string()),
- baseboard_product_name: Some("Baseboard Product Name".to_string()),
- baseboard_version: Some("1.0".to_string()),
- baseboard_serial_number: Some("0123456789".to_string()),
- baseboard_asset_tag: Some("0123456789".to_string()),
- baseboard_location_in_chassis: Some("123".to_string()),
- enclosure_manufacturer: Some("Enclosure Manufacturer".to_string()),
- enclosure_version: Some("1.0".to_string()),
- enclosure_serial_number: Some("0123456789".to_string()),
- enclosure_asset_tag: Some("0123456789".to_string()),
- battery_manufacturer: Some("Battery Manufacturer".to_string()),
- battery_device_name: Some("Battery Device".to_string()),
- oems: Some(vec!["Xenith".to_string(), "Xen".to_string()]),
- };
- let tsc_mode = TimeStampCounterMode::Native;
-
- let domain = Domain {
- name,
- r#type,
- memory,
- maximum_memory,
- boot_devices,
- disks,
- emulated_disk_controller,
- network_interfaces,
- domain_actions,
- virtual_cpus,
- maximum_virtual_cpus,
- smbios,
- tsc_mode,
- };
-
- // Create a new domain template and render it
- let template = DomainTemplate::new(domain)?;
- let rendered = template.render()?;
- println!("{}", rendered);
-
- // Read test fixture and compare line by line, this allows easier debugging
- let expected = std::fs::read_to_string("tests/fixtures/default.xml")?;
- for (i, (expected_line, rendered_line)) in
- expected.lines().zip(rendered.lines()).enumerate()
- {
- assert_eq!(expected_line, rendered_line, "Line {} does not match", i);
- }
-
- Ok(())
- }
-}
diff --git a/xenith-domain-management/templates/default.xml b/xenith-domain-management/templates/default.xml
deleted file mode 100644
index ddb320f..0000000
--- a/xenith-domain-management/templates/default.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-
-
- {{ domain.name }}
- {{ domain.memory }}
- {{ domain.maximum_memory }}
- {{ domain.maximum_virtual_cpus }}
-
- {{ domain.type }}
- /usr/lib/xen/boot/hvmloader
-
- {%- for device in domain.boot_devices %}
-
- {%- endfor %}
-
-
-
-
-
-
-
-
-
-
- {{ domain.domain_actions.on_poweroff }}
- {{ domain.domain_actions.on_reboot }}
- {{ domain.domain_actions.on_crash }}
-
-
-
-
-
-
-
- {%- for disk in domain.disks %}
-
-
-
-
- {%- if disk.access == "ReadOnly" %}
-
- {%- endif %}
-
- {%- endfor %}
-
-
- {%- for interface in domain.network_interfaces %}
-
-
-
-
-
- {%- endfor %}
-
-
-
- {{ domain.smbios.bios_vendor }}
-
-
- {{ domain.smbios.system_manufacturer }}
- {{ domain.smbios.system_product_name }}
- {{ domain.smbios.system_version }}
-
-
- {{ domain.smbios.baseboard_manufacturer }}
- {{ domain.smbios.baseboard_product_name }}
- {{ domain.smbios.baseboard_version }}
- {{ domain.smbios.baseboard_serial_number }}
-
-
- {{ domain.smbios.enclosure_manufacturer }}
- {{ domain.smbios.enclosure_version }}
- {{ domain.smbios.enclosure_serial_number }}
- {{ domain.smbios.enclosure_asset_tag }}
-
- {%- if domain.smbios.oems %}
-
- {%- for oem in domain.smbios.oems %}
- {{ oem }}
- {%- endfor %}
-
- {%- endif %}
-
-
diff --git a/xenith-domain-management/tests/fixtures/default-config.cfg b/xenith-domain-management/tests/fixtures/default-config.cfg
deleted file mode 100644
index 1a3ad20..0000000
--- a/xenith-domain-management/tests/fixtures/default-config.cfg
+++ /dev/null
@@ -1,37 +0,0 @@
-# Configuration file for a Xenith domain
-# This file was auto-generated by xenith-vm
-# --------------------------------------
-
-# Generic domain configuration
-name = "Xenith"
-type = "hvm"
-memory = 8000 # in MB
-maxmem = 10000 # in MB
-nestedhvm = 1
-
-# Boot
-firmware = "uefi"
-boot = "cdn"
-
-# Devices
-disk = [ "format=qcow2, vdev=xvda, access=rw, target=/dev/sda", "format=raw, vdev=xvdb, access=ro, target=/dev/sdb" ]
-hdtype = "ahci"
-
-# Network
-vif = [ "mac=00:16:3E:00:00:00, bridge=xenbr0, gatewaydev=eth0, type=ioemu, model=rtl8139", "mac=00:16:3E:00:00:01, bridge=xenbr0, gatewaydev=eth0, type=ioemu, model=rtl8139" ]
-
-# Events
-on_poweroff = "destroy"
-on_reboot = "restart"
-on_watchdog = "destroy"
-on_crash = "destroy"
-on_soft_reset = "soft-reset"
-
-# Processor
-vcpus = 4
-maxvcpus = 8
-altp2m = "mixed"
-smbios = [ "bios_vendor=Bios Vendor", "bios_version=1.0.0", "system_manufacturer=System Manufacturer", "system_product_name=System Product Name", "system_version=1.0", "system_serial_number=0123456789", "baseboard_manufacturer=Baseboard", "baseboard_product_name=Baseboard Product Name", "baseboard_version=1.0", "baseboard_serial_number=0123456789", "baseboard_asset_tag=0123456789", "baseboard_location_in_chassis=123", "enclosure_manufacturer=Enclosure Manufacturer", "enclosure_serial_number=0123456789", "enclosure_asset_tag=0123456789", "battery_manufacturer=Battery Manufacturer", "battery_device_name=Battery Device", "oem=Xenith", "oem=Xen" ]
-
-# Time Stamp Counter (TSC)
-tsc_mode = "native"
diff --git a/xenith-domain-management/tests/fixtures/default.xml b/xenith-domain-management/tests/fixtures/default.xml
deleted file mode 100644
index 8d6ae7d..0000000
--- a/xenith-domain-management/tests/fixtures/default.xml
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-
-
- Xenith
- 8000
- 10000
- 8
-
- hvm
- /usr/lib/xen/boot/hvmloader
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- destroy
- restart
- destroy
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Bios Vendor
-
-
- System Manufacturer
- System Product Name
- 1.0
-
-
- Baseboard
- Baseboard Product Name
- 1.0
- 0123456789
-
-
- Enclosure Manufacturer
- 1.0
- 0123456789
- 0123456789
-
-
- Xenith
- Xen
-
-
-
diff --git a/xenith-gui/Cargo.toml b/xenith-gui/Cargo.toml
deleted file mode 100644
index 5629301..0000000
--- a/xenith-gui/Cargo.toml
+++ /dev/null
@@ -1,16 +0,0 @@
-[package]
-name = "xenith-gui"
-description = "User interface for Xenith tools."
-
-edition.workspace = true
-rust-version.workspace = true
-license.workspace = true
-version.workspace = true
-publish.workspace = true
-authors.workspace = true
-homepage.workspace = true
-documentation.workspace = true
-repository.workspace = true
-keywords.workspace = true
-
-[dependencies]
diff --git a/xenith-gui/src/main.rs b/xenith-gui/src/main.rs
deleted file mode 100644
index 4ee771f..0000000
--- a/xenith-gui/src/main.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
-Xenith - Xen-based security hypervisor
-Copyright (C) 2025-2026 Xenith contributors
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-fn main() {}
diff --git a/xenith-redpill/xenith-redpill-macros/src/technique.rs b/xenith-redpill/xenith-redpill-macros/src/technique.rs
index 7ef754c..e3601d6 100644
--- a/xenith-redpill/xenith-redpill-macros/src/technique.rs
+++ b/xenith-redpill/xenith-redpill-macros/src/technique.rs
@@ -81,7 +81,7 @@ pub fn technique_impl(args: TokenStream, input: TokenStream) -> TokenStream {
_ => {
return TokenStream::from(
Error::custom("Invalid OS, choose from 'linux', 'windows' or 'all'").write_errors(),
- )
+ );
}
};
diff --git a/xenith-scripting/Cargo.toml b/xenith-scripting/Cargo.toml
deleted file mode 100644
index 38601da..0000000
--- a/xenith-scripting/Cargo.toml
+++ /dev/null
@@ -1,16 +0,0 @@
-[package]
-name = "xenith-scripting"
-description = "Scripting engine / bindings for Xenith tools, exposes Rust and Python 3."
-
-edition.workspace = true
-rust-version.workspace = true
-license.workspace = true
-version.workspace = true
-publish.workspace = true
-authors.workspace = true
-homepage.workspace = true
-documentation.workspace = true
-repository.workspace = true
-keywords.workspace = true
-
-[dependencies]
diff --git a/xenith-scripting/src/lib.rs b/xenith-scripting/src/lib.rs
deleted file mode 100644
index e7a04e2..0000000
--- a/xenith-scripting/src/lib.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
-Xenith - Xen-based security hypervisor
-Copyright (C) 2025-2026 Xenith contributors
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
diff --git a/xenith-stealth/Cargo.toml b/xenith-stealth/Cargo.toml
new file mode 100644
index 0000000..e2d1841
--- /dev/null
+++ b/xenith-stealth/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "xenith-stealth"
+description = "Anti-detection QEMU configuration; generates hardware identity and QEMU args to hide the VM."
+
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+version.workspace = true
+publish.workspace = true
+authors.workspace = true
+homepage.workspace = true
+documentation.workspace = true
+repository.workspace = true
+keywords.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+serde = { workspace = true }
+serde_yml = { workspace = true }
+thiserror = { workspace = true }
+rand = "0.9"
+mac-addr = { workspace = true }
diff --git a/xenith-stealth/src/acpi.rs b/xenith-stealth/src/acpi.rs
new file mode 100644
index 0000000..e87f83e
--- /dev/null
+++ b/xenith-stealth/src/acpi.rs
@@ -0,0 +1,22 @@
+/// Build QEMU args related to ACPI.
+///
+/// Overriding ACPI OEM strings (OEM ID, OEM Table ID) requires injecting a
+/// custom ACPI blob via `-acpitable`, which involves generating or patching
+/// binary DSDT/SSDT blobs at runtime. This is to be implemented in the future,
+/// but for now we just return an empty arg list, which leaves QEMU's default
+/// ACPI tables in place. QEMU's defaults are fairly generic and contain no
+/// obvious hypervisor signatures, so this is not a major issue for now.
+#[must_use]
+pub fn build_args() -> Vec {
+ vec![]
+}
+
+#[cfg(test)]
+mod tests {
+ use super::build_args;
+
+ #[test]
+ fn returns_empty_vec() {
+ assert!(build_args().is_empty());
+ }
+}
diff --git a/xenith-stealth/src/builder.rs b/xenith-stealth/src/builder.rs
new file mode 100644
index 0000000..4bcb090
--- /dev/null
+++ b/xenith-stealth/src/builder.rs
@@ -0,0 +1,110 @@
+use serde::{Deserialize, Serialize};
+
+use crate::profile::HardwareProfile;
+use crate::{acpi, cpuid, devices, smbios, timing};
+
+/// Complete stealth configuration for a VM.
+///
+/// Call [`StealthConfig::build`] to get the ready-to-use QEMU arg list, which
+/// can be passed directly to [`xenith_vm::VmConfig::with_extra_args`].
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct StealthConfig {
+ profile: HardwareProfile,
+}
+
+impl StealthConfig {
+ /// Generate a new stealth config from a randomly generated hardware profile.
+ #[must_use]
+ pub fn generate() -> Self {
+ Self {
+ profile: HardwareProfile::generate(),
+ }
+ }
+
+ /// Return the hardware profile used by this config.
+ #[must_use]
+ pub fn profile(&self) -> &HardwareProfile {
+ &self.profile
+ }
+
+ /// Build the complete QEMU arg list from this config.
+ ///
+ /// The args are ordered so that CPUID settings come first (they influence
+ /// machine-type defaults), followed by SMBIOS, ACPI, timing, and devices.
+ #[must_use]
+ pub fn build(&self) -> Vec {
+ let mut args = Vec::new();
+ args.extend(cpuid::build_args(&self.profile));
+ args.extend(smbios::build_args(&self.profile));
+ args.extend(acpi::build_args());
+ args.extend(timing::build_args());
+ args.extend(devices::build_args());
+ args
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn build_produces_nonempty_args() {
+ let cfg = StealthConfig::generate();
+ assert!(!cfg.build().is_empty());
+ }
+
+ #[test]
+ fn build_contains_cpu_flag() {
+ let args = StealthConfig::generate().build();
+ assert!(args.iter().any(|a| a == "-cpu"));
+ }
+
+ #[test]
+ fn build_contains_smbios_flag() {
+ let args = StealthConfig::generate().build();
+ assert!(args.iter().any(|a| a == "-smbios"));
+ }
+
+ #[test]
+ fn build_cpu_arg_disables_kvmclock() {
+ let args = StealthConfig::generate().build();
+ // kvmclock is disabled via a CPU feature flag, not a top-level QEMU option
+ assert!(args.iter().any(|a| a.contains("-kvmclock")));
+ }
+
+ #[test]
+ fn build_contains_device_flag() {
+ let args = StealthConfig::generate().build();
+ assert!(args.iter().any(|a| a == "-device"));
+ }
+
+ #[test]
+ fn profile_accessor_returns_nonempty_vendor() {
+ let cfg = StealthConfig::generate();
+ assert!(!cfg.profile().cpu_vendor().is_empty());
+ }
+
+ #[test]
+ fn cpu_args_precede_smbios_args() {
+ let args = StealthConfig::generate().build();
+ let cpu_pos = args.iter().position(|a| a == "-cpu").expect("-cpu present");
+ let smbios_pos = args
+ .iter()
+ .position(|a| a == "-smbios")
+ .expect("-smbios present");
+ assert!(cpu_pos < smbios_pos, "-cpu must come before -smbios");
+ }
+
+ #[test]
+ fn cpu_arg_precedes_smbios_args() {
+ // The only ordering guarantee now is -cpu before -smbios; timing is
+ // folded into -cpu flags so there is no separate ordering to assert.
+ let args = StealthConfig::generate().build();
+ let cpu_pos = args.iter().position(|a| a == "-cpu").expect("-cpu present");
+ let smbios_pos = args
+ .iter()
+ .position(|a| a == "-smbios")
+ .expect("-smbios present");
+ assert!(cpu_pos < smbios_pos);
+ }
+}
diff --git a/xenith-stealth/src/cpuid.rs b/xenith-stealth/src/cpuid.rs
new file mode 100644
index 0000000..d7237c9
--- /dev/null
+++ b/xenith-stealth/src/cpuid.rs
@@ -0,0 +1,89 @@
+use crate::profile::HardwareProfile;
+
+/// Build QEMU `-cpu` arguments that mask hypervisor CPUID leaves.
+///
+/// The resulting arg list hides the hypervisor bit, spoofs the CPU vendor
+/// string, and disables KVM paravirtual leaves so the guest sees bare metal.
+#[must_use]
+pub fn build_args(profile: &HardwareProfile) -> Vec {
+ let base_cpu = if profile.cpu_vendor() == "AuthenticAMD" {
+ "EPYC-v4"
+ } else {
+ "Skylake-Client-v4"
+ };
+
+ // Feature flags:
+ // -hypervisor hide the hypervisor present bit (CPUID.1:ECX[31])
+ // +invtsc expose invariant TSC (expected by anti-VM checks)
+ // kvm=off disable KVM paravirtualisation CPUID leaves
+ // -kvmclock disable the KVM paravirtual clock device
+ // vendor= spoof the vendor string (GenuineIntel / AuthenticAMD)
+ let cpu_arg = format!(
+ "{base_cpu},-hypervisor,+invtsc,kvm=off,-kvmclock,vendor={vendor}",
+ vendor = profile.cpu_vendor(),
+ );
+
+ vec![String::from("-cpu"), cpu_arg]
+}
+
+#[cfg(test)]
+mod tests {
+ use super::build_args;
+ use crate::profile::HardwareProfile;
+
+ fn intel_profile() -> HardwareProfile {
+ (0..100)
+ .map(|_| HardwareProfile::generate())
+ .find(|p| p.cpu_vendor() == "GenuineIntel")
+ .expect("no Intel profile in 100 tries")
+ }
+
+ fn amd_profile() -> HardwareProfile {
+ (0..100)
+ .map(|_| HardwareProfile::generate())
+ .find(|p| p.cpu_vendor() == "AuthenticAMD")
+ .expect("no AMD profile in 100 tries")
+ }
+
+ #[test]
+ fn args_are_exactly_two_elements() {
+ assert_eq!(build_args(&HardwareProfile::generate()).len(), 2);
+ }
+
+ #[test]
+ fn first_arg_is_cpu_flag() {
+ assert_eq!(build_args(&HardwareProfile::generate())[0], "-cpu");
+ }
+
+ #[test]
+ fn intel_vendor_selects_skylake_base() {
+ assert!(build_args(&intel_profile())[1].starts_with("Skylake-Client-v4,"));
+ }
+
+ #[test]
+ fn amd_vendor_selects_epyc_base() {
+ assert!(build_args(&amd_profile())[1].starts_with("EPYC-v4,"));
+ }
+
+ #[test]
+ fn cpu_arg_hides_hypervisor_bit() {
+ assert!(build_args(&HardwareProfile::generate())[1].contains("-hypervisor"));
+ }
+
+ #[test]
+ fn cpu_arg_exposes_invtsc() {
+ assert!(build_args(&HardwareProfile::generate())[1].contains("+invtsc"));
+ }
+
+ #[test]
+ fn cpu_arg_disables_kvm_leaves() {
+ assert!(build_args(&HardwareProfile::generate())[1].contains("kvm=off"));
+ }
+
+ #[test]
+ fn cpu_arg_propagates_vendor_string() {
+ let p = HardwareProfile::generate();
+ let expected = format!("vendor={}", p.cpu_vendor());
+ assert!(build_args(&p)[1].contains(&expected));
+ }
+}
diff --git a/xenith-stealth/src/devices.rs b/xenith-stealth/src/devices.rs
new file mode 100644
index 0000000..1033a64
--- /dev/null
+++ b/xenith-stealth/src/devices.rs
@@ -0,0 +1,42 @@
+/// Build QEMU args that present plausible real PCI/USB device IDs.
+///
+/// QEMU's default devices (e.g. `virtio-*`, `QEMU HARDDISK`, `QEMU DVD-ROM`)
+/// are trivially detected. This module replaces them with well-known real-world
+/// device vendor/product IDs.
+#[must_use]
+pub fn build_args() -> Vec {
+ vec![
+ // USB controller: Intel Panther Point xHCI (very common on 3rd-gen Intel boards)
+ String::from("-device"),
+ String::from("qemu-xhci,id=usb,p2=15,p3=15"),
+ // USB HID tablet (prevents relative-mouse issues; looks like a real USB device)
+ String::from("-device"),
+ String::from("usb-tablet,bus=usb.0"),
+ ]
+}
+
+#[cfg(test)]
+mod tests {
+ use super::build_args;
+
+ #[test]
+ fn returns_four_args() {
+ assert_eq!(build_args().len(), 4);
+ }
+
+ #[test]
+ fn both_device_flags_present() {
+ let count = build_args().iter().filter(|a| *a == "-device").count();
+ assert_eq!(count, 2);
+ }
+
+ #[test]
+ fn contains_xhci_controller() {
+ assert!(build_args().iter().any(|a| a.contains("qemu-xhci")));
+ }
+
+ #[test]
+ fn contains_usb_tablet() {
+ assert!(build_args().iter().any(|a| a.contains("usb-tablet")));
+ }
+}
diff --git a/xenith-stealth/src/error.rs b/xenith-stealth/src/error.rs
new file mode 100644
index 0000000..6434b5b
--- /dev/null
+++ b/xenith-stealth/src/error.rs
@@ -0,0 +1,18 @@
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum Error {
+ #[error("profile generation failed: {reason}")]
+ ProfileGeneration { reason: String },
+
+ #[error("CPUID configuration error: {reason}")]
+ Cpuid { reason: String },
+
+ #[error("SMBIOS configuration error: {reason}")]
+ Smbios { reason: String },
+
+ #[error("ACPI configuration error: {reason}")]
+ Acpi { reason: String },
+}
+
+pub type Result = std::result::Result;
diff --git a/xenith-stealth/src/lib.rs b/xenith-stealth/src/lib.rs
new file mode 100644
index 0000000..3c35e0d
--- /dev/null
+++ b/xenith-stealth/src/lib.rs
@@ -0,0 +1,18 @@
+pub mod acpi;
+pub mod builder;
+pub mod cpuid;
+pub mod devices;
+pub mod error;
+pub mod profile;
+pub mod smbios;
+pub mod timing;
+
+pub use builder::StealthConfig;
+pub use error::Error as StealthError;
+pub use error::Result as StealthResult;
+pub use profile::HardwareProfile;
+
+pub mod prelude {
+ pub use crate::builder::StealthConfig;
+ pub use crate::profile::HardwareProfile;
+}
diff --git a/xenith-stealth/src/profile.rs b/xenith-stealth/src/profile.rs
new file mode 100644
index 0000000..6d16c70
--- /dev/null
+++ b/xenith-stealth/src/profile.rs
@@ -0,0 +1,318 @@
+use std::sync::OnceLock;
+
+use mac_addr::MacAddr;
+use rand::Rng;
+use serde::{Deserialize, Serialize};
+
+/// A CPU vendor + model pair. CPU selection is independent of the board within
+/// the same platform, so these are pooled separately.
+#[derive(Debug, Deserialize)]
+struct CpuTemplate {
+ vendor: String,
+ model: String,
+}
+
+/// A board identity: system info, board info, and the associated BIOS strings.
+/// BIOS version strings are board-specific, so they stay coupled to the board.
+#[derive(Debug, Deserialize)]
+struct BoardTemplate {
+ system_manufacturer: String,
+ system_product: String,
+ board_manufacturer: String,
+ board_product: String,
+ bios_vendor: String,
+ bios_version: String,
+}
+
+/// A hardware platform (Intel or AMD) with independent CPU and board pools.
+/// Sampling one CPU and one board from the same platform guarantees coherence.
+#[derive(Debug, Deserialize)]
+struct PlatformTemplate {
+ cpus: Vec,
+ boards: Vec,
+}
+
+/// A real NIC vendor OUI prefix. The last three bytes of the MAC are random.
+#[derive(Debug, Deserialize)]
+struct OuiTemplate {
+ /// Human-readable vendor label (e.g. `"Intel"`). Not used at runtime.
+ #[allow(dead_code)]
+ vendor: String,
+ /// First three octets of the MAC in `"XX:XX:XX"` hex format.
+ oui: String,
+}
+
+impl OuiTemplate {
+ /// Parse the OUI string into raw bytes.
+ fn bytes(&self) -> [u8; 3] {
+ let mut parts = self.oui.splitn(3, ':');
+ let mut next = || {
+ let h = parts.next().expect("OUI must be XX:XX:XX");
+ u8::from_str_radix(h, 16).expect("OUI must be valid hex")
+ };
+ [next(), next(), next()]
+ }
+}
+
+/// Root of `templates.yaml`: OUI pool and platform list.
+#[derive(Debug, Deserialize)]
+struct Templates {
+ ouis: Vec,
+ platforms: Vec,
+}
+
+static TEMPLATES: OnceLock = OnceLock::new();
+
+fn templates() -> &'static Templates {
+ TEMPLATES.get_or_init(|| {
+ serde_yml::from_str(include_str!("../templates.yaml")).expect("templates.yaml is malformed")
+ })
+}
+
+fn platforms() -> &'static [PlatformTemplate] {
+ &templates().platforms
+}
+
+fn ouis() -> &'static [OuiTemplate] {
+ &templates().ouis
+}
+
+/// A coherent fake hardware identity for a VM.
+///
+/// All fields are randomised by [`HardwareProfile::generate`] to produce a
+/// plausible real-world machine. The profile is consumed by the other stealth
+/// modules to derive CPUID args, SMBIOS tables, and device IDs.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct HardwareProfile {
+ /// CPU vendor string (e.g. `"GenuineIntel"`).
+ cpu_vendor: String,
+ /// CPU model string (e.g. `"Intel(R) Core(TM) i7-10700K CPU @ 3.80GHz"`).
+ cpu_model: String,
+ /// BIOS vendor (e.g. `"American Megatrends Inc."`).
+ bios_vendor: String,
+ /// BIOS version string (e.g. `"F14"`).
+ bios_version: String,
+ /// System manufacturer (e.g. `"GIGABYTE"`).
+ system_manufacturer: String,
+ /// System product name (e.g. `"Z490 AORUS ELITE"`).
+ system_product: String,
+ /// System serial number (randomised hex).
+ system_serial: String,
+ /// Base board manufacturer.
+ board_manufacturer: String,
+ /// Base board product name.
+ board_product: String,
+ /// Base board serial number.
+ board_serial: String,
+ /// MAC address for the primary NIC.
+ mac_address: MacAddr,
+}
+
+impl HardwareProfile {
+ /// Generate a randomised but internally consistent hardware profile.
+ #[must_use]
+ pub fn generate() -> Self {
+ let mut rng = rand::rng();
+ let plats = platforms();
+ let plat = &plats[rng.random_range(0..plats.len())];
+ let cpu = &plat.cpus[rng.random_range(0..plat.cpus.len())];
+ let board = &plat.boards[rng.random_range(0..plat.boards.len())];
+
+ Self {
+ cpu_vendor: cpu.vendor.clone(),
+ cpu_model: cpu.model.clone(),
+ bios_vendor: board.bios_vendor.clone(),
+ bios_version: board.bios_version.clone(),
+ system_manufacturer: board.system_manufacturer.clone(),
+ system_product: board.system_product.clone(),
+ system_serial: random_serial(&mut rng, 10),
+ board_manufacturer: board.board_manufacturer.clone(),
+ board_product: board.board_product.clone(),
+ board_serial: random_serial(&mut rng, 8),
+ mac_address: random_mac(&mut rng),
+ }
+ }
+
+ /// CPU vendor string reported by CPUID (e.g. `"GenuineIntel"`).
+ #[must_use]
+ pub fn cpu_vendor(&self) -> &str {
+ &self.cpu_vendor
+ }
+
+ /// CPU model string (e.g. `"Intel(R) Core(TM) i7-10700K CPU @ 3.80GHz"`).
+ #[must_use]
+ pub fn cpu_model(&self) -> &str {
+ &self.cpu_model
+ }
+
+ /// BIOS vendor (e.g. `"American Megatrends Inc."`).
+ #[must_use]
+ pub fn bios_vendor(&self) -> &str {
+ &self.bios_vendor
+ }
+
+ /// BIOS version string (e.g. `"F14"`).
+ #[must_use]
+ pub fn bios_version(&self) -> &str {
+ &self.bios_version
+ }
+
+ /// System manufacturer (e.g. `"GIGABYTE"`).
+ #[must_use]
+ pub fn system_manufacturer(&self) -> &str {
+ &self.system_manufacturer
+ }
+
+ /// System product name (e.g. `"Z490 AORUS ELITE"`).
+ #[must_use]
+ pub fn system_product(&self) -> &str {
+ &self.system_product
+ }
+
+ /// System serial number.
+ #[must_use]
+ pub fn system_serial(&self) -> &str {
+ &self.system_serial
+ }
+
+ /// Base board manufacturer.
+ #[must_use]
+ pub fn board_manufacturer(&self) -> &str {
+ &self.board_manufacturer
+ }
+
+ /// Base board product name.
+ #[must_use]
+ pub fn board_product(&self) -> &str {
+ &self.board_product
+ }
+
+ /// Base board serial number.
+ #[must_use]
+ pub fn board_serial(&self) -> &str {
+ &self.board_serial
+ }
+
+ /// MAC address for the primary NIC.
+ #[must_use]
+ pub fn mac_address(&self) -> MacAddr {
+ self.mac_address
+ }
+}
+
+fn random_serial(rng: &mut impl Rng, len: usize) -> String {
+ const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ (0..len)
+ .map(|_| char::from(CHARSET[rng.random_range(0..CHARSET.len())]))
+ .collect()
+}
+
+fn random_mac(rng: &mut impl Rng) -> MacAddr {
+ let ouis = ouis();
+ let oui = ouis[rng.random_range(0..ouis.len())].bytes();
+ MacAddr::new(
+ oui[0],
+ oui[1],
+ oui[2],
+ rng.random(),
+ rng.random(),
+ rng.random(),
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn generate_returns_valid_profile() {
+ let p = HardwareProfile::generate();
+ assert!(!p.cpu_vendor().is_empty());
+ assert!(!p.mac_address().to_string().starts_with("52:54"));
+ }
+
+ #[test]
+ fn cpu_vendor_is_intel_or_amd() {
+ let p = HardwareProfile::generate();
+ assert!(
+ p.cpu_vendor() == "GenuineIntel" || p.cpu_vendor() == "AuthenticAMD",
+ "unexpected vendor: {}",
+ p.cpu_vendor()
+ );
+ }
+
+ #[test]
+ fn all_string_fields_are_nonempty() {
+ let p = HardwareProfile::generate();
+ assert!(!p.cpu_model().is_empty());
+ assert!(!p.bios_vendor().is_empty());
+ assert!(!p.bios_version().is_empty());
+ assert!(!p.system_manufacturer().is_empty());
+ assert!(!p.system_product().is_empty());
+ assert!(!p.system_serial().is_empty());
+ assert!(!p.board_manufacturer().is_empty());
+ assert!(!p.board_product().is_empty());
+ assert!(!p.board_serial().is_empty());
+ }
+
+ #[test]
+ fn system_serial_is_ten_uppercase_alphanumeric_chars() {
+ let p = HardwareProfile::generate();
+ let s = p.system_serial();
+ assert_eq!(s.len(), 10, "system serial must be 10 chars");
+ assert!(
+ s.chars()
+ .all(|c| c.is_ascii_alphanumeric() && !c.is_ascii_lowercase()),
+ "serial must be uppercase alphanumeric: {s}",
+ );
+ }
+
+ #[test]
+ fn board_serial_is_eight_uppercase_alphanumeric_chars() {
+ let p = HardwareProfile::generate();
+ let s = p.board_serial();
+ assert_eq!(s.len(), 8, "board serial must be 8 chars");
+ assert!(
+ s.chars()
+ .all(|c| c.is_ascii_alphanumeric() && !c.is_ascii_lowercase()),
+ "serial must be uppercase alphanumeric: {s}",
+ );
+ }
+
+ #[test]
+ fn mac_uses_known_oui_prefix() {
+ let known: Vec<[u8; 3]> = ouis().iter().map(|o| o.bytes()).collect();
+ let p = HardwareProfile::generate();
+ let oui = p.mac_address().oui();
+ assert!(
+ known.contains(&oui),
+ "MAC {} has unrecognised OUI {:02X?}",
+ p.mac_address(),
+ oui,
+ );
+ }
+
+ #[test]
+ fn random_serial_length_matches_requested() {
+ let mut rng = rand::rng();
+ assert_eq!(random_serial(&mut rng, 5).len(), 5);
+ assert_eq!(random_serial(&mut rng, 12).len(), 12);
+ }
+
+ #[test]
+ fn generate_produces_varied_results() {
+ let models: std::collections::HashSet = (0..20)
+ .map(|_| HardwareProfile::generate().cpu_model().to_owned())
+ .collect();
+ assert!(models.len() > 1, "all profiles had the same CPU model");
+ }
+
+ #[test]
+ fn profile_is_clone_and_equal_debug() {
+ let p = HardwareProfile::generate();
+ let cloned = p.clone();
+ assert_eq!(p.cpu_vendor(), cloned.cpu_vendor());
+ assert_eq!(p.system_serial(), cloned.system_serial());
+ assert_eq!(p.mac_address(), cloned.mac_address());
+ }
+}
diff --git a/xenith-stealth/src/smbios.rs b/xenith-stealth/src/smbios.rs
new file mode 100644
index 0000000..b61e37e
--- /dev/null
+++ b/xenith-stealth/src/smbios.rs
@@ -0,0 +1,122 @@
+use crate::profile::HardwareProfile;
+
+/// Strip characters that QEMU's `-smbios` comma-delimited parser cannot handle.
+///
+/// QEMU splits `-smbios` values on commas with no escaping mechanism, so any
+/// comma inside a field value (e.g. `"Co., Ltd."`) is treated as a separator.
+/// Remove commas in-place; the resulting string remains human-readable.
+fn smbios_safe(s: &str) -> String {
+ s.replace(',', "")
+}
+
+/// Build QEMU `-smbios` arguments for BIOS, system, board, and chassis tables.
+///
+/// Covers DMI types 0 (BIOS), 1 (System), 2 (Base Board), 3 (Chassis).
+#[must_use]
+pub fn build_args(profile: &HardwareProfile) -> Vec {
+ vec![
+ // Type 0: BIOS information
+ String::from("-smbios"),
+ format!(
+ "type=0,vendor={},version={}",
+ smbios_safe(profile.bios_vendor()),
+ smbios_safe(profile.bios_version()),
+ ),
+ // Type 1: System information
+ String::from("-smbios"),
+ format!(
+ "type=1,manufacturer={},product={},serial={}",
+ smbios_safe(profile.system_manufacturer()),
+ smbios_safe(profile.system_product()),
+ profile.system_serial(),
+ ),
+ // Type 2: Base board
+ String::from("-smbios"),
+ format!(
+ "type=2,manufacturer={},product={},serial={}",
+ smbios_safe(profile.board_manufacturer()),
+ smbios_safe(profile.board_product()),
+ profile.board_serial(),
+ ),
+ // Type 3: Chassis (generic, no identifying strings)
+ String::from("-smbios"),
+ String::from("type=3,manufacturer=Default string"),
+ ]
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{build_args, smbios_safe};
+ use crate::profile::HardwareProfile;
+
+ fn profile() -> HardwareProfile {
+ HardwareProfile::generate()
+ }
+
+ #[test]
+ fn produces_eight_strings() {
+ assert_eq!(build_args(&profile()).len(), 8);
+ }
+
+ #[test]
+ fn every_even_index_is_smbios_flag() {
+ let args = build_args(&profile());
+ for i in (0..args.len()).step_by(2) {
+ assert_eq!(args[i], "-smbios", "index {i} should be -smbios");
+ }
+ }
+
+ #[test]
+ fn type0_arg_contains_bios_vendor() {
+ let p = profile();
+ assert!(build_args(&p)[1].contains(&format!("vendor={}", smbios_safe(p.bios_vendor()))));
+ }
+
+ #[test]
+ fn type0_arg_contains_bios_version() {
+ let p = profile();
+ assert!(build_args(&p)[1].contains(&format!("version={}", smbios_safe(p.bios_version()))));
+ }
+
+ #[test]
+ fn type1_arg_contains_system_manufacturer() {
+ let p = profile();
+ assert!(build_args(&p)[3].contains(&smbios_safe(p.system_manufacturer())));
+ }
+
+ #[test]
+ fn type1_arg_contains_system_product() {
+ let p = profile();
+ assert!(build_args(&p)[3].contains(&smbios_safe(p.system_product())));
+ }
+
+ #[test]
+ fn type1_arg_contains_system_serial() {
+ let p = profile();
+ assert!(build_args(&p)[3].contains(p.system_serial()));
+ }
+
+ #[test]
+ fn type2_arg_contains_board_manufacturer() {
+ let p = profile();
+ assert!(build_args(&p)[5].contains(&smbios_safe(p.board_manufacturer())));
+ }
+
+ #[test]
+ fn type2_arg_contains_board_product() {
+ let p = profile();
+ assert!(build_args(&p)[5].contains(&smbios_safe(p.board_product())));
+ }
+
+ #[test]
+ fn type2_arg_contains_board_serial() {
+ let p = profile();
+ assert!(build_args(&p)[5].contains(p.board_serial()));
+ }
+
+ #[test]
+ fn type3_is_generic_chassis() {
+ let args = build_args(&profile());
+ assert_eq!(args[7], "type=3,manufacturer=Default string");
+ }
+}
diff --git a/xenith-stealth/src/timing.rs b/xenith-stealth/src/timing.rs
new file mode 100644
index 0000000..76d13d2
--- /dev/null
+++ b/xenith-stealth/src/timing.rs
@@ -0,0 +1,19 @@
+/// Build QEMU args that configure timing behaviour to look like bare metal.
+///
+/// kvmclock and TSC flags (`kvm=off`, `-kvmclock`, `+invtsc`, `-hypervisor`)
+/// are all expressed as CPU feature flags and handled by [`crate::cpuid::build_args`].
+/// No additional top-level QEMU args are required at this time.
+#[must_use]
+pub fn build_args() -> Vec {
+ vec![]
+}
+
+#[cfg(test)]
+mod tests {
+ use super::build_args;
+
+ #[test]
+ fn returns_empty_vec() {
+ assert!(build_args().is_empty());
+ }
+}
diff --git a/xenith-stealth/templates.yaml b/xenith-stealth/templates.yaml
new file mode 100644
index 0000000..714fde1
--- /dev/null
+++ b/xenith-stealth/templates.yaml
@@ -0,0 +1,107 @@
+# Hardware identity templates for xenith-stealth.
+#
+# Top-level structure
+# -------------------
+# ouis : pool of real NIC vendor OUI prefixes. One is sampled at random
+# to form the first three bytes of the spoofed MAC address.
+# The last three bytes are always randomised.
+#
+# platforms : list of hardware platforms. A platform groups components that are
+# physically compatible (Intel LGA + Intel CPUs, AMD AM4/AM5 + AMD
+# CPUs). Mixing across platforms produces incoherent profiles that
+# anti-VM checks can detect.
+#
+# Within a platform, `cpus` and `boards` are independent pools. At runtime one
+# CPU and one board are sampled separately, giving N×M distinct profiles per
+# platform without N×M template entries.
+#
+# Adding entries
+# --------------
+# New OUI : add a { vendor, oui } entry. `oui` must be a real, registered
+# NIC vendor prefix in "XX:XX:XX" hex format. Never use 52:54
+# (QEMU default) or other locally-administered prefixes.
+#
+# New CPU : add a { vendor, model } entry under the matching platform's
+# `cpus` list. It pairs automatically with every existing board.
+#
+# New board : add a full board entry under `boards`. `bios_version` must
+# match the real string shipped by that manufacturer (GIGABYTE
+# uses "F"-prefixed strings, ASUS uses numeric strings, etc.).
+#
+# New platform: add a top-level entry with at least one cpu and one board.
+#
+# Field reference
+# ---------------
+# ouis[].vendor : human-readable NIC vendor label (not parsed)
+# ouis[].oui : 3-byte OUI in "XX:XX:XX" hex format
+# platforms[].platform : human-readable label (not parsed)
+# platforms[].cpus[].vendor : CPUID vendor string ("GenuineIntel" / "AuthenticAMD")
+# platforms[].cpus[].model : CPUID brand string as reported by the CPU
+# platforms[].boards[].system_* : DMI type 1 (System Information) fields
+# platforms[].boards[].board_* : DMI type 2 (Base Board) fields
+# platforms[].boards[].bios_vendor : DMI type 0 — almost always an AMI variant
+# platforms[].boards[].bios_version: DMI type 0 — board-specific, must be authentic
+
+ouis:
+ # took from https://udger.com/resources/mac-address-vendor-detail?name=intel_corporation
+ - vendor: Intel Corporate
+ oui: "00:02:B3"
+ - vendor: Intel Corporate
+ oui: "00:03:47"
+ - vendor: Intel Corporate
+ oui: "00:04:23"
+ - vendor: Intel Corporate
+ oui: "00:07:E9"
+ - vendor: Intel Corporate
+ oui: "00:0C:F1"
+ - vendor: Intel Corporate
+ oui: "00:0E:0C"
+ - vendor: Intel Corporate
+ oui: "00:0E:35"
+ - vendor: Intel Corporate
+ oui: "00:11:11"
+ - vendor: REALTEK SEMICONDUCTOR CORP.
+ oui: "00:E0:4C"
+ - vendor: Micro-Star INTL CO., LTD.
+ oui: "D8:BB:C1"
+
+platforms:
+ - platform: intel
+ cpus:
+ - vendor: GenuineIntel
+ model: "Intel(R) Core(TM) i7-10700K CPU @ 3.80GHz"
+ - vendor: GenuineIntel
+ model: "Intel(R) Core(TM) i9-12900K CPU @ 3.20GHz"
+ boards:
+ - system_manufacturer: GIGABYTE
+ system_product: Z490 AORUS ELITE
+ board_manufacturer: GIGABYTE
+ board_product: Z490 AORUS ELITE
+ bios_vendor: "American Megatrends Inc."
+ bios_version: F14
+ - system_manufacturer: "ASUSTeK COMPUTER INC."
+ system_product: "ROG STRIX Z690-E GAMING WIFI"
+ board_manufacturer: "ASUSTeK COMPUTER INC."
+ board_product: "ROG STRIX Z690-E GAMING WIFI"
+ bios_vendor: "American Megatrends International, LLC."
+ bios_version: "1602"
+
+ - platform: amd
+ cpus:
+ - vendor: AuthenticAMD
+ model: "AMD Ryzen 9 5900X 12-Core Processor"
+ - vendor: AuthenticAMD
+ model: "AMD Ryzen 7 5800X 8-Core Processor"
+ boards:
+ - system_manufacturer: "Micro-Star International Co., Ltd."
+ system_product: MEG X570 UNIFY
+ board_manufacturer: "Micro-Star International Co., Ltd."
+ board_product: "MEG X570 UNIFY (MS-7C35)"
+ bios_vendor: "American Megatrends Inc."
+ bios_version: "3003"
+ - system_manufacturer: GIGABYTE
+ system_product: X570 AORUS MASTER
+ board_manufacturer: GIGABYTE
+ board_product: X570 AORUS MASTER
+ bios_vendor: "American Megatrends Inc."
+ bios_version: F35
diff --git a/xenith-stealth/tests/integration.rs b/xenith-stealth/tests/integration.rs
new file mode 100644
index 0000000..25dae3f
--- /dev/null
+++ b/xenith-stealth/tests/integration.rs
@@ -0,0 +1,117 @@
+use xenith_stealth::{HardwareProfile, StealthConfig};
+
+#[test]
+fn stealth_config_build_arg_count_is_stable() {
+ // At minimum: 2 cpu args + 8 smbios args + 0 acpi + 0 timing + 4 devices = 14.
+ // kvmclock is now a CPU feature flag (-kvmclock inside the -cpu arg), not a
+ // separate top-level option.
+ let cfg = StealthConfig::generate();
+ assert!(cfg.build().len() >= 14, "expected at least 14 args");
+}
+
+#[test]
+fn stealth_config_produces_no_qemu_default_vendor() {
+ // QEMU's default CPUID vendor is "KVMKVMKVM" or "TCGTCGTCGTCG".
+ // build_args must not emit those strings.
+ let args = StealthConfig::generate().build();
+ let full = args.join(" ");
+ assert!(!full.contains("KVMKVMKVM"), "KVM vendor must be masked");
+ assert!(!full.contains("TCGTCGTCG"), "TCG vendor must be masked");
+}
+
+#[test]
+fn stealth_config_does_not_expose_hypervisor_bit() {
+ let args = StealthConfig::generate().build();
+ assert!(
+ args.iter().any(|a| a.contains("-hypervisor")),
+ "must pass -hypervisor to QEMU to hide the hypervisor CPUID bit"
+ );
+}
+
+#[test]
+fn stealth_config_kvm_parairt_disabled() {
+ let args = StealthConfig::generate().build();
+ assert!(args.iter().any(|a| a.contains("kvm=off")));
+}
+
+#[test]
+fn stealth_config_includes_invtsc() {
+ let args = StealthConfig::generate().build();
+ assert!(args.iter().any(|a| a.contains("+invtsc")));
+}
+
+#[test]
+fn stealth_config_has_four_smbios_tables() {
+ let args = StealthConfig::generate().build();
+ let count = args.iter().filter(|a| *a == "-smbios").count();
+ assert_eq!(count, 4, "expected 4 -smbios flags (types 0, 1, 2, 3)");
+}
+
+#[test]
+fn hardware_profile_cpu_and_board_coherent() {
+ // Intel CPUs must not appear with AMD-only board strings and vice-versa.
+ // The templates only have Intel-labelled boards in the intel platform and
+ // AMD-labelled boards in the amd platform — so vendor must be consistent.
+ let p = HardwareProfile::generate();
+ let vendor = p.cpu_vendor();
+ assert!(
+ vendor == "GenuineIntel" || vendor == "AuthenticAMD",
+ "unexpected vendor: {vendor}"
+ );
+}
+
+#[test]
+fn hardware_profile_mac_is_unicast_globally_administered() {
+ // Real vendor OUIs are globally unique (universally administered) and unicast.
+ let p = HardwareProfile::generate();
+ let mac = p.mac_address();
+ assert!(mac.is_unicast(), "spoofed MAC must be unicast");
+ assert!(
+ mac.is_universal(),
+ "spoofed MAC must use a globally-administered OUI"
+ );
+}
+
+#[test]
+fn stealth_config_kvmclock_disabled_via_cpu_flag() {
+ // -no-kvmclock is not a valid QEMU option; kvmclock is disabled by passing
+ // -kvmclock as a CPU feature flag inside the -cpu argument.
+ let args = StealthConfig::generate().build();
+ let cpu_arg = args
+ .windows(2)
+ .find(|w| w[0] == "-cpu")
+ .map(|w| &w[1])
+ .expect("-cpu arg present");
+ assert!(
+ cpu_arg.contains("-kvmclock"),
+ "kvmclock must be disabled via CPU feature flag, got: {cpu_arg}"
+ );
+}
+
+#[test]
+fn stealth_config_arg_ordering() {
+ let args = StealthConfig::generate().build();
+
+ let find =
+ |needle: &str| -> usize { args.iter().position(|a| a == needle).unwrap_or(usize::MAX) };
+
+ let cpu_pos = find("-cpu");
+ let smbios_pos = find("-smbios");
+ let device_pos = find("-device");
+
+ assert!(cpu_pos < smbios_pos, "-cpu must precede -smbios");
+ assert!(smbios_pos < device_pos, "-smbios must precede -device");
+}
+
+#[test]
+fn hardware_profile_serials_differ() {
+ // Two generated profiles must produce different serials (with overwhelming probability).
+ let a = HardwareProfile::generate();
+ let b = HardwareProfile::generate();
+ // Very unlikely to collide given a 36^10 space; if it does the RNG is broken.
+ assert_ne!(
+ a.system_serial(),
+ b.system_serial(),
+ "two consecutive profiles share the same system serial — RNG may be seeded identically"
+ );
+}
diff --git a/xenith-vm/Cargo.toml b/xenith-vm/Cargo.toml
new file mode 100644
index 0000000..caaa808
--- /dev/null
+++ b/xenith-vm/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "xenith-vm"
+description = "VM lifecycle management — QEMU process launch and QMP control."
+
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+version.workspace = true
+publish.workspace = true
+authors.workspace = true
+homepage.workspace = true
+documentation.workspace = true
+repository.workspace = true
+keywords.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+tokio = { workspace = true }
+serde = { workspace = true }
+thiserror = { workspace = true }
+tracing = { workspace = true }
+uuid = { version = "1", features = ["v4"] }
+libc = "0.2"
+qapi = { version = "0.15", features = ["qmp", "async-tokio-all"] }
+toml = "0.8"
+mac-addr = { workspace = true }
+
+[dev-dependencies]
+xenith-stealth = { path = "../xenith-stealth" }
diff --git a/xenith-vm/examples/boot.rs b/xenith-vm/examples/boot.rs
new file mode 100644
index 0000000..df037eb
--- /dev/null
+++ b/xenith-vm/examples/boot.rs
@@ -0,0 +1,238 @@
+//! Launch a VM with a randomly generated xenith-stealth hardware profile.
+//!
+//! # Usage
+//!
+//! ```bash
+//! cargo run -p xenith-vm --example boot -- \
+//! --name win11 --vcpus 4 --memory-gib 8 \
+//! --disk /path/to/win11.qcow2 \
+//! --tap tap0 --mac 52:54:00:11:22:33 \
+//! --firmware /usr/share/OVMF/OVMF_CODE.fd \
+//! --display sdl
+//! ```
+//!
+//! Add `--dry-run` to print the full QEMU command without launching.
+//! `--display` accepts `sdl`, `none`, or `vnc=HOST:PORT`.
+//!
+//! # Host setup (TAP networking)
+//!
+//! ```bash
+//! sudo ip tuntap add tap0 mode tap user $(whoami)
+//! sudo ip link set tap0 up
+//! sudo ip link set tap0 master virbr0 # or your bridge
+//! ```
+
+use std::path::PathBuf;
+
+use mac_addr::MacAddr;
+use xenith_stealth::StealthConfig;
+use xenith_vm::{
+ DiskImage, DisplayMode, NetworkInterface, VmConfig, backend::qemu::QemuBackend, vm::Vm,
+};
+
+const USAGE: &str = "\
+Usage: boot --name --vcpus --memory-gib --disk \
+ --tap --mac \
+ [--firmware ] [--display sdl|none|vnc=HOST:PORT] [--dry-run]";
+
+struct Args {
+ name: String,
+ vcpus: u32,
+ memory_gib: u64,
+ disk: PathBuf,
+ tap: String,
+ mac: MacAddr,
+ firmware: Option,
+ display: DisplayMode,
+ dry_run: bool,
+}
+
+#[derive(Default)]
+struct ArgsBuilder {
+ name: Option,
+ vcpus: Option,
+ memory_gib: Option,
+ disk: Option,
+ tap: Option,
+ mac: Option,
+ firmware: Option,
+ display: Option,
+ dry_run: bool,
+}
+
+impl ArgsBuilder {
+ fn finish(self) -> Result {
+ Ok(Args {
+ name: self.name.ok_or("--name is required")?,
+ vcpus: self.vcpus.ok_or("--vcpus is required")?,
+ memory_gib: self.memory_gib.ok_or("--memory-gib is required")?,
+ disk: self.disk.ok_or("--disk is required")?,
+ tap: self.tap.ok_or("--tap is required")?,
+ mac: self.mac.ok_or("--mac is required")?,
+ firmware: self.firmware,
+ display: self.display.unwrap_or(DisplayMode::Sdl),
+ dry_run: self.dry_run,
+ })
+ }
+}
+
+fn parse_display(s: &str) -> Result {
+ match s {
+ "sdl" => Ok(DisplayMode::Sdl),
+ "none" => Ok(DisplayMode::None),
+ vnc if vnc.starts_with("vnc=") => Ok(DisplayMode::Vnc(vnc[4..].to_owned())),
+ other => Err(format!(
+ "unknown display '{other}': use sdl, none, or vnc=HOST:PORT"
+ )),
+ }
+}
+
+fn apply_flag(b: &mut ArgsBuilder, flag: &str, val: String) -> Result<(), String> {
+ match flag {
+ "--name" => b.name = Some(val),
+ "--vcpus" => b.vcpus = Some(val.parse().map_err(|e| format!("--vcpus: {e}"))?),
+ "--memory-gib" => {
+ b.memory_gib = Some(val.parse().map_err(|e| format!("--memory-gib: {e}"))?);
+ }
+ "--disk" => b.disk = Some(PathBuf::from(val)),
+ "--tap" => b.tap = Some(val),
+ "--mac" => b.mac = Some(val.parse().map_err(|e| format!("--mac: {e}"))?),
+ "--firmware" => b.firmware = Some(PathBuf::from(val)),
+ "--display" => b.display = Some(parse_display(&val)?),
+ other => return Err(format!("unknown flag: {other}")),
+ }
+ Ok(())
+}
+
+fn parse_args() -> Result {
+ let raw: Vec = std::env::args().skip(1).collect();
+ let mut b = ArgsBuilder::default();
+ let mut i = 0;
+ while i < raw.len() {
+ if raw[i] == "--dry-run" {
+ b.dry_run = true;
+ i += 1;
+ continue;
+ }
+ let val = raw
+ .get(i + 1)
+ .ok_or_else(|| format!("{} requires a value", raw[i]))?
+ .clone();
+ apply_flag(&mut b, &raw[i], val)?;
+ i += 2;
+ }
+ b.finish()
+}
+
+/// Build a `DiskImage` pointing at an existing file without calling `qemu-img`.
+///
+/// `DiskImage::create` spawns `qemu-img create`, which is wrong for a disk that
+/// already exists. We deserialise directly so the path is accepted as-is.
+fn existing_disk(path: PathBuf) -> DiskImage {
+ #[derive(serde::Serialize)]
+ struct Proxy<'a> {
+ path: &'a PathBuf,
+ format: &'static str,
+ size_bytes: u64,
+ }
+ let s = toml::to_string(&Proxy {
+ path: &path,
+ format: "qcow2",
+ size_bytes: 0,
+ })
+ .expect("serialise DiskImage proxy");
+ toml::from_str(&s).expect("deserialise DiskImage from existing path")
+}
+
+fn build_config(args: &Args) -> VmConfig {
+ let stealth = StealthConfig::generate();
+ let disk = existing_disk(args.disk.clone());
+ let net = NetworkInterface::new(args.tap.clone(), args.mac, "e1000");
+ let cfg = VmConfig::new(
+ &args.name,
+ args.vcpus,
+ VmConfig::gib_to_bytes(args.memory_gib),
+ )
+ .with_disk(disk)
+ .with_network(net)
+ .with_display(args.display.clone())
+ .with_extra_args(stealth.build());
+ match &args.firmware {
+ Some(fw) => cfg.with_firmware(fw.clone()),
+ None => cfg,
+ }
+}
+
+fn shell_quote(s: &str) -> String {
+ // Wrap in single quotes if the string contains any shell-special characters.
+ // Single-quote the whole value; a literal ' inside is escaped as '\'' .
+ let needs_quoting = s.chars().any(|c| {
+ matches!(
+ c,
+ ' ' | '\t'
+ | '('
+ | ')'
+ | ','
+ | ';'
+ | '&'
+ | '|'
+ | '<'
+ | '>'
+ | '$'
+ | '`'
+ | '\\'
+ | '"'
+ | '\''
+ )
+ });
+ if needs_quoting {
+ format!("'{}'", s.replace('\'', r"'\''"))
+ } else {
+ s.to_owned()
+ }
+}
+
+fn print_command(config: &VmConfig) {
+ let args = QemuBackend::build_args(config);
+ let parts: Vec<_> = args
+ .iter()
+ .map(|a| shell_quote(&a.to_string_lossy()))
+ .collect();
+ println!("qemu-system-x86_64 {}", parts.join(" "));
+}
+
+async fn start_vm(config: VmConfig) {
+ let mut vm = Vm::new(config, QemuBackend);
+ match vm.start().await {
+ Ok(()) => {
+ let pid = vm.pid().expect("PID set after start");
+ println!("VM started (PID {pid})");
+ println!("QMP socket: {}", vm.config().qmp_socket().display());
+ println!("Stop with: xenith-vm stop {}", vm.config().name());
+ }
+ Err(e) => {
+ eprintln!("error: {e}");
+ std::process::exit(1);
+ }
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let args = match parse_args() {
+ Ok(a) => a,
+ Err(e) => {
+ eprintln!("error: {e}");
+ eprintln!("{USAGE}");
+ std::process::exit(1);
+ }
+ };
+
+ let config = build_config(&args);
+
+ if args.dry_run {
+ print_command(&config);
+ } else {
+ start_vm(config).await;
+ }
+}
diff --git a/xenith-vm/src/backend.rs b/xenith-vm/src/backend.rs
new file mode 100644
index 0000000..1121739
--- /dev/null
+++ b/xenith-vm/src/backend.rs
@@ -0,0 +1,55 @@
+pub mod qemu;
+
+use std::future::Future;
+
+use crate::config::VmConfig;
+use crate::error::Error as VmError;
+
+/// Abstraction over a hypervisor backend.
+///
+/// Implementing this trait for a new backend (e.g. cloud-hypervisor, Firecracker)
+/// does not require changes to the `Vm` public API.
+///
+/// # Object safety
+///
+/// This trait is **not object-safe** because each method returns `impl Future<…>`
+/// (RPITIT). Use it with static dispatch (generics) only. If dynamic dispatch is
+/// needed, wrap the concrete backend in an `Arc` and introduce a boxed-future
+/// variant of this trait.
+pub trait VmBackend: Send + Sync {
+ /// Build the process and start the VM. Returns the OS PID.
+ fn start(&self, config: &VmConfig) -> impl Future