@push.rocks/smartnftables 2.6.0 → 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 +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/managed.egress.types.d.ts +25 -3
- package/package.json +6 -6
- package/readme.md +205 -63
- package/rust/src/egress.compile.rs +155 -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 +126 -11
- package/rust/src/egress_hostgrant_tests.rs +149 -18
- package/rust/src/egress_localport_tests.rs +3 -1
- 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 +6 -7
- 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 +29 -25
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/managed.egress.types.ts +25 -3
|
@@ -55,6 +55,8 @@ pub(super) struct Flow {
|
|
|
55
55
|
/// The file system uid of the packet's user socket; None without one, when
|
|
56
56
|
/// `meta skuid` breaks the rule like the kernel.
|
|
57
57
|
pub(super) uid: Option<u32>,
|
|
58
|
+
/// The conntrack label of the flow's entry.
|
|
59
|
+
pub(super) label: [u8; 16],
|
|
58
60
|
}
|
|
59
61
|
const HOST: [u8; 4] = [10, 240, 0, 1];
|
|
60
62
|
const WORK: [u8; 4] = [10, 241, 0, 2];
|
|
@@ -82,6 +84,7 @@ pub(super) fn opening(
|
|
|
82
84
|
zone: 0,
|
|
83
85
|
original: (HOST, WORK, 443, 8080),
|
|
84
86
|
uid: None,
|
|
87
|
+
label: [0; 16],
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
/// The workload's answer to `flow`, arriving on the reverse links.
|
|
@@ -194,11 +197,47 @@ pub(super) struct Decision {
|
|
|
194
197
|
pub(super) verdict: i32,
|
|
195
198
|
pub(super) chain: String,
|
|
196
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>,
|
|
197
203
|
}
|
|
198
|
-
///
|
|
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.
|
|
199
214
|
struct Table {
|
|
200
215
|
chains: BTreeMap<String, Vec<Vec<Attr>>>,
|
|
201
216
|
sets: BTreeMap<String, Vec<Vec<u8>>>,
|
|
217
|
+
intervals: BTreeMap<String, Interval>,
|
|
218
|
+
}
|
|
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
|
+
}
|
|
202
241
|
}
|
|
203
242
|
pub(super) fn chains(prepared: &Prepared) -> BTreeMap<String, Vec<Vec<Attr>>> {
|
|
204
243
|
table(prepared).chains
|
|
@@ -206,6 +245,7 @@ pub(super) fn chains(prepared: &Prepared) -> BTreeMap<String, Vec<Vec<Attr>>> {
|
|
|
206
245
|
fn table(prepared: &Prepared) -> Table {
|
|
207
246
|
let mut chains: BTreeMap<String, Vec<Vec<Attr>>> = BTreeMap::new();
|
|
208
247
|
let mut sets: BTreeMap<String, Vec<Vec<u8>>> = BTreeMap::new();
|
|
248
|
+
let mut intervals: BTreeMap<String, Interval> = BTreeMap::new();
|
|
209
249
|
for (kind, attributes) in prepared.program("snft_host_grant").unwrap() {
|
|
210
250
|
match kind {
|
|
211
251
|
3 => {
|
|
@@ -218,21 +258,59 @@ fn table(prepared: &Prepared) -> Table {
|
|
|
218
258
|
.or_default()
|
|
219
259
|
.push(wire::attrs(&wire::one(&attributes, 4).unwrap().value).unwrap()),
|
|
220
260
|
9 => {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.
|
|
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
|
+
}
|
|
224
283
|
}
|
|
225
284
|
12 => {
|
|
226
|
-
let
|
|
285
|
+
let name = wire::text(&attributes, 2).unwrap();
|
|
227
286
|
for element in wire::attrs(&wire::one(&attributes, 3).unwrap().value).unwrap() {
|
|
228
287
|
let key = wire::attrs(&element.value).unwrap();
|
|
229
|
-
|
|
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
|
+
}
|
|
230
304
|
}
|
|
231
305
|
}
|
|
232
306
|
other => panic!("operation {other}"),
|
|
233
307
|
}
|
|
234
308
|
}
|
|
235
|
-
Table {
|
|
309
|
+
Table {
|
|
310
|
+
chains,
|
|
311
|
+
sets,
|
|
312
|
+
intervals,
|
|
313
|
+
}
|
|
236
314
|
}
|
|
237
315
|
fn data(attributes: &[Attr], id: u16) -> Vec<u8> {
|
|
238
316
|
wire::attrs(&wire::one(attributes, id).unwrap().value).unwrap()[0]
|
|
@@ -290,7 +368,7 @@ fn load(flow: &Flow, name: &str, fields: &[Attr]) -> Option<Vec<u8>> {
|
|
|
290
368
|
10 => vec![flow.protocol],
|
|
291
369
|
11 => source_port.to_be_bytes().to_vec(),
|
|
292
370
|
12 => destination_port.to_be_bytes().to_vec(),
|
|
293
|
-
13 =>
|
|
371
|
+
13 => flow.label.to_vec(),
|
|
294
372
|
17 => flow.zone.to_ne_bytes().to_vec(),
|
|
295
373
|
19 => source.to_vec(),
|
|
296
374
|
20 => destination.to_vec(),
|
|
@@ -321,6 +399,7 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
321
399
|
let mut registers = [0_u8; 80];
|
|
322
400
|
let mut matched = true;
|
|
323
401
|
let mut verdict: Option<(i32, Option<String>)> = None;
|
|
402
|
+
let mut translation = None;
|
|
324
403
|
for expression in expressions {
|
|
325
404
|
let parts = wire::attrs(&expression.value).unwrap();
|
|
326
405
|
let name = wire::text(&parts, 1).unwrap();
|
|
@@ -367,12 +446,29 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
367
446
|
matched = loaded >= &first[..] && loaded <= &last[..];
|
|
368
447
|
}
|
|
369
448
|
"lookup" => {
|
|
370
|
-
|
|
371
|
-
let
|
|
449
|
+
let inverted = wire::number(&fields, 5).unwrap() == 1;
|
|
450
|
+
let name = wire::text(&fields, 1).unwrap();
|
|
372
451
|
let at = offset(&fields, 2);
|
|
373
|
-
|
|
374
|
-
.
|
|
375
|
-
|
|
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
|
+
}
|
|
376
472
|
}
|
|
377
473
|
"immediate" if wire::number(&fields, 1).unwrap() == 0 => {
|
|
378
474
|
let code = wire::attrs(
|
|
@@ -394,7 +490,19 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
394
490
|
let at = offset(&fields, 1);
|
|
395
491
|
registers[at..at + value.len()].copy_from_slice(&value);
|
|
396
492
|
}
|
|
397
|
-
"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
|
+
}
|
|
398
506
|
"reject" => {
|
|
399
507
|
assert_eq!(fields, vec![Attr::u32(1, 1)]);
|
|
400
508
|
verdict = Some((REJECT, None));
|
|
@@ -414,6 +522,7 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
414
522
|
verdict: code,
|
|
415
523
|
chain: chain.into(),
|
|
416
524
|
zone: *zone,
|
|
525
|
+
translation,
|
|
417
526
|
})
|
|
418
527
|
}
|
|
419
528
|
Some((-5, _)) => return Step::Next,
|
|
@@ -431,13 +540,34 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
431
540
|
}
|
|
432
541
|
/// A base chain that ends without a terminal verdict accepts by policy.
|
|
433
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 {
|
|
434
563
|
let mut zone = 0;
|
|
435
|
-
match run(
|
|
564
|
+
match run(table, chain, flow, &mut zone) {
|
|
436
565
|
Step::Final(decision) => decision,
|
|
437
566
|
Step::Next => Decision {
|
|
438
567
|
verdict: 1,
|
|
439
568
|
chain: "policy".into(),
|
|
440
569
|
zone,
|
|
570
|
+
translation: None,
|
|
441
571
|
},
|
|
442
572
|
}
|
|
443
573
|
}
|
|
@@ -835,7 +965,8 @@ fn host_grants_compile_a_constant_rule_graph_and_one_element_per_grant() {
|
|
|
835
965
|
if let Some(arrival) = all.sets.get("host_grant_arrival") {
|
|
836
966
|
assert_eq!(arrival.len(), 1024);
|
|
837
967
|
}
|
|
838
|
-
|
|
968
|
+
let without = table(&prepared(fixture));
|
|
969
|
+
assert!(without.sets.keys().all(|name| !name.starts_with("host_grant")));
|
|
839
970
|
}
|
|
840
971
|
}
|
|
841
972
|
|
|
@@ -917,7 +1048,7 @@ fn host_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
|
917
1048
|
};
|
|
918
1049
|
let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
|
|
919
1050
|
assert!(
|
|
920
|
-
program.len() < 768 && graph <=
|
|
1051
|
+
program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES
|
|
921
1052
|
);
|
|
922
1053
|
assert!(deletion + graph + elements + 64 <= crate::wire::BATCH_BYTES);
|
|
923
1054
|
// The contract maximum is the validation bound.
|
|
@@ -929,7 +1060,7 @@ fn host_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
|
929
1060
|
);
|
|
930
1061
|
}
|
|
931
1062
|
// The reserve itself: a maximum previous graph beside a maximum target.
|
|
932
|
-
assert!(deletion +
|
|
1063
|
+
assert!(deletion + super::compile::TARGET_BYTES + 64 <= crate::wire::BATCH_BYTES);
|
|
933
1064
|
}
|
|
934
1065
|
|
|
935
1066
|
/// A status carries up to three complete policies: the applied one and the
|
|
@@ -118,6 +118,7 @@ fn local_tcp_port_owner_rejects_every_other_uid_at_its_exact_loopback_destinatio
|
|
|
118
118
|
input,
|
|
119
119
|
output: None,
|
|
120
120
|
uid: None,
|
|
121
|
+
label: [0; 16],
|
|
121
122
|
..dial(10010, None)
|
|
122
123
|
};
|
|
123
124
|
assert_eq!(decide(&with, "input", &arriving(LO)).verdict, 1);
|
|
@@ -132,6 +133,7 @@ fn local_tcp_port_owner_rejects_every_other_uid_at_its_exact_loopback_destinatio
|
|
|
132
133
|
// Guarded pool traffic from any user keeps the guard's decision.
|
|
133
134
|
let pool = Flow {
|
|
134
135
|
uid: Some(1000),
|
|
136
|
+
label: [0; 16],
|
|
135
137
|
..opening(None, Some((4, "handoff")))
|
|
136
138
|
};
|
|
137
139
|
assert_eq!(
|
|
@@ -250,7 +252,7 @@ fn local_tcp_port_owners_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
|
250
252
|
.sum()
|
|
251
253
|
};
|
|
252
254
|
let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
|
|
253
|
-
assert!(program.len() < 768 && graph <=
|
|
255
|
+
assert!(program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES);
|
|
254
256
|
assert!(deletion + graph + elements + 64 <= wire::BATCH_BYTES);
|
|
255
257
|
println!(
|
|
256
258
|
"LOCAL_TCP_PORT_OWNER_MAX owners=8 prefixes=64 grants=1024 operations={} rule_bytes={graph}",
|