From b8bead05902f9825fc61ffcd822d47af9212be2e Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 19 Oct 2025 11:13:14 -0700 Subject: [PATCH] Select exit node for local sites --- server/routers/site/createSite.ts | 67 +++++++++++++++++++++++----- server/setup/scriptsPg/1.11.1.ts | 45 +++++++++++++++++++ server/setup/scriptsSqlite/1.11.1.ts | 37 +++++++++++++++ 3 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 server/setup/scriptsPg/1.11.1.ts create mode 100644 server/setup/scriptsSqlite/1.11.1.ts diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 0ffc5956..36e049bc 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -17,6 +17,7 @@ import { hashPassword } from "@server/auth/password"; import { isValidIP } from "@server/lib/validators"; import { isIpInCidr } from "@server/lib/ip"; import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes"; +import { build } from "@server/build"; const createSiteParamsSchema = z .object({ @@ -203,10 +204,10 @@ export async function createSite( const niceId = await getUniqueSiteName(orgId); - await db.transaction(async (trx) => { - let newSite: Site; + let newSite: Site; - if ((type == "wireguard" || type == "newt") && exitNodeId) { + await db.transaction(async (trx) => { + if (type == "wireguard" || type == "newt") { // we are creating a site with an exit node (tunneled) if (!subnet) { return next( @@ -217,11 +218,19 @@ export async function createSite( ); } - const { exitNode, hasAccess } = - await verifyExitNodeOrgAccess( - exitNodeId, - orgId + if (!exitNodeId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Exit node ID is required for tunneled sites" + ) ); + } + + const { exitNode, hasAccess } = await verifyExitNodeOrgAccess( + exitNodeId, + orgId + ); if (!exitNode) { logger.warn("Exit node not found"); @@ -257,13 +266,51 @@ export async function createSite( ...(pubKey && type == "wireguard" && { pubKey }) }) .returning(); - } else { - // we are creating a site with no tunneling + } else if (type == "local") { + let exitNodeIdToCreate = exitNodeId; + if (!exitNodeIdToCreate) { + if (build == "saas") { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Exit node ID of a remote node is required for local sites" + ) + ); + } + + // select the exit node for local sites + // TODO: THIS SHOULD BE CHOSEN IN THE FRONTEND OR SOMETHING BECAUSE + // YOU CAN HAVE MORE THAN ONE NODE IN THE SYSTEM AND YOU SHOULD SELECT + // WHICH GERBIL NODE TO PUT THE SITE ON BUT FOR NOW THIS WILL DO + const [localExitNode] = await trx + .select() + .from(exitNodes) + .where(eq(exitNodes.type, "gerbil")) + .limit(1); + + if (!localExitNode) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "No gerbil exit node found for organization. Please create a gerbil exit node first." + ) + ); + } + + exitNodeIdToCreate = localExitNode.exitNodeId; + } else { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Site type not recognized" + ) + ); + } [newSite] = await trx .insert(sites) .values({ - exitNodeId: exitNodeId, + exitNodeId: exitNodeIdToCreate, orgId, name, niceId, diff --git a/server/setup/scriptsPg/1.11.1.ts b/server/setup/scriptsPg/1.11.1.ts new file mode 100644 index 00000000..9fc1422c --- /dev/null +++ b/server/setup/scriptsPg/1.11.1.ts @@ -0,0 +1,45 @@ +import { db } from "@server/db/pg/driver"; +import { sql } from "drizzle-orm"; + +const version = "1.11.1"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + // Get the first exit node with type 'gerbil' + const exitNodesQuery = await db.execute( + sql`SELECT "exitNodeId" FROM "exitNodes" WHERE "type" = 'gerbil' LIMIT 1` + ); + const exitNodes = exitNodesQuery.rows as { + exitNodeId: number; + }[]; + + const exitNodeId = exitNodes.length > 0 ? exitNodes[0].exitNodeId : null; + + // Get all sites with type 'local' + const sitesQuery = await db.execute( + sql`SELECT "siteId" FROM "sites" WHERE "type" = 'local'` + ); + const sites = sitesQuery.rows as { + siteId: number; + }[]; + + // Update sites to use the exit node + for (const site of sites) { + await db.execute(sql` + UPDATE "sites" SET "exitNodeId" = ${exitNodeId} WHERE "siteId" = ${site.siteId} + `); + } + + await db.execute(sql`COMMIT`); + console.log(`Updated sites with exit node`); + } catch (e) { + await db.execute(sql`ROLLBACK`); + console.log("Unable to update sites with exit node"); + console.log(e); + throw e; + } + + console.log(`${version} migration complete`); +} diff --git a/server/setup/scriptsSqlite/1.11.1.ts b/server/setup/scriptsSqlite/1.11.1.ts new file mode 100644 index 00000000..7f9065b6 --- /dev/null +++ b/server/setup/scriptsSqlite/1.11.1.ts @@ -0,0 +1,37 @@ +import { APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import path from "path"; + +const version = "1.11.1"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); + + db.transaction(() => { + const exitNodes = db.prepare(`SELECT * FROM exitNodes WHERE type = 'gerbil' LIMIT 1`).all() as { + exitNodeId: number; + name: string; + }[]; + + const exitNodeId = exitNodes.length > 0 ? exitNodes[0].exitNodeId : null; + + // get all of the targets + const sites = db.prepare(`SELECT * FROM sites WHERE type = 'local'`).all() as { + siteId: number; + exitNodeId: number | null; + }[]; + + const defineExitNodeOnSite = db.prepare( + `UPDATE sites SET exitNodeId = ? WHERE siteId = ?` + ); + + for (const site of sites) { + defineExitNodeOnSite.run(exitNodeId, site.siteId); + } + })(); + + console.log(`${version} migration complete`); +} \ No newline at end of file