pebble.
Pebble / Docs / Start

Pebble

Pebble turns a small FreeBSD machine into a router that is configured from one declarative YAML file. It is a configuration compiler and deployment manager: it writes native FreeBSD configuration (PF, Unbound, dnsmasq, WireGuard, rc.conf), validates it with FreeBSD's own tools, applies it as a transaction with automatic rollback, and then gets out of the way.

The operating system performs networking. Pebble configures and manages the operating system. Routing, firewalling, DHCP, DNS and WireGuard are ordinary FreeBSD services that keep working (and survive reboots) without the pebble binary. An optional agent adds a flight recorder, a terminal dashboard and a web interface; forwarding never depends on it.

Licence: AGPL-3.0-or-later.

Project status#

Pebble runs the author's home router (FreeBSD 15.1-RELEASE, amd64), and everything below marked in use has run there, on top of unit tests, fuzzing and a simulated router. It is still young software with one production deployment: keep a console within reach when you first deploy it.

Area Status
Configuration schema, validation, compilation Unit-tested and fuzzed
pebble bootstrap, installer, rc.d scripts In use on FreeBSD 15.1
Transactional apply, confirm, rollback, crash and boot recovery In use; state machine and crash injection also unit-tested
PF ruleset, NAT, port forwards In use; permitted/denied flows unit-tested against Pebble's PF model
DHCP (dnsmasq) and DNS (Unbound, DNSSEC) In use
DNS blocklists In use; local-zone vs RPZ measured on macOS
Firewall IP lists In use
WireGuard site-to-site In use
VPN provider exits, per-device routing and Internet blocking In use
Live per-device traffic In use
Web interface and privileged agent In use
Flight recorder, health checks, pebble explain outage, pebble top In use
Per-device DNS lookup history (dns.query_log, newest) Unit-tested against real Unbound 1.24 output and checked in the simulated router; not yet deployed on the router
OPNsense importer Tested against synthetic fixtures of three layouts and fuzzed; not yet tried on real exports
Memory, throughput, boot-time figures Not yet measured on the target; see docs/benchmarks for macOS figures

Changes made after a deployment reaches the router (the 2026-10-19 security audit's follow-ups among them) are verified there as they are rolled out; docs/roadmap.md lists what each release still needs checked. scripts/test-freebsd.sh is a complete integration suite (VNET jails, real DHCP, NAT, DNS, WireGuard, rollback, SIGKILL recovery and restart tests) for a disposable FreeBSD 15 VM.

What Pebble is#

What Pebble is not#

Supported platforms and hardware#

Installation#

The pebble binary is self-contained: the rc.d scripts and examples are built in. On a clean FreeBSD install, copy the binary over and run:

./pebble bootstrap                      # checks, pkg, unbound + dnsmasq, kldload pf/pflog/if_wg, installs itself

or, from the local console, go from a clean install to a running router in one step:

./pebble bootstrap --wan igb0 --lan igb1 --lan-address 192.168.10.1/24 \
    --hostname pebble --timezone Europe/London --apply
pebble confirm

bootstrap is idempotent, --dry-run shows what it would do, and --packages DIR installs the packages offline. It also creates Unbound's DNSSEC trust anchor (root.key) in its chroot and sets pf_enable=YES with sysrc. Without --apply it changes no interface or routing, loads no firewall ruleset and starts no service; those are enabled in rc.conf and started when a configuration is applied.

Alternatively, from a release tarball (built anywhere with scripts/build.sh freebsd, then copied over) or a source checkout:

sha256 pebble-*-freebsd-amd64.tar.gz; cat pebble-*-freebsd-amd64.tar.gz.sha256   # digests must match
tar xzf pebble-*-freebsd-amd64.tar.gz && cd pebble-*/
sh scripts/install.sh

From source (requires Go, see go.mod):

make build && sh scripts/install.sh

Offline installation from previously fetched packages (pkg fetch -d -o /media/pkgs unbound dnsmasq on a connected machine):

sh scripts/install.sh --offline /media/pkgs/All

The installer checks the release, architecture, disk space and privileges, installs packages, the binary, rc.d scripts, examples and documentation, and does not touch interfaces, routing, rc.conf or the firewall. See docs/installation.md.

Initial configuration#

Do the first deployment from the local console, not over SSH: it replaces the firewall and interface configuration.

pebble interfaces                       # identify the ports (unplug/replug a cable to be sure)
pebble init --wan igb0 --lan igb1 --lan-address 192.168.10.1/24 --hostname pebble --timezone Europe/London
pebble validate
pebble plan --diff
pebble apply
pebble confirm                          # within the rollback window (default 120 s)

Pebble never guesses interface assignments; init refuses to run without them. A complete two-interface example is in examples/minimal.yaml and every setting is documented in examples/annotated.yaml and docs/configuration.md.

Applying changes#

Edit /usr/local/etc/pebble/pebble.yaml, then:

pebble plan --diff       # what would change (nothing is modified)
pebble apply             # validate natively, snapshot, apply, health-check
pebble confirm           # keep it; otherwise it is rolled back automatically

Changes to networking, the firewall or WireGuard need confirmation; DNS, DHCP and blocklist-only changes commit after passing health checks. Use --confirm-always to require confirmation for everything, and --rollback-after 5m to change the window.

Verifying routing#

pebble status
pebble diagnose
pebble routes --lookup 1.1.1.1
pebble firewall explain
pebble firewall test --in lan --src 192.168.10.50 --dst 1.1.1.1 --proto tcp --port 443
pebble firewall status                  # per-rule counters

DNS blocking#

Add HTTPS feeds under dns.blocking.lists, apply, then fetch them:

pebble dns blocklist update
pebble dns blocklist status
pebble dns blocklist test ads.example.com
pebble dns test ads.example.com

Feeds refresh on the configured interval via cron. A failed download keeps the previous copy; a list that Unbound rejects is never installed. See docs/dns-filtering.md.

Blocking traffic with downloaded IP lists#

firewall:
  ip_lists:
    update_interval: 24h
    lists:
      - name: doh
        url: https://raw.githubusercontent.com/dibdot/DoH-IP-blocklists/master/doh-ipv4.txt
  rules:
    - {description: Block DNS-over-HTTPS bypass, action: block, interface: lan,
       protocol: tcp/udp, source: lan, destination: list:doh, ports: ["443"]}
pebble apply && pebble confirm
pebble firewall iplist update           # first download; cron refreshes it afterwards
pebble firewall iplist status
pebble firewall iplist test 1.1.1.1

Each list is a PF table loaded from a file; refreshes swap the table atomically without reloading the ruleset. See docs/networking.md.

Site-to-site WireGuard#

pebble vpn genkey wg0                   # writes /usr/local/etc/pebble/keys/wg0.key (0600), prints the public key
# add the interface and peer (examples/site-to-site.yaml), then:
pebble apply && pebble confirm
pebble vpn status
pebble vpn test site-b
pebble vpn show site-b --endpoint router.example.net:51820   # settings for the other side

Traffic between sites is routed (never NATed) and needs explicit firewall rules. See docs/wireguard.md.

Per-device Internet access and VPN exits#

Each device with a DHCP reservation can reach the Internet through the WAN (the default), through a VPN provider exit, or not at all:

dhcp:
  reservations:
    - {name: laptop, mac: 02:00:00:00:00:50, address: 192.168.10.50, egress: wg1}   # via the VPN exit wg1
    - {name: tablet, mac: 02:00:00:00:00:60, address: 192.168.10.60, egress: none}  # no Internet; LAN still works

A VPN exit is a WireGuard interface with role: egress built from the provider's configuration file (examples/vpn-egress.yaml). Nothing is routed into it except the assigned devices; if it is down they have no Internet access rather than falling back to the WAN. Both settings can be changed from the web interface's Devices page, which also shows each device's live traffic and connections. See docs/wireguard.md and docs/networking.md.

Monitoring and the web interface#

Both are optional. To record events and traffic history, and to watch the router from a terminal:

monitoring:
  enabled: true
pebble apply && pebble confirm
pebble top                      # live dashboard over SSH
pebble explain outage --last 1h # what happened, and how sure Pebble is

For the web interface, create an account (there is no default password), enable management.web and apply; see docs/web.md:

pebble web user add alice --role admin

Importing from OPNsense#

pebble import opnsense config.xml -o import --map wan=igb0,lan=igb1
less import/migration-report.md

The importer never applies anything and never copies secrets into ordinary files. Follow docs/opnsense-migration.md to migrate safely using spare hardware.

Backup and recovery#

pebble backup                           # /var/db/pebble/backups/pebble-<time>.tar.gz (keys excluded by default)
pebble restore backup.tar.gz            # restores pebble.yaml; apply separately
pebble rollback                         # undo a pending deployment, or redeploy the previous generation
pebble rollback --generation 3

If a deployment is never confirmed — because SSH dropped, the firewall locked you out, or the machine rebooted — the previous configuration is restored by a detached timer, by a cron check every minute, or at boot before networking starts. See docs/recovery.md.

Upgrading#

pebble update check                     # reports available updates; installs nothing
pkg upgrade                             # packages (unbound, dnsmasq)
sh scripts/install.sh --skip-packages   # from a newer Pebble release
pebble plan                             # check what the new version would generate

Uninstalling#

sh scripts/uninstall.sh --software          # remove the tool; the router keeps working
sh scripts/uninstall.sh --disable-services  # stop routing at next boot (console only)
sh scripts/uninstall.sh --config            # remove Pebble's configuration and state

Building and testing#

make build            # bin/pebble for this machine
make build-freebsd    # bin/pebble-freebsd-amd64 (static, reproducible)
make test             # unit tests: no root, no network, no FreeBSD
make lint             # go vet, gofmt, sh -n (and staticcheck if installed)
make fuzz
scripts/build.sh freebsd   # tested release tarball + checksum
PEBBLE_TEST_DESTRUCTIVE=yes make test-integration   # on a disposable FreeBSD VM

Known limitations#

Documentation#

New here? Start with the User Guide (also as PDF and LaTeX source).

Architecture · Configuration · Installation · Networking · DNS filtering · WireGuard · Recovery · Web interface · Flight recorder and pebble top · Security · OPNsense migration · Troubleshooting