Compare commits

..

4 Commits

Author SHA1 Message Date
f82d44164c 19.6.16
Some checks failed
Default (tags) / security (push) Successful in 1m20s
Default (tags) / test (push) Failing after 29m31s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-07-03 03:17:35 +00:00
2a4ed38f6b update logs 2025-07-03 02:54:56 +00:00
bb2c82b44a 19.6.15
Some checks failed
Default (tags) / security (push) Successful in 1m22s
Default (tags) / test (push) Failing after 29m38s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-07-03 02:45:30 +00:00
dddcf8dec4 improve logging 2025-07-03 02:45:08 +00:00
6 changed files with 167 additions and 19 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartproxy", "name": "@push.rocks/smartproxy",
"version": "19.6.14", "version": "19.6.16",
"private": false, "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.", "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", "main": "dist_ts/index.js",

View File

@ -263,3 +263,36 @@ To reduce log spam during high-traffic scenarios or attacks, SmartProxy implemen
- Improves log readability and analysis - Improves log readability and analysis
- Prevents log storage overflow - Prevents log storage overflow
- Maintains detailed information in aggregated form - Maintains detailed information in aggregated form
### Log Output Examples
Instead of hundreds of individual logs:
```
Connection rejected
Connection rejected
Connection rejected
... (repeated 500 times)
```
You'll see:
```
[SUMMARY] Rejected 500 connections from 10 IPs in 5s (rate-limit: 350, per-ip-limit: 150) (top offenders: 192.168.1.100 (200x, rate-limit), 10.0.0.1 (150x, per-ip-limit))
```
Instead of:
```
Connection terminated: ::ffff:127.0.0.1 (client_closed). Active: 266
Connection terminated: ::ffff:127.0.0.1 (client_closed). Active: 265
... (repeated 266 times)
```
You'll see:
```
[SUMMARY] 266 HttpProxy connections terminated in 5s (reasons: client_closed: 266, activeConnections: 0)
```
### Rapid Event Handling
- During attacks or high-volume scenarios, logs are flushed more frequently
- If 50+ events occur within 1 second, immediate flush is triggered
- Prevents memory buildup during flooding attacks
- Maintains real-time visibility during incidents

View File

@ -37,9 +37,17 @@ Command to re-read CLAUDE.md: `cat /home/philkunz/.claude/CLAUDE.md`
- [x] Test cleanup queue edge cases - [x] Test cleanup queue edge cases
- [x] Test memory usage with many unique IPs - [x] Test memory usage with many unique IPs
### 6. Log Deduplication for High-Volume Scenarios ✓
- [x] Implement LogDeduplicator utility for batching similar events
- [x] Add deduplication for connection rejections, terminations, and cleanups
- [x] Include rejection reasons in IP rejection summaries
- [x] Provide aggregated summaries with meaningful context
## Notes ## Notes
- All connection limiting is now consistent across SmartProxy and HttpProxy - All connection limiting is now consistent across SmartProxy and HttpProxy
- Route-level limits provide additional granular control - Route-level limits provide additional granular control
- Memory usage is optimized for high-traffic scenarios - Memory usage is optimized for high-traffic scenarios
- Comprehensive test coverage ensures reliability - Comprehensive test coverage ensures reliability
- Log deduplication reduces spam during attacks or high-traffic periods
- IP rejection summaries now include rejection reasons in main message

View File

@ -23,6 +23,8 @@ export class LogDeduplicator {
private aggregatedEvents: Map<string, IAggregatedEvent> = new Map(); private aggregatedEvents: Map<string, IAggregatedEvent> = new Map();
private flushInterval: number = 5000; // 5 seconds private flushInterval: number = 5000; // 5 seconds
private maxBatchSize: number = 100; private maxBatchSize: number = 100;
private rapidEventThreshold: number = 50; // Flush early if this many events in 1 second
private lastRapidCheck: number = Date.now();
constructor(flushInterval?: number) { constructor(flushInterval?: number) {
if (flushInterval) { if (flushInterval) {
@ -85,8 +87,15 @@ export class LogDeduplicator {
}); });
} }
// Check if we should flush due to size // Check for rapid events (many events in short time)
if (aggregated.events.size >= this.maxBatchSize) { const totalEvents = Array.from(aggregated.events.values()).reduce((sum, e) => sum + e.count, 0);
// If we're getting flooded with events, flush more frequently
if (now - this.lastRapidCheck < 1000 && totalEvents >= this.rapidEventThreshold) {
this.flush(key);
this.lastRapidCheck = now;
} else if (aggregated.events.size >= this.maxBatchSize) {
// Check if we should flush due to size
this.flush(key); this.flush(key);
} else if (!aggregated.flushTimer) { } else if (!aggregated.flushTimer) {
// Schedule flush // Schedule flush
@ -98,6 +107,11 @@ export class LogDeduplicator {
aggregated.flushTimer.unref(); aggregated.flushTimer.unref();
} }
} }
// Update rapid check time
if (now - this.lastRapidCheck >= 1000) {
this.lastRapidCheck = now;
}
} }
/** /**
@ -122,6 +136,9 @@ export class LogDeduplicator {
case 'connection-cleanup': case 'connection-cleanup':
this.flushConnectionCleanups(aggregated); this.flushConnectionCleanups(aggregated);
break; break;
case 'connection-terminated':
this.flushConnectionTerminations(aggregated);
break;
case 'ip-rejected': case 'ip-rejected':
this.flushIPRejections(aggregated); this.flushIPRejections(aggregated);
break; break;
@ -156,10 +173,10 @@ export class LogDeduplicator {
.map(([reason, count]) => `${reason}: ${count}`) .map(([reason, count]) => `${reason}: ${count}`)
.join(', '); .join(', ');
logger.log('warn', `Rejected ${totalCount} connections`, { const duration = Date.now() - Math.min(...Array.from(aggregated.events.values()).map(e => e.firstSeen));
logger.log('warn', `[SUMMARY] Rejected ${totalCount} connections in ${Math.round(duration/1000)}s`, {
reasons: reasonSummary, reasons: reasonSummary,
uniqueIPs: aggregated.events.size, uniqueIPs: aggregated.events.size,
duration: Date.now() - Math.min(...Array.from(aggregated.events.values()).map(e => e.firstSeen)),
component: 'connection-dedup' component: 'connection-dedup'
}); });
} }
@ -186,8 +203,73 @@ export class LogDeduplicator {
}); });
} }
private flushConnectionTerminations(aggregated: IAggregatedEvent): void {
const totalCount = Array.from(aggregated.events.values()).reduce((sum, e) => sum + e.count, 0);
const byReason = new Map<string, number>();
const byIP = new Map<string, number>();
let lastActiveCount = 0;
for (const [, event] of aggregated.events) {
const reason = event.data?.reason || 'unknown';
const ip = event.data?.remoteIP || 'unknown';
byReason.set(reason, (byReason.get(reason) || 0) + event.count);
// Track by IP
if (ip !== 'unknown') {
byIP.set(ip, (byIP.get(ip) || 0) + event.count);
}
// Track the last active connection count
if (event.data?.activeConnections !== undefined) {
lastActiveCount = event.data.activeConnections;
}
}
const reasonSummary = Array.from(byReason.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5) // Top 5 reasons
.map(([reason, count]) => `${reason}: ${count}`)
.join(', ');
// Show top IPs if there are many different ones
let ipInfo = '';
if (byIP.size > 3) {
const topIPs = Array.from(byIP.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(([ip, count]) => `${ip} (${count})`)
.join(', ');
ipInfo = `, from ${byIP.size} IPs (top: ${topIPs})`;
} else if (byIP.size > 0) {
ipInfo = `, IPs: ${Array.from(byIP.keys()).join(', ')}`;
}
const duration = Date.now() - Math.min(...Array.from(aggregated.events.values()).map(e => e.firstSeen));
// Special handling for localhost connections (HttpProxy)
const localhostCount = byIP.get('::ffff:127.0.0.1') || 0;
if (localhostCount > 0 && byIP.size === 1) {
// All connections are from localhost (HttpProxy)
logger.log('info', `[SUMMARY] ${totalCount} HttpProxy connections terminated in ${Math.round(duration/1000)}s`, {
reasons: reasonSummary,
activeConnections: lastActiveCount,
component: 'connection-dedup'
});
} else {
logger.log('info', `[SUMMARY] ${totalCount} connections terminated in ${Math.round(duration/1000)}s`, {
reasons: reasonSummary,
activeConnections: lastActiveCount,
uniqueReasons: byReason.size,
...(ipInfo ? { ips: ipInfo } : {}),
component: 'connection-dedup'
});
}
}
private flushIPRejections(aggregated: IAggregatedEvent): void { private flushIPRejections(aggregated: IAggregatedEvent): void {
const byIP = new Map<string, { count: number; reasons: Set<string> }>(); const byIP = new Map<string, { count: number; reasons: Set<string> }>();
const allReasons = new Map<string, number>();
for (const [ip, event] of aggregated.events) { for (const [ip, event] of aggregated.events) {
if (!byIP.has(ip)) { if (!byIP.has(ip)) {
@ -197,9 +279,17 @@ export class LogDeduplicator {
ipData.count += event.count; ipData.count += event.count;
if (event.data?.reason) { if (event.data?.reason) {
ipData.reasons.add(event.data.reason); ipData.reasons.add(event.data.reason);
// Track overall reason counts
allReasons.set(event.data.reason, (allReasons.get(event.data.reason) || 0) + event.count);
} }
} }
// Create reason summary
const reasonSummary = Array.from(allReasons.entries())
.sort((a, b) => b[1] - a[1])
.map(([reason, count]) => `${reason}: ${count}`)
.join(', ');
// Log top offenders // Log top offenders
const topOffenders = Array.from(byIP.entries()) const topOffenders = Array.from(byIP.entries())
.sort((a, b) => b[1].count - a[1].count) .sort((a, b) => b[1].count - a[1].count)
@ -209,9 +299,9 @@ export class LogDeduplicator {
const totalRejections = Array.from(byIP.values()).reduce((sum, data) => sum + data.count, 0); const totalRejections = Array.from(byIP.values()).reduce((sum, data) => sum + data.count, 0);
logger.log('warn', `Rejected ${totalRejections} connections from ${byIP.size} IPs`, { const duration = Date.now() - Math.min(...Array.from(aggregated.events.values()).map(e => e.firstSeen));
logger.log('warn', `[SUMMARY] Rejected ${totalRejections} connections from ${byIP.size} IPs in ${Math.round(duration/1000)}s (${reasonSummary})`, {
topOffenders, topOffenders,
duration: Date.now() - Math.min(...Array.from(aggregated.events.values()).map(e => e.firstSeen)),
component: 'ip-dedup' component: 'ip-dedup'
}); });
} }

View File

@ -401,23 +401,34 @@ export class ConnectionManager extends LifecycleComponent {
// Remove the record from the tracking map // Remove the record from the tracking map
this.connectionRecords.delete(record.id); this.connectionRecords.delete(record.id);
// Log connection details // Use deduplicated logging for connection termination
if (this.smartProxy.settings.enableDetailedLogging) { if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', // For detailed logging, include more info but still deduplicate by IP+reason
`Connection terminated: ${record.remoteIP}:${record.localPort} (${reason}) - ` + connectionLogDeduplicator.log(
`${plugins.prettyMs(duration)}, IN: ${record.bytesReceived}B, OUT: ${record.bytesSent}B`, 'connection-terminated',
logData 'info',
`Connection terminated: ${record.remoteIP}:${record.localPort}`,
{
...logData,
duration_ms: duration,
bytesIn: record.bytesReceived,
bytesOut: record.bytesSent
},
`${record.remoteIP}-${reason}`
); );
} else { } else {
logger.log('info', // For normal logging, deduplicate by termination reason
`Connection terminated: ${record.remoteIP} (${reason}). Active: ${this.connectionRecords.size}`, connectionLogDeduplicator.log(
'connection-terminated',
'info',
`Connection terminated`,
{ {
connectionId: record.id,
remoteIP: record.remoteIP, remoteIP: record.remoteIP,
reason, reason,
activeConnections: this.connectionRecords.size, activeConnections: this.connectionRecords.size,
component: 'connection-manager' component: 'connection-manager'
} },
reason // Group by termination reason
); );
} }
} }

View File

@ -90,7 +90,13 @@ export class RouteConnectionHandler {
// Note: For wrapped sockets, this will use the underlying socket IP until PROXY protocol is parsed // Note: For wrapped sockets, this will use the underlying socket IP until PROXY protocol is parsed
const ipValidation = this.smartProxy.securityManager.validateIP(wrappedSocket.remoteAddress || ''); const ipValidation = this.smartProxy.securityManager.validateIP(wrappedSocket.remoteAddress || '');
if (!ipValidation.allowed) { if (!ipValidation.allowed) {
logger.log('warn', `Connection rejected`, { remoteIP: wrappedSocket.remoteAddress, reason: ipValidation.reason, component: 'route-handler' }); connectionLogDeduplicator.log(
'ip-rejected',
'warn',
`Connection rejected from ${wrappedSocket.remoteAddress}`,
{ remoteIP: wrappedSocket.remoteAddress, reason: ipValidation.reason, component: 'route-handler' },
wrappedSocket.remoteAddress
);
cleanupSocket(wrappedSocket.socket, `rejected-${ipValidation.reason}`, { immediate: true }); cleanupSocket(wrappedSocket.socket, `rejected-${ipValidation.reason}`, { immediate: true });
return; return;
} }