Merge branch 'main' into dev

This commit is contained in:
Owen
2025-12-09 16:26:21 -05:00
5 changed files with 99 additions and 18 deletions

8
package-lock.json generated
View File

@@ -128,6 +128,7 @@
"@types/express": "5.0.6", "@types/express": "5.0.6",
"@types/express-session": "1.18.2", "@types/express-session": "1.18.2",
"@types/jmespath": "0.15.2", "@types/jmespath": "0.15.2",
"@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "9.0.10", "@types/jsonwebtoken": "9.0.10",
"@types/node": "24.10.2", "@types/node": "24.10.2",
"@types/nodemailer": "7.0.4", "@types/nodemailer": "7.0.4",
@@ -9292,6 +9293,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",

View File

@@ -34,9 +34,9 @@
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "8.2.0", "@asteasolutions/zod-to-openapi": "8.2.0",
"@aws-sdk/client-s3": "3.947.0",
"@faker-js/faker": "10.1.0", "@faker-js/faker": "10.1.0",
"@headlessui/react": "2.2.9", "@headlessui/react": "2.2.9",
"@aws-sdk/client-s3": "3.947.0",
"@hookform/resolvers": "5.2.2", "@hookform/resolvers": "5.2.2",
"@monaco-editor/react": "4.7.0", "@monaco-editor/react": "4.7.0",
"@node-rs/argon2": "2.0.2", "@node-rs/argon2": "2.0.2",
@@ -125,9 +125,8 @@
"semver": "7.7.3", "semver": "7.7.3",
"stripe": "20.0.0", "stripe": "20.0.0",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"topojson-client": "3.1.0",
"tailwind-merge": "3.4.0", "tailwind-merge": "3.4.0",
"topojson-client": "3.1.0",
"tw-animate-css": "1.4.0", "tw-animate-css": "1.4.0",
"uuid": "13.0.0", "uuid": "13.0.0",
"vaul": "1.1.2", "vaul": "1.1.2",
@@ -155,8 +154,8 @@
"@types/jmespath": "0.15.2", "@types/jmespath": "0.15.2",
"@types/jsonwebtoken": "9.0.10", "@types/jsonwebtoken": "9.0.10",
"@types/node": "24.10.2", "@types/node": "24.10.2",
"@types/nprogress": "0.2.3",
"@types/nodemailer": "7.0.4", "@types/nodemailer": "7.0.4",
"@types/nprogress": "0.2.3",
"@types/pg": "8.15.6", "@types/pg": "8.15.6",
"@types/react": "19.2.7", "@types/react": "19.2.7",
"@types/react-dom": "19.2.3", "@types/react-dom": "19.2.3",
@@ -164,18 +163,19 @@
"@types/swagger-ui-express": "4.1.8", "@types/swagger-ui-express": "4.1.8",
"@types/topojson-client": "3.1.5", "@types/topojson-client": "3.1.5",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"babel-plugin-react-compiler": "1.0.0",
"@types/yargs": "17.0.35", "@types/yargs": "17.0.35",
"@types/js-yaml": "4.0.9",
"babel-plugin-react-compiler": "1.0.0",
"drizzle-kit": "0.31.8", "drizzle-kit": "0.31.8",
"esbuild": "0.27.1", "esbuild": "0.27.1",
"esbuild-node-externals": "1.20.1", "esbuild-node-externals": "1.20.1",
"postcss": "8.5.6", "postcss": "8.5.6",
"prettier": "3.7.4",
"react-email": "5.0.6", "react-email": "5.0.6",
"tailwindcss": "4.1.17", "tailwindcss": "4.1.17",
"prettier": "3.7.4",
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
"tsx": "4.21.0", "tsx": "4.21.0",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.49.0" "typescript-eslint": "8.49.0"
} }
} }

View File

@@ -116,6 +116,68 @@ function bigIntToIp(num: bigint, version: IPVersion): string {
} }
} }
/**
* Parses an endpoint string (ip:port) handling both IPv4 and IPv6 addresses.
* IPv6 addresses may be bracketed like [::1]:8080 or unbracketed like ::1:8080.
* For unbracketed IPv6, the last colon-separated segment is treated as the port.
*
* @param endpoint The endpoint string to parse (e.g., "192.168.1.1:8080" or "[::1]:8080" or "2607:fea8::1:8080")
* @returns An object with ip and port, or null if parsing fails
*/
export function parseEndpoint(endpoint: string): { ip: string; port: number } | null {
if (!endpoint) return null;
// Check for bracketed IPv6 format: [ip]:port
const bracketedMatch = endpoint.match(/^\[([^\]]+)\]:(\d+)$/);
if (bracketedMatch) {
const ip = bracketedMatch[1];
const port = parseInt(bracketedMatch[2], 10);
if (isNaN(port)) return null;
return { ip, port };
}
// Check if this looks like IPv6 (contains multiple colons)
const colonCount = (endpoint.match(/:/g) || []).length;
if (colonCount > 1) {
// This is IPv6 - the port is after the last colon
const lastColonIndex = endpoint.lastIndexOf(":");
const ip = endpoint.substring(0, lastColonIndex);
const portStr = endpoint.substring(lastColonIndex + 1);
const port = parseInt(portStr, 10);
if (isNaN(port)) return null;
return { ip, port };
}
// IPv4 format: ip:port
if (colonCount === 1) {
const [ip, portStr] = endpoint.split(":");
const port = parseInt(portStr, 10);
if (isNaN(port)) return null;
return { ip, port };
}
return null;
}
/**
* Formats an IP and port into a consistent endpoint string.
* IPv6 addresses are wrapped in brackets for proper parsing.
*
* @param ip The IP address (IPv4 or IPv6)
* @param port The port number
* @returns Formatted endpoint string
*/
export function formatEndpoint(ip: string, port: number): string {
// Check if this is IPv6 (contains colons)
if (ip.includes(":")) {
// Remove brackets if already present
const cleanIp = ip.replace(/^\[|\]$/g, "");
return `[${cleanIp}]:${port}`;
}
return `${ip}:${port}`;
}
/** /**
* Converts CIDR to IP range * Converts CIDR to IP range
*/ */

View File

@@ -32,7 +32,9 @@ import logger from "@server/logger";
import { import {
generateAliasConfig, generateAliasConfig,
generateRemoteSubnets, generateRemoteSubnets,
generateSubnetProxyTargets generateSubnetProxyTargets,
parseEndpoint,
formatEndpoint
} from "@server/lib/ip"; } from "@server/lib/ip";
import { import {
addPeerData, addPeerData,
@@ -542,6 +544,13 @@ export async function updateClientSiteDestinations(
continue; continue;
} }
// Parse the endpoint properly for both IPv4 and IPv6
const parsedEndpoint = parseEndpoint(site.clientSitesAssociationsCache.endpoint);
if (!parsedEndpoint) {
logger.warn(`Failed to parse endpoint ${site.clientSitesAssociationsCache.endpoint}, skipping`);
continue;
}
// find the destinations in the array // find the destinations in the array
let destinations = exitNodeDestinations.find( let destinations = exitNodeDestinations.find(
(d) => d.reachableAt === site.exitNodes?.reachableAt (d) => d.reachableAt === site.exitNodes?.reachableAt
@@ -553,13 +562,8 @@ export async function updateClientSiteDestinations(
exitNodeId: site.exitNodes?.exitNodeId || 0, exitNodeId: site.exitNodes?.exitNodeId || 0,
type: site.exitNodes?.type || "", type: site.exitNodes?.type || "",
name: site.exitNodes?.name || "", name: site.exitNodes?.name || "",
sourceIp: sourceIp: parsedEndpoint.ip,
site.clientSitesAssociationsCache.endpoint.split(":")[0] || sourcePort: parsedEndpoint.port,
"",
sourcePort:
parseInt(
site.clientSitesAssociationsCache.endpoint.split(":")[1]
) || 0,
destinations: [ destinations: [
{ {
destinationIP: site.sites.subnet.split("/")[0], destinationIP: site.sites.subnet.split("/")[0],

View File

@@ -21,6 +21,7 @@ import { validateOlmSessionToken } from "@server/auth/sessions/olm";
import { checkExitNodeOrg } from "#dynamic/lib/exitNodes"; import { checkExitNodeOrg } from "#dynamic/lib/exitNodes";
import { updatePeer as updateOlmPeer } from "../olm/peers"; import { updatePeer as updateOlmPeer } from "../olm/peers";
import { updatePeer as updateNewtPeer } from "../newt/peers"; import { updatePeer as updateNewtPeer } from "../newt/peers";
import { formatEndpoint } from "@server/lib/ip";
// Define Zod schema for request validation // Define Zod schema for request validation
const updateHolePunchSchema = z.object({ const updateHolePunchSchema = z.object({
@@ -207,9 +208,12 @@ export async function updateAndGenerateEndpointDestinations(
// `Updating site ${site.siteId} on exit node ${exitNode.exitNodeId}` // `Updating site ${site.siteId} on exit node ${exitNode.exitNodeId}`
// ); // );
// Format the endpoint properly for both IPv4 and IPv6
const formattedEndpoint = formatEndpoint(ip, port);
// if the public key or endpoint has changed, update it otherwise continue // if the public key or endpoint has changed, update it otherwise continue
if ( if (
site.endpoint === `${ip}:${port}` && site.endpoint === formattedEndpoint &&
site.publicKey === publicKey site.publicKey === publicKey
) { ) {
continue; continue;
@@ -218,7 +222,7 @@ export async function updateAndGenerateEndpointDestinations(
const [updatedClientSitesAssociationsCache] = await db const [updatedClientSitesAssociationsCache] = await db
.update(clientSitesAssociationsCache) .update(clientSitesAssociationsCache)
.set({ .set({
endpoint: `${ip}:${port}`, endpoint: formattedEndpoint,
publicKey: publicKey publicKey: publicKey
}) })
.where( .where(
@@ -310,11 +314,14 @@ export async function updateAndGenerateEndpointDestinations(
currentSiteId = newt.siteId; currentSiteId = newt.siteId;
// Format the endpoint properly for both IPv4 and IPv6
const formattedSiteEndpoint = formatEndpoint(ip, port);
// Update the current site with the new endpoint // Update the current site with the new endpoint
const [updatedSite] = await db const [updatedSite] = await db
.update(sites) .update(sites)
.set({ .set({
endpoint: `${ip}:${port}`, endpoint: formattedSiteEndpoint,
lastHolePunch: timestamp lastHolePunch: timestamp
}) })
.where(eq(sites.siteId, newt.siteId)) .where(eq(sites.siteId, newt.siteId))