Architecture
Pebble is a declarative configuration compiler and a transactional deployment manager for FreeBSD. It is not a network stack, a firewall, a DHCP server or a resolver: those are PF, dnsmasq, Unbound and wg(4).
Pipeline#
pebble.yaml
│ config.Parse strict YAML: unknown fields, duplicate keys, extra documents rejected
▼
config.Config typed schema (internal/config/types.go)
│ config.Validate static + semantic checks, errors and warnings with paths
▼
config.Topology parsed addressing; zone of every network (lan / wgN / wan)
│ compiler.Compile pure, deterministic; no files, no commands, no root
▼
compiler.Result pf.conf, pf-fallback.conf, unbound.conf, dnsmasq.conf,
│ wg setconf + meta files, routes, dhclient.conf, cron,
│ rc.conf variables, PF policy model
│ deploy.Engine.Apply
▼
FreeBSD native validation → snapshot → write → live operations →
health checks → confirm or roll back
The compiler and everything above it are pure Go and fully testable on any
platform. All contact with the operating system goes through
internal/platform: a command runner that only executes a fixed list of
absolute paths with structured arguments (never a shell), and a filesystem
layer that writes atomically and refuses symbolic links.
Packages#
| Package | Responsibility |
|---|---|
cmd/pebble |
entry point |
internal/cli |
command-line interface, exit codes, JSON output |
internal/config |
schema, strict loader, defaults, validation, topology |
internal/compiler |
assembles every generated artefact deterministically |
internal/network |
rc.conf managed block, routes file, dhclient.conf |
internal/firewall |
PF policy model, pf.conf renderer, flow evaluator, explanation |
internal/dhcp |
dnsmasq configuration, lease parsing |
internal/dns |
Unbound configuration, DNS query client for diagnostics |
internal/dns/blocklist |
DNS blocklist parsing, merging, installation |
internal/feed |
bounded HTTPS downloads shared by DNS blocklists and IP lists |
internal/firewall/iplist |
firewall IP lists: parsing, safety filtering, PF table updates |
internal/wireguard |
wg(8) files (site tunnels and VPN provider exits), key generation, status parsing |
internal/deploy |
transactions, generations, rollback, recovery, health checks |
internal/diagnostic |
status and health information |
internal/importer/opnsense |
OPNsense config.xml translation |
internal/platform |
command execution, safe file operations, FreeBSD output parsers |
internal/paths |
every path Pebble reads or owns |
internal/collect |
read-only collectors shared by the agent, web interface and pebble top, including per-device connections from PF's state table |
internal/health |
health checks with evidence and actions |
internal/journal, internal/logwatch |
the flight recorder's event journal and log parsing |
internal/monitor, internal/incident |
sampling, change detection, traffic history; evidence-based incident analysis |
internal/configedit |
comment-preserving structured edits of pebble.yaml |
internal/preflight |
checks shared by pebble apply and the agent (including lock-out protection) |
internal/agent |
the privileged management API on a Unix socket |
internal/web |
the unprivileged web interface |
internal/top |
the terminal dashboard |
internal/devfixture |
a simulated router for development; never used on a router |
internal/netx, internal/dnsname |
address, port and DNS name helpers |
Ownership#
Pebble writes only the files listed in internal/paths:
| Path | Purpose |
|---|---|
/etc/rc.conf |
one block between # BEGIN PEBBLE MANAGED BLOCK and # END PEBBLE MANAGED BLOCK; everything else is preserved byte for byte |
/usr/local/etc/pebble/active/ |
generated pf.conf, fallback ruleset, dnsmasq.conf, dhclient.conf, routes, WireGuard files |
/usr/local/etc/unbound/pebble/ |
generated unbound.conf and the blocklist |
/usr/local/etc/cron.d/pebble, pebble-rollback |
scheduled refreshes; the rollback watchdog while a deployment is pending |
/etc/resolv.conf |
only with a static WAN and local DNS enabled |
/var/db/pebble/ |
generations, snapshots, journal, blocklist cache, firewall IP list tables (iplists/) |
/etc/pf.conf is never written; pf_rules points PF at Pebble's file.
Administrator PF rules go in /usr/local/etc/pebble/pf.local.conf (anchor
pebble-local), which Pebble creates once and never modifies.
Managed rc.conf variables appear after any administrator assignments, so
they take effect (rc.conf is sourced as a shell script). pebble plan
warns about earlier assignments of the same variable and refuses to apply
if an administrator assignment follows the block.
Who starts what at boot#
| Function | Component |
|---|---|
| Pending deployment recovery | rc.d/pebble_recovery (before netif, pf, routing) |
| WAN and LAN addressing | base rc.d/netif, rc.d/dhclient from ifconfig_<dev> |
| WAN default route | dhclient (DHCP) or base rc.d/routing (defaultrouter) |
| PF | base rc.d/pf with pf_rules and the fallback ruleset |
| WireGuard interfaces | rc.d/pebble (after pf, before unbound) using wg(8) |
| Static routes | rc.d/pebble from active/routes |
| IPv4 forwarding | rc.d/pebble, only after PF is enabled (gateway_enable is set to NO so rc.d/routing cannot enable it earlier) |
| DHCP | rc.d/dnsmasq from the dnsmasq package |
| DNS | rc.d/unbound from the unbound package |
| Blocklist and IP list refresh, endpoint re-resolution | cron |
| Flight recorder and management API (optional) | rc.d/pebble_agent (root, supervised by daemon(8)) |
| Web interface (optional) | rc.d/pebble_web (user pebble, supervised by daemon(8)) |
None of these require the pebble binary except boot recovery (which does
nothing if no deployment is pending), the cron jobs and the optional agent
and web interface, which forwarding never depends on.
Determinism#
Generated files are byte-identical for identical input: every map is iterated in sorted order, lists are sorted where order has no meaning, and no timestamps are embedded. The cron minute for blocklist refreshes is derived from the host name so that many routers do not fetch feeds at the same moment, while staying deterministic. Generation manifests record the creation time; that is the only volatile metadata.
Transactions#
See recovery.md for the state machine, the rollback mechanisms and the boot-time recovery policy.