@push.rocks/smartnftables 2.5.1 → 2.6.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 (65) hide show
  1. package/changelog.md +19 -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 +44 -0
  8. package/native-notices/crates/memchr-2.8.3/unlicense +24 -0
  9. package/native-notices/manifest.json +566 -344
  10. package/native-notices/readme.md +40 -34
  11. package/native-notices/runtime/gcc/gpl-3.0.txt +674 -0
  12. package/native-notices/runtime/gcc/runtime-library-exception-3.1.txt +73 -0
  13. package/native-notices/{rust-standard-library/dependency-notices.html → rust/copyright-library.html} +185 -991
  14. package/native-notices/rust/crates/adler2-2.0.1/license-0bsd +12 -0
  15. package/native-notices/rust/crates/adler2-2.0.1/license-apache +201 -0
  16. package/native-notices/rust/crates/adler2-2.0.1/license-mit +23 -0
  17. package/native-notices/rust/crates/memchr-2.7.6/copying +3 -0
  18. package/native-notices/rust/crates/memchr-2.7.6/license-mit +21 -0
  19. package/native-notices/rust/crates/memchr-2.7.6/unlicense +24 -0
  20. package/native-notices/rust/crates/miniz_oxide-0.8.9/license +25 -0
  21. package/native-notices/{rust-standard-library/llvm-libunwind-license.txt → rust/crates/miniz_oxide-0.8.9/license-apache.md} +11 -145
  22. package/native-notices/rust/crates/miniz_oxide-0.8.9/license-mit.md +24 -0
  23. package/native-notices/rust/crates/miniz_oxide-0.8.9/license-zlib.md +14 -0
  24. package/native-notices/{rust-standard-library → rust/crates/object-0.37.3}/license-apache +25 -0
  25. package/native-notices/{rust-standard-library → rust/crates/object-0.37.3}/license-mit +1 -1
  26. package/native-notices/rust/licenses/apache-2.0.txt +73 -0
  27. package/native-notices/rust/licenses/bsd-2-clause.txt +9 -0
  28. package/native-notices/rust/licenses/cc-by-sa-4.0.txt +427 -0
  29. package/native-notices/rust/licenses/gcc-exception-3.1.txt +30 -0
  30. package/native-notices/rust/licenses/gpl-2.0-only.txt +133 -0
  31. package/native-notices/rust/licenses/gpl-3.0-or-later.txt +202 -0
  32. package/native-notices/rust/licenses/isc.txt +7 -0
  33. package/native-notices/rust/licenses/llvm-exception.txt +15 -0
  34. package/native-notices/rust/licenses/mit.txt +9 -0
  35. package/native-notices/rust/licenses/ncsa.txt +29 -0
  36. package/native-notices/rust/licenses/ofl-1.1.txt +43 -0
  37. package/native-notices/{rust-standard-library/unicode-license.txt → rust/licenses/unicode-3.0.txt} +1 -1
  38. package/package.json +4 -5
  39. package/readme.md +149 -5
  40. package/rust/src/egress.compile.rs +2 -0
  41. package/rust/src/egress.host.rs +88 -1
  42. package/rust/src/egress.hostgrant.rs +13 -13
  43. package/rust/src/egress.poolguard.rs +45 -0
  44. package/rust/src/egress.router.rs +3 -1
  45. package/rust/src/egress.rs +122 -6
  46. package/rust/src/egress.workloadgrant.rs +88 -0
  47. package/rust/src/egress_hostgrant_tests.rs +47 -29
  48. package/rust/src/egress_localplatform_tests.rs +204 -0
  49. package/rust/src/egress_localport_tests.rs +264 -0
  50. package/rust/src/egress_workloadgrant_tests.rs +314 -0
  51. package/rust/src/owner.rs +8 -1
  52. package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
  53. package/rust/src/owner_identity_tests.rs +62 -0
  54. package/rust/src/owner_localplatform_tests.rs +274 -0
  55. package/rust/src/owner_localport_tests.rs +241 -0
  56. package/rust/src/owner_tests.rs +9 -0
  57. package/rust/src/owner_workloadgrant_tests.rs +278 -0
  58. package/ts/00_commitinfo_data.ts +1 -1
  59. package/ts/managed.egress.types.ts +46 -0
  60. package/native-notices/rust-standard-library/cargo.lock +0 -522
  61. package/scripts/check-native-notices.mjs +0 -27
  62. /package/native-notices/{rust-standard-library/musl-1.2.5-copyright.txt → runtime/musl-1.2.5/copyright} +0 -0
  63. /package/native-notices/{rust-standard-library → rust}/compiler-builtins-license.txt +0 -0
  64. /package/native-notices/{rust-standard-library/addr2line-0.25.1-license-apache → rust/crates/addr2line-0.25.1/license-apache} +0 -0
  65. /package/native-notices/{rust-standard-library/addr2line-0.25.1-license-mit → rust/crates/addr2line-0.25.1/license-mit} +0 -0
@@ -0,0 +1,278 @@
1
+ use super::egress_traffic_tests::{
2
+ allocation, binding, connect, denied, ns_ip, protection, roundtrip,
3
+ };
4
+ use super::hostgrant_traffic_tests::Hop;
5
+ use super::router_traffic_tests::connect_from;
6
+ use super::*;
7
+ use crate::egress;
8
+ use serde_json::{json, Value};
9
+ use std::io::{Read, Write};
10
+ use std::net::{TcpListener, TcpStream, UdpSocket};
11
+ use std::time::Duration;
12
+
13
+ const A: &str = "wg_a";
14
+ const B: &str = "wg_b";
15
+ const C: &str = "wg_c";
16
+ const ROUTER: &str = "wg_router";
17
+ const HOST: &str = "wg_host";
18
+
19
+ fn endpoint(name: &str, prefix: &str, link: &Value) -> Value {
20
+ json!({"id":name,"interfaceIndex":link["interfaceIndex"],"interfaceName":name,
21
+ "interfaceKind":"veth","sourcePrefixes":[prefix]})
22
+ }
23
+ /// Three workloads behind the router, each with leased public egress on every
24
+ /// port of both protocols, exactly as a node composes it: a leased classifier
25
+ /// would claim a workload's packets if grants were not default-zone flows.
26
+ fn router_policy(revision: u64, granted: bool) -> egress::Policy {
27
+ let links: Vec<_> = [
28
+ ("wka", "10.241.0.1"),
29
+ ("wkb", "10.241.1.1"),
30
+ ("wkc", "10.241.2.1"),
31
+ ]
32
+ .into_iter()
33
+ .map(|(name, address)| binding(name, address, "veth"))
34
+ .collect();
35
+ let mut generation = allocation();
36
+ generation["conntrackZone"] = json!(17);
37
+ generation["conntrackLabel"] = json!("a".repeat(32));
38
+ let mut grants = Vec::new();
39
+ for (name, source) in [
40
+ ("wka", "10.241.0.2"),
41
+ ("wkb", "10.241.1.2"),
42
+ ("wkc", "10.241.2.2"),
43
+ ] {
44
+ for (protocol, first, last) in [("udp", 10000, 10015), ("tcp", 10016, 10031)] {
45
+ grants.push(json!({"sourceEndpoint":name,"sourcePrefix":format!("{source}/32"),
46
+ "destination":{"kind":"public","prefix":"0.0.0.0/0"},"protocol":protocol,
47
+ "sourcePort":null,"destinationPort":null,"sourcePortRange":{"first":first,"last":last}}));
48
+ }
49
+ }
50
+ generation["grants"] = json!(grants);
51
+ let workload_grants = if granted {
52
+ json!([
53
+ {"protocol":"tcp","sourceAddress":"10.241.0.2","destinationAddress":"10.241.1.2","destinationPort":8080},
54
+ {"protocol":"udp","sourceAddress":"10.241.0.2","destinationAddress":"10.241.1.2","destinationPort":5353}
55
+ ])
56
+ } else {
57
+ json!([])
58
+ };
59
+ serde_json::from_value(json!({"schemaVersion":2,"revision":revision,"scope":{"kind":"routerEgress",
60
+ "endpoints":[endpoint("wka","10.241.0.2/32",&links[0]),endpoint("wkb","10.241.1.2/32",&links[1]),
61
+ endpoint("wkc","10.241.2.2/32",&links[2])],
62
+ "rules":[],"links":links,"handoff":binding("handoff","10.240.0.2","veth"),
63
+ "protection":protection(),"generations":[generation],"workloadGrants":workload_grants}}))
64
+ .unwrap()
65
+ }
66
+ fn udp(namespace: &str, address: &str) -> UdpSocket {
67
+ let address = address.to_owned();
68
+ let socket = in_namespace(namespace, move || UdpSocket::bind(address).unwrap());
69
+ socket
70
+ .set_read_timeout(Some(Duration::from_millis(250)))
71
+ .unwrap();
72
+ socket
73
+ }
74
+ /// A TCP flow from an exact source; on success the listener observes it and
75
+ /// data crosses both ways.
76
+ fn dial(
77
+ namespace: &str,
78
+ source: &str,
79
+ destination: &str,
80
+ listener: &TcpListener,
81
+ ) -> Option<(TcpStream, TcpStream)> {
82
+ let (from, to) = (source.to_owned(), destination.to_owned());
83
+ let client = in_namespace(namespace, move || connect_from(&from, &to)).ok()?;
84
+ let (server, peer) = listener.accept().unwrap();
85
+ assert_eq!(peer.to_string(), source);
86
+ let (mut client, mut server) = (client, server);
87
+ for stream in [&client, &server] {
88
+ stream
89
+ .set_read_timeout(Some(Duration::from_secs(1)))
90
+ .unwrap();
91
+ }
92
+ client.write_all(b"ping").unwrap();
93
+ server.read_exact(&mut [0; 4]).unwrap();
94
+ server.write_all(b"pong").unwrap();
95
+ client.read_exact(&mut [0; 4]).unwrap();
96
+ Some((client, server))
97
+ }
98
+
99
+ #[test]
100
+ #[ignore = "requires disposable isolated native qualification guest"]
101
+ fn v2_workload_grant_opens_one_way_between_workloads_and_never_in_reverse() {
102
+ isolated();
103
+ for name in [A, B, C, ROUTER, HOST] {
104
+ ip(&["netns", "add", name]);
105
+ ns_ip(name, &["link", "set", "lo", "up"]);
106
+ }
107
+ in_namespace(ROUTER, || {
108
+ std::fs::write("/proc/sys/net/ipv4/ip_forward", "1").unwrap()
109
+ });
110
+ for (namespace, name, octet) in [(A, "wka", 0), (B, "wkb", 1), (C, "wkc", 2)] {
111
+ connect(
112
+ namespace,
113
+ "eth0",
114
+ &format!("10.241.{octet}.2/24"),
115
+ ROUTER,
116
+ name,
117
+ &format!("10.241.{octet}.1/24"),
118
+ );
119
+ ns_ip(
120
+ namespace,
121
+ &[
122
+ "route",
123
+ "add",
124
+ "default",
125
+ "via",
126
+ &format!("10.241.{octet}.1"),
127
+ ],
128
+ );
129
+ }
130
+ connect(
131
+ ROUTER,
132
+ "handoff",
133
+ "10.240.0.2/30",
134
+ HOST,
135
+ "router",
136
+ "10.240.0.1/30",
137
+ );
138
+ let granted = in_namespace(B, || TcpListener::bind("10.241.1.2:8080").unwrap());
139
+ let other = in_namespace(B, || TcpListener::bind("10.241.1.2:8081").unwrap());
140
+ let third = in_namespace(C, || TcpListener::bind("10.241.2.2:8080").unwrap());
141
+ let source = in_namespace(A, || TcpListener::bind("10.241.0.2:9000").unwrap());
142
+ let served = udp(B, "10.241.1.2:5353");
143
+ let other_served = udp(B, "10.241.1.2:5354");
144
+ // Each phase uses its own client ports; TCP ports that closed stay in TIME_WAIT
145
+ // and an unreplied UDP entry would carry a decision over a policy change.
146
+ let exact = |phase: u16| {
147
+ let result = dial(
148
+ A,
149
+ &format!("10.241.0.2:{}", 40000 + phase),
150
+ "10.241.1.2:8080",
151
+ &granted,
152
+ );
153
+ let client = udp(A, &format!("10.241.0.2:{}", 41000 + phase));
154
+ (result, client)
155
+ };
156
+ let refused = |phase: u16| {
157
+ // Another port, another source workload, another destination workload.
158
+ assert!(dial(
159
+ A,
160
+ &format!("10.241.0.2:{}", 42000 + phase),
161
+ "10.241.1.2:8081",
162
+ &other
163
+ )
164
+ .is_none());
165
+ assert!(dial(
166
+ C,
167
+ &format!("10.241.2.2:{}", 40000 + phase),
168
+ "10.241.1.2:8080",
169
+ &granted
170
+ )
171
+ .is_none());
172
+ assert!(dial(
173
+ A,
174
+ &format!("10.241.0.2:{}", 43000 + phase),
175
+ "10.241.2.2:8080",
176
+ &third
177
+ )
178
+ .is_none());
179
+ // No reverse opening: the destination cannot open toward the source.
180
+ assert!(dial(
181
+ B,
182
+ &format!("10.241.1.2:{}", 44000 + phase),
183
+ "10.241.0.2:9000",
184
+ &source
185
+ )
186
+ .is_none());
187
+ let reverse = udp(B, &format!("10.241.1.2:{}", 45000 + phase));
188
+ denied(
189
+ &reverse,
190
+ &udp(A, &format!("10.241.0.2:{}", 46000 + phase)),
191
+ false,
192
+ );
193
+ denied(
194
+ &udp(A, &format!("10.241.0.2:{}", 47000 + phase)),
195
+ &other_served,
196
+ false,
197
+ );
198
+ };
199
+ // Positive controls without policy: every probe below can succeed.
200
+ let (flow, client) = exact(0);
201
+ assert!(flow.is_some());
202
+ roundtrip(&client, &served);
203
+ assert!(dial(A, "10.241.0.2:42000", "10.241.1.2:8081", &other).is_some());
204
+ assert!(dial(C, "10.241.2.2:40000", "10.241.1.2:8080", &granted).is_some());
205
+ assert!(dial(A, "10.241.0.2:43000", "10.241.2.2:8080", &third).is_some());
206
+ assert!(dial(B, "10.241.1.2:44000", "10.241.0.2:9000", &source).is_some());
207
+ roundtrip(&udp(B, "10.241.1.2:45000"), &udp(A, "10.241.0.2:46000"));
208
+ roundtrip(&udp(A, "10.241.0.2:47000"), &other_served);
209
+
210
+ let mut router = Hop::new(ROUTER, "workload_grant_router");
211
+ // Without grants the private endpoint denials refuse every workload flow.
212
+ router.apply(router_policy, 1, false);
213
+ let (flow, client) = exact(1);
214
+ assert!(flow.is_none());
215
+ denied(&client, &served, false);
216
+ refused(1);
217
+ // With them, exactly the source's openings and their answers pass.
218
+ router.apply(router_policy, 2, true);
219
+ let (flow, client) = exact(2);
220
+ let (mut established, mut accepted) = flow.expect("granted workload tcp");
221
+ let observed = roundtrip(&client, &served);
222
+ assert_eq!(observed.to_string(), "10.241.0.2:41002");
223
+ refused(2);
224
+ // Withdrawal stops new flows and the established one.
225
+ router.apply(router_policy, 3, false);
226
+ let (flow, client) = exact(3);
227
+ assert!(flow.is_none());
228
+ denied(&client, &served, false);
229
+ established.write_all(b"late").unwrap();
230
+ assert!(accepted.read_exact(&mut [0; 4]).is_err());
231
+ drop((established, accepted));
232
+ router.release();
233
+ // Release control.
234
+ let (flow, client) = exact(4);
235
+ assert!(flow.is_some());
236
+ roundtrip(&client, &served);
237
+ println!("V2_WORKLOAD_GRANT_PROOF positive_controls=true ungranted_dark=true exact_tcp=true exact_udp=true source_preserved=true leased_egress_unaffected=true other_port_denied=true other_source_denied=true other_destination_denied=true reverse_tcp_denied=true reverse_udp_denied=true withdrawn_dark=true established_withdrawn=true release_control=true");
238
+ }
239
+
240
+ /// A full grant set of 1024 persists across owner loss, is re-verified element
241
+ /// by element before adoption, and is replaced and released like the rest.
242
+ #[test]
243
+ #[ignore = "requires disposable isolated native qualification guest"]
244
+ fn v2_workload_grant_set_persist_and_lost_ack_recovery() {
245
+ isolated();
246
+ // Peers live in another namespace, as the bound veth facts expect.
247
+ ip(&["netns", "add", HOST]);
248
+ for (name, address) in [
249
+ ("wka", "10.241.0.1/24"),
250
+ ("wkb", "10.241.1.1/24"),
251
+ ("wkc", "10.241.2.1/24"),
252
+ ("handoff", "10.240.0.2/30"),
253
+ ] {
254
+ ip(&[
255
+ "link", "add", name, "type", "veth", "peer", "name", name, "netns", HOST,
256
+ ]);
257
+ ip(&["address", "add", address, "dev", name]);
258
+ ip(&["link", "set", name, "up"]);
259
+ }
260
+ let mut policy = router_policy(1, true);
261
+ let egress::Scope::RouterEgress(scope) = &mut policy.scope else {
262
+ panic!()
263
+ };
264
+ scope.workload_grants = (0..1024_u16)
265
+ .map(|index| egress::WorkloadGrant {
266
+ protocol: if index % 2 == 0 { "tcp" } else { "udp" }.into(),
267
+ source_address: "10.241.0.2".into(),
268
+ destination_address: if index < 512 {
269
+ "10.241.1.2"
270
+ } else {
271
+ "10.241.2.2"
272
+ }
273
+ .into(),
274
+ destination_port: 20000 + index,
275
+ })
276
+ .collect();
277
+ super::egress_tests::recover("workload_grant_v2", policy);
278
+ }
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '2.5.1',
6
+ version: '2.6.0',
7
7
  description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
8
8
  }
@@ -88,6 +88,22 @@ export interface IManagedNftHostGrant {
88
88
  destinationPort: number;
89
89
  }
90
90
 
91
+ /** One stateful one-way flow between two workloads behind the same router: the source workload opens
92
+ * connections to one exact port of the destination workload, which may only answer. Both addresses are
93
+ * exact current addresses of two different veth workload endpoints of the scope. It admits the tuple
94
+ * (a TCP opening only with SYN) and its ESTABLISHED replies in the default conntrack zone and nothing
95
+ * else: no reverse opening, no translation and no source-port selection. Grant the reverse direction as
96
+ * a second grant. */
97
+ export interface IManagedNftWorkloadGrant {
98
+ protocol: 'tcp' | 'udp';
99
+ /** Exact unicast address of the dialling workload endpoint, never a prefix. */
100
+ sourceAddress: string;
101
+ /** Exact unicast address of the dialled workload endpoint, never a prefix. */
102
+ destinationAddress: string;
103
+ /** The destination workload's exact listening port, 1–65535; never a range. */
104
+ destinationPort: number;
105
+ }
106
+
91
107
  /** One combined private/TUN/local/egress table; never compose a second ACCEPT over v1 terminal drops. */
92
108
  export interface IManagedNftRouterEgressScopeV2 {
93
109
  kind: 'routerEgress';
@@ -111,6 +127,10 @@ export interface IManagedNftRouterEgressScopeV2 {
111
127
  * workload. Absent and empty are the same canonical policy, digest and compiled graph; at most 1024,
112
128
  * held in a named set behind a constant number of rules. */
113
129
  hostGrants?: IManagedNftHostGrant[];
130
+ /** Optional stateful one-way workload-to-workload grants. Absent and empty are the same canonical
131
+ * policy, digest and compiled graph; at most 1024, held in two named sets behind four rules. They
132
+ * share the scope's set element budget with `hostGrants`. */
133
+ workloadGrants?: IManagedNftWorkloadGrant[];
114
134
  }
115
135
 
116
136
  /** One inbound uplink publication, compiled inside the same host-transit generation. */
@@ -143,6 +163,28 @@ export interface IManagedNftHostTransitScopeV2 {
143
163
  * host-local, a platform endpoint nor a leased transit source address. Absent and empty are the
144
164
  * same canonical policy; at most 1024, held in a named set behind a constant number of rules. */
145
165
  hostGrants?: IManagedNftHostGrant[];
166
+ /** Optional ids of `protection.platformEndpoints` this host serves itself, on an exact current address
167
+ * of the uplink or of a handoff link (rtnetlink verifies it at apply, recovery and inspection). Only a
168
+ * declared endpoint may hold a host address. Leased flows reach it from their exact handoff with the
169
+ * leased source address and a source port of the allocation's range for the endpoint's protocol; its
170
+ * ESTABLISHED replies return. Absent and empty are the same canonical policy, digest and compiled graph. */
171
+ localPlatformEndpoints?: string[];
172
+ }
173
+
174
+ /** One loopback TCP service that only one local user may dial. Every packet the host sends to the
175
+ * exact address and port from a socket of any other user is rejected with a TCP reset, after ordinary
176
+ * output destination NAT; the address and port arriving on any interface but loopback are dropped.
177
+ * The service must bind exactly this address: a wildcard listener stays reachable through others. */
178
+ export interface IManagedNftLocalTcpPortOwner {
179
+ /** Exact IPv4 loopback host address in 127.0.0.0/8, e.g. `127.0.0.1`. Never a prefix or wildcard. */
180
+ address: string;
181
+ /** The service's exact listening port, 1–65535; never a range. */
182
+ port: number;
183
+ /** The only user id whose sockets may send to the port: the socket file's file-system uid as
184
+ * the user namespace owning the network namespace sees it, 0–4294967294. Packets without a socket file carry no uid and
185
+ * are not matched: kernel replies, the teardown of an orphaned socket, and in-kernel sockets such as NFS or CIFS clients,
186
+ * which only privileged mounts create. */
187
+ uid: number;
146
188
  }
147
189
 
148
190
  /** Host-wide IPv4 destination denial for caller-authenticated private allocation pools.
@@ -156,6 +198,10 @@ export interface IManagedNftAllocationPoolGuardScopeV2 {
156
198
  * listed pool, and their replies. Absent and empty are the same canonical policy; at most 1024,
157
199
  * held in a named set behind a constant number of rules. */
158
200
  hostGrants?: IManagedNftHostGrant[];
201
+ /** Optional host-wide loopback port ownership, compiled ahead of the guard in the same table.
202
+ * Absent and empty are the same canonical policy, digest and compiled graph; at most 8, one per
203
+ * exact address and port. */
204
+ localTcpPortOwners?: IManagedNftLocalTcpPortOwner[];
159
205
  }
160
206
 
161
207
  export interface IManagedNftPolicyV2 {