[pbs-devel] [PATCH v2 proxmox-backup 10/20] file-restore-daemon: add binary with virtio-vsock API server
Stefan Reiter
s.reiter at proxmox.com
Wed Mar 24 16:18:17 CET 2021
Implements the base of a small daemon to run within a file-restore VM.
The binary spawns an API server on a virtio-vsock socket, listening for
connections from the host. This happens mostly manually via the standard
Unix socket API, since tokio/hyper do not have support for vsock built
in. Once we have the accept'ed file descriptor, we can create a
UnixStream and use our tower service implementation for that.
The binary is deliberately not installed in the usual $PATH location,
since it shouldn't be executed on the host by a user anyway.
For now, only the API calls 'status' and 'stop' are implemented, to
demonstrate and test proxmox::api functionality.
Authorization is provided via a custom ApiAuth only checking a header
value against a static /ticket file.
Since the REST server implementation uses the log!() macro, we can
redirect its output to stdout by registering env_logger as the logging
target. env_logger is already in our dependency tree via zstd/bindgen.
Signed-off-by: Stefan Reiter <s.reiter at proxmox.com>
---
v2:
* implement custom static ticket auth with ApiAuth impl
Cargo.toml | 1 +
Makefile | 9 ++-
debian/control | 1 +
debian/proxmox-file-restore.install | 1 +
src/api2/types/file_restore.rs | 12 +++
src/api2/types/mod.rs | 3 +
src/bin/proxmox-restore-daemon.rs | 108 +++++++++++++++++++++++++
src/bin/proxmox_restore_daemon/api.rs | 62 ++++++++++++++
src/bin/proxmox_restore_daemon/auth.rs | 48 +++++++++++
src/bin/proxmox_restore_daemon/mod.rs | 5 ++
10 files changed, 249 insertions(+), 1 deletion(-)
create mode 100644 src/api2/types/file_restore.rs
create mode 100644 src/bin/proxmox-restore-daemon.rs
create mode 100644 src/bin/proxmox_restore_daemon/api.rs
create mode 100644 src/bin/proxmox_restore_daemon/auth.rs
create mode 100644 src/bin/proxmox_restore_daemon/mod.rs
diff --git a/Cargo.toml b/Cargo.toml
index e849406c..2ffda29f 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"
anyhow = "1.0"
futures = "0.3"
h2 = { version = "0.3", features = [ "stream" ] }
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 5c72b986..de1aa616 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-futures-0.3+default-dev,
librust-h2-0.3+default-dev,
librust-h2-0.3+stream-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 3e720dad..85f7fa14 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<Result<tokio::net::UnixStream, Error>> {
+ use nix::sys::socket::*;
+ let (sender, receiver) = mpsc::channel(MAX_PENDING);
+
+ tokio::spawn(async move {
+ loop {
+ let stream: Result<tokio::net::UnixStream, Error> = 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<RawFd, Error> {
+ 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<f32, Error> {
+ 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<RestoreDaemonStatus, Error> {
+ 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..4a8bc5e0
--- /dev/null
+++ b/src/bin/proxmox_restore_daemon/auth.rs
@@ -0,0 +1,48 @@
+//! Authentication via a static ticket file
+use anyhow::{bail, 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;
+
+const TICKET_FILE: &str = "/ticket";
+
+pub struct StaticAuth {
+ ticket: String,
+}
+
+impl ApiAuth for StaticAuth {
+ type AuthData = String;
+
+ fn extract_auth_data(&self, headers: &http::HeaderMap) -> Option<Self::AuthData> {
+ headers.get(hyper::header::AUTHORIZATION).map(|val| val.to_str().unwrap_or("").to_owned())
+ }
+
+ fn check_auth(
+ &self,
+ _method: &hyper::Method,
+ auth_data: &Self::AuthData,
+ _user_info: &CachedUserInfo,
+ ) -> Result<Authid, Error> {
+ if auth_data == &self.ticket {
+ Ok(Authid::root_auth_id().to_owned())
+ } else {
+ bail!("invalid file restore ticket provided")
+ }
+ }
+}
+
+pub fn ticket_auth() -> Result<StaticAuth, Error> {
+ 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;
--
2.20.1
More information about the pbs-devel
mailing list