mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-07 06:39:52 +02:00
Unique subnets for exit nodes
This commit is contained in:
@@ -1,30 +1,55 @@
|
|||||||
import { db, exitNodes } from "@server/db";
|
import { db, exitNodes, 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";
|
||||||
|
|
||||||
export async function getNextAvailableSubnet(): Promise<string> {
|
/**
|
||||||
// Get all existing subnets from routes table
|
* Reserves the next available exit node subnet.
|
||||||
const existingAddresses = await db
|
*
|
||||||
.select({
|
* Exit node subnets must never overlap with one another - regardless of
|
||||||
address: exitNodes.address
|
* which org(s) they belong to - since HA exit nodes can end up routing for
|
||||||
})
|
* the same org. This acquires a lock that the caller MUST release (via the
|
||||||
.from(exitNodes);
|
* returned `release`) only after the chosen address has been durably
|
||||||
|
* persisted (e.g. after the enclosing transaction commits), otherwise
|
||||||
const addresses = existingAddresses.map((a) => a.address);
|
* concurrent callers can race and pick the same subnet.
|
||||||
let subnet = findNextAvailableCidr(
|
*/
|
||||||
addresses,
|
export async function getNextAvailableSubnet(
|
||||||
config.getRawConfig().gerbil.block_size,
|
trx: Transaction | typeof db = db
|
||||||
config.getRawConfig().gerbil.subnet_group
|
): Promise<{ value: string; release: () => Promise<void> }> {
|
||||||
);
|
const lockKey = "exit-node-subnet-allocation";
|
||||||
if (!subnet) {
|
const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
|
||||||
throw new Error("No available subnets remaining in space");
|
if (!acquired) {
|
||||||
|
throw new Error(`Failed to acquire lock: ${lockKey}`);
|
||||||
}
|
}
|
||||||
|
const release = () => lockManager.releaseLock(lockKey, acquired);
|
||||||
|
|
||||||
// replace the last octet with 1
|
try {
|
||||||
subnet =
|
// Get all existing subnets from routes table
|
||||||
subnet.split(".").slice(0, 3).join(".") +
|
const existingAddresses = await trx
|
||||||
".1" +
|
.select({
|
||||||
"/" +
|
address: exitNodes.address
|
||||||
subnet.split("/")[1];
|
})
|
||||||
return subnet;
|
.from(exitNodes);
|
||||||
|
|
||||||
|
const addresses = existingAddresses.map((a) => a.address);
|
||||||
|
let subnet = findNextAvailableCidr(
|
||||||
|
addresses,
|
||||||
|
config.getRawConfig().gerbil.block_size,
|
||||||
|
config.getRawConfig().gerbil.subnet_group
|
||||||
|
);
|
||||||
|
if (!subnet) {
|
||||||
|
throw new Error("No available subnets remaining in space");
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace the last octet with 1
|
||||||
|
subnet =
|
||||||
|
subnet.split(".").slice(0, 3).join(".") +
|
||||||
|
".1" +
|
||||||
|
"/" +
|
||||||
|
subnet.split("/")[1];
|
||||||
|
return { value: subnet, release };
|
||||||
|
} catch (e) {
|
||||||
|
await release();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,37 +29,41 @@ export async function createExitNode(
|
|||||||
.where(eq(exitNodes.publicKey, publicKey));
|
.where(eq(exitNodes.publicKey, publicKey));
|
||||||
let exitNode: ExitNode;
|
let exitNode: ExitNode;
|
||||||
if (!exitNodeQuery) {
|
if (!exitNodeQuery) {
|
||||||
const address = await getNextAvailableSubnet();
|
const { value: address, release } = await getNextAvailableSubnet();
|
||||||
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
|
try {
|
||||||
// const listenPort = await getNextAvailablePort();
|
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
|
||||||
const listenPort = config.getRawConfig().gerbil.start_port;
|
// const listenPort = await getNextAvailablePort();
|
||||||
let subEndpoint = "";
|
const listenPort = config.getRawConfig().gerbil.start_port;
|
||||||
if (config.getRawConfig().gerbil.use_subdomain) {
|
let subEndpoint = "";
|
||||||
subEndpoint = await getUniqueExitNodeEndpointName();
|
if (config.getRawConfig().gerbil.use_subdomain) {
|
||||||
|
subEndpoint = await getUniqueExitNodeEndpointName();
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitNodeName =
|
||||||
|
config.getRawConfig().gerbil.exit_node_name ||
|
||||||
|
`Exit Node ${publicKey.slice(0, 8)}`;
|
||||||
|
|
||||||
|
// create a new exit node
|
||||||
|
[exitNode] = await db
|
||||||
|
.insert(exitNodes)
|
||||||
|
.values({
|
||||||
|
publicKey,
|
||||||
|
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
|
||||||
|
address,
|
||||||
|
listenPort,
|
||||||
|
online: true,
|
||||||
|
reachableAt,
|
||||||
|
name: exitNodeName
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await release();
|
||||||
}
|
}
|
||||||
|
|
||||||
const exitNodeName =
|
|
||||||
config.getRawConfig().gerbil.exit_node_name ||
|
|
||||||
`Exit Node ${publicKey.slice(0, 8)}`;
|
|
||||||
|
|
||||||
// create a new exit node
|
|
||||||
[exitNode] = await db
|
|
||||||
.insert(exitNodes)
|
|
||||||
.values({
|
|
||||||
publicKey,
|
|
||||||
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
|
|
||||||
address,
|
|
||||||
listenPort,
|
|
||||||
online: true,
|
|
||||||
reachableAt,
|
|
||||||
name: exitNodeName
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// update the reachable at
|
// update the reachable at
|
||||||
[exitNode] = await db
|
[exitNode] = await db
|
||||||
|
|||||||
@@ -114,8 +114,6 @@ export async function createRemoteExitNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const secretHash = await hashPassword(secret);
|
const secretHash = await hashPassword(secret);
|
||||||
// const address = await getNextAvailableSubnet();
|
|
||||||
const address = "100.89.140.1/24"; // FOR NOW LETS HARDCODE THESE ADDRESSES
|
|
||||||
|
|
||||||
const [existingRemoteExitNode] = await db
|
const [existingRemoteExitNode] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -191,89 +189,106 @@ export async function createRemoteExitNode(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
// If this remote exit node isn't already backing an exit node in
|
||||||
if (!existingExitNode) {
|
// another org, we're about to create a brand new one. Reserve a
|
||||||
const [res] = await trx
|
// subnet for it up front so the allocation lock is held across the
|
||||||
.insert(exitNodes)
|
// whole insert - this guarantees exit node subnets never overlap,
|
||||||
.values({
|
// even under concurrent creation, which matters for HA setups.
|
||||||
name: remoteExitNodeId,
|
let releaseSubnetLock: (() => Promise<void>) | null = null;
|
||||||
address,
|
let newExitNodeAddress: string | null = null;
|
||||||
endpoint: "",
|
if (!existingExitNode) {
|
||||||
publicKey: "",
|
const { value, release } = await getNextAvailableSubnet();
|
||||||
listenPort: 0,
|
newExitNodeAddress = value;
|
||||||
online: false,
|
releaseSubnetLock = release;
|
||||||
type: "remoteExitNode"
|
}
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
existingExitNode = res;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existingRemoteExitNode) {
|
try {
|
||||||
await trx.insert(remoteExitNodes).values({
|
await db.transaction(async (trx) => {
|
||||||
remoteExitNodeId: remoteExitNodeId,
|
if (!existingExitNode) {
|
||||||
secretHash,
|
const [res] = await trx
|
||||||
dateCreated: moment().toISOString(),
|
.insert(exitNodes)
|
||||||
exitNodeId: existingExitNode.exitNodeId
|
.values({
|
||||||
});
|
name: remoteExitNodeId,
|
||||||
} else {
|
address: newExitNodeAddress!,
|
||||||
// update the existing remote exit node
|
endpoint: "",
|
||||||
await trx
|
publicKey: "",
|
||||||
.update(remoteExitNodes)
|
listenPort: 0,
|
||||||
.set({
|
online: false,
|
||||||
exitNodeId: existingExitNode.exitNodeId
|
type: "remoteExitNode"
|
||||||
})
|
})
|
||||||
.where(
|
.returning();
|
||||||
eq(
|
existingExitNode = res;
|
||||||
remoteExitNodes.remoteExitNodeId,
|
|
||||||
existingRemoteExitNode.remoteExitNodeId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existingExitNodeOrg) {
|
|
||||||
await trx.insert(exitNodeOrgs).values({
|
|
||||||
exitNodeId: existingExitNode.exitNodeId,
|
|
||||||
orgId: orgId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate if the node is in any other of the orgs before we count it as an add to the billing org
|
|
||||||
if (org.billingOrgId) {
|
|
||||||
const otherBillingOrgs = await trx
|
|
||||||
.select()
|
|
||||||
.from(orgs)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(orgs.billingOrgId, org.billingOrgId),
|
|
||||||
ne(orgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const billingOrgIds = otherBillingOrgs.map((o) => o.orgId);
|
|
||||||
|
|
||||||
const orgsInBillingDomainThatTheNodeIsStillIn = await trx
|
|
||||||
.select()
|
|
||||||
.from(exitNodeOrgs)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(
|
|
||||||
exitNodeOrgs.exitNodeId,
|
|
||||||
existingExitNode.exitNodeId
|
|
||||||
),
|
|
||||||
inArray(exitNodeOrgs.orgId, billingOrgIds)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
|
|
||||||
await usageService.add(
|
|
||||||
orgId,
|
|
||||||
LimitId.REMOTE_EXIT_NODES,
|
|
||||||
1,
|
|
||||||
trx
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
if (!existingRemoteExitNode) {
|
||||||
|
await trx.insert(remoteExitNodes).values({
|
||||||
|
remoteExitNodeId: remoteExitNodeId,
|
||||||
|
secretHash,
|
||||||
|
dateCreated: moment().toISOString(),
|
||||||
|
exitNodeId: existingExitNode.exitNodeId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// update the existing remote exit node
|
||||||
|
await trx
|
||||||
|
.update(remoteExitNodes)
|
||||||
|
.set({
|
||||||
|
exitNodeId: existingExitNode.exitNodeId
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
remoteExitNodes.remoteExitNodeId,
|
||||||
|
existingRemoteExitNode.remoteExitNodeId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingExitNodeOrg) {
|
||||||
|
await trx.insert(exitNodeOrgs).values({
|
||||||
|
exitNodeId: existingExitNode.exitNodeId,
|
||||||
|
orgId: orgId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate if the node is in any other of the orgs before we count it as an add to the billing org
|
||||||
|
if (org.billingOrgId) {
|
||||||
|
const otherBillingOrgs = await trx
|
||||||
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(orgs.billingOrgId, org.billingOrgId),
|
||||||
|
ne(orgs.orgId, orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const billingOrgIds = otherBillingOrgs.map((o) => o.orgId);
|
||||||
|
|
||||||
|
const orgsInBillingDomainThatTheNodeIsStillIn = await trx
|
||||||
|
.select()
|
||||||
|
.from(exitNodeOrgs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
exitNodeOrgs.exitNodeId,
|
||||||
|
existingExitNode.exitNodeId
|
||||||
|
),
|
||||||
|
inArray(exitNodeOrgs.orgId, billingOrgIds)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
|
||||||
|
await usageService.add(
|
||||||
|
orgId,
|
||||||
|
LimitId.REMOTE_EXIT_NODES,
|
||||||
|
1,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await releaseSubnetLock?.();
|
||||||
|
}
|
||||||
|
|
||||||
const token = generateSessionToken();
|
const token = generateSessionToken();
|
||||||
await createRemoteExitNodeSession(token, remoteExitNodeId);
|
await createRemoteExitNodeSession(token, remoteExitNodeId);
|
||||||
|
|||||||
@@ -13,37 +13,41 @@ export async function createExitNode(
|
|||||||
const [exitNodeQuery] = await db.select().from(exitNodes).limit(1);
|
const [exitNodeQuery] = await db.select().from(exitNodes).limit(1);
|
||||||
let exitNode: ExitNode;
|
let exitNode: ExitNode;
|
||||||
if (!exitNodeQuery) {
|
if (!exitNodeQuery) {
|
||||||
const address = await getNextAvailableSubnet();
|
const { value: address, release } = await getNextAvailableSubnet();
|
||||||
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
|
try {
|
||||||
// const listenPort = await getNextAvailablePort();
|
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
|
||||||
const listenPort = config.getRawConfig().gerbil.start_port;
|
// const listenPort = await getNextAvailablePort();
|
||||||
let subEndpoint = "";
|
const listenPort = config.getRawConfig().gerbil.start_port;
|
||||||
if (config.getRawConfig().gerbil.use_subdomain) {
|
let subEndpoint = "";
|
||||||
subEndpoint = await getUniqueExitNodeEndpointName();
|
if (config.getRawConfig().gerbil.use_subdomain) {
|
||||||
|
subEndpoint = await getUniqueExitNodeEndpointName();
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitNodeName =
|
||||||
|
config.getRawConfig().gerbil.exit_node_name ||
|
||||||
|
`Exit Node ${publicKey.slice(0, 8)}`;
|
||||||
|
|
||||||
|
// create a new exit node
|
||||||
|
[exitNode] = await db
|
||||||
|
.insert(exitNodes)
|
||||||
|
.values({
|
||||||
|
publicKey,
|
||||||
|
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
|
||||||
|
address,
|
||||||
|
online: true,
|
||||||
|
listenPort,
|
||||||
|
reachableAt,
|
||||||
|
name: exitNodeName
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await release();
|
||||||
}
|
}
|
||||||
|
|
||||||
const exitNodeName =
|
|
||||||
config.getRawConfig().gerbil.exit_node_name ||
|
|
||||||
`Exit Node ${publicKey.slice(0, 8)}`;
|
|
||||||
|
|
||||||
// create a new exit node
|
|
||||||
[exitNode] = await db
|
|
||||||
.insert(exitNodes)
|
|
||||||
.values({
|
|
||||||
publicKey,
|
|
||||||
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
|
|
||||||
address,
|
|
||||||
online: true,
|
|
||||||
listenPort,
|
|
||||||
reachableAt,
|
|
||||||
name: exitNodeName
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// update the existing exit node
|
// update the existing exit node
|
||||||
[exitNode] = await db
|
[exitNode] = await db
|
||||||
|
|||||||
Reference in New Issue
Block a user