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}