@push.rocks/smartnftables 1.0.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.
Files changed (43) hide show
  1. package/changelog.md +10 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/index.d.ts +10 -0
  5. package/dist_ts/index.js +14 -0
  6. package/dist_ts/nft.executor.d.ts +32 -0
  7. package/dist_ts/nft.executor.js +59 -0
  8. package/dist_ts/nft.manager.d.ts +59 -0
  9. package/dist_ts/nft.manager.firewall.d.ts +49 -0
  10. package/dist_ts/nft.manager.firewall.js +121 -0
  11. package/dist_ts/nft.manager.js +132 -0
  12. package/dist_ts/nft.manager.nat.d.ts +34 -0
  13. package/dist_ts/nft.manager.nat.js +61 -0
  14. package/dist_ts/nft.manager.ratelimit.d.ts +26 -0
  15. package/dist_ts/nft.manager.ratelimit.js +39 -0
  16. package/dist_ts/nft.rulebuilder.firewall.d.ts +30 -0
  17. package/dist_ts/nft.rulebuilder.firewall.js +119 -0
  18. package/dist_ts/nft.rulebuilder.nat.d.ts +15 -0
  19. package/dist_ts/nft.rulebuilder.nat.js +51 -0
  20. package/dist_ts/nft.rulebuilder.ratelimit.d.ts +16 -0
  21. package/dist_ts/nft.rulebuilder.ratelimit.js +61 -0
  22. package/dist_ts/nft.rulebuilder.table.d.ts +13 -0
  23. package/dist_ts/nft.rulebuilder.table.js +27 -0
  24. package/dist_ts/nft.types.d.ts +88 -0
  25. package/dist_ts/nft.types.js +2 -0
  26. package/dist_ts/plugins.d.ts +6 -0
  27. package/dist_ts/plugins.js +9 -0
  28. package/license +21 -0
  29. package/package.json +57 -0
  30. package/readme.md +357 -0
  31. package/ts/00_commitinfo_data.ts +8 -0
  32. package/ts/index.ts +59 -0
  33. package/ts/nft.executor.ts +74 -0
  34. package/ts/nft.manager.firewall.ts +148 -0
  35. package/ts/nft.manager.nat.ts +76 -0
  36. package/ts/nft.manager.ratelimit.ts +43 -0
  37. package/ts/nft.manager.ts +157 -0
  38. package/ts/nft.rulebuilder.firewall.ts +173 -0
  39. package/ts/nft.rulebuilder.nat.ts +82 -0
  40. package/ts/nft.rulebuilder.ratelimit.ts +89 -0
  41. package/ts/nft.rulebuilder.table.ts +30 -0
  42. package/ts/nft.types.ts +102 -0
  43. package/ts/plugins.ts +16 -0
@@ -0,0 +1,76 @@
1
+ import type { SmartNftables } from './nft.manager.js';
2
+ import type { INftDnatRule, INftSnatRule, INftMasqueradeRule, TNftProtocol } from './nft.types.js';
3
+ import { buildDnatRules, buildSnatRule, buildMasqueradeRule } from './nft.rulebuilder.nat.js';
4
+
5
+ /**
6
+ * Manages NAT (DNAT/SNAT/masquerade) rules.
7
+ */
8
+ export class NatManager {
9
+ constructor(private parent: SmartNftables) {}
10
+
11
+ /**
12
+ * Add a port forwarding rule (DNAT + optional masquerade).
13
+ */
14
+ public async addPortForwarding(groupId: string, rule: INftDnatRule): Promise<void> {
15
+ const commands = buildDnatRules(this.parent.tableName, this.parent.family, rule);
16
+ await this.parent.applyRuleGroup(`nat:${groupId}`, commands);
17
+ }
18
+
19
+ /**
20
+ * Remove a previously added port forwarding group.
21
+ */
22
+ public async removePortForwarding(groupId: string): Promise<void> {
23
+ await this.parent.removeRuleGroup(`nat:${groupId}`);
24
+ }
25
+
26
+ /**
27
+ * Add SNAT (source NAT) rule.
28
+ */
29
+ public async addSnat(groupId: string, rule: INftSnatRule): Promise<void> {
30
+ const commands = buildSnatRule(this.parent.tableName, this.parent.family, rule);
31
+ await this.parent.applyRuleGroup(`nat:snat:${groupId}`, commands);
32
+ }
33
+
34
+ /**
35
+ * Add masquerade rule for outgoing traffic.
36
+ */
37
+ public async addMasquerade(groupId: string, rule: INftMasqueradeRule): Promise<void> {
38
+ const commands = buildMasqueradeRule(this.parent.tableName, this.parent.family, rule);
39
+ await this.parent.applyRuleGroup(`nat:masq:${groupId}`, commands);
40
+ }
41
+
42
+ /**
43
+ * Add port forwarding for a range of ports.
44
+ * Maps sourceStart..sourceStart+count to targetStart..targetStart+count.
45
+ */
46
+ public async addPortRange(
47
+ groupId: string,
48
+ sourceStart: number,
49
+ sourceEnd: number,
50
+ targetHost: string,
51
+ targetStart: number,
52
+ protocol?: TNftProtocol,
53
+ ): Promise<void> {
54
+ const allCommands: string[] = [];
55
+ const count = sourceEnd - sourceStart;
56
+
57
+ for (let i = 0; i <= count; i++) {
58
+ const commands = buildDnatRules(this.parent.tableName, this.parent.family, {
59
+ sourcePort: sourceStart + i,
60
+ targetHost,
61
+ targetPort: targetStart + i,
62
+ protocol,
63
+ });
64
+ allCommands.push(...commands);
65
+ }
66
+
67
+ await this.parent.applyRuleGroup(`nat:range:${groupId}`, allCommands);
68
+ }
69
+
70
+ /**
71
+ * Remove a port range forwarding group.
72
+ */
73
+ public async removePortRange(groupId: string): Promise<void> {
74
+ await this.parent.removeRuleGroup(`nat:range:${groupId}`);
75
+ }
76
+ }
@@ -0,0 +1,43 @@
1
+ import type { SmartNftables } from './nft.manager.js';
2
+ import type { INftRateLimitRule, INftConnectionRateRule } from './nft.types.js';
3
+ import { buildRateLimitRule, buildConnectionRateRule } from './nft.rulebuilder.ratelimit.js';
4
+
5
+ /**
6
+ * Manages rate limiting rules using nft meters and limit expressions.
7
+ */
8
+ export class RateLimitManager {
9
+ constructor(private parent: SmartNftables) {}
10
+
11
+ /**
12
+ * Add a rate limit rule (global or per-IP).
13
+ */
14
+ public async addRateLimit(groupId: string, rule: INftRateLimitRule): Promise<void> {
15
+ await this.parent.ensureFilterChains();
16
+ const commands = buildRateLimitRule(this.parent.tableName, this.parent.family, rule);
17
+ await this.parent.applyRuleGroup(`ratelimit:${groupId}`, commands);
18
+ }
19
+
20
+ /**
21
+ * Remove a rate limit rule group.
22
+ */
23
+ public async removeRateLimit(groupId: string): Promise<void> {
24
+ await this.parent.removeRuleGroup(`ratelimit:${groupId}`);
25
+ }
26
+
27
+ /**
28
+ * Add a new-connection rate limit rule.
29
+ * Limits the rate of new TCP/UDP connections (ct state new).
30
+ */
31
+ public async addConnectionRateLimit(groupId: string, rule: INftConnectionRateRule): Promise<void> {
32
+ await this.parent.ensureFilterChains();
33
+ const commands = buildConnectionRateRule(this.parent.tableName, this.parent.family, rule);
34
+ await this.parent.applyRuleGroup(`connrate:${groupId}`, commands);
35
+ }
36
+
37
+ /**
38
+ * Remove a connection rate limit rule group.
39
+ */
40
+ public async removeConnectionRateLimit(groupId: string): Promise<void> {
41
+ await this.parent.removeRuleGroup(`connrate:${groupId}`);
42
+ }
43
+ }
@@ -0,0 +1,157 @@
1
+ import { NftExecutor } from './nft.executor.js';
2
+ import { buildTableSetup, buildFilterChains, buildTableCleanup } from './nft.rulebuilder.table.js';
3
+ import { NatManager } from './nft.manager.nat.js';
4
+ import { FirewallManager } from './nft.manager.firewall.js';
5
+ import { RateLimitManager } from './nft.manager.ratelimit.js';
6
+ import type { TNftFamily, INftRuleGroup, INftStatus, ISmartNftablesOptions } from './nft.types.js';
7
+
8
+ /**
9
+ * SmartNftables — high-level facade for managing nftables rules.
10
+ *
11
+ * Provides sub-managers for NAT, firewall, and rate limiting.
12
+ * All rules are tracked in logical groups and can be removed individually or cleaned up entirely.
13
+ */
14
+ export class SmartNftables {
15
+ public readonly nat: NatManager;
16
+ public readonly firewall: FirewallManager;
17
+ public readonly rateLimit: RateLimitManager;
18
+
19
+ public readonly tableName: string;
20
+ public readonly family: TNftFamily;
21
+ public readonly executor: NftExecutor;
22
+
23
+ private initialized = false;
24
+ private hasFilterChains = false;
25
+ private warnedNonRoot = false;
26
+ private ruleGroups: Map<string, INftRuleGroup> = new Map();
27
+
28
+ constructor(options?: ISmartNftablesOptions) {
29
+ this.tableName = options?.tableName ?? 'smartnftables';
30
+ this.family = options?.family ?? 'ip';
31
+ this.executor = new NftExecutor({ dryRun: options?.dryRun });
32
+
33
+ this.nat = new NatManager(this);
34
+ this.firewall = new FirewallManager(this);
35
+ this.rateLimit = new RateLimitManager(this);
36
+ }
37
+
38
+ /**
39
+ * Initialize the nftables table and NAT chains. Idempotent.
40
+ */
41
+ public async initialize(): Promise<void> {
42
+ if (this.initialized) return;
43
+
44
+ if (!this.executor.isRoot()) {
45
+ if (!this.warnedNonRoot) {
46
+ console.warn('smartnftables: not running as root. Rules are tracked but not applied to kernel.');
47
+ this.warnedNonRoot = true;
48
+ }
49
+ this.initialized = true;
50
+ return;
51
+ }
52
+
53
+ const commands = buildTableSetup(this.tableName, this.family);
54
+ await this.executor.execBatch(commands);
55
+ this.initialized = true;
56
+ }
57
+
58
+ /**
59
+ * Ensure filter chains (input/forward/output) are created.
60
+ * Called automatically when firewall or rate-limit rules are added.
61
+ */
62
+ public async ensureFilterChains(): Promise<void> {
63
+ if (this.hasFilterChains) return;
64
+ await this.ensureInitialized();
65
+
66
+ if (this.executor.isRoot()) {
67
+ const commands = buildFilterChains(this.tableName, this.family);
68
+ await this.executor.execBatch(commands);
69
+ }
70
+
71
+ this.hasFilterChains = true;
72
+ }
73
+
74
+ /**
75
+ * Ensure the table is initialized before applying rules.
76
+ */
77
+ public async ensureInitialized(): Promise<void> {
78
+ if (!this.initialized) {
79
+ await this.initialize();
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Apply a group of nft commands and track them under the given ID.
85
+ */
86
+ public async applyRuleGroup(groupId: string, commands: string[]): Promise<void> {
87
+ // Always track the group locally
88
+ this.ruleGroups.set(groupId, {
89
+ id: groupId,
90
+ commands,
91
+ createdAt: Date.now(),
92
+ });
93
+
94
+ if (!this.executor.isRoot()) {
95
+ if (!this.warnedNonRoot) {
96
+ console.warn('smartnftables: not running as root. Rules are tracked but not applied to kernel.');
97
+ this.warnedNonRoot = true;
98
+ }
99
+ return;
100
+ }
101
+
102
+ await this.ensureInitialized();
103
+ await this.executor.execBatch(commands);
104
+ }
105
+
106
+ /**
107
+ * Remove a tracked rule group. Removes from tracking.
108
+ * Note: full kernel cleanup requires cleanup() — individual rule removal
109
+ * would require handle-based tracking.
110
+ */
111
+ public async removeRuleGroup(groupId: string): Promise<void> {
112
+ this.ruleGroups.delete(groupId);
113
+ }
114
+
115
+ /**
116
+ * Get a tracked rule group by ID.
117
+ */
118
+ public getRuleGroup(groupId: string): INftRuleGroup | undefined {
119
+ return this.ruleGroups.get(groupId);
120
+ }
121
+
122
+ /**
123
+ * Delete the entire nftables table and clear all tracking.
124
+ */
125
+ public async cleanup(): Promise<void> {
126
+ if (this.executor.isRoot() && this.initialized) {
127
+ const commands = buildTableCleanup(this.tableName, this.family);
128
+ await this.executor.execBatch(commands, { continueOnError: true });
129
+ }
130
+
131
+ this.ruleGroups.clear();
132
+ this.initialized = false;
133
+ this.hasFilterChains = false;
134
+ }
135
+
136
+ /**
137
+ * Get status report of the managed nftables state.
138
+ */
139
+ public status(): INftStatus {
140
+ const groups: Record<string, { ruleCount: number; createdAt: number }> = {};
141
+ for (const [id, group] of this.ruleGroups) {
142
+ groups[id] = {
143
+ ruleCount: group.commands.length,
144
+ createdAt: group.createdAt,
145
+ };
146
+ }
147
+
148
+ return {
149
+ initialized: this.initialized,
150
+ tableName: this.tableName,
151
+ family: this.family,
152
+ isRoot: this.executor.isRoot(),
153
+ activeGroups: this.ruleGroups.size,
154
+ groups,
155
+ };
156
+ }
157
+ }
@@ -0,0 +1,173 @@
1
+ import type { TNftFamily, INftFirewallRule, INftIPSetConfig } from './nft.types.js';
2
+
3
+ /**
4
+ * Build an nft firewall rule for input/output/forward chains.
5
+ */
6
+ export function buildFirewallRule(
7
+ tableName: string,
8
+ family: TNftFamily,
9
+ rule: INftFirewallRule,
10
+ ): string[] {
11
+ const chain = rule.direction;
12
+ const parts: string[] = [`nft add rule ${family} ${tableName} ${chain}`];
13
+
14
+ // Connection tracking states
15
+ if (rule.ctStates && rule.ctStates.length > 0) {
16
+ parts.push(`ct state { ${rule.ctStates.join(', ')} }`);
17
+ }
18
+
19
+ // Protocol and port matching
20
+ const protocols = expandProtocols(rule.protocol);
21
+ const commands: string[] = [];
22
+
23
+ for (const proto of protocols) {
24
+ const ruleParts = [...parts];
25
+
26
+ if (rule.protocol) {
27
+ ruleParts.push(proto);
28
+ }
29
+
30
+ if (rule.sourceIP) {
31
+ ruleParts.push(`ip saddr ${rule.sourceIP}`);
32
+ }
33
+
34
+ if (rule.destIP) {
35
+ ruleParts.push(`ip daddr ${rule.destIP}`);
36
+ }
37
+
38
+ if (rule.sourcePort != null) {
39
+ ruleParts.push(`${proto} sport ${rule.sourcePort}`);
40
+ }
41
+
42
+ if (rule.destPort != null) {
43
+ // If protocol wasn't explicitly set but we have a port, we need the protocol
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}"`);
53
+ }
54
+
55
+ 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
+ if (rule.comment) {
69
+ ruleParts.push(`comment "${rule.comment}"`);
70
+ }
71
+ ruleParts.push(rule.action);
72
+ commands.push(ruleParts.join(' '));
73
+ }
74
+
75
+ return commands;
76
+ }
77
+
78
+ /**
79
+ * Build commands to create an nft named set (IP set).
80
+ */
81
+ export function buildIPSetCreate(
82
+ tableName: string,
83
+ family: TNftFamily,
84
+ config: INftIPSetConfig,
85
+ ): string[] {
86
+ const commands: string[] = [];
87
+
88
+ // Create the set
89
+ commands.push(
90
+ `nft add set ${family} ${tableName} ${config.name} { type ${config.type} \\; }`
91
+ );
92
+
93
+ // Add initial elements if provided
94
+ if (config.elements && config.elements.length > 0) {
95
+ commands.push(
96
+ `nft add element ${family} ${tableName} ${config.name} { ${config.elements.join(', ')} }`
97
+ );
98
+ }
99
+
100
+ return commands;
101
+ }
102
+
103
+ /**
104
+ * Build command to add elements to an existing set.
105
+ */
106
+ export function buildIPSetAddElements(
107
+ tableName: string,
108
+ family: TNftFamily,
109
+ setName: string,
110
+ elements: string[],
111
+ ): string[] {
112
+ if (elements.length === 0) return [];
113
+ return [
114
+ `nft add element ${family} ${tableName} ${setName} { ${elements.join(', ')} }`
115
+ ];
116
+ }
117
+
118
+ /**
119
+ * Build command to remove elements from a set.
120
+ */
121
+ export function buildIPSetRemoveElements(
122
+ tableName: string,
123
+ family: TNftFamily,
124
+ setName: string,
125
+ elements: string[],
126
+ ): string[] {
127
+ if (elements.length === 0) return [];
128
+ return [
129
+ `nft delete element ${family} ${tableName} ${setName} { ${elements.join(', ')} }`
130
+ ];
131
+ }
132
+
133
+ /**
134
+ * Build command to delete an entire set.
135
+ */
136
+ export function buildIPSetDelete(
137
+ tableName: string,
138
+ family: TNftFamily,
139
+ setName: string,
140
+ ): string[] {
141
+ return [
142
+ `nft delete set ${family} ${tableName} ${setName}`
143
+ ];
144
+ }
145
+
146
+ /**
147
+ * Build a rule that matches against a named set.
148
+ */
149
+ export function buildIPSetMatchRule(
150
+ tableName: string,
151
+ family: TNftFamily,
152
+ options: {
153
+ setName: string;
154
+ direction: 'input' | 'output' | 'forward';
155
+ matchField: 'saddr' | 'daddr';
156
+ action: 'accept' | 'drop' | 'reject';
157
+ },
158
+ ): string[] {
159
+ return [
160
+ `nft add rule ${family} ${tableName} ${options.direction} ip ${options.matchField} @${options.setName} ${options.action}`
161
+ ];
162
+ }
163
+
164
+ // ─── Internal helpers ─────────────────────────────────────────────
165
+
166
+ function expandProtocols(protocol?: 'tcp' | 'udp' | 'both'): string[] {
167
+ if (!protocol) return [];
168
+ switch (protocol) {
169
+ case 'tcp': return ['tcp'];
170
+ case 'udp': return ['udp'];
171
+ case 'both': return ['tcp', 'udp'];
172
+ }
173
+ }
@@ -0,0 +1,82 @@
1
+ import type { TNftFamily, TNftProtocol, INftDnatRule, INftSnatRule, INftMasqueradeRule } from './nft.types.js';
2
+
3
+ /**
4
+ * Expand a protocol spec into concrete protocol strings.
5
+ */
6
+ function expandProtocols(protocol?: TNftProtocol): string[] {
7
+ switch (protocol ?? 'tcp') {
8
+ case 'tcp': return ['tcp'];
9
+ case 'udp': return ['udp'];
10
+ case 'both': return ['tcp', 'udp'];
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Build DNAT rules for port forwarding.
16
+ * Generates DNAT + optional masquerade for each protocol.
17
+ * Direct port of Rust build_dnat_rule.
18
+ */
19
+ export function buildDnatRules(
20
+ tableName: string,
21
+ family: TNftFamily,
22
+ rule: INftDnatRule,
23
+ ): string[] {
24
+ const protocols = expandProtocols(rule.protocol);
25
+ const commands: string[] = [];
26
+
27
+ for (const proto of protocols) {
28
+ // DNAT rule
29
+ commands.push(
30
+ `nft add rule ${family} ${tableName} prerouting ${proto} dport ${rule.sourcePort} dnat to ${rule.targetHost}:${rule.targetPort}`
31
+ );
32
+
33
+ // Masquerade (SNAT) unless preserveSourceIP is set
34
+ if (!rule.preserveSourceIP) {
35
+ commands.push(
36
+ `nft add rule ${family} ${tableName} postrouting ${proto} dport ${rule.targetPort} masquerade`
37
+ );
38
+ }
39
+ }
40
+
41
+ return commands;
42
+ }
43
+
44
+ /**
45
+ * Build an SNAT rule to rewrite source address.
46
+ */
47
+ export function buildSnatRule(
48
+ tableName: string,
49
+ family: TNftFamily,
50
+ rule: INftSnatRule,
51
+ ): string[] {
52
+ const protocols = expandProtocols(rule.protocol);
53
+ const commands: string[] = [];
54
+
55
+ for (const proto of protocols) {
56
+ commands.push(
57
+ `nft add rule ${family} ${tableName} postrouting ${proto} dport ${rule.targetPort} snat to ${rule.sourceAddress}`
58
+ );
59
+ }
60
+
61
+ return commands;
62
+ }
63
+
64
+ /**
65
+ * Build a masquerade rule for outgoing NAT.
66
+ */
67
+ export function buildMasqueradeRule(
68
+ tableName: string,
69
+ family: TNftFamily,
70
+ rule: INftMasqueradeRule,
71
+ ): string[] {
72
+ const protocols = expandProtocols(rule.protocol);
73
+ const commands: string[] = [];
74
+
75
+ for (const proto of protocols) {
76
+ commands.push(
77
+ `nft add rule ${family} ${tableName} postrouting ${proto} dport ${rule.targetPort} masquerade`
78
+ );
79
+ }
80
+
81
+ return commands;
82
+ }
@@ -0,0 +1,89 @@
1
+ import type { TNftFamily, INftRateLimitRule, INftConnectionRateRule } from './nft.types.js';
2
+
3
+ /**
4
+ * Expand a protocol spec into concrete protocol strings.
5
+ */
6
+ function expandProtocols(protocol?: 'tcp' | 'udp' | 'both'): string[] {
7
+ switch (protocol ?? 'tcp') {
8
+ case 'tcp': return ['tcp'];
9
+ case 'udp': return ['udp'];
10
+ case 'both': return ['tcp', 'udp'];
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Build a rate limit rule.
16
+ * Packets exceeding the rate are subjected to the specified action (default: drop).
17
+ */
18
+ export function buildRateLimitRule(
19
+ tableName: string,
20
+ family: TNftFamily,
21
+ rule: INftRateLimitRule,
22
+ ): string[] {
23
+ const protocols = expandProtocols(rule.protocol);
24
+ const chain = rule.chain ?? 'input';
25
+ const action = rule.action ?? 'drop';
26
+ const commands: string[] = [];
27
+
28
+ for (const proto of protocols) {
29
+ const portMatch = rule.port != null ? ` ${proto} dport ${rule.port}` : '';
30
+ const burstClause = rule.burst != null ? ` burst ${rule.burst} packets` : '';
31
+
32
+ if (rule.perSourceIP) {
33
+ // Per-IP rate limiting using nft meters
34
+ const meterName = `meter_${proto}_${rule.port ?? 'all'}`;
35
+ commands.push(
36
+ `nft add rule ${family} ${tableName} ${chain}${portMatch} meter ${meterName} { ip saddr limit rate over ${rule.rate}${burstClause} } ${action}`
37
+ );
38
+ } else {
39
+ // Global rate limiting
40
+ commands.push(
41
+ `nft add rule ${family} ${tableName} ${chain}${portMatch} limit rate over ${rule.rate}${burstClause} ${action}`
42
+ );
43
+ }
44
+ }
45
+
46
+ return commands;
47
+ }
48
+
49
+ /**
50
+ * Build a per-IP rate limit rule using nft meters.
51
+ * Convenience wrapper around buildRateLimitRule with perSourceIP=true.
52
+ */
53
+ export function buildPerIpRateLimitRule(
54
+ tableName: string,
55
+ family: TNftFamily,
56
+ rule: Omit<INftRateLimitRule, 'perSourceIP'>,
57
+ ): string[] {
58
+ return buildRateLimitRule(tableName, family, { ...rule, perSourceIP: true });
59
+ }
60
+
61
+ /**
62
+ * Build a new-connection rate limit rule.
63
+ * Limits the rate of new connections (ct state new) on the given port.
64
+ */
65
+ export function buildConnectionRateRule(
66
+ tableName: string,
67
+ family: TNftFamily,
68
+ rule: INftConnectionRateRule,
69
+ ): string[] {
70
+ const protocols = expandProtocols(rule.protocol);
71
+ const commands: string[] = [];
72
+
73
+ for (const proto of protocols) {
74
+ const portMatch = rule.port != null ? ` ${proto} dport ${rule.port}` : '';
75
+
76
+ if (rule.perSourceIP) {
77
+ const meterName = `connrate_${proto}_${rule.port ?? 'all'}`;
78
+ commands.push(
79
+ `nft add rule ${family} ${tableName} input ct state new${portMatch} meter ${meterName} { ip saddr limit rate over ${rule.rate} } drop`
80
+ );
81
+ } else {
82
+ commands.push(
83
+ `nft add rule ${family} ${tableName} input ct state new${portMatch} limit rate over ${rule.rate} drop`
84
+ );
85
+ }
86
+ }
87
+
88
+ return commands;
89
+ }
@@ -0,0 +1,30 @@
1
+ import type { TNftFamily } from './nft.types.js';
2
+
3
+ /**
4
+ * Build commands to create the nftables table and NAT chains (prerouting + postrouting).
5
+ */
6
+ export function buildTableSetup(tableName: string, family: TNftFamily = 'ip'): string[] {
7
+ return [
8
+ `nft add table ${family} ${tableName}`,
9
+ `nft add chain ${family} ${tableName} prerouting { type nat hook prerouting priority 0 \\; }`,
10
+ `nft add chain ${family} ${tableName} postrouting { type nat hook postrouting priority 100 \\; }`,
11
+ ];
12
+ }
13
+
14
+ /**
15
+ * Build commands to create filter chains (input, forward, output).
16
+ */
17
+ export function buildFilterChains(tableName: string, family: TNftFamily = 'ip'): string[] {
18
+ return [
19
+ `nft add chain ${family} ${tableName} input { type filter hook input priority 0 \\; policy accept \\; }`,
20
+ `nft add chain ${family} ${tableName} forward { type filter hook forward priority 0 \\; policy accept \\; }`,
21
+ `nft add chain ${family} ${tableName} output { type filter hook output priority 0 \\; policy accept \\; }`,
22
+ ];
23
+ }
24
+
25
+ /**
26
+ * Build command to delete the entire nftables table.
27
+ */
28
+ export function buildTableCleanup(tableName: string, family: TNftFamily = 'ip'): string[] {
29
+ return [`nft delete table ${family} ${tableName}`];
30
+ }