@push.rocks/smartnftables 2.5.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/changelog.md +31 -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 +69 -3
  8. package/package.json +6 -6
  9. package/readme.md +342 -61
  10. package/rust/src/egress.compile.rs +157 -76
  11. package/rust/src/egress.host.rs +104 -44
  12. package/rust/src/egress.hostgrant.rs +14 -14
  13. package/rust/src/egress.keys.rs +305 -0
  14. package/rust/src/egress.leased.rs +399 -0
  15. package/rust/src/egress.poolguard.rs +45 -0
  16. package/rust/src/egress.private.rs +197 -0
  17. package/rust/src/egress.published.rs +370 -0
  18. package/rust/src/egress.router.rs +64 -295
  19. package/rust/src/egress.rs +248 -17
  20. package/rust/src/egress.workloadgrant.rs +88 -0
  21. package/rust/src/egress_hostgrant_tests.rs +196 -47
  22. package/rust/src/egress_localplatform_tests.rs +204 -0
  23. package/rust/src/egress_localport_tests.rs +266 -0
  24. package/rust/src/egress_publishedrange_tests.rs +812 -0
  25. package/rust/src/egress_routerequivalence_tests.rs +448 -0
  26. package/rust/src/egress_tests.rs +132 -230
  27. package/rust/src/egress_workloadgrant_tests.rs +313 -0
  28. package/rust/src/owner.rs +77 -15
  29. package/rust/src/owner_host_traffic_tests.rs +4 -0
  30. package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
  31. package/rust/src/owner_identity_tests.rs +228 -80
  32. package/rust/src/owner_localplatform_tests.rs +274 -0
  33. package/rust/src/owner_localport_tests.rs +241 -0
  34. package/rust/src/owner_publishedrange_traffic_tests.rs +306 -0
  35. package/rust/src/owner_router_traffic_tests.rs +2 -0
  36. package/rust/src/owner_scale_traffic_tests.rs +383 -0
  37. package/rust/src/owner_tests.rs +15 -0
  38. package/rust/src/owner_workloadgrant_tests.rs +278 -0
  39. package/rust/src/policy.rs +29 -25
  40. package/ts/00_commitinfo_data.ts +1 -1
  41. package/ts/managed.egress.types.ts +71 -3
@@ -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
+ }
@@ -0,0 +1,306 @@
1
+ //! Published port ranges and symmetric publications across both hops, in the
2
+ //! four-namespace topology of the router publication qualification.
3
+ use super::egress_traffic_tests::{allocation, binding, connect, denied, host_policy, ns_ip};
4
+ use super::egress_traffic_tests::{protection, roundtrip, socket};
5
+ use super::host_traffic_tests::{control, dark_udp, published_udp};
6
+ use super::router_traffic_tests::connect_from;
7
+ use super::*;
8
+ use crate::egress;
9
+ use serde_json::json;
10
+ use std::io::{Read, Write};
11
+ use std::net::TcpListener;
12
+ use std::time::Duration;
13
+
14
+ const WORK: &str = "pr_work";
15
+ const ROUTER: &str = "pr_router";
16
+ const HOST: &str = "pr_host";
17
+ const PEER: &str = "pr_peer";
18
+
19
+ /// The SIP/RTP shape on both hops: UDP and TCP signalling on one port and one UDP
20
+ /// RTP range, all symmetric, beside a plain publication and leased egress.
21
+ fn host_ranges() -> egress::Policy {
22
+ let mut policy = host_policy();
23
+ let egress::Scope::HostTransit(scope) = &mut policy.scope else {
24
+ panic!()
25
+ };
26
+ scope.published_ports = [
27
+ ("udp", 20000, Some(20200), 20000, true),
28
+ ("udp", 5070, None, 5070, true),
29
+ ("tcp", 5070, None, 5070, true),
30
+ ("udp", 8125, None, 8125, false),
31
+ ]
32
+ .into_iter()
33
+ .map(
34
+ |(protocol, host_port, host_port_end, target_port, symmetric)| egress::PublishedPort {
35
+ protocol: protocol.into(),
36
+ host_port,
37
+ host_port_end,
38
+ target_port,
39
+ target_address: "10.240.0.2".into(),
40
+ host_ip: "192.0.2.2".into(),
41
+ symmetric,
42
+ },
43
+ )
44
+ .collect();
45
+ policy
46
+ }
47
+ fn router_ranges() -> egress::Policy {
48
+ let link = binding("work", "10.241.0.1", "veth");
49
+ let mut generation = allocation();
50
+ generation["conntrackZone"] = json!(17);
51
+ generation["conntrackLabel"] = json!("a".repeat(32));
52
+ generation["grants"] = json!([
53
+ {"sourceEndpoint":"work","sourcePrefix":"10.241.0.2/32","destination":{"kind":"public","prefix":"203.0.113.2/32"},
54
+ "protocol":"udp","sourcePort":null,"destinationPort":42000,"sourcePortRange":{"first":10000,"last":10015}}
55
+ ]);
56
+ let publication = |protocol: &str,
57
+ port: u16,
58
+ end: Option<u16>,
59
+ endpoint: u16,
60
+ symmetric: bool| {
61
+ let mut value = json!({"protocol":protocol,"transitPort":port,"endpointPort":endpoint,
62
+ "endpointAddress":"10.241.0.2","transitSourceAddress":"10.240.0.2","symmetric":symmetric});
63
+ if let Some(end) = end {
64
+ value["transitPortEnd"] = json!(end);
65
+ }
66
+ value
67
+ };
68
+ serde_json::from_value(
69
+ json!({"schemaVersion":2,"revision":1,"scope":{"kind":"routerEgress",
70
+ "endpoints":[{"id":"work","interfaceIndex":link["interfaceIndex"],"interfaceName":"work",
71
+ "interfaceKind":"veth","sourcePrefixes":["10.241.0.2/32"]}],
72
+ "rules":[],"links":[link],"handoff":binding("handoff","10.240.0.2","veth"),
73
+ "protection":protection(),"generations":[generation],"publishedPorts":[
74
+ publication("udp", 20000, Some(20200), 20000, true),
75
+ publication("udp", 5070, None, 5070, true),
76
+ publication("tcp", 5070, None, 5070, true),
77
+ publication("udp", 8125, None, 9125, false)
78
+ ]}}),
79
+ )
80
+ .unwrap()
81
+ }
82
+ /// Binds and applies a policy inside its namespace, where its links exist, and
83
+ /// returns the owned sets with their element counts as the kernel dumps them.
84
+ /// Enforcement means the dump, every interval element and map datum included,
85
+ /// matched the compiled graph.
86
+ fn apply(
87
+ namespace: &str,
88
+ name: &'static str,
89
+ policy: fn() -> egress::Policy,
90
+ ) -> (Owner, Applied, Vec<(String, usize)>) {
91
+ in_namespace(namespace, move || {
92
+ let mut owner = Owner::new(options(name)).unwrap();
93
+ let target: Prepared = policy().prepare().unwrap().into();
94
+ target
95
+ .validate_interfaces()
96
+ .expect("published range fixture binding");
97
+ let applied = owner
98
+ .reconcile(Transition {
99
+ previous: None,
100
+ target,
101
+ })
102
+ .unwrap();
103
+ assert!(owner.inspect().enforced);
104
+ let sets = owner
105
+ .graph()
106
+ .unwrap()
107
+ .expect("owned table present")
108
+ .sets
109
+ .iter()
110
+ .map(|(attributes, elements)| (wire::text(attributes, 2).unwrap(), elements.len()))
111
+ .collect();
112
+ (owner, applied, sets)
113
+ })
114
+ }
115
+
116
+ #[test]
117
+ #[ignore = "requires disposable isolated native qualification guest"]
118
+ fn v2_published_ranges_and_symmetric_publications_cross_both_hops() {
119
+ isolated();
120
+ for name in [WORK, ROUTER, HOST, PEER] {
121
+ ip(&["netns", "add", name]);
122
+ ns_ip(name, &["link", "set", "lo", "up"]);
123
+ in_namespace(name, || {
124
+ std::fs::write("/proc/sys/net/ipv4/ip_forward", "1").unwrap()
125
+ });
126
+ }
127
+ connect(
128
+ WORK,
129
+ "client",
130
+ "10.241.0.2/24",
131
+ ROUTER,
132
+ "work",
133
+ "10.241.0.1/24",
134
+ );
135
+ connect(
136
+ ROUTER,
137
+ "handoff",
138
+ "10.240.0.2/30",
139
+ HOST,
140
+ "router",
141
+ "10.240.0.1/30",
142
+ );
143
+ connect(
144
+ HOST,
145
+ "uplink",
146
+ "192.0.2.2/24",
147
+ PEER,
148
+ "underlay",
149
+ "192.0.2.3/24",
150
+ );
151
+ ns_ip(WORK, &["route", "add", "default", "via", "10.241.0.1"]);
152
+ ns_ip(ROUTER, &["route", "add", "default", "via", "10.240.0.1"]);
153
+ // The workload route exists throughout, so every later denial is caused by
154
+ // policy and not by a missing route.
155
+ ns_ip(
156
+ HOST,
157
+ &["route", "add", "10.241.0.0/24", "via", "10.240.0.2"],
158
+ );
159
+ ns_ip(
160
+ HOST,
161
+ &["route", "add", "203.0.113.2/32", "via", "192.0.2.3"],
162
+ );
163
+ ns_ip(PEER, &["address", "add", "203.0.113.2/32", "dev", "lo"]);
164
+ ns_ip(PEER, &["route", "add", "default", "via", "192.0.2.2"]);
165
+ let first = socket(WORK, "10.241.0.2:20000");
166
+ let middle = socket(WORK, "10.241.0.2:20100");
167
+ let last = socket(WORK, "10.241.0.2:20200");
168
+ let above = socket(WORK, "10.241.0.2:20201");
169
+ let below = socket(WORK, "10.241.0.2:19999");
170
+ let rtp = socket(WORK, "10.241.0.2:20150");
171
+ let sip = socket(WORK, "10.241.0.2:5070");
172
+ let plain = socket(WORK, "10.241.0.2:9125");
173
+ let work_egress = socket(WORK, "10.241.0.2:40001");
174
+ let public = socket(PEER, "203.0.113.2:42000");
175
+ let rtp_peer = socket(PEER, "203.0.113.2:42001");
176
+ let sip_peer = socket(PEER, "203.0.113.2:42002");
177
+ let plain_peer = socket(PEER, "203.0.113.2:42003");
178
+ let protected_peer = socket(PEER, "192.0.2.3:40300");
179
+ // Positive controls: without policy the workload is routed directly and its
180
+ // packets reach the protected peer address, so every later result is caused
181
+ // by policy. Each probe uses its own tuple, so no unreplied conntrack entry
182
+ // carries a null translation across the policy change.
183
+ control(
184
+ &socket(PEER, "192.0.2.3:40010"),
185
+ "10.241.0.2:20201",
186
+ &above,
187
+ true,
188
+ );
189
+ control(
190
+ &socket(PEER, "192.0.2.3:40011"),
191
+ "10.241.0.2:19999",
192
+ &below,
193
+ true,
194
+ );
195
+ control(
196
+ &socket(WORK, "10.241.0.2:5071"),
197
+ "192.0.2.3:40300",
198
+ &protected_peer,
199
+ true,
200
+ );
201
+ dark_udp(&socket(PEER, "192.0.2.3:40012"), &middle, "192.0.2.2:20100");
202
+ let (host_owner, host_applied, host_sets) = apply(HOST, "published_range_host", host_ranges);
203
+ let (router_owner, router_applied, router_sets) =
204
+ apply(ROUTER, "published_range", router_ranges);
205
+ // Publications are set elements: one-port translations, range translations,
206
+ // the inbound admissions and the symmetric map on both hops, and the two raw
207
+ // classifications in the router.
208
+ let named = |sets: &[(String, usize)], name: &str| {
209
+ sets.iter()
210
+ .find(|(set, _)| set == name)
211
+ .map(|(_, count)| *count)
212
+ };
213
+ for (name, host, router) in [
214
+ ("published_port", Some(3), Some(3)),
215
+ ("published_address", Some(1), Some(1)),
216
+ ("published_forward", Some(4), Some(4)),
217
+ ("published_symmetric", Some(3), Some(3)),
218
+ ("published_arrival", None, Some(4)),
219
+ // 9125 and the 5070 ports of both protocols and the 20000 range.
220
+ ("published_endpoint", None, Some(4)),
221
+ ] {
222
+ assert_eq!(named(&host_sets, name), host, "host {name}");
223
+ assert_eq!(named(&router_sets, name), router, "router {name}");
224
+ }
225
+ // First, middle and last port of the range reach the listener behind both
226
+ // hops with the client address preserved, and the reply comes back from the
227
+ // published address and port.
228
+ for (port, client_port, workload) in [
229
+ (20000, 40100, &first),
230
+ (20100, 40101, &middle),
231
+ (20200, 40102, &last),
232
+ ] {
233
+ let client = in_namespace(PEER, move || {
234
+ std::net::UdpSocket::bind(format!("192.0.2.3:{client_port}")).unwrap()
235
+ });
236
+ client
237
+ .set_read_timeout(Some(Duration::from_millis(250)))
238
+ .unwrap();
239
+ published_udp(
240
+ &client,
241
+ workload,
242
+ &format!("192.0.2.2:{port}"),
243
+ &format!("192.0.2.3:{client_port}"),
244
+ );
245
+ }
246
+ // The ports just outside the range stay dark.
247
+ dark_udp(&socket(PEER, "192.0.2.3:40103"), &above, "192.0.2.2:20201");
248
+ dark_udp(&socket(PEER, "192.0.2.3:40104"), &below, "192.0.2.2:19999");
249
+ // Symmetric outbound flows reach the uplink with their own port, from a range
250
+ // port and from the single signalling port, and their replies return.
251
+ let observed = roundtrip(&rtp, &rtp_peer);
252
+ assert_eq!(observed.to_string(), "192.0.2.2:20150");
253
+ let observed = roundtrip(&sip, &sip_peer);
254
+ assert_eq!(observed.to_string(), "192.0.2.2:5070");
255
+ // A later request from the same peer tuple toward the published port is the
256
+ // flow's own reply direction and reaches the workload from that peer.
257
+ assert_eq!(sip_peer.send_to(b"invite", "192.0.2.2:5070").unwrap(), 6);
258
+ let mut bytes = [0; 64];
259
+ let (size, from) = sip
260
+ .recv_from(&mut bytes)
261
+ .expect("request on the symmetric flow");
262
+ assert_eq!(
263
+ (&bytes[..size], from.to_string().as_str()),
264
+ (&b"invite"[..], "203.0.113.2:42002")
265
+ );
266
+ // An unrelated client still reaches the publication inbound.
267
+ let unrelated = socket(PEER, "192.0.2.3:40105");
268
+ published_udp(&unrelated, &sip, "192.0.2.2:5070", "192.0.2.3:40105");
269
+ let listener = in_namespace(PEER, || TcpListener::bind("203.0.113.2:5060").unwrap());
270
+ let mut outbound = in_namespace(WORK, || connect_from("10.241.0.2:5070", "203.0.113.2:5060"))
271
+ .expect("symmetric tcp opening");
272
+ let (mut served, peer) = listener.accept().unwrap();
273
+ assert_eq!(peer.to_string(), "192.0.2.2:5070");
274
+ for stream in [&outbound, &served] {
275
+ stream
276
+ .set_read_timeout(Some(Duration::from_secs(1)))
277
+ .unwrap();
278
+ }
279
+ outbound.write_all(b"ping").unwrap();
280
+ served.read_exact(&mut [0; 4]).unwrap();
281
+ served.write_all(b"pong").unwrap();
282
+ outbound.read_exact(&mut [0; 4]).unwrap();
283
+ drop(outbound);
284
+ drop(served);
285
+ // Negative controls: a plain publication's workload port opens nothing, and a
286
+ // symmetric flow into the protected union stays denied from either port kind.
287
+ denied(&plain, &plain_peer, false);
288
+ denied(&sip, &protected_peer, false);
289
+ denied(&rtp, &protected_peer, false);
290
+ // Leased egress keeps its own leased source range.
291
+ let outer = roundtrip(&work_egress, &public);
292
+ assert_eq!(outer.ip().to_string(), "192.0.2.2");
293
+ assert!((10000..=10015).contains(&outer.port()));
294
+ in_namespace(ROUTER, move || {
295
+ let mut owner = router_owner;
296
+ assert!(owner.inspect().enforced);
297
+ owner.release(router_applied).unwrap();
298
+ });
299
+ in_namespace(HOST, move || {
300
+ let mut owner = host_owner;
301
+ assert!(owner.inspect().enforced);
302
+ owner.release(host_applied).unwrap();
303
+ });
304
+ dark_udp(&socket(PEER, "192.0.2.3:40106"), &middle, "192.0.2.2:20100");
305
+ println!("V2_PUBLISHED_RANGE_PROOF set_backed_graph_verified=true range_first_delivered=true range_middle_delivered=true range_last_delivered=true client_address_preserved=true range_outside_dark=true symmetric_range_source_port_kept=true symmetric_udp_source_port_kept=true symmetric_tcp_source_port_kept=true peer_request_on_symmetric_flow=true inbound_beside_symmetric=true plain_publication_outbound_denied=true symmetric_protected_denied=true leased_egress_unaffected=true protected_positive_control=true released_range_dark=true");
306
+ }
@@ -135,6 +135,8 @@ fn published_host_policy() -> egress::Policy {
135
135
  target_port,
136
136
  target_address: "10.240.0.2".into(),
137
137
  host_ip: "192.0.2.2".into(),
138
+ host_port_end: None,
139
+ symmetric: false,
138
140
  })
139
141
  .collect();
140
142
  policy