Adds the ability to route a specific peer's internet traffic through a
connected cell acting as an exit relay.
Cell A side:
- PUT /api/peers/<peer>/route-via {"via_cell": "cellB"} sets route_via
- Updates WG AllowedIPs to include 0.0.0.0/0 for the exit cell peer
- Adds ip rule + ip route in policy table inside cell-wireguard so the
specific peer's traffic egresses via cellB's WG IP
- Sets exit_relay_active on the cell link and pushes use_as_exit_relay=True
to cellB via peer-sync
Cell B side:
- Receives use_as_exit_relay in the peer-sync payload
- Calls apply_cell_rules(..., exit_relay=True) to add FORWARD -o eth0 ACCEPT
- Stores remote_exit_relay_active flag for startup recovery
Startup recovery:
- apply_all_cell_rules passes exit_relay=remote_exit_relay_active (cellB)
- _apply_startup_enforcement reapplies ip rule for each peer with route_via (cellA)
since policy routing rules don't survive container restart
peer_registry gets route_via field with lazy migration.
22 new tests across test_cell_link_manager, test_peer_registry, test_peer_route_via.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Personal Internet Cell (PIC)
PIC is a self-hosted digital infrastructure platform. It manages DNS, DHCP, NTP, WireGuard VPN, email, calendar/contacts (CalDAV), file storage (WebDAV), a reverse proxy, and a certificate authority — all controlled from a single REST API and React web UI. No manual config file editing is required for normal operations.
Architecture
Browser
└── React SPA (cell-webui :8081)
└── Flask REST API (cell-api :3000, bound to 127.0.0.1)
└── Docker SDK / config files
├── cell-caddy :80/:443 reverse proxy
├── cell-dns :53 CoreDNS
├── cell-dhcp :67/udp dnsmasq
├── cell-ntp :123/udp chrony
├── cell-wireguard :51820/udp WireGuard VPN
├── cell-mail :25/:587/:993 Postfix + Dovecot
├── cell-radicale 127.0.0.1:5232 CalDAV/CardDAV
├── cell-webdav 127.0.0.1:8080 WebDAV
├── cell-rainloop :8888 webmail (RainLoop)
├── cell-filegator :8082 file manager UI
└── cell-webui :8081 React UI (Nginx)
All containers run on a custom Docker bridge network (cell-network, default 172.20.0.0/16). Static IPs per container are set in docker-compose.yml and overridden via .env.
The Flask API (api/app.py, ~2800 lines) contains all REST endpoints, runs a background health-monitoring thread, and manages the entire lifecycle of generated config artefacts: Caddyfile, Corefile, wg0.conf, and cell_config.json (the single source of truth at config/api/cell_config.json).
The React frontend (webui/) is built with Vite + Tailwind CSS. All API calls go through src/services/api.js (Axios). Pages: Dashboard, Peers, Network Services, WireGuard, Email, Calendar, Files, Routing, Vault, Containers, Cell Network, Logs, Settings.
Requirements
- Linux host with the WireGuard kernel module loaded
- Docker Engine and Docker Compose (v2 plugin or v1 standalone)
- Python 3.10+ (for
make setupand local dev only; not needed at runtime) - 2 GB+ RAM, 10 GB+ disk
- Ports available: 53, 67/udp, 80, 443, 51820/udp, 25, 587, 993
Quick Start
See QUICKSTART.md for step-by-step setup.
Configuration
Runtime configuration is controlled by .env in the project root. Copy .env.example to .env before first run.
| Variable | Default | Description |
|---|---|---|
CELL_NETWORK |
172.20.0.0/16 |
Docker bridge subnet for all containers |
CADDY_IP through FILEGATOR_IP |
172.20.0.2–.13 |
Static IP for each container |
DNS_PORT |
53 |
DNS (UDP+TCP) |
DHCP_PORT |
67 |
DHCP (UDP) |
NTP_PORT |
123 |
NTP (UDP) |
WG_PORT |
51820 |
WireGuard listen port (UDP) |
API_PORT |
3000 |
Flask API (bound to 127.0.0.1) |
WEBUI_PORT |
8081 |
React UI |
MAIL_SMTP_PORT |
25 |
SMTP |
MAIL_SUBMISSION_PORT |
587 |
SMTP submission |
MAIL_IMAP_PORT |
993 |
IMAP |
RADICALE_PORT |
5232 |
CalDAV (bound to 127.0.0.1) |
WEBDAV_PORT |
8080 |
WebDAV (bound to 127.0.0.1) |
RAINLOOP_PORT |
8888 |
Webmail |
FILEGATOR_PORT |
8082 |
File manager UI |
WEBDAV_USER |
admin |
WebDAV basic-auth username |
WEBDAV_PASS |
(required) | WebDAV basic-auth password — must be set before make start |
FLASK_DEBUG |
(unset) | Set to 1 to enable Flask debug mode; do not use in production |
PUID / PGID |
current user | UID/GID passed to the WireGuard container |
Cell identity (cell name, domain, VPN IP range) is configured via make setup or the Settings → Identity page in the UI after startup. The VPN IP range must be an RFC-1918 CIDR (10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16); the API and UI both enforce this.
Security Notes
Ports exposed to the network:
80/443— Caddy (HTTP/HTTPS reverse proxy)51820/udp— WireGuard25/587/993— Mail (SMTP, submission, IMAP)53— DNS (UDP + TCP)67/udp— DHCP8081— Web UI8888— Webmail (RainLoop)8082— File manager (Filegator)
Ports bound to 127.0.0.1 only (not directly reachable from the network):
3000— Flask API5232— Radicale (CalDAV)8080— WebDAV
The API has no authentication layer. It relies on is_local_request() to restrict sensitive endpoints (containers, vault) to requests originating from loopback or the cell's Docker network. The Docker socket is mounted into cell-api; treat access to port 3000 as equivalent to root access on the host.
For internet-facing deployments, place the host behind a firewall or VPN and restrict access to the API and UI ports.
Development
# Start the full stack (builds api and webui images)
make start
# Rebuild a single image after code changes
make build-api
make build-webui
# Run Flask API locally without Docker (port 3000)
pip install -r api/requirements.txt
python api/app.py
# Run React UI dev server locally (port 5173, proxies /api to :3000)
cd webui && npm install && npm run dev
# Follow all container logs
make logs
# Follow logs for one service (e.g. api, dns, caddy, wireguard, mail)
make logs-api
# Open a shell inside a container
make shell-api
Testing
make test # run the full pytest suite
make test-coverage # run with coverage; HTML report in htmlcov/
Tests live in tests/ (34 files, 642 test functions). Coverage includes:
- All service managers (network, WireGuard, email, calendar, file, routing, vault, container)
- API endpoint tests for each service area
- Config manager (CRUD, validation, backup/restore)
- IP utilities and Caddyfile generation
- Peer registry and WireGuard peer lifecycle
- Service bus pub/sub
- Firewall manager
- Pending-restart logic
Integration tests (tests/integration/) require a running PIC stack:
make test-integration # full suite (creates peers)
make test-integration-readonly # read-only checks, safe to run anytime
Management Commands
make setup # generate WireGuard keys, write configs, create data dirs
make start # docker compose up -d --build
make stop # docker compose down
make restart # docker compose restart
make status # container status + API health check
make logs # follow all service logs
make logs-<svc> # follow logs for one service
make shell-<svc> # shell inside a container
make update # git pull + rebuild + restart
make reinstall # full wipe of config/ and data/, then setup + start
make uninstall # stop containers; prompts whether to also delete config/ and data/
make backup # tar config/ + data/ into backups/
make restore # list available backups
make list-peers # show WireGuard peers via API
make show-routes # wg show inside the wireguard container
make add-peer PEER_NAME=foo PEER_IP=10.0.0.5 PEER_KEY=<pubkey>
License
MIT — see LICENSE.