From 7514dd8a17b63292e511f6bcbde4fa55b2c89f0a Mon Sep 17 00:00:00 2001 From: Sasha Le Date: Mon, 23 Feb 2026 18:23:44 +1000 Subject: [PATCH 01/25] testing punchd in keylessh --- .env.example | 19 +- client/src/App.tsx | 4 + client/src/components/layout/AppLayout.tsx | 3 +- client/src/lib/api.ts | 36 +- client/src/pages/AdminSignalServers.tsx | 451 ++++++++++++++ client/src/pages/Dashboard.tsx | 102 +++- punchd | 1 + server/routes.ts | 174 +++++- server/storage.ts | 120 ++++ shared/schema.ts | 13 + signal-server/Dockerfile | 45 ++ signal-server/deploy.sh | 245 ++++++++ signal-server/package-lock.json | 635 ++++++++++++++++++++ signal-server/package.json | 22 + signal-server/public/portal.html | 244 ++++++++ signal-server/src/index.ts | 646 +++++++++++++++++++++ signal-server/src/relay/http-relay.ts | 227 ++++++++ signal-server/src/signaling/pairing.ts | 155 +++++ signal-server/src/signaling/registry.ts | 254 ++++++++ signal-server/tsconfig.json | 15 + tcp-bridge/src/index.ts | 37 +- 21 files changed, 3427 insertions(+), 21 deletions(-) create mode 100644 client/src/pages/AdminSignalServers.tsx create mode 160000 punchd create mode 100644 signal-server/Dockerfile create mode 100755 signal-server/deploy.sh create mode 100644 signal-server/package-lock.json create mode 100644 signal-server/package.json create mode 100644 signal-server/public/portal.html create mode 100644 signal-server/src/index.ts create mode 100644 signal-server/src/relay/http-relay.ts create mode 100644 signal-server/src/signaling/pairing.ts create mode 100644 signal-server/src/signaling/registry.ts create mode 100644 signal-server/tsconfig.json diff --git a/.env.example b/.env.example index 4ed5a8d..53f342a 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,7 @@ NODE_ENV=development # Database (SQLite file path) DATABASE_URL=./data/keylessh.db -# External TCP Bridge (optional, for production scaling) +# External TCP Bridge (optional, for production SSH scaling) # BRIDGE_URL=wss://your-bridge.azurecontainerapps.io # Note: Bridge authenticates using JWT verified against JWKS (same tidecloak.json) @@ -16,6 +16,23 @@ DATABASE_URL=./data/keylessh.db # If not set, tcp-bridge falls back to data/tidecloak.json # CLIENT_ADAPTER='{"realm":"...","auth-server-url":"...","resource":"...","jwk":{...}}' +# ============================================ +# Signal Server (VM deployment — see signal-server/) +# ============================================ +# The signal server handles P2P signaling, HTTP relay, and WAF portal. +# Deploy it on a VM alongside coturn using: signal-server/deploy.sh +# +# API_SECRET: Shared secret for WAF registration authentication (timing-safe) +# API_SECRET=your-shared-secret-here +# +# STUN/TURN configuration for WebRTC P2P upgrade: +# ICE_SERVERS: Comma-separated STUN server URLs +# ICE_SERVERS=stun:relay.example.com:3478 +# TURN_SERVER: TURN relay fallback URL +# TURN_SERVER=turn:relay.example.com:3478 +# TURN_SECRET: Shared secret for TURN REST API ephemeral credentials (HMAC-SHA256) +# TURN_SECRET=your-turn-secret-here + # Auth server override (optional, for testing) # AUTH_SERVER_OVERRIDE_URL=http://localhost:8080 diff --git a/client/src/App.tsx b/client/src/App.tsx index 4c77c50..67f808e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -22,6 +22,7 @@ import AdminLogs from "@/pages/AdminLogs"; import AdminLicense from "@/pages/AdminLicense"; import AdminRecordings from "@/pages/AdminRecordings"; import AdminBridges from "@/pages/AdminBridges"; +import AdminSignalServers from "@/pages/AdminSignalServers"; import NotFound from "@/pages/not-found"; import { Loader2, Terminal } from "lucide-react"; @@ -146,6 +147,9 @@ function AuthenticatedApp() { {isAdmin ? : } + + {isAdmin ? : } + {isAdmin ? : } diff --git a/client/src/components/layout/AppLayout.tsx b/client/src/components/layout/AppLayout.tsx index f9aa65a..9ea8d75 100644 --- a/client/src/components/layout/AppLayout.tsx +++ b/client/src/components/layout/AppLayout.tsx @@ -24,7 +24,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Server, Users, Activity, LogOut, Shield, ChevronDown, Layers, ScrollText, KeyRound, CheckSquare, RefreshCw, CreditCard, Video, Zap, Sun, Moon, Network } from "lucide-react"; +import { Server, Users, Activity, LogOut, Shield, ChevronDown, Layers, ScrollText, KeyRound, CheckSquare, RefreshCw, CreditCard, Video, Zap, Sun, Moon, Network, Radio } from "lucide-react"; function KeyleSSHLogo({ className = "" }: { className?: string }) { return ( @@ -76,6 +76,7 @@ const adminNavGroups = [ { title: "Overview", url: "/admin", icon: Shield }, { title: "Servers", url: "/admin/servers", icon: Server }, { title: "Bridges", url: "/admin/bridges", icon: Network }, + { title: "Signal Servers", url: "/admin/signal-servers", icon: Radio }, { title: "Sessions", url: "/admin/sessions", icon: Activity }, ], }, diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 9dee00d..1a56b93 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -10,6 +10,8 @@ import type { TemplateParameter, Bridge, InsertBridge, + SignalServer, + InsertSignalServer, } from "@shared/schema"; const API_BASE = import.meta.env.VITE_API_BASE_URL || ""; @@ -96,6 +98,9 @@ export const api = { list: () => apiRequest("/api/servers"), get: (id: string) => apiRequest(`/api/servers/${id}`), }, + wafEndpoints: { + list: () => apiRequest("/api/waf-endpoints"), + }, sessions: { list: () => apiRequest("/api/sessions"), create: (data: { serverId: string; sshUser: string }) => @@ -138,6 +143,22 @@ export const api = { delete: (id: string) => apiRequest(`/api/admin/bridges/${id}`, { method: "DELETE" }), }, + signalServers: { + list: () => apiRequest("/api/admin/signal-servers"), + get: (id: string) => apiRequest(`/api/admin/signal-servers/${id}`), + create: (data: InsertSignalServer) => + apiRequest("/api/admin/signal-servers", { + method: "POST", + body: JSON.stringify(data), + }), + update: (id: string, data: Partial) => + apiRequest(`/api/admin/signal-servers/${id}`, { + method: "PUT", + body: JSON.stringify(data), + }), + delete: (id: string) => + apiRequest(`/api/admin/signal-servers/${id}`, { method: "DELETE" }), + }, users: { list: () => apiRequest("/api/admin/users"), add: (data: { username: string; firstName: string; lastName: string; email: string }) => @@ -422,8 +443,21 @@ export const api = { }, }; +// WAF endpoint from signal server aggregation +export interface WafEndpoint { + id: string; + displayName: string; + description: string; + backends: { name: string }[]; + online: boolean; + clientCount: number; + signalServerId: string; + signalServerName: string; + signalServerUrl: string; +} + // Re-export types for convenience -export type { PolicyTemplate, InsertPolicyTemplate, TemplateParameter, Bridge, InsertBridge }; +export type { PolicyTemplate, InsertPolicyTemplate, TemplateParameter, Bridge, InsertBridge, SignalServer, InsertSignalServer }; // SSH Policy Configuration for role creation export interface SshPolicyConfig { diff --git a/client/src/pages/AdminSignalServers.tsx b/client/src/pages/AdminSignalServers.tsx new file mode 100644 index 0000000..e443726 --- /dev/null +++ b/client/src/pages/AdminSignalServers.tsx @@ -0,0 +1,451 @@ +import { useState } from "react"; +import { useQuery, useMutation, useIsFetching } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Textarea } from "@/components/ui/textarea"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useToast } from "@/hooks/use-toast"; +import { queryClient } from "@/lib/queryClient"; +import { useAutoRefresh } from "@/hooks/useAutoRefresh"; +import { Plus, Pencil, Trash2, Radio, Search, Loader2, CheckCircle, XCircle, Wifi } from "lucide-react"; +import type { SignalServer } from "@shared/schema"; +import { api } from "@/lib/api"; +import { RefreshButton } from "@/components/RefreshButton"; + +interface SignalServerFormData { + name: string; + url: string; + description: string; + enabled: boolean; +} + +const defaultFormData: SignalServerFormData = { + name: "", + url: "", + description: "", + enabled: true, +}; + +function SignalServerForm({ + initialData, + onSubmit, + onCancel, + isLoading, +}: { + initialData?: SignalServerFormData; + onSubmit: (data: SignalServerFormData) => void; + onCancel: () => void; + isLoading: boolean; +}) { + const [formData, setFormData] = useState(initialData || defaultFormData); + const [testStatus, setTestStatus] = useState<"idle" | "testing" | "success" | "error">("idle"); + const [testMessage, setTestMessage] = useState(""); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(formData); + }; + + const handleTestConnection = async () => { + if (!formData.url) { + setTestStatus("error"); + setTestMessage("Please enter a URL first"); + return; + } + + setTestStatus("testing"); + setTestMessage(""); + + try { + const healthUrl = formData.url.replace(/\/$/, "") + "/health"; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + const resp = await fetch(healthUrl, { signal: controller.signal }); + clearTimeout(timeout); + + if (resp.ok) { + const data = await resp.json(); + setTestStatus("success"); + setTestMessage(`Online — ${data.wafs ?? 0} WAFs, ${data.clients ?? 0} clients`); + } else { + setTestStatus("error"); + setTestMessage(`Server returned ${resp.status}`); + } + } catch (err) { + setTestStatus("error"); + setTestMessage(err instanceof Error && err.name === "AbortError" ? "Connection timeout" : "Cannot reach server"); + } + }; + + return ( +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Production Signal Server" + required + /> +
+ +
+ + setFormData({ ...formData, url: e.target.value })} + placeholder="https://tidestun.codesyo.com:9090" + required + /> +

+ The signal server URL (HTTPS). WAFs register here for P2P signaling and HTTP relay. +

+
+ +
+ + {testStatus === "success" && ( +
+ + {testMessage} +
+ )} + {testStatus === "error" && ( +
+ + {testMessage} +
+ )} +
+ +
+ +