Two gaps found while walking through a real single-tenant deployment scenario, both fixed and verified against the actual running stack (not just unit tests): - wg-easy moves to network_mode: host so Traefik can actually route to WireGuard peer addresses (wg-quick's route only existed inside wg-easy's own isolated network namespace before). This forced host-level sysctls (ops/host-sysctls.sh — Docker/runc rejects namespaced sysctls under host networking) and a host firewall rule (ops/firewall.sh) to keep wg-easy's now-unisolated admin API off the public interface. - control-plane gets a real Traefik route (traefik/dynamic/control-plane.yml.example) instead of being reachable only at 127.0.0.1:8090 — box registration carries a WireGuard private key and needs TLS, not bare loopback HTTP. Along the way, running the actual pinned wg-easy image (ghcr.io/wg-easy/wg-easy:14) surfaced that it's a different, older, Express-based codebase than what wg-easy's current docs/master branch describe: auth is a plain Authorization header per request (no session cookie), routes live under /api/wireguard/client, and its default WG_ALLOWED_IPS is full-tunnel (0.0.0.0/0) rather than the split-tunnel this design assumed. wgeasy.py and docker-compose.yaml are corrected accordingly, verified end-to-end against the real container. Also fixed: Docker Compose silently truncates a bcrypt hash's `$` characters when read from .env — documented the required $$ escaping.
59 lines
1.7 KiB
Python
59 lines
1.7 KiB
Python
import argparse
|
|
import os
|
|
from http.server import ThreadingHTTPServer
|
|
|
|
from control_plane import accounts
|
|
from control_plane.api import Context, Handler
|
|
from control_plane.db import Database
|
|
from control_plane.wgeasy import WgEasyClient
|
|
|
|
DEFAULT_BOX_LIMIT = 20
|
|
DEFAULT_ROUTE_LIMIT_PER_BOX = 20
|
|
|
|
|
|
def build_context(db: Database | None = None) -> Context:
|
|
mode = os.environ.get("GATEWAY_MODE", "single")
|
|
db = db or Database()
|
|
|
|
wgeasy = WgEasyClient(
|
|
base_url=os.environ.get("WG_EASY_URL", "http://host.docker.internal:51821"),
|
|
admin_password=os.environ.get("WG_EASY_ADMIN_PASSWORD", ""),
|
|
)
|
|
|
|
single_account_id = None
|
|
if mode == "single":
|
|
single_account_id = accounts.ensure_single_tenant_account(
|
|
db, DEFAULT_BOX_LIMIT, DEFAULT_ROUTE_LIMIT_PER_BOX
|
|
)
|
|
|
|
# Shared mode uses one wildcard defaultGeneratedCert for every router
|
|
# (see traefik.shared.yml) rather than a per-router certResolver.
|
|
cert_resolver = "le" if mode == "single" else None
|
|
|
|
return Context(
|
|
db=db,
|
|
wgeasy=wgeasy,
|
|
mode=mode,
|
|
cert_resolver=cert_resolver,
|
|
single_account_id=single_account_id,
|
|
box_limit=DEFAULT_BOX_LIMIT,
|
|
route_limit_per_box=DEFAULT_ROUTE_LIMIT_PER_BOX,
|
|
)
|
|
|
|
|
|
def serve(host: str = "0.0.0.0", port: int = 8090) -> None:
|
|
server = ThreadingHTTPServer((host, port), Handler)
|
|
server.ctx = build_context()
|
|
server.serve_forever()
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="furtka-gateway control plane")
|
|
parser.add_argument("--host", default="0.0.0.0")
|
|
parser.add_argument("--port", type=int, default=8090)
|
|
args = parser.parse_args()
|
|
serve(args.host, args.port)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|