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}
256
257impl<N: NetworkProvider + 'static> Node<N> {
258 pub(crate) fn from_parts(
264 network: Arc<N>,
265 session: Arc<PeerRegistry<N>>,
266 codec: Arc<dyn EnvelopeCodec>,
267 ) -> Self {
268 let (incoming_tx, _) = broadcast::channel(1024);
269 let namespace_filters: Arc<
270 StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>,
271 > = Arc::new(StdRwLock::new(HashMap::new()));
272
273 let node = Self {
274 network,
275 session: session.clone(),
276 codec: codec.clone(),
277 incoming_tx: incoming_tx.clone(),
278 namespace_filters: namespace_filters.clone(),
279 file_transfer_state: FileTransferState::new(),
280 };
281
282 node.spawn_envelope_router(session, codec, incoming_tx, namespace_filters);
284
285 node
286 }
287
288 fn spawn_envelope_router(
292 &self,
293 session: Arc<PeerRegistry<N>>,
294 codec: Arc<dyn EnvelopeCodec>,
295 incoming_tx: broadcast::Sender<NamespacedMessage>,
296 namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
297 ) {
298 let mut rx = session.subscribe();
299
300 tokio::spawn(async move {
301 loop {
302 match rx.recv().await {
303 Ok(msg) => {
304 if let Ok(envelope) = codec.decode(&msg.data) {
305 let namespaced = NamespacedMessage {
306 from: msg.from,
307 namespace: envelope.namespace.clone(),
308 msg_type: envelope.msg_type,
309 payload: envelope.payload,
310 timestamp: envelope.timestamp,
311 };
312
313 tracing::debug!(
314 from = %namespaced.from,
315 namespace = %namespaced.namespace,
316 msg_type = %namespaced.msg_type,
317 "envelope router: dispatching message"
318 );
319
320 let _ = incoming_tx.send(namespaced.clone());
322
323 let filters = namespace_filters
327 .read()
328 .expect("namespace_filters std RwLock poisoned");
329 let _has_subscriber = filters.contains_key(&namespaced.namespace);
330 if let Some(tx) = filters.get(&namespaced.namespace) {
331 let send_result = tx.send(namespaced);
332 tracing::debug!(
333 namespace = %envelope.namespace,
334 subscriber_count = tx.receiver_count(),
335 sent = send_result.is_ok(),
336 "envelope router: sent to namespace subscriber"
337 );
338 } else {
339 tracing::debug!(
340 namespace = %envelope.namespace,
341 "envelope router: no subscriber for namespace"
342 );
343 }
344 } else {
345 tracing::warn!(
346 from = %msg.from,
347 data_len = msg.data.len(),
348 "node: failed to decode envelope from incoming message"
349 );
350 }
351 }
352 Err(broadcast::error::RecvError::Lagged(n)) => {
353 tracing::warn!(
354 missed = n,
355 "node: envelope router lagged, missed {n} messages"
356 );
357 continue;
358 }
359 Err(broadcast::error::RecvError::Closed) => {
360 tracing::debug!("node: session incoming channel closed, router exiting");
361 break;
362 }
363 }
364 }
365 });
366 }
367
368 pub fn builder() -> NodeBuilder {
372 NodeBuilder::default()
373 }
374
375 pub fn file_transfer(&self) -> file_transfer::FileTransfer<'_, N> {
382 file_transfer::FileTransfer::new(self)
383 }
384
385 pub fn synced_store<T>(
394 self: &Arc<Self>,
395 store_id: &str,
396 ) -> Arc<crate::synced_store::SyncedStore<T>>
397 where
398 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
399 {
400 crate::synced_store::SyncedStore::new(self.clone(), store_id)
401 }
402
403 pub fn synced_store_with_backend<T>(
408 self: &Arc<Self>,
409 store_id: &str,
410 backend: std::sync::Arc<dyn crate::synced_store::StoreBackend>,
411 ) -> Arc<crate::synced_store::SyncedStore<T>>
412 where
413 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
414 {
415 crate::synced_store::SyncedStore::new_with_backend(self.clone(), store_id, backend)
416 }
417
418 pub async fn stop(&self) {
426 tracing::info!("node: stopping");
427 }
431
432 pub fn local_info(&self) -> NodeIdentity {
436 self.network.local_identity()
437 }
438
439 pub async fn peers(&self) -> Vec<Peer> {
446 self.session
447 .peers()
448 .await
449 .into_iter()
450 .map(Peer::from)
451 .collect()
452 }
453
454 pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
456 self.session.on_peer_change()
457 }
458
459 pub async fn resolve_peer_id(&self, peer_id: &str) -> Result<String, NodeError> {
474 let peers = self.session.peers().await;
475
476 for p in &peers {
479 if let Some(identity) = p.identity.as_ref() {
480 if identity.device_id == peer_id || identity.device_name == peer_id {
481 return Ok(identity.device_id.clone());
482 }
483 }
484 if p.id == peer_id || p.name == peer_id {
485 return Ok(p
489 .identity
490 .as_ref()
491 .map(|i| i.device_id.clone())
492 .unwrap_or_else(|| p.id.clone()));
493 }
494 }
495
496 if peer_id.len() >= 4 {
500 let prefix_hits: Vec<&PeerState> = peers
501 .iter()
502 .filter(|p| {
503 p.identity
504 .as_ref()
505 .map(|i| i.device_id.starts_with(peer_id))
506 .unwrap_or(false)
507 })
508 .collect();
509 if prefix_hits.len() == 1 {
510 if let Some(identity) = prefix_hits[0].identity.as_ref() {
511 return Ok(identity.device_id.clone());
512 }
513 }
514 }
515
516 Err(NodeError::PeerNotFound(peer_id.to_string()))
517 }
518
519 pub async fn ping(&self, peer_id: &str) -> Result<PingResult, NodeError> {
526 let peers = self.session.peers().await;
527 let peer = peers
528 .iter()
529 .find(|p| {
530 p.id == peer_id
531 || p.name == peer_id
532 || p.identity
533 .as_ref()
534 .map(|i| i.device_id == peer_id || i.device_name == peer_id)
535 .unwrap_or(false)
536 })
537 .ok_or_else(|| NodeError::PeerNotFound(peer_id.to_string()))?;
538
539 let addr = peer.ip.to_string();
540 self.network.ping(&addr).await.map_err(NodeError::Network)
541 }
542
543 pub async fn health(&self) -> HealthInfo {
545 self.network.health().await
546 }
547
548 pub async fn send(&self, peer_id: &str, namespace: &str, data: &[u8]) -> Result<(), NodeError> {
556 let payload = std::str::from_utf8(data)
562 .ok()
563 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
564 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
565
566 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
567
568 let encoded = self.codec.encode(&envelope)?;
569 self.session.send(peer_id, &encoded).await?;
570 Ok(())
571 }
572
573 pub async fn send_typed(
581 &self,
582 peer_id: &str,
583 namespace: &str,
584 msg_type: &str,
585 payload: &serde_json::Value,
586 ) -> Result<(), NodeError> {
587 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
588 let encoded = self.codec.encode(&envelope)?;
589 self.session.send(peer_id, &encoded).await?;
590 Ok(())
591 }
592
593 pub async fn broadcast_typed(
596 &self,
597 namespace: &str,
598 msg_type: &str,
599 payload: &serde_json::Value,
600 ) {
601 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
602 match self.codec.encode(&envelope) {
603 Ok(encoded) => {
604 self.session.broadcast(&encoded).await;
605 }
606 Err(e) => {
607 tracing::error!("node: failed to encode broadcast envelope: {e}");
608 }
609 }
610 }
611
612 pub async fn broadcast(&self, namespace: &str, data: &[u8]) {
617 let payload = std::str::from_utf8(data)
618 .ok()
619 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
620 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
621
622 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
623
624 match self.codec.encode(&envelope) {
625 Ok(encoded) => {
626 self.session.broadcast(&encoded).await;
627 }
628 Err(e) => {
629 tracing::error!("node: failed to encode broadcast envelope: {e}");
630 }
631 }
632 }
633
634 pub fn subscribe(&self, namespace: &str) -> broadcast::Receiver<NamespacedMessage> {
640 {
642 let filters = self
643 .namespace_filters
644 .read()
645 .expect("namespace_filters std RwLock poisoned");
646 if let Some(tx) = filters.get(namespace) {
647 return tx.subscribe();
648 }
649 }
650
651 let mut filters = self
653 .namespace_filters
654 .write()
655 .expect("namespace_filters std RwLock poisoned");
656 if let Some(tx) = filters.get(namespace) {
658 return tx.subscribe();
659 }
660 let (tx, rx) = broadcast::channel(256);
661 filters.insert(namespace.to_string(), tx);
662 rx
663 }
664
665 pub async fn open_tcp(&self, peer_id: &str, port: u16) -> Result<TcpStream, NodeError> {
674 let peers = self.session.peers().await;
675 let peer = peers
676 .iter()
677 .find(|p| {
678 p.id == peer_id
679 || p.name == peer_id
680 || p.identity
681 .as_ref()
682 .map(|i| i.device_id == peer_id || i.device_name == peer_id)
683 .unwrap_or(false)
684 })
685 .ok_or_else(|| NodeError::PeerNotFound(peer_id.to_string()))?;
686
687 let addr = peer.ip.to_string();
688 self.network
689 .dial_tcp(&addr, port)
690 .await
691 .map_err(|e| NodeError::ConnectionFailed(e.to_string()))
692 }
693
694 pub async fn listen_tcp(&self, port: u16) -> Result<RawListener, NodeError> {
699 use crate::transport::tcp::TcpTransport;
700 use crate::transport::RawTransport;
701
702 let tcp = TcpTransport::new(self.network.clone());
703 tcp.listen(port).await.map_err(NodeError::Transport)
704 }
705
706 pub async fn open_quic(&self, _peer_id: &str) -> Result<(), NodeError> {
710 Err(NodeError::NotImplemented(
711 "QUIC connections are not yet implemented".to_string(),
712 ))
713 }
714
715 pub async fn open_udp(&self, _peer_id: &str) -> Result<NetworkUdpSocket, NodeError> {
719 Err(NodeError::NotImplemented(
720 "UDP sockets are not yet implemented".to_string(),
721 ))
722 }
723}
724
725#[derive(Debug, Clone)]
746pub struct NodeBuilder {
747 app_id: Option<AppId>,
748 device_name: Option<DeviceName>,
749 device_id: Option<DeviceId>,
750 sidecar_path: Option<PathBuf>,
751 state_dir: Option<String>,
752 auth_key: Option<String>,
753 ephemeral: bool,
754 ws_port: u16,
755}
756
757fn atomic_write_string(path: &Path, content: &str) -> std::io::Result<()> {
766 let _parent = path.parent().ok_or_else(|| {
767 std::io::Error::new(
768 std::io::ErrorKind::InvalidInput,
769 "path has no parent directory",
770 )
771 })?;
772 let mut tmp = path.to_path_buf();
773 tmp.set_extension("tmp");
774 std::fs::write(&tmp, content)?;
775 #[cfg(unix)]
776 {
777 std::fs::rename(&tmp, path)?;
778 }
779 #[cfg(windows)]
780 {
781 let _ = std::fs::remove_file(path);
784 std::fs::rename(&tmp, path)?;
785 }
786 Ok(())
787}
788
789impl Default for NodeBuilder {
790 fn default() -> Self {
791 Self {
792 app_id: None,
793 device_name: None,
794 device_id: None,
795 sidecar_path: None,
796 state_dir: None,
797 auth_key: None,
798 ephemeral: false,
799 ws_port: 9417,
800 }
801 }
802}
803
804impl NodeBuilder {
805 pub fn app_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
810 let raw: String = s.into();
811 let app_id = AppId::parse(&raw)
812 .map_err(|e| NodeError::BuildError(format!("invalid app_id: {e}")))?;
813 self.app_id = Some(app_id);
814 Ok(self)
815 }
816
817 pub fn device_name(mut self, s: impl Into<String>) -> Self {
822 self.device_name = Some(DeviceName::parse(s));
823 self
824 }
825
826 pub fn device_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
832 let raw: String = s.into();
833 let device_id = DeviceId::parse(&raw)
834 .map_err(|e| NodeError::BuildError(format!("invalid device_id: {e}")))?;
835 self.device_id = Some(device_id);
836 Ok(self)
837 }
838
839 pub fn sidecar_path(mut self, path: impl Into<PathBuf>) -> Self {
841 self.sidecar_path = Some(path.into());
842 self
843 }
844
845 pub fn state_dir(mut self, dir: &str) -> Self {
847 self.state_dir = Some(dir.to_string());
848 self
849 }
850
851 pub fn auth_key(mut self, key: &str) -> Self {
853 self.auth_key = Some(key.to_string());
854 self
855 }
856
857 pub fn ephemeral(mut self, val: bool) -> Self {
859 self.ephemeral = val;
860 self
861 }
862
863 pub fn ws_port(mut self, port: u16) -> Self {
865 self.ws_port = port;
866 self
867 }
868
869 fn prepare_config(&self) -> Result<TailscaleConfig, NodeError> {
876 let binary_path = self
878 .sidecar_path
879 .clone()
880 .ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
881
882 let app_id = self
884 .app_id
885 .clone()
886 .ok_or_else(|| NodeError::BuildError("app_id is required".into()))?;
887
888 let device_name = match self.device_name.clone() {
890 Some(name) => name,
891 None => {
892 let os_hostname = hostname::get()
893 .map_err(|e| {
894 NodeError::BuildError(format!(
895 "device_name is unset and hostname::get() failed: {e}"
896 ))
897 })?
898 .to_string_lossy()
899 .into_owned();
900 DeviceName::parse(os_hostname)
901 }
902 };
903
904 let tailscale_host = identity::tailscale_hostname(&app_id, &device_name);
907
908 let state_dir = self.state_dir.clone().unwrap_or_else(|| {
911 let base = dirs::data_dir().unwrap_or_else(|| {
912 tracing::warn!(
913 "dirs::data_dir() returned None, falling back to std::env::temp_dir()"
914 );
915 std::env::temp_dir()
916 });
917 base.join("truffle")
918 .join(app_id.as_str())
919 .join(identity::slug(device_name.as_str(), 255))
920 .to_string_lossy()
921 .into_owned()
922 });
923
924 std::fs::create_dir_all(&state_dir)?;
926
927 let device_id_file = Path::new(&state_dir).join("device-id.txt");
932 let device_id = match self.device_id.clone() {
933 Some(id) => {
934 atomic_write_string(&device_id_file, id.as_str())?;
936 id
937 }
938 None => {
939 if device_id_file.exists() {
940 let s = std::fs::read_to_string(&device_id_file)?.trim().to_string();
941 DeviceId::parse(&s).map_err(|e| {
942 NodeError::BuildError(format!(
943 "device-id.txt at {device_id_file:?} contains an invalid ULID: {e}"
944 ))
945 })?
946 } else {
947 let id = DeviceId::generate();
948 atomic_write_string(&device_id_file, id.as_str())?;
949 id
950 }
951 }
952 };
953
954 Ok(TailscaleConfig {
955 binary_path,
956 app_id: app_id.as_str().to_string(),
957 device_id: device_id.as_str().to_string(),
958 device_name: device_name.as_str().to_string(),
959 hostname: tailscale_host,
960 state_dir,
961 auth_key: self.auth_key.clone(),
962 ephemeral: if self.ephemeral { Some(true) } else { None },
963 tags: None,
964 })
965 }
966
967 pub async fn build(self) -> Result<Node<TailscaleProvider>, NodeError> {
978 let ws_port = self.ws_port;
979 let config = self.prepare_config()?;
980
981 let mut provider = TailscaleProvider::new(config);
982 provider.start().await.map_err(NodeError::Network)?;
983
984 let network = Arc::new(provider);
985
986 let ws_config = WsConfig {
988 port: ws_port,
989 ..Default::default()
990 };
991 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
992
993 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
995 session.start().await;
996
997 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
999 let node = Node::from_parts(network, session, codec);
1000
1001 tracing::info!("node: started successfully");
1002 Ok(node)
1003 }
1004
1005 pub async fn build_with_auth_handler(
1017 self,
1018 on_auth: impl Fn(String) + Send + 'static,
1019 ) -> Result<Node<TailscaleProvider>, NodeError> {
1020 let ws_port = self.ws_port;
1021 let config = self.prepare_config()?;
1022
1023 let mut provider = TailscaleProvider::new(config);
1024
1025 let mut auth_rx = provider.peer_events();
1027
1028 let auth_task = tokio::spawn(async move {
1030 use crate::network::NetworkPeerEvent;
1031 loop {
1032 match auth_rx.recv().await {
1033 Ok(NetworkPeerEvent::AuthRequired { url }) => {
1034 on_auth(url);
1035 }
1036 Err(broadcast::error::RecvError::Closed) => break,
1037 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1038 _ => {} }
1040 }
1041 });
1042
1043 let start_result = provider.start().await.map_err(NodeError::Network);
1045
1046 auth_task.abort();
1048
1049 start_result?;
1050
1051 let network = Arc::new(provider);
1052
1053 let ws_config = WsConfig {
1055 port: ws_port,
1056 ..Default::default()
1057 };
1058 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1059
1060 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1062 session.start().await;
1063
1064 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1066 let node = Node::from_parts(network, session, codec);
1067
1068 tracing::info!("node: started successfully (with auth handler)");
1069 Ok(node)
1070 }
1071}
1072
1073#[cfg(test)]
1078mod tests {
1079 use super::*;
1080 use crate::network::{
1081 HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
1082 NetworkTcpListener, NetworkUdpSocket, PeerAddr,
1083 };
1084 use crate::transport::WsConfig;
1085 use serde_json::json;
1086 use std::time::Duration;
1087 use tokio::sync::{broadcast, mpsc};
1088
1089 struct MockNetworkProvider {
1092 identity: NodeIdentity,
1093 local_addr: PeerAddr,
1094 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
1095 mock_peers: Arc<RwLock<Vec<NetworkPeer>>>,
1097 }
1098
1099 impl MockNetworkProvider {
1100 fn new(id: &str) -> Self {
1101 let (peer_event_tx, _) = broadcast::channel(64);
1102 Self {
1103 identity: NodeIdentity {
1104 app_id: "test".to_string(),
1105 device_id: id.to_string(),
1108 device_name: format!("Test Node {id}"),
1109 tailscale_hostname: format!("truffle-test-{id}"),
1110 tailscale_id: id.to_string(),
1111 dns_name: None,
1112 ip: Some("127.0.0.1".parse().unwrap()),
1113 },
1114 local_addr: PeerAddr {
1115 ip: Some("127.0.0.1".parse().unwrap()),
1116 hostname: format!("truffle-test-{id}"),
1117 dns_name: None,
1118 },
1119 peer_event_tx,
1120 mock_peers: Arc::new(RwLock::new(Vec::new())),
1121 }
1122 }
1123
1124 fn event_sender(&self) -> broadcast::Sender<NetworkPeerEvent> {
1125 self.peer_event_tx.clone()
1126 }
1127 }
1128
1129 impl NetworkProvider for MockNetworkProvider {
1130 async fn start(&mut self) -> Result<(), NetworkError> {
1131 Ok(())
1132 }
1133
1134 async fn stop(&mut self) -> Result<(), NetworkError> {
1135 Ok(())
1136 }
1137
1138 fn local_identity(&self) -> NodeIdentity {
1139 self.identity.clone()
1140 }
1141
1142 fn local_addr(&self) -> PeerAddr {
1143 self.local_addr.clone()
1144 }
1145
1146 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
1147 self.peer_event_tx.subscribe()
1148 }
1149
1150 async fn peers(&self) -> Vec<NetworkPeer> {
1151 self.mock_peers.read().await.clone()
1152 }
1153
1154 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
1155 let target = format!("{addr}:{port}");
1156 TcpStream::connect(&target)
1157 .await
1158 .map_err(|e| NetworkError::DialFailed(format!("mock dial {target}: {e}")))
1159 }
1160
1161 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
1162 let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
1163 .await
1164 .map_err(|e| NetworkError::ListenFailed(format!("mock listen :{port}: {e}")))?;
1165
1166 let actual_port = listener.local_addr().unwrap().port();
1167 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
1168
1169 tokio::spawn(async move {
1170 loop {
1171 match listener.accept().await {
1172 Ok((stream, addr)) => {
1173 let conn = IncomingConnection {
1174 stream,
1175 remote_addr: addr.to_string(),
1176 remote_identity: String::new(),
1177 port: actual_port,
1178 };
1179 if tx.send(conn).await.is_err() {
1180 break;
1181 }
1182 }
1183 Err(e) => {
1184 tracing::debug!("mock listener error: {e}");
1185 break;
1186 }
1187 }
1188 }
1189 });
1190
1191 Ok(NetworkTcpListener {
1192 port: actual_port,
1193 incoming: rx,
1194 })
1195 }
1196
1197 async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
1198 Ok(())
1199 }
1200
1201 async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
1202 Err(NetworkError::Internal("mock: UDP not supported".into()))
1203 }
1204
1205 async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
1206 Ok(PingResult {
1207 latency: Duration::from_millis(1),
1208 connection: "direct".to_string(),
1209 peer_addr: None,
1210 })
1211 }
1212
1213 async fn health(&self) -> HealthInfo {
1214 HealthInfo {
1215 state: "running".to_string(),
1216 healthy: true,
1217 ..Default::default()
1218 }
1219 }
1220 }
1221
1222 fn make_loopback_peer(id: &str) -> NetworkPeer {
1225 NetworkPeer {
1226 id: id.to_string(),
1227 hostname: format!("truffle-test-{id}"),
1228 ip: "127.0.0.1".parse().unwrap(),
1229 online: true,
1230 cur_addr: Some("127.0.0.1:41641".to_string()),
1231 relay: None,
1232 os: Some("linux".to_string()),
1233 last_seen: Some("2026-03-25T12:00:00Z".to_string()),
1234 key_expiry: None,
1235 dns_name: None,
1236 }
1237 }
1238
1239 fn ws_config(port: u16) -> WsConfig {
1240 WsConfig {
1241 port,
1242 ping_interval: Duration::from_secs(300),
1243 pong_timeout: Duration::from_secs(300),
1244 ..Default::default()
1245 }
1246 }
1247
1248 async fn random_port() -> u16 {
1249 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1250 l.local_addr().unwrap().port()
1251 }
1252
1253 async fn make_test_node(
1255 id: &str,
1256 ws_port: u16,
1257 ) -> (
1258 Node<MockNetworkProvider>,
1259 broadcast::Sender<NetworkPeerEvent>,
1260 Arc<MockNetworkProvider>,
1261 ) {
1262 let provider = MockNetworkProvider::new(id);
1263 let event_tx = provider.event_sender();
1264 let network = Arc::new(provider);
1265 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config(ws_port)));
1266 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1267 session.start().await;
1268
1269 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1270 let node = Node::from_parts(network.clone(), session, codec);
1271
1272 (node, event_tx, network)
1273 }
1274
1275 #[tokio::test]
1278 async fn test_node_builder_creates_node() {
1279 let ws_port = random_port().await;
1280 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1281
1282 let identity = node.local_info();
1283 assert_eq!(identity.tailscale_id, "node-1");
1284 assert_eq!(identity.device_id, "node-1");
1285 assert!(identity.tailscale_hostname.contains("node-1"));
1286 }
1287
1288 #[tokio::test]
1289 async fn test_node_peers_from_network() {
1290 let ws_port = random_port().await;
1291 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1292
1293 let peers = node.peers().await;
1295 assert!(peers.is_empty());
1296
1297 let peer = make_loopback_peer("peer-a");
1299 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1300 tokio::time::sleep(Duration::from_millis(50)).await;
1301
1302 let peers = node.peers().await;
1303 assert_eq!(peers.len(), 1);
1304 assert_eq!(peers[0].id, "peer-a");
1305 assert!(peers[0].online);
1306 assert!(!peers[0].ws_connected);
1307 }
1308
1309 #[tokio::test]
1310 async fn test_node_send_to_unknown_peer_errors() {
1311 let ws_port = random_port().await;
1312 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1313
1314 let result = node.send("nonexistent", "test", b"hello").await;
1315 assert!(result.is_err());
1316 let err_str = result.unwrap_err().to_string();
1317 assert!(
1318 err_str.contains("unknown peer") || err_str.contains("not found"),
1319 "expected unknown peer error, got: {err_str}"
1320 );
1321 }
1322
1323 #[tokio::test]
1324 async fn test_node_send_wraps_in_envelope() {
1325 let codec = JsonCodec;
1328 let data = b"hello world";
1329 let envelope = Envelope::new("test-ns", "message", serde_json::Value::from(data.to_vec()))
1330 .with_timestamp();
1331
1332 let encoded = codec.encode(&envelope).unwrap();
1333 let decoded = codec.decode(&encoded).unwrap();
1334
1335 assert_eq!(decoded.namespace, "test-ns");
1336 assert_eq!(decoded.msg_type, "message");
1337 assert!(decoded.timestamp.is_some());
1338 }
1339
1340 #[tokio::test]
1341 async fn test_node_subscribe_filters_by_namespace() {
1342 let ws_port = random_port().await;
1343 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1344
1345 let _rx_chat = node.subscribe("chat");
1347 let _rx_ft = node.subscribe("ft");
1348
1349 let _rx_chat2 = node.subscribe("chat");
1351 }
1352
1353 #[tokio::test]
1354 async fn test_node_broadcast() {
1355 let ws_port = random_port().await;
1356 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1357
1358 node.broadcast("test", b"hello everyone").await;
1360 }
1361
1362 #[tokio::test]
1363 async fn test_node_open_tcp_resolves_peer() {
1364 let ws_port = random_port().await;
1365 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1366
1367 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1369 let tcp_port = listener.local_addr().unwrap().port();
1370
1371 let peer = make_loopback_peer("peer-tcp");
1373 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1374 tokio::time::sleep(Duration::from_millis(50)).await;
1375
1376 let accept_handle = tokio::spawn(async move {
1378 let (stream, _) = listener.accept().await.unwrap();
1379 stream
1380 });
1381
1382 let stream = node.open_tcp("peer-tcp", tcp_port).await;
1384 assert!(stream.is_ok(), "open_tcp failed: {:?}", stream.err());
1385
1386 let _ = accept_handle.await;
1387 }
1388
1389 #[tokio::test]
1390 async fn test_node_open_tcp_unknown_peer_errors() {
1391 let ws_port = random_port().await;
1392 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1393
1394 let result = node.open_tcp("nonexistent", 8080).await;
1395 assert!(result.is_err());
1396 let err_str = result.unwrap_err().to_string();
1397 assert!(
1398 err_str.contains("not found"),
1399 "expected peer not found error, got: {err_str}"
1400 );
1401 }
1402
1403 #[tokio::test]
1404 async fn test_node_ping_resolves_peer() {
1405 let ws_port = random_port().await;
1406 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1407
1408 let result = node.ping("peer-ping").await;
1410 assert!(result.is_err());
1411
1412 let peer = make_loopback_peer("peer-ping");
1414 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1415 tokio::time::sleep(Duration::from_millis(50)).await;
1416
1417 let result = node.ping("peer-ping").await;
1419 assert!(result.is_ok());
1420 assert_eq!(result.unwrap().latency, Duration::from_millis(1));
1421 }
1422
1423 #[tokio::test]
1424 async fn test_node_health() {
1425 let ws_port = random_port().await;
1426 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1427
1428 let health = node.health().await;
1429 assert!(health.healthy);
1430 assert_eq!(health.state, "running");
1431 }
1432
1433 #[tokio::test]
1434 async fn test_node_open_quic_not_implemented() {
1435 let ws_port = random_port().await;
1436 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1437
1438 let result = node.open_quic("peer").await;
1439 assert!(matches!(result, Err(NodeError::NotImplemented(_))));
1440 }
1441
1442 #[tokio::test]
1443 async fn test_node_open_udp_not_implemented() {
1444 let ws_port = random_port().await;
1445 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1446
1447 let result = node.open_udp("peer").await;
1448 assert!(matches!(result, Err(NodeError::NotImplemented(_))));
1449 }
1450
1451 #[tokio::test]
1452 async fn test_node_listen_tcp() {
1453 let ws_port = random_port().await;
1454 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1455
1456 let listener = node.listen_tcp(0).await;
1458 assert!(listener.is_ok(), "listen_tcp failed: {:?}", listener.err());
1459 }
1460
1461 #[tokio::test]
1462 async fn test_envelope_serialize_deserialize() {
1463 let envelope = Envelope::new("chat", "message", json!({"text": "hello"})).with_timestamp();
1464
1465 let bytes = envelope.serialize().unwrap();
1466 let decoded = Envelope::deserialize(&bytes).unwrap();
1467
1468 assert_eq!(decoded.namespace, "chat");
1469 assert_eq!(decoded.msg_type, "message");
1470 assert_eq!(decoded.payload["text"], "hello");
1471 assert!(decoded.timestamp.is_some());
1472 }
1473
1474 #[tokio::test]
1475 async fn test_envelope_codec_json() {
1476 let codec = JsonCodec;
1477 let envelope = Envelope::new("ft", "offer", json!({"file": "test.bin"}));
1478
1479 let encoded = codec.encode(&envelope).unwrap();
1480 let decoded = codec.decode(&encoded).unwrap();
1481
1482 assert_eq!(decoded.namespace, "ft");
1483 assert_eq!(decoded.payload["file"], "test.bin");
1484 }
1485
1486 #[tokio::test]
1487 async fn test_envelope_unknown_fields_ignored() {
1488 let json_bytes = br#"{
1489 "namespace": "v2",
1490 "msg_type": "new",
1491 "payload": {},
1492 "future_field": "ignored"
1493 }"#;
1494
1495 let codec = JsonCodec;
1496 let decoded = codec.decode(json_bytes).unwrap();
1497 assert_eq!(decoded.namespace, "v2");
1498 assert_eq!(decoded.msg_type, "new");
1499 }
1500
1501 #[tokio::test]
1502 async fn test_node_send_and_receive_roundtrip() {
1503 let port_a = random_port().await;
1505 let port_b = random_port().await;
1506
1507 let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
1508 let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
1509
1510 let peer_b = NetworkPeer {
1512 id: "node-b".to_string(),
1513 hostname: "truffle-test-node-b".to_string(),
1514 ip: "127.0.0.1".parse().unwrap(),
1515 online: true,
1516 cur_addr: Some("127.0.0.1:41641".to_string()),
1517 relay: None,
1518 os: None,
1519 last_seen: None,
1520 key_expiry: None,
1521 dns_name: None,
1522 };
1523 let peer_a = NetworkPeer {
1524 id: "node-a".to_string(),
1525 hostname: "truffle-test-node-a".to_string(),
1526 ip: "127.0.0.1".parse().unwrap(),
1527 online: true,
1528 cur_addr: Some("127.0.0.1:41641".to_string()),
1529 relay: None,
1530 os: None,
1531 last_seen: None,
1532 key_expiry: None,
1533 dns_name: None,
1534 };
1535
1536 let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
1537 let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
1538 tokio::time::sleep(Duration::from_millis(100)).await;
1539
1540 let mut rx = node_b.subscribe("test");
1542
1543 let send_result = node_a.send("node-b", "test", b"hello from a").await;
1546
1547 if send_result.is_ok() {
1555 let msg = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await;
1557 if let Ok(Ok(msg)) = msg {
1558 assert_eq!(msg.namespace, "test");
1559 }
1560 }
1561 }
1564
1565 async fn inject_peer_with_identity(
1571 node: &Node<MockNetworkProvider>,
1572 event_tx: &broadcast::Sender<NetworkPeerEvent>,
1573 tailscale_id: &str,
1574 device_id: &str,
1575 device_name: &str,
1576 ) {
1577 let peer = make_loopback_peer(tailscale_id);
1578 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1579 tokio::time::sleep(Duration::from_millis(30)).await;
1580
1581 let identity = crate::session::PeerIdentity {
1582 app_id: "test".into(),
1583 device_id: device_id.into(),
1584 device_name: device_name.into(),
1585 os: "linux".into(),
1586 tailscale_id: tailscale_id.into(),
1587 };
1588 assert!(
1589 node.session
1590 .test_stamp_identity(tailscale_id, identity)
1591 .await,
1592 "peer {tailscale_id} should exist in session registry before stamping identity"
1593 );
1594 }
1595
1596 #[tokio::test]
1597 async fn test_resolve_peer_id_by_device_id() {
1598 let ws_port = random_port().await;
1599 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1600
1601 inject_peer_with_identity(
1602 &node,
1603 &event_tx,
1604 "tailscale-abc",
1605 "01HZZZZZZZZZZZZZZZZZZZZZZZ",
1606 "Alice MacBook",
1607 )
1608 .await;
1609
1610 let resolved = node
1611 .resolve_peer_id("01HZZZZZZZZZZZZZZZZZZZZZZZ")
1612 .await
1613 .unwrap();
1614 assert_eq!(resolved, "01HZZZZZZZZZZZZZZZZZZZZZZZ");
1615 }
1616
1617 #[tokio::test]
1618 async fn test_resolve_peer_id_by_device_name() {
1619 let ws_port = random_port().await;
1620 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1621
1622 inject_peer_with_identity(
1623 &node,
1624 &event_tx,
1625 "tailscale-abc",
1626 "01HXYZXYZXYZXYZXYZXYZXYZXY",
1627 "Bob's Mac",
1628 )
1629 .await;
1630
1631 let resolved = node.resolve_peer_id("Bob's Mac").await.unwrap();
1632 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
1633 }
1634
1635 #[tokio::test]
1636 async fn test_resolve_peer_id_by_device_id_prefix() {
1637 let ws_port = random_port().await;
1638 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1639
1640 inject_peer_with_identity(
1641 &node,
1642 &event_tx,
1643 "tailscale-abc",
1644 "01HXYZXYZXYZXYZXYZXYZXYZXY",
1645 "laptop",
1646 )
1647 .await;
1648
1649 let resolved = node.resolve_peer_id("01HX").await.unwrap();
1652 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
1653 }
1654
1655 #[tokio::test]
1656 async fn test_resolve_peer_id_by_tailscale_id_legacy() {
1657 let ws_port = random_port().await;
1661 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1662
1663 inject_peer_with_identity(
1664 &node,
1665 &event_tx,
1666 "tailscale-legacy",
1667 "01HLEGACY0000000000000000X",
1668 "legacy box",
1669 )
1670 .await;
1671
1672 let resolved = node.resolve_peer_id("tailscale-legacy").await.unwrap();
1673 assert_eq!(resolved, "01HLEGACY0000000000000000X");
1674 }
1675
1676 #[tokio::test]
1677 async fn test_resolve_peer_id_unknown() {
1678 let ws_port = random_port().await;
1679 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
1680 let result = node.resolve_peer_id("nope").await;
1681 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
1682 }
1683}