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};
15
16use super::bridge::{Bridge, DIAL_TIMEOUT};
17use super::protocol::{
18 PingResultEventData, ProxyAddCommandData, ProxyInfoEventData, WhoisResultEventData,
19};
20use super::sidecar::{GoSidecar, ReplyGuard, SidecarConfig, SidecarInternalEvent};
21use crate::network::{
22 DialOpts, HealthInfo, IncomingConnection, ListenOpts, NetworkError, NetworkPeer,
23 NetworkPeerEvent, NetworkTcpListener, NodeIdentity, PeerAddr, PingResult, ProxyAddParams,
24 ProxyAddResult, ProxyListEntry, ProxyRuntimeError,
25};
26
27#[derive(Clone)]
29pub struct TailscaleConfig {
30 pub binary_path: PathBuf,
32 pub app_id: String,
36 pub device_id: String,
38 pub device_name: String,
41 pub hostname: String,
45 pub state_dir: String,
47 pub auth_key: Option<String>,
49 pub ephemeral: Option<bool>,
51 pub tags: Option<Vec<String>>,
53 pub idle_timeout_secs: Option<u64>,
56}
57
58impl std::fmt::Debug for TailscaleConfig {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.debug_struct("TailscaleConfig")
63 .field("binary_path", &self.binary_path)
64 .field("app_id", &self.app_id)
65 .field("device_id", &self.device_id)
66 .field("device_name", &self.device_name)
67 .field("hostname", &self.hostname)
68 .field("state_dir", &self.state_dir)
69 .field("auth_key", &self.auth_key.as_ref().map(|_| "[REDACTED]"))
70 .field("ephemeral", &self.ephemeral)
71 .field("tags", &self.tags)
72 .field("idle_timeout_secs", &self.idle_timeout_secs)
73 .finish()
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79enum ProviderState {
80 Stopped,
81 Starting,
82 Running,
83 Stopping,
84}
85
86pub struct TailscaleProvider {
97 config: TailscaleConfig,
98 state: Arc<RwLock<ProviderState>>,
99
100 identity: Arc<std::sync::RwLock<NodeIdentity>>,
105 local_addr: Arc<std::sync::RwLock<PeerAddr>>,
110
111 peers: Arc<RwLock<HashMap<String, NetworkPeer>>>,
113
114 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
116
117 health: Arc<RwLock<HealthInfo>>,
119
120 sidecar: Arc<Mutex<Option<GoSidecar>>>,
122
123 bridge: Arc<Mutex<Option<Arc<Bridge>>>>,
125
126 bridge_shutdown_tx: Arc<Mutex<Option<tokio::sync::watch::Sender<bool>>>>,
128
129 session_token: Arc<RwLock<[u8; 32]>>,
131
132 local_tailscale_id: Arc<std::sync::RwLock<Option<String>>>,
138
139 proxy_error_tx: broadcast::Sender<ProxyRuntimeError>,
142
143 sidecar_protocol_version: Arc<std::sync::atomic::AtomicU32>,
147}
148
149impl TailscaleProvider {
150 pub fn new(config: TailscaleConfig) -> Self {
154 let (peer_event_tx, _) = broadcast::channel(256);
155 let (proxy_error_tx, _) = broadcast::channel(64);
156
157 let initial_identity = NodeIdentity {
161 app_id: config.app_id.clone(),
162 device_id: config.device_id.clone(),
163 device_name: config.device_name.clone(),
164 tailscale_hostname: config.hostname.clone(),
165 tailscale_id: String::new(),
166 dns_name: None,
167 ip: None,
168 };
169
170 Self {
171 config,
172 state: Arc::new(RwLock::new(ProviderState::Stopped)),
173 identity: Arc::new(std::sync::RwLock::new(initial_identity)),
174 local_addr: Arc::new(std::sync::RwLock::new(PeerAddr::default())),
175 peers: Arc::new(RwLock::new(HashMap::new())),
176 peer_event_tx,
177 health: Arc::new(RwLock::new(HealthInfo {
178 state: "stopped".to_string(),
179 healthy: false,
180 ..Default::default()
181 })),
182 sidecar: Arc::new(Mutex::new(None)),
183 bridge: Arc::new(Mutex::new(None)),
184 bridge_shutdown_tx: Arc::new(Mutex::new(None)),
185 session_token: Arc::new(RwLock::new([0u8; 32])),
186 local_tailscale_id: Arc::new(std::sync::RwLock::new(None)),
187 proxy_error_tx,
188 sidecar_protocol_version: Arc::new(std::sync::atomic::AtomicU32::new(0)),
189 }
190 }
191
192 fn generate_session_token() -> Result<[u8; 32], NetworkError> {
194 let mut token = [0u8; 32];
195 getrandom::getrandom(&mut token).map_err(|e| {
196 NetworkError::Internal(format!("failed to generate session token: {e}"))
197 })?;
198 Ok(token)
199 }
200
201 fn sidecar_peer_to_network_peer(peer: &super::protocol::SidecarPeer) -> NetworkPeer {
203 let ip = peer
204 .tailscale_ips
205 .first()
206 .and_then(|s| s.parse::<IpAddr>().ok())
207 .unwrap_or(IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED));
208
209 NetworkPeer {
210 id: peer.id.clone(),
211 hostname: peer.hostname.clone(),
212 ip,
213 online: peer.online,
214 cur_addr: if peer.cur_addr.is_empty() {
215 None
216 } else {
217 Some(peer.cur_addr.clone())
218 },
219 relay: if peer.relay.is_empty() {
220 None
221 } else {
222 Some(peer.relay.clone())
223 },
224 os: if peer.os.is_empty() {
225 None
226 } else {
227 Some(peer.os.clone())
228 },
229 last_seen: peer.last_seen.clone(),
230 key_expiry: peer.key_expiry.clone(),
231 dns_name: Some(peer.dns_name.clone()),
232 }
233 }
234
235 #[allow(clippy::too_many_arguments)]
238 fn spawn_event_processor(
239 mut sidecar_rx: broadcast::Receiver<SidecarInternalEvent>,
240 peers: Arc<RwLock<HashMap<String, NetworkPeer>>>,
241 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
242 health: Arc<RwLock<HealthInfo>>,
243 identity: Arc<std::sync::RwLock<NodeIdentity>>,
244 local_addr: Arc<std::sync::RwLock<PeerAddr>>,
245 local_tailscale_id: Arc<std::sync::RwLock<Option<String>>>,
246 state: Arc<RwLock<ProviderState>>,
247 started_tx: Option<oneshot::Sender<Result<(), NetworkError>>>,
248 app_id: String,
249 proxy_error_tx: broadcast::Sender<ProxyRuntimeError>,
250 sidecar_protocol_version: Arc<std::sync::atomic::AtomicU32>,
251 ) {
252 tokio::spawn(async move {
253 let mut started_tx = started_tx;
254
255 loop {
256 match sidecar_rx.recv().await {
257 Ok(event) => {
258 match event {
259 SidecarInternalEvent::Started {
260 hostname,
261 dns_name,
262 tailscale_ip,
263 node_id,
264 protocol_version,
265 } => {
266 sidecar_protocol_version.store(
268 protocol_version.unwrap_or(0),
269 std::sync::atomic::Ordering::Relaxed,
270 );
271 let ip: Option<IpAddr> = tailscale_ip.parse().ok();
272
273 {
274 let mut id = identity.write().unwrap();
275 id.tailscale_hostname = hostname.clone();
281 id.dns_name = Some(dns_name.clone());
282 id.ip = ip;
283 if !node_id.is_empty() {
284 id.tailscale_id = node_id.clone();
285 }
286 }
287
288 if !node_id.is_empty() {
291 *local_tailscale_id.write().unwrap() = Some(node_id);
292 }
293
294 {
295 let mut addr = local_addr.write().unwrap();
296 addr.hostname = hostname;
297 addr.dns_name = Some(dns_name);
298 addr.ip = ip;
299 }
300
301 {
302 let mut h = health.write().await;
303 h.state = "running".to_string();
304 h.healthy = true;
305 }
306
307 *state.write().await = ProviderState::Running;
308
309 if let Some(tx) = started_tx.take() {
311 let _ = tx.send(Ok(()));
312 }
313 }
314 SidecarInternalEvent::AuthRequired { auth_url } => {
315 tracing::info!("tailscale auth required: {auth_url}");
316 let _ = peer_event_tx
319 .send(NetworkPeerEvent::AuthRequired { url: auth_url });
320 }
321 SidecarInternalEvent::Stopped => {
322 *state.write().await = ProviderState::Stopped;
323 let mut h = health.write().await;
324 h.state = "stopped".to_string();
325 h.healthy = false;
326 tracing::info!("tailscale provider stopped");
327 return;
328 }
329 SidecarInternalEvent::StateChange { state: new_state } => {
330 let mut h = health.write().await;
331 h.state = new_state;
332 }
333 SidecarInternalEvent::KeyExpiring { expires_at } => {
334 let mut h = health.write().await;
335 h.key_expiry = Some(expires_at);
336 }
337 SidecarInternalEvent::HealthWarning { warnings } => {
338 let mut h = health.write().await;
339 h.warnings = warnings;
340 h.healthy = h.warnings.is_empty();
341 }
342 SidecarInternalEvent::PeersReceived(sidecar_peers) => {
343 let mut peer_map = peers.write().await;
344 let self_id = local_tailscale_id.read().unwrap().clone();
350 let new_peers: HashMap<String, NetworkPeer> = sidecar_peers
353 .iter()
354 .filter(|p| {
355 if let Some(ref me) = self_id {
356 if p.id == *me {
357 return false;
358 }
359 }
360 is_app_peer(&p.hostname, &app_id)
361 })
362 .map(|p| {
363 let np = Self::sidecar_peer_to_network_peer(p);
364 (np.id.clone(), np)
365 })
366 .collect();
367
368 for (id, new_peer) in &new_peers {
370 if let Some(_existing) = peer_map.get(id) {
371 let _ = peer_event_tx
372 .send(NetworkPeerEvent::Updated(new_peer.clone()));
373 } else {
374 let _ = peer_event_tx
375 .send(NetworkPeerEvent::Joined(new_peer.clone()));
376 }
377 }
378 for id in peer_map.keys() {
379 if !new_peers.contains_key(id) {
380 let _ =
381 peer_event_tx.send(NetworkPeerEvent::Left(id.clone()));
382 }
383 }
384
385 *peer_map = new_peers;
386 }
387 SidecarInternalEvent::PeerChanged(change) => {
388 let mut peer_map = peers.write().await;
389 let self_id = local_tailscale_id.read().unwrap().clone();
392 match change.change_type.as_str() {
393 "joined" => {
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::Joined(np));
405 }
406 }
407 }
408 "left" => {
409 if peer_map.remove(&change.peer_id).is_some() {
410 let _ = peer_event_tx
411 .send(NetworkPeerEvent::Left(change.peer_id));
412 }
413 }
414 "updated" => {
415 if let Some(p) = change.peer {
416 if let Some(ref me) = self_id {
417 if p.id == *me {
418 continue;
419 }
420 }
421 if is_app_peer(&p.hostname, &app_id) {
422 let np = Self::sidecar_peer_to_network_peer(&p);
423 peer_map.insert(np.id.clone(), np.clone());
424 let _ = peer_event_tx
425 .send(NetworkPeerEvent::Updated(np));
426 }
427 }
428 }
429 other => {
430 tracing::warn!("unknown peer change type: {other}");
431 }
432 }
433 }
434 SidecarInternalEvent::Error { code, message } => {
435 tracing::error!("sidecar error [{code}]: {message}");
436 if let Some(tx) = started_tx.take() {
438 let _ = tx.send(Err(NetworkError::SidecarError(format!(
439 "[{code}] {message}"
440 ))));
441 }
442 }
443 SidecarInternalEvent::ProcessExited { exit_code } => {
444 tracing::error!("sidecar process exited: {exit_code:?}");
445 *state.write().await = ProviderState::Stopped;
446 let mut h = health.write().await;
447 h.state = "crashed".to_string();
448 h.healthy = false;
449 if let Some(tx) = started_tx.take() {
450 let _ = tx.send(Err(NetworkError::SidecarError(format!(
451 "process exited with code {exit_code:?}"
452 ))));
453 }
454 return;
455 }
456 SidecarInternalEvent::ProxyError { id, code, message } => {
457 tracing::warn!("proxy runtime error [{code}] for {id}: {message}");
462 let _ =
463 proxy_error_tx.send(ProxyRuntimeError { id, code, message });
464 }
465 _ => {}
468 }
469 }
470 Err(broadcast::error::RecvError::Lagged(n)) => {
471 tracing::warn!("event processor lagged by {n} events");
472 }
473 Err(broadcast::error::RecvError::Closed) => {
474 tracing::info!("sidecar event channel closed, stopping event processor");
475 return;
476 }
477 }
478 }
479 });
480 }
481}
482
483pub(crate) fn is_app_peer(hostname: &str, app_id: &str) -> bool {
493 let prefix = format!("truffle-{app_id}-");
494 hostname.len() > prefix.len() && hostname.starts_with(&prefix)
495}
496
497impl super::super::NetworkProvider for TailscaleProvider {
498 async fn start(&mut self) -> Result<(), NetworkError> {
499 {
500 let current_state = *self.state.read().await;
501 if current_state != ProviderState::Stopped {
502 return Err(NetworkError::AlreadyRunning);
503 }
504 }
505 *self.state.write().await = ProviderState::Starting;
506
507 let token = Self::generate_session_token()?;
509 let token_hex = hex::encode(token);
510 *self.session_token.write().await = token;
511
512 let bridge = Bridge::bind(token).await?;
514 let bridge_port = bridge.local_port()?;
515 let bridge = Arc::new(bridge);
516
517 let (bridge_shutdown_tx, bridge_shutdown_rx) = tokio::sync::watch::channel(false);
519
520 {
522 let bridge_clone = bridge.clone();
523 tokio::spawn(async move {
524 bridge_clone.run(bridge_shutdown_rx).await;
525 });
526 }
527
528 *self.bridge.lock().await = Some(bridge.clone());
529 *self.bridge_shutdown_tx.lock().await = Some(bridge_shutdown_tx);
530
531 let sidecar_config = SidecarConfig {
533 binary_path: self.config.binary_path.clone(),
534 hostname: self.config.hostname.clone(),
535 state_dir: self.config.state_dir.clone(),
536 auth_key: self.config.auth_key.clone(),
537 bridge_port,
538 session_token_hex: token_hex,
539 ephemeral: self.config.ephemeral,
540 tags: self.config.tags.clone(),
541 idle_timeout_secs: self.config.idle_timeout_secs,
542 };
543
544 let (sidecar, sidecar_rx) = GoSidecar::spawn(sidecar_config.clone()).await?;
546
547 let (started_tx, started_rx) = oneshot::channel();
549
550 Self::spawn_event_processor(
552 sidecar_rx,
553 self.peers.clone(),
554 self.peer_event_tx.clone(),
555 self.health.clone(),
556 self.identity.clone(),
557 self.local_addr.clone(),
558 self.local_tailscale_id.clone(),
559 self.state.clone(),
560 Some(started_tx),
561 self.config.app_id.clone(),
562 self.proxy_error_tx.clone(),
563 self.sidecar_protocol_version.clone(),
564 );
565
566 sidecar.send_start(&sidecar_config).await?;
568
569 *self.sidecar.lock().await = Some(sidecar);
570
571 let auth_timeout = Duration::from_secs(300);
575 let result = tokio::time::timeout(auth_timeout, started_rx)
576 .await
577 .map_err(|_| {
578 NetworkError::StartFailed(
579 "timed out waiting for authentication (5 min). \
580 Subscribe to peer_events() to display auth URLs."
581 .into(),
582 )
583 })?
584 .map_err(|_| NetworkError::StartFailed("start signal channel dropped".into()))?;
585
586 match result {
587 Ok(()) => {
588 if let Some(ref sidecar) = *self.sidecar.lock().await {
590 let _ = sidecar.send_get_peers().await;
591 let _ = sidecar.send_watch_peers().await;
593 }
594 tracing::info!("tailscale provider started successfully");
595 Ok(())
596 }
597 Err(e) => {
598 *self.state.write().await = ProviderState::Stopped;
599 Err(e)
600 }
601 }
602 }
603
604 async fn stop(&self) -> Result<(), NetworkError> {
605 *self.state.write().await = ProviderState::Stopping;
606
607 if let Some(sidecar) = self.sidecar.lock().await.take() {
609 sidecar.shutdown().await;
610 }
611
612 if let Some(tx) = self.bridge_shutdown_tx.lock().await.take() {
614 let _ = tx.send(true);
615 }
616 *self.bridge.lock().await = None;
617
618 self.peers.write().await.clear();
620 *self.state.write().await = ProviderState::Stopped;
621 let mut h = self.health.write().await;
622 h.state = "stopped".to_string();
623 h.healthy = false;
624
625 tracing::info!("tailscale provider stopped");
626 Ok(())
627 }
628
629 fn local_identity(&self) -> NodeIdentity {
630 self.identity.read().unwrap().clone()
631 }
632
633 fn local_addr(&self) -> PeerAddr {
634 self.local_addr.read().unwrap().clone()
635 }
636
637 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
638 self.peer_event_tx.subscribe()
639 }
640
641 async fn peers(&self) -> Vec<NetworkPeer> {
642 self.peers.read().await.values().cloned().collect()
643 }
644
645 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
646 self.dial_tcp_opts(addr, port, DialOpts::default()).await
647 }
648
649 async fn dial_tcp_opts(
650 &self,
651 addr: &str,
652 port: u16,
653 opts: DialOpts,
654 ) -> Result<TcpStream, NetworkError> {
655 if *self.state.read().await != ProviderState::Running {
656 return Err(NetworkError::NotRunning);
657 }
658
659 let bridge = self
660 .bridge
661 .lock()
662 .await
663 .clone()
664 .ok_or(NetworkError::NotRunning)?;
665
666 let request_id = uuid::Uuid::new_v4().to_string();
668
669 let dial_rx = bridge.register_dial(request_id.clone()).await;
671
672 let reply = {
674 let sidecar_guard = self.sidecar.lock().await;
675 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
676
677 let reply = sidecar.register_reply(&request_id);
678
679 sidecar
680 .send_dial(request_id.clone(), addr.to_string(), port, opts.tls)
681 .await?;
682
683 reply
684 };
685
686 Self::await_dial_result(&bridge, &request_id, dial_rx, reply, DIAL_TIMEOUT).await
691 }
692
693 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
694 self.listen_tcp_opts(port, ListenOpts::default()).await
695 }
696
697 async fn listen_tcp_opts(
698 &self,
699 port: u16,
700 opts: ListenOpts,
701 ) -> Result<NetworkTcpListener, NetworkError> {
702 if *self.state.read().await != ProviderState::Running {
703 return Err(NetworkError::NotRunning);
704 }
705
706 let bridge = self
707 .bridge
708 .lock()
709 .await
710 .clone()
711 .ok_or(NetworkError::NotRunning)?;
712
713 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
715
716 let tls = if opts.tls { Some(true) } else { None };
721
722 let actual_port = if self.sidecar_version() >= Self::SIDECAR_V4_REPLY_ROUTING {
728 let request_id = uuid::Uuid::new_v4().to_string();
729 let mut reply = {
730 let sidecar_guard = self.sidecar.lock().await;
731 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
732 let reply = sidecar.register_reply(&request_id);
733 sidecar
734 .send_listen(port, tls, Some(request_id.clone()))
735 .await?;
736 reply
737 };
738 match tokio::time::timeout(Duration::from_secs(10), reply.recv())
739 .await
740 .map_err(|_| NetworkError::ListenFailed("listen confirmation timed out".into()))??
741 {
742 SidecarInternalEvent::Listening { port: p } => p,
745 SidecarInternalEvent::Error { code, message } => {
746 return Err(NetworkError::ListenFailed(format!("[{code}] {message}")));
747 }
748 other => return Err(Self::unexpected_reply("listen", other)),
749 }
750 } else {
751 let mut event_rx = {
753 let sidecar_guard = self.sidecar.lock().await;
754 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
755
756 let event_rx = sidecar.subscribe();
757 sidecar.send_listen(port, tls, None).await?;
758 event_rx
759 };
760
761 tokio::time::timeout(Duration::from_secs(10), async {
762 loop {
763 match event_rx.recv().await {
764 Ok(SidecarInternalEvent::Listening { port: p })
765 if port == 0 || p == port =>
766 {
767 return Ok(p);
768 }
769 Ok(SidecarInternalEvent::Error { code, message }) => {
770 return Err(NetworkError::ListenFailed(format!("[{code}] {message}")));
771 }
772 Err(broadcast::error::RecvError::Closed) => {
773 return Err(NetworkError::SidecarError("event channel closed".into()));
774 }
775 Err(broadcast::error::RecvError::Lagged(_)) => {
776 return Err(NetworkError::SidecarError(
777 "event channel lagged: listen confirmation may have been lost"
778 .into(),
779 ));
780 }
781 _ => continue,
782 }
783 }
784 })
785 .await
786 .map_err(|_| NetworkError::ListenFailed("listen confirmation timed out".into()))??
787 };
788
789 bridge.register_listener(actual_port, tx).await;
791
792 Ok(NetworkTcpListener {
793 port: actual_port,
794 incoming: rx,
795 })
796 }
797
798 async fn unlisten_tcp(&self, port: u16) -> Result<(), NetworkError> {
799 if *self.state.read().await != ProviderState::Running {
800 return Err(NetworkError::NotRunning);
801 }
802
803 let bridge = self
804 .bridge
805 .lock()
806 .await
807 .clone()
808 .ok_or(NetworkError::NotRunning)?;
809
810 bridge.remove_listener(port).await;
812
813 {
815 let sidecar_guard = self.sidecar.lock().await;
816 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
817 sidecar.send_unlisten(port).await?;
818 }
819
820 Ok(())
821 }
822
823 async fn ping(&self, addr: &str) -> Result<PingResult, NetworkError> {
824 if *self.state.read().await != ProviderState::Running {
825 return Err(NetworkError::NotRunning);
826 }
827
828 let target = addr.to_string();
829
830 if self.sidecar_version() >= Self::SIDECAR_V2_PING_ECHO {
834 let request_id = uuid::Uuid::new_v4().to_string();
835 let mut reply = {
836 let sidecar_guard = self.sidecar.lock().await;
837 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
838 let reply = sidecar.register_reply(&request_id);
839 sidecar
840 .send_ping(target, None, Some(request_id.clone()))
841 .await?;
842 reply
843 };
844 return match tokio::time::timeout(Duration::from_secs(15), reply.recv())
845 .await
846 .map_err(|_| NetworkError::PingFailed("ping timed out".into()))??
847 {
848 SidecarInternalEvent::PingResult(data) => Self::map_ping_result(data),
849 SidecarInternalEvent::Error { code, message } => {
850 Err(NetworkError::PingFailed(format!("[{code}] {message}")))
851 }
852 other => Err(Self::unexpected_reply("ping", other)),
853 };
854 }
855
856 let mut event_rx = {
858 let sidecar_guard = self.sidecar.lock().await;
859 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
860
861 let event_rx = sidecar.subscribe();
862 sidecar.send_ping(target.clone(), None, None).await?;
863 event_rx
864 };
865
866 tokio::time::timeout(Duration::from_secs(15), async {
867 loop {
868 match event_rx.recv().await {
869 Ok(SidecarInternalEvent::PingResult(data)) if data.target == target => {
870 return Self::map_ping_result(data);
871 }
872 Err(broadcast::error::RecvError::Closed) => {
873 return Err(NetworkError::SidecarError("event channel closed".into()));
874 }
875 Err(broadcast::error::RecvError::Lagged(_)) => {
876 return Err(NetworkError::SidecarError(
877 "event channel lagged: ping result may have been lost".into(),
878 ));
879 }
880 _ => continue,
881 }
882 }
883 })
884 .await
885 .map_err(|_| NetworkError::PingFailed("ping timed out".into()))?
886 }
887
888 async fn whois(
889 &self,
890 addr: &str,
891 ) -> Result<Option<super::super::TailscalePeerIdentity>, NetworkError> {
892 if *self.state.read().await != ProviderState::Running {
893 return Err(NetworkError::NotRunning);
894 }
895
896 let version = self
900 .sidecar_protocol_version
901 .load(std::sync::atomic::Ordering::Relaxed);
902 if version < 3 {
903 return Err(NetworkError::Unsupported(format!(
904 "sidecar protocol v{version} predates tsnet:whois; upgrade the \
905 sidecar binary (needs v3)"
906 )));
907 }
908
909 let target = addr.to_string();
910 let request_id = uuid::Uuid::new_v4().to_string();
911
912 let mut reply = {
917 let sidecar_guard = self.sidecar.lock().await;
918 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
919 let reply = sidecar.register_reply(&request_id);
920 sidecar.send_whois(target, Some(request_id.clone())).await?;
921 reply
922 };
923
924 match tokio::time::timeout(Duration::from_secs(10), reply.recv())
925 .await
926 .map_err(|_| NetworkError::SidecarError("whois timed out".into()))??
927 {
928 SidecarInternalEvent::WhoisResult(data) => Self::map_whois_result(data),
929 SidecarInternalEvent::Error { code, message } => {
930 Err(NetworkError::SidecarError(format!("[{code}] {message}")))
931 }
932 other => Err(Self::unexpected_reply("whois", other)),
933 }
934 }
935
936 async fn bind_udp(&self, port: u16) -> Result<super::super::NetworkUdpSocket, NetworkError> {
937 if *self.state.read().await != ProviderState::Running {
938 return Err(NetworkError::NotRunning);
939 }
940
941 let local_port = if self.sidecar_version() >= Self::SIDECAR_V4_REPLY_ROUTING {
945 let request_id = uuid::Uuid::new_v4().to_string();
946 let mut reply = {
947 let sidecar_guard = self.sidecar.lock().await;
948 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
949 let reply = sidecar.register_reply(&request_id);
950 sidecar
951 .send_listen_packet(port, Some(request_id.clone()))
952 .await?;
953 reply
954 };
955 match tokio::time::timeout(Duration::from_secs(10), reply.recv())
956 .await
957 .map_err(|_| {
958 NetworkError::ListenFailed("UDP listenPacket confirmation timed out".into())
959 })?? {
960 SidecarInternalEvent::ListeningPacket { local_port, .. } => local_port,
961 SidecarInternalEvent::Error { code, message } => {
962 return Err(NetworkError::ListenFailed(format!(
963 "UDP bind failed [{code}] {message}"
964 )));
965 }
966 other => return Err(Self::unexpected_reply("listenPacket", other)),
967 }
968 } else {
969 let mut event_rx = {
971 let sidecar_guard = self.sidecar.lock().await;
972 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
973
974 let event_rx = sidecar.subscribe();
975 sidecar.send_listen_packet(port, None).await?;
976 event_rx
977 };
978
979 tokio::time::timeout(Duration::from_secs(10), async {
980 loop {
981 match event_rx.recv().await {
982 Ok(SidecarInternalEvent::ListeningPacket {
983 port: p,
984 local_port,
985 }) if p == port => {
986 return Ok(local_port);
987 }
988 Ok(SidecarInternalEvent::Error { code, message }) => {
989 return Err(NetworkError::ListenFailed(format!(
990 "UDP bind failed [{code}] {message}"
991 )));
992 }
993 Err(broadcast::error::RecvError::Closed) => {
994 return Err(NetworkError::SidecarError("event channel closed".into()));
995 }
996 Err(broadcast::error::RecvError::Lagged(_)) => {
997 return Err(NetworkError::SidecarError(
998 "event channel lagged: UDP bind confirmation may have been lost"
999 .into(),
1000 ));
1001 }
1002 _ => continue,
1003 }
1004 }
1005 })
1006 .await
1007 .map_err(|_| {
1008 NetworkError::ListenFailed("UDP listenPacket confirmation timed out".into())
1009 })??
1010 };
1011
1012 let local_socket = tokio::net::UdpSocket::bind("127.0.0.1:0")
1014 .await
1015 .map_err(|e| NetworkError::Internal(format!("failed to bind local UDP socket: {e}")))?;
1016
1017 local_socket
1018 .connect(format!("127.0.0.1:{local_port}"))
1019 .await
1020 .map_err(|e| {
1021 NetworkError::Internal(format!("failed to connect local UDP socket to relay: {e}"))
1022 })?;
1023
1024 let rust_local_addr = local_socket
1025 .local_addr()
1026 .map_err(|e| NetworkError::Internal(format!("failed to get local UDP addr: {e}")))?;
1027
1028 local_socket
1033 .send(b"TRUFFLE_UDP_REGISTER")
1034 .await
1035 .map_err(|e| NetworkError::Internal(format!("failed to send UDP registration: {e}")))?;
1036
1037 tracing::info!(
1038 tsnet_port = port,
1039 relay_port = local_port,
1040 rust_local_addr = %rust_local_addr,
1041 "UDP socket bound via tsnet relay (registered)"
1042 );
1043
1044 Ok(super::super::NetworkUdpSocket::new(local_socket, port))
1045 }
1046
1047 async fn health(&self) -> HealthInfo {
1048 self.health.read().await.clone()
1049 }
1050
1051 fn proxy_runtime_errors(&self) -> Option<broadcast::Receiver<ProxyRuntimeError>> {
1052 Some(self.proxy_error_tx.subscribe())
1053 }
1054
1055 async fn proxy_add(&self, config: ProxyAddParams) -> Result<ProxyAddResult, NetworkError> {
1058 if *self.state.read().await != ProviderState::Running {
1059 return Err(NetworkError::NotRunning);
1060 }
1061
1062 let uses_v2 = !config.tls
1066 || config.allow_non_loopback
1067 || !config.allow.is_empty()
1068 || !config.routes.is_empty();
1069 let version = self
1070 .sidecar_protocol_version
1071 .load(std::sync::atomic::Ordering::Relaxed);
1072 if uses_v2 && version < 2 {
1073 return Err(NetworkError::ProxyError(format!(
1074 "sidecar protocol v{version} predates RFC 023 — routes, allow lists, \
1075 tls: false, and non-loopback targets need a v2 sidecar; upgrade the \
1076 sidecar binary"
1077 )));
1078 }
1079
1080 let command = ProxyAddCommandData {
1081 id: config.id.clone(),
1082 name: config.name.clone(),
1083 listen_port: config.listen_port,
1084 target_host: config.target_host.clone(),
1085 target_port: config.target_port,
1086 target_scheme: config.target_scheme.clone(),
1087 tls: config.tls,
1088 allow_non_loopback: config.allow_non_loopback,
1089 allow: config.allow.clone(),
1090 routes: config.routes.clone(),
1091 request_id: None,
1092 };
1093
1094 if self.sidecar_version() >= Self::SIDECAR_V4_REPLY_ROUTING {
1098 let request_id = uuid::Uuid::new_v4().to_string();
1099 let mut reply = {
1100 let sidecar_guard = self.sidecar.lock().await;
1101 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
1102 let reply = sidecar.register_reply(&request_id);
1103 sidecar
1104 .send_proxy_add(ProxyAddCommandData {
1105 request_id: Some(request_id.clone()),
1106 ..command
1107 })
1108 .await?;
1109 reply
1110 };
1111 return match tokio::time::timeout(Duration::from_secs(10), reply.recv())
1112 .await
1113 .map_err(|_| NetworkError::ProxyError("proxy add timed out".into()))??
1114 {
1115 SidecarInternalEvent::ProxyAdded {
1116 id,
1117 listen_port,
1118 url,
1119 } => Ok(ProxyAddResult {
1120 id,
1121 listen_port,
1122 url,
1123 }),
1124 SidecarInternalEvent::ProxyError { code, message, .. }
1125 | SidecarInternalEvent::Error { code, message } => {
1126 Err(NetworkError::ProxyError(format!("[{code}] {message}")))
1127 }
1128 other => Err(Self::unexpected_reply("proxy add", other)),
1129 };
1130 }
1131
1132 let mut event_rx = {
1134 let sidecar_guard = self.sidecar.lock().await;
1135 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
1136 let event_rx = sidecar.subscribe();
1137 sidecar.send_proxy_add(command).await?;
1138 event_rx
1139 };
1140
1141 tokio::time::timeout(Duration::from_secs(10), async {
1143 loop {
1144 match event_rx.recv().await {
1145 Ok(SidecarInternalEvent::ProxyAdded {
1146 id,
1147 listen_port,
1148 url,
1149 }) if id == config.id => {
1150 return Ok(ProxyAddResult {
1151 id,
1152 listen_port,
1153 url,
1154 });
1155 }
1156 Ok(SidecarInternalEvent::ProxyError { id, code, message })
1157 if id == config.id =>
1158 {
1159 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
1160 }
1161 Ok(SidecarInternalEvent::Error { code, message }) => {
1162 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
1163 }
1164 Err(broadcast::error::RecvError::Closed) => {
1165 return Err(NetworkError::SidecarError("event channel closed".into()));
1166 }
1167 Err(broadcast::error::RecvError::Lagged(_)) => {
1168 return Err(NetworkError::SidecarError(
1169 "event channel lagged: proxy confirmation may have been lost".into(),
1170 ));
1171 }
1172 Ok(_) => continue,
1173 }
1174 }
1175 })
1176 .await
1177 .map_err(|_| NetworkError::ProxyError("proxy add timed out".into()))?
1178 }
1179
1180 async fn proxy_remove(&self, id: &str) -> Result<(), NetworkError> {
1181 if *self.state.read().await != ProviderState::Running {
1182 return Err(NetworkError::NotRunning);
1183 }
1184
1185 let target_id = id.to_string();
1186
1187 if self.sidecar_version() >= Self::SIDECAR_V4_REPLY_ROUTING {
1190 let request_id = uuid::Uuid::new_v4().to_string();
1191 let mut reply = {
1192 let sidecar_guard = self.sidecar.lock().await;
1193 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
1194 let reply = sidecar.register_reply(&request_id);
1195 sidecar
1196 .send_proxy_remove(id, Some(request_id.clone()))
1197 .await?;
1198 reply
1199 };
1200 return match tokio::time::timeout(Duration::from_secs(10), reply.recv())
1201 .await
1202 .map_err(|_| NetworkError::ProxyError("proxy remove timed out".into()))??
1203 {
1204 SidecarInternalEvent::ProxyRemoved { .. } => Ok(()),
1205 SidecarInternalEvent::ProxyError { code, message, .. }
1206 | SidecarInternalEvent::Error { code, message } => {
1207 Err(NetworkError::ProxyError(format!("[{code}] {message}")))
1208 }
1209 other => Err(Self::unexpected_reply("proxy remove", other)),
1210 };
1211 }
1212
1213 let mut event_rx = {
1215 let sidecar_guard = self.sidecar.lock().await;
1216 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
1217 let event_rx = sidecar.subscribe();
1218 sidecar.send_proxy_remove(id, None).await?;
1219 event_rx
1220 };
1221
1222 tokio::time::timeout(Duration::from_secs(10), async {
1224 loop {
1225 match event_rx.recv().await {
1226 Ok(SidecarInternalEvent::ProxyRemoved { id }) if id == target_id => {
1227 return Ok(());
1228 }
1229 Ok(SidecarInternalEvent::ProxyError { id, code, message })
1230 if id == target_id =>
1231 {
1232 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
1233 }
1234 Ok(SidecarInternalEvent::Error { code, message }) => {
1235 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
1236 }
1237 Err(broadcast::error::RecvError::Closed) => {
1238 return Err(NetworkError::SidecarError("event channel closed".into()));
1239 }
1240 Err(broadcast::error::RecvError::Lagged(_)) => {
1241 return Err(NetworkError::SidecarError(
1242 "event channel lagged: proxy confirmation may have been lost".into(),
1243 ));
1244 }
1245 Ok(_) => continue,
1246 }
1247 }
1248 })
1249 .await
1250 .map_err(|_| NetworkError::ProxyError("proxy remove timed out".into()))?
1251 }
1252
1253 async fn proxy_list(&self) -> Result<Vec<ProxyListEntry>, NetworkError> {
1254 if *self.state.read().await != ProviderState::Running {
1255 return Err(NetworkError::NotRunning);
1256 }
1257
1258 if self.sidecar_version() >= Self::SIDECAR_V4_REPLY_ROUTING {
1262 let request_id = uuid::Uuid::new_v4().to_string();
1263 let mut reply = {
1264 let sidecar_guard = self.sidecar.lock().await;
1265 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
1266 let reply = sidecar.register_reply(&request_id);
1267 sidecar.send_proxy_list(Some(request_id.clone())).await?;
1268 reply
1269 };
1270 return match tokio::time::timeout(Duration::from_secs(10), reply.recv())
1271 .await
1272 .map_err(|_| NetworkError::ProxyError("proxy list timed out".into()))??
1273 {
1274 SidecarInternalEvent::ProxyList { proxies } => Ok(Self::proxy_entries(proxies)),
1275 SidecarInternalEvent::ProxyError { code, message, .. }
1276 | SidecarInternalEvent::Error { code, message } => {
1277 Err(NetworkError::ProxyError(format!("[{code}] {message}")))
1278 }
1279 other => Err(Self::unexpected_reply("proxy list", other)),
1280 };
1281 }
1282
1283 let mut event_rx = {
1285 let sidecar_guard = self.sidecar.lock().await;
1286 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
1287 let event_rx = sidecar.subscribe();
1288 sidecar.send_proxy_list(None).await?;
1289 event_rx
1290 };
1291
1292 tokio::time::timeout(Duration::from_secs(10), async {
1294 loop {
1295 match event_rx.recv().await {
1296 Ok(SidecarInternalEvent::ProxyList { proxies }) => {
1297 return Ok(Self::proxy_entries(proxies));
1298 }
1299 Ok(SidecarInternalEvent::Error { code, message }) => {
1300 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
1301 }
1302 Err(broadcast::error::RecvError::Closed) => {
1303 return Err(NetworkError::SidecarError("event channel closed".into()));
1304 }
1305 Err(broadcast::error::RecvError::Lagged(_)) => {
1306 return Err(NetworkError::SidecarError(
1307 "event channel lagged: proxy confirmation may have been lost".into(),
1308 ));
1309 }
1310 Ok(_) => continue,
1311 }
1312 }
1313 })
1314 .await
1315 .map_err(|_| NetworkError::ProxyError("proxy list timed out".into()))?
1316 }
1317}
1318
1319impl TailscaleProvider {
1320 pub async fn local_identity_async(&self) -> NodeIdentity {
1325 self.identity.read().unwrap().clone()
1326 }
1327
1328 pub async fn local_addr_async(&self) -> PeerAddr {
1333 self.local_addr.read().unwrap().clone()
1334 }
1335
1336 const SIDECAR_V2_PING_ECHO: u32 = 2;
1349
1350 const SIDECAR_V4_REPLY_ROUTING: u32 = 4;
1354
1355 fn sidecar_version(&self) -> u32 {
1356 self.sidecar_protocol_version
1357 .load(std::sync::atomic::Ordering::Relaxed)
1358 }
1359
1360 fn unexpected_reply(what: &str, event: SidecarInternalEvent) -> NetworkError {
1363 NetworkError::SidecarError(format!("unexpected {what} reply: {event:?}"))
1364 }
1365
1366 fn map_ping_result(data: PingResultEventData) -> Result<PingResult, NetworkError> {
1369 if !data.error.is_empty() {
1370 return Err(NetworkError::PingFailed(data.error));
1371 }
1372 let connection = if data.direct {
1373 "direct".to_string()
1374 } else if !data.relay.is_empty() {
1375 format!("relay:{}", data.relay)
1376 } else {
1377 "unknown".to_string()
1378 };
1379 Ok(PingResult {
1380 latency: Duration::from_secs_f64(data.latency_ms / 1000.0),
1381 connection,
1382 peer_addr: if data.peer_addr.is_empty() {
1383 None
1384 } else {
1385 Some(data.peer_addr)
1386 },
1387 })
1388 }
1389
1390 fn map_whois_result(
1397 data: WhoisResultEventData,
1398 ) -> Result<Option<super::super::TailscalePeerIdentity>, NetworkError> {
1399 if !data.error.is_empty() {
1400 return Err(NetworkError::SidecarError(data.error));
1401 }
1402 Ok(data
1403 .identity
1404 .map(super::super::TailscalePeerIdentity::normalized)
1405 .filter(|identity| !identity.is_empty()))
1406 }
1407
1408 fn proxy_entries(proxies: Vec<ProxyInfoEventData>) -> Vec<ProxyListEntry> {
1410 proxies
1411 .into_iter()
1412 .map(|p| ProxyListEntry {
1413 id: p.id,
1414 name: p.name,
1415 listen_port: p.listen_port,
1416 target_host: p.target_host,
1417 target_port: p.target_port,
1418 target_scheme: p.target_scheme,
1419 url: p.url,
1420 })
1421 .collect()
1422 }
1423
1424 pub(super) async fn await_dial_result(
1431 bridge: &Bridge,
1432 request_id: &str,
1433 mut dial_rx: oneshot::Receiver<TcpStream>,
1434 mut reply: ReplyGuard,
1435 timeout: Duration,
1436 ) -> Result<TcpStream, NetworkError> {
1437 let result = tokio::time::timeout(timeout, async {
1438 let mut reply_pending = true;
1439 loop {
1440 tokio::select! {
1441 stream_result = &mut dial_rx => {
1442 return stream_result
1443 .map_err(|_| NetworkError::DialFailed("dial cancelled".into()));
1444 }
1445 reply_result = reply.recv(), if reply_pending => {
1446 match reply_result {
1447 Ok(SidecarInternalEvent::DialFailed { error, .. }) => {
1448 return Err(NetworkError::DialFailed(error));
1449 }
1450 Ok(SidecarInternalEvent::Error { code, message }) => {
1451 return Err(NetworkError::DialFailed(format!(
1452 "[{code}] {message}"
1453 )));
1454 }
1455 _ => reply_pending = false,
1459 }
1460 }
1461 }
1462 }
1463 })
1464 .await
1465 .unwrap_or(Err(NetworkError::DialTimeout(timeout)));
1466
1467 if result.is_err() {
1469 bridge.remove_dial(request_id).await;
1470 }
1471
1472 result
1473 }
1474}
1475
1476#[cfg(test)]
1477mod config_debug_tests {
1478 use super::*;
1479
1480 #[test]
1481 fn tailscale_config_debug_redacts_auth_key() {
1482 let config = TailscaleConfig {
1483 binary_path: PathBuf::from("/opt/sidecar"),
1484 app_id: "demo".to_string(),
1485 device_id: "01JZZZZZZZZZZZZZZZZZZZZZZZ".to_string(),
1486 device_name: "dev".to_string(),
1487 hostname: "truffle-demo-dev".to_string(),
1488 state_dir: "/tmp/state".to_string(),
1489 auth_key: Some("dummy-auth-SECRET123".to_string()),
1490 ephemeral: None,
1491 tags: None,
1492 idle_timeout_secs: None,
1493 };
1494 let dbg = format!("{config:?}");
1495 assert!(!dbg.contains("SECRET123"));
1496 assert!(dbg.contains("[REDACTED]"));
1497 }
1498}