Restrict the ai site resource router to exit node subnet

This commit is contained in:
Owen
2026-08-06 10:42:23 -04:00
parent 656eea5bb1
commit 75ce7e91d7
2 changed files with 38 additions and 4 deletions
+19 -2
View File
@@ -4,7 +4,8 @@ import {
domains, domains,
aiProviders, aiProviders,
resourceAiProviders, resourceAiProviders,
siteResources siteResources,
exitNodes
} from "@server/db"; } from "@server/db";
import { import {
and, and,
@@ -22,6 +23,7 @@ import config from "@server/lib/config";
import { resources, sites, Target, targets } from "@server/db"; import { resources, sites, Target, targets } from "@server/db";
import createPathRewriteMiddleware from "./middleware"; import createPathRewriteMiddleware from "./middleware";
import { sanitize, encodePath, validatePathRewriteConfig } from "./utils"; import { sanitize, encodePath, validatePathRewriteConfig } from "./utils";
import regionalCache from "@server/lib/cache";
const redirectHttpsMiddlewareName = "redirect-to-https"; const redirectHttpsMiddlewareName = "redirect-to-https";
const badgerMiddlewareName = "badger"; const badgerMiddlewareName = "badger";
@@ -55,6 +57,21 @@ export async function getTraefikConfig(
browserGatewayUiUrl: string | null = null, // UNUSED BUT USED IN PRIVATE browserGatewayUiUrl: string | null = null, // UNUSED BUT USED IN PRIVATE
aiGatewayUrl: string | null = null aiGatewayUrl: string | null = null
): Promise<any> { ): Promise<any> {
// Get the exit node but cache it for 5 minutes to avoid hitting the DB too often
const exitNodeCacheKey = `exitNode:${exitNodeId}`;
let exitNode =
await regionalCache.get<typeof exitNodes.$inferSelect>(
exitNodeCacheKey
);
if (!exitNode) {
[exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, exitNodeId))
.limit(1);
await regionalCache.set(exitNodeCacheKey, exitNode, 300);
}
// Get resources with their targets and sites in a single optimized query // Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources // Start from sites on this exit node, then join to targets and resources
const resourcesWithTargetsAndSites = await db const resourcesWithTargetsAndSites = await db
@@ -866,7 +883,7 @@ export async function getTraefikConfig(
const srKey = `inference-sr${sr.siteResourceId}`; const srKey = `inference-sr${sr.siteResourceId}`;
const routerName = `${srKey}-router`; const routerName = `${srKey}-router`;
const serviceName = `${srKey}-service`; const serviceName = `${srKey}-service`;
const rule = `Host(\`${alias}\`)`; const rule = `Host(\`${alias}\`) && ClientIP(${exitNode.address})`; // restrict to coming from the exit node ip range that the client is connected to
const domainParts = alias.split("."); const domainParts = alias.split(".");
const wildCard = const wildCard =
+19 -2
View File
@@ -58,6 +58,7 @@ import {
getValidCertificatesForDomains getValidCertificatesForDomains
} from "#private/lib/certificates"; } from "#private/lib/certificates";
import { build } from "@server/build"; import { build } from "@server/build";
import regionalCache from "#private/lib/cache";
const redirectHttpsMiddlewareName = "redirect-to-https"; const redirectHttpsMiddlewareName = "redirect-to-https";
const redirectToRootMiddlewareName = "redirect-to-root"; const redirectToRootMiddlewareName = "redirect-to-root";
@@ -94,6 +95,21 @@ export async function getTraefikConfig(
browserGatewayUiUrl: string | null = null, browserGatewayUiUrl: string | null = null,
aiGatewayUrl: string | null = null aiGatewayUrl: string | null = null
): Promise<any> { ): Promise<any> {
// Get the exit node but cache it for 5 minutes to avoid hitting the DB too often
const exitNodeCacheKey = `exitNode:${exitNodeId}`;
let exitNode =
await regionalCache.get<typeof exitNodes.$inferSelect>(
exitNodeCacheKey
);
if (!exitNode) {
[exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, exitNodeId))
.limit(1);
await regionalCache.set(exitNodeCacheKey, exitNode, 300);
}
// Get resources with their targets and sites in a single optimized query // Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources // Start from sites on this exit node, then join to targets and resources
const resourcesWithTargetsAndSites = await db const resourcesWithTargetsAndSites = await db
@@ -1546,7 +1562,8 @@ export async function getTraefikConfig(
// is overridden to a different host than the resource's own. In the // 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 // default case, leave the Host header untouched so it's visible on
// the other end. // the other end.
const aiGatewayOverride = config.getRawConfig().server.ai_gateway_override; const aiGatewayOverride =
config.getRawConfig().server.ai_gateway_override;
// Public inference resources: same TLS/cert-resolver handling as // Public inference resources: same TLS/cert-resolver handling as
// plain http-mode resources, but the service points at the AI // plain http-mode resources, but the service points at the AI
@@ -1679,7 +1696,7 @@ export async function getTraefikConfig(
const srKey = `inference-sr${sr.siteResourceId}`; const srKey = `inference-sr${sr.siteResourceId}`;
const routerName = `${srKey}-router`; const routerName = `${srKey}-router`;
const serviceName = `${srKey}-service`; const serviceName = `${srKey}-service`;
const rule = `Host(\`${alias}\`)`; const rule = `Host(\`${alias}\`) && ClientIP(${exitNode.address})`; // restrict to coming from the exit node ip range that the client is connected to
let tls: any = {}; let tls: any = {};
if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {