@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
@@ -281,14 +281,10 @@ fn prepared_v2_compiles_both_scopes_with_bounded_and_identifiable_graphs() {
281
281
  prepared.validate().unwrap();
282
282
  let program = prepared.program("snft_v2_fixture").unwrap();
283
283
  assert!(program.len() <= 768);
284
- assert!(
285
- program
286
- .iter()
287
- .map(|(_, attributes)| crate::wire::encode_attrs(attributes).len() + 20)
288
- .sum::<usize>()
289
- <= 100_000
290
- );
291
- assert!(program.iter().all(|(kind, _)| [3, 6].contains(kind)));
284
+ let (_, rule_bytes, element_bytes) = cost(&prepared);
285
+ assert!(rule_bytes <= 100_000 && rule_bytes + element_bytes <= 212_000);
286
+ // Chains, rules, sets and their elements only.
287
+ assert!(program.iter().all(|(kind, _)| [3, 6, 9, 12].contains(kind)));
292
288
  let mut changed = prepared.clone();
293
289
  changed.policy.revision += 1;
294
290
  assert!(changed.validate().is_err());
@@ -366,17 +362,31 @@ fn combined_router_full_transports_and_protection_fit_atomic_reserve() {
366
362
  }
367
363
 
368
364
  #[test]
369
- fn v2_compiled_graph_limit_rejects_bounded_but_excessive_cross_products() {
370
- let mut value = router();
371
- let mut grants = Vec::new();
372
- for port in 1000..1032 {
373
- let mut grant = value["scope"]["generations"][0]["grants"][0].clone();
374
- grant["destinationPort"] = json!(port);
375
- grants.push(grant);
376
- }
377
- value["scope"]["generations"][0]["grants"] = json!(grants);
378
- let policy = normalized(value).unwrap();
379
- assert!(policy.prepare().is_err());
365
+ fn v2_router_grants_are_elements_and_the_target_budget_rejects_excessive_ones() {
366
+ let grants = |count: u16| {
367
+ let mut value = router();
368
+ let template = value["scope"]["generations"][0]["grants"][0].clone();
369
+ value["scope"]["generations"][0]["grants"] = json!((0..count)
370
+ .map(|index| {
371
+ let mut grant = template.clone();
372
+ grant["destinationPort"] = json!(1000 + index);
373
+ grant
374
+ })
375
+ .collect::<Vec<_>>());
376
+ value
377
+ };
378
+ // The rule graph is the same for one grant and for 32; the elements carry them.
379
+ let rules = |value: Value| {
380
+ prepared(value)
381
+ .program("snft_v2_grants")
382
+ .unwrap()
383
+ .into_iter()
384
+ .filter(|(kind, _)| *kind == 6)
385
+ .count()
386
+ };
387
+ assert_eq!(rules(grants(1)), rules(grants(32)));
388
+ // Within the input bound, the elements still meet the atomic target budget.
389
+ assert!(normalized(grants(1024)).unwrap().prepare().is_err());
380
390
  }
381
391
 
382
392
  #[test]
@@ -516,6 +526,9 @@ fn graph(prepared: &Prepared, table: &str) -> (Vec<String>, Vec<(String, Vec<Str
516
526
  chains.push(crate::wire::text(&attributes, 3).unwrap());
517
527
  continue;
518
528
  }
529
+ if kind != 6 {
530
+ continue;
531
+ }
519
532
  let expressions = crate::wire::attrs(&crate::wire::one(&attributes, 4).unwrap().value).unwrap();
520
533
  rules.push((
521
534
  crate::wire::text(&attributes, 2).unwrap(),
@@ -575,27 +588,29 @@ fn host_v2_published_ports_compile_uplink_dnat_and_exact_forward_admission() {
575
588
  let (chains, rules) = graph(&published, "snft_v2_host_golden");
576
589
  assert_eq!(chains.len(), baseline.0.len() + 1);
577
590
  assert_eq!(chains.iter().filter(|name| *name == "pre").count(), 1);
578
- assert_eq!(rules.len(), baseline.1.len() + 8);
591
+ // One map translation and four admissions whatever the number of
592
+ // publications: ESTABLISHED, a NEW opening per protocol and the reply.
593
+ assert_eq!(rules.len(), baseline.1.len() + 5);
579
594
  let translations: Vec<_> = rules.iter().filter(|(chain, _)| chain == "pre").collect();
580
- assert_eq!(translations.len(), 2);
581
- for (_, expressions) in &translations {
582
- assert_eq!(expressions.last().unwrap(), "nat");
583
- assert_eq!(expressions.iter().filter(|name| *name == "immediate").count(), 2);
595
+ assert_eq!(translations.len(), 1);
596
+ let (_, translation) = translations[0];
597
+ assert_eq!(translation.last().unwrap(), "nat");
598
+ assert!(translation.contains(&"lookup".to_string()));
599
+ assert!(!translation.contains(&"immediate".to_string()));
600
+ let forward: Vec<_> = rules.iter().filter(|(chain, _)| chain == "forward").collect();
601
+ assert_eq!(
602
+ forward.len(),
603
+ baseline.1.iter().filter(|(chain, _)| chain == "forward").count() + 4
604
+ );
605
+ // The admissions lead the forward chain, ahead of the capture barrier.
606
+ for (_, expressions) in &forward[..4] {
607
+ assert_eq!(expressions[expressions.len() - 2], "lookup");
584
608
  }
585
- // The three admissions precede the capture barrier jumps of their handoff.
586
- let forward: Vec<_> = rules
587
- .iter()
588
- .filter(|(chain, _)| chain == "forward")
589
- .map(|(_, expressions)| expressions.len())
590
- .collect();
591
- assert_eq!(forward.len(), baseline.1.iter().filter(|(chain, _)| chain == "forward").count() + 6);
592
- let (_, first) = rules.iter().find(|(chain, _)| chain == "forward").unwrap();
593
- assert!(first.contains(&"ct".to_string()) && first.len() > 4);
594
609
  let bytes = program_bytes(&published, "snft_v2_host_golden");
595
- assert_eq!(bytes.len(), 36322);
610
+ assert_eq!(bytes.len(), 30988);
596
611
  assert_eq!(
597
612
  format!("{:x}", Sha256::digest(&bytes)),
598
- "1d3485bb739ac3601642610eba465b80c9b5ac34df22a5374cfbc8773623710b"
613
+ "b36f359c6439e9fcd77c291c7dac7854014225d3de1f8a4e49df6fec6ae570c4"
599
614
  );
600
615
  }
601
616
 
@@ -653,28 +668,39 @@ fn host_v2_published_ports_reject_unbound_targets_conflicts_and_out_of_range_por
653
668
  // One protocol/port is published exactly once, including across addresses.
654
669
  published_host(json!([published_port("tcp", 443, 8443), published_port("tcp", 443, 9443)])),
655
670
  duplicate_across_addresses,
656
- published_host(json!((0..65).map(|index| published_port("tcp", 20000 + index, 8443)).collect::<Vec<_>>())),
671
+ published_host(json!((0..1025).map(|index| published_port("tcp", 20000 + index, 8443)).collect::<Vec<_>>())),
657
672
  ] {
658
673
  assert!(normalized(invalid).is_err());
659
674
  }
660
- // Bounded input still meets the atomic reserve first: every publication costs
661
- // one translation and three admissions, so prepare rejects before any kernel work.
675
+ // Publications are set elements: the input maximum compiles the same rules
676
+ // as one publication, and its elements fit their own budget.
662
677
  let ports = |count: u16| -> Value {
663
678
  json!((0..count)
664
679
  .map(|index| published_port("tcp", 20000 + index, 8443))
665
680
  .collect::<Vec<_>>())
666
681
  };
667
- let prepared = prepared(published_host(ports(12)));
682
+ let one = graph(&prepared(published_host(ports(1))), "snft_v2_host_max");
683
+ let maximum = prepared(published_host(ports(64)));
684
+ let (_, rules) = graph(&maximum, "snft_v2_host_max");
685
+ assert_eq!(rules, one.1);
686
+ let (operations, rule_bytes, element_bytes) = cost(&maximum);
687
+ assert!(operations <= 768 && rule_bytes <= 100_000 && element_bytes <= 100_000);
688
+ println!("PUBLISHED_MAX ports=64 operations={operations} rule_bytes={rule_bytes} element_bytes={element_bytes}");
689
+ }
690
+ /// Operations, rule graph bytes and set element bytes at the longest table name,
691
+ /// against the atomic graph reserve and the element budget.
692
+ pub(crate) fn cost(prepared: &Prepared) -> (usize, usize, usize) {
668
693
  let program = prepared
669
694
  .program(&format!("snft_{}", "x".repeat(59)))
670
695
  .unwrap();
671
- let bytes: usize = program
672
- .iter()
673
- .map(|(_, attributes)| crate::wire::encode_attrs(attributes).len() + 20)
674
- .sum();
675
- assert!(program.len() <= 768 && bytes <= 100_000);
676
- assert!(normalized(published_host(ports(13))).unwrap().prepare().is_err());
677
- println!("PUBLISHED_MAX ports=12 operations={} bytes={bytes}", program.len());
696
+ let bytes = |elements: bool| -> usize {
697
+ program
698
+ .iter()
699
+ .filter(|(kind, _)| (*kind == 12) == elements)
700
+ .map(|(_, attributes)| crate::wire::encode_attrs(attributes).len() + 20)
701
+ .sum()
702
+ };
703
+ (program.len(), bytes(false), bytes(true))
678
704
  }
679
705
 
680
706
  pub(crate) fn router_published_port(protocol: &str, transit_port: u16, endpoint_port: u16) -> Value {
@@ -696,11 +722,13 @@ fn router_v2_without_published_ports_keeps_its_exact_digest_and_compiled_bytes()
696
722
  baseline.digest,
697
723
  "sha256:762a556e0bf512b653acfcf86699ac7681f26e6cc9ea1d26671443e3d2ca0b61"
698
724
  );
725
+ // Re-frozen with the set-backed router graph of 3.0.0: the digest is the
726
+ // policy's and did not change, the compiled graph did.
699
727
  let bytes = program_bytes(&baseline, "snft_v2_router_golden");
700
- assert_eq!(bytes.len(), 53272);
728
+ assert_eq!(bytes.len(), 47674);
701
729
  assert_eq!(
702
730
  format!("{:x}", Sha256::digest(&bytes)),
703
- "66850722fbd38a45a986cebeddcc75a541ebb1e2f003ea4375621decb2e08f99"
731
+ "ea609865f32031f6835ef4fc7f67f5754a15f466580308bdccc9db87d4ccd2c5"
704
732
  );
705
733
  let serialized = serde_json::to_string(&baseline.policy).unwrap();
706
734
  assert!(!serialized.contains("publishedPorts"));
@@ -731,25 +759,18 @@ fn router_v2_published_ports_compile_handoff_dnat_classification_and_admission()
731
759
  let (chains, rules) = graph(&published, "snft_v2_router_golden");
732
760
  assert_eq!(chains.len(), baseline.0.len() + 1);
733
761
  assert_eq!(chains.iter().filter(|name| *name == "pre").count(), 1);
734
- // Six rules per publication: one translation, one handoff ingress and one
735
- // endpoint ingress classification, and the three forward admissions.
736
- assert_eq!(rules.len(), baseline.1.len() + 12);
762
+ // Seven rules whatever the number of publications: one map translation, one
763
+ // handoff ingress and one endpoint ingress classification, and four
764
+ // admissions (ESTABLISHED, a NEW opening per protocol and the reply).
765
+ assert_eq!(rules.len(), baseline.1.len() + 7);
737
766
  let translations: Vec<_> = rules.iter().filter(|(chain, _)| chain == "pre").collect();
738
- assert_eq!(translations.len(), 2);
739
- for (_, expressions) in &translations {
740
- assert_eq!(expressions.last().unwrap(), "nat");
741
- assert_eq!(
742
- expressions.iter().filter(|name| *name == "immediate").count(),
743
- 2
744
- );
745
- }
767
+ assert_eq!(translations.len(), 1);
768
+ assert_eq!(translations[0].1.last().unwrap(), "nat");
769
+ assert!(!translations[0].1.contains(&"immediate".to_string()));
746
770
  let counted = |graph: &Vec<(String, Vec<String>)>, chain: &str| {
747
771
  graph.iter().filter(|(name, _)| name == chain).count()
748
772
  };
749
- assert_eq!(
750
- counted(&rules, "forward"),
751
- counted(&baseline.1, "forward") + 6
752
- );
773
+ assert_eq!(counted(&rules, "forward"), counted(&baseline.1, "forward") + 4);
753
774
  // The classification leads the handoff ingress barrier, ahead of every denial
754
775
  // in it, and never touches conntrack because raw runs before it.
755
776
  let returns: Vec<_> = rules
@@ -757,11 +778,9 @@ fn router_v2_published_ports_compile_handoff_dnat_classification_and_admission()
757
778
  .filter(|(chain, _)| chain == "raw_return")
758
779
  .map(|(_, expressions)| expressions)
759
780
  .collect();
760
- assert_eq!(returns.len(), counted(&baseline.1, "raw_return") + 2);
761
- for expressions in &returns[..2] {
762
- assert!(!expressions.contains(&"ct".to_string()));
763
- }
764
- assert!(returns[2..]
781
+ assert_eq!(returns.len(), counted(&baseline.1, "raw_return") + 1);
782
+ assert!(returns[0].contains(&"lookup".to_string()) && !returns[0].contains(&"ct".to_string()));
783
+ assert!(returns[1..]
765
784
  .iter()
766
785
  .any(|expressions| expressions.contains(&"ct".to_string())));
767
786
  assert_eq!(returns.last().unwrap().len(), 1);
@@ -772,18 +791,16 @@ fn router_v2_published_ports_compile_handoff_dnat_classification_and_admission()
772
791
  .filter(|(chain, _)| chain == "raw_in")
773
792
  .map(|(_, expressions)| expressions)
774
793
  .collect();
775
- assert_eq!(incoming.len(), counted(&baseline.1, "raw_in") + 2);
776
- for expressions in &incoming[3..5] {
777
- assert!(!expressions.contains(&"ct".to_string()));
778
- }
779
- assert!(incoming[5..]
794
+ assert_eq!(incoming.len(), counted(&baseline.1, "raw_in") + 1);
795
+ assert!(incoming[3].contains(&"lookup".to_string()) && !incoming[3].contains(&"ct".to_string()));
796
+ assert!(incoming[4..]
780
797
  .iter()
781
798
  .any(|expressions| expressions.contains(&"ct".to_string())));
782
799
  let bytes = program_bytes(&published, "snft_v2_router_golden");
783
- assert_eq!(bytes.len(), 67818);
800
+ assert_eq!(bytes.len(), 55494);
784
801
  assert_eq!(
785
802
  format!("{:x}", Sha256::digest(&bytes)),
786
- "62bf0f5aace2a19fb8a92571d1c4d45d774aa003884c239e473c67a7e103bb12"
803
+ "60a7ba1c15d49e5ff54cfbf9fbe2d72f03973acd1a897eef2d975547ef4abf5b"
787
804
  );
788
805
  }
789
806
 
@@ -892,201 +909,86 @@ fn router_v2_published_ports_reject_unbound_endpoints_conflicts_and_out_of_range
892
909
  router_published_port("tcp", 443, 8443),
893
910
  router_published_port("tcp", 443, 9443)
894
911
  ])),
895
- published_router(json!((0..65)
912
+ published_router(json!((0..1025)
896
913
  .map(|index| router_published_port("tcp", 20000 + index, 8443))
897
914
  .collect::<Vec<_>>())),
898
915
  ] {
899
916
  assert!(normalized(invalid).is_err());
900
917
  }
901
- // Bounded input still meets the atomic reserve first: every publication costs
902
- // six operations, so prepare rejects before any kernel work.
918
+ // Publications are set elements: the input maximum compiles the same rules
919
+ // as one publication, and its elements fit their own budget.
903
920
  let ports = |count: u16| -> Value {
904
921
  json!((0..count)
905
922
  .map(|index| router_published_port("tcp", 20000 + index, 8443))
906
923
  .collect::<Vec<_>>())
907
924
  };
908
- let prepared = prepared(published_router(ports(5)));
909
- let program = prepared
910
- .program(&format!("snft_{}", "x".repeat(59)))
911
- .unwrap();
912
- let bytes: usize = program
913
- .iter()
914
- .map(|(_, attributes)| crate::wire::encode_attrs(attributes).len() + 20)
915
- .sum();
916
- assert!(program.len() <= 768 && bytes <= 100_000);
917
- assert!(normalized(published_router(ports(6)))
918
- .unwrap()
919
- .prepare()
920
- .is_err());
921
- println!("ROUTER_PUBLISHED_MAX ports=5 operations={} bytes={bytes}", program.len());
922
- }
923
-
924
- /// One arriving IPv4 tuple for the ordered raw classifiers of a compiled chain.
925
- #[derive(Clone, Copy)]
926
- struct Arrival {
927
- interface_index: u32,
928
- interface_name: &'static str,
929
- protocol: u8,
930
- source: [u8; 4],
931
- destination: [u8; 4],
932
- source_port: u16,
933
- destination_port: u16,
934
- }
935
- /// Walks one compiled chain in program order and reports the first rule that
936
- /// matches an arriving tuple as (position, verdict, conntrack zone it sets). Raw
937
- /// prerouting classification is the only hook this covers, so an unexpected
938
- /// expression panics instead of being silently ignored.
939
- fn classify(prepared: &Prepared, chain: &str, arrival: &Arrival) -> Option<(usize, i32, u16)> {
940
- let table = "snft_v2_router_order";
941
- let mut position = 0;
942
- for (kind, attributes) in prepared.program(table).unwrap() {
943
- if kind == 3 || crate::wire::text(&attributes, 2).unwrap() != chain {
944
- continue;
945
- }
946
- let index = position;
947
- position += 1;
948
- let mut pending: Option<Vec<u8>> = None;
949
- let mut immediate: Option<Vec<u8>> = None;
950
- let mut verdict = 0;
951
- let mut zone = 0;
952
- let mut matched = true;
953
- for expression in
954
- crate::wire::attrs(&crate::wire::one(&attributes, 4).unwrap().value).unwrap()
955
- {
956
- let fields = crate::wire::attrs(&expression.value).unwrap();
957
- let data = crate::wire::attrs(&crate::wire::one(&fields, 2).unwrap().value).unwrap();
958
- match crate::wire::text(&fields, 1).unwrap().as_str() {
959
- "meta" => {
960
- pending = Some(match crate::wire::number(&data, 2).unwrap() {
961
- 4 => arrival.interface_index.to_ne_bytes().to_vec(),
962
- 6 => [arrival.interface_name.as_bytes(), &[0]].concat(),
963
- 15 => vec![libc::NFPROTO_IPV4 as u8],
964
- 16 => vec![arrival.protocol],
965
- key => panic!("meta {key}"),
966
- })
967
- }
968
- "payload" => {
969
- pending = Some(
970
- match (
971
- crate::wire::number(&data, 2).unwrap(),
972
- crate::wire::number(&data, 3).unwrap(),
973
- ) {
974
- (1, 12) => arrival.source.to_vec(),
975
- (1, 16) => arrival.destination.to_vec(),
976
- (2, 0) => arrival.source_port.to_be_bytes().to_vec(),
977
- (2, 2) => arrival.destination_port.to_be_bytes().to_vec(),
978
- load => panic!("payload {load:?}"),
979
- },
980
- )
981
- }
982
- "bitwise" => {
983
- let mask = crate::wire::attrs(&crate::wire::one(&data, 4).unwrap().value)
984
- .unwrap()[0]
985
- .value
986
- .clone();
987
- let loaded = pending.take().unwrap();
988
- pending = Some(
989
- loaded
990
- .iter()
991
- .zip(&mask)
992
- .map(|(byte, mask)| byte & mask)
993
- .collect(),
994
- );
995
- }
996
- "cmp" => {
997
- assert_eq!(crate::wire::number(&data, 2).unwrap(), 0);
998
- let expected = crate::wire::attrs(&crate::wire::one(&data, 3).unwrap().value)
999
- .unwrap()[0]
1000
- .value
1001
- .clone();
1002
- matched = pending.take().unwrap() == expected;
1003
- }
1004
- "ct" => {
1005
- // Only the zone store reaches raw classification; a loaded
1006
- // conntrack key would need a conntrack lookup that has not run.
1007
- assert_eq!(crate::wire::number(&data, 2).unwrap(), 17);
1008
- assert_eq!(crate::wire::number(&data, 4).unwrap(), 1);
1009
- zone = u16::from_ne_bytes(immediate.take().unwrap().try_into().unwrap());
1010
- }
1011
- "immediate" => {
1012
- let inner =
1013
- crate::wire::attrs(&crate::wire::one(&data, 2).unwrap().value).unwrap();
1014
- if crate::wire::number(&data, 1).unwrap() == 0 {
1015
- let code = crate::wire::attrs(&crate::wire::one(&inner, 2).unwrap().value)
1016
- .unwrap();
1017
- verdict = crate::wire::number(&code, 1).unwrap() as i32;
1018
- } else {
1019
- immediate = Some(crate::wire::one(&inner, 1).unwrap().value.clone());
1020
- }
1021
- }
1022
- other => panic!("{other}"),
1023
- }
1024
- if !matched {
1025
- break;
1026
- }
1027
- }
1028
- if matched {
1029
- return Some((index, verdict, zone));
1030
- }
1031
- }
1032
- None
925
+ let one = graph(&prepared(published_router(ports(1))), "snft_v2_router_max");
926
+ let maximum = prepared(published_router(ports(64)));
927
+ let (_, rules) = graph(&maximum, "snft_v2_router_max");
928
+ assert_eq!(rules, one.1);
929
+ let (operations, rule_bytes, element_bytes) = cost(&maximum);
930
+ assert!(operations <= 768 && rule_bytes <= 100_000 && element_bytes <= 100_000);
931
+ println!("ROUTER_PUBLISHED_MAX ports=64 operations={operations} rule_bytes={rule_bytes} element_bytes={element_bytes}");
1033
932
  }
1034
933
 
1035
934
  #[test]
1036
935
  fn router_v2_published_endpoint_traffic_is_classified_before_the_leased_grants() {
936
+ use super::hostgrant_tests::{decide, Flow, ESTABLISHED};
1037
937
  // A published reply to a client whose source port equals the leased grant's
1038
938
  // destination port on its public prefix. Raw runs before conntrack, so only
1039
939
  // the payload tuple separates it from ordinary leased egress.
1040
- let reply = Arrival {
1041
- interface_index: 2,
1042
- interface_name: "work",
940
+ let reply = Flow {
941
+ input: Some((2, "work")),
942
+ output: None,
1043
943
  protocol: 6,
1044
944
  source: [10, 241, 0, 2],
1045
945
  destination: [198, 51, 100, 7],
1046
946
  source_port: 9443,
1047
947
  destination_port: 443,
948
+ tcp_flags: 0x10,
949
+ state: ESTABLISHED,
950
+ reply: true,
951
+ zone: 0,
952
+ original: ([198, 51, 100, 7], [10, 240, 0, 2], 443, 443),
953
+ uid: None,
954
+ label: [0; 16],
1048
955
  };
1049
956
  // Without the publication that exact tuple is leased into the generation zone,
1050
- // so the ordering below is what keeps the publication usable at all.
1051
- let leased = classify(&prepared(router()), "raw_in", &reply).unwrap();
1052
- assert_eq!((leased.1, leased.2), (1, 1));
957
+ // so the precedence below is what keeps the publication usable at all.
958
+ let baseline = prepared(router());
959
+ let leased = decide(&baseline, "raw_in", &reply);
960
+ assert_eq!((leased.verdict, leased.zone), (1, 1));
1053
961
  let published = prepared(published_router(json!([router_published_port(
1054
962
  "tcp", 443, 9443
1055
963
  )])));
1056
- let (index, verdict, zone) = classify(&published, "raw_in", &reply).unwrap();
1057
964
  // Accepted with no zone set: the reply stays in the default zone, where the
1058
965
  // translated publication and its admission live.
1059
- assert_eq!((verdict, zone), (1, 0));
1060
- // Every other workload flow keeps its leased classification, and the leased
1061
- // classifier that claims the tuple above is reached only after this one.
1062
- let (leased_index, leased_verdict, leased_zone) = classify(
1063
- &published,
1064
- "raw_in",
1065
- &Arrival {
966
+ let classified = decide(&published, "raw_in", &reply);
967
+ assert_eq!((classified.verdict, classified.zone), (1, 0));
968
+ // Every other workload flow keeps its leased classification, and the
969
+ // classification is exact: another link, another workload address and
970
+ // another protocol meet exactly the decision the unpublished scope makes.
971
+ for foreign in [
972
+ Flow {
1066
973
  source_port: 50000,
1067
974
  ..reply
1068
975
  },
1069
- )
1070
- .unwrap();
1071
- assert_eq!((leased_verdict, leased_zone), (1, 1));
1072
- assert!(index < leased_index, "{index} {leased_index}");
1073
- // The classification is exact: another link, another workload address and
1074
- // another protocol are not published and are not classified here.
1075
- for foreign in [
1076
- Arrival {
1077
- interface_index: 3,
1078
- interface_name: "vpn",
976
+ Flow {
977
+ input: Some((3, "vpn")),
1079
978
  ..reply
1080
979
  },
1081
- Arrival {
980
+ Flow {
1082
981
  source: [10, 241, 0, 3],
1083
982
  ..reply
1084
983
  },
1085
- Arrival {
984
+ Flow {
1086
985
  protocol: 17,
1087
986
  ..reply
1088
987
  },
1089
988
  ] {
1090
- assert!(classify(&published, "raw_in", &foreign).is_none());
989
+ assert_eq!(
990
+ decide(&published, "raw_in", &foreign),
991
+ decide(&baseline, "raw_in", &foreign)
992
+ );
1091
993
  }
1092
994
  }
@@ -297,18 +297,17 @@ fn workload_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
297
297
  .sum()
298
298
  };
299
299
  let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
300
- assert!(program.len() < 768 && graph <= 100_000 && elements <= super::compile::ELEMENT_BYTES);
300
+ assert!(program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES);
301
301
  assert!(deletion + graph + elements + 64 <= wire::BATCH_BYTES);
302
302
  println!(
303
303
  "WORKLOAD_GRANT_MAX grants=1024 operations={} rule_bytes={graph} element_bytes={elements}",
304
304
  program.len()
305
305
  );
306
- // Host grants and workload grants share one element budget per target:
307
- // half the host grant maximum fits beside the workload maximum, both maxima
308
- // together are refused before any kernel work.
306
+ // Host grants and workload grants share the target budget with the rest of
307
+ // the scope's elements; both maxima together fit it.
309
308
  let mut both = granted(two_workloads(), json!(maximum));
310
- both["scope"]["hostGrants"] = many(512);
311
- normalized(both.clone()).unwrap().prepare().unwrap();
312
309
  both["scope"]["hostGrants"] = many(1024);
313
- assert!(normalized(both).unwrap().prepare().is_err());
310
+ let both = normalized(both).unwrap().prepare().unwrap();
311
+ let (operations, rule_bytes, element_bytes) = super::tests::cost(&both);
312
+ assert!(operations < 768 && rule_bytes + element_bytes <= super::compile::TARGET_BYTES);
314
313
  }
package/rust/src/owner.rs CHANGED
@@ -680,7 +680,8 @@ fn chain_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
680
680
  }
681
681
  serde_json::to_vec(&wire::canonical(&identity, false)?).map_err(|_| Error::Protocol)
682
682
  }
683
- /// A set is its table, name, flags, key type and key length. The handle, the
683
+ /// A set is its table, name, flags, key type and key length, a map's data type
684
+ /// and length, and the field lengths of a concatenation. The handle, the
684
685
  /// informational backend type and count, the transaction ID and an empty
685
686
  /// description are not identity; anything else rejects.
686
687
  fn set_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
@@ -688,30 +689,78 @@ fn set_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
688
689
  for attribute in attributes {
689
690
  match attribute.id() {
690
691
  10 | 16 | 19 | 20 => {}
691
- 9 if wire::attrs(&attribute.value)?.is_empty() => {}
692
- 1..=5 => identity.push(attribute.clone()),
692
+ 9 => {
693
+ if let Some(fields) = concatenation(&attribute.value)? {
694
+ identity.push(Attr::bytes(9, fields));
695
+ }
696
+ }
697
+ 1..=7 => identity.push(attribute.clone()),
693
698
  _ => return Err(Error::Conflict),
694
699
  }
695
700
  }
696
701
  for id in [1, 2, 3, 4, 5] {
697
702
  wire::one(&identity, id)?;
698
703
  }
704
+ // A map carries its data type and length, and nothing else does.
705
+ let map = wire::number(&identity, 3)? & 8 != 0;
706
+ for id in [6, 7] {
707
+ if wire::one(&identity, id).is_ok() != map {
708
+ return Err(Error::Conflict);
709
+ }
710
+ }
699
711
  serde_json::to_vec(&wire::canonical(&identity, false)?).map_err(|_| Error::Protocol)
700
712
  }
701
- /// Only plain keys: an element with data, flags, timeouts, expressions or any
702
- /// other extension is not one the compiler created.
713
+ /// The field lengths of a set description: only a concatenation, never a size.
714
+ /// An empty description is none.
715
+ fn concatenation(value: &[u8]) -> Result<Option<Vec<u8>>> {
716
+ let description = wire::attrs(value)?;
717
+ if description.is_empty() {
718
+ return Ok(None);
719
+ }
720
+ if description.len() != 1 || description[0].id() != 2 {
721
+ return Err(Error::Conflict);
722
+ }
723
+ let mut lengths = Vec::new();
724
+ for field in wire::attrs(&description[0].value)? {
725
+ let length = wire::attrs(&field.value)?;
726
+ if field.id() != 1 || length.len() != 1 {
727
+ return Err(Error::Conflict);
728
+ }
729
+ lengths.extend(wire::number(&length, 1)?.to_be_bytes());
730
+ }
731
+ Ok(Some(lengths))
732
+ }
733
+ /// Only compiler elements: a key, the last key of an interval and a map's data.
734
+ /// Flags, timeouts, expressions or any other extension reject. The identity is
735
+ /// the three values, so any changed bound or datum is a changed element.
703
736
  fn element_keys(list: &[Attr]) -> Result<Vec<Vec<u8>>> {
704
737
  let mut keys = Vec::new();
705
738
  for element in list {
706
- let fields = wire::attrs(&element.value)?;
707
- if element.id() != 1 || fields.len() != 1 {
739
+ if element.id() != 1 {
708
740
  return Err(Error::Conflict);
709
741
  }
710
- let key = wire::attrs(&wire::one(&fields, 1)?.value)?;
711
- if key.len() != 1 {
712
- return Err(Error::Conflict);
742
+ let mut values: [Option<Vec<u8>>; 3] = [None, None, None];
743
+ for field in wire::attrs(&element.value)? {
744
+ let slot = match field.id() {
745
+ 1 => 0,
746
+ 10 => 1,
747
+ 2 => 2,
748
+ _ => return Err(Error::Conflict),
749
+ };
750
+ let value = wire::attrs(&field.value)?;
751
+ if value.len() != 1 || values[slot].is_some() {
752
+ return Err(Error::Conflict);
753
+ }
754
+ values[slot] = Some(wire::one(&value, 1)?.value.clone());
713
755
  }
714
- keys.push(wire::one(&key, 1)?.value.clone());
756
+ let [Some(key), last, data] = values else {
757
+ return Err(Error::Conflict);
758
+ };
759
+ keys.push(if last.is_none() && data.is_none() {
760
+ key
761
+ } else {
762
+ serde_json::to_vec(&(key, last, data)).map_err(|_| Error::Protocol)?
763
+ });
715
764
  }
716
765
  Ok(keys)
717
766
  }
@@ -772,7 +821,7 @@ fn rule_identity(attributes: &[Attr]) -> Result<(String, Vec<u8>)> {
772
821
  /// neither unknown fields nor default repair may hide a changed kernel rule.
773
822
  fn validate_egress_expression(name: &str, data: &[Attr]) -> Result<()> {
774
823
  let allowed: &[u16] = match name {
775
- "lookup" => &[1, 2, 5],
824
+ "lookup" => &[1, 2, 3, 5],
776
825
  "ct" | "range" => &[1, 2, 3, 4],
777
826
  "nat" => &[1, 2, 3, 4, 5, 6, 7],
778
827
  "reject" => &[1],
@@ -786,10 +835,16 @@ fn validate_egress_expression(name: &str, data: &[Attr]) -> Result<()> {
786
835
  }
787
836
  match name {
788
837
  "lookup" => {
789
- // A plain membership test of a named set: no map, no inversion.
838
+ // A membership or absence test of a named set, or a map lookup into
839
+ // a data register, which is never inverted.
790
840
  wire::text(data, 1)?;
791
841
  wire::number(data, 2)?;
792
- if data.len() != 3 || wire::number(data, 5)? != 0 {
842
+ let map = wire::one(data, 3).is_ok();
843
+ if map {
844
+ wire::number(data, 3)?;
845
+ }
846
+ let flags = wire::number(data, 5)?;
847
+ if data.len() != 3 + usize::from(map) || flags > 1 || map && flags != 0 {
793
848
  return Err(Error::Conflict);
794
849
  }
795
850
  }