Compare commits

...

8 Commits

5 changed files with 483 additions and 340 deletions

View File

@ -1,5 +1,39 @@
# Changelog
## 2025-03-05 - 3.26.0 - feat(readme)
Updated README with enhanced TLS handling, connection management, and troubleshooting sections.
- Added details on enhanced TLS handling and browser compatibility improvements.
- Included advanced connection management features like random timeout prevention.
- Provided comprehensive troubleshooting tips for browser certificate errors and connection stability.
- Clarified default configuration options and optimization settings for PortProxy.
## 2025-03-05 - 3.25.4 - fix(portproxy)
Improve connection timeouts and detailed logging for PortProxy
- Refactored timeout management for connections to include enhanced defaults and prevent thundering herd.
- Improved support for TLS handshake detection with logging capabilities in PortProxy.
- Removed protocol-specific handling which is now managed generically.
- Introduced enhanced logging for SNI extraction and connection management.
## 2025-03-05 - 3.25.3 - fix(core)
Update dependencies and configuration improvements.
- Upgrade TypeScript version to 5.8.2 for better compatibility.
- Ensure all proxy and server tests pass with updated configurations.
- Improve logging for better traceability in proxy operations.
- Add handlers for WebSockets and HTTPS improvements.
- Fix various issues related to proxy timeout and connection handling.
- Update test certificates validation for better test coverage.
## 2025-03-05 - 3.25.2 - fix(PortProxy)
Adjust timeout settings and handle inactivity properly in PortProxy.
- Changed initialDataTimeout default to 30 seconds for better handling of initial data reception.
- Adjusted keepAliveInitialDelay to 30 seconds for consistent socket optimization.
- Introduced proper inactivity handling with updated timeout logic.
- Parity check now accounts for a 120-second threshold for outgoing socket closure.
## 2025-03-05 - 3.25.1 - fix(PortProxy)
Adjust inactivity threshold to a random value between 20 and 30 minutes for better variability

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartproxy",
"version": "3.25.1",
"version": "3.26.0",
"private": false,
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.",
"main": "dist_ts/index.js",

126
readme.md
View File

@ -193,12 +193,14 @@ sequenceDiagram
- **HTTPS Reverse Proxy** - Route traffic to backend services based on hostname with TLS termination
- **WebSocket Support** - Full WebSocket proxying with heartbeat monitoring
- **TCP Port Forwarding** - Advanced port forwarding with SNI inspection and domain-based routing
- **Enhanced TLS Handling** - Robust TLS handshake processing with improved certificate error handling
- **HTTP to HTTPS Redirection** - Automatically redirect HTTP requests to HTTPS
- **Let's Encrypt Integration** - Automatic certificate management using ACME protocol
- **IP Filtering** - Control access with IP allow/block lists using glob patterns
- **IPTables Integration** - Direct manipulation of iptables for low-level port forwarding
- **Basic Authentication** - Support for basic auth on proxied routes
- **Connection Management** - Intelligent connection tracking and cleanup
- **Connection Management** - Intelligent connection tracking and cleanup with configurable timeouts
- **Browser Compatibility** - Optimized for modern browsers with fixes for common TLS handshake issues
## Installation
@ -275,18 +277,38 @@ const portProxy = new PortProxy({
toPort: 8443,
targetIP: 'localhost', // Default target host
sniEnabled: true, // Enable SNI inspection
// Enhanced reliability settings
initialDataTimeout: 60000, // 60 seconds for initial TLS handshake
socketTimeout: 3600000, // 1 hour socket timeout
maxConnectionLifetime: 3600000, // 1 hour connection lifetime
inactivityTimeout: 3600000, // 1 hour inactivity timeout
maxPendingDataSize: 10 * 1024 * 1024, // 10MB buffer for large TLS handshakes
// Browser compatibility enhancement
enableTlsDebugLogging: false, // Enable for troubleshooting TLS issues
// Port and IP configuration
globalPortRanges: [{ from: 443, to: 443 }],
defaultAllowedIPs: ['*'], // Allow all IPs by default
// Socket optimizations for better connection stability
noDelay: true, // Disable Nagle's algorithm
keepAlive: true, // Enable TCP keepalive
enableKeepAliveProbes: true, // Enhanced keepalive for stability
// Domain-specific routing configuration
domainConfigs: [
{
domains: ['example.com', '*.example.com'], // Glob patterns for matching domains
allowedIPs: ['192.168.1.*'], // Restrict access by IP
blockedIPs: ['192.168.1.100'], // Block specific IPs
targetIPs: ['10.0.0.1', '10.0.0.2'], // Round-robin between multiple targets
portRanges: [{ from: 443, to: 443 }]
portRanges: [{ from: 443, to: 443 }],
connectionTimeout: 7200000 // Domain-specific timeout (2 hours)
}
],
maxConnectionLifetime: 3600000, // 1 hour in milliseconds
preserveSourceIP: true
});
@ -333,19 +355,31 @@ acmeHandler.addDomain('api.example.com');
### PortProxy Settings
| Option | Description | Default |
|--------------------------|--------------------------------------------------------|-------------|
| `fromPort` | Port to listen on | - |
| `toPort` | Destination port to forward to | - |
| `targetIP` | Default destination IP if not specified in domainConfig | 'localhost' |
| `sniEnabled` | Enable SNI inspection for TLS connections | false |
| `defaultAllowedIPs` | IP patterns allowed by default | - |
| `defaultBlockedIPs` | IP patterns blocked by default | - |
| `preserveSourceIP` | Preserve the original client IP | false |
| `maxConnectionLifetime` | Maximum time in ms to keep a connection open | 600000 |
| `globalPortRanges` | Array of port ranges to listen on | - |
| `forwardAllGlobalRanges` | Forward all global range connections to targetIP | false |
| `gracefulShutdownTimeout`| Time in ms to wait during shutdown | 30000 |
| Option | Description | Default |
|---------------------------|--------------------------------------------------------|-------------|
| `fromPort` | Port to listen on | - |
| `toPort` | Destination port to forward to | - |
| `targetIP` | Default destination IP if not specified in domainConfig | 'localhost' |
| `sniEnabled` | Enable SNI inspection for TLS connections | false |
| `defaultAllowedIPs` | IP patterns allowed by default | - |
| `defaultBlockedIPs` | IP patterns blocked by default | - |
| `preserveSourceIP` | Preserve the original client IP | false |
| `maxConnectionLifetime` | Maximum time in ms to keep a connection open | 3600000 |
| `initialDataTimeout` | Timeout for initial data/handshake in ms | 60000 |
| `socketTimeout` | Socket inactivity timeout in ms | 3600000 |
| `inactivityTimeout` | Connection inactivity check timeout in ms | 3600000 |
| `inactivityCheckInterval` | How often to check for inactive connections in ms | 60000 |
| `maxPendingDataSize` | Maximum bytes to buffer during connection setup | 10485760 |
| `globalPortRanges` | Array of port ranges to listen on | - |
| `forwardAllGlobalRanges` | Forward all global range connections to targetIP | false |
| `gracefulShutdownTimeout` | Time in ms to wait during shutdown | 30000 |
| `noDelay` | Disable Nagle's algorithm | true |
| `keepAlive` | Enable TCP keepalive | true |
| `keepAliveInitialDelay` | Initial delay before sending keepalive probes in ms | 30000 |
| `enableKeepAliveProbes` | Enable enhanced TCP keep-alive probes | false |
| `enableTlsDebugLogging` | Enable detailed TLS handshake debugging | false |
| `enableDetailedLogging` | Enable detailed connection logging | false |
| `enableRandomizedTimeouts`| Randomize timeouts slightly to prevent thundering herd | true |
### IPTablesProxy Settings
@ -359,14 +393,37 @@ acmeHandler.addDomain('api.example.com');
## Advanced Features
### TLS Handshake Optimization
The enhanced `PortProxy` implementation includes significant improvements for TLS handshake handling:
- Robust SNI extraction with improved error handling
- Increased buffer size for complex TLS handshakes (10MB)
- Longer initial handshake timeout (60 seconds)
- Detection and tracking of TLS connection states
- Optional detailed TLS debug logging for troubleshooting
- Browser compatibility fixes for Chrome certificate errors
```typescript
// Example configuration to solve Chrome certificate errors
const portProxy = new PortProxy({
// ... other settings
initialDataTimeout: 60000, // Give browser more time for handshake
maxPendingDataSize: 10 * 1024 * 1024, // Larger buffer for complex handshakes
enableTlsDebugLogging: true, // Enable when troubleshooting
});
```
### Connection Management and Monitoring
The `PortProxy` class includes built-in connection tracking and monitoring:
- Automatic cleanup of idle connections
- Automatic cleanup of idle connections with configurable timeouts
- Timeouts for connections that exceed maximum lifetime
- Detailed logging of connection states
- Termination statistics
- Randomized timeouts to prevent "thundering herd" problems
- Per-domain timeout configuration
### WebSocket Support
@ -385,6 +442,39 @@ The `PortProxy` class can inspect the SNI (Server Name Indication) field in TLS
- Domain-specific allowed IP ranges
- Protection against SNI renegotiation attacks
## Troubleshooting
### Browser Certificate Errors
If you experience certificate errors in browsers, especially in Chrome, try these solutions:
1. **Increase Initial Data Timeout**: Set `initialDataTimeout` to 60 seconds or higher
2. **Increase Buffer Size**: Set `maxPendingDataSize` to 10MB or higher
3. **Enable TLS Debug Logging**: Set `enableTlsDebugLogging: true` to troubleshoot handshake issues
4. **Enable Keep-Alive Probes**: Set `enableKeepAliveProbes: true` for better connection stability
5. **Check Certificate Chain**: Ensure your certificate chain is complete and in the correct order
```typescript
// Configuration to fix Chrome certificate errors
const portProxy = new PortProxy({
// ... other settings
initialDataTimeout: 60000,
maxPendingDataSize: 10 * 1024 * 1024,
enableTlsDebugLogging: true,
enableKeepAliveProbes: true
});
```
### Connection Stability
For improved connection stability in high-traffic environments:
1. **Set Appropriate Timeouts**: Use longer timeouts for long-lived connections
2. **Use Domain-Specific Timeouts**: Configure per-domain timeouts for different types of services
3. **Enable TCP Keep-Alive**: Ensure `keepAlive` is set to `true`
4. **Monitor Connection Statistics**: Enable detailed logging to track termination reasons
5. **Fine-tune Inactivity Checks**: Adjust `inactivityCheckInterval` based on your traffic patterns
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
@ -402,4 +492,4 @@ Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '3.25.1',
version: '3.26.0',
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.'
}

View File

@ -7,14 +7,10 @@ export interface IDomainConfig {
blockedIPs?: string[]; // Glob patterns for blocked IPs
targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
// Protocol-specific timeout overrides
httpTimeout?: number; // HTTP connection timeout override (ms)
wsTimeout?: number; // WebSocket connection timeout override (ms)
// Allow domain-specific timeout override
connectionTimeout?: number; // Connection timeout override (ms)
}
/** Connection protocol types for timeout management */
export type ProtocolType = 'http' | 'websocket' | 'https' | 'tls' | 'unknown';
/** Port proxy settings including global allowed port ranges */
export interface IPortProxySettings extends plugins.tls.TlsOptions {
fromPort: number;
@ -26,40 +22,37 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
defaultBlockedIPs?: string[];
preserveSourceIP?: boolean;
// Updated timeout settings with better defaults
initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 15000 (15s)
socketTimeout?: number; // Socket inactivity timeout (ms), default: 300000 (5m)
inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 30000 (30s)
// Timeout settings
initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s)
maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 3600000 (1h)
inactivityTimeout?: number; // Inactivity timeout (ms), default: 3600000 (1h)
// Protocol-specific timeouts
maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 3600000 (1h)
httpConnectionTimeout?: number; // HTTP specific timeout (ms), default: 1800000 (30m)
wsConnectionTimeout?: number; // WebSocket specific timeout (ms), default: 14400000 (4h)
httpKeepAliveTimeout?: number; // HTTP keep-alive header timeout (ms), default: 1200000 (20m)
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
// Socket optimization settings
noDelay?: boolean; // Disable Nagle's algorithm (default: true)
keepAlive?: boolean; // Enable TCP keepalive (default: true)
keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
noDelay?: boolean; // Disable Nagle's algorithm (default: true)
keepAlive?: boolean; // Enable TCP keepalive (default: true)
keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
// Enable enhanced features
disableInactivityCheck?: boolean; // Disable inactivity checking entirely
enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
enableProtocolDetection?: boolean; // Enable HTTP/WebSocket protocol detection
enableDetailedLogging?: boolean; // Enable detailed connection logging
// Enhanced features
disableInactivityCheck?: boolean; // Disable inactivity checking entirely
enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
enableDetailedLogging?: boolean; // Enable detailed connection logging
enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
// Rate limiting and security
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
}
/**
* Enhanced connection record with protocol-specific handling
* Enhanced connection record
*/
interface IConnectionRecord {
id: string; // Unique connection identifier
@ -76,78 +69,161 @@ interface IConnectionRecord {
pendingDataSize: number; // Track total size of pending data
// Enhanced tracking fields
protocolType: ProtocolType; // Connection protocol type
isPooledConnection: boolean; // Whether this is likely a browser pooled connection
lastHttpRequest?: number; // Timestamp of last HTTP request (for keep-alive tracking)
httpKeepAliveTimeout?: number; // HTTP keep-alive timeout from headers
bytesReceived: number; // Total bytes received
bytesSent: number; // Total bytes sent
remoteIP: string; // Remote IP (cached for logging after socket close)
localPort: number; // Local port (cached for logging)
httpRequests: number; // Count of HTTP requests on this connection
isTLS: boolean; // Whether this connection is a TLS connection
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
hasReceivedInitialData: boolean; // Whether initial data has been received
domainConfig?: IDomainConfig; // Associated domain config for this connection
}
/**
* Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
* Enhanced for robustness and detailed logging.
* @param buffer - Buffer containing the TLS ClientHello.
* @param enableLogging - Whether to enable detailed logging.
* @returns The server name if found, otherwise undefined.
*/
function extractSNI(buffer: Buffer): string | undefined {
let offset = 0;
if (buffer.length < 5) return undefined;
const recordType = buffer.readUInt8(0);
if (recordType !== 22) return undefined; // 22 = handshake
const recordLength = buffer.readUInt16BE(3);
if (buffer.length < 5 + recordLength) return undefined;
offset = 5;
const handshakeType = buffer.readUInt8(offset);
if (handshakeType !== 1) return undefined; // 1 = ClientHello
offset += 4; // Skip handshake header (type + length)
offset += 2 + 32; // Skip client version and random
const sessionIDLength = buffer.readUInt8(offset);
offset += 1 + sessionIDLength; // Skip session ID
const cipherSuitesLength = buffer.readUInt16BE(offset);
offset += 2 + cipherSuitesLength; // Skip cipher suites
const compressionMethodsLength = buffer.readUInt8(offset);
offset += 1 + compressionMethodsLength; // Skip compression methods
if (offset + 2 > buffer.length) return undefined;
const extensionsLength = buffer.readUInt16BE(offset);
offset += 2;
const extensionsEnd = offset + extensionsLength;
while (offset + 4 <= extensionsEnd) {
const extensionType = buffer.readUInt16BE(offset);
const extensionLength = buffer.readUInt16BE(offset + 2);
offset += 4;
if (extensionType === 0x0000) { // SNI extension
if (offset + 2 > buffer.length) return undefined;
const sniListLength = buffer.readUInt16BE(offset);
offset += 2;
const sniListEnd = offset + sniListLength;
while (offset + 3 < sniListEnd) {
const nameType = buffer.readUInt8(offset++);
const nameLen = buffer.readUInt16BE(offset);
offset += 2;
if (nameType === 0) { // host_name
if (offset + nameLen > buffer.length) return undefined;
return buffer.toString('utf8', offset, offset + nameLen);
}
offset += nameLen;
}
break;
} else {
offset += extensionLength;
function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
try {
// Check if buffer is too small for TLS
if (buffer.length < 5) {
if (enableLogging) console.log("Buffer too small for TLS header");
return undefined;
}
// Check record type (has to be handshake - 22)
const recordType = buffer.readUInt8(0);
if (recordType !== 22) {
if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
return undefined;
}
// Check TLS version (has to be 3.1 or higher)
const majorVersion = buffer.readUInt8(1);
const minorVersion = buffer.readUInt8(2);
if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion}`);
// Check record length
const recordLength = buffer.readUInt16BE(3);
if (buffer.length < 5 + recordLength) {
if (enableLogging) console.log(`Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`);
return undefined;
}
let offset = 5;
const handshakeType = buffer.readUInt8(offset);
if (handshakeType !== 1) {
if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
return undefined;
}
offset += 4; // Skip handshake header (type + length)
// Client version
const clientMajorVersion = buffer.readUInt8(offset);
const clientMinorVersion = buffer.readUInt8(offset + 1);
if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
offset += 2 + 32; // Skip client version and random
// Session ID
const sessionIDLength = buffer.readUInt8(offset);
if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
offset += 1 + sessionIDLength; // Skip session ID
// Cipher suites
if (offset + 2 > buffer.length) {
if (enableLogging) console.log("Buffer too small for cipher suites length");
return undefined;
}
const cipherSuitesLength = buffer.readUInt16BE(offset);
if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
offset += 2 + cipherSuitesLength; // Skip cipher suites
// Compression methods
if (offset + 1 > buffer.length) {
if (enableLogging) console.log("Buffer too small for compression methods length");
return undefined;
}
const compressionMethodsLength = buffer.readUInt8(offset);
if (enableLogging) console.log(`Compression Methods Length: ${compressionMethodsLength}`);
offset += 1 + compressionMethodsLength; // Skip compression methods
// Extensions
if (offset + 2 > buffer.length) {
if (enableLogging) console.log("Buffer too small for extensions length");
return undefined;
}
const extensionsLength = buffer.readUInt16BE(offset);
if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
offset += 2;
const extensionsEnd = offset + extensionsLength;
if (extensionsEnd > buffer.length) {
if (enableLogging) console.log(`Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`);
return undefined;
}
// Parse extensions
while (offset + 4 <= extensionsEnd) {
const extensionType = buffer.readUInt16BE(offset);
const extensionLength = buffer.readUInt16BE(offset + 2);
if (enableLogging) console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
offset += 4;
if (extensionType === 0x0000) { // SNI extension
if (offset + 2 > buffer.length) {
if (enableLogging) console.log("Buffer too small for SNI list length");
return undefined;
}
const sniListLength = buffer.readUInt16BE(offset);
if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
offset += 2;
const sniListEnd = offset + sniListLength;
if (sniListEnd > buffer.length) {
if (enableLogging) console.log(`Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`);
return undefined;
}
while (offset + 3 < sniListEnd) {
const nameType = buffer.readUInt8(offset++);
const nameLen = buffer.readUInt16BE(offset);
offset += 2;
if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
if (nameType === 0) { // host_name
if (offset + nameLen > buffer.length) {
if (enableLogging) console.log(`Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${buffer.length}`);
return undefined;
}
const serverName = buffer.toString('utf8', offset, offset + nameLen);
if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
return serverName;
}
offset += nameLen;
}
break;
} else {
offset += extensionLength;
}
}
if (enableLogging) console.log("No SNI extension found");
return undefined;
} catch (err) {
console.log(`Error extracting SNI: ${err}`);
return undefined;
}
return undefined;
}
// Helper: Check if a port falls within any of the given port ranges
@ -157,7 +233,10 @@ const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }
// Helper: Check if a given IP matches any of the glob patterns
const isAllowed = (ip: string, patterns: string[]): boolean => {
if (!ip || !patterns || patterns.length === 0) return false;
const normalizeIP = (ip: string): string[] => {
if (!ip) return [];
if (ip.startsWith('::ffff:')) {
const ipv4 = ip.slice(7);
return [ip, ipv4];
@ -167,7 +246,10 @@ const isAllowed = (ip: string, patterns: string[]): boolean => {
}
return [ip];
};
const normalizedIPVariants = normalizeIP(ip);
if (normalizedIPVariants.length === 0) return false;
const expandedPatterns = patterns.flatMap(normalizeIP);
return normalizedIPVariants.some(ipVariant =>
expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern))
@ -176,6 +258,7 @@ const isAllowed = (ip: string, patterns: string[]): boolean => {
// Helper: Check if an IP is allowed considering allowed and blocked glob patterns
const isGlobIPAllowed = (ip: string, allowed: string[], blocked: string[] = []): boolean => {
if (!ip) return false;
if (blocked.length > 0 && isAllowed(ip, blocked)) return false;
return isAllowed(ip, allowed);
};
@ -185,34 +268,17 @@ const generateConnectionId = (): string => {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
};
// Protocol detection helpers
const isHttpRequest = (buffer: Buffer): boolean => {
if (buffer.length < 4) return false;
const start = buffer.toString('ascii', 0, 4).toUpperCase();
return (
start.startsWith('GET ') ||
start.startsWith('POST') ||
start.startsWith('PUT ') ||
start.startsWith('HEAD') ||
start.startsWith('DELE') ||
start.startsWith('PATC') ||
start.startsWith('OPTI')
);
};
const isWebSocketUpgrade = (buffer: Buffer): boolean => {
if (buffer.length < 20) return false;
const data = buffer.toString('ascii', 0, Math.min(buffer.length, 200));
return (
data.includes('Upgrade: websocket') ||
data.includes('Upgrade: WebSocket')
);
};
// Helper: Check if a buffer contains a TLS handshake
const isTlsHandshake = (buffer: Buffer): boolean => {
return buffer.length > 0 && buffer[0] === 22; // ContentType.handshake
};
// Helper: Generate a slightly randomized timeout to prevent thundering herd
const randomizeTimeout = (baseTimeout: number, variationPercent: number = 5): number => {
const variation = baseTimeout * (variationPercent / 100);
return baseTimeout + Math.floor(Math.random() * variation * 2) - variation;
};
export class PortProxy {
private netServers: plugins.net.Server[] = [];
settings: IPortProxySettings;
@ -242,30 +308,27 @@ export class PortProxy {
...settingsArg,
targetIP: settingsArg.targetIP || 'localhost',
// Timeout settings with browser-friendly defaults
initialDataTimeout: settingsArg.initialDataTimeout || 15000, // 15 seconds
socketTimeout: settingsArg.socketTimeout || 300000, // 5 minutes
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 30000, // 30 seconds
// Protocol-specific timeouts
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 3600000, // 1 hour default
httpConnectionTimeout: settingsArg.httpConnectionTimeout || 1800000, // 30 minutes
wsConnectionTimeout: settingsArg.wsConnectionTimeout || 14400000, // 4 hours
httpKeepAliveTimeout: settingsArg.httpKeepAliveTimeout || 1200000, // 20 minutes
// Timeout settings with our enhanced defaults
initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial data
socketTimeout: settingsArg.socketTimeout || 3600000, // 1 hour socket timeout
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 3600000, // 1 hour default lifetime
inactivityTimeout: settingsArg.inactivityTimeout || 3600000, // 1 hour inactivity timeout
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
// Socket optimization settings
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 60000, // 1 minute
maxPendingDataSize: settingsArg.maxPendingDataSize || 1024 * 1024, // 1MB
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
// Feature flags
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes || false,
enableProtocolDetection: settingsArg.enableProtocolDetection !== undefined ? settingsArg.enableProtocolDetection : true,
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || true,
// Rate limiting defaults
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
@ -332,115 +395,22 @@ export class PortProxy {
}
/**
* Get protocol-specific timeout based on connection type
* Get connection timeout based on domain config or default settings
*/
private getProtocolTimeout(record: IConnectionRecord, domainConfig?: IDomainConfig): number {
// If the protocol has a domain-specific timeout, use that
if (domainConfig) {
if (record.protocolType === 'http' && domainConfig.httpTimeout) {
return domainConfig.httpTimeout;
}
if (record.protocolType === 'websocket' && domainConfig.wsTimeout) {
return domainConfig.wsTimeout;
}
}
// Use HTTP keep-alive timeout from headers if available
if (record.httpKeepAliveTimeout) {
return record.httpKeepAliveTimeout;
private getConnectionTimeout(record: IConnectionRecord): number {
// If the connection has a domain-specific timeout, use that
if (record.domainConfig?.connectionTimeout) {
return record.domainConfig.connectionTimeout;
}
// Otherwise use default protocol-specific timeout
switch (record.protocolType) {
case 'http':
return this.settings.httpConnectionTimeout!;
case 'websocket':
return this.settings.wsConnectionTimeout!;
case 'https':
case 'tls':
return this.settings.httpConnectionTimeout!; // Use HTTP timeout for HTTPS by default
default:
return this.settings.maxConnectionLifetime!;
}
}
/**
* Detect protocol and update connection record
*/
private detectProtocol(data: Buffer, record: IConnectionRecord): void {
if (!this.settings.enableProtocolDetection || record.protocolType !== 'unknown') {
return;
}
try {
// Detect TLS/HTTPS
if (isTlsHandshake(data)) {
record.protocolType = 'tls';
if (this.settings.enableDetailedLogging) {
console.log(`[${record.id}] Protocol detected: TLS`);
}
return;
}
// Detect HTTP including WebSocket upgrades
if (isHttpRequest(data)) {
record.httpRequests++;
record.lastHttpRequest = Date.now();
// Check for WebSocket upgrade
if (isWebSocketUpgrade(data)) {
record.protocolType = 'websocket';
if (this.settings.enableDetailedLogging) {
console.log(`[${record.id}] Protocol detected: WebSocket Upgrade`);
}
} else {
record.protocolType = 'http';
// Parse HTTP keep-alive headers
this.parseHttpHeaders(data, record);
if (this.settings.enableDetailedLogging) {
console.log(`[${record.id}] Protocol detected: HTTP${record.isPooledConnection ? ' (KeepAlive)' : ''}`);
}
}
}
} catch (err) {
console.log(`[${record.id}] Error detecting protocol: ${err}`);
}
}
/**
* Parse HTTP headers for keep-alive and other connection info
*/
private parseHttpHeaders(data: Buffer, record: IConnectionRecord): void {
try {
const headerStr = data.toString('utf8', 0, Math.min(data.length, 1024));
// Check for HTTP keep-alive
const connectionHeader = headerStr.match(/\r\nConnection:\s*([^\r\n]+)/i);
if (connectionHeader && connectionHeader[1].toLowerCase().includes('keep-alive')) {
record.isPooledConnection = true;
// Check for Keep-Alive timeout value
const keepAliveHeader = headerStr.match(/\r\nKeep-Alive:\s*([^\r\n]+)/i);
if (keepAliveHeader) {
const timeoutMatch = keepAliveHeader[1].match(/timeout=(\d+)/i);
if (timeoutMatch && timeoutMatch[1]) {
const timeoutSec = parseInt(timeoutMatch[1], 10);
if (!isNaN(timeoutSec) && timeoutSec > 0) {
// Convert seconds to milliseconds and add some buffer
record.httpKeepAliveTimeout = (timeoutSec * 1000) + 5000;
if (this.settings.enableDetailedLogging) {
console.log(`[${record.id}] HTTP Keep-Alive timeout set to ${timeoutSec} seconds`);
}
}
}
}
}
} catch (err) {
console.log(`[${record.id}] Error parsing HTTP headers: ${err}`);
// Use default timeout, potentially randomized
const baseTimeout = this.settings.maxConnectionLifetime!;
if (this.settings.enableRandomizedTimeouts) {
return randomizeTimeout(baseTimeout);
}
return baseTimeout;
}
/**
@ -465,7 +435,6 @@ export class PortProxy {
const duration = Date.now() - record.incomingStartTime;
const bytesReceived = record.bytesReceived;
const bytesSent = record.bytesSent;
const httpRequests = record.httpRequests;
try {
if (!record.incoming.destroyed) {
@ -538,7 +507,7 @@ export class PortProxy {
if (this.settings.enableDetailedLogging) {
console.log(`[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` +
` Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
`HTTP Requests: ${httpRequests}, Protocol: ${record.protocolType}, Pooled: ${record.isPooledConnection}`);
`TLS: ${record.isTLS ? 'Yes' : 'No'}`);
} else {
console.log(`[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`);
}
@ -608,6 +577,21 @@ export class PortProxy {
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
}
// Apply enhanced TCP options if available
if (this.settings.enableKeepAliveProbes) {
try {
// These are platform-specific and may not be available
if ('setKeepAliveProbes' in socket) {
(socket as any).setKeepAliveProbes(10);
}
if ('setKeepAliveInterval' in socket) {
(socket as any).setKeepAliveInterval(1000);
}
} catch (err) {
// Ignore errors - these are optional enhancements
}
}
// Create a unique connection ID and record
const connectionId = generateConnectionId();
const connectionRecord: IConnectionRecord = {
@ -621,13 +605,13 @@ export class PortProxy {
pendingDataSize: 0,
// Initialize enhanced tracking fields
protocolType: 'unknown',
isPooledConnection: false,
bytesReceived: 0,
bytesSent: 0,
remoteIP: remoteIP,
localPort: localPort,
httpRequests: 0
isTLS: false,
tlsHandshakeComplete: false,
hasReceivedInitialData: false
};
// Track connection by IP
@ -685,9 +669,15 @@ export class PortProxy {
socket.end();
cleanupOnce();
}
}, this.settings.initialDataTimeout);
}, this.settings.initialDataTimeout!);
// Make sure timeout doesn't keep the process alive
if (initialTimeout.unref) {
initialTimeout.unref();
}
} else {
initialDataReceived = true;
connectionRecord.hasReceivedInitialData = true;
}
socket.on('error', (err: Error) => {
@ -699,39 +689,14 @@ export class PortProxy {
connectionRecord.bytesReceived += chunk.length;
this.updateActivity(connectionRecord);
// Detect protocol on first data chunk
if (connectionRecord.protocolType === 'unknown') {
this.detectProtocol(chunk, connectionRecord);
// Check for TLS handshake if this is the first chunk
if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
connectionRecord.isTLS = true;
// Update timeout based on protocol
if (connectionRecord.cleanupTimer) {
clearTimeout(connectionRecord.cleanupTimer);
// Set new timeout based on protocol
const protocolTimeout = this.getProtocolTimeout(connectionRecord);
connectionRecord.cleanupTimer = setTimeout(() => {
console.log(`[${connectionId}] ${connectionRecord.protocolType} connection timeout after ${plugins.prettyMs(protocolTimeout)}`);
initiateCleanupOnce(`${connectionRecord.protocolType}_timeout`);
}, protocolTimeout);
}
} else if (connectionRecord.protocolType === 'http' && isHttpRequest(chunk)) {
// Additional HTTP request on the same connection
connectionRecord.httpRequests++;
connectionRecord.lastHttpRequest = Date.now();
// Parse HTTP headers again for keep-alive changes
this.parseHttpHeaders(chunk, connectionRecord);
// Update timeout based on new HTTP headers
if (connectionRecord.cleanupTimer) {
clearTimeout(connectionRecord.cleanupTimer);
// Set new timeout based on updated HTTP info
const protocolTimeout = this.getProtocolTimeout(connectionRecord);
connectionRecord.cleanupTimer = setTimeout(() => {
console.log(`[${connectionId}] HTTP connection timeout after ${plugins.prettyMs(protocolTimeout)}`);
initiateCleanupOnce('http_timeout');
}, protocolTimeout);
if (this.settings.enableTlsDebugLogging) {
console.log(`[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`);
// Try to extract SNI and log detailed debug info
extractSNI(chunk, true);
}
}
});
@ -797,9 +762,17 @@ export class PortProxy {
initialTimeout = null;
}
// Detect protocol if initial chunk is available
if (initialChunk && this.settings.enableProtocolDetection) {
this.detectProtocol(initialChunk, connectionRecord);
// Mark that we've received initial data
initialDataReceived = true;
connectionRecord.hasReceivedInitialData = true;
// Check if this looks like a TLS handshake
if (initialChunk && isTlsHandshake(initialChunk)) {
connectionRecord.isTLS = true;
if (this.settings.enableTlsDebugLogging) {
console.log(`[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`);
}
}
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
@ -809,6 +782,9 @@ export class PortProxy {
config.domains.some(d => plugins.minimatch(serverName, d))
) : undefined);
// Save domain config in connection record
connectionRecord.domainConfig = domainConfig;
// IP validation is skipped if allowedIPs is empty
if (domainConfig) {
const effectiveAllowedIPs: string[] = [
@ -847,9 +823,13 @@ export class PortProxy {
// Track bytes received
connectionRecord.bytesReceived += chunk.length;
// Detect protocol even during connection setup
if (this.settings.enableProtocolDetection && connectionRecord.protocolType === 'unknown') {
this.detectProtocol(chunk, connectionRecord);
// Check for TLS handshake
if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
connectionRecord.isTLS = true;
if (this.settings.enableTlsDebugLogging) {
console.log(`[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`);
}
}
// Check if adding this chunk would exceed the buffer limit
@ -888,6 +868,20 @@ export class PortProxy {
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
}
// Apply enhanced TCP options if available
if (this.settings.enableKeepAliveProbes) {
try {
if ('setKeepAliveProbes' in targetSocket) {
(targetSocket as any).setKeepAliveProbes(10);
}
if ('setKeepAliveInterval' in targetSocket) {
(targetSocket as any).setKeepAliveInterval(1000);
}
} catch (err) {
// Ignore errors - these are optional enhancements
}
}
// Setup specific error handler for connection phase
targetSocket.once('error', (err) => {
// This handler runs only once during the initial connection phase
@ -928,7 +922,7 @@ export class PortProxy {
// Handle timeouts
socket.on('timeout', () => {
console.log(`[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 300000)}`);
console.log(`[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`);
if (incomingTerminationReason === null) {
incomingTerminationReason = 'timeout';
this.incrementTerminationStat('incoming', 'timeout');
@ -937,7 +931,7 @@ export class PortProxy {
});
targetSocket.on('timeout', () => {
console.log(`[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 300000)}`);
console.log(`[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`);
if (outgoingTerminationReason === null) {
outgoingTerminationReason = 'timeout';
this.incrementTerminationStat('outgoing', 'timeout');
@ -946,8 +940,8 @@ export class PortProxy {
});
// Set appropriate timeouts using the configured value
socket.setTimeout(this.settings.socketTimeout || 300000);
targetSocket.setTimeout(this.settings.socketTimeout || 300000);
socket.setTimeout(this.settings.socketTimeout || 3600000);
targetSocket.setTimeout(this.settings.socketTimeout || 3600000);
// Track outgoing data for bytes counting
targetSocket.on('data', (chunk: Buffer) => {
@ -984,7 +978,7 @@ export class PortProxy {
console.log(
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` +
` Protocol: ${connectionRecord.protocolType}`
` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
);
} else {
console.log(
@ -1003,7 +997,7 @@ export class PortProxy {
console.log(
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` +
` Protocol: ${connectionRecord.protocolType}`
` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
);
} else {
console.log(
@ -1023,7 +1017,7 @@ export class PortProxy {
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
try {
// Try to extract SNI from potential renegotiation
const newSNI = extractSNI(renegChunk);
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
if (newSNI && newSNI !== connectionRecord.lockedDomain) {
console.log(`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`);
initiateCleanupOnce('sni_mismatch');
@ -1037,17 +1031,31 @@ export class PortProxy {
});
}
// Set protocol-specific timeout based on detected protocol
// Set connection timeout
if (connectionRecord.cleanupTimer) {
clearTimeout(connectionRecord.cleanupTimer);
}
// Set timeout based on protocol
const protocolTimeout = this.getProtocolTimeout(connectionRecord, domainConfig);
// Set timeout based on domain config or default
const connectionTimeout = this.getConnectionTimeout(connectionRecord);
connectionRecord.cleanupTimer = setTimeout(() => {
console.log(`[${connectionId}] ${connectionRecord.protocolType} connection exceeded max lifetime (${plugins.prettyMs(protocolTimeout)}), forcing cleanup.`);
initiateCleanupOnce(`${connectionRecord.protocolType}_max_lifetime`);
}, protocolTimeout);
console.log(`[${connectionId}] Connection from ${remoteIP} exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`);
initiateCleanupOnce('connection_timeout');
}, connectionTimeout);
// Make sure timeout doesn't keep the process alive
if (connectionRecord.cleanupTimer.unref) {
connectionRecord.cleanupTimer.unref();
}
// Mark TLS handshake as complete for TLS connections
if (connectionRecord.isTLS) {
connectionRecord.tlsHandshakeComplete = true;
if (this.settings.enableTlsDebugLogging) {
console.log(`[${connectionId}] TLS handshake complete for connection from ${remoteIP}`);
}
}
});
};
@ -1055,7 +1063,7 @@ export class PortProxy {
// Only apply port-based rules if the incoming port is within one of the global port ranges.
if (this.settings.globalPortRanges && isPortInRanges(localPort, this.settings.globalPortRanges)) {
if (this.settings.forwardAllGlobalRanges) {
if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0 && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
console.log(`[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
socket.end();
return;
@ -1111,7 +1119,20 @@ export class PortProxy {
}
initialDataReceived = true;
const serverName = extractSNI(chunk) || '';
// Try to extract SNI
let serverName = '';
if (isTlsHandshake(chunk)) {
connectionRecord.isTLS = true;
if (this.settings.enableTlsDebugLogging) {
console.log(`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`);
}
serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
}
// Lock the connection to the negotiated SNI.
connectionRecord.lockedDomain = serverName;
@ -1123,9 +1144,12 @@ export class PortProxy {
});
} else {
initialDataReceived = true;
if (!this.settings.defaultAllowedIPs || this.settings.defaultAllowedIPs.length === 0 || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
connectionRecord.hasReceivedInitialData = true;
if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0 && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
}
setupConnection('');
}
};
@ -1167,11 +1191,10 @@ export class PortProxy {
const now = Date.now();
let maxIncoming = 0;
let maxOutgoing = 0;
let httpConnections = 0;
let wsConnections = 0;
let tlsConnections = 0;
let unknownConnections = 0;
let pooledConnections = 0;
let nonTlsConnections = 0;
let completedTlsHandshakes = 0;
let pendingTlsHandshakes = 0;
// Create a copy of the keys to avoid modification during iteration
const connectionIds = [...this.connectionRecords.keys()];
@ -1180,17 +1203,16 @@ export class PortProxy {
const record = this.connectionRecords.get(id);
if (!record) continue;
// Track connection stats by protocol
switch (record.protocolType) {
case 'http': httpConnections++; break;
case 'websocket': wsConnections++; break;
case 'tls':
case 'https': tlsConnections++; break;
default: unknownConnections++; break;
}
if (record.isPooledConnection) {
pooledConnections++;
// Track connection stats
if (record.isTLS) {
tlsConnections++;
if (record.tlsHandshakeComplete) {
completedTlsHandshakes++;
} else {
pendingTlsHandshakes++;
}
} else {
nonTlsConnections++;
}
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
@ -1202,29 +1224,26 @@ export class PortProxy {
if (record.outgoingClosedTime &&
!record.incoming.destroyed &&
!record.connectionClosed &&
(now - record.outgoingClosedTime > 30000)) {
(now - record.outgoingClosedTime > 120000)) {
const remoteIP = record.remoteIP;
console.log(`[${id}] Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(now - record.outgoingClosedTime)} after outgoing closed.`);
this.cleanupConnection(record, 'parity_check');
}
// Check for stalled connections waiting for initial data
if (!record.hasReceivedInitialData &&
(now - record.incomingStartTime > this.settings.initialDataTimeout! / 2)) {
console.log(`[${id}] Warning: Connection from ${record.remoteIP} has not received initial data after ${plugins.prettyMs(now - record.incomingStartTime)}`);
}
// Skip inactivity check if disabled
if (!this.settings.disableInactivityCheck) {
// Inactivity check - use protocol-specific values
let inactivityThreshold = Math.floor(Math.random() * (1800000 - 1200000 + 1)) + 1200000; // random between 20 and 30 minutes
// Set protocol-specific inactivity thresholds
if (record.protocolType === 'http' && record.isPooledConnection) {
inactivityThreshold = this.settings.httpKeepAliveTimeout || 1200000; // 20 minutes for pooled HTTP
} else if (record.protocolType === 'websocket') {
inactivityThreshold = this.settings.wsConnectionTimeout || 14400000; // 4 hours for WebSocket
} else if (record.protocolType === 'http') {
inactivityThreshold = this.settings.httpConnectionTimeout || 1800000; // 30 minutes for HTTP
}
// Inactivity check with configurable timeout
const inactivityThreshold = this.settings.inactivityTimeout!;
const inactivityTime = now - record.lastActivity;
if (inactivityTime > inactivityThreshold && !record.connectionClosed) {
console.log(`[${id}] Inactivity check: No activity on ${record.protocolType} connection from ${record.remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
console.log(`[${id}] Inactivity check: No activity on connection from ${record.remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
this.cleanupConnection(record, 'inactivity');
}
}
@ -1233,11 +1252,11 @@ export class PortProxy {
// Log detailed stats periodically
console.log(
`Active connections: ${this.connectionRecords.size}. ` +
`Types: HTTP=${httpConnections}, WS=${wsConnections}, TLS=${tlsConnections}, Unknown=${unknownConnections}, Pooled=${pooledConnections}. ` +
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), Non-TLS=${nonTlsConnections}. ` +
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` +
`Termination stats: ${JSON.stringify({IN: this.terminationStats.incoming, OUT: this.terminationStats.outgoing})}`
);
}, this.settings.inactivityCheckInterval || 30000);
}, this.settings.inactivityCheckInterval || 60000);
// Make sure the interval doesn't keep the process alive
if (this.connectionLogger.unref) {