@push.rocks/smartnftables 1.9.0 → 2.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.
package/changelog.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-09-14 - 2.0.0
4
+
5
+ ### Breaking Changes
6
+
7
+ - Share original-direction connection-state checks while preserving exact grant, tuple, link and generation validation. Router tables compiled by 1.x require fenced recovery and release with the old engine before preparing and applying with version 2; other policy compiler identities are unchanged.
8
+
9
+ ### Maintenance
10
+
11
+ - Update Node typings to the current release-age eligible version.
12
+
3
13
  ## 2026-09-14 - 1.9.0
4
14
 
5
15
  ### Features
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "format": "tsrust.build-provenance.v2",
3
- "binarySha256": "e7f3f755b5f9579969e94d70442710a8b2ebde0aa8dda4c0b312094b8771caf6",
3
+ "binarySha256": "f87173a416bf8ff6aad2d59dcb8534bbc720325b51a78134ed84c758f1944881",
4
4
  "buildInfo": {
5
5
  "projectName": "@push.rocks/smartnftables",
6
- "projectVersion": "1.9.0",
7
- "gitCommit": "fb3d6ad37496d2010fa77e8e6619565898519748",
6
+ "projectVersion": "2.0.0",
7
+ "gitCommit": "baace9c25621df66a88552dcd1d2f5e63c3e477f",
8
8
  "gitDirty": false,
9
- "builtAt": "2026-09-14T04:36:00.044Z",
9
+ "builtAt": "2026-09-14T22:07:32.392Z",
10
10
  "tsrustVersion": "1.13.0",
11
11
  "binary": "smartnftables",
12
12
  "target": "linux_amd64_musl"
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "format": "tsrust.build-provenance.v2",
3
- "binarySha256": "f80787be41352d19daf0b3916cb285fe29aa3e88f637180109a07ab8ec609b14",
3
+ "binarySha256": "206ed227d6c78079f1240f3a70fd2cf305258e84682b2affd97b92fcf3509b5a",
4
4
  "buildInfo": {
5
5
  "projectName": "@push.rocks/smartnftables",
6
- "projectVersion": "1.9.0",
7
- "gitCommit": "fb3d6ad37496d2010fa77e8e6619565898519748",
6
+ "projectVersion": "2.0.0",
7
+ "gitCommit": "baace9c25621df66a88552dcd1d2f5e63c3e477f",
8
8
  "gitDirty": false,
9
- "builtAt": "2026-09-14T04:36:00.187Z",
9
+ "builtAt": "2026-09-14T22:07:32.519Z",
10
10
  "tsrustVersion": "1.13.0",
11
11
  "binary": "smartnftables",
12
12
  "target": "linux_arm64_musl"
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '1.9.0',
6
+ version: '2.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=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartnftables",
3
- "version": "1.9.0",
3
+ "version": "2.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",
@@ -14,7 +14,7 @@
14
14
  "@git.zone/tsrun": "^2.0.6",
15
15
  "@git.zone/tsrust": "1.13.0",
16
16
  "@git.zone/tstest": "6.0.0",
17
- "@types/node": "26.4.1",
17
+ "@types/node": "26.5.0",
18
18
  "typescript": "^7.0.2"
19
19
  },
20
20
  "dependencies": {
package/readme.md CHANGED
@@ -287,6 +287,23 @@ compiled graph still must fit 768 operations and 100,000 bytes; cross-products c
287
287
  reach that limit before individual input limits. Exhausted source-port ranges
288
288
  drop new flows rather than allocate outside the lease.
289
289
 
290
+ The router compiler shares original-direction connection-state and label checks
291
+ per generation and protocol. Each jump still checks the complete current/original
292
+ tuple, links, zone and direction; replies and source NAT keep their exact checks.
293
+ A combined two-endpoint policy with six private rules, five protected prefixes and
294
+ six workload/router grants fits the existing atomic graph limit.
295
+
296
+ **Upgrading from 1.x:** version 2 changes the compiled identity of schema-v2
297
+ `routerEgress` tables. Retained router tables and their receipts cannot be adopted
298
+ by the new engine, even when the policy digest is unchanged. Fence workload and
299
+ router-local traffic, then use the exact previous engine and retained intent/receipt
300
+ to recover and release each router table. Join that owner before upgrading.
301
+ Prepare and apply the policy with the new engine and persist its complete receipt
302
+ before lifting the traffic fence. Keep lease quarantine and host/pool protection
303
+ throughout; this migration provides no proof of conntrack drainage or safe allocation reuse.
304
+ Do not discard an unresolved retained table or its old engine/receipt. Schema-v1,
305
+ host-transit and allocation-pool-guard compiled identities are unchanged.
306
+
290
307
  The caller must coordinate router and host apply order, retain complete intents
291
308
  and receipts, authenticate protected authority, prevent reuse of quarantined
292
309
  leases, and qualify other packet owners. An ACCEPT in this table cannot override
@@ -108,13 +108,48 @@ fn raw(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
108
108
  }
109
109
  program.end("raw_return", vec![], 0)
110
110
  }
111
+ fn original_states(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
112
+ for (index, allocation) in scope.generations.iter().enumerate() {
113
+ for protocol in ["tcp", "udp"] {
114
+ if !allocation
115
+ .grants
116
+ .iter()
117
+ .any(|grant| grant.protocol == protocol)
118
+ {
119
+ continue;
120
+ }
121
+ let chain = format!("original_{index}_{protocol}");
122
+ program.chain(&chain, None)?;
123
+ // Only exact tuple/link/zone/direction classifiers jump here. Share
124
+ // state checks instead of repeating those classifiers three times.
125
+ for connection_state in [2, 8] {
126
+ let mut labelled = ct(13, None, label(allocation));
127
+ labelled.extend(state(connection_state));
128
+ if connection_state == 8 && protocol == "tcp" {
129
+ labelled.extend(opening_tcp());
130
+ }
131
+ program.end(&chain, labelled, 1)?;
132
+ }
133
+ let mut fresh = state(8);
134
+ fresh.extend(ct(13, None, vec![0; 16]));
135
+ if protocol == "tcp" {
136
+ fresh.extend(opening_tcp());
137
+ }
138
+ fresh.extend(set_ct(13, label(allocation)));
139
+ program.end(&chain, fresh, 1)?;
140
+ // A failed state/label match returns to the caller's remaining
141
+ // classifiers and terminal denials, exactly as the inlined rules.
142
+ }
143
+ }
144
+ Ok(())
145
+ }
111
146
  fn grants(
112
147
  program: &mut Program<'_>,
113
148
  scope: &RouterScope,
114
149
  chain: &str,
115
150
  platform: bool,
116
151
  ) -> Result<()> {
117
- for allocation in &scope.generations {
152
+ for (index, allocation) in scope.generations.iter().enumerate() {
118
153
  for grant in &allocation.grants {
119
154
  if matches!(grant.destination, Destination::Platform { .. }) != platform {
120
155
  continue;
@@ -126,24 +161,13 @@ fn grants(
126
161
  if chain != "input" {
127
162
  let mut original = filter(scope, grant, false)?;
128
163
  original.extend(generation(allocation, 0, false));
129
- // Both original directions are checked for every packet; a
130
- // label alone never authorizes a changed grant or endpoint.
131
- for connection_state in [2, 8] {
132
- let mut labelled = original.clone();
133
- labelled.extend(ct(13, None, label(allocation)));
134
- labelled.extend(state(connection_state));
135
- if connection_state == 8 && grant.protocol == "tcp" {
136
- labelled.extend(opening_tcp());
137
- }
138
- program.end(chain, labelled, 1)?;
139
- }
140
- original.extend(state(8));
141
- original.extend(ct(13, None, vec![0; 16]));
142
- if grant.protocol == "tcp" {
143
- original.extend(opening_tcp());
144
- }
145
- original.extend(set_ct(13, label(allocation)));
146
- program.end(chain, original, 1)?;
164
+ // Current and original tuples remain checked on every packet;
165
+ // a label alone never authorizes a changed grant or endpoint.
166
+ program.jump(
167
+ chain,
168
+ original,
169
+ &format!("original_{index}_{}", grant.protocol),
170
+ )?;
147
171
  }
148
172
  if chain != "output" {
149
173
  let mut reply = filter(scope, grant, true)?;
@@ -182,6 +206,7 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &RouterScope, revision:
182
206
  program.rule(&name, expressions)?;
183
207
  }
184
208
  }
209
+ original_states(program, scope)?;
185
210
  raw(program, scope)?;
186
211
  protected(program, "protected_out", scope, false)?;
187
212
  protected(program, "protected_back", scope, true)?;
@@ -297,6 +297,74 @@ fn prepared_v2_compiles_both_scopes_with_bounded_and_identifiable_graphs() {
297
297
  }
298
298
  }
299
299
 
300
+ /// A combined workload/router policy must fit without discarding private DNS,
301
+ /// either public transport, explicit platform grants, or the protected union.
302
+ pub(crate) fn combined_router_capacity() -> Value {
303
+ let mut value = router();
304
+ value["scope"]["protection"]["prefixes"] = json!([
305
+ "10.0.0.0/8",
306
+ "127.0.0.0/8",
307
+ "169.254.0.0/16",
308
+ "172.16.0.0/12",
309
+ "192.0.2.0/24"
310
+ ]);
311
+ value["scope"]["protection"]["platformEndpoints"] = json!([
312
+ {"id":"dns-udp","address":"192.0.2.1","protocol":"udp","port":53},
313
+ {"id":"dns-tcp","address":"192.0.2.1","protocol":"tcp","port":53},
314
+ {"id":"relay","address":"192.0.2.3","protocol":"tcp","port":3000}
315
+ ]);
316
+ let rules = value["scope"]["rules"].as_array_mut().unwrap();
317
+ rules[0]["protocol"] = json!("any");
318
+ rules[0]["destinationPort"] = Value::Null;
319
+ let mut reverse = rules[0].clone();
320
+ reverse["sourceEndpoint"] = json!("vpn");
321
+ reverse["destinationEndpoint"] = json!("work");
322
+ reverse["sourcePrefix"] = json!("10.242.0.2/32");
323
+ reverse["destinationPrefix"] = json!("10.241.0.2/32");
324
+ for index in [1, 2] {
325
+ let mut dns = rules[index].clone();
326
+ dns["protocol"] = json!("tcp");
327
+ rules.push(dns);
328
+ }
329
+ rules.push(reverse);
330
+ value["scope"]["generations"][0]["grants"] = json!([
331
+ {"sourceEndpoint":"work","sourcePrefix":"10.241.0.2/32","destination":{"kind":"public","prefix":"0.0.0.0/0"},
332
+ "protocol":"tcp","sourcePort":null,"destinationPort":null,"sourcePortRange":{"first":10000,"last":10999}},
333
+ {"sourceEndpoint":"work","sourcePrefix":"10.241.0.2/32","destination":{"kind":"public","prefix":"0.0.0.0/0"},
334
+ "protocol":"udp","sourcePort":null,"destinationPort":null,"sourcePortRange":{"first":10000,"last":10999}},
335
+ {"sourceEndpoint":"work","sourcePrefix":"10.241.0.2/32","destination":{"kind":"platform","endpointId":"relay"},
336
+ "protocol":"tcp","sourcePort":null,"destinationPort":3000,"sourcePortRange":{"first":10000,"last":10999}},
337
+ {"sourceEndpoint":null,"sourcePrefix":"10.240.0.2/32","destination":{"kind":"platform","endpointId":"dns-tcp"},
338
+ "protocol":"tcp","sourcePort":null,"destinationPort":53,"sourcePortRange":{"first":10000,"last":10999}},
339
+ {"sourceEndpoint":null,"sourcePrefix":"10.240.0.2/32","destination":{"kind":"platform","endpointId":"dns-udp"},
340
+ "protocol":"udp","sourcePort":null,"destinationPort":53,"sourcePortRange":{"first":10000,"last":10999}},
341
+ {"sourceEndpoint":null,"sourcePrefix":"10.240.0.2/32","destination":{"kind":"platform","endpointId":"relay"},
342
+ "protocol":"tcp","sourcePort":null,"destinationPort":3000,"sourcePortRange":{"first":10000,"last":10999}}
343
+ ]);
344
+ value
345
+ }
346
+
347
+ #[test]
348
+ fn combined_router_full_transports_and_protection_fit_atomic_reserve() {
349
+ let mut value = combined_router_capacity();
350
+ value["revision"] = json!(9007199254740991_u64);
351
+ let policy = normalized(value).unwrap();
352
+ let prepared = policy.prepare().unwrap();
353
+ let program = prepared
354
+ .program(&format!("snft_{}", "x".repeat(59)))
355
+ .unwrap();
356
+ let bytes: usize = program
357
+ .iter()
358
+ .map(|(_, attributes)| crate::wire::encode_attrs(attributes).len() + 20)
359
+ .sum();
360
+ assert!(program.len() <= 768);
361
+ assert!(bytes <= 100_000);
362
+ println!(
363
+ "COMBINED_ROUTER_GRAPH operations={} bytes={bytes}",
364
+ program.len()
365
+ );
366
+ }
367
+
300
368
  #[test]
301
369
  fn v2_compiled_graph_limit_rejects_bounded_but_excessive_cross_products() {
302
370
  let mut value = router();
@@ -42,7 +42,7 @@ fn router() -> egress::Policy {
42
42
  veth("handoff", "10.240.0.2/30");
43
43
  ip(&["tuntap", "add", "dev", "vpn", "mode", "tun"]);
44
44
  ip(&["address", "add", "10.242.0.1/24", "dev", "vpn"]);
45
- let mut value = egress::tests::router();
45
+ let mut value = egress::tests::combined_router_capacity();
46
46
  value["scope"]["links"] = json!([
47
47
  binding("work", "10.241.0.1", "veth"),
48
48
  binding("vpn", "10.242.0.1", "tun")
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '1.9.0',
6
+ version: '2.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
  }