mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-17 08:06:38 +00:00
Handle newt online offline with websocket
This commit is contained in:
@@ -89,6 +89,7 @@ export const sites = pgTable("sites", {
|
|||||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||||
type: varchar("type").notNull(), // "newt" or "wireguard"
|
type: varchar("type").notNull(), // "newt" or "wireguard"
|
||||||
online: boolean("online").notNull().default(false),
|
online: boolean("online").notNull().default(false),
|
||||||
|
lastPing: integer("lastPing"),
|
||||||
address: varchar("address"),
|
address: varchar("address"),
|
||||||
endpoint: varchar("endpoint"),
|
endpoint: varchar("endpoint"),
|
||||||
publicKey: varchar("publicKey"),
|
publicKey: varchar("publicKey"),
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const sites = sqliteTable("sites", {
|
|||||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||||
type: text("type").notNull(), // "newt" or "wireguard"
|
type: text("type").notNull(), // "newt" or "wireguard"
|
||||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||||
|
lastPing: integer("lastPing"),
|
||||||
|
|
||||||
// exit node stuff that is how to connect to the site when it has a wg server
|
// exit node stuff that is how to connect to the site when it has a wg server
|
||||||
address: text("address"), // this is the address of the wireguard interface in newt
|
address: text("address"), // this is the address of the wireguard interface in newt
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import {
|
|||||||
OlmSession,
|
OlmSession,
|
||||||
RemoteExitNode,
|
RemoteExitNode,
|
||||||
RemoteExitNodeSession,
|
RemoteExitNodeSession,
|
||||||
remoteExitNodes
|
remoteExitNodes,
|
||||||
|
sites
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
@@ -846,6 +847,31 @@ const setupConnection = async (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle WebSocket protocol-level pings from older newt clients that do
|
||||||
|
// not send application-level "newt/ping" messages. Update the site's
|
||||||
|
// online state and lastPing timestamp so the offline checker treats them
|
||||||
|
// the same as modern newt clients.
|
||||||
|
if (clientType === "newt") {
|
||||||
|
const newtClient = client as Newt;
|
||||||
|
ws.on("ping", async () => {
|
||||||
|
if (!newtClient.siteId) return;
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.update(sites)
|
||||||
|
.set({
|
||||||
|
online: true,
|
||||||
|
lastPing: Math.floor(Date.now() / 1000)
|
||||||
|
})
|
||||||
|
.where(eq(sites.siteId, newtClient.siteId));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"Error updating newt site online state on WS ping",
|
||||||
|
{ error }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ws.on("error", (error: Error) => {
|
ws.on("error", (error: Error) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
`WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`,
|
`WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`,
|
||||||
|
|||||||
@@ -1,105 +1,107 @@
|
|||||||
import { db, sites } from "@server/db";
|
import { db, newts, sites } from "@server/db";
|
||||||
import { disconnectClient, getClientConfigVersion } from "#dynamic/routers/ws";
|
import { hasActiveConnections, getClientConfigVersion } from "#dynamic/routers/ws";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { clients, Newt } from "@server/db";
|
import { Newt } from "@server/db";
|
||||||
import { eq, lt, isNull, and, or } from "drizzle-orm";
|
import { eq, lt, isNull, and, or } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { validateSessionToken } from "@server/auth/sessions/app";
|
|
||||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
|
||||||
import { sendTerminateClient } from "../client/terminate";
|
|
||||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
|
||||||
import { sendNewtSyncMessage } from "./sync";
|
import { sendNewtSyncMessage } from "./sync";
|
||||||
|
|
||||||
// Track if the offline checker interval is running
|
// Track if the offline checker interval is running
|
||||||
// let offlineCheckerInterval: NodeJS.Timeout | null = null;
|
let offlineCheckerInterval: NodeJS.Timeout | null = null;
|
||||||
// const OFFLINE_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds
|
const OFFLINE_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds
|
||||||
// const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
|
const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the background interval that checks for clients that haven't pinged recently
|
* Starts the background interval that checks for newt sites that haven't
|
||||||
* and marks them as offline
|
* pinged recently and marks them as offline. For backward compatibility,
|
||||||
|
* a site is only marked offline when there is no active WebSocket connection
|
||||||
|
* either — so older newt versions that don't send pings but remain connected
|
||||||
|
* continue to be treated as online.
|
||||||
*/
|
*/
|
||||||
// export const startNewtOfflineChecker = (): void => {
|
export const startNewtOfflineChecker = (): void => {
|
||||||
// if (offlineCheckerInterval) {
|
if (offlineCheckerInterval) {
|
||||||
// return; // Already running
|
return; // Already running
|
||||||
// }
|
}
|
||||||
|
|
||||||
// offlineCheckerInterval = setInterval(async () => {
|
offlineCheckerInterval = setInterval(async () => {
|
||||||
// try {
|
try {
|
||||||
// const twoMinutesAgo = Math.floor(
|
const twoMinutesAgo = Math.floor(
|
||||||
// (Date.now() - OFFLINE_THRESHOLD_MS) / 1000
|
(Date.now() - OFFLINE_THRESHOLD_MS) / 1000
|
||||||
// );
|
);
|
||||||
|
|
||||||
// // TODO: WE NEED TO MAKE SURE THIS WORKS WITH DISTRIBUTED NODES ALL DOING THE SAME THING
|
// Find all online newt-type sites that haven't pinged recently
|
||||||
|
// (or have never pinged at all). Join newts to obtain the newtId
|
||||||
|
// needed for the WebSocket connection check.
|
||||||
|
const staleSites = await db
|
||||||
|
.select({
|
||||||
|
siteId: sites.siteId,
|
||||||
|
newtId: newts.newtId,
|
||||||
|
lastPing: sites.lastPing
|
||||||
|
})
|
||||||
|
.from(sites)
|
||||||
|
.innerJoin(newts, eq(newts.siteId, sites.siteId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.online, true),
|
||||||
|
eq(sites.type, "newt"),
|
||||||
|
or(
|
||||||
|
lt(sites.lastPing, twoMinutesAgo),
|
||||||
|
isNull(sites.lastPing)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// // Find clients that haven't pinged in the last 2 minutes and mark them as offline
|
for (const staleSite of staleSites) {
|
||||||
// const offlineClients = await db
|
// Backward-compatibility check: if the newt still has an
|
||||||
// .update(clients)
|
// active WebSocket connection (older clients that don't send
|
||||||
// .set({ online: false })
|
// pings), keep the site online.
|
||||||
// .where(
|
const isConnected = await hasActiveConnections(staleSite.newtId);
|
||||||
// and(
|
if (isConnected) {
|
||||||
// eq(clients.online, true),
|
logger.debug(
|
||||||
// or(
|
`Newt ${staleSite.newtId} has not pinged recently but is still connected via WebSocket — keeping site ${staleSite.siteId} online`
|
||||||
// lt(clients.lastPing, twoMinutesAgo),
|
);
|
||||||
// isNull(clients.lastPing)
|
continue;
|
||||||
// )
|
}
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// .returning();
|
|
||||||
|
|
||||||
// for (const offlineClient of offlineClients) {
|
logger.info(
|
||||||
// logger.info(
|
`Marking site ${staleSite.siteId} offline: newt ${staleSite.newtId} has no recent ping and no active WebSocket connection`
|
||||||
// `Kicking offline newt client ${offlineClient.clientId} due to inactivity`
|
);
|
||||||
// );
|
|
||||||
|
|
||||||
// if (!offlineClient.newtId) {
|
await db
|
||||||
// logger.warn(
|
.update(sites)
|
||||||
// `Offline client ${offlineClient.clientId} has no newtId, cannot disconnect`
|
.set({ online: false })
|
||||||
// );
|
.where(eq(sites.siteId, staleSite.siteId));
|
||||||
// continue;
|
}
|
||||||
// }
|
} catch (error) {
|
||||||
|
logger.error("Error in newt offline checker interval", { error });
|
||||||
|
}
|
||||||
|
}, OFFLINE_CHECK_INTERVAL);
|
||||||
|
|
||||||
// // Send a disconnect message to the client if connected
|
logger.debug("Started newt offline checker interval");
|
||||||
// try {
|
};
|
||||||
// await sendTerminateClient(
|
|
||||||
// offlineClient.clientId,
|
|
||||||
// offlineClient.newtId
|
|
||||||
// ); // terminate first
|
|
||||||
// // wait a moment to ensure the message is sent
|
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
// await disconnectClient(offlineClient.newtId);
|
|
||||||
// } catch (error) {
|
|
||||||
// logger.error(
|
|
||||||
// `Error sending disconnect to offline newt ${offlineClient.clientId}`,
|
|
||||||
// { error }
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// logger.error("Error in offline checker interval", { error });
|
|
||||||
// }
|
|
||||||
// }, OFFLINE_CHECK_INTERVAL);
|
|
||||||
|
|
||||||
// logger.debug("Started offline checker interval");
|
|
||||||
// };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the background interval that checks for offline clients
|
* Stops the background interval that checks for offline newt sites.
|
||||||
*/
|
*/
|
||||||
// export const stopNewtOfflineChecker = (): void => {
|
export const stopNewtOfflineChecker = (): void => {
|
||||||
// if (offlineCheckerInterval) {
|
if (offlineCheckerInterval) {
|
||||||
// clearInterval(offlineCheckerInterval);
|
clearInterval(offlineCheckerInterval);
|
||||||
// offlineCheckerInterval = null;
|
offlineCheckerInterval = null;
|
||||||
// logger.info("Stopped offline checker interval");
|
logger.info("Stopped newt offline checker interval");
|
||||||
// }
|
}
|
||||||
// };
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles ping messages from clients and responds with pong
|
* Handles ping messages from newt clients.
|
||||||
|
*
|
||||||
|
* On each ping:
|
||||||
|
* - Marks the associated site as online.
|
||||||
|
* - Records the current timestamp as the newt's last-ping time.
|
||||||
|
* - Triggers a config sync if the newt is running an outdated config version.
|
||||||
|
* - Responds with a pong message.
|
||||||
*/
|
*/
|
||||||
export const handleNewtPingMessage: MessageHandler = async (context) => {
|
export const handleNewtPingMessage: MessageHandler = async (context) => {
|
||||||
const { message, client: c, sendToClient } = context;
|
const { message, client: c } = context;
|
||||||
const newt = c as Newt;
|
const newt = c as Newt;
|
||||||
|
|
||||||
if (!newt) {
|
if (!newt) {
|
||||||
@@ -112,15 +114,31 @@ export const handleNewtPingMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the version
|
try {
|
||||||
|
// Mark the site as online and record the ping timestamp.
|
||||||
|
await db
|
||||||
|
.update(sites)
|
||||||
|
.set({
|
||||||
|
online: true,
|
||||||
|
lastPing: Math.floor(Date.now() / 1000)
|
||||||
|
})
|
||||||
|
.where(eq(sites.siteId, newt.siteId));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error updating online state on newt ping", { error });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check config version and sync if stale.
|
||||||
const configVersion = await getClientConfigVersion(newt.newtId);
|
const configVersion = await getClientConfigVersion(newt.newtId);
|
||||||
|
|
||||||
if (message.configVersion && configVersion != null && configVersion != message.configVersion) {
|
if (
|
||||||
|
message.configVersion != null &&
|
||||||
|
configVersion != null &&
|
||||||
|
configVersion !== message.configVersion
|
||||||
|
) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Newt ping with outdated config version: ${message.configVersion} (current: ${configVersion})`
|
`Newt ping with outdated config version: ${message.configVersion} (current: ${configVersion})`
|
||||||
);
|
);
|
||||||
|
|
||||||
// get the site
|
|
||||||
const [site] = await db
|
const [site] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
@@ -137,19 +155,6 @@ export const handleNewtPingMessage: MessageHandler = async (context) => {
|
|||||||
await sendNewtSyncMessage(newt, site);
|
await sendNewtSyncMessage(newt, site);
|
||||||
}
|
}
|
||||||
|
|
||||||
// try {
|
|
||||||
// // Update the client's last ping timestamp
|
|
||||||
// await db
|
|
||||||
// .update(clients)
|
|
||||||
// .set({
|
|
||||||
// lastPing: Math.floor(Date.now() / 1000),
|
|
||||||
// online: true
|
|
||||||
// })
|
|
||||||
// .where(eq(clients.clientId, newt.clientId));
|
|
||||||
// } catch (error) {
|
|
||||||
// logger.error("Error handling ping message", { error });
|
|
||||||
// }
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
type: "pong",
|
type: "pong",
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
handleDockerContainersMessage,
|
handleDockerContainersMessage,
|
||||||
handleNewtPingRequestMessage,
|
handleNewtPingRequestMessage,
|
||||||
handleApplyBlueprintMessage,
|
handleApplyBlueprintMessage,
|
||||||
handleNewtPingMessage
|
handleNewtPingMessage,
|
||||||
|
startNewtOfflineChecker
|
||||||
} from "../newt";
|
} from "../newt";
|
||||||
import {
|
import {
|
||||||
handleOlmRegisterMessage,
|
handleOlmRegisterMessage,
|
||||||
@@ -43,3 +44,4 @@ export const messageHandlers: Record<string, MessageHandler> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
startOlmOfflineChecker(); // this is to handle the offline check for olms
|
startOlmOfflineChecker(); // this is to handle the offline check for olms
|
||||||
|
startNewtOfflineChecker(); // this is to handle the offline check for newts
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import zlib from "zlib";
|
|||||||
import { Server as HttpServer } from "http";
|
import { Server as HttpServer } from "http";
|
||||||
import { WebSocket, WebSocketServer } from "ws";
|
import { WebSocket, WebSocketServer } from "ws";
|
||||||
import { Socket } from "net";
|
import { Socket } from "net";
|
||||||
import { Newt, newts, NewtSession, olms, Olm, OlmSession } from "@server/db";
|
import { Newt, newts, NewtSession, olms, Olm, OlmSession, sites } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
|
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
|
||||||
@@ -380,6 +380,31 @@ const setupConnection = async (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle WebSocket protocol-level pings from older newt clients that do
|
||||||
|
// not send application-level "newt/ping" messages. Update the site's
|
||||||
|
// online state and lastPing timestamp so the offline checker treats them
|
||||||
|
// the same as modern newt clients.
|
||||||
|
if (clientType === "newt") {
|
||||||
|
const newtClient = client as Newt;
|
||||||
|
ws.on("ping", async () => {
|
||||||
|
if (!newtClient.siteId) return;
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.update(sites)
|
||||||
|
.set({
|
||||||
|
online: true,
|
||||||
|
lastPing: Math.floor(Date.now() / 1000)
|
||||||
|
})
|
||||||
|
.where(eq(sites.siteId, newtClient.siteId));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"Error updating newt site online state on WS ping",
|
||||||
|
{ error }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ws.on("error", (error: Error) => {
|
ws.on("error", (error: Error) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
`WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`,
|
`WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`,
|
||||||
|
|||||||
Reference in New Issue
Block a user