Design principles
These are the load-bearing design decisions. If you understand them, the rest of the daemon follows.
Fail-closed is the default
Section titled “Fail-closed is the default”A privacy-first router that quietly leaks when a tunnel goes down is worse than no router. So:
fail_policy: fail-closedis a first-class option on every routing policy, route profile, and bridge. The API default isfail-open(lab and balancing use-cases would otherwise hard-break on day one) — but every page about sensitive workloads, every recipe, and the threat-model page push you to setfail-closeddeliberately.- The kill-switch lives at
preroutingpriority-1, ahead of any routing decision the kernel might make. - It is reference-counted so overlapping policies can engage and disengage without leak windows.
- It survives daemon restart — counters are re-derived from policy state, never trusted across crashes.
- Validation rejects typos: anything other than
fail-open/fail-closedreturns HTTP 400 at the API boundary, so a silent degradation can’t slip in through a “fail-clos” misspelling.
The trade-off: brief outages during tunnel cycle. The alternative — silent leaks — we consider non-negotiable.
Per-policy, not global
Section titled “Per-policy, not global”Every kill-switch, every splitting counter, every privacy class, every routing table is per-policy. Two policies pooling over the same three tunnels don’t share state. One policy failing doesn’t blackhole the rest of the LAN.
Why: a global kill-switch protects nothing more than a per-policy one, but it does take the whole household offline when a single sensitive policy’s tunnel fails. The cost of per-policy state is some kernel memory; the benefit is that a finance VPN dying doesn’t kill the kids’ YouTube.
The reconciler is the truth
Section titled “The reconciler is the truth”The kernel state is derived from the daemon’s SQLite store (policies, devices, privacy classes, tunnels, etc.) — not the other way around. The reconciler scans nftables, ip rule, routing tables, redsocks processes, and tunnel interfaces on every tick (default 60 s) and re-applies the desired state if anything drifted.
Consequences:
- A sibling script that flushes a chain loses on the next tick.
- A partial restart that leaves orphan rules gets cleaned up automatically.
- You don’t need to “save” or “commit” config changes — they go into SQLite and the reconciler picks them up.
- Out-of-band kernel changes always lose. Always.
This is why The Gateway and a sibling firewall manager (Docker iptables, ufw, firewalld) don’t coexist gracefully. The daemon owns its kernel resources.
Minute-cadence, not real-time
Section titled “Minute-cadence, not real-time”The reconciler runs at minute granularity by default. Not microsecond, not second.
Why: the cost of running the reconciler more often than every few seconds rapidly exceeds its benefit. The kill-switch is enforced in the kernel; you don’t need the userspace reconciler to plug a leak in real time. The reconciler exists to recover from drift, and drift is rare. A minute is enough.
You can lower the interval to 5 s if you have a specific reason — but understand it doesn’t make the kill-switch more secure; it only catches drift faster.
One control plane, one API
Section titled “One control plane, one API”The web UI, gwctl, and the REST API all talk to the same daemon over the same endpoints. There is no parallel “admin-only” channel, no private RPC for the UI.
Consequences:
- Anything you can click, you can script.
- Anything in a screenshot, you can
curl. - Permissions are the same everywhere — there’s no UI that bypasses RBAC.
The cost: the REST API has to be expressive enough for everything the UI needs. We accept that.
SQLite, not a distributed database
Section titled “SQLite, not a distributed database”State lives in a single SQLite file. WAL-mode. Fsync on commit. No replicas.
Why: a programmable network gateway is single-host software. The benefit of a distributed database (HA, geographic redundancy) doesn’t apply to a thing that runs on one box and owns one box’s nftables. The cost (operational complexity, consensus rounds, eventual-consistency surprises) is real.
Federation gives you multi-node configuration sharing without sharing the database. That’s a deliberate choice, not a limitation we’d want to lift.
Local AI, not cloud
Section titled “Local AI, not cloud”The AI policy assistant (/api/v1/ai/policy) runs against a local Ollama by default. You can point it at a cloud provider via kv_settings.ai_ollama_url if you want, but it’s not the default.
Why: the assistant sees your policy structure, tunnel names, and device IDs in its prompt context. Sending that to a third party would defeat the privacy posture the rest of the daemon is built around. Latency in censored / high-latency networks is also a concern — a local model answers in seconds even when the WAN is down.
(The other /api/v1/ai/ endpoint, /ai/validate, is a rule-based reference checker — no LLM involved, no privacy concern, runs entirely in-daemon.)
Boring code paths, fancy edge paths
Section titled “Boring code paths, fancy edge paths”The packet’s path through the daemon is almost entirely in-kernel: nftables numgen, ip rule, tc HTB. The userspace code that decides what to install is opinionated and explicit; the dataplane that enforces those decisions is the standard Linux kernel.
We picked this because:
- The kernel is fast, well-tested, and not going to surprise us.
- Userspace bugs can’t open a leak as long as the kernel rules are in place.
- We can rely on tools that have been hardened by every Linux distribution for two decades.
No half-finished features
Section titled “No half-finished features”If a feature is in the daemon, it works end-to-end: schema, API, UI, audit, alert categories, backup round-trip, federation behaviour. We don’t ship half-features behind feature flags — that creates uncertainty about what the daemon actually does.
The cost is fewer features per release. The benefit is that operators can read the docs and trust them.
Make the operator the smart one
Section titled “Make the operator the smart one”The daemon doesn’t try to be clever. It exposes the dials and tells you what each does. The AI policy assistant suggests, you decide. The drift reconciler logs every diff so you can see what it fixed. The audit log records every change with full context. The leak matrix shows the exact posture in a grid you can read.
This is a tool for people who already know what they want. Hiding the dials would make it less useful, not more.
Related
Section titled “Related”- Threat model — what these principles defend against.
- Architecture — how they’re realised in code.
- Comparisons — what other systems choose differently.