BREAKING CHANGE(nftables): Replace IPTablesProxy with NfTablesProxy and update module exports in index.ts
This commit is contained in:
		| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '4.3.0', | ||||
|   version: '5.0.0', | ||||
|   description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.' | ||||
| } | ||||
|   | ||||
| @@ -1,901 +0,0 @@ | ||||
| import { exec, execSync } from 'child_process'; | ||||
| import { promisify } from 'util'; | ||||
|  | ||||
| const execAsync = promisify(exec); | ||||
|  | ||||
| /** | ||||
|  * Represents a port range for forwarding | ||||
|  */ | ||||
| export interface IPortRange { | ||||
|   from: number; | ||||
|   to: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Settings for IPTablesProxy. | ||||
|  */ | ||||
| export interface IIpTableProxySettings { | ||||
|   // Basic settings | ||||
|   fromPort: number | IPortRange | Array<number | IPortRange>; // Support single port, port range, or multiple ports/ranges | ||||
|   toPort: number | IPortRange | Array<number | IPortRange>; | ||||
|   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 marked iptables rules before process exit | ||||
|   protocol?: 'tcp' | 'udp' | 'all'; // Protocol to forward, defaults to 'tcp' | ||||
|   enableLogging?: boolean;    // Enable detailed logging | ||||
|   ipv6Support?: boolean;      // Enable IPv6 support (ip6tables) | ||||
|    | ||||
|   // Source filtering | ||||
|   allowedSourceIPs?: string[]; // If provided, only these IPs are allowed | ||||
|   bannedSourceIPs?: string[];  // If provided, these IPs are blocked | ||||
|    | ||||
|   // Rule management | ||||
|   forceCleanSlate?: boolean;   // Clear all IPTablesProxy rules before starting | ||||
|   addJumpRule?: boolean;       // Add a custom chain for cleaner rule management | ||||
|   checkExistingRules?: boolean; // Check if rules already exist before adding | ||||
|    | ||||
|   // Integration with PortProxy/NetworkProxy | ||||
|   netProxyIntegration?: { | ||||
|     enabled: boolean; | ||||
|     redirectLocalhost?: boolean; // Redirect localhost traffic to NetworkProxy | ||||
|     sslTerminationPort?: number; // Port where NetworkProxy handles SSL termination | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Represents a rule added to iptables | ||||
|  */ | ||||
| interface IpTablesRule { | ||||
|   table: string; | ||||
|   chain: string; | ||||
|   command: string; | ||||
|   tag: string; | ||||
|   added: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * IPTablesProxy sets up iptables NAT rules to forward TCP traffic. | ||||
|  * Enhanced with multi-port support, IPv6, and integration with PortProxy/NetworkProxy. | ||||
|  */ | ||||
| export class IPTablesProxy { | ||||
|   public settings: IIpTableProxySettings; | ||||
|   private rules: IpTablesRule[] = []; | ||||
|   private ruleTag: string; | ||||
|   private customChain: string | null = null; | ||||
|  | ||||
|   constructor(settings: IIpTableProxySettings) { | ||||
|     // Validate inputs to prevent command injection | ||||
|     this.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, | ||||
|       checkExistingRules: settings.checkExistingRules !== undefined ? settings.checkExistingRules : true, | ||||
|       netProxyIntegration: settings.netProxyIntegration || { enabled: false } | ||||
|     }; | ||||
|      | ||||
|     // Generate a unique identifier for the rules added by this instance | ||||
|     this.ruleTag = `IPTablesProxy:${Date.now()}:${Math.random().toString(36).substr(2, 5)}`; | ||||
|      | ||||
|     if (this.settings.addJumpRule) { | ||||
|       this.customChain = `IPTablesProxy_${Math.random().toString(36).substr(2, 5)}`; | ||||
|     } | ||||
|  | ||||
|     // Register cleanup handlers if deleteOnExit is true | ||||
|     if (this.settings.deleteOnExit) { | ||||
|       const cleanup = () => { | ||||
|         try { | ||||
|           this.stopSync(); | ||||
|         } catch (err) { | ||||
|           console.error('Error cleaning iptables rules on exit:', err); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       process.on('exit', cleanup); | ||||
|       process.on('SIGINT', () => { | ||||
|         cleanup(); | ||||
|         process.exit(); | ||||
|       }); | ||||
|       process.on('SIGTERM', () => { | ||||
|         cleanup(); | ||||
|         process.exit(); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Validates settings to prevent command injection and ensure valid values | ||||
|    */ | ||||
|   private validateSettings(settings: IIpTableProxySettings): void { | ||||
|     // Validate port numbers | ||||
|     const validatePorts = (port: number | IPortRange | Array<number | IPortRange>) => { | ||||
|       if (Array.isArray(port)) { | ||||
|         port.forEach(p => validatePorts(p)); | ||||
|         return; | ||||
|       } | ||||
|        | ||||
|       if (typeof port === 'number') { | ||||
|         if (port < 1 || port > 65535) { | ||||
|           throw new Error(`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 Error(`Invalid port range: ${port.from}-${port.to}`); | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
|      | ||||
|     validatePorts(settings.fromPort); | ||||
|     validatePorts(settings.toPort); | ||||
|      | ||||
|     // Define regex patterns at the method level so they're available throughout | ||||
|     const ipRegex = /^(([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 ipv6Regex = /^(([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]))?$/; | ||||
|      | ||||
|     // Validate IP addresses | ||||
|     const validateIPs = (ips?: string[]) => { | ||||
|       if (!ips) return; | ||||
|        | ||||
|       for (const ip of ips) { | ||||
|         if (!ipRegex.test(ip) && !ipv6Regex.test(ip)) { | ||||
|           throw new Error(`Invalid IP address format: ${ip}`); | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
|      | ||||
|     validateIPs(settings.allowedSourceIPs); | ||||
|     validateIPs(settings.bannedSourceIPs); | ||||
|      | ||||
|     // Validate toHost - only allow hostnames or IPs | ||||
|     if (settings.toHost) { | ||||
|       const hostRegex = /^(([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])$/; | ||||
|       if (!hostRegex.test(settings.toHost) && !ipRegex.test(settings.toHost) && !ipv6Regex.test(settings.toHost)) { | ||||
|         throw new Error(`Invalid host format: ${settings.toHost}`); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Normalizes port specifications into an array of port ranges | ||||
|    */ | ||||
|   private normalizePortSpec(portSpec: number | IPortRange | Array<number | IPortRange>): IPortRange[] { | ||||
|     const result: IPortRange[] = []; | ||||
|      | ||||
|     if (Array.isArray(portSpec)) { | ||||
|       // If it's an array, process each element | ||||
|       for (const spec of portSpec) { | ||||
|         result.push(...this.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; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Gets the appropriate iptables command based on settings | ||||
|    */ | ||||
|   private getIptablesCommand(isIpv6: boolean = false): string { | ||||
|     return isIpv6 ? 'ip6tables' : 'iptables'; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Checks if a rule already exists in iptables | ||||
|    */ | ||||
|   private async ruleExists(table: string, command: string, isIpv6: boolean = false): Promise<boolean> { | ||||
|     try { | ||||
|       const iptablesCmd = this.getIptablesCommand(isIpv6); | ||||
|       const { stdout } = await execAsync(`${iptablesCmd}-save -t ${table}`); | ||||
|       // Convert the command to the format found in iptables-save output | ||||
|       // (This is a simplification - in reality, you'd need more parsing) | ||||
|       const rulePattern = command.replace(`${iptablesCmd} -t ${table} -A `, '-A '); | ||||
|       return stdout.split('\n').some(line => line.trim() === rulePattern); | ||||
|     } catch (err) { | ||||
|       this.log('error', `Failed to check if rule exists: ${err}`); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Sets up a custom chain for better rule management | ||||
|    */ | ||||
|   private async setupCustomChain(isIpv6: boolean = false): Promise<boolean> { | ||||
|     if (!this.customChain) return true; | ||||
|      | ||||
|     const iptablesCmd = this.getIptablesCommand(isIpv6); | ||||
|     const table = 'nat'; | ||||
|      | ||||
|     try { | ||||
|       // Create the chain | ||||
|       await execAsync(`${iptablesCmd} -t ${table} -N ${this.customChain}`); | ||||
|       this.log('info', `Created custom chain: ${this.customChain}`); | ||||
|        | ||||
|       // Add jump rule to PREROUTING chain | ||||
|       const jumpCommand = `${iptablesCmd} -t ${table} -A PREROUTING -j ${this.customChain} -m comment --comment "${this.ruleTag}:JUMP"`; | ||||
|       await execAsync(jumpCommand); | ||||
|       this.log('info', `Added jump rule to ${this.customChain}`); | ||||
|        | ||||
|       // Store the jump rule | ||||
|       this.rules.push({ | ||||
|         table, | ||||
|         chain: 'PREROUTING', | ||||
|         command: jumpCommand, | ||||
|         tag: `${this.ruleTag}:JUMP`, | ||||
|         added: true | ||||
|       }); | ||||
|        | ||||
|       return true; | ||||
|     } catch (err) { | ||||
|       this.log('error', `Failed to set up custom chain: ${err}`); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Add a source IP filter rule | ||||
|    */ | ||||
|   private async addSourceIPFilter(isIpv6: boolean = false): Promise<boolean> { | ||||
|     if (!this.settings.allowedSourceIPs && !this.settings.bannedSourceIPs) { | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     const iptablesCmd = this.getIptablesCommand(isIpv6); | ||||
|     const table = 'nat'; | ||||
|     const chain = this.customChain || 'PREROUTING'; | ||||
|      | ||||
|     try { | ||||
|       // Add banned IPs first (explicit deny) | ||||
|       if (this.settings.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) { | ||||
|         for (const ip of this.settings.bannedSourceIPs) { | ||||
|           const command = `${iptablesCmd} -t ${table} -A ${chain} -s ${ip} -j DROP -m comment --comment "${this.ruleTag}:BANNED"`; | ||||
|            | ||||
|           // Check if rule already exists | ||||
|           if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) { | ||||
|             this.log('info', `Rule already exists, skipping: ${command}`); | ||||
|             continue; | ||||
|           } | ||||
|            | ||||
|           await execAsync(command); | ||||
|           this.log('info', `Added banned IP rule: ${command}`); | ||||
|            | ||||
|           this.rules.push({ | ||||
|             table, | ||||
|             chain, | ||||
|             command, | ||||
|             tag: `${this.ruleTag}:BANNED`, | ||||
|             added: true | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Add allowed IPs (explicit allow) | ||||
|       if (this.settings.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) { | ||||
|         // First add a default deny for all | ||||
|         const denyAllCommand = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} -j DROP -m comment --comment "${this.ruleTag}:DENY_ALL"`; | ||||
|          | ||||
|         // Add allow rules for specific IPs | ||||
|         for (const ip of this.settings.allowedSourceIPs) { | ||||
|           const command = `${iptablesCmd} -t ${table} -A ${chain} -s ${ip} -p ${this.settings.protocol} -j ACCEPT -m comment --comment "${this.ruleTag}:ALLOWED"`; | ||||
|            | ||||
|           // Check if rule already exists | ||||
|           if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) { | ||||
|             this.log('info', `Rule already exists, skipping: ${command}`); | ||||
|             continue; | ||||
|           } | ||||
|            | ||||
|           await execAsync(command); | ||||
|           this.log('info', `Added allowed IP rule: ${command}`); | ||||
|            | ||||
|           this.rules.push({ | ||||
|             table, | ||||
|             chain, | ||||
|             command, | ||||
|             tag: `${this.ruleTag}:ALLOWED`, | ||||
|             added: true | ||||
|           }); | ||||
|         } | ||||
|          | ||||
|         // Now add the default deny after all allows | ||||
|         if (this.settings.checkExistingRules && await this.ruleExists(table, denyAllCommand, isIpv6)) { | ||||
|           this.log('info', `Rule already exists, skipping: ${denyAllCommand}`); | ||||
|         } else { | ||||
|           await execAsync(denyAllCommand); | ||||
|           this.log('info', `Added default deny rule: ${denyAllCommand}`); | ||||
|            | ||||
|           this.rules.push({ | ||||
|             table, | ||||
|             chain, | ||||
|             command: denyAllCommand, | ||||
|             tag: `${this.ruleTag}:DENY_ALL`, | ||||
|             added: true | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return true; | ||||
|     } catch (err) { | ||||
|       this.log('error', `Failed to add source IP filter rules: ${err}`); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Adds a port forwarding rule | ||||
|    */ | ||||
|   private async addPortForwardingRule( | ||||
|     fromPortRange: IPortRange, | ||||
|     toPortRange: IPortRange, | ||||
|     isIpv6: boolean = false | ||||
|   ): Promise<boolean> { | ||||
|     const iptablesCmd = this.getIptablesCommand(isIpv6); | ||||
|     const table = 'nat'; | ||||
|     const chain = this.customChain || 'PREROUTING'; | ||||
|      | ||||
|     try { | ||||
|       // Handle single port case | ||||
|       if (fromPortRange.from === fromPortRange.to && toPortRange.from === toPortRange.to) { | ||||
|         // Single port forward | ||||
|         const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPortRange.from} ` + | ||||
|           `-j DNAT --to-destination ${this.settings.toHost}:${toPortRange.from} ` + | ||||
|           `-m comment --comment "${this.ruleTag}:DNAT"`; | ||||
|          | ||||
|         // Check if rule already exists | ||||
|         if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) { | ||||
|           this.log('info', `Rule already exists, skipping: ${command}`); | ||||
|         } else { | ||||
|           await execAsync(command); | ||||
|           this.log('info', `Added port forwarding rule: ${command}`); | ||||
|            | ||||
|           this.rules.push({ | ||||
|             table, | ||||
|             chain, | ||||
|             command, | ||||
|             tag: `${this.ruleTag}:DNAT`, | ||||
|             added: true | ||||
|           }); | ||||
|         } | ||||
|       } else if (fromPortRange.to - fromPortRange.from === toPortRange.to - toPortRange.from) { | ||||
|         // Port range forward with equal ranges | ||||
|         const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPortRange.from}:${fromPortRange.to} ` + | ||||
|           `-j DNAT --to-destination ${this.settings.toHost}:${toPortRange.from}-${toPortRange.to} ` + | ||||
|           `-m comment --comment "${this.ruleTag}:DNAT_RANGE"`; | ||||
|          | ||||
|         // Check if rule already exists | ||||
|         if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) { | ||||
|           this.log('info', `Rule already exists, skipping: ${command}`); | ||||
|         } else { | ||||
|           await execAsync(command); | ||||
|           this.log('info', `Added port range forwarding rule: ${command}`); | ||||
|            | ||||
|           this.rules.push({ | ||||
|             table, | ||||
|             chain, | ||||
|             command, | ||||
|             tag: `${this.ruleTag}:DNAT_RANGE`, | ||||
|             added: true | ||||
|           }); | ||||
|         } | ||||
|       } else { | ||||
|         // Unequal port ranges need individual rules | ||||
|         for (let i = 0; i <= fromPortRange.to - fromPortRange.from; i++) { | ||||
|           const fromPort = fromPortRange.from + i; | ||||
|           const toPort = toPortRange.from + i % (toPortRange.to - toPortRange.from + 1); | ||||
|            | ||||
|           const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPort} ` + | ||||
|             `-j DNAT --to-destination ${this.settings.toHost}:${toPort} ` + | ||||
|             `-m comment --comment "${this.ruleTag}:DNAT_INDIVIDUAL"`; | ||||
|            | ||||
|           // Check if rule already exists | ||||
|           if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) { | ||||
|             this.log('info', `Rule already exists, skipping: ${command}`); | ||||
|             continue; | ||||
|           } | ||||
|            | ||||
|           await execAsync(command); | ||||
|           this.log('info', `Added individual port forwarding rule: ${command}`); | ||||
|            | ||||
|           this.rules.push({ | ||||
|             table, | ||||
|             chain, | ||||
|             command, | ||||
|             tag: `${this.ruleTag}:DNAT_INDIVIDUAL`, | ||||
|             added: true | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // If preserveSourceIP is false, add a MASQUERADE rule | ||||
|       if (!this.settings.preserveSourceIP) { | ||||
|         // For port range | ||||
|         const masqCommand = `${iptablesCmd} -t nat -A POSTROUTING -p ${this.settings.protocol} -d ${this.settings.toHost} ` + | ||||
|           `--dport ${toPortRange.from}:${toPortRange.to} -j MASQUERADE ` + | ||||
|           `-m comment --comment "${this.ruleTag}:MASQ"`; | ||||
|          | ||||
|         // Check if rule already exists | ||||
|         if (this.settings.checkExistingRules && await this.ruleExists('nat', masqCommand, isIpv6)) { | ||||
|           this.log('info', `Rule already exists, skipping: ${masqCommand}`); | ||||
|         } else { | ||||
|           await execAsync(masqCommand); | ||||
|           this.log('info', `Added MASQUERADE rule: ${masqCommand}`); | ||||
|            | ||||
|           this.rules.push({ | ||||
|             table: 'nat', | ||||
|             chain: 'POSTROUTING', | ||||
|             command: masqCommand, | ||||
|             tag: `${this.ruleTag}:MASQ`, | ||||
|             added: true | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return true; | ||||
|     } catch (err) { | ||||
|       this.log('error', `Failed to add port forwarding rule: ${err}`); | ||||
|        | ||||
|       // Try to roll back any rules that were already added | ||||
|       await this.rollbackRules(); | ||||
|        | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Special handling for NetworkProxy integration | ||||
|    */ | ||||
|   private async setupNetworkProxyIntegration(isIpv6: boolean = false): Promise<boolean> { | ||||
|     if (!this.settings.netProxyIntegration?.enabled) { | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     const netProxyConfig = this.settings.netProxyIntegration; | ||||
|     const iptablesCmd = this.getIptablesCommand(isIpv6); | ||||
|     const table = 'nat'; | ||||
|     const chain = this.customChain || 'PREROUTING'; | ||||
|      | ||||
|     try { | ||||
|       // If redirectLocalhost is true, set up special rule to redirect localhost traffic to NetworkProxy | ||||
|       if (netProxyConfig.redirectLocalhost && netProxyConfig.sslTerminationPort) { | ||||
|         const redirectCommand = `${iptablesCmd} -t ${table} -A OUTPUT -p tcp -d 127.0.0.1 -j REDIRECT ` + | ||||
|           `--to-port ${netProxyConfig.sslTerminationPort} ` + | ||||
|           `-m comment --comment "${this.ruleTag}:NETPROXY_REDIRECT"`; | ||||
|          | ||||
|         // Check if rule already exists | ||||
|         if (this.settings.checkExistingRules && await this.ruleExists(table, redirectCommand, isIpv6)) { | ||||
|           this.log('info', `Rule already exists, skipping: ${redirectCommand}`); | ||||
|         } else { | ||||
|           await execAsync(redirectCommand); | ||||
|           this.log('info', `Added NetworkProxy redirection rule: ${redirectCommand}`); | ||||
|            | ||||
|           this.rules.push({ | ||||
|             table, | ||||
|             chain: 'OUTPUT', | ||||
|             command: redirectCommand, | ||||
|             tag: `${this.ruleTag}:NETPROXY_REDIRECT`, | ||||
|             added: true | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return true; | ||||
|     } catch (err) { | ||||
|       this.log('error', `Failed to set up NetworkProxy integration: ${err}`); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Rolls back rules that were added in case of error | ||||
|    */ | ||||
|   private async rollbackRules(): Promise<void> { | ||||
|     // 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 { | ||||
|           // Convert -A (add) to -D (delete) | ||||
|           const deleteCommand = rule.command.replace('-A', '-D'); | ||||
|           await execAsync(deleteCommand); | ||||
|           this.log('info', `Rolled back rule: ${deleteCommand}`); | ||||
|            | ||||
|           rule.added = false; | ||||
|         } catch (err) { | ||||
|           this.log('error', `Failed to roll back rule: ${err}`); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Sets up iptables rules for port forwarding with enhanced features | ||||
|    */ | ||||
|   public async start(): Promise<void> { | ||||
|     // Optionally clean the slate first | ||||
|     if (this.settings.forceCleanSlate) { | ||||
|       await IPTablesProxy.cleanSlate(); | ||||
|     } | ||||
|      | ||||
|     // First set up any custom chains | ||||
|     if (this.settings.addJumpRule) { | ||||
|       const chainSetupSuccess = await this.setupCustomChain(); | ||||
|       if (!chainSetupSuccess) { | ||||
|         throw new Error('Failed to set up custom chain'); | ||||
|       } | ||||
|        | ||||
|       // For IPv6 if enabled | ||||
|       if (this.settings.ipv6Support) { | ||||
|         const chainSetupSuccessIpv6 = await this.setupCustomChain(true); | ||||
|         if (!chainSetupSuccessIpv6) { | ||||
|           this.log('warn', 'Failed to set up IPv6 custom chain, continuing with IPv4 only'); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Add source IP filters | ||||
|     await this.addSourceIPFilter(); | ||||
|     if (this.settings.ipv6Support) { | ||||
|       await this.addSourceIPFilter(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); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Normalize port specifications | ||||
|     const fromPortRanges = this.normalizePortSpec(this.settings.fromPort); | ||||
|     const toPortRanges = this.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 | ||||
|         for (const fromRange of fromPortRanges) { | ||||
|           await this.addPortForwardingRule(fromRange, toPortRanges[0]); | ||||
|            | ||||
|           if (this.settings.ipv6Support) { | ||||
|             await this.addPortForwardingRule(fromRange, toPortRanges[0], true); | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         throw new Error('Mismatched port counts: fromPort and toPort arrays must have equal length or toPort must be a single value'); | ||||
|       } | ||||
|     } else { | ||||
|       // Add port forwarding rules for each port specification | ||||
|       for (let i = 0; i < fromPortRanges.length; i++) { | ||||
|         await this.addPortForwardingRule(fromPortRanges[i], toPortRanges[i]); | ||||
|          | ||||
|         if (this.settings.ipv6Support) { | ||||
|           await this.addPortForwardingRule(fromPortRanges[i], toPortRanges[i], true); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Final check - ensure we have at least one rule added | ||||
|     if (this.rules.filter(r => r.added).length === 0) { | ||||
|       throw new Error('No rules were added'); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Removes all added iptables rules | ||||
|    */ | ||||
|   public async stop(): Promise<void> { | ||||
|     // 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 { | ||||
|           // Convert -A (add) to -D (delete) | ||||
|           const deleteCommand = rule.command.replace('-A', '-D'); | ||||
|           await execAsync(deleteCommand); | ||||
|           this.log('info', `Removed rule: ${deleteCommand}`); | ||||
|            | ||||
|           rule.added = false; | ||||
|         } catch (err) { | ||||
|           this.log('error', `Failed to remove rule: ${err}`); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // If we created a custom chain, we need to clean it up | ||||
|     if (this.customChain) { | ||||
|       try { | ||||
|         // First flush the chain | ||||
|         await execAsync(`iptables -t nat -F ${this.customChain}`); | ||||
|         this.log('info', `Flushed custom chain: ${this.customChain}`); | ||||
|          | ||||
|         // Then delete it | ||||
|         await execAsync(`iptables -t nat -X ${this.customChain}`); | ||||
|         this.log('info', `Deleted custom chain: ${this.customChain}`); | ||||
|          | ||||
|         // Same for IPv6 if enabled | ||||
|         if (this.settings.ipv6Support) { | ||||
|           try { | ||||
|             await execAsync(`ip6tables -t nat -F ${this.customChain}`); | ||||
|             await execAsync(`ip6tables -t nat -X ${this.customChain}`); | ||||
|             this.log('info', `Deleted IPv6 custom chain: ${this.customChain}`); | ||||
|           } catch (err) { | ||||
|             this.log('error', `Failed to delete IPv6 custom chain: ${err}`); | ||||
|           } | ||||
|         } | ||||
|       } catch (err) { | ||||
|         this.log('error', `Failed to delete custom chain: ${err}`); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Clear rules array | ||||
|     this.rules = []; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Synchronous version of stop, for use in exit handlers | ||||
|    */ | ||||
|   public stopSync(): void { | ||||
|     // 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 { | ||||
|           // Convert -A (add) to -D (delete) | ||||
|           const deleteCommand = rule.command.replace('-A', '-D'); | ||||
|           execSync(deleteCommand); | ||||
|           this.log('info', `Removed rule: ${deleteCommand}`); | ||||
|            | ||||
|           rule.added = false; | ||||
|         } catch (err) { | ||||
|           this.log('error', `Failed to remove rule: ${err}`); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // If we created a custom chain, we need to clean it up | ||||
|     if (this.customChain) { | ||||
|       try { | ||||
|         // First flush the chain | ||||
|         execSync(`iptables -t nat -F ${this.customChain}`); | ||||
|          | ||||
|         // Then delete it | ||||
|         execSync(`iptables -t nat -X ${this.customChain}`); | ||||
|         this.log('info', `Deleted custom chain: ${this.customChain}`); | ||||
|          | ||||
|         // Same for IPv6 if enabled | ||||
|         if (this.settings.ipv6Support) { | ||||
|           try { | ||||
|             execSync(`ip6tables -t nat -F ${this.customChain}`); | ||||
|             execSync(`ip6tables -t nat -X ${this.customChain}`); | ||||
|           } catch (err) { | ||||
|             // IPv6 failures are non-critical | ||||
|           } | ||||
|         } | ||||
|       } catch (err) { | ||||
|         this.log('error', `Failed to delete custom chain: ${err}`); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Clear rules array | ||||
|     this.rules = []; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Asynchronously cleans up any iptables rules in the nat table that were added by this module. | ||||
|    * It looks for rules with comments containing "IPTablesProxy:". | ||||
|    */ | ||||
|   public static async cleanSlate(): Promise<void> { | ||||
|     await IPTablesProxy.cleanSlateInternal(); | ||||
|      | ||||
|     // Also clean IPv6 rules | ||||
|     await IPTablesProxy.cleanSlateInternal(true); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Internal implementation of cleanSlate with IPv6 support | ||||
|    */ | ||||
|   private static async cleanSlateInternal(isIpv6: boolean = false): Promise<void> { | ||||
|     const iptablesCmd = isIpv6 ? 'ip6tables' : 'iptables'; | ||||
|      | ||||
|     try { | ||||
|       const { stdout } = await execAsync(`${iptablesCmd}-save -t nat`); | ||||
|       const lines = stdout.split('\n'); | ||||
|       const proxyLines = lines.filter(line => line.includes('IPTablesProxy:')); | ||||
|        | ||||
|       // First, find and remove any custom chains | ||||
|       const customChains = new Set<string>(); | ||||
|       const jumpRules: string[] = []; | ||||
|        | ||||
|       for (const line of proxyLines) { | ||||
|         if (line.includes('IPTablesProxy:JUMP')) { | ||||
|           // Extract chain name from jump rule | ||||
|           const match = line.match(/\s+-j\s+(\S+)\s+/); | ||||
|           if (match && match[1].startsWith('IPTablesProxy_')) { | ||||
|             customChains.add(match[1]); | ||||
|             jumpRules.push(line); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Remove jump rules first | ||||
|       for (const line of jumpRules) { | ||||
|         const trimmedLine = line.trim(); | ||||
|         if (trimmedLine.startsWith('-A')) { | ||||
|           // Replace the "-A" with "-D" to form a deletion command | ||||
|           const deleteRule = trimmedLine.replace('-A', '-D'); | ||||
|           const cmd = `${iptablesCmd} -t nat ${deleteRule}`; | ||||
|           try { | ||||
|             await execAsync(cmd); | ||||
|             console.log(`Cleaned up iptables jump rule: ${cmd}`); | ||||
|           } catch (err) { | ||||
|             console.error(`Failed to remove iptables jump rule: ${cmd}`, err); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Then remove all other rules | ||||
|       for (const line of proxyLines) { | ||||
|         if (!line.includes('IPTablesProxy:JUMP')) { // Skip jump rules we already handled | ||||
|           const trimmedLine = line.trim(); | ||||
|           if (trimmedLine.startsWith('-A')) { | ||||
|             // Replace the "-A" with "-D" to form a deletion command | ||||
|             const deleteRule = trimmedLine.replace('-A', '-D'); | ||||
|             const cmd = `${iptablesCmd} -t nat ${deleteRule}`; | ||||
|             try { | ||||
|               await execAsync(cmd); | ||||
|               console.log(`Cleaned up iptables rule: ${cmd}`); | ||||
|             } catch (err) { | ||||
|               console.error(`Failed to remove iptables rule: ${cmd}`, err); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Finally clean up custom chains | ||||
|       for (const chain of customChains) { | ||||
|         try { | ||||
|           // Flush the chain | ||||
|           await execAsync(`${iptablesCmd} -t nat -F ${chain}`); | ||||
|           console.log(`Flushed custom chain: ${chain}`); | ||||
|            | ||||
|           // Delete the chain | ||||
|           await execAsync(`${iptablesCmd} -t nat -X ${chain}`); | ||||
|           console.log(`Deleted custom chain: ${chain}`); | ||||
|         } catch (err) { | ||||
|           console.error(`Failed to delete custom chain ${chain}:`, err); | ||||
|         } | ||||
|       } | ||||
|     } catch (err) { | ||||
|       console.error(`Failed to run ${iptablesCmd}-save: ${err}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Synchronously cleans up any iptables rules in the nat table that were added by this module. | ||||
|    * It looks for rules with comments containing "IPTablesProxy:". | ||||
|    * This method is intended for use in process exit handlers. | ||||
|    */ | ||||
|   public static cleanSlateSync(): void { | ||||
|     IPTablesProxy.cleanSlateSyncInternal(); | ||||
|      | ||||
|     // Also clean IPv6 rules | ||||
|     IPTablesProxy.cleanSlateSyncInternal(true); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Internal implementation of cleanSlateSync with IPv6 support | ||||
|    */ | ||||
|   private static cleanSlateSyncInternal(isIpv6: boolean = false): void { | ||||
|     const iptablesCmd = isIpv6 ? 'ip6tables' : 'iptables'; | ||||
|      | ||||
|     try { | ||||
|       const stdout = execSync(`${iptablesCmd}-save -t nat`).toString(); | ||||
|       const lines = stdout.split('\n'); | ||||
|       const proxyLines = lines.filter(line => line.includes('IPTablesProxy:')); | ||||
|        | ||||
|       // First, find and remove any custom chains | ||||
|       const customChains = new Set<string>(); | ||||
|       const jumpRules: string[] = []; | ||||
|        | ||||
|       for (const line of proxyLines) { | ||||
|         if (line.includes('IPTablesProxy:JUMP')) { | ||||
|           // Extract chain name from jump rule | ||||
|           const match = line.match(/\s+-j\s+(\S+)\s+/); | ||||
|           if (match && match[1].startsWith('IPTablesProxy_')) { | ||||
|             customChains.add(match[1]); | ||||
|             jumpRules.push(line); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Remove jump rules first | ||||
|       for (const line of jumpRules) { | ||||
|         const trimmedLine = line.trim(); | ||||
|         if (trimmedLine.startsWith('-A')) { | ||||
|           // Replace the "-A" with "-D" to form a deletion command | ||||
|           const deleteRule = trimmedLine.replace('-A', '-D'); | ||||
|           const cmd = `${iptablesCmd} -t nat ${deleteRule}`; | ||||
|           try { | ||||
|             execSync(cmd); | ||||
|             console.log(`Cleaned up iptables jump rule: ${cmd}`); | ||||
|           } catch (err) { | ||||
|             console.error(`Failed to remove iptables jump rule: ${cmd}`, err); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Then remove all other rules | ||||
|       for (const line of proxyLines) { | ||||
|         if (!line.includes('IPTablesProxy:JUMP')) { // Skip jump rules we already handled | ||||
|           const trimmedLine = line.trim(); | ||||
|           if (trimmedLine.startsWith('-A')) { | ||||
|             const deleteRule = trimmedLine.replace('-A', '-D'); | ||||
|             const cmd = `${iptablesCmd} -t nat ${deleteRule}`; | ||||
|             try { | ||||
|               execSync(cmd); | ||||
|               console.log(`Cleaned up iptables rule: ${cmd}`); | ||||
|             } catch (err) { | ||||
|               console.error(`Failed to remove iptables rule: ${cmd}`, err); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Finally clean up custom chains | ||||
|       for (const chain of customChains) { | ||||
|         try { | ||||
|           // Flush the chain | ||||
|           execSync(`${iptablesCmd} -t nat -F ${chain}`); | ||||
|            | ||||
|           // Delete the chain | ||||
|           execSync(`${iptablesCmd} -t nat -X ${chain}`); | ||||
|           console.log(`Deleted custom chain: ${chain}`); | ||||
|         } catch (err) { | ||||
|           console.error(`Failed to delete custom chain ${chain}:`, err); | ||||
|         } | ||||
|       } | ||||
|     } catch (err) { | ||||
|       console.error(`Failed to run ${iptablesCmd}-save: ${err}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Logging utility that respects the enableLogging setting | ||||
|    */ | ||||
|   private log(level: 'info' | 'warn' | 'error', message: string): void { | ||||
|     if (!this.settings.enableLogging && level === 'info') { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     const timestamp = new Date().toISOString(); | ||||
|      | ||||
|     switch (level) { | ||||
|       case 'info': | ||||
|         console.log(`[${timestamp}] [INFO] ${message}`); | ||||
|         break; | ||||
|       case 'warn': | ||||
|         console.warn(`[${timestamp}] [WARN] ${message}`); | ||||
|         break; | ||||
|       case 'error': | ||||
|         console.error(`[${timestamp}] [ERROR] ${message}`); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										2045
									
								
								ts/classes.nftablesproxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2045
									
								
								ts/classes.nftablesproxy.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,4 +1,4 @@ | ||||
| export * from './classes.iptablesproxy.js'; | ||||
| export * from './classes.nftablesproxy.js'; | ||||
| export * from './classes.networkproxy.js'; | ||||
| export * from './classes.port80handler.js'; | ||||
| export * from './classes.sslredirect.js'; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user