Compare commits

...

4 Commits

5 changed files with 463 additions and 338 deletions

View File

@ -1,5 +1,21 @@
# Changelog # 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) ## 2025-03-05 - 3.25.3 - fix(core)
Update dependencies and configuration improvements. Update dependencies and configuration improvements.

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartproxy", "name": "@push.rocks/smartproxy",
"version": "3.25.3", "version": "3.26.0",
"private": false, "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.", "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", "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 - **HTTPS Reverse Proxy** - Route traffic to backend services based on hostname with TLS termination
- **WebSocket Support** - Full WebSocket proxying with heartbeat monitoring - **WebSocket Support** - Full WebSocket proxying with heartbeat monitoring
- **TCP Port Forwarding** - Advanced port forwarding with SNI inspection and domain-based routing - **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 - **HTTP to HTTPS Redirection** - Automatically redirect HTTP requests to HTTPS
- **Let's Encrypt Integration** - Automatic certificate management using ACME protocol - **Let's Encrypt Integration** - Automatic certificate management using ACME protocol
- **IP Filtering** - Control access with IP allow/block lists using glob patterns - **IP Filtering** - Control access with IP allow/block lists using glob patterns
- **IPTables Integration** - Direct manipulation of iptables for low-level port forwarding - **IPTables Integration** - Direct manipulation of iptables for low-level port forwarding
- **Basic Authentication** - Support for basic auth on proxied routes - **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 ## Installation
@ -275,18 +277,38 @@ const portProxy = new PortProxy({
toPort: 8443, toPort: 8443,
targetIP: 'localhost', // Default target host targetIP: 'localhost', // Default target host
sniEnabled: true, // Enable SNI inspection 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 }], globalPortRanges: [{ from: 443, to: 443 }],
defaultAllowedIPs: ['*'], // Allow all IPs by default 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: [ domainConfigs: [
{ {
domains: ['example.com', '*.example.com'], // Glob patterns for matching domains domains: ['example.com', '*.example.com'], // Glob patterns for matching domains
allowedIPs: ['192.168.1.*'], // Restrict access by IP allowedIPs: ['192.168.1.*'], // Restrict access by IP
blockedIPs: ['192.168.1.100'], // Block specific IPs blockedIPs: ['192.168.1.100'], // Block specific IPs
targetIPs: ['10.0.0.1', '10.0.0.2'], // Round-robin between multiple targets 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 preserveSourceIP: true
}); });
@ -333,19 +355,31 @@ acmeHandler.addDomain('api.example.com');
### PortProxy Settings ### PortProxy Settings
| Option | Description | Default | | Option | Description | Default |
|--------------------------|--------------------------------------------------------|-------------| |---------------------------|--------------------------------------------------------|-------------|
| `fromPort` | Port to listen on | - | | `fromPort` | Port to listen on | - |
| `toPort` | Destination port to forward to | - | | `toPort` | Destination port to forward to | - |
| `targetIP` | Default destination IP if not specified in domainConfig | 'localhost' | | `targetIP` | Default destination IP if not specified in domainConfig | 'localhost' |
| `sniEnabled` | Enable SNI inspection for TLS connections | false | | `sniEnabled` | Enable SNI inspection for TLS connections | false |
| `defaultAllowedIPs` | IP patterns allowed by default | - | | `defaultAllowedIPs` | IP patterns allowed by default | - |
| `defaultBlockedIPs` | IP patterns blocked by default | - | | `defaultBlockedIPs` | IP patterns blocked by default | - |
| `preserveSourceIP` | Preserve the original client IP | false | | `preserveSourceIP` | Preserve the original client IP | false |
| `maxConnectionLifetime` | Maximum time in ms to keep a connection open | 600000 | | `maxConnectionLifetime` | Maximum time in ms to keep a connection open | 3600000 |
| `globalPortRanges` | Array of port ranges to listen on | - | | `initialDataTimeout` | Timeout for initial data/handshake in ms | 60000 |
| `forwardAllGlobalRanges` | Forward all global range connections to targetIP | false | | `socketTimeout` | Socket inactivity timeout in ms | 3600000 |
| `gracefulShutdownTimeout`| Time in ms to wait during shutdown | 30000 | | `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 ### IPTablesProxy Settings
@ -359,14 +393,37 @@ acmeHandler.addDomain('api.example.com');
## Advanced Features ## 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 ### Connection Management and Monitoring
The `PortProxy` class includes built-in connection tracking 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 - Timeouts for connections that exceed maximum lifetime
- Detailed logging of connection states - Detailed logging of connection states
- Termination statistics - Termination statistics
- Randomized timeouts to prevent "thundering herd" problems
- Per-domain timeout configuration
### WebSocket Support ### WebSocket Support
@ -385,6 +442,39 @@ The `PortProxy` class can inspect the SNI (Server Name Indication) field in TLS
- Domain-specific allowed IP ranges - Domain-specific allowed IP ranges
- Protection against SNI renegotiation attacks - 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 ## 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. 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. 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 = { export const commitinfo = {
name: '@push.rocks/smartproxy', name: '@push.rocks/smartproxy',
version: '3.25.3', 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.' 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 blockedIPs?: string[]; // Glob patterns for blocked IPs
targetIPs?: string[]; // If multiple targetIPs are given, use round robin. targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
// Protocol-specific timeout overrides // Allow domain-specific timeout override
httpTimeout?: number; // HTTP connection timeout override (ms) connectionTimeout?: number; // Connection timeout override (ms)
wsTimeout?: number; // WebSocket 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 */ /** Port proxy settings including global allowed port ranges */
export interface IPortProxySettings extends plugins.tls.TlsOptions { export interface IPortProxySettings extends plugins.tls.TlsOptions {
fromPort: number; fromPort: number;
@ -26,40 +22,37 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
defaultBlockedIPs?: string[]; defaultBlockedIPs?: string[];
preserveSourceIP?: boolean; preserveSourceIP?: boolean;
// Updated timeout settings with better defaults // Timeout settings
initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 15000 (15s) initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
socketTimeout?: number; // Socket inactivity timeout (ms), default: 300000 (5m) socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 30000 (30s) 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 gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
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
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges 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 // Socket optimization settings
noDelay?: boolean; // Disable Nagle's algorithm (default: true) noDelay?: boolean; // Disable Nagle's algorithm (default: true)
keepAlive?: boolean; // Enable TCP keepalive (default: true) keepAlive?: boolean; // Enable TCP keepalive (default: true)
keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms) keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
// Enable enhanced features // Enhanced features
disableInactivityCheck?: boolean; // Disable inactivity checking entirely disableInactivityCheck?: boolean; // Disable inactivity checking entirely
enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
enableProtocolDetection?: boolean; // Enable HTTP/WebSocket protocol detection enableDetailedLogging?: boolean; // Enable detailed connection logging
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 // 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 connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
} }
/** /**
* Enhanced connection record with protocol-specific handling * Enhanced connection record
*/ */
interface IConnectionRecord { interface IConnectionRecord {
id: string; // Unique connection identifier id: string; // Unique connection identifier
@ -76,78 +69,161 @@ interface IConnectionRecord {
pendingDataSize: number; // Track total size of pending data pendingDataSize: number; // Track total size of pending data
// Enhanced tracking fields // 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 bytesReceived: number; // Total bytes received
bytesSent: number; // Total bytes sent bytesSent: number; // Total bytes sent
remoteIP: string; // Remote IP (cached for logging after socket close) remoteIP: string; // Remote IP (cached for logging after socket close)
localPort: number; // Local port (cached for logging) 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. * 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 buffer - Buffer containing the TLS ClientHello.
* @param enableLogging - Whether to enable detailed logging.
* @returns The server name if found, otherwise undefined. * @returns The server name if found, otherwise undefined.
*/ */
function extractSNI(buffer: Buffer): string | undefined { function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
let offset = 0; try {
if (buffer.length < 5) return undefined; // Check if buffer is too small for TLS
if (buffer.length < 5) {
const recordType = buffer.readUInt8(0); if (enableLogging) console.log("Buffer too small for TLS header");
if (recordType !== 22) return undefined; // 22 = handshake return undefined;
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;
} }
// 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 // 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 // Helper: Check if a given IP matches any of the glob patterns
const isAllowed = (ip: string, patterns: string[]): boolean => { const isAllowed = (ip: string, patterns: string[]): boolean => {
if (!ip || !patterns || patterns.length === 0) return false;
const normalizeIP = (ip: string): string[] => { const normalizeIP = (ip: string): string[] => {
if (!ip) return [];
if (ip.startsWith('::ffff:')) { if (ip.startsWith('::ffff:')) {
const ipv4 = ip.slice(7); const ipv4 = ip.slice(7);
return [ip, ipv4]; return [ip, ipv4];
@ -167,7 +246,10 @@ const isAllowed = (ip: string, patterns: string[]): boolean => {
} }
return [ip]; return [ip];
}; };
const normalizedIPVariants = normalizeIP(ip); const normalizedIPVariants = normalizeIP(ip);
if (normalizedIPVariants.length === 0) return false;
const expandedPatterns = patterns.flatMap(normalizeIP); const expandedPatterns = patterns.flatMap(normalizeIP);
return normalizedIPVariants.some(ipVariant => return normalizedIPVariants.some(ipVariant =>
expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern)) 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 // Helper: Check if an IP is allowed considering allowed and blocked glob patterns
const isGlobIPAllowed = (ip: string, allowed: string[], blocked: string[] = []): boolean => { const isGlobIPAllowed = (ip: string, allowed: string[], blocked: string[] = []): boolean => {
if (!ip) return false;
if (blocked.length > 0 && isAllowed(ip, blocked)) return false; if (blocked.length > 0 && isAllowed(ip, blocked)) return false;
return isAllowed(ip, allowed); 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); return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}; };
// Protocol detection helpers // Helper: Check if a buffer contains a TLS handshake
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')
);
};
const isTlsHandshake = (buffer: Buffer): boolean => { const isTlsHandshake = (buffer: Buffer): boolean => {
return buffer.length > 0 && buffer[0] === 22; // ContentType.handshake 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 { export class PortProxy {
private netServers: plugins.net.Server[] = []; private netServers: plugins.net.Server[] = [];
settings: IPortProxySettings; settings: IPortProxySettings;
@ -242,16 +308,12 @@ export class PortProxy {
...settingsArg, ...settingsArg,
targetIP: settingsArg.targetIP || 'localhost', targetIP: settingsArg.targetIP || 'localhost',
// Timeout settings with browser-friendly defaults // Timeout settings with our enhanced defaults
initialDataTimeout: settingsArg.initialDataTimeout || 30000, // 30 seconds initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial data
socketTimeout: settingsArg.socketTimeout || 300000, // 5 minutes socketTimeout: settingsArg.socketTimeout || 3600000, // 1 hour socket timeout
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 30000, // 30 seconds inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 3600000, // 1 hour default lifetime
// Protocol-specific timeouts inactivityTimeout: settingsArg.inactivityTimeout || 3600000, // 1 hour inactivity timeout
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
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
@ -259,13 +321,14 @@ export class PortProxy {
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true, noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true, keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds
maxPendingDataSize: settingsArg.maxPendingDataSize || 1024 * 1024, // 1MB maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
// Feature flags // Feature flags
disableInactivityCheck: settingsArg.disableInactivityCheck || false, disableInactivityCheck: settingsArg.disableInactivityCheck || false,
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes || false, enableKeepAliveProbes: settingsArg.enableKeepAliveProbes || false,
enableProtocolDetection: settingsArg.enableProtocolDetection !== undefined ? settingsArg.enableProtocolDetection : true,
enableDetailedLogging: settingsArg.enableDetailedLogging || false, enableDetailedLogging: settingsArg.enableDetailedLogging || false,
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || true,
// Rate limiting defaults // Rate limiting defaults
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP 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 { private getConnectionTimeout(record: IConnectionRecord): number {
// If the protocol has a domain-specific timeout, use that // If the connection has a domain-specific timeout, use that
if (domainConfig) { if (record.domainConfig?.connectionTimeout) {
if (record.protocolType === 'http' && domainConfig.httpTimeout) { return record.domainConfig.connectionTimeout;
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;
} }
// Otherwise use default protocol-specific timeout // Use default timeout, potentially randomized
switch (record.protocolType) { const baseTimeout = this.settings.maxConnectionLifetime!;
case 'http':
return this.settings.httpConnectionTimeout!; if (this.settings.enableRandomizedTimeouts) {
case 'websocket': return randomizeTimeout(baseTimeout);
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}`);
} }
return baseTimeout;
} }
/** /**
@ -465,7 +435,6 @@ export class PortProxy {
const duration = Date.now() - record.incomingStartTime; const duration = Date.now() - record.incomingStartTime;
const bytesReceived = record.bytesReceived; const bytesReceived = record.bytesReceived;
const bytesSent = record.bytesSent; const bytesSent = record.bytesSent;
const httpRequests = record.httpRequests;
try { try {
if (!record.incoming.destroyed) { if (!record.incoming.destroyed) {
@ -538,7 +507,7 @@ export class PortProxy {
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log(`[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` + console.log(`[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` +
` Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` + ` Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
`HTTP Requests: ${httpRequests}, Protocol: ${record.protocolType}, Pooled: ${record.isPooledConnection}`); `TLS: ${record.isTLS ? 'Yes' : 'No'}`);
} else { } else {
console.log(`[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`); 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); 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 // Create a unique connection ID and record
const connectionId = generateConnectionId(); const connectionId = generateConnectionId();
const connectionRecord: IConnectionRecord = { const connectionRecord: IConnectionRecord = {
@ -621,13 +605,13 @@ export class PortProxy {
pendingDataSize: 0, pendingDataSize: 0,
// Initialize enhanced tracking fields // Initialize enhanced tracking fields
protocolType: 'unknown',
isPooledConnection: false,
bytesReceived: 0, bytesReceived: 0,
bytesSent: 0, bytesSent: 0,
remoteIP: remoteIP, remoteIP: remoteIP,
localPort: localPort, localPort: localPort,
httpRequests: 0 isTLS: false,
tlsHandshakeComplete: false,
hasReceivedInitialData: false
}; };
// Track connection by IP // Track connection by IP
@ -685,9 +669,15 @@ export class PortProxy {
socket.end(); socket.end();
cleanupOnce(); cleanupOnce();
} }
}, this.settings.initialDataTimeout); }, this.settings.initialDataTimeout!);
// Make sure timeout doesn't keep the process alive
if (initialTimeout.unref) {
initialTimeout.unref();
}
} else { } else {
initialDataReceived = true; initialDataReceived = true;
connectionRecord.hasReceivedInitialData = true;
} }
socket.on('error', (err: Error) => { socket.on('error', (err: Error) => {
@ -699,39 +689,14 @@ export class PortProxy {
connectionRecord.bytesReceived += chunk.length; connectionRecord.bytesReceived += chunk.length;
this.updateActivity(connectionRecord); this.updateActivity(connectionRecord);
// Detect protocol on first data chunk // Check for TLS handshake if this is the first chunk
if (connectionRecord.protocolType === 'unknown') { if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
this.detectProtocol(chunk, connectionRecord); connectionRecord.isTLS = true;
// Update timeout based on protocol if (this.settings.enableTlsDebugLogging) {
if (connectionRecord.cleanupTimer) { console.log(`[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`);
clearTimeout(connectionRecord.cleanupTimer); // Try to extract SNI and log detailed debug info
extractSNI(chunk, true);
// 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);
} }
} }
}); });
@ -797,9 +762,17 @@ export class PortProxy {
initialTimeout = null; initialTimeout = null;
} }
// Detect protocol if initial chunk is available // Mark that we've received initial data
if (initialChunk && this.settings.enableProtocolDetection) { initialDataReceived = true;
this.detectProtocol(initialChunk, connectionRecord); 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. // 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)) config.domains.some(d => plugins.minimatch(serverName, d))
) : undefined); ) : undefined);
// Save domain config in connection record
connectionRecord.domainConfig = domainConfig;
// IP validation is skipped if allowedIPs is empty // IP validation is skipped if allowedIPs is empty
if (domainConfig) { if (domainConfig) {
const effectiveAllowedIPs: string[] = [ const effectiveAllowedIPs: string[] = [
@ -847,9 +823,13 @@ export class PortProxy {
// Track bytes received // Track bytes received
connectionRecord.bytesReceived += chunk.length; connectionRecord.bytesReceived += chunk.length;
// Detect protocol even during connection setup // Check for TLS handshake
if (this.settings.enableProtocolDetection && connectionRecord.protocolType === 'unknown') { if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
this.detectProtocol(chunk, connectionRecord); 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 // Check if adding this chunk would exceed the buffer limit
@ -888,6 +868,20 @@ export class PortProxy {
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay); 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 // Setup specific error handler for connection phase
targetSocket.once('error', (err) => { targetSocket.once('error', (err) => {
// This handler runs only once during the initial connection phase // This handler runs only once during the initial connection phase
@ -928,7 +922,7 @@ export class PortProxy {
// Handle timeouts // Handle timeouts
socket.on('timeout', () => { 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) { if (incomingTerminationReason === null) {
incomingTerminationReason = 'timeout'; incomingTerminationReason = 'timeout';
this.incrementTerminationStat('incoming', 'timeout'); this.incrementTerminationStat('incoming', 'timeout');
@ -937,7 +931,7 @@ export class PortProxy {
}); });
targetSocket.on('timeout', () => { 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) { if (outgoingTerminationReason === null) {
outgoingTerminationReason = 'timeout'; outgoingTerminationReason = 'timeout';
this.incrementTerminationStat('outgoing', 'timeout'); this.incrementTerminationStat('outgoing', 'timeout');
@ -946,8 +940,8 @@ export class PortProxy {
}); });
// Set appropriate timeouts using the configured value // Set appropriate timeouts using the configured value
socket.setTimeout(this.settings.socketTimeout || 300000); socket.setTimeout(this.settings.socketTimeout || 3600000);
targetSocket.setTimeout(this.settings.socketTimeout || 300000); targetSocket.setTimeout(this.settings.socketTimeout || 3600000);
// Track outgoing data for bytes counting // Track outgoing data for bytes counting
targetSocket.on('data', (chunk: Buffer) => { targetSocket.on('data', (chunk: Buffer) => {
@ -984,7 +978,7 @@ export class PortProxy {
console.log( console.log(
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` + `[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` + `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` +
` Protocol: ${connectionRecord.protocolType}` ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
); );
} else { } else {
console.log( console.log(
@ -1003,7 +997,7 @@ export class PortProxy {
console.log( console.log(
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` + `[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` + `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` +
` Protocol: ${connectionRecord.protocolType}` ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
); );
} else { } else {
console.log( console.log(
@ -1023,7 +1017,7 @@ export class PortProxy {
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) { if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
try { try {
// Try to extract SNI from potential renegotiation // Try to extract SNI from potential renegotiation
const newSNI = extractSNI(renegChunk); const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
if (newSNI && newSNI !== connectionRecord.lockedDomain) { if (newSNI && newSNI !== connectionRecord.lockedDomain) {
console.log(`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`); console.log(`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`);
initiateCleanupOnce('sni_mismatch'); initiateCleanupOnce('sni_mismatch');
@ -1037,17 +1031,31 @@ export class PortProxy {
}); });
} }
// Set protocol-specific timeout based on detected protocol // Set connection timeout
if (connectionRecord.cleanupTimer) { if (connectionRecord.cleanupTimer) {
clearTimeout(connectionRecord.cleanupTimer); clearTimeout(connectionRecord.cleanupTimer);
} }
// Set timeout based on protocol // Set timeout based on domain config or default
const protocolTimeout = this.getProtocolTimeout(connectionRecord, domainConfig); const connectionTimeout = this.getConnectionTimeout(connectionRecord);
connectionRecord.cleanupTimer = setTimeout(() => { connectionRecord.cleanupTimer = setTimeout(() => {
console.log(`[${connectionId}] ${connectionRecord.protocolType} connection exceeded max lifetime (${plugins.prettyMs(protocolTimeout)}), forcing cleanup.`); console.log(`[${connectionId}] Connection from ${remoteIP} exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`);
initiateCleanupOnce(`${connectionRecord.protocolType}_max_lifetime`); initiateCleanupOnce('connection_timeout');
}, protocolTimeout); }, 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. // 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.globalPortRanges && isPortInRanges(localPort, this.settings.globalPortRanges)) {
if (this.settings.forwardAllGlobalRanges) { 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.`); console.log(`[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
socket.end(); socket.end();
return; return;
@ -1111,7 +1119,20 @@ export class PortProxy {
} }
initialDataReceived = true; 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. // Lock the connection to the negotiated SNI.
connectionRecord.lockedDomain = serverName; connectionRecord.lockedDomain = serverName;
@ -1123,9 +1144,12 @@ export class PortProxy {
}); });
} else { } else {
initialDataReceived = true; 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`); return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
} }
setupConnection(''); setupConnection('');
} }
}; };
@ -1167,11 +1191,10 @@ export class PortProxy {
const now = Date.now(); const now = Date.now();
let maxIncoming = 0; let maxIncoming = 0;
let maxOutgoing = 0; let maxOutgoing = 0;
let httpConnections = 0;
let wsConnections = 0;
let tlsConnections = 0; let tlsConnections = 0;
let unknownConnections = 0; let nonTlsConnections = 0;
let pooledConnections = 0; let completedTlsHandshakes = 0;
let pendingTlsHandshakes = 0;
// Create a copy of the keys to avoid modification during iteration // Create a copy of the keys to avoid modification during iteration
const connectionIds = [...this.connectionRecords.keys()]; const connectionIds = [...this.connectionRecords.keys()];
@ -1180,17 +1203,16 @@ export class PortProxy {
const record = this.connectionRecords.get(id); const record = this.connectionRecords.get(id);
if (!record) continue; if (!record) continue;
// Track connection stats by protocol // Track connection stats
switch (record.protocolType) { if (record.isTLS) {
case 'http': httpConnections++; break; tlsConnections++;
case 'websocket': wsConnections++; break; if (record.tlsHandshakeComplete) {
case 'tls': completedTlsHandshakes++;
case 'https': tlsConnections++; break; } else {
default: unknownConnections++; break; pendingTlsHandshakes++;
} }
} else {
if (record.isPooledConnection) { nonTlsConnections++;
pooledConnections++;
} }
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime); maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
@ -1208,23 +1230,20 @@ export class PortProxy {
this.cleanupConnection(record, 'parity_check'); 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 // Skip inactivity check if disabled
if (!this.settings.disableInactivityCheck) { if (!this.settings.disableInactivityCheck) {
// Inactivity check - use protocol-specific values // Inactivity check with configurable timeout
let inactivityThreshold = Math.floor(Math.random() * (1800000 - 1200000 + 1)) + 1200000; // random between 20 and 30 minutes const inactivityThreshold = this.settings.inactivityTimeout!;
// 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
}
const inactivityTime = now - record.lastActivity; const inactivityTime = now - record.lastActivity;
if (inactivityTime > inactivityThreshold && !record.connectionClosed) { 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'); this.cleanupConnection(record, 'inactivity');
} }
} }
@ -1233,11 +1252,11 @@ export class PortProxy {
// Log detailed stats periodically // Log detailed stats periodically
console.log( console.log(
`Active connections: ${this.connectionRecords.size}. ` + `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)}. ` + `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` +
`Termination stats: ${JSON.stringify({IN: this.terminationStats.incoming, OUT: this.terminationStats.outgoing})}` `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 // Make sure the interval doesn't keep the process alive
if (this.connectionLogger.unref) { if (this.connectionLogger.unref) {