Skip to main content

truffle_core/proxy/
mod.rs

1//! Reverse proxy subsystem.
2//!
3//! Exposes local HTTP/HTTPS ports to the Tailscale mesh network.
4//! The Go sidecar handles the actual HTTP reverse proxying (TLS listener,
5//! `httputil.ReverseProxy`, WebSocket hijack). This module provides the
6//! Rust API surface and mesh discovery.
7
8pub mod discovery;
9pub mod types;
10
11use std::collections::HashMap;
12use std::sync::Mutex;
13use tokio::sync::broadcast;
14
15pub use types::*;
16
17use crate::network::{NetworkProvider, ProxyAddParams};
18use crate::node::{Node, NodeError};
19
20/// Internal state for the proxy subsystem, stored in [`Node`].
21///
22/// **Limitation**: [`ProxyEvent::Error`] is currently only emitted during
23/// [`Proxy::add()`] and [`Proxy::remove()`] calls (synchronous failures).
24/// Post-start runtime errors from the sidecar (e.g. `SERVE_ERROR`,
25/// `CONNECTION_REFUSED`) are logged but *not* forwarded to `event_tx`.
26/// Addressing this requires a persistent background task that subscribes to
27/// sidecar proxy error events — tracked as a future enhancement.
28pub(crate) struct ProxyState {
29    /// Broadcast channel for proxy lifecycle events.
30    pub(crate) event_tx: broadcast::Sender<ProxyEvent>,
31    /// Currently active proxies (local to this node).
32    pub(crate) proxies: Mutex<HashMap<String, ProxyInfo>>,
33}
34
35impl ProxyState {
36    pub(crate) fn new() -> Self {
37        let (event_tx, _) = broadcast::channel(64);
38        Self {
39            event_tx,
40            proxies: Mutex::new(HashMap::new()),
41        }
42    }
43
44    /// Forward an error from the sidecar to the proxy event channel.
45    ///
46    /// Currently unused — see the limitation note on [`ProxyState`]. Provided
47    /// so that a future background error-forwarding task can call it without
48    /// reaching into `event_tx` directly.
49    #[allow(dead_code)]
50    pub(crate) fn emit_error(&self, id: String, code: String, message: String) {
51        let _ = self.event_tx.send(ProxyEvent::Error { id, code, message });
52    }
53}
54
55/// Handle for interacting with the reverse proxy subsystem.
56///
57/// Obtained via [`Node::proxy()`]. Follows the same pattern as
58/// [`FileTransfer`](crate::file_transfer::FileTransfer).
59pub struct Proxy<'a, N: NetworkProvider + 'static> {
60    #[allow(dead_code)]
61    node: &'a Node<N>,
62}
63
64impl<'a, N: NetworkProvider + 'static> Proxy<'a, N> {
65    pub(crate) fn new(node: &'a Node<N>) -> Self {
66        Self { node }
67    }
68
69    fn state(&self) -> &ProxyState {
70        &self.node.proxy_state
71    }
72
73    /// Subscribe to proxy lifecycle events.
74    pub fn subscribe(&self) -> broadcast::Receiver<ProxyEvent> {
75        self.state().event_tx.subscribe()
76    }
77
78    /// Get a snapshot of all locally active proxies.
79    pub fn list(&self) -> Vec<ProxyInfo> {
80        self.state()
81            .proxies
82            .lock()
83            .expect("proxy state poisoned")
84            .values()
85            .cloned()
86            .collect()
87    }
88
89    /// Add a reverse proxy that forwards traffic from a TLS port on the mesh
90    /// to a local target.
91    ///
92    /// Sends the `proxy:add` command to the sidecar and waits for confirmation.
93    /// On success, stores the proxy in local state and emits a
94    /// [`ProxyEvent::Started`] event.
95    pub async fn add(&self, config: ProxyConfig) -> Result<ProxyInfo, NodeError> {
96        let params = ProxyAddParams {
97            id: config.id.clone(),
98            name: config.name.clone(),
99            listen_port: config.listen_port,
100            target_host: config.target.host.clone(),
101            target_port: config.target.port,
102            target_scheme: config.target.scheme.clone(),
103        };
104
105        let result = self.node.network.proxy_add(params).await?;
106
107        let info = ProxyInfo {
108            id: result.id.clone(),
109            name: config.name,
110            listen_port: result.listen_port,
111            target: config.target,
112            url: result.url.clone(),
113            status: ProxyStatus::Running,
114        };
115
116        // Store in local state
117        self.state()
118            .proxies
119            .lock()
120            .expect("proxy state poisoned")
121            .insert(info.id.clone(), info.clone());
122
123        // Emit event
124        let _ = self.state().event_tx.send(ProxyEvent::Started {
125            id: result.id,
126            url: result.url,
127            listen_port: result.listen_port,
128        });
129
130        Ok(info)
131    }
132
133    /// Remove a reverse proxy by ID.
134    ///
135    /// Sends the `proxy:remove` command to the sidecar and waits for
136    /// confirmation. On success, removes the proxy from local state and
137    /// emits a [`ProxyEvent::Stopped`] event.
138    pub async fn remove(&self, id: &str) -> Result<(), NodeError> {
139        self.node.network.proxy_remove(id).await?;
140
141        // Remove from local state
142        self.state()
143            .proxies
144            .lock()
145            .expect("proxy state poisoned")
146            .remove(id);
147
148        // Emit event
149        let _ = self
150            .state()
151            .event_tx
152            .send(ProxyEvent::Stopped { id: id.to_string() });
153
154        Ok(())
155    }
156}