@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.
- package/changelog.md +19 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/managed.egress.types.d.ts +25 -3
- package/package.json +6 -6
- package/readme.md +205 -63
- package/rust/src/egress.compile.rs +155 -76
- package/rust/src/egress.host.rs +24 -51
- package/rust/src/egress.hostgrant.rs +1 -1
- package/rust/src/egress.keys.rs +305 -0
- package/rust/src/egress.leased.rs +399 -0
- package/rust/src/egress.private.rs +197 -0
- package/rust/src/egress.published.rs +370 -0
- package/rust/src/egress.router.rs +61 -294
- package/rust/src/egress.rs +126 -11
- package/rust/src/egress_hostgrant_tests.rs +149 -18
- package/rust/src/egress_localport_tests.rs +3 -1
- package/rust/src/egress_publishedrange_tests.rs +812 -0
- package/rust/src/egress_routerequivalence_tests.rs +448 -0
- package/rust/src/egress_tests.rs +132 -230
- package/rust/src/egress_workloadgrant_tests.rs +6 -7
- package/rust/src/owner.rs +69 -14
- package/rust/src/owner_host_traffic_tests.rs +4 -0
- package/rust/src/owner_identity_tests.rs +166 -80
- package/rust/src/owner_publishedrange_traffic_tests.rs +306 -0
- package/rust/src/owner_router_traffic_tests.rs +2 -0
- package/rust/src/owner_scale_traffic_tests.rs +383 -0
- package/rust/src/owner_tests.rs +6 -0
- package/rust/src/policy.rs +29 -25
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/managed.egress.types.ts +25 -3
package/rust/src/egress_tests.rs
CHANGED
|
@@ -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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
370
|
-
let
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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(),
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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(),
|
|
610
|
+
assert_eq!(bytes.len(), 30988);
|
|
596
611
|
assert_eq!(
|
|
597
612
|
format!("{:x}", Sha256::digest(&bytes)),
|
|
598
|
-
"
|
|
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..
|
|
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
|
-
//
|
|
661
|
-
// one
|
|
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
|
|
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
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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(),
|
|
728
|
+
assert_eq!(bytes.len(), 47674);
|
|
701
729
|
assert_eq!(
|
|
702
730
|
format!("{:x}", Sha256::digest(&bytes)),
|
|
703
|
-
"
|
|
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
|
-
//
|
|
735
|
-
// endpoint ingress classification, and
|
|
736
|
-
|
|
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(),
|
|
739
|
-
|
|
740
|
-
|
|
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") +
|
|
761
|
-
|
|
762
|
-
|
|
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") +
|
|
776
|
-
|
|
777
|
-
|
|
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(),
|
|
800
|
+
assert_eq!(bytes.len(), 55494);
|
|
784
801
|
assert_eq!(
|
|
785
802
|
format!("{:x}", Sha256::digest(&bytes)),
|
|
786
|
-
"
|
|
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..
|
|
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
|
-
//
|
|
902
|
-
//
|
|
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
|
|
909
|
-
let
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
let
|
|
913
|
-
|
|
914
|
-
|
|
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 =
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
|
1051
|
-
let
|
|
1052
|
-
|
|
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
|
-
|
|
1060
|
-
|
|
1061
|
-
//
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
-
|
|
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
|
-
|
|
980
|
+
Flow {
|
|
1082
981
|
source: [10, 241, 0, 3],
|
|
1083
982
|
..reply
|
|
1084
983
|
},
|
|
1085
|
-
|
|
984
|
+
Flow {
|
|
1086
985
|
protocol: 17,
|
|
1087
986
|
..reply
|
|
1088
987
|
},
|
|
1089
988
|
] {
|
|
1090
|
-
|
|
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 <=
|
|
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
|
|
307
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
692
|
-
|
|
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
|
-
///
|
|
702
|
-
///
|
|
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
|
-
|
|
707
|
-
if element.id() != 1 || fields.len() != 1 {
|
|
739
|
+
if element.id() != 1 {
|
|
708
740
|
return Err(Error::Conflict);
|
|
709
741
|
}
|
|
710
|
-
let
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|