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
|
||||
|
||||
## 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user