fix(smartproxy): Correct NFTables forwarding handling to avoid premature connection termination and add comprehensive tests
This commit is contained in:
		| @@ -1,5 +1,13 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-05-19 - 19.3.5 - fix(smartproxy) | ||||
| Correct NFTables forwarding handling to avoid premature connection termination and add comprehensive tests | ||||
|  | ||||
| - Removed overly aggressive socket closing for routes using NFTables forwarding in route-connection-handler.ts | ||||
| - Now logs NFTables-handled connections for monitoring while letting kernel-level forwarding operate transparently | ||||
| - Added and updated tests for connection forwarding, NFTables integration and port forwarding fixes | ||||
| - Enhanced logging and error handling in NFTables and TLS handling functions | ||||
|  | ||||
| ## 2025-05-19 - 19.3.4 - fix(docs, tests, acme) | ||||
| fix: update changelog, documentation, examples and tests for v19.4.0 release. Adjust global ACME configuration to use ssl@bleu.de and add non-privileged port examples. | ||||
|  | ||||
|   | ||||
							
								
								
									
										21
									
								
								test/helpers/test-cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								test/helpers/test-cert.pem
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| -----BEGIN CERTIFICATE----- | ||||
| MIIDizCCAnOgAwIBAgIUAzpwtk6k5v/7LfY1KR7PreezvsswDQYJKoZIhvcNAQEL | ||||
| BQAwVTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3Qx | ||||
| DTALBgNVBAoMBFRlc3QxGTAXBgNVBAMMEHRlc3QuZXhhbXBsZS5jb20wHhcNMjUw | ||||
| NTE5MTc1MDM0WhcNMjYwNTE5MTc1MDM0WjBVMQswCQYDVQQGEwJVUzENMAsGA1UE | ||||
| CAwEVGVzdDENMAsGA1UEBwwEVGVzdDENMAsGA1UECgwEVGVzdDEZMBcGA1UEAwwQ | ||||
| dGVzdC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB | ||||
| AK9FivUNjXz5q+snqKLCno0i3cYzJ+LTzSf+x+a/G7CA/rtigIvSYEqWC4+/MXPM | ||||
| ifpU/iIRtj7RzoPKH44uJie7mS5kKSHsMnh/qixaxxJph+tVYdNGi9hNvL12T/5n | ||||
| ihXkpMAK8MV6z3Y+ObiaKbCe4w19sLu2IIpff0U0mo6rTKOQwAfGa/N1dtzFaogP | ||||
| f/iO5kcksWUPqZowM3lwXXgy8vg5ZeU7IZk9fRTBfrEJAr9TCQ8ivdluxq59Ax86 | ||||
| 0AMmlbeu/dUMBcujLiTVjzqD3jz/Hr+iHq2y48NiF3j5oE/1qsD04d+QDWAygdmd | ||||
| bQOy0w/W1X0ppnuPhLILQzcCAwEAAaNTMFEwHQYDVR0OBBYEFID88wvDJXrQyTsx | ||||
| s+zl/wwx5BCMMB8GA1UdIwQYMBaAFID88wvDJXrQyTsxs+zl/wwx5BCMMA8GA1Ud | ||||
| EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIRp9bUxAip5s0dx700PPVAd | ||||
| mrS7kDCZ+KFD6UgF/F3ykshh33MfYNLghJCfhcWvUHQgiPKohWcZq1g4oMuDZPFW | ||||
| EHTr2wkX9j6A3KNjgFT5OVkLdjNPYdxMbTvmKbsJPc82C9AFN/Xz97XlZvmE4mKc | ||||
| JCKqTz9hK3JpoayEUrf9g4TJcVwNnl/UnMp2sZX3aId4wD2+jSb40H/5UPFO2stv | ||||
| SvCSdMcq0ZOQ/g/P56xOKV/5RAdIYV+0/3LWNGU/dH0nUfJO9K31e3eR+QZ1Iyn3 | ||||
| iGPcaSKPDptVx+2hxcvhFuRgRjfJ0mu6/hnK5wvhrXrSm43FBgvmlo4MaX0HVss= | ||||
| -----END CERTIFICATE----- | ||||
							
								
								
									
										28
									
								
								test/helpers/test-key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								test/helpers/test-key.pem
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| -----BEGIN PRIVATE KEY----- | ||||
| MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCvRYr1DY18+avr | ||||
| J6iiwp6NIt3GMyfi080n/sfmvxuwgP67YoCL0mBKlguPvzFzzIn6VP4iEbY+0c6D | ||||
| yh+OLiYnu5kuZCkh7DJ4f6osWscSaYfrVWHTRovYTby9dk/+Z4oV5KTACvDFes92 | ||||
| Pjm4mimwnuMNfbC7tiCKX39FNJqOq0yjkMAHxmvzdXbcxWqID3/4juZHJLFlD6ma | ||||
| MDN5cF14MvL4OWXlOyGZPX0UwX6xCQK/UwkPIr3ZbsaufQMfOtADJpW3rv3VDAXL | ||||
| oy4k1Y86g948/x6/oh6tsuPDYhd4+aBP9arA9OHfkA1gMoHZnW0DstMP1tV9KaZ7 | ||||
| j4SyC0M3AgMBAAECggEAKfW6ng74C+7TtxDAAPMZtQ0fTcdKabWt/EC1B6tBzEAd | ||||
| e6vJvW+IaOLB8tBhXOkfMSRu0KYv3Jsq1wcpBcdLkCCLu/zzkfDzZkCd809qMCC+ | ||||
| jtraeBOAADEgGbV80hlkh/g8btNPr99GUnb0J5sUlvl6vuyTxmSEJsxU8jL1O2km | ||||
| YgK34fS5NS73h138P3UQAGC0dGK8Rt61EsFIKWTyH/r8tlz9nQrYcDG3LwTbFQQf | ||||
| bsRLAjolxTRV6t1CzcjsSGtrAqm/4QNypP5McCyOXAqajb3pNGaJyGg1nAEOZclK | ||||
| oagU7PPwaFmSquwo7Y1Uov72XuLJLVryBl0fOCen7QKBgQDieqvaL9gHsfaZKNoY | ||||
| +0Cnul/Dw0kjuqJIKhar/mfLY7NwYmFSgH17r26g+X7mzuzaN0rnEhjh7L3j6xQJ | ||||
| qhs9zL+/OIa581Ptvb8H/42O+mxnqx7Z8s5JwH0+f5EriNkU3euoAe/W9x4DqJiE | ||||
| 2VyvlM1gngxI+vFo+iewmg+vOwKBgQDGHiPKxXWD50tXvvDdRTjH+/4GQuXhEQjl | ||||
| Po59AJ/PLc/AkQkVSzr8Fspf7MHN6vufr3tS45tBuf5Qf2Y9GPBRKR3e+M1CJdoi | ||||
| 1RXy0nMsnR0KujxgiIe6WQFumcT81AsIVXtDYk11Sa057tYPeeOmgtmUMJZb6lek | ||||
| wqUxrFw0NQKBgQCs/p7+jsUpO5rt6vKNWn5MoGQ+GJFppUoIbX3b6vxFs+aA1eUZ | ||||
| K+St8ZdDhtCUZUMufEXOs1gmWrvBuPMZXsJoNlnRKtBegat+Ug31ghMTP95GYcOz | ||||
| H3DLjSkd8DtnUaTf95PmRXR6c1CN4t59u7q8s6EdSByCMozsbwiaMVQBuQKBgQCY | ||||
| QxG/BYMLnPeKuHTlmg3JpSHWLhP+pdjwVuOrro8j61F/7ffNJcRvehSPJKbOW4qH | ||||
| b5aYXdU07n1F4KPy0PfhaHhMpWsbK3w6yQnVVWivIRDw7bD5f/TQgxdWqVd7+HuC | ||||
| LDBP2X0uZzF7FNPvkP4lOut9uNnWSoSRXAcZ5h33AQKBgQDWJYKGNoA8/IT9+e8n | ||||
| v1Fy0RNL/SmBfGZW9pFGFT2pcu6TrzVSugQeWY/YFO2X6FqLPbL4p72Ar4rF0Uxl | ||||
| 31aYIjy3jDGzMabdIuW7mBogvtNjBG+0UgcLQzbdG6JkvTkQgqUjwIn/+Jo+0sS5 | ||||
| dEylNM0zC6zx1f1U1dGGZaNcLg== | ||||
| -----END PRIVATE KEY----- | ||||
							
								
								
									
										294
									
								
								test/test.connection-forwarding.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								test/test.connection-forwarding.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,294 @@ | ||||
| import { expect, tap } from '@git.zone/tapbundle'; | ||||
| import * as net from 'net'; | ||||
| import * as tls from 'tls'; | ||||
| import * as fs from 'fs'; | ||||
| import * as path from 'path'; | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/smart-proxy.js'; | ||||
| import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; | ||||
|  | ||||
| // Setup test infrastructure | ||||
| const testCertPath = path.join(process.cwd(), 'test', 'helpers', 'test-cert.pem'); | ||||
| const testKeyPath = path.join(process.cwd(), 'test', 'helpers', 'test-key.pem'); | ||||
|  | ||||
| let testServer: net.Server; | ||||
| let tlsTestServer: tls.Server; | ||||
| let smartProxy: SmartProxy; | ||||
|  | ||||
| tap.test('setup test servers', async () => { | ||||
|   // Create TCP test server | ||||
|   testServer = net.createServer((socket) => { | ||||
|     socket.write('Connected to TCP test server\n'); | ||||
|     socket.on('data', (data) => { | ||||
|       socket.write(`TCP Echo: ${data}`); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   await new Promise<void>((resolve) => { | ||||
|     testServer.listen(7001, '127.0.0.1', () => { | ||||
|       console.log('TCP test server listening on port 7001'); | ||||
|       resolve(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   // Create TLS test server for SNI testing | ||||
|   tlsTestServer = tls.createServer( | ||||
|     { | ||||
|       cert: fs.readFileSync(testCertPath), | ||||
|       key: fs.readFileSync(testKeyPath), | ||||
|     }, | ||||
|     (socket) => { | ||||
|       socket.write('Connected to TLS test server\n'); | ||||
|       socket.on('data', (data) => { | ||||
|         socket.write(`TLS Echo: ${data}`); | ||||
|       }); | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   await new Promise<void>((resolve) => { | ||||
|     tlsTestServer.listen(7002, '127.0.0.1', () => { | ||||
|       console.log('TLS test server listening on port 7002'); | ||||
|       resolve(); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| tap.test('should forward TCP connections correctly', async () => { | ||||
|   // Create SmartProxy with forward route | ||||
|   smartProxy = new SmartProxy({ | ||||
|     enableDetailedLogging: true, | ||||
|     routes: [ | ||||
|       { | ||||
|         id: 'tcp-forward', | ||||
|         name: 'TCP Forward Route', | ||||
|         match: { | ||||
|           port: 8080, | ||||
|         }, | ||||
|         action: { | ||||
|           type: 'forward', | ||||
|           target: { | ||||
|             host: '127.0.0.1', | ||||
|             port: 7001, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     ], | ||||
|   }); | ||||
|  | ||||
|   await smartProxy.start(); | ||||
|  | ||||
|   // Test TCP forwarding | ||||
|   const client = await new Promise<net.Socket>((resolve, reject) => { | ||||
|     const socket = net.connect(8080, '127.0.0.1', () => { | ||||
|       console.log('Connected to proxy'); | ||||
|       resolve(socket); | ||||
|     }); | ||||
|     socket.on('error', reject); | ||||
|   }); | ||||
|  | ||||
|   // Test data transmission | ||||
|   await new Promise<void>((resolve) => { | ||||
|     client.on('data', (data) => { | ||||
|       const response = data.toString(); | ||||
|       console.log('Received:', response); | ||||
|       expect(response).toContain('Connected to TCP test server'); | ||||
|       client.end(); | ||||
|       resolve(); | ||||
|     }); | ||||
|  | ||||
|     client.write('Hello from client'); | ||||
|   }); | ||||
|  | ||||
|   await smartProxy.stop(); | ||||
| }); | ||||
|  | ||||
| tap.test('should handle TLS passthrough correctly', async () => { | ||||
|   // Create SmartProxy with TLS passthrough route | ||||
|   smartProxy = new SmartProxy({ | ||||
|     enableDetailedLogging: true, | ||||
|     routes: [ | ||||
|       { | ||||
|         id: 'tls-passthrough', | ||||
|         name: 'TLS Passthrough Route', | ||||
|         match: { | ||||
|           port: 8443, | ||||
|           domain: 'test.example.com', | ||||
|         }, | ||||
|         action: { | ||||
|           type: 'forward', | ||||
|           tls: { | ||||
|             mode: 'passthrough', | ||||
|           }, | ||||
|           target: { | ||||
|             host: '127.0.0.1', | ||||
|             port: 7002, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     ], | ||||
|   }); | ||||
|  | ||||
|   await smartProxy.start(); | ||||
|  | ||||
|   // Test TLS passthrough | ||||
|   const client = await new Promise<tls.TLSSocket>((resolve, reject) => { | ||||
|     const socket = tls.connect( | ||||
|       { | ||||
|         port: 8443, | ||||
|         host: '127.0.0.1', | ||||
|         servername: 'test.example.com', | ||||
|         rejectUnauthorized: false, | ||||
|       }, | ||||
|       () => { | ||||
|         console.log('Connected via TLS'); | ||||
|         resolve(socket); | ||||
|       } | ||||
|     ); | ||||
|     socket.on('error', reject); | ||||
|   }); | ||||
|  | ||||
|   // Test data transmission over TLS | ||||
|   await new Promise<void>((resolve) => { | ||||
|     client.on('data', (data) => { | ||||
|       const response = data.toString(); | ||||
|       console.log('TLS Received:', response); | ||||
|       expect(response).toContain('Connected to TLS test server'); | ||||
|       client.end(); | ||||
|       resolve(); | ||||
|     }); | ||||
|  | ||||
|     client.write('Hello from TLS client'); | ||||
|   }); | ||||
|  | ||||
|   await smartProxy.stop(); | ||||
| }); | ||||
|  | ||||
| tap.test('should handle SNI-based forwarding', async () => { | ||||
|   // Create SmartProxy with multiple domain routes | ||||
|   smartProxy = new SmartProxy({ | ||||
|     enableDetailedLogging: true, | ||||
|     routes: [ | ||||
|       { | ||||
|         id: 'domain-a', | ||||
|         name: 'Domain A Route', | ||||
|         match: { | ||||
|           port: 8443, | ||||
|           domain: 'a.example.com', | ||||
|         }, | ||||
|         action: { | ||||
|           type: 'forward', | ||||
|           tls: { | ||||
|             mode: 'passthrough', | ||||
|           }, | ||||
|           target: { | ||||
|             host: '127.0.0.1', | ||||
|             port: 7002, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         id: 'domain-b', | ||||
|         name: 'Domain B Route', | ||||
|         match: { | ||||
|           port: 8443, | ||||
|           domain: 'b.example.com', | ||||
|         }, | ||||
|         action: { | ||||
|           type: 'forward', | ||||
|           target: { | ||||
|             host: '127.0.0.1', | ||||
|             port: 7001, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     ], | ||||
|   }); | ||||
|  | ||||
|   await smartProxy.start(); | ||||
|  | ||||
|   // Test domain A (TLS passthrough) | ||||
|   const clientA = await new Promise<tls.TLSSocket>((resolve, reject) => { | ||||
|     const socket = tls.connect( | ||||
|       { | ||||
|         port: 8443, | ||||
|         host: '127.0.0.1', | ||||
|         servername: 'a.example.com', | ||||
|         rejectUnauthorized: false, | ||||
|       }, | ||||
|       () => { | ||||
|         console.log('Connected to domain A'); | ||||
|         resolve(socket); | ||||
|       } | ||||
|     ); | ||||
|     socket.on('error', reject); | ||||
|   }); | ||||
|  | ||||
|   await new Promise<void>((resolve) => { | ||||
|     clientA.on('data', (data) => { | ||||
|       const response = data.toString(); | ||||
|       console.log('Domain A response:', response); | ||||
|       expect(response).toContain('Connected to TLS test server'); | ||||
|       clientA.end(); | ||||
|       resolve(); | ||||
|     }); | ||||
|  | ||||
|     clientA.write('Hello from domain A'); | ||||
|   }); | ||||
|  | ||||
|   // Test domain B (non-TLS forward) | ||||
|   const clientB = await new Promise<net.Socket>((resolve, reject) => { | ||||
|     const socket = net.connect(8443, '127.0.0.1', () => { | ||||
|       // Send TLS ClientHello with SNI for b.example.com | ||||
|       const clientHello = Buffer.from([ | ||||
|         0x16, 0x03, 0x01, 0x00, 0x4e, // TLS Record header | ||||
|         0x01, 0x00, 0x00, 0x4a, // Handshake header | ||||
|         0x03, 0x03, // TLS version | ||||
|         // Random bytes | ||||
|         ...Array(32).fill(0), | ||||
|         0x00, // Session ID length | ||||
|         0x00, 0x02, // Cipher suites length | ||||
|         0x00, 0x35, // Cipher suite | ||||
|         0x01, 0x00, // Compression methods | ||||
|         0x00, 0x1f, // Extensions length | ||||
|         0x00, 0x00, // SNI extension | ||||
|         0x00, 0x1b, // Extension length | ||||
|         0x00, 0x19, // SNI list length | ||||
|         0x00, // SNI type (hostname) | ||||
|         0x00, 0x16, // SNI length | ||||
|         // "b.example.com" in ASCII | ||||
|         0x62, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, | ||||
|       ]); | ||||
|        | ||||
|       socket.write(clientHello); | ||||
|        | ||||
|       setTimeout(() => { | ||||
|         resolve(socket); | ||||
|       }, 100); | ||||
|     }); | ||||
|     socket.on('error', reject); | ||||
|   }); | ||||
|  | ||||
|   await new Promise<void>((resolve) => { | ||||
|     clientB.on('data', (data) => { | ||||
|       const response = data.toString(); | ||||
|       console.log('Domain B response:', response); | ||||
|       // Should be forwarded to TCP server | ||||
|       expect(response).toContain('Connected to TCP test server'); | ||||
|       clientB.end(); | ||||
|       resolve(); | ||||
|     }); | ||||
|  | ||||
|     // Send regular data after initial handshake | ||||
|     setTimeout(() => { | ||||
|       clientB.write('Hello from domain B'); | ||||
|     }, 200); | ||||
|   }); | ||||
|  | ||||
|   await smartProxy.stop(); | ||||
| }); | ||||
|  | ||||
| tap.test('cleanup', async () => { | ||||
|   testServer.close(); | ||||
|   tlsTestServer.close(); | ||||
| }); | ||||
|  | ||||
| export default tap.start(); | ||||
							
								
								
									
										105
									
								
								test/test.forwarding-regression.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								test/test.forwarding-regression.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| import { expect, tap } from '@git.zone/tapbundle'; | ||||
| import * as net from 'net'; | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/smart-proxy.js'; | ||||
|  | ||||
| // Test to verify port forwarding works correctly | ||||
| tap.test('forward connections should not be immediately closed', async (t) => { | ||||
|   // Create a backend server that accepts connections | ||||
|   const testServer = net.createServer((socket) => { | ||||
|     console.log('Client connected to test server'); | ||||
|     socket.write('Welcome from test server\n'); | ||||
|      | ||||
|     socket.on('data', (data) => { | ||||
|       console.log('Test server received:', data.toString()); | ||||
|       socket.write(`Echo: ${data}`); | ||||
|     }); | ||||
|      | ||||
|     socket.on('error', (err) => { | ||||
|       console.error('Test server socket error:', err); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   // Listen on a non-privileged port | ||||
|   await new Promise<void>((resolve) => { | ||||
|     testServer.listen(9090, '127.0.0.1', () => { | ||||
|       console.log('Test server listening on port 9090'); | ||||
|       resolve(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   // Create SmartProxy with a forward route | ||||
|   const smartProxy = new SmartProxy({ | ||||
|     enableDetailedLogging: true, | ||||
|     routes: [ | ||||
|       { | ||||
|         id: 'forward-test', | ||||
|         name: 'Forward Test Route', | ||||
|         match: { | ||||
|           port: 8080, | ||||
|         }, | ||||
|         action: { | ||||
|           type: 'forward', | ||||
|           target: { | ||||
|             host: '127.0.0.1', | ||||
|             port: 9090, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     ], | ||||
|   }); | ||||
|  | ||||
|   await smartProxy.start(); | ||||
|  | ||||
|   // Create a client connection through the proxy | ||||
|   const client = net.createConnection({ | ||||
|     port: 8080, | ||||
|     host: '127.0.0.1', | ||||
|   }); | ||||
|  | ||||
|   let connectionClosed = false; | ||||
|   let dataReceived = false; | ||||
|   let welcomeMessage = ''; | ||||
|  | ||||
|   client.on('connect', () => { | ||||
|     console.log('Client connected to proxy'); | ||||
|   }); | ||||
|  | ||||
|   client.on('data', (data) => { | ||||
|     console.log('Client received:', data.toString()); | ||||
|     dataReceived = true; | ||||
|     welcomeMessage = data.toString(); | ||||
|   }); | ||||
|  | ||||
|   client.on('close', () => { | ||||
|     console.log('Client connection closed'); | ||||
|     connectionClosed = true; | ||||
|   }); | ||||
|  | ||||
|   client.on('error', (err) => { | ||||
|     console.error('Client error:', err); | ||||
|   }); | ||||
|  | ||||
|   // Wait for the welcome message | ||||
|   await t.waitForExpect(() => { | ||||
|     return dataReceived; | ||||
|   }, 'Data should be received from the server', 2000); | ||||
|  | ||||
|   // Verify we got the welcome message | ||||
|   expect(welcomeMessage).toContain('Welcome from test server'); | ||||
|    | ||||
|   // Send some data | ||||
|   client.write('Hello from client'); | ||||
|    | ||||
|   // Wait a bit to make sure connection isn't immediately closed | ||||
|   await new Promise(resolve => setTimeout(resolve, 100)); | ||||
|    | ||||
|   // Connection should still be open | ||||
|   expect(connectionClosed).toBe(false); | ||||
|    | ||||
|   // Clean up | ||||
|   client.end(); | ||||
|   await smartProxy.stop(); | ||||
|   testServer.close(); | ||||
| }); | ||||
|  | ||||
| export default tap.start(); | ||||
							
								
								
									
										116
									
								
								test/test.nftables-forwarding.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								test/test.nftables-forwarding.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| import { expect, tap } from '@git.zone/tapbundle'; | ||||
| import * as net from 'net'; | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/smart-proxy.js'; | ||||
| import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; | ||||
|  | ||||
| // Test to verify NFTables forwarding doesn't terminate connections | ||||
| tap.test('NFTables forwarding should not terminate connections', async () => { | ||||
|   // Create a test server that receives connections | ||||
|   const testServer = net.createServer((socket) => { | ||||
|     socket.write('Connected to test server\n'); | ||||
|     socket.on('data', (data) => { | ||||
|       socket.write(`Echo: ${data}`); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   // Start test server | ||||
|   await new Promise<void>((resolve) => { | ||||
|     testServer.listen(8001, '127.0.0.1', () => { | ||||
|       console.log('Test server listening on port 8001'); | ||||
|       resolve(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   // Create SmartProxy with NFTables route | ||||
|   const smartProxy = new SmartProxy({ | ||||
|     enableDetailedLogging: true, | ||||
|     acceptedRoutes: [ | ||||
|       { | ||||
|         id: 'nftables-test', | ||||
|         name: 'NFTables Test Route', | ||||
|         match: { | ||||
|           port: 8080, | ||||
|         }, | ||||
|         action: { | ||||
|           type: 'forward', | ||||
|           forwardingEngine: 'nftables', | ||||
|           target: { | ||||
|             host: '127.0.0.1', | ||||
|             port: 8001, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|       // Also add regular forwarding route for comparison | ||||
|       { | ||||
|         id: 'regular-test', | ||||
|         name: 'Regular Forward Route', | ||||
|         match: { | ||||
|           port: 8081, | ||||
|         }, | ||||
|         action: { | ||||
|           type: 'forward', | ||||
|           target: { | ||||
|             host: '127.0.0.1', | ||||
|             port: 8001, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     ], | ||||
|   }); | ||||
|  | ||||
|   await smartProxy.start(); | ||||
|  | ||||
|   // Test NFTables route | ||||
|   const nftablesConnection = await new Promise<net.Socket>((resolve, reject) => { | ||||
|     const client = net.connect(8080, '127.0.0.1', () => { | ||||
|       console.log('Connected to NFTables route'); | ||||
|       resolve(client); | ||||
|     }); | ||||
|     client.on('error', reject); | ||||
|   }); | ||||
|  | ||||
|   // Add timeout to check if connection stays alive | ||||
|   await new Promise<void>((resolve) => { | ||||
|     let dataReceived = false; | ||||
|     nftablesConnection.on('data', (data) => { | ||||
|       console.log('NFTables route data:', data.toString()); | ||||
|       dataReceived = true; | ||||
|     }); | ||||
|  | ||||
|     // Send test data | ||||
|     nftablesConnection.write('Test NFTables'); | ||||
|  | ||||
|     // Check connection after 100ms | ||||
|     setTimeout(() => { | ||||
|       // Connection should still be alive even if app doesn't handle it | ||||
|       expect(nftablesConnection.destroyed).toBe(false); | ||||
|       nftablesConnection.end(); | ||||
|       resolve(); | ||||
|     }, 100); | ||||
|   }); | ||||
|  | ||||
|   // Test regular forwarding route for comparison | ||||
|   const regularConnection = await new Promise<net.Socket>((resolve, reject) => { | ||||
|     const client = net.connect(8081, '127.0.0.1', () => { | ||||
|       console.log('Connected to regular route'); | ||||
|       resolve(client); | ||||
|     }); | ||||
|     client.on('error', reject); | ||||
|   }); | ||||
|  | ||||
|   // Test regular connection works | ||||
|   await new Promise<void>((resolve) => { | ||||
|     regularConnection.on('data', (data) => { | ||||
|       console.log('Regular route data:', data.toString()); | ||||
|       expect(data.toString()).toContain('Connected to test server'); | ||||
|       regularConnection.end(); | ||||
|       resolve(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   // Cleanup | ||||
|   await smartProxy.stop(); | ||||
|   testServer.close(); | ||||
| }); | ||||
|  | ||||
| export default tap.start(); | ||||
							
								
								
									
										73
									
								
								test/test.port-forwarding-fix.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								test/test.port-forwarding-fix.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| import { expect, tap } from '@git.zone/tapbundle'; | ||||
| import * as net from 'net'; | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/smart-proxy.js'; | ||||
|  | ||||
| tap.test('Port forwarding should not immediately close connections', async () => { | ||||
|   // Create an echo server | ||||
|   const echoServer = net.createServer((socket) => { | ||||
|     socket.on('data', (data) => { | ||||
|       socket.write(`ECHO: ${data}`); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   await new Promise<void>((resolve) => { | ||||
|     echoServer.listen(8888, () => resolve()); | ||||
|   }); | ||||
|  | ||||
|   // Create proxy with forwarding route | ||||
|   const proxy = new SmartProxy({ | ||||
|     routes: [{ | ||||
|       id: 'test', | ||||
|       match: { port: 9999 }, | ||||
|       action: { | ||||
|         type: 'forward', | ||||
|         target: { host: 'localhost', port: 8888 } | ||||
|       } | ||||
|     }] | ||||
|   }); | ||||
|  | ||||
|   await proxy.start(); | ||||
|  | ||||
|   // Test connection through proxy | ||||
|   const client = net.createConnection(9999, 'localhost'); | ||||
|    | ||||
|   const result = await new Promise<string>((resolve, reject) => { | ||||
|     client.on('data', (data) => { | ||||
|       resolve(data.toString()); | ||||
|     }); | ||||
|      | ||||
|     client.on('error', reject); | ||||
|      | ||||
|     client.write('Hello'); | ||||
|   }); | ||||
|  | ||||
|   expect(result).toEqual('ECHO: Hello'); | ||||
|    | ||||
|   client.end(); | ||||
|   await proxy.stop(); | ||||
|   echoServer.close(); | ||||
| }); | ||||
|  | ||||
| tap.test('TLS passthrough should work correctly', async () => { | ||||
|   // Create proxy with TLS passthrough | ||||
|   const proxy = new SmartProxy({ | ||||
|     routes: [{ | ||||
|       id: 'tls-test',  | ||||
|       match: { port: 8443, domain: 'test.example.com' }, | ||||
|       action: { | ||||
|         type: 'forward', | ||||
|         tls: { mode: 'passthrough' }, | ||||
|         target: { host: 'localhost', port: 443 } | ||||
|       } | ||||
|     }] | ||||
|   }); | ||||
|  | ||||
|   await proxy.start(); | ||||
|  | ||||
|   // For now just verify the proxy starts correctly with TLS passthrough route | ||||
|   expect(proxy).toBeDefined(); | ||||
|  | ||||
|   await proxy.stop(); | ||||
| }); | ||||
|  | ||||
| export default tap.start(); | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '19.3.4', | ||||
|   version: '19.3.5', | ||||
|   description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.' | ||||
| } | ||||
|   | ||||
| @@ -339,21 +339,6 @@ export class RouteConnectionHandler { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // Check if this route uses NFTables for forwarding | ||||
|     if (route.action.forwardingEngine === 'nftables') { | ||||
|       // For NFTables routes, we don't need to do anything at the application level | ||||
|       // The packet is forwarded at the kernel level | ||||
|  | ||||
|       // Log the connection | ||||
|       console.log( | ||||
|         `[${connectionId}] Connection forwarded by NFTables: ${record.remoteIP} -> port ${record.localPort}` | ||||
|       ); | ||||
|  | ||||
|       // Just close the socket in our application since it's handled at kernel level | ||||
|       socket.end(); | ||||
|       this.connectionManager.cleanupConnection(record, 'nftables_handled'); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Handle the route based on its action type | ||||
|     switch (route.action.type) { | ||||
| @@ -391,10 +376,13 @@ export class RouteConnectionHandler { | ||||
|  | ||||
|     // Check if this route uses NFTables for forwarding | ||||
|     if (action.forwardingEngine === 'nftables') { | ||||
|       // Log detailed information about NFTables-handled connection | ||||
|       // NFTables handles packet forwarding at the kernel level | ||||
|       // The application should NOT interfere with these connections | ||||
|        | ||||
|       // Just log the connection for monitoring purposes | ||||
|       if (this.settings.enableDetailedLogging) { | ||||
|         console.log( | ||||
|           `[${record.id}] Connection forwarded by NFTables (kernel-level): ` + | ||||
|           `[${record.id}] NFTables forwarding (kernel-level): ` + | ||||
|             `${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` + | ||||
|             ` (Route: "${route.name || 'unnamed'}", Domain: ${record.lockedDomain || 'n/a'})` | ||||
|         ); | ||||
| @@ -420,14 +408,8 @@ export class RouteConnectionHandler { | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // This connection is handled at the kernel level, no need to process at application level | ||||
|       // Close the socket gracefully in our application layer | ||||
|       socket.end(); | ||||
|  | ||||
|       // Mark the connection as handled by NFTables for proper cleanup | ||||
|       record.nftablesHandled = true; | ||||
|       this.connectionManager.initiateCleanupOnce(record, 'nftables_handled'); | ||||
|       return; | ||||
|       // For NFTables routes, continue processing the connection normally | ||||
|       // since the packet forwarding happens transparently at the kernel level | ||||
|     } | ||||
|  | ||||
|     // We should have a target configuration for forwarding | ||||
|   | ||||
		Reference in New Issue
	
	Block a user