We just landed a feature we’re pretty excited about: Pantavisor can now host a shared D-Bus system bus for you. Your D-Bus providers and clients get to be plain, single-pid app containers — no bundled dbus-daemon, no hand-written policy XML, no role users stuffed into /etc/passwd. You declare what a container owns and who it allows in a small JSON manifest, and Pantavisor wires up the bus, the identities, and a default-deny policy for you.
This shipped across a couple of PRs (“and friends”):
- pantavisor/pantavisor#733 — the design + runtime: managed
pv-dbusdaemon, role-per-socket identity, generated policy, state validation. - pantavisor/meta-pantavisor#367 — the BSP side: feature flag, images, example containers, release configs, and pvtests.
- Plus the avahi work that turns the flagship mDNS stack into a first-class hosted-bus citizen (part of #367’s commit series).
What & why
D-Bus is everywhere in the Linux userspace (NetworkManager, BlueZ, Avahi, systemd, …), but it has historically been painful to adopt in a container-per-service world. To ship one D-Bus provider today, a container image typically has to:
- run its own
dbus-daemon(another process to supervise inside the container), - ship a
busconfigpolicy XML that you have to keep correct by hand, - and carry role users in
/etc/passwdso the daemon can map policy to UIDs.
That’s a lot of ceremony for “I just want to expose an API.” It also means every provider re-implements the same plumbing, and there’s no single, device-wide bus that containers can share the way a normal Linux system has one.
The hosted system bus flips that around. Pantavisor runs one shared dbus-daemon as a managed daemon (pv-dbus), and containers connect to it through xconnect’s injected socket proxies. A provider becomes a single-pid app that just… provides. A client becomes a single-pid app that just… calls. The bus, the identities, and the security policy are the platform’s job now, not yours.
How it works
A few ideas do the heavy lifting:
Role-per-socket identity. Every attachment to the bus is keyed by (name, role, target). A container that needs more than one identity on the bus simply declares more than one requirement. Pantavisor allocates a stable, persistent numeric UID per role, and the xconnect proxy masquerades each connection to that role’s UID when it authenticates to pv-dbus. No shared-rootfs /etc/passwd edits — the daemon runs in a private passwd jail so the generated role users are visible only to it.
Declarative ownership + allow lists. A provider says what well-known name it owns and which caller roles may reach it. Here’s the example provider’s services.json:
{
"#spec": "service-manifest-xconnect@1",
"services": [
{ "type": "dbus", "bus": "system-bus", "owns": "org.pantavisor.Example",
"role": "example-service", "allow": ["operator", "monitor"] }
]
}
Both provider and consumer attach to the bus with a system-bus requirement under their role — the provider as example-service, the consumer as operator:
{ "name": "system-bus", "type": "dbus", "role": "operator",
"target": "/run/dbus/system_bus_socket" }
Generated, default-deny policy. From those owns/allow declarations, Pantavisor generates the D-Bus policy under /run/pv/dbus/policy.d. It’s default-deny: a caller whose role isn’t in a service’s allow list gets AccessDenied. Nobody hand-writes busconfig XML.
Validation that fails loud. State validation rejects collisions with the builtin system-bus export and rejects two containers trying to own the same well-known name. Misconfigurations fail at validation time, not mysteriously at runtime.
Opt-in, backwards-compatible. It’s gated by the xconnect-dbus-systembus build feature and the runtime config key xconnect.dbus.systembus.enabled (default true on images that build the feature). The older per-provider model — where a container brings its own dbus-daemon and XML — still works for cases that genuinely need hand-authored policy.
Avahi is the flagship driver
To prove this on a real daemon and not just a toy, we made pv-avahi a name-owning app on the hosted bus: it owns org.freedesktop.Avahi, so any other container can browse/resolve/publish mDNS through the standard Avahi D-Bus API — no per-container Avahi stack required. Its manifest looks exactly like the example above, just owning org.freedesktop.Avahi under an avahi-service role.
It degrades gracefully: if the bus socket isn’t present, avahi falls back to plain mDNS. Set PV_AVAHI_REQUIRE_DBUS=1 to make the bus mandatory (avahi is the container’s PID 1, so a missing bus becomes a non-zero exit and a clean rollback).
Coming soon to the starter image: the
pv-avahiexample containers are moving into the default pantavisor-starter image, so shared mDNS over the hosted bus works out of the box on a fresh device — no extra containers to add by hand.
Try it
Everything ships as example containers and automated pvtests in meta-pantavisor.
Example containers (recipes-containers/pv-examples/):
pv-example-dbus-host-server— single-pid provider, ownsorg.pantavisor.Examplepv-example-dbus-host-client— single-pid consumer, calls it under theoperatorrole- (the legacy
pv-example-dbus-server/pv-example-dbus-clientpair is kept as regression coverage of the bring-your-own-daemon model)
Build them:
./kas-container build kas/build-configs/release/docker-x86_64-scarthgap.yaml \
--target pv-example-dbus-host-server --target pv-example-dbus-host-client
Then watch the consumer make a successful call, and confirm a container asking for a role outside the allow list gets denied.
Automated pvtests (recipes-pv/pantavisor-pvtests/files/local/xconnect/):
local/xconnect/dbus-systembus— end-to-end owns/allow roundtrip, plus astrangerrole that is not in the allow list and is correctly DENIED (so a broken all-permit policy can’t sneak through).local/xconnect/avahi-systembus— the real deal:pv-avahiownsorg.freedesktop.Avahi, and a new single-pid consumerpv-avahi-browse(rolemonitor) roundtripsServer.GetVersionString/GetHostNameover the injected bus socket. A rogueRequestNameconfirms default-deny.
Run them from a built appengine distro:
./test.docker.sh -v run local/xconnect/dbus-systembus
./test.docker.sh -v run local/xconnect/avahi-systembus
Full walkthrough: docs/examples/xconnect-examples.md (“Hosted System Bus Example”).
A preview of what’s next: on-demand activation
The hosted bus is the foundation; the next layer we’re actively building on top of it is D-Bus service activation. The idea: a provider container doesn’t have to be running to be reachable. It sits passive (MOUNTED), and the moment a client makes the first call to a name it owns, Pantavisor starts it — the classic “activate on first use” behaviour you get from a normal Linux system bus, but for whole containers.
We’re doing this in-proxy inside pv-xconnect — fully async, with no per-service helper process and no .service activation files to author. You declare owns/allow exactly as you do today; a MOUNTED provider just becomes eligible for activation. The payoff is idle providers that cost nothing until something actually needs them — great for the long tail of “nice to have, rarely called” services on a constrained device.
This is still in progress (early phases are landing on a stacked branch), so treat it as a preview rather than something to build against yet. But it’s where the hosted bus is heading, and it’s why the declarative owns/allow model matters: the same manifests light up activation for free when it ships.
Where this is going
This is the foundation for making standard Linux D-Bus services trivially composable on Pantavisor devices. If you maintain a container around a D-Bus daemon (NetworkManager, BlueZ, …) and want it to be a clean single-pid app on a shared bus, this is the path — and we’d love to hear which services you’d want next.
Questions, kick the tires, tell us what breaks. ![]()
Links: meta-pantavisor#367 · pantavisor#733