mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
vmm: tighten migration URL validation
For TLS we have to parse the hostname from the given migration URL. For that we have to make a few assumptions about the URL (e.g. it always has a port). To catch problems early, we tighten the URL validation. On-behalf-of: SAP sebastian.eydam@sap.com Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
This commit is contained in:
committed by
Rob Bradford
parent
0ec2ae376b
commit
001bdde75f
+41
-18
@@ -53,7 +53,9 @@ pub use self::http::{start_http_fd_thread, start_http_path_thread};
|
|||||||
use crate::Error as VmmError;
|
use crate::Error as VmmError;
|
||||||
use crate::config::RestoreConfig;
|
use crate::config::RestoreConfig;
|
||||||
use crate::device_tree::DeviceTree;
|
use crate::device_tree::DeviceTree;
|
||||||
use crate::migration_transport::MAX_MIGRATION_CONNECTIONS;
|
use crate::migration_transport::{
|
||||||
|
MAX_MIGRATION_CONNECTIONS, TcpAddressParseError, tcp_address_to_server_name,
|
||||||
|
};
|
||||||
use crate::vm::{Error as VmError, VmState};
|
use crate::vm::{Error as VmError, VmState};
|
||||||
use crate::vm_config::{
|
use crate::vm_config::{
|
||||||
DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig, NetConfig, PmemConfig,
|
DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig, NetConfig, PmemConfig,
|
||||||
@@ -301,6 +303,11 @@ pub enum VmSendMigrationConfigError {
|
|||||||
#[error("Error parsing send migration parameters")]
|
#[error("Error parsing send migration parameters")]
|
||||||
ParseError(#[source] OptionParserError),
|
ParseError(#[source] OptionParserError),
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"Error validating send migration parameters: destination_url must use tcp:<host>:<port> or unix:<path>."
|
||||||
|
)]
|
||||||
|
InvalidDestinationUrl(#[source] TcpAddressParseError),
|
||||||
|
|
||||||
#[error("Error validating send migration parameters")]
|
#[error("Error validating send migration parameters")]
|
||||||
ValidationError(String),
|
ValidationError(String),
|
||||||
}
|
}
|
||||||
@@ -441,26 +448,24 @@ impl VmSendMigrationData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate(&self) -> Result<(), VmSendMigrationConfigError> {
|
pub fn validate(&self) -> Result<(), VmSendMigrationConfigError> {
|
||||||
match self.destination_url.as_str() {
|
if let Some(addr) = self.destination_url.strip_prefix("tcp:") {
|
||||||
url if url
|
tcp_address_to_server_name(addr)
|
||||||
.strip_prefix("tcp:")
|
.map_err(VmSendMigrationConfigError::InvalidDestinationUrl)?;
|
||||||
.is_some_and(|addr| !addr.is_empty()) => {}
|
} else if self
|
||||||
url if url
|
.destination_url
|
||||||
.strip_prefix("unix:")
|
.strip_prefix("unix:")
|
||||||
.is_some_and(|path| !path.is_empty()) =>
|
.is_some_and(|path| !path.is_empty())
|
||||||
{
|
{
|
||||||
if self.connections.get() > 1 {
|
if self.connections.get() > 1 {
|
||||||
return Err(VmSendMigrationConfigError::ValidationError(
|
|
||||||
"UNIX sockets and connections option cannot be used at the same time."
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(VmSendMigrationConfigError::ValidationError(
|
return Err(VmSendMigrationConfigError::ValidationError(
|
||||||
"destination_url must use tcp:<host>:<port> or unix:<path>.".to_string(),
|
"UNIX sockets and connections option cannot be used at the same time."
|
||||||
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return Err(VmSendMigrationConfigError::ValidationError(
|
||||||
|
"destination_url must use tcp:<host>:<port> or unix:<path>.".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.connections.get() > MAX_MIGRATION_CONNECTIONS {
|
if self.connections.get() > MAX_MIGRATION_CONNECTIONS {
|
||||||
@@ -1782,6 +1787,14 @@ mod unit_tests {
|
|||||||
assert_eq!(data.timeout_strategy, TimeoutStrategy::default());
|
assert_eq!(data.timeout_strategy, TimeoutStrategy::default());
|
||||||
assert_eq!(data.connections, VmSendMigrationData::default_connections());
|
assert_eq!(data.connections, VmSendMigrationData::default_connections());
|
||||||
|
|
||||||
|
let data = VmSendMigrationData::parse("destination_url=tcp:[2001:db8::1]:8080")
|
||||||
|
.expect("IPv6 migration string should parse");
|
||||||
|
assert_eq!(data.destination_url, "tcp:[2001:db8::1]:8080");
|
||||||
|
|
||||||
|
let data = VmSendMigrationData::parse("destination_url=tcp:destination.example:8080")
|
||||||
|
.expect("hostname migration string should parse");
|
||||||
|
assert_eq!(data.destination_url, "tcp:destination.example:8080");
|
||||||
|
|
||||||
// Missing destination_url is an error
|
// Missing destination_url is an error
|
||||||
VmSendMigrationData::parse("local=on,downtime_ms=200").unwrap_err();
|
VmSendMigrationData::parse("local=on,downtime_ms=200").unwrap_err();
|
||||||
|
|
||||||
@@ -1818,6 +1831,16 @@ mod unit_tests {
|
|||||||
|
|
||||||
// Invalid destination URL scheme is rejected
|
// Invalid destination URL scheme is rejected
|
||||||
VmSendMigrationData::parse("destination_url=file:///tmp/migration").unwrap_err();
|
VmSendMigrationData::parse("destination_url=file:///tmp/migration").unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
VmSendMigrationData::parse("destination_url=tcp:192.168.1.1").unwrap_err(),
|
||||||
|
VmSendMigrationConfigError::InvalidDestinationUrl(TcpAddressParseError::MissingPort)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
VmSendMigrationData::parse("destination_url=tcp:[2001:db8::1]").unwrap_err(),
|
||||||
|
VmSendMigrationConfigError::InvalidDestinationUrl(
|
||||||
|
TcpAddressParseError::MissingPortSeparatorAfterBracketedHost
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
// Local migration requires a UNIX socket destination
|
// Local migration requires a UNIX socket destination
|
||||||
VmSendMigrationData::parse("destination_url=tcp:192.168.1.1:8080,local=yes").unwrap_err();
|
VmSendMigrationData::parse("destination_url=tcp:192.168.1.1:8080,local=yes").unwrap_err();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use std::io::{self, ErrorKind, Read, Write};
|
use std::io::{self, ErrorKind, Read, Write};
|
||||||
use std::net::{TcpListener, TcpStream};
|
use std::net::{TcpListener, TcpStream};
|
||||||
use std::num::NonZeroU32;
|
use std::num::{NonZeroU32, ParseIntError};
|
||||||
use std::os::fd::{AsFd, BorrowedFd};
|
use std::os::fd::{AsFd, BorrowedFd};
|
||||||
use std::os::unix::io::AsRawFd;
|
use std::os::unix::io::AsRawFd;
|
||||||
use std::os::unix::net::{UnixListener, UnixStream};
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
@@ -20,6 +20,7 @@ use std::time::Duration;
|
|||||||
use anyhow::{Context, anyhow};
|
use anyhow::{Context, anyhow};
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
use thiserror::Error;
|
||||||
use vm_memory::bitmap::BitmapSlice;
|
use vm_memory::bitmap::BitmapSlice;
|
||||||
use vm_memory::{
|
use vm_memory::{
|
||||||
Bytes, GuestAddress, GuestAddressSpace, GuestMemoryAtomic, ReadVolatile, VolatileMemoryError,
|
Bytes, GuestAddress, GuestAddressSpace, GuestMemoryAtomic, ReadVolatile, VolatileMemoryError,
|
||||||
@@ -782,6 +783,72 @@ fn socket_url_to_path(url: &str) -> Result<PathBuf, anyhow::Error> {
|
|||||||
.map(|s| s.into())
|
.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.
|
/// Connect to a migration endpoint and return the established stream.
|
||||||
pub(crate) fn send_migration_socket(
|
pub(crate) fn send_migration_socket(
|
||||||
destination_url: &str,
|
destination_url: &str,
|
||||||
@@ -975,3 +1042,78 @@ pub(crate) fn receive_memory_ranges(
|
|||||||
|
|
||||||
Ok(())
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user