furtka-gateway/control_plane/app.py
Robert Syrnicki 3a9d18fcd5 Scaffold gateway stack and single-tenant control plane
wg-easy + Traefik docker-compose stack (Phase 1) plus a stdlib-only
control-plane API for box registration, WireGuard peer provisioning via
wg-easy, and per-box route publish/unpublish backed by Traefik's file
provider (Phase 2, single-tenant mode). SQLite holds accounts/boxes/routes
so a later multi-tenant shared instance is the same schema with more rows,
not a reshape.

wg-easy's actual REST API was verified against its source rather than
assumed: it has no bearer-token auth (session-cookie login via
POST /api/auth/password) and no way to accept an externally-generated
public key (it always mints the keypair itself, private key included) —
both corrected from the original plan during implementation.
2026-08-24 11:50:25 +02:00

60 lines
1.8 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://wg-easy:51821"),
username=os.environ.get("WG_EASY_ADMIN_USERNAME", "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()