@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
|
@@ -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,14 +398,81 @@ 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
|
}
|
|
330
416
|
}
|
|
417
|
+
|
|
418
|
+
/// A loopback port owner compiles `meta skuid != uid` and a TCP reset. The graph
|
|
419
|
+
/// identity keeps the comparison operator, the uid and the reject type, and the
|
|
420
|
+
/// only reject Linux may dump back is a plain TCP reset.
|
|
421
|
+
#[test]
|
|
422
|
+
fn local_tcp_port_owner_graph_identity_preserves_uid_operator_and_tcp_reset() {
|
|
423
|
+
let policy = |uid: u32| {
|
|
424
|
+
crate::egress::localport_tests::owned(
|
|
425
|
+
crate::egress::tests::pool_guard(),
|
|
426
|
+
serde_json::json!([crate::egress::localport_tests::owner(
|
|
427
|
+
"127.0.0.1",
|
|
428
|
+
10010,
|
|
429
|
+
uid
|
|
430
|
+
)]),
|
|
431
|
+
)
|
|
432
|
+
};
|
|
433
|
+
let output = |uid: u32| {
|
|
434
|
+
let prepared = serde_json::from_value::<crate::egress::Policy>(policy(uid))
|
|
435
|
+
.unwrap()
|
|
436
|
+
.prepare()
|
|
437
|
+
.unwrap();
|
|
438
|
+
let program = prepared.program("snft_identity").unwrap();
|
|
439
|
+
let (_, rule) = program
|
|
440
|
+
.into_iter()
|
|
441
|
+
.find(|(kind, attributes)| *kind == 6 && wire::text(attributes, 2).unwrap() == "output")
|
|
442
|
+
.unwrap();
|
|
443
|
+
rule
|
|
444
|
+
};
|
|
445
|
+
let root = rule_identity(&output(0)).unwrap();
|
|
446
|
+
assert_ne!(rule_identity(&output(1000)).unwrap().1, root.1);
|
|
447
|
+
// The same rule with `==` instead of `!=` is a different graph.
|
|
448
|
+
let mut equal = output(0);
|
|
449
|
+
let expressions = wire::attrs(&wire::one(&equal, 4).unwrap().value).unwrap();
|
|
450
|
+
let flipped: Vec<_> = expressions
|
|
451
|
+
.iter()
|
|
452
|
+
.enumerate()
|
|
453
|
+
.map(|(index, item)| {
|
|
454
|
+
if index + 2 != expressions.len() {
|
|
455
|
+
return item.clone();
|
|
456
|
+
}
|
|
457
|
+
let parts = wire::attrs(&item.value).unwrap();
|
|
458
|
+
let mut fields = wire::attrs(&wire::one(&parts, 2).unwrap().value).unwrap();
|
|
459
|
+
assert_eq!(wire::number(&fields, 2).unwrap(), 1);
|
|
460
|
+
fields[1] = Attr::u32(2, 0);
|
|
461
|
+
expression("cmp", fields)
|
|
462
|
+
})
|
|
463
|
+
.collect();
|
|
464
|
+
equal[2] = Attr::nested(4, flipped);
|
|
465
|
+
assert_ne!(rule_identity(&equal).unwrap(), root);
|
|
466
|
+
let reject = |data: Vec<Attr>| rule(vec![expression("reject", data)]);
|
|
467
|
+
assert!(rule_identity(&reject(vec![Attr::u32(1, 1)])).is_ok());
|
|
468
|
+
for invalid in [
|
|
469
|
+
vec![],
|
|
470
|
+
vec![Attr::u32(1, 0)],
|
|
471
|
+
vec![Attr::u32(1, 2)],
|
|
472
|
+
vec![Attr::u32(1, 1), Attr::bytes(2, vec![3])],
|
|
473
|
+
vec![Attr::u32(1, 1), Attr::u32(1, 1)],
|
|
474
|
+
vec![Attr::bytes(1, vec![1])],
|
|
475
|
+
] {
|
|
476
|
+
assert!(rule_identity(&reject(invalid)).is_err());
|
|
477
|
+
}
|
|
478
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
use super::egress_traffic_tests::{connect, denied, host_policy, ns_ip, roundtrip};
|
|
2
|
+
use super::host_traffic_tests::{immediate, payload};
|
|
3
|
+
use super::hostgrant_traffic_tests::Hop;
|
|
4
|
+
use super::router_traffic_tests::connect_from;
|
|
5
|
+
use super::*;
|
|
6
|
+
use crate::{
|
|
7
|
+
egress,
|
|
8
|
+
policy::{expr, meta},
|
|
9
|
+
};
|
|
10
|
+
use std::io::{Read, Write};
|
|
11
|
+
use std::net::{TcpListener, TcpStream, UdpSocket};
|
|
12
|
+
use std::time::Duration;
|
|
13
|
+
|
|
14
|
+
const ROUTER: &str = "lp_router";
|
|
15
|
+
const HOST: &str = "lp_host";
|
|
16
|
+
const PEER: &str = "lp_peer";
|
|
17
|
+
|
|
18
|
+
/// Two platform endpoints the host serves itself: a TCP hub on the uplink
|
|
19
|
+
/// address and a UDP store on the handoff address.
|
|
20
|
+
fn served(policy: &mut egress::Policy, declared: bool) {
|
|
21
|
+
let egress::Scope::HostTransit(scope) = &mut policy.scope else {
|
|
22
|
+
panic!()
|
|
23
|
+
};
|
|
24
|
+
if declared {
|
|
25
|
+
scope.protection.platform_endpoints.extend([
|
|
26
|
+
egress::PlatformEndpoint {
|
|
27
|
+
id: "hub".into(),
|
|
28
|
+
address: "192.0.2.2".into(),
|
|
29
|
+
protocol: "tcp".into(),
|
|
30
|
+
port: 8443,
|
|
31
|
+
},
|
|
32
|
+
egress::PlatformEndpoint {
|
|
33
|
+
id: "store".into(),
|
|
34
|
+
address: "10.240.0.1".into(),
|
|
35
|
+
protocol: "udp".into(),
|
|
36
|
+
port: 7000,
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
scope.local_platform_endpoints = vec!["hub".into(), "store".into()];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
fn transit_policy(revision: u64, declared: bool) -> egress::Policy {
|
|
43
|
+
let mut policy = host_policy();
|
|
44
|
+
policy.revision = revision;
|
|
45
|
+
served(&mut policy, declared);
|
|
46
|
+
policy
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A foreign prerouting DNAT, only in this guest: the handoff address port 9000
|
|
50
|
+
// translates to the served hub, so a leased flow could otherwise reach the hub
|
|
51
|
+
// under another destination.
|
|
52
|
+
fn foreign_translation() {
|
|
53
|
+
let table = "snft_foreign_local_platform";
|
|
54
|
+
let mut expressions = meta(16, vec![6]);
|
|
55
|
+
expressions.extend(payload(16, vec![10, 240, 0, 1], false));
|
|
56
|
+
expressions.extend(payload(2, 9000_u16.to_be_bytes().to_vec(), true));
|
|
57
|
+
expressions.push(immediate(vec![192, 0, 2, 2]));
|
|
58
|
+
expressions.push(expr(
|
|
59
|
+
"immediate",
|
|
60
|
+
vec![
|
|
61
|
+
Attr::u32(1, 2),
|
|
62
|
+
Attr::nested(2, vec![Attr::bytes(1, 8443_u16.to_be_bytes().to_vec())]),
|
|
63
|
+
],
|
|
64
|
+
));
|
|
65
|
+
expressions.push(expr(
|
|
66
|
+
"nat",
|
|
67
|
+
vec![
|
|
68
|
+
Attr::u32(1, 1),
|
|
69
|
+
Attr::u32(2, 2),
|
|
70
|
+
Attr::u32(3, 1),
|
|
71
|
+
Attr::u32(4, 1),
|
|
72
|
+
Attr::u32(5, 2),
|
|
73
|
+
Attr::u32(6, 2),
|
|
74
|
+
Attr::u32(7, 3),
|
|
75
|
+
],
|
|
76
|
+
));
|
|
77
|
+
let operations = vec![
|
|
78
|
+
(0, 0x600, vec![Attr::string(1, table), Attr::u32(2, 6)]),
|
|
79
|
+
(
|
|
80
|
+
3,
|
|
81
|
+
0x600,
|
|
82
|
+
vec![
|
|
83
|
+
Attr::string(1, table),
|
|
84
|
+
Attr::string(3, "pre"),
|
|
85
|
+
Attr::nested(4, vec![Attr::u32(1, 0), Attr::u32(2, -100_i32 as u32)]),
|
|
86
|
+
Attr::u32(5, 1),
|
|
87
|
+
Attr::string(7, "nat"),
|
|
88
|
+
Attr::u32(10, 1),
|
|
89
|
+
],
|
|
90
|
+
),
|
|
91
|
+
(
|
|
92
|
+
6,
|
|
93
|
+
0xc00,
|
|
94
|
+
vec![
|
|
95
|
+
Attr::string(1, table),
|
|
96
|
+
Attr::string(2, "pre"),
|
|
97
|
+
Attr::nested(4, expressions),
|
|
98
|
+
],
|
|
99
|
+
),
|
|
100
|
+
];
|
|
101
|
+
let mut socket = wire::Socket::open().unwrap();
|
|
102
|
+
let generation = socket.generation().unwrap();
|
|
103
|
+
socket.batch(generation, operations).unwrap();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// A UDP socket in `namespace` bound to an exact address and port.
|
|
107
|
+
fn udp(namespace: &str, address: String) -> UdpSocket {
|
|
108
|
+
let socket = in_namespace(namespace, move || UdpSocket::bind(address).unwrap());
|
|
109
|
+
socket
|
|
110
|
+
.set_read_timeout(Some(Duration::from_millis(250)))
|
|
111
|
+
.unwrap();
|
|
112
|
+
socket
|
|
113
|
+
}
|
|
114
|
+
/// A TCP flow from an exact leased source address and port in the router
|
|
115
|
+
/// namespace. On success the host's listener observes that exact source and
|
|
116
|
+
/// data crosses both ways.
|
|
117
|
+
fn dial(source: &str, destination: &str, listener: &TcpListener) -> Option<(TcpStream, TcpStream)> {
|
|
118
|
+
let (from, to) = (source.to_owned(), destination.to_owned());
|
|
119
|
+
let client = in_namespace(ROUTER, move || connect_from(&from, &to)).ok()?;
|
|
120
|
+
let (server, peer) = listener.accept().unwrap();
|
|
121
|
+
assert_eq!(peer.to_string(), source);
|
|
122
|
+
let (mut client, mut server) = (client, server);
|
|
123
|
+
for stream in [&client, &server] {
|
|
124
|
+
stream
|
|
125
|
+
.set_read_timeout(Some(Duration::from_secs(1)))
|
|
126
|
+
.unwrap();
|
|
127
|
+
}
|
|
128
|
+
client.write_all(b"ping").unwrap();
|
|
129
|
+
server.read_exact(&mut [0; 4]).unwrap();
|
|
130
|
+
server.write_all(b"pong").unwrap();
|
|
131
|
+
client.read_exact(&mut [0; 4]).unwrap();
|
|
132
|
+
Some((client, server))
|
|
133
|
+
}
|
|
134
|
+
fn dark(source: &str, destination: &str, listener: &TcpListener) {
|
|
135
|
+
assert!(
|
|
136
|
+
dial(source, destination, listener).is_none(),
|
|
137
|
+
"{source} -> {destination}"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
143
|
+
fn v2_host_serves_local_platform_endpoints_only_to_exact_leased_untranslated_flows() {
|
|
144
|
+
isolated();
|
|
145
|
+
for name in [ROUTER, HOST, PEER] {
|
|
146
|
+
ip(&["netns", "add", name]);
|
|
147
|
+
ns_ip(name, &["link", "set", "lo", "up"]);
|
|
148
|
+
}
|
|
149
|
+
connect(
|
|
150
|
+
ROUTER,
|
|
151
|
+
"handoff",
|
|
152
|
+
"10.240.0.2/29",
|
|
153
|
+
HOST,
|
|
154
|
+
"router",
|
|
155
|
+
"10.240.0.1/29",
|
|
156
|
+
);
|
|
157
|
+
connect(
|
|
158
|
+
HOST,
|
|
159
|
+
"uplink",
|
|
160
|
+
"192.0.2.2/24",
|
|
161
|
+
PEER,
|
|
162
|
+
"underlay",
|
|
163
|
+
"192.0.2.3/24",
|
|
164
|
+
);
|
|
165
|
+
// A second, unleased source address on the router side of the handoff.
|
|
166
|
+
ns_ip(
|
|
167
|
+
ROUTER,
|
|
168
|
+
&["address", "add", "10.240.0.3/29", "dev", "handoff"],
|
|
169
|
+
);
|
|
170
|
+
ns_ip(
|
|
171
|
+
ROUTER,
|
|
172
|
+
&["route", "add", "192.0.2.0/24", "via", "10.240.0.1"],
|
|
173
|
+
);
|
|
174
|
+
in_namespace(HOST, foreign_translation);
|
|
175
|
+
let hub = in_namespace(HOST, || TcpListener::bind("192.0.2.2:8443").unwrap());
|
|
176
|
+
let other = in_namespace(HOST, || TcpListener::bind("192.0.2.2:8444").unwrap());
|
|
177
|
+
let store = udp(HOST, "10.240.0.1:7000".into());
|
|
178
|
+
let other_store = udp(HOST, "10.240.0.1:7001".into());
|
|
179
|
+
let transit = in_namespace(ROUTER, || TcpListener::bind("10.240.0.2:10031").unwrap());
|
|
180
|
+
// The host's own opening toward the transit address.
|
|
181
|
+
let host_dial = |expected: bool| {
|
|
182
|
+
let result = in_namespace(HOST, || {
|
|
183
|
+
TcpStream::connect_timeout(&"10.240.0.2:10031".parse().unwrap(), Duration::from_secs(1))
|
|
184
|
+
});
|
|
185
|
+
assert_eq!(result.is_ok(), expected);
|
|
186
|
+
if expected {
|
|
187
|
+
transit.accept().unwrap();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
// Every probe of one phase uses its own source ports: TCP ports that closed
|
|
191
|
+
// stay in TIME_WAIT and an unreplied UDP entry would carry a decision over.
|
|
192
|
+
// The leased ranges are UDP 10000–10015 and TCP 10016–10031.
|
|
193
|
+
let leased = |port: u16| format!("10.240.0.2:{port}");
|
|
194
|
+
let probes = |phase: u16, served: bool| {
|
|
195
|
+
let exact = dial(&leased(10016 + phase), "192.0.2.2:8443", &hub);
|
|
196
|
+
assert_eq!(exact.is_some(), served, "phase {phase}");
|
|
197
|
+
let client = udp(ROUTER, leased(10000 + phase));
|
|
198
|
+
if served {
|
|
199
|
+
let observed = roundtrip(&client, &store);
|
|
200
|
+
assert_eq!(observed.to_string(), leased(10000 + phase));
|
|
201
|
+
} else {
|
|
202
|
+
denied(&client, &store, false);
|
|
203
|
+
}
|
|
204
|
+
exact
|
|
205
|
+
};
|
|
206
|
+
// The rest never passes the member: another port, another protocol range,
|
|
207
|
+
// outside the lease, another source address, another endpoint port, and a
|
|
208
|
+
// flow whose destination was rewritten in transit.
|
|
209
|
+
let refused = |phase: u16| {
|
|
210
|
+
dark(&leased(20000 + phase), "192.0.2.2:8443", &hub);
|
|
211
|
+
dark(&leased(10005 + phase), "192.0.2.2:8443", &hub);
|
|
212
|
+
dark(
|
|
213
|
+
&format!("10.240.0.3:{}", 10021 + phase),
|
|
214
|
+
"192.0.2.2:8443",
|
|
215
|
+
&hub,
|
|
216
|
+
);
|
|
217
|
+
dark(&leased(10021 + phase), "192.0.2.2:8444", &other);
|
|
218
|
+
dark(&leased(10026 + phase), "10.240.0.1:9000", &hub);
|
|
219
|
+
denied(&udp(ROUTER, leased(10006 + phase)), &other_store, false);
|
|
220
|
+
denied(&udp(ROUTER, leased(10026 + phase)), &store, false);
|
|
221
|
+
host_dial(false);
|
|
222
|
+
};
|
|
223
|
+
// Positive controls without policy: every probe can succeed, including the
|
|
224
|
+
// translated flow and the host's own opening.
|
|
225
|
+
probes(0, true);
|
|
226
|
+
assert!(dial("10.240.0.2:20000", "192.0.2.2:8443", &hub).is_some());
|
|
227
|
+
assert!(dial("10.240.0.2:10005", "192.0.2.2:8443", &hub).is_some());
|
|
228
|
+
assert!(dial("10.240.0.3:10021", "192.0.2.2:8443", &hub).is_some());
|
|
229
|
+
assert!(dial("10.240.0.2:10021", "192.0.2.2:8444", &other).is_some());
|
|
230
|
+
assert!(dial("10.240.0.2:10026", "10.240.0.1:9000", &hub).is_some());
|
|
231
|
+
roundtrip(&udp(ROUTER, leased(10006)), &other_store);
|
|
232
|
+
roundtrip(&udp(ROUTER, leased(10026)), &store);
|
|
233
|
+
host_dial(true);
|
|
234
|
+
|
|
235
|
+
let mut transit_hop = Hop::new(HOST, "local_platform_transit");
|
|
236
|
+
// Without the member the handoff denials refuse the host's own addresses.
|
|
237
|
+
transit_hop.apply(transit_policy, 1, false);
|
|
238
|
+
assert!(probes(1, false).is_none());
|
|
239
|
+
refused(1);
|
|
240
|
+
// With it, exactly the leased flows reach the served endpoints.
|
|
241
|
+
transit_hop.apply(transit_policy, 2, true);
|
|
242
|
+
let (mut established, mut accepted) = probes(2, true).unwrap();
|
|
243
|
+
refused(2);
|
|
244
|
+
// Withdrawal stops new flows and the established one.
|
|
245
|
+
transit_hop.apply(transit_policy, 3, false);
|
|
246
|
+
assert!(probes(3, false).is_none());
|
|
247
|
+
established.write_all(b"late").unwrap();
|
|
248
|
+
assert!(accepted.read_exact(&mut [0; 4]).is_err());
|
|
249
|
+
drop((established, accepted));
|
|
250
|
+
transit_hop.release();
|
|
251
|
+
// Release control: the path is open again, so each denial was this table.
|
|
252
|
+
assert!(probes(4, true).is_some());
|
|
253
|
+
println!("V2_LOCAL_PLATFORM_PROOF positive_controls=true undeclared_dark=true exact_tcp=true exact_udp=true source_preserved=true outside_lease_denied=true other_protocol_range_denied=true other_source_denied=true other_port_denied=true translated_denied=true host_origin_denied=true withdrawn_dark=true established_withdrawn=true release_control=true");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// The complete graph with served endpoints persists across owner loss and
|
|
257
|
+
/// verifies before adoption, like every other transit graph.
|
|
258
|
+
#[test]
|
|
259
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
260
|
+
fn v2_host_local_platform_graph_persist_and_lost_ack_recovery() {
|
|
261
|
+
isolated();
|
|
262
|
+
// Peers live in another namespace, as the bound veth facts expect.
|
|
263
|
+
ip(&["netns", "add", PEER]);
|
|
264
|
+
for (name, address) in [("router", "10.240.0.1/30"), ("uplink", "192.0.2.2/24")] {
|
|
265
|
+
ip(&[
|
|
266
|
+
"link", "add", name, "type", "veth", "peer", "name", name, "netns", PEER,
|
|
267
|
+
]);
|
|
268
|
+
ip(&["address", "add", address, "dev", name]);
|
|
269
|
+
ip(&["link", "set", name, "up"]);
|
|
270
|
+
}
|
|
271
|
+
let mut policy = host_policy();
|
|
272
|
+
served(&mut policy, true);
|
|
273
|
+
super::egress_tests::recover("local_platform_v2", policy);
|
|
274
|
+
}
|