vm-migration: client-side of a TLS connection

TLS connections have a TLS server (listens for incoming connections) and
a TLS client (initiates the connection). This commit adds the code for
the client side, which is the sender of a migration

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
This commit is contained in:
Sebastian Eydam
2026-04-14 10:18:56 +02:00
committed by Rob Bradford
parent 6e501cc2dc
commit 8e9d779688
5 changed files with 373 additions and 12 deletions

211
Cargo.lock generated
View File

@@ -71,7 +71,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -82,7 +82,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -182,7 +182,7 @@ dependencies = [
"polling",
"rustix",
"slab",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -240,7 +240,7 @@ dependencies = [
"rustix",
"signal-hook-registry",
"slab",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -272,6 +272,28 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "aws-lc-rs"
version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
]
[[package]]
name = "backtrace"
version = "0.3.76"
@@ -482,6 +504,15 @@ dependencies = [
"zbus",
]
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]]
name = "colorchoice"
version = "1.0.5"
@@ -668,9 +699,15 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "either"
version = "1.16.0"
@@ -750,7 +787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -840,6 +877,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futures"
version = "0.3.32"
@@ -1784,7 +1827,7 @@ dependencies = [
"hermit-abi",
"pin-project-lite",
"rustix",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -2017,6 +2060,20 @@ dependencies = [
"syn",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustc-demangle"
version = "0.1.27"
@@ -2039,7 +2096,44 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
@@ -2249,6 +2343,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.117"
@@ -2270,7 +2370,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -2280,7 +2380,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [
"rustix",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -2420,7 +2520,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -2441,6 +2541,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "utf8parse"
version = "0.2.2"
@@ -2677,6 +2783,8 @@ version = "0.1.0"
dependencies = [
"anyhow",
"itertools",
"log",
"rustls",
"serde",
"serde_json",
"thiserror",
@@ -2907,6 +3015,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -2916,6 +3033,70 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "1.0.3"
@@ -3047,7 +3228,7 @@ dependencies = [
"tracing",
"uds_windows",
"uuid",
"windows-sys",
"windows-sys 0.61.2",
"winnow",
"zbus_macros",
"zbus_names",
@@ -3100,6 +3281,12 @@ dependencies = [
"syn",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zmij"
version = "1.0.21"

View File

@@ -95,6 +95,7 @@ itertools = "0.14.0"
jiff = { version = "0.2", default-features = false, features = ["std"] }
libc = "0.2.186"
log = "0.4.30"
rustls = { version = "0.23.40", features = ["ring"] }
sha2 = "0.11.0"
signal-hook = "0.4.4"
thiserror = "2.0.18"

View File

@@ -8,6 +8,8 @@ version = "0.1.0"
[dependencies]
anyhow = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
rustls = { workspace = true }
serde = { workspace = true, features = ["derive", "rc"] }
serde_json = { workspace = true }
thiserror = { workspace = true }

View File

@@ -19,6 +19,7 @@ use crate::protocol::MemoryRangeTable;
mod bitpos_iterator;
mod context;
pub mod protocol;
pub mod tls;
#[derive(Error, Debug)]
pub enum UffdError {
@@ -102,6 +103,9 @@ pub enum MigratableError {
#[error("Lifecycle operation skipped for disconnected component {0}")]
DeviceDisconnected(String),
#[error("Error setting up a TLS-encrypted connection")]
Tls(#[source] tls::TlsError),
}
/// A Pausable component can be paused and resumed.

167
vm-migration/src/tls.rs Normal file
View File

@@ -0,0 +1,167 @@
// Copyright © 2026 Cyberus Technology GmbH
//
// SPDX-License-Identifier: Apache-2.0
//
//! TLS support for migration streams over TCP.
//!
//! This module wraps `rustls` to provide a blocking [`TlsStream`] for migration
//! traffic. [`TlsStream::new_client`] authenticates the server against
//! `ca-cert.pem` and the expected hostname, and presents `client-cert.pem` and
//! `client-key.pem` for mutual TLS (mTLS) authentication. [`TlsServerConfig`] loads
//! `server-cert.pem` and `server-key.pem`, trusts client certificates issued by
//! the CA in `ca-cert.pem`, and [`TlsStream::new_server`] uses that
//! configuration to establish the server side of the connection.
//!
//! [`TlsStream`] implements [`Read`], [`Write`], [`ReadVolatile`],
//! [`WriteVolatile`], and [`AsFd`] so it can be used by the transport layer like
//! other migration streams. All data must pass through rustls; direct I/O on the
//! underlying socket would bypass TLS processing and break the connection.
use std::net::TcpStream;
use std::path::Path;
use std::result;
use std::sync::Arc;
use log::warn;
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, InvalidDnsNameError, PrivateKeyDer, ServerName};
use rustls::server::VerifierBuilderError;
use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned};
use thiserror::Error;
use crate::MigratableError;
const CA_CERT_FILE: &str = "ca-cert.pem";
const CLIENT_CERT_FILE: &str = "client-cert.pem";
const CLIENT_KEY_FILE: &str = "client-key.pem";
/// Errors that can occur when establishing a TLS-encrypted migration channel.
#[derive(Error, Debug)]
pub enum TlsError {
#[error("The provided hostname could not be parsed")]
InvalidDnsName(#[source] InvalidDnsNameError),
#[error("Rustls protocol error")]
RustlsError(#[from] rustls::Error),
#[error("Rustls verifier configuration error")]
RustlsVerifierBuilderError(#[source] VerifierBuilderError),
#[error("Rustls protocol IO error")]
RustlsIoError(#[from] std::io::Error),
#[error("TLS handshake stalled: no read/write progress while handshake is still in progress")]
HandshakeError,
#[error("Error handling PEM file")]
RustlsPemError(#[from] rustls::pki_types::pem::Error),
}
/// Wraps the concrete rustls stream for either side (server or client) of the
/// TLS connection.
///
/// [`TlsStream`] uses this enum to store a [`StreamOwned`] with either a
/// [`ClientConnection`] or [`ServerConnection`] while exposing a single
/// transport-agnostic API.
#[derive(Debug)]
enum TlsStreamParticipant {
Client(StreamOwned<ClientConnection, TcpStream>),
}
/// Server/Client-agnostic TLS stream.
pub struct TlsStream {
stream: TlsStreamParticipant,
}
impl TlsStream {
/// Creates a client [`TlsStream`].
///
/// The client verifies the server certificate against `ca-cert.pem` and the
/// provided `hostname`, and presents the certificate chain in
/// `client-cert.pem` together with the private key in `client-key.pem` for
/// mutual TLS authentication.
pub fn new_client(
socket: TcpStream,
cert_dir: &Path,
hostname: &str,
) -> result::Result<Self, MigratableError> {
let root_store = load_root_store(&cert_dir.join(CA_CERT_FILE))?;
let client_certs = load_cert_chain(&cert_dir.join(CLIENT_CERT_FILE))?;
let client_key = load_private_key(&cert_dir.join(CLIENT_KEY_FILE))?;
let config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_client_auth_cert(client_certs, client_key)
.map_err(TlsError::RustlsError)
.map_err(MigratableError::Tls)?;
let config = Arc::new(config);
let server_name = ServerName::try_from(hostname.to_string())
.map_err(TlsError::InvalidDnsName)
.map_err(MigratableError::Tls)?;
let conn = ClientConnection::new(config, server_name)
.map_err(TlsError::RustlsError)
.map_err(MigratableError::Tls)?;
let mut tls = StreamOwned::new(conn, socket);
while tls.conn.is_handshaking() {
let (rd, wr) = tls
.conn
.complete_io(&mut tls.sock)
.map_err(TlsError::RustlsIoError)
.map_err(MigratableError::Tls)?;
// No handshake progress on a connection that should be handshaking, we treat
// that as a failure.
if rd == 0 && wr == 0 {
return Err(MigratableError::Tls(TlsError::HandshakeError));
}
}
Ok(Self {
stream: TlsStreamParticipant::Client(tls),
})
}
}
/// Loads trusted CA certificates into a root store, i.e. the set of trust anchors
/// used to verify the peer's certificate chain.
fn load_root_store(cert_path: &Path) -> result::Result<RootCertStore, MigratableError> {
let mut root_store = RootCertStore::empty();
let (_, ignored) = root_store.add_parsable_certificates(
CertificateDer::pem_file_iter(cert_path)
.map_err(TlsError::RustlsPemError)
.map_err(MigratableError::Tls)?
.map(|cert| cert.map_err(TlsError::RustlsPemError))
.collect::<Result<Vec<CertificateDer<'static>>, TlsError>>()
.map_err(MigratableError::Tls)?,
);
if ignored > 0 {
warn!(
"Ignored {ignored} certificate(s) while loading TLS CA file {}",
cert_path.display()
);
}
Ok(root_store)
}
/// Loads a certificate chain to present during the TLS handshake.
fn load_cert_chain(
cert_path: &Path,
) -> result::Result<Vec<CertificateDer<'static>>, MigratableError> {
CertificateDer::pem_file_iter(cert_path)
.map_err(TlsError::RustlsPemError)
.map_err(MigratableError::Tls)?
.map(|cert| cert.map_err(TlsError::RustlsPemError))
.collect::<Result<Vec<CertificateDer<'static>>, TlsError>>()
.map_err(MigratableError::Tls)
}
/// Loads the private key that proves ownership of the presented certificate chain.
fn load_private_key(key_path: &Path) -> result::Result<PrivateKeyDer<'static>, MigratableError> {
PrivateKeyDer::from_pem_file(key_path)
.map_err(TlsError::RustlsPemError)
.map_err(MigratableError::Tls)
}