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 | # 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) | ## 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. | 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 = { | export const commitinfo = { | ||||||
|   name: '@push.rocks/smartproxy', |   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.' |   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 |     // Handle the route based on its action type | ||||||
|     switch (route.action.type) { |     switch (route.action.type) { | ||||||
| @@ -391,10 +376,13 @@ export class RouteConnectionHandler { | |||||||
|  |  | ||||||
|     // Check if this route uses NFTables for forwarding |     // Check if this route uses NFTables for forwarding | ||||||
|     if (action.forwardingEngine === 'nftables') { |     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) { |       if (this.settings.enableDetailedLogging) { | ||||||
|         console.log( |         console.log( | ||||||
|           `[${record.id}] Connection forwarded by NFTables (kernel-level): ` + |           `[${record.id}] NFTables forwarding (kernel-level): ` + | ||||||
|             `${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` + |             `${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` + | ||||||
|             ` (Route: "${route.name || 'unnamed'}", Domain: ${record.lockedDomain || 'n/a'})` |             ` (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 |       // For NFTables routes, continue processing the connection normally | ||||||
|       // Close the socket gracefully in our application layer |       // since the packet forwarding happens transparently at the kernel level | ||||||
|       socket.end(); |  | ||||||
|  |  | ||||||
|       // Mark the connection as handled by NFTables for proper cleanup |  | ||||||
|       record.nftablesHandled = true; |  | ||||||
|       this.connectionManager.initiateCleanupOnce(record, 'nftables_handled'); |  | ||||||
|       return; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // We should have a target configuration for forwarding |     // We should have a target configuration for forwarding | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user