truffle_core/network/tailscale/
provider.rs1use 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::sidecar::{GoSidecar, SidecarConfig, SidecarInternalEvent};
19use crate::network::{
20 HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
21 NetworkTcpListener, NodeIdentity, PeerAddr, PingResult,
22};
23
24#[derive(Debug, Clone)]
26pub struct TailscaleConfig {
27 pub binary_path: PathBuf,
29 pub hostname: String,
31 pub state_dir: String,
33 pub auth_key: Option<String>,
35 pub ephemeral: Option<bool>,
37 pub tags: Option<Vec<String>>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43enum ProviderState {
44 Stopped,
45 Starting,
46 Running,
47 Stopping,
48}
49
50pub struct TailscaleProvider {
61 config: TailscaleConfig,
62 state: Arc<RwLock<ProviderState>>,
63
64 identity: Arc<std::sync::RwLock<NodeIdentity>>,
69 local_addr: Arc<std::sync::RwLock<PeerAddr>>,
74
75 peers: Arc<RwLock<HashMap<String, NetworkPeer>>>,
77
78 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
80
81 health: Arc<RwLock<HealthInfo>>,
83
84 sidecar: Arc<Mutex<Option<GoSidecar>>>,
86
87 bridge: Arc<Mutex<Option<Arc<Bridge>>>>,
89
90 bridge_shutdown_tx: Arc<Mutex<Option<tokio::sync::watch::Sender<bool>>>>,
92
93 session_token: Arc<RwLock<[u8; 32]>>,
95}
96
97impl TailscaleProvider {
98 pub fn new(config: TailscaleConfig) -> Self {
102 let (peer_event_tx, _) = broadcast::channel(256);
103
104 Self {
105 config,
106 state: Arc::new(RwLock::new(ProviderState::Stopped)),
107 identity: Arc::new(std::sync::RwLock::new(NodeIdentity::default())),
108 local_addr: Arc::new(std::sync::RwLock::new(PeerAddr::default())),
109 peers: Arc::new(RwLock::new(HashMap::new())),
110 peer_event_tx,
111 health: Arc::new(RwLock::new(HealthInfo {
112 state: "stopped".to_string(),
113 healthy: false,
114 ..Default::default()
115 })),
116 sidecar: Arc::new(Mutex::new(None)),
117 bridge: Arc::new(Mutex::new(None)),
118 bridge_shutdown_tx: Arc::new(Mutex::new(None)),
119 session_token: Arc::new(RwLock::new([0u8; 32])),
120 }
121 }
122
123 fn generate_session_token() -> Result<[u8; 32], NetworkError> {
125 let mut token = [0u8; 32];
126 getrandom::getrandom(&mut token)
127 .map_err(|e| NetworkError::Internal(format!("failed to generate session token: {e}")))?;
128 Ok(token)
129 }
130
131 fn sidecar_peer_to_network_peer(
133 peer: &super::protocol::SidecarPeer,
134 ) -> NetworkPeer {
135 let ip = peer
136 .tailscale_ips
137 .first()
138 .and_then(|s| s.parse::<IpAddr>().ok())
139 .unwrap_or(IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED));
140
141 NetworkPeer {
142 id: peer.id.clone(),
143 hostname: peer.hostname.clone(),
144 ip,
145 online: peer.online,
146 cur_addr: if peer.cur_addr.is_empty() {
147 None
148 } else {
149 Some(peer.cur_addr.clone())
150 },
151 relay: if peer.relay.is_empty() {
152 None
153 } else {
154 Some(peer.relay.clone())
155 },
156 os: if peer.os.is_empty() {
157 None
158 } else {
159 Some(peer.os.clone())
160 },
161 last_seen: peer.last_seen.clone(),
162 key_expiry: peer.key_expiry.clone(),
163 dns_name: Some(peer.dns_name.clone()),
164 }
165 }
166
167 fn spawn_event_processor(
170 mut sidecar_rx: broadcast::Receiver<SidecarInternalEvent>,
171 peers: Arc<RwLock<HashMap<String, NetworkPeer>>>,
172 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
173 health: Arc<RwLock<HealthInfo>>,
174 identity: Arc<std::sync::RwLock<NodeIdentity>>,
175 local_addr: Arc<std::sync::RwLock<PeerAddr>>,
176 state: Arc<RwLock<ProviderState>>,
177 started_tx: Option<oneshot::Sender<Result<(), NetworkError>>>,
178 ) {
179 tokio::spawn(async move {
180 let mut started_tx = started_tx;
181
182 loop {
183 match sidecar_rx.recv().await {
184 Ok(event) => {
185 match event {
186 SidecarInternalEvent::Started {
187 hostname,
188 dns_name,
189 tailscale_ip,
190 node_id,
191 } => {
192 let ip: Option<IpAddr> = tailscale_ip.parse().ok();
193
194 {
195 let mut id = identity.write().unwrap();
196 id.hostname = hostname.clone();
197 id.dns_name = Some(dns_name.clone());
198 id.name = hostname.clone();
199 id.ip = ip;
200 if !node_id.is_empty() {
201 id.id = node_id;
202 }
203 }
204
205 {
206 let mut addr = local_addr.write().unwrap();
207 addr.hostname = hostname;
208 addr.dns_name = Some(dns_name);
209 addr.ip = ip;
210 }
211
212 {
213 let mut h = health.write().await;
214 h.state = "running".to_string();
215 h.healthy = true;
216 }
217
218 *state.write().await = ProviderState::Running;
219
220 if let Some(tx) = started_tx.take() {
222 let _ = tx.send(Ok(()));
223 }
224 }
225 SidecarInternalEvent::AuthRequired { auth_url } => {
226 tracing::info!("tailscale auth required: {auth_url}");
227 let _ = peer_event_tx.send(NetworkPeerEvent::AuthRequired {
230 url: auth_url,
231 });
232 }
233 SidecarInternalEvent::Stopped => {
234 *state.write().await = ProviderState::Stopped;
235 let mut h = health.write().await;
236 h.state = "stopped".to_string();
237 h.healthy = false;
238 tracing::info!("tailscale provider stopped");
239 return;
240 }
241 SidecarInternalEvent::StateChange { state: new_state } => {
242 let mut h = health.write().await;
243 h.state = new_state;
244 }
245 SidecarInternalEvent::KeyExpiring { expires_at } => {
246 let mut h = health.write().await;
247 h.key_expiry = Some(expires_at);
248 }
249 SidecarInternalEvent::HealthWarning { warnings } => {
250 let mut h = health.write().await;
251 h.warnings = warnings;
252 h.healthy = h.warnings.is_empty();
253 }
254 SidecarInternalEvent::PeersReceived(sidecar_peers) => {
255 let mut peer_map = peers.write().await;
256 let new_peers: HashMap<String, NetworkPeer> = sidecar_peers
258 .iter()
259 .filter(|p| is_truffle_peer(&p.hostname))
260 .map(|p| {
261 let np = Self::sidecar_peer_to_network_peer(p);
262 (np.id.clone(), np)
263 })
264 .collect();
265
266 for (id, new_peer) in &new_peers {
268 if let Some(_existing) = peer_map.get(id) {
269 let _ = peer_event_tx
270 .send(NetworkPeerEvent::Updated(new_peer.clone()));
271 } else {
272 let _ = peer_event_tx
273 .send(NetworkPeerEvent::Joined(new_peer.clone()));
274 }
275 }
276 for id in peer_map.keys() {
277 if !new_peers.contains_key(id) {
278 let _ = peer_event_tx
279 .send(NetworkPeerEvent::Left(id.clone()));
280 }
281 }
282
283 *peer_map = new_peers;
284 }
285 SidecarInternalEvent::PeerChanged(change) => {
286 let mut peer_map = peers.write().await;
287 match change.change_type.as_str() {
288 "joined" => {
289 if let Some(p) = change.peer {
290 if is_truffle_peer(&p.hostname) {
291 let np = Self::sidecar_peer_to_network_peer(&p);
292 peer_map.insert(np.id.clone(), np.clone());
293 let _ = peer_event_tx
294 .send(NetworkPeerEvent::Joined(np));
295 }
296 }
297 }
298 "left" => {
299 if peer_map.remove(&change.peer_id).is_some() {
300 let _ = peer_event_tx
301 .send(NetworkPeerEvent::Left(change.peer_id));
302 }
303 }
304 "updated" => {
305 if let Some(p) = change.peer {
306 if is_truffle_peer(&p.hostname) {
307 let np = Self::sidecar_peer_to_network_peer(&p);
308 peer_map.insert(np.id.clone(), np.clone());
309 let _ = peer_event_tx
310 .send(NetworkPeerEvent::Updated(np));
311 }
312 }
313 }
314 other => {
315 tracing::warn!("unknown peer change type: {other}");
316 }
317 }
318 }
319 SidecarInternalEvent::Error { code, message } => {
320 tracing::error!("sidecar error [{code}]: {message}");
321 if let Some(tx) = started_tx.take() {
323 let _ = tx.send(Err(NetworkError::SidecarError(
324 format!("[{code}] {message}"),
325 )));
326 }
327 }
328 SidecarInternalEvent::ProcessExited { exit_code } => {
329 tracing::error!("sidecar process exited: {exit_code:?}");
330 *state.write().await = ProviderState::Stopped;
331 let mut h = health.write().await;
332 h.state = "crashed".to_string();
333 h.healthy = false;
334 if let Some(tx) = started_tx.take() {
335 let _ = tx.send(Err(NetworkError::SidecarError(
336 format!("process exited with code {exit_code:?}"),
337 )));
338 }
339 return;
340 }
341 _ => {}
344 }
345 }
346 Err(broadcast::error::RecvError::Lagged(n)) => {
347 tracing::warn!("event processor lagged by {n} events");
348 }
349 Err(broadcast::error::RecvError::Closed) => {
350 tracing::info!("sidecar event channel closed, stopping event processor");
351 return;
352 }
353 }
354 }
355 });
356 }
357}
358
359pub(crate) fn is_truffle_peer(hostname: &str) -> bool {
362 hostname.starts_with("truffle-")
363}
364
365impl super::super::NetworkProvider for TailscaleProvider {
366 async fn start(&mut self) -> Result<(), NetworkError> {
367 {
368 let current_state = *self.state.read().await;
369 if current_state != ProviderState::Stopped {
370 return Err(NetworkError::AlreadyRunning);
371 }
372 }
373 *self.state.write().await = ProviderState::Starting;
374
375 let token = Self::generate_session_token()?;
377 let token_hex = hex::encode(token);
378 *self.session_token.write().await = token;
379
380 let bridge = Bridge::bind(token).await?;
382 let bridge_port = bridge.local_port()?;
383 let bridge = Arc::new(bridge);
384
385 let (bridge_shutdown_tx, bridge_shutdown_rx) = tokio::sync::watch::channel(false);
387
388 {
390 let bridge_clone = bridge.clone();
391 tokio::spawn(async move {
392 bridge_clone.run(bridge_shutdown_rx).await;
393 });
394 }
395
396 *self.bridge.lock().await = Some(bridge.clone());
397 *self.bridge_shutdown_tx.lock().await = Some(bridge_shutdown_tx);
398
399 let sidecar_config = SidecarConfig {
401 binary_path: self.config.binary_path.clone(),
402 hostname: self.config.hostname.clone(),
403 state_dir: self.config.state_dir.clone(),
404 auth_key: self.config.auth_key.clone(),
405 bridge_port,
406 session_token_hex: token_hex,
407 ephemeral: self.config.ephemeral,
408 tags: self.config.tags.clone(),
409 };
410
411 let (sidecar, sidecar_rx) = GoSidecar::spawn(sidecar_config.clone()).await?;
413
414 let (started_tx, started_rx) = oneshot::channel();
416
417 Self::spawn_event_processor(
419 sidecar_rx,
420 self.peers.clone(),
421 self.peer_event_tx.clone(),
422 self.health.clone(),
423 self.identity.clone(),
424 self.local_addr.clone(),
425 self.state.clone(),
426 Some(started_tx),
427 );
428
429 sidecar.send_start(&sidecar_config).await?;
431
432 *self.sidecar.lock().await = Some(sidecar);
433
434 let auth_timeout = Duration::from_secs(300);
438 let result = tokio::time::timeout(auth_timeout, started_rx)
439 .await
440 .map_err(|_| NetworkError::StartFailed(
441 "timed out waiting for authentication (5 min). \
442 Subscribe to peer_events() to display auth URLs.".into()
443 ))?
444 .map_err(|_| NetworkError::StartFailed("start signal channel dropped".into()))?;
445
446 match result {
447 Ok(()) => {
448 if let Some(ref sidecar) = *self.sidecar.lock().await {
450 let _ = sidecar.send_get_peers().await;
451 let _ = sidecar.send_watch_peers().await;
453 }
454 tracing::info!("tailscale provider started successfully");
455 Ok(())
456 }
457 Err(e) => {
458 *self.state.write().await = ProviderState::Stopped;
459 Err(e)
460 }
461 }
462 }
463
464 async fn stop(&mut self) -> Result<(), NetworkError> {
465 *self.state.write().await = ProviderState::Stopping;
466
467 if let Some(sidecar) = self.sidecar.lock().await.take() {
469 sidecar.shutdown().await;
470 }
471
472 if let Some(tx) = self.bridge_shutdown_tx.lock().await.take() {
474 let _ = tx.send(true);
475 }
476 *self.bridge.lock().await = None;
477
478 self.peers.write().await.clear();
480 *self.state.write().await = ProviderState::Stopped;
481 let mut h = self.health.write().await;
482 h.state = "stopped".to_string();
483 h.healthy = false;
484
485 tracing::info!("tailscale provider stopped");
486 Ok(())
487 }
488
489 fn local_identity(&self) -> NodeIdentity {
490 self.identity.read().unwrap().clone()
491 }
492
493 fn local_addr(&self) -> PeerAddr {
494 self.local_addr.read().unwrap().clone()
495 }
496
497 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
498 self.peer_event_tx.subscribe()
499 }
500
501 async fn peers(&self) -> Vec<NetworkPeer> {
502 self.peers.read().await.values().cloned().collect()
503 }
504
505 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
506 if *self.state.read().await != ProviderState::Running {
507 return Err(NetworkError::NotRunning);
508 }
509
510 let bridge = self
511 .bridge
512 .lock()
513 .await
514 .clone()
515 .ok_or(NetworkError::NotRunning)?;
516
517 let request_id = uuid::Uuid::new_v4().to_string();
519
520 let dial_rx = bridge.register_dial(request_id.clone()).await;
522
523 let mut event_rx = {
525 let sidecar_guard = self.sidecar.lock().await;
526 let sidecar = sidecar_guard
527 .as_ref()
528 .ok_or(NetworkError::NotRunning)?;
529
530 let event_rx = sidecar.subscribe();
531
532 sidecar
533 .send_dial(request_id.clone(), addr.to_string(), port)
534 .await?;
535
536 event_rx
537 };
538
539 let result = tokio::time::timeout(DIAL_TIMEOUT, async {
548 let fail_request_id = request_id.clone();
552 let (fail_tx, fail_rx) = oneshot::channel::<String>();
553 let fail_watcher: JoinHandle<()> = tokio::spawn(async move {
554 loop {
555 match event_rx.recv().await {
556 Ok(SidecarInternalEvent::DialFailed { request_id: rid, error })
557 if rid == fail_request_id =>
558 {
559 let _ = fail_tx.send(error);
560 return;
561 }
562 Err(broadcast::error::RecvError::Closed) => {
563 let _ = fail_tx.send("event channel closed".to_string());
564 return;
565 }
566 _ => continue,
567 }
568 }
569 });
570
571 let result = tokio::select! {
572 stream_result = dial_rx => {
573 stream_result.map_err(|_| NetworkError::DialFailed("dial cancelled".into()))
574 }
575 fail_result = fail_rx => {
576 let error = fail_result.unwrap_or_else(|_| "dial watcher dropped".to_string());
577 Err(NetworkError::DialFailed(error))
578 }
579 };
580
581 fail_watcher.abort();
583
584 result
585 })
586 .await
587 .map_err(|_| NetworkError::DialTimeout(DIAL_TIMEOUT))?;
588
589 if result.is_err() {
591 bridge.remove_dial(&request_id).await;
592 }
593
594 result
595 }
596
597 async fn listen_tcp(
598 &self,
599 port: u16,
600 ) -> Result<NetworkTcpListener, NetworkError> {
601 if *self.state.read().await != ProviderState::Running {
602 return Err(NetworkError::NotRunning);
603 }
604
605 let bridge = self
606 .bridge
607 .lock()
608 .await
609 .clone()
610 .ok_or(NetworkError::NotRunning)?;
611
612 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
614
615 let mut event_rx = {
617 let sidecar_guard = self.sidecar.lock().await;
618 let sidecar = sidecar_guard
619 .as_ref()
620 .ok_or(NetworkError::NotRunning)?;
621
622 let event_rx = sidecar.subscribe();
623 sidecar.send_listen(port, None).await?;
624 event_rx
625 };
626
627 let actual_port = tokio::time::timeout(Duration::from_secs(10), async {
631 loop {
632 match event_rx.recv().await {
633 Ok(SidecarInternalEvent::Listening { port: p })
634 if port == 0 || p == port =>
635 {
636 return Ok(p);
637 }
638 Ok(SidecarInternalEvent::Error { code, message }) => {
639 return Err(NetworkError::ListenFailed(format!("[{code}] {message}")));
640 }
641 Err(broadcast::error::RecvError::Closed) => {
642 return Err(NetworkError::SidecarError("event channel closed".into()));
643 }
644 _ => continue,
645 }
646 }
647 })
648 .await
649 .map_err(|_| NetworkError::ListenFailed("listen confirmation timed out".into()))??;
650
651 bridge.register_listener(actual_port, tx).await;
653
654 Ok(NetworkTcpListener { port: actual_port, incoming: rx })
655 }
656
657 async fn unlisten_tcp(&self, port: u16) -> Result<(), NetworkError> {
658 if *self.state.read().await != ProviderState::Running {
659 return Err(NetworkError::NotRunning);
660 }
661
662 let bridge = self
663 .bridge
664 .lock()
665 .await
666 .clone()
667 .ok_or(NetworkError::NotRunning)?;
668
669 bridge.remove_listener(port).await;
671
672 {
674 let sidecar_guard = self.sidecar.lock().await;
675 let sidecar = sidecar_guard
676 .as_ref()
677 .ok_or(NetworkError::NotRunning)?;
678 sidecar.send_unlisten(port).await?;
679 }
680
681 Ok(())
682 }
683
684 async fn ping(&self, addr: &str) -> Result<PingResult, NetworkError> {
685 if *self.state.read().await != ProviderState::Running {
686 return Err(NetworkError::NotRunning);
687 }
688
689 let target = addr.to_string();
690
691 let mut event_rx = {
693 let sidecar_guard = self.sidecar.lock().await;
694 let sidecar = sidecar_guard
695 .as_ref()
696 .ok_or(NetworkError::NotRunning)?;
697
698 let event_rx = sidecar.subscribe();
699 sidecar.send_ping(target.clone(), None).await?;
700 event_rx
701 };
702
703 let result = tokio::time::timeout(Duration::from_secs(15), async {
705 loop {
706 match event_rx.recv().await {
707 Ok(SidecarInternalEvent::PingResult(data)) if data.target == target => {
708 if !data.error.is_empty() {
709 return Err(NetworkError::PingFailed(data.error));
710 }
711 let connection = if data.direct {
712 "direct".to_string()
713 } else if !data.relay.is_empty() {
714 format!("relay:{}", data.relay)
715 } else {
716 "unknown".to_string()
717 };
718 return Ok(PingResult {
719 latency: Duration::from_secs_f64(data.latency_ms / 1000.0),
720 connection,
721 peer_addr: if data.peer_addr.is_empty() {
722 None
723 } else {
724 Some(data.peer_addr)
725 },
726 });
727 }
728 Err(broadcast::error::RecvError::Closed) => {
729 return Err(NetworkError::SidecarError("event channel closed".into()));
730 }
731 _ => continue,
732 }
733 }
734 })
735 .await
736 .map_err(|_| NetworkError::PingFailed("ping timed out".into()))?;
737
738 result
739 }
740
741 async fn bind_udp(&self, port: u16) -> Result<super::super::NetworkUdpSocket, NetworkError> {
742 if *self.state.read().await != ProviderState::Running {
743 return Err(NetworkError::NotRunning);
744 }
745
746 let mut event_rx = {
748 let sidecar_guard = self.sidecar.lock().await;
749 let sidecar = sidecar_guard
750 .as_ref()
751 .ok_or(NetworkError::NotRunning)?;
752
753 let event_rx = sidecar.subscribe();
754 sidecar.send_listen_packet(port).await?;
755 event_rx
756 };
757
758 let local_port = tokio::time::timeout(Duration::from_secs(10), async {
760 loop {
761 match event_rx.recv().await {
762 Ok(SidecarInternalEvent::ListeningPacket {
763 port: p,
764 local_port,
765 }) if p == port => {
766 return Ok(local_port);
767 }
768 Ok(SidecarInternalEvent::Error { code, message }) => {
769 return Err(NetworkError::ListenFailed(format!(
770 "UDP bind failed [{code}] {message}"
771 )));
772 }
773 Err(broadcast::error::RecvError::Closed) => {
774 return Err(NetworkError::SidecarError(
775 "event channel closed".into(),
776 ));
777 }
778 _ => continue,
779 }
780 }
781 })
782 .await
783 .map_err(|_| {
784 NetworkError::ListenFailed("UDP listenPacket confirmation timed out".into())
785 })??;
786
787 let local_socket = tokio::net::UdpSocket::bind("127.0.0.1:0")
789 .await
790 .map_err(|e| {
791 NetworkError::Internal(format!("failed to bind local UDP socket: {e}"))
792 })?;
793
794 local_socket
795 .connect(format!("127.0.0.1:{local_port}"))
796 .await
797 .map_err(|e| {
798 NetworkError::Internal(format!(
799 "failed to connect local UDP socket to relay: {e}"
800 ))
801 })?;
802
803 let rust_local_addr = local_socket.local_addr().map_err(|e| {
804 NetworkError::Internal(format!("failed to get local UDP addr: {e}"))
805 })?;
806
807 local_socket
812 .send(b"TRUFFLE_UDP_REGISTER")
813 .await
814 .map_err(|e| {
815 NetworkError::Internal(format!("failed to send UDP registration: {e}"))
816 })?;
817
818 tracing::info!(
819 tsnet_port = port,
820 relay_port = local_port,
821 rust_local_addr = %rust_local_addr,
822 "UDP socket bound via tsnet relay (registered)"
823 );
824
825 Ok(super::super::NetworkUdpSocket::new(local_socket, port))
826 }
827
828 async fn health(&self) -> HealthInfo {
829 self.health.read().await.clone()
830 }
831}
832
833impl TailscaleProvider {
834 pub async fn local_identity_async(&self) -> NodeIdentity {
839 self.identity.read().unwrap().clone()
840 }
841
842 pub async fn local_addr_async(&self) -> PeerAddr {
847 self.local_addr.read().unwrap().clone()
848 }
849}