@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.
@@ -1,112 +0,0 @@
1
- # SmartProxy: Proxy Protocol and Proxy Chaining Summary
2
-
3
- ## Quick Summary
4
-
5
- SmartProxy supports proxy chaining through the **WrappedSocket** infrastructure, which is designed to handle PROXY protocol for preserving real client IP addresses across multiple proxy layers. While the infrastructure is in place (v19.5.19+), the actual PROXY protocol parsing is not yet implemented.
6
-
7
- ## Current State
8
-
9
- ### ✅ What's Implemented
10
- - **WrappedSocket class** - Foundation for proxy protocol support
11
- - **Proxy IP configuration** - `proxyIPs` setting to define trusted proxies
12
- - **Socket wrapping** - All incoming connections wrapped automatically
13
- - **Connection tracking** - Real client IP tracking in connection records
14
- - **Test infrastructure** - Tests for proxy chaining scenarios
15
-
16
- ### ❌ What's Missing
17
- - **PROXY protocol v1 parsing** - Header parsing not implemented
18
- - **PROXY protocol v2 support** - Binary format not supported
19
- - **Automatic header generation** - Must be manually implemented
20
- - **Production testing** - No HAProxy/AWS ELB compatibility tests
21
-
22
- ## Key Files
23
-
24
- ### Core Implementation
25
- - `ts/core/models/wrapped-socket.ts` - WrappedSocket class
26
- - `ts/core/models/socket-types.ts` - Helper functions
27
- - `ts/proxies/smart-proxy/route-connection-handler.ts` - Connection handling
28
- - `ts/proxies/smart-proxy/models/interfaces.ts` - Configuration interfaces
29
-
30
- ### Tests
31
- - `test/test.wrapped-socket.ts` - WrappedSocket unit tests
32
- - `test/test.proxy-chain-simple.node.ts` - Basic proxy chain test
33
- - `test/test.proxy-chaining-accumulation.node.ts` - Connection leak tests
34
-
35
- ### Documentation
36
- - `readme.proxy-protocol.md` - Detailed implementation guide
37
- - `readme.proxy-protocol-example.md` - Code examples and future implementation
38
- - `readme.hints.md` - Project overview with WrappedSocket notes
39
-
40
- ## Quick Configuration Example
41
-
42
- ```typescript
43
- // Outer proxy (internet-facing)
44
- const outerProxy = new SmartProxy({
45
- sendProxyProtocol: true, // Will send PROXY protocol (when implemented)
46
- routes: [{
47
- name: 'forward-to-inner',
48
- match: { ports: 443 },
49
- action: {
50
- type: 'forward',
51
- target: { host: 'inner-proxy.local', port: 443 },
52
- tls: { mode: 'passthrough' }
53
- }
54
- }]
55
- });
56
-
57
- // Inner proxy (backend-facing)
58
- const innerProxy = new SmartProxy({
59
- proxyIPs: ['outer-proxy.local'], // Trust the outer proxy
60
- acceptProxyProtocol: true, // Will parse PROXY protocol (when implemented)
61
- routes: [{
62
- name: 'forward-to-backend',
63
- match: { ports: 443, domains: 'api.example.com' },
64
- action: {
65
- type: 'forward',
66
- target: { host: 'backend.local', port: 8080 },
67
- tls: { mode: 'terminate' }
68
- }
69
- }]
70
- });
71
- ```
72
-
73
- ## How It Works (Conceptually)
74
-
75
- 1. **Client** connects to **Outer Proxy**
76
- 2. **Outer Proxy** wraps socket in WrappedSocket
77
- 3. **Outer Proxy** forwards to **Inner Proxy**
78
- - Would prepend: `PROXY TCP4 <client-ip> <proxy-ip> <client-port> <proxy-port>\r\n`
79
- 4. **Inner Proxy** receives connection from trusted proxy
80
- 5. **Inner Proxy** would parse PROXY protocol header
81
- 6. **Inner Proxy** updates WrappedSocket with real client IP
82
- 7. **Backend** receives connection with preserved client information
83
-
84
- ## Important Notes
85
-
86
- ### Connection Cleanup
87
- The fix for proxy chain connection accumulation (v19.5.14+) changed the default socket behavior:
88
- - **Before**: Half-open connections supported by default (caused accumulation)
89
- - **After**: Both sockets close when one closes (prevents accumulation)
90
- - **Override**: Set `enableHalfOpen: true` if half-open needed
91
-
92
- ### Security
93
- - Only parse PROXY protocol from IPs listed in `proxyIPs`
94
- - Never use `0.0.0.0/0` as a trusted proxy range
95
- - Each proxy in chain must explicitly trust the previous proxy
96
-
97
- ### Testing
98
- Use the test files as reference implementations:
99
- - Simple chains: `test.proxy-chain-simple.node.ts`
100
- - Connection leaks: `test.proxy-chaining-accumulation.node.ts`
101
- - Rapid reconnects: `test.rapid-retry-cleanup.node.ts`
102
-
103
- ## Next Steps
104
-
105
- To fully implement PROXY protocol support:
106
- 1. Implement the parser in `ProxyProtocolParser` class
107
- 2. Integrate parser into `handleConnection` method
108
- 3. Add header generation to `setupDirectConnection`
109
- 4. Test with real proxies (HAProxy, nginx, AWS ELB)
110
- 5. Add PROXY protocol v2 support for better performance
111
-
112
- See `readme.proxy-protocol-example.md` for detailed implementation examples.
@@ -1,462 +0,0 @@
1
- # SmartProxy PROXY Protocol Implementation Example
2
-
3
- This document shows how PROXY protocol parsing could be implemented in SmartProxy. Note that this is a conceptual implementation guide - the actual parsing is not yet implemented in the current version.
4
-
5
- ## Conceptual PROXY Protocol v1 Parser Implementation
6
-
7
- ### Parser Class
8
-
9
- ```typescript
10
- // This would go in ts/core/utils/proxy-protocol-parser.ts
11
- import { logger } from './logger.js';
12
-
13
- export interface IProxyProtocolInfo {
14
- version: 1 | 2;
15
- command: 'PROXY' | 'LOCAL';
16
- family: 'TCP4' | 'TCP6' | 'UNKNOWN';
17
- sourceIP: string;
18
- destIP: string;
19
- sourcePort: number;
20
- destPort: number;
21
- headerLength: number;
22
- }
23
-
24
- export class ProxyProtocolParser {
25
- private static readonly PROXY_V1_SIGNATURE = 'PROXY ';
26
- private static readonly MAX_V1_HEADER_LENGTH = 108; // Max possible v1 header
27
-
28
- /**
29
- * Parse PROXY protocol v1 header from buffer
30
- * Returns null if not a valid PROXY protocol header
31
- */
32
- static parseV1(buffer: Buffer): IProxyProtocolInfo | null {
33
- // Need at least 8 bytes for "PROXY " + newline
34
- if (buffer.length < 8) {
35
- return null;
36
- }
37
-
38
- // Check for v1 signature
39
- const possibleHeader = buffer.toString('ascii', 0, 6);
40
- if (possibleHeader !== this.PROXY_V1_SIGNATURE) {
41
- return null;
42
- }
43
-
44
- // Find the end of the header (CRLF)
45
- let headerEnd = -1;
46
- for (let i = 6; i < Math.min(buffer.length, this.MAX_V1_HEADER_LENGTH); i++) {
47
- if (buffer[i] === 0x0D && buffer[i + 1] === 0x0A) { // \r\n
48
- headerEnd = i + 2;
49
- break;
50
- }
51
- }
52
-
53
- if (headerEnd === -1) {
54
- // No complete header found
55
- return null;
56
- }
57
-
58
- // Parse the header line
59
- const headerLine = buffer.toString('ascii', 0, headerEnd - 2);
60
- const parts = headerLine.split(' ');
61
-
62
- if (parts.length !== 6) {
63
- logger.log('warn', 'Invalid PROXY v1 header format', {
64
- headerLine,
65
- partCount: parts.length
66
- });
67
- return null;
68
- }
69
-
70
- const [proxy, family, srcIP, dstIP, srcPort, dstPort] = parts;
71
-
72
- // Validate family
73
- if (!['TCP4', 'TCP6', 'UNKNOWN'].includes(family)) {
74
- logger.log('warn', 'Invalid PROXY protocol family', { family });
75
- return null;
76
- }
77
-
78
- // Validate ports
79
- const sourcePort = parseInt(srcPort);
80
- const destPort = parseInt(dstPort);
81
-
82
- if (isNaN(sourcePort) || sourcePort < 1 || sourcePort > 65535 ||
83
- isNaN(destPort) || destPort < 1 || destPort > 65535) {
84
- logger.log('warn', 'Invalid PROXY protocol ports', { srcPort, dstPort });
85
- return null;
86
- }
87
-
88
- return {
89
- version: 1,
90
- command: 'PROXY',
91
- family: family as 'TCP4' | 'TCP6' | 'UNKNOWN',
92
- sourceIP: srcIP,
93
- destIP: dstIP,
94
- sourcePort,
95
- destPort,
96
- headerLength: headerEnd
97
- };
98
- }
99
-
100
- /**
101
- * Check if buffer potentially contains PROXY protocol
102
- */
103
- static mightBeProxyProtocol(buffer: Buffer): boolean {
104
- if (buffer.length < 6) return false;
105
-
106
- // Check for v1 signature
107
- const start = buffer.toString('ascii', 0, 6);
108
- if (start === this.PROXY_V1_SIGNATURE) return true;
109
-
110
- // Check for v2 signature (12 bytes: \x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A)
111
- if (buffer.length >= 12) {
112
- const v2Sig = Buffer.from([0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A]);
113
- if (buffer.compare(v2Sig, 0, 12, 0, 12) === 0) return true;
114
- }
115
-
116
- return false;
117
- }
118
- }
119
- ```
120
-
121
- ### Integration with RouteConnectionHandler
122
-
123
- ```typescript
124
- // This shows how it would be integrated into route-connection-handler.ts
125
-
126
- private async handleProxyProtocol(
127
- socket: plugins.net.Socket,
128
- wrappedSocket: WrappedSocket,
129
- record: IConnectionRecord
130
- ): Promise<Buffer | null> {
131
- const remoteIP = socket.remoteAddress || '';
132
-
133
- // Only parse PROXY protocol from trusted IPs
134
- if (!this.settings.proxyIPs?.includes(remoteIP)) {
135
- return null;
136
- }
137
-
138
- return new Promise((resolve) => {
139
- let buffer = Buffer.alloc(0);
140
- let headerParsed = false;
141
-
142
- const parseHandler = (chunk: Buffer) => {
143
- // Accumulate data
144
- buffer = Buffer.concat([buffer, chunk]);
145
-
146
- // Try to parse PROXY protocol
147
- const proxyInfo = ProxyProtocolParser.parseV1(buffer);
148
-
149
- if (proxyInfo) {
150
- // Update wrapped socket with real client info
151
- wrappedSocket.setProxyInfo(proxyInfo.sourceIP, proxyInfo.sourcePort);
152
-
153
- // Update connection record
154
- record.remoteIP = proxyInfo.sourceIP;
155
-
156
- logger.log('info', 'PROXY protocol parsed', {
157
- connectionId: record.id,
158
- realIP: proxyInfo.sourceIP,
159
- realPort: proxyInfo.sourcePort,
160
- proxyIP: remoteIP
161
- });
162
-
163
- // Remove this handler
164
- socket.removeListener('data', parseHandler);
165
- headerParsed = true;
166
-
167
- // Return remaining data after header
168
- const remaining = buffer.slice(proxyInfo.headerLength);
169
- resolve(remaining.length > 0 ? remaining : null);
170
- } else if (buffer.length > 108) {
171
- // Max v1 header length exceeded, not PROXY protocol
172
- socket.removeListener('data', parseHandler);
173
- headerParsed = true;
174
- resolve(buffer);
175
- }
176
- };
177
-
178
- // Set timeout for PROXY protocol parsing
179
- const timeout = setTimeout(() => {
180
- if (!headerParsed) {
181
- socket.removeListener('data', parseHandler);
182
- logger.log('warn', 'PROXY protocol parsing timeout', {
183
- connectionId: record.id,
184
- bufferLength: buffer.length
185
- });
186
- resolve(buffer.length > 0 ? buffer : null);
187
- }
188
- }, 1000); // 1 second timeout
189
-
190
- socket.on('data', parseHandler);
191
-
192
- // Clean up on early close
193
- socket.once('close', () => {
194
- clearTimeout(timeout);
195
- if (!headerParsed) {
196
- socket.removeListener('data', parseHandler);
197
- resolve(null);
198
- }
199
- });
200
- });
201
- }
202
-
203
- // Modified handleConnection to include PROXY protocol parsing
204
- public async handleConnection(socket: plugins.net.Socket): void {
205
- const remoteIP = socket.remoteAddress || '';
206
- const localPort = socket.localPort || 0;
207
-
208
- // Always wrap the socket
209
- const wrappedSocket = new WrappedSocket(socket);
210
-
211
- // Create connection record
212
- const record = this.connectionManager.createConnection(wrappedSocket);
213
- if (!record) return;
214
-
215
- // If from trusted proxy, parse PROXY protocol
216
- if (this.settings.proxyIPs?.includes(remoteIP)) {
217
- const remainingData = await this.handleProxyProtocol(socket, wrappedSocket, record);
218
-
219
- if (remainingData) {
220
- // Process remaining data as normal
221
- this.handleInitialData(wrappedSocket, record, remainingData);
222
- } else {
223
- // Wait for more data
224
- this.handleInitialData(wrappedSocket, record);
225
- }
226
- } else {
227
- // Not from trusted proxy, handle normally
228
- this.handleInitialData(wrappedSocket, record);
229
- }
230
- }
231
- ```
232
-
233
- ### Sending PROXY Protocol When Forwarding
234
-
235
- ```typescript
236
- // This would be added to setupDirectConnection method
237
-
238
- private setupDirectConnection(
239
- socket: plugins.net.Socket | WrappedSocket,
240
- record: IConnectionRecord,
241
- serverName?: string,
242
- initialChunk?: Buffer,
243
- overridePort?: number,
244
- targetHost?: string,
245
- targetPort?: number
246
- ): void {
247
- // ... existing code ...
248
-
249
- // Create target socket
250
- const targetSocket = createSocketWithErrorHandler({
251
- port: finalTargetPort,
252
- host: finalTargetHost,
253
- onConnect: () => {
254
- // If sendProxyProtocol is enabled, send PROXY header first
255
- if (this.settings.sendProxyProtocol) {
256
- const proxyHeader = this.buildProxyProtocolHeader(wrappedSocket, targetSocket);
257
- targetSocket.write(proxyHeader);
258
- }
259
-
260
- // Then send any pending data
261
- if (record.pendingData.length > 0) {
262
- const combinedData = Buffer.concat(record.pendingData);
263
- targetSocket.write(combinedData);
264
- }
265
-
266
- // ... rest of connection setup ...
267
- }
268
- });
269
- }
270
-
271
- private buildProxyProtocolHeader(
272
- clientSocket: WrappedSocket,
273
- serverSocket: net.Socket
274
- ): Buffer {
275
- const family = clientSocket.remoteFamily === 'IPv6' ? 'TCP6' : 'TCP4';
276
- const srcIP = clientSocket.remoteAddress || '0.0.0.0';
277
- const srcPort = clientSocket.remotePort || 0;
278
- const dstIP = serverSocket.localAddress || '0.0.0.0';
279
- const dstPort = serverSocket.localPort || 0;
280
-
281
- const header = `PROXY ${family} ${srcIP} ${dstIP} ${srcPort} ${dstPort}\r\n`;
282
- return Buffer.from(header, 'ascii');
283
- }
284
- ```
285
-
286
- ## Complete Example: HAProxy Compatible Setup
287
-
288
- ```typescript
289
- // Example showing a complete HAProxy-compatible SmartProxy setup
290
-
291
- import { SmartProxy } from '@push.rocks/smartproxy';
292
-
293
- // Configuration matching HAProxy's proxy protocol behavior
294
- const proxy = new SmartProxy({
295
- // Accept PROXY protocol from these sources (like HAProxy's 'accept-proxy')
296
- proxyIPs: [
297
- '10.0.0.0/8', // Private network load balancers
298
- '172.16.0.0/12', // Docker networks
299
- '192.168.0.0/16' // Local networks
300
- ],
301
-
302
- // Send PROXY protocol to backends (like HAProxy's 'send-proxy')
303
- sendProxyProtocol: true,
304
-
305
- routes: [
306
- {
307
- name: 'web-app',
308
- match: {
309
- ports: 443,
310
- domains: ['app.example.com', 'www.example.com']
311
- },
312
- action: {
313
- type: 'forward',
314
- target: {
315
- host: 'backend-pool.internal',
316
- port: 8080
317
- },
318
- tls: {
319
- mode: 'terminate',
320
- certificate: 'auto',
321
- acme: {
322
- email: 'ssl@example.com'
323
- }
324
- }
325
- }
326
- }
327
- ]
328
- });
329
-
330
- // Start the proxy
331
- await proxy.start();
332
-
333
- // The proxy will now:
334
- // 1. Accept connections on port 443
335
- // 2. Parse PROXY protocol from trusted IPs
336
- // 3. Terminate TLS
337
- // 4. Forward to backend with PROXY protocol header
338
- // 5. Backend sees real client IP
339
- ```
340
-
341
- ## Testing PROXY Protocol
342
-
343
- ```typescript
344
- // Test client that sends PROXY protocol
345
- import * as net from 'net';
346
-
347
- function createProxyProtocolClient(
348
- realClientIP: string,
349
- realClientPort: number,
350
- proxyHost: string,
351
- proxyPort: number
352
- ): net.Socket {
353
- const client = net.connect(proxyPort, proxyHost);
354
-
355
- client.on('connect', () => {
356
- // Send PROXY protocol header
357
- const header = `PROXY TCP4 ${realClientIP} ${proxyHost} ${realClientPort} ${proxyPort}\r\n`;
358
- client.write(header);
359
-
360
- // Then send actual request
361
- client.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n');
362
- });
363
-
364
- return client;
365
- }
366
-
367
- // Usage
368
- const client = createProxyProtocolClient(
369
- '203.0.113.45', // Real client IP
370
- 54321, // Real client port
371
- 'localhost', // Proxy host
372
- 8080 // Proxy port
373
- );
374
- ```
375
-
376
- ## AWS Network Load Balancer Example
377
-
378
- ```typescript
379
- // Configuration for AWS NLB with PROXY protocol v2
380
- const proxy = new SmartProxy({
381
- // AWS NLB IP ranges (get current list from AWS)
382
- proxyIPs: [
383
- '10.0.0.0/8', // VPC CIDR
384
- // Add specific NLB IPs or use AWS IP ranges
385
- ],
386
-
387
- // AWS NLB uses PROXY protocol v2 by default
388
- acceptProxyProtocolV2: true, // Future feature
389
-
390
- routes: [{
391
- name: 'aws-app',
392
- match: { ports: 443 },
393
- action: {
394
- type: 'forward',
395
- target: {
396
- host: 'app-cluster.internal',
397
- port: 8443
398
- },
399
- tls: { mode: 'passthrough' }
400
- }
401
- }]
402
- });
403
-
404
- // The proxy will:
405
- // 1. Accept PROXY protocol v2 from AWS NLB
406
- // 2. Preserve VPC endpoint IDs and other metadata
407
- // 3. Forward to backend with real client information
408
- ```
409
-
410
- ## Debugging PROXY Protocol
411
-
412
- ```typescript
413
- // Enable detailed logging to debug PROXY protocol parsing
414
- const proxy = new SmartProxy({
415
- enableDetailedLogging: true,
416
- proxyIPs: ['10.0.0.1'],
417
-
418
- // Add custom logging for debugging
419
- routes: [{
420
- name: 'debug-route',
421
- match: { ports: 8080 },
422
- action: {
423
- type: 'socket-handler',
424
- socketHandler: async (socket, context) => {
425
- console.log('Socket handler called with context:', {
426
- clientIp: context.clientIp, // Real IP from PROXY protocol
427
- port: context.port,
428
- connectionId: context.connectionId,
429
- timestamp: context.timestamp
430
- });
431
-
432
- // Handle the socket...
433
- }
434
- }
435
- }]
436
- });
437
- ```
438
-
439
- ## Security Considerations
440
-
441
- 1. **Always validate trusted proxy IPs** - Never accept PROXY protocol from untrusted sources
442
- 2. **Use specific IP ranges** - Avoid wildcards like `0.0.0.0/0`
443
- 3. **Implement rate limiting** - PROXY protocol parsing has a computational cost
444
- 4. **Validate header format** - Reject malformed headers immediately
445
- 5. **Set parsing timeouts** - Prevent slow loris attacks via PROXY headers
446
- 6. **Log parsing failures** - Monitor for potential attacks or misconfigurations
447
-
448
- ## Performance Considerations
449
-
450
- 1. **Header parsing overhead** - Minimal, one-time cost per connection
451
- 2. **Memory usage** - Small buffer for header accumulation (max 108 bytes for v1)
452
- 3. **Connection establishment** - Slight delay for PROXY protocol parsing
453
- 4. **Throughput impact** - None after initial header parsing
454
- 5. **CPU usage** - Negligible for well-formed headers
455
-
456
- ## Future Enhancements
457
-
458
- 1. **PROXY Protocol v2** - Binary format for better performance
459
- 2. **TLS information preservation** - Pass TLS version, cipher, SNI via PP2
460
- 3. **Custom type-length-value (TLV) fields** - Extended metadata support
461
- 4. **Connection pooling** - Reuse backend connections with different client IPs
462
- 5. **Health checks** - Skip PROXY protocol for health check connections