Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
0674ca7163 | |||
e31c84493f | |||
d2ad659d37 | |||
df7a12041e | |||
2b69150545 | |||
85cc57ae10 | |||
e021b66898 | |||
865d21b36a | |||
58ba0d9362 | |||
ccccc5b8c8 | |||
d8466a866c | |||
119b643690 | |||
98f1e0df4c | |||
d6022c8f8a | |||
0ea0f02428 | |||
e452f55203 | |||
55f25f1976 | |||
98b7f3ed7f | |||
cb83caeafd | |||
7850a80452 | |||
ef8f583a90 | |||
2bdd6f8c1f | |||
99d28eafd1 | |||
788b444fcc | |||
4225abe3c4 | |||
74fdb58f84 | |||
bffdaffe39 | |||
67a4228518 | |||
681209f2e1 | |||
c415a6c361 | |||
009e3c4f0e | |||
f9c42975dc | |||
feef949afe | |||
8d3b07b1e6 |
125
changelog.md
125
changelog.md
@ -1,5 +1,130 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.34.0 - feat(core)
|
||||||
|
Improve wildcard domain matching and enhance NetworkProxy integration in PortProxy. Added support for TLD wildcards and complex wildcard patterns in the router, and refactored TLS renegotiation handling for stricter SNI enforcement.
|
||||||
|
|
||||||
|
- Added support for TLD wildcard matching (e.g., 'example.*') to improve domain routing.
|
||||||
|
- Implemented complex wildcard pattern matching (e.g., '*.lossless*') in the router.
|
||||||
|
- Enhanced NetworkProxy integration by initializing a single NetworkProxy instance and forwarding TLS connections accordingly.
|
||||||
|
- Refactored TLS renegotiation handling to terminate connections on SNI mismatch for stricter enforcement.
|
||||||
|
- Updated tests to cover the new wildcard matching scenarios.
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.33.0 - feat(portproxy)
|
||||||
|
Add browser-friendly mode and SNI renegotiation configuration options to PortProxy
|
||||||
|
|
||||||
|
- Introduce new properties: browserFriendlyMode (default true) to optimize handling for browser connections.
|
||||||
|
- Add allowRenegotiationWithDifferentSNI (default false) to enable or disable SNI changes during renegotiation.
|
||||||
|
- Include relatedDomainPatterns to define patterns for related domains that can share connections.
|
||||||
|
- Update TypeScript interfaces and internal renegotiation logic to support these options.
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.32.2 - fix(PortProxy)
|
||||||
|
Simplify TLS handshake SNI extraction and update timeout settings in PortProxy for improved maintainability and reliability.
|
||||||
|
|
||||||
|
- Removed legacy and deprecated fields related to chained proxy configurations (isChainedProxy, chainPosition, aggressiveTlsRefresh).
|
||||||
|
- Refactored the extractSNI functions to use a simpler, more robust approach for TLS ClientHello processing.
|
||||||
|
- Adjusted default timeout and keep-alive settings to more standard values (e.g. initialDataTimeout set to 60s, socketTimeout to 1h).
|
||||||
|
- Eliminated redundant TLS session cache and deep TLS refresh logic.
|
||||||
|
- Improved logging and error handling during connection setup and renegotiation phases.
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.32.1 - fix(portproxy)
|
||||||
|
Relax TLS handshake and connection timeout settings for improved stability in chained proxy scenarios; update TLS session cache defaults and add keep-alive flags to connection records.
|
||||||
|
|
||||||
|
- Increased TLS session cache maximum entries from 10,000 to 20,000, expiry time from 24 hours to 7 days, and cleanup interval from 10 minutes to 30 minutes
|
||||||
|
- Relaxed socket timeouts: standalone connections now use up to 6 hours, with chained proxies adjusted for 5–6 hours based on proxy position
|
||||||
|
- Updated inactivity, connection, and initial handshake timeouts to provide a more relaxed behavior under high-traffic chained proxy scenarios
|
||||||
|
- Increased keepAliveInitialDelay from 10 seconds to 30 seconds and introduced separate incoming and outgoing keep-alive flags
|
||||||
|
- Enhanced TLS renegotiation handling with more detailed logging and temporary processing flags to avoid duplicate processing
|
||||||
|
- Updated NetworkProxy integration to use optimized connection settings and more aggressive application-level keep-alive probes
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.32.0 - feat(PortProxy)
|
||||||
|
Enhance TLS session cache, SNI extraction, and chained proxy support in PortProxy. Improve handling of multiple and fragmented TLS records, and add new configuration options (isChainedProxy, chainPosition, aggressiveTlsRefresh, tlsSessionCache) for robust TLS certificate refresh.
|
||||||
|
|
||||||
|
- Implement TlsSessionCache with configurable cleanup, eviction, and statistics.
|
||||||
|
- Improve extractSNIInfo to process multiple TLS records and partial handshake data.
|
||||||
|
- Add new settings to detect chained proxy scenarios and adjust timeouts accordingly.
|
||||||
|
- Enhance TLS state refresh with aggressive probing and deep refresh sequence.
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.31.2 - fix(PortProxy)
|
||||||
|
Improve SNI renegotiation handling by adding flexible domain configuration matching on rehandshake and session resumption events.
|
||||||
|
|
||||||
|
- When a rehandshake is detected with a changed SNI, first check existing domain config rules and log if allowed.
|
||||||
|
- If the exact domain config is not found, additionally attempt flexible matching using parent domain and wildcard patterns.
|
||||||
|
- For resumed sessions, try an exact match first and then use fallback logic to select a similar domain config based on matching target IP.
|
||||||
|
- Enhanced logging added to help diagnose missing or mismatched domain configurations.
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.31.1 - fix(PortProxy)
|
||||||
|
Improve TLS handshake buffering and enhance debug logging for SNI forwarding in PortProxy
|
||||||
|
|
||||||
|
- Explicitly copy the initial TLS handshake data to prevent mutation before buffering
|
||||||
|
- Log buffered TLS handshake data with SNI information for better diagnostics
|
||||||
|
- Add detailed error logs on TLS connection failures, including server and domain config status
|
||||||
|
- Output additional debug messages during ClientHello forwarding to verify proper TLS handshake processing
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.31.0 - feat(PortProxy)
|
||||||
|
Improve TLS handshake SNI extraction and add session resumption tracking in PortProxy
|
||||||
|
|
||||||
|
- Added ITlsSessionInfo interface and a global tlsSessionCache to track TLS session IDs for session resumption
|
||||||
|
- Implemented a cleanup timer for the TLS session cache with startSessionCleanupTimer and stopSessionCleanupTimer
|
||||||
|
- Enhanced extractSNIInfo to return detailed SNI information including session IDs, ticket details, and resumption status
|
||||||
|
- Updated renegotiation handlers to use extractSNIInfo for proper SNI extraction during TLS rehandshake
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.30.8 - fix(core)
|
||||||
|
No changes in this commit.
|
||||||
|
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.30.7 - fix(PortProxy)
|
||||||
|
Improve TLS renegotiation SNI handling by first checking if the new SNI is allowed under the existing domain config. If not, attempt to find an alternative domain config and update the locked domain accordingly; otherwise, terminate the connection on SNI mismatch.
|
||||||
|
|
||||||
|
- Added a preliminary check against the original domain config to allow re-handshakes if the new SNI matches allowed patterns.
|
||||||
|
- If the original config does not allow, search for an alternative domain config and validate IP rules.
|
||||||
|
- Update the locked domain when allowed, ensuring connection reuse with valid certificate context.
|
||||||
|
- Terminate the connection if no suitable domain config is found or IP restrictions are violated.
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.30.6 - fix(PortProxy)
|
||||||
|
Improve TLS renegotiation handling in PortProxy by validating the new SNI against allowed domain configurations. If the new SNI is permitted based on existing IP rules, update the locked domain to allow connection reuse; otherwise, terminate the connection to prevent misrouting.
|
||||||
|
|
||||||
|
- Added logic to check if a new SNI during renegotiation is allowed by comparing IP rules from the matching domain configuration.
|
||||||
|
- Updated detailed logging to indicate when a valid SNI change is accepted and when it results in a mismatch termination.
|
||||||
|
|
||||||
|
## 2025-03-10 - 3.30.5 - fix(internal)
|
||||||
|
No uncommitted changes detected; project files and tests remain unchanged.
|
||||||
|
|
||||||
|
|
||||||
|
## 2025-03-10 - 3.30.4 - fix(PortProxy)
|
||||||
|
Fix TLS renegotiation handling and adjust TLS keep-alive timeouts in PortProxy implementation
|
||||||
|
|
||||||
|
- Allow TLS renegotiation data without an explicit SNI extraction to pass through, ensuring valid renegotiations are not dropped (critical for Chrome).
|
||||||
|
- Update TLS keep-alive timeout from an aggressive 30 minutes to a more generous 4 hours to reduce unnecessary reconnections.
|
||||||
|
- Increase inactivity thresholds for TLS connections from 20 minutes to 2 hours with an additional verification interval extended from 5 to 15 minutes.
|
||||||
|
- Adjust long-lived TLS connection timeout from 45 minutes to 8 hours for improved certificate context refresh in chained proxy scenarios.
|
||||||
|
|
||||||
|
## 2025-03-10 - 3.30.3 - fix(classes.portproxy.ts)
|
||||||
|
Simplify timeout management in PortProxy and fix chained proxy certificate refresh issues
|
||||||
|
|
||||||
|
- Reduced TLS keep-alive timeout from 8 hours to 30 minutes to ensure frequent certificate refresh
|
||||||
|
- Added aggressive TLS state refresh after 20 minutes of inactivity and secondary verification checks
|
||||||
|
- Lowered long-lived TLS connection lifetime from 12 hours to 45 minutes to prevent stale certificates
|
||||||
|
- Removed configurable timeout settings from the public API in favor of hardcoded sensible defaults
|
||||||
|
- Simplified internal timeout management to reduce code complexity and improve certificate handling in chained proxies
|
||||||
|
|
||||||
|
## 2025-03-10 - 3.31.0 - fix(classes.portproxy.ts)
|
||||||
|
Simplified timeout management and fixed certificate issues in chained proxy scenarios
|
||||||
|
|
||||||
|
- Dramatically reduced TLS keep-alive timeout from 8 hours to 30 minutes to ensure fresh certificates
|
||||||
|
- Added aggressive certificate refresh after 20 minutes of inactivity (down from 4 hours)
|
||||||
|
- Added secondary verification checks for TLS refresh operations
|
||||||
|
- Reduced long-lived TLS connection lifetime from 12 hours to 45 minutes
|
||||||
|
- Removed configurable timeouts completely from the public API in favor of hardcoded sensible defaults
|
||||||
|
- Simplified interface by removing no-longer-configurable settings while maintaining internal compatibility
|
||||||
|
- Reduced overall code complexity by eliminating complex timeout management
|
||||||
|
- Fixed chained proxy certificate issues by ensuring more frequent certificate refreshes in all deployment scenarios
|
||||||
|
|
||||||
|
## 2025-03-10 - 3.30.2 - fix(classes.portproxy.ts)
|
||||||
|
Adjust TLS keep-alive timeout to refresh certificate context.
|
||||||
|
|
||||||
|
- Modified TLS keep-alive timeout for connections to 8 hours to refresh certificate context.
|
||||||
|
- Updated timeout log messages for clarity on TLS certificate refresh.
|
||||||
|
|
||||||
## 2025-03-10 - 3.30.1 - fix(PortProxy)
|
## 2025-03-10 - 3.30.1 - fix(PortProxy)
|
||||||
Improve TLS keep-alive management and fix whitespace formatting
|
Improve TLS keep-alive management and fix whitespace formatting
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "3.30.1",
|
"version": "3.34.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",
|
||||||
|
@ -197,6 +197,52 @@ tap.test('should match wildcard subdomains', async () => {
|
|||||||
expect(result).toEqual(wildcardConfig);
|
expect(result).toEqual(wildcardConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Test TLD wildcards (example.*)
|
||||||
|
tap.test('should match TLD wildcards', async () => {
|
||||||
|
const tldWildcardConfig = createProxyConfig('example.*');
|
||||||
|
router.setNewProxyConfigs([tldWildcardConfig]);
|
||||||
|
|
||||||
|
// Test that example.com matches example.*
|
||||||
|
const req1 = createMockRequest('example.com');
|
||||||
|
const result1 = router.routeReq(req1);
|
||||||
|
expect(result1).toBeTruthy();
|
||||||
|
expect(result1).toEqual(tldWildcardConfig);
|
||||||
|
|
||||||
|
// Test that example.org matches example.*
|
||||||
|
const req2 = createMockRequest('example.org');
|
||||||
|
const result2 = router.routeReq(req2);
|
||||||
|
expect(result2).toBeTruthy();
|
||||||
|
expect(result2).toEqual(tldWildcardConfig);
|
||||||
|
|
||||||
|
// Test that subdomain.example.com doesn't match example.*
|
||||||
|
const req3 = createMockRequest('subdomain.example.com');
|
||||||
|
const result3 = router.routeReq(req3);
|
||||||
|
expect(result3).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test complex pattern matching (*.lossless*)
|
||||||
|
tap.test('should match complex wildcard patterns', async () => {
|
||||||
|
const complexWildcardConfig = createProxyConfig('*.lossless*');
|
||||||
|
router.setNewProxyConfigs([complexWildcardConfig]);
|
||||||
|
|
||||||
|
// Test that sub.lossless.com matches *.lossless*
|
||||||
|
const req1 = createMockRequest('sub.lossless.com');
|
||||||
|
const result1 = router.routeReq(req1);
|
||||||
|
expect(result1).toBeTruthy();
|
||||||
|
expect(result1).toEqual(complexWildcardConfig);
|
||||||
|
|
||||||
|
// Test that api.lossless.org matches *.lossless*
|
||||||
|
const req2 = createMockRequest('api.lossless.org');
|
||||||
|
const result2 = router.routeReq(req2);
|
||||||
|
expect(result2).toBeTruthy();
|
||||||
|
expect(result2).toEqual(complexWildcardConfig);
|
||||||
|
|
||||||
|
// Test that losslessapi.com matches *.lossless*
|
||||||
|
const req3 = createMockRequest('losslessapi.com');
|
||||||
|
const result3 = router.routeReq(req3);
|
||||||
|
expect(result3).toBeUndefined(); // Should not match as it doesn't have a subdomain
|
||||||
|
});
|
||||||
|
|
||||||
// Test default configuration fallback
|
// Test default configuration fallback
|
||||||
tap.test('should fall back to default configuration', async () => {
|
tap.test('should fall back to default configuration', async () => {
|
||||||
const defaultConfig = createProxyConfig('*');
|
const defaultConfig = createProxyConfig('*');
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '3.30.1',
|
version: '3.34.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.'
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,6 @@ export interface IDomainConfig {
|
|||||||
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
||||||
// Allow domain-specific timeout override
|
// Allow domain-specific timeout override
|
||||||
connectionTimeout?: number; // Connection timeout override (ms)
|
connectionTimeout?: number; // Connection timeout override (ms)
|
||||||
|
|
||||||
// New properties for NetworkProxy integration
|
|
||||||
useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy
|
|
||||||
networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Port proxy settings including global allowed port ranges */
|
/** Port proxy settings including global allowed port ranges */
|
||||||
@ -61,7 +57,8 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|||||||
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
|
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
|
||||||
|
|
||||||
// New property for NetworkProxy integration
|
// New property for NetworkProxy integration
|
||||||
networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
|
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
|
||||||
|
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,13 +94,15 @@ interface IConnectionRecord {
|
|||||||
incomingTerminationReason?: string | null; // Reason for incoming termination
|
incomingTerminationReason?: string | null; // Reason for incoming termination
|
||||||
outgoingTerminationReason?: string | null; // Reason for outgoing termination
|
outgoingTerminationReason?: string | null; // Reason for outgoing termination
|
||||||
|
|
||||||
// New field for NetworkProxy tracking
|
// NetworkProxy tracking
|
||||||
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
|
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
|
||||||
networkProxyIndex?: number; // Which NetworkProxy instance is being used
|
|
||||||
|
|
||||||
// Sleep detection fields
|
// Renegotiation handler
|
||||||
possibleSystemSleep?: boolean; // Flag to indicate a possible system sleep was detected
|
renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection
|
||||||
lastSleepDetection?: number; // Timestamp of the last sleep detection
|
|
||||||
|
// Browser connection tracking
|
||||||
|
isBrowserConnection?: boolean; // Whether this connection appears to be from a browser
|
||||||
|
domainSwitches?: number; // Number of times the domain has been switched on this connection
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -270,6 +269,29 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a TLS record is a proper ClientHello message (more accurate than just checking record type)
|
||||||
|
* @param buffer - Buffer containing the TLS record
|
||||||
|
* @returns true if the buffer contains a proper ClientHello message
|
||||||
|
*/
|
||||||
|
function isClientHello(buffer: Buffer): boolean {
|
||||||
|
try {
|
||||||
|
if (buffer.length < 9) return false; // Too small for a proper ClientHello
|
||||||
|
|
||||||
|
// Check record type (has to be handshake - 22)
|
||||||
|
if (buffer.readUInt8(0) !== 22) return false;
|
||||||
|
|
||||||
|
// After the TLS record header (5 bytes), check the handshake type (1 for ClientHello)
|
||||||
|
if (buffer.readUInt8(5) !== 1) return false;
|
||||||
|
|
||||||
|
// Basic checks passed, this appears to be a ClientHello
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error checking for ClientHello: ${err}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
|
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
|
||||||
return ranges.some((range) => port >= range.from && port <= range.to);
|
return ranges.some((range) => port >= range.from && port <= range.to);
|
||||||
@ -353,8 +375,8 @@ export class PortProxy {
|
|||||||
private connectionsByIP: Map<string, Set<string>> = new Map();
|
private connectionsByIP: Map<string, Set<string>> = new Map();
|
||||||
private connectionRateByIP: Map<string, number[]> = new Map();
|
private connectionRateByIP: Map<string, number[]> = new Map();
|
||||||
|
|
||||||
// New property to store NetworkProxy instances
|
// NetworkProxy instance for TLS termination
|
||||||
private networkProxies: NetworkProxy[] = [];
|
private networkProxy: NetworkProxy | null = null;
|
||||||
|
|
||||||
constructor(settingsArg: IPortProxySettings) {
|
constructor(settingsArg: IPortProxySettings) {
|
||||||
// Set reasonable defaults for all settings
|
// Set reasonable defaults for all settings
|
||||||
@ -374,29 +396,49 @@ export class PortProxy {
|
|||||||
// 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 || 10000, // 10 seconds (reduced for responsiveness)
|
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds
|
||||||
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
|
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB
|
||||||
|
|
||||||
// Feature flags
|
// Feature flags
|
||||||
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
||||||
enableKeepAliveProbes:
|
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined
|
||||||
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, // Enable by default
|
? settingsArg.enableKeepAliveProbes : true,
|
||||||
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
||||||
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
||||||
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
||||||
|
|
||||||
// Rate limiting defaults
|
// Rate limiting defaults
|
||||||
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
|
||||||
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
|
||||||
|
|
||||||
// Enhanced keep-alive settings
|
// Enhanced keep-alive settings
|
||||||
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default
|
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
|
||||||
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
|
||||||
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
|
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||||
|
|
||||||
|
// NetworkProxy settings
|
||||||
|
networkProxyPort: settingsArg.networkProxyPort || 8443, // Default NetworkProxy port
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store NetworkProxy instances if provided
|
// Initialize NetworkProxy if enabled
|
||||||
this.networkProxies = settingsArg.networkProxies || [];
|
if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
|
||||||
|
this.initializeNetworkProxy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize NetworkProxy instance
|
||||||
|
*/
|
||||||
|
private initializeNetworkProxy(): void {
|
||||||
|
if (!this.networkProxy) {
|
||||||
|
this.networkProxy = new NetworkProxy({
|
||||||
|
port: this.settings.networkProxyPort!,
|
||||||
|
portProxyIntegration: true,
|
||||||
|
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -404,45 +446,36 @@ export class PortProxy {
|
|||||||
* @param connectionId - Unique connection identifier
|
* @param connectionId - Unique connection identifier
|
||||||
* @param socket - The incoming client socket
|
* @param socket - The incoming client socket
|
||||||
* @param record - The connection record
|
* @param record - The connection record
|
||||||
* @param domainConfig - The domain configuration
|
|
||||||
* @param initialData - Initial data chunk (TLS ClientHello)
|
* @param initialData - Initial data chunk (TLS ClientHello)
|
||||||
* @param serverName - SNI hostname (if available)
|
|
||||||
*/
|
*/
|
||||||
private forwardToNetworkProxy(
|
private forwardToNetworkProxy(
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
socket: plugins.net.Socket,
|
socket: plugins.net.Socket,
|
||||||
record: IConnectionRecord,
|
record: IConnectionRecord,
|
||||||
domainConfig: IDomainConfig,
|
initialData: Buffer
|
||||||
initialData: Buffer,
|
|
||||||
serverName?: string
|
|
||||||
): void {
|
): void {
|
||||||
// Determine which NetworkProxy to use
|
// Ensure NetworkProxy is initialized
|
||||||
const proxyIndex =
|
if (!this.networkProxy) {
|
||||||
domainConfig.networkProxyIndex !== undefined ? domainConfig.networkProxyIndex : 0;
|
|
||||||
|
|
||||||
// Validate the NetworkProxy index
|
|
||||||
if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) {
|
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`
|
`[${connectionId}] NetworkProxy not initialized. Using fallback direct connection.`
|
||||||
);
|
);
|
||||||
// Fall back to direct connection
|
// Fall back to direct connection
|
||||||
return this.setupDirectConnection(
|
return this.setupDirectConnection(
|
||||||
connectionId,
|
connectionId,
|
||||||
socket,
|
socket,
|
||||||
record,
|
record,
|
||||||
domainConfig,
|
undefined,
|
||||||
serverName,
|
undefined,
|
||||||
initialData
|
initialData
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const networkProxy = this.networkProxies[proxyIndex];
|
const proxyPort = this.networkProxy.getListeningPort();
|
||||||
const proxyPort = networkProxy.getListeningPort();
|
|
||||||
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
|
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
|
||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
|
`[${connectionId}] Forwarding TLS connection to NetworkProxy at ${proxyHost}:${proxyPort}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,7 +489,6 @@ export class PortProxy {
|
|||||||
record.outgoing = proxySocket;
|
record.outgoing = proxySocket;
|
||||||
record.outgoingStartTime = Date.now();
|
record.outgoingStartTime = Date.now();
|
||||||
record.usingNetworkProxy = true;
|
record.usingNetworkProxy = true;
|
||||||
record.networkProxyIndex = proxyIndex;
|
|
||||||
|
|
||||||
// Set up error handlers
|
// Set up error handlers
|
||||||
proxySocket.on('error', (err) => {
|
proxySocket.on('error', (err) => {
|
||||||
@ -495,29 +527,12 @@ export class PortProxy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update activity on data transfer
|
// Update activity on data transfer
|
||||||
socket.on('data', (chunk: Buffer) => {
|
socket.on('data', () => this.updateActivity(record));
|
||||||
this.updateActivity(record);
|
|
||||||
|
|
||||||
// Check for potential TLS renegotiation or reconnection packets
|
|
||||||
if (chunk.length > 0 && chunk[0] === 22) {
|
|
||||||
// ContentType.handshake
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
|
||||||
console.log(
|
|
||||||
`[${connectionId}] Detected potential TLS handshake data while connected to NetworkProxy`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let the NetworkProxy handle the TLS renegotiation
|
|
||||||
// Just update the activity timestamp to prevent timeouts
|
|
||||||
record.lastActivity = Date.now();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proxySocket.on('data', () => this.updateActivity(record));
|
proxySocket.on('data', () => this.updateActivity(record));
|
||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
|
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -838,30 +853,45 @@ export class PortProxy {
|
|||||||
record.pendingData = [];
|
record.pendingData = [];
|
||||||
record.pendingDataSize = 0;
|
record.pendingDataSize = 0;
|
||||||
|
|
||||||
// Add the renegotiation listener for SNI validation
|
// Add the renegotiation handler for SNI validation with strict domain enforcement
|
||||||
if (serverName) {
|
if (serverName) {
|
||||||
socket.on('data', (renegChunk: Buffer) => {
|
// Define a handler for checking renegotiation with improved detection
|
||||||
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
const renegotiationHandler = (renegChunk: Buffer) => {
|
||||||
|
// Only process if this looks like a TLS ClientHello
|
||||||
|
if (isClientHello(renegChunk)) {
|
||||||
try {
|
try {
|
||||||
// Try to extract SNI from potential renegotiation
|
// Extract SNI from ClientHello
|
||||||
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
|
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
|
||||||
if (newSNI && newSNI !== record.lockedDomain) {
|
|
||||||
|
// Skip if no SNI was found
|
||||||
|
if (!newSNI) return;
|
||||||
|
|
||||||
|
// Handle SNI change during renegotiation - always terminate for domain switches
|
||||||
|
if (newSNI !== record.lockedDomain) {
|
||||||
|
// Log and terminate the connection for any SNI change
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.`
|
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
|
||||||
|
`Terminating connection - SNI domain switching is not allowed.`
|
||||||
);
|
);
|
||||||
this.initiateCleanupOnce(record, 'sni_mismatch');
|
this.initiateCleanupOnce(record, 'sni_mismatch');
|
||||||
} else if (newSNI && this.settings.enableDetailedLogging) {
|
} else if (this.settings.enableDetailedLogging) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
|
`[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
|
`[${connectionId}] Error processing ClientHello: ${err}. Allowing connection to continue.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Store the handler in the connection record so we can remove it during cleanup
|
||||||
|
record.renegotiationHandler = renegotiationHandler;
|
||||||
|
|
||||||
|
// Add the listener
|
||||||
|
socket.on('data', renegotiationHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set connection timeout with simpler logic
|
// Set connection timeout with simpler logic
|
||||||
@ -878,37 +908,6 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
// No cleanup timer for immortal connections
|
// No cleanup timer for immortal connections
|
||||||
}
|
}
|
||||||
// For TLS keep-alive connections, use a very extended timeout
|
|
||||||
else if (record.hasKeepAlive && record.isTLS) {
|
|
||||||
// For TLS keep-alive connections, use a very extended timeout
|
|
||||||
// This helps prevent certificate errors after sleep/wake cycles
|
|
||||||
const tlsKeepAliveTimeout = 14 * 24 * 60 * 60 * 1000; // 14 days for TLS keep-alive
|
|
||||||
const safeTimeout = ensureSafeTimeout(tlsKeepAliveTimeout);
|
|
||||||
|
|
||||||
record.cleanupTimer = setTimeout(() => {
|
|
||||||
console.log(
|
|
||||||
`[${connectionId}] TLS keep-alive connection from ${
|
|
||||||
record.remoteIP
|
|
||||||
} exceeded extended lifetime (${plugins.prettyMs(
|
|
||||||
tlsKeepAliveTimeout
|
|
||||||
)}), forcing cleanup.`
|
|
||||||
);
|
|
||||||
this.initiateCleanupOnce(record, 'tls_extended_lifetime');
|
|
||||||
}, safeTimeout);
|
|
||||||
|
|
||||||
// Make sure timeout doesn't keep the process alive
|
|
||||||
if (record.cleanupTimer.unref) {
|
|
||||||
record.cleanupTimer.unref();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
|
||||||
console.log(
|
|
||||||
`[${connectionId}] TLS keep-alive connection with enhanced protection, lifetime: ${plugins.prettyMs(
|
|
||||||
tlsKeepAliveTimeout
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For extended keep-alive connections, use extended timeout
|
// For extended keep-alive connections, use extended timeout
|
||||||
else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
||||||
const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
|
const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||||
@ -1029,100 +1028,6 @@ export class PortProxy {
|
|||||||
this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
|
this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update connection activity timestamp with sleep detection
|
|
||||||
*/
|
|
||||||
private updateActivity(record: IConnectionRecord): void {
|
|
||||||
// Get the current time
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Check if there was a large time gap that suggests system sleep
|
|
||||||
if (record.lastActivity > 0) {
|
|
||||||
const timeDiff = now - record.lastActivity;
|
|
||||||
|
|
||||||
// If time difference is very large (> 30 minutes) and this is a keep-alive connection,
|
|
||||||
// this might indicate system sleep rather than just inactivity
|
|
||||||
if (timeDiff > 30 * 60 * 1000 && record.hasKeepAlive) {
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
|
||||||
console.log(
|
|
||||||
`[${record.id}] Detected possible system sleep for ${plugins.prettyMs(timeDiff)}. ` +
|
|
||||||
`Handling keep-alive connection after long inactivity.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For TLS keep-alive connections after sleep/long inactivity, force close
|
|
||||||
// to make browser establish a new connection with fresh certificate context
|
|
||||||
if (record.isTLS && record.tlsHandshakeComplete) {
|
|
||||||
if (timeDiff > 4 * 60 * 60 * 1000) {
|
|
||||||
// If inactive for more than 4 hours
|
|
||||||
console.log(
|
|
||||||
`[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
|
|
||||||
`Closing to force new connection with fresh certificate.`
|
|
||||||
);
|
|
||||||
return this.initiateCleanupOnce(record, 'certificate_refresh_needed');
|
|
||||||
} else {
|
|
||||||
// For shorter inactivity periods, try to refresh the TLS state
|
|
||||||
this.refreshTlsStateAfterSleep(record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark that we detected sleep
|
|
||||||
record.possibleSystemSleep = true;
|
|
||||||
record.lastSleepDetection = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the activity timestamp
|
|
||||||
record.lastActivity = now;
|
|
||||||
|
|
||||||
// Clear any inactivity warning
|
|
||||||
if (record.inactivityWarningIssued) {
|
|
||||||
record.inactivityWarningIssued = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh TLS state after sleep detection
|
|
||||||
*/
|
|
||||||
private refreshTlsStateAfterSleep(record: IConnectionRecord): void {
|
|
||||||
// Skip if we're using a NetworkProxy as it handles its own TLS state
|
|
||||||
if (record.usingNetworkProxy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// For outgoing connections that might need to be refreshed
|
|
||||||
if (record.outgoing && !record.outgoing.destroyed) {
|
|
||||||
// Check how long this connection has been established
|
|
||||||
const connectionAge = Date.now() - record.incomingStartTime;
|
|
||||||
const hourInMs = 60 * 60 * 1000;
|
|
||||||
|
|
||||||
// For TLS browser connections that are very old, it's better to force a new connection
|
|
||||||
// rather than trying to refresh the state, to avoid certificate issues
|
|
||||||
if (record.isTLS && record.hasKeepAlive && connectionAge > 12 * hourInMs) {
|
|
||||||
console.log(
|
|
||||||
`[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` +
|
|
||||||
`Closing to ensure proper certificate handling on browser reconnect.`
|
|
||||||
);
|
|
||||||
return this.initiateCleanupOnce(record, 'certificate_context_refresh');
|
|
||||||
}
|
|
||||||
|
|
||||||
// For newer connections, try to send a refresh packet
|
|
||||||
record.outgoing.write(Buffer.alloc(0));
|
|
||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
|
||||||
console.log(`[${record.id}] Sent refresh packet after sleep detection`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log(`[${record.id}] Error refreshing TLS state: ${err}`);
|
|
||||||
|
|
||||||
// If we hit an error, it's likely the connection is already broken
|
|
||||||
// Force cleanup to ensure browser reconnects cleanly
|
|
||||||
return this.initiateCleanupOnce(record, 'tls_refresh_error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleans up a connection record.
|
* Cleans up a connection record.
|
||||||
* Destroys both incoming and outgoing sockets, clears timers, and removes the record.
|
* Destroys both incoming and outgoing sockets, clears timers, and removes the record.
|
||||||
@ -1146,6 +1051,16 @@ export class PortProxy {
|
|||||||
const bytesReceived = record.bytesReceived;
|
const bytesReceived = record.bytesReceived;
|
||||||
const bytesSent = record.bytesSent;
|
const bytesSent = record.bytesSent;
|
||||||
|
|
||||||
|
// Remove the renegotiation handler if present
|
||||||
|
if (record.renegotiationHandler && record.incoming) {
|
||||||
|
try {
|
||||||
|
record.incoming.removeListener('data', record.renegotiationHandler);
|
||||||
|
record.renegotiationHandler = undefined;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[${record.id}] Error removing renegotiation handler: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!record.incoming.destroyed) {
|
if (!record.incoming.destroyed) {
|
||||||
// Try graceful shutdown first, then force destroy after a short timeout
|
// Try graceful shutdown first, then force destroy after a short timeout
|
||||||
@ -1223,7 +1138,8 @@ export class PortProxy {
|
|||||||
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
||||||
record.hasKeepAlive ? 'Yes' : 'No'
|
record.hasKeepAlive ? 'Yes' : 'No'
|
||||||
}` +
|
}` +
|
||||||
`${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}`
|
`${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
|
||||||
|
`${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
@ -1233,6 +1149,18 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update connection activity timestamp
|
||||||
|
*/
|
||||||
|
private updateActivity(record: IConnectionRecord): void {
|
||||||
|
record.lastActivity = Date.now();
|
||||||
|
|
||||||
|
// Clear any inactivity warning
|
||||||
|
if (record.inactivityWarningIssued) {
|
||||||
|
record.inactivityWarningIssued = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get target IP with round-robin support
|
* Get target IP with round-robin support
|
||||||
*/
|
*/
|
||||||
@ -1350,6 +1278,12 @@ export class PortProxy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start NetworkProxy if configured
|
||||||
|
if (this.networkProxy) {
|
||||||
|
await this.networkProxy.start();
|
||||||
|
console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Define a unified connection handler for all listening ports.
|
// Define a unified connection handler for all listening ports.
|
||||||
const connectionHandler = (socket: plugins.net.Socket) => {
|
const connectionHandler = (socket: plugins.net.Socket) => {
|
||||||
if (this.isShuttingDown) {
|
if (this.isShuttingDown) {
|
||||||
@ -1410,11 +1344,12 @@ export class PortProxy {
|
|||||||
incomingTerminationReason: null,
|
incomingTerminationReason: null,
|
||||||
outgoingTerminationReason: null,
|
outgoingTerminationReason: null,
|
||||||
|
|
||||||
// Initialize NetworkProxy tracking fields
|
// Initialize NetworkProxy tracking
|
||||||
usingNetworkProxy: false,
|
usingNetworkProxy: false,
|
||||||
|
|
||||||
// Initialize sleep detection fields
|
// Initialize browser connection tracking
|
||||||
possibleSystemSleep: false,
|
isBrowserConnection: false,
|
||||||
|
domainSwitches: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply keep-alive settings if enabled
|
// Apply keep-alive settings if enabled
|
||||||
@ -1459,8 +1394,63 @@ export class PortProxy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this connection should be forwarded directly to NetworkProxy based on port
|
||||||
|
const shouldUseNetworkProxy = this.settings.useNetworkProxy &&
|
||||||
|
this.settings.useNetworkProxy.includes(localPort);
|
||||||
|
|
||||||
|
if (shouldUseNetworkProxy) {
|
||||||
|
// For NetworkProxy ports, we want to capture the TLS handshake and forward directly
|
||||||
let initialDataReceived = false;
|
let initialDataReceived = false;
|
||||||
|
|
||||||
|
// Set an initial timeout for handshake data
|
||||||
|
let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
|
||||||
|
if (!initialDataReceived) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
|
||||||
|
);
|
||||||
|
if (connectionRecord.incomingTerminationReason === null) {
|
||||||
|
connectionRecord.incomingTerminationReason = 'initial_timeout';
|
||||||
|
this.incrementTerminationStat('incoming', 'initial_timeout');
|
||||||
|
}
|
||||||
|
socket.end();
|
||||||
|
this.cleanupConnection(connectionRecord, 'initial_timeout');
|
||||||
|
}
|
||||||
|
}, this.settings.initialDataTimeout!);
|
||||||
|
|
||||||
|
// Make sure timeout doesn't keep the process alive
|
||||||
|
if (initialTimeout.unref) {
|
||||||
|
initialTimeout.unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('error', this.handleError('incoming', connectionRecord));
|
||||||
|
|
||||||
|
// First data handler to capture initial TLS handshake for NetworkProxy
|
||||||
|
socket.once('data', (chunk: Buffer) => {
|
||||||
|
// Clear the initial timeout since we've received data
|
||||||
|
if (initialTimeout) {
|
||||||
|
clearTimeout(initialTimeout);
|
||||||
|
initialTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialDataReceived = true;
|
||||||
|
connectionRecord.hasReceivedInitialData = true;
|
||||||
|
|
||||||
|
// Check if this looks like a TLS handshake
|
||||||
|
if (isTlsHandshake(chunk)) {
|
||||||
|
connectionRecord.isTLS = true;
|
||||||
|
|
||||||
|
// Forward directly to NetworkProxy without SNI processing
|
||||||
|
this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk);
|
||||||
|
} else {
|
||||||
|
// If not TLS, use normal direct connection
|
||||||
|
console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${localPort}`);
|
||||||
|
this.setupDirectConnection(connectionId, socket, connectionRecord, undefined, undefined, chunk);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// For non-NetworkProxy ports, proceed with normal processing
|
||||||
|
|
||||||
// Define helpers for rejecting connections
|
// Define helpers for rejecting connections
|
||||||
const rejectIncomingConnection = (reason: string, logMessage: string) => {
|
const rejectIncomingConnection = (reason: string, logMessage: string) => {
|
||||||
console.log(`[${connectionId}] ${logMessage}`);
|
console.log(`[${connectionId}] ${logMessage}`);
|
||||||
@ -1472,6 +1462,8 @@ export class PortProxy {
|
|||||||
this.cleanupConnection(connectionRecord, reason);
|
this.cleanupConnection(connectionRecord, reason);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let initialDataReceived = false;
|
||||||
|
|
||||||
// Set an initial timeout for SNI data if needed
|
// Set an initial timeout for SNI data if needed
|
||||||
let initialTimeout: NodeJS.Timeout | null = null;
|
let initialTimeout: NodeJS.Timeout | null = null;
|
||||||
if (this.settings.sniEnabled) {
|
if (this.settings.sniEnabled) {
|
||||||
@ -1520,7 +1512,7 @@ export class PortProxy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the connection to the target host or NetworkProxy.
|
* Sets up the connection to the target host.
|
||||||
* @param serverName - The SNI hostname (unused when forcedDomain is provided).
|
* @param serverName - The SNI hostname (unused when forcedDomain is provided).
|
||||||
* @param initialChunk - Optional initial data chunk.
|
* @param initialChunk - Optional initial data chunk.
|
||||||
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
|
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
|
||||||
@ -1589,23 +1581,6 @@ export class PortProxy {
|
|||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we should forward this to a NetworkProxy
|
|
||||||
if (
|
|
||||||
isTlsHandshakeDetected &&
|
|
||||||
domainConfig.useNetworkProxy === true &&
|
|
||||||
initialChunk &&
|
|
||||||
this.networkProxies.length > 0
|
|
||||||
) {
|
|
||||||
return this.forwardToNetworkProxy(
|
|
||||||
connectionId,
|
|
||||||
socket,
|
|
||||||
connectionRecord,
|
|
||||||
domainConfig,
|
|
||||||
initialChunk,
|
|
||||||
serverName
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
||||||
if (
|
if (
|
||||||
!isGlobIPAllowed(
|
!isGlobIPAllowed(
|
||||||
@ -1621,7 +1596,12 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we didn't forward to NetworkProxy, proceed with direct connection
|
// Save the initial SNI
|
||||||
|
if (serverName) {
|
||||||
|
connectionRecord.lockedDomain = serverName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the direct connection
|
||||||
return this.setupDirectConnection(
|
return this.setupDirectConnection(
|
||||||
connectionId,
|
connectionId,
|
||||||
socket,
|
socket,
|
||||||
@ -1766,6 +1746,7 @@ export class PortProxy {
|
|||||||
|
|
||||||
setupConnection('');
|
setupConnection('');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- SETUP LISTENERS ---
|
// --- SETUP LISTENERS ---
|
||||||
@ -1790,10 +1771,11 @@ export class PortProxy {
|
|||||||
console.log(`Server Error on port ${port}: ${err.message}`);
|
console.log(`Server Error on port ${port}: ${err.message}`);
|
||||||
});
|
});
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
|
const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
|
||||||
console.log(
|
console.log(
|
||||||
`PortProxy -> OK: Now listening on port ${port}${
|
`PortProxy -> OK: Now listening on port ${port}${
|
||||||
this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''
|
this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : ''
|
||||||
}${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}`
|
}${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.netServers.push(server);
|
this.netServers.push(server);
|
||||||
@ -1813,6 +1795,7 @@ export class PortProxy {
|
|||||||
let pendingTlsHandshakes = 0;
|
let pendingTlsHandshakes = 0;
|
||||||
let keepAliveConnections = 0;
|
let keepAliveConnections = 0;
|
||||||
let networkProxyConnections = 0;
|
let networkProxyConnections = 0;
|
||||||
|
let domainSwitchedConnections = 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()];
|
||||||
@ -1841,11 +1824,14 @@ export class PortProxy {
|
|||||||
networkProxyConnections++;
|
networkProxyConnections++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (record.domainSwitches && record.domainSwitches > 0) {
|
||||||
|
domainSwitchedConnections++;
|
||||||
|
}
|
||||||
|
|
||||||
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
|
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
|
||||||
if (record.outgoingStartTime) {
|
if (record.outgoingStartTime) {
|
||||||
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
|
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parity check: if outgoing socket closed and incoming remains active
|
// Parity check: if outgoing socket closed and incoming remains active
|
||||||
if (
|
if (
|
||||||
record.outgoingClosedTime &&
|
record.outgoingClosedTime &&
|
||||||
@ -1883,48 +1869,6 @@ export class PortProxy {
|
|||||||
) {
|
) {
|
||||||
const inactivityTime = now - record.lastActivity;
|
const inactivityTime = now - record.lastActivity;
|
||||||
|
|
||||||
// Special handling for TLS keep-alive connections
|
|
||||||
if (
|
|
||||||
record.hasKeepAlive &&
|
|
||||||
record.isTLS &&
|
|
||||||
inactivityTime > this.settings.inactivityTimeout! / 2
|
|
||||||
) {
|
|
||||||
// For TLS keep-alive connections that are getting stale, try to refresh before closing
|
|
||||||
if (!record.inactivityWarningIssued) {
|
|
||||||
console.log(
|
|
||||||
`[${id}] TLS keep-alive connection from ${
|
|
||||||
record.remoteIP
|
|
||||||
} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
|
|
||||||
`Attempting to preserve connection.`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set warning flag but give a much longer grace period for TLS connections
|
|
||||||
record.inactivityWarningIssued = true;
|
|
||||||
|
|
||||||
// For TLS connections, extend the last activity time considerably
|
|
||||||
// This gives browsers more time to re-establish the connection properly
|
|
||||||
record.lastActivity = now - this.settings.inactivityTimeout! / 3;
|
|
||||||
|
|
||||||
// Try to stimulate the connection with a probe packet
|
|
||||||
if (record.outgoing && !record.outgoing.destroyed) {
|
|
||||||
try {
|
|
||||||
// For TLS connections, send a proper TLS heartbeat-like packet
|
|
||||||
// This is just a small empty buffer that won't affect the TLS session
|
|
||||||
record.outgoing.write(Buffer.alloc(0));
|
|
||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
|
||||||
console.log(`[${id}] Sent TLS keep-alive probe packet`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log(`[${id}] Error sending TLS probe packet: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't proceed to the normal inactivity check logic
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use extended timeout for extended-treatment keep-alive connections
|
// Use extended timeout for extended-treatment keep-alive connections
|
||||||
let effectiveTimeout = this.settings.inactivityTimeout!;
|
let effectiveTimeout = this.settings.inactivityTimeout!;
|
||||||
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
||||||
@ -1958,33 +1902,6 @@ export class PortProxy {
|
|||||||
console.log(`[${id}] Error sending probe packet: ${err}`);
|
console.log(`[${id}] Error sending probe packet: ${err}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// MODIFIED: For TLS connections, be more lenient before closing
|
|
||||||
// For TLS browser connections, we need to handle certificate context properly
|
|
||||||
if (record.isTLS && record.hasKeepAlive) {
|
|
||||||
// For very long inactivity, it's better to close the connection
|
|
||||||
// so the browser establishes a new one with a fresh certificate context
|
|
||||||
if (inactivityTime > 6 * 60 * 60 * 1000) {
|
|
||||||
// 6 hours
|
|
||||||
console.log(
|
|
||||||
`[${id}] TLS keep-alive connection from ${
|
|
||||||
record.remoteIP
|
|
||||||
} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
|
|
||||||
`Closing to ensure proper certificate handling on browser reconnect.`
|
|
||||||
);
|
|
||||||
this.cleanupConnection(record, 'tls_certificate_refresh');
|
|
||||||
} else {
|
|
||||||
// For shorter inactivity periods, add grace period
|
|
||||||
console.log(
|
|
||||||
`[${id}] TLS keep-alive connection from ${
|
|
||||||
record.remoteIP
|
|
||||||
} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
|
|
||||||
`Adding extra grace period.`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Give additional time for browsers to reconnect properly
|
|
||||||
record.lastActivity = now - effectiveTimeout / 2;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// For non-keep-alive or after warning, close the connection
|
// For non-keep-alive or after warning, close the connection
|
||||||
console.log(
|
console.log(
|
||||||
@ -1994,7 +1911,6 @@ export class PortProxy {
|
|||||||
);
|
);
|
||||||
this.cleanupConnection(record, 'inactivity');
|
this.cleanupConnection(record, 'inactivity');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
|
} else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
|
||||||
// If activity detected after warning, clear the warning
|
// If activity detected after warning, clear the warning
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
@ -2011,7 +1927,8 @@ export class PortProxy {
|
|||||||
console.log(
|
console.log(
|
||||||
`Active connections: ${this.connectionRecords.size}. ` +
|
`Active connections: ${this.connectionRecords.size}. ` +
|
||||||
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
|
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
|
||||||
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` +
|
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}, ` +
|
||||||
|
`DomainSwitched=${domainSwitchedConnections}. ` +
|
||||||
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
|
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
|
||||||
maxOutgoing
|
maxOutgoing
|
||||||
)}. ` +
|
)}. ` +
|
||||||
@ -2028,21 +1945,6 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add or replace NetworkProxy instances
|
|
||||||
*/
|
|
||||||
public setNetworkProxies(networkProxies: NetworkProxy[]): void {
|
|
||||||
this.networkProxies = networkProxies;
|
|
||||||
console.log(`Updated NetworkProxy instances: ${this.networkProxies.length} proxies configured`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a list of configured NetworkProxy instances
|
|
||||||
*/
|
|
||||||
public getNetworkProxies(): NetworkProxy[] {
|
|
||||||
return this.networkProxies;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gracefully shut down the proxy
|
* Gracefully shut down the proxy
|
||||||
*/
|
*/
|
||||||
@ -2134,6 +2036,16 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop NetworkProxy if it was started
|
||||||
|
if (this.networkProxy) {
|
||||||
|
try {
|
||||||
|
await this.networkProxy.stop();
|
||||||
|
console.log('NetworkProxy stopped successfully');
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error stopping NetworkProxy: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear all tracking maps
|
// Clear all tracking maps
|
||||||
this.connectionRecords.clear();
|
this.connectionRecords.clear();
|
||||||
this.domainTargetIndices.clear();
|
this.domainTargetIndices.clear();
|
||||||
|
@ -19,6 +19,21 @@ export interface IRouterResult {
|
|||||||
pathRemainder?: string;
|
pathRemainder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router for HTTP reverse proxy requests
|
||||||
|
*
|
||||||
|
* Supports the following domain matching patterns:
|
||||||
|
* - Exact matches: "example.com"
|
||||||
|
* - Wildcard subdomains: "*.example.com" (matches any subdomain of example.com)
|
||||||
|
* - TLD wildcards: "example.*" (matches example.com, example.org, etc.)
|
||||||
|
* - Complex wildcards: "*.lossless*" (matches any subdomain of any lossless domain)
|
||||||
|
* - Default fallback: "*" (matches any unmatched domain)
|
||||||
|
*
|
||||||
|
* Also supports path pattern matching for each domain:
|
||||||
|
* - Exact path: "/api/users"
|
||||||
|
* - Wildcard paths: "/api/*"
|
||||||
|
* - Path parameters: "/users/:id/profile"
|
||||||
|
*/
|
||||||
export class ProxyRouter {
|
export class ProxyRouter {
|
||||||
// Store original configs for reference
|
// Store original configs for reference
|
||||||
private reverseProxyConfigs: tsclass.network.IReverseProxyConfig[] = [];
|
private reverseProxyConfigs: tsclass.network.IReverseProxyConfig[] = [];
|
||||||
@ -98,9 +113,11 @@ export class ProxyRouter {
|
|||||||
return exactConfig;
|
return exactConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try wildcard subdomain
|
// Try various wildcard patterns
|
||||||
if (hostWithoutPort.includes('.')) {
|
if (hostWithoutPort.includes('.')) {
|
||||||
const domainParts = hostWithoutPort.split('.');
|
const domainParts = hostWithoutPort.split('.');
|
||||||
|
|
||||||
|
// Try wildcard subdomain (*.example.com)
|
||||||
if (domainParts.length > 2) {
|
if (domainParts.length > 2) {
|
||||||
const wildcardDomain = `*.${domainParts.slice(1).join('.')}`;
|
const wildcardDomain = `*.${domainParts.slice(1).join('.')}`;
|
||||||
const wildcardConfig = this.findConfigForHost(wildcardDomain, urlPath);
|
const wildcardConfig = this.findConfigForHost(wildcardDomain, urlPath);
|
||||||
@ -108,6 +125,23 @@ export class ProxyRouter {
|
|||||||
return wildcardConfig;
|
return wildcardConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try TLD wildcard (example.*)
|
||||||
|
const baseDomain = domainParts.slice(0, -1).join('.');
|
||||||
|
const tldWildcardDomain = `${baseDomain}.*`;
|
||||||
|
const tldWildcardConfig = this.findConfigForHost(tldWildcardDomain, urlPath);
|
||||||
|
if (tldWildcardConfig) {
|
||||||
|
return tldWildcardConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try complex wildcard patterns
|
||||||
|
const wildcardPatterns = this.findWildcardMatches(hostWithoutPort);
|
||||||
|
for (const pattern of wildcardPatterns) {
|
||||||
|
const wildcardConfig = this.findConfigForHost(pattern, urlPath);
|
||||||
|
if (wildcardConfig) {
|
||||||
|
return wildcardConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to default config if available
|
// Fall back to default config if available
|
||||||
@ -120,6 +154,53 @@ export class ProxyRouter {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find potential wildcard patterns that could match a given hostname
|
||||||
|
* Handles complex patterns like "*.lossless*" or other partial matches
|
||||||
|
* @param hostname The hostname to find wildcard matches for
|
||||||
|
* @returns Array of potential wildcard patterns that could match
|
||||||
|
*/
|
||||||
|
private findWildcardMatches(hostname: string): string[] {
|
||||||
|
const patterns: string[] = [];
|
||||||
|
const hostnameParts = hostname.split('.');
|
||||||
|
|
||||||
|
// Find all configured hostnames that contain wildcards
|
||||||
|
const wildcardConfigs = this.reverseProxyConfigs.filter(
|
||||||
|
config => config.hostName.includes('*')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extract unique wildcard patterns
|
||||||
|
const wildcardPatterns = [...new Set(
|
||||||
|
wildcardConfigs.map(config => config.hostName.toLowerCase())
|
||||||
|
)];
|
||||||
|
|
||||||
|
// For each wildcard pattern, check if it could match the hostname
|
||||||
|
// using simplified regex pattern matching
|
||||||
|
for (const pattern of wildcardPatterns) {
|
||||||
|
// Skip the default wildcard '*'
|
||||||
|
if (pattern === '*') continue;
|
||||||
|
|
||||||
|
// Skip already checked patterns (*.domain.com and domain.*)
|
||||||
|
if (pattern.startsWith('*.') && pattern.indexOf('*', 2) === -1) continue;
|
||||||
|
if (pattern.endsWith('.*') && pattern.indexOf('*') === pattern.length - 1) continue;
|
||||||
|
|
||||||
|
// Convert wildcard pattern to regex
|
||||||
|
const regexPattern = pattern
|
||||||
|
.replace(/\./g, '\\.') // Escape dots
|
||||||
|
.replace(/\*/g, '.*'); // Convert * to .* for regex
|
||||||
|
|
||||||
|
// Create regex object with case insensitive flag
|
||||||
|
const regex = new RegExp(`^${regexPattern}$`, 'i');
|
||||||
|
|
||||||
|
// If hostname matches this complex pattern, add it to the list
|
||||||
|
if (regex.test(hostname)) {
|
||||||
|
patterns.push(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return patterns;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a config for a specific host and path
|
* Find a config for a specific host and path
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user