Improve VPN client connection management
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user