/** * 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; } }