Files
cloud-hypervisor/vmm/src/migration/transport.rs
Philipp Schuster 013981b649 vm-migration: improve debuggability on receiver for failed migrations
We cannot reliably send Request::abandon() on every kind of failure on
the sender side, as we might be in the middle of a memory transmission.
The receiver would not reliably know what to do with that. So instead,
when the receiver cannot read from the socket, we log that the migration
sender failed, which is the only likely cause of that failure.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-30 16:59:20 +00:00

1332 lines
48 KiB
Rust

// Copyright © 2026 Contributors to the Cloud Hypervisor project
//
// SPDX-License-Identifier: Apache-2.0
//
use std::io::{self, ErrorKind, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::num::{NonZeroU32, ParseIntError};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::result::Result;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, Sender, SyncSender, TrySendError, channel, sync_channel};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::{mem, thread};
use anyhow::{Context, anyhow};
use log::{debug, error, info, warn};
use seccompiler::{BpfProgram, SeccompAction, apply_filter};
use serde_json;
use thiserror::Error;
use vm_memory::bitmap::BitmapSlice;
use vm_memory::{
Bytes, GuestAddress, GuestAddressSpace, GuestMemoryAtomic, ReadVolatile, VolatileMemoryError,
VolatileSlice, WriteVolatile,
};
use vm_migration::protocol::{Command, ConnectionRole, MemoryRangeTable, Request, Response};
use vm_migration::tls::{TlsServerConfig, TlsStream};
use vm_migration::{MigratableError, Snapshot};
use vmm_sys_util::eventfd::EventFd;
use crate::seccomp_filters::{Thread, get_seccomp_filter};
use crate::sync_utils::Gate;
use crate::{GuestMemoryMmap, VmMigrationConfig};
/// Hard upper bound for migration worker connections on both the sender and
/// receiver side.
pub(crate) const MAX_MIGRATION_CONNECTIONS: u32 = 128;
/// Transport-agnostic listener used to receive connections.
#[derive(Debug)]
pub(crate) enum ReceiveListener {
Tcp(TcpListener),
Unix(UnixListener),
Tls(TcpListener, TlsServerConfig),
}
impl ReceiveListener {
/// Block until a connection is accepted.
pub(crate) fn accept(&mut self) -> Result<SocketStream, MigratableError> {
match self {
ReceiveListener::Tcp(listener) => listener
.accept()
.map(|(socket, _)| SocketStream::Tcp(socket))
.context("Failed to accept TCP migration connection")
.map_err(MigratableError::MigrateReceive),
ReceiveListener::Unix(listener) => listener
.accept()
.map(|(socket, _)| SocketStream::Unix(socket))
.context("Failed to accept Unix migration connection")
.map_err(MigratableError::MigrateReceive),
ReceiveListener::Tls(listener, config) => {
let (socket, _) = listener
.accept()
.context("Failed to accept TCP connection")
.map_err(MigratableError::MigrateReceive)?;
TlsStream::new_server(socket, config)
.map(Box::new)
.map(SocketStream::Tls)
.context("Failed to accept TLS migration connection")
.map_err(MigratableError::MigrateReceive)
.inspect(|_| info!("TLS handshake (server-side/receiver) successful"))
}
}
}
/// Same as [`Self::accept`], but returns `None` if the abort event was signaled.
fn abortable_accept(
&mut self,
abort_event: &impl AsRawFd,
) -> Result<Option<SocketStream>, MigratableError> {
if wait_for_readable(&self, abort_event)
.context("Error while waiting for socket to become readable")
.map_err(MigratableError::MigrateReceive)?
{
// The listener is readable; accept the connection.
Ok(Some(self.accept()?))
} else {
// The abort event was signaled before any connection arrived.
Ok(None)
}
}
/// Tries to clone a [`ReceiveListener`].
pub(crate) fn try_clone(&self) -> Result<Self, MigratableError> {
match self {
ReceiveListener::Tcp(listener) => listener
.try_clone()
.map(ReceiveListener::Tcp)
.context("Failed to clone TCP listener")
.map_err(MigratableError::MigrateReceive),
ReceiveListener::Unix(listener) => listener
.try_clone()
.map(ReceiveListener::Unix)
.context("Failed to clone Unix listener")
.map_err(MigratableError::MigrateReceive),
ReceiveListener::Tls(listener, config) => listener
.try_clone()
.map(|listener| ReceiveListener::Tls(listener, config.clone()))
.context("Failed to clone TLS listener")
.map_err(MigratableError::MigrateReceive),
}
}
}
impl AsFd for ReceiveListener {
fn as_fd(&self) -> BorrowedFd<'_> {
match self {
ReceiveListener::Tcp(listener) => listener.as_fd(),
ReceiveListener::Unix(listener) => listener.as_fd(),
ReceiveListener::Tls(listener, _) => listener.as_fd(),
}
}
}
/// Transport-agnostic stream used by the migration protocol.
pub(crate) enum SocketStream {
Unix(UnixStream),
Tcp(TcpStream),
Tls(Box<TlsStream>),
}
impl Read for SocketStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
SocketStream::Unix(stream) => stream.read(buf),
SocketStream::Tcp(stream) => stream.read(buf),
SocketStream::Tls(stream) => stream.read(buf),
}
}
}
impl Write for SocketStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
SocketStream::Unix(stream) => stream.write(buf),
SocketStream::Tcp(stream) => stream.write(buf),
SocketStream::Tls(stream) => stream.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
SocketStream::Unix(stream) => stream.flush(),
SocketStream::Tcp(stream) => stream.flush(),
SocketStream::Tls(stream) => stream.flush(),
}
}
}
impl SocketStream {
pub(crate) fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
match self {
SocketStream::Unix(s) => s.set_read_timeout(dur),
SocketStream::Tcp(s) => s.set_read_timeout(dur),
SocketStream::Tls(s) => {
let fd = s.as_fd().as_raw_fd();
// SAFETY: fd is borrowed from the TLS stream and forgotten
// immediately. The TLS stream retains ownership.
let tcp = unsafe { TcpStream::from_raw_fd(fd) };
let r = tcp.set_read_timeout(dur);
mem::forget(tcp);
r
}
}
}
pub(crate) fn set_nodelay(&self, nodelay: bool) -> io::Result<()> {
match self {
SocketStream::Unix(_) => Ok(()),
SocketStream::Tcp(s) => s.set_nodelay(nodelay),
SocketStream::Tls(s) => {
let fd = s.as_fd().as_raw_fd();
// SAFETY: fd is borrowed from the TLS stream and forgotten
// immediately. The TLS stream retains ownership.
let tcp = unsafe { TcpStream::from_raw_fd(fd) };
let r = tcp.set_nodelay(nodelay);
mem::forget(tcp);
r
}
}
}
}
impl AsFd for SocketStream {
fn as_fd(&self) -> BorrowedFd<'_> {
match self {
SocketStream::Unix(s) => s.as_fd(),
SocketStream::Tcp(s) => s.as_fd(),
SocketStream::Tls(s) => s.as_fd(),
}
}
}
impl ReadVolatile for SocketStream {
fn read_volatile<B: BitmapSlice>(
&mut self,
buf: &mut VolatileSlice<B>,
) -> Result<usize, VolatileMemoryError> {
match self {
SocketStream::Unix(s) => s.read_volatile(buf),
SocketStream::Tcp(s) => s.read_volatile(buf),
SocketStream::Tls(s) => s.read_volatile(buf),
}
}
}
impl WriteVolatile for SocketStream {
fn write_volatile<B: BitmapSlice>(
&mut self,
buf: &VolatileSlice<B>,
) -> Result<usize, VolatileMemoryError> {
match self {
SocketStream::Unix(s) => s.write_volatile(buf),
SocketStream::Tcp(s) => s.write_volatile(buf),
SocketStream::Tls(s) => s.write_volatile(buf),
}
}
}
// Wait for `fd` to become readable. In this case, we return true. In case
// `abort_event` was signaled, return false.
fn wait_for_readable(fd: &impl AsFd, abort_event: &impl AsRawFd) -> Result<bool, io::Error> {
let fd = fd.as_fd().as_raw_fd();
let abort_event = abort_event.as_raw_fd();
let mut poll_fds = [
libc::pollfd {
fd: abort_event,
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
},
];
loop {
// SAFETY: This is safe, because the file descriptors are valid and the
// poll_fds array is properly initialized.
let ret = unsafe { libc::poll(poll_fds.as_mut_ptr(), poll_fds.len() as libc::nfds_t, -1) };
if ret >= 0 {
break;
}
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(err);
}
if poll_fds[0].revents & libc::POLLIN != 0 {
return Ok(false);
}
if poll_fds[1].revents & libc::POLLIN != 0 {
return Ok(true);
}
Err(io::Error::other(
"Poll returned, but neither file descriptor is readable?",
))
}
/// Coordinates the variable number of receive side TCP connections for
/// memory transfer and fault handling.
///
/// VM migration uses one main connection plus a bounded number of additional
/// connections. A dedicated accept thread classifies each auxiliary socket by
/// its [`ConnectionRole`]:
///
/// - fault sockets are handed to the main migration thread through `fault_tx`
/// - precopy memory sockets get a worker thread that receives
/// [`Command::Memory`] requests and writes them into guest memory.
///
/// The accept thread joins all memory workers before exiting when
/// [`Self::cleanup`] is called.
#[derive(Debug)]
pub(crate) struct ReceiveAdditionalConnections {
accept_thread: Option<thread::JoinHandle<Result<(), MigratableError>>>,
/// Shared termination event for the accept thread and memory workers.
terminate_fd: EventFd,
}
impl ReceiveAdditionalConnections {
/// Starts the auxiliary migration receive path.
///
/// The caller must call [`Self::cleanup`] once migration finishes so the
/// accept thread and its memory workers are signaled and joined.
pub(crate) fn new(
listener: ReceiveListener,
guest_memory: GuestMemoryAtomic<GuestMemoryMmap>,
fault_tx: Sender<SocketStream>,
seccomp_action: &SeccompAction,
) -> Result<Self, MigratableError> {
let event_fd = EventFd::new(0)
.context("Error creating terminate fd")
.map_err(MigratableError::MigrateReceive)?;
let terminate_fd = event_fd
.try_clone()
.context("Error cloning terminate fd")
.map_err(MigratableError::MigrateReceive)?;
let seccomp_filter = get_seccomp_filter(seccomp_action, Thread::MigrationTcpWorker, None)
.context("Error creating migration TCP worker seccomp filter")
.map_err(MigratableError::MigrateReceive)?;
let accept_thread = thread::Builder::new()
.name("migrate-receive-accept-connections".to_owned())
.spawn(move || {
if !seccomp_filter.is_empty() {
apply_filter(&seccomp_filter)
.context("Error applying migration TCP worker seccomp filter")
.map_err(MigratableError::MigrateReceive)?;
}
Self::accept_connections(
listener,
&terminate_fd,
&guest_memory,
&fault_tx,
&seccomp_filter,
)
})
.context("Error creating connection accept thread")
.map_err(MigratableError::MigrateReceive)?;
Ok(Self {
accept_thread: Some(accept_thread),
terminate_fd: event_fd,
})
}
/// Accepts any incoming migration connections and dispatches them by
/// role.
///
/// Runs on the accept thread created by [`Self::new`]. It accepts sockets
/// until termination is signaled, accepting/classifying a socket fails, or
/// the memory connection limit is exceeded.
///
/// Each socket must send a [`ConnectionRole`] header soon after connecting.
/// Invalid sockets are dropped. Fault sockets are sent through `fault_tx`.
/// Precopy memory sockets get a worker thread that receives memory ranges
/// until EOF, error, or termination.
fn accept_connections(
mut listener: ReceiveListener,
terminate_fd: &EventFd,
guest_memory: &GuestMemoryAtomic<GuestMemoryMmap>,
fault_tx: &Sender<SocketStream>,
seccomp_filter: &BpfProgram,
) -> Result<(), MigratableError> {
let mut threads = Vec::new();
let first_err = Self::accept_connections_loop(
&mut listener,
terminate_fd,
guest_memory,
fault_tx,
&mut threads,
seccomp_filter,
);
if first_err.is_err() {
warn!("Signaling termination due to an error while accepting connections.");
let _ = terminate_fd.write(1);
}
debug!("Stopped accepting additional connections. Cleaning up threads.");
Self::join_memory_threads(threads, first_err)
}
/// Runs the accept loop and spawns workers for memory connections.
///
/// This accepts at most [`MAX_MIGRATION_CONNECTIONS`] precopy memory
/// connections, depending on how many additional connections the migration
/// sender opens. Fault connections (postcopy) are handed off to the main
/// migration thread and do not spawn workers.
fn accept_connections_loop(
listener: &mut ReceiveListener,
terminate_fd: &EventFd,
guest_memory: &GuestMemoryAtomic<GuestMemoryMmap>,
fault_tx: &Sender<SocketStream>,
threads: &mut Vec<thread::JoinHandle<Result<(), MigratableError>>>,
seccomp_filter: &BpfProgram,
) -> Result<(), MigratableError> {
loop {
let socket = listener.abortable_accept(terminate_fd)?;
let Some(socket) = socket else {
return Ok(());
};
if threads.len() >= MAX_MIGRATION_CONNECTIONS as usize {
return Err(MigratableError::MigrateReceive(anyhow!(
"Received more than {MAX_MIGRATION_CONNECTIONS} additional migration connections."
)));
}
let Some(socket) = Self::handle_accepted_connection(socket, fault_tx)? else {
continue;
};
let thread = Self::spawn_memory_worker(
socket,
threads.len(),
terminate_fd,
guest_memory.clone(),
seccomp_filter,
)?;
threads.push(thread);
}
}
/// Classifies a newly accepted auxiliary socket.
///
/// Returns `Some(socket)` for precopy memory connections. Invalid sockets
/// are dropped, and fault sockets (postcopy) are handed to the main
/// migration thread.
fn handle_accepted_connection(
mut socket: SocketStream,
fault_tx: &Sender<SocketStream>,
) -> Result<Option<SocketStream>, MigratableError> {
// Timeout the role header so one stalled peer cannot block the accept
// thread from handling other connections.
socket
.set_read_timeout(Some(Duration::from_secs(10)))
.map_err(MigratableError::MigrateSocket)?;
let role = match ConnectionRole::read_from(&mut socket) {
Ok(role) => role,
Err(e) => {
warn!("Dropping connection: failed to read connection role: {e}");
return Ok(None);
}
};
socket
.set_read_timeout(None)
.map_err(MigratableError::MigrateSocket)?;
match role {
ConnectionRole::Invalid => {
warn!("Dropping connection: invalid connection role");
Ok(None)
}
ConnectionRole::Fault => {
fault_tx.send(socket).map_err(|e| {
MigratableError::MigrateReceive(anyhow!(
"Failed to hand off fault connection: {e}"
))
})?;
Ok(None)
}
ConnectionRole::PrecopyMemory => Ok(Some(socket)),
}
}
fn spawn_memory_worker(
mut socket: SocketStream,
index: usize,
terminate_fd: &EventFd,
guest_memory: GuestMemoryAtomic<GuestMemoryMmap>,
seccomp_filter: &BpfProgram,
) -> Result<thread::JoinHandle<Result<(), MigratableError>>, MigratableError> {
let terminate_fd = terminate_fd
.try_clone()
.context("Error cloning terminate fd")
.map_err(MigratableError::MigrateReceive)?;
let seccomp_filter_t = seccomp_filter.clone();
thread::Builder::new()
.name(format!("migrate-receive-memory-{index}"))
.spawn(move || {
if !seccomp_filter_t.is_empty() {
apply_filter(&seccomp_filter_t)
.context("Error applying migration TCP worker seccomp filter")
.map_err(MigratableError::MigrateReceive)?;
}
Self::worker_receive_memory(&mut socket, &terminate_fd, &guest_memory)
})
.map_err(|e| {
error!("Error spawning receive-memory thread: {e}");
MigratableError::MigrateReceive(
anyhow!(e).context("Error spawning receive-memory thread"),
)
})
}
fn join_memory_threads(
threads: Vec<thread::JoinHandle<Result<(), MigratableError>>>,
mut first_err: Result<(), MigratableError>,
) -> Result<(), MigratableError> {
for thread in threads {
let err = match thread.join() {
Ok(Ok(())) => None,
Ok(Err(e)) => Some(e),
Err(panic) => Some(MigratableError::MigrateReceive(anyhow!(
"receive-memory thread panicked: {panic:?}"
))),
};
if let Some(e) = err {
warn!("Error in receive-memory thread: {e}");
if first_err.is_ok() {
first_err = Err(e);
}
}
}
first_err
}
/// Handles a [`Command::Memory`] request by writing its payload to the VM
/// memory.
fn worker_receive_memory(
mut socket: &mut SocketStream,
terminate_fd: &EventFd,
guest_memory: &GuestMemoryAtomic<GuestMemoryMmap>,
) -> Result<(), MigratableError> {
loop {
// We only check whether we should abort when waiting for a new request. If the
// sender stops sending data mid-request, we will hang forever.
if !wait_for_readable(socket, terminate_fd)
.context("Failed to poll fds")
.map_err(MigratableError::MigrateReceive)?
{
debug!("Got signal to tear down connection.");
return Ok(());
}
let req = match Request::read_from(&mut socket) {
Ok(req) => req,
Err(MigratableError::MigrateSocket(io_error))
if io_error.kind() == ErrorKind::UnexpectedEof =>
{
// EOF is only handled here while reading the next request
// header. Each memory chunk is fully received and acked
// before the worker loops back to Request::read_from(), so
// EOF at this point means the sender finished sending
// memory rather than dropping a chunk mid-transfer (happy
// path) or the sender failed (error path).
debug!("Connection closed by peer");
return Ok(());
}
Err(e) => return Err(e),
};
if req.command() != Command::Memory {
error!(
"Dropping connection. Only Memory commands are allowed on additional connections."
);
return Err(MigratableError::MigrateReceive(anyhow!(
"Received non memory command on migration receive worker: {:?}",
req.command()
)));
}
receive_memory_ranges(guest_memory, &req, socket)?;
Response::ok().write_to(socket)?;
}
}
/// Signals to the worker threads that the migration is finished and joins them.
/// If any thread encountered an error, this error is returned by this function.
pub(crate) fn cleanup(&mut self) -> Result<(), MigratableError> {
self.terminate_fd
.write(1)
.context("Failed to signal termination to worker threads.")
.map_err(MigratableError::MigrateReceive)?;
let accept_thread = self
.accept_thread
.take()
.context("Error taking accept thread.")
.map_err(MigratableError::MigrateReceive)?;
accept_thread
.join()
.map_err(|panic| {
MigratableError::MigrateReceive(anyhow!(
"Accept connections thread panicked: {panic:?}"
))
})
.flatten()
}
}
impl Drop for ReceiveAdditionalConnections {
fn drop(&mut self) {
if self.accept_thread.is_some() {
warn!(
"ReceiveAdditionalConnections was not cleaned up! Either cleanup() was never called (programming error) or it failed before completing."
);
}
}
}
/// The different kinds of messages we can send to memory sending threads.
#[derive(Debug)]
enum SendMemoryThreadMessage {
/// A chunk of memory that the thread should send to the receiving side of the
/// live migration.
Memory(MemoryRangeTable),
/// A synchronization point after each iteration of sending memory. That way the
/// main thread knows when all memory is sent and acknowledged.
Gate(Arc<Gate>),
/// Sending memory is done and the threads are not needed anymore.
Disconnect,
}
/// The different kinds of messages the main thread can receive from a memory
/// sending thread.
#[derive(Debug)]
enum SendMemoryThreadNotify {
/// A sending thread arrived at the gate. The main thread does not wait at the
/// gate, otherwise we could miss error messages.
Gate,
/// A sending thread encountered an error while sending memory.
Error,
}
/// This struct keeps track of additional threads we use to send VM memory.
pub(crate) struct SendAdditionalConnections {
guest_memory: GuestMemoryAtomic<GuestMemoryMmap>,
threads: Vec<thread::JoinHandle<Result<(), MigratableError>>>,
/// Sender to all workers. The receiver is shared by all workers.
message_tx: SyncSender<SendMemoryThreadMessage>,
/// If an error occurs in one of the memory sending threads, the main thread signals
/// this using this flag. Only the main thread checks this variable, the worker
/// threads will be stopped during cleanup.
worker_error: Arc<AtomicBool>,
/// After the main thread sent all memory chunks to the sender threads, it waits
/// until one of the workers notifies it. Either because an error occurred, or
/// because they arrived at the gate.
notify_rx: Receiver<SendMemoryThreadNotify>,
}
impl SendAdditionalConnections {
/// How many requests can be queued for each connection before the main
/// thread has to wait for workers to catch up. This bounded [`SyncChannel`]
/// provides backpressure, so send_chunk() re-checks worker_error promptly
/// instead of queueing all memory descriptors up front and only noticing
/// failures at the next gate synchronization point.
const BUFFERED_REQUESTS_PER_THREAD: usize = 64;
/// The size of each chunk of memory to send.
///
/// We want to make this large, because each chunk is acknowledged and we wait
/// for the ack before sending the next chunk. The challenge is that if it is
/// _too_ large, we become more sensitive to network issues, like packet drops
/// in individual connections, because large amounts of data can pool when
/// throughput on one connection is temporarily reduced.
///
/// We can consider making this configurable, but a better network protocol that
/// doesn't require ACKs would be more efficient.
///
/// The best-case throughput per connection can be estimated via:
/// chunk_size / (chunk_size / throughput_per_connection + round_trip_time)
///
/// This chunk size together with eight connections is sufficient to saturate a 100G link.
const CHUNK_SIZE: u64 = 64 /* MiB */ << 20;
pub(crate) fn new(
destination: &str,
connections: NonZeroU32,
tls_dir: Option<&Path>,
guest_memory: &GuestMemoryAtomic<GuestMemoryMmap>,
seccomp_filter: &BpfProgram,
) -> Result<Self, MigratableError> {
let mut threads = Vec::new();
let configured_connections = connections.get();
let buffer_size = Self::BUFFERED_REQUESTS_PER_THREAD * configured_connections as usize;
let (message_tx, message_rx) = sync_channel::<SendMemoryThreadMessage>(buffer_size);
let worker_error = Arc::new(AtomicBool::new(false));
let (notify_tx, notify_rx) = channel::<SendMemoryThreadNotify>();
// If one connection is configured, we don't have to create any additional threads.
// In this case the main thread does the sending.
if configured_connections == 1 {
return Ok(Self {
guest_memory: guest_memory.clone(),
threads,
message_tx,
worker_error,
notify_rx,
});
}
let message_rx = Arc::new(Mutex::new(message_rx));
// If we use multiple threads to send memory, the main thread only distributes
// the memory chunks to the workers, but does not send memory anymore. Thus in
// this case we create one additional thread for each connection.
for n in 0..configured_connections {
let mut socket = send_migration_socket(destination, tls_dir)?;
ConnectionRole::PrecopyMemory.write_to(&mut socket)?;
let guest_memory = guest_memory.clone();
let message_rx = message_rx.clone();
let worker_error = worker_error.clone();
let notify_tx = notify_tx.clone();
let seccomp_filter = seccomp_filter.clone();
let thread = thread::Builder::new()
.name(format!("migrate-send-memory-{n}"))
.spawn(move || {
if !seccomp_filter.is_empty() {
apply_filter(&seccomp_filter)
.context("Error applying migration TCP worker seccomp filter")
.map_err(MigratableError::MigrateReceive)?;
}
Self::worker_send_memory(
&mut socket,
&guest_memory,
&message_rx,
&worker_error,
&notify_tx,
)
})
.inspect_err(|_| {
// If an error occurs here, we still do some light cleanup.
for _ in 0..threads.len() {
message_tx.send(SendMemoryThreadMessage::Disconnect).ok();
}
threads.drain(..).for_each(|thread| {
thread.join().ok();
});
})
.context("Error spawning send-memory thread")
.map_err(MigratableError::MigrateSend)?;
threads.push(thread);
}
Ok(Self {
guest_memory: guest_memory.clone(),
threads,
message_tx,
worker_error,
notify_rx,
})
}
fn worker_send_memory(
socket: &mut SocketStream,
guest_memory: &GuestMemoryAtomic<GuestMemoryMmap>,
message_rx: &Mutex<Receiver<SendMemoryThreadMessage>>,
worker_error: &AtomicBool,
notify_tx: &Sender<SendMemoryThreadNotify>,
) -> Result<(), MigratableError> {
loop {
// Every memory sending thread receives messages from the main thread through this
// channel. The lock is necessary to synchronize the multiple consumers. If the
// workers are very quick, lock contention could become a performance issue.
let message = message_rx
.lock()
.map_err(|_| MigratableError::MigrateSend(anyhow!("message_rx mutex is poisoned")))
.inspect_err(|_| {
worker_error.store(true, Ordering::Relaxed);
// We ignore errors during error handling.
notify_tx.send(SendMemoryThreadNotify::Error).ok();
})?
.recv()
.context("Error receiving message from main thread")
.map_err(MigratableError::MigrateSend)
.inspect_err(|_| {
worker_error.store(true, Ordering::Relaxed);
notify_tx.send(SendMemoryThreadNotify::Error).ok();
})?;
match message {
SendMemoryThreadMessage::Memory(table) => {
send_memory_ranges(guest_memory, &table, socket)
.inspect_err(|_| {
worker_error.store(true, Ordering::Relaxed);
notify_tx.send(SendMemoryThreadNotify::Error).ok();
})
.context("Error sending memory to receiver side")
.map_err(MigratableError::MigrateSend)?;
}
SendMemoryThreadMessage::Gate(gate) => {
notify_tx
.send(SendMemoryThreadNotify::Gate)
.context("Error sending gate notification to main thread")
.map_err(MigratableError::MigrateSend)
.inspect_err(|_| {
// Sending via `notify_tx` just failed, so we don't try to send another
// message via it.
worker_error.store(true, Ordering::Relaxed);
})?;
gate.wait();
}
SendMemoryThreadMessage::Disconnect => {
return Ok(());
}
}
}
}
/// Send memory via all connections that we have. `socket` is the original socket
/// that was used to connect to the destination. Returns Ok(true) if memory was
/// sent, Ok(false) if the given table was empty.
///
/// When this function returns, all memory has been sent and acknowledged.
pub(crate) fn send_memory(
&mut self,
table: MemoryRangeTable,
socket: &mut SocketStream,
) -> Result<bool, MigratableError> {
if table.regions().is_empty() {
return Ok(false);
}
// If we use only one connection, we send the memory directly.
if self.threads.is_empty() {
send_memory_ranges(&self.guest_memory, &table, socket)?;
return Ok(true);
}
// The chunk size is chosen to be big enough so that even very fast links need some
// milliseconds to send it.
for chunk in table.partition(Self::CHUNK_SIZE) {
self.send_chunk(chunk)?;
}
self.wait_for_pending_data()?;
Ok(true)
}
fn send_chunk(&mut self, chunk: MemoryRangeTable) -> Result<(), MigratableError> {
let mut chunk = SendMemoryThreadMessage::Memory(chunk);
// [`Self::message_tx`] has a limited size, so we may have to retry sending the chunk
loop {
if self.worker_error.load(Ordering::Relaxed) {
return self.cleanup();
}
// Use try_send() so we can keep checking worker_error while the
// workers catch up. A blocking send() could wait forever if a
// worker failed and stopped making progress.
match self.message_tx.try_send(chunk) {
Ok(()) => {
return Ok(());
}
Err(TrySendError::Full(unsent_chunk)) => {
// The channel is full. We wait for a short time and retry.
thread::sleep(Duration::from_millis(10));
chunk = unsent_chunk;
}
Err(TrySendError::Disconnected(_)) => {
// The workers didn't disconnect for no reason, thus we do a cleanup.
return Err(self.cleanup().err().unwrap_or(MigratableError::MigrateSend(
anyhow!("All sending threads disconnected, but none returned an error?"),
)));
}
}
}
}
/// Wait until all data that is in-flight has actually been sent and acknowledged.
fn wait_for_pending_data(&mut self) -> Result<(), MigratableError> {
let gate = Arc::new(Gate::new());
for _ in 0..self.threads.len() {
self.message_tx
.send(SendMemoryThreadMessage::Gate(gate.clone()))
.context("Error sending gate message to workers")
.map_err(MigratableError::MigrateSend)?;
}
// We cannot simply wait at the gate, otherwise we might miss it when a sender
// thread encounters an error. Thus we wait for the workers to notify us that
// they arrived at the gate.
let mut seen_threads = 0;
loop {
match self
.notify_rx
.recv()
.context("Error receiving message from workers")
.map_err(MigratableError::MigrateSend)?
{
SendMemoryThreadNotify::Gate => {
seen_threads += 1;
if seen_threads == self.threads.len() {
gate.open();
return Ok(());
}
}
SendMemoryThreadNotify::Error => {
// If an error occurred in one of the worker threads, we open
// the gate to make sure that no thread hangs. After that, we
// receive the error from Self::cleanup() and return it.
gate.open();
return self.cleanup();
}
}
}
}
/// Sends disconnect messages to all workers and joins them.
pub(crate) fn cleanup(&mut self) -> Result<(), MigratableError> {
// Send disconnect messages to all workers.
for _ in 0..self.threads.len() {
// All threads may have terminated, leading to a dropped receiver. Thus we ignore
// errors here.
self.message_tx
.try_send(SendMemoryThreadMessage::Disconnect)
.ok();
}
let mut first_err = Ok(());
self.threads.drain(..).for_each(|thread| {
let err = match thread.join() {
Ok(Ok(())) => None,
Ok(Err(e)) => Some(e),
Err(panic) => Some(MigratableError::MigrateSend(anyhow!(
"send-memory thread panicked: {panic:?}"
))),
};
if let Some(e) = err {
warn!("Error in send-memory thread: {e}");
if first_err.is_ok() {
first_err = Err(e);
}
}
});
first_err
}
}
impl Drop for SendAdditionalConnections {
fn drop(&mut self) {
if !self.threads.is_empty() {
warn!(
"SendAdditionalConnections was not cleaned up! Either cleanup() was never called (programming error) or it failed before completing."
);
}
}
}
/// Extract a UNIX socket path from a "unix:" migration URL.
fn socket_url_to_path(url: &str) -> Result<PathBuf, anyhow::Error> {
url.strip_prefix("unix:")
.ok_or_else(|| anyhow!("Could not extract path from URL: {url}"))
.map(|s| s.into())
}
/// Errors that can occur when parsing a TCP address.
#[derive(Debug, Error)]
pub enum TcpAddressParseError {
#[error("Missing closing ']' for bracketed IPv6 address")]
MissingIpv6ClosingBracket,
#[error("Missing port separator after bracketed host")]
MissingPortSeparatorAfterBracketedHost,
#[error("Missing TCP port")]
MissingPort,
#[error("Host must not be empty")]
EmptyHost,
#[error("Port must not be empty")]
EmptyPort,
#[error("Invalid TCP port: {port}")]
InvalidPort {
port: String,
#[source]
source: ParseIntError,
},
}
/// Extract the server name from a TCP address. This function assumes that
/// `tcp:` has already been stripped.
///
/// The expected format is `<host>:<port>` for hostnames and IPv4 addresses, or
/// `[<ipv6-address>]:<port>` for IPv6 addresses. The host and port must both be
/// present, and the port must parse as a `u16`.
pub fn tcp_address_to_server_name(address: &str) -> Result<&str, TcpAddressParseError> {
let (host, port) = if let Some(rest) = address.strip_prefix('[') {
let (host, rest) = rest
.split_once(']')
.ok_or(TcpAddressParseError::MissingIpv6ClosingBracket)?;
let port = rest
.strip_prefix(':')
.ok_or(TcpAddressParseError::MissingPortSeparatorAfterBracketedHost)?;
(host, port)
} else {
address
.rsplit_once(':')
.ok_or(TcpAddressParseError::MissingPort)?
};
if host.is_empty() {
return Err(TcpAddressParseError::EmptyHost);
}
if port.is_empty() {
return Err(TcpAddressParseError::EmptyPort);
}
port.parse::<u16>()
.map_err(|source| TcpAddressParseError::InvalidPort {
port: port.to_owned(),
source,
})?;
Ok(host)
}
/// Connect to a migration endpoint and return the established stream.
pub(crate) fn send_migration_socket(
destination_url: &str,
tls_dir: Option<&Path>,
) -> Result<SocketStream, MigratableError> {
if let Some(address) = destination_url.strip_prefix("tcp:") {
info!("Connecting to TCP socket at {address}");
let socket = TcpStream::connect(address)
.context("Error connecting to TCP socket")
.map_err(MigratableError::MigrateSend)?;
if let Some(tls_dir) = tls_dir {
// The address should have been validated by the API using this exact function.
// Any error here has to be treated as a programming error.
let server_name = tcp_address_to_server_name(address)
.expect("TCP address should have been validated by the API");
TlsStream::new_client(socket, tls_dir, server_name)
.map(Box::new)
.map(SocketStream::Tls)
.context("Error creating TLS migration stream")
.map_err(MigratableError::MigrateSend)
.inspect(|_| info!("TLS handshake (client-side/sender) successful"))
} else {
Ok(SocketStream::Tcp(socket))
}
} else {
let path = socket_url_to_path(destination_url).map_err(MigratableError::MigrateSend)?;
info!("Connecting to UNIX socket at {path:?}");
let socket = UnixStream::connect(&path)
.context("Error connecting to UNIX socket")
.map_err(MigratableError::MigrateSend)?;
Ok(SocketStream::Unix(socket))
}
}
/// Open a dedicated fault connection with the destination VM and announce
/// the [`ConnectionRole::Fault`] role. Used by the source to serve `PageFault`
/// requests asynchronously.
pub(crate) fn open_fault_connection(
destination_url: &str,
tls_dir: Option<&Path>,
) -> Result<SocketStream, MigratableError> {
let mut socket = send_migration_socket(destination_url, tls_dir)?;
ConnectionRole::Fault.write_to(&mut socket)?;
// Enable fault request/response round-trips.
socket
.set_nodelay(true)
.map_err(MigratableError::MigrateSocket)?;
Ok(socket)
}
/// Bind a migration listener for the receiver side.
pub(crate) fn receive_migration_listener(
receiver_url: &str,
tls_dir: Option<&Path>,
) -> Result<ReceiveListener, MigratableError> {
if let Some(address) = receiver_url.strip_prefix("tcp:") {
let listener = TcpListener::bind(address)
.context("Error binding to TCP socket")
.map_err(MigratableError::MigrateReceive)?;
if let Some(tls_dir) = tls_dir {
let config = TlsServerConfig::new(tls_dir)
.context("Error creating TLS server config")
.map_err(MigratableError::MigrateReceive)?;
Ok(ReceiveListener::Tls(listener, config))
} else {
Ok(ReceiveListener::Tcp(listener))
}
} else {
let path = socket_url_to_path(receiver_url).map_err(MigratableError::MigrateReceive)?;
UnixListener::bind(&path)
.map(ReceiveListener::Unix)
.context("Error binding to UNIX socket")
.map_err(MigratableError::MigrateReceive)
}
}
/// Read a response and return Ok(()) if it was a [`Response::Ok`].
pub(crate) fn expect_ok_response(
socket: &mut SocketStream,
error: MigratableError,
) -> Result<(), MigratableError> {
Response::read_from(socket)?
.ok_or_fatal_error(error)
.map(|_| ())
}
/// Send a request and validate that the peer responds with OK.
pub(crate) fn send_request_expect_ok(
socket: &mut SocketStream,
request: Request,
error: MigratableError,
) -> Result<(), MigratableError> {
request.write_to(socket)?;
expect_ok_response(socket, error)
}
/// Serialize and send the VM configuration payload.
pub(crate) fn send_config(
socket: &mut SocketStream,
config: &VmMigrationConfig,
) -> Result<(), MigratableError> {
let config_data = serde_json::to_vec(config)
.context("Error serializing VM migration config")
.map_err(MigratableError::MigrateSend)?;
Request::config(config_data.len() as u64).write_to(socket)?;
socket
.write_all(&config_data)
.map_err(MigratableError::MigrateSocket)?;
expect_ok_response(
socket,
MigratableError::MigrateSend(anyhow!("Error during config migration")),
)
}
/// Serialize and send the VM snapshot payload.
pub(crate) fn send_state(
socket: &mut SocketStream,
snapshot: &Snapshot,
) -> Result<(), MigratableError> {
let snapshot_data = serde_json::to_vec(snapshot)
.context("Error serializing VM snapshot")
.map_err(MigratableError::MigrateSend)?;
Request::state(snapshot_data.len() as u64).write_to(socket)?;
socket
.write_all(&snapshot_data)
.map_err(MigratableError::MigrateSocket)?;
expect_ok_response(
socket,
MigratableError::MigrateSend(anyhow!("Error during state migration")),
)
}
/// Transmits the given [`MemoryRangeTable`] and the corresponding guest memory
/// content over the wire if there is at least one range.
///
/// Sends a memory migration request, the range table, and the corresponding
/// guest memory range over the given socket. Waits for acknowledgment
/// from the destination.
pub(crate) fn send_memory_ranges(
guest_memory: &GuestMemoryAtomic<GuestMemoryMmap>,
ranges: &MemoryRangeTable,
socket: &mut SocketStream,
) -> Result<(), MigratableError> {
if ranges.regions().is_empty() {
return Ok(());
}
// Send the memory table
Request::memory(ranges.length()).write_to(socket)?;
ranges.write_to(socket)?;
// And then the memory itself
let mem = guest_memory.memory();
for range in ranges.regions() {
let mut offset: u64 = 0;
// Here we are manually handling the retry in case we can't read the
// whole region at once because we can't use the implementation
// from vm-memory::GuestMemory of write_all_to() as it is not
// following the correct behavior. For more info about this issue
// see: https://github.com/rust-vmm/vm-memory/issues/174
loop {
let bytes_written = mem
.write_volatile_to(
GuestAddress(range.gpa + offset),
socket,
(range.length - offset) as usize,
)
.context("Error transferring memory to socket")
.map_err(MigratableError::MigrateSend)?;
offset += bytes_written as u64;
if offset == range.length {
break;
}
}
}
expect_ok_response(
socket,
MigratableError::MigrateSend(anyhow!("Error during dirty memory migration")),
)
}
/// Receive memory contents for the given range table into guest memory.
pub(crate) fn receive_memory_ranges(
guest_memory: &GuestMemoryAtomic<GuestMemoryMmap>,
req: &Request,
socket: &mut SocketStream,
) -> Result<(), MigratableError> {
debug_assert_eq!(req.command(), Command::Memory);
// Read the memory table
let ranges = MemoryRangeTable::read_from(socket, req.length())?;
// And then the memory itself
let mem = guest_memory.memory();
for range in ranges.regions() {
let mut offset: u64 = 0;
// Here we are manually handling the retry in case we can't read the
// whole region at once because we can't use the implementation
// from vm-memory::GuestMemory of read_exact_from() as it is not
// following the correct behavior. For more info about this issue
// see: https://github.com/rust-vmm/vm-memory/issues/174
loop {
let bytes_read = mem
.read_volatile_from(
GuestAddress(range.gpa + offset),
socket,
(range.length - offset) as usize,
)
.context("Error receiving memory from socket")
.map_err(MigratableError::MigrateReceive)?;
offset += bytes_read as u64;
if offset == range.length {
break;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::tcp_address_to_server_name;
#[test]
fn test_tcp_address_to_server_name() {
assert_eq!(
tcp_address_to_server_name("example.com:1234").unwrap(),
"example.com"
);
assert_eq!(
tcp_address_to_server_name("192.0.2.1:1234").unwrap(),
"192.0.2.1"
);
assert_eq!(
tcp_address_to_server_name("[2001:db8::1]:1234").unwrap(),
"2001:db8::1"
);
}
#[test]
fn test_tcp_address_to_server_name_rejects_invalid_addresses() {
assert_eq!(
tcp_address_to_server_name("192.168.1.1")
.unwrap_err()
.to_string(),
"Missing TCP port"
);
assert_eq!(
tcp_address_to_server_name(":8080").unwrap_err().to_string(),
"Host must not be empty"
);
assert_eq!(
tcp_address_to_server_name("host:").unwrap_err().to_string(),
"Port must not be empty"
);
assert_eq!(
tcp_address_to_server_name("host:not-a-port")
.unwrap_err()
.to_string(),
"Invalid TCP port: not-a-port"
);
assert_eq!(
tcp_address_to_server_name("[2001:db8::1")
.unwrap_err()
.to_string(),
"Missing closing ']' for bracketed IPv6 address"
);
assert_eq!(
tcp_address_to_server_name("[]:8080")
.unwrap_err()
.to_string(),
"Host must not be empty"
);
assert_eq!(
tcp_address_to_server_name("[2001:db8::1]")
.unwrap_err()
.to_string(),
"Missing port separator after bracketed host"
);
assert_eq!(
tcp_address_to_server_name("[2001:db8::1]:")
.unwrap_err()
.to_string(),
"Port must not be empty"
);
assert_eq!(
tcp_address_to_server_name("[2001:db8::1]:99999")
.unwrap_err()
.to_string(),
"Invalid TCP port: 99999"
);
}
}