Properly lock the ip selection through writes to db

This commit is contained in:
Owen
2026-05-27 21:06:34 -07:00
parent cd9e56fdb7
commit 64c901d91f
8 changed files with 598 additions and 551 deletions
+9 -1
View File
@@ -364,8 +364,14 @@ export async function updateClientResources(
}); });
} else { } else {
let aliasAddress: string | null = null; let aliasAddress: string | null = null;
let releaseAliasLock: (() => Promise<void>) | null = null;
if (resourceData.mode === "host" || resourceData.mode === "http") { if (resourceData.mode === "host" || resourceData.mode === "http") {
aliasAddress = await getNextAvailableAliasAddress(orgId, trx); const { value, release } = await getNextAvailableAliasAddress(
orgId,
trx
);
aliasAddress = value;
releaseAliasLock = release;
} }
let domainInfo: let domainInfo:
@@ -427,6 +433,8 @@ export async function updateClientResources(
}) })
.returning(); .returning();
await releaseAliasLock?.();
const siteResourceId = newResource.siteResourceId; const siteResourceId = newResource.siteResourceId;
for (const site of allSites) { for (const site of allSites) {
+3 -10
View File
@@ -331,16 +331,8 @@ export async function calculateUserClientsForOrgs(
]; ];
// Get next available subnet // Get next available subnet
const newSubnet = await getNextAvailableClientSubnet( const { value: newSubnet, release: releaseSubnetLock } =
orgId, await getNextAvailableClientSubnet(orgId, transaction);
transaction
);
if (!newSubnet) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no available subnet found`
);
continue;
}
const subnet = newSubnet.split("/")[0]; const subnet = newSubnet.split("/")[0];
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`; const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`;
@@ -370,6 +362,7 @@ export async function calculateUserClientsForOrgs(
.insert(clients) .insert(clients)
.values(newClientData) .values(newClientData)
.returning(); .returning();
await releaseSubnetLock();
existingClientCache.set( existingClientCache.set(
getOrgOlmKey(orgId, olm.olmId), getOrgOlmKey(orgId, olm.olmId),
newClient newClient
+131 -110
View File
@@ -327,127 +327,145 @@ export function doCidrsOverlap(cidr1: string, cidr2: string): boolean {
export async function getNextAvailableClientSubnet( export async function getNextAvailableClientSubnet(
orgId: string, orgId: string,
transaction: Transaction | typeof db = db transaction: Transaction | typeof db = db
): Promise<string> { ): Promise<{ value: string; release: () => Promise<void> }> {
return await lockManager.withLock( const lockKey = `client-subnet-allocation:${orgId}`;
`client-subnet-allocation:${orgId}`, const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
async () => { if (!acquired) {
const [org] = await transaction throw new Error(`Failed to acquire lock: ${lockKey}`);
.select() }
.from(orgs) const release = () => lockManager.releaseLock(lockKey);
.where(eq(orgs.orgId, orgId));
if (!org) { try {
throw new Error(`Organization with ID ${orgId} not found`); const [org] = await transaction
} .select()
.from(orgs)
.where(eq(orgs.orgId, orgId));
if (!org.subnet) { if (!org) {
throw new Error( throw new Error(`Organization with ID ${orgId} not found`);
`Organization with ID ${orgId} has no subnet defined`
);
}
const existingAddressesSites = await transaction
.select({
address: sites.address
})
.from(sites)
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
const existingAddressesClients = await transaction
.select({
address: clients.subnet
})
.from(clients)
.where(
and(isNotNull(clients.subnet), eq(clients.orgId, orgId))
);
const addresses = [
...existingAddressesSites.map(
(site) => `${site.address?.split("/")[0]}/32`
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
...existingAddressesClients.map(
(client) => `${client.address.split("/")}/32`
)
].filter((address) => address !== null) as string[];
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
if (!subnet) {
throw new Error("No available subnets remaining in space");
}
return subnet;
} }
);
if (!org.subnet) {
throw new Error(
`Organization with ID ${orgId} has no subnet defined`
);
}
const existingAddressesSites = await transaction
.select({
address: sites.address
})
.from(sites)
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
const existingAddressesClients = await transaction
.select({
address: clients.subnet
})
.from(clients)
.where(and(isNotNull(clients.subnet), eq(clients.orgId, orgId)));
const addresses = [
...existingAddressesSites.map(
(site) => `${site.address?.split("/")[0]}/32`
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
...existingAddressesClients.map(
(client) => `${client.address.split("/")[0]}/32`
)
].filter((address) => address !== null) as string[];
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
if (!subnet) {
throw new Error("No available subnets remaining in space");
}
return { value: subnet, release };
} catch (e) {
await release();
throw e;
}
} }
export async function getNextAvailableAliasAddress( export async function getNextAvailableAliasAddress(
orgId: string, orgId: string,
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<string> { ): Promise<{ value: string; release: () => Promise<void> }> {
return await lockManager.withLock( const lockKey = `alias-address-allocation:${orgId}`;
`alias-address-allocation:${orgId}`, const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
async () => { if (!acquired) {
const [org] = await trx throw new Error(`Failed to acquire lock: ${lockKey}`);
.select() }
.from(orgs) const release = () => lockManager.releaseLock(lockKey);
.where(eq(orgs.orgId, orgId));
if (!org) { try {
throw new Error(`Organization with ID ${orgId} not found`); const [org] = await trx
} .select()
.from(orgs)
.where(eq(orgs.orgId, orgId));
if (!org.subnet) { if (!org) {
throw new Error( throw new Error(`Organization with ID ${orgId} not found`);
`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 trx
.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;
} }
);
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 trx
.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 { value: subnet, release };
} catch (e) {
await release();
throw e;
}
} }
export async function getNextAvailableOrgSubnet(): Promise<string> { export async function getNextAvailableOrgSubnet(): Promise<{
return await lockManager.withLock("org-subnet-allocation", async () => { value: string;
release: () => Promise<void>;
}> {
const lockKey = "org-subnet-allocation";
const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
if (!acquired) {
throw new Error(`Failed to acquire lock: ${lockKey}`);
}
const release = () => lockManager.releaseLock(lockKey);
try {
const existingAddresses = await db const existingAddresses = await db
.select({ .select({
subnet: orgs.subnet subnet: orgs.subnet
@@ -466,8 +484,11 @@ export async function getNextAvailableOrgSubnet(): Promise<string> {
throw new Error("No available subnets remaining in space"); throw new Error("No available subnets remaining in space");
} }
return subnet; return { value: subnet, release };
}); } catch (e) {
await release();
throw e;
}
} }
export function generateRemoteSubnets( export function generateRemoteSubnets(
+3 -1
View File
@@ -51,7 +51,9 @@ export async function pickClientDefaults(
const olmId = generateId(15); const olmId = generateId(15);
const secret = generateId(48); const secret = generateId(48);
const newSubnet = await getNextAvailableClientSubnet(orgId); const { value: newSubnet, release } =
await getNextAvailableClientSubnet(orgId);
await release(); // release immediately — this endpoint only previews the next available value
if (!newSubnet) { if (!newSubnet) {
return next( return next(
createHttpError( createHttpError(
+72 -74
View File
@@ -203,84 +203,82 @@ export async function registerNewt(
let newSiteId: number | undefined; let newSiteId: number | undefined;
await db.transaction(async (trx) => { const { value: newClientAddress, release: releaseSubnetLock } =
const newClientAddress = await getNextAvailableClientSubnet(orgId); await getNextAvailableClientSubnet(orgId);
if (!newClientAddress) { try {
return next( await db.transaction(async (trx) => {
createHttpError( let clientAddress = newClientAddress.split("/")[0];
HttpCode.INTERNAL_SERVER_ERROR, clientAddress = `${clientAddress}/${org.subnet!.split("/")[1]}`; // we want the block size of the whole org
"No available subnet found"
)
);
}
let clientAddress = newClientAddress.split("/")[0]; // Create the site (type "newt", name = niceId)
clientAddress = `${clientAddress}/${org.subnet!.split("/")[1]}`; // we want the block size of the whole org const [newSite] = await trx
.insert(sites)
.values({
orgId,
name: name || niceId,
niceId,
address: clientAddress,
type: "newt",
dockerSocketEnabled: true,
status: keyRecord.approveNewSites
? "approved"
: "pending"
})
.returning();
// Create the site (type "newt", name = niceId) await logsDb.insert(statusHistory).values({
const [newSite] = await trx entityType: "site",
.insert(sites) entityId: newSite.siteId,
.values({ orgId: orgId,
orgId, status: "offline",
name: name || niceId, timestamp: Math.floor(Date.now() / 1000)
niceId, });
address: clientAddress,
type: "newt",
dockerSocketEnabled: true,
status: keyRecord.approveNewSites ? "approved" : "pending"
})
.returning();
await logsDb.insert(statusHistory).values({ newSiteId = newSite.siteId;
entityType: "site",
entityId: newSite.siteId, // Grant admin role access to the new site
orgId: orgId, const [adminRole] = await trx
status: "offline", .select()
timestamp: Math.floor(Date.now() / 1000) .from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
if (!adminRole) {
throw new Error(`Admin role not found for org ${orgId}`);
}
await trx.insert(roleSites).values({
roleId: adminRole.roleId,
siteId: newSite.siteId
});
// Create the newt for this site
await trx.insert(newts).values({
newtId,
secretHash,
siteId: newSite.siteId,
dateCreated: moment().toISOString()
});
// Consume the provisioning key - cascade removes siteProvisioningKeyOrg
await trx
.update(siteProvisioningKeys)
.set({
lastUsed: moment().toISOString(),
numUsed: sql`${siteProvisioningKeys.numUsed} + 1`
})
.where(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
provisioningKeyId
)
);
await usageService.add(orgId, FeatureId.SITES, 1, trx);
}); });
} finally {
newSiteId = newSite.siteId; await releaseSubnetLock();
}
// Grant admin role access to the new site
const [adminRole] = await trx
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
if (!adminRole) {
throw new Error(`Admin role not found for org ${orgId}`);
}
await trx.insert(roleSites).values({
roleId: adminRole.roleId,
siteId: newSite.siteId
});
// Create the newt for this site
await trx.insert(newts).values({
newtId,
secretHash,
siteId: newSite.siteId,
dateCreated: moment().toISOString()
});
// Consume the provisioning key - cascade removes siteProvisioningKeyOrg
await trx
.update(siteProvisioningKeys)
.set({
lastUsed: moment().toISOString(),
numUsed: sql`${siteProvisioningKeys.numUsed} + 1`
})
.where(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
provisioningKeyId
)
);
await usageService.add(orgId, FeatureId.SITES, 1, trx);
});
logger.info( logger.info(
`Provisioned new site (ID: ${newSiteId}) and newt (ID: ${newtId}) for org ${orgId} via provisioning key ${provisioningKeyId}` `Provisioned new site (ID: ${newSiteId}) and newt (ID: ${newtId}) for org ${orgId} via provisioning key ${provisioningKeyId}`
+230 -226
View File
@@ -174,6 +174,7 @@ export async function createSite(
} }
let updatedAddress = null; let updatedAddress = null;
let releaseSubnetLock: (() => Promise<void>) | null = null;
if (address) { if (address) {
if (!org.subnet) { if (!org.subnet) {
return next( return next(
@@ -244,147 +245,22 @@ export async function createSite(
); );
} }
} else { } else {
const newClientAddress = await getNextAvailableClientSubnet(orgId); const { value: newClientAddress, release } =
if (!newClientAddress) { await getNextAvailableClientSubnet(orgId);
return next( releaseSubnetLock = release;
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"No available address found"
)
);
}
updatedAddress = newClientAddress.split("/")[0]; updatedAddress = newClientAddress.split("/")[0];
} }
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.`
)
);
}
}
let updatedNiceId = niceId;
if (!niceId) {
updatedNiceId = await getUniqueSiteName(orgId);
} else {
// make sure the niceId is unique
const existingSite = await db
.select()
.from(sites)
.where(and(eq(sites.niceId, niceId), eq(sites.orgId, orgId)))
.limit(1);
if (existingSite.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
`Nice ID ${niceId} already exists. Please choose a different one.`
)
);
}
}
let newSite: Site | undefined; let newSite: Site | undefined;
await db.transaction(async (trx) => { try {
if (type == "newt") { if (subnet && exitNodeId) {
[newSite] = await trx //make sure the subnet is in the range of the exit node if provided
.insert(sites) const [exitNode] = await db
.values({ .select()
// NOTE: NO SUBNET OR EXIT NODE ID PASSED IN HERE BECAUSE ITS NOW CHOSEN ON CONNECT .from(exitNodes)
orgId, .where(eq(exitNodes.exitNodeId, exitNodeId));
name,
niceId: updatedNiceId!,
address: updatedAddress || null,
type,
dockerSocketEnabled: true,
status: "approved"
})
.returning();
await logsDb.insert(statusHistory).values({
entityType: "site",
entityId: newSite.siteId,
orgId: orgId,
status: "offline",
timestamp: Math.floor(Date.now() / 1000)
});
} else if (type == "wireguard") {
// we are creating a site with an exit node (tunneled)
if (!subnet) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Subnet is required for tunneled sites"
)
);
}
if (!exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Exit node ID is required for tunneled sites"
)
);
}
const { exitNode, hasAccess } = await verifyExitNodeOrgAccess(
exitNodeId,
orgId
);
if (!exitNode) { if (!exitNode) {
logger.warn("Exit node not found");
return next( return next(
createHttpError( createHttpError(
HttpCode.NOT_FOUND, HttpCode.NOT_FOUND,
@@ -393,118 +269,246 @@ export async function createSite(
); );
} }
if (!hasAccess) { if (!exitNode.address) {
logger.warn("Not authorized to use this exit node");
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.BAD_REQUEST,
"Not authorized to use this exit node" "Exit node has no subnet defined"
) )
); );
} }
[newSite] = await trx const subnetIp = subnet.split("/")[0];
.insert(sites)
.values({ if (!isIpInCidr(subnetIp, exitNode.address)) {
orgId, return next(
exitNodeId, createHttpError(
name, HttpCode.BAD_REQUEST,
niceId: updatedNiceId!, "Subnet is not in the CIDR range of the exit node address."
subnet, )
type, );
pubKey: pubKey || null, }
status: "approved"
// lets also make sure there is no overlap with other sites on the exit node
const sitesQuery = await db
.select({
subnet: sites.subnet
}) })
.returning(); .from(sites)
} else if (type == "local") { .where(
[newSite] = await trx and(
.insert(sites) eq(sites.exitNodeId, exitNodeId),
.values({ eq(sites.subnet, subnet)
exitNodeId: exitNodeId || null, )
orgId, );
name,
niceId: updatedNiceId!, if (sitesQuery.length > 0) {
type, return next(
dockerSocketEnabled: false, createHttpError(
online: true, HttpCode.CONFLICT,
subnet: "0.0.0.0/32", `Subnet ${subnet} overlaps with an existing site on this exit node. Please restart site creation.`
status: "approved" )
}) );
.returning(); }
}
let updatedNiceId = niceId;
if (!niceId) {
updatedNiceId = await getUniqueSiteName(orgId);
} else { } else {
return next( // make sure the niceId is unique
createHttpError( const existingSite = await db
HttpCode.BAD_REQUEST, .select()
"Site type not recognized" .from(sites)
.where(
and(eq(sites.niceId, niceId), eq(sites.orgId, orgId))
) )
); .limit(1);
if (existingSite.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
`Nice ID ${niceId} already exists. Please choose a different one.`
)
);
}
} }
const adminRole = await trx await db.transaction(async (trx) => {
.select() if (type == "newt") {
.from(roles) [newSite] = await trx
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) .insert(sites)
.limit(1); .values({
// NOTE: NO SUBNET OR EXIT NODE ID PASSED IN HERE BECAUSE ITS NOW CHOSEN ON CONNECT
orgId,
name,
niceId: updatedNiceId!,
address: updatedAddress || null,
type,
dockerSocketEnabled: true,
status: "approved"
})
.returning();
if (adminRole.length === 0) { await logsDb.insert(statusHistory).values({
return next( entityType: "site",
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`) entityId: newSite.siteId,
); orgId: orgId,
} status: "offline",
timestamp: Math.floor(Date.now() / 1000)
});
} else if (type == "wireguard") {
// we are creating a site with an exit node (tunneled)
if (!subnet) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Subnet is required for tunneled sites"
)
);
}
await trx.insert(roleSites).values({ if (!exitNodeId) {
roleId: adminRole[0].roleId, return next(
siteId: newSite.siteId createHttpError(
}); HttpCode.BAD_REQUEST,
"Exit node ID is required for tunneled sites"
)
);
}
if ( const { exitNode, hasAccess } =
req.user && await verifyExitNodeOrgAccess(exitNodeId, orgId);
!req.userOrgRoleIds?.includes(adminRole[0].roleId)
) { if (!exitNode) {
// make sure the user can access the site logger.warn("Exit node not found");
trx.insert(userSites).values({ return next(
userId: req.user?.userId!, createHttpError(
HttpCode.NOT_FOUND,
"Exit node not found"
)
);
}
if (!hasAccess) {
logger.warn("Not authorized to use this exit node");
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Not authorized to use this exit node"
)
);
}
[newSite] = await trx
.insert(sites)
.values({
orgId,
exitNodeId,
name,
niceId: updatedNiceId!,
subnet,
type,
pubKey: pubKey || null,
status: "approved"
})
.returning();
} else if (type == "local") {
[newSite] = await trx
.insert(sites)
.values({
exitNodeId: exitNodeId || null,
orgId,
name,
niceId: updatedNiceId!,
type,
dockerSocketEnabled: false,
online: true,
subnet: "0.0.0.0/32",
status: "approved"
})
.returning();
} else {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Site type not recognized"
)
);
}
const adminRole = await trx
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
if (adminRole.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Admin role not found`
)
);
}
await trx.insert(roleSites).values({
roleId: adminRole[0].roleId,
siteId: newSite.siteId siteId: newSite.siteId
}); });
}
// add the peer to the exit node if (
if (type == "newt") { req.user &&
const secretHash = await hashPassword(updatedNewtSecret); !req.userOrgRoleIds?.includes(adminRole[0].roleId)
) {
await trx.insert(newts).values({ // make sure the user can access the site
newtId: updatedNewtId, trx.insert(userSites).values({
secretHash, userId: req.user?.userId!,
siteId: newSite.siteId, siteId: newSite.siteId
dateCreated: moment().toISOString() });
});
} else if (type == "wireguard") {
if (!pubKey) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Public key is required for wireguard sites"
)
);
} }
if (!exitNodeId) { // add the peer to the exit node
return next( if (type == "newt") {
createHttpError( const secretHash = await hashPassword(updatedNewtSecret);
HttpCode.BAD_REQUEST,
"Exit node ID is required for wireguard sites" await trx.insert(newts).values({
) newtId: updatedNewtId,
); secretHash,
siteId: newSite.siteId,
dateCreated: moment().toISOString()
});
} else if (type == "wireguard") {
if (!pubKey) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Public key is required for wireguard sites"
)
);
}
if (!exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Exit node ID is required for wireguard sites"
)
);
}
await addPeer(exitNodeId, {
publicKey: pubKey,
allowedIps: []
});
} }
await addPeer(exitNodeId, { await usageService.add(orgId, FeatureId.SITES, 1, trx);
publicKey: pubKey, });
allowedIps: [] } finally {
}); await releaseSubnetLock?.();
} }
await usageService.add(orgId, FeatureId.SITES, 1, trx);
});
if (!newSite) { if (!newSite) {
return next( return next(
+3 -1
View File
@@ -119,7 +119,9 @@ export async function pickSiteDefaults(
); );
} }
const newClientAddress = await getNextAvailableClientSubnet(orgId); const { value: newClientAddress, release: releaseSubnetLock } =
await getNextAvailableClientSubnet(orgId);
await releaseSubnetLock(); // release immediately — this endpoint only previews the next available value
if (!newClientAddress) { if (!newClientAddress) {
return next( return next(
createHttpError( createHttpError(
+147 -128
View File
@@ -397,144 +397,163 @@ export async function createSiteResource(
} }
let aliasAddress: string | null = null; let aliasAddress: string | null = null;
let releaseAliasLock: (() => Promise<void>) | null = null;
if (mode === "host" || mode === "http") { if (mode === "host" || mode === "http") {
aliasAddress = await getNextAvailableAliasAddress(orgId); const { value, release } =
await getNextAvailableAliasAddress(orgId);
aliasAddress = value;
releaseAliasLock = release;
} }
let newSiteResource: SiteResource | undefined; let newSiteResource: SiteResource | undefined;
await db.transaction(async (trx) => { try {
const [network] = await trx await db.transaction(async (trx) => {
.insert(networks) const [network] = await trx
.values({ .insert(networks)
scope: "resource", .values({
orgId: orgId scope: "resource",
}) orgId: orgId
.returning(); })
.returning();
if (!network) { if (!network) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Failed to create network`
)
);
}
let tcpPortRangeStringAdjusted = tcpPortRangeString;
if (mode === "http") {
tcpPortRangeStringAdjusted = "443,80";
} else if (mode === "ssh") {
tcpPortRangeStringAdjusted = destinationPort
? destinationPort.toString()
: "22";
}
// Create the site resource
const insertValues: typeof siteResources.$inferInsert = {
niceId: updatedNiceId!,
orgId,
name,
mode,
ssl,
networkId: network.networkId,
destination: destination, // the ssh can be null
scheme,
destinationPort,
enabled,
alias: alias ? alias.trim() : null,
aliasAddress,
tcpPortRangeString: tcpPortRangeStringAdjusted,
udpPortRangeString:
mode == "http" || mode == "ssh" ? "" : udpPortRangeString,
disableIcmp:
disableIcmp ||
(mode == "http" || mode == "ssh" ? true : false), // default to true for http resources, otherwise false
domainId,
subdomain: finalSubdomain,
fullDomain
};
if (isLicensedSshPam) {
if (authDaemonPort !== undefined)
insertValues.authDaemonPort = authDaemonPort;
if (authDaemonMode !== undefined)
insertValues.authDaemonMode = authDaemonMode;
if (pamMode !== undefined) insertValues.pamMode = pamMode;
}
[newSiteResource] = await trx
.insert(siteResources)
.values(insertValues)
.returning();
const siteResourceId = newSiteResource.siteResourceId;
//////////////////// update the associations ////////////////////
for (const siteId of siteIds) {
await trx.insert(siteNetworks).values({
siteId: siteId,
networkId: network.networkId
});
}
const [adminRole] = await trx
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
if (!adminRole) {
return next(
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
);
}
await trx.insert(roleSiteResources).values({
roleId: adminRole.roleId,
siteResourceId: siteResourceId
});
if (roleIds.length > 0) {
await trx
.insert(roleSiteResources)
.values(
roleIds.map((roleId) => ({ roleId, siteResourceId }))
);
}
if (userIds.length > 0) {
await trx
.insert(userSiteResources)
.values(
userIds.map((userId) => ({ userId, siteResourceId }))
);
}
if (clientIds.length > 0) {
await trx.insert(clientSiteResources).values(
clientIds.map((clientId) => ({
clientId,
siteResourceId
}))
);
}
for (const siteToAssign of sitesToAssign) {
const [newt] = await trx
.select()
.from(newts)
.where(eq(newts.siteId, siteToAssign.siteId))
.limit(1);
if (!newt) {
return next( return next(
createHttpError( createHttpError(
HttpCode.NOT_FOUND, HttpCode.INTERNAL_SERVER_ERROR,
`Newt not found for site ${siteToAssign.siteId}` `Failed to create network`
) )
); );
} }
}
}); let tcpPortRangeStringAdjusted = tcpPortRangeString;
if (mode === "http") {
tcpPortRangeStringAdjusted = "443,80";
} else if (mode === "ssh") {
tcpPortRangeStringAdjusted = destinationPort
? destinationPort.toString()
: "22";
}
// Create the site resource
const insertValues: typeof siteResources.$inferInsert = {
niceId: updatedNiceId!,
orgId,
name,
mode,
ssl,
networkId: network.networkId,
destination: destination, // the ssh can be null
scheme,
destinationPort,
enabled,
alias: alias ? alias.trim() : null,
aliasAddress,
tcpPortRangeString: tcpPortRangeStringAdjusted,
udpPortRangeString:
mode == "http" || mode == "ssh"
? ""
: udpPortRangeString,
disableIcmp:
disableIcmp ||
(mode == "http" || mode == "ssh" ? true : false), // default to true for http resources, otherwise false
domainId,
subdomain: finalSubdomain,
fullDomain
};
if (isLicensedSshPam) {
if (authDaemonPort !== undefined)
insertValues.authDaemonPort = authDaemonPort;
if (authDaemonMode !== undefined)
insertValues.authDaemonMode = authDaemonMode;
if (pamMode !== undefined) insertValues.pamMode = pamMode;
}
[newSiteResource] = await trx
.insert(siteResources)
.values(insertValues)
.returning();
const siteResourceId = newSiteResource.siteResourceId;
//////////////////// update the associations ////////////////////
for (const siteId of siteIds) {
await trx.insert(siteNetworks).values({
siteId: siteId,
networkId: network.networkId
});
}
const [adminRole] = await trx
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
if (!adminRole) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Admin role not found`
)
);
}
await trx.insert(roleSiteResources).values({
roleId: adminRole.roleId,
siteResourceId: siteResourceId
});
if (roleIds.length > 0) {
await trx
.insert(roleSiteResources)
.values(
roleIds.map((roleId) => ({
roleId,
siteResourceId
}))
);
}
if (userIds.length > 0) {
await trx
.insert(userSiteResources)
.values(
userIds.map((userId) => ({
userId,
siteResourceId
}))
);
}
if (clientIds.length > 0) {
await trx.insert(clientSiteResources).values(
clientIds.map((clientId) => ({
clientId,
siteResourceId
}))
);
}
for (const siteToAssign of sitesToAssign) {
const [newt] = await trx
.select()
.from(newts)
.where(eq(newts.siteId, siteToAssign.siteId))
.limit(1);
if (!newt) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Newt not found for site ${siteToAssign.siteId}`
)
);
}
}
});
} finally {
await releaseAliasLock?.();
}
if (!newSiteResource) { if (!newSiteResource) {
return next( return next(