Guest Daemon Architecture¶
The guest daemon (podbox-guest) runs inside the container. It bridges
container capabilities (notifications, URI opening, clipboard, host execution)
to the host via a Unix socket connection.
Entry Point (entry.rs)¶
The container starts with podbox-guest --entry [<command>...].
-
Create flatpak sandbox marker — symlink
/run/user/%U/flatpak-info→/.flatpak-infoso that portal-aware toolkits believe they are inside a Flatpak sandbox and route audio/video capture through portals. -
fork()splits into two processes: -
Child (daemon process): redirects stdio to
/dev/null, then execspodbox-guest --daemon(re-exec). Runs the event loop. -
Parent (shell/command process): if a command was given, execs it via
execv. If empty, execs a login shell ($SHELLor/bin/bash, withargv[0]prefixed by-for login mode). -
The parent replaces itself with the shell/command. The child runs independently as a background daemon with a 5-minute idle timeout.
Daemon Lifecycle (daemon.rs)¶
Startup sequence¶
- Create
/run/podbox/bin/— directory for interceptor symlinks - Check version drift — compare
PODBOX_HOST_VERSIONenv var againstpodbox-guestversion; warn on mismatch via notification or stderr - Connect to host socket —
$XDG_RUNTIME_DIR/podbox/<container>.sockwith poll-based retry (3 attempts, 500ms interval, zero CPU) - Handshake — sends capability list (
notify,xdg_open,clipboard,host_exec) to host; host responds with accepted subset - Install interceptors — creates symlinks in
/run/podbox/bin/for each accepted capability - PATH injection — writes
/etc/profile.d/podbox.shand/etc/fish/conf.d/podbox.fishthat prepend/run/podbox/bintoPATH - Event loop — polls the host socket for messages
Event loop¶
The event loop is poll()-based on the host socket connection and tracked
user processes via pidfds (Linux 5.3+). It uses a configurable idle
timeout (lifecycle.idle_timeout, default "off") — if no user
processes remain and the timeout expires, the daemon sends IdleTimeout
and exits gracefully.
| Event | Action |
|---|---|
Shutdown message |
Exit daemon |
Ping message |
No-op (keepalive) |
CheckIdle message |
Scan /proc for user processes; reply Busy or IdleTimeout |
None / EOF |
Host disconnected; exit |
POLLHUP / POLLERR |
Host socket hung up; exit |
| Idle timeout | No user processes found before timeout; send IdleTimeout, exit |
EINTR |
Retry poll() |
The daemon consumes 0% CPU when idle — it is parked in the kernel by
poll().
Socket Protocol¶
The daemon connects to the host socket at
$XDG_RUNTIME_DIR/podbox/<container>.sock. Messages are length-prefixed
JSON over a Unix stream socket (see protocol.md for the wire
format).
Handshake¶
→ {"type":"hello","version":"0.1.0","container":"myenv","capabilities":["notify","xdg_open","clipboard","host_exec"]}
← {"type":"hello_ack","accepted":["notify","xdg_open"],"rejected":["clipboard","host_exec"]}
Interceptors¶
Symlink dispatch¶
The daemon creates symlinks in /run/podbox/bin/ pointing to the
podbox-guest binary:
| Symlink | Target | Capability |
|---|---|---|
/run/podbox/bin/notify-send |
podbox-guest |
notify |
/run/podbox/bin/xdg-open |
podbox-guest |
xdg_open |
/run/podbox/bin/podbox-clipboard |
podbox-guest |
clipboard |
/run/podbox/bin/host-exec |
podbox-guest |
host_exec |
The binary detects which name was used to invoke it via argv[0] and
dispatches to the appropriate interceptor module (main.rs).
PATH injection¶
Two files are written to ensure interceptors are always found:
/etc/profile.d/podbox.shfor POSIX shells/etc/fish/conf.d/podbox.fishfor Fish shell
Both prepend /run/podbox/bin to PATH. This ensures the interceptor
symlinks take precedence over system-installed binaries.
Interceptor types¶
| Interceptor | File | What it does |
|---|---|---|
notify-send |
interceptors/notify.rs |
Parses CLI args, sends GuestMessage::Notify to host. Supports --action/-A for action buttons. Waits for NotifyActionResult response when actions are present. |
xdg-open |
interceptors/xdg_open.rs |
Sends URI in GuestMessage::XdgOpen to host |
podbox-clipboard |
interceptors/clipboard.rs |
set: reads stdin, sends ClipboardSet; get: sends ClipboardGet, writes response to stdout |
host-exec |
interceptors/host_exec.rs |
Connects to host socket, sends HostExec with command, then relays HostExecStdout/HostExecStderr/HostExecDone responses to stdout/stderr and exits with the remote exit code |
Each interceptor opens a direct, ephemeral Unix socket connection to the host socket (not the daemon's persistent connection), sends its message, and waits for acknowledgement before exiting.
Host-exec security¶
Host-exec is disabled by default. When enabled via [integration.host_exec], the host validates every command:
-
Allowlist — If configured, only commands whose aliases appear in the map may be run. The mapped host path is used (guest
$PATHis ignored). Example error: -
Shell metacharacters — Arguments containing
;,|,&,$, or`are rejected: -
Dangerous flag patterns — Arguments matching
--exec-path,--config,-o, etc. are blocked: -
Absolute path bypass — Using
/usr/bin/gitwhen the allowlist key isgitis rejected: