163 lines
4.5 KiB
TypeScript
163 lines
4.5 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<string, any>): 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<string> {
|
||
|
|
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<void> {
|
||
|
|
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<boolean> {
|
||
|
|
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<boolean> {
|
||
|
|
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<string> {
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|