From 90848581a105b2997b0e7e2de625cec3ba452605 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 25 Sep 2026 11:51:38 -0400 Subject: [PATCH] Push gateway updates to clients --- server/lib/rebuildClientAssociations.ts | 40 ++++++++++++ server/routers/olm/gateway.ts | 82 +++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 server/routers/olm/gateway.ts diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts index 012c391bf..02b84c993 100644 --- a/server/lib/rebuildClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -23,6 +23,10 @@ import { import { and, count, eq, inArray, isNotNull, ne } from "drizzle-orm"; import { deletePeersBatch as newtDeletePeersBatch } from "@server/routers/newt/peers"; +import { + sendGatewayDisable, + sendGatewaySitesUpdate +} from "@server/routers/olm/gateway"; import { initPeerAddHandshakeBatch, deletePeersBatch as olmDeletePeersBatch @@ -536,6 +540,19 @@ async function rebuildClientAssociationsFromSiteResourceImpl( ); } + // A client that loses access to a gateway resource (it was deleted, or the + // client's roles/users/machines no longer include it) can't keep using it + // as its gateway. The olm ignores this unless it selected this resource. + if ( + siteResource.mode === "gateway" && + clientSiteResourcesToRemove.length > 0 + ) { + await sendGatewayDisable( + clientSiteResourcesToRemove, + siteResource.siteResourceId + ); + } + /////////// process the client-site associations /////////// logger.debug( @@ -2056,6 +2073,29 @@ export async function handleMessagingForUpdatedSiteResource( ); } + // The olm only knows which gateway resource it selected and the sites it + // is currently using for it, so tell the clients that have access to this + // one what changed. Clients that lost access are handled by the rebuild. + if (existingSiteResource?.mode === "gateway") { + const clientIds = mergedAllClients.map((c) => c.clientId); + if ( + updatedSiteResource.mode !== "gateway" || + !updatedSiteResource.enabled + ) { + await sendGatewayDisable( + clientIds, + updatedSiteResource.siteResourceId + ); + } else { + await sendGatewaySitesUpdate( + clientIds, + updatedSiteResource.siteResourceId, + addedSiteIds, + removedSiteIds + ); + } + } + logger.debug( `handleMessagingForUpdatedSiteResource: DONE siteResourceId=${updatedSiteResource.siteResourceId}` ); diff --git a/server/routers/olm/gateway.ts b/server/routers/olm/gateway.ts new file mode 100644 index 000000000..d6a421c3e --- /dev/null +++ b/server/routers/olm/gateway.ts @@ -0,0 +1,82 @@ +import { sendToClientsBatch } from "#dynamic/routers/ws"; +import { db, olms } from "@server/db"; +import { canCompress } from "@server/lib/clientVersionChecks"; +import logger from "@server/logger"; +import { inArray } from "drizzle-orm"; + +// The olm only tracks which gateway (exit node) site resource it selected, by +// its numeric siteResourceId, and the site IDs that resource currently +// resolves to. So all the server has to push is what changed for that one +// resource; the olm ignores a message whose siteResourceId isn't the one it +// selected, which stops a site added to some other gateway resource from +// being pulled into the client's gateway set. (Creates aren't pushed: a +// client has to select a gateway resource before it can be using it.) + +async function sendGatewayMessageToClients( + clientIds: number[], + type: string, + data: Record +): Promise { + const uniqueClientIds = Array.from(new Set(clientIds)); + if (uniqueClientIds.length === 0) { + return; + } + + const olmRows = await db + .select({ + olmId: olms.olmId, + version: olms.version + }) + .from(olms) + .where(inArray(olms.clientId, uniqueClientIds)); + + const payloads = olmRows.map((olm) => ({ + clientId: olm.olmId, + message: { type, data }, + options: { + compress: canCompress(olm.version, "olm"), + incrementConfigVersion: true + } + })); + + if (payloads.length === 0) { + return; + } + + await sendToClientsBatch(payloads).catch((error) => { + logger.error(`Error sending ${type} messages to olms:`, error); + }); +} + +// Tells the olms of the given clients that sites were added to / removed from +// the gateway site resource, so those that selected it can adjust the set of +// sites they use as the gateway. +export async function sendGatewaySitesUpdate( + clientIds: number[], + siteResourceId: number, + addedSiteIds: number[], + removedSiteIds: number[] +): Promise { + if (addedSiteIds.length === 0 && removedSiteIds.length === 0) { + return; + } + + await sendGatewayMessageToClients( + clientIds, + "olm/wg/gateway/sites/update", + { siteResourceId, addedSiteIds, removedSiteIds } + ); +} + +// Tells the olms of the given clients that the gateway site resource can no +// longer be used as a gateway (it was deleted, disabled, changed to another +// mode, or the client lost access to it), so those that selected it drop out +// of gateway mode. +export async function sendGatewayDisable( + clientIds: number[], + siteResourceId: number +): Promise { + await sendGatewayMessageToClients(clientIds, "olm/wg/gateway/disable", { + siteResourceId + }); +}