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 health(&self) -> HealthInfo {
982 self.network.health().await
983 }
984
985 fn ensure_not_stopped(&self) -> Result<(), NodeError> {
992 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
993 return Err(NodeError::Stopped);
994 }
995 Ok(())
996 }
997
998 fn map_session_send_err(e: crate::session::SessionError) -> NodeError {
1001 match e {
1002 crate::session::SessionError::PeerGone(r) => NodeError::PeerGone(r),
1003 e => NodeError::Session(e),
1004 }
1005 }
1006
1007 #[deprecated(
1018 since = "0.7.0",
1019 note = "wire type depends on data contents; use send_json or send_bytes"
1020 )]
1021 pub async fn send(&self, peer_id: &str, namespace: &str, data: &[u8]) -> Result<(), NodeError> {
1022 self.ensure_not_stopped()?;
1023 let payload = std::str::from_utf8(data)
1027 .ok()
1028 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
1029 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
1030
1031 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
1032
1033 let encoded = self.codec.encode(&envelope)?;
1034 self.session
1035 .send(peer_id, &encoded)
1036 .await
1037 .map_err(Self::map_session_send_err)?;
1038 Ok(())
1039 }
1040
1041 pub async fn send_json(
1048 &self,
1049 peer_id: &str,
1050 namespace: &str,
1051 payload: &serde_json::Value,
1052 ) -> Result<(), NodeError> {
1053 self.send_typed(peer_id, namespace, "message", payload)
1054 .await
1055 }
1056
1057 pub async fn send_bytes(
1065 &self,
1066 peer_id: &str,
1067 namespace: &str,
1068 data: &[u8],
1069 ) -> Result<(), NodeError> {
1070 let payload = Self::bytes_payload(data);
1071 self.send_typed(peer_id, namespace, "bytes", &payload).await
1072 }
1073
1074 fn bytes_payload(data: &[u8]) -> serde_json::Value {
1076 use base64::Engine as _;
1077 serde_json::json!({
1078 "encoding": "base64",
1079 "data": base64::engine::general_purpose::STANDARD.encode(data),
1080 })
1081 }
1082
1083 pub async fn send_typed(
1091 &self,
1092 peer_id: &str,
1093 namespace: &str,
1094 msg_type: &str,
1095 payload: &serde_json::Value,
1096 ) -> Result<(), NodeError> {
1097 self.ensure_not_stopped()?;
1098 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
1099 let encoded = self.codec.encode(&envelope)?;
1100 self.session
1101 .send(peer_id, &encoded)
1102 .await
1103 .map_err(Self::map_session_send_err)?;
1104 Ok(())
1105 }
1106
1107 pub async fn broadcast_typed(
1110 &self,
1111 namespace: &str,
1112 msg_type: &str,
1113 payload: &serde_json::Value,
1114 ) {
1115 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
1116 tracing::debug!("node: broadcast after stop ignored");
1117 return;
1118 }
1119 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
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 #[deprecated(
1138 since = "0.7.0",
1139 note = "wire type depends on data contents and failures are hidden; \
1140 use broadcast_json or broadcast_bytes"
1141 )]
1142 pub async fn broadcast(&self, namespace: &str, data: &[u8]) {
1143 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
1144 tracing::debug!("node: broadcast after stop ignored");
1145 return;
1146 }
1147 let payload = std::str::from_utf8(data)
1149 .ok()
1150 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
1151 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
1152
1153 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
1154
1155 match self.codec.encode(&envelope) {
1156 Ok(encoded) => {
1157 self.session.broadcast(&encoded).await;
1158 }
1159 Err(e) => {
1160 tracing::error!("node: failed to encode broadcast envelope: {e}");
1161 }
1162 }
1163 }
1164
1165 pub async fn broadcast_json(
1172 &self,
1173 namespace: &str,
1174 payload: &serde_json::Value,
1175 ) -> Result<crate::session::BroadcastReport, NodeError> {
1176 self.broadcast_reported(namespace, "message", payload).await
1177 }
1178
1179 pub async fn broadcast_bytes(
1184 &self,
1185 namespace: &str,
1186 data: &[u8],
1187 ) -> Result<crate::session::BroadcastReport, NodeError> {
1188 let payload = Self::bytes_payload(data);
1189 self.broadcast_reported(namespace, "bytes", &payload).await
1190 }
1191
1192 async fn broadcast_reported(
1196 &self,
1197 namespace: &str,
1198 msg_type: &str,
1199 payload: &serde_json::Value,
1200 ) -> Result<crate::session::BroadcastReport, NodeError> {
1201 self.ensure_not_stopped()?;
1202 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
1203 let encoded = self.codec.encode(&envelope)?;
1204 Ok(self.session.broadcast(&encoded).await)
1205 }
1206
1207 pub fn subscribe(&self, namespace: &str) -> broadcast::Receiver<NamespacedMessage> {
1213 {
1217 let filters = self
1218 .namespace_filters
1219 .read()
1220 .unwrap_or_else(std::sync::PoisonError::into_inner);
1221 if let Some(tx) = filters.get(namespace) {
1222 return tx.subscribe();
1223 }
1224 }
1225
1226 let mut filters = self
1228 .namespace_filters
1229 .write()
1230 .unwrap_or_else(std::sync::PoisonError::into_inner);
1231 if let Some(tx) = filters.get(namespace) {
1233 return tx.subscribe();
1234 }
1235 filters.retain(|_, tx| tx.receiver_count() > 0);
1239 let (tx, rx) = broadcast::channel(256);
1240 filters.insert(namespace.to_string(), tx);
1241 rx
1242 }
1243
1244 pub async fn open_tcp(&self, peer_id: &str, port: u16) -> Result<TcpStream, NodeError> {
1256 self.ensure_not_stopped()?;
1257 let peer = self.resolve_peer(peer_id).await?;
1258 let addr = peer.ip.to_string();
1259 self.network
1260 .dial_tcp_opts(&addr, port, DialOpts { tls: Some(false) })
1261 .await
1262 .map_err(|e| NodeError::ConnectionFailed(e.to_string()))
1263 }
1264
1265 pub async fn listen_tcp(&self, port: u16) -> Result<RawListener, NodeError> {
1273 self.listen_tcp_opts(port, ListenOpts::default()).await
1274 }
1275
1276 pub async fn listen_tcp_opts(
1282 &self,
1283 port: u16,
1284 opts: ListenOpts,
1285 ) -> Result<RawListener, NodeError> {
1286 self.ensure_not_stopped()?;
1287 use crate::transport::tcp::TcpTransport;
1288
1289 ensure_port_unreserved(port, self.ws_port)?;
1290 let tcp = TcpTransport::new(self.network.clone());
1291 tcp.listen_opts(port, opts).await.map_err(|e| {
1292 if port == 443 {
1296 NodeError::ConnectionFailed(format!(
1297 "{e} (port 443 requires a sidecar built with RFC 023 — older sidecars bind \
1298 a legacy TLS listener on 443; upgrade the sidecar binary)"
1299 ))
1300 } else {
1301 NodeError::Transport(e)
1302 }
1303 })
1304 }
1305
1306 pub async fn unlisten_tcp(&self, port: u16) -> Result<(), NodeError> {
1311 self.ensure_not_stopped()?;
1312 self.network
1313 .unlisten_tcp(port)
1314 .await
1315 .map_err(NodeError::Network)
1316 }
1317
1318 pub async fn connect_quic(
1327 &self,
1328 peer_id: &str,
1329 port: u16,
1330 ) -> Result<QuicConnection, NodeError> {
1331 self.ensure_not_stopped()?;
1332 let peer = self.resolve_peer(peer_id).await?;
1333 let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
1334 crate::transport::quic::connect_raw(&self.network, &peer.ip.to_string(), port, &alpn)
1335 .await
1336 .map_err(NodeError::Transport)
1337 }
1338
1339 pub async fn listen_quic(&self, port: u16) -> Result<QuicListener, NodeError> {
1347 self.ensure_not_stopped()?;
1348 ensure_port_unreserved(port, self.ws_port)?;
1349 if port == 0 {
1350 return Err(NodeError::NotImplemented(
1351 "ephemeral (port 0) QUIC listeners are not supported over the tsnet relay yet — choose an explicit port"
1352 .to_string(),
1353 ));
1354 }
1355 let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
1356 crate::transport::quic::listen_raw(&self.network, port, &alpn)
1357 .await
1358 .map_err(NodeError::Transport)
1359 }
1360
1361 pub async fn bind_udp(&self, port: u16) -> Result<DatagramSocket, NodeError> {
1371 self.ensure_not_stopped()?;
1372 use crate::transport::udp::{UdpConfig, UdpTransport};
1373 use crate::transport::DatagramTransport;
1374
1375 let udp = UdpTransport::new(self.network.clone(), UdpConfig::default());
1376 udp.bind(port).await.map_err(NodeError::Transport)
1377 }
1378}
1379
1380impl<N: NetworkProvider + 'static> Drop for Node<N> {
1381 fn drop(&mut self) {
1382 self.tasks.cancel.cancel();
1386 }
1387}
1388
1389fn hostname_slug<'a>(hostname: &'a str, app_id: &str) -> Option<&'a str> {
1392 hostname
1393 .strip_prefix("truffle-")?
1394 .strip_prefix(app_id)?
1395 .strip_prefix('-')
1396}
1397
1398fn validate_hostname_label(s: &str) -> Result<(), String> {
1402 let ok_len = (1..=63).contains(&s.len());
1403 let ok_chars = s
1404 .chars()
1405 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
1406 let ok_edges = !s.starts_with('-') && !s.ends_with('-');
1407 if ok_len && ok_chars && ok_edges {
1408 Ok(())
1409 } else {
1410 Err(format!(
1411 "invalid hostname {s:?}: must be a single lowercase DNS label \
1412 (1-63 chars of [a-z0-9-], no leading/trailing hyphen, no dots)"
1413 ))
1414 }
1415}
1416
1417pub(crate) fn ensure_port_unreserved(port: u16, ws_port: u16) -> Result<(), NodeError> {
1422 if port == ws_port {
1423 Err(NodeError::ReservedPort(port))
1424 } else {
1425 Ok(())
1426 }
1427}
1428
1429#[derive(Clone)]
1450pub struct NodeBuilder {
1451 app_id: Option<AppId>,
1452 device_name: Option<DeviceName>,
1453 device_id: Option<DeviceId>,
1454 hostname: Option<String>,
1457 sidecar_path: Option<PathBuf>,
1458 state_dir: Option<String>,
1459 auth_key: Option<String>,
1460 ephemeral: bool,
1461 ws_port: u16,
1462 idle_timeout_secs: Option<u64>,
1463 eager_identity: bool,
1465}
1466
1467impl std::fmt::Debug for NodeBuilder {
1470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1471 f.debug_struct("NodeBuilder")
1472 .field("app_id", &self.app_id)
1473 .field("device_name", &self.device_name)
1474 .field("device_id", &self.device_id)
1475 .field("hostname", &self.hostname)
1476 .field("sidecar_path", &self.sidecar_path)
1477 .field("state_dir", &self.state_dir)
1478 .field("auth_key", &self.auth_key.as_ref().map(|_| "[REDACTED]"))
1479 .field("ephemeral", &self.ephemeral)
1480 .field("ws_port", &self.ws_port)
1481 .field("idle_timeout_secs", &self.idle_timeout_secs)
1482 .field("eager_identity", &self.eager_identity)
1483 .finish()
1484 }
1485}
1486
1487fn atomic_write_string(path: &Path, content: &str) -> std::io::Result<()> {
1500 let parent = path.parent().ok_or_else(|| {
1501 std::io::Error::new(
1502 std::io::ErrorKind::InvalidInput,
1503 "path has no parent directory",
1504 )
1505 })?;
1506 let mut tmp = path.to_path_buf();
1507 tmp.set_extension("tmp");
1508 {
1509 use std::io::Write as _;
1510 let mut f = std::fs::File::create(&tmp)?;
1511 f.write_all(content.as_bytes())?;
1512 f.sync_all()?;
1513 }
1514 #[cfg(unix)]
1515 {
1516 std::fs::rename(&tmp, path)?;
1517 if let Ok(dir) = std::fs::File::open(parent) {
1521 let _ = dir.sync_all();
1522 }
1523 }
1524 #[cfg(windows)]
1525 {
1526 let _ = parent; replace_file_windows(&tmp, path)?;
1528 }
1529 Ok(())
1530}
1531
1532#[cfg(windows)]
1538fn replace_file_windows(tmp: &Path, dest: &Path) -> std::io::Result<()> {
1539 use std::os::windows::ffi::OsStrExt;
1540
1541 const MOVEFILE_REPLACE_EXISTING: u32 = 0x1;
1542 const MOVEFILE_WRITE_THROUGH: u32 = 0x8;
1543
1544 #[link(name = "kernel32")]
1545 extern "system" {
1546 fn MoveFileExW(from: *const u16, to: *const u16, flags: u32) -> i32;
1547 }
1548
1549 fn wide(p: &Path) -> Vec<u16> {
1550 p.as_os_str()
1551 .encode_wide()
1552 .chain(std::iter::once(0))
1553 .collect()
1554 }
1555
1556 let (from, to) = (wide(tmp), wide(dest));
1557 let ok = unsafe {
1558 MoveFileExW(
1559 from.as_ptr(),
1560 to.as_ptr(),
1561 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
1562 )
1563 };
1564 if ok == 0 {
1565 return Err(std::io::Error::last_os_error());
1566 }
1567 Ok(())
1568}
1569
1570impl Default for NodeBuilder {
1571 fn default() -> Self {
1572 Self {
1573 app_id: None,
1574 device_name: None,
1575 device_id: None,
1576 sidecar_path: None,
1577 state_dir: None,
1578 auth_key: None,
1579 hostname: None,
1580 ephemeral: false,
1581 ws_port: 9417,
1582 idle_timeout_secs: None,
1583 eager_identity: true,
1584 }
1585 }
1586}
1587
1588impl NodeBuilder {
1589 pub fn app_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1594 let raw: String = s.into();
1595 let app_id = AppId::parse(&raw)
1596 .map_err(|e| NodeError::BuildError(format!("invalid app_id: {e}")))?;
1597 self.app_id = Some(app_id);
1598 Ok(self)
1599 }
1600
1601 pub fn device_name(mut self, s: impl Into<String>) -> Self {
1606 self.device_name = Some(DeviceName::parse(s));
1607 self
1608 }
1609
1610 pub fn hostname(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1624 let raw: String = s.into();
1625 validate_hostname_label(&raw).map_err(NodeError::BuildError)?;
1626 self.hostname = Some(raw);
1627 Ok(self)
1628 }
1629
1630 pub fn device_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1636 let raw: String = s.into();
1637 let device_id = DeviceId::parse(&raw)
1638 .map_err(|e| NodeError::BuildError(format!("invalid device_id: {e}")))?;
1639 self.device_id = Some(device_id);
1640 Ok(self)
1641 }
1642
1643 pub fn sidecar_path(mut self, path: impl Into<PathBuf>) -> Self {
1645 self.sidecar_path = Some(path.into());
1646 self
1647 }
1648
1649 pub fn state_dir(mut self, dir: &str) -> Self {
1651 self.state_dir = Some(dir.to_string());
1652 self
1653 }
1654
1655 pub fn auth_key(mut self, key: &str) -> Self {
1657 self.auth_key = Some(key.to_string());
1658 self
1659 }
1660
1661 pub fn ephemeral(mut self, val: bool) -> Self {
1663 self.ephemeral = val;
1664 self
1665 }
1666
1667 pub fn eager_identity(mut self, enabled: bool) -> Self {
1671 self.eager_identity = enabled;
1672 self
1673 }
1674
1675 pub fn ws_port(mut self, port: u16) -> Self {
1676 self.ws_port = port;
1677 self
1678 }
1679
1680 pub fn idle_timeout_secs(mut self, secs: u64) -> Self {
1686 self.idle_timeout_secs = Some(secs);
1687 self
1688 }
1689
1690 fn prepare_config(&self) -> Result<TailscaleConfig, NodeError> {
1697 let binary_path = self
1699 .sidecar_path
1700 .clone()
1701 .ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
1702
1703 let app_id = self
1705 .app_id
1706 .clone()
1707 .ok_or_else(|| NodeError::BuildError("app_id is required".into()))?;
1708
1709 let device_name = match self.device_name.clone() {
1711 Some(name) => name,
1712 None => {
1713 let os_hostname = hostname::get()
1714 .map_err(|e| {
1715 NodeError::BuildError(format!(
1716 "device_name is unset and hostname::get() failed: {e}"
1717 ))
1718 })?
1719 .to_string_lossy()
1720 .into_owned();
1721 DeviceName::parse(os_hostname)
1722 }
1723 };
1724
1725 let tailscale_host = match self.hostname.clone() {
1731 Some(hostname) => hostname,
1732 None => identity::tailscale_hostname(&app_id, &device_name),
1733 };
1734
1735 let state_dir = match self.state_dir.clone() {
1741 Some(dir) => dir,
1742 None => {
1743 let base = dirs::data_dir().ok_or_else(|| {
1744 NodeError::BuildError(
1745 "no platform data directory available (dirs::data_dir() returned None); \
1746 set state_dir explicitly to a durable location"
1747 .into(),
1748 )
1749 })?;
1750 base.join("truffle")
1751 .join(app_id.as_str())
1752 .join(identity::slug(device_name.as_str(), 255))
1753 .to_string_lossy()
1754 .into_owned()
1755 }
1756 };
1757
1758 std::fs::create_dir_all(&state_dir)?;
1760
1761 let device_id_file = Path::new(&state_dir).join("device-id.txt");
1766 let device_id = match self.device_id.clone() {
1767 Some(id) => {
1768 atomic_write_string(&device_id_file, id.as_str())?;
1770 id
1771 }
1772 None => {
1773 if device_id_file.exists() {
1774 let s = std::fs::read_to_string(&device_id_file)?.trim().to_string();
1775 DeviceId::parse(&s).map_err(|e| {
1776 NodeError::BuildError(format!(
1777 "device-id.txt at {device_id_file:?} contains an invalid ULID: {e}"
1778 ))
1779 })?
1780 } else {
1781 let id = DeviceId::generate();
1782 atomic_write_string(&device_id_file, id.as_str())?;
1783 id
1784 }
1785 }
1786 };
1787
1788 Ok(TailscaleConfig {
1789 binary_path,
1790 app_id: app_id.as_str().to_string(),
1791 device_id: device_id.as_str().to_string(),
1792 device_name: device_name.as_str().to_string(),
1793 hostname: tailscale_host,
1794 state_dir,
1795 auth_key: self.auth_key.clone(),
1796 ephemeral: if self.ephemeral { Some(true) } else { None },
1797 tags: None,
1798 idle_timeout_secs: self.idle_timeout_secs,
1799 })
1800 }
1801
1802 pub async fn build(self) -> Result<Node<TailscaleProvider>, NodeError> {
1813 let ws_port = self.ws_port;
1814 let eager_identity = self.eager_identity;
1815 let config = self.prepare_config()?;
1816 let state_dir = PathBuf::from(&config.state_dir);
1817
1818 let mut provider = TailscaleProvider::new(config);
1819 provider.start().await.map_err(NodeError::Network)?;
1820
1821 let network = Arc::new(provider);
1822
1823 let ws_config = WsConfig {
1825 port: ws_port,
1826 ..Default::default()
1827 };
1828 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1829
1830 let session = Arc::new(PeerRegistry::with_options(
1832 network.clone(),
1833 ws_transport,
1834 crate::session::PeerRegistryOptions {
1835 eager_identity,
1836 ..Default::default()
1837 },
1838 ));
1839 session.start().await;
1840
1841 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1843 let node = Node::from_parts(network, session, codec)
1844 .with_state_dir(state_dir)
1845 .with_ws_port(ws_port);
1846
1847 tracing::info!("node: started successfully");
1848 Ok(node)
1849 }
1850
1851 pub async fn build_with_auth_handler(
1863 self,
1864 on_auth: impl Fn(String) + Send + 'static,
1865 ) -> Result<Node<TailscaleProvider>, NodeError> {
1866 let ws_port = self.ws_port;
1867 let eager_identity = self.eager_identity;
1868 let config = self.prepare_config()?;
1869 let state_dir = PathBuf::from(&config.state_dir);
1870
1871 let mut provider = TailscaleProvider::new(config);
1872
1873 let mut auth_rx = provider.peer_events();
1875
1876 let auth_task = tokio::spawn(async move {
1878 use crate::network::NetworkPeerEvent;
1879 loop {
1880 match auth_rx.recv().await {
1881 Ok(NetworkPeerEvent::AuthRequired { url }) => {
1882 on_auth(url);
1883 }
1884 Err(broadcast::error::RecvError::Closed) => break,
1885 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1886 _ => {} }
1888 }
1889 });
1890
1891 let start_result = provider.start().await.map_err(NodeError::Network);
1893
1894 auth_task.abort();
1896
1897 start_result?;
1898
1899 let network = Arc::new(provider);
1900
1901 let ws_config = WsConfig {
1903 port: ws_port,
1904 ..Default::default()
1905 };
1906 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1907
1908 let session = Arc::new(PeerRegistry::with_options(
1910 network.clone(),
1911 ws_transport,
1912 crate::session::PeerRegistryOptions {
1913 eager_identity,
1914 ..Default::default()
1915 },
1916 ));
1917 session.start().await;
1918
1919 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1921 let node = Node::from_parts(network, session, codec)
1922 .with_state_dir(state_dir)
1923 .with_ws_port(ws_port);
1924
1925 tracing::info!("node: started successfully (with auth handler)");
1926 Ok(node)
1927 }
1928}
1929
1930#[cfg(test)]
1935mod tests {
1936 use super::*;
1937 use crate::network::{
1938 HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
1939 NetworkTcpListener, NetworkUdpSocket, PeerAddr,
1940 };
1941 use crate::transport::WsConfig;
1942 use serde_json::json;
1943 use std::sync::atomic::{AtomicUsize, Ordering};
1944 use std::time::Duration;
1945 use tokio::sync::{broadcast, mpsc};
1946
1947 #[test]
1948 fn node_builder_debug_redacts_auth_key() {
1949 let builder = NodeBuilder::default().auth_key("dummy-auth-SECRET123");
1950 let dbg = format!("{builder:?}");
1951 assert!(!dbg.contains("SECRET123"));
1952 assert!(dbg.contains("[REDACTED]"));
1953 }
1954
1955 #[test]
1956 fn atomic_write_string_creates_and_replaces() {
1957 let dir =
1958 std::env::temp_dir().join(format!("truffle-atomic-write-test-{}", std::process::id()));
1959 std::fs::create_dir_all(&dir).unwrap();
1960 let path = dir.join("device-id.txt");
1961
1962 atomic_write_string(&path, "01AAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
1963 assert_eq!(
1964 std::fs::read_to_string(&path).unwrap(),
1965 "01AAAAAAAAAAAAAAAAAAAAAAAA"
1966 );
1967
1968 atomic_write_string(&path, "01BBBBBBBBBBBBBBBBBBBBBBBB").unwrap();
1971 assert_eq!(
1972 std::fs::read_to_string(&path).unwrap(),
1973 "01BBBBBBBBBBBBBBBBBBBBBBBB"
1974 );
1975
1976 std::fs::remove_dir_all(&dir).ok();
1977 }
1978
1979 struct MockNetworkProvider {
1982 identity: NodeIdentity,
1983 local_addr: PeerAddr,
1984 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
1985 mock_peers: Arc<RwLock<Vec<NetworkPeer>>>,
1987 stop_calls: Arc<AtomicUsize>,
1990 }
1991
1992 impl MockNetworkProvider {
1993 fn new(id: &str) -> Self {
1994 Self::new_with_device_id(id, id)
1997 }
1998
1999 fn new_with_device_id(id: &str, device_id: &str) -> Self {
2004 let (peer_event_tx, _) = broadcast::channel(64);
2005 Self {
2006 identity: NodeIdentity {
2007 app_id: "test".to_string(),
2008 device_id: device_id.to_string(),
2009 device_name: format!("Test Node {id}"),
2010 tailscale_hostname: format!("truffle-test-{id}"),
2011 tailscale_id: id.to_string(),
2012 dns_name: None,
2013 ip: Some("127.0.0.1".parse().unwrap()),
2014 },
2015 local_addr: PeerAddr {
2016 ip: Some("127.0.0.1".parse().unwrap()),
2017 hostname: format!("truffle-test-{id}"),
2018 dns_name: None,
2019 },
2020 peer_event_tx,
2021 mock_peers: Arc::new(RwLock::new(Vec::new())),
2022 stop_calls: Arc::new(AtomicUsize::new(0)),
2023 }
2024 }
2025
2026 fn event_sender(&self) -> broadcast::Sender<NetworkPeerEvent> {
2027 self.peer_event_tx.clone()
2028 }
2029
2030 fn stop_call_count(&self) -> usize {
2032 self.stop_calls.load(Ordering::SeqCst)
2033 }
2034 }
2035
2036 impl NetworkProvider for MockNetworkProvider {
2037 async fn start(&mut self) -> Result<(), NetworkError> {
2038 Ok(())
2039 }
2040
2041 async fn stop(&self) -> Result<(), NetworkError> {
2042 self.stop_calls.fetch_add(1, Ordering::SeqCst);
2043 Ok(())
2044 }
2045
2046 fn local_identity(&self) -> NodeIdentity {
2047 self.identity.clone()
2048 }
2049
2050 fn local_addr(&self) -> PeerAddr {
2051 self.local_addr.clone()
2052 }
2053
2054 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
2055 self.peer_event_tx.subscribe()
2056 }
2057
2058 async fn peers(&self) -> Vec<NetworkPeer> {
2059 self.mock_peers.read().await.clone()
2060 }
2061
2062 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
2063 let target = format!("{addr}:{port}");
2064 TcpStream::connect(&target)
2065 .await
2066 .map_err(|e| NetworkError::DialFailed(format!("mock dial {target}: {e}")))
2067 }
2068
2069 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
2070 let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
2071 .await
2072 .map_err(|e| NetworkError::ListenFailed(format!("mock listen :{port}: {e}")))?;
2073
2074 let actual_port = listener.local_addr().unwrap().port();
2075 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
2076
2077 tokio::spawn(async move {
2078 loop {
2079 match listener.accept().await {
2080 Ok((stream, addr)) => {
2081 let conn = IncomingConnection {
2082 stream,
2083 remote_addr: addr.to_string(),
2084 remote_identity: String::new(),
2085 port: actual_port,
2086 };
2087 if tx.send(conn).await.is_err() {
2088 break;
2089 }
2090 }
2091 Err(e) => {
2092 tracing::debug!("mock listener error: {e}");
2093 break;
2094 }
2095 }
2096 }
2097 });
2098
2099 Ok(NetworkTcpListener {
2100 port: actual_port,
2101 incoming: rx,
2102 })
2103 }
2104
2105 async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
2106 Ok(())
2107 }
2108
2109 async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
2110 Err(NetworkError::Internal("mock: UDP not supported".into()))
2111 }
2112
2113 async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
2114 Ok(PingResult {
2115 latency: Duration::from_millis(1),
2116 connection: "direct".to_string(),
2117 peer_addr: None,
2118 })
2119 }
2120
2121 async fn health(&self) -> HealthInfo {
2122 HealthInfo {
2123 state: "running".to_string(),
2124 healthy: true,
2125 ..Default::default()
2126 }
2127 }
2128 }
2129
2130 fn make_loopback_peer(id: &str) -> NetworkPeer {
2133 NetworkPeer {
2134 id: id.to_string(),
2135 hostname: format!("truffle-test-{id}"),
2136 ip: "127.0.0.1".parse().unwrap(),
2137 online: true,
2138 cur_addr: Some("127.0.0.1:41641".to_string()),
2139 relay: None,
2140 os: Some("linux".to_string()),
2141 last_seen: Some("2026-03-25T12:00:00Z".to_string()),
2142 key_expiry: None,
2143 dns_name: None,
2144 }
2145 }
2146
2147 fn ws_config(port: u16) -> WsConfig {
2148 WsConfig {
2149 port,
2150 ping_interval: Duration::from_secs(300),
2151 pong_timeout: Duration::from_secs(300),
2152 ..Default::default()
2153 }
2154 }
2155
2156 async fn random_port() -> u16 {
2157 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2158 l.local_addr().unwrap().port()
2159 }
2160
2161 async fn make_test_node(
2163 id: &str,
2164 ws_port: u16,
2165 ) -> (
2166 Node<MockNetworkProvider>,
2167 broadcast::Sender<NetworkPeerEvent>,
2168 Arc<MockNetworkProvider>,
2169 ) {
2170 make_test_node_with_device_id(id, id, ws_port).await
2171 }
2172
2173 async fn make_test_node_with_device_id(
2176 id: &str,
2177 device_id: &str,
2178 ws_port: u16,
2179 ) -> (
2180 Node<MockNetworkProvider>,
2181 broadcast::Sender<NetworkPeerEvent>,
2182 Arc<MockNetworkProvider>,
2183 ) {
2184 let provider = MockNetworkProvider::new_with_device_id(id, device_id);
2185 let event_tx = provider.event_sender();
2186 let network = Arc::new(provider);
2187 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config(ws_port)));
2188 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
2189 session.start().await;
2190
2191 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
2192 let node = Node::from_parts(network.clone(), session, codec);
2193
2194 (node, event_tx, network)
2195 }
2196
2197 #[tokio::test]
2205 async fn offer_flood_respects_caps_and_stop_drains() {
2206 let ws_port = random_port().await;
2207 let (node, _event_tx, _network) = make_test_node("node-flood", ws_port).await;
2208 let node = Arc::new(node);
2209 let mut offers = node.file_transfer().offer_channel(node.clone()).await;
2210
2211 for i in 0..500u32 {
2212 let payload = json!({
2213 "type": "offer",
2214 "file_name": format!("f{i}.bin"),
2215 "size": 1,
2216 "sha256": "0".repeat(64),
2217 "save_path": "",
2218 "token": format!("tok-{i}"),
2219 "tcp_port": 0,
2220 });
2221 let envelope = Envelope::new("ft", "offer", payload).with_timestamp();
2222 node.session
2223 .test_inject_incoming("peer-flood", JsonCodec.encode(&envelope).unwrap());
2224 if i % 64 == 0 {
2225 tokio::task::yield_now().await;
2226 }
2227 }
2228 tokio::time::sleep(Duration::from_millis(300)).await;
2229
2230 let pending = node
2231 .file_transfer_state
2232 .pending_offers_per_peer
2233 .lock()
2234 .unwrap()
2235 .get("peer-flood")
2236 .copied()
2237 .unwrap_or(0);
2238 assert!(
2239 pending <= crate::file_transfer::MAX_PENDING_OFFERS_PER_PEER,
2240 "per-peer pending cap violated: {pending}"
2241 );
2242
2243 let mut buffered = 0;
2245 while offers.try_recv().is_ok() {
2246 buffered += 1;
2247 }
2248 assert!(buffered <= 36, "app offer queue grew to {buffered}");
2249
2250 let started = std::time::Instant::now();
2253 node.stop().await;
2254 assert!(
2255 started.elapsed() < Duration::from_secs(5),
2256 "stop() took {:?} with parked offers",
2257 started.elapsed()
2258 );
2259 assert!(node.tasks.tracker.is_empty());
2260 }
2261
2262 #[tokio::test]
2264 async fn dynamic_namespace_churn_stays_bounded() {
2265 let ws_port = random_port().await;
2266 let (node, _event_tx, _network) = make_test_node("node-ns-churn", ws_port).await;
2267 for i in 0..1000 {
2268 let rx = node.subscribe(&format!("dyn-{i}"));
2269 drop(rx);
2270 }
2271 let len = node
2272 .namespace_filters
2273 .read()
2274 .unwrap_or_else(std::sync::PoisonError::into_inner)
2275 .len();
2276 assert!(len <= 1, "namespace map grew to {len} entries");
2277 }
2278
2279 #[tokio::test]
2281 async fn lifecycle_churn_leaves_no_tasks() {
2282 for _ in 0..25 {
2283 let ws_port = random_port().await;
2284 let (node, _event_tx, _network) = make_test_node("node-churn", ws_port).await;
2285 let node = Arc::new(node);
2286 let _rx = node.subscribe("churn");
2287 let _offers = node.file_transfer().offer_channel(node.clone()).await;
2288 node.stop().await;
2289 assert!(node.tasks.tracker.is_empty(), "task leak after stop()");
2290 }
2291 }
2292
2293 #[tokio::test]
2296 async fn slow_subscriber_lags_instead_of_growing() {
2297 let ws_port = random_port().await;
2298 let (node, _event_tx, _network) = make_test_node("node-lag", ws_port).await;
2299 let mut rx = node.subscribe("chat");
2300
2301 for i in 0..2000 {
2302 let envelope = Envelope::new("chat", "message", json!({ "i": i })).with_timestamp();
2303 node.session
2304 .test_inject_incoming("peer-x", JsonCodec.encode(&envelope).unwrap());
2305 if i % 100 == 0 {
2306 tokio::task::yield_now().await;
2307 }
2308 }
2309 tokio::time::sleep(Duration::from_millis(300)).await;
2310
2311 let mut lagged = false;
2312 loop {
2313 match rx.try_recv() {
2314 Ok(_) => {}
2315 Err(broadcast::error::TryRecvError::Lagged(_)) => lagged = true,
2316 Err(_) => break,
2317 }
2318 }
2319 assert!(lagged, "slow subscriber should observe Lagged");
2320 }
2321
2322 #[tokio::test]
2323 async fn stop_drains_all_background_tasks() {
2324 let ws_port = random_port().await;
2325 let (node, _event_tx, _network) = make_test_node("node-drain", ws_port).await;
2326 let node = Arc::new(node);
2327
2328 let _offers = node.file_transfer().offer_channel(node.clone()).await;
2330 assert!(!node.tasks.tracker.is_empty(), "tasks should be running");
2331
2332 node.stop().await;
2333
2334 assert!(
2336 node.tasks.tracker.is_empty(),
2337 "background tasks still alive after stop()"
2338 );
2339 assert!(
2340 node.file_transfer_state
2341 .receiver_handle
2342 .lock()
2343 .await
2344 .is_none(),
2345 "FT receiver handle not cleared by stop()"
2346 );
2347 }
2348
2349 #[tokio::test]
2350 async fn stopped_node_fails_raw_and_resolution_apis_fast() {
2351 let ws_port = random_port().await;
2352 let (node, _event_tx, _network) = make_test_node("node-guards", ws_port).await;
2353 node.stop().await;
2354
2355 assert!(matches!(
2356 node.open_tcp("nobody", 4242).await.unwrap_err(),
2357 NodeError::Stopped
2358 ));
2359 assert!(matches!(
2360 node.listen_tcp(4242).await.unwrap_err(),
2361 NodeError::Stopped
2362 ));
2363 assert!(matches!(
2364 node.connect_quic("nobody", 4242).await.unwrap_err(),
2365 NodeError::Stopped
2366 ));
2367 assert!(matches!(
2368 node.listen_quic(4242).await.unwrap_err(),
2369 NodeError::Stopped
2370 ));
2371 assert!(matches!(
2372 node.bind_udp(4242).await.err(),
2373 Some(NodeError::Stopped)
2374 ));
2375 assert!(matches!(
2376 node.ping("nobody").await.unwrap_err(),
2377 NodeError::Stopped
2378 ));
2379 assert!(matches!(
2380 node.resolve_peer_id("nobody").await.unwrap_err(),
2381 NodeError::Stopped
2382 ));
2383 }
2384
2385 #[test]
2386 fn bytes_payload_roundtrip() {
2387 let data = vec![0u8, 159, 146, 150, 255];
2390 let payload = Node::<MockNetworkProvider>::bytes_payload(&data);
2391 assert_eq!(payload["encoding"], "base64");
2392
2393 let msg = NamespacedMessage {
2394 from: "p".into(),
2395 namespace: "ns".into(),
2396 msg_type: "bytes".into(),
2397 payload,
2398 timestamp: None,
2399 };
2400 assert_eq!(msg.payload_bytes().unwrap(), data);
2401 }
2402
2403 #[test]
2404 fn payload_bytes_rejects_other_msg_types() {
2405 let msg = NamespacedMessage {
2406 from: "p".into(),
2407 namespace: "ns".into(),
2408 msg_type: "message".into(),
2409 payload: json!({"data": "aGk="}),
2410 timestamp: None,
2411 };
2412 assert!(msg.payload_bytes().is_none());
2413 }
2414
2415 #[tokio::test]
2416 async fn broadcast_json_reports_zero_peers() {
2417 let ws_port = random_port().await;
2418 let (node, _event_tx, _network) = make_test_node("node-bj", ws_port).await;
2419
2420 let report = node.broadcast_json("ns", &json!({"a": 1})).await.unwrap();
2421 assert_eq!(report.attempted, 0);
2422 assert_eq!(report.queued, 0);
2423 assert!(report.failed.is_empty());
2424 }
2425
2426 #[tokio::test]
2427 async fn subscribe_slow_path_sweeps_dead_namespaces() {
2428 let ws_port = random_port().await;
2429 let (node, _event_tx, _network) = make_test_node("node-sweep", ws_port).await;
2430
2431 let rx = node.subscribe("dyn-a");
2432 drop(rx);
2433 let _rx_b = node.subscribe("dyn-b");
2436
2437 let filters = node.namespace_filters.read().unwrap();
2438 assert!(!filters.contains_key("dyn-a"), "dead entry not swept");
2439 assert!(filters.contains_key("dyn-b"));
2440 }
2441
2442 #[tokio::test]
2443 async fn router_prunes_namespace_after_receivers_drop() {
2444 let ws_port = random_port().await;
2445 let (node, _event_tx, _network) = make_test_node("node-prune", ws_port).await;
2446
2447 let rx = node.subscribe("dyn-c");
2448 drop(rx);
2449
2450 let envelope = Envelope::new("dyn-c", "message", json!({"x": 1})).with_timestamp();
2452 let data = JsonCodec.encode(&envelope).unwrap();
2453 node.session.test_inject_incoming("peer-x", data);
2454
2455 for _ in 0..50 {
2457 {
2458 let filters = node.namespace_filters.read().unwrap();
2459 if !filters.contains_key("dyn-c") {
2460 return;
2461 }
2462 }
2463 tokio::time::sleep(Duration::from_millis(10)).await;
2464 }
2465 panic!("dead namespace entry was not pruned by the router");
2466 }
2467
2468 #[tokio::test]
2469 async fn test_node_builder_creates_node() {
2470 let ws_port = random_port().await;
2471 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2472
2473 let identity = node.local_info();
2474 assert_eq!(identity.tailscale_id, "node-1");
2475 assert_eq!(identity.device_id, "node-1");
2476 assert!(identity.tailscale_hostname.contains("node-1"));
2477 }
2478
2479 #[tokio::test]
2480 async fn test_node_peers_from_network() {
2481 let ws_port = random_port().await;
2482 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
2483
2484 let peers = node.peers().await;
2486 assert!(peers.is_empty());
2487
2488 let peer = make_loopback_peer("peer-a");
2490 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2491 tokio::time::sleep(Duration::from_millis(50)).await;
2492
2493 let peers = node.peers().await;
2494 assert_eq!(peers.len(), 1);
2495 assert_eq!(peers[0].tailscale_id, "peer-a");
2496 assert!(
2497 peers[0].device_id.is_none(),
2498 "RFC 022: no ULID before identity"
2499 );
2500 assert!(peers[0].online);
2501 assert!(!peers[0].ws_connected);
2502 }
2503
2504 #[tokio::test]
2505 #[allow(deprecated)] async fn test_node_send_to_unknown_peer_errors() {
2507 let ws_port = random_port().await;
2508 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2509
2510 let result = node.send("nonexistent", "test", b"hello").await;
2511 assert!(result.is_err());
2512 let err_str = result.unwrap_err().to_string();
2513 assert!(
2514 err_str.contains("unknown peer") || err_str.contains("not found"),
2515 "expected unknown peer error, got: {err_str}"
2516 );
2517 }
2518
2519 #[tokio::test]
2520 async fn test_node_send_wraps_in_envelope() {
2521 let codec = JsonCodec;
2524 let data = b"hello world";
2525 let envelope = Envelope::new("test-ns", "message", serde_json::Value::from(data.to_vec()))
2526 .with_timestamp();
2527
2528 let encoded = codec.encode(&envelope).unwrap();
2529 let decoded = codec.decode(&encoded).unwrap();
2530
2531 assert_eq!(decoded.namespace, "test-ns");
2532 assert_eq!(decoded.msg_type, "message");
2533 assert!(decoded.timestamp.is_some());
2534 }
2535
2536 #[tokio::test]
2537 async fn test_node_subscribe_filters_by_namespace() {
2538 let ws_port = random_port().await;
2539 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2540
2541 let _rx_chat = node.subscribe("chat");
2543 let _rx_ft = node.subscribe("ft");
2544
2545 let _rx_chat2 = node.subscribe("chat");
2547 }
2548
2549 #[tokio::test]
2550 #[allow(deprecated)] async fn test_node_broadcast() {
2552 let ws_port = random_port().await;
2553 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2554
2555 node.broadcast("test", b"hello everyone").await;
2557 }
2558
2559 #[tokio::test]
2560 async fn test_node_open_tcp_resolves_peer() {
2561 let ws_port = random_port().await;
2562 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
2563
2564 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2566 let tcp_port = listener.local_addr().unwrap().port();
2567
2568 let peer = make_loopback_peer("peer-tcp");
2570 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2571 tokio::time::sleep(Duration::from_millis(50)).await;
2572
2573 let accept_handle = tokio::spawn(async move {
2575 let (stream, _) = listener.accept().await.unwrap();
2576 stream
2577 });
2578
2579 let stream = node.open_tcp("peer-tcp", tcp_port).await;
2581 assert!(stream.is_ok(), "open_tcp failed: {:?}", stream.err());
2582
2583 let _ = accept_handle.await;
2584 }
2585
2586 #[tokio::test]
2587 async fn test_node_open_tcp_unknown_peer_errors() {
2588 let ws_port = random_port().await;
2589 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2590
2591 let result = node.open_tcp("nonexistent", 8080).await;
2592 assert!(result.is_err());
2593 let err_str = result.unwrap_err().to_string();
2594 assert!(
2595 err_str.contains("not found"),
2596 "expected peer not found error, got: {err_str}"
2597 );
2598 }
2599
2600 #[tokio::test]
2601 async fn test_node_ping_resolves_peer() {
2602 let ws_port = random_port().await;
2603 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
2604
2605 let result = node.ping("peer-ping").await;
2607 assert!(result.is_err());
2608
2609 let peer = make_loopback_peer("peer-ping");
2611 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2612 tokio::time::sleep(Duration::from_millis(50)).await;
2613
2614 let result = node.ping("peer-ping").await;
2616 assert!(result.is_ok());
2617 assert_eq!(result.unwrap().latency, Duration::from_millis(1));
2618 }
2619
2620 #[tokio::test]
2621 async fn test_node_health() {
2622 let ws_port = random_port().await;
2623 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2624
2625 let health = node.health().await;
2626 assert!(health.healthy);
2627 assert_eq!(health.state, "running");
2628 }
2629
2630 #[tokio::test]
2631 async fn test_node_connect_quic_unknown_peer_errors() {
2632 let ws_port = random_port().await;
2633 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2634
2635 let result = node.connect_quic("peer", 4433).await;
2636 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
2637 }
2638
2639 #[tokio::test]
2640 async fn test_node_listen_tcp() {
2641 let ws_port = random_port().await;
2642 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
2643
2644 let listener = node.listen_tcp(0).await;
2646 assert!(listener.is_ok(), "listen_tcp failed: {:?}", listener.err());
2647 }
2648
2649 #[tokio::test]
2650 async fn test_envelope_serialize_deserialize() {
2651 let envelope = Envelope::new("chat", "message", json!({"text": "hello"})).with_timestamp();
2652
2653 let bytes = envelope.serialize().unwrap();
2654 let decoded = Envelope::deserialize(&bytes).unwrap();
2655
2656 assert_eq!(decoded.namespace, "chat");
2657 assert_eq!(decoded.msg_type, "message");
2658 assert_eq!(decoded.payload["text"], "hello");
2659 assert!(decoded.timestamp.is_some());
2660 }
2661
2662 #[tokio::test]
2663 async fn test_envelope_codec_json() {
2664 let codec = JsonCodec;
2665 let envelope = Envelope::new("ft", "offer", json!({"file": "test.bin"}));
2666
2667 let encoded = codec.encode(&envelope).unwrap();
2668 let decoded = codec.decode(&encoded).unwrap();
2669
2670 assert_eq!(decoded.namespace, "ft");
2671 assert_eq!(decoded.payload["file"], "test.bin");
2672 }
2673
2674 #[tokio::test]
2675 async fn test_envelope_unknown_fields_ignored() {
2676 let json_bytes = br#"{
2677 "namespace": "v2",
2678 "msg_type": "new",
2679 "payload": {},
2680 "future_field": "ignored"
2681 }"#;
2682
2683 let codec = JsonCodec;
2684 let decoded = codec.decode(json_bytes).unwrap();
2685 assert_eq!(decoded.namespace, "v2");
2686 assert_eq!(decoded.msg_type, "new");
2687 }
2688
2689 #[tokio::test]
2690 #[allow(deprecated)] async fn test_node_send_and_receive_roundtrip() {
2692 let port_a = random_port().await;
2694 let port_b = random_port().await;
2695
2696 let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
2697 let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
2698
2699 let peer_b = NetworkPeer {
2701 id: "node-b".to_string(),
2702 hostname: "truffle-test-node-b".to_string(),
2703 ip: "127.0.0.1".parse().unwrap(),
2704 online: true,
2705 cur_addr: Some("127.0.0.1:41641".to_string()),
2706 relay: None,
2707 os: None,
2708 last_seen: None,
2709 key_expiry: None,
2710 dns_name: None,
2711 };
2712 let peer_a = NetworkPeer {
2713 id: "node-a".to_string(),
2714 hostname: "truffle-test-node-a".to_string(),
2715 ip: "127.0.0.1".parse().unwrap(),
2716 online: true,
2717 cur_addr: Some("127.0.0.1:41641".to_string()),
2718 relay: None,
2719 os: None,
2720 last_seen: None,
2721 key_expiry: None,
2722 dns_name: None,
2723 };
2724
2725 let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
2726 let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
2727 tokio::time::sleep(Duration::from_millis(100)).await;
2728
2729 let mut rx = node_b.subscribe("test");
2731
2732 let send_result = node_a.send("node-b", "test", b"hello from a").await;
2735
2736 if send_result.is_ok() {
2744 let msg = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await;
2746 if let Ok(Ok(msg)) = msg {
2747 assert_eq!(msg.namespace, "test");
2748 }
2749 }
2750 }
2753
2754 #[tokio::test]
2757 #[allow(deprecated)] async fn test_node_stop_shuts_down_provider_and_is_idempotent() {
2759 let ws_port = random_port().await;
2760 let (node, event_tx, network) = make_test_node("node-1", ws_port).await;
2761
2762 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-1")));
2763 tokio::time::sleep(Duration::from_millis(50)).await;
2764
2765 assert_eq!(network.stop_call_count(), 0);
2766 node.stop().await;
2767 assert_eq!(
2768 network.stop_call_count(),
2769 1,
2770 "stop() must shut down the provider"
2771 );
2772
2773 node.stop().await;
2775 assert_eq!(network.stop_call_count(), 1);
2776
2777 let err = node.send("peer-1", "test", b"hi").await.unwrap_err();
2779 assert!(matches!(err, NodeError::Stopped));
2780 }
2781
2782 #[tokio::test]
2783 #[allow(deprecated)] async fn test_node_stop_closes_ws_connections() {
2785 let port_a = random_port().await;
2794 let (node_a, event_tx_a, _net_a) =
2795 make_test_node_with_device_id("node-a", "dev-node-a", port_a).await;
2796
2797 let self_peer = NetworkPeer {
2798 id: "node-a".to_string(),
2799 hostname: "truffle-test-node-a".to_string(),
2800 ip: "127.0.0.1".parse().unwrap(),
2801 online: true,
2802 cur_addr: Some("127.0.0.1:41641".to_string()),
2803 relay: None,
2804 os: None,
2805 last_seen: None,
2806 key_expiry: None,
2807 dns_name: None,
2808 };
2809 let _ = event_tx_a.send(NetworkPeerEvent::Joined(self_peer));
2810 tokio::time::sleep(Duration::from_millis(100)).await;
2811
2812 node_a
2813 .send("node-a", "test", b"hello from a")
2814 .await
2815 .expect("loopback self-dial should establish a WS connection");
2816
2817 let peers = node_a.peers().await;
2818 let entry = peers
2819 .iter()
2820 .find(|p| p.tailscale_id == "node-a")
2821 .expect("self peer discovered");
2822 assert!(
2823 entry.ws_connected,
2824 "send() should have established a WS connection"
2825 );
2826
2827 node_a.stop().await;
2828 let peers = node_a.peers().await;
2829 let entry = peers
2830 .iter()
2831 .find(|p| p.tailscale_id == "node-a")
2832 .expect("self peer still discovered");
2833 assert!(
2834 !entry.ws_connected,
2835 "stop() must close and un-mark WS connections"
2836 );
2837 }
2838
2839 async fn inject_peer_with_identity(
2845 node: &Node<MockNetworkProvider>,
2846 event_tx: &broadcast::Sender<NetworkPeerEvent>,
2847 tailscale_id: &str,
2848 device_id: &str,
2849 device_name: &str,
2850 ) {
2851 let peer = make_loopback_peer(tailscale_id);
2852 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2853 tokio::time::sleep(Duration::from_millis(30)).await;
2854
2855 let identity = crate::session::PeerIdentity {
2856 app_id: "test".into(),
2857 device_id: device_id.into(),
2858 device_name: device_name.into(),
2859 os: "linux".into(),
2860 tailscale_id: tailscale_id.into(),
2861 };
2862 assert!(
2863 node.session
2864 .test_stamp_identity(tailscale_id, identity)
2865 .await,
2866 "peer {tailscale_id} should exist in session registry before stamping identity"
2867 );
2868 }
2869
2870 #[tokio::test]
2871 async fn test_resolve_peer_id_by_device_id() {
2872 let ws_port = random_port().await;
2873 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2874
2875 inject_peer_with_identity(
2876 &node,
2877 &event_tx,
2878 "tailscale-abc",
2879 "01HZZZZZZZZZZZZZZZZZZZZZZZ",
2880 "Alice MacBook",
2881 )
2882 .await;
2883
2884 let resolved = node
2885 .resolve_peer_id("01HZZZZZZZZZZZZZZZZZZZZZZZ")
2886 .await
2887 .unwrap();
2888 assert_eq!(resolved, "01HZZZZZZZZZZZZZZZZZZZZZZZ");
2889 }
2890
2891 #[tokio::test]
2892 async fn test_resolve_peer_id_by_device_name() {
2893 let ws_port = random_port().await;
2894 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2895
2896 inject_peer_with_identity(
2897 &node,
2898 &event_tx,
2899 "tailscale-abc",
2900 "01HXYZXYZXYZXYZXYZXYZXYZXY",
2901 "Bob's Mac",
2902 )
2903 .await;
2904
2905 let resolved = node.resolve_peer_id("Bob's Mac").await.unwrap();
2906 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
2907 }
2908
2909 #[tokio::test]
2910 async fn test_resolve_peer_id_by_device_id_prefix() {
2911 let ws_port = random_port().await;
2912 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2913
2914 inject_peer_with_identity(
2915 &node,
2916 &event_tx,
2917 "tailscale-abc",
2918 "01HXYZXYZXYZXYZXYZXYZXYZXY",
2919 "laptop",
2920 )
2921 .await;
2922
2923 let resolved = node.resolve_peer_id("01HX").await.unwrap();
2926 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
2927 }
2928
2929 #[tokio::test]
2930 async fn test_resolve_peer_id_by_tailscale_id_legacy() {
2931 let ws_port = random_port().await;
2935 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2936
2937 inject_peer_with_identity(
2938 &node,
2939 &event_tx,
2940 "tailscale-legacy",
2941 "01HLEGACY0000000000000000X",
2942 "legacy box",
2943 )
2944 .await;
2945
2946 let resolved = node.resolve_peer_id("tailscale-legacy").await.unwrap();
2947 assert_eq!(resolved, "01HLEGACY0000000000000000X");
2948 }
2949
2950 #[tokio::test]
2951 async fn test_resolve_peer_id_unknown() {
2952 let ws_port = random_port().await;
2953 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2954 let result = node.resolve_peer_id("nope").await;
2955 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
2956 }
2957
2958 #[tokio::test]
2961 async fn test_resolve_peer_by_bare_name_before_hello() {
2962 let ws_port = random_port().await;
2966 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2967
2968 let mut peer = make_loopback_peer("nodeid-9");
2969 peer.hostname = "truffle-test-ec2-smoke".to_string();
2970 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2971 tokio::time::sleep(Duration::from_millis(50)).await;
2972
2973 assert_eq!(node.resolve_peer("ec2-smoke").await.unwrap().id, "nodeid-9");
2975 assert_eq!(node.resolve_peer("EC2 Smoke").await.unwrap().id, "nodeid-9");
2976 assert!(node.resolve_peer("other-box").await.is_err());
2978 }
2979
2980 #[tokio::test]
2981 async fn test_peers_device_name_strips_hostname_before_hello() {
2982 let ws_port = random_port().await;
2983 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2984
2985 let mut peer = make_loopback_peer("nodeid-9");
2986 peer.hostname = "truffle-test-ec2-smoke".to_string();
2987 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2988 tokio::time::sleep(Duration::from_millis(50)).await;
2989
2990 let peers = node.peers().await;
2991 assert_eq!(peers.len(), 1);
2992 assert!(peers[0].device_name.is_none());
2994 assert_eq!(peers[0].display_name, "ec2-smoke");
2995 assert_eq!(peers[0].hostname, "truffle-test-ec2-smoke");
2996 assert!(peers[0].device_id.is_none());
2997 }
2998
2999 #[tokio::test]
3000 async fn test_resolve_peer_stale_ref_is_peer_gone() {
3001 let ws_port = random_port().await;
3002 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
3003
3004 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-9")));
3005 tokio::time::sleep(Duration::from_millis(50)).await;
3006
3007 let live_ref = node.peers().await[0].peer_ref.clone();
3008 assert_eq!(node.resolve_peer(&live_ref).await.unwrap().id, "peer-9");
3009
3010 let _ = event_tx.send(NetworkPeerEvent::Left("peer-9".to_string()));
3014 tokio::time::sleep(Duration::from_millis(50)).await;
3015 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-9")));
3016 tokio::time::sleep(Duration::from_millis(50)).await;
3017
3018 assert!(matches!(
3019 node.resolve_peer(&live_ref).await,
3020 Err(NodeError::PeerGone(_))
3021 ));
3022 assert!(matches!(
3025 node.resolve_peer("peer-9:99").await,
3026 Err(NodeError::PeerGone(_))
3027 ));
3028 assert!(matches!(
3031 node.peer(&live_ref, Some(2_000)).await,
3032 Err(NodeError::PeerGone(_))
3033 ));
3034 }
3035
3036 #[tokio::test]
3037 async fn test_resolve_peer_accepts_ip() {
3038 let ws_port = random_port().await;
3039 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
3040
3041 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-a")));
3042 tokio::time::sleep(Duration::from_millis(50)).await;
3043
3044 let resolved = node.resolve_peer("127.0.0.1").await.unwrap();
3045 assert_eq!(resolved.id, "peer-a");
3046 assert_eq!(node.resolve_peer_id("127.0.0.1").await.unwrap(), "peer-a");
3049 }
3050
3051 #[tokio::test]
3052 async fn test_listen_tcp_rejects_reserved_ports() {
3053 let ws_port = random_port().await;
3054 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
3055
3056 let err = node.listen_tcp(9417).await.unwrap_err();
3057 assert!(
3058 matches!(err, NodeError::ReservedPort(9417)),
3059 "expected ReservedPort(9417), got: {err}"
3060 );
3061
3062 match node.listen_tcp(443).await {
3068 Ok(listener) => assert_eq!(listener.port, 443),
3069 Err(NodeError::ReservedPort(_)) => panic!("443 must not be reserved (RFC 023 D4)"),
3070 Err(e) => assert!(
3071 e.to_string().contains("RFC 023"),
3072 "443 bind errors should hint at the sidecar upgrade, got: {e}"
3073 ),
3074 }
3075 }
3076
3077 #[tokio::test]
3078 async fn test_listen_quic_rejects_reserved_and_ephemeral_ports() {
3079 let ws_port = random_port().await;
3080 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
3081
3082 let err = node.listen_quic(9417).await.unwrap_err();
3083 assert!(matches!(err, NodeError::ReservedPort(9417)));
3084
3085 let err = node.listen_quic(0).await.unwrap_err();
3086 assert!(matches!(err, NodeError::NotImplemented(_)));
3087 }
3088
3089 #[test]
3090 fn test_builder_hostname_override_validation() {
3091 assert!(NodeBuilder::default().hostname("dashboard").is_ok());
3092 assert!(NodeBuilder::default().hostname("my-app-1").is_ok());
3093 for bad in ["", "Dashboard", "has.dot", "-lead", "trail-"] {
3094 assert!(
3095 NodeBuilder::default().hostname(bad).is_err(),
3096 "expected hostname {bad:?} to be rejected"
3097 );
3098 }
3099 let too_long = "x".repeat(64);
3100 assert!(NodeBuilder::default().hostname(too_long).is_err());
3101 }
3102
3103 #[tokio::test]
3104 async fn test_bind_udp_falls_back_to_direct_socket() {
3105 let ws_port = random_port().await;
3108 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
3109
3110 let socket = node.bind_udp(0).await.unwrap();
3111 let addr = socket.local_addr().unwrap();
3112 assert_ne!(addr.port(), 0);
3113 }
3114
3115 #[test]
3116 fn test_raw_alpn_scoping() {
3117 assert_eq!(
3118 crate::transport::quic::raw_alpn("demo"),
3119 b"truffle-raw.demo".to_vec()
3120 );
3121 assert_eq!(
3122 crate::transport::quic::raw_alpn(""),
3123 b"truffle-raw".to_vec()
3124 );
3125 }
3126
3127 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3128 async fn test_connect_quic_roundtrip_via_raw_listener() {
3129 let ws_port = random_port().await;
3130 let (client_node, event_tx, _net) = make_test_node("cli", ws_port).await;
3131
3132 let server_network = Arc::new(MockNetworkProvider::new("srv"));
3135 let alpn = crate::transport::quic::raw_alpn("test");
3136 let listener = crate::transport::quic::listen_raw(&server_network, 0, &alpn)
3137 .await
3138 .unwrap();
3139 let port = listener.port();
3140 assert_ne!(port, 0);
3141
3142 let echo_task = tokio::spawn(async move {
3143 let conn = listener.accept().await.expect("no incoming connection");
3144 let mut stream = conn
3145 .accept_stream()
3146 .await
3147 .unwrap()
3148 .expect("connection closed before stream");
3149 let mut received = Vec::new();
3150 while let Some(chunk) = stream.read(1024).await.unwrap() {
3151 received.extend_from_slice(&chunk);
3152 }
3153 stream.write(&received).await.unwrap();
3154 stream.finish();
3155 tokio::time::sleep(Duration::from_millis(500)).await;
3158 conn.close();
3159 });
3160
3161 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("srv-peer")));
3163 tokio::time::sleep(Duration::from_millis(50)).await;
3164
3165 let conn = client_node.connect_quic("srv-peer", port).await.unwrap();
3166 let mut stream = conn.open_stream().await.unwrap();
3167 stream.write(b"hello quic").await.unwrap();
3168 stream.finish();
3169
3170 let mut echoed = Vec::new();
3171 while let Some(chunk) = stream.read(1024).await.unwrap() {
3172 echoed.extend_from_slice(&chunk);
3173 }
3174 assert_eq!(echoed, b"hello quic");
3175
3176 echo_task.await.unwrap();
3177 conn.close();
3178 }
3179
3180 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3181 async fn test_quic_raw_alpn_mismatch_rejected() {
3182 let server_network = Arc::new(MockNetworkProvider::new("srv"));
3185 let listener = crate::transport::quic::listen_raw(
3186 &server_network,
3187 0,
3188 &crate::transport::quic::raw_alpn("app-a"),
3189 )
3190 .await
3191 .unwrap();
3192 let port = listener.port();
3193
3194 let client_network = Arc::new(MockNetworkProvider::new("cli"));
3195 let result = crate::transport::quic::connect_raw(
3196 &client_network,
3197 "127.0.0.1",
3198 port,
3199 &crate::transport::quic::raw_alpn("app-b"),
3200 )
3201 .await;
3202
3203 assert!(
3204 result.is_err(),
3205 "cross-app QUIC connect must fail (ALPN mismatch)"
3206 );
3207 }
3208
3209 #[tokio::test]
3212 async fn test_rfc022_node_peer_resolves_and_wait_miss_returns_none() {
3213 let ws_port = random_port().await;
3214 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
3215
3216 assert!(node.peer("nope", None).await.unwrap().is_none());
3218
3219 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-a")));
3220 tokio::time::sleep(Duration::from_millis(50)).await;
3221
3222 let p = node.peer("peer-a", None).await.unwrap().expect("found");
3223 assert_eq!(p.tailscale_id, "peer-a");
3224 assert!(p.device_id.is_none());
3225
3226 let start = std::time::Instant::now();
3228 let miss = node.peer("still-missing", Some(80)).await.unwrap();
3229 assert!(miss.is_none());
3230 assert!(start.elapsed() >= Duration::from_millis(70));
3231 }
3232
3233 #[test]
3234 fn test_rfc022_peer_projection_never_uses_tailscale_as_device_id() {
3235 use crate::session::PeerState;
3236 use std::net::Ipv4Addr;
3237
3238 let pre = PeerState {
3240 id: "ts-abc".into(),
3241 generation: 1,
3242 name: "truffle-app-laptop".into(),
3243 ip: Ipv4Addr::new(100, 64, 0, 1).into(),
3244 online: true,
3245 ws_connected: false,
3246 connection_type: "direct".into(),
3247 os: None,
3248 last_seen: None,
3249 identity: None,
3250 identity_suppressed: false,
3251 };
3252 let p = Peer::from(pre);
3253 assert!(p.device_id.is_none());
3254 assert_eq!(p.tailscale_id, "ts-abc");
3255 assert_eq!(p.peer_ref, "ts-abc:1");
3256 assert_eq!(p.display_name, "laptop");
3257 assert!(p
3258 .device_id
3259 .as_ref()
3260 .map(|d| d.as_str() != p.tailscale_id)
3261 .unwrap_or(true));
3262
3263 let post = PeerState {
3265 id: "ts-abc".into(),
3266 generation: 1,
3267 name: "truffle-app-laptop".into(),
3268 ip: Ipv4Addr::new(100, 64, 0, 1).into(),
3269 online: true,
3270 ws_connected: true,
3271 connection_type: "direct".into(),
3272 os: Some("darwin".into()),
3273 last_seen: None,
3274 identity: Some(crate::session::PeerIdentity {
3275 app_id: "app".into(),
3276 device_id: "01J4K9M2Z8AB3RNYQPW6H5TC0X".into(),
3277 device_name: "Alice's Mac".into(),
3278 os: "darwin".into(),
3279 tailscale_id: "ts-abc".into(),
3280 }),
3281 identity_suppressed: false,
3282 };
3283 let p = Peer::from(post);
3284 assert_eq!(p.device_id.as_deref(), Some("01J4K9M2Z8AB3RNYQPW6H5TC0X"));
3285 assert_ne!(p.device_id.as_deref().unwrap(), p.tailscale_id);
3286 assert_eq!(p.display_name, "Alice's Mac");
3287
3288 let suppressed = PeerState {
3290 id: "ts-xyz".into(),
3291 generation: 1,
3292 name: "truffle-app-other".into(),
3293 ip: Ipv4Addr::new(100, 64, 0, 2).into(),
3294 online: true,
3295 ws_connected: true,
3296 connection_type: "direct".into(),
3297 os: None,
3298 last_seen: None,
3299 identity: Some(crate::session::PeerIdentity {
3300 app_id: "app".into(),
3301 device_id: "01J4K9M2Z8AB3RNYQPW6H5TC0X".into(),
3302 device_name: "Clone".into(),
3303 os: "linux".into(),
3304 tailscale_id: "ts-xyz".into(),
3305 }),
3306 identity_suppressed: true,
3307 };
3308 let p = Peer::from(suppressed);
3309 assert!(
3310 p.device_id.is_none(),
3311 "suppressed claim must project null device_id"
3312 );
3313 }
3314}