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; // Support single port, port range, or multiple ports/ranges toPort: number | IPortRange | 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 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) => { 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): 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 { 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 { 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 { 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 { 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 { 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 { // 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 { // 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 { // 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 { 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 { 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(); 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(); 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; } } }