#[cfg(target_os = "linux")]
mod agent {
use std::collections::HashMap;
use std::io::{Read, Write};
use std::os::unix::io::RawFd;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use serde::Deserialize;
pub const AGENT_PORT: u32 = 52;
const MSG_START: u8 = 0x01;
const MSG_STDIN: u8 = 0x02;
const MSG_RESIZE: u8 = 0x03;
const MSG_EOF: u8 = 0x04;
const MSG_CLOCK_SYNC: u8 = 0x05;
const MSG_STDOUT: u8 = 0x10;
const MSG_STDERR: u8 = 0x11;
const MSG_EXIT: u8 = 0x12;
use arcbox_vm::boot_proto::KernelIpParam;
use arcbox_vm::file_io::proto::{
FILE_ACK, FILE_DATA, FILE_DONE, FILE_ERR, FILE_PORT, FILE_READ_REQ, FILE_WRITE_REQ,
MAX_FILE_SIZE,
};
const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
const MAX_ACTIVE_CONNECTIONS: usize = 64;
const THREAD_STACK_SIZE: usize = 1 << 20;
#[derive(Debug, Deserialize)]
struct StartCommand {
cmd: Vec<String>,
#[serde(default)]
env: HashMap<String, String>,
#[serde(default)]
working_dir: String,
#[serde(default, rename = "user")]
_user: String,
#[serde(default)]
tty: bool,
#[serde(default = "default_tty_width")]
tty_width: u16,
#[serde(default = "default_tty_height")]
tty_height: u16,
#[serde(default)]
timeout_seconds: u32,
}
fn default_tty_width() -> u16 {
80
}
fn default_tty_height() -> u16 {
24
}
struct VsockStream {
fd: RawFd,
}
impl VsockStream {
unsafe fn from_raw_fd(fd: RawFd) -> Self {
Self { fd }
}
}
impl Read for VsockStream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let n =
unsafe { libc::read(self.fd, buf.as_mut_ptr().cast::<libc::c_void>(), buf.len()) };
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(n as usize)
}
}
}
impl Write for VsockStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let n = unsafe { libc::write(self.fd, buf.as_ptr().cast::<libc::c_void>(), buf.len()) };
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(n as usize)
}
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl Drop for VsockStream {
fn drop(&mut self) {
unsafe { libc::close(self.fd) };
}
}
fn read_frame(r: &mut impl Read) -> std::io::Result<(u8, Vec<u8>)> {
let mut type_buf = [0u8; 1];
r.read_exact(&mut type_buf)?;
let mut len_buf = [0u8; 4];
r.read_exact(&mut len_buf)?;
let len = u32::from_le_bytes(len_buf) as usize;
if len > MAX_FRAME_SIZE {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("frame too large: {len} bytes (max {MAX_FRAME_SIZE})"),
));
}
let mut payload = vec![0u8; len];
if len > 0 {
r.read_exact(&mut payload)?;
}
Ok((type_buf[0], payload))
}
fn write_frame(w: &mut impl Write, msg_type: u8, payload: &[u8]) -> std::io::Result<()> {
let mut buf = Vec::with_capacity(5 + payload.len());
buf.push(msg_type);
buf.extend_from_slice(&(payload.len() as u32).to_le_bytes());
buf.extend_from_slice(payload);
w.write_all(&buf)
}
fn handle_connection(conn_fd: RawFd) {
let mut conn = unsafe { VsockStream::from_raw_fd(conn_fd) };
let (msg_type, payload) = match read_frame(&mut conn) {
Ok(f) => f,
Err(e) => {
eprintln!("agent: read first frame: {e}");
return;
}
};
match msg_type {
MSG_CLOCK_SYNC => handle_clock_sync(conn, &payload),
MSG_START => {
let start: StartCommand = match serde_json::from_slice(&payload) {
Ok(s) => s,
Err(e) => {
eprintln!("agent: parse StartCommand: {e}");
return;
}
};
if start.tty {
handle_tty(conn, start);
} else {
handle_piped(conn, start);
}
}
other => {
eprintln!("agent: unexpected frame type 0x{other:02x} on exec port");
}
}
}
fn handle_clock_sync(mut conn: VsockStream, payload: &[u8]) {
if payload.len() < 12 {
eprintln!(
"agent: MSG_CLOCK_SYNC: payload too short ({} bytes)",
payload.len()
);
let _ = write_frame(&mut conn, MSG_EXIT, &(-1i32).to_le_bytes());
return;
}
let secs = i64::from_le_bytes(payload[..8].try_into().unwrap());
let nanos = u32::from_le_bytes(payload[8..12].try_into().unwrap());
let ret = unsafe {
let ts = libc::timespec {
tv_sec: secs,
tv_nsec: libc::c_long::from(nanos),
};
libc::clock_settime(libc::CLOCK_REALTIME, &raw const ts)
};
if ret != 0 {
eprintln!(
"agent: clock_settime failed: {}",
std::io::Error::last_os_error()
);
let _ = write_frame(&mut conn, MSG_EXIT, &(-1i32).to_le_bytes());
return;
}
let _ = write_frame(&mut conn, MSG_EXIT, &0i32.to_le_bytes());
}
fn handle_file_connection(conn_fd: RawFd) {
let mut conn = unsafe { VsockStream::from_raw_fd(conn_fd) };
let (msg_type, payload) = match read_frame(&mut conn) {
Ok(f) => f,
Err(e) => {
eprintln!("agent: file: read first frame: {e}");
return;
}
};
match msg_type {
FILE_WRITE_REQ => handle_file_write(conn, &payload),
FILE_READ_REQ => handle_file_read(conn, &payload),
other => eprintln!("agent: file: unexpected frame type 0x{other:02x}"),
}
}
#[derive(serde::Deserialize)]
struct WriteReq {
path: String,
#[serde(default)]
mode: u32,
}
#[derive(serde::Deserialize)]
struct ReadReq {
path: String,
}
fn handle_file_write(mut conn: VsockStream, header_payload: &[u8]) {
let req: WriteReq = match serde_json::from_slice(header_payload) {
Ok(r) => r,
Err(e) => {
let _ = write_frame(
&mut conn,
FILE_ERR,
format!("parse WriteReq: {e}").as_bytes(),
);
return;
}
};
let mode = if req.mode == 0 { 0o644 } else { req.mode };
let mut data: Vec<u8> = Vec::new();
loop {
match read_frame(&mut conn) {
Ok((FILE_DATA, chunk)) => {
data.extend_from_slice(&chunk);
if data.len() > MAX_FILE_SIZE {
let _ = write_frame(
&mut conn,
FILE_ERR,
format!("file too large (>{} bytes)", MAX_FILE_SIZE).as_bytes(),
);
return;
}
}
Ok((FILE_DONE, _)) => break,
Ok((other, _)) => {
let _ = write_frame(
&mut conn,
FILE_ERR,
format!("expected FILE_DATA/DONE, got 0x{other:02x}").as_bytes(),
);
return;
}
Err(e) => {
let _ = write_frame(&mut conn, FILE_ERR, format!("read data: {e}").as_bytes());
return;
}
}
}
let path = std::path::Path::new(&req.path);
if let Some(parent) = path.parent()
&& let Err(e) = std::fs::create_dir_all(parent)
{
let _ = write_frame(&mut conn, FILE_ERR, format!("create dirs: {e}").as_bytes());
return;
}
use std::os::unix::fs::OpenOptionsExt;
let result = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(mode)
.open(path)
.and_then(|mut f| {
use std::io::Write;
f.write_all(&data)
});
match result {
Ok(()) => {
let _ = write_frame(&mut conn, FILE_ACK, &[]);
}
Err(e) => {
let _ = write_frame(&mut conn, FILE_ERR, format!("write file: {e}").as_bytes());
}
}
}
fn handle_file_read(mut conn: VsockStream, header_payload: &[u8]) {
let req: ReadReq = match serde_json::from_slice(header_payload) {
Ok(r) => r,
Err(e) => {
let _ = write_frame(
&mut conn,
FILE_ERR,
format!("parse ReadReq: {e}").as_bytes(),
);
return;
}
};
let data = match std::fs::read(&req.path) {
Ok(d) => d,
Err(e) => {
let _ = write_frame(&mut conn, FILE_ERR, format!("read file: {e}").as_bytes());
return;
}
};
for chunk in data.chunks(MAX_FRAME_SIZE) {
if write_frame(&mut conn, FILE_DATA, chunk).is_err() {
return;
}
}
let _ = write_frame(&mut conn, FILE_DONE, &[]);
}
fn handle_piped(conn: VsockStream, start: StartCommand) {
use std::process::{Command, Stdio};
let mut cmd = Command::new(start.cmd.first().expect("empty cmd"));
cmd.args(start.cmd.get(1..).unwrap_or(&[]));
cmd.envs(&start.env);
if !start.working_dir.is_empty() {
cmd.current_dir(&start.working_dir);
}
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
eprintln!("agent: spawn {:?}: {e}", start.cmd);
return;
}
};
let mut child_stdin = child.stdin.take().unwrap();
let child_stdout = child.stdout.take().unwrap();
let child_stderr = child.stderr.take().unwrap();
let writer: Arc<Mutex<VsockStream>> = Arc::new(Mutex::new(conn));
let w1 = Arc::clone(&writer);
let t_stdout = thread::spawn(move || {
let mut buf = [0u8; 4096];
let mut out = child_stdout;
loop {
match out.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
let _ = write_frame(&mut *w1.lock().unwrap(), MSG_STDOUT, &buf[..n]);
}
}
}
});
let w2 = Arc::clone(&writer);
let t_stderr = thread::spawn(move || {
let mut buf = [0u8; 4096];
let mut err = child_stderr;
loop {
match err.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
let _ = write_frame(&mut *w2.lock().unwrap(), MSG_STDERR, &buf[..n]);
}
}
}
});
if start.timeout_seconds > 0 {
let pid = child.id();
let timeout = start.timeout_seconds;
thread::spawn(move || {
thread::sleep(std::time::Duration::from_secs(timeout as u64));
unsafe {
#[allow(clippy::cast_possible_wrap)]
let pid_i32 = pid as i32;
if libc::kill(pid_i32, 0) == 0 {
let _ = libc::kill(pid_i32, libc::SIGKILL);
}
}
});
}
let read_fd = unsafe { libc::dup(writer.lock().unwrap().fd) };
let mut reader = unsafe { VsockStream::from_raw_fd(read_fd) };
loop {
match read_frame(&mut reader) {
Ok((MSG_STDIN, data)) => {
if child_stdin.write_all(&data).is_err() {
break;
}
}
Ok((MSG_EOF, _)) | Err(_) => {
drop(child_stdin);
break;
}
Ok(_) => {}
}
}
let _ = t_stdout.join();
let _ = t_stderr.join();
let exit_code = child.wait().map_or(-1, |s| s.code().unwrap_or(-1));
let _ = write_frame(
&mut *writer.lock().unwrap(),
MSG_EXIT,
&exit_code.to_le_bytes(),
);
}
fn handle_tty(conn: VsockStream, start: StartCommand) {
use nix::pty::OpenptyResult;
use nix::unistd::{ForkResult, fork, setsid};
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
let OpenptyResult { master, slave } = match nix::pty::openpty(None, None) {
Ok(r) => r,
Err(e) => {
eprintln!("agent: openpty: {e}");
return;
}
};
if start.tty_width > 0 && start.tty_height > 0 {
let winsize = libc::winsize {
ws_col: start.tty_width,
ws_row: start.tty_height,
ws_xpixel: 0,
ws_ypixel: 0,
};
unsafe { libc::ioctl(master.as_raw_fd(), libc::TIOCSWINSZ, &winsize) };
}
let master_fd: RawFd = master.into_raw_fd();
let slave_fd: RawFd = slave.as_raw_fd();
match unsafe { fork() } {
Err(e) => {
unsafe { libc::close(master_fd) };
eprintln!("agent: fork: {e}");
}
Ok(ForkResult::Child) => {
unsafe { libc::close(master_fd) };
let _ = setsid();
unsafe {
libc::ioctl(slave_fd, libc::TIOCSCTTY, 0);
libc::dup2(slave_fd, libc::STDIN_FILENO);
libc::dup2(slave_fd, libc::STDOUT_FILENO);
libc::dup2(slave_fd, libc::STDERR_FILENO);
if slave_fd > libc::STDERR_FILENO {
libc::close(slave_fd);
}
}
let cstrings: Vec<std::ffi::CString> = start
.cmd
.iter()
.filter_map(|s| std::ffi::CString::new(s.as_str()).ok())
.collect();
let mut argv: Vec<*const libc::c_char> =
cstrings.iter().map(|s| s.as_ptr()).collect();
argv.push(std::ptr::null());
for (k, v) in &start.env {
if let (Ok(ck), Ok(cv)) = (
std::ffi::CString::new(k.as_str()),
std::ffi::CString::new(v.as_str()),
) {
unsafe { libc::setenv(ck.as_ptr(), cv.as_ptr(), 1) };
}
}
if !start.working_dir.is_empty()
&& let Ok(cwd) = std::ffi::CString::new(start.working_dir.as_str())
{
unsafe { libc::chdir(cwd.as_ptr()) };
}
unsafe { libc::execvp(argv[0], argv.as_ptr()) };
unsafe { libc::_exit(127) };
}
Ok(ForkResult::Parent { child }) => {
drop(slave);
let writer: Arc<Mutex<VsockStream>> = Arc::new(Mutex::new(conn));
let w_read = Arc::clone(&writer);
let t_pty = thread::spawn(move || {
let mut buf = [0u8; 4096];
let mut r = unsafe { VsockStream::from_raw_fd(libc::dup(master_fd)) };
loop {
match r.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
let _ = write_frame(
&mut *w_read.lock().unwrap(),
MSG_STDOUT,
&buf[..n],
);
}
}
}
});
let read_fd = unsafe { libc::dup(writer.lock().unwrap().fd) };
let mut reader = unsafe { VsockStream::from_raw_fd(read_fd) };
let mut master_writer = unsafe { std::fs::File::from_raw_fd(master_fd) };
loop {
match read_frame(&mut reader) {
Ok((MSG_STDIN, data)) => {
let _ = master_writer.write_all(&data);
}
Ok((MSG_RESIZE, data)) if data.len() >= 4 => {
let winsize = libc::winsize {
ws_col: u16::from_le_bytes([data[0], data[1]]),
ws_row: u16::from_le_bytes([data[2], data[3]]),
ws_xpixel: 0,
ws_ypixel: 0,
};
unsafe { libc::ioctl(master_fd, libc::TIOCSWINSZ, &winsize) };
}
Ok((MSG_EOF, _)) | Err(_) => break,
Ok(_) => {}
}
}
let _ = t_pty.join();
let mut status: libc::c_int = 0;
unsafe { libc::waitpid(child.as_raw(), &raw mut status, 0) };
let exit_code = if libc::WIFEXITED(status) {
libc::WEXITSTATUS(status)
} else {
-1
};
let _ = write_frame(
&mut *writer.lock().unwrap(),
MSG_EXIT,
&exit_code.to_le_bytes(),
);
}
}
}
fn mount_filesystems() {
use std::ffi::CString;
let mounts: &[(&str, &str, &str, libc::c_ulong, &str)] = &[
(
"/proc",
"proc",
"proc",
libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC,
"",
),
(
"/sys",
"sysfs",
"sysfs",
libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC,
"",
),
("/dev", "devtmpfs", "devtmpfs", libc::MS_NOSUID, "mode=0755"),
(
"/dev/pts",
"devpts",
"devpts",
libc::MS_NOSUID | libc::MS_NOEXEC,
"newinstance,ptmxmode=0666",
),
];
for (target, source, fstype, flags, data) in mounts {
let _ = std::fs::create_dir_all(target);
let c_source = CString::new(*source).unwrap();
let c_target = CString::new(*target).unwrap();
let c_fstype = CString::new(*fstype).unwrap();
let c_data = CString::new(*data).unwrap();
let ret = unsafe {
libc::mount(
c_source.as_ptr(),
c_target.as_ptr(),
c_fstype.as_ptr(),
*flags,
c_data.as_ptr().cast(),
)
};
if ret != 0 {
eprintln!("mount {target}: {}", std::io::Error::last_os_error());
}
}
let _ = std::os::unix::fs::symlink("/dev/pts/ptmx", "/dev/ptmx");
}
fn cmdline_token(prefix: &str) -> Option<String> {
let cmdline = std::fs::read_to_string("/proc/cmdline").ok()?;
cmdline
.split_whitespace()
.find(|t| t.starts_with(prefix))
.map(str::to_string)
}
fn setup_dns() {
let Some(token) = cmdline_token("ip=") else {
eprintln!("vmm-guest-agent: no ip= parameter in cmdline, skipping DNS setup");
return;
};
let ip_param = match token.parse::<KernelIpParam>() {
Ok(p) => p,
Err(e) => {
eprintln!("vmm-guest-agent: invalid ip= parameter: {e}");
return;
}
};
let content = format!("nameserver {}\n", ip_param.gateway);
match std::fs::write("/etc/resolv.conf", &content) {
Ok(()) => eprintln!(
"vmm-guest-agent: wrote /etc/resolv.conf (nameserver {})",
ip_param.gateway
),
Err(e) => eprintln!("vmm-guest-agent: failed to write /etc/resolv.conf: {e}"),
}
}
fn spawn_bounded(active: &Arc<AtomicUsize>, name: &str, conn_fd: RawFd, handler: fn(RawFd)) {
let current = active.fetch_add(1, Ordering::Relaxed);
if current >= MAX_ACTIVE_CONNECTIONS {
active.fetch_sub(1, Ordering::Relaxed);
eprintln!(
"agent: {name}: connection limit reached ({MAX_ACTIVE_CONNECTIONS}), dropping fd {conn_fd}"
);
unsafe { libc::close(conn_fd) };
return;
}
let active_clone = Arc::clone(active);
let thread_name = format!("{name}-{conn_fd}");
let builder = thread::Builder::new()
.name(thread_name)
.stack_size(THREAD_STACK_SIZE);
if let Err(e) = builder.spawn(move || {
handler(conn_fd);
active_clone.fetch_sub(1, Ordering::Relaxed);
}) {
active.fetch_sub(1, Ordering::Relaxed);
eprintln!("agent: {name}: failed to spawn thread: {e}");
unsafe { libc::close(conn_fd) };
}
}
pub fn run() {
mount_filesystems();
setup_dns();
eprintln!(
"vmm-guest-agent: listening on vsock ports {AGENT_PORT} (exec), {FILE_PORT} (file I/O)"
);
let exec_fd = create_vsock_listener(AGENT_PORT);
let file_fd = create_vsock_listener(FILE_PORT);
let file_active: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let exec_active: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let file_active_clone = Arc::clone(&file_active);
thread::spawn(move || {
loop {
let conn_fd = accept_connection(file_fd);
spawn_bounded(&file_active_clone, "file", conn_fd, handle_file_connection);
}
});
loop {
let conn_fd = accept_connection(exec_fd);
spawn_bounded(&exec_active, "exec", conn_fd, handle_connection);
}
}
fn create_vsock_listener(port: u32) -> RawFd {
let fd = unsafe { libc::socket(libc::AF_VSOCK, libc::SOCK_STREAM, 0) };
assert!(
fd >= 0,
"socket(AF_VSOCK): {}",
std::io::Error::last_os_error()
);
let addr = libc::sockaddr_vm {
svm_family: libc::AF_VSOCK as libc::sa_family_t,
svm_reserved1: 0,
svm_port: port,
svm_cid: libc::VMADDR_CID_ANY,
..unsafe { std::mem::zeroed() }
};
let ret = unsafe {
libc::bind(
fd,
(&raw const addr).cast::<libc::sockaddr>(),
std::mem::size_of::<libc::sockaddr_vm>() as libc::socklen_t,
)
};
assert!(
ret == 0,
"bind vsock port {port}: {}",
std::io::Error::last_os_error()
);
unsafe { libc::listen(fd, 128) };
fd
}
fn accept_connection(server_fd: RawFd) -> RawFd {
loop {
let conn_fd =
unsafe { libc::accept(server_fd, std::ptr::null_mut(), std::ptr::null_mut()) };
if conn_fd >= 0 {
return conn_fd;
}
let err = std::io::Error::last_os_error();
assert!(
err.kind() == std::io::ErrorKind::Interrupted,
"accept: {err}"
);
}
}
}
fn main() {
#[cfg(target_os = "linux")]
agent::run();
#[cfg(not(target_os = "linux"))]
{
eprintln!("vmm-guest-agent requires Linux (AF_VSOCK)");
std::process::exit(1);
}
}