Add alias config

This commit is contained in:
Owen
2025-11-24 20:43:26 -05:00
parent d23f61d995
commit 73b0411e1c
9 changed files with 176 additions and 41 deletions

View File

@@ -11,6 +11,7 @@ import {
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { InferSelectModel } from "drizzle-orm"; import { InferSelectModel } from "drizzle-orm";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { alias } from "yargs";
export const domains = pgTable("domains", { export const domains = pgTable("domains", {
domainId: varchar("domainId").primaryKey(), domainId: varchar("domainId").primaryKey(),
@@ -40,6 +41,7 @@ export const orgs = pgTable("orgs", {
orgId: varchar("orgId").primaryKey(), orgId: varchar("orgId").primaryKey(),
name: varchar("name").notNull(), name: varchar("name").notNull(),
subnet: varchar("subnet"), subnet: varchar("subnet"),
utilitySubnet: varchar("utilitySubnet"), // this is the subnet for utility addresses
createdAt: text("createdAt"), createdAt: text("createdAt"),
requireTwoFactor: boolean("requireTwoFactor"), requireTwoFactor: boolean("requireTwoFactor"),
maxSessionLengthHours: integer("maxSessionLengthHours"), maxSessionLengthHours: integer("maxSessionLengthHours"),
@@ -209,7 +211,8 @@ export const siteResources = pgTable("siteResources", {
destinationPort: integer("destinationPort"), // only for port mode destinationPort: integer("destinationPort"), // only for port mode
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
enabled: boolean("enabled").notNull().default(true), enabled: boolean("enabled").notNull().default(true),
alias: varchar("alias") alias: varchar("alias"),
aliasAddress: varchar("aliasAddress")
}); });
export const clientSiteResources = pgTable("clientSiteResources", { export const clientSiteResources = pgTable("clientSiteResources", {

View File

@@ -32,6 +32,7 @@ export const orgs = sqliteTable("orgs", {
orgId: text("orgId").primaryKey(), orgId: text("orgId").primaryKey(),
name: text("name").notNull(), name: text("name").notNull(),
subnet: text("subnet"), subnet: text("subnet"),
utilitySubnet: text("utilitySubnet"), // this is the subnet for utility addresses
createdAt: text("createdAt"), createdAt: text("createdAt"),
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }), requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }),
maxSessionLengthHours: integer("maxSessionLengthHours"), // hours maxSessionLengthHours: integer("maxSessionLengthHours"), // hours
@@ -230,7 +231,8 @@ export const siteResources = sqliteTable("siteResources", {
destinationPort: integer("destinationPort"), // only for port mode destinationPort: integer("destinationPort"), // only for port mode
destination: text("destination").notNull(), // ip, cidr, hostname destination: text("destination").notNull(), // ip, cidr, hostname
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
alias: text("alias") alias: text("alias"),
aliasAddress: text("aliasAddress")
}); });
export const clientSiteResources = sqliteTable("clientSiteResources", { export const clientSiteResources = sqliteTable("clientSiteResources", {

View File

@@ -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 { clients, orgs, sites } from "@server/db";
import { and, eq, isNotNull } from "drizzle-orm"; import { and, eq, isNotNull } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";
@@ -281,6 +287,56 @@ export async function getNextAvailableClientSubnet(
return subnet; return subnet;
} }
export async function getNextAvailableAliasAddress(
orgId: string
): Promise<string> {
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<string> { export async function getNextAvailableOrgSubnet(): Promise<string> {
const existingAddresses = await db const existingAddresses = await db
.select({ .select({
@@ -303,7 +359,9 @@ export async function getNextAvailableOrgSubnet(): Promise<string> {
return subnet; return subnet;
} }
export function generateRemoteSubnets(allSiteResources: SiteResource[]): string[] { export function generateRemoteSubnets(
allSiteResources: SiteResource[]
): string[] {
let remoteSubnets = allSiteResources let remoteSubnets = allSiteResources
.filter((sr) => { .filter((sr) => {
if (sr.mode === "cidr") return true; if (sr.mode === "cidr") return true;
@@ -327,6 +385,18 @@ export function generateRemoteSubnets(allSiteResources: SiteResource[]): string[
return Array.from(new Set(remoteSubnets)); 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 = { export type SubnetProxyTarget = {
sourcePrefix: string; sourcePrefix: string;
destPrefix: string; destPrefix: string;
@@ -372,6 +442,14 @@ export function generateSubnetProxyTargets(
destPrefix: `${siteResource.destination}/32` 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") { } else if (siteResource.mode == "cidr") {
targets.push({ targets.push({
sourcePrefix: clientPrefix, sourcePrefix: clientPrefix,

View File

@@ -31,14 +31,15 @@ import {
import { sendToExitNode } from "#dynamic/lib/exitNodes"; import { sendToExitNode } from "#dynamic/lib/exitNodes";
import logger from "@server/logger"; import logger from "@server/logger";
import { import {
generateAliasConfig,
generateRemoteSubnets, generateRemoteSubnets,
generateSubnetProxyTargets, generateSubnetProxyTargets,
SubnetProxyTarget SubnetProxyTarget
} from "@server/lib/ip"; } from "@server/lib/ip";
import { import {
addRemoteSubnets, addPeerData,
addTargets as addSubnetProxyTargets, addTargets as addSubnetProxyTargets,
removeRemoteSubnets, removePeerData,
removeTargets as removeSubnetProxyTargets removeTargets as removeSubnetProxyTargets
} from "@server/routers/client/targets"; } from "@server/routers/client/targets";
@@ -703,10 +704,11 @@ async function handleSubnetProxyTargetUpdates(
for (const client of addedClients) { for (const client of addedClients) {
olmJobs.push( olmJobs.push(
addRemoteSubnets( addPeerData(
client.clientId, client.clientId,
siteResource.siteId, siteResource.siteId,
generateRemoteSubnets([siteResource]) generateRemoteSubnets([siteResource]),
generateAliasConfig([siteResource])
) )
); );
} }
@@ -738,10 +740,11 @@ async function handleSubnetProxyTargetUpdates(
for (const client of removedClients) { for (const client of removedClients) {
olmJobs.push( olmJobs.push(
removeRemoteSubnets( removePeerData(
client.clientId, client.clientId,
siteResource.siteId, siteResource.siteId,
generateRemoteSubnets([siteResource]) generateRemoteSubnets([siteResource]),
generateAliasConfig([siteResource])
) )
); );
} }

View File

@@ -1,6 +1,6 @@
import { sendToClient } from "#dynamic/routers/ws"; import { sendToClient } from "#dynamic/routers/ws";
import { db, olms } from "@server/db"; import { db, olms } from "@server/db";
import { SubnetProxyTarget } from "@server/lib/ip"; import { Alias, SubnetProxyTarget } from "@server/lib/ip";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) { 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, clientId: number,
siteId: number, siteId: number,
remoteSubnets: string[], remoteSubnets: string[],
aliases: Alias[],
olmId?: string olmId?: string
) { ) {
if (!olmId) { if (!olmId) {
@@ -52,18 +53,20 @@ export async function addRemoteSubnets(
} }
await sendToClient(olmId, { await sendToClient(olmId, {
type: `olm/wg/peer/add-remote-subnets`, type: `olm/wg/peer/data/add`,
data: { data: {
siteId: siteId, siteId: siteId,
remoteSubnets: remoteSubnets remoteSubnets: remoteSubnets,
aliases: aliases
} }
}); });
} }
export async function removeRemoteSubnets( export async function removePeerData(
clientId: number, clientId: number,
siteId: number, siteId: number,
remoteSubnets: string[], remoteSubnets: string[],
aliases: Alias[],
olmId?: string olmId?: string
) { ) {
if (!olmId) { if (!olmId) {
@@ -79,21 +82,26 @@ export async function removeRemoteSubnets(
} }
await sendToClient(olmId, { await sendToClient(olmId, {
type: `olm/wg/peer/remove-remote-subnets`, type: `olm/wg/peer/data/remove`,
data: { data: {
siteId: siteId, siteId: siteId,
remoteSubnets: remoteSubnets remoteSubnets: remoteSubnets,
aliases: aliases
} }
}); });
} }
export async function updateRemoteSubnets( export async function updatePeerData(
clientId: number, clientId: number,
siteId: number, siteId: number,
remoteSubnets: { remoteSubnets: {
oldRemoteSubnets: string[], oldRemoteSubnets: string[],
newRemoteSubnets: string[] newRemoteSubnets: string[]
}, },
aliases: {
oldAliases: Alias[],
newAliases: Alias[]
},
olmId?: string olmId?: string
) { ) {
if (!olmId) { if (!olmId) {
@@ -109,10 +117,11 @@ export async function updateRemoteSubnets(
} }
await sendToClient(olmId, { await sendToClient(olmId, {
type: `olm/wg/peer/update-remote-subnets`, type: `olm/wg/peer/data/update`,
data: { data: {
siteId: siteId, siteId: siteId,
...remoteSubnets ...remoteSubnets,
...aliases
} }
}); });
} }

View File

@@ -275,6 +275,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
resource, resource,
resourceClients resourceClients
); );
targetsToSend.push(...resourceTargets); targetsToSend.push(...resourceTargets);
} }

View File

@@ -3,6 +3,7 @@ import {
clientSiteResourcesAssociationsCache, clientSiteResourcesAssociationsCache,
db, db,
ExitNode, ExitNode,
Org,
orgs, orgs,
roleClients, roleClients,
roles, roles,
@@ -25,7 +26,10 @@ import { and, eq, inArray, isNull } from "drizzle-orm";
import { addPeer, deletePeer } from "../newt/peers"; import { addPeer, deletePeer } from "../newt/peers";
import logger from "@server/logger"; import logger from "@server/logger";
import { listExitNodes } from "#dynamic/lib/exitNodes"; 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"; import { generateRemoteSubnets } from "@server/lib/ip";
export const handleOlmRegisterMessage: MessageHandler = async (context) => { export const handleOlmRegisterMessage: MessageHandler = async (context) => {
@@ -42,18 +46,24 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
const { publicKey, relay, olmVersion, orgId, doNotCreateNewClient } = const { publicKey, relay, olmVersion, orgId, doNotCreateNewClient } =
message.data; message.data;
let client: Client;
let client: Client | undefined;
let org: Org | undefined;
if (orgId) { if (orgId) {
try { try {
client = await getOrCreateOrgClient( const { client: clientRes, org: orgRes } =
orgId, await getOrCreateOrgClient(
olm.userId, orgId,
olm.olmId, olm.userId,
olm.name || "User Device", olm.olmId,
// doNotCreateNewClient ? true : false olm.name || "User Device",
true // for now never create a new client automatically because we create the users clients when they are added to the org // 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) { } catch (err) {
logger.error( logger.error(
`Error switching olm client ${olm.olmId} to org ${orgId}: ${err}` `Error switching olm client ${olm.olmId} to org ${orgId}: ${err}`
@@ -96,6 +106,11 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return; return;
} }
if (!org) {
logger.warn("Org not found");
return;
}
logger.debug( logger.debug(
`Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}` `Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`
); );
@@ -302,7 +317,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
publicKey: site.publicKey, publicKey: site.publicKey,
serverIP: site.address, serverIP: site.address,
serverPort: site.listenPort, 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", type: "olm/wg/connect",
data: { data: {
sites: siteConfigurations, sites: siteConfigurations,
tunnelIP: client.subnet tunnelIP: client.subnet,
utilitySubnet: org.utilitySubnet
} }
}, },
broadcast: false, broadcast: false,
@@ -333,7 +354,10 @@ async function getOrCreateOrgClient(
name: string, name: string,
doNotCreateNewClient: boolean, doNotCreateNewClient: boolean,
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<Client> { ): Promise<{
client: Client;
org: Org;
}> {
// get the org // get the org
const [org] = await trx const [org] = await trx
.select() .select()
@@ -441,5 +465,8 @@ async function getOrCreateOrgClient(
client = newClient; client = newClient;
} }
return client; return {
client: client,
org: org
};
} }

View File

@@ -18,6 +18,7 @@ import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { getUniqueSiteResourceName } from "@server/db/names"; import { getUniqueSiteResourceName } from "@server/db/names";
import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
import { getNextAvailableAliasAddress } from "@server/lib/ip";
const createSiteResourceParamsSchema = z.strictObject({ const createSiteResourceParamsSchema = z.strictObject({
siteId: z.string().transform(Number).pipe(z.int().positive()), siteId: z.string().transform(Number).pipe(z.int().positive()),
@@ -193,6 +194,10 @@ export async function createSiteResource(
// } // }
const niceId = await getUniqueSiteResourceName(orgId); 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; let newSiteResource: SiteResource | undefined;
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
@@ -210,7 +215,8 @@ export async function createSiteResource(
// destinationPort: mode === "port" ? destinationPort : null, // destinationPort: mode === "port" ? destinationPort : null,
destination, destination,
enabled, enabled,
alias: alias || null alias,
aliasAddress
}) })
.returning(); .returning();

View File

@@ -17,11 +17,9 @@ import { eq, and, ne } from "drizzle-orm";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { updatePeerData, updateTargets } from "@server/routers/client/targets";
import { import {
updateRemoteSubnets, generateAliasConfig,
updateTargets
} from "@server/routers/client/targets";
import {
generateRemoteSubnets, generateRemoteSubnets,
generateSubnetProxyTargets generateSubnetProxyTargets
} from "@server/lib/ip"; } from "@server/lib/ip";
@@ -266,7 +264,7 @@ export async function updateSiteResource(
for (const client of mergedAllClients) { 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 // we also need to update the remote subnets on the olms for each client that has access to this site
olmJobs.push( olmJobs.push(
updateRemoteSubnets( updatePeerData(
client.clientId, client.clientId,
updatedSiteResource.siteId, updatedSiteResource.siteId,
{ {
@@ -276,6 +274,14 @@ export async function updateSiteResource(
newRemoteSubnets: generateRemoteSubnets([ newRemoteSubnets: generateRemoteSubnets([
updatedSiteResource updatedSiteResource
]) ])
},
{
oldAliases: generateAliasConfig([
existingSiteResource
]),
newAliases: generateAliasConfig([
updatedSiteResource
])
} }
) )
); );