@push.rocks/smartnftables 1.2.0 → 1.3.1

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