@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
@@ -0,0 +1,448 @@
1
+ //! Router decision corpus: every hook's verdict, stored zone and NAT for a
2
+ //! deterministic corpus of packets around each fixture's grants, rules and
3
+ //! protection. The digests were frozen from the per-rule router compiler, so the
4
+ //! set-backed compiler must decide every one of these packets identically.
5
+ use super::hostgrant_tests::{Flow, Simulator};
6
+ use super::{Destination, Policy, Prepared, RouterScope, Scope};
7
+ use serde_json::{json, Value};
8
+
9
+ const NEW: u32 = 8;
10
+ const ESTABLISHED: u32 = 2;
11
+ const SYN: u8 = 0x02;
12
+ const ACK: u8 = 0x10;
13
+ const CHAINS: [&str; 7] = [
14
+ "raw_in", "raw_out", "input", "forward", "output", "post", "pre",
15
+ ];
16
+
17
+ /// A small deterministic generator; the corpus must never change between runs.
18
+ struct Random(u64);
19
+ impl Random {
20
+ fn next(&mut self) -> u64 {
21
+ self.0 ^= self.0 << 13;
22
+ self.0 ^= self.0 >> 7;
23
+ self.0 ^= self.0 << 17;
24
+ self.0
25
+ }
26
+ fn pick<T: Copy>(&mut self, items: &[T]) -> T {
27
+ items[(self.next() % items.len() as u64) as usize]
28
+ }
29
+ fn chance(&mut self, percent: u64) -> bool {
30
+ self.next() % 100 < percent
31
+ }
32
+ }
33
+
34
+ type Link = Option<(u32, &'static str)>;
35
+ fn leak(value: String) -> &'static str {
36
+ Box::leak(value.into_boxed_str())
37
+ }
38
+ fn first(prefix: &str) -> [u8; 4] {
39
+ let (address, _) = super::prefix(prefix).unwrap();
40
+ address.to_be_bytes()
41
+ }
42
+ fn last(prefix: &str) -> [u8; 4] {
43
+ let (address, mask) = super::prefix(prefix).unwrap();
44
+ (address | !mask).to_be_bytes()
45
+ }
46
+ fn ip(value: &str) -> [u8; 4] {
47
+ value.parse::<std::net::Ipv4Addr>().unwrap().octets()
48
+ }
49
+ fn label(value: &str) -> [u8; 16] {
50
+ let bytes: Vec<u8> = (0..16)
51
+ .map(|index| u8::from_str_radix(&value[index * 2..index * 2 + 2], 16).unwrap())
52
+ .collect();
53
+ bytes.try_into().unwrap()
54
+ }
55
+
56
+ /// The values a fixture's packets are drawn from, and templates of the flows its
57
+ /// authority names, in both directions.
58
+ struct Pools {
59
+ links: Vec<Link>,
60
+ addresses: Vec<[u8; 4]>,
61
+ ports: Vec<u16>,
62
+ zones: Vec<u16>,
63
+ labels: Vec<[u8; 16]>,
64
+ templates: Vec<Flow>,
65
+ }
66
+ fn flow(
67
+ input: Link,
68
+ output: Link,
69
+ protocol: u8,
70
+ source: ([u8; 4], u16),
71
+ destination: ([u8; 4], u16),
72
+ ) -> Flow {
73
+ Flow {
74
+ input,
75
+ output,
76
+ protocol,
77
+ source: source.0,
78
+ destination: destination.0,
79
+ source_port: source.1,
80
+ destination_port: destination.1,
81
+ tcp_flags: SYN,
82
+ state: NEW,
83
+ reply: false,
84
+ zone: 0,
85
+ original: (source.0, destination.0, source.1, destination.1),
86
+ uid: None,
87
+ label: [0; 16],
88
+ }
89
+ }
90
+ fn answer(flow: Flow) -> Flow {
91
+ Flow {
92
+ input: flow.output,
93
+ output: flow.input,
94
+ source: flow.destination,
95
+ destination: flow.source,
96
+ source_port: flow.destination_port,
97
+ destination_port: flow.source_port,
98
+ tcp_flags: ACK,
99
+ state: ESTABLISHED,
100
+ reply: true,
101
+ ..flow
102
+ }
103
+ }
104
+ fn protocol(name: &str) -> u8 {
105
+ if name == "tcp" {
106
+ 6
107
+ } else {
108
+ 17
109
+ }
110
+ }
111
+ fn pools(scope: &RouterScope) -> Pools {
112
+ let link = |index: u32, name: &str| Some((index, leak(name.to_string())));
113
+ let mut links: Vec<Link> = vec![
114
+ None,
115
+ link(scope.handoff.interface_index, &scope.handoff.interface_name),
116
+ ];
117
+ for item in &scope.links {
118
+ links.push(link(item.interface_index, &item.interface_name));
119
+ // The same index under another name, and the same name at another index.
120
+ links.push(link(item.interface_index, "renamed"));
121
+ links.push(link(item.interface_index + 100, &item.interface_name));
122
+ }
123
+ let endpoint_link = |id: &str| {
124
+ let endpoint = scope.endpoints.iter().find(|item| item.id == id).unwrap();
125
+ link(endpoint.interface_index, &endpoint.interface_name)
126
+ };
127
+ let handoff = link(scope.handoff.interface_index, &scope.handoff.interface_name);
128
+ let mut addresses = vec![
129
+ ip("198.51.100.7"),
130
+ ip("8.8.8.8"),
131
+ ip("10.9.9.9"),
132
+ ip("192.0.2.200"),
133
+ ];
134
+ for item in scope.links.iter().chain(std::iter::once(&scope.handoff)) {
135
+ addresses.extend(item.required_ipv4_addresses.iter().map(|value| ip(value)));
136
+ }
137
+ for endpoint in &scope.endpoints {
138
+ for prefix in &endpoint.source_prefixes {
139
+ addresses.extend([first(prefix), last(prefix)]);
140
+ }
141
+ }
142
+ for prefix in &scope.protection.prefixes {
143
+ addresses.extend([first(prefix), last(prefix)]);
144
+ }
145
+ for endpoint in &scope.protection.platform_endpoints {
146
+ addresses.push(ip(&endpoint.address));
147
+ }
148
+ let mut ports = vec![53, 443, 3000, 5353, 8080, 40000, 65535];
149
+ let mut zones = vec![0, 7];
150
+ let mut labels = vec![[0; 16], [0x55; 16]];
151
+ let mut templates = Vec::new();
152
+ for generation in &scope.generations {
153
+ zones.push(generation.conntrack_zone);
154
+ labels.push(label(&generation.conntrack_label));
155
+ let transit = ip(&generation.transit_source_address);
156
+ for range in &generation.source_port_ranges {
157
+ ports.extend([
158
+ range.first,
159
+ range.last,
160
+ range.first.saturating_sub(1),
161
+ range.last.saturating_add(1),
162
+ ]);
163
+ }
164
+ for grant in &generation.grants {
165
+ let target = match &grant.destination {
166
+ Destination::Public { prefix } => {
167
+ if prefix == "0.0.0.0/0" {
168
+ ip("198.51.100.7")
169
+ } else {
170
+ first(prefix)
171
+ }
172
+ }
173
+ Destination::Platform { endpoint_id } => ip(&scope
174
+ .protection
175
+ .platform_endpoints
176
+ .iter()
177
+ .find(|item| item.id == *endpoint_id)
178
+ .unwrap()
179
+ .address),
180
+ };
181
+ let source = first(&grant.source_prefix);
182
+ let input = grant
183
+ .source_endpoint
184
+ .as_deref()
185
+ .map(endpoint_link)
186
+ .unwrap_or(None);
187
+ let original = flow(
188
+ input,
189
+ handoff,
190
+ protocol(&grant.protocol),
191
+ (source, grant.source_port.unwrap_or(40000)),
192
+ (target, grant.destination_port.unwrap_or(443)),
193
+ );
194
+ let zoned = Flow {
195
+ zone: generation.conntrack_zone,
196
+ ..original
197
+ };
198
+ let labelled = Flow {
199
+ label: label(&generation.conntrack_label),
200
+ ..zoned
201
+ };
202
+ templates.extend([original, zoned, labelled, answer(labelled)]);
203
+ // The translated return arriving on the handoff, before conntrack.
204
+ templates.push(flow(
205
+ handoff,
206
+ None,
207
+ protocol(&grant.protocol),
208
+ (target, grant.destination_port.unwrap_or(443)),
209
+ (transit, grant.source_port_range.first),
210
+ ));
211
+ }
212
+ }
213
+ for rule in &scope.rules {
214
+ let input = rule
215
+ .source_endpoint
216
+ .as_deref()
217
+ .map(endpoint_link)
218
+ .unwrap_or(None);
219
+ let output = rule
220
+ .destination_endpoint
221
+ .as_deref()
222
+ .map(endpoint_link)
223
+ .unwrap_or(None);
224
+ let protocols: Vec<u8> = if rule.protocol == "any" {
225
+ vec![6, 17, 1]
226
+ } else {
227
+ vec![protocol(&rule.protocol)]
228
+ };
229
+ for value in protocols {
230
+ let template = flow(
231
+ input,
232
+ output,
233
+ value,
234
+ (
235
+ first(&rule.source_prefix),
236
+ rule.source_port.unwrap_or(40000),
237
+ ),
238
+ (
239
+ first(&rule.destination_prefix),
240
+ rule.destination_port.unwrap_or(40001),
241
+ ),
242
+ );
243
+ templates.extend([template, answer(template)]);
244
+ }
245
+ }
246
+ for grant in scope.host_grants.iter().chain(&scope.workload_grants) {
247
+ ports.push(grant.destination_port);
248
+ let template = flow(
249
+ handoff,
250
+ links[3],
251
+ protocol(&grant.protocol),
252
+ (ip(&grant.source_address), 40000),
253
+ (ip(&grant.destination_address), grant.destination_port),
254
+ );
255
+ templates.extend([template, answer(template)]);
256
+ }
257
+ Pools {
258
+ links,
259
+ addresses,
260
+ ports,
261
+ zones,
262
+ labels,
263
+ templates,
264
+ }
265
+ }
266
+ /// A template changed in up to three operands, or an arbitrary packet.
267
+ fn packet(random: &mut Random, pools: &Pools) -> Flow {
268
+ let mut flow = if random.chance(80) {
269
+ random.pick(&pools.templates)
270
+ } else {
271
+ flow(
272
+ random.pick(&pools.links),
273
+ random.pick(&pools.links),
274
+ random.pick(&[6, 17]),
275
+ (random.pick(&pools.addresses), random.pick(&pools.ports)),
276
+ (random.pick(&pools.addresses), random.pick(&pools.ports)),
277
+ )
278
+ };
279
+ for _ in 0..random.next() % 4 {
280
+ match random.next() % 14 {
281
+ 0 => flow.input = random.pick(&pools.links),
282
+ 1 => flow.output = random.pick(&pools.links),
283
+ 2 => flow.source = random.pick(&pools.addresses),
284
+ 3 => flow.destination = random.pick(&pools.addresses),
285
+ 4 => flow.source_port = random.pick(&pools.ports),
286
+ 5 => flow.destination_port = random.pick(&pools.ports),
287
+ 6 => flow.protocol = random.pick(&[6, 17, 1]),
288
+ 7 => flow.state = random.pick(&[NEW, ESTABLISHED]),
289
+ 8 => flow.reply = !flow.reply,
290
+ 9 => flow.zone = random.pick(&pools.zones),
291
+ 10 => flow.label = random.pick(&pools.labels),
292
+ 11 => flow.tcp_flags = random.pick(&[SYN, ACK, SYN | ACK, 0x04, 0x42]),
293
+ 12 => {
294
+ flow.original.1 = random.pick(&pools.addresses);
295
+ flow.original.3 = random.pick(&pools.ports);
296
+ }
297
+ _ => {
298
+ flow.original.0 = random.pick(&pools.addresses);
299
+ flow.original.2 = random.pick(&pools.ports);
300
+ }
301
+ }
302
+ }
303
+ flow
304
+ }
305
+ /// Linux runs each hook with exactly these interfaces: prerouting and input with
306
+ /// an incoming one only, output with an outgoing one only, forward with both, and
307
+ /// postrouting with an outgoing one.
308
+ fn possible(chain: &str, flow: &Flow) -> bool {
309
+ let (incoming, outgoing) = (flow.input.is_some(), flow.output.is_some());
310
+ match chain {
311
+ "raw_in" | "pre" | "input" => incoming && !outgoing,
312
+ "raw_out" | "output" => !incoming && outgoing,
313
+ "forward" => incoming && outgoing,
314
+ _ => outgoing,
315
+ }
316
+ }
317
+ fn prepared(value: Value) -> Prepared {
318
+ serde_json::from_value::<Policy>(value)
319
+ .unwrap()
320
+ .prepare()
321
+ .unwrap()
322
+ }
323
+ /// Every decision of the corpus, as `chain verdict zone translation` lines.
324
+ fn decisions(prepared: &Prepared, seed: u64, count: usize) -> Vec<String> {
325
+ let Scope::RouterEgress(scope) = &prepared.policy.scope else {
326
+ panic!("router fixture")
327
+ };
328
+ let pools = pools(scope);
329
+ let simulator = Simulator::new(prepared);
330
+ let mut random = Random(seed);
331
+ let mut lines = Vec::new();
332
+ for _ in 0..count {
333
+ let flow = packet(&mut random, &pools);
334
+ for chain in CHAINS {
335
+ if !simulator.has(chain) || !possible(chain, &flow) {
336
+ continue;
337
+ }
338
+ // Postrouting only ever sees what forward or output let through.
339
+ let filter = if flow.input.is_some() {
340
+ "forward"
341
+ } else {
342
+ "output"
343
+ };
344
+ if chain == "post" && simulator.decide(filter, &flow).verdict != 1 {
345
+ continue;
346
+ }
347
+ let decision = simulator.decide(chain, &flow);
348
+ lines.push(format!(
349
+ "{chain} {} {} {} {:?}",
350
+ decision.verdict,
351
+ decision.chain == "policy",
352
+ decision.zone,
353
+ decision.translation
354
+ ));
355
+ }
356
+ }
357
+ lines
358
+ }
359
+ fn two_generations() -> Value {
360
+ let mut value = super::tests::router();
361
+ let mut second = value["scope"]["generations"][0].clone();
362
+ second["lease"]["id"] = json!("lease-b");
363
+ second["transitSourceAddress"] = json!("10.240.0.2");
364
+ second["conntrackZone"] = json!(2);
365
+ second["conntrackLabel"] = json!("b".repeat(32));
366
+ second["sourcePortRanges"] = json!([{"protocol":"tcp","first":11000,"last":11999},
367
+ {"protocol":"udp","first":11000,"last":11999}]);
368
+ second["grants"] = json!([
369
+ {"sourceEndpoint":"work","sourcePrefix":"10.241.0.2/32","destination":{"kind":"public","prefix":"198.51.100.0/24"},
370
+ "protocol":"udp","sourcePort":null,"destinationPort":5353,"sourcePortRange":{"first":11000,"last":11999}}
371
+ ]);
372
+ value["scope"]["generations"]
373
+ .as_array_mut()
374
+ .unwrap()
375
+ .push(second);
376
+ value
377
+ }
378
+ fn fixtures() -> Vec<(&'static str, Value)> {
379
+ let grants = super::hostgrant_tests::granted(
380
+ super::workloadgrant_tests::granted(
381
+ super::workloadgrant_tests::two_workloads(),
382
+ json!([super::workloadgrant_tests::workload_grant(
383
+ "tcp",
384
+ "10.241.0.2",
385
+ "10.241.1.2",
386
+ 8080
387
+ )]),
388
+ ),
389
+ json!([super::hostgrant_tests::host_grant(
390
+ "tcp",
391
+ "10.240.0.1",
392
+ "10.241.0.2",
393
+ 8080
394
+ )]),
395
+ );
396
+ let (node, _) = super::publishedrange_tests::node(3, true, 0, 0, false);
397
+ vec![
398
+ ("router", super::tests::router()),
399
+ (
400
+ "combined_capacity",
401
+ super::tests::combined_router_capacity(),
402
+ ),
403
+ ("two_generations", two_generations()),
404
+ ("grants", grants),
405
+ ("pallet_3", node),
406
+ ]
407
+ }
408
+
409
+ #[test]
410
+ fn router_decisions_match_the_frozen_per_rule_corpus() {
411
+ use sha2::{Digest, Sha256};
412
+ let mut summary = Vec::new();
413
+ for (name, value) in fixtures() {
414
+ let lines = decisions(&prepared(value), 0x9e37_79b9_7f4a_7c15, 16000);
415
+ let accepted = lines
416
+ .iter()
417
+ .filter(|line| {
418
+ line.split(' ').nth(1) == Some("1") && line.split(' ').nth(2) == Some("false")
419
+ })
420
+ .count();
421
+ let digest = format!("{:x}", Sha256::digest(lines.join("\n")));
422
+ if let Ok(directory) = std::env::var("SMARTNFTABLES_CORPUS_DIR") {
423
+ std::fs::write(format!("{directory}/{name}.txt"), lines.join("\n")).unwrap();
424
+ }
425
+ println!(
426
+ "ROUTER_CORPUS {name} decisions={} accepted={accepted} sha256={digest}",
427
+ lines.len()
428
+ );
429
+ summary.push(format!("{name} {digest}"));
430
+ }
431
+ assert_eq!(
432
+ summary,
433
+ FROZEN
434
+ .iter()
435
+ .map(|item| item.to_string())
436
+ .collect::<Vec<_>>()
437
+ );
438
+ }
439
+ /// Frozen from the per-rule router compiler of 2.6.0 (with this change's
440
+ /// simulator, which models hooks, interval sets, maps and inverted lookups), and
441
+ /// reproduced unchanged by the set-backed compiler.
442
+ const FROZEN: [&str; 5] = [
443
+ "router d9a005d2c0ce7ceb6b6a6de91ff087fc4802c6fb2100a9a9ea7ad9e01cc131db",
444
+ "combined_capacity c2c055acc2b7c68570454289a6c150dbb29a9dd1de3b3fecda79b096430900f4",
445
+ "two_generations b6750e4930be4cb2713a812d56624f410447c0d57eeb0111136054b43e48d814",
446
+ "grants 434776a2946b64fd27fb595f792a96cbdb6f6684d2ace15f57cdaec22eee6557",
447
+ "pallet_3 7d2d06280b903bbfba712eb1a55d62270f0c0b473597b5940fcb4d91b0d5c325",
448
+ ];