diff --git a/cloud-hypervisor/src/main.rs b/cloud-hypervisor/src/main.rs index dfac20138f..fed616aa66 100644 --- a/cloud-hypervisor/src/main.rs +++ b/cloud-hypervisor/src/main.rs @@ -881,6 +881,11 @@ fn main() { if cmd_arguments.get_flag("version") { println!("{} {}", env!("CARGO_BIN_NAME"), env!("BUILD_VERSION")); + println!( + "vm-migration protocol versions v{}-v{}", + vm_migration::protocol::PRIOR_PROTOCOL_VERSION, + vm_migration::protocol::CURRENT_PROTOCOL_VERSION + ); if cmd_arguments.get_count("v") != 0 { println!("Enabled features: {:?}", vmm::feature_list()); diff --git a/vm-migration/src/protocol.rs b/vm-migration/src/protocol.rs index 012284f858..d68a83b50a 100644 --- a/vm-migration/src/protocol.rs +++ b/vm-migration/src/protocol.rs @@ -73,6 +73,19 @@ //! Source->>Destination: Complete //! Destination-->>Source: OK //! ``` +//! +//! ## Protocol Versioning +//! +//! `Start` carries the sender's migration protocol version. +//! A zeroed version field is treated as legacy protocol `v0`. +//! +//! The destination validates that version and replies with a plain `OK` or +//! `Error`. +//! +//! Only the current and immediately previous protocol versions are +//! supported. Compatibility is one-way, from older protocol versions +//! to newer ones. +//! use std::io::{Read, Write}; @@ -124,11 +137,25 @@ pub enum Command { KeepAlive, } +pub type MigrationProtocolVersion = u16; + +pub const CURRENT_PROTOCOL_VERSION: MigrationProtocolVersion = 0; +pub const PRIOR_PROTOCOL_VERSION: MigrationProtocolVersion = + CURRENT_PROTOCOL_VERSION.saturating_sub(1); + +/// Encodes a sender protocol version into the `command_headers` bytes of a +/// `Start` request. +fn encode_sender_version(version: MigrationProtocolVersion) -> [u8; 6] { + let mut command_headers = [0; 6]; + command_headers[..2].copy_from_slice(&version.to_le_bytes()); + command_headers +} + #[repr(C)] #[derive(Default, Copy, Clone, Immutable, IntoBytes, KnownLayout, TryFromBytes)] pub struct Request { command: Command, - padding: [u8; 6], + command_headers: [u8; 6], length: u64, // Length of payload for command excluding the Request struct } @@ -142,7 +169,11 @@ impl Request { } pub fn start() -> Self { - Self::new(Command::Start, 0) + Self { + command: Command::Start, + command_headers: encode_sender_version(CURRENT_PROTOCOL_VERSION), + length: 0, + } } pub fn state(length: u64) -> Self { @@ -181,6 +212,36 @@ impl Request { self.length } + pub fn command_headers(&self) -> &[u8; 6] { + &self.command_headers + } + + /// Returns the sender protocol version carried by a `Start` request. + /// + /// Only the first two bytes are used. The remaining bytes are ignored. + pub fn sender_protocol_version(&self) -> MigrationProtocolVersion { + u16::from_le_bytes([self.command_headers[0], self.command_headers[1]]) + } + + pub fn validate_start_protocol_version( + &self, + ) -> Result { + debug_assert_eq!( + self.command(), + Command::Start, + "validate_start_protocol_version() must only be called for Start requests", + ); + + let sender_version = self.sender_protocol_version(); + if sender_version != PRIOR_PROTOCOL_VERSION && sender_version != CURRENT_PROTOCOL_VERSION { + return Err(MigratableError::MigrateReceive(anyhow!( + "Migration protocol version {sender_version} doesn't match supported versions: {PRIOR_PROTOCOL_VERSION}, {CURRENT_PROTOCOL_VERSION}" + ))); + } + + Ok(sender_version) + } + pub fn read_from(fd: &mut dyn Read) -> Result { /// A byte buffer that matches `Self` in size and alignment to allow deserializing `Self` into. #[repr(C, align(8))] @@ -502,7 +563,29 @@ impl MemoryRangeTable { #[cfg(test)] mod unit_tests { - use crate::protocol::{MemoryRange, MemoryRangeTable}; + use crate::protocol::{Command, MemoryRange, MemoryRangeTable, Request}; + + #[test] + fn test_start_request_ignores_residual_command_headers_bytes() { + let request = Request { + command: Command::Start, + command_headers: [1, 0, 0xaa, 0xbb, 0xcc, 0xdd], + length: 0, + }; + + assert_eq!(request.sender_protocol_version(), 1); + } + + #[test] + fn test_validate_start_protocol_version_rejects_unsupported_version() { + let request = Request { + command: Command::Start, + command_headers: [2, 0, 0, 0, 0, 0], + length: 0, + }; + + request.validate_start_protocol_version().unwrap_err(); + } #[test] fn test_memory_range_table_from_dirty_ranges_iter() { diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index 85c57d9190..b83018bcc2 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -2179,7 +2179,11 @@ impl Vmm { match state { Established => match req.command() { - Command::Start => Ok(Started), + Command::Start => { + let migration_protocol_version = req.validate_start_protocol_version()?; + info!("Using migration protocol v{migration_protocol_version} on the receiver"); + Ok(Started) + } _ => invalid_command(), }, Started => match req.command() { @@ -2769,6 +2773,7 @@ impl Vmm { &mut socket, MigratableError::MigrateSend(anyhow!("Error starting migration (got bad response)")), )?; + info!("Using migration protocol v{CURRENT_PROTOCOL_VERSION} on the sender"); return_if_cancelled_cb(&mut socket)?;