import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { clients, exitNodes, newts, olms, Site, sites, clientSitesAssociationsCache, ExitNode } from "@server/db"; import { db } from "@server/db"; import { eq, inArray } from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; // Define Zod schema for request validation const getAllRelaysSchema = z.object({ publicKey: z.string().optional() }); // Type for peer destination interface PeerDestination { destinationIP: string; destinationPort: number; } // Updated mappings type to support multiple destinations per endpoint interface ProxyMapping { destinations: PeerDestination[]; } export async function getAllRelays( req: Request, res: Response, next: NextFunction ): Promise { try { // Validate request parameters const parsedParams = getAllRelaysSchema.safeParse(req.body); if (!parsedParams.success) { return next( createHttpError( HttpCode.BAD_REQUEST, fromError(parsedParams.error).toString() ) ); } const { publicKey } = parsedParams.data; if (!publicKey) { return next( createHttpError(HttpCode.BAD_REQUEST, "publicKey is required") ); } // Fetch exit node const [exitNode] = await db .select() .from(exitNodes) .where(eq(exitNodes.publicKey, publicKey)); if (!exitNode) { return next( createHttpError(HttpCode.NOT_FOUND, "Exit node not found") ); } const mappings = await generateRelayMappings(exitNode); logger.debug( `Returning mappings for ${Object.keys(mappings).length} endpoints` ); return res.status(HttpCode.OK).send({ mappings }); } catch (error) { logger.error(error); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..." ) ); } } export async function generateRelayMappings(exitNode: ExitNode) { // Fetch sites for this exit node const sitesRes = await db .select() .from(sites) .where(eq(sites.exitNodeId, exitNode.exitNodeId)); if (sitesRes.length === 0) { 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 } = {}; // 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: [] }; } const key = `${dest.destinationIP}:${dest.destinationPort}`; if (!destSet.has(key)) { destSet.add(key); mappings[endpoint].destinations.push(dest); } }; // 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 }; // 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); } } // 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 }); } } } } return mappings; }