libink: a message bus is not a peer

libink was written against the only bus it had, its own, where the
peer on the other end is the client.  A broker is not: it routes for
senders it names itself, expects a DESTINATION on anything addressed
through it, and answers on its own schedule rather than next.

Runlevels go on the wire as S and N rather than the digits Finit
keeps internally, since that is what a caller outside Finit means by
one.

The library stays a convenience library, linked into finit and
initctl and installed nowhere: the ABI promise waits until libink is
its own project.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg committed 2026-08-13 09:28:26 +02:00
1 parent 6310d9e760
commit b55dada80b
16 files changed
+340 -250

No files matched your search

+8 -1
View File
@@ -50,9 +50,16 @@ Below are a few of the main switches to configure:
`/proc/cmdline`, this is *not recommended* since Finit may be running as the
init for container apps that can see the host's `/proc` filesystem
* `--disable-dbus`: Opt out of Finit's built-in D-Bus support, enabled by
default. See [D-Bus Integration](dbus.md) for what it provides. Not to
be confused with `--enable-dbus-plugin` below, which only starts an
external `dbus-daemon`
* `--enable-alsa-utils-plugin`: Enable the optional `alsa-utils.so` sound plugin.
* `--enable-dbus-plugin`: Enable the optional D-Bus `dbus.so` plugin.
* `--enable-dbus-plugin`: Enable the optional D-Bus `dbus.so` plugin, which
launches `dbus-daemon` at boot. Unrelated to the built-in bus, see
`--disable-dbus` above.
* `--enable-resolvconf-plugin`: Enable the `resolvconf.so` optional plugin.
+152 -166
View File
@@ -1,45 +1,45 @@
D-Bus Integration
=================
Finit ships with a built-in, brokerless [D-Bus][] implementation,
**libink**, that exposes the running init system as a peer on its own
private bus, and optionally on the system bus when `dbus-daemon` is
available. Everything `initctl` does is also reachable from any
generic D-Bus tooling — `dbus-send`, `dbus-monitor`, `gdbus`,
language bindings, dashboards, monitoring agents, etc.
Finit ships with a built-in, brokerless [D-Bus][] implementation, **libink**
(`-link`), that exposes the running init system as a peer on its own private
bus, and optionally on the system bus when `dbus-daemon` is available.
Everything `initctl` does is also reachable from any generic D-Bus tooling —
`dbus-send`, `dbus-monitor`, `gdbus`, language bindings, dashboards,
monitoring agents, etc.
> [!NOTE]
> D-Bus support is enabled at build time with `--enable-dbus`. See
> [Building](build.md) for details. When disabled, `initctl` keeps
> using the legacy `INIT_SOCKET` transport and Finit exposes no bus.
> D-Bus support is enabled by default, opt out at build time with
> `--disable-dbus`, see [Building](build.md) for details. When disabled,
> `initctl` keeps using the legacy `INIT_SOCKET` transport and Finit
> exposes no bus.
Bus address
-----------
| Bus | Address |
| --- | --- |
| Local (always) | `unix:path=/run/finit/bus` |
| System (opportunistic) | `unix:path=/var/run/dbus/system_bus_socket`, well-known name `org.finit` |
| Bus | Address |
|------------------------|---------------------------------------------|
| Local (always) | `unix:path=/run/finit/bus` |
| System (opportunistic) | `unix:path=/var/run/dbus/system_bus_socket` |
The **local** bus is brokerless: clients connect straight to Finit
over a Unix-domain socket using the standard D-Bus SASL EXTERNAL
handshake. No `dbus-daemon` is required, which makes it suitable for
embedded systems that don't ship one.
The **local** bus is brokerless: clients connect straight to Finit over a
Unix-domain socket using the standard D-Bus SASL EXTERNAL handshake. No
`dbus-daemon` is required, which makes it suitable for embedded systems that
don't ship one.
The **system** bus is best-effort: at start-up Finit probes for a
running `dbus-daemon` and, if reachable, registers `org.finit` so that
standard tooling sees Finit just like any other system service:
The **system** bus is best-effort: Finit probes for a running `dbus-daemon`
and, when reachable, claims the well-known name `org.finit` so that standard
tooling sees Finit just like any other system service:
```sh
dbus-send --system --print-reply --dest=org.finit \
/org/finit/manager \
dbus-send --system --print-reply --dest=org.finit /org/finit/manager \
org.finit.Manager1.ListServices
dbus-monitor --system "sender='org.finit'"
```
If no system bus is present (the common case on embedded targets),
this step is silently skipped.
If no system bus is present (the common case on embedded targets), this step
is silently skipped.
Object tree
-----------
@@ -59,12 +59,15 @@ Object tree
Every node implements the usual stock interfaces:
| Interface | Purpose |
| ------------------------------------ | ------- |
| `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` (on `/org/freedesktop/DBus`) |
| `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` |
| `org.freedesktop.DBus.Introspectable`| `Introspect()` — XML description |
| `org.freedesktop.DBus.Properties` | `Get`, `GetAll` (Set not yet implemented) |
| Interface | Purpose |
|---------------------------------------|-------------------------------------------|
| `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` |
| `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` |
| `org.freedesktop.DBus.Introspectable` | `Introspect()` — XML description |
| `org.freedesktop.DBus.Properties` | `Get`, `GetAll` (Set not yet implemented) |
Note: `Hello`, `AddMatch`, and `RemoveMatch` are answered on the canonical
`/org/freedesktop/DBus` object only, as per the D-Bus specification.
`org.finit.Manager1`
--------------------
@@ -74,88 +77,89 @@ and the service registry.
### Methods
| Method | In sig | Out sig | Privileged | Notes |
| ----------------------- | ------ | ------- | ---------- | ----- |
| `ListServices` | — | `as` | no | Returns the identities (`name`, `name:id`) of every loaded service. |
| `GetService` | `s` | `o` | no | Resolves a service identity to its `Service1` object path. |
| `Start` | `s` | — | yes | Start the service(s) matching the identity. |
| `Stop` | `s` | — | yes | Stop the service(s) matching the identity. |
| `Restart` | `s` | — | yes | Restart (stop + start) the service(s). |
| `Reload` | — | — | yes | Re-read all `*.conf` and apply changes (same as `initctl reload`). |
| `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–6). |
| `SetDebug` | — | — | yes | Toggle Finit's runtime debug flag. |
| `Signal` | `su` | — | yes | Send signal number `u` (1–31) to every running service matching identity `s`. Halted matches are silently skipped. |
| `Suspend` | — | — | yes | `sync()` + suspend-to-RAM. |
| `Reboot` / `Halt` / `Poweroff` | — | — | yes | Trigger the corresponding shutdown sequence. |
| Method | In sig | Out sig | Priv. | Notes |
|------------------------------|--------|---------|-------|-----------------------------------------------------------|
| `ListServices` | — | `as` | no | Returns identities (`name`, `name:id`) of loaded services |
| `GetService` | `s` | `o` | no | Resolves an identity to its `Service1` object path |
| `Start` | `s` | — | yes | Start the service(s) matching the identity |
| `Stop` | `s` | — | yes | Stop the service(s) matching the identity |
| `Restart` | `s` | — | yes | Restart (stop + start) the service(s) |
| `Reload` | — | — | yes | Re-read all `*.conf` and apply changes |
| `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–6) |
| `SetDebug` | — | — | yes | Toggle Finit's runtime debug flag |
| `Signal` | `su` | — | yes | Send signal `u` (1–31) to services matching identity `s` |
| `Suspend` | — | — | yes | `sync()` + suspend-to-RAM |
| `Reboot`, `Halt`, `Poweroff` | — | — | yes | Trigger the corresponding shutdown sequence |
### Properties
All read-only strings; observable via `Properties.Get` and
`Properties.GetAll`.
| Property | Type | Returns |
| -------------- | ---- | ------- |
| `Runlevel` | `s` | Current runlevel as a digit (`"2"`, `"3"`, …) or `"S"`. |
| `PrevRunlevel` | `s` | Previous runlevel, same encoding. |
| `Version` | `s` | Finit's version string (`PACKAGE_VERSION`). |
| Property | Type | Returns |
|----------------|------|--------------------------------------------------------|
| `Runlevel` | `s` | Current runlevel as a digit (`"2"`, `"3"`, …) or `"S"` |
| `PrevRunlevel` | `s` | Previous runlevel, same encoding, `"N"` when none |
| `Version` | `s` | Finit's version string (`PACKAGE_VERSION`) |
### Signals
| Signal | Body | Fires when |
| ----------------------- | ---- | ---------- |
| `ServiceStateChanged` | `(sss)` — identity, old state, new state | A service transitions between supervisor states. |
| `RunlevelChanged` | `(ss)` — old level, new level | The system enters a new runlevel. |
| Signal | Body | Fires when |
|-----------------------|----------------------------------------|------------------------|
| `ServiceStateChanged` | `sss` — identity, old state, new state | Service transitions |
| `RunlevelChanged` | `ss` — old level, new level | System runlevel change |
State names emitted by `ServiceStateChanged` are stable wire strings:
`halted`, `done`, `dead`, `cleanup`, `teardown`, `stopping`, `setup`,
`paused`, `waiting`, `starting`, `running`.
`paused`, `waiting`, `starting`, `running`. `RunlevelChanged` levels
use the same encoding as the `Runlevel` property: digits, `"S"`, `"N"`.
`org.finit.Service1` (per-service objects)
------------------------------------------
Lives at **`/org/finit/service/<encoded>`**, one object per loaded
service. `<encoded>` is the service identity (name, or `name:id` for
templated services) put through systemd-style `_HH` hex escaping —
ASCII alphanumerics and `_` pass through, anything else becomes `_HH`
where `HH` is the hex byte. Use `Manager1.GetService(identity)` to
look up the exact path rather than constructing it by hand.
Lives at **`/org/finit/service/<encoded>`**, one object per loaded service.
`<encoded>` is the service identity (name, or `name:id` for templated
services) put through systemd-style `_HH` hex escaping — ASCII alphanumerics
and `_` pass through, anything else becomes `_HH` where `HH` is the hex byte.
Use `Manager1.GetService(identity)` to look up the exact path rather than
constructing it by hand.
| Method | In sig | Out sig | Privileged | Notes |
| --------- | ------ | ------- | ---------- | ----- |
| `Start` | — | — | yes | Equivalent to `Manager1.Start(<identity>)` for this service. |
| `Stop` | — | — | yes | … |
| `Restart` | — | — | yes | … |
| `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart). |
| Method | In sig | Out sig | Priv. | Notes |
|-----------|--------|---------|-------|-------------------------------------------------------------|
| `Start` | — | — | yes | Equivalent to `Manager1.Start(<identity>)` for this service |
| `Stop` | — | — | yes | … |
| `Restart` | — | — | yes | … |
| `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart) |
### Properties
All read-only; observable via `Properties.Get` and `Properties.GetAll`.
| Property | Type | Returns |
| -------------- | ---- | ------- |
| `Identity` | `s` | Service identity, `name` or `name:id`. |
| `Name` | `s` | Program name (basename of the command). |
| `State` | `s` | Current status, same vocabulary as `initctl status` (richer than the coarse `ServiceStateChanged` strings). |
| `Pid` | `u` | Current PID, 0 when not running. |
| `RestartCount` | `u` | Restarts since the last stable run. |
| `Runlevels` | `u` | Allowed runlevels as a bitmask, bit N = runlevel N, bit 10 = S. |
| `Description` | `s` | The service's `description` string. |
| `Command` | `s` | Full command line, arguments included. |
| `Conditions` | `s` | Declared conditions, raw `.conf` form. |
| `Type` | `s` | Unit type: `service`, `task`, `run`, `sysv`, `tty`, `free`. |
| `Origin` | `s` | Source `.conf` file, empty for built-ins. |
| `Environment` | `s` | The service's `env` setting, raw. |
| `PidFile` | `s` | Declared PID file, raw (`!` prefix included). |
| `User` | `s` | User the service runs as. |
| `Group` | `s` | Group the service runs as. |
| `Uptime` | `u` | Seconds since start, 0 when not running. |
| `ExitStatus` | `u` | Raw `waitpid(2)` status from the last exit. |
| `RestartsTotal`| `u` | Restarts over the service's lifetime. |
| `RestartMax` | `u` | Restart limit before the service is blocked. |
| `Starts` | `u` | Times started, for `manual-start` units. |
| `ManualStart` | `b` | `manual-start` set in the `.conf`. |
| `Forking` | `b` | Daemon forks to background. |
| `Started` | `b` | Run/task completed successfully. |
| Property | Type | Returns |
|-----------------|------|----------------------------------------------------------------|
| `Identity` | `s` | Service identity, `name` or `name:id` |
| `Name` | `s` | Program name (basename of the command) |
| `State` | `s` | Current status, same vocabulary as `initctl status` |
| `Pid` | `u` | Current PID, 0 when not running |
| `RestartCount` | `u` | Restarts since the last stable run |
| `Runlevels` | `u` | Allowed runlevels as a bitmask, bit N = runlevel N, bit 10 = S |
| `Description` | `s` | The service's `description` string |
| `Command` | `s` | Full command line, arguments included |
| `Conditions` | `s` | Declared conditions, raw `.conf` form |
| `Type` | `s` | Unit type: `service`, `task`, `run`, `sysv`, `tty`, `free` |
| `Origin` | `s` | Source `.conf` file, empty for built-ins |
| `Environment` | `s` | The service's `env` setting, raw |
| `PidFile` | `s` | Declared PID file, raw (`!` prefix included) |
| `User` | `s` | User the service runs as |
| `Group` | `s` | Group the service runs as |
| `Uptime` | `u` | Seconds since start, 0 when not running |
| `ExitStatus` | `u` | Raw `waitpid(2)` status from the last exit |
| `RestartsTotal` | `u` | Restarts over the service's lifetime |
| `RestartMax` | `u` | Restart limit before the service is blocked |
| `Starts` | `u` | Times started, for `manual-start` units |
| `ManualStart` | `b` | `manual-start` set in the `.conf` |
| `Forking` | `b` | Daemon forks to background |
| `Started` | `b` | Run/task completed successfully |
On every state transition the object also emits the standard
`org.freedesktop.DBus.Properties.PropertiesChanged` signal: `State`
@@ -174,89 +178,87 @@ Lives at **`/org/finit/cond`**. Exposes Finit's
### Methods
| Method | In sig | Out sig | Privileged | Notes |
| -------- | ------ | ------- | ---------- | ----- |
| `Get` | `s` | `s` | no | Returns `"on"`, `"off"`, or `"flux"` for the named condition. |
| `Set` | `s` | — | yes | Assert a `usr/<name>` condition. Non-`usr/*` paths are rejected with `InvalidArgs` (system conditions belong to Finit's state machine). |
| `Clear` | `s` | — | yes | Deassert a `usr/<name>` condition. |
| `List` | — | `as` | no | Names of all known conditions. |
| `Dump` | — | `a(ss)` | no | `(name, state)` pairs for everything `List` returns. |
| Method | In sig | Out sig | Priv. | Notes |
|---------|--------|---------|-------|--------------------------------------------------------------|
| `Get` | `s` | `s` | no | Returns `"on"`, `"off"`, or `"flux"` for the named condition |
| `Set` | `s` | — | yes | Assert a `usr/<name>` condition |
| `Clear` | `s` | — | yes | Deassert a `usr/<name>` condition |
| `List` | — | `as` | no | Names of all known conditions |
| `Dump` | — | `a(ss)` | no | `(name, state)` pairs for everything `List` returns |
Note: non-`usr/*` paths are rejected with `InvalidArgs` -- system
conditions belong to Finit's state machine.
### Signals
| Signal | Body | Fires when |
| ------------------- | ---- | ---------- |
| `ConditionChanged` | `(ss)` — name, new state | A condition is asserted or deasserted. |
| Signal | Body | Fires when |
|--------------------|------------------------|---------------------------------------|
| `ConditionChanged` | `ss` — name, new state | A condition is asserted or deasserted |
Authorization
-------------
Privileged methods reject any caller whose peer `uid` isn't 0.
On the **local** bus the kernel's `SO_PEERCRED` socket option tells
Finit exactly who's calling, so privilege escalation through the bus
is impossible.
Privileged methods reject any caller whose peer `uid` isn't 0. On the
**local** bus the kernel's `SO_PEERCRED` socket option tells Finit exactly
who's calling, so privilege escalation through the bus is impossible.
On the **system** bus, all incoming traffic is treated as
unprivileged: it arrives through `dbus-daemon` (typically running as
root) and Finit cannot yet ask the daemon for the real requester's
uid via `GetConnectionUnixUser`. This means external tooling can
freely `Get`/`Introspect`/`ListServices`, but every state-changing
method returns `org.freedesktop.DBus.Error.AccessDenied`. Per-sender
uid lookup is on the roadmap.
On the **system** bus, all incoming traffic is treated as unprivileged: it
arrives through `dbus-daemon` (typically running as root) and Finit cannot yet
ask the daemon for the real requester's uid via `GetConnectionUnixUser`. This
means external tooling can freely `Get`/`Introspect`/`ListServices`, but every
state-changing method returns `org.freedesktop.DBus.Error.AccessDenied`.
Per-sender uid lookup is on the roadmap.
When a privileged method is rejected the error name is exactly
`org.freedesktop.DBus.Error.AccessDenied`, and the body carries a
short reason string (e.g. `"permission denied: Start requires root"`).
`org.freedesktop.DBus.Error.AccessDenied`, and the body carries a short reason
string (e.g. `"permission denied: Start requires root"`).
`initctl` integration
---------------------
`initctl` transparently routes through D-Bus when the bus socket is
present, and falls back to the legacy `INIT_SOCKET` transport
otherwise. Concretely, the following subcommands use the bus first:
`initctl` transparently routes through D-Bus when the bus socket is present,
and falls back to the legacy `INIT_SOCKET` transport otherwise. Concretely,
the following subcommands use the bus first:
| Subcommand | Method |
| ------------------- | ------------------------------- |
| `initctl start` | `Manager1.Start(svc)` |
| `initctl stop` | `Manager1.Stop(svc)` |
| `initctl restart` | `Manager1.Restart(svc)` |
| `initctl reload` | `Manager1.Reload()` |
| `initctl reload S` | `Service1.Reload()` (per-svc) |
| `initctl reboot` | `Manager1.Reboot()` |
| `initctl halt` | `Manager1.Halt()` |
| `initctl poweroff` | `Manager1.Poweroff()` |
| `initctl suspend` | `Manager1.Suspend()` |
| `initctl debug` | `Manager1.SetDebug()` |
| `initctl signal` | `Manager1.Signal(svc, signo)` |
| `initctl runlevel` | `Properties.Get(Manager1.Runlevel/PrevRunlevel)` |
| `initctl cond set/get/clr` | `Cond1.{Set,Get,Clear}` |
| Subcommand | Method |
|----------------------------|--------------------------------------------------|
| `initctl start` | `Manager1.Start(svc)` |
| `initctl stop` | `Manager1.Stop(svc)` |
| `initctl restart` | `Manager1.Restart(svc)` |
| `initctl reload` | `Manager1.Reload()` |
| `initctl reload S` | `Service1.Reload()` (per-svc) |
| `initctl reboot` | `Manager1.Reboot()` |
| `initctl halt` | `Manager1.Halt()` |
| `initctl poweroff` | `Manager1.Poweroff()` |
| `initctl suspend` | `Manager1.Suspend()` |
| `initctl debug` | `Manager1.SetDebug()` |
| `initctl signal` | `Manager1.Signal(svc, signo)` |
| `initctl runlevel` | `Properties.Get(Manager1.Runlevel/PrevRunlevel)` |
| `initctl cond set/get/clear` | `Cond1.{Set,Get,Clear}`, `clr` is an alias |
Two `initctl` subcommands are pure D-Bus features without legacy
equivalents:
Two `initctl` subcommands are pure D-Bus features without legacy equivalents:
* `initctl monitor` — subscribes to every signal on the local bus and
prints one line per delivery (with timestamp, interface and
member). Same idea as `dbus-monitor`, but scoped to Finit and with
no need to pass `--address`.
* `initctl monitor` — subscribes to every signal on the local bus and prints
one line per delivery (with timestamp, interface and member). Same idea as
`dbus-monitor`, but scoped to Finit and with no need to pass `--address`.
* `initctl cond` (when D-Bus is reachable) emits the standard
`Cond1.ConditionChanged` signal as a side effect, so subscribers
observe user-driven state changes the same way they observe
service-driven ones.
* `initctl cond` emits the standard `Cond1.ConditionChanged` signal as a side
effect, so subscribers observe user-driven state changes the same way they
observe service-driven ones.
Examples
--------
The examples below use `dbus-send` and `dbus-monitor`, which ship as
part of the [dbus][] reference implementation; they're widely
packaged and don't pull in any extra runtime. Any tool that speaks
D-Bus over an AF_UNIX socket works equally well — `gdbus`, Python's
`jeepney`/`dasbus`, etc. — substitute their syntax for setting the
bus address.
The examples below use `dbus-send` and `dbus-monitor`, which ship as part of
the [dbus][] reference implementation; they're widely packaged and don't pull
in any extra runtime. Any tool that speaks D-Bus over an AF_UNIX socket works
equally well — `gdbus`, Python's `jeepney`/`dasbus`, etc. — substitute their
syntax for setting the bus address. The wire protocol is the compatibility
surface: Finit's own **libink** is an internal implementation detail, external
clients should use any standard D-Bus library.
When `org.finit` is registered on the system bus you can replace
`--address=unix:path=/run/finit/bus` with `--system` in any example
below.
`--address=unix:path=/run/finit/bus` with `--system` in any example below.
List the running services:
@@ -309,21 +311,5 @@ The `--dest=org.finit` argument is informational on the local
brokerless bus — Finit accepts any destination because there's no
broker to route by name — but `dbus-send` requires it syntactically.
[dbus]: https://gitlab.freedesktop.org/dbus/dbus
Implementation notes
--------------------
The D-Bus server library lives in `libink/`. It speaks the binary
D-Bus 1.0 wire format directly, has no `libdbus`/`sd-bus`/`GIO`
dependency, and is tiny — a few thousand lines of C. The Finit-side
glue in `src/dbus.c` registers vtables for Manager1/Service1/Cond1,
emits the four signals from the appropriate hook points (state
transitions, runlevel transitions, condition flips), and bridges the
event loop to the libink server.
The `initctl` client uses the same library — `link_client_open`,
`link_client_call_v`, `link_client_reply`, `link_reader_*` — so the
single wire-format implementation serves both ends.
[dbus]: https://gitlab.freedesktop.org/dbus/dbus
[D-Bus]: https://dbus.freedesktop.org/doc/dbus-specification.html