diff --git a/sidebus-broker/src/main.rs b/sidebus-broker/src/main.rs index fc79898..5946f48 100644 --- a/sidebus-broker/src/main.rs +++ b/sidebus-broker/src/main.rs @@ -4,6 +4,7 @@ mod vsock; use bus::SharedHostedBus; use clap::Parser; +use eyre::OptionExt; use futures::{TryFutureExt, stream::FuturesUnordered}; use std::{path::PathBuf, sync::Arc, time::Duration}; use tokio::{net::UnixListener, process::Command, sync::Mutex}; @@ -36,6 +37,10 @@ struct BrokerCli { #[clap(long, default_value = "/run/vm-doc-portal")] guest_mountpoint: PathBuf, + /// Mappings from guest paths to host paths for passthrough file systems (for file transfer), in guest=host format + #[clap(long)] + path_mapping: Vec, + /// Vsock port number to listen on for the VM bus #[clap(long)] vsock_port: Option, @@ -65,11 +70,31 @@ async fn new_hosted_bus() -> eyre::Result<( Ok((Arc::new(Mutex::new(bus)), guid, owner_stream)) } +fn parse_path_mapping(s: &str) -> eyre::Result<(PathBuf, PathBuf)> { + let mut split = s.split('='); + let guest_path = PathBuf::from(split.next().ok_or_eyre("failed to split mapping")?); + let host_path = PathBuf::from(split.next().ok_or_eyre("failed to split mapping")?); + Ok((guest_path, host_path)) +} + #[tokio::main] async fn main() -> eyre::Result<()> { tracing_subscriber::fmt::init(); let cli = BrokerCli::parse(); + let mut path_prefix_to_host: Vec<(PathBuf, PathBuf)> = cli + .path_mapping + .iter() + .flat_map(|arg| match parse_path_mapping(arg) { + Ok(mapping) => Some(mapping), + Err(err) => { + error!(?err, %arg, "could not parse path mapping"); + None + } + }) + .collect(); + path_prefix_to_host.sort_unstable_by_key(|(prefix, _)| -(prefix.as_os_str().len() as isize)); + debug!(?path_prefix_to_host, "parsed path mappings"); let (priv_bus, _, mut priv_lst) = new_hosted_bus().await?; @@ -153,6 +178,7 @@ async fn main() -> eyre::Result<()> { &host_session_conn, &priv_bus_conn, cli.guest_mountpoint, + path_prefix_to_host, ) .await?; let notification_imp = portal::notification::Notification::new(&host_session_conn).await?; diff --git a/sidebus-broker/src/portal/file_transfer.rs b/sidebus-broker/src/portal/file_transfer.rs index 9695b6d..8c175f6 100644 --- a/sidebus-broker/src/portal/file_transfer.rs +++ b/sidebus-broker/src/portal/file_transfer.rs @@ -1,4 +1,9 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{ + collections::HashMap, + os::fd::{AsFd, AsRawFd}, + os::unix::ffi::OsStrExt, + path::PathBuf, +}; use tokio::sync::broadcast; use tokio_stream::StreamExt; @@ -14,6 +19,7 @@ pub struct FileTransfer { host: FileTransferProxy<'static>, file_transformer: FileTransformer, tx: broadcast::Sender, + path_prefix_to_host: Vec<(PathBuf, PathBuf)>, } #[derive(Clone, Debug)] @@ -32,13 +38,53 @@ enum ForwarderCommand { impl FileTransfer { async fn add_files( &self, - _key: &str, - _fds: Vec>, - _options: HashMap<&str, zvariant::Value<'_>>, + key: &str, + fds: Vec>, + options: HashMap<&str, zvariant::Value<'_>>, ) -> Result<()> { - Err(zbus::fdo::Error::NotSupported( - "Adding files to transfer out is not yet implemented".to_owned(), - )) + let mut host_paths = Vec::with_capacity(fds.len()); + for fd in fds.iter() { + let link = rustix::fs::readlink( + format!("/proc/self/fd/{}", fd.as_fd().as_raw_fd()), + Vec::new(), + ) + .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?; + let guest_path = std::path::PathBuf::from(std::ffi::OsStr::from_bytes( + &link.to_string_lossy().as_bytes(), + )); + let (prefix, host_prefix) = self + .path_prefix_to_host + .iter() + .find(|(prefix, _)| guest_path.starts_with(prefix)) + .ok_or_else(|| { + zbus::fdo::Error::Failed("Could not find host mapping for path".to_owned()) + })?; + let guest_suffix = guest_path + .strip_prefix(prefix) + .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?; + let host_path = if guest_suffix.as_os_str().is_empty() { + // Edge case: a bind-mounted file exposed at the same path would get an extra '/' after its path + host_prefix.to_path_buf() + } else { + host_prefix.join(guest_suffix) + }; + debug!( + ?guest_path, + ?prefix, + ?guest_suffix, + ?host_prefix, + ?host_path, + "mapped path" + ); + let path_fd = rustix::fs::open( + host_path, + rustix::fs::OFlags::PATH, + rustix::fs::Mode::empty(), + ) + .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?; + host_paths.push(path_fd.into()); // OwnedFd variant of zbus's Fd enum, so still owned by the Vec + } + self.host.add_files(key, host_paths, options).await } async fn retrieve_files( @@ -56,7 +102,7 @@ impl FileTransfer { { result.push(guest_path.to_string_lossy().into_owned()); } else { - debug!("could not expose path {host_path}"); + debug!(%host_path, "could not add path as doc to retrieve file"); } } Ok(result) @@ -72,19 +118,19 @@ impl FileTransfer { .ok_or_else(|| zbus::Error::MissingField)? .to_owned(); let key = self.host.start_transfer(options).await?; - debug!("start_transfer: {key}"); + debug!(%key, %sender, "started transfer"); if let Err(err) = self .tx .send(ForwarderCommand::Add(key.clone(), sender.into())) { - error!("file_transfer internal channel error: {err:?}"); + error!(?err, "file_transfer internal channel error"); return Err(zbus::fdo::Error::IOError("channel error".to_owned())); } Ok(key) } async fn stop_transfer(&self, key: &str) -> Result<()> { - debug!("stop_transfer: {key}"); + debug!(%key, "stopping transfer"); self.host.stop_transfer(key).await } @@ -102,6 +148,7 @@ impl FileTransfer { host_session_conn: &Connection, priv_conn: &Connection, guest_root: PathBuf, + path_prefix_to_host: Vec<(PathBuf, PathBuf)>, ) -> Result { let host = FileTransferProxy::builder(host_session_conn) .build() @@ -119,6 +166,7 @@ impl FileTransfer { host, file_transformer, tx, + path_prefix_to_host, }) } @@ -137,15 +185,15 @@ impl FileTransfer { // ForwarderCommand::Remove(key) => { receivers.remove(&key); }, }, Some(signal) = stream.next() => { - debug!("transfer closed {signal:?}"); + debug!(?signal, "transfer closed"); if let Ok((key,)) = signal.0.deserialize::<(&str,)>() { if let Some(bus_name) = receivers.remove(key) { signal_emitter = signal_emitter.set_destination(zbus::names::BusName::Unique(bus_name.clone().into())); if let Err(err) = FileTransfer::transfer_closed(&signal_emitter, key).await { - error!("could not forward signal for key {key}: {err:?}"); + error!(?err, %key, "could not forward signal"); } } else { - error!("got a signal for unknown key {key}"); + error!(%key, "got a signal for unknown key"); } } else { error!("could not deserialize transfer closed signal");