158 lines
4.7 KiB
TypeScript
158 lines
4.7 KiB
TypeScript
|
|
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,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|