Switch over to a reverse connection scheme (bridge to a guest bus)

This commit is contained in:
Val Packett 2025-12-05 05:57:22 -03:00
parent ea34b7b08c
commit 52c3ea7cd3
5 changed files with 53 additions and 82 deletions

View file

@ -14,9 +14,8 @@ A cross-domain smart D-Bus proxying system that makes (some) [XDG Desktop Portal
- `sidebus-broker` host process: - `sidebus-broker` host process:
- to be launched alongside the VMM - to be launched alongside the VMM
- hosts D-Bus servers in-process, based on [busd](https://github.com/dbus2/busd): - hosts a "private" bus for VM-instance-specific daemons such as permission-store and document-portal
- a "private" bus for VM-instance-specific daemons such as permission-store and document-portal - listens on vsock (or on a unix socket that muvm would proxy as vsock) and connects to the VM bus as a client when the agent connects
- a "VM" bus, the one actually exposed to the guest over vsock
- orchestrates the lifecycle of the aforementioned daemons + virtiofsd - orchestrates the lifecycle of the aforementioned daemons + virtiofsd
- (we are sharing the directory *provided by* the document-portal FUSE filesystem!) - (we are sharing the directory *provided by* the document-portal FUSE filesystem!)
- provides portal front-end interfaces like `org.freedesktop.portal.FileChooser` on the VM bus - provides portal front-end interfaces like `org.freedesktop.portal.FileChooser` on the VM bus
@ -24,11 +23,10 @@ A cross-domain smart D-Bus proxying system that makes (some) [XDG Desktop Portal
- (not talking directly to impls: don't want to reimplement per-DE portal selection; also 1:1 mapping is nicer to code) - (not talking directly to impls: don't want to reimplement per-DE portal selection; also 1:1 mapping is nicer to code)
- but with extra hooks like exposing files to the guest using our private (per-VM) document-portal! - but with extra hooks like exposing files to the guest using our private (per-VM) document-portal!
- `sidebus-agent` guest process: - `sidebus-agent` guest process:
- listens on a guest unix socket, proxies D-Bus messages to a vsock - connects to the broker over vsock and splices the connection into the VM (session) bus
- spawned on-demand by systemd via socket activation - can be spawned spawned on-demand by D-Bus
- uses systemd credentials for config args like vsock port - uses systemd credentials for config args like vsock port
- (very convenient to pass via the VMM, e.g. qemu: `-smbios type=11,value=io.systemd.credential:sidebus.port=1337`) - (very convenient to pass via the VMM, e.g. qemu: `-smbios type=11,value=io.systemd.credential:sidebus.port=1337`)
- guest NixOS configuration exposed via the flake
## Development Notes ## Development Notes

View file

@ -1,5 +1,3 @@
use eyre::OptionExt;
use tokio::net::UnixListener;
use tracing::info; use tracing::info;
#[tokio::main] #[tokio::main]
@ -10,21 +8,15 @@ async fn main() -> eyre::Result<()> {
let vsock_port = std::fs::read_to_string(creds_dir.join("sidebus.port"))? let vsock_port = std::fs::read_to_string(creds_dir.join("sidebus.port"))?
.trim() .trim()
.parse::<u32>()?; .parse::<u32>()?;
let unix_listener = UnixListener::from_std(
listenfd::ListenFd::from_env()
.take_unix_listener(0)?
.ok_or_eyre("no unix listener provided")?,
)?;
info!("listening for unix clients");
while let Ok((unix_client, client_addr)) = unix_listener.accept().await {
info!(?client_addr, "new unix client");
tokio::spawn(async move {
let vsock_addr = zbus::Address::from(zbus::address::Transport::Vsock( let vsock_addr = zbus::Address::from(zbus::address::Transport::Vsock(
zbus::address::transport::Vsock::new(2, vsock_port), zbus::address::transport::Vsock::new(2, vsock_port),
)); ));
let vsock_conn = zbus::connection::Builder::address(vsock_addr)
info!("connecting to session bus");
let session_conn = zbus::connection::Builder::session()?.p2p().build().await?;
let vsock_conn = zbus::connection::Builder::address(vsock_addr)?
.server(session_conn.server_guid())
.unwrap() .unwrap()
.p2p() .p2p()
.auth_mechanism(zbus::AuthMechanism::Anonymous) .auth_mechanism(zbus::AuthMechanism::Anonymous)
@ -32,16 +24,7 @@ async fn main() -> eyre::Result<()> {
.await .await
.unwrap(); .unwrap();
info!(guid = %vsock_conn.server_guid(), "connected to vsock bus"); info!(guid = %vsock_conn.server_guid(), "connected to vsock bus");
let client_conn = zbus::connection::Builder::unix_stream(unix_client) sidebus_common::raw::splice_conns(vsock_conn, session_conn).await;
.server(vsock_conn.server_guid())
.unwrap()
.p2p()
.auth_mechanism(zbus::AuthMechanism::External)
.build()
.await
.unwrap();
sidebus_common::raw::splice_conns(client_conn, vsock_conn).await;
});
}
Ok(()) Ok(())
} }

View file

@ -5,7 +5,7 @@ mod vsock;
use bus::SharedHostedBus; use bus::SharedHostedBus;
use clap::Parser; use clap::Parser;
use futures::{TryFutureExt, stream::FuturesUnordered}; use futures::{TryFutureExt, stream::FuturesUnordered};
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc, time::Duration};
use tokio::{net::UnixListener, process::Command, sync::Mutex}; use tokio::{net::UnixListener, process::Command, sync::Mutex};
use tokio_stream::StreamExt as _; use tokio_stream::StreamExt as _;
use tracing::{Instrument, debug, error, info_span}; use tracing::{Instrument, debug, error, info_span};
@ -71,7 +71,6 @@ async fn main() -> eyre::Result<()> {
let cli = BrokerCli::parse(); let cli = BrokerCli::parse();
let (vm_bus, vm_bus_guid, _) = new_hosted_bus().await?;
let (priv_bus, _, mut priv_lst) = new_hosted_bus().await?; let (priv_bus, _, mut priv_lst) = new_hosted_bus().await?;
let mut server_tasks = tokio::task::JoinSet::new(); let mut server_tasks = tokio::task::JoinSet::new();
@ -88,12 +87,6 @@ async fn main() -> eyre::Result<()> {
.clone() .clone()
.run_unix_listener(priv_dbg_listener, zbus::AuthMechanism::External), .run_unix_listener(priv_dbg_listener, zbus::AuthMechanism::External),
); );
let vm_dbg_listener = UnixListener::bind(dir_path.join("vm.sock"))?;
server_tasks.spawn(
vm_bus
.clone()
.run_unix_listener(vm_dbg_listener, zbus::AuthMechanism::External),
);
// TODO: unlink sockets on exit // TODO: unlink sockets on exit
} }
@ -148,7 +141,6 @@ async fn main() -> eyre::Result<()> {
.spawn()?, .spawn()?,
); );
let vm_bus_conn = vm_bus.lock().await.connect_channel(false).await?;
let priv_bus_conn = priv_bus.lock().await.connect_channel(false).await?; let priv_bus_conn = priv_bus.lock().await.connect_channel(false).await?;
let host_session_conn = zbus::connection::Builder::session()?.build().await?; let host_session_conn = zbus::connection::Builder::session()?.build().await?;
let file_chooser_imp = portal::file_chooser::FileChooser::new( let file_chooser_imp = portal::file_chooser::FileChooser::new(
@ -157,47 +149,49 @@ async fn main() -> eyre::Result<()> {
cli.guest_mountpoint, cli.guest_mountpoint,
) )
.await?; .await?;
async fn on_vm_bus_connected(
vm_bus_conn: zbus::Connection,
file_chooser: portal::file_chooser::FileChooser,
) -> Result<(), eyre::Report> {
vm_bus_conn vm_bus_conn
.request_name("org.freedesktop.portal.Desktop") .request_name("org.freedesktop.portal.Desktop")
.await?; .await?;
let true = vm_bus_conn if !vm_bus_conn
.object_server() .object_server()
.at("/org/freedesktop/portal/desktop", file_chooser_imp) .at("/org/freedesktop/portal/desktop", file_chooser)
.await? .await?
else { {
unreachable!("our own fresh bus can't have interfaces already provided"); error!("org.freedesktop.portal.Desktop already provided");
}; };
// XXX: no method for "wait until the conn dies"?
// NOTE: Every individual D-Bus client inside of the VM is a new client on the VM bus listeners! loop {
tokio::time::sleep(Duration::from_millis(5000)).await;
}
}
if let Some(path) = cli.unix_path { if let Some(path) = cli.unix_path {
// XXX: going through the channel just to strip fds
let vm_unix_listener = UnixListener::bind(path)?; let vm_unix_listener = UnixListener::bind(path)?;
server_tasks.spawn(enclose! { (vm_bus, vm_bus_guid) async move { server_tasks.spawn(enclose!((file_chooser_imp) async move {
while let Ok((socket, remote_addr)) = vm_unix_listener.accept().await { while let Ok((socket, remote_addr)) = vm_unix_listener.accept().await {
let f = enclose! { (vm_bus, vm_bus_guid) async move { let f = enclose!((file_chooser_imp) async move {
let client_conn = zbus::connection::Builder::unix_stream(socket) let client_conn = zbus::connection::Builder::unix_stream(socket)
.server(&vm_bus_guid)
.unwrap()
.p2p()
.auth_mechanism(zbus::AuthMechanism::Anonymous) .auth_mechanism(zbus::AuthMechanism::Anonymous)
.build() .build()
.await?; .await?;
let vmbus_conn = vm_bus.lock().await.connect_channel(true).await?; on_vm_bus_connected(client_conn, file_chooser_imp).await
sidebus_common::raw::splice_conns(client_conn, vmbus_conn).await; });
Ok::<(), eyre::Report>(())
} };
tokio::spawn( tokio::spawn(
async { async {
match f.await { match f.await {
Ok(()) => debug!("done with client"), Ok(()) => debug!("done with server"),
Err(err) => error!(%err, "error dealing with client"), Err(err) => error!(%err, "error dealing with server"),
} }
} }
.instrument(info_span!("serve", ?remote_addr)), .instrument(info_span!("serve", ?remote_addr)),
); );
} }
} }); }));
} }
if let Some(port) = cli.vsock_port { if let Some(port) = cli.vsock_port {
@ -206,13 +200,11 @@ async fn main() -> eyre::Result<()> {
vsock::ListenerBuilder::new(vsock::VsockAddr::new(vsock::VMADDR_CID_HOST, port)) vsock::ListenerBuilder::new(vsock::VsockAddr::new(vsock::VMADDR_CID_HOST, port))
.with_label("VM Bus") .with_label("VM Bus")
.listen(move |client| { .listen(move |client| {
enclose! { (vm_bus, vm_bus_guid) async move { enclose!((file_chooser_imp) async move {
// TODO: Not necessary to go through the channel, add vsock support to the Peer too? // TODO: Not necessary to go through the channel, add vsock support to the Peer too?
let client_conn = client.build((&vm_bus_guid).into()).await?; let client_conn = client.build().await?;
let vmbus_conn = vm_bus.lock().await.connect_channel(true).await?; on_vm_bus_connected(client_conn, file_chooser_imp).await
sidebus_common::raw::splice_conns(client_conn, vmbus_conn).await; })
Ok(())
} }
}) })
.map_ok_or_else( .map_ok_or_else(
|e| { |e| {

View file

@ -8,6 +8,7 @@ use zbus::{Connection, ObjectServer, fdo::Result, zvariant};
use super::documents::DocumentsProxy; use super::documents::DocumentsProxy;
use super::request::{RESPONSE_SUCCESS, ReqHandler, ResultTransformer}; use super::request::{RESPONSE_SUCCESS, ReqHandler, ResultTransformer};
#[derive(Clone)]
pub struct FileChooser { pub struct FileChooser {
host: FileChooserProxy<'static>, host: FileChooserProxy<'static>,
docs: DocumentsProxy<'static>, docs: DocumentsProxy<'static>,

View file

@ -13,11 +13,8 @@ impl ConnectionBuilder {
&self.remote_addr &self.remote_addr
} }
pub async fn build<'a>(self, guid: zbus::Guid<'a>) -> eyre::Result<zbus::Connection> { pub async fn build<'a>(self) -> eyre::Result<zbus::Connection> {
zbus::connection::Builder::vsock_stream(self.socket) zbus::connection::Builder::vsock_stream(self.socket)
.server(guid)
.unwrap()
.p2p()
.auth_mechanism(zbus::AuthMechanism::Anonymous) .auth_mechanism(zbus::AuthMechanism::Anonymous)
.build() .build()
.await .await