import type { TNftFamily, INftFirewallRule, INftIPSetConfig } from './nft.types.js'; /** * Build an nft firewall rule for input/output/forward chains. */ export function buildFirewallRule( tableName: string, family: TNftFamily, rule: INftFirewallRule, ): string[] { const chain = rule.direction; const parts: string[] = [`nft add rule ${family} ${tableName} ${chain}`]; // Connection tracking states if (rule.ctStates && rule.ctStates.length > 0) { parts.push(`ct state { ${rule.ctStates.join(', ')} }`); } // Protocol and port matching const protocols = expandProtocols(rule.protocol); const commands: string[] = []; for (const proto of protocols) { const ruleParts = [...parts]; if (rule.protocol) { ruleParts.push(proto); } if (rule.sourceIP) { ruleParts.push(`ip saddr ${rule.sourceIP}`); } if (rule.destIP) { ruleParts.push(`ip daddr ${rule.destIP}`); } if (rule.sourcePort != null) { ruleParts.push(`${proto} sport ${rule.sourcePort}`); } if (rule.destPort != null) { // If protocol wasn't explicitly set but we have a port, we need the protocol if (!rule.protocol) { ruleParts.push(`tcp dport ${rule.destPort}`); } else { ruleParts.push(`${proto} dport ${rule.destPort}`); } } if (rule.comment) { ruleParts.push(`comment "${rule.comment}"`); } ruleParts.push(rule.action); commands.push(ruleParts.join(' ')); } // If no protocol expansion needed (no protocol-specific fields) if (commands.length === 0) { const ruleParts = [...parts]; if (rule.sourceIP) { ruleParts.push(`ip saddr ${rule.sourceIP}`); } if (rule.destIP) { ruleParts.push(`ip daddr ${rule.destIP}`); } if (rule.comment) { ruleParts.push(`comment "${rule.comment}"`); } ruleParts.push(rule.action); commands.push(ruleParts.join(' ')); } return commands; } /** * Build commands to create an nft named set (IP set). */ export function buildIPSetCreate( tableName: string, family: TNftFamily, config: INftIPSetConfig, ): string[] { const commands: string[] = []; // Create the set commands.push( `nft add set ${family} ${tableName} ${config.name} { type ${config.type} \\; }` ); // Add initial elements if provided if (config.elements && config.elements.length > 0) { commands.push( `nft add element ${family} ${tableName} ${config.name} { ${config.elements.join(', ')} }` ); } return commands; } /** * Build command to add elements to an existing set. */ export function buildIPSetAddElements( tableName: string, family: TNftFamily, setName: string, elements: string[], ): string[] { if (elements.length === 0) return []; return [ `nft add element ${family} ${tableName} ${setName} { ${elements.join(', ')} }` ]; } /** * Build command to remove elements from a set. */ export function buildIPSetRemoveElements( tableName: string, family: TNftFamily, setName: string, elements: string[], ): string[] { if (elements.length === 0) return []; return [ `nft delete element ${family} ${tableName} ${setName} { ${elements.join(', ')} }` ]; } /** * Build command to delete an entire set. */ export function buildIPSetDelete( tableName: string, family: TNftFamily, setName: string, ): string[] { return [ `nft delete set ${family} ${tableName} ${setName}` ]; } /** * Build a rule that matches against a named set. */ export function buildIPSetMatchRule( tableName: string, family: TNftFamily, options: { setName: string; direction: 'input' | 'output' | 'forward'; matchField: 'saddr' | 'daddr'; action: 'accept' | 'drop' | 'reject'; }, ): string[] { return [ `nft add rule ${family} ${tableName} ${options.direction} ip ${options.matchField} @${options.setName} ${options.action}` ]; } // ─── Internal helpers ───────────────────────────────────────────── function expandProtocols(protocol?: 'tcp' | 'udp' | 'both'): string[] { if (!protocol) return []; switch (protocol) { case 'tcp': return ['tcp']; case 'udp': return ['udp']; case 'both': return ['tcp', 'udp']; } }