From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id E6CE06A307 for ; Wed, 24 Mar 2021 16:21:46 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id E49B7C3DF for ; Wed, 24 Mar 2021 16:21:16 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id EAF3BC266 for ; Wed, 24 Mar 2021 16:21:04 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id B13BD46472 for ; Wed, 24 Mar 2021 16:21:04 +0100 (CET) From: Stefan Reiter To: pbs-devel@lists.proxmox.com Date: Wed, 24 Mar 2021 16:18:17 +0100 Message-Id: <20210324151827.26200-11-s.reiter@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20210324151827.26200-1-s.reiter@proxmox.com> References: <20210324151827.26200-1-s.reiter@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.171 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_1 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_3 0.1 random spam to be learned in bayes RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [api.rs, mod.rs, auth.rs, proxmox-restore-daemon.rs] Subject: [pbs-devel] [PATCH v2 proxmox-backup 10/20] file-restore-daemon: add binary with virtio-vsock API server X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 24 Mar 2021 15:21:46 -0000 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 --- 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> { + 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..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 { + 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 { + if auth_data == &self.ticket { + Ok(Authid::root_auth_id().to_owned()) + } else { + bail!("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; -- 2.20.1