vmm: Unix socket backend for serial port

Cloud-Hypervisor takes a path for Unix socket, where it will listen
on. Users can connect to the other end of the socket and access serial
port on the guest.

    "--serial socket=/path/to/socket" is the cmdline option to pass to
cloud-hypervisor.

Users can use socat like below to access guest's serial port once the
guest starts to boot:

    socat -,crnl UNIX-CONNECT:/path/to/socket

Signed-off-by: Praveen K Paladugu <prapal@linux.microsoft.com>
This commit is contained in:
Praveen K Paladugu 2023-06-08 19:41:37 +00:00 committed by Rob Bradford
parent 399c596af8
commit 6d1077fc3c
9 changed files with 177 additions and 20 deletions

View File

@ -166,8 +166,8 @@ impl Serial {
Self::new(id, interrupt, None, state) Self::new(id, interrupt, None, state)
} }
pub fn set_out(&mut self, out: Box<dyn io::Write + Send>) { pub fn set_out(&mut self, out: Option<Box<dyn io::Write + Send>>) {
self.out = Some(out); self.out = out;
} }
/// Queues raw bytes for the guest to read and signals the interrupt if the line status would /// Queues raw bytes for the guest to read and signals the interrupt if the line status would

View File

@ -201,8 +201,8 @@ impl Pl011 {
} }
} }
pub fn set_out(&mut self, out: Box<dyn io::Write + Send>) { pub fn set_out(&mut self, out: Option<Box<dyn io::Write + Send>>) {
self.out = Some(out); self.out = out;
} }
fn state(&self) -> Pl011State { fn state(&self) -> Pl011State {

View File

@ -813,11 +813,13 @@ mod unit_tests {
file: None, file: None,
mode: ConsoleOutputMode::Null, mode: ConsoleOutputMode::Null,
iommu: false, iommu: false,
socket: None,
}, },
console: ConsoleConfig { console: ConsoleConfig {
file: None, file: None,
mode: ConsoleOutputMode::Tty, mode: ConsoleOutputMode::Tty,
iommu: false, iommu: false,
socket: None,
}, },
devices: None, devices: None,
user_devices: None, user_devices: None,

View File

@ -951,9 +951,11 @@ components:
properties: properties:
file: file:
type: string type: string
socket:
type: string
mode: mode:
type: string type: string
enum: [Off, Pty, Tty, File, Null] enum: [Off, Pty, Tty, File, Socket, Null]
iommu: iommu:
type: boolean type: boolean
default: false default: false

View File

@ -111,6 +111,8 @@ pub enum ValidationError {
KernelMissing, KernelMissing,
/// Missing file value for console /// Missing file value for console
ConsoleFileMissing, ConsoleFileMissing,
/// Missing socket path for console
ConsoleSocketPathMissing,
/// Max is less than boot /// Max is less than boot
CpusMaxLowerThanBoot, CpusMaxLowerThanBoot,
/// Both socket and path specified /// Both socket and path specified
@ -185,6 +187,7 @@ impl fmt::Display for ValidationError {
DoubleTtyMode => write!(f, "Console mode tty specified for both serial and console"), DoubleTtyMode => write!(f, "Console mode tty specified for both serial and console"),
KernelMissing => write!(f, "No kernel specified"), KernelMissing => write!(f, "No kernel specified"),
ConsoleFileMissing => write!(f, "Path missing when using file console mode"), ConsoleFileMissing => write!(f, "Path missing when using file console mode"),
ConsoleSocketPathMissing => write!(f, "Path missing when using socket console mode"),
CpusMaxLowerThanBoot => write!(f, "Max CPUs lower than boot CPUs"), CpusMaxLowerThanBoot => write!(f, "Max CPUs lower than boot CPUs"),
DiskSocketAndPath => write!(f, "Disk path and vhost socket both provided"), DiskSocketAndPath => write!(f, "Disk path and vhost socket both provided"),
VhostUserRequiresSharedMemory => { VhostUserRequiresSharedMemory => {
@ -1341,10 +1344,12 @@ impl ConsoleConfig {
.add_valueless("tty") .add_valueless("tty")
.add_valueless("null") .add_valueless("null")
.add("file") .add("file")
.add("iommu"); .add("iommu")
.add("socket");
parser.parse(console).map_err(Error::ParseConsole)?; parser.parse(console).map_err(Error::ParseConsole)?;
let mut file: Option<PathBuf> = default_consoleconfig_file(); let mut file: Option<PathBuf> = default_consoleconfig_file();
let mut socket: Option<PathBuf> = None;
let mut mode: ConsoleOutputMode = ConsoleOutputMode::Off; let mut mode: ConsoleOutputMode = ConsoleOutputMode::Off;
if parser.is_set("off") { if parser.is_set("off") {
@ -1360,6 +1365,11 @@ impl ConsoleConfig {
Some(PathBuf::from(parser.get("file").ok_or( Some(PathBuf::from(parser.get("file").ok_or(
Error::Validation(ValidationError::ConsoleFileMissing), Error::Validation(ValidationError::ConsoleFileMissing),
)?)); )?));
} else if parser.is_set("socket") {
mode = ConsoleOutputMode::Socket;
socket = Some(PathBuf::from(parser.get("socket").ok_or(
Error::Validation(ValidationError::ConsoleSocketPathMissing),
)?));
} else { } else {
return Err(Error::ParseConsoleInvalidModeGiven); return Err(Error::ParseConsoleInvalidModeGiven);
} }
@ -1369,7 +1379,12 @@ impl ConsoleConfig {
.unwrap_or(Toggle(false)) .unwrap_or(Toggle(false))
.0; .0;
Ok(Self { file, mode, iommu }) Ok(Self {
file,
mode,
iommu,
socket,
})
} }
} }
@ -2659,6 +2674,7 @@ mod tests {
mode: ConsoleOutputMode::Off, mode: ConsoleOutputMode::Off,
iommu: false, iommu: false,
file: None, file: None,
socket: None,
} }
); );
assert_eq!( assert_eq!(
@ -2667,6 +2683,7 @@ mod tests {
mode: ConsoleOutputMode::Pty, mode: ConsoleOutputMode::Pty,
iommu: false, iommu: false,
file: None, file: None,
socket: None,
} }
); );
assert_eq!( assert_eq!(
@ -2675,6 +2692,7 @@ mod tests {
mode: ConsoleOutputMode::Tty, mode: ConsoleOutputMode::Tty,
iommu: false, iommu: false,
file: None, file: None,
socket: None,
} }
); );
assert_eq!( assert_eq!(
@ -2683,6 +2701,7 @@ mod tests {
mode: ConsoleOutputMode::Null, mode: ConsoleOutputMode::Null,
iommu: false, iommu: false,
file: None, file: None,
socket: None,
} }
); );
assert_eq!( assert_eq!(
@ -2690,7 +2709,8 @@ mod tests {
ConsoleConfig { ConsoleConfig {
mode: ConsoleOutputMode::File, mode: ConsoleOutputMode::File,
iommu: false, iommu: false,
file: Some(PathBuf::from("/tmp/console")) file: Some(PathBuf::from("/tmp/console")),
socket: None,
} }
); );
assert_eq!( assert_eq!(
@ -2699,6 +2719,7 @@ mod tests {
mode: ConsoleOutputMode::Null, mode: ConsoleOutputMode::Null,
iommu: true, iommu: true,
file: None, file: None,
socket: None,
} }
); );
assert_eq!( assert_eq!(
@ -2706,7 +2727,17 @@ mod tests {
ConsoleConfig { ConsoleConfig {
mode: ConsoleOutputMode::File, mode: ConsoleOutputMode::File,
iommu: true, iommu: true,
file: Some(PathBuf::from("/tmp/console")) file: Some(PathBuf::from("/tmp/console")),
socket: None,
}
);
assert_eq!(
ConsoleConfig::parse("socket=/tmp/serial.sock,iommu=on")?,
ConsoleConfig {
mode: ConsoleOutputMode::Socket,
iommu: true,
file: None,
socket: Some(PathBuf::from("/tmp/serial.sock")),
} }
); );
Ok(()) Ok(())
@ -2852,11 +2883,13 @@ mod tests {
file: None, file: None,
mode: ConsoleOutputMode::Null, mode: ConsoleOutputMode::Null,
iommu: false, iommu: false,
socket: None,
}, },
console: ConsoleConfig { console: ConsoleConfig {
file: None, file: None,
mode: ConsoleOutputMode::Tty, mode: ConsoleOutputMode::Tty,
iommu: false, iommu: false,
socket: None,
}, },
devices: None, devices: None,
user_devices: None, user_devices: None,

View File

@ -395,6 +395,9 @@ pub enum DeviceManagerError {
/// No support for device passthrough /// No support for device passthrough
NoDevicePassthroughSupport, NoDevicePassthroughSupport,
/// No socket option support for console device
NoSocketOptionSupportForConsoleDevice,
/// Failed to resize virtio-balloon /// Failed to resize virtio-balloon
VirtioBalloonResize(virtio_devices::balloon::Error), VirtioBalloonResize(virtio_devices::balloon::Error),
@ -1995,6 +1998,9 @@ impl DeviceManager {
Endpoint::File(stdout) Endpoint::File(stdout)
} }
} }
ConsoleOutputMode::Socket => {
return Err(DeviceManagerError::NoSocketOptionSupportForConsoleDevice);
}
ConsoleOutputMode::Null => Endpoint::Null, ConsoleOutputMode::Null => Endpoint::Null,
ConsoleOutputMode::Off => return Ok(None), ConsoleOutputMode::Off => return Ok(None),
}; };
@ -2074,14 +2080,18 @@ impl DeviceManager {
let _ = self.set_raw_mode(&out); let _ = self.set_raw_mode(&out);
Some(Box::new(out)) Some(Box::new(out))
} }
ConsoleOutputMode::Off | ConsoleOutputMode::Null => None, ConsoleOutputMode::Off | ConsoleOutputMode::Null | ConsoleOutputMode::Socket => None,
}; };
if serial_config.mode != ConsoleOutputMode::Off { if serial_config.mode != ConsoleOutputMode::Off {
let serial = self.add_serial_device(interrupt_manager, serial_writer)?; let serial = self.add_serial_device(interrupt_manager, serial_writer)?;
self.serial_manager = match serial_config.mode { self.serial_manager = match serial_config.mode {
ConsoleOutputMode::Pty | ConsoleOutputMode::Tty => { ConsoleOutputMode::Pty | ConsoleOutputMode::Tty | ConsoleOutputMode::Socket => {
let serial_manager = let serial_manager = SerialManager::new(
SerialManager::new(serial, self.serial_pty.clone(), serial_config.mode) serial,
self.serial_pty.clone(),
serial_config.mode,
serial_config.socket,
)
.map_err(DeviceManagerError::CreateSerialManager)?; .map_err(DeviceManagerError::CreateSerialManager)?;
if let Some(mut serial_manager) = serial_manager { if let Some(mut serial_manager) = serial_manager {
serial_manager serial_manager

View File

@ -2274,11 +2274,13 @@ mod unit_tests {
file: None, file: None,
mode: ConsoleOutputMode::Null, mode: ConsoleOutputMode::Null,
iommu: false, iommu: false,
socket: None,
}, },
console: ConsoleConfig { console: ConsoleConfig {
file: None, file: None,
mode: ConsoleOutputMode::Tty, mode: ConsoleOutputMode::Tty,
iommu: false, iommu: false,
socket: None,
}, },
devices: None, devices: None,
user_devices: None, user_devices: None,

View File

@ -13,8 +13,11 @@ use libc::EFD_NONBLOCK;
use serial_buffer::SerialBuffer; use serial_buffer::SerialBuffer;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::os::unix::io::{AsRawFd, FromRawFd}; use std::net::Shutdown;
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
use std::os::unix::net::{UnixListener, UnixStream};
use std::panic::AssertUnwindSafe; use std::panic::AssertUnwindSafe;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::{io, result, thread}; use std::{io, result, thread};
@ -54,6 +57,22 @@ pub enum Error {
/// Cannot spawn SerialManager thread. /// Cannot spawn SerialManager thread.
#[error("Error spawning SerialManager thread: {0}")] #[error("Error spawning SerialManager thread: {0}")]
SpawnSerialManager(#[source] io::Error), SpawnSerialManager(#[source] io::Error),
/// Cannot bind to Unix socket
#[error("Error binding to socket: {0}")]
BindUnixSocket(#[source] io::Error),
/// Cannot accept connection from Unix socket
#[error("Error accepting connection: {0}")]
AcceptConnection(#[source] io::Error),
/// Cannot clone the UnixStream
#[error("Error cloning UnixStream: {0}")]
CloneUnixStream(#[source] io::Error),
/// Cannot shutdown the connection
#[error("Error shutting down a connection: {0}")]
ShutdownConnection(#[source] io::Error),
} }
pub type Result<T> = result::Result<T, Error>; pub type Result<T> = result::Result<T, Error>;
@ -62,6 +81,7 @@ pub type Result<T> = result::Result<T, Error>;
pub enum EpollDispatch { pub enum EpollDispatch {
File = 0, File = 0,
Kill = 1, Kill = 1,
Socket = 2,
Unknown, Unknown,
} }
@ -71,6 +91,7 @@ impl From<u64> for EpollDispatch {
match v { match v {
0 => File, 0 => File,
1 => Kill, 1 => Kill,
2 => Socket,
_ => Unknown, _ => Unknown,
} }
} }
@ -86,6 +107,7 @@ pub struct SerialManager {
kill_evt: EventFd, kill_evt: EventFd,
handle: Option<thread::JoinHandle<()>>, handle: Option<thread::JoinHandle<()>>,
pty_write_out: Option<Arc<AtomicBool>>, pty_write_out: Option<Arc<AtomicBool>>,
mode: ConsoleOutputMode,
} }
impl SerialManager { impl SerialManager {
@ -94,6 +116,7 @@ impl SerialManager {
#[cfg(target_arch = "aarch64")] serial: Arc<Mutex<Pl011>>, #[cfg(target_arch = "aarch64")] serial: Arc<Mutex<Pl011>>,
pty_pair: Option<Arc<Mutex<PtyPair>>>, pty_pair: Option<Arc<Mutex<PtyPair>>>,
mode: ConsoleOutputMode, mode: ConsoleOutputMode,
socket: Option<PathBuf>,
) -> Result<Option<Self>> { ) -> Result<Option<Self>> {
let in_file = match mode { let in_file = match mode {
ConsoleOutputMode::Pty => { ConsoleOutputMode::Pty => {
@ -130,6 +153,16 @@ impl SerialManager {
return Ok(None); return Ok(None);
} }
} }
ConsoleOutputMode::Socket => {
if let Some(socket_path) = socket {
let listener =
UnixListener::bind(socket_path.as_path()).map_err(Error::BindUnixSocket)?;
// SAFETY: listener is valid and will return valid fd
unsafe { File::from_raw_fd(listener.into_raw_fd()) }
} else {
return Ok(None);
}
}
_ => return Ok(None), _ => return Ok(None),
}; };
@ -144,11 +177,17 @@ impl SerialManager {
) )
.map_err(Error::Epoll)?; .map_err(Error::Epoll)?;
let epoll_fd_data = if mode == ConsoleOutputMode::Socket {
EpollDispatch::Socket
} else {
EpollDispatch::File
};
epoll::ctl( epoll::ctl(
epoll_fd, epoll_fd,
epoll::ControlOptions::EPOLL_CTL_ADD, epoll::ControlOptions::EPOLL_CTL_ADD,
in_file.as_raw_fd(), in_file.as_raw_fd(),
epoll::Event::new(epoll::Events::EPOLLIN, EpollDispatch::File as u64), epoll::Event::new(epoll::Events::EPOLLIN, epoll_fd_data as u64),
) )
.map_err(Error::Epoll)?; .map_err(Error::Epoll)?;
@ -158,7 +197,11 @@ impl SerialManager {
pty_write_out = Some(write_out.clone()); pty_write_out = Some(write_out.clone());
let writer = in_file.try_clone().map_err(Error::FileClone)?; let writer = in_file.try_clone().map_err(Error::FileClone)?;
let buffer = SerialBuffer::new(Box::new(writer), write_out); let buffer = SerialBuffer::new(Box::new(writer), write_out);
serial.as_ref().lock().unwrap().set_out(Box::new(buffer)); serial
.as_ref()
.lock()
.unwrap()
.set_out(Some(Box::new(buffer)));
} }
// Use 'File' to enforce closing on 'epoll_fd' // Use 'File' to enforce closing on 'epoll_fd'
@ -172,6 +215,7 @@ impl SerialManager {
kill_evt, kill_evt,
handle: None, handle: None,
pty_write_out, pty_write_out,
mode,
})) }))
} }
@ -212,6 +256,10 @@ impl SerialManager {
let mut in_file = self.in_file.try_clone().map_err(Error::FileClone)?; let mut in_file = self.in_file.try_clone().map_err(Error::FileClone)?;
let serial = self.serial.clone(); let serial = self.serial.clone();
let pty_write_out = self.pty_write_out.clone(); let pty_write_out = self.pty_write_out.clone();
//SAFETY: in_file is has a valid fd
let listener = unsafe { UnixListener::from_raw_fd(self.in_file.as_raw_fd()) };
let mut reader: Option<UnixStream> = None;
let mode = self.mode.clone();
// In case of PTY, we want to be able to detect a connection on the // In case of PTY, we want to be able to detect a connection on the
// other end of the PTY. This is done by detecting there's no event // other end of the PTY. This is done by detecting there's no event
@ -250,7 +298,7 @@ impl SerialManager {
} }
}; };
if num_events == 0 { if mode != ConsoleOutputMode::Socket && num_events == 0 {
// This very specific case happens when the serial is connected // This very specific case happens when the serial is connected
// to a PTY. We know EPOLLHUP is always present when there's nothing // to a PTY. We know EPOLLHUP is always present when there's nothing
// connected at the other end of the PTY. That's why getting no event // connected at the other end of the PTY. That's why getting no event
@ -266,11 +314,67 @@ impl SerialManager {
let event = event.data; let event = event.data;
warn!("Unknown serial manager loop event: {}", event); warn!("Unknown serial manager loop event: {}", event);
} }
EpollDispatch::Socket => {
// New connection request arrived.
// Shutdown the previous connection, if any
if let Some(previous_reader) = reader {
previous_reader
.shutdown(Shutdown::Both)
.map_err(Error::AcceptConnection)?;
}
// Events on the listening socket will be connection requests.
// Accept them, create a reader and a writer.
let (unix_stream, _) =
listener.accept().map_err(Error::AcceptConnection)?;
let writer =
unix_stream.try_clone().map_err(Error::CloneUnixStream)?;
reader = Some(
unix_stream.try_clone().map_err(Error::CloneUnixStream)?,
);
epoll::ctl(
epoll_fd,
epoll::ControlOptions::EPOLL_CTL_ADD,
unix_stream.into_raw_fd(),
epoll::Event::new(
epoll::Events::EPOLLIN,
EpollDispatch::File as u64,
),
)
.map_err(Error::Epoll)?;
serial.lock().unwrap().set_out(Some(Box::new(writer)));
}
EpollDispatch::File => { EpollDispatch::File => {
if event.events & libc::EPOLLIN as u32 != 0 { if event.events & libc::EPOLLIN as u32 != 0 {
let mut input = [0u8; 64]; let mut input = [0u8; 64];
let count = let count = match mode {
in_file.read(&mut input).map_err(Error::ReadInput)?; ConsoleOutputMode::Socket => {
if let Some(mut serial_reader) = reader.as_ref() {
let count = serial_reader
.read(&mut input)
.map_err(Error::ReadInput)?;
if count == 0 {
info!("Remote end closed serial socket");
serial_reader
.shutdown(Shutdown::Both)
.map_err(Error::ShutdownConnection)?;
reader = None;
serial
.as_ref()
.lock()
.unwrap()
.set_out(None);
}
count
} else {
0
}
}
_ => in_file
.read(&mut input)
.map_err(Error::ReadInput)?,
};
// Replace "\n" with "\r" to deal with Windows SAC (#1170) // Replace "\n" with "\r" to deal with Windows SAC (#1170)
if count == 1 && input[0] == 0x0a { if count == 1 && input[0] == 0x0a {

View File

@ -440,6 +440,7 @@ pub enum ConsoleOutputMode {
Pty, Pty,
Tty, Tty,
File, File,
Socket,
Null, Null,
} }
@ -450,6 +451,7 @@ pub struct ConsoleConfig {
pub mode: ConsoleOutputMode, pub mode: ConsoleOutputMode,
#[serde(default)] #[serde(default)]
pub iommu: bool, pub iommu: bool,
pub socket: Option<PathBuf>,
} }
pub fn default_consoleconfig_file() -> Option<PathBuf> { pub fn default_consoleconfig_file() -> Option<PathBuf> {
@ -555,6 +557,7 @@ pub fn default_serial() -> ConsoleConfig {
file: None, file: None,
mode: ConsoleOutputMode::Null, mode: ConsoleOutputMode::Null,
iommu: false, iommu: false,
socket: None,
} }
} }
@ -563,6 +566,7 @@ pub fn default_console() -> ConsoleConfig {
file: None, file: None,
mode: ConsoleOutputMode::Tty, mode: ConsoleOutputMode::Tty,
iommu: false, iommu: false,
socket: None,
} }
} }