@push.rocks/smartproxy 19.6.1 → 19.6.6

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/readme.hints.md DELETED
@@ -1,897 +0,0 @@
1
- # SmartProxy Project Hints
2
-
3
- ## Project Overview
4
- - Package: `@push.rocks/smartproxy` – high-performance proxy supporting HTTP(S), TCP, WebSocket, and ACME integration.
5
- - Written in TypeScript, compiled output in `dist_ts/`, uses ESM with NodeNext resolution.
6
-
7
- ## Important: ACME Configuration in v19.0.0
8
- - **Breaking Change**: ACME configuration must be placed within individual route TLS settings, not at the top level
9
- - Route-level ACME config is the ONLY way to enable SmartAcme initialization
10
- - SmartCertManager requires email in route config for certificate acquisition
11
- - Top-level ACME configuration is ignored in v19.0.0
12
-
13
- ## Repository Structure
14
- - `ts/` – TypeScript source files:
15
- - `index.ts` exports main modules.
16
- - `plugins.ts` centralizes native and third-party imports.
17
- - Subdirectories: `networkproxy/`, `nftablesproxy/`, `port80handler/`, `redirect/`, `smartproxy/`.
18
- - Key classes: `ProxyRouter` (`classes.router.ts`), `SmartProxy` (`classes.smartproxy.ts`), plus handlers/managers.
19
- - `dist_ts/` – transpiled `.js` and `.d.ts` files mirroring `ts/` structure.
20
- - `test/` – test suites in TypeScript:
21
- - `test.router.ts` – routing logic (hostname matching, wildcards, path parameters, config management).
22
- - `test.smartproxy.ts` – proxy behavior tests (TCP forwarding, SNI handling, concurrency, chaining, timeouts).
23
- - `test/helpers/` – utilities (e.g., certificates).
24
- - `assets/certs/` – placeholder certificates for ACME and TLS.
25
-
26
- ## Development Setup
27
- - Requires `pnpm` (v10+).
28
- - Install dependencies: `pnpm install`.
29
- - Build: `pnpm build` (runs `tsbuild --web --allowimplicitany`).
30
- - Test: `pnpm test` (runs `tstest test/`).
31
- - Format: `pnpm format` (runs `gitzone format`).
32
-
33
- ## How to Test
34
-
35
- ### Test Structure
36
- Tests use tapbundle from `@git.zone/tstest`. The correct pattern is:
37
-
38
- ```typescript
39
- import { tap, expect } from '@git.zone/tstest/tapbundle';
40
-
41
- tap.test('test description', async () => {
42
- // Test logic here
43
- expect(someValue).toEqual(expectedValue);
44
- });
45
-
46
- // IMPORTANT: Must end with tap.start()
47
- tap.start();
48
- ```
49
-
50
- ### Expect Syntax (from @push.rocks/smartexpect)
51
- ```typescript
52
- // Type assertions
53
- expect('hello').toBeTypeofString();
54
- expect(42).toBeTypeofNumber();
55
-
56
- // Equality
57
- expect('hithere').toEqual('hithere');
58
-
59
- // Negated assertions
60
- expect(1).not.toBeTypeofString();
61
-
62
- // Regular expressions
63
- expect('hithere').toMatch(/hi/);
64
-
65
- // Numeric comparisons
66
- expect(5).toBeGreaterThan(3);
67
- expect(0.1 + 0.2).toBeCloseTo(0.3, 10);
68
-
69
- // Arrays
70
- expect([1, 2, 3]).toContain(2);
71
- expect([1, 2, 3]).toHaveLength(3);
72
-
73
- // Async assertions
74
- await expect(asyncFunction()).resolves.toEqual('expected');
75
- await expect(asyncFunction()).resolves.withTimeout(5000).toBeTypeofString();
76
-
77
- // Complex object navigation
78
- expect(complexObject)
79
- .property('users')
80
- .arrayItem(0)
81
- .property('name')
82
- .toEqual('Alice');
83
- ```
84
-
85
- ### Test Modifiers
86
- - `tap.only.test()` - Run only this test
87
- - `tap.skip.test()` - Skip a test
88
- - `tap.timeout()` - Set test-specific timeout
89
-
90
- ### Running Tests
91
- - All tests: `pnpm test`
92
- - Specific test: `tsx test/test.router.ts`
93
- - With options: `tstest test/**/*.ts --verbose --timeout 60`
94
-
95
- ### Test File Requirements
96
- - Must start with `test.` prefix
97
- - Must use `.ts` extension
98
- - Must call `tap.start()` at the end
99
-
100
- ## Coding Conventions
101
- - Import modules via `plugins.ts`:
102
- ```ts
103
- import * as plugins from './plugins.ts';
104
- const server = new plugins.http.Server();
105
- ```
106
- - Reference plugins with full path: `plugins.acme`, `plugins.smartdelay`, `plugins.minimatch`, etc.
107
- - Path patterns support globs (`*`) and parameters (`:param`) in `ProxyRouter`.
108
- - Wildcard hostname matching leverages `minimatch` patterns.
109
-
110
- ## Key Components
111
- - **ProxyRouter**
112
- - Methods: `routeReq`, `routeReqWithDetails`.
113
- - Hostname matching: case-insensitive, strips port, supports exact, wildcard, TLD, complex patterns.
114
- - Path routing: exact, wildcard, parameter extraction (`pathParams`), returns `pathMatch` and `pathRemainder`.
115
- - Config API: `setNewProxyConfigs`, `addProxyConfig`, `removeProxyConfig`, `getHostnames`, `getProxyConfigs`.
116
- - **SmartProxy**
117
- - Manages one or more `net.Server` instances to forward TCP streams.
118
- - Options: `preserveSourceIP`, `defaultAllowedIPs`, `globalPortRanges`, `sniEnabled`.
119
- - DomainConfigManager: round-robin selection for multiple target IPs.
120
- - Graceful shutdown in `stop()`, ensures no lingering servers or sockets.
121
-
122
- ## Notable Points
123
- - **TSConfig**: `module: NodeNext`, `verbatimModuleSyntax`, allows `.js` extension imports in TS.
124
- - Mermaid diagrams and architecture flows in `readme.md` illustrate component interactions and protocol flows.
125
- - CLI entrypoint (`cli.js`) supports command-line usage (ACME, proxy controls).
126
- - ACME and certificate handling via `Port80Handler` and `helpers.certificates.ts`.
127
-
128
- ## ACME/Certificate Configuration Example (v19.0.0)
129
- ```typescript
130
- const proxy = new SmartProxy({
131
- routes: [{
132
- name: 'example.com',
133
- match: { domains: 'example.com', ports: 443 },
134
- action: {
135
- type: 'forward',
136
- target: { host: 'localhost', port: 8080 },
137
- tls: {
138
- mode: 'terminate',
139
- certificate: 'auto',
140
- acme: { // ACME config MUST be here, not at top level
141
- email: 'ssl@example.com',
142
- useProduction: false,
143
- challengePort: 80
144
- }
145
- }
146
- }
147
- }]
148
- });
149
- ```
150
-
151
- ## TODOs / Considerations
152
- - Ensure import extensions in source match build outputs (`.ts` vs `.js`).
153
- - Update `plugins.ts` when adding new dependencies.
154
- - Maintain test coverage for new routing or proxy features.
155
- - Keep `ts/` and `dist_ts/` in sync after refactors.
156
- - Consider implementing top-level ACME config support for backward compatibility
157
-
158
- ## HTTP-01 ACME Challenge Fix (v19.3.8)
159
-
160
- ### Issue
161
- Non-TLS connections on ports configured in `useHttpProxy` were not being forwarded to HttpProxy. This caused ACME HTTP-01 challenges to fail when the ACME port (usually 80) was included in `useHttpProxy`.
162
-
163
- ### Root Cause
164
- In the `RouteConnectionHandler.handleForwardAction` method, only connections with TLS settings (mode: 'terminate' or 'terminate-and-reencrypt') were being forwarded to HttpProxy. Non-TLS connections were always handled as direct connections, even when the port was configured for HttpProxy.
165
-
166
- ### Solution
167
- Added a check for non-TLS connections on ports listed in `useHttpProxy`:
168
- ```typescript
169
- // No TLS settings - check if this port should use HttpProxy
170
- const isHttpProxyPort = this.settings.useHttpProxy?.includes(record.localPort);
171
-
172
- if (isHttpProxyPort && this.httpProxyBridge.getHttpProxy()) {
173
- // Forward non-TLS connections to HttpProxy if configured
174
- this.httpProxyBridge.forwardToHttpProxy(/*...*/);
175
- return;
176
- }
177
- ```
178
-
179
- ### Test Coverage
180
- - `test/test.http-fix-unit.ts` - Unit tests verifying the fix
181
- - Tests confirm that non-TLS connections on HttpProxy ports are properly forwarded
182
- - Tests verify that non-HttpProxy ports still use direct connections
183
-
184
- ### Configuration Example
185
- ```typescript
186
- const proxy = new SmartProxy({
187
- useHttpProxy: [80], // Enable HttpProxy for port 80
188
- httpProxyPort: 8443,
189
- acme: {
190
- email: 'ssl@example.com',
191
- port: 80
192
- },
193
- routes: [
194
- // Your routes here
195
- ]
196
- });
197
- ```
198
-
199
- ## ACME Certificate Provisioning Timing Fix (v19.3.9)
200
-
201
- ### Issue
202
- Certificate provisioning would start before ports were listening, causing ACME HTTP-01 challenges to fail with connection refused errors.
203
-
204
- ### Root Cause
205
- SmartProxy initialization sequence:
206
- 1. Certificate manager initialized → immediately starts provisioning
207
- 2. Ports start listening (too late for ACME challenges)
208
-
209
- ### Solution
210
- Deferred certificate provisioning until after ports are ready:
211
- ```typescript
212
- // SmartCertManager.initialize() now skips automatic provisioning
213
- // SmartProxy.start() calls provisionAllCertificates() directly after ports are listening
214
- ```
215
-
216
- ### Test Coverage
217
- - `test/test.acme-timing-simple.ts` - Verifies proper timing sequence
218
-
219
- ### Migration
220
- Update to v19.3.9+, no configuration changes needed.
221
-
222
- ## Socket Handler Race Condition Fix (v19.5.0)
223
-
224
- ### Issue
225
- Initial data chunks were being emitted before async socket handlers had completed setup, causing data loss when handlers performed async operations before setting up data listeners.
226
-
227
- ### Root Cause
228
- The `handleSocketHandlerAction` method was using `process.nextTick` to emit initial chunks regardless of whether the handler was sync or async. This created a race condition where async handlers might not have their listeners ready when the initial data was emitted.
229
-
230
- ### Solution
231
- Differentiated between sync and async handlers:
232
- ```typescript
233
- const result = route.action.socketHandler(socket);
234
-
235
- if (result instanceof Promise) {
236
- // Async handler - wait for completion before emitting initial data
237
- result.then(() => {
238
- if (initialChunk && initialChunk.length > 0) {
239
- socket.emit('data', initialChunk);
240
- }
241
- }).catch(/*...*/);
242
- } else {
243
- // Sync handler - use process.nextTick as before
244
- if (initialChunk && initialChunk.length > 0) {
245
- process.nextTick(() => {
246
- socket.emit('data', initialChunk);
247
- });
248
- }
249
- }
250
- ```
251
-
252
- ### Test Coverage
253
- - `test/test.socket-handler-race.ts` - Specifically tests async handlers with delayed listener setup
254
- - Verifies that initial data is received even when handler sets up listeners after async work
255
-
256
- ### Usage Note
257
- Socket handlers require initial data from the client to trigger routing (not just a TLS handshake). Clients must send at least one byte of data for the handler to be invoked.
258
-
259
- ## Route-Specific Security Implementation (v19.5.3)
260
-
261
- ### Issue
262
- Route-specific security configurations (ipAllowList, ipBlockList, authentication) were defined in the route types but not enforced at runtime.
263
-
264
- ### Root Cause
265
- The RouteConnectionHandler only checked global IP validation but didn't enforce route-specific security rules after matching a route.
266
-
267
- ### Solution
268
- Added security checks after route matching:
269
- ```typescript
270
- // Apply route-specific security checks
271
- const routeSecurity = route.action.security || route.security;
272
- if (routeSecurity) {
273
- // Check IP allow/block lists
274
- if (routeSecurity.ipAllowList || routeSecurity.ipBlockList) {
275
- const isIPAllowed = this.securityManager.isIPAuthorized(
276
- remoteIP,
277
- routeSecurity.ipAllowList || [],
278
- routeSecurity.ipBlockList || []
279
- );
280
-
281
- if (!isIPAllowed) {
282
- socket.end();
283
- this.connectionManager.cleanupConnection(record, 'route_ip_blocked');
284
- return;
285
- }
286
- }
287
- }
288
- ```
289
-
290
- ### Test Coverage
291
- - `test/test.route-security-unit.ts` - Unit tests verifying SecurityManager.isIPAuthorized logic
292
- - Tests confirm IP allow/block lists work correctly with glob patterns
293
-
294
- ### Configuration Example
295
- ```typescript
296
- const routes: IRouteConfig[] = [{
297
- name: 'secure-api',
298
- match: { ports: 8443, domains: 'api.example.com' },
299
- action: {
300
- type: 'forward',
301
- target: { host: 'localhost', port: 3000 },
302
- security: {
303
- ipAllowList: ['192.168.1.*', '10.0.0.0/8'], // Allow internal IPs
304
- ipBlockList: ['192.168.1.100'], // But block specific IP
305
- maxConnections: 100, // Per-route limit (TODO)
306
- authentication: { // HTTP-only, requires TLS termination
307
- type: 'basic',
308
- credentials: [{ username: 'api', password: 'secret' }]
309
- }
310
- }
311
- }
312
- }];
313
- ```
314
-
315
- ### Notes
316
- - IP lists support glob patterns (via minimatch): `192.168.*`, `10.?.?.1`
317
- - Block lists take precedence over allow lists
318
- - Authentication requires TLS termination (cannot be enforced on passthrough/direct connections)
319
- - Per-route connection limits are not yet implemented
320
- - Security is defined at the route level (route.security), not in the action
321
- - Route matching is based solely on match criteria; security is enforced after matching
322
-
323
- ## Performance Issues Investigation (v19.5.3+)
324
-
325
- ### Critical Blocking Operations Found
326
- 1. **Busy Wait Loop** in `ts/proxies/nftables-proxy/nftables-proxy.ts:235-238`
327
- - Blocks entire event loop with `while (Date.now() < waitUntil) {}`
328
- - Should use `await new Promise(resolve => setTimeout(resolve, delay))`
329
-
330
- 2. **Synchronous Filesystem Operations**
331
- - Certificate management uses `fs.existsSync()`, `fs.mkdirSync()`, `fs.readFileSync()`
332
- - NFTables proxy uses `execSync()` for system commands
333
- - Certificate store uses `ensureDirSync()`, `fileExistsSync()`, `removeManySync()`
334
-
335
- 3. **Memory Leak Risks**
336
- - Several `setInterval()` calls without storing references for cleanup
337
- - Event listeners added without proper cleanup in error paths
338
- - Missing `removeAllListeners()` calls in some connection cleanup scenarios
339
-
340
- ### Performance Recommendations
341
- - Replace all sync filesystem operations with async alternatives
342
- - Fix the busy wait loop immediately (critical event loop blocker)
343
- - Add proper cleanup for all timers and event listeners
344
- - Consider worker threads for CPU-intensive operations
345
- - See `readme.problems.md` for detailed analysis and recommendations
346
-
347
- ## Performance Optimizations Implemented (Phase 1 - v19.6.0)
348
-
349
- ### 1. Async Utilities Created (`ts/core/utils/async-utils.ts`)
350
- - **delay()**: Non-blocking alternative to busy wait loops
351
- - **retryWithBackoff()**: Retry operations with exponential backoff
352
- - **withTimeout()**: Execute operations with timeout protection
353
- - **parallelLimit()**: Run async operations with concurrency control
354
- - **debounceAsync()**: Debounce async functions
355
- - **AsyncMutex**: Ensure exclusive access to resources
356
- - **CircuitBreaker**: Protect against cascading failures
357
-
358
- ### 2. Filesystem Utilities Created (`ts/core/utils/fs-utils.ts`)
359
- - **AsyncFileSystem**: Complete async filesystem operations
360
- - exists(), ensureDir(), readFile(), writeFile()
361
- - readJSON(), writeJSON() with proper error handling
362
- - copyFile(), moveFile(), removeDir()
363
- - Stream creation and file listing utilities
364
-
365
- ### 3. Critical Fixes Applied
366
-
367
- #### Busy Wait Loop Fixed
368
- - **Location**: `ts/proxies/nftables-proxy/nftables-proxy.ts:235-238`
369
- - **Fix**: Replaced `while (Date.now() < waitUntil) {}` with `await delay(ms)`
370
- - **Impact**: Unblocks event loop, massive performance improvement
371
-
372
- #### Certificate Manager Migration
373
- - **File**: `ts/proxies/http-proxy/certificate-manager.ts`
374
- - Added async initialization method
375
- - Kept sync methods for backward compatibility with deprecation warnings
376
- - Added `loadDefaultCertificatesAsync()` method
377
-
378
- #### Certificate Store Migration
379
- - **File**: `ts/proxies/smart-proxy/cert-store.ts`
380
- - Replaced all `fileExistsSync`, `ensureDirSync`, `removeManySync`
381
- - Used parallel operations with `Promise.all()` for better performance
382
- - Improved error handling and async JSON operations
383
-
384
- #### NFTables Proxy Improvements
385
- - Added deprecation warnings to sync methods
386
- - Created `executeWithTempFile()` helper for common pattern
387
- - Started migration of sync filesystem operations to async
388
- - Added import for delay and AsyncFileSystem utilities
389
-
390
- ### 4. Backward Compatibility Maintained
391
- - All sync methods retained with deprecation warnings
392
- - Existing APIs unchanged, new async methods added alongside
393
- - Feature flags prepared for gradual rollout
394
-
395
- ### 5. Phase 1 Completion Status
396
- ✅ **Phase 1 COMPLETE** - All critical performance fixes have been implemented:
397
- - ✅ Fixed busy wait loop in nftables-proxy.ts
398
- - ✅ Created async utilities (delay, retry, timeout, parallelLimit, mutex, circuit breaker)
399
- - ✅ Created filesystem utilities (AsyncFileSystem with full async operations)
400
- - ✅ Migrated all certificate management to async operations
401
- - ✅ Migrated nftables-proxy filesystem operations to async (except stopSync for exit handlers)
402
- - ✅ All tests passing for new utilities
403
-
404
- ### 6. Phase 2 Progress Status
405
- 🔨 **Phase 2 IN PROGRESS** - Resource Lifecycle Management:
406
- - ✅ Created LifecycleComponent base class for automatic resource cleanup
407
- - ✅ Created BinaryHeap data structure for priority queue operations
408
- - ✅ Created EnhancedConnectionPool with backpressure and health checks
409
- - ✅ Cleaned up legacy code (removed ts/common/, event-utils.ts, event-system.ts)
410
- - 📋 TODO: Migrate existing components to extend LifecycleComponent
411
- - 📋 TODO: Add integration tests for resource management
412
-
413
- ### 7. Next Steps (Remaining Work)
414
- - **Phase 2 (cont)**: Migrate components to use LifecycleComponent
415
- - **Phase 3**: Add worker threads for CPU-intensive operations
416
- - **Phase 4**: Performance monitoring dashboard
417
-
418
- ## Socket Error Handling Fix (v19.5.11+)
419
-
420
- ### Issue
421
- Server crashed with unhandled 'error' event when backend connections failed (ECONNREFUSED). Also caused memory leak with rising active connection count as failed connections weren't cleaned up properly.
422
-
423
- ### Root Cause
424
- 1. **Race Condition**: In forwarding handlers, sockets were created with `net.connect()` but error handlers were attached later, creating a window where errors could crash the server
425
- 2. **Incomplete Cleanup**: When server connections failed, client sockets weren't properly cleaned up, leaving connection records in memory
426
-
427
- ### Solution
428
- Created `createSocketWithErrorHandler()` utility that attaches error handlers immediately:
429
- ```typescript
430
- // Before (race condition):
431
- const socket = net.connect(port, host);
432
- // ... other code ...
433
- socket.on('error', handler); // Too late!
434
-
435
- // After (safe):
436
- const socket = createSocketWithErrorHandler({
437
- port, host,
438
- onError: (error) => {
439
- // Handle error immediately
440
- clientSocket.destroy();
441
- },
442
- onConnect: () => {
443
- // Set up forwarding
444
- }
445
- });
446
- ```
447
-
448
- ### Changes Made
449
- 1. **New Utility**: `ts/core/utils/socket-utils.ts` - Added `createSocketWithErrorHandler()`
450
- 2. **Updated Handlers**:
451
- - `https-passthrough-handler.ts` - Uses safe socket creation
452
- - `https-terminate-to-http-handler.ts` - Uses safe socket creation
453
- 3. **Connection Cleanup**: Client sockets destroyed immediately on server connection failure
454
-
455
- ### Test Coverage
456
- - `test/test.socket-error-handling.node.ts` - Verifies server doesn't crash on ECONNREFUSED
457
- - `test/test.forwarding-error-fix.node.ts` - Tests forwarding handlers handle errors gracefully
458
-
459
- ### Configuration
460
- No configuration changes needed. The fix is transparent to users.
461
-
462
- ### Important Note
463
- The fix was applied in two places:
464
- 1. **ForwardingHandler classes** (`https-passthrough-handler.ts`, etc.) - These are standalone forwarding utilities
465
- 2. **SmartProxy route-connection-handler** (`route-connection-handler.ts`) - This is where the actual SmartProxy connection handling happens
466
-
467
- The critical fix for SmartProxy was in `setupDirectConnection()` method in route-connection-handler.ts, which now uses `createSocketWithErrorHandler()` to properly handle connection failures and clean up connection records.
468
-
469
- ## Connection Cleanup Improvements (v19.5.12+)
470
-
471
- ### Issue
472
- Connections were still counting up during rapid retry scenarios, especially when routing failed or backend connections were refused. This was due to:
473
- 1. **Delayed Cleanup**: Using `initiateCleanupOnce` queued cleanup operations (batch of 100 every 100ms) instead of immediate cleanup
474
- 2. **NFTables Memory Leak**: NFTables connections were never cleaned up, staying in memory forever
475
- 3. **Connection Limit Bypass**: When max connections reached, connection record check happened after creation
476
-
477
- ### Root Cause Analysis
478
- 1. **Queued vs Immediate Cleanup**:
479
- - `initiateCleanupOnce()`: Adds to cleanup queue, processes up to 100 connections every 100ms
480
- - `cleanupConnection()`: Immediate synchronous cleanup
481
- - Under rapid retries, connections were created faster than the queue could process them
482
-
483
- 2. **NFTables Connections**:
484
- - Marked with `usingNetworkProxy = true` but never cleaned up
485
- - Connection records stayed in memory indefinitely
486
-
487
- 3. **Error Path Cleanup**:
488
- - Many error paths used `socket.end()` (async) followed by cleanup
489
- - Created timing windows where connections weren't fully cleaned
490
-
491
- ### Solution
492
- 1. **Immediate Cleanup**: Changed all error paths from `initiateCleanupOnce()` to `cleanupConnection()` for immediate cleanup
493
- 2. **NFTables Cleanup**: Added socket close listener to clean up connection records when NFTables connections close
494
- 3. **Connection Limit Fix**: Added null check after `createConnection()` to handle rejection properly
495
-
496
- ### Changes Made in route-connection-handler.ts
497
- ```typescript
498
- // 1. NFTables cleanup (line 551-553)
499
- socket.once('close', () => {
500
- this.connectionManager.cleanupConnection(record, 'nftables_closed');
501
- });
502
-
503
- // 2. Connection limit check (line 93-96)
504
- const record = this.connectionManager.createConnection(socket);
505
- if (!record) {
506
- // Connection was rejected due to limit - socket already destroyed
507
- return;
508
- }
509
-
510
- // 3. Changed all error paths to use immediate cleanup
511
- // Before: this.connectionManager.initiateCleanupOnce(record, reason)
512
- // After: this.connectionManager.cleanupConnection(record, reason)
513
- ```
514
-
515
- ### Test Coverage
516
- - `test/test.rapid-retry-cleanup.node.ts` - Verifies connection cleanup under rapid retry scenarios
517
- - Test shows connection count stays at 0 even with 20 rapid retries with 50ms intervals
518
- - Confirms both ECONNREFUSED and routing failure scenarios are handled correctly
519
-
520
- ### Performance Impact
521
- - **Positive**: No more connection accumulation under load
522
- - **Positive**: Immediate cleanup reduces memory usage
523
- - **Consideration**: More frequent cleanup operations, but prevents queue backlog
524
-
525
- ### Migration Notes
526
- No configuration changes needed. The improvements are automatic and backward compatible.
527
-
528
- ## Early Client Disconnect Handling (v19.5.13+)
529
-
530
- ### Issue
531
- Connections were accumulating when clients connected but disconnected before sending data or during routing. This occurred in two scenarios:
532
- 1. **TLS Path**: Clients connecting and disconnecting before sending initial TLS handshake data
533
- 2. **Non-TLS Immediate Routing**: Clients disconnecting while backend connection was being established
534
-
535
- ### Root Cause
536
- 1. **Missing Cleanup Handlers**: During initial data wait and immediate routing, no close/end handlers were attached to catch early disconnections
537
- 2. **Race Condition**: Backend connection attempts continued even after client disconnected, causing unhandled errors
538
- 3. **Timing Window**: Between accepting connection and establishing full bidirectional flow, disconnections weren't properly handled
539
-
540
- ### Solution
541
- 1. **TLS Path Fix**: Added close/end handlers during initial data wait (lines 224-253 in route-connection-handler.ts)
542
- 2. **Immediate Routing Fix**: Used `setupSocketHandlers` for proper handler attachment (lines 180-205)
543
- 3. **Backend Error Handling**: Check if connection already closed before handling backend errors (line 1144)
544
-
545
- ### Changes Made
546
- ```typescript
547
- // 1. TLS path - handle disconnect before initial data
548
- socket.once('close', () => {
549
- if (!initialDataReceived) {
550
- this.connectionManager.cleanupConnection(record, 'closed_before_data');
551
- }
552
- });
553
-
554
- // 2. Immediate routing path - proper handler setup
555
- setupSocketHandlers(socket, (reason) => {
556
- if (!record.outgoing || record.outgoing.readyState !== 'open') {
557
- if (record.outgoing && !record.outgoing.destroyed) {
558
- record.outgoing.destroy(); // Abort pending backend connection
559
- }
560
- this.connectionManager.cleanupConnection(record, reason);
561
- }
562
- }, undefined, 'immediate-route-client');
563
-
564
- // 3. Backend connection error handling
565
- onError: (error) => {
566
- if (record.connectionClosed) {
567
- logger.log('debug', 'Backend connection failed but client already disconnected');
568
- return; // Client already gone, nothing to clean up
569
- }
570
- // ... normal error handling
571
- }
572
- ```
573
-
574
- ### Test Coverage
575
- - `test/test.connect-disconnect-cleanup.node.ts` - Comprehensive test for early disconnect scenarios
576
- - Tests verify connection count stays at 0 even with rapid connect/disconnect patterns
577
- - Covers immediate disconnect, delayed disconnect, and mixed patterns
578
-
579
- ### Performance Impact
580
- - **Positive**: No more connection accumulation from early disconnects
581
- - **Positive**: Immediate cleanup reduces memory usage
582
- - **Positive**: Prevents resource exhaustion from rapid reconnection attempts
583
-
584
- ### Migration Notes
585
- No configuration changes needed. The fix is automatic and backward compatible.
586
-
587
- ## Proxy Chain Connection Accumulation Fix (v19.5.14+)
588
-
589
- ### Issue
590
- When chaining SmartProxies (Client → SmartProxy1 → SmartProxy2 → Backend), connections would accumulate and never be cleaned up. This was particularly severe when the backend was down or closing connections immediately.
591
-
592
- ### Root Cause
593
- The half-open connection support was preventing proper cascade cleanup in proxy chains:
594
- 1. Backend closes → SmartProxy2's server socket closes
595
- 2. SmartProxy2 keeps client socket open (half-open support)
596
- 3. SmartProxy1 never gets notified that downstream is closed
597
- 4. Connections accumulate at each proxy in the chain
598
-
599
- The issue was in `createIndependentSocketHandlers()` which waited for BOTH sockets to close before cleanup.
600
-
601
- ### Solution
602
- 1. **Changed default behavior**: When one socket closes, both close immediately
603
- 2. **Made half-open support opt-in**: Only enabled when explicitly requested
604
- 3. **Centralized socket handling**: Created `setupBidirectionalForwarding()` for consistent behavior
605
- 4. **Applied everywhere**: Updated HttpProxyBridge and route-connection-handler to use centralized handling
606
-
607
- ### Changes Made
608
- ```typescript
609
- // socket-utils.ts - Default behavior now closes both sockets
610
- export function createIndependentSocketHandlers(
611
- clientSocket, serverSocket, onBothClosed,
612
- options: { enableHalfOpen?: boolean } = {} // Half-open is opt-in
613
- ) {
614
- // When server closes, immediately close client (unless half-open enabled)
615
- if (!clientClosed && !options.enableHalfOpen) {
616
- clientSocket.destroy();
617
- }
618
- }
619
-
620
- // New centralized function for consistent socket pairing
621
- export function setupBidirectionalForwarding(
622
- clientSocket, serverSocket,
623
- handlers: {
624
- onClientData?: (chunk) => void;
625
- onServerData?: (chunk) => void;
626
- onCleanup: (reason) => void;
627
- enableHalfOpen?: boolean; // Default: false
628
- }
629
- )
630
- ```
631
-
632
- ### Test Coverage
633
- - `test/test.proxy-chain-simple.node.ts` - Verifies proxy chains don't accumulate connections
634
- - Tests confirm connections stay at 0 even with backend closing immediately
635
- - Works for any proxy chain configuration (not just localhost)
636
-
637
- ### Performance Impact
638
- - **Positive**: No more connection accumulation in proxy chains
639
- - **Positive**: Immediate cleanup reduces memory usage
640
- - **Neutral**: Half-open connections still available when needed (opt-in)
641
-
642
- ### Migration Notes
643
- No configuration changes needed. The fix applies to all proxy chains automatically.
644
-
645
- ## Socket Cleanup Handler Deprecation (v19.5.15+)
646
-
647
- ### Issue
648
- The deprecated `createSocketCleanupHandler()` function was still being used in forwarding handlers, despite being marked as deprecated.
649
-
650
- ### Solution
651
- Updated all forwarding handlers to use the new centralized socket utilities:
652
- 1. **Replaced `createSocketCleanupHandler()`** with `setupBidirectionalForwarding()` in:
653
- - `https-terminate-to-https-handler.ts`
654
- - `https-terminate-to-http-handler.ts`
655
- 2. **Removed deprecated function** from `socket-utils.ts`
656
-
657
- ### Benefits
658
- - Consistent socket handling across all handlers
659
- - Proper cleanup in proxy chains (no half-open connections by default)
660
- - Better backpressure handling with the centralized implementation
661
- - Reduced code duplication
662
-
663
- ### Migration Notes
664
- No user-facing changes. All forwarding handlers now use the same robust socket handling as the main SmartProxy connection handler.
665
-
666
- ## WrappedSocket Class Evaluation for PROXY Protocol (v19.5.19+)
667
-
668
- ### Current Socket Handling Architecture
669
- - Sockets are handled directly as `net.Socket` instances throughout the codebase
670
- - Socket augmentation via TypeScript module augmentation for TLS properties
671
- - Metadata tracked separately in `IConnectionRecord` objects
672
- - Socket utilities provide helper functions but don't encapsulate the socket
673
- - Connection records track extensive metadata (IDs, timestamps, byte counters, TLS state, etc.)
674
-
675
- ### Evaluation: Should We Introduce a WrappedSocket Class?
676
-
677
- **Yes, a WrappedSocket class would make sense**, particularly for PROXY protocol implementation and future extensibility.
678
-
679
- ### Design Considerations for WrappedSocket
680
-
681
- ```typescript
682
- class WrappedSocket {
683
- private socket: net.Socket;
684
- private connectionId: string;
685
- private metadata: {
686
- realClientIP?: string; // From PROXY protocol
687
- realClientPort?: number; // From PROXY protocol
688
- proxyIP?: string; // Immediate connection IP
689
- proxyPort?: number; // Immediate connection port
690
- bytesReceived: number;
691
- bytesSent: number;
692
- lastActivity: number;
693
- isTLS: boolean;
694
- // ... other metadata
695
- };
696
-
697
- // PROXY protocol handling
698
- private proxyProtocolParsed: boolean = false;
699
- private pendingData: Buffer[] = [];
700
-
701
- constructor(socket: net.Socket) {
702
- this.socket = socket;
703
- this.setupHandlers();
704
- }
705
-
706
- // Getters for clean access
707
- get remoteAddress(): string {
708
- return this.metadata.realClientIP || this.socket.remoteAddress || '';
709
- }
710
-
711
- get remotePort(): number {
712
- return this.metadata.realClientPort || this.socket.remotePort || 0;
713
- }
714
-
715
- get isFromTrustedProxy(): boolean {
716
- return !!this.metadata.realClientIP;
717
- }
718
-
719
- // PROXY protocol parsing
720
- async parseProxyProtocol(trustedProxies: string[]): Promise<boolean> {
721
- // Implementation here
722
- }
723
-
724
- // Delegate socket methods
725
- write(data: any): boolean {
726
- this.metadata.bytesSent += Buffer.byteLength(data);
727
- return this.socket.write(data);
728
- }
729
-
730
- destroy(error?: Error): void {
731
- this.socket.destroy(error);
732
- }
733
-
734
- // Event forwarding
735
- on(event: string, listener: Function): this {
736
- this.socket.on(event, listener);
737
- return this;
738
- }
739
- }
740
- ```
741
-
742
- ### Implementation Benefits
743
-
744
- 1. **Encapsulation**: Bundle socket + metadata + behavior in one place
745
- 2. **PROXY Protocol Integration**: Cleaner handling without modifying existing socket code
746
- 3. **State Management**: Centralized socket state tracking and validation
747
- 4. **API Consistency**: Uniform interface for all socket operations
748
- 5. **Future Extensibility**: Easy to add new socket-level features (compression, encryption, etc.)
749
- 6. **Type Safety**: Better TypeScript support without module augmentation
750
- 7. **Testing**: Easier to mock and test socket behavior
751
-
752
- ### Implementation Drawbacks
753
-
754
- 1. **Major Refactoring**: Would require changes throughout the codebase
755
- 2. **Performance Overhead**: Additional abstraction layer (minimal but present)
756
- 3. **Compatibility**: Need to maintain event emitter compatibility
757
- 4. **Learning Curve**: Developers need to understand the wrapper
758
-
759
- ### Recommended Approach: Phased Implementation
760
-
761
- **Phase 1: PROXY Protocol Only** (Immediate)
762
- - Create minimal `ProxyProtocolSocket` wrapper for new connections from trusted proxies
763
- - Use in connection handler when receiving from trusted proxy IPs
764
- - Minimal disruption to existing code
765
-
766
- ```typescript
767
- class ProxyProtocolSocket {
768
- constructor(
769
- public socket: net.Socket,
770
- public realClientIP?: string,
771
- public realClientPort?: number
772
- ) {}
773
-
774
- get remoteAddress(): string {
775
- return this.realClientIP || this.socket.remoteAddress || '';
776
- }
777
-
778
- get remotePort(): number {
779
- return this.realClientPort || this.socket.remotePort || 0;
780
- }
781
- }
782
- ```
783
-
784
- **Phase 2: Gradual Migration** (Future)
785
- - Extend wrapper with more functionality
786
- - Migrate critical paths to use wrapper
787
- - Add performance monitoring
788
-
789
- **Phase 3: Full Adoption** (Long-term)
790
- - Complete migration to WrappedSocket
791
- - Remove socket augmentation
792
- - Standardize all socket handling
793
-
794
- ### Decision Summary
795
-
796
- ✅ **Implement minimal ProxyProtocolSocket for immediate PROXY protocol support**
797
- - Low risk, high value
798
- - Solves the immediate proxy chain connection limit issue
799
- - Sets foundation for future improvements
800
- - Can be implemented alongside existing code
801
-
802
- 📋 **Consider full WrappedSocket for future major version**
803
- - Cleaner architecture
804
- - Better maintainability
805
- - But requires significant refactoring
806
-
807
- ## WrappedSocket Implementation (PROXY Protocol Phase 1) - v19.5.19+
808
-
809
- The WrappedSocket class has been implemented as the foundation for PROXY protocol support:
810
-
811
- ### Implementation Details
812
-
813
- 1. **Design Approach**: Uses JavaScript Proxy to delegate all Socket methods/properties to the underlying socket while allowing override of specific properties (remoteAddress, remotePort).
814
-
815
- 2. **Key Design Decisions**:
816
- - NOT a Duplex stream - Initially tried this approach but it created infinite loops
817
- - Simple wrapper using Proxy pattern for transparent delegation
818
- - All sockets are wrapped, not just those from trusted proxies
819
- - Trusted proxy detection happens after wrapping
820
-
821
- 3. **Usage Pattern**:
822
- ```typescript
823
- // In RouteConnectionHandler.handleConnection()
824
- const wrappedSocket = new WrappedSocket(socket);
825
- // Pass wrappedSocket throughout the flow
826
-
827
- // When calling socket-utils functions, extract underlying socket:
828
- const underlyingSocket = getUnderlyingSocket(socket);
829
- setupBidirectionalForwarding(underlyingSocket, targetSocket, {...});
830
- ```
831
-
832
- 4. **Important Implementation Notes**:
833
- - Socket utility functions (setupBidirectionalForwarding, cleanupSocket) expect raw net.Socket
834
- - Always extract underlying socket before passing to these utilities using `getUnderlyingSocket()`
835
- - WrappedSocket preserves all Socket functionality through Proxy delegation
836
- - TypeScript typing handled via index signature: `[key: string]: any`
837
-
838
- 5. **Files Modified**:
839
- - `ts/core/models/wrapped-socket.ts` - The WrappedSocket implementation
840
- - `ts/core/models/socket-types.ts` - Helper functions and type guards
841
- - `ts/proxies/smart-proxy/route-connection-handler.ts` - Updated to wrap all incoming sockets
842
- - `ts/proxies/smart-proxy/connection-manager.ts` - Updated to accept WrappedSocket
843
- - `ts/proxies/smart-proxy/http-proxy-bridge.ts` - Updated to handle WrappedSocket
844
-
845
- 6. **Test Coverage**:
846
- - `test/test.wrapped-socket-forwarding.ts` - Verifies data forwarding through wrapped sockets
847
-
848
- ### Next Steps for PROXY Protocol
849
- - Phase 2: Parse PROXY protocol header from trusted proxies
850
- - Phase 3: Update real client IP/port after parsing
851
- - Phase 4: Test with HAProxy and AWS ELB
852
- - Phase 5: Documentation and configuration
853
-
854
- ## Proxy Protocol Documentation
855
-
856
- For detailed information about proxy protocol implementation and proxy chaining:
857
- - **[Proxy Protocol Guide](./readme.proxy-protocol.md)** - Complete implementation details and configuration
858
- - **[Proxy Protocol Examples](./readme.proxy-protocol-example.md)** - Code examples and conceptual implementation
859
- - **[Proxy Chain Summary](./readme.proxy-chain-summary.md)** - Quick reference for proxy chaining setup
860
-
861
- ## Connection Cleanup Edge Cases Investigation (v19.5.20+)
862
-
863
- ### Issue Discovered
864
- "Zombie connections" can occur when both sockets are destroyed but the connection record hasn't been cleaned up. This happens when sockets are destroyed without triggering their close/error event handlers.
865
-
866
- ### Root Cause
867
- 1. **Event Handler Bypass**: In edge cases (network failures, proxy chain failures, forced socket destruction), sockets can be destroyed without their event handlers being called
868
- 2. **Cleanup Queue Delay**: The `initiateCleanupOnce` method adds connections to a cleanup queue (batch of 100 every 100ms), which may not process fast enough
869
- 3. **Inactivity Check Limitation**: The periodic inactivity check only examines `lastActivity` timestamps, not actual socket states
870
-
871
- ### Test Results
872
- Debug script (`connection-manager-direct-test.ts`) revealed:
873
- - **Normal cleanup works**: When socket events fire normally, cleanup is reliable
874
- - **Zombies ARE created**: Direct socket destruction creates zombies (destroyed sockets, connectionClosed=false)
875
- - **Manual cleanup works**: Calling `initiateCleanupOnce` on a zombie does clean it up
876
- - **Inactivity check misses zombies**: The check doesn't detect connections with destroyed sockets
877
-
878
- ### Potential Solutions
879
- 1. **Periodic Zombie Detection**: Add zombie detection to the inactivity check:
880
- ```typescript
881
- // In performOptimizedInactivityCheck
882
- if (record.incoming?.destroyed && record.outgoing?.destroyed && !record.connectionClosed) {
883
- this.cleanupConnection(record, 'zombie_detected');
884
- }
885
- ```
886
-
887
- 2. **Socket State Monitoring**: Check socket states during connection operations
888
- 3. **Defensive Socket Handling**: Always attach cleanup handlers before any operation that might destroy sockets
889
- 4. **Immediate Cleanup Option**: For critical paths, use `cleanupConnection` instead of `initiateCleanupOnce`
890
-
891
- ### Impact
892
- - Memory leaks in edge cases (network failures, proxy chain issues)
893
- - Connection count inaccuracy
894
- - Potential resource exhaustion over time
895
-
896
- ### Test Files
897
- - `.nogit/debug/connection-manager-direct-test.ts` - Direct ConnectionManager testing showing zombie creation