Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11ddbc3edf | |||
| 6e7c0d90d8 |
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-26 - 1.2.0 - feat(firewall)
|
||||||
|
add IP set blocking convenience API with CIDR interval support and optional rule comments
|
||||||
|
|
||||||
|
- introduces firewall.blockIPSet() to create a set, populate it with IPs or CIDR ranges, and apply a single drop rule
|
||||||
|
- adds interval set support so CIDR entries can be stored in nftables sets
|
||||||
|
- supports optional comments on generated IP set match rules
|
||||||
|
- adds forced cleanup support and allows table existence checks without requiring prior initialization
|
||||||
|
|
||||||
## 2026-03-30 - 1.1.0 - feat(nft)
|
## 2026-03-30 - 1.1.0 - feat(nft)
|
||||||
add source IP filtering for DNAT rules and expose table existence checks
|
add source IP filtering for DNAT rules and expose table existence checks
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartnftables",
|
"name": "@push.rocks/smartnftables",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.",
|
"description": "A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
|
|||||||
@@ -137,4 +137,16 @@ tap.test('should build IP set match rule', async () => {
|
|||||||
expect(cmds[0]).toInclude('drop');
|
expect(cmds[0]).toInclude('drop');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tap.test('should build IP set match rule with comment', async () => {
|
||||||
|
const cmds = buildIPSetMatchRule('mytable', 'ip', {
|
||||||
|
setName: 'blocklist',
|
||||||
|
direction: 'input',
|
||||||
|
matchField: 'saddr',
|
||||||
|
action: 'drop',
|
||||||
|
comment: 'managed blocklist',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(cmds[0]).toInclude('comment "managed blocklist"');
|
||||||
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
||||||
|
|||||||
@@ -97,6 +97,24 @@ tap.test('should handle blockIP convenience method', async () => {
|
|||||||
await nft.cleanup();
|
await nft.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tap.test('should handle blockIPSet convenience method', async () => {
|
||||||
|
const nft = new SmartNftables({ tableName: 'test' });
|
||||||
|
await nft.initialize();
|
||||||
|
|
||||||
|
await nft.firewall.blockIPSet('bad-actors', {
|
||||||
|
ips: ['1.2.3.4', '5.6.0.0/16'],
|
||||||
|
setName: 'blocked_ipv4',
|
||||||
|
comment: 'test blocklist',
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = nft.status();
|
||||||
|
const group = status.groups['fw:blockset:bad-actors'];
|
||||||
|
expect(group).toBeDefined();
|
||||||
|
expect(group.ruleCount).toEqual(3); // create set, add elements, match rule
|
||||||
|
|
||||||
|
await nft.cleanup({ force: true });
|
||||||
|
});
|
||||||
|
|
||||||
tap.test('should handle stateful tracking convenience', async () => {
|
tap.test('should handle stateful tracking convenience', async () => {
|
||||||
const nft = new SmartNftables({ tableName: 'test' });
|
const nft = new SmartNftables({ tableName: 'test' });
|
||||||
await nft.initialize();
|
await nft.initialize();
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartnftables',
|
name: '@push.rocks/smartnftables',
|
||||||
version: '1.1.0',
|
version: '1.2.0',
|
||||||
description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
|
description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ export type {
|
|||||||
INftRateLimitRule,
|
INftRateLimitRule,
|
||||||
INftConnectionRateRule,
|
INftConnectionRateRule,
|
||||||
INftFirewallRule,
|
INftFirewallRule,
|
||||||
|
INftIPSetBlockOptions,
|
||||||
INftIPSetConfig,
|
INftIPSetConfig,
|
||||||
|
INftCleanupOptions,
|
||||||
INftRuleGroup,
|
INftRuleGroup,
|
||||||
ISmartNftablesOptions,
|
ISmartNftablesOptions,
|
||||||
INftStatus,
|
INftStatus,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { SmartNftables } from './nft.manager.js';
|
import type { SmartNftables } from './nft.manager.js';
|
||||||
import type { INftFirewallRule, INftIPSetConfig } from './nft.types.js';
|
import type { INftFirewallRule, INftIPSetBlockOptions, INftIPSetConfig } from './nft.types.js';
|
||||||
import {
|
import {
|
||||||
buildFirewallRule,
|
buildFirewallRule,
|
||||||
buildIPSetCreate,
|
buildIPSetCreate,
|
||||||
@@ -82,6 +82,41 @@ export class FirewallManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience: block many IPs or CIDR subnets using one nftables set and
|
||||||
|
* one matching drop rule. This is substantially cheaper than one rule per IP.
|
||||||
|
*/
|
||||||
|
public async blockIPSet(groupId: string, options: INftIPSetBlockOptions): Promise<void> {
|
||||||
|
const ips = [...new Set(options.ips)].filter(Boolean).sort();
|
||||||
|
if (ips.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.parent.ensureFilterChains();
|
||||||
|
|
||||||
|
const setName = options.setName ?? `${groupId.replace(/[^a-zA-Z0-9_]/g, '_')}_set`;
|
||||||
|
const setType = options.type ?? (this.parent.family === 'ip6' ? 'ipv6_addr' : 'ipv4_addr');
|
||||||
|
const needsIntervalSet = ips.some((ip) => ip.includes('/'));
|
||||||
|
const direction = options.direction ?? 'input';
|
||||||
|
const commands = [
|
||||||
|
...buildIPSetCreate(this.parent.tableName, this.parent.family, {
|
||||||
|
name: setName,
|
||||||
|
type: setType,
|
||||||
|
elements: ips,
|
||||||
|
interval: needsIntervalSet,
|
||||||
|
}),
|
||||||
|
...buildIPSetMatchRule(this.parent.tableName, this.parent.family, {
|
||||||
|
setName,
|
||||||
|
direction,
|
||||||
|
matchField: 'saddr',
|
||||||
|
action: 'drop',
|
||||||
|
comment: options.comment,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
await this.parent.applyRuleGroup(`fw:blockset:${groupId}`, commands);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience: allow only specific IPs on a port.
|
* Convenience: allow only specific IPs on a port.
|
||||||
* Adds accept rules for each IP, then a drop rule for everything else on that port.
|
* Adds accept rules for each IP, then a drop rule for everything else on that port.
|
||||||
|
|||||||
+4
-4
@@ -3,7 +3,7 @@ import { buildTableSetup, buildFilterChains, buildTableCleanup } from './nft.rul
|
|||||||
import { NatManager } from './nft.manager.nat.js';
|
import { NatManager } from './nft.manager.nat.js';
|
||||||
import { FirewallManager } from './nft.manager.firewall.js';
|
import { FirewallManager } from './nft.manager.firewall.js';
|
||||||
import { RateLimitManager } from './nft.manager.ratelimit.js';
|
import { RateLimitManager } from './nft.manager.ratelimit.js';
|
||||||
import type { TNftFamily, INftRuleGroup, INftStatus, ISmartNftablesOptions } from './nft.types.js';
|
import type { TNftFamily, INftCleanupOptions, INftRuleGroup, INftStatus, ISmartNftablesOptions } from './nft.types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SmartNftables — high-level facade for managing nftables rules.
|
* SmartNftables — high-level facade for managing nftables rules.
|
||||||
@@ -122,8 +122,8 @@ export class SmartNftables {
|
|||||||
/**
|
/**
|
||||||
* Delete the entire nftables table and clear all tracking.
|
* Delete the entire nftables table and clear all tracking.
|
||||||
*/
|
*/
|
||||||
public async cleanup(): Promise<void> {
|
public async cleanup(options: INftCleanupOptions = {}): Promise<void> {
|
||||||
if (this.executor.isRoot() && this.initialized) {
|
if (this.executor.isRoot() && (this.initialized || options.force)) {
|
||||||
const commands = buildTableCleanup(this.tableName, this.family);
|
const commands = buildTableCleanup(this.tableName, this.family);
|
||||||
await this.executor.execBatch(commands, { continueOnError: true });
|
await this.executor.execBatch(commands, { continueOnError: true });
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ export class SmartNftables {
|
|||||||
* Returns false if not root, not initialized, or the table was removed externally.
|
* Returns false if not root, not initialized, or the table was removed externally.
|
||||||
*/
|
*/
|
||||||
public async tableExists(): Promise<boolean> {
|
public async tableExists(): Promise<boolean> {
|
||||||
if (!this.executor.isRoot() || !this.initialized) {
|
if (!this.executor.isRoot()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -84,10 +84,11 @@ export function buildIPSetCreate(
|
|||||||
config: INftIPSetConfig,
|
config: INftIPSetConfig,
|
||||||
): string[] {
|
): string[] {
|
||||||
const commands: string[] = [];
|
const commands: string[] = [];
|
||||||
|
const flags = config.interval ? ' flags interval \\;' : '';
|
||||||
|
|
||||||
// Create the set
|
// Create the set
|
||||||
commands.push(
|
commands.push(
|
||||||
`nft add set ${family} ${tableName} ${config.name} { type ${config.type} \\; }`
|
`nft add set ${family} ${tableName} ${config.name} { type ${config.type} \\;${flags} }`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add initial elements if provided
|
// Add initial elements if provided
|
||||||
@@ -154,10 +155,12 @@ export function buildIPSetMatchRule(
|
|||||||
direction: 'input' | 'output' | 'forward';
|
direction: 'input' | 'output' | 'forward';
|
||||||
matchField: 'saddr' | 'daddr';
|
matchField: 'saddr' | 'daddr';
|
||||||
action: 'accept' | 'drop' | 'reject';
|
action: 'accept' | 'drop' | 'reject';
|
||||||
|
comment?: string;
|
||||||
},
|
},
|
||||||
): string[] {
|
): string[] {
|
||||||
|
const comment = options.comment ? ` comment "${options.comment}"` : '';
|
||||||
return [
|
return [
|
||||||
`nft add rule ${family} ${tableName} ${options.direction} ip ${options.matchField} @${options.setName} ${options.action}`
|
`nft add rule ${family} ${tableName} ${options.direction} ip ${options.matchField} @${options.setName}${comment} ${options.action}`
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,29 @@ export interface INftIPSetConfig {
|
|||||||
name: string;
|
name: string;
|
||||||
type: 'ipv4_addr' | 'ipv6_addr' | 'inet_service';
|
type: 'ipv4_addr' | 'ipv6_addr' | 'inet_service';
|
||||||
elements?: string[];
|
elements?: string[];
|
||||||
|
/** Enable interval sets so CIDR/range elements can be stored. */
|
||||||
|
interval?: boolean;
|
||||||
comment?: string;
|
comment?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface INftIPSetBlockOptions {
|
||||||
|
/** Name of the nftables set to create and match against. */
|
||||||
|
setName?: string;
|
||||||
|
/** IPs or CIDR ranges to add to the set. */
|
||||||
|
ips: string[];
|
||||||
|
/** Chain to apply the block rule to. Default: input. */
|
||||||
|
direction?: 'input' | 'forward';
|
||||||
|
/** Set type. Defaults to ipv4_addr for the default ip family. */
|
||||||
|
type?: 'ipv4_addr' | 'ipv6_addr';
|
||||||
|
/** Optional rule comment. */
|
||||||
|
comment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INftCleanupOptions {
|
||||||
|
/** Delete the table even when this process did not initialize it. */
|
||||||
|
force?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Rule Group (tracking unit) ───────────────────────────────────
|
// ─── Rule Group (tracking unit) ───────────────────────────────────
|
||||||
export interface INftRuleGroup {
|
export interface INftRuleGroup {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user