Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
4568623600 | |||
ddcfb2f00d | |||
a2e3e38025 | |||
cf96ff8a47 | |||
94e9eafa25 | |||
3e411667e6 | |||
35d7dfcedf | |||
1067177d82 | |||
ac3a888453 |
36
changelog.md
36
changelog.md
@ -1,5 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-05-15 - 18.0.2 - fix(smartproxy)
|
||||
Update project documentation and internal configuration files; no functional changes.
|
||||
|
||||
- Synchronized readme, hints, and configuration metadata with current implementation
|
||||
- Updated tests and commit info details to reflect project structure
|
||||
|
||||
## 2025-05-15 - 18.0.1 - fix(smartproxy)
|
||||
Consolidate duplicate IRouteSecurity interfaces to use standardized property names (ipAllowList and ipBlockList), fix port preservation logic for 'preserve' mode in forward actions, and update dependency versions in package.json.
|
||||
|
||||
- Unified the duplicate IRouteSecurity interfaces into a single definition using ipAllowList and ipBlockList.
|
||||
- Updated security checks (e.g. isClientIpAllowed) to use the new standardized property names.
|
||||
- Fixed the resolvePort function to properly handle 'preserve' mode and function-based port mapping.
|
||||
- Bumped dependency versions: @push.rocks/smartacme from 7.3.2 to 7.3.3 and @types/node to 22.15.18, and updated tsbuild from 2.3.2 to 2.4.1.
|
||||
- Revised documentation and changelog to reflect the interface consolidation and bug fixes.
|
||||
|
||||
## 2025-05-15 - 18.0.0 - BREAKING CHANGE(IRouteSecurity)
|
||||
Consolidate duplicated IRouteSecurity interfaces by unifying property names (using 'ipAllowList' and 'ipBlockList' exclusively) and removing legacy definitions, updating security checks throughout the codebase to handle IPv6-mapped IPv4 addresses and cleaning up deprecated forwarding helpers.
|
||||
|
||||
- Unified duplicate IRouteSecurity definitions into a single interface with consistent property names.
|
||||
- Replaced 'allowedIps' and 'blockedIps' with 'ipAllowList' and 'ipBlockList' respectively.
|
||||
- Updated references in security and route managers to use the new properties.
|
||||
- Ensured consistent IPv6-mapped IPv4 normalization in IP security checks.
|
||||
- Removed deprecated helpers and legacy code affecting port forwarding and route migration.
|
||||
|
||||
## 2025-05-15 - 17.0.0 - BREAKING CHANGE(smartproxy)
|
||||
Remove legacy migration utilities and deprecated forwarding helpers; consolidate route utilities, streamline interface definitions, and normalize IPv6-mapped IPv4 addresses
|
||||
|
||||
- Deleted ts/proxies/smart-proxy/utils/route-migration-utils.ts and removed its re-exports
|
||||
- Removed deprecated helper functions (httpOnly, tlsTerminateToHttp, tlsTerminateToHttps, httpsPassthrough) from ts/forwarding/config/forwarding-types.ts
|
||||
- Updated ts/common/port80-adapter.ts to consistently normalize IPv6-mapped IPv4 addresses in IP comparisons
|
||||
- Cleaned up legacy connection handling code in route-connection-handler.ts by removing unused parameters and obsolete comments
|
||||
- Consolidated route utilities by replacing imports from route-helpers.js with route-patterns.js in multiple modules
|
||||
- Simplified interface definitions by removing legacy aliases and type checking functions from models/interfaces.ts
|
||||
- Enhanced type safety by replacing any remaining 'any' types with specific types throughout the codebase
|
||||
- Updated documentation comments and removed references to deprecated functionality
|
||||
|
||||
## 2025-05-14 - 16.0.4 - fix(smartproxy)
|
||||
Update dynamic port mapping to support 'preserve' target port value
|
||||
|
||||
|
214
examples/nftables-integration.ts
Normal file
214
examples/nftables-integration.ts
Normal file
@ -0,0 +1,214 @@
|
||||
/**
|
||||
* NFTables Integration Example
|
||||
*
|
||||
* This example demonstrates how to use the NFTables forwarding engine with SmartProxy
|
||||
* for high-performance network routing that operates at the kernel level.
|
||||
*
|
||||
* NOTE: This requires elevated privileges to run (sudo) as it interacts with nftables.
|
||||
*/
|
||||
|
||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||
import {
|
||||
createNfTablesRoute,
|
||||
createNfTablesTerminateRoute,
|
||||
createCompleteNfTablesHttpsServer
|
||||
} from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
||||
|
||||
// Simple NFTables-based HTTP forwarding example
|
||||
async function simpleForwardingExample() {
|
||||
console.log('Starting simple NFTables forwarding example...');
|
||||
|
||||
// Create a SmartProxy instance with a simple NFTables route
|
||||
const proxy = new SmartProxy({
|
||||
routes: [
|
||||
createNfTablesRoute('example.com', {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}, {
|
||||
ports: 80,
|
||||
protocol: 'tcp',
|
||||
preserveSourceIP: true,
|
||||
tableName: 'smartproxy_example'
|
||||
})
|
||||
],
|
||||
enableDetailedLogging: true
|
||||
});
|
||||
|
||||
// Start the proxy
|
||||
await proxy.start();
|
||||
console.log('NFTables proxy started. Press Ctrl+C to stop.');
|
||||
|
||||
// Handle shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('Stopping proxy...');
|
||||
await proxy.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// HTTPS termination example with NFTables
|
||||
async function httpsTerminationExample() {
|
||||
console.log('Starting HTTPS termination with NFTables example...');
|
||||
|
||||
// Create a SmartProxy instance with an HTTPS termination route using NFTables
|
||||
const proxy = new SmartProxy({
|
||||
routes: [
|
||||
createNfTablesTerminateRoute('secure.example.com', {
|
||||
host: 'localhost',
|
||||
port: 8443
|
||||
}, {
|
||||
ports: 443,
|
||||
certificate: 'auto', // Automatic certificate provisioning
|
||||
tableName: 'smartproxy_https'
|
||||
})
|
||||
],
|
||||
enableDetailedLogging: true
|
||||
});
|
||||
|
||||
// Start the proxy
|
||||
await proxy.start();
|
||||
console.log('HTTPS termination proxy started. Press Ctrl+C to stop.');
|
||||
|
||||
// Handle shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('Stopping proxy...');
|
||||
await proxy.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// Complete HTTPS server with HTTP redirects using NFTables
|
||||
async function completeHttpsServerExample() {
|
||||
console.log('Starting complete HTTPS server with NFTables example...');
|
||||
|
||||
// Create a SmartProxy instance with a complete HTTPS server
|
||||
const proxy = new SmartProxy({
|
||||
routes: createCompleteNfTablesHttpsServer('complete.example.com', {
|
||||
host: 'localhost',
|
||||
port: 8443
|
||||
}, {
|
||||
certificate: 'auto',
|
||||
tableName: 'smartproxy_complete'
|
||||
}),
|
||||
enableDetailedLogging: true
|
||||
});
|
||||
|
||||
// Start the proxy
|
||||
await proxy.start();
|
||||
console.log('Complete HTTPS server started. Press Ctrl+C to stop.');
|
||||
|
||||
// Handle shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('Stopping proxy...');
|
||||
await proxy.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// Load balancing example with NFTables
|
||||
async function loadBalancingExample() {
|
||||
console.log('Starting load balancing with NFTables example...');
|
||||
|
||||
// Create a SmartProxy instance with a load balancing configuration
|
||||
const proxy = new SmartProxy({
|
||||
routes: [
|
||||
createNfTablesRoute('lb.example.com', {
|
||||
// NFTables will automatically distribute connections to these hosts
|
||||
host: 'backend1.example.com',
|
||||
port: 8080
|
||||
}, {
|
||||
ports: 80,
|
||||
tableName: 'smartproxy_lb'
|
||||
})
|
||||
],
|
||||
enableDetailedLogging: true
|
||||
});
|
||||
|
||||
// Start the proxy
|
||||
await proxy.start();
|
||||
console.log('Load balancing proxy started. Press Ctrl+C to stop.');
|
||||
|
||||
// Handle shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('Stopping proxy...');
|
||||
await proxy.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// Advanced example with QoS and security settings
|
||||
async function advancedExample() {
|
||||
console.log('Starting advanced NFTables example with QoS and security...');
|
||||
|
||||
// Create a SmartProxy instance with advanced settings
|
||||
const proxy = new SmartProxy({
|
||||
routes: [
|
||||
createNfTablesRoute('advanced.example.com', {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}, {
|
||||
ports: 80,
|
||||
protocol: 'tcp',
|
||||
preserveSourceIP: true,
|
||||
maxRate: '10mbps', // QoS rate limiting
|
||||
priority: 2, // QoS priority (1-10, lower is higher priority)
|
||||
ipAllowList: ['192.168.1.0/24'], // Only allow this subnet
|
||||
ipBlockList: ['192.168.1.100'], // Block this specific IP
|
||||
useIPSets: true, // Use IP sets for more efficient rule processing
|
||||
useAdvancedNAT: true, // Use connection tracking for stateful NAT
|
||||
tableName: 'smartproxy_advanced'
|
||||
})
|
||||
],
|
||||
enableDetailedLogging: true
|
||||
});
|
||||
|
||||
// Start the proxy
|
||||
await proxy.start();
|
||||
console.log('Advanced NFTables proxy started. Press Ctrl+C to stop.');
|
||||
|
||||
// Handle shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('Stopping proxy...');
|
||||
await proxy.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// Run one of the examples based on the command line argument
|
||||
async function main() {
|
||||
const example = process.argv[2] || 'simple';
|
||||
|
||||
switch (example) {
|
||||
case 'simple':
|
||||
await simpleForwardingExample();
|
||||
break;
|
||||
case 'https':
|
||||
await httpsTerminationExample();
|
||||
break;
|
||||
case 'complete':
|
||||
await completeHttpsServerExample();
|
||||
break;
|
||||
case 'lb':
|
||||
await loadBalancingExample();
|
||||
break;
|
||||
case 'advanced':
|
||||
await advancedExample();
|
||||
break;
|
||||
default:
|
||||
console.error('Unknown example:', example);
|
||||
console.log('Available examples: simple, https, complete, lb, advanced');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if running as root/sudo
|
||||
if (process.getuid && process.getuid() !== 0) {
|
||||
console.error('This example requires root privileges to modify nftables rules.');
|
||||
console.log('Please run with sudo: sudo tsx examples/nftables-integration.ts');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Error running example:', err);
|
||||
process.exit(1);
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "16.0.4",
|
||||
"version": "18.0.2",
|
||||
"private": false,
|
||||
"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.",
|
||||
"main": "dist_ts/index.js",
|
||||
@ -15,16 +15,16 @@
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.3.2",
|
||||
"@git.zone/tsbuild": "^2.5.0",
|
||||
"@git.zone/tsrun": "^1.2.44",
|
||||
"@git.zone/tstest": "^1.0.77",
|
||||
"@push.rocks/tapbundle": "^6.0.3",
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/node": "^22.15.18",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/lik": "^6.2.2",
|
||||
"@push.rocks/smartacme": "^7.3.2",
|
||||
"@push.rocks/smartacme": "^7.3.3",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartnetwork": "^4.0.1",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
|
303
pnpm-lock.yaml
generated
303
pnpm-lock.yaml
generated
@ -12,8 +12,8 @@ importers:
|
||||
specifier: ^6.2.2
|
||||
version: 6.2.2
|
||||
'@push.rocks/smartacme':
|
||||
specifier: ^7.3.2
|
||||
version: 7.3.2(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
specifier: ^7.3.3
|
||||
version: 7.3.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
'@push.rocks/smartdelay':
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
@ -52,8 +52,8 @@ importers:
|
||||
version: 8.18.2
|
||||
devDependencies:
|
||||
'@git.zone/tsbuild':
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
'@git.zone/tsrun':
|
||||
specifier: ^1.2.44
|
||||
version: 1.3.3
|
||||
@ -64,8 +64,8 @@ importers:
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
'@types/node':
|
||||
specifier: ^22.15.3
|
||||
version: 22.15.3
|
||||
specifier: ^22.15.18
|
||||
version: 22.15.18
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
@ -355,8 +355,8 @@ packages:
|
||||
'@cloudflare/workers-types@4.20250303.0':
|
||||
resolution: {integrity: sha512-O7F7nRT4bbmwHf3gkRBLfJ7R6vHIJ/oZzWdby6obOiw2yavUfp/AIwS7aO2POu5Cv8+h3TXS3oHs3kKCZLraUA==}
|
||||
|
||||
'@cloudflare/workers-types@4.20250505.0':
|
||||
resolution: {integrity: sha512-pLQ/UaCupEy3fTTfy7yCR7FuAbawvCohYAdadGHPUfzssksA9MhkqBLlzYWRwIoC34R8grVn4XOCknEg+NMr0Q==}
|
||||
'@cloudflare/workers-types@4.20250515.0':
|
||||
resolution: {integrity: sha512-KoHFMH04gOXp3KEI+wrFIU+3ZfoSXnwqZTpybNQjalHoN3pWjtWBb/030cCRAZ639YX+DAHAxNF7AvEYGz1oaA==}
|
||||
|
||||
'@colors/colors@1.6.0':
|
||||
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||
@ -680,8 +680,8 @@ packages:
|
||||
'@esm-bundle/chai@4.3.4-fix.0':
|
||||
resolution: {integrity: sha512-26SKdM4uvDWlY8/OOOxSB1AqQWeBosCX3wRYUZO7enTAj03CtVxIiCimYVG2WpULcyV51qapK4qTovwkUr5Mlw==}
|
||||
|
||||
'@git.zone/tsbuild@2.3.2':
|
||||
resolution: {integrity: sha512-PG7N39/MkpIKGgRvT2MC7eyLHMcoofaQJQgUlJzicp62Wfk2W9qbnI8Xexb52uy7zvmndao/G4xZ391exJAj+A==}
|
||||
'@git.zone/tsbuild@2.5.0':
|
||||
resolution: {integrity: sha512-4IL81yMtOdyA9hp/OLpo8t1svj/hjQhlTOWy5Y0S147GXKoGj2lD6/HZaxJ98nzlf/uQ1utQAcRb31KaC6misw==}
|
||||
hasBin: true
|
||||
|
||||
'@git.zone/tsbundle@2.2.5':
|
||||
@ -872,8 +872,8 @@ packages:
|
||||
'@push.rocks/qenv@6.1.0':
|
||||
resolution: {integrity: sha512-1FUFMlSVwFSFg8LbqfkzJ2LLP4lMGApUtgOpsvrde6+AxBmB4gjoNgCUH7z3xXfDAtYqcrtSELXBNE0xVL1MqQ==}
|
||||
|
||||
'@push.rocks/smartacme@7.3.2':
|
||||
resolution: {integrity: sha512-pfNd31wqvEn/2Bi9qZGCzvpV6/5V1jB9xOuWlsUTp4RihDVwQq2/se69pUeXDd1smWOM1yF4zq+45VO5DMDsCg==}
|
||||
'@push.rocks/smartacme@7.3.3':
|
||||
resolution: {integrity: sha512-48g9V4EpZI8B/YiPseIuB/balH22IMp/p26+DAE57jxvv1hh+PSV4I/UPWCPWP/z7OTtKF+EfEco9TEb5BF2Lw==}
|
||||
|
||||
'@push.rocks/smartarchive@3.0.8':
|
||||
resolution: {integrity: sha512-1jPmR0b7hXmjYQoRiTlRXrIbZcdcFmSdGOfznufjcDpGPe86Km0d8TBnzqghTx4dTihzKC67IxAaz/DM3lvxpA==}
|
||||
@ -896,6 +896,9 @@ packages:
|
||||
'@push.rocks/smartcli@4.0.11':
|
||||
resolution: {integrity: sha512-KDWfUqWBoUZsOEtsDx36d6qc8GG7Zo5E+HHamYY68KVDO8BMu6jbBucoUUPDksczLEmbXKLmroBP1mn/xozQOA==}
|
||||
|
||||
'@push.rocks/smartclickhouse@2.0.17':
|
||||
resolution: {integrity: sha512-IYO8Obor/Ruam2KQ2B/+5uQ+rL0exU5KZoSgOc3jkkrfjn+zZenN2xoV8lVqavAtxZVfG7MfxFrcv6I7I9ZMmA==}
|
||||
|
||||
'@push.rocks/smartcrypto@2.0.4':
|
||||
resolution: {integrity: sha512-1+/5bsjyataf5uUkUNnnVXGRAt+gHVk1KDzozjTqgqJxHvQk1d9fVDohL6CxUhUucTPtu5VR5xNBiV8YCDuGyw==}
|
||||
|
||||
@ -953,6 +956,9 @@ packages:
|
||||
'@push.rocks/smartlog@3.0.7':
|
||||
resolution: {integrity: sha512-WHOw0iHHjCEbYY4KGX40iFtLI11QJvvWIbC9yFn3Mt+nrdupMnry7Ztc5v/PqO8lu33Q6xDBMXiNQ9yNY0HVGw==}
|
||||
|
||||
'@push.rocks/smartlog@3.0.9':
|
||||
resolution: {integrity: sha512-B/YIJrwXsbxPkAJly8+55yx3Eqm5bIaCZ/xD2oe6fD8Zp58VLF2P8hpoQZJOiSO+KI7wXVlTEFHsmt8fpRZIVA==}
|
||||
|
||||
'@push.rocks/smartmanifest@2.0.2':
|
||||
resolution: {integrity: sha512-QGc5C9vunjfUbYsPGz5bynV/mVmPHkrQDkWp8ZO8VJtK1GZe+njgbrNyxn2SUHR0IhSAbSXl1j4JvBqYf5eTVg==}
|
||||
|
||||
@ -1742,11 +1748,11 @@ packages:
|
||||
'@types/node-forge@1.3.11':
|
||||
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
|
||||
|
||||
'@types/node@18.19.87':
|
||||
resolution: {integrity: sha512-OIAAu6ypnVZHmsHCeJ+7CCSub38QNBS9uceMQeg7K5Ur0Jr+wG9wEOEvvMbhp09pxD5czIUy/jND7s7Tb6Nw7A==}
|
||||
'@types/node@18.19.100':
|
||||
resolution: {integrity: sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA==}
|
||||
|
||||
'@types/node@22.15.3':
|
||||
resolution: {integrity: sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==}
|
||||
'@types/node@22.15.18':
|
||||
resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==}
|
||||
|
||||
'@types/parse5@6.0.3':
|
||||
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
|
||||
@ -2118,6 +2124,10 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chalk@5.4.1:
|
||||
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
character-entities-html4@2.1.0:
|
||||
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
|
||||
|
||||
@ -2160,6 +2170,14 @@ packages:
|
||||
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
cli-spinners@2.9.2:
|
||||
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
@ -2340,6 +2358,15 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
debug@4.4.1:
|
||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decode-named-character-reference@1.1.0:
|
||||
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
|
||||
|
||||
@ -2447,6 +2474,9 @@ packages:
|
||||
elliptic@6.6.1:
|
||||
resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==}
|
||||
|
||||
emoji-regex@10.4.0:
|
||||
resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
@ -2762,6 +2792,10 @@ packages:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
get-east-asian-width@1.3.0:
|
||||
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -3018,6 +3052,10 @@ packages:
|
||||
resolution: {integrity: sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-interactive@2.0.0:
|
||||
resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
is-ip@3.1.0:
|
||||
resolution: {integrity: sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==}
|
||||
engines: {node: '>=8'}
|
||||
@ -3066,6 +3104,10 @@ packages:
|
||||
resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
is-unicode-supported@1.3.0:
|
||||
resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
is-unicode-supported@2.1.0:
|
||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||
engines: {node: '>=18'}
|
||||
@ -3281,6 +3323,10 @@ packages:
|
||||
lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
log-symbols@6.0.0:
|
||||
resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
log-update@4.0.0:
|
||||
resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==}
|
||||
engines: {node: '>=10'}
|
||||
@ -3519,6 +3565,10 @@ packages:
|
||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
mimic-function@5.0.1:
|
||||
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -3716,6 +3766,10 @@ packages:
|
||||
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
onetime@7.0.0:
|
||||
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
only@0.0.2:
|
||||
resolution: {integrity: sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=}
|
||||
|
||||
@ -3723,6 +3777,10 @@ packages:
|
||||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ora@8.2.0:
|
||||
resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
p-cancelable@3.0.0:
|
||||
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
|
||||
engines: {node: '>=12.20'}
|
||||
@ -4066,6 +4124,10 @@ packages:
|
||||
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
restore-cursor@5.1.0:
|
||||
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
reusify@1.1.0:
|
||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
@ -4117,6 +4179,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.2:
|
||||
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
send@0.19.0:
|
||||
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@ -4243,6 +4310,10 @@ packages:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
stdin-discarder@0.2.2:
|
||||
resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
stream-shift@1.0.3:
|
||||
resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
|
||||
|
||||
@ -4261,6 +4332,10 @@ packages:
|
||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
string-width@7.2.0:
|
||||
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
|
||||
@ -4434,6 +4509,10 @@ packages:
|
||||
resolution: {integrity: sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
type-fest@4.41.0:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
type-is@1.6.18:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -4760,7 +4839,7 @@ snapshots:
|
||||
'@api.global/typedrequest': 3.1.10
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 3.0.1
|
||||
'@cloudflare/workers-types': 4.20250505.0
|
||||
'@cloudflare/workers-types': 4.20250515.0
|
||||
'@design.estate/dees-comms': 1.0.27
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartchok': 1.0.34
|
||||
@ -4769,7 +4848,7 @@ snapshots:
|
||||
'@push.rocks/smartfeed': 1.0.11
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartjson': 5.0.20
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartlog': 3.0.9
|
||||
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||
'@push.rocks/smartmanifest': 2.0.2
|
||||
@ -4823,7 +4902,7 @@ snapshots:
|
||||
'@apiclient.xyz/cloudflare@6.4.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartlog': 3.0.9
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@push.rocks/smartstring': 4.0.15
|
||||
@ -5671,7 +5750,7 @@ snapshots:
|
||||
|
||||
'@cloudflare/workers-types@4.20250303.0': {}
|
||||
|
||||
'@cloudflare/workers-types@4.20250505.0': {}
|
||||
'@cloudflare/workers-types@4.20250515.0': {}
|
||||
|
||||
'@colors/colors@1.6.0': {}
|
||||
|
||||
@ -5884,14 +5963,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/chai': 4.3.20
|
||||
|
||||
'@git.zone/tsbuild@2.3.2':
|
||||
'@git.zone/tsbuild@2.5.0':
|
||||
dependencies:
|
||||
'@git.zone/tspublish': 1.9.1
|
||||
'@push.rocks/early': 4.0.4
|
||||
'@push.rocks/smartcli': 4.0.11
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartlog': 3.0.9
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
typescript: 5.7.3
|
||||
@ -5921,7 +6000,7 @@ snapshots:
|
||||
'@push.rocks/smartcli': 4.0.11
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartlog': 3.0.9
|
||||
'@push.rocks/smartnpm': 2.0.4
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
@ -5997,7 +6076,7 @@ snapshots:
|
||||
'@jest/schemas': 29.6.3
|
||||
'@types/istanbul-lib-coverage': 2.0.6
|
||||
'@types/istanbul-reports': 3.0.4
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
'@types/yargs': 17.0.33
|
||||
chalk: 4.1.2
|
||||
|
||||
@ -6284,7 +6363,7 @@ snapshots:
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
|
||||
'@push.rocks/smartacme@7.3.2(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)':
|
||||
'@push.rocks/smartacme@7.3.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)':
|
||||
dependencies:
|
||||
'@api.global/typedserver': 3.0.74
|
||||
'@apiclient.xyz/cloudflare': 6.4.1
|
||||
@ -6293,7 +6372,7 @@ snapshots:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartdns': 6.2.2
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartlog': 3.0.9
|
||||
'@push.rocks/smartnetwork': 4.0.1
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
@ -6306,7 +6385,6 @@ snapshots:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- aws-crt
|
||||
- bufferutil
|
||||
- encoding
|
||||
- gcp-metadata
|
||||
@ -6389,6 +6467,15 @@ snapshots:
|
||||
'@push.rocks/smartrx': 3.0.7
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
'@push.rocks/smartclickhouse@2.0.17':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartobject': 1.0.12
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smarturl': 3.1.0
|
||||
'@push.rocks/webrequest': 3.0.37
|
||||
|
||||
'@push.rocks/smartcrypto@2.0.4':
|
||||
dependencies:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
@ -6548,6 +6635,20 @@ snapshots:
|
||||
'@push.rocks/isounique': 1.0.5
|
||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||
|
||||
'@push.rocks/smartlog@3.0.9':
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@push.rocks/consolecolor': 2.0.2
|
||||
'@push.rocks/isounique': 1.0.5
|
||||
'@push.rocks/smartclickhouse': 2.0.17
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smarthash': 3.0.4
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
'@push.rocks/webrequest': 3.0.37
|
||||
'@tsclass/tsclass': 9.2.0
|
||||
ora: 8.2.0
|
||||
|
||||
'@push.rocks/smartmanifest@2.0.2': {}
|
||||
|
||||
'@push.rocks/smartmarkdown@3.0.3':
|
||||
@ -6843,7 +6944,7 @@ snapshots:
|
||||
'@push.rocks/smartversion@3.0.5':
|
||||
dependencies:
|
||||
'@types/semver': 7.7.0
|
||||
semver: 7.7.1
|
||||
semver: 7.7.2
|
||||
|
||||
'@push.rocks/smartxml@1.1.1':
|
||||
dependencies:
|
||||
@ -7732,7 +7833,7 @@ snapshots:
|
||||
|
||||
'@tsclass/tsclass@5.0.0':
|
||||
dependencies:
|
||||
type-fest: 4.40.1
|
||||
type-fest: 4.41.0
|
||||
|
||||
'@tsclass/tsclass@8.2.1':
|
||||
dependencies:
|
||||
@ -7744,18 +7845,18 @@ snapshots:
|
||||
|
||||
'@types/accepts@1.3.7':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/babel__code-frame@7.0.6': {}
|
||||
|
||||
'@types/bn.js@5.1.6':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/body-parser@1.19.5':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/buffer-json@2.0.3': {}
|
||||
|
||||
@ -7771,17 +7872,17 @@ snapshots:
|
||||
|
||||
'@types/clean-css@4.2.11':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
source-map: 0.6.1
|
||||
|
||||
'@types/co-body@6.1.3':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
'@types/qs': 6.9.18
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/content-disposition@0.5.8': {}
|
||||
|
||||
@ -7794,11 +7895,11 @@ snapshots:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/express': 5.0.0
|
||||
'@types/keygrip': 1.0.6
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/cors@2.8.17':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/debounce@1.2.4': {}
|
||||
|
||||
@ -7814,7 +7915,7 @@ snapshots:
|
||||
|
||||
'@types/dns-packet@5.6.5':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/elliptic@6.4.18':
|
||||
dependencies:
|
||||
@ -7822,14 +7923,14 @@ snapshots:
|
||||
|
||||
'@types/express-serve-static-core@4.19.6':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
'@types/qs': 6.9.18
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 0.17.4
|
||||
|
||||
'@types/express-serve-static-core@5.0.6':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
'@types/qs': 6.9.18
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 0.17.4
|
||||
@ -7860,30 +7961,30 @@ snapshots:
|
||||
|
||||
'@types/from2@2.3.5':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/fs-extra@9.0.13':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/glob@7.2.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/glob@8.1.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/gunzip-maybe@1.4.2':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
@ -7917,7 +8018,7 @@ snapshots:
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/keygrip@1.0.6': {}
|
||||
|
||||
@ -7934,7 +8035,7 @@ snapshots:
|
||||
'@types/http-errors': 2.0.4
|
||||
'@types/keygrip': 1.0.6
|
||||
'@types/koa-compose': 3.2.8
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
dependencies:
|
||||
@ -7952,18 +8053,18 @@ snapshots:
|
||||
|
||||
'@types/node-fetch@2.6.12':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
form-data: 4.0.2
|
||||
|
||||
'@types/node-forge@1.3.11':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/node@18.19.87':
|
||||
'@types/node@18.19.100':
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/node@22.15.3':
|
||||
'@types/node@22.15.18':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
@ -7981,19 +8082,19 @@ snapshots:
|
||||
|
||||
'@types/s3rver@3.7.4':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/semver@7.7.0': {}
|
||||
|
||||
'@types/send@0.17.4':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/serve-static@1.15.7':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.4
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
'@types/send': 0.17.4
|
||||
|
||||
'@types/sinon-chai@3.2.12':
|
||||
@ -8013,11 +8114,11 @@ snapshots:
|
||||
|
||||
'@types/tar-stream@2.2.3':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/through2@2.0.41':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/triple-beam@1.3.5': {}
|
||||
|
||||
@ -8041,18 +8142,18 @@ snapshots:
|
||||
|
||||
'@types/whatwg-url@8.2.2':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
'@types/webidl-conversions': 7.0.3
|
||||
|
||||
'@types/which@3.0.4': {}
|
||||
|
||||
'@types/ws@7.4.7':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/yargs-parser@21.0.3': {}
|
||||
|
||||
@ -8062,7 +8163,7 @@ snapshots:
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
optional: true
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
@ -8156,8 +8257,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@peculiar/x509': 1.12.3
|
||||
asn1js: 3.0.6
|
||||
axios: 1.9.0(debug@4.4.0)
|
||||
debug: 4.4.0
|
||||
axios: 1.9.0(debug@4.4.1)
|
||||
debug: 4.4.1
|
||||
node-forge: 1.3.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -8227,9 +8328,9 @@ snapshots:
|
||||
|
||||
axe-core@4.10.3: {}
|
||||
|
||||
axios@1.9.0(debug@4.4.0):
|
||||
axios@1.9.0(debug@4.4.1):
|
||||
dependencies:
|
||||
follow-redirects: 1.15.9(debug@4.4.0)
|
||||
follow-redirects: 1.15.9(debug@4.4.1)
|
||||
form-data: 4.0.2
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
@ -8409,6 +8510,8 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chalk@5.4.1: {}
|
||||
|
||||
character-entities-html4@2.1.0: {}
|
||||
|
||||
character-entities-legacy@3.0.0: {}
|
||||
@ -8443,6 +8546,12 @@ snapshots:
|
||||
dependencies:
|
||||
restore-cursor: 3.1.0
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
dependencies:
|
||||
restore-cursor: 5.1.0
|
||||
|
||||
cli-spinners@2.9.2: {}
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@ -8457,7 +8566,7 @@ snapshots:
|
||||
|
||||
cloudflare@4.2.0:
|
||||
dependencies:
|
||||
'@types/node': 18.19.87
|
||||
'@types/node': 18.19.100
|
||||
'@types/node-fetch': 2.6.12
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.6.0
|
||||
@ -8600,6 +8709,10 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decode-named-character-reference@1.1.0:
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
@ -8703,6 +8816,8 @@ snapshots:
|
||||
minimalistic-assert: 1.0.1
|
||||
minimalistic-crypto-utils: 1.0.1
|
||||
|
||||
emoji-regex@10.4.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
@ -8735,7 +8850,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/cookie': 0.4.1
|
||||
'@types/cors': 2.8.17
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
cookie: 0.4.2
|
||||
@ -9031,6 +9146,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
debug: 4.4.0
|
||||
|
||||
follow-redirects@1.15.9(debug@4.4.1):
|
||||
optionalDependencies:
|
||||
debug: 4.4.1
|
||||
|
||||
foreground-child@2.0.0:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
@ -9101,6 +9220,8 @@ snapshots:
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-east-asian-width@1.3.0: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@ -9428,6 +9549,8 @@ snapshots:
|
||||
|
||||
is-gzip@1.0.0: {}
|
||||
|
||||
is-interactive@2.0.0: {}
|
||||
|
||||
is-ip@3.1.0:
|
||||
dependencies:
|
||||
ip-regex: 4.3.0
|
||||
@ -9467,6 +9590,8 @@ snapshots:
|
||||
|
||||
is-stream@4.0.1: {}
|
||||
|
||||
is-unicode-supported@1.3.0: {}
|
||||
|
||||
is-unicode-supported@2.1.0: {}
|
||||
|
||||
is-windows@1.0.2: {}
|
||||
@ -9539,7 +9664,7 @@ snapshots:
|
||||
jest-util@29.7.0:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.15.3
|
||||
'@types/node': 22.15.18
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
graceful-fs: 4.2.11
|
||||
@ -9728,6 +9853,11 @@ snapshots:
|
||||
|
||||
lodash@4.17.21: {}
|
||||
|
||||
log-symbols@6.0.0:
|
||||
dependencies:
|
||||
chalk: 5.4.1
|
||||
is-unicode-supported: 1.3.0
|
||||
|
||||
log-update@4.0.0:
|
||||
dependencies:
|
||||
ansi-escapes: 4.3.2
|
||||
@ -10138,6 +10268,8 @@ snapshots:
|
||||
|
||||
mimic-fn@2.1.0: {}
|
||||
|
||||
mimic-function@5.0.1: {}
|
||||
|
||||
mimic-response@3.1.0: {}
|
||||
|
||||
mimic-response@4.0.0: {}
|
||||
@ -10315,6 +10447,10 @@ snapshots:
|
||||
dependencies:
|
||||
mimic-fn: 2.1.0
|
||||
|
||||
onetime@7.0.0:
|
||||
dependencies:
|
||||
mimic-function: 5.0.1
|
||||
|
||||
only@0.0.2: {}
|
||||
|
||||
open@8.4.2:
|
||||
@ -10323,6 +10459,18 @@ snapshots:
|
||||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
|
||||
ora@8.2.0:
|
||||
dependencies:
|
||||
chalk: 5.4.1
|
||||
cli-cursor: 5.0.0
|
||||
cli-spinners: 2.9.2
|
||||
is-interactive: 2.0.0
|
||||
is-unicode-supported: 2.1.0
|
||||
log-symbols: 6.0.0
|
||||
stdin-discarder: 0.2.2
|
||||
string-width: 7.2.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
p-cancelable@3.0.0: {}
|
||||
|
||||
p-event@4.2.0:
|
||||
@ -10375,7 +10523,7 @@ snapshots:
|
||||
got: 12.6.1
|
||||
registry-auth-token: 5.1.0
|
||||
registry-url: 6.0.1
|
||||
semver: 7.7.1
|
||||
semver: 7.7.2
|
||||
|
||||
pako@0.2.9: {}
|
||||
|
||||
@ -10710,6 +10858,11 @@ snapshots:
|
||||
onetime: 5.1.2
|
||||
signal-exit: 3.0.7
|
||||
|
||||
restore-cursor@5.1.0:
|
||||
dependencies:
|
||||
onetime: 7.0.0
|
||||
signal-exit: 4.1.0
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
rimraf@3.0.2:
|
||||
@ -10765,6 +10918,8 @@ snapshots:
|
||||
|
||||
semver@7.7.1: {}
|
||||
|
||||
semver@7.7.2: {}
|
||||
|
||||
send@0.19.0:
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
@ -10944,6 +11099,8 @@ snapshots:
|
||||
|
||||
statuses@2.0.1: {}
|
||||
|
||||
stdin-discarder@0.2.2: {}
|
||||
|
||||
stream-shift@1.0.3: {}
|
||||
|
||||
streamsearch@0.1.2: {}
|
||||
@ -10967,6 +11124,12 @@ snapshots:
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
string-width@7.2.0:
|
||||
dependencies:
|
||||
emoji-regex: 10.4.0
|
||||
get-east-asian-width: 1.3.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
string_decoder@1.1.1:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
@ -11144,6 +11307,8 @@ snapshots:
|
||||
|
||||
type-fest@4.40.1: {}
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
type-is@1.6.18:
|
||||
dependencies:
|
||||
media-typer: 0.3.0
|
||||
|
725
readme.plan.md
725
readme.plan.md
@ -1,103 +1,654 @@
|
||||
# SmartProxy Configuration Troubleshooting
|
||||
# NFTables-SmartProxy Integration Plan
|
||||
|
||||
## IPv6/IPv4 Mapping Issue
|
||||
## Overview
|
||||
|
||||
### Problem Identified
|
||||
The SmartProxy is failing to match connections for wildcard domains (like `*.lossless.digital`) when IP restrictions are in place. After extensive debugging, the root cause has been identified:
|
||||
This document outlines a comprehensive plan to integrate the existing NFTables functionality with the SmartProxy core to provide advanced network-level routing capabilities. The NFTables proxy already exists in the codebase but is not fully integrated with the SmartProxy routing system. This integration will allow SmartProxy to leverage the power of Linux's NFTables firewall system for high-performance port forwarding, load balancing, and security filtering.
|
||||
|
||||
When a connection comes in from an IPv4 address (e.g., `212.95.99.130`), the Node.js server receives it as an IPv6-mapped IPv4 address with the format `::ffff:212.95.99.130`. However, the route configuration is expecting the exact string `212.95.99.130`, causing a mismatch.
|
||||
## Current State
|
||||
|
||||
From the debug logs:
|
||||
```
|
||||
[DEBUG] Route rejected: clientIp mismatch. Request: ::ffff:212.95.99.130, Route patterns: ["212.95.99.130"]
|
||||
```
|
||||
1. **NFTablesProxy**: A standalone implementation exists in `ts/proxies/nftables-proxy/` with its own configuration and API.
|
||||
2. **SmartProxy**: The main routing system with route-based configuration.
|
||||
3. **No Integration**: Currently, these systems operate independently with no shared configuration or coordination.
|
||||
|
||||
### Solution
|
||||
## Goals
|
||||
|
||||
To fix this issue, update the route configurations to include both formats of the IP address. Here's how to modify the affected route:
|
||||
1. Create a unified configuration system where SmartProxy routes can specify NFTables-based forwarding.
|
||||
2. Allow SmartProxy to dynamically provision and manage NFTables rules based on route configuration.
|
||||
3. Support advanced filtering and security rules through NFTables for better performance.
|
||||
4. Ensure backward compatibility with existing setups.
|
||||
5. Provide metrics integration between the systems.
|
||||
|
||||
```typescript
|
||||
// Wildcard domain route for *.lossless.digital
|
||||
{
|
||||
match: {
|
||||
ports: 443,
|
||||
domains: ['*.lossless.digital'],
|
||||
clientIp: ['212.95.99.130', '::ffff:212.95.99.130'], // Include both formats
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: '212.95.99.130',
|
||||
port: 443
|
||||
},
|
||||
tls: {
|
||||
mode: 'passthrough'
|
||||
},
|
||||
security: {
|
||||
allowedIps: ['212.95.99.130', '::ffff:212.95.99.130'] // Include both formats
|
||||
}
|
||||
},
|
||||
name: 'Wildcard lossless.digital route (IP restricted)'
|
||||
}
|
||||
```
|
||||
## Implementation Plan
|
||||
|
||||
### Alternative Long-Term Fix
|
||||
### Phase 1: Route Configuration Schema Extension
|
||||
|
||||
A more robust solution would be to modify the SmartProxy codebase to automatically handle IPv6-mapped IPv4 addresses by normalizing them before comparison. This would involve:
|
||||
1. **Extend Route Configuration Schema**:
|
||||
- Add new `forwardingEngine` option to IRouteAction to specify the forwarding implementation.
|
||||
- Support values: 'node' (current NodeJS implementation) and 'nftables' (Linux NFTables).
|
||||
- Add NFTables-specific configuration options to IRouteAction.
|
||||
|
||||
1. Modifying the `matchIpPattern` function in `route-manager.ts` to normalize IPv6-mapped IPv4 addresses:
|
||||
2. **Update Type Definitions**:
|
||||
```typescript
|
||||
// In route-types.ts
|
||||
export interface IRouteAction {
|
||||
type: 'forward' | 'redirect' | 'block';
|
||||
target?: IRouteTarget;
|
||||
security?: IRouteSecurity;
|
||||
options?: IRouteOptions;
|
||||
tls?: IRouteTlsOptions;
|
||||
forwardingEngine?: 'node' | 'nftables'; // New field
|
||||
nftables?: INfTablesOptions; // New field
|
||||
}
|
||||
|
||||
```typescript
|
||||
private matchIpPattern(pattern: string, ip: string): boolean {
|
||||
// Normalize IPv6-mapped IPv4 addresses
|
||||
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||
const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern;
|
||||
|
||||
// Handle exact match with normalized addresses
|
||||
if (normalizedPattern === normalizedIp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Rest of the existing function...
|
||||
}
|
||||
```
|
||||
|
||||
2. Making similar modifications to other IP-related functions in the codebase.
|
||||
|
||||
## Wild Card Domain Matching Issue
|
||||
|
||||
### Explanation
|
||||
|
||||
The wildcard domain matching in SmartProxy works as follows:
|
||||
|
||||
1. When a pattern like `*.lossless.digital` is specified, it's converted to a regex: `/^.*\.lossless\.digital$/i`
|
||||
2. This correctly matches any subdomain like `my.lossless.digital`, `api.lossless.digital`, etc.
|
||||
3. However, it does NOT match the apex domain `lossless.digital` (without a subdomain)
|
||||
|
||||
If you need to match both the apex domain and subdomains, use a list:
|
||||
```typescript
|
||||
domains: ['lossless.digital', '*.lossless.digital']
|
||||
```
|
||||
|
||||
## Debugging SmartProxy
|
||||
|
||||
To debug routing issues in SmartProxy:
|
||||
|
||||
1. Add detailed logging to the `route-manager.js` file in the `dist_ts` directory:
|
||||
- `findMatchingRoute` method - to see what criteria are being checked
|
||||
- `matchRouteDomain` method - to see domain matching logic
|
||||
- `matchDomain` method - to see pattern matching
|
||||
- `matchIpPattern` method - to see IP matching logic
|
||||
|
||||
2. Run the proxy with debugging enabled:
|
||||
```
|
||||
pnpm run startNew
|
||||
export interface INfTablesOptions {
|
||||
preserveSourceIP?: boolean;
|
||||
protocol?: 'tcp' | 'udp' | 'all';
|
||||
maxRate?: string; // QoS rate limiting
|
||||
priority?: number; // QoS priority
|
||||
tableName?: string; // Optional custom table name
|
||||
useIPSets?: boolean; // Use IP sets for performance
|
||||
useAdvancedNAT?: boolean; // Use connection tracking
|
||||
}
|
||||
```
|
||||
|
||||
3. Monitor the logs for detailed information about the routing process and identify where matches are failing.
|
||||
### Phase 2: NFTablesManager Implementation
|
||||
|
||||
## Priority and Route Order
|
||||
1. **Create NFTablesManager Class**:
|
||||
- Create a new class to manage NFTables rules based on SmartProxy routes.
|
||||
- Add methods to create, update, and remove NFTables rules.
|
||||
- Design a rule naming scheme to track which rules correspond to which routes.
|
||||
|
||||
Remember that routes are evaluated in priority order (higher priority first). If multiple routes could match the same request, ensure that the more specific routes have higher priority.
|
||||
2. **Implementation**:
|
||||
```typescript
|
||||
// In ts/proxies/smart-proxy/nftables-manager.ts
|
||||
export class NFTablesManager {
|
||||
private rulesMap: Map<string, NfTablesProxy> = new Map();
|
||||
|
||||
constructor(private options: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Provision NFTables rules for a route
|
||||
*/
|
||||
public async provisionRoute(route: IRouteConfig): Promise<boolean> {
|
||||
// Generate a unique ID for this route
|
||||
const routeId = this.generateRouteId(route);
|
||||
|
||||
// Skip if route doesn't use NFTables
|
||||
if (route.action.forwardingEngine !== 'nftables') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create NFTables options from route configuration
|
||||
const nftOptions = this.createNfTablesOptions(route);
|
||||
|
||||
// Create and start an NFTablesProxy instance
|
||||
const proxy = new NfTablesProxy(nftOptions);
|
||||
|
||||
try {
|
||||
await proxy.start();
|
||||
this.rulesMap.set(routeId, proxy);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Failed to provision NFTables rules for route ${route.name}: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove NFTables rules for a route
|
||||
*/
|
||||
public async deprovisionRoute(route: IRouteConfig): Promise<boolean> {
|
||||
const routeId = this.generateRouteId(route);
|
||||
|
||||
const proxy = this.rulesMap.get(routeId);
|
||||
if (!proxy) {
|
||||
return true; // Nothing to remove
|
||||
}
|
||||
|
||||
try {
|
||||
await proxy.stop();
|
||||
this.rulesMap.delete(routeId);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Failed to deprovision NFTables rules for route ${route.name}: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update NFTables rules when route changes
|
||||
*/
|
||||
public async updateRoute(oldRoute: IRouteConfig, newRoute: IRouteConfig): Promise<boolean> {
|
||||
// Remove old rules and add new ones
|
||||
await this.deprovisionRoute(oldRoute);
|
||||
return this.provisionRoute(newRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique ID for a route
|
||||
*/
|
||||
private generateRouteId(route: IRouteConfig): string {
|
||||
// Generate a unique ID based on route properties
|
||||
return `${route.name || 'unnamed'}-${JSON.stringify(route.match)}-${Date.now()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create NFTablesProxy options from a route configuration
|
||||
*/
|
||||
private createNfTablesOptions(route: IRouteConfig): NfTableProxyOptions {
|
||||
const { action } = route;
|
||||
|
||||
// Ensure we have a target
|
||||
if (!action.target) {
|
||||
throw new Error('Route must have a target to use NFTables forwarding');
|
||||
}
|
||||
|
||||
// Convert port specifications
|
||||
const fromPorts = this.expandPortRange(route.match.ports);
|
||||
|
||||
// Determine target port
|
||||
let toPorts;
|
||||
if (action.target.port === 'preserve') {
|
||||
// 'preserve' means use the same ports as the source
|
||||
toPorts = fromPorts;
|
||||
} else if (typeof action.target.port === 'function') {
|
||||
// For function-based ports, we can't determine at setup time
|
||||
// Use the "preserve" approach and let NFTables handle it
|
||||
toPorts = fromPorts;
|
||||
} else {
|
||||
toPorts = action.target.port;
|
||||
}
|
||||
|
||||
// Create options
|
||||
const options: NfTableProxyOptions = {
|
||||
fromPort: fromPorts,
|
||||
toPort: toPorts,
|
||||
toHost: typeof action.target.host === 'function'
|
||||
? 'localhost' // Can't determine at setup time, use localhost
|
||||
: (Array.isArray(action.target.host)
|
||||
? action.target.host[0] // Use first host for now
|
||||
: action.target.host),
|
||||
protocol: action.nftables?.protocol || 'tcp',
|
||||
preserveSourceIP: action.nftables?.preserveSourceIP,
|
||||
useIPSets: action.nftables?.useIPSets !== false,
|
||||
useAdvancedNAT: action.nftables?.useAdvancedNAT,
|
||||
enableLogging: this.options.enableDetailedLogging,
|
||||
deleteOnExit: true,
|
||||
tableName: action.nftables?.tableName || 'smartproxy'
|
||||
};
|
||||
|
||||
// Add security-related options
|
||||
if (action.security?.ipAllowList?.length) {
|
||||
options.allowedSourceIPs = action.security.ipAllowList;
|
||||
}
|
||||
|
||||
if (action.security?.ipBlockList?.length) {
|
||||
options.bannedSourceIPs = action.security.ipBlockList;
|
||||
}
|
||||
|
||||
// Add QoS options
|
||||
if (action.nftables?.maxRate || action.nftables?.priority) {
|
||||
options.qos = {
|
||||
enabled: true,
|
||||
maxRate: action.nftables.maxRate,
|
||||
priority: action.nftables.priority
|
||||
};
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand port range specifications
|
||||
*/
|
||||
private expandPortRange(ports: TPortRange): number | PortRange | Array<number | PortRange> {
|
||||
// Use RouteManager's expandPortRange to convert to actual port numbers
|
||||
const routeManager = new RouteManager(this.options);
|
||||
|
||||
// Process different port specifications
|
||||
if (typeof ports === 'number') {
|
||||
return ports;
|
||||
} else if (Array.isArray(ports)) {
|
||||
const result: Array<number | PortRange> = [];
|
||||
|
||||
for (const item of ports) {
|
||||
if (typeof item === 'number') {
|
||||
result.push(item);
|
||||
} else if ('from' in item && 'to' in item) {
|
||||
result.push({ from: item.from, to: item.to });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} else if ('from' in ports && 'to' in ports) {
|
||||
return { from: ports.from, to: ports.to };
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status of all managed rules
|
||||
*/
|
||||
public async getStatus(): Promise<Record<string, NfTablesStatus>> {
|
||||
const result: Record<string, NfTablesStatus> = {};
|
||||
|
||||
for (const [routeId, proxy] of this.rulesMap.entries()) {
|
||||
result[routeId] = await proxy.getStatus();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all NFTables rules
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
// Stop all NFTables proxies
|
||||
const stopPromises = Array.from(this.rulesMap.values()).map(proxy => proxy.stop());
|
||||
await Promise.all(stopPromises);
|
||||
|
||||
this.rulesMap.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When routes have the same priority (or none specified), they're evaluated in the order they're defined in the configuration.
|
||||
### Phase 3: SmartProxy Integration
|
||||
|
||||
1. **Extend SmartProxy Class**:
|
||||
- Add NFTablesManager as a property of SmartProxy.
|
||||
- Hook into route configuration to provision NFTables rules.
|
||||
- Add methods to manage NFTables functionality.
|
||||
|
||||
2. **Implementation**:
|
||||
```typescript
|
||||
// In ts/proxies/smart-proxy/smart-proxy.ts
|
||||
import { NFTablesManager } from './nftables-manager.js';
|
||||
|
||||
export class SmartProxy {
|
||||
// Existing properties
|
||||
private nftablesManager: NFTablesManager;
|
||||
|
||||
constructor(options: ISmartProxyOptions) {
|
||||
// Existing initialization
|
||||
|
||||
// Initialize NFTablesManager
|
||||
this.nftablesManager = new NFTablesManager(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the SmartProxy server
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
// Existing initialization
|
||||
|
||||
// If we have routes, provision NFTables rules for them
|
||||
for (const route of this.settings.routes) {
|
||||
if (route.action.forwardingEngine === 'nftables') {
|
||||
await this.nftablesManager.provisionRoute(route);
|
||||
}
|
||||
}
|
||||
|
||||
// Rest of existing start method
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the SmartProxy server
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
// Stop NFTablesManager first
|
||||
await this.nftablesManager.stop();
|
||||
|
||||
// Rest of existing stop method
|
||||
}
|
||||
|
||||
/**
|
||||
* Update routes
|
||||
*/
|
||||
public async updateRoutes(routes: IRouteConfig[]): Promise<void> {
|
||||
// Get existing routes that use NFTables
|
||||
const oldNfTablesRoutes = this.settings.routes.filter(
|
||||
r => r.action.forwardingEngine === 'nftables'
|
||||
);
|
||||
|
||||
// Get new routes that use NFTables
|
||||
const newNfTablesRoutes = routes.filter(
|
||||
r => r.action.forwardingEngine === 'nftables'
|
||||
);
|
||||
|
||||
// Find routes to remove, update, or add
|
||||
for (const oldRoute of oldNfTablesRoutes) {
|
||||
const newRoute = newNfTablesRoutes.find(r => r.name === oldRoute.name);
|
||||
|
||||
if (!newRoute) {
|
||||
// Route was removed
|
||||
await this.nftablesManager.deprovisionRoute(oldRoute);
|
||||
} else {
|
||||
// Route was updated
|
||||
await this.nftablesManager.updateRoute(oldRoute, newRoute);
|
||||
}
|
||||
}
|
||||
|
||||
// Find new routes to add
|
||||
for (const newRoute of newNfTablesRoutes) {
|
||||
const oldRoute = oldNfTablesRoutes.find(r => r.name === newRoute.name);
|
||||
|
||||
if (!oldRoute) {
|
||||
// New route
|
||||
await this.nftablesManager.provisionRoute(newRoute);
|
||||
}
|
||||
}
|
||||
|
||||
// Update settings with the new routes
|
||||
this.settings.routes = routes;
|
||||
|
||||
// Update route manager with new routes
|
||||
this.routeManager.updateRoutes(routes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NFTables status
|
||||
*/
|
||||
public async getNfTablesStatus(): Promise<Record<string, NfTablesStatus>> {
|
||||
return this.nftablesManager.getStatus();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Routing System Integration
|
||||
|
||||
1. **Extend the Route-Connection-Handler**:
|
||||
- Modify to check if a route uses NFTables.
|
||||
- Skip Node.js-based connection handling for NFTables routes.
|
||||
|
||||
2. **Implementation**:
|
||||
```typescript
|
||||
// In ts/proxies/smart-proxy/route-connection-handler.ts
|
||||
export class RouteConnectionHandler {
|
||||
// Existing methods
|
||||
|
||||
/**
|
||||
* Route the connection based on match criteria
|
||||
*/
|
||||
private routeConnection(
|
||||
socket: plugins.net.Socket,
|
||||
record: IConnectionRecord,
|
||||
serverName: string,
|
||||
initialChunk?: Buffer
|
||||
): void {
|
||||
// Find matching route
|
||||
const routeMatch = this.routeManager.findMatchingRoute({
|
||||
port: record.localPort,
|
||||
domain: serverName,
|
||||
clientIp: record.remoteIP,
|
||||
path: undefined,
|
||||
tlsVersion: undefined
|
||||
});
|
||||
|
||||
if (!routeMatch) {
|
||||
// Existing code for no matching route
|
||||
return;
|
||||
}
|
||||
|
||||
const route = routeMatch.route;
|
||||
|
||||
// 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(
|
||||
`[${record.id}] 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.initiateCleanupOnce(record, 'nftables_handled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Existing code for handling the route
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 5: CLI and Configuration Helpers
|
||||
|
||||
1. **Add Helper Functions**:
|
||||
- Create helper functions for easy route creation with NFTables.
|
||||
- Update the route-helpers.ts utility file.
|
||||
|
||||
2. **Implementation**:
|
||||
```typescript
|
||||
// In ts/proxies/smart-proxy/utils/route-helpers.ts
|
||||
|
||||
/**
|
||||
* Create an NFTables-based route
|
||||
*/
|
||||
export function createNfTablesRoute(
|
||||
nameOrDomains: string | string[],
|
||||
target: { host: string; port: number | 'preserve' },
|
||||
options: {
|
||||
ports?: TPortRange;
|
||||
protocol?: 'tcp' | 'udp' | 'all';
|
||||
preserveSourceIP?: boolean;
|
||||
allowedIps?: string[];
|
||||
maxRate?: string;
|
||||
priority?: number;
|
||||
useTls?: boolean;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
// Determine if this is a name or domain
|
||||
let name: string;
|
||||
let domains: string | string[];
|
||||
|
||||
if (Array.isArray(nameOrDomains) || nameOrDomains.includes('.')) {
|
||||
domains = nameOrDomains;
|
||||
name = Array.isArray(nameOrDomains) ? nameOrDomains[0] : nameOrDomains;
|
||||
} else {
|
||||
name = nameOrDomains;
|
||||
domains = []; // No domains
|
||||
}
|
||||
|
||||
const route: IRouteConfig = {
|
||||
name,
|
||||
match: {
|
||||
domains,
|
||||
ports: options.ports || 80
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: target.host,
|
||||
port: target.port
|
||||
},
|
||||
forwardingEngine: 'nftables',
|
||||
nftables: {
|
||||
protocol: options.protocol || 'tcp',
|
||||
preserveSourceIP: options.preserveSourceIP,
|
||||
maxRate: options.maxRate,
|
||||
priority: options.priority
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add security if allowed IPs are specified
|
||||
if (options.allowedIps?.length) {
|
||||
route.action.security = {
|
||||
ipAllowList: options.allowedIps
|
||||
};
|
||||
}
|
||||
|
||||
// Add TLS options if needed
|
||||
if (options.useTls) {
|
||||
route.action.tls = {
|
||||
mode: 'passthrough'
|
||||
};
|
||||
}
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an NFTables-based TLS termination route
|
||||
*/
|
||||
export function createNfTablesTerminateRoute(
|
||||
nameOrDomains: string | string[],
|
||||
target: { host: string; port: number | 'preserve' },
|
||||
options: {
|
||||
ports?: TPortRange;
|
||||
protocol?: 'tcp' | 'udp' | 'all';
|
||||
preserveSourceIP?: boolean;
|
||||
allowedIps?: string[];
|
||||
maxRate?: string;
|
||||
priority?: number;
|
||||
certificate?: string | { cert: string; key: string };
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
const route = createNfTablesRoute(
|
||||
nameOrDomains,
|
||||
target,
|
||||
{
|
||||
...options,
|
||||
ports: options.ports || 443,
|
||||
useTls: false
|
||||
}
|
||||
);
|
||||
|
||||
// Set TLS termination
|
||||
route.action.tls = {
|
||||
mode: 'terminate',
|
||||
certificate: options.certificate || 'auto'
|
||||
};
|
||||
|
||||
return route;
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 6: Documentation and Testing
|
||||
|
||||
1. **Update Documentation**:
|
||||
- Add NFTables integration documentation to README and API docs.
|
||||
- Document the implementation and use cases.
|
||||
|
||||
2. **Test Cases**:
|
||||
- Create test cases for NFTables-based routing.
|
||||
- Test performance comparison with Node.js-based forwarding.
|
||||
- Test security features with IP allowlists/blocklists.
|
||||
|
||||
```typescript
|
||||
// In test/test.nftables-integration.ts
|
||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||
import { createNfTablesRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import * as net from 'net';
|
||||
|
||||
// Test server and client utilities
|
||||
let testServer: net.Server;
|
||||
let smartProxy: SmartProxy;
|
||||
|
||||
const TEST_PORT = 4000;
|
||||
const PROXY_PORT = 5000;
|
||||
const TEST_DATA = 'Hello through NFTables!';
|
||||
|
||||
tap.test('setup NFTables integration test environment', async () => {
|
||||
// Create a test TCP server
|
||||
testServer = net.createServer((socket) => {
|
||||
socket.on('data', (data) => {
|
||||
socket.write(`Server says: ${data.toString()}`);
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
testServer.listen(TEST_PORT, () => {
|
||||
console.log(`Test server listening on port ${TEST_PORT}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Create SmartProxy with NFTables route
|
||||
smartProxy = new SmartProxy({
|
||||
routes: [
|
||||
createNfTablesRoute('test-nftables', {
|
||||
host: 'localhost',
|
||||
port: TEST_PORT
|
||||
}, {
|
||||
ports: PROXY_PORT,
|
||||
protocol: 'tcp'
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// Start the proxy
|
||||
await smartProxy.start();
|
||||
});
|
||||
|
||||
tap.test('should forward TCP connections through NFTables', async () => {
|
||||
// Connect to the proxy port
|
||||
const client = new net.Socket();
|
||||
|
||||
const response = await new Promise<string>((resolve, reject) => {
|
||||
let responseData = '';
|
||||
|
||||
client.connect(PROXY_PORT, 'localhost', () => {
|
||||
client.write(TEST_DATA);
|
||||
});
|
||||
|
||||
client.on('data', (data) => {
|
||||
responseData += data.toString();
|
||||
client.end();
|
||||
});
|
||||
|
||||
client.on('end', () => {
|
||||
resolve(responseData);
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
expect(response).toEqual(`Server says: ${TEST_DATA}`);
|
||||
});
|
||||
|
||||
tap.test('cleanup NFTables integration test environment', async () => {
|
||||
// Stop the proxy and test server
|
||||
await smartProxy.stop();
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
testServer.close(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
```
|
||||
|
||||
## Expected Benefits
|
||||
|
||||
1. **Performance**: NFTables operates at the kernel level, offering much higher performance than Node.js-based routing.
|
||||
2. **Scalability**: Handle more connections with less CPU and memory usage.
|
||||
3. **Security**: Leverage kernel-level security features for better protection.
|
||||
4. **Integration**: Unified configuration model between application and network layers.
|
||||
5. **Advanced Features**: Support for QoS, rate limiting, and other advanced networking features.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- This integration requires root/sudo access to configure NFTables rules.
|
||||
- Consider adding a capability check to gracefully fall back to Node.js routing if NFTables is not available.
|
||||
- The NFTables integration should be optional and SmartProxy should continue to work without it.
|
||||
- The integration provides a path for future extensions to other kernel-level networking features.
|
||||
|
||||
## Timeline
|
||||
|
||||
- Phase 1 (Route Configuration Schema): 1-2 days
|
||||
- Phase 2 (NFTablesManager): 2-3 days
|
||||
- Phase 3 (SmartProxy Integration): 1-2 days
|
||||
- Phase 4 (Routing System Integration): 1 day
|
||||
- Phase 5 (CLI and Helpers): 1 day
|
||||
- Phase 6 (Documentation and Testing): 2 days
|
||||
|
||||
**Total Estimated Time: 8-11 days**
|
34
summary-nftables-naming-consolidation.md
Normal file
34
summary-nftables-naming-consolidation.md
Normal file
@ -0,0 +1,34 @@
|
||||
# NFTables Naming Consolidation Summary
|
||||
|
||||
This document summarizes the changes made to consolidate the naming convention for IP allow/block lists in the NFTables integration.
|
||||
|
||||
## Changes Made
|
||||
|
||||
1. **Updated NFTablesProxy interface** (`ts/proxies/nftables-proxy/models/interfaces.ts`):
|
||||
- Changed `allowedSourceIPs` to `ipAllowList`
|
||||
- Changed `bannedSourceIPs` to `ipBlockList`
|
||||
|
||||
2. **Updated NFTablesProxy implementation** (`ts/proxies/nftables-proxy/nftables-proxy.ts`):
|
||||
- Updated all references from `allowedSourceIPs` to `ipAllowList`
|
||||
- Updated all references from `bannedSourceIPs` to `ipBlockList`
|
||||
|
||||
3. **Updated NFTablesManager** (`ts/proxies/smart-proxy/nftables-manager.ts`):
|
||||
- Changed mapping from `allowedSourceIPs` to `ipAllowList`
|
||||
- Changed mapping from `bannedSourceIPs` to `ipBlockList`
|
||||
|
||||
## Files Already Using Consistent Naming
|
||||
|
||||
The following files already used the consistent naming convention `ipAllowList` and `ipBlockList`:
|
||||
|
||||
1. **Route helpers** (`ts/proxies/smart-proxy/utils/route-helpers.ts`)
|
||||
2. **Integration test** (`test/test.nftables-integration.ts`)
|
||||
3. **NFTables example** (`examples/nftables-integration.ts`)
|
||||
4. **Route types** (`ts/proxies/smart-proxy/models/route-types.ts`)
|
||||
|
||||
## Result
|
||||
|
||||
The naming is now consistent throughout the codebase:
|
||||
- `ipAllowList` is used for lists of allowed IP addresses
|
||||
- `ipBlockList` is used for lists of blocked IP addresses
|
||||
|
||||
This matches the naming convention already established in SmartProxy's core routing system.
|
@ -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();
|
@ -235,7 +235,7 @@ tap.test('SmartProxy: Should create instance with route-based config', async ()
|
||||
port: 8080
|
||||
},
|
||||
security: {
|
||||
allowedIps: ['127.0.0.1', '192.168.0.*'],
|
||||
ipAllowList: ['127.0.0.1', '192.168.0.*'],
|
||||
maxConnections: 100
|
||||
}
|
||||
},
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '16.0.4',
|
||||
version: '18.0.2',
|
||||
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.'
|
||||
}
|
||||
|
@ -21,9 +21,21 @@ export function convertToLegacyForwardConfig(
|
||||
? forwardConfig.target.host[0] // Use the first host in the array
|
||||
: forwardConfig.target.host;
|
||||
|
||||
// Extract port number, handling different port formats
|
||||
let port: number;
|
||||
if (typeof forwardConfig.target.port === 'function') {
|
||||
// Use a default port for function-based ports in adapter context
|
||||
port = 80;
|
||||
} else if (forwardConfig.target.port === 'preserve') {
|
||||
// For 'preserve', use the default port 80 in this adapter context
|
||||
port = 80;
|
||||
} else {
|
||||
port = forwardConfig.target.port;
|
||||
}
|
||||
|
||||
return {
|
||||
ip: host,
|
||||
port: forwardConfig.target.port
|
||||
port: port
|
||||
};
|
||||
}
|
||||
|
||||
@ -75,11 +87,23 @@ export function createPort80HandlerOptions(
|
||||
forwardConfig.type === 'https-terminate-to-https'));
|
||||
|
||||
if (supportsHttp) {
|
||||
// Determine port value handling different formats
|
||||
let port: number;
|
||||
if (typeof forwardConfig.target.port === 'function') {
|
||||
// Use a default port for function-based ports
|
||||
port = 80;
|
||||
} else if (forwardConfig.target.port === 'preserve') {
|
||||
// For 'preserve', use 80 in this adapter context
|
||||
port = 80;
|
||||
} else {
|
||||
port = forwardConfig.target.port;
|
||||
}
|
||||
|
||||
options.forward = {
|
||||
ip: Array.isArray(forwardConfig.target.host)
|
||||
? forwardConfig.target.host[0]
|
||||
: forwardConfig.target.host,
|
||||
port: forwardConfig.target.port
|
||||
port: port
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -209,18 +209,18 @@ export function matchIpPattern(pattern: string, ip: string): boolean {
|
||||
* Match an IP against allowed and blocked IP patterns
|
||||
*
|
||||
* @param ip IP to check
|
||||
* @param allowedIps Array of allowed IP patterns
|
||||
* @param blockedIps Array of blocked IP patterns
|
||||
* @param ipAllowList Array of allowed IP patterns
|
||||
* @param ipBlockList Array of blocked IP patterns
|
||||
* @returns Whether the IP is allowed
|
||||
*/
|
||||
export function isIpAuthorized(
|
||||
ip: string,
|
||||
allowedIps: string[] = ['*'],
|
||||
blockedIps: string[] = []
|
||||
ipAllowList: string[] = ['*'],
|
||||
ipBlockList: string[] = []
|
||||
): boolean {
|
||||
// Check blocked IPs first
|
||||
if (blockedIps.length > 0) {
|
||||
for (const pattern of blockedIps) {
|
||||
if (ipBlockList.length > 0) {
|
||||
for (const pattern of ipBlockList) {
|
||||
if (matchIpPattern(pattern, ip)) {
|
||||
return false; // IP is blocked
|
||||
}
|
||||
@ -228,13 +228,13 @@ export function isIpAuthorized(
|
||||
}
|
||||
|
||||
// If there are allowed IPs, check them
|
||||
if (allowedIps.length > 0) {
|
||||
if (ipAllowList.length > 0) {
|
||||
// Special case: if '*' is in allowed IPs, all non-blocked IPs are allowed
|
||||
if (allowedIps.includes('*')) {
|
||||
if (ipAllowList.includes('*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const pattern of allowedIps) {
|
||||
for (const pattern of ipAllowList) {
|
||||
if (matchIpPattern(pattern, ip)) {
|
||||
return true; // IP is allowed
|
||||
}
|
||||
|
@ -199,8 +199,8 @@ export class SharedSecurityManager {
|
||||
}
|
||||
|
||||
// Check IP against route security settings
|
||||
const ipAllowList = route.security.ipAllowList || route.security.allowedIps;
|
||||
const ipBlockList = route.security.ipBlockList || route.security.blockedIps;
|
||||
const ipAllowList = route.security.ipAllowList;
|
||||
const ipBlockList = route.security.ipBlockList;
|
||||
|
||||
const allowed = this.isIPAuthorized(clientIp, ipAllowList, ipBlockList);
|
||||
|
||||
|
@ -1,10 +1,8 @@
|
||||
import type * as plugins from '../../plugins.js';
|
||||
|
||||
/**
|
||||
* @deprecated The legacy forwarding types are being replaced by the route-based configuration system.
|
||||
* See /ts/proxies/smart-proxy/models/route-types.ts for the new route-based configuration.
|
||||
*
|
||||
* The primary forwarding types supported by SmartProxy
|
||||
* Used for configuration compatibility
|
||||
*/
|
||||
export type TForwardingType =
|
||||
| 'http-only' // HTTP forwarding only (no HTTPS)
|
||||
@ -35,7 +33,7 @@ export interface IForwardingHandler extends plugins.EventEmitter {
|
||||
handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void;
|
||||
}
|
||||
|
||||
// Import and re-export the route-based helpers for seamless transition
|
||||
// Route-based helpers are now available directly from route-patterns.ts
|
||||
import {
|
||||
createHttpRoute,
|
||||
createHttpsTerminateRoute,
|
||||
@ -43,7 +41,7 @@ import {
|
||||
createHttpToHttpsRedirect,
|
||||
createCompleteHttpsServer,
|
||||
createLoadBalancerRoute
|
||||
} from '../../proxies/smart-proxy/utils/route-helpers.js';
|
||||
} from '../../proxies/smart-proxy/utils/route-patterns.js';
|
||||
|
||||
export {
|
||||
createHttpRoute,
|
||||
@ -54,23 +52,20 @@ export {
|
||||
createLoadBalancerRoute
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated These helper functions are maintained for backward compatibility.
|
||||
* Please use the route-based helpers instead:
|
||||
* - createHttpRoute
|
||||
* - createHttpsTerminateRoute
|
||||
* - createHttpsPassthroughRoute
|
||||
* - createHttpToHttpsRedirect
|
||||
*/
|
||||
// Note: Legacy helper functions have been removed
|
||||
// Please use the route-based helpers instead:
|
||||
// - createHttpRoute
|
||||
// - createHttpsTerminateRoute
|
||||
// - createHttpsPassthroughRoute
|
||||
// - createHttpToHttpsRedirect
|
||||
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
|
||||
import { domainConfigToRouteConfig } from '../../proxies/smart-proxy/utils/route-migration-utils.js';
|
||||
|
||||
// For backward compatibility
|
||||
// For backward compatibility, kept only the basic configuration interface
|
||||
export interface IForwardConfig {
|
||||
type: TForwardingType;
|
||||
target: {
|
||||
host: string | string[];
|
||||
port: number;
|
||||
port: number | 'preserve' | ((ctx: any) => number);
|
||||
};
|
||||
http?: any;
|
||||
https?: any;
|
||||
@ -78,57 +73,4 @@ export interface IForwardConfig {
|
||||
security?: any;
|
||||
advanced?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface IDeprecatedForwardConfig {
|
||||
type: TForwardingType;
|
||||
target: {
|
||||
host: string | string[];
|
||||
port: number;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use createHttpRoute instead
|
||||
*/
|
||||
export const httpOnly = (
|
||||
partialConfig: Partial<IDeprecatedForwardConfig> & Pick<IDeprecatedForwardConfig, 'target'>
|
||||
): IDeprecatedForwardConfig => ({
|
||||
type: 'http-only',
|
||||
target: partialConfig.target,
|
||||
...(partialConfig)
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Use createHttpsTerminateRoute instead
|
||||
*/
|
||||
export const tlsTerminateToHttp = (
|
||||
partialConfig: Partial<IDeprecatedForwardConfig> & Pick<IDeprecatedForwardConfig, 'target'>
|
||||
): IDeprecatedForwardConfig => ({
|
||||
type: 'https-terminate-to-http',
|
||||
target: partialConfig.target,
|
||||
...(partialConfig)
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Use createHttpsTerminateRoute with reencrypt option instead
|
||||
*/
|
||||
export const tlsTerminateToHttps = (
|
||||
partialConfig: Partial<IDeprecatedForwardConfig> & Pick<IDeprecatedForwardConfig, 'target'>
|
||||
): IDeprecatedForwardConfig => ({
|
||||
type: 'https-terminate-to-https',
|
||||
target: partialConfig.target,
|
||||
...(partialConfig)
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Use createHttpsPassthroughRoute instead
|
||||
*/
|
||||
export const httpsPassthrough = (
|
||||
partialConfig: Partial<IDeprecatedForwardConfig> & Pick<IDeprecatedForwardConfig, 'target'>
|
||||
): IDeprecatedForwardConfig => ({
|
||||
type: 'https-passthrough',
|
||||
target: partialConfig.target,
|
||||
...(partialConfig)
|
||||
});
|
||||
}
|
@ -5,5 +5,22 @@
|
||||
* See /ts/proxies/smart-proxy/models/route-types.ts for the new route-based configuration.
|
||||
*/
|
||||
|
||||
export * from './forwarding-types.js';
|
||||
export * from '../../proxies/smart-proxy/utils/route-helpers.js';
|
||||
export type {
|
||||
TForwardingType,
|
||||
IForwardConfig,
|
||||
IForwardingHandler
|
||||
} from './forwarding-types.js';
|
||||
|
||||
export {
|
||||
ForwardingHandlerEvents
|
||||
} from './forwarding-types.js';
|
||||
|
||||
// Import route helpers from route-patterns instead of deleted route-helpers
|
||||
export {
|
||||
createHttpRoute,
|
||||
createHttpsTerminateRoute,
|
||||
createHttpsPassthroughRoute,
|
||||
createHttpToHttpsRedirect,
|
||||
createCompleteHttpsServer,
|
||||
createLoadBalancerRoute
|
||||
} from '../../proxies/smart-proxy/utils/route-patterns.js';
|
@ -122,8 +122,13 @@ export class ForwardingHandlerFactory {
|
||||
throw new Error('Target must include a host or array of hosts');
|
||||
}
|
||||
|
||||
if (!config.target.port || config.target.port <= 0 || config.target.port > 65535) {
|
||||
throw new Error('Target must include a valid port (1-65535)');
|
||||
// Validate port if it's a number
|
||||
if (typeof config.target.port === 'number') {
|
||||
if (config.target.port <= 0 || config.target.port > 65535) {
|
||||
throw new Error('Target must include a valid port (1-65535)');
|
||||
}
|
||||
} else if (config.target.port !== 'preserve' && typeof config.target.port !== 'function') {
|
||||
throw new Error('Target port must be a number, "preserve", or a function');
|
||||
}
|
||||
|
||||
// Type-specific validation
|
||||
|
@ -40,9 +40,10 @@ export abstract class ForwardingHandler extends plugins.EventEmitter implements
|
||||
|
||||
/**
|
||||
* Get a target from the configuration, supporting round-robin selection
|
||||
* @param incomingPort Optional incoming port for 'preserve' mode
|
||||
* @returns A resolved target object with host and port
|
||||
*/
|
||||
protected getTargetFromConfig(): { host: string, port: number } {
|
||||
protected getTargetFromConfig(incomingPort: number = 80): { host: string, port: number } {
|
||||
const { target } = this.config;
|
||||
|
||||
// Handle round-robin host selection
|
||||
@ -55,17 +56,42 @@ export abstract class ForwardingHandler extends plugins.EventEmitter implements
|
||||
const randomIndex = Math.floor(Math.random() * target.host.length);
|
||||
return {
|
||||
host: target.host[randomIndex],
|
||||
port: target.port
|
||||
port: this.resolvePort(target.port, incomingPort)
|
||||
};
|
||||
}
|
||||
|
||||
// Single host
|
||||
return {
|
||||
host: target.host,
|
||||
port: target.port
|
||||
port: this.resolvePort(target.port, incomingPort)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a port value, handling 'preserve' and function ports
|
||||
* @param port The port value to resolve
|
||||
* @param incomingPort Optional incoming port to use for 'preserve' mode
|
||||
*/
|
||||
protected resolvePort(
|
||||
port: number | 'preserve' | ((ctx: any) => number),
|
||||
incomingPort: number = 80
|
||||
): number {
|
||||
if (typeof port === 'function') {
|
||||
try {
|
||||
// Create a minimal context for the function that includes the incoming port
|
||||
const ctx = { port: incomingPort };
|
||||
return port(ctx);
|
||||
} catch (err) {
|
||||
console.error('Error resolving port function:', err);
|
||||
return incomingPort; // Fall back to incoming port
|
||||
}
|
||||
} else if (port === 'preserve') {
|
||||
return incomingPort; // Use the actual incoming port for 'preserve'
|
||||
} else {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect an HTTP request to HTTPS
|
||||
* @param req The HTTP request
|
||||
|
@ -38,6 +38,7 @@ export class HttpForwardingHandler extends ForwardingHandler {
|
||||
// For HTTP, we mainly handle parsed requests, but we can still set up
|
||||
// some basic connection tracking
|
||||
const remoteAddress = socket.remoteAddress || 'unknown';
|
||||
const localPort = socket.localPort || 80;
|
||||
|
||||
socket.on('close', (hadError) => {
|
||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
||||
@ -54,7 +55,8 @@ export class HttpForwardingHandler extends ForwardingHandler {
|
||||
});
|
||||
|
||||
this.emit(ForwardingHandlerEvents.CONNECTED, {
|
||||
remoteAddress
|
||||
remoteAddress,
|
||||
localPort
|
||||
});
|
||||
}
|
||||
|
||||
@ -64,8 +66,11 @@ export class HttpForwardingHandler extends ForwardingHandler {
|
||||
* @param res The HTTP response
|
||||
*/
|
||||
public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
||||
// Get the target from configuration
|
||||
const target = this.getTargetFromConfig();
|
||||
// Get the local port from the request (for 'preserve' port handling)
|
||||
const localPort = req.socket.localPort || 80;
|
||||
|
||||
// Get the target from configuration, passing the incoming port
|
||||
const target = this.getTargetFromConfig(localPort);
|
||||
|
||||
// Create a custom headers object with variables for substitution
|
||||
const variables = {
|
||||
|
@ -3,9 +3,6 @@
|
||||
* Provides a flexible and type-safe way to configure and manage various forwarding strategies
|
||||
*/
|
||||
|
||||
// Export types and configuration
|
||||
export * from './config/forwarding-types.js';
|
||||
|
||||
// Export handlers
|
||||
export { ForwardingHandler } from './handlers/base-handler.js';
|
||||
export * from './handlers/http-handler.js';
|
||||
@ -16,20 +13,23 @@ export * from './handlers/https-terminate-to-https-handler.js';
|
||||
// Export factory
|
||||
export * from './factory/forwarding-factory.js';
|
||||
|
||||
// Helper functions as a convenience object
|
||||
import {
|
||||
httpOnly,
|
||||
tlsTerminateToHttp,
|
||||
tlsTerminateToHttps,
|
||||
httpsPassthrough
|
||||
// Export types - these include TForwardingType and IForwardConfig
|
||||
export type {
|
||||
TForwardingType,
|
||||
IForwardConfig,
|
||||
IForwardingHandler
|
||||
} from './config/forwarding-types.js';
|
||||
|
||||
// Export route-based helpers from smart-proxy
|
||||
export * from '../proxies/smart-proxy/utils/route-helpers.js';
|
||||
export {
|
||||
ForwardingHandlerEvents
|
||||
} from './config/forwarding-types.js';
|
||||
|
||||
export const helpers = {
|
||||
httpOnly,
|
||||
tlsTerminateToHttp,
|
||||
tlsTerminateToHttps,
|
||||
httpsPassthrough
|
||||
};
|
||||
// Export route helpers directly from route-patterns
|
||||
export {
|
||||
createHttpRoute,
|
||||
createHttpsTerminateRoute,
|
||||
createHttpsPassthroughRoute,
|
||||
createHttpToHttpsRedirect,
|
||||
createCompleteHttpsServer,
|
||||
createLoadBalancerRoute
|
||||
} from '../proxies/smart-proxy/utils/route-patterns.js';
|
@ -41,11 +41,12 @@ export class HttpRequestHandler {
|
||||
};
|
||||
|
||||
// Optionally rewrite host header to match target
|
||||
if (options.headers && options.headers.host) {
|
||||
if (options.headers && 'host' in options.headers) {
|
||||
// Only apply if host header rewrite is enabled or not explicitly disabled
|
||||
const shouldRewriteHost = route?.action.options?.rewriteHostHeader !== false;
|
||||
if (shouldRewriteHost) {
|
||||
options.headers.host = `${destination.host}:${destination.port}`;
|
||||
// Safely cast to OutgoingHttpHeaders to access host property
|
||||
(options.headers as plugins.http.OutgoingHttpHeaders).host = `${destination.host}:${destination.port}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,8 +31,8 @@ export interface NfTableProxyOptions {
|
||||
logFormat?: 'plain' | 'json'; // Format for logs
|
||||
|
||||
// Source filtering
|
||||
allowedSourceIPs?: string[]; // If provided, only these IPs are allowed
|
||||
bannedSourceIPs?: string[]; // If provided, these IPs are blocked
|
||||
ipAllowList?: string[]; // If provided, only these IPs are allowed
|
||||
ipBlockList?: string[]; // If provided, these IPs are blocked
|
||||
useIPSets?: boolean; // Use nftables sets for efficient IP management
|
||||
|
||||
// Rule management
|
||||
|
@ -134,8 +134,8 @@ export class NfTablesProxy {
|
||||
}
|
||||
};
|
||||
|
||||
validateIPs(settings.allowedSourceIPs);
|
||||
validateIPs(settings.bannedSourceIPs);
|
||||
validateIPs(settings.ipAllowList);
|
||||
validateIPs(settings.ipBlockList);
|
||||
|
||||
// Validate toHost - only allow hostnames or IPs
|
||||
if (settings.toHost) {
|
||||
@ -426,7 +426,7 @@ export class NfTablesProxy {
|
||||
* Adds source IP filtering rules, potentially using IP sets for efficiency
|
||||
*/
|
||||
private async addSourceIPFilters(isIpv6: boolean = false): Promise<boolean> {
|
||||
if (!this.settings.allowedSourceIPs && !this.settings.bannedSourceIPs) {
|
||||
if (!this.settings.ipAllowList && !this.settings.ipBlockList) {
|
||||
return true; // Nothing to do
|
||||
}
|
||||
|
||||
@ -441,9 +441,9 @@ export class NfTablesProxy {
|
||||
// Using IP sets for more efficient rule processing with large IP lists
|
||||
if (this.settings.useIPSets) {
|
||||
// Create sets for banned and allowed IPs if needed
|
||||
if (this.settings.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) {
|
||||
if (this.settings.ipBlockList && this.settings.ipBlockList.length > 0) {
|
||||
const setName = 'banned_ips';
|
||||
await this.createIPSet(family, setName, this.settings.bannedSourceIPs, setType as any);
|
||||
await this.createIPSet(family, setName, this.settings.ipBlockList, setType as any);
|
||||
|
||||
// Add rule to drop traffic from banned IPs
|
||||
const rule = `add rule ${family} ${this.tableName} ${chain} ip${isIpv6 ? '6' : ''} saddr @${setName} drop comment "${this.ruleTag}:BANNED_SET"`;
|
||||
@ -458,9 +458,9 @@ export class NfTablesProxy {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.settings.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) {
|
||||
if (this.settings.ipAllowList && this.settings.ipAllowList.length > 0) {
|
||||
const setName = 'allowed_ips';
|
||||
await this.createIPSet(family, setName, this.settings.allowedSourceIPs, setType as any);
|
||||
await this.createIPSet(family, setName, this.settings.ipAllowList, setType as any);
|
||||
|
||||
// Add rule to allow traffic from allowed IPs
|
||||
const rule = `add rule ${family} ${this.tableName} ${chain} ip${isIpv6 ? '6' : ''} saddr @${setName} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} accept comment "${this.ruleTag}:ALLOWED_SET"`;
|
||||
@ -490,8 +490,8 @@ export class NfTablesProxy {
|
||||
// Traditional approach without IP sets - less efficient for large IP lists
|
||||
|
||||
// Ban specific IPs first
|
||||
if (this.settings.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) {
|
||||
for (const ip of this.settings.bannedSourceIPs) {
|
||||
if (this.settings.ipBlockList && this.settings.ipBlockList.length > 0) {
|
||||
for (const ip of this.settings.ipBlockList) {
|
||||
// Skip IPv4 addresses for IPv6 rules and vice versa
|
||||
if (isIpv6 && ip.includes('.')) continue;
|
||||
if (!isIpv6 && ip.includes(':')) continue;
|
||||
@ -510,9 +510,9 @@ export class NfTablesProxy {
|
||||
}
|
||||
|
||||
// Allow specific IPs
|
||||
if (this.settings.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) {
|
||||
if (this.settings.ipAllowList && this.settings.ipAllowList.length > 0) {
|
||||
// Add rules to allow specific IPs
|
||||
for (const ip of this.settings.allowedSourceIPs) {
|
||||
for (const ip of this.settings.ipAllowList) {
|
||||
// Skip IPv4 addresses for IPv6 rules and vice versa
|
||||
if (isIpv6 && ip.includes('.')) continue;
|
||||
if (!isIpv6 && ip.includes(':')) continue;
|
||||
@ -1398,28 +1398,28 @@ export class NfTablesProxy {
|
||||
|
||||
// Source IP filters
|
||||
if (this.settings.useIPSets) {
|
||||
if (this.settings.bannedSourceIPs?.length) {
|
||||
if (this.settings.ipBlockList?.length) {
|
||||
commands.push(`add set ip ${this.tableName} banned_ips { type ipv4_addr; }`);
|
||||
commands.push(`add element ip ${this.tableName} banned_ips { ${this.settings.bannedSourceIPs.join(', ')} }`);
|
||||
commands.push(`add element ip ${this.tableName} banned_ips { ${this.settings.ipBlockList.join(', ')} }`);
|
||||
commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr @banned_ips drop comment "${this.ruleTag}:BANNED_SET"`);
|
||||
}
|
||||
|
||||
if (this.settings.allowedSourceIPs?.length) {
|
||||
if (this.settings.ipAllowList?.length) {
|
||||
commands.push(`add set ip ${this.tableName} allowed_ips { type ipv4_addr; }`);
|
||||
commands.push(`add element ip ${this.tableName} allowed_ips { ${this.settings.allowedSourceIPs.join(', ')} }`);
|
||||
commands.push(`add element ip ${this.tableName} allowed_ips { ${this.settings.ipAllowList.join(', ')} }`);
|
||||
commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr @allowed_ips ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} accept comment "${this.ruleTag}:ALLOWED_SET"`);
|
||||
commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} drop comment "${this.ruleTag}:DENY_ALL"`);
|
||||
}
|
||||
} else if (this.settings.bannedSourceIPs?.length || this.settings.allowedSourceIPs?.length) {
|
||||
} else if (this.settings.ipBlockList?.length || this.settings.ipAllowList?.length) {
|
||||
// Traditional approach without IP sets
|
||||
if (this.settings.bannedSourceIPs?.length) {
|
||||
for (const ip of this.settings.bannedSourceIPs) {
|
||||
if (this.settings.ipBlockList?.length) {
|
||||
for (const ip of this.settings.ipBlockList) {
|
||||
commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr ${ip} drop comment "${this.ruleTag}:BANNED"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.settings.allowedSourceIPs?.length) {
|
||||
for (const ip of this.settings.allowedSourceIPs) {
|
||||
if (this.settings.ipAllowList?.length) {
|
||||
for (const ip of this.settings.ipAllowList) {
|
||||
commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr ${ip} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} accept comment "${this.ruleTag}:ALLOWED"`);
|
||||
}
|
||||
commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} drop comment "${this.ruleTag}:DENY_ALL"`);
|
||||
|
@ -19,6 +19,7 @@ export { NetworkProxyBridge } from './network-proxy-bridge.js';
|
||||
// Export route-based components
|
||||
export { RouteManager } from './route-manager.js';
|
||||
export { RouteConnectionHandler } from './route-connection-handler.js';
|
||||
export { NFTablesManager } from './nftables-manager.js';
|
||||
|
||||
// Export all helper functions from the utils directory
|
||||
export * from './utils/index.js';
|
||||
|
@ -3,6 +3,3 @@
|
||||
*/
|
||||
export * from './interfaces.js';
|
||||
export * from './route-types.js';
|
||||
|
||||
// Re-export IRoutedSmartProxyOptions explicitly to avoid ambiguity
|
||||
export type { ISmartProxyOptions as IRoutedSmartProxyOptions } from './interfaces.js';
|
||||
|
@ -8,23 +8,7 @@ import type { TForwardingType } from '../../../forwarding/config/forwarding-type
|
||||
*/
|
||||
export type TSmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01';
|
||||
|
||||
/**
|
||||
* Alias for backward compatibility with code that uses IRoutedSmartProxyOptions
|
||||
*/
|
||||
export type IRoutedSmartProxyOptions = ISmartProxyOptions;
|
||||
|
||||
/**
|
||||
* Helper functions for type checking configuration types
|
||||
*/
|
||||
export function isLegacyOptions(options: any): boolean {
|
||||
// Legacy options are no longer supported
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isRoutedOptions(options: any): boolean {
|
||||
// All configurations are now route-based
|
||||
return true;
|
||||
}
|
||||
// Legacy options and type checking functions have been removed
|
||||
|
||||
/**
|
||||
* SmartProxy configuration options
|
||||
@ -43,8 +27,8 @@ export interface ISmartProxyOptions {
|
||||
port: number; // Default port to use when not specified in routes
|
||||
};
|
||||
security?: {
|
||||
allowedIps?: string[]; // Default allowed IPs
|
||||
blockedIps?: string[]; // Default blocked IPs
|
||||
ipAllowList?: string[]; // Default allowed IPs
|
||||
ipBlockList?: string[]; // Default blocked IPs
|
||||
maxConnections?: number; // Default max connections
|
||||
};
|
||||
preserveSourceIP?: boolean; // Default source IP preservation
|
||||
@ -158,4 +142,7 @@ export interface IConnectionRecord {
|
||||
// Browser connection tracking
|
||||
isBrowserConnection?: boolean; // Whether this connection appears to be from a browser
|
||||
domainSwitches?: number; // Number of times the domain has been switched on this connection
|
||||
|
||||
// NFTables tracking
|
||||
nftablesHandled?: boolean; // Whether this connection is being handled by NFTables at kernel level
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js';
|
||||
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
|
||||
import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js';
|
||||
|
||||
/**
|
||||
* Supported action types for route configurations
|
||||
@ -112,13 +113,39 @@ export interface IRouteAuthentication {
|
||||
}
|
||||
|
||||
/**
|
||||
* Security options for route actions
|
||||
* Security options for routes
|
||||
*/
|
||||
export interface IRouteSecurity {
|
||||
allowedIps?: string[];
|
||||
blockedIps?: string[];
|
||||
maxConnections?: number;
|
||||
// Access control lists
|
||||
ipAllowList?: string[]; // IP addresses that are allowed to connect
|
||||
ipBlockList?: string[]; // IP addresses that are blocked from connecting
|
||||
|
||||
// Connection limits
|
||||
maxConnections?: number; // Maximum concurrent connections
|
||||
|
||||
// Authentication
|
||||
authentication?: IRouteAuthentication;
|
||||
|
||||
// Rate limiting
|
||||
rateLimit?: IRouteRateLimit;
|
||||
|
||||
// Authentication methods
|
||||
basicAuth?: {
|
||||
enabled: boolean;
|
||||
users: Array<{ username: string; password: string }>;
|
||||
realm?: string;
|
||||
excludePaths?: string[];
|
||||
};
|
||||
|
||||
jwtAuth?: {
|
||||
enabled: boolean;
|
||||
secret: string;
|
||||
algorithm?: string;
|
||||
issuer?: string;
|
||||
audience?: string;
|
||||
expiresIn?: number;
|
||||
excludePaths?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -233,6 +260,12 @@ export interface IRouteAction {
|
||||
backendProtocol?: 'http1' | 'http2';
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
// Forwarding engine specification
|
||||
forwardingEngine?: 'node' | 'nftables';
|
||||
|
||||
// NFTables-specific options
|
||||
nftables?: INfTablesOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -247,28 +280,19 @@ export interface IRouteRateLimit {
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
// IRouteSecurity is defined above - unified definition is used for all routes
|
||||
|
||||
/**
|
||||
* Security features for routes
|
||||
* NFTables-specific configuration options
|
||||
*/
|
||||
export interface IRouteSecurity {
|
||||
rateLimit?: IRouteRateLimit;
|
||||
basicAuth?: {
|
||||
enabled: boolean;
|
||||
users: Array<{ username: string; password: string }>;
|
||||
realm?: string;
|
||||
excludePaths?: string[];
|
||||
};
|
||||
jwtAuth?: {
|
||||
enabled: boolean;
|
||||
secret: string;
|
||||
algorithm?: string;
|
||||
issuer?: string;
|
||||
audience?: string;
|
||||
expiresIn?: number;
|
||||
excludePaths?: string[];
|
||||
};
|
||||
ipAllowList?: string[];
|
||||
ipBlockList?: string[];
|
||||
export interface INfTablesOptions {
|
||||
preserveSourceIP?: boolean; // Preserve original source IP address
|
||||
protocol?: 'tcp' | 'udp' | 'all'; // Protocol to forward
|
||||
maxRate?: string; // QoS rate limiting (e.g. "10mbps")
|
||||
priority?: number; // QoS priority (1-10, lower is higher priority)
|
||||
tableName?: string; // Optional custom table name
|
||||
useIPSets?: boolean; // Use IP sets for performance
|
||||
useAdvancedNAT?: boolean; // Use connection tracking for stateful NAT
|
||||
}
|
||||
|
||||
/**
|
||||
@ -321,61 +345,4 @@ export interface IRouteConfig {
|
||||
enabled?: boolean; // Whether the route is active (default: true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified SmartProxy options with routes-based configuration
|
||||
*/
|
||||
export interface IRoutedSmartProxyOptions {
|
||||
// The unified configuration array (required)
|
||||
routes: IRouteConfig[];
|
||||
|
||||
// Global/default settings
|
||||
defaults?: {
|
||||
target?: {
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
security?: IRouteSecurity;
|
||||
tls?: IRouteTls;
|
||||
// ...other defaults
|
||||
};
|
||||
|
||||
// Other global settings remain (acme, etc.)
|
||||
acme?: IAcmeOptions;
|
||||
|
||||
// Connection timeouts and other global settings
|
||||
initialDataTimeout?: number;
|
||||
socketTimeout?: number;
|
||||
inactivityCheckInterval?: number;
|
||||
maxConnectionLifetime?: number;
|
||||
inactivityTimeout?: number;
|
||||
gracefulShutdownTimeout?: number;
|
||||
|
||||
// Socket optimization settings
|
||||
noDelay?: boolean;
|
||||
keepAlive?: boolean;
|
||||
keepAliveInitialDelay?: number;
|
||||
maxPendingDataSize?: number;
|
||||
|
||||
// Enhanced features
|
||||
disableInactivityCheck?: boolean;
|
||||
enableKeepAliveProbes?: boolean;
|
||||
enableDetailedLogging?: boolean;
|
||||
enableTlsDebugLogging?: boolean;
|
||||
enableRandomizedTimeouts?: boolean;
|
||||
allowSessionTicket?: boolean;
|
||||
|
||||
// Rate limiting and security
|
||||
maxConnectionsPerIP?: number;
|
||||
connectionRateLimitPerMinute?: number;
|
||||
|
||||
// Enhanced keep-alive settings
|
||||
keepAliveTreatment?: 'standard' | 'extended' | 'immortal';
|
||||
keepAliveInactivityMultiplier?: number;
|
||||
extendedKeepAliveLifetime?: number;
|
||||
|
||||
/**
|
||||
* Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges,
|
||||
* or a static certificate object for immediate provisioning.
|
||||
*/
|
||||
certProvisionFunction?: (domain: string) => Promise<any>;
|
||||
}
|
||||
// Configuration moved to models/interfaces.ts as ISmartProxyOptions
|
268
ts/proxies/smart-proxy/nftables-manager.ts
Normal file
268
ts/proxies/smart-proxy/nftables-manager.ts
Normal file
@ -0,0 +1,268 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { NfTablesProxy } from '../nftables-proxy/nftables-proxy.js';
|
||||
import type {
|
||||
NfTableProxyOptions,
|
||||
PortRange,
|
||||
NfTablesStatus
|
||||
} from '../nftables-proxy/models/interfaces.js';
|
||||
import type {
|
||||
IRouteConfig,
|
||||
TPortRange,
|
||||
INfTablesOptions
|
||||
} from './models/route-types.js';
|
||||
import type { ISmartProxyOptions } from './models/interfaces.js';
|
||||
|
||||
/**
|
||||
* Manages NFTables rules based on SmartProxy route configurations
|
||||
*
|
||||
* This class bridges the gap between SmartProxy routes and the NFTablesProxy,
|
||||
* allowing high-performance kernel-level packet forwarding for routes that
|
||||
* specify NFTables as their forwarding engine.
|
||||
*/
|
||||
export class NFTablesManager {
|
||||
private rulesMap: Map<string, NfTablesProxy> = new Map();
|
||||
|
||||
/**
|
||||
* Creates a new NFTablesManager
|
||||
*
|
||||
* @param options The SmartProxy options
|
||||
*/
|
||||
constructor(private options: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Provision NFTables rules for a route
|
||||
*
|
||||
* @param route The route configuration
|
||||
* @returns A promise that resolves to true if successful, false otherwise
|
||||
*/
|
||||
public async provisionRoute(route: IRouteConfig): Promise<boolean> {
|
||||
// Generate a unique ID for this route
|
||||
const routeId = this.generateRouteId(route);
|
||||
|
||||
// Skip if route doesn't use NFTables
|
||||
if (route.action.forwardingEngine !== 'nftables') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create NFTables options from route configuration
|
||||
const nftOptions = this.createNfTablesOptions(route);
|
||||
|
||||
// Create and start an NFTablesProxy instance
|
||||
const proxy = new NfTablesProxy(nftOptions);
|
||||
|
||||
try {
|
||||
await proxy.start();
|
||||
this.rulesMap.set(routeId, proxy);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Failed to provision NFTables rules for route ${route.name || 'unnamed'}: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove NFTables rules for a route
|
||||
*
|
||||
* @param route The route configuration
|
||||
* @returns A promise that resolves to true if successful, false otherwise
|
||||
*/
|
||||
public async deprovisionRoute(route: IRouteConfig): Promise<boolean> {
|
||||
const routeId = this.generateRouteId(route);
|
||||
|
||||
const proxy = this.rulesMap.get(routeId);
|
||||
if (!proxy) {
|
||||
return true; // Nothing to remove
|
||||
}
|
||||
|
||||
try {
|
||||
await proxy.stop();
|
||||
this.rulesMap.delete(routeId);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Failed to deprovision NFTables rules for route ${route.name || 'unnamed'}: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update NFTables rules when route changes
|
||||
*
|
||||
* @param oldRoute The previous route configuration
|
||||
* @param newRoute The new route configuration
|
||||
* @returns A promise that resolves to true if successful, false otherwise
|
||||
*/
|
||||
public async updateRoute(oldRoute: IRouteConfig, newRoute: IRouteConfig): Promise<boolean> {
|
||||
// Remove old rules and add new ones
|
||||
await this.deprovisionRoute(oldRoute);
|
||||
return this.provisionRoute(newRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique ID for a route
|
||||
*
|
||||
* @param route The route configuration
|
||||
* @returns A unique ID string
|
||||
*/
|
||||
private generateRouteId(route: IRouteConfig): string {
|
||||
// Generate a unique ID based on route properties
|
||||
// Include the route name, match criteria, and a timestamp
|
||||
const matchStr = JSON.stringify({
|
||||
ports: route.match.ports,
|
||||
domains: route.match.domains
|
||||
});
|
||||
|
||||
return `${route.name || 'unnamed'}-${matchStr}-${route.id || Date.now().toString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create NFTablesProxy options from a route configuration
|
||||
*
|
||||
* @param route The route configuration
|
||||
* @returns NFTableProxyOptions object
|
||||
*/
|
||||
private createNfTablesOptions(route: IRouteConfig): NfTableProxyOptions {
|
||||
const { action } = route;
|
||||
|
||||
// Ensure we have a target
|
||||
if (!action.target) {
|
||||
throw new Error('Route must have a target to use NFTables forwarding');
|
||||
}
|
||||
|
||||
// Convert port specifications
|
||||
const fromPorts = this.expandPortRange(route.match.ports);
|
||||
|
||||
// Determine target port
|
||||
let toPorts: number | PortRange | Array<number | PortRange>;
|
||||
|
||||
if (action.target.port === 'preserve') {
|
||||
// 'preserve' means use the same ports as the source
|
||||
toPorts = fromPorts;
|
||||
} else if (typeof action.target.port === 'function') {
|
||||
// For function-based ports, we can't determine at setup time
|
||||
// Use the "preserve" approach and let NFTables handle it
|
||||
toPorts = fromPorts;
|
||||
} else {
|
||||
toPorts = action.target.port;
|
||||
}
|
||||
|
||||
// Determine target host
|
||||
let toHost: string;
|
||||
if (typeof action.target.host === 'function') {
|
||||
// Can't determine at setup time, use localhost as a placeholder
|
||||
// and rely on run-time handling
|
||||
toHost = 'localhost';
|
||||
} else if (Array.isArray(action.target.host)) {
|
||||
// Use first host for now - NFTables will do simple round-robin
|
||||
toHost = action.target.host[0];
|
||||
} else {
|
||||
toHost = action.target.host;
|
||||
}
|
||||
|
||||
// Create options
|
||||
const options: NfTableProxyOptions = {
|
||||
fromPort: fromPorts,
|
||||
toPort: toPorts,
|
||||
toHost: toHost,
|
||||
protocol: action.nftables?.protocol || 'tcp',
|
||||
preserveSourceIP: action.nftables?.preserveSourceIP !== undefined ?
|
||||
action.nftables.preserveSourceIP :
|
||||
this.options.preserveSourceIP,
|
||||
useIPSets: action.nftables?.useIPSets !== false,
|
||||
useAdvancedNAT: action.nftables?.useAdvancedNAT,
|
||||
enableLogging: this.options.enableDetailedLogging,
|
||||
deleteOnExit: true,
|
||||
tableName: action.nftables?.tableName || 'smartproxy'
|
||||
};
|
||||
|
||||
// Add security-related options
|
||||
const security = action.security || route.security;
|
||||
if (security?.ipAllowList?.length) {
|
||||
options.ipAllowList = security.ipAllowList;
|
||||
}
|
||||
|
||||
if (security?.ipBlockList?.length) {
|
||||
options.ipBlockList = security.ipBlockList;
|
||||
}
|
||||
|
||||
// Add QoS options
|
||||
if (action.nftables?.maxRate || action.nftables?.priority) {
|
||||
options.qos = {
|
||||
enabled: true,
|
||||
maxRate: action.nftables.maxRate,
|
||||
priority: action.nftables.priority
|
||||
};
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand port range specifications
|
||||
*
|
||||
* @param ports The port range specification
|
||||
* @returns Expanded port range
|
||||
*/
|
||||
private expandPortRange(ports: TPortRange): number | PortRange | Array<number | PortRange> {
|
||||
// Process different port specifications
|
||||
if (typeof ports === 'number') {
|
||||
return ports;
|
||||
} else if (Array.isArray(ports)) {
|
||||
const result: Array<number | PortRange> = [];
|
||||
|
||||
for (const item of ports) {
|
||||
if (typeof item === 'number') {
|
||||
result.push(item);
|
||||
} else if ('from' in item && 'to' in item) {
|
||||
result.push({ from: item.from, to: item.to });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} else if (typeof ports === 'object' && ports !== null && 'from' in ports && 'to' in ports) {
|
||||
return { from: (ports as any).from, to: (ports as any).to };
|
||||
}
|
||||
|
||||
// Fallback to port 80 if something went wrong
|
||||
console.warn('Invalid port range specification, using port 80 as fallback');
|
||||
return 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status of all managed rules
|
||||
*
|
||||
* @returns A promise that resolves to a record of NFTables status objects
|
||||
*/
|
||||
public async getStatus(): Promise<Record<string, NfTablesStatus>> {
|
||||
const result: Record<string, NfTablesStatus> = {};
|
||||
|
||||
for (const [routeId, proxy] of this.rulesMap.entries()) {
|
||||
result[routeId] = await proxy.getStatus();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a route is currently provisioned
|
||||
*
|
||||
* @param route The route configuration
|
||||
* @returns True if the route is provisioned, false otherwise
|
||||
*/
|
||||
public isRouteProvisioned(route: IRouteConfig): boolean {
|
||||
const routeId = this.generateRouteId(route);
|
||||
return this.rulesMap.has(routeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all NFTables rules
|
||||
*
|
||||
* @returns A promise that resolves when all rules have been stopped
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
// Stop all NFTables proxies
|
||||
const stopPromises = Array.from(this.rulesMap.values()).map(proxy => proxy.stop());
|
||||
await Promise.all(stopPromises);
|
||||
|
||||
this.rulesMap.clear();
|
||||
}
|
||||
}
|
@ -3,9 +3,7 @@ import type {
|
||||
IConnectionRecord,
|
||||
ISmartProxyOptions
|
||||
} from './models/interfaces.js';
|
||||
import {
|
||||
isRoutedOptions
|
||||
} from './models/interfaces.js';
|
||||
// Route checking functions have been removed
|
||||
import type {
|
||||
IRouteConfig,
|
||||
IRouteAction,
|
||||
@ -291,11 +289,11 @@ export class RouteConnectionHandler {
|
||||
// Check default security settings
|
||||
const defaultSecuritySettings = this.settings.defaults?.security;
|
||||
if (defaultSecuritySettings) {
|
||||
if (defaultSecuritySettings.allowedIps && defaultSecuritySettings.allowedIps.length > 0) {
|
||||
if (defaultSecuritySettings.ipAllowList && defaultSecuritySettings.ipAllowList.length > 0) {
|
||||
const isAllowed = this.securityManager.isIPAuthorized(
|
||||
remoteIP,
|
||||
defaultSecuritySettings.allowedIps,
|
||||
defaultSecuritySettings.blockedIps || []
|
||||
defaultSecuritySettings.ipAllowList,
|
||||
defaultSecuritySettings.ipBlockList || []
|
||||
);
|
||||
|
||||
if (!isAllowed) {
|
||||
@ -316,7 +314,6 @@ export class RouteConnectionHandler {
|
||||
return this.setupDirectConnection(
|
||||
socket,
|
||||
record,
|
||||
undefined,
|
||||
serverName,
|
||||
initialChunk,
|
||||
undefined,
|
||||
@ -341,6 +338,22 @@ 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) {
|
||||
case 'forward':
|
||||
@ -371,6 +384,45 @@ export class RouteConnectionHandler {
|
||||
const connectionId = record.id;
|
||||
const action = route.action;
|
||||
|
||||
// Check if this route uses NFTables for forwarding
|
||||
if (action.forwardingEngine === 'nftables') {
|
||||
// Log detailed information about NFTables-handled connection
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(
|
||||
`[${record.id}] Connection forwarded by NFTables (kernel-level): ` +
|
||||
`${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` +
|
||||
` (Route: "${route.name || 'unnamed'}", Domain: ${record.lockedDomain || 'n/a'})`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`[${record.id}] NFTables forwarding: ${record.remoteIP} -> port ${record.localPort} (Route: "${route.name || 'unnamed'}")`
|
||||
);
|
||||
}
|
||||
|
||||
// Additional NFTables-specific logging if configured
|
||||
if (action.nftables) {
|
||||
const nftConfig = action.nftables;
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(
|
||||
`[${record.id}] NFTables config: ` +
|
||||
`protocol=${nftConfig.protocol || 'tcp'}, ` +
|
||||
`preserveSourceIP=${nftConfig.preserveSourceIP || false}, ` +
|
||||
`priority=${nftConfig.priority || 'default'}, ` +
|
||||
`maxRate=${nftConfig.maxRate || 'unlimited'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// We should have a target configuration for forwarding
|
||||
if (!action.target) {
|
||||
console.log(`[${connectionId}] Forward action missing target configuration`);
|
||||
@ -457,7 +509,6 @@ export class RouteConnectionHandler {
|
||||
return this.setupDirectConnection(
|
||||
socket,
|
||||
record,
|
||||
undefined,
|
||||
record.lockedDomain,
|
||||
initialChunk,
|
||||
undefined,
|
||||
@ -538,7 +589,6 @@ export class RouteConnectionHandler {
|
||||
return this.setupDirectConnection(
|
||||
socket,
|
||||
record,
|
||||
undefined,
|
||||
record.lockedDomain,
|
||||
initialChunk,
|
||||
undefined,
|
||||
@ -656,17 +706,12 @@ export class RouteConnectionHandler {
|
||||
this.connectionManager.initiateCleanupOnce(record, 'route_blocked');
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy connection handling has been removed in favor of pure route-based approach
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets up a direct connection to the target
|
||||
*/
|
||||
private setupDirectConnection(
|
||||
socket: plugins.net.Socket,
|
||||
record: IConnectionRecord,
|
||||
_unused?: any, // kept for backward compatibility
|
||||
serverName?: string,
|
||||
initialChunk?: Buffer,
|
||||
overridePort?: number,
|
||||
|
@ -6,12 +6,7 @@ import type {
|
||||
TPortRange
|
||||
} from './models/route-types.js';
|
||||
import type {
|
||||
ISmartProxyOptions,
|
||||
IRoutedSmartProxyOptions
|
||||
} from './models/interfaces.js';
|
||||
import {
|
||||
isRoutedOptions,
|
||||
isLegacyOptions
|
||||
ISmartProxyOptions
|
||||
} from './models/interfaces.js';
|
||||
|
||||
/**
|
||||
@ -29,12 +24,12 @@ export interface IRouteMatchResult {
|
||||
export class RouteManager extends plugins.EventEmitter {
|
||||
private routes: IRouteConfig[] = [];
|
||||
private portMap: Map<number, IRouteConfig[]> = new Map();
|
||||
private options: IRoutedSmartProxyOptions;
|
||||
private options: ISmartProxyOptions;
|
||||
|
||||
constructor(options: ISmartProxyOptions) {
|
||||
super();
|
||||
|
||||
// We no longer support legacy options, always use provided options
|
||||
// Store options
|
||||
this.options = options;
|
||||
|
||||
// Initialize routes from either source
|
||||
@ -218,8 +213,8 @@ export class RouteManager extends plugins.EventEmitter {
|
||||
}
|
||||
|
||||
// Check blocked IPs first
|
||||
if (security.blockedIps && security.blockedIps.length > 0) {
|
||||
for (const pattern of security.blockedIps) {
|
||||
if (security.ipBlockList && security.ipBlockList.length > 0) {
|
||||
for (const pattern of security.ipBlockList) {
|
||||
if (this.matchIpPattern(pattern, clientIp)) {
|
||||
return false; // IP is blocked
|
||||
}
|
||||
@ -227,8 +222,8 @@ export class RouteManager extends plugins.EventEmitter {
|
||||
}
|
||||
|
||||
// If there are allowed IPs, check them
|
||||
if (security.allowedIps && security.allowedIps.length > 0) {
|
||||
for (const pattern of security.allowedIps) {
|
||||
if (security.ipAllowList && security.ipAllowList.length > 0) {
|
||||
for (const pattern of security.ipAllowList) {
|
||||
if (this.matchIpPattern(pattern, clientIp)) {
|
||||
return true; // IP is allowed
|
||||
}
|
||||
|
@ -63,16 +63,15 @@ export class SecurityManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP is authorized using forwarding security rules
|
||||
* Check if an IP is authorized using security rules
|
||||
*
|
||||
* This method is used to determine if an IP is allowed to connect, based on security
|
||||
* rules configured in the forwarding configuration. The allowed and blocked IPs are
|
||||
* typically derived from domain.forwarding.security.allowedIps and blockedIps through
|
||||
* DomainConfigManager.getEffectiveIPRules().
|
||||
* rules configured in the route configuration. The allowed and blocked IPs are
|
||||
* typically derived from route.security.ipAllowList and ipBlockList.
|
||||
*
|
||||
* @param ip - The IP address to check
|
||||
* @param allowedIPs - Array of allowed IP patterns from forwarding.security.allowedIps
|
||||
* @param blockedIPs - Array of blocked IP patterns from forwarding.security.blockedIps
|
||||
* @param allowedIPs - Array of allowed IP patterns from security.ipAllowList
|
||||
* @param blockedIPs - Array of blocked IP patterns from security.ipBlockList
|
||||
* @returns true if IP is authorized, false if blocked
|
||||
*/
|
||||
public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean {
|
||||
@ -94,10 +93,10 @@ export class SecurityManager {
|
||||
* Check if the IP matches any of the glob patterns from security configuration
|
||||
*
|
||||
* This method checks IP addresses against glob patterns and handles IPv4/IPv6 normalization.
|
||||
* It's used to implement IP filtering based on the forwarding.security configuration.
|
||||
* It's used to implement IP filtering based on the route.security configuration.
|
||||
*
|
||||
* @param ip - The IP address to check
|
||||
* @param patterns - Array of glob patterns from forwarding.security.allowedIps or blockedIps
|
||||
* @param patterns - Array of glob patterns from security.ipAllowList or ipBlockList
|
||||
* @returns true if IP matches any pattern, false otherwise
|
||||
*/
|
||||
private isGlobIPMatch(ip: string, patterns: string[]): boolean {
|
||||
|
@ -9,6 +9,7 @@ import { TimeoutManager } from './timeout-manager.js';
|
||||
import { PortManager } from './port-manager.js';
|
||||
import { RouteManager } from './route-manager.js';
|
||||
import { RouteConnectionHandler } from './route-connection-handler.js';
|
||||
import { NFTablesManager } from './nftables-manager.js';
|
||||
|
||||
// External dependencies
|
||||
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
||||
@ -19,10 +20,8 @@ import { createPort80HandlerOptions } from '../../common/port80-adapter.js';
|
||||
|
||||
// Import types and utilities
|
||||
import type {
|
||||
ISmartProxyOptions,
|
||||
IRoutedSmartProxyOptions
|
||||
ISmartProxyOptions
|
||||
} from './models/interfaces.js';
|
||||
import { isRoutedOptions, isLegacyOptions } from './models/interfaces.js';
|
||||
import type { IRouteConfig } from './models/route-types.js';
|
||||
|
||||
/**
|
||||
@ -52,6 +51,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
private timeoutManager: TimeoutManager;
|
||||
public routeManager: RouteManager; // Made public for route management
|
||||
private routeConnectionHandler: RouteConnectionHandler;
|
||||
private nftablesManager: NFTablesManager;
|
||||
|
||||
// Port80Handler for ACME certificate management
|
||||
private port80Handler: Port80Handler | null = null;
|
||||
@ -84,7 +84,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
* ],
|
||||
* defaults: {
|
||||
* target: { host: 'localhost', port: 8080 },
|
||||
* security: { allowedIps: ['*'] }
|
||||
* security: { ipAllowList: ['*'] }
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
@ -169,6 +169,9 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
|
||||
// Initialize port manager
|
||||
this.portManager = new PortManager(this.settings, this.routeConnectionHandler);
|
||||
|
||||
// Initialize NFTablesManager
|
||||
this.nftablesManager = new NFTablesManager(this.settings);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -272,6 +275,13 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
// Get listening ports from RouteManager
|
||||
const listeningPorts = this.routeManager.getListeningPorts();
|
||||
|
||||
// Provision NFTables rules for routes that use NFTables
|
||||
for (const route of this.settings.routes) {
|
||||
if (route.action.forwardingEngine === 'nftables') {
|
||||
await this.nftablesManager.provisionRoute(route);
|
||||
}
|
||||
}
|
||||
|
||||
// Start port listeners using the PortManager
|
||||
await this.portManager.addPorts(listeningPorts);
|
||||
|
||||
@ -366,6 +376,10 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
await this.certProvisioner.stop();
|
||||
console.log('CertProvisioner stopped');
|
||||
}
|
||||
|
||||
// Stop NFTablesManager
|
||||
await this.nftablesManager.stop();
|
||||
console.log('NFTablesManager stopped');
|
||||
|
||||
// Stop the Port80Handler if running
|
||||
if (this.port80Handler) {
|
||||
@ -434,6 +448,39 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
|
||||
console.log(`Updating routes (${newRoutes.length} routes)`);
|
||||
|
||||
// Get existing routes that use NFTables
|
||||
const oldNfTablesRoutes = this.settings.routes.filter(
|
||||
r => r.action.forwardingEngine === 'nftables'
|
||||
);
|
||||
|
||||
// Get new routes that use NFTables
|
||||
const newNfTablesRoutes = newRoutes.filter(
|
||||
r => r.action.forwardingEngine === 'nftables'
|
||||
);
|
||||
|
||||
// Find routes to remove, update, or add
|
||||
for (const oldRoute of oldNfTablesRoutes) {
|
||||
const newRoute = newNfTablesRoutes.find(r => r.name === oldRoute.name);
|
||||
|
||||
if (!newRoute) {
|
||||
// Route was removed
|
||||
await this.nftablesManager.deprovisionRoute(oldRoute);
|
||||
} else {
|
||||
// Route was updated
|
||||
await this.nftablesManager.updateRoute(oldRoute, newRoute);
|
||||
}
|
||||
}
|
||||
|
||||
// Find new routes to add
|
||||
for (const newRoute of newNfTablesRoutes) {
|
||||
const oldRoute = oldNfTablesRoutes.find(r => r.name === newRoute.name);
|
||||
|
||||
if (!oldRoute) {
|
||||
// New route
|
||||
await this.nftablesManager.provisionRoute(newRoute);
|
||||
}
|
||||
}
|
||||
|
||||
// Update routes in RouteManager
|
||||
this.routeManager.updateRoutes(newRoutes);
|
||||
|
||||
@ -442,6 +489,9 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
|
||||
// Update port listeners to match the new configuration
|
||||
await this.portManager.updatePorts(requiredPorts);
|
||||
|
||||
// Update settings with the new routes
|
||||
this.settings.routes = newRoutes;
|
||||
|
||||
// If NetworkProxy is initialized, resync the configurations
|
||||
if (this.networkProxyBridge.getNetworkProxy()) {
|
||||
@ -650,7 +700,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
const domains: string[] = [];
|
||||
|
||||
// Get domains from routes
|
||||
const routes = isRoutedOptions(this.settings) ? this.settings.routes : [];
|
||||
const routes = this.settings.routes || [];
|
||||
|
||||
for (const route of routes) {
|
||||
if (!route.match.domains) continue;
|
||||
@ -678,6 +728,13 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
return domains;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NFTables status
|
||||
*/
|
||||
public async getNfTablesStatus(): Promise<Record<string, any>> {
|
||||
return this.nftablesManager.getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status of certificates managed by Port80Handler
|
||||
*/
|
||||
|
@ -5,7 +5,7 @@
|
||||
* including helpers, validators, utilities, and patterns for working with routes.
|
||||
*/
|
||||
|
||||
// Export route helpers for creating routes
|
||||
// Export route helpers for creating route configurations
|
||||
export * from './route-helpers.js';
|
||||
|
||||
// Export route validators for validating route configurations
|
||||
@ -35,6 +35,4 @@ export {
|
||||
addJwtAuth
|
||||
};
|
||||
|
||||
// Export migration utilities for transitioning from domain-based to route-based configs
|
||||
// Note: These will be removed in a future version once migration is complete
|
||||
export * from './route-migration-utils.js';
|
||||
// Migration utilities have been removed as they are no longer needed
|
@ -16,6 +16,7 @@
|
||||
* - WebSocket routes (createWebSocketRoute)
|
||||
* - Port mapping routes (createPortMappingRoute, createOffsetPortMappingRoute)
|
||||
* - Dynamic routing (createDynamicRoute, createSmartLoadBalancer)
|
||||
* - NFTables routes (createNfTablesRoute, createNfTablesTerminateRoute)
|
||||
*/
|
||||
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../models/route-types.js';
|
||||
@ -618,4 +619,195 @@ export function createSmartLoadBalancer(options: {
|
||||
priority: options.priority,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an NFTables-based route for high-performance packet forwarding
|
||||
* @param nameOrDomains Name or domain(s) to match
|
||||
* @param target Target host and port
|
||||
* @param options Additional route options
|
||||
* @returns Route configuration object
|
||||
*/
|
||||
export function createNfTablesRoute(
|
||||
nameOrDomains: string | string[],
|
||||
target: { host: string; port: number | 'preserve' },
|
||||
options: {
|
||||
ports?: TPortRange;
|
||||
protocol?: 'tcp' | 'udp' | 'all';
|
||||
preserveSourceIP?: boolean;
|
||||
ipAllowList?: string[];
|
||||
ipBlockList?: string[];
|
||||
maxRate?: string;
|
||||
priority?: number;
|
||||
useTls?: boolean;
|
||||
tableName?: string;
|
||||
useIPSets?: boolean;
|
||||
useAdvancedNAT?: boolean;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
// Determine if this is a name or domain
|
||||
let name: string;
|
||||
let domains: string | string[] | undefined;
|
||||
|
||||
if (Array.isArray(nameOrDomains) || (typeof nameOrDomains === 'string' && nameOrDomains.includes('.'))) {
|
||||
domains = nameOrDomains;
|
||||
name = Array.isArray(nameOrDomains) ? nameOrDomains[0] : nameOrDomains;
|
||||
} else {
|
||||
name = nameOrDomains;
|
||||
domains = undefined; // No domains
|
||||
}
|
||||
|
||||
// Create route match
|
||||
const match: IRouteMatch = {
|
||||
domains,
|
||||
ports: options.ports || 80
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: target.host,
|
||||
port: target.port
|
||||
},
|
||||
forwardingEngine: 'nftables',
|
||||
nftables: {
|
||||
protocol: options.protocol || 'tcp',
|
||||
preserveSourceIP: options.preserveSourceIP,
|
||||
maxRate: options.maxRate,
|
||||
priority: options.priority,
|
||||
tableName: options.tableName,
|
||||
useIPSets: options.useIPSets,
|
||||
useAdvancedNAT: options.useAdvancedNAT
|
||||
}
|
||||
};
|
||||
|
||||
// Add security if allowed or blocked IPs are specified
|
||||
if (options.ipAllowList?.length || options.ipBlockList?.length) {
|
||||
action.security = {
|
||||
ipAllowList: options.ipAllowList,
|
||||
ipBlockList: options.ipBlockList
|
||||
};
|
||||
}
|
||||
|
||||
// Add TLS options if needed
|
||||
if (options.useTls) {
|
||||
action.tls = {
|
||||
mode: 'passthrough'
|
||||
};
|
||||
}
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
name,
|
||||
match,
|
||||
action
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an NFTables-based TLS termination route
|
||||
* @param nameOrDomains Name or domain(s) to match
|
||||
* @param target Target host and port
|
||||
* @param options Additional route options
|
||||
* @returns Route configuration object
|
||||
*/
|
||||
export function createNfTablesTerminateRoute(
|
||||
nameOrDomains: string | string[],
|
||||
target: { host: string; port: number | 'preserve' },
|
||||
options: {
|
||||
ports?: TPortRange;
|
||||
protocol?: 'tcp' | 'udp' | 'all';
|
||||
preserveSourceIP?: boolean;
|
||||
ipAllowList?: string[];
|
||||
ipBlockList?: string[];
|
||||
maxRate?: string;
|
||||
priority?: number;
|
||||
tableName?: string;
|
||||
useIPSets?: boolean;
|
||||
useAdvancedNAT?: boolean;
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
// Create basic NFTables route
|
||||
const route = createNfTablesRoute(
|
||||
nameOrDomains,
|
||||
target,
|
||||
{
|
||||
...options,
|
||||
ports: options.ports || 443,
|
||||
useTls: false
|
||||
}
|
||||
);
|
||||
|
||||
// Set TLS termination
|
||||
route.action.tls = {
|
||||
mode: 'terminate',
|
||||
certificate: options.certificate || 'auto'
|
||||
};
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a complete NFTables-based HTTPS setup with HTTP redirect
|
||||
* @param nameOrDomains Name or domain(s) to match
|
||||
* @param target Target host and port
|
||||
* @param options Additional route options
|
||||
* @returns Array of two route configurations (HTTPS and HTTP redirect)
|
||||
*/
|
||||
export function createCompleteNfTablesHttpsServer(
|
||||
nameOrDomains: string | string[],
|
||||
target: { host: string; port: number | 'preserve' },
|
||||
options: {
|
||||
httpPort?: TPortRange;
|
||||
httpsPort?: TPortRange;
|
||||
protocol?: 'tcp' | 'udp' | 'all';
|
||||
preserveSourceIP?: boolean;
|
||||
ipAllowList?: string[];
|
||||
ipBlockList?: string[];
|
||||
maxRate?: string;
|
||||
priority?: number;
|
||||
tableName?: string;
|
||||
useIPSets?: boolean;
|
||||
useAdvancedNAT?: boolean;
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
} = {}
|
||||
): IRouteConfig[] {
|
||||
// Create the HTTPS route using NFTables
|
||||
const httpsRoute = createNfTablesTerminateRoute(
|
||||
nameOrDomains,
|
||||
target,
|
||||
{
|
||||
...options,
|
||||
ports: options.httpsPort || 443
|
||||
}
|
||||
);
|
||||
|
||||
// Determine the domain(s) for HTTP redirect
|
||||
const domains = typeof nameOrDomains === 'string' && !nameOrDomains.includes('.')
|
||||
? undefined
|
||||
: nameOrDomains;
|
||||
|
||||
// Extract the HTTPS port for the redirect destination
|
||||
const httpsPort = typeof options.httpsPort === 'number'
|
||||
? options.httpsPort
|
||||
: Array.isArray(options.httpsPort) && typeof options.httpsPort[0] === 'number'
|
||||
? options.httpsPort[0]
|
||||
: 443;
|
||||
|
||||
// Create the HTTP redirect route (this uses standard forwarding, not NFTables)
|
||||
const httpRedirectRoute = createHttpToHttpsRedirect(
|
||||
domains as any, // Type cast needed since domains can be undefined now
|
||||
httpsPort,
|
||||
{
|
||||
match: {
|
||||
ports: options.httpPort || 80,
|
||||
domains: domains as any // Type cast needed since domains can be undefined now
|
||||
},
|
||||
name: `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains || 'all domains'}`
|
||||
}
|
||||
);
|
||||
|
||||
return [httpsRoute, httpRedirectRoute];
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
/**
|
||||
* Route Migration Utilities
|
||||
*
|
||||
* This file provides utility functions for migrating from legacy domain-based
|
||||
* configuration to the new route-based configuration system. These functions
|
||||
* are temporary and will be removed after the migration is complete.
|
||||
*/
|
||||
|
||||
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget } from '../models/route-types.js';
|
||||
|
||||
/**
|
||||
* Legacy domain config interface (for migration only)
|
||||
* @deprecated This interface will be removed in a future version
|
||||
*/
|
||||
export interface ILegacyDomainConfig {
|
||||
domains: string[];
|
||||
forwarding: {
|
||||
type: TForwardingType;
|
||||
target: {
|
||||
host: string | string[];
|
||||
port: number;
|
||||
};
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a legacy domain config to a route-based config
|
||||
* @param domainConfig Legacy domain configuration
|
||||
* @param additionalOptions Additional options to add to the route
|
||||
* @returns Route configuration
|
||||
* @deprecated This function will be removed in a future version
|
||||
*/
|
||||
export function domainConfigToRouteConfig(
|
||||
domainConfig: ILegacyDomainConfig,
|
||||
additionalOptions: Partial<IRouteConfig> = {}
|
||||
): IRouteConfig {
|
||||
// Default port based on forwarding type
|
||||
let defaultPort = 80;
|
||||
let tlsMode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt' | undefined;
|
||||
|
||||
switch (domainConfig.forwarding.type) {
|
||||
case 'http-only':
|
||||
defaultPort = 80;
|
||||
break;
|
||||
case 'https-passthrough':
|
||||
defaultPort = 443;
|
||||
tlsMode = 'passthrough';
|
||||
break;
|
||||
case 'https-terminate-to-http':
|
||||
defaultPort = 443;
|
||||
tlsMode = 'terminate';
|
||||
break;
|
||||
case 'https-terminate-to-https':
|
||||
defaultPort = 443;
|
||||
tlsMode = 'terminate-and-reencrypt';
|
||||
break;
|
||||
}
|
||||
|
||||
// Create route match criteria
|
||||
const match: IRouteMatch = {
|
||||
ports: additionalOptions.match?.ports || defaultPort,
|
||||
domains: domainConfig.domains
|
||||
};
|
||||
|
||||
// Create route target
|
||||
const target: IRouteTarget = {
|
||||
host: domainConfig.forwarding.target.host,
|
||||
port: domainConfig.forwarding.target.port
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'forward',
|
||||
target
|
||||
};
|
||||
|
||||
// Add TLS configuration if needed
|
||||
if (tlsMode) {
|
||||
action.tls = {
|
||||
mode: tlsMode,
|
||||
certificate: 'auto'
|
||||
};
|
||||
|
||||
// If the legacy config has custom certificates, use them
|
||||
if (domainConfig.forwarding.https?.customCert) {
|
||||
action.tls.certificate = {
|
||||
key: domainConfig.forwarding.https.customCert.key,
|
||||
cert: domainConfig.forwarding.https.customCert.cert
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Add security options if present
|
||||
if (domainConfig.forwarding.security) {
|
||||
action.security = domainConfig.forwarding.security;
|
||||
}
|
||||
|
||||
// Create the route config
|
||||
const routeConfig: IRouteConfig = {
|
||||
match,
|
||||
action,
|
||||
// Include a name based on domains if not provided
|
||||
name: additionalOptions.name || `Legacy route for ${domainConfig.domains.join(', ')}`,
|
||||
// Include a note that this was converted from a legacy config
|
||||
description: additionalOptions.description || 'Converted from legacy domain configuration'
|
||||
};
|
||||
|
||||
// Add optional properties if provided
|
||||
if (additionalOptions.priority !== undefined) {
|
||||
routeConfig.priority = additionalOptions.priority;
|
||||
}
|
||||
|
||||
if (additionalOptions.tags) {
|
||||
routeConfig.tags = additionalOptions.tags;
|
||||
}
|
||||
|
||||
return routeConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an array of legacy domain configs to route configurations
|
||||
* @param domainConfigs Array of legacy domain configurations
|
||||
* @returns Array of route configurations
|
||||
* @deprecated This function will be removed in a future version
|
||||
*/
|
||||
export function domainConfigsToRouteConfigs(
|
||||
domainConfigs: ILegacyDomainConfig[]
|
||||
): IRouteConfig[] {
|
||||
return domainConfigs.map(config => domainConfigToRouteConfig(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract domains from a route configuration
|
||||
* @param route Route configuration
|
||||
* @returns Array of domains
|
||||
*/
|
||||
export function extractDomainsFromRoute(route: IRouteConfig): string[] {
|
||||
if (!route.match.domains) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract domains from an array of route configurations
|
||||
* @param routes Array of route configurations
|
||||
* @returns Array of unique domains
|
||||
*/
|
||||
export function extractDomainsFromRoutes(routes: IRouteConfig[]): string[] {
|
||||
const domains = new Set<string>();
|
||||
|
||||
for (const route of routes) {
|
||||
const routeDomains = extractDomainsFromRoute(route);
|
||||
for (const domain of routeDomains) {
|
||||
domains.add(domain);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(domains);
|
||||
}
|
@ -5,10 +5,154 @@
|
||||
* These patterns can be used as templates for creating route configurations.
|
||||
*/
|
||||
|
||||
import type { IRouteConfig } from '../models/route-types.js';
|
||||
import { createHttpRoute, createHttpsTerminateRoute, createHttpsPassthroughRoute, createCompleteHttpsServer } from './route-helpers.js';
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget } from '../models/route-types.js';
|
||||
import { mergeRouteConfigs } from './route-utils.js';
|
||||
|
||||
/**
|
||||
* Create a basic HTTP route configuration
|
||||
*/
|
||||
export function createHttpRoute(
|
||||
domains: string | string[],
|
||||
target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) },
|
||||
options: Partial<IRouteConfig> = {}
|
||||
): IRouteConfig {
|
||||
const route: IRouteConfig = {
|
||||
match: {
|
||||
domains,
|
||||
ports: 80
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: target.host,
|
||||
port: target.port
|
||||
}
|
||||
},
|
||||
name: options.name || `HTTP: ${Array.isArray(domains) ? domains.join(', ') : domains}`
|
||||
};
|
||||
|
||||
return mergeRouteConfigs(route, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTPS route with TLS termination
|
||||
*/
|
||||
export function createHttpsTerminateRoute(
|
||||
domains: string | string[],
|
||||
target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) },
|
||||
options: Partial<IRouteConfig> & {
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
reencrypt?: boolean;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
const route: IRouteConfig = {
|
||||
match: {
|
||||
domains,
|
||||
ports: 443
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: target.host,
|
||||
port: target.port
|
||||
},
|
||||
tls: {
|
||||
mode: options.reencrypt ? 'terminate-and-reencrypt' : 'terminate',
|
||||
certificate: options.certificate || 'auto'
|
||||
}
|
||||
},
|
||||
name: options.name || `HTTPS (terminate): ${Array.isArray(domains) ? domains.join(', ') : domains}`
|
||||
};
|
||||
|
||||
return mergeRouteConfigs(route, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTPS route with TLS passthrough
|
||||
*/
|
||||
export function createHttpsPassthroughRoute(
|
||||
domains: string | string[],
|
||||
target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) },
|
||||
options: Partial<IRouteConfig> = {}
|
||||
): IRouteConfig {
|
||||
const route: IRouteConfig = {
|
||||
match: {
|
||||
domains,
|
||||
ports: 443
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: target.host,
|
||||
port: target.port
|
||||
},
|
||||
tls: {
|
||||
mode: 'passthrough'
|
||||
}
|
||||
},
|
||||
name: options.name || `HTTPS (passthrough): ${Array.isArray(domains) ? domains.join(', ') : domains}`
|
||||
};
|
||||
|
||||
return mergeRouteConfigs(route, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP to HTTPS redirect route
|
||||
*/
|
||||
export function createHttpToHttpsRedirect(
|
||||
domains: string | string[],
|
||||
options: Partial<IRouteConfig> & {
|
||||
redirectCode?: 301 | 302 | 307 | 308;
|
||||
preservePath?: boolean;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
const route: IRouteConfig = {
|
||||
match: {
|
||||
domains,
|
||||
ports: 80
|
||||
},
|
||||
action: {
|
||||
type: 'redirect',
|
||||
redirect: {
|
||||
to: options.preservePath ? 'https://{domain}{path}' : 'https://{domain}',
|
||||
status: options.redirectCode || 301
|
||||
}
|
||||
},
|
||||
name: options.name || `HTTP to HTTPS redirect: ${Array.isArray(domains) ? domains.join(', ') : domains}`
|
||||
};
|
||||
|
||||
return mergeRouteConfigs(route, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a complete HTTPS server with redirect from HTTP
|
||||
*/
|
||||
export function createCompleteHttpsServer(
|
||||
domains: string | string[],
|
||||
target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) },
|
||||
options: Partial<IRouteConfig> & {
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
tlsMode?: 'terminate' | 'passthrough' | 'terminate-and-reencrypt';
|
||||
redirectCode?: 301 | 302 | 307 | 308;
|
||||
} = {}
|
||||
): IRouteConfig[] {
|
||||
// Create the TLS route based on the selected mode
|
||||
const tlsRoute = options.tlsMode === 'passthrough'
|
||||
? createHttpsPassthroughRoute(domains, target, options)
|
||||
: createHttpsTerminateRoute(domains, target, {
|
||||
...options,
|
||||
reencrypt: options.tlsMode === 'terminate-and-reencrypt'
|
||||
});
|
||||
|
||||
// Create the HTTP to HTTPS redirect route
|
||||
const redirectRoute = createHttpToHttpsRedirect(domains, {
|
||||
redirectCode: options.redirectCode,
|
||||
preservePath: true
|
||||
});
|
||||
|
||||
return [tlsRoute, redirectRoute];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an API Gateway route pattern
|
||||
* @param domains Domain(s) to match
|
||||
|
Reference in New Issue
Block a user