@push.rocks/smartnftables 3.0.0 → 4.0.1
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 +20 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +4 -4
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +4 -4
- 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 +2 -0
- package/dist_ts/managed.types.d.ts +14 -0
- package/package.json +2 -2
- package/readme.md +69 -3
- package/rust/src/docker.policy.rs +2 -1
- package/rust/src/egress.compile.rs +8 -10
- package/rust/src/egress.host.rs +19 -1
- package/rust/src/egress.rs +28 -18
- package/rust/src/egress_hostgrant_tests.rs +9 -2
- package/rust/src/egress_localport_tests.rs +8 -1
- package/rust/src/egress_tests.rs +157 -0
- package/rust/src/egress_uplink_tests.rs +159 -0
- package/rust/src/egress_workloadgrant_tests.rs +15 -6
- package/rust/src/main.rs +57 -3
- package/rust/src/policy.rs +10 -13
- 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 +2 -0
- package/ts/managed.types.ts +17 -0
package/rust/src/egress.rs
CHANGED
|
@@ -484,7 +484,7 @@ pub fn destination(grant: &Grant, protection: &Protection) -> Result<String> {
|
|
|
484
484
|
/// with the leased outbound source ports translated to that same transit address.
|
|
485
485
|
fn normalize_router_published(value: &mut RouterScope) -> Result<()> {
|
|
486
486
|
let mut ports = std::mem::take(&mut value.published_ports);
|
|
487
|
-
|
|
487
|
+
crate::capacity("publishedPorts", PUBLISHED_PORTS, ports.len())?;
|
|
488
488
|
ports.sort();
|
|
489
489
|
for (index, port) in ports.iter().enumerate() {
|
|
490
490
|
require(
|
|
@@ -599,11 +599,12 @@ fn exclusive_symmetric<'a>(
|
|
|
599
599
|
/// the enclosing scope.
|
|
600
600
|
fn normalize_host_grants(
|
|
601
601
|
grants: &mut [HostGrant],
|
|
602
|
+
name: &'static str,
|
|
602
603
|
bound: impl Fn(&HostGrant) -> Result<()>,
|
|
603
604
|
) -> Result<()> {
|
|
604
605
|
// The contract maximum per projection; set lookups keep the rule graph
|
|
605
606
|
// constant, and the element budget holds this many in every scope.
|
|
606
|
-
|
|
607
|
+
crate::capacity(name, HOST_GRANTS, grants.len())?;
|
|
607
608
|
grants.sort();
|
|
608
609
|
for (index, grant) in grants.iter().enumerate() {
|
|
609
610
|
require(
|
|
@@ -648,7 +649,7 @@ fn normalize_router_host_grants(value: &mut RouterScope) -> Result<()> {
|
|
|
648
649
|
.any(|source| covers(source, &target) == Ok(true))
|
|
649
650
|
})
|
|
650
651
|
};
|
|
651
|
-
normalize_host_grants(&mut grants, |grant| {
|
|
652
|
+
normalize_host_grants(&mut grants, "hostGrants", |grant| {
|
|
652
653
|
let endpoint = workload(&grant.destination_address).ok_or(Error::Invalid)?;
|
|
653
654
|
require(
|
|
654
655
|
endpoint.interface_kind == "veth"
|
|
@@ -695,7 +696,7 @@ fn normalize_router_workload_grants(value: &mut RouterScope) -> Result<()> {
|
|
|
695
696
|
require(endpoint.interface_kind == "veth" && !local && !platform)?;
|
|
696
697
|
Ok(endpoint)
|
|
697
698
|
};
|
|
698
|
-
normalize_host_grants(&mut grants, |grant| {
|
|
699
|
+
normalize_host_grants(&mut grants, "workloadGrants", |grant| {
|
|
699
700
|
let source = workload(&grant.source_address)?;
|
|
700
701
|
let destination = workload(&grant.destination_address)?;
|
|
701
702
|
require(source.id != destination.id)
|
|
@@ -711,8 +712,11 @@ pub const ROUTER_RULES: usize = 1024;
|
|
|
711
712
|
pub const ROUTER_GRANTS: usize = 1024;
|
|
712
713
|
/// Publications per scope; they are set elements on both hops.
|
|
713
714
|
pub const PUBLISHED_PORTS: usize = 1024;
|
|
715
|
+
/// Host grants per scope and workload grants per router, each a set element.
|
|
716
|
+
pub const HOST_GRANTS: usize = 1024;
|
|
714
717
|
fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
715
|
-
|
|
718
|
+
crate::capacity("links", ROUTER_LINKS, value.links.len())?;
|
|
719
|
+
require(value.generations.len() <= 32)?;
|
|
716
720
|
let private = policy::Policy {
|
|
717
721
|
schema_version: 1,
|
|
718
722
|
revision,
|
|
@@ -779,7 +783,11 @@ fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
|
779
783
|
let mut zones = BTreeSet::new();
|
|
780
784
|
let mut labels = BTreeSet::new();
|
|
781
785
|
let mut classifiers: Vec<(&Grant, String)> = Vec::new();
|
|
782
|
-
|
|
786
|
+
crate::capacity(
|
|
787
|
+
"grants",
|
|
788
|
+
ROUTER_GRANTS,
|
|
789
|
+
value.generations.iter().map(|item| item.grants.len()).sum(),
|
|
790
|
+
)?;
|
|
783
791
|
value
|
|
784
792
|
.generations
|
|
785
793
|
.sort_by(|a, b| (&a.lease.id, a.lease.generation).cmp(&(&b.lease.id, b.lease.generation)));
|
|
@@ -805,8 +813,6 @@ fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
|
805
813
|
&& generation.conntrack_label.bytes().any(|item| item != b'0')
|
|
806
814
|
&& labels.insert(generation.conntrack_label.clone()),
|
|
807
815
|
)?;
|
|
808
|
-
count += generation.grants.len();
|
|
809
|
-
require(count <= ROUTER_GRANTS)?;
|
|
810
816
|
generation.grants.sort();
|
|
811
817
|
for grant in &generation.grants {
|
|
812
818
|
require(
|
|
@@ -903,7 +909,7 @@ fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
|
|
|
903
909
|
}
|
|
904
910
|
// An exception is only ever an exact flow into a guarded pool.
|
|
905
911
|
let mut grants = std::mem::take(&mut value.host_grants);
|
|
906
|
-
normalize_host_grants(&mut grants, |grant| {
|
|
912
|
+
normalize_host_grants(&mut grants, "hostGrants", |grant| {
|
|
907
913
|
require(covers_address(&value.prefixes, &grant.destination_address)?)
|
|
908
914
|
})?;
|
|
909
915
|
value.host_grants = grants;
|
|
@@ -913,7 +919,7 @@ fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
|
|
|
913
919
|
/// wildcard or range, each with exactly one owning uid. `(uid_t)-1` is not a
|
|
914
920
|
/// user; it is the kernel's "unchanged" sentinel.
|
|
915
921
|
fn normalize_local_tcp_port_owners(owners: &mut [LocalTcpPortOwner]) -> Result<()> {
|
|
916
|
-
|
|
922
|
+
crate::capacity("localTcpPortOwners", LOCAL_TCP_PORT_OWNERS, owners.len())?;
|
|
917
923
|
owners.sort();
|
|
918
924
|
for (index, owner) in owners.iter().enumerate() {
|
|
919
925
|
let ip = address(&owner.address)?;
|
|
@@ -939,7 +945,7 @@ const LOCAL_TCP_PORT_OWNERS: usize = 8;
|
|
|
939
945
|
fn normalize_host_host_grants(value: &mut HostScope) -> Result<()> {
|
|
940
946
|
let mut grants = std::mem::take(&mut value.host_grants);
|
|
941
947
|
let scope = &*value;
|
|
942
|
-
normalize_host_grants(&mut grants, |grant| {
|
|
948
|
+
normalize_host_grants(&mut grants, "hostGrants", |grant| {
|
|
943
949
|
let destination = &grant.destination_address;
|
|
944
950
|
require(
|
|
945
951
|
scope
|
|
@@ -984,7 +990,7 @@ fn normalize_host_host_grants(value: &mut HostScope) -> Result<()> {
|
|
|
984
990
|
/// overlap with the leased outbound source ports translated to that same address.
|
|
985
991
|
fn normalize_published(value: &mut HostScope) -> Result<()> {
|
|
986
992
|
let mut ports = std::mem::take(&mut value.published_ports);
|
|
987
|
-
|
|
993
|
+
crate::capacity("publishedPorts", PUBLISHED_PORTS, ports.len())?;
|
|
988
994
|
ports.sort();
|
|
989
995
|
for (index, port) in ports.iter().enumerate() {
|
|
990
996
|
require(
|
|
@@ -1096,13 +1102,14 @@ fn normalize_host(value: &mut HostScope) -> Result<()> {
|
|
|
1096
1102
|
.ok_or(Error::Invalid)?;
|
|
1097
1103
|
require(local_addresses.contains(&endpoint.address))?;
|
|
1098
1104
|
}
|
|
1105
|
+
// Handoff addresses lie in protected space, checked per handoff below. An
|
|
1106
|
+
// uplink address need not: the uplink lease and SNAT address is the host's
|
|
1107
|
+
// public side, usually outside the protected authority, and the compiler
|
|
1108
|
+
// adds each uncovered one to the host barrier as a protected destination.
|
|
1099
1109
|
for ip in &local_addresses {
|
|
1100
|
-
require(
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
endpoint.address != *ip || value.local_platform_endpoints.contains(&endpoint.id)
|
|
1104
|
-
}),
|
|
1105
|
-
)?;
|
|
1110
|
+
require(value.protection.platform_endpoints.iter().all(|endpoint| {
|
|
1111
|
+
endpoint.address != *ip || value.local_platform_endpoints.contains(&endpoint.id)
|
|
1112
|
+
}))?;
|
|
1106
1113
|
}
|
|
1107
1114
|
for handoff in &mut value.handoffs {
|
|
1108
1115
|
normalize_link(&mut handoff.link)?;
|
|
@@ -1180,6 +1187,9 @@ mod routerequivalence_tests;
|
|
|
1180
1187
|
#[path = "egress_tests.rs"]
|
|
1181
1188
|
pub(crate) mod tests;
|
|
1182
1189
|
#[cfg(test)]
|
|
1190
|
+
#[path = "egress_uplink_tests.rs"]
|
|
1191
|
+
mod uplink_tests;
|
|
1192
|
+
#[cfg(test)]
|
|
1183
1193
|
#[path = "egress_workloadgrant_tests.rs"]
|
|
1184
1194
|
pub(crate) mod workloadgrant_tests;
|
|
1185
1195
|
impl Policy {
|
|
@@ -1051,8 +1051,15 @@ fn host_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
|
1051
1051
|
program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES
|
|
1052
1052
|
);
|
|
1053
1053
|
assert!(deletion + graph + elements + 64 <= crate::wire::BATCH_BYTES);
|
|
1054
|
-
// The contract maximum is
|
|
1055
|
-
|
|
1054
|
+
// The contract maximum is a capacity bound.
|
|
1055
|
+
assert_eq!(
|
|
1056
|
+
normalized(granted(fixture, many(1025))).unwrap_err(),
|
|
1057
|
+
crate::Error::Exhausted(crate::Bound {
|
|
1058
|
+
name: "hostGrants",
|
|
1059
|
+
limit: 1024,
|
|
1060
|
+
actual: 1025
|
|
1061
|
+
})
|
|
1062
|
+
);
|
|
1056
1063
|
println!(
|
|
1057
1064
|
"HOST_GRANT_MAX scope={name} grants=1024 operations={} rule_bytes={graph} element_bytes={elements} batch_bound={}",
|
|
1058
1065
|
program.len(),
|
|
@@ -262,5 +262,12 @@ fn local_tcp_port_owners_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
|
262
262
|
.as_array_mut()
|
|
263
263
|
.unwrap()
|
|
264
264
|
.push(owner("127.0.0.1", 10008, 0));
|
|
265
|
-
|
|
265
|
+
assert_eq!(
|
|
266
|
+
normalized(value).unwrap_err(),
|
|
267
|
+
crate::Error::Exhausted(crate::Bound {
|
|
268
|
+
name: "localTcpPortOwners",
|
|
269
|
+
limit: 8,
|
|
270
|
+
actual: 9
|
|
271
|
+
})
|
|
272
|
+
);
|
|
266
273
|
}
|
package/rust/src/egress_tests.rs
CHANGED
|
@@ -992,3 +992,160 @@ fn router_v2_published_endpoint_traffic_is_classified_before_the_leased_grants()
|
|
|
992
992
|
);
|
|
993
993
|
}
|
|
994
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
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
//! Host transit on an uplink whose own addresses lie outside the protected
|
|
2
|
+
//! authority: the cluster-wide authority covers pools, resolvers, platform
|
|
3
|
+
//! endpoints and the VPN, not a node's uplink lease, which is usually public.
|
|
4
|
+
//! The scope prepares, and the host barrier protects the uplink addresses itself.
|
|
5
|
+
use super::hostgrant_tests::{
|
|
6
|
+
chains, decide, normalized, opening, prepared, Flow, ACK, ESTABLISHED,
|
|
7
|
+
};
|
|
8
|
+
use super::tests::host;
|
|
9
|
+
use serde_json::{json, Value};
|
|
10
|
+
|
|
11
|
+
const TRANSIT: [u8; 4] = [10, 240, 0, 2];
|
|
12
|
+
const UPLINK: [u8; 4] = [198, 51, 100, 7];
|
|
13
|
+
const PUBLIC: [u8; 4] = [198, 51, 100, 200];
|
|
14
|
+
const HANDOFF_LINK: Option<(u32, &str)> = Some((4, "handoff"));
|
|
15
|
+
const UPLINK_LINK: Option<(u32, &str)> = Some((2, "ens18"));
|
|
16
|
+
|
|
17
|
+
/// The `preparePolicy` request Pallet 35.1.0 sent pallet-guard on a lab node,
|
|
18
|
+
/// captured on the native IPC (lab-only private addresses). Its uplink lease and
|
|
19
|
+
/// SNAT address 10.92.26.30 lies outside the protected prefix 10.240.0.0/12; the
|
|
20
|
+
/// two other uplink addresses hold host-local platform endpoints inside it.
|
|
21
|
+
fn captured() -> Value {
|
|
22
|
+
json!({"schemaVersion":2,"revision":1,"scope":{"kind":"hostTransit",
|
|
23
|
+
"protection":{
|
|
24
|
+
"authorityDigest":"sha256:f436a15c8c8f4d4f0f23bc9fc69f45a8c9137a9586db1d2c2be8c09dac3467c9",
|
|
25
|
+
"prefixes":["10.240.0.0/12"],
|
|
26
|
+
"platformEndpoints":[
|
|
27
|
+
{"id":"5aaa706266a3c4e9b0cc99eb354496a732f88cdf3d36fd5911ab6fbb98ef7489","address":"10.244.0.30","port":8443,"protocol":"udp"},
|
|
28
|
+
{"id":"0298aa0b89b4248e058b7a279abc9d9c1793d2d012752432e95881367b43d6ac","address":"10.245.0.30","port":27017,"protocol":"tcp"},
|
|
29
|
+
{"id":"f0808a59e876a51c1a8a5b8cf38e0fc323dd5309dec3a8890468a1afc69dc9ff","address":"10.245.0.30","port":9000,"protocol":"tcp"},
|
|
30
|
+
{"id":"f4744ddb8e7d63883a7fbd4635f3ee4a031650effaaf5c13a1c664182b2b0501","address":"10.245.0.30","port":8443,"protocol":"tcp"},
|
|
31
|
+
{"id":"8933bdd0997dc2b230b0be7960c1708a57d72a0fa9481ec4aa56b9311ae68751","address":"10.243.0.1","port":53,"protocol":"tcp"},
|
|
32
|
+
{"id":"6e88da7a803f69d77d8d8fd86c5952ae87c26c3172b2f9a751662793e0ee8a7e","address":"10.243.0.1","port":53,"protocol":"udp"}]},
|
|
33
|
+
"handoffs":[{
|
|
34
|
+
"link":{"interfaceIndex":11,"interfaceName":"phc05f15afde9d","interfaceKind":"veth",
|
|
35
|
+
"macAddress":"4a:d5:5b:95:91:fa","interfaceLinkIndex":2,"requiredIpv4Addresses":["10.240.0.0"]},
|
|
36
|
+
"allocations":[{
|
|
37
|
+
"lease":{"id":"049eda6820b222e7e371bb4d1e24a325ac92422cff1900443c1e74a9efbb5d5e","generation":1,
|
|
38
|
+
"digest":"sha256:9a5635b213263796ac3b883cce942a0ca45161d9412ec6c6c9a5d748c22a3a7c"},
|
|
39
|
+
"transitSourceAddress":"10.240.0.1",
|
|
40
|
+
"sourcePortRanges":[{"protocol":"tcp","first":1024,"last":65535},{"protocol":"udp","first":1024,"last":65535}]}]}],
|
|
41
|
+
"uplink":{"interfaceIndex":2,"interfaceName":"enp1s0","interfaceKind":"ethernet",
|
|
42
|
+
"macAddress":"52:54:00:92:26:30","interfaceLinkIndex":2,
|
|
43
|
+
"requiredIpv4Addresses":["10.244.0.30","10.245.0.30","10.92.26.30"]},
|
|
44
|
+
"snatAddress":"10.92.26.30",
|
|
45
|
+
"localPlatformEndpoints":[
|
|
46
|
+
"0298aa0b89b4248e058b7a279abc9d9c1793d2d012752432e95881367b43d6ac",
|
|
47
|
+
"5aaa706266a3c4e9b0cc99eb354496a732f88cdf3d36fd5911ab6fbb98ef7489",
|
|
48
|
+
"f0808a59e876a51c1a8a5b8cf38e0fc323dd5309dec3a8890468a1afc69dc9ff",
|
|
49
|
+
"f4744ddb8e7d63883a7fbd4635f3ee4a031650effaaf5c13a1c664182b2b0501"]}})
|
|
50
|
+
}
|
|
51
|
+
/// The host fixture on a public uplink lease outside every protected prefix.
|
|
52
|
+
fn public() -> Value {
|
|
53
|
+
let mut value = host();
|
|
54
|
+
value["scope"]["uplink"]["requiredIpv4Addresses"] = json!(["198.51.100.7"]);
|
|
55
|
+
value["scope"]["snatAddress"] = json!("198.51.100.7");
|
|
56
|
+
value
|
|
57
|
+
}
|
|
58
|
+
/// A leased opening from the handoff out through the uplink, to `destination`
|
|
59
|
+
/// in the live tuple and `original` in the originally tracked one.
|
|
60
|
+
fn egress(destination: [u8; 4], original: [u8; 4]) -> Flow {
|
|
61
|
+
Flow {
|
|
62
|
+
source: TRANSIT,
|
|
63
|
+
destination,
|
|
64
|
+
source_port: 10500,
|
|
65
|
+
destination_port: 443,
|
|
66
|
+
original: (TRANSIT, original, 10500, 443),
|
|
67
|
+
..opening(HANDOFF_LINK, UPLINK_LINK)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[test]
|
|
72
|
+
fn host_transit_prepares_the_captured_lab_request_with_an_unprotected_uplink_lease() {
|
|
73
|
+
// Refused INVALID by 4.0.0: the uplink lease was required to be protected.
|
|
74
|
+
let lab = prepared(captured());
|
|
75
|
+
// The lease joins the barrier as a /32, exactly as if the authority had
|
|
76
|
+
// named it; the covered uplink addresses meet their prefix already.
|
|
77
|
+
let mut value = captured();
|
|
78
|
+
value["scope"]["protection"]["prefixes"] = json!(["10.240.0.0/12", "10.92.26.30/32"]);
|
|
79
|
+
assert_eq!(chains(&lab), chains(&prepared(value)));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn host_transit_uplink_addresses_need_no_protected_prefix_but_handoff_addresses_do() {
|
|
84
|
+
assert!(normalized(public()).is_ok());
|
|
85
|
+
// A handoff address outside the authority still refuses; the control
|
|
86
|
+
// covering it prepares.
|
|
87
|
+
let handoff = |prefix: &str| {
|
|
88
|
+
let mut value = public();
|
|
89
|
+
value["scope"]["protection"]["prefixes"] =
|
|
90
|
+
json!([prefix, "192.0.2.0/24", "203.0.113.10/32"]);
|
|
91
|
+
normalized(value)
|
|
92
|
+
};
|
|
93
|
+
assert!(handoff("10.240.0.2/32").is_err());
|
|
94
|
+
assert!(handoff("10.240.0.0/30").is_ok());
|
|
95
|
+
// A platform endpoint on the uplink lease lies in protected space, and it
|
|
96
|
+
// still refuses unless declared host-local.
|
|
97
|
+
let mut value = public();
|
|
98
|
+
value["scope"]["protection"]["prefixes"] =
|
|
99
|
+
json!(["10.0.0.0/8", "192.0.2.0/24", "198.51.100.7/32"]);
|
|
100
|
+
value["scope"]["protection"]["platformEndpoints"] = json!([
|
|
101
|
+
{"id":"hub","address":"198.51.100.7","protocol":"tcp","port":8443}]);
|
|
102
|
+
assert!(normalized(value.clone()).is_err());
|
|
103
|
+
value["scope"]["localPlatformEndpoints"] = json!(["hub"]);
|
|
104
|
+
assert!(normalized(value).is_ok());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn host_transit_barrier_protects_an_unprotected_uplink_lease_as_a_destination() {
|
|
109
|
+
let with = prepared(public());
|
|
110
|
+
// Positive control: leased egress to public space passes, including a flow
|
|
111
|
+
// another owner translated between two public destinations.
|
|
112
|
+
for flow in [egress(PUBLIC, PUBLIC), egress(PUBLIC, [198, 51, 100, 201])] {
|
|
113
|
+
assert_eq!(decide(&with, "forward", &flow).verdict, 1);
|
|
114
|
+
}
|
|
115
|
+
// A flow whose live or originally tracked destination is the host's own
|
|
116
|
+
// uplink lease is a translation into the host: the barrier refuses it.
|
|
117
|
+
for flow in [egress(PUBLIC, UPLINK), egress(UPLINK, PUBLIC)] {
|
|
118
|
+
let decision = decide(&with, "forward", &flow);
|
|
119
|
+
assert_eq!(
|
|
120
|
+
(decision.verdict, decision.chain.as_str()),
|
|
121
|
+
(0, "protected_out")
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
// A packet forwarded toward the handoff from the uplink lease is a spoof.
|
|
125
|
+
let reply = Flow {
|
|
126
|
+
input: UPLINK_LINK,
|
|
127
|
+
output: HANDOFF_LINK,
|
|
128
|
+
source: UPLINK,
|
|
129
|
+
destination: TRANSIT,
|
|
130
|
+
source_port: 443,
|
|
131
|
+
destination_port: 10500,
|
|
132
|
+
tcp_flags: ACK,
|
|
133
|
+
state: ESTABLISHED,
|
|
134
|
+
reply: true,
|
|
135
|
+
..egress(PUBLIC, PUBLIC)
|
|
136
|
+
};
|
|
137
|
+
let decision = decide(&with, "forward", &reply);
|
|
138
|
+
assert_eq!(
|
|
139
|
+
(decision.verdict, decision.chain.as_str()),
|
|
140
|
+
(0, "protected_back")
|
|
141
|
+
);
|
|
142
|
+
let reply = Flow {
|
|
143
|
+
source: PUBLIC,
|
|
144
|
+
..reply
|
|
145
|
+
};
|
|
146
|
+
assert_eq!(decide(&with, "forward", &reply).verdict, 1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[test]
|
|
150
|
+
fn host_transit_covered_uplink_compiles_exactly_the_authority_barrier() {
|
|
151
|
+
// The fixture's uplink lies inside the authority: no address joins the
|
|
152
|
+
// barrier, and its compiled bytes stay frozen by the golden host test.
|
|
153
|
+
let covered = chains(&prepared(host()));
|
|
154
|
+
let public = chains(&prepared(public()));
|
|
155
|
+
for name in ["protected_out", "protected_back"] {
|
|
156
|
+
// One /32 adds a live and an originally tracked destination check.
|
|
157
|
+
assert_eq!(public[name].len(), covered[name].len() + 2, "{name}");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -241,12 +241,6 @@ fn workload_grants_reject_unbound_same_link_local_platform_and_tun_ends() {
|
|
|
241
241
|
json!([{"protocol":"tcp","sourceAddress":"10.241.0.2","destinationAddress":"10.241.1.2",
|
|
242
242
|
"destinationPort":80,"bidirectional":true}]),
|
|
243
243
|
),
|
|
244
|
-
granted(
|
|
245
|
-
two_workloads(),
|
|
246
|
-
json!((0..1025_u16)
|
|
247
|
-
.map(|port| workload_grant("tcp", "10.241.0.2", "10.241.1.2", 1 + port))
|
|
248
|
-
.collect::<Vec<_>>()),
|
|
249
|
-
),
|
|
250
244
|
] {
|
|
251
245
|
assert!(
|
|
252
246
|
normalized(invalid.clone()).is_err(),
|
|
@@ -254,6 +248,21 @@ fn workload_grants_reject_unbound_same_link_local_platform_and_tun_ends() {
|
|
|
254
248
|
invalid["scope"]["workloadGrants"]
|
|
255
249
|
);
|
|
256
250
|
}
|
|
251
|
+
// The contract maximum is a capacity bound.
|
|
252
|
+
assert_eq!(
|
|
253
|
+
normalized(granted(
|
|
254
|
+
two_workloads(),
|
|
255
|
+
json!((0..1025_u16)
|
|
256
|
+
.map(|port| workload_grant("tcp", "10.241.0.2", "10.241.1.2", 1 + port))
|
|
257
|
+
.collect::<Vec<_>>()),
|
|
258
|
+
))
|
|
259
|
+
.unwrap_err(),
|
|
260
|
+
crate::Error::Exhausted(crate::Bound {
|
|
261
|
+
name: "workloadGrants",
|
|
262
|
+
limit: 1024,
|
|
263
|
+
actual: 1025
|
|
264
|
+
})
|
|
265
|
+
);
|
|
257
266
|
// Both directions between two workloads are two separate grants.
|
|
258
267
|
assert!(normalized(granted(
|
|
259
268
|
two_workloads(),
|
package/rust/src/main.rs
CHANGED
|
@@ -15,9 +15,22 @@ use std::io::{BufRead, Read, Write};
|
|
|
15
15
|
/// same lines. A transition carries two complete policies.
|
|
16
16
|
pub(crate) const IPC_BYTES: usize = 1_048_576;
|
|
17
17
|
|
|
18
|
+
/// A capacity bound of the atomic replacement budget that a policy exceeded: the
|
|
19
|
+
/// stable bound name, its limit and the size the policy reached. Input counts and
|
|
20
|
+
/// the schema-v1 budget are exact; the other compiled budgets stop at the first
|
|
21
|
+
/// message or rule past the limit, so the complete policy needs at least `actual`.
|
|
22
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
23
|
+
pub struct Bound {
|
|
24
|
+
pub name: &'static str,
|
|
25
|
+
pub limit: usize,
|
|
26
|
+
pub actual: usize,
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
19
30
|
pub enum Error {
|
|
20
31
|
Invalid,
|
|
32
|
+
/// The policy does not fit a capacity bound; a smaller policy may fit.
|
|
33
|
+
Exhausted(Bound),
|
|
21
34
|
Unsupported,
|
|
22
35
|
UnsupportedBackend,
|
|
23
36
|
Permission,
|
|
@@ -28,10 +41,23 @@ pub enum Error {
|
|
|
28
41
|
Unavailable,
|
|
29
42
|
}
|
|
30
43
|
pub type Result<T> = std::result::Result<T, Error>;
|
|
44
|
+
/// Refuses `actual` above the capacity bound `name`.
|
|
45
|
+
pub fn capacity(name: &'static str, limit: usize, actual: usize) -> Result<()> {
|
|
46
|
+
if actual > limit {
|
|
47
|
+
Err(Error::Exhausted(Bound {
|
|
48
|
+
name,
|
|
49
|
+
limit,
|
|
50
|
+
actual,
|
|
51
|
+
}))
|
|
52
|
+
} else {
|
|
53
|
+
Ok(())
|
|
54
|
+
}
|
|
55
|
+
}
|
|
31
56
|
impl Error {
|
|
32
57
|
pub fn code(self) -> &'static str {
|
|
33
58
|
match self {
|
|
34
59
|
Self::Invalid => "INVALID",
|
|
60
|
+
Self::Exhausted(_) => "EXHAUSTED",
|
|
35
61
|
Self::Unsupported => "UNSUPPORTED_KERNEL",
|
|
36
62
|
Self::UnsupportedBackend => "UNSUPPORTED_BACKEND",
|
|
37
63
|
Self::Permission => "PERMISSION",
|
|
@@ -42,6 +68,26 @@ impl Error {
|
|
|
42
68
|
Self::Unavailable => "UNAVAILABLE",
|
|
43
69
|
}
|
|
44
70
|
}
|
|
71
|
+
/// The bounded, non-secret refusal text of the response.
|
|
72
|
+
pub fn message(self) -> String {
|
|
73
|
+
match self {
|
|
74
|
+
Self::Invalid => "Managed policy input is invalid.".into(),
|
|
75
|
+
Self::Exhausted(bound) => format!(
|
|
76
|
+
"Managed policy exceeds the {} capacity of {}: it reached {}.",
|
|
77
|
+
bound.name, bound.limit, bound.actual
|
|
78
|
+
),
|
|
79
|
+
_ => "Managed policy operation failed.".into(),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/// The structured refusal details of the response, if any.
|
|
83
|
+
pub fn data(self) -> Option<serde_json::Value> {
|
|
84
|
+
match self {
|
|
85
|
+
Self::Exhausted(bound) => Some(
|
|
86
|
+
serde_json::json!({"bound":bound.name,"limit":bound.limit,"actual":bound.actual}),
|
|
87
|
+
),
|
|
88
|
+
_ => None,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
45
91
|
pub fn kernel(code: i32) -> Self {
|
|
46
92
|
match -code {
|
|
47
93
|
libc::ENOENT => Self::Absent,
|
|
@@ -114,6 +160,16 @@ fn dispatch(owner: &mut Option<owner::Owner>, request: Request) -> Result<serde_
|
|
|
114
160
|
}
|
|
115
161
|
}
|
|
116
162
|
|
|
163
|
+
/// The failure response line: the refusal text, the stable code and, for a
|
|
164
|
+
/// capacity refusal, its bound.
|
|
165
|
+
fn failure(id: &str, error: Error) -> serde_json::Value {
|
|
166
|
+
let mut value = serde_json::json!({"id":id,"success":false,"error":error.message(),"errorCode":error.code()});
|
|
167
|
+
if let Some(data) = error.data() {
|
|
168
|
+
value["errorData"] = data;
|
|
169
|
+
}
|
|
170
|
+
value
|
|
171
|
+
}
|
|
172
|
+
|
|
117
173
|
fn main() {
|
|
118
174
|
let arguments = std::env::args().skip(1).collect::<Vec<_>>();
|
|
119
175
|
let (docker_mode, descriptor) = match arguments.as_slice() {
|
|
@@ -163,9 +219,7 @@ fn main() {
|
|
|
163
219
|
let id = request.id.clone();
|
|
164
220
|
let response = match if docker_mode { docker::dispatch(&mut docker, request) } else { dispatch(&mut owner, request) } {
|
|
165
221
|
Ok(result) => serde_json::json!({"id":id,"success":true,"result":result}),
|
|
166
|
-
Err(error) =>
|
|
167
|
-
serde_json::json!({"id":id,"success":false,"error":"Managed policy operation failed.","errorCode":error.code()})
|
|
168
|
-
}
|
|
222
|
+
Err(error) => failure(&id, error),
|
|
169
223
|
};
|
|
170
224
|
if writeln!(output, "{response}")
|
|
171
225
|
.and_then(|_| output.flush())
|
package/rust/src/policy.rs
CHANGED
|
@@ -101,29 +101,26 @@ impl Policy {
|
|
|
101
101
|
let program = prepared.program(&format!("snft_{}", "x".repeat(59)))?;
|
|
102
102
|
// Reserve enough of the batch for deleting a previous maximum
|
|
103
103
|
// graph, then installing this complete graph in the same transaction.
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
crate::capacity("operations", 768, program.len())?;
|
|
105
|
+
crate::capacity(
|
|
106
|
+
"ruleBytes",
|
|
107
|
+
100_000,
|
|
108
|
+
program
|
|
106
109
|
.iter()
|
|
107
110
|
.map(|(_, attrs)| wire::encode_attrs(attrs).len() + 20)
|
|
108
|
-
.sum
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
return Err(Error::Invalid);
|
|
112
|
-
}
|
|
111
|
+
.sum(),
|
|
112
|
+
)?;
|
|
113
113
|
Ok(prepared)
|
|
114
114
|
}
|
|
115
115
|
/// Validates and orders the endpoints and rules. Schema v1 compiles one rule
|
|
116
116
|
/// per endpoint and grant, so it admits 32 endpoints and 128 rules; the
|
|
117
117
|
/// set-backed router scope of schema v2 admits more.
|
|
118
118
|
pub(crate) fn normalize(mut self, endpoints: usize, rules: usize) -> Result<Self> {
|
|
119
|
-
if self.schema_version != 1
|
|
120
|
-
|| self.revision == 0
|
|
121
|
-
|| self.revision > 9_007_199_254_740_991
|
|
122
|
-
|| self.endpoints.len() > endpoints
|
|
123
|
-
|| self.rules.len() > rules
|
|
124
|
-
{
|
|
119
|
+
if self.schema_version != 1 || self.revision == 0 || self.revision > 9_007_199_254_740_991 {
|
|
125
120
|
return Err(Error::Invalid);
|
|
126
121
|
}
|
|
122
|
+
crate::capacity("endpoints", endpoints, self.endpoints.len())?;
|
|
123
|
+
crate::capacity("rules", rules, self.rules.len())?;
|
|
127
124
|
let mut ids = BTreeSet::new();
|
|
128
125
|
let mut indices = BTreeSet::new();
|
|
129
126
|
let mut names = BTreeSet::new();
|