Skip to main content

rings_node/native/
gateway.rs

1//! Foreground native TUN gateway supervision.
2
3#[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
49/// Prepared foreground gateway with a status capability available before platform setup.
50pub struct NativeGatewayRunner {
51    processor: Arc<Processor>,
52    runtime: GatewayRuntime,
53    config: NativeGatewayConfig,
54}
55
56impl NativeGatewayRunner {
57    /// Build the pure runtime and Onion connector without changing host network state.
58    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    /// Return process-independent status for the native HTTP inspection endpoint.
84    pub fn status_handle(&self) -> GatewayStatusHandle {
85        self.runtime.status_handle()
86    }
87
88    /// Establish the packet interface and explicit routes, run, then reconcile the lease.
89    pub async fn run(self, stop: StopToken) -> anyhow::Result<()> {
90        self.run_inner(stop, None).await
91    }
92
93    /// Run the gateway and publish when its explicitly selected packet ingress is active.
94    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            // An explicitly selected route may remain installed. Retaining the packet descriptor
209            // keeps that selected traffic fail-closed until privileged cleanup succeeds; unrelated
210            // host traffic remains outside the gateway's route authority.
211            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}