Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
6e16f9423a | |||
e5ec48abd3 | |||
131a454b28 | |||
de1269665a |
@ -1,5 +1,5 @@
|
||||
{
|
||||
"expiryDate": "2025-09-03T17:57:28.583Z",
|
||||
"issueDate": "2025-06-05T17:57:28.583Z",
|
||||
"savedAt": "2025-06-05T17:57:28.583Z"
|
||||
"expiryDate": "2025-09-20T22:46:46.609Z",
|
||||
"issueDate": "2025-06-22T22:46:46.609Z",
|
||||
"savedAt": "2025-06-22T22:46:46.610Z"
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "19.6.5",
|
||||
"version": "19.6.7",
|
||||
"private": false,
|
||||
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
||||
"main": "dist_ts/index.js",
|
||||
|
0
readme.hints.md
Normal file
0
readme.hints.md
Normal file
318
readme.md
318
readme.md
@ -1576,150 +1576,316 @@ Available helper functions:
|
||||
|
||||
## Metrics and Monitoring
|
||||
|
||||
SmartProxy includes a comprehensive metrics collection system that provides real-time insights into proxy performance, connection statistics, and throughput data.
|
||||
SmartProxy includes a comprehensive metrics collection system that provides real-time insights into proxy performance, connection statistics, and throughput data. The metrics system uses a clean, grouped API design for intuitive access to different metric categories.
|
||||
|
||||
### Enabling Metrics
|
||||
|
||||
```typescript
|
||||
const proxy = new SmartProxy({
|
||||
// Enable metrics collection
|
||||
metrics: {
|
||||
enabled: true,
|
||||
sampleIntervalMs: 1000, // Sample throughput every second
|
||||
retentionSeconds: 3600 // Keep 1 hour of history
|
||||
},
|
||||
routes: [/* your routes */]
|
||||
});
|
||||
|
||||
await proxy.start();
|
||||
```
|
||||
|
||||
### Getting Metrics
|
||||
|
||||
```typescript
|
||||
const proxy = new SmartProxy({ /* config */ });
|
||||
await proxy.start();
|
||||
// Access metrics through the getMetrics() method
|
||||
const metrics = proxy.getMetrics();
|
||||
|
||||
// Access metrics through the getStats() method
|
||||
const stats = proxy.getStats();
|
||||
// The metrics object provides grouped methods for different categories
|
||||
```
|
||||
|
||||
### Connection Metrics
|
||||
|
||||
Monitor active connections, total connections, and connection distribution:
|
||||
|
||||
```typescript
|
||||
// Get current active connections
|
||||
console.log(`Active connections: ${stats.getActiveConnections()}`);
|
||||
console.log(`Active connections: ${metrics.connections.active()}`);
|
||||
|
||||
// Get total connections since start
|
||||
console.log(`Total connections: ${stats.getTotalConnections()}`);
|
||||
|
||||
// Get requests per second (RPS)
|
||||
console.log(`Current RPS: ${stats.getRequestsPerSecond()}`);
|
||||
|
||||
// Get throughput data
|
||||
const throughput = stats.getThroughput();
|
||||
console.log(`Bytes received: ${throughput.bytesIn}`);
|
||||
console.log(`Bytes sent: ${throughput.bytesOut}`);
|
||||
console.log(`Total connections: ${metrics.connections.total()}`);
|
||||
|
||||
// Get connections by route
|
||||
const routeConnections = stats.getConnectionsByRoute();
|
||||
const routeConnections = metrics.connections.byRoute();
|
||||
for (const [route, count] of routeConnections) {
|
||||
console.log(`Route ${route}: ${count} connections`);
|
||||
}
|
||||
|
||||
// Get connections by IP address
|
||||
const ipConnections = stats.getConnectionsByIP();
|
||||
const ipConnections = metrics.connections.byIP();
|
||||
for (const [ip, count] of ipConnections) {
|
||||
console.log(`IP ${ip}: ${count} connections`);
|
||||
}
|
||||
|
||||
// Get top IPs by connection count
|
||||
const topIPs = metrics.connections.topIPs(10);
|
||||
topIPs.forEach(({ ip, count }) => {
|
||||
console.log(`${ip}: ${count} connections`);
|
||||
});
|
||||
```
|
||||
|
||||
### Available Metrics
|
||||
### Throughput Metrics
|
||||
|
||||
The `IProxyStats` interface provides the following methods:
|
||||
|
||||
- `getActiveConnections()`: Current number of active connections
|
||||
- `getTotalConnections()`: Total connections handled since proxy start
|
||||
- `getRequestsPerSecond()`: Current requests per second (1-minute average)
|
||||
- `getThroughput()`: Total bytes transferred (in/out)
|
||||
- `getConnectionsByRoute()`: Connection count per route
|
||||
- `getConnectionsByIP()`: Connection count per client IP
|
||||
|
||||
Additional extended methods available:
|
||||
|
||||
- `getThroughputRate()`: Bytes per second rate for the last minute
|
||||
- `getTopIPs(limit?: number)`: Get top IPs by connection count
|
||||
- `isIPBlocked(ip: string, maxConnectionsPerIP: number)`: Check if an IP has reached the connection limit
|
||||
|
||||
### Extended Metrics Example
|
||||
Real-time and historical throughput data with customizable time windows:
|
||||
|
||||
```typescript
|
||||
const stats = proxy.getStats() as any; // Extended methods are available
|
||||
// Get instant throughput (last 1 second)
|
||||
const instant = metrics.throughput.instant();
|
||||
console.log(`Current: ${instant.in} bytes/sec in, ${instant.out} bytes/sec out`);
|
||||
|
||||
// Get throughput rate
|
||||
const rate = stats.getThroughputRate();
|
||||
console.log(`Incoming: ${rate.bytesInPerSec} bytes/sec`);
|
||||
console.log(`Outgoing: ${rate.bytesOutPerSec} bytes/sec`);
|
||||
// Get recent throughput (last 10 seconds average)
|
||||
const recent = metrics.throughput.recent();
|
||||
console.log(`Recent: ${recent.in} bytes/sec in, ${recent.out} bytes/sec out`);
|
||||
|
||||
// Get top 10 IPs by connection count
|
||||
const topIPs = stats.getTopIPs(10);
|
||||
topIPs.forEach(({ ip, connections }) => {
|
||||
console.log(`${ip}: ${connections} connections`);
|
||||
// Get average throughput (last 60 seconds)
|
||||
const average = metrics.throughput.average();
|
||||
console.log(`Average: ${average.in} bytes/sec in, ${average.out} bytes/sec out`);
|
||||
|
||||
// Get custom time window (e.g., last 5 minutes)
|
||||
const custom = metrics.throughput.custom(300);
|
||||
console.log(`5-min avg: ${custom.in} bytes/sec in, ${custom.out} bytes/sec out`);
|
||||
|
||||
// Get throughput history for graphing
|
||||
const history = metrics.throughput.history(300); // Last 5 minutes
|
||||
history.forEach(point => {
|
||||
console.log(`${new Date(point.timestamp)}: ${point.in} in, ${point.out} out`);
|
||||
});
|
||||
|
||||
// Check if an IP should be rate limited
|
||||
if (stats.isIPBlocked('192.168.1.100', 100)) {
|
||||
console.log('IP has too many connections');
|
||||
}
|
||||
// Get throughput by route
|
||||
const routeThroughput = metrics.throughput.byRoute(60); // Last 60 seconds
|
||||
routeThroughput.forEach((stats, route) => {
|
||||
console.log(`Route ${route}: ${stats.in} in, ${stats.out} out bytes/sec`);
|
||||
});
|
||||
|
||||
// Get throughput by IP
|
||||
const ipThroughput = metrics.throughput.byIP(60);
|
||||
ipThroughput.forEach((stats, ip) => {
|
||||
console.log(`IP ${ip}: ${stats.in} in, ${stats.out} out bytes/sec`);
|
||||
});
|
||||
```
|
||||
|
||||
### Monitoring Example
|
||||
### Request Metrics
|
||||
|
||||
Track request rates:
|
||||
|
||||
```typescript
|
||||
// Create a monitoring loop
|
||||
// Get requests per second
|
||||
console.log(`RPS: ${metrics.requests.perSecond()}`);
|
||||
|
||||
// Get requests per minute
|
||||
console.log(`RPM: ${metrics.requests.perMinute()}`);
|
||||
|
||||
// Get total requests
|
||||
console.log(`Total requests: ${metrics.requests.total()}`);
|
||||
```
|
||||
|
||||
### Cumulative Totals
|
||||
|
||||
Track total bytes transferred and connections:
|
||||
|
||||
```typescript
|
||||
// Get total bytes
|
||||
console.log(`Total bytes in: ${metrics.totals.bytesIn()}`);
|
||||
console.log(`Total bytes out: ${metrics.totals.bytesOut()}`);
|
||||
console.log(`Total connections: ${metrics.totals.connections()}`);
|
||||
```
|
||||
|
||||
### Performance Percentiles
|
||||
|
||||
Get percentile statistics (when implemented):
|
||||
|
||||
```typescript
|
||||
// Connection duration percentiles
|
||||
const durations = metrics.percentiles.connectionDuration();
|
||||
console.log(`Connection durations - P50: ${durations.p50}ms, P95: ${durations.p95}ms, P99: ${durations.p99}ms`);
|
||||
|
||||
// Bytes transferred percentiles
|
||||
const bytes = metrics.percentiles.bytesTransferred();
|
||||
console.log(`Bytes in - P50: ${bytes.in.p50}, P95: ${bytes.in.p95}, P99: ${bytes.in.p99}`);
|
||||
console.log(`Bytes out - P50: ${bytes.out.p50}, P95: ${bytes.out.p95}, P99: ${bytes.out.p99}`);
|
||||
```
|
||||
|
||||
### Complete Monitoring Example
|
||||
|
||||
```typescript
|
||||
// Create a monitoring dashboard
|
||||
setInterval(() => {
|
||||
const stats = proxy.getStats();
|
||||
const metrics = proxy.getMetrics();
|
||||
|
||||
// Log key metrics
|
||||
console.log({
|
||||
timestamp: new Date().toISOString(),
|
||||
activeConnections: stats.getActiveConnections(),
|
||||
rps: stats.getRequestsPerSecond(),
|
||||
throughput: stats.getThroughput()
|
||||
connections: {
|
||||
active: metrics.connections.active(),
|
||||
total: metrics.connections.total()
|
||||
},
|
||||
throughput: {
|
||||
instant: metrics.throughput.instant(),
|
||||
average: metrics.throughput.average()
|
||||
},
|
||||
requests: {
|
||||
rps: metrics.requests.perSecond(),
|
||||
total: metrics.requests.total()
|
||||
},
|
||||
totals: {
|
||||
bytesIn: metrics.totals.bytesIn(),
|
||||
bytesOut: metrics.totals.bytesOut()
|
||||
}
|
||||
});
|
||||
|
||||
// Check for high connection counts from specific IPs
|
||||
const ipConnections = stats.getConnectionsByIP();
|
||||
for (const [ip, count] of ipConnections) {
|
||||
// Alert on high connection counts
|
||||
const topIPs = metrics.connections.topIPs(5);
|
||||
topIPs.forEach(({ ip, count }) => {
|
||||
if (count > 100) {
|
||||
console.warn(`High connection count from ${ip}: ${count}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Alert on high throughput
|
||||
const instant = metrics.throughput.instant();
|
||||
if (instant.in > 100_000_000) { // 100 MB/s
|
||||
console.warn(`High incoming throughput: ${instant.in} bytes/sec`);
|
||||
}
|
||||
}, 10000); // Every 10 seconds
|
||||
```
|
||||
|
||||
### Exporting Metrics
|
||||
|
||||
You can export metrics in various formats for external monitoring systems:
|
||||
Export metrics in various formats for external monitoring systems:
|
||||
|
||||
```typescript
|
||||
// Export as JSON
|
||||
app.get('/metrics.json', (req, res) => {
|
||||
const stats = proxy.getStats();
|
||||
const metrics = proxy.getMetrics();
|
||||
res.json({
|
||||
activeConnections: stats.getActiveConnections(),
|
||||
totalConnections: stats.getTotalConnections(),
|
||||
requestsPerSecond: stats.getRequestsPerSecond(),
|
||||
throughput: stats.getThroughput(),
|
||||
connectionsByRoute: Object.fromEntries(stats.getConnectionsByRoute()),
|
||||
connectionsByIP: Object.fromEntries(stats.getConnectionsByIP())
|
||||
connections: {
|
||||
active: metrics.connections.active(),
|
||||
total: metrics.connections.total(),
|
||||
byRoute: Object.fromEntries(metrics.connections.byRoute()),
|
||||
byIP: Object.fromEntries(metrics.connections.byIP())
|
||||
},
|
||||
throughput: {
|
||||
instant: metrics.throughput.instant(),
|
||||
recent: metrics.throughput.recent(),
|
||||
average: metrics.throughput.average()
|
||||
},
|
||||
requests: {
|
||||
perSecond: metrics.requests.perSecond(),
|
||||
perMinute: metrics.requests.perMinute(),
|
||||
total: metrics.requests.total()
|
||||
},
|
||||
totals: {
|
||||
bytesIn: metrics.totals.bytesIn(),
|
||||
bytesOut: metrics.totals.bytesOut(),
|
||||
connections: metrics.totals.connections()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Export as Prometheus format
|
||||
app.get('/metrics', (req, res) => {
|
||||
const stats = proxy.getStats();
|
||||
const metrics = proxy.getMetrics();
|
||||
const instant = metrics.throughput.instant();
|
||||
|
||||
res.set('Content-Type', 'text/plain');
|
||||
res.send(`
|
||||
# HELP smartproxy_active_connections Current active connections
|
||||
# TYPE smartproxy_active_connections gauge
|
||||
smartproxy_active_connections ${stats.getActiveConnections()}
|
||||
# HELP smartproxy_connections_active Current active connections
|
||||
# TYPE smartproxy_connections_active gauge
|
||||
smartproxy_connections_active ${metrics.connections.active()}
|
||||
|
||||
# HELP smartproxy_connections_total Total connections since start
|
||||
# TYPE smartproxy_connections_total counter
|
||||
smartproxy_connections_total ${metrics.connections.total()}
|
||||
|
||||
# HELP smartproxy_throughput_bytes_per_second Current throughput in bytes per second
|
||||
# TYPE smartproxy_throughput_bytes_per_second gauge
|
||||
smartproxy_throughput_bytes_per_second{direction="in"} ${instant.in}
|
||||
smartproxy_throughput_bytes_per_second{direction="out"} ${instant.out}
|
||||
|
||||
# HELP smartproxy_requests_per_second Current requests per second
|
||||
# TYPE smartproxy_requests_per_second gauge
|
||||
smartproxy_requests_per_second ${stats.getRequestsPerSecond()}
|
||||
smartproxy_requests_per_second ${metrics.requests.perSecond()}
|
||||
|
||||
# HELP smartproxy_bytes_in Total bytes received
|
||||
# TYPE smartproxy_bytes_in counter
|
||||
smartproxy_bytes_in ${stats.getThroughput().bytesIn}
|
||||
|
||||
# HELP smartproxy_bytes_out Total bytes sent
|
||||
# TYPE smartproxy_bytes_out counter
|
||||
smartproxy_bytes_out ${stats.getThroughput().bytesOut}
|
||||
# HELP smartproxy_bytes_total Total bytes transferred
|
||||
# TYPE smartproxy_bytes_total counter
|
||||
smartproxy_bytes_total{direction="in"} ${metrics.totals.bytesIn()}
|
||||
smartproxy_bytes_total{direction="out"} ${metrics.totals.bytesOut()}
|
||||
`);
|
||||
});
|
||||
```
|
||||
|
||||
### Metrics API Reference
|
||||
|
||||
The metrics API is organized into logical groups:
|
||||
|
||||
```typescript
|
||||
interface IMetrics {
|
||||
connections: {
|
||||
active(): number;
|
||||
total(): number;
|
||||
byRoute(): Map<string, number>;
|
||||
byIP(): Map<string, number>;
|
||||
topIPs(limit?: number): Array<{ ip: string; count: number }>;
|
||||
};
|
||||
|
||||
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>;
|
||||
};
|
||||
|
||||
requests: {
|
||||
perSecond(): number;
|
||||
perMinute(): number;
|
||||
total(): number;
|
||||
};
|
||||
|
||||
totals: {
|
||||
bytesIn(): number;
|
||||
bytesOut(): number;
|
||||
connections(): number;
|
||||
};
|
||||
|
||||
percentiles: {
|
||||
connectionDuration(): { p50: number; p95: number; p99: number };
|
||||
bytesTransferred(): {
|
||||
in: { p50: number; p95: number; p99: number };
|
||||
out: { p50: number; p95: number; p99: number };
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Where `IThroughputData` is:
|
||||
```typescript
|
||||
interface IThroughputData {
|
||||
in: number; // Bytes per second incoming
|
||||
out: number; // Bytes per second outgoing
|
||||
}
|
||||
```
|
||||
|
||||
And `IThroughputHistoryPoint` is:
|
||||
```typescript
|
||||
interface IThroughputHistoryPoint {
|
||||
timestamp: number; // Unix timestamp in milliseconds
|
||||
in: number; // Bytes per second at this point
|
||||
out: number; // Bytes per second at this point
|
||||
}
|
||||
```
|
||||
|
||||
## Other Components
|
||||
|
||||
While SmartProxy provides a unified API for most needs, you can also use individual components:
|
||||
|
364
readme.plan.md
Normal file
364
readme.plan.md
Normal file
@ -0,0 +1,364 @@
|
||||
# SmartProxy Metrics Improvement Plan
|
||||
|
||||
## Overview
|
||||
|
||||
The current `getThroughputRate()` implementation calculates cumulative throughput over a 60-second window rather than providing an actual rate, making metrics misleading for monitoring systems. This plan outlines a comprehensive redesign of the metrics system to provide accurate, time-series based metrics suitable for production monitoring.
|
||||
|
||||
## 1. Core Issues with Current Implementation
|
||||
|
||||
- **Cumulative vs Rate**: Current method accumulates all bytes from connections in the last minute rather than calculating actual throughput rate
|
||||
- **No Time-Series Data**: Cannot track throughput changes over time
|
||||
- **Inaccurate Estimates**: Attempting to estimate rates for older connections is fundamentally flawed
|
||||
- **No Sliding Windows**: Cannot provide different time window views (1s, 10s, 60s, etc.)
|
||||
- **Limited Granularity**: Only provides a single 60-second view
|
||||
|
||||
## 2. Proposed Architecture
|
||||
|
||||
### A. Time-Series Throughput Tracking
|
||||
|
||||
```typescript
|
||||
interface IThroughputSample {
|
||||
timestamp: number;
|
||||
bytesIn: number;
|
||||
bytesOut: number;
|
||||
}
|
||||
|
||||
class ThroughputTracker {
|
||||
private samples: IThroughputSample[] = [];
|
||||
private readonly MAX_SAMPLES = 3600; // 1 hour at 1 sample/second
|
||||
private lastSampleTime: number = 0;
|
||||
private accumulatedBytesIn: number = 0;
|
||||
private accumulatedBytesOut: number = 0;
|
||||
|
||||
// Called on every data transfer
|
||||
public recordBytes(bytesIn: number, bytesOut: number): void {
|
||||
this.accumulatedBytesIn += bytesIn;
|
||||
this.accumulatedBytesOut += bytesOut;
|
||||
}
|
||||
|
||||
// Called periodically (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;
|
||||
|
||||
// Trim old samples
|
||||
const cutoff = now - 3600000; // 1 hour
|
||||
this.samples = this.samples.filter(s => s.timestamp > cutoff);
|
||||
}
|
||||
|
||||
// Get rate over specified window
|
||||
public getRate(windowSeconds: number): { bytesInPerSec: number; bytesOutPerSec: number } {
|
||||
const now = Date.now();
|
||||
const windowStart = now - (windowSeconds * 1000);
|
||||
|
||||
const relevantSamples = this.samples.filter(s => s.timestamp > windowStart);
|
||||
|
||||
if (relevantSamples.length === 0) {
|
||||
return { bytesInPerSec: 0, bytesOutPerSec: 0 };
|
||||
}
|
||||
|
||||
const totalBytesIn = relevantSamples.reduce((sum, s) => sum + s.bytesIn, 0);
|
||||
const totalBytesOut = relevantSamples.reduce((sum, s) => sum + s.bytesOut, 0);
|
||||
|
||||
const actualWindow = (now - relevantSamples[0].timestamp) / 1000;
|
||||
|
||||
return {
|
||||
bytesInPerSec: Math.round(totalBytesIn / actualWindow),
|
||||
bytesOutPerSec: Math.round(totalBytesOut / actualWindow)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### B. Connection-Level Byte Tracking
|
||||
|
||||
```typescript
|
||||
// In ConnectionRecord, add:
|
||||
interface IConnectionRecord {
|
||||
// ... existing fields ...
|
||||
|
||||
// Byte counters with timestamps
|
||||
bytesReceivedHistory: Array<{ timestamp: number; bytes: number }>;
|
||||
bytesSentHistory: Array<{ timestamp: number; bytes: number }>;
|
||||
|
||||
// For efficiency, could use circular buffer
|
||||
lastBytesReceivedUpdate: number;
|
||||
lastBytesSentUpdate: number;
|
||||
}
|
||||
```
|
||||
|
||||
### C. Enhanced Metrics Interface
|
||||
|
||||
```typescript
|
||||
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 }>;
|
||||
};
|
||||
|
||||
// Throughput metrics (bytes per second)
|
||||
throughput: {
|
||||
instant(): { in: number; out: number }; // Last 1 second
|
||||
recent(): { in: number; out: number }; // Last 10 seconds
|
||||
average(): { in: number; out: number }; // Last 60 seconds
|
||||
custom(seconds: number): { in: number; out: number };
|
||||
history(seconds: number): Array<{ timestamp: number; in: number; out: number }>;
|
||||
byRoute(windowSeconds?: number): Map<string, { in: number; out: number }>;
|
||||
byIP(windowSeconds?: number): Map<string, { in: number; out: number }>;
|
||||
};
|
||||
|
||||
// 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 };
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Implementation Plan
|
||||
|
||||
### Current Status
|
||||
- **Phase 1**: ~90% complete (core functionality implemented, tests need fixing)
|
||||
- **Phase 2**: ~60% complete (main features done, percentiles pending)
|
||||
- **Phase 3**: ~40% complete (basic optimizations in place)
|
||||
- **Phase 4**: 0% complete (export formats not started)
|
||||
|
||||
### Phase 1: Core Throughput Tracking (Week 1)
|
||||
- [x] Implement `ThroughputTracker` class
|
||||
- [x] Integrate byte recording into socket data handlers
|
||||
- [x] Add periodic sampling (1-second intervals)
|
||||
- [x] Update `getThroughputRate()` to use time-series data (replaced with new clean API)
|
||||
- [ ] Add unit tests for throughput tracking
|
||||
|
||||
### Phase 2: Enhanced Metrics (Week 2)
|
||||
- [x] Add configurable time windows (1s, 10s, 60s, 5m, etc.)
|
||||
- [ ] Implement percentile calculations
|
||||
- [x] Add route-specific and IP-specific throughput tracking
|
||||
- [x] Create historical data access methods
|
||||
- [ ] Add integration tests
|
||||
|
||||
### Phase 3: Performance Optimization (Week 3)
|
||||
- [x] Use circular buffers for efficiency
|
||||
- [ ] Implement data aggregation for longer time windows
|
||||
- [x] Add configurable retention periods
|
||||
- [ ] Optimize memory usage
|
||||
- [ ] Add performance benchmarks
|
||||
|
||||
### Phase 4: Export Formats (Week 4)
|
||||
- [ ] Add Prometheus metric format with proper metric types
|
||||
- [ ] Add StatsD format support
|
||||
- [ ] Add JSON export with metadata
|
||||
- [ ] Create OpenMetrics compatibility
|
||||
- [ ] Add documentation and examples
|
||||
|
||||
## 4. Key Design Decisions
|
||||
|
||||
### A. Sampling Strategy
|
||||
- **1-second samples** for fine-grained data
|
||||
- **Aggregate to 1-minute** for longer retention
|
||||
- **Keep 1 hour** of second-level data
|
||||
- **Keep 24 hours** of minute-level data
|
||||
|
||||
### B. Memory Management
|
||||
- **Circular buffers** for fixed memory usage
|
||||
- **Configurable retention** periods
|
||||
- **Lazy aggregation** for older data
|
||||
- **Efficient data structures** (typed arrays for samples)
|
||||
|
||||
### C. Performance Considerations
|
||||
- **Batch updates** during high throughput
|
||||
- **Debounced calculations** for expensive metrics
|
||||
- **Cached results** with TTL
|
||||
- **Worker thread** option for heavy calculations
|
||||
|
||||
## 5. Configuration Options
|
||||
|
||||
```typescript
|
||||
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_
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Example Usage
|
||||
|
||||
```typescript
|
||||
const proxy = new SmartProxy({
|
||||
metrics: {
|
||||
enabled: true,
|
||||
sampleIntervalMs: 1000,
|
||||
enableDetailedTracking: true
|
||||
}
|
||||
});
|
||||
|
||||
// Get metrics instance
|
||||
const metrics = proxy.getMetrics();
|
||||
|
||||
// Connection metrics
|
||||
console.log(`Active connections: ${metrics.connections.active()}`);
|
||||
console.log(`Total connections: ${metrics.connections.total()}`);
|
||||
|
||||
// Throughput metrics
|
||||
const instant = metrics.throughput.instant();
|
||||
console.log(`Current: ${instant.in} bytes/sec in, ${instant.out} bytes/sec out`);
|
||||
|
||||
const recent = metrics.throughput.recent(); // Last 10 seconds
|
||||
const average = metrics.throughput.average(); // Last 60 seconds
|
||||
|
||||
// Custom time window
|
||||
const custom = metrics.throughput.custom(30); // Last 30 seconds
|
||||
|
||||
// Historical data for graphing
|
||||
const history = metrics.throughput.history(300); // Last 5 minutes
|
||||
history.forEach(point => {
|
||||
console.log(`${new Date(point.timestamp)}: ${point.in} bytes/sec in, ${point.out} bytes/sec out`);
|
||||
});
|
||||
|
||||
// Top routes by throughput
|
||||
const routeThroughput = metrics.throughput.byRoute(60);
|
||||
routeThroughput.forEach((stats, route) => {
|
||||
console.log(`Route ${route}: ${stats.in} bytes/sec in, ${stats.out} bytes/sec out`);
|
||||
});
|
||||
|
||||
// Request metrics
|
||||
console.log(`RPS: ${metrics.requests.perSecond()}`);
|
||||
console.log(`RPM: ${metrics.requests.perMinute()}`);
|
||||
|
||||
// Totals
|
||||
console.log(`Total bytes in: ${metrics.totals.bytesIn()}`);
|
||||
console.log(`Total bytes out: ${metrics.totals.bytesOut()}`);
|
||||
```
|
||||
|
||||
## 7. Prometheus Export Example
|
||||
|
||||
```
|
||||
# HELP smartproxy_throughput_bytes_per_second Current throughput in bytes per second
|
||||
# TYPE smartproxy_throughput_bytes_per_second gauge
|
||||
smartproxy_throughput_bytes_per_second{direction="in",window="1s"} 1234567
|
||||
smartproxy_throughput_bytes_per_second{direction="out",window="1s"} 987654
|
||||
smartproxy_throughput_bytes_per_second{direction="in",window="10s"} 1134567
|
||||
smartproxy_throughput_bytes_per_second{direction="out",window="10s"} 887654
|
||||
|
||||
# HELP smartproxy_bytes_total Total bytes transferred
|
||||
# TYPE smartproxy_bytes_total counter
|
||||
smartproxy_bytes_total{direction="in"} 123456789
|
||||
smartproxy_bytes_total{direction="out"} 98765432
|
||||
|
||||
# HELP smartproxy_active_connections Current number of active connections
|
||||
# TYPE smartproxy_active_connections gauge
|
||||
smartproxy_active_connections 42
|
||||
|
||||
# HELP smartproxy_connection_duration_seconds Connection duration in seconds
|
||||
# TYPE smartproxy_connection_duration_seconds histogram
|
||||
smartproxy_connection_duration_seconds_bucket{le="0.1"} 100
|
||||
smartproxy_connection_duration_seconds_bucket{le="1"} 500
|
||||
smartproxy_connection_duration_seconds_bucket{le="10"} 800
|
||||
smartproxy_connection_duration_seconds_bucket{le="+Inf"} 850
|
||||
smartproxy_connection_duration_seconds_sum 4250
|
||||
smartproxy_connection_duration_seconds_count 850
|
||||
```
|
||||
|
||||
## 8. Migration Strategy
|
||||
|
||||
### Breaking Changes
|
||||
- Completely replace the old metrics API with the new clean design
|
||||
- Remove all `get*` prefixed methods in favor of grouped properties
|
||||
- Use simple `{ in, out }` objects instead of verbose property names
|
||||
- Provide clear migration guide in documentation
|
||||
|
||||
### Implementation Approach
|
||||
1. ✅ Create new `ThroughputTracker` class for time-series data
|
||||
2. ✅ Implement new `IMetrics` interface with clean API
|
||||
3. ✅ Replace `MetricsCollector` implementation entirely
|
||||
4. ✅ Update all references to use new API
|
||||
5. ⚠️ Add comprehensive tests for accuracy validation (partial)
|
||||
|
||||
### Additional Refactoring Completed
|
||||
- Refactored all SmartProxy components to use cleaner dependency pattern
|
||||
- Components now receive only `SmartProxy` instance instead of individual dependencies
|
||||
- Access to other components via `this.smartProxy.componentName`
|
||||
- Significantly simplified constructor signatures across the codebase
|
||||
|
||||
## 9. Success Metrics
|
||||
|
||||
- **Accuracy**: Throughput metrics accurate within 1% of actual
|
||||
- **Performance**: < 1% CPU overhead for metrics collection
|
||||
- **Memory**: < 10MB memory usage for 1 hour of data
|
||||
- **Latency**: < 1ms to retrieve any metric
|
||||
- **Reliability**: No metrics data loss under load
|
||||
|
||||
## 10. Future Enhancements
|
||||
|
||||
### Phase 5: Advanced Analytics
|
||||
- Anomaly detection for traffic patterns
|
||||
- Predictive analytics for capacity planning
|
||||
- Correlation analysis between routes
|
||||
- Real-time alerting integration
|
||||
|
||||
### Phase 6: Distributed Metrics
|
||||
- Metrics aggregation across multiple proxies
|
||||
- Distributed time-series storage
|
||||
- Cross-proxy analytics
|
||||
- Global dashboard support
|
||||
|
||||
## 11. Risks and Mitigations
|
||||
|
||||
### Risk: Memory Usage
|
||||
- **Mitigation**: Circular buffers and configurable retention
|
||||
- **Monitoring**: Track memory usage per metric type
|
||||
|
||||
### Risk: Performance Impact
|
||||
- **Mitigation**: Efficient data structures and caching
|
||||
- **Testing**: Load test with metrics enabled/disabled
|
||||
|
||||
### Risk: Data Accuracy
|
||||
- **Mitigation**: Atomic operations and proper synchronization
|
||||
- **Validation**: Compare with external monitoring tools
|
||||
|
||||
## Conclusion
|
||||
|
||||
This plan transforms SmartProxy's metrics from a basic cumulative system to a comprehensive, time-series based monitoring solution suitable for production environments. The phased approach ensures minimal disruption while delivering immediate value through accurate throughput measurements.
|
@ -30,10 +30,27 @@ tap.test('cleanup queue bug - verify queue processing handles more than batch si
|
||||
const mockConnections: any[] = [];
|
||||
|
||||
for (let i = 0; i < 150; i++) {
|
||||
// Create mock socket objects with necessary methods
|
||||
const mockIncoming = {
|
||||
destroyed: true,
|
||||
writable: false,
|
||||
remoteAddress: '127.0.0.1',
|
||||
removeAllListeners: () => {},
|
||||
destroy: () => {},
|
||||
end: () => {}
|
||||
};
|
||||
|
||||
const mockOutgoing = {
|
||||
destroyed: true,
|
||||
writable: false,
|
||||
destroy: () => {},
|
||||
end: () => {}
|
||||
};
|
||||
|
||||
const mockRecord = {
|
||||
id: `mock-${i}`,
|
||||
incoming: { destroyed: true, remoteAddress: '127.0.0.1' },
|
||||
outgoing: { destroyed: true },
|
||||
incoming: mockIncoming,
|
||||
outgoing: mockOutgoing,
|
||||
connectionClosed: false,
|
||||
incomingStartTime: Date.now(),
|
||||
lastActivity: Date.now(),
|
||||
@ -66,9 +83,17 @@ tap.test('cleanup queue bug - verify queue processing handles more than batch si
|
||||
// Wait for cleanup to complete
|
||||
console.log('\n--- Waiting for cleanup batches to process ---');
|
||||
|
||||
// The first batch should process immediately (100 connections)
|
||||
// Then additional batches should be scheduled
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
// The cleanup happens in batches, wait for all to complete
|
||||
let waitTime = 0;
|
||||
while (cm.getConnectionCount() > 0 || cm.cleanupQueue.size > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
waitTime += 100;
|
||||
if (waitTime > 5000) {
|
||||
console.log('Timeout waiting for cleanup to complete');
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log(`Cleanup completed in ${waitTime}ms`);
|
||||
|
||||
// Check final state
|
||||
const finalCount = cm.getConnectionCount();
|
||||
@ -85,6 +110,7 @@ tap.test('cleanup queue bug - verify queue processing handles more than batch si
|
||||
expect(stats.incoming.test_cleanup).toEqual(150);
|
||||
|
||||
// Cleanup
|
||||
console.log('\n--- Stopping proxy ---');
|
||||
await proxy.stop();
|
||||
|
||||
console.log('\n✓ Test complete: Cleanup queue now correctly processes all connections');
|
||||
|
261
test/test.metrics-new.ts
Normal file
261
test/test.metrics-new.ts
Normal file
@ -0,0 +1,261 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from '../ts/plugins.js';
|
||||
import { SmartProxy } from '../ts/index.js';
|
||||
import * as net from 'net';
|
||||
|
||||
let smartProxyInstance: SmartProxy;
|
||||
let echoServer: net.Server;
|
||||
const echoServerPort = 9876;
|
||||
const proxyPort = 8080;
|
||||
|
||||
// Create an echo server for testing
|
||||
tap.test('should create echo server for testing', async () => {
|
||||
echoServer = net.createServer((socket) => {
|
||||
socket.on('data', (data) => {
|
||||
socket.write(data); // Echo back the data
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
echoServer.listen(echoServerPort, () => {
|
||||
console.log(`Echo server listening on port ${echoServerPort}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should create SmartProxy instance with new metrics', async () => {
|
||||
smartProxyInstance = new SmartProxy({
|
||||
routes: [{
|
||||
name: 'test-route',
|
||||
match: {
|
||||
matchType: 'startsWith',
|
||||
matchAgainst: 'domain',
|
||||
value: ['*'],
|
||||
ports: [proxyPort] // Add the port to match on
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: 'localhost',
|
||||
port: echoServerPort
|
||||
},
|
||||
tls: {
|
||||
mode: 'passthrough'
|
||||
}
|
||||
}
|
||||
}],
|
||||
defaultTarget: {
|
||||
host: 'localhost',
|
||||
port: echoServerPort
|
||||
},
|
||||
metrics: {
|
||||
enabled: true,
|
||||
sampleIntervalMs: 100, // Sample every 100ms for faster testing
|
||||
retentionSeconds: 60
|
||||
}
|
||||
});
|
||||
|
||||
await smartProxyInstance.start();
|
||||
});
|
||||
|
||||
tap.test('should verify new metrics API structure', async () => {
|
||||
const metrics = smartProxyInstance.getMetrics();
|
||||
|
||||
// Check API structure
|
||||
expect(metrics).toHaveProperty('connections');
|
||||
expect(metrics).toHaveProperty('throughput');
|
||||
expect(metrics).toHaveProperty('requests');
|
||||
expect(metrics).toHaveProperty('totals');
|
||||
expect(metrics).toHaveProperty('percentiles');
|
||||
|
||||
// Check connections methods
|
||||
expect(metrics.connections).toHaveProperty('active');
|
||||
expect(metrics.connections).toHaveProperty('total');
|
||||
expect(metrics.connections).toHaveProperty('byRoute');
|
||||
expect(metrics.connections).toHaveProperty('byIP');
|
||||
expect(metrics.connections).toHaveProperty('topIPs');
|
||||
|
||||
// Check throughput methods
|
||||
expect(metrics.throughput).toHaveProperty('instant');
|
||||
expect(metrics.throughput).toHaveProperty('recent');
|
||||
expect(metrics.throughput).toHaveProperty('average');
|
||||
expect(metrics.throughput).toHaveProperty('custom');
|
||||
expect(metrics.throughput).toHaveProperty('history');
|
||||
expect(metrics.throughput).toHaveProperty('byRoute');
|
||||
expect(metrics.throughput).toHaveProperty('byIP');
|
||||
});
|
||||
|
||||
tap.test('should track throughput correctly', async (tools) => {
|
||||
const metrics = smartProxyInstance.getMetrics();
|
||||
|
||||
// Initial state - no connections yet
|
||||
expect(metrics.connections.active()).toEqual(0);
|
||||
expect(metrics.throughput.instant()).toEqual({ in: 0, out: 0 });
|
||||
|
||||
// Create a test connection
|
||||
const client = new net.Socket();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.connect(proxyPort, 'localhost', () => {
|
||||
console.log('Connected to proxy');
|
||||
resolve();
|
||||
});
|
||||
|
||||
client.on('error', reject);
|
||||
});
|
||||
|
||||
// Send some data
|
||||
const testData = Buffer.from('Hello, World!'.repeat(100)); // ~1.3KB
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
client.write(testData, () => {
|
||||
console.log('Data sent');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for echo response
|
||||
await new Promise<void>((resolve) => {
|
||||
client.once('data', (data) => {
|
||||
console.log(`Received ${data.length} bytes back`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for metrics to be sampled
|
||||
await tools.delayFor(200);
|
||||
|
||||
// Check metrics
|
||||
expect(metrics.connections.active()).toEqual(1);
|
||||
expect(metrics.requests.total()).toBeGreaterThan(0);
|
||||
|
||||
// Check throughput - should show bytes transferred
|
||||
const instant = metrics.throughput.instant();
|
||||
console.log('Instant throughput:', instant);
|
||||
|
||||
// Should have recorded some throughput
|
||||
expect(instant.in).toBeGreaterThan(0);
|
||||
expect(instant.out).toBeGreaterThan(0);
|
||||
|
||||
// Check totals
|
||||
expect(metrics.totals.bytesIn()).toBeGreaterThan(0);
|
||||
expect(metrics.totals.bytesOut()).toBeGreaterThan(0);
|
||||
|
||||
// Clean up
|
||||
client.destroy();
|
||||
await tools.delayFor(100);
|
||||
|
||||
// Verify connection was cleaned up
|
||||
expect(metrics.connections.active()).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should track multiple connections and routes', async (tools) => {
|
||||
const metrics = smartProxyInstance.getMetrics();
|
||||
|
||||
// Create multiple connections
|
||||
const clients: net.Socket[] = [];
|
||||
const connectionCount = 5;
|
||||
|
||||
for (let i = 0; i < connectionCount; i++) {
|
||||
const client = new net.Socket();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.connect(proxyPort, 'localhost', () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
client.on('error', reject);
|
||||
});
|
||||
|
||||
clients.push(client);
|
||||
}
|
||||
|
||||
// Verify active connections
|
||||
expect(metrics.connections.active()).toEqual(connectionCount);
|
||||
|
||||
// Send data on each connection
|
||||
const dataPromises = clients.map((client, index) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const data = Buffer.from(`Connection ${index}: `.repeat(50));
|
||||
client.write(data, () => {
|
||||
client.once('data', () => resolve());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(dataPromises);
|
||||
await tools.delayFor(200);
|
||||
|
||||
// Check metrics by route
|
||||
const routeConnections = metrics.connections.byRoute();
|
||||
console.log('Connections by route:', Array.from(routeConnections.entries()));
|
||||
expect(routeConnections.get('test-route')).toEqual(connectionCount);
|
||||
|
||||
// Check top IPs
|
||||
const topIPs = metrics.connections.topIPs(5);
|
||||
console.log('Top IPs:', topIPs);
|
||||
expect(topIPs.length).toBeGreaterThan(0);
|
||||
expect(topIPs[0].count).toEqual(connectionCount);
|
||||
|
||||
// Clean up all connections
|
||||
clients.forEach(client => client.destroy());
|
||||
await tools.delayFor(100);
|
||||
|
||||
expect(metrics.connections.active()).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should provide throughput history', async (tools) => {
|
||||
const metrics = smartProxyInstance.getMetrics();
|
||||
|
||||
// Create a connection and send data periodically
|
||||
const client = new net.Socket();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.connect(proxyPort, 'localhost', () => resolve());
|
||||
client.on('error', reject);
|
||||
});
|
||||
|
||||
// Send data every 100ms for 1 second
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const data = Buffer.from(`Packet ${i}: `.repeat(100));
|
||||
client.write(data);
|
||||
await tools.delayFor(100);
|
||||
}
|
||||
|
||||
// Get throughput history
|
||||
const history = metrics.throughput.history(2); // Last 2 seconds
|
||||
console.log('Throughput history entries:', history.length);
|
||||
console.log('Sample history entry:', history[0]);
|
||||
|
||||
expect(history.length).toBeGreaterThan(0);
|
||||
expect(history[0]).toHaveProperty('timestamp');
|
||||
expect(history[0]).toHaveProperty('in');
|
||||
expect(history[0]).toHaveProperty('out');
|
||||
|
||||
// Verify different time windows show different rates
|
||||
const instant = metrics.throughput.instant();
|
||||
const recent = metrics.throughput.recent();
|
||||
const average = metrics.throughput.average();
|
||||
|
||||
console.log('Throughput windows:');
|
||||
console.log(' Instant (1s):', instant);
|
||||
console.log(' Recent (10s):', recent);
|
||||
console.log(' Average (60s):', average);
|
||||
|
||||
// Clean up
|
||||
client.destroy();
|
||||
});
|
||||
|
||||
tap.test('should clean up resources', async () => {
|
||||
await smartProxyInstance.stop();
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
echoServer.close(() => {
|
||||
console.log('Echo server closed');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tap.start();
|
@ -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 {
|
||||
// Connection metrics implementation
|
||||
public connections = {
|
||||
active: (): number => {
|
||||
return this.smartProxy.connectionManager.getConnectionCount();
|
||||
},
|
||||
|
||||
total: (): number => {
|
||||
const stats = this.smartProxy.connectionManager.getTerminationStats();
|
||||
let total = this.smartProxy.connectionManager.getConnectionCount();
|
||||
|
||||
for (const reason in stats.incoming) {
|
||||
total += stats.incoming[reason];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection counts grouped by route name
|
||||
*/
|
||||
public getConnectionsByRoute(): Map<string, number> {
|
||||
const now = Date.now();
|
||||
return total;
|
||||
},
|
||||
|
||||
// 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
|
||||
byRoute: (): Map<string, number> => {
|
||||
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';
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
const current = routeCounts.get(routeName) || 0;
|
||||
routeCounts.set(routeName, current + 1);
|
||||
}
|
||||
|
||||
// Cache and return
|
||||
this.cachedMetrics.connectionsByRoute = routeCounts;
|
||||
this.cachedMetrics.timestamp = now;
|
||||
return new Map(routeCounts);
|
||||
}
|
||||
return routeCounts;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get connection counts grouped by IP address
|
||||
*/
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
||||
// Cache and return
|
||||
this.cachedMetrics.connectionsByIP = ipCounts;
|
||||
this.cachedMetrics.timestamp = now;
|
||||
return new Map(ipCounts);
|
||||
}
|
||||
return 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 {
|
||||
const now = Date.now();
|
||||
this.requestTimestamps.push(now);
|
||||
|
||||
// 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;
|
||||
this.requestTimestamps = this.requestTimestamps.filter(ts => ts > cutoff);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total throughput (bytes transferred)
|
||||
*/
|
||||
public getThroughput(): { bytesIn: number; bytesOut: number } {
|
||||
let bytesIn = 0;
|
||||
let bytesOut = 0;
|
||||
|
||||
// Sum bytes from all active connections
|
||||
for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
|
||||
bytesIn += record.bytesReceived;
|
||||
bytesOut += record.bytesSent;
|
||||
}
|
||||
|
||||
return { bytesIn, bytesOut };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get throughput rate (bytes per second) for last minute
|
||||
*/
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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())
|
||||
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, connections]) => ({ ip, connections }));
|
||||
.map(([ip, count]) => ({ ip, count }));
|
||||
}
|
||||
};
|
||||
|
||||
return sorted;
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
// Convert to rates
|
||||
for (const [route, bytes] of routeBytes) {
|
||||
routeThroughput.set(route, {
|
||||
in: Math.round(bytes.in / windowSeconds),
|
||||
out: Math.round(bytes.out / windowSeconds)
|
||||
});
|
||||
}
|
||||
|
||||
return routeThroughput;
|
||||
},
|
||||
|
||||
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 }
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean up old request timestamps
|
||||
* Record a new request
|
||||
*/
|
||||
private cleanupOldRequests(): void {
|
||||
const cutoff = Date.now() - this.RPS_WINDOW_SIZE;
|
||||
public recordRequest(connectionId: string, routeName: string, remoteIP: string): void {
|
||||
const now = Date.now();
|
||||
this.requestTimestamps.push(now);
|
||||
this.totalRequests++;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the metrics collector and set up subscriptions
|
||||
* Record bytes transferred for a connection
|
||||
*/
|
||||
public recordBytes(connectionId: string, bytesIn: number, bytesOut: number): void {
|
||||
// Update global throughput tracker
|
||||
this.throughputTracker.recordBytes(bytesIn, bytesOut);
|
||||
|
||||
// Update connection-specific tracker
|
||||
const tracker = this.connectionByteTrackers.get(connectionId);
|
||||
if (tracker) {
|
||||
tracker.bytesIn += bytesIn;
|
||||
tracker.bytesOut += bytesOut;
|
||||
tracker.lastUpdate = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up tracking for a closed connection
|
||||
*/
|
||||
public removeConnection(connectionId: string): void {
|
||||
this.connectionByteTrackers.delete(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
@ -105,6 +105,13 @@ export interface ISmartProxyOptions {
|
||||
useHttpProxy?: number[]; // Array of ports to forward to HttpProxy
|
||||
httpProxyPort?: number; // Port where HttpProxy is listening (default: 8443)
|
||||
|
||||
// Metrics configuration
|
||||
metrics?: {
|
||||
enabled?: boolean;
|
||||
sampleIntervalMs?: number;
|
||||
retentionSeconds?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Global ACME configuration options for SmartProxy
|
||||
*
|
||||
|
@ -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 };
|
||||
|
||||
/**
|
||||
* Get top IPs by connection count
|
||||
*/
|
||||
getTopIPs(limit?: number): Array<{ ip: string; connections: number }>;
|
||||
|
||||
/**
|
||||
* Check if an IP has reached the connection limit
|
||||
*/
|
||||
isIPBlocked(ip: string, maxConnectionsPerIP: number): boolean;
|
||||
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 }>;
|
||||
};
|
||||
|
||||
// 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>;
|
||||
};
|
||||
|
||||
// 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();
|
||||
@ -213,7 +201,10 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
this.acmeStateManager = new AcmeStateManager();
|
||||
|
||||
// Initialize metrics collector with reference to this SmartProxy instance
|
||||
this.metricsCollector = new MetricsCollector(this);
|
||||
this.metricsCollector = new MetricsCollector(this, {
|
||||
sampleIntervalMs: this.settings.metrics?.sampleIntervalMs,
|
||||
retentionSeconds: this.settings.metrics?.retentionSeconds
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -922,11 +913,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