@push.rocks/smartproxy 3.25.3 → 3.25.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.portproxy.d.ts +6 -18
- package/dist_ts/classes.portproxy.js +291 -271
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +327 -308
package/ts/classes.portproxy.ts
CHANGED
|
@@ -7,14 +7,10 @@ export interface IDomainConfig {
|
|
|
7
7
|
blockedIPs?: string[]; // Glob patterns for blocked IPs
|
|
8
8
|
targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
|
|
9
9
|
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
wsTimeout?: number; // WebSocket connection timeout override (ms)
|
|
10
|
+
// Allow domain-specific timeout override
|
|
11
|
+
connectionTimeout?: number; // Connection timeout override (ms)
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
/** Connection protocol types for timeout management */
|
|
16
|
-
export type ProtocolType = 'http' | 'websocket' | 'https' | 'tls' | 'unknown';
|
|
17
|
-
|
|
18
14
|
/** Port proxy settings including global allowed port ranges */
|
|
19
15
|
export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
20
16
|
fromPort: number;
|
|
@@ -26,40 +22,37 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
26
22
|
defaultBlockedIPs?: string[];
|
|
27
23
|
preserveSourceIP?: boolean;
|
|
28
24
|
|
|
29
|
-
//
|
|
30
|
-
initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default:
|
|
31
|
-
socketTimeout?: number; // Socket inactivity timeout (ms), default:
|
|
32
|
-
inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default:
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 3600000 (1h)
|
|
36
|
-
httpConnectionTimeout?: number; // HTTP specific timeout (ms), default: 1800000 (30m)
|
|
37
|
-
wsConnectionTimeout?: number; // WebSocket specific timeout (ms), default: 14400000 (4h)
|
|
38
|
-
httpKeepAliveTimeout?: number; // HTTP keep-alive header timeout (ms), default: 1200000 (20m)
|
|
25
|
+
// Timeout settings
|
|
26
|
+
initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
|
|
27
|
+
socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
|
|
28
|
+
inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s)
|
|
29
|
+
maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 3600000 (1h)
|
|
30
|
+
inactivityTimeout?: number; // Inactivity timeout (ms), default: 3600000 (1h)
|
|
39
31
|
|
|
40
|
-
gracefulShutdownTimeout?: number;
|
|
32
|
+
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
|
|
41
33
|
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
|
|
42
|
-
forwardAllGlobalRanges?: boolean;
|
|
34
|
+
forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
|
|
43
35
|
|
|
44
36
|
// Socket optimization settings
|
|
45
|
-
noDelay?: boolean;
|
|
46
|
-
keepAlive?: boolean;
|
|
47
|
-
keepAliveInitialDelay?: number;
|
|
48
|
-
maxPendingDataSize?: number;
|
|
37
|
+
noDelay?: boolean; // Disable Nagle's algorithm (default: true)
|
|
38
|
+
keepAlive?: boolean; // Enable TCP keepalive (default: true)
|
|
39
|
+
keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
|
|
40
|
+
maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
|
|
49
41
|
|
|
50
|
-
//
|
|
51
|
-
disableInactivityCheck?: boolean;
|
|
52
|
-
enableKeepAliveProbes?: boolean;
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
// Enhanced features
|
|
43
|
+
disableInactivityCheck?: boolean; // Disable inactivity checking entirely
|
|
44
|
+
enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
|
|
45
|
+
enableDetailedLogging?: boolean; // Enable detailed connection logging
|
|
46
|
+
enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
|
|
47
|
+
enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
|
|
55
48
|
|
|
56
49
|
// Rate limiting and security
|
|
57
|
-
maxConnectionsPerIP?: number;
|
|
50
|
+
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
|
|
58
51
|
connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
|
|
59
52
|
}
|
|
60
53
|
|
|
61
54
|
/**
|
|
62
|
-
* Enhanced connection record
|
|
55
|
+
* Enhanced connection record
|
|
63
56
|
*/
|
|
64
57
|
interface IConnectionRecord {
|
|
65
58
|
id: string; // Unique connection identifier
|
|
@@ -76,78 +69,161 @@ interface IConnectionRecord {
|
|
|
76
69
|
pendingDataSize: number; // Track total size of pending data
|
|
77
70
|
|
|
78
71
|
// Enhanced tracking fields
|
|
79
|
-
protocolType: ProtocolType; // Connection protocol type
|
|
80
|
-
isPooledConnection: boolean; // Whether this is likely a browser pooled connection
|
|
81
|
-
lastHttpRequest?: number; // Timestamp of last HTTP request (for keep-alive tracking)
|
|
82
|
-
httpKeepAliveTimeout?: number; // HTTP keep-alive timeout from headers
|
|
83
72
|
bytesReceived: number; // Total bytes received
|
|
84
73
|
bytesSent: number; // Total bytes sent
|
|
85
74
|
remoteIP: string; // Remote IP (cached for logging after socket close)
|
|
86
75
|
localPort: number; // Local port (cached for logging)
|
|
87
|
-
|
|
76
|
+
isTLS: boolean; // Whether this connection is a TLS connection
|
|
77
|
+
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
|
|
78
|
+
hasReceivedInitialData: boolean; // Whether initial data has been received
|
|
79
|
+
domainConfig?: IDomainConfig; // Associated domain config for this connection
|
|
88
80
|
}
|
|
89
81
|
|
|
90
82
|
/**
|
|
91
83
|
* Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
|
|
84
|
+
* Enhanced for robustness and detailed logging.
|
|
92
85
|
* @param buffer - Buffer containing the TLS ClientHello.
|
|
86
|
+
* @param enableLogging - Whether to enable detailed logging.
|
|
93
87
|
* @returns The server name if found, otherwise undefined.
|
|
94
88
|
*/
|
|
95
|
-
function extractSNI(buffer: Buffer): string | undefined {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const recordLength = buffer.readUInt16BE(3);
|
|
103
|
-
if (buffer.length < 5 + recordLength) return undefined;
|
|
89
|
+
function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
|
|
90
|
+
try {
|
|
91
|
+
// Check if buffer is too small for TLS
|
|
92
|
+
if (buffer.length < 5) {
|
|
93
|
+
if (enableLogging) console.log("Buffer too small for TLS header");
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
104
96
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
// Check record type (has to be handshake - 22)
|
|
98
|
+
const recordType = buffer.readUInt8(0);
|
|
99
|
+
if (recordType !== 22) {
|
|
100
|
+
if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
108
103
|
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
// Check TLS version (has to be 3.1 or higher)
|
|
105
|
+
const majorVersion = buffer.readUInt8(1);
|
|
106
|
+
const minorVersion = buffer.readUInt8(2);
|
|
107
|
+
if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion}`);
|
|
108
|
+
|
|
109
|
+
// Check record length
|
|
110
|
+
const recordLength = buffer.readUInt16BE(3);
|
|
111
|
+
if (buffer.length < 5 + recordLength) {
|
|
112
|
+
if (enableLogging) console.log(`Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
111
115
|
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
let offset = 5;
|
|
117
|
+
const handshakeType = buffer.readUInt8(offset);
|
|
118
|
+
if (handshakeType !== 1) {
|
|
119
|
+
if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
114
122
|
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
offset += 4; // Skip handshake header (type + length)
|
|
124
|
+
|
|
125
|
+
// Client version
|
|
126
|
+
const clientMajorVersion = buffer.readUInt8(offset);
|
|
127
|
+
const clientMinorVersion = buffer.readUInt8(offset + 1);
|
|
128
|
+
if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
|
|
129
|
+
|
|
130
|
+
offset += 2 + 32; // Skip client version and random
|
|
117
131
|
|
|
118
|
-
|
|
119
|
-
|
|
132
|
+
// Session ID
|
|
133
|
+
const sessionIDLength = buffer.readUInt8(offset);
|
|
134
|
+
if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
|
|
135
|
+
offset += 1 + sessionIDLength; // Skip session ID
|
|
120
136
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
// Cipher suites
|
|
138
|
+
if (offset + 2 > buffer.length) {
|
|
139
|
+
if (enableLogging) console.log("Buffer too small for cipher suites length");
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const cipherSuitesLength = buffer.readUInt16BE(offset);
|
|
143
|
+
if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
|
|
144
|
+
offset += 2 + cipherSuitesLength; // Skip cipher suites
|
|
145
|
+
|
|
146
|
+
// Compression methods
|
|
147
|
+
if (offset + 1 > buffer.length) {
|
|
148
|
+
if (enableLogging) console.log("Buffer too small for compression methods length");
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const compressionMethodsLength = buffer.readUInt8(offset);
|
|
152
|
+
if (enableLogging) console.log(`Compression Methods Length: ${compressionMethodsLength}`);
|
|
153
|
+
offset += 1 + compressionMethodsLength; // Skip compression methods
|
|
154
|
+
|
|
155
|
+
// Extensions
|
|
156
|
+
if (offset + 2 > buffer.length) {
|
|
157
|
+
if (enableLogging) console.log("Buffer too small for extensions length");
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const extensionsLength = buffer.readUInt16BE(offset);
|
|
161
|
+
if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
|
|
162
|
+
offset += 2;
|
|
163
|
+
const extensionsEnd = offset + extensionsLength;
|
|
164
|
+
|
|
165
|
+
if (extensionsEnd > buffer.length) {
|
|
166
|
+
if (enableLogging) console.log(`Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`);
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
125
169
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
offset +=
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
170
|
+
// Parse extensions
|
|
171
|
+
while (offset + 4 <= extensionsEnd) {
|
|
172
|
+
const extensionType = buffer.readUInt16BE(offset);
|
|
173
|
+
const extensionLength = buffer.readUInt16BE(offset + 2);
|
|
174
|
+
|
|
175
|
+
if (enableLogging) console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
|
|
176
|
+
|
|
177
|
+
offset += 4;
|
|
178
|
+
|
|
179
|
+
if (extensionType === 0x0000) { // SNI extension
|
|
180
|
+
if (offset + 2 > buffer.length) {
|
|
181
|
+
if (enableLogging) console.log("Buffer too small for SNI list length");
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const sniListLength = buffer.readUInt16BE(offset);
|
|
186
|
+
if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
|
|
138
187
|
offset += 2;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
188
|
+
const sniListEnd = offset + sniListLength;
|
|
189
|
+
|
|
190
|
+
if (sniListEnd > buffer.length) {
|
|
191
|
+
if (enableLogging) console.log(`Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`);
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
while (offset + 3 < sniListEnd) {
|
|
196
|
+
const nameType = buffer.readUInt8(offset++);
|
|
197
|
+
const nameLen = buffer.readUInt16BE(offset);
|
|
198
|
+
offset += 2;
|
|
199
|
+
|
|
200
|
+
if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
|
|
201
|
+
|
|
202
|
+
if (nameType === 0) { // host_name
|
|
203
|
+
if (offset + nameLen > buffer.length) {
|
|
204
|
+
if (enableLogging) console.log(`Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${buffer.length}`);
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const serverName = buffer.toString('utf8', offset, offset + nameLen);
|
|
209
|
+
if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
|
|
210
|
+
return serverName;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
offset += nameLen;
|
|
142
214
|
}
|
|
143
|
-
|
|
215
|
+
break;
|
|
216
|
+
} else {
|
|
217
|
+
offset += extensionLength;
|
|
144
218
|
}
|
|
145
|
-
break;
|
|
146
|
-
} else {
|
|
147
|
-
offset += extensionLength;
|
|
148
219
|
}
|
|
220
|
+
|
|
221
|
+
if (enableLogging) console.log("No SNI extension found");
|
|
222
|
+
return undefined;
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.log(`Error extracting SNI: ${err}`);
|
|
225
|
+
return undefined;
|
|
149
226
|
}
|
|
150
|
-
return undefined;
|
|
151
227
|
}
|
|
152
228
|
|
|
153
229
|
// 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 }
|
|
|
157
233
|
|
|
158
234
|
// Helper: Check if a given IP matches any of the glob patterns
|
|
159
235
|
const isAllowed = (ip: string, patterns: string[]): boolean => {
|
|
236
|
+
if (!ip || !patterns || patterns.length === 0) return false;
|
|
237
|
+
|
|
160
238
|
const normalizeIP = (ip: string): string[] => {
|
|
239
|
+
if (!ip) return [];
|
|
161
240
|
if (ip.startsWith('::ffff:')) {
|
|
162
241
|
const ipv4 = ip.slice(7);
|
|
163
242
|
return [ip, ipv4];
|
|
@@ -167,7 +246,10 @@ const isAllowed = (ip: string, patterns: string[]): boolean => {
|
|
|
167
246
|
}
|
|
168
247
|
return [ip];
|
|
169
248
|
};
|
|
249
|
+
|
|
170
250
|
const normalizedIPVariants = normalizeIP(ip);
|
|
251
|
+
if (normalizedIPVariants.length === 0) return false;
|
|
252
|
+
|
|
171
253
|
const expandedPatterns = patterns.flatMap(normalizeIP);
|
|
172
254
|
return normalizedIPVariants.some(ipVariant =>
|
|
173
255
|
expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern))
|
|
@@ -176,6 +258,7 @@ const isAllowed = (ip: string, patterns: string[]): boolean => {
|
|
|
176
258
|
|
|
177
259
|
// Helper: Check if an IP is allowed considering allowed and blocked glob patterns
|
|
178
260
|
const isGlobIPAllowed = (ip: string, allowed: string[], blocked: string[] = []): boolean => {
|
|
261
|
+
if (!ip) return false;
|
|
179
262
|
if (blocked.length > 0 && isAllowed(ip, blocked)) return false;
|
|
180
263
|
return isAllowed(ip, allowed);
|
|
181
264
|
};
|
|
@@ -185,34 +268,17 @@ const generateConnectionId = (): string => {
|
|
|
185
268
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
186
269
|
};
|
|
187
270
|
|
|
188
|
-
//
|
|
189
|
-
const isHttpRequest = (buffer: Buffer): boolean => {
|
|
190
|
-
if (buffer.length < 4) return false;
|
|
191
|
-
const start = buffer.toString('ascii', 0, 4).toUpperCase();
|
|
192
|
-
return (
|
|
193
|
-
start.startsWith('GET ') ||
|
|
194
|
-
start.startsWith('POST') ||
|
|
195
|
-
start.startsWith('PUT ') ||
|
|
196
|
-
start.startsWith('HEAD') ||
|
|
197
|
-
start.startsWith('DELE') ||
|
|
198
|
-
start.startsWith('PATC') ||
|
|
199
|
-
start.startsWith('OPTI')
|
|
200
|
-
);
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const isWebSocketUpgrade = (buffer: Buffer): boolean => {
|
|
204
|
-
if (buffer.length < 20) return false;
|
|
205
|
-
const data = buffer.toString('ascii', 0, Math.min(buffer.length, 200));
|
|
206
|
-
return (
|
|
207
|
-
data.includes('Upgrade: websocket') ||
|
|
208
|
-
data.includes('Upgrade: WebSocket')
|
|
209
|
-
);
|
|
210
|
-
};
|
|
211
|
-
|
|
271
|
+
// Helper: Check if a buffer contains a TLS handshake
|
|
212
272
|
const isTlsHandshake = (buffer: Buffer): boolean => {
|
|
213
273
|
return buffer.length > 0 && buffer[0] === 22; // ContentType.handshake
|
|
214
274
|
};
|
|
215
275
|
|
|
276
|
+
// Helper: Generate a slightly randomized timeout to prevent thundering herd
|
|
277
|
+
const randomizeTimeout = (baseTimeout: number, variationPercent: number = 5): number => {
|
|
278
|
+
const variation = baseTimeout * (variationPercent / 100);
|
|
279
|
+
return baseTimeout + Math.floor(Math.random() * variation * 2) - variation;
|
|
280
|
+
};
|
|
281
|
+
|
|
216
282
|
export class PortProxy {
|
|
217
283
|
private netServers: plugins.net.Server[] = [];
|
|
218
284
|
settings: IPortProxySettings;
|
|
@@ -242,16 +308,12 @@ export class PortProxy {
|
|
|
242
308
|
...settingsArg,
|
|
243
309
|
targetIP: settingsArg.targetIP || 'localhost',
|
|
244
310
|
|
|
245
|
-
// Timeout settings with
|
|
246
|
-
initialDataTimeout: settingsArg.initialDataTimeout ||
|
|
247
|
-
socketTimeout: settingsArg.socketTimeout ||
|
|
248
|
-
inactivityCheckInterval: settingsArg.inactivityCheckInterval ||
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 3600000, // 1 hour default
|
|
252
|
-
httpConnectionTimeout: settingsArg.httpConnectionTimeout || 1800000, // 30 minutes
|
|
253
|
-
wsConnectionTimeout: settingsArg.wsConnectionTimeout || 14400000, // 4 hours
|
|
254
|
-
httpKeepAliveTimeout: settingsArg.httpKeepAliveTimeout || 1200000, // 20 minutes
|
|
311
|
+
// Timeout settings with our enhanced defaults
|
|
312
|
+
initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial data
|
|
313
|
+
socketTimeout: settingsArg.socketTimeout || 3600000, // 1 hour socket timeout
|
|
314
|
+
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval
|
|
315
|
+
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 3600000, // 1 hour default lifetime
|
|
316
|
+
inactivityTimeout: settingsArg.inactivityTimeout || 3600000, // 1 hour inactivity timeout
|
|
255
317
|
|
|
256
318
|
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
|
|
257
319
|
|
|
@@ -259,13 +321,14 @@ export class PortProxy {
|
|
|
259
321
|
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
|
260
322
|
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
|
261
323
|
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds
|
|
262
|
-
maxPendingDataSize: settingsArg.maxPendingDataSize || 1024 * 1024, //
|
|
324
|
+
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
|
|
263
325
|
|
|
264
326
|
// Feature flags
|
|
265
327
|
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
|
266
328
|
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes || false,
|
|
267
|
-
enableProtocolDetection: settingsArg.enableProtocolDetection !== undefined ? settingsArg.enableProtocolDetection : true,
|
|
268
329
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
330
|
+
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
|
331
|
+
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || true,
|
|
269
332
|
|
|
270
333
|
// Rate limiting defaults
|
|
271
334
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
|
|
@@ -332,115 +395,22 @@ export class PortProxy {
|
|
|
332
395
|
}
|
|
333
396
|
|
|
334
397
|
/**
|
|
335
|
-
* Get
|
|
398
|
+
* Get connection timeout based on domain config or default settings
|
|
336
399
|
*/
|
|
337
|
-
private
|
|
338
|
-
// If the
|
|
339
|
-
if (domainConfig) {
|
|
340
|
-
|
|
341
|
-
return domainConfig.httpTimeout;
|
|
342
|
-
}
|
|
343
|
-
if (record.protocolType === 'websocket' && domainConfig.wsTimeout) {
|
|
344
|
-
return domainConfig.wsTimeout;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Use HTTP keep-alive timeout from headers if available
|
|
349
|
-
if (record.httpKeepAliveTimeout) {
|
|
350
|
-
return record.httpKeepAliveTimeout;
|
|
400
|
+
private getConnectionTimeout(record: IConnectionRecord): number {
|
|
401
|
+
// If the connection has a domain-specific timeout, use that
|
|
402
|
+
if (record.domainConfig?.connectionTimeout) {
|
|
403
|
+
return record.domainConfig.connectionTimeout;
|
|
351
404
|
}
|
|
352
405
|
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return this.settings.wsConnectionTimeout!;
|
|
359
|
-
case 'https':
|
|
360
|
-
case 'tls':
|
|
361
|
-
return this.settings.httpConnectionTimeout!; // Use HTTP timeout for HTTPS by default
|
|
362
|
-
default:
|
|
363
|
-
return this.settings.maxConnectionLifetime!;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Detect protocol and update connection record
|
|
369
|
-
*/
|
|
370
|
-
private detectProtocol(data: Buffer, record: IConnectionRecord): void {
|
|
371
|
-
if (!this.settings.enableProtocolDetection || record.protocolType !== 'unknown') {
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
try {
|
|
376
|
-
// Detect TLS/HTTPS
|
|
377
|
-
if (isTlsHandshake(data)) {
|
|
378
|
-
record.protocolType = 'tls';
|
|
379
|
-
if (this.settings.enableDetailedLogging) {
|
|
380
|
-
console.log(`[${record.id}] Protocol detected: TLS`);
|
|
381
|
-
}
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Detect HTTP including WebSocket upgrades
|
|
386
|
-
if (isHttpRequest(data)) {
|
|
387
|
-
record.httpRequests++;
|
|
388
|
-
record.lastHttpRequest = Date.now();
|
|
389
|
-
|
|
390
|
-
// Check for WebSocket upgrade
|
|
391
|
-
if (isWebSocketUpgrade(data)) {
|
|
392
|
-
record.protocolType = 'websocket';
|
|
393
|
-
if (this.settings.enableDetailedLogging) {
|
|
394
|
-
console.log(`[${record.id}] Protocol detected: WebSocket Upgrade`);
|
|
395
|
-
}
|
|
396
|
-
} else {
|
|
397
|
-
record.protocolType = 'http';
|
|
398
|
-
|
|
399
|
-
// Parse HTTP keep-alive headers
|
|
400
|
-
this.parseHttpHeaders(data, record);
|
|
401
|
-
|
|
402
|
-
if (this.settings.enableDetailedLogging) {
|
|
403
|
-
console.log(`[${record.id}] Protocol detected: HTTP${record.isPooledConnection ? ' (KeepAlive)' : ''}`);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
} catch (err) {
|
|
408
|
-
console.log(`[${record.id}] Error detecting protocol: ${err}`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Parse HTTP headers for keep-alive and other connection info
|
|
414
|
-
*/
|
|
415
|
-
private parseHttpHeaders(data: Buffer, record: IConnectionRecord): void {
|
|
416
|
-
try {
|
|
417
|
-
const headerStr = data.toString('utf8', 0, Math.min(data.length, 1024));
|
|
418
|
-
|
|
419
|
-
// Check for HTTP keep-alive
|
|
420
|
-
const connectionHeader = headerStr.match(/\r\nConnection:\s*([^\r\n]+)/i);
|
|
421
|
-
if (connectionHeader && connectionHeader[1].toLowerCase().includes('keep-alive')) {
|
|
422
|
-
record.isPooledConnection = true;
|
|
423
|
-
|
|
424
|
-
// Check for Keep-Alive timeout value
|
|
425
|
-
const keepAliveHeader = headerStr.match(/\r\nKeep-Alive:\s*([^\r\n]+)/i);
|
|
426
|
-
if (keepAliveHeader) {
|
|
427
|
-
const timeoutMatch = keepAliveHeader[1].match(/timeout=(\d+)/i);
|
|
428
|
-
if (timeoutMatch && timeoutMatch[1]) {
|
|
429
|
-
const timeoutSec = parseInt(timeoutMatch[1], 10);
|
|
430
|
-
if (!isNaN(timeoutSec) && timeoutSec > 0) {
|
|
431
|
-
// Convert seconds to milliseconds and add some buffer
|
|
432
|
-
record.httpKeepAliveTimeout = (timeoutSec * 1000) + 5000;
|
|
433
|
-
|
|
434
|
-
if (this.settings.enableDetailedLogging) {
|
|
435
|
-
console.log(`[${record.id}] HTTP Keep-Alive timeout set to ${timeoutSec} seconds`);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
} catch (err) {
|
|
442
|
-
console.log(`[${record.id}] Error parsing HTTP headers: ${err}`);
|
|
406
|
+
// Use default timeout, potentially randomized
|
|
407
|
+
const baseTimeout = this.settings.maxConnectionLifetime!;
|
|
408
|
+
|
|
409
|
+
if (this.settings.enableRandomizedTimeouts) {
|
|
410
|
+
return randomizeTimeout(baseTimeout);
|
|
443
411
|
}
|
|
412
|
+
|
|
413
|
+
return baseTimeout;
|
|
444
414
|
}
|
|
445
415
|
|
|
446
416
|
/**
|
|
@@ -465,7 +435,6 @@ export class PortProxy {
|
|
|
465
435
|
const duration = Date.now() - record.incomingStartTime;
|
|
466
436
|
const bytesReceived = record.bytesReceived;
|
|
467
437
|
const bytesSent = record.bytesSent;
|
|
468
|
-
const httpRequests = record.httpRequests;
|
|
469
438
|
|
|
470
439
|
try {
|
|
471
440
|
if (!record.incoming.destroyed) {
|
|
@@ -538,7 +507,7 @@ export class PortProxy {
|
|
|
538
507
|
if (this.settings.enableDetailedLogging) {
|
|
539
508
|
console.log(`[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` +
|
|
540
509
|
` Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
|
|
541
|
-
`
|
|
510
|
+
`TLS: ${record.isTLS ? 'Yes' : 'No'}`);
|
|
542
511
|
} else {
|
|
543
512
|
console.log(`[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`);
|
|
544
513
|
}
|
|
@@ -608,6 +577,21 @@ export class PortProxy {
|
|
|
608
577
|
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
609
578
|
}
|
|
610
579
|
|
|
580
|
+
// Apply enhanced TCP options if available
|
|
581
|
+
if (this.settings.enableKeepAliveProbes) {
|
|
582
|
+
try {
|
|
583
|
+
// These are platform-specific and may not be available
|
|
584
|
+
if ('setKeepAliveProbes' in socket) {
|
|
585
|
+
(socket as any).setKeepAliveProbes(10);
|
|
586
|
+
}
|
|
587
|
+
if ('setKeepAliveInterval' in socket) {
|
|
588
|
+
(socket as any).setKeepAliveInterval(1000);
|
|
589
|
+
}
|
|
590
|
+
} catch (err) {
|
|
591
|
+
// Ignore errors - these are optional enhancements
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
611
595
|
// Create a unique connection ID and record
|
|
612
596
|
const connectionId = generateConnectionId();
|
|
613
597
|
const connectionRecord: IConnectionRecord = {
|
|
@@ -621,13 +605,13 @@ export class PortProxy {
|
|
|
621
605
|
pendingDataSize: 0,
|
|
622
606
|
|
|
623
607
|
// Initialize enhanced tracking fields
|
|
624
|
-
protocolType: 'unknown',
|
|
625
|
-
isPooledConnection: false,
|
|
626
608
|
bytesReceived: 0,
|
|
627
609
|
bytesSent: 0,
|
|
628
610
|
remoteIP: remoteIP,
|
|
629
611
|
localPort: localPort,
|
|
630
|
-
|
|
612
|
+
isTLS: false,
|
|
613
|
+
tlsHandshakeComplete: false,
|
|
614
|
+
hasReceivedInitialData: false
|
|
631
615
|
};
|
|
632
616
|
|
|
633
617
|
// Track connection by IP
|
|
@@ -685,9 +669,15 @@ export class PortProxy {
|
|
|
685
669
|
socket.end();
|
|
686
670
|
cleanupOnce();
|
|
687
671
|
}
|
|
688
|
-
}, this.settings.initialDataTimeout);
|
|
672
|
+
}, this.settings.initialDataTimeout!);
|
|
673
|
+
|
|
674
|
+
// Make sure timeout doesn't keep the process alive
|
|
675
|
+
if (initialTimeout.unref) {
|
|
676
|
+
initialTimeout.unref();
|
|
677
|
+
}
|
|
689
678
|
} else {
|
|
690
679
|
initialDataReceived = true;
|
|
680
|
+
connectionRecord.hasReceivedInitialData = true;
|
|
691
681
|
}
|
|
692
682
|
|
|
693
683
|
socket.on('error', (err: Error) => {
|
|
@@ -699,39 +689,14 @@ export class PortProxy {
|
|
|
699
689
|
connectionRecord.bytesReceived += chunk.length;
|
|
700
690
|
this.updateActivity(connectionRecord);
|
|
701
691
|
|
|
702
|
-
//
|
|
703
|
-
if (connectionRecord.
|
|
704
|
-
|
|
692
|
+
// Check for TLS handshake if this is the first chunk
|
|
693
|
+
if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
|
|
694
|
+
connectionRecord.isTLS = true;
|
|
705
695
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
// Set new timeout based on protocol
|
|
711
|
-
const protocolTimeout = this.getProtocolTimeout(connectionRecord);
|
|
712
|
-
connectionRecord.cleanupTimer = setTimeout(() => {
|
|
713
|
-
console.log(`[${connectionId}] ${connectionRecord.protocolType} connection timeout after ${plugins.prettyMs(protocolTimeout)}`);
|
|
714
|
-
initiateCleanupOnce(`${connectionRecord.protocolType}_timeout`);
|
|
715
|
-
}, protocolTimeout);
|
|
716
|
-
}
|
|
717
|
-
} else if (connectionRecord.protocolType === 'http' && isHttpRequest(chunk)) {
|
|
718
|
-
// Additional HTTP request on the same connection
|
|
719
|
-
connectionRecord.httpRequests++;
|
|
720
|
-
connectionRecord.lastHttpRequest = Date.now();
|
|
721
|
-
|
|
722
|
-
// Parse HTTP headers again for keep-alive changes
|
|
723
|
-
this.parseHttpHeaders(chunk, connectionRecord);
|
|
724
|
-
|
|
725
|
-
// Update timeout based on new HTTP headers
|
|
726
|
-
if (connectionRecord.cleanupTimer) {
|
|
727
|
-
clearTimeout(connectionRecord.cleanupTimer);
|
|
728
|
-
|
|
729
|
-
// Set new timeout based on updated HTTP info
|
|
730
|
-
const protocolTimeout = this.getProtocolTimeout(connectionRecord);
|
|
731
|
-
connectionRecord.cleanupTimer = setTimeout(() => {
|
|
732
|
-
console.log(`[${connectionId}] HTTP connection timeout after ${plugins.prettyMs(protocolTimeout)}`);
|
|
733
|
-
initiateCleanupOnce('http_timeout');
|
|
734
|
-
}, protocolTimeout);
|
|
696
|
+
if (this.settings.enableTlsDebugLogging) {
|
|
697
|
+
console.log(`[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`);
|
|
698
|
+
// Try to extract SNI and log detailed debug info
|
|
699
|
+
extractSNI(chunk, true);
|
|
735
700
|
}
|
|
736
701
|
}
|
|
737
702
|
});
|
|
@@ -797,9 +762,17 @@ export class PortProxy {
|
|
|
797
762
|
initialTimeout = null;
|
|
798
763
|
}
|
|
799
764
|
|
|
800
|
-
//
|
|
801
|
-
|
|
802
|
-
|
|
765
|
+
// Mark that we've received initial data
|
|
766
|
+
initialDataReceived = true;
|
|
767
|
+
connectionRecord.hasReceivedInitialData = true;
|
|
768
|
+
|
|
769
|
+
// Check if this looks like a TLS handshake
|
|
770
|
+
if (initialChunk && isTlsHandshake(initialChunk)) {
|
|
771
|
+
connectionRecord.isTLS = true;
|
|
772
|
+
|
|
773
|
+
if (this.settings.enableTlsDebugLogging) {
|
|
774
|
+
console.log(`[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`);
|
|
775
|
+
}
|
|
803
776
|
}
|
|
804
777
|
|
|
805
778
|
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
|
|
@@ -809,6 +782,9 @@ export class PortProxy {
|
|
|
809
782
|
config.domains.some(d => plugins.minimatch(serverName, d))
|
|
810
783
|
) : undefined);
|
|
811
784
|
|
|
785
|
+
// Save domain config in connection record
|
|
786
|
+
connectionRecord.domainConfig = domainConfig;
|
|
787
|
+
|
|
812
788
|
// IP validation is skipped if allowedIPs is empty
|
|
813
789
|
if (domainConfig) {
|
|
814
790
|
const effectiveAllowedIPs: string[] = [
|
|
@@ -847,9 +823,13 @@ export class PortProxy {
|
|
|
847
823
|
// Track bytes received
|
|
848
824
|
connectionRecord.bytesReceived += chunk.length;
|
|
849
825
|
|
|
850
|
-
//
|
|
851
|
-
if (
|
|
852
|
-
|
|
826
|
+
// Check for TLS handshake
|
|
827
|
+
if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
|
|
828
|
+
connectionRecord.isTLS = true;
|
|
829
|
+
|
|
830
|
+
if (this.settings.enableTlsDebugLogging) {
|
|
831
|
+
console.log(`[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`);
|
|
832
|
+
}
|
|
853
833
|
}
|
|
854
834
|
|
|
855
835
|
// Check if adding this chunk would exceed the buffer limit
|
|
@@ -888,6 +868,20 @@ export class PortProxy {
|
|
|
888
868
|
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
889
869
|
}
|
|
890
870
|
|
|
871
|
+
// Apply enhanced TCP options if available
|
|
872
|
+
if (this.settings.enableKeepAliveProbes) {
|
|
873
|
+
try {
|
|
874
|
+
if ('setKeepAliveProbes' in targetSocket) {
|
|
875
|
+
(targetSocket as any).setKeepAliveProbes(10);
|
|
876
|
+
}
|
|
877
|
+
if ('setKeepAliveInterval' in targetSocket) {
|
|
878
|
+
(targetSocket as any).setKeepAliveInterval(1000);
|
|
879
|
+
}
|
|
880
|
+
} catch (err) {
|
|
881
|
+
// Ignore errors - these are optional enhancements
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
891
885
|
// Setup specific error handler for connection phase
|
|
892
886
|
targetSocket.once('error', (err) => {
|
|
893
887
|
// This handler runs only once during the initial connection phase
|
|
@@ -928,7 +922,7 @@ export class PortProxy {
|
|
|
928
922
|
|
|
929
923
|
// Handle timeouts
|
|
930
924
|
socket.on('timeout', () => {
|
|
931
|
-
console.log(`[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout ||
|
|
925
|
+
console.log(`[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`);
|
|
932
926
|
if (incomingTerminationReason === null) {
|
|
933
927
|
incomingTerminationReason = 'timeout';
|
|
934
928
|
this.incrementTerminationStat('incoming', 'timeout');
|
|
@@ -937,7 +931,7 @@ export class PortProxy {
|
|
|
937
931
|
});
|
|
938
932
|
|
|
939
933
|
targetSocket.on('timeout', () => {
|
|
940
|
-
console.log(`[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout ||
|
|
934
|
+
console.log(`[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`);
|
|
941
935
|
if (outgoingTerminationReason === null) {
|
|
942
936
|
outgoingTerminationReason = 'timeout';
|
|
943
937
|
this.incrementTerminationStat('outgoing', 'timeout');
|
|
@@ -946,8 +940,8 @@ export class PortProxy {
|
|
|
946
940
|
});
|
|
947
941
|
|
|
948
942
|
// Set appropriate timeouts using the configured value
|
|
949
|
-
socket.setTimeout(this.settings.socketTimeout ||
|
|
950
|
-
targetSocket.setTimeout(this.settings.socketTimeout ||
|
|
943
|
+
socket.setTimeout(this.settings.socketTimeout || 3600000);
|
|
944
|
+
targetSocket.setTimeout(this.settings.socketTimeout || 3600000);
|
|
951
945
|
|
|
952
946
|
// Track outgoing data for bytes counting
|
|
953
947
|
targetSocket.on('data', (chunk: Buffer) => {
|
|
@@ -984,7 +978,7 @@ export class PortProxy {
|
|
|
984
978
|
console.log(
|
|
985
979
|
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
986
980
|
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` +
|
|
987
|
-
`
|
|
981
|
+
` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
|
|
988
982
|
);
|
|
989
983
|
} else {
|
|
990
984
|
console.log(
|
|
@@ -1003,7 +997,7 @@ export class PortProxy {
|
|
|
1003
997
|
console.log(
|
|
1004
998
|
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
1005
999
|
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` +
|
|
1006
|
-
`
|
|
1000
|
+
` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
|
|
1007
1001
|
);
|
|
1008
1002
|
} else {
|
|
1009
1003
|
console.log(
|
|
@@ -1023,7 +1017,7 @@ export class PortProxy {
|
|
|
1023
1017
|
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
1024
1018
|
try {
|
|
1025
1019
|
// Try to extract SNI from potential renegotiation
|
|
1026
|
-
const newSNI = extractSNI(renegChunk);
|
|
1020
|
+
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
|
|
1027
1021
|
if (newSNI && newSNI !== connectionRecord.lockedDomain) {
|
|
1028
1022
|
console.log(`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`);
|
|
1029
1023
|
initiateCleanupOnce('sni_mismatch');
|
|
@@ -1037,17 +1031,31 @@ export class PortProxy {
|
|
|
1037
1031
|
});
|
|
1038
1032
|
}
|
|
1039
1033
|
|
|
1040
|
-
// Set
|
|
1034
|
+
// Set connection timeout
|
|
1041
1035
|
if (connectionRecord.cleanupTimer) {
|
|
1042
1036
|
clearTimeout(connectionRecord.cleanupTimer);
|
|
1043
1037
|
}
|
|
1044
1038
|
|
|
1045
|
-
// Set timeout based on
|
|
1046
|
-
const
|
|
1039
|
+
// Set timeout based on domain config or default
|
|
1040
|
+
const connectionTimeout = this.getConnectionTimeout(connectionRecord);
|
|
1047
1041
|
connectionRecord.cleanupTimer = setTimeout(() => {
|
|
1048
|
-
console.log(`[${connectionId}] ${
|
|
1049
|
-
initiateCleanupOnce(
|
|
1050
|
-
},
|
|
1042
|
+
console.log(`[${connectionId}] Connection from ${remoteIP} exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`);
|
|
1043
|
+
initiateCleanupOnce('connection_timeout');
|
|
1044
|
+
}, connectionTimeout);
|
|
1045
|
+
|
|
1046
|
+
// Make sure timeout doesn't keep the process alive
|
|
1047
|
+
if (connectionRecord.cleanupTimer.unref) {
|
|
1048
|
+
connectionRecord.cleanupTimer.unref();
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Mark TLS handshake as complete for TLS connections
|
|
1052
|
+
if (connectionRecord.isTLS) {
|
|
1053
|
+
connectionRecord.tlsHandshakeComplete = true;
|
|
1054
|
+
|
|
1055
|
+
if (this.settings.enableTlsDebugLogging) {
|
|
1056
|
+
console.log(`[${connectionId}] TLS handshake complete for connection from ${remoteIP}`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1051
1059
|
});
|
|
1052
1060
|
};
|
|
1053
1061
|
|
|
@@ -1055,7 +1063,7 @@ export class PortProxy {
|
|
|
1055
1063
|
// Only apply port-based rules if the incoming port is within one of the global port ranges.
|
|
1056
1064
|
if (this.settings.globalPortRanges && isPortInRanges(localPort, this.settings.globalPortRanges)) {
|
|
1057
1065
|
if (this.settings.forwardAllGlobalRanges) {
|
|
1058
|
-
if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
1066
|
+
if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0 && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
1059
1067
|
console.log(`[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
|
|
1060
1068
|
socket.end();
|
|
1061
1069
|
return;
|
|
@@ -1111,7 +1119,20 @@ export class PortProxy {
|
|
|
1111
1119
|
}
|
|
1112
1120
|
|
|
1113
1121
|
initialDataReceived = true;
|
|
1114
|
-
|
|
1122
|
+
|
|
1123
|
+
// Try to extract SNI
|
|
1124
|
+
let serverName = '';
|
|
1125
|
+
|
|
1126
|
+
if (isTlsHandshake(chunk)) {
|
|
1127
|
+
connectionRecord.isTLS = true;
|
|
1128
|
+
|
|
1129
|
+
if (this.settings.enableTlsDebugLogging) {
|
|
1130
|
+
console.log(`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1115
1136
|
// Lock the connection to the negotiated SNI.
|
|
1116
1137
|
connectionRecord.lockedDomain = serverName;
|
|
1117
1138
|
|
|
@@ -1123,9 +1144,12 @@ export class PortProxy {
|
|
|
1123
1144
|
});
|
|
1124
1145
|
} else {
|
|
1125
1146
|
initialDataReceived = true;
|
|
1126
|
-
|
|
1147
|
+
connectionRecord.hasReceivedInitialData = true;
|
|
1148
|
+
|
|
1149
|
+
if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0 && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
1127
1150
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
|
|
1128
1151
|
}
|
|
1152
|
+
|
|
1129
1153
|
setupConnection('');
|
|
1130
1154
|
}
|
|
1131
1155
|
};
|
|
@@ -1167,11 +1191,10 @@ export class PortProxy {
|
|
|
1167
1191
|
const now = Date.now();
|
|
1168
1192
|
let maxIncoming = 0;
|
|
1169
1193
|
let maxOutgoing = 0;
|
|
1170
|
-
let httpConnections = 0;
|
|
1171
|
-
let wsConnections = 0;
|
|
1172
1194
|
let tlsConnections = 0;
|
|
1173
|
-
let
|
|
1174
|
-
let
|
|
1195
|
+
let nonTlsConnections = 0;
|
|
1196
|
+
let completedTlsHandshakes = 0;
|
|
1197
|
+
let pendingTlsHandshakes = 0;
|
|
1175
1198
|
|
|
1176
1199
|
// Create a copy of the keys to avoid modification during iteration
|
|
1177
1200
|
const connectionIds = [...this.connectionRecords.keys()];
|
|
@@ -1180,17 +1203,16 @@ export class PortProxy {
|
|
|
1180
1203
|
const record = this.connectionRecords.get(id);
|
|
1181
1204
|
if (!record) continue;
|
|
1182
1205
|
|
|
1183
|
-
// Track connection stats
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
pooledConnections++;
|
|
1206
|
+
// Track connection stats
|
|
1207
|
+
if (record.isTLS) {
|
|
1208
|
+
tlsConnections++;
|
|
1209
|
+
if (record.tlsHandshakeComplete) {
|
|
1210
|
+
completedTlsHandshakes++;
|
|
1211
|
+
} else {
|
|
1212
|
+
pendingTlsHandshakes++;
|
|
1213
|
+
}
|
|
1214
|
+
} else {
|
|
1215
|
+
nonTlsConnections++;
|
|
1194
1216
|
}
|
|
1195
1217
|
|
|
1196
1218
|
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
|
|
@@ -1208,23 +1230,20 @@ export class PortProxy {
|
|
|
1208
1230
|
this.cleanupConnection(record, 'parity_check');
|
|
1209
1231
|
}
|
|
1210
1232
|
|
|
1233
|
+
// Check for stalled connections waiting for initial data
|
|
1234
|
+
if (!record.hasReceivedInitialData &&
|
|
1235
|
+
(now - record.incomingStartTime > this.settings.initialDataTimeout! / 2)) {
|
|
1236
|
+
console.log(`[${id}] Warning: Connection from ${record.remoteIP} has not received initial data after ${plugins.prettyMs(now - record.incomingStartTime)}`);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1211
1239
|
// Skip inactivity check if disabled
|
|
1212
1240
|
if (!this.settings.disableInactivityCheck) {
|
|
1213
|
-
// Inactivity check
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
// Set protocol-specific inactivity thresholds
|
|
1217
|
-
if (record.protocolType === 'http' && record.isPooledConnection) {
|
|
1218
|
-
inactivityThreshold = this.settings.httpKeepAliveTimeout || 1200000; // 20 minutes for pooled HTTP
|
|
1219
|
-
} else if (record.protocolType === 'websocket') {
|
|
1220
|
-
inactivityThreshold = this.settings.wsConnectionTimeout || 14400000; // 4 hours for WebSocket
|
|
1221
|
-
} else if (record.protocolType === 'http') {
|
|
1222
|
-
inactivityThreshold = this.settings.httpConnectionTimeout || 1800000; // 30 minutes for HTTP
|
|
1223
|
-
}
|
|
1241
|
+
// Inactivity check with configurable timeout
|
|
1242
|
+
const inactivityThreshold = this.settings.inactivityTimeout!;
|
|
1224
1243
|
|
|
1225
1244
|
const inactivityTime = now - record.lastActivity;
|
|
1226
1245
|
if (inactivityTime > inactivityThreshold && !record.connectionClosed) {
|
|
1227
|
-
console.log(`[${id}] Inactivity check: No activity on
|
|
1246
|
+
console.log(`[${id}] Inactivity check: No activity on connection from ${record.remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
|
|
1228
1247
|
this.cleanupConnection(record, 'inactivity');
|
|
1229
1248
|
}
|
|
1230
1249
|
}
|
|
@@ -1233,11 +1252,11 @@ export class PortProxy {
|
|
|
1233
1252
|
// Log detailed stats periodically
|
|
1234
1253
|
console.log(
|
|
1235
1254
|
`Active connections: ${this.connectionRecords.size}. ` +
|
|
1236
|
-
`Types:
|
|
1255
|
+
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), Non-TLS=${nonTlsConnections}. ` +
|
|
1237
1256
|
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` +
|
|
1238
1257
|
`Termination stats: ${JSON.stringify({IN: this.terminationStats.incoming, OUT: this.terminationStats.outgoing})}`
|
|
1239
1258
|
);
|
|
1240
|
-
}, this.settings.inactivityCheckInterval ||
|
|
1259
|
+
}, this.settings.inactivityCheckInterval || 60000);
|
|
1241
1260
|
|
|
1242
1261
|
// Make sure the interval doesn't keep the process alive
|
|
1243
1262
|
if (this.connectionLogger.unref) {
|