@push.rocks/smartproxy 19.5.20 → 19.5.22
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/core/utils/index.d.ts +1 -0
- package/dist_ts/core/utils/index.js +2 -1
- package/dist_ts/core/utils/proxy-protocol.d.ts +45 -0
- package/dist_ts/core/utils/proxy-protocol.js +201 -0
- package/dist_ts/proxies/smart-proxy/connection-manager.js +3 -1
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +1 -0
- package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +1 -0
- package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +125 -24
- package/package.json +1 -1
- package/readme.hints.md +8 -1
- package/readme.plan.md +23 -19
- package/readme.proxy-chain-summary.md +112 -0
- package/readme.proxy-protocol-example.md +462 -0
- package/readme.proxy-protocol.md +415 -0
- package/ts/core/utils/index.ts +1 -0
- package/ts/core/utils/proxy-protocol.ts +246 -0
- package/ts/proxies/smart-proxy/connection-manager.ts +2 -0
- package/ts/proxies/smart-proxy/models/interfaces.ts +1 -0
- package/ts/proxies/smart-proxy/models/route-types.ts +3 -0
- package/ts/proxies/smart-proxy/route-connection-handler.ts +134 -27
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# SmartProxy PROXY Protocol and Proxy Chaining Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
SmartProxy implements support for the PROXY protocol v1 to enable proxy chaining and preserve real client IP addresses across multiple proxy layers. This documentation covers the implementation details, configuration, and usage patterns for proxy chaining scenarios.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
### WrappedSocket Implementation
|
|
10
|
+
|
|
11
|
+
The foundation of PROXY protocol support is the `WrappedSocket` class, which wraps regular `net.Socket` instances to provide transparent access to real client information when behind a proxy.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// ts/core/models/wrapped-socket.ts
|
|
15
|
+
export class WrappedSocket {
|
|
16
|
+
public readonly socket: plugins.net.Socket;
|
|
17
|
+
private realClientIP?: string;
|
|
18
|
+
private realClientPort?: number;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
socket: plugins.net.Socket,
|
|
22
|
+
realClientIP?: string,
|
|
23
|
+
realClientPort?: number
|
|
24
|
+
) {
|
|
25
|
+
this.socket = socket;
|
|
26
|
+
this.realClientIP = realClientIP;
|
|
27
|
+
this.realClientPort = realClientPort;
|
|
28
|
+
|
|
29
|
+
// Uses JavaScript Proxy to delegate all methods to underlying socket
|
|
30
|
+
return new Proxy(this, {
|
|
31
|
+
get(target, prop, receiver) {
|
|
32
|
+
// Override specific properties
|
|
33
|
+
if (prop === 'remoteAddress') {
|
|
34
|
+
return target.remoteAddress;
|
|
35
|
+
}
|
|
36
|
+
if (prop === 'remotePort') {
|
|
37
|
+
return target.remotePort;
|
|
38
|
+
}
|
|
39
|
+
// ... delegate other properties to underlying socket
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get remoteAddress(): string | undefined {
|
|
45
|
+
return this.realClientIP || this.socket.remoteAddress;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get remotePort(): number | undefined {
|
|
49
|
+
return this.realClientPort || this.socket.remotePort;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get isFromTrustedProxy(): boolean {
|
|
53
|
+
return !!this.realClientIP;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Key Design Decisions
|
|
59
|
+
|
|
60
|
+
1. **All sockets are wrapped** - Every incoming connection is wrapped in a WrappedSocket, not just those from trusted proxies
|
|
61
|
+
2. **Proxy pattern for delegation** - Uses JavaScript Proxy to transparently delegate all Socket methods while allowing property overrides
|
|
62
|
+
3. **Not a Duplex stream** - Simple wrapper approach avoids complexity and infinite loops
|
|
63
|
+
4. **Trust-based parsing** - PROXY protocol parsing only occurs for connections from trusted proxy IPs
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
### Basic PROXY Protocol Configuration
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const proxy = new SmartProxy({
|
|
71
|
+
// List of trusted proxy IPs that can send PROXY protocol
|
|
72
|
+
proxyIPs: ['10.0.0.1', '10.0.0.2', '192.168.1.0/24'],
|
|
73
|
+
|
|
74
|
+
// Global option to accept PROXY protocol (defaults based on proxyIPs)
|
|
75
|
+
acceptProxyProtocol: true,
|
|
76
|
+
|
|
77
|
+
// Global option to send PROXY protocol to all targets
|
|
78
|
+
sendProxyProtocol: false,
|
|
79
|
+
|
|
80
|
+
routes: [
|
|
81
|
+
{
|
|
82
|
+
name: 'backend-app',
|
|
83
|
+
match: { ports: 443, domains: 'app.example.com' },
|
|
84
|
+
action: {
|
|
85
|
+
type: 'forward',
|
|
86
|
+
target: { host: 'backend.internal', port: 8443 },
|
|
87
|
+
tls: { mode: 'passthrough' }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Proxy Chain Configuration
|
|
95
|
+
|
|
96
|
+
Setting up two SmartProxies in a chain:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Outer Proxy (Internet-facing)
|
|
100
|
+
const outerProxy = new SmartProxy({
|
|
101
|
+
proxyIPs: [], // No trusted proxies for outer proxy
|
|
102
|
+
sendProxyProtocol: true, // Send PROXY protocol to inner proxy
|
|
103
|
+
|
|
104
|
+
routes: [{
|
|
105
|
+
name: 'to-inner-proxy',
|
|
106
|
+
match: { ports: 443 },
|
|
107
|
+
action: {
|
|
108
|
+
type: 'forward',
|
|
109
|
+
target: {
|
|
110
|
+
host: 'inner-proxy.internal',
|
|
111
|
+
port: 443
|
|
112
|
+
},
|
|
113
|
+
tls: { mode: 'passthrough' }
|
|
114
|
+
}
|
|
115
|
+
}]
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Inner Proxy (Backend-facing)
|
|
119
|
+
const innerProxy = new SmartProxy({
|
|
120
|
+
proxyIPs: ['outer-proxy.internal'], // Trust the outer proxy
|
|
121
|
+
acceptProxyProtocol: true,
|
|
122
|
+
|
|
123
|
+
routes: [{
|
|
124
|
+
name: 'to-backend',
|
|
125
|
+
match: { ports: 443, domains: 'app.example.com' },
|
|
126
|
+
action: {
|
|
127
|
+
type: 'forward',
|
|
128
|
+
target: {
|
|
129
|
+
host: 'backend.internal',
|
|
130
|
+
port: 8080
|
|
131
|
+
},
|
|
132
|
+
tls: {
|
|
133
|
+
mode: 'terminate',
|
|
134
|
+
certificate: 'auto'
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}]
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## How Two SmartProxies Communicate
|
|
142
|
+
|
|
143
|
+
### Connection Flow
|
|
144
|
+
|
|
145
|
+
1. **Client connects to Outer Proxy**
|
|
146
|
+
```
|
|
147
|
+
Client (203.0.113.45:54321) → Outer Proxy (1.2.3.4:443)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
2. **Outer Proxy wraps the socket**
|
|
151
|
+
```typescript
|
|
152
|
+
// In RouteConnectionHandler.handleConnection()
|
|
153
|
+
const wrappedSocket = new WrappedSocket(socket);
|
|
154
|
+
// At this point:
|
|
155
|
+
// wrappedSocket.remoteAddress = '203.0.113.45'
|
|
156
|
+
// wrappedSocket.remotePort = 54321
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
3. **Outer Proxy forwards to Inner Proxy**
|
|
160
|
+
- Creates new connection to inner proxy
|
|
161
|
+
- If `sendProxyProtocol` is enabled, prepends PROXY protocol header:
|
|
162
|
+
```
|
|
163
|
+
PROXY TCP4 203.0.113.45 1.2.3.4 54321 443\r\n
|
|
164
|
+
[Original TLS/HTTP data follows]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
4. **Inner Proxy receives connection**
|
|
168
|
+
- Sees connection from outer proxy IP
|
|
169
|
+
- Checks if IP is in `proxyIPs` list
|
|
170
|
+
- If trusted, parses PROXY protocol header
|
|
171
|
+
- Updates WrappedSocket with real client info:
|
|
172
|
+
```typescript
|
|
173
|
+
wrappedSocket.setProxyInfo('203.0.113.45', 54321);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
5. **Inner Proxy routes based on real client IP**
|
|
177
|
+
- Security checks use real client IP
|
|
178
|
+
- Connection records track real client IP
|
|
179
|
+
- Backend sees requests from the original client IP
|
|
180
|
+
|
|
181
|
+
### Connection Record Tracking
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// In ConnectionManager
|
|
185
|
+
interface IConnectionRecord {
|
|
186
|
+
id: string;
|
|
187
|
+
incoming: WrappedSocket; // Wrapped socket with real client info
|
|
188
|
+
outgoing: net.Socket | null;
|
|
189
|
+
remoteIP: string; // Real client IP from PROXY protocol or direct connection
|
|
190
|
+
localPort: number;
|
|
191
|
+
// ... other fields
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Implementation Details
|
|
196
|
+
|
|
197
|
+
### Socket Wrapping in Route Handler
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// ts/proxies/smart-proxy/route-connection-handler.ts
|
|
201
|
+
public handleConnection(socket: plugins.net.Socket): void {
|
|
202
|
+
const remoteIP = socket.remoteAddress || '';
|
|
203
|
+
|
|
204
|
+
// Always wrap the socket to prepare for potential PROXY protocol
|
|
205
|
+
const wrappedSocket = new WrappedSocket(socket);
|
|
206
|
+
|
|
207
|
+
// If this is from a trusted proxy, log it
|
|
208
|
+
if (this.settings.proxyIPs?.includes(remoteIP)) {
|
|
209
|
+
logger.log('debug', `Connection from trusted proxy ${remoteIP}, PROXY protocol parsing will be enabled`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Create connection record with wrapped socket
|
|
213
|
+
const record = this.connectionManager.createConnection(wrappedSocket);
|
|
214
|
+
|
|
215
|
+
// Continue with normal connection handling...
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Socket Utility Integration
|
|
220
|
+
|
|
221
|
+
When passing wrapped sockets to socket utility functions, the underlying socket must be extracted:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import { getUnderlyingSocket } from '../../core/models/socket-types.js';
|
|
225
|
+
|
|
226
|
+
// In setupDirectConnection()
|
|
227
|
+
const incomingSocket = getUnderlyingSocket(socket); // Extract raw socket
|
|
228
|
+
|
|
229
|
+
setupBidirectionalForwarding(incomingSocket, targetSocket, {
|
|
230
|
+
onClientData: (chunk) => {
|
|
231
|
+
record.bytesReceived += chunk.length;
|
|
232
|
+
},
|
|
233
|
+
onServerData: (chunk) => {
|
|
234
|
+
record.bytesSent += chunk.length;
|
|
235
|
+
},
|
|
236
|
+
onCleanup: (reason) => {
|
|
237
|
+
this.connectionManager.cleanupConnection(record, reason);
|
|
238
|
+
},
|
|
239
|
+
enableHalfOpen: false // Required for proxy chains
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Current Status and Limitations
|
|
244
|
+
|
|
245
|
+
### Implemented (v19.5.19+)
|
|
246
|
+
- ✅ WrappedSocket foundation class
|
|
247
|
+
- ✅ Socket wrapping in connection handler
|
|
248
|
+
- ✅ Connection manager support for wrapped sockets
|
|
249
|
+
- ✅ Socket utility integration helpers
|
|
250
|
+
- ✅ Proxy IP configuration options
|
|
251
|
+
|
|
252
|
+
### Not Yet Implemented
|
|
253
|
+
- ❌ PROXY protocol v1 header parsing
|
|
254
|
+
- ❌ PROXY protocol v2 binary format support
|
|
255
|
+
- ❌ Automatic PROXY protocol header generation when forwarding
|
|
256
|
+
- ❌ HAProxy compatibility testing
|
|
257
|
+
- ❌ AWS ELB/NLB compatibility testing
|
|
258
|
+
|
|
259
|
+
### Known Issues
|
|
260
|
+
1. **No actual PROXY protocol parsing** - The infrastructure is in place but the protocol parsing is not yet implemented
|
|
261
|
+
2. **Manual configuration required** - No automatic detection of PROXY protocol support
|
|
262
|
+
3. **Limited to TCP connections** - WebSocket connections through proxy chains may not preserve client IPs
|
|
263
|
+
|
|
264
|
+
## Testing Proxy Chains
|
|
265
|
+
|
|
266
|
+
### Basic Proxy Chain Test
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// test/test.proxy-chain-simple.node.ts
|
|
270
|
+
tap.test('simple proxy chain test', async () => {
|
|
271
|
+
// Create backend server
|
|
272
|
+
const backend = net.createServer((socket) => {
|
|
273
|
+
console.log('Backend: Connection received');
|
|
274
|
+
socket.write('HTTP/1.1 200 OK\r\n\r\nHello from backend');
|
|
275
|
+
socket.end();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Create inner proxy (downstream)
|
|
279
|
+
const innerProxy = new SmartProxy({
|
|
280
|
+
proxyIPs: ['127.0.0.1'], // Trust localhost for testing
|
|
281
|
+
routes: [{
|
|
282
|
+
name: 'to-backend',
|
|
283
|
+
match: { ports: 8591 },
|
|
284
|
+
action: {
|
|
285
|
+
type: 'forward',
|
|
286
|
+
target: { host: 'localhost', port: 9999 }
|
|
287
|
+
}
|
|
288
|
+
}]
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Create outer proxy (upstream)
|
|
292
|
+
const outerProxy = new SmartProxy({
|
|
293
|
+
sendProxyProtocol: true, // Send PROXY to inner
|
|
294
|
+
routes: [{
|
|
295
|
+
name: 'to-inner',
|
|
296
|
+
match: { ports: 8590 },
|
|
297
|
+
action: {
|
|
298
|
+
type: 'forward',
|
|
299
|
+
target: { host: 'localhost', port: 8591 }
|
|
300
|
+
}
|
|
301
|
+
}]
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Test connection through chain
|
|
305
|
+
const client = net.connect(8590, 'localhost');
|
|
306
|
+
client.write('GET / HTTP/1.1\r\nHost: test.com\r\n\r\n');
|
|
307
|
+
|
|
308
|
+
// Verify no connection accumulation
|
|
309
|
+
const counts = getConnectionCounts();
|
|
310
|
+
expect(counts.proxy1).toEqual(0);
|
|
311
|
+
expect(counts.proxy2).toEqual(0);
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Best Practices
|
|
316
|
+
|
|
317
|
+
### 1. Always Configure Trusted Proxies
|
|
318
|
+
```typescript
|
|
319
|
+
// Be specific about which IPs can send PROXY protocol
|
|
320
|
+
proxyIPs: ['10.0.0.1', '10.0.0.2'], // Good
|
|
321
|
+
proxyIPs: ['0.0.0.0/0'], // Bad - trusts everyone
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### 2. Use CIDR Notation for Subnets
|
|
325
|
+
```typescript
|
|
326
|
+
proxyIPs: [
|
|
327
|
+
'10.0.0.0/24', // Trust entire subnet
|
|
328
|
+
'192.168.1.5', // Trust specific IP
|
|
329
|
+
'172.16.0.0/16' // Trust private network
|
|
330
|
+
]
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### 3. Enable Half-Open Only When Needed
|
|
334
|
+
```typescript
|
|
335
|
+
// For proxy chains, always disable half-open
|
|
336
|
+
setupBidirectionalForwarding(client, server, {
|
|
337
|
+
enableHalfOpen: false // Ensures proper cascade cleanup
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### 4. Monitor Connection Counts
|
|
342
|
+
```typescript
|
|
343
|
+
// Regular monitoring prevents connection leaks
|
|
344
|
+
setInterval(() => {
|
|
345
|
+
const stats = proxy.getStatistics();
|
|
346
|
+
console.log(`Active connections: ${stats.activeConnections}`);
|
|
347
|
+
if (stats.activeConnections > 1000) {
|
|
348
|
+
console.warn('High connection count detected');
|
|
349
|
+
}
|
|
350
|
+
}, 60000);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Future Enhancements
|
|
354
|
+
|
|
355
|
+
### Phase 2: PROXY Protocol v1 Parser
|
|
356
|
+
```typescript
|
|
357
|
+
// Planned implementation
|
|
358
|
+
class ProxyProtocolParser {
|
|
359
|
+
static parse(buffer: Buffer): ProxyInfo | null {
|
|
360
|
+
// Parse "PROXY TCP4 <src-ip> <dst-ip> <src-port> <dst-port>\r\n"
|
|
361
|
+
const header = buffer.toString('ascii', 0, 108);
|
|
362
|
+
const match = header.match(/^PROXY (TCP4|TCP6) (\S+) (\S+) (\d+) (\d+)\r\n/);
|
|
363
|
+
if (match) {
|
|
364
|
+
return {
|
|
365
|
+
protocol: match[1],
|
|
366
|
+
sourceIP: match[2],
|
|
367
|
+
destIP: match[3],
|
|
368
|
+
sourcePort: parseInt(match[4]),
|
|
369
|
+
destPort: parseInt(match[5]),
|
|
370
|
+
headerLength: match[0].length
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Phase 3: Automatic PROXY Protocol Detection
|
|
379
|
+
- Peek at first bytes to detect PROXY protocol signature
|
|
380
|
+
- Automatic fallback to direct connection if not present
|
|
381
|
+
- Configurable timeout for protocol detection
|
|
382
|
+
|
|
383
|
+
### Phase 4: PROXY Protocol v2 Support
|
|
384
|
+
- Binary protocol format for better performance
|
|
385
|
+
- Additional metadata support (TLS info, ALPN, etc.)
|
|
386
|
+
- AWS VPC endpoint ID preservation
|
|
387
|
+
|
|
388
|
+
## Troubleshooting
|
|
389
|
+
|
|
390
|
+
### Connection Accumulation in Proxy Chains
|
|
391
|
+
If connections accumulate when chaining proxies:
|
|
392
|
+
1. Verify `enableHalfOpen: false` in socket forwarding
|
|
393
|
+
2. Check that both proxies have proper cleanup handlers
|
|
394
|
+
3. Monitor with connection count logging
|
|
395
|
+
4. Use `test.proxy-chain-simple.node.ts` as reference
|
|
396
|
+
|
|
397
|
+
### Real Client IP Not Preserved
|
|
398
|
+
If the backend sees proxy IP instead of client IP:
|
|
399
|
+
1. Verify outer proxy has `sendProxyProtocol: true`
|
|
400
|
+
2. Verify inner proxy has outer proxy IP in `proxyIPs` list
|
|
401
|
+
3. Check logs for "Connection from trusted proxy" message
|
|
402
|
+
4. Ensure PROXY protocol parsing is implemented (currently pending)
|
|
403
|
+
|
|
404
|
+
### Performance Impact
|
|
405
|
+
PROXY protocol adds minimal overhead:
|
|
406
|
+
- One-time parsing cost per connection
|
|
407
|
+
- Small memory overhead for real client info storage
|
|
408
|
+
- No impact on data transfer performance
|
|
409
|
+
- Negligible CPU impact for header generation
|
|
410
|
+
|
|
411
|
+
## Related Documentation
|
|
412
|
+
- [Socket Utilities](./ts/core/utils/socket-utils.ts) - Low-level socket handling
|
|
413
|
+
- [Connection Manager](./ts/proxies/smart-proxy/connection-manager.ts) - Connection lifecycle
|
|
414
|
+
- [Route Handler](./ts/proxies/smart-proxy/route-connection-handler.ts) - Request routing
|
|
415
|
+
- [Test Suite](./test/test.wrapped-socket.ts) - WrappedSocket unit tests
|
package/ts/core/utils/index.ts
CHANGED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interface representing parsed PROXY protocol information
|
|
6
|
+
*/
|
|
7
|
+
export interface IProxyInfo {
|
|
8
|
+
protocol: 'TCP4' | 'TCP6' | 'UNKNOWN';
|
|
9
|
+
sourceIP: string;
|
|
10
|
+
sourcePort: number;
|
|
11
|
+
destinationIP: string;
|
|
12
|
+
destinationPort: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Interface for parse result including remaining data
|
|
17
|
+
*/
|
|
18
|
+
export interface IProxyParseResult {
|
|
19
|
+
proxyInfo: IProxyInfo | null;
|
|
20
|
+
remainingData: Buffer;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parser for PROXY protocol v1 (text format)
|
|
25
|
+
* Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
|
26
|
+
*/
|
|
27
|
+
export class ProxyProtocolParser {
|
|
28
|
+
static readonly PROXY_V1_SIGNATURE = 'PROXY ';
|
|
29
|
+
static readonly MAX_HEADER_LENGTH = 107; // Max length for v1 header
|
|
30
|
+
static readonly HEADER_TERMINATOR = '\r\n';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse PROXY protocol v1 header from buffer
|
|
34
|
+
* Returns proxy info and remaining data after header
|
|
35
|
+
*/
|
|
36
|
+
static parse(data: Buffer): IProxyParseResult {
|
|
37
|
+
// Check if buffer starts with PROXY signature
|
|
38
|
+
if (!data.toString('ascii', 0, 6).startsWith(this.PROXY_V1_SIGNATURE)) {
|
|
39
|
+
return {
|
|
40
|
+
proxyInfo: null,
|
|
41
|
+
remainingData: data
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Find header terminator
|
|
46
|
+
const headerEndIndex = data.indexOf(this.HEADER_TERMINATOR);
|
|
47
|
+
if (headerEndIndex === -1) {
|
|
48
|
+
// Header incomplete, need more data
|
|
49
|
+
if (data.length > this.MAX_HEADER_LENGTH) {
|
|
50
|
+
// Header too long, invalid
|
|
51
|
+
throw new Error('PROXY protocol header exceeds maximum length');
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
proxyInfo: null,
|
|
55
|
+
remainingData: data
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Extract header line
|
|
60
|
+
const headerLine = data.toString('ascii', 0, headerEndIndex);
|
|
61
|
+
const remainingData = data.slice(headerEndIndex + 2); // Skip \r\n
|
|
62
|
+
|
|
63
|
+
// Parse header
|
|
64
|
+
const parts = headerLine.split(' ');
|
|
65
|
+
|
|
66
|
+
if (parts.length < 2) {
|
|
67
|
+
throw new Error(`Invalid PROXY protocol header format: ${headerLine}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const [signature, protocol] = parts;
|
|
71
|
+
|
|
72
|
+
// Validate protocol
|
|
73
|
+
if (!['TCP4', 'TCP6', 'UNKNOWN'].includes(protocol)) {
|
|
74
|
+
throw new Error(`Invalid PROXY protocol: ${protocol}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// For UNKNOWN protocol, ignore addresses
|
|
78
|
+
if (protocol === 'UNKNOWN') {
|
|
79
|
+
return {
|
|
80
|
+
proxyInfo: {
|
|
81
|
+
protocol: 'UNKNOWN',
|
|
82
|
+
sourceIP: '',
|
|
83
|
+
sourcePort: 0,
|
|
84
|
+
destinationIP: '',
|
|
85
|
+
destinationPort: 0
|
|
86
|
+
},
|
|
87
|
+
remainingData
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// For TCP4/TCP6, we need all 6 parts
|
|
92
|
+
if (parts.length !== 6) {
|
|
93
|
+
throw new Error(`Invalid PROXY protocol header format: ${headerLine}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const [, , srcIP, dstIP, srcPort, dstPort] = parts;
|
|
97
|
+
|
|
98
|
+
// Validate and parse ports
|
|
99
|
+
const sourcePort = parseInt(srcPort, 10);
|
|
100
|
+
const destinationPort = parseInt(dstPort, 10);
|
|
101
|
+
|
|
102
|
+
if (isNaN(sourcePort) || sourcePort < 0 || sourcePort > 65535) {
|
|
103
|
+
throw new Error(`Invalid source port: ${srcPort}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (isNaN(destinationPort) || destinationPort < 0 || destinationPort > 65535) {
|
|
107
|
+
throw new Error(`Invalid destination port: ${dstPort}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Validate IP addresses
|
|
111
|
+
const protocolType = protocol as 'TCP4' | 'TCP6' | 'UNKNOWN';
|
|
112
|
+
if (!this.isValidIP(srcIP, protocolType)) {
|
|
113
|
+
throw new Error(`Invalid source IP for ${protocol}: ${srcIP}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!this.isValidIP(dstIP, protocolType)) {
|
|
117
|
+
throw new Error(`Invalid destination IP for ${protocol}: ${dstIP}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
proxyInfo: {
|
|
122
|
+
protocol: protocol as 'TCP4' | 'TCP6',
|
|
123
|
+
sourceIP: srcIP,
|
|
124
|
+
sourcePort,
|
|
125
|
+
destinationIP: dstIP,
|
|
126
|
+
destinationPort
|
|
127
|
+
},
|
|
128
|
+
remainingData
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generate PROXY protocol v1 header
|
|
134
|
+
*/
|
|
135
|
+
static generate(info: IProxyInfo): Buffer {
|
|
136
|
+
if (info.protocol === 'UNKNOWN') {
|
|
137
|
+
return Buffer.from(`PROXY UNKNOWN\r\n`, 'ascii');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const header = `PROXY ${info.protocol} ${info.sourceIP} ${info.destinationIP} ${info.sourcePort} ${info.destinationPort}\r\n`;
|
|
141
|
+
|
|
142
|
+
if (header.length > this.MAX_HEADER_LENGTH) {
|
|
143
|
+
throw new Error('Generated PROXY protocol header exceeds maximum length');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return Buffer.from(header, 'ascii');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validate IP address format
|
|
151
|
+
*/
|
|
152
|
+
private static isValidIP(ip: string, protocol: 'TCP4' | 'TCP6' | 'UNKNOWN'): boolean {
|
|
153
|
+
if (protocol === 'TCP4') {
|
|
154
|
+
return plugins.net.isIPv4(ip);
|
|
155
|
+
} else if (protocol === 'TCP6') {
|
|
156
|
+
return plugins.net.isIPv6(ip);
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Attempt to read a complete PROXY protocol header from a socket
|
|
163
|
+
* Returns null if no PROXY protocol detected or incomplete
|
|
164
|
+
*/
|
|
165
|
+
static async readFromSocket(socket: plugins.net.Socket, timeout: number = 5000): Promise<IProxyParseResult | null> {
|
|
166
|
+
return new Promise((resolve) => {
|
|
167
|
+
let buffer = Buffer.alloc(0);
|
|
168
|
+
let resolved = false;
|
|
169
|
+
|
|
170
|
+
const cleanup = () => {
|
|
171
|
+
socket.removeListener('data', onData);
|
|
172
|
+
socket.removeListener('error', onError);
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const timer = setTimeout(() => {
|
|
177
|
+
if (!resolved) {
|
|
178
|
+
resolved = true;
|
|
179
|
+
cleanup();
|
|
180
|
+
resolve({
|
|
181
|
+
proxyInfo: null,
|
|
182
|
+
remainingData: buffer
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}, timeout);
|
|
186
|
+
|
|
187
|
+
const onData = (chunk: Buffer) => {
|
|
188
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
189
|
+
|
|
190
|
+
// Check if we have enough data
|
|
191
|
+
if (!buffer.toString('ascii', 0, Math.min(6, buffer.length)).startsWith(this.PROXY_V1_SIGNATURE)) {
|
|
192
|
+
// Not PROXY protocol
|
|
193
|
+
resolved = true;
|
|
194
|
+
cleanup();
|
|
195
|
+
resolve({
|
|
196
|
+
proxyInfo: null,
|
|
197
|
+
remainingData: buffer
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Try to parse
|
|
203
|
+
try {
|
|
204
|
+
const result = this.parse(buffer);
|
|
205
|
+
if (result.proxyInfo) {
|
|
206
|
+
// Successfully parsed
|
|
207
|
+
resolved = true;
|
|
208
|
+
cleanup();
|
|
209
|
+
resolve(result);
|
|
210
|
+
} else if (buffer.length > this.MAX_HEADER_LENGTH) {
|
|
211
|
+
// Header too long
|
|
212
|
+
resolved = true;
|
|
213
|
+
cleanup();
|
|
214
|
+
resolve({
|
|
215
|
+
proxyInfo: null,
|
|
216
|
+
remainingData: buffer
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// Otherwise continue reading
|
|
220
|
+
} catch (error) {
|
|
221
|
+
// Parse error
|
|
222
|
+
logger.log('error', `PROXY protocol parse error: ${error.message}`);
|
|
223
|
+
resolved = true;
|
|
224
|
+
cleanup();
|
|
225
|
+
resolve({
|
|
226
|
+
proxyInfo: null,
|
|
227
|
+
remainingData: buffer
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const onError = (error: Error) => {
|
|
233
|
+
logger.log('error', `Socket error while reading PROXY protocol: ${error.message}`);
|
|
234
|
+
resolved = true;
|
|
235
|
+
cleanup();
|
|
236
|
+
resolve({
|
|
237
|
+
proxyInfo: null,
|
|
238
|
+
remainingData: buffer
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
socket.on('data', onData);
|
|
243
|
+
socket.on('error', onError);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -70,6 +70,7 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
70
70
|
|
|
71
71
|
const connectionId = this.generateConnectionId();
|
|
72
72
|
const remoteIP = socket.remoteAddress || '';
|
|
73
|
+
const remotePort = socket.remotePort || 0;
|
|
73
74
|
const localPort = socket.localPort || 0;
|
|
74
75
|
const now = Date.now();
|
|
75
76
|
|
|
@@ -85,6 +86,7 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
85
86
|
bytesReceived: 0,
|
|
86
87
|
bytesSent: 0,
|
|
87
88
|
remoteIP,
|
|
89
|
+
remotePort,
|
|
88
90
|
localPort,
|
|
89
91
|
isTLS: false,
|
|
90
92
|
tlsHandshakeComplete: false,
|
|
@@ -151,6 +151,7 @@ export interface IConnectionRecord {
|
|
|
151
151
|
bytesReceived: number; // Total bytes received
|
|
152
152
|
bytesSent: number; // Total bytes sent
|
|
153
153
|
remoteIP: string; // Remote IP (cached for logging after socket close)
|
|
154
|
+
remotePort: number; // Remote port (cached for logging after socket close)
|
|
154
155
|
localPort: number; // Local port (cached for logging)
|
|
155
156
|
isTLS: boolean; // Whether this connection is a TLS connection
|
|
156
157
|
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
|