@push.rocks/smartnftables 2.5.1 → 2.6.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 (65) hide show
  1. package/changelog.md +19 -0
  2. package/dist_rust/smartnftables_linux_amd64_musl +0 -0
  3. package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
  4. package/dist_rust/smartnftables_linux_arm64_musl +0 -0
  5. package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/managed.egress.types.d.ts +44 -0
  8. package/native-notices/crates/memchr-2.8.3/unlicense +24 -0
  9. package/native-notices/manifest.json +566 -344
  10. package/native-notices/readme.md +40 -34
  11. package/native-notices/runtime/gcc/gpl-3.0.txt +674 -0
  12. package/native-notices/runtime/gcc/runtime-library-exception-3.1.txt +73 -0
  13. package/native-notices/{rust-standard-library/dependency-notices.html → rust/copyright-library.html} +185 -991
  14. package/native-notices/rust/crates/adler2-2.0.1/license-0bsd +12 -0
  15. package/native-notices/rust/crates/adler2-2.0.1/license-apache +201 -0
  16. package/native-notices/rust/crates/adler2-2.0.1/license-mit +23 -0
  17. package/native-notices/rust/crates/memchr-2.7.6/copying +3 -0
  18. package/native-notices/rust/crates/memchr-2.7.6/license-mit +21 -0
  19. package/native-notices/rust/crates/memchr-2.7.6/unlicense +24 -0
  20. package/native-notices/rust/crates/miniz_oxide-0.8.9/license +25 -0
  21. package/native-notices/{rust-standard-library/llvm-libunwind-license.txt → rust/crates/miniz_oxide-0.8.9/license-apache.md} +11 -145
  22. package/native-notices/rust/crates/miniz_oxide-0.8.9/license-mit.md +24 -0
  23. package/native-notices/rust/crates/miniz_oxide-0.8.9/license-zlib.md +14 -0
  24. package/native-notices/{rust-standard-library → rust/crates/object-0.37.3}/license-apache +25 -0
  25. package/native-notices/{rust-standard-library → rust/crates/object-0.37.3}/license-mit +1 -1
  26. package/native-notices/rust/licenses/apache-2.0.txt +73 -0
  27. package/native-notices/rust/licenses/bsd-2-clause.txt +9 -0
  28. package/native-notices/rust/licenses/cc-by-sa-4.0.txt +427 -0
  29. package/native-notices/rust/licenses/gcc-exception-3.1.txt +30 -0
  30. package/native-notices/rust/licenses/gpl-2.0-only.txt +133 -0
  31. package/native-notices/rust/licenses/gpl-3.0-or-later.txt +202 -0
  32. package/native-notices/rust/licenses/isc.txt +7 -0
  33. package/native-notices/rust/licenses/llvm-exception.txt +15 -0
  34. package/native-notices/rust/licenses/mit.txt +9 -0
  35. package/native-notices/rust/licenses/ncsa.txt +29 -0
  36. package/native-notices/rust/licenses/ofl-1.1.txt +43 -0
  37. package/native-notices/{rust-standard-library/unicode-license.txt → rust/licenses/unicode-3.0.txt} +1 -1
  38. package/package.json +4 -5
  39. package/readme.md +149 -5
  40. package/rust/src/egress.compile.rs +2 -0
  41. package/rust/src/egress.host.rs +88 -1
  42. package/rust/src/egress.hostgrant.rs +13 -13
  43. package/rust/src/egress.poolguard.rs +45 -0
  44. package/rust/src/egress.router.rs +3 -1
  45. package/rust/src/egress.rs +122 -6
  46. package/rust/src/egress.workloadgrant.rs +88 -0
  47. package/rust/src/egress_hostgrant_tests.rs +47 -29
  48. package/rust/src/egress_localplatform_tests.rs +204 -0
  49. package/rust/src/egress_localport_tests.rs +264 -0
  50. package/rust/src/egress_workloadgrant_tests.rs +314 -0
  51. package/rust/src/owner.rs +8 -1
  52. package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
  53. package/rust/src/owner_identity_tests.rs +62 -0
  54. package/rust/src/owner_localplatform_tests.rs +274 -0
  55. package/rust/src/owner_localport_tests.rs +241 -0
  56. package/rust/src/owner_tests.rs +9 -0
  57. package/rust/src/owner_workloadgrant_tests.rs +278 -0
  58. package/ts/00_commitinfo_data.ts +1 -1
  59. package/ts/managed.egress.types.ts +46 -0
  60. package/native-notices/rust-standard-library/cargo.lock +0 -522
  61. package/scripts/check-native-notices.mjs +0 -27
  62. /package/native-notices/{rust-standard-library/musl-1.2.5-copyright.txt → runtime/musl-1.2.5/copyright} +0 -0
  63. /package/native-notices/{rust-standard-library → rust}/compiler-builtins-license.txt +0 -0
  64. /package/native-notices/{rust-standard-library/addr2line-0.25.1-license-apache → rust/crates/addr2line-0.25.1/license-apache} +0 -0
  65. /package/native-notices/{rust-standard-library/addr2line-0.25.1-license-mit → rust/crates/addr2line-0.25.1/license-mit} +0 -0
@@ -328,3 +328,65 @@ fn host_grant_sets_elements_and_lookups_have_exact_graph_identity() {
328
328
  assert!(rule_identity(&lookup(invalid)).is_err());
329
329
  }
330
330
  }
331
+
332
+ /// A loopback port owner compiles `meta skuid != uid` and a TCP reset. The graph
333
+ /// identity keeps the comparison operator, the uid and the reject type, and the
334
+ /// only reject Linux may dump back is a plain TCP reset.
335
+ #[test]
336
+ fn local_tcp_port_owner_graph_identity_preserves_uid_operator_and_tcp_reset() {
337
+ let policy = |uid: u32| {
338
+ crate::egress::localport_tests::owned(
339
+ crate::egress::tests::pool_guard(),
340
+ serde_json::json!([crate::egress::localport_tests::owner(
341
+ "127.0.0.1",
342
+ 10010,
343
+ uid
344
+ )]),
345
+ )
346
+ };
347
+ let output = |uid: u32| {
348
+ let prepared = serde_json::from_value::<crate::egress::Policy>(policy(uid))
349
+ .unwrap()
350
+ .prepare()
351
+ .unwrap();
352
+ let program = prepared.program("snft_identity").unwrap();
353
+ let (_, rule) = program
354
+ .into_iter()
355
+ .find(|(kind, attributes)| *kind == 6 && wire::text(attributes, 2).unwrap() == "output")
356
+ .unwrap();
357
+ rule
358
+ };
359
+ let root = rule_identity(&output(0)).unwrap();
360
+ assert_ne!(rule_identity(&output(1000)).unwrap().1, root.1);
361
+ // The same rule with `==` instead of `!=` is a different graph.
362
+ let mut equal = output(0);
363
+ let expressions = wire::attrs(&wire::one(&equal, 4).unwrap().value).unwrap();
364
+ let flipped: Vec<_> = expressions
365
+ .iter()
366
+ .enumerate()
367
+ .map(|(index, item)| {
368
+ if index + 2 != expressions.len() {
369
+ return item.clone();
370
+ }
371
+ let parts = wire::attrs(&item.value).unwrap();
372
+ let mut fields = wire::attrs(&wire::one(&parts, 2).unwrap().value).unwrap();
373
+ assert_eq!(wire::number(&fields, 2).unwrap(), 1);
374
+ fields[1] = Attr::u32(2, 0);
375
+ expression("cmp", fields)
376
+ })
377
+ .collect();
378
+ equal[2] = Attr::nested(4, flipped);
379
+ assert_ne!(rule_identity(&equal).unwrap(), root);
380
+ let reject = |data: Vec<Attr>| rule(vec![expression("reject", data)]);
381
+ assert!(rule_identity(&reject(vec![Attr::u32(1, 1)])).is_ok());
382
+ for invalid in [
383
+ vec![],
384
+ vec![Attr::u32(1, 0)],
385
+ vec![Attr::u32(1, 2)],
386
+ vec![Attr::u32(1, 1), Attr::bytes(2, vec![3])],
387
+ vec![Attr::u32(1, 1), Attr::u32(1, 1)],
388
+ vec![Attr::bytes(1, vec![1])],
389
+ ] {
390
+ assert!(rule_identity(&reject(invalid)).is_err());
391
+ }
392
+ }
@@ -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
+ }
@@ -0,0 +1,241 @@
1
+ use super::egress_traffic_tests::{connect, ns_ip};
2
+ use super::host_traffic_tests::{immediate, payload};
3
+ use super::*;
4
+ use crate::{
5
+ egress,
6
+ policy::{expr, meta},
7
+ };
8
+ use serde_json::json;
9
+ use std::io::ErrorKind;
10
+ use std::net::{SocketAddr, TcpListener, TcpStream};
11
+ use std::time::Duration;
12
+
13
+ const HOST: &str = "lp_host";
14
+ const PEER: &str = "lp_peer";
15
+
16
+ fn owned(owners: serde_json::Value) -> egress::Policy {
17
+ serde_json::from_value(egress::localport_tests::owned(
18
+ egress::tests::pool_guard(),
19
+ owners,
20
+ ))
21
+ .unwrap()
22
+ }
23
+
24
+ /// The complete graph with the maximum of loopback owners, including every
25
+ /// TCP reset, persists across owner loss and verifies before adoption.
26
+ #[test]
27
+ #[ignore = "requires disposable isolated native qualification guest"]
28
+ fn local_tcp_port_owner_maximum_graph_persist_and_lost_ack_recovery() {
29
+ isolated();
30
+ let owners: Vec<_> = (0..8)
31
+ .map(|index| egress::localport_tests::owner("127.0.0.1", 10010 + index, index.into()))
32
+ .collect();
33
+ super::egress_tests::recover("local_port_v2", owned(json!(owners)));
34
+ }
35
+
36
+ // Foreign NAT controls exist only in this offline guest: the uplink address and
37
+ // an off-host destination both translate to the loopback service, as a foreign
38
+ // owner's prerouting or output DNAT could. They are never part of the policy.
39
+ fn foreign_loopback_translation() {
40
+ let table = "snft_foreign_loopback";
41
+ let mut operations = vec![(0, 0x600, vec![Attr::string(1, table), Attr::u32(2, 6)])];
42
+ for (chain, hook, destination) in [("pre", 0, [192, 0, 2, 2]), ("out", 3, [198, 51, 100, 1])] {
43
+ operations.push((
44
+ 3,
45
+ 0x600,
46
+ vec![
47
+ Attr::string(1, table),
48
+ Attr::string(3, chain),
49
+ Attr::nested(4, vec![Attr::u32(1, hook), Attr::u32(2, -100_i32 as u32)]),
50
+ Attr::u32(5, 1),
51
+ Attr::string(7, "nat"),
52
+ Attr::u32(10, 1),
53
+ ],
54
+ ));
55
+ let mut expressions = meta(16, vec![6]);
56
+ expressions.extend(payload(16, destination.to_vec(), false));
57
+ expressions.extend(payload(2, 10010_u16.to_be_bytes().to_vec(), true));
58
+ expressions.push(immediate(vec![127, 0, 0, 1]));
59
+ expressions.push(expr(
60
+ "nat",
61
+ vec![
62
+ Attr::u32(1, 1),
63
+ Attr::u32(2, 2),
64
+ Attr::u32(3, 1),
65
+ Attr::u32(4, 1),
66
+ Attr::u32(7, 1),
67
+ ],
68
+ ));
69
+ operations.push((
70
+ 6,
71
+ 0xc00,
72
+ vec![
73
+ Attr::string(1, table),
74
+ Attr::string(2, chain),
75
+ Attr::nested(4, expressions),
76
+ ],
77
+ ));
78
+ }
79
+ let mut socket = wire::Socket::open().unwrap();
80
+ let generation = socket.generation().unwrap();
81
+ socket.batch(generation, operations).unwrap();
82
+ }
83
+
84
+ /// One TCP connection attempt from `namespace`. A `uid` changes only this
85
+ /// thread's credentials (the raw syscall, not libc's process-wide setresuid)
86
+ /// before the socket is created, so the socket file carries that uid.
87
+ fn dial(namespace: &str, uid: Option<u32>, destination: &str) -> std::io::Result<TcpStream> {
88
+ let destination: SocketAddr = destination.parse().unwrap();
89
+ in_namespace(namespace, move || {
90
+ if let Some(uid) = uid {
91
+ assert_eq!(
92
+ unsafe { libc::syscall(libc::SYS_setresuid, uid, uid, uid) },
93
+ 0
94
+ );
95
+ assert_eq!(unsafe { libc::syscall(libc::SYS_geteuid) }, i64::from(uid));
96
+ }
97
+ TcpStream::connect_timeout(&destination, Duration::from_millis(750))
98
+ })
99
+ }
100
+ fn reaches(namespace: &str, uid: Option<u32>, destination: &str) {
101
+ if let Err(error) = dial(namespace, uid, destination) {
102
+ panic!("{namespace} uid {uid:?} to {destination}: {error}");
103
+ }
104
+ }
105
+ fn refused(namespace: &str, uid: Option<u32>, destination: &str) {
106
+ let error = dial(namespace, uid, destination).unwrap_err();
107
+ assert_eq!(
108
+ error.kind(),
109
+ ErrorKind::ConnectionRefused,
110
+ "{uid:?} {destination}"
111
+ );
112
+ }
113
+ fn dark(namespace: &str, uid: Option<u32>, destination: &str) {
114
+ let error = dial(namespace, uid, destination).unwrap_err();
115
+ assert!(
116
+ matches!(
117
+ error.kind(),
118
+ ErrorKind::TimedOut | ErrorKind::ConnectionRefused
119
+ ),
120
+ "{uid:?} {destination}: {error}"
121
+ );
122
+ }
123
+
124
+ /// Every probe runs first as a positive control on the same listeners, then
125
+ /// under the policy, then again after release, so each denial is this owner's.
126
+ #[test]
127
+ #[ignore = "requires disposable isolated native qualification guest"]
128
+ fn local_tcp_port_owner_resets_other_users_and_drops_non_loopback_arrivals() {
129
+ isolated();
130
+ for name in [HOST, PEER] {
131
+ ip(&["netns", "add", name]);
132
+ ns_ip(name, &["link", "set", "lo", "up"]);
133
+ }
134
+ connect(
135
+ HOST,
136
+ "uplink",
137
+ "192.0.2.2/24",
138
+ PEER,
139
+ "underlay",
140
+ "192.0.2.3/24",
141
+ );
142
+ ns_ip(
143
+ HOST,
144
+ &["route", "add", "198.51.100.0/24", "via", "192.0.2.3"],
145
+ );
146
+ in_namespace(HOST, || {
147
+ // The setting that let adjacent hosts reach loopback services (CVE-2020-8558).
148
+ for device in ["all", "uplink"] {
149
+ std::fs::write(
150
+ format!("/proc/sys/net/ipv4/conf/{device}/route_localnet"),
151
+ "1",
152
+ )
153
+ .unwrap();
154
+ }
155
+ foreign_loopback_translation();
156
+ });
157
+ let listeners: Vec<TcpListener> = in_namespace(HOST, || {
158
+ [
159
+ "127.0.0.1:10010",
160
+ "127.0.0.1:10011",
161
+ "127.0.0.1:10012",
162
+ "127.0.0.2:10010",
163
+ ]
164
+ .into_iter()
165
+ .map(|address| TcpListener::bind(address).unwrap())
166
+ .collect()
167
+ });
168
+ let probes = |applied: bool| {
169
+ // Root owns 10010, uid 1000 owns 10011; 10012 and 127.0.0.2 are unowned.
170
+ reaches(HOST, None, "127.0.0.1:10010");
171
+ reaches(HOST, Some(1000), "127.0.0.1:10011");
172
+ for uid in [1000, 65534] {
173
+ reaches(HOST, Some(uid), "127.0.0.1:10012");
174
+ reaches(HOST, Some(uid), "127.0.0.2:10010");
175
+ }
176
+ reaches(HOST, None, "198.51.100.1:10010");
177
+ if applied {
178
+ for uid in [1, 1000, 65534] {
179
+ refused(HOST, Some(uid), "127.0.0.1:10010");
180
+ }
181
+ refused(HOST, None, "127.0.0.1:10011");
182
+ // Output DNAT runs before filter priority 0: the translated
183
+ // destination is owned, so another user never connects.
184
+ dark(HOST, Some(1000), "198.51.100.1:10010");
185
+ // Arrival on the uplink, translated to loopback, drops at INPUT.
186
+ dark(PEER, None, "192.0.2.2:10010");
187
+ } else {
188
+ for uid in [1, 1000, 65534] {
189
+ reaches(HOST, Some(uid), "127.0.0.1:10010");
190
+ }
191
+ reaches(HOST, None, "127.0.0.1:10011");
192
+ reaches(HOST, Some(1000), "198.51.100.1:10010");
193
+ reaches(PEER, None, "192.0.2.2:10010");
194
+ }
195
+ };
196
+ probes(false);
197
+ let (owner, applied) = in_namespace(HOST, || {
198
+ let mut owner = Owner::new(options("local_port_traffic")).unwrap();
199
+ let target = owned(json!([
200
+ egress::localport_tests::owner("127.0.0.1", 10010, 0),
201
+ egress::localport_tests::owner("127.0.0.1", 10011, 1000)
202
+ ]))
203
+ .prepare()
204
+ .unwrap()
205
+ .into();
206
+ let applied = owner
207
+ .reconcile(Transition {
208
+ previous: None,
209
+ target,
210
+ })
211
+ .unwrap();
212
+ assert!(owner.inspect().enforced);
213
+ (owner, applied)
214
+ });
215
+ probes(true);
216
+ // Retained after the owning process is gone: the kernel still enforces it,
217
+ // and a fresh owner verifies the exact graph before adopting it.
218
+ let applied = in_namespace(HOST, move || {
219
+ let mut owner = owner;
220
+ owner.detach(applied.clone()).unwrap();
221
+ assert_eq!(owner.inspect().state, "retained");
222
+ drop(owner);
223
+ applied
224
+ });
225
+ probes(true);
226
+ in_namespace(HOST, move || {
227
+ let mut owner = Owner::new(options("local_port_traffic")).unwrap();
228
+ let recovered = owner
229
+ .reconcile(Transition {
230
+ previous: None,
231
+ target: applied.prepared.clone(),
232
+ })
233
+ .unwrap();
234
+ assert_eq!(recovered, applied);
235
+ assert!(owner.inspect().enforced);
236
+ owner.release(recovered).unwrap();
237
+ });
238
+ probes(false);
239
+ drop(listeners);
240
+ println!("LOCAL_TCP_PORT_OWNER_PROOF owner_reaches=true other_uids_reset=true uid_generic=true unowned_port_and_address_open=true output_dnat_denied=true non_loopback_arrival_dropped=true retained_after_owner_loss=true adopted_after_verification=true release_control=true");
241
+ }
@@ -27,6 +27,15 @@ mod router_traffic_tests;
27
27
  #[path = "owner_poolguard_tests.rs"]
28
28
  mod poolguard_tests;
29
29
 
30
+ #[path = "owner_localport_tests.rs"]
31
+ mod localport_tests;
32
+
33
+ #[path = "owner_localplatform_tests.rs"]
34
+ mod localplatform_tests;
35
+
36
+ #[path = "owner_workloadgrant_tests.rs"]
37
+ mod workloadgrant_tests;
38
+
30
39
  #[path = "owner_hostgrant_traffic_tests.rs"]
31
40
  mod hostgrant_traffic_tests;
32
41