1use std::collections::HashMap;
33use std::net::IpAddr;
34use std::path::{Path, PathBuf};
35use std::sync::Arc;
36use std::sync::RwLock as StdRwLock;
44
45use tokio::net::TcpStream;
46use tokio::sync::broadcast;
47#[cfg(test)]
51use tokio::sync::RwLock;
52
53use crate::envelope::codec::{EnvelopeCodec, JsonCodec};
54use crate::envelope::{Envelope, EnvelopeError};
55use crate::file_transfer::{self, FileTransferState};
56use crate::identity::{self, AppId, DeviceId, DeviceName};
57use crate::network::tailscale::{TailscaleConfig, TailscaleProvider};
58use crate::network::{HealthInfo, NetworkProvider, NetworkUdpSocket, NodeIdentity, PingResult};
59use crate::session::{PeerEvent, PeerRegistry, PeerState};
60use crate::transport::websocket::WebSocketTransport;
61use crate::transport::{RawListener, WsConfig};
62
63#[derive(Debug, Clone)]
72pub struct NamespacedMessage {
73 pub from: String,
75 pub namespace: String,
77 pub msg_type: String,
79 pub payload: serde_json::Value,
81 pub timestamp: Option<u64>,
83}
84
85#[derive(Debug, Clone)]
101pub struct Peer {
102 pub id: String,
107 pub name: String,
110 pub device_id: String,
114 pub device_name: String,
118 pub tailscale_id: String,
121 pub ip: IpAddr,
123 pub online: bool,
125 pub ws_connected: bool,
127 pub connection_type: String,
129 pub os: Option<String>,
132 pub last_seen: Option<String>,
134}
135
136impl From<PeerState> for Peer {
137 fn from(s: PeerState) -> Self {
138 let (device_id, device_name, os) = match s.identity.as_ref() {
139 Some(identity) => (
140 identity.device_id.clone(),
141 identity.device_name.clone(),
142 Some(identity.os.clone()),
143 ),
144 None => (s.id.clone(), s.name.clone(), s.os.clone()),
145 };
146 let legacy_id = s
147 .identity
148 .as_ref()
149 .map(|i| i.device_id.clone())
150 .unwrap_or_else(|| s.id.clone());
151 Self {
152 id: legacy_id,
153 name: s.name,
154 device_id,
155 device_name,
156 tailscale_id: s.id,
157 ip: s.ip,
158 online: s.online,
159 ws_connected: s.ws_connected,
160 connection_type: s.connection_type,
161 os,
162 last_seen: s.last_seen,
163 }
164 }
165}
166
167#[derive(Debug, thiserror::Error)]
173pub enum NodeError {
174 #[error("peer not found: {0}")]
176 PeerNotFound(String),
177
178 #[error("connection failed: {0}")]
180 ConnectionFailed(String),
181
182 #[error("send failed: {0}")]
184 SendFailed(String),
185
186 #[error("envelope error: {0}")]
188 Envelope(#[from] EnvelopeError),
189
190 #[error("session error: {0}")]
192 Session(#[from] crate::session::SessionError),
193
194 #[error("network error: {0}")]
196 Network(#[from] crate::network::NetworkError),
197
198 #[error("transport error: {0}")]
200 Transport(#[from] crate::transport::TransportError),
201
202 #[error("not implemented: {0}")]
204 NotImplemented(String),
205
206 #[error("node stopped")]
208 Stopped,
209
210 #[error("build error: {0}")]
212 BuildError(String),
213
214 #[error("io error: {0}")]
216 Io(#[from] std::io::Error),
217}
218
219pub struct Node<N: NetworkProvider + 'static> {
235 pub(crate) network: Arc<N>,
237 session: Arc<PeerRegistry<N>>,
239 codec: Arc<dyn EnvelopeCodec>,
241 #[allow(dead_code)]
244 incoming_tx: broadcast::Sender<NamespacedMessage>,
245 namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
253 pub(crate) file_transfer_state: FileTransferState,
255 state_dir: PathBuf,
258 pub(crate) proxy_state: crate::proxy::ProxyState,
260}
261
262impl<N: NetworkProvider + 'static> Node<N> {
263 pub(crate) fn from_parts(
269 network: Arc<N>,
270 session: Arc<PeerRegistry<N>>,
271 codec: Arc<dyn EnvelopeCodec>,
272 ) -> Self {
273 let (incoming_tx, _) = broadcast::channel(1024);
274 let namespace_filters: Arc<
275 StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>,
276 > = Arc::new(StdRwLock::new(HashMap::new()));
277
278 let node = Self {
279 network,
280 session: session.clone(),
281 codec: codec.clone(),
282 incoming_tx: incoming_tx.clone(),
283 namespace_filters: namespace_filters.clone(),
284 file_transfer_state: FileTransferState::new(),
285 state_dir: PathBuf::new(),
286 proxy_state: crate::proxy::ProxyState::new(),
287 };
288
289 node.spawn_envelope_router(session, codec, incoming_tx, namespace_filters);
291
292 node
293 }
294
295 fn spawn_envelope_router(
299 &self,
300 session: Arc<PeerRegistry<N>>,
301 codec: Arc<dyn EnvelopeCodec>,
302 incoming_tx: broadcast::Sender<NamespacedMessage>,
303 namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
304 ) {
305 let mut rx = session.subscribe();
306
307 tokio::spawn(async move {
308 loop {
309 match rx.recv().await {
310 Ok(msg) => {
311 if let Ok(envelope) = codec.decode(&msg.data) {
312 let namespaced = NamespacedMessage {
313 from: msg.from,
314 namespace: envelope.namespace.clone(),
315 msg_type: envelope.msg_type,
316 payload: envelope.payload,
317 timestamp: envelope.timestamp,
318 };
319
320 tracing::debug!(
321 from = %namespaced.from,
322 namespace = %namespaced.namespace,
323 msg_type = %namespaced.msg_type,
324 "envelope router: dispatching message"
325 );
326
327 let _ = incoming_tx.send(namespaced.clone());
329
330 let filters = namespace_filters
334 .read()
335 .expect("namespace_filters std RwLock poisoned");
336 let _has_subscriber = filters.contains_key(&namespaced.namespace);
337 if let Some(tx) = filters.get(&namespaced.namespace) {
338 let send_result = tx.send(namespaced);
339 tracing::debug!(
340 namespace = %envelope.namespace,
341 subscriber_count = tx.receiver_count(),
342 sent = send_result.is_ok(),
343 "envelope router: sent to namespace subscriber"
344 );
345 } else {
346 tracing::debug!(
347 namespace = %envelope.namespace,
348 "envelope router: no subscriber for namespace"
349 );
350 }
351 } else {
352 tracing::warn!(
353 from = %msg.from,
354 data_len = msg.data.len(),
355 "node: failed to decode envelope from incoming message"
356 );
357 }
358 }
359 Err(broadcast::error::RecvError::Lagged(n)) => {
360 tracing::warn!(
361 missed = n,
362 "node: envelope router lagged, missed {n} messages"
363 );
364 continue;
365 }
366 Err(broadcast::error::RecvError::Closed) => {
367 tracing::debug!("node: session incoming channel closed, router exiting");
368 break;
369 }
370 }
371 }
372 });
373 }
374
375 pub fn builder() -> NodeBuilder {
379 NodeBuilder::default()
380 }
381
382 pub fn file_transfer(&self) -> file_transfer::FileTransfer<'_, N> {
389 file_transfer::FileTransfer::new(self)
390 }
391
392 pub fn proxy(&self) -> crate::proxy::Proxy<'_, N> {
399 crate::proxy::Proxy::new(self)
400 }
401
402 pub fn synced_store<T>(
411 self: &Arc<Self>,
412 store_id: &str,
413 ) -> Arc<crate::synced_store::SyncedStore<T>>
414 where
415 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
416 {
417 crate::synced_store::SyncedStore::new(self.clone(), store_id)
418 }
419
420 pub fn synced_store_with_backend<T>(
425 self: &Arc<Self>,
426 store_id: &str,
427 backend: std::sync::Arc<dyn crate::synced_store::StoreBackend>,
428 ) -> Arc<crate::synced_store::SyncedStore<T>>
429 where
430 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
431 {
432 crate::synced_store::SyncedStore::new_with_backend(self.clone(), store_id, backend)
433 }
434
435 pub async fn crdt_doc(
443 self: &Arc<Self>,
444 doc_id: &str,
445 ) -> Result<Arc<crate::crdt_doc::CrdtDoc>, crate::crdt_doc::CrdtDocError> {
446 crate::crdt_doc::CrdtDoc::new(self.clone(), doc_id).await
447 }
448
449 pub async fn crdt_doc_with_backend(
454 self: &Arc<Self>,
455 doc_id: &str,
456 backend: Arc<dyn crate::crdt_doc::CrdtBackend>,
457 ) -> Result<Arc<crate::crdt_doc::CrdtDoc>, crate::crdt_doc::CrdtDocError> {
458 crate::crdt_doc::CrdtDoc::new_with_backend(self.clone(), doc_id, backend).await
459 }
460
461 pub(crate) fn with_state_dir(mut self, dir: PathBuf) -> Self {
465 self.state_dir = dir;
466 self
467 }
468
469 pub fn state_dir(&self) -> &Path {
473 &self.state_dir
474 }
475
476 pub async fn stop(&self) {
484 tracing::info!("node: stopping");
485 }
489
490 pub fn local_info(&self) -> NodeIdentity {
494 self.network.local_identity()
495 }
496
497 pub async fn peers(&self) -> Vec<Peer> {
504 self.session
505 .peers()
506 .await
507 .into_iter()
508 .map(Peer::from)
509 .collect()
510 }
511
512 pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
514 self.session.on_peer_change()
515 }
516
517 pub async fn resolve_peer_id(&self, peer_id: &str) -> Result<String, NodeError> {
532 let peers = self.session.peers().await;
533
534 for p in &peers {
537 if let Some(identity) = p.identity.as_ref() {
538 if identity.device_id == peer_id || identity.device_name == peer_id {
539 return Ok(identity.device_id.clone());
540 }
541 }
542 if p.id == peer_id || p.name == peer_id {
543 return Ok(p
547 .identity
548 .as_ref()
549 .map(|i| i.device_id.clone())
550 .unwrap_or_else(|| p.id.clone()));
551 }
552 }
553
554 if peer_id.len() >= 4 {
558 let prefix_hits: Vec<&PeerState> = peers
559 .iter()
560 .filter(|p| {
561 p.identity
562 .as_ref()
563 .map(|i| i.device_id.starts_with(peer_id))
564 .unwrap_or(false)
565 })
566 .collect();
567 if prefix_hits.len() == 1 {
568 if let Some(identity) = prefix_hits[0].identity.as_ref() {
569 return Ok(identity.device_id.clone());
570 }
571 }
572 }
573
574 Err(NodeError::PeerNotFound(peer_id.to_string()))
575 }
576
577 pub async fn ping(&self, peer_id: &str) -> Result<PingResult, NodeError> {
584 let peers = self.session.peers().await;
585 let peer = peers
586 .iter()
587 .find(|p| {
588 p.id == peer_id
589 || p.name == peer_id
590 || p.identity
591 .as_ref()
592 .map(|i| i.device_id == peer_id || i.device_name == peer_id)
593 .unwrap_or(false)
594 })
595 .ok_or_else(|| NodeError::PeerNotFound(peer_id.to_string()))?;
596
597 let addr = peer.ip.to_string();
598 self.network.ping(&addr).await.map_err(NodeError::Network)
599 }
600
601 pub async fn health(&self) -> HealthInfo {
603 self.network.health().await
604 }
605
606 pub async fn send(&self, peer_id: &str, namespace: &str, data: &[u8]) -> Result<(), NodeError> {
614 let payload = std::str::from_utf8(data)
620 .ok()
621 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
622 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
623
624 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
625
626 let encoded = self.codec.encode(&envelope)?;
627 self.session.send(peer_id, &encoded).await?;
628 Ok(())
629 }
630
631 pub async fn send_typed(
639 &self,
640 peer_id: &str,
641 namespace: &str,
642 msg_type: &str,
643 payload: &serde_json::Value,
644 ) -> Result<(), NodeError> {
645 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
646 let encoded = self.codec.encode(&envelope)?;
647 self.session.send(peer_id, &encoded).await?;
648 Ok(())
649 }
650
651 pub async fn broadcast_typed(
654 &self,
655 namespace: &str,
656 msg_type: &str,
657 payload: &serde_json::Value,
658 ) {
659 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
660 match self.codec.encode(&envelope) {
661 Ok(encoded) => {
662 self.session.broadcast(&encoded).await;
663 }
664 Err(e) => {
665 tracing::error!("node: failed to encode broadcast envelope: {e}");
666 }
667 }
668 }
669
670 pub async fn broadcast(&self, namespace: &str, data: &[u8]) {
675 let payload = std::str::from_utf8(data)
676 .ok()
677 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
678 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
679
680 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
681
682 match self.codec.encode(&envelope) {
683 Ok(encoded) => {
684 self.session.broadcast(&encoded).await;
685 }
686 Err(e) => {
687 tracing::error!("node: failed to encode broadcast envelope: {e}");
688 }
689 }
690 }
691
692 pub fn subscribe(&self, namespace: &str) -> broadcast::Receiver<NamespacedMessage> {
698 {
700 let filters = self
701 .namespace_filters
702 .read()
703 .expect("namespace_filters std RwLock poisoned");
704 if let Some(tx) = filters.get(namespace) {
705 return tx.subscribe();
706 }
707 }
708
709 let mut filters = self
711 .namespace_filters
712 .write()
713 .expect("namespace_filters std RwLock poisoned");
714 if let Some(tx) = filters.get(namespace) {
716 return tx.subscribe();
717 }
718 let (tx, rx) = broadcast::channel(256);
719 filters.insert(namespace.to_string(), tx);
720 rx
721 }
722
723 pub async fn open_tcp(&self, peer_id: &str, port: u16) -> Result<TcpStream, NodeError> {
732 let peers = self.session.peers().await;
733 let peer = peers
734 .iter()
735 .find(|p| {
736 p.id == peer_id
737 || p.name == peer_id
738 || p.identity
739 .as_ref()
740 .map(|i| i.device_id == peer_id || i.device_name == peer_id)
741 .unwrap_or(false)
742 })
743 .ok_or_else(|| NodeError::PeerNotFound(peer_id.to_string()))?;
744
745 let addr = peer.ip.to_string();
746 self.network
747 .dial_tcp(&addr, port)
748 .await
749 .map_err(|e| NodeError::ConnectionFailed(e.to_string()))
750 }
751
752 pub async fn listen_tcp(&self, port: u16) -> Result<RawListener, NodeError> {
757 use crate::transport::tcp::TcpTransport;
758 use crate::transport::RawTransport;
759
760 let tcp = TcpTransport::new(self.network.clone());
761 tcp.listen(port).await.map_err(NodeError::Transport)
762 }
763
764 pub async fn open_quic(&self, _peer_id: &str) -> Result<(), NodeError> {
768 Err(NodeError::NotImplemented(
769 "QUIC connections are not yet implemented".to_string(),
770 ))
771 }
772
773 pub async fn open_udp(&self, _peer_id: &str) -> Result<NetworkUdpSocket, NodeError> {
777 Err(NodeError::NotImplemented(
778 "UDP sockets are not yet implemented".to_string(),
779 ))
780 }
781}
782
783#[derive(Debug, Clone)]
804pub struct NodeBuilder {
805 app_id: Option<AppId>,
806 device_name: Option<DeviceName>,
807 device_id: Option<DeviceId>,
808 sidecar_path: Option<PathBuf>,
809 state_dir: Option<String>,
810 auth_key: Option<String>,
811 ephemeral: bool,
812 ws_port: u16,
813}
814
815fn atomic_write_string(path: &Path, content: &str) -> std::io::Result<()> {
824 let _parent = path.parent().ok_or_else(|| {
825 std::io::Error::new(
826 std::io::ErrorKind::InvalidInput,
827 "path has no parent directory",
828 )
829 })?;
830 let mut tmp = path.to_path_buf();
831 tmp.set_extension("tmp");
832 std::fs::write(&tmp, content)?;
833 #[cfg(unix)]
834 {
835 std::fs::rename(&tmp, path)?;
836 }
837 #[cfg(windows)]
838 {
839 let _ = std::fs::remove_file(path);
842 std::fs::rename(&tmp, path)?;
843 }
844 Ok(())
845}
846
847impl Default for NodeBuilder {
848 fn default() -> Self {
849 Self {
850 app_id: None,
851 device_name: None,
852 device_id: None,
853 sidecar_path: None,
854 state_dir: None,
855 auth_key: None,
856 ephemeral: false,
857 ws_port: 9417,
858 }
859 }
860}
861
862impl NodeBuilder {
863 pub fn app_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
868 let raw: String = s.into();
869 let app_id = AppId::parse(&raw)
870 .map_err(|e| NodeError::BuildError(format!("invalid app_id: {e}")))?;
871 self.app_id = Some(app_id);
872 Ok(self)
873 }
874
875 pub fn device_name(mut self, s: impl Into<String>) -> Self {
880 self.device_name = Some(DeviceName::parse(s));
881 self
882 }
883
884 pub fn device_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
890 let raw: String = s.into();
891 let device_id = DeviceId::parse(&raw)
892 .map_err(|e| NodeError::BuildError(format!("invalid device_id: {e}")))?;
893 self.device_id = Some(device_id);
894 Ok(self)
895 }
896
897 pub fn sidecar_path(mut self, path: impl Into<PathBuf>) -> Self {
899 self.sidecar_path = Some(path.into());
900 self
901 }
902
903 pub fn state_dir(mut self, dir: &str) -> Self {
905 self.state_dir = Some(dir.to_string());
906 self
907 }
908
909 pub fn auth_key(mut self, key: &str) -> Self {
911 self.auth_key = Some(key.to_string());
912 self
913 }
914
915 pub fn ephemeral(mut self, val: bool) -> Self {
917 self.ephemeral = val;
918 self
919 }
920
921 pub fn ws_port(mut self, port: u16) -> Self {
923 self.ws_port = port;
924 self
925 }
926
927 fn prepare_config(&self) -> Result<TailscaleConfig, NodeError> {
934 let binary_path = self
936 .sidecar_path
937 .clone()
938 .ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
939
940 let app_id = self
942 .app_id
943 .clone()
944 .ok_or_else(|| NodeError::BuildError("app_id is required".into()))?;
945
946 let device_name = match self.device_name.clone() {
948 Some(name) => name,
949 None => {
950 let os_hostname = hostname::get()
951 .map_err(|e| {
952 NodeError::BuildError(format!(
953 "device_name is unset and hostname::get() failed: {e}"
954 ))
955 })?
956 .to_string_lossy()
957 .into_owned();
958 DeviceName::parse(os_hostname)
959 }
960 };
961
962 let tailscale_host = identity::tailscale_hostname(&app_id, &device_name);
965
966 let state_dir = self.state_dir.clone().unwrap_or_else(|| {
969 let base = dirs::data_dir().unwrap_or_else(|| {
970 tracing::warn!(
971 "dirs::data_dir() returned None, falling back to std::env::temp_dir()"
972 );
973 std::env::temp_dir()
974 });
975 base.join("truffle")
976 .join(app_id.as_str())
977 .join(identity::slug(device_name.as_str(), 255))
978 .to_string_lossy()
979 .into_owned()
980 });
981
982 std::fs::create_dir_all(&state_dir)?;
984
985 let device_id_file = Path::new(&state_dir).join("device-id.txt");
990 let device_id = match self.device_id.clone() {
991 Some(id) => {
992 atomic_write_string(&device_id_file, id.as_str())?;
994 id
995 }
996 None => {
997 if device_id_file.exists() {
998 let s = std::fs::read_to_string(&device_id_file)?.trim().to_string();
999 DeviceId::parse(&s).map_err(|e| {
1000 NodeError::BuildError(format!(
1001 "device-id.txt at {device_id_file:?} contains an invalid ULID: {e}"
1002 ))
1003 })?
1004 } else {
1005 let id = DeviceId::generate();
1006 atomic_write_string(&device_id_file, id.as_str())?;
1007 id
1008 }
1009 }
1010 };
1011
1012 Ok(TailscaleConfig {
1013 binary_path,
1014 app_id: app_id.as_str().to_string(),
1015 device_id: device_id.as_str().to_string(),
1016 device_name: device_name.as_str().to_string(),
1017 hostname: tailscale_host,
1018 state_dir,
1019 auth_key: self.auth_key.clone(),
1020 ephemeral: if self.ephemeral { Some(true) } else { None },
1021 tags: None,
1022 })
1023 }
1024
1025 pub async fn build(self) -> Result<Node<TailscaleProvider>, NodeError> {
1036 let ws_port = self.ws_port;
1037 let config = self.prepare_config()?;
1038 let state_dir = PathBuf::from(&config.state_dir);
1039
1040 let mut provider = TailscaleProvider::new(config);
1041 provider.start().await.map_err(NodeError::Network)?;
1042
1043 let network = Arc::new(provider);
1044
1045 let ws_config = WsConfig {
1047 port: ws_port,
1048 ..Default::default()
1049 };
1050 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1051
1052 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1054 session.start().await;
1055
1056 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1058 let node = Node::from_parts(network, session, codec).with_state_dir(state_dir);
1059
1060 tracing::info!("node: started successfully");
1061 Ok(node)
1062 }
1063
1064 pub async fn build_with_auth_handler(
1076 self,
1077 on_auth: impl Fn(String) + Send + 'static,
1078 ) -> Result<Node<TailscaleProvider>, NodeError> {
1079 let ws_port = self.ws_port;
1080 let config = self.prepare_config()?;
1081 let state_dir = PathBuf::from(&config.state_dir);
1082
1083 let mut provider = TailscaleProvider::new(config);
1084
1085 let mut auth_rx = provider.peer_events();
1087
1088 let auth_task = tokio::spawn(async move {
1090 use crate::network::NetworkPeerEvent;
1091 loop {
1092 match auth_rx.recv().await {
1093 Ok(NetworkPeerEvent::AuthRequired { url }) => {
1094 on_auth(url);
1095 }
1096 Err(broadcast::error::RecvError::Closed) => break,
1097 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1098 _ => {} }
1100 }
1101 });
1102
1103 let start_result = provider.start().await.map_err(NodeError::Network);
1105
1106 auth_task.abort();
1108
1109 start_result?;
1110
1111 let network = Arc::new(provider);
1112
1113 let ws_config = WsConfig {
1115 port: ws_port,
1116 ..Default::default()
1117 };
1118 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1119
1120 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1122 session.start().await;
1123
1124 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1126 let node = Node::from_parts(network, session, codec).with_state_dir(state_dir);
1127
1128 tracing::info!("node: started successfully (with auth handler)");
1129 Ok(node)
1130 }
1131}
1132
1133#[cfg(test)]
1138mod tests {
1139 use super::*;
1140 use crate::network::{
1141 HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
1142 NetworkTcpListener, NetworkUdpSocket, PeerAddr,
1143 };
1144 use crate::transport::WsConfig;
1145 use serde_json::json;
1146 use std::time::Duration;
1147 use tokio::sync::{broadcast, mpsc};
1148
1149 struct MockNetworkProvider {
1152 identity: NodeIdentity,
1153 local_addr: PeerAddr,
1154 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
1155 mock_peers: Arc<RwLock<Vec<NetworkPeer>>>,
1157 }
1158
1159 impl MockNetworkProvider {
1160 fn new(id: &str) -> Self {
1161 let (peer_event_tx, _) = broadcast::channel(64);
1162 Self {
1163 identity: NodeIdentity {
1164 app_id: "test".to_string(),
1165 device_id: id.to_string(),
1168 device_name: format!("Test Node {id}"),
1169 tailscale_hostname: format!("truffle-test-{id}"),
1170 tailscale_id: id.to_string(),
1171 dns_name: None,
1172 ip: Some("127.0.0.1".parse().unwrap()),
1173 },
1174 local_addr: PeerAddr {
1175 ip: Some("127.0.0.1".parse().unwrap()),
1176 hostname: format!("truffle-test-{id}"),
1177 dns_name: None,
1178 },
1179 peer_event_tx,
1180 mock_peers: Arc::new(RwLock::new(Vec::new())),
1181 }
1182 }
1183
1184 fn event_sender(&self) -> broadcast::Sender<NetworkPeerEvent> {
1185 self.peer_event_tx.clone()
1186 }
1187 }
1188
1189 impl NetworkProvider for MockNetworkProvider {
1190 async fn start(&mut self) -> Result<(), NetworkError> {
1191 Ok(())
1192 }
1193
1194 async fn stop(&mut self) -> Result<(), NetworkError> {
1195 Ok(())
1196 }
1197
1198 fn local_identity(&self) -> NodeIdentity {
1199 self.identity.clone()
1200 }
1201
1202 fn local_addr(&self) -> PeerAddr {
1203 self.local_addr.clone()
1204 }
1205
1206 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
1207 self.peer_event_tx.subscribe()
1208 }
1209
1210 async fn peers(&self) -> Vec<NetworkPeer> {
1211 self.mock_peers.read().await.clone()
1212 }
1213
1214 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
1215 let target = format!("{addr}:{port}");
1216 TcpStream::connect(&target)
1217 .await
1218 .map_err(|e| NetworkError::DialFailed(format!("mock dial {target}: {e}")))
1219 }
1220
1221 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
1222 let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
1223 .await
1224 .map_err(|e| NetworkError::ListenFailed(format!("mock listen :{port}: {e}")))?;
1225
1226 let actual_port = listener.local_addr().unwrap().port();
1227 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
1228
1229 tokio::spawn(async move {
1230 loop {
1231 match listener.accept().await {
1232 Ok((stream, addr)) => {
1233 let conn = IncomingConnection {
1234 stream,
1235 remote_addr: addr.to_string(),
1236 remote_identity: String::new(),
1237 port: actual_port,
1238 };
1239 if tx.send(conn).await.is_err() {
1240 break;
1241 }
1242 }
1243 Err(e) => {
1244 tracing::debug!("mock listener error: {e}");
1245 break;
1246 }
1247 }
1248 }
1249 });
1250
1251 Ok(NetworkTcpListener {
1252 port: actual_port,
1253 incoming: rx,
1254 })
1255 }
1256
1257 async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
1258 Ok(())
1259 }
1260
1261 async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
1262 Err(NetworkError::Internal("mock: UDP not supported".into()))
1263 }
1264
1265 async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
1266 Ok(PingResult {
1267 latency: Duration::from_millis(1),
1268 connection: "direct".to_string(),
1269 peer_addr: None,
1270 })
1271 }
1272
1273 async fn health(&self) -> HealthInfo {
1274 HealthInfo {
1275 state: "running".to_string(),
1276 healthy: true,
1277 ..Default::default()
1278 }
1279 }
1280 }
1281
1282 fn make_loopback_peer(id: &str) -> NetworkPeer {
1285 NetworkPeer {
1286 id: id.to_string(),
1287 hostname: format!("truffle-test-{id}"),
1288 ip: "127.0.0.1".parse().unwrap(),
1289 online: true,
1290 cur_addr: Some("127.0.0.1:41641".to_string()),
1291 relay: None,
1292 os: Some("linux".to_string()),
1293 last_seen: Some("2026-03-25T12:00:00Z".to_string()),
1294 key_expiry: None,
1295 dns_name: None,
1296 }
1297 }
1298
1299 fn ws_config(port: u16) -> WsConfig {
1300 WsConfig {
1301 port,
1302 ping_interval: Duration::from_secs(300),
1303 pong_timeout: Duration::from_secs(300),
1304 ..Default::default()
1305 }
1306 }
1307
1308 async fn random_port() -> u16 {
1309 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1310 l.local_addr().unwrap().port()
1311 }
1312
1313 async fn make_test_node(
1315 id: &str,
1316 ws_port: u16,
1317 ) -> (
1318 Node<MockNetworkProvider>,
1319 broadcast::Sender<NetworkPeerEvent>,
1320 Arc<MockNetworkProvider>,
1321 ) {
1322 let provider = MockNetworkProvider::new(id);
1323 let event_tx = provider.event_sender();
1324 let network = Arc::new(provider);
1325 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config(ws_port)));
1326 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1327 session.start().await;
1328
1329 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1330 let node = Node::from_parts(network.clone(), session, codec);
1331
1332 (node, event_tx, network)
1333 }
1334
1335 #[tokio::test]
1338 async fn test_node_builder_creates_node() {
1339 let ws_port = random_port().await;
1340 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1341
1342 let identity = node.local_info();
1343 assert_eq!(identity.tailscale_id, "node-1");
1344 assert_eq!(identity.device_id, "node-1");
1345 assert!(identity.tailscale_hostname.contains("node-1"));
1346 }
1347
1348 #[tokio::test]
1349 async fn test_node_peers_from_network() {
1350 let ws_port = random_port().await;
1351 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1352
1353 let peers = node.peers().await;
1355 assert!(peers.is_empty());
1356
1357 let peer = make_loopback_peer("peer-a");
1359 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1360 tokio::time::sleep(Duration::from_millis(50)).await;
1361
1362 let peers = node.peers().await;
1363 assert_eq!(peers.len(), 1);
1364 assert_eq!(peers[0].id, "peer-a");
1365 assert!(peers[0].online);
1366 assert!(!peers[0].ws_connected);
1367 }
1368
1369 #[tokio::test]
1370 async fn test_node_send_to_unknown_peer_errors() {
1371 let ws_port = random_port().await;
1372 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1373
1374 let result = node.send("nonexistent", "test", b"hello").await;
1375 assert!(result.is_err());
1376 let err_str = result.unwrap_err().to_string();
1377 assert!(
1378 err_str.contains("unknown peer") || err_str.contains("not found"),
1379 "expected unknown peer error, got: {err_str}"
1380 );
1381 }
1382
1383 #[tokio::test]
1384 async fn test_node_send_wraps_in_envelope() {
1385 let codec = JsonCodec;
1388 let data = b"hello world";
1389 let envelope = Envelope::new("test-ns", "message", serde_json::Value::from(data.to_vec()))
1390 .with_timestamp();
1391
1392 let encoded = codec.encode(&envelope).unwrap();
1393 let decoded = codec.decode(&encoded).unwrap();
1394
1395 assert_eq!(decoded.namespace, "test-ns");
1396 assert_eq!(decoded.msg_type, "message");
1397 assert!(decoded.timestamp.is_some());
1398 }
1399
1400 #[tokio::test]
1401 async fn test_node_subscribe_filters_by_namespace() {
1402 let ws_port = random_port().await;
1403 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1404
1405 let _rx_chat = node.subscribe("chat");
1407 let _rx_ft = node.subscribe("ft");
1408
1409 let _rx_chat2 = node.subscribe("chat");
1411 }
1412
1413 #[tokio::test]
1414 async fn test_node_broadcast() {
1415 let ws_port = random_port().await;
1416 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1417
1418 node.broadcast("test", b"hello everyone").await;
1420 }
1421
1422 #[tokio::test]
1423 async fn test_node_open_tcp_resolves_peer() {
1424 let ws_port = random_port().await;
1425 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1426
1427 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1429 let tcp_port = listener.local_addr().unwrap().port();
1430
1431 let peer = make_loopback_peer("peer-tcp");
1433 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1434 tokio::time::sleep(Duration::from_millis(50)).await;
1435
1436 let accept_handle = tokio::spawn(async move {
1438 let (stream, _) = listener.accept().await.unwrap();
1439 stream
1440 });
1441
1442 let stream = node.open_tcp("peer-tcp", tcp_port).await;
1444 assert!(stream.is_ok(), "open_tcp failed: {:?}", stream.err());
1445
1446 let _ = accept_handle.await;
1447 }
1448
1449 #[tokio::test]
1450 async fn test_node_open_tcp_unknown_peer_errors() {
1451 let ws_port = random_port().await;
1452 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1453
1454 let result = node.open_tcp("nonexistent", 8080).await;
1455 assert!(result.is_err());
1456 let err_str = result.unwrap_err().to_string();
1457 assert!(
1458 err_str.contains("not found"),
1459 "expected peer not found error, got: {err_str}"
1460 );
1461 }
1462
1463 #[tokio::test]
1464 async fn test_node_ping_resolves_peer() {
1465 let ws_port = random_port().await;
1466 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1467
1468 let result = node.ping("peer-ping").await;
1470 assert!(result.is_err());
1471
1472 let peer = make_loopback_peer("peer-ping");
1474 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1475 tokio::time::sleep(Duration::from_millis(50)).await;
1476
1477 let result = node.ping("peer-ping").await;
1479 assert!(result.is_ok());
1480 assert_eq!(result.unwrap().latency, Duration::from_millis(1));
1481 }
1482
1483 #[tokio::test]
1484 async fn test_node_health() {
1485 let ws_port = random_port().await;
1486 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1487
1488 let health = node.health().await;
1489 assert!(health.healthy);
1490 assert_eq!(health.state, "running");
1491 }
1492
1493 #[tokio::test]
1494 async fn test_node_open_quic_not_implemented() {
1495 let ws_port = random_port().await;
1496 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1497
1498 let result = node.open_quic("peer").await;
1499 assert!(matches!(result, Err(NodeError::NotImplemented(_))));
1500 }
1501
1502 #[tokio::test]
1503 async fn test_node_open_udp_not_implemented() {
1504 let ws_port = random_port().await;
1505 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1506
1507 let result = node.open_udp("peer").await;
1508 assert!(matches!(result, Err(NodeError::NotImplemented(_))));
1509 }
1510
1511 #[tokio::test]
1512 async fn test_node_listen_tcp() {
1513 let ws_port = random_port().await;
1514 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1515
1516 let listener = node.listen_tcp(0).await;
1518 assert!(listener.is_ok(), "listen_tcp failed: {:?}", listener.err());
1519 }
1520
1521 #[tokio::test]
1522 async fn test_envelope_serialize_deserialize() {
1523 let envelope = Envelope::new("chat", "message", json!({"text": "hello"})).with_timestamp();
1524
1525 let bytes = envelope.serialize().unwrap();
1526 let decoded = Envelope::deserialize(&bytes).unwrap();
1527
1528 assert_eq!(decoded.namespace, "chat");
1529 assert_eq!(decoded.msg_type, "message");
1530 assert_eq!(decoded.payload["text"], "hello");
1531 assert!(decoded.timestamp.is_some());
1532 }
1533
1534 #[tokio::test]
1535 async fn test_envelope_codec_json() {
1536 let codec = JsonCodec;
1537 let envelope = Envelope::new("ft", "offer", json!({"file": "test.bin"}));
1538
1539 let encoded = codec.encode(&envelope).unwrap();
1540 let decoded = codec.decode(&encoded).unwrap();
1541
1542 assert_eq!(decoded.namespace, "ft");
1543 assert_eq!(decoded.payload["file"], "test.bin");
1544 }
1545
1546 #[tokio::test]
1547 async fn test_envelope_unknown_fields_ignored() {
1548 let json_bytes = br#"{
1549 "namespace": "v2",
1550 "msg_type": "new",
1551 "payload": {},
1552 "future_field": "ignored"
1553 }"#;
1554
1555 let codec = JsonCodec;
1556 let decoded = codec.decode(json_bytes).unwrap();
1557 assert_eq!(decoded.namespace, "v2");
1558 assert_eq!(decoded.msg_type, "new");
1559 }
1560
1561 #[tokio::test]
1562 async fn test_node_send_and_receive_roundtrip() {
1563 let port_a = random_port().await;
1565 let port_b = random_port().await;
1566
1567 let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
1568 let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
1569
1570 let peer_b = NetworkPeer {
1572 id: "node-b".to_string(),
1573 hostname: "truffle-test-node-b".to_string(),
1574 ip: "127.0.0.1".parse().unwrap(),
1575 online: true,
1576 cur_addr: Some("127.0.0.1:41641".to_string()),
1577 relay: None,
1578 os: None,
1579 last_seen: None,
1580 key_expiry: None,
1581 dns_name: None,
1582 };
1583 let peer_a = NetworkPeer {
1584 id: "node-a".to_string(),
1585 hostname: "truffle-test-node-a".to_string(),
1586 ip: "127.0.0.1".parse().unwrap(),
1587 online: true,
1588 cur_addr: Some("127.0.0.1:41641".to_string()),
1589 relay: None,
1590 os: None,
1591 last_seen: None,
1592 key_expiry: None,
1593 dns_name: None,
1594 };
1595
1596 let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
1597 let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
1598 tokio::time::sleep(Duration::from_millis(100)).await;
1599
1600 let mut rx = node_b.subscribe("test");
1602
1603 let send_result = node_a.send("node-b", "test", b"hello from a").await;
1606
1607 if send_result.is_ok() {
1615 let msg = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await;
1617 if let Ok(Ok(msg)) = msg {
1618 assert_eq!(msg.namespace, "test");
1619 }
1620 }
1621 }
1624
1625 async fn inject_peer_with_identity(
1631 node: &Node<MockNetworkProvider>,
1632 event_tx: &broadcast::Sender<NetworkPeerEvent>,
1633 tailscale_id: &str,
1634 device_id: &str,
1635 device_name: &str,
1636 ) {
1637 let peer = make_loopback_peer(tailscale_id);
1638 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1639 tokio::time::sleep(Duration::from_millis(30)).await;
1640
1641 let identity = crate::session::PeerIdentity {
1642 app_id: "test".into(),
1643 device_id: device_id.into(),
1644 device_name: device_name.into(),
1645 os: "linux".into(),
1646 tailscale_id: tailscale_id.into(),
1647 };
1648 assert!(
1649 node.session
1650 .test_stamp_identity(tailscale_id, identity)
1651 .await,
1652 "peer {tailscale_id} should exist in session registry before stamping identity"
1653 );
1654 }
1655
1656 #[tokio::test]
1657 async fn test_resolve_peer_id_by_device_id() {
1658 let ws_port = random_port().await;
1659 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1660
1661 inject_peer_with_identity(
1662 &node,
1663 &event_tx,
1664 "tailscale-abc",
1665 "01HZZZZZZZZZZZZZZZZZZZZZZZ",
1666 "Alice MacBook",
1667 )
1668 .await;
1669
1670 let resolved = node
1671 .resolve_peer_id("01HZZZZZZZZZZZZZZZZZZZZZZZ")
1672 .await
1673 .unwrap();
1674 assert_eq!(resolved, "01HZZZZZZZZZZZZZZZZZZZZZZZ");
1675 }
1676
1677 #[tokio::test]
1678 async fn test_resolve_peer_id_by_device_name() {
1679 let ws_port = random_port().await;
1680 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1681
1682 inject_peer_with_identity(
1683 &node,
1684 &event_tx,
1685 "tailscale-abc",
1686 "01HXYZXYZXYZXYZXYZXYZXYZXY",
1687 "Bob's Mac",
1688 )
1689 .await;
1690
1691 let resolved = node.resolve_peer_id("Bob's Mac").await.unwrap();
1692 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
1693 }
1694
1695 #[tokio::test]
1696 async fn test_resolve_peer_id_by_device_id_prefix() {
1697 let ws_port = random_port().await;
1698 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1699
1700 inject_peer_with_identity(
1701 &node,
1702 &event_tx,
1703 "tailscale-abc",
1704 "01HXYZXYZXYZXYZXYZXYZXYZXY",
1705 "laptop",
1706 )
1707 .await;
1708
1709 let resolved = node.resolve_peer_id("01HX").await.unwrap();
1712 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
1713 }
1714
1715 #[tokio::test]
1716 async fn test_resolve_peer_id_by_tailscale_id_legacy() {
1717 let ws_port = random_port().await;
1721 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1722
1723 inject_peer_with_identity(
1724 &node,
1725 &event_tx,
1726 "tailscale-legacy",
1727 "01HLEGACY0000000000000000X",
1728 "legacy box",
1729 )
1730 .await;
1731
1732 let resolved = node.resolve_peer_id("tailscale-legacy").await.unwrap();
1733 assert_eq!(resolved, "01HLEGACY0000000000000000X");
1734 }
1735
1736 #[tokio::test]
1737 async fn test_resolve_peer_id_unknown() {
1738 let ws_port = random_port().await;
1739 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
1740 let result = node.resolve_peer_id("nope").await;
1741 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
1742 }
1743}