@push.rocks/smartnftables 1.2.0 → 1.3.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 (91) hide show
  1. package/changelog.md +38 -1
  2. package/dist_rust/smartnftables_linux_amd64_musl +0 -0
  3. package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +14 -0
  4. package/dist_rust/smartnftables_linux_arm64_musl +0 -0
  5. package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +14 -0
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/classes.managednftables.d.ts +21 -0
  8. package/dist_ts/classes.managednftables.js +176 -0
  9. package/dist_ts/index.d.ts +2 -0
  10. package/dist_ts/index.js +2 -1
  11. package/dist_ts/managed.types.d.ts +99 -0
  12. package/dist_ts/managed.types.js +2 -0
  13. package/dist_ts/nft.executor.d.ts +1 -0
  14. package/dist_ts/nft.executor.js +66 -4
  15. package/dist_ts/nft.rulebuilder.firewall.js +16 -31
  16. package/dist_ts/plugins.d.ts +5 -2
  17. package/dist_ts/plugins.js +6 -3
  18. package/native-notices/crates/block-buffer-0.10.4/license-apache +201 -0
  19. package/native-notices/crates/block-buffer-0.10.4/license-mit +25 -0
  20. package/native-notices/crates/cfg-if-1.0.4/license-apache +201 -0
  21. package/native-notices/crates/cfg-if-1.0.4/license-mit +25 -0
  22. package/native-notices/crates/cpufeatures-0.2.17/license-apache +201 -0
  23. package/native-notices/crates/cpufeatures-0.2.17/license-mit +25 -0
  24. package/native-notices/crates/crypto-common-0.1.7/license-apache +201 -0
  25. package/native-notices/crates/crypto-common-0.1.7/license-mit +25 -0
  26. package/native-notices/crates/digest-0.10.7/license-apache +201 -0
  27. package/native-notices/crates/digest-0.10.7/license-mit +25 -0
  28. package/native-notices/crates/generic-array-0.14.7/license +21 -0
  29. package/native-notices/crates/itoa-1.0.18/license-apache +176 -0
  30. package/native-notices/crates/itoa-1.0.18/license-mit +23 -0
  31. package/native-notices/crates/libc-0.2.189/license-apache +176 -0
  32. package/native-notices/crates/libc-0.2.189/license-mit +25 -0
  33. package/native-notices/crates/memchr-2.8.3/copying +3 -0
  34. package/native-notices/crates/memchr-2.8.3/license-mit +21 -0
  35. package/native-notices/crates/proc-macro2-1.0.107/license-apache +176 -0
  36. package/native-notices/crates/proc-macro2-1.0.107/license-mit +23 -0
  37. package/native-notices/crates/quote-1.0.47/license-apache +176 -0
  38. package/native-notices/crates/quote-1.0.47/license-mit +23 -0
  39. package/native-notices/crates/serde-1.0.229/license-apache +176 -0
  40. package/native-notices/crates/serde-1.0.229/license-mit +23 -0
  41. package/native-notices/crates/serde_core-1.0.229/license-apache +176 -0
  42. package/native-notices/crates/serde_core-1.0.229/license-mit +23 -0
  43. package/native-notices/crates/serde_derive-1.0.229/license-apache +176 -0
  44. package/native-notices/crates/serde_derive-1.0.229/license-mit +23 -0
  45. package/native-notices/crates/serde_json-1.0.151/license-apache +176 -0
  46. package/native-notices/crates/serde_json-1.0.151/license-mit +23 -0
  47. package/native-notices/crates/sha2-0.10.9/license-apache +201 -0
  48. package/native-notices/crates/sha2-0.10.9/license-mit +27 -0
  49. package/native-notices/crates/syn-3.0.5/license-apache +176 -0
  50. package/native-notices/crates/syn-3.0.5/license-mit +23 -0
  51. package/native-notices/crates/typenum-1.20.1/license +1 -0
  52. package/native-notices/crates/typenum-1.20.1/license-apache +201 -0
  53. package/native-notices/crates/typenum-1.20.1/license-mit +21 -0
  54. package/native-notices/crates/unicode-ident-1.0.24/license-apache +176 -0
  55. package/native-notices/crates/unicode-ident-1.0.24/license-mit +23 -0
  56. package/native-notices/crates/unicode-ident-1.0.24/license-unicode +39 -0
  57. package/native-notices/crates/version_check-0.9.5/license-apache +201 -0
  58. package/native-notices/crates/version_check-0.9.5/license-mit +19 -0
  59. package/native-notices/crates/zmij-1.0.23/license-mit +23 -0
  60. package/native-notices/manifest.json +520 -0
  61. package/native-notices/readme.md +36 -0
  62. package/native-notices/rust-standard-library/addr2line-0.25.1-license-apache +201 -0
  63. package/native-notices/rust-standard-library/addr2line-0.25.1-license-mit +25 -0
  64. package/native-notices/rust-standard-library/cargo.lock +522 -0
  65. package/native-notices/rust-standard-library/compiler-builtins-license.txt +275 -0
  66. package/native-notices/rust-standard-library/dependency-notices.html +9072 -0
  67. package/native-notices/rust-standard-library/license-apache +176 -0
  68. package/native-notices/rust-standard-library/license-mit +25 -0
  69. package/native-notices/rust-standard-library/llvm-libunwind-license.txt +311 -0
  70. package/native-notices/rust-standard-library/musl-1.2.5-copyright.txt +193 -0
  71. package/native-notices/rust-standard-library/unicode-license.txt +39 -0
  72. package/package.json +32 -16
  73. package/readme.md +140 -3
  74. package/rust/.cargo/config.toml +2 -0
  75. package/rust/Cargo.lock +192 -0
  76. package/rust/Cargo.toml +13 -0
  77. package/rust/src/main.rs +141 -0
  78. package/rust/src/owner.rs +604 -0
  79. package/rust/src/owner_tests.rs +795 -0
  80. package/rust/src/policy.rs +427 -0
  81. package/rust/src/tests.rs +97 -0
  82. package/rust/src/wire.rs +437 -0
  83. package/scripts/check-native-notices.mjs +27 -0
  84. package/ts/00_commitinfo_data.ts +1 -1
  85. package/ts/classes.managednftables.ts +148 -0
  86. package/ts/index.ts +2 -0
  87. package/ts/managed.types.ts +87 -0
  88. package/ts/nft.executor.ts +71 -4
  89. package/ts/nft.rulebuilder.firewall.ts +16 -31
  90. package/ts/plugins.ts +6 -0
  91. /package/{license → license.md} +0 -0
@@ -0,0 +1,87 @@
1
+ /** The caller owns these workload links and keeps them disabled until apply is confirmed. */
2
+ export interface IManagedNftEndpoint {
3
+ id: string;
4
+ interfaceIndex: number;
5
+ interfaceName: string;
6
+ /** Routed, unbridged veth or L3 TUN; TAP/bridge/uplink interfaces reject. */
7
+ interfaceKind: 'veth' | 'tun';
8
+ sourcePrefixes: string[];
9
+ }
10
+
11
+ /** Null selects the actual local host (input/output only), never an unbound forwarding side. */
12
+ export interface IManagedNftRule {
13
+ sourceEndpoint: string | null;
14
+ destinationEndpoint: string | null;
15
+ sourcePrefix: string;
16
+ destinationPrefix: string;
17
+ protocol: 'any' | 'tcp' | 'udp';
18
+ sourcePort: number | null;
19
+ destinationPort: number | null;
20
+ }
21
+
22
+ export interface IManagedNftPolicy {
23
+ schemaVersion: 1;
24
+ revision: number;
25
+ endpoints: IManagedNftEndpoint[];
26
+ rules: IManagedNftRule[];
27
+ }
28
+
29
+ export interface IPreparedManagedNftPolicy {
30
+ policy: IManagedNftPolicy;
31
+ digest: string;
32
+ }
33
+
34
+ export interface IManagedNftIdentity {
35
+ ownerId: string;
36
+ instanceId: string;
37
+ tableName: string;
38
+ bootId: string;
39
+ namespaceDevice: string;
40
+ namespaceInode: string;
41
+ }
42
+
43
+ export interface IManagedNftReceipt {
44
+ identity: IManagedNftIdentity;
45
+ tableHandle: string;
46
+ revision: number;
47
+ digest: string;
48
+ }
49
+
50
+ export interface IAppliedManagedNftPolicy {
51
+ receipt: IManagedNftReceipt;
52
+ prepared: IPreparedManagedNftPolicy;
53
+ }
54
+
55
+ /** Persist this complete command before reconcile, then retain its complete result. */
56
+ export interface IManagedNftTransition {
57
+ previous: IAppliedManagedNftPolicy | null;
58
+ target: IPreparedManagedNftPolicy;
59
+ }
60
+
61
+ export interface IManagedNftStatus {
62
+ identity: IManagedNftIdentity;
63
+ state: 'idle' | 'applied' | 'failed-owned' | 'released';
64
+ enforced: boolean;
65
+ applied: IAppliedManagedNftPolicy | null;
66
+ pending: IManagedNftTransition | null;
67
+ error: string | null;
68
+ }
69
+
70
+ export interface IManagedNftOptions {
71
+ ownerId: string;
72
+ /** Caller-retained unique operation identity, at least 16 identifier characters. */
73
+ instanceId: string;
74
+ /** Dedicated name beginning snft_; existing foreign tables always reject. */
75
+ tableName: string;
76
+ /** Explicit executable selection for qualified distributions and isolated tests. */
77
+ binaryPath?: string;
78
+ }
79
+
80
+ export type TManagedNftCommands = {
81
+ preparePolicy: { params: IManagedNftPolicy; result: IPreparedManagedNftPolicy };
82
+ openOwner: { params: Omit<IManagedNftOptions, 'binaryPath'>; result: IManagedNftStatus };
83
+ reconcilePolicy: { params: IManagedNftTransition; result: IAppliedManagedNftPolicy };
84
+ inspectPolicy: { params: Record<string, never>; result: IManagedNftStatus };
85
+ releasePolicy: { params: IAppliedManagedNftPolicy; result: { released: true } };
86
+ closeOwner: { params: Record<string, never>; result: { released: true } };
87
+ };
@@ -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
- // Strip "nft " prefix if present
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 protocols = expandProtocols(rule.protocol);
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 protocols) {
29
+ for (const proto of protocolVariants) {
24
30
  const ruleParts = [...parts];
31
+ const portProtocol = proto ?? 'tcp';
25
32
 
26
- if (rule.protocol) {
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(`${proto} sport ${rule.sourcePort}`);
46
+ ruleParts.push(`${portProtocol} sport ${rule.sourcePort}`);
40
47
  }
41
48
 
42
49
  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}"`);
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}${comment} ${options.action}`
148
+ `nft add rule ${family} ${tableName} ${options.direction} ip ${options.matchField} @${options.setName} ${options.action}${comment}`
164
149
  ];
165
150
  }
166
151
 
package/ts/plugins.ts CHANGED
@@ -1,16 +1,22 @@
1
1
  // node native scope
2
2
  import * as childProcess from 'node:child_process';
3
3
  import * as util from 'node:util';
4
+ import * as path from 'node:path';
5
+ import * as url from 'node:url';
4
6
 
5
7
  export {
6
8
  childProcess,
7
9
  util,
10
+ path,
11
+ url,
8
12
  };
9
13
 
10
14
  // @push.rocks scope
11
15
  import * as smartlog from '@push.rocks/smartlog';
12
16
  import * as smartpromise from '@push.rocks/smartpromise';
17
+ import * as smartrust from '@push.rocks/smartrust';
13
18
  export {
14
19
  smartlog,
15
20
  smartpromise,
21
+ smartrust,
16
22
  };
File without changes