diff --git a/Cargo.toml b/Cargo.toml index 1805103a..7802f951 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ bitflags = "1.2.1" bytes = "1.0" crc32fast = "1" endian_trait = { version = "0.6", features = ["arrays"] } +env_logger = "0.7" flate2 = "1.0" anyhow = "1.0" futures = "0.3" diff --git a/Makefile b/Makefile index ec52d88f..269bb80c 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,10 @@ SERVICE_BIN := \ proxmox-backup-proxy \ proxmox-daily-update +# Single file restore daemon +RESTORE_BIN := \ + proxmox-restore-daemon + ifeq ($(BUILD_MODE), release) CARGO_BUILD_ARGS += --release COMPILEDIR := target/release @@ -40,7 +44,7 @@ endif CARGO ?= cargo COMPILED_BINS := \ - $(addprefix $(COMPILEDIR)/,$(USR_BIN) $(USR_SBIN) $(SERVICE_BIN)) + $(addprefix $(COMPILEDIR)/,$(USR_BIN) $(USR_SBIN) $(SERVICE_BIN) $(RESTORE_BIN)) export DEB_VERSION DEB_VERSION_UPSTREAM @@ -148,6 +152,9 @@ install: $(COMPILED_BINS) install -m755 $(COMPILEDIR)/$(i) $(DESTDIR)$(SBINDIR)/ ; \ install -m644 zsh-completions/_$(i) $(DESTDIR)$(ZSH_COMPL_DEST)/ ;) install -dm755 $(DESTDIR)$(LIBEXECDIR)/proxmox-backup + install -dm755 $(DESTDIR)$(LIBEXECDIR)/proxmox-backup/file-restore + $(foreach i,$(RESTORE_BIN), \ + install -m755 $(COMPILEDIR)/$(i) $(DESTDIR)$(LIBEXECDIR)/proxmox-backup/file-restore/ ;) # install sg-tape-cmd as setuid binary install -m4755 -o root -g root $(COMPILEDIR)/sg-tape-cmd $(DESTDIR)$(LIBEXECDIR)/proxmox-backup/sg-tape-cmd $(foreach i,$(SERVICE_BIN), \ diff --git a/debian/control b/debian/control index 331920f0..b9549633 100644 --- a/debian/control +++ b/debian/control @@ -15,6 +15,7 @@ Build-Depends: debhelper (>= 11), librust-crossbeam-channel-0.5+default-dev, librust-endian-trait-0.6+arrays-dev, librust-endian-trait-0.6+default-dev, + librust-env-logger-0.7+default-dev, librust-flate2-1+default-dev, librust-futures-0.3+default-dev, librust-h2-0.3+default-dev, diff --git a/debian/proxmox-file-restore.install b/debian/proxmox-file-restore.install index 2082e46b..d952836e 100644 --- a/debian/proxmox-file-restore.install +++ b/debian/proxmox-file-restore.install @@ -1,3 +1,4 @@ usr/bin/proxmox-file-restore usr/share/man/man1/proxmox-file-restore.1 usr/share/zsh/vendor-completions/_proxmox-file-restore +usr/lib/x86_64-linux-gnu/proxmox-backup/file-restore/proxmox-restore-daemon diff --git a/src/api2/types/file_restore.rs b/src/api2/types/file_restore.rs new file mode 100644 index 00000000..cd8df16a --- /dev/null +++ b/src/api2/types/file_restore.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; +use proxmox::api::api; + +#[api()] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// General status information about a running VM file-restore daemon +pub struct RestoreDaemonStatus { + /// VM uptime in seconds + pub uptime: i64, +} + diff --git a/src/api2/types/mod.rs b/src/api2/types/mod.rs index 1bd4f92a..19186ea2 100644 --- a/src/api2/types/mod.rs +++ b/src/api2/types/mod.rs @@ -34,6 +34,9 @@ pub use userid::{PROXMOX_TOKEN_ID_SCHEMA, PROXMOX_TOKEN_NAME_SCHEMA, PROXMOX_GRO mod tape; pub use tape::*; +mod file_restore; +pub use file_restore::*; + // File names: may not contain slashes, may not start with "." pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| { if name.starts_with('.') { diff --git a/src/bin/proxmox-restore-daemon.rs b/src/bin/proxmox-restore-daemon.rs new file mode 100644 index 00000000..e803238a --- /dev/null +++ b/src/bin/proxmox-restore-daemon.rs @@ -0,0 +1,108 @@ +///! Daemon binary to run inside a micro-VM for secure single file restore of disk images +use anyhow::{bail, format_err, Error}; +use log::error; + +use std::os::unix::{ + io::{FromRawFd, RawFd}, + net, +}; +use std::path::Path; +use std::sync::Arc; + +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; + +use proxmox::api::RpcEnvironmentType; +use proxmox_backup::client::DEFAULT_VSOCK_PORT; +use proxmox_backup::server::{rest::*, ApiConfig}; + +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"; + +/// This is expected to be run by 'proxmox-file-restore' within a mini-VM +fn main() -> Result<(), Error> { + if !Path::new(VM_DETECT_FILE).exists() { + bail!(concat!( + "This binary is not supposed to be run manually. ", + "Please 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::from_env(env_logger::Env::default().default_filter_or("info")) + .write_style(env_logger::WriteStyle::Never) + .init(); + + proxmox_backup::tools::runtime::main(run()) +} + +async fn run() -> Result<(), Error> { + let auth_config = Arc::new( + auth::ticket_auth().map_err(|err| format_err!("reading ticket file failed: {}", err))?, + ); + let config = ApiConfig::new("", &ROUTER, RpcEnvironmentType::PUBLIC, auth_config)?; + 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); + + hyper::Server::builder(acceptor).serve(rest_server).await?; + + 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) +} diff --git a/src/bin/proxmox_restore_daemon/api.rs b/src/bin/proxmox_restore_daemon/api.rs new file mode 100644 index 00000000..2dec11fe --- /dev/null +++ b/src/bin/proxmox_restore_daemon/api.rs @@ -0,0 +1,62 @@ +///! File-restore API running inside the restore VM +use anyhow::Error; +use serde_json::Value; +use std::fs; + +use proxmox::api::{api, ApiMethod, Permission, Router, RpcEnvironment, SubdirMap}; +use proxmox::list_subdirs_api_method; + +use proxmox_backup::api2::types::*; + +// NOTE: All API endpoints must have Permission::Superuser, as the configs for authentication do +// not exist within the restore VM. Safety is guaranteed by checking a ticket via a custom ApiAuth. + +const SUBDIRS: SubdirMap = &[ + ("status", &Router::new().get(&API_METHOD_STATUS)), + ("stop", &Router::new().get(&API_METHOD_STOP)), +]; + +pub const ROUTER: Router = Router::new() + .get(&list_subdirs_api_method!(SUBDIRS)) + .subdirs(SUBDIRS); + +fn read_uptime() -> Result { + let uptime = fs::read_to_string("/proc/uptime")?; + // unwrap the Option, if /proc/uptime is empty we have bigger problems + Ok(uptime.split_ascii_whitespace().next().unwrap().parse()?) +} + +#[api( + access: { + description: "Permissions are handled outside restore VM.", + permission: &Permission::Superuser, + }, + returns: { + type: RestoreDaemonStatus, + } +)] +/// General status information +fn status( + _param: Value, + _info: &ApiMethod, + _rpcenv: &mut dyn RpcEnvironment, +) -> Result { + Ok(RestoreDaemonStatus { + uptime: read_uptime()? as i64, + }) +} + +#[api( + access: { + description: "Permissions are handled outside restore VM.", + permission: &Permission::Superuser, + }, +)] +/// Stop the restore VM immediately, this will never return if successful +fn stop() { + use nix::sys::reboot; + println!("/stop called, shutting down"); + let err = reboot::reboot(reboot::RebootMode::RB_POWER_OFF).unwrap_err(); + println!("'reboot' syscall failed: {}", err); + std::process::exit(1); +} diff --git a/src/bin/proxmox_restore_daemon/auth.rs b/src/bin/proxmox_restore_daemon/auth.rs new file mode 100644 index 00000000..0973849e --- /dev/null +++ b/src/bin/proxmox_restore_daemon/auth.rs @@ -0,0 +1,45 @@ +//! Authentication via a static ticket file +use anyhow::{bail, format_err, Error}; + +use std::fs::File; +use std::io::prelude::*; + +use proxmox_backup::api2::types::Authid; +use proxmox_backup::config::cached_user_info::CachedUserInfo; +use proxmox_backup::server::auth::{ApiAuth, AuthError}; + +const TICKET_FILE: &str = "/ticket"; + +pub struct StaticAuth { + ticket: String, +} + +impl ApiAuth for StaticAuth { + fn check_auth( + &self, + headers: &http::HeaderMap, + _method: &hyper::Method, + _user_info: &CachedUserInfo, + ) -> Result { + match headers.get(hyper::header::AUTHORIZATION) { + Some(header) if header.to_str().unwrap_or("") == &self.ticket => { + Ok(Authid::root_auth_id().to_owned()) + } + _ => { + return Err(AuthError::Generic(format_err!( + "invalid file restore ticket provided" + ))); + } + } + } +} + +pub fn ticket_auth() -> Result { + let mut ticket_file = File::open(TICKET_FILE)?; + let mut ticket = String::new(); + let len = ticket_file.read_to_string(&mut ticket)?; + if len <= 0 { + bail!("invalid ticket: cannot be empty"); + } + Ok(StaticAuth { ticket }) +} diff --git a/src/bin/proxmox_restore_daemon/mod.rs b/src/bin/proxmox_restore_daemon/mod.rs new file mode 100644 index 00000000..8396ebc5 --- /dev/null +++ b/src/bin/proxmox_restore_daemon/mod.rs @@ -0,0 +1,5 @@ +///! File restore VM related functionality +mod api; +pub use api::*; + +pub mod auth;