From 58baee16acf3319c81a4c8c36f24126b4931539a Mon Sep 17 00:00:00 2001 From: Sebastian Eydam Date: Wed, 15 Apr 2026 10:32:02 +0200 Subject: [PATCH] vmm: add TLS API option to receive migration call As we now have more than one parameter for the receive migration call, this commit also adds parsing and validation for those parameters. We maintain backwards compatibility by also correctly parsing the case where the caller only provides a URL. On-behalf-of: SAP sebastian.eydam@sap.com Signed-off-by: Sebastian Eydam --- cloud-hypervisor/src/bin/ch-remote.rs | 18 ++-- cloud-hypervisor/tests/common/utils.rs | 2 +- cloud-hypervisor/tests/integration.rs | 4 +- vmm/src/api/mod.rs | 112 ++++++++++++++++++++++ vmm/src/api/openapi/cloud-hypervisor.yaml | 6 ++ 5 files changed, 130 insertions(+), 12 deletions(-) diff --git a/cloud-hypervisor/src/bin/ch-remote.rs b/cloud-hypervisor/src/bin/ch-remote.rs index 22d81e26a..3c1b32606 100644 --- a/cloud-hypervisor/src/bin/ch-remote.rs +++ b/cloud-hypervisor/src/bin/ch-remote.rs @@ -71,6 +71,8 @@ enum Error { ReadingFile(#[source] std::io::Error), #[error("Invalid disk size")] InvalidDiskSize(#[source] ByteSizedParseError), + #[error("Error parsing receive migration configuration")] + ReceiveMigrationConfig(#[from] vmm::api::VmReceiveMigrationConfigError), #[error("Error parsing send migration configuration")] SendMigrationConfig(#[from] vmm::api::VmSendMigrationConfigError), } @@ -534,7 +536,7 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu .unwrap() .get_one::("receive_migration_config") .unwrap(), - ); + )?; simple_api_command( socket, "PUT", @@ -753,7 +755,7 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>) .unwrap() .get_one::("receive_migration_config") .unwrap(), - ); + )?; proxy.api_vm_receive_migration(&receive_migration_data) } Some("create") => { @@ -941,12 +943,10 @@ fn coredump_config(destination_url: &str) -> String { serde_json::to_string(&coredump_config).unwrap() } -fn receive_migration_data(url: &str) -> String { - let receive_migration_data = vmm::api::VmReceiveMigrationData { - receiver_url: url.to_owned(), - }; - - serde_json::to_string(&receive_migration_data).unwrap() +fn receive_migration_data(config: &str) -> Result { + let receive_migration_data = + vmm::api::VmReceiveMigrationData::parse(config).map_err(Error::ReceiveMigrationConfig)?; + Ok(serde_json::to_string(&receive_migration_data).unwrap()) } fn send_migration_data(config: &str) -> Result { @@ -1069,7 +1069,7 @@ fn get_cli_commands_sorted() -> Box<[Command]> { .arg( Arg::new("receive_migration_config") .index(1) - .help(""), + .help(vmm::api::VmReceiveMigrationData::SYNTAX), ), Command::new("remove-device") .about("Remove VFIO and PCI device") diff --git a/cloud-hypervisor/tests/common/utils.rs b/cloud-hypervisor/tests/common/utils.rs index 4139df727..43fd02c3b 100644 --- a/cloud-hypervisor/tests/common/utils.rs +++ b/cloud-hypervisor/tests/common/utils.rs @@ -1136,7 +1136,7 @@ pub(crate) fn start_live_migration( .args([ &format!("--api-socket={dest_api_socket}"), "receive-migration", - &format! {"unix:{migration_socket}"}, + &format!("receiver_url=unix:{migration_socket}"), ]) .stderr(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/cloud-hypervisor/tests/integration.rs b/cloud-hypervisor/tests/integration.rs index 97f1afbd4..85d168479 100644 --- a/cloud-hypervisor/tests/integration.rs +++ b/cloud-hypervisor/tests/integration.rs @@ -6740,7 +6740,7 @@ mod common_parallel { .args([ &format!("--api-socket={dest_api_socket}"), "receive-migration", - &format!("tcp:0.0.0.0:{migration_port}"), + &format!("receiver_url=tcp:0.0.0.0:{migration_port}"), ]) .stdin(Stdio::null()) .stderr(Stdio::piped()) @@ -7077,7 +7077,7 @@ mod common_parallel { .args([ &format!("--api-socket={dest_api_socket}"), "receive-migration", - &format!("tcp:0.0.0.0:{migration_port}"), + &format!("receiver_url=tcp:0.0.0.0:{migration_port}"), ]) .stdin(Stdio::null()) .stderr(Stdio::piped()) diff --git a/vmm/src/api/mod.rs b/vmm/src/api/mod.rs index 93bc40a5c..2c933820e 100644 --- a/vmm/src/api/mod.rs +++ b/vmm/src/api/mod.rs @@ -270,9 +270,80 @@ pub struct VmCoredumpData { } #[derive(Clone, Deserialize, Serialize, Default, Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct VmReceiveMigrationData { /// URL for the reception of migration state pub receiver_url: String, + /// Directory containing the TLS server certificate (server-cert.pem), the TLS server key (server-key.pem), and the client TLS root CA certificate (ca-cert.pem). + #[serde(default)] + pub tls_dir: Option, +} + +#[derive(Debug, Error)] +pub enum VmReceiveMigrationConfigError { + #[error("Error parsing receive migration parameters")] + ParseError(#[source] OptionParserError), + + #[error("Error validating receive migration parameters")] + ValidationError(String), +} + +impl VmReceiveMigrationData { + pub const SYNTAX: &'static str = "VM receive migration parameters \ + \"\" or \"receiver_url=[,tls_dir=]\""; + + pub fn parse(migration: &str) -> Result { + let mut parser = OptionParser::new(); + parser.add("receiver_url").add("tls_dir"); + parser + .parse(migration) + .map_err(VmReceiveMigrationConfigError::ParseError)?; + + let receiver_url = parser.get("receiver_url").ok_or_else(|| { + VmReceiveMigrationConfigError::ParseError(OptionParserError::InvalidSyntax( + "receiver_url is required".to_string(), + )) + })?; + let tls_dir = parser + .convert::("tls_dir") + .map_err(VmReceiveMigrationConfigError::ParseError)? + .map(|path| PathBuf::from(&path)); + + let data = Self { + receiver_url, + tls_dir, + }; + + data.validate()?; + + Ok(data) + } + + pub fn validate(&self) -> Result<(), VmReceiveMigrationConfigError> { + if let Some(addr) = self.receiver_url.strip_prefix("tcp:") { + tcp_address_to_server_name(addr).map_err(|e| { + VmReceiveMigrationConfigError::ValidationError(format!( + "receiver_url must use tcp:: or unix:: {e}." + )) + })?; + } else if self + .receiver_url + .strip_prefix("unix:") + .is_some_and(|path| !path.is_empty()) + { + if self.tls_dir.is_some() { + return Err(VmReceiveMigrationConfigError::ValidationError( + "UNIX sockets and TLS encryption cannot be used at the same time.".to_string(), + )); + } + } else { + return Err(VmReceiveMigrationConfigError::ValidationError( + "receiver_url must use tcp:: or unix:.".to_string(), + )); + } + + Ok(()) + } } #[derive(Copy, Clone, Default, Deserialize, Serialize, Debug, PartialEq, Eq)] @@ -1780,6 +1851,47 @@ impl ApiAction for VmNmi { mod unit_tests { use super::*; + #[test] + fn test_vm_receive_migration_data_parse() { + let data = VmReceiveMigrationData::parse("receiver_url=tcp:192.168.1.1:8080").unwrap(); + assert_eq!( + data, + VmReceiveMigrationData { + receiver_url: "tcp:192.168.1.1:8080".to_string(), + tls_dir: None, + } + ); + + let data = VmReceiveMigrationData::parse("receiver_url=tcp:[2001:db8::1]:8080").unwrap(); + assert_eq!(data.receiver_url, "tcp:[2001:db8::1]:8080"); + + let data = + VmReceiveMigrationData::parse("receiver_url=tcp:destination.example:8080").unwrap(); + assert_eq!(data.receiver_url, "tcp:destination.example:8080"); + + let data = VmReceiveMigrationData::parse("receiver_url=unix:/tmp/ch=migrate.sock").unwrap(); + assert_eq!(data.receiver_url, "unix:/tmp/ch=migrate.sock"); + + let tls_dir = std::env::temp_dir(); + let data = VmReceiveMigrationData::parse(&format!( + "receiver_url=tcp:192.168.1.1:8080,tls_dir={}", + tls_dir.display() + )) + .unwrap(); + assert_eq!( + data, + VmReceiveMigrationData { + receiver_url: "tcp:192.168.1.1:8080".to_string(), + tls_dir: Some(tls_dir), + } + ); + + VmReceiveMigrationData::parse("receiver_url=file:///tmp/migration").unwrap_err(); + VmReceiveMigrationData::parse("receiver_url=tcp:192.168.1.1").unwrap_err(); + VmReceiveMigrationData::parse("receiver_url=tcp:[2001:db8::1]").unwrap_err(); + VmReceiveMigrationData::parse("receiver_url=unix:/tmp/sock,tls_dir=/tmp").unwrap_err(); + } + #[test] fn test_vm_send_migration_data_parse() { // Fully specified diff --git a/vmm/src/api/openapi/cloud-hypervisor.yaml b/vmm/src/api/openapi/cloud-hypervisor.yaml index 4ed651ced..e27117528 100644 --- a/vmm/src/api/openapi/cloud-hypervisor.yaml +++ b/vmm/src/api/openapi/cloud-hypervisor.yaml @@ -1506,6 +1506,12 @@ components: properties: receiver_url: type: string + tls_dir: + type: string + description: > + Directory containing the TLS server certificate (server-cert.pem), the TLS + server key (server-key.pem), and the client TLS root CA certificate (ca-cert.pem). + TLS is only supported with tcp:: receiver URLs. TimeoutStrategy: type: string