Add UI, update API, send to newt

This commit is contained in:
Owen
2025-12-15 21:02:04 -05:00
committed by Owen Schwartz
parent 48110ccda3
commit c44c1a5518
11 changed files with 689 additions and 22 deletions

View File

@@ -213,7 +213,9 @@ export const siteResources = pgTable("siteResources", {
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
enabled: boolean("enabled").notNull().default(true),
alias: varchar("alias"),
aliasAddress: varchar("aliasAddress")
aliasAddress: varchar("aliasAddress"),
tcpPortRangeString: varchar("tcpPortRangeString"),
udpPortRangeString: varchar("udpPortRangeString")
});
export const clientSiteResources = pgTable("clientSiteResources", {

View File

@@ -234,7 +234,9 @@ export const siteResources = sqliteTable("siteResources", {
destination: text("destination").notNull(), // ip, cidr, hostname
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
alias: text("alias"),
aliasAddress: text("aliasAddress")
aliasAddress: text("aliasAddress"),
tcpPortRangeString: text("tcpPortRangeString"),
udpPortRangeString: text("udpPortRangeString")
});
export const clientSiteResources = sqliteTable("clientSiteResources", {

View File

@@ -1,10 +1,4 @@
import {
clientSitesAssociationsCache,
db,
SiteResource,
siteResources,
Transaction
} from "@server/db";
import { db, SiteResource, siteResources, Transaction } from "@server/db";
import { clients, orgs, sites } from "@server/db";
import { and, eq, isNotNull } from "drizzle-orm";
import config from "@server/lib/config";
@@ -476,6 +470,7 @@ export type SubnetProxyTarget = {
portRange?: {
min: number;
max: number;
protocol: "tcp" | "udp";
}[];
};
@@ -505,6 +500,10 @@ export function generateSubnetProxyTargets(
}
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
const portRange = [
...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"),
...parsePortRangeString(siteResource.udpPortRangeString, "udp")
];
if (siteResource.mode == "host") {
let destination = siteResource.destination;
@@ -515,7 +514,8 @@ export function generateSubnetProxyTargets(
targets.push({
sourcePrefix: clientPrefix,
destPrefix: destination
destPrefix: destination,
portRange
});
}
@@ -524,13 +524,15 @@ export function generateSubnetProxyTargets(
targets.push({
sourcePrefix: clientPrefix,
destPrefix: `${siteResource.aliasAddress}/32`,
rewriteTo: destination
rewriteTo: destination,
portRange
});
}
} else if (siteResource.mode == "cidr") {
targets.push({
sourcePrefix: clientPrefix,
destPrefix: siteResource.destination
destPrefix: siteResource.destination,
portRange
});
}
}
@@ -542,3 +544,117 @@ export function generateSubnetProxyTargets(
return targets;
}
// Custom schema for validating port range strings
// Format: "80,443,8000-9000" or "*" for all ports, or empty string
export const portRangeStringSchema = z
.string()
.optional()
.refine(
(val) => {
if (!val || val.trim() === "" || val.trim() === "*") {
return true;
}
// Split by comma and validate each part
const parts = val.split(",").map((p) => p.trim());
for (const part of parts) {
if (part === "") {
return false; // empty parts not allowed
}
// Check if it's a range (contains dash)
if (part.includes("-")) {
const [start, end] = part.split("-").map((p) => p.trim());
// Both parts must be present
if (!start || !end) {
return false;
}
const startPort = parseInt(start, 10);
const endPort = parseInt(end, 10);
// Must be valid numbers
if (isNaN(startPort) || isNaN(endPort)) {
return false;
}
// Must be valid port range (1-65535)
if (
startPort < 1 ||
startPort > 65535 ||
endPort < 1 ||
endPort > 65535
) {
return false;
}
// Start must be <= end
if (startPort > endPort) {
return false;
}
} else {
// Single port
const port = parseInt(part, 10);
// Must be a valid number
if (isNaN(port)) {
return false;
}
// Must be valid port range (1-65535)
if (port < 1 || port > 65535) {
return false;
}
}
}
return true;
},
{
message:
'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535, and ranges must have start <= end.'
}
);
/**
* Parses a port range string into an array of port range objects
* @param portRangeStr - Port range string (e.g., "80,443,8000-9000", "*", or "")
* @param protocol - Protocol to use for all ranges (default: "tcp")
* @returns Array of port range objects with min, max, and protocol fields
*/
export function parsePortRangeString(
portRangeStr: string | undefined | null,
protocol: "tcp" | "udp" = "tcp"
): { min: number; max: number; protocol: "tcp" | "udp" }[] {
// Handle undefined or empty string - insert dummy value with port 0
if (!portRangeStr || portRangeStr.trim() === "") {
return [{ min: 0, max: 0, protocol }];
}
// Handle wildcard - return empty array (all ports allowed)
if (portRangeStr.trim() === "*") {
return [];
}
const result: { min: number; max: number; protocol: "tcp" | "udp" }[] = [];
const parts = portRangeStr.split(",").map((p) => p.trim());
for (const part of parts) {
if (part.includes("-")) {
// Range
const [start, end] = part.split("-").map((p) => p.trim());
const startPort = parseInt(start, 10);
const endPort = parseInt(end, 10);
result.push({ min: startPort, max: endPort, protocol });
} else {
// Single port
const port = parseInt(part, 10);
result.push({ min: port, max: port, protocol });
}
}
return result;
}

View File

@@ -10,7 +10,7 @@ import {
userSiteResources
} from "@server/db";
import { getUniqueSiteResourceName } from "@server/db/names";
import { getNextAvailableAliasAddress } from "@server/lib/ip";
import { getNextAvailableAliasAddress, portRangeStringSchema } from "@server/lib/ip";
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
import response from "@server/lib/response";
import logger from "@server/logger";
@@ -45,7 +45,9 @@ const createSiteResourceSchema = z
.optional(),
userIds: z.array(z.string()),
roleIds: z.array(z.int()),
clientIds: z.array(z.int())
clientIds: z.array(z.int()),
tcpPortRangeString: portRangeStringSchema,
udpPortRangeString: portRangeStringSchema
})
.strict()
.refine(
@@ -154,7 +156,9 @@ export async function createSiteResource(
alias,
userIds,
roleIds,
clientIds
clientIds,
tcpPortRangeString,
udpPortRangeString
} = parsedBody.data;
// Verify the site exists and belongs to the org
@@ -239,7 +243,9 @@ export async function createSiteResource(
destination,
enabled,
alias,
aliasAddress
aliasAddress,
tcpPortRangeString,
udpPortRangeString
})
.returning();

View File

@@ -97,6 +97,8 @@ export async function listAllSiteResourcesByOrg(
destination: siteResources.destination,
enabled: siteResources.enabled,
alias: siteResources.alias,
tcpPortRangeString: siteResources.tcpPortRangeString,
udpPortRangeString: siteResources.udpPortRangeString,
siteName: sites.name,
siteNiceId: sites.niceId,
siteAddress: sites.address

View File

@@ -23,7 +23,8 @@ import { updatePeerData, updateTargets } from "@server/routers/client/targets";
import {
generateAliasConfig,
generateRemoteSubnets,
generateSubnetProxyTargets
generateSubnetProxyTargets,
portRangeStringSchema
} from "@server/lib/ip";
import {
getClientSiteResourceAccess,
@@ -55,7 +56,9 @@ const updateSiteResourceSchema = z
.nullish(),
userIds: z.array(z.string()),
roleIds: z.array(z.int()),
clientIds: z.array(z.int())
clientIds: z.array(z.int()),
tcpPortRangeString: portRangeStringSchema,
udpPortRangeString: portRangeStringSchema
})
.strict()
.refine(
@@ -160,7 +163,9 @@ export async function updateSiteResource(
enabled,
userIds,
roleIds,
clientIds
clientIds,
tcpPortRangeString,
udpPortRangeString
} = parsedBody.data;
const [site] = await db
@@ -226,7 +231,9 @@ export async function updateSiteResource(
mode: mode,
destination: destination,
enabled: enabled,
alias: alias && alias.trim() ? alias : null
alias: alias && alias.trim() ? alias : null,
tcpPortRangeString: tcpPortRangeString,
udpPortRangeString: udpPortRangeString
})
.where(
and(