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 { and, eq } from "drizzle-orm";
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 {
AiProviderAuthType,
applyAiProviderAuthHeaders,
applyAiProviderCustomHeaders,
authTypeRequiresApiKey
} from "@server/lib/aiProviderDefaults";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
@@ -15,6 +23,12 @@ const PROVIDER_TARGETS_TTL_SEC = 7;
// match gerbil's `pangolinDestHeader` constant.
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([
"p-host",
"host",
@@ -36,6 +50,10 @@ type ResolvedProviderTarget = {
// gerbil as the destination to proxy the request to over the WireGuard
// tunnel.
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/*).
gerbilBaseUrl: string;
};
@@ -46,6 +64,7 @@ async function fetchProviderTargets(
const rows = await db
.select({
targetId: targets.targetId,
ip: targets.ip,
internalPort: targets.internalPort,
port: targets.port,
method: targets.method,
@@ -72,6 +91,7 @@ async function fetchProviderTargets(
resolved.push({
targetId: row.targetId,
destination: `${scheme}://${host}:${port}`,
hostHeader: row.ip,
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
* provider's site targets, via that site's gerbil sidecar. Gerbil's
* /router/* endpoint forwards the request (untouched body, same path minus
* the /router prefix, and all headers besides PANGOLIN_DEST_HEADER) over the
* WireGuard tunnel to the destination named in that header. Always writes a
* response to `res`, including on failure.
* the /router prefix, and all headers besides PANGOLIN_DEST_HEADER and
* PANGOLIN_HOST_HEADER) over the WireGuard tunnel to the destination named
* 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(
req: Request,
@@ -147,7 +168,30 @@ export async function proxyAiGatewayToSiteTarget(
}
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_HOST_HEADER] = target.hostHeader;
const body = JSON.stringify(req.body);
@@ -155,7 +199,10 @@ export async function proxyAiGatewayToSiteTarget(
providerId: provider.providerId,
targetId: target.targetId,
destination: target.destination,
url: gerbilUrl
hostHeader: target.hostHeader,
url: gerbilUrl,
headers,
body: req.body
});
let upstreamRes: globalThis.Response;