mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-25 05:25:39 +02:00
Scope exit node creation to orgs
This commit is contained in:
@@ -1,20 +1,26 @@
|
|||||||
import { db, exitNodes, Transaction } from "@server/db";
|
import { db, exitNodes, exitNodeOrgs, Transaction } from "@server/db";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { findNextAvailableCidr } from "@server/lib/ip";
|
import { findNextAvailableCidr } from "@server/lib/ip";
|
||||||
import { lockManager } from "#dynamic/lib/lock";
|
import { lockManager } from "#dynamic/lib/lock";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reserves the next available exit node subnet.
|
* Reserves the next available exit node subnet.
|
||||||
*
|
*
|
||||||
* Exit node subnets must never overlap with one another - regardless of
|
* There isn't enough address space to give every exit node in every org a
|
||||||
* which org(s) they belong to - since HA exit nodes can end up routing for
|
* globally unique subnet, so we only guarantee uniqueness among exit nodes
|
||||||
* the same org. This acquires a lock that the caller MUST release (via the
|
* that already belong to the same org - that's all that actually matters,
|
||||||
* returned `release`) only after the chosen address has been durably
|
* since HA only routes multiple exit nodes for a single org. Pass `orgId` to
|
||||||
* persisted (e.g. after the enclosing transaction commits), otherwise
|
* scope the search to that org's existing exit nodes; without it, the search
|
||||||
* concurrent callers can race and pick the same subnet.
|
* considers every exit node (used by flows with no org context, e.g. the
|
||||||
|
* initial gerbil exit node bootstrap). This acquires a lock that the caller
|
||||||
|
* MUST release (via the returned `release`) only after the chosen address
|
||||||
|
* has been durably persisted (e.g. after the enclosing transaction commits),
|
||||||
|
* otherwise concurrent callers can race and pick the same subnet.
|
||||||
*/
|
*/
|
||||||
export async function getNextAvailableSubnet(
|
export async function getNextAvailableSubnet(
|
||||||
trx: Transaction | typeof db = db
|
trx: Transaction | typeof db = db,
|
||||||
|
orgId?: string
|
||||||
): Promise<{ value: string; release: () => Promise<void> }> {
|
): Promise<{ value: string; release: () => Promise<void> }> {
|
||||||
const lockKey = "exit-node-subnet-allocation";
|
const lockKey = "exit-node-subnet-allocation";
|
||||||
const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
|
const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
|
||||||
@@ -24,12 +30,19 @@ export async function getNextAvailableSubnet(
|
|||||||
const release = () => lockManager.releaseLock(lockKey, acquired);
|
const release = () => lockManager.releaseLock(lockKey, acquired);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all existing subnets from routes table
|
// Get existing subnets, scoped to this org's exit nodes when known
|
||||||
const existingAddresses = await trx
|
const existingAddresses = orgId
|
||||||
.select({
|
? await trx
|
||||||
address: exitNodes.address
|
.select({ address: exitNodes.address })
|
||||||
})
|
.from(exitNodes)
|
||||||
.from(exitNodes);
|
.innerJoin(
|
||||||
|
exitNodeOrgs,
|
||||||
|
eq(exitNodeOrgs.exitNodeId, exitNodes.exitNodeId)
|
||||||
|
)
|
||||||
|
.where(eq(exitNodeOrgs.orgId, orgId))
|
||||||
|
: await trx
|
||||||
|
.select({ address: exitNodes.address })
|
||||||
|
.from(exitNodes);
|
||||||
|
|
||||||
const addresses = existingAddresses.map((a) => a.address);
|
const addresses = existingAddresses.map((a) => a.address);
|
||||||
let subnet = findNextAvailableCidr(
|
let subnet = findNextAvailableCidr(
|
||||||
|
|||||||
@@ -191,13 +191,20 @@ export async function createRemoteExitNode(
|
|||||||
|
|
||||||
// If this remote exit node isn't already backing an exit node in
|
// If this remote exit node isn't already backing an exit node in
|
||||||
// another org, we're about to create a brand new one. Reserve a
|
// another org, we're about to create a brand new one. Reserve a
|
||||||
// subnet for it up front so the allocation lock is held across the
|
// subnet for it up front, scoped to this org's existing exit nodes,
|
||||||
// whole insert - this guarantees exit node subnets never overlap,
|
// so the allocation lock is held across the whole insert - this
|
||||||
// even under concurrent creation, which matters for HA setups.
|
// guarantees exit node subnets never overlap within the org, even
|
||||||
|
// under concurrent creation, which matters for HA setups. Subnets
|
||||||
|
// may still be reused across different orgs; there isn't enough
|
||||||
|
// address space to avoid that, and it isn't necessary since HA only
|
||||||
|
// routes multiple exit nodes for the same org.
|
||||||
let releaseSubnetLock: (() => Promise<void>) | null = null;
|
let releaseSubnetLock: (() => Promise<void>) | null = null;
|
||||||
let newExitNodeAddress: string | null = null;
|
let newExitNodeAddress: string | null = null;
|
||||||
if (!existingExitNode) {
|
if (!existingExitNode) {
|
||||||
const { value, release } = await getNextAvailableSubnet();
|
const { value, release } = await getNextAvailableSubnet(
|
||||||
|
db,
|
||||||
|
orgId
|
||||||
|
);
|
||||||
newExitNodeAddress = value;
|
newExitNodeAddress = value;
|
||||||
releaseSubnetLock = release;
|
releaseSubnetLock = release;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user