From 73a4d7d3515d6c91ac61795399212e456a6c0a3e Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 May 2026 11:57:02 -0700 Subject: [PATCH 01/13] Quiet log message --- server/lib/rebuildClientAssociations.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts index a5c9a9321..a7a134f6d 100644 --- a/server/lib/rebuildClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -20,9 +20,7 @@ import { } from "@server/db"; import { and, eq, inArray, ne } from "drizzle-orm"; -import { - deletePeer as newtDeletePeer -} from "@server/routers/newt/peers"; +import { deletePeer as newtDeletePeer } from "@server/routers/newt/peers"; import { initPeerAddHandshake, deletePeer as olmDeletePeer @@ -33,7 +31,7 @@ import { generateAliasConfig, generateRemoteSubnets, generateSubnetProxyTargetV2, - parseEndpoint, + parseEndpoint } from "@server/lib/ip"; import { addPeerData, @@ -51,10 +49,7 @@ export async function getClientSiteResourceAccess( ? await trx .select() .from(sites) - .innerJoin( - siteNetworks, - eq(siteNetworks.siteId, sites.siteId) - ) + .innerJoin(siteNetworks, eq(siteNetworks.siteId, sites.siteId)) .where(eq(siteNetworks.networkId, siteResource.networkId)) .then((rows) => rows.map((row) => row.sites)) : []; @@ -362,7 +357,8 @@ export async function rebuildClientAssociationsFromSiteResource( .where(inArray(clients.clientId, existingClientSiteIds)) : []; - const otherResourceClientIds = clientsFromOtherResourcesBySite.get(siteId) ?? new Set(); + const otherResourceClientIds = + clientsFromOtherResourcesBySite.get(siteId) ?? new Set(); logger.debug( `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} otherResourceClientIds=[${[...otherResourceClientIds].join(", ")}] mergedAllClientIds=[${mergedAllClientIds.join(", ")}]` @@ -709,7 +705,7 @@ export async function updateClientSiteDestinations( sourcePort: destination.sourcePort, destinations: destination.destinations }; - logger.info( + logger.debug( `Payload for update-destinations: ${JSON.stringify(payload, null, 2)}` ); From c16d2ff2ed7794aef3d5b7f3f76f0161b6889a23 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 May 2026 13:52:35 -0700 Subject: [PATCH 02/13] Fix log message --- server/routers/olm/handleOlmServerInitAddPeerHandshake.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/olm/handleOlmServerInitAddPeerHandshake.ts b/server/routers/olm/handleOlmServerInitAddPeerHandshake.ts index 05a83a146..b3aaffb93 100644 --- a/server/routers/olm/handleOlmServerInitAddPeerHandshake.ts +++ b/server/routers/olm/handleOlmServerInitAddPeerHandshake.ts @@ -17,7 +17,7 @@ import { initPeerAddHandshake } from "./peers"; export const handleOlmServerInitAddPeerHandshake: MessageHandler = async ( context ) => { - logger.info("Handling register olm message!"); + logger.info("Handle Olm Server Init Add Peer Handshake Message"); const { message, client: c, sendToClient } = context; const olm = c as Olm; From 92a06e0ea34eb5387e257a9b6463174a5d4f0bee Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 May 2026 14:00:20 -0700 Subject: [PATCH 03/13] Handle jit mode with syncs --- server/routers/olm/buildConfiguration.ts | 18 +++++------ server/routers/olm/sync.ts | 39 ++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/server/routers/olm/buildConfiguration.ts b/server/routers/olm/buildConfiguration.ts index 4182725d3..640031bca 100644 --- a/server/routers/olm/buildConfiguration.ts +++ b/server/routers/olm/buildConfiguration.ts @@ -27,11 +27,11 @@ export async function buildSiteConfigurationForOlmClient( ) { const siteConfigurations: { siteId: number; - name?: string - endpoint?: string - publicKey?: string - serverIP?: string | null - serverPort?: number | null + name?: string; + endpoint?: string; + publicKey?: string; + serverIP?: string | null; + serverPort?: number | null; remoteSubnets?: string[]; aliases: Alias[]; }[] = []; @@ -79,7 +79,6 @@ export async function buildSiteConfigurationForOlmClient( ) ); - if (jitMode) { // Add site configuration to the array siteConfigurations.push({ @@ -109,10 +108,9 @@ export async function buildSiteConfigurationForOlmClient( continue; } - if (!site.publicKey || site.publicKey == "") { // the site is not ready to accept new peers - logger.warn( - `Site ${site.siteId} has no public key, skipping` - ); + if (!site.publicKey || site.publicKey == "") { + // the site is not ready to accept new peers + logger.warn(`Site ${site.siteId} has no public key, skipping`); continue; } diff --git a/server/routers/olm/sync.ts b/server/routers/olm/sync.ts index c994b2c73..46e1fbd88 100644 --- a/server/routers/olm/sync.ts +++ b/server/routers/olm/sync.ts @@ -9,16 +9,50 @@ import { import { buildSiteConfigurationForOlmClient } from "./buildConfiguration"; import { sendToClient } from "#dynamic/routers/ws"; import logger from "@server/logger"; -import { eq, inArray } from "drizzle-orm"; +import { count, eq, inArray } from "drizzle-orm"; import config from "@server/lib/config"; import { canCompress } from "@server/lib/clientVersionChecks"; +import { build } from "@server/build"; export async function sendOlmSyncMessage(olm: Olm, client: Client) { + // Get all sites data + const sitesCountResult = await db + .select({ count: count() }) + .from(sites) + .innerJoin( + clientSitesAssociationsCache, + eq(sites.siteId, clientSitesAssociationsCache.siteId) + ) + .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); + + // Extract the count value from the result array + const sitesCount = + sitesCountResult.length > 0 ? sitesCountResult[0].count : 0; + + // Prepare an array to store site configurations + logger.debug( + `[handleOlmRegisterMessage] Found ${sitesCount} sites for client ${client.clientId}`, + { orgId: client.orgId } + ); + + let jitMode = false; + if (sitesCount > 250 && build == "saas") { + // THIS IS THE MAX ON THE BUSINESS TIER + // we have too many sites + // If we have too many sites we need to drop into fully JIT mode by not sending any of the sites + logger.info( + `[handleOlmRegisterMessage] Too many sites (${sitesCount}), dropping into JIT mode`, + { orgId: client.orgId } + ); + jitMode = true; + } + // NOTE: WE ARE HARDCODING THE RELAY PARAMETER TO FALSE HERE BUT IN THE REGISTER MESSAGE ITS DEFINED BY THE CLIENT const siteConfigurations = await buildSiteConfigurationForOlmClient( client, client.pubKey, - false + false, + jitMode ); // Get all exit nodes from sites where the client has peers @@ -82,7 +116,6 @@ export async function sendOlmSyncMessage(olm: Olm, client: Client) { exitNodes: exitNodesData } }, - { compress: canCompress(olm.version, "olm") } From 02ba2393b93244744d10e6cf6dae6c8156f49379 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 14 May 2026 11:08:12 -0700 Subject: [PATCH 04/13] New translations en-us.json (German) [ci skip] --- messages/de-DE.json | 84 ++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/messages/de-DE.json b/messages/de-DE.json index 504005334..386fff46f 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -22,11 +22,11 @@ "componentsMember": "Du bist Mitglied von {count, plural, =0 {keiner Organisation} one {einer Organisation} other {# Organisationen}}.", "componentsInvalidKey": "Ungültige oder abgelaufene Lizenzschlüssel erkannt. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.", "dismiss": "Verwerfen", - "subscriptionViolationMessage": "Sie überschreiten Ihre Grenzen für Ihr aktuelles Paket. Korrigieren Sie das Problem, indem Sie Webseiten, Benutzer oder andere Ressourcen entfernen, um in Ihrem Paket zu bleiben.", + "subscriptionViolationMessage": "Sie überschreiten Ihre Grenzen für Ihr aktuelles Paket. Korrigieren Sie das Problem, indem Sie Standorte, Benutzer oder andere Ressourcen entfernen, um in Ihrem Paket zu bleiben.", "trialBannerMessage": "Ihre Testversion läuft in {countdown} ab. Upgraden, um den Zugriff zu behalten.", "trialBannerExpired": "Ihre Testversion ist abgelaufen. Jetzt upgraden, um den Zugriff wiederherzustellen.", "billingTrialBannerTitle": "Kostenlose Testversion aktiv", - "billingTrialBannerDescription": "Sie nutzen derzeit eine kostenlose Testversion auf der Geschäftsstufe. Wenn die Testversion endet, wird Ihr Konto automatisch auf die Funktionen und Beschränkungen der Basisstufe zurückgesetzt. Upgraden Sie jederzeit, um weiterhin Zugriff auf die Funktionen Ihres aktuellen Plans zu behalten.", + "billingTrialBannerDescription": "Sie nutzen derzeit eine kostenlose Testversion auf der Business-Tarif. Wenn die Testversion endet, wird Ihr Konto automatisch auf die Funktionen und Beschränkungen der Basis-Tarif zurückgesetzt. Upgraden Sie jederzeit, um weiterhin Zugriff auf die Funktionen Ihres aktuellen Plans zu behalten.", "billingTrialBannerUpgrade": "Jetzt upgraden", "billingTrialBadge": "Kostenlose Testversion", "trialActive": "Kostenlose Testversion aktiv", @@ -34,8 +34,8 @@ "trialHasEnded": "Ihre Testversion ist beendet.", "trialDaysRemaining": "{count, plural, one {# Tag übrig} other {# Tage übrig}}", "trialDaysLeftShort": "Noch {days}d in der Testversion", - "trialGoToBilling": "Zur Rechnungsseite gehen", - "subscriptionViolationViewBilling": "Rechnung anzeigen", + "trialGoToBilling": "Zur Abrechnung gehen", + "subscriptionViolationViewBilling": "Abrechnung anzeigen", "componentsLicenseViolation": "Lizenzverstoß: Dieser Server benutzt {usedSites} Standorte, was das Lizenzlimit von {maxSites} Standorten überschreitet. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.", "componentsSupporterMessage": "Vielen Dank für die Unterstützung von Pangolin als {tier}!", "inviteErrorNotValid": "Es tut uns leid, aber es sieht so aus, als wäre die Einladung, auf die du zugreifen möchtest, entweder nicht angenommen worden oder nicht mehr gültig.", @@ -67,7 +67,7 @@ "edit": "Bearbeiten", "siteConfirmDelete": "Löschen des Standorts bestätigen", "siteDelete": "Standort löschen", - "siteMessageRemove": "Sobald der Standort entfernt ist, wird sie nicht mehr zugänglich sein. Alle mit dem Standort verbundenen Ziele werden ebenfalls entfernt.", + "siteMessageRemove": "Sobald der Standort entfernt ist, wird er nicht mehr zugänglich sein. Alle mit dem Standort verbundenen Ziele werden ebenfalls entfernt.", "siteQuestionRemove": "Sind Sie sicher, dass Sie den Standort aus der Organisation entfernen möchten?", "siteManageSites": "Standorte verwalten", "siteDescription": "Erstellen und Verwalten von Standorten, um die Verbindung zu privaten Netzwerken zu ermöglichen", @@ -117,20 +117,20 @@ "siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren", "siteSettingDescription": "Standorteinstellungen konfigurieren", "siteResourcesTab": "Ressourcen", - "siteResourcesNoneOnSite": "Diese Seite hat noch keine öffentlichen oder privaten Ressourcen.", + "siteResourcesNoneOnSite": "Dieser Standort hat noch keine öffentlichen oder privaten Ressourcen", "siteResourcesSectionPublic": "Öffentliche Ressourcen", "siteResourcesSectionPrivate": "Private Ressourcen", "siteResourcesSectionPublicDescription": "Ressourcen, die extern über Domains oder Ports bereitgestellt werden.", - "siteResourcesSectionPrivateDescription": "Ressourcen, die in Ihrem privaten Netzwerk über die Seite verfügbar sind.", + "siteResourcesSectionPrivateDescription": "Ressourcen, die in Ihrem privaten Netzwerk über den Standort verfügbar sind.", "siteResourcesViewAllPublic": "Alle Ressourcen anzeigen", "siteResourcesViewAllPrivate": "Alle Ressourcen anzeigen", - "siteResourcesDialogDescription": "Überblick über öffentliche und private Ressourcen, die mit dieser Seite verbunden sind.", + "siteResourcesDialogDescription": "Überblick über öffentliche und private Ressourcen, die mit diesem Standort verbunden sind.", "siteResourcesShowMore": "Mehr anzeigen", "siteResourcesPermissionDenied": "Sie haben keine Berechtigung, diese Ressourcen aufzulisten.", - "siteResourcesEmptyPublic": "Noch sind keine öffentlichen Ressourcen für diese Seite vorhanden.", - "siteResourcesEmptyPrivate": "Noch sind keine privaten Ressourcen mit dieser Seite verbunden.", + "siteResourcesEmptyPublic": "Noch sind keine öffentlichen Ressourcen für diesen Standort vorhanden.", + "siteResourcesEmptyPrivate": "Noch sind keine privaten Ressourcen mit diesem Standort verbunden.", "siteResourcesHowToAccess": "Zugriffsmöglichkeiten", - "siteResourcesTargetsOnSite": "Ziele auf dieser Seite", + "siteResourcesTargetsOnSite": "Ziele an diesem Standort", "siteSetting": "{siteName} Einstellungen", "siteNewtTunnel": "Newt Standort (empfohlen)", "siteNewtTunnelDescription": "Einfachster Weg, einen Einstiegspunkt in jedes Netzwerk zu erstellen. Keine zusätzliche Einrichtung.", @@ -148,10 +148,10 @@ "siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.", "siteInfo": "Standortinformationen", "status": "Status", - "shareTitle": "Links zum Teilen verwalten", + "shareTitle": "Freigabelinks verwalten", "shareDescription": "Erstelle teilbare Links, um temporären oder permanenten Zugriff auf Proxy-Ressourcen zu gewähren", - "shareSearch": "Freigabe-Links suchen...", - "shareCreate": "Link erstellen", + "shareSearch": "Freigabelinks suchen...", + "shareCreate": "Freigabelink erstellen", "shareErrorDelete": "Link konnte nicht gelöscht werden", "shareErrorDeleteMessage": "Fehler beim Löschen des Links", "shareDeleted": "Link gelöscht", @@ -161,7 +161,7 @@ "shareQuestionRemove": "Sind Sie sicher, dass Sie diesen Freigabelink löschen möchten?", "shareMessageRemove": "Nach dem Löschen funktioniert der Link nicht mehr, und jeder, der ihn nutzt, verliert den Zugriff auf die Ressource.", "shareTokenDescription": "Das Zugriffstoken kann auf zwei Arten übergeben werden: als Abfrageparameter oder in den Request-Headern. Diese müssen vom Client auf jeder Anfrage für authentifizierten Zugriff weitergegeben werden.", - "accessToken": "Zugangs-Token", + "accessToken": "Zugriffstoken", "usageExamples": "Nutzungsbeispiele", "tokenId": "Token-ID", "requestHeades": "Anfrage-Header", @@ -172,12 +172,12 @@ "shareTokenSecurety": "Bewahren Sie das Zugriffstoken sicher. Teilen Sie es nicht in öffentlich zugänglichen Bereichen oder Client-seitigem Code.", "shareErrorFetchResource": "Fehler beim Abrufen der Ressourcen", "shareErrorFetchResourceDescription": "Beim Abrufen der Ressourcen ist ein Fehler aufgetreten", - "shareErrorCreate": "Fehler beim Erstellen des Teilen-Links", - "shareErrorCreateDescription": "Beim Erstellen des Teilen-Links ist ein Fehler aufgetreten", + "shareErrorCreate": "Fehler beim Erstellen des Freigabelinks", + "shareErrorCreateDescription": "Beim Erstellen des Freigabelinks ist ein Fehler aufgetreten", "shareCreateDescription": "Jeder mit diesem Link kann auf die Ressource zugreifen", "shareTitleOptional": "Titel (optional)", - "expireIn": "Verfällt in", - "neverExpire": "Nie ablaufen", + "expireIn": "Läuft ab in", + "neverExpire": "Läuft nie ab", "shareExpireDescription": "Ablaufzeit ist, wie lange der Link verwendet werden kann und bietet Zugriff auf die Ressource. Nach dieser Zeit wird der Link nicht mehr funktionieren und Benutzer, die diesen Link benutzt haben, verlieren den Zugriff auf die Ressource.", "shareSeeOnce": "Sie können diesen Link nur einmal sehen. Bitte kopieren Sie ihn.", "shareAccessHint": "Jeder mit diesem Link kann auf die Ressource zugreifen. Teilen Sie sie mit Vorsicht.", @@ -186,7 +186,7 @@ "resourcesNotFound": "Keine Ressourcen gefunden", "resourceSearch": "Suche Ressourcen", "machineSearch": "Maschinen suchen", - "machinesSearch": "Suche Maschinen-Klienten...", + "machinesSearch": "Maschinen-Clients suchen", "machineNotFound": "Keine Maschinen gefunden", "userDeviceSearch": "Benutzergeräte durchsuchen", "userDevicesSearch": "Benutzergeräte durchsuchen...", @@ -203,7 +203,7 @@ "proxyResourcesBannerDescription": "Öffentliche Ressourcen sind HTTPS oder TCP/UDP-Proxys, die über einen Webbrowser für jeden zugänglich sind. Im Gegensatz zu privaten Ressourcen benötigen sie keine Client-seitige Software und können Identitäts- und kontextbezogene Zugriffsrichtlinien beinhalten.", "clientResourceTitle": "Private Ressourcen verwalten", "clientResourceDescription": "Erstelle und verwalte Ressourcen, die nur über einen verbundenen Client zugänglich sind", - "privateResourcesBannerTitle": "Zero-Trust Privater Zugang", + "privateResourcesBannerTitle": "Zero-Trust-Zugriff auf private Ressourcen", "privateResourcesBannerDescription": "Private Ressourcen nutzen Zero-Trust und stellen sicher, dass Benutzer und Maschinen nur auf Ressourcen zugreifen können, die Sie explizit gewähren. Verbinden Sie Benutzergeräte oder Maschinen-Clients, um auf diese Ressourcen über ein sicheres virtuelles privates Netzwerk zuzugreifen.", "resourcesSearch": "Suche Ressourcen...", "resourceAdd": "Ressource hinzufügen", @@ -265,7 +265,7 @@ "rules": "Regeln", "resourceSettingDescription": "Einstellungen für die Ressource konfigurieren", "resourceSetting": "{resourceName} Einstellungen", - "alwaysAllow": "Auth umgehen", + "alwaysAllow": "Authentifizierung umgehen", "alwaysDeny": "Zugriff blockieren", "passToAuth": "Weiterleiten zur Authentifizierung", "orgSettingsDescription": "Organisationseinstellungen konfigurieren", @@ -274,7 +274,7 @@ "saveGeneralSettings": "Allgemeine Einstellungen speichern", "saveSettings": "Einstellungen speichern", "orgDangerZone": "Gefahrenzone", - "orgDangerZoneDescription": "Sobald Sie diesen Org löschen, gibt es kein Zurück mehr. Bitte seien Sie vorsichtig.", + "orgDangerZoneDescription": "Sobald Sie diese Organisation löschen, gibt es kein Zurück mehr. Bitte seien Sie vorsichtig.", "orgDelete": "Organisation löschen", "orgDeleteConfirm": "Organisation löschen bestätigen", "orgMessageRemove": "Diese Aktion ist unwiderruflich und löscht alle zugehörigen Daten.", @@ -323,7 +323,7 @@ "accessApprovalsManage": "Genehmigungen verwalten", "accessApprovalsDescription": "Zeige und verwalte ausstehende Genehmigungen für den Zugriff auf diese Organisation", "description": "Beschreibung", - "inviteTitle": "Einladungen öffnen", + "inviteTitle": "Offene Einladungen", "inviteDescription": "Einladungen für andere Benutzer verwalten, der Organisation beizutreten", "inviteSearch": "Einladungen suchen...", "minutes": "Minuten", @@ -370,12 +370,12 @@ "apiKeysDescription": "API-Schlüssel werden zur Authentifizierung mit der Integrations-API verwendet", "provisioningKeysTitle": "Bereitstellungsschlüssel", "provisioningKeysManage": "Bereitstellungsschlüssel verwalten", - "provisioningKeysDescription": "Bereitstellungsschlüssel werden verwendet, um die automatisierte Bereitstellung von Seiten für Ihr Unternehmen zu authentifizieren.", + "provisioningKeysDescription": "Bereitstellungsschlüssel werden verwendet, um die automatisierte Bereitstellung von Standorten für Ihr Unternehmen zu authentifizieren.", "provisioningManage": "Bereitstellung", - "provisioningDescription": "Bereitstellungsschlüssel verwalten und ausstehende Seiten prüfen, die noch auf Genehmigung warten.", - "pendingSites": "Ausstehende Seiten", - "siteApproveSuccess": "Site erfolgreich freigegeben", - "siteApproveError": "Fehler beim Bestätigen der Seite", + "provisioningDescription": "Bereitstellungsschlüssel verwalten und ausstehende Standorte prüfen, die noch auf Genehmigung warten.", + "pendingSites": "Ausstehende Standorte", + "siteApproveSuccess": "Standort erfolgreich freigegeben", + "siteApproveError": "Fehler beim Genehmigen des Standorts", "provisioningKeys": "Bereitstellungsschlüssel", "searchProvisioningKeys": "Bereitstellungsschlüssel suchen...", "provisioningKeysAdd": "Bereitstellungsschlüssel generieren", @@ -405,7 +405,7 @@ "provisioningKeysNeverUsed": "Nie", "provisioningKeysEdit": "Bereitstellungsschlüssel bearbeiten", "provisioningKeysEditDescription": "Aktualisieren Sie die maximale Batch-Größe und Ablaufzeit für diesen Schlüssel.", - "provisioningKeysApproveNewSites": "Neue Seiten genehmigen", + "provisioningKeysApproveNewSites": "Neuen Standort genehmigen", "provisioningKeysApproveNewSitesDescription": "Sites, die sich mit diesem Schlüssel registrieren, automatisch freigeben.", "provisioningKeysUpdateError": "Fehler beim Aktualisieren des Bereitstellungsschlüssels", "provisioningKeysUpdated": "Bereitstellungsschlüssel aktualisiert", @@ -413,8 +413,8 @@ "provisioningKeysBannerTitle": "Website-Bereitstellungsschlüssel", "provisioningKeysBannerDescription": "Generieren Sie einen Bereitstellungsschlüssel und verwenden Sie ihn mit dem Newt-Connector, um Standorte beim ersten Start automatisch zu erstellen - keine Notwendigkeit, separate Anmeldedaten für jede Seite einzurichten.", "provisioningKeysBannerButtonText": "Mehr erfahren", - "pendingSitesBannerTitle": "Ausstehende Seiten", - "pendingSitesBannerDescription": "Websites, die mit einem Bereitstellungsschlüssel verbunden sind, erscheinen hier zur Überprüfung.", + "pendingSitesBannerTitle": "Ausstehende Standorte", + "pendingSitesBannerDescription": "Standorte, die mit einem Bereitstellungsschlüssel verbunden sind, erscheinen hier zur Überprüfung.", "pendingSitesBannerButtonText": "Mehr erfahren", "apiKeysSettings": "{apiKeyName} Einstellungen", "userTitle": "Alle Benutzer verwalten", @@ -461,7 +461,7 @@ "licenseActivateKeyDescription": "Geben Sie einen Lizenzschlüssel ein, um ihn zu aktivieren.", "licenseActivate": "Lizenz aktivieren", "licenseAgreement": "Durch Ankreuzung dieses Kästchens bestätigen Sie, dass Sie die Lizenzbedingungen gelesen und akzeptiert haben, die mit dem Lizenzschlüssel in Verbindung stehen.", - "fossorialLicense": "Fossorial Gewerbelizenz & Abonnementbedingungen anzeigen", + "fossorialLicense": "Kommerzielle Fossorial-Lizenz und Abonnementbedingungen anzeigen", "licenseMessageRemove": "Dadurch werden der Lizenzschlüssel und alle zugehörigen Berechtigungen entfernt.", "licenseMessageConfirm": "Um zu bestätigen, geben Sie bitte den Lizenzschlüssel unten ein.", "licenseQuestionRemove": "Sind Sie sicher, dass Sie den Lizenzschlüssel löschen möchten?", @@ -481,7 +481,7 @@ "licensePurchaseSites": "Zusätzliche Standorte kaufen\n", "licenseSitesUsedMax": "{usedSites} von {maxSites} Standorten verwendet", "licenseSitesUsed": "{count, plural, =0 {# Standorte} one {# Standort} other {# Standorte}} im System.", - "licensePurchaseDescription": "Wähle aus, für wieviele Seiten du möchtest {selectedMode, select, license {kaufe eine Lizenz. Du kannst später immer weitere Seiten hinzufügen.} other {Füge zu deiner bestehenden Lizenz hinzu.}}", + "licensePurchaseDescription": "Wähle aus, für wie viele Standorte du möchtest {selectedMode, select, license {kaufe eine Lizenz. Du kannst später immer weitere Standorte hinzufügen.} other {Füge zu deiner bestehenden Lizenz hinzu.}}", "licenseFee": "Lizenzgebühr", "licensePriceSite": "Preis pro Standort", "total": "Gesamt", @@ -532,7 +532,7 @@ "userRemoveOrgConfirmSelf": "Entfernung bestätigen", "userRemoveOrgSelf": "Sich selbst aus der Organisation entfernen", "userRemoveOrgSelfWarning": "Sie verlieren sofort den Zugriff auf diese Organisation.", - "userRemoveOrgConfirmPhraseSelf": "ENTFERNUNG MICH SELBST AUS DER ORGANISATION", + "userRemoveOrgConfirmPhraseSelf": "MICH SELBST AUS DER ORGANISATION ENTFERNEN", "users": "Benutzer", "accessRoleMember": "Mitglied", "accessRoleOwner": "Eigentümer", @@ -1711,11 +1711,11 @@ "regionSelectorComingSoon": "Kommt bald", "billingLoadingSubscription": "Abonnement wird geladen...", "billingFreeTier": "Kostenlose Stufe", - "billingWarningOverLimit": "Warnung: Sie haben ein oder mehrere Nutzungslimits überschritten. Ihre Webseiten werden nicht verbunden, bis Sie Ihr Abonnement ändern oder Ihren Verbrauch anpassen.", + "billingWarningOverLimit": "Warnung: Sie haben ein oder mehrere Nutzungslimits überschritten. Ihre Standorte werden nicht verbunden, bis Sie Ihr Abonnement ändern oder Ihren Verbrauch anpassen.", "billingUsageLimitsOverview": "Übersicht über Nutzungsgrenzen", "billingMonitorUsage": "Überwachen Sie Ihren Verbrauch im Vergleich zu konfigurierten Grenzwerten. Wenn Sie eine Erhöhung der Limits benötigen, kontaktieren Sie uns bitte support@pangolin.net.", "billingDataUsage": "Datenverbrauch", - "billingSites": "Seiten", + "billingSites": "Standorte", "billingUsers": "Benutzergeräte", "billingDomains": "Domänen", "billingOrganizations": "Orden", @@ -1743,7 +1743,7 @@ "billingCheckoutError": "Checkout-Fehler", "billingFailedToGetPortalUrl": "Fehler beim Abrufen der Portal-URL", "billingPortalError": "Portalfehler", - "billingDataUsageInfo": "Wenn Sie mit der Cloud verbunden sind, werden alle Daten über Ihre sicheren Tunnel belastet. Dies schließt eingehenden und ausgehenden Datenverkehr über alle Ihre Websites ein. Wenn Sie Ihr Limit erreichen, werden Ihre Seiten die Verbindung trennen, bis Sie Ihr Paket upgraden oder die Nutzung verringern. Daten werden nicht belastet, wenn Sie Knoten verwenden.", + "billingDataUsageInfo": "Wenn Sie mit der Cloud verbunden sind, werden alle Daten über Ihre sicheren Tunnel belastet. Dies schließt eingehenden und ausgehenden Datenverkehr über alle Ihre Standorte ein. Wenn Sie Ihr Limit erreichen, werden Ihre Standorte die Verbindung trennen, bis Sie Ihr Paket upgraden oder die Nutzung verringern. Daten werden nicht belastet, wenn Sie Knoten verwenden.", "billingSInfo": "Anzahl der Sites die Sie verwenden können", "billingUsersInfo": "Wie viele Benutzer Sie verwenden können", "billingDomainInfo": "Wie viele Domains Sie verwenden können", @@ -1927,7 +1927,7 @@ "configureHealthCheck": "Gesundheits-Check konfigurieren", "configureHealthCheckDescription": "Richten Sie die Gesundheitsüberwachung für {target} ein", "enableHealthChecks": "Gesundheits-Checks aktivieren", - "healthCheckDisabledStateDescription": "Wenn deaktiviert, führt die Seite keine Gesundheitsprüfungen durch und der Zustand wird als unbekannt betrachtet.", + "healthCheckDisabledStateDescription": "Wenn deaktiviert, führt der Standort keine Gesundheitsprüfungen durch und der Zustand wird als unbekannt betrachtet.", "enableHealthChecksDescription": "Überwachen Sie die Gesundheit dieses Ziels. Bei Bedarf können Sie einen anderen Endpunkt als das Ziel überwachen.", "healthScheme": "Methode", "healthSelectScheme": "Methode auswählen", @@ -2187,8 +2187,8 @@ } }, "remoteExitNodeSelection": "Knotenauswahl", - "remoteExitNodeSelectionDescription": "Wählen Sie einen Knoten aus, durch den Traffic für diese lokale Seite geleitet werden soll", - "remoteExitNodeRequired": "Ein Knoten muss für lokale Seiten ausgewählt sein", + "remoteExitNodeSelectionDescription": "Wählen Sie einen Knoten aus, durch den Traffic für diesen lokalen Standort geleitet werden soll", + "remoteExitNodeRequired": "Ein Knoten muss für lokale Standorte ausgewählt sein", "noRemoteExitNodesAvailable": "Keine Knoten verfügbar", "noRemoteExitNodesAvailableDescription": "Für diese Organisation sind keine Knoten verfügbar. Erstellen Sie zuerst einen Knoten, um lokale Standorte zu verwenden.", "exitNode": "Exit-Node", @@ -3235,7 +3235,7 @@ "uptimeAddAlert": "Warnmeldung hinzufügen", "uptimeViewAlerts": "Warnungen anzeigen", "uptimeCreateEmailAlert": "E-Mail Alarm erstellen", - "uptimeAlertDescriptionSite": "Werde per E-Mail benachrichtigt, wenn diese Seite offline oder wieder online ist.", + "uptimeAlertDescriptionSite": "Werde per E-Mail benachrichtigt, wenn dieser Standort offline oder wieder online ist.", "uptimeAlertDescriptionResource": "Werde per E-Mail benachrichtigt, wenn diese Ressource offline oder wieder online ist.", "uptimeAlertNamePlaceholder": "Alarmname", "uptimeAdditionalEmails": "Zusätzliche E-Mails", From 0e0666cacfe862497c09c71f647dda316d8d009c Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 May 2026 11:34:09 -0700 Subject: [PATCH 05/13] Optimize this --- server/routers/gerbil/updateHolePunch.ts | 116 +++++++++++++---------- 1 file changed, 65 insertions(+), 51 deletions(-) diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 810c44ff7..ac3ff3945 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -11,7 +11,7 @@ import { ExitNode } from "@server/db"; import { db } from "@server/db"; -import { eq, and } from "drizzle-orm"; +import { eq, and, inArray } from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; @@ -202,24 +202,29 @@ export async function updateAndGenerateEndpointDestinations( ) ); - // Update clientSites for each site on this exit node + // Format the endpoint properly for both IPv4 and IPv6 + const formattedEndpoint = formatEndpoint(ip, port); + + // Determine which rows actually need updating and whether the endpoint + // (as opposed to only the publicKey) changed for any of them. + const siteIdsToUpdate: number[] = []; + let endpointChanged = false; for (const site of sitesOnExitNode) { - // logger.debug( - // `Updating site ${site.siteId} on exit node ${exitNode.exitNodeId}` - // ); - - // Format the endpoint properly for both IPv4 and IPv6 - const formattedEndpoint = formatEndpoint(ip, port); - - // if the public key or endpoint has changed, update it otherwise continue if ( site.endpoint === formattedEndpoint && site.publicKey === publicKey ) { continue; } + siteIdsToUpdate.push(site.siteId); + if (site.endpoint !== formattedEndpoint) { + endpointChanged = true; + } + } - const [updatedClientSitesAssociationsCache] = await db + if (siteIdsToUpdate.length > 0) { + // Single bulk update for all affected rows for this client on this exit node + await db .update(clientSitesAssociationsCache) .set({ endpoint: formattedEndpoint, @@ -228,24 +233,22 @@ export async function updateAndGenerateEndpointDestinations( .where( and( eq(clientSitesAssociationsCache.clientId, olm.clientId), - eq(clientSitesAssociationsCache.siteId, site.siteId) + inArray( + clientSitesAssociationsCache.siteId, + siteIdsToUpdate + ) ) - ) - .returning(); + ); - if ( - updatedClientSitesAssociationsCache.endpoint !== - site.endpoint && // this is the endpoint from the join table not the site - updatedClient.pubKey === publicKey // only trigger if the client's public key matches the current public key which means it has registered so we dont prematurely send the update - ) { + // Only trigger downstream peer updates once per hole punch: the + // endpoint is the same for every site on this exit node, and + // handleClientEndpointChange already fans out to all connected + // sites for this client. + if (endpointChanged && updatedClient.pubKey === publicKey) { logger.info( - `ClientSitesAssociationsCache for client ${olm.clientId} and site ${site.siteId} endpoint changed from ${site.endpoint} to ${updatedClientSitesAssociationsCache.endpoint}` - ); - // Handle any additional logic for endpoint change - handleClientEndpointChange( - olm.clientId, - updatedClientSitesAssociationsCache.endpoint! + `ClientSitesAssociationsCache for client ${olm.clientId} endpoint changed to ${formattedEndpoint} for ${siteIdsToUpdate.length} site(s) on exit node ${exitNode.exitNodeId}` ); + handleClientEndpointChange(olm.clientId, formattedEndpoint); } } @@ -456,11 +459,11 @@ async function handleSiteEndpointChange(siteId: number, newEndpoint: string) { } } -async function handleClientEndpointChange( +async function handleClientEndpointChange( // TODO: I THINK WE DONT NEED TO HIT EVERY SITE HERE BECAUSE WE ONLY NEED TO UPDATE THE SITES CONNECTED TO THIS NODE WHICH WE ALREADY HAVE FROM ABOVE clientId: number, newEndpoint: string ) { - // Alert all sites connected to this client that the endpoint has changed (only if NOT relayed) + // Alert all sites connected to this client that the endpoint has changed (only if NOT relayed and NOT JIT MODE) try { // Get client details const [client] = await db @@ -480,6 +483,7 @@ async function handleClientEndpointChange( siteId: sites.siteId, newtId: newts.newtId, isRelayed: clientSitesAssociationsCache.isRelayed, + isJitMode: clientSitesAssociationsCache.isJitMode, subnet: clients.subnet }) .from(clientSitesAssociationsCache) @@ -495,37 +499,47 @@ async function handleClientEndpointChange( .where( and( eq(clientSitesAssociationsCache.clientId, clientId), - eq(clientSitesAssociationsCache.isRelayed, false) + eq(clientSitesAssociationsCache.isRelayed, false), + eq(clientSitesAssociationsCache.isJitMode, false) ) ); - // Update each non-relayed site with the new client endpoint - for (const siteData of connectedSites) { - try { - if (!siteData.subnet) { + if (connectedSites.length > 250) { + logger.warn( + `Client ${clientId} has ${connectedSites.length} connected sites so the client will be in jit mode anyway, skipping endpoint updates` + ); + return; + } + + // Update each non-relayed site with the new client endpoint (in parallel) + await Promise.allSettled( + connectedSites.map(async (siteData) => { + if (!siteData.subnet || !client.pubKey) { logger.warn( - `Client ${clientId} has no subnet, skipping update for site ${siteData.siteId}` + `Client ${clientId} has no subnet or public key, skipping update for site ${siteData.siteId}` ); - continue; + return; } - await updateNewtPeer( - siteData.siteId, - client.pubKey, - { - endpoint: newEndpoint - }, - siteData.newtId - ); - logger.debug( - `Updated site ${siteData.siteId} with new client ${clientId} endpoint: ${newEndpoint}` - ); - } catch (error) { - logger.error( - `Failed to update site ${siteData.siteId} with new client endpoint: ${error}` - ); - } - } + try { + await updateNewtPeer( + siteData.siteId, + client.pubKey, + { + endpoint: newEndpoint + }, + siteData.newtId + ); + logger.debug( + `Updated site ${siteData.siteId} with new client ${clientId} endpoint: ${newEndpoint}` + ); + } catch (error) { + logger.error( + `Failed to update site ${siteData.siteId} with new client endpoint: ${error}` + ); + } + }) + ); } catch (error) { logger.error( `Error handling client endpoint change for client ${clientId}: ${error}` From 5a8de8210be1a617b3c01719832b8cc6955dfbb1 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 May 2026 11:59:59 -0700 Subject: [PATCH 06/13] Further optimizations --- server/routers/gerbil/updateHolePunch.ts | 55 +++++++++++-------- server/routers/olm/getOlmToken.ts | 19 ++++++- .../routers/olm/handleOlmRegisterMessage.ts | 15 ++++- 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index ac3ff3945..ed60c6a40 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -411,12 +411,14 @@ async function handleSiteEndpointChange(siteId: number, newEndpoint: string) { return; } - // Get all non-relayed clients connected to this site + // Get all non-relayed and not jit clients connected to this site const connectedClients = await db .select({ + online: clients.online, clientId: clients.clientId, olmId: olms.olmId, - isRelayed: clientSitesAssociationsCache.isRelayed + isRelayed: clientSitesAssociationsCache.isRelayed, + isJitMode: clientSitesAssociationsCache.isJitMode }) .from(clientSitesAssociationsCache) .innerJoin( @@ -426,32 +428,36 @@ async function handleSiteEndpointChange(siteId: number, newEndpoint: string) { .innerJoin(olms, eq(olms.clientId, clients.clientId)) .where( and( + eq(clients.online, true), // the client has to be online or it does not matter... eq(clientSitesAssociationsCache.siteId, siteId), - eq(clientSitesAssociationsCache.isRelayed, false) + eq(clientSitesAssociationsCache.isRelayed, false), + eq(clientSitesAssociationsCache.isJitMode, false) ) ); - // Update each non-relayed client with the new site endpoint - for (const client of connectedClients) { - try { - await updateOlmPeer( - client.clientId, - { - siteId: siteId, - publicKey: site.publicKey, - endpoint: newEndpoint - }, - client.olmId - ); - logger.debug( - `Updated client ${client.clientId} with new site ${siteId} endpoint: ${newEndpoint}` - ); - } catch (error) { - logger.error( - `Failed to update client ${client.clientId} with new site endpoint: ${error}` - ); - } - } + // Update each non-relayed client with the new site endpoint (in parallel) + await Promise.allSettled( + connectedClients.map(async (client) => { + try { + await updateOlmPeer( + client.clientId, + { + siteId: siteId, + publicKey: site.publicKey!, + endpoint: newEndpoint + }, + client.olmId + ); + logger.debug( + `Updated client ${client.clientId} with new site ${siteId} endpoint: ${newEndpoint}` + ); + } catch (error) { + logger.error( + `Failed to update client ${client.clientId} with new site endpoint: ${error}` + ); + } + }) + ); } catch (error) { logger.error( `Error handling site endpoint change for site ${siteId}: ${error}` @@ -498,6 +504,7 @@ async function handleClientEndpointChange( // TODO: I THINK WE DONT NEED TO HIT ) .where( and( + eq(sites.online, true), // the site has to be online or it does not matter... eq(clientSitesAssociationsCache.clientId, clientId), eq(clientSitesAssociationsCache.isRelayed, false), eq(clientSitesAssociationsCache.isJitMode, false) diff --git a/server/routers/olm/getOlmToken.ts b/server/routers/olm/getOlmToken.ts index 5b8411eb7..51cdc8b4e 100644 --- a/server/routers/olm/getOlmToken.ts +++ b/server/routers/olm/getOlmToken.ts @@ -8,7 +8,7 @@ import { ExitNode, exitNodes, sites, - clientSitesAssociationsCache, + clientSitesAssociationsCache } from "@server/db"; import { olms } from "@server/db"; import HttpCode from "@server/types/HttpCode"; @@ -28,6 +28,7 @@ import { verifyPassword } from "@server/auth/password"; import logger from "@server/logger"; import config from "@server/lib/config"; import { APP_VERSION } from "@server/lib/consts"; +import { build } from "@server/build"; export const olmGetTokenBodySchema = z.object({ olmId: z.string(), @@ -220,6 +221,22 @@ export async function getOlmToken( ) .where(eq(clientSitesAssociationsCache.clientId, clientIdToUse!)); + if (clientSites.length > 250 && build == "saas") { + // set all of the cache rows isJitMode to true + await db + .update(clientSitesAssociationsCache) + .set({ isJitMode: true }) + .where( + and( + eq( + clientSitesAssociationsCache.clientId, + clientIdToUse! + ), + eq(clientSitesAssociationsCache.isJitMode, false) + ) + ); + } + // Extract unique exit node IDs const exitNodeIds = Array.from( new Set( diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index aaaf81c41..073e86f7b 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -7,7 +7,7 @@ import { olms, sites } from "@server/db"; -import { count, eq } from "drizzle-orm"; +import { and, count, eq, ne, or } from "drizzle-orm"; import logger from "@server/logger"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { validateSessionToken } from "@server/auth/sessions/app"; @@ -301,7 +301,18 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { isRelayed: relay == true, isJitMode: jitMode }) - .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); + .where( + and( + eq(clientSitesAssociationsCache.clientId, client.clientId), + or( + ne( + clientSitesAssociationsCache.isRelayed, + relay == true + ), + ne(clientSitesAssociationsCache.isJitMode, jitMode) + ) + ) + ); } // this prevents us from accepting a register from an olm that has not hole punched yet. From 78ad2d17c7b0149751b09a37e5ea7bcb62e43350 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 May 2026 12:25:05 -0700 Subject: [PATCH 07/13] Optimize building aliases in jit mode --- server/routers/olm/buildConfiguration.ts | 155 ++++++++++-------- .../routers/olm/handleOlmRegisterMessage.ts | 42 ++--- 2 files changed, 112 insertions(+), 85 deletions(-) diff --git a/server/routers/olm/buildConfiguration.ts b/server/routers/olm/buildConfiguration.ts index 640031bca..41bb6d60d 100644 --- a/server/routers/olm/buildConfiguration.ts +++ b/server/routers/olm/buildConfiguration.ts @@ -5,6 +5,7 @@ import { db, exitNodes, networks, + SiteResource, siteNetworks, siteResources, sites @@ -15,7 +16,7 @@ import { generateRemoteSubnets } from "@server/lib/ip"; import logger from "@server/logger"; -import { and, eq } from "drizzle-orm"; +import { eq, inArray } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; import config from "@server/lib/config"; @@ -46,49 +47,79 @@ export async function buildSiteConfigurationForOlmClient( ) .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); + if (sitesData.length === 0) { + return siteConfigurations; + } + + // Batch-fetch every site resource this client has access to across ALL sites + // in a single query, then group by siteId in memory. This avoids issuing one + // query per site (which would be N round-trips for N sites). + const allClientSiteResources = await db + .select({ + siteResource: siteResources, + siteId: siteNetworks.siteId + }) + .from(siteResources) + .innerJoin( + clientSiteResourcesAssociationsCache, + eq( + siteResources.siteResourceId, + clientSiteResourcesAssociationsCache.siteResourceId + ) + ) + .innerJoin(networks, eq(siteResources.networkId, networks.networkId)) + .innerJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId)) + .where( + eq(clientSiteResourcesAssociationsCache.clientId, client.clientId) + ); + + const siteResourcesBySiteId = new Map(); + for (const row of allClientSiteResources) { + const arr = siteResourcesBySiteId.get(row.siteId); + if (arr) { + arr.push(row.siteResource); + } else { + siteResourcesBySiteId.set(row.siteId, [row.siteResource]); + } + } + + // Batch-fetch exit nodes for all sites in one query (only needed in relay mode). + const exitNodesById = new Map(); + if (!jitMode && relay) { + const exitNodeIds = Array.from( + new Set( + sitesData + .map(({ sites: s }) => s.exitNodeId) + .filter((id): id is number => id != null) + ) + ); + if (exitNodeIds.length > 0) { + const nodes = await db + .select() + .from(exitNodes) + .where(inArray(exitNodes.exitNodeId, exitNodeIds)); + for (const n of nodes) { + exitNodesById.set(n.exitNodeId, n); + } + } + } + + const clientsStartPort = config.getRawConfig().gerbil.clients_start_port; + const peerOps: Promise[] = []; + // Process each site for (const { sites: site, clientSitesAssociationsCache: association } of sitesData) { - const allSiteResources = await db // only get the site resources that this client has access to - .select() - .from(siteResources) - .innerJoin( - clientSiteResourcesAssociationsCache, - eq( - siteResources.siteResourceId, - clientSiteResourcesAssociationsCache.siteResourceId - ) - ) - .innerJoin( - networks, - eq(siteResources.networkId, networks.networkId) - ) - .innerJoin( - siteNetworks, - eq(networks.networkId, siteNetworks.networkId) - ) - .where( - and( - eq(siteNetworks.siteId, site.siteId), - eq( - clientSiteResourcesAssociationsCache.clientId, - client.clientId - ) - ) - ); + const allSiteResources = siteResourcesBySiteId.get(site.siteId) ?? []; if (jitMode) { // Add site configuration to the array siteConfigurations.push({ siteId: site.siteId, - // remoteSubnets: generateRemoteSubnets( - // allSiteResources.map(({ siteResources }) => siteResources) - // ), - aliases: generateAliasConfig( - allSiteResources.map(({ siteResources }) => siteResources) - ) + // remoteSubnets: generateRemoteSubnets(allSiteResources), + aliases: generateAliasConfig(allSiteResources) }); continue; } @@ -126,7 +157,7 @@ export async function buildSiteConfigurationForOlmClient( logger.info( `Public key mismatch. Deleting old peer from site ${site.siteId}...` ); - await deletePeer(site.siteId, client.pubKey!); + peerOps.push(deletePeer(site.siteId, client.pubKey!)); } if (!site.subnet) { @@ -134,27 +165,19 @@ export async function buildSiteConfigurationForOlmClient( continue; } - const [clientSite] = await db - .select() - .from(clientSitesAssociationsCache) - .where( - and( - eq(clientSitesAssociationsCache.clientId, client.clientId), - eq(clientSitesAssociationsCache.siteId, site.siteId) - ) - ) - .limit(1); - - // Add the peer to the exit node for this site - if (clientSite.endpoint && publicKey) { + // Add the peer to the exit node for this site. The endpoint comes from + // the already-joined association row above, so no extra query needed. + if (association.endpoint && publicKey) { logger.info( - `Adding peer ${publicKey} to site ${site.siteId} with endpoint ${clientSite.endpoint}` + `Adding peer ${publicKey} to site ${site.siteId} with endpoint ${association.endpoint}` + ); + peerOps.push( + addPeer(site.siteId, { + publicKey: publicKey, + allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client + endpoint: relay ? "" : association.endpoint + }) ); - await addPeer(site.siteId, { - publicKey: publicKey, - allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client - endpoint: relay ? "" : clientSite.endpoint - }); } else { logger.warn( `Client ${client.clientId} has no endpoint, skipping peer addition` @@ -163,16 +186,12 @@ export async function buildSiteConfigurationForOlmClient( let relayEndpoint: string | undefined = undefined; if (relay) { - const [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.exitNodeId, site.exitNodeId)) - .limit(1); + const exitNode = exitNodesById.get(site.exitNodeId); if (!exitNode) { logger.warn(`Exit node not found for site ${site.siteId}`); continue; } - relayEndpoint = `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`; + relayEndpoint = `${exitNode.endpoint}:${clientsStartPort}`; } // Add site configuration to the array @@ -184,12 +203,16 @@ export async function buildSiteConfigurationForOlmClient( publicKey: site.publicKey, serverIP: site.address, serverPort: site.listenPort, - remoteSubnets: generateRemoteSubnets( - allSiteResources.map(({ siteResources }) => siteResources) - ), - aliases: generateAliasConfig( - allSiteResources.map(({ siteResources }) => siteResources) - ) + remoteSubnets: generateRemoteSubnets(allSiteResources), + aliases: generateAliasConfig(allSiteResources) + }); + } + + // Run all peer add/delete operations concurrently rather than serially per + // site, so total time is bounded by the slowest call instead of the sum. + if (peerOps.length > 0) { + Promise.allSettled(peerOps).catch((err) => { + logger.error("Error processing peer operations: ", err); }); } diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 073e86f7b..7735e4d30 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -1,4 +1,4 @@ -import { db, orgs } from "@server/db"; +import { db, orgs, primaryDb } from "@server/db"; import { MessageHandler } from "@server/routers/ws"; import { clients, @@ -81,7 +81,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { .where(eq(olms.olmId, olm.olmId)); } - const [client] = await db + const [client] = await primaryDb // read from the primary here so there is no latency with the last update on the holepunch .select() .from(clients) .where(eq(clients.clientId, olm.clientId)) @@ -98,7 +98,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { if (client.blocked) { logger.debug( `[handleOlmRegisterMessage] Client ${client.clientId} is blocked. Ignoring register.`, - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); sendOlmError(OlmErrorCodes.CLIENT_BLOCKED, olm.olmId); return; @@ -107,7 +107,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { if (client.approvalState == "pending") { logger.debug( `[handleOlmRegisterMessage] Client ${client.clientId} approval is pending. Ignoring register.`, - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); sendOlmError(OlmErrorCodes.CLIENT_PENDING, olm.olmId); return; @@ -136,7 +136,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { if (!org) { logger.warn("[handleOlmRegisterMessage] Org not found", { - orgId: client.orgId + orgId: client.orgId, + clientId: client.clientId }); sendOlmError(OlmErrorCodes.ORG_NOT_FOUND, olm.olmId); return; @@ -145,7 +146,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { if (orgId) { if (!olm.userId) { logger.warn("[handleOlmRegisterMessage] Olm has no user ID", { - orgId: client.orgId + orgId: client.orgId, + clientId: client.clientId }); sendOlmError(OlmErrorCodes.USER_ID_NOT_FOUND, olm.olmId); return; @@ -156,7 +158,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { if (!userSession || !user) { logger.warn( "[handleOlmRegisterMessage] Invalid user session for olm register", - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); sendOlmError(OlmErrorCodes.INVALID_USER_SESSION, olm.olmId); return; @@ -164,7 +166,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { if (user.userId !== olm.userId) { logger.warn( "[handleOlmRegisterMessage] User ID mismatch for olm register", - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); sendOlmError(OlmErrorCodes.USER_ID_MISMATCH, olm.olmId); return; @@ -182,13 +184,14 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { logger.debug("[handleOlmRegisterMessage] Policy check result", { orgId: client.orgId, + clientId: client.clientId, policyCheck }); if (policyCheck?.error) { logger.error( `[handleOlmRegisterMessage] Error checking access policies for olm user ${olm.userId} in org ${orgId}: ${policyCheck?.error}`, - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId); return; @@ -197,7 +200,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { if (policyCheck.policies?.passwordAge?.compliant === false) { logger.warn( `[handleOlmRegisterMessage] Olm user ${olm.userId} has non-compliant password age for org ${orgId}`, - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); sendOlmError( OlmErrorCodes.ORG_ACCESS_POLICY_PASSWORD_EXPIRED, @@ -209,7 +212,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { ) { logger.warn( `[handleOlmRegisterMessage] Olm user ${olm.userId} has non-compliant session length for org ${orgId}`, - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); sendOlmError( OlmErrorCodes.ORG_ACCESS_POLICY_SESSION_EXPIRED, @@ -219,7 +222,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { } else if (policyCheck.policies?.requiredTwoFactor === false) { logger.warn( `[handleOlmRegisterMessage] Olm user ${olm.userId} does not have 2FA enabled for org ${orgId}`, - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); sendOlmError( OlmErrorCodes.ORG_ACCESS_POLICY_2FA_REQUIRED, @@ -229,7 +232,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { } else if (!policyCheck.allowed) { logger.warn( `[handleOlmRegisterMessage] Olm user ${olm.userId} does not pass access policies for org ${orgId}: ${policyCheck.error}`, - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId); return; @@ -253,7 +256,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { // Prepare an array to store site configurations logger.debug( `[handleOlmRegisterMessage] Found ${sitesCount} sites for client ${client.clientId}`, - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); let jitMode = false; @@ -263,19 +266,20 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { // If we have too many sites we need to drop into fully JIT mode by not sending any of the sites logger.info( `[handleOlmRegisterMessage] Too many sites (${sitesCount}), dropping into JIT mode`, - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); jitMode = true; } logger.debug( `[handleOlmRegisterMessage] Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`, - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); if (!publicKey) { logger.warn("[handleOlmRegisterMessage] Public key not provided", { - orgId: client.orgId + orgId: client.orgId, + clientId: client.clientId }); return; } @@ -283,7 +287,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { if (client.pubKey !== publicKey || client.archived) { logger.info( "[handleOlmRegisterMessage] Public key mismatch. Updating public key and clearing session info...", - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); // Update the client's public key await db @@ -321,7 +325,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) { logger.warn( `[handleOlmRegisterMessage] Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`, - { orgId: client.orgId } + { orgId: client.orgId, clientId: client.clientId } ); return; } From c3ebc423b589e0ab352e070c6459d49bf8792ac9 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 May 2026 16:48:54 -0700 Subject: [PATCH 08/13] Each node should only update its own sites --- server/routers/gerbil/updateHolePunch.ts | 131 +++++++---------------- 1 file changed, 39 insertions(+), 92 deletions(-) diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index ed60c6a40..fad3f277d 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -185,16 +185,20 @@ export async function updateAndGenerateEndpointDestinations( const sitesOnExitNode = await db .select({ siteId: sites.siteId, + newtId: newts.newtId, subnet: sites.subnet, listenPort: sites.listenPort, publicKey: sites.publicKey, - endpoint: clientSitesAssociationsCache.endpoint + endpoint: clientSitesAssociationsCache.endpoint, + isRelayed: clientSitesAssociationsCache.isRelayed, + isJitMode: clientSitesAssociationsCache.isJitMode }) .from(sites) .innerJoin( clientSitesAssociationsCache, eq(sites.siteId, clientSitesAssociationsCache.siteId) ) + .innerJoin(newts, eq(sites.siteId, newts.siteId)) .where( and( eq(sites.exitNodeId, exitNode.exitNodeId), @@ -208,6 +212,7 @@ export async function updateAndGenerateEndpointDestinations( // Determine which rows actually need updating and whether the endpoint // (as opposed to only the publicKey) changed for any of them. const siteIdsToUpdate: number[] = []; + const sitesWithNewtsToUpdate: { siteId: number; newtId: string }[] = []; let endpointChanged = false; for (const site of sitesOnExitNode) { if ( @@ -217,6 +222,12 @@ export async function updateAndGenerateEndpointDestinations( continue; } siteIdsToUpdate.push(site.siteId); + if (!site.isRelayed && !site.isJitMode) { + sitesWithNewtsToUpdate.push({ + siteId: site.siteId, + newtId: site.newtId + }); + } if (site.endpoint !== formattedEndpoint) { endpointChanged = true; } @@ -248,7 +259,15 @@ export async function updateAndGenerateEndpointDestinations( logger.info( `ClientSitesAssociationsCache for client ${olm.clientId} endpoint changed to ${formattedEndpoint} for ${siteIdsToUpdate.length} site(s) on exit node ${exitNode.exitNodeId}` ); - handleClientEndpointChange(olm.clientId, formattedEndpoint); + handleClientEndpointChange( + sitesWithNewtsToUpdate, + olm.clientId, + formattedEndpoint + ).catch((error) => { + logger.error( + `Failed to handle client endpoint change for client ${olm.clientId}: ${error}` + ); + }); } } @@ -339,59 +358,14 @@ export async function updateAndGenerateEndpointDestinations( `Site ${newt.siteId} endpoint changed from ${site.endpoint} to ${updatedSite.endpoint}` ); // Handle any additional logic for endpoint change - handleSiteEndpointChange(newt.siteId, updatedSite.endpoint!); + handleSiteEndpointChange(newt.siteId, updatedSite.endpoint!).catch( + (error) => { + logger.error( + `Failed to handle site endpoint change for site ${newt.siteId}: ${error}` + ); + } + ); } - - // if (!updatedSite || !updatedSite.subnet) { - // logger.warn(`Site not found: ${newt.siteId}`); - // throw new Error("Site not found"); - // } - - // Find all clients that connect to this site - // const sitesClientPairs = await db - // .select() - // .from(clientSites) - // .where(eq(clientSites.siteId, newt.siteId)); - - // THE NEWT IS NOT SENDING RAW WG TO THE GERBIL SO IDK IF WE REALLY NEED THIS - REMOVING - // Get client details for each client - // for (const pair of sitesClientPairs) { - // const [client] = await db - // .select() - // .from(clients) - // .where(eq(clients.clientId, pair.clientId)); - - // if (client && client.endpoint) { - // const [host, portStr] = client.endpoint.split(':'); - // if (host && portStr) { - // destinations.push({ - // destinationIP: host, - // destinationPort: parseInt(portStr, 10) - // }); - // } - // } - // } - - // If this is a newt/site, also add other sites in the same org - // if (updatedSite.orgId) { - // const orgSites = await db - // .select() - // .from(sites) - // .where(eq(sites.orgId, updatedSite.orgId)); - - // for (const site of orgSites) { - // // Don't add the current site to the destinations - // if (site.siteId !== currentSiteId && site.subnet && site.endpoint && site.listenPort) { - // const [host, portStr] = site.endpoint.split(':'); - // if (host && portStr) { - // destinations.push({ - // destinationIP: host, - // destinationPort: site.listenPort - // }); - // } - // } - // } - // } } return destinations; } @@ -465,7 +439,8 @@ async function handleSiteEndpointChange(siteId: number, newEndpoint: string) { } } -async function handleClientEndpointChange( // TODO: I THINK WE DONT NEED TO HIT EVERY SITE HERE BECAUSE WE ONLY NEED TO UPDATE THE SITES CONNECTED TO THIS NODE WHICH WE ALREADY HAVE FROM ABOVE +async function handleClientEndpointChange( + sitesWithNewtsToUpdate: { siteId: number; newtId: string }[], clientId: number, newEndpoint: string ) { @@ -483,66 +458,38 @@ async function handleClientEndpointChange( // TODO: I THINK WE DONT NEED TO HIT return; } - // Get all non-relayed sites connected to this client - const connectedSites = await db - .select({ - siteId: sites.siteId, - newtId: newts.newtId, - isRelayed: clientSitesAssociationsCache.isRelayed, - isJitMode: clientSitesAssociationsCache.isJitMode, - subnet: clients.subnet - }) - .from(clientSitesAssociationsCache) - .innerJoin( - sites, - eq(clientSitesAssociationsCache.siteId, sites.siteId) - ) - .innerJoin(newts, eq(newts.siteId, sites.siteId)) - .innerJoin( - clients, - eq(clientSitesAssociationsCache.clientId, clients.clientId) - ) - .where( - and( - eq(sites.online, true), // the site has to be online or it does not matter... - eq(clientSitesAssociationsCache.clientId, clientId), - eq(clientSitesAssociationsCache.isRelayed, false), - eq(clientSitesAssociationsCache.isJitMode, false) - ) - ); - - if (connectedSites.length > 250) { + if (sitesWithNewtsToUpdate.length > 250) { logger.warn( - `Client ${clientId} has ${connectedSites.length} connected sites so the client will be in jit mode anyway, skipping endpoint updates` + `Client ${clientId} has ${sitesWithNewtsToUpdate.length} connected sites so the client will be in jit mode anyway, skipping endpoint updates` ); return; } // Update each non-relayed site with the new client endpoint (in parallel) await Promise.allSettled( - connectedSites.map(async (siteData) => { - if (!siteData.subnet || !client.pubKey) { + sitesWithNewtsToUpdate.map(async ({ siteId, newtId }) => { + if (!client.pubKey) { logger.warn( - `Client ${clientId} has no subnet or public key, skipping update for site ${siteData.siteId}` + `Client ${clientId} has no public key, skipping update for site ${siteId}` ); return; } try { await updateNewtPeer( - siteData.siteId, + siteId, client.pubKey, { endpoint: newEndpoint }, - siteData.newtId + newtId ); logger.debug( - `Updated site ${siteData.siteId} with new client ${clientId} endpoint: ${newEndpoint}` + `Updated site ${siteId} with new client ${clientId} endpoint: ${newEndpoint}` ); } catch (error) { logger.error( - `Failed to update site ${siteData.siteId} with new client endpoint: ${error}` + `Failed to update site ${siteId} with new client endpoint: ${error}` ); } }) From 6422208f69cd268c0d7fcffb050fae5107e95ff6 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 May 2026 16:59:15 -0700 Subject: [PATCH 09/13] Optimize get all relays --- server/routers/gerbil/getAllRelays.ts | 175 +++++++++++++++----------- 1 file changed, 104 insertions(+), 71 deletions(-) diff --git a/server/routers/gerbil/getAllRelays.ts b/server/routers/gerbil/getAllRelays.ts index bbe314b2a..23d538e62 100644 --- a/server/routers/gerbil/getAllRelays.ts +++ b/server/routers/gerbil/getAllRelays.ts @@ -11,7 +11,7 @@ import { ExitNode } from "@server/db"; import { db } from "@server/db"; -import { eq } from "drizzle-orm"; +import { eq, inArray } from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; @@ -97,86 +97,119 @@ export async function generateRelayMappings(exitNode: ExitNode) { return {}; } + // Filter to sites with the required fields up front so the rest of the + // function can safely treat endpoint/subnet/listenPort as defined. + const validSites = sitesRes.filter( + (s) => s.endpoint && s.subnet && s.listenPort + ); + + if (validSites.length === 0) { + return {}; + } + + const siteIds = validSites.map((s) => s.siteId); + const orgIds = Array.from( + new Set( + validSites + .map((s) => s.orgId) + .filter((id): id is NonNullable => id != null) + ) + ); + + // Batch fetch all client-site associations for these sites in one query. + const clientSitesRes = siteIds.length + ? await db + .select() + .from(clientSitesAssociationsCache) + .where(inArray(clientSitesAssociationsCache.siteId, siteIds)) + : []; + + // Batch fetch all sites in the relevant orgs in one query (covers + // site-to-site communication for every site processed below). + const orgSitesRes = orgIds.length + ? await db.select().from(sites).where(inArray(sites.orgId, orgIds)) + : []; + + // Index org sites by orgId for O(1) lookup per site. + const sitesByOrg = new Map(); + for (const peer of orgSitesRes) { + if ( + peer.orgId == null || + !peer.endpoint || + !peer.subnet || + !peer.listenPort + ) { + continue; + } + let arr = sitesByOrg.get(peer.orgId); + if (!arr) { + arr = []; + sitesByOrg.set(peer.orgId, arr); + } + arr.push(peer); + } + + // Index client-site associations by siteId for O(1) lookup per site. + const clientSitesBySite = new Map(); + for (const cs of clientSitesRes) { + let arr = clientSitesBySite.get(cs.siteId); + if (!arr) { + arr = []; + clientSitesBySite.set(cs.siteId, arr); + } + arr.push(cs); + } + // Initialize mappings object for multi-peer support const mappings: { [key: string]: ProxyMapping } = {}; - // Process each site - for (const site of sitesRes) { - if (!site.endpoint || !site.subnet || !site.listenPort) { - continue; + // Track destinations per endpoint to deduplicate in O(1). + const seen = new Map>(); + + const addDestination = (endpoint: string, dest: PeerDestination) => { + let destSet = seen.get(endpoint); + if (!destSet) { + destSet = new Set(); + seen.set(endpoint, destSet); + mappings[endpoint] = { destinations: [] }; } - - // Find all clients associated with this site through clientSites - const clientSitesRes = await db - .select() - .from(clientSitesAssociationsCache) - .where(eq(clientSitesAssociationsCache.siteId, site.siteId)); - - for (const clientSite of clientSitesRes) { - if (!clientSite.endpoint) { - continue; - } - - // Add this site as a destination for the client - if (!mappings[clientSite.endpoint]) { - mappings[clientSite.endpoint] = { destinations: [] }; - } - - // Add site as a destination for this client - const destination: PeerDestination = { - destinationIP: site.subnet.split("/")[0], - destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated - }; - - // Check if this destination is already in the array to avoid duplicates - const isDuplicate = mappings[clientSite.endpoint].destinations.some( - (dest) => - dest.destinationIP === destination.destinationIP && - dest.destinationPort === destination.destinationPort - ); - - if (!isDuplicate) { - mappings[clientSite.endpoint].destinations.push(destination); - } + const key = `${dest.destinationIP}:${dest.destinationPort}`; + if (!destSet.has(key)) { + destSet.add(key); + mappings[endpoint].destinations.push(dest); } + }; - // Also handle site-to-site communication (all sites in the same org) - if (site.orgId) { - const orgSites = await db - .select() - .from(sites) - .where(eq(sites.orgId, site.orgId)); + // Process each site using the pre-fetched data. + for (const site of validSites) { + const siteDestination: PeerDestination = { + destinationIP: site.subnet!.split("/")[0], + destinationPort: site.listenPort! || 1 // this satisfies gerbil for now but should be reevaluated + }; - for (const peer of orgSites) { - // Skip self - if ( - peer.siteId === site.siteId || - !peer.endpoint || - !peer.subnet || - !peer.listenPort - ) { + // Add this site as a destination for each associated client. + const clientSites = clientSitesBySite.get(site.siteId); + if (clientSites) { + for (const clientSite of clientSites) { + if (!clientSite.endpoint) { continue; } + addDestination(clientSite.endpoint, siteDestination); + } + } - // Add peer site as a destination for this site - if (!mappings[site.endpoint]) { - mappings[site.endpoint] = { destinations: [] }; - } - - const destination: PeerDestination = { - destinationIP: peer.subnet.split("/")[0], - destinationPort: peer.listenPort || 1 // this satisfies gerbil for now but should be reevaluated - }; - - // Check for duplicates - const isDuplicate = mappings[site.endpoint].destinations.some( - (dest) => - dest.destinationIP === destination.destinationIP && - dest.destinationPort === destination.destinationPort - ); - - if (!isDuplicate) { - mappings[site.endpoint].destinations.push(destination); + // Site-to-site communication (all sites in the same org). + if (site.orgId != null) { + const peers = sitesByOrg.get(site.orgId); + if (peers) { + for (const peer of peers) { + if (peer.siteId === site.siteId) { + continue; + } + addDestination(site.endpoint!, { + destinationIP: peer.subnet!.split("/")[0], + destinationPort: peer.listenPort! || 1 // this satisfies gerbil for now but should be reevaluated + }); } } } From d58b96f4b1924969df81ea3a78dbd1f9c4138365 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 May 2026 17:39:22 -0700 Subject: [PATCH 10/13] Add port and icmp information to api endpoint --- server/routers/resource/getUserResources.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/routers/resource/getUserResources.ts b/server/routers/resource/getUserResources.ts index 7b82870ea..49b14e810 100644 --- a/server/routers/resource/getUserResources.ts +++ b/server/routers/resource/getUserResources.ts @@ -156,6 +156,9 @@ export async function getUserResources( enabled: boolean; alias: string | null; aliasAddress: string | null; + tcpPortRangeString: string | null; + udpPortRangeString: string | null; + disableIcmp: boolean | null; }> = []; if (accessibleSiteResourceIds.length > 0) { siteResourcesData = await db @@ -170,7 +173,10 @@ export async function getUserResources( fullDomain: siteResources.fullDomain, enabled: siteResources.enabled, alias: siteResources.alias, - aliasAddress: siteResources.aliasAddress + aliasAddress: siteResources.aliasAddress, + tcpPortRangeString: siteResources.tcpPortRangeString, + udpPortRangeString: siteResources.udpPortRangeString, + disableIcmp: siteResources.disableIcmp }) .from(siteResources) .where( @@ -260,6 +266,9 @@ export async function getUserResources( enabled: siteResource.enabled, alias: siteResource.alias, aliasAddress: siteResource.aliasAddress, + tcpPortRangeString: siteResource.tcpPortRangeString, + udpPortRangeString: siteResource.udpPortRangeString, + disableIcmp: siteResource.disableIcmp, type: "site" as const }; }); @@ -302,6 +311,9 @@ export type GetUserResourcesResponse = { destination: string; mode: string; protocol: string | null; + tcpPortRangeString: string | null; + udpPortRangeString: string | null; + disableIcmp: boolean | null; ssl: boolean; fullDomain: string | null; enabled: boolean; From 6e4a28f227dca9a0a536aee1df3bbdfb9bff0112 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 May 2026 18:02:42 -0700 Subject: [PATCH 11/13] Add site information as well --- server/routers/resource/getUserResources.ts | 94 +++++++++++++++++++-- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/server/routers/resource/getUserResources.ts b/server/routers/resource/getUserResources.ts index 49b14e810..48df9735e 100644 --- a/server/routers/resource/getUserResources.ts +++ b/server/routers/resource/getUserResources.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { and, eq, or, inArray } from "drizzle-orm"; +import { db, DB_TYPE } from "@server/db"; +import { and, eq, or, inArray, sql } from "drizzle-orm"; import { resources, userResources, @@ -12,7 +12,9 @@ import { resourceWhitelist, siteResources, userSiteResources, - roleSiteResources + roleSiteResources, + siteNetworks, + sites } from "@server/db"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -159,9 +161,21 @@ export async function getUserResources( tcpPortRangeString: string | null; udpPortRangeString: string | null; disableIcmp: boolean | null; + siteIds: number[]; + siteNames: string[]; + siteNiceIds: string[]; + siteAddresses: (string | null)[]; + siteOnlines: boolean[]; }> = []; if (accessibleSiteResourceIds.length > 0) { - siteResourcesData = await db + const aggCol = (column: any) => { + if (DB_TYPE === "sqlite") { + return sql`json_group_array(${column})`; + } + return sql`COALESCE(array_agg(${column}) FILTER (WHERE ${sites.siteId} IS NOT NULL), '{}')`; + }; + + const siteResourcesRaw = await db .select({ siteResourceId: siteResources.siteResourceId, name: siteResources.name, @@ -176,9 +190,19 @@ export async function getUserResources( aliasAddress: siteResources.aliasAddress, tcpPortRangeString: siteResources.tcpPortRangeString, udpPortRangeString: siteResources.udpPortRangeString, - disableIcmp: siteResources.disableIcmp + disableIcmp: siteResources.disableIcmp, + siteIds: aggCol(sites.siteId), + siteNames: aggCol(sites.name), + siteNiceIds: aggCol(sites.niceId), + siteAddresses: aggCol<(string | null)[]>(sites.address), + siteOnlines: aggCol(sites.online) }) .from(siteResources) + .leftJoin( + siteNetworks, + eq(siteResources.networkId, siteNetworks.networkId) + ) + .leftJoin(sites, eq(siteNetworks.siteId, sites.siteId)) .where( and( inArray( @@ -188,7 +212,55 @@ export async function getUserResources( eq(siteResources.orgId, orgId), eq(siteResources.enabled, true) ) - ); + ) + .groupBy(siteResources.siteResourceId); + + siteResourcesData = siteResourcesRaw.map((row: any) => { + if (DB_TYPE !== "sqlite") { + return row; + } + const siteIdsRaw = JSON.parse(row.siteIds) as (number | null)[]; + const siteNamesRaw = JSON.parse(row.siteNames) as ( + | string + | null + )[]; + const siteNiceIdsRaw = JSON.parse(row.siteNiceIds) as ( + | string + | null + )[]; + const siteAddressesRaw = JSON.parse(row.siteAddresses) as ( + | string + | null + )[]; + const siteOnlinesRaw = JSON.parse(row.siteOnlines) as ( + | 0 + | 1 + | null + )[]; + + const siteIds: number[] = []; + const siteNames: string[] = []; + const siteNiceIds: string[] = []; + const siteAddresses: (string | null)[] = []; + const siteOnlines: boolean[] = []; + for (let i = 0; i < siteIdsRaw.length; i++) { + if (siteIdsRaw[i] == null) continue; + siteIds.push(siteIdsRaw[i] as number); + siteNames.push((siteNamesRaw[i] ?? "") as string); + siteNiceIds.push((siteNiceIdsRaw[i] ?? "") as string); + siteAddresses.push(siteAddressesRaw[i] ?? null); + siteOnlines.push(siteOnlinesRaw[i] === 1); + } + + return { + ...row, + siteIds, + siteNames, + siteNiceIds, + siteAddresses, + siteOnlines + }; + }); } // Check for password, pincode, and whitelist protection for each resource @@ -269,6 +341,11 @@ export async function getUserResources( tcpPortRangeString: siteResource.tcpPortRangeString, udpPortRangeString: siteResource.udpPortRangeString, disableIcmp: siteResource.disableIcmp, + siteIds: siteResource.siteIds, + siteNames: siteResource.siteNames, + siteNiceIds: siteResource.siteNiceIds, + siteAddresses: siteResource.siteAddresses, + siteOnlines: siteResource.siteOnlines, type: "site" as const }; }); @@ -319,6 +396,11 @@ export type GetUserResourcesResponse = { enabled: boolean; alias: string | null; aliasAddress: string | null; + siteIds: number[]; + siteNames: string[]; + siteNiceIds: string[]; + siteAddresses: (string | null)[]; + siteOnlines: boolean[]; type: "site"; }>; }; From b414f04cce2d22c265efffa9428e8a88aafe63ca Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 May 2026 21:20:34 -0700 Subject: [PATCH 12/13] Remove funding --- .github/FUNDING.yml | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index e3d0a4477..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms - -github: [fosrl] From dd1f7ba5444927fa59b9bd9b7c5881a89d9de3a7 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 May 2026 21:46:26 -0700 Subject: [PATCH 13/13] Make crowdsec --crowdsec --- install/main.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/install/main.go b/install/main.go index 13e506d06..b3979ace9 100644 --- a/install/main.go +++ b/install/main.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "embed" "encoding/base64" + "flag" "fmt" "io" "io/fs" @@ -68,6 +69,9 @@ const ( func main() { + crowdsecFlag := flag.Bool("crowdsec", false, "Enable the CrowdSec installation prompt") + flag.Parse() + // print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking fmt.Println("Welcome to the Pangolin installer!") @@ -206,7 +210,7 @@ func main() { } } - if !checkIsCrowdsecInstalledInCompose() { + if *crowdsecFlag && !checkIsCrowdsecInstalledInCompose() { fmt.Println("\n=== CrowdSec Install ===") // check if crowdsec is installed if readBool("Would you like to install CrowdSec?", false) {