Update vpn service

This commit is contained in:
MacRimi
2026-03-14 20:30:53 +01:00
parent db5ac37ad3
commit 4546adb894
3 changed files with 280 additions and 15 deletions

View File

@@ -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<Set<string>>(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() {
</div>
{/* Approval notice */}
{(config.access_mode !== "none" || config.exit_node) && !deploying && (
<div className="bg-cyan-500/10 border border-cyan-500/20 rounded-lg p-3">
{(config.access_mode && config.access_mode !== "none") && !deploying && (
<div className="bg-cyan-500/10 border border-cyan-500/20 rounded-lg p-3 space-y-2">
<p className="text-xs text-cyan-400 flex items-start gap-2">
<Info className="h-4 w-4 flex-shrink-0 mt-0.5" />
<span>
After deployment, you{"'"}ll need to <strong>approve the subnet routes</strong>
{config.exit_node && <span> and <strong>exit node</strong></span>} in your Tailscale Admin Console for them to work.
<strong>Important:</strong> After deployment, you must approve the subnet route in Tailscale Admin for remote access to work.
{config.exit_node && <span> You{"'"}ll also need to approve the exit node.</span>}
</span>
</p>
<p className="text-xs text-muted-foreground ml-6">
We{"'"}ll show you exactly what to do after the gateway is deployed.
</p>
</div>
)}
@@ -760,8 +813,18 @@ export function SecureGatewaySetup() {
</Button>
</div>
{/* Tailscale admin link */}
{/* Update Auth Key button */}
<div className="pt-2 border-t border-border flex items-center justify-between">
<Button
size="sm"
variant="ghost"
onClick={() => setShowUpdateAuthKey(true)}
disabled={actionLoading !== null}
className="text-xs h-7 px-2"
>
<Key className="h-3 w-3 mr-1" />
Update Auth Key
</Button>
<a
href="https://login.tailscale.com/admin/machines"
target="_blank"
@@ -830,6 +893,75 @@ export function SecureGatewaySetup() {
</DialogContent>
</Dialog>
{/* Update Auth Key Dialog */}
<Dialog open={showUpdateAuthKey} onOpenChange={(open) => {
setShowUpdateAuthKey(open)
if (!open) {
setNewAuthKey("")
setUpdateAuthKeyError("")
}
}}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Key className="h-5 w-5 text-cyan-500" />
Update Auth Key
</DialogTitle>
<DialogDescription>
Enter a new Tailscale auth key to re-authenticate the gateway. This is useful if your previous key has expired.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">New Auth Key</label>
<Input
type="password"
value={newAuthKey}
onChange={(e) => setNewAuthKey(e.target.value)}
placeholder="tskey-auth-..."
className="font-mono text-sm"
/>
<p className="text-xs text-muted-foreground">
Generate a new key at{" "}
<a
href="https://login.tailscale.com/admin/settings/keys"
target="_blank"
rel="noopener noreferrer"
className="text-cyan-500 hover:text-cyan-400 underline"
>
Tailscale Admin &gt; Settings &gt; Keys
</a>
</p>
</div>
{updateAuthKeyError && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3">
<p className="text-xs text-red-500">{updateAuthKeyError}</p>
</div>
)}
</div>
<div className="flex justify-end gap-2 pt-2">
<Button variant="outline" onClick={() => setShowUpdateAuthKey(false)}>
Cancel
</Button>
<Button
onClick={handleUpdateAuthKey}
disabled={updateAuthKeyLoading || !newAuthKey.trim()}
className="bg-cyan-600 hover:bg-cyan-700"
>
{updateAuthKeyLoading ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<Key className="h-4 w-4 mr-2" />
)}
Update Key
</Button>
</div>
</DialogContent>
</Dialog>
{/* Post-Deploy Info Dialog */}
<Dialog open={showPostDeployInfo} onOpenChange={setShowPostDeployInfo}>
<DialogContent className="max-w-md">
@@ -880,12 +1012,24 @@ export function SecureGatewaySetup() {
<div className="bg-muted/30 rounded-lg p-4 space-y-2">
<p className="text-sm font-medium">How to approve:</p>
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
<li>Go to Tailscale Admin Console</li>
<li>Find the machine "{deployedConfig.hostname || "proxmox-gateway"}"</li>
<li>Click on it and approve the pending routes/exit node</li>
<ol className="text-sm text-muted-foreground space-y-2 list-decimal list-inside">
<li>Click the button below to open Tailscale Admin</li>
<li>Find <span className="font-mono text-cyan-400">{deployedConfig.hostname || "proxmox-gateway"}</span> in the machines list</li>
<li>Click on it to open machine details</li>
<li>In the <strong>Subnets</strong> section, click <strong>Edit</strong> and enable the route</li>
{deployedConfig.exit_node && (
<li>In <strong>Routing Settings</strong>, enable <strong>Exit Node</strong></li>
)}
</ol>
</div>
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3">
<p className="text-xs text-green-400">
Once approved, you can access your Proxmox host at{" "}
<span className="font-mono">{deployedConfig.advertise_routes?.[0]?.replace("/32", "") || hostIp}:8006</span> (Proxmox UI) or{" "}
<span className="font-mono">{deployedConfig.advertise_routes?.[0]?.replace("/32", "") || hostIp}:8008</span> (ProxMenux Monitor) from any device with Tailscale.
</p>
</div>
</div>
<div className="flex justify-end gap-2 pt-2">

View File

@@ -475,3 +475,48 @@ def get_app_status(app_id: str):
"success": False,
"message": str(e)
}), 500
@oci_bp.route("/installed/<app_id>/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

View File

@@ -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)
# =================================================================