From b27faaaa45919d64d680b1a4e2bbd7e056b60d00 Mon Sep 17 00:00:00 2001 From: Leander Kohler Date: Wed, 18 Mar 2026 13:36:33 +0100 Subject: [PATCH] vm-migration: add protocol versioning Add protocol-side support for migration protocol versioning. Use the existing 6-byte Start command header, which was previously zero padding, to carry the sender's migration protocol version without changing the wire layout. Store the version as a little-endian u16 in the first two bytes and ignore the remaining four bytes. A zeroed command header continues to mean a legacy v0 sender. This keeps the message flow unchanged for rollout: Start is still followed by plain OK or Error (Aborted), and no new command is needed. On-behalf-of: SAP leander.kohler@sap.com Signed-off-by: Leander Kohler --- vm-migration/src/protocol.rs | 59 +++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/vm-migration/src/protocol.rs b/vm-migration/src/protocol.rs index 7c14ec7bf..e3f899759 100644 --- a/vm-migration/src/protocol.rs +++ b/vm-migration/src/protocol.rs @@ -73,8 +73,22 @@ //! 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}; +use std::mem::size_of; +use std::ops::RangeInclusive; use std::{mem, slice}; use itertools::Itertools; @@ -127,18 +141,33 @@ pub enum Command { CompletePaused = 8, } +/// Newest migration protocol version sent by this implementation. +pub const CURRENT_PROTOCOL_VERSION: u16 = 0; + +/// Returns the current migration protocol version and the previous version, if any. +pub fn supported_protocol_versions() -> RangeInclusive { + CURRENT_PROTOCOL_VERSION.saturating_sub(1)..=CURRENT_PROTOCOL_VERSION +} + #[repr(C)] #[derive(Default, Copy, Clone)] pub struct Request { command: Command, - padding: [u8; 6], - length: u64, // Length of payload for command excluding the Request struct + command_headers: [u8; 6], + /// Length of payload for command excluding the Request struct + length: u64, } // SAFETY: Request contains a series of integers with no implicit padding unsafe impl ByteValued for Request {} impl Request { + fn encode_sender_version(version: u16) -> [u8; 6] { + let mut command_headers = [0; 6]; + command_headers[..size_of::()].copy_from_slice(&version.to_le_bytes()); + command_headers + } + pub fn new(command: Command, length: u64) -> Self { Self { command, @@ -148,7 +177,11 @@ impl Request { } pub fn start() -> Self { - Self::new(Command::Start, 0) + Self { + command: Command::Start, + command_headers: Self::encode_sender_version(CURRENT_PROTOCOL_VERSION), + length: 0, + } } pub fn state(length: u64) -> Self { @@ -189,6 +222,10 @@ impl Request { self.length } + pub fn command_headers(&self) -> &[u8; 6] { + &self.command_headers + } + pub fn read_from(fd: &mut dyn Read) -> Result { let mut request = Request::default(); fd.read_exact(Self::as_mut_slice(&mut request)) @@ -485,7 +522,21 @@ 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!( + u16::from_le_bytes([request.command_headers()[0], request.command_headers()[1]]), + 1 + ); + } #[test] fn test_memory_range_table_from_dirty_ranges_iter() {