The local control-plane API for a PrivacyBrick — a Raspberry Pi (DietPi or Raspberry Pi OS) plugged into your router that runs Unbound (with DNS-over-TLS), AdGuard Home, Tailscale, ntopng, and the NextDNS CLI.
It runs on the Pi itself. There is no cloud, no centralized server, and no account. The PrivacyBrick iOS app finds the Pi on your WiFi automatically (Bonjour/mDNS), pairs once with a 6-digit code, and talks to this API directly. If Tailscale is up, the same API is reachable securely from anywhere — still with no central server.
┌──────────┐ Bonjour discovery + HTTP (LAN) ┌─────────────────────┐
│ iPhone │ ───────────────────────────────────────▶ │ Raspberry Pi │
│ (app) │ or via Tailscale from anywhere │ privacybrick-api │
└──────────┘ │ ├─ unbound-control│
│ ├─ tailscale CLI │
│ ├─ nextdns CLI │
│ ├─ AdGuard REST │
│ ├─ ntopng REST │
│ └─ systemd/DietPi │
└─────────────────────┘
git clone https://github.com/MarwanNakhaleh/PrivacyBrickAPI.git
cd PrivacyBrickAPI
sudo bash deploy/install.shThe installer creates a venv in /opt/privacybrick, installs a systemd
service (privacybrick-api, port 8787), writes a config template to
/etc/privacybrick/.env, and prints a pairing code.
AdGuard credentials are wired automatically: deploy/provision.sh creates a
dedicated privacybrick service account in AdGuard Home and writes it to
/etc/privacybrick/.env. (Installing the API alone with install.sh? Add
your AdGuard admin login to .env yourself — or run provision.sh, which
converges safely on an existing setup. Add PRIVACYBRICK_NTOPNG_TOKEN too if
you use one, then sudo systemctl restart privacybrick-api.)
To pair another phone later: privacybrick-pair
| App-facing name | Underlying tool | Mechanism |
|---|---|---|
| Ad Blocking | AdGuard Home | local REST API (/control/...) |
| Private DNS | Unbound | unbound-control |
| Encrypted DNS | Unbound DoT forwarding | systemd unit status |
| Remote Access | Tailscale | tailscale CLI (--json) |
| Cloud Filtering | NextDNS | nextdns CLI |
| Network Monitor | ntopng | local REST API (/lua/rest/v2/...) |
| Device | DietPi / OS | systemctl, vcgencmd, free, df, … |
All subprocess calls go through an allowlist (privacybrick_api/runner.py) —
the API can only run the specific binaries above, argv-style with no shell.
GET /ping— unauthenticated identity check (used during discovery)POST /pair{code, client_name}→{token}— exchange pairing code for a bearer tokenGET /overview— one call for the app's home screen: per-service health + overall "protected"GET /identity— device name/version + LAN and Tailscale addresses (how the app learns its remote address)GET|POST /adguard/{status,stats,querylog,protection}GET|POST /adguard/blocklists,POST /adguard/blocklists/remove— manage blocklist subscriptionsPOST /adguard/rules{domain, action: allow|deny}— allow/block a single domainGET|POST /unbound/{status,stats,flush-cache,restart}GET|POST /doh/{status,restart}GET|POST /tailscale/{status,up,down}GET|POST /nextdns/{status,config,activate,deactivate,restart}GET /ntopng/{status,hosts,interface-stats}GET|POST /system/{info,reboot}
Everything except /ping and /pair requires Authorization: Bearer <token>.
Interactive docs at http://<pi>:8787/docs while developing.
- Pairing: single-use, 5-minute, 6-digit codes generated on the device
(
privacybrick-pair). Successful pairing issues a long-lived token stored in the phone's Keychain. Tokens live in/etc/privacybrick/state.json(0600). - No inbound cloud dependency: the API binds to the LAN; remote access is only via your own tailnet.
- Command allowlist: no shell execution, fixed binary set, hard timeouts.
- Secrets for AdGuard/ntopng stay on the Pi; the phone only ever holds its bearer token.
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
PRIVACYBRICK_STATE_DIR=./tmp-state privacybrick-api # http://localhost:8787/docs
pytest # smoke tests, no Pi neededService wrappers degrade gracefully when a tool isn't installed (reported as
installed: false in /overview), so the API runs fine on a laptop for UI
development.