Files
smartnftables/ts/nft.rulebuilder.firewall.ts
2026-03-26 10:32:05 +00:00

174 lines
4.2 KiB
TypeScript

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'];
}
}