Merge branch 'dev' into clients-user

This commit is contained in:
miloschwartz
2025-12-05 15:17:32 -05:00
27 changed files with 2882 additions and 1896 deletions

View File

@@ -1,8 +1,8 @@
import { db, exitNodeOrgs, newts } from "@server/db";
import { db, ExitNode, exitNodeOrgs, newts, Transaction } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { exitNodes, Newt, resources, sites, Target, targets } from "@server/db";
import { targetHealthCheck } from "@server/db";
import { eq, and, sql, inArray } from "drizzle-orm";
import { eq, and, sql, inArray, ne } from "drizzle-orm";
import { addPeer, deletePeer } from "../gerbil/peers";
import logger from "@server/logger";
import config from "@server/lib/config";
@@ -17,6 +17,7 @@ import {
verifyExitNodeOrgAccess
} from "#dynamic/lib/exitNodes";
import { fetchContainers } from "./dockerSocket";
import { lockManager } from "#dynamic/lib/lock";
export type ExitNodePingResult = {
exitNodeId: number;
@@ -151,27 +152,8 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
return;
}
const sitesQuery = await db
.select({
subnet: sites.subnet
})
.from(sites)
.where(eq(sites.exitNodeId, exitNodeId));
const newSubnet = await getUniqueSubnetForSite(exitNode);
const blockSize = config.getRawConfig().gerbil.site_block_size;
const subnets = sitesQuery
.map((site) => site.subnet)
.filter(
(subnet) =>
subnet && /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(subnet)
)
.filter((subnet) => subnet !== null);
subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`));
const newSubnet = findNextAvailableCidr(
subnets,
blockSize,
exitNode.address
);
if (!newSubnet) {
logger.error(
`No available subnets found for the new exit node id ${exitNodeId} and site id ${siteId}`
@@ -272,7 +254,8 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
hcUnhealthyInterval: targetHealthCheck.hcUnhealthyInterval,
hcTimeout: targetHealthCheck.hcTimeout,
hcHeaders: targetHealthCheck.hcHeaders,
hcMethod: targetHealthCheck.hcMethod
hcMethod: targetHealthCheck.hcMethod,
hcTlsServerName: targetHealthCheck.hcTlsServerName,
})
.from(targets)
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
@@ -344,7 +327,8 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
hcUnhealthyInterval: target.hcUnhealthyInterval, // in seconds
hcTimeout: target.hcTimeout, // in seconds
hcHeaders: hcHeadersSend,
hcMethod: target.hcMethod
hcMethod: target.hcMethod,
hcTlsServerName: target.hcTlsServerName,
};
});
@@ -376,3 +360,39 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
excludeSender: false // Include sender in broadcast
};
};
async function getUniqueSubnetForSite(
exitNode: ExitNode,
trx: Transaction | typeof db = db
): Promise<string | null> {
const lockKey = `subnet-allocation:${exitNode.exitNodeId}`;
return await lockManager.withLock(
lockKey,
async () => {
const sitesQuery = await trx
.select({
subnet: sites.subnet
})
.from(sites)
.where(eq(sites.exitNodeId, exitNode.exitNodeId));
const blockSize = config.getRawConfig().gerbil.site_block_size;
const subnets = sitesQuery
.map((site) => site.subnet)
.filter(
(subnet) =>
subnet && /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(subnet)
)
.filter((subnet) => subnet !== null);
subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`));
const newSubnet = findNextAvailableCidr(
subnets,
blockSize,
exitNode.address
);
return newSubnet;
},
5000 // 5 second lock TTL - subnet allocation should be quick
);
}

View File

@@ -66,7 +66,8 @@ export async function addTargets(
hcUnhealthyInterval: hc.hcUnhealthyInterval, // in seconds
hcTimeout: hc.hcTimeout, // in seconds
hcHeaders: hcHeadersSend,
hcMethod: hc.hcMethod
hcMethod: hc.hcMethod,
hcTlsServerName: hc.hcTlsServerName,
};
});

View File

@@ -198,6 +198,62 @@ export async function createSite(
}
}
if (subnet && exitNodeId) {
//make sure the subnet is in the range of the exit node if provided
const [exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, exitNodeId));
if (!exitNode) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Exit node not found")
);
}
if (!exitNode.address) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Exit node has no subnet defined"
)
);
}
const subnetIp = subnet.split("/")[0];
if (!isIpInCidr(subnetIp, exitNode.address)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Subnet is not in the CIDR range of the exit node address."
)
);
}
// lets also make sure there is no overlap with other sites on the exit node
const sitesQuery = await db
.select({
subnet: sites.subnet
})
.from(sites)
.where(
and(
eq(sites.exitNodeId, exitNodeId),
eq(sites.subnet, subnet)
)
);
if (sitesQuery.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
`Subnet ${subnet} overlaps with an existing site on this exit node. Please restart site creation.`
)
);
}
}
const niceId = await getUniqueSiteName(orgId);
let newSite: Site;

View File

@@ -48,6 +48,7 @@ const createTargetSchema = z.strictObject({
hcFollowRedirects: z.boolean().optional().nullable(),
hcMethod: z.string().min(1).optional().nullable(),
hcStatus: z.int().optional().nullable(),
hcTlsServerName: z.string().optional().nullable(),
path: z.string().optional().nullable(),
pathMatchType: z
.enum(["exact", "prefix", "regex"])
@@ -247,7 +248,8 @@ export async function createTarget(
hcFollowRedirects: targetData.hcFollowRedirects ?? null,
hcMethod: targetData.hcMethod ?? null,
hcStatus: targetData.hcStatus ?? null,
hcHealth: "unknown"
hcHealth: "unknown",
hcTlsServerName: targetData.hcTlsServerName ?? null
})
.returning();

View File

@@ -57,6 +57,7 @@ function queryTargets(resourceId: number) {
hcMethod: targetHealthCheck.hcMethod,
hcStatus: targetHealthCheck.hcStatus,
hcHealth: targetHealthCheck.hcHealth,
hcTlsServerName: targetHealthCheck.hcTlsServerName,
path: targets.path,
pathMatchType: targets.pathMatchType,
rewritePath: targets.rewritePath,

View File

@@ -42,6 +42,7 @@ const updateTargetBodySchema = z.strictObject({
hcFollowRedirects: z.boolean().optional().nullable(),
hcMethod: z.string().min(1).optional().nullable(),
hcStatus: z.int().optional().nullable(),
hcTlsServerName: z.string().optional().nullable(),
path: z.string().optional().nullable(),
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(),
rewritePath: z.string().optional().nullable(),
@@ -217,7 +218,8 @@ export async function updateTarget(
hcHeaders: hcHeaders,
hcFollowRedirects: parsedBody.data.hcFollowRedirects,
hcMethod: parsedBody.data.hcMethod,
hcStatus: parsedBody.data.hcStatus
hcStatus: parsedBody.data.hcStatus,
hcTlsServerName: parsedBody.data.hcTlsServerName,
})
.where(eq(targetHealthCheck.targetId, targetId))
.returning();