fix(metrics): improve metrics
This commit is contained in:
@ -1,11 +1,10 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
|
||||
import { SecurityManager } from './security-manager.js';
|
||||
import { TimeoutManager } from './timeout-manager.js';
|
||||
import type { IConnectionRecord } from './models/interfaces.js';
|
||||
import { logger } from '../../core/utils/logger.js';
|
||||
import { LifecycleComponent } from '../../core/utils/lifecycle-component.js';
|
||||
import { cleanupSocket } from '../../core/utils/socket-utils.js';
|
||||
import { WrappedSocket } from '../../core/models/wrapped-socket.js';
|
||||
import type { SmartProxy } from './smart-proxy.js';
|
||||
|
||||
/**
|
||||
* Manages connection lifecycle, tracking, and cleanup with performance optimizations
|
||||
@ -29,17 +28,15 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
private settings: ISmartProxyOptions,
|
||||
private securityManager: SecurityManager,
|
||||
private timeoutManager: TimeoutManager
|
||||
private smartProxy: SmartProxy
|
||||
) {
|
||||
super();
|
||||
|
||||
// Set reasonable defaults for connection limits
|
||||
this.maxConnections = settings.defaults?.security?.maxConnections || 10000;
|
||||
this.maxConnections = smartProxy.settings.defaults?.security?.maxConnections || 10000;
|
||||
|
||||
// Start inactivity check timer if not disabled
|
||||
if (!settings.disableInactivityCheck) {
|
||||
if (!smartProxy.settings.disableInactivityCheck) {
|
||||
this.startInactivityCheckTimer();
|
||||
}
|
||||
}
|
||||
@ -108,10 +105,10 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
*/
|
||||
public trackConnection(connectionId: string, record: IConnectionRecord): void {
|
||||
this.connectionRecords.set(connectionId, record);
|
||||
this.securityManager.trackConnectionByIP(record.remoteIP, connectionId);
|
||||
this.smartProxy.securityManager.trackConnectionByIP(record.remoteIP, connectionId);
|
||||
|
||||
// Schedule inactivity check
|
||||
if (!this.settings.disableInactivityCheck) {
|
||||
if (!this.smartProxy.settings.disableInactivityCheck) {
|
||||
this.scheduleInactivityCheck(connectionId, record);
|
||||
}
|
||||
}
|
||||
@ -120,14 +117,14 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
* Schedule next inactivity check for a connection
|
||||
*/
|
||||
private scheduleInactivityCheck(connectionId: string, record: IConnectionRecord): void {
|
||||
let timeout = this.settings.inactivityTimeout!;
|
||||
let timeout = this.smartProxy.settings.inactivityTimeout!;
|
||||
|
||||
if (record.hasKeepAlive) {
|
||||
if (this.settings.keepAliveTreatment === 'immortal') {
|
||||
if (this.smartProxy.settings.keepAliveTreatment === 'immortal') {
|
||||
// Don't schedule check for immortal connections
|
||||
return;
|
||||
} else if (this.settings.keepAliveTreatment === 'extended') {
|
||||
const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
|
||||
} else if (this.smartProxy.settings.keepAliveTreatment === 'extended') {
|
||||
const multiplier = this.smartProxy.settings.keepAliveInactivityMultiplier || 6;
|
||||
timeout = timeout * multiplier;
|
||||
}
|
||||
}
|
||||
@ -172,7 +169,7 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
* Initiates cleanup once for a connection
|
||||
*/
|
||||
public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `Connection cleanup initiated`, {
|
||||
connectionId: record.id,
|
||||
remoteIP: record.remoteIP,
|
||||
@ -253,7 +250,12 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
this.nextInactivityCheck.delete(record.id);
|
||||
|
||||
// Track connection termination
|
||||
this.securityManager.removeConnectionByIP(record.remoteIP, record.id);
|
||||
this.smartProxy.securityManager.removeConnectionByIP(record.remoteIP, record.id);
|
||||
|
||||
// Remove from metrics tracking
|
||||
if (this.smartProxy.metricsCollector) {
|
||||
this.smartProxy.metricsCollector.removeConnection(record.id);
|
||||
}
|
||||
|
||||
if (record.cleanupTimer) {
|
||||
clearTimeout(record.cleanupTimer);
|
||||
@ -334,7 +336,7 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
this.connectionRecords.delete(record.id);
|
||||
|
||||
// Log connection details
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info',
|
||||
`Connection terminated: ${record.remoteIP}:${record.localPort} (${reason}) - ` +
|
||||
`${plugins.prettyMs(duration)}, IN: ${record.bytesReceived}B, OUT: ${record.bytesSent}B`,
|
||||
@ -414,7 +416,7 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
*/
|
||||
public handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
|
||||
return () => {
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `Connection closed on ${side} side`, {
|
||||
connectionId: record.id,
|
||||
side,
|
||||
@ -553,9 +555,9 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
const inactivityTime = now - record.lastActivity;
|
||||
|
||||
// Use extended timeout for extended-treatment keep-alive connections
|
||||
let effectiveTimeout = this.settings.inactivityTimeout!;
|
||||
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
||||
const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
|
||||
let effectiveTimeout = this.smartProxy.settings.inactivityTimeout!;
|
||||
if (record.hasKeepAlive && this.smartProxy.settings.keepAliveTreatment === 'extended') {
|
||||
const multiplier = this.smartProxy.settings.keepAliveInactivityMultiplier || 6;
|
||||
effectiveTimeout = effectiveTimeout * multiplier;
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,15 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { HttpProxy } from '../http-proxy/index.js';
|
||||
import { setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
|
||||
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
|
||||
import type { IConnectionRecord } from './models/interfaces.js';
|
||||
import type { IRouteConfig } from './models/route-types.js';
|
||||
import { WrappedSocket } from '../../core/models/wrapped-socket.js';
|
||||
import type { SmartProxy } from './smart-proxy.js';
|
||||
|
||||
export class HttpProxyBridge {
|
||||
private httpProxy: HttpProxy | null = null;
|
||||
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
constructor(private smartProxy: SmartProxy) {}
|
||||
|
||||
/**
|
||||
* Get the HttpProxy instance
|
||||
@ -21,18 +22,18 @@ export class HttpProxyBridge {
|
||||
* Initialize HttpProxy instance
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
if (!this.httpProxy && this.settings.useHttpProxy && this.settings.useHttpProxy.length > 0) {
|
||||
if (!this.httpProxy && this.smartProxy.settings.useHttpProxy && this.smartProxy.settings.useHttpProxy.length > 0) {
|
||||
const httpProxyOptions: any = {
|
||||
port: this.settings.httpProxyPort!,
|
||||
port: this.smartProxy.settings.httpProxyPort!,
|
||||
portProxyIntegration: true,
|
||||
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
|
||||
logLevel: this.smartProxy.settings.enableDetailedLogging ? 'debug' : 'info'
|
||||
};
|
||||
|
||||
this.httpProxy = new HttpProxy(httpProxyOptions);
|
||||
console.log(`Initialized HttpProxy on port ${this.settings.httpProxyPort}`);
|
||||
console.log(`Initialized HttpProxy on port ${this.smartProxy.settings.httpProxyPort}`);
|
||||
|
||||
// Apply route configurations to HttpProxy
|
||||
await this.syncRoutesToHttpProxy(this.settings.routes || []);
|
||||
await this.syncRoutesToHttpProxy(this.smartProxy.settings.routes || []);
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +52,7 @@ export class HttpProxyBridge {
|
||||
: [route.match.ports];
|
||||
|
||||
return routePorts.some(port =>
|
||||
this.settings.useHttpProxy?.includes(port)
|
||||
this.smartProxy.settings.useHttpProxy?.includes(port)
|
||||
);
|
||||
})
|
||||
.map(route => this.routeToHttpProxyConfig(route));
|
||||
|
@ -1,258 +1,341 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { SmartProxy } from './smart-proxy.js';
|
||||
import type { IProxyStats, IProxyStatsExtended } from './models/metrics-types.js';
|
||||
import type {
|
||||
IMetrics,
|
||||
IThroughputData,
|
||||
IThroughputHistoryPoint,
|
||||
IByteTracker
|
||||
} from './models/metrics-types.js';
|
||||
import { ThroughputTracker } from './throughput-tracker.js';
|
||||
import { logger } from '../../core/utils/logger.js';
|
||||
|
||||
/**
|
||||
* Collects and computes metrics for SmartProxy on-demand
|
||||
* Collects and provides metrics for SmartProxy with clean API
|
||||
*/
|
||||
export class MetricsCollector implements IProxyStatsExtended {
|
||||
// RPS tracking (the only state we need to maintain)
|
||||
export class MetricsCollector implements IMetrics {
|
||||
// Throughput tracking
|
||||
private throughputTracker: ThroughputTracker;
|
||||
|
||||
// Request tracking
|
||||
private requestTimestamps: number[] = [];
|
||||
private readonly RPS_WINDOW_SIZE = 60000; // 1 minute window
|
||||
private readonly MAX_TIMESTAMPS = 5000; // Maximum timestamps to keep
|
||||
private totalRequests: number = 0;
|
||||
|
||||
// Optional caching for performance
|
||||
private cachedMetrics: {
|
||||
timestamp: number;
|
||||
connectionsByRoute?: Map<string, number>;
|
||||
connectionsByIP?: Map<string, number>;
|
||||
} = { timestamp: 0 };
|
||||
// Connection byte tracking for per-route/IP metrics
|
||||
private connectionByteTrackers = new Map<string, IByteTracker>();
|
||||
|
||||
private readonly CACHE_TTL = 1000; // 1 second cache
|
||||
|
||||
// RxJS subscription for connection events
|
||||
// Subscriptions
|
||||
private samplingInterval?: NodeJS.Timeout;
|
||||
private connectionSubscription?: plugins.smartrx.rxjs.Subscription;
|
||||
|
||||
// Configuration
|
||||
private readonly sampleIntervalMs: number;
|
||||
private readonly retentionSeconds: number;
|
||||
|
||||
constructor(
|
||||
private smartProxy: SmartProxy
|
||||
private smartProxy: SmartProxy,
|
||||
config?: {
|
||||
sampleIntervalMs?: number;
|
||||
retentionSeconds?: number;
|
||||
}
|
||||
) {
|
||||
// Subscription will be set up in start() method
|
||||
this.sampleIntervalMs = config?.sampleIntervalMs || 1000;
|
||||
this.retentionSeconds = config?.retentionSeconds || 3600;
|
||||
this.throughputTracker = new ThroughputTracker(this.retentionSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current number of active connections
|
||||
*/
|
||||
public getActiveConnections(): number {
|
||||
return this.smartProxy.connectionManager.getConnectionCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection counts grouped by route name
|
||||
*/
|
||||
public getConnectionsByRoute(): Map<string, number> {
|
||||
const now = Date.now();
|
||||
// Connection metrics implementation
|
||||
public connections = {
|
||||
active: (): number => {
|
||||
return this.smartProxy.connectionManager.getConnectionCount();
|
||||
},
|
||||
|
||||
// Return cached value if fresh
|
||||
if (this.cachedMetrics.connectionsByRoute &&
|
||||
now - this.cachedMetrics.timestamp < this.CACHE_TTL) {
|
||||
return new Map(this.cachedMetrics.connectionsByRoute);
|
||||
}
|
||||
|
||||
// Compute fresh value
|
||||
const routeCounts = new Map<string, number>();
|
||||
const connections = this.smartProxy.connectionManager.getConnections();
|
||||
|
||||
if (this.smartProxy.settings?.enableDetailedLogging) {
|
||||
logger.log('debug', `MetricsCollector: Computing route connections`, {
|
||||
totalConnections: connections.size,
|
||||
component: 'metrics'
|
||||
});
|
||||
}
|
||||
|
||||
for (const [_, record] of connections) {
|
||||
// Try different ways to get the route name
|
||||
const routeName = (record as any).routeName ||
|
||||
record.routeConfig?.name ||
|
||||
(record.routeConfig as any)?.routeName ||
|
||||
'unknown';
|
||||
total: (): number => {
|
||||
const stats = this.smartProxy.connectionManager.getTerminationStats();
|
||||
let total = this.smartProxy.connectionManager.getConnectionCount();
|
||||
|
||||
if (this.smartProxy.settings?.enableDetailedLogging) {
|
||||
logger.log('debug', `MetricsCollector: Connection route info`, {
|
||||
connectionId: record.id,
|
||||
routeName,
|
||||
hasRouteConfig: !!record.routeConfig,
|
||||
routeConfigName: record.routeConfig?.name,
|
||||
routeConfigKeys: record.routeConfig ? Object.keys(record.routeConfig) : [],
|
||||
component: 'metrics'
|
||||
for (const reason in stats.incoming) {
|
||||
total += stats.incoming[reason];
|
||||
}
|
||||
|
||||
return total;
|
||||
},
|
||||
|
||||
byRoute: (): Map<string, number> => {
|
||||
const routeCounts = new Map<string, number>();
|
||||
const connections = this.smartProxy.connectionManager.getConnections();
|
||||
|
||||
for (const [_, record] of connections) {
|
||||
const routeName = (record as any).routeName ||
|
||||
record.routeConfig?.name ||
|
||||
'unknown';
|
||||
|
||||
const current = routeCounts.get(routeName) || 0;
|
||||
routeCounts.set(routeName, current + 1);
|
||||
}
|
||||
|
||||
return routeCounts;
|
||||
},
|
||||
|
||||
byIP: (): Map<string, number> => {
|
||||
const ipCounts = new Map<string, number>();
|
||||
|
||||
for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
|
||||
const ip = record.remoteIP;
|
||||
const current = ipCounts.get(ip) || 0;
|
||||
ipCounts.set(ip, current + 1);
|
||||
}
|
||||
|
||||
return ipCounts;
|
||||
},
|
||||
|
||||
topIPs: (limit: number = 10): Array<{ ip: string; count: number }> => {
|
||||
const ipCounts = this.connections.byIP();
|
||||
return Array.from(ipCounts.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, limit)
|
||||
.map(([ip, count]) => ({ ip, count }));
|
||||
}
|
||||
};
|
||||
|
||||
// Throughput metrics implementation
|
||||
public throughput = {
|
||||
instant: (): IThroughputData => {
|
||||
return this.throughputTracker.getRate(1);
|
||||
},
|
||||
|
||||
recent: (): IThroughputData => {
|
||||
return this.throughputTracker.getRate(10);
|
||||
},
|
||||
|
||||
average: (): IThroughputData => {
|
||||
return this.throughputTracker.getRate(60);
|
||||
},
|
||||
|
||||
custom: (seconds: number): IThroughputData => {
|
||||
return this.throughputTracker.getRate(seconds);
|
||||
},
|
||||
|
||||
history: (seconds: number): Array<IThroughputHistoryPoint> => {
|
||||
return this.throughputTracker.getHistory(seconds);
|
||||
},
|
||||
|
||||
byRoute: (windowSeconds: number = 60): Map<string, IThroughputData> => {
|
||||
const routeThroughput = new Map<string, IThroughputData>();
|
||||
const now = Date.now();
|
||||
const windowStart = now - (windowSeconds * 1000);
|
||||
|
||||
// Aggregate bytes by route from trackers
|
||||
const routeBytes = new Map<string, { in: number; out: number }>();
|
||||
|
||||
for (const [_, tracker] of this.connectionByteTrackers) {
|
||||
if (tracker.lastUpdate > windowStart) {
|
||||
const current = routeBytes.get(tracker.routeName) || { in: 0, out: 0 };
|
||||
current.in += tracker.bytesIn;
|
||||
current.out += tracker.bytesOut;
|
||||
routeBytes.set(tracker.routeName, current);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to rates
|
||||
for (const [route, bytes] of routeBytes) {
|
||||
routeThroughput.set(route, {
|
||||
in: Math.round(bytes.in / windowSeconds),
|
||||
out: Math.round(bytes.out / windowSeconds)
|
||||
});
|
||||
}
|
||||
|
||||
const current = routeCounts.get(routeName) || 0;
|
||||
routeCounts.set(routeName, current + 1);
|
||||
}
|
||||
return routeThroughput;
|
||||
},
|
||||
|
||||
// Cache and return
|
||||
this.cachedMetrics.connectionsByRoute = routeCounts;
|
||||
this.cachedMetrics.timestamp = now;
|
||||
return new Map(routeCounts);
|
||||
}
|
||||
byIP: (windowSeconds: number = 60): Map<string, IThroughputData> => {
|
||||
const ipThroughput = new Map<string, IThroughputData>();
|
||||
const now = Date.now();
|
||||
const windowStart = now - (windowSeconds * 1000);
|
||||
|
||||
// Aggregate bytes by IP from trackers
|
||||
const ipBytes = new Map<string, { in: number; out: number }>();
|
||||
|
||||
for (const [_, tracker] of this.connectionByteTrackers) {
|
||||
if (tracker.lastUpdate > windowStart) {
|
||||
const current = ipBytes.get(tracker.remoteIP) || { in: 0, out: 0 };
|
||||
current.in += tracker.bytesIn;
|
||||
current.out += tracker.bytesOut;
|
||||
ipBytes.set(tracker.remoteIP, current);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to rates
|
||||
for (const [ip, bytes] of ipBytes) {
|
||||
ipThroughput.set(ip, {
|
||||
in: Math.round(bytes.in / windowSeconds),
|
||||
out: Math.round(bytes.out / windowSeconds)
|
||||
});
|
||||
}
|
||||
|
||||
return ipThroughput;
|
||||
}
|
||||
};
|
||||
|
||||
// Request metrics implementation
|
||||
public requests = {
|
||||
perSecond: (): number => {
|
||||
const now = Date.now();
|
||||
const oneSecondAgo = now - 1000;
|
||||
|
||||
// Clean old timestamps
|
||||
this.requestTimestamps = this.requestTimestamps.filter(ts => ts > now - 60000);
|
||||
|
||||
// Count requests in last second
|
||||
const recentRequests = this.requestTimestamps.filter(ts => ts > oneSecondAgo);
|
||||
return recentRequests.length;
|
||||
},
|
||||
|
||||
perMinute: (): number => {
|
||||
const now = Date.now();
|
||||
const oneMinuteAgo = now - 60000;
|
||||
|
||||
// Count requests in last minute
|
||||
const recentRequests = this.requestTimestamps.filter(ts => ts > oneMinuteAgo);
|
||||
return recentRequests.length;
|
||||
},
|
||||
|
||||
total: (): number => {
|
||||
return this.totalRequests;
|
||||
}
|
||||
};
|
||||
|
||||
// Totals implementation
|
||||
public totals = {
|
||||
bytesIn: (): number => {
|
||||
let total = 0;
|
||||
|
||||
// Sum from all active connections
|
||||
for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
|
||||
total += record.bytesReceived;
|
||||
}
|
||||
|
||||
// TODO: Add historical data from terminated connections
|
||||
|
||||
return total;
|
||||
},
|
||||
|
||||
bytesOut: (): number => {
|
||||
let total = 0;
|
||||
|
||||
// Sum from all active connections
|
||||
for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
|
||||
total += record.bytesSent;
|
||||
}
|
||||
|
||||
// TODO: Add historical data from terminated connections
|
||||
|
||||
return total;
|
||||
},
|
||||
|
||||
connections: (): number => {
|
||||
return this.connections.total();
|
||||
}
|
||||
};
|
||||
|
||||
// Percentiles implementation (placeholder for now)
|
||||
public percentiles = {
|
||||
connectionDuration: (): { p50: number; p95: number; p99: number } => {
|
||||
// TODO: Implement percentile calculations
|
||||
return { p50: 0, p95: 0, p99: 0 };
|
||||
},
|
||||
|
||||
bytesTransferred: (): {
|
||||
in: { p50: number; p95: number; p99: number };
|
||||
out: { p50: number; p95: number; p99: number };
|
||||
} => {
|
||||
// TODO: Implement percentile calculations
|
||||
return {
|
||||
in: { p50: 0, p95: 0, p99: 0 },
|
||||
out: { p50: 0, p95: 0, p99: 0 }
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get connection counts grouped by IP address
|
||||
* Record a new request
|
||||
*/
|
||||
public getConnectionsByIP(): Map<string, number> {
|
||||
const now = Date.now();
|
||||
|
||||
// Return cached value if fresh
|
||||
if (this.cachedMetrics.connectionsByIP &&
|
||||
now - this.cachedMetrics.timestamp < this.CACHE_TTL) {
|
||||
return new Map(this.cachedMetrics.connectionsByIP);
|
||||
}
|
||||
|
||||
// Compute fresh value
|
||||
const ipCounts = new Map<string, number>();
|
||||
for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
|
||||
const ip = record.remoteIP;
|
||||
const current = ipCounts.get(ip) || 0;
|
||||
ipCounts.set(ip, current + 1);
|
||||
}
|
||||
|
||||
// Cache and return
|
||||
this.cachedMetrics.connectionsByIP = ipCounts;
|
||||
this.cachedMetrics.timestamp = now;
|
||||
return new Map(ipCounts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of connections since proxy start
|
||||
*/
|
||||
public getTotalConnections(): number {
|
||||
// Get from termination stats
|
||||
const stats = this.smartProxy.connectionManager.getTerminationStats();
|
||||
let total = this.smartProxy.connectionManager.getConnectionCount(); // Add active connections
|
||||
|
||||
// Add all terminated connections
|
||||
for (const reason in stats.incoming) {
|
||||
total += stats.incoming[reason];
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current requests per second rate
|
||||
*/
|
||||
public getRequestsPerSecond(): number {
|
||||
const now = Date.now();
|
||||
const windowStart = now - this.RPS_WINDOW_SIZE;
|
||||
|
||||
// Clean old timestamps
|
||||
this.requestTimestamps = this.requestTimestamps.filter(ts => ts > windowStart);
|
||||
|
||||
// Calculate RPS based on window
|
||||
const requestsInWindow = this.requestTimestamps.length;
|
||||
return requestsInWindow / (this.RPS_WINDOW_SIZE / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a new request for RPS tracking
|
||||
*/
|
||||
public recordRequest(): void {
|
||||
public recordRequest(connectionId: string, routeName: string, remoteIP: string): void {
|
||||
const now = Date.now();
|
||||
this.requestTimestamps.push(now);
|
||||
this.totalRequests++;
|
||||
|
||||
// Prevent unbounded growth - clean up more aggressively
|
||||
if (this.requestTimestamps.length > this.MAX_TIMESTAMPS) {
|
||||
// Keep only timestamps within the window
|
||||
const cutoff = now - this.RPS_WINDOW_SIZE;
|
||||
// Initialize byte tracker for this connection
|
||||
this.connectionByteTrackers.set(connectionId, {
|
||||
connectionId,
|
||||
routeName,
|
||||
remoteIP,
|
||||
bytesIn: 0,
|
||||
bytesOut: 0,
|
||||
lastUpdate: now
|
||||
});
|
||||
|
||||
// Cleanup old request timestamps (keep last minute only)
|
||||
if (this.requestTimestamps.length > 1000) {
|
||||
const cutoff = now - 60000;
|
||||
this.requestTimestamps = this.requestTimestamps.filter(ts => ts > cutoff);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total throughput (bytes transferred)
|
||||
* Record bytes transferred for a connection
|
||||
*/
|
||||
public getThroughput(): { bytesIn: number; bytesOut: number } {
|
||||
let bytesIn = 0;
|
||||
let bytesOut = 0;
|
||||
public recordBytes(connectionId: string, bytesIn: number, bytesOut: number): void {
|
||||
// Update global throughput tracker
|
||||
this.throughputTracker.recordBytes(bytesIn, bytesOut);
|
||||
|
||||
// Sum bytes from all active connections
|
||||
for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
|
||||
bytesIn += record.bytesReceived;
|
||||
bytesOut += record.bytesSent;
|
||||
// Update connection-specific tracker
|
||||
const tracker = this.connectionByteTrackers.get(connectionId);
|
||||
if (tracker) {
|
||||
tracker.bytesIn += bytesIn;
|
||||
tracker.bytesOut += bytesOut;
|
||||
tracker.lastUpdate = Date.now();
|
||||
}
|
||||
|
||||
return { bytesIn, bytesOut };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get throughput rate (bytes per second) for last minute
|
||||
* Clean up tracking for a closed connection
|
||||
*/
|
||||
public getThroughputRate(): { bytesInPerSec: number; bytesOutPerSec: number } {
|
||||
const now = Date.now();
|
||||
let recentBytesIn = 0;
|
||||
let recentBytesOut = 0;
|
||||
|
||||
// Calculate bytes transferred in last minute from active connections
|
||||
for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
|
||||
const connectionAge = now - record.incomingStartTime;
|
||||
if (connectionAge < 60000) { // Connection started within last minute
|
||||
recentBytesIn += record.bytesReceived;
|
||||
recentBytesOut += record.bytesSent;
|
||||
} else {
|
||||
// For older connections, estimate rate based on average
|
||||
const rate = connectionAge / 60000;
|
||||
recentBytesIn += record.bytesReceived / rate;
|
||||
recentBytesOut += record.bytesSent / rate;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bytesInPerSec: Math.round(recentBytesIn / 60),
|
||||
bytesOutPerSec: Math.round(recentBytesOut / 60)
|
||||
};
|
||||
public removeConnection(connectionId: string): void {
|
||||
this.connectionByteTrackers.delete(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top IPs by connection count
|
||||
*/
|
||||
public getTopIPs(limit: number = 10): Array<{ ip: string; connections: number }> {
|
||||
const ipCounts = this.getConnectionsByIP();
|
||||
const sorted = Array.from(ipCounts.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, limit)
|
||||
.map(([ip, connections]) => ({ ip, connections }));
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP has reached the connection limit
|
||||
*/
|
||||
public isIPBlocked(ip: string, maxConnectionsPerIP: number): boolean {
|
||||
const ipCounts = this.getConnectionsByIP();
|
||||
const currentConnections = ipCounts.get(ip) || 0;
|
||||
return currentConnections >= maxConnectionsPerIP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old request timestamps
|
||||
*/
|
||||
private cleanupOldRequests(): void {
|
||||
const cutoff = Date.now() - this.RPS_WINDOW_SIZE;
|
||||
this.requestTimestamps = this.requestTimestamps.filter(ts => ts > cutoff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the metrics collector and set up subscriptions
|
||||
* Start the metrics collector
|
||||
*/
|
||||
public start(): void {
|
||||
if (!this.smartProxy.routeConnectionHandler) {
|
||||
throw new Error('MetricsCollector: RouteConnectionHandler not available');
|
||||
}
|
||||
|
||||
// Subscribe to the newConnectionSubject from RouteConnectionHandler
|
||||
// Start periodic sampling
|
||||
this.samplingInterval = setInterval(() => {
|
||||
this.throughputTracker.takeSample();
|
||||
|
||||
// Clean up old connection trackers (connections closed more than 5 minutes ago)
|
||||
const cutoff = Date.now() - 300000;
|
||||
for (const [id, tracker] of this.connectionByteTrackers) {
|
||||
if (tracker.lastUpdate < cutoff) {
|
||||
this.connectionByteTrackers.delete(id);
|
||||
}
|
||||
}
|
||||
}, this.sampleIntervalMs);
|
||||
|
||||
// Subscribe to new connections
|
||||
this.connectionSubscription = this.smartProxy.routeConnectionHandler.newConnectionSubject.subscribe({
|
||||
next: (record) => {
|
||||
this.recordRequest();
|
||||
const routeName = record.routeConfig?.name || 'unknown';
|
||||
this.recordRequest(record.id, routeName, record.remoteIP);
|
||||
|
||||
// Optional: Log connection details
|
||||
if (this.smartProxy.settings?.enableDetailedLogging) {
|
||||
logger.log('debug', `MetricsCollector: New connection recorded`, {
|
||||
connectionId: record.id,
|
||||
remoteIP: record.remoteIP,
|
||||
routeName: record.routeConfig?.name || 'unknown',
|
||||
routeName,
|
||||
component: 'metrics'
|
||||
});
|
||||
}
|
||||
@ -269,9 +352,14 @@ export class MetricsCollector implements IProxyStatsExtended {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the metrics collector and clean up resources
|
||||
* Stop the metrics collector
|
||||
*/
|
||||
public stop(): void {
|
||||
if (this.samplingInterval) {
|
||||
clearInterval(this.samplingInterval);
|
||||
this.samplingInterval = undefined;
|
||||
}
|
||||
|
||||
if (this.connectionSubscription) {
|
||||
this.connectionSubscription.unsubscribe();
|
||||
this.connectionSubscription = undefined;
|
||||
@ -281,7 +369,7 @@ export class MetricsCollector implements IProxyStatsExtended {
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for stop() for backward compatibility
|
||||
* Alias for stop() for compatibility
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.stop();
|
||||
|
@ -1,54 +1,106 @@
|
||||
/**
|
||||
* Interface for proxy statistics and metrics
|
||||
* Interface for throughput sample data
|
||||
*/
|
||||
export interface IProxyStats {
|
||||
/**
|
||||
* Get the current number of active connections
|
||||
*/
|
||||
getActiveConnections(): number;
|
||||
|
||||
/**
|
||||
* Get connection counts grouped by route name
|
||||
*/
|
||||
getConnectionsByRoute(): Map<string, number>;
|
||||
|
||||
/**
|
||||
* Get connection counts grouped by IP address
|
||||
*/
|
||||
getConnectionsByIP(): Map<string, number>;
|
||||
|
||||
/**
|
||||
* Get the total number of connections since proxy start
|
||||
*/
|
||||
getTotalConnections(): number;
|
||||
|
||||
/**
|
||||
* Get the current requests per second rate
|
||||
*/
|
||||
getRequestsPerSecond(): number;
|
||||
|
||||
/**
|
||||
* Get total throughput (bytes transferred)
|
||||
*/
|
||||
getThroughput(): { bytesIn: number; bytesOut: number };
|
||||
export interface IThroughputSample {
|
||||
timestamp: number;
|
||||
bytesIn: number;
|
||||
bytesOut: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended interface for additional metrics helpers
|
||||
* Interface for throughput data
|
||||
*/
|
||||
export interface IProxyStatsExtended extends IProxyStats {
|
||||
/**
|
||||
* Get throughput rate (bytes per second) for last minute
|
||||
*/
|
||||
getThroughputRate(): { bytesInPerSec: number; bytesOutPerSec: number };
|
||||
export interface IThroughputData {
|
||||
in: number;
|
||||
out: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for time-series throughput data
|
||||
*/
|
||||
export interface IThroughputHistoryPoint {
|
||||
timestamp: number;
|
||||
in: number;
|
||||
out: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main metrics interface with clean, grouped API
|
||||
*/
|
||||
export interface IMetrics {
|
||||
// Connection metrics
|
||||
connections: {
|
||||
active(): number;
|
||||
total(): number;
|
||||
byRoute(): Map<string, number>;
|
||||
byIP(): Map<string, number>;
|
||||
topIPs(limit?: number): Array<{ ip: string; count: number }>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get top IPs by connection count
|
||||
*/
|
||||
getTopIPs(limit?: number): Array<{ ip: string; connections: number }>;
|
||||
// Throughput metrics (bytes per second)
|
||||
throughput: {
|
||||
instant(): IThroughputData; // Last 1 second
|
||||
recent(): IThroughputData; // Last 10 seconds
|
||||
average(): IThroughputData; // Last 60 seconds
|
||||
custom(seconds: number): IThroughputData;
|
||||
history(seconds: number): Array<IThroughputHistoryPoint>;
|
||||
byRoute(windowSeconds?: number): Map<string, IThroughputData>;
|
||||
byIP(windowSeconds?: number): Map<string, IThroughputData>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an IP has reached the connection limit
|
||||
*/
|
||||
isIPBlocked(ip: string, maxConnectionsPerIP: number): boolean;
|
||||
// Request metrics
|
||||
requests: {
|
||||
perSecond(): number;
|
||||
perMinute(): number;
|
||||
total(): number;
|
||||
};
|
||||
|
||||
// Cumulative totals
|
||||
totals: {
|
||||
bytesIn(): number;
|
||||
bytesOut(): number;
|
||||
connections(): number;
|
||||
};
|
||||
|
||||
// Performance metrics
|
||||
percentiles: {
|
||||
connectionDuration(): { p50: number; p95: number; p99: number };
|
||||
bytesTransferred(): {
|
||||
in: { p50: number; p95: number; p99: number };
|
||||
out: { p50: number; p95: number; p99: number };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for metrics collection
|
||||
*/
|
||||
export interface IMetricsConfig {
|
||||
enabled: boolean;
|
||||
|
||||
// Sampling configuration
|
||||
sampleIntervalMs: number; // Default: 1000 (1 second)
|
||||
retentionSeconds: number; // Default: 3600 (1 hour)
|
||||
|
||||
// Performance tuning
|
||||
enableDetailedTracking: boolean; // Per-connection byte history
|
||||
enablePercentiles: boolean; // Calculate percentiles
|
||||
cacheResultsMs: number; // Cache expensive calculations
|
||||
|
||||
// Export configuration
|
||||
prometheusEnabled: boolean;
|
||||
prometheusPath: string; // Default: /metrics
|
||||
prometheusPrefix: string; // Default: smartproxy_
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal interface for connection byte tracking
|
||||
*/
|
||||
export interface IByteTracker {
|
||||
connectionId: string;
|
||||
routeName: string;
|
||||
remoteIP: string;
|
||||
bytesIn: number;
|
||||
bytesOut: number;
|
||||
lastUpdate: number;
|
||||
}
|
@ -10,7 +10,7 @@ import type {
|
||||
TPortRange,
|
||||
INfTablesOptions
|
||||
} from './models/route-types.js';
|
||||
import type { ISmartProxyOptions } from './models/interfaces.js';
|
||||
import type { SmartProxy } from './smart-proxy.js';
|
||||
|
||||
/**
|
||||
* Manages NFTables rules based on SmartProxy route configurations
|
||||
@ -25,9 +25,9 @@ export class NFTablesManager {
|
||||
/**
|
||||
* Creates a new NFTablesManager
|
||||
*
|
||||
* @param options The SmartProxy options
|
||||
* @param smartProxy The SmartProxy instance
|
||||
*/
|
||||
constructor(private options: ISmartProxyOptions) {}
|
||||
constructor(private smartProxy: SmartProxy) {}
|
||||
|
||||
/**
|
||||
* Provision NFTables rules for a route
|
||||
@ -166,10 +166,10 @@ export class NFTablesManager {
|
||||
protocol: action.nftables?.protocol || 'tcp',
|
||||
preserveSourceIP: action.nftables?.preserveSourceIP !== undefined ?
|
||||
action.nftables.preserveSourceIP :
|
||||
this.options.preserveSourceIP,
|
||||
this.smartProxy.settings.preserveSourceIP,
|
||||
useIPSets: action.nftables?.useIPSets !== false,
|
||||
useAdvancedNAT: action.nftables?.useAdvancedNAT,
|
||||
enableLogging: this.options.enableDetailedLogging,
|
||||
enableLogging: this.smartProxy.settings.enableDetailedLogging,
|
||||
deleteOnExit: true,
|
||||
tableName: action.nftables?.tableName || 'smartproxy'
|
||||
};
|
||||
|
@ -1,8 +1,7 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { ISmartProxyOptions } from './models/interfaces.js';
|
||||
import { RouteConnectionHandler } from './route-connection-handler.js';
|
||||
import { logger } from '../../core/utils/logger.js';
|
||||
import { cleanupSocket } from '../../core/utils/socket-utils.js';
|
||||
import type { SmartProxy } from './smart-proxy.js';
|
||||
|
||||
/**
|
||||
* PortManager handles the dynamic creation and removal of port listeners
|
||||
@ -16,8 +15,6 @@ import { cleanupSocket } from '../../core/utils/socket-utils.js';
|
||||
*/
|
||||
export class PortManager {
|
||||
private servers: Map<number, plugins.net.Server> = new Map();
|
||||
private settings: ISmartProxyOptions;
|
||||
private routeConnectionHandler: RouteConnectionHandler;
|
||||
private isShuttingDown: boolean = false;
|
||||
// Track how many routes are using each port
|
||||
private portRefCounts: Map<number, number> = new Map();
|
||||
@ -25,16 +22,11 @@ export class PortManager {
|
||||
/**
|
||||
* Create a new PortManager
|
||||
*
|
||||
* @param settings The SmartProxy settings
|
||||
* @param routeConnectionHandler The handler for new connections
|
||||
* @param smartProxy The SmartProxy instance
|
||||
*/
|
||||
constructor(
|
||||
settings: ISmartProxyOptions,
|
||||
routeConnectionHandler: RouteConnectionHandler
|
||||
) {
|
||||
this.settings = settings;
|
||||
this.routeConnectionHandler = routeConnectionHandler;
|
||||
}
|
||||
private smartProxy: SmartProxy
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Start listening on a specific port
|
||||
@ -70,7 +62,7 @@ export class PortManager {
|
||||
}
|
||||
|
||||
// Delegate to route connection handler
|
||||
this.routeConnectionHandler.handleConnection(socket);
|
||||
this.smartProxy.routeConnectionHandler.handleConnection(socket);
|
||||
}).on('error', (err: Error) => {
|
||||
try {
|
||||
logger.log('error', `Server Error on port ${port}: ${err.message}`, {
|
||||
@ -86,7 +78,7 @@ export class PortManager {
|
||||
// Start listening on the port
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
server.listen(port, () => {
|
||||
const isHttpProxyPort = this.settings.useHttpProxy?.includes(port);
|
||||
const isHttpProxyPort = this.smartProxy.settings.useHttpProxy?.includes(port);
|
||||
try {
|
||||
logger.log('info', `SmartProxy -> OK: Now listening on port ${port}${
|
||||
isHttpProxyPort ? ' (HttpProxy forwarding enabled)' : ''
|
||||
|
@ -4,23 +4,16 @@ import { logger } from '../../core/utils/logger.js';
|
||||
// Route checking functions have been removed
|
||||
import type { IRouteConfig, IRouteAction } from './models/route-types.js';
|
||||
import type { IRouteContext } from '../../core/models/route-context.js';
|
||||
import { ConnectionManager } from './connection-manager.js';
|
||||
import { SecurityManager } from './security-manager.js';
|
||||
import { TlsManager } from './tls-manager.js';
|
||||
import { HttpProxyBridge } from './http-proxy-bridge.js';
|
||||
import { TimeoutManager } from './timeout-manager.js';
|
||||
import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
|
||||
import { cleanupSocket, setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
|
||||
import { WrappedSocket } from '../../core/models/wrapped-socket.js';
|
||||
import { getUnderlyingSocket } from '../../core/models/socket-types.js';
|
||||
import { ProxyProtocolParser } from '../../core/utils/proxy-protocol.js';
|
||||
import type { SmartProxy } from './smart-proxy.js';
|
||||
|
||||
/**
|
||||
* Handles new connection processing and setup logic with support for route-based configuration
|
||||
*/
|
||||
export class RouteConnectionHandler {
|
||||
private settings: ISmartProxyOptions;
|
||||
|
||||
// Note: Route context caching was considered but not implemented
|
||||
// as route contexts are lightweight and should be created fresh
|
||||
// for each connection to ensure accurate context data
|
||||
@ -29,16 +22,8 @@ export class RouteConnectionHandler {
|
||||
public newConnectionSubject = new plugins.smartrx.rxjs.Subject<IConnectionRecord>();
|
||||
|
||||
constructor(
|
||||
settings: ISmartProxyOptions,
|
||||
private connectionManager: ConnectionManager,
|
||||
private securityManager: SecurityManager,
|
||||
private tlsManager: TlsManager,
|
||||
private httpProxyBridge: HttpProxyBridge,
|
||||
private timeoutManager: TimeoutManager,
|
||||
private routeManager: RouteManager
|
||||
) {
|
||||
this.settings = settings;
|
||||
}
|
||||
private smartProxy: SmartProxy
|
||||
) {}
|
||||
|
||||
|
||||
/**
|
||||
@ -93,7 +78,7 @@ export class RouteConnectionHandler {
|
||||
const wrappedSocket = new WrappedSocket(socket);
|
||||
|
||||
// If this is from a trusted proxy, log it
|
||||
if (this.settings.proxyIPs?.includes(remoteIP)) {
|
||||
if (this.smartProxy.settings.proxyIPs?.includes(remoteIP)) {
|
||||
logger.log('debug', `Connection from trusted proxy ${remoteIP}, PROXY protocol parsing will be enabled`, {
|
||||
remoteIP,
|
||||
component: 'route-handler'
|
||||
@ -102,7 +87,7 @@ export class RouteConnectionHandler {
|
||||
|
||||
// Validate IP against rate limits and connection limits
|
||||
// Note: For wrapped sockets, this will use the underlying socket IP until PROXY protocol is parsed
|
||||
const ipValidation = this.securityManager.validateIP(wrappedSocket.remoteAddress || '');
|
||||
const ipValidation = this.smartProxy.securityManager.validateIP(wrappedSocket.remoteAddress || '');
|
||||
if (!ipValidation.allowed) {
|
||||
logger.log('warn', `Connection rejected`, { remoteIP: wrappedSocket.remoteAddress, reason: ipValidation.reason, component: 'route-handler' });
|
||||
cleanupSocket(wrappedSocket.socket, `rejected-${ipValidation.reason}`, { immediate: true });
|
||||
@ -110,7 +95,7 @@ export class RouteConnectionHandler {
|
||||
}
|
||||
|
||||
// Create a new connection record with the wrapped socket
|
||||
const record = this.connectionManager.createConnection(wrappedSocket);
|
||||
const record = this.smartProxy.connectionManager.createConnection(wrappedSocket);
|
||||
if (!record) {
|
||||
// Connection was rejected due to limit - socket already destroyed by connection manager
|
||||
return;
|
||||
@ -122,15 +107,15 @@ export class RouteConnectionHandler {
|
||||
|
||||
// Apply socket optimizations (apply to underlying socket)
|
||||
const underlyingSocket = wrappedSocket.socket;
|
||||
underlyingSocket.setNoDelay(this.settings.noDelay);
|
||||
underlyingSocket.setNoDelay(this.smartProxy.settings.noDelay);
|
||||
|
||||
// Apply keep-alive settings if enabled
|
||||
if (this.settings.keepAlive) {
|
||||
underlyingSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
||||
if (this.smartProxy.settings.keepAlive) {
|
||||
underlyingSocket.setKeepAlive(true, this.smartProxy.settings.keepAliveInitialDelay);
|
||||
record.hasKeepAlive = true;
|
||||
|
||||
// Apply enhanced TCP keep-alive options if enabled
|
||||
if (this.settings.enableKeepAliveProbes) {
|
||||
if (this.smartProxy.settings.enableKeepAliveProbes) {
|
||||
try {
|
||||
// These are platform-specific and may not be available
|
||||
if ('setKeepAliveProbes' in underlyingSocket) {
|
||||
@ -141,34 +126,34 @@ export class RouteConnectionHandler {
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors - these are optional enhancements
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('warn', `Enhanced TCP keep-alive settings not supported`, { connectionId, error: err, component: 'route-handler' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info',
|
||||
`New connection from ${remoteIP} on port ${localPort}. ` +
|
||||
`Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
|
||||
`Active connections: ${this.connectionManager.getConnectionCount()}`,
|
||||
`Active connections: ${this.smartProxy.connectionManager.getConnectionCount()}`,
|
||||
{
|
||||
connectionId,
|
||||
remoteIP,
|
||||
localPort,
|
||||
keepAlive: record.hasKeepAlive ? 'Enabled' : 'Disabled',
|
||||
activeConnections: this.connectionManager.getConnectionCount(),
|
||||
activeConnections: this.smartProxy.connectionManager.getConnectionCount(),
|
||||
component: 'route-handler'
|
||||
}
|
||||
);
|
||||
} else {
|
||||
logger.log('info',
|
||||
`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionManager.getConnectionCount()}`,
|
||||
`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.smartProxy.connectionManager.getConnectionCount()}`,
|
||||
{
|
||||
remoteIP,
|
||||
localPort,
|
||||
activeConnections: this.connectionManager.getConnectionCount(),
|
||||
activeConnections: this.smartProxy.connectionManager.getConnectionCount(),
|
||||
component: 'route-handler'
|
||||
}
|
||||
);
|
||||
@ -187,10 +172,10 @@ export class RouteConnectionHandler {
|
||||
let initialDataReceived = false;
|
||||
|
||||
// Check if any routes on this port require TLS handling
|
||||
const allRoutes = this.routeManager.getRoutes();
|
||||
const allRoutes = this.smartProxy.routeManager.getRoutes();
|
||||
const needsTlsHandling = allRoutes.some(route => {
|
||||
// Check if route matches this port
|
||||
const matchesPort = this.routeManager.getRoutesForPort(localPort).includes(route);
|
||||
const matchesPort = this.smartProxy.routeManager.getRoutesForPort(localPort).includes(route);
|
||||
|
||||
return matchesPort &&
|
||||
route.action.type === 'forward' &&
|
||||
@ -229,7 +214,7 @@ export class RouteConnectionHandler {
|
||||
}
|
||||
|
||||
// Always cleanup the connection record
|
||||
this.connectionManager.cleanupConnection(record, reason);
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, reason);
|
||||
},
|
||||
undefined, // Use default timeout handler
|
||||
'immediate-route-client'
|
||||
@ -244,9 +229,9 @@ export class RouteConnectionHandler {
|
||||
// Set an initial timeout for handshake data
|
||||
let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
|
||||
if (!initialDataReceived) {
|
||||
logger.log('warn', `No initial data received from ${record.remoteIP} after ${this.settings.initialDataTimeout}ms for connection ${connectionId}`, {
|
||||
logger.log('warn', `No initial data received from ${record.remoteIP} after ${this.smartProxy.settings.initialDataTimeout}ms for connection ${connectionId}`, {
|
||||
connectionId,
|
||||
timeout: this.settings.initialDataTimeout,
|
||||
timeout: this.smartProxy.settings.initialDataTimeout,
|
||||
remoteIP: record.remoteIP,
|
||||
component: 'route-handler'
|
||||
});
|
||||
@ -260,14 +245,14 @@ export class RouteConnectionHandler {
|
||||
});
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'initial_timeout';
|
||||
this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
|
||||
this.smartProxy.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
|
||||
}
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'initial_timeout');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'initial_timeout');
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
}, this.settings.initialDataTimeout!);
|
||||
}, this.smartProxy.settings.initialDataTimeout!);
|
||||
|
||||
// Make sure timeout doesn't keep the process alive
|
||||
if (initialTimeout.unref) {
|
||||
@ -275,7 +260,7 @@ export class RouteConnectionHandler {
|
||||
}
|
||||
|
||||
// Set up error handler
|
||||
socket.on('error', this.connectionManager.handleError('incoming', record));
|
||||
socket.on('error', this.smartProxy.connectionManager.handleError('incoming', record));
|
||||
|
||||
// Add close/end handlers to catch immediate disconnections
|
||||
socket.once('close', () => {
|
||||
@ -289,7 +274,7 @@ export class RouteConnectionHandler {
|
||||
clearTimeout(initialTimeout);
|
||||
initialTimeout = null;
|
||||
}
|
||||
this.connectionManager.cleanupConnection(record, 'closed_before_data');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'closed_before_data');
|
||||
}
|
||||
});
|
||||
|
||||
@ -311,7 +296,7 @@ export class RouteConnectionHandler {
|
||||
// Handler for processing initial data (after potential PROXY protocol)
|
||||
const processInitialData = (chunk: Buffer) => {
|
||||
// Block non-TLS connections on port 443
|
||||
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
|
||||
if (!this.smartProxy.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
|
||||
logger.log('warn', `Non-TLS connection ${connectionId} detected on port 443. Terminating connection - only TLS traffic is allowed on standard HTTPS port.`, {
|
||||
connectionId,
|
||||
message: 'Terminating connection - only TLS traffic is allowed on standard HTTPS port.',
|
||||
@ -319,20 +304,20 @@ export class RouteConnectionHandler {
|
||||
});
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'non_tls_blocked';
|
||||
this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
|
||||
this.smartProxy.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
|
||||
}
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'non_tls_blocked');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'non_tls_blocked');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this looks like a TLS handshake
|
||||
let serverName = '';
|
||||
if (this.tlsManager.isTlsHandshake(chunk)) {
|
||||
if (this.smartProxy.tlsManager.isTlsHandshake(chunk)) {
|
||||
record.isTLS = true;
|
||||
|
||||
// Check for ClientHello to extract SNI
|
||||
if (this.tlsManager.isClientHello(chunk)) {
|
||||
if (this.smartProxy.tlsManager.isClientHello(chunk)) {
|
||||
// Create connection info for SNI extraction
|
||||
const connInfo = {
|
||||
sourceIp: record.remoteIP,
|
||||
@ -342,20 +327,20 @@ export class RouteConnectionHandler {
|
||||
};
|
||||
|
||||
// Extract SNI
|
||||
serverName = this.tlsManager.extractSNI(chunk, connInfo) || '';
|
||||
serverName = this.smartProxy.tlsManager.extractSNI(chunk, connInfo) || '';
|
||||
|
||||
// Lock the connection to the negotiated SNI
|
||||
record.lockedDomain = serverName;
|
||||
|
||||
// Check if we should reject connections without SNI
|
||||
if (!serverName && this.settings.allowSessionTicket === false) {
|
||||
if (!serverName && this.smartProxy.settings.allowSessionTicket === false) {
|
||||
logger.log('warn', `No SNI detected in TLS ClientHello for connection ${connectionId}; sending TLS alert`, {
|
||||
connectionId,
|
||||
component: 'route-handler'
|
||||
});
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
||||
this.connectionManager.incrementTerminationStat(
|
||||
this.smartProxy.connectionManager.incrementTerminationStat(
|
||||
'incoming',
|
||||
'session_ticket_blocked_no_sni'
|
||||
);
|
||||
@ -369,11 +354,11 @@ export class RouteConnectionHandler {
|
||||
} catch {
|
||||
socket.end();
|
||||
}
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `TLS connection with SNI`, {
|
||||
connectionId,
|
||||
serverName: serverName || '(empty)',
|
||||
@ -399,7 +384,7 @@ export class RouteConnectionHandler {
|
||||
record.hasReceivedInitialData = true;
|
||||
|
||||
// Check if this is from a trusted proxy and might have PROXY protocol
|
||||
if (this.settings.proxyIPs?.includes(socket.remoteAddress || '') && this.settings.acceptProxyProtocol !== false) {
|
||||
if (this.smartProxy.settings.proxyIPs?.includes(socket.remoteAddress || '') && this.smartProxy.settings.acceptProxyProtocol !== false) {
|
||||
// Check if this starts with PROXY protocol
|
||||
if (chunk.toString('ascii', 0, Math.min(6, chunk.length)).startsWith('PROXY ')) {
|
||||
try {
|
||||
@ -463,7 +448,7 @@ export class RouteConnectionHandler {
|
||||
const remoteIP = record.remoteIP;
|
||||
|
||||
// Check if this is an HTTP proxy port
|
||||
const isHttpProxyPort = this.settings.useHttpProxy?.includes(localPort);
|
||||
const isHttpProxyPort = this.smartProxy.settings.useHttpProxy?.includes(localPort);
|
||||
|
||||
// For HTTP proxy ports without TLS, skip domain check since domain info comes from HTTP headers
|
||||
const skipDomainCheck = isHttpProxyPort && !record.isTLS;
|
||||
@ -482,7 +467,7 @@ export class RouteConnectionHandler {
|
||||
};
|
||||
|
||||
// Find matching route
|
||||
const routeMatch = this.routeManager.findMatchingRoute(routeContext);
|
||||
const routeMatch = this.smartProxy.routeManager.findMatchingRoute(routeContext);
|
||||
|
||||
if (!routeMatch) {
|
||||
logger.log('warn', `No route found for ${serverName || 'connection'} on port ${localPort} (connection: ${connectionId})`, {
|
||||
@ -499,10 +484,10 @@ export class RouteConnectionHandler {
|
||||
});
|
||||
|
||||
// Check default security settings
|
||||
const defaultSecuritySettings = this.settings.defaults?.security;
|
||||
const defaultSecuritySettings = this.smartProxy.settings.defaults?.security;
|
||||
if (defaultSecuritySettings) {
|
||||
if (defaultSecuritySettings.ipAllowList && defaultSecuritySettings.ipAllowList.length > 0) {
|
||||
const isAllowed = this.securityManager.isIPAuthorized(
|
||||
const isAllowed = this.smartProxy.securityManager.isIPAuthorized(
|
||||
remoteIP,
|
||||
defaultSecuritySettings.ipAllowList,
|
||||
defaultSecuritySettings.ipBlockList || []
|
||||
@ -515,17 +500,17 @@ export class RouteConnectionHandler {
|
||||
component: 'route-handler'
|
||||
});
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'ip_blocked');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'ip_blocked');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup direct connection with default settings
|
||||
if (this.settings.defaults?.target) {
|
||||
if (this.smartProxy.settings.defaults?.target) {
|
||||
// Use defaults from configuration
|
||||
const targetHost = this.settings.defaults.target.host;
|
||||
const targetPort = this.settings.defaults.target.port;
|
||||
const targetHost = this.smartProxy.settings.defaults.target.host;
|
||||
const targetPort = this.smartProxy.settings.defaults.target.port;
|
||||
|
||||
return this.setupDirectConnection(
|
||||
socket,
|
||||
@ -543,7 +528,7 @@ export class RouteConnectionHandler {
|
||||
component: 'route-handler'
|
||||
});
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'no_default_target');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'no_default_target');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -551,7 +536,7 @@ export class RouteConnectionHandler {
|
||||
// A matching route was found
|
||||
const route = routeMatch.route;
|
||||
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `Route matched`, {
|
||||
connectionId,
|
||||
routeName: route.name || 'unnamed',
|
||||
@ -565,7 +550,7 @@ export class RouteConnectionHandler {
|
||||
if (route.security) {
|
||||
// Check IP allow/block lists
|
||||
if (route.security.ipAllowList || route.security.ipBlockList) {
|
||||
const isIPAllowed = this.securityManager.isIPAuthorized(
|
||||
const isIPAllowed = this.smartProxy.securityManager.isIPAuthorized(
|
||||
remoteIP,
|
||||
route.security.ipAllowList || [],
|
||||
route.security.ipBlockList || []
|
||||
@ -579,7 +564,7 @@ export class RouteConnectionHandler {
|
||||
component: 'route-handler'
|
||||
});
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'route_ip_blocked');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'route_ip_blocked');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -588,7 +573,7 @@ export class RouteConnectionHandler {
|
||||
if (route.security.maxConnections !== undefined) {
|
||||
// TODO: Implement per-route connection tracking
|
||||
// For now, log that this feature is not yet implemented
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('warn', `Route ${route.name} has maxConnections=${route.security.maxConnections} configured but per-route connection limits are not yet implemented`, {
|
||||
connectionId,
|
||||
routeName: route.name,
|
||||
@ -633,7 +618,7 @@ export class RouteConnectionHandler {
|
||||
component: 'route-handler'
|
||||
});
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'unknown_action');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'unknown_action');
|
||||
}
|
||||
}
|
||||
|
||||
@ -658,7 +643,7 @@ export class RouteConnectionHandler {
|
||||
// The application should NOT interfere with these connections
|
||||
|
||||
// Log the connection for monitoring purposes
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `NFTables forwarding (kernel-level)`, {
|
||||
connectionId: record.id,
|
||||
source: `${record.remoteIP}:${socket.remotePort}`,
|
||||
@ -680,7 +665,7 @@ export class RouteConnectionHandler {
|
||||
// Additional NFTables-specific logging if configured
|
||||
if (action.nftables) {
|
||||
const nftConfig = action.nftables;
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `NFTables config`, {
|
||||
connectionId: record.id,
|
||||
protocol: nftConfig.protocol || 'tcp',
|
||||
@ -701,7 +686,7 @@ export class RouteConnectionHandler {
|
||||
|
||||
// Set up cleanup when the socket eventually closes
|
||||
socket.once('close', () => {
|
||||
this.connectionManager.cleanupConnection(record, 'nftables_closed');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'nftables_closed');
|
||||
});
|
||||
|
||||
return;
|
||||
@ -714,7 +699,7 @@ export class RouteConnectionHandler {
|
||||
component: 'route-handler'
|
||||
});
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'missing_target');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'missing_target');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -738,7 +723,7 @@ export class RouteConnectionHandler {
|
||||
if (typeof action.target.host === 'function') {
|
||||
try {
|
||||
targetHost = action.target.host(routeContext);
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `Dynamic host resolved to ${Array.isArray(targetHost) ? targetHost.join(', ') : targetHost} for connection ${connectionId}`, {
|
||||
connectionId,
|
||||
targetHost: Array.isArray(targetHost) ? targetHost.join(', ') : targetHost,
|
||||
@ -752,7 +737,7 @@ export class RouteConnectionHandler {
|
||||
component: 'route-handler'
|
||||
});
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'host_mapping_error');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'host_mapping_error');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@ -769,7 +754,7 @@ export class RouteConnectionHandler {
|
||||
if (typeof action.target.port === 'function') {
|
||||
try {
|
||||
targetPort = action.target.port(routeContext);
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `Dynamic port mapping from ${record.localPort} to ${targetPort} for connection ${connectionId}`, {
|
||||
connectionId,
|
||||
sourcePort: record.localPort,
|
||||
@ -786,7 +771,7 @@ export class RouteConnectionHandler {
|
||||
component: 'route-handler'
|
||||
});
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'port_mapping_error');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'port_mapping_error');
|
||||
return;
|
||||
}
|
||||
} else if (action.target.port === 'preserve') {
|
||||
@ -805,7 +790,7 @@ export class RouteConnectionHandler {
|
||||
switch (action.tls.mode) {
|
||||
case 'passthrough':
|
||||
// For TLS passthrough, just forward directly
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `Using TLS passthrough to ${selectedHost}:${targetPort} for connection ${connectionId}`, {
|
||||
connectionId,
|
||||
targetHost: selectedHost,
|
||||
@ -827,8 +812,8 @@ export class RouteConnectionHandler {
|
||||
case 'terminate':
|
||||
case 'terminate-and-reencrypt':
|
||||
// For TLS termination, use HttpProxy
|
||||
if (this.httpProxyBridge.getHttpProxy()) {
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.httpProxyBridge.getHttpProxy()) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `Using HttpProxy for TLS termination to ${Array.isArray(action.target.host) ? action.target.host.join(', ') : action.target.host} for connection ${connectionId}`, {
|
||||
connectionId,
|
||||
targetHost: action.target.host,
|
||||
@ -838,13 +823,13 @@ export class RouteConnectionHandler {
|
||||
|
||||
// If we have an initial chunk with TLS data, start processing it
|
||||
if (initialChunk && record.isTLS) {
|
||||
this.httpProxyBridge.forwardToHttpProxy(
|
||||
this.smartProxy.httpProxyBridge.forwardToHttpProxy(
|
||||
connectionId,
|
||||
socket,
|
||||
record,
|
||||
initialChunk,
|
||||
this.settings.httpProxyPort || 8443,
|
||||
(reason) => this.connectionManager.cleanupConnection(record, reason)
|
||||
this.smartProxy.settings.httpProxyPort || 8443,
|
||||
(reason) => this.smartProxy.connectionManager.cleanupConnection(record, reason)
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -855,7 +840,7 @@ export class RouteConnectionHandler {
|
||||
component: 'route-handler'
|
||||
});
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'tls_error');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'tls_error');
|
||||
return;
|
||||
} else {
|
||||
logger.log('error', `HttpProxy not available for TLS termination for connection ${connectionId}`, {
|
||||
@ -863,29 +848,29 @@ export class RouteConnectionHandler {
|
||||
component: 'route-handler'
|
||||
});
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'no_http_proxy');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'no_http_proxy');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No TLS settings - check if this port should use HttpProxy
|
||||
const isHttpProxyPort = this.settings.useHttpProxy?.includes(record.localPort);
|
||||
const isHttpProxyPort = this.smartProxy.settings.useHttpProxy?.includes(record.localPort);
|
||||
|
||||
// Debug logging
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
logger.log('debug', `Checking HttpProxy forwarding: port=${record.localPort}, useHttpProxy=${JSON.stringify(this.settings.useHttpProxy)}, isHttpProxyPort=${isHttpProxyPort}, hasHttpProxy=${!!this.httpProxyBridge.getHttpProxy()}`, {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('debug', `Checking HttpProxy forwarding: port=${record.localPort}, useHttpProxy=${JSON.stringify(this.smartProxy.settings.useHttpProxy)}, isHttpProxyPort=${isHttpProxyPort}, hasHttpProxy=${!!this.smartProxy.httpProxyBridge.getHttpProxy()}`, {
|
||||
connectionId,
|
||||
localPort: record.localPort,
|
||||
useHttpProxy: this.settings.useHttpProxy,
|
||||
useHttpProxy: this.smartProxy.settings.useHttpProxy,
|
||||
isHttpProxyPort,
|
||||
hasHttpProxy: !!this.httpProxyBridge.getHttpProxy(),
|
||||
hasHttpProxy: !!this.smartProxy.httpProxyBridge.getHttpProxy(),
|
||||
component: 'route-handler'
|
||||
});
|
||||
}
|
||||
|
||||
if (isHttpProxyPort && this.httpProxyBridge.getHttpProxy()) {
|
||||
if (isHttpProxyPort && this.smartProxy.httpProxyBridge.getHttpProxy()) {
|
||||
// Forward non-TLS connections to HttpProxy if configured
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `Using HttpProxy for non-TLS connection ${connectionId} on port ${record.localPort}`, {
|
||||
connectionId,
|
||||
port: record.localPort,
|
||||
@ -893,18 +878,18 @@ export class RouteConnectionHandler {
|
||||
});
|
||||
}
|
||||
|
||||
this.httpProxyBridge.forwardToHttpProxy(
|
||||
this.smartProxy.httpProxyBridge.forwardToHttpProxy(
|
||||
connectionId,
|
||||
socket,
|
||||
record,
|
||||
initialChunk,
|
||||
this.settings.httpProxyPort || 8443,
|
||||
(reason) => this.connectionManager.cleanupConnection(record, reason)
|
||||
this.smartProxy.settings.httpProxyPort || 8443,
|
||||
(reason) => this.smartProxy.connectionManager.cleanupConnection(record, reason)
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
// Basic forwarding
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `Using basic forwarding to ${Array.isArray(action.target.host) ? action.target.host.join(', ') : action.target.host}:${action.target.port} for connection ${connectionId}`, {
|
||||
connectionId,
|
||||
targetHost: action.target.host,
|
||||
@ -977,7 +962,7 @@ export class RouteConnectionHandler {
|
||||
component: 'route-handler'
|
||||
});
|
||||
socket.destroy();
|
||||
this.connectionManager.cleanupConnection(record, 'missing_handler');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'missing_handler');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1052,7 +1037,7 @@ export class RouteConnectionHandler {
|
||||
if (!socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
this.connectionManager.cleanupConnection(record, 'handler_error');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'handler_error');
|
||||
});
|
||||
} else {
|
||||
// For sync handlers, emit on next tick
|
||||
@ -1074,7 +1059,7 @@ export class RouteConnectionHandler {
|
||||
if (!socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
this.connectionManager.cleanupConnection(record, 'handler_error');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'handler_error');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1095,19 +1080,19 @@ export class RouteConnectionHandler {
|
||||
|
||||
// Determine target host and port if not provided
|
||||
const finalTargetHost =
|
||||
targetHost || record.targetHost || this.settings.defaults?.target?.host || 'localhost';
|
||||
targetHost || record.targetHost || this.smartProxy.settings.defaults?.target?.host || 'localhost';
|
||||
|
||||
// Determine target port
|
||||
const finalTargetPort =
|
||||
targetPort ||
|
||||
record.targetPort ||
|
||||
(overridePort !== undefined ? overridePort : this.settings.defaults?.target?.port || 443);
|
||||
(overridePort !== undefined ? overridePort : this.smartProxy.settings.defaults?.target?.port || 443);
|
||||
|
||||
// Update record with final target information
|
||||
record.targetHost = finalTargetHost;
|
||||
record.targetPort = finalTargetPort;
|
||||
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `Setting up direct connection ${connectionId} to ${finalTargetHost}:${finalTargetPort}`, {
|
||||
connectionId,
|
||||
targetHost: finalTargetHost,
|
||||
@ -1123,7 +1108,7 @@ export class RouteConnectionHandler {
|
||||
};
|
||||
|
||||
// Preserve source IP if configured
|
||||
if (this.settings.defaults?.preserveSourceIP || this.settings.preserveSourceIP) {
|
||||
if (this.smartProxy.settings.defaults?.preserveSourceIP || this.smartProxy.settings.preserveSourceIP) {
|
||||
connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
|
||||
}
|
||||
|
||||
@ -1132,13 +1117,18 @@ export class RouteConnectionHandler {
|
||||
record.bytesReceived += initialChunk.length;
|
||||
record.pendingData.push(Buffer.from(initialChunk));
|
||||
record.pendingDataSize = initialChunk.length;
|
||||
|
||||
// Record bytes for metrics
|
||||
if (this.smartProxy.metricsCollector) {
|
||||
this.smartProxy.metricsCollector.recordBytes(record.id, initialChunk.length, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the target socket with immediate error handling
|
||||
const targetSocket = createSocketWithErrorHandler({
|
||||
port: finalTargetPort,
|
||||
host: finalTargetHost,
|
||||
timeout: this.settings.connectionTimeout || 30000, // Connection timeout (default: 30s)
|
||||
timeout: this.smartProxy.settings.connectionTimeout || 30000, // Connection timeout (default: 30s)
|
||||
onError: (error) => {
|
||||
// Connection failed - clean up everything immediately
|
||||
// Check if connection record is still valid (client might have disconnected)
|
||||
@ -1188,10 +1178,10 @@ export class RouteConnectionHandler {
|
||||
}
|
||||
|
||||
// Clean up the connection record - this is critical!
|
||||
this.connectionManager.cleanupConnection(record, `connection_failed_${(error as any).code || 'unknown'}`);
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, `connection_failed_${(error as any).code || 'unknown'}`);
|
||||
},
|
||||
onConnect: async () => {
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `Connection ${connectionId} established to target ${finalTargetHost}:${finalTargetPort}`, {
|
||||
connectionId,
|
||||
targetHost: finalTargetHost,
|
||||
@ -1204,11 +1194,11 @@ export class RouteConnectionHandler {
|
||||
targetSocket.removeAllListeners('error');
|
||||
|
||||
// Add the normal error handler for established connections
|
||||
targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
|
||||
targetSocket.on('error', this.smartProxy.connectionManager.handleError('outgoing', record));
|
||||
|
||||
// Check if we should send PROXY protocol header
|
||||
const shouldSendProxyProtocol = record.routeConfig?.action?.sendProxyProtocol ||
|
||||
this.settings.sendProxyProtocol;
|
||||
this.smartProxy.settings.sendProxyProtocol;
|
||||
|
||||
if (shouldSendProxyProtocol) {
|
||||
try {
|
||||
@ -1260,7 +1250,7 @@ export class RouteConnectionHandler {
|
||||
if (record.pendingData.length > 0) {
|
||||
const combinedData = Buffer.concat(record.pendingData);
|
||||
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`
|
||||
);
|
||||
@ -1274,7 +1264,7 @@ export class RouteConnectionHandler {
|
||||
error: err.message,
|
||||
component: 'route-handler'
|
||||
});
|
||||
return this.connectionManager.cleanupConnection(record, 'write_error');
|
||||
return this.smartProxy.connectionManager.cleanupConnection(record, 'write_error');
|
||||
}
|
||||
});
|
||||
|
||||
@ -1290,22 +1280,32 @@ export class RouteConnectionHandler {
|
||||
setupBidirectionalForwarding(incomingSocket, targetSocket, {
|
||||
onClientData: (chunk) => {
|
||||
record.bytesReceived += chunk.length;
|
||||
this.timeoutManager.updateActivity(record);
|
||||
this.smartProxy.timeoutManager.updateActivity(record);
|
||||
|
||||
// Record bytes for metrics
|
||||
if (this.smartProxy.metricsCollector) {
|
||||
this.smartProxy.metricsCollector.recordBytes(record.id, chunk.length, 0);
|
||||
}
|
||||
},
|
||||
onServerData: (chunk) => {
|
||||
record.bytesSent += chunk.length;
|
||||
this.timeoutManager.updateActivity(record);
|
||||
this.smartProxy.timeoutManager.updateActivity(record);
|
||||
|
||||
// Record bytes for metrics
|
||||
if (this.smartProxy.metricsCollector) {
|
||||
this.smartProxy.metricsCollector.recordBytes(record.id, 0, chunk.length);
|
||||
}
|
||||
},
|
||||
onCleanup: (reason) => {
|
||||
this.connectionManager.cleanupConnection(record, reason);
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, reason);
|
||||
},
|
||||
enableHalfOpen: false // Default: close both when one closes (required for proxy chains)
|
||||
});
|
||||
|
||||
// Apply timeouts if keep-alive is enabled
|
||||
if (record.hasKeepAlive) {
|
||||
socket.setTimeout(this.settings.socketTimeout || 3600000);
|
||||
targetSocket.setTimeout(this.settings.socketTimeout || 3600000);
|
||||
socket.setTimeout(this.smartProxy.settings.socketTimeout || 3600000);
|
||||
targetSocket.setTimeout(this.smartProxy.settings.socketTimeout || 3600000);
|
||||
}
|
||||
|
||||
// Log successful connection
|
||||
@ -1333,11 +1333,11 @@ export class RouteConnectionHandler {
|
||||
};
|
||||
|
||||
// Create a renegotiation handler function
|
||||
const renegotiationHandler = this.tlsManager.createRenegotiationHandler(
|
||||
const renegotiationHandler = this.smartProxy.tlsManager.createRenegotiationHandler(
|
||||
connectionId,
|
||||
serverName,
|
||||
connInfo,
|
||||
(_connectionId, reason) => this.connectionManager.cleanupConnection(record, reason)
|
||||
(_connectionId, reason) => this.smartProxy.connectionManager.cleanupConnection(record, reason)
|
||||
);
|
||||
|
||||
// Store the handler in the connection record so we can remove it during cleanup
|
||||
@ -1346,7 +1346,7 @@ export class RouteConnectionHandler {
|
||||
// Add the handler to the socket
|
||||
socket.on('data', renegotiationHandler);
|
||||
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `TLS renegotiation handler installed for connection ${connectionId} with SNI ${serverName}`, {
|
||||
connectionId,
|
||||
serverName,
|
||||
@ -1356,13 +1356,13 @@ export class RouteConnectionHandler {
|
||||
}
|
||||
|
||||
// Set connection timeout
|
||||
record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
|
||||
record.cleanupTimer = this.smartProxy.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
|
||||
logger.log('warn', `Connection ${connectionId} from ${record.remoteIP} exceeded max lifetime, forcing cleanup`, {
|
||||
connectionId,
|
||||
remoteIP: record.remoteIP,
|
||||
component: 'route-handler'
|
||||
});
|
||||
this.connectionManager.cleanupConnection(record, reason);
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, reason);
|
||||
});
|
||||
|
||||
// Mark TLS handshake as complete for TLS connections
|
||||
@ -1377,14 +1377,14 @@ export class RouteConnectionHandler {
|
||||
record.outgoingStartTime = Date.now();
|
||||
|
||||
// Apply socket optimizations
|
||||
targetSocket.setNoDelay(this.settings.noDelay);
|
||||
targetSocket.setNoDelay(this.smartProxy.settings.noDelay);
|
||||
|
||||
// Apply keep-alive settings if enabled
|
||||
if (this.settings.keepAlive) {
|
||||
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
||||
if (this.smartProxy.settings.keepAlive) {
|
||||
targetSocket.setKeepAlive(true, this.smartProxy.settings.keepAliveInitialDelay);
|
||||
|
||||
// Apply enhanced TCP keep-alive options if enabled
|
||||
if (this.settings.enableKeepAliveProbes) {
|
||||
if (this.smartProxy.settings.enableKeepAliveProbes) {
|
||||
try {
|
||||
if ('setKeepAliveProbes' in targetSocket) {
|
||||
(targetSocket as any).setKeepAliveProbes(10);
|
||||
@ -1394,7 +1394,7 @@ export class RouteConnectionHandler {
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors - these are optional enhancements
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('warn', `Enhanced TCP keep-alive not supported for outgoing socket on connection ${connectionId}: ${err}`, {
|
||||
connectionId,
|
||||
error: err,
|
||||
@ -1406,16 +1406,16 @@ export class RouteConnectionHandler {
|
||||
}
|
||||
|
||||
// Setup error handlers for incoming socket
|
||||
socket.on('error', this.connectionManager.handleError('incoming', record));
|
||||
socket.on('error', this.smartProxy.connectionManager.handleError('incoming', record));
|
||||
|
||||
// Handle timeouts with keep-alive awareness
|
||||
socket.on('timeout', () => {
|
||||
// For keep-alive connections, just log a warning instead of closing
|
||||
if (record.hasKeepAlive) {
|
||||
logger.log('warn', `Timeout event on incoming keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}. Connection preserved.`, {
|
||||
logger.log('warn', `Timeout event on incoming keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000)}. Connection preserved.`, {
|
||||
connectionId,
|
||||
remoteIP: record.remoteIP,
|
||||
timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000),
|
||||
timeout: plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000),
|
||||
status: 'Connection preserved',
|
||||
component: 'route-handler'
|
||||
});
|
||||
@ -1423,26 +1423,26 @@ export class RouteConnectionHandler {
|
||||
}
|
||||
|
||||
// For non-keep-alive connections, proceed with normal cleanup
|
||||
logger.log('warn', `Timeout on incoming side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`, {
|
||||
logger.log('warn', `Timeout on incoming side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000)}`, {
|
||||
connectionId,
|
||||
remoteIP: record.remoteIP,
|
||||
timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000),
|
||||
timeout: plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000),
|
||||
component: 'route-handler'
|
||||
});
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'timeout';
|
||||
this.connectionManager.incrementTerminationStat('incoming', 'timeout');
|
||||
this.smartProxy.connectionManager.incrementTerminationStat('incoming', 'timeout');
|
||||
}
|
||||
this.connectionManager.cleanupConnection(record, 'timeout_incoming');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'timeout_incoming');
|
||||
});
|
||||
|
||||
targetSocket.on('timeout', () => {
|
||||
// For keep-alive connections, just log a warning instead of closing
|
||||
if (record.hasKeepAlive) {
|
||||
logger.log('warn', `Timeout event on outgoing keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}. Connection preserved.`, {
|
||||
logger.log('warn', `Timeout event on outgoing keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000)}. Connection preserved.`, {
|
||||
connectionId,
|
||||
remoteIP: record.remoteIP,
|
||||
timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000),
|
||||
timeout: plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000),
|
||||
status: 'Connection preserved',
|
||||
component: 'route-handler'
|
||||
});
|
||||
@ -1450,20 +1450,20 @@ export class RouteConnectionHandler {
|
||||
}
|
||||
|
||||
// For non-keep-alive connections, proceed with normal cleanup
|
||||
logger.log('warn', `Timeout on outgoing side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`, {
|
||||
logger.log('warn', `Timeout on outgoing side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000)}`, {
|
||||
connectionId,
|
||||
remoteIP: record.remoteIP,
|
||||
timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000),
|
||||
timeout: plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000),
|
||||
component: 'route-handler'
|
||||
});
|
||||
if (record.outgoingTerminationReason === null) {
|
||||
record.outgoingTerminationReason = 'timeout';
|
||||
this.connectionManager.incrementTerminationStat('outgoing', 'timeout');
|
||||
this.smartProxy.connectionManager.incrementTerminationStat('outgoing', 'timeout');
|
||||
}
|
||||
this.connectionManager.cleanupConnection(record, 'timeout_outgoing');
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'timeout_outgoing');
|
||||
});
|
||||
|
||||
// Apply socket timeouts
|
||||
this.timeoutManager.applySocketTimeouts(record);
|
||||
this.smartProxy.timeoutManager.applySocketTimeouts(record);
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { ISmartProxyOptions } from './models/interfaces.js';
|
||||
import type { SmartProxy } from './smart-proxy.js';
|
||||
|
||||
/**
|
||||
* Handles security aspects like IP tracking, rate limiting, and authorization
|
||||
@ -8,7 +8,7 @@ export class SecurityManager {
|
||||
private connectionsByIP: Map<string, Set<string>> = new Map();
|
||||
private connectionRateByIP: Map<string, number[]> = new Map();
|
||||
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
constructor(private smartProxy: SmartProxy) {}
|
||||
|
||||
/**
|
||||
* Get connections count by IP
|
||||
@ -36,7 +36,7 @@ export class SecurityManager {
|
||||
this.connectionRateByIP.set(ip, timestamps);
|
||||
|
||||
// Check if rate exceeds limit
|
||||
return timestamps.length <= this.settings.connectionRateLimitPerMinute!;
|
||||
return timestamps.length <= this.smartProxy.settings.connectionRateLimitPerMinute!;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -137,23 +137,23 @@ export class SecurityManager {
|
||||
public validateIP(ip: string): { allowed: boolean; reason?: string } {
|
||||
// Check connection count limit
|
||||
if (
|
||||
this.settings.maxConnectionsPerIP &&
|
||||
this.getConnectionCountByIP(ip) >= this.settings.maxConnectionsPerIP
|
||||
this.smartProxy.settings.maxConnectionsPerIP &&
|
||||
this.getConnectionCountByIP(ip) >= this.smartProxy.settings.maxConnectionsPerIP
|
||||
) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded`
|
||||
reason: `Maximum connections per IP (${this.smartProxy.settings.maxConnectionsPerIP}) exceeded`
|
||||
};
|
||||
}
|
||||
|
||||
// Check connection rate limit
|
||||
if (
|
||||
this.settings.connectionRateLimitPerMinute &&
|
||||
this.smartProxy.settings.connectionRateLimitPerMinute &&
|
||||
!this.checkConnectionRate(ip)
|
||||
) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded`
|
||||
reason: `Connection rate limit (${this.smartProxy.settings.connectionRateLimitPerMinute}/min) exceeded`
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ import { AcmeStateManager } from './acme-state-manager.js';
|
||||
|
||||
// Import metrics collector
|
||||
import { MetricsCollector } from './metrics-collector.js';
|
||||
import type { IProxyStats } from './models/metrics-types.js';
|
||||
import type { IMetrics } from './models/metrics-types.js';
|
||||
|
||||
/**
|
||||
* SmartProxy - Pure route-based API
|
||||
@ -52,24 +52,24 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
|
||||
// Component managers
|
||||
public connectionManager: ConnectionManager;
|
||||
private securityManager: SecurityManager;
|
||||
private tlsManager: TlsManager;
|
||||
private httpProxyBridge: HttpProxyBridge;
|
||||
private timeoutManager: TimeoutManager;
|
||||
public routeManager: RouteManager; // Made public for route management
|
||||
public routeConnectionHandler: RouteConnectionHandler; // Made public for metrics
|
||||
private nftablesManager: NFTablesManager;
|
||||
public securityManager: SecurityManager;
|
||||
public tlsManager: TlsManager;
|
||||
public httpProxyBridge: HttpProxyBridge;
|
||||
public timeoutManager: TimeoutManager;
|
||||
public routeManager: RouteManager;
|
||||
public routeConnectionHandler: RouteConnectionHandler;
|
||||
public nftablesManager: NFTablesManager;
|
||||
|
||||
// Certificate manager for ACME and static certificates
|
||||
private certManager: SmartCertManager | null = null;
|
||||
public certManager: SmartCertManager | null = null;
|
||||
|
||||
// Global challenge route tracking
|
||||
private globalChallengeRouteActive: boolean = false;
|
||||
private routeUpdateLock: any = null; // Will be initialized as AsyncMutex
|
||||
private acmeStateManager: AcmeStateManager;
|
||||
public acmeStateManager: AcmeStateManager;
|
||||
|
||||
// Metrics collector
|
||||
private metricsCollector: MetricsCollector;
|
||||
public metricsCollector: MetricsCollector;
|
||||
|
||||
// Track port usage across route updates
|
||||
private portUsageMap: Map<number, Set<string>> = new Map();
|
||||
@ -161,13 +161,9 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
}
|
||||
|
||||
// Initialize component managers
|
||||
this.timeoutManager = new TimeoutManager(this.settings);
|
||||
this.securityManager = new SecurityManager(this.settings);
|
||||
this.connectionManager = new ConnectionManager(
|
||||
this.settings,
|
||||
this.securityManager,
|
||||
this.timeoutManager
|
||||
);
|
||||
this.timeoutManager = new TimeoutManager(this);
|
||||
this.securityManager = new SecurityManager(this);
|
||||
this.connectionManager = new ConnectionManager(this);
|
||||
|
||||
// Create the route manager with SharedRouteManager API
|
||||
// Create a logger adapter to match ILogger interface
|
||||
@ -186,25 +182,17 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
|
||||
|
||||
// Create other required components
|
||||
this.tlsManager = new TlsManager(this.settings);
|
||||
this.httpProxyBridge = new HttpProxyBridge(this.settings);
|
||||
this.tlsManager = new TlsManager(this);
|
||||
this.httpProxyBridge = new HttpProxyBridge(this);
|
||||
|
||||
// Initialize connection handler with route support
|
||||
this.routeConnectionHandler = new RouteConnectionHandler(
|
||||
this.settings,
|
||||
this.connectionManager,
|
||||
this.securityManager,
|
||||
this.tlsManager,
|
||||
this.httpProxyBridge,
|
||||
this.timeoutManager,
|
||||
this.routeManager
|
||||
);
|
||||
this.routeConnectionHandler = new RouteConnectionHandler(this);
|
||||
|
||||
// Initialize port manager
|
||||
this.portManager = new PortManager(this.settings, this.routeConnectionHandler);
|
||||
this.portManager = new PortManager(this);
|
||||
|
||||
// Initialize NFTablesManager
|
||||
this.nftablesManager = new NFTablesManager(this.settings);
|
||||
this.nftablesManager = new NFTablesManager(this);
|
||||
|
||||
// Initialize route update mutex for synchronization
|
||||
this.routeUpdateLock = new Mutex();
|
||||
@ -922,11 +910,11 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get proxy statistics and metrics
|
||||
* Get proxy metrics with clean API
|
||||
*
|
||||
* @returns IProxyStats interface with various metrics methods
|
||||
* @returns IMetrics interface with grouped metrics methods
|
||||
*/
|
||||
public getStats(): IProxyStats {
|
||||
public getMetrics(): IMetrics {
|
||||
return this.metricsCollector;
|
||||
}
|
||||
|
||||
|
144
ts/proxies/smart-proxy/throughput-tracker.ts
Normal file
144
ts/proxies/smart-proxy/throughput-tracker.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import type { IThroughputSample, IThroughputData, IThroughputHistoryPoint } from './models/metrics-types.js';
|
||||
|
||||
/**
|
||||
* Tracks throughput data using time-series sampling
|
||||
*/
|
||||
export class ThroughputTracker {
|
||||
private samples: IThroughputSample[] = [];
|
||||
private readonly maxSamples: number;
|
||||
private accumulatedBytesIn: number = 0;
|
||||
private accumulatedBytesOut: number = 0;
|
||||
private lastSampleTime: number = 0;
|
||||
|
||||
constructor(retentionSeconds: number = 3600) {
|
||||
// Keep samples for the retention period at 1 sample per second
|
||||
this.maxSamples = retentionSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record bytes transferred (called on every data transfer)
|
||||
*/
|
||||
public recordBytes(bytesIn: number, bytesOut: number): void {
|
||||
this.accumulatedBytesIn += bytesIn;
|
||||
this.accumulatedBytesOut += bytesOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a sample of accumulated bytes (called every second)
|
||||
*/
|
||||
public takeSample(): void {
|
||||
const now = Date.now();
|
||||
|
||||
// Record accumulated bytes since last sample
|
||||
this.samples.push({
|
||||
timestamp: now,
|
||||
bytesIn: this.accumulatedBytesIn,
|
||||
bytesOut: this.accumulatedBytesOut
|
||||
});
|
||||
|
||||
// Reset accumulators
|
||||
this.accumulatedBytesIn = 0;
|
||||
this.accumulatedBytesOut = 0;
|
||||
this.lastSampleTime = now;
|
||||
|
||||
// Maintain circular buffer - remove oldest samples
|
||||
if (this.samples.length > this.maxSamples) {
|
||||
this.samples.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get throughput rate over specified window (bytes per second)
|
||||
*/
|
||||
public getRate(windowSeconds: number): IThroughputData {
|
||||
if (this.samples.length === 0) {
|
||||
return { in: 0, out: 0 };
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const windowStart = now - (windowSeconds * 1000);
|
||||
|
||||
// Find samples within the window
|
||||
const relevantSamples = this.samples.filter(s => s.timestamp > windowStart);
|
||||
|
||||
if (relevantSamples.length === 0) {
|
||||
return { in: 0, out: 0 };
|
||||
}
|
||||
|
||||
// Sum bytes in the window
|
||||
const totalBytesIn = relevantSamples.reduce((sum, s) => sum + s.bytesIn, 0);
|
||||
const totalBytesOut = relevantSamples.reduce((sum, s) => sum + s.bytesOut, 0);
|
||||
|
||||
// Calculate actual window duration (might be less than requested if not enough data)
|
||||
const actualWindowSeconds = Math.min(
|
||||
windowSeconds,
|
||||
(now - relevantSamples[0].timestamp) / 1000
|
||||
);
|
||||
|
||||
// Avoid division by zero
|
||||
if (actualWindowSeconds === 0) {
|
||||
return { in: 0, out: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
in: Math.round(totalBytesIn / actualWindowSeconds),
|
||||
out: Math.round(totalBytesOut / actualWindowSeconds)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get throughput history for specified duration
|
||||
*/
|
||||
public getHistory(durationSeconds: number): IThroughputHistoryPoint[] {
|
||||
const now = Date.now();
|
||||
const startTime = now - (durationSeconds * 1000);
|
||||
|
||||
// Filter samples within duration
|
||||
const relevantSamples = this.samples.filter(s => s.timestamp > startTime);
|
||||
|
||||
// Convert to history points with per-second rates
|
||||
const history: IThroughputHistoryPoint[] = [];
|
||||
|
||||
for (let i = 0; i < relevantSamples.length; i++) {
|
||||
const sample = relevantSamples[i];
|
||||
|
||||
// For the first sample or samples after gaps, we can't calculate rate
|
||||
if (i === 0 || sample.timestamp - relevantSamples[i - 1].timestamp > 2000) {
|
||||
history.push({
|
||||
timestamp: sample.timestamp,
|
||||
in: sample.bytesIn,
|
||||
out: sample.bytesOut
|
||||
});
|
||||
} else {
|
||||
// Calculate rate based on time since previous sample
|
||||
const prevSample = relevantSamples[i - 1];
|
||||
const timeDelta = (sample.timestamp - prevSample.timestamp) / 1000;
|
||||
|
||||
history.push({
|
||||
timestamp: sample.timestamp,
|
||||
in: Math.round(sample.bytesIn / timeDelta),
|
||||
out: Math.round(sample.bytesOut / timeDelta)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all samples
|
||||
*/
|
||||
public clear(): void {
|
||||
this.samples = [];
|
||||
this.accumulatedBytesIn = 0;
|
||||
this.accumulatedBytesOut = 0;
|
||||
this.lastSampleTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sample count for debugging
|
||||
*/
|
||||
public getSampleCount(): number {
|
||||
return this.samples.length;
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
|
||||
import type { IConnectionRecord } from './models/interfaces.js';
|
||||
import type { SmartProxy } from './smart-proxy.js';
|
||||
|
||||
/**
|
||||
* Manages timeouts and inactivity tracking for connections
|
||||
*/
|
||||
export class TimeoutManager {
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
constructor(private smartProxy: SmartProxy) {}
|
||||
|
||||
/**
|
||||
* Ensure timeout values don't exceed Node.js max safe integer
|
||||
@ -41,16 +42,16 @@ export class TimeoutManager {
|
||||
* Calculate effective inactivity timeout based on connection type
|
||||
*/
|
||||
public getEffectiveInactivityTimeout(record: IConnectionRecord): number {
|
||||
let effectiveTimeout = this.settings.inactivityTimeout || 14400000; // 4 hours default
|
||||
let effectiveTimeout = this.smartProxy.settings.inactivityTimeout || 14400000; // 4 hours default
|
||||
|
||||
// For immortal keep-alive connections, use an extremely long timeout
|
||||
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
||||
if (record.hasKeepAlive && this.smartProxy.settings.keepAliveTreatment === 'immortal') {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
// For extended keep-alive connections, apply multiplier
|
||||
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
||||
const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
|
||||
if (record.hasKeepAlive && this.smartProxy.settings.keepAliveTreatment === 'extended') {
|
||||
const multiplier = this.smartProxy.settings.keepAliveInactivityMultiplier || 6;
|
||||
effectiveTimeout = effectiveTimeout * multiplier;
|
||||
}
|
||||
|
||||
@ -63,23 +64,23 @@ export class TimeoutManager {
|
||||
public getEffectiveMaxLifetime(record: IConnectionRecord): number {
|
||||
// Use route-specific timeout if available from the routeConfig
|
||||
const baseTimeout = record.routeConfig?.action.advanced?.timeout ||
|
||||
this.settings.maxConnectionLifetime ||
|
||||
this.smartProxy.settings.maxConnectionLifetime ||
|
||||
86400000; // 24 hours default
|
||||
|
||||
// For immortal keep-alive connections, use an extremely long lifetime
|
||||
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
||||
if (record.hasKeepAlive && this.smartProxy.settings.keepAliveTreatment === 'immortal') {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
// For extended keep-alive connections, use the extended lifetime setting
|
||||
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
||||
if (record.hasKeepAlive && this.smartProxy.settings.keepAliveTreatment === 'extended') {
|
||||
return this.ensureSafeTimeout(
|
||||
this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000 // 7 days default
|
||||
this.smartProxy.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000 // 7 days default
|
||||
);
|
||||
}
|
||||
|
||||
// Apply randomization if enabled
|
||||
if (this.settings.enableRandomizedTimeouts) {
|
||||
if (this.smartProxy.settings.enableRandomizedTimeouts) {
|
||||
return this.randomizeTimeout(baseTimeout);
|
||||
}
|
||||
|
||||
@ -127,7 +128,7 @@ export class TimeoutManager {
|
||||
effectiveTimeout: number;
|
||||
} {
|
||||
// Skip for connections with inactivity check disabled
|
||||
if (this.settings.disableInactivityCheck) {
|
||||
if (this.smartProxy.settings.disableInactivityCheck) {
|
||||
return {
|
||||
isInactive: false,
|
||||
shouldWarn: false,
|
||||
@ -137,7 +138,7 @@ export class TimeoutManager {
|
||||
}
|
||||
|
||||
// Skip for immortal keep-alive connections
|
||||
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
||||
if (record.hasKeepAlive && this.smartProxy.settings.keepAliveTreatment === 'immortal') {
|
||||
return {
|
||||
isInactive: false,
|
||||
shouldWarn: false,
|
||||
@ -171,7 +172,7 @@ export class TimeoutManager {
|
||||
*/
|
||||
public applySocketTimeouts(record: IConnectionRecord): void {
|
||||
// Skip for immortal keep-alive connections
|
||||
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
||||
if (record.hasKeepAlive && this.smartProxy.settings.keepAliveTreatment === 'immortal') {
|
||||
// Disable timeouts completely for immortal connections
|
||||
record.incoming.setTimeout(0);
|
||||
if (record.outgoing) {
|
||||
@ -181,7 +182,7 @@ export class TimeoutManager {
|
||||
}
|
||||
|
||||
// Apply normal timeouts
|
||||
const timeout = this.ensureSafeTimeout(this.settings.socketTimeout || 3600000); // 1 hour default
|
||||
const timeout = this.ensureSafeTimeout(this.smartProxy.settings.socketTimeout || 3600000); // 1 hour default
|
||||
record.incoming.setTimeout(timeout);
|
||||
if (record.outgoing) {
|
||||
record.outgoing.setTimeout(timeout);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { ISmartProxyOptions } from './models/interfaces.js';
|
||||
import { SniHandler } from '../../tls/sni/sni-handler.js';
|
||||
import type { SmartProxy } from './smart-proxy.js';
|
||||
|
||||
/**
|
||||
* Interface for connection information used for SNI extraction
|
||||
@ -16,7 +16,7 @@ interface IConnectionInfo {
|
||||
* Manages TLS-related operations including SNI extraction and validation
|
||||
*/
|
||||
export class TlsManager {
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
constructor(private smartProxy: SmartProxy) {}
|
||||
|
||||
/**
|
||||
* Check if a data chunk appears to be a TLS handshake
|
||||
@ -44,7 +44,7 @@ export class TlsManager {
|
||||
return SniHandler.processTlsPacket(
|
||||
chunk,
|
||||
connInfo,
|
||||
this.settings.enableTlsDebugLogging || false,
|
||||
this.smartProxy.settings.enableTlsDebugLogging || false,
|
||||
previousDomain
|
||||
);
|
||||
}
|
||||
@ -58,19 +58,19 @@ export class TlsManager {
|
||||
hasSNI: boolean
|
||||
): { shouldBlock: boolean; reason?: string } {
|
||||
// Skip if session tickets are allowed
|
||||
if (this.settings.allowSessionTicket !== false) {
|
||||
if (this.smartProxy.settings.allowSessionTicket !== false) {
|
||||
return { shouldBlock: false };
|
||||
}
|
||||
|
||||
// Check for session resumption attempt
|
||||
const resumptionInfo = SniHandler.hasSessionResumption(
|
||||
chunk,
|
||||
this.settings.enableTlsDebugLogging || false
|
||||
this.smartProxy.settings.enableTlsDebugLogging || false
|
||||
);
|
||||
|
||||
// If this is a resumption attempt without SNI, block it
|
||||
if (resumptionInfo.isResumption && !hasSNI && !resumptionInfo.hasSNI) {
|
||||
if (this.settings.enableTlsDebugLogging) {
|
||||
if (this.smartProxy.settings.enableTlsDebugLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Session resumption detected without SNI and allowSessionTicket=false. ` +
|
||||
`Terminating connection to force new TLS handshake.`
|
||||
@ -104,7 +104,7 @@ export class TlsManager {
|
||||
const newSNI = SniHandler.extractSNIWithResumptionSupport(
|
||||
chunk,
|
||||
connInfo,
|
||||
this.settings.enableTlsDebugLogging || false
|
||||
this.smartProxy.settings.enableTlsDebugLogging || false
|
||||
);
|
||||
|
||||
// Skip if no SNI was found
|
||||
@ -112,14 +112,14 @@ export class TlsManager {
|
||||
|
||||
// Check for SNI mismatch
|
||||
if (newSNI !== expectedDomain) {
|
||||
if (this.settings.enableTlsDebugLogging) {
|
||||
if (this.smartProxy.settings.enableTlsDebugLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Renegotiation with different SNI: ${expectedDomain} -> ${newSNI}. ` +
|
||||
`Terminating connection - SNI domain switching is not allowed.`
|
||||
);
|
||||
}
|
||||
return { hasMismatch: true, extractedSNI: newSNI };
|
||||
} else if (this.settings.enableTlsDebugLogging) {
|
||||
} else if (this.smartProxy.settings.enableTlsDebugLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
|
||||
);
|
||||
@ -175,13 +175,13 @@ export class TlsManager {
|
||||
// Check for session resumption
|
||||
const resumptionInfo = SniHandler.hasSessionResumption(
|
||||
chunk,
|
||||
this.settings.enableTlsDebugLogging || false
|
||||
this.smartProxy.settings.enableTlsDebugLogging || false
|
||||
);
|
||||
|
||||
// Extract SNI
|
||||
const sni = SniHandler.extractSNI(
|
||||
chunk,
|
||||
this.settings.enableTlsDebugLogging || false
|
||||
this.smartProxy.settings.enableTlsDebugLogging || false
|
||||
);
|
||||
|
||||
// Update result
|
||||
|
Reference in New Issue
Block a user