Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
0574331b91 | |||
06e6c2eb52 | |||
edd9db31c2 | |||
d4251b2cf9 | |||
4ccc1db8a2 | |||
7e3ed93bc9 | |||
fa793f2c4a | |||
fe8106f0c8 | |||
b317ab8b3a | |||
4fd5524a0f |
39
changelog.md
39
changelog.md
@ -1,5 +1,44 @@
|
|||||||
# 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)
|
||||||
|
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
|
||||||
|
|
||||||
|
- Modified inactivity threshold calculation within PortProxy to use a random value between 1.2 and 1.8 million milliseconds.
|
||||||
|
|
||||||
## 2025-03-05 - 3.25.0 - feat(PortProxy)
|
## 2025-03-05 - 3.25.0 - feat(PortProxy)
|
||||||
Enhanced PortProxy with detailed logging, protocol detection, and rate limiting.
|
Enhanced PortProxy with detailed logging, protocol detection, and rate limiting.
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "3.25.0",
|
"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
126
readme.md
@ -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.
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '3.25.0',
|
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.'
|
||||||
}
|
}
|
||||||
|
@ -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,30 +308,27 @@ 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 || 15000, // 15 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
|
||||||
|
|
||||||
// Socket optimization settings
|
// Socket optimization settings
|
||||||
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 || 60000, // 1 minute
|
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);
|
||||||
@ -1202,29 +1224,26 @@ export class PortProxy {
|
|||||||
if (record.outgoingClosedTime &&
|
if (record.outgoingClosedTime &&
|
||||||
!record.incoming.destroyed &&
|
!record.incoming.destroyed &&
|
||||||
!record.connectionClosed &&
|
!record.connectionClosed &&
|
||||||
(now - record.outgoingClosedTime > 30000)) {
|
(now - record.outgoingClosedTime > 120000)) {
|
||||||
const remoteIP = record.remoteIP;
|
const remoteIP = record.remoteIP;
|
||||||
console.log(`[${id}] Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(now - record.outgoingClosedTime)} after outgoing closed.`);
|
console.log(`[${id}] Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(now - record.outgoingClosedTime)} after outgoing closed.`);
|
||||||
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 = 180000; // 3 minutes default
|
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) {
|
||||||
|
Reference in New Issue
Block a user