@push.rocks/smartproxy 3.28.0 → 3.28.1
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.js +73 -44
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +487 -283
package/ts/classes.portproxy.ts
CHANGED
|
@@ -2,10 +2,10 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
|
|
3
3
|
/** Domain configuration with per-domain allowed port ranges */
|
|
4
4
|
export interface IDomainConfig {
|
|
5
|
-
domains: string[];
|
|
6
|
-
allowedIPs: string[];
|
|
7
|
-
blockedIPs?: string[];
|
|
8
|
-
targetIPs?: string[];
|
|
5
|
+
domains: string[]; // Glob patterns for domain(s)
|
|
6
|
+
allowedIPs: string[]; // Glob patterns for allowed IPs
|
|
7
|
+
blockedIPs?: string[]; // Glob patterns for blocked IPs
|
|
8
|
+
targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
|
|
9
9
|
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
|
10
10
|
// Allow domain-specific timeout override
|
|
11
11
|
connectionTimeout?: number; // Connection timeout override (ms)
|
|
@@ -21,33 +21,33 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
21
21
|
defaultAllowedIPs?: string[];
|
|
22
22
|
defaultBlockedIPs?: string[];
|
|
23
23
|
preserveSourceIP?: boolean;
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
// Timeout settings
|
|
26
|
-
initialDataTimeout?: number;
|
|
27
|
-
socketTimeout?: number;
|
|
28
|
-
inactivityCheckInterval?: number;
|
|
29
|
-
maxConnectionLifetime?: number;
|
|
30
|
-
inactivityTimeout?: number;
|
|
31
|
-
|
|
32
|
-
gracefulShutdownTimeout?: number;
|
|
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)
|
|
31
|
+
|
|
32
|
+
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
|
|
33
33
|
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
|
|
34
|
-
forwardAllGlobalRanges?: boolean;
|
|
35
|
-
|
|
34
|
+
forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
|
|
35
|
+
|
|
36
36
|
// Socket optimization settings
|
|
37
|
-
noDelay?: boolean;
|
|
38
|
-
keepAlive?: boolean;
|
|
39
|
-
keepAliveInitialDelay?: number;
|
|
40
|
-
maxPendingDataSize?: number;
|
|
41
|
-
|
|
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
|
|
41
|
+
|
|
42
42
|
// Enhanced features
|
|
43
|
-
disableInactivityCheck?: boolean;
|
|
44
|
-
enableKeepAliveProbes?: boolean;
|
|
45
|
-
enableDetailedLogging?: boolean;
|
|
46
|
-
enableTlsDebugLogging?: boolean;
|
|
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
47
|
enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
// Rate limiting and security
|
|
50
|
-
maxConnectionsPerIP?: number;
|
|
50
|
+
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
|
|
51
51
|
connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -55,28 +55,28 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
55
55
|
* Enhanced connection record
|
|
56
56
|
*/
|
|
57
57
|
interface IConnectionRecord {
|
|
58
|
-
id: string;
|
|
58
|
+
id: string; // Unique connection identifier
|
|
59
59
|
incoming: plugins.net.Socket;
|
|
60
60
|
outgoing: plugins.net.Socket | null;
|
|
61
61
|
incomingStartTime: number;
|
|
62
62
|
outgoingStartTime?: number;
|
|
63
63
|
outgoingClosedTime?: number;
|
|
64
|
-
lockedDomain?: string;
|
|
65
|
-
connectionClosed: boolean;
|
|
66
|
-
cleanupTimer?: NodeJS.Timeout;
|
|
67
|
-
lastActivity: number;
|
|
68
|
-
pendingData: Buffer[];
|
|
69
|
-
pendingDataSize: number;
|
|
70
|
-
|
|
64
|
+
lockedDomain?: string; // Used to lock this connection to the initial SNI
|
|
65
|
+
connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
|
|
66
|
+
cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
|
|
67
|
+
lastActivity: number; // Last activity timestamp for inactivity detection
|
|
68
|
+
pendingData: Buffer[]; // Buffer to hold data during connection setup
|
|
69
|
+
pendingDataSize: number; // Track total size of pending data
|
|
70
|
+
|
|
71
71
|
// Enhanced tracking fields
|
|
72
|
-
bytesReceived: number;
|
|
73
|
-
bytesSent: number;
|
|
74
|
-
remoteIP: string;
|
|
75
|
-
localPort: number;
|
|
76
|
-
isTLS: boolean;
|
|
77
|
-
tlsHandshakeComplete: boolean;
|
|
78
|
-
hasReceivedInitialData: boolean;
|
|
79
|
-
domainConfig?: IDomainConfig;
|
|
72
|
+
bytesReceived: number; // Total bytes received
|
|
73
|
+
bytesSent: number; // Total bytes sent
|
|
74
|
+
remoteIP: string; // Remote IP (cached for logging after socket close)
|
|
75
|
+
localPort: number; // Local port (cached for logging)
|
|
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
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
@@ -90,7 +90,7 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
90
90
|
try {
|
|
91
91
|
// Check if buffer is too small for TLS
|
|
92
92
|
if (buffer.length < 5) {
|
|
93
|
-
if (enableLogging) console.log(
|
|
93
|
+
if (enableLogging) console.log('Buffer too small for TLS header');
|
|
94
94
|
return undefined;
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -105,11 +105,14 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
105
105
|
const majorVersion = buffer.readUInt8(1);
|
|
106
106
|
const minorVersion = buffer.readUInt8(2);
|
|
107
107
|
if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion}`);
|
|
108
|
-
|
|
108
|
+
|
|
109
109
|
// Check record length
|
|
110
110
|
const recordLength = buffer.readUInt16BE(3);
|
|
111
111
|
if (buffer.length < 5 + recordLength) {
|
|
112
|
-
if (enableLogging)
|
|
112
|
+
if (enableLogging)
|
|
113
|
+
console.log(
|
|
114
|
+
`Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`
|
|
115
|
+
);
|
|
113
116
|
return undefined;
|
|
114
117
|
}
|
|
115
118
|
|
|
@@ -121,12 +124,12 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
offset += 4; // Skip handshake header (type + length)
|
|
124
|
-
|
|
127
|
+
|
|
125
128
|
// Client version
|
|
126
129
|
const clientMajorVersion = buffer.readUInt8(offset);
|
|
127
130
|
const clientMinorVersion = buffer.readUInt8(offset + 1);
|
|
128
131
|
if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
|
|
129
|
-
|
|
132
|
+
|
|
130
133
|
offset += 2 + 32; // Skip client version and random
|
|
131
134
|
|
|
132
135
|
// Session ID
|
|
@@ -136,7 +139,7 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
136
139
|
|
|
137
140
|
// Cipher suites
|
|
138
141
|
if (offset + 2 > buffer.length) {
|
|
139
|
-
if (enableLogging) console.log(
|
|
142
|
+
if (enableLogging) console.log('Buffer too small for cipher suites length');
|
|
140
143
|
return undefined;
|
|
141
144
|
}
|
|
142
145
|
const cipherSuitesLength = buffer.readUInt16BE(offset);
|
|
@@ -145,7 +148,7 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
145
148
|
|
|
146
149
|
// Compression methods
|
|
147
150
|
if (offset + 1 > buffer.length) {
|
|
148
|
-
if (enableLogging) console.log(
|
|
151
|
+
if (enableLogging) console.log('Buffer too small for compression methods length');
|
|
149
152
|
return undefined;
|
|
150
153
|
}
|
|
151
154
|
const compressionMethodsLength = buffer.readUInt8(offset);
|
|
@@ -154,7 +157,7 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
154
157
|
|
|
155
158
|
// Extensions
|
|
156
159
|
if (offset + 2 > buffer.length) {
|
|
157
|
-
if (enableLogging) console.log(
|
|
160
|
+
if (enableLogging) console.log('Buffer too small for extensions length');
|
|
158
161
|
return undefined;
|
|
159
162
|
}
|
|
160
163
|
const extensionsLength = buffer.readUInt16BE(offset);
|
|
@@ -163,7 +166,10 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
163
166
|
const extensionsEnd = offset + extensionsLength;
|
|
164
167
|
|
|
165
168
|
if (extensionsEnd > buffer.length) {
|
|
166
|
-
if (enableLogging)
|
|
169
|
+
if (enableLogging)
|
|
170
|
+
console.log(
|
|
171
|
+
`Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`
|
|
172
|
+
);
|
|
167
173
|
return undefined;
|
|
168
174
|
}
|
|
169
175
|
|
|
@@ -171,45 +177,56 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
171
177
|
while (offset + 4 <= extensionsEnd) {
|
|
172
178
|
const extensionType = buffer.readUInt16BE(offset);
|
|
173
179
|
const extensionLength = buffer.readUInt16BE(offset + 2);
|
|
174
|
-
|
|
175
|
-
if (enableLogging)
|
|
176
|
-
|
|
180
|
+
|
|
181
|
+
if (enableLogging)
|
|
182
|
+
console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
|
|
183
|
+
|
|
177
184
|
offset += 4;
|
|
178
|
-
|
|
179
|
-
if (extensionType === 0x0000) {
|
|
185
|
+
|
|
186
|
+
if (extensionType === 0x0000) {
|
|
187
|
+
// SNI extension
|
|
180
188
|
if (offset + 2 > buffer.length) {
|
|
181
|
-
if (enableLogging) console.log(
|
|
189
|
+
if (enableLogging) console.log('Buffer too small for SNI list length');
|
|
182
190
|
return undefined;
|
|
183
191
|
}
|
|
184
|
-
|
|
192
|
+
|
|
185
193
|
const sniListLength = buffer.readUInt16BE(offset);
|
|
186
194
|
if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
|
|
187
195
|
offset += 2;
|
|
188
196
|
const sniListEnd = offset + sniListLength;
|
|
189
|
-
|
|
197
|
+
|
|
190
198
|
if (sniListEnd > buffer.length) {
|
|
191
|
-
if (enableLogging)
|
|
199
|
+
if (enableLogging)
|
|
200
|
+
console.log(
|
|
201
|
+
`Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`
|
|
202
|
+
);
|
|
192
203
|
return undefined;
|
|
193
204
|
}
|
|
194
|
-
|
|
205
|
+
|
|
195
206
|
while (offset + 3 < sniListEnd) {
|
|
196
207
|
const nameType = buffer.readUInt8(offset++);
|
|
197
208
|
const nameLen = buffer.readUInt16BE(offset);
|
|
198
209
|
offset += 2;
|
|
199
|
-
|
|
210
|
+
|
|
200
211
|
if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
|
|
201
|
-
|
|
202
|
-
if (nameType === 0) {
|
|
212
|
+
|
|
213
|
+
if (nameType === 0) {
|
|
214
|
+
// host_name
|
|
203
215
|
if (offset + nameLen > buffer.length) {
|
|
204
|
-
if (enableLogging)
|
|
216
|
+
if (enableLogging)
|
|
217
|
+
console.log(
|
|
218
|
+
`Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${
|
|
219
|
+
buffer.length
|
|
220
|
+
}`
|
|
221
|
+
);
|
|
205
222
|
return undefined;
|
|
206
223
|
}
|
|
207
|
-
|
|
224
|
+
|
|
208
225
|
const serverName = buffer.toString('utf8', offset, offset + nameLen);
|
|
209
226
|
if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
|
|
210
227
|
return serverName;
|
|
211
228
|
}
|
|
212
|
-
|
|
229
|
+
|
|
213
230
|
offset += nameLen;
|
|
214
231
|
}
|
|
215
232
|
break;
|
|
@@ -217,8 +234,8 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
217
234
|
offset += extensionLength;
|
|
218
235
|
}
|
|
219
236
|
}
|
|
220
|
-
|
|
221
|
-
if (enableLogging) console.log(
|
|
237
|
+
|
|
238
|
+
if (enableLogging) console.log('No SNI extension found');
|
|
222
239
|
return undefined;
|
|
223
240
|
} catch (err) {
|
|
224
241
|
console.log(`Error extracting SNI: ${err}`);
|
|
@@ -228,13 +245,13 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
228
245
|
|
|
229
246
|
// Helper: Check if a port falls within any of the given port ranges
|
|
230
247
|
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
|
|
231
|
-
return ranges.some(range => port >= range.from && port <= range.to);
|
|
248
|
+
return ranges.some((range) => port >= range.from && port <= range.to);
|
|
232
249
|
};
|
|
233
250
|
|
|
234
251
|
// Helper: Check if a given IP matches any of the glob patterns
|
|
235
252
|
const isAllowed = (ip: string, patterns: string[]): boolean => {
|
|
236
253
|
if (!ip || !patterns || patterns.length === 0) return false;
|
|
237
|
-
|
|
254
|
+
|
|
238
255
|
const normalizeIP = (ip: string): string[] => {
|
|
239
256
|
if (!ip) return [];
|
|
240
257
|
if (ip.startsWith('::ffff:')) {
|
|
@@ -246,13 +263,13 @@ const isAllowed = (ip: string, patterns: string[]): boolean => {
|
|
|
246
263
|
}
|
|
247
264
|
return [ip];
|
|
248
265
|
};
|
|
249
|
-
|
|
266
|
+
|
|
250
267
|
const normalizedIPVariants = normalizeIP(ip);
|
|
251
268
|
if (normalizedIPVariants.length === 0) return false;
|
|
252
|
-
|
|
269
|
+
|
|
253
270
|
const expandedPatterns = patterns.flatMap(normalizeIP);
|
|
254
|
-
return normalizedIPVariants.some(ipVariant =>
|
|
255
|
-
expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern))
|
|
271
|
+
return normalizedIPVariants.some((ipVariant) =>
|
|
272
|
+
expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern))
|
|
256
273
|
);
|
|
257
274
|
};
|
|
258
275
|
|
|
@@ -297,7 +314,7 @@ export class PortProxy {
|
|
|
297
314
|
incoming: {},
|
|
298
315
|
outgoing: {},
|
|
299
316
|
};
|
|
300
|
-
|
|
317
|
+
|
|
301
318
|
// Connection tracking by IP for rate limiting
|
|
302
319
|
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
303
320
|
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
@@ -307,29 +324,29 @@ export class PortProxy {
|
|
|
307
324
|
this.settings = {
|
|
308
325
|
...settingsArg,
|
|
309
326
|
targetIP: settingsArg.targetIP || 'localhost',
|
|
310
|
-
|
|
327
|
+
|
|
311
328
|
// Timeout settings with our enhanced defaults
|
|
312
|
-
initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial
|
|
313
|
-
socketTimeout: settingsArg.socketTimeout ||
|
|
329
|
+
initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial handshake
|
|
330
|
+
socketTimeout: settingsArg.socketTimeout || 2592000000, // 30 days socket timeout
|
|
314
331
|
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval
|
|
315
|
-
maxConnectionLifetime: settingsArg.maxConnectionLifetime ||
|
|
316
|
-
inactivityTimeout: settingsArg.inactivityTimeout ||
|
|
317
|
-
|
|
332
|
+
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 2592000000, // 30 days max lifetime
|
|
333
|
+
inactivityTimeout: settingsArg.inactivityTimeout || 14400000, // 4 hours inactivity timeout
|
|
334
|
+
|
|
318
335
|
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
|
|
319
|
-
|
|
336
|
+
|
|
320
337
|
// Socket optimization settings
|
|
321
338
|
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
|
322
339
|
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
|
323
340
|
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds
|
|
324
341
|
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
|
|
325
|
-
|
|
342
|
+
|
|
326
343
|
// Feature flags
|
|
327
344
|
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
|
328
345
|
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes || false,
|
|
329
346
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
330
347
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
|
331
348
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || true,
|
|
332
|
-
|
|
349
|
+
|
|
333
350
|
// Rate limiting defaults
|
|
334
351
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
|
|
335
352
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
|
|
@@ -349,17 +366,17 @@ export class PortProxy {
|
|
|
349
366
|
private checkConnectionRate(ip: string): boolean {
|
|
350
367
|
const now = Date.now();
|
|
351
368
|
const minute = 60 * 1000;
|
|
352
|
-
|
|
369
|
+
|
|
353
370
|
if (!this.connectionRateByIP.has(ip)) {
|
|
354
371
|
this.connectionRateByIP.set(ip, [now]);
|
|
355
372
|
return true;
|
|
356
373
|
}
|
|
357
|
-
|
|
374
|
+
|
|
358
375
|
// Get timestamps and filter out entries older than 1 minute
|
|
359
|
-
const timestamps = this.connectionRateByIP.get(ip)!.filter(time => now - time < minute);
|
|
376
|
+
const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute);
|
|
360
377
|
timestamps.push(now);
|
|
361
378
|
this.connectionRateByIP.set(ip, timestamps);
|
|
362
|
-
|
|
379
|
+
|
|
363
380
|
// Check if rate exceeds limit
|
|
364
381
|
return timestamps.length <= this.settings.connectionRateLimitPerMinute!;
|
|
365
382
|
}
|
|
@@ -402,14 +419,14 @@ export class PortProxy {
|
|
|
402
419
|
if (record.domainConfig?.connectionTimeout) {
|
|
403
420
|
return record.domainConfig.connectionTimeout;
|
|
404
421
|
}
|
|
405
|
-
|
|
422
|
+
|
|
406
423
|
// Use default timeout, potentially randomized
|
|
407
424
|
const baseTimeout = this.settings.maxConnectionLifetime!;
|
|
408
|
-
|
|
425
|
+
|
|
409
426
|
if (this.settings.enableRandomizedTimeouts) {
|
|
410
427
|
return randomizeTimeout(baseTimeout);
|
|
411
428
|
}
|
|
412
|
-
|
|
429
|
+
|
|
413
430
|
return baseTimeout;
|
|
414
431
|
}
|
|
415
432
|
|
|
@@ -422,20 +439,20 @@ export class PortProxy {
|
|
|
422
439
|
private cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void {
|
|
423
440
|
if (!record.connectionClosed) {
|
|
424
441
|
record.connectionClosed = true;
|
|
425
|
-
|
|
442
|
+
|
|
426
443
|
// Track connection termination
|
|
427
444
|
this.removeConnectionByIP(record.remoteIP, record.id);
|
|
428
|
-
|
|
445
|
+
|
|
429
446
|
if (record.cleanupTimer) {
|
|
430
447
|
clearTimeout(record.cleanupTimer);
|
|
431
448
|
record.cleanupTimer = undefined;
|
|
432
449
|
}
|
|
433
|
-
|
|
450
|
+
|
|
434
451
|
// Detailed logging data
|
|
435
452
|
const duration = Date.now() - record.incomingStartTime;
|
|
436
453
|
const bytesReceived = record.bytesReceived;
|
|
437
454
|
const bytesSent = record.bytesSent;
|
|
438
|
-
|
|
455
|
+
|
|
439
456
|
try {
|
|
440
457
|
if (!record.incoming.destroyed) {
|
|
441
458
|
// Try graceful shutdown first, then force destroy after a short timeout
|
|
@@ -449,7 +466,7 @@ export class PortProxy {
|
|
|
449
466
|
console.log(`[${record.id}] Error destroying incoming socket: ${err}`);
|
|
450
467
|
}
|
|
451
468
|
}, 1000);
|
|
452
|
-
|
|
469
|
+
|
|
453
470
|
// Ensure the timeout doesn't block Node from exiting
|
|
454
471
|
if (incomingTimeout.unref) {
|
|
455
472
|
incomingTimeout.unref();
|
|
@@ -465,7 +482,7 @@ export class PortProxy {
|
|
|
465
482
|
console.log(`[${record.id}] Error destroying incoming socket: ${destroyErr}`);
|
|
466
483
|
}
|
|
467
484
|
}
|
|
468
|
-
|
|
485
|
+
|
|
469
486
|
try {
|
|
470
487
|
if (record.outgoing && !record.outgoing.destroyed) {
|
|
471
488
|
// Try graceful shutdown first, then force destroy after a short timeout
|
|
@@ -479,7 +496,7 @@ export class PortProxy {
|
|
|
479
496
|
console.log(`[${record.id}] Error destroying outgoing socket: ${err}`);
|
|
480
497
|
}
|
|
481
498
|
}, 1000);
|
|
482
|
-
|
|
499
|
+
|
|
483
500
|
// Ensure the timeout doesn't block Node from exiting
|
|
484
501
|
if (outgoingTimeout.unref) {
|
|
485
502
|
outgoingTimeout.unref();
|
|
@@ -495,21 +512,27 @@ export class PortProxy {
|
|
|
495
512
|
console.log(`[${record.id}] Error destroying outgoing socket: ${destroyErr}`);
|
|
496
513
|
}
|
|
497
514
|
}
|
|
498
|
-
|
|
515
|
+
|
|
499
516
|
// Clear pendingData to avoid memory leaks
|
|
500
517
|
record.pendingData = [];
|
|
501
518
|
record.pendingDataSize = 0;
|
|
502
|
-
|
|
519
|
+
|
|
503
520
|
// Remove the record from the tracking map
|
|
504
521
|
this.connectionRecords.delete(record.id);
|
|
505
|
-
|
|
522
|
+
|
|
506
523
|
// Log connection details
|
|
507
524
|
if (this.settings.enableDetailedLogging) {
|
|
508
|
-
console.log(
|
|
509
|
-
|
|
510
|
-
|
|
525
|
+
console.log(
|
|
526
|
+
`[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` +
|
|
527
|
+
` Duration: ${plugins.prettyMs(
|
|
528
|
+
duration
|
|
529
|
+
)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
|
|
530
|
+
`TLS: ${record.isTLS ? 'Yes' : 'No'}`
|
|
531
|
+
);
|
|
511
532
|
} else {
|
|
512
|
-
console.log(
|
|
533
|
+
console.log(
|
|
534
|
+
`[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`
|
|
535
|
+
);
|
|
513
536
|
}
|
|
514
537
|
}
|
|
515
538
|
}
|
|
@@ -543,7 +566,7 @@ export class PortProxy {
|
|
|
543
566
|
console.log("Cannot start PortProxy while it's shutting down");
|
|
544
567
|
return;
|
|
545
568
|
}
|
|
546
|
-
|
|
569
|
+
|
|
547
570
|
// Define a unified connection handler for all listening ports.
|
|
548
571
|
const connectionHandler = (socket: plugins.net.Socket) => {
|
|
549
572
|
if (this.isShuttingDown) {
|
|
@@ -554,29 +577,35 @@ export class PortProxy {
|
|
|
554
577
|
|
|
555
578
|
const remoteIP = socket.remoteAddress || '';
|
|
556
579
|
const localPort = socket.localPort || 0; // The port on which this connection was accepted.
|
|
557
|
-
|
|
580
|
+
|
|
558
581
|
// Check rate limits
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
-
|
|
582
|
+
if (
|
|
583
|
+
this.settings.maxConnectionsPerIP &&
|
|
584
|
+
this.getConnectionCountByIP(remoteIP) >= this.settings.maxConnectionsPerIP
|
|
585
|
+
) {
|
|
586
|
+
console.log(
|
|
587
|
+
`Connection rejected from ${remoteIP}: Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded`
|
|
588
|
+
);
|
|
562
589
|
socket.end();
|
|
563
590
|
socket.destroy();
|
|
564
591
|
return;
|
|
565
592
|
}
|
|
566
|
-
|
|
593
|
+
|
|
567
594
|
if (this.settings.connectionRateLimitPerMinute && !this.checkConnectionRate(remoteIP)) {
|
|
568
|
-
console.log(
|
|
595
|
+
console.log(
|
|
596
|
+
`Connection rejected from ${remoteIP}: Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded`
|
|
597
|
+
);
|
|
569
598
|
socket.end();
|
|
570
599
|
socket.destroy();
|
|
571
600
|
return;
|
|
572
601
|
}
|
|
573
|
-
|
|
602
|
+
|
|
574
603
|
// Apply socket optimizations
|
|
575
604
|
socket.setNoDelay(this.settings.noDelay);
|
|
576
605
|
if (this.settings.keepAlive) {
|
|
577
606
|
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
578
607
|
}
|
|
579
|
-
|
|
608
|
+
|
|
580
609
|
// Apply enhanced TCP options if available
|
|
581
610
|
if (this.settings.enableKeepAliveProbes) {
|
|
582
611
|
try {
|
|
@@ -591,7 +620,7 @@ export class PortProxy {
|
|
|
591
620
|
// Ignore errors - these are optional enhancements
|
|
592
621
|
}
|
|
593
622
|
}
|
|
594
|
-
|
|
623
|
+
|
|
595
624
|
// Create a unique connection ID and record
|
|
596
625
|
const connectionId = generateConnectionId();
|
|
597
626
|
const connectionRecord: IConnectionRecord = {
|
|
@@ -603,7 +632,7 @@ export class PortProxy {
|
|
|
603
632
|
connectionClosed: false,
|
|
604
633
|
pendingData: [],
|
|
605
634
|
pendingDataSize: 0,
|
|
606
|
-
|
|
635
|
+
|
|
607
636
|
// Initialize enhanced tracking fields
|
|
608
637
|
bytesReceived: 0,
|
|
609
638
|
bytesSent: 0,
|
|
@@ -611,17 +640,21 @@ export class PortProxy {
|
|
|
611
640
|
localPort: localPort,
|
|
612
641
|
isTLS: false,
|
|
613
642
|
tlsHandshakeComplete: false,
|
|
614
|
-
hasReceivedInitialData: false
|
|
643
|
+
hasReceivedInitialData: false,
|
|
615
644
|
};
|
|
616
|
-
|
|
645
|
+
|
|
617
646
|
// Track connection by IP
|
|
618
647
|
this.trackConnectionByIP(remoteIP, connectionId);
|
|
619
648
|
this.connectionRecords.set(connectionId, connectionRecord);
|
|
620
|
-
|
|
649
|
+
|
|
621
650
|
if (this.settings.enableDetailedLogging) {
|
|
622
|
-
console.log(
|
|
651
|
+
console.log(
|
|
652
|
+
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`
|
|
653
|
+
);
|
|
623
654
|
} else {
|
|
624
|
-
console.log(
|
|
655
|
+
console.log(
|
|
656
|
+
`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`
|
|
657
|
+
);
|
|
625
658
|
}
|
|
626
659
|
|
|
627
660
|
let initialDataReceived = false;
|
|
@@ -632,7 +665,7 @@ export class PortProxy {
|
|
|
632
665
|
const cleanupOnce = () => {
|
|
633
666
|
this.cleanupConnection(connectionRecord);
|
|
634
667
|
};
|
|
635
|
-
|
|
668
|
+
|
|
636
669
|
// Define initiateCleanupOnce for compatibility
|
|
637
670
|
const initiateCleanupOnce = (reason: string = 'normal') => {
|
|
638
671
|
if (this.settings.enableDetailedLogging) {
|
|
@@ -661,7 +694,9 @@ export class PortProxy {
|
|
|
661
694
|
if (this.settings.sniEnabled) {
|
|
662
695
|
initialTimeout = setTimeout(() => {
|
|
663
696
|
if (!initialDataReceived) {
|
|
664
|
-
console.log(
|
|
697
|
+
console.log(
|
|
698
|
+
`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
|
|
699
|
+
);
|
|
665
700
|
if (incomingTerminationReason === null) {
|
|
666
701
|
incomingTerminationReason = 'initial_timeout';
|
|
667
702
|
this.incrementTerminationStat('incoming', 'initial_timeout');
|
|
@@ -670,7 +705,7 @@ export class PortProxy {
|
|
|
670
705
|
cleanupOnce();
|
|
671
706
|
}
|
|
672
707
|
}, this.settings.initialDataTimeout!);
|
|
673
|
-
|
|
708
|
+
|
|
674
709
|
// Make sure timeout doesn't keep the process alive
|
|
675
710
|
if (initialTimeout.unref) {
|
|
676
711
|
initialTimeout.unref();
|
|
@@ -688,13 +723,15 @@ export class PortProxy {
|
|
|
688
723
|
socket.on('data', (chunk: Buffer) => {
|
|
689
724
|
connectionRecord.bytesReceived += chunk.length;
|
|
690
725
|
this.updateActivity(connectionRecord);
|
|
691
|
-
|
|
726
|
+
|
|
692
727
|
// Check for TLS handshake if this is the first chunk
|
|
693
728
|
if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
|
|
694
729
|
connectionRecord.isTLS = true;
|
|
695
|
-
|
|
730
|
+
|
|
696
731
|
if (this.settings.enableTlsDebugLogging) {
|
|
697
|
-
console.log(
|
|
732
|
+
console.log(
|
|
733
|
+
`[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`
|
|
734
|
+
);
|
|
698
735
|
// Try to extract SNI and log detailed debug info
|
|
699
736
|
extractSNI(chunk, true);
|
|
700
737
|
}
|
|
@@ -704,21 +741,39 @@ export class PortProxy {
|
|
|
704
741
|
const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
|
|
705
742
|
const code = (err as any).code;
|
|
706
743
|
let reason = 'error';
|
|
707
|
-
|
|
744
|
+
|
|
708
745
|
const now = Date.now();
|
|
709
746
|
const connectionDuration = now - connectionRecord.incomingStartTime;
|
|
710
747
|
const lastActivityAge = now - connectionRecord.lastActivity;
|
|
711
|
-
|
|
748
|
+
|
|
712
749
|
if (code === 'ECONNRESET') {
|
|
713
750
|
reason = 'econnreset';
|
|
714
|
-
console.log(
|
|
751
|
+
console.log(
|
|
752
|
+
`[${connectionId}] ECONNRESET on ${side} side from ${remoteIP}: ${
|
|
753
|
+
err.message
|
|
754
|
+
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
755
|
+
lastActivityAge
|
|
756
|
+
)} ago`
|
|
757
|
+
);
|
|
715
758
|
} else if (code === 'ETIMEDOUT') {
|
|
716
759
|
reason = 'etimedout';
|
|
717
|
-
console.log(
|
|
760
|
+
console.log(
|
|
761
|
+
`[${connectionId}] ETIMEDOUT on ${side} side from ${remoteIP}: ${
|
|
762
|
+
err.message
|
|
763
|
+
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
764
|
+
lastActivityAge
|
|
765
|
+
)} ago`
|
|
766
|
+
);
|
|
718
767
|
} else {
|
|
719
|
-
console.log(
|
|
768
|
+
console.log(
|
|
769
|
+
`[${connectionId}] Error on ${side} side from ${remoteIP}: ${
|
|
770
|
+
err.message
|
|
771
|
+
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
772
|
+
lastActivityAge
|
|
773
|
+
)} ago`
|
|
774
|
+
);
|
|
720
775
|
}
|
|
721
|
-
|
|
776
|
+
|
|
722
777
|
if (side === 'incoming' && incomingTerminationReason === null) {
|
|
723
778
|
incomingTerminationReason = reason;
|
|
724
779
|
this.incrementTerminationStat('incoming', reason);
|
|
@@ -726,7 +781,7 @@ export class PortProxy {
|
|
|
726
781
|
outgoingTerminationReason = reason;
|
|
727
782
|
this.incrementTerminationStat('outgoing', reason);
|
|
728
783
|
}
|
|
729
|
-
|
|
784
|
+
|
|
730
785
|
initiateCleanupOnce(reason);
|
|
731
786
|
};
|
|
732
787
|
|
|
@@ -734,7 +789,7 @@ export class PortProxy {
|
|
|
734
789
|
if (this.settings.enableDetailedLogging) {
|
|
735
790
|
console.log(`[${connectionId}] Connection closed on ${side} side from ${remoteIP}`);
|
|
736
791
|
}
|
|
737
|
-
|
|
792
|
+
|
|
738
793
|
if (side === 'incoming' && incomingTerminationReason === null) {
|
|
739
794
|
incomingTerminationReason = 'normal';
|
|
740
795
|
this.incrementTerminationStat('incoming', 'normal');
|
|
@@ -744,7 +799,7 @@ export class PortProxy {
|
|
|
744
799
|
// Record the time when outgoing socket closed.
|
|
745
800
|
connectionRecord.outgoingClosedTime = Date.now();
|
|
746
801
|
}
|
|
747
|
-
|
|
802
|
+
|
|
748
803
|
initiateCleanupOnce('closed_' + side);
|
|
749
804
|
};
|
|
750
805
|
|
|
@@ -755,54 +810,80 @@ export class PortProxy {
|
|
|
755
810
|
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
|
|
756
811
|
* @param overridePort - If provided, use this port for the outgoing connection.
|
|
757
812
|
*/
|
|
758
|
-
const setupConnection = (
|
|
813
|
+
const setupConnection = (
|
|
814
|
+
serverName: string,
|
|
815
|
+
initialChunk?: Buffer,
|
|
816
|
+
forcedDomain?: IDomainConfig,
|
|
817
|
+
overridePort?: number
|
|
818
|
+
) => {
|
|
759
819
|
// Clear the initial timeout since we've received data
|
|
760
820
|
if (initialTimeout) {
|
|
761
821
|
clearTimeout(initialTimeout);
|
|
762
822
|
initialTimeout = null;
|
|
763
823
|
}
|
|
764
|
-
|
|
824
|
+
|
|
765
825
|
// Mark that we've received initial data
|
|
766
826
|
initialDataReceived = true;
|
|
767
827
|
connectionRecord.hasReceivedInitialData = true;
|
|
768
|
-
|
|
828
|
+
|
|
769
829
|
// Check if this looks like a TLS handshake
|
|
770
830
|
if (initialChunk && isTlsHandshake(initialChunk)) {
|
|
771
831
|
connectionRecord.isTLS = true;
|
|
772
|
-
|
|
832
|
+
|
|
773
833
|
if (this.settings.enableTlsDebugLogging) {
|
|
774
|
-
console.log(
|
|
834
|
+
console.log(
|
|
835
|
+
`[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`
|
|
836
|
+
);
|
|
775
837
|
}
|
|
776
838
|
}
|
|
777
|
-
|
|
839
|
+
|
|
778
840
|
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
|
|
779
841
|
const domainConfig = forcedDomain
|
|
780
842
|
? forcedDomain
|
|
781
|
-
:
|
|
782
|
-
|
|
783
|
-
|
|
843
|
+
: serverName
|
|
844
|
+
? this.settings.domainConfigs.find((config) =>
|
|
845
|
+
config.domains.some((d) => plugins.minimatch(serverName, d))
|
|
846
|
+
)
|
|
847
|
+
: undefined;
|
|
784
848
|
|
|
785
849
|
// Save domain config in connection record
|
|
786
850
|
connectionRecord.domainConfig = domainConfig;
|
|
787
|
-
|
|
851
|
+
|
|
788
852
|
// IP validation is skipped if allowedIPs is empty
|
|
789
853
|
if (domainConfig) {
|
|
790
854
|
const effectiveAllowedIPs: string[] = [
|
|
791
855
|
...domainConfig.allowedIPs,
|
|
792
|
-
...(this.settings.defaultAllowedIPs || [])
|
|
856
|
+
...(this.settings.defaultAllowedIPs || []),
|
|
793
857
|
];
|
|
794
858
|
const effectiveBlockedIPs: string[] = [
|
|
795
859
|
...(domainConfig.blockedIPs || []),
|
|
796
|
-
...(this.settings.defaultBlockedIPs || [])
|
|
860
|
+
...(this.settings.defaultBlockedIPs || []),
|
|
797
861
|
];
|
|
798
|
-
|
|
862
|
+
|
|
799
863
|
// Skip IP validation if allowedIPs is empty
|
|
800
|
-
if (
|
|
801
|
-
|
|
864
|
+
if (
|
|
865
|
+
domainConfig.allowedIPs.length > 0 &&
|
|
866
|
+
!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)
|
|
867
|
+
) {
|
|
868
|
+
return rejectIncomingConnection(
|
|
869
|
+
'rejected',
|
|
870
|
+
`Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(
|
|
871
|
+
', '
|
|
872
|
+
)}`
|
|
873
|
+
);
|
|
802
874
|
}
|
|
803
875
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
|
804
|
-
if (
|
|
805
|
-
|
|
876
|
+
if (
|
|
877
|
+
!isGlobIPAllowed(
|
|
878
|
+
remoteIP,
|
|
879
|
+
this.settings.defaultAllowedIPs,
|
|
880
|
+
this.settings.defaultBlockedIPs || []
|
|
881
|
+
)
|
|
882
|
+
) {
|
|
883
|
+
return rejectIncomingConnection(
|
|
884
|
+
'rejected',
|
|
885
|
+
`Connection rejected: IP ${remoteIP} not allowed by default allowed list`
|
|
886
|
+
);
|
|
806
887
|
}
|
|
807
888
|
}
|
|
808
889
|
|
|
@@ -822,25 +903,29 @@ export class PortProxy {
|
|
|
822
903
|
const tempDataHandler = (chunk: Buffer) => {
|
|
823
904
|
// Track bytes received
|
|
824
905
|
connectionRecord.bytesReceived += chunk.length;
|
|
825
|
-
|
|
906
|
+
|
|
826
907
|
// Check for TLS handshake
|
|
827
908
|
if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
|
|
828
909
|
connectionRecord.isTLS = true;
|
|
829
|
-
|
|
910
|
+
|
|
830
911
|
if (this.settings.enableTlsDebugLogging) {
|
|
831
|
-
console.log(
|
|
912
|
+
console.log(
|
|
913
|
+
`[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
|
|
914
|
+
);
|
|
832
915
|
}
|
|
833
916
|
}
|
|
834
|
-
|
|
917
|
+
|
|
835
918
|
// Check if adding this chunk would exceed the buffer limit
|
|
836
919
|
const newSize = connectionRecord.pendingDataSize + chunk.length;
|
|
837
|
-
|
|
920
|
+
|
|
838
921
|
if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
|
|
839
|
-
console.log(
|
|
922
|
+
console.log(
|
|
923
|
+
`[${connectionId}] Buffer limit exceeded for connection from ${remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
|
|
924
|
+
);
|
|
840
925
|
socket.end(); // Gracefully close the socket
|
|
841
926
|
return initiateCleanupOnce('buffer_limit_exceeded');
|
|
842
927
|
}
|
|
843
|
-
|
|
928
|
+
|
|
844
929
|
// Buffer the chunk and update the size counter
|
|
845
930
|
connectionRecord.pendingData.push(Buffer.from(chunk));
|
|
846
931
|
connectionRecord.pendingDataSize = newSize;
|
|
@@ -861,13 +946,13 @@ export class PortProxy {
|
|
|
861
946
|
const targetSocket = plugins.net.connect(connectionOptions);
|
|
862
947
|
connectionRecord.outgoing = targetSocket;
|
|
863
948
|
connectionRecord.outgoingStartTime = Date.now();
|
|
864
|
-
|
|
949
|
+
|
|
865
950
|
// Apply socket optimizations
|
|
866
951
|
targetSocket.setNoDelay(this.settings.noDelay);
|
|
867
952
|
if (this.settings.keepAlive) {
|
|
868
953
|
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
869
954
|
}
|
|
870
|
-
|
|
955
|
+
|
|
871
956
|
// Apply enhanced TCP options if available
|
|
872
957
|
if (this.settings.enableKeepAliveProbes) {
|
|
873
958
|
try {
|
|
@@ -881,57 +966,73 @@ export class PortProxy {
|
|
|
881
966
|
// Ignore errors - these are optional enhancements
|
|
882
967
|
}
|
|
883
968
|
}
|
|
884
|
-
|
|
969
|
+
|
|
885
970
|
// Setup specific error handler for connection phase
|
|
886
971
|
targetSocket.once('error', (err) => {
|
|
887
972
|
// This handler runs only once during the initial connection phase
|
|
888
973
|
const code = (err as any).code;
|
|
889
|
-
console.log(
|
|
890
|
-
|
|
974
|
+
console.log(
|
|
975
|
+
`[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`
|
|
976
|
+
);
|
|
977
|
+
|
|
891
978
|
// Resume the incoming socket to prevent it from hanging
|
|
892
979
|
socket.resume();
|
|
893
|
-
|
|
980
|
+
|
|
894
981
|
if (code === 'ECONNREFUSED') {
|
|
895
|
-
console.log(
|
|
982
|
+
console.log(
|
|
983
|
+
`[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`
|
|
984
|
+
);
|
|
896
985
|
} else if (code === 'ETIMEDOUT') {
|
|
897
|
-
console.log(
|
|
986
|
+
console.log(
|
|
987
|
+
`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`
|
|
988
|
+
);
|
|
898
989
|
} else if (code === 'ECONNRESET') {
|
|
899
|
-
console.log(
|
|
990
|
+
console.log(
|
|
991
|
+
`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`
|
|
992
|
+
);
|
|
900
993
|
} else if (code === 'EHOSTUNREACH') {
|
|
901
994
|
console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
|
|
902
995
|
}
|
|
903
|
-
|
|
996
|
+
|
|
904
997
|
// Clear any existing error handler after connection phase
|
|
905
998
|
targetSocket.removeAllListeners('error');
|
|
906
|
-
|
|
999
|
+
|
|
907
1000
|
// Re-add the normal error handler for established connections
|
|
908
1001
|
targetSocket.on('error', handleError('outgoing'));
|
|
909
|
-
|
|
1002
|
+
|
|
910
1003
|
if (outgoingTerminationReason === null) {
|
|
911
1004
|
outgoingTerminationReason = 'connection_failed';
|
|
912
1005
|
this.incrementTerminationStat('outgoing', 'connection_failed');
|
|
913
1006
|
}
|
|
914
|
-
|
|
1007
|
+
|
|
915
1008
|
// Clean up the connection
|
|
916
1009
|
initiateCleanupOnce(`connection_failed_${code}`);
|
|
917
1010
|
});
|
|
918
|
-
|
|
1011
|
+
|
|
919
1012
|
// Setup close handler
|
|
920
1013
|
targetSocket.on('close', handleClose('outgoing'));
|
|
921
1014
|
socket.on('close', handleClose('incoming'));
|
|
922
|
-
|
|
1015
|
+
|
|
923
1016
|
// Handle timeouts
|
|
924
1017
|
socket.on('timeout', () => {
|
|
925
|
-
console.log(
|
|
1018
|
+
console.log(
|
|
1019
|
+
`[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(
|
|
1020
|
+
this.settings.socketTimeout || 3600000
|
|
1021
|
+
)}`
|
|
1022
|
+
);
|
|
926
1023
|
if (incomingTerminationReason === null) {
|
|
927
1024
|
incomingTerminationReason = 'timeout';
|
|
928
1025
|
this.incrementTerminationStat('incoming', 'timeout');
|
|
929
1026
|
}
|
|
930
1027
|
initiateCleanupOnce('timeout_incoming');
|
|
931
1028
|
});
|
|
932
|
-
|
|
1029
|
+
|
|
933
1030
|
targetSocket.on('timeout', () => {
|
|
934
|
-
console.log(
|
|
1031
|
+
console.log(
|
|
1032
|
+
`[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(
|
|
1033
|
+
this.settings.socketTimeout || 3600000
|
|
1034
|
+
)}`
|
|
1035
|
+
);
|
|
935
1036
|
if (outgoingTerminationReason === null) {
|
|
936
1037
|
outgoingTerminationReason = 'timeout';
|
|
937
1038
|
this.incrementTerminationStat('outgoing', 'timeout');
|
|
@@ -942,48 +1043,62 @@ export class PortProxy {
|
|
|
942
1043
|
// Set appropriate timeouts using the configured value
|
|
943
1044
|
socket.setTimeout(this.settings.socketTimeout || 3600000);
|
|
944
1045
|
targetSocket.setTimeout(this.settings.socketTimeout || 3600000);
|
|
945
|
-
|
|
1046
|
+
|
|
946
1047
|
// Track outgoing data for bytes counting
|
|
947
1048
|
targetSocket.on('data', (chunk: Buffer) => {
|
|
948
1049
|
connectionRecord.bytesSent += chunk.length;
|
|
949
1050
|
this.updateActivity(connectionRecord);
|
|
950
1051
|
});
|
|
951
|
-
|
|
1052
|
+
|
|
952
1053
|
// Wait for the outgoing connection to be ready before setting up piping
|
|
953
1054
|
targetSocket.once('connect', () => {
|
|
954
1055
|
// Clear the initial connection error handler
|
|
955
1056
|
targetSocket.removeAllListeners('error');
|
|
956
|
-
|
|
1057
|
+
|
|
957
1058
|
// Add the normal error handler for established connections
|
|
958
1059
|
targetSocket.on('error', handleError('outgoing'));
|
|
959
|
-
|
|
1060
|
+
|
|
960
1061
|
// Remove temporary data handler
|
|
961
1062
|
socket.removeListener('data', tempDataHandler);
|
|
962
|
-
|
|
1063
|
+
|
|
963
1064
|
// Flush all pending data to target
|
|
964
1065
|
if (connectionRecord.pendingData.length > 0) {
|
|
965
1066
|
const combinedData = Buffer.concat(connectionRecord.pendingData);
|
|
966
1067
|
targetSocket.write(combinedData, (err) => {
|
|
967
1068
|
if (err) {
|
|
968
|
-
console.log(
|
|
1069
|
+
console.log(
|
|
1070
|
+
`[${connectionId}] Error writing pending data to target: ${err.message}`
|
|
1071
|
+
);
|
|
969
1072
|
return initiateCleanupOnce('write_error');
|
|
970
1073
|
}
|
|
971
|
-
|
|
1074
|
+
|
|
972
1075
|
// Now set up piping for future data and resume the socket
|
|
973
1076
|
socket.pipe(targetSocket);
|
|
974
1077
|
targetSocket.pipe(socket);
|
|
975
1078
|
socket.resume(); // Resume the socket after piping is established
|
|
976
|
-
|
|
1079
|
+
|
|
977
1080
|
if (this.settings.enableDetailedLogging) {
|
|
978
1081
|
console.log(
|
|
979
1082
|
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
980
|
-
|
|
981
|
-
|
|
1083
|
+
`${
|
|
1084
|
+
serverName
|
|
1085
|
+
? ` (SNI: ${serverName})`
|
|
1086
|
+
: forcedDomain
|
|
1087
|
+
? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
|
|
1088
|
+
: ''
|
|
1089
|
+
}` +
|
|
1090
|
+
` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
|
|
982
1091
|
);
|
|
983
1092
|
} else {
|
|
984
1093
|
console.log(
|
|
985
1094
|
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
986
|
-
|
|
1095
|
+
`${
|
|
1096
|
+
serverName
|
|
1097
|
+
? ` (SNI: ${serverName})`
|
|
1098
|
+
: forcedDomain
|
|
1099
|
+
? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
|
|
1100
|
+
: ''
|
|
1101
|
+
}`
|
|
987
1102
|
);
|
|
988
1103
|
}
|
|
989
1104
|
});
|
|
@@ -992,25 +1107,37 @@ export class PortProxy {
|
|
|
992
1107
|
socket.pipe(targetSocket);
|
|
993
1108
|
targetSocket.pipe(socket);
|
|
994
1109
|
socket.resume(); // Resume the socket after piping is established
|
|
995
|
-
|
|
1110
|
+
|
|
996
1111
|
if (this.settings.enableDetailedLogging) {
|
|
997
1112
|
console.log(
|
|
998
1113
|
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
999
|
-
|
|
1000
|
-
|
|
1114
|
+
`${
|
|
1115
|
+
serverName
|
|
1116
|
+
? ` (SNI: ${serverName})`
|
|
1117
|
+
: forcedDomain
|
|
1118
|
+
? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
|
|
1119
|
+
: ''
|
|
1120
|
+
}` +
|
|
1121
|
+
` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
|
|
1001
1122
|
);
|
|
1002
1123
|
} else {
|
|
1003
1124
|
console.log(
|
|
1004
1125
|
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
1005
|
-
|
|
1126
|
+
`${
|
|
1127
|
+
serverName
|
|
1128
|
+
? ` (SNI: ${serverName})`
|
|
1129
|
+
: forcedDomain
|
|
1130
|
+
? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
|
|
1131
|
+
: ''
|
|
1132
|
+
}`
|
|
1006
1133
|
);
|
|
1007
1134
|
}
|
|
1008
1135
|
}
|
|
1009
|
-
|
|
1136
|
+
|
|
1010
1137
|
// Clear the buffer now that we've processed it
|
|
1011
1138
|
connectionRecord.pendingData = [];
|
|
1012
1139
|
connectionRecord.pendingDataSize = 0;
|
|
1013
|
-
|
|
1140
|
+
|
|
1014
1141
|
// Add the renegotiation listener for SNI validation
|
|
1015
1142
|
if (serverName) {
|
|
1016
1143
|
socket.on('data', (renegChunk: Buffer) => {
|
|
@@ -1019,41 +1146,53 @@ export class PortProxy {
|
|
|
1019
1146
|
// Try to extract SNI from potential renegotiation
|
|
1020
1147
|
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
|
|
1021
1148
|
if (newSNI && newSNI !== connectionRecord.lockedDomain) {
|
|
1022
|
-
console.log(
|
|
1149
|
+
console.log(
|
|
1150
|
+
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`
|
|
1151
|
+
);
|
|
1023
1152
|
initiateCleanupOnce('sni_mismatch');
|
|
1024
1153
|
} else if (newSNI && this.settings.enableDetailedLogging) {
|
|
1025
|
-
console.log(
|
|
1154
|
+
console.log(
|
|
1155
|
+
`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
|
|
1156
|
+
);
|
|
1026
1157
|
}
|
|
1027
1158
|
} catch (err) {
|
|
1028
|
-
console.log(
|
|
1159
|
+
console.log(
|
|
1160
|
+
`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
|
|
1161
|
+
);
|
|
1029
1162
|
}
|
|
1030
1163
|
}
|
|
1031
1164
|
});
|
|
1032
1165
|
}
|
|
1033
|
-
|
|
1166
|
+
|
|
1034
1167
|
// Set connection timeout
|
|
1035
1168
|
if (connectionRecord.cleanupTimer) {
|
|
1036
1169
|
clearTimeout(connectionRecord.cleanupTimer);
|
|
1037
1170
|
}
|
|
1038
|
-
|
|
1171
|
+
|
|
1039
1172
|
// Set timeout based on domain config or default
|
|
1040
1173
|
const connectionTimeout = this.getConnectionTimeout(connectionRecord);
|
|
1041
1174
|
connectionRecord.cleanupTimer = setTimeout(() => {
|
|
1042
|
-
console.log(
|
|
1175
|
+
console.log(
|
|
1176
|
+
`[${connectionId}] Connection from ${remoteIP} exceeded max lifetime (${plugins.prettyMs(
|
|
1177
|
+
connectionTimeout
|
|
1178
|
+
)}), forcing cleanup.`
|
|
1179
|
+
);
|
|
1043
1180
|
initiateCleanupOnce('connection_timeout');
|
|
1044
1181
|
}, connectionTimeout);
|
|
1045
|
-
|
|
1182
|
+
|
|
1046
1183
|
// Make sure timeout doesn't keep the process alive
|
|
1047
1184
|
if (connectionRecord.cleanupTimer.unref) {
|
|
1048
1185
|
connectionRecord.cleanupTimer.unref();
|
|
1049
1186
|
}
|
|
1050
|
-
|
|
1187
|
+
|
|
1051
1188
|
// Mark TLS handshake as complete for TLS connections
|
|
1052
1189
|
if (connectionRecord.isTLS) {
|
|
1053
1190
|
connectionRecord.tlsHandshakeComplete = true;
|
|
1054
|
-
|
|
1191
|
+
|
|
1055
1192
|
if (this.settings.enableTlsDebugLogging) {
|
|
1056
|
-
console.log(
|
|
1193
|
+
console.log(
|
|
1194
|
+
`[${connectionId}] TLS handshake complete for connection from ${remoteIP}`
|
|
1195
|
+
);
|
|
1057
1196
|
}
|
|
1058
1197
|
}
|
|
1059
1198
|
});
|
|
@@ -1061,45 +1200,72 @@ export class PortProxy {
|
|
|
1061
1200
|
|
|
1062
1201
|
// --- PORT RANGE-BASED HANDLING ---
|
|
1063
1202
|
// Only apply port-based rules if the incoming port is within one of the global port ranges.
|
|
1064
|
-
if (
|
|
1203
|
+
if (
|
|
1204
|
+
this.settings.globalPortRanges &&
|
|
1205
|
+
isPortInRanges(localPort, this.settings.globalPortRanges)
|
|
1206
|
+
) {
|
|
1065
1207
|
if (this.settings.forwardAllGlobalRanges) {
|
|
1066
|
-
if (
|
|
1067
|
-
|
|
1208
|
+
if (
|
|
1209
|
+
this.settings.defaultAllowedIPs &&
|
|
1210
|
+
this.settings.defaultAllowedIPs.length > 0 &&
|
|
1211
|
+
!isAllowed(remoteIP, this.settings.defaultAllowedIPs)
|
|
1212
|
+
) {
|
|
1213
|
+
console.log(
|
|
1214
|
+
`[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`
|
|
1215
|
+
);
|
|
1068
1216
|
socket.end();
|
|
1069
1217
|
return;
|
|
1070
1218
|
}
|
|
1071
1219
|
if (this.settings.enableDetailedLogging) {
|
|
1072
|
-
console.log(
|
|
1220
|
+
console.log(
|
|
1221
|
+
`[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`
|
|
1222
|
+
);
|
|
1073
1223
|
}
|
|
1074
|
-
setupConnection(
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1224
|
+
setupConnection(
|
|
1225
|
+
'',
|
|
1226
|
+
undefined,
|
|
1227
|
+
{
|
|
1228
|
+
domains: ['global'],
|
|
1229
|
+
allowedIPs: this.settings.defaultAllowedIPs || [],
|
|
1230
|
+
blockedIPs: this.settings.defaultBlockedIPs || [],
|
|
1231
|
+
targetIPs: [this.settings.targetIP!],
|
|
1232
|
+
portRanges: [],
|
|
1233
|
+
},
|
|
1234
|
+
localPort
|
|
1235
|
+
);
|
|
1081
1236
|
return;
|
|
1082
1237
|
} else {
|
|
1083
1238
|
// Attempt to find a matching forced domain config based on the local port.
|
|
1084
1239
|
const forcedDomain = this.settings.domainConfigs.find(
|
|
1085
|
-
domain =>
|
|
1240
|
+
(domain) =>
|
|
1241
|
+
domain.portRanges &&
|
|
1242
|
+
domain.portRanges.length > 0 &&
|
|
1243
|
+
isPortInRanges(localPort, domain.portRanges)
|
|
1086
1244
|
);
|
|
1087
1245
|
if (forcedDomain) {
|
|
1088
1246
|
const effectiveAllowedIPs: string[] = [
|
|
1089
1247
|
...forcedDomain.allowedIPs,
|
|
1090
|
-
...(this.settings.defaultAllowedIPs || [])
|
|
1248
|
+
...(this.settings.defaultAllowedIPs || []),
|
|
1091
1249
|
];
|
|
1092
1250
|
const effectiveBlockedIPs: string[] = [
|
|
1093
1251
|
...(forcedDomain.blockedIPs || []),
|
|
1094
|
-
...(this.settings.defaultBlockedIPs || [])
|
|
1252
|
+
...(this.settings.defaultBlockedIPs || []),
|
|
1095
1253
|
];
|
|
1096
1254
|
if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
|
1097
|
-
console.log(
|
|
1255
|
+
console.log(
|
|
1256
|
+
`[${connectionId}] Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(
|
|
1257
|
+
', '
|
|
1258
|
+
)} on port ${localPort}.`
|
|
1259
|
+
);
|
|
1098
1260
|
socket.end();
|
|
1099
1261
|
return;
|
|
1100
1262
|
}
|
|
1101
1263
|
if (this.settings.enableDetailedLogging) {
|
|
1102
|
-
console.log(
|
|
1264
|
+
console.log(
|
|
1265
|
+
`[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(
|
|
1266
|
+
', '
|
|
1267
|
+
)}.`
|
|
1268
|
+
);
|
|
1103
1269
|
}
|
|
1104
1270
|
setupConnection('', undefined, forcedDomain, localPort);
|
|
1105
1271
|
return;
|
|
@@ -1117,39 +1283,52 @@ export class PortProxy {
|
|
|
1117
1283
|
clearTimeout(initialTimeout);
|
|
1118
1284
|
initialTimeout = null;
|
|
1119
1285
|
}
|
|
1120
|
-
|
|
1286
|
+
|
|
1121
1287
|
initialDataReceived = true;
|
|
1122
|
-
|
|
1288
|
+
|
|
1123
1289
|
// Try to extract SNI
|
|
1124
1290
|
let serverName = '';
|
|
1125
|
-
|
|
1291
|
+
|
|
1126
1292
|
if (isTlsHandshake(chunk)) {
|
|
1127
1293
|
connectionRecord.isTLS = true;
|
|
1128
|
-
|
|
1294
|
+
|
|
1129
1295
|
if (this.settings.enableTlsDebugLogging) {
|
|
1130
|
-
console.log(
|
|
1296
|
+
console.log(
|
|
1297
|
+
`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
|
|
1298
|
+
);
|
|
1131
1299
|
}
|
|
1132
|
-
|
|
1300
|
+
|
|
1133
1301
|
serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
|
|
1134
1302
|
}
|
|
1135
|
-
|
|
1303
|
+
|
|
1136
1304
|
// Lock the connection to the negotiated SNI.
|
|
1137
1305
|
connectionRecord.lockedDomain = serverName;
|
|
1138
|
-
|
|
1306
|
+
|
|
1139
1307
|
if (this.settings.enableDetailedLogging) {
|
|
1140
|
-
console.log(
|
|
1308
|
+
console.log(
|
|
1309
|
+
`[${connectionId}] Received connection from ${remoteIP} with SNI: ${
|
|
1310
|
+
serverName || '(empty)'
|
|
1311
|
+
}`
|
|
1312
|
+
);
|
|
1141
1313
|
}
|
|
1142
|
-
|
|
1314
|
+
|
|
1143
1315
|
setupConnection(serverName, chunk);
|
|
1144
1316
|
});
|
|
1145
1317
|
} else {
|
|
1146
1318
|
initialDataReceived = true;
|
|
1147
1319
|
connectionRecord.hasReceivedInitialData = true;
|
|
1148
|
-
|
|
1149
|
-
if (
|
|
1150
|
-
|
|
1320
|
+
|
|
1321
|
+
if (
|
|
1322
|
+
this.settings.defaultAllowedIPs &&
|
|
1323
|
+
this.settings.defaultAllowedIPs.length > 0 &&
|
|
1324
|
+
!isAllowed(remoteIP, this.settings.defaultAllowedIPs)
|
|
1325
|
+
) {
|
|
1326
|
+
return rejectIncomingConnection(
|
|
1327
|
+
'rejected',
|
|
1328
|
+
`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`
|
|
1329
|
+
);
|
|
1151
1330
|
}
|
|
1152
|
-
|
|
1331
|
+
|
|
1153
1332
|
setupConnection('');
|
|
1154
1333
|
}
|
|
1155
1334
|
};
|
|
@@ -1172,13 +1351,15 @@ export class PortProxy {
|
|
|
1172
1351
|
|
|
1173
1352
|
// Create a server for each port.
|
|
1174
1353
|
for (const port of listeningPorts) {
|
|
1175
|
-
const server = plugins.net
|
|
1176
|
-
.
|
|
1177
|
-
|
|
1178
|
-
console.log(`Server Error on port ${port}: ${err.message}`);
|
|
1179
|
-
});
|
|
1354
|
+
const server = plugins.net.createServer(connectionHandler).on('error', (err: Error) => {
|
|
1355
|
+
console.log(`Server Error on port ${port}: ${err.message}`);
|
|
1356
|
+
});
|
|
1180
1357
|
server.listen(port, () => {
|
|
1181
|
-
console.log(
|
|
1358
|
+
console.log(
|
|
1359
|
+
`PortProxy -> OK: Now listening on port ${port}${
|
|
1360
|
+
this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''
|
|
1361
|
+
}`
|
|
1362
|
+
);
|
|
1182
1363
|
});
|
|
1183
1364
|
this.netServers.push(server);
|
|
1184
1365
|
}
|
|
@@ -1187,7 +1368,7 @@ export class PortProxy {
|
|
|
1187
1368
|
this.connectionLogger = setInterval(() => {
|
|
1188
1369
|
// Immediately return if shutting down
|
|
1189
1370
|
if (this.isShuttingDown) return;
|
|
1190
|
-
|
|
1371
|
+
|
|
1191
1372
|
const now = Date.now();
|
|
1192
1373
|
let maxIncoming = 0;
|
|
1193
1374
|
let maxOutgoing = 0;
|
|
@@ -1195,14 +1376,14 @@ export class PortProxy {
|
|
|
1195
1376
|
let nonTlsConnections = 0;
|
|
1196
1377
|
let completedTlsHandshakes = 0;
|
|
1197
1378
|
let pendingTlsHandshakes = 0;
|
|
1198
|
-
|
|
1379
|
+
|
|
1199
1380
|
// Create a copy of the keys to avoid modification during iteration
|
|
1200
1381
|
const connectionIds = [...this.connectionRecords.keys()];
|
|
1201
|
-
|
|
1382
|
+
|
|
1202
1383
|
for (const id of connectionIds) {
|
|
1203
1384
|
const record = this.connectionRecords.get(id);
|
|
1204
1385
|
if (!record) continue;
|
|
1205
|
-
|
|
1386
|
+
|
|
1206
1387
|
// Track connection stats
|
|
1207
1388
|
if (record.isTLS) {
|
|
1208
1389
|
tlsConnections++;
|
|
@@ -1214,50 +1395,73 @@ export class PortProxy {
|
|
|
1214
1395
|
} else {
|
|
1215
1396
|
nonTlsConnections++;
|
|
1216
1397
|
}
|
|
1217
|
-
|
|
1398
|
+
|
|
1218
1399
|
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
|
|
1219
1400
|
if (record.outgoingStartTime) {
|
|
1220
1401
|
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
|
|
1221
1402
|
}
|
|
1222
|
-
|
|
1403
|
+
|
|
1223
1404
|
// Parity check: if outgoing socket closed and incoming remains active
|
|
1224
|
-
if (
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1405
|
+
if (
|
|
1406
|
+
record.outgoingClosedTime &&
|
|
1407
|
+
!record.incoming.destroyed &&
|
|
1408
|
+
!record.connectionClosed &&
|
|
1409
|
+
now - record.outgoingClosedTime > 120000
|
|
1410
|
+
) {
|
|
1228
1411
|
const remoteIP = record.remoteIP;
|
|
1229
|
-
console.log(
|
|
1412
|
+
console.log(
|
|
1413
|
+
`[${id}] Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(
|
|
1414
|
+
now - record.outgoingClosedTime
|
|
1415
|
+
)} after outgoing closed.`
|
|
1416
|
+
);
|
|
1230
1417
|
this.cleanupConnection(record, 'parity_check');
|
|
1231
1418
|
}
|
|
1232
|
-
|
|
1419
|
+
|
|
1233
1420
|
// Check for stalled connections waiting for initial data
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
|
|
1421
|
+
if (
|
|
1422
|
+
!record.hasReceivedInitialData &&
|
|
1423
|
+
now - record.incomingStartTime > this.settings.initialDataTimeout! / 2
|
|
1424
|
+
) {
|
|
1425
|
+
console.log(
|
|
1426
|
+
`[${id}] Warning: Connection from ${
|
|
1427
|
+
record.remoteIP
|
|
1428
|
+
} has not received initial data after ${plugins.prettyMs(
|
|
1429
|
+
now - record.incomingStartTime
|
|
1430
|
+
)}`
|
|
1431
|
+
);
|
|
1237
1432
|
}
|
|
1238
|
-
|
|
1433
|
+
|
|
1239
1434
|
// Skip inactivity check if disabled
|
|
1240
1435
|
if (!this.settings.disableInactivityCheck) {
|
|
1241
1436
|
// Inactivity check with configurable timeout
|
|
1242
1437
|
const inactivityThreshold = this.settings.inactivityTimeout!;
|
|
1243
|
-
|
|
1438
|
+
|
|
1244
1439
|
const inactivityTime = now - record.lastActivity;
|
|
1245
1440
|
if (inactivityTime > inactivityThreshold && !record.connectionClosed) {
|
|
1246
|
-
console.log(
|
|
1441
|
+
console.log(
|
|
1442
|
+
`[${id}] Inactivity check: No activity on connection from ${
|
|
1443
|
+
record.remoteIP
|
|
1444
|
+
} for ${plugins.prettyMs(inactivityTime)}.`
|
|
1445
|
+
);
|
|
1247
1446
|
this.cleanupConnection(record, 'inactivity');
|
|
1248
1447
|
}
|
|
1249
1448
|
}
|
|
1250
1449
|
}
|
|
1251
|
-
|
|
1450
|
+
|
|
1252
1451
|
// Log detailed stats periodically
|
|
1253
1452
|
console.log(
|
|
1254
1453
|
`Active connections: ${this.connectionRecords.size}. ` +
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1454
|
+
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), Non-TLS=${nonTlsConnections}. ` +
|
|
1455
|
+
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
|
|
1456
|
+
maxOutgoing
|
|
1457
|
+
)}. ` +
|
|
1458
|
+
`Termination stats: ${JSON.stringify({
|
|
1459
|
+
IN: this.terminationStats.incoming,
|
|
1460
|
+
OUT: this.terminationStats.outgoing,
|
|
1461
|
+
})}`
|
|
1258
1462
|
);
|
|
1259
1463
|
}, this.settings.inactivityCheckInterval || 60000);
|
|
1260
|
-
|
|
1464
|
+
|
|
1261
1465
|
// Make sure the interval doesn't keep the process alive
|
|
1262
1466
|
if (this.connectionLogger.unref) {
|
|
1263
1467
|
this.connectionLogger.unref();
|
|
@@ -1268,12 +1472,12 @@ export class PortProxy {
|
|
|
1268
1472
|
* Gracefully shut down the proxy
|
|
1269
1473
|
*/
|
|
1270
1474
|
public async stop() {
|
|
1271
|
-
console.log(
|
|
1475
|
+
console.log('PortProxy shutting down...');
|
|
1272
1476
|
this.isShuttingDown = true;
|
|
1273
|
-
|
|
1477
|
+
|
|
1274
1478
|
// Stop accepting new connections
|
|
1275
1479
|
const closeServerPromises: Promise<void>[] = this.netServers.map(
|
|
1276
|
-
server =>
|
|
1480
|
+
(server) =>
|
|
1277
1481
|
new Promise<void>((resolve) => {
|
|
1278
1482
|
if (!server.listening) {
|
|
1279
1483
|
resolve();
|
|
@@ -1287,7 +1491,7 @@ export class PortProxy {
|
|
|
1287
1491
|
});
|
|
1288
1492
|
})
|
|
1289
1493
|
);
|
|
1290
|
-
|
|
1494
|
+
|
|
1291
1495
|
// Stop the connection logger
|
|
1292
1496
|
if (this.connectionLogger) {
|
|
1293
1497
|
clearInterval(this.connectionLogger);
|
|
@@ -1296,12 +1500,12 @@ export class PortProxy {
|
|
|
1296
1500
|
|
|
1297
1501
|
// Wait for servers to close
|
|
1298
1502
|
await Promise.all(closeServerPromises);
|
|
1299
|
-
console.log(
|
|
1300
|
-
|
|
1503
|
+
console.log('All servers closed. Cleaning up active connections...');
|
|
1504
|
+
|
|
1301
1505
|
// Force destroy all active connections immediately
|
|
1302
1506
|
const connectionIds = [...this.connectionRecords.keys()];
|
|
1303
1507
|
console.log(`Cleaning up ${connectionIds.length} active connections...`);
|
|
1304
|
-
|
|
1508
|
+
|
|
1305
1509
|
// First pass: End all connections gracefully
|
|
1306
1510
|
for (const id of connectionIds) {
|
|
1307
1511
|
const record = this.connectionRecords.get(id);
|
|
@@ -1312,12 +1516,12 @@ export class PortProxy {
|
|
|
1312
1516
|
clearTimeout(record.cleanupTimer);
|
|
1313
1517
|
record.cleanupTimer = undefined;
|
|
1314
1518
|
}
|
|
1315
|
-
|
|
1519
|
+
|
|
1316
1520
|
// End sockets gracefully
|
|
1317
1521
|
if (record.incoming && !record.incoming.destroyed) {
|
|
1318
1522
|
record.incoming.end();
|
|
1319
1523
|
}
|
|
1320
|
-
|
|
1524
|
+
|
|
1321
1525
|
if (record.outgoing && !record.outgoing.destroyed) {
|
|
1322
1526
|
record.outgoing.end();
|
|
1323
1527
|
}
|
|
@@ -1326,10 +1530,10 @@ export class PortProxy {
|
|
|
1326
1530
|
}
|
|
1327
1531
|
}
|
|
1328
1532
|
}
|
|
1329
|
-
|
|
1533
|
+
|
|
1330
1534
|
// Short delay to allow graceful ends to process
|
|
1331
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1332
|
-
|
|
1535
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1536
|
+
|
|
1333
1537
|
// Second pass: Force destroy everything
|
|
1334
1538
|
for (const id of connectionIds) {
|
|
1335
1539
|
const record = this.connectionRecords.get(id);
|
|
@@ -1342,7 +1546,7 @@ export class PortProxy {
|
|
|
1342
1546
|
record.incoming.destroy();
|
|
1343
1547
|
}
|
|
1344
1548
|
}
|
|
1345
|
-
|
|
1549
|
+
|
|
1346
1550
|
if (record.outgoing) {
|
|
1347
1551
|
record.outgoing.removeAllListeners();
|
|
1348
1552
|
if (!record.outgoing.destroyed) {
|
|
@@ -1354,20 +1558,20 @@ export class PortProxy {
|
|
|
1354
1558
|
}
|
|
1355
1559
|
}
|
|
1356
1560
|
}
|
|
1357
|
-
|
|
1561
|
+
|
|
1358
1562
|
// Clear all tracking maps
|
|
1359
1563
|
this.connectionRecords.clear();
|
|
1360
1564
|
this.domainTargetIndices.clear();
|
|
1361
1565
|
this.connectionsByIP.clear();
|
|
1362
1566
|
this.connectionRateByIP.clear();
|
|
1363
1567
|
this.netServers = [];
|
|
1364
|
-
|
|
1568
|
+
|
|
1365
1569
|
// Reset termination stats
|
|
1366
1570
|
this.terminationStats = {
|
|
1367
1571
|
incoming: {},
|
|
1368
|
-
outgoing: {}
|
|
1572
|
+
outgoing: {},
|
|
1369
1573
|
};
|
|
1370
|
-
|
|
1371
|
-
console.log(
|
|
1574
|
+
|
|
1575
|
+
console.log('PortProxy shutdown complete.');
|
|
1372
1576
|
}
|
|
1373
|
-
}
|
|
1577
|
+
}
|