1#[cfg(any(test, target_os = "windows"))]
4use std::ffi::OsString;
5#[cfg(any(test, target_os = "windows"))]
6use std::path::PathBuf;
7use std::sync::Arc;
8use std::time::Duration;
9
10#[cfg(any(target_os = "linux", target_os = "macos"))]
11use rings_gateway::bindings::unix::UnixTunnelControl;
12#[cfg(any(target_os = "linux", target_os = "macos"))]
13use rings_gateway::bindings::unix::UnixTunnelOptions;
14use rings_gateway::bindings::EstablishedTunnel;
15#[cfg(target_os = "windows")]
16use rings_gateway::bindings::NativeTunnelControl;
17#[cfg(target_os = "windows")]
18use rings_gateway::bindings::NativeTunnelOptions;
19use rings_gateway::bindings::TeardownFailure;
20use rings_gateway::bindings::TunnelControl;
21#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
22use rings_gateway::bindings::UnsupportedTunnelControl;
23use rings_gateway::ExitAvailability;
24use rings_gateway::GatewayControlHandle;
25use rings_gateway::GatewayError;
26use rings_gateway::GatewayRuntime;
27use rings_gateway::GatewayStatusHandle;
28use tokio::sync::oneshot;
29
30use super::config::NativeGatewayConfig;
31use crate::onion::proxy::OnionProxyConfig;
32use crate::onion::tcp::NativeOnionCircuitHandle;
33use crate::onion::NativeOnionGatewayConnector;
34use crate::prelude::StopSource;
35use crate::prelude::StopToken;
36use crate::processor::Processor;
37#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
38use crate::util::expand_home;
39
40#[cfg(target_os = "windows")]
41type PlatformTunnelControl = NativeTunnelControl;
42#[cfg(any(target_os = "linux", target_os = "macos"))]
43type PlatformTunnelControl = UnixTunnelControl;
44#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
45type PlatformTunnelControl = UnsupportedTunnelControl;
46
47const MAX_STATUS_REFRESH_SECS: u64 = 30;
48
49pub struct NativeGatewayRunner {
51 processor: Arc<Processor>,
52 runtime: GatewayRuntime,
53 config: NativeGatewayConfig,
54}
55
56impl NativeGatewayRunner {
57 pub fn new(
59 processor: Arc<Processor>,
60 onion: NativeOnionCircuitHandle,
61 config: NativeGatewayConfig,
62 ) -> anyhow::Result<Self> {
63 validate_status_refresh_secs(config.status_refresh_secs)?;
64 config.runtime.validate()?;
65 let proxy = OnionProxyConfig::tcp_connect_service(
66 config.onion_service.clone(),
67 config.onion_hop_count,
68 config.onion_allow_short_paths,
69 )?;
70 let connector = Arc::new(NativeOnionGatewayConnector::new(
71 processor.clone(),
72 onion,
73 proxy,
74 ));
75 let runtime = GatewayRuntime::new(config.runtime.clone(), connector, rand::random())?;
76 Ok(Self {
77 processor,
78 runtime,
79 config,
80 })
81 }
82
83 pub fn status_handle(&self) -> GatewayStatusHandle {
85 self.runtime.status_handle()
86 }
87
88 pub async fn run(self, stop: StopToken) -> anyhow::Result<()> {
90 self.run_inner(stop, None).await
91 }
92
93 pub async fn run_with_startup_barrier(
95 self,
96 stop: StopToken,
97 started: oneshot::Sender<()>,
98 ) -> anyhow::Result<()> {
99 self.run_inner(stop, Some(started)).await
100 }
101
102 async fn run_inner(
103 mut self,
104 stop: StopToken,
105 started: Option<oneshot::Sender<()>>,
106 ) -> anyhow::Result<()> {
107 let mut control = self.platform_control()?;
108 let EstablishedTunnel {
109 mut device,
110 lease,
111 interface_name,
112 } = control.establish(&self.config.runtime.plan).await?;
113
114 if let Err(error) = self.runtime.activate(interface_name) {
115 let cleanup = control
116 .teardown(lease)
117 .await
118 .map_err(TeardownFailure::into_error);
119 let cleanup = finish_gateway_cleanup(&mut self.runtime, device, cleanup);
120 return combine_gateway_results(Err(error), Ok(()), cleanup);
121 }
122 if let Some(started) = started {
123 let _ = started.send(());
124 }
125
126 let runtime_done = StopSource::new();
127 let updater_failed = StopSource::new();
128 let update = refresh_exit_availability(GatewayRefresh {
129 processor: self.processor.clone(),
130 gateway: self.runtime.control_handle(),
131 onion_service: self.config.onion_service.as_str().to_string(),
132 interval: Duration::from_secs(self.config.status_refresh_secs),
133 stop: stop.clone(),
134 runtime_done: runtime_done.token(),
135 updater_failed: updater_failed.clone(),
136 });
137 let updater_failed_token = updater_failed.token();
138 let runtime_done_after_run = runtime_done.clone();
139 let run = async {
140 let result = self.runtime.run(&mut device, || {
141 stop.should_stop() || updater_failed_token.should_stop()
142 });
143 let result = result.await;
144 runtime_done_after_run.request_stop();
145 result
146 };
147 let (runtime_result, updater_result) = tokio::join!(run, update);
148
149 let cleanup_result = control
150 .teardown(lease)
151 .await
152 .map_err(TeardownFailure::into_error);
153 let cleanup_result = finish_gateway_cleanup(&mut self.runtime, device, cleanup_result);
154 combine_gateway_results(runtime_result, updater_result, cleanup_result)
155 }
156
157 #[cfg(target_os = "windows")]
158 fn platform_control(&self) -> anyhow::Result<PlatformTunnelControl> {
159 let ledger = expand_home(&self.config.route_ledger_path)?;
160 let mut options = NativeTunnelOptions::new(ledger);
161 if let Some(interface_name) = self.config.interface_name.clone() {
162 options = options.with_interface_name(interface_name);
163 }
164 let environment = std::env::var_os("RINGS_GATEWAY_WINTUN_DLL");
165 if let Some(path) =
166 select_wintun_dll_path(self.config.wintun_dll_path.as_deref(), environment)?
167 {
168 options = options.with_wintun_dll(path);
169 }
170 NativeTunnelControl::new(options).map_err(Into::into)
171 }
172
173 #[cfg(any(target_os = "linux", target_os = "macos"))]
174 fn platform_control(&self) -> anyhow::Result<PlatformTunnelControl> {
175 let socket_path = expand_home(&self.config.unix_helper_socket)?;
176 Ok(UnixTunnelControl::new(UnixTunnelOptions::new(socket_path)))
177 }
178
179 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
180 fn platform_control(&self) -> anyhow::Result<PlatformTunnelControl> {
181 Ok(UnsupportedTunnelControl::new())
182 }
183}
184
185fn validate_status_refresh_secs(seconds: u64) -> Result<(), GatewayError> {
186 if seconds == 0 || seconds > MAX_STATUS_REFRESH_SECS {
187 return Err(GatewayError::Platform {
188 operation: "validate-gateway-status-refresh",
189 message: format!(
190 "gateway status_refresh_secs must be in 1..={MAX_STATUS_REFRESH_SECS}, got {seconds}"
191 ),
192 });
193 }
194 Ok(())
195}
196
197fn finish_gateway_cleanup<D: rings_gateway::PacketIo>(
198 runtime: &mut GatewayRuntime,
199 device: D,
200 cleanup: Result<(), GatewayError>,
201) -> Result<(), GatewayError> {
202 match cleanup {
203 Ok(()) => {
204 drop(device);
205 Ok(())
206 }
207 Err(error) => {
208 let reason = format!(
212 "gateway route cleanup failed; the packet device remains open for selected routes \
213 until privileged cleanup succeeds: {error}"
214 );
215 runtime.set_exit_availability(ExitAvailability::Unknown, Some(reason.clone()));
216 tracing::error!("{reason}");
217 std::mem::forget(device);
218 Err(error)
219 }
220 }
221}
222
223#[cfg(any(test, target_os = "windows"))]
224fn select_wintun_dll_path(
225 configured: Option<&str>,
226 environment: Option<OsString>,
227) -> Result<Option<PathBuf>, crate::error::Error> {
228 let selected = configured.map(PathBuf::from).or_else(|| {
229 environment
230 .filter(|path| !path.is_empty())
231 .map(PathBuf::from)
232 });
233 selected.map(expand_home).transpose()
234}
235
236struct GatewayRefresh {
237 processor: Arc<Processor>,
238 gateway: GatewayControlHandle,
239 onion_service: String,
240 interval: Duration,
241 stop: StopToken,
242 runtime_done: StopToken,
243 updater_failed: StopSource,
244}
245
246enum RefreshWake {
247 Tick,
248 Stop,
249}
250
251async fn wait_for_refresh(
252 ticker: &mut tokio::time::Interval,
253 stop: &StopToken,
254 runtime_done: &StopToken,
255) -> RefreshWake {
256 tokio::select! {
257 _ = stop.stopped() => RefreshWake::Stop,
258 _ = runtime_done.stopped() => RefreshWake::Stop,
259 _ = ticker.tick() => RefreshWake::Tick,
260 }
261}
262
263async fn refresh_exit_availability(refresh: GatewayRefresh) -> Result<(), GatewayError> {
264 let mut ticker = tokio::time::interval(refresh.interval);
265 loop {
266 if matches!(
267 wait_for_refresh(&mut ticker, &refresh.stop, &refresh.runtime_done).await,
268 RefreshWake::Stop
269 ) {
270 return Ok(());
271 }
272 let (availability, reason) = match refresh
273 .processor
274 .lookup_onion_exits(&refresh.onion_service, false)
275 .await
276 {
277 Ok(exits) if exits.is_empty() => (
278 ExitAvailability::Unavailable,
279 Some(format!(
280 "no live Onion TCP exit advertises {}",
281 refresh.onion_service
282 )),
283 ),
284 Ok(_) => (ExitAvailability::Available, None),
285 Err(error) => (
286 ExitAvailability::Unknown,
287 Some(format!("Onion exit discovery failed: {error}")),
288 ),
289 };
290 if refresh
291 .gateway
292 .set_exit_availability(availability, reason)
293 .await
294 .is_err()
295 {
296 if refresh.runtime_done.should_stop() {
297 return Ok(());
298 }
299 refresh.updater_failed.request_stop();
300 return Err(GatewayError::Platform {
301 operation: "update-gateway-exit-availability",
302 message: "gateway runtime control channel closed".to_string(),
303 });
304 }
305 }
306}
307
308fn combine_gateway_results(
309 runtime: Result<(), GatewayError>,
310 updater: Result<(), GatewayError>,
311 cleanup: Result<(), GatewayError>,
312) -> anyhow::Result<()> {
313 match (runtime, updater, cleanup) {
314 (Ok(()), Ok(()), Ok(())) => Ok(()),
315 (runtime, updater, cleanup) => {
316 let failures = [
317 runtime.err().map(|error| format!("data plane: {error}")),
318 updater
319 .err()
320 .map(|error| format!("status updater: {error}")),
321 cleanup.err().map(|error| format!("cleanup: {error}")),
322 ]
323 .into_iter()
324 .flatten()
325 .collect::<Vec<_>>()
326 .join("; ");
327 Err(anyhow::anyhow!(failures))
328 }
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn configured_wintun_path_precedes_environment_fallback() {
338 let configured = select_wintun_dll_path(
339 Some("/configured/wintun.dll"),
340 Some(OsString::from("/environment/wintun.dll")),
341 )
342 .expect("select configured path");
343 assert_eq!(configured, Some(PathBuf::from("/configured/wintun.dll")));
344
345 let fallback =
346 select_wintun_dll_path(None, Some(OsString::from("/environment/wintun.dll")))
347 .expect("select environment path");
348 assert_eq!(fallback, Some(PathBuf::from("/environment/wintun.dll")));
349 }
350
351 #[test]
352 fn status_refresh_interval_is_bounded() {
353 assert!(validate_status_refresh_secs(0).is_err());
354 assert!(validate_status_refresh_secs(1).is_ok());
355 assert!(validate_status_refresh_secs(MAX_STATUS_REFRESH_SECS).is_ok());
356 assert!(validate_status_refresh_secs(MAX_STATUS_REFRESH_SECS + 1).is_err());
357 }
358
359 #[tokio::test]
360 async fn refresh_wait_wakes_immediately_when_stopped() {
361 let stop = StopSource::new();
362 let runtime_done = StopSource::new();
363 let mut ticker = tokio::time::interval(Duration::from_secs(3_600));
364 ticker.tick().await;
365 let stop_token = stop.token();
366 let runtime_done_token = runtime_done.token();
367 let wait = wait_for_refresh(&mut ticker, &stop_token, &runtime_done_token);
368 stop.request_stop();
369
370 let wake = tokio::time::timeout(Duration::from_secs(1), wait)
371 .await
372 .expect("stop wakes long refresh interval");
373 assert!(matches!(wake, RefreshWake::Stop));
374 }
375
376 #[test]
377 fn result_aggregation_preserves_all_failure_boundaries() {
378 let error = |operation| GatewayError::Platform {
379 operation,
380 message: "failed".to_string(),
381 };
382 let combined = combine_gateway_results(
383 Err(error("runtime")),
384 Err(error("status")),
385 Err(error("cleanup")),
386 )
387 .expect_err("three failures must remain visible")
388 .to_string();
389
390 assert!(combined.contains("data plane"));
391 assert!(combined.contains("status updater"));
392 assert!(combined.contains("cleanup"));
393 }
394}