Root README now lets you choose between the two paths, with requirements, the three ways to route Traefik to WireGuard peers (including the bridge-network recipe verified on a live Traefik v3.7 + wg-easy v15), box connection, troubleshooting and security notes. The starter-stack README lists server, firewall and DNS requirements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
206 lines
8.8 KiB
Markdown
206 lines
8.8 KiB
Markdown
# furtka-gateway
|
|
|
|
Lets a Furtka box — typically at home behind NAT or CGNAT, with no public IP —
|
|
publish individual apps on the internet under a real domain, with automatic
|
|
HTTPS.
|
|
|
|
```
|
|
internet ──443──▶ Traefik ──WireGuard tunnel──▶ Furtka box (behind NAT) ──▶ app
|
|
▲
|
|
Furtka box ──51820/udp──▶ wg-easy (box dials out; no port forwarding at home)
|
|
```
|
|
|
|
A box connects as a WireGuard peer of wg-easy. Traefik then reverse-proxies
|
|
HTTPS traffic through that tunnel to the apps the box owner has explicitly
|
|
published. Only apps whose Furtka manifest declares `internet.viable: true` can
|
|
be published at all, and each one is a separate opt-in on the box — the
|
|
gateway never exposes anything by itself.
|
|
|
|
This repo's own code is the **control-plane** (`control_plane/`, one small
|
|
stdlib-only Docker image). It creates WireGuard peers in wg-easy when a box
|
|
registers, and writes Traefik file-provider routes when a box publishes an app.
|
|
|
|
## Choose your setup
|
|
|
|
| | [Option A: New server](#option-a-new-server) | [Option B: Existing Traefik + wg-easy](#option-b-existing-traefik--wg-easy) |
|
|
|---|---|---|
|
|
| For | An empty VPS | A server that already runs Traefik and wg-easy |
|
|
| Runs | wg-easy + Traefik + control-plane | control-plane only |
|
|
| Setup | `examples/starter-stack/deploy.sh`, 4 prompts | `ops/deploy.sh`, plus one routing step you do yourself |
|
|
|
|
## Option A: New server
|
|
|
|
Full guide, including server and DNS requirements:
|
|
**[examples/starter-stack/README.md](examples/starter-stack/README.md)**.
|
|
|
|
In short: a Linux VPS with Docker, ports `80/tcp`, `443/tcp`, `51820/udp`
|
|
open, and a wildcard DNS record `*.example.com` pointing at it. Then:
|
|
|
|
```bash
|
|
git clone https://forgejo.sourcegate.online/robert/furtka-gateway.git
|
|
cd furtka-gateway/examples/starter-stack && ./deploy.sh
|
|
```
|
|
|
|
## Option B: Existing Traefik + wg-easy
|
|
|
|
### Requirements
|
|
|
|
- **Traefik v3** with a **file provider directory** (`providers.file.directory`).
|
|
The control-plane writes one file per published app into it. If your Traefik
|
|
uses `providers.file.filename` today, switch it to a directory and move your
|
|
existing file into it — Traefik allows only one file provider.
|
|
- **wg-easy 14 or 15.** Their admin APIs differ; you tell the deploy script
|
|
which one you run. **On v15, the control-plane sets wg-easy's default
|
|
"Allowed IPs" for new clients to your peer subnet (split tunnel)** on every
|
|
start. Existing clients aren't touched, but new non-Furtka clients (phones,
|
|
laptops) also get split tunnel unless you edit them.
|
|
- **Docker Compose v2.24+** on the server.
|
|
- **A route from Traefik to wg-easy's peer subnet** (default `10.8.0.0/24`) —
|
|
see the next section. This is the one step you do yourself.
|
|
- **DNS:** an A record for the gateway API hostname (e.g. `gw.example.com`)
|
|
pointing at your Traefik, plus one per published app — or simply a wildcard
|
|
`*.example.com`. Any registrar works; certificates come from your Traefik's
|
|
existing resolver.
|
|
- **No new ports.** Traefik's 443 and wg-easy's 51820/udp are already open.
|
|
|
|
### Routing: Traefik must reach the WireGuard peers
|
|
|
|
Published routes point Traefik at a box's tunnel address (`10.8.0.x`). That
|
|
address only exists inside wg-easy's network namespace, so a standard setup
|
|
(wg-easy on a Docker bridge network) can't reach it. Pick whichever fits:
|
|
|
|
**1. wg-easy on a bridge network (most common)** — route the subnet through
|
|
wg-easy and NAT it. Verified on a live Traefik v3.7 + wg-easy v15 setup.
|
|
|
|
a. Pin wg-easy's address on Traefik's network (in wg-easy's compose file),
|
|
so the route below stays valid:
|
|
|
|
```yaml
|
|
networks:
|
|
traefik:
|
|
ipv4_address: 172.19.0.5 # any free address in that network's subnet
|
|
```
|
|
|
|
b. Route the peer subnet from the host to wg-easy, permanently
|
|
(`/etc/systemd/system/furtka-gateway-route.service`):
|
|
|
|
```ini
|
|
[Unit]
|
|
Description=Route WireGuard peer subnet via wg-easy (furtka-gateway)
|
|
Requires=docker.service
|
|
After=docker.service
|
|
PartOf=docker.service
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
RemainAfterExit=yes
|
|
ExecStart=/bin/sh -c 'for i in $(seq 90); do ip route replace 10.8.0.0/24 via 172.19.0.5 && exit 0; sleep 2; done; exit 1'
|
|
ExecStop=/sbin/ip route del 10.8.0.0/24 via 172.19.0.5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
```bash
|
|
sudo systemctl daemon-reload && sudo systemctl enable --now furtka-gateway-route
|
|
```
|
|
|
|
c. Make replies come back through the tunnel: in wg-easy's admin UI
|
|
(**Admin → Hooks**), append to **PostUp**
|
|
|
|
```
|
|
iptables -t nat -A POSTROUTING -s 172.19.0.0/16 -o wg0 -j MASQUERADE;
|
|
```
|
|
|
|
and to **PostDown** the same line with `-D` instead of `-A` (use your
|
|
Traefik network's subnet). Then restart wg-easy once.
|
|
|
|
**2. Traefik in wg-easy's network namespace** — no routes or NAT at all; this
|
|
is what [Option A](examples/starter-stack/) does. Only practical if you can
|
|
restructure both containers.
|
|
|
|
**3. wg-easy with `network_mode: host`** — wg0 then lives on the host. Needs
|
|
`net.ipv4.ip_forward=1` on the host, and a firewall rule keeping wg-easy's
|
|
admin port (51821) off the public interface, because host networking publishes
|
|
it.
|
|
|
|
**Check it**, once a box is connected:
|
|
|
|
```bash
|
|
docker exec <your-traefik> ping -c1 10.8.0.<box>
|
|
```
|
|
|
|
### Deploy
|
|
|
|
```bash
|
|
git clone https://forgejo.sourcegate.online/robert/furtka-gateway.git
|
|
cd furtka-gateway
|
|
ops/deploy.sh --render-only # optional: inspect .env and rendered/control-plane.yml first
|
|
ops/deploy.sh
|
|
```
|
|
|
|
It asks for your wg-easy URL, version and login, your Traefik's dynamic
|
|
directory, cert resolver and docker network, and the API hostname. It then
|
|
builds and starts the control-plane on your Traefik's network, installs its
|
|
route into the dynamic directory, and checks `https://<api-host>/healthz`.
|
|
|
|
Finding the answers:
|
|
|
|
| Prompt | Where to look |
|
|
|---|---|
|
|
| wg-easy URL | `http://<wg-easy container name>:51821` if it's on your Traefik's network |
|
|
| Version | The tag of your wg-easy image (`docker ps`) |
|
|
| Dynamic directory | The **host** path mounted at your Traefik's `providers.file.directory` |
|
|
| Cert resolver | `certificatesResolvers.<name>` in your Traefik's static config |
|
|
| Docker network | `docker inspect <traefik> --format '{{json .NetworkSettings.Networks}}'` |
|
|
|
|
Update later with `git pull && ops/deploy.sh --non-interactive`. The image is
|
|
always rebuilt from your checkout.
|
|
|
|
## Connect a box
|
|
|
|
Same for both options. On the Furtka box: **Settings → Gateway**, enter the
|
|
gateway URL (`https://gw.example.com`), the registration token
|
|
(`GATEWAY_BOX_TOKEN` in the server's `.env`) and any box name. Then on the Apps
|
|
page, open an app's **⋯** menu → **Expose to Internet**, and give it a hostname
|
|
covered by your DNS.
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Cause / fix |
|
|
|---|---|
|
|
| Box: `Unit wg-quick@furtka-gw.service does not exist` | Box image lacks `wireguard-tools`. Install it (`pacman -S wireguard-tools`) or reinstall from a newer ISO, then click Connect again — the existing registration is reused. |
|
|
| Box says connected, but wg-easy shows no handshake | Older box builds don't write wg-easy v15's preshared key into the tunnel config. Update the box. |
|
|
| App URL gives 502 / timeout, but the box's tunnel is up | Traefik can't reach `10.8.0.x` — see [Routing](#routing-traefik-must-reach-the-wireguard-peers). |
|
|
| No "Expose to Internet" in an app's menu | That app's manifest lacks `internet.viable: true`. |
|
|
| `deploy.sh`: control-plane not healthy | `docker compose logs control-plane` — usually a wrong wg-easy URL or password. |
|
|
| `https://<api-host>/healthz` fails after deploy | DNS not pointing at Traefik yet, certificate still being issued, or Traefik isn't watching the dynamic directory. |
|
|
|
|
## Security notes
|
|
|
|
- A box's registration response contains its WireGuard private key, so the API
|
|
is only exposed through Traefik over HTTPS. The direct port is bound to
|
|
`127.0.0.1:8090`.
|
|
- `GATEWAY_BOX_TOKEN` lets anyone register a box. Treat it like a password;
|
|
rotate it by deleting it from `.env` and re-running the deploy script.
|
|
- Keep wg-easy's admin UI (port 51821) off the public internet.
|
|
|
|
## Docker Swarm (Option B only)
|
|
|
|
- `build:` doesn't work under Swarm. It uses the `image:` CI publishes on every
|
|
push to `master` (`latest` plus the short commit sha); set
|
|
`GATEWAY_CONTROL_PLANE_IMAGE` in `.env` to pin one.
|
|
- `docker stack deploy` doesn't read `.env` — `set -a; source .env; set +a` first.
|
|
- Attach the service to your Traefik's network in the stack file's own
|
|
`networks:` section (the generated `docker-compose.override.yaml` isn't used).
|
|
- Option A's stack can't run under Swarm (`network_mode: service:`).
|
|
|
|
## Development
|
|
|
|
```bash
|
|
python -m pytest # control-plane tests
|
|
ruff check . && ruff format --check .
|
|
```
|
|
|
|
`GATEWAY_MODE` is a tenancy switch: `single` (one operator — everything above)
|
|
or `shared` (multi-tenant; same code, not covered here).
|