From 73b0411e1c864244c2354cf6cb9313b27bd481ff Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 24 Nov 2025 20:43:26 -0500 Subject: [PATCH] Add alias config --- server/db/pg/schema/schema.ts | 5 +- server/db/sqlite/schema/schema.ts | 4 +- server/lib/ip.ts | 84 ++++++++++++++++++- server/lib/rebuildClientAssociations.ts | 15 ++-- server/routers/client/targets.ts | 29 ++++--- server/routers/newt/handleGetConfigMessage.ts | 1 + .../routers/olm/handleOlmRegisterMessage.ts | 55 ++++++++---- .../siteResource/createSiteResource.ts | 8 +- .../siteResource/updateSiteResource.ts | 16 ++-- 9 files changed, 176 insertions(+), 41 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 8ab1b24c..d15676a0 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -11,6 +11,7 @@ import { } from "drizzle-orm/pg-core"; import { InferSelectModel } from "drizzle-orm"; import { randomUUID } from "crypto"; +import { alias } from "yargs"; export const domains = pgTable("domains", { domainId: varchar("domainId").primaryKey(), @@ -40,6 +41,7 @@ export const orgs = pgTable("orgs", { orgId: varchar("orgId").primaryKey(), name: varchar("name").notNull(), subnet: varchar("subnet"), + utilitySubnet: varchar("utilitySubnet"), // this is the subnet for utility addresses createdAt: text("createdAt"), requireTwoFactor: boolean("requireTwoFactor"), maxSessionLengthHours: integer("maxSessionLengthHours"), @@ -209,7 +211,8 @@ export const siteResources = pgTable("siteResources", { destinationPort: integer("destinationPort"), // only for port mode destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode enabled: boolean("enabled").notNull().default(true), - alias: varchar("alias") + alias: varchar("alias"), + aliasAddress: varchar("aliasAddress") }); export const clientSiteResources = pgTable("clientSiteResources", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index cfffdba7..634afd36 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -32,6 +32,7 @@ export const orgs = sqliteTable("orgs", { orgId: text("orgId").primaryKey(), name: text("name").notNull(), subnet: text("subnet"), + utilitySubnet: text("utilitySubnet"), // this is the subnet for utility addresses createdAt: text("createdAt"), requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }), maxSessionLengthHours: integer("maxSessionLengthHours"), // hours @@ -230,7 +231,8 @@ export const siteResources = sqliteTable("siteResources", { destinationPort: integer("destinationPort"), // only for port mode destination: text("destination").notNull(), // ip, cidr, hostname enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), - alias: text("alias") + alias: text("alias"), + aliasAddress: text("aliasAddress") }); export const clientSiteResources = sqliteTable("clientSiteResources", { diff --git a/server/lib/ip.ts b/server/lib/ip.ts index d530e2f0..7835ad84 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -1,4 +1,10 @@ -import { clientSitesAssociationsCache, db, SiteResource, Transaction } from "@server/db"; +import { + clientSitesAssociationsCache, + 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"; @@ -281,6 +287,56 @@ export async function getNextAvailableClientSubnet( return subnet; } +export async function getNextAvailableAliasAddress( + orgId: string +): Promise { + const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId)); + + if (!org) { + throw new Error(`Organization with ID ${orgId} not found`); + } + + if (!org.subnet) { + throw new Error(`Organization with ID ${orgId} has no subnet defined`); + } + + if (!org.utilitySubnet) { + throw new Error( + `Organization with ID ${orgId} has no utility subnet defined` + ); + } + + const existingAddresses = await db + .select({ + aliasAddress: siteResources.aliasAddress + }) + .from(siteResources) + .where( + and( + isNotNull(siteResources.aliasAddress), + eq(siteResources.orgId, orgId) + ) + ); + + const addresses = [ + ...existingAddresses.map( + (site) => `${site.aliasAddress?.split("/")[0]}/32` + ), + // reserve a /29 for the dns server and other stuff + `${org.utilitySubnet.split("/")[0]}/29` + ].filter((address) => address !== null) as string[]; + + let subnet = findNextAvailableCidr(addresses, 32, org.utilitySubnet); + if (!subnet) { + throw new Error("No available subnets remaining in space"); + } + + // remove the cidr + subnet = subnet.split("/")[0]; + + return subnet; +} + export async function getNextAvailableOrgSubnet(): Promise { const existingAddresses = await db .select({ @@ -303,7 +359,9 @@ export async function getNextAvailableOrgSubnet(): Promise { return subnet; } -export function generateRemoteSubnets(allSiteResources: SiteResource[]): string[] { +export function generateRemoteSubnets( + allSiteResources: SiteResource[] +): string[] { let remoteSubnets = allSiteResources .filter((sr) => { if (sr.mode === "cidr") return true; @@ -327,6 +385,18 @@ export function generateRemoteSubnets(allSiteResources: SiteResource[]): string[ return Array.from(new Set(remoteSubnets)); } +export type Alias = { alias: string | null; aliasAddress: string | null }; + +export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] { + let aliasConfigs = allSiteResources + .filter((sr) => sr.alias && sr.aliasAddress && sr.mode == "host") + .map((sr) => ({ + alias: sr.alias, + aliasAddress: sr.aliasAddress + })); + return aliasConfigs; +} + export type SubnetProxyTarget = { sourcePrefix: string; destPrefix: string; @@ -372,6 +442,14 @@ export function generateSubnetProxyTargets( destPrefix: `${siteResource.destination}/32` }); } + + if (siteResource.alias && siteResource.aliasAddress) { + // also push a match for the alias address + targets.push({ + sourcePrefix: clientPrefix, + destPrefix: `${siteResource.aliasAddress}/32` + }); + } } else if (siteResource.mode == "cidr") { targets.push({ sourcePrefix: clientPrefix, @@ -386,4 +464,4 @@ export function generateSubnetProxyTargets( ); return targets; -} \ No newline at end of file +} diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts index 2773e098..a1072196 100644 --- a/server/lib/rebuildClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -31,14 +31,15 @@ import { import { sendToExitNode } from "#dynamic/lib/exitNodes"; import logger from "@server/logger"; import { + generateAliasConfig, generateRemoteSubnets, generateSubnetProxyTargets, SubnetProxyTarget } from "@server/lib/ip"; import { - addRemoteSubnets, + addPeerData, addTargets as addSubnetProxyTargets, - removeRemoteSubnets, + removePeerData, removeTargets as removeSubnetProxyTargets } from "@server/routers/client/targets"; @@ -703,10 +704,11 @@ async function handleSubnetProxyTargetUpdates( for (const client of addedClients) { olmJobs.push( - addRemoteSubnets( + addPeerData( client.clientId, siteResource.siteId, - generateRemoteSubnets([siteResource]) + generateRemoteSubnets([siteResource]), + generateAliasConfig([siteResource]) ) ); } @@ -738,10 +740,11 @@ async function handleSubnetProxyTargetUpdates( for (const client of removedClients) { olmJobs.push( - removeRemoteSubnets( + removePeerData( client.clientId, siteResource.siteId, - generateRemoteSubnets([siteResource]) + generateRemoteSubnets([siteResource]), + generateAliasConfig([siteResource]) ) ); } diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts index c94cb680..b5684436 100644 --- a/server/routers/client/targets.ts +++ b/server/routers/client/targets.ts @@ -1,6 +1,6 @@ import { sendToClient } from "#dynamic/routers/ws"; import { db, olms } from "@server/db"; -import { SubnetProxyTarget } from "@server/lib/ip"; +import { Alias, SubnetProxyTarget } from "@server/lib/ip"; import { eq } from "drizzle-orm"; export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) { @@ -33,10 +33,11 @@ export async function updateTargets( }); } -export async function addRemoteSubnets( +export async function addPeerData( clientId: number, siteId: number, remoteSubnets: string[], + aliases: Alias[], olmId?: string ) { if (!olmId) { @@ -52,18 +53,20 @@ export async function addRemoteSubnets( } await sendToClient(olmId, { - type: `olm/wg/peer/add-remote-subnets`, + type: `olm/wg/peer/data/add`, data: { siteId: siteId, - remoteSubnets: remoteSubnets + remoteSubnets: remoteSubnets, + aliases: aliases } }); } -export async function removeRemoteSubnets( +export async function removePeerData( clientId: number, siteId: number, remoteSubnets: string[], + aliases: Alias[], olmId?: string ) { if (!olmId) { @@ -79,21 +82,26 @@ export async function removeRemoteSubnets( } await sendToClient(olmId, { - type: `olm/wg/peer/remove-remote-subnets`, + type: `olm/wg/peer/data/remove`, data: { siteId: siteId, - remoteSubnets: remoteSubnets + remoteSubnets: remoteSubnets, + aliases: aliases } }); } -export async function updateRemoteSubnets( +export async function updatePeerData( clientId: number, siteId: number, remoteSubnets: { oldRemoteSubnets: string[], newRemoteSubnets: string[] }, + aliases: { + oldAliases: Alias[], + newAliases: Alias[] + }, olmId?: string ) { if (!olmId) { @@ -109,10 +117,11 @@ export async function updateRemoteSubnets( } await sendToClient(olmId, { - type: `olm/wg/peer/update-remote-subnets`, + type: `olm/wg/peer/data/update`, data: { siteId: siteId, - ...remoteSubnets + ...remoteSubnets, + ...aliases } }); } diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 68116686..fbbcb4fb 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -275,6 +275,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { resource, resourceClients ); + targetsToSend.push(...resourceTargets); } diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 5c438e4f..048e6baa 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -3,6 +3,7 @@ import { clientSiteResourcesAssociationsCache, db, ExitNode, + Org, orgs, roleClients, roles, @@ -25,7 +26,10 @@ import { and, eq, inArray, isNull } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; import logger from "@server/logger"; import { listExitNodes } from "#dynamic/lib/exitNodes"; -import { getNextAvailableClientSubnet } from "@server/lib/ip"; +import { + generateAliasConfig, + getNextAvailableClientSubnet +} from "@server/lib/ip"; import { generateRemoteSubnets } from "@server/lib/ip"; export const handleOlmRegisterMessage: MessageHandler = async (context) => { @@ -42,18 +46,24 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { const { publicKey, relay, olmVersion, orgId, doNotCreateNewClient } = message.data; - let client: Client; + + let client: Client | undefined; + let org: Org | undefined; if (orgId) { try { - client = await getOrCreateOrgClient( - orgId, - olm.userId, - olm.olmId, - olm.name || "User Device", - // doNotCreateNewClient ? true : false - true // for now never create a new client automatically because we create the users clients when they are added to the org - ); + const { client: clientRes, org: orgRes } = + await getOrCreateOrgClient( + orgId, + olm.userId, + olm.olmId, + olm.name || "User Device", + // doNotCreateNewClient ? true : false + true // for now never create a new client automatically because we create the users clients when they are added to the org + ); + + client = clientRes; + org = orgRes; } catch (err) { logger.error( `Error switching olm client ${olm.olmId} to org ${orgId}: ${err}` @@ -96,6 +106,11 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } + if (!org) { + logger.warn("Org not found"); + return; + } + logger.debug( `Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}` ); @@ -302,7 +317,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { publicKey: site.publicKey, serverIP: site.address, serverPort: site.listenPort, - remoteSubnets: generateRemoteSubnets(allSiteResources.map(({ siteResources }) => siteResources)) + remoteSubnets: generateRemoteSubnets( + allSiteResources.map(({ siteResources }) => siteResources) + ), + aliases: generateAliasConfig( + allSiteResources.map(({ siteResources }) => siteResources) + ) }); } @@ -318,7 +338,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { type: "olm/wg/connect", data: { sites: siteConfigurations, - tunnelIP: client.subnet + tunnelIP: client.subnet, + utilitySubnet: org.utilitySubnet } }, broadcast: false, @@ -333,7 +354,10 @@ async function getOrCreateOrgClient( name: string, doNotCreateNewClient: boolean, trx: Transaction | typeof db = db -): Promise { +): Promise<{ + client: Client; + org: Org; +}> { // get the org const [org] = await trx .select() @@ -441,5 +465,8 @@ async function getOrCreateOrgClient( client = newClient; } - return client; + return { + client: client, + org: org + }; } diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index 2c7bf0fe..ecbb7768 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -18,6 +18,7 @@ import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; import { getUniqueSiteResourceName } from "@server/db/names"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; +import { getNextAvailableAliasAddress } from "@server/lib/ip"; const createSiteResourceParamsSchema = z.strictObject({ siteId: z.string().transform(Number).pipe(z.int().positive()), @@ -193,6 +194,10 @@ export async function createSiteResource( // } const niceId = await getUniqueSiteResourceName(orgId); + let aliasAddress: string | null = null; + if (mode == "host") { // we can only have an alias on a host + aliasAddress = await getNextAvailableAliasAddress(orgId); + } let newSiteResource: SiteResource | undefined; await db.transaction(async (trx) => { @@ -210,7 +215,8 @@ export async function createSiteResource( // destinationPort: mode === "port" ? destinationPort : null, destination, enabled, - alias: alias || null + alias, + aliasAddress }) .returning(); diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 2e2c1592..d66d2cb8 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -17,11 +17,9 @@ import { eq, and, ne } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; +import { updatePeerData, updateTargets } from "@server/routers/client/targets"; import { - updateRemoteSubnets, - updateTargets -} from "@server/routers/client/targets"; -import { + generateAliasConfig, generateRemoteSubnets, generateSubnetProxyTargets } from "@server/lib/ip"; @@ -266,7 +264,7 @@ export async function updateSiteResource( for (const client of mergedAllClients) { // we also need to update the remote subnets on the olms for each client that has access to this site olmJobs.push( - updateRemoteSubnets( + updatePeerData( client.clientId, updatedSiteResource.siteId, { @@ -276,6 +274,14 @@ export async function updateSiteResource( newRemoteSubnets: generateRemoteSubnets([ updatedSiteResource ]) + }, + { + oldAliases: generateAliasConfig([ + existingSiteResource + ]), + newAliases: generateAliasConfig([ + updatedSiteResource + ]) } ) );