pebble.
Pebble / Docs / Internals

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.