]> git.proxmox.com Git - proxmox-backup.git/commitdiff
file-restore-daemon: add binary with virtio-vsock API server
authorStefan Reiter <s.reiter@proxmox.com>
Wed, 31 Mar 2021 10:21:52 +0000 (12:21 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Thu, 8 Apr 2021 11:57:57 +0000 (13:57 +0200)
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@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Cargo.toml
Makefile
debian/control
debian/proxmox-file-restore.install
src/api2/types/file_restore.rs [new file with mode: 0644]
src/api2/types/mod.rs
src/bin/proxmox-restore-daemon.rs [new file with mode: 0644]
src/bin/proxmox_restore_daemon/api.rs [new file with mode: 0644]
src/bin/proxmox_restore_daemon/auth.rs [new file with mode: 0644]
src/bin/proxmox_restore_daemon/mod.rs [new file with mode: 0644]

index 1805103a3f35114901feaccd6a6e422e795ebba2..7802f951d379a74bb36b9a947ae403a1e3ed4724 100644 (file)
@@ -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"
index ec52d88fbbf28b5e81f4f3d328393845e3712177..269bb80c21a5deabec940ceb1f049f1a7f336180 100644 (file)
--- 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), \
index 331920f0a88ce85bade062191bdd125d54d1058a..b9549633401728d2ad9c81440df1d79284bf022e 100644 (file)
@@ -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,
index 2082e46b2d1c0e724c307165dd2a3b237d9c24c0..d952836ebeab785be15045548e4eb9d5bf9277ee 100644 (file)
@@ -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 (file)
index 0000000..cd8df16
--- /dev/null
@@ -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,
+}
+
index 1bd4f92aede148621fd1bfc732abefaa52639d95..19186ea26b4869e15c69e3a9dcfd29a715a3f4b1 100644 (file)
@@ -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 (file)
index 0000000..e803238
--- /dev/null
@@ -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 (file)
index 0000000..2dec11f
--- /dev/null
@@ -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 (file)
index 0000000..0973849
--- /dev/null
@@ -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<Authid, AuthError> {
+        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<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 (file)
index 0000000..8396ebc
--- /dev/null
@@ -0,0 +1,5 @@
+///! File restore VM related functionality
+mod api;
+pub use api::*;
+
+pub mod auth;