1use std::collections::HashMap;
34use std::net::IpAddr;
35use std::path::{Path, PathBuf};
36use std::sync::Arc;
37use std::time::Duration;
38use std::sync::RwLock as StdRwLock;
46
47use tokio::net::TcpStream;
48use tokio::sync::broadcast;
49#[cfg(test)]
53use tokio::sync::RwLock;
54
55use crate::envelope::codec::{EnvelopeCodec, JsonCodec};
56use crate::envelope::{Envelope, EnvelopeError};
57use crate::file_transfer::{self, FileTransferState};
58use crate::identity::{self, AppId, DeviceId, DeviceName};
59use crate::network::tailscale::{TailscaleConfig, TailscaleProvider};
60use crate::network::{DialOpts, HealthInfo, ListenOpts, NetworkProvider, NodeIdentity, PingResult};
61use crate::session::{PeerEvent, PeerRegistry, PeerState};
62use crate::transport::quic::{QuicConnection, QuicListener};
63use crate::transport::websocket::WebSocketTransport;
64use crate::transport::{DatagramSocket, RawListener, WsConfig};
65
66#[derive(Debug, Clone)]
75pub struct NamespacedMessage {
76 pub from: String,
78 pub namespace: String,
80 pub msg_type: String,
82 pub payload: serde_json::Value,
84 pub timestamp: Option<u64>,
86}
87
88impl NamespacedMessage {
89 pub fn payload_bytes(&self) -> Option<Vec<u8>> {
95 if self.msg_type != "bytes" {
96 return None;
97 }
98 let s = self.payload.get("data")?.as_str()?;
99 use base64::Engine as _;
100 base64::engine::general_purpose::STANDARD.decode(s).ok()
101 }
102}
103
104#[derive(Debug, Clone)]
115pub struct Peer {
116 pub device_id: Option<String>,
119 pub device_name: Option<String>,
121 pub display_name: String,
124 pub hostname: String,
126 pub tailscale_id: String,
128 pub peer_ref: String,
130 pub generation: u64,
132 pub ip: IpAddr,
134 pub online: bool,
136 pub ws_connected: bool,
138 pub connection_type: String,
140 pub os: Option<String>,
143 pub last_seen: Option<String>,
145}
146
147impl From<PeerState> for Peer {
148 fn from(s: PeerState) -> Self {
149 let (device_id, device_name, os_from_hello) = if s.identity_suppressed {
150 (None, None, None)
151 } else {
152 match s.identity.as_ref() {
153 Some(identity) => (
154 Some(identity.device_id.clone()),
155 Some(identity.device_name.clone()),
156 Some(identity.os.clone()),
157 ),
158 None => (None, None, None),
159 }
160 };
161
162 debug_assert!(
164 device_id
165 .as_ref()
166 .map(|d| d.as_str() != s.id.as_str())
167 .unwrap_or(true),
168 "RFC 022 I1 violated: device_id must not equal tailscale_id"
169 );
170
171 let display_name = device_name
172 .clone()
173 .filter(|n| !n.is_empty())
174 .unwrap_or_else(|| display_name_from_hostname(&s.name, &s.id));
175
176 let peer_ref = s.peer_ref();
177 let generation = s.generation;
178 let os = os_from_hello.or(s.os.clone());
179
180 Self {
181 device_id,
182 device_name,
183 display_name,
184 hostname: s.name,
185 tailscale_id: s.id,
186 peer_ref,
187 generation,
188 ip: s.ip,
189 online: s.online,
190 ws_connected: s.ws_connected,
191 connection_type: s.connection_type,
192 os,
193 last_seen: s.last_seen,
194 }
195 }
196}
197
198fn display_name_from_hostname(hostname: &str, tailscale_id: &str) -> String {
201 const PREFIX: &str = "truffle-";
202 if let Some(rest) = hostname.strip_prefix(PREFIX) {
203 if let Some((_, slug)) = rest.split_once('-') {
205 if !slug.is_empty() {
206 return slug.to_string();
207 }
208 }
209 }
210 if !hostname.is_empty() {
211 return hostname.to_string();
212 }
213 let short: String = tailscale_id.chars().take(8).collect();
215 if short.is_empty() {
216 "peer".to_string()
217 } else {
218 short
219 }
220}
221
222#[derive(Debug, thiserror::Error)]
228pub enum NodeError {
229 #[error("peer not found: {0}")]
231 PeerNotFound(String),
232
233 #[error("ambiguous peer query '{query}': {} candidates", candidates.len())]
235 AmbiguousPeer {
236 query: String,
238 candidates: Vec<String>,
240 },
241
242 #[error("peer gone: {0}")]
244 PeerGone(String),
245
246 #[error("connection failed: {0}")]
248 ConnectionFailed(String),
249
250 #[error("send failed: {0}")]
252 SendFailed(String),
253
254 #[error("envelope error: {0}")]
256 Envelope(#[from] EnvelopeError),
257
258 #[error("session error: {0}")]
260 Session(#[from] crate::session::SessionError),
261
262 #[error("network error: {0}")]
264 Network(#[from] crate::network::NetworkError),
265
266 #[error("transport error: {0}")]
268 Transport(#[from] crate::transport::TransportError),
269
270 #[error("not implemented: {0}")]
272 NotImplemented(String),
273
274 #[error("port {0} is reserved by truffle (the session WebSocket port)")]
276 ReservedPort(u16),
277
278 #[error("node stopped")]
280 Stopped,
281
282 #[error("build error: {0}")]
284 BuildError(String),
285
286 #[error("io error: {0}")]
288 Io(#[from] std::io::Error),
289}
290
291pub struct Node<N: NetworkProvider + 'static> {
307 pub(crate) network: Arc<N>,
309 session: Arc<PeerRegistry<N>>,
311 codec: Arc<dyn EnvelopeCodec>,
313 #[allow(dead_code)]
316 incoming_tx: broadcast::Sender<NamespacedMessage>,
317 namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
325 pub(crate) file_transfer_state: FileTransferState,
327 state_dir: PathBuf,
330 pub(crate) ws_port: u16,
334 pub(crate) proxy_state: Arc<crate::proxy::ProxyState>,
336 stopped: std::sync::atomic::AtomicBool,
340 pub(crate) tasks: NodeTasks,
342}
343
344pub(crate) struct NodeTasks {
353 pub(crate) cancel: tokio_util::sync::CancellationToken,
355 pub(crate) tracker: tokio_util::task::TaskTracker,
357 long_lived: std::sync::Mutex<Vec<tokio::task::JoinHandle<()>>>,
362}
363
364impl NodeTasks {
365 fn new() -> Self {
366 Self {
367 cancel: tokio_util::sync::CancellationToken::new(),
368 tracker: tokio_util::task::TaskTracker::new(),
369 long_lived: std::sync::Mutex::new(Vec::new()),
370 }
371 }
372
373 fn spawn_long_lived<F>(&self, fut: F)
376 where
377 F: std::future::Future<Output = ()> + Send + 'static,
378 {
379 let handle = self.tracker.spawn(fut);
380 self.long_lived.lock().unwrap().push(handle);
381 }
382
383 fn abort_long_lived(&self) {
385 for h in self.long_lived.lock().unwrap().drain(..) {
386 h.abort();
387 }
388 }
389}
390
391impl<N: NetworkProvider + 'static> Node<N> {
392 pub(crate) fn from_parts(
398 network: Arc<N>,
399 session: Arc<PeerRegistry<N>>,
400 codec: Arc<dyn EnvelopeCodec>,
401 ) -> Self {
402 let (incoming_tx, _) = broadcast::channel(1024);
403 let namespace_filters: Arc<
404 StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>,
405 > = Arc::new(StdRwLock::new(HashMap::new()));
406
407 let node = Self {
408 network,
409 session: session.clone(),
410 codec: codec.clone(),
411 incoming_tx: incoming_tx.clone(),
412 namespace_filters: namespace_filters.clone(),
413 file_transfer_state: FileTransferState::new(),
414 state_dir: PathBuf::new(),
415 ws_port: 9417,
416 proxy_state: Arc::new(crate::proxy::ProxyState::new()),
417 stopped: std::sync::atomic::AtomicBool::new(false),
418 tasks: NodeTasks::new(),
419 };
420
421 node.spawn_envelope_router(session, codec, incoming_tx, namespace_filters);
423
424 node.spawn_proxy_error_forwarder();
428
429 node
430 }
431
432 fn spawn_proxy_error_forwarder(&self) {
437 let Some(mut rx) = self.network.proxy_runtime_errors() else {
438 return;
439 };
440 let proxy_state = self.proxy_state.clone();
441 let cancel = self.tasks.cancel.clone();
442 self.tasks.spawn_long_lived(async move {
443 loop {
444 tokio::select! {
445 _ = cancel.cancelled() => {
446 tracing::debug!("proxy error forwarder: cancelled by stop()");
447 return;
448 }
449 event = rx.recv() => match event {
450 Ok(e) => proxy_state.record_runtime_error(e.id, e.code, e.message),
451 Err(broadcast::error::RecvError::Lagged(n)) => {
452 tracing::warn!("proxy error forwarder lagged by {n} events");
453 }
454 Err(broadcast::error::RecvError::Closed) => return,
455 }
456 }
457 }
458 });
459 }
460
461 fn spawn_envelope_router(
465 &self,
466 session: Arc<PeerRegistry<N>>,
467 codec: Arc<dyn EnvelopeCodec>,
468 incoming_tx: broadcast::Sender<NamespacedMessage>,
469 namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
470 ) {
471 let mut rx = session.subscribe();
472 let cancel = self.tasks.cancel.clone();
473
474 self.tasks.spawn_long_lived(async move {
475 loop {
476 let recv = tokio::select! {
477 _ = cancel.cancelled() => {
478 tracing::debug!("envelope router: cancelled by stop()");
479 break;
480 }
481 result = rx.recv() => result,
482 };
483 match recv {
484 Ok(msg) => {
485 if let Ok(envelope) = codec.decode(&msg.data) {
486 let namespaced = NamespacedMessage {
487 from: msg.from,
488 namespace: envelope.namespace.clone(),
489 msg_type: envelope.msg_type,
490 payload: envelope.payload,
491 timestamp: envelope.timestamp,
492 };
493
494 tracing::debug!(
495 from = %namespaced.from,
496 namespace = %namespaced.namespace,
497 msg_type = %namespaced.msg_type,
498 "envelope router: dispatching message"
499 );
500
501 let _ = incoming_tx.send(namespaced.clone());
503
504 let namespace = namespaced.namespace.clone();
512 let dead_subscriber = {
513 let filters = namespace_filters
514 .read()
515 .unwrap_or_else(std::sync::PoisonError::into_inner);
516 match filters.get(&namespace) {
517 Some(tx) => {
518 let send_result = tx.send(namespaced);
519 tracing::debug!(
520 namespace = %namespace,
521 subscriber_count = tx.receiver_count(),
522 sent = send_result.is_ok(),
523 "envelope router: sent to namespace subscriber"
524 );
525 send_result.is_err()
526 }
527 None => {
528 tracing::debug!(
529 namespace = %namespace,
530 "envelope router: no subscriber for namespace"
531 );
532 false
533 }
534 }
535 };
536
537 if dead_subscriber {
544 let mut filters = namespace_filters
545 .write()
546 .unwrap_or_else(std::sync::PoisonError::into_inner);
547 if filters
548 .get(&namespace)
549 .is_some_and(|tx| tx.receiver_count() == 0)
550 {
551 filters.remove(&namespace);
552 tracing::debug!(
553 namespace = %namespace,
554 "envelope router: pruned dead namespace subscriber"
555 );
556 }
557 }
558 } else {
559 tracing::warn!(
560 from = %msg.from,
561 data_len = msg.data.len(),
562 "node: failed to decode envelope from incoming message"
563 );
564 }
565 }
566 Err(broadcast::error::RecvError::Lagged(n)) => {
567 tracing::warn!(
568 missed = n,
569 "node: envelope router lagged, missed {n} messages"
570 );
571 continue;
572 }
573 Err(broadcast::error::RecvError::Closed) => {
574 tracing::debug!("node: session incoming channel closed, router exiting");
575 break;
576 }
577 }
578 }
579 });
580 }
581
582 pub fn builder() -> NodeBuilder {
586 NodeBuilder::default()
587 }
588
589 pub fn file_transfer(&self) -> file_transfer::FileTransfer<'_, N> {
596 file_transfer::FileTransfer::new(self)
597 }
598
599 pub fn proxy(&self) -> crate::proxy::Proxy<'_, N> {
606 crate::proxy::Proxy::new(self)
607 }
608
609 pub fn synced_store<T>(
618 self: &Arc<Self>,
619 store_id: &str,
620 ) -> Arc<crate::synced_store::SyncedStore<T>>
621 where
622 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
623 {
624 crate::synced_store::SyncedStore::new(self.clone(), store_id)
625 }
626
627 pub fn synced_store_with_backend<T>(
632 self: &Arc<Self>,
633 store_id: &str,
634 backend: std::sync::Arc<dyn crate::synced_store::StoreBackend>,
635 ) -> Arc<crate::synced_store::SyncedStore<T>>
636 where
637 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
638 {
639 crate::synced_store::SyncedStore::new_with_backend(self.clone(), store_id, backend)
640 }
641
642 pub(crate) fn with_state_dir(mut self, dir: PathBuf) -> Self {
646 self.state_dir = dir;
647 self
648 }
649
650 pub(crate) fn with_ws_port(mut self, port: u16) -> Self {
653 self.ws_port = port;
654 self
655 }
656
657 pub fn state_dir(&self) -> &Path {
661 &self.state_dir
662 }
663
664 pub async fn stop(&self) {
683 if self.stopped.swap(true, std::sync::atomic::Ordering::SeqCst) {
684 tracing::debug!("node: stop() called on already-stopped node");
685 return;
686 }
687 tracing::info!("node: stopping");
688
689 self.tasks.cancel.cancel();
693
694 if let Some(h) = self.file_transfer_state.receiver_handle.lock().await.take() {
697 h.abort();
698 }
699
700 self.session.shutdown().await;
703
704 if let Err(e) = self.network.stop().await {
706 tracing::warn!(error = %e, "node: network provider stop failed");
707 }
708
709 self.tasks.tracker.close();
713 let drain =
714 tokio::time::timeout(std::time::Duration::from_secs(5), self.tasks.tracker.wait());
715 if drain.await.is_err() {
716 tracing::warn!("node: background tasks did not drain in 5s; aborting stragglers");
717 self.tasks.abort_long_lived();
718 let _ =
719 tokio::time::timeout(std::time::Duration::from_secs(1), self.tasks.tracker.wait())
720 .await;
721 }
722 tracing::info!("node: stopped");
723 }
724
725 pub fn local_info(&self) -> NodeIdentity {
729 self.network.local_identity()
730 }
731
732 pub async fn peers(&self) -> Vec<Peer> {
739 let app_id = self.network.local_identity().app_id;
740 self.session
741 .peers()
742 .await
743 .into_iter()
744 .map(|s| Self::project_peer(s, &app_id))
745 .collect()
746 }
747
748 fn project_peer(s: PeerState, app_id: &str) -> Peer {
750 let bare = s
754 .identity
755 .is_none()
756 .then(|| hostname_slug(&s.name, app_id).map(str::to_string))
757 .flatten();
758 let mut peer = Peer::from(s);
759 if let Some(bare) = bare {
760 peer.display_name = bare;
761 }
762 peer
763 }
764
765 pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
767 self.session.on_peer_change()
768 }
769
770 pub async fn peer(&self, query: &str, wait_ms: Option<u64>) -> Result<Option<Peer>, NodeError> {
779 match self.resolve_peer(query).await {
780 Ok(state) => {
781 let app_id = self.network.local_identity().app_id;
782 return Ok(Some(Self::project_peer(state, &app_id)));
783 }
784 Err(NodeError::PeerNotFound(_)) => {}
785 Err(e) => return Err(e),
786 }
787
788 let Some(ms) = wait_ms.filter(|m| *m > 0) else {
789 return Ok(None);
790 };
791
792 let mut rx = self.session.on_peer_change();
793
794 match self.resolve_peer(query).await {
799 Ok(state) => {
800 let app_id = self.network.local_identity().app_id;
801 return Ok(Some(Self::project_peer(state, &app_id)));
802 }
803 Err(NodeError::PeerNotFound(_)) => {}
804 Err(e) => return Err(e),
805 }
806
807 let deadline = tokio::time::Instant::now() + Duration::from_millis(ms);
808 loop {
809 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
810 if remaining.is_zero() {
811 return Ok(None);
812 }
813 match tokio::time::timeout(remaining, rx.recv()).await {
814 Ok(Ok(_)) => match self.resolve_peer(query).await {
815 Ok(state) => {
816 let app_id = self.network.local_identity().app_id;
817 return Ok(Some(Self::project_peer(state, &app_id)));
818 }
819 Err(NodeError::PeerNotFound(_)) => continue,
820 Err(e) => return Err(e),
821 },
822 Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,
823 Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => return Ok(None),
824 Err(_) => return Ok(None), }
826 }
827 }
828
829 pub(crate) async fn resolve_peer(&self, peer_ref: &str) -> Result<PeerState, NodeError> {
837 let peers = self.session.peers().await;
838 let app_id = self.network.local_identity().app_id;
839
840 for p in &peers {
842 if let Some(uid) = p.published_device_id() {
843 if uid == peer_ref {
844 return Ok(p.clone());
845 }
846 }
847 if p.id == peer_ref || p.name == peer_ref || p.ip.to_string() == peer_ref {
848 return Ok(p.clone());
849 }
850 if p.peer_ref() == peer_ref {
851 return Ok(p.clone());
852 }
853 }
854
855 let name_hits: Vec<&PeerState> = peers
857 .iter()
858 .filter(|p| {
859 p.identity
860 .as_ref()
861 .map(|i| !p.identity_suppressed && i.device_name.eq_ignore_ascii_case(peer_ref))
862 .unwrap_or(false)
863 })
864 .collect();
865 if name_hits.len() > 1 {
866 return Err(NodeError::AmbiguousPeer {
867 query: peer_ref.to_string(),
868 candidates: name_hits
869 .iter()
870 .map(|p| p.published_device_id().unwrap_or(p.id.as_str()).to_string())
871 .collect(),
872 });
873 }
874 if let Some(hit) = name_hits.first() {
875 return Ok((*hit).clone());
876 }
877
878 let ref_slug = identity::slug(peer_ref, 255);
880 if !ref_slug.is_empty() {
881 let slug_hits: Vec<&PeerState> = peers
882 .iter()
883 .filter(|p| hostname_slug(&p.name, &app_id) == Some(ref_slug.as_str()))
884 .collect();
885 if slug_hits.len() > 1 {
886 return Err(NodeError::AmbiguousPeer {
887 query: peer_ref.to_string(),
888 candidates: slug_hits.iter().map(|p| p.id.clone()).collect(),
889 });
890 }
891 if let Some(hit) = slug_hits.first() {
892 return Ok((*hit).clone());
893 }
894 }
895
896 if peer_ref.len() >= 4 {
898 let hits: Vec<&PeerState> = peers
899 .iter()
900 .filter(|p| {
901 p.published_device_id()
902 .map(|uid| uid.starts_with(peer_ref))
903 .unwrap_or(false)
904 })
905 .collect();
906 if hits.len() > 1 {
907 return Err(NodeError::AmbiguousPeer {
908 query: peer_ref.to_string(),
909 candidates: hits
910 .iter()
911 .filter_map(|p| p.published_device_id().map(|s| s.to_string()))
912 .collect(),
913 });
914 }
915 if let Some(hit) = hits.first() {
916 return Ok((*hit).clone());
917 }
918 }
919
920 if crate::session::parse_peer_ref(peer_ref).is_some() {
926 return Err(NodeError::PeerGone(peer_ref.to_string()));
927 }
928
929 Err(NodeError::PeerNotFound(peer_ref.to_string()))
930 }
931
932 pub async fn resolve_peer_ip(&self, peer_ref: &str) -> Result<IpAddr, NodeError> {
938 self.ensure_not_stopped()?;
939 Ok(self.resolve_peer(peer_ref).await?.ip)
940 }
941
942 pub async fn resolve_peer_id(&self, peer_id: &str) -> Result<String, NodeError> {
960 self.ensure_not_stopped()?;
961 let p = self.resolve_peer(peer_id).await?;
962 Ok(p.published_device_id()
963 .map(|s| s.to_string())
964 .unwrap_or_else(|| p.id.clone()))
965 }
966
967 pub async fn ping(&self, peer_id: &str) -> Result<PingResult, NodeError> {
974 self.ensure_not_stopped()?;
975 let peer = self.resolve_peer(peer_id).await?;
976 let addr = peer.ip.to_string();
977 self.network.ping(&addr).await.map_err(NodeError::Network)
978 }
979
980 pub async fn whois(
993 &self,
994 addr: &str,
995 ) -> Result<Option<crate::network::TailscalePeerIdentity>, NodeError> {
996 self.ensure_not_stopped()?;
997 let target = if let Ok(ip) = addr.parse::<std::net::IpAddr>() {
1002 ip.to_string()
1003 } else if let Ok(sock) = addr.parse::<std::net::SocketAddr>() {
1004 sock.ip().to_string()
1005 } else {
1006 self.resolve_peer(addr).await?.ip.to_string()
1007 };
1008 self.network
1009 .whois(&target)
1010 .await
1011 .map_err(NodeError::Network)
1012 }
1013
1014 pub async fn health(&self) -> HealthInfo {
1016 self.network.health().await
1017 }
1018
1019 fn ensure_not_stopped(&self) -> Result<(), NodeError> {
1026 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
1027 return Err(NodeError::Stopped);
1028 }
1029 Ok(())
1030 }
1031
1032 fn map_session_send_err(e: crate::session::SessionError) -> NodeError {
1035 match e {
1036 crate::session::SessionError::PeerGone(r) => NodeError::PeerGone(r),
1037 e => NodeError::Session(e),
1038 }
1039 }
1040
1041 #[deprecated(
1052 since = "0.7.0",
1053 note = "wire type depends on data contents; use send_json or send_bytes"
1054 )]
1055 pub async fn send(&self, peer_id: &str, namespace: &str, data: &[u8]) -> Result<(), NodeError> {
1056 self.ensure_not_stopped()?;
1057 let payload = std::str::from_utf8(data)
1061 .ok()
1062 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
1063 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
1064
1065 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
1066
1067 let encoded = self.codec.encode(&envelope)?;
1068 self.session
1069 .send(peer_id, &encoded)
1070 .await
1071 .map_err(Self::map_session_send_err)?;
1072 Ok(())
1073 }
1074
1075 pub async fn send_json(
1082 &self,
1083 peer_id: &str,
1084 namespace: &str,
1085 payload: &serde_json::Value,
1086 ) -> Result<(), NodeError> {
1087 self.send_typed(peer_id, namespace, "message", payload)
1088 .await
1089 }
1090
1091 pub async fn send_bytes(
1099 &self,
1100 peer_id: &str,
1101 namespace: &str,
1102 data: &[u8],
1103 ) -> Result<(), NodeError> {
1104 let payload = Self::bytes_payload(data);
1105 self.send_typed(peer_id, namespace, "bytes", &payload).await
1106 }
1107
1108 fn bytes_payload(data: &[u8]) -> serde_json::Value {
1110 use base64::Engine as _;
1111 serde_json::json!({
1112 "encoding": "base64",
1113 "data": base64::engine::general_purpose::STANDARD.encode(data),
1114 })
1115 }
1116
1117 pub async fn send_typed(
1125 &self,
1126 peer_id: &str,
1127 namespace: &str,
1128 msg_type: &str,
1129 payload: &serde_json::Value,
1130 ) -> Result<(), NodeError> {
1131 self.ensure_not_stopped()?;
1132 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
1133 let encoded = self.codec.encode(&envelope)?;
1134 self.session
1135 .send(peer_id, &encoded)
1136 .await
1137 .map_err(Self::map_session_send_err)?;
1138 Ok(())
1139 }
1140
1141 pub async fn broadcast_typed(
1144 &self,
1145 namespace: &str,
1146 msg_type: &str,
1147 payload: &serde_json::Value,
1148 ) {
1149 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
1150 tracing::debug!("node: broadcast after stop ignored");
1151 return;
1152 }
1153 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
1154 match self.codec.encode(&envelope) {
1155 Ok(encoded) => {
1156 self.session.broadcast(&encoded).await;
1157 }
1158 Err(e) => {
1159 tracing::error!("node: failed to encode broadcast envelope: {e}");
1160 }
1161 }
1162 }
1163
1164 #[deprecated(
1172 since = "0.7.0",
1173 note = "wire type depends on data contents and failures are hidden; \
1174 use broadcast_json or broadcast_bytes"
1175 )]
1176 pub async fn broadcast(&self, namespace: &str, data: &[u8]) {
1177 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
1178 tracing::debug!("node: broadcast after stop ignored");
1179 return;
1180 }
1181 let payload = std::str::from_utf8(data)
1183 .ok()
1184 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
1185 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
1186
1187 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
1188
1189 match self.codec.encode(&envelope) {
1190 Ok(encoded) => {
1191 self.session.broadcast(&encoded).await;
1192 }
1193 Err(e) => {
1194 tracing::error!("node: failed to encode broadcast envelope: {e}");
1195 }
1196 }
1197 }
1198
1199 pub async fn broadcast_json(
1206 &self,
1207 namespace: &str,
1208 payload: &serde_json::Value,
1209 ) -> Result<crate::session::BroadcastReport, NodeError> {
1210 self.broadcast_reported(namespace, "message", payload).await
1211 }
1212
1213 pub async fn broadcast_bytes(
1218 &self,
1219 namespace: &str,
1220 data: &[u8],
1221 ) -> Result<crate::session::BroadcastReport, NodeError> {
1222 let payload = Self::bytes_payload(data);
1223 self.broadcast_reported(namespace, "bytes", &payload).await
1224 }
1225
1226 async fn broadcast_reported(
1230 &self,
1231 namespace: &str,
1232 msg_type: &str,
1233 payload: &serde_json::Value,
1234 ) -> Result<crate::session::BroadcastReport, NodeError> {
1235 self.ensure_not_stopped()?;
1236 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
1237 let encoded = self.codec.encode(&envelope)?;
1238 Ok(self.session.broadcast(&encoded).await)
1239 }
1240
1241 pub fn subscribe(&self, namespace: &str) -> broadcast::Receiver<NamespacedMessage> {
1247 {
1251 let filters = self
1252 .namespace_filters
1253 .read()
1254 .unwrap_or_else(std::sync::PoisonError::into_inner);
1255 if let Some(tx) = filters.get(namespace) {
1256 return tx.subscribe();
1257 }
1258 }
1259
1260 let mut filters = self
1262 .namespace_filters
1263 .write()
1264 .unwrap_or_else(std::sync::PoisonError::into_inner);
1265 if let Some(tx) = filters.get(namespace) {
1267 return tx.subscribe();
1268 }
1269 filters.retain(|_, tx| tx.receiver_count() > 0);
1273 let (tx, rx) = broadcast::channel(256);
1274 filters.insert(namespace.to_string(), tx);
1275 rx
1276 }
1277
1278 pub async fn open_tcp(&self, peer_id: &str, port: u16) -> Result<TcpStream, NodeError> {
1290 self.ensure_not_stopped()?;
1291 let peer = self.resolve_peer(peer_id).await?;
1292 let addr = peer.ip.to_string();
1293 self.network
1294 .dial_tcp_opts(&addr, port, DialOpts { tls: Some(false) })
1295 .await
1296 .map_err(|e| NodeError::ConnectionFailed(e.to_string()))
1297 }
1298
1299 pub async fn listen_tcp(&self, port: u16) -> Result<RawListener, NodeError> {
1307 self.listen_tcp_opts(port, ListenOpts::default()).await
1308 }
1309
1310 pub async fn listen_tcp_opts(
1316 &self,
1317 port: u16,
1318 opts: ListenOpts,
1319 ) -> Result<RawListener, NodeError> {
1320 self.ensure_not_stopped()?;
1321 use crate::transport::tcp::TcpTransport;
1322
1323 ensure_port_unreserved(port, self.ws_port)?;
1324 let tcp = TcpTransport::new(self.network.clone());
1325 tcp.listen_opts(port, opts).await.map_err(|e| {
1326 if port == 443 {
1330 NodeError::ConnectionFailed(format!(
1331 "{e} (port 443 requires a sidecar built with RFC 023 — older sidecars bind \
1332 a legacy TLS listener on 443; upgrade the sidecar binary)"
1333 ))
1334 } else {
1335 NodeError::Transport(e)
1336 }
1337 })
1338 }
1339
1340 pub async fn unlisten_tcp(&self, port: u16) -> Result<(), NodeError> {
1345 self.ensure_not_stopped()?;
1346 self.network
1347 .unlisten_tcp(port)
1348 .await
1349 .map_err(NodeError::Network)
1350 }
1351
1352 pub async fn connect_quic(
1361 &self,
1362 peer_id: &str,
1363 port: u16,
1364 ) -> Result<QuicConnection, NodeError> {
1365 self.ensure_not_stopped()?;
1366 let peer = self.resolve_peer(peer_id).await?;
1367 let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
1368 crate::transport::quic::connect_raw(&self.network, &peer.ip.to_string(), port, &alpn)
1369 .await
1370 .map_err(NodeError::Transport)
1371 }
1372
1373 pub async fn listen_quic(&self, port: u16) -> Result<QuicListener, NodeError> {
1381 self.ensure_not_stopped()?;
1382 ensure_port_unreserved(port, self.ws_port)?;
1383 if port == 0 {
1384 return Err(NodeError::NotImplemented(
1385 "ephemeral (port 0) QUIC listeners are not supported over the tsnet relay yet — choose an explicit port"
1386 .to_string(),
1387 ));
1388 }
1389 let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
1390 crate::transport::quic::listen_raw(&self.network, port, &alpn)
1391 .await
1392 .map_err(NodeError::Transport)
1393 }
1394
1395 pub async fn bind_udp(&self, port: u16) -> Result<DatagramSocket, NodeError> {
1405 self.ensure_not_stopped()?;
1406 use crate::transport::udp::{UdpConfig, UdpTransport};
1407 use crate::transport::DatagramTransport;
1408
1409 let udp = UdpTransport::new(self.network.clone(), UdpConfig::default());
1410 udp.bind(port).await.map_err(NodeError::Transport)
1411 }
1412}
1413
1414impl<N: NetworkProvider + 'static> Drop for Node<N> {
1415 fn drop(&mut self) {
1416 self.tasks.cancel.cancel();
1420 }
1421}
1422
1423fn hostname_slug<'a>(hostname: &'a str, app_id: &str) -> Option<&'a str> {
1426 hostname
1427 .strip_prefix("truffle-")?
1428 .strip_prefix(app_id)?
1429 .strip_prefix('-')
1430}
1431
1432fn validate_hostname_label(s: &str) -> Result<(), String> {
1436 let ok_len = (1..=63).contains(&s.len());
1437 let ok_chars = s
1438 .chars()
1439 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
1440 let ok_edges = !s.starts_with('-') && !s.ends_with('-');
1441 if ok_len && ok_chars && ok_edges {
1442 Ok(())
1443 } else {
1444 Err(format!(
1445 "invalid hostname {s:?}: must be a single lowercase DNS label \
1446 (1-63 chars of [a-z0-9-], no leading/trailing hyphen, no dots)"
1447 ))
1448 }
1449}
1450
1451pub(crate) fn ensure_port_unreserved(port: u16, ws_port: u16) -> Result<(), NodeError> {
1456 if port == ws_port {
1457 Err(NodeError::ReservedPort(port))
1458 } else {
1459 Ok(())
1460 }
1461}
1462
1463#[derive(Clone)]
1484pub struct NodeBuilder {
1485 app_id: Option<AppId>,
1486 device_name: Option<DeviceName>,
1487 device_id: Option<DeviceId>,
1488 hostname: Option<String>,
1491 sidecar_path: Option<PathBuf>,
1492 state_dir: Option<String>,
1493 auth_key: Option<String>,
1494 ephemeral: bool,
1495 ws_port: u16,
1496 idle_timeout_secs: Option<u64>,
1497 eager_identity: bool,
1499}
1500
1501impl std::fmt::Debug for NodeBuilder {
1504 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1505 f.debug_struct("NodeBuilder")
1506 .field("app_id", &self.app_id)
1507 .field("device_name", &self.device_name)
1508 .field("device_id", &self.device_id)
1509 .field("hostname", &self.hostname)
1510 .field("sidecar_path", &self.sidecar_path)
1511 .field("state_dir", &self.state_dir)
1512 .field("auth_key", &self.auth_key.as_ref().map(|_| "[REDACTED]"))
1513 .field("ephemeral", &self.ephemeral)
1514 .field("ws_port", &self.ws_port)
1515 .field("idle_timeout_secs", &self.idle_timeout_secs)
1516 .field("eager_identity", &self.eager_identity)
1517 .finish()
1518 }
1519}
1520
1521fn atomic_write_string(path: &Path, content: &str) -> std::io::Result<()> {
1534 let parent = path.parent().ok_or_else(|| {
1535 std::io::Error::new(
1536 std::io::ErrorKind::InvalidInput,
1537 "path has no parent directory",
1538 )
1539 })?;
1540 let mut tmp = path.to_path_buf();
1541 tmp.set_extension("tmp");
1542 {
1543 use std::io::Write as _;
1544 let mut f = std::fs::File::create(&tmp)?;
1545 f.write_all(content.as_bytes())?;
1546 f.sync_all()?;
1547 }
1548 #[cfg(unix)]
1549 {
1550 std::fs::rename(&tmp, path)?;
1551 if let Ok(dir) = std::fs::File::open(parent) {
1555 let _ = dir.sync_all();
1556 }
1557 }
1558 #[cfg(windows)]
1559 {
1560 let _ = parent; replace_file_windows(&tmp, path)?;
1562 }
1563 Ok(())
1564}
1565
1566#[cfg(windows)]
1572fn replace_file_windows(tmp: &Path, dest: &Path) -> std::io::Result<()> {
1573 use std::os::windows::ffi::OsStrExt;
1574
1575 const MOVEFILE_REPLACE_EXISTING: u32 = 0x1;
1576 const MOVEFILE_WRITE_THROUGH: u32 = 0x8;
1577
1578 #[link(name = "kernel32")]
1579 extern "system" {
1580 fn MoveFileExW(from: *const u16, to: *const u16, flags: u32) -> i32;
1581 }
1582
1583 fn wide(p: &Path) -> Vec<u16> {
1584 p.as_os_str()
1585 .encode_wide()
1586 .chain(std::iter::once(0))
1587 .collect()
1588 }
1589
1590 let (from, to) = (wide(tmp), wide(dest));
1591 let ok = unsafe {
1592 MoveFileExW(
1593 from.as_ptr(),
1594 to.as_ptr(),
1595 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
1596 )
1597 };
1598 if ok == 0 {
1599 return Err(std::io::Error::last_os_error());
1600 }
1601 Ok(())
1602}
1603
1604impl Default for NodeBuilder {
1605 fn default() -> Self {
1606 Self {
1607 app_id: None,
1608 device_name: None,
1609 device_id: None,
1610 sidecar_path: None,
1611 state_dir: None,
1612 auth_key: None,
1613 hostname: None,
1614 ephemeral: false,
1615 ws_port: 9417,
1616 idle_timeout_secs: None,
1617 eager_identity: true,
1618 }
1619 }
1620}
1621
1622impl NodeBuilder {
1623 pub fn app_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1628 let raw: String = s.into();
1629 let app_id = AppId::parse(&raw)
1630 .map_err(|e| NodeError::BuildError(format!("invalid app_id: {e}")))?;
1631 self.app_id = Some(app_id);
1632 Ok(self)
1633 }
1634
1635 pub fn device_name(mut self, s: impl Into<String>) -> Self {
1640 self.device_name = Some(DeviceName::parse(s));
1641 self
1642 }
1643
1644 pub fn hostname(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1658 let raw: String = s.into();
1659 validate_hostname_label(&raw).map_err(NodeError::BuildError)?;
1660 self.hostname = Some(raw);
1661 Ok(self)
1662 }
1663
1664 pub fn device_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1670 let raw: String = s.into();
1671 let device_id = DeviceId::parse(&raw)
1672 .map_err(|e| NodeError::BuildError(format!("invalid device_id: {e}")))?;
1673 self.device_id = Some(device_id);
1674 Ok(self)
1675 }
1676
1677 pub fn sidecar_path(mut self, path: impl Into<PathBuf>) -> Self {
1679 self.sidecar_path = Some(path.into());
1680 self
1681 }
1682
1683 pub fn state_dir(mut self, dir: &str) -> Self {
1685 self.state_dir = Some(dir.to_string());
1686 self
1687 }
1688
1689 pub fn auth_key(mut self, key: &str) -> Self {
1691 self.auth_key = Some(key.to_string());
1692 self
1693 }
1694
1695 pub fn ephemeral(mut self, val: bool) -> Self {
1697 self.ephemeral = val;
1698 self
1699 }
1700
1701 pub fn eager_identity(mut self, enabled: bool) -> Self {
1705 self.eager_identity = enabled;
1706 self
1707 }
1708
1709 pub fn ws_port(mut self, port: u16) -> Self {
1710 self.ws_port = port;
1711 self
1712 }
1713
1714 pub fn idle_timeout_secs(mut self, secs: u64) -> Self {
1720 self.idle_timeout_secs = Some(secs);
1721 self
1722 }
1723
1724 fn prepare_config(&self) -> Result<TailscaleConfig, NodeError> {
1731 let binary_path = self
1733 .sidecar_path
1734 .clone()
1735 .ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
1736
1737 let app_id = self
1739 .app_id
1740 .clone()
1741 .ok_or_else(|| NodeError::BuildError("app_id is required".into()))?;
1742
1743 let device_name = match self.device_name.clone() {
1745 Some(name) => name,
1746 None => {
1747 let os_hostname = hostname::get()
1748 .map_err(|e| {
1749 NodeError::BuildError(format!(
1750 "device_name is unset and hostname::get() failed: {e}"
1751 ))
1752 })?
1753 .to_string_lossy()
1754 .into_owned();
1755 DeviceName::parse(os_hostname)
1756 }
1757 };
1758
1759 let tailscale_host = match self.hostname.clone() {
1765 Some(hostname) => hostname,
1766 None => identity::tailscale_hostname(&app_id, &device_name),
1767 };
1768
1769 let state_dir = match self.state_dir.clone() {
1775 Some(dir) => dir,
1776 None => {
1777 let base = dirs::data_dir().ok_or_else(|| {
1778 NodeError::BuildError(
1779 "no platform data directory available (dirs::data_dir() returned None); \
1780 set state_dir explicitly to a durable location"
1781 .into(),
1782 )
1783 })?;
1784 base.join("truffle")
1785 .join(app_id.as_str())
1786 .join(identity::slug(device_name.as_str(), 255))
1787 .to_string_lossy()
1788 .into_owned()
1789 }
1790 };
1791
1792 std::fs::create_dir_all(&state_dir)?;
1794
1795 let device_id_file = Path::new(&state_dir).join("device-id.txt");
1800 let device_id = match self.device_id.clone() {
1801 Some(id) => {
1802 atomic_write_string(&device_id_file, id.as_str())?;
1804 id
1805 }
1806 None => {
1807 if device_id_file.exists() {
1808 let s = std::fs::read_to_string(&device_id_file)?.trim().to_string();
1809 DeviceId::parse(&s).map_err(|e| {
1810 NodeError::BuildError(format!(
1811 "device-id.txt at {device_id_file:?} contains an invalid ULID: {e}"
1812 ))
1813 })?
1814 } else {
1815 let id = DeviceId::generate();
1816 atomic_write_string(&device_id_file, id.as_str())?;
1817 id
1818 }
1819 }
1820 };
1821
1822 Ok(TailscaleConfig {
1823 binary_path,
1824 app_id: app_id.as_str().to_string(),
1825 device_id: device_id.as_str().to_string(),
1826 device_name: device_name.as_str().to_string(),
1827 hostname: tailscale_host,
1828 state_dir,
1829 auth_key: self.auth_key.clone(),
1830 ephemeral: if self.ephemeral { Some(true) } else { None },
1831 tags: None,
1832 idle_timeout_secs: self.idle_timeout_secs,
1833 })
1834 }
1835
1836 pub async fn build(self) -> Result<Node<TailscaleProvider>, NodeError> {
1847 let ws_port = self.ws_port;
1848 let eager_identity = self.eager_identity;
1849 let config = self.prepare_config()?;
1850 let state_dir = PathBuf::from(&config.state_dir);
1851
1852 let mut provider = TailscaleProvider::new(config);
1853 provider.start().await.map_err(NodeError::Network)?;
1854
1855 let network = Arc::new(provider);
1856
1857 let ws_config = WsConfig {
1859 port: ws_port,
1860 ..Default::default()
1861 };
1862 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1863
1864 let session = Arc::new(PeerRegistry::with_options(
1866 network.clone(),
1867 ws_transport,
1868 crate::session::PeerRegistryOptions {
1869 eager_identity,
1870 ..Default::default()
1871 },
1872 ));
1873 session.start().await;
1874
1875 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1877 let node = Node::from_parts(network, session, codec)
1878 .with_state_dir(state_dir)
1879 .with_ws_port(ws_port);
1880
1881 tracing::info!("node: started successfully");
1882 Ok(node)
1883 }
1884
1885 pub async fn build_with_auth_handler(
1897 self,
1898 on_auth: impl Fn(String) + Send + 'static,
1899 ) -> Result<Node<TailscaleProvider>, NodeError> {
1900 let ws_port = self.ws_port;
1901 let eager_identity = self.eager_identity;
1902 let config = self.prepare_config()?;
1903 let state_dir = PathBuf::from(&config.state_dir);
1904
1905 let mut provider = TailscaleProvider::new(config);
1906
1907 let mut auth_rx = provider.peer_events();
1909
1910 let auth_task = tokio::spawn(async move {
1912 use crate::network::NetworkPeerEvent;
1913 loop {
1914 match auth_rx.recv().await {
1915 Ok(NetworkPeerEvent::AuthRequired { url }) => {
1916 on_auth(url);
1917 }
1918 Err(broadcast::error::RecvError::Closed) => break,
1919 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1920 _ => {} }
1922 }
1923 });
1924
1925 let start_result = provider.start().await.map_err(NodeError::Network);
1927
1928 auth_task.abort();
1930
1931 start_result?;
1932
1933 let network = Arc::new(provider);
1934
1935 let ws_config = WsConfig {
1937 port: ws_port,
1938 ..Default::default()
1939 };
1940 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1941
1942 let session = Arc::new(PeerRegistry::with_options(
1944 network.clone(),
1945 ws_transport,
1946 crate::session::PeerRegistryOptions {
1947 eager_identity,
1948 ..Default::default()
1949 },
1950 ));
1951 session.start().await;
1952
1953 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1955 let node = Node::from_parts(network, session, codec)
1956 .with_state_dir(state_dir)
1957 .with_ws_port(ws_port);
1958
1959 tracing::info!("node: started successfully (with auth handler)");
1960 Ok(node)
1961 }
1962}
1963
1964#[cfg(test)]
1969mod tests {
1970 use super::*;
1971 use crate::network::{
1972 HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
1973 NetworkTcpListener, NetworkUdpSocket, PeerAddr,
1974 };
1975 use crate::transport::WsConfig;
1976 use serde_json::json;
1977 use std::sync::atomic::{AtomicUsize, Ordering};
1978 use std::time::Duration;
1979 use tokio::sync::{broadcast, mpsc};
1980
1981 #[test]
1982 fn node_builder_debug_redacts_auth_key() {
1983 let builder = NodeBuilder::default().auth_key("dummy-auth-SECRET123");
1984 let dbg = format!("{builder:?}");
1985 assert!(!dbg.contains("SECRET123"));
1986 assert!(dbg.contains("[REDACTED]"));
1987 }
1988
1989 #[test]
1990 fn atomic_write_string_creates_and_replaces() {
1991 let dir =
1992 std::env::temp_dir().join(format!("truffle-atomic-write-test-{}", std::process::id()));
1993 std::fs::create_dir_all(&dir).unwrap();
1994 let path = dir.join("device-id.txt");
1995
1996 atomic_write_string(&path, "01AAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
1997 assert_eq!(
1998 std::fs::read_to_string(&path).unwrap(),
1999 "01AAAAAAAAAAAAAAAAAAAAAAAA"
2000 );
2001
2002 atomic_write_string(&path, "01BBBBBBBBBBBBBBBBBBBBBBBB").unwrap();
2005 assert_eq!(
2006 std::fs::read_to_string(&path).unwrap(),
2007 "01BBBBBBBBBBBBBBBBBBBBBBBB"
2008 );
2009
2010 std::fs::remove_dir_all(&dir).ok();
2011 }
2012
2013 struct MockNetworkProvider {
2016 identity: NodeIdentity,
2017 local_addr: PeerAddr,
2018 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
2019 mock_peers: Arc<RwLock<Vec<NetworkPeer>>>,
2021 stop_calls: Arc<AtomicUsize>,
2024 }
2025
2026 impl MockNetworkProvider {
2027 fn new(id: &str) -> Self {
2028 Self::new_with_device_id(id, id)
2031 }
2032
2033 fn new_with_device_id(id: &str, device_id: &str) -> Self {
2038 let (peer_event_tx, _) = broadcast::channel(64);
2039 Self {
2040 identity: NodeIdentity {
2041 app_id: "test".to_string(),
2042 device_id: device_id.to_string(),
2043 device_name: format!("Test Node {id}"),
2044 tailscale_hostname: format!("truffle-test-{id}"),
2045 tailscale_id: id.to_string(),
2046 dns_name: None,
2047 ip: Some("127.0.0.1".parse().unwrap()),
2048 },
2049 local_addr: PeerAddr {
2050 ip: Some("127.0.0.1".parse().unwrap()),
2051 hostname: format!("truffle-test-{id}"),
2052 dns_name: None,
2053 },
2054 peer_event_tx,
2055 mock_peers: Arc::new(RwLock::new(Vec::new())),
2056 stop_calls: Arc::new(AtomicUsize::new(0)),
2057 }
2058 }
2059
2060 fn event_sender(&self) -> broadcast::Sender<NetworkPeerEvent> {
2061 self.peer_event_tx.clone()
2062 }
2063
2064 fn stop_call_count(&self) -> usize {
2066 self.stop_calls.load(Ordering::SeqCst)
2067 }
2068 }
2069
2070 impl NetworkProvider for MockNetworkProvider {
2071 async fn start(&mut self) -> Result<(), NetworkError> {
2072 Ok(())
2073 }
2074
2075 async fn stop(&self) -> Result<(), NetworkError> {
2076 self.stop_calls.fetch_add(1, Ordering::SeqCst);
2077 Ok(())
2078 }
2079
2080 fn local_identity(&self) -> NodeIdentity {
2081 self.identity.clone()
2082 }
2083
2084 fn local_addr(&self) -> PeerAddr {
2085 self.local_addr.clone()
2086 }
2087
2088 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
2089 self.peer_event_tx.subscribe()
2090 }
2091
2092 async fn peers(&self) -> Vec<NetworkPeer> {
2093 self.mock_peers.read().await.clone()
2094 }
2095
2096 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
2097 let target = format!("{addr}:{port}");
2098 TcpStream::connect(&target)
2099 .await
2100 .map_err(|e| NetworkError::DialFailed(format!("mock dial {target}: {e}")))
2101 }
2102
2103 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
2104 let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
2105 .await
2106 .map_err(|e| NetworkError::ListenFailed(format!("mock listen :{port}: {e}")))?;
2107
2108 let actual_port = listener.local_addr().unwrap().port();
2109 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
2110
2111 tokio::spawn(async move {
2112 loop {
2113 match listener.accept().await {
2114 Ok((stream, addr)) => {
2115 let conn = IncomingConnection {
2116 stream,
2117 remote_addr: addr.to_string(),
2118 remote_identity: String::new(),
2119 port: actual_port,
2120 };
2121 if tx.send(conn).await.is_err() {
2122 break;
2123 }
2124 }
2125 Err(e) => {
2126 tracing::debug!("mock listener error: {e}");
2127 break;
2128 }
2129 }
2130 }
2131 });
2132
2133 Ok(NetworkTcpListener {
2134 port: actual_port,
2135 incoming: rx,
2136 })
2137 }
2138
2139 async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
2140 Ok(())
2141 }
2142
2143 async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
2144 Err(NetworkError::Internal("mock: UDP not supported".into()))
2145 }
2146
2147 async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
2148 Ok(PingResult {
2149 latency: Duration::from_millis(1),
2150 connection: "direct".to_string(),
2151 peer_addr: None,
2152 })
2153 }
2154
2155 async fn health(&self) -> HealthInfo {
2156 HealthInfo {
2157 state: "running".to_string(),
2158 healthy: true,
2159 ..Default::default()
2160 }
2161 }
2162 }
2163
2164 fn make_loopback_peer(id: &str) -> NetworkPeer {
2167 NetworkPeer {
2168 id: id.to_string(),
2169 hostname: format!("truffle-test-{id}"),
2170 ip: "127.0.0.1".parse().unwrap(),
2171 online: true,
2172 cur_addr: Some("127.0.0.1:41641".to_string()),
2173 relay: None,
2174 os: Some("linux".to_string()),
2175 last_seen: Some("2026-03-25T12:00:00Z".to_string()),
2176 key_expiry: None,
2177 dns_name: None,
2178 }
2179 }
2180
2181 fn ws_config(port: u16) -> WsConfig {
2182 WsConfig {
2183 port,
2184 ping_interval: Duration::from_secs(300),
2185 pong_timeout: Duration::from_secs(300),
2186 ..Default::default()
2187 }
2188 }
2189
2190 async fn random_port() -> u16 {
2191 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2192 l.local_addr().unwrap().port()
2193 }
2194
2195 async fn make_test_node(
2197 id: &str,
2198 ws_port: u16,
2199 ) -> (
2200 Node<MockNetworkProvider>,
2201 broadcast::Sender<NetworkPeerEvent>,
2202 Arc<MockNetworkProvider>,
2203 ) {
2204 make_test_node_with_device_id(id, id, ws_port).await
2205 }
2206
2207 async fn make_test_node_with_device_id(
2210 id: &str,
2211 device_id: &str,
2212 ws_port: u16,
2213 ) -> (
2214 Node<MockNetworkProvider>,
2215 broadcast::Sender<NetworkPeerEvent>,
2216 Arc<MockNetworkProvider>,
2217 ) {
2218 let provider = MockNetworkProvider::new_with_device_id(id, device_id);
2219 let event_tx = provider.event_sender();
2220 let network = Arc::new(provider);
2221 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config(ws_port)));
2222 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
2223 session.start().await;
2224
2225 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
2226 let node = Node::from_parts(network.clone(), session, codec);
2227
2228 (node, event_tx, network)
2229 }
2230
2231 #[tokio::test]
2238 async fn whois_unsupported_provider_and_unknown_peer() {
2239 let ws_port = random_port().await;
2240 let (node, _event_tx, _network) = make_test_node("whois-node", ws_port).await;
2241
2242 match node.whois("100.64.0.9").await {
2243 Err(NodeError::Network(NetworkError::Unsupported(_))) => {}
2244 other => panic!("expected Unsupported for raw IP on mock provider, got {other:?}"),
2245 }
2246 match node.whois("100.64.0.9:52133").await {
2249 Err(NodeError::Network(NetworkError::Unsupported(_))) => {}
2250 other => panic!("expected Unsupported for ip:port on mock provider, got {other:?}"),
2251 }
2252 match node.whois("no-such-peer").await {
2253 Err(NodeError::PeerNotFound(_)) => {}
2254 other => panic!("expected PeerNotFound for unknown identifier, got {other:?}"),
2255 }
2256
2257 node.stop().await;
2258 }
2259
2260 #[tokio::test]
2266 async fn offer_flood_respects_caps_and_stop_drains() {
2267 let ws_port = random_port().await;
2268 let (node, _event_tx, _network) = make_test_node("node-flood", ws_port).await;
2269 let node = Arc::new(node);
2270 let mut offers = node.file_transfer().offer_channel(node.clone()).await;
2271
2272 for i in 0..500u32 {
2273 let payload = json!({
2274 "type": "offer",
2275 "file_name": format!("f{i}.bin"),
2276 "size": 1,
2277 "sha256": "0".repeat(64),
2278 "save_path": "",
2279 "token": format!("tok-{i}"),
2280 "tcp_port": 0,
2281 });
2282 let envelope = Envelope::new("ft", "offer", payload).with_timestamp();
2283 node.session
2284 .test_inject_incoming("peer-flood", JsonCodec.encode(&envelope).unwrap());
2285 if i % 64 == 0 {
2286 tokio::task::yield_now().await;
2287 }
2288 }
2289 tokio::time::sleep(Duration::from_millis(300)).await;
2290
2291 let pending = node
2292 .file_transfer_state
2293 .pending_offers_per_peer
2294 .lock()
2295 .unwrap()
2296 .get("peer-flood")
2297 .copied()
2298 .unwrap_or(0);
2299 assert!(
2300 pending <= crate::file_transfer::MAX_PENDING_OFFERS_PER_PEER,
2301 "per-peer pending cap violated: {pending}"
2302 );
2303
2304 let mut buffered = 0;
2306 while offers.try_recv().is_ok() {
2307 buffered += 1;
2308 }
2309 assert!(buffered <= 36, "app offer queue grew to {buffered}");
2310
2311 let started = std::time::Instant::now();
2314 node.stop().await;
2315 assert!(
2316 started.elapsed() < Duration::from_secs(5),
2317 "stop() took {:?} with parked offers",
2318 started.elapsed()
2319 );
2320 assert!(node.tasks.tracker.is_empty());
2321 }
2322
2323 #[tokio::test]
2325 async fn dynamic_namespace_churn_stays_bounded() {
2326 let ws_port = random_port().await;
2327 let (node, _event_tx, _network) = make_test_node("node-ns-churn", ws_port).await;
2328 for i in 0..1000 {
2329 let rx = node.subscribe(&format!("dyn-{i}"));
2330 drop(rx);
2331 }
2332 let len = node
2333 .namespace_filters
2334 .read()
2335 .unwrap_or_else(std::sync::PoisonError::into_inner)
2336 .len();
2337 assert!(len <= 1, "namespace map grew to {len} entries");
2338 }
2339
2340 #[tokio::test]
2342 async fn lifecycle_churn_leaves_no_tasks() {
2343 for _ in 0..25 {
2344 let ws_port = random_port().await;
2345 let (node, _event_tx, _network) = make_test_node("node-churn", ws_port).await;
2346 let node = Arc::new(node);
2347 let _rx = node.subscribe("churn");
2348 let _offers = node.file_transfer().offer_channel(node.clone()).await;
2349 node.stop().await;
2350 assert!(node.tasks.tracker.is_empty(), "task leak after stop()");
2351 }
2352 }
2353
2354 #[tokio::test]
2357 async fn slow_subscriber_lags_instead_of_growing() {
2358 let ws_port = random_port().await;
2359 let (node, _event_tx, _network) = make_test_node("node-lag", ws_port).await;
2360 let mut rx = node.subscribe("chat");
2361
2362 for i in 0..2000 {
2363 let envelope = Envelope::new("chat", "message", json!({ "i": i })).with_timestamp();
2364 node.session
2365 .test_inject_incoming("peer-x", JsonCodec.encode(&envelope).unwrap());
2366 if i % 100 == 0 {
2367 tokio::task::yield_now().await;
2368 }
2369 }
2370 tokio::time::sleep(Duration::from_millis(300)).await;
2371
2372 let mut lagged = false;
2373 loop {
2374 match rx.try_recv() {
2375 Ok(_) => {}
2376 Err(broadcast::error::TryRecvError::Lagged(_)) => lagged = true,
2377 Err(_) => break,
2378 }
2379 }
2380 assert!(lagged, "slow subscriber should observe Lagged");
2381 }
2382
2383 #[tokio::test]
2384 async fn stop_drains_all_background_tasks() {
2385 let ws_port = random_port().await;
2386 let (node, _event_tx, _network) = make_test_node("node-drain", ws_port).await;
2387 let node = Arc::new(node);
2388
2389 let _offers = node.file_transfer().offer_channel(node.clone()).await;
2391 assert!(!node.tasks.tracker.is_empty(), "tasks should be running");
2392
2393 node.stop().await;
2394
2395 assert!(
2397 node.tasks.tracker.is_empty(),
2398 "background tasks still alive after stop()"
2399 );
2400 assert!(
2401 node.file_transfer_state
2402 .receiver_handle
2403 .lock()
2404 .await
2405 .is_none(),
2406 "FT receiver handle not cleared by stop()"
2407 );
2408 }
2409
2410 #[tokio::test]
2411 async fn stopped_node_fails_raw_and_resolution_apis_fast() {
2412 let ws_port = random_port().await;
2413 let (node, _event_tx, _network) = make_test_node("node-guards", ws_port).await;
2414 node.stop().await;
2415
2416 assert!(matches!(
2417 node.open_tcp("nobody", 4242).await.unwrap_err(),
2418 NodeError::Stopped
2419 ));
2420 assert!(matches!(
2421 node.listen_tcp(4242).await.unwrap_err(),
2422 NodeError::Stopped
2423 ));
2424 assert!(matches!(
2425 node.connect_quic("nobody", 4242).await.unwrap_err(),
2426 NodeError::Stopped
2427 ));
2428 assert!(matches!(
2429 node.listen_quic(4242).await.unwrap_err(),
2430 NodeError::Stopped
2431 ));
2432 assert!(matches!(
2433 node.bind_udp(4242).await.err(),
2434 Some(NodeError::Stopped)
2435 ));
2436 assert!(matches!(
2437 node.ping("nobody").await.unwrap_err(),
2438 NodeError::Stopped
2439 ));
2440 assert!(matches!(
2441 node.resolve_peer_id("nobody").await.unwrap_err(),
2442 NodeError::Stopped
2443 ));
2444 }
2445
2446 #[test]
2447 fn bytes_payload_roundtrip() {
2448 let data = vec![0u8, 159, 146, 150, 255];
2451 let payload = Node::<MockNetworkProvider>::bytes_payload(&data);
2452 assert_eq!(payload["encoding"], "base64");
2453
2454 let msg = NamespacedMessage {
2455 from: "p".into(),
2456 namespace: "ns".into(),
2457 msg_type: "bytes".into(),
2458 payload,
2459 timestamp: None,
2460 };
2461 assert_eq!(msg.payload_bytes().unwrap(), data);
2462 }
2463
2464 #[test]
2465 fn payload_bytes_rejects_other_msg_types() {
2466 let msg = NamespacedMessage {
2467 from: "p".into(),
2468 namespace: "ns".into(),
2469 msg_type: "message".into(),
2470 payload: json!({"data": "aGk="}),
2471 timestamp: None,
2472 };
2473 assert!(msg.payload_bytes().is_none());
2474 }
2475
2476 #[tokio::test]
2477 async fn broadcast_json_reports_zero_peers() {
2478 let ws_port = random_port().await;
2479 let (node, _event_tx, _network) = make_test_node("node-bj", ws_port).await;
2480
2481 let report = node.broadcast_json("ns", &json!({"a": 1})).await.unwrap();
2482 assert_eq!(report.attempted, 0);
2483 assert_eq!(report.queued, 0);
2484 assert!(report.failed.is_empty());
2485 }
2486
2487 #[tokio::test]
2488 async fn subscribe_slow_path_sweeps_dead_namespaces() {
2489 let ws_port = random_port().await;
2490 let (node, _event_tx, _network) = make_test_node("node-sweep", ws_port).await;
2491
2492 let rx = node.subscribe("dyn-a");
2493 drop(rx);
2494 let _rx_b = node.subscribe("dyn-b");
2497
2498 let filters = node.namespace_filters.read().unwrap();
2499 assert!(!filters.contains_key("dyn-a"), "dead entry not swept");
2500 assert!(filters.contains_key("dyn-b"));
2501 }
2502
2503 #[tokio::test]
2504 async fn router_prunes_namespace_after_receivers_drop() {
2505 let ws_port = random_port().await;
2506 let (node, _event_tx, _network) = make_test_node("node-prune", ws_port).await;
2507
2508 let rx = node.subscribe("dyn-c");
2509 drop(rx);
2510
2511 let envelope = Envelope::new("dyn-c", "message", json!({"x": 1})).with_timestamp();
2513 let data = JsonCodec.encode(&envelope).unwrap();
2514 node.session.test_inject_incoming("peer-x", data);
2515
2516 for _ in 0..50 {
2518 {
2519 let filters = node.namespace_filters.read().unwrap();
2520 if !filters.contains_key("dyn-c") {
2521 return;
2522 }
2523 }
2524 tokio::time::sleep(Duration::from_millis(10)).await;
2525 }
2526 panic!("dead namespace entry was not pruned by the router");
2527 }
2528
2529 #[tokio::test]
2530 async fn test_node_builder_creates_node() {
2531 let ws_port = random_port().await;
2532 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2533
2534 let identity = node.local_info();
2535 assert_eq!(identity.tailscale_id, "node-1");
2536 assert_eq!(identity.device_id, "node-1");
2537 assert!(identity.tailscale_hostname.contains("node-1"));
2538 }
2539
2540 #[tokio::test]
2541 async fn test_node_peers_from_network() {
2542 let ws_port = random_port().await;
2543 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
2544
2545 let peers = node.peers().await;
2547 assert!(peers.is_empty());
2548
2549 let peer = make_loopback_peer("peer-a");
2551 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2552 tokio::time::sleep(Duration::from_millis(50)).await;
2553
2554 let peers = node.peers().await;
2555 assert_eq!(peers.len(), 1);
2556 assert_eq!(peers[0].tailscale_id, "peer-a");
2557 assert!(
2558 peers[0].device_id.is_none(),
2559 "RFC 022: no ULID before identity"
2560 );
2561 assert!(peers[0].online);
2562 assert!(!peers[0].ws_connected);
2563 }
2564
2565 #[tokio::test]
2566 #[allow(deprecated)] async fn test_node_send_to_unknown_peer_errors() {
2568 let ws_port = random_port().await;
2569 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2570
2571 let result = node.send("nonexistent", "test", b"hello").await;
2572 assert!(result.is_err());
2573 let err_str = result.unwrap_err().to_string();
2574 assert!(
2575 err_str.contains("unknown peer") || err_str.contains("not found"),
2576 "expected unknown peer error, got: {err_str}"
2577 );
2578 }
2579
2580 #[tokio::test]
2581 async fn test_node_send_wraps_in_envelope() {
2582 let codec = JsonCodec;
2585 let data = b"hello world";
2586 let envelope = Envelope::new("test-ns", "message", serde_json::Value::from(data.to_vec()))
2587 .with_timestamp();
2588
2589 let encoded = codec.encode(&envelope).unwrap();
2590 let decoded = codec.decode(&encoded).unwrap();
2591
2592 assert_eq!(decoded.namespace, "test-ns");
2593 assert_eq!(decoded.msg_type, "message");
2594 assert!(decoded.timestamp.is_some());
2595 }
2596
2597 #[tokio::test]
2598 async fn test_node_subscribe_filters_by_namespace() {
2599 let ws_port = random_port().await;
2600 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2601
2602 let _rx_chat = node.subscribe("chat");
2604 let _rx_ft = node.subscribe("ft");
2605
2606 let _rx_chat2 = node.subscribe("chat");
2608 }
2609
2610 #[tokio::test]
2611 #[allow(deprecated)] async fn test_node_broadcast() {
2613 let ws_port = random_port().await;
2614 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2615
2616 node.broadcast("test", b"hello everyone").await;
2618 }
2619
2620 #[tokio::test]
2621 async fn test_node_open_tcp_resolves_peer() {
2622 let ws_port = random_port().await;
2623 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
2624
2625 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2627 let tcp_port = listener.local_addr().unwrap().port();
2628
2629 let peer = make_loopback_peer("peer-tcp");
2631 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2632 tokio::time::sleep(Duration::from_millis(50)).await;
2633
2634 let accept_handle = tokio::spawn(async move {
2636 let (stream, _) = listener.accept().await.unwrap();
2637 stream
2638 });
2639
2640 let stream = node.open_tcp("peer-tcp", tcp_port).await;
2642 assert!(stream.is_ok(), "open_tcp failed: {:?}", stream.err());
2643
2644 let _ = accept_handle.await;
2645 }
2646
2647 #[tokio::test]
2648 async fn test_node_open_tcp_unknown_peer_errors() {
2649 let ws_port = random_port().await;
2650 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2651
2652 let result = node.open_tcp("nonexistent", 8080).await;
2653 assert!(result.is_err());
2654 let err_str = result.unwrap_err().to_string();
2655 assert!(
2656 err_str.contains("not found"),
2657 "expected peer not found error, got: {err_str}"
2658 );
2659 }
2660
2661 #[tokio::test]
2662 async fn test_node_ping_resolves_peer() {
2663 let ws_port = random_port().await;
2664 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
2665
2666 let result = node.ping("peer-ping").await;
2668 assert!(result.is_err());
2669
2670 let peer = make_loopback_peer("peer-ping");
2672 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2673 tokio::time::sleep(Duration::from_millis(50)).await;
2674
2675 let result = node.ping("peer-ping").await;
2677 assert!(result.is_ok());
2678 assert_eq!(result.unwrap().latency, Duration::from_millis(1));
2679 }
2680
2681 #[tokio::test]
2682 async fn test_node_health() {
2683 let ws_port = random_port().await;
2684 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2685
2686 let health = node.health().await;
2687 assert!(health.healthy);
2688 assert_eq!(health.state, "running");
2689 }
2690
2691 #[tokio::test]
2692 async fn test_node_connect_quic_unknown_peer_errors() {
2693 let ws_port = random_port().await;
2694 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2695
2696 let result = node.connect_quic("peer", 4433).await;
2697 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
2698 }
2699
2700 #[tokio::test]
2701 async fn test_node_listen_tcp() {
2702 let ws_port = random_port().await;
2703 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2704
2705 let listener = node.listen_tcp(0).await;
2707 assert!(listener.is_ok(), "listen_tcp failed: {:?}", listener.err());
2708 }
2709
2710 #[tokio::test]
2711 async fn test_envelope_serialize_deserialize() {
2712 let envelope = Envelope::new("chat", "message", json!({"text": "hello"})).with_timestamp();
2713
2714 let bytes = envelope.serialize().unwrap();
2715 let decoded = Envelope::deserialize(&bytes).unwrap();
2716
2717 assert_eq!(decoded.namespace, "chat");
2718 assert_eq!(decoded.msg_type, "message");
2719 assert_eq!(decoded.payload["text"], "hello");
2720 assert!(decoded.timestamp.is_some());
2721 }
2722
2723 #[tokio::test]
2724 async fn test_envelope_codec_json() {
2725 let codec = JsonCodec;
2726 let envelope = Envelope::new("ft", "offer", json!({"file": "test.bin"}));
2727
2728 let encoded = codec.encode(&envelope).unwrap();
2729 let decoded = codec.decode(&encoded).unwrap();
2730
2731 assert_eq!(decoded.namespace, "ft");
2732 assert_eq!(decoded.payload["file"], "test.bin");
2733 }
2734
2735 #[tokio::test]
2736 async fn test_envelope_unknown_fields_ignored() {
2737 let json_bytes = br#"{
2738 "namespace": "v2",
2739 "msg_type": "new",
2740 "payload": {},
2741 "future_field": "ignored"
2742 }"#;
2743
2744 let codec = JsonCodec;
2745 let decoded = codec.decode(json_bytes).unwrap();
2746 assert_eq!(decoded.namespace, "v2");
2747 assert_eq!(decoded.msg_type, "new");
2748 }
2749
2750 #[tokio::test]
2751 #[allow(deprecated)] async fn test_node_send_and_receive_roundtrip() {
2753 let port_a = random_port().await;
2755 let port_b = random_port().await;
2756
2757 let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
2758 let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
2759
2760 let peer_b = NetworkPeer {
2762 id: "node-b".to_string(),
2763 hostname: "truffle-test-node-b".to_string(),
2764 ip: "127.0.0.1".parse().unwrap(),
2765 online: true,
2766 cur_addr: Some("127.0.0.1:41641".to_string()),
2767 relay: None,
2768 os: None,
2769 last_seen: None,
2770 key_expiry: None,
2771 dns_name: None,
2772 };
2773 let peer_a = NetworkPeer {
2774 id: "node-a".to_string(),
2775 hostname: "truffle-test-node-a".to_string(),
2776 ip: "127.0.0.1".parse().unwrap(),
2777 online: true,
2778 cur_addr: Some("127.0.0.1:41641".to_string()),
2779 relay: None,
2780 os: None,
2781 last_seen: None,
2782 key_expiry: None,
2783 dns_name: None,
2784 };
2785
2786 let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
2787 let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
2788 tokio::time::sleep(Duration::from_millis(100)).await;
2789
2790 let mut rx = node_b.subscribe("test");
2792
2793 let send_result = node_a.send("node-b", "test", b"hello from a").await;
2796
2797 if send_result.is_ok() {
2805 let msg = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await;
2807 if let Ok(Ok(msg)) = msg {
2808 assert_eq!(msg.namespace, "test");
2809 }
2810 }
2811 }
2814
2815 #[tokio::test]
2818 #[allow(deprecated)] async fn test_node_stop_shuts_down_provider_and_is_idempotent() {
2820 let ws_port = random_port().await;
2821 let (node, event_tx, network) = make_test_node("node-1", ws_port).await;
2822
2823 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-1")));
2824 tokio::time::sleep(Duration::from_millis(50)).await;
2825
2826 assert_eq!(network.stop_call_count(), 0);
2827 node.stop().await;
2828 assert_eq!(
2829 network.stop_call_count(),
2830 1,
2831 "stop() must shut down the provider"
2832 );
2833
2834 node.stop().await;
2836 assert_eq!(network.stop_call_count(), 1);
2837
2838 let err = node.send("peer-1", "test", b"hi").await.unwrap_err();
2840 assert!(matches!(err, NodeError::Stopped));
2841 }
2842
2843 #[tokio::test]
2844 #[allow(deprecated)] async fn test_node_stop_closes_ws_connections() {
2846 let port_a = random_port().await;
2855 let (node_a, event_tx_a, _net_a) =
2856 make_test_node_with_device_id("node-a", "dev-node-a", port_a).await;
2857
2858 let self_peer = NetworkPeer {
2859 id: "node-a".to_string(),
2860 hostname: "truffle-test-node-a".to_string(),
2861 ip: "127.0.0.1".parse().unwrap(),
2862 online: true,
2863 cur_addr: Some("127.0.0.1:41641".to_string()),
2864 relay: None,
2865 os: None,
2866 last_seen: None,
2867 key_expiry: None,
2868 dns_name: None,
2869 };
2870 let _ = event_tx_a.send(NetworkPeerEvent::Joined(self_peer));
2871 tokio::time::sleep(Duration::from_millis(100)).await;
2872
2873 node_a
2874 .send("node-a", "test", b"hello from a")
2875 .await
2876 .expect("loopback self-dial should establish a WS connection");
2877
2878 let peers = node_a.peers().await;
2879 let entry = peers
2880 .iter()
2881 .find(|p| p.tailscale_id == "node-a")
2882 .expect("self peer discovered");
2883 assert!(
2884 entry.ws_connected,
2885 "send() should have established a WS connection"
2886 );
2887
2888 node_a.stop().await;
2889 let peers = node_a.peers().await;
2890 let entry = peers
2891 .iter()
2892 .find(|p| p.tailscale_id == "node-a")
2893 .expect("self peer still discovered");
2894 assert!(
2895 !entry.ws_connected,
2896 "stop() must close and un-mark WS connections"
2897 );
2898 }
2899
2900 async fn inject_peer_with_identity(
2906 node: &Node<MockNetworkProvider>,
2907 event_tx: &broadcast::Sender<NetworkPeerEvent>,
2908 tailscale_id: &str,
2909 device_id: &str,
2910 device_name: &str,
2911 ) {
2912 let peer = make_loopback_peer(tailscale_id);
2913 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2914 tokio::time::sleep(Duration::from_millis(30)).await;
2915
2916 let identity = crate::session::PeerIdentity {
2917 app_id: "test".into(),
2918 device_id: device_id.into(),
2919 device_name: device_name.into(),
2920 os: "linux".into(),
2921 tailscale_id: tailscale_id.into(),
2922 };
2923 assert!(
2924 node.session
2925 .test_stamp_identity(tailscale_id, identity)
2926 .await,
2927 "peer {tailscale_id} should exist in session registry before stamping identity"
2928 );
2929 }
2930
2931 #[tokio::test]
2932 async fn test_resolve_peer_id_by_device_id() {
2933 let ws_port = random_port().await;
2934 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2935
2936 inject_peer_with_identity(
2937 &node,
2938 &event_tx,
2939 "tailscale-abc",
2940 "01HZZZZZZZZZZZZZZZZZZZZZZZ",
2941 "Alice MacBook",
2942 )
2943 .await;
2944
2945 let resolved = node
2946 .resolve_peer_id("01HZZZZZZZZZZZZZZZZZZZZZZZ")
2947 .await
2948 .unwrap();
2949 assert_eq!(resolved, "01HZZZZZZZZZZZZZZZZZZZZZZZ");
2950 }
2951
2952 #[tokio::test]
2953 async fn test_resolve_peer_id_by_device_name() {
2954 let ws_port = random_port().await;
2955 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2956
2957 inject_peer_with_identity(
2958 &node,
2959 &event_tx,
2960 "tailscale-abc",
2961 "01HXYZXYZXYZXYZXYZXYZXYZXY",
2962 "Bob's Mac",
2963 )
2964 .await;
2965
2966 let resolved = node.resolve_peer_id("Bob's Mac").await.unwrap();
2967 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
2968 }
2969
2970 #[tokio::test]
2971 async fn test_resolve_peer_id_by_device_id_prefix() {
2972 let ws_port = random_port().await;
2973 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2974
2975 inject_peer_with_identity(
2976 &node,
2977 &event_tx,
2978 "tailscale-abc",
2979 "01HXYZXYZXYZXYZXYZXYZXYZXY",
2980 "laptop",
2981 )
2982 .await;
2983
2984 let resolved = node.resolve_peer_id("01HX").await.unwrap();
2987 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
2988 }
2989
2990 #[tokio::test]
2991 async fn test_resolve_peer_id_by_tailscale_id_legacy() {
2992 let ws_port = random_port().await;
2996 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2997
2998 inject_peer_with_identity(
2999 &node,
3000 &event_tx,
3001 "tailscale-legacy",
3002 "01HLEGACY0000000000000000X",
3003 "legacy box",
3004 )
3005 .await;
3006
3007 let resolved = node.resolve_peer_id("tailscale-legacy").await.unwrap();
3008 assert_eq!(resolved, "01HLEGACY0000000000000000X");
3009 }
3010
3011 #[tokio::test]
3012 async fn test_resolve_peer_id_unknown() {
3013 let ws_port = random_port().await;
3014 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
3015 let result = node.resolve_peer_id("nope").await;
3016 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
3017 }
3018
3019 #[tokio::test]
3022 async fn test_resolve_peer_by_bare_name_before_hello() {
3023 let ws_port = random_port().await;
3027 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
3028
3029 let mut peer = make_loopback_peer("nodeid-9");
3030 peer.hostname = "truffle-test-ec2-smoke".to_string();
3031 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
3032 tokio::time::sleep(Duration::from_millis(50)).await;
3033
3034 assert_eq!(node.resolve_peer("ec2-smoke").await.unwrap().id, "nodeid-9");
3036 assert_eq!(node.resolve_peer("EC2 Smoke").await.unwrap().id, "nodeid-9");
3037 assert!(node.resolve_peer("other-box").await.is_err());
3039 }
3040
3041 #[tokio::test]
3042 async fn test_peers_device_name_strips_hostname_before_hello() {
3043 let ws_port = random_port().await;
3044 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
3045
3046 let mut peer = make_loopback_peer("nodeid-9");
3047 peer.hostname = "truffle-test-ec2-smoke".to_string();
3048 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
3049 tokio::time::sleep(Duration::from_millis(50)).await;
3050
3051 let peers = node.peers().await;
3052 assert_eq!(peers.len(), 1);
3053 assert!(peers[0].device_name.is_none());
3055 assert_eq!(peers[0].display_name, "ec2-smoke");
3056 assert_eq!(peers[0].hostname, "truffle-test-ec2-smoke");
3057 assert!(peers[0].device_id.is_none());
3058 }
3059
3060 #[tokio::test]
3061 async fn test_resolve_peer_stale_ref_is_peer_gone() {
3062 let ws_port = random_port().await;
3063 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
3064
3065 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-9")));
3066 tokio::time::sleep(Duration::from_millis(50)).await;
3067
3068 let live_ref = node.peers().await[0].peer_ref.clone();
3069 assert_eq!(node.resolve_peer(&live_ref).await.unwrap().id, "peer-9");
3070
3071 let _ = event_tx.send(NetworkPeerEvent::Left("peer-9".to_string()));
3075 tokio::time::sleep(Duration::from_millis(50)).await;
3076 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-9")));
3077 tokio::time::sleep(Duration::from_millis(50)).await;
3078
3079 assert!(matches!(
3080 node.resolve_peer(&live_ref).await,
3081 Err(NodeError::PeerGone(_))
3082 ));
3083 assert!(matches!(
3086 node.resolve_peer("peer-9:99").await,
3087 Err(NodeError::PeerGone(_))
3088 ));
3089 assert!(matches!(
3092 node.peer(&live_ref, Some(2_000)).await,
3093 Err(NodeError::PeerGone(_))
3094 ));
3095 }
3096
3097 #[tokio::test]
3098 async fn test_resolve_peer_accepts_ip() {
3099 let ws_port = random_port().await;
3100 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
3101
3102 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-a")));
3103 tokio::time::sleep(Duration::from_millis(50)).await;
3104
3105 let resolved = node.resolve_peer("127.0.0.1").await.unwrap();
3106 assert_eq!(resolved.id, "peer-a");
3107 assert_eq!(node.resolve_peer_id("127.0.0.1").await.unwrap(), "peer-a");
3110 }
3111
3112 #[tokio::test]
3113 async fn test_listen_tcp_rejects_reserved_ports() {
3114 let ws_port = random_port().await;
3115 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
3116
3117 let err = node.listen_tcp(9417).await.unwrap_err();
3118 assert!(
3119 matches!(err, NodeError::ReservedPort(9417)),
3120 "expected ReservedPort(9417), got: {err}"
3121 );
3122
3123 match node.listen_tcp(443).await {
3129 Ok(listener) => assert_eq!(listener.port, 443),
3130 Err(NodeError::ReservedPort(_)) => panic!("443 must not be reserved (RFC 023 D4)"),
3131 Err(e) => assert!(
3132 e.to_string().contains("RFC 023"),
3133 "443 bind errors should hint at the sidecar upgrade, got: {e}"
3134 ),
3135 }
3136 }
3137
3138 #[tokio::test]
3139 async fn test_listen_quic_rejects_reserved_and_ephemeral_ports() {
3140 let ws_port = random_port().await;
3141 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
3142
3143 let err = node.listen_quic(9417).await.unwrap_err();
3144 assert!(matches!(err, NodeError::ReservedPort(9417)));
3145
3146 let err = node.listen_quic(0).await.unwrap_err();
3147 assert!(matches!(err, NodeError::NotImplemented(_)));
3148 }
3149
3150 #[test]
3151 fn test_builder_hostname_override_validation() {
3152 assert!(NodeBuilder::default().hostname("dashboard").is_ok());
3153 assert!(NodeBuilder::default().hostname("my-app-1").is_ok());
3154 for bad in ["", "Dashboard", "has.dot", "-lead", "trail-"] {
3155 assert!(
3156 NodeBuilder::default().hostname(bad).is_err(),
3157 "expected hostname {bad:?} to be rejected"
3158 );
3159 }
3160 let too_long = "x".repeat(64);
3161 assert!(NodeBuilder::default().hostname(too_long).is_err());
3162 }
3163
3164 #[tokio::test]
3165 async fn test_bind_udp_falls_back_to_direct_socket() {
3166 let ws_port = random_port().await;
3169 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
3170
3171 let socket = node.bind_udp(0).await.unwrap();
3172 let addr = socket.local_addr().unwrap();
3173 assert_ne!(addr.port(), 0);
3174 }
3175
3176 #[test]
3177 fn test_raw_alpn_scoping() {
3178 assert_eq!(
3179 crate::transport::quic::raw_alpn("demo"),
3180 b"truffle-raw.demo".to_vec()
3181 );
3182 assert_eq!(
3183 crate::transport::quic::raw_alpn(""),
3184 b"truffle-raw".to_vec()
3185 );
3186 }
3187
3188 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3189 async fn test_connect_quic_roundtrip_via_raw_listener() {
3190 let ws_port = random_port().await;
3191 let (client_node, event_tx, _net) = make_test_node("cli", ws_port).await;
3192
3193 let server_network = Arc::new(MockNetworkProvider::new("srv"));
3196 let alpn = crate::transport::quic::raw_alpn("test");
3197 let listener = crate::transport::quic::listen_raw(&server_network, 0, &alpn)
3198 .await
3199 .unwrap();
3200 let port = listener.port();
3201 assert_ne!(port, 0);
3202
3203 let echo_task = tokio::spawn(async move {
3204 let conn = listener.accept().await.expect("no incoming connection");
3205 let mut stream = conn
3206 .accept_stream()
3207 .await
3208 .unwrap()
3209 .expect("connection closed before stream");
3210 let mut received = Vec::new();
3211 while let Some(chunk) = stream.read(1024).await.unwrap() {
3212 received.extend_from_slice(&chunk);
3213 }
3214 stream.write(&received).await.unwrap();
3215 stream.finish();
3216 tokio::time::sleep(Duration::from_millis(500)).await;
3219 conn.close();
3220 });
3221
3222 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("srv-peer")));
3224 tokio::time::sleep(Duration::from_millis(50)).await;
3225
3226 let conn = client_node.connect_quic("srv-peer", port).await.unwrap();
3227 let mut stream = conn.open_stream().await.unwrap();
3228 stream.write(b"hello quic").await.unwrap();
3229 stream.finish();
3230
3231 let mut echoed = Vec::new();
3232 while let Some(chunk) = stream.read(1024).await.unwrap() {
3233 echoed.extend_from_slice(&chunk);
3234 }
3235 assert_eq!(echoed, b"hello quic");
3236
3237 echo_task.await.unwrap();
3238 conn.close();
3239 }
3240
3241 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3242 async fn test_quic_raw_alpn_mismatch_rejected() {
3243 let server_network = Arc::new(MockNetworkProvider::new("srv"));
3246 let listener = crate::transport::quic::listen_raw(
3247 &server_network,
3248 0,
3249 &crate::transport::quic::raw_alpn("app-a"),
3250 )
3251 .await
3252 .unwrap();
3253 let port = listener.port();
3254
3255 let client_network = Arc::new(MockNetworkProvider::new("cli"));
3256 let result = crate::transport::quic::connect_raw(
3257 &client_network,
3258 "127.0.0.1",
3259 port,
3260 &crate::transport::quic::raw_alpn("app-b"),
3261 )
3262 .await;
3263
3264 assert!(
3265 result.is_err(),
3266 "cross-app QUIC connect must fail (ALPN mismatch)"
3267 );
3268 }
3269
3270 #[tokio::test]
3273 async fn test_rfc022_node_peer_resolves_and_wait_miss_returns_none() {
3274 let ws_port = random_port().await;
3275 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
3276
3277 assert!(node.peer("nope", None).await.unwrap().is_none());
3279
3280 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-a")));
3281 tokio::time::sleep(Duration::from_millis(50)).await;
3282
3283 let p = node.peer("peer-a", None).await.unwrap().expect("found");
3284 assert_eq!(p.tailscale_id, "peer-a");
3285 assert!(p.device_id.is_none());
3286
3287 let start = std::time::Instant::now();
3289 let miss = node.peer("still-missing", Some(80)).await.unwrap();
3290 assert!(miss.is_none());
3291 assert!(start.elapsed() >= Duration::from_millis(70));
3292 }
3293
3294 #[test]
3295 fn test_rfc022_peer_projection_never_uses_tailscale_as_device_id() {
3296 use crate::session::PeerState;
3297 use std::net::Ipv4Addr;
3298
3299 let pre = PeerState {
3301 id: "ts-abc".into(),
3302 generation: 1,
3303 name: "truffle-app-laptop".into(),
3304 ip: Ipv4Addr::new(100, 64, 0, 1).into(),
3305 online: true,
3306 ws_connected: false,
3307 connection_type: "direct".into(),
3308 os: None,
3309 last_seen: None,
3310 identity: None,
3311 identity_suppressed: false,
3312 };
3313 let p = Peer::from(pre);
3314 assert!(p.device_id.is_none());
3315 assert_eq!(p.tailscale_id, "ts-abc");
3316 assert_eq!(p.peer_ref, "ts-abc:1");
3317 assert_eq!(p.display_name, "laptop");
3318 assert!(p
3319 .device_id
3320 .as_ref()
3321 .map(|d| d.as_str() != p.tailscale_id)
3322 .unwrap_or(true));
3323
3324 let post = PeerState {
3326 id: "ts-abc".into(),
3327 generation: 1,
3328 name: "truffle-app-laptop".into(),
3329 ip: Ipv4Addr::new(100, 64, 0, 1).into(),
3330 online: true,
3331 ws_connected: true,
3332 connection_type: "direct".into(),
3333 os: Some("darwin".into()),
3334 last_seen: None,
3335 identity: Some(crate::session::PeerIdentity {
3336 app_id: "app".into(),
3337 device_id: "01J4K9M2Z8AB3RNYQPW6H5TC0X".into(),
3338 device_name: "Alice's Mac".into(),
3339 os: "darwin".into(),
3340 tailscale_id: "ts-abc".into(),
3341 }),
3342 identity_suppressed: false,
3343 };
3344 let p = Peer::from(post);
3345 assert_eq!(p.device_id.as_deref(), Some("01J4K9M2Z8AB3RNYQPW6H5TC0X"));
3346 assert_ne!(p.device_id.as_deref().unwrap(), p.tailscale_id);
3347 assert_eq!(p.display_name, "Alice's Mac");
3348
3349 let suppressed = PeerState {
3351 id: "ts-xyz".into(),
3352 generation: 1,
3353 name: "truffle-app-other".into(),
3354 ip: Ipv4Addr::new(100, 64, 0, 2).into(),
3355 online: true,
3356 ws_connected: true,
3357 connection_type: "direct".into(),
3358 os: None,
3359 last_seen: None,
3360 identity: Some(crate::session::PeerIdentity {
3361 app_id: "app".into(),
3362 device_id: "01J4K9M2Z8AB3RNYQPW6H5TC0X".into(),
3363 device_name: "Clone".into(),
3364 os: "linux".into(),
3365 tailscale_id: "ts-xyz".into(),
3366 }),
3367 identity_suppressed: true,
3368 };
3369 let p = Peer::from(suppressed);
3370 assert!(
3371 p.device_id.is_none(),
3372 "suppressed claim must project null device_id"
3373 );
3374 }
3375}