@push.rocks/smartnftables 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +19 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.manageddockerforwarding.js +3 -3
- package/dist_ts/classes.managednftables.d.ts +5 -0
- package/dist_ts/classes.managednftables.js +13 -6
- package/dist_ts/managed.egress.types.d.ts +53 -1
- package/package.json +7 -7
- package/readme.md +153 -4
- package/rust/src/egress.compile.rs +49 -1
- package/rust/src/egress.host.rs +22 -0
- package/rust/src/egress.hostgrant.rs +187 -0
- package/rust/src/egress.poolguard.rs +7 -0
- package/rust/src/egress.router.rs +189 -3
- package/rust/src/egress.rs +244 -1
- package/rust/src/egress_hostgrant_tests.rs +948 -0
- package/rust/src/egress_tests.rs +414 -0
- package/rust/src/main.rs +6 -2
- package/rust/src/owner.rs +116 -4
- package/rust/src/owner_egress_tests.rs +1 -1
- package/rust/src/owner_egress_traffic_tests.rs +3 -3
- package/rust/src/owner_host_traffic_tests.rs +4 -4
- package/rust/src/owner_hostgrant_traffic_tests.rs +280 -0
- package/rust/src/owner_identity_tests.rs +125 -1
- package/rust/src/owner_poolguard_tests.rs +69 -0
- package/rust/src/owner_router_traffic_tests.rs +393 -0
- package/rust/src/owner_tests.rs +6 -0
- package/rust/src/policy.rs +1 -1
- package/rust/src/wire.rs +15 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.manageddockerforwarding.ts +2 -2
- package/ts/classes.managednftables.ts +13 -5
- package/ts/managed.egress.types.ts +55 -1
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
//! Host-origin exact-port grants: compile-level exactness across the three
|
|
2
|
+
//! scopes a host-to-workload flow crosses (allocation-pool guard and host
|
|
3
|
+
//! transit in the host namespace, router egress in the router namespace).
|
|
4
|
+
use super::tests::{host, pool_guard, router};
|
|
5
|
+
use super::{Policy, Prepared};
|
|
6
|
+
use crate::wire::{self, Attr};
|
|
7
|
+
use serde_json::{json, Value};
|
|
8
|
+
use std::collections::BTreeMap;
|
|
9
|
+
|
|
10
|
+
pub(crate) fn host_grant(protocol: &str, source: &str, destination: &str, port: u16) -> Value {
|
|
11
|
+
json!({"protocol":protocol,"sourceAddress":source,"destinationAddress":destination,
|
|
12
|
+
"destinationPort":port})
|
|
13
|
+
}
|
|
14
|
+
pub(crate) fn granted(mut value: Value, grants: Value) -> Value {
|
|
15
|
+
value["scope"]["hostGrants"] = grants;
|
|
16
|
+
value
|
|
17
|
+
}
|
|
18
|
+
fn normalized(value: Value) -> crate::Result<Policy> {
|
|
19
|
+
serde_json::from_value::<Policy>(value)
|
|
20
|
+
.map_err(|_| crate::Error::Invalid)?
|
|
21
|
+
.normalize()
|
|
22
|
+
}
|
|
23
|
+
fn prepared(value: Value) -> Prepared {
|
|
24
|
+
normalized(value).unwrap().prepare().unwrap()
|
|
25
|
+
}
|
|
26
|
+
fn bytes(prepared: &Prepared) -> Vec<u8> {
|
|
27
|
+
let mut result = Vec::new();
|
|
28
|
+
for (kind, attributes) in prepared.program("snft_host_grant").unwrap() {
|
|
29
|
+
result.extend(kind.to_be_bytes());
|
|
30
|
+
result.extend(wire::encode_attrs(&attributes));
|
|
31
|
+
}
|
|
32
|
+
result
|
|
33
|
+
}
|
|
34
|
+
/// The fixture flow: the host's transit address on the handoff dials the workload.
|
|
35
|
+
fn exact() -> Value {
|
|
36
|
+
json!([host_grant("tcp", "10.240.0.1", "10.241.0.2", 8080)])
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// One packet with the conntrack facts a filter hook observes.
|
|
40
|
+
#[derive(Clone, Copy)]
|
|
41
|
+
struct Flow {
|
|
42
|
+
input: Option<(u32, &'static str)>,
|
|
43
|
+
output: Option<(u32, &'static str)>,
|
|
44
|
+
protocol: u8,
|
|
45
|
+
source: [u8; 4],
|
|
46
|
+
destination: [u8; 4],
|
|
47
|
+
source_port: u16,
|
|
48
|
+
destination_port: u16,
|
|
49
|
+
tcp_flags: u8,
|
|
50
|
+
state: u32,
|
|
51
|
+
reply: bool,
|
|
52
|
+
zone: u16,
|
|
53
|
+
/// Original-direction conntrack tuple: source, destination, ports.
|
|
54
|
+
original: ([u8; 4], [u8; 4], u16, u16),
|
|
55
|
+
}
|
|
56
|
+
const HOST: [u8; 4] = [10, 240, 0, 1];
|
|
57
|
+
const WORK: [u8; 4] = [10, 241, 0, 2];
|
|
58
|
+
const NEW: u32 = 8;
|
|
59
|
+
const ESTABLISHED: u32 = 2;
|
|
60
|
+
const SYN: u8 = 0x02;
|
|
61
|
+
const ACK: u8 = 0x10;
|
|
62
|
+
fn opening(input: Option<(u32, &'static str)>, output: Option<(u32, &'static str)>) -> Flow {
|
|
63
|
+
Flow {
|
|
64
|
+
input,
|
|
65
|
+
output,
|
|
66
|
+
protocol: 6,
|
|
67
|
+
source: HOST,
|
|
68
|
+
destination: WORK,
|
|
69
|
+
source_port: 443,
|
|
70
|
+
destination_port: 8080,
|
|
71
|
+
tcp_flags: SYN,
|
|
72
|
+
state: NEW,
|
|
73
|
+
reply: false,
|
|
74
|
+
zone: 0,
|
|
75
|
+
original: (HOST, WORK, 443, 8080),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/// The workload's answer to `flow`, arriving on the reverse links.
|
|
79
|
+
fn answer(flow: Flow) -> Flow {
|
|
80
|
+
Flow {
|
|
81
|
+
input: flow.output,
|
|
82
|
+
output: flow.input,
|
|
83
|
+
source: flow.destination,
|
|
84
|
+
destination: flow.source,
|
|
85
|
+
source_port: flow.destination_port,
|
|
86
|
+
destination_port: flow.source_port,
|
|
87
|
+
tcp_flags: ACK,
|
|
88
|
+
state: ESTABLISHED,
|
|
89
|
+
reply: true,
|
|
90
|
+
..flow
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/// Every variant of an admitted flow that the grant does not name.
|
|
94
|
+
fn foreign(flow: Flow) -> Vec<Flow> {
|
|
95
|
+
let (source, destination, source_port, _) = flow.original;
|
|
96
|
+
let original = |value: (_, _, _, _)| {
|
|
97
|
+
if flow.reply {
|
|
98
|
+
(value.1, value.0, value.3, value.2)
|
|
99
|
+
} else {
|
|
100
|
+
value
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
let tuple = |changed: Flow| Flow {
|
|
104
|
+
original: original((
|
|
105
|
+
changed.source,
|
|
106
|
+
changed.destination,
|
|
107
|
+
changed.source_port,
|
|
108
|
+
changed.destination_port,
|
|
109
|
+
)),
|
|
110
|
+
..changed
|
|
111
|
+
};
|
|
112
|
+
let mut result = vec![
|
|
113
|
+
// Another port, protocol, workload address and host source address.
|
|
114
|
+
if flow.reply {
|
|
115
|
+
tuple(Flow {
|
|
116
|
+
source_port: 8081,
|
|
117
|
+
..flow
|
|
118
|
+
})
|
|
119
|
+
} else {
|
|
120
|
+
tuple(Flow {
|
|
121
|
+
destination_port: 8081,
|
|
122
|
+
..flow
|
|
123
|
+
})
|
|
124
|
+
},
|
|
125
|
+
Flow {
|
|
126
|
+
protocol: 17,
|
|
127
|
+
..flow
|
|
128
|
+
},
|
|
129
|
+
if flow.reply {
|
|
130
|
+
tuple(Flow {
|
|
131
|
+
source: [10, 241, 0, 3],
|
|
132
|
+
..flow
|
|
133
|
+
})
|
|
134
|
+
} else {
|
|
135
|
+
tuple(Flow {
|
|
136
|
+
destination: [10, 241, 0, 3],
|
|
137
|
+
..flow
|
|
138
|
+
})
|
|
139
|
+
},
|
|
140
|
+
if flow.reply {
|
|
141
|
+
tuple(Flow {
|
|
142
|
+
destination: [192, 0, 2, 2],
|
|
143
|
+
..flow
|
|
144
|
+
})
|
|
145
|
+
} else {
|
|
146
|
+
tuple(Flow {
|
|
147
|
+
source: [192, 0, 2, 2],
|
|
148
|
+
..flow
|
|
149
|
+
})
|
|
150
|
+
},
|
|
151
|
+
// Foreign destination NAT onto the granted tuple.
|
|
152
|
+
Flow {
|
|
153
|
+
original: (source, [203, 0, 113, 9], source_port, 80),
|
|
154
|
+
..flow
|
|
155
|
+
},
|
|
156
|
+
Flow {
|
|
157
|
+
original: (source, destination, source_port, 80),
|
|
158
|
+
..flow
|
|
159
|
+
},
|
|
160
|
+
// Another conntrack zone, the wrong direction, a non-SYN opening and an
|
|
161
|
+
// unreplied flow on the reply path.
|
|
162
|
+
Flow { zone: 7, ..flow },
|
|
163
|
+
Flow {
|
|
164
|
+
reply: !flow.reply,
|
|
165
|
+
..flow
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
result.push(if flow.reply {
|
|
169
|
+
Flow { state: NEW, ..flow }
|
|
170
|
+
} else {
|
|
171
|
+
Flow {
|
|
172
|
+
state: NEW,
|
|
173
|
+
tcp_flags: ACK,
|
|
174
|
+
..flow
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
result
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// What one hook decides for a flow, following jumps and returns exactly as the
|
|
181
|
+
/// kernel does: the terminal verdict, the chain that reached it (`policy` for the
|
|
182
|
+
/// base chain's accept policy) and the conntrack zone a raw rule stored.
|
|
183
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
184
|
+
struct Decision {
|
|
185
|
+
verdict: i32,
|
|
186
|
+
chain: String,
|
|
187
|
+
zone: u16,
|
|
188
|
+
}
|
|
189
|
+
/// The compiled table: rules per chain and element keys per set.
|
|
190
|
+
struct Table {
|
|
191
|
+
chains: BTreeMap<String, Vec<Vec<Attr>>>,
|
|
192
|
+
sets: BTreeMap<String, Vec<Vec<u8>>>,
|
|
193
|
+
}
|
|
194
|
+
fn chains(prepared: &Prepared) -> BTreeMap<String, Vec<Vec<Attr>>> {
|
|
195
|
+
table(prepared).chains
|
|
196
|
+
}
|
|
197
|
+
fn table(prepared: &Prepared) -> Table {
|
|
198
|
+
let mut chains: BTreeMap<String, Vec<Vec<Attr>>> = BTreeMap::new();
|
|
199
|
+
let mut sets: BTreeMap<String, Vec<Vec<u8>>> = BTreeMap::new();
|
|
200
|
+
for (kind, attributes) in prepared.program("snft_host_grant").unwrap() {
|
|
201
|
+
match kind {
|
|
202
|
+
3 => {
|
|
203
|
+
chains
|
|
204
|
+
.entry(wire::text(&attributes, 3).unwrap())
|
|
205
|
+
.or_default();
|
|
206
|
+
}
|
|
207
|
+
6 => chains
|
|
208
|
+
.entry(wire::text(&attributes, 2).unwrap())
|
|
209
|
+
.or_default()
|
|
210
|
+
.push(wire::attrs(&wire::one(&attributes, 4).unwrap().value).unwrap()),
|
|
211
|
+
9 => {
|
|
212
|
+
assert!(sets
|
|
213
|
+
.insert(wire::text(&attributes, 2).unwrap(), Vec::new())
|
|
214
|
+
.is_none());
|
|
215
|
+
}
|
|
216
|
+
12 => {
|
|
217
|
+
let keys = sets.get_mut(&wire::text(&attributes, 2).unwrap()).unwrap();
|
|
218
|
+
for element in wire::attrs(&wire::one(&attributes, 3).unwrap().value).unwrap() {
|
|
219
|
+
let key = wire::attrs(&element.value).unwrap();
|
|
220
|
+
keys.push(data(&key, 1));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
other => panic!("operation {other}"),
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
Table { chains, sets }
|
|
227
|
+
}
|
|
228
|
+
fn data(attributes: &[Attr], id: u16) -> Vec<u8> {
|
|
229
|
+
wire::attrs(&wire::one(attributes, id).unwrap().value).unwrap()[0]
|
|
230
|
+
.value
|
|
231
|
+
.clone()
|
|
232
|
+
}
|
|
233
|
+
/// A missing device loads as index 0 and an empty name, exactly like Linux.
|
|
234
|
+
fn interface(value: Option<(u32, &'static str)>, name: bool) -> Vec<u8> {
|
|
235
|
+
let (index, text) = value.unwrap_or((0, ""));
|
|
236
|
+
if name {
|
|
237
|
+
let mut padded = text.as_bytes().to_vec();
|
|
238
|
+
padded.resize(16, 0);
|
|
239
|
+
padded
|
|
240
|
+
} else {
|
|
241
|
+
index.to_ne_bytes().to_vec()
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/// None when a load cannot produce a value, which breaks the rule like the kernel.
|
|
245
|
+
fn load(flow: &Flow, name: &str, fields: &[Attr]) -> Option<Vec<u8>> {
|
|
246
|
+
Some(match name {
|
|
247
|
+
"meta" => match wire::number(fields, 2).unwrap() {
|
|
248
|
+
4 => interface(flow.input, false),
|
|
249
|
+
5 => interface(flow.output, false),
|
|
250
|
+
6 => interface(flow.input, true),
|
|
251
|
+
7 => interface(flow.output, true),
|
|
252
|
+
15 => vec![libc::NFPROTO_IPV4 as u8],
|
|
253
|
+
16 => vec![flow.protocol],
|
|
254
|
+
key => panic!("meta {key}"),
|
|
255
|
+
},
|
|
256
|
+
"payload" => {
|
|
257
|
+
let loaded = match (
|
|
258
|
+
wire::number(fields, 2).unwrap(),
|
|
259
|
+
wire::number(fields, 3).unwrap(),
|
|
260
|
+
) {
|
|
261
|
+
(1, 12) => flow.source.to_vec(),
|
|
262
|
+
(1, 16) => flow.destination.to_vec(),
|
|
263
|
+
(2, 0) => flow.source_port.to_be_bytes().to_vec(),
|
|
264
|
+
(2, 2) => flow.destination_port.to_be_bytes().to_vec(),
|
|
265
|
+
(2, 13) if flow.protocol == 6 => vec![flow.tcp_flags],
|
|
266
|
+
(2, 13) => return None,
|
|
267
|
+
load => panic!("payload {load:?}"),
|
|
268
|
+
};
|
|
269
|
+
assert_eq!(loaded.len() as u32, wire::number(fields, 4).unwrap());
|
|
270
|
+
loaded
|
|
271
|
+
}
|
|
272
|
+
"ct" => {
|
|
273
|
+
if let Ok(direction) = wire::one(fields, 3) {
|
|
274
|
+
assert_eq!(direction.value, vec![0]);
|
|
275
|
+
}
|
|
276
|
+
let (source, destination, source_port, destination_port) = flow.original;
|
|
277
|
+
match wire::number(fields, 2).unwrap() {
|
|
278
|
+
0 => flow.state.to_ne_bytes().to_vec(),
|
|
279
|
+
1 => vec![u8::from(flow.reply)],
|
|
280
|
+
10 => vec![flow.protocol],
|
|
281
|
+
11 => source_port.to_be_bytes().to_vec(),
|
|
282
|
+
12 => destination_port.to_be_bytes().to_vec(),
|
|
283
|
+
13 => vec![0; 16],
|
|
284
|
+
17 => flow.zone.to_ne_bytes().to_vec(),
|
|
285
|
+
19 => source.to_vec(),
|
|
286
|
+
20 => destination.to_vec(),
|
|
287
|
+
key => panic!("ct {key}"),
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
other => panic!("{other}"),
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
/// Byte offset of a register operand, as the kernel parses it.
|
|
294
|
+
fn offset(fields: &[Attr], id: u16) -> usize {
|
|
295
|
+
let register = wire::number(fields, id).unwrap() as usize;
|
|
296
|
+
4 * if register <= 4 {
|
|
297
|
+
register * 4
|
|
298
|
+
} else {
|
|
299
|
+
register - 4
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
enum Step {
|
|
303
|
+
Next,
|
|
304
|
+
Final(Decision),
|
|
305
|
+
}
|
|
306
|
+
/// A register machine over the compiled rules: loads store into their register
|
|
307
|
+
/// and zero the rest of a partially written 32-bit word, exactly as the kernel's
|
|
308
|
+
/// store helpers do, so concatenated set keys are checked byte for byte.
|
|
309
|
+
fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
310
|
+
for expressions in &table.chains[chain] {
|
|
311
|
+
let mut registers = [0_u8; 80];
|
|
312
|
+
let mut matched = true;
|
|
313
|
+
let mut verdict: Option<(i32, Option<String>)> = None;
|
|
314
|
+
for expression in expressions {
|
|
315
|
+
let parts = wire::attrs(&expression.value).unwrap();
|
|
316
|
+
let name = wire::text(&parts, 1).unwrap();
|
|
317
|
+
let fields = wire::attrs(&wire::one(&parts, 2).unwrap().value).unwrap();
|
|
318
|
+
match name.as_str() {
|
|
319
|
+
"ct" if wire::one(&fields, 4).is_ok() => {
|
|
320
|
+
if wire::number(&fields, 2).unwrap() == 17 {
|
|
321
|
+
let at = offset(&fields, 4);
|
|
322
|
+
*zone = u16::from_ne_bytes(registers[at..at + 2].try_into().unwrap());
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
"meta" | "payload" | "ct" => match load(flow, &name, &fields) {
|
|
326
|
+
Some(value) => {
|
|
327
|
+
let at = offset(&fields, 1);
|
|
328
|
+
let end = at + value.len().div_ceil(4) * 4;
|
|
329
|
+
registers[at..end].fill(0);
|
|
330
|
+
registers[at..at + value.len()].copy_from_slice(&value);
|
|
331
|
+
}
|
|
332
|
+
None => matched = false,
|
|
333
|
+
},
|
|
334
|
+
"bitwise" => {
|
|
335
|
+
let (from, to) = (offset(&fields, 1), offset(&fields, 2));
|
|
336
|
+
let length = wire::number(&fields, 3).unwrap() as usize;
|
|
337
|
+
let (mask, xor) = (data(&fields, 4), data(&fields, 5));
|
|
338
|
+
for index in 0..length {
|
|
339
|
+
registers[to + index] = registers[from + index] & mask[index] ^ xor[index];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
"cmp" => {
|
|
343
|
+
assert_eq!(wire::number(&fields, 2).unwrap(), 0);
|
|
344
|
+
let expected = data(&fields, 3);
|
|
345
|
+
let at = offset(&fields, 1);
|
|
346
|
+
matched = registers[at..at + expected.len()] == expected[..];
|
|
347
|
+
}
|
|
348
|
+
"range" => {
|
|
349
|
+
assert_eq!(wire::number(&fields, 2).unwrap(), 0);
|
|
350
|
+
let (first, last) = (data(&fields, 3), data(&fields, 4));
|
|
351
|
+
let at = offset(&fields, 1);
|
|
352
|
+
let loaded = ®isters[at..at + first.len()];
|
|
353
|
+
matched = loaded >= &first[..] && loaded <= &last[..];
|
|
354
|
+
}
|
|
355
|
+
"lookup" => {
|
|
356
|
+
assert_eq!(wire::number(&fields, 5).unwrap(), 0);
|
|
357
|
+
let keys = &table.sets[&wire::text(&fields, 1).unwrap()];
|
|
358
|
+
let at = offset(&fields, 2);
|
|
359
|
+
matched = keys
|
|
360
|
+
.iter()
|
|
361
|
+
.any(|key| registers[at..at + key.len()] == key[..]);
|
|
362
|
+
}
|
|
363
|
+
"immediate" if wire::number(&fields, 1).unwrap() == 0 => {
|
|
364
|
+
let code = wire::attrs(
|
|
365
|
+
&wire::one(
|
|
366
|
+
&wire::attrs(&wire::one(&fields, 2).unwrap().value).unwrap(),
|
|
367
|
+
2,
|
|
368
|
+
)
|
|
369
|
+
.unwrap()
|
|
370
|
+
.value,
|
|
371
|
+
)
|
|
372
|
+
.unwrap();
|
|
373
|
+
verdict = Some((
|
|
374
|
+
wire::number(&code, 1).unwrap() as i32,
|
|
375
|
+
wire::text(&code, 2).ok(),
|
|
376
|
+
));
|
|
377
|
+
}
|
|
378
|
+
"immediate" => {
|
|
379
|
+
let value = data(&fields, 2);
|
|
380
|
+
let at = offset(&fields, 1);
|
|
381
|
+
registers[at..at + value.len()].copy_from_slice(&value);
|
|
382
|
+
}
|
|
383
|
+
"nat" => {}
|
|
384
|
+
other => panic!("{other}"),
|
|
385
|
+
}
|
|
386
|
+
if !matched {
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if !matched {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
match verdict {
|
|
394
|
+
Some((code @ (0 | 1), _)) => {
|
|
395
|
+
return Step::Final(Decision {
|
|
396
|
+
verdict: code,
|
|
397
|
+
chain: chain.into(),
|
|
398
|
+
zone: *zone,
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
Some((-5, _)) => return Step::Next,
|
|
402
|
+
Some((-3, Some(target))) => {
|
|
403
|
+
if let Step::Final(decision) = run(table, &target, flow, zone) {
|
|
404
|
+
return Step::Final(decision);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
Some((-4, Some(target))) => return run(table, &target, flow, zone),
|
|
408
|
+
Some(other) => panic!("verdict {other:?}"),
|
|
409
|
+
None => {}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
Step::Next
|
|
413
|
+
}
|
|
414
|
+
/// A base chain that ends without a terminal verdict accepts by policy.
|
|
415
|
+
fn decide(prepared: &Prepared, chain: &str, flow: &Flow) -> Decision {
|
|
416
|
+
let mut zone = 0;
|
|
417
|
+
match run(&table(prepared), chain, flow, &mut zone) {
|
|
418
|
+
Step::Final(decision) => decision,
|
|
419
|
+
Step::Next => Decision {
|
|
420
|
+
verdict: 1,
|
|
421
|
+
chain: "policy".into(),
|
|
422
|
+
zone,
|
|
423
|
+
},
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/// The grant admits exactly its flow in both directions of every hook, and every
|
|
427
|
+
/// other tuple meets exactly the decision the scope without the grant makes.
|
|
428
|
+
fn assert_exact(baseline: &Prepared, granted: &Prepared, hooks: &[(&str, Flow)]) {
|
|
429
|
+
for (chain, flow) in hooks {
|
|
430
|
+
assert_ne!(decide(baseline, chain, flow).verdict, 1, "{chain} baseline");
|
|
431
|
+
assert_eq!(decide(granted, chain, flow).verdict, 1, "{chain} granted");
|
|
432
|
+
for (index, other) in foreign(*flow).iter().enumerate() {
|
|
433
|
+
assert_eq!(
|
|
434
|
+
decide(granted, chain, other),
|
|
435
|
+
decide(baseline, chain, other),
|
|
436
|
+
"{chain} variant {index}"
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
#[test]
|
|
443
|
+
fn host_grants_absent_and_empty_keep_every_scope_digest_and_compiled_bytes() {
|
|
444
|
+
for fixture in [router(), host(), pool_guard()] {
|
|
445
|
+
let baseline = prepared(fixture.clone());
|
|
446
|
+
assert!(!serde_json::to_string(&baseline.policy)
|
|
447
|
+
.unwrap()
|
|
448
|
+
.contains("hostGrants"));
|
|
449
|
+
let empty = prepared(granted(fixture.clone(), json!([])));
|
|
450
|
+
assert_eq!(empty, baseline);
|
|
451
|
+
assert_eq!(bytes(&empty), bytes(&baseline));
|
|
452
|
+
let with = prepared(granted(fixture, exact()));
|
|
453
|
+
assert_ne!(with.digest, baseline.digest);
|
|
454
|
+
assert!(serde_json::to_string(&with.policy)
|
|
455
|
+
.unwrap()
|
|
456
|
+
.contains("\"hostGrants\""));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
#[test]
|
|
461
|
+
fn router_host_grant_classifies_and_admits_only_the_exact_handoff_flow() {
|
|
462
|
+
let baseline = prepared(router());
|
|
463
|
+
let with = prepared(granted(router(), exact()));
|
|
464
|
+
let handoff = Some((4, "handoff"));
|
|
465
|
+
let work = Some((2, "work"));
|
|
466
|
+
let original = opening(handoff, work);
|
|
467
|
+
// Forward admission of the opening, of the established flow and of the reply.
|
|
468
|
+
assert_exact(
|
|
469
|
+
&baseline,
|
|
470
|
+
&with,
|
|
471
|
+
&[
|
|
472
|
+
("forward", original),
|
|
473
|
+
(
|
|
474
|
+
"forward",
|
|
475
|
+
Flow {
|
|
476
|
+
state: ESTABLISHED,
|
|
477
|
+
tcp_flags: ACK,
|
|
478
|
+
..original
|
|
479
|
+
},
|
|
480
|
+
),
|
|
481
|
+
("forward", answer(original)),
|
|
482
|
+
],
|
|
483
|
+
);
|
|
484
|
+
// Raw handoff ingress runs before conntrack: the exact arriving tuple is
|
|
485
|
+
// accepted in the default zone ahead of the protected-source denial that
|
|
486
|
+
// drops every host-origin packet without the grant.
|
|
487
|
+
let raw = |prepared: &Prepared, flow: &Flow| decide(prepared, "raw_in", flow);
|
|
488
|
+
assert_eq!(raw(&baseline, &original).verdict, 0);
|
|
489
|
+
let classified = raw(&with, &original);
|
|
490
|
+
assert_eq!(
|
|
491
|
+
(classified.verdict, classified.chain.as_str()),
|
|
492
|
+
(1, "raw_return")
|
|
493
|
+
);
|
|
494
|
+
for other in [
|
|
495
|
+
Flow {
|
|
496
|
+
destination_port: 8081,
|
|
497
|
+
..original
|
|
498
|
+
},
|
|
499
|
+
Flow {
|
|
500
|
+
protocol: 17,
|
|
501
|
+
..original
|
|
502
|
+
},
|
|
503
|
+
Flow {
|
|
504
|
+
destination: [10, 241, 0, 3],
|
|
505
|
+
..original
|
|
506
|
+
},
|
|
507
|
+
Flow {
|
|
508
|
+
source: [10, 240, 0, 5],
|
|
509
|
+
..original
|
|
510
|
+
},
|
|
511
|
+
Flow {
|
|
512
|
+
input: Some((3, "vpn")),
|
|
513
|
+
..original
|
|
514
|
+
},
|
|
515
|
+
] {
|
|
516
|
+
assert_eq!(raw(&with, &other), raw(&baseline, &other));
|
|
517
|
+
}
|
|
518
|
+
// The workload's answer stays in the default zone even when the host's source
|
|
519
|
+
// port equals the destination port of a leased public grant (tcp/443 here):
|
|
520
|
+
// protected destinations return before any leased classifier can zone it.
|
|
521
|
+
let reply = raw(&with, &answer(original));
|
|
522
|
+
assert_eq!(reply, raw(&baseline, &answer(original)));
|
|
523
|
+
assert_eq!((reply.verdict, reply.zone), (1, 0));
|
|
524
|
+
let leased = raw(
|
|
525
|
+
&with,
|
|
526
|
+
&Flow {
|
|
527
|
+
destination: [198, 51, 100, 7],
|
|
528
|
+
..answer(original)
|
|
529
|
+
},
|
|
530
|
+
);
|
|
531
|
+
assert_eq!(
|
|
532
|
+
(leased.verdict, leased.chain.as_str(), leased.zone),
|
|
533
|
+
(1, "raw_in", 1)
|
|
534
|
+
);
|
|
535
|
+
// One raw classification and four forward admissions, whatever the number
|
|
536
|
+
// of grants.
|
|
537
|
+
let count = |prepared: &Prepared, chain: &str| chains(prepared)[chain].len();
|
|
538
|
+
assert_eq!(
|
|
539
|
+
count(&with, "raw_return"),
|
|
540
|
+
count(&baseline, "raw_return") + 1
|
|
541
|
+
);
|
|
542
|
+
assert_eq!(count(&with, "forward"), count(&baseline, "forward") + 4);
|
|
543
|
+
assert_eq!(chains(&with).len(), chains(&baseline).len());
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
#[test]
|
|
547
|
+
fn host_transit_host_grant_opens_only_the_exact_output_and_reply_input() {
|
|
548
|
+
let baseline = prepared(host());
|
|
549
|
+
let with = prepared(granted(host(), exact()));
|
|
550
|
+
let original = opening(None, Some((4, "handoff")));
|
|
551
|
+
assert_exact(
|
|
552
|
+
&baseline,
|
|
553
|
+
&with,
|
|
554
|
+
&[
|
|
555
|
+
("output", original),
|
|
556
|
+
(
|
|
557
|
+
"output",
|
|
558
|
+
Flow {
|
|
559
|
+
state: ESTABLISHED,
|
|
560
|
+
tcp_flags: ACK,
|
|
561
|
+
..original
|
|
562
|
+
},
|
|
563
|
+
),
|
|
564
|
+
("input", answer(original)),
|
|
565
|
+
],
|
|
566
|
+
);
|
|
567
|
+
// No forwarding, no uplink path and no other handoff opens.
|
|
568
|
+
for (chain, flow) in [
|
|
569
|
+
(
|
|
570
|
+
"forward",
|
|
571
|
+
Flow {
|
|
572
|
+
input: Some((2, "ens18")),
|
|
573
|
+
..original
|
|
574
|
+
},
|
|
575
|
+
),
|
|
576
|
+
(
|
|
577
|
+
"output",
|
|
578
|
+
Flow {
|
|
579
|
+
output: Some((2, "ens18")),
|
|
580
|
+
..original
|
|
581
|
+
},
|
|
582
|
+
),
|
|
583
|
+
(
|
|
584
|
+
"input",
|
|
585
|
+
Flow {
|
|
586
|
+
input: Some((2, "ens18")),
|
|
587
|
+
..answer(original)
|
|
588
|
+
},
|
|
589
|
+
),
|
|
590
|
+
] {
|
|
591
|
+
assert_eq!(decide(&with, chain, &flow), decide(&baseline, chain, &flow));
|
|
592
|
+
}
|
|
593
|
+
let count = |prepared: &Prepared, chain: &str| chains(prepared)[chain].len();
|
|
594
|
+
assert_eq!(count(&with, "output"), count(&baseline, "output") + 3);
|
|
595
|
+
assert_eq!(count(&with, "input"), count(&baseline, "input") + 1);
|
|
596
|
+
assert_eq!(count(&with, "forward"), count(&baseline, "forward"));
|
|
597
|
+
assert_eq!(chains(&with).len(), chains(&baseline).len());
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
#[test]
|
|
601
|
+
fn pool_guard_host_grant_is_the_only_exception_and_the_guard_keeps_every_other_pool_denial() {
|
|
602
|
+
let baseline = prepared(pool_guard());
|
|
603
|
+
let with = prepared(granted(pool_guard(), exact()));
|
|
604
|
+
// The guard has no link binding; the exception is the exact host-local tuple.
|
|
605
|
+
let original = opening(None, Some((4, "handoff")));
|
|
606
|
+
assert_exact(
|
|
607
|
+
&baseline,
|
|
608
|
+
&with,
|
|
609
|
+
&[
|
|
610
|
+
("output", original),
|
|
611
|
+
(
|
|
612
|
+
"output",
|
|
613
|
+
Flow {
|
|
614
|
+
state: ESTABLISHED,
|
|
615
|
+
tcp_flags: ACK,
|
|
616
|
+
..original
|
|
617
|
+
},
|
|
618
|
+
),
|
|
619
|
+
("input", answer(original)),
|
|
620
|
+
],
|
|
621
|
+
);
|
|
622
|
+
// Forwarded pool traffic never matches a host grant.
|
|
623
|
+
let forwarded = Flow {
|
|
624
|
+
input: Some((2, "ens18")),
|
|
625
|
+
..original
|
|
626
|
+
};
|
|
627
|
+
assert_eq!(
|
|
628
|
+
decide(&with, "forward", &forwarded),
|
|
629
|
+
decide(&baseline, "forward", &forwarded)
|
|
630
|
+
);
|
|
631
|
+
assert_eq!(decide(&with, "forward", &forwarded).verdict, 0);
|
|
632
|
+
let graph = chains(&with);
|
|
633
|
+
let reference = chains(&baseline);
|
|
634
|
+
for name in ["pool_guard", "pool_reply", "forward"] {
|
|
635
|
+
assert_eq!(graph[name], reference[name], "{name}");
|
|
636
|
+
}
|
|
637
|
+
assert_eq!(graph["output"].len(), reference["output"].len() + 3);
|
|
638
|
+
assert_eq!(graph["input"].len(), reference["input"].len() + 1);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
#[test]
|
|
642
|
+
fn host_grant_withdrawal_compiles_the_exact_previous_graph() {
|
|
643
|
+
for fixture in [router(), host(), pool_guard()] {
|
|
644
|
+
let baseline = prepared(fixture.clone());
|
|
645
|
+
let two = prepared(granted(
|
|
646
|
+
fixture.clone(),
|
|
647
|
+
json!([
|
|
648
|
+
host_grant("tcp", "10.240.0.1", "10.241.0.2", 8080),
|
|
649
|
+
host_grant("udp", "10.240.0.1", "10.241.0.2", 5353)
|
|
650
|
+
]),
|
|
651
|
+
));
|
|
652
|
+
// Order is canonical, so a reordered list is the same policy.
|
|
653
|
+
let reordered = prepared(granted(
|
|
654
|
+
fixture.clone(),
|
|
655
|
+
json!([
|
|
656
|
+
host_grant("udp", "10.240.0.1", "10.241.0.2", 5353),
|
|
657
|
+
host_grant("tcp", "10.240.0.1", "10.241.0.2", 8080)
|
|
658
|
+
]),
|
|
659
|
+
));
|
|
660
|
+
assert_eq!(reordered, two);
|
|
661
|
+
// Withdrawing one grant is the single-grant policy; withdrawing all of them
|
|
662
|
+
// is byte-identical to the scope that never had one.
|
|
663
|
+
let one = prepared(granted(fixture.clone(), exact()));
|
|
664
|
+
let tcp = opening(None, Some((4, "handoff")));
|
|
665
|
+
assert_ne!(one.digest, two.digest);
|
|
666
|
+
assert_eq!(prepared(granted(fixture, json!([]))), baseline);
|
|
667
|
+
let (chain, flow) = if matches!(baseline.policy.scope, super::Scope::RouterEgress(_)) {
|
|
668
|
+
(
|
|
669
|
+
"forward",
|
|
670
|
+
Flow {
|
|
671
|
+
input: Some((4, "handoff")),
|
|
672
|
+
output: Some((2, "work")),
|
|
673
|
+
..tcp
|
|
674
|
+
},
|
|
675
|
+
)
|
|
676
|
+
} else {
|
|
677
|
+
("output", tcp)
|
|
678
|
+
};
|
|
679
|
+
let udp = Flow {
|
|
680
|
+
protocol: 17,
|
|
681
|
+
destination_port: 5353,
|
|
682
|
+
original: (HOST, WORK, 443, 5353),
|
|
683
|
+
..flow
|
|
684
|
+
};
|
|
685
|
+
assert_eq!(decide(&two, chain, &udp).verdict, 1);
|
|
686
|
+
assert_eq!(decide(&one, chain, &udp), decide(&baseline, chain, &udp));
|
|
687
|
+
assert_eq!(decide(&one, chain, &flow).verdict, 1);
|
|
688
|
+
assert_ne!(decide(&baseline, chain, &flow).verdict, 1);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
#[test]
|
|
693
|
+
fn host_grants_reject_ranges_wildcards_and_unbound_addresses() {
|
|
694
|
+
let valid = |fixture: Value| granted(fixture, exact());
|
|
695
|
+
for fixture in [router(), host(), pool_guard()] {
|
|
696
|
+
assert!(normalized(valid(fixture.clone())).is_ok());
|
|
697
|
+
let grant = host_grant("tcp", "10.240.0.1", "10.241.0.2", 8080);
|
|
698
|
+
let changed = |path: &str, value: Value| {
|
|
699
|
+
let mut changed = grant.clone();
|
|
700
|
+
*changed.pointer_mut(path).unwrap() = value;
|
|
701
|
+
granted(fixture.clone(), json!([changed]))
|
|
702
|
+
};
|
|
703
|
+
let mut unknown = grant.clone();
|
|
704
|
+
unknown["destinationPortLast"] = json!(8090);
|
|
705
|
+
let mut missing = grant.clone();
|
|
706
|
+
missing.as_object_mut().unwrap().remove("sourceAddress");
|
|
707
|
+
for invalid in [
|
|
708
|
+
granted(fixture.clone(), json!([unknown])),
|
|
709
|
+
granted(fixture.clone(), json!([missing])),
|
|
710
|
+
changed("/protocol", json!("any")),
|
|
711
|
+
changed("/protocol", json!("sctp")),
|
|
712
|
+
// Exact ports only: no zero, range, list, wildcard or null.
|
|
713
|
+
changed("/destinationPort", json!(0)),
|
|
714
|
+
changed("/destinationPort", json!(65536)),
|
|
715
|
+
changed("/destinationPort", json!(-1)),
|
|
716
|
+
changed("/destinationPort", json!("8080-8090")),
|
|
717
|
+
changed("/destinationPort", json!([8080, 8090])),
|
|
718
|
+
changed("/destinationPort", Value::Null),
|
|
719
|
+
// Exact unicast addresses only: no prefix, wildcard, broadcast or loopback.
|
|
720
|
+
changed("/sourceAddress", json!("10.240.0.1/32")),
|
|
721
|
+
changed("/sourceAddress", json!("0.0.0.0")),
|
|
722
|
+
changed("/sourceAddress", json!("10.240.0.01")),
|
|
723
|
+
changed("/destinationAddress", json!("10.241.0.0/24")),
|
|
724
|
+
changed("/destinationAddress", json!("0.0.0.0")),
|
|
725
|
+
changed("/destinationAddress", json!("255.255.255.255")),
|
|
726
|
+
changed("/destinationAddress", json!("127.0.0.1")),
|
|
727
|
+
changed("/destinationAddress", json!("fd00::2")),
|
|
728
|
+
changed("/destinationAddress", json!("10.240.0.1")),
|
|
729
|
+
// One entry per exact tuple, and a bounded list.
|
|
730
|
+
granted(fixture.clone(), json!([grant.clone(), grant.clone()])),
|
|
731
|
+
granted(
|
|
732
|
+
fixture.clone(),
|
|
733
|
+
json!((0..1025)
|
|
734
|
+
.map(|index| host_grant("tcp", "10.240.0.1", "10.241.0.2", 20000 + index))
|
|
735
|
+
.collect::<Vec<_>>()),
|
|
736
|
+
),
|
|
737
|
+
] {
|
|
738
|
+
assert!(normalized(invalid).is_err());
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
let bound = |fixture: Value, source: &str, destination: &str| {
|
|
742
|
+
normalized(granted(
|
|
743
|
+
fixture,
|
|
744
|
+
json!([host_grant("tcp", source, destination, 8080)]),
|
|
745
|
+
))
|
|
746
|
+
};
|
|
747
|
+
// Router: the destination is a current veth workload endpoint address, and the
|
|
748
|
+
// source is protected handoff-side space that is neither router-local, a
|
|
749
|
+
// platform endpoint nor a workload.
|
|
750
|
+
for (source, destination) in [
|
|
751
|
+
("10.240.0.1", "10.241.0.3"),
|
|
752
|
+
("10.240.0.1", "10.241.0.1"),
|
|
753
|
+
("10.240.0.1", "10.240.0.2"),
|
|
754
|
+
("10.240.0.1", "10.242.0.2"),
|
|
755
|
+
("10.240.0.1", "192.0.2.1"),
|
|
756
|
+
("10.240.0.2", "10.241.0.2"),
|
|
757
|
+
("10.241.0.1", "10.241.0.2"),
|
|
758
|
+
("10.242.0.2", "10.241.0.2"),
|
|
759
|
+
("192.0.2.1", "10.241.0.2"),
|
|
760
|
+
("203.0.113.9", "10.241.0.2"),
|
|
761
|
+
] {
|
|
762
|
+
assert!(
|
|
763
|
+
bound(router(), source, destination).is_err(),
|
|
764
|
+
"{source} {destination}"
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
// Host transit: the source is an exact current handoff address, and the
|
|
768
|
+
// destination is protected, non-local space that is no leased router address.
|
|
769
|
+
for (source, destination) in [
|
|
770
|
+
("10.240.0.5", "10.241.0.2"),
|
|
771
|
+
("192.0.2.2", "10.241.0.2"),
|
|
772
|
+
("10.240.0.1", "10.240.0.2"),
|
|
773
|
+
("10.240.0.1", "192.0.2.2"),
|
|
774
|
+
("10.240.0.1", "192.0.2.1"),
|
|
775
|
+
("10.240.0.1", "198.51.100.7"),
|
|
776
|
+
] {
|
|
777
|
+
assert!(
|
|
778
|
+
bound(host(), source, destination).is_err(),
|
|
779
|
+
"{source} {destination}"
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
// Guard: the exception names an address inside a guarded pool.
|
|
783
|
+
for (source, destination) in [
|
|
784
|
+
("10.240.0.1", "10.242.0.2"),
|
|
785
|
+
("10.240.0.1", "192.168.1.1"),
|
|
786
|
+
("10.240.0.1", "198.51.100.7"),
|
|
787
|
+
] {
|
|
788
|
+
assert!(
|
|
789
|
+
bound(pool_guard(), source, destination).is_err(),
|
|
790
|
+
"{source} {destination}"
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/// `count` distinct exact grants to the fixture workload, both protocols.
|
|
796
|
+
fn many(count: usize) -> Value {
|
|
797
|
+
json!((0..count)
|
|
798
|
+
.map(|index| host_grant(
|
|
799
|
+
if index % 2 == 0 { "tcp" } else { "udp" },
|
|
800
|
+
"10.240.0.1",
|
|
801
|
+
"10.241.0.2",
|
|
802
|
+
20000 + (index / 2) as u16
|
|
803
|
+
))
|
|
804
|
+
.collect::<Vec<_>>())
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
#[test]
|
|
808
|
+
fn host_grants_compile_a_constant_rule_graph_and_one_element_per_grant() {
|
|
809
|
+
for fixture in [router(), host(), pool_guard()] {
|
|
810
|
+
let one = table(&prepared(granted(fixture.clone(), many(1))));
|
|
811
|
+
let all = table(&prepared(granted(fixture.clone(), many(1024))));
|
|
812
|
+
// The rule graph is independent of the number of grants.
|
|
813
|
+
assert_eq!(one.chains, all.chains);
|
|
814
|
+
assert_eq!(all.sets["host_grant"].len(), 1024);
|
|
815
|
+
let keys: std::collections::BTreeSet<_> = all.sets["host_grant"].iter().collect();
|
|
816
|
+
assert_eq!(keys.len(), 1024);
|
|
817
|
+
if let Some(arrival) = all.sets.get("host_grant_arrival") {
|
|
818
|
+
assert_eq!(arrival.len(), 1024);
|
|
819
|
+
}
|
|
820
|
+
assert!(table(&prepared(fixture)).sets.is_empty());
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
#[test]
|
|
825
|
+
fn host_grants_admit_every_member_of_a_large_set_and_no_other_tuple() {
|
|
826
|
+
let flows = |fixture: &Value| -> Vec<(&'static str, Flow, bool)> {
|
|
827
|
+
let (chain, original) = if fixture["scope"]["kind"] == "routerEgress" {
|
|
828
|
+
("forward", opening(Some((4, "handoff")), Some((2, "work"))))
|
|
829
|
+
} else {
|
|
830
|
+
("output", opening(None, Some((4, "handoff"))))
|
|
831
|
+
};
|
|
832
|
+
let reply_chain = if chain == "forward" {
|
|
833
|
+
"forward"
|
|
834
|
+
} else {
|
|
835
|
+
"input"
|
|
836
|
+
};
|
|
837
|
+
let at = |protocol: u8, port: u16| Flow {
|
|
838
|
+
protocol,
|
|
839
|
+
destination_port: port,
|
|
840
|
+
original: (HOST, WORK, 443, port),
|
|
841
|
+
..original
|
|
842
|
+
};
|
|
843
|
+
vec![
|
|
844
|
+
// First, middle and last members of both protocols.
|
|
845
|
+
(chain, at(6, 20000), true),
|
|
846
|
+
(chain, at(17, 20000), true),
|
|
847
|
+
(chain, at(6, 20255), true),
|
|
848
|
+
(chain, at(17, 20511), true),
|
|
849
|
+
(reply_chain, answer(at(6, 20511)), true),
|
|
850
|
+
(reply_chain, answer(at(17, 20300)), true),
|
|
851
|
+
// Just outside the member range, and a translated member.
|
|
852
|
+
(chain, at(6, 20512), false),
|
|
853
|
+
(chain, at(17, 19999), false),
|
|
854
|
+
(reply_chain, answer(at(6, 20512)), false),
|
|
855
|
+
(
|
|
856
|
+
chain,
|
|
857
|
+
Flow {
|
|
858
|
+
original: (HOST, WORK, 443, 20001),
|
|
859
|
+
..at(6, 20000)
|
|
860
|
+
},
|
|
861
|
+
false,
|
|
862
|
+
),
|
|
863
|
+
]
|
|
864
|
+
};
|
|
865
|
+
for fixture in [router(), host(), pool_guard()] {
|
|
866
|
+
let with = prepared(granted(fixture.clone(), many(1024)));
|
|
867
|
+
let baseline = prepared(fixture.clone());
|
|
868
|
+
for (chain, flow, admitted) in flows(&fixture) {
|
|
869
|
+
let decision = decide(&with, chain, &flow);
|
|
870
|
+
if admitted {
|
|
871
|
+
assert_eq!(decision.verdict, 1, "{chain}");
|
|
872
|
+
} else {
|
|
873
|
+
assert_eq!(decision, decide(&baseline, chain, &flow), "{chain}");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
#[test]
|
|
880
|
+
fn host_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
881
|
+
// Worst-case handle-only deletion of a previous maximum graph: 768 messages
|
|
882
|
+
// naming the longest table, a chain name and a handle.
|
|
883
|
+
let deletion = 768 * (20 + 72 + 36 + 12);
|
|
884
|
+
for (name, fixture) in [
|
|
885
|
+
("router", router()),
|
|
886
|
+
("host", host()),
|
|
887
|
+
("guard", pool_guard()),
|
|
888
|
+
] {
|
|
889
|
+
let prepared = prepared(granted(fixture.clone(), many(1024)));
|
|
890
|
+
let program = prepared
|
|
891
|
+
.program(&format!("snft_{}", "x".repeat(59)))
|
|
892
|
+
.unwrap();
|
|
893
|
+
let size = |kinds: &[u16]| -> usize {
|
|
894
|
+
program
|
|
895
|
+
.iter()
|
|
896
|
+
.filter(|(kind, _)| kinds.contains(kind))
|
|
897
|
+
.map(|(_, attributes)| wire::encode_attrs(attributes).len() + 20)
|
|
898
|
+
.sum()
|
|
899
|
+
};
|
|
900
|
+
let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
|
|
901
|
+
assert!(
|
|
902
|
+
program.len() < 768 && graph <= 100_000 && elements <= super::compile::ELEMENT_BYTES
|
|
903
|
+
);
|
|
904
|
+
assert!(deletion + graph + elements + 64 <= crate::wire::BATCH_BYTES);
|
|
905
|
+
// The contract maximum is the validation bound.
|
|
906
|
+
assert!(normalized(granted(fixture, many(1025))).is_err());
|
|
907
|
+
println!(
|
|
908
|
+
"HOST_GRANT_MAX scope={name} grants=1024 operations={} rule_bytes={graph} element_bytes={elements} batch_bound={}",
|
|
909
|
+
program.len(),
|
|
910
|
+
deletion + graph + elements + 64
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
// The reserve itself: a maximum previous graph beside a maximum target.
|
|
914
|
+
assert!(deletion + 100_000 + super::compile::ELEMENT_BYTES + 64 <= crate::wire::BATCH_BYTES);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/// A status carries up to three complete policies: the applied one and the
|
|
918
|
+
/// pending transition's previous and target. At the contract maximum in every
|
|
919
|
+
/// scope, that status still fits the private IPC bounds (1,000,000 JSON bytes
|
|
920
|
+
/// and 65,536 values in the facade, 1,048,576-byte lines).
|
|
921
|
+
#[test]
|
|
922
|
+
fn host_grant_policies_fit_the_ipc_bounds_three_at_a_time() {
|
|
923
|
+
fn values(value: &Value) -> usize {
|
|
924
|
+
1 + match value {
|
|
925
|
+
Value::Array(items) => items.iter().map(values).sum(),
|
|
926
|
+
Value::Object(map) => map.values().map(values).sum(),
|
|
927
|
+
_ => 0,
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
for fixture in [router(), host(), pool_guard()] {
|
|
931
|
+
let prepared = serde_json::to_value(prepared(granted(fixture, many(1024)))).unwrap();
|
|
932
|
+
let applied = json!({"receipt":{"identity":{"ownerId":"o".repeat(64),"instanceId":"i".repeat(64),
|
|
933
|
+
"tableName":format!("snft_{}", "x".repeat(59)),"bootId":"0".repeat(36),
|
|
934
|
+
"namespaceDevice":"18446744073709551615","namespaceInode":"18446744073709551615"},
|
|
935
|
+
"tableHandle":"18446744073709551615","revision":9007199254740991_u64,"digest":prepared["digest"]},
|
|
936
|
+
"prepared":prepared});
|
|
937
|
+
let status = json!({"id":"x".repeat(128),"success":true,"result":{"identity":applied["receipt"]["identity"],
|
|
938
|
+
"state":"failed-owned","enforced":false,"applied":applied,
|
|
939
|
+
"pending":{"previous":applied,"target":prepared},"error":"CONFLICT"}});
|
|
940
|
+
let bytes = serde_json::to_vec(&status).unwrap().len();
|
|
941
|
+
assert!(bytes <= 1_000_000 && bytes < crate::IPC_BYTES, "{bytes}");
|
|
942
|
+
assert!(values(&status) <= 65_536);
|
|
943
|
+
println!(
|
|
944
|
+
"HOST_GRANT_IPC status_bytes={bytes} values={}",
|
|
945
|
+
values(&status)
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
}
|