#![allow(missing_docs)]
#[allow(unused_imports)]
use crate::builder::ContainerStdio;
#[allow(unused_imports)]
use crate::error::{Error, Result};
#[allow(unused_imports)]
use crate::runtime::VminitdClient;
#[allow(unused_imports)]
use crate::runtime_rpc_error;
#[allow(unused_imports)]
use crate::sealed;
#[allow(unused_imports)]
pub use firkin_oci::{
LinuxSeccompAction as SeccompAction, LinuxSeccompArch as SeccompArch,
LinuxSeccompArg as SeccompArgRule, LinuxSeccompFlag as SeccompFlag,
LinuxSeccompOperator as SeccompOp, LinuxSeccompProfile as Seccomp,
LinuxSyscall as SeccompSyscallRule, Mount,
};
#[allow(unused_imports)]
use firkin_types::VirtiofsTag;
#[allow(unused_imports)]
use firkin_types::VsockPort;
#[allow(unused_imports)]
use firkin_types::{ContainerId, ProcessId};
#[allow(unused_imports)]
use firkin_vminitd_client::ProcessCreate;
use sha2::Digest as _;
#[allow(unused_imports)]
use sha2::Sha256;
#[allow(unused_imports)]
use std::io;
#[allow(unused_imports)]
use std::net::IpAddr;
#[allow(unused_imports)]
use std::path::Path;
#[allow(unused_imports)]
use std::path::PathBuf;
#[allow(unused_imports)]
use std::pin::Pin;
#[allow(unused_imports)]
use std::task::{Context, Poll};
#[allow(unused_imports)]
use std::time::Duration;
#[allow(unused_imports)]
use tokio::io::AsyncReadExt;
#[allow(unused_imports)]
use tokio::io::AsyncWriteExt;
#[allow(unused_imports)]
use tokio::io::{AsyncRead, AsyncWrite};
pub(crate) const STDIN_PORT: VsockPort = VsockPort::new(0x1000_0000);
pub(crate) const STDOUT_PORT: VsockPort = VsockPort::new(0x1000_0001);
pub(crate) const STDERR_PORT: VsockPort = VsockPort::new(0x1000_0002);
pub(crate) const EXEC_STDIO_PORT_START: u32 = 0x1000_0100;
pub(crate) const STDIO_CAPTURE_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Streams;
impl sealed::Sealed for Streams {}
impl ContainerStdio for Streams {
const TERMINAL: bool = false;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PtyConfig {
pub cols: u16,
pub rows: u16,
}
impl PtyConfig {
#[must_use]
pub const fn new(cols: u16, rows: u16) -> Self {
Self { cols, rows }
}
}
impl Default for PtyConfig {
fn default() -> Self {
Self { cols: 80, rows: 24 }
}
}
impl From<(u16, u16)> for PtyConfig {
fn from((cols, rows): (u16, u16)) -> Self {
Self { cols, rows }
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DnsConfig {
pub nameservers: Vec<IpAddr>,
pub domain: Option<String>,
pub search: Vec<String>,
pub options: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HostsConfig {
pub entries: Vec<HostsEntry>,
pub comment: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HostsEntry {
pub ip: IpAddr,
pub hostnames: Vec<String>,
pub comment: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnixSocketConfig {
pub id: String,
pub source: PathBuf,
pub destination: PathBuf,
pub direction: SocketDirection,
pub permissions: Option<u32>,
}
impl UnixSocketConfig {
#[must_use]
pub fn new(
id: impl Into<String>,
source: impl Into<PathBuf>,
destination: impl Into<PathBuf>,
direction: SocketDirection,
) -> Self {
Self {
id: id.into(),
source: source.into(),
destination: destination.into(),
direction,
permissions: None,
}
}
#[must_use]
pub fn into_guest(
id: impl Into<String>,
source: impl Into<PathBuf>,
destination: impl Into<PathBuf>,
) -> Self {
Self::new(id, source, destination, SocketDirection::Into)
}
#[must_use]
pub fn out_of_guest(
id: impl Into<String>,
source: impl Into<PathBuf>,
destination: impl Into<PathBuf>,
) -> Self {
Self::new(id, source, destination, SocketDirection::OutOf)
}
#[must_use]
pub const fn permissions(mut self, permissions: u32) -> Self {
self.permissions = Some(permissions);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SocketDirection {
Into,
OutOf,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileMount {
pub source: PathBuf,
pub destination: PathBuf,
pub read_only: bool,
}
impl FileMount {
#[must_use]
pub fn read_only(source: impl Into<PathBuf>, destination: impl Into<PathBuf>) -> Self {
Self {
source: source.into(),
destination: destination.into(),
read_only: true,
}
}
#[must_use]
pub fn read_write(source: impl Into<PathBuf>, destination: impl Into<PathBuf>) -> Self {
Self {
source: source.into(),
destination: destination.into(),
read_only: false,
}
}
}
pub struct Pty {
input: firkin_vsock::VsockStream,
output: firkin_vsock::VsockStream,
size: PtyConfig,
pub(crate) client: VminitdClient,
pub(crate) container_id: ContainerId,
pub(crate) process_id: ProcessId,
}
impl std::fmt::Debug for Pty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pty")
.field("size", &self.size)
.field("container_id", &self.container_id)
.field("process_id", &self.process_id)
.finish_non_exhaustive()
}
}
impl Pty {
pub(crate) fn new(
input: firkin_vsock::VsockStream,
output: firkin_vsock::VsockStream,
size: PtyConfig,
client: VminitdClient,
container_id: ContainerId,
process_id: ProcessId,
) -> Self {
Self {
input,
output,
size,
client,
container_id,
process_id,
}
}
pub async fn resize(&mut self, size: impl Into<PtyConfig>) -> Result<()> {
let size = size.into();
self.client
.resize_process(tonic::Request::new(ProcessCreate::resize_request(
&self.process_id,
Some(&self.container_id),
u32::from(size.rows),
u32::from(size.cols),
)))
.await
.map_err(runtime_rpc_error("resize pty"))?;
self.size = size;
Ok(())
}
#[must_use]
pub const fn size(&self) -> PtyConfig {
self.size
}
#[must_use]
pub fn split(self) -> (PtyInput, PtyOutput, PtyControl) {
(
PtyInput { inner: self.input },
PtyOutput { inner: self.output },
PtyControl {
size: self.size,
client: self.client,
container_id: self.container_id,
process_id: self.process_id,
},
)
}
}
impl AsyncRead for Pty {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.output).poll_read(cx, buf)
}
}
impl AsyncWrite for Pty {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.input).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.input).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.input).poll_shutdown(cx)
}
}
impl sealed::Sealed for Pty {}
impl ContainerStdio for Pty {
const TERMINAL: bool = true;
}
#[derive(Debug)]
pub struct PtyInput {
inner: firkin_vsock::VsockStream,
}
impl AsyncWrite for PtyInput {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.inner).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_shutdown(cx)
}
}
#[derive(Debug)]
pub struct PtyOutput {
inner: firkin_vsock::VsockStream,
}
impl AsyncRead for PtyOutput {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_read(cx, buf)
}
}
#[derive(Debug)]
pub struct PtyControl {
size: PtyConfig,
pub(crate) client: VminitdClient,
pub(crate) container_id: ContainerId,
pub(crate) process_id: ProcessId,
}
impl PtyControl {
pub async fn resize(&mut self, size: impl Into<PtyConfig>) -> Result<()> {
let size = size.into();
self.client
.resize_process(tonic::Request::new(ProcessCreate::resize_request(
&self.process_id,
Some(&self.container_id),
u32::from(size.rows),
u32::from(size.cols),
)))
.await
.map_err(runtime_rpc_error("resize pty"))?;
self.size = size;
Ok(())
}
#[must_use]
pub const fn size(&self) -> PtyConfig {
self.size
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Stdio {
#[default]
Null,
Piped,
Inherit,
}
impl Stdio {
#[must_use]
pub const fn null() -> Self {
Self::Null
}
#[must_use]
pub const fn piped() -> Self {
Self::Piped
}
#[must_use]
pub const fn inherit() -> Self {
Self::Inherit
}
}
#[derive(Debug)]
pub struct ChildStdin {
inner: firkin_vsock::VsockStream,
}
impl ChildStdin {
pub(crate) fn new(inner: firkin_vsock::VsockStream) -> Self {
Self { inner }
}
#[must_use]
pub fn into_inner(self) -> firkin_vsock::VsockStream {
self.inner
}
}
impl AsyncWrite for ChildStdin {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.inner).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_shutdown(cx)
}
}
#[derive(Debug)]
pub struct ChildStdout {
inner: firkin_vsock::VsockStream,
}
impl ChildStdout {
pub(crate) fn new(inner: firkin_vsock::VsockStream) -> Self {
Self { inner }
}
#[must_use]
pub fn into_inner(self) -> firkin_vsock::VsockStream {
self.inner
}
}
impl AsyncRead for ChildStdout {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_read(cx, buf)
}
}
#[derive(Debug)]
pub struct ChildStderr {
inner: firkin_vsock::VsockStream,
}
impl ChildStderr {
pub(crate) fn new(inner: firkin_vsock::VsockStream) -> Self {
Self { inner }
}
#[must_use]
pub fn into_inner(self) -> firkin_vsock::VsockStream {
self.inner
}
}
impl AsyncRead for ChildStderr {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_read(cx, buf)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct PreparedFileMount {
pub(crate) tag: VirtiofsTag,
pub(crate) parent: PathBuf,
filename: String,
pub(crate) guest_holding_path: String,
container_destination: PathBuf,
pub(crate) read_only: bool,
}
impl PreparedFileMount {
fn from_mount(mount: &FileMount) -> Result<Self> {
let source =
std::fs::canonicalize(&mount.source).map_err(|error| Error::RuntimeOperation {
operation: "prepare file mount",
reason: format!("{}: {error}", mount.source.display()),
})?;
let metadata = std::fs::metadata(&source).map_err(|error| Error::RuntimeOperation {
operation: "prepare file mount",
reason: format!("{}: {error}", source.display()),
})?;
if !metadata.is_file() {
return Err(Error::RuntimeOperation {
operation: "prepare file mount",
reason: format!("{} is not a regular file", source.display()),
});
}
let filename = source
.file_name()
.and_then(|filename| filename.to_str())
.ok_or_else(|| Error::RuntimeOperation {
operation: "prepare file mount",
reason: format!("{} has no UTF-8 file name", source.display()),
})?
.to_owned();
let parent = source
.parent()
.ok_or_else(|| Error::RuntimeOperation {
operation: "prepare file mount",
reason: format!("{} has no parent directory", source.display()),
})?
.to_path_buf();
let tag = file_mount_tag(&parent)?;
let guest_holding_path = format!("/run/file-mounts/{}", tag.as_str());
Ok(Self {
tag,
parent,
filename,
guest_holding_path,
container_destination: mount.destination.clone(),
read_only: mount.read_only,
})
}
pub(crate) fn oci_bind_mount(&self) -> Result<Mount> {
let destination =
self.container_destination
.to_str()
.ok_or_else(|| Error::RuntimeOperation {
operation: "build file bind mount",
reason: format!(
"file mount destination {} is not valid UTF-8",
self.container_destination.display()
),
})?;
let mut mount = Mount::custom(
"none",
format!("{}/{}", self.guest_holding_path, self.filename),
destination,
)
.extra_option("bind");
mount = if self.read_only {
mount.read_only()
} else {
mount.extra_option("rw")
};
Ok(mount)
}
}
pub(crate) fn prepare_file_mounts(file_mounts: &[FileMount]) -> Result<Vec<PreparedFileMount>> {
file_mounts
.iter()
.map(PreparedFileMount::from_mount)
.collect()
}
fn file_mount_tag(parent: &Path) -> Result<VirtiofsTag> {
let parent = parent.to_str().ok_or_else(|| Error::RuntimeOperation {
operation: "prepare file mount",
reason: format!("file mount parent {} is not valid UTF-8", parent.display()),
})?;
let digest = Sha256::digest(parent.as_bytes());
let hex = format!("{digest:x}");
VirtiofsTag::new(hex[..36].to_owned()).map_err(|error| Error::RuntimeOperation {
operation: "prepare file mount",
reason: error.to_string(),
})
}