@push.rocks/smartnftables 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +11 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/nft.executor.d.ts +1 -0
- package/dist_ts/nft.executor.js +66 -4
- package/dist_ts/nft.rulebuilder.firewall.js +16 -31
- package/package.json +16 -15
- package/readme.md +4 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nft.executor.ts +71 -4
- package/ts/nft.rulebuilder.firewall.ts +16 -31
package/changelog.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-07-31 - 1.2.1
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- normalize command execution and firewall rule generation (nft)
|
|
8
|
+
- Preserve quoted values and escaped semicolons when executing nft commands.
|
|
9
|
+
- Default port-only firewall rules to TCP and use meta l4proto for protocol-only rules.
|
|
10
|
+
- Escape rule comments so generated nft commands remain parseable.
|
|
11
|
+
- Update tests, documentation, release configuration, and package tooling dependencies.
|
|
12
|
+
|
|
3
13
|
## 2026-04-26 - 1.2.0 - feat(firewall)
|
|
4
14
|
add IP set blocking convenience API with CIDR interval support and optional rule comments
|
|
5
15
|
|
|
@@ -22,4 +32,4 @@ no changes to commit
|
|
|
22
32
|
## 2026-03-26 - 1.0.0 - project
|
|
23
33
|
Initial release.
|
|
24
34
|
|
|
25
|
-
- Project initialized with first commit.
|
|
35
|
+
- Project initialized with first commit.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartnftables',
|
|
6
|
-
version: '1.2.
|
|
6
|
+
version: '1.2.1',
|
|
7
7
|
description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSwyQkFBMkI7SUFDakMsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLG1IQUFtSDtDQUNqSSxDQUFBIn0=
|
package/dist_ts/nft.executor.js
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
const execFile = plugins.util.promisify(plugins.childProcess.execFile);
|
|
3
|
+
export function normalizeNftCommand(command) {
|
|
4
|
+
let input = command.trim();
|
|
5
|
+
if (!input) {
|
|
6
|
+
throw new Error('Invalid nft command: command is empty');
|
|
7
|
+
}
|
|
8
|
+
if (input === 'nft') {
|
|
9
|
+
throw new Error('Invalid nft command: command is empty');
|
|
10
|
+
}
|
|
11
|
+
if (/^nft\s/.test(input)) {
|
|
12
|
+
input = input.slice(3).trimStart();
|
|
13
|
+
}
|
|
14
|
+
let normalized = '';
|
|
15
|
+
let quote;
|
|
16
|
+
for (let index = 0; index < input.length; index++) {
|
|
17
|
+
const character = input[index];
|
|
18
|
+
if (quote === 'single') {
|
|
19
|
+
normalized += character;
|
|
20
|
+
if (character === "'") {
|
|
21
|
+
quote = undefined;
|
|
22
|
+
}
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (quote === 'double') {
|
|
26
|
+
normalized += character;
|
|
27
|
+
if (character === '"') {
|
|
28
|
+
quote = undefined;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (character === '\\') {
|
|
32
|
+
const nextCharacter = input[index + 1];
|
|
33
|
+
if (nextCharacter === undefined) {
|
|
34
|
+
throw new Error('Invalid nft command: trailing escape in double-quoted string');
|
|
35
|
+
}
|
|
36
|
+
normalized += nextCharacter;
|
|
37
|
+
index++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (character === "'") {
|
|
43
|
+
quote = 'single';
|
|
44
|
+
}
|
|
45
|
+
else if (character === '"') {
|
|
46
|
+
quote = 'double';
|
|
47
|
+
}
|
|
48
|
+
else if (character === '\\') {
|
|
49
|
+
const nextCharacter = input[index + 1];
|
|
50
|
+
if (nextCharacter === undefined) {
|
|
51
|
+
throw new Error('Invalid nft command: trailing escape');
|
|
52
|
+
}
|
|
53
|
+
if (nextCharacter === ';') {
|
|
54
|
+
normalized += ';';
|
|
55
|
+
index++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
normalized += character;
|
|
60
|
+
}
|
|
61
|
+
if (quote) {
|
|
62
|
+
throw new Error(`Invalid nft command: unterminated ${quote}-quoted string`);
|
|
63
|
+
}
|
|
64
|
+
return normalized;
|
|
65
|
+
}
|
|
3
66
|
export class NftNotRootError extends Error {
|
|
4
67
|
constructor() {
|
|
5
68
|
super('Not running as root — nftables commands require root privileges');
|
|
@@ -31,12 +94,11 @@ export class NftExecutor {
|
|
|
31
94
|
* Returns stdout on success.
|
|
32
95
|
*/
|
|
33
96
|
async exec(command) {
|
|
97
|
+
const normalizedCommand = normalizeNftCommand(command);
|
|
34
98
|
if (this.dryRun) {
|
|
35
99
|
return '';
|
|
36
100
|
}
|
|
37
|
-
|
|
38
|
-
const args = command.startsWith('nft ') ? command.slice(4) : command;
|
|
39
|
-
const { stdout } = await execFile('nft', args.split(/\s+/));
|
|
101
|
+
const { stdout } = await execFile('nft', [normalizedCommand]);
|
|
40
102
|
return stdout;
|
|
41
103
|
}
|
|
42
104
|
/**
|
|
@@ -56,4 +118,4 @@ export class NftExecutor {
|
|
|
56
118
|
}
|
|
57
119
|
}
|
|
58
120
|
}
|
|
59
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
121
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibmZ0LmV4ZWN1dG9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvbmZ0LmV4ZWN1dG9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBRXhDLE1BQU0sUUFBUSxHQUFHLE9BQU8sQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxZQUFZLENBQUMsUUFBUSxDQUFDLENBQUM7QUFFdkUsTUFBTSxVQUFVLG1CQUFtQixDQUFDLE9BQWU7SUFDakQsSUFBSSxLQUFLLEdBQUcsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDO0lBQzNCLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUNYLE1BQU0sSUFBSSxLQUFLLENBQUMsdUNBQXVDLENBQUMsQ0FBQztJQUMzRCxDQUFDO0lBQ0QsSUFBSSxLQUFLLEtBQUssS0FBSyxFQUFFLENBQUM7UUFDcEIsTUFBTSxJQUFJLEtBQUssQ0FBQyx1Q0FBdUMsQ0FBQyxDQUFDO0lBQzNELENBQUM7SUFDRCxJQUFJLFFBQVEsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUN6QixLQUFLLEdBQUcsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLEVBQUUsQ0FBQztJQUNyQyxDQUFDO0lBRUQsSUFBSSxVQUFVLEdBQUcsRUFBRSxDQUFDO0lBQ3BCLElBQUksS0FBc0MsQ0FBQztJQUUzQyxLQUFLLElBQUksS0FBSyxHQUFHLENBQUMsRUFBRSxLQUFLLEdBQUcsS0FBSyxDQUFDLE1BQU0sRUFBRSxLQUFLLEVBQUUsRUFBRSxDQUFDO1FBQ2xELE1BQU0sU0FBUyxHQUFHLEtBQUssQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUUvQixJQUFJLEtBQUssS0FBSyxRQUFRLEVBQUUsQ0FBQztZQUN2QixVQUFVLElBQUksU0FBUyxDQUFDO1lBQ3hCLElBQUksU0FBUyxLQUFLLEdBQUcsRUFBRSxDQUFDO2dCQUN0QixLQUFLLEdBQUcsU0FBUyxDQUFDO1lBQ3BCLENBQUM7WUFDRCxTQUFTO1FBQ1gsQ0FBQztRQUVELElBQUksS0FBSyxLQUFLLFFBQVEsRUFBRSxDQUFDO1lBQ3ZCLFVBQVUsSUFBSSxTQUFTLENBQUM7WUFDeEIsSUFBSSxTQUFTLEtBQUssR0FBRyxFQUFFLENBQUM7Z0JBQ3RCLEtBQUssR0FBRyxTQUFTLENBQUM7Z0JBQ2xCLFNBQVM7WUFDWCxDQUFDO1lBQ0QsSUFBSSxTQUFTLEtBQUssSUFBSSxFQUFFLENBQUM7Z0JBQ3ZCLE1BQU0sYUFBYSxHQUFHLEtBQUssQ0FBQyxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUM7Z0JBQ3ZDLElBQUksYUFBYSxLQUFLLFNBQVMsRUFBRSxDQUFDO29CQUNoQyxNQUFNLElBQUksS0FBSyxDQUFDLDhEQUE4RCxDQUFDLENBQUM7Z0JBQ2xGLENBQUM7Z0JBQ0QsVUFBVSxJQUFJLGFBQWEsQ0FBQztnQkFDNUIsS0FBSyxFQUFFLENBQUM7Z0JBQ1IsU0FBUztZQUNYLENBQUM7WUFDRCxTQUFTO1FBQ1gsQ0FBQztRQUVELElBQUksU0FBUyxLQUFLLEdBQUcsRUFBRSxDQUFDO1lBQ3RCLEtBQUssR0FBRyxRQUFRLENBQUM7UUFDbkIsQ0FBQzthQUFNLElBQUksU0FBUyxLQUFLLEdBQUcsRUFBRSxDQUFDO1lBQzdCLEtBQUssR0FBRyxRQUFRLENBQUM7UUFDbkIsQ0FBQzthQUFNLElBQUksU0FBUyxLQUFLLElBQUksRUFBRSxDQUFDO1lBQzlCLE1BQU0sYUFBYSxHQUFHLEtBQUssQ0FBQyxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUM7WUFDdkMsSUFBSSxhQUFhLEtBQUssU0FBUyxFQUFFLENBQUM7Z0JBQ2hDLE1BQU0sSUFBSSxLQUFLLENBQUMsc0NBQXNDLENBQUMsQ0FBQztZQUMxRCxDQUFDO1lBQ0QsSUFBSSxhQUFhLEtBQUssR0FBRyxFQUFFLENBQUM7Z0JBQzFCLFVBQVUsSUFBSSxHQUFHLENBQUM7Z0JBQ2xCLEtBQUssRUFBRSxDQUFDO2dCQUNSLFNBQVM7WUFDWCxDQUFDO1FBQ0gsQ0FBQztRQUVELFVBQVUsSUFBSSxTQUFTLENBQUM7SUFDMUIsQ0FBQztJQUVELElBQUksS0FBSyxFQUFFLENBQUM7UUFDVixNQUFNLElBQUksS0FBSyxDQUFDLHFDQUFxQyxLQUFLLGdCQUFnQixDQUFDLENBQUM7SUFDOUUsQ0FBQztJQUNELE9BQU8sVUFBVSxDQUFDO0FBQ3BCLENBQUM7QUFFRCxNQUFNLE9BQU8sZUFBZ0IsU0FBUSxLQUFLO0lBQ3hDO1FBQ0UsS0FBSyxDQUFDLGlFQUFpRSxDQUFDLENBQUM7UUFDekUsSUFBSSxDQUFDLElBQUksR0FBRyxpQkFBaUIsQ0FBQztJQUNoQyxDQUFDO0NBQ0Y7QUFFRCxNQUFNLE9BQU8sZUFBZ0IsU0FBUSxLQUFLO0lBSXhDLFlBQVksT0FBZSxFQUFFLE1BQWM7UUFDekMsS0FBSyxDQUFDLHVCQUF1QixPQUFPLE1BQU0sTUFBTSxFQUFFLENBQUMsQ0FBQztRQUNwRCxJQUFJLENBQUMsSUFBSSxHQUFHLGlCQUFpQixDQUFDO1FBQzlCLElBQUksQ0FBQyxPQUFPLEdBQUcsT0FBTyxDQUFDO1FBQ3ZCLElBQUksQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFDO0lBQ3ZCLENBQUM7Q0FDRjtBQUVEOztHQUVHO0FBQ0gsTUFBTSxPQUFPLFdBQVc7SUFHdEIsWUFBWSxPQUE4QjtRQUN4QyxJQUFJLENBQUMsTUFBTSxHQUFHLE9BQU8sRUFBRSxNQUFNLElBQUksS0FBSyxDQUFDO0lBQ3pDLENBQUM7SUFFRCx5Q0FBeUM7SUFDbEMsTUFBTTtRQUNYLE9BQU8sT0FBTyxDQUFDLE1BQU0sRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQ2xDLENBQUM7SUFFRDs7OztPQUlHO0lBQ0ksS0FBSyxDQUFDLElBQUksQ0FBQyxPQUFlO1FBQy9CLE1BQU0saUJBQWlCLEdBQUcsbUJBQW1CLENBQUMsT0FBTyxDQUFDLENBQUM7UUFDdkQsSUFBSSxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUM7WUFDaEIsT0FBTyxFQUFFLENBQUM7UUFDWixDQUFDO1FBRUQsTUFBTSxFQUFFLE1BQU0sRUFBRSxHQUFHLE1BQU0sUUFBUSxDQUFDLEtBQUssRUFBRSxDQUFDLGlCQUFpQixDQUFDLENBQUMsQ0FBQztRQUM5RCxPQUFPLE1BQU0sQ0FBQztJQUNoQixDQUFDO0lBRUQ7OztPQUdHO0lBQ0ksS0FBSyxDQUFDLFNBQVMsQ0FDcEIsUUFBa0IsRUFDbEIsT0FBdUM7UUFFdkMsS0FBSyxNQUFNLEdBQUcsSUFBSSxRQUFRLEVBQUUsQ0FBQztZQUMzQixJQUFJLENBQUM7Z0JBQ0gsTUFBTSxJQUFJLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQ3ZCLENBQUM7WUFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO2dCQUNiLElBQUksQ0FBQyxPQUFPLEVBQUUsZUFBZSxFQUFFLENBQUM7b0JBQzlCLE1BQU0sR0FBRyxDQUFDO2dCQUNaLENBQUM7WUFDSCxDQUFDO1FBQ0gsQ0FBQztJQUNILENBQUM7Q0FDRiJ9
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
function escapeNftString(value) {
|
|
2
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
3
|
+
}
|
|
1
4
|
/**
|
|
2
5
|
* Build an nft firewall rule for input/output/forward chains.
|
|
3
6
|
*/
|
|
@@ -9,12 +12,15 @@ export function buildFirewallRule(tableName, family, rule) {
|
|
|
9
12
|
parts.push(`ct state { ${rule.ctStates.join(', ')} }`);
|
|
10
13
|
}
|
|
11
14
|
// Protocol and port matching
|
|
12
|
-
const
|
|
15
|
+
const hasPort = rule.sourcePort != null || rule.destPort != null;
|
|
16
|
+
const protocols = expandProtocols(rule.protocol ?? (hasPort ? 'tcp' : undefined));
|
|
17
|
+
const protocolVariants = protocols.length > 0 ? protocols : [undefined];
|
|
13
18
|
const commands = [];
|
|
14
|
-
for (const proto of
|
|
19
|
+
for (const proto of protocolVariants) {
|
|
15
20
|
const ruleParts = [...parts];
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
const portProtocol = proto ?? 'tcp';
|
|
22
|
+
if (proto && !hasPort) {
|
|
23
|
+
ruleParts.push(`meta l4proto ${proto}`);
|
|
18
24
|
}
|
|
19
25
|
if (rule.sourceIP) {
|
|
20
26
|
ruleParts.push(`ip saddr ${rule.sourceIP}`);
|
|
@@ -23,36 +29,15 @@ export function buildFirewallRule(tableName, family, rule) {
|
|
|
23
29
|
ruleParts.push(`ip daddr ${rule.destIP}`);
|
|
24
30
|
}
|
|
25
31
|
if (rule.sourcePort != null) {
|
|
26
|
-
ruleParts.push(`${
|
|
32
|
+
ruleParts.push(`${portProtocol} sport ${rule.sourcePort}`);
|
|
27
33
|
}
|
|
28
34
|
if (rule.destPort != null) {
|
|
29
|
-
|
|
30
|
-
if (!rule.protocol) {
|
|
31
|
-
ruleParts.push(`tcp dport ${rule.destPort}`);
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
ruleParts.push(`${proto} dport ${rule.destPort}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (rule.comment) {
|
|
38
|
-
ruleParts.push(`comment "${rule.comment}"`);
|
|
35
|
+
ruleParts.push(`${portProtocol} dport ${rule.destPort}`);
|
|
39
36
|
}
|
|
40
37
|
ruleParts.push(rule.action);
|
|
41
|
-
commands.push(ruleParts.join(' '));
|
|
42
|
-
}
|
|
43
|
-
// If no protocol expansion needed (no protocol-specific fields)
|
|
44
|
-
if (commands.length === 0) {
|
|
45
|
-
const ruleParts = [...parts];
|
|
46
|
-
if (rule.sourceIP) {
|
|
47
|
-
ruleParts.push(`ip saddr ${rule.sourceIP}`);
|
|
48
|
-
}
|
|
49
|
-
if (rule.destIP) {
|
|
50
|
-
ruleParts.push(`ip daddr ${rule.destIP}`);
|
|
51
|
-
}
|
|
52
38
|
if (rule.comment) {
|
|
53
|
-
ruleParts.push(`comment "${rule.comment}"`);
|
|
39
|
+
ruleParts.push(`comment "${escapeNftString(rule.comment)}"`);
|
|
54
40
|
}
|
|
55
|
-
ruleParts.push(rule.action);
|
|
56
41
|
commands.push(ruleParts.join(' '));
|
|
57
42
|
}
|
|
58
43
|
return commands;
|
|
@@ -103,9 +88,9 @@ export function buildIPSetDelete(tableName, family, setName) {
|
|
|
103
88
|
* Build a rule that matches against a named set.
|
|
104
89
|
*/
|
|
105
90
|
export function buildIPSetMatchRule(tableName, family, options) {
|
|
106
|
-
const comment = options.comment ? ` comment "${options.comment}"` : '';
|
|
91
|
+
const comment = options.comment ? ` comment "${escapeNftString(options.comment)}"` : '';
|
|
107
92
|
return [
|
|
108
|
-
`nft add rule ${family} ${tableName} ${options.direction} ip ${options.matchField} @${options.setName}
|
|
93
|
+
`nft add rule ${family} ${tableName} ${options.direction} ip ${options.matchField} @${options.setName} ${options.action}${comment}`
|
|
109
94
|
];
|
|
110
95
|
}
|
|
111
96
|
// ─── Internal helpers ─────────────────────────────────────────────
|
|
@@ -118,4 +103,4 @@ function expandProtocols(protocol) {
|
|
|
118
103
|
case 'both': return ['tcp', 'udp'];
|
|
119
104
|
}
|
|
120
105
|
}
|
|
121
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
106
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibmZ0LnJ1bGVidWlsZGVyLmZpcmV3YWxsLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvbmZ0LnJ1bGVidWlsZGVyLmZpcmV3YWxsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUVBLFNBQVMsZUFBZSxDQUFDLEtBQWE7SUFDcEMsT0FBTyxLQUFLLENBQUMsT0FBTyxDQUFDLEtBQUssRUFBRSxNQUFNLENBQUMsQ0FBQyxPQUFPLENBQUMsSUFBSSxFQUFFLEtBQUssQ0FBQyxDQUFDO0FBQzNELENBQUM7QUFFRDs7R0FFRztBQUNILE1BQU0sVUFBVSxpQkFBaUIsQ0FDL0IsU0FBaUIsRUFDakIsTUFBa0IsRUFDbEIsSUFBc0I7SUFFdEIsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQztJQUM3QixNQUFNLEtBQUssR0FBYSxDQUFDLGdCQUFnQixNQUFNLElBQUksU0FBUyxJQUFJLEtBQUssRUFBRSxDQUFDLENBQUM7SUFFekUsNkJBQTZCO0lBQzdCLElBQUksSUFBSSxDQUFDLFFBQVEsSUFBSSxJQUFJLENBQUMsUUFBUSxDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUM5QyxLQUFLLENBQUMsSUFBSSxDQUFDLGNBQWMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3pELENBQUM7SUFFRCw2QkFBNkI7SUFDN0IsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLFVBQVUsSUFBSSxJQUFJLElBQUksSUFBSSxDQUFDLFFBQVEsSUFBSSxJQUFJLENBQUM7SUFDakUsTUFBTSxTQUFTLEdBQUcsZUFBZSxDQUFDLElBQUksQ0FBQyxRQUFRLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQztJQUNsRixNQUFNLGdCQUFnQixHQUFHLFNBQVMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUM7SUFDeEUsTUFBTSxRQUFRLEdBQWEsRUFBRSxDQUFDO0lBRTlCLEtBQUssTUFBTSxLQUFLLElBQUksZ0JBQWdCLEVBQUUsQ0FBQztRQUNyQyxNQUFNLFNBQVMsR0FBRyxDQUFDLEdBQUcsS0FBSyxDQUFDLENBQUM7UUFDN0IsTUFBTSxZQUFZLEdBQUcsS0FBSyxJQUFJLEtBQUssQ0FBQztRQUVwQyxJQUFJLEtBQUssSUFBSSxDQUFDLE9BQU8sRUFBRSxDQUFDO1lBQ3RCLFNBQVMsQ0FBQyxJQUFJLENBQUMsZ0JBQWdCLEtBQUssRUFBRSxDQUFDLENBQUM7UUFDMUMsQ0FBQztRQUVELElBQUksSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO1lBQ2xCLFNBQVMsQ0FBQyxJQUFJLENBQUMsWUFBWSxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztRQUM5QyxDQUFDO1FBRUQsSUFBSSxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUM7WUFDaEIsU0FBUyxDQUFDLElBQUksQ0FBQyxZQUFZLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO1FBQzVDLENBQUM7UUFFRCxJQUFJLElBQUksQ0FBQyxVQUFVLElBQUksSUFBSSxFQUFFLENBQUM7WUFDNUIsU0FBUyxDQUFDLElBQUksQ0FBQyxHQUFHLFlBQVksVUFBVSxJQUFJLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQztRQUM3RCxDQUFDO1FBRUQsSUFBSSxJQUFJLENBQUMsUUFBUSxJQUFJLElBQUksRUFBRSxDQUFDO1lBQzFCLFNBQVMsQ0FBQyxJQUFJLENBQUMsR0FBRyxZQUFZLFVBQVUsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDLENBQUM7UUFDM0QsQ0FBQztRQUVELFNBQVMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQzVCLElBQUksSUFBSSxDQUFDLE9BQU8sRUFBRSxDQUFDO1lBQ2pCLFNBQVMsQ0FBQyxJQUFJLENBQUMsWUFBWSxlQUFlLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUMvRCxDQUFDO1FBQ0QsUUFBUSxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7SUFDckMsQ0FBQztJQUVELE9BQU8sUUFBUSxDQUFDO0FBQ2xCLENBQUM7QUFFRDs7R0FFRztBQUNILE1BQU0sVUFBVSxnQkFBZ0IsQ0FDOUIsU0FBaUIsRUFDakIsTUFBa0IsRUFDbEIsTUFBdUI7SUFFdkIsTUFBTSxRQUFRLEdBQWEsRUFBRSxDQUFDO0lBQzlCLE1BQU0sS0FBSyxHQUFHLE1BQU0sQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLHFCQUFxQixDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7SUFFM0QsaUJBQWlCO0lBQ2pCLFFBQVEsQ0FBQyxJQUFJLENBQ1gsZUFBZSxNQUFNLElBQUksU0FBUyxJQUFJLE1BQU0sQ0FBQyxJQUFJLFdBQVcsTUFBTSxDQUFDLElBQUksT0FBTyxLQUFLLElBQUksQ0FDeEYsQ0FBQztJQUVGLG1DQUFtQztJQUNuQyxJQUFJLE1BQU0sQ0FBQyxRQUFRLElBQUksTUFBTSxDQUFDLFFBQVEsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDbEQsUUFBUSxDQUFDLElBQUksQ0FDWCxtQkFBbUIsTUFBTSxJQUFJLFNBQVMsSUFBSSxNQUFNLENBQUMsSUFBSSxNQUFNLE1BQU0sQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQzFGLENBQUM7SUFDSixDQUFDO0lBRUQsT0FBTyxRQUFRLENBQUM7QUFDbEIsQ0FBQztBQUVEOztHQUVHO0FBQ0gsTUFBTSxVQUFVLHFCQUFxQixDQUNuQyxTQUFpQixFQUNqQixNQUFrQixFQUNsQixPQUFlLEVBQ2YsUUFBa0I7SUFFbEIsSUFBSSxRQUFRLENBQUMsTUFBTSxLQUFLLENBQUM7UUFBRSxPQUFPLEVBQUUsQ0FBQztJQUNyQyxPQUFPO1FBQ0wsbUJBQW1CLE1BQU0sSUFBSSxTQUFTLElBQUksT0FBTyxNQUFNLFFBQVEsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUk7S0FDL0UsQ0FBQztBQUNKLENBQUM7QUFFRDs7R0FFRztBQUNILE1BQU0sVUFBVSx3QkFBd0IsQ0FDdEMsU0FBaUIsRUFDakIsTUFBa0IsRUFDbEIsT0FBZSxFQUNmLFFBQWtCO0lBRWxCLElBQUksUUFBUSxDQUFDLE1BQU0sS0FBSyxDQUFDO1FBQUUsT0FBTyxFQUFFLENBQUM7SUFDckMsT0FBTztRQUNMLHNCQUFzQixNQUFNLElBQUksU0FBUyxJQUFJLE9BQU8sTUFBTSxRQUFRLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJO0tBQ2xGLENBQUM7QUFDSixDQUFDO0FBRUQ7O0dBRUc7QUFDSCxNQUFNLFVBQVUsZ0JBQWdCLENBQzlCLFNBQWlCLEVBQ2pCLE1BQWtCLEVBQ2xCLE9BQWU7SUFFZixPQUFPO1FBQ0wsa0JBQWtCLE1BQU0sSUFBSSxTQUFTLElBQUksT0FBTyxFQUFFO0tBQ25ELENBQUM7QUFDSixDQUFDO0FBRUQ7O0dBRUc7QUFDSCxNQUFNLFVBQVUsbUJBQW1CLENBQ2pDLFNBQWlCLEVBQ2pCLE1BQWtCLEVBQ2xCLE9BTUM7SUFFRCxNQUFNLE9BQU8sR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxhQUFhLGVBQWUsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQ3hGLE9BQU87UUFDTCxnQkFBZ0IsTUFBTSxJQUFJLFNBQVMsSUFBSSxPQUFPLENBQUMsU0FBUyxPQUFPLE9BQU8sQ0FBQyxVQUFVLEtBQUssT0FBTyxDQUFDLE9BQU8sSUFBSSxPQUFPLENBQUMsTUFBTSxHQUFHLE9BQU8sRUFBRTtLQUNwSSxDQUFDO0FBQ0osQ0FBQztBQUVELHFFQUFxRTtBQUVyRSxTQUFTLGVBQWUsQ0FBQyxRQUFpQztJQUN4RCxJQUFJLENBQUMsUUFBUTtRQUFFLE9BQU8sRUFBRSxDQUFDO0lBQ3pCLFFBQVEsUUFBUSxFQUFFLENBQUM7UUFDakIsS0FBSyxLQUFLLENBQUMsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDM0IsS0FBSyxLQUFLLENBQUMsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDM0IsS0FBSyxNQUFNLENBQUMsQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQ3JDLENBQUM7QUFDSCxDQUFDIn0=
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartnftables",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
@@ -8,22 +8,17 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"author": "Lossless GmbH",
|
|
10
10
|
"license": "MIT",
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "(tstest test/**/test*.ts --verbose --timeout 60)",
|
|
13
|
-
"build": "(tsbuild tsfolders)",
|
|
14
|
-
"format": "(gitzone format)",
|
|
15
|
-
"buildDocs": "tsdoc"
|
|
16
|
-
},
|
|
17
11
|
"devDependencies": {
|
|
18
|
-
"@git.zone/
|
|
19
|
-
"@git.zone/
|
|
20
|
-
"@git.zone/
|
|
21
|
-
"@
|
|
22
|
-
"
|
|
12
|
+
"@git.zone/cli": "^3.0.1",
|
|
13
|
+
"@git.zone/tsbuild": "^4.4.2",
|
|
14
|
+
"@git.zone/tsrun": "^2.0.6",
|
|
15
|
+
"@git.zone/tstest": "^3.6.7",
|
|
16
|
+
"@types/node": "26.1.1",
|
|
17
|
+
"typescript": "^7.0.2"
|
|
23
18
|
},
|
|
24
19
|
"dependencies": {
|
|
25
|
-
"@push.rocks/smartlog": "^3.2.
|
|
26
|
-
"@push.rocks/smartpromise": "^4.2.
|
|
20
|
+
"@push.rocks/smartlog": "^3.2.2",
|
|
21
|
+
"@push.rocks/smartpromise": "^4.2.5"
|
|
27
22
|
},
|
|
28
23
|
"files": [
|
|
29
24
|
"ts/**/*",
|
|
@@ -53,5 +48,11 @@
|
|
|
53
48
|
},
|
|
54
49
|
"bugs": {
|
|
55
50
|
"url": "https://code.foss.global/push.rocks/smartnftables/issues"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"test": "(tstest test/**/test*.ts --verbose --timeout 60)",
|
|
54
|
+
"build": "(tsbuild tsfolders)",
|
|
55
|
+
"format": "(gitzone format)",
|
|
56
|
+
"buildDocs": "tsdoc"
|
|
56
57
|
}
|
|
57
|
-
}
|
|
58
|
+
}
|
package/readme.md
CHANGED
|
@@ -146,6 +146,8 @@ await nft.firewall.addRule('allow-ssh', {
|
|
|
146
146
|
await nft.firewall.removeRule('allow-ssh');
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
+
When `sourcePort` or `destPort` is provided without `protocol`, TCP is used by default. Protocol-only rules match the specified layer-4 protocol.
|
|
150
|
+
|
|
149
151
|
#### Block an IP
|
|
150
152
|
|
|
151
153
|
```typescript
|
|
@@ -337,7 +339,7 @@ All interfaces and types are fully exported for use in your own code:
|
|
|
337
339
|
|
|
338
340
|
## License and Legal Information
|
|
339
341
|
|
|
340
|
-
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [
|
|
342
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [repository license file](./license).
|
|
341
343
|
|
|
342
344
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
343
345
|
|
|
@@ -349,7 +351,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
|
|
349
351
|
|
|
350
352
|
### Company Information
|
|
351
353
|
|
|
352
|
-
Task Venture Capital GmbH
|
|
354
|
+
Task Venture Capital GmbH<br>
|
|
353
355
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
354
356
|
|
|
355
357
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/nft.executor.ts
CHANGED
|
@@ -2,6 +2,75 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
|
|
3
3
|
const execFile = plugins.util.promisify(plugins.childProcess.execFile);
|
|
4
4
|
|
|
5
|
+
export function normalizeNftCommand(command: string): string {
|
|
6
|
+
let input = command.trim();
|
|
7
|
+
if (!input) {
|
|
8
|
+
throw new Error('Invalid nft command: command is empty');
|
|
9
|
+
}
|
|
10
|
+
if (input === 'nft') {
|
|
11
|
+
throw new Error('Invalid nft command: command is empty');
|
|
12
|
+
}
|
|
13
|
+
if (/^nft\s/.test(input)) {
|
|
14
|
+
input = input.slice(3).trimStart();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let normalized = '';
|
|
18
|
+
let quote: 'single' | 'double' | undefined;
|
|
19
|
+
|
|
20
|
+
for (let index = 0; index < input.length; index++) {
|
|
21
|
+
const character = input[index];
|
|
22
|
+
|
|
23
|
+
if (quote === 'single') {
|
|
24
|
+
normalized += character;
|
|
25
|
+
if (character === "'") {
|
|
26
|
+
quote = undefined;
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (quote === 'double') {
|
|
32
|
+
normalized += character;
|
|
33
|
+
if (character === '"') {
|
|
34
|
+
quote = undefined;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (character === '\\') {
|
|
38
|
+
const nextCharacter = input[index + 1];
|
|
39
|
+
if (nextCharacter === undefined) {
|
|
40
|
+
throw new Error('Invalid nft command: trailing escape in double-quoted string');
|
|
41
|
+
}
|
|
42
|
+
normalized += nextCharacter;
|
|
43
|
+
index++;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (character === "'") {
|
|
50
|
+
quote = 'single';
|
|
51
|
+
} else if (character === '"') {
|
|
52
|
+
quote = 'double';
|
|
53
|
+
} else if (character === '\\') {
|
|
54
|
+
const nextCharacter = input[index + 1];
|
|
55
|
+
if (nextCharacter === undefined) {
|
|
56
|
+
throw new Error('Invalid nft command: trailing escape');
|
|
57
|
+
}
|
|
58
|
+
if (nextCharacter === ';') {
|
|
59
|
+
normalized += ';';
|
|
60
|
+
index++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
normalized += character;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (quote) {
|
|
69
|
+
throw new Error(`Invalid nft command: unterminated ${quote}-quoted string`);
|
|
70
|
+
}
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
|
|
5
74
|
export class NftNotRootError extends Error {
|
|
6
75
|
constructor() {
|
|
7
76
|
super('Not running as root — nftables commands require root privileges');
|
|
@@ -42,14 +111,12 @@ export class NftExecutor {
|
|
|
42
111
|
* Returns stdout on success.
|
|
43
112
|
*/
|
|
44
113
|
public async exec(command: string): Promise<string> {
|
|
114
|
+
const normalizedCommand = normalizeNftCommand(command);
|
|
45
115
|
if (this.dryRun) {
|
|
46
116
|
return '';
|
|
47
117
|
}
|
|
48
118
|
|
|
49
|
-
|
|
50
|
-
const args = command.startsWith('nft ') ? command.slice(4) : command;
|
|
51
|
-
|
|
52
|
-
const { stdout } = await execFile('nft', args.split(/\s+/));
|
|
119
|
+
const { stdout } = await execFile('nft', [normalizedCommand]);
|
|
53
120
|
return stdout;
|
|
54
121
|
}
|
|
55
122
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { TNftFamily, INftFirewallRule, INftIPSetConfig } from './nft.types.js';
|
|
2
2
|
|
|
3
|
+
function escapeNftString(value: string): string {
|
|
4
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
5
|
+
}
|
|
6
|
+
|
|
3
7
|
/**
|
|
4
8
|
* Build an nft firewall rule for input/output/forward chains.
|
|
5
9
|
*/
|
|
@@ -17,14 +21,17 @@ export function buildFirewallRule(
|
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
// Protocol and port matching
|
|
20
|
-
const
|
|
24
|
+
const hasPort = rule.sourcePort != null || rule.destPort != null;
|
|
25
|
+
const protocols = expandProtocols(rule.protocol ?? (hasPort ? 'tcp' : undefined));
|
|
26
|
+
const protocolVariants = protocols.length > 0 ? protocols : [undefined];
|
|
21
27
|
const commands: string[] = [];
|
|
22
28
|
|
|
23
|
-
for (const proto of
|
|
29
|
+
for (const proto of protocolVariants) {
|
|
24
30
|
const ruleParts = [...parts];
|
|
31
|
+
const portProtocol = proto ?? 'tcp';
|
|
25
32
|
|
|
26
|
-
if (
|
|
27
|
-
ruleParts.push(proto);
|
|
33
|
+
if (proto && !hasPort) {
|
|
34
|
+
ruleParts.push(`meta l4proto ${proto}`);
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
if (rule.sourceIP) {
|
|
@@ -36,39 +43,17 @@ export function buildFirewallRule(
|
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
if (rule.sourcePort != null) {
|
|
39
|
-
ruleParts.push(`${
|
|
46
|
+
ruleParts.push(`${portProtocol} sport ${rule.sourcePort}`);
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
if (rule.destPort != null) {
|
|
43
|
-
|
|
44
|
-
if (!rule.protocol) {
|
|
45
|
-
ruleParts.push(`tcp dport ${rule.destPort}`);
|
|
46
|
-
} else {
|
|
47
|
-
ruleParts.push(`${proto} dport ${rule.destPort}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (rule.comment) {
|
|
52
|
-
ruleParts.push(`comment "${rule.comment}"`);
|
|
50
|
+
ruleParts.push(`${portProtocol} dport ${rule.destPort}`);
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
ruleParts.push(rule.action);
|
|
56
|
-
commands.push(ruleParts.join(' '));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// If no protocol expansion needed (no protocol-specific fields)
|
|
60
|
-
if (commands.length === 0) {
|
|
61
|
-
const ruleParts = [...parts];
|
|
62
|
-
if (rule.sourceIP) {
|
|
63
|
-
ruleParts.push(`ip saddr ${rule.sourceIP}`);
|
|
64
|
-
}
|
|
65
|
-
if (rule.destIP) {
|
|
66
|
-
ruleParts.push(`ip daddr ${rule.destIP}`);
|
|
67
|
-
}
|
|
68
54
|
if (rule.comment) {
|
|
69
|
-
ruleParts.push(`comment "${rule.comment}"`);
|
|
55
|
+
ruleParts.push(`comment "${escapeNftString(rule.comment)}"`);
|
|
70
56
|
}
|
|
71
|
-
ruleParts.push(rule.action);
|
|
72
57
|
commands.push(ruleParts.join(' '));
|
|
73
58
|
}
|
|
74
59
|
|
|
@@ -158,9 +143,9 @@ export function buildIPSetMatchRule(
|
|
|
158
143
|
comment?: string;
|
|
159
144
|
},
|
|
160
145
|
): string[] {
|
|
161
|
-
const comment = options.comment ? ` comment "${options.comment}"` : '';
|
|
146
|
+
const comment = options.comment ? ` comment "${escapeNftString(options.comment)}"` : '';
|
|
162
147
|
return [
|
|
163
|
-
`nft add rule ${family} ${tableName} ${options.direction} ip ${options.matchField} @${options.setName}
|
|
148
|
+
`nft add rule ${family} ${tableName} ${options.direction} ip ${options.matchField} @${options.setName} ${options.action}${comment}`
|
|
164
149
|
];
|
|
165
150
|
}
|
|
166
151
|
|