@push.rocks/smartnftables 2.5.2 → 3.0.0

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 (41) hide show
  1. package/changelog.md +31 -0
  2. package/dist_rust/smartnftables_linux_amd64_musl +0 -0
  3. package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
  4. package/dist_rust/smartnftables_linux_arm64_musl +0 -0
  5. package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/managed.egress.types.d.ts +69 -3
  8. package/package.json +6 -6
  9. package/readme.md +342 -61
  10. package/rust/src/egress.compile.rs +157 -76
  11. package/rust/src/egress.host.rs +104 -44
  12. package/rust/src/egress.hostgrant.rs +14 -14
  13. package/rust/src/egress.keys.rs +305 -0
  14. package/rust/src/egress.leased.rs +399 -0
  15. package/rust/src/egress.poolguard.rs +45 -0
  16. package/rust/src/egress.private.rs +197 -0
  17. package/rust/src/egress.published.rs +370 -0
  18. package/rust/src/egress.router.rs +64 -295
  19. package/rust/src/egress.rs +248 -17
  20. package/rust/src/egress.workloadgrant.rs +88 -0
  21. package/rust/src/egress_hostgrant_tests.rs +196 -47
  22. package/rust/src/egress_localplatform_tests.rs +204 -0
  23. package/rust/src/egress_localport_tests.rs +266 -0
  24. package/rust/src/egress_publishedrange_tests.rs +812 -0
  25. package/rust/src/egress_routerequivalence_tests.rs +448 -0
  26. package/rust/src/egress_tests.rs +132 -230
  27. package/rust/src/egress_workloadgrant_tests.rs +313 -0
  28. package/rust/src/owner.rs +77 -15
  29. package/rust/src/owner_host_traffic_tests.rs +4 -0
  30. package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
  31. package/rust/src/owner_identity_tests.rs +228 -80
  32. package/rust/src/owner_localplatform_tests.rs +274 -0
  33. package/rust/src/owner_localport_tests.rs +241 -0
  34. package/rust/src/owner_publishedrange_traffic_tests.rs +306 -0
  35. package/rust/src/owner_router_traffic_tests.rs +2 -0
  36. package/rust/src/owner_scale_traffic_tests.rs +383 -0
  37. package/rust/src/owner_tests.rs +15 -0
  38. package/rust/src/owner_workloadgrant_tests.rs +278 -0
  39. package/rust/src/policy.rs +29 -25
  40. package/ts/00_commitinfo_data.ts +1 -1
  41. package/ts/managed.egress.types.ts +71 -3
package/changelog.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-09-28 - 3.0.0
4
+
5
+ ### Breaking Changes
6
+
7
+ - The schema-v2 `routerEgress` compiler now keeps private endpoints, directed rules and leased egress grants in named sets and maps behind a fixed set of rules, and every scope keeps its `publishedPorts` the same way (see Features). The policy API is additive and policy digests are unchanged, but the compiled graph of every `routerEgress` table and of every scope with publications changes: reconcile, inspection or release against such a table applied by 2.x returns `Conflict`, because the new engine does not adopt the old graph. Recover and release each such table with the exact previous engine and its retained intent and receipt, under a traffic fence, before upgrading. Tables of other scopes and without publications are unchanged.
8
+ - Rebalance the atomic replacement budget inside the unchanged 320,000-byte batch: the complete target (rules, chains and sets at most 100,000 bytes, plus set elements) may take 212,000 bytes beside a 107,520-byte handle-only deletion of the previous graph, instead of two fixed 100,000-byte halves for rules and elements. The hard ceiling is Linux's `sendmsg` limit, the socket send buffer (`net.core.wmem_max` doubled, 425,984 bytes by default); 320,000 bytes fits any `wmem_max` of at least 160,016.
9
+ - Raise the router's input bounds to 128 private endpoints and links, 1024 private rules and 1024 egress grants, and the publication bound of both scopes to 1024. Schema-v1 policies keep their bounds, digests and compiled bytes.
10
+
11
+ ### Features
12
+
13
+ - Router: a node of 64 workloads, each with local DNS, public TCP and UDP egress, four platform endpoints and one to three publications, compiles about 66,000 rule bytes and 105,000 element bytes (89 such workloads fit), where 2.x held three. Private links, indices, names and source prefixes, directed rules, and per-kind and per-shape leased zone, return and flow maps, with the generation labels in two exact sets, replace the per-endpoint guard chains, per-rule grants, per-grant classifiers and admissions and the per-generation state chains; every lookup keeps the operands the rules compared, a key's link index is proven with the interface name through `private_link`, and a decision corpus of about 133,000 packets frozen from the 2.6.0 compiler is reproduced exactly. Owner verification admits absence (inverted) lookups and exact maps, and interval elements without a distinct last key.
14
+ - Add published port ranges to both hops of a schema-v2 publication: an optional `hostPortEnd` on `hostTransit.publishedPorts` and `transitPortEnd` on `routerEgress.publishedPorts` publish `port..end` with every port kept (the inside port must equal the first port). A range compiles to one interval match per classification, translation and admission and to address-only destination NAT, so it costs the same operations as one port. Inverted and one-port ranges, a range with a differing inside port, overlaps between publications of one protocol and overlaps with a leased source-port range on the translated address are refused.
15
+ - Add `symmetric` publications on both hops: the workload may open flows from its published address and port(s) toward unprotected space, NEW (a SYN-only TCP opening) or ESTABLISHED with ESTABLISHED replies, source-translated to the transit address and published transit port in the router and to `hostIp` and the published uplink port on the host, so outbound flows leave from the port clients reach. The admissions follow the capture barrier, so the protected union stays denied; a symmetric publication's inside ports may not overlap another publication's on the same address or, on the host, a leased range of its target address.
16
+ - Compile every publication, of both hops, as elements of CONSTANT concatenated interval sets and maps (the kernel's pipapo backend) behind a constant number of rules: at most eleven rules and four sets on the host and thirteen rules and six sets in the router, whatever the number of publications, instead of four and six rules per publication. Destination NAT is a map lookup (to address and port for one port, to the address alone for a range) and symmetric source NAT a map lookup to the outside address and port span. Owner verification now admits interval and map sets, reads back their concatenation, every element's bounds and data, and map lookups with their data register, so apply, inspection, adoption and recovery reject a changed element like a changed rule. Absent and empty publications, `hostPortEnd`, `transitPortEnd` and `symmetric` (and `symmetric: false`) keep the previous canonical policy and digest, and a scope without publications keeps its compiled bytes; a scope with publications compiles a different graph for the same digest.
17
+
18
+ ### Maintenance
19
+
20
+ - Release tooling: pnpm 12.5.1, `@git.zone/cli` 7.3.0, `@git.zone/tsrust` 1.15.1, `@git.zone/tstest` 6.3.2, `@git.zone/tsrun` 3.0.1, `@types/node` 26.6.2 (pnpm 12.6.0 and `@types/node` 26.6.3 stay out until they clear the seven-day rule).
21
+
22
+ ## 2026-09-25 - 2.6.0
23
+
24
+ ### Features
25
+
26
+ - Add optional loopback TCP port owners to the schema-v2 `allocationPoolGuard` scope: `localTcpPortOwners` entries `{ address, port, uid }` (an exact IPv4 loopback host address, one port and one owning uid, at most 8) compile, ahead of the guard, an OUTPUT rule that rejects with a TCP reset every packet to that address and port from a socket whose uid differs (`meta skuid != uid`, after ordinary output destination NAT) and an INPUT rule that drops the address and port arriving on any interface but loopback. Owner verification now admits the TCP-reset `reject` expression and reads it back like every other operand, so the entries are verified on apply, retained, adopted, recovered and released with the table. Absent and empty keep the previous canonical policy, digest and compiled bytes. The native qualification guest loads the `nf_reject_ipv4`, `nf_reject_ipv6`, `nft_reject` and `nft_reject_inet` modules.
27
+ - Add optional host-local platform endpoints to the schema-v2 `hostTransit` scope: `localPlatformEndpoints` lists ids of `protection.platformEndpoints` the host serves on an exact address of its uplink or a handoff link (verified through rtnetlink at apply, recovery and inspection). INPUT admits leased flows from their exact handoff with the lease's transit source address and a source port in its range for the endpoint's protocol, to the exact endpoint tuple in the live and originally tracked tuple (NEW with a SYN-only TCP opening, or ESTABLISHED), and OUTPUT admits only the ESTABLISHED reply; translated flows and host-origin openings stay denied. The refusal of a platform endpoint on a host address is lifted only for declared ids. Absent and empty keep the previous canonical policy, digest and compiled bytes.
28
+ - Add optional stateful one-way workload grants to the schema-v2 `routerEgress` scope: `workloadGrants` entries `{ protocol, sourceAddress, destinationAddress, destinationPort }` let one veth workload endpoint open one exact port of another. Four constant FORWARD admissions look up the incoming and outgoing link with their addresses in a CONSTANT `workload_link` set and the protocols with the live and originally tracked tuple in a CONSTANT `workload_grant` set, in the default conntrack zone; the destination only answers (ESTABLISHED replies) and can never open toward the source. Up to 1024 grants, sharing the scope's set element budget with host grants. Absent and empty keep the previous canonical policy, digest and compiled bytes.
29
+
30
+ ### Maintenance
31
+
32
+ - Bring the release-time tooling current: `@git.zone/tsrust` 1.14.0 → 1.15.0. `@types/node` stays 26.6.1 and pnpm 12.4.2, because their newer releases are younger than the seven-day rule; `@git.zone/cli` 7.2.2, tsbuild 5.0.0, tsrun 3.0.0 and tstest 6.2.0 are already current. The native notices verify unchanged.
33
+
3
34
  ## 2026-09-24 - 2.5.2
4
35
 
5
36
  ### Maintenance
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "format": "tsrust.build-provenance.v2",
3
- "binarySha256": "8171a73d23d26828e29fc1a403ec08a4a24664d7b0f625d90217f59cb760638e",
3
+ "binarySha256": "17c4308039280f584cd51d6cf9030e3c4512746d316561925b6da95bb3c67ff8",
4
4
  "buildInfo": {
5
5
  "projectName": "@push.rocks/smartnftables",
6
- "projectVersion": "2.5.2",
7
- "gitCommit": "af81d179977c8903fc265a0b66a0b6c52c2b7e04",
6
+ "projectVersion": "3.0.0",
7
+ "gitCommit": "61c5a8be9c99a83cecde6509fb31c47f43e15bff",
8
8
  "gitDirty": false,
9
- "builtAt": "2026-09-24T23:25:26.155Z",
10
- "tsrustVersion": "1.14.0",
9
+ "builtAt": "2026-09-28T00:27:05.031Z",
10
+ "tsrustVersion": "1.15.1",
11
11
  "binary": "smartnftables",
12
12
  "target": "linux_amd64_musl"
13
13
  }
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "format": "tsrust.build-provenance.v2",
3
- "binarySha256": "05b1bfa1d470edec18ced9a47b2426fad72801b81ac8e29f3a8c3dc9cb1787f7",
3
+ "binarySha256": "3756fe460edc3bdea00c7f598a683081d8238f5172dad9776304a030508d4276",
4
4
  "buildInfo": {
5
5
  "projectName": "@push.rocks/smartnftables",
6
- "projectVersion": "2.5.2",
7
- "gitCommit": "af81d179977c8903fc265a0b66a0b6c52c2b7e04",
6
+ "projectVersion": "3.0.0",
7
+ "gitCommit": "61c5a8be9c99a83cecde6509fb31c47f43e15bff",
8
8
  "gitDirty": false,
9
- "builtAt": "2026-09-24T23:25:35.983Z",
10
- "tsrustVersion": "1.14.0",
9
+ "builtAt": "2026-09-28T00:27:15.164Z",
10
+ "tsrustVersion": "1.15.1",
11
11
  "binary": "smartnftables",
12
12
  "target": "linux_arm64_musl"
13
13
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '2.5.2',
6
+ version: '3.0.0',
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=
@@ -65,9 +65,14 @@ export interface IManagedNftEgressGenerationV2 extends IManagedNftHandoffAllocat
65
65
  * `transitSourceAddress:transitPort`, this translates on to the workload. */
66
66
  export interface IManagedNftRouterPublishedPortV2 {
67
67
  protocol: 'tcp' | 'udp';
68
- /** Published handoff port, 1–65535 and unique per protocol across the scope.
69
- * It is the `targetPort` of the matching host-transit publication. */
68
+ /** Published handoff port, 1–65535; published ports never overlap per protocol across the
69
+ * scope. It is the `targetPort` of the matching host-transit publication. */
70
70
  transitPort: number;
71
+ /** Optional last port of a published range `transitPort..transitPortEnd`, greater than
72
+ * `transitPort`. A range keeps every port, so `endpointPort` must equal `transitPort`; like one
73
+ * port it is one interval set element per set, and its translation keeps the port. Absent for
74
+ * one port, which is its only canonical form. */
75
+ transitPortEnd?: number;
71
76
  endpointPort: number;
72
77
  /** Exact current workload endpoint address behind one veth endpoint of this
73
78
  * scope. Never a router-local address, a gateway or a platform endpoint. */
@@ -75,6 +80,12 @@ export interface IManagedNftRouterPublishedPortV2 {
75
80
  /** Exact leased transit source address of one current generation, present on
76
81
  * `handoff`. No wildcard, secondary-address or route inference. */
77
82
  transitSourceAddress: string;
83
+ /** Optional: the workload may open flows from `endpointAddress` and its published endpoint
84
+ * port(s) toward unprotected space, source-translated to `transitSourceAddress` and the
85
+ * published transit port (a range keeps the port). The protected union stays denied, and its
86
+ * endpoint ports may not overlap another publication's on the same address. Absent and
87
+ * `false` are the same canonical policy. */
88
+ symmetric?: boolean;
78
89
  }
79
90
  /** One exact host-origin flow: the node's own host network namespace dials one port of one local
80
91
  * workload at its lease address, from the transit host address of the current handoff. Compile the
@@ -91,6 +102,21 @@ export interface IManagedNftHostGrant {
91
102
  /** The workload's exact listening port, 1–65535; never a range. */
92
103
  destinationPort: number;
93
104
  }
105
+ /** One stateful one-way flow between two workloads behind the same router: the source workload opens
106
+ * connections to one exact port of the destination workload, which may only answer. Both addresses are
107
+ * exact current addresses of two different veth workload endpoints of the scope. It admits the tuple
108
+ * (a TCP opening only with SYN) and its ESTABLISHED replies in the default conntrack zone and nothing
109
+ * else: no reverse opening, no translation and no source-port selection. Grant the reverse direction as
110
+ * a second grant. */
111
+ export interface IManagedNftWorkloadGrant {
112
+ protocol: 'tcp' | 'udp';
113
+ /** Exact unicast address of the dialling workload endpoint, never a prefix. */
114
+ sourceAddress: string;
115
+ /** Exact unicast address of the dialled workload endpoint, never a prefix. */
116
+ destinationAddress: string;
117
+ /** The destination workload's exact listening port, 1–65535; never a range. */
118
+ destinationPort: number;
119
+ }
94
120
  /** One combined private/TUN/local/egress table; never compose a second ACCEPT over v1 terminal drops. */
95
121
  export interface IManagedNftRouterEgressScopeV2 {
96
122
  kind: 'routerEgress';
@@ -114,17 +140,32 @@ export interface IManagedNftRouterEgressScopeV2 {
114
140
  * workload. Absent and empty are the same canonical policy, digest and compiled graph; at most 1024,
115
141
  * held in a named set behind a constant number of rules. */
116
142
  hostGrants?: IManagedNftHostGrant[];
143
+ /** Optional stateful one-way workload-to-workload grants. Absent and empty are the same canonical
144
+ * policy, digest and compiled graph; at most 1024, held in two named sets behind four rules. They
145
+ * share the scope's set element budget with `hostGrants`. */
146
+ workloadGrants?: IManagedNftWorkloadGrant[];
117
147
  }
118
148
  /** One inbound uplink publication, compiled inside the same host-transit generation. */
119
149
  export interface IManagedNftPublishedPortV2 {
120
150
  protocol: 'tcp' | 'udp';
121
- /** Published uplink port, 1–65535 and unique per protocol across the scope. */
151
+ /** Published uplink port, 1–65535; published ports never overlap per protocol across the scope. */
122
152
  hostPort: number;
153
+ /** Optional last port of a published range `hostPort..hostPortEnd`, greater than `hostPort`.
154
+ * A range keeps every port, so `targetPort` must equal `hostPort`; like one port it is one
155
+ * interval set element per set, and its translation keeps the port. Absent for one port, which
156
+ * is its only canonical form. */
157
+ hostPortEnd?: number;
123
158
  targetPort: number;
124
159
  /** Exact leased transit source address of one current allocation; it selects the handoff. */
125
160
  targetAddress: string;
126
161
  /** Exact current uplink address that publishes this port. No wildcard or route inference. */
127
162
  hostIp: string;
163
+ /** Optional: flows from `targetAddress` and the published target port(s) may leave through the
164
+ * uplink toward unprotected space, source-translated to `hostIp` and the published uplink port
165
+ * (a range keeps the port). The protected union stays denied, its target ports may not fall in
166
+ * a leased range of `targetAddress` or overlap another publication's. Absent and `false` are
167
+ * the same canonical policy. */
168
+ symmetric?: boolean;
128
169
  }
129
170
  /** Shared-host capture barrier and explicit outer SNAT; Docker forwarding remains a separate owner. */
130
171
  export interface IManagedNftHostTransitScopeV2 {
@@ -147,6 +188,27 @@ export interface IManagedNftHostTransitScopeV2 {
147
188
  * host-local, a platform endpoint nor a leased transit source address. Absent and empty are the
148
189
  * same canonical policy; at most 1024, held in a named set behind a constant number of rules. */
149
190
  hostGrants?: IManagedNftHostGrant[];
191
+ /** Optional ids of `protection.platformEndpoints` this host serves itself, on an exact current address
192
+ * of the uplink or of a handoff link (rtnetlink verifies it at apply, recovery and inspection). Only a
193
+ * declared endpoint may hold a host address. Leased flows reach it from their exact handoff with the
194
+ * leased source address and a source port of the allocation's range for the endpoint's protocol; its
195
+ * ESTABLISHED replies return. Absent and empty are the same canonical policy, digest and compiled graph. */
196
+ localPlatformEndpoints?: string[];
197
+ }
198
+ /** One loopback TCP service that only one local user may dial. Every packet the host sends to the
199
+ * exact address and port from a socket of any other user is rejected with a TCP reset, after ordinary
200
+ * output destination NAT; the address and port arriving on any interface but loopback are dropped.
201
+ * The service must bind exactly this address: a wildcard listener stays reachable through others. */
202
+ export interface IManagedNftLocalTcpPortOwner {
203
+ /** Exact IPv4 loopback host address in 127.0.0.0/8, e.g. `127.0.0.1`. Never a prefix or wildcard. */
204
+ address: string;
205
+ /** The service's exact listening port, 1–65535; never a range. */
206
+ port: number;
207
+ /** The only user id whose sockets may send to the port: the socket file's file-system uid as
208
+ * the user namespace owning the network namespace sees it, 0–4294967294. Packets without a socket file carry no uid and
209
+ * are not matched: kernel replies, the teardown of an orphaned socket, and in-kernel sockets such as NFS or CIFS clients,
210
+ * which only privileged mounts create. */
211
+ uid: number;
150
212
  }
151
213
  /** Host-wide IPv4 destination denial for caller-authenticated private allocation pools.
152
214
  * It has no link dependency; exact host grants are its only exceptions. It is not allocation-release or boot-order proof. */
@@ -159,6 +221,10 @@ export interface IManagedNftAllocationPoolGuardScopeV2 {
159
221
  * listed pool, and their replies. Absent and empty are the same canonical policy; at most 1024,
160
222
  * held in a named set behind a constant number of rules. */
161
223
  hostGrants?: IManagedNftHostGrant[];
224
+ /** Optional host-wide loopback port ownership, compiled ahead of the guard in the same table.
225
+ * Absent and empty are the same canonical policy, digest and compiled graph; at most 8, one per
226
+ * exact address and port. */
227
+ localTcpPortOwners?: IManagedNftLocalTcpPortOwner[];
162
228
  }
163
229
  export interface IManagedNftPolicyV2 {
164
230
  schemaVersion: 2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartnftables",
3
- "version": "2.5.2",
3
+ "version": "3.0.0",
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",
@@ -9,12 +9,12 @@
9
9
  "author": "Task Venture Capital GmbH",
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
- "@git.zone/cli": "7.2.2",
12
+ "@git.zone/cli": "7.3.0",
13
13
  "@git.zone/tsbuild": "^5.0.0",
14
- "@git.zone/tsrun": "^3.0.0",
15
- "@git.zone/tsrust": "1.14.0",
16
- "@git.zone/tstest": "6.2.0",
17
- "@types/node": "26.6.1",
14
+ "@git.zone/tsrun": "^3.0.1",
15
+ "@git.zone/tsrust": "1.15.1",
16
+ "@git.zone/tstest": "6.3.2",
17
+ "@types/node": "26.6.2",
18
18
  "typescript": "^7.0.2"
19
19
  },
20
20
  "dependencies": {