mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-10 22:48:14 +02:00
Compare commits
4 Commits
633d9031af
...
backhaul
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c18936be7 | |||
| cf07cceb5d | |||
| c9cc9581b1 | |||
| eac7c67dcc |
@@ -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
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user