@push.rocks/smartnftables 2.5.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +31 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/managed.egress.types.d.ts +69 -3
- package/package.json +6 -6
- package/readme.md +342 -61
- package/rust/src/egress.compile.rs +157 -76
- package/rust/src/egress.host.rs +104 -44
- package/rust/src/egress.hostgrant.rs +14 -14
- package/rust/src/egress.keys.rs +305 -0
- package/rust/src/egress.leased.rs +399 -0
- package/rust/src/egress.poolguard.rs +45 -0
- package/rust/src/egress.private.rs +197 -0
- package/rust/src/egress.published.rs +370 -0
- package/rust/src/egress.router.rs +64 -295
- package/rust/src/egress.rs +248 -17
- package/rust/src/egress.workloadgrant.rs +88 -0
- package/rust/src/egress_hostgrant_tests.rs +196 -47
- package/rust/src/egress_localplatform_tests.rs +204 -0
- package/rust/src/egress_localport_tests.rs +266 -0
- package/rust/src/egress_publishedrange_tests.rs +812 -0
- package/rust/src/egress_routerequivalence_tests.rs +448 -0
- package/rust/src/egress_tests.rs +132 -230
- package/rust/src/egress_workloadgrant_tests.rs +313 -0
- package/rust/src/owner.rs +77 -15
- package/rust/src/owner_host_traffic_tests.rs +4 -0
- package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
- package/rust/src/owner_identity_tests.rs +228 -80
- package/rust/src/owner_localplatform_tests.rs +274 -0
- package/rust/src/owner_localport_tests.rs +241 -0
- 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 +15 -0
- package/rust/src/owner_workloadgrant_tests.rs +278 -0
- package/rust/src/policy.rs +29 -25
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/managed.egress.types.ts +71 -3
|
@@ -15,15 +15,15 @@ pub(crate) fn granted(mut value: Value, grants: Value) -> Value {
|
|
|
15
15
|
value["scope"]["hostGrants"] = grants;
|
|
16
16
|
value
|
|
17
17
|
}
|
|
18
|
-
fn normalized(value: Value) -> crate::Result<Policy> {
|
|
18
|
+
pub(super) fn normalized(value: Value) -> crate::Result<Policy> {
|
|
19
19
|
serde_json::from_value::<Policy>(value)
|
|
20
20
|
.map_err(|_| crate::Error::Invalid)?
|
|
21
21
|
.normalize()
|
|
22
22
|
}
|
|
23
|
-
fn prepared(value: Value) -> Prepared {
|
|
23
|
+
pub(super) fn prepared(value: Value) -> Prepared {
|
|
24
24
|
normalized(value).unwrap().prepare().unwrap()
|
|
25
25
|
}
|
|
26
|
-
fn bytes(prepared: &Prepared) -> Vec<u8> {
|
|
26
|
+
pub(super) fn bytes(prepared: &Prepared) -> Vec<u8> {
|
|
27
27
|
let mut result = Vec::new();
|
|
28
28
|
for (kind, attributes) in prepared.program("snft_host_grant").unwrap() {
|
|
29
29
|
result.extend(kind.to_be_bytes());
|
|
@@ -38,28 +38,38 @@ fn exact() -> Value {
|
|
|
38
38
|
|
|
39
39
|
/// One packet with the conntrack facts a filter hook observes.
|
|
40
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,
|
|
41
|
+
pub(super) struct Flow {
|
|
42
|
+
pub(super) input: Option<(u32, &'static str)>,
|
|
43
|
+
pub(super) output: Option<(u32, &'static str)>,
|
|
44
|
+
pub(super) protocol: u8,
|
|
45
|
+
pub(super) source: [u8; 4],
|
|
46
|
+
pub(super) destination: [u8; 4],
|
|
47
|
+
pub(super) source_port: u16,
|
|
48
|
+
pub(super) destination_port: u16,
|
|
49
|
+
pub(super) tcp_flags: u8,
|
|
50
|
+
pub(super) state: u32,
|
|
51
|
+
pub(super) reply: bool,
|
|
52
|
+
pub(super) zone: u16,
|
|
53
53
|
/// Original-direction conntrack tuple: source, destination, ports.
|
|
54
|
-
original: ([u8; 4], [u8; 4], u16, u16),
|
|
54
|
+
pub(super) original: ([u8; 4], [u8; 4], u16, u16),
|
|
55
|
+
/// The file system uid of the packet's user socket; None without one, when
|
|
56
|
+
/// `meta skuid` breaks the rule like the kernel.
|
|
57
|
+
pub(super) uid: Option<u32>,
|
|
58
|
+
/// The conntrack label of the flow's entry.
|
|
59
|
+
pub(super) label: [u8; 16],
|
|
55
60
|
}
|
|
56
61
|
const HOST: [u8; 4] = [10, 240, 0, 1];
|
|
57
62
|
const WORK: [u8; 4] = [10, 241, 0, 2];
|
|
58
63
|
const NEW: u32 = 8;
|
|
59
|
-
const ESTABLISHED: u32 = 2;
|
|
64
|
+
pub(super) const ESTABLISHED: u32 = 2;
|
|
60
65
|
const SYN: u8 = 0x02;
|
|
61
|
-
const ACK: u8 = 0x10;
|
|
62
|
-
|
|
66
|
+
pub(super) const ACK: u8 = 0x10;
|
|
67
|
+
/// The `reject with tcp reset` decision, distinct from a plain drop.
|
|
68
|
+
pub(super) const REJECT: i32 = i32::MIN;
|
|
69
|
+
pub(super) fn opening(
|
|
70
|
+
input: Option<(u32, &'static str)>,
|
|
71
|
+
output: Option<(u32, &'static str)>,
|
|
72
|
+
) -> Flow {
|
|
63
73
|
Flow {
|
|
64
74
|
input,
|
|
65
75
|
output,
|
|
@@ -73,6 +83,8 @@ fn opening(input: Option<(u32, &'static str)>, output: Option<(u32, &'static str
|
|
|
73
83
|
reply: false,
|
|
74
84
|
zone: 0,
|
|
75
85
|
original: (HOST, WORK, 443, 8080),
|
|
86
|
+
uid: None,
|
|
87
|
+
label: [0; 16],
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
90
|
/// The workload's answer to `flow`, arriving on the reverse links.
|
|
@@ -181,22 +193,59 @@ fn foreign(flow: Flow) -> Vec<Flow> {
|
|
|
181
193
|
/// kernel does: the terminal verdict, the chain that reached it (`policy` for the
|
|
182
194
|
/// base chain's accept policy) and the conntrack zone a raw rule stored.
|
|
183
195
|
#[derive(Debug, PartialEq, Eq)]
|
|
184
|
-
struct Decision {
|
|
185
|
-
verdict: i32,
|
|
186
|
-
chain: String,
|
|
187
|
-
zone: u16,
|
|
196
|
+
pub(super) struct Decision {
|
|
197
|
+
pub(super) verdict: i32,
|
|
198
|
+
pub(super) chain: String,
|
|
199
|
+
pub(super) zone: u16,
|
|
200
|
+
/// The NAT a nat-chain rule performed, which also ends that rule with an
|
|
201
|
+
/// accept exactly as the kernel's nat expression does.
|
|
202
|
+
pub(super) translation: Option<Translation>,
|
|
188
203
|
}
|
|
189
|
-
///
|
|
204
|
+
/// Source (0) or destination (1) NAT to an address and, when the rule names
|
|
205
|
+
/// them, an inclusive port range.
|
|
206
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
207
|
+
pub(super) struct Translation {
|
|
208
|
+
pub(super) kind: u32,
|
|
209
|
+
pub(super) address: [u8; 4],
|
|
210
|
+
pub(super) ports: Option<(u16, u16)>,
|
|
211
|
+
}
|
|
212
|
+
/// The compiled table: rules per chain, element keys per exact set, and the
|
|
213
|
+
/// fields and elements of every interval set or map.
|
|
190
214
|
struct Table {
|
|
191
215
|
chains: BTreeMap<String, Vec<Vec<Attr>>>,
|
|
192
216
|
sets: BTreeMap<String, Vec<Vec<u8>>>,
|
|
217
|
+
intervals: BTreeMap<String, Interval>,
|
|
193
218
|
}
|
|
194
|
-
|
|
219
|
+
/// Field byte lengths, then `(first, last, data)` per element.
|
|
220
|
+
#[derive(Default)]
|
|
221
|
+
struct Interval {
|
|
222
|
+
fields: Vec<usize>,
|
|
223
|
+
elements: Vec<(Vec<u8>, Vec<u8>, Option<Vec<u8>>)>,
|
|
224
|
+
}
|
|
225
|
+
impl Interval {
|
|
226
|
+
/// Every field of the key within its bounds, compared as the kernel's
|
|
227
|
+
/// pipapo backend does: byte strings of the field's own length.
|
|
228
|
+
fn find(&self, key: &[u8]) -> Option<&(Vec<u8>, Vec<u8>, Option<Vec<u8>>)> {
|
|
229
|
+
self.elements.iter().find(|(first, last, _)| {
|
|
230
|
+
let mut at = 0;
|
|
231
|
+
self.fields.iter().all(|length| {
|
|
232
|
+
let range = at..at + length;
|
|
233
|
+
at += length.div_ceil(4) * 4;
|
|
234
|
+
first[range.clone()] <= key[range.clone()] && key[range.clone()] <= last[range]
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
fn width(&self) -> usize {
|
|
239
|
+
self.fields.iter().map(|length| length.div_ceil(4) * 4).sum()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
pub(super) fn chains(prepared: &Prepared) -> BTreeMap<String, Vec<Vec<Attr>>> {
|
|
195
243
|
table(prepared).chains
|
|
196
244
|
}
|
|
197
245
|
fn table(prepared: &Prepared) -> Table {
|
|
198
246
|
let mut chains: BTreeMap<String, Vec<Vec<Attr>>> = BTreeMap::new();
|
|
199
247
|
let mut sets: BTreeMap<String, Vec<Vec<u8>>> = BTreeMap::new();
|
|
248
|
+
let mut intervals: BTreeMap<String, Interval> = BTreeMap::new();
|
|
200
249
|
for (kind, attributes) in prepared.program("snft_host_grant").unwrap() {
|
|
201
250
|
match kind {
|
|
202
251
|
3 => {
|
|
@@ -209,21 +258,59 @@ fn table(prepared: &Prepared) -> Table {
|
|
|
209
258
|
.or_default()
|
|
210
259
|
.push(wire::attrs(&wire::one(&attributes, 4).unwrap().value).unwrap()),
|
|
211
260
|
9 => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
.
|
|
261
|
+
let name = wire::text(&attributes, 2).unwrap();
|
|
262
|
+
if let Ok(description) = wire::one(&attributes, 9) {
|
|
263
|
+
let concatenation = wire::attrs(&description.value).unwrap();
|
|
264
|
+
let fields = wire::attrs(&wire::one(&concatenation, 2).unwrap().value)
|
|
265
|
+
.unwrap()
|
|
266
|
+
.iter()
|
|
267
|
+
.map(|field| {
|
|
268
|
+
wire::number(&wire::attrs(&field.value).unwrap(), 1).unwrap() as usize
|
|
269
|
+
})
|
|
270
|
+
.collect();
|
|
271
|
+
assert!(intervals
|
|
272
|
+
.insert(name, Interval { fields, elements: Vec::new() })
|
|
273
|
+
.is_none());
|
|
274
|
+
} else if wire::number(&attributes, 3).unwrap() & 8 != 0 {
|
|
275
|
+
// An exact map is one field of the whole key.
|
|
276
|
+
let fields = vec![wire::number(&attributes, 5).unwrap() as usize];
|
|
277
|
+
assert!(intervals
|
|
278
|
+
.insert(name, Interval { fields, elements: Vec::new() })
|
|
279
|
+
.is_none());
|
|
280
|
+
} else {
|
|
281
|
+
assert!(sets.insert(name, Vec::new()).is_none());
|
|
282
|
+
}
|
|
215
283
|
}
|
|
216
284
|
12 => {
|
|
217
|
-
let
|
|
285
|
+
let name = wire::text(&attributes, 2).unwrap();
|
|
218
286
|
for element in wire::attrs(&wire::one(&attributes, 3).unwrap().value).unwrap() {
|
|
219
287
|
let key = wire::attrs(&element.value).unwrap();
|
|
220
|
-
|
|
288
|
+
if let Some(interval) = intervals.get_mut(&name) {
|
|
289
|
+
// An element without a last key is one exact key.
|
|
290
|
+
let first = data(&key, 1);
|
|
291
|
+
let last = if wire::one(&key, 10).is_ok() {
|
|
292
|
+
data(&key, 10)
|
|
293
|
+
} else {
|
|
294
|
+
first.clone()
|
|
295
|
+
};
|
|
296
|
+
interval.elements.push((
|
|
297
|
+
first,
|
|
298
|
+
last,
|
|
299
|
+
wire::one(&key, 2).ok().map(|_| data(&key, 2)),
|
|
300
|
+
));
|
|
301
|
+
} else {
|
|
302
|
+
sets.get_mut(&name).unwrap().push(data(&key, 1));
|
|
303
|
+
}
|
|
221
304
|
}
|
|
222
305
|
}
|
|
223
306
|
other => panic!("operation {other}"),
|
|
224
307
|
}
|
|
225
308
|
}
|
|
226
|
-
Table {
|
|
309
|
+
Table {
|
|
310
|
+
chains,
|
|
311
|
+
sets,
|
|
312
|
+
intervals,
|
|
313
|
+
}
|
|
227
314
|
}
|
|
228
315
|
fn data(attributes: &[Attr], id: u16) -> Vec<u8> {
|
|
229
316
|
wire::attrs(&wire::one(attributes, id).unwrap().value).unwrap()[0]
|
|
@@ -246,6 +333,7 @@ fn load(flow: &Flow, name: &str, fields: &[Attr]) -> Option<Vec<u8>> {
|
|
|
246
333
|
Some(match name {
|
|
247
334
|
"meta" => match wire::number(fields, 2).unwrap() {
|
|
248
335
|
4 => interface(flow.input, false),
|
|
336
|
+
10 => flow.uid?.to_ne_bytes().to_vec(),
|
|
249
337
|
5 => interface(flow.output, false),
|
|
250
338
|
6 => interface(flow.input, true),
|
|
251
339
|
7 => interface(flow.output, true),
|
|
@@ -280,7 +368,7 @@ fn load(flow: &Flow, name: &str, fields: &[Attr]) -> Option<Vec<u8>> {
|
|
|
280
368
|
10 => vec![flow.protocol],
|
|
281
369
|
11 => source_port.to_be_bytes().to_vec(),
|
|
282
370
|
12 => destination_port.to_be_bytes().to_vec(),
|
|
283
|
-
13 =>
|
|
371
|
+
13 => flow.label.to_vec(),
|
|
284
372
|
17 => flow.zone.to_ne_bytes().to_vec(),
|
|
285
373
|
19 => source.to_vec(),
|
|
286
374
|
20 => destination.to_vec(),
|
|
@@ -311,6 +399,7 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
311
399
|
let mut registers = [0_u8; 80];
|
|
312
400
|
let mut matched = true;
|
|
313
401
|
let mut verdict: Option<(i32, Option<String>)> = None;
|
|
402
|
+
let mut translation = None;
|
|
314
403
|
for expression in expressions {
|
|
315
404
|
let parts = wire::attrs(&expression.value).unwrap();
|
|
316
405
|
let name = wire::text(&parts, 1).unwrap();
|
|
@@ -340,10 +429,14 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
340
429
|
}
|
|
341
430
|
}
|
|
342
431
|
"cmp" => {
|
|
343
|
-
assert_eq!(wire::number(&fields, 2).unwrap(), 0);
|
|
344
432
|
let expected = data(&fields, 3);
|
|
345
433
|
let at = offset(&fields, 1);
|
|
346
|
-
|
|
434
|
+
let equal = registers[at..at + expected.len()] == expected[..];
|
|
435
|
+
matched = match wire::number(&fields, 2).unwrap() {
|
|
436
|
+
0 => equal,
|
|
437
|
+
1 => !equal,
|
|
438
|
+
operation => panic!("cmp {operation}"),
|
|
439
|
+
};
|
|
347
440
|
}
|
|
348
441
|
"range" => {
|
|
349
442
|
assert_eq!(wire::number(&fields, 2).unwrap(), 0);
|
|
@@ -353,12 +446,29 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
353
446
|
matched = loaded >= &first[..] && loaded <= &last[..];
|
|
354
447
|
}
|
|
355
448
|
"lookup" => {
|
|
356
|
-
|
|
357
|
-
let
|
|
449
|
+
let inverted = wire::number(&fields, 5).unwrap() == 1;
|
|
450
|
+
let name = wire::text(&fields, 1).unwrap();
|
|
358
451
|
let at = offset(&fields, 2);
|
|
359
|
-
|
|
360
|
-
.
|
|
361
|
-
|
|
452
|
+
if let Some(interval) = table.intervals.get(&name) {
|
|
453
|
+
let key = registers[at..at + interval.width()].to_vec();
|
|
454
|
+
let found = interval.find(&key);
|
|
455
|
+
matched = found.is_some();
|
|
456
|
+
if let (Some((_, _, data)), Ok(_)) = (found, wire::one(&fields, 3)) {
|
|
457
|
+
// A map lookup stores the element's data.
|
|
458
|
+
let data = data.as_ref().unwrap();
|
|
459
|
+
let to = offset(&fields, 3);
|
|
460
|
+
registers[to..to + data.len()].copy_from_slice(data);
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
let keys = &table.sets[&name];
|
|
464
|
+
matched = keys
|
|
465
|
+
.iter()
|
|
466
|
+
.any(|key| registers[at..at + key.len()] == key[..]);
|
|
467
|
+
}
|
|
468
|
+
if inverted {
|
|
469
|
+
assert!(wire::one(&fields, 3).is_err());
|
|
470
|
+
matched = !matched;
|
|
471
|
+
}
|
|
362
472
|
}
|
|
363
473
|
"immediate" if wire::number(&fields, 1).unwrap() == 0 => {
|
|
364
474
|
let code = wire::attrs(
|
|
@@ -380,7 +490,23 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
380
490
|
let at = offset(&fields, 1);
|
|
381
491
|
registers[at..at + value.len()].copy_from_slice(&value);
|
|
382
492
|
}
|
|
383
|
-
"nat" => {
|
|
493
|
+
"nat" => {
|
|
494
|
+
let at = offset(&fields, 3);
|
|
495
|
+
let port = |id: u16| {
|
|
496
|
+
let at = offset(&fields, id);
|
|
497
|
+
u16::from_be_bytes(registers[at..at + 2].try_into().unwrap())
|
|
498
|
+
};
|
|
499
|
+
translation = Some(Translation {
|
|
500
|
+
kind: wire::number(&fields, 1).unwrap(),
|
|
501
|
+
address: registers[at..at + 4].try_into().unwrap(),
|
|
502
|
+
ports: wire::one(&fields, 5).ok().map(|_| (port(5), port(6))),
|
|
503
|
+
});
|
|
504
|
+
verdict = Some((1, None));
|
|
505
|
+
}
|
|
506
|
+
"reject" => {
|
|
507
|
+
assert_eq!(fields, vec![Attr::u32(1, 1)]);
|
|
508
|
+
verdict = Some((REJECT, None));
|
|
509
|
+
}
|
|
384
510
|
other => panic!("{other}"),
|
|
385
511
|
}
|
|
386
512
|
if !matched {
|
|
@@ -391,11 +517,12 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
391
517
|
continue;
|
|
392
518
|
}
|
|
393
519
|
match verdict {
|
|
394
|
-
Some((code @ (0 | 1), _)) => {
|
|
520
|
+
Some((code @ (0 | 1 | REJECT), _)) => {
|
|
395
521
|
return Step::Final(Decision {
|
|
396
522
|
verdict: code,
|
|
397
523
|
chain: chain.into(),
|
|
398
524
|
zone: *zone,
|
|
525
|
+
translation,
|
|
399
526
|
})
|
|
400
527
|
}
|
|
401
528
|
Some((-5, _)) => return Step::Next,
|
|
@@ -412,14 +539,35 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
412
539
|
Step::Next
|
|
413
540
|
}
|
|
414
541
|
/// A base chain that ends without a terminal verdict accepts by policy.
|
|
415
|
-
fn decide(prepared: &Prepared, chain: &str, flow: &Flow) -> Decision {
|
|
542
|
+
pub(super) fn decide(prepared: &Prepared, chain: &str, flow: &Flow) -> Decision {
|
|
543
|
+
Simulator::new(prepared).decide(chain, flow)
|
|
544
|
+
}
|
|
545
|
+
/// One compiled table, parsed once for many decisions.
|
|
546
|
+
pub(super) struct Simulator {
|
|
547
|
+
table: Table,
|
|
548
|
+
}
|
|
549
|
+
impl Simulator {
|
|
550
|
+
pub(super) fn new(prepared: &Prepared) -> Self {
|
|
551
|
+
Self {
|
|
552
|
+
table: table(prepared),
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
pub(super) fn has(&self, chain: &str) -> bool {
|
|
556
|
+
self.table.chains.contains_key(chain)
|
|
557
|
+
}
|
|
558
|
+
pub(super) fn decide(&self, chain: &str, flow: &Flow) -> Decision {
|
|
559
|
+
decide_in(&self.table, chain, flow)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
fn decide_in(table: &Table, chain: &str, flow: &Flow) -> Decision {
|
|
416
563
|
let mut zone = 0;
|
|
417
|
-
match run(
|
|
564
|
+
match run(table, chain, flow, &mut zone) {
|
|
418
565
|
Step::Final(decision) => decision,
|
|
419
566
|
Step::Next => Decision {
|
|
420
567
|
verdict: 1,
|
|
421
568
|
chain: "policy".into(),
|
|
422
569
|
zone,
|
|
570
|
+
translation: None,
|
|
423
571
|
},
|
|
424
572
|
}
|
|
425
573
|
}
|
|
@@ -793,7 +941,7 @@ fn host_grants_reject_ranges_wildcards_and_unbound_addresses() {
|
|
|
793
941
|
}
|
|
794
942
|
|
|
795
943
|
/// `count` distinct exact grants to the fixture workload, both protocols.
|
|
796
|
-
fn many(count: usize) -> Value {
|
|
944
|
+
pub(super) fn many(count: usize) -> Value {
|
|
797
945
|
json!((0..count)
|
|
798
946
|
.map(|index| host_grant(
|
|
799
947
|
if index % 2 == 0 { "tcp" } else { "udp" },
|
|
@@ -817,7 +965,8 @@ fn host_grants_compile_a_constant_rule_graph_and_one_element_per_grant() {
|
|
|
817
965
|
if let Some(arrival) = all.sets.get("host_grant_arrival") {
|
|
818
966
|
assert_eq!(arrival.len(), 1024);
|
|
819
967
|
}
|
|
820
|
-
|
|
968
|
+
let without = table(&prepared(fixture));
|
|
969
|
+
assert!(without.sets.keys().all(|name| !name.starts_with("host_grant")));
|
|
821
970
|
}
|
|
822
971
|
}
|
|
823
972
|
|
|
@@ -899,7 +1048,7 @@ fn host_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
|
899
1048
|
};
|
|
900
1049
|
let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
|
|
901
1050
|
assert!(
|
|
902
|
-
program.len() < 768 && graph <=
|
|
1051
|
+
program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES
|
|
903
1052
|
);
|
|
904
1053
|
assert!(deletion + graph + elements + 64 <= crate::wire::BATCH_BYTES);
|
|
905
1054
|
// The contract maximum is the validation bound.
|
|
@@ -911,7 +1060,7 @@ fn host_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
|
911
1060
|
);
|
|
912
1061
|
}
|
|
913
1062
|
// The reserve itself: a maximum previous graph beside a maximum target.
|
|
914
|
-
assert!(deletion +
|
|
1063
|
+
assert!(deletion + super::compile::TARGET_BYTES + 64 <= crate::wire::BATCH_BYTES);
|
|
915
1064
|
}
|
|
916
1065
|
|
|
917
1066
|
/// A status carries up to three complete policies: the applied one and the
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
//! Host-local platform endpoints in host transit: leased flows from a handoff
|
|
2
|
+
//! into a platform endpoint served on a host address, and nothing else.
|
|
3
|
+
use super::hostgrant_tests::{
|
|
4
|
+
bytes, chains, decide, normalized, opening, prepared, Flow, ACK, ESTABLISHED,
|
|
5
|
+
};
|
|
6
|
+
use super::tests::host;
|
|
7
|
+
use serde_json::{json, Value};
|
|
8
|
+
|
|
9
|
+
const TRANSIT: [u8; 4] = [10, 240, 0, 2];
|
|
10
|
+
const UPLINK: [u8; 4] = [192, 0, 2, 2];
|
|
11
|
+
const HANDOFF: [u8; 4] = [10, 240, 0, 1];
|
|
12
|
+
const LINK: Option<(u32, &str)> = Some((4, "handoff"));
|
|
13
|
+
|
|
14
|
+
/// The host fixture with two platform endpoints on host addresses: a TCP hub
|
|
15
|
+
/// on the uplink address and a UDP store on the handoff address.
|
|
16
|
+
pub(crate) fn served(mut value: Value) -> Value {
|
|
17
|
+
value["scope"]["protection"]["platformEndpoints"]
|
|
18
|
+
.as_array_mut()
|
|
19
|
+
.unwrap()
|
|
20
|
+
.extend([
|
|
21
|
+
json!({"id":"hub","address":"192.0.2.2","protocol":"tcp","port":8443}),
|
|
22
|
+
json!({"id":"store","address":"10.240.0.1","protocol":"udp","port":7000}),
|
|
23
|
+
]);
|
|
24
|
+
value
|
|
25
|
+
}
|
|
26
|
+
pub(crate) fn local(mut value: Value, ids: Value) -> Value {
|
|
27
|
+
value["scope"]["localPlatformEndpoints"] = ids;
|
|
28
|
+
value
|
|
29
|
+
}
|
|
30
|
+
fn exact() -> Value {
|
|
31
|
+
local(served(host()), json!(["store", "hub"]))
|
|
32
|
+
}
|
|
33
|
+
/// A leased opening from the transit source address arriving on the handoff.
|
|
34
|
+
fn arrival(protocol: u8, source_port: u16, destination: [u8; 4], port: u16) -> Flow {
|
|
35
|
+
Flow {
|
|
36
|
+
input: LINK,
|
|
37
|
+
output: None,
|
|
38
|
+
protocol,
|
|
39
|
+
source: TRANSIT,
|
|
40
|
+
destination,
|
|
41
|
+
source_port,
|
|
42
|
+
destination_port: port,
|
|
43
|
+
original: (TRANSIT, destination, source_port, port),
|
|
44
|
+
..opening(LINK, None)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/// The host's answer to `flow`, leaving on the handoff.
|
|
48
|
+
fn answer(flow: Flow) -> Flow {
|
|
49
|
+
Flow {
|
|
50
|
+
input: None,
|
|
51
|
+
output: LINK,
|
|
52
|
+
source: flow.destination,
|
|
53
|
+
destination: flow.source,
|
|
54
|
+
source_port: flow.destination_port,
|
|
55
|
+
destination_port: flow.source_port,
|
|
56
|
+
tcp_flags: ACK,
|
|
57
|
+
state: ESTABLISHED,
|
|
58
|
+
reply: true,
|
|
59
|
+
..flow
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[test]
|
|
64
|
+
fn local_platform_endpoints_absent_and_empty_keep_the_host_digest_and_compiled_bytes() {
|
|
65
|
+
let baseline = prepared(host());
|
|
66
|
+
let empty = prepared(local(host(), json!([])));
|
|
67
|
+
assert_eq!(empty, baseline);
|
|
68
|
+
assert_eq!(bytes(&empty), bytes(&baseline));
|
|
69
|
+
let with = prepared(exact());
|
|
70
|
+
assert!(serde_json::to_string(&with.policy)
|
|
71
|
+
.unwrap()
|
|
72
|
+
.contains("\"localPlatformEndpoints\":[\"hub\",\"store\"]"));
|
|
73
|
+
// Canonical order.
|
|
74
|
+
assert_eq!(
|
|
75
|
+
prepared(local(served(host()), json!(["hub", "store"]))),
|
|
76
|
+
with
|
|
77
|
+
);
|
|
78
|
+
// The endpoints and the chains exist only when declared.
|
|
79
|
+
let graph = chains(&with);
|
|
80
|
+
assert!(
|
|
81
|
+
graph.contains_key("local_platform_in")
|
|
82
|
+
&& !chains(&baseline).contains_key("local_platform_in")
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn local_platform_endpoint_on_a_host_address_is_refused_unless_declared() {
|
|
88
|
+
// The refusal stays for every undeclared endpoint on a host address.
|
|
89
|
+
assert!(normalized(served(host())).is_err());
|
|
90
|
+
assert!(normalized(local(served(host()), json!(["hub"]))).is_err());
|
|
91
|
+
assert!(normalized(local(served(host()), json!(["store"]))).is_err());
|
|
92
|
+
assert!(normalized(exact()).is_ok());
|
|
93
|
+
for invalid in [
|
|
94
|
+
// Unknown, duplicate, or an endpoint the host does not hold.
|
|
95
|
+
json!(["hub", "store", "missing"]),
|
|
96
|
+
json!(["hub", "store", "hub"]),
|
|
97
|
+
json!(["hub", "store", "dns"]),
|
|
98
|
+
json!(["hub", "store", "relay"]),
|
|
99
|
+
json!(["hub", "store", ""]),
|
|
100
|
+
json!(["hub", "store", 1]),
|
|
101
|
+
json!("hub"),
|
|
102
|
+
json!({"hub": true}),
|
|
103
|
+
] {
|
|
104
|
+
assert!(
|
|
105
|
+
normalized(local(served(host()), invalid.clone())).is_err(),
|
|
106
|
+
"{invalid}"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[test]
|
|
112
|
+
fn local_platform_endpoint_admits_only_leased_exact_untranslated_flows_and_their_replies() {
|
|
113
|
+
let baseline = prepared(host());
|
|
114
|
+
let with = prepared(exact());
|
|
115
|
+
let tcp = arrival(6, 10500, UPLINK, 8443);
|
|
116
|
+
let udp = arrival(17, 10500, HANDOFF, 7000);
|
|
117
|
+
let decided = |flow: &Flow| {
|
|
118
|
+
let chain = if flow.input.is_some() {
|
|
119
|
+
"input"
|
|
120
|
+
} else {
|
|
121
|
+
"output"
|
|
122
|
+
};
|
|
123
|
+
(
|
|
124
|
+
decide(&baseline, chain, flow).verdict,
|
|
125
|
+
decide(&with, chain, flow).verdict,
|
|
126
|
+
)
|
|
127
|
+
};
|
|
128
|
+
// Every admitted packet: the TCP opening, established TCP, the UDP opening,
|
|
129
|
+
// and both replies. The baseline denies each at the handoff.
|
|
130
|
+
for flow in [
|
|
131
|
+
tcp,
|
|
132
|
+
Flow {
|
|
133
|
+
state: ESTABLISHED,
|
|
134
|
+
tcp_flags: ACK,
|
|
135
|
+
..tcp
|
|
136
|
+
},
|
|
137
|
+
udp,
|
|
138
|
+
answer(tcp),
|
|
139
|
+
answer(udp),
|
|
140
|
+
] {
|
|
141
|
+
assert_eq!(decided(&flow), (0, 1));
|
|
142
|
+
}
|
|
143
|
+
// Everything else keeps the baseline decision: a source port outside the
|
|
144
|
+
// leased range of the protocol, another source address, another port or
|
|
145
|
+
// protocol, a flow translated in transit, another zone, a TCP opening
|
|
146
|
+
// without SYN, and a host-origin opening toward the transit address.
|
|
147
|
+
for flow in [
|
|
148
|
+
arrival(6, 20000, UPLINK, 8443),
|
|
149
|
+
arrival(6, 9999, UPLINK, 8443),
|
|
150
|
+
arrival(6, 10500, UPLINK, 8444),
|
|
151
|
+
arrival(17, 10500, UPLINK, 8443),
|
|
152
|
+
arrival(6, 10500, HANDOFF, 7000),
|
|
153
|
+
arrival(17, 10500, HANDOFF, 7001),
|
|
154
|
+
Flow {
|
|
155
|
+
source: [10, 240, 0, 3],
|
|
156
|
+
original: ([10, 240, 0, 3], UPLINK, 10500, 8443),
|
|
157
|
+
..tcp
|
|
158
|
+
},
|
|
159
|
+
Flow {
|
|
160
|
+
original: (TRANSIT, HANDOFF, 10500, 9000),
|
|
161
|
+
..tcp
|
|
162
|
+
},
|
|
163
|
+
Flow { zone: 7, ..tcp },
|
|
164
|
+
Flow {
|
|
165
|
+
tcp_flags: ACK,
|
|
166
|
+
..tcp
|
|
167
|
+
},
|
|
168
|
+
Flow {
|
|
169
|
+
reply: false,
|
|
170
|
+
state: 8,
|
|
171
|
+
tcp_flags: 0x02,
|
|
172
|
+
original: (UPLINK, TRANSIT, 8443, 10500),
|
|
173
|
+
..answer(tcp)
|
|
174
|
+
},
|
|
175
|
+
Flow {
|
|
176
|
+
state: 8,
|
|
177
|
+
..answer(tcp)
|
|
178
|
+
},
|
|
179
|
+
] {
|
|
180
|
+
let (before, after) = decided(&flow);
|
|
181
|
+
assert_eq!(after, before, "{:?}", flow.original);
|
|
182
|
+
}
|
|
183
|
+
// A translated arrival that the source range admits is still refused: the
|
|
184
|
+
// original tuple is part of every check.
|
|
185
|
+
assert_eq!(
|
|
186
|
+
decided(&Flow {
|
|
187
|
+
original: (TRANSIT, HANDOFF, 10500, 9000),
|
|
188
|
+
..tcp
|
|
189
|
+
}),
|
|
190
|
+
(0, 0)
|
|
191
|
+
);
|
|
192
|
+
// Forwarded traffic and the uplink keep their exact previous rules. (The
|
|
193
|
+
// protected chains list every platform endpoint, served here or not.)
|
|
194
|
+
let graph = chains(&with);
|
|
195
|
+
let reference = chains(&baseline);
|
|
196
|
+
for name in ["forward", "post"] {
|
|
197
|
+
assert_eq!(graph[name], reference[name], "{name}");
|
|
198
|
+
}
|
|
199
|
+
// One jump per leased range per direction, ahead of the handoff denials.
|
|
200
|
+
assert_eq!(graph["input"].len(), reference["input"].len() + 2);
|
|
201
|
+
assert_eq!(graph["output"].len(), reference["output"].len() + 2);
|
|
202
|
+
assert_eq!(graph["local_platform_in"].len(), 4);
|
|
203
|
+
assert_eq!(graph["local_platform_back"].len(), 2);
|
|
204
|
+
}
|