mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-29 14:18:26 +02:00
Merge branch 'dev'
This commit is contained in:
commit
c225a54dbe
3 changed files with 18 additions and 107 deletions
|
@ -30,7 +30,7 @@ orgs:
|
||||||
email:
|
email:
|
||||||
smtp_host: "{{.EmailSMTPHost}}"
|
smtp_host: "{{.EmailSMTPHost}}"
|
||||||
smtp_port: {{.EmailSMTPPort}}
|
smtp_port: {{.EmailSMTPPort}}
|
||||||
smtp_user: "{{.EmailSMTPUser}allow_base_domain_resources}"
|
smtp_user: "{{.EmailSMTPUser}}"
|
||||||
smtp_pass: "{{.EmailSMTPPass}}"
|
smtp_pass: "{{.EmailSMTPPass}}"
|
||||||
no_reply: "{{.EmailNoReply}}"
|
no_reply: "{{.EmailNoReply}}"
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -258,101 +258,13 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Selects the most suitable exit node from a list of ping results.
|
|
||||||
*
|
|
||||||
* The selection algorithm follows these steps:
|
|
||||||
*
|
|
||||||
* 1. **Filter Invalid Nodes**: Excludes nodes with errors or zero weight.
|
|
||||||
*
|
|
||||||
* 2. **Sort by Latency**: Sorts valid nodes in ascending order of latency.
|
|
||||||
*
|
|
||||||
* 3. **Preferred Selection**:
|
|
||||||
* - If the lowest-latency node has sufficient capacity (≥10% weight),
|
|
||||||
* check if a previously connected node is also acceptable.
|
|
||||||
* - The previously connected node is preferred if its latency is within
|
|
||||||
* 30ms or 15% of the best node’s latency.
|
|
||||||
*
|
|
||||||
* 4. **Fallback to Next Best**:
|
|
||||||
* - If the lowest-latency node is under capacity, find the next node
|
|
||||||
* with acceptable capacity.
|
|
||||||
*
|
|
||||||
* 5. **Final Fallback**:
|
|
||||||
* - If no nodes meet the capacity threshold, fall back to the node
|
|
||||||
* with the highest weight (i.e., most available capacity).
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function selectBestExitNode(
|
function selectBestExitNode(
|
||||||
pingResults: ExitNodePingResult[]
|
pingResults: ExitNodePingResult[]
|
||||||
): ExitNodePingResult | null {
|
): ExitNodePingResult | null {
|
||||||
const MIN_CAPACITY_THRESHOLD = 0.1;
|
if (!pingResults || pingResults.length === 0) {
|
||||||
const LATENCY_TOLERANCE_MS = 30;
|
logger.warn("No ping results provided");
|
||||||
const LATENCY_TOLERANCE_PERCENT = 0.15;
|
|
||||||
|
|
||||||
// Filter out invalid nodes
|
|
||||||
const validNodes = pingResults.filter((n) => !n.error && n.weight > 0);
|
|
||||||
|
|
||||||
if (validNodes.length === 0) {
|
|
||||||
logger.error("No valid exit nodes available");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by latency (ascending)
|
return pingResults[0];
|
||||||
const sortedNodes = validNodes
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => a.latencyMs - b.latencyMs);
|
|
||||||
const lowestLatencyNode = sortedNodes[0];
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Lowest latency node: ${lowestLatencyNode.exitNodeName} (${lowestLatencyNode.latencyMs} ms, weight=${lowestLatencyNode.weight.toFixed(2)})`
|
|
||||||
);
|
|
||||||
|
|
||||||
// If lowest latency node has enough capacity, check if previously connected node is acceptable
|
|
||||||
if (lowestLatencyNode.weight >= MIN_CAPACITY_THRESHOLD) {
|
|
||||||
const previouslyConnectedNode = sortedNodes.find(
|
|
||||||
(n) =>
|
|
||||||
n.wasPreviouslyConnected && n.weight >= MIN_CAPACITY_THRESHOLD
|
|
||||||
);
|
|
||||||
|
|
||||||
if (previouslyConnectedNode) {
|
|
||||||
const latencyDiff =
|
|
||||||
previouslyConnectedNode.latencyMs - lowestLatencyNode.latencyMs;
|
|
||||||
const percentDiff = latencyDiff / lowestLatencyNode.latencyMs;
|
|
||||||
|
|
||||||
if (
|
|
||||||
latencyDiff <= LATENCY_TOLERANCE_MS ||
|
|
||||||
percentDiff <= LATENCY_TOLERANCE_PERCENT
|
|
||||||
) {
|
|
||||||
logger.info(
|
|
||||||
`Sticking with previously connected node: ${previouslyConnectedNode.exitNodeName} ` +
|
|
||||||
`(${previouslyConnectedNode.latencyMs} ms), latency diff = ${latencyDiff.toFixed(1)}ms ` +
|
|
||||||
`/ ${(percentDiff * 100).toFixed(1)}%.`
|
|
||||||
);
|
|
||||||
return previouslyConnectedNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lowestLatencyNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, find the next node (after the lowest) that has enough capacity
|
|
||||||
for (let i = 1; i < sortedNodes.length; i++) {
|
|
||||||
const node = sortedNodes[i];
|
|
||||||
if (node.weight >= MIN_CAPACITY_THRESHOLD) {
|
|
||||||
logger.info(
|
|
||||||
`Lowest latency node under capacity. Using next best: ${node.exitNodeName} ` +
|
|
||||||
`(${node.latencyMs} ms, weight=${node.weight.toFixed(2)})`
|
|
||||||
);
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: pick the highest weight node
|
|
||||||
const fallbackNode = validNodes.reduce((a, b) =>
|
|
||||||
a.weight > b.weight ? a : b
|
|
||||||
);
|
|
||||||
logger.warn(
|
|
||||||
`No nodes with ≥10% weight. Falling back to highest capacity node: ${fallbackNode.exitNodeName}`
|
|
||||||
);
|
|
||||||
return fallbackNode;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { db, exitNodes } from "@server/db";
|
import { db, exitNodes } from "@server/db";
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray, or, isNull } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
@ -27,13 +27,9 @@ export async function traefikConfigProvider(
|
||||||
})
|
})
|
||||||
.from(exitNodes)
|
.from(exitNodes)
|
||||||
.where(eq(exitNodes.name, exitNodeName));
|
.where(eq(exitNodes.name, exitNodeName));
|
||||||
if (!exitNode) {
|
if (exitNode) {
|
||||||
logger.error(
|
|
||||||
`Exit node with name ${exitNodeName} not found in the database`
|
|
||||||
);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
currentExitNodeId = exitNode.exitNodeId;
|
currentExitNodeId = exitNode.exitNodeId;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const [exitNode] = await tx
|
const [exitNode] = await tx
|
||||||
.select({
|
.select({
|
||||||
|
@ -42,14 +38,11 @@ export async function traefikConfigProvider(
|
||||||
.from(exitNodes)
|
.from(exitNodes)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!exitNode) {
|
if (exitNode) {
|
||||||
logger.error("No exit node found in the database");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
currentExitNodeId = exitNode.exitNodeId;
|
currentExitNodeId = exitNode.exitNodeId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get the site(s) on this exit node
|
// Get the site(s) on this exit node
|
||||||
const resourcesWithRelations = await tx
|
const resourcesWithRelations = await tx
|
||||||
|
@ -68,7 +61,7 @@ export async function traefikConfigProvider(
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
type: sites.type,
|
type: sites.type,
|
||||||
subnet: sites.subnet,
|
subnet: sites.subnet,
|
||||||
exitNodeId: sites.exitNodeId,
|
exitNodeId: sites.exitNodeId
|
||||||
},
|
},
|
||||||
enabled: resources.enabled,
|
enabled: resources.enabled,
|
||||||
stickySession: resources.stickySession,
|
stickySession: resources.stickySession,
|
||||||
|
@ -77,7 +70,12 @@ export async function traefikConfigProvider(
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.innerJoin(sites, eq(sites.siteId, resources.siteId))
|
.innerJoin(sites, eq(sites.siteId, resources.siteId))
|
||||||
.where(eq(sites.exitNodeId, currentExitNodeId));
|
.where(
|
||||||
|
or(
|
||||||
|
eq(sites.exitNodeId, currentExitNodeId),
|
||||||
|
isNull(sites.exitNodeId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Get all resource IDs from the first query
|
// Get all resource IDs from the first query
|
||||||
const resourceIds = resourcesWithRelations.map((r) => r.resourceId);
|
const resourceIds = resourcesWithRelations.map((r) => r.resourceId);
|
||||||
|
@ -284,7 +282,8 @@ export async function traefikConfigProvider(
|
||||||
} else if (site.type === "newt") {
|
} else if (site.type === "newt") {
|
||||||
if (
|
if (
|
||||||
!target.internalPort ||
|
!target.internalPort ||
|
||||||
!target.method || !site.subnet
|
!target.method ||
|
||||||
|
!site.subnet
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue