mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-16 10:41:52 +02:00
Unique subnets for exit nodes
This commit is contained in:
@@ -1,10 +1,31 @@
|
|||||||
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> {
|
/**
|
||||||
|
* Reserves the next available exit node subnet.
|
||||||
|
*
|
||||||
|
* Exit node subnets must never overlap with one another - regardless of
|
||||||
|
* 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
|
||||||
|
* 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(
|
||||||
|
trx: Transaction | typeof db = db
|
||||||
|
): Promise<{ value: string; release: () => Promise<void> }> {
|
||||||
|
const lockKey = "exit-node-subnet-allocation";
|
||||||
|
const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
|
||||||
|
if (!acquired) {
|
||||||
|
throw new Error(`Failed to acquire lock: ${lockKey}`);
|
||||||
|
}
|
||||||
|
const release = () => lockManager.releaseLock(lockKey, acquired);
|
||||||
|
|
||||||
|
try {
|
||||||
// Get all existing subnets from routes table
|
// Get all existing subnets from routes table
|
||||||
const existingAddresses = await db
|
const existingAddresses = await trx
|
||||||
.select({
|
.select({
|
||||||
address: exitNodes.address
|
address: exitNodes.address
|
||||||
})
|
})
|
||||||
@@ -26,5 +47,9 @@ export async function getNextAvailableSubnet(): Promise<string> {
|
|||||||
".1" +
|
".1" +
|
||||||
"/" +
|
"/" +
|
||||||
subnet.split("/")[1];
|
subnet.split("/")[1];
|
||||||
return subnet;
|
return { value: subnet, release };
|
||||||
|
} catch (e) {
|
||||||
|
await release();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ 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();
|
||||||
|
try {
|
||||||
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
|
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
|
||||||
// const listenPort = await getNextAvailablePort();
|
// const listenPort = await getNextAvailablePort();
|
||||||
const listenPort = config.getRawConfig().gerbil.start_port;
|
const listenPort = config.getRawConfig().gerbil.start_port;
|
||||||
@@ -60,6 +61,9 @@ export async function createExitNode(
|
|||||||
logger.info(
|
logger.info(
|
||||||
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
|
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
await release();
|
||||||
|
}
|
||||||
} 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,13 +189,27 @@ export async function createRemoteExitNode(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// subnet for it up front so the allocation lock is held across the
|
||||||
|
// whole insert - this guarantees exit node subnets never overlap,
|
||||||
|
// even under concurrent creation, which matters for HA setups.
|
||||||
|
let releaseSubnetLock: (() => Promise<void>) | null = null;
|
||||||
|
let newExitNodeAddress: string | null = null;
|
||||||
|
if (!existingExitNode) {
|
||||||
|
const { value, release } = await getNextAvailableSubnet();
|
||||||
|
newExitNodeAddress = value;
|
||||||
|
releaseSubnetLock = release;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
if (!existingExitNode) {
|
if (!existingExitNode) {
|
||||||
const [res] = await trx
|
const [res] = await trx
|
||||||
.insert(exitNodes)
|
.insert(exitNodes)
|
||||||
.values({
|
.values({
|
||||||
name: remoteExitNodeId,
|
name: remoteExitNodeId,
|
||||||
address,
|
address: newExitNodeAddress!,
|
||||||
endpoint: "",
|
endpoint: "",
|
||||||
publicKey: "",
|
publicKey: "",
|
||||||
listenPort: 0,
|
listenPort: 0,
|
||||||
@@ -274,6 +286,9 @@ export async function createRemoteExitNode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
await releaseSubnetLock?.();
|
||||||
|
}
|
||||||
|
|
||||||
const token = generateSessionToken();
|
const token = generateSessionToken();
|
||||||
await createRemoteExitNodeSession(token, remoteExitNodeId);
|
await createRemoteExitNodeSession(token, remoteExitNodeId);
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ 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();
|
||||||
|
try {
|
||||||
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
|
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
|
||||||
// const listenPort = await getNextAvailablePort();
|
// const listenPort = await getNextAvailablePort();
|
||||||
const listenPort = config.getRawConfig().gerbil.start_port;
|
const listenPort = config.getRawConfig().gerbil.start_port;
|
||||||
@@ -44,6 +45,9 @@ export async function createExitNode(
|
|||||||
logger.info(
|
logger.info(
|
||||||
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
|
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
await release();
|
||||||
|
}
|
||||||
} 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