Merge branch 'dev'

This commit is contained in:
miloschwartz 2025-07-17 15:05:02 -07:00
commit c225a54dbe
No known key found for this signature in database
3 changed files with 18 additions and 107 deletions

View file

@ -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}}

View file

@ -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 nodes 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;
} }

View file

@ -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( currentExitNodeId = exitNode.exitNodeId;
`Exit node with name ${exitNodeName} not found in the database`
);
return [];
} }
currentExitNodeId = exitNode.exitNodeId;
} else { } else {
const [exitNode] = await tx const [exitNode] = await tx
.select({ .select({
@ -42,12 +38,9 @@ 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"); currentExitNodeId = exitNode.exitNodeId;
return [];
} }
currentExitNodeId = exitNode.exitNodeId;
} }
} }
@ -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;
} }