Compare commits

..

1 Commits

Author SHA1 Message Date
Owen 080bcbaf97 Use endpoint instead of reachableAt for remote nodes 2026-09-07 11:55:45 -04:00
4 changed files with 486 additions and 958 deletions
+411 -889
View File
File diff suppressed because it is too large Load Diff
+20 -20
View File
@@ -33,7 +33,7 @@
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "9.1.0", "@asteasolutions/zod-to-openapi": "9.1.0",
"@aws-sdk/client-s3": "3.1127.0", "@aws-sdk/client-s3": "3.1121.0",
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz", "@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz",
"@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz", "@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz",
"@headlessui/react": "2.2.10", "@headlessui/react": "2.2.10",
@@ -65,11 +65,11 @@
"@react-email/components": "1.0.12", "@react-email/components": "1.0.12",
"@react-email/render": "2.1.0", "@react-email/render": "2.1.0",
"@react-email/tailwind": "2.0.7", "@react-email/tailwind": "2.0.7",
"@simplewebauthn/browser": "14.0.0", "@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "14.0.0", "@simplewebauthn/server": "13.3.3",
"@tailwindcss/forms": "0.5.11", "@tailwindcss/forms": "0.5.11",
"@tanstack/react-query": "5.102.8", "@tanstack/react-query": "5.102.8",
"@tanstack/react-table": "9.2.4", "@tanstack/react-table": "8.21.3",
"@xterm/addon-fit": "^0.11.0", "@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0", "@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^6.0.0",
@@ -96,21 +96,21 @@
"jmespath": "0.16.0", "jmespath": "0.16.0",
"js-yaml": "5.4.1", "js-yaml": "5.4.1",
"jsonwebtoken": "9.0.3", "jsonwebtoken": "9.0.3",
"lucide-react": "1.41.0", "lucide-react": "1.38.0",
"maxmind": "5.0.7", "maxmind": "5.0.7",
"moment": "2.30.1", "moment": "2.30.1",
"next": "16.3.4", "next": "16.3.3",
"next-intl": "4.14.2", "next-intl": "4.14.1",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"nextjs-toploader": "3.9.17", "nextjs-toploader": "3.9.17",
"node-cache": "5.1.2", "node-cache": "5.1.2",
"nodemailer": "10.0.0", "nodemailer": "9.1.0",
"oslo": "1.2.1", "oslo": "1.2.1",
"pg": "8.23.0", "pg": "8.23.0",
"posthog-node": "5.51.6", "posthog-node": "5.51.4",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.2.8", "react": "19.2.8",
"react-day-picker": "10.0.1", "react-day-picker": "9.14.0",
"react-dom": "19.2.8", "react-dom": "19.2.8",
"react-easy-sort": "1.8.0", "react-easy-sort": "1.8.0",
"react-hook-form": "7.87.0", "react-hook-form": "7.87.0",
@@ -119,7 +119,7 @@
"reodotdev": "1.1.0", "reodotdev": "1.1.0",
"semver": "7.8.5", "semver": "7.8.5",
"sshpk": "1.18.0", "sshpk": "1.18.0",
"stripe": "22.6.1", "stripe": "22.6.0",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"tailwind-merge": "3.6.0", "tailwind-merge": "3.6.0",
"topojson-client": "3.1.0", "topojson-client": "3.1.0",
@@ -152,12 +152,12 @@
"@types/jmespath": "0.15.2", "@types/jmespath": "0.15.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "9.0.10", "@types/jsonwebtoken": "9.0.10",
"@types/node": "26.4.1", "@types/node": "26.4.0",
"@types/nodemailer": "8.0.1", "@types/nodemailer": "8.0.1",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@types/pg": "8.23.1", "@types/pg": "8.23.1",
"@types/react": "19.2.18", "@types/react": "19.2.18",
"@types/react-dom": "19.2.7", "@types/react-dom": "19.2.5",
"@types/semver": "7.8.0", "@types/semver": "7.8.0",
"@types/sshpk": "1.17.5", "@types/sshpk": "1.17.5",
"@types/swagger-ui-express": "4.1.8", "@types/swagger-ui-express": "4.1.8",
@@ -168,20 +168,20 @@
"drizzle-kit": "0.31.10", "drizzle-kit": "0.31.10",
"esbuild": "0.28.2", "esbuild": "0.28.2",
"esbuild-node-externals": "2.0.0", "esbuild-node-externals": "2.0.0",
"eslint": "10.10.0", "eslint": "10.9.1",
"eslint-config-next": "16.3.4", "eslint-config-next": "16.3.3",
"postcss": "8.5.28", "postcss": "8.5.26",
"prettier": "3.9.6", "prettier": "3.9.6",
"react-email": "6.9.3", "react-email": "6.9.3",
"tailwindcss": "4.3.3", "tailwindcss": "4.3.3",
"tsc-alias": "1.9.4", "tsc-alias": "1.9.2",
"tsx": "4.23.13", "tsx": "4.23.13",
"typescript": "7.0.2", "typescript": "6.0.3",
"typescript-eslint": "8.69.0" "typescript-eslint": "8.68.0"
}, },
"overrides": { "overrides": {
"esbuild": "0.28.2", "esbuild": "0.28.2",
"dompurify": "3.4.0", "dompurify": "3.4.0",
"postcss": "8.5.28" "postcss": "8.5.26"
} }
} }
@@ -16,7 +16,7 @@ import { db, exitNodes, newts, sites } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import redisManager from "#private/lib/redis"; import redisManager from "#private/lib/redis";
// import { sendToClient } from "#private/routers/ws"; import { sendToClient } from "#private/routers/ws";
const INITIAL_DELAY_MS = 15 * 1000; // 15 seconds before first check const INITIAL_DELAY_MS = 15 * 1000; // 15 seconds before first check
const CHECK_INTERVAL_MS = 10 * 1000; // Check every 10 seconds const CHECK_INTERVAL_MS = 10 * 1000; // Check every 10 seconds
@@ -26,7 +26,7 @@ const REDIS_HASH_PREFIX = "exit-node-reconnect:";
interface PendingReconnect { interface PendingReconnect {
startTime: number; startTime: number;
reachableAt: string; endpoint: string;
} }
// In-memory tracking for this node // In-memory tracking for this node
@@ -40,15 +40,15 @@ let schedulerInterval: NodeJS.Timeout | null = null;
*/ */
export async function scheduleExitNodeReconnect( export async function scheduleExitNodeReconnect(
exitNodeId: number, exitNodeId: number,
reachableAt: string endpoint: string
): Promise<void> { ): Promise<void> {
logger.info( logger.info(
`Scheduling newt reconnect for exit node ${exitNodeId} (reachableAt: ${reachableAt})` `Scheduling newt reconnect for exit node ${exitNodeId} (endpoint: ${endpoint})`
); );
const entry: PendingReconnect = { const entry: PendingReconnect = {
startTime: Date.now(), startTime: Date.now(),
reachableAt endpoint
}; };
pendingReconnects.set(exitNodeId, entry); pendingReconnects.set(exitNodeId, entry);
@@ -63,8 +63,8 @@ export async function scheduleExitNodeReconnect(
); );
await redisManager.hset( await redisManager.hset(
`${REDIS_HASH_PREFIX}${exitNodeId}`, `${REDIS_HASH_PREFIX}${exitNodeId}`,
"reachableAt", "endpoint",
reachableAt endpoint
); );
} }
} }
@@ -101,14 +101,14 @@ async function processPendingReconnects(): Promise<void> {
`${REDIS_HASH_PREFIX}${id}`, `${REDIS_HASH_PREFIX}${id}`,
"startTime" "startTime"
); );
const reachableAt = await redisManager.hget( const endpoint = await redisManager.hget(
`${REDIS_HASH_PREFIX}${id}`, `${REDIS_HASH_PREFIX}${id}`,
"reachableAt" "endpoint"
); );
if (startTimeStr && reachableAt) { if (startTimeStr && endpoint) {
toProcess.set(id, { toProcess.set(id, {
startTime: parseInt(startTimeStr, 10), startTime: parseInt(startTimeStr, 10),
reachableAt endpoint
}); });
} }
} }
@@ -135,7 +135,7 @@ async function processPendingReconnects(): Promise<void> {
} }
// Check if the exit node HTTP endpoint is reachable // Check if the exit node HTTP endpoint is reachable
const pingUrl = `${entry.reachableAt}/ping`; const pingUrl = `http://${entry.endpoint}/ping`;
try { try {
await axios.get(pingUrl, { timeout: 5000 }); await axios.get(pingUrl, { timeout: 5000 });
} catch { } catch {
@@ -150,47 +150,47 @@ async function processPendingReconnects(): Promise<void> {
`Exit node ${exitNodeId} is reachable. Sending newt/wg/reconnect to connected newts.` `Exit node ${exitNodeId} is reachable. Sending newt/wg/reconnect to connected newts.`
); );
// await sendReconnectToNewts(exitNodeId); await sendReconnectToNewts(exitNodeId);
await removePending(exitNodeId); await removePending(exitNodeId);
} }
} }
// async function sendReconnectToNewts(exitNodeId: number): Promise<void> { async function sendReconnectToNewts(exitNodeId: number): Promise<void> {
// try { try {
// const connectedNewts = await db const connectedNewts = await db
// .select({ newtId: newts.newtId }) .select({ newtId: newts.newtId })
// .from(newts) .from(newts)
// .innerJoin(sites, eq(newts.siteId, sites.siteId)) .innerJoin(sites, eq(newts.siteId, sites.siteId))
// .where(eq(sites.exitNodeId, exitNodeId)); .where(eq(sites.exitNodeId, exitNodeId));
// if (connectedNewts.length === 0) { if (connectedNewts.length === 0) {
// logger.debug( logger.debug(
// `No newts found for exit node ${exitNodeId}, nothing to reconnect` `No newts found for exit node ${exitNodeId}, nothing to reconnect`
// ); );
// return; return;
// } }
// logger.info( logger.info(
// `Sending newt/wg/reconnect to ${connectedNewts.length} newt(s) for exit node ${exitNodeId}` `Sending newt/wg/reconnect to ${connectedNewts.length} newt(s) for exit node ${exitNodeId}`
// ); );
// const reconnectMessage = { const reconnectMessage = {
// type: "newt/wg/reconnect", type: "newt/wg/reconnect",
// data: {} data: {}
// }; };
// await Promise.allSettled( await Promise.allSettled(
// connectedNewts.map(({ newtId }) => connectedNewts.map(({ newtId }) =>
// sendToClient(newtId, reconnectMessage) sendToClient(newtId, reconnectMessage)
// ) )
// ); );
// } catch (error) { } catch (error) {
// logger.error( logger.error(
// `Failed to send reconnect messages for exit node ${exitNodeId}`, `Failed to send reconnect messages for exit node ${exitNodeId}`,
// { error } { error }
// ); );
// } }
// } }
async function removePending(exitNodeId: number): Promise<void> { async function removePending(exitNodeId: number): Promise<void> {
pendingReconnects.delete(exitNodeId); pendingReconnects.delete(exitNodeId);
@@ -40,7 +40,7 @@ export const handleRemoteExitNodePingMessage: MessageHandler = async (
try { try {
// Fetch the current state before updating so we can detect the offline→online transition // Fetch the current state before updating so we can detect the offline→online transition
const [currentExitNode] = await db const [currentExitNode] = await db
.select({ online: exitNodes.online, reachableAt: exitNodes.reachableAt }) .select({ online: exitNodes.online, endpoint: exitNodes.endpoint })
.from(exitNodes) .from(exitNodes)
.where(eq(exitNodes.exitNodeId, remoteExitNode.exitNodeId)) .where(eq(exitNodes.exitNodeId, remoteExitNode.exitNodeId))
.limit(1); .limit(1);
@@ -55,12 +55,18 @@ export const handleRemoteExitNodePingMessage: MessageHandler = async (
.where(eq(exitNodes.exitNodeId, remoteExitNode.exitNodeId)); .where(eq(exitNodes.exitNodeId, remoteExitNode.exitNodeId));
// If the exit node was offline and is now coming online, schedule newt reconnects // If the exit node was offline and is now coming online, schedule newt reconnects
if (currentExitNode && !currentExitNode.online && currentExitNode.reachableAt) { if (
currentExitNode &&
!currentExitNode.online &&
currentExitNode.endpoint
) {
scheduleExitNodeReconnect( scheduleExitNodeReconnect(
remoteExitNode.exitNodeId, remoteExitNode.exitNodeId,
currentExitNode.reachableAt currentExitNode.endpoint
).catch((error) => { ).catch((error) => {
logger.error("Failed to schedule exit node reconnect", { error }); logger.error("Failed to schedule exit node reconnect", {
error
});
}); });
} }
} catch (error) { } catch (error) {