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, 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 (443 = sidecar TLS, or 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 ws_port: u16,
333 pub(crate) proxy_state: crate::proxy::ProxyState,
335 stopped: std::sync::atomic::AtomicBool,
339 pub(crate) tasks: NodeTasks,
341}
342
343pub(crate) struct NodeTasks {
352 pub(crate) cancel: tokio_util::sync::CancellationToken,
354 pub(crate) tracker: tokio_util::task::TaskTracker,
356 long_lived: std::sync::Mutex<Vec<tokio::task::JoinHandle<()>>>,
361}
362
363impl NodeTasks {
364 fn new() -> Self {
365 Self {
366 cancel: tokio_util::sync::CancellationToken::new(),
367 tracker: tokio_util::task::TaskTracker::new(),
368 long_lived: std::sync::Mutex::new(Vec::new()),
369 }
370 }
371
372 fn spawn_long_lived<F>(&self, fut: F)
375 where
376 F: std::future::Future<Output = ()> + Send + 'static,
377 {
378 let handle = self.tracker.spawn(fut);
379 self.long_lived.lock().unwrap().push(handle);
380 }
381
382 fn abort_long_lived(&self) {
384 for h in self.long_lived.lock().unwrap().drain(..) {
385 h.abort();
386 }
387 }
388}
389
390impl<N: NetworkProvider + 'static> Node<N> {
391 pub(crate) fn from_parts(
397 network: Arc<N>,
398 session: Arc<PeerRegistry<N>>,
399 codec: Arc<dyn EnvelopeCodec>,
400 ) -> Self {
401 let (incoming_tx, _) = broadcast::channel(1024);
402 let namespace_filters: Arc<
403 StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>,
404 > = Arc::new(StdRwLock::new(HashMap::new()));
405
406 let node = Self {
407 network,
408 session: session.clone(),
409 codec: codec.clone(),
410 incoming_tx: incoming_tx.clone(),
411 namespace_filters: namespace_filters.clone(),
412 file_transfer_state: FileTransferState::new(),
413 state_dir: PathBuf::new(),
414 ws_port: 9417,
415 proxy_state: crate::proxy::ProxyState::new(),
416 stopped: std::sync::atomic::AtomicBool::new(false),
417 tasks: NodeTasks::new(),
418 };
419
420 node.spawn_envelope_router(session, codec, incoming_tx, namespace_filters);
422
423 node
424 }
425
426 fn spawn_envelope_router(
430 &self,
431 session: Arc<PeerRegistry<N>>,
432 codec: Arc<dyn EnvelopeCodec>,
433 incoming_tx: broadcast::Sender<NamespacedMessage>,
434 namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
435 ) {
436 let mut rx = session.subscribe();
437 let cancel = self.tasks.cancel.clone();
438
439 self.tasks.spawn_long_lived(async move {
440 loop {
441 let recv = tokio::select! {
442 _ = cancel.cancelled() => {
443 tracing::debug!("envelope router: cancelled by stop()");
444 break;
445 }
446 result = rx.recv() => result,
447 };
448 match recv {
449 Ok(msg) => {
450 if let Ok(envelope) = codec.decode(&msg.data) {
451 let namespaced = NamespacedMessage {
452 from: msg.from,
453 namespace: envelope.namespace.clone(),
454 msg_type: envelope.msg_type,
455 payload: envelope.payload,
456 timestamp: envelope.timestamp,
457 };
458
459 tracing::debug!(
460 from = %namespaced.from,
461 namespace = %namespaced.namespace,
462 msg_type = %namespaced.msg_type,
463 "envelope router: dispatching message"
464 );
465
466 let _ = incoming_tx.send(namespaced.clone());
468
469 let namespace = namespaced.namespace.clone();
477 let dead_subscriber = {
478 let filters = namespace_filters
479 .read()
480 .unwrap_or_else(std::sync::PoisonError::into_inner);
481 match filters.get(&namespace) {
482 Some(tx) => {
483 let send_result = tx.send(namespaced);
484 tracing::debug!(
485 namespace = %namespace,
486 subscriber_count = tx.receiver_count(),
487 sent = send_result.is_ok(),
488 "envelope router: sent to namespace subscriber"
489 );
490 send_result.is_err()
491 }
492 None => {
493 tracing::debug!(
494 namespace = %namespace,
495 "envelope router: no subscriber for namespace"
496 );
497 false
498 }
499 }
500 };
501
502 if dead_subscriber {
509 let mut filters = namespace_filters
510 .write()
511 .unwrap_or_else(std::sync::PoisonError::into_inner);
512 if filters
513 .get(&namespace)
514 .is_some_and(|tx| tx.receiver_count() == 0)
515 {
516 filters.remove(&namespace);
517 tracing::debug!(
518 namespace = %namespace,
519 "envelope router: pruned dead namespace subscriber"
520 );
521 }
522 }
523 } else {
524 tracing::warn!(
525 from = %msg.from,
526 data_len = msg.data.len(),
527 "node: failed to decode envelope from incoming message"
528 );
529 }
530 }
531 Err(broadcast::error::RecvError::Lagged(n)) => {
532 tracing::warn!(
533 missed = n,
534 "node: envelope router lagged, missed {n} messages"
535 );
536 continue;
537 }
538 Err(broadcast::error::RecvError::Closed) => {
539 tracing::debug!("node: session incoming channel closed, router exiting");
540 break;
541 }
542 }
543 }
544 });
545 }
546
547 pub fn builder() -> NodeBuilder {
551 NodeBuilder::default()
552 }
553
554 pub fn file_transfer(&self) -> file_transfer::FileTransfer<'_, N> {
561 file_transfer::FileTransfer::new(self)
562 }
563
564 pub fn proxy(&self) -> crate::proxy::Proxy<'_, N> {
571 crate::proxy::Proxy::new(self)
572 }
573
574 pub fn synced_store<T>(
583 self: &Arc<Self>,
584 store_id: &str,
585 ) -> Arc<crate::synced_store::SyncedStore<T>>
586 where
587 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
588 {
589 crate::synced_store::SyncedStore::new(self.clone(), store_id)
590 }
591
592 pub fn synced_store_with_backend<T>(
597 self: &Arc<Self>,
598 store_id: &str,
599 backend: std::sync::Arc<dyn crate::synced_store::StoreBackend>,
600 ) -> Arc<crate::synced_store::SyncedStore<T>>
601 where
602 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
603 {
604 crate::synced_store::SyncedStore::new_with_backend(self.clone(), store_id, backend)
605 }
606
607 pub(crate) fn with_state_dir(mut self, dir: PathBuf) -> Self {
611 self.state_dir = dir;
612 self
613 }
614
615 pub(crate) fn with_ws_port(mut self, port: u16) -> Self {
618 self.ws_port = port;
619 self
620 }
621
622 pub fn state_dir(&self) -> &Path {
626 &self.state_dir
627 }
628
629 pub async fn stop(&self) {
648 if self.stopped.swap(true, std::sync::atomic::Ordering::SeqCst) {
649 tracing::debug!("node: stop() called on already-stopped node");
650 return;
651 }
652 tracing::info!("node: stopping");
653
654 self.tasks.cancel.cancel();
658
659 if let Some(h) = self.file_transfer_state.receiver_handle.lock().await.take() {
662 h.abort();
663 }
664
665 self.session.shutdown().await;
668
669 if let Err(e) = self.network.stop().await {
671 tracing::warn!(error = %e, "node: network provider stop failed");
672 }
673
674 self.tasks.tracker.close();
678 let drain =
679 tokio::time::timeout(std::time::Duration::from_secs(5), self.tasks.tracker.wait());
680 if drain.await.is_err() {
681 tracing::warn!("node: background tasks did not drain in 5s; aborting stragglers");
682 self.tasks.abort_long_lived();
683 let _ =
684 tokio::time::timeout(std::time::Duration::from_secs(1), self.tasks.tracker.wait())
685 .await;
686 }
687 tracing::info!("node: stopped");
688 }
689
690 pub fn local_info(&self) -> NodeIdentity {
694 self.network.local_identity()
695 }
696
697 pub async fn peers(&self) -> Vec<Peer> {
704 let app_id = self.network.local_identity().app_id;
705 self.session
706 .peers()
707 .await
708 .into_iter()
709 .map(|s| Self::project_peer(s, &app_id))
710 .collect()
711 }
712
713 fn project_peer(s: PeerState, app_id: &str) -> Peer {
715 let bare = s
719 .identity
720 .is_none()
721 .then(|| hostname_slug(&s.name, app_id).map(str::to_string))
722 .flatten();
723 let mut peer = Peer::from(s);
724 if let Some(bare) = bare {
725 peer.display_name = bare;
726 }
727 peer
728 }
729
730 pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
732 self.session.on_peer_change()
733 }
734
735 pub async fn peer(&self, query: &str, wait_ms: Option<u64>) -> Result<Option<Peer>, NodeError> {
744 match self.resolve_peer(query).await {
745 Ok(state) => {
746 let app_id = self.network.local_identity().app_id;
747 return Ok(Some(Self::project_peer(state, &app_id)));
748 }
749 Err(NodeError::PeerNotFound(_)) => {}
750 Err(e) => return Err(e),
751 }
752
753 let Some(ms) = wait_ms.filter(|m| *m > 0) else {
754 return Ok(None);
755 };
756
757 let mut rx = self.session.on_peer_change();
758
759 match self.resolve_peer(query).await {
764 Ok(state) => {
765 let app_id = self.network.local_identity().app_id;
766 return Ok(Some(Self::project_peer(state, &app_id)));
767 }
768 Err(NodeError::PeerNotFound(_)) => {}
769 Err(e) => return Err(e),
770 }
771
772 let deadline = tokio::time::Instant::now() + Duration::from_millis(ms);
773 loop {
774 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
775 if remaining.is_zero() {
776 return Ok(None);
777 }
778 match tokio::time::timeout(remaining, rx.recv()).await {
779 Ok(Ok(_)) => 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(_)) => continue,
785 Err(e) => return Err(e),
786 },
787 Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,
788 Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => return Ok(None),
789 Err(_) => return Ok(None), }
791 }
792 }
793
794 pub(crate) async fn resolve_peer(&self, peer_ref: &str) -> Result<PeerState, NodeError> {
802 let peers = self.session.peers().await;
803 let app_id = self.network.local_identity().app_id;
804
805 for p in &peers {
807 if let Some(uid) = p.published_device_id() {
808 if uid == peer_ref {
809 return Ok(p.clone());
810 }
811 }
812 if p.id == peer_ref || p.name == peer_ref || p.ip.to_string() == peer_ref {
813 return Ok(p.clone());
814 }
815 if p.peer_ref() == peer_ref {
816 return Ok(p.clone());
817 }
818 }
819
820 let name_hits: Vec<&PeerState> = peers
822 .iter()
823 .filter(|p| {
824 p.identity
825 .as_ref()
826 .map(|i| !p.identity_suppressed && i.device_name.eq_ignore_ascii_case(peer_ref))
827 .unwrap_or(false)
828 })
829 .collect();
830 if name_hits.len() > 1 {
831 return Err(NodeError::AmbiguousPeer {
832 query: peer_ref.to_string(),
833 candidates: name_hits
834 .iter()
835 .map(|p| p.published_device_id().unwrap_or(p.id.as_str()).to_string())
836 .collect(),
837 });
838 }
839 if let Some(hit) = name_hits.first() {
840 return Ok((*hit).clone());
841 }
842
843 let ref_slug = identity::slug(peer_ref, 255);
845 if !ref_slug.is_empty() {
846 let slug_hits: Vec<&PeerState> = peers
847 .iter()
848 .filter(|p| hostname_slug(&p.name, &app_id) == Some(ref_slug.as_str()))
849 .collect();
850 if slug_hits.len() > 1 {
851 return Err(NodeError::AmbiguousPeer {
852 query: peer_ref.to_string(),
853 candidates: slug_hits.iter().map(|p| p.id.clone()).collect(),
854 });
855 }
856 if let Some(hit) = slug_hits.first() {
857 return Ok((*hit).clone());
858 }
859 }
860
861 if peer_ref.len() >= 4 {
863 let hits: Vec<&PeerState> = peers
864 .iter()
865 .filter(|p| {
866 p.published_device_id()
867 .map(|uid| uid.starts_with(peer_ref))
868 .unwrap_or(false)
869 })
870 .collect();
871 if hits.len() > 1 {
872 return Err(NodeError::AmbiguousPeer {
873 query: peer_ref.to_string(),
874 candidates: hits
875 .iter()
876 .filter_map(|p| p.published_device_id().map(|s| s.to_string()))
877 .collect(),
878 });
879 }
880 if let Some(hit) = hits.first() {
881 return Ok((*hit).clone());
882 }
883 }
884
885 if crate::session::parse_peer_ref(peer_ref).is_some() {
891 return Err(NodeError::PeerGone(peer_ref.to_string()));
892 }
893
894 Err(NodeError::PeerNotFound(peer_ref.to_string()))
895 }
896
897 pub async fn resolve_peer_ip(&self, peer_ref: &str) -> Result<IpAddr, NodeError> {
903 self.ensure_not_stopped()?;
904 Ok(self.resolve_peer(peer_ref).await?.ip)
905 }
906
907 pub async fn resolve_peer_id(&self, peer_id: &str) -> Result<String, NodeError> {
925 self.ensure_not_stopped()?;
926 let p = self.resolve_peer(peer_id).await?;
927 Ok(p.published_device_id()
928 .map(|s| s.to_string())
929 .unwrap_or_else(|| p.id.clone()))
930 }
931
932 pub async fn ping(&self, peer_id: &str) -> Result<PingResult, NodeError> {
939 self.ensure_not_stopped()?;
940 let peer = self.resolve_peer(peer_id).await?;
941 let addr = peer.ip.to_string();
942 self.network.ping(&addr).await.map_err(NodeError::Network)
943 }
944
945 pub async fn health(&self) -> HealthInfo {
947 self.network.health().await
948 }
949
950 fn ensure_not_stopped(&self) -> Result<(), NodeError> {
957 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
958 return Err(NodeError::Stopped);
959 }
960 Ok(())
961 }
962
963 fn map_session_send_err(e: crate::session::SessionError) -> NodeError {
966 match e {
967 crate::session::SessionError::PeerGone(r) => NodeError::PeerGone(r),
968 e => NodeError::Session(e),
969 }
970 }
971
972 #[deprecated(
983 since = "0.7.0",
984 note = "wire type depends on data contents; use send_json or send_bytes"
985 )]
986 pub async fn send(&self, peer_id: &str, namespace: &str, data: &[u8]) -> Result<(), NodeError> {
987 self.ensure_not_stopped()?;
988 let payload = std::str::from_utf8(data)
992 .ok()
993 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
994 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
995
996 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
997
998 let encoded = self.codec.encode(&envelope)?;
999 self.session
1000 .send(peer_id, &encoded)
1001 .await
1002 .map_err(Self::map_session_send_err)?;
1003 Ok(())
1004 }
1005
1006 pub async fn send_json(
1013 &self,
1014 peer_id: &str,
1015 namespace: &str,
1016 payload: &serde_json::Value,
1017 ) -> Result<(), NodeError> {
1018 self.send_typed(peer_id, namespace, "message", payload)
1019 .await
1020 }
1021
1022 pub async fn send_bytes(
1030 &self,
1031 peer_id: &str,
1032 namespace: &str,
1033 data: &[u8],
1034 ) -> Result<(), NodeError> {
1035 let payload = Self::bytes_payload(data);
1036 self.send_typed(peer_id, namespace, "bytes", &payload).await
1037 }
1038
1039 fn bytes_payload(data: &[u8]) -> serde_json::Value {
1041 use base64::Engine as _;
1042 serde_json::json!({
1043 "encoding": "base64",
1044 "data": base64::engine::general_purpose::STANDARD.encode(data),
1045 })
1046 }
1047
1048 pub async fn send_typed(
1056 &self,
1057 peer_id: &str,
1058 namespace: &str,
1059 msg_type: &str,
1060 payload: &serde_json::Value,
1061 ) -> Result<(), NodeError> {
1062 self.ensure_not_stopped()?;
1063 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
1064 let encoded = self.codec.encode(&envelope)?;
1065 self.session
1066 .send(peer_id, &encoded)
1067 .await
1068 .map_err(Self::map_session_send_err)?;
1069 Ok(())
1070 }
1071
1072 pub async fn broadcast_typed(
1075 &self,
1076 namespace: &str,
1077 msg_type: &str,
1078 payload: &serde_json::Value,
1079 ) {
1080 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
1081 tracing::debug!("node: broadcast after stop ignored");
1082 return;
1083 }
1084 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
1085 match self.codec.encode(&envelope) {
1086 Ok(encoded) => {
1087 self.session.broadcast(&encoded).await;
1088 }
1089 Err(e) => {
1090 tracing::error!("node: failed to encode broadcast envelope: {e}");
1091 }
1092 }
1093 }
1094
1095 #[deprecated(
1103 since = "0.7.0",
1104 note = "wire type depends on data contents and failures are hidden; \
1105 use broadcast_json or broadcast_bytes"
1106 )]
1107 pub async fn broadcast(&self, namespace: &str, data: &[u8]) {
1108 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
1109 tracing::debug!("node: broadcast after stop ignored");
1110 return;
1111 }
1112 let payload = std::str::from_utf8(data)
1114 .ok()
1115 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
1116 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
1117
1118 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
1119
1120 match self.codec.encode(&envelope) {
1121 Ok(encoded) => {
1122 self.session.broadcast(&encoded).await;
1123 }
1124 Err(e) => {
1125 tracing::error!("node: failed to encode broadcast envelope: {e}");
1126 }
1127 }
1128 }
1129
1130 pub async fn broadcast_json(
1137 &self,
1138 namespace: &str,
1139 payload: &serde_json::Value,
1140 ) -> Result<crate::session::BroadcastReport, NodeError> {
1141 self.broadcast_reported(namespace, "message", payload).await
1142 }
1143
1144 pub async fn broadcast_bytes(
1149 &self,
1150 namespace: &str,
1151 data: &[u8],
1152 ) -> Result<crate::session::BroadcastReport, NodeError> {
1153 let payload = Self::bytes_payload(data);
1154 self.broadcast_reported(namespace, "bytes", &payload).await
1155 }
1156
1157 async fn broadcast_reported(
1161 &self,
1162 namespace: &str,
1163 msg_type: &str,
1164 payload: &serde_json::Value,
1165 ) -> Result<crate::session::BroadcastReport, NodeError> {
1166 self.ensure_not_stopped()?;
1167 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
1168 let encoded = self.codec.encode(&envelope)?;
1169 Ok(self.session.broadcast(&encoded).await)
1170 }
1171
1172 pub fn subscribe(&self, namespace: &str) -> broadcast::Receiver<NamespacedMessage> {
1178 {
1182 let filters = self
1183 .namespace_filters
1184 .read()
1185 .unwrap_or_else(std::sync::PoisonError::into_inner);
1186 if let Some(tx) = filters.get(namespace) {
1187 return tx.subscribe();
1188 }
1189 }
1190
1191 let mut filters = self
1193 .namespace_filters
1194 .write()
1195 .unwrap_or_else(std::sync::PoisonError::into_inner);
1196 if let Some(tx) = filters.get(namespace) {
1198 return tx.subscribe();
1199 }
1200 filters.retain(|_, tx| tx.receiver_count() > 0);
1204 let (tx, rx) = broadcast::channel(256);
1205 filters.insert(namespace.to_string(), tx);
1206 rx
1207 }
1208
1209 pub async fn open_tcp(&self, peer_id: &str, port: u16) -> Result<TcpStream, NodeError> {
1221 self.ensure_not_stopped()?;
1222 let peer = self.resolve_peer(peer_id).await?;
1223 let addr = peer.ip.to_string();
1224 self.network
1225 .dial_tcp_opts(&addr, port, DialOpts { tls: Some(false) })
1226 .await
1227 .map_err(|e| NodeError::ConnectionFailed(e.to_string()))
1228 }
1229
1230 pub async fn listen_tcp(&self, port: u16) -> Result<RawListener, NodeError> {
1238 self.ensure_not_stopped()?;
1239 use crate::transport::tcp::TcpTransport;
1240 use crate::transport::RawTransport;
1241
1242 ensure_port_unreserved(port, self.ws_port)?;
1243 let tcp = TcpTransport::new(self.network.clone());
1244 tcp.listen(port).await.map_err(NodeError::Transport)
1245 }
1246
1247 pub async fn unlisten_tcp(&self, port: u16) -> Result<(), NodeError> {
1252 self.ensure_not_stopped()?;
1253 self.network
1254 .unlisten_tcp(port)
1255 .await
1256 .map_err(NodeError::Network)
1257 }
1258
1259 pub async fn connect_quic(
1268 &self,
1269 peer_id: &str,
1270 port: u16,
1271 ) -> Result<QuicConnection, NodeError> {
1272 self.ensure_not_stopped()?;
1273 let peer = self.resolve_peer(peer_id).await?;
1274 let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
1275 crate::transport::quic::connect_raw(&self.network, &peer.ip.to_string(), port, &alpn)
1276 .await
1277 .map_err(NodeError::Transport)
1278 }
1279
1280 pub async fn listen_quic(&self, port: u16) -> Result<QuicListener, NodeError> {
1288 self.ensure_not_stopped()?;
1289 ensure_port_unreserved(port, self.ws_port)?;
1290 if port == 0 {
1291 return Err(NodeError::NotImplemented(
1292 "ephemeral (port 0) QUIC listeners are not supported over the tsnet relay yet — choose an explicit port"
1293 .to_string(),
1294 ));
1295 }
1296 let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
1297 crate::transport::quic::listen_raw(&self.network, port, &alpn)
1298 .await
1299 .map_err(NodeError::Transport)
1300 }
1301
1302 pub async fn bind_udp(&self, port: u16) -> Result<DatagramSocket, NodeError> {
1312 self.ensure_not_stopped()?;
1313 use crate::transport::udp::{UdpConfig, UdpTransport};
1314 use crate::transport::DatagramTransport;
1315
1316 let udp = UdpTransport::new(self.network.clone(), UdpConfig::default());
1317 udp.bind(port).await.map_err(NodeError::Transport)
1318 }
1319}
1320
1321impl<N: NetworkProvider + 'static> Drop for Node<N> {
1322 fn drop(&mut self) {
1323 self.tasks.cancel.cancel();
1327 }
1328}
1329
1330fn hostname_slug<'a>(hostname: &'a str, app_id: &str) -> Option<&'a str> {
1333 hostname
1334 .strip_prefix("truffle-")?
1335 .strip_prefix(app_id)?
1336 .strip_prefix('-')
1337}
1338
1339fn ensure_port_unreserved(port: u16, ws_port: u16) -> Result<(), NodeError> {
1342 if port == 443 || port == ws_port {
1343 Err(NodeError::ReservedPort(port))
1344 } else {
1345 Ok(())
1346 }
1347}
1348
1349#[derive(Clone)]
1370pub struct NodeBuilder {
1371 app_id: Option<AppId>,
1372 device_name: Option<DeviceName>,
1373 device_id: Option<DeviceId>,
1374 sidecar_path: Option<PathBuf>,
1375 state_dir: Option<String>,
1376 auth_key: Option<String>,
1377 ephemeral: bool,
1378 ws_port: u16,
1379 idle_timeout_secs: Option<u64>,
1380 eager_identity: bool,
1382}
1383
1384impl std::fmt::Debug for NodeBuilder {
1387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1388 f.debug_struct("NodeBuilder")
1389 .field("app_id", &self.app_id)
1390 .field("device_name", &self.device_name)
1391 .field("device_id", &self.device_id)
1392 .field("sidecar_path", &self.sidecar_path)
1393 .field("state_dir", &self.state_dir)
1394 .field("auth_key", &self.auth_key.as_ref().map(|_| "[REDACTED]"))
1395 .field("ephemeral", &self.ephemeral)
1396 .field("ws_port", &self.ws_port)
1397 .field("idle_timeout_secs", &self.idle_timeout_secs)
1398 .field("eager_identity", &self.eager_identity)
1399 .finish()
1400 }
1401}
1402
1403fn atomic_write_string(path: &Path, content: &str) -> std::io::Result<()> {
1416 let parent = path.parent().ok_or_else(|| {
1417 std::io::Error::new(
1418 std::io::ErrorKind::InvalidInput,
1419 "path has no parent directory",
1420 )
1421 })?;
1422 let mut tmp = path.to_path_buf();
1423 tmp.set_extension("tmp");
1424 {
1425 use std::io::Write as _;
1426 let mut f = std::fs::File::create(&tmp)?;
1427 f.write_all(content.as_bytes())?;
1428 f.sync_all()?;
1429 }
1430 #[cfg(unix)]
1431 {
1432 std::fs::rename(&tmp, path)?;
1433 if let Ok(dir) = std::fs::File::open(parent) {
1437 let _ = dir.sync_all();
1438 }
1439 }
1440 #[cfg(windows)]
1441 {
1442 let _ = parent; replace_file_windows(&tmp, path)?;
1444 }
1445 Ok(())
1446}
1447
1448#[cfg(windows)]
1454fn replace_file_windows(tmp: &Path, dest: &Path) -> std::io::Result<()> {
1455 use std::os::windows::ffi::OsStrExt;
1456
1457 const MOVEFILE_REPLACE_EXISTING: u32 = 0x1;
1458 const MOVEFILE_WRITE_THROUGH: u32 = 0x8;
1459
1460 #[link(name = "kernel32")]
1461 extern "system" {
1462 fn MoveFileExW(from: *const u16, to: *const u16, flags: u32) -> i32;
1463 }
1464
1465 fn wide(p: &Path) -> Vec<u16> {
1466 p.as_os_str()
1467 .encode_wide()
1468 .chain(std::iter::once(0))
1469 .collect()
1470 }
1471
1472 let (from, to) = (wide(tmp), wide(dest));
1473 let ok = unsafe {
1474 MoveFileExW(
1475 from.as_ptr(),
1476 to.as_ptr(),
1477 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
1478 )
1479 };
1480 if ok == 0 {
1481 return Err(std::io::Error::last_os_error());
1482 }
1483 Ok(())
1484}
1485
1486impl Default for NodeBuilder {
1487 fn default() -> Self {
1488 Self {
1489 app_id: None,
1490 device_name: None,
1491 device_id: None,
1492 sidecar_path: None,
1493 state_dir: None,
1494 auth_key: None,
1495 ephemeral: false,
1496 ws_port: 9417,
1497 idle_timeout_secs: None,
1498 eager_identity: true,
1499 }
1500 }
1501}
1502
1503impl NodeBuilder {
1504 pub fn app_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1509 let raw: String = s.into();
1510 let app_id = AppId::parse(&raw)
1511 .map_err(|e| NodeError::BuildError(format!("invalid app_id: {e}")))?;
1512 self.app_id = Some(app_id);
1513 Ok(self)
1514 }
1515
1516 pub fn device_name(mut self, s: impl Into<String>) -> Self {
1521 self.device_name = Some(DeviceName::parse(s));
1522 self
1523 }
1524
1525 pub fn device_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1531 let raw: String = s.into();
1532 let device_id = DeviceId::parse(&raw)
1533 .map_err(|e| NodeError::BuildError(format!("invalid device_id: {e}")))?;
1534 self.device_id = Some(device_id);
1535 Ok(self)
1536 }
1537
1538 pub fn sidecar_path(mut self, path: impl Into<PathBuf>) -> Self {
1540 self.sidecar_path = Some(path.into());
1541 self
1542 }
1543
1544 pub fn state_dir(mut self, dir: &str) -> Self {
1546 self.state_dir = Some(dir.to_string());
1547 self
1548 }
1549
1550 pub fn auth_key(mut self, key: &str) -> Self {
1552 self.auth_key = Some(key.to_string());
1553 self
1554 }
1555
1556 pub fn ephemeral(mut self, val: bool) -> Self {
1558 self.ephemeral = val;
1559 self
1560 }
1561
1562 pub fn eager_identity(mut self, enabled: bool) -> Self {
1566 self.eager_identity = enabled;
1567 self
1568 }
1569
1570 pub fn ws_port(mut self, port: u16) -> Self {
1571 self.ws_port = port;
1572 self
1573 }
1574
1575 pub fn idle_timeout_secs(mut self, secs: u64) -> Self {
1581 self.idle_timeout_secs = Some(secs);
1582 self
1583 }
1584
1585 fn prepare_config(&self) -> Result<TailscaleConfig, NodeError> {
1592 let binary_path = self
1594 .sidecar_path
1595 .clone()
1596 .ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
1597
1598 let app_id = self
1600 .app_id
1601 .clone()
1602 .ok_or_else(|| NodeError::BuildError("app_id is required".into()))?;
1603
1604 let device_name = match self.device_name.clone() {
1606 Some(name) => name,
1607 None => {
1608 let os_hostname = hostname::get()
1609 .map_err(|e| {
1610 NodeError::BuildError(format!(
1611 "device_name is unset and hostname::get() failed: {e}"
1612 ))
1613 })?
1614 .to_string_lossy()
1615 .into_owned();
1616 DeviceName::parse(os_hostname)
1617 }
1618 };
1619
1620 let tailscale_host = identity::tailscale_hostname(&app_id, &device_name);
1623
1624 let state_dir = match self.state_dir.clone() {
1630 Some(dir) => dir,
1631 None => {
1632 let base = dirs::data_dir().ok_or_else(|| {
1633 NodeError::BuildError(
1634 "no platform data directory available (dirs::data_dir() returned None); \
1635 set state_dir explicitly to a durable location"
1636 .into(),
1637 )
1638 })?;
1639 base.join("truffle")
1640 .join(app_id.as_str())
1641 .join(identity::slug(device_name.as_str(), 255))
1642 .to_string_lossy()
1643 .into_owned()
1644 }
1645 };
1646
1647 std::fs::create_dir_all(&state_dir)?;
1649
1650 let device_id_file = Path::new(&state_dir).join("device-id.txt");
1655 let device_id = match self.device_id.clone() {
1656 Some(id) => {
1657 atomic_write_string(&device_id_file, id.as_str())?;
1659 id
1660 }
1661 None => {
1662 if device_id_file.exists() {
1663 let s = std::fs::read_to_string(&device_id_file)?.trim().to_string();
1664 DeviceId::parse(&s).map_err(|e| {
1665 NodeError::BuildError(format!(
1666 "device-id.txt at {device_id_file:?} contains an invalid ULID: {e}"
1667 ))
1668 })?
1669 } else {
1670 let id = DeviceId::generate();
1671 atomic_write_string(&device_id_file, id.as_str())?;
1672 id
1673 }
1674 }
1675 };
1676
1677 Ok(TailscaleConfig {
1678 binary_path,
1679 app_id: app_id.as_str().to_string(),
1680 device_id: device_id.as_str().to_string(),
1681 device_name: device_name.as_str().to_string(),
1682 hostname: tailscale_host,
1683 state_dir,
1684 auth_key: self.auth_key.clone(),
1685 ephemeral: if self.ephemeral { Some(true) } else { None },
1686 tags: None,
1687 idle_timeout_secs: self.idle_timeout_secs,
1688 })
1689 }
1690
1691 pub async fn build(self) -> Result<Node<TailscaleProvider>, NodeError> {
1702 let ws_port = self.ws_port;
1703 let eager_identity = self.eager_identity;
1704 let config = self.prepare_config()?;
1705 let state_dir = PathBuf::from(&config.state_dir);
1706
1707 let mut provider = TailscaleProvider::new(config);
1708 provider.start().await.map_err(NodeError::Network)?;
1709
1710 let network = Arc::new(provider);
1711
1712 let ws_config = WsConfig {
1714 port: ws_port,
1715 ..Default::default()
1716 };
1717 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1718
1719 let session = Arc::new(PeerRegistry::with_options(
1721 network.clone(),
1722 ws_transport,
1723 crate::session::PeerRegistryOptions {
1724 eager_identity,
1725 ..Default::default()
1726 },
1727 ));
1728 session.start().await;
1729
1730 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1732 let node = Node::from_parts(network, session, codec)
1733 .with_state_dir(state_dir)
1734 .with_ws_port(ws_port);
1735
1736 tracing::info!("node: started successfully");
1737 Ok(node)
1738 }
1739
1740 pub async fn build_with_auth_handler(
1752 self,
1753 on_auth: impl Fn(String) + Send + 'static,
1754 ) -> Result<Node<TailscaleProvider>, NodeError> {
1755 let ws_port = self.ws_port;
1756 let eager_identity = self.eager_identity;
1757 let config = self.prepare_config()?;
1758 let state_dir = PathBuf::from(&config.state_dir);
1759
1760 let mut provider = TailscaleProvider::new(config);
1761
1762 let mut auth_rx = provider.peer_events();
1764
1765 let auth_task = tokio::spawn(async move {
1767 use crate::network::NetworkPeerEvent;
1768 loop {
1769 match auth_rx.recv().await {
1770 Ok(NetworkPeerEvent::AuthRequired { url }) => {
1771 on_auth(url);
1772 }
1773 Err(broadcast::error::RecvError::Closed) => break,
1774 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1775 _ => {} }
1777 }
1778 });
1779
1780 let start_result = provider.start().await.map_err(NodeError::Network);
1782
1783 auth_task.abort();
1785
1786 start_result?;
1787
1788 let network = Arc::new(provider);
1789
1790 let ws_config = WsConfig {
1792 port: ws_port,
1793 ..Default::default()
1794 };
1795 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1796
1797 let session = Arc::new(PeerRegistry::with_options(
1799 network.clone(),
1800 ws_transport,
1801 crate::session::PeerRegistryOptions {
1802 eager_identity,
1803 ..Default::default()
1804 },
1805 ));
1806 session.start().await;
1807
1808 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1810 let node = Node::from_parts(network, session, codec)
1811 .with_state_dir(state_dir)
1812 .with_ws_port(ws_port);
1813
1814 tracing::info!("node: started successfully (with auth handler)");
1815 Ok(node)
1816 }
1817}
1818
1819#[cfg(test)]
1824mod tests {
1825 use super::*;
1826 use crate::network::{
1827 HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
1828 NetworkTcpListener, NetworkUdpSocket, PeerAddr,
1829 };
1830 use crate::transport::WsConfig;
1831 use serde_json::json;
1832 use std::sync::atomic::{AtomicUsize, Ordering};
1833 use std::time::Duration;
1834 use tokio::sync::{broadcast, mpsc};
1835
1836 #[test]
1837 fn node_builder_debug_redacts_auth_key() {
1838 let builder = NodeBuilder::default().auth_key("dummy-auth-SECRET123");
1839 let dbg = format!("{builder:?}");
1840 assert!(!dbg.contains("SECRET123"));
1841 assert!(dbg.contains("[REDACTED]"));
1842 }
1843
1844 #[test]
1845 fn atomic_write_string_creates_and_replaces() {
1846 let dir =
1847 std::env::temp_dir().join(format!("truffle-atomic-write-test-{}", std::process::id()));
1848 std::fs::create_dir_all(&dir).unwrap();
1849 let path = dir.join("device-id.txt");
1850
1851 atomic_write_string(&path, "01AAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
1852 assert_eq!(
1853 std::fs::read_to_string(&path).unwrap(),
1854 "01AAAAAAAAAAAAAAAAAAAAAAAA"
1855 );
1856
1857 atomic_write_string(&path, "01BBBBBBBBBBBBBBBBBBBBBBBB").unwrap();
1860 assert_eq!(
1861 std::fs::read_to_string(&path).unwrap(),
1862 "01BBBBBBBBBBBBBBBBBBBBBBBB"
1863 );
1864
1865 std::fs::remove_dir_all(&dir).ok();
1866 }
1867
1868 struct MockNetworkProvider {
1871 identity: NodeIdentity,
1872 local_addr: PeerAddr,
1873 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
1874 mock_peers: Arc<RwLock<Vec<NetworkPeer>>>,
1876 stop_calls: Arc<AtomicUsize>,
1879 }
1880
1881 impl MockNetworkProvider {
1882 fn new(id: &str) -> Self {
1883 Self::new_with_device_id(id, id)
1886 }
1887
1888 fn new_with_device_id(id: &str, device_id: &str) -> Self {
1893 let (peer_event_tx, _) = broadcast::channel(64);
1894 Self {
1895 identity: NodeIdentity {
1896 app_id: "test".to_string(),
1897 device_id: device_id.to_string(),
1898 device_name: format!("Test Node {id}"),
1899 tailscale_hostname: format!("truffle-test-{id}"),
1900 tailscale_id: id.to_string(),
1901 dns_name: None,
1902 ip: Some("127.0.0.1".parse().unwrap()),
1903 },
1904 local_addr: PeerAddr {
1905 ip: Some("127.0.0.1".parse().unwrap()),
1906 hostname: format!("truffle-test-{id}"),
1907 dns_name: None,
1908 },
1909 peer_event_tx,
1910 mock_peers: Arc::new(RwLock::new(Vec::new())),
1911 stop_calls: Arc::new(AtomicUsize::new(0)),
1912 }
1913 }
1914
1915 fn event_sender(&self) -> broadcast::Sender<NetworkPeerEvent> {
1916 self.peer_event_tx.clone()
1917 }
1918
1919 fn stop_call_count(&self) -> usize {
1921 self.stop_calls.load(Ordering::SeqCst)
1922 }
1923 }
1924
1925 impl NetworkProvider for MockNetworkProvider {
1926 async fn start(&mut self) -> Result<(), NetworkError> {
1927 Ok(())
1928 }
1929
1930 async fn stop(&self) -> Result<(), NetworkError> {
1931 self.stop_calls.fetch_add(1, Ordering::SeqCst);
1932 Ok(())
1933 }
1934
1935 fn local_identity(&self) -> NodeIdentity {
1936 self.identity.clone()
1937 }
1938
1939 fn local_addr(&self) -> PeerAddr {
1940 self.local_addr.clone()
1941 }
1942
1943 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
1944 self.peer_event_tx.subscribe()
1945 }
1946
1947 async fn peers(&self) -> Vec<NetworkPeer> {
1948 self.mock_peers.read().await.clone()
1949 }
1950
1951 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
1952 let target = format!("{addr}:{port}");
1953 TcpStream::connect(&target)
1954 .await
1955 .map_err(|e| NetworkError::DialFailed(format!("mock dial {target}: {e}")))
1956 }
1957
1958 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
1959 let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
1960 .await
1961 .map_err(|e| NetworkError::ListenFailed(format!("mock listen :{port}: {e}")))?;
1962
1963 let actual_port = listener.local_addr().unwrap().port();
1964 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
1965
1966 tokio::spawn(async move {
1967 loop {
1968 match listener.accept().await {
1969 Ok((stream, addr)) => {
1970 let conn = IncomingConnection {
1971 stream,
1972 remote_addr: addr.to_string(),
1973 remote_identity: String::new(),
1974 port: actual_port,
1975 };
1976 if tx.send(conn).await.is_err() {
1977 break;
1978 }
1979 }
1980 Err(e) => {
1981 tracing::debug!("mock listener error: {e}");
1982 break;
1983 }
1984 }
1985 }
1986 });
1987
1988 Ok(NetworkTcpListener {
1989 port: actual_port,
1990 incoming: rx,
1991 })
1992 }
1993
1994 async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
1995 Ok(())
1996 }
1997
1998 async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
1999 Err(NetworkError::Internal("mock: UDP not supported".into()))
2000 }
2001
2002 async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
2003 Ok(PingResult {
2004 latency: Duration::from_millis(1),
2005 connection: "direct".to_string(),
2006 peer_addr: None,
2007 })
2008 }
2009
2010 async fn health(&self) -> HealthInfo {
2011 HealthInfo {
2012 state: "running".to_string(),
2013 healthy: true,
2014 ..Default::default()
2015 }
2016 }
2017 }
2018
2019 fn make_loopback_peer(id: &str) -> NetworkPeer {
2022 NetworkPeer {
2023 id: id.to_string(),
2024 hostname: format!("truffle-test-{id}"),
2025 ip: "127.0.0.1".parse().unwrap(),
2026 online: true,
2027 cur_addr: Some("127.0.0.1:41641".to_string()),
2028 relay: None,
2029 os: Some("linux".to_string()),
2030 last_seen: Some("2026-03-25T12:00:00Z".to_string()),
2031 key_expiry: None,
2032 dns_name: None,
2033 }
2034 }
2035
2036 fn ws_config(port: u16) -> WsConfig {
2037 WsConfig {
2038 port,
2039 ping_interval: Duration::from_secs(300),
2040 pong_timeout: Duration::from_secs(300),
2041 ..Default::default()
2042 }
2043 }
2044
2045 async fn random_port() -> u16 {
2046 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2047 l.local_addr().unwrap().port()
2048 }
2049
2050 async fn make_test_node(
2052 id: &str,
2053 ws_port: u16,
2054 ) -> (
2055 Node<MockNetworkProvider>,
2056 broadcast::Sender<NetworkPeerEvent>,
2057 Arc<MockNetworkProvider>,
2058 ) {
2059 make_test_node_with_device_id(id, id, ws_port).await
2060 }
2061
2062 async fn make_test_node_with_device_id(
2065 id: &str,
2066 device_id: &str,
2067 ws_port: u16,
2068 ) -> (
2069 Node<MockNetworkProvider>,
2070 broadcast::Sender<NetworkPeerEvent>,
2071 Arc<MockNetworkProvider>,
2072 ) {
2073 let provider = MockNetworkProvider::new_with_device_id(id, device_id);
2074 let event_tx = provider.event_sender();
2075 let network = Arc::new(provider);
2076 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config(ws_port)));
2077 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
2078 session.start().await;
2079
2080 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
2081 let node = Node::from_parts(network.clone(), session, codec);
2082
2083 (node, event_tx, network)
2084 }
2085
2086 #[tokio::test]
2089 async fn stop_drains_all_background_tasks() {
2090 let ws_port = random_port().await;
2091 let (node, _event_tx, _network) = make_test_node("node-drain", ws_port).await;
2092 let node = Arc::new(node);
2093
2094 let _offers = node.file_transfer().offer_channel(node.clone()).await;
2096 assert!(!node.tasks.tracker.is_empty(), "tasks should be running");
2097
2098 node.stop().await;
2099
2100 assert!(
2102 node.tasks.tracker.is_empty(),
2103 "background tasks still alive after stop()"
2104 );
2105 assert!(
2106 node.file_transfer_state
2107 .receiver_handle
2108 .lock()
2109 .await
2110 .is_none(),
2111 "FT receiver handle not cleared by stop()"
2112 );
2113 }
2114
2115 #[tokio::test]
2116 async fn stopped_node_fails_raw_and_resolution_apis_fast() {
2117 let ws_port = random_port().await;
2118 let (node, _event_tx, _network) = make_test_node("node-guards", ws_port).await;
2119 node.stop().await;
2120
2121 assert!(matches!(
2122 node.open_tcp("nobody", 4242).await.unwrap_err(),
2123 NodeError::Stopped
2124 ));
2125 assert!(matches!(
2126 node.listen_tcp(4242).await.unwrap_err(),
2127 NodeError::Stopped
2128 ));
2129 assert!(matches!(
2130 node.connect_quic("nobody", 4242).await.unwrap_err(),
2131 NodeError::Stopped
2132 ));
2133 assert!(matches!(
2134 node.listen_quic(4242).await.unwrap_err(),
2135 NodeError::Stopped
2136 ));
2137 assert!(matches!(
2138 node.bind_udp(4242).await.err(),
2139 Some(NodeError::Stopped)
2140 ));
2141 assert!(matches!(
2142 node.ping("nobody").await.unwrap_err(),
2143 NodeError::Stopped
2144 ));
2145 assert!(matches!(
2146 node.resolve_peer_id("nobody").await.unwrap_err(),
2147 NodeError::Stopped
2148 ));
2149 }
2150
2151 #[test]
2152 fn bytes_payload_roundtrip() {
2153 let data = vec![0u8, 159, 146, 150, 255];
2156 let payload = Node::<MockNetworkProvider>::bytes_payload(&data);
2157 assert_eq!(payload["encoding"], "base64");
2158
2159 let msg = NamespacedMessage {
2160 from: "p".into(),
2161 namespace: "ns".into(),
2162 msg_type: "bytes".into(),
2163 payload,
2164 timestamp: None,
2165 };
2166 assert_eq!(msg.payload_bytes().unwrap(), data);
2167 }
2168
2169 #[test]
2170 fn payload_bytes_rejects_other_msg_types() {
2171 let msg = NamespacedMessage {
2172 from: "p".into(),
2173 namespace: "ns".into(),
2174 msg_type: "message".into(),
2175 payload: json!({"data": "aGk="}),
2176 timestamp: None,
2177 };
2178 assert!(msg.payload_bytes().is_none());
2179 }
2180
2181 #[tokio::test]
2182 async fn broadcast_json_reports_zero_peers() {
2183 let ws_port = random_port().await;
2184 let (node, _event_tx, _network) = make_test_node("node-bj", ws_port).await;
2185
2186 let report = node.broadcast_json("ns", &json!({"a": 1})).await.unwrap();
2187 assert_eq!(report.attempted, 0);
2188 assert_eq!(report.queued, 0);
2189 assert!(report.failed.is_empty());
2190 }
2191
2192 #[tokio::test]
2193 async fn subscribe_slow_path_sweeps_dead_namespaces() {
2194 let ws_port = random_port().await;
2195 let (node, _event_tx, _network) = make_test_node("node-sweep", ws_port).await;
2196
2197 let rx = node.subscribe("dyn-a");
2198 drop(rx);
2199 let _rx_b = node.subscribe("dyn-b");
2202
2203 let filters = node.namespace_filters.read().unwrap();
2204 assert!(!filters.contains_key("dyn-a"), "dead entry not swept");
2205 assert!(filters.contains_key("dyn-b"));
2206 }
2207
2208 #[tokio::test]
2209 async fn router_prunes_namespace_after_receivers_drop() {
2210 let ws_port = random_port().await;
2211 let (node, _event_tx, _network) = make_test_node("node-prune", ws_port).await;
2212
2213 let rx = node.subscribe("dyn-c");
2214 drop(rx);
2215
2216 let envelope = Envelope::new("dyn-c", "message", json!({"x": 1})).with_timestamp();
2218 let data = JsonCodec.encode(&envelope).unwrap();
2219 node.session.test_inject_incoming("peer-x", data);
2220
2221 for _ in 0..50 {
2223 {
2224 let filters = node.namespace_filters.read().unwrap();
2225 if !filters.contains_key("dyn-c") {
2226 return;
2227 }
2228 }
2229 tokio::time::sleep(Duration::from_millis(10)).await;
2230 }
2231 panic!("dead namespace entry was not pruned by the router");
2232 }
2233
2234 #[tokio::test]
2235 async fn test_node_builder_creates_node() {
2236 let ws_port = random_port().await;
2237 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2238
2239 let identity = node.local_info();
2240 assert_eq!(identity.tailscale_id, "node-1");
2241 assert_eq!(identity.device_id, "node-1");
2242 assert!(identity.tailscale_hostname.contains("node-1"));
2243 }
2244
2245 #[tokio::test]
2246 async fn test_node_peers_from_network() {
2247 let ws_port = random_port().await;
2248 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
2249
2250 let peers = node.peers().await;
2252 assert!(peers.is_empty());
2253
2254 let peer = make_loopback_peer("peer-a");
2256 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2257 tokio::time::sleep(Duration::from_millis(50)).await;
2258
2259 let peers = node.peers().await;
2260 assert_eq!(peers.len(), 1);
2261 assert_eq!(peers[0].tailscale_id, "peer-a");
2262 assert!(
2263 peers[0].device_id.is_none(),
2264 "RFC 022: no ULID before identity"
2265 );
2266 assert!(peers[0].online);
2267 assert!(!peers[0].ws_connected);
2268 }
2269
2270 #[tokio::test]
2271 #[allow(deprecated)] async fn test_node_send_to_unknown_peer_errors() {
2273 let ws_port = random_port().await;
2274 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2275
2276 let result = node.send("nonexistent", "test", b"hello").await;
2277 assert!(result.is_err());
2278 let err_str = result.unwrap_err().to_string();
2279 assert!(
2280 err_str.contains("unknown peer") || err_str.contains("not found"),
2281 "expected unknown peer error, got: {err_str}"
2282 );
2283 }
2284
2285 #[tokio::test]
2286 async fn test_node_send_wraps_in_envelope() {
2287 let codec = JsonCodec;
2290 let data = b"hello world";
2291 let envelope = Envelope::new("test-ns", "message", serde_json::Value::from(data.to_vec()))
2292 .with_timestamp();
2293
2294 let encoded = codec.encode(&envelope).unwrap();
2295 let decoded = codec.decode(&encoded).unwrap();
2296
2297 assert_eq!(decoded.namespace, "test-ns");
2298 assert_eq!(decoded.msg_type, "message");
2299 assert!(decoded.timestamp.is_some());
2300 }
2301
2302 #[tokio::test]
2303 async fn test_node_subscribe_filters_by_namespace() {
2304 let ws_port = random_port().await;
2305 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2306
2307 let _rx_chat = node.subscribe("chat");
2309 let _rx_ft = node.subscribe("ft");
2310
2311 let _rx_chat2 = node.subscribe("chat");
2313 }
2314
2315 #[tokio::test]
2316 #[allow(deprecated)] async fn test_node_broadcast() {
2318 let ws_port = random_port().await;
2319 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2320
2321 node.broadcast("test", b"hello everyone").await;
2323 }
2324
2325 #[tokio::test]
2326 async fn test_node_open_tcp_resolves_peer() {
2327 let ws_port = random_port().await;
2328 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
2329
2330 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2332 let tcp_port = listener.local_addr().unwrap().port();
2333
2334 let peer = make_loopback_peer("peer-tcp");
2336 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2337 tokio::time::sleep(Duration::from_millis(50)).await;
2338
2339 let accept_handle = tokio::spawn(async move {
2341 let (stream, _) = listener.accept().await.unwrap();
2342 stream
2343 });
2344
2345 let stream = node.open_tcp("peer-tcp", tcp_port).await;
2347 assert!(stream.is_ok(), "open_tcp failed: {:?}", stream.err());
2348
2349 let _ = accept_handle.await;
2350 }
2351
2352 #[tokio::test]
2353 async fn test_node_open_tcp_unknown_peer_errors() {
2354 let ws_port = random_port().await;
2355 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2356
2357 let result = node.open_tcp("nonexistent", 8080).await;
2358 assert!(result.is_err());
2359 let err_str = result.unwrap_err().to_string();
2360 assert!(
2361 err_str.contains("not found"),
2362 "expected peer not found error, got: {err_str}"
2363 );
2364 }
2365
2366 #[tokio::test]
2367 async fn test_node_ping_resolves_peer() {
2368 let ws_port = random_port().await;
2369 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
2370
2371 let result = node.ping("peer-ping").await;
2373 assert!(result.is_err());
2374
2375 let peer = make_loopback_peer("peer-ping");
2377 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2378 tokio::time::sleep(Duration::from_millis(50)).await;
2379
2380 let result = node.ping("peer-ping").await;
2382 assert!(result.is_ok());
2383 assert_eq!(result.unwrap().latency, Duration::from_millis(1));
2384 }
2385
2386 #[tokio::test]
2387 async fn test_node_health() {
2388 let ws_port = random_port().await;
2389 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2390
2391 let health = node.health().await;
2392 assert!(health.healthy);
2393 assert_eq!(health.state, "running");
2394 }
2395
2396 #[tokio::test]
2397 async fn test_node_connect_quic_unknown_peer_errors() {
2398 let ws_port = random_port().await;
2399 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2400
2401 let result = node.connect_quic("peer", 4433).await;
2402 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
2403 }
2404
2405 #[tokio::test]
2406 async fn test_node_listen_tcp() {
2407 let ws_port = random_port().await;
2408 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2409
2410 let listener = node.listen_tcp(0).await;
2412 assert!(listener.is_ok(), "listen_tcp failed: {:?}", listener.err());
2413 }
2414
2415 #[tokio::test]
2416 async fn test_envelope_serialize_deserialize() {
2417 let envelope = Envelope::new("chat", "message", json!({"text": "hello"})).with_timestamp();
2418
2419 let bytes = envelope.serialize().unwrap();
2420 let decoded = Envelope::deserialize(&bytes).unwrap();
2421
2422 assert_eq!(decoded.namespace, "chat");
2423 assert_eq!(decoded.msg_type, "message");
2424 assert_eq!(decoded.payload["text"], "hello");
2425 assert!(decoded.timestamp.is_some());
2426 }
2427
2428 #[tokio::test]
2429 async fn test_envelope_codec_json() {
2430 let codec = JsonCodec;
2431 let envelope = Envelope::new("ft", "offer", json!({"file": "test.bin"}));
2432
2433 let encoded = codec.encode(&envelope).unwrap();
2434 let decoded = codec.decode(&encoded).unwrap();
2435
2436 assert_eq!(decoded.namespace, "ft");
2437 assert_eq!(decoded.payload["file"], "test.bin");
2438 }
2439
2440 #[tokio::test]
2441 async fn test_envelope_unknown_fields_ignored() {
2442 let json_bytes = br#"{
2443 "namespace": "v2",
2444 "msg_type": "new",
2445 "payload": {},
2446 "future_field": "ignored"
2447 }"#;
2448
2449 let codec = JsonCodec;
2450 let decoded = codec.decode(json_bytes).unwrap();
2451 assert_eq!(decoded.namespace, "v2");
2452 assert_eq!(decoded.msg_type, "new");
2453 }
2454
2455 #[tokio::test]
2456 #[allow(deprecated)] async fn test_node_send_and_receive_roundtrip() {
2458 let port_a = random_port().await;
2460 let port_b = random_port().await;
2461
2462 let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
2463 let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
2464
2465 let peer_b = NetworkPeer {
2467 id: "node-b".to_string(),
2468 hostname: "truffle-test-node-b".to_string(),
2469 ip: "127.0.0.1".parse().unwrap(),
2470 online: true,
2471 cur_addr: Some("127.0.0.1:41641".to_string()),
2472 relay: None,
2473 os: None,
2474 last_seen: None,
2475 key_expiry: None,
2476 dns_name: None,
2477 };
2478 let peer_a = NetworkPeer {
2479 id: "node-a".to_string(),
2480 hostname: "truffle-test-node-a".to_string(),
2481 ip: "127.0.0.1".parse().unwrap(),
2482 online: true,
2483 cur_addr: Some("127.0.0.1:41641".to_string()),
2484 relay: None,
2485 os: None,
2486 last_seen: None,
2487 key_expiry: None,
2488 dns_name: None,
2489 };
2490
2491 let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
2492 let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
2493 tokio::time::sleep(Duration::from_millis(100)).await;
2494
2495 let mut rx = node_b.subscribe("test");
2497
2498 let send_result = node_a.send("node-b", "test", b"hello from a").await;
2501
2502 if send_result.is_ok() {
2510 let msg = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await;
2512 if let Ok(Ok(msg)) = msg {
2513 assert_eq!(msg.namespace, "test");
2514 }
2515 }
2516 }
2519
2520 #[tokio::test]
2523 #[allow(deprecated)] async fn test_node_stop_shuts_down_provider_and_is_idempotent() {
2525 let ws_port = random_port().await;
2526 let (node, event_tx, network) = make_test_node("node-1", ws_port).await;
2527
2528 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-1")));
2529 tokio::time::sleep(Duration::from_millis(50)).await;
2530
2531 assert_eq!(network.stop_call_count(), 0);
2532 node.stop().await;
2533 assert_eq!(
2534 network.stop_call_count(),
2535 1,
2536 "stop() must shut down the provider"
2537 );
2538
2539 node.stop().await;
2541 assert_eq!(network.stop_call_count(), 1);
2542
2543 let err = node.send("peer-1", "test", b"hi").await.unwrap_err();
2545 assert!(matches!(err, NodeError::Stopped));
2546 }
2547
2548 #[tokio::test]
2549 #[allow(deprecated)] async fn test_node_stop_closes_ws_connections() {
2551 let port_a = random_port().await;
2560 let (node_a, event_tx_a, _net_a) =
2561 make_test_node_with_device_id("node-a", "dev-node-a", port_a).await;
2562
2563 let self_peer = NetworkPeer {
2564 id: "node-a".to_string(),
2565 hostname: "truffle-test-node-a".to_string(),
2566 ip: "127.0.0.1".parse().unwrap(),
2567 online: true,
2568 cur_addr: Some("127.0.0.1:41641".to_string()),
2569 relay: None,
2570 os: None,
2571 last_seen: None,
2572 key_expiry: None,
2573 dns_name: None,
2574 };
2575 let _ = event_tx_a.send(NetworkPeerEvent::Joined(self_peer));
2576 tokio::time::sleep(Duration::from_millis(100)).await;
2577
2578 node_a
2579 .send("node-a", "test", b"hello from a")
2580 .await
2581 .expect("loopback self-dial should establish a WS connection");
2582
2583 let peers = node_a.peers().await;
2584 let entry = peers
2585 .iter()
2586 .find(|p| p.tailscale_id == "node-a")
2587 .expect("self peer discovered");
2588 assert!(
2589 entry.ws_connected,
2590 "send() should have established a WS connection"
2591 );
2592
2593 node_a.stop().await;
2594 let peers = node_a.peers().await;
2595 let entry = peers
2596 .iter()
2597 .find(|p| p.tailscale_id == "node-a")
2598 .expect("self peer still discovered");
2599 assert!(
2600 !entry.ws_connected,
2601 "stop() must close and un-mark WS connections"
2602 );
2603 }
2604
2605 async fn inject_peer_with_identity(
2611 node: &Node<MockNetworkProvider>,
2612 event_tx: &broadcast::Sender<NetworkPeerEvent>,
2613 tailscale_id: &str,
2614 device_id: &str,
2615 device_name: &str,
2616 ) {
2617 let peer = make_loopback_peer(tailscale_id);
2618 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2619 tokio::time::sleep(Duration::from_millis(30)).await;
2620
2621 let identity = crate::session::PeerIdentity {
2622 app_id: "test".into(),
2623 device_id: device_id.into(),
2624 device_name: device_name.into(),
2625 os: "linux".into(),
2626 tailscale_id: tailscale_id.into(),
2627 };
2628 assert!(
2629 node.session
2630 .test_stamp_identity(tailscale_id, identity)
2631 .await,
2632 "peer {tailscale_id} should exist in session registry before stamping identity"
2633 );
2634 }
2635
2636 #[tokio::test]
2637 async fn test_resolve_peer_id_by_device_id() {
2638 let ws_port = random_port().await;
2639 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2640
2641 inject_peer_with_identity(
2642 &node,
2643 &event_tx,
2644 "tailscale-abc",
2645 "01HZZZZZZZZZZZZZZZZZZZZZZZ",
2646 "Alice MacBook",
2647 )
2648 .await;
2649
2650 let resolved = node
2651 .resolve_peer_id("01HZZZZZZZZZZZZZZZZZZZZZZZ")
2652 .await
2653 .unwrap();
2654 assert_eq!(resolved, "01HZZZZZZZZZZZZZZZZZZZZZZZ");
2655 }
2656
2657 #[tokio::test]
2658 async fn test_resolve_peer_id_by_device_name() {
2659 let ws_port = random_port().await;
2660 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2661
2662 inject_peer_with_identity(
2663 &node,
2664 &event_tx,
2665 "tailscale-abc",
2666 "01HXYZXYZXYZXYZXYZXYZXYZXY",
2667 "Bob's Mac",
2668 )
2669 .await;
2670
2671 let resolved = node.resolve_peer_id("Bob's Mac").await.unwrap();
2672 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
2673 }
2674
2675 #[tokio::test]
2676 async fn test_resolve_peer_id_by_device_id_prefix() {
2677 let ws_port = random_port().await;
2678 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2679
2680 inject_peer_with_identity(
2681 &node,
2682 &event_tx,
2683 "tailscale-abc",
2684 "01HXYZXYZXYZXYZXYZXYZXYZXY",
2685 "laptop",
2686 )
2687 .await;
2688
2689 let resolved = node.resolve_peer_id("01HX").await.unwrap();
2692 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
2693 }
2694
2695 #[tokio::test]
2696 async fn test_resolve_peer_id_by_tailscale_id_legacy() {
2697 let ws_port = random_port().await;
2701 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2702
2703 inject_peer_with_identity(
2704 &node,
2705 &event_tx,
2706 "tailscale-legacy",
2707 "01HLEGACY0000000000000000X",
2708 "legacy box",
2709 )
2710 .await;
2711
2712 let resolved = node.resolve_peer_id("tailscale-legacy").await.unwrap();
2713 assert_eq!(resolved, "01HLEGACY0000000000000000X");
2714 }
2715
2716 #[tokio::test]
2717 async fn test_resolve_peer_id_unknown() {
2718 let ws_port = random_port().await;
2719 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2720 let result = node.resolve_peer_id("nope").await;
2721 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
2722 }
2723
2724 #[tokio::test]
2727 async fn test_resolve_peer_by_bare_name_before_hello() {
2728 let ws_port = random_port().await;
2732 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2733
2734 let mut peer = make_loopback_peer("nodeid-9");
2735 peer.hostname = "truffle-test-ec2-smoke".to_string();
2736 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2737 tokio::time::sleep(Duration::from_millis(50)).await;
2738
2739 assert_eq!(node.resolve_peer("ec2-smoke").await.unwrap().id, "nodeid-9");
2741 assert_eq!(node.resolve_peer("EC2 Smoke").await.unwrap().id, "nodeid-9");
2742 assert!(node.resolve_peer("other-box").await.is_err());
2744 }
2745
2746 #[tokio::test]
2747 async fn test_peers_device_name_strips_hostname_before_hello() {
2748 let ws_port = random_port().await;
2749 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2750
2751 let mut peer = make_loopback_peer("nodeid-9");
2752 peer.hostname = "truffle-test-ec2-smoke".to_string();
2753 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2754 tokio::time::sleep(Duration::from_millis(50)).await;
2755
2756 let peers = node.peers().await;
2757 assert_eq!(peers.len(), 1);
2758 assert!(peers[0].device_name.is_none());
2760 assert_eq!(peers[0].display_name, "ec2-smoke");
2761 assert_eq!(peers[0].hostname, "truffle-test-ec2-smoke");
2762 assert!(peers[0].device_id.is_none());
2763 }
2764
2765 #[tokio::test]
2766 async fn test_resolve_peer_stale_ref_is_peer_gone() {
2767 let ws_port = random_port().await;
2768 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2769
2770 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-9")));
2771 tokio::time::sleep(Duration::from_millis(50)).await;
2772
2773 let live_ref = node.peers().await[0].peer_ref.clone();
2774 assert_eq!(node.resolve_peer(&live_ref).await.unwrap().id, "peer-9");
2775
2776 let _ = event_tx.send(NetworkPeerEvent::Left("peer-9".to_string()));
2780 tokio::time::sleep(Duration::from_millis(50)).await;
2781 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-9")));
2782 tokio::time::sleep(Duration::from_millis(50)).await;
2783
2784 assert!(matches!(
2785 node.resolve_peer(&live_ref).await,
2786 Err(NodeError::PeerGone(_))
2787 ));
2788 assert!(matches!(
2791 node.resolve_peer("peer-9:99").await,
2792 Err(NodeError::PeerGone(_))
2793 ));
2794 assert!(matches!(
2797 node.peer(&live_ref, Some(2_000)).await,
2798 Err(NodeError::PeerGone(_))
2799 ));
2800 }
2801
2802 #[tokio::test]
2803 async fn test_resolve_peer_accepts_ip() {
2804 let ws_port = random_port().await;
2805 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2806
2807 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-a")));
2808 tokio::time::sleep(Duration::from_millis(50)).await;
2809
2810 let resolved = node.resolve_peer("127.0.0.1").await.unwrap();
2811 assert_eq!(resolved.id, "peer-a");
2812 assert_eq!(node.resolve_peer_id("127.0.0.1").await.unwrap(), "peer-a");
2815 }
2816
2817 #[tokio::test]
2818 async fn test_listen_tcp_rejects_reserved_ports() {
2819 let ws_port = random_port().await;
2820 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2821
2822 for port in [443u16, 9417] {
2823 let err = node.listen_tcp(port).await.unwrap_err();
2824 assert!(
2825 matches!(err, NodeError::ReservedPort(p) if p == port),
2826 "expected ReservedPort({port}), got: {err}"
2827 );
2828 }
2829 }
2830
2831 #[tokio::test]
2832 async fn test_listen_quic_rejects_reserved_and_ephemeral_ports() {
2833 let ws_port = random_port().await;
2834 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2835
2836 let err = node.listen_quic(443).await.unwrap_err();
2837 assert!(matches!(err, NodeError::ReservedPort(443)));
2838
2839 let err = node.listen_quic(0).await.unwrap_err();
2840 assert!(matches!(err, NodeError::NotImplemented(_)));
2841 }
2842
2843 #[tokio::test]
2844 async fn test_bind_udp_falls_back_to_direct_socket() {
2845 let ws_port = random_port().await;
2848 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2849
2850 let socket = node.bind_udp(0).await.unwrap();
2851 let addr = socket.local_addr().unwrap();
2852 assert_ne!(addr.port(), 0);
2853 }
2854
2855 #[test]
2856 fn test_raw_alpn_scoping() {
2857 assert_eq!(
2858 crate::transport::quic::raw_alpn("demo"),
2859 b"truffle-raw.demo".to_vec()
2860 );
2861 assert_eq!(
2862 crate::transport::quic::raw_alpn(""),
2863 b"truffle-raw".to_vec()
2864 );
2865 }
2866
2867 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2868 async fn test_connect_quic_roundtrip_via_raw_listener() {
2869 let ws_port = random_port().await;
2870 let (client_node, event_tx, _net) = make_test_node("cli", ws_port).await;
2871
2872 let server_network = Arc::new(MockNetworkProvider::new("srv"));
2875 let alpn = crate::transport::quic::raw_alpn("test");
2876 let listener = crate::transport::quic::listen_raw(&server_network, 0, &alpn)
2877 .await
2878 .unwrap();
2879 let port = listener.port();
2880 assert_ne!(port, 0);
2881
2882 let echo_task = tokio::spawn(async move {
2883 let conn = listener.accept().await.expect("no incoming connection");
2884 let mut stream = conn
2885 .accept_stream()
2886 .await
2887 .unwrap()
2888 .expect("connection closed before stream");
2889 let mut received = Vec::new();
2890 while let Some(chunk) = stream.read(1024).await.unwrap() {
2891 received.extend_from_slice(&chunk);
2892 }
2893 stream.write(&received).await.unwrap();
2894 stream.finish();
2895 tokio::time::sleep(Duration::from_millis(500)).await;
2898 conn.close();
2899 });
2900
2901 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("srv-peer")));
2903 tokio::time::sleep(Duration::from_millis(50)).await;
2904
2905 let conn = client_node.connect_quic("srv-peer", port).await.unwrap();
2906 let mut stream = conn.open_stream().await.unwrap();
2907 stream.write(b"hello quic").await.unwrap();
2908 stream.finish();
2909
2910 let mut echoed = Vec::new();
2911 while let Some(chunk) = stream.read(1024).await.unwrap() {
2912 echoed.extend_from_slice(&chunk);
2913 }
2914 assert_eq!(echoed, b"hello quic");
2915
2916 echo_task.await.unwrap();
2917 conn.close();
2918 }
2919
2920 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2921 async fn test_quic_raw_alpn_mismatch_rejected() {
2922 let server_network = Arc::new(MockNetworkProvider::new("srv"));
2925 let listener = crate::transport::quic::listen_raw(
2926 &server_network,
2927 0,
2928 &crate::transport::quic::raw_alpn("app-a"),
2929 )
2930 .await
2931 .unwrap();
2932 let port = listener.port();
2933
2934 let client_network = Arc::new(MockNetworkProvider::new("cli"));
2935 let result = crate::transport::quic::connect_raw(
2936 &client_network,
2937 "127.0.0.1",
2938 port,
2939 &crate::transport::quic::raw_alpn("app-b"),
2940 )
2941 .await;
2942
2943 assert!(
2944 result.is_err(),
2945 "cross-app QUIC connect must fail (ALPN mismatch)"
2946 );
2947 }
2948
2949 #[tokio::test]
2952 async fn test_rfc022_node_peer_resolves_and_wait_miss_returns_none() {
2953 let ws_port = random_port().await;
2954 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2955
2956 assert!(node.peer("nope", None).await.unwrap().is_none());
2958
2959 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-a")));
2960 tokio::time::sleep(Duration::from_millis(50)).await;
2961
2962 let p = node.peer("peer-a", None).await.unwrap().expect("found");
2963 assert_eq!(p.tailscale_id, "peer-a");
2964 assert!(p.device_id.is_none());
2965
2966 let start = std::time::Instant::now();
2968 let miss = node.peer("still-missing", Some(80)).await.unwrap();
2969 assert!(miss.is_none());
2970 assert!(start.elapsed() >= Duration::from_millis(70));
2971 }
2972
2973 #[test]
2974 fn test_rfc022_peer_projection_never_uses_tailscale_as_device_id() {
2975 use crate::session::PeerState;
2976 use std::net::Ipv4Addr;
2977
2978 let pre = PeerState {
2980 id: "ts-abc".into(),
2981 generation: 1,
2982 name: "truffle-app-laptop".into(),
2983 ip: Ipv4Addr::new(100, 64, 0, 1).into(),
2984 online: true,
2985 ws_connected: false,
2986 connection_type: "direct".into(),
2987 os: None,
2988 last_seen: None,
2989 identity: None,
2990 identity_suppressed: false,
2991 };
2992 let p = Peer::from(pre);
2993 assert!(p.device_id.is_none());
2994 assert_eq!(p.tailscale_id, "ts-abc");
2995 assert_eq!(p.peer_ref, "ts-abc:1");
2996 assert_eq!(p.display_name, "laptop");
2997 assert!(p
2998 .device_id
2999 .as_ref()
3000 .map(|d| d.as_str() != p.tailscale_id)
3001 .unwrap_or(true));
3002
3003 let post = PeerState {
3005 id: "ts-abc".into(),
3006 generation: 1,
3007 name: "truffle-app-laptop".into(),
3008 ip: Ipv4Addr::new(100, 64, 0, 1).into(),
3009 online: true,
3010 ws_connected: true,
3011 connection_type: "direct".into(),
3012 os: Some("darwin".into()),
3013 last_seen: None,
3014 identity: Some(crate::session::PeerIdentity {
3015 app_id: "app".into(),
3016 device_id: "01J4K9M2Z8AB3RNYQPW6H5TC0X".into(),
3017 device_name: "Alice's Mac".into(),
3018 os: "darwin".into(),
3019 tailscale_id: "ts-abc".into(),
3020 }),
3021 identity_suppressed: false,
3022 };
3023 let p = Peer::from(post);
3024 assert_eq!(p.device_id.as_deref(), Some("01J4K9M2Z8AB3RNYQPW6H5TC0X"));
3025 assert_ne!(p.device_id.as_deref().unwrap(), p.tailscale_id);
3026 assert_eq!(p.display_name, "Alice's Mac");
3027
3028 let suppressed = PeerState {
3030 id: "ts-xyz".into(),
3031 generation: 1,
3032 name: "truffle-app-other".into(),
3033 ip: Ipv4Addr::new(100, 64, 0, 2).into(),
3034 online: true,
3035 ws_connected: true,
3036 connection_type: "direct".into(),
3037 os: None,
3038 last_seen: None,
3039 identity: Some(crate::session::PeerIdentity {
3040 app_id: "app".into(),
3041 device_id: "01J4K9M2Z8AB3RNYQPW6H5TC0X".into(),
3042 device_name: "Clone".into(),
3043 os: "linux".into(),
3044 tailscale_id: "ts-xyz".into(),
3045 }),
3046 identity_suppressed: true,
3047 };
3048 let p = Peer::from(suppressed);
3049 assert!(
3050 p.device_id.is_none(),
3051 "suppressed claim must project null device_id"
3052 );
3053 }
3054}