Improve VPN client connection management
Build and Deploy Gateway / build-and-push (push) Successful in 20s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-12 21:48:37 +03:00
parent 068a7f9890
commit 9e52ccc24d
22 changed files with 962 additions and 76 deletions
+99 -4
View File
@@ -9,6 +9,7 @@ const DEFAULT_MAX_SERIES = 4096;
const UNKNOWN_DOMAIN = { domain: '_unknown', service: 'Не распознано' };
const ATTRIBUTION_OUTCOMES = ['unresolved_host', 'unknown_device', 'unsupported_source'] as const;
type AttributionOutcome = typeof ATTRIBUTION_OUTCOMES[number];
type TrafficRoute = 'vpn' | 'direct' | 'unknown';
const SERVICE_DOMAINS = [
['YouTube', ['youtube.com', 'youtube-nocookie.com', 'youtu.be', 'googlevideo.com', 'ytimg.com']],
['OpenAI / ChatGPT', ['chatgpt.com', 'openai.com', 'oaistatic.com', 'oaiusercontent.com']],
@@ -21,13 +22,19 @@ interface ParsedBaseConnection {
}
type ParsedConnection =
| (ParsedBaseConnection & { outcome: 'unknown_device' | 'unsupported_source' })
| (ParsedBaseConnection & { outcome: 'unsupported_source' })
| (ParsedBaseConnection & {
outcome: 'unknown_device';
source: 'gateway' | 'proxy';
outbound: TrafficRoute;
})
| (ParsedBaseConnection & {
outcome: 'classified' | 'unresolved_host';
deviceId: string;
domain: string;
service: string;
source: 'gateway' | 'proxy';
outbound: TrafficRoute;
});
interface PreviousConnection {
@@ -36,6 +43,8 @@ interface PreviousConnection {
requestedKey?: string;
countedUpload: bigint | null;
countedDownload: bigint | null;
trackedUpload: bigint | null;
trackedDownload: bigint | null;
}
interface DomainSeriesTotal {
@@ -47,12 +56,28 @@ interface DomainSeriesTotal {
downloadBytes: bigint;
}
interface RouteSeriesTotal {
deviceId?: string;
source: 'gateway' | 'proxy';
outbound: TrafficRoute;
uploadBytes: bigint;
downloadBytes: bigint;
}
interface DomainTrafficSnapshot {
epoch: string;
observedAt: string | null;
source: { error: string | null };
overflowConnections: string;
attributionEvents: Record<AttributionOutcome, string>;
tracked: Array<Omit<RouteSeriesTotal, 'deviceId' | 'uploadBytes' | 'downloadBytes'> & {
uploadBytes: string;
downloadBytes: string;
}>;
routes: Array<Omit<RouteSeriesTotal, 'uploadBytes' | 'downloadBytes'> & {
uploadBytes: string;
downloadBytes: string;
}>;
series: Array<Omit<DomainSeriesTotal, 'uploadBytes' | 'downloadBytes'> & {
uploadBytes: string;
downloadBytes: string;
@@ -86,6 +111,12 @@ function sourceFor(type: string): 'gateway' | 'proxy' | null {
return null;
}
function routeFor(value: unknown): TrafficRoute {
if (!Array.isArray(value) || !value.length
|| value.some((entry) => typeof entry !== 'string' || !entry.trim())) return 'unknown';
return value[0].trim() === 'direct' ? 'direct' : 'vpn';
}
function parseConnection(value: unknown, devicesByIp: Map<string, string | null>): ParsedConnection {
const connection = record(value);
const id = String(connection.id || '');
@@ -103,8 +134,9 @@ function parseConnection(value: unknown, devicesByIp: Map<string, string | null>
};
const source = sourceFor(String(metadata.type || ''));
if (!source) return { ...parsed, outcome: 'unsupported_source' };
const outbound = routeFor(connection.chains);
const currentDeviceId = devicesByIp.get(String(metadata.sourceIP || ''));
if (!currentDeviceId) return { ...parsed, outcome: 'unknown_device' };
if (!currentDeviceId) return { ...parsed, outcome: 'unknown_device', source, outbound };
const classifiedDomain = classifyDomain(metadata.host);
const domain = classifiedDomain || UNKNOWN_DOMAIN;
return {
@@ -113,6 +145,7 @@ function parseConnection(value: unknown, devicesByIp: Map<string, string | null>
deviceId: currentDeviceId,
...domain,
source,
outbound,
};
}
@@ -164,6 +197,8 @@ export function createDomainTrafficService({
if (!Number.isInteger(maxSeries) || maxSeries < 2) throw new Error('Domain traffic series limit должен быть не меньше 2');
const epoch = crypto.randomUUID();
const totals = new Map<string, DomainSeriesTotal>();
const routeTotals = new Map<string, RouteSeriesTotal>();
const trackedTotals = new Map<string, RouteSeriesTotal>();
const normalSeriesLimit = maxSeries - 2;
let normalSeries = 0;
let previousConnections = new Map<string, PreviousConnection>();
@@ -180,6 +215,8 @@ export function createDomainTrafficService({
source: { error: null },
overflowConnections: '0',
attributionEvents: { unresolved_host: '0', unknown_device: '0', unsupported_source: '0' },
tracked: [],
routes: [],
series: [],
};
@@ -192,6 +229,27 @@ export function createDomainTrafficService({
attributionEvents: Object.fromEntries(
ATTRIBUTION_OUTCOMES.map((outcome) => [outcome, attributionEvents[outcome].toString()]),
) as Record<AttributionOutcome, string>,
tracked: [...trackedTotals.values()]
.map((entry) => ({
source: entry.source,
outbound: entry.outbound,
uploadBytes: entry.uploadBytes.toString(),
downloadBytes: entry.downloadBytes.toString(),
}))
.sort((left, right) => (
left.source.localeCompare(right.source) || left.outbound.localeCompare(right.outbound)
)),
routes: [...routeTotals.values()]
.map((entry) => ({
...entry,
uploadBytes: entry.uploadBytes.toString(),
downloadBytes: entry.downloadBytes.toString(),
}))
.sort((left, right) => (
String(left.deviceId).localeCompare(String(right.deviceId))
|| left.source.localeCompare(right.source)
|| left.outbound.localeCompare(right.outbound)
)),
series: [...totals.values()]
.map((entry) => ({
...entry,
@@ -220,18 +278,42 @@ export function createDomainTrafficService({
if (!net.isIPv4(ip) || !id) continue;
devicesByIp.set(ip, devicesByIp.has(ip) ? null : id);
}
const connections = response.connections.map((connection) => parseConnection(connection, devicesByIp));
const activeConnections = new Map<string, PreviousConnection>();
for (const rawConnection of response.connections) {
const connection = parseConnection(rawConnection, devicesByIp);
for (const connection of connections) {
const previous = previousConnections.get(connection.id);
if (connection.outcome !== 'classified' && previous?.outcome !== connection.outcome) {
attributionEvents[connection.outcome] += 1n;
}
if (connection.outcome !== 'unsupported_source') {
const uploadDelta = previous?.trackedUpload != null && connection.upload >= previous.trackedUpload
? connection.upload - previous.trackedUpload
: connection.upload;
const downloadDelta = previous?.trackedDownload != null && connection.download >= previous.trackedDownload
? connection.download - previous.trackedDownload
: connection.download;
const trackedKey = `${connection.source}\0${connection.outbound}`;
const tracked = trackedTotals.get(trackedKey) || {
source: connection.source,
outbound: connection.outbound,
uploadBytes: 0n,
downloadBytes: 0n,
};
tracked.uploadBytes += uploadDelta;
tracked.downloadBytes += downloadDelta;
trackedTotals.set(trackedKey, tracked);
}
if (connection.outcome === 'unknown_device' || connection.outcome === 'unsupported_source') {
activeConnections.set(connection.id, {
outcome: connection.outcome,
countedUpload: previous?.countedUpload ?? null,
countedDownload: previous?.countedDownload ?? null,
trackedUpload: connection.outcome === 'unknown_device'
? connection.upload
: previous?.trackedUpload ?? null,
trackedDownload: connection.outcome === 'unknown_device'
? connection.download
: previous?.trackedDownload ?? null,
});
continue;
}
@@ -257,6 +339,17 @@ export function createDomainTrafficService({
const downloadDelta = previous?.countedDownload != null && connection.download >= previous.countedDownload
? connection.download - previous.countedDownload
: connection.download;
const routeKey = `${connection.deviceId}\0${connection.source}\0${connection.outbound}`;
const routeTotal = routeTotals.get(routeKey) || {
deviceId: connection.deviceId,
source: connection.source,
outbound: connection.outbound,
uploadBytes: 0n,
downloadBytes: 0n,
};
routeTotal.uploadBytes += uploadDelta;
routeTotal.downloadBytes += downloadDelta;
routeTotals.set(routeKey, routeTotal);
const total = totals.get(key) || {
deviceId: key === requestedKey ? connection.deviceId : '_other',
domain,
@@ -274,6 +367,8 @@ export function createDomainTrafficService({
requestedKey,
countedUpload: connection.upload,
countedDownload: connection.download,
trackedUpload: connection.upload,
trackedDownload: connection.download,
});
}
previousConnections = activeConnections;