feat(proxies): introduce nftables command executor and utilities, default certificate provider, expanded route/socket helper modules, and security improvements
This commit is contained in:
162
ts/proxies/nftables-proxy/utils/nft-command-executor.ts
Normal file
162
ts/proxies/nftables-proxy/utils/nft-command-executor.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user