From 9e104286406bcd5d506f59ad91821f317a5dc827 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 31 Jul 2026 17:22:08 -0400 Subject: [PATCH] Handle the alias create and update special case exit nodes --- server/lib/rebuildClientAssociations.ts | 79 ++++++++++++++++++- .../siteResource/createSiteResource.ts | 9 ++- .../siteResource/updateSiteResource.ts | 9 ++- 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts index ce0897aab..194bfe446 100644 --- a/server/lib/rebuildClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -1137,7 +1137,9 @@ async function syncClientExitNodeConnections( ) ); - const needsConnectSet = new Set(requiresExitNodeRows.map((r) => r.clientId)); + const needsConnectSet = new Set( + requiresExitNodeRows.map((r) => r.clientId) + ); const exitNodeIds = Array.from( new Set( @@ -1249,6 +1251,61 @@ async function syncClientExitNodeConnections( } } +// Notifies the olms of every given client that the alias of the site resource +// they're using an exit node connection for has changed, via the dedicated +// exit node data-update message. Unlike syncClientExitNodeConnections, this +// doesn't touch connect/disconnect state - it's purely a rename for clients +// that are (and remain) connected to the exit node for this resource. +async function syncClientExitNodeAliasUpdate( + clientIds: number[], + oldAlias: string | null, + newAlias: string | null, + trx: Transaction | typeof db = db +): Promise { + const uniqueClientIds = Array.from(new Set(clientIds)); + if (uniqueClientIds.length === 0) { + return; + } + + const oldAliases = oldAlias ? [oldAlias] : []; + const newAliases = newAlias ? [newAlias] : []; + if (oldAliases.length === 0 && newAliases.length === 0) { + return; + } + + const olmRows = await trx + .select({ + clientId: olms.clientId, + olmId: olms.olmId, + version: olms.version + }) + .from(olms) + .where(inArray(olms.clientId, uniqueClientIds)); + + const updatePayloads = olmRows + .filter((r) => r.clientId !== null) + .map((olm) => ({ + clientId: olm.olmId, + message: { + type: "olm/wg/exitnode/data/update", + data: { + oldAliases, + newAliases + } + }, + options: { compress: canCompress(olm.version, "olm") } + })); + + if (updatePayloads.length > 0) { + await sendToClientsBatch(updatePayloads).catch((error) => { + logger.error( + `rebuildClientAssociations: Error sending exit node alias update messages:`, + error + ); + }); + } +} + async function handleSubnetProxyTargetUpdates( siteResource: SiteResource, sitesList: Site[], @@ -1479,7 +1536,7 @@ export async function handleMessagingForUpdatedSiteResource( `handleMessagingForUpdatedSiteResource: fetched newts for ${newtsForSites.length}/${allSiteIds.length} site(s)` ); - // WARNING: THIS RELIES ON THE CACHE TABLES BEING UP TO DATE, SO CALL THIS AFTER THE ASSOCIATION CACHE IS UPDATED + // !!!!!!!!!!!!!!!!!! WARNING: THIS RELIES ON THE CACHE TABLES BEING UP TO DATE, SO CALL THIS AFTER THE ASSOCIATION CACHE IS UPDATED !!!!!!!!!!!!!!!!!! const mergedAllClients = await trx .select({ clientId: clientSiteResourcesAssociationsCache.clientId, @@ -1906,6 +1963,24 @@ export async function handleMessagingForUpdatedSiteResource( ); } + // For a resource that stays on an exit node connection across the update, + // the alias is the only field that affects already-connected clients (the + // exit node itself, its endpoint, etc. are not per-resource). Tell those + // clients' olms about the rename directly via the exit node data-update + // message rather than a full connect/disconnect cycle. + if ( + existingSiteResource?.requiresExitNodeConnection && + updatedSiteResource.requiresExitNodeConnection && + aliasChanged + ) { + await syncClientExitNodeAliasUpdate( + mergedAllClients.map((c) => c.clientId), + existingSiteResource.alias, + updatedSiteResource.alias, + trx + ); + } + // If this resource requires (or required) clients to be connected to the // exit node (e.g. an inference resource), re-sync connect/disconnect // state for every client currently associated with it - covers toggling diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index d220a4b08..1a4cd9a4d 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -29,7 +29,7 @@ import response from "@server/lib/response"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; import HttpCode from "@server/types/HttpCode"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, inArray, ne } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -490,7 +490,11 @@ export async function createSiteResource( .where( and( eq(siteResources.orgId, orgId), - eq(siteResources.alias, alias.trim()) + eq(siteResources.alias, alias.trim()), + ne( + siteResources.requiresExitNodeConnection, + mode == "inference" + ) // exclude looking at the ones on exit nodes if this is an inference resource ) ) .limit(1); @@ -527,6 +531,7 @@ export async function createSiteResource( let aliasAddress: string | null = null; let releaseAliasLock: (() => Promise) | null = null; if (mode === "host" || mode === "http" || mode === "ssh") { + // no alias address but we do have an alias for inference const { value, release } = await getNextAvailableAliasAddress(orgId); aliasAddress = value; diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 1b79e7b16..0ab928d04 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -507,7 +507,11 @@ export async function updateSiteResource( and( eq(siteResources.orgId, existingSiteResource.orgId), eq(siteResources.alias, alias.trim()), - ne(siteResources.siteResourceId, siteResourceId) // exclude self + ne(siteResources.siteResourceId, siteResourceId), // exclude self + ne( + siteResources.requiresExitNodeConnection, + mode == "inference" + ) // exclude looking at the ones on exit nodes if this is an inference resource ) ) .limit(1); @@ -587,8 +591,7 @@ export async function updateSiteResource( domainId, subdomain: finalSubdomain, fullDomain, - networkId: - mode === "inference" ? null : undefined, + networkId: mode === "inference" ? null : undefined, requiresExitNodeConnection: mode !== undefined ? mode === "inference" : undefined, ...sshPamSet