Compare commits

...

4 Commits

Author SHA1 Message Date
Owen 9c18936be7 Filter the nodes based on the preference labels 2026-06-29 11:40:25 -04:00
Owen cf07cceb5d Fix bad col in pg 2026-06-29 11:31:34 -04:00
Owen c9cc9581b1 Add batch update 2026-06-26 23:24:14 -04:00
Owen eac7c67dcc Send down remote subnets 2026-06-26 18:09:56 -04:00
6 changed files with 151 additions and 13 deletions
+2 -2
View File
@@ -216,7 +216,7 @@ export const remoteExitNodePreferenceLabels = pgTable(
remoteExitNodePreferenceLabelId: serial( remoteExitNodePreferenceLabelId: serial(
"remoteExitNodePreferenceLabelId" "remoteExitNodePreferenceLabelId"
).primaryKey(), ).primaryKey(),
remoteExitNode: integer("remoteExitNode") remoteExitNodeId: varchar("remoteExitNodeId")
.references(() => remoteExitNodes.remoteExitNodeId, { .references(() => remoteExitNodes.remoteExitNodeId, {
onDelete: "cascade" onDelete: "cascade"
}) })
@@ -229,7 +229,7 @@ export const remoteExitNodePreferenceLabels = pgTable(
}, },
(t) => [ (t) => [
unique("remote_exit_node_preference_label_uniq").on( unique("remote_exit_node_preference_label_uniq").on(
t.remoteExitNode, t.remoteExitNodeId,
t.labelId t.labelId
) )
] ]
+81 -4
View File
@@ -18,12 +18,15 @@ import {
resources, resources,
targets, targets,
sites, sites,
siteLabels,
remoteExitNodes,
remoteExitNodePreferenceLabels,
targetHealthCheck, targetHealthCheck,
Transaction Transaction
} from "@server/db"; } from "@server/db";
import logger from "@server/logger"; import logger from "@server/logger";
import { ExitNodePingResult } from "@server/routers/newt"; import { ExitNodePingResult } from "@server/routers/newt";
import { eq, and, or, ne, isNull } from "drizzle-orm"; import { eq, and, or, ne, isNull, inArray } from "drizzle-orm";
import axios from "axios"; import axios from "axios";
import config from "../config"; import config from "../config";
@@ -150,7 +153,8 @@ export async function verifyExitNodeOrgAccess(
export async function listExitNodes( export async function listExitNodes(
orgId: string, orgId: string,
filterOnline = false, filterOnline = false,
noCloud = false noCloud = false,
siteId?: number
) { ) {
const allExitNodes = await db const allExitNodes = await db
.select({ .select({
@@ -237,7 +241,7 @@ export async function listExitNodes(
// }) // })
// ); // );
const remoteExitNodes = allExitNodes.filter( let remoteExitNodesList = allExitNodes.filter(
(node) => (node) =>
node.type === "remoteExitNode" && (!filterOnline || node.online) node.type === "remoteExitNode" && (!filterOnline || node.online)
); );
@@ -246,9 +250,82 @@ export async function listExitNodes(
node.type === "gerbil" && (!filterOnline || node.online) && !noCloud node.type === "gerbil" && (!filterOnline || node.online) && !noCloud
); );
// Apply label-based filtering to remote exit nodes if siteId is provided
if (siteId !== undefined && remoteExitNodesList.length > 0) {
// Get the site's labels
const siteLabelRows = await db
.select({ labelId: siteLabels.labelId })
.from(siteLabels)
.where(eq(siteLabels.siteId, siteId));
const siteLabelIds = new Set(siteLabelRows.map((r) => r.labelId));
// Get the remoteExitNode records for these exit nodes so we have the remoteExitNodeId
const exitNodeIds = remoteExitNodesList.map((n) => n.exitNodeId);
const remoteNodeRows = await db
.select({
exitNodeId: remoteExitNodes.exitNodeId,
remoteExitNodeId: remoteExitNodes.remoteExitNodeId
})
.from(remoteExitNodes)
.where(inArray(remoteExitNodes.exitNodeId, exitNodeIds));
const exitNodeIdToRemoteId = new Map(
remoteNodeRows
.filter((r) => r.exitNodeId !== null)
.map((r) => [r.exitNodeId!, r.remoteExitNodeId])
);
// Get preference labels for all remote exit nodes
const remoteExitNodeIds = remoteNodeRows.map((r) => r.remoteExitNodeId);
const prefLabelRows =
remoteExitNodeIds.length > 0
? await db
.select({
remoteExitNodeId:
remoteExitNodePreferenceLabels.remoteExitNodeId,
labelId: remoteExitNodePreferenceLabels.labelId
})
.from(remoteExitNodePreferenceLabels)
.where(
inArray(
remoteExitNodePreferenceLabels.remoteExitNodeId,
remoteExitNodeIds
)
)
: [];
// Build a map of remoteExitNodeId -> Set of labelIds
const prefLabelsMap = new Map<string, Set<number>>();
for (const row of prefLabelRows) {
if (!prefLabelsMap.has(row.remoteExitNodeId)) {
prefLabelsMap.set(row.remoteExitNodeId, new Set());
}
prefLabelsMap.get(row.remoteExitNodeId)!.add(row.labelId);
}
// Filter: include node if it has no preference labels, or if site shares at least one label
const filtered = remoteExitNodesList.filter((node) => {
const remoteId = exitNodeIdToRemoteId.get(node.exitNodeId);
if (!remoteId) return true; // no remoteExitNode record, don't filter
const prefLabels = prefLabelsMap.get(remoteId);
if (!prefLabels || prefLabels.size === 0) return true; // no preference labels, include
// include only if site has at least one matching label
for (const labelId of siteLabelIds) {
if (prefLabels.has(labelId)) return true;
}
return false;
});
// Only apply the filtered list if at least one remote node remains;
// otherwise fall through to the gerbil fallback below
if (filtered.length > 0 || remoteExitNodesList.length === 0) {
remoteExitNodesList = filtered;
}
}
// THIS PROVIDES THE FALL // THIS PROVIDES THE FALL
const exitNodesList = const exitNodesList =
remoteExitNodes.length > 0 ? remoteExitNodes : gerbilExitNodes; remoteExitNodesList.length > 0 ? remoteExitNodesList : gerbilExitNodes;
return exitNodesList; return exitNodesList;
} }
@@ -13,13 +13,20 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, remoteExitNodeResources, remoteExitNodes } from "@server/db"; import {
db,
newts,
remoteExitNodeResources,
remoteExitNodes,
sites
} from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { sendToClientsBatch } from "#private/routers/ws";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
orgId: z.string().min(1), orgId: z.string().min(1),
@@ -113,6 +120,25 @@ export async function setRemoteExitNodeResources(
eq(remoteExitNodeResources.remoteExitNodeId, remoteExitNodeId) eq(remoteExitNodeResources.remoteExitNodeId, remoteExitNodeId)
); );
// Notify all newts connected to this remote exit node's exit node
if (remoteExitNode.exitNodeId) {
const connectedNewts = await db
.select({ newtId: newts.newtId })
.from(newts)
.innerJoin(sites, eq(newts.siteId, sites.siteId))
.where(eq(sites.exitNodeId, remoteExitNode.exitNodeId));
await sendToClientsBatch(
connectedNewts.map(({ newtId }) => ({
clientId: newtId,
message: {
type: "newt/wg/subnets/update",
data: { subnets: destinations }
}
}))
);
}
return response<SetRemoteExitNodeResourcesResponse>(res, { return response<SetRemoteExitNodeResourcesResponse>(res, {
data: { resources }, data: { resources },
success: true, success: true,
+18 -2
View File
@@ -5,6 +5,7 @@ import {
db, db,
ExitNode, ExitNode,
networks, networks,
remoteExitNodeResources,
resources, resources,
Site, Site,
siteNetworks, siteNetworks,
@@ -223,7 +224,8 @@ export async function buildClientConfigurationForNewtClient(
export async function buildTargetConfigurationForNewtClient( export async function buildTargetConfigurationForNewtClient(
siteId: number, siteId: number,
version?: string | null version?: string | null,
remoteExitNodeId?: string
) { ) {
// Get all enabled targets with their resource mode information // Get all enabled targets with their resource mode information
const allTargets = await db const allTargets = await db
@@ -379,10 +381,24 @@ export async function buildTargetConfigurationForNewtClient(
}; };
}); });
let remoteExitNodeSubnets: string[] = [];
if (remoteExitNodeId) {
const remoteNodeResources = await db
.select()
.from(remoteExitNodeResources)
.where(
eq(remoteExitNodeResources.remoteExitNodeId, remoteExitNodeId)
);
// filter through these and provide the subnets
remoteExitNodeSubnets = remoteNodeResources.map((r) => r.destination);
}
return { return {
validHealthCheckTargets, validHealthCheckTargets,
tcpTargets, tcpTargets,
udpTargets, udpTargets,
browserGatewayTargets browserGatewayTargets,
remoteExitNodeSubnets
}; };
} }
@@ -38,7 +38,8 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
const exitNodesList = await listExitNodes( const exitNodesList = await listExitNodes(
site.orgId, site.orgId,
true, true,
noCloud || false noCloud || false,
newt.siteId
); // filter for only the online ones ); // filter for only the online ones
let lastExitNodeId = null; let lastExitNodeId = null;
@@ -1,4 +1,4 @@
import { db, ExitNode, newts, Transaction } from "@server/db"; import { db, ExitNode, newts, remoteExitNodes, Transaction } from "@server/db";
import { MessageHandler } from "@server/routers/ws"; import { MessageHandler } from "@server/routers/ws";
import { exitNodes, Newt, sites } from "@server/db"; import { exitNodes, Newt, sites } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -196,12 +196,29 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
.where(eq(newts.newtId, newt.newtId)); .where(eq(newts.newtId, newt.newtId));
} }
let remoteExitNodeId: string | undefined;
if (exitNode.type == "remoteExitNode") {
// get the remote exit node ID associated with this exit node
const [remoteExitNode] = await db
.select()
.from(remoteExitNodes)
.where(eq(remoteExitNodes.exitNodeId, exitNode.exitNodeId))
.limit(1);
remoteExitNodeId = remoteExitNode?.remoteExitNodeId;
}
const { const {
tcpTargets, tcpTargets,
udpTargets, udpTargets,
validHealthCheckTargets, validHealthCheckTargets,
browserGatewayTargets browserGatewayTargets,
} = await buildTargetConfigurationForNewtClient(siteId, newtVersion); remoteExitNodeSubnets
} = await buildTargetConfigurationForNewtClient(
siteId,
newtVersion,
remoteExitNodeId // this is for the remote node resources
);
logger.debug( logger.debug(
`Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}` `Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}`
@@ -222,6 +239,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
}, },
healthCheckTargets: validHealthCheckTargets, healthCheckTargets: validHealthCheckTargets,
browserGatewayTargets: browserGatewayTargets, browserGatewayTargets: browserGatewayTargets,
remoteExitNodeSubnets: remoteExitNodeSubnets,
chainId: chainId chainId: chainId
} }
}, },