182 lines
6.7 KiB
YAML
182 lines
6.7 KiB
YAML
# services/game-stream/wolf-gatekeeper-configmap.yaml
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: wolf-gatekeeper
|
|
namespace: game-stream
|
|
data:
|
|
wolf_gatekeeper.py: |
|
|
#!/usr/bin/env python3
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
import ipaddress
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
|
|
LISTEN = ("0.0.0.0", int(os.environ.get("GATEKEEPER_HTTP_PORT", "8087")))
|
|
HOST_ROOT = os.environ.get("HOST_ROOT", "/host")
|
|
NFT_PATH = os.environ.get("NFT_PATH", "/sbin/nft")
|
|
MAX_TTL_SECONDS = int(os.environ.get("MAX_TTL_SECONDS", "28800"))
|
|
TCP_PORTS = ["47984", "47989", "48010"]
|
|
UDP_PORTS = ["47999", "48100", "48200"]
|
|
PRIVATE_V4 = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"]
|
|
PRIVATE_V6 = ["::1/128", "fc00::/7", "fe80::/10"]
|
|
|
|
|
|
def _json(handler, status, payload):
|
|
body = json.dumps(payload).encode("utf-8")
|
|
handler.send_response(status)
|
|
handler.send_header("content-type", "application/json")
|
|
handler.send_header("content-length", str(len(body)))
|
|
handler.end_headers()
|
|
handler.wfile.write(body)
|
|
|
|
|
|
def _nft(args, check=True):
|
|
command = ["chroot", HOST_ROOT, NFT_PATH, *args]
|
|
proc = subprocess.run(command, check=False, capture_output=True, text=True)
|
|
if check and proc.returncode != 0:
|
|
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "nft failed")
|
|
return proc
|
|
|
|
|
|
def _port_set(ports):
|
|
return ["{", *[f"{port}," for port in ports[:-1]], ports[-1], "}"]
|
|
|
|
|
|
def _install_chain(name, hook, priority):
|
|
_nft(
|
|
[
|
|
"add",
|
|
"chain",
|
|
"inet",
|
|
"wolf_gatekeeper",
|
|
name,
|
|
"{",
|
|
"type",
|
|
"filter",
|
|
"hook",
|
|
hook,
|
|
"priority",
|
|
priority,
|
|
"policy",
|
|
"accept;",
|
|
"}",
|
|
],
|
|
check=False,
|
|
)
|
|
_nft(["flush", "chain", "inet", "wolf_gatekeeper", name], check=False)
|
|
_nft(["add", "rule", "inet", "wolf_gatekeeper", name, "iifname", "lo", "accept"])
|
|
for cidr in PRIVATE_V4:
|
|
_nft(["add", "rule", "inet", "wolf_gatekeeper", name, "ip", "saddr", cidr, "accept"])
|
|
for cidr in PRIVATE_V6:
|
|
_nft(["add", "rule", "inet", "wolf_gatekeeper", name, "ip6", "saddr", cidr, "accept"])
|
|
_nft(["add", "rule", "inet", "wolf_gatekeeper", name, "ip", "saddr", "@allowed_v4", "tcp", "dport", *_port_set(TCP_PORTS), "accept"])
|
|
_nft(["add", "rule", "inet", "wolf_gatekeeper", name, "ip", "saddr", "@allowed_v4", "udp", "dport", *_port_set(UDP_PORTS), "accept"])
|
|
_nft(["add", "rule", "inet", "wolf_gatekeeper", name, "ip6", "saddr", "@allowed_v6", "tcp", "dport", *_port_set(TCP_PORTS), "accept"])
|
|
_nft(["add", "rule", "inet", "wolf_gatekeeper", name, "ip6", "saddr", "@allowed_v6", "udp", "dport", *_port_set(UDP_PORTS), "accept"])
|
|
_nft(["add", "rule", "inet", "wolf_gatekeeper", name, "tcp", "dport", *_port_set(TCP_PORTS), "drop"])
|
|
_nft(["add", "rule", "inet", "wolf_gatekeeper", name, "udp", "dport", *_port_set(UDP_PORTS), "drop"])
|
|
|
|
|
|
def _ensure_rules():
|
|
_nft(["add", "table", "inet", "wolf_gatekeeper"], check=False)
|
|
_nft(["add", "set", "inet", "wolf_gatekeeper", "allowed_v4", "{", "type", "ipv4_addr;", "flags", "timeout;", "}"], check=False)
|
|
_nft(["add", "set", "inet", "wolf_gatekeeper", "allowed_v6", "{", "type", "ipv6_addr;", "flags", "timeout;", "}"], check=False)
|
|
_install_chain("prerouting", "prerouting", "-300;")
|
|
_install_chain("input", "input", "-90;")
|
|
|
|
|
|
def _validate_ip(value):
|
|
ip = ipaddress.ip_address(str(value or "").strip())
|
|
return str(ip), ip.version
|
|
|
|
|
|
def _set_name(version):
|
|
return "allowed_v4" if version == 4 else "allowed_v6"
|
|
|
|
|
|
def _unlock(value, ttl_seconds):
|
|
ip, version = _validate_ip(value)
|
|
ttl = max(60, min(int(ttl_seconds or MAX_TTL_SECONDS), MAX_TTL_SECONDS))
|
|
set_name = _set_name(version)
|
|
_nft(["delete", "element", "inet", "wolf_gatekeeper", set_name, "{", ip, "}"], check=False)
|
|
_nft(["add", "element", "inet", "wolf_gatekeeper", set_name, "{", ip, "timeout", f"{ttl}s", "}"])
|
|
return {"ip": ip, "ttl_seconds": ttl}
|
|
|
|
|
|
def _revoke(value):
|
|
ip, version = _validate_ip(value)
|
|
_nft(["delete", "element", "inet", "wolf_gatekeeper", _set_name(version), "{", ip, "}"], check=False)
|
|
return {"ip": ip}
|
|
|
|
|
|
def _entries(set_name, pattern):
|
|
proc = _nft(["list", "set", "inet", "wolf_gatekeeper", set_name], check=False)
|
|
if proc.returncode != 0:
|
|
return []
|
|
values = []
|
|
for match in re.finditer(pattern, proc.stdout):
|
|
value = match.group(0)
|
|
if value not in values:
|
|
values.append(value)
|
|
return values
|
|
|
|
|
|
def _status():
|
|
_ensure_rules()
|
|
v4 = _entries("allowed_v4", r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
|
|
v6 = _entries("allowed_v6", r"\b[0-9a-fA-F:]{2,}\b")
|
|
return {"success": True, "active_unlocks": [{"ip": ip} for ip in [*v4, *v6]], "tcp_ports": TCP_PORTS, "udp_ports": UDP_PORTS}
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def log_message(self, _format, *args):
|
|
return
|
|
|
|
def _payload(self):
|
|
length = int(self.headers.get("content-length") or "0")
|
|
if not length:
|
|
return {}
|
|
try:
|
|
data = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
return data if isinstance(data, dict) else {}
|
|
except Exception:
|
|
return {}
|
|
|
|
def do_GET(self):
|
|
try:
|
|
if self.path == "/healthz":
|
|
_ensure_rules()
|
|
_json(self, 200, {"ok": True})
|
|
elif self.path == "/status":
|
|
_json(self, 200, _status())
|
|
else:
|
|
_json(self, 404, {"success": False, "error": "not found"})
|
|
except Exception as exc:
|
|
_json(self, 500, {"success": False, "error": str(exc)})
|
|
|
|
def do_POST(self):
|
|
try:
|
|
payload = self._payload()
|
|
if self.path == "/unlock":
|
|
_ensure_rules()
|
|
result = _unlock(payload.get("ip"), payload.get("ttl_seconds"))
|
|
result.update({"success": True, "actor": payload.get("actor") or "", "target_user": payload.get("target_user") or ""})
|
|
_json(self, 200, result)
|
|
elif self.path == "/revoke":
|
|
_ensure_rules()
|
|
result = _revoke(payload.get("ip"))
|
|
result.update({"success": True})
|
|
_json(self, 200, result)
|
|
else:
|
|
_json(self, 404, {"success": False, "error": "not found"})
|
|
except Exception as exc:
|
|
_json(self, 400, {"success": False, "error": str(exc)})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_ensure_rules()
|
|
ThreadingHTTPServer(LISTEN, Handler).serve_forever()
|