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, ListenOpts, NetworkError, NetworkPeer,
22 NetworkPeerEvent, NetworkTcpListener, NodeIdentity, PeerAddr, PingResult, ProxyAddParams,
23 ProxyAddResult, ProxyListEntry, ProxyRuntimeError,
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 proxy_error_tx: broadcast::Sender<ProxyRuntimeError>,
141
142 sidecar_protocol_version: Arc<std::sync::atomic::AtomicU32>,
146}
147
148impl TailscaleProvider {
149 pub fn new(config: TailscaleConfig) -> Self {
153 let (peer_event_tx, _) = broadcast::channel(256);
154 let (proxy_error_tx, _) = broadcast::channel(64);
155
156 let initial_identity = NodeIdentity {
160 app_id: config.app_id.clone(),
161 device_id: config.device_id.clone(),
162 device_name: config.device_name.clone(),
163 tailscale_hostname: config.hostname.clone(),
164 tailscale_id: String::new(),
165 dns_name: None,
166 ip: None,
167 };
168
169 Self {
170 config,
171 state: Arc::new(RwLock::new(ProviderState::Stopped)),
172 identity: Arc::new(std::sync::RwLock::new(initial_identity)),
173 local_addr: Arc::new(std::sync::RwLock::new(PeerAddr::default())),
174 peers: Arc::new(RwLock::new(HashMap::new())),
175 peer_event_tx,
176 health: Arc::new(RwLock::new(HealthInfo {
177 state: "stopped".to_string(),
178 healthy: false,
179 ..Default::default()
180 })),
181 sidecar: Arc::new(Mutex::new(None)),
182 bridge: Arc::new(Mutex::new(None)),
183 bridge_shutdown_tx: Arc::new(Mutex::new(None)),
184 session_token: Arc::new(RwLock::new([0u8; 32])),
185 local_tailscale_id: Arc::new(std::sync::RwLock::new(None)),
186 proxy_error_tx,
187 sidecar_protocol_version: Arc::new(std::sync::atomic::AtomicU32::new(0)),
188 }
189 }
190
191 fn generate_session_token() -> Result<[u8; 32], NetworkError> {
193 let mut token = [0u8; 32];
194 getrandom::getrandom(&mut token).map_err(|e| {
195 NetworkError::Internal(format!("failed to generate session token: {e}"))
196 })?;
197 Ok(token)
198 }
199
200 fn sidecar_peer_to_network_peer(peer: &super::protocol::SidecarPeer) -> NetworkPeer {
202 let ip = peer
203 .tailscale_ips
204 .first()
205 .and_then(|s| s.parse::<IpAddr>().ok())
206 .unwrap_or(IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED));
207
208 NetworkPeer {
209 id: peer.id.clone(),
210 hostname: peer.hostname.clone(),
211 ip,
212 online: peer.online,
213 cur_addr: if peer.cur_addr.is_empty() {
214 None
215 } else {
216 Some(peer.cur_addr.clone())
217 },
218 relay: if peer.relay.is_empty() {
219 None
220 } else {
221 Some(peer.relay.clone())
222 },
223 os: if peer.os.is_empty() {
224 None
225 } else {
226 Some(peer.os.clone())
227 },
228 last_seen: peer.last_seen.clone(),
229 key_expiry: peer.key_expiry.clone(),
230 dns_name: Some(peer.dns_name.clone()),
231 }
232 }
233
234 #[allow(clippy::too_many_arguments)]
237 fn spawn_event_processor(
238 mut sidecar_rx: broadcast::Receiver<SidecarInternalEvent>,
239 peers: Arc<RwLock<HashMap<String, NetworkPeer>>>,
240 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
241 health: Arc<RwLock<HealthInfo>>,
242 identity: Arc<std::sync::RwLock<NodeIdentity>>,
243 local_addr: Arc<std::sync::RwLock<PeerAddr>>,
244 local_tailscale_id: Arc<std::sync::RwLock<Option<String>>>,
245 state: Arc<RwLock<ProviderState>>,
246 started_tx: Option<oneshot::Sender<Result<(), NetworkError>>>,
247 app_id: String,
248 proxy_error_tx: broadcast::Sender<ProxyRuntimeError>,
249 sidecar_protocol_version: Arc<std::sync::atomic::AtomicU32>,
250 ) {
251 tokio::spawn(async move {
252 let mut started_tx = started_tx;
253
254 loop {
255 match sidecar_rx.recv().await {
256 Ok(event) => {
257 match event {
258 SidecarInternalEvent::Started {
259 hostname,
260 dns_name,
261 tailscale_ip,
262 node_id,
263 protocol_version,
264 } => {
265 sidecar_protocol_version.store(
267 protocol_version.unwrap_or(0),
268 std::sync::atomic::Ordering::Relaxed,
269 );
270 let ip: Option<IpAddr> = tailscale_ip.parse().ok();
271
272 {
273 let mut id = identity.write().unwrap();
274 id.tailscale_hostname = hostname.clone();
280 id.dns_name = Some(dns_name.clone());
281 id.ip = ip;
282 if !node_id.is_empty() {
283 id.tailscale_id = node_id.clone();
284 }
285 }
286
287 if !node_id.is_empty() {
290 *local_tailscale_id.write().unwrap() = Some(node_id);
291 }
292
293 {
294 let mut addr = local_addr.write().unwrap();
295 addr.hostname = hostname;
296 addr.dns_name = Some(dns_name);
297 addr.ip = ip;
298 }
299
300 {
301 let mut h = health.write().await;
302 h.state = "running".to_string();
303 h.healthy = true;
304 }
305
306 *state.write().await = ProviderState::Running;
307
308 if let Some(tx) = started_tx.take() {
310 let _ = tx.send(Ok(()));
311 }
312 }
313 SidecarInternalEvent::AuthRequired { auth_url } => {
314 tracing::info!("tailscale auth required: {auth_url}");
315 let _ = peer_event_tx
318 .send(NetworkPeerEvent::AuthRequired { url: auth_url });
319 }
320 SidecarInternalEvent::Stopped => {
321 *state.write().await = ProviderState::Stopped;
322 let mut h = health.write().await;
323 h.state = "stopped".to_string();
324 h.healthy = false;
325 tracing::info!("tailscale provider stopped");
326 return;
327 }
328 SidecarInternalEvent::StateChange { state: new_state } => {
329 let mut h = health.write().await;
330 h.state = new_state;
331 }
332 SidecarInternalEvent::KeyExpiring { expires_at } => {
333 let mut h = health.write().await;
334 h.key_expiry = Some(expires_at);
335 }
336 SidecarInternalEvent::HealthWarning { warnings } => {
337 let mut h = health.write().await;
338 h.warnings = warnings;
339 h.healthy = h.warnings.is_empty();
340 }
341 SidecarInternalEvent::PeersReceived(sidecar_peers) => {
342 let mut peer_map = peers.write().await;
343 let self_id = local_tailscale_id.read().unwrap().clone();
349 let new_peers: HashMap<String, NetworkPeer> = sidecar_peers
352 .iter()
353 .filter(|p| {
354 if let Some(ref me) = self_id {
355 if p.id == *me {
356 return false;
357 }
358 }
359 is_app_peer(&p.hostname, &app_id)
360 })
361 .map(|p| {
362 let np = Self::sidecar_peer_to_network_peer(p);
363 (np.id.clone(), np)
364 })
365 .collect();
366
367 for (id, new_peer) in &new_peers {
369 if let Some(_existing) = peer_map.get(id) {
370 let _ = peer_event_tx
371 .send(NetworkPeerEvent::Updated(new_peer.clone()));
372 } else {
373 let _ = peer_event_tx
374 .send(NetworkPeerEvent::Joined(new_peer.clone()));
375 }
376 }
377 for id in peer_map.keys() {
378 if !new_peers.contains_key(id) {
379 let _ =
380 peer_event_tx.send(NetworkPeerEvent::Left(id.clone()));
381 }
382 }
383
384 *peer_map = new_peers;
385 }
386 SidecarInternalEvent::PeerChanged(change) => {
387 let mut peer_map = peers.write().await;
388 let self_id = local_tailscale_id.read().unwrap().clone();
391 match change.change_type.as_str() {
392 "joined" => {
393 if let Some(p) = change.peer {
394 if let Some(ref me) = self_id {
395 if p.id == *me {
396 continue;
397 }
398 }
399 if is_app_peer(&p.hostname, &app_id) {
400 let np = Self::sidecar_peer_to_network_peer(&p);
401 peer_map.insert(np.id.clone(), np.clone());
402 let _ = peer_event_tx
403 .send(NetworkPeerEvent::Joined(np));
404 }
405 }
406 }
407 "left" => {
408 if peer_map.remove(&change.peer_id).is_some() {
409 let _ = peer_event_tx
410 .send(NetworkPeerEvent::Left(change.peer_id));
411 }
412 }
413 "updated" => {
414 if let Some(p) = change.peer {
415 if let Some(ref me) = self_id {
416 if p.id == *me {
417 continue;
418 }
419 }
420 if is_app_peer(&p.hostname, &app_id) {
421 let np = Self::sidecar_peer_to_network_peer(&p);
422 peer_map.insert(np.id.clone(), np.clone());
423 let _ = peer_event_tx
424 .send(NetworkPeerEvent::Updated(np));
425 }
426 }
427 }
428 other => {
429 tracing::warn!("unknown peer change type: {other}");
430 }
431 }
432 }
433 SidecarInternalEvent::Error { code, message } => {
434 tracing::error!("sidecar error [{code}]: {message}");
435 if let Some(tx) = started_tx.take() {
437 let _ = tx.send(Err(NetworkError::SidecarError(format!(
438 "[{code}] {message}"
439 ))));
440 }
441 }
442 SidecarInternalEvent::ProcessExited { exit_code } => {
443 tracing::error!("sidecar process exited: {exit_code:?}");
444 *state.write().await = ProviderState::Stopped;
445 let mut h = health.write().await;
446 h.state = "crashed".to_string();
447 h.healthy = false;
448 if let Some(tx) = started_tx.take() {
449 let _ = tx.send(Err(NetworkError::SidecarError(format!(
450 "process exited with code {exit_code:?}"
451 ))));
452 }
453 return;
454 }
455 SidecarInternalEvent::ProxyError { id, code, message } => {
456 tracing::warn!("proxy runtime error [{code}] for {id}: {message}");
461 let _ =
462 proxy_error_tx.send(ProxyRuntimeError { id, code, message });
463 }
464 _ => {}
467 }
468 }
469 Err(broadcast::error::RecvError::Lagged(n)) => {
470 tracing::warn!("event processor lagged by {n} events");
471 }
472 Err(broadcast::error::RecvError::Closed) => {
473 tracing::info!("sidecar event channel closed, stopping event processor");
474 return;
475 }
476 }
477 }
478 });
479 }
480}
481
482pub(crate) fn is_app_peer(hostname: &str, app_id: &str) -> bool {
492 let prefix = format!("truffle-{app_id}-");
493 hostname.len() > prefix.len() && hostname.starts_with(&prefix)
494}
495
496impl super::super::NetworkProvider for TailscaleProvider {
497 async fn start(&mut self) -> Result<(), NetworkError> {
498 {
499 let current_state = *self.state.read().await;
500 if current_state != ProviderState::Stopped {
501 return Err(NetworkError::AlreadyRunning);
502 }
503 }
504 *self.state.write().await = ProviderState::Starting;
505
506 let token = Self::generate_session_token()?;
508 let token_hex = hex::encode(token);
509 *self.session_token.write().await = token;
510
511 let bridge = Bridge::bind(token).await?;
513 let bridge_port = bridge.local_port()?;
514 let bridge = Arc::new(bridge);
515
516 let (bridge_shutdown_tx, bridge_shutdown_rx) = tokio::sync::watch::channel(false);
518
519 {
521 let bridge_clone = bridge.clone();
522 tokio::spawn(async move {
523 bridge_clone.run(bridge_shutdown_rx).await;
524 });
525 }
526
527 *self.bridge.lock().await = Some(bridge.clone());
528 *self.bridge_shutdown_tx.lock().await = Some(bridge_shutdown_tx);
529
530 let sidecar_config = SidecarConfig {
532 binary_path: self.config.binary_path.clone(),
533 hostname: self.config.hostname.clone(),
534 state_dir: self.config.state_dir.clone(),
535 auth_key: self.config.auth_key.clone(),
536 bridge_port,
537 session_token_hex: token_hex,
538 ephemeral: self.config.ephemeral,
539 tags: self.config.tags.clone(),
540 idle_timeout_secs: self.config.idle_timeout_secs,
541 };
542
543 let (sidecar, sidecar_rx) = GoSidecar::spawn(sidecar_config.clone()).await?;
545
546 let (started_tx, started_rx) = oneshot::channel();
548
549 Self::spawn_event_processor(
551 sidecar_rx,
552 self.peers.clone(),
553 self.peer_event_tx.clone(),
554 self.health.clone(),
555 self.identity.clone(),
556 self.local_addr.clone(),
557 self.local_tailscale_id.clone(),
558 self.state.clone(),
559 Some(started_tx),
560 self.config.app_id.clone(),
561 self.proxy_error_tx.clone(),
562 self.sidecar_protocol_version.clone(),
563 );
564
565 sidecar.send_start(&sidecar_config).await?;
567
568 *self.sidecar.lock().await = Some(sidecar);
569
570 let auth_timeout = Duration::from_secs(300);
574 let result = tokio::time::timeout(auth_timeout, started_rx)
575 .await
576 .map_err(|_| {
577 NetworkError::StartFailed(
578 "timed out waiting for authentication (5 min). \
579 Subscribe to peer_events() to display auth URLs."
580 .into(),
581 )
582 })?
583 .map_err(|_| NetworkError::StartFailed("start signal channel dropped".into()))?;
584
585 match result {
586 Ok(()) => {
587 if let Some(ref sidecar) = *self.sidecar.lock().await {
589 let _ = sidecar.send_get_peers().await;
590 let _ = sidecar.send_watch_peers().await;
592 }
593 tracing::info!("tailscale provider started successfully");
594 Ok(())
595 }
596 Err(e) => {
597 *self.state.write().await = ProviderState::Stopped;
598 Err(e)
599 }
600 }
601 }
602
603 async fn stop(&self) -> Result<(), NetworkError> {
604 *self.state.write().await = ProviderState::Stopping;
605
606 if let Some(sidecar) = self.sidecar.lock().await.take() {
608 sidecar.shutdown().await;
609 }
610
611 if let Some(tx) = self.bridge_shutdown_tx.lock().await.take() {
613 let _ = tx.send(true);
614 }
615 *self.bridge.lock().await = None;
616
617 self.peers.write().await.clear();
619 *self.state.write().await = ProviderState::Stopped;
620 let mut h = self.health.write().await;
621 h.state = "stopped".to_string();
622 h.healthy = false;
623
624 tracing::info!("tailscale provider stopped");
625 Ok(())
626 }
627
628 fn local_identity(&self) -> NodeIdentity {
629 self.identity.read().unwrap().clone()
630 }
631
632 fn local_addr(&self) -> PeerAddr {
633 self.local_addr.read().unwrap().clone()
634 }
635
636 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
637 self.peer_event_tx.subscribe()
638 }
639
640 async fn peers(&self) -> Vec<NetworkPeer> {
641 self.peers.read().await.values().cloned().collect()
642 }
643
644 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
645 self.dial_tcp_opts(addr, port, DialOpts::default()).await
646 }
647
648 async fn dial_tcp_opts(
649 &self,
650 addr: &str,
651 port: u16,
652 opts: DialOpts,
653 ) -> Result<TcpStream, NetworkError> {
654 if *self.state.read().await != ProviderState::Running {
655 return Err(NetworkError::NotRunning);
656 }
657
658 let bridge = self
659 .bridge
660 .lock()
661 .await
662 .clone()
663 .ok_or(NetworkError::NotRunning)?;
664
665 let request_id = uuid::Uuid::new_v4().to_string();
667
668 let dial_rx = bridge.register_dial(request_id.clone()).await;
670
671 let event_rx = {
673 let sidecar_guard = self.sidecar.lock().await;
674 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
675
676 let event_rx = sidecar.subscribe();
677
678 sidecar
679 .send_dial(request_id.clone(), addr.to_string(), port, opts.tls)
680 .await?;
681
682 event_rx
683 };
684
685 Self::await_dial_result(&bridge, &request_id, dial_rx, event_rx, DIAL_TIMEOUT).await
690 }
691
692 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
693 self.listen_tcp_opts(port, ListenOpts::default()).await
694 }
695
696 async fn listen_tcp_opts(
697 &self,
698 port: u16,
699 opts: ListenOpts,
700 ) -> Result<NetworkTcpListener, NetworkError> {
701 if *self.state.read().await != ProviderState::Running {
702 return Err(NetworkError::NotRunning);
703 }
704
705 let bridge = self
706 .bridge
707 .lock()
708 .await
709 .clone()
710 .ok_or(NetworkError::NotRunning)?;
711
712 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
714
715 let mut event_rx = {
717 let sidecar_guard = self.sidecar.lock().await;
718 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
719
720 let event_rx = sidecar.subscribe();
721 let tls = if opts.tls { Some(true) } else { None };
726 sidecar.send_listen(port, tls).await?;
727 event_rx
728 };
729
730 let actual_port = tokio::time::timeout(Duration::from_secs(10), async {
734 loop {
735 match event_rx.recv().await {
736 Ok(SidecarInternalEvent::Listening { port: p }) if port == 0 || p == port => {
737 return Ok(p);
738 }
739 Ok(SidecarInternalEvent::Error { code, message }) => {
740 return Err(NetworkError::ListenFailed(format!("[{code}] {message}")));
741 }
742 Err(broadcast::error::RecvError::Closed) => {
743 return Err(NetworkError::SidecarError("event channel closed".into()));
744 }
745 _ => continue,
746 }
747 }
748 })
749 .await
750 .map_err(|_| NetworkError::ListenFailed("listen confirmation timed out".into()))??;
751
752 bridge.register_listener(actual_port, tx).await;
754
755 Ok(NetworkTcpListener {
756 port: actual_port,
757 incoming: rx,
758 })
759 }
760
761 async fn unlisten_tcp(&self, port: u16) -> Result<(), NetworkError> {
762 if *self.state.read().await != ProviderState::Running {
763 return Err(NetworkError::NotRunning);
764 }
765
766 let bridge = self
767 .bridge
768 .lock()
769 .await
770 .clone()
771 .ok_or(NetworkError::NotRunning)?;
772
773 bridge.remove_listener(port).await;
775
776 {
778 let sidecar_guard = self.sidecar.lock().await;
779 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
780 sidecar.send_unlisten(port).await?;
781 }
782
783 Ok(())
784 }
785
786 async fn ping(&self, addr: &str) -> Result<PingResult, NetworkError> {
787 if *self.state.read().await != ProviderState::Running {
788 return Err(NetworkError::NotRunning);
789 }
790
791 let target = addr.to_string();
792
793 let mut event_rx = {
795 let sidecar_guard = self.sidecar.lock().await;
796 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
797
798 let event_rx = sidecar.subscribe();
799 sidecar.send_ping(target.clone(), None).await?;
800 event_rx
801 };
802
803 let result = tokio::time::timeout(Duration::from_secs(15), async {
805 loop {
806 match event_rx.recv().await {
807 Ok(SidecarInternalEvent::PingResult(data)) if data.target == target => {
808 if !data.error.is_empty() {
809 return Err(NetworkError::PingFailed(data.error));
810 }
811 let connection = if data.direct {
812 "direct".to_string()
813 } else if !data.relay.is_empty() {
814 format!("relay:{}", data.relay)
815 } else {
816 "unknown".to_string()
817 };
818 return Ok(PingResult {
819 latency: Duration::from_secs_f64(data.latency_ms / 1000.0),
820 connection,
821 peer_addr: if data.peer_addr.is_empty() {
822 None
823 } else {
824 Some(data.peer_addr)
825 },
826 });
827 }
828 Err(broadcast::error::RecvError::Closed) => {
829 return Err(NetworkError::SidecarError("event channel closed".into()));
830 }
831 _ => continue,
832 }
833 }
834 })
835 .await
836 .map_err(|_| NetworkError::PingFailed("ping timed out".into()))?;
837
838 result
839 }
840
841 async fn bind_udp(&self, port: u16) -> Result<super::super::NetworkUdpSocket, NetworkError> {
842 if *self.state.read().await != ProviderState::Running {
843 return Err(NetworkError::NotRunning);
844 }
845
846 let mut event_rx = {
848 let sidecar_guard = self.sidecar.lock().await;
849 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
850
851 let event_rx = sidecar.subscribe();
852 sidecar.send_listen_packet(port).await?;
853 event_rx
854 };
855
856 let local_port = tokio::time::timeout(Duration::from_secs(10), async {
858 loop {
859 match event_rx.recv().await {
860 Ok(SidecarInternalEvent::ListeningPacket {
861 port: p,
862 local_port,
863 }) if p == port => {
864 return Ok(local_port);
865 }
866 Ok(SidecarInternalEvent::Error { code, message }) => {
867 return Err(NetworkError::ListenFailed(format!(
868 "UDP bind failed [{code}] {message}"
869 )));
870 }
871 Err(broadcast::error::RecvError::Closed) => {
872 return Err(NetworkError::SidecarError("event channel closed".into()));
873 }
874 _ => continue,
875 }
876 }
877 })
878 .await
879 .map_err(|_| {
880 NetworkError::ListenFailed("UDP listenPacket confirmation timed out".into())
881 })??;
882
883 let local_socket = tokio::net::UdpSocket::bind("127.0.0.1:0")
885 .await
886 .map_err(|e| NetworkError::Internal(format!("failed to bind local UDP socket: {e}")))?;
887
888 local_socket
889 .connect(format!("127.0.0.1:{local_port}"))
890 .await
891 .map_err(|e| {
892 NetworkError::Internal(format!("failed to connect local UDP socket to relay: {e}"))
893 })?;
894
895 let rust_local_addr = local_socket
896 .local_addr()
897 .map_err(|e| NetworkError::Internal(format!("failed to get local UDP addr: {e}")))?;
898
899 local_socket
904 .send(b"TRUFFLE_UDP_REGISTER")
905 .await
906 .map_err(|e| NetworkError::Internal(format!("failed to send UDP registration: {e}")))?;
907
908 tracing::info!(
909 tsnet_port = port,
910 relay_port = local_port,
911 rust_local_addr = %rust_local_addr,
912 "UDP socket bound via tsnet relay (registered)"
913 );
914
915 Ok(super::super::NetworkUdpSocket::new(local_socket, port))
916 }
917
918 async fn health(&self) -> HealthInfo {
919 self.health.read().await.clone()
920 }
921
922 fn proxy_runtime_errors(&self) -> Option<broadcast::Receiver<ProxyRuntimeError>> {
923 Some(self.proxy_error_tx.subscribe())
924 }
925
926 async fn proxy_add(&self, config: ProxyAddParams) -> Result<ProxyAddResult, NetworkError> {
929 if *self.state.read().await != ProviderState::Running {
930 return Err(NetworkError::NotRunning);
931 }
932
933 let uses_v2 = !config.tls
937 || config.allow_non_loopback
938 || !config.allow.is_empty()
939 || !config.routes.is_empty();
940 let version = self
941 .sidecar_protocol_version
942 .load(std::sync::atomic::Ordering::Relaxed);
943 if uses_v2 && version < 2 {
944 return Err(NetworkError::ProxyError(format!(
945 "sidecar protocol v{version} predates RFC 023 — routes, allow lists, \
946 tls: false, and non-loopback targets need a v2 sidecar; upgrade the \
947 sidecar binary"
948 )));
949 }
950
951 let mut event_rx = {
953 let sidecar_guard = self.sidecar.lock().await;
954 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
955 let event_rx = sidecar.subscribe();
956 sidecar
957 .send_proxy_add(ProxyAddCommandData {
958 id: config.id.clone(),
959 name: config.name.clone(),
960 listen_port: config.listen_port,
961 target_host: config.target_host.clone(),
962 target_port: config.target_port,
963 target_scheme: config.target_scheme.clone(),
964 tls: config.tls,
965 allow_non_loopback: config.allow_non_loopback,
966 allow: config.allow.clone(),
967 routes: config.routes.clone(),
968 })
969 .await?;
970 event_rx
971 };
972
973 tokio::time::timeout(Duration::from_secs(10), async {
975 loop {
976 match event_rx.recv().await {
977 Ok(SidecarInternalEvent::ProxyAdded {
978 id,
979 listen_port,
980 url,
981 }) if id == config.id => {
982 return Ok(ProxyAddResult {
983 id,
984 listen_port,
985 url,
986 });
987 }
988 Ok(SidecarInternalEvent::ProxyError { id, code, message })
989 if id == config.id =>
990 {
991 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
992 }
993 Ok(SidecarInternalEvent::Error { code, message }) => {
994 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
995 }
996 Err(broadcast::error::RecvError::Closed) => {
997 return Err(NetworkError::SidecarError("event channel closed".into()));
998 }
999 Err(broadcast::error::RecvError::Lagged(_)) => {
1000 return Err(NetworkError::SidecarError(
1001 "event channel lagged: proxy confirmation may have been lost".into(),
1002 ));
1003 }
1004 Ok(_) => continue,
1005 }
1006 }
1007 })
1008 .await
1009 .map_err(|_| NetworkError::ProxyError("proxy add timed out".into()))?
1010 }
1011
1012 async fn proxy_remove(&self, id: &str) -> Result<(), NetworkError> {
1013 if *self.state.read().await != ProviderState::Running {
1014 return Err(NetworkError::NotRunning);
1015 }
1016
1017 let target_id = id.to_string();
1018
1019 let mut event_rx = {
1021 let sidecar_guard = self.sidecar.lock().await;
1022 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
1023 let event_rx = sidecar.subscribe();
1024 sidecar.send_proxy_remove(id).await?;
1025 event_rx
1026 };
1027
1028 tokio::time::timeout(Duration::from_secs(10), async {
1030 loop {
1031 match event_rx.recv().await {
1032 Ok(SidecarInternalEvent::ProxyRemoved { id }) if id == target_id => {
1033 return Ok(());
1034 }
1035 Ok(SidecarInternalEvent::ProxyError { id, code, message })
1036 if id == target_id =>
1037 {
1038 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
1039 }
1040 Ok(SidecarInternalEvent::Error { code, message }) => {
1041 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
1042 }
1043 Err(broadcast::error::RecvError::Closed) => {
1044 return Err(NetworkError::SidecarError("event channel closed".into()));
1045 }
1046 Err(broadcast::error::RecvError::Lagged(_)) => {
1047 return Err(NetworkError::SidecarError(
1048 "event channel lagged: proxy confirmation may have been lost".into(),
1049 ));
1050 }
1051 Ok(_) => continue,
1052 }
1053 }
1054 })
1055 .await
1056 .map_err(|_| NetworkError::ProxyError("proxy remove timed out".into()))?
1057 }
1058
1059 async fn proxy_list(&self) -> Result<Vec<ProxyListEntry>, NetworkError> {
1060 if *self.state.read().await != ProviderState::Running {
1061 return Err(NetworkError::NotRunning);
1062 }
1063
1064 let mut event_rx = {
1066 let sidecar_guard = self.sidecar.lock().await;
1067 let sidecar = sidecar_guard.as_ref().ok_or(NetworkError::NotRunning)?;
1068 let event_rx = sidecar.subscribe();
1069 sidecar.send_proxy_list().await?;
1070 event_rx
1071 };
1072
1073 tokio::time::timeout(Duration::from_secs(10), async {
1075 loop {
1076 match event_rx.recv().await {
1077 Ok(SidecarInternalEvent::ProxyList { proxies }) => {
1078 return Ok(proxies
1079 .into_iter()
1080 .map(|p| ProxyListEntry {
1081 id: p.id,
1082 name: p.name,
1083 listen_port: p.listen_port,
1084 target_host: p.target_host,
1085 target_port: p.target_port,
1086 target_scheme: p.target_scheme,
1087 url: p.url,
1088 })
1089 .collect());
1090 }
1091 Ok(SidecarInternalEvent::Error { code, message }) => {
1092 return Err(NetworkError::ProxyError(format!("[{code}] {message}")));
1093 }
1094 Err(broadcast::error::RecvError::Closed) => {
1095 return Err(NetworkError::SidecarError("event channel closed".into()));
1096 }
1097 Err(broadcast::error::RecvError::Lagged(_)) => {
1098 return Err(NetworkError::SidecarError(
1099 "event channel lagged: proxy confirmation may have been lost".into(),
1100 ));
1101 }
1102 Ok(_) => continue,
1103 }
1104 }
1105 })
1106 .await
1107 .map_err(|_| NetworkError::ProxyError("proxy list timed out".into()))?
1108 }
1109}
1110
1111impl TailscaleProvider {
1112 pub async fn local_identity_async(&self) -> NodeIdentity {
1117 self.identity.read().unwrap().clone()
1118 }
1119
1120 pub async fn local_addr_async(&self) -> PeerAddr {
1125 self.local_addr.read().unwrap().clone()
1126 }
1127
1128 pub(super) async fn await_dial_result(
1138 bridge: &Bridge,
1139 request_id: &str,
1140 dial_rx: oneshot::Receiver<TcpStream>,
1141 mut event_rx: broadcast::Receiver<SidecarInternalEvent>,
1142 timeout: Duration,
1143 ) -> Result<TcpStream, NetworkError> {
1144 let fail_request_id = request_id.to_string();
1149 let (fail_tx, fail_rx) = oneshot::channel::<String>();
1150 let fail_watcher: JoinHandle<()> = tokio::spawn(async move {
1151 loop {
1152 match event_rx.recv().await {
1153 Ok(SidecarInternalEvent::DialFailed {
1154 request_id: rid,
1155 error,
1156 }) if rid == fail_request_id => {
1157 let _ = fail_tx.send(error);
1158 return;
1159 }
1160 Err(broadcast::error::RecvError::Closed) => {
1161 let _ = fail_tx.send("event channel closed".to_string());
1162 return;
1163 }
1164 _ => continue,
1165 }
1166 }
1167 });
1168
1169 let result = match tokio::time::timeout(timeout, async {
1170 tokio::select! {
1171 stream_result = dial_rx => {
1172 stream_result.map_err(|_| NetworkError::DialFailed("dial cancelled".into()))
1173 }
1174 fail_result = fail_rx => {
1175 let error = fail_result.unwrap_or_else(|_| "dial watcher dropped".to_string());
1176 Err(NetworkError::DialFailed(error))
1177 }
1178 }
1179 })
1180 .await
1181 {
1182 Ok(result) => result,
1183 Err(_) => Err(NetworkError::DialTimeout(timeout)),
1184 };
1185
1186 fail_watcher.abort();
1188
1189 if result.is_err() {
1191 bridge.remove_dial(request_id).await;
1192 }
1193
1194 result
1195 }
1196}
1197
1198#[cfg(test)]
1199mod config_debug_tests {
1200 use super::*;
1201
1202 #[test]
1203 fn tailscale_config_debug_redacts_auth_key() {
1204 let config = TailscaleConfig {
1205 binary_path: PathBuf::from("/opt/sidecar"),
1206 app_id: "demo".to_string(),
1207 device_id: "01JZZZZZZZZZZZZZZZZZZZZZZZ".to_string(),
1208 device_name: "dev".to_string(),
1209 hostname: "truffle-demo-dev".to_string(),
1210 state_dir: "/tmp/state".to_string(),
1211 auth_key: Some("dummy-auth-SECRET123".to_string()),
1212 ephemeral: None,
1213 tags: None,
1214 idle_timeout_secs: None,
1215 };
1216 let dbg = format!("{config:?}");
1217 assert!(!dbg.contains("SECRET123"));
1218 assert!(dbg.contains("[REDACTED]"));
1219 }
1220}