feat(nftables):add nftables support for nftables
This commit is contained in:
@ -28,7 +28,7 @@ tap.test('Route-based configuration examples', async (tools) => {
|
||||
port: 3000
|
||||
},
|
||||
security: {
|
||||
allowedIps: ['*'] // Allow all
|
||||
ipAllowList: ['*'] // Allow all
|
||||
},
|
||||
name: 'Basic HTTP Route'
|
||||
});
|
||||
@ -45,7 +45,7 @@ tap.test('Route-based configuration examples', async (tools) => {
|
||||
port: 443
|
||||
},
|
||||
security: {
|
||||
allowedIps: ['*'] // Allow all
|
||||
ipAllowList: ['*'] // Allow all
|
||||
},
|
||||
name: 'HTTPS Passthrough Route'
|
||||
});
|
||||
@ -67,7 +67,7 @@ tap.test('Route-based configuration examples', async (tools) => {
|
||||
'X-Forwarded-Proto': 'https'
|
||||
},
|
||||
security: {
|
||||
allowedIps: ['*'] // Allow all
|
||||
ipAllowList: ['*'] // Allow all
|
||||
},
|
||||
name: 'HTTPS Termination to HTTP Backend'
|
||||
});
|
||||
@ -94,7 +94,7 @@ tap.test('Route-based configuration examples', async (tools) => {
|
||||
'X-Original-Host': '{domain}'
|
||||
},
|
||||
security: {
|
||||
allowedIps: ['10.0.0.0/24', '192.168.1.0/24'],
|
||||
ipAllowList: ['10.0.0.0/24', '192.168.1.0/24'],
|
||||
maxConnections: 1000
|
||||
},
|
||||
name: 'Load Balanced HTTPS Route'
|
||||
@ -103,7 +103,7 @@ tap.test('Route-based configuration examples', async (tools) => {
|
||||
expect(loadBalancerRoute).toBeTruthy();
|
||||
expect(loadBalancerRoute.action.tls?.mode).toEqual('terminate-and-reencrypt');
|
||||
expect(Array.isArray(loadBalancerRoute.action.target?.host)).toBeTrue();
|
||||
expect(loadBalancerRoute.action.security?.allowedIps?.length).toEqual(2);
|
||||
expect(loadBalancerRoute.action.security?.ipAllowList?.length).toEqual(2);
|
||||
|
||||
// Example 5: Block specific IPs
|
||||
const blockRoute = createBlockRoute({
|
||||
|
94
test/test.nftables-integration.simple.ts
Normal file
94
test/test.nftables-integration.simple.ts
Normal file
@ -0,0 +1,94 @@
|
||||
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 '@push.rocks/tapbundle';
|
||||
import * as child_process from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const exec = promisify(child_process.exec);
|
||||
|
||||
// Check if we have root privileges to run NFTables tests
|
||||
async function checkRootPrivileges(): Promise<boolean> {
|
||||
try {
|
||||
// Check if we're running as root
|
||||
const { stdout } = await exec('id -u');
|
||||
return stdout.trim() === '0';
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if tests should run
|
||||
const isRoot = await checkRootPrivileges();
|
||||
|
||||
if (!isRoot) {
|
||||
console.log('');
|
||||
console.log('========================================');
|
||||
console.log('NFTables tests require root privileges');
|
||||
console.log('Skipping NFTables integration tests');
|
||||
console.log('========================================');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
tap.test('NFTables integration tests', async () => {
|
||||
|
||||
console.log('Running NFTables tests with root privileges');
|
||||
|
||||
// Create test routes
|
||||
const routes = [
|
||||
createNfTablesRoute('tcp-forward', {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}, {
|
||||
ports: 9080,
|
||||
protocol: 'tcp'
|
||||
}),
|
||||
|
||||
createNfTablesRoute('udp-forward', {
|
||||
host: 'localhost',
|
||||
port: 5353
|
||||
}, {
|
||||
ports: 5354,
|
||||
protocol: 'udp'
|
||||
}),
|
||||
|
||||
createNfTablesRoute('port-range', {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}, {
|
||||
ports: { from: 9000, to: 9100 },
|
||||
protocol: 'tcp'
|
||||
})
|
||||
];
|
||||
|
||||
const smartProxy = new SmartProxy({
|
||||
enableDetailedLogging: true,
|
||||
routes
|
||||
});
|
||||
|
||||
// Start the proxy
|
||||
await smartProxy.start();
|
||||
console.log('SmartProxy started with NFTables routes');
|
||||
|
||||
// Get NFTables status
|
||||
const status = await smartProxy.getNfTablesStatus();
|
||||
console.log('NFTables status:', JSON.stringify(status, null, 2));
|
||||
|
||||
// Verify all routes are provisioned
|
||||
expect(Object.keys(status).length).toEqual(routes.length);
|
||||
|
||||
for (const routeStatus of Object.values(status)) {
|
||||
expect(routeStatus.active).toBeTrue();
|
||||
expect(routeStatus.ruleCount.total).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// Stop the proxy
|
||||
await smartProxy.stop();
|
||||
console.log('SmartProxy stopped');
|
||||
|
||||
// Verify all rules are cleaned up
|
||||
const finalStatus = await smartProxy.getNfTablesStatus();
|
||||
expect(Object.keys(finalStatus).length).toEqual(0);
|
||||
});
|
||||
|
||||
export default tap.start();
|
351
test/test.nftables-integration.ts
Normal file
351
test/test.nftables-integration.ts
Normal file
@ -0,0 +1,351 @@
|
||||
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 '@push.rocks/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('');
|
||||
|
||||
// Exit without running any tests
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 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.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.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.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.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.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.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.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();
|
208
test/test.nftables-manager.ts
Normal file
208
test/test.nftables-manager.ts
Normal file
@ -0,0 +1,208 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import { NFTablesManager } from '../ts/proxies/smart-proxy/nftables-manager.js';
|
||||
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
||||
import type { ISmartProxyOptions } from '../ts/proxies/smart-proxy/models/interfaces.js';
|
||||
import * as child_process from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const exec = promisify(child_process.exec);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip tests if not root
|
||||
const isRoot = await checkRootPrivileges();
|
||||
if (!isRoot) {
|
||||
console.log('');
|
||||
console.log('========================================');
|
||||
console.log('NFTablesManager tests require root privileges');
|
||||
console.log('Skipping NFTablesManager tests');
|
||||
console.log('========================================');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for the NFTablesManager class
|
||||
*/
|
||||
|
||||
// Sample route configurations for testing
|
||||
const sampleRoute: IRouteConfig = {
|
||||
name: 'test-nftables-route',
|
||||
match: {
|
||||
ports: 8080,
|
||||
domains: 'test.example.com'
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: 'localhost',
|
||||
port: 8000
|
||||
},
|
||||
forwardingEngine: 'nftables',
|
||||
nftables: {
|
||||
protocol: 'tcp',
|
||||
preserveSourceIP: true,
|
||||
useIPSets: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Sample SmartProxy options
|
||||
const sampleOptions: ISmartProxyOptions = {
|
||||
routes: [sampleRoute],
|
||||
enableDetailedLogging: true
|
||||
};
|
||||
|
||||
// Instance of NFTablesManager for testing
|
||||
let manager: NFTablesManager;
|
||||
|
||||
// Skip these tests by default since they require root privileges to run NFTables commands
|
||||
// When running as root, change this to false
|
||||
const SKIP_TESTS = true;
|
||||
|
||||
tap.test('NFTablesManager setup test', async () => {
|
||||
if (SKIP_TESTS) {
|
||||
console.log('Test skipped - requires root privileges to run NFTables commands');
|
||||
expect(true).toEqual(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new instance of NFTablesManager
|
||||
manager = new NFTablesManager(sampleOptions);
|
||||
|
||||
// Verify the instance was created successfully
|
||||
expect(manager).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('NFTablesManager route provisioning test', async () => {
|
||||
if (SKIP_TESTS) {
|
||||
console.log('Test skipped - requires root privileges to run NFTables commands');
|
||||
expect(true).toEqual(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Provision the sample route
|
||||
const result = await manager.provisionRoute(sampleRoute);
|
||||
|
||||
// Verify the route was provisioned successfully
|
||||
expect(result).toEqual(true);
|
||||
|
||||
// Verify the route is listed as provisioned
|
||||
expect(manager.isRouteProvisioned(sampleRoute)).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('NFTablesManager status test', async () => {
|
||||
if (SKIP_TESTS) {
|
||||
console.log('Test skipped - requires root privileges to run NFTables commands');
|
||||
expect(true).toEqual(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the status of the managed rules
|
||||
const status = await manager.getStatus();
|
||||
|
||||
// Verify status includes our route
|
||||
const keys = Object.keys(status);
|
||||
expect(keys.length).toBeGreaterThan(0);
|
||||
|
||||
// Check the status of the first rule
|
||||
const firstStatus = status[keys[0]];
|
||||
expect(firstStatus.active).toEqual(true);
|
||||
expect(firstStatus.ruleCount.added).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('NFTablesManager route updating test', async () => {
|
||||
if (SKIP_TESTS) {
|
||||
console.log('Test skipped - requires root privileges to run NFTables commands');
|
||||
expect(true).toEqual(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an updated version of the sample route
|
||||
const updatedRoute: IRouteConfig = {
|
||||
...sampleRoute,
|
||||
action: {
|
||||
...sampleRoute.action,
|
||||
target: {
|
||||
host: 'localhost',
|
||||
port: 9000 // Different port
|
||||
},
|
||||
nftables: {
|
||||
...sampleRoute.action.nftables,
|
||||
protocol: 'all' // Different protocol
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Update the route
|
||||
const result = await manager.updateRoute(sampleRoute, updatedRoute);
|
||||
|
||||
// Verify the route was updated successfully
|
||||
expect(result).toEqual(true);
|
||||
|
||||
// Verify the old route is no longer provisioned
|
||||
expect(manager.isRouteProvisioned(sampleRoute)).toEqual(false);
|
||||
|
||||
// Verify the new route is provisioned
|
||||
expect(manager.isRouteProvisioned(updatedRoute)).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('NFTablesManager route deprovisioning test', async () => {
|
||||
if (SKIP_TESTS) {
|
||||
console.log('Test skipped - requires root privileges to run NFTables commands');
|
||||
expect(true).toEqual(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an updated version of the sample route from the previous test
|
||||
const updatedRoute: IRouteConfig = {
|
||||
...sampleRoute,
|
||||
action: {
|
||||
...sampleRoute.action,
|
||||
target: {
|
||||
host: 'localhost',
|
||||
port: 9000 // Different port from original test
|
||||
},
|
||||
nftables: {
|
||||
...sampleRoute.action.nftables,
|
||||
protocol: 'all' // Different protocol from original test
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Deprovision the route
|
||||
const result = await manager.deprovisionRoute(updatedRoute);
|
||||
|
||||
// Verify the route was deprovisioned successfully
|
||||
expect(result).toEqual(true);
|
||||
|
||||
// Verify the route is no longer provisioned
|
||||
expect(manager.isRouteProvisioned(updatedRoute)).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('NFTablesManager cleanup test', async () => {
|
||||
if (SKIP_TESTS) {
|
||||
console.log('Test skipped - requires root privileges to run NFTables commands');
|
||||
expect(true).toEqual(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop all NFTables rules
|
||||
await manager.stop();
|
||||
|
||||
// Get the status of the managed rules
|
||||
const status = await manager.getStatus();
|
||||
|
||||
// Verify there are no active rules
|
||||
expect(Object.keys(status).length).toEqual(0);
|
||||
});
|
||||
|
||||
export default tap.start();
|
162
test/test.nftables-status.ts
Normal file
162
test/test.nftables-status.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||
import { NFTablesManager } from '../ts/proxies/smart-proxy/nftables-manager.js';
|
||||
import { createNfTablesRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import * as child_process from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const exec = promisify(child_process.exec);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip tests if not root
|
||||
const isRoot = await checkRootPrivileges();
|
||||
if (!isRoot) {
|
||||
console.log('');
|
||||
console.log('========================================');
|
||||
console.log('NFTables status tests require root privileges');
|
||||
console.log('Skipping NFTables status tests');
|
||||
console.log('========================================');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
tap.test('NFTablesManager status functionality', async () => {
|
||||
const nftablesManager = new NFTablesManager();
|
||||
|
||||
// Create test routes
|
||||
const testRoutes = [
|
||||
createNfTablesRoute('test-route-1', { host: 'localhost', port: 8080 }, { ports: 9080 }),
|
||||
createNfTablesRoute('test-route-2', { host: 'localhost', port: 8081 }, { ports: 9081 }),
|
||||
createNfTablesRoute('test-route-3', { host: 'localhost', port: 8082 }, {
|
||||
ports: 9082,
|
||||
ipAllowList: ['127.0.0.1', '192.168.1.0/24']
|
||||
})
|
||||
];
|
||||
|
||||
// Get initial status (should be empty)
|
||||
let status = await nftablesManager.getStatus();
|
||||
expect(Object.keys(status).length).toEqual(0);
|
||||
|
||||
// Provision routes
|
||||
for (const route of testRoutes) {
|
||||
await nftablesManager.provisionRoute(route);
|
||||
}
|
||||
|
||||
// Get status after provisioning
|
||||
status = await nftablesManager.getStatus();
|
||||
expect(Object.keys(status).length).toEqual(3);
|
||||
|
||||
// Check status structure
|
||||
for (const routeStatus of Object.values(status)) {
|
||||
expect(routeStatus).toHaveProperty('active');
|
||||
expect(routeStatus).toHaveProperty('ruleCount');
|
||||
expect(routeStatus).toHaveProperty('lastUpdate');
|
||||
expect(routeStatus.active).toBeTrue();
|
||||
}
|
||||
|
||||
// Deprovision one route
|
||||
await nftablesManager.deprovisionRoute(testRoutes[0]);
|
||||
|
||||
// Check status after deprovisioning
|
||||
status = await nftablesManager.getStatus();
|
||||
expect(Object.keys(status).length).toEqual(2);
|
||||
|
||||
// Cleanup remaining routes
|
||||
await nftablesManager.stop();
|
||||
|
||||
// Final status should be empty
|
||||
status = await nftablesManager.getStatus();
|
||||
expect(Object.keys(status).length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('SmartProxy getNfTablesStatus functionality', async () => {
|
||||
const smartProxy = new SmartProxy({
|
||||
routes: [
|
||||
createNfTablesRoute('proxy-test-1', { host: 'localhost', port: 3000 }, { ports: 3001 }),
|
||||
createNfTablesRoute('proxy-test-2', { host: 'localhost', port: 3002 }, { ports: 3003 }),
|
||||
// Include a non-NFTables route to ensure it's not included in the status
|
||||
{
|
||||
name: 'non-nftables-route',
|
||||
match: { ports: 3004 },
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: { host: 'localhost', port: 3005 }
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Start the proxy
|
||||
await smartProxy.start();
|
||||
|
||||
// Get NFTables status
|
||||
const status = await smartProxy.getNfTablesStatus();
|
||||
|
||||
// Should only have 2 NFTables routes
|
||||
const statusKeys = Object.keys(status);
|
||||
expect(statusKeys.length).toEqual(2);
|
||||
|
||||
// Check that both NFTables routes are in the status
|
||||
const routeIds = statusKeys.sort();
|
||||
expect(routeIds).toContain('proxy-test-1:3001');
|
||||
expect(routeIds).toContain('proxy-test-2:3003');
|
||||
|
||||
// Verify status structure
|
||||
for (const [routeId, routeStatus] of Object.entries(status)) {
|
||||
expect(routeStatus).toHaveProperty('active', true);
|
||||
expect(routeStatus).toHaveProperty('ruleCount');
|
||||
expect(routeStatus.ruleCount).toHaveProperty('total');
|
||||
expect(routeStatus.ruleCount.total).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// Stop the proxy
|
||||
await smartProxy.stop();
|
||||
|
||||
// After stopping, status should be empty
|
||||
const finalStatus = await smartProxy.getNfTablesStatus();
|
||||
expect(Object.keys(finalStatus).length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('NFTables route update status tracking', async () => {
|
||||
const smartProxy = new SmartProxy({
|
||||
routes: [
|
||||
createNfTablesRoute('update-test', { host: 'localhost', port: 4000 }, { ports: 4001 })
|
||||
]
|
||||
});
|
||||
|
||||
await smartProxy.start();
|
||||
|
||||
// Get initial status
|
||||
let status = await smartProxy.getNfTablesStatus();
|
||||
expect(Object.keys(status).length).toEqual(1);
|
||||
const initialUpdate = status['update-test:4001'].lastUpdate;
|
||||
|
||||
// Wait a moment
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Update the route
|
||||
await smartProxy.updateRoutes([
|
||||
createNfTablesRoute('update-test', { host: 'localhost', port: 4002 }, { ports: 4001 })
|
||||
]);
|
||||
|
||||
// Get status after update
|
||||
status = await smartProxy.getNfTablesStatus();
|
||||
expect(Object.keys(status).length).toEqual(1);
|
||||
const updatedTime = status['update-test:4001'].lastUpdate;
|
||||
|
||||
// The update time should be different
|
||||
expect(updatedTime.getTime()).toBeGreaterThan(initialUpdate.getTime());
|
||||
|
||||
await smartProxy.stop();
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -82,7 +82,7 @@ tap.test('setup port proxy test environment', async () => {
|
||||
],
|
||||
defaults: {
|
||||
security: {
|
||||
allowedIps: ['127.0.0.1']
|
||||
ipAllowList: ['127.0.0.1']
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -121,7 +121,7 @@ tap.test('should forward TCP connections to custom host', async () => {
|
||||
],
|
||||
defaults: {
|
||||
security: {
|
||||
allowedIps: ['127.0.0.1']
|
||||
ipAllowList: ['127.0.0.1']
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -166,7 +166,7 @@ tap.test('should forward connections to custom IP', async () => {
|
||||
],
|
||||
defaults: {
|
||||
security: {
|
||||
allowedIps: ['127.0.0.1', '::ffff:127.0.0.1']
|
||||
ipAllowList: ['127.0.0.1', '::ffff:127.0.0.1']
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -261,7 +261,7 @@ tap.test('should support optional source IP preservation in chained proxies', as
|
||||
],
|
||||
defaults: {
|
||||
security: {
|
||||
allowedIps: ['127.0.0.1', '::ffff:127.0.0.1']
|
||||
ipAllowList: ['127.0.0.1', '::ffff:127.0.0.1']
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -282,7 +282,7 @@ tap.test('should support optional source IP preservation in chained proxies', as
|
||||
],
|
||||
defaults: {
|
||||
security: {
|
||||
allowedIps: ['127.0.0.1', '::ffff:127.0.0.1']
|
||||
ipAllowList: ['127.0.0.1', '::ffff:127.0.0.1']
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -320,7 +320,7 @@ tap.test('should support optional source IP preservation in chained proxies', as
|
||||
],
|
||||
defaults: {
|
||||
security: {
|
||||
allowedIps: ['127.0.0.1']
|
||||
ipAllowList: ['127.0.0.1']
|
||||
},
|
||||
preserveSourceIP: true
|
||||
},
|
||||
@ -343,7 +343,7 @@ tap.test('should support optional source IP preservation in chained proxies', as
|
||||
],
|
||||
defaults: {
|
||||
security: {
|
||||
allowedIps: ['127.0.0.1']
|
||||
ipAllowList: ['127.0.0.1']
|
||||
},
|
||||
preserveSourceIP: true
|
||||
},
|
||||
|
Reference in New Issue
Block a user