diff --git a/server/db/sqlite/driver.ts b/server/db/sqlite/driver.ts index a7eee52b7..6b115e4ee 100644 --- a/server/db/sqlite/driver.ts +++ b/server/db/sqlite/driver.ts @@ -6,13 +6,48 @@ import fs from "fs"; import { APP_PATH } from "@server/lib/consts"; import { existsSync, mkdirSync } from "fs"; +// Temporary diagnostic trace for the random better-sqlite3 native aborts +// (Statement::~Statement -> RemoveEnvironmentCleanupHook assertion). That +// abort is a hard SIGABRT from native code, so it bypasses uncaughtException/ +// unhandledRejection and can outrun winston's async file transport. This +// writes every statement text synchronously (fsync'd via appendFileSync) so +// the statements immediately preceding a crash survive it. better-sqlite3's +// `verbose` hook fires for BEGIN/SAVEPOINT/RELEASE/COMMIT/ROLLBACK too, since +// those are just prepared statements under the hood - so this also lets us +// see if two "transactions" ever overlap on the shared connection. +// Enable with SQL_TRACE=true; remove once the crash is root-caused. +function sqlTraceVerbose(): + | ((message: unknown, ...args: unknown[]) => void) + | undefined { + if (process.env.SQL_TRACE !== "true") { + return undefined; + } + const traceLogDir = path.join(APP_PATH, "logs"); + if (!existsSync(traceLogDir)) { + mkdirSync(traceLogDir, { recursive: true }); + } + const traceLogPath = path.join(traceLogDir, "sql-trace.log"); + let seq = 0; + return (message: unknown) => { + seq += 1; + const line = `${new Date().toISOString()} pid=${process.pid} #${seq} ${String(message).replace(/\s+/g, " ").trim()}\n`; + try { + fs.appendFileSync(traceLogPath, line); + } catch { + // best-effort diagnostic logging only + } + }; +} + export const location = path.join(APP_PATH, "db", "db.sqlite"); export const exists = checkFileExists(location); bootstrapVolume(); function createDb() { - const sqlite = new Database(location); + const verbose = + process.env.QUERY_LOGGING == "true" ? sqlTraceVerbose() : undefined; + const sqlite = new Database(location, { verbose }); if (process.env.ENABLE_SQLITE_WAL_MODE == "true") { // Enable WAL mode — allows concurrent readers + single writer, preventing diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 67c037550..47d6b69c8 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -1542,6 +1542,12 @@ export async function getTraefikConfig( aiGatewayHost = undefined; } + // The p-host smuggling above is only necessary when the AI gateway + // is overridden to a different host than the resource's own. In the + // default case, leave the Host header untouched so it's visible on + // the other end. + const aiGatewayOverride = config.getRawConfig().server.ai_gateway_override; + // Public inference resources: same TLS/cert-resolver handling as // plain http-mode resources, but the service points at the AI // gateway instead of any real backend targets. @@ -1607,23 +1613,24 @@ export async function getTraefikConfig( } } - const irHeadersMiddlewareName = `${irKey}-headers-middleware`; - config_output.http.middlewares[irHeadersMiddlewareName] = { - headers: { - customRequestHeaders: { - ...(aiGatewayHost ? { Host: aiGatewayHost } : {}), - "p-host": fullDomain - } - } - }; - const additionalMiddlewares = config.getRawConfig().traefik.additional_middlewares || []; - const routerMiddlewares = [ - badgerMiddlewareName, - irHeadersMiddlewareName, - ...additionalMiddlewares - ]; + const routerMiddlewares = [badgerMiddlewareName]; + + if (aiGatewayOverride) { + const irHeadersMiddlewareName = `${irKey}-headers-middleware`; + config_output.http.middlewares[irHeadersMiddlewareName] = { + headers: { + customRequestHeaders: { + ...(aiGatewayHost ? { Host: aiGatewayHost } : {}), + "p-host": fullDomain + } + } + }; + routerMiddlewares.push(irHeadersMiddlewareName); + } + + routerMiddlewares.push(...additionalMiddlewares); if (ir.ssl) { config_output.http.routers[routerName + "-redirect"] = { @@ -1705,22 +1712,24 @@ export async function getTraefikConfig( } } - const srHeadersMiddlewareName = `${srKey}-headers-middleware`; - config_output.http.middlewares[srHeadersMiddlewareName] = { - headers: { - customRequestHeaders: { - ...(aiGatewayHost ? { Host: aiGatewayHost } : {}), - "p-host": alias - } - } - }; - const additionalMiddlewares = config.getRawConfig().traefik.additional_middlewares || []; - const routerMiddlewares = [ - srHeadersMiddlewareName, - ...additionalMiddlewares - ]; + const routerMiddlewares: string[] = []; + + if (aiGatewayOverride) { + const srHeadersMiddlewareName = `${srKey}-headers-middleware`; + config_output.http.middlewares[srHeadersMiddlewareName] = { + headers: { + customRequestHeaders: { + ...(aiGatewayHost ? { Host: aiGatewayHost } : {}), + "p-host": alias + } + } + }; + routerMiddlewares.push(srHeadersMiddlewareName); + } + + routerMiddlewares.push(...additionalMiddlewares); if (sr.ssl) { config_output.http.routers[routerName + "-redirect"] = { diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index 77921c39f..ffc3c6120 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { sites, exitNodes, ExitNode } from "@server/db"; +import { sites, exitNodes, ExitNode, clients } from "@server/db"; import { db } from "@server/db"; import { eq, isNotNull, and } from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; @@ -93,7 +93,23 @@ export async function generateGerbilConfig(exitNode: ExitNode) { ) ); - const peers = await Promise.all( + const clientsRes = await db + .select() + .from(clients) + .where( + and( + eq(clients.exitNodeId, exitNode.exitNodeId), + isNotNull(clients.pubKey), + isNotNull(clients.exitNodeSubnet) + ) + ); + + let peers: { + publicKey: string | null; + allowedIps: string[]; + }[] = []; + + const sitePeers = await Promise.all( sitesRes.map(async (site) => { if (site.type === "wireguard") { return { @@ -113,6 +129,15 @@ export async function generateGerbilConfig(exitNode: ExitNode) { }) ); + const clientPeers = clientsRes.map((client) => { + return { + publicKey: client.pubKey, + allowedIps: [client.exitNodeSubnet!] + }; + }); + + peers = [...sitePeers, ...clientPeers]; + const configResponse: GetConfigResponse = { listenPort: exitNode.listenPort || 51820, ipAddress: exitNode.address,