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 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}
259
260impl<N: NetworkProvider + 'static> Node<N> {
261 pub(crate) fn from_parts(
267 network: Arc<N>,
268 session: Arc<PeerRegistry<N>>,
269 codec: Arc<dyn EnvelopeCodec>,
270 ) -> Self {
271 let (incoming_tx, _) = broadcast::channel(1024);
272 let namespace_filters: Arc<
273 StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>,
274 > = Arc::new(StdRwLock::new(HashMap::new()));
275
276 let node = Self {
277 network,
278 session: session.clone(),
279 codec: codec.clone(),
280 incoming_tx: incoming_tx.clone(),
281 namespace_filters: namespace_filters.clone(),
282 file_transfer_state: FileTransferState::new(),
283 state_dir: PathBuf::new(),
284 };
285
286 node.spawn_envelope_router(session, codec, incoming_tx, namespace_filters);
288
289 node
290 }
291
292 fn spawn_envelope_router(
296 &self,
297 session: Arc<PeerRegistry<N>>,
298 codec: Arc<dyn EnvelopeCodec>,
299 incoming_tx: broadcast::Sender<NamespacedMessage>,
300 namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
301 ) {
302 let mut rx = session.subscribe();
303
304 tokio::spawn(async move {
305 loop {
306 match rx.recv().await {
307 Ok(msg) => {
308 if let Ok(envelope) = codec.decode(&msg.data) {
309 let namespaced = NamespacedMessage {
310 from: msg.from,
311 namespace: envelope.namespace.clone(),
312 msg_type: envelope.msg_type,
313 payload: envelope.payload,
314 timestamp: envelope.timestamp,
315 };
316
317 tracing::debug!(
318 from = %namespaced.from,
319 namespace = %namespaced.namespace,
320 msg_type = %namespaced.msg_type,
321 "envelope router: dispatching message"
322 );
323
324 let _ = incoming_tx.send(namespaced.clone());
326
327 let filters = namespace_filters
331 .read()
332 .expect("namespace_filters std RwLock poisoned");
333 let _has_subscriber = filters.contains_key(&namespaced.namespace);
334 if let Some(tx) = filters.get(&namespaced.namespace) {
335 let send_result = tx.send(namespaced);
336 tracing::debug!(
337 namespace = %envelope.namespace,
338 subscriber_count = tx.receiver_count(),
339 sent = send_result.is_ok(),
340 "envelope router: sent to namespace subscriber"
341 );
342 } else {
343 tracing::debug!(
344 namespace = %envelope.namespace,
345 "envelope router: no subscriber for namespace"
346 );
347 }
348 } else {
349 tracing::warn!(
350 from = %msg.from,
351 data_len = msg.data.len(),
352 "node: failed to decode envelope from incoming message"
353 );
354 }
355 }
356 Err(broadcast::error::RecvError::Lagged(n)) => {
357 tracing::warn!(
358 missed = n,
359 "node: envelope router lagged, missed {n} messages"
360 );
361 continue;
362 }
363 Err(broadcast::error::RecvError::Closed) => {
364 tracing::debug!("node: session incoming channel closed, router exiting");
365 break;
366 }
367 }
368 }
369 });
370 }
371
372 pub fn builder() -> NodeBuilder {
376 NodeBuilder::default()
377 }
378
379 pub fn file_transfer(&self) -> file_transfer::FileTransfer<'_, N> {
386 file_transfer::FileTransfer::new(self)
387 }
388
389 pub fn synced_store<T>(
398 self: &Arc<Self>,
399 store_id: &str,
400 ) -> Arc<crate::synced_store::SyncedStore<T>>
401 where
402 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
403 {
404 crate::synced_store::SyncedStore::new(self.clone(), store_id)
405 }
406
407 pub fn synced_store_with_backend<T>(
412 self: &Arc<Self>,
413 store_id: &str,
414 backend: std::sync::Arc<dyn crate::synced_store::StoreBackend>,
415 ) -> Arc<crate::synced_store::SyncedStore<T>>
416 where
417 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
418 {
419 crate::synced_store::SyncedStore::new_with_backend(self.clone(), store_id, backend)
420 }
421
422 pub async fn crdt_doc(
430 self: &Arc<Self>,
431 doc_id: &str,
432 ) -> Result<Arc<crate::crdt_doc::CrdtDoc>, crate::crdt_doc::CrdtDocError> {
433 crate::crdt_doc::CrdtDoc::new(self.clone(), doc_id).await
434 }
435
436 pub async fn crdt_doc_with_backend(
441 self: &Arc<Self>,
442 doc_id: &str,
443 backend: Arc<dyn crate::crdt_doc::CrdtBackend>,
444 ) -> Result<Arc<crate::crdt_doc::CrdtDoc>, crate::crdt_doc::CrdtDocError> {
445 crate::crdt_doc::CrdtDoc::new_with_backend(self.clone(), doc_id, backend).await
446 }
447
448 pub(crate) fn with_state_dir(mut self, dir: PathBuf) -> Self {
452 self.state_dir = dir;
453 self
454 }
455
456 pub fn state_dir(&self) -> &Path {
460 &self.state_dir
461 }
462
463 pub async fn stop(&self) {
471 tracing::info!("node: stopping");
472 }
476
477 pub fn local_info(&self) -> NodeIdentity {
481 self.network.local_identity()
482 }
483
484 pub async fn peers(&self) -> Vec<Peer> {
491 self.session
492 .peers()
493 .await
494 .into_iter()
495 .map(Peer::from)
496 .collect()
497 }
498
499 pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
501 self.session.on_peer_change()
502 }
503
504 pub async fn resolve_peer_id(&self, peer_id: &str) -> Result<String, NodeError> {
519 let peers = self.session.peers().await;
520
521 for p in &peers {
524 if let Some(identity) = p.identity.as_ref() {
525 if identity.device_id == peer_id || identity.device_name == peer_id {
526 return Ok(identity.device_id.clone());
527 }
528 }
529 if p.id == peer_id || p.name == peer_id {
530 return Ok(p
534 .identity
535 .as_ref()
536 .map(|i| i.device_id.clone())
537 .unwrap_or_else(|| p.id.clone()));
538 }
539 }
540
541 if peer_id.len() >= 4 {
545 let prefix_hits: Vec<&PeerState> = peers
546 .iter()
547 .filter(|p| {
548 p.identity
549 .as_ref()
550 .map(|i| i.device_id.starts_with(peer_id))
551 .unwrap_or(false)
552 })
553 .collect();
554 if prefix_hits.len() == 1 {
555 if let Some(identity) = prefix_hits[0].identity.as_ref() {
556 return Ok(identity.device_id.clone());
557 }
558 }
559 }
560
561 Err(NodeError::PeerNotFound(peer_id.to_string()))
562 }
563
564 pub async fn ping(&self, peer_id: &str) -> Result<PingResult, NodeError> {
571 let peers = self.session.peers().await;
572 let peer = peers
573 .iter()
574 .find(|p| {
575 p.id == peer_id
576 || p.name == peer_id
577 || p.identity
578 .as_ref()
579 .map(|i| i.device_id == peer_id || i.device_name == peer_id)
580 .unwrap_or(false)
581 })
582 .ok_or_else(|| NodeError::PeerNotFound(peer_id.to_string()))?;
583
584 let addr = peer.ip.to_string();
585 self.network.ping(&addr).await.map_err(NodeError::Network)
586 }
587
588 pub async fn health(&self) -> HealthInfo {
590 self.network.health().await
591 }
592
593 pub async fn send(&self, peer_id: &str, namespace: &str, data: &[u8]) -> Result<(), NodeError> {
601 let payload = std::str::from_utf8(data)
607 .ok()
608 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
609 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
610
611 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
612
613 let encoded = self.codec.encode(&envelope)?;
614 self.session.send(peer_id, &encoded).await?;
615 Ok(())
616 }
617
618 pub async fn send_typed(
626 &self,
627 peer_id: &str,
628 namespace: &str,
629 msg_type: &str,
630 payload: &serde_json::Value,
631 ) -> Result<(), NodeError> {
632 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
633 let encoded = self.codec.encode(&envelope)?;
634 self.session.send(peer_id, &encoded).await?;
635 Ok(())
636 }
637
638 pub async fn broadcast_typed(
641 &self,
642 namespace: &str,
643 msg_type: &str,
644 payload: &serde_json::Value,
645 ) {
646 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
647 match self.codec.encode(&envelope) {
648 Ok(encoded) => {
649 self.session.broadcast(&encoded).await;
650 }
651 Err(e) => {
652 tracing::error!("node: failed to encode broadcast envelope: {e}");
653 }
654 }
655 }
656
657 pub async fn broadcast(&self, namespace: &str, data: &[u8]) {
662 let payload = std::str::from_utf8(data)
663 .ok()
664 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
665 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
666
667 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
668
669 match self.codec.encode(&envelope) {
670 Ok(encoded) => {
671 self.session.broadcast(&encoded).await;
672 }
673 Err(e) => {
674 tracing::error!("node: failed to encode broadcast envelope: {e}");
675 }
676 }
677 }
678
679 pub fn subscribe(&self, namespace: &str) -> broadcast::Receiver<NamespacedMessage> {
685 {
687 let filters = self
688 .namespace_filters
689 .read()
690 .expect("namespace_filters std RwLock poisoned");
691 if let Some(tx) = filters.get(namespace) {
692 return tx.subscribe();
693 }
694 }
695
696 let mut filters = self
698 .namespace_filters
699 .write()
700 .expect("namespace_filters std RwLock poisoned");
701 if let Some(tx) = filters.get(namespace) {
703 return tx.subscribe();
704 }
705 let (tx, rx) = broadcast::channel(256);
706 filters.insert(namespace.to_string(), tx);
707 rx
708 }
709
710 pub async fn open_tcp(&self, peer_id: &str, port: u16) -> Result<TcpStream, NodeError> {
719 let peers = self.session.peers().await;
720 let peer = peers
721 .iter()
722 .find(|p| {
723 p.id == peer_id
724 || p.name == peer_id
725 || p.identity
726 .as_ref()
727 .map(|i| i.device_id == peer_id || i.device_name == peer_id)
728 .unwrap_or(false)
729 })
730 .ok_or_else(|| NodeError::PeerNotFound(peer_id.to_string()))?;
731
732 let addr = peer.ip.to_string();
733 self.network
734 .dial_tcp(&addr, port)
735 .await
736 .map_err(|e| NodeError::ConnectionFailed(e.to_string()))
737 }
738
739 pub async fn listen_tcp(&self, port: u16) -> Result<RawListener, NodeError> {
744 use crate::transport::tcp::TcpTransport;
745 use crate::transport::RawTransport;
746
747 let tcp = TcpTransport::new(self.network.clone());
748 tcp.listen(port).await.map_err(NodeError::Transport)
749 }
750
751 pub async fn open_quic(&self, _peer_id: &str) -> Result<(), NodeError> {
755 Err(NodeError::NotImplemented(
756 "QUIC connections are not yet implemented".to_string(),
757 ))
758 }
759
760 pub async fn open_udp(&self, _peer_id: &str) -> Result<NetworkUdpSocket, NodeError> {
764 Err(NodeError::NotImplemented(
765 "UDP sockets are not yet implemented".to_string(),
766 ))
767 }
768}
769
770#[derive(Debug, Clone)]
791pub struct NodeBuilder {
792 app_id: Option<AppId>,
793 device_name: Option<DeviceName>,
794 device_id: Option<DeviceId>,
795 sidecar_path: Option<PathBuf>,
796 state_dir: Option<String>,
797 auth_key: Option<String>,
798 ephemeral: bool,
799 ws_port: u16,
800}
801
802fn atomic_write_string(path: &Path, content: &str) -> std::io::Result<()> {
811 let _parent = path.parent().ok_or_else(|| {
812 std::io::Error::new(
813 std::io::ErrorKind::InvalidInput,
814 "path has no parent directory",
815 )
816 })?;
817 let mut tmp = path.to_path_buf();
818 tmp.set_extension("tmp");
819 std::fs::write(&tmp, content)?;
820 #[cfg(unix)]
821 {
822 std::fs::rename(&tmp, path)?;
823 }
824 #[cfg(windows)]
825 {
826 let _ = std::fs::remove_file(path);
829 std::fs::rename(&tmp, path)?;
830 }
831 Ok(())
832}
833
834impl Default for NodeBuilder {
835 fn default() -> Self {
836 Self {
837 app_id: None,
838 device_name: None,
839 device_id: None,
840 sidecar_path: None,
841 state_dir: None,
842 auth_key: None,
843 ephemeral: false,
844 ws_port: 9417,
845 }
846 }
847}
848
849impl NodeBuilder {
850 pub fn app_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
855 let raw: String = s.into();
856 let app_id = AppId::parse(&raw)
857 .map_err(|e| NodeError::BuildError(format!("invalid app_id: {e}")))?;
858 self.app_id = Some(app_id);
859 Ok(self)
860 }
861
862 pub fn device_name(mut self, s: impl Into<String>) -> Self {
867 self.device_name = Some(DeviceName::parse(s));
868 self
869 }
870
871 pub fn device_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
877 let raw: String = s.into();
878 let device_id = DeviceId::parse(&raw)
879 .map_err(|e| NodeError::BuildError(format!("invalid device_id: {e}")))?;
880 self.device_id = Some(device_id);
881 Ok(self)
882 }
883
884 pub fn sidecar_path(mut self, path: impl Into<PathBuf>) -> Self {
886 self.sidecar_path = Some(path.into());
887 self
888 }
889
890 pub fn state_dir(mut self, dir: &str) -> Self {
892 self.state_dir = Some(dir.to_string());
893 self
894 }
895
896 pub fn auth_key(mut self, key: &str) -> Self {
898 self.auth_key = Some(key.to_string());
899 self
900 }
901
902 pub fn ephemeral(mut self, val: bool) -> Self {
904 self.ephemeral = val;
905 self
906 }
907
908 pub fn ws_port(mut self, port: u16) -> Self {
910 self.ws_port = port;
911 self
912 }
913
914 fn prepare_config(&self) -> Result<TailscaleConfig, NodeError> {
921 let binary_path = self
923 .sidecar_path
924 .clone()
925 .ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
926
927 let app_id = self
929 .app_id
930 .clone()
931 .ok_or_else(|| NodeError::BuildError("app_id is required".into()))?;
932
933 let device_name = match self.device_name.clone() {
935 Some(name) => name,
936 None => {
937 let os_hostname = hostname::get()
938 .map_err(|e| {
939 NodeError::BuildError(format!(
940 "device_name is unset and hostname::get() failed: {e}"
941 ))
942 })?
943 .to_string_lossy()
944 .into_owned();
945 DeviceName::parse(os_hostname)
946 }
947 };
948
949 let tailscale_host = identity::tailscale_hostname(&app_id, &device_name);
952
953 let state_dir = self.state_dir.clone().unwrap_or_else(|| {
956 let base = dirs::data_dir().unwrap_or_else(|| {
957 tracing::warn!(
958 "dirs::data_dir() returned None, falling back to std::env::temp_dir()"
959 );
960 std::env::temp_dir()
961 });
962 base.join("truffle")
963 .join(app_id.as_str())
964 .join(identity::slug(device_name.as_str(), 255))
965 .to_string_lossy()
966 .into_owned()
967 });
968
969 std::fs::create_dir_all(&state_dir)?;
971
972 let device_id_file = Path::new(&state_dir).join("device-id.txt");
977 let device_id = match self.device_id.clone() {
978 Some(id) => {
979 atomic_write_string(&device_id_file, id.as_str())?;
981 id
982 }
983 None => {
984 if device_id_file.exists() {
985 let s = std::fs::read_to_string(&device_id_file)?.trim().to_string();
986 DeviceId::parse(&s).map_err(|e| {
987 NodeError::BuildError(format!(
988 "device-id.txt at {device_id_file:?} contains an invalid ULID: {e}"
989 ))
990 })?
991 } else {
992 let id = DeviceId::generate();
993 atomic_write_string(&device_id_file, id.as_str())?;
994 id
995 }
996 }
997 };
998
999 Ok(TailscaleConfig {
1000 binary_path,
1001 app_id: app_id.as_str().to_string(),
1002 device_id: device_id.as_str().to_string(),
1003 device_name: device_name.as_str().to_string(),
1004 hostname: tailscale_host,
1005 state_dir,
1006 auth_key: self.auth_key.clone(),
1007 ephemeral: if self.ephemeral { Some(true) } else { None },
1008 tags: None,
1009 })
1010 }
1011
1012 pub async fn build(self) -> Result<Node<TailscaleProvider>, NodeError> {
1023 let ws_port = self.ws_port;
1024 let config = self.prepare_config()?;
1025 let state_dir = PathBuf::from(&config.state_dir);
1026
1027 let mut provider = TailscaleProvider::new(config);
1028 provider.start().await.map_err(NodeError::Network)?;
1029
1030 let network = Arc::new(provider);
1031
1032 let ws_config = WsConfig {
1034 port: ws_port,
1035 ..Default::default()
1036 };
1037 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1038
1039 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1041 session.start().await;
1042
1043 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1045 let node = Node::from_parts(network, session, codec).with_state_dir(state_dir);
1046
1047 tracing::info!("node: started successfully");
1048 Ok(node)
1049 }
1050
1051 pub async fn build_with_auth_handler(
1063 self,
1064 on_auth: impl Fn(String) + Send + 'static,
1065 ) -> Result<Node<TailscaleProvider>, NodeError> {
1066 let ws_port = self.ws_port;
1067 let config = self.prepare_config()?;
1068 let state_dir = PathBuf::from(&config.state_dir);
1069
1070 let mut provider = TailscaleProvider::new(config);
1071
1072 let mut auth_rx = provider.peer_events();
1074
1075 let auth_task = tokio::spawn(async move {
1077 use crate::network::NetworkPeerEvent;
1078 loop {
1079 match auth_rx.recv().await {
1080 Ok(NetworkPeerEvent::AuthRequired { url }) => {
1081 on_auth(url);
1082 }
1083 Err(broadcast::error::RecvError::Closed) => break,
1084 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1085 _ => {} }
1087 }
1088 });
1089
1090 let start_result = provider.start().await.map_err(NodeError::Network);
1092
1093 auth_task.abort();
1095
1096 start_result?;
1097
1098 let network = Arc::new(provider);
1099
1100 let ws_config = WsConfig {
1102 port: ws_port,
1103 ..Default::default()
1104 };
1105 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1106
1107 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1109 session.start().await;
1110
1111 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1113 let node = Node::from_parts(network, session, codec).with_state_dir(state_dir);
1114
1115 tracing::info!("node: started successfully (with auth handler)");
1116 Ok(node)
1117 }
1118}
1119
1120#[cfg(test)]
1125mod tests {
1126 use super::*;
1127 use crate::network::{
1128 HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
1129 NetworkTcpListener, NetworkUdpSocket, PeerAddr,
1130 };
1131 use crate::transport::WsConfig;
1132 use serde_json::json;
1133 use std::time::Duration;
1134 use tokio::sync::{broadcast, mpsc};
1135
1136 struct MockNetworkProvider {
1139 identity: NodeIdentity,
1140 local_addr: PeerAddr,
1141 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
1142 mock_peers: Arc<RwLock<Vec<NetworkPeer>>>,
1144 }
1145
1146 impl MockNetworkProvider {
1147 fn new(id: &str) -> Self {
1148 let (peer_event_tx, _) = broadcast::channel(64);
1149 Self {
1150 identity: NodeIdentity {
1151 app_id: "test".to_string(),
1152 device_id: id.to_string(),
1155 device_name: format!("Test Node {id}"),
1156 tailscale_hostname: format!("truffle-test-{id}"),
1157 tailscale_id: id.to_string(),
1158 dns_name: None,
1159 ip: Some("127.0.0.1".parse().unwrap()),
1160 },
1161 local_addr: PeerAddr {
1162 ip: Some("127.0.0.1".parse().unwrap()),
1163 hostname: format!("truffle-test-{id}"),
1164 dns_name: None,
1165 },
1166 peer_event_tx,
1167 mock_peers: Arc::new(RwLock::new(Vec::new())),
1168 }
1169 }
1170
1171 fn event_sender(&self) -> broadcast::Sender<NetworkPeerEvent> {
1172 self.peer_event_tx.clone()
1173 }
1174 }
1175
1176 impl NetworkProvider for MockNetworkProvider {
1177 async fn start(&mut self) -> Result<(), NetworkError> {
1178 Ok(())
1179 }
1180
1181 async fn stop(&mut self) -> Result<(), NetworkError> {
1182 Ok(())
1183 }
1184
1185 fn local_identity(&self) -> NodeIdentity {
1186 self.identity.clone()
1187 }
1188
1189 fn local_addr(&self) -> PeerAddr {
1190 self.local_addr.clone()
1191 }
1192
1193 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
1194 self.peer_event_tx.subscribe()
1195 }
1196
1197 async fn peers(&self) -> Vec<NetworkPeer> {
1198 self.mock_peers.read().await.clone()
1199 }
1200
1201 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
1202 let target = format!("{addr}:{port}");
1203 TcpStream::connect(&target)
1204 .await
1205 .map_err(|e| NetworkError::DialFailed(format!("mock dial {target}: {e}")))
1206 }
1207
1208 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
1209 let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
1210 .await
1211 .map_err(|e| NetworkError::ListenFailed(format!("mock listen :{port}: {e}")))?;
1212
1213 let actual_port = listener.local_addr().unwrap().port();
1214 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
1215
1216 tokio::spawn(async move {
1217 loop {
1218 match listener.accept().await {
1219 Ok((stream, addr)) => {
1220 let conn = IncomingConnection {
1221 stream,
1222 remote_addr: addr.to_string(),
1223 remote_identity: String::new(),
1224 port: actual_port,
1225 };
1226 if tx.send(conn).await.is_err() {
1227 break;
1228 }
1229 }
1230 Err(e) => {
1231 tracing::debug!("mock listener error: {e}");
1232 break;
1233 }
1234 }
1235 }
1236 });
1237
1238 Ok(NetworkTcpListener {
1239 port: actual_port,
1240 incoming: rx,
1241 })
1242 }
1243
1244 async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
1245 Ok(())
1246 }
1247
1248 async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
1249 Err(NetworkError::Internal("mock: UDP not supported".into()))
1250 }
1251
1252 async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
1253 Ok(PingResult {
1254 latency: Duration::from_millis(1),
1255 connection: "direct".to_string(),
1256 peer_addr: None,
1257 })
1258 }
1259
1260 async fn health(&self) -> HealthInfo {
1261 HealthInfo {
1262 state: "running".to_string(),
1263 healthy: true,
1264 ..Default::default()
1265 }
1266 }
1267 }
1268
1269 fn make_loopback_peer(id: &str) -> NetworkPeer {
1272 NetworkPeer {
1273 id: id.to_string(),
1274 hostname: format!("truffle-test-{id}"),
1275 ip: "127.0.0.1".parse().unwrap(),
1276 online: true,
1277 cur_addr: Some("127.0.0.1:41641".to_string()),
1278 relay: None,
1279 os: Some("linux".to_string()),
1280 last_seen: Some("2026-03-25T12:00:00Z".to_string()),
1281 key_expiry: None,
1282 dns_name: None,
1283 }
1284 }
1285
1286 fn ws_config(port: u16) -> WsConfig {
1287 WsConfig {
1288 port,
1289 ping_interval: Duration::from_secs(300),
1290 pong_timeout: Duration::from_secs(300),
1291 ..Default::default()
1292 }
1293 }
1294
1295 async fn random_port() -> u16 {
1296 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1297 l.local_addr().unwrap().port()
1298 }
1299
1300 async fn make_test_node(
1302 id: &str,
1303 ws_port: u16,
1304 ) -> (
1305 Node<MockNetworkProvider>,
1306 broadcast::Sender<NetworkPeerEvent>,
1307 Arc<MockNetworkProvider>,
1308 ) {
1309 let provider = MockNetworkProvider::new(id);
1310 let event_tx = provider.event_sender();
1311 let network = Arc::new(provider);
1312 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config(ws_port)));
1313 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1314 session.start().await;
1315
1316 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1317 let node = Node::from_parts(network.clone(), session, codec);
1318
1319 (node, event_tx, network)
1320 }
1321
1322 #[tokio::test]
1325 async fn test_node_builder_creates_node() {
1326 let ws_port = random_port().await;
1327 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1328
1329 let identity = node.local_info();
1330 assert_eq!(identity.tailscale_id, "node-1");
1331 assert_eq!(identity.device_id, "node-1");
1332 assert!(identity.tailscale_hostname.contains("node-1"));
1333 }
1334
1335 #[tokio::test]
1336 async fn test_node_peers_from_network() {
1337 let ws_port = random_port().await;
1338 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1339
1340 let peers = node.peers().await;
1342 assert!(peers.is_empty());
1343
1344 let peer = make_loopback_peer("peer-a");
1346 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1347 tokio::time::sleep(Duration::from_millis(50)).await;
1348
1349 let peers = node.peers().await;
1350 assert_eq!(peers.len(), 1);
1351 assert_eq!(peers[0].id, "peer-a");
1352 assert!(peers[0].online);
1353 assert!(!peers[0].ws_connected);
1354 }
1355
1356 #[tokio::test]
1357 async fn test_node_send_to_unknown_peer_errors() {
1358 let ws_port = random_port().await;
1359 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1360
1361 let result = node.send("nonexistent", "test", b"hello").await;
1362 assert!(result.is_err());
1363 let err_str = result.unwrap_err().to_string();
1364 assert!(
1365 err_str.contains("unknown peer") || err_str.contains("not found"),
1366 "expected unknown peer error, got: {err_str}"
1367 );
1368 }
1369
1370 #[tokio::test]
1371 async fn test_node_send_wraps_in_envelope() {
1372 let codec = JsonCodec;
1375 let data = b"hello world";
1376 let envelope = Envelope::new("test-ns", "message", serde_json::Value::from(data.to_vec()))
1377 .with_timestamp();
1378
1379 let encoded = codec.encode(&envelope).unwrap();
1380 let decoded = codec.decode(&encoded).unwrap();
1381
1382 assert_eq!(decoded.namespace, "test-ns");
1383 assert_eq!(decoded.msg_type, "message");
1384 assert!(decoded.timestamp.is_some());
1385 }
1386
1387 #[tokio::test]
1388 async fn test_node_subscribe_filters_by_namespace() {
1389 let ws_port = random_port().await;
1390 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1391
1392 let _rx_chat = node.subscribe("chat");
1394 let _rx_ft = node.subscribe("ft");
1395
1396 let _rx_chat2 = node.subscribe("chat");
1398 }
1399
1400 #[tokio::test]
1401 async fn test_node_broadcast() {
1402 let ws_port = random_port().await;
1403 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1404
1405 node.broadcast("test", b"hello everyone").await;
1407 }
1408
1409 #[tokio::test]
1410 async fn test_node_open_tcp_resolves_peer() {
1411 let ws_port = random_port().await;
1412 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1413
1414 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1416 let tcp_port = listener.local_addr().unwrap().port();
1417
1418 let peer = make_loopback_peer("peer-tcp");
1420 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1421 tokio::time::sleep(Duration::from_millis(50)).await;
1422
1423 let accept_handle = tokio::spawn(async move {
1425 let (stream, _) = listener.accept().await.unwrap();
1426 stream
1427 });
1428
1429 let stream = node.open_tcp("peer-tcp", tcp_port).await;
1431 assert!(stream.is_ok(), "open_tcp failed: {:?}", stream.err());
1432
1433 let _ = accept_handle.await;
1434 }
1435
1436 #[tokio::test]
1437 async fn test_node_open_tcp_unknown_peer_errors() {
1438 let ws_port = random_port().await;
1439 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1440
1441 let result = node.open_tcp("nonexistent", 8080).await;
1442 assert!(result.is_err());
1443 let err_str = result.unwrap_err().to_string();
1444 assert!(
1445 err_str.contains("not found"),
1446 "expected peer not found error, got: {err_str}"
1447 );
1448 }
1449
1450 #[tokio::test]
1451 async fn test_node_ping_resolves_peer() {
1452 let ws_port = random_port().await;
1453 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1454
1455 let result = node.ping("peer-ping").await;
1457 assert!(result.is_err());
1458
1459 let peer = make_loopback_peer("peer-ping");
1461 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1462 tokio::time::sleep(Duration::from_millis(50)).await;
1463
1464 let result = node.ping("peer-ping").await;
1466 assert!(result.is_ok());
1467 assert_eq!(result.unwrap().latency, Duration::from_millis(1));
1468 }
1469
1470 #[tokio::test]
1471 async fn test_node_health() {
1472 let ws_port = random_port().await;
1473 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1474
1475 let health = node.health().await;
1476 assert!(health.healthy);
1477 assert_eq!(health.state, "running");
1478 }
1479
1480 #[tokio::test]
1481 async fn test_node_open_quic_not_implemented() {
1482 let ws_port = random_port().await;
1483 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1484
1485 let result = node.open_quic("peer").await;
1486 assert!(matches!(result, Err(NodeError::NotImplemented(_))));
1487 }
1488
1489 #[tokio::test]
1490 async fn test_node_open_udp_not_implemented() {
1491 let ws_port = random_port().await;
1492 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1493
1494 let result = node.open_udp("peer").await;
1495 assert!(matches!(result, Err(NodeError::NotImplemented(_))));
1496 }
1497
1498 #[tokio::test]
1499 async fn test_node_listen_tcp() {
1500 let ws_port = random_port().await;
1501 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1502
1503 let listener = node.listen_tcp(0).await;
1505 assert!(listener.is_ok(), "listen_tcp failed: {:?}", listener.err());
1506 }
1507
1508 #[tokio::test]
1509 async fn test_envelope_serialize_deserialize() {
1510 let envelope = Envelope::new("chat", "message", json!({"text": "hello"})).with_timestamp();
1511
1512 let bytes = envelope.serialize().unwrap();
1513 let decoded = Envelope::deserialize(&bytes).unwrap();
1514
1515 assert_eq!(decoded.namespace, "chat");
1516 assert_eq!(decoded.msg_type, "message");
1517 assert_eq!(decoded.payload["text"], "hello");
1518 assert!(decoded.timestamp.is_some());
1519 }
1520
1521 #[tokio::test]
1522 async fn test_envelope_codec_json() {
1523 let codec = JsonCodec;
1524 let envelope = Envelope::new("ft", "offer", json!({"file": "test.bin"}));
1525
1526 let encoded = codec.encode(&envelope).unwrap();
1527 let decoded = codec.decode(&encoded).unwrap();
1528
1529 assert_eq!(decoded.namespace, "ft");
1530 assert_eq!(decoded.payload["file"], "test.bin");
1531 }
1532
1533 #[tokio::test]
1534 async fn test_envelope_unknown_fields_ignored() {
1535 let json_bytes = br#"{
1536 "namespace": "v2",
1537 "msg_type": "new",
1538 "payload": {},
1539 "future_field": "ignored"
1540 }"#;
1541
1542 let codec = JsonCodec;
1543 let decoded = codec.decode(json_bytes).unwrap();
1544 assert_eq!(decoded.namespace, "v2");
1545 assert_eq!(decoded.msg_type, "new");
1546 }
1547
1548 #[tokio::test]
1549 async fn test_node_send_and_receive_roundtrip() {
1550 let port_a = random_port().await;
1552 let port_b = random_port().await;
1553
1554 let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
1555 let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
1556
1557 let peer_b = NetworkPeer {
1559 id: "node-b".to_string(),
1560 hostname: "truffle-test-node-b".to_string(),
1561 ip: "127.0.0.1".parse().unwrap(),
1562 online: true,
1563 cur_addr: Some("127.0.0.1:41641".to_string()),
1564 relay: None,
1565 os: None,
1566 last_seen: None,
1567 key_expiry: None,
1568 dns_name: None,
1569 };
1570 let peer_a = NetworkPeer {
1571 id: "node-a".to_string(),
1572 hostname: "truffle-test-node-a".to_string(),
1573 ip: "127.0.0.1".parse().unwrap(),
1574 online: true,
1575 cur_addr: Some("127.0.0.1:41641".to_string()),
1576 relay: None,
1577 os: None,
1578 last_seen: None,
1579 key_expiry: None,
1580 dns_name: None,
1581 };
1582
1583 let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
1584 let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
1585 tokio::time::sleep(Duration::from_millis(100)).await;
1586
1587 let mut rx = node_b.subscribe("test");
1589
1590 let send_result = node_a.send("node-b", "test", b"hello from a").await;
1593
1594 if send_result.is_ok() {
1602 let msg = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await;
1604 if let Ok(Ok(msg)) = msg {
1605 assert_eq!(msg.namespace, "test");
1606 }
1607 }
1608 }
1611
1612 async fn inject_peer_with_identity(
1618 node: &Node<MockNetworkProvider>,
1619 event_tx: &broadcast::Sender<NetworkPeerEvent>,
1620 tailscale_id: &str,
1621 device_id: &str,
1622 device_name: &str,
1623 ) {
1624 let peer = make_loopback_peer(tailscale_id);
1625 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1626 tokio::time::sleep(Duration::from_millis(30)).await;
1627
1628 let identity = crate::session::PeerIdentity {
1629 app_id: "test".into(),
1630 device_id: device_id.into(),
1631 device_name: device_name.into(),
1632 os: "linux".into(),
1633 tailscale_id: tailscale_id.into(),
1634 };
1635 assert!(
1636 node.session
1637 .test_stamp_identity(tailscale_id, identity)
1638 .await,
1639 "peer {tailscale_id} should exist in session registry before stamping identity"
1640 );
1641 }
1642
1643 #[tokio::test]
1644 async fn test_resolve_peer_id_by_device_id() {
1645 let ws_port = random_port().await;
1646 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1647
1648 inject_peer_with_identity(
1649 &node,
1650 &event_tx,
1651 "tailscale-abc",
1652 "01HZZZZZZZZZZZZZZZZZZZZZZZ",
1653 "Alice MacBook",
1654 )
1655 .await;
1656
1657 let resolved = node
1658 .resolve_peer_id("01HZZZZZZZZZZZZZZZZZZZZZZZ")
1659 .await
1660 .unwrap();
1661 assert_eq!(resolved, "01HZZZZZZZZZZZZZZZZZZZZZZZ");
1662 }
1663
1664 #[tokio::test]
1665 async fn test_resolve_peer_id_by_device_name() {
1666 let ws_port = random_port().await;
1667 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1668
1669 inject_peer_with_identity(
1670 &node,
1671 &event_tx,
1672 "tailscale-abc",
1673 "01HXYZXYZXYZXYZXYZXYZXYZXY",
1674 "Bob's Mac",
1675 )
1676 .await;
1677
1678 let resolved = node.resolve_peer_id("Bob's Mac").await.unwrap();
1679 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
1680 }
1681
1682 #[tokio::test]
1683 async fn test_resolve_peer_id_by_device_id_prefix() {
1684 let ws_port = random_port().await;
1685 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1686
1687 inject_peer_with_identity(
1688 &node,
1689 &event_tx,
1690 "tailscale-abc",
1691 "01HXYZXYZXYZXYZXYZXYZXYZXY",
1692 "laptop",
1693 )
1694 .await;
1695
1696 let resolved = node.resolve_peer_id("01HX").await.unwrap();
1699 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
1700 }
1701
1702 #[tokio::test]
1703 async fn test_resolve_peer_id_by_tailscale_id_legacy() {
1704 let ws_port = random_port().await;
1708 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1709
1710 inject_peer_with_identity(
1711 &node,
1712 &event_tx,
1713 "tailscale-legacy",
1714 "01HLEGACY0000000000000000X",
1715 "legacy box",
1716 )
1717 .await;
1718
1719 let resolved = node.resolve_peer_id("tailscale-legacy").await.unwrap();
1720 assert_eq!(resolved, "01HLEGACY0000000000000000X");
1721 }
1722
1723 #[tokio::test]
1724 async fn test_resolve_peer_id_unknown() {
1725 let ws_port = random_port().await;
1726 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
1727 let result = node.resolve_peer_id("nope").await;
1728 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
1729 }
1730}