D-Bus without the daemon: Pantavisor now hosts a shared system bus for your containers

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-dbus daemon, 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 busconfig policy XML that you have to keep correct by hand,
  • and carry role users in /etc/passwd so 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-avahi example 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, owns org.pantavisor.Example
  • pv-example-dbus-host-client — single-pid consumer, calls it under the operator role
  • (the legacy pv-example-dbus-server / pv-example-dbus-client pair 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 a stranger role 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-avahi owns org.freedesktop.Avahi, and a new single-pid consumer pv-avahi-browse (role monitor) roundtrips Server.GetVersionString / GetHostName over the injected bus socket. A rogue RequestName confirms 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. :rocket:


Links: meta-pantavisor#367 · pantavisor#733

cool feature! I will give a try!