1use std::collections::HashMap;
8use std::net::IpAddr;
9use std::path::PathBuf;
10use std::sync::Arc;
11use std::time::Duration;
12
13use tokio::net::TcpStream;
14use tokio::sync::{broadcast, mpsc, oneshot, Mutex, RwLock};
15use tokio::task::JoinHandle;
16
17use super::bridge::{Bridge, DIAL_TIMEOUT};
18use super::protocol::ProxyAddCommandData;
19use super::sidecar::{GoSidecar, SidecarConfig, SidecarInternalEvent};
20use crate::network::{
21 DialOpts, HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
22 NetworkTcpListener, NodeIdentity, PeerAddr, PingResult, ProxyAddParams, ProxyAddResult,
23 ProxyListEntry,
24};
25
26#[derive(Clone)]
28pub struct TailscaleConfig {
29 pub binary_path: PathBuf,
31 pub app_id: String,
35 pub device_id: String,
37 pub device_name: String,
40 pub hostname: String,
44 pub state_dir: String,
46 pub auth_key: Option<String>,
48 pub ephemeral: Option<bool>,
50 pub tags: Option<Vec<String>>,
52 pub idle_timeout_secs: Option<u64>,
55}
56
57impl std::fmt::Debug for TailscaleConfig {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_struct("TailscaleConfig")
62 .field("binary_path", &self.binary_path)
63 .field("app_id", &self.app_id)
64 .field("device_id", &self.device_id)
65 .field("device_name", &self.device_name)
66 .field("hostname", &self.hostname)
67 .field("state_dir", &self.state_dir)
68 .field("auth_key", &self.auth_key.as_ref().map(|_| "[REDACTED]"))
69 .field("ephemeral", &self.ephemeral)
70 .field("tags", &self.tags)
71 .field("idle_timeout_secs", &self.idle_timeout_secs)
72 .finish()
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78enum ProviderState {
79 Stopped,
80 Starting,
81 Running,
82 Stopping,
83}
84
85pub struct TailscaleProvider {
96 config: TailscaleConfig,
97 state: Arc<RwLock<ProviderState>>,
98
99 identity: Arc<std::sync::RwLock<NodeIdentity>>,
104 local_addr: Arc<std::sync::RwLock<PeerAddr>>,
109
110 peers: Arc<RwLock<HashMap<String, NetworkPeer>>>,
112
113 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
115
116 health: Arc<RwLock<HealthInfo>>,
118
119 sidecar: Arc<Mutex<Option<GoSidecar>>>,
121
122 bridge: Arc<Mutex<Option<Arc<Bridge>>>>,
124
125 bridge_shutdown_tx: Arc<Mutex<Option<tokio::sync::watch::Sender<bool>>>>,
127
128 session_token: Arc<RwLock<[u8; 32]>>,
130
131 local_tailscale_id: Arc<std::sync::RwLock<Option<String>>>,
137}
138
139impl TailscaleProvider {
140 pub fn new(config: TailscaleConfig) -> Self {
144 let (peer_event_tx, _) = broadcast::channel(256);
145
146 let initial_identity = NodeIdentity {
150 app_id: config.app_id.clone(),
151 device_id: config.device_id.clone(),
152 device_name: config.device_name.clone(),
153 tailscale_hostname: config.hostname.clone(),
154 tailscale_id: String::new(),
155 dns_name: None,
156 ip: None,
157 };
158
159 Self {
160 config,
161 state: Arc::new(RwLock::new(ProviderState::Stopped)),
162 identity: Arc::new(std::sync::RwLock::new(initial_identity)),
163 local_addr: Arc::new(std::sync::RwLock::new(PeerAddr::default())),
164 peers: Arc::new(RwLock::new(HashMap::new())),
165 peer_event_tx,
166 health: Arc::new(RwLock::new(HealthInfo {
167 state: "stopped".to_string(),
168 healthy: false,
169 ..Default::default()
170 })),
171 sidecar: Arc::new(Mutex::new(None)),
172 bridge: Arc::new(Mutex::new(None)),
173 bridge_shutdown_tx: Arc::new(Mutex::new(None)),
174 session_token: Arc::new(RwLock::new([0u8; 32])),
175 local_tailscale_id: Arc::new(std::sync::RwLock::new(None)),
176 }
177 }
178
179 fn generate_session_token() -> Result<[u8; 32], NetworkError> {
181 let mut token = [0u8; 32];
182 getrandom::getrandom(&mut token).map_err(|e| {
183 NetworkError::Internal(format!("failed to generate session token: {e}"))
184 })?;
185 Ok(token)
186 }
187
188 fn sidecar_peer_to_network_peer(peer: &super::protocol::SidecarPeer) -> NetworkPeer {
190 let ip = peer
191 .tailscale_ips
192 .first()
193 .and_then(|s| s.parse::<IpAddr>().ok())
194 .unwrap_or(IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED));
195
196 NetworkPeer {
197 id: peer.id.clone(),
198 hostname: peer.hostname.clone(),
199 ip,
200 online: peer.online,
201 cur_addr: if peer.cur_addr.is_empty() {
202 None
203 } else {
204 Some(peer.cur_addr.clone())
205 },
206 relay: if peer.relay.is_empty() {
207 None
208 } else {
209 Some(peer.relay.clone())
210 },
211 os: if peer.os.is_empty() {
212 None
213 } else {
214 Some(peer.os.clone())
215 },
216 last_seen: peer.last_seen.clone(),
217 key_expiry: peer.key_expiry.clone(),
218 dns_name: Some(peer.dns_name.clone()),
219 }
220 }
221
222 #[allow(clippy::too_many_arguments)]
225 fn spawn_event_processor(
226 mut sidecar_rx: broadcast::Receiver<SidecarInternalEvent>,
227 peers: Arc<RwLock<HashMap<String, NetworkPeer>>>,
228 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
229 health: Arc<RwLock<HealthInfo>>,
230 identity: Arc<std::sync::RwLock<NodeIdentity>>,
231 local_addr: Arc<std::sync::RwLock<PeerAddr>>,
232 local_tailscale_id: Arc<std::sync::RwLock<Option<String>>>,
233 state: Arc<RwLock<ProviderState>>,
234 started_tx: Option<oneshot::Sender<Result<(), NetworkError>>>,
235 app_id: String,
236 ) {
237 tokio::spawn(async move {
238 let mut started_tx = started_tx;
239
240 loop {
241 match sidecar_rx.recv().await {
242 Ok(event) => {
243 match event {
244 SidecarInternalEvent::Started {
245 hostname,
246 dns_name,
247 tailscale_ip,
248 node_id,
249 } => {
250 let ip: Option<IpAddr> = tailscale_ip.parse().ok();
251
252 {
253 let mut id = identity.write().unwrap();
254 id.tailscale_hostname = hostname.clone();
260 id.dns_name = Some(dns_name.clone());
261 id.ip = ip;
262 if !node_id.is_empty() {
263 id.tailscale_id = node_id.clone();
264 }
265 }
266
267 if !node_id.is_empty() {
270 *local_tailscale_id.write().unwrap() = Some(node_id);
271 }
272
273 {
274 let mut addr = local_addr.write().unwrap();
275 addr.hostname = hostname;
276 addr.dns_name = Some(dns_name);
277 addr.ip = ip;
278 }
279
280 {
281 let mut h = health.write().await;
282 h.state = "running".to_string();
283 h.healthy = true;
284 }
285
286 *state.write().await = ProviderState::Running;
287
288 if let Some(tx) = started_tx.take() {
290 let _ = tx.send(Ok(()));
291 }
292 }
293 SidecarInternalEvent::AuthRequired { auth_url } => {
294 tracing::info!("tailscale auth required: {auth_url}");
295 let _ = peer_event_tx
298 .send(NetworkPeerEvent::AuthRequired { url: auth_url });
299 }
300 SidecarInternalEvent::Stopped => {
301 *state.write().await = ProviderState::Stopped;
302 let mut h = health.write().await;
303 h.state = "stopped".to_string();
304 h.healthy = false;
305 tracing::info!("tailscale provider stopped");
306 return;
307 }
308 SidecarInternalEvent::StateChange { state: new_state } => {
309 let mut h = health.write().await;
310 h.state = new_state;
311 }
312 SidecarInternalEvent::KeyExpiring { expires_at } => {
313 let mut h = health.write().await;
314 h.key_expiry = Some(expires_at);
315 }
316 SidecarInternalEvent::HealthWarning { warnings } => {
317 let mut h = health.write().await;
318 h.warnings = warnings;
319 h.healthy = h.warnings.is_empty();
320 }
321 SidecarInternalEvent::PeersReceived(sidecar_peers) => {
322 let mut peer_map = peers.write().await;
323 let self_id = local_tailscale_id.read().unwrap().clone();
329 let new_peers: HashMap<String, NetworkPeer> = sidecar_peers
332 .iter()
333 .filter(|p| {
334 if let Some(ref me) = self_id {
335 if p.id == *me {
336 return false;
337 }
338 }
339 is_app_peer(&p.hostname, &app_id)
340 })
341 .map(|p| {
342 let np = Self::sidecar_peer_to_network_peer(p);
343 (np.id.clone(), np)
344 })
345 .collect();
346
347 for (id, new_peer) in &new_peers {
349 if let Some(_existing) = peer_map.get(id) {
350 let _ = peer_event_tx
351 .send(NetworkPeerEvent::Updated(new_peer.clone()));
352 } else {
353 let _ = peer_event_tx
354 .send(NetworkPeerEvent::Joined(new_peer.clone()));
355 }
356 }
357 for id in peer_map.keys() {
358 if !new_peers.contains_key(id) {
359 let _ =
360 peer_event_tx.send(NetworkPeerEvent::Left(id.clone()));
361 }
362 }
363
364 *peer_map = new_peers;
365 }
366 SidecarInternalEvent::PeerChanged(change) => {
367 let mut peer_map = peers.write().await;
368 let self_id = local_tailscale_id.read().unwrap().clone();
371 match change.change_type.as_str() {
372 "joined" => {
373 if let Some(p) = change.peer {
374 if let Some(ref me) = self_id {
375 if p.id == *me {
376 continue;
377 }
378 }
379 if is_app_peer(&p.hostname, &app_id) {
380 let np = Self::sidecar_peer_to_network_peer(&p);
381 peer_map.insert(np.id.clone(), np.clone());
382 let _ = peer_event_tx
383 .send(NetworkPeerEvent::Joined(np));
384 }
385 }
386 }
387 "left" => {
388 if peer_map.remove(&change.peer_id).is_some() {
389 let _ = peer_event_tx
390 .send(NetworkPeerEvent::Left(change.peer_id));
391 }
392 }
393 "updated" => {
394 if let Some(p) = change.peer {
395 if let Some(ref me) = self_id {
396 if p.id == *me {
397 continue;
398 }
399 }
400 if is_app_peer(&p.hostname, &app_id) {
401 let np = Self::sidecar_peer_to_network_peer(&p);
402 peer_map.insert(np.id.clone(), np.clone());
403 let _ = peer_event_tx
404 .send(NetworkPeerEvent::Updated(np));
405 }
406 }
407 }
408 other => {
409 tracing::warn!("unknown peer change type: {other}");
410 }
411 }
412 }
413 SidecarInternalEvent::Error { code, message } => {
414 tracing::error!("sidecar error [{code}]: {message}");
415 if let Some(tx) = started_tx.take() {
417 let _ = tx.send(Err(NetworkError::SidecarError(format!(
418 "[{code}] {message}"
419 ))));
420 }
421 }
422 SidecarInternalEvent::ProcessExited { exit_code } => {
423 tracing::error!("sidecar process exited: {exit_code:?}");
424 *state.write().await = ProviderState::Stopped;
425 let mut h = health.write().await;
426 h.state = "crashed".to_string();
427 h.healthy = false;
428 if let Some(tx) = started_tx.take() {
429 let _ = tx.send(Err(NetworkError::SidecarError(format!(
430 "process exited with code {exit_code:?}"
431 ))));
432 }
433 return;
434 }
435 _ => {}
438 }
439 }
440 Err(broadcast::error::RecvError::Lagged(n)) => {
441 tracing::warn!("event processor lagged by {n} events");
442 }
443 Err(broadcast::error::RecvError::Closed) => {
444 tracing::info!("sidecar event channel closed, stopping event processor");
445 return;
446 }
447 }
448 }
449 });
450 }
451}
452
453pub(crate) fn is_app_peer(hostname: &str, app_id: &str) -> bool {
463 let prefix = format!("truffle-{app_id}-");
464 hostname.len() > prefix.len() && hostname.starts_with(&prefix)
465}
466
467impl super::super::NetworkProvider for TailscaleProvider {
468 async fn start(&mut self) -> Result<(), NetworkError> {
469 {
470 let current_state = *self.state.read().await;
471 if current_state != ProviderState::Stopped {
472 return Err(NetworkError::AlreadyRunning);
473 }
474 }
475 *self.state.write().await = ProviderState::Starting;
476
477 let token = Self::generate_session_token()?;
479 let token_hex = hex::encode(token);
480 *self.session_token.write().await = token;
481
482 let bridge = Bridge::bind(token).await?;
484 let bridge_port = bridge.local_port()?;
485 let bridge = Arc::new(bridge);
486
487 let (bridge_shutdown_tx, bridge_shutdown_rx) = tokio::sync::watch::channel(false);
489
490 {
492 let bridge_clone = bridge.clone();
493 tokio::spawn(async move {
494 bridge_clone.run(bridge_shutdown_rx).await;
495 });
496 }
497
498 *self.bridge.lock().await = Some(bridge.clone());
499 *self.bridge_shutdown_tx.lock().await = Some(bridge_shutdown_tx);
500
501 let sidecar_config = SidecarConfig {
503 binary_path: self.config.binary_path.clone(),
504 hostname: self.config.hostname.clone(),
505 state_dir: self.config.state_dir.clone(),
506 auth_key: self.config.auth_key.clone(),
507 bridge_port,
508 session_token_hex: token_hex,
509 ephemeral: self.config.ephemeral,
510 tags: self.config.tags.clone(),
511 idle_timeout_secs: self.config.idle_timeout_secs,
512 };
513
514 let (sidecar, sidecar_rx) = GoSidecar::spawn(sidecar_config.clone()).await?;
516
517 let (started_tx, started_rx) = oneshot::channel();
519
520 Self::spawn_event_processor(
522 sidecar_rx,
523 self.peers.clone(),
524 self.peer_event_tx.clone(),
525 self.health.clone(),
526 self.identity.clone(),
527 self.local_addr.clone(),
528 self.local_tailscale_id.clone(),
529 self.state.clone(),
530 Some(started_tx),
531 self.config.app_id.clone(),
532 );
533
534 sidecar.send_start(&sidecar_config).await?;
536
537 *self.sidecar.lock().await = Some(sidecar);
538
539 let auth_timeout = Duration::from_secs(300);
543 let result = tokio::time::timeout(auth_timeout, started_rx)
544 .await
545 .map_err(|_| {
546 NetworkError::StartFailed(
547 "timed out waiting for authentication (5 min). \
548 Subscribe to peer_events() to display auth URLs."
549 .into(),
550 )
551 })?
552 .map_err(|_| NetworkError::StartFailed("start signal channel dropped".into()))?;
553
554 match result {
555 Ok(()) => {
556 if let Some(ref sidecar) = *self.sidecar.lock().await {
558 let _ = sidecar.send_get_peers().await;
559 let _ = sidecar.send_watch_peers().await;
561 }
562 tracing::info!("tailscale provider started successfully");
563 Ok(())
564 }
565 Err(e) => {
566 *self.state.write().await = ProviderState::Stopped;
567 Err(e)
568 }
569 }
570 }
571
572 async fn stop(&self) -> Result<(), NetworkError> {
573 *self.state.write().await = ProviderState::Stopping;
574
575 if let Some(sidecar) = self.sidecar.lock().await.take() {
577 sidecar.shutdown().await;
578 }
579
580 if let Some(tx) = self.bridge_shutdown_tx.lock().await.take() {
582 let _ = tx.send(true);
583 }
584 *self.bridge.lock().await = None;
585
586 self.peers.write().await.clear();
588 *self.state.write().await = ProviderState::Stopped;
589 let mut h = self.health.write().await;
590 h.state = "stopped".to_string();
591 h.healthy = false;
592
593 tracing::info!("tailscale provider stopped");
594 Ok(())
595 }
596
597 fn local_identity(&self) -> NodeIdentity {
598 self.identity.read().unwrap().clone()
599 }
600
601 fn local_addr(&self) -> PeerAddr {
602 self.local_addr.read().unwrap().clone()
603 }
604
605 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
606 self.peer_event_tx.subscribe()
607 }
608
609 async fn peers(&self) -> Vec<NetworkPeer> {
610 self.peers.read().await.values().cloned().collect()
611 }
612
613 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
614 self.dial_tcp_opts(addr, port, DialOpts::default()).await
615 }
616
617 async fn dial_tcp_opts(
618 &self,
619 addr: &str,
620 port: u16,
621 opts: DialOpts,
622 ) -> Result<TcpStream, NetworkError> {
623 if *self.state.read().await != ProviderState::Running {
624 return Err(NetworkError::NotRunning);
625 }
626
627 let bridge = self
628 .bridge
629 .lock()
630 .await
631 .clone()
632 .ok_or(NetworkError::NotRunning)?;
633
634 let request_id = uuid::Uuid::new_v4().to_string();
636
637 let dial_rx = bridge.register_dial(request_id.clone()).await;
639
640 let event_rx = {
642 let sidecar_guard = self.sidecar.lock().await;
643 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
644
645 let event_rx = sidecar.subscribe();
646
647 sidecar
648 .send_dial(request_id.clone(), addr.to_string(), port, opts.tls)
649 .await?;
650
651 event_rx
652 };
653
654 Self::await_dial_result(&bridge, &request_id, dial_rx, event_rx, DIAL_TIMEOUT).await
659 }
660
661 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
662 if *self.state.read().await != ProviderState::Running {
663 return Err(NetworkError::NotRunning);
664 }
665
666 let bridge = self
667 .bridge
668 .lock()
669 .await
670 .clone()
671 .ok_or(NetworkError::NotRunning)?;
672
673 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
675
676 let mut event_rx = {
678 let sidecar_guard = self.sidecar.lock().await;
679 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
680
681 let event_rx = sidecar.subscribe();
682 sidecar.send_listen(port, None).await?;
683 event_rx
684 };
685
686 let actual_port = tokio::time::timeout(Duration::from_secs(10), async {
690 loop {
691 match event_rx.recv().await {
692 Ok(SidecarInternalEvent::Listening { port: p }) if port == 0 || p == port => {
693 return Ok(p);
694 }
695 Ok(SidecarInternalEvent::Error { code, message }) => {
696 return Err(NetworkError::ListenFailed(format!("[{code}] {message}")));
697 }
698 Err(broadcast::error::RecvError::Closed) => {
699 return Err(NetworkError::SidecarError("event channel closed".into()));
700 }
701 _ => continue,
702 }
703 }
704 })
705 .await
706 .map_err(|_| NetworkError::ListenFailed("listen confirmation timed out".into()))??;
707
708 bridge.register_listener(actual_port, tx).await;
710
711 Ok(NetworkTcpListener {
712 port: actual_port,
713 incoming: rx,
714 })
715 }
716
717 async fn unlisten_tcp(&self, port: u16) -> Result<(), NetworkError> {
718 if *self.state.read().await != ProviderState::Running {
719 return Err(NetworkError::NotRunning);
720 }
721
722 let bridge = self
723 .bridge
724 .lock()
725 .await
726 .clone()
727 .ok_or(NetworkError::NotRunning)?;
728
729 bridge.remove_listener(port).await;
731
732 {
734 let sidecar_guard = self.sidecar.lock().await;
735 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
736 sidecar.send_unlisten(port).await?;
737 }
738
739 Ok(())
740 }
741
742 async fn ping(&self, addr: &str) -> Result<PingResult, NetworkError> {
743 if *self.state.read().await != ProviderState::Running {
744 return Err(NetworkError::NotRunning);
745 }
746
747 let target = addr.to_string();
748
749 let mut event_rx = {
751 let sidecar_guard = self.sidecar.lock().await;
752 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
753
754 let event_rx = sidecar.subscribe();
755 sidecar.send_ping(target.clone(), None).await?;
756 event_rx
757 };
758
759 let result = tokio::time::timeout(Duration::from_secs(15), async {
761 loop {
762 match event_rx.recv().await {
763 Ok(SidecarInternalEvent::PingResult(data)) if data.target == target => {
764 if !data.error.is_empty() {
765 return Err(NetworkError::PingFailed(data.error));
766 }
767 let connection = if data.direct {
768 "direct".to_string()
769 } else if !data.relay.is_empty() {
770 format!("relay:{}", data.relay)
771 } else {
772 "unknown".to_string()
773 };
774 return Ok(PingResult {
775 latency: Duration::from_secs_f64(data.latency_ms / 1000.0),
776 connection,
777 peer_addr: if data.peer_addr.is_empty() {
778 None
779 } else {
780 Some(data.peer_addr)
781 },
782 });
783 }
784 Err(broadcast::error::RecvError::Closed) => {
785 return Err(NetworkError::SidecarError("event channel closed".into()));
786 }
787 _ => continue,
788 }
789 }
790 })
791 .await
792 .map_err(|_| NetworkError::PingFailed("ping timed out".into()))?;
793
794 result
795 }
796
797 async fn bind_udp(&self, port: u16) -> Result<super::super::NetworkUdpSocket, NetworkError> {
798 if *self.state.read().await != ProviderState::Running {
799 return Err(NetworkError::NotRunning);
800 }
801
802 let mut event_rx = {
804 let sidecar_guard = self.sidecar.lock().await;
805 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
806
807 let event_rx = sidecar.subscribe();
808 sidecar.send_listen_packet(port).await?;
809 event_rx
810 };
811
812 let local_port = tokio::time::timeout(Duration::from_secs(10), async {
814 loop {
815 match event_rx.recv().await {
816 Ok(SidecarInternalEvent::ListeningPacket {
817 port: p,
818 local_port,
819 }) if p == port => {
820 return Ok(local_port);
821 }
822 Ok(SidecarInternalEvent::Error { code, message }) => {
823 return Err(NetworkError::ListenFailed(format!(
824 "UDP bind failed [{code}] {message}"
825 )));
826 }
827 Err(broadcast::error::RecvError::Closed) => {
828 return Err(NetworkError::SidecarError("event channel closed".into()));
829 }
830 _ => continue,
831 }
832 }
833 })
834 .await
835 .map_err(|_| {
836 NetworkError::ListenFailed("UDP listenPacket confirmation timed out".into())
837 })??;
838
839 let local_socket = tokio::net::UdpSocket::bind("127.0.0.1:0")
841 .await
842 .map_err(|e| NetworkError::Internal(format!("failed to bind local UDP socket: {e}")))?;
843
844 local_socket
845 .connect(format!("127.0.0.1:{local_port}"))
846 .await
847 .map_err(|e| {
848 NetworkError::Internal(format!("failed to connect local UDP socket to relay: {e}"))
849 })?;
850
851 let rust_local_addr = local_socket
852 .local_addr()
853 .map_err(|e| NetworkError::Internal(format!("failed to get local UDP addr: {e}")))?;
854
855 local_socket
860 .send(b"TRUFFLE_UDP_REGISTER")
861 .await
862 .map_err(|e| NetworkError::Internal(format!("failed to send UDP registration: {e}")))?;
863
864 tracing::info!(
865 tsnet_port = port,
866 relay_port = local_port,
867 rust_local_addr = %rust_local_addr,
868 "UDP socket bound via tsnet relay (registered)"
869 );
870
871 Ok(super::super::NetworkUdpSocket::new(local_socket, port))
872 }
873
874 async fn health(&self) -> HealthInfo {
875 self.health.read().await.clone()
876 }
877
878 async fn proxy_add(&self, config: ProxyAddParams) -> Result<ProxyAddResult, NetworkError> {
881 if *self.state.read().await != ProviderState::Running {
882 return Err(NetworkError::NotRunning);
883 }
884
885 let mut event_rx = {
887 let sidecar_guard = self.sidecar.lock().await;
888 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
889 let event_rx = sidecar.subscribe();
890 sidecar
891 .send_proxy_add(ProxyAddCommandData {
892 id: config.id.clone(),
893 name: config.name.clone(),
894 listen_port: config.listen_port,
895 target_host: config.target_host.clone(),
896 target_port: config.target_port,
897 target_scheme: config.target_scheme.clone(),
898 })
899 .await?;
900 event_rx
901 };
902
903 tokio::time::timeout(Duration::from_secs(10), async {
905 loop {
906 match event_rx.recv().await {
907 Ok(SidecarInternalEvent::ProxyAdded {
908 id,
909 listen_port,
910 url,
911 }) if id == config.id => {
912 return Ok(ProxyAddResult {
913 id,
914 listen_port,
915 url,
916 });
917 }
918 Ok(SidecarInternalEvent::ProxyError { id, code, message })
919 if id == config.id =>
920 {
921 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
922 }
923 Ok(SidecarInternalEvent::Error { code, message }) => {
924 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
925 }
926 Err(broadcast::error::RecvError::Closed) => {
927 return Err(NetworkError::SidecarError("event channel closed".into()));
928 }
929 Err(broadcast::error::RecvError::Lagged(_)) => {
930 return Err(NetworkError::SidecarError(
931 "event channel lagged: proxy confirmation may have been lost".into(),
932 ));
933 }
934 Ok(_) => continue,
935 }
936 }
937 })
938 .await
939 .map_err(|_| NetworkError::ProxyError("proxy add timed out".into()))?
940 }
941
942 async fn proxy_remove(&self, id: &str) -> Result<(), NetworkError> {
943 if *self.state.read().await != ProviderState::Running {
944 return Err(NetworkError::NotRunning);
945 }
946
947 let target_id = id.to_string();
948
949 let mut event_rx = {
951 let sidecar_guard = self.sidecar.lock().await;
952 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
953 let event_rx = sidecar.subscribe();
954 sidecar.send_proxy_remove(id).await?;
955 event_rx
956 };
957
958 tokio::time::timeout(Duration::from_secs(10), async {
960 loop {
961 match event_rx.recv().await {
962 Ok(SidecarInternalEvent::ProxyRemoved { id }) if id == target_id => {
963 return Ok(());
964 }
965 Ok(SidecarInternalEvent::ProxyError { id, code, message })
966 if id == target_id =>
967 {
968 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
969 }
970 Ok(SidecarInternalEvent::Error { code, message }) => {
971 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
972 }
973 Err(broadcast::error::RecvError::Closed) => {
974 return Err(NetworkError::SidecarError("event channel closed".into()));
975 }
976 Err(broadcast::error::RecvError::Lagged(_)) => {
977 return Err(NetworkError::SidecarError(
978 "event channel lagged: proxy confirmation may have been lost".into(),
979 ));
980 }
981 Ok(_) => continue,
982 }
983 }
984 })
985 .await
986 .map_err(|_| NetworkError::ProxyError("proxy remove timed out".into()))?
987 }
988
989 async fn proxy_list(&self) -> Result<Vec<ProxyListEntry>, NetworkError> {
990 if *self.state.read().await != ProviderState::Running {
991 return Err(NetworkError::NotRunning);
992 }
993
994 let mut event_rx = {
996 let sidecar_guard = self.sidecar.lock().await;
997 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
998 let event_rx = sidecar.subscribe();
999 sidecar.send_proxy_list().await?;
1000 event_rx
1001 };
1002
1003 tokio::time::timeout(Duration::from_secs(10), async {
1005 loop {
1006 match event_rx.recv().await {
1007 Ok(SidecarInternalEvent::ProxyList { proxies }) => {
1008 return Ok(proxies
1009 .into_iter()
1010 .map(|p| ProxyListEntry {
1011 id: p.id,
1012 name: p.name,
1013 listen_port: p.listen_port,
1014 target_host: p.target_host,
1015 target_port: p.target_port,
1016 target_scheme: p.target_scheme,
1017 url: p.url,
1018 })
1019 .collect());
1020 }
1021 Ok(SidecarInternalEvent::Error { code, message }) => {
1022 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
1023 }
1024 Err(broadcast::error::RecvError::Closed) => {
1025 return Err(NetworkError::SidecarError("event channel closed".into()));
1026 }
1027 Err(broadcast::error::RecvError::Lagged(_)) => {
1028 return Err(NetworkError::SidecarError(
1029 "event channel lagged: proxy confirmation may have been lost".into(),
1030 ));
1031 }
1032 Ok(_) => continue,
1033 }
1034 }
1035 })
1036 .await
1037 .map_err(|_| NetworkError::ProxyError("proxy list timed out".into()))?
1038 }
1039}
1040
1041impl TailscaleProvider {
1042 pub async fn local_identity_async(&self) -> NodeIdentity {
1047 self.identity.read().unwrap().clone()
1048 }
1049
1050 pub async fn local_addr_async(&self) -> PeerAddr {
1055 self.local_addr.read().unwrap().clone()
1056 }
1057
1058 pub(super) async fn await_dial_result(
1068 bridge: &Bridge,
1069 request_id: &str,
1070 dial_rx: oneshot::Receiver<TcpStream>,
1071 mut event_rx: broadcast::Receiver<SidecarInternalEvent>,
1072 timeout: Duration,
1073 ) -> Result<TcpStream, NetworkError> {
1074 let fail_request_id = request_id.to_string();
1079 let (fail_tx, fail_rx) = oneshot::channel::<String>();
1080 let fail_watcher: JoinHandle<()> = tokio::spawn(async move {
1081 loop {
1082 match event_rx.recv().await {
1083 Ok(SidecarInternalEvent::DialFailed {
1084 request_id: rid,
1085 error,
1086 }) if rid == fail_request_id => {
1087 let _ = fail_tx.send(error);
1088 return;
1089 }
1090 Err(broadcast::error::RecvError::Closed) => {
1091 let _ = fail_tx.send("event channel closed".to_string());
1092 return;
1093 }
1094 _ => continue,
1095 }
1096 }
1097 });
1098
1099 let result = match tokio::time::timeout(timeout, async {
1100 tokio::select! {
1101 stream_result = dial_rx => {
1102 stream_result.map_err(|_| NetworkError::DialFailed("dial cancelled".into()))
1103 }
1104 fail_result = fail_rx => {
1105 let error = fail_result.unwrap_or_else(|_| "dial watcher dropped".to_string());
1106 Err(NetworkError::DialFailed(error))
1107 }
1108 }
1109 })
1110 .await
1111 {
1112 Ok(result) => result,
1113 Err(_) => Err(NetworkError::DialTimeout(timeout)),
1114 };
1115
1116 fail_watcher.abort();
1118
1119 if result.is_err() {
1121 bridge.remove_dial(request_id).await;
1122 }
1123
1124 result
1125 }
1126}
1127
1128#[cfg(test)]
1129mod config_debug_tests {
1130 use super::*;
1131
1132 #[test]
1133 fn tailscale_config_debug_redacts_auth_key() {
1134 let config = TailscaleConfig {
1135 binary_path: PathBuf::from("/opt/sidecar"),
1136 app_id: "demo".to_string(),
1137 device_id: "01JZZZZZZZZZZZZZZZZZZZZZZZ".to_string(),
1138 device_name: "dev".to_string(),
1139 hostname: "truffle-demo-dev".to_string(),
1140 state_dir: "/tmp/state".to_string(),
1141 auth_key: Some("dummy-auth-SECRET123".to_string()),
1142 ephemeral: None,
1143 tags: None,
1144 idle_timeout_secs: None,
1145 };
1146 let dbg = format!("{config:?}");
1147 assert!(!dbg.contains("SECRET123"));
1148 assert!(dbg.contains("[REDACTED]"));
1149 }
1150}