Compare commits

...

21 commits

Author SHA1 Message Date
Val Packett
c42eaef554 Support EXTERNAL auth to the client bus with given UID
With the virtgpu channel, the proxy is part of muvm-guest, so it does
not do the protocol-aware splicing we do here, so it can't do different
auth between the sides like sidebus-agent does. But turns out EXTERNAL
auth works fine, as long as we correct for the UID difference.
2026-02-27 06:05:03 -03:00
Val Packett
eedf1f889d Update zbus to 5.0 2026-02-27 06:05:03 -03:00
Val Packett
52a0ccee0d Add Print support 2026-02-27 05:29:31 -03:00
Val Packett
95bc64076d Add file path mapping for FileTransfer
Match by longest prefix to get host paths from guest ones
2026-02-27 05:27:24 -03:00
Val Packett
2626130659 Add Notification support 2026-02-06 05:05:07 -03:00
Val Packett
2561342e0c Add FileTransfer (drag & drop / copy & paste files)
Host->Guest is easy; Guest->Host is not there yet because we have to do
something for the (O_PATH) fd passing such as converting to inode numbers
and getting an O_PATH fd back through the VMM, which requires modifying
libkrun/muvm to have a connection channel.. Or we should switch this from
vsock to a virtgpu channel where it would be easier to handle.
2026-02-06 04:14:01 -03:00
Jörg Thalheim
762dd2dd84 move nixos module to external file
Avoids anonymous imports which makes the module easier to
find and reference.
2026-01-16 15:34:17 +01:00
Jörg Thalheim
d145ee2b5c drop rust-overlay, use rustc from nixpkgs
nixpkgs has a recent enough rustc version, no need for the
extra complexity of rust-overlay and rust-toolchain.toml.
2026-01-16 15:31:56 +01:00
Jörg Thalheim
1315858592 switch from flake-utils to flake-parts
There is not so much of a difference in this project, but
it means adding this project to clan is one less flake input
and also more consistent with other projects in clan.
2026-01-16 15:31:44 +01:00
Val Packett
c9095421c6 Request the D-Bus name earlier 2025-12-12 05:24:35 -03:00
Val Packett
2456dc0ea5 Fix interface names in error messages 2025-12-12 04:55:10 -03:00
Val Packett
df832a5141 Add proxy for the Settings portal (dark mode, reduced motion, etc.) 2025-12-12 04:45:19 -03:00
Val Packett
1d931ae191 Use pending instead of timer loop 2025-12-12 03:42:35 -03:00
Val Packett
52c3ea7cd3 Switch over to a reverse connection scheme (bridge to a guest bus) 2025-12-05 08:18:44 -03:00
Val Packett
ea34b7b08c Unhardcode user IDs 2025-12-05 04:58:11 -03:00
Val Packett
fa0bf056d0 Do not handle the unix socket server in-place
Let the code after that actually run..
2025-11-14 06:12:03 -03:00
Val Packett
52ad012e13 Implement process cleanup for the broker 2025-11-14 05:57:40 -03:00
Val Packett
73c92e3e69 Update deps 2025-11-14 03:40:48 -03:00
Val Packett
4fd76692db Add option to expose a (non-fd-capable) unix socket instead of vsock
libkrun does not use vsock on the host, so we need to provide this to
work with muvm.
2025-10-03 05:17:36 -03:00
Val Packett
250c459866 Update cargo hashes 2025-10-03 05:17:36 -03:00
Val Packett
50ea609880 Add demo video 2025-08-08 06:18:21 -03:00
18 changed files with 902 additions and 301 deletions

94
Cargo.lock generated
View file

@ -142,8 +142,8 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "busd"
version = "0.4.0"
source = "git+https://github.com/valpackett/busd?branch=val%2Fmsksqvsqqrxm#25736c855284b13371f0be2ef38f16af8f73bda1"
version = "0.5.0"
source = "git+https://github.com/valpackett/busd?branch=val%2Fmsksqvsqqrxm#7084025107e02600043856c56fd178730526daad"
dependencies = [
"anyhow",
"clap",
@ -151,8 +151,8 @@ dependencies = [
"event-listener",
"fastrand",
"futures-util",
"nix 0.30.1",
"quick-xml",
"rustix",
"serde",
"tokio",
"tracing",
@ -635,15 +635,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.174"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "listenfd"
@ -726,19 +726,6 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -871,9 +858,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.38.0"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b"
checksum = "f2e3bf4aa9d243beeb01a7b3bc30b77cfe2c44e24ec02d751a7104a53c2c49a1"
dependencies = [
"memchr",
"serde",
@ -940,15 +927,15 @@ checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
[[package]]
name = "rustix"
version = "1.0.8"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@ -1026,6 +1013,7 @@ dependencies = [
"clap",
"eyre",
"futures",
"libc",
"rand",
"rustix",
"sidebus-common",
@ -1321,7 +1309,9 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"getrandom",
"js-sys",
"serde",
"wasm-bindgen",
]
@ -1338,7 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8b4d00e672f147fc86a09738fadb1445bd1c0a40542378dfb82909deeee688"
dependencies = [
"libc",
"nix 0.29.0",
"nix",
]
[[package]]
@ -1436,6 +1426,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.52.0"
@ -1463,6 +1459,15 @@ dependencies = [
"windows-targets 0.53.2",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@ -1651,8 +1656,9 @@ dependencies = [
[[package]]
name = "zbus"
version = "5.9.0"
source = "git+https://github.com/dbus2/zbus#6da6b1b5f528fe2d14b0f25ae1dca1a9fd31575c"
version = "5.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc"
dependencies = [
"async-broadcast",
"async-recursion",
@ -1662,16 +1668,17 @@ dependencies = [
"futures-core",
"futures-lite",
"hex",
"nix 0.30.1",
"libc",
"ordered-stream",
"rand",
"rustix",
"serde",
"serde_repr",
"tokio",
"tokio-vsock",
"tracing",
"uds_windows",
"windows-sys 0.60.2",
"uuid",
"windows-sys 0.61.2",
"winnow",
"zbus_macros",
"zbus_names",
@ -1680,8 +1687,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
version = "5.9.0"
source = "git+https://github.com/dbus2/zbus#6da6b1b5f528fe2d14b0f25ae1dca1a9fd31575c"
version = "5.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@ -1694,8 +1702,9 @@ dependencies = [
[[package]]
name = "zbus_names"
version = "4.2.0"
source = "git+https://github.com/dbus2/zbus#6da6b1b5f528fe2d14b0f25ae1dca1a9fd31575c"
version = "4.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
dependencies = [
"serde",
"winnow",
@ -1778,8 +1787,9 @@ dependencies = [
[[package]]
name = "zvariant"
version = "5.6.0"
source = "git+https://github.com/dbus2/zbus#6da6b1b5f528fe2d14b0f25ae1dca1a9fd31575c"
version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b"
dependencies = [
"endi",
"enumflags2",
@ -1791,8 +1801,9 @@ dependencies = [
[[package]]
name = "zvariant_derive"
version = "5.6.0"
source = "git+https://github.com/dbus2/zbus#6da6b1b5f528fe2d14b0f25ae1dca1a9fd31575c"
version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@ -1803,8 +1814,9 @@ dependencies = [
[[package]]
name = "zvariant_utils"
version = "3.2.0"
source = "git+https://github.com/dbus2/zbus#6da6b1b5f528fe2d14b0f25ae1dca1a9fd31575c"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"
dependencies = [
"proc-macro2",
"quote",

View file

@ -9,6 +9,4 @@ members = [
[workspace.dependencies]
sidebus-common = { path = "sidebus-common" }
busd = { git = "https://github.com/valpackett/busd", branch = "val/msksqvsqqrxm", default-features = false }
zbus = { git = "https://github.com/dbus2/zbus", default-features = false, features = ["tokio", "tokio-vsock", "bus-impl", "p2p"] }
# zbus git to match busd git
zbus = { version = "5.0", default-features = false, features = ["tokio", "tokio-vsock", "bus-impl", "p2p"] }

View file

@ -2,6 +2,10 @@
(it.. runs as a "sidecar" to a VM.. but manages D-*Bus*.. get it?)
<video controls muted src="https://git.clan.lol/valpackett/sidebus/releases/download/__assets__/vm-filechooser-portal-2.webm">
<p><a href="https://git.clan.lol/valpackett/sidebus/releases/download/__assets__/vm-filechooser-portal-2.webm">Watch demo →</a></p>
</video>
A cross-domain smart D-Bus proxying system that makes (some) [XDG Desktop Portals] work across virtual machines.
[XDG Desktop Portals]: https://flatpak.github.io/xdg-desktop-portal/docs/index.html
@ -10,9 +14,8 @@ A cross-domain smart D-Bus proxying system that makes (some) [XDG Desktop Portal
- `sidebus-broker` host process:
- to be launched alongside the VMM
- hosts D-Bus servers in-process, based on [busd](https://github.com/dbus2/busd):
- a "private" bus for VM-instance-specific daemons such as permission-store and document-portal
- a "VM" bus, the one actually exposed to the guest over vsock
- hosts 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
- orchestrates the lifecycle of the aforementioned daemons + virtiofsd
- (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
@ -20,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)
- but with extra hooks like exposing files to the guest using our private (per-VM) document-portal!
- `sidebus-agent` guest process:
- listens on a guest unix socket, proxies D-Bus messages to a vsock
- spawned on-demand by systemd via socket activation
- connects to the broker over vsock and splices the connection into the VM (session) bus
- can be spawned spawned on-demand by D-Bus
- 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`)
- guest NixOS configuration exposed via the flake
## Development Notes

66
flake.lock generated
View file

@ -1,30 +1,32 @@
{
"nodes": {
"flake-utils": {
"flake-parts": {
"inputs": {
"systems": "systems"
"nixpkgs-lib": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"lastModified": 1768135262,
"narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1751271578,
"narHash": "sha256-P/SQmKDu06x8yv7i0s8bvnnuJYkxVGBWLWHaU+tt4YY=",
"lastModified": 1762977756,
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3016b4b15d13f3089db8a41ef937b13a9e33a8df",
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
"type": "github"
},
"original": {
@ -36,44 +38,8 @@
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1751510438,
"narHash": "sha256-m8PjOoyyCR4nhqtHEBP1tB/jF+gJYYguSZmUmVTEAQE=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "7f415261f298656f8164bd636c0dc05af4e95b6b",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
}
}
},

114
flake.nix
View file

@ -1,80 +1,60 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-parts.url = "github:hercules-ci/flake-parts";
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
};
outputs = {self, nixpkgs, flake-utils, rust-overlay}:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
outputs =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [
"x86_64-linux"
"aarch64-linux"
];
buildEnvVars = {
BIN_XDG_PERMISSION_STORE = "${pkgs.xdg-desktop-portal}/libexec/xdg-permission-store";
BIN_XDG_DOCUMENT_PORTAL = "${pkgs.xdg-desktop-portal}/libexec/xdg-document-portal";
BIN_VIRTIOFSD = "${pkgs.virtiofsd}/bin/virtiofsd";
};
perSystem =
{ pkgs, ... }:
let
buildEnvVars = {
BIN_XDG_PERMISSION_STORE = "${pkgs.xdg-desktop-portal}/libexec/xdg-permission-store";
BIN_XDG_DOCUMENT_PORTAL = "${pkgs.xdg-desktop-portal}/libexec/xdg-document-portal";
BIN_VIRTIOFSD = "${pkgs.virtiofsd}/bin/virtiofsd";
};
rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
rustPlatform = pkgs.makeRustPlatform {
cargo = rustToolchain;
rustc = rustToolchain;
};
rustPackage = crate:
let cargoToml = builtins.fromTOML (builtins.readFile ./${crate}/Cargo.toml);
in rustPlatform.buildRustPackage {
inherit (cargoToml.package) name version;
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
cargoLock.outputHashes = {
"zbus-5.9.0" = "sha256-3xaKbf+JmO5yVwPbvA3z9dHvqICh7yCeKk1SIX8zhJA=";
"busd-0.4.0" = "sha256-UzTclEJ8lRMmiuLJJi+gsm7vkx+MLfnDdi5s9OVT1HE=";
rustPackage =
crate:
let
cargoToml = builtins.fromTOML (builtins.readFile ./${crate}/Cargo.toml);
in
pkgs.rustPlatform.buildRustPackage {
inherit (cargoToml.package) name version;
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
cargoLock.outputHashes = {
"busd-0.5.0" = "sha256-IZZ2MeEmUbzRrH6SUz0pnecMH4f8Mh54WdhI4q44YfI=";
};
buildAndTestSubdir = crate;
env = buildEnvVars;
};
buildAndTestSubdir = crate;
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
cargo
rustc
rust-analyzer
clippy
];
env = buildEnvVars;
};
in
{
devShells.default = pkgs.mkShell {
buildInputs = [ rustToolchain ];
env = buildEnvVars;
packages.sidebus-agent = rustPackage "sidebus-agent";
packages.sidebus-broker = rustPackage "sidebus-broker";
};
packages.sidebus-agent = rustPackage "sidebus-agent";
packages.sidebus-broker = rustPackage "sidebus-broker";
nixosModules.sidebus-vm = { ... }: {
environment.sessionVariables.DBUS_SESSION_BUS_ADDRESS = "unix:path=/run/sidebus.sock";
systemd.sockets.sidebus-agent = {
# SocketMode= is 0666 by default
listenStreams = [ "/run/sidebus.sock" ];
wantedBy = [ "sockets.target" ];
documentation = [ "https://git.clan.lol/valpackett/sidebus" ];
};
systemd.services.sidebus-agent = {
# TODO: confinement (can do a lot)
serviceConfig = {
ExecStart = "${rustPackage "sidebus-agent"}/bin/sidebus-agent";
ImportCredential = "sidebus.*";
};
documentation = [ "https://git.clan.lol/valpackett/sidebus" ];
};
systemd.mounts = [
{
type = "virtiofs";
what = "vm-doc-portal";
where = "/run/vm-doc-portal";
wantedBy = [ "multi-user.target" ];
}
];
};
}
);
flake = {
nixosModules.sidebus-vm = ./nixosModules/sidebus-vm.nix;
};
};
}

View file

@ -0,0 +1,25 @@
{
environment.sessionVariables.DBUS_SESSION_BUS_ADDRESS = "unix:path=/run/sidebus.sock";
systemd.sockets.sidebus-agent = {
# SocketMode= is 0666 by default
listenStreams = [ "/run/sidebus.sock" ];
wantedBy = [ "sockets.target" ];
documentation = [ "https://git.clan.lol/valpackett/sidebus" ];
};
systemd.services.sidebus-agent = {
# TODO: confinement (can do a lot)
serviceConfig = {
ExecStart = throw "sidebus-vm module requires setting systemd.services.sidebus-agent.serviceConfig.ExecStart to a sidebus-agent package";
ImportCredential = "sidebus.*";
};
documentation = [ "https://git.clan.lol/valpackett/sidebus" ];
};
systemd.mounts = [
{
type = "virtiofs";
what = "vm-doc-portal";
where = "/run/vm-doc-portal";
wantedBy = [ "multi-user.target" ];
}
];
}

View file

@ -1,3 +0,0 @@
[toolchain]
channel = "1.88.0"
components = ["rust-analyzer", "rust-src", "clippy"]

View file

@ -1,5 +1,3 @@
use eyre::OptionExt;
use tokio::net::UnixListener;
use tracing::info;
#[tokio::main]
@ -10,38 +8,23 @@ async fn main() -> eyre::Result<()> {
let vsock_port = std::fs::read_to_string(creds_dir.join("sidebus.port"))?
.trim()
.parse::<u32>()?;
let vsock_addr = zbus::Address::from(zbus::address::Transport::Vsock(
zbus::address::transport::Vsock::new(2, vsock_port),
));
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");
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()
.p2p()
.auth_mechanism(zbus::AuthMechanism::Anonymous)
.build()
.await
.unwrap();
info!(guid = %vsock_conn.server_guid(), "connected to vsock bus");
sidebus_common::raw::splice_conns(vsock_conn, session_conn).await;
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(
zbus::address::transport::Vsock::new(2, vsock_port),
));
let vsock_conn = zbus::connection::Builder::address(vsock_addr)
.unwrap()
.p2p()
.auth_mechanism(zbus::AuthMechanism::Anonymous)
.build()
.await
.unwrap();
info!(guid = %vsock_conn.server_guid(), "connected to vsock bus");
let client_conn = zbus::connection::Builder::unix_stream(unix_client)
.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(())
}

View file

@ -18,3 +18,4 @@ rustix = { version = "1.0.8", features = ["fs"] }
url = "2.5.4"
rand = "0.9.2"
futures = "0.3.31"
libc = "0.2.178"

View file

@ -1,6 +1,6 @@
use std::sync::Arc;
use std::{hash::DefaultHasher, sync::Arc};
use tokio_stream::StreamExt as _;
use tracing::{debug, trace};
use tracing::{debug, error, trace};
pub struct HostedBus {
peers: Arc<busd::peers::Peers>,
@ -57,10 +57,14 @@ impl HostedBus {
.map_err(|err| eyre::eyre!(Box::new(err))) // https://github.com/eyre-rs/eyre/issues/31 XXX: busd should not use anyhow!
}
pub async fn connect_unix(&mut self, socket: tokio::net::UnixStream) -> eyre::Result<()> {
pub async fn connect_unix(
&mut self,
socket: tokio::net::UnixStream,
auth: zbus::AuthMechanism,
) -> eyre::Result<()> {
let id = self.next_id();
self.peers
.add(&self.guid, id, socket.into(), zbus::AuthMechanism::External)
.add(&self.guid, id, socket.into(), auth)
.await
.map_err(|err| eyre::eyre!(Box::new(err)))
}
@ -77,7 +81,7 @@ impl HostedBus {
}
pub trait SharedHostedBus {
async fn run_unix_listener(self, listener: tokio::net::UnixListener);
async fn run_unix_listener(self, listener: tokio::net::UnixListener, auth: zbus::AuthMechanism);
async fn spawn_external_client(
self,
command: &mut tokio::process::Command,
@ -85,9 +89,15 @@ pub trait SharedHostedBus {
}
impl SharedHostedBus for Arc<tokio::sync::Mutex<HostedBus>> {
async fn run_unix_listener(self, listener: tokio::net::UnixListener) {
async fn run_unix_listener(
self,
listener: tokio::net::UnixListener,
auth: zbus::AuthMechanism,
) {
while let Ok((socket, _remote_addr)) = listener.accept().await {
self.lock().await.connect_unix(socket).await.unwrap()
if let Err(e) = self.lock().await.connect_unix(socket, auth).await {
error!("unix connection: {:?}", e);
}
}
}
@ -100,7 +110,7 @@ impl SharedHostedBus for Arc<tokio::sync::Mutex<HostedBus>> {
let abstract_path = format!("/run/sidebus-broker/{}", zbus::Guid::generate());
let listener = tokio::net::UnixListener::bind(format!("\0{abstract_path}"))?;
debug!(%abstract_path, "opened listener for external client");
tokio::spawn(self.run_unix_listener(listener));
tokio::spawn(self.run_unix_listener(listener, zbus::AuthMechanism::External));
Ok(command
.env(
"DBUS_SESSION_BUS_ADDRESS",

View file

@ -4,9 +4,12 @@ mod vsock;
use bus::SharedHostedBus;
use clap::Parser;
use std::{path::PathBuf, sync::Arc};
use eyre::OptionExt;
use futures::{TryFutureExt, stream::FuturesUnordered};
use std::{path::PathBuf, sync::Arc, time::Duration};
use tokio::{net::UnixListener, process::Command, sync::Mutex};
use tracing::error;
use tokio_stream::StreamExt as _;
use tracing::{Instrument, debug, error, info_span};
use zbus::names::WellKnownName;
// https://github.com/rust-lang/rfcs/issues/2407#issuecomment-385291238
@ -22,7 +25,7 @@ macro_rules! enclose {
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct BrokerCli {
/// Create unix socket listeners for all internal busses in the provided directory
/// Create unix socket listeners for internal busses in the provided directory
#[clap(long)]
debug_access: Option<PathBuf>,
@ -34,9 +37,29 @@ struct BrokerCli {
#[clap(long, default_value = "/run/vm-doc-portal")]
guest_mountpoint: PathBuf,
/// Vsock port number to listen on
/// Mappings from guest paths to host paths for passthrough file systems (for file transfer), in guest=host format
#[clap(long)]
vsock_port: u32,
path_mapping: Vec<String>,
/// Vsock port number to listen on for the VM bus
#[clap(long)]
vsock_port: Option<u32>,
/// Unix socket path to listen on for the VM bus
#[clap(long)]
unix_path: Option<PathBuf>,
/// Use ANONYMOUS auth to connect to the guest bus instead of EXTERNAL with the provided --guest-uid
#[clap(long)]
guest_bus_anonymous_auth: bool,
/// The user ID for the appvm user inside of the guest
#[clap(long, default_value = "1337")]
guest_uid: u32,
/// The group ID for the appvm group inside of the guest
#[clap(long, default_value = "1337")]
guest_gid: u32,
}
async fn new_hosted_bus() -> eyre::Result<(
@ -51,110 +74,272 @@ 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 (vm_bus, vm_bus_guid, _) = new_hosted_bus().await?;
let (priv_bus, _, mut priv_lst) = new_hosted_bus().await?;
let mut server_tasks = tokio::task::JoinSet::new();
let mut child_procs = Vec::new();
if let Some(dir_path) = cli.debug_access {
if !dir_path.is_dir() {
error!(path = %dir_path.display(), "--debug-access path is not an existing directory");
std::process::exit(1);
}
let vm_dbg_listener = UnixListener::bind(dir_path.join("vm.sock"))?;
let _vm_dbg_task = tokio::spawn(vm_bus.clone().run_unix_listener(vm_dbg_listener));
let priv_dbg_listener = UnixListener::bind(dir_path.join("priv.sock"))?;
let _priv_dbg_task = tokio::spawn(priv_bus.clone().run_unix_listener(priv_dbg_listener));
server_tasks.spawn(
priv_bus
.clone()
.run_unix_listener(priv_dbg_listener, zbus::AuthMechanism::External),
);
// TODO: unlink sockets on exit
}
std::fs::create_dir_all(&cli.runtime_dir)?;
let _xps = priv_bus
.clone()
.spawn_external_client(
Command::new(env!("BIN_XDG_PERMISSION_STORE"))
.env("XDG_RUNTIME_DIR", cli.runtime_dir.as_os_str())
.kill_on_drop(true),
)
.await?;
child_procs.push(
priv_bus
.clone()
.spawn_external_client(
Command::new(env!("BIN_XDG_PERMISSION_STORE"))
.env("XDG_RUNTIME_DIR", cli.runtime_dir.as_os_str())
.kill_on_drop(true),
)
.await?,
);
let impl_permission_store =
WellKnownName::from_static_str("org.freedesktop.impl.portal.PermissionStore")?.into();
priv_lst.wait_for_acquisition(impl_permission_store).await?;
let _xdp = priv_bus
.clone()
.spawn_external_client(
Command::new(env!("BIN_XDG_DOCUMENT_PORTAL"))
.env("XDG_RUNTIME_DIR", cli.runtime_dir.as_os_str())
.kill_on_drop(true),
)
.await?;
child_procs.push(
priv_bus
.clone()
.spawn_external_client(
Command::new(env!("BIN_XDG_DOCUMENT_PORTAL"))
.env("XDG_RUNTIME_DIR", cli.runtime_dir.as_os_str())
.kill_on_drop(true),
)
.await?,
);
let portal_documents =
WellKnownName::from_static_str("org.freedesktop.portal.Documents")?.into();
priv_lst.wait_for_acquisition(portal_documents).await?;
let _vfs = Command::new(env!("BIN_VIRTIOFSD"))
.args(&[
"--shared-dir",
cli.runtime_dir.join("doc").to_str().unwrap(),
"--socket-path",
cli.runtime_dir.join("fs.sock").to_str().unwrap(),
"--uid-map",
":1000:1001:1:",
"--gid-map",
":100:100:1:",
"--log-level",
"debug",
])
.env("XDG_RUNTIME_DIR", cli.runtime_dir.as_os_str())
.kill_on_drop(true)
.spawn();
// TODO: die when it exits
child_procs.push(
Command::new(env!("BIN_VIRTIOFSD"))
.args(&[
"--shared-dir",
cli.runtime_dir.join("doc").to_str().unwrap(),
"--socket-path",
cli.runtime_dir.join("fs.sock").to_str().unwrap(),
"--uid-map",
&format!(":{}:{}:1:", cli.guest_uid, unsafe { libc::getuid() }),
"--gid-map",
&format!(":{}:{}:1:", cli.guest_gid, unsafe { libc::getgid() }),
"--log-level",
"debug",
])
.env("XDG_RUNTIME_DIR", cli.runtime_dir.as_os_str())
.kill_on_drop(true)
.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 host_session_conn = zbus::connection::Builder::session()?.build().await?;
let file_chooser_imp = portal::file_chooser::FileChooser::new(
&host_session_conn,
&priv_bus_conn,
cli.guest_mountpoint,
cli.guest_mountpoint.clone(),
)
.await?;
vm_bus_conn
.request_name("org.freedesktop.portal.Desktop")
.await?;
let true = vm_bus_conn
.object_server()
.at("/org/freedesktop/portal/desktop", file_chooser_imp)
.await?
else {
unreachable!("our own fresh bus can't have interfaces already provided");
};
// TODO: modprobe vhost_vsock first!
// NOTE: Every individual D-Bus client inside of the VM is a new client here!
vsock::ListenerBuilder::new(vsock::VsockAddr::new(
vsock::VMADDR_CID_HOST,
cli.vsock_port,
))
.with_label("VM Bus")
.listen(move |client| {
enclose! { (vm_bus, vm_bus_guid) async move {
// 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 vmbus_conn = vm_bus.lock().await.connect_channel(true).await?;
sidebus_common::raw::splice_conns(client_conn, vmbus_conn).await;
Ok(())
} }
})
let file_transfer_imp = portal::file_transfer::FileTransfer::new(
&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?;
let print_imp = portal::print::Print::new(&host_session_conn).await?;
let settings_imp = portal::settings::Settings::new(&host_session_conn).await?;
async fn on_vm_bus_connected(
vm_bus_conn: zbus::Connection,
file_chooser: portal::file_chooser::FileChooser,
file_transfer: portal::file_transfer::FileTransfer,
notification: portal::notification::Notification,
print: portal::print::Print,
settings: portal::settings::Settings,
) -> Result<(), eyre::Report> {
if !vm_bus_conn
.object_server()
.at("/org/freedesktop/portal/desktop", file_chooser)
.await?
{
error!("org.freedesktop.portal.FileChooser already provided");
};
if !vm_bus_conn
.object_server()
.at("/org/freedesktop/portal/documents", file_transfer)
.await?
{
error!("org.freedesktop.portal.FileTransfer already provided");
};
let file_transfer_ref = vm_bus_conn
.object_server()
.interface::<_, portal::file_transfer::FileTransfer>(
"/org/freedesktop/portal/documents",
)
.await?;
tokio::spawn(async move {
let file_transfer = file_transfer_ref.get().await;
let emitter = file_transfer_ref.signal_emitter();
if let Err(err) = file_transfer.forward_transfer_closed(emitter.clone()).await {
error!(%err, "forwarding forward_transfer_closed changes ended");
}
});
if !vm_bus_conn
.object_server()
.at("/org/freedesktop/portal/desktop", notification)
.await?
{
error!("org.freedesktop.portal.Notification already provided");
};
let notification_ref = vm_bus_conn
.object_server()
.interface::<_, portal::notification::Notification>("/org/freedesktop/portal/desktop")
.await?;
tokio::spawn(async move {
let notification = notification_ref.get().await;
let emitter = notification_ref.signal_emitter();
if let Err(err) = notification.forward_actions(emitter.clone()).await {
error!(%err, "forwarding notification changes ended");
}
});
if !vm_bus_conn
.object_server()
.at("/org/freedesktop/portal/desktop", print)
.await?
{
error!("org.freedesktop.portal.Print already provided");
};
if !vm_bus_conn
.object_server()
.at("/org/freedesktop/portal/desktop", settings)
.await?
{
error!("org.freedesktop.portal.Settings already provided");
};
let settings_ref = vm_bus_conn
.object_server()
.interface::<_, portal::settings::Settings>("/org/freedesktop/portal/desktop")
.await?;
tokio::spawn(async move {
let settings = settings_ref.get().await;
let emitter = settings_ref.signal_emitter();
if let Err(err) = settings.forward_changes(emitter).await {
error!(%err, "forwarding settings changes ended");
}
});
// XXX: no method for "wait until the conn dies"?
Ok(std::future::pending::<()>().await)
}
if let Some(path) = cli.unix_path {
let vm_unix_listener = UnixListener::bind(path)?;
server_tasks.spawn(enclose!((file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp) async move {
while let Ok((socket, remote_addr)) = vm_unix_listener.accept().await {
let f = enclose!((file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp) async move {
let client_conn = if cli.guest_bus_anonymous_auth {
zbus::connection::Builder::unix_stream(socket).auth_mechanism(zbus::AuthMechanism::Anonymous)
} else {
zbus::connection::Builder::unix_stream(socket).user_id(cli.guest_uid)
}
.name("org.freedesktop.portal.Desktop")?
.name("org.freedesktop.portal.Documents")?
.build()
.await?;
on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp).await
});
tokio::spawn(
async {
match f.await {
Ok(()) => debug!("done with server"),
Err(err) => error!(%err, "error dealing with server"),
}
}
.instrument(info_span!("serve", ?remote_addr)),
);
}
}));
}
if let Some(port) = cli.vsock_port {
// TODO: modprobe vhost_vsock first!
server_tasks.spawn(
vsock::ListenerBuilder::new(vsock::VsockAddr::new(vsock::VMADDR_CID_HOST, port))
.with_label("VM Bus")
.listen(move |client| {
enclose!((file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp) async move {
// TODO: Not necessary to go through the channel, add vsock support to the Peer too?
let client_conn = client.build().await?;
on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp).await
})
})
.map_ok_or_else(
|e| {
error!("vsock listener: {:?}", e);
},
|()| (),
),
);
}
let mut waiter = child_procs
.iter_mut()
.map(|child| child.wait())
.collect::<FuturesUnordered<_>>();
tokio::select! {
_ = server_tasks.join_all() => debug!("server tasks ended"),
res = waiter.next() => debug!(?res, "child process terminated"),
_ = tokio::signal::ctrl_c() => debug!("interrupt signal"),
};
drop(waiter);
for mut child in child_procs {
if let Err(e) = child.kill().await {
error!(?e, "could not kill process");
}
}
Ok(())
}

View file

@ -8,6 +8,7 @@ use zbus::{Connection, ObjectServer, fdo::Result, zvariant};
use super::documents::DocumentsProxy;
use super::request::{RESPONSE_SUCCESS, ReqHandler, ResultTransformer};
#[derive(Clone)]
pub struct FileChooser {
host: FileChooserProxy<'static>,
docs: DocumentsProxy<'static>,
@ -52,6 +53,7 @@ impl FileChooser {
docs: self.docs.clone(),
guest_root: self.guest_root.clone(),
for_save: false,
persistent: true,
directory: options.get_as("directory")?.unwrap_or(false),
})
.perform(async || self.host.open_file(parent_window, title, options).await)
@ -72,6 +74,7 @@ impl FileChooser {
docs: self.docs.clone(),
guest_root: self.guest_root.clone(),
for_save: true,
persistent: true,
directory: false,
})
.perform(async || self.host.save_file(parent_window, title, options).await)
@ -92,6 +95,7 @@ impl FileChooser {
docs: self.docs.clone(),
guest_root: self.guest_root.clone(),
for_save: true,
persistent: true,
directory: false,
})
.perform(async || self.host.save_files(parent_window, title, options).await)
@ -105,11 +109,13 @@ impl FileChooser {
}
}
struct FileTransformer {
docs: DocumentsProxy<'static>,
guest_root: PathBuf,
for_save: bool,
directory: bool,
#[derive(Clone)]
pub struct FileTransformer {
pub docs: DocumentsProxy<'static>,
pub guest_root: PathBuf,
pub for_save: bool,
pub persistent: bool,
pub directory: bool,
}
// ref: send_response_in_thread_func
@ -133,6 +139,14 @@ impl ResultTransformer for FileTransformer {
.async_map(|u| self.add_path_as_doc(u))
.await
.flatten()
.map(|path| match url::Url::from_file_path(&path) {
Ok(url) => Some(url.to_string()),
Err(err) => {
warn!(?err, ?path, "could not make url from returned path");
None
}
})
.flatten()
.collect::<Vec<_>>();
results.insert("uris", guest_uris.into());
@ -149,7 +163,7 @@ const DIRECTORY: u32 = 1 << 3;
// https://github.com/flatpak/xdg-desktop-portal/blob/10e712e06aa8eb9cd0e59c73c5be62ba53e981a4/src/xdp-documents.c#L71
impl FileTransformer {
async fn add_path_as_doc(&self, path: PathBuf) -> Option<String> {
pub async fn add_path_as_doc(&self, path: PathBuf) -> Option<PathBuf> {
use rustix::fs::{Mode, OFlags};
let o_path_fd = match rustix::fs::open(
@ -165,8 +179,8 @@ impl FileTransformer {
};
let flags = REUSE_EXISTING
| PERSISTENT
| AS_NEEDED_BY_APP
| if self.persistent { PERSISTENT } else { 0 }
| if self.directory { DIRECTORY } else { 0 };
// XXX: portal impl can return writable=false but host frontend does not pass that back..
@ -211,14 +225,7 @@ impl FileTransformer {
return None;
}
};
let path = self.guest_root.join(doc_id).join(filename);
match url::Url::from_file_path(&path) {
Ok(url) => Some(url.to_string()),
Err(err) => {
warn!(?err, ?path, "could not make url from returned path");
None
}
}
Some(self.guest_root.join(doc_id).join(filename))
}
}

View file

@ -0,0 +1,205 @@
use std::{
collections::HashMap,
os::fd::{AsFd, AsRawFd},
os::unix::ffi::OsStrExt,
path::PathBuf,
};
use tokio::sync::broadcast;
use tokio_stream::StreamExt;
use tracing::{debug, error};
use zbus::{
Connection, fdo::Result, names::OwnedUniqueName, object_server::SignalEmitter, zvariant,
};
use super::{documents::DocumentsProxy, file_chooser::FileTransformer};
#[derive(Clone)]
pub struct FileTransfer {
host: FileTransferProxy<'static>,
file_transformer: FileTransformer,
tx: broadcast::Sender<ForwarderCommand>,
path_prefix_to_host: Vec<(PathBuf, PathBuf)>,
}
#[derive(Clone, Debug)]
enum ForwarderCommand {
Add(String, OwnedUniqueName),
// Remove(String),
}
#[zbus::interface(
name = "org.freedesktop.portal.FileTransfer",
proxy(
default_service = "org.freedesktop.portal.Documents",
default_path = "/org/freedesktop/portal/documents"
)
)]
impl FileTransfer {
async fn add_files(
&self,
key: &str,
fds: Vec<zbus::zvariant::Fd<'_>>,
options: HashMap<&str, zvariant::Value<'_>>,
) -> Result<()> {
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(
&self,
key: &str,
options: HashMap<&str, zvariant::Value<'_>>,
) -> Result<Vec<String>> {
let host_paths = self.host.retrieve_files(key, options).await?;
let mut result = Vec::with_capacity(host_paths.len());
for host_path in host_paths {
if let Some(guest_path) = self
.file_transformer
.add_path_as_doc(PathBuf::from(&host_path))
.await
{
result.push(guest_path.to_string_lossy().into_owned());
} else {
debug!(%host_path, "could not add path as doc to retrieve file");
}
}
Ok(result)
}
async fn start_transfer(
&self,
#[zbus(header)] hdr: zbus::message::Header<'_>,
options: HashMap<&str, zvariant::Value<'_>>,
) -> Result<String> {
let sender = hdr
.sender()
.ok_or_else(|| zbus::Error::MissingField)?
.to_owned();
let key = self.host.start_transfer(options).await?;
debug!(%key, %sender, "started transfer");
if let Err(err) = self
.tx
.send(ForwarderCommand::Add(key.clone(), sender.into()))
{
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!(%key, "stopping transfer");
self.host.stop_transfer(key).await
}
#[zbus(signal)]
async fn transfer_closed(signal_emitter: &SignalEmitter<'_>, key: &str) -> zbus::Result<()>;
#[zbus(property, name = "version")]
fn version(&self) -> Result<u32> {
Ok(1)
}
}
impl FileTransfer {
pub async fn new(
host_session_conn: &Connection,
priv_conn: &Connection,
guest_root: PathBuf,
path_prefix_to_host: Vec<(PathBuf, PathBuf)>,
) -> Result<Self> {
let host = FileTransferProxy::builder(host_session_conn)
.build()
.await?;
let docs = DocumentsProxy::builder(priv_conn).build().await?;
let file_transformer = FileTransformer {
docs,
guest_root,
for_save: false,
persistent: false,
directory: false,
};
let (tx, _) = broadcast::channel(8);
Ok(FileTransfer {
host,
file_transformer,
tx,
path_prefix_to_host,
})
}
pub async fn forward_transfer_closed(
&self,
mut signal_emitter: SignalEmitter<'static>,
) -> Result<()> {
let mut stream = self.host.receive_transfer_closed().await?;
let mut cmds = self.tx.subscribe();
let mut receivers = HashMap::new();
loop {
tokio::select! {
Ok(cmd) = cmds.recv() => match cmd {
ForwarderCommand::Add(key, receiver) => { receivers.insert(key, receiver); },
// ForwarderCommand::Remove(key) => { receivers.remove(&key); },
},
Some(signal) = stream.next() => {
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!(?err, %key, "could not forward signal");
}
} else {
error!(%key, "got a signal for unknown key");
}
} else {
error!("could not deserialize transfer closed signal");
};
}
}
}
}
}

View file

@ -1,3 +1,7 @@
pub mod documents;
pub mod file_chooser;
pub mod file_transfer;
pub mod notification;
pub mod print;
pub mod request;
pub mod settings;

View file

@ -0,0 +1,95 @@
use std::collections::HashMap;
use tokio_stream::StreamExt;
use tracing::warn;
use zbus::{Connection, fdo::Result, names::UniqueName, object_server::SignalEmitter, zvariant};
#[derive(Clone)]
pub struct Notification {
host: NotificationProxy<'static>,
}
#[zbus::interface(
name = "org.freedesktop.portal.Notification",
proxy(
default_service = "org.freedesktop.portal.Desktop",
default_path = "/org/freedesktop/portal/desktop"
)
)]
impl Notification {
async fn add_notification(
&self,
#[zbus(header)] hdr: zbus::message::Header<'_>,
id: &str,
notification: HashMap<&str, zvariant::Value<'_>>,
) -> Result<()> {
let sender = hdr.sender().ok_or_else(|| zbus::Error::MissingField)?;
self.host
.add_notification(
&format!("{sender}\x0C\x0CSIDEBUS\x0C\x0C{id}"),
notification,
)
.await
}
async fn remove_notification(
&self,
#[zbus(header)] hdr: zbus::message::Header<'_>,
id: &str,
) -> Result<()> {
let sender = hdr.sender().ok_or_else(|| zbus::Error::MissingField)?;
self.host
.remove_notification(&format!("{sender}\x0C\x0CSIDEBUS\x0C\x0C{id}"))
.await
}
#[zbus(signal)]
async fn action_invoked(
signal_emitter: &SignalEmitter<'_>,
id: &str,
action: &str,
parameter: Vec<zvariant::Value<'_>>,
) -> zbus::Result<()>;
#[zbus(property)]
async fn supported_options(&self) -> Result<HashMap<String, zvariant::OwnedValue>> {
self.host
.supported_options()
.await
.map_err(|err| err.into())
}
#[zbus(property, name = "version")]
fn version(&self) -> Result<u32> {
Ok(2)
}
}
impl Notification {
pub async fn new(host_session_conn: &Connection) -> Result<Self> {
let host = NotificationProxy::builder(host_session_conn)
.build()
.await?;
Ok(Self { host })
}
pub async fn forward_actions(&self, mut signal_emitter: SignalEmitter<'static>) -> Result<()> {
let mut stream = self.host.receive_action_invoked().await?;
while let Some(x) = stream.next().await {
let args = x.args()?;
let mut split = args.id.split("\x0C\x0CSIDEBUS\x0C\x0C");
let sender = split
.next()
.and_then(|x| UniqueName::try_from(x).ok().map(|x| x.to_owned()))
.ok_or_else(|| zbus::fdo::Error::Failed("bad ID".to_owned()))?;
let id = split
.next()
.ok_or_else(|| zbus::fdo::Error::Failed("bad ID".to_owned()))?;
signal_emitter = signal_emitter.set_destination(sender.into());
Notification::action_invoked(&signal_emitter, id, args.action, args.parameter).await?;
()
}
warn!("actions stream end");
Ok(())
}
}

View file

@ -0,0 +1,66 @@
use std::collections::HashMap;
use zbus::{Connection, ObjectServer, fdo::Result, zvariant};
use super::request::ReqHandler;
#[derive(Clone)]
pub struct Print {
host: PrintProxy<'static>,
}
#[zbus::interface(
name = "org.freedesktop.portal.Print",
proxy(
default_service = "org.freedesktop.portal.Desktop",
default_path = "/org/freedesktop/portal/desktop"
)
)]
impl Print {
async fn prepare_print(
&self,
#[zbus(header)] hdr: zbus::message::Header<'_>,
#[zbus(object_server)] server: &ObjectServer,
#[zbus(connection)] conn: &Connection,
parent_window: &str,
title: &str,
settings: HashMap<&str, zvariant::Value<'_>>,
page_setup: HashMap<&str, zvariant::Value<'_>>,
options: HashMap<&str, zvariant::Value<'_>>,
) -> Result<zvariant::OwnedObjectPath> {
ReqHandler::prepare(&self.host, hdr, server, conn, &options)
.perform(async || {
self.host
.prepare_print(parent_window, title, settings, page_setup, options)
.await
})
.await
}
async fn print(
&self,
#[zbus(header)] hdr: zbus::message::Header<'_>,
#[zbus(object_server)] server: &ObjectServer,
#[zbus(connection)] conn: &Connection,
parent_window: &str,
title: &str,
fd: zvariant::Fd<'_>,
options: HashMap<&str, zvariant::Value<'_>>,
) -> Result<zvariant::OwnedObjectPath> {
ReqHandler::prepare(&self.host, hdr, server, conn, &options)
.perform(async || self.host.print(parent_window, title, fd, options).await)
.await
}
#[zbus(property, name = "version")]
fn version(&self) -> Result<u32> {
Ok(3)
}
}
impl Print {
pub async fn new(host_session_conn: &Connection) -> Result<Self> {
let host = PrintProxy::builder(host_session_conn).build().await?;
Ok(Self { host })
}
}

View file

@ -0,0 +1,66 @@
use std::collections::HashMap;
use tokio_stream::StreamExt;
use tracing::warn;
use zbus::{Connection, fdo::Result, object_server::SignalEmitter, zvariant};
#[derive(Clone)]
pub struct Settings {
host: SettingsProxy<'static>,
}
#[zbus::interface(
name = "org.freedesktop.portal.Settings",
proxy(
default_service = "org.freedesktop.portal.Desktop",
default_path = "/org/freedesktop/portal/desktop"
)
)]
impl Settings {
async fn read(&self, namespace: &str, key: &str) -> Result<zvariant::OwnedValue> {
self.host.read(namespace, key).await
}
async fn read_all(
&self,
namespaces: Vec<&str>,
) -> Result<HashMap<String, HashMap<String, zvariant::OwnedValue>>> {
self.host.read_all(namespaces).await
}
async fn read_one(&self, namespace: &str, key: &str) -> Result<zvariant::OwnedValue> {
self.host.read_one(namespace, key).await
}
/// SettingChanged signal
#[zbus(signal)]
async fn setting_changed(
signal_emitter: &SignalEmitter<'_>,
namespace: &str,
key: &str,
value: zvariant::Value<'_>,
) -> zbus::Result<()>;
#[zbus(property, name = "version")]
async fn version(&self) -> Result<u32> {
Ok(2)
}
}
impl Settings {
pub(crate) async fn new(host_session_conn: &Connection) -> Result<Self> {
let host = SettingsProxy::builder(host_session_conn).build().await?;
Ok(Self { host })
}
pub async fn forward_changes(&self, signal_emitter: &SignalEmitter<'static>) -> Result<()> {
let mut stream = self.host.receive_setting_changed().await?;
while let Some(x) = stream.next().await {
let args = x.args()?;
Settings::setting_changed(signal_emitter, args.namespace, args.key, args.value).await?;
()
}
warn!("settings change stream end");
Ok(())
}
}

View file

@ -13,12 +13,11 @@ impl ConnectionBuilder {
&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)
.server(guid)
.unwrap()
.p2p()
.auth_mechanism(zbus::AuthMechanism::Anonymous)
.name("org.freedesktop.portal.Desktop")?
.name("org.freedesktop.portal.Documents")?
.build()
.await
.map_err(|e| e.into())