@push.rocks/smartnftables 1.2.1 → 1.4.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.
Files changed (88) hide show
  1. package/changelog.md +37 -0
  2. package/dist_rust/smartnftables_linux_amd64_musl +0 -0
  3. package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +14 -0
  4. package/dist_rust/smartnftables_linux_arm64_musl +0 -0
  5. package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +14 -0
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/classes.managednftables.d.ts +21 -0
  8. package/dist_ts/classes.managednftables.js +182 -0
  9. package/dist_ts/index.d.ts +2 -0
  10. package/dist_ts/index.js +2 -1
  11. package/dist_ts/managed.types.d.ts +103 -0
  12. package/dist_ts/managed.types.js +2 -0
  13. package/dist_ts/plugins.d.ts +5 -2
  14. package/dist_ts/plugins.js +6 -3
  15. package/native-notices/crates/block-buffer-0.10.4/license-apache +201 -0
  16. package/native-notices/crates/block-buffer-0.10.4/license-mit +25 -0
  17. package/native-notices/crates/cfg-if-1.0.4/license-apache +201 -0
  18. package/native-notices/crates/cfg-if-1.0.4/license-mit +25 -0
  19. package/native-notices/crates/cpufeatures-0.2.17/license-apache +201 -0
  20. package/native-notices/crates/cpufeatures-0.2.17/license-mit +25 -0
  21. package/native-notices/crates/crypto-common-0.1.7/license-apache +201 -0
  22. package/native-notices/crates/crypto-common-0.1.7/license-mit +25 -0
  23. package/native-notices/crates/digest-0.10.7/license-apache +201 -0
  24. package/native-notices/crates/digest-0.10.7/license-mit +25 -0
  25. package/native-notices/crates/generic-array-0.14.7/license +21 -0
  26. package/native-notices/crates/itoa-1.0.18/license-apache +176 -0
  27. package/native-notices/crates/itoa-1.0.18/license-mit +23 -0
  28. package/native-notices/crates/libc-0.2.189/license-apache +176 -0
  29. package/native-notices/crates/libc-0.2.189/license-mit +25 -0
  30. package/native-notices/crates/memchr-2.8.3/copying +3 -0
  31. package/native-notices/crates/memchr-2.8.3/license-mit +21 -0
  32. package/native-notices/crates/proc-macro2-1.0.107/license-apache +176 -0
  33. package/native-notices/crates/proc-macro2-1.0.107/license-mit +23 -0
  34. package/native-notices/crates/quote-1.0.47/license-apache +176 -0
  35. package/native-notices/crates/quote-1.0.47/license-mit +23 -0
  36. package/native-notices/crates/serde-1.0.229/license-apache +176 -0
  37. package/native-notices/crates/serde-1.0.229/license-mit +23 -0
  38. package/native-notices/crates/serde_core-1.0.229/license-apache +176 -0
  39. package/native-notices/crates/serde_core-1.0.229/license-mit +23 -0
  40. package/native-notices/crates/serde_derive-1.0.229/license-apache +176 -0
  41. package/native-notices/crates/serde_derive-1.0.229/license-mit +23 -0
  42. package/native-notices/crates/serde_json-1.0.151/license-apache +176 -0
  43. package/native-notices/crates/serde_json-1.0.151/license-mit +23 -0
  44. package/native-notices/crates/sha2-0.10.9/license-apache +201 -0
  45. package/native-notices/crates/sha2-0.10.9/license-mit +27 -0
  46. package/native-notices/crates/syn-3.0.5/license-apache +176 -0
  47. package/native-notices/crates/syn-3.0.5/license-mit +23 -0
  48. package/native-notices/crates/typenum-1.20.1/license +1 -0
  49. package/native-notices/crates/typenum-1.20.1/license-apache +201 -0
  50. package/native-notices/crates/typenum-1.20.1/license-mit +21 -0
  51. package/native-notices/crates/unicode-ident-1.0.24/license-apache +176 -0
  52. package/native-notices/crates/unicode-ident-1.0.24/license-mit +23 -0
  53. package/native-notices/crates/unicode-ident-1.0.24/license-unicode +39 -0
  54. package/native-notices/crates/version_check-0.9.5/license-apache +201 -0
  55. package/native-notices/crates/version_check-0.9.5/license-mit +19 -0
  56. package/native-notices/crates/zmij-1.0.23/license-mit +23 -0
  57. package/native-notices/manifest.json +520 -0
  58. package/native-notices/readme.md +36 -0
  59. package/native-notices/rust-standard-library/addr2line-0.25.1-license-apache +201 -0
  60. package/native-notices/rust-standard-library/addr2line-0.25.1-license-mit +25 -0
  61. package/native-notices/rust-standard-library/cargo.lock +522 -0
  62. package/native-notices/rust-standard-library/compiler-builtins-license.txt +275 -0
  63. package/native-notices/rust-standard-library/dependency-notices.html +9072 -0
  64. package/native-notices/rust-standard-library/license-apache +176 -0
  65. package/native-notices/rust-standard-library/license-mit +25 -0
  66. package/native-notices/rust-standard-library/llvm-libunwind-license.txt +311 -0
  67. package/native-notices/rust-standard-library/musl-1.2.5-copyright.txt +193 -0
  68. package/native-notices/rust-standard-library/unicode-license.txt +39 -0
  69. package/package.json +23 -8
  70. package/readme.md +160 -2
  71. package/rust/.cargo/config.toml +2 -0
  72. package/rust/Cargo.lock +192 -0
  73. package/rust/Cargo.toml +13 -0
  74. package/rust/src/main.rs +159 -0
  75. package/rust/src/namespace.rs +51 -0
  76. package/rust/src/owner.rs +604 -0
  77. package/rust/src/owner_namespace_tests.rs +242 -0
  78. package/rust/src/owner_tests.rs +798 -0
  79. package/rust/src/policy.rs +427 -0
  80. package/rust/src/tests.rs +97 -0
  81. package/rust/src/wire.rs +437 -0
  82. package/scripts/check-native-notices.mjs +27 -0
  83. package/ts/00_commitinfo_data.ts +1 -1
  84. package/ts/classes.managednftables.ts +153 -0
  85. package/ts/index.ts +2 -0
  86. package/ts/managed.types.ts +91 -0
  87. package/ts/plugins.ts +6 -0
  88. /package/{license → license.md} +0 -0
@@ -0,0 +1,798 @@
1
+ use super::*;
2
+
3
+ #[path = "owner_namespace_tests.rs"]
4
+ mod namespace_tests;
5
+
6
+ fn isolated() {
7
+ assert_eq!(
8
+ std::env::var("SMARTNFTABLES_ISOLATED_NATIVE").as_deref(),
9
+ Ok("qemu-6.18.35")
10
+ );
11
+ assert!(std::fs::read_to_string("/proc/cmdline")
12
+ .unwrap()
13
+ .contains("smartnftables_qualification=1"));
14
+ assert_eq!(unsafe { libc::geteuid() }, 0);
15
+ let initial = context().unwrap();
16
+ assert_eq!(
17
+ unsafe { libc::unshare(libc::CLONE_NEWNET | libc::CLONE_NEWNS) },
18
+ 0
19
+ );
20
+ assert_eq!(
21
+ unsafe {
22
+ libc::mount(
23
+ std::ptr::null(),
24
+ c"/".as_ptr(),
25
+ std::ptr::null(),
26
+ libc::MS_PRIVATE | libc::MS_REC,
27
+ std::ptr::null(),
28
+ )
29
+ },
30
+ 0
31
+ );
32
+ assert_ne!(initial.2, context().unwrap().2);
33
+ assert_eq!(
34
+ unsafe {
35
+ libc::mount(
36
+ c"tmpfs".as_ptr(),
37
+ c"/run".as_ptr(),
38
+ c"tmpfs".as_ptr(),
39
+ libc::MS_NOSUID | libc::MS_NODEV,
40
+ c"size=4m".as_ptr().cast(),
41
+ )
42
+ },
43
+ 0
44
+ );
45
+ }
46
+
47
+ fn ip(arguments: &[&str]) {
48
+ let result = std::process::Command::new("/bin/ip")
49
+ .args(arguments)
50
+ .output()
51
+ .unwrap();
52
+ assert!(
53
+ result.status.success(),
54
+ "ip {arguments:?}: {}",
55
+ String::from_utf8_lossy(&result.stderr)
56
+ );
57
+ }
58
+ fn in_namespace<T: Send + 'static>(name: &str, work: impl FnOnce() -> T + Send + 'static) -> T {
59
+ let file = std::fs::File::open(format!("/run/netns/{name}")).unwrap();
60
+ std::thread::spawn(move || {
61
+ use std::os::fd::AsRawFd;
62
+ assert_eq!(
63
+ unsafe { libc::setns(file.as_raw_fd(), libc::CLONE_NEWNET) },
64
+ 0
65
+ );
66
+ work()
67
+ })
68
+ .join()
69
+ .unwrap()
70
+ }
71
+ fn endpoint(id: &str, part: u8) -> crate::policy::Endpoint {
72
+ let host = format!("work{id}");
73
+ let peer = format!("peer{id}");
74
+ ip(&["netns", "add", id]);
75
+ ip(&["link", "add", &host, "type", "veth", "peer", "name", &peer]);
76
+ ip(&["link", "set", &peer, "netns", id]);
77
+ ip(&[
78
+ "address",
79
+ "add",
80
+ &format!("10.81.{part}.1/24"),
81
+ "dev",
82
+ &host,
83
+ ]);
84
+ ip(&[
85
+ "-6",
86
+ "address",
87
+ "add",
88
+ &format!("2001:db8:{part}::1/64"),
89
+ "dev",
90
+ &host,
91
+ "nodad",
92
+ ]);
93
+ ip(&["link", "set", &host, "up"]);
94
+ for args in [
95
+ vec!["link", "set", "lo", "up"],
96
+ vec!["link", "set", &peer, "up"],
97
+ ] {
98
+ let mut command = vec!["-n", id];
99
+ command.extend(args);
100
+ ip(&command);
101
+ }
102
+ ip(&[
103
+ "-n",
104
+ id,
105
+ "address",
106
+ "add",
107
+ &format!("10.81.{part}.2/24"),
108
+ "dev",
109
+ &peer,
110
+ ]);
111
+ ip(&[
112
+ "-n",
113
+ id,
114
+ "route",
115
+ "add",
116
+ "default",
117
+ "via",
118
+ &format!("10.81.{part}.1"),
119
+ ]);
120
+ ip(&[
121
+ "-n",
122
+ id,
123
+ "-6",
124
+ "address",
125
+ "add",
126
+ &format!("2001:db8:{part}::2/64"),
127
+ "dev",
128
+ &peer,
129
+ "nodad",
130
+ ]);
131
+ ip(&[
132
+ "-n",
133
+ id,
134
+ "-6",
135
+ "route",
136
+ "add",
137
+ "default",
138
+ "via",
139
+ &format!("2001:db8:{part}::1"),
140
+ ]);
141
+ // Fixed peer neighbors isolate packet policy from multicast discovery timing.
142
+ let mac = |args: &[&str]| -> String {
143
+ let data = std::process::Command::new("/bin/ip")
144
+ .args(args)
145
+ .output()
146
+ .unwrap();
147
+ assert!(data.status.success());
148
+ serde_json::from_slice::<serde_json::Value>(&data.stdout).unwrap()[0]["address"]
149
+ .as_str()
150
+ .unwrap()
151
+ .into()
152
+ };
153
+ let host_mac = mac(&["-j", "link", "show", "dev", &host]);
154
+ let peer_mac = mac(&["-n", id, "-j", "link", "show", "dev", &peer]);
155
+ ip(&[
156
+ "-6",
157
+ "neigh",
158
+ "replace",
159
+ &format!("2001:db8:{part}::2"),
160
+ "lladdr",
161
+ &peer_mac,
162
+ "nud",
163
+ "permanent",
164
+ "dev",
165
+ &host,
166
+ ]);
167
+ ip(&[
168
+ "-n",
169
+ id,
170
+ "-6",
171
+ "neigh",
172
+ "replace",
173
+ &format!("2001:db8:{part}::1"),
174
+ "lladdr",
175
+ &host_mac,
176
+ "nud",
177
+ "permanent",
178
+ "dev",
179
+ &peer,
180
+ ]);
181
+ let cname = std::ffi::CString::new(host.clone()).unwrap();
182
+ crate::policy::Endpoint {
183
+ id: id.into(),
184
+ interface_name: host,
185
+ interface_kind: "veth".into(),
186
+ interface_index: unsafe { libc::if_nametoindex(cname.as_ptr()) },
187
+ source_prefixes: vec![format!("10.81.{part}.2/32")],
188
+ }
189
+ }
190
+ fn udp(source: &std::net::UdpSocket, target: &std::net::UdpSocket, expected: bool) {
191
+ target
192
+ .set_read_timeout(Some(std::time::Duration::from_millis(if expected {
193
+ 1500
194
+ } else {
195
+ 200
196
+ })))
197
+ .unwrap();
198
+ source
199
+ .send_to(b"exact-grant", target.local_addr().unwrap())
200
+ .unwrap();
201
+ let received = target.recv(&mut [0; 64]);
202
+ assert_eq!(
203
+ received.is_ok(),
204
+ expected,
205
+ "UDP {:?} -> {:?}: {received:?}",
206
+ source.local_addr(),
207
+ target.local_addr()
208
+ );
209
+ }
210
+
211
+ #[test]
212
+ #[ignore = "requires the explicitly isolated QEMU qualification guest"]
213
+ fn native_interface_ipv4_directed_grants_and_spoof_denial() {
214
+ isolated();
215
+ ip(&["link", "set", "lo", "up"]);
216
+ std::fs::write("/proc/sys/net/ipv4/ip_forward", "1").unwrap();
217
+ std::fs::write("/proc/sys/net/ipv6/conf/all/forwarding", "1").unwrap();
218
+ std::fs::write("/proc/sys/net/ipv6/conf/default/forwarding", "1").unwrap();
219
+ let a = endpoint("a", 1);
220
+ let b = endpoint("b", 2);
221
+ let c = endpoint("c", 3);
222
+ let _unmanaged = endpoint("d", 4);
223
+ let client = in_namespace("a", || {
224
+ std::net::UdpSocket::bind("10.81.1.2:23456").unwrap()
225
+ });
226
+ let server = in_namespace("b", || std::net::UdpSocket::bind("10.81.2.2:53").unwrap());
227
+ let forbidden = in_namespace("c", || std::net::UdpSocket::bind("10.81.3.2:53").unwrap());
228
+ let v6client = in_namespace("a", || {
229
+ std::net::UdpSocket::bind("[2001:db8:1::2]:23456").unwrap()
230
+ });
231
+ let v6server = in_namespace("b", || {
232
+ std::net::UdpSocket::bind("[2001:db8:2::2]:53").unwrap()
233
+ });
234
+ let tcp = in_namespace("b", || std::net::TcpListener::bind("10.81.2.2:53").unwrap());
235
+ let host = std::net::UdpSocket::bind("10.81.1.1:53").unwrap();
236
+ let unmanaged = in_namespace("d", || {
237
+ std::net::UdpSocket::bind("10.81.4.2:34567").unwrap()
238
+ });
239
+ udp(&client, &server, true);
240
+ udp(&client, &forbidden, true);
241
+ udp(&v6client, &v6server, true);
242
+ let mut rules = Vec::new();
243
+ for protocol in ["udp", "tcp"] {
244
+ for remote in [Some("b"), None] {
245
+ let destination = if remote.is_some() {
246
+ "10.81.2.2/32"
247
+ } else {
248
+ "10.81.1.1/32"
249
+ };
250
+ rules.push(crate::policy::Rule {
251
+ source_endpoint: Some("a".into()),
252
+ destination_endpoint: remote.map(Into::into),
253
+ source_prefix: "10.81.1.2/32".into(),
254
+ destination_prefix: destination.into(),
255
+ protocol: protocol.into(),
256
+ source_port: None,
257
+ destination_port: Some(53),
258
+ });
259
+ rules.push(crate::policy::Rule {
260
+ source_endpoint: remote.map(Into::into),
261
+ destination_endpoint: Some("a".into()),
262
+ source_prefix: destination.into(),
263
+ destination_prefix: "10.81.1.2/32".into(),
264
+ protocol: protocol.into(),
265
+ source_port: Some(53),
266
+ destination_port: None,
267
+ });
268
+ }
269
+ }
270
+ let policy = crate::policy::Policy {
271
+ schema_version: 1,
272
+ revision: 1,
273
+ endpoints: vec![a, b, c],
274
+ rules,
275
+ };
276
+ let mut owner = Owner::new(options("packets")).unwrap();
277
+ let mut invalid_pair = policy.clone();
278
+ invalid_pair.endpoints[0].interface_index = 1;
279
+ assert_eq!(
280
+ owner.reconcile(Transition {
281
+ previous: None,
282
+ target: invalid_pair.prepare().unwrap()
283
+ }),
284
+ Err(Error::Conflict)
285
+ );
286
+ assert!(owner.graph().unwrap().is_none());
287
+ ip(&["link", "add", "brtest", "type", "bridge"]);
288
+ ip(&["link", "set", "workc", "master", "brtest"]);
289
+ assert_eq!(
290
+ owner.reconcile(Transition {
291
+ previous: None,
292
+ target: policy.clone().prepare().unwrap()
293
+ }),
294
+ Err(Error::Conflict)
295
+ );
296
+ assert!(owner.graph().unwrap().is_none());
297
+ ip(&["link", "set", "workc", "nomaster"]);
298
+ let current = owner
299
+ .reconcile(Transition {
300
+ previous: None,
301
+ target: policy.clone().prepare().unwrap(),
302
+ })
303
+ .expect("apply interface policy");
304
+ assert!(owner.inspect().enforced);
305
+ udp(&client, &server, true);
306
+ udp(&server, &client, true);
307
+ udp(&client, &host, true);
308
+ udp(&host, &client, true);
309
+ udp(&client, &forbidden, false);
310
+ udp(&v6client, &v6server, false);
311
+ // Input unrelated to managed workload interfaces retains its original behavior.
312
+ udp(&unmanaged, &host, true);
313
+ let stream = in_namespace("a", || {
314
+ std::net::TcpStream::connect_timeout(
315
+ &"10.81.2.2:53".parse().unwrap(),
316
+ std::time::Duration::from_millis(500),
317
+ )
318
+ });
319
+ assert!(stream.is_ok());
320
+ let _accepted = tcp.accept().unwrap();
321
+ drop(stream);
322
+ ip(&["-n", "a", "address", "add", "10.81.99.2/32", "dev", "peera"]);
323
+ let spoof = in_namespace("a", || {
324
+ std::net::UdpSocket::bind("10.81.99.2:34567").unwrap()
325
+ });
326
+ udp(&spoof, &server, false);
327
+ // Renaming a bound interface cannot inherit the allow branch: the retained
328
+ // index still reaches denial, while allows require both exact selectors.
329
+ ip(&["link", "set", "worka", "name", "renameda"]);
330
+ udp(&client, &server, false);
331
+ ip(&["link", "set", "renameda", "name", "worka"]);
332
+ udp(&client, &server, true);
333
+ let mut denied = policy;
334
+ denied.revision = 2;
335
+ denied.rules.clear();
336
+ let revoked = owner
337
+ .reconcile(Transition {
338
+ previous: Some(current),
339
+ target: denied.prepare().unwrap(),
340
+ })
341
+ .expect("atomic complete deny");
342
+ udp(&client, &server, false);
343
+ udp(&client, &host, false);
344
+ udp(&unmanaged, &host, true);
345
+ // Caller closes its workload links before releasing persistent enforcement.
346
+ for name in ["worka", "workb", "workc"] {
347
+ ip(&["link", "set", name, "down"]);
348
+ }
349
+ owner.release(revoked).unwrap();
350
+ }
351
+ fn options(name: &str) -> Options {
352
+ Options {
353
+ owner_id: "fixture".into(),
354
+ instance_id: "qualification_20260909".into(),
355
+ table_name: format!("snft_{name}"),
356
+ }
357
+ }
358
+ fn empty(revision: u64) -> Prepared {
359
+ crate::policy::Policy {
360
+ schema_version: 1,
361
+ revision,
362
+ endpoints: vec![],
363
+ rules: vec![],
364
+ }
365
+ .prepare()
366
+ .unwrap()
367
+ }
368
+
369
+ #[test]
370
+ #[ignore = "requires the explicitly isolated QEMU qualification guest"]
371
+ fn native_owner_atomic_recovery_and_foreign_rejection() {
372
+ isolated();
373
+ let mut first = Owner::new(options("owner")).unwrap();
374
+ let intent = Transition {
375
+ previous: None,
376
+ target: empty(1),
377
+ };
378
+ let applied = first
379
+ .reconcile(intent.clone())
380
+ .expect("create owned persistent table");
381
+ assert!(first.inspect().enforced);
382
+ assert_eq!(first.reconcile(intent.clone()).unwrap(), applied);
383
+ let mut contender = Owner::new(options("owner")).unwrap();
384
+ assert_eq!(
385
+ contender.reconcile(intent.clone()),
386
+ Err(Error::ForeignOwner)
387
+ );
388
+ assert!(first.inspect().enforced);
389
+ drop(contender);
390
+ // Socket loss must retain the graph while clearing only exclusive ownership.
391
+ drop(first);
392
+ let mut recovered = Owner::new(options("owner")).unwrap();
393
+ assert_eq!(recovered.reconcile(intent).unwrap(), applied);
394
+ let next = recovered
395
+ .reconcile(Transition {
396
+ previous: Some(applied),
397
+ target: empty(2),
398
+ })
399
+ .unwrap();
400
+ assert!(recovered.inspect().enforced);
401
+ recovered.release(next.clone()).unwrap();
402
+ recovered.release(next).unwrap();
403
+ assert_eq!(recovered.inspect().state, "released");
404
+ assert!(recovered.inspect().error.is_none());
405
+ assert!(recovered.socket.is_none());
406
+ recovered.close().unwrap();
407
+ }
408
+
409
+ #[test]
410
+ #[ignore = "requires the explicitly isolated QEMU qualification guest"]
411
+ fn native_batch_rollback_generation_fence_and_lost_ack_recovery() {
412
+ isolated();
413
+ let mut owner = Owner::new(options("transactions")).unwrap();
414
+ let intent = Transition {
415
+ previous: None,
416
+ target: empty(1),
417
+ };
418
+ // The real batch commits, but its return is not recorded by the owner/caller.
419
+ owner.pending = Some(intent.clone());
420
+ let committed = owner.reconcile_inner(&intent).unwrap();
421
+ owner.error = Some(Error::Unavailable);
422
+ assert_eq!(owner.inspect().state, "failed-owned");
423
+ assert!(owner.close().is_err());
424
+ assert_eq!(
425
+ owner.reconcile(Transition {
426
+ previous: None,
427
+ target: empty(2)
428
+ }),
429
+ Err(Error::Conflict)
430
+ );
431
+ let exact = owner.reconcile(intent).unwrap();
432
+ assert_eq!(exact, committed);
433
+ let before = owner.graph().unwrap().unwrap();
434
+ let mut other = Socket::open().unwrap();
435
+ let gen = other.generation().unwrap();
436
+ other
437
+ .batch(
438
+ gen,
439
+ vec![(0, 0x600, vec![Attr::string(1, "other"), Attr::u32(2, 6)])],
440
+ )
441
+ .unwrap();
442
+ assert_eq!(
443
+ owner.socket().unwrap().batch(
444
+ before.generation,
445
+ vec![(
446
+ 3,
447
+ 0x600,
448
+ vec![
449
+ Attr::string(1, "snft_transactions"),
450
+ Attr::string(3, "stale")
451
+ ]
452
+ )]
453
+ ),
454
+ Err(Error::Conflict)
455
+ );
456
+ // A failing last operation rolls back the earlier chain creation as well.
457
+ let gen = owner.socket().unwrap().generation().unwrap();
458
+ assert!(owner
459
+ .socket()
460
+ .unwrap()
461
+ .batch(
462
+ gen,
463
+ vec![
464
+ (
465
+ 3,
466
+ 0x600,
467
+ vec![
468
+ Attr::string(1, "snft_transactions"),
469
+ Attr::string(3, "staged")
470
+ ]
471
+ ),
472
+ (
473
+ 6,
474
+ 0xc00,
475
+ vec![
476
+ Attr::string(1, "snft_transactions"),
477
+ Attr::string(2, "missing")
478
+ ]
479
+ ),
480
+ ]
481
+ )
482
+ .is_err());
483
+ let after = owner.graph().unwrap().unwrap();
484
+ assert!(owner.matches(&after, &committed.prepared).unwrap());
485
+ assert_eq!(
486
+ wire::handle(&before.table, 4),
487
+ wire::handle(&after.table, 4)
488
+ );
489
+ let gen = other.generation().unwrap();
490
+ assert_eq!(
491
+ other.batch(
492
+ gen,
493
+ vec![(2, 0, vec![Attr::string(1, "snft_transactions")])]
494
+ ),
495
+ Err(Error::Permission)
496
+ );
497
+ // Commit exact deletion, lose the ACK, then fully restart the native owner.
498
+ let gen = owner.socket().unwrap().generation().unwrap();
499
+ owner
500
+ .socket()
501
+ .unwrap()
502
+ .batch(
503
+ gen,
504
+ vec![(
505
+ 2,
506
+ 0,
507
+ vec![Attr::u64(4, wire::handle(&after.table, 4).unwrap())],
508
+ )],
509
+ )
510
+ .unwrap();
511
+ drop(owner);
512
+ let mut restarted = Owner::new(options("transactions")).unwrap();
513
+ restarted.release(committed.clone()).unwrap();
514
+ restarted.release(committed).unwrap();
515
+ assert_eq!(restarted.inspect().state, "released");
516
+ }
517
+
518
+ #[test]
519
+ #[ignore = "requires the explicitly isolated QEMU qualification guest"]
520
+ fn native_postclaim_rejects_changed_graph_even_with_matching_generation() {
521
+ isolated();
522
+ let intent = Transition {
523
+ previous: None,
524
+ target: empty(1),
525
+ };
526
+ let mut owner = Owner::new(options("postclaim")).unwrap();
527
+ owner.reconcile(intent.clone()).unwrap();
528
+ drop(owner);
529
+ let mut recovery = Owner::new(options("postclaim")).unwrap();
530
+ let mut stale = recovery.graph().unwrap().unwrap();
531
+ let mut foreign = Socket::open().unwrap();
532
+ let gen = foreign.generation().unwrap();
533
+ foreign
534
+ .batch(
535
+ gen,
536
+ vec![
537
+ (
538
+ 0,
539
+ 0,
540
+ vec![Attr::string(1, "snft_postclaim"), Attr::u32(2, 6)],
541
+ ),
542
+ (
543
+ 3,
544
+ 0x600,
545
+ vec![
546
+ Attr::string(1, "snft_postclaim"),
547
+ Attr::string(3, "foreign"),
548
+ ],
549
+ ),
550
+ ],
551
+ )
552
+ .unwrap();
553
+ drop(foreign);
554
+ // Model the same 32-bit generation value recurring after counter wrap.
555
+ // The stale graph now passes GENID; only postclaim graph proof can reject it.
556
+ stale.generation = recovery.socket().unwrap().generation().unwrap();
557
+ assert!(matches!(
558
+ recovery.adopt(stale, &intent),
559
+ Err(Error::Conflict)
560
+ ));
561
+ let unchanged = recovery.graph().unwrap().unwrap();
562
+ assert_eq!(wire::number(&unchanged.table, 2).unwrap(), 4);
563
+ assert!(unchanged
564
+ .chains
565
+ .iter()
566
+ .any(|chain| wire::text(chain, 3).unwrap() == "foreign"));
567
+ assert_eq!(recovery.reconcile(intent), Err(Error::Conflict));
568
+ assert!(recovery.close().is_err());
569
+ }
570
+
571
+ #[test]
572
+ #[ignore = "requires the explicitly isolated QEMU qualification guest"]
573
+ fn native_foreign_table_binding_is_never_removed() {
574
+ isolated();
575
+ let mut foreign = Socket::open().unwrap();
576
+ let gen = foreign.generation().unwrap();
577
+ foreign
578
+ .batch(
579
+ gen,
580
+ vec![(
581
+ 0,
582
+ 0x600,
583
+ vec![
584
+ Attr::string(1, "snft_foreign"),
585
+ Attr::u32(2, 6),
586
+ Attr::bytes(6, b"foreign-binding".to_vec()),
587
+ ],
588
+ )],
589
+ )
590
+ .unwrap();
591
+ let mut owner = Owner::new(options("foreign")).unwrap();
592
+ assert_eq!(
593
+ owner.reconcile(Transition {
594
+ previous: None,
595
+ target: empty(1)
596
+ }),
597
+ Err(Error::Conflict)
598
+ );
599
+ assert!(owner.close().is_err());
600
+ let graph = owner.graph().unwrap().unwrap();
601
+ assert_eq!(
602
+ wire::one(&graph.table, 6).unwrap().value,
603
+ b"foreign-binding"
604
+ );
605
+ assert_eq!(wire::number(&graph.table, 7).unwrap(), foreign.port);
606
+ }
607
+
608
+ #[test]
609
+ #[ignore = "requires the explicitly isolated QEMU qualification guest"]
610
+ fn native_tun_forwarding_binds_remote_ingress_and_rejects_tap() {
611
+ use std::io::Write;
612
+ use std::os::fd::AsRawFd;
613
+ isolated();
614
+ ip(&["link", "set", "lo", "up"]);
615
+ std::fs::write("/proc/sys/net/ipv4/ip_forward", "1").unwrap();
616
+ let workload = endpoint("a", 1);
617
+ let _unmanaged = endpoint("d", 4);
618
+ ip(&["tuntap", "add", "dev", "relaya", "mode", "tun"]);
619
+ ip(&["address", "add", "10.90.0.1/24", "dev", "relaya"]);
620
+ ip(&["link", "set", "relaya", "up"]);
621
+ let tunnel = crate::policy::Endpoint {
622
+ id: "relay".into(),
623
+ interface_name: "relaya".into(),
624
+ interface_index: unsafe { libc::if_nametoindex(c"relaya".as_ptr()) },
625
+ interface_kind: "tun".into(),
626
+ source_prefixes: vec!["10.90.0.2/32".into()],
627
+ };
628
+ let receiver = in_namespace("a", || std::net::UdpSocket::bind("10.81.1.2:53").unwrap());
629
+ receiver
630
+ .set_read_timeout(Some(std::time::Duration::from_millis(500)))
631
+ .unwrap();
632
+ let mut device = std::fs::OpenOptions::new()
633
+ .read(true)
634
+ .write(true)
635
+ .open("/dev/net/tun")
636
+ .unwrap();
637
+ let mut request: libc::ifreq = unsafe { std::mem::zeroed() };
638
+ for (slot, byte) in request.ifr_name.iter_mut().zip(b"relaya") {
639
+ *slot = *byte as libc::c_char;
640
+ }
641
+ request.ifr_ifru.ifru_flags = (libc::IFF_TUN | libc::IFF_NO_PI) as libc::c_short;
642
+ assert_eq!(
643
+ unsafe { libc::ioctl(device.as_raw_fd(), libc::TUNSETIFF, &request) },
644
+ 0
645
+ );
646
+ let mut bytes = vec![0_u8; 32];
647
+ bytes[0] = 0x45;
648
+ bytes[2..4].copy_from_slice(&32_u16.to_be_bytes());
649
+ bytes[8] = 64;
650
+ bytes[9] = 17;
651
+ bytes[12..16].copy_from_slice(&[10, 90, 0, 2]);
652
+ bytes[16..20].copy_from_slice(&[10, 81, 1, 2]);
653
+ let sum: u32 = bytes[..20]
654
+ .chunks_exact(2)
655
+ .map(|v| u16::from_be_bytes(v.try_into().unwrap()) as u32)
656
+ .sum();
657
+ let sum = (sum & 0xffff) + (sum >> 16);
658
+ bytes[10..12].copy_from_slice(&(!(sum as u16)).to_be_bytes());
659
+ bytes[20..22].copy_from_slice(&23456_u16.to_be_bytes());
660
+ bytes[22..24].copy_from_slice(&53_u16.to_be_bytes());
661
+ bytes[24..26].copy_from_slice(&12_u16.to_be_bytes());
662
+ bytes[28..].copy_from_slice(b"test");
663
+ let send_tunnel = |device: &mut std::fs::File, expected| {
664
+ device.write_all(&bytes).unwrap();
665
+ assert_eq!(receiver.recv(&mut [0; 64]).is_ok(), expected);
666
+ };
667
+ send_tunnel(&mut device, true);
668
+ let policy = crate::policy::Policy {
669
+ schema_version: 1,
670
+ revision: 1,
671
+ endpoints: vec![workload, tunnel],
672
+ rules: vec![
673
+ crate::policy::Rule {
674
+ source_endpoint: Some("relay".into()),
675
+ destination_endpoint: Some("a".into()),
676
+ source_prefix: "10.90.0.2/32".into(),
677
+ destination_prefix: "10.81.1.2/32".into(),
678
+ protocol: "udp".into(),
679
+ source_port: None,
680
+ destination_port: Some(53),
681
+ },
682
+ // This host-side rule must never authorize forwarded traffic from workd.
683
+ crate::policy::Rule {
684
+ source_endpoint: None,
685
+ destination_endpoint: Some("a".into()),
686
+ source_prefix: "10.90.0.3/32".into(),
687
+ destination_prefix: "10.81.1.2/32".into(),
688
+ protocol: "udp".into(),
689
+ source_port: None,
690
+ destination_port: Some(53),
691
+ },
692
+ ],
693
+ };
694
+ let mut owner = Owner::new(options("tun")).unwrap();
695
+ ip(&["tuntap", "add", "dev", "badlayer2", "mode", "tap"]);
696
+ let mut invalid = policy.clone();
697
+ invalid.endpoints[1].interface_name = "badlayer2".into();
698
+ invalid.endpoints[1].interface_index = unsafe { libc::if_nametoindex(c"badlayer2".as_ptr()) };
699
+ assert_eq!(
700
+ owner.reconcile(Transition {
701
+ previous: None,
702
+ target: invalid.prepare().unwrap()
703
+ }),
704
+ Err(Error::Conflict)
705
+ );
706
+ let applied = owner
707
+ .reconcile(Transition {
708
+ previous: None,
709
+ target: policy.prepare().unwrap(),
710
+ })
711
+ .unwrap();
712
+ send_tunnel(&mut device, true);
713
+ for source in ["10.90.0.2", "10.90.0.3"] {
714
+ ip(&[
715
+ "-n",
716
+ "d",
717
+ "address",
718
+ "add",
719
+ &format!("{source}/32"),
720
+ "dev",
721
+ "peerd",
722
+ ]);
723
+ let address = format!("{source}:34567");
724
+ let forged = in_namespace("d", move || std::net::UdpSocket::bind(address).unwrap());
725
+ udp(&forged, &receiver, false);
726
+ }
727
+ ip(&["link", "set", "worka", "down"]);
728
+ ip(&["link", "set", "relaya", "down"]);
729
+ owner.release(applied).unwrap();
730
+ }
731
+
732
+ #[test]
733
+ #[ignore = "requires the explicitly isolated QEMU qualification guest"]
734
+ fn native_unprivileged_process_never_claims_enforcement_and_joins() {
735
+ use std::io::{BufRead, Write};
736
+ use std::os::unix::process::CommandExt;
737
+ isolated();
738
+ struct Child(std::process::Child);
739
+ impl Drop for Child {
740
+ fn drop(&mut self) {
741
+ let _ = self.0.kill();
742
+ let _ = self.0.wait();
743
+ }
744
+ }
745
+ let mut child = Child(
746
+ std::process::Command::new("/bin/managed")
747
+ .arg("--management")
748
+ .uid(65534)
749
+ .gid(65534)
750
+ .stdin(std::process::Stdio::piped())
751
+ .stdout(std::process::Stdio::piped())
752
+ .stderr(std::process::Stdio::null())
753
+ .spawn()
754
+ .unwrap(),
755
+ );
756
+ let mut output = std::io::BufReader::new(child.0.stdout.take().unwrap());
757
+ let mut line = String::new();
758
+ output.read_line(&mut line).unwrap();
759
+ assert_eq!(
760
+ serde_json::from_str::<serde_json::Value>(&line).unwrap()["event"],
761
+ "ready"
762
+ );
763
+ let mut command = |method: &str, params: serde_json::Value| {
764
+ let request = serde_json::json!({"id":"fixture", "method":method, "params":params});
765
+ let input = child.0.stdin.as_mut().unwrap();
766
+ writeln!(input, "{request}").unwrap();
767
+ input.flush().unwrap();
768
+ line.clear();
769
+ output.read_line(&mut line).unwrap();
770
+ serde_json::from_str::<serde_json::Value>(&line).unwrap()
771
+ };
772
+ let opened = command(
773
+ "openOwner",
774
+ serde_json::to_value(options("unprivileged")).unwrap(),
775
+ );
776
+ assert_eq!(opened["success"], true);
777
+ assert_eq!(opened["result"]["enforced"], false);
778
+ let applied = command(
779
+ "reconcilePolicy",
780
+ serde_json::to_value(Transition {
781
+ previous: None,
782
+ target: empty(1),
783
+ })
784
+ .unwrap(),
785
+ );
786
+ assert_eq!(applied["success"], false);
787
+ assert_eq!(applied["errorCode"], "PERMISSION");
788
+ let status = command("inspectPolicy", serde_json::json!({}));
789
+ assert_eq!(status["result"]["enforced"], false);
790
+ assert_eq!(status["result"]["pending"], serde_json::Value::Null);
791
+ assert_eq!(
792
+ command("closeOwner", serde_json::json!({}))["result"]["released"],
793
+ true
794
+ );
795
+ drop(command);
796
+ child.0.stdin.take();
797
+ assert!(child.0.wait().unwrap().success());
798
+ }