@push.rocks/smartnftables 2.6.0 → 4.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 +33 -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/classes.manageddockerforwarding.js +3 -4
- package/dist_ts/classes.managednftables.d.ts +14 -2
- package/dist_ts/classes.managednftables.js +48 -8
- package/dist_ts/managed.egress.types.d.ts +25 -3
- package/dist_ts/managed.types.d.ts +14 -0
- package/package.json +7 -7
- package/readme.md +262 -64
- package/rust/src/docker.policy.rs +2 -1
- package/rust/src/egress.compile.rs +153 -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 +140 -19
- package/rust/src/egress_hostgrant_tests.rs +158 -20
- package/rust/src/egress_localport_tests.rs +11 -2
- package/rust/src/egress_publishedrange_tests.rs +812 -0
- package/rust/src/egress_routerequivalence_tests.rs +448 -0
- package/rust/src/egress_tests.rs +289 -230
- package/rust/src/egress_workloadgrant_tests.rs +21 -13
- package/rust/src/main.rs +57 -3
- 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 +30 -29
- package/rust/src/tests.rs +100 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.manageddockerforwarding.ts +2 -3
- package/ts/classes.managednftables.ts +46 -8
- package/ts/managed.egress.types.ts +25 -3
- package/ts/managed.types.ts +17 -0
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,243 @@ 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
|
}
|
|
995
|
+
|
|
996
|
+
fn exhausted(value: super::Result<Prepared>) -> crate::Bound {
|
|
997
|
+
match value {
|
|
998
|
+
Err(crate::Error::Exhausted(bound)) => bound,
|
|
999
|
+
other => panic!("expected a capacity refusal, got {other:?}"),
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
fn bound(name: &'static str, limit: usize, actual: usize) -> crate::Bound {
|
|
1003
|
+
crate::Bound {
|
|
1004
|
+
name,
|
|
1005
|
+
limit,
|
|
1006
|
+
actual,
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
fn prepare(value: Value) -> super::Result<Prepared> {
|
|
1010
|
+
normalized(value)?.prepare()
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/// The compiled budget and the input counts that exist to hold it refuse with
|
|
1014
|
+
/// EXHAUSTED and their bound; bounds of the topology's shape stay INVALID.
|
|
1015
|
+
#[test]
|
|
1016
|
+
fn v2_capacity_refusals_name_their_bound_and_shape_bounds_stay_invalid() {
|
|
1017
|
+
// Router rules grow with protected prefixes: 128 exceed the rule reserve.
|
|
1018
|
+
let mut value = router();
|
|
1019
|
+
let mut prefixes = vec![
|
|
1020
|
+
json!("10.0.0.0/8"),
|
|
1021
|
+
json!("192.0.2.0/24"),
|
|
1022
|
+
json!("203.0.113.10/32"),
|
|
1023
|
+
];
|
|
1024
|
+
prefixes.extend((0..125).map(|index| json!(format!("172.16.{index}.0/24"))));
|
|
1025
|
+
value["scope"]["protection"]["prefixes"] = json!(prefixes);
|
|
1026
|
+
let refused = exhausted(prepare(value.clone()));
|
|
1027
|
+
assert_eq!((refused.name, refused.limit), ("ruleBytes", 100_000));
|
|
1028
|
+
assert!(refused.actual > refused.limit);
|
|
1029
|
+
// 129 protected prefixes are outside the schema, not over capacity.
|
|
1030
|
+
value["scope"]["protection"]["prefixes"]
|
|
1031
|
+
.as_array_mut()
|
|
1032
|
+
.unwrap()
|
|
1033
|
+
.push(json!("172.17.0.0/24"));
|
|
1034
|
+
assert_eq!(prepare(value).unwrap_err(), crate::Error::Invalid);
|
|
1035
|
+
|
|
1036
|
+
// Host transit rules grow with its handoffs and their leased ranges.
|
|
1037
|
+
let handoff = |index: u32, ranges: u32| {
|
|
1038
|
+
let mut item = allocation();
|
|
1039
|
+
item["lease"]["id"] = json!(format!("lease-{index}"));
|
|
1040
|
+
item["transitSourceAddress"] = json!(format!("10.240.{index}.2"));
|
|
1041
|
+
item["sourcePortRanges"] = json!((0..ranges)
|
|
1042
|
+
.map(
|
|
1043
|
+
|range| json!({"protocol":if range % 2 == 0 { "tcp" } else { "udp" },
|
|
1044
|
+
"first":10000 + range * 100,"last":10099 + range * 100})
|
|
1045
|
+
)
|
|
1046
|
+
.collect::<Vec<_>>());
|
|
1047
|
+
json!({"link":link(10 + index, &format!("h{index}"), "veth", &format!("10.240.{index}.1")),
|
|
1048
|
+
"allocations":[item]})
|
|
1049
|
+
};
|
|
1050
|
+
let handoffs = |count: u32, ranges: u32| {
|
|
1051
|
+
json!((0..count)
|
|
1052
|
+
.map(|index| handoff(index, ranges))
|
|
1053
|
+
.collect::<Vec<_>>())
|
|
1054
|
+
};
|
|
1055
|
+
let mut value = host();
|
|
1056
|
+
value["scope"]["handoffs"] = handoffs(4, 2);
|
|
1057
|
+
assert_eq!(prepare(value.clone()).map(|_| ()), Ok(()));
|
|
1058
|
+
value["scope"]["handoffs"] = handoffs(32, 16);
|
|
1059
|
+
let refused = exhausted(prepare(value.clone()));
|
|
1060
|
+
assert_eq!((refused.name, refused.limit), ("ruleBytes", 100_000));
|
|
1061
|
+
assert!(refused.actual > refused.limit);
|
|
1062
|
+
// 33 handoffs are outside the schema, not over capacity.
|
|
1063
|
+
value["scope"]["handoffs"] = handoffs(33, 2);
|
|
1064
|
+
assert_eq!(prepare(value).unwrap_err(), crate::Error::Invalid);
|
|
1065
|
+
|
|
1066
|
+
// Router input counts.
|
|
1067
|
+
let mut value = router();
|
|
1068
|
+
let template = value["scope"]["links"][0].clone();
|
|
1069
|
+
value["scope"]["links"] = json!((0..129u32)
|
|
1070
|
+
.map(|index| {
|
|
1071
|
+
let mut item = template.clone();
|
|
1072
|
+
item["interfaceIndex"] = json!(10 + index);
|
|
1073
|
+
item["interfaceName"] = json!(format!("l{index}"));
|
|
1074
|
+
item
|
|
1075
|
+
})
|
|
1076
|
+
.collect::<Vec<_>>());
|
|
1077
|
+
assert_eq!(exhausted(prepare(value)), bound("links", 128, 129));
|
|
1078
|
+
let mut value = router();
|
|
1079
|
+
let template = value["scope"]["endpoints"][0].clone();
|
|
1080
|
+
value["scope"]["endpoints"] = json!((0..129u32)
|
|
1081
|
+
.map(|index| {
|
|
1082
|
+
let mut item = template.clone();
|
|
1083
|
+
item["id"] = json!(format!("e{index}"));
|
|
1084
|
+
item
|
|
1085
|
+
})
|
|
1086
|
+
.collect::<Vec<_>>());
|
|
1087
|
+
assert_eq!(exhausted(prepare(value)), bound("endpoints", 128, 129));
|
|
1088
|
+
let mut value = router();
|
|
1089
|
+
let template = value["scope"]["rules"][0].clone();
|
|
1090
|
+
value["scope"]["rules"] = json!((0..1025u16)
|
|
1091
|
+
.map(|index| {
|
|
1092
|
+
let mut item = template.clone();
|
|
1093
|
+
item["destinationPort"] = json!(1 + index);
|
|
1094
|
+
item
|
|
1095
|
+
})
|
|
1096
|
+
.collect::<Vec<_>>());
|
|
1097
|
+
assert_eq!(exhausted(prepare(value)), bound("rules", 1024, 1025));
|
|
1098
|
+
// Egress grants count across every generation.
|
|
1099
|
+
let mut value = router();
|
|
1100
|
+
let generation = value["scope"]["generations"][0].clone();
|
|
1101
|
+
let template = generation["grants"][0].clone();
|
|
1102
|
+
value["scope"]["generations"] = json!((0..2u16)
|
|
1103
|
+
.map(|part| {
|
|
1104
|
+
let mut item = generation.clone();
|
|
1105
|
+
item["grants"] = json!((0..(512 + part))
|
|
1106
|
+
.map(|index| {
|
|
1107
|
+
let mut grant = template.clone();
|
|
1108
|
+
grant["destinationPort"] = json!(1 + index);
|
|
1109
|
+
grant
|
|
1110
|
+
})
|
|
1111
|
+
.collect::<Vec<_>>());
|
|
1112
|
+
item
|
|
1113
|
+
})
|
|
1114
|
+
.collect::<Vec<_>>());
|
|
1115
|
+
assert_eq!(exhausted(prepare(value)), bound("grants", 1024, 1025));
|
|
1116
|
+
// Active generations are lease lifecycle, not capacity.
|
|
1117
|
+
let mut value = router();
|
|
1118
|
+
let generation = value["scope"]["generations"][0].clone();
|
|
1119
|
+
value["scope"]["generations"] = json!(vec![generation; 33]);
|
|
1120
|
+
assert_eq!(prepare(value).unwrap_err(), crate::Error::Invalid);
|
|
1121
|
+
|
|
1122
|
+
// Publications on both hops.
|
|
1123
|
+
let ports = json!((0..1025u16)
|
|
1124
|
+
.map(|index| published_port("tcp", 20000 + index, 20000 + index))
|
|
1125
|
+
.collect::<Vec<_>>());
|
|
1126
|
+
assert_eq!(
|
|
1127
|
+
exhausted(prepare(published_host(ports.clone()))),
|
|
1128
|
+
bound("publishedPorts", 1024, 1025)
|
|
1129
|
+
);
|
|
1130
|
+
let ports = json!((0..1025u16)
|
|
1131
|
+
.map(|index| router_published_port("tcp", 20000 + index, 20000 + index))
|
|
1132
|
+
.collect::<Vec<_>>());
|
|
1133
|
+
assert_eq!(
|
|
1134
|
+
exhausted(prepare(published_router(ports))),
|
|
1135
|
+
bound("publishedPorts", 1024, 1025)
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
// Within the input bound, the grant elements exceed the target budget.
|
|
1139
|
+
let mut value = router();
|
|
1140
|
+
let template = value["scope"]["generations"][0]["grants"][0].clone();
|
|
1141
|
+
value["scope"]["generations"][0]["grants"] = json!((0..1024u16)
|
|
1142
|
+
.map(|index| {
|
|
1143
|
+
let mut grant = template.clone();
|
|
1144
|
+
grant["destinationPort"] = json!(1000 + index);
|
|
1145
|
+
grant
|
|
1146
|
+
})
|
|
1147
|
+
.collect::<Vec<_>>());
|
|
1148
|
+
let refused = exhausted(prepare(value));
|
|
1149
|
+
assert_eq!((refused.name, refused.limit), ("targetBytes", 212_000));
|
|
1150
|
+
assert!(refused.actual > refused.limit);
|
|
1151
|
+
}
|