[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