#![cfg(target_os = "linux")]
use std::collections::HashMap;
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::thread;
use microvm_runtime::guest_metadata::{
DEFAULT_GUEST_METADATA_PORT, OwnedRequest, Response, base64_decode, validate_env_key,
validate_secret_name,
};
use vsock::{VMADDR_CID_ANY, VsockListener};
const DEFAULT_ENV_FILE: &str = "/var/run/microvm-guest/env";
const DEFAULT_SECRETS_DIR: &str = "/var/run/microvm-guest/secrets";
#[derive(Debug, Clone)]
struct DaemonConfig {
port: u32,
env_file: PathBuf,
secrets_dir: PathBuf,
}
impl Default for DaemonConfig {
fn default() -> Self {
Self {
port: DEFAULT_GUEST_METADATA_PORT,
env_file: PathBuf::from(DEFAULT_ENV_FILE),
secrets_dir: PathBuf::from(DEFAULT_SECRETS_DIR),
}
}
}
fn main() {
let cfg = match parse_args() {
Ok(c) => c,
Err(e) => {
eprintln!("microvm-guest-metadatad: {e}");
print_usage();
std::process::exit(2);
}
};
if let Err(e) = ensure_state_dirs(&cfg) {
eprintln!("microvm-guest-metadatad: prepare state dirs: {e}");
std::process::exit(1);
}
let listener = match VsockListener::bind_with_cid_port(VMADDR_CID_ANY, cfg.port) {
Ok(l) => l,
Err(e) => {
eprintln!("microvm-guest-metadatad: bind vsock port {}: {e}", cfg.port);
std::process::exit(1);
}
};
eprintln!(
"microvm-guest-metadatad: listening on vsock port {} (env={}, secrets={})",
cfg.port,
cfg.env_file.display(),
cfg.secrets_dir.display()
);
for incoming in listener.incoming() {
match incoming {
Ok(stream) => {
let cfg = cfg.clone();
thread::spawn(move || serve_connection(stream, &cfg));
}
Err(e) => {
eprintln!("microvm-guest-metadatad: accept failed: {e}");
}
}
}
}
fn parse_args() -> Result<DaemonConfig, String> {
let mut cfg = DaemonConfig::default();
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--port" => {
let v = args.next().ok_or("--port requires a value")?;
cfg.port = v.parse().map_err(|e| format!("invalid --port: {e}"))?;
}
"--env-file" => {
cfg.env_file = PathBuf::from(args.next().ok_or("--env-file requires a value")?);
}
"--secrets-dir" => {
cfg.secrets_dir =
PathBuf::from(args.next().ok_or("--secrets-dir requires a value")?);
}
"--help" | "-h" => {
print_usage();
std::process::exit(0);
}
other => return Err(format!("unknown argument: {other}")),
}
}
Ok(cfg)
}
fn print_usage() {
eprintln!("usage: microvm-guest-metadatad [--port N] [--env-file PATH] [--secrets-dir PATH]");
}
fn ensure_state_dirs(cfg: &DaemonConfig) -> std::io::Result<()> {
if let Some(parent) = cfg.env_file.parent() {
fs::create_dir_all(parent)?;
fs::set_permissions(parent, fs::Permissions::from_mode(0o700))?;
}
fs::create_dir_all(&cfg.secrets_dir)?;
fs::set_permissions(&cfg.secrets_dir, fs::Permissions::from_mode(0o700))?;
Ok(())
}
fn serve_connection<S>(stream: S, cfg: &DaemonConfig)
where
S: std::io::Read + std::io::Write + Send + 'static,
{
let stream = ReadWriteShim(stream);
let mut reader = BufReader::new(stream);
loop {
let mut line = String::new();
let n = match reader.read_line(&mut line) {
Ok(n) => n,
Err(e) => {
eprintln!("microvm-guest-metadatad: read failed: {e}");
return;
}
};
if n == 0 {
return; }
let resp = handle_line(line.trim_end_matches(['\r', '\n']), cfg);
let mut encoded = match serde_json::to_string(&resp) {
Ok(s) => s,
Err(e) => {
eprintln!("microvm-guest-metadatad: encode response: {e}");
continue;
}
};
encoded.push('\n');
if let Err(e) = reader.get_mut().write_all(encoded.as_bytes()) {
eprintln!("microvm-guest-metadatad: write response: {e}");
return;
}
}
}
fn handle_line(line: &str, cfg: &DaemonConfig) -> Response {
let req: OwnedRequest = match serde_json::from_str(line) {
Ok(r) => r,
Err(e) => {
return Response {
id: String::new(),
ok: false,
error: Some(format!("invalid request json: {e}")),
};
}
};
let id = req.id().to_string();
let result = match req {
OwnedRequest::Ping { .. } => Ok(()),
OwnedRequest::SetEnv { env, .. } => write_env_file(&cfg.env_file, &env),
OwnedRequest::SetSecret {
name, value_b64, ..
} => write_secret(&cfg.secrets_dir, &name, &value_b64),
};
match result {
Ok(()) => Response {
id,
ok: true,
error: None,
},
Err(e) => Response {
id,
ok: false,
error: Some(e),
},
}
}
fn write_env_file(path: &Path, env: &HashMap<String, String>) -> Result<(), String> {
for k in env.keys() {
validate_env_key(k).map_err(|e| e.to_string())?;
}
let mut keys: Vec<&String> = env.keys().collect();
keys.sort();
let mut buf = String::new();
for k in keys {
let v = &env[k];
if v.is_empty() {
continue;
}
if v.contains('\n') {
return Err(format!("env value for {k:?} contains newline"));
}
buf.push_str(k);
buf.push('=');
buf.push_str(v);
buf.push('\n');
}
atomic_write(path, buf.as_bytes(), 0o600)
}
fn write_secret(dir: &Path, name: &str, value_b64: &str) -> Result<(), String> {
validate_secret_name(name).map_err(|e| e.to_string())?;
let bytes = base64_decode(value_b64).map_err(|e| e.to_string())?;
let target = dir.join(name);
atomic_write(&target, &bytes, 0o600)
}
fn atomic_write(path: &Path, bytes: &[u8], mode: u32) -> Result<(), String> {
let parent = path
.parent()
.ok_or_else(|| format!("path has no parent: {}", path.display()))?;
fs::create_dir_all(parent).map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
let file_name = path
.file_name()
.ok_or_else(|| format!("path has no file name: {}", path.display()))?
.to_string_lossy()
.into_owned();
let tmp = parent.join(format!(".{file_name}.tmp.{}", tmp_suffix()));
{
let mut f = OpenOptions::new()
.write(true)
.create_new(true)
.mode(mode)
.open(&tmp)
.map_err(|e| format!("open tmp {}: {e}", tmp.display()))?;
f.write_all(bytes)
.map_err(|e| format!("write tmp {}: {e}", tmp.display()))?;
f.sync_all()
.map_err(|e| format!("fsync tmp {}: {e}", tmp.display()))?;
drop(f);
}
fs::rename(&tmp, path)
.map_err(|e| format!("rename {} -> {}: {e}", tmp.display(), path.display()))?;
if let Ok(dir) = File::open(parent) {
let _ = dir.sync_all();
}
Ok(())
}
fn tmp_suffix() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
format!("{pid}-{n}")
}
struct ReadWriteShim<S>(S);
impl<S: std::io::Read> std::io::Read for ReadWriteShim<S> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.read(buf)
}
}
impl<S: std::io::Write> std::io::Write for ReadWriteShim<S> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.0.flush()
}
}