@push.rocks/smartproxy 19.5.20 → 19.5.21

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.
@@ -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
@@ -15,3 +15,4 @@ export * from './lifecycle-component.js';
15
15
  export * from './binary-heap.js';
16
16
  export * from './enhanced-connection-pool.js';
17
17
  export * from './socket-utils.js';
18
+ export * from './proxy-protocol.js';
@@ -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
@@ -250,6 +250,9 @@ export interface IRouteAction {
250
250
 
251
251
  // Socket handler function (when type is 'socket-handler')
252
252
  socketHandler?: TSocketHandler;
253
+
254
+ // PROXY protocol support
255
+ sendProxyProtocol?: boolean;
253
256
  }
254
257
 
255
258
  /**