Edge gateway that joins a WireGuard controller, runs Traefik on the host network, and exposes an agent API for dynamically creating:
- HTTPS host-based routes via Traefik dynamic config files
- TCP port forwards via iptables DNAT across the WireGuard network
Designed to be the public entrypoint for your private WG mesh.
WireGuard client container that:
- generates/persists WG keypair
- joins controller via
/join - writes
/etc/wireguard/wg0.conf - brings up
wg0on the host network (network_mode: host) - stays alive (
sleep infinity) - provides healthcheck:
wg show wg0
Traefik reverse proxy:
- runs on host network
- listens on
:80and:443 - watches
/etc/traefik/dynamicfor routes (file provider)
FastAPI service that:
- writes Traefik dynamic YAML files (
/http/routes) - manages TCP DNAT forwards via iptables (
/tcp/forwards) - persists state in
/data/state.json - restores iptables rules on startup
Internet
│
(80/443 + TCP ports)
│
┌───────────────┐
│ Traefik │ (host network)
└───────┬────────┘
│ dynamic config (*.yml)
┌───────▼────────┐
│ Proxy Agent │ (FastAPI)
│ - http routes │
│ - tcp forwards │
└───────┬────────┘
│ DNAT / WG forwarding
┌───────▼────────┐
│ WireGuard wg0 │ (edge-net)
└───────┬────────┘
│ WG mesh (10.50.0.0/24)
┌────────▼─────────┐
│ nodes / services │
└───────────────────┘
- Auto-join WG controller on startup (idempotent)
- Traefik dynamic routing via file provider
- Create/remove HTTPS host routes via API
- Create/remove TCP port forwards via API
- Persistent state for routes/forwards
- Automatic restore of iptables rules after restart
- Runs fully with Docker Compose +
network_mode: host
Host:
- Linux with kernel WireGuard support
- iptables available (iptables-nft or legacy both ok if consistent)
- Docker / docker compose
cap_add: NET_ADMINandSYS_MODULEneeded for wg/iptables in containers/lib/modulesmounted read-only for wg kernel module usage
Minimal required:
CONTROLLER_URL=http://<controller_public_ip>:9000
JOIN_TOKEN=<join_token>
NODE_ID=edge-gateway-1
AGENT_TOKEN=<optional_token>
# Traefik DNS / ACME values are up to your setup
EDGE_AGENT_PORT=8081docker compose up -d --builddocker ps
docker logs edge-net
docker logs edge-agent-
./wireguard:/etc/wireguardStores generatedwg0.conf(and related files if you place them there) -
./agent-data:/dataStores:join.jsonfrom controllerstate.jsonfor proxy-agent (routes + forwards)
-
./traefik/dynamic:/etc/traefik/dynamicDynamic Traefik config files (*.yml) generated by the agent -
./traefik/acme:/acmeACME storage (if you use cert resolvers that need it)
| Variable | Description |
|---|---|
CONTROLLER_URL |
Controller API base URL |
JOIN_TOKEN |
Join token (sent as X-Join-Token) |
NODE_ID |
Node id used in controller peers |
WG_IFACE |
WG interface name (default: wg0) |
DATA_DIR |
Persistent dir (default: /data) |
| Variable | Description |
|---|---|
AGENT_TOKEN |
Optional auth token for agent API |
TRAEFIK_DYNAMIC_DIR |
Directory for Traefik dynamic YAML |
STATE_FILE |
Persistent state file |
WG_IFACE |
WG interface used for masquerade (wg0) |
TRAEFIK_ENTRYPOINT |
EntryPoint name (default: websecure) |
TRAEFIK_CERTRESOLVER |
Cert resolver name |
Base URL (host network):
http://127.0.0.1:8081 (or your host IP)
Auth header (if enabled):
X-Agent-Token: <AGENT_TOKEN>
NOTE: In your code
require_token()is currently “pass”-ed (auth disabled). If you want auth: uncomment the HTTPException lines.
GET /healthPOST /http/routes
Content-Type: application/jsonBody:
{
"route_id": "vm-123",
"hostname": "vm-123.service.com",
"target_url": "http://10.50.0.12:8080",
"entrypoint": "websecure",
"certresolver": "dnsresolver"
}What it does:
- writes
/etc/traefik/dynamic/<route_id>.yml - Traefik picks it up automatically
GET /http/routesDELETE /http/routes/{route_id}POST /tcp/forwards
Content-Type: application/jsonBody:
{
"forward_id": "vm-123-ssh",
"public_port": 22015,
"target_ip": "10.50.0.12",
"target_port": 32215,
"proto": "tcp"
}What it does:
nat/PREROUTINGDNAT:public_port → target_ip:target_portfilter/FORWARDACCEPT rule- ensures
net.ipv4.ip_forward=1 - ensures
MASQUERADE -o wg0exists (critical for reply path) - persists into
/data/state.json
GET /tcp/forwardsDELETE /tcp/forwards/{forward_id}Also attempts to cleanup wg masquerade if no forwards remain.
On agent startup:
- reads
/data/state.json - re-applies TCP forwards (DNAT + FORWARD + wg0 MASQUERADE)
- re-adds established/related conntrack accept rule
HTTP routes are restored implicitly because YAML files live in TRAEFIK_DYNAMIC_DIR.
network_mode: hostmeans containers share host network namespace.- TCP forwarding + routing are powerful: lock down access properly.
- If you expose agent API beyond localhost, enable token auth and firewall it.
- Consider restricting inbound ports and using allowlists.
Make sure wg masquerade exists on the edge host:
iptables -t nat -S POSTROUTING | grep wg0You should see something like:
-A POSTROUTING -o wg0 -j MASQUERADE
Check if YAML file is written:
ls -la ./traefik/dynamic
docker logs edge-traefikCheck state:
cat ./agent-data/state.json
docker logs edge-agent