349 lines
9.9 KiB
TypeScript
349 lines
9.9 KiB
TypeScript
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
|
import { createNfTablesRoute, createNfTablesTerminateRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import * as net from 'net';
|
|
import * as http from 'http';
|
|
import * as https from 'https';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import * as child_process from 'child_process';
|
|
import { promisify } from 'util';
|
|
|
|
const exec = promisify(child_process.exec);
|
|
|
|
// Get __dirname equivalent for ES modules
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// Check if we have root privileges
|
|
async function checkRootPrivileges(): Promise<boolean> {
|
|
try {
|
|
const { stdout } = await exec('id -u');
|
|
return stdout.trim() === '0';
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check if tests should run
|
|
const runTests = await checkRootPrivileges();
|
|
|
|
if (!runTests) {
|
|
console.log('');
|
|
console.log('========================================');
|
|
console.log('NFTables tests require root privileges');
|
|
console.log('Skipping NFTables integration tests');
|
|
console.log('========================================');
|
|
console.log('');
|
|
// Skip tests when not running as root - tests are marked with tap.skip.test
|
|
}
|
|
|
|
// Test server and client utilities
|
|
let testTcpServer: net.Server;
|
|
let testHttpServer: http.Server;
|
|
let testHttpsServer: https.Server;
|
|
let smartProxy: SmartProxy;
|
|
|
|
const TEST_TCP_PORT = 4000;
|
|
const TEST_HTTP_PORT = 4001;
|
|
const TEST_HTTPS_PORT = 4002;
|
|
const PROXY_TCP_PORT = 5000;
|
|
const PROXY_HTTP_PORT = 5001;
|
|
const PROXY_HTTPS_PORT = 5002;
|
|
const TEST_DATA = 'Hello through NFTables!';
|
|
|
|
// Helper to create test certificates
|
|
async function createTestCertificates() {
|
|
try {
|
|
// Import the certificate helper
|
|
const certsModule = await import('./helpers/certificates.js');
|
|
const certificates = certsModule.loadTestCertificates();
|
|
return {
|
|
cert: certificates.publicKey,
|
|
key: certificates.privateKey
|
|
};
|
|
} catch (err) {
|
|
console.error('Failed to load test certificates:', err);
|
|
// Use dummy certificates for testing
|
|
return {
|
|
cert: fs.readFileSync(path.join(__dirname, '..', 'assets', 'certs', 'cert.pem'), 'utf8'),
|
|
key: fs.readFileSync(path.join(__dirname, '..', 'assets', 'certs', 'key.pem'), 'utf8')
|
|
};
|
|
}
|
|
}
|
|
|
|
tap.skip.test('setup NFTables integration test environment', async () => {
|
|
console.log('Running NFTables integration tests with root privileges');
|
|
|
|
// Create a basic TCP test server
|
|
testTcpServer = net.createServer((socket) => {
|
|
socket.on('data', (data) => {
|
|
socket.write(`Server says: ${data.toString()}`);
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
testTcpServer.listen(TEST_TCP_PORT, () => {
|
|
console.log(`TCP test server listening on port ${TEST_TCP_PORT}`);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// Create an HTTP test server
|
|
testHttpServer = http.createServer((req, res) => {
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end(`HTTP Server says: ${TEST_DATA}`);
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
testHttpServer.listen(TEST_HTTP_PORT, () => {
|
|
console.log(`HTTP test server listening on port ${TEST_HTTP_PORT}`);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// Create an HTTPS test server
|
|
const certs = await createTestCertificates();
|
|
testHttpsServer = https.createServer({ key: certs.key, cert: certs.cert }, (req, res) => {
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end(`HTTPS Server says: ${TEST_DATA}`);
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
testHttpsServer.listen(TEST_HTTPS_PORT, () => {
|
|
console.log(`HTTPS test server listening on port ${TEST_HTTPS_PORT}`);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// Create SmartProxy with various NFTables routes
|
|
smartProxy = new SmartProxy({
|
|
enableDetailedLogging: true,
|
|
routes: [
|
|
// TCP forwarding route
|
|
createNfTablesRoute('tcp-nftables', {
|
|
host: 'localhost',
|
|
port: TEST_TCP_PORT
|
|
}, {
|
|
ports: PROXY_TCP_PORT,
|
|
protocol: 'tcp'
|
|
}),
|
|
|
|
// HTTP forwarding route
|
|
createNfTablesRoute('http-nftables', {
|
|
host: 'localhost',
|
|
port: TEST_HTTP_PORT
|
|
}, {
|
|
ports: PROXY_HTTP_PORT,
|
|
protocol: 'tcp'
|
|
}),
|
|
|
|
// HTTPS termination route
|
|
createNfTablesTerminateRoute('https-nftables.example.com', {
|
|
host: 'localhost',
|
|
port: TEST_HTTPS_PORT
|
|
}, {
|
|
ports: PROXY_HTTPS_PORT,
|
|
protocol: 'tcp',
|
|
certificate: certs
|
|
}),
|
|
|
|
// Route with IP allow list
|
|
createNfTablesRoute('secure-tcp', {
|
|
host: 'localhost',
|
|
port: TEST_TCP_PORT
|
|
}, {
|
|
ports: 5003,
|
|
protocol: 'tcp',
|
|
ipAllowList: ['127.0.0.1', '::1']
|
|
}),
|
|
|
|
// Route with QoS settings
|
|
createNfTablesRoute('qos-tcp', {
|
|
host: 'localhost',
|
|
port: TEST_TCP_PORT
|
|
}, {
|
|
ports: 5004,
|
|
protocol: 'tcp',
|
|
maxRate: '10mbps',
|
|
priority: 1
|
|
})
|
|
]
|
|
});
|
|
|
|
console.log('SmartProxy created, now starting...');
|
|
|
|
// Start the proxy
|
|
try {
|
|
await smartProxy.start();
|
|
console.log('SmartProxy started successfully');
|
|
|
|
// Verify proxy is listening on expected ports
|
|
const listeningPorts = smartProxy.getListeningPorts();
|
|
console.log(`SmartProxy is listening on ports: ${listeningPorts.join(', ')}`);
|
|
} catch (err) {
|
|
console.error('Failed to start SmartProxy:', err);
|
|
throw err;
|
|
}
|
|
});
|
|
|
|
tap.skip.test('should forward TCP connections through NFTables', async () => {
|
|
console.log(`Attempting to connect to proxy TCP port ${PROXY_TCP_PORT}...`);
|
|
|
|
// First verify our test server is running
|
|
try {
|
|
const testClient = new net.Socket();
|
|
await new Promise<void>((resolve, reject) => {
|
|
testClient.connect(TEST_TCP_PORT, 'localhost', () => {
|
|
console.log(`Test server on port ${TEST_TCP_PORT} is accessible`);
|
|
testClient.end();
|
|
resolve();
|
|
});
|
|
testClient.on('error', reject);
|
|
});
|
|
} catch (err) {
|
|
console.error(`Test server on port ${TEST_TCP_PORT} is not accessible: ${err}`);
|
|
}
|
|
|
|
// Connect to the proxy port
|
|
const client = new net.Socket();
|
|
|
|
const response = await new Promise<string>((resolve, reject) => {
|
|
let responseData = '';
|
|
const timeout = setTimeout(() => {
|
|
client.destroy();
|
|
reject(new Error(`Connection timeout after 5 seconds to proxy port ${PROXY_TCP_PORT}`));
|
|
}, 5000);
|
|
|
|
client.connect(PROXY_TCP_PORT, 'localhost', () => {
|
|
console.log(`Connected to proxy port ${PROXY_TCP_PORT}, sending data...`);
|
|
client.write(TEST_DATA);
|
|
});
|
|
|
|
client.on('data', (data) => {
|
|
console.log(`Received data from proxy: ${data.toString()}`);
|
|
responseData += data.toString();
|
|
client.end();
|
|
});
|
|
|
|
client.on('end', () => {
|
|
clearTimeout(timeout);
|
|
resolve(responseData);
|
|
});
|
|
|
|
client.on('error', (err) => {
|
|
clearTimeout(timeout);
|
|
console.error(`Connection error on proxy port ${PROXY_TCP_PORT}: ${err.message}`);
|
|
reject(err);
|
|
});
|
|
});
|
|
|
|
expect(response).toEqual(`Server says: ${TEST_DATA}`);
|
|
});
|
|
|
|
tap.skip.test('should forward HTTP connections through NFTables', async () => {
|
|
const response = await new Promise<string>((resolve, reject) => {
|
|
http.get(`http://localhost:${PROXY_HTTP_PORT}`, (res) => {
|
|
let data = '';
|
|
res.on('data', (chunk) => {
|
|
data += chunk;
|
|
});
|
|
res.on('end', () => {
|
|
resolve(data);
|
|
});
|
|
}).on('error', reject);
|
|
});
|
|
|
|
expect(response).toEqual(`HTTP Server says: ${TEST_DATA}`);
|
|
});
|
|
|
|
tap.skip.test('should handle HTTPS termination with NFTables', async () => {
|
|
// Skip this test if running without proper certificates
|
|
const response = await new Promise<string>((resolve, reject) => {
|
|
const options = {
|
|
hostname: 'localhost',
|
|
port: PROXY_HTTPS_PORT,
|
|
path: '/',
|
|
method: 'GET',
|
|
rejectUnauthorized: false // For self-signed cert
|
|
};
|
|
|
|
https.get(options, (res) => {
|
|
let data = '';
|
|
res.on('data', (chunk) => {
|
|
data += chunk;
|
|
});
|
|
res.on('end', () => {
|
|
resolve(data);
|
|
});
|
|
}).on('error', reject);
|
|
});
|
|
|
|
expect(response).toEqual(`HTTPS Server says: ${TEST_DATA}`);
|
|
});
|
|
|
|
tap.skip.test('should respect IP allow lists in NFTables', async () => {
|
|
// This test should pass since we're connecting from localhost
|
|
const client = new net.Socket();
|
|
|
|
const connected = await new Promise<boolean>((resolve) => {
|
|
const timeout = setTimeout(() => {
|
|
client.destroy();
|
|
resolve(false);
|
|
}, 2000);
|
|
|
|
client.connect(5003, 'localhost', () => {
|
|
clearTimeout(timeout);
|
|
client.end();
|
|
resolve(true);
|
|
});
|
|
|
|
client.on('error', () => {
|
|
clearTimeout(timeout);
|
|
resolve(false);
|
|
});
|
|
});
|
|
|
|
expect(connected).toBeTrue();
|
|
});
|
|
|
|
tap.skip.test('should get NFTables status', async () => {
|
|
const status = await smartProxy.getNfTablesStatus();
|
|
|
|
// Check that we have status for our routes
|
|
const statusKeys = Object.keys(status);
|
|
expect(statusKeys.length).toBeGreaterThan(0);
|
|
|
|
// Check status structure for one of the routes
|
|
const firstStatus = status[statusKeys[0]];
|
|
expect(firstStatus).toHaveProperty('active');
|
|
expect(firstStatus).toHaveProperty('ruleCount');
|
|
expect(firstStatus.ruleCount).toHaveProperty('total');
|
|
expect(firstStatus.ruleCount).toHaveProperty('added');
|
|
});
|
|
|
|
tap.skip.test('cleanup NFTables integration test environment', async () => {
|
|
// Stop the proxy and test servers
|
|
await smartProxy.stop();
|
|
|
|
await new Promise<void>((resolve) => {
|
|
testTcpServer.close(() => {
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
testHttpServer.close(() => {
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
testHttpsServer.close(() => {
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
|
|
export default tap.start(); |