diff --git a/changelog.md b/changelog.md index 17c0100..5bf38af 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-02-09 - 23.0.0 - BREAKING CHANGE(proxies/nftables-proxy) +remove nftables-proxy implementation, models, and utilities from the repository + +- Deleted nftables-proxy module files under ts/proxies/nftables-proxy (index, models, utils, command executor, validators, etc.) +- Removed nftables-proxy exports from ts/index.ts and ts/proxies/index.ts +- Updated smart-proxy types to drop dependency on nftables proxy models +- Breaking change: any consumers importing nftables-proxy will no longer find those exports; update imports or install/use the extracted/alternative package if applicable + ## 2026-02-09 - 22.6.0 - feat(smart-proxy) add socket-handler relay, fast-path port-only forwarding, metrics and bridge improvements, and various TS/Rust integration fixes diff --git a/readme.md b/readme.md index b129d94..767452f 100644 --- a/readme.md +++ b/readme.md @@ -730,32 +730,6 @@ interface ISmartProxyOptions { } ``` -### NfTablesProxy Class - -A standalone class for managing nftables NAT rules directly (Linux only, requires root): - -```typescript -import { NfTablesProxy } from '@push.rocks/smartproxy'; - -const nftProxy = new NfTablesProxy({ - fromPort: [80, 443], - toPort: [8080, 8443], - toHost: 'backend-server', - protocol: 'tcp', - preserveSourceIP: true, - ipv6Support: true, - useIPSets: true, - qos: { - enabled: true, - maxRate: '1gbps' - } -}); - -await nftProxy.start(); // Apply nftables rules -const status = await nftProxy.getStatus(); -await nftProxy.stop(); // Remove rules -``` - ## 🐛 Troubleshooting ### Certificate Issues diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 1636f72..00aa051 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '22.6.0', + version: '23.0.0', 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.' } diff --git a/ts/index.ts b/ts/index.ts index 3a60d3b..8600d14 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -2,9 +2,6 @@ * SmartProxy main module exports */ -// NFTables proxy exports -export * from './proxies/nftables-proxy/index.js'; - // Export SmartProxy elements export { SmartProxy } from './proxies/smart-proxy/index.js'; export { SharedRouteManager as RouteManager } from './core/routing/route-manager.js'; diff --git a/ts/proxies/index.ts b/ts/proxies/index.ts index 3bdfae9..bcd232e 100644 --- a/ts/proxies/index.ts +++ b/ts/proxies/index.ts @@ -8,6 +8,3 @@ export { SharedRouteManager as SmartProxyRouteManager } from '../core/routing/ro export * from './smart-proxy/utils/index.js'; // Export smart-proxy models except IAcmeOptions export type { ISmartProxyOptions, IConnectionRecord, IRouteConfig, IRouteMatch, IRouteAction, IRouteTls, IRouteContext } from './smart-proxy/models/index.js'; - -// Export NFTables proxy (no conflicts) -export * from './nftables-proxy/index.js'; diff --git a/ts/proxies/nftables-proxy/index.ts b/ts/proxies/nftables-proxy/index.ts deleted file mode 100644 index b7eecea..0000000 --- a/ts/proxies/nftables-proxy/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * NfTablesProxy implementation - */ -export * from './nftables-proxy.js'; -export * from './models/index.js'; -export * from './utils/index.js'; diff --git a/ts/proxies/nftables-proxy/models/errors.ts b/ts/proxies/nftables-proxy/models/errors.ts deleted file mode 100644 index 42f259d..0000000 --- a/ts/proxies/nftables-proxy/models/errors.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Custom error classes for better error handling - */ -export class NftBaseError extends Error { - constructor(message: string) { - super(message); - this.name = 'NftBaseError'; - } -} - -export class NftValidationError extends NftBaseError { - constructor(message: string) { - super(message); - this.name = 'NftValidationError'; - } -} - -export class NftExecutionError extends NftBaseError { - constructor(message: string) { - super(message); - this.name = 'NftExecutionError'; - } -} - -export class NftResourceError extends NftBaseError { - constructor(message: string) { - super(message); - this.name = 'NftResourceError'; - } -} \ No newline at end of file diff --git a/ts/proxies/nftables-proxy/models/index.ts b/ts/proxies/nftables-proxy/models/index.ts deleted file mode 100644 index a5bf3a7..0000000 --- a/ts/proxies/nftables-proxy/models/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Export all models - */ -export * from './interfaces.js'; -export * from './errors.js'; \ No newline at end of file diff --git a/ts/proxies/nftables-proxy/models/interfaces.ts b/ts/proxies/nftables-proxy/models/interfaces.ts deleted file mode 100644 index 5d0f637..0000000 --- a/ts/proxies/nftables-proxy/models/interfaces.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Interfaces for NfTablesProxy - */ - -/** - * Represents a port range for forwarding - */ -export interface PortRange { - from: number; - to: number; -} - -// Legacy interface name for backward compatibility -export type IPortRange = PortRange; - -/** - * Settings for NfTablesProxy. - */ -export interface NfTableProxyOptions { - // Basic settings - fromPort: number | PortRange | Array; // Support single port, port range, or multiple ports/ranges - toPort: number | PortRange | Array; - toHost?: string; // Target host for proxying; defaults to 'localhost' - - // Advanced settings - preserveSourceIP?: boolean; // If true, the original source IP is preserved - deleteOnExit?: boolean; // If true, clean up rules before process exit - protocol?: 'tcp' | 'udp' | 'all'; // Protocol to forward, defaults to 'tcp' - enableLogging?: boolean; // Enable detailed logging - ipv6Support?: boolean; // Enable IPv6 support - logFormat?: 'plain' | 'json'; // Format for logs - - // Source filtering - 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 - forceCleanSlate?: boolean; // Clear all NfTablesProxy rules before starting - tableName?: string; // Custom table name (defaults to 'portproxy') - - // Connection management - maxRetries?: number; // Maximum number of retries for failed commands - retryDelayMs?: number; // Delay between retries in milliseconds - useAdvancedNAT?: boolean; // Use connection tracking for stateful NAT - - // Quality of Service - qos?: { - enabled: boolean; - maxRate?: string; // e.g. "10mbps" - priority?: number; // 1 (highest) to 10 (lowest) - markConnections?: boolean; // Mark connections for easier management - }; - - // Integration with PortProxy/NetworkProxy - netProxyIntegration?: { - enabled: boolean; - redirectLocalhost?: boolean; // Redirect localhost traffic to NetworkProxy - sslTerminationPort?: number; // Port where NetworkProxy handles SSL termination - }; -} - -// Legacy interface name for backward compatibility -export type INfTableProxySettings = NfTableProxyOptions; - -/** - * Interface for status reporting - */ -export interface NfTablesStatus { - active: boolean; - ruleCount: { - total: number; - added: number; - verified: number; - }; - tablesConfigured: { family: string; tableName: string }[]; - metrics: { - forwardedConnections?: number; - activeConnections?: number; - bytesForwarded?: { - sent: number; - received: number; - }; - }; - qosEnabled?: boolean; - ipSetsConfigured?: { - name: string; - elementCount: number; - type: string; - }[]; -} - -// Legacy interface name for backward compatibility -export type INfTablesStatus = NfTablesStatus; \ No newline at end of file diff --git a/ts/proxies/nftables-proxy/nftables-proxy.ts b/ts/proxies/nftables-proxy/nftables-proxy.ts deleted file mode 100644 index 5fd75c9..0000000 --- a/ts/proxies/nftables-proxy/nftables-proxy.ts +++ /dev/null @@ -1,1754 +0,0 @@ -import { exec, execSync } from 'child_process'; -import { promisify } from 'util'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import { AsyncFileSystem } from '../../core/utils/fs-utils.js'; -import { - NftValidationError, - NftExecutionError, - NftResourceError -} from './models/index.js'; -import type { - PortRange, - NfTableProxyOptions, - NfTablesStatus -} from './models/index.js'; -import { - NftCommandExecutor, - normalizePortSpec, - validateSettings, - filterIPsByFamily -} from './utils/index.js'; - -const execAsync = promisify(exec); - -/** - * Represents a rule added to nftables - */ -interface NfTablesRule { - handle?: number; // Rule handle for deletion - tableFamily: string; // 'ip' or 'ip6' - tableName: string; // Table name - chainName: string; // Chain name - ruleContents: string; // Rule definition - added: boolean; // Whether the rule was successfully added - verified?: boolean; // Whether the rule has been verified as applied -} - -/** - * NfTablesProxy sets up nftables NAT rules to forward TCP traffic. - * Enhanced with multi-port support, IPv6, connection tracking, metrics, - * and more advanced features. - */ -export class NfTablesProxy { - public settings: NfTableProxyOptions; - private rules: NfTablesRule[] = []; - private ipSets: Map = new Map(); // Store IP sets for tracking - private ruleTag: string; - private tableName: string; - private tempFilePath: string; - private executor: NftCommandExecutor; - private static NFT_CMD = 'nft'; - - constructor(settings: NfTableProxyOptions) { - // Validate inputs to prevent command injection - validateSettings(settings); - - // Set default settings - this.settings = { - ...settings, - toHost: settings.toHost || 'localhost', - protocol: settings.protocol || 'tcp', - enableLogging: settings.enableLogging !== undefined ? settings.enableLogging : false, - ipv6Support: settings.ipv6Support !== undefined ? settings.ipv6Support : false, - tableName: settings.tableName || 'portproxy', - logFormat: settings.logFormat || 'plain', - useIPSets: settings.useIPSets !== undefined ? settings.useIPSets : true, - maxRetries: settings.maxRetries || 3, - retryDelayMs: settings.retryDelayMs || 1000, - useAdvancedNAT: settings.useAdvancedNAT !== undefined ? settings.useAdvancedNAT : false, - }; - - // Generate a unique identifier for the rules added by this instance - this.ruleTag = `NfTablesProxy:${Date.now()}:${Math.random().toString(36).substr(2, 5)}`; - - // Set table name - this.tableName = this.settings.tableName || 'portproxy'; - - // Create a temp file path for batch operations - this.tempFilePath = path.join(os.tmpdir(), `nft-rules-${Date.now()}.nft`); - - // Create the command executor - this.executor = new NftCommandExecutor( - (level, message, data) => this.log(level, message, data), - { - maxRetries: this.settings.maxRetries, - retryDelayMs: this.settings.retryDelayMs, - tempFilePath: this.tempFilePath - } - ); - - // Register cleanup handlers if deleteOnExit is true - if (this.settings.deleteOnExit) { - // Synchronous cleanup for 'exit' event (only sync code runs here) - const syncCleanup = () => { - try { - this.stopSync(); - } catch (err) { - this.log('error', 'Error cleaning nftables rules on exit:', { error: err.message }); - } - }; - - // Async cleanup for signal handlers (preferred, non-blocking) - const asyncCleanup = async () => { - try { - await this.stop(); - } catch (err) { - this.log('error', 'Error cleaning nftables rules on signal:', { error: err.message }); - } - }; - - process.on('exit', syncCleanup); - process.on('SIGINT', () => { - asyncCleanup().finally(() => process.exit()); - }); - process.on('SIGTERM', () => { - asyncCleanup().finally(() => process.exit()); - }); - } - } - - /** - * Checks if nftables is available and the required modules are loaded - */ - private async checkNftablesAvailability(): Promise { - const available = await this.executor.checkAvailability(); - - if (available && this.settings.useAdvancedNAT) { - await this.executor.checkConntrackModules(); - } - - return available; - } - - /** - * Creates the necessary tables and chains - */ - private async setupTablesAndChains(isIpv6: boolean = false): Promise { - const family = isIpv6 ? 'ip6' : 'ip'; - - try { - // Check if the table already exists - const stdout = await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} list tables ${family}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - const tableExists = stdout.includes(`table ${family} ${this.tableName}`); - - if (!tableExists) { - // Create the table - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} add table ${family} ${this.tableName}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', `Created table ${family} ${this.tableName}`); - - // Create the nat chain for the prerouting hook - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} add chain ${family} ${this.tableName} nat_prerouting { type nat hook prerouting priority -100 ; }`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', `Created nat_prerouting chain in ${family} ${this.tableName}`); - - // Create the nat chain for the postrouting hook if not preserving source IP - if (!this.settings.preserveSourceIP) { - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} add chain ${family} ${this.tableName} nat_postrouting { type nat hook postrouting priority 100 ; }`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', `Created nat_postrouting chain in ${family} ${this.tableName}`); - } - - // Create the chain for NetworkProxy integration if needed - if (this.settings.netProxyIntegration?.enabled && this.settings.netProxyIntegration.redirectLocalhost) { - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} add chain ${family} ${this.tableName} nat_output { type nat hook output priority 0 ; }`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', `Created nat_output chain in ${family} ${this.tableName}`); - } - - // Create the QoS chain if needed - if (this.settings.qos?.enabled) { - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} add chain ${family} ${this.tableName} qos_forward { type filter hook forward priority 0 ; }`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', `Created QoS forward chain in ${family} ${this.tableName}`); - } - } else { - this.log('info', `Table ${family} ${this.tableName} already exists, using existing table`); - } - - return true; - } catch (err) { - this.log('error', `Failed to set up tables and chains: ${err.message}`); - return false; - } - } - - /** - * Creates IP sets for efficient filtering of large IP lists - */ - private async createIPSet( - family: string, - setName: string, - ips: string[], - setType: 'ipv4_addr' | 'ipv6_addr' = 'ipv4_addr' - ): Promise { - try { - // Filter IPs based on family - const filteredIPs = filterIPsByFamily(ips, family as 'ip' | 'ip6'); - - if (filteredIPs.length === 0) { - this.log('info', `No IP addresses of type ${setType} to add to set ${setName}`); - return true; - } - - // Check if set already exists - try { - const sets = await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} list sets ${family} ${this.tableName}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - if (sets.includes(`set ${setName} {`)) { - this.log('info', `IP set ${setName} already exists, will add elements`); - } else { - // Create the set - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} add set ${family} ${this.tableName} ${setName} { type ${setType}; }`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', `Created IP set ${setName} for ${family} with type ${setType}`); - } - } catch (err) { - // Set might not exist yet, create it - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} add set ${family} ${this.tableName} ${setName} { type ${setType}; }`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', `Created IP set ${setName} for ${family} with type ${setType}`); - } - - // Add IPs to the set in batches to avoid command line length limitations - const batchSize = 100; - for (let i = 0; i < filteredIPs.length; i += batchSize) { - const batch = filteredIPs.slice(i, i + batchSize); - const elements = batch.join(', '); - - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} add element ${family} ${this.tableName} ${setName} { ${elements} }`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', `Added batch of ${batch.length} IPs to set ${setName}`); - } - - // Track the IP set - this.ipSets.set(`${family}:${setName}`, filteredIPs); - - return true; - } catch (err) { - this.log('error', `Failed to create IP set ${setName}: ${err.message}`); - return false; - } - } - - /** - * Adds source IP filtering rules, potentially using IP sets for efficiency - */ - private async addSourceIPFilters(isIpv6: boolean = false): Promise { - if (!this.settings.ipAllowList && !this.settings.ipBlockList) { - return true; // Nothing to do - } - - const family = isIpv6 ? 'ip6' : 'ip'; - const chain = 'nat_prerouting'; - const setType = isIpv6 ? 'ipv6_addr' : 'ipv4_addr'; - - try { - // Start building the ruleset file content - let rulesetContent = ''; - - // 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.ipBlockList && this.settings.ipBlockList.length > 0) { - const setName = 'banned_ips'; - 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"`; - rulesetContent += `${rule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: chain, - ruleContents: rule, - added: false - }); - } - - if (this.settings.ipAllowList && this.settings.ipAllowList.length > 0) { - const setName = 'allowed_ips'; - 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"`; - rulesetContent += `${rule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: chain, - ruleContents: rule, - added: false - }); - - // Add default deny rule for unlisted IPs - const denyRule = `add rule ${family} ${this.tableName} ${chain} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} drop comment "${this.ruleTag}:DENY_ALL"`; - rulesetContent += `${denyRule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: chain, - ruleContents: denyRule, - added: false - }); - } - } else { - // Traditional approach without IP sets - less efficient for large IP lists - - // Ban specific IPs first - 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; - - const rule = `add rule ${family} ${this.tableName} ${chain} ip${isIpv6 ? '6' : ''} saddr ${ip} drop comment "${this.ruleTag}:BANNED"`; - rulesetContent += `${rule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: chain, - ruleContents: rule, - added: false - }); - } - } - - // Allow specific IPs - if (this.settings.ipAllowList && this.settings.ipAllowList.length > 0) { - // Add rules to allow specific IPs - 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; - - const rule = `add rule ${family} ${this.tableName} ${chain} ip${isIpv6 ? '6' : ''} saddr ${ip} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} accept comment "${this.ruleTag}:ALLOWED"`; - rulesetContent += `${rule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: chain, - ruleContents: rule, - added: false - }); - } - - // Add default deny rule for unlisted IPs - const denyRule = `add rule ${family} ${this.tableName} ${chain} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} drop comment "${this.ruleTag}:DENY_ALL"`; - rulesetContent += `${denyRule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: chain, - ruleContents: denyRule, - added: false - }); - } - } - - // Only write and apply if we have rules to add - if (rulesetContent) { - // Apply the ruleset using the helper - await this.executor.executeWithTempFile(rulesetContent); - - this.log('info', `Added source IP filter rules for ${family}`); - - // Mark rules as added - for (const rule of this.rules) { - if (rule.tableFamily === family && !rule.added) { - rule.added = true; - - // Verify the rule was applied - await this.verifyRuleApplication(rule); - } - } - } - - return true; - } catch (err) { - this.log('error', `Failed to add source IP filter rules: ${err.message}`); - - // Try to clean up any rules that might have been added - this.rollbackRules(); - - return false; - } - } - - /** - * Gets a comma-separated list of all ports from a port specification - */ - private getAllPorts(portSpec: number | PortRange | Array): string { - const portRanges = normalizePortSpec(portSpec); - const ports: string[] = []; - - for (const range of portRanges) { - if (range.from === range.to) { - ports.push(range.from.toString()); - } else { - ports.push(`${range.from}-${range.to}`); - } - } - - return ports.join(', '); - } - - /** - * Configures advanced NAT with connection tracking - */ - private async setupAdvancedNAT(isIpv6: boolean = false): Promise { - if (!this.settings.useAdvancedNAT) { - return true; // Skip if not using advanced NAT - } - - const family = isIpv6 ? 'ip6' : 'ip'; - const preroutingChain = 'nat_prerouting'; - - try { - // Get the port ranges - const fromPortRanges = normalizePortSpec(this.settings.fromPort); - const toPortRanges = normalizePortSpec(this.settings.toPort); - - let rulesetContent = ''; - - // Simple case - one-to-one mapping with connection tracking - if (fromPortRanges.length === 1 && toPortRanges.length === 1) { - const fromRange = fromPortRanges[0]; - const toRange = toPortRanges[0]; - - // Single port to single port with connection tracking - if (fromRange.from === fromRange.to && toRange.from === toRange.to) { - const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${fromRange.from} ct state new dnat to ${this.settings.toHost}:${toRange.from} comment "${this.ruleTag}:DNAT_CT"`; - rulesetContent += `${rule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: preroutingChain, - ruleContents: rule, - added: false - }); - } - // Port range with same size - else if ((fromRange.to - fromRange.from) === (toRange.to - toRange.from)) { - const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${fromRange.from}-${fromRange.to} ct state new dnat to ${this.settings.toHost}:${toRange.from}-${toRange.to} comment "${this.ruleTag}:DNAT_RANGE_CT"`; - rulesetContent += `${rule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: preroutingChain, - ruleContents: rule, - added: false - }); - } - // Add related and established connection rule for efficient connection handling - const ctRule = `add rule ${family} ${this.tableName} ${preroutingChain} ct state established,related accept comment "${this.ruleTag}:CT_ESTABLISHED"`; - rulesetContent += `${ctRule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: preroutingChain, - ruleContents: ctRule, - added: false - }); - - // Apply the rules if we have any - if (rulesetContent) { - await this.executor.executeWithTempFile(rulesetContent); - - this.log('info', `Added advanced NAT rules for ${family}`); - - // Mark rules as added - for (const rule of this.rules) { - if (rule.tableFamily === family && !rule.added) { - rule.added = true; - - // Verify the rule was applied - await this.verifyRuleApplication(rule); - } - } - } - } - - return true; - } catch (err) { - this.log('error', `Failed to set up advanced NAT: ${err.message}`); - return false; - } - } - - /** - * Adds port forwarding rules - */ - private async addPortForwardingRules(isIpv6: boolean = false): Promise { - // Skip if using advanced NAT as that already handles the port forwarding - if (this.settings.useAdvancedNAT) { - return true; - } - - const family = isIpv6 ? 'ip6' : 'ip'; - const preroutingChain = 'nat_prerouting'; - const postroutingChain = 'nat_postrouting'; - - try { - // Normalize port specifications - const fromPortRanges = normalizePortSpec(this.settings.fromPort); - const toPortRanges = normalizePortSpec(this.settings.toPort); - - // Handle the case where fromPort and toPort counts don't match - if (fromPortRanges.length !== toPortRanges.length) { - if (toPortRanges.length === 1) { - // If there's only one toPort, use it for all fromPorts - const singleToRange = toPortRanges[0]; - - return await this.addPortMappings(family, preroutingChain, postroutingChain, fromPortRanges, singleToRange); - } else { - throw new NftValidationError('Mismatched port counts: fromPort and toPort arrays must have equal length or toPort must be a single value'); - } - } else { - // Add port mapping rules for each port pair - return await this.addPortPairMappings(family, preroutingChain, postroutingChain, fromPortRanges, toPortRanges); - } - } catch (err) { - this.log('error', `Failed to add port forwarding rules: ${err.message}`); - return false; - } - } - - /** - * Adds port forwarding rules for the case where one toPortRange maps to multiple fromPortRanges - */ - private async addPortMappings( - family: string, - preroutingChain: string, - postroutingChain: string, - fromPortRanges: PortRange[], - toPortRange: PortRange - ): Promise { - try { - let rulesetContent = ''; - - // For each from port range, create a mapping to the single to port range - for (const fromRange of fromPortRanges) { - // Simple case: single port to single port - if (fromRange.from === fromRange.to && toPortRange.from === toPortRange.to) { - const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${fromRange.from} dnat to ${this.settings.toHost}:${toPortRange.from} comment "${this.ruleTag}:DNAT"`; - rulesetContent += `${rule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: preroutingChain, - ruleContents: rule, - added: false - }); - } - // Multiple ports in from range, but only one port in to range - else if (toPortRange.from === toPortRange.to) { - // Map each port in from range to the single to port - for (let p = fromRange.from; p <= fromRange.to; p++) { - const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${p} dnat to ${this.settings.toHost}:${toPortRange.from} comment "${this.ruleTag}:DNAT"`; - rulesetContent += `${rule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: preroutingChain, - ruleContents: rule, - added: false - }); - } - } - // Port range to port range mapping with modulo distribution - else { - const toRangeSize = toPortRange.to - toPortRange.from + 1; - - for (let p = fromRange.from; p <= fromRange.to; p++) { - const offset = (p - fromRange.from) % toRangeSize; - const targetPort = toPortRange.from + offset; - - const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${p} dnat to ${this.settings.toHost}:${targetPort} comment "${this.ruleTag}:DNAT"`; - rulesetContent += `${rule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: preroutingChain, - ruleContents: rule, - added: false - }); - } - } - } - - // Add masquerade rule for source NAT if not preserving source IP - if (!this.settings.preserveSourceIP) { - const ports = this.getAllPorts(this.settings.toPort); - const masqRule = `add rule ${family} ${this.tableName} ${postroutingChain} ${this.settings.protocol} daddr ${this.settings.toHost} dport {${ports}} masquerade comment "${this.ruleTag}:MASQ"`; - rulesetContent += `${masqRule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: postroutingChain, - ruleContents: masqRule, - added: false - }); - } - - // Apply the ruleset if we have any rules - if (rulesetContent) { - // Apply the ruleset using the helper - await this.executor.executeWithTempFile(rulesetContent); - - this.log('info', `Added port forwarding rules for ${family}`); - - // Mark rules as added - for (const rule of this.rules) { - if (rule.tableFamily === family && !rule.added) { - rule.added = true; - - // Verify the rule was applied - await this.verifyRuleApplication(rule); - } - } - } - - return true; - } catch (err) { - this.log('error', `Failed to add port mappings: ${err.message}`); - return false; - } - } - - /** - * Adds port forwarding rules for pairs of fromPortRanges and toPortRanges - */ - private async addPortPairMappings( - family: string, - preroutingChain: string, - postroutingChain: string, - fromPortRanges: PortRange[], - toPortRanges: PortRange[] - ): Promise { - try { - let rulesetContent = ''; - - // Process each fromPort and toPort pair - for (let i = 0; i < fromPortRanges.length; i++) { - const fromRange = fromPortRanges[i]; - const toRange = toPortRanges[i]; - - // Simple case: single port to single port - if (fromRange.from === fromRange.to && toRange.from === toRange.to) { - const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${fromRange.from} dnat to ${this.settings.toHost}:${toRange.from} comment "${this.ruleTag}:DNAT"`; - rulesetContent += `${rule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: preroutingChain, - ruleContents: rule, - added: false - }); - } - // Port range with equal size - can use direct mapping - else if ((fromRange.to - fromRange.from) === (toRange.to - toRange.from)) { - const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${fromRange.from}-${fromRange.to} dnat to ${this.settings.toHost}:${toRange.from}-${toRange.to} comment "${this.ruleTag}:DNAT_RANGE"`; - rulesetContent += `${rule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: preroutingChain, - ruleContents: rule, - added: false - }); - } - // Unequal port ranges - need to map individually - else { - const toRangeSize = toRange.to - toRange.from + 1; - - for (let p = fromRange.from; p <= fromRange.to; p++) { - const offset = (p - fromRange.from) % toRangeSize; - const targetPort = toRange.from + offset; - - const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${p} dnat to ${this.settings.toHost}:${targetPort} comment "${this.ruleTag}:DNAT_INDIVIDUAL"`; - rulesetContent += `${rule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: preroutingChain, - ruleContents: rule, - added: false - }); - } - } - - // Add masquerade rule for this port range if not preserving source IP - if (!this.settings.preserveSourceIP) { - const masqRule = `add rule ${family} ${this.tableName} ${postroutingChain} ${this.settings.protocol} daddr ${this.settings.toHost} dport ${toRange.from}-${toRange.to} masquerade comment "${this.ruleTag}:MASQ"`; - rulesetContent += `${masqRule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: postroutingChain, - ruleContents: masqRule, - added: false - }); - } - } - - // Apply the ruleset if we have any rules - if (rulesetContent) { - await this.executor.executeWithTempFile(rulesetContent); - - this.log('info', `Added port forwarding rules for ${family}`); - - // Mark rules as added - for (const rule of this.rules) { - if (rule.tableFamily === family && !rule.added) { - rule.added = true; - - // Verify the rule was applied - await this.verifyRuleApplication(rule); - } - } - } - - return true; - } catch (err) { - this.log('error', `Failed to add port pair mappings: ${err.message}`); - return false; - } - } - - /** - * Setup quality of service rules - */ - private async addTrafficShaping(isIpv6: boolean = false): Promise { - if (!this.settings.qos?.enabled) { - return true; - } - - const family = isIpv6 ? 'ip6' : 'ip'; - const qosChain = 'qos_forward'; - - try { - let rulesetContent = ''; - - // Add rate limiting rule if specified - if (this.settings.qos.maxRate) { - const ruleContent = `add rule ${family} ${this.tableName} ${qosChain} ip daddr ${this.settings.toHost} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.toPort)}} limit rate over ${this.settings.qos.maxRate} drop comment "${this.ruleTag}:QOS_RATE"`; - rulesetContent += `${ruleContent}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: qosChain, - ruleContents: ruleContent, - added: false - }); - } - - // Add priority marking if specified - if (this.settings.qos.priority !== undefined) { - // Check if the chain exists - const chainsOutput = await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} list chains ${family} ${this.tableName}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - // Check if we need to create priority queues - const hasPrioChain = chainsOutput.includes(`chain prio${this.settings.qos.priority}`); - - if (!hasPrioChain) { - // Create priority chain - const prioChainRule = `add chain ${family} ${this.tableName} prio${this.settings.qos.priority} { type filter hook forward priority ${this.settings.qos.priority * 10}; }`; - rulesetContent += `${prioChainRule}\n`; - } - - // Add the rules to mark packets with this priority - for (const range of normalizePortSpec(this.settings.toPort)) { - const markRule = `add rule ${family} ${this.tableName} ${qosChain} ${this.settings.protocol} dport ${range.from}-${range.to} counter goto prio${this.settings.qos.priority} comment "${this.ruleTag}:QOS_PRIORITY"`; - rulesetContent += `${markRule}\n`; - - this.rules.push({ - tableFamily: family, - tableName: this.tableName, - chainName: qosChain, - ruleContents: markRule, - added: false - }); - } - } - - // Apply the ruleset if we have any rules - if (rulesetContent) { - // Apply the ruleset using the helper - await this.executor.executeWithTempFile(rulesetContent); - - this.log('info', `Added QoS rules for ${family}`); - - // Mark rules as added - for (const rule of this.rules) { - if (rule.tableFamily === family && !rule.added) { - rule.added = true; - - // Verify the rule was applied - await this.verifyRuleApplication(rule); - } - } - } - - return true; - } catch (err) { - this.log('error', `Failed to add traffic shaping: ${err.message}`); - return false; - } - } - - /** - * Setup NetworkProxy integration rules - */ - private async setupNetworkProxyIntegration(isIpv6: boolean = false): Promise { - if (!this.settings.netProxyIntegration?.enabled) { - return true; - } - - const netProxyConfig = this.settings.netProxyIntegration; - const family = isIpv6 ? 'ip6' : 'ip'; - const outputChain = 'nat_output'; - - try { - // Only proceed if we're redirecting localhost and have a port - if (netProxyConfig.redirectLocalhost && netProxyConfig.sslTerminationPort) { - const localhost = isIpv6 ? '::1' : '127.0.0.1'; - - // Create the redirect rule - const rule = `add rule ${family} ${this.tableName} ${outputChain} ${this.settings.protocol} daddr ${localhost} redirect to :${netProxyConfig.sslTerminationPort} comment "${this.ruleTag}:NETPROXY_REDIRECT"`; - - // Apply the rule - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} ${rule}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', `Added NetworkProxy redirection rule for ${family}`); - - const newRule = { - tableFamily: family, - tableName: this.tableName, - chainName: outputChain, - ruleContents: rule, - added: true - }; - - this.rules.push(newRule); - - // Verify the rule was actually applied - await this.verifyRuleApplication(newRule); - } - - return true; - } catch (err) { - this.log('error', `Failed to set up NetworkProxy integration: ${err.message}`); - return false; - } - } - - /** - * Verify that a rule was successfully applied - */ - private async verifyRuleApplication(rule: NfTablesRule): Promise { - try { - const { tableFamily, tableName, chainName, ruleContents } = rule; - - // Extract the distinctive parts of the rule to create a search pattern - const commentMatch = ruleContents.match(/comment "([^"]+)"/); - if (!commentMatch) return false; - - const commentTag = commentMatch[1]; - - // List the chain to check if our rule is there - const stdout = await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} list chain ${tableFamily} ${tableName} ${chainName}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - // Check if the comment appears in the output - const isApplied = stdout.includes(commentTag); - - rule.verified = isApplied; - - if (!isApplied) { - this.log('warn', `Rule verification failed: ${commentTag} not found in chain ${chainName}`); - } else { - this.log('debug', `Rule verified: ${commentTag} found in chain ${chainName}`); - } - - return isApplied; - } catch (err) { - this.log('error', `Failed to verify rule application: ${err.message}`); - return false; - } - } - - /** - * Rolls back rules in case of error during setup - */ - private async rollbackRules(): Promise { - // Process rules in reverse order (LIFO) - for (let i = this.rules.length - 1; i >= 0; i--) { - const rule = this.rules[i]; - - if (rule.added) { - try { - // For nftables, create a delete rule by replacing 'add' with 'delete' - const deleteRule = rule.ruleContents.replace('add rule', 'delete rule'); - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} ${deleteRule}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', `Rolled back rule: ${deleteRule}`); - - rule.added = false; - rule.verified = false; - } catch (err) { - this.log('error', `Failed to roll back rule: ${err.message}`); - } - } - } - } - - /** - * Checks if nftables table exists - */ - private async tableExists(family: string, tableName: string): Promise { - try { - const stdout = await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} list tables ${family}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - return stdout.includes(`table ${family} ${tableName}`); - } catch (err) { - return false; - } - } - - /** - * Get system metrics like connection counts - */ - private async getSystemMetrics(): Promise<{ - activeConnections?: number; - forwardedConnections?: number; - bytesForwarded?: { sent: number; received: number }; - }> { - const metrics: { - activeConnections?: number; - forwardedConnections?: number; - bytesForwarded?: { sent: number; received: number }; - } = {}; - - try { - // Try to get connection metrics if conntrack is available - try { - const stdout = await this.executor.executeWithRetry('conntrack -C', this.settings.maxRetries, this.settings.retryDelayMs); - metrics.activeConnections = parseInt(stdout.trim(), 10); - } catch (err) { - // conntrack not available, skip this metric - } - - // Try to get forwarded connections count from nftables counters - try { - // Look for counters in our rules - const stdout = await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} list table ip ${this.tableName}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - // Parse counter information from the output - const counterMatches = stdout.matchAll(/counter packets (\d+) bytes (\d+)/g); - let totalPackets = 0; - let totalBytes = 0; - - for (const match of counterMatches) { - totalPackets += parseInt(match[1], 10); - totalBytes += parseInt(match[2], 10); - } - - if (totalPackets > 0) { - metrics.forwardedConnections = totalPackets; - metrics.bytesForwarded = { - sent: totalBytes, - received: 0 // We can't easily determine this without additional rules - }; - } - } catch (err) { - // Failed to get counter info, skip this metric - } - - return metrics; - } catch (err) { - this.log('error', `Failed to get system metrics: ${err.message}`); - return metrics; - } - } - - /** - * Get status of IP sets - */ - private async getIPSetStatus(): Promise<{ - name: string; - elementCount: number; - type: string; - }[]> { - const result: { - name: string; - elementCount: number; - type: string; - }[] = []; - - try { - for (const family of ['ip', 'ip6']) { - try { - const stdout = await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} list sets ${family} ${this.tableName}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - const setMatches = stdout.matchAll(/set (\w+) {\s*type (\w+)/g); - - for (const match of setMatches) { - const setName = match[1]; - const setType = match[2]; - - // Get element count from tracking map - const setKey = `${family}:${setName}`; - const elements = this.ipSets.get(setKey) || []; - - result.push({ - name: setName, - elementCount: elements.length, - type: setType - }); - } - } catch (err) { - // No sets for this family, or table doesn't exist - } - } - - return result; - } catch (err) { - this.log('error', `Failed to get IP set status: ${err.message}`); - return result; - } - } - - /** - * Get detailed status about the current state of the proxy - */ - public async getStatus(): Promise { - const result: NfTablesStatus = { - active: this.rules.some(r => r.added), - ruleCount: { - total: this.rules.length, - added: this.rules.filter(r => r.added).length, - verified: this.rules.filter(r => r.verified).length - }, - tablesConfigured: [], - metrics: {}, - qosEnabled: this.settings.qos?.enabled || false - }; - - try { - // Get list of configured tables - const stdout = await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} list tables`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - const tableRegex = /table (ip|ip6) (\w+)/g; - let match; - - while ((match = tableRegex.exec(stdout)) !== null) { - const [, family, name] = match; - if (name === this.tableName) { - result.tablesConfigured.push({ family, tableName: name }); - } - } - - // Get system metrics - result.metrics = await this.getSystemMetrics(); - - // Get IP set status if using IP sets - if (this.settings.useIPSets) { - result.ipSetsConfigured = await this.getIPSetStatus(); - } - - return result; - } catch (err) { - this.log('error', `Failed to get status: ${err.message}`); - return result; - } - } - - /** - * Performs a dry run to see what commands would be executed without actually applying them - */ - public async dryRun(): Promise { - const commands: string[] = []; - - // Simulate all the necessary setup steps and collect commands - - // Tables and chains - commands.push(`add table ip ${this.tableName}`); - commands.push(`add chain ip ${this.tableName} nat_prerouting { type nat hook prerouting priority -100; }`); - - if (!this.settings.preserveSourceIP) { - commands.push(`add chain ip ${this.tableName} nat_postrouting { type nat hook postrouting priority 100; }`); - } - - if (this.settings.netProxyIntegration?.enabled && this.settings.netProxyIntegration.redirectLocalhost) { - commands.push(`add chain ip ${this.tableName} nat_output { type nat hook output priority 0; }`); - } - - if (this.settings.qos?.enabled) { - commands.push(`add chain ip ${this.tableName} qos_forward { type filter hook forward priority 0; }`); - } - - // Add IPv6 tables if enabled - if (this.settings.ipv6Support) { - commands.push(`add table ip6 ${this.tableName}`); - commands.push(`add chain ip6 ${this.tableName} nat_prerouting { type nat hook prerouting priority -100; }`); - - if (!this.settings.preserveSourceIP) { - commands.push(`add chain ip6 ${this.tableName} nat_postrouting { type nat hook postrouting priority 100; }`); - } - - if (this.settings.netProxyIntegration?.enabled && this.settings.netProxyIntegration.redirectLocalhost) { - commands.push(`add chain ip6 ${this.tableName} nat_output { type nat hook output priority 0; }`); - } - - if (this.settings.qos?.enabled) { - commands.push(`add chain ip6 ${this.tableName} qos_forward { type filter hook forward priority 0; }`); - } - } - - // Source IP filters - if (this.settings.useIPSets) { - 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.ipBlockList.join(', ')} }`); - commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr @banned_ips drop comment "${this.ruleTag}:BANNED_SET"`); - } - - 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.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.ipBlockList?.length || this.settings.ipAllowList?.length) { - // Traditional approach without IP sets - 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.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"`); - } - } - - // Port forwarding rules - if (this.settings.useAdvancedNAT) { - // Advanced NAT with connection tracking - const fromPortRanges = normalizePortSpec(this.settings.fromPort); - const toPortRanges = normalizePortSpec(this.settings.toPort); - - if (fromPortRanges.length === 1 && toPortRanges.length === 1) { - const fromRange = fromPortRanges[0]; - const toRange = toPortRanges[0]; - - if (fromRange.from === fromRange.to && toRange.from === toRange.to) { - commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport ${fromRange.from} ct state new dnat to ${this.settings.toHost}:${toRange.from} comment "${this.ruleTag}:DNAT_CT"`); - } else if ((fromRange.to - fromRange.from) === (toRange.to - toRange.from)) { - commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport ${fromRange.from}-${fromRange.to} ct state new dnat to ${this.settings.toHost}:${toRange.from}-${toRange.to} comment "${this.ruleTag}:DNAT_RANGE_CT"`); - } - - commands.push(`add rule ip ${this.tableName} nat_prerouting ct state established,related accept comment "${this.ruleTag}:CT_ESTABLISHED"`); - } - } else { - // Standard NAT rules - const fromRanges = normalizePortSpec(this.settings.fromPort); - const toRanges = normalizePortSpec(this.settings.toPort); - - if (fromRanges.length === 1 && toRanges.length === 1) { - const fromRange = fromRanges[0]; - const toRange = toRanges[0]; - - if (fromRange.from === fromRange.to && toRange.from === toRange.to) { - commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport ${fromRange.from} dnat to ${this.settings.toHost}:${toRange.from} comment "${this.ruleTag}:DNAT"`); - } else { - commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport ${fromRange.from}-${fromRange.to} dnat to ${this.settings.toHost}:${toRange.from}-${toRange.to} comment "${this.ruleTag}:DNAT_RANGE"`); - } - } else if (toRanges.length === 1) { - // One-to-many mapping - for (const fromRange of fromRanges) { - commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport ${fromRange.from}-${fromRange.to} dnat to ${this.settings.toHost}:${toRanges[0].from}-${toRanges[0].to} comment "${this.ruleTag}:DNAT_RANGE"`); - } - } else { - // One-to-one mapping of multiple ranges - for (let i = 0; i < fromRanges.length; i++) { - commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport ${fromRanges[i].from}-${fromRanges[i].to} dnat to ${this.settings.toHost}:${toRanges[i].from}-${toRanges[i].to} comment "${this.ruleTag}:DNAT_RANGE"`); - } - } - } - - // Masquerade rules if not preserving source IP - if (!this.settings.preserveSourceIP) { - commands.push(`add rule ip ${this.tableName} nat_postrouting ${this.settings.protocol} daddr ${this.settings.toHost} dport {${this.getAllPorts(this.settings.toPort)}} masquerade comment "${this.ruleTag}:MASQ"`); - } - - // NetworkProxy integration - if (this.settings.netProxyIntegration?.enabled && - this.settings.netProxyIntegration.redirectLocalhost && - this.settings.netProxyIntegration.sslTerminationPort) { - - commands.push(`add rule ip ${this.tableName} nat_output ${this.settings.protocol} daddr 127.0.0.1 redirect to :${this.settings.netProxyIntegration.sslTerminationPort} comment "${this.ruleTag}:NETPROXY_REDIRECT"`); - } - - // QoS rules - if (this.settings.qos?.enabled) { - if (this.settings.qos.maxRate) { - commands.push(`add rule ip ${this.tableName} qos_forward ip daddr ${this.settings.toHost} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.toPort)}} limit rate over ${this.settings.qos.maxRate} drop comment "${this.ruleTag}:QOS_RATE"`); - } - - if (this.settings.qos.priority !== undefined) { - commands.push(`add chain ip ${this.tableName} prio${this.settings.qos.priority} { type filter hook forward priority ${this.settings.qos.priority * 10}; }`); - - for (const range of normalizePortSpec(this.settings.toPort)) { - commands.push(`add rule ip ${this.tableName} qos_forward ${this.settings.protocol} dport ${range.from}-${range.to} counter goto prio${this.settings.qos.priority} comment "${this.ruleTag}:QOS_PRIORITY"`); - } - } - } - - return commands; - } - - /** - * Starts the proxy by setting up all nftables rules - */ - public async start(): Promise { - // Check if nftables is available - const nftablesAvailable = await this.checkNftablesAvailability(); - if (!nftablesAvailable) { - throw new NftResourceError('nftables is not available or not properly configured'); - } - - // Optionally clean slate first - if (this.settings.forceCleanSlate) { - await NfTablesProxy.cleanSlate(); - } - - // Set up tables and chains for IPv4 - const setupSuccess = await this.setupTablesAndChains(); - if (!setupSuccess) { - throw new NftExecutionError('Failed to set up nftables tables and chains'); - } - - // Set up IPv6 tables and chains if enabled - if (this.settings.ipv6Support) { - const setupIPv6Success = await this.setupTablesAndChains(true); - if (!setupIPv6Success) { - this.log('warn', 'Failed to set up IPv6 tables and chains, continuing with IPv4 only'); - } - } - - // Add source IP filters - await this.addSourceIPFilters(); - if (this.settings.ipv6Support) { - await this.addSourceIPFilters(true); - } - - // Set up advanced NAT with connection tracking if enabled - if (this.settings.useAdvancedNAT) { - const advancedNatSuccess = await this.setupAdvancedNAT(); - if (!advancedNatSuccess) { - this.log('warn', 'Failed to set up advanced NAT, falling back to standard NAT'); - this.settings.useAdvancedNAT = false; - } else if (this.settings.ipv6Support) { - await this.setupAdvancedNAT(true); - } - } - - // Add port forwarding rules (skip if using advanced NAT) - if (!this.settings.useAdvancedNAT) { - const forwardingSuccess = await this.addPortForwardingRules(); - if (!forwardingSuccess) { - throw new NftExecutionError('Failed to add port forwarding rules'); - } - - // Add IPv6 port forwarding rules if enabled - if (this.settings.ipv6Support) { - const forwardingIPv6Success = await this.addPortForwardingRules(true); - if (!forwardingIPv6Success) { - this.log('warn', 'Failed to add IPv6 port forwarding rules'); - } - } - } - - // Set up QoS if enabled - if (this.settings.qos?.enabled) { - const qosSuccess = await this.addTrafficShaping(); - if (!qosSuccess) { - this.log('warn', 'Failed to set up QoS rules, continuing without traffic shaping'); - } else if (this.settings.ipv6Support) { - await this.addTrafficShaping(true); - } - } - - // Set up NetworkProxy integration if enabled - if (this.settings.netProxyIntegration?.enabled) { - const netProxySetupSuccess = await this.setupNetworkProxyIntegration(); - if (!netProxySetupSuccess) { - this.log('warn', 'Failed to set up NetworkProxy integration'); - } - - if (this.settings.ipv6Support) { - await this.setupNetworkProxyIntegration(true); - } - } - - // Final check - ensure we have at least one rule added - if (this.rules.filter(r => r.added).length === 0) { - throw new NftExecutionError('No rules were added'); - } - - this.log('info', 'NfTablesProxy started successfully'); - } - - /** - * Stops the proxy by removing all added rules - */ - public async stop(): Promise { - try { - let rulesetContent = ''; - - // Process rules in reverse order (LIFO) - for (let i = this.rules.length - 1; i >= 0; i--) { - const rule = this.rules[i]; - - if (rule.added) { - // Create delete rules by replacing 'add' with 'delete' - const deleteRule = rule.ruleContents.replace('add rule', 'delete rule'); - rulesetContent += `${deleteRule}\n`; - } - } - - // Apply the ruleset if we have any rules to delete - if (rulesetContent) { - // Write to temporary file - await AsyncFileSystem.writeFile(this.tempFilePath, rulesetContent); - - try { - // Apply the ruleset - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', 'Removed all added rules'); - - // Mark all rules as removed - this.rules.forEach(rule => { - rule.added = false; - rule.verified = false; - }); - } finally { - // Remove temporary file - await AsyncFileSystem.remove(this.tempFilePath); - } - } - - // Clean up IP sets if we created any - if (this.settings.useIPSets && this.ipSets.size > 0) { - for (const [key, _] of this.ipSets) { - const [family, setName] = key.split(':'); - - try { - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} delete set ${family} ${this.tableName} ${setName}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', `Removed IP set ${setName} from ${family} ${this.tableName}`); - } catch (err) { - this.log('warn', `Failed to remove IP set ${setName}: ${err.message}`); - } - } - - this.ipSets.clear(); - } - - // Optionally clean up tables if they're empty - await this.cleanupEmptyTables(); - - this.log('info', 'NfTablesProxy stopped successfully'); - } catch (err) { - this.log('error', `Error stopping NfTablesProxy: ${err.message}`); - throw err; - } - } - - /** - * Synchronous version of stop, for use in exit handlers only. - * Uses single-attempt commands without retry (process is exiting anyway). - */ - public stopSync(): void { - try { - let rulesetContent = ''; - - // Process rules in reverse order (LIFO) - for (let i = this.rules.length - 1; i >= 0; i--) { - const rule = this.rules[i]; - - if (rule.added) { - // Create delete rules by replacing 'add' with 'delete' - const deleteRule = rule.ruleContents.replace('add rule', 'delete rule'); - rulesetContent += `${deleteRule}\n`; - } - } - - // Apply the ruleset if we have any rules to delete - if (rulesetContent) { - // Write to temporary file - fs.writeFileSync(this.tempFilePath, rulesetContent); - - // Apply the ruleset (single attempt, no retry - process is exiting) - this.executor.executeSync(`${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`); - - this.log('info', 'Removed all added rules'); - - // Mark all rules as removed - this.rules.forEach(rule => { - rule.added = false; - rule.verified = false; - }); - - // Remove temporary file - try { - fs.unlinkSync(this.tempFilePath); - } catch { - // Ignore - process is exiting - } - } - - // Clean up IP sets if we created any - if (this.settings.useIPSets && this.ipSets.size > 0) { - for (const [key, _] of this.ipSets) { - const [family, setName] = key.split(':'); - - try { - this.executor.executeSync( - `${NfTablesProxy.NFT_CMD} delete set ${family} ${this.tableName} ${setName}` - ); - } catch { - // Non-critical error, continue - } - } - } - - // Optionally clean up tables if they're empty (sync version) - this.cleanupEmptyTablesSync(); - - this.log('info', 'NfTablesProxy stopped successfully'); - } catch (err) { - this.log('error', `Error stopping NfTablesProxy: ${err.message}`); - } - } - - /** - * Cleans up empty tables - */ - private async cleanupEmptyTables(): Promise { - // Check if tables are empty, and if so, delete them - for (const family of ['ip', 'ip6']) { - // Skip IPv6 if not enabled - if (family === 'ip6' && !this.settings.ipv6Support) { - continue; - } - - try { - // Check if table exists - const tableExists = await this.tableExists(family, this.tableName); - if (!tableExists) { - continue; - } - - // Check if the table has any rules - const stdout = await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} list table ${family} ${this.tableName}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - const hasRules = stdout.includes('rule'); - - if (!hasRules) { - // Table is empty, delete it - await this.executor.executeWithRetry( - `${NfTablesProxy.NFT_CMD} delete table ${family} ${this.tableName}`, - this.settings.maxRetries, - this.settings.retryDelayMs - ); - - this.log('info', `Deleted empty table ${family} ${this.tableName}`); - } - } catch (err) { - this.log('error', `Error cleaning up tables: ${err.message}`); - } - } - } - - /** - * Synchronous version of cleanupEmptyTables (for exit handlers only) - */ - private cleanupEmptyTablesSync(): void { - // Check if tables are empty, and if so, delete them - for (const family of ['ip', 'ip6']) { - // Skip IPv6 if not enabled - if (family === 'ip6' && !this.settings.ipv6Support) { - continue; - } - - try { - // Check if table exists - const tableExistsOutput = this.executor.executeSync( - `${NfTablesProxy.NFT_CMD} list tables ${family}` - ); - - const tableExists = tableExistsOutput.includes(`table ${family} ${this.tableName}`); - - if (!tableExists) { - continue; - } - - // Check if the table has any rules - const stdout = this.executor.executeSync( - `${NfTablesProxy.NFT_CMD} list table ${family} ${this.tableName}` - ); - - const hasRules = stdout.includes('rule'); - - if (!hasRules) { - // Table is empty, delete it - this.executor.executeSync( - `${NfTablesProxy.NFT_CMD} delete table ${family} ${this.tableName}` - ); - - this.log('info', `Deleted empty table ${family} ${this.tableName}`); - } - } catch (err) { - this.log('error', `Error cleaning up tables: ${err.message}`); - } - } - } - - /** - * Removes all nftables rules created by this module - */ - public static async cleanSlate(): Promise { - try { - // Check for rules with our comment pattern - const stdout = await execAsync(`${NfTablesProxy.NFT_CMD} list ruleset`); - - // Extract our tables - const tableMatches = stdout.stdout.match(/table (ip|ip6) (\w+) {[^}]*NfTablesProxy:[^}]*}/g); - - if (tableMatches) { - for (const tableMatch of tableMatches) { - // Extract table family and name - const familyMatch = tableMatch.match(/table (ip|ip6) (\w+)/); - if (familyMatch) { - const family = familyMatch[1]; - const tableName = familyMatch[2]; - - // Delete the table - await execAsync(`${NfTablesProxy.NFT_CMD} delete table ${family} ${tableName}`); - console.log(`Deleted table ${family} ${tableName} containing NfTablesProxy rules`); - } - } - } else { - console.log('No NfTablesProxy rules found to clean up'); - } - } catch (err) { - console.error(`Error in cleanSlate: ${err}`); - } - } - - /** - * Synchronous version of cleanSlate - * @deprecated This method blocks the event loop and should be avoided. Use cleanSlate() instead. - * WARNING: This method uses execSync which blocks the entire Node.js event loop! - */ - public static cleanSlateSync(): void { - console.warn('[DEPRECATION WARNING] cleanSlateSync blocks the event loop and should not be used. Consider using the async cleanSlate() method instead.'); - - try { - // Check for rules with our comment pattern - const stdout = execSync(`${NfTablesProxy.NFT_CMD} list ruleset`).toString(); - - // Extract our tables - const tableMatches = stdout.match(/table (ip|ip6) (\w+) {[^}]*NfTablesProxy:[^}]*}/g); - - if (tableMatches) { - for (const tableMatch of tableMatches) { - // Extract table family and name - const familyMatch = tableMatch.match(/table (ip|ip6) (\w+)/); - if (familyMatch) { - const family = familyMatch[1]; - const tableName = familyMatch[2]; - - // Delete the table - execSync(`${NfTablesProxy.NFT_CMD} delete table ${family} ${tableName}`); - console.log(`Deleted table ${family} ${tableName} containing NfTablesProxy rules`); - } - } - } else { - console.log('No NfTablesProxy rules found to clean up'); - } - } catch (err) { - console.error(`Error in cleanSlateSync: ${err}`); - } - } - - /** - * Improved logging with structured output - */ - private log(level: 'info' | 'warn' | 'error' | 'debug', message: string, meta?: Record): void { - if (!this.settings.enableLogging && (level === 'info' || level === 'debug')) { - return; - } - - const timestamp = new Date().toISOString(); - - const logData = { - timestamp, - level: level.toUpperCase(), - message, - ...meta, - context: { - instance: this.ruleTag, - table: this.tableName - } - }; - - // Determine if output should be JSON or plain text based on settings - const useJson = this.settings.logFormat === 'json'; - - if (useJson) { - const logOutput = JSON.stringify(logData); - console.log(logOutput); - return; - } - - // Plain text format - const metaStr = meta ? ` ${JSON.stringify(meta)}` : ''; - - switch (level) { - case 'info': - console.log(`[${timestamp}] [INFO] ${message}${metaStr}`); - break; - case 'warn': - console.warn(`[${timestamp}] [WARN] ${message}${metaStr}`); - break; - case 'error': - console.error(`[${timestamp}] [ERROR] ${message}${metaStr}`); - break; - case 'debug': - console.log(`[${timestamp}] [DEBUG] ${message}${metaStr}`); - break; - } - } -} \ No newline at end of file diff --git a/ts/proxies/nftables-proxy/utils/index.ts b/ts/proxies/nftables-proxy/utils/index.ts deleted file mode 100644 index 86f8d62..0000000 --- a/ts/proxies/nftables-proxy/utils/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * NFTables Proxy Utilities - * - * This module exports utility functions and classes for NFTables operations. - */ - -// Command execution -export { NftCommandExecutor } from './nft-command-executor.js'; -export type { INftLoggerFn, INftExecutorOptions } from './nft-command-executor.js'; - -// Port specification normalization -export { - normalizePortSpec, - validatePorts, - formatPortRange, - portSpecToNftExpr, - rangesOverlap, - mergeOverlappingRanges, - countPorts, - isPortInSpec -} from './nft-port-spec-normalizer.js'; - -// Rule validation -export { - isValidIP, - isValidIPv4, - isValidIPv6, - isValidHostname, - isValidTableName, - isValidRate, - validateIPs, - validateHost, - validateTableName, - validateQosSettings, - validateSettings, - isIPForFamily, - filterIPsByFamily -} from './nft-rule-validator.js'; diff --git a/ts/proxies/nftables-proxy/utils/nft-command-executor.ts b/ts/proxies/nftables-proxy/utils/nft-command-executor.ts deleted file mode 100644 index caf8a03..0000000 --- a/ts/proxies/nftables-proxy/utils/nft-command-executor.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * NFTables Command Executor - * - * Handles command execution with retry logic, temp file management, - * and error handling for nftables operations. - */ - -import { exec, execSync } from 'child_process'; -import { promisify } from 'util'; -import { delay } from '../../../core/utils/async-utils.js'; -import { AsyncFileSystem } from '../../../core/utils/fs-utils.js'; -import { NftExecutionError } from '../models/index.js'; - -const execAsync = promisify(exec); - -export interface INftLoggerFn { - (level: 'info' | 'warn' | 'error' | 'debug', message: string, data?: Record): void; -} - -export interface INftExecutorOptions { - maxRetries?: number; - retryDelayMs?: number; - tempFilePath?: string; -} - -/** - * NFTables command executor with retry logic and temp file support - */ -export class NftCommandExecutor { - private static readonly NFT_CMD = 'nft'; - private maxRetries: number; - private retryDelayMs: number; - private tempFilePath: string; - - constructor( - private log: INftLoggerFn, - options: INftExecutorOptions = {} - ) { - this.maxRetries = options.maxRetries || 3; - this.retryDelayMs = options.retryDelayMs || 1000; - this.tempFilePath = options.tempFilePath || `/tmp/nft-rules-${Date.now()}.nft`; - } - - /** - * Execute a command with retry capability - */ - async executeWithRetry(command: string, maxRetries?: number, retryDelayMs?: number): Promise { - const retries = maxRetries ?? this.maxRetries; - const delayMs = retryDelayMs ?? this.retryDelayMs; - let lastError: Error | undefined; - - for (let i = 0; i < retries; i++) { - try { - const { stdout } = await execAsync(command); - return stdout; - } catch (err) { - lastError = err as Error; - this.log('warn', `Command failed (attempt ${i+1}/${retries}): ${command}`, { error: lastError.message }); - - // Wait before retry, unless it's the last attempt - if (i < retries - 1) { - await delay(delayMs); - } - } - } - - throw new NftExecutionError(`Failed after ${retries} attempts: ${lastError?.message || 'Unknown error'}`); - } - - /** - * Execute system command synchronously (single attempt, no retry) - * Used only for exit handlers where the process is terminating anyway. - */ - executeSync(command: string): string { - try { - return execSync(command, { timeout: 5000 }).toString(); - } catch (err) { - this.log('warn', `Sync command failed: ${command}`, { error: (err as Error).message }); - throw err; - } - } - - /** - * Execute nftables commands with a temporary file - */ - async executeWithTempFile(rulesetContent: string): Promise { - await AsyncFileSystem.writeFile(this.tempFilePath, rulesetContent); - - try { - await this.executeWithRetry( - `${NftCommandExecutor.NFT_CMD} -f ${this.tempFilePath}`, - this.maxRetries, - this.retryDelayMs - ); - } finally { - // Always clean up the temp file - await AsyncFileSystem.remove(this.tempFilePath); - } - } - - /** - * Check if nftables is available - */ - async checkAvailability(): Promise { - try { - await this.executeWithRetry(`${NftCommandExecutor.NFT_CMD} --version`, this.maxRetries, this.retryDelayMs); - return true; - } catch (err) { - this.log('error', `nftables is not available: ${(err as Error).message}`); - return false; - } - } - - /** - * Check if connection tracking modules are loaded - */ - async checkConntrackModules(): Promise { - try { - await this.executeWithRetry('lsmod | grep nf_conntrack', this.maxRetries, this.retryDelayMs); - return true; - } catch (err) { - this.log('warn', 'Connection tracking modules might not be loaded, advanced NAT features may not work'); - return false; - } - } - - /** - * Run an nft command directly - */ - async nft(args: string): Promise { - return this.executeWithRetry(`${NftCommandExecutor.NFT_CMD} ${args}`, this.maxRetries, this.retryDelayMs); - } - - /** - * Run an nft command synchronously (for cleanup on exit) - */ - nftSync(args: string): string { - return this.executeSync(`${NftCommandExecutor.NFT_CMD} ${args}`); - } - - /** - * Get the NFT command path - */ - static get nftCmd(): string { - return NftCommandExecutor.NFT_CMD; - } - - /** - * Update the temp file path - */ - setTempFilePath(path: string): void { - this.tempFilePath = path; - } - - /** - * Update retry settings - */ - setRetryOptions(maxRetries: number, retryDelayMs: number): void { - this.maxRetries = maxRetries; - this.retryDelayMs = retryDelayMs; - } -} diff --git a/ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.ts b/ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.ts deleted file mode 100644 index f52aa83..0000000 --- a/ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * NFTables Port Specification Normalizer - * - * Handles normalization and validation of port specifications - * for nftables rules. - */ - -import type { PortRange } from '../models/index.js'; -import { NftValidationError } from '../models/index.js'; - -/** - * Normalizes port specifications into an array of port ranges - */ -export function normalizePortSpec(portSpec: number | PortRange | Array): PortRange[] { - const result: PortRange[] = []; - - if (Array.isArray(portSpec)) { - // If it's an array, process each element - for (const spec of portSpec) { - result.push(...normalizePortSpec(spec)); - } - } else if (typeof portSpec === 'number') { - // Single port becomes a range with the same start and end - result.push({ from: portSpec, to: portSpec }); - } else { - // Already a range - result.push(portSpec); - } - - return result; -} - -/** - * Validates port numbers or ranges - */ -export function validatePorts(port: number | PortRange | Array): void { - if (Array.isArray(port)) { - port.forEach(p => validatePorts(p)); - return; - } - - if (typeof port === 'number') { - if (port < 1 || port > 65535) { - throw new NftValidationError(`Invalid port number: ${port}`); - } - } else if (typeof port === 'object') { - if (port.from < 1 || port.from > 65535 || port.to < 1 || port.to > 65535 || port.from > port.to) { - throw new NftValidationError(`Invalid port range: ${port.from}-${port.to}`); - } - } -} - -/** - * Format port range for nftables rule - */ -export function formatPortRange(range: PortRange): string { - if (range.from === range.to) { - return String(range.from); - } - return `${range.from}-${range.to}`; -} - -/** - * Convert port spec to nftables expression - */ -export function portSpecToNftExpr(portSpec: number | PortRange | Array): string { - const ranges = normalizePortSpec(portSpec); - - if (ranges.length === 1) { - return formatPortRange(ranges[0]); - } - - // Multiple ports/ranges need to use a set - const ports = ranges.map(formatPortRange); - return `{ ${ports.join(', ')} }`; -} - -/** - * Check if two port ranges overlap - */ -export function rangesOverlap(range1: PortRange, range2: PortRange): boolean { - return range1.from <= range2.to && range2.from <= range1.to; -} - -/** - * Merge overlapping port ranges - */ -export function mergeOverlappingRanges(ranges: PortRange[]): PortRange[] { - if (ranges.length <= 1) return ranges; - - // Sort by start port - const sorted = [...ranges].sort((a, b) => a.from - b.from); - const merged: PortRange[] = [sorted[0]]; - - for (let i = 1; i < sorted.length; i++) { - const current = sorted[i]; - const lastMerged = merged[merged.length - 1]; - - if (current.from <= lastMerged.to + 1) { - // Ranges overlap or are adjacent, merge them - lastMerged.to = Math.max(lastMerged.to, current.to); - } else { - // No overlap, add as new range - merged.push(current); - } - } - - return merged; -} - -/** - * Calculate the total number of ports in a port specification - */ -export function countPorts(portSpec: number | PortRange | Array): number { - const ranges = normalizePortSpec(portSpec); - return ranges.reduce((total, range) => total + (range.to - range.from + 1), 0); -} - -/** - * Check if a port is within the given specification - */ -export function isPortInSpec(port: number, portSpec: number | PortRange | Array): boolean { - const ranges = normalizePortSpec(portSpec); - return ranges.some(range => port >= range.from && port <= range.to); -} diff --git a/ts/proxies/nftables-proxy/utils/nft-rule-validator.ts b/ts/proxies/nftables-proxy/utils/nft-rule-validator.ts deleted file mode 100644 index d35b58c..0000000 --- a/ts/proxies/nftables-proxy/utils/nft-rule-validator.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * NFTables Rule Validator - * - * Handles validation of settings and inputs for nftables operations. - * Prevents command injection and ensures valid values. - */ - -import type { PortRange, NfTableProxyOptions } from '../models/index.js'; -import { NftValidationError } from '../models/index.js'; -import { validatePorts } from './nft-port-spec-normalizer.js'; - -// IP address validation patterns -const IPV4_REGEX = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))?$/; -const IPV6_REGEX = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/; -const HOSTNAME_REGEX = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/; -const TABLE_NAME_REGEX = /^[a-zA-Z0-9_]+$/; -const RATE_REGEX = /^[0-9]+[kKmMgG]?bps$/; - -/** - * Validates an IP address (IPv4 or IPv6) - */ -export function isValidIP(ip: string): boolean { - return IPV4_REGEX.test(ip) || IPV6_REGEX.test(ip); -} - -/** - * Validates an IPv4 address - */ -export function isValidIPv4(ip: string): boolean { - return IPV4_REGEX.test(ip); -} - -/** - * Validates an IPv6 address - */ -export function isValidIPv6(ip: string): boolean { - return IPV6_REGEX.test(ip); -} - -/** - * Validates a hostname - */ -export function isValidHostname(hostname: string): boolean { - return HOSTNAME_REGEX.test(hostname); -} - -/** - * Validates a table name for nftables - */ -export function isValidTableName(tableName: string): boolean { - return TABLE_NAME_REGEX.test(tableName); -} - -/** - * Validates a rate specification (e.g., "10mbps") - */ -export function isValidRate(rate: string): boolean { - return RATE_REGEX.test(rate); -} - -/** - * Validates an array of IP addresses - */ -export function validateIPs(ips?: string[]): void { - if (!ips) return; - - for (const ip of ips) { - if (!isValidIP(ip)) { - throw new NftValidationError(`Invalid IP address format: ${ip}`); - } - } -} - -/** - * Validates a host (can be hostname or IP) - */ -export function validateHost(host?: string): void { - if (!host) return; - - if (!isValidHostname(host) && !isValidIP(host)) { - throw new NftValidationError(`Invalid host format: ${host}`); - } -} - -/** - * Validates a table name - */ -export function validateTableName(tableName?: string): void { - if (!tableName) return; - - if (!isValidTableName(tableName)) { - throw new NftValidationError( - `Invalid table name: ${tableName}. Only alphanumeric characters and underscores are allowed.` - ); - } -} - -/** - * Validates QoS settings - */ -export function validateQosSettings(qos?: NfTableProxyOptions['qos']): void { - if (!qos?.enabled) return; - - if (qos.maxRate && !isValidRate(qos.maxRate)) { - throw new NftValidationError( - `Invalid rate format: ${qos.maxRate}. Use format like "10mbps", "1gbps", etc.` - ); - } - - if (qos.priority !== undefined) { - if (qos.priority < 1 || qos.priority > 10 || !Number.isInteger(qos.priority)) { - throw new NftValidationError( - `Invalid priority: ${qos.priority}. Must be an integer between 1 and 10.` - ); - } - } -} - -/** - * Validates all NfTablesProxy settings - */ -export function validateSettings(settings: NfTableProxyOptions): void { - // Validate port numbers - validatePorts(settings.fromPort); - validatePorts(settings.toPort); - - // Validate IP addresses - validateIPs(settings.ipAllowList); - validateIPs(settings.ipBlockList); - - // Validate target host - validateHost(settings.toHost); - - // Validate table name - validateTableName(settings.tableName); - - // Validate QoS settings - validateQosSettings(settings.qos); -} - -/** - * Check if an IP matches the given family (ip or ip6) - */ -export function isIPForFamily(ip: string, family: 'ip' | 'ip6'): boolean { - if (family === 'ip6') { - return ip.includes(':'); - } - return ip.includes('.'); -} - -/** - * Filter IPs by family - */ -export function filterIPsByFamily(ips: string[], family: 'ip' | 'ip6'): string[] { - return ips.filter(ip => isIPForFamily(ip, family)); -} diff --git a/ts/proxies/smart-proxy/models/route-types.ts b/ts/proxies/smart-proxy/models/route-types.ts index 791617c..61ec957 100644 --- a/ts/proxies/smart-proxy/models/route-types.ts +++ b/ts/proxies/smart-proxy/models/route-types.ts @@ -1,6 +1,4 @@ import * as plugins from '../../../plugins.js'; -// Certificate types removed - use local definition -import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js'; import type { IRouteContext } from '../../../core/models/route-context.js'; // Re-export IRouteContext for convenience