@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
|
@@ -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(),
|
|
@@ -297,18 +306,17 @@ fn workload_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
|
297
306
|
.sum()
|
|
298
307
|
};
|
|
299
308
|
let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
|
|
300
|
-
assert!(program.len() < 768 && graph <=
|
|
309
|
+
assert!(program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES);
|
|
301
310
|
assert!(deletion + graph + elements + 64 <= wire::BATCH_BYTES);
|
|
302
311
|
println!(
|
|
303
312
|
"WORKLOAD_GRANT_MAX grants=1024 operations={} rule_bytes={graph} element_bytes={elements}",
|
|
304
313
|
program.len()
|
|
305
314
|
);
|
|
306
|
-
// Host grants and workload grants share
|
|
307
|
-
//
|
|
308
|
-
// together are refused before any kernel work.
|
|
315
|
+
// Host grants and workload grants share the target budget with the rest of
|
|
316
|
+
// the scope's elements; both maxima together fit it.
|
|
309
317
|
let mut both = granted(two_workloads(), json!(maximum));
|
|
310
|
-
both["scope"]["hostGrants"] = many(512);
|
|
311
|
-
normalized(both.clone()).unwrap().prepare().unwrap();
|
|
312
318
|
both["scope"]["hostGrants"] = many(1024);
|
|
313
|
-
|
|
319
|
+
let both = normalized(both).unwrap().prepare().unwrap();
|
|
320
|
+
let (operations, rule_bytes, element_bytes) = super::tests::cost(&both);
|
|
321
|
+
assert!(operations < 768 && rule_bytes + element_bytes <= super::compile::TARGET_BYTES);
|
|
314
322
|
}
|
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/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
|
}
|
|
@@ -389,6 +389,8 @@ fn v2_host_publishes_exact_uplink_ports_and_removes_them_with_the_generation() {
|
|
|
389
389
|
target_port: 8443,
|
|
390
390
|
target_address: "10.240.0.2".into(),
|
|
391
391
|
host_ip: "192.0.2.2".into(),
|
|
392
|
+
host_port_end: None,
|
|
393
|
+
symmetric: false,
|
|
392
394
|
},
|
|
393
395
|
egress::PublishedPort {
|
|
394
396
|
protocol: "udp".into(),
|
|
@@ -396,6 +398,8 @@ fn v2_host_publishes_exact_uplink_ports_and_removes_them_with_the_generation() {
|
|
|
396
398
|
target_port: 8125,
|
|
397
399
|
target_address: "10.240.0.2".into(),
|
|
398
400
|
host_ip: "192.0.2.2".into(),
|
|
401
|
+
host_port_end: None,
|
|
402
|
+
symmetric: false,
|
|
399
403
|
},
|
|
400
404
|
];
|
|
401
405
|
let mut owner = Owner::new(options("host_published")).unwrap();
|
|
@@ -168,94 +168,162 @@ fn egress_identity_rejects_unknown_duplicate_or_incomplete_operands() {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/// The compiled chains, rules, and sets with their element identities of one
|
|
172
|
+
/// prepared policy, as the owner's graph comparison sees them.
|
|
173
|
+
type Compiled = (
|
|
174
|
+
Vec<Vec<u8>>,
|
|
175
|
+
Vec<(String, Vec<u8>)>,
|
|
176
|
+
std::collections::BTreeMap<String, (Vec<u8>, Vec<Vec<u8>>)>,
|
|
177
|
+
);
|
|
178
|
+
fn compiled(value: serde_json::Value) -> Compiled {
|
|
179
|
+
let prepared = serde_json::from_value::<crate::egress::Policy>(value)
|
|
180
|
+
.unwrap()
|
|
181
|
+
.prepare()
|
|
182
|
+
.unwrap();
|
|
183
|
+
let (mut chains, mut rules) = (Vec::new(), Vec::new());
|
|
184
|
+
let mut sets = std::collections::BTreeMap::new();
|
|
185
|
+
for (kind, attributes) in prepared.program("snft_identity").unwrap() {
|
|
186
|
+
match kind {
|
|
187
|
+
3 => chains.push(super::chain_identity(&attributes).unwrap()),
|
|
188
|
+
6 => {
|
|
189
|
+
// The stamp carries the policy digest; compare the rule itself.
|
|
190
|
+
let unstamped: Vec<Attr> = attributes
|
|
191
|
+
.iter()
|
|
192
|
+
.map(|attribute| match attribute.id() {
|
|
193
|
+
7 => Attr::bytes(7, b"stamp".to_vec()),
|
|
194
|
+
_ => attribute.clone(),
|
|
195
|
+
})
|
|
196
|
+
.collect();
|
|
197
|
+
rules.push(rule_identity(&unstamped).unwrap());
|
|
198
|
+
}
|
|
199
|
+
9 => {
|
|
200
|
+
// The kernel's dump of the same set has the same identity.
|
|
201
|
+
let identity = set_identity(&attributes).unwrap();
|
|
202
|
+
assert_eq!(set_identity(&dumped_set(&attributes)).unwrap(), identity);
|
|
203
|
+
sets.insert(wire::text(&attributes, 2).unwrap(), (identity, Vec::new()));
|
|
204
|
+
}
|
|
205
|
+
12 => {
|
|
206
|
+
let list = wire::attrs(&wire::one(&attributes, 3).unwrap().value).unwrap();
|
|
207
|
+
sets.get_mut(&wire::text(&attributes, 2).unwrap())
|
|
208
|
+
.unwrap()
|
|
209
|
+
.1
|
|
210
|
+
.extend(element_keys(&list).unwrap());
|
|
211
|
+
}
|
|
212
|
+
other => panic!("operation {other}"),
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
(chains, rules, sets)
|
|
216
|
+
}
|
|
217
|
+
|
|
171
218
|
/// A published host-transit generation must survive the same apply-time graph
|
|
172
|
-
/// identity as the rest of the table:
|
|
173
|
-
/// operand, so destination NAT and its admissions cannot be
|
|
219
|
+
/// identity as the rest of the table: its rules and every set element are
|
|
220
|
+
/// compared operand by operand, so destination NAT and its admissions cannot be
|
|
221
|
+
/// silently rewritten, while the rules stay the same for any publication.
|
|
174
222
|
#[test]
|
|
175
223
|
fn published_host_graph_identity_preserves_destination_nat_and_admission_operands() {
|
|
176
|
-
let
|
|
177
|
-
|
|
224
|
+
let host = |target_port: u16| {
|
|
225
|
+
compiled(crate::egress::tests::published_host(serde_json::json!([
|
|
178
226
|
crate::egress::tests::published_port("tcp", 443, target_port)
|
|
179
|
-
]))
|
|
180
|
-
let prepared = serde_json::from_value::<crate::egress::Policy>(value)
|
|
181
|
-
.unwrap()
|
|
182
|
-
.prepare()
|
|
183
|
-
.unwrap();
|
|
184
|
-
let mut chains = Vec::new();
|
|
185
|
-
let mut rules = Vec::new();
|
|
186
|
-
for (kind, attributes) in prepared.program("snft_identity").unwrap() {
|
|
187
|
-
if kind == 3 {
|
|
188
|
-
chains.push(super::chain_identity(&attributes).unwrap());
|
|
189
|
-
} else {
|
|
190
|
-
rules.push(rule_identity(&attributes).unwrap());
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
(chains, rules)
|
|
227
|
+
])))
|
|
194
228
|
};
|
|
195
|
-
let (chains, rules) =
|
|
229
|
+
let (chains, rules, sets) = host(8443);
|
|
196
230
|
assert_eq!(rules.iter().filter(|(chain, _)| chain == "pre").count(), 1);
|
|
197
231
|
assert_eq!(chains.len(), 7);
|
|
198
|
-
let
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
);
|
|
232
|
+
let (_, changed_rules, changed_sets) = host(9443);
|
|
233
|
+
assert_eq!(rules, changed_rules);
|
|
234
|
+
// The translation's port is map data and the admission's port is a key.
|
|
235
|
+
for name in ["published_port", "published_forward"] {
|
|
236
|
+
assert_eq!(sets[name].0, changed_sets[name].0);
|
|
237
|
+
assert_ne!(sets[name].1, changed_sets[name].1, "{name}");
|
|
238
|
+
}
|
|
206
239
|
}
|
|
207
240
|
|
|
208
241
|
/// A published router generation must survive the same apply-time graph identity
|
|
209
|
-
/// as the rest of the table:
|
|
210
|
-
/// the workload translation
|
|
242
|
+
/// as the rest of the table: its rules and every set element are compared operand
|
|
243
|
+
/// by operand, so the workload translation, both classifications and the
|
|
244
|
+
/// admissions cannot be silently rewritten.
|
|
211
245
|
#[test]
|
|
212
246
|
fn published_router_graph_identity_preserves_destination_nat_and_admission_operands() {
|
|
213
|
-
let
|
|
214
|
-
|
|
247
|
+
let router = |endpoint_port: u16| {
|
|
248
|
+
compiled(crate::egress::tests::published_router(serde_json::json!([
|
|
215
249
|
crate::egress::tests::router_published_port("tcp", 443, endpoint_port)
|
|
216
|
-
]))
|
|
217
|
-
let prepared = serde_json::from_value::<crate::egress::Policy>(value)
|
|
218
|
-
.unwrap()
|
|
219
|
-
.prepare()
|
|
220
|
-
.unwrap();
|
|
221
|
-
let mut chains = Vec::new();
|
|
222
|
-
let mut rules = Vec::new();
|
|
223
|
-
for (kind, attributes) in prepared.program("snft_identity").unwrap() {
|
|
224
|
-
if kind == 3 {
|
|
225
|
-
chains.push(super::chain_identity(&attributes).unwrap());
|
|
226
|
-
} else {
|
|
227
|
-
rules.push(rule_identity(&attributes).unwrap());
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
(chains, rules)
|
|
250
|
+
])))
|
|
231
251
|
};
|
|
232
|
-
let (chains, rules) =
|
|
252
|
+
let (chains, rules, sets) = router(8443);
|
|
233
253
|
assert_eq!(rules.iter().filter(|(chain, _)| chain == "pre").count(), 1);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
let (_,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
254
|
+
// The router's base chains, the pre chain and its three regular chains.
|
|
255
|
+
assert_eq!(chains.len(), 10);
|
|
256
|
+
let (_, changed_rules, changed_sets) = router(9443);
|
|
257
|
+
assert_eq!(rules, changed_rules);
|
|
258
|
+
for name in ["published_port", "published_forward", "published_endpoint"] {
|
|
259
|
+
assert_ne!(sets[name].1, changed_sets[name].1, "{name}");
|
|
260
|
+
}
|
|
261
|
+
assert_eq!(sets["published_arrival"], changed_sets["published_arrival"]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// Interval elements carry their last key and a map's data in their identity;
|
|
265
|
+
/// a changed bound or datum is a changed element, and a set description is only
|
|
266
|
+
/// ever a concatenation.
|
|
267
|
+
#[test]
|
|
268
|
+
fn interval_sets_and_elements_have_exact_graph_identity() {
|
|
269
|
+
let (_, _, sets) = compiled(crate::egress::tests::published_router(serde_json::json!([
|
|
270
|
+
crate::egress::tests::router_published_port("tcp", 443, 8443)
|
|
271
|
+
])));
|
|
244
272
|
assert_eq!(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
.iter()
|
|
248
|
-
.filter(|(chain, _)| chain == "raw_return")
|
|
249
|
-
.count()
|
|
273
|
+
sets.keys().filter(|name| name.starts_with("published_")).count(),
|
|
274
|
+
4
|
|
250
275
|
);
|
|
276
|
+
let element = |last: u8, data: Option<u8>| {
|
|
277
|
+
let value = |kind: u16, byte: u8| Attr::nested(kind, vec![Attr::bytes(1, vec![byte; 8])]);
|
|
278
|
+
let mut fields = vec![value(1, 1), value(10, last)];
|
|
279
|
+
if let Some(data) = data {
|
|
280
|
+
fields.push(value(2, data));
|
|
281
|
+
}
|
|
282
|
+
Attr::nested(1, fields)
|
|
283
|
+
};
|
|
284
|
+
let identity = |attribute: Attr| element_keys(&[attribute]).unwrap();
|
|
285
|
+
let exact = identity(element(1, Some(4)));
|
|
286
|
+
assert_ne!(identity(element(2, Some(4))), exact);
|
|
287
|
+
assert_ne!(identity(element(1, Some(5))), exact);
|
|
288
|
+
assert_ne!(identity(element(1, None)), exact);
|
|
289
|
+
for extension in [
|
|
290
|
+
Attr::u32(3, 1),
|
|
291
|
+
Attr::nested(10, vec![Attr::bytes(1, vec![1; 8])]),
|
|
292
|
+
] {
|
|
293
|
+
let Attr { value, .. } = element(1, Some(4));
|
|
294
|
+
let mut fields = wire::attrs(&value).unwrap();
|
|
295
|
+
fields.push(extension);
|
|
296
|
+
assert!(element_keys(&[Attr::nested(1, fields)]).is_err());
|
|
297
|
+
}
|
|
251
298
|
}
|
|
252
299
|
|
|
253
|
-
/// A
|
|
254
|
-
///
|
|
300
|
+
/// A set as Linux dumps it: the compiled creation plus the kernel's handle, its
|
|
301
|
+
/// description (empty for a plain key, the concatenation's field lengths without
|
|
302
|
+
/// nesting flags otherwise), the backend type and the element count.
|
|
255
303
|
fn dumped_set(created: &[Attr]) -> Vec<Attr> {
|
|
256
|
-
let mut dump: Vec<Attr> = created
|
|
304
|
+
let mut dump: Vec<Attr> = created
|
|
305
|
+
.iter()
|
|
306
|
+
.filter(|attribute| ![9, 10].contains(&attribute.id()))
|
|
307
|
+
.cloned()
|
|
308
|
+
.collect();
|
|
257
309
|
dump.push(Attr::u64(16, 7));
|
|
258
|
-
|
|
310
|
+
let fields = created
|
|
311
|
+
.iter()
|
|
312
|
+
.find(|attribute| attribute.id() == 9)
|
|
313
|
+
.map(|description| {
|
|
314
|
+
let concatenation = wire::attrs(&description.value).unwrap();
|
|
315
|
+
wire::attrs(&wire::one(&concatenation, 2).unwrap().value)
|
|
316
|
+
.unwrap()
|
|
317
|
+
.iter()
|
|
318
|
+
.map(|field| {
|
|
319
|
+
Attr::bytes(1, wire::encode_attrs(&[Attr::u32(1, wire::number(&wire::attrs(&field.value).unwrap(), 1).unwrap())]))
|
|
320
|
+
})
|
|
321
|
+
.collect::<Vec<_>>()
|
|
322
|
+
});
|
|
323
|
+
dump.push(match fields {
|
|
324
|
+
Some(fields) => Attr::bytes(9, wire::encode_attrs(&[Attr::bytes(2, wire::encode_attrs(&fields))])),
|
|
325
|
+
None => Attr::nested(9, vec![]),
|
|
326
|
+
});
|
|
259
327
|
dump.push(Attr::string(19, "nft_rhash_type"));
|
|
260
328
|
dump.push(Attr::u32(20, 1024));
|
|
261
329
|
dump
|
|
@@ -273,7 +341,11 @@ fn host_grant_sets_elements_and_lookups_have_exact_graph_identity() {
|
|
|
273
341
|
.prepare()
|
|
274
342
|
.unwrap();
|
|
275
343
|
let program = prepared.program("snft_identity").unwrap();
|
|
276
|
-
let sets: Vec<_> = program
|
|
344
|
+
let sets: Vec<_> = program
|
|
345
|
+
.iter()
|
|
346
|
+
.filter(|(kind, attributes)| *kind == 9 && wire::text(attributes, 2).unwrap().starts_with("host_grant"))
|
|
347
|
+
.map(|(_, attributes)| attributes.clone())
|
|
348
|
+
.collect();
|
|
277
349
|
assert_eq!(sets.len(), 2);
|
|
278
350
|
for created in &sets {
|
|
279
351
|
// The kernel's informational attributes are not identity.
|
|
@@ -294,21 +366,30 @@ fn host_grant_sets_elements_and_lookups_have_exact_graph_identity() {
|
|
|
294
366
|
assert!(set_identity(&changed).is_err());
|
|
295
367
|
}
|
|
296
368
|
}
|
|
297
|
-
// Elements are plain keys;
|
|
298
|
-
let (_, elements) = program
|
|
369
|
+
// Elements are plain keys; flags reject, and data is a different element.
|
|
370
|
+
let (_, elements) = program
|
|
371
|
+
.iter()
|
|
372
|
+
.find(|(kind, attributes)| *kind == 12 && wire::text(attributes, 2).unwrap() == "host_grant")
|
|
373
|
+
.unwrap();
|
|
299
374
|
let list = wire::attrs(&wire::one(elements, 3).unwrap().value).unwrap();
|
|
300
375
|
assert_eq!(element_keys(&list).unwrap().len(), 1);
|
|
301
376
|
let key = wire::attrs(&list[0].value).unwrap();
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
377
|
+
let mut flagged = key.clone();
|
|
378
|
+
flagged.push(Attr::u32(3, 1));
|
|
379
|
+
assert!(element_keys(&[Attr::nested(1, flagged)]).is_err());
|
|
380
|
+
let mut mapped = key.clone();
|
|
381
|
+
mapped.push(Attr::nested(2, vec![Attr::bytes(1, vec![1; 4])]));
|
|
382
|
+
assert_ne!(
|
|
383
|
+
element_keys(&[Attr::nested(1, mapped)]).unwrap(),
|
|
384
|
+
element_keys(&list[..1]).unwrap()
|
|
385
|
+
);
|
|
386
|
+
// Five rules look up the host grant sets (the router's private and leased
|
|
387
|
+
// sets have their own lookups); the set name and key registers are identity.
|
|
309
388
|
let lookups: Vec<_> = program
|
|
310
389
|
.iter()
|
|
311
|
-
.filter(|(kind, attributes)|
|
|
390
|
+
.filter(|(kind, attributes)| {
|
|
391
|
+
*kind == 6 && wire::encode_attrs(attributes).windows(10).any(|window| window == b"host_grant")
|
|
392
|
+
})
|
|
312
393
|
.collect();
|
|
313
394
|
assert_eq!(lookups.len(), 5);
|
|
314
395
|
let lookup = |data: Vec<Attr>| rule(vec![expression("lookup", data)]);
|
|
@@ -317,13 +398,18 @@ fn host_grant_sets_elements_and_lookups_have_exact_graph_identity() {
|
|
|
317
398
|
for changed in [
|
|
318
399
|
vec![Attr::string(1, "host_grant_arrival"), Attr::u32(2, 1), Attr::u32(5, 0)],
|
|
319
400
|
vec![Attr::string(1, "host_grant"), Attr::u32(2, 9), Attr::u32(5, 0)],
|
|
401
|
+
// A map lookup's data register and an absence test are identity.
|
|
402
|
+
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(3, 2), Attr::u32(5, 0)],
|
|
403
|
+
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(5, 1)],
|
|
320
404
|
] {
|
|
321
405
|
assert_ne!(rule_identity(&lookup(changed)).unwrap(), identity);
|
|
322
406
|
}
|
|
323
407
|
for invalid in [
|
|
324
|
-
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(5,
|
|
408
|
+
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(5, 2)],
|
|
325
409
|
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1)],
|
|
326
|
-
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(3, 2)
|
|
410
|
+
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(3, 2)],
|
|
411
|
+
// A map lookup is never inverted.
|
|
412
|
+
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(3, 2), Attr::u32(5, 1)],
|
|
327
413
|
] {
|
|
328
414
|
assert!(rule_identity(&lookup(invalid)).is_err());
|
|
329
415
|
}
|