@push.rocks/smartnftables 2.6.0 → 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 (34) 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 +25 -3
  8. package/package.json +6 -6
  9. package/readme.md +205 -63
  10. package/rust/src/egress.compile.rs +155 -76
  11. package/rust/src/egress.host.rs +24 -51
  12. package/rust/src/egress.hostgrant.rs +1 -1
  13. package/rust/src/egress.keys.rs +305 -0
  14. package/rust/src/egress.leased.rs +399 -0
  15. package/rust/src/egress.private.rs +197 -0
  16. package/rust/src/egress.published.rs +370 -0
  17. package/rust/src/egress.router.rs +61 -294
  18. package/rust/src/egress.rs +126 -11
  19. package/rust/src/egress_hostgrant_tests.rs +149 -18
  20. package/rust/src/egress_localport_tests.rs +3 -1
  21. package/rust/src/egress_publishedrange_tests.rs +812 -0
  22. package/rust/src/egress_routerequivalence_tests.rs +448 -0
  23. package/rust/src/egress_tests.rs +132 -230
  24. package/rust/src/egress_workloadgrant_tests.rs +6 -7
  25. package/rust/src/owner.rs +69 -14
  26. package/rust/src/owner_host_traffic_tests.rs +4 -0
  27. package/rust/src/owner_identity_tests.rs +166 -80
  28. package/rust/src/owner_publishedrange_traffic_tests.rs +306 -0
  29. package/rust/src/owner_router_traffic_tests.rs +2 -0
  30. package/rust/src/owner_scale_traffic_tests.rs +383 -0
  31. package/rust/src/owner_tests.rs +6 -0
  32. package/rust/src/policy.rs +29 -25
  33. package/ts/00_commitinfo_data.ts +1 -1
  34. package/ts/managed.egress.types.ts +25 -3
@@ -55,6 +55,8 @@ pub(super) struct Flow {
55
55
  /// The file system uid of the packet's user socket; None without one, when
56
56
  /// `meta skuid` breaks the rule like the kernel.
57
57
  pub(super) uid: Option<u32>,
58
+ /// The conntrack label of the flow's entry.
59
+ pub(super) label: [u8; 16],
58
60
  }
59
61
  const HOST: [u8; 4] = [10, 240, 0, 1];
60
62
  const WORK: [u8; 4] = [10, 241, 0, 2];
@@ -82,6 +84,7 @@ pub(super) fn opening(
82
84
  zone: 0,
83
85
  original: (HOST, WORK, 443, 8080),
84
86
  uid: None,
87
+ label: [0; 16],
85
88
  }
86
89
  }
87
90
  /// The workload's answer to `flow`, arriving on the reverse links.
@@ -194,11 +197,47 @@ pub(super) struct Decision {
194
197
  pub(super) verdict: i32,
195
198
  pub(super) chain: String,
196
199
  pub(super) zone: u16,
200
+ /// The NAT a nat-chain rule performed, which also ends that rule with an
201
+ /// accept exactly as the kernel's nat expression does.
202
+ pub(super) translation: Option<Translation>,
197
203
  }
198
- /// The compiled table: rules per chain and element keys per set.
204
+ /// Source (0) or destination (1) NAT to an address and, when the rule names
205
+ /// them, an inclusive port range.
206
+ #[derive(Debug, PartialEq, Eq)]
207
+ pub(super) struct Translation {
208
+ pub(super) kind: u32,
209
+ pub(super) address: [u8; 4],
210
+ pub(super) ports: Option<(u16, u16)>,
211
+ }
212
+ /// The compiled table: rules per chain, element keys per exact set, and the
213
+ /// fields and elements of every interval set or map.
199
214
  struct Table {
200
215
  chains: BTreeMap<String, Vec<Vec<Attr>>>,
201
216
  sets: BTreeMap<String, Vec<Vec<u8>>>,
217
+ intervals: BTreeMap<String, Interval>,
218
+ }
219
+ /// Field byte lengths, then `(first, last, data)` per element.
220
+ #[derive(Default)]
221
+ struct Interval {
222
+ fields: Vec<usize>,
223
+ elements: Vec<(Vec<u8>, Vec<u8>, Option<Vec<u8>>)>,
224
+ }
225
+ impl Interval {
226
+ /// Every field of the key within its bounds, compared as the kernel's
227
+ /// pipapo backend does: byte strings of the field's own length.
228
+ fn find(&self, key: &[u8]) -> Option<&(Vec<u8>, Vec<u8>, Option<Vec<u8>>)> {
229
+ self.elements.iter().find(|(first, last, _)| {
230
+ let mut at = 0;
231
+ self.fields.iter().all(|length| {
232
+ let range = at..at + length;
233
+ at += length.div_ceil(4) * 4;
234
+ first[range.clone()] <= key[range.clone()] && key[range.clone()] <= last[range]
235
+ })
236
+ })
237
+ }
238
+ fn width(&self) -> usize {
239
+ self.fields.iter().map(|length| length.div_ceil(4) * 4).sum()
240
+ }
202
241
  }
203
242
  pub(super) fn chains(prepared: &Prepared) -> BTreeMap<String, Vec<Vec<Attr>>> {
204
243
  table(prepared).chains
@@ -206,6 +245,7 @@ pub(super) fn chains(prepared: &Prepared) -> BTreeMap<String, Vec<Vec<Attr>>> {
206
245
  fn table(prepared: &Prepared) -> Table {
207
246
  let mut chains: BTreeMap<String, Vec<Vec<Attr>>> = BTreeMap::new();
208
247
  let mut sets: BTreeMap<String, Vec<Vec<u8>>> = BTreeMap::new();
248
+ let mut intervals: BTreeMap<String, Interval> = BTreeMap::new();
209
249
  for (kind, attributes) in prepared.program("snft_host_grant").unwrap() {
210
250
  match kind {
211
251
  3 => {
@@ -218,21 +258,59 @@ fn table(prepared: &Prepared) -> Table {
218
258
  .or_default()
219
259
  .push(wire::attrs(&wire::one(&attributes, 4).unwrap().value).unwrap()),
220
260
  9 => {
221
- assert!(sets
222
- .insert(wire::text(&attributes, 2).unwrap(), Vec::new())
223
- .is_none());
261
+ let name = wire::text(&attributes, 2).unwrap();
262
+ if let Ok(description) = wire::one(&attributes, 9) {
263
+ let concatenation = wire::attrs(&description.value).unwrap();
264
+ let fields = wire::attrs(&wire::one(&concatenation, 2).unwrap().value)
265
+ .unwrap()
266
+ .iter()
267
+ .map(|field| {
268
+ wire::number(&wire::attrs(&field.value).unwrap(), 1).unwrap() as usize
269
+ })
270
+ .collect();
271
+ assert!(intervals
272
+ .insert(name, Interval { fields, elements: Vec::new() })
273
+ .is_none());
274
+ } else if wire::number(&attributes, 3).unwrap() & 8 != 0 {
275
+ // An exact map is one field of the whole key.
276
+ let fields = vec![wire::number(&attributes, 5).unwrap() as usize];
277
+ assert!(intervals
278
+ .insert(name, Interval { fields, elements: Vec::new() })
279
+ .is_none());
280
+ } else {
281
+ assert!(sets.insert(name, Vec::new()).is_none());
282
+ }
224
283
  }
225
284
  12 => {
226
- let keys = sets.get_mut(&wire::text(&attributes, 2).unwrap()).unwrap();
285
+ let name = wire::text(&attributes, 2).unwrap();
227
286
  for element in wire::attrs(&wire::one(&attributes, 3).unwrap().value).unwrap() {
228
287
  let key = wire::attrs(&element.value).unwrap();
229
- keys.push(data(&key, 1));
288
+ if let Some(interval) = intervals.get_mut(&name) {
289
+ // An element without a last key is one exact key.
290
+ let first = data(&key, 1);
291
+ let last = if wire::one(&key, 10).is_ok() {
292
+ data(&key, 10)
293
+ } else {
294
+ first.clone()
295
+ };
296
+ interval.elements.push((
297
+ first,
298
+ last,
299
+ wire::one(&key, 2).ok().map(|_| data(&key, 2)),
300
+ ));
301
+ } else {
302
+ sets.get_mut(&name).unwrap().push(data(&key, 1));
303
+ }
230
304
  }
231
305
  }
232
306
  other => panic!("operation {other}"),
233
307
  }
234
308
  }
235
- Table { chains, sets }
309
+ Table {
310
+ chains,
311
+ sets,
312
+ intervals,
313
+ }
236
314
  }
237
315
  fn data(attributes: &[Attr], id: u16) -> Vec<u8> {
238
316
  wire::attrs(&wire::one(attributes, id).unwrap().value).unwrap()[0]
@@ -290,7 +368,7 @@ fn load(flow: &Flow, name: &str, fields: &[Attr]) -> Option<Vec<u8>> {
290
368
  10 => vec![flow.protocol],
291
369
  11 => source_port.to_be_bytes().to_vec(),
292
370
  12 => destination_port.to_be_bytes().to_vec(),
293
- 13 => vec![0; 16],
371
+ 13 => flow.label.to_vec(),
294
372
  17 => flow.zone.to_ne_bytes().to_vec(),
295
373
  19 => source.to_vec(),
296
374
  20 => destination.to_vec(),
@@ -321,6 +399,7 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
321
399
  let mut registers = [0_u8; 80];
322
400
  let mut matched = true;
323
401
  let mut verdict: Option<(i32, Option<String>)> = None;
402
+ let mut translation = None;
324
403
  for expression in expressions {
325
404
  let parts = wire::attrs(&expression.value).unwrap();
326
405
  let name = wire::text(&parts, 1).unwrap();
@@ -367,12 +446,29 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
367
446
  matched = loaded >= &first[..] && loaded <= &last[..];
368
447
  }
369
448
  "lookup" => {
370
- assert_eq!(wire::number(&fields, 5).unwrap(), 0);
371
- let keys = &table.sets[&wire::text(&fields, 1).unwrap()];
449
+ let inverted = wire::number(&fields, 5).unwrap() == 1;
450
+ let name = wire::text(&fields, 1).unwrap();
372
451
  let at = offset(&fields, 2);
373
- matched = keys
374
- .iter()
375
- .any(|key| registers[at..at + key.len()] == key[..]);
452
+ if let Some(interval) = table.intervals.get(&name) {
453
+ let key = registers[at..at + interval.width()].to_vec();
454
+ let found = interval.find(&key);
455
+ matched = found.is_some();
456
+ if let (Some((_, _, data)), Ok(_)) = (found, wire::one(&fields, 3)) {
457
+ // A map lookup stores the element's data.
458
+ let data = data.as_ref().unwrap();
459
+ let to = offset(&fields, 3);
460
+ registers[to..to + data.len()].copy_from_slice(data);
461
+ }
462
+ } else {
463
+ let keys = &table.sets[&name];
464
+ matched = keys
465
+ .iter()
466
+ .any(|key| registers[at..at + key.len()] == key[..]);
467
+ }
468
+ if inverted {
469
+ assert!(wire::one(&fields, 3).is_err());
470
+ matched = !matched;
471
+ }
376
472
  }
377
473
  "immediate" if wire::number(&fields, 1).unwrap() == 0 => {
378
474
  let code = wire::attrs(
@@ -394,7 +490,19 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
394
490
  let at = offset(&fields, 1);
395
491
  registers[at..at + value.len()].copy_from_slice(&value);
396
492
  }
397
- "nat" => {}
493
+ "nat" => {
494
+ let at = offset(&fields, 3);
495
+ let port = |id: u16| {
496
+ let at = offset(&fields, id);
497
+ u16::from_be_bytes(registers[at..at + 2].try_into().unwrap())
498
+ };
499
+ translation = Some(Translation {
500
+ kind: wire::number(&fields, 1).unwrap(),
501
+ address: registers[at..at + 4].try_into().unwrap(),
502
+ ports: wire::one(&fields, 5).ok().map(|_| (port(5), port(6))),
503
+ });
504
+ verdict = Some((1, None));
505
+ }
398
506
  "reject" => {
399
507
  assert_eq!(fields, vec![Attr::u32(1, 1)]);
400
508
  verdict = Some((REJECT, None));
@@ -414,6 +522,7 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
414
522
  verdict: code,
415
523
  chain: chain.into(),
416
524
  zone: *zone,
525
+ translation,
417
526
  })
418
527
  }
419
528
  Some((-5, _)) => return Step::Next,
@@ -431,13 +540,34 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
431
540
  }
432
541
  /// A base chain that ends without a terminal verdict accepts by policy.
433
542
  pub(super) fn decide(prepared: &Prepared, chain: &str, flow: &Flow) -> Decision {
543
+ Simulator::new(prepared).decide(chain, flow)
544
+ }
545
+ /// One compiled table, parsed once for many decisions.
546
+ pub(super) struct Simulator {
547
+ table: Table,
548
+ }
549
+ impl Simulator {
550
+ pub(super) fn new(prepared: &Prepared) -> Self {
551
+ Self {
552
+ table: table(prepared),
553
+ }
554
+ }
555
+ pub(super) fn has(&self, chain: &str) -> bool {
556
+ self.table.chains.contains_key(chain)
557
+ }
558
+ pub(super) fn decide(&self, chain: &str, flow: &Flow) -> Decision {
559
+ decide_in(&self.table, chain, flow)
560
+ }
561
+ }
562
+ fn decide_in(table: &Table, chain: &str, flow: &Flow) -> Decision {
434
563
  let mut zone = 0;
435
- match run(&table(prepared), chain, flow, &mut zone) {
564
+ match run(table, chain, flow, &mut zone) {
436
565
  Step::Final(decision) => decision,
437
566
  Step::Next => Decision {
438
567
  verdict: 1,
439
568
  chain: "policy".into(),
440
569
  zone,
570
+ translation: None,
441
571
  },
442
572
  }
443
573
  }
@@ -835,7 +965,8 @@ fn host_grants_compile_a_constant_rule_graph_and_one_element_per_grant() {
835
965
  if let Some(arrival) = all.sets.get("host_grant_arrival") {
836
966
  assert_eq!(arrival.len(), 1024);
837
967
  }
838
- assert!(table(&prepared(fixture)).sets.is_empty());
968
+ let without = table(&prepared(fixture));
969
+ assert!(without.sets.keys().all(|name| !name.starts_with("host_grant")));
839
970
  }
840
971
  }
841
972
 
@@ -917,7 +1048,7 @@ fn host_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
917
1048
  };
918
1049
  let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
919
1050
  assert!(
920
- program.len() < 768 && graph <= 100_000 && elements <= super::compile::ELEMENT_BYTES
1051
+ program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES
921
1052
  );
922
1053
  assert!(deletion + graph + elements + 64 <= crate::wire::BATCH_BYTES);
923
1054
  // The contract maximum is the validation bound.
@@ -929,7 +1060,7 @@ fn host_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
929
1060
  );
930
1061
  }
931
1062
  // The reserve itself: a maximum previous graph beside a maximum target.
932
- assert!(deletion + 100_000 + super::compile::ELEMENT_BYTES + 64 <= crate::wire::BATCH_BYTES);
1063
+ assert!(deletion + super::compile::TARGET_BYTES + 64 <= crate::wire::BATCH_BYTES);
933
1064
  }
934
1065
 
935
1066
  /// A status carries up to three complete policies: the applied one and the
@@ -118,6 +118,7 @@ fn local_tcp_port_owner_rejects_every_other_uid_at_its_exact_loopback_destinatio
118
118
  input,
119
119
  output: None,
120
120
  uid: None,
121
+ label: [0; 16],
121
122
  ..dial(10010, None)
122
123
  };
123
124
  assert_eq!(decide(&with, "input", &arriving(LO)).verdict, 1);
@@ -132,6 +133,7 @@ fn local_tcp_port_owner_rejects_every_other_uid_at_its_exact_loopback_destinatio
132
133
  // Guarded pool traffic from any user keeps the guard's decision.
133
134
  let pool = Flow {
134
135
  uid: Some(1000),
136
+ label: [0; 16],
135
137
  ..opening(None, Some((4, "handoff")))
136
138
  };
137
139
  assert_eq!(
@@ -250,7 +252,7 @@ fn local_tcp_port_owners_meet_the_atomic_reserve_at_the_contract_maximum() {
250
252
  .sum()
251
253
  };
252
254
  let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
253
- assert!(program.len() < 768 && graph <= 100_000 && elements <= super::compile::ELEMENT_BYTES);
255
+ assert!(program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES);
254
256
  assert!(deletion + graph + elements + 64 <= wire::BATCH_BYTES);
255
257
  println!(
256
258
  "LOCAL_TCP_PORT_OWNER_MAX owners=8 prefixes=64 grants=1024 operations={} rule_bytes={graph}",