@push.rocks/smartnftables 2.1.0 → 2.3.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 +12 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +4 -4
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +4 -4
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.managednftables.d.ts +9 -1
- package/dist_ts/classes.managednftables.js +33 -9
- package/dist_ts/managed.egress.types.d.ts +16 -0
- package/dist_ts/managed.types.d.ts +13 -0
- package/package.json +3 -3
- package/readme.md +72 -1
- package/rust/src/egress.compile.rs +20 -0
- package/rust/src/egress.host.rs +73 -0
- package/rust/src/egress.rs +61 -0
- package/rust/src/egress_tests.rs +194 -1
- package/rust/src/main.rs +4 -0
- package/rust/src/owner.detach.rs +1 -1
- package/rust/src/owner.rs +18 -2
- package/rust/src/owner.transitionrelease.rs +107 -0
- package/rust/src/owner_host_traffic_tests.rs +224 -1
- package/rust/src/owner_identity_tests.rs +37 -0
- package/rust/src/owner_tests.rs +3 -0
- package/rust/src/owner_transitionrelease_tests.rs +327 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.managednftables.ts +33 -9
- package/ts/managed.egress.types.ts +17 -0
- package/ts/managed.types.ts +9 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
use super::egress_traffic_tests::{connect, denied, host_policy, ns_ip, roundtrip, socket};
|
|
2
2
|
use super::*;
|
|
3
|
+
use crate::egress;
|
|
3
4
|
use crate::policy::{compare, expr, meta};
|
|
4
|
-
use std::
|
|
5
|
+
use std::io::{Read, Write};
|
|
6
|
+
use std::net::{TcpListener, TcpStream, UdpSocket};
|
|
7
|
+
use std::time::Duration;
|
|
5
8
|
|
|
6
9
|
const SOURCE: &str = "hs_source";
|
|
7
10
|
const HOST: &str = "hs_host";
|
|
@@ -297,3 +300,223 @@ fn v2_host_rejects_foreign_dnat_zone_local_diversion_and_ipv6() {
|
|
|
297
300
|
});
|
|
298
301
|
println!("V2_HOST_PROOF original_protected_dnat_denied=true current_protected_dnat_denied=true nonzero_zone_denied=true input_diversion_denied=true output_spoof_denied=true ipv6_forward_denied=true unleased_port_denied=true exact_platform_allowed=true positive_controls=true");
|
|
299
302
|
}
|
|
303
|
+
|
|
304
|
+
const PUBLISH_SOURCE: &str = "hp_source";
|
|
305
|
+
const PUBLISH_HOST: &str = "hp_host";
|
|
306
|
+
const PUBLISH_PEER: &str = "hp_peer";
|
|
307
|
+
|
|
308
|
+
fn chain_names(owner: &mut Owner) -> Vec<String> {
|
|
309
|
+
owner
|
|
310
|
+
.graph()
|
|
311
|
+
.unwrap()
|
|
312
|
+
.expect("owned table present")
|
|
313
|
+
.chains
|
|
314
|
+
.iter()
|
|
315
|
+
.map(|chain| wire::text(chain, 3).unwrap())
|
|
316
|
+
.collect()
|
|
317
|
+
}
|
|
318
|
+
/// Inbound publication through the uplink: the workload must observe the actual
|
|
319
|
+
/// client address, and the client must observe the published address back.
|
|
320
|
+
fn published_udp(client: &UdpSocket, workload: &UdpSocket, published: &str, client_address: &str) {
|
|
321
|
+
let mut bytes = [0; 64];
|
|
322
|
+
assert_eq!(client.send_to(b"query", published).unwrap(), 5);
|
|
323
|
+
let (size, observed) = workload.recv_from(&mut bytes).expect("published delivery");
|
|
324
|
+
assert_eq!(&bytes[..size], b"query");
|
|
325
|
+
assert_eq!(observed.to_string(), client_address);
|
|
326
|
+
assert_eq!(workload.send_to(b"reply", observed).unwrap(), 5);
|
|
327
|
+
let (size, from) = client.recv_from(&mut bytes).expect("published reply");
|
|
328
|
+
assert_eq!(&bytes[..size], b"reply");
|
|
329
|
+
assert_eq!(from.to_string(), published);
|
|
330
|
+
}
|
|
331
|
+
fn dark_udp(client: &UdpSocket, workload: &UdpSocket, published: &str) {
|
|
332
|
+
assert_eq!(client.send_to(b"query", published).unwrap(), 5);
|
|
333
|
+
assert!(workload.recv(&mut [0; 64]).is_err());
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#[test]
|
|
337
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
338
|
+
fn v2_host_publishes_exact_uplink_ports_and_removes_them_with_the_generation() {
|
|
339
|
+
isolated();
|
|
340
|
+
for name in [PUBLISH_SOURCE, PUBLISH_HOST, PUBLISH_PEER] {
|
|
341
|
+
ip(&["netns", "add", name]);
|
|
342
|
+
ns_ip(name, &["link", "set", "lo", "up"]);
|
|
343
|
+
in_namespace(name, || {
|
|
344
|
+
std::fs::write("/proc/sys/net/ipv4/ip_forward", "1").unwrap()
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
connect(
|
|
348
|
+
PUBLISH_SOURCE,
|
|
349
|
+
"source",
|
|
350
|
+
"10.240.0.2/30",
|
|
351
|
+
PUBLISH_HOST,
|
|
352
|
+
"router",
|
|
353
|
+
"10.240.0.1/30",
|
|
354
|
+
);
|
|
355
|
+
connect(
|
|
356
|
+
PUBLISH_HOST,
|
|
357
|
+
"uplink",
|
|
358
|
+
"192.0.2.2/24",
|
|
359
|
+
PUBLISH_PEER,
|
|
360
|
+
"underlay",
|
|
361
|
+
"192.0.2.3/24",
|
|
362
|
+
);
|
|
363
|
+
ns_ip(PUBLISH_SOURCE, &["route", "add", "default", "via", "10.240.0.1"]);
|
|
364
|
+
ns_ip(PUBLISH_PEER, &["route", "add", "default", "via", "192.0.2.2"]);
|
|
365
|
+
let workload_udp = socket(PUBLISH_SOURCE, "10.240.0.2:8125");
|
|
366
|
+
let workload_direct = socket(PUBLISH_SOURCE, "10.240.0.2:8444");
|
|
367
|
+
let workload_unpublished = socket(PUBLISH_SOURCE, "10.240.0.2:9999");
|
|
368
|
+
// Every probe uses its own client port: an unreplied conntrack entry would
|
|
369
|
+
// otherwise carry its null translation across the policy change.
|
|
370
|
+
let control_client = socket(PUBLISH_PEER, "192.0.2.3:40010");
|
|
371
|
+
let client_udp = socket(PUBLISH_PEER, "192.0.2.3:40000");
|
|
372
|
+
// Positive controls: without policy the workload is reachable directly, and
|
|
373
|
+
// the published address is not, so every later result is caused by the policy.
|
|
374
|
+
control(&control_client, "10.240.0.2:8444", &workload_direct, true);
|
|
375
|
+
dark_udp(
|
|
376
|
+
&socket(PUBLISH_PEER, "192.0.2.3:40011"),
|
|
377
|
+
&workload_udp,
|
|
378
|
+
"192.0.2.2:8125",
|
|
379
|
+
);
|
|
380
|
+
let (owner, applied, published_policy, names) = in_namespace(PUBLISH_HOST, || {
|
|
381
|
+
let mut policy = host_policy();
|
|
382
|
+
let egress::Scope::HostTransit(scope) = &mut policy.scope else {
|
|
383
|
+
panic!()
|
|
384
|
+
};
|
|
385
|
+
scope.published_ports = vec![
|
|
386
|
+
egress::PublishedPort {
|
|
387
|
+
protocol: "tcp".into(),
|
|
388
|
+
host_port: 443,
|
|
389
|
+
target_port: 8443,
|
|
390
|
+
target_address: "10.240.0.2".into(),
|
|
391
|
+
host_ip: "192.0.2.2".into(),
|
|
392
|
+
},
|
|
393
|
+
egress::PublishedPort {
|
|
394
|
+
protocol: "udp".into(),
|
|
395
|
+
host_port: 8125,
|
|
396
|
+
target_port: 8125,
|
|
397
|
+
target_address: "10.240.0.2".into(),
|
|
398
|
+
host_ip: "192.0.2.2".into(),
|
|
399
|
+
},
|
|
400
|
+
];
|
|
401
|
+
let mut owner = Owner::new(options("host_published")).unwrap();
|
|
402
|
+
let target: Prepared = policy.clone().prepare().unwrap().into();
|
|
403
|
+
target
|
|
404
|
+
.validate_interfaces()
|
|
405
|
+
.expect("published host fixture binding");
|
|
406
|
+
let applied = owner
|
|
407
|
+
.reconcile(Transition {
|
|
408
|
+
previous: None,
|
|
409
|
+
target,
|
|
410
|
+
})
|
|
411
|
+
.unwrap();
|
|
412
|
+
assert!(owner.inspect().enforced);
|
|
413
|
+
let names = chain_names(&mut owner);
|
|
414
|
+
(owner, applied, policy, names)
|
|
415
|
+
});
|
|
416
|
+
assert!(names.contains(&"pre".to_string()));
|
|
417
|
+
// The fixture observes ingress only, so the translated opening is read where
|
|
418
|
+
// it arrives: the workload side of the handoff.
|
|
419
|
+
let observer = in_namespace(PUBLISH_SOURCE, || packet_fixture::PacketSocket::open("source"));
|
|
420
|
+
published_udp(&client_udp, &workload_udp, "192.0.2.2:8125", "192.0.2.3:40000");
|
|
421
|
+
let listener = in_namespace(PUBLISH_SOURCE, || {
|
|
422
|
+
TcpListener::bind("10.240.0.2:8443").unwrap()
|
|
423
|
+
});
|
|
424
|
+
let mut client = in_namespace(PUBLISH_PEER, || {
|
|
425
|
+
TcpStream::connect_timeout(&"192.0.2.2:443".parse().unwrap(), Duration::from_secs(1))
|
|
426
|
+
.expect("published tcp opening")
|
|
427
|
+
});
|
|
428
|
+
let (mut server, peer) = listener.accept().unwrap();
|
|
429
|
+
assert_eq!(peer.ip().to_string(), "192.0.2.3");
|
|
430
|
+
assert_eq!(client.peer_addr().unwrap().to_string(), "192.0.2.2:443");
|
|
431
|
+
for stream in [&client, &server] {
|
|
432
|
+
stream.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
|
|
433
|
+
}
|
|
434
|
+
client.write_all(b"ping").unwrap();
|
|
435
|
+
server.read_exact(&mut [0; 4]).unwrap();
|
|
436
|
+
server.write_all(b"pong").unwrap();
|
|
437
|
+
client.read_exact(&mut [0; 4]).unwrap();
|
|
438
|
+
// The translated opening is observable on the handoff, with the actual client
|
|
439
|
+
// address preserved and SYN only.
|
|
440
|
+
let syn = observer
|
|
441
|
+
.matching(|packet| {
|
|
442
|
+
packet.len() >= 40
|
|
443
|
+
&& packet[9] == 6
|
|
444
|
+
&& packet[16..20] == [10, 240, 0, 2]
|
|
445
|
+
&& packet[22..24] == 8443_u16.to_be_bytes()
|
|
446
|
+
&& packet[33] & 0x17 == 0x02
|
|
447
|
+
})
|
|
448
|
+
.expect("translated opening on the handoff");
|
|
449
|
+
assert_eq!(syn[12..16], [192, 0, 2, 3]);
|
|
450
|
+
// An opening that is not SYN cannot acquire the publication.
|
|
451
|
+
let injector = in_namespace(PUBLISH_PEER, || {
|
|
452
|
+
packet_fixture::PacketSocket::open("underlay")
|
|
453
|
+
});
|
|
454
|
+
let uplink_mac = in_namespace(PUBLISH_HOST, || packet_fixture::mac("uplink"));
|
|
455
|
+
for (port, flags) in [(40050_u16, 0x10_u8), (40051, 0x04), (40052, 0x12)] {
|
|
456
|
+
injector.send(
|
|
457
|
+
uplink_mac,
|
|
458
|
+
&packet_fixture::tcp([192, 0, 2, 3], [192, 0, 2, 2], port, 443, flags),
|
|
459
|
+
);
|
|
460
|
+
// The exact injected flow, never the live connection's own acknowledgements.
|
|
461
|
+
assert!(
|
|
462
|
+
observer
|
|
463
|
+
.matching(|packet| packet.len() >= 40
|
|
464
|
+
&& packet[9] == 6
|
|
465
|
+
&& packet[16..20] == [10, 240, 0, 2]
|
|
466
|
+
&& packet[20..22] == port.to_be_bytes()
|
|
467
|
+
&& packet[22..24] == 8443_u16.to_be_bytes()
|
|
468
|
+
&& packet[24..28] == 100_u32.to_be_bytes()
|
|
469
|
+
&& packet[33] == flags)
|
|
470
|
+
.is_none(),
|
|
471
|
+
"{flags:#04x}"
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
// The capture barrier still denies every unpublished path to the workload.
|
|
475
|
+
control(&control_client, "10.240.0.2:8444", &workload_direct, false);
|
|
476
|
+
let unpublished_client = socket(PUBLISH_PEER, "192.0.2.3:40002");
|
|
477
|
+
dark_udp(&unpublished_client, &workload_unpublished, "192.0.2.2:9999");
|
|
478
|
+
assert!(in_namespace(PUBLISH_PEER, || TcpStream::connect_timeout(
|
|
479
|
+
&"192.0.2.2:444".parse().unwrap(),
|
|
480
|
+
Duration::from_secs(1)
|
|
481
|
+
))
|
|
482
|
+
.is_err());
|
|
483
|
+
drop(client);
|
|
484
|
+
drop(server);
|
|
485
|
+
// Withdrawing the publication is an ordinary generation replacement.
|
|
486
|
+
let (mut owner, rolled_back, names) = in_namespace(PUBLISH_HOST, {
|
|
487
|
+
let mut owner = owner;
|
|
488
|
+
move || {
|
|
489
|
+
let mut withdrawn = published_policy;
|
|
490
|
+
withdrawn.revision = 2;
|
|
491
|
+
let egress::Scope::HostTransit(scope) = &mut withdrawn.scope else {
|
|
492
|
+
panic!()
|
|
493
|
+
};
|
|
494
|
+
scope.published_ports.clear();
|
|
495
|
+
let applied = owner
|
|
496
|
+
.reconcile(Transition {
|
|
497
|
+
previous: Some(applied),
|
|
498
|
+
target: withdrawn.prepare().unwrap().into(),
|
|
499
|
+
})
|
|
500
|
+
.unwrap();
|
|
501
|
+
assert!(owner.inspect().enforced);
|
|
502
|
+
let names = chain_names(&mut owner);
|
|
503
|
+
(owner, applied, names)
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
assert!(!names.contains(&"pre".to_string()));
|
|
507
|
+
let dark_client = socket(PUBLISH_PEER, "192.0.2.3:40003");
|
|
508
|
+
dark_udp(&dark_client, &workload_udp, "192.0.2.2:8125");
|
|
509
|
+
assert!(in_namespace(PUBLISH_PEER, || TcpStream::connect_timeout(
|
|
510
|
+
&"192.0.2.2:443".parse().unwrap(),
|
|
511
|
+
Duration::from_secs(1)
|
|
512
|
+
))
|
|
513
|
+
.is_err());
|
|
514
|
+
control(&control_client, "10.240.0.2:8444", &workload_direct, false);
|
|
515
|
+
in_namespace(PUBLISH_HOST, move || {
|
|
516
|
+
assert!(owner.inspect().enforced);
|
|
517
|
+
owner.release(rolled_back).unwrap();
|
|
518
|
+
});
|
|
519
|
+
let released_client = socket(PUBLISH_PEER, "192.0.2.3:40004");
|
|
520
|
+
dark_udp(&released_client, &workload_udp, "192.0.2.2:8125");
|
|
521
|
+
println!("V2_HOST_PUBLISHED_PROOF udp_published_delivered=true tcp_published_delivered=true client_address_preserved=true reverse_translation=true syn_only_opening=true nonsyn_opening_denied=true unpublished_workload_port_denied=true unpublished_uplink_port_denied=true barrier_positive_control=true withdrawn_chain_absent=true withdrawn_port_dark=true released_port_dark=true");
|
|
522
|
+
}
|
|
@@ -167,3 +167,40 @@ fn egress_identity_rejects_unknown_duplicate_or_incomplete_operands() {
|
|
|
167
167
|
assert!(rule_identity(&rule(vec![expression(name, extra)])).is_err());
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
+
|
|
171
|
+
/// A published host-transit generation must survive the same apply-time graph
|
|
172
|
+
/// identity as the rest of the table: the kernel dump is compared operand by
|
|
173
|
+
/// operand, so destination NAT and its admissions cannot be silently rewritten.
|
|
174
|
+
#[test]
|
|
175
|
+
fn published_host_graph_identity_preserves_destination_nat_and_admission_operands() {
|
|
176
|
+
let compiled = |target_port: u16| {
|
|
177
|
+
let value = crate::egress::tests::published_host(serde_json::json!([
|
|
178
|
+
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)
|
|
194
|
+
};
|
|
195
|
+
let (chains, rules) = compiled(8443);
|
|
196
|
+
assert_eq!(rules.iter().filter(|(chain, _)| chain == "pre").count(), 1);
|
|
197
|
+
assert_eq!(chains.len(), 7);
|
|
198
|
+
let translation = rules.iter().find(|(chain, _)| chain == "pre").unwrap();
|
|
199
|
+
let (_, changed) = compiled(9443);
|
|
200
|
+
let other = changed.iter().find(|(chain, _)| chain == "pre").unwrap();
|
|
201
|
+
assert_ne!(translation.1, other.1);
|
|
202
|
+
assert_eq!(
|
|
203
|
+
rules.iter().filter(|(chain, _)| chain == "forward").count(),
|
|
204
|
+
changed.iter().filter(|(chain, _)| chain == "forward").count()
|
|
205
|
+
);
|
|
206
|
+
}
|
package/rust/src/owner_tests.rs
CHANGED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
use super::namespace_tests::NativeOwner;
|
|
2
|
+
use super::*;
|
|
3
|
+
use serde_json::json;
|
|
4
|
+
use std::io::Write;
|
|
5
|
+
use std::time::{Duration, Instant};
|
|
6
|
+
|
|
7
|
+
fn target(revision: u64) -> Prepared {
|
|
8
|
+
let mut policy = crate::egress::tests::pool_guard();
|
|
9
|
+
policy["revision"] = json!(revision);
|
|
10
|
+
serde_json::from_value::<crate::egress::Policy>(policy)
|
|
11
|
+
.unwrap()
|
|
12
|
+
.prepare()
|
|
13
|
+
.unwrap()
|
|
14
|
+
.into()
|
|
15
|
+
}
|
|
16
|
+
fn request(owner: &Owner, transition: Transition) -> TransitionRelease {
|
|
17
|
+
TransitionRelease {
|
|
18
|
+
identity: owner.identity.clone(),
|
|
19
|
+
transition,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[test]
|
|
24
|
+
fn transition_release_validates_closed_identity_and_body_before_admission() {
|
|
25
|
+
let mut owner = Owner::new(options("release_input")).unwrap();
|
|
26
|
+
let command = request(
|
|
27
|
+
&owner,
|
|
28
|
+
Transition {
|
|
29
|
+
previous: None,
|
|
30
|
+
target: target(1),
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
for field in ["bootId", "namespaceDevice", "namespaceInode", "instanceId"] {
|
|
34
|
+
let mut invalid = serde_json::to_value(&command).unwrap();
|
|
35
|
+
invalid["identity"][field] = json!("invalid");
|
|
36
|
+
assert_eq!(
|
|
37
|
+
owner.release_transition(serde_json::from_value(invalid).unwrap()),
|
|
38
|
+
Err(Error::Conflict)
|
|
39
|
+
);
|
|
40
|
+
assert!(owner.transition_release.is_none());
|
|
41
|
+
}
|
|
42
|
+
let mut invalid = command.clone();
|
|
43
|
+
invalid.transition.target.digest = format!("sha256:{}", "0".repeat(64));
|
|
44
|
+
assert!(owner.release_transition(invalid).is_err());
|
|
45
|
+
assert!(owner.transition_release.is_none());
|
|
46
|
+
for pointer in ["", "/identity", "/transition", "/transition/target"] {
|
|
47
|
+
let mut invalid = serde_json::to_value(&command).unwrap();
|
|
48
|
+
invalid.pointer_mut(pointer).unwrap()["extra"] = json!(true);
|
|
49
|
+
assert!(serde_json::from_value::<TransitionRelease>(invalid).is_err());
|
|
50
|
+
}
|
|
51
|
+
let mut invalid = serde_json::to_value(&command).unwrap();
|
|
52
|
+
invalid["transition"]
|
|
53
|
+
.as_object_mut()
|
|
54
|
+
.unwrap()
|
|
55
|
+
.remove("previous");
|
|
56
|
+
assert!(serde_json::from_value::<TransitionRelease>(invalid).is_err());
|
|
57
|
+
assert!(owner.socket.is_none());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[test]
|
|
61
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
62
|
+
fn release_original_transition_after_interfaces_disappear_never_recreates_policy() {
|
|
63
|
+
isolated();
|
|
64
|
+
for replacement in [false, true] {
|
|
65
|
+
for delivered in [false, true] {
|
|
66
|
+
let endpoint = endpoint("a", 72);
|
|
67
|
+
let policy = crate::policy::Policy {
|
|
68
|
+
schema_version: 1,
|
|
69
|
+
revision: 1,
|
|
70
|
+
endpoints: vec![endpoint],
|
|
71
|
+
rules: vec![],
|
|
72
|
+
};
|
|
73
|
+
let mut owner = Owner::new(options("release_links")).unwrap();
|
|
74
|
+
let previous = replacement.then(|| {
|
|
75
|
+
owner
|
|
76
|
+
.reconcile(Transition {
|
|
77
|
+
previous: None,
|
|
78
|
+
target: policy.clone().prepare().unwrap().into(),
|
|
79
|
+
})
|
|
80
|
+
.unwrap()
|
|
81
|
+
});
|
|
82
|
+
let mut next = policy;
|
|
83
|
+
if replacement {
|
|
84
|
+
next.revision = 2;
|
|
85
|
+
}
|
|
86
|
+
let command = request(
|
|
87
|
+
&owner,
|
|
88
|
+
Transition {
|
|
89
|
+
previous,
|
|
90
|
+
target: next.prepare().unwrap().into(),
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
if delivered {
|
|
94
|
+
owner.reconcile(command.transition.clone()).unwrap();
|
|
95
|
+
}
|
|
96
|
+
// Only the original command crosses this process boundary.
|
|
97
|
+
drop(owner);
|
|
98
|
+
ip(&["link", "del", "worka"]);
|
|
99
|
+
ip(&["netns", "del", "a"]);
|
|
100
|
+
let mut fresh = Owner::new(options("release_links")).unwrap();
|
|
101
|
+
assert!(fresh.reconcile(command.transition.clone()).is_err());
|
|
102
|
+
if replacement && delivered {
|
|
103
|
+
// Model a retained native pending command after an ambiguous batch.
|
|
104
|
+
fresh.pending = Some(command.transition.clone());
|
|
105
|
+
}
|
|
106
|
+
fresh.release_transition(command.clone()).unwrap();
|
|
107
|
+
let status = fresh.inspect();
|
|
108
|
+
assert_eq!(status.state, "released");
|
|
109
|
+
assert!(!status.enforced);
|
|
110
|
+
assert!(fresh.graph().unwrap().is_none());
|
|
111
|
+
fresh.release_transition(command.clone()).unwrap();
|
|
112
|
+
assert_eq!(
|
|
113
|
+
fresh.reconcile(command.transition.clone()),
|
|
114
|
+
Err(Error::Conflict)
|
|
115
|
+
);
|
|
116
|
+
let mut different = command;
|
|
117
|
+
different.transition.target = target(3);
|
|
118
|
+
assert_eq!(fresh.release_transition(different), Err(Error::Conflict));
|
|
119
|
+
fresh.close().unwrap();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[test]
|
|
125
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
126
|
+
fn release_transition_rejects_foreign_owner_and_keeps_exact_terminal_choice() {
|
|
127
|
+
isolated();
|
|
128
|
+
let mut active = Owner::new(options("release_foreign")).unwrap();
|
|
129
|
+
let command = request(
|
|
130
|
+
&active,
|
|
131
|
+
Transition {
|
|
132
|
+
previous: None,
|
|
133
|
+
target: target(1),
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
let applied = active.reconcile(command.transition.clone()).unwrap();
|
|
137
|
+
let mut fresh = Owner::new(options("release_foreign")).unwrap();
|
|
138
|
+
assert_eq!(
|
|
139
|
+
fresh.release_transition(command.clone()),
|
|
140
|
+
Err(Error::ForeignOwner)
|
|
141
|
+
);
|
|
142
|
+
assert_eq!(fresh.inspect().state, "failed-owned");
|
|
143
|
+
assert!(!fresh.inspect().enforced);
|
|
144
|
+
assert_eq!(
|
|
145
|
+
fresh.reconcile(command.transition.clone()),
|
|
146
|
+
Err(Error::Conflict)
|
|
147
|
+
);
|
|
148
|
+
assert_eq!(fresh.detach(applied.clone()), Err(Error::Conflict));
|
|
149
|
+
assert_eq!(fresh.release(applied), Err(Error::Conflict));
|
|
150
|
+
assert!(active.inspect().enforced);
|
|
151
|
+
drop(active);
|
|
152
|
+
// close may retry only the already-selected exact release operation.
|
|
153
|
+
fresh.close().unwrap();
|
|
154
|
+
assert!(fresh.graph().unwrap().is_none());
|
|
155
|
+
fresh.release_transition(command).unwrap();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[test]
|
|
159
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
160
|
+
fn release_transition_binds_previous_handle_and_complete_graph() {
|
|
161
|
+
isolated();
|
|
162
|
+
for fault in ["handle", "body", "pending"] {
|
|
163
|
+
let mut active = Owner::new(options("release_binding")).unwrap();
|
|
164
|
+
let previous = active
|
|
165
|
+
.reconcile(Transition {
|
|
166
|
+
previous: None,
|
|
167
|
+
target: target(1),
|
|
168
|
+
})
|
|
169
|
+
.unwrap();
|
|
170
|
+
let command = request(
|
|
171
|
+
&active,
|
|
172
|
+
Transition {
|
|
173
|
+
previous: Some(previous.clone()),
|
|
174
|
+
target: target(2),
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
drop(active);
|
|
178
|
+
let mut fresh = Owner::new(options("release_binding")).unwrap();
|
|
179
|
+
let mut invalid = command.clone();
|
|
180
|
+
if fault == "handle" {
|
|
181
|
+
invalid
|
|
182
|
+
.transition
|
|
183
|
+
.previous
|
|
184
|
+
.as_mut()
|
|
185
|
+
.unwrap()
|
|
186
|
+
.receipt
|
|
187
|
+
.table_handle = "18446744073709551615".into();
|
|
188
|
+
} else if fault == "pending" {
|
|
189
|
+
fresh.pending = Some(Transition {
|
|
190
|
+
previous: Some(previous.clone()),
|
|
191
|
+
target: target(3),
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
let mut foreign = Socket::open().unwrap();
|
|
195
|
+
let generation = foreign.generation().unwrap();
|
|
196
|
+
foreign
|
|
197
|
+
.batch(
|
|
198
|
+
generation,
|
|
199
|
+
vec![(
|
|
200
|
+
3,
|
|
201
|
+
0x600,
|
|
202
|
+
vec![
|
|
203
|
+
Attr::string(1, "snft_release_binding"),
|
|
204
|
+
Attr::string(3, "foreign"),
|
|
205
|
+
],
|
|
206
|
+
)],
|
|
207
|
+
)
|
|
208
|
+
.unwrap();
|
|
209
|
+
}
|
|
210
|
+
assert_eq!(fresh.release_transition(invalid), Err(Error::Conflict));
|
|
211
|
+
assert!(fresh.graph().unwrap().is_some());
|
|
212
|
+
assert!(!fresh.inspect().enforced);
|
|
213
|
+
drop(fresh);
|
|
214
|
+
// Disposable fixture cleanup never enters the production API.
|
|
215
|
+
let mut cleanup = Socket::open().unwrap();
|
|
216
|
+
let generation = cleanup.generation().unwrap();
|
|
217
|
+
cleanup
|
|
218
|
+
.batch(
|
|
219
|
+
generation,
|
|
220
|
+
vec![(2, 0, vec![Attr::string(1, "snft_release_binding")])],
|
|
221
|
+
)
|
|
222
|
+
.unwrap();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
228
|
+
fn release_transition_retry_never_deletes_a_replacement_for_its_observed_handle() {
|
|
229
|
+
isolated();
|
|
230
|
+
let mut owner = Owner::new(options("release_replaced")).unwrap();
|
|
231
|
+
let command = request(
|
|
232
|
+
&owner,
|
|
233
|
+
Transition {
|
|
234
|
+
previous: None,
|
|
235
|
+
target: target(1),
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
let first = owner.reconcile(command.transition.clone()).unwrap();
|
|
239
|
+
// Stop at the exact production boundary after deletion, before its final
|
|
240
|
+
// observation. Retained state must still bind the handle already acquired.
|
|
241
|
+
owner.transition_release = Some(command.clone());
|
|
242
|
+
let graph = owner.graph().unwrap().unwrap();
|
|
243
|
+
owner
|
|
244
|
+
.socket()
|
|
245
|
+
.unwrap()
|
|
246
|
+
.batch(
|
|
247
|
+
graph.generation,
|
|
248
|
+
vec![(
|
|
249
|
+
2,
|
|
250
|
+
0,
|
|
251
|
+
vec![Attr::u64(4, wire::handle(&graph.table, 4).unwrap())],
|
|
252
|
+
)],
|
|
253
|
+
)
|
|
254
|
+
.unwrap();
|
|
255
|
+
let mut replacement = Owner::new(options("release_replaced")).unwrap();
|
|
256
|
+
let second = replacement.reconcile(command.transition.clone()).unwrap();
|
|
257
|
+
assert_ne!(first.receipt.table_handle, second.receipt.table_handle);
|
|
258
|
+
drop(replacement);
|
|
259
|
+
assert_eq!(owner.release_transition(command), Err(Error::Conflict));
|
|
260
|
+
let graph = owner.graph().unwrap().unwrap();
|
|
261
|
+
assert_eq!(
|
|
262
|
+
wire::handle(&graph.table, 4).unwrap().to_string(),
|
|
263
|
+
second.receipt.table_handle
|
|
264
|
+
);
|
|
265
|
+
assert_eq!(owner.applied, Some(first));
|
|
266
|
+
assert!(!owner.inspect().enforced);
|
|
267
|
+
drop(owner);
|
|
268
|
+
Owner::new(options("release_replaced"))
|
|
269
|
+
.unwrap()
|
|
270
|
+
.release(second)
|
|
271
|
+
.unwrap();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#[test]
|
|
275
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
276
|
+
fn release_transition_recovers_lost_delete_ack_across_actual_native_processes() {
|
|
277
|
+
isolated();
|
|
278
|
+
let namespace = std::fs::File::open("/proc/thread-self/ns/net").unwrap();
|
|
279
|
+
let mut native = NativeOwner::start(&namespace);
|
|
280
|
+
let status = native.call(
|
|
281
|
+
"openOwner",
|
|
282
|
+
serde_json::to_value(options("release_ack")).unwrap(),
|
|
283
|
+
);
|
|
284
|
+
let command = TransitionRelease {
|
|
285
|
+
identity: serde_json::from_value(status["identity"].clone()).unwrap(),
|
|
286
|
+
transition: Transition {
|
|
287
|
+
previous: None,
|
|
288
|
+
target: target(1),
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
native.call(
|
|
292
|
+
"reconcilePolicy",
|
|
293
|
+
serde_json::to_value(&command.transition).unwrap(),
|
|
294
|
+
);
|
|
295
|
+
writeln!(
|
|
296
|
+
native.child.stdin.as_mut().unwrap(),
|
|
297
|
+
"{}",
|
|
298
|
+
json!({
|
|
299
|
+
"id":"lost-release", "method":"releasePolicyTransition", "params":command,
|
|
300
|
+
})
|
|
301
|
+
)
|
|
302
|
+
.unwrap();
|
|
303
|
+
let mut observer = Owner::new(options("release_ack")).unwrap();
|
|
304
|
+
let until = Instant::now() + Duration::from_secs(3);
|
|
305
|
+
while observer.graph().unwrap().is_some() {
|
|
306
|
+
assert!(Instant::now() < until);
|
|
307
|
+
std::thread::sleep(Duration::from_millis(5));
|
|
308
|
+
}
|
|
309
|
+
// Never read or retain the release response.
|
|
310
|
+
drop(native);
|
|
311
|
+
let mut fresh = NativeOwner::start(&namespace);
|
|
312
|
+
fresh.call(
|
|
313
|
+
"openOwner",
|
|
314
|
+
serde_json::to_value(options("release_ack")).unwrap(),
|
|
315
|
+
);
|
|
316
|
+
assert_eq!(
|
|
317
|
+
fresh.call(
|
|
318
|
+
"releasePolicyTransition",
|
|
319
|
+
serde_json::to_value(&command).unwrap()
|
|
320
|
+
),
|
|
321
|
+
json!({"released":true})
|
|
322
|
+
);
|
|
323
|
+
let status = fresh.call("inspectPolicy", json!({}));
|
|
324
|
+
assert_eq!(status["state"], "released");
|
|
325
|
+
assert_eq!(status["enforced"], false);
|
|
326
|
+
assert!(observer.graph().unwrap().is_none());
|
|
327
|
+
}
|
package/ts/00_commitinfo_data.ts
CHANGED