///! Daemon binary to run inside a micro-VM for secure single file restore of disk images use std::fs::File; use std::io::prelude::*; use std::os::unix::{ io::{FromRawFd, RawFd}, net, }; use std::path::Path; use std::sync::{Arc, Mutex}; use anyhow::{bail, format_err, Error}; use lazy_static::lazy_static; use log::{error, info}; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; use proxmox_router::RpcEnvironmentType; use pbs_client::DEFAULT_VSOCK_PORT; use proxmox_rest_server::{ApiConfig, RestServer}; mod proxmox_restore_daemon; use proxmox_restore_daemon::*; /// Maximum amount of pending requests. If saturated, virtio-vsock returns ETIMEDOUT immediately. /// We should never have more than a few requests in queue, so use a low number. pub const MAX_PENDING: usize = 32; /// Will be present in base initramfs pub const VM_DETECT_FILE: &str = "/restore-vm-marker"; lazy_static! { /// The current disks state. Use for accessing data on the attached snapshots. pub static ref DISK_STATE: Arc> = { Arc::new(Mutex::new(DiskState::scan().unwrap())) }; } fn init_disk_state() { info!("scanning all disks..."); { let _disk_state = DISK_STATE.lock().unwrap(); } info!("disk scan complete.") } /// This is expected to be run by 'proxmox-file-restore' within a mini-VM fn main() -> Result<(), Error> { pbs_tools::setup_libc_malloc_opts(); if !Path::new(VM_DETECT_FILE).exists() { bail!( "This binary is not supposed to be run manually, use 'proxmox-file-restore' instead." ); } // don't have a real syslog (and no persistance), so use env_logger to print to a log file (via // stdout to a serial terminal attached by QEMU) env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) .write_style(env_logger::WriteStyle::Never) .format_timestamp_millis() .init(); info!("setup basic system environment..."); setup_system_env().map_err(|err| format_err!("system environment setup failed: {}", err))?; proxmox_async::runtime::main(run()) } /// ensure we have our /run dirs, system users and stuff like that setup fn setup_system_env() -> Result<(), Error> { // the API may save some stuff there, e.g., the memcon tracking file // we do not care much, but it's way less headache to just create it std::fs::create_dir_all("/run/proxmox-backup")?; // we now ensure that all lock files are owned by the backup user, and as we reuse the // specialized REST module from pbs api/daemon we have some checks there for user/acl stuff // that gets locked, and thus needs the backup system user to work. std::fs::create_dir_all("/etc")?; let mut passwd = File::create("/etc/passwd")?; writeln!(passwd, "root:x:0:0:root:/root:/bin/sh")?; writeln!( passwd, "backup:x:34:34:backup:/var/backups:/usr/sbin/nologin" )?; let mut group = File::create("/etc/group")?; writeln!(group, "root:x:0:")?; writeln!(group, "backup:x:34:")?; Ok(()) } async fn run() -> Result<(), Error> { watchdog_init(); let init_future = async move { match tokio::time::timeout( std::time::Duration::from_secs(120), tokio::task::spawn_blocking(init_disk_state), ) .await { Ok(res) => res.map_err(|err| format_err!("disk init failed: {}", err)), Err(_) => bail!("disk init timed out after 120 seconds"), } }; let adaptor = StaticAuthAdapter::new() .map_err(|err| format_err!("reading ticket file failed: {}", err))?; let config = ApiConfig::new("", &ROUTER, RpcEnvironmentType::PUBLIC, adaptor)?; let rest_server = RestServer::new(config); let vsock_fd = get_vsock_fd()?; let connections = accept_vsock_connections(vsock_fd); let receiver_stream = ReceiverStream::new(connections); let acceptor = hyper::server::accept::from_stream(receiver_stream); let hyper_future = async move { hyper::Server::builder(acceptor) .serve(rest_server) .await .map_err(|err| format_err!("hyper finished with error: {}", err)) }; tokio::try_join!(init_future, hyper_future)?; bail!("hyper server exited"); } fn accept_vsock_connections( vsock_fd: RawFd, ) -> mpsc::Receiver> { use nix::sys::socket::*; let (sender, receiver) = mpsc::channel(MAX_PENDING); tokio::spawn(async move { loop { let stream: Result = tokio::task::block_in_place(|| { // we need to accept manually, as UnixListener aborts if socket type != AF_UNIX ... let client_fd = accept(vsock_fd)?; let stream = unsafe { net::UnixStream::from_raw_fd(client_fd) }; stream.set_nonblocking(true)?; tokio::net::UnixStream::from_std(stream).map_err(|err| err.into()) }); match stream { Ok(stream) => { if sender.send(Ok(stream)).await.is_err() { error!("connection accept channel was closed"); } } Err(err) => { error!("error accepting vsock connetion: {}", err); } } } }); receiver } fn get_vsock_fd() -> Result { use nix::sys::socket::*; let sock_fd = socket( AddressFamily::Vsock, SockType::Stream, SockFlag::empty(), None, )?; let sock_addr = VsockAddr::new(libc::VMADDR_CID_ANY, DEFAULT_VSOCK_PORT as u32); bind(sock_fd, &SockAddr::Vsock(sock_addr))?; listen(sock_fd, MAX_PENDING)?; Ok(sock_fd) }