From 4546adb894c9647b93a101324c7a470f80384a28 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Sat, 14 Mar 2026 20:30:53 +0100 Subject: [PATCH] Update vpn service --- AppImage/components/secure-gateway-setup.tsx | 174 +++++++++++++++++-- AppImage/scripts/flask_oci_routes.py | 45 +++++ AppImage/scripts/oci_manager.py | 76 ++++++++ 3 files changed, 280 insertions(+), 15 deletions(-) diff --git a/AppImage/components/secure-gateway-setup.tsx b/AppImage/components/secure-gateway-setup.tsx index ab9462e1..b5281594 100644 --- a/AppImage/components/secure-gateway-setup.tsx +++ b/AppImage/components/secure-gateway-setup.tsx @@ -16,7 +16,7 @@ import { import { ShieldCheck, Globe, ExternalLink, Loader2, CheckCircle, XCircle, Play, Square, RotateCw, Trash2, FileText, ChevronRight, ChevronDown, - AlertTriangle, Info, Network, Eye, EyeOff, Settings, Wifi, + AlertTriangle, Info, Network, Eye, EyeOff, Settings, Wifi, Key, } from "lucide-react" import { fetchApi } from "../lib/api-config" @@ -96,6 +96,12 @@ export function SecureGatewaySetup() { // Host IP for "Host Only" mode const [hostIp, setHostIp] = useState("") + // Update Auth Key + const [showUpdateAuthKey, setShowUpdateAuthKey] = useState(false) + const [newAuthKey, setNewAuthKey] = useState("") + const [updateAuthKeyLoading, setUpdateAuthKeyLoading] = useState(false) + const [updateAuthKeyError, setUpdateAuthKeyError] = useState("") + // Password visibility const [visiblePasswords, setVisiblePasswords] = useState>(new Set()) @@ -139,10 +145,13 @@ export function SecureGatewaySetup() { const networksRes = await fetchApi("/api/oci/networks") if (networksRes.success) { setNetworks(networksRes.networks || []) - // Get host IP for "Host Only" mode + // Get host IP for "Host Only" mode - extract just the IP without CIDR const primaryNetwork = networksRes.networks?.find((n: NetworkInfo) => n.recommended) || networksRes.networks?.[0] if (primaryNetwork?.address) { - setHostIp(primaryNetwork.address) + // Remove CIDR notation if present (e.g., "192.168.0.55/24" -> "192.168.0.55") + const ip = primaryNetwork.address.split("/")[0] + setHostIp(ip) + console.log("[v0] Host IP for Host Only mode:", ip) } } } catch (err) { @@ -187,6 +196,8 @@ export function SecureGatewaySetup() { if (config.access_mode === "host_only" && hostIp) { deployConfig.advertise_routes = [`${hostIp}/32`] } + + console.log("[v0] Deploy config:", JSON.stringify(deployConfig, null, 2)) setDeployProgress("Creating LXC container...") @@ -218,10 +229,15 @@ export function SecureGatewaySetup() { setDeploying(false) setCurrentStep(0) - // Show post-deploy confirmation if user needs to approve routes - const needsApproval = deployConfig.advertise_routes?.length > 0 || deployConfig.exit_node || deployConfig.accept_routes + // Show post-deploy confirmation - always show when access mode is set (routes need approval) + const needsApproval = deployConfig.access_mode && deployConfig.access_mode !== "none" if (needsApproval) { - setDeployedConfig(deployConfig) + // Ensure advertise_routes is set for the dialog + const finalConfig = { ...deployConfig } + if (deployConfig.access_mode === "host_only" && hostIp) { + finalConfig.advertise_routes = [`${hostIp}/32`] + } + setDeployedConfig(finalConfig) setShowPostDeployInfo(true) } }, 2000) @@ -248,6 +264,40 @@ export function SecureGatewaySetup() { } } + const handleUpdateAuthKey = async () => { + if (!newAuthKey.trim()) { + setUpdateAuthKeyError("Auth Key is required") + return + } + + setUpdateAuthKeyLoading(true) + setUpdateAuthKeyError("") + + try { + const result = await fetchApi("/api/oci/installed/secure-gateway/update-auth-key", { + method: "POST", + body: JSON.stringify({ + auth_key: newAuthKey.trim() + }) + }) + + if (!result.success) { + setUpdateAuthKeyError(result.message || "Failed to update auth key") + setUpdateAuthKeyLoading(false) + return + } + + // Success - close dialog and reload status + setShowUpdateAuthKey(false) + setNewAuthKey("") + await loadStatus() + } catch (err: any) { + setUpdateAuthKeyError(err.message || "Failed to update auth key") + } finally { + setUpdateAuthKeyLoading(false) + } + } + const handleRemove = async () => { setActionLoading("remove") try { @@ -588,15 +638,18 @@ export function SecureGatewaySetup() { {/* Approval notice */} - {(config.access_mode !== "none" || config.exit_node) && !deploying && ( -
+ {(config.access_mode && config.access_mode !== "none") && !deploying && ( +

- After deployment, you{"'"}ll need to approve the subnet routes - {config.exit_node && and exit node} in your Tailscale Admin Console for them to work. + Important: After deployment, you must approve the subnet route in Tailscale Admin for remote access to work. + {config.exit_node && You{"'"}ll also need to approve the exit node.}

+

+ We{"'"}ll show you exactly what to do after the gateway is deployed. +

)} @@ -760,8 +813,18 @@ export function SecureGatewaySetup() {
- {/* Tailscale admin link */} + {/* Update Auth Key button */}
+ + {/* Update Auth Key Dialog */} + { + setShowUpdateAuthKey(open) + if (!open) { + setNewAuthKey("") + setUpdateAuthKeyError("") + } + }}> + + + + + Update Auth Key + + + Enter a new Tailscale auth key to re-authenticate the gateway. This is useful if your previous key has expired. + + + +
+
+ + setNewAuthKey(e.target.value)} + placeholder="tskey-auth-..." + className="font-mono text-sm" + /> +

+ Generate a new key at{" "} + + Tailscale Admin > Settings > Keys + +

+
+ + {updateAuthKeyError && ( +
+

{updateAuthKeyError}

+
+ )} +
+ +
+ + +
+ + + {/* Post-Deploy Info Dialog */} @@ -880,12 +1012,24 @@ export function SecureGatewaySetup() {

How to approve:

-
    -
  1. Go to Tailscale Admin Console
  2. -
  3. Find the machine "{deployedConfig.hostname || "proxmox-gateway"}"
  4. -
  5. Click on it and approve the pending routes/exit node
  6. +
      +
    1. Click the button below to open Tailscale Admin
    2. +
    3. Find {deployedConfig.hostname || "proxmox-gateway"} in the machines list
    4. +
    5. Click on it to open machine details
    6. +
    7. In the Subnets section, click Edit and enable the route
    8. + {deployedConfig.exit_node && ( +
    9. In Routing Settings, enable Exit Node
    10. + )}
+ +
+

+ Once approved, you can access your Proxmox host at{" "} + {deployedConfig.advertise_routes?.[0]?.replace("/32", "") || hostIp}:8006 (Proxmox UI) or{" "} + {deployedConfig.advertise_routes?.[0]?.replace("/32", "") || hostIp}:8008 (ProxMenux Monitor) from any device with Tailscale. +

+
diff --git a/AppImage/scripts/flask_oci_routes.py b/AppImage/scripts/flask_oci_routes.py index 5ea911e4..0475221c 100644 --- a/AppImage/scripts/flask_oci_routes.py +++ b/AppImage/scripts/flask_oci_routes.py @@ -475,3 +475,48 @@ def get_app_status(app_id: str): "success": False, "message": str(e) }), 500 + + +@oci_bp.route("/installed//update-auth-key", methods=["POST"]) +@require_auth +def update_auth_key(app_id: str): + """ + Update the Tailscale auth key for an installed gateway. + + This is useful when the auth key expires and the gateway needs to re-authenticate. + + Body: + { + "auth_key": "tskey-auth-xxx" + } + + Returns: + Success status and message. + """ + try: + data = request.get_json() + + if not data or "auth_key" not in data: + return jsonify({ + "success": False, + "message": "auth_key is required in request body" + }), 400 + + auth_key = data["auth_key"] + + if not auth_key.startswith("tskey-"): + return jsonify({ + "success": False, + "message": "Invalid auth key format. Should start with 'tskey-'" + }), 400 + + result = oci_manager.update_auth_key(app_id, auth_key) + status_code = 200 if result.get("success") else 400 + return jsonify(result), status_code + + except Exception as e: + logger.error(f"Failed to update auth key: {e}") + return jsonify({ + "success": False, + "message": str(e) + }), 500 diff --git a/AppImage/scripts/oci_manager.py b/AppImage/scripts/oci_manager.py index 7d8369a9..a6767e36 100644 --- a/AppImage/scripts/oci_manager.py +++ b/AppImage/scripts/oci_manager.py @@ -1111,6 +1111,82 @@ def detect_networks() -> List[Dict[str, str]]: return networks +# ================================================================= +# Update Auth Key (for Tailscale re-authentication) +# ================================================================= +def update_auth_key(app_id: str, auth_key: str) -> Dict[str, Any]: + """Update the Tailscale auth key for a running gateway.""" + result = {"success": False, "message": "", "app_id": app_id} + + # Get VMID for the app + vmid = _get_vmid_for_app(app_id) + if not vmid: + result["message"] = f"App {app_id} not found or not installed" + return result + + # Check if container is running + status = get_app_status(app_id) + if status.get("state") != "running": + result["message"] = "Container must be running to update auth key" + return result + + logger.info(f"Updating auth key for {app_id} (VMID: {vmid})") + print(f"[*] Updating auth key for {app_id}...") + + # Run tailscale logout first to clear existing state + print(f"[*] Logging out of Tailscale...") + _run_pve_cmd(["pct", "exec", str(vmid), "--", "tailscale", "logout"], timeout=30) + + # Wait a moment for logout to complete + import time + time.sleep(2) + + # Run tailscale up with new auth key + print(f"[*] Authenticating with new key...") + + # Load saved config to get original settings + config_file = os.path.join(INSTANCES_DIR, app_id, "config.json") + config = {} + if os.path.exists(config_file): + try: + with open(config_file) as f: + saved_config = json.load(f) + config = saved_config.get("values", {}) + except: + pass + + # Build tailscale up command + ts_cmd = ["tailscale", "up", f"--authkey={auth_key}"] + + hostname = config.get("hostname") + if hostname: + ts_cmd.append(f"--hostname={hostname}") + + advertise_routes = config.get("advertise_routes") + if advertise_routes: + if isinstance(advertise_routes, list): + advertise_routes = ",".join(advertise_routes) + ts_cmd.append(f"--advertise-routes={advertise_routes}") + + if config.get("exit_node"): + ts_cmd.append("--advertise-exit-node") + + if config.get("accept_routes"): + ts_cmd.append("--accept-routes") + + rc, out, err = _run_pve_cmd(["pct", "exec", str(vmid), "--"] + ts_cmd, timeout=60) + + if rc != 0: + logger.error(f"Failed to update auth key: {err}") + result["message"] = f"Failed to authenticate: {err}" + return result + + print(f"[OK] Auth key updated successfully") + result["success"] = True + result["message"] = "Auth key updated successfully" + return result + + # ================================================================= # Runtime Detection (for backward compatibility) # =================================================================