@push.rocks/smartproxy
Version:
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.
237 lines • 16.3 kB
JavaScript
import * as plugins from '../../plugins.js';
import { NfTablesProxy } from '../nftables-proxy/nftables-proxy.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 {
/**
* Creates a new NFTablesManager
*
* @param smartProxy The SmartProxy instance
*/
constructor(smartProxy) {
this.smartProxy = smartProxy;
this.rulesMap = new Map();
}
/**
* Provision NFTables rules for a route
*
* @param route The route configuration
* @returns A promise that resolves to true if successful, false otherwise
*/
async provisionRoute(route) {
// 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
*/
async deprovisionRoute(route) {
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
*/
async updateRoute(oldRoute, newRoute) {
// 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
*/
generateRouteId(route) {
// 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
*/
createNfTablesOptions(route) {
const { action } = route;
// Ensure we have targets
if (!action.targets || action.targets.length === 0) {
throw new Error('Route must have targets to use NFTables forwarding');
}
// NFTables can only handle a single target, so we use the first target without match criteria
// or the first target if all have match criteria
const defaultTarget = action.targets.find(t => !t.match) || action.targets[0];
// Convert port specifications
const fromPorts = this.expandPortRange(route.match.ports);
// Determine target port
let toPorts;
if (defaultTarget.port === 'preserve') {
// 'preserve' means use the same ports as the source
toPorts = fromPorts;
}
else if (typeof defaultTarget.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 = defaultTarget.port;
}
// Determine target host
let toHost;
if (typeof defaultTarget.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(defaultTarget.host)) {
// Use first host for now - NFTables will do simple round-robin
toHost = defaultTarget.host[0];
}
else {
toHost = defaultTarget.host;
}
// Create options
const options = {
fromPort: fromPorts,
toPort: toPorts,
toHost: toHost,
protocol: action.nftables?.protocol || 'tcp',
preserveSourceIP: action.nftables?.preserveSourceIP !== undefined ?
action.nftables.preserveSourceIP :
this.smartProxy.settings.preserveSourceIP,
useIPSets: action.nftables?.useIPSets !== false,
useAdvancedNAT: action.nftables?.useAdvancedNAT,
enableLogging: this.smartProxy.settings.enableDetailedLogging,
deleteOnExit: true,
tableName: action.nftables?.tableName || 'smartproxy'
};
// Add security-related options
if (route.security?.ipAllowList?.length) {
options.ipAllowList = route.security.ipAllowList;
}
if (route.security?.ipBlockList?.length) {
options.ipBlockList = route.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
*/
expandPortRange(ports) {
// Process different port specifications
if (typeof ports === 'number') {
return ports;
}
else if (Array.isArray(ports)) {
const result = [];
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.from, to: ports.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
*/
async getStatus() {
const result = {};
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
*/
isRouteProvisioned(route) {
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
*/
async stop() {
// Stop all NFTables proxies
const stopPromises = Array.from(this.rulesMap.values()).map(proxy => proxy.stop());
await Promise.all(stopPromises);
this.rulesMap.clear();
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibmZ0YWJsZXMtbWFuYWdlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL3Byb3hpZXMvc21hcnQtcHJveHkvbmZ0YWJsZXMtbWFuYWdlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxxQ0FBcUMsQ0FBQztBQWFwRTs7Ozs7O0dBTUc7QUFDSCxNQUFNLE9BQU8sZUFBZTtJQUcxQjs7OztPQUlHO0lBQ0gsWUFBb0IsVUFBc0I7UUFBdEIsZUFBVSxHQUFWLFVBQVUsQ0FBWTtRQVBsQyxhQUFRLEdBQStCLElBQUksR0FBRyxFQUFFLENBQUM7SUFPWixDQUFDO0lBRTlDOzs7OztPQUtHO0lBQ0ksS0FBSyxDQUFDLGNBQWMsQ0FBQyxLQUFtQjtRQUM3QyxzQ0FBc0M7UUFDdEMsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLGVBQWUsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUU1QyxxQ0FBcUM7UUFDckMsSUFBSSxLQUFLLENBQUMsTUFBTSxDQUFDLGdCQUFnQixLQUFLLFVBQVUsRUFBRSxDQUFDO1lBQ2pELE9BQU8sSUFBSSxDQUFDO1FBQ2QsQ0FBQztRQUVELG1EQUFtRDtRQUNuRCxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMscUJBQXFCLENBQUMsS0FBSyxDQUFDLENBQUM7UUFFckQsNkNBQTZDO1FBQzdDLE1BQU0sS0FBSyxHQUFHLElBQUksYUFBYSxDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBRTVDLElBQUksQ0FBQztZQUNILE1BQU0sS0FBSyxDQUFDLEtBQUssRUFBRSxDQUFDO1lBQ3BCLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLE9BQU8sRUFBRSxLQUFLLENBQUMsQ0FBQztZQUNsQyxPQUFPLElBQUksQ0FBQztRQUNkLENBQUM7UUFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO1lBQ2IsT0FBTyxDQUFD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