import { Client, clientSiteResourcesAssociationsCache, clientSitesAssociationsCache, db, exitNodes, networks, SiteResource, siteNetworks, siteResources, sites } from "@server/db"; import { Alias, generateAliasConfig, generateRemoteSubnets } from "@server/lib/ip"; import logger from "@server/logger"; import { eq, inArray } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; import config from "@server/lib/config"; export async function buildSiteConfigurationForOlmClient( client: Client, publicKey: string | null, relay: boolean, jitMode: boolean = false ) { const siteConfigurations: { siteId: number; name?: string; endpoint?: string; publicKey?: string; serverIP?: string | null; serverPort?: number | null; remoteSubnets?: string[]; aliases: Alias[]; }[] = []; // Get all sites data const sitesData = await db .select() .from(sites) .innerJoin( clientSitesAssociationsCache, eq(sites.siteId, clientSitesAssociationsCache.siteId) ) .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 = siteResourcesBySiteId.get(site.siteId) ?? []; if (jitMode) { // Add site configuration to the array siteConfigurations.push({ siteId: site.siteId, // remoteSubnets: generateRemoteSubnets(allSiteResources), aliases: generateAliasConfig(allSiteResources) }); continue; } if (!site.exitNodeId) { logger.warn( `Site ${site.siteId} does not have exit node, skipping` ); continue; } // Validate endpoint and hole punch status if (!site.endpoint) { logger.warn( `In olm register: site ${site.siteId} has no endpoint, skipping` ); 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`); continue; } // if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) { // logger.warn( // `Site ${site.siteId} last hole punch is too old, skipping` // ); // continue; // } // If public key changed, delete old peer from this site if (client.pubKey && client.pubKey != publicKey) { logger.info( `Public key mismatch. Deleting old peer from site ${site.siteId}...` ); peerOps.push(deletePeer(site.siteId, client.pubKey!)); } if (!site.subnet) { logger.warn(`Site ${site.siteId} has no subnet, skipping`); continue; } // 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 ${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 }) ); } else { logger.warn( `Client ${client.clientId} has no endpoint, skipping peer addition` ); } let relayEndpoint: string | undefined = undefined; if (relay) { const exitNode = exitNodesById.get(site.exitNodeId); if (!exitNode) { logger.warn(`Exit node not found for site ${site.siteId}`); continue; } relayEndpoint = `${exitNode.endpoint}:${clientsStartPort}`; } // Add site configuration to the array siteConfigurations.push({ siteId: site.siteId, name: site.name, // relayEndpoint: relayEndpoint, // this can be undefined now if not relayed // lets not do this for now because it would conflict with the hole punch testing endpoint: site.endpoint, publicKey: site.publicKey, serverIP: site.address, serverPort: site.listenPort, 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); }); } return siteConfigurations; }