This commit is contained in:
2026-03-26 10:32:05 +00:00
commit 450bc4a2b0
26 changed files with 10156 additions and 0 deletions

8
ts/00_commitinfo_data.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@push.rocks/smartnftables',
version: '1.0.0',
description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
}

59
ts/index.ts Normal file
View File

@@ -0,0 +1,59 @@
// Main facade
export { SmartNftables } from './nft.manager.js';
// Sub-managers
export { NatManager } from './nft.manager.nat.js';
export { FirewallManager } from './nft.manager.firewall.js';
export { RateLimitManager } from './nft.manager.ratelimit.js';
// Executor
export { NftExecutor, NftNotRootError, NftCommandError } from './nft.executor.js';
// Rule builders (for advanced usage)
export {
buildTableSetup,
buildFilterChains,
buildTableCleanup,
} from './nft.rulebuilder.table.js';
export {
buildDnatRules,
buildSnatRule,
buildMasqueradeRule,
} from './nft.rulebuilder.nat.js';
export {
buildRateLimitRule,
buildPerIpRateLimitRule,
buildConnectionRateRule,
} from './nft.rulebuilder.ratelimit.js';
export {
buildFirewallRule,
buildIPSetCreate,
buildIPSetAddElements,
buildIPSetRemoveElements,
buildIPSetDelete,
buildIPSetMatchRule,
} from './nft.rulebuilder.firewall.js';
// Types
export type {
TNftProtocol,
TNftFamily,
TNftChainHook,
TNftChainType,
TNftPolicy,
TFirewallAction,
TCtState,
INftDnatRule,
INftSnatRule,
INftMasqueradeRule,
INftRateLimitRule,
INftConnectionRateRule,
INftFirewallRule,
INftIPSetConfig,
INftRuleGroup,
ISmartNftablesOptions,
INftStatus,
} from './nft.types.js';

74
ts/nft.executor.ts Normal file
View File

@@ -0,0 +1,74 @@
import * as plugins from './plugins.js';
const execFile = plugins.util.promisify(plugins.childProcess.execFile);
export class NftNotRootError extends Error {
constructor() {
super('Not running as root — nftables commands require root privileges');
this.name = 'NftNotRootError';
}
}
export class NftCommandError extends Error {
public readonly command: string;
public readonly stderr: string;
constructor(command: string, stderr: string) {
super(`nft command failed: ${command}${stderr}`);
this.name = 'NftCommandError';
this.command = command;
this.stderr = stderr;
}
}
/**
* Low-level executor for nft CLI commands.
*/
export class NftExecutor {
private dryRun: boolean;
constructor(options?: { dryRun?: boolean }) {
this.dryRun = options?.dryRun ?? false;
}
/** Check if running as root (euid 0). */
public isRoot(): boolean {
return process.getuid?.() === 0;
}
/**
* Execute a single nft command.
* The command may or may not start with "nft " — the prefix is stripped if present.
* Returns stdout on success.
*/
public async exec(command: string): Promise<string> {
if (this.dryRun) {
return '';
}
// Strip "nft " prefix if present
const args = command.startsWith('nft ') ? command.slice(4) : command;
const { stdout } = await execFile('nft', args.split(/\s+/));
return stdout;
}
/**
* Execute multiple nft commands sequentially.
* By default stops on first failure. Set continueOnError to keep going.
*/
public async execBatch(
commands: string[],
options?: { continueOnError?: boolean }
): Promise<void> {
for (const cmd of commands) {
try {
await this.exec(cmd);
} catch (err) {
if (!options?.continueOnError) {
throw err;
}
}
}
}
}

148
ts/nft.manager.firewall.ts Normal file
View File

@@ -0,0 +1,148 @@
import type { SmartNftables } from './nft.manager.js';
import type { INftFirewallRule, INftIPSetConfig } from './nft.types.js';
import {
buildFirewallRule,
buildIPSetCreate,
buildIPSetAddElements,
buildIPSetRemoveElements,
buildIPSetDelete,
buildIPSetMatchRule,
} from './nft.rulebuilder.firewall.js';
/**
* Manages firewall (filter) rules, IP sets, and convenience methods.
*/
export class FirewallManager {
constructor(private parent: SmartNftables) {}
/**
* Add a firewall rule (input/output/forward).
*/
public async addRule(groupId: string, rule: INftFirewallRule): Promise<void> {
await this.parent.ensureFilterChains();
const commands = buildFirewallRule(this.parent.tableName, this.parent.family, rule);
await this.parent.applyRuleGroup(`fw:${groupId}`, commands);
}
/**
* Remove a firewall rule group.
*/
public async removeRule(groupId: string): Promise<void> {
await this.parent.removeRuleGroup(`fw:${groupId}`);
}
/**
* Create a named IP set.
*/
public async createIPSet(config: INftIPSetConfig): Promise<void> {
await this.parent.ensureInitialized();
const commands = buildIPSetCreate(this.parent.tableName, this.parent.family, config);
await this.parent.applyRuleGroup(`ipset:${config.name}`, commands);
}
/**
* Add elements to an existing IP set.
*/
public async addToIPSet(setName: string, elements: string[]): Promise<void> {
const commands = buildIPSetAddElements(this.parent.tableName, this.parent.family, setName, elements);
if (commands.length > 0) {
await this.parent.executor.execBatch(commands);
}
}
/**
* Remove elements from an IP set.
*/
public async removeFromIPSet(setName: string, elements: string[]): Promise<void> {
const commands = buildIPSetRemoveElements(this.parent.tableName, this.parent.family, setName, elements);
if (commands.length > 0) {
await this.parent.executor.execBatch(commands);
}
}
/**
* Delete an IP set entirely.
*/
public async deleteIPSet(setName: string): Promise<void> {
const commands = buildIPSetDelete(this.parent.tableName, this.parent.family, setName);
await this.parent.executor.execBatch(commands);
await this.parent.removeRuleGroup(`ipset:${setName}`);
}
/**
* Convenience: block an IP or CIDR subnet.
*/
public async blockIP(ip: string, options?: { direction?: 'input' | 'forward' }): Promise<void> {
const direction = options?.direction ?? 'input';
const safeId = ip.replace(/[/.]/g, '_');
await this.addRule(`block-${safeId}`, {
direction,
action: 'drop',
sourceIP: ip,
});
}
/**
* Convenience: allow only specific IPs on a port.
* Adds accept rules for each IP, then a drop rule for everything else on that port.
*/
public async allowOnlyIPs(
groupId: string,
ips: string[],
port?: number,
protocol?: 'tcp' | 'udp' | 'both',
): Promise<void> {
await this.parent.ensureFilterChains();
const commands: string[] = [];
for (const ip of ips) {
const acceptCmds = buildFirewallRule(this.parent.tableName, this.parent.family, {
direction: 'input',
action: 'accept',
sourceIP: ip,
destPort: port,
protocol,
});
commands.push(...acceptCmds);
}
// Drop everything else on that port
const dropCmds = buildFirewallRule(this.parent.tableName, this.parent.family, {
direction: 'input',
action: 'drop',
destPort: port,
protocol,
});
commands.push(...dropCmds);
await this.parent.applyRuleGroup(`fw:allowonly:${groupId}`, commands);
}
/**
* Convenience: enable stateful connection tracking.
* Allows established+related, drops invalid.
*/
public async enableStatefulTracking(chain?: 'input' | 'forward' | 'output'): Promise<void> {
await this.parent.ensureFilterChains();
const direction = chain ?? 'input';
const commands: string[] = [];
// Allow established and related connections
const allowCmds = buildFirewallRule(this.parent.tableName, this.parent.family, {
direction,
action: 'accept',
ctStates: ['established', 'related'],
});
commands.push(...allowCmds);
// Drop invalid connections
const dropCmds = buildFirewallRule(this.parent.tableName, this.parent.family, {
direction,
action: 'drop',
ctStates: ['invalid'],
});
commands.push(...dropCmds);
await this.parent.applyRuleGroup(`fw:stateful:${direction}`, commands);
}
}

76
ts/nft.manager.nat.ts Normal file
View File

@@ -0,0 +1,76 @@
import type { SmartNftables } from './nft.manager.js';
import type { INftDnatRule, INftSnatRule, INftMasqueradeRule, TNftProtocol } from './nft.types.js';
import { buildDnatRules, buildSnatRule, buildMasqueradeRule } from './nft.rulebuilder.nat.js';
/**
* Manages NAT (DNAT/SNAT/masquerade) rules.
*/
export class NatManager {
constructor(private parent: SmartNftables) {}
/**
* Add a port forwarding rule (DNAT + optional masquerade).
*/
public async addPortForwarding(groupId: string, rule: INftDnatRule): Promise<void> {
const commands = buildDnatRules(this.parent.tableName, this.parent.family, rule);
await this.parent.applyRuleGroup(`nat:${groupId}`, commands);
}
/**
* Remove a previously added port forwarding group.
*/
public async removePortForwarding(groupId: string): Promise<void> {
await this.parent.removeRuleGroup(`nat:${groupId}`);
}
/**
* Add SNAT (source NAT) rule.
*/
public async addSnat(groupId: string, rule: INftSnatRule): Promise<void> {
const commands = buildSnatRule(this.parent.tableName, this.parent.family, rule);
await this.parent.applyRuleGroup(`nat:snat:${groupId}`, commands);
}
/**
* Add masquerade rule for outgoing traffic.
*/
public async addMasquerade(groupId: string, rule: INftMasqueradeRule): Promise<void> {
const commands = buildMasqueradeRule(this.parent.tableName, this.parent.family, rule);
await this.parent.applyRuleGroup(`nat:masq:${groupId}`, commands);
}
/**
* Add port forwarding for a range of ports.
* Maps sourceStart..sourceStart+count to targetStart..targetStart+count.
*/
public async addPortRange(
groupId: string,
sourceStart: number,
sourceEnd: number,
targetHost: string,
targetStart: number,
protocol?: TNftProtocol,
): Promise<void> {
const allCommands: string[] = [];
const count = sourceEnd - sourceStart;
for (let i = 0; i <= count; i++) {
const commands = buildDnatRules(this.parent.tableName, this.parent.family, {
sourcePort: sourceStart + i,
targetHost,
targetPort: targetStart + i,
protocol,
});
allCommands.push(...commands);
}
await this.parent.applyRuleGroup(`nat:range:${groupId}`, allCommands);
}
/**
* Remove a port range forwarding group.
*/
public async removePortRange(groupId: string): Promise<void> {
await this.parent.removeRuleGroup(`nat:range:${groupId}`);
}
}

View File

@@ -0,0 +1,43 @@
import type { SmartNftables } from './nft.manager.js';
import type { INftRateLimitRule, INftConnectionRateRule } from './nft.types.js';
import { buildRateLimitRule, buildConnectionRateRule } from './nft.rulebuilder.ratelimit.js';
/**
* Manages rate limiting rules using nft meters and limit expressions.
*/
export class RateLimitManager {
constructor(private parent: SmartNftables) {}
/**
* Add a rate limit rule (global or per-IP).
*/
public async addRateLimit(groupId: string, rule: INftRateLimitRule): Promise<void> {
await this.parent.ensureFilterChains();
const commands = buildRateLimitRule(this.parent.tableName, this.parent.family, rule);
await this.parent.applyRuleGroup(`ratelimit:${groupId}`, commands);
}
/**
* Remove a rate limit rule group.
*/
public async removeRateLimit(groupId: string): Promise<void> {
await this.parent.removeRuleGroup(`ratelimit:${groupId}`);
}
/**
* Add a new-connection rate limit rule.
* Limits the rate of new TCP/UDP connections (ct state new).
*/
public async addConnectionRateLimit(groupId: string, rule: INftConnectionRateRule): Promise<void> {
await this.parent.ensureFilterChains();
const commands = buildConnectionRateRule(this.parent.tableName, this.parent.family, rule);
await this.parent.applyRuleGroup(`connrate:${groupId}`, commands);
}
/**
* Remove a connection rate limit rule group.
*/
public async removeConnectionRateLimit(groupId: string): Promise<void> {
await this.parent.removeRuleGroup(`connrate:${groupId}`);
}
}

157
ts/nft.manager.ts Normal file
View File

@@ -0,0 +1,157 @@
import { NftExecutor } from './nft.executor.js';
import { buildTableSetup, buildFilterChains, buildTableCleanup } from './nft.rulebuilder.table.js';
import { NatManager } from './nft.manager.nat.js';
import { FirewallManager } from './nft.manager.firewall.js';
import { RateLimitManager } from './nft.manager.ratelimit.js';
import type { TNftFamily, INftRuleGroup, INftStatus, ISmartNftablesOptions } from './nft.types.js';
/**
* SmartNftables — high-level facade for managing nftables rules.
*
* Provides sub-managers for NAT, firewall, and rate limiting.
* All rules are tracked in logical groups and can be removed individually or cleaned up entirely.
*/
export class SmartNftables {
public readonly nat: NatManager;
public readonly firewall: FirewallManager;
public readonly rateLimit: RateLimitManager;
public readonly tableName: string;
public readonly family: TNftFamily;
public readonly executor: NftExecutor;
private initialized = false;
private hasFilterChains = false;
private warnedNonRoot = false;
private ruleGroups: Map<string, INftRuleGroup> = new Map();
constructor(options?: ISmartNftablesOptions) {
this.tableName = options?.tableName ?? 'smartnftables';
this.family = options?.family ?? 'ip';
this.executor = new NftExecutor({ dryRun: options?.dryRun });
this.nat = new NatManager(this);
this.firewall = new FirewallManager(this);
this.rateLimit = new RateLimitManager(this);
}
/**
* Initialize the nftables table and NAT chains. Idempotent.
*/
public async initialize(): Promise<void> {
if (this.initialized) return;
if (!this.executor.isRoot()) {
if (!this.warnedNonRoot) {
console.warn('smartnftables: not running as root. Rules are tracked but not applied to kernel.');
this.warnedNonRoot = true;
}
this.initialized = true;
return;
}
const commands = buildTableSetup(this.tableName, this.family);
await this.executor.execBatch(commands);
this.initialized = true;
}
/**
* Ensure filter chains (input/forward/output) are created.
* Called automatically when firewall or rate-limit rules are added.
*/
public async ensureFilterChains(): Promise<void> {
if (this.hasFilterChains) return;
await this.ensureInitialized();
if (this.executor.isRoot()) {
const commands = buildFilterChains(this.tableName, this.family);
await this.executor.execBatch(commands);
}
this.hasFilterChains = true;
}
/**
* Ensure the table is initialized before applying rules.
*/
public async ensureInitialized(): Promise<void> {
if (!this.initialized) {
await this.initialize();
}
}
/**
* Apply a group of nft commands and track them under the given ID.
*/
public async applyRuleGroup(groupId: string, commands: string[]): Promise<void> {
// Always track the group locally
this.ruleGroups.set(groupId, {
id: groupId,
commands,
createdAt: Date.now(),
});
if (!this.executor.isRoot()) {
if (!this.warnedNonRoot) {
console.warn('smartnftables: not running as root. Rules are tracked but not applied to kernel.');
this.warnedNonRoot = true;
}
return;
}
await this.ensureInitialized();
await this.executor.execBatch(commands);
}
/**
* Remove a tracked rule group. Removes from tracking.
* Note: full kernel cleanup requires cleanup() — individual rule removal
* would require handle-based tracking.
*/
public async removeRuleGroup(groupId: string): Promise<void> {
this.ruleGroups.delete(groupId);
}
/**
* Get a tracked rule group by ID.
*/
public getRuleGroup(groupId: string): INftRuleGroup | undefined {
return this.ruleGroups.get(groupId);
}
/**
* Delete the entire nftables table and clear all tracking.
*/
public async cleanup(): Promise<void> {
if (this.executor.isRoot() && this.initialized) {
const commands = buildTableCleanup(this.tableName, this.family);
await this.executor.execBatch(commands, { continueOnError: true });
}
this.ruleGroups.clear();
this.initialized = false;
this.hasFilterChains = false;
}
/**
* Get status report of the managed nftables state.
*/
public status(): INftStatus {
const groups: Record<string, { ruleCount: number; createdAt: number }> = {};
for (const [id, group] of this.ruleGroups) {
groups[id] = {
ruleCount: group.commands.length,
createdAt: group.createdAt,
};
}
return {
initialized: this.initialized,
tableName: this.tableName,
family: this.family,
isRoot: this.executor.isRoot(),
activeGroups: this.ruleGroups.size,
groups,
};
}
}

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

82
ts/nft.rulebuilder.nat.ts Normal file
View File

@@ -0,0 +1,82 @@
import type { TNftFamily, TNftProtocol, INftDnatRule, INftSnatRule, INftMasqueradeRule } from './nft.types.js';
/**
* Expand a protocol spec into concrete protocol strings.
*/
function expandProtocols(protocol?: TNftProtocol): string[] {
switch (protocol ?? 'tcp') {
case 'tcp': return ['tcp'];
case 'udp': return ['udp'];
case 'both': return ['tcp', 'udp'];
}
}
/**
* Build DNAT rules for port forwarding.
* Generates DNAT + optional masquerade for each protocol.
* Direct port of Rust build_dnat_rule.
*/
export function buildDnatRules(
tableName: string,
family: TNftFamily,
rule: INftDnatRule,
): string[] {
const protocols = expandProtocols(rule.protocol);
const commands: string[] = [];
for (const proto of protocols) {
// DNAT rule
commands.push(
`nft add rule ${family} ${tableName} prerouting ${proto} dport ${rule.sourcePort} dnat to ${rule.targetHost}:${rule.targetPort}`
);
// Masquerade (SNAT) unless preserveSourceIP is set
if (!rule.preserveSourceIP) {
commands.push(
`nft add rule ${family} ${tableName} postrouting ${proto} dport ${rule.targetPort} masquerade`
);
}
}
return commands;
}
/**
* Build an SNAT rule to rewrite source address.
*/
export function buildSnatRule(
tableName: string,
family: TNftFamily,
rule: INftSnatRule,
): string[] {
const protocols = expandProtocols(rule.protocol);
const commands: string[] = [];
for (const proto of protocols) {
commands.push(
`nft add rule ${family} ${tableName} postrouting ${proto} dport ${rule.targetPort} snat to ${rule.sourceAddress}`
);
}
return commands;
}
/**
* Build a masquerade rule for outgoing NAT.
*/
export function buildMasqueradeRule(
tableName: string,
family: TNftFamily,
rule: INftMasqueradeRule,
): string[] {
const protocols = expandProtocols(rule.protocol);
const commands: string[] = [];
for (const proto of protocols) {
commands.push(
`nft add rule ${family} ${tableName} postrouting ${proto} dport ${rule.targetPort} masquerade`
);
}
return commands;
}

View File

@@ -0,0 +1,89 @@
import type { TNftFamily, INftRateLimitRule, INftConnectionRateRule } from './nft.types.js';
/**
* Expand a protocol spec into concrete protocol strings.
*/
function expandProtocols(protocol?: 'tcp' | 'udp' | 'both'): string[] {
switch (protocol ?? 'tcp') {
case 'tcp': return ['tcp'];
case 'udp': return ['udp'];
case 'both': return ['tcp', 'udp'];
}
}
/**
* Build a rate limit rule.
* Packets exceeding the rate are subjected to the specified action (default: drop).
*/
export function buildRateLimitRule(
tableName: string,
family: TNftFamily,
rule: INftRateLimitRule,
): string[] {
const protocols = expandProtocols(rule.protocol);
const chain = rule.chain ?? 'input';
const action = rule.action ?? 'drop';
const commands: string[] = [];
for (const proto of protocols) {
const portMatch = rule.port != null ? ` ${proto} dport ${rule.port}` : '';
const burstClause = rule.burst != null ? ` burst ${rule.burst} packets` : '';
if (rule.perSourceIP) {
// Per-IP rate limiting using nft meters
const meterName = `meter_${proto}_${rule.port ?? 'all'}`;
commands.push(
`nft add rule ${family} ${tableName} ${chain}${portMatch} meter ${meterName} { ip saddr limit rate over ${rule.rate}${burstClause} } ${action}`
);
} else {
// Global rate limiting
commands.push(
`nft add rule ${family} ${tableName} ${chain}${portMatch} limit rate over ${rule.rate}${burstClause} ${action}`
);
}
}
return commands;
}
/**
* Build a per-IP rate limit rule using nft meters.
* Convenience wrapper around buildRateLimitRule with perSourceIP=true.
*/
export function buildPerIpRateLimitRule(
tableName: string,
family: TNftFamily,
rule: Omit<INftRateLimitRule, 'perSourceIP'>,
): string[] {
return buildRateLimitRule(tableName, family, { ...rule, perSourceIP: true });
}
/**
* Build a new-connection rate limit rule.
* Limits the rate of new connections (ct state new) on the given port.
*/
export function buildConnectionRateRule(
tableName: string,
family: TNftFamily,
rule: INftConnectionRateRule,
): string[] {
const protocols = expandProtocols(rule.protocol);
const commands: string[] = [];
for (const proto of protocols) {
const portMatch = rule.port != null ? ` ${proto} dport ${rule.port}` : '';
if (rule.perSourceIP) {
const meterName = `connrate_${proto}_${rule.port ?? 'all'}`;
commands.push(
`nft add rule ${family} ${tableName} input ct state new${portMatch} meter ${meterName} { ip saddr limit rate over ${rule.rate} } drop`
);
} else {
commands.push(
`nft add rule ${family} ${tableName} input ct state new${portMatch} limit rate over ${rule.rate} drop`
);
}
}
return commands;
}

View File

@@ -0,0 +1,30 @@
import type { TNftFamily } from './nft.types.js';
/**
* Build commands to create the nftables table and NAT chains (prerouting + postrouting).
*/
export function buildTableSetup(tableName: string, family: TNftFamily = 'ip'): string[] {
return [
`nft add table ${family} ${tableName}`,
`nft add chain ${family} ${tableName} prerouting { type nat hook prerouting priority 0 \\; }`,
`nft add chain ${family} ${tableName} postrouting { type nat hook postrouting priority 100 \\; }`,
];
}
/**
* Build commands to create filter chains (input, forward, output).
*/
export function buildFilterChains(tableName: string, family: TNftFamily = 'ip'): string[] {
return [
`nft add chain ${family} ${tableName} input { type filter hook input priority 0 \\; policy accept \\; }`,
`nft add chain ${family} ${tableName} forward { type filter hook forward priority 0 \\; policy accept \\; }`,
`nft add chain ${family} ${tableName} output { type filter hook output priority 0 \\; policy accept \\; }`,
];
}
/**
* Build command to delete the entire nftables table.
*/
export function buildTableCleanup(tableName: string, family: TNftFamily = 'ip'): string[] {
return [`nft delete table ${family} ${tableName}`];
}

102
ts/nft.types.ts Normal file
View File

@@ -0,0 +1,102 @@
// ─── Protocol & Family ────────────────────────────────────────────
export type TNftProtocol = 'tcp' | 'udp' | 'both';
export type TNftFamily = 'ip' | 'ip6' | 'inet';
export type TNftChainHook = 'prerouting' | 'postrouting' | 'input' | 'output' | 'forward';
export type TNftChainType = 'nat' | 'filter';
export type TNftPolicy = 'accept' | 'drop';
export type TFirewallAction = 'accept' | 'drop' | 'reject';
export type TCtState = 'new' | 'established' | 'related' | 'invalid';
// ─── NAT ──────────────────────────────────────────────────────────
export interface INftDnatRule {
sourcePort: number;
targetHost: string;
targetPort: number;
protocol?: TNftProtocol;
preserveSourceIP?: boolean;
}
export interface INftSnatRule {
sourceAddress: string;
targetPort: number;
protocol?: TNftProtocol;
}
export interface INftMasqueradeRule {
targetPort: number;
protocol?: TNftProtocol;
}
// ─── Rate Limiting ────────────────────────────────────────────────
export interface INftRateLimitRule {
/** Port to rate-limit on. If omitted, applies to all ports. */
port?: number;
protocol?: TNftProtocol;
/** Rate expression, e.g. "100/second", "10 mbytes/second" */
rate: string;
/** Burst allowance in packets or bytes */
burst?: number;
/** If true, track rate per source IP using nft meters */
perSourceIP?: boolean;
/** Action for packets exceeding rate. Default: 'drop' */
action?: TFirewallAction;
/** Chain to apply the rule to. Default: 'input' */
chain?: 'input' | 'forward' | 'prerouting';
}
export interface INftConnectionRateRule {
/** Port to limit new connections on */
port?: number;
protocol?: TNftProtocol;
/** New connection rate, e.g. "10/second" */
rate: string;
/** If true, track per source IP */
perSourceIP?: boolean;
}
// ─── Firewall ─────────────────────────────────────────────────────
export interface INftFirewallRule {
direction: 'input' | 'output' | 'forward';
action: TFirewallAction;
sourceIP?: string;
destIP?: string;
sourcePort?: number;
destPort?: number;
protocol?: TNftProtocol;
ctStates?: TCtState[];
comment?: string;
}
export interface INftIPSetConfig {
name: string;
type: 'ipv4_addr' | 'ipv6_addr' | 'inet_service';
elements?: string[];
comment?: string;
}
// ─── Rule Group (tracking unit) ───────────────────────────────────
export interface INftRuleGroup {
id: string;
commands: string[];
createdAt: number;
}
// ─── Manager Options ──────────────────────────────────────────────
export interface ISmartNftablesOptions {
/** nftables table name. Default: 'smartnftables' */
tableName?: string;
/** Address family. Default: 'ip' */
family?: TNftFamily;
/** If true, generate commands but never execute them */
dryRun?: boolean;
}
// ─── Status / Reporting ───────────────────────────────────────────
export interface INftStatus {
initialized: boolean;
tableName: string;
family: TNftFamily;
isRoot: boolean;
activeGroups: number;
groups: Record<string, { ruleCount: number; createdAt: number }>;
}

16
ts/plugins.ts Normal file
View File

@@ -0,0 +1,16 @@
// node native scope
import * as childProcess from 'node:child_process';
import * as util from 'node:util';
export {
childProcess,
util,
};
// @push.rocks scope
import * as smartlog from '@push.rocks/smartlog';
import * as smartpromise from '@push.rocks/smartpromise';
export {
smartlog,
smartpromise,
};