Basic target routing

This commit is contained in:
Owen
2026-08-06 15:48:13 -04:00
parent e9f9cf54f4
commit 6c28c5f383
+51 -4
View File
@@ -1,7 +1,15 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { AiProvider, db, exitNodes, sites, targets } from "@server/db"; import { AiProvider, db, exitNodes, sites, targets } from "@server/db";
import config from "@server/lib/config";
import { decrypt } from "@server/lib/crypto";
import { localCache } from "@server/lib/cache"; import { localCache } from "@server/lib/cache";
import {
AiProviderAuthType,
applyAiProviderAuthHeaders,
applyAiProviderCustomHeaders,
authTypeRequiresApiKey
} from "@server/lib/aiProviderDefaults";
import logger from "@server/logger"; import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@@ -15,6 +23,12 @@ const PROVIDER_TARGETS_TTL_SEC = 7;
// match gerbil's `pangolinDestHeader` constant. // match gerbil's `pangolinDestHeader` constant.
const PANGOLIN_DEST_HEADER = "p-dest-header"; const PANGOLIN_DEST_HEADER = "p-dest-header";
// Header gerbil reads for the Host header value to send to the destination,
// when it should differ from PANGOLIN_DEST_HEADER (the target's configured
// ip rather than the WireGuard routing address). Must match gerbil's
// `pangolinHostHeader` constant.
const PANGOLIN_HOST_HEADER = "p-dest-host-header";
const SKIP_HEADERS = new Set([ const SKIP_HEADERS = new Set([
"p-host", "p-host",
"host", "host",
@@ -36,6 +50,10 @@ type ResolvedProviderTarget = {
// gerbil as the destination to proxy the request to over the WireGuard // gerbil as the destination to proxy the request to over the WireGuard
// tunnel. // tunnel.
destination: string; destination: string;
// The target's configured ip, passed to gerbil as the Host header to
// send to the destination (which may differ from the WireGuard routing
// address above, e.g. for vhost-based targets).
hostHeader: string;
// The target's site's exit node HTTP API base URL (gerbil's /router/*). // The target's site's exit node HTTP API base URL (gerbil's /router/*).
gerbilBaseUrl: string; gerbilBaseUrl: string;
}; };
@@ -46,6 +64,7 @@ async function fetchProviderTargets(
const rows = await db const rows = await db
.select({ .select({
targetId: targets.targetId, targetId: targets.targetId,
ip: targets.ip,
internalPort: targets.internalPort, internalPort: targets.internalPort,
port: targets.port, port: targets.port,
method: targets.method, method: targets.method,
@@ -72,6 +91,7 @@ async function fetchProviderTargets(
resolved.push({ resolved.push({
targetId: row.targetId, targetId: row.targetId,
destination: `${scheme}://${host}:${port}`, destination: `${scheme}://${host}:${port}`,
hostHeader: row.ip,
gerbilBaseUrl: row.reachableAt gerbilBaseUrl: row.reachableAt
}); });
} }
@@ -117,9 +137,10 @@ function pathFromRequest(req: Request): string {
* Proxies an AI gateway request to one of a "custom" / "target" routing-mode * Proxies an AI gateway request to one of a "custom" / "target" routing-mode
* provider's site targets, via that site's gerbil sidecar. Gerbil's * provider's site targets, via that site's gerbil sidecar. Gerbil's
* /router/* endpoint forwards the request (untouched body, same path minus * /router/* endpoint forwards the request (untouched body, same path minus
* the /router prefix, and all headers besides PANGOLIN_DEST_HEADER) over the * the /router prefix, and all headers besides PANGOLIN_DEST_HEADER and
* WireGuard tunnel to the destination named in that header. Always writes a * PANGOLIN_HOST_HEADER) over the WireGuard tunnel to the destination named
* response to `res`, including on failure. * in PANGOLIN_DEST_HEADER, sending PANGOLIN_HOST_HEADER as the Host header.
* Always writes a response to `res`, including on failure.
*/ */
export async function proxyAiGatewayToSiteTarget( export async function proxyAiGatewayToSiteTarget(
req: Request, req: Request,
@@ -147,7 +168,30 @@ export async function proxyAiGatewayToSiteTarget(
} }
headers[key] = Array.isArray(value) ? value.join(", ") : value; headers[key] = Array.isArray(value) ? value.join(", ") : value;
} }
const authType = provider.authType as AiProviderAuthType;
let apiKey: string | null = null;
if (authTypeRequiresApiKey(authType)) {
if (!provider.apiKey) {
res.status(HttpCode.INTERNAL_SERVER_ERROR).json({
error: {
message: "AI provider has no API key configured"
}
});
return;
}
const secret = config.getRawConfig().server.secret!;
apiKey = decrypt(provider.apiKey, secret);
}
applyAiProviderCustomHeaders(
headers,
provider.headers,
config.getRawConfig().server.secret!
);
applyAiProviderAuthHeaders(headers, authType, apiKey);
headers[PANGOLIN_DEST_HEADER] = target.destination; headers[PANGOLIN_DEST_HEADER] = target.destination;
headers[PANGOLIN_HOST_HEADER] = target.hostHeader;
const body = JSON.stringify(req.body); const body = JSON.stringify(req.body);
@@ -155,7 +199,10 @@ export async function proxyAiGatewayToSiteTarget(
providerId: provider.providerId, providerId: provider.providerId,
targetId: target.targetId, targetId: target.targetId,
destination: target.destination, destination: target.destination,
url: gerbilUrl hostHeader: target.hostHeader,
url: gerbilUrl,
headers,
body: req.body
}); });
let upstreamRes: globalThis.Response; let upstreamRes: globalThis.Response;