@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 +10 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +4 -4
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +4 -4
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/package.json +2 -2
- package/readme.md +17 -0
- package/rust/src/egress.router.rs +44 -19
- package/rust/src/egress_tests.rs +68 -0
- package/rust/src/owner_egress_tests.rs +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
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
|
|
Binary file
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": "tsrust.build-provenance.v2",
|
|
3
|
-
"binarySha256": "
|
|
3
|
+
"binarySha256": "f87173a416bf8ff6aad2d59dcb8534bbc720325b51a78134ed84c758f1944881",
|
|
4
4
|
"buildInfo": {
|
|
5
5
|
"projectName": "@push.rocks/smartnftables",
|
|
6
|
-
"projectVersion": "
|
|
7
|
-
"gitCommit": "
|
|
6
|
+
"projectVersion": "2.0.0",
|
|
7
|
+
"gitCommit": "baace9c25621df66a88552dcd1d2f5e63c3e477f",
|
|
8
8
|
"gitDirty": false,
|
|
9
|
-
"builtAt": "2026-09-
|
|
9
|
+
"builtAt": "2026-09-14T22:07:32.392Z",
|
|
10
10
|
"tsrustVersion": "1.13.0",
|
|
11
11
|
"binary": "smartnftables",
|
|
12
12
|
"target": "linux_amd64_musl"
|
|
Binary file
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": "tsrust.build-provenance.v2",
|
|
3
|
-
"binarySha256": "
|
|
3
|
+
"binarySha256": "206ed227d6c78079f1240f3a70fd2cf305258e84682b2affd97b92fcf3509b5a",
|
|
4
4
|
"buildInfo": {
|
|
5
5
|
"projectName": "@push.rocks/smartnftables",
|
|
6
|
-
"projectVersion": "
|
|
7
|
-
"gitCommit": "
|
|
6
|
+
"projectVersion": "2.0.0",
|
|
7
|
+
"gitCommit": "baace9c25621df66a88552dcd1d2f5e63c3e477f",
|
|
8
8
|
"gitDirty": false,
|
|
9
|
-
"builtAt": "2026-09-
|
|
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: '
|
|
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": "
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
130
|
-
// label alone never authorizes a changed grant or endpoint.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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)?;
|
package/rust/src/egress_tests.rs
CHANGED
|
@@ -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::
|
|
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")
|
package/ts/00_commitinfo_data.ts
CHANGED