fix(smartproxy): Correct NFTables forwarding handling to avoid premature connection termination and add comprehensive tests
This commit is contained in:
parent
8d821b4e25
commit
33732c2361
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user