mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-05-01 11:56:21 +00:00
Update vpn service
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ShieldCheck, Globe, ExternalLink, Loader2, CheckCircle, XCircle,
|
ShieldCheck, Globe, ExternalLink, Loader2, CheckCircle, XCircle,
|
||||||
Play, Square, RotateCw, Trash2, FileText, ChevronRight, ChevronDown,
|
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"
|
} from "lucide-react"
|
||||||
import { fetchApi } from "../lib/api-config"
|
import { fetchApi } from "../lib/api-config"
|
||||||
|
|
||||||
@@ -96,6 +96,12 @@ export function SecureGatewaySetup() {
|
|||||||
// Host IP for "Host Only" mode
|
// Host IP for "Host Only" mode
|
||||||
const [hostIp, setHostIp] = useState("")
|
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
|
// Password visibility
|
||||||
const [visiblePasswords, setVisiblePasswords] = useState<Set<string>>(new Set())
|
const [visiblePasswords, setVisiblePasswords] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
@@ -139,10 +145,13 @@ export function SecureGatewaySetup() {
|
|||||||
const networksRes = await fetchApi("/api/oci/networks")
|
const networksRes = await fetchApi("/api/oci/networks")
|
||||||
if (networksRes.success) {
|
if (networksRes.success) {
|
||||||
setNetworks(networksRes.networks || [])
|
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]
|
const primaryNetwork = networksRes.networks?.find((n: NetworkInfo) => n.recommended) || networksRes.networks?.[0]
|
||||||
if (primaryNetwork?.address) {
|
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) {
|
} catch (err) {
|
||||||
@@ -187,6 +196,8 @@ export function SecureGatewaySetup() {
|
|||||||
if (config.access_mode === "host_only" && hostIp) {
|
if (config.access_mode === "host_only" && hostIp) {
|
||||||
deployConfig.advertise_routes = [`${hostIp}/32`]
|
deployConfig.advertise_routes = [`${hostIp}/32`]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[v0] Deploy config:", JSON.stringify(deployConfig, null, 2))
|
||||||
|
|
||||||
setDeployProgress("Creating LXC container...")
|
setDeployProgress("Creating LXC container...")
|
||||||
|
|
||||||
@@ -218,10 +229,15 @@ export function SecureGatewaySetup() {
|
|||||||
setDeploying(false)
|
setDeploying(false)
|
||||||
setCurrentStep(0)
|
setCurrentStep(0)
|
||||||
|
|
||||||
// Show post-deploy confirmation if user needs to approve routes
|
// Show post-deploy confirmation - always show when access mode is set (routes need approval)
|
||||||
const needsApproval = deployConfig.advertise_routes?.length > 0 || deployConfig.exit_node || deployConfig.accept_routes
|
const needsApproval = deployConfig.access_mode && deployConfig.access_mode !== "none"
|
||||||
if (needsApproval) {
|
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)
|
setShowPostDeployInfo(true)
|
||||||
}
|
}
|
||||||
}, 2000)
|
}, 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 () => {
|
const handleRemove = async () => {
|
||||||
setActionLoading("remove")
|
setActionLoading("remove")
|
||||||
try {
|
try {
|
||||||
@@ -588,15 +638,18 @@ export function SecureGatewaySetup() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Approval notice */}
|
{/* Approval notice */}
|
||||||
{(config.access_mode !== "none" || config.exit_node) && !deploying && (
|
{(config.access_mode && config.access_mode !== "none") && !deploying && (
|
||||||
<div className="bg-cyan-500/10 border border-cyan-500/20 rounded-lg p-3">
|
<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">
|
<p className="text-xs text-cyan-400 flex items-start gap-2">
|
||||||
<Info className="h-4 w-4 flex-shrink-0 mt-0.5" />
|
<Info className="h-4 w-4 flex-shrink-0 mt-0.5" />
|
||||||
<span>
|
<span>
|
||||||
After deployment, you{"'"}ll need to <strong>approve the subnet routes</strong>
|
<strong>Important:</strong> After deployment, you must approve the subnet route in Tailscale Admin for remote access to work.
|
||||||
{config.exit_node && <span> and <strong>exit node</strong></span>} in your Tailscale Admin Console for them to work.
|
{config.exit_node && <span> You{"'"}ll also need to approve the exit node.</span>}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -760,8 +813,18 @@ export function SecureGatewaySetup() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tailscale admin link */}
|
{/* Update Auth Key button */}
|
||||||
<div className="pt-2 border-t border-border flex items-center justify-between">
|
<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
|
<a
|
||||||
href="https://login.tailscale.com/admin/machines"
|
href="https://login.tailscale.com/admin/machines"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -830,6 +893,75 @@ export function SecureGatewaySetup() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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 > Settings > 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 */}
|
{/* Post-Deploy Info Dialog */}
|
||||||
<Dialog open={showPostDeployInfo} onOpenChange={setShowPostDeployInfo}>
|
<Dialog open={showPostDeployInfo} onOpenChange={setShowPostDeployInfo}>
|
||||||
<DialogContent className="max-w-md">
|
<DialogContent className="max-w-md">
|
||||||
@@ -880,12 +1012,24 @@ export function SecureGatewaySetup() {
|
|||||||
|
|
||||||
<div className="bg-muted/30 rounded-lg p-4 space-y-2">
|
<div className="bg-muted/30 rounded-lg p-4 space-y-2">
|
||||||
<p className="text-sm font-medium">How to approve:</p>
|
<p className="text-sm font-medium">How to approve:</p>
|
||||||
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
|
<ol className="text-sm text-muted-foreground space-y-2 list-decimal list-inside">
|
||||||
<li>Go to Tailscale Admin Console</li>
|
<li>Click the button below to open Tailscale Admin</li>
|
||||||
<li>Find the machine "{deployedConfig.hostname || "proxmox-gateway"}"</li>
|
<li>Find <span className="font-mono text-cyan-400">{deployedConfig.hostname || "proxmox-gateway"}</span> in the machines list</li>
|
||||||
<li>Click on it and approve the pending routes/exit node</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>
|
</ol>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
|||||||
@@ -475,3 +475,48 @@ def get_app_status(app_id: str):
|
|||||||
"success": False,
|
"success": False,
|
||||||
"message": str(e)
|
"message": str(e)
|
||||||
}), 500
|
}), 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
|
||||||
|
|||||||
@@ -1111,6 +1111,82 @@ def detect_networks() -> List[Dict[str, str]]:
|
|||||||
return networks
|
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)
|
# Runtime Detection (for backward compatibility)
|
||||||
# =================================================================
|
# =================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user