initial
This commit is contained in:
173
ts/nft.rulebuilder.firewall.ts
Normal file
173
ts/nft.rulebuilder.firewall.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user