1use std::collections::HashMap;
34use std::net::IpAddr;
35use std::path::{Path, PathBuf};
36use std::sync::Arc;
37use std::time::Duration;
38use std::sync::RwLock as StdRwLock;
46
47use tokio::net::TcpStream;
48use tokio::sync::broadcast;
49#[cfg(test)]
53use tokio::sync::RwLock;
54
55use crate::envelope::codec::{EnvelopeCodec, JsonCodec};
56use crate::envelope::{Envelope, EnvelopeError};
57use crate::file_transfer::{self, FileTransferState};
58use crate::identity::{self, AppId, DeviceId, DeviceName};
59use crate::network::tailscale::{TailscaleConfig, TailscaleProvider};
60use crate::network::{DialOpts, HealthInfo, NetworkProvider, NodeIdentity, PingResult};
61use crate::session::{PeerEvent, PeerRegistry, PeerState};
62use crate::transport::quic::{QuicConnection, QuicListener};
63use crate::transport::websocket::WebSocketTransport;
64use crate::transport::{DatagramSocket, RawListener, WsConfig};
65
66#[derive(Debug, Clone)]
75pub struct NamespacedMessage {
76 pub from: String,
78 pub namespace: String,
80 pub msg_type: String,
82 pub payload: serde_json::Value,
84 pub timestamp: Option<u64>,
86}
87
88#[derive(Debug, Clone)]
99pub struct Peer {
100 pub device_id: Option<String>,
103 pub device_name: Option<String>,
105 pub display_name: String,
108 pub hostname: String,
110 pub tailscale_id: String,
112 pub peer_ref: String,
114 pub generation: u64,
116 pub ip: IpAddr,
118 pub online: bool,
120 pub ws_connected: bool,
122 pub connection_type: String,
124 pub os: Option<String>,
127 pub last_seen: Option<String>,
129}
130
131impl From<PeerState> for Peer {
132 fn from(s: PeerState) -> Self {
133 let (device_id, device_name, os_from_hello) = if s.identity_suppressed {
134 (None, None, None)
135 } else {
136 match s.identity.as_ref() {
137 Some(identity) => (
138 Some(identity.device_id.clone()),
139 Some(identity.device_name.clone()),
140 Some(identity.os.clone()),
141 ),
142 None => (None, None, None),
143 }
144 };
145
146 debug_assert!(
148 device_id
149 .as_ref()
150 .map(|d| d.as_str() != s.id.as_str())
151 .unwrap_or(true),
152 "RFC 022 I1 violated: device_id must not equal tailscale_id"
153 );
154
155 let display_name = device_name
156 .clone()
157 .filter(|n| !n.is_empty())
158 .unwrap_or_else(|| display_name_from_hostname(&s.name, &s.id));
159
160 let peer_ref = s.peer_ref();
161 let generation = s.generation;
162 let os = os_from_hello.or(s.os.clone());
163
164 Self {
165 device_id,
166 device_name,
167 display_name,
168 hostname: s.name,
169 tailscale_id: s.id,
170 peer_ref,
171 generation,
172 ip: s.ip,
173 online: s.online,
174 ws_connected: s.ws_connected,
175 connection_type: s.connection_type,
176 os,
177 last_seen: s.last_seen,
178 }
179 }
180}
181
182fn display_name_from_hostname(hostname: &str, tailscale_id: &str) -> String {
185 const PREFIX: &str = "truffle-";
186 if let Some(rest) = hostname.strip_prefix(PREFIX) {
187 if let Some((_, slug)) = rest.split_once('-') {
189 if !slug.is_empty() {
190 return slug.to_string();
191 }
192 }
193 }
194 if !hostname.is_empty() {
195 return hostname.to_string();
196 }
197 let short: String = tailscale_id.chars().take(8).collect();
199 if short.is_empty() {
200 "peer".to_string()
201 } else {
202 short
203 }
204}
205
206#[derive(Debug, thiserror::Error)]
212pub enum NodeError {
213 #[error("peer not found: {0}")]
215 PeerNotFound(String),
216
217 #[error("ambiguous peer query '{query}': {} candidates", candidates.len())]
219 AmbiguousPeer {
220 query: String,
222 candidates: Vec<String>,
224 },
225
226 #[error("peer gone: {0}")]
228 PeerGone(String),
229
230 #[error("connection failed: {0}")]
232 ConnectionFailed(String),
233
234 #[error("send failed: {0}")]
236 SendFailed(String),
237
238 #[error("envelope error: {0}")]
240 Envelope(#[from] EnvelopeError),
241
242 #[error("session error: {0}")]
244 Session(#[from] crate::session::SessionError),
245
246 #[error("network error: {0}")]
248 Network(#[from] crate::network::NetworkError),
249
250 #[error("transport error: {0}")]
252 Transport(#[from] crate::transport::TransportError),
253
254 #[error("not implemented: {0}")]
256 NotImplemented(String),
257
258 #[error("port {0} is reserved by truffle (443 = sidecar TLS, or the session WebSocket port)")]
260 ReservedPort(u16),
261
262 #[error("node stopped")]
264 Stopped,
265
266 #[error("build error: {0}")]
268 BuildError(String),
269
270 #[error("io error: {0}")]
272 Io(#[from] std::io::Error),
273}
274
275pub struct Node<N: NetworkProvider + 'static> {
291 pub(crate) network: Arc<N>,
293 session: Arc<PeerRegistry<N>>,
295 codec: Arc<dyn EnvelopeCodec>,
297 #[allow(dead_code)]
300 incoming_tx: broadcast::Sender<NamespacedMessage>,
301 namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
309 pub(crate) file_transfer_state: FileTransferState,
311 state_dir: PathBuf,
314 ws_port: u16,
317 pub(crate) proxy_state: crate::proxy::ProxyState,
319 stopped: std::sync::atomic::AtomicBool,
323}
324
325impl<N: NetworkProvider + 'static> Node<N> {
326 pub(crate) fn from_parts(
332 network: Arc<N>,
333 session: Arc<PeerRegistry<N>>,
334 codec: Arc<dyn EnvelopeCodec>,
335 ) -> Self {
336 let (incoming_tx, _) = broadcast::channel(1024);
337 let namespace_filters: Arc<
338 StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>,
339 > = Arc::new(StdRwLock::new(HashMap::new()));
340
341 let node = Self {
342 network,
343 session: session.clone(),
344 codec: codec.clone(),
345 incoming_tx: incoming_tx.clone(),
346 namespace_filters: namespace_filters.clone(),
347 file_transfer_state: FileTransferState::new(),
348 state_dir: PathBuf::new(),
349 ws_port: 9417,
350 proxy_state: crate::proxy::ProxyState::new(),
351 stopped: std::sync::atomic::AtomicBool::new(false),
352 };
353
354 node.spawn_envelope_router(session, codec, incoming_tx, namespace_filters);
356
357 node
358 }
359
360 fn spawn_envelope_router(
364 &self,
365 session: Arc<PeerRegistry<N>>,
366 codec: Arc<dyn EnvelopeCodec>,
367 incoming_tx: broadcast::Sender<NamespacedMessage>,
368 namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
369 ) {
370 let mut rx = session.subscribe();
371
372 tokio::spawn(async move {
373 loop {
374 match rx.recv().await {
375 Ok(msg) => {
376 if let Ok(envelope) = codec.decode(&msg.data) {
377 let namespaced = NamespacedMessage {
378 from: msg.from,
379 namespace: envelope.namespace.clone(),
380 msg_type: envelope.msg_type,
381 payload: envelope.payload,
382 timestamp: envelope.timestamp,
383 };
384
385 tracing::debug!(
386 from = %namespaced.from,
387 namespace = %namespaced.namespace,
388 msg_type = %namespaced.msg_type,
389 "envelope router: dispatching message"
390 );
391
392 let _ = incoming_tx.send(namespaced.clone());
394
395 let filters = namespace_filters
399 .read()
400 .expect("namespace_filters std RwLock poisoned");
401 let _has_subscriber = filters.contains_key(&namespaced.namespace);
402 if let Some(tx) = filters.get(&namespaced.namespace) {
403 let send_result = tx.send(namespaced);
404 tracing::debug!(
405 namespace = %envelope.namespace,
406 subscriber_count = tx.receiver_count(),
407 sent = send_result.is_ok(),
408 "envelope router: sent to namespace subscriber"
409 );
410 } else {
411 tracing::debug!(
412 namespace = %envelope.namespace,
413 "envelope router: no subscriber for namespace"
414 );
415 }
416 } else {
417 tracing::warn!(
418 from = %msg.from,
419 data_len = msg.data.len(),
420 "node: failed to decode envelope from incoming message"
421 );
422 }
423 }
424 Err(broadcast::error::RecvError::Lagged(n)) => {
425 tracing::warn!(
426 missed = n,
427 "node: envelope router lagged, missed {n} messages"
428 );
429 continue;
430 }
431 Err(broadcast::error::RecvError::Closed) => {
432 tracing::debug!("node: session incoming channel closed, router exiting");
433 break;
434 }
435 }
436 }
437 });
438 }
439
440 pub fn builder() -> NodeBuilder {
444 NodeBuilder::default()
445 }
446
447 pub fn file_transfer(&self) -> file_transfer::FileTransfer<'_, N> {
454 file_transfer::FileTransfer::new(self)
455 }
456
457 pub fn proxy(&self) -> crate::proxy::Proxy<'_, N> {
464 crate::proxy::Proxy::new(self)
465 }
466
467 pub fn synced_store<T>(
476 self: &Arc<Self>,
477 store_id: &str,
478 ) -> Arc<crate::synced_store::SyncedStore<T>>
479 where
480 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
481 {
482 crate::synced_store::SyncedStore::new(self.clone(), store_id)
483 }
484
485 pub fn synced_store_with_backend<T>(
490 self: &Arc<Self>,
491 store_id: &str,
492 backend: std::sync::Arc<dyn crate::synced_store::StoreBackend>,
493 ) -> Arc<crate::synced_store::SyncedStore<T>>
494 where
495 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
496 {
497 crate::synced_store::SyncedStore::new_with_backend(self.clone(), store_id, backend)
498 }
499
500 pub(crate) fn with_state_dir(mut self, dir: PathBuf) -> Self {
504 self.state_dir = dir;
505 self
506 }
507
508 pub(crate) fn with_ws_port(mut self, port: u16) -> Self {
511 self.ws_port = port;
512 self
513 }
514
515 pub fn state_dir(&self) -> &Path {
519 &self.state_dir
520 }
521
522 pub async fn stop(&self) {
539 if self.stopped.swap(true, std::sync::atomic::Ordering::SeqCst) {
540 tracing::debug!("node: stop() called on already-stopped node");
541 return;
542 }
543 tracing::info!("node: stopping");
544 self.session.shutdown().await;
546 if let Err(e) = self.network.stop().await {
548 tracing::warn!(error = %e, "node: network provider stop failed");
549 }
550 tracing::info!("node: stopped");
551 }
552
553 pub fn local_info(&self) -> NodeIdentity {
557 self.network.local_identity()
558 }
559
560 pub async fn peers(&self) -> Vec<Peer> {
567 let app_id = self.network.local_identity().app_id;
568 self.session
569 .peers()
570 .await
571 .into_iter()
572 .map(|s| Self::project_peer(s, &app_id))
573 .collect()
574 }
575
576 fn project_peer(s: PeerState, app_id: &str) -> Peer {
578 let bare = s
582 .identity
583 .is_none()
584 .then(|| hostname_slug(&s.name, app_id).map(str::to_string))
585 .flatten();
586 let mut peer = Peer::from(s);
587 if let Some(bare) = bare {
588 peer.display_name = bare;
589 }
590 peer
591 }
592
593 pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
595 self.session.on_peer_change()
596 }
597
598 pub async fn peer(&self, query: &str, wait_ms: Option<u64>) -> Result<Option<Peer>, NodeError> {
607 match self.resolve_peer(query).await {
608 Ok(state) => {
609 let app_id = self.network.local_identity().app_id;
610 return Ok(Some(Self::project_peer(state, &app_id)));
611 }
612 Err(NodeError::PeerNotFound(_)) => {}
613 Err(e) => return Err(e),
614 }
615
616 let Some(ms) = wait_ms.filter(|m| *m > 0) else {
617 return Ok(None);
618 };
619
620 let mut rx = self.session.on_peer_change();
621
622 match self.resolve_peer(query).await {
627 Ok(state) => {
628 let app_id = self.network.local_identity().app_id;
629 return Ok(Some(Self::project_peer(state, &app_id)));
630 }
631 Err(NodeError::PeerNotFound(_)) => {}
632 Err(e) => return Err(e),
633 }
634
635 let deadline = tokio::time::Instant::now() + Duration::from_millis(ms);
636 loop {
637 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
638 if remaining.is_zero() {
639 return Ok(None);
640 }
641 match tokio::time::timeout(remaining, rx.recv()).await {
642 Ok(Ok(_)) => match self.resolve_peer(query).await {
643 Ok(state) => {
644 let app_id = self.network.local_identity().app_id;
645 return Ok(Some(Self::project_peer(state, &app_id)));
646 }
647 Err(NodeError::PeerNotFound(_)) => continue,
648 Err(e) => return Err(e),
649 },
650 Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,
651 Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => return Ok(None),
652 Err(_) => return Ok(None), }
654 }
655 }
656
657 pub(crate) async fn resolve_peer(&self, peer_ref: &str) -> Result<PeerState, NodeError> {
665 let peers = self.session.peers().await;
666 let app_id = self.network.local_identity().app_id;
667
668 for p in &peers {
670 if let Some(uid) = p.published_device_id() {
671 if uid == peer_ref {
672 return Ok(p.clone());
673 }
674 }
675 if p.id == peer_ref || p.name == peer_ref || p.ip.to_string() == peer_ref {
676 return Ok(p.clone());
677 }
678 if p.peer_ref() == peer_ref {
679 return Ok(p.clone());
680 }
681 }
682
683 let name_hits: Vec<&PeerState> = peers
685 .iter()
686 .filter(|p| {
687 p.identity
688 .as_ref()
689 .map(|i| !p.identity_suppressed && i.device_name.eq_ignore_ascii_case(peer_ref))
690 .unwrap_or(false)
691 })
692 .collect();
693 if name_hits.len() > 1 {
694 return Err(NodeError::AmbiguousPeer {
695 query: peer_ref.to_string(),
696 candidates: name_hits
697 .iter()
698 .map(|p| p.published_device_id().unwrap_or(p.id.as_str()).to_string())
699 .collect(),
700 });
701 }
702 if let Some(hit) = name_hits.first() {
703 return Ok((*hit).clone());
704 }
705
706 let ref_slug = identity::slug(peer_ref, 255);
708 if !ref_slug.is_empty() {
709 let slug_hits: Vec<&PeerState> = peers
710 .iter()
711 .filter(|p| hostname_slug(&p.name, &app_id) == Some(ref_slug.as_str()))
712 .collect();
713 if slug_hits.len() > 1 {
714 return Err(NodeError::AmbiguousPeer {
715 query: peer_ref.to_string(),
716 candidates: slug_hits.iter().map(|p| p.id.clone()).collect(),
717 });
718 }
719 if let Some(hit) = slug_hits.first() {
720 return Ok((*hit).clone());
721 }
722 }
723
724 if peer_ref.len() >= 4 {
726 let hits: Vec<&PeerState> = peers
727 .iter()
728 .filter(|p| {
729 p.published_device_id()
730 .map(|uid| uid.starts_with(peer_ref))
731 .unwrap_or(false)
732 })
733 .collect();
734 if hits.len() > 1 {
735 return Err(NodeError::AmbiguousPeer {
736 query: peer_ref.to_string(),
737 candidates: hits
738 .iter()
739 .filter_map(|p| p.published_device_id().map(|s| s.to_string()))
740 .collect(),
741 });
742 }
743 if let Some(hit) = hits.first() {
744 return Ok((*hit).clone());
745 }
746 }
747
748 if crate::session::parse_peer_ref(peer_ref).is_some() {
754 return Err(NodeError::PeerGone(peer_ref.to_string()));
755 }
756
757 Err(NodeError::PeerNotFound(peer_ref.to_string()))
758 }
759
760 pub async fn resolve_peer_ip(&self, peer_ref: &str) -> Result<IpAddr, NodeError> {
766 Ok(self.resolve_peer(peer_ref).await?.ip)
767 }
768
769 pub async fn resolve_peer_id(&self, peer_id: &str) -> Result<String, NodeError> {
787 let p = self.resolve_peer(peer_id).await?;
788 Ok(p.published_device_id()
789 .map(|s| s.to_string())
790 .unwrap_or_else(|| p.id.clone()))
791 }
792
793 pub async fn ping(&self, peer_id: &str) -> Result<PingResult, NodeError> {
800 let peer = self.resolve_peer(peer_id).await?;
801 let addr = peer.ip.to_string();
802 self.network.ping(&addr).await.map_err(NodeError::Network)
803 }
804
805 pub async fn health(&self) -> HealthInfo {
807 self.network.health().await
808 }
809
810 fn ensure_not_stopped(&self) -> Result<(), NodeError> {
817 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
818 return Err(NodeError::Stopped);
819 }
820 Ok(())
821 }
822
823 fn map_session_send_err(e: crate::session::SessionError) -> NodeError {
826 match e {
827 crate::session::SessionError::PeerGone(r) => NodeError::PeerGone(r),
828 e => NodeError::Session(e),
829 }
830 }
831
832 pub async fn send(&self, peer_id: &str, namespace: &str, data: &[u8]) -> Result<(), NodeError> {
840 self.ensure_not_stopped()?;
841 let payload = std::str::from_utf8(data)
847 .ok()
848 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
849 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
850
851 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
852
853 let encoded = self.codec.encode(&envelope)?;
854 self.session
855 .send(peer_id, &encoded)
856 .await
857 .map_err(Self::map_session_send_err)?;
858 Ok(())
859 }
860
861 pub async fn send_typed(
869 &self,
870 peer_id: &str,
871 namespace: &str,
872 msg_type: &str,
873 payload: &serde_json::Value,
874 ) -> Result<(), NodeError> {
875 self.ensure_not_stopped()?;
876 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
877 let encoded = self.codec.encode(&envelope)?;
878 self.session
879 .send(peer_id, &encoded)
880 .await
881 .map_err(Self::map_session_send_err)?;
882 Ok(())
883 }
884
885 pub async fn broadcast_typed(
888 &self,
889 namespace: &str,
890 msg_type: &str,
891 payload: &serde_json::Value,
892 ) {
893 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
894 tracing::debug!("node: broadcast after stop ignored");
895 return;
896 }
897 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
898 match self.codec.encode(&envelope) {
899 Ok(encoded) => {
900 self.session.broadcast(&encoded).await;
901 }
902 Err(e) => {
903 tracing::error!("node: failed to encode broadcast envelope: {e}");
904 }
905 }
906 }
907
908 pub async fn broadcast(&self, namespace: &str, data: &[u8]) {
913 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
914 tracing::debug!("node: broadcast after stop ignored");
915 return;
916 }
917 let payload = std::str::from_utf8(data)
918 .ok()
919 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
920 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
921
922 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
923
924 match self.codec.encode(&envelope) {
925 Ok(encoded) => {
926 self.session.broadcast(&encoded).await;
927 }
928 Err(e) => {
929 tracing::error!("node: failed to encode broadcast envelope: {e}");
930 }
931 }
932 }
933
934 pub fn subscribe(&self, namespace: &str) -> broadcast::Receiver<NamespacedMessage> {
940 {
942 let filters = self
943 .namespace_filters
944 .read()
945 .expect("namespace_filters std RwLock poisoned");
946 if let Some(tx) = filters.get(namespace) {
947 return tx.subscribe();
948 }
949 }
950
951 let mut filters = self
953 .namespace_filters
954 .write()
955 .expect("namespace_filters std RwLock poisoned");
956 if let Some(tx) = filters.get(namespace) {
958 return tx.subscribe();
959 }
960 let (tx, rx) = broadcast::channel(256);
961 filters.insert(namespace.to_string(), tx);
962 rx
963 }
964
965 pub async fn open_tcp(&self, peer_id: &str, port: u16) -> Result<TcpStream, NodeError> {
977 let peer = self.resolve_peer(peer_id).await?;
978 let addr = peer.ip.to_string();
979 self.network
980 .dial_tcp_opts(&addr, port, DialOpts { tls: Some(false) })
981 .await
982 .map_err(|e| NodeError::ConnectionFailed(e.to_string()))
983 }
984
985 pub async fn listen_tcp(&self, port: u16) -> Result<RawListener, NodeError> {
993 use crate::transport::tcp::TcpTransport;
994 use crate::transport::RawTransport;
995
996 ensure_port_unreserved(port, self.ws_port)?;
997 let tcp = TcpTransport::new(self.network.clone());
998 tcp.listen(port).await.map_err(NodeError::Transport)
999 }
1000
1001 pub async fn unlisten_tcp(&self, port: u16) -> Result<(), NodeError> {
1006 self.network
1007 .unlisten_tcp(port)
1008 .await
1009 .map_err(NodeError::Network)
1010 }
1011
1012 pub async fn connect_quic(
1021 &self,
1022 peer_id: &str,
1023 port: u16,
1024 ) -> Result<QuicConnection, NodeError> {
1025 let peer = self.resolve_peer(peer_id).await?;
1026 let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
1027 crate::transport::quic::connect_raw(&self.network, &peer.ip.to_string(), port, &alpn)
1028 .await
1029 .map_err(NodeError::Transport)
1030 }
1031
1032 pub async fn listen_quic(&self, port: u16) -> Result<QuicListener, NodeError> {
1040 ensure_port_unreserved(port, self.ws_port)?;
1041 if port == 0 {
1042 return Err(NodeError::NotImplemented(
1043 "ephemeral (port 0) QUIC listeners are not supported over the tsnet relay yet — choose an explicit port"
1044 .to_string(),
1045 ));
1046 }
1047 let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
1048 crate::transport::quic::listen_raw(&self.network, port, &alpn)
1049 .await
1050 .map_err(NodeError::Transport)
1051 }
1052
1053 pub async fn bind_udp(&self, port: u16) -> Result<DatagramSocket, NodeError> {
1063 use crate::transport::udp::{UdpConfig, UdpTransport};
1064 use crate::transport::DatagramTransport;
1065
1066 let udp = UdpTransport::new(self.network.clone(), UdpConfig::default());
1067 udp.bind(port).await.map_err(NodeError::Transport)
1068 }
1069}
1070
1071fn hostname_slug<'a>(hostname: &'a str, app_id: &str) -> Option<&'a str> {
1074 hostname
1075 .strip_prefix("truffle-")?
1076 .strip_prefix(app_id)?
1077 .strip_prefix('-')
1078}
1079
1080fn ensure_port_unreserved(port: u16, ws_port: u16) -> Result<(), NodeError> {
1083 if port == 443 || port == ws_port {
1084 Err(NodeError::ReservedPort(port))
1085 } else {
1086 Ok(())
1087 }
1088}
1089
1090#[derive(Debug, Clone)]
1111pub struct NodeBuilder {
1112 app_id: Option<AppId>,
1113 device_name: Option<DeviceName>,
1114 device_id: Option<DeviceId>,
1115 sidecar_path: Option<PathBuf>,
1116 state_dir: Option<String>,
1117 auth_key: Option<String>,
1118 ephemeral: bool,
1119 ws_port: u16,
1120 idle_timeout_secs: Option<u64>,
1121 eager_identity: bool,
1123}
1124
1125fn atomic_write_string(path: &Path, content: &str) -> std::io::Result<()> {
1134 let _parent = path.parent().ok_or_else(|| {
1135 std::io::Error::new(
1136 std::io::ErrorKind::InvalidInput,
1137 "path has no parent directory",
1138 )
1139 })?;
1140 let mut tmp = path.to_path_buf();
1141 tmp.set_extension("tmp");
1142 std::fs::write(&tmp, content)?;
1143 #[cfg(unix)]
1144 {
1145 std::fs::rename(&tmp, path)?;
1146 }
1147 #[cfg(windows)]
1148 {
1149 let _ = std::fs::remove_file(path);
1152 std::fs::rename(&tmp, path)?;
1153 }
1154 Ok(())
1155}
1156
1157impl Default for NodeBuilder {
1158 fn default() -> Self {
1159 Self {
1160 app_id: None,
1161 device_name: None,
1162 device_id: None,
1163 sidecar_path: None,
1164 state_dir: None,
1165 auth_key: None,
1166 ephemeral: false,
1167 ws_port: 9417,
1168 idle_timeout_secs: None,
1169 eager_identity: true,
1170 }
1171 }
1172}
1173
1174impl NodeBuilder {
1175 pub fn app_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1180 let raw: String = s.into();
1181 let app_id = AppId::parse(&raw)
1182 .map_err(|e| NodeError::BuildError(format!("invalid app_id: {e}")))?;
1183 self.app_id = Some(app_id);
1184 Ok(self)
1185 }
1186
1187 pub fn device_name(mut self, s: impl Into<String>) -> Self {
1192 self.device_name = Some(DeviceName::parse(s));
1193 self
1194 }
1195
1196 pub fn device_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1202 let raw: String = s.into();
1203 let device_id = DeviceId::parse(&raw)
1204 .map_err(|e| NodeError::BuildError(format!("invalid device_id: {e}")))?;
1205 self.device_id = Some(device_id);
1206 Ok(self)
1207 }
1208
1209 pub fn sidecar_path(mut self, path: impl Into<PathBuf>) -> Self {
1211 self.sidecar_path = Some(path.into());
1212 self
1213 }
1214
1215 pub fn state_dir(mut self, dir: &str) -> Self {
1217 self.state_dir = Some(dir.to_string());
1218 self
1219 }
1220
1221 pub fn auth_key(mut self, key: &str) -> Self {
1223 self.auth_key = Some(key.to_string());
1224 self
1225 }
1226
1227 pub fn ephemeral(mut self, val: bool) -> Self {
1229 self.ephemeral = val;
1230 self
1231 }
1232
1233 pub fn eager_identity(mut self, enabled: bool) -> Self {
1237 self.eager_identity = enabled;
1238 self
1239 }
1240
1241 pub fn ws_port(mut self, port: u16) -> Self {
1242 self.ws_port = port;
1243 self
1244 }
1245
1246 pub fn idle_timeout_secs(mut self, secs: u64) -> Self {
1252 self.idle_timeout_secs = Some(secs);
1253 self
1254 }
1255
1256 fn prepare_config(&self) -> Result<TailscaleConfig, NodeError> {
1263 let binary_path = self
1265 .sidecar_path
1266 .clone()
1267 .ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
1268
1269 let app_id = self
1271 .app_id
1272 .clone()
1273 .ok_or_else(|| NodeError::BuildError("app_id is required".into()))?;
1274
1275 let device_name = match self.device_name.clone() {
1277 Some(name) => name,
1278 None => {
1279 let os_hostname = hostname::get()
1280 .map_err(|e| {
1281 NodeError::BuildError(format!(
1282 "device_name is unset and hostname::get() failed: {e}"
1283 ))
1284 })?
1285 .to_string_lossy()
1286 .into_owned();
1287 DeviceName::parse(os_hostname)
1288 }
1289 };
1290
1291 let tailscale_host = identity::tailscale_hostname(&app_id, &device_name);
1294
1295 let state_dir = self.state_dir.clone().unwrap_or_else(|| {
1298 let base = dirs::data_dir().unwrap_or_else(|| {
1299 tracing::warn!(
1300 "dirs::data_dir() returned None, falling back to std::env::temp_dir()"
1301 );
1302 std::env::temp_dir()
1303 });
1304 base.join("truffle")
1305 .join(app_id.as_str())
1306 .join(identity::slug(device_name.as_str(), 255))
1307 .to_string_lossy()
1308 .into_owned()
1309 });
1310
1311 std::fs::create_dir_all(&state_dir)?;
1313
1314 let device_id_file = Path::new(&state_dir).join("device-id.txt");
1319 let device_id = match self.device_id.clone() {
1320 Some(id) => {
1321 atomic_write_string(&device_id_file, id.as_str())?;
1323 id
1324 }
1325 None => {
1326 if device_id_file.exists() {
1327 let s = std::fs::read_to_string(&device_id_file)?.trim().to_string();
1328 DeviceId::parse(&s).map_err(|e| {
1329 NodeError::BuildError(format!(
1330 "device-id.txt at {device_id_file:?} contains an invalid ULID: {e}"
1331 ))
1332 })?
1333 } else {
1334 let id = DeviceId::generate();
1335 atomic_write_string(&device_id_file, id.as_str())?;
1336 id
1337 }
1338 }
1339 };
1340
1341 Ok(TailscaleConfig {
1342 binary_path,
1343 app_id: app_id.as_str().to_string(),
1344 device_id: device_id.as_str().to_string(),
1345 device_name: device_name.as_str().to_string(),
1346 hostname: tailscale_host,
1347 state_dir,
1348 auth_key: self.auth_key.clone(),
1349 ephemeral: if self.ephemeral { Some(true) } else { None },
1350 tags: None,
1351 idle_timeout_secs: self.idle_timeout_secs,
1352 })
1353 }
1354
1355 pub async fn build(self) -> Result<Node<TailscaleProvider>, NodeError> {
1366 let ws_port = self.ws_port;
1367 let eager_identity = self.eager_identity;
1368 let config = self.prepare_config()?;
1369 let state_dir = PathBuf::from(&config.state_dir);
1370
1371 let mut provider = TailscaleProvider::new(config);
1372 provider.start().await.map_err(NodeError::Network)?;
1373
1374 let network = Arc::new(provider);
1375
1376 let ws_config = WsConfig {
1378 port: ws_port,
1379 ..Default::default()
1380 };
1381 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1382
1383 let session = Arc::new(PeerRegistry::with_options(
1385 network.clone(),
1386 ws_transport,
1387 crate::session::PeerRegistryOptions {
1388 eager_identity,
1389 ..Default::default()
1390 },
1391 ));
1392 session.start().await;
1393
1394 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1396 let node = Node::from_parts(network, session, codec)
1397 .with_state_dir(state_dir)
1398 .with_ws_port(ws_port);
1399
1400 tracing::info!("node: started successfully");
1401 Ok(node)
1402 }
1403
1404 pub async fn build_with_auth_handler(
1416 self,
1417 on_auth: impl Fn(String) + Send + 'static,
1418 ) -> Result<Node<TailscaleProvider>, NodeError> {
1419 let ws_port = self.ws_port;
1420 let eager_identity = self.eager_identity;
1421 let config = self.prepare_config()?;
1422 let state_dir = PathBuf::from(&config.state_dir);
1423
1424 let mut provider = TailscaleProvider::new(config);
1425
1426 let mut auth_rx = provider.peer_events();
1428
1429 let auth_task = tokio::spawn(async move {
1431 use crate::network::NetworkPeerEvent;
1432 loop {
1433 match auth_rx.recv().await {
1434 Ok(NetworkPeerEvent::AuthRequired { url }) => {
1435 on_auth(url);
1436 }
1437 Err(broadcast::error::RecvError::Closed) => break,
1438 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1439 _ => {} }
1441 }
1442 });
1443
1444 let start_result = provider.start().await.map_err(NodeError::Network);
1446
1447 auth_task.abort();
1449
1450 start_result?;
1451
1452 let network = Arc::new(provider);
1453
1454 let ws_config = WsConfig {
1456 port: ws_port,
1457 ..Default::default()
1458 };
1459 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1460
1461 let session = Arc::new(PeerRegistry::with_options(
1463 network.clone(),
1464 ws_transport,
1465 crate::session::PeerRegistryOptions {
1466 eager_identity,
1467 ..Default::default()
1468 },
1469 ));
1470 session.start().await;
1471
1472 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1474 let node = Node::from_parts(network, session, codec)
1475 .with_state_dir(state_dir)
1476 .with_ws_port(ws_port);
1477
1478 tracing::info!("node: started successfully (with auth handler)");
1479 Ok(node)
1480 }
1481}
1482
1483#[cfg(test)]
1488mod tests {
1489 use super::*;
1490 use crate::network::{
1491 HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
1492 NetworkTcpListener, NetworkUdpSocket, PeerAddr,
1493 };
1494 use crate::transport::WsConfig;
1495 use serde_json::json;
1496 use std::sync::atomic::{AtomicUsize, Ordering};
1497 use std::time::Duration;
1498 use tokio::sync::{broadcast, mpsc};
1499
1500 struct MockNetworkProvider {
1503 identity: NodeIdentity,
1504 local_addr: PeerAddr,
1505 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
1506 mock_peers: Arc<RwLock<Vec<NetworkPeer>>>,
1508 stop_calls: Arc<AtomicUsize>,
1511 }
1512
1513 impl MockNetworkProvider {
1514 fn new(id: &str) -> Self {
1515 Self::new_with_device_id(id, id)
1518 }
1519
1520 fn new_with_device_id(id: &str, device_id: &str) -> Self {
1525 let (peer_event_tx, _) = broadcast::channel(64);
1526 Self {
1527 identity: NodeIdentity {
1528 app_id: "test".to_string(),
1529 device_id: device_id.to_string(),
1530 device_name: format!("Test Node {id}"),
1531 tailscale_hostname: format!("truffle-test-{id}"),
1532 tailscale_id: id.to_string(),
1533 dns_name: None,
1534 ip: Some("127.0.0.1".parse().unwrap()),
1535 },
1536 local_addr: PeerAddr {
1537 ip: Some("127.0.0.1".parse().unwrap()),
1538 hostname: format!("truffle-test-{id}"),
1539 dns_name: None,
1540 },
1541 peer_event_tx,
1542 mock_peers: Arc::new(RwLock::new(Vec::new())),
1543 stop_calls: Arc::new(AtomicUsize::new(0)),
1544 }
1545 }
1546
1547 fn event_sender(&self) -> broadcast::Sender<NetworkPeerEvent> {
1548 self.peer_event_tx.clone()
1549 }
1550
1551 fn stop_call_count(&self) -> usize {
1553 self.stop_calls.load(Ordering::SeqCst)
1554 }
1555 }
1556
1557 impl NetworkProvider for MockNetworkProvider {
1558 async fn start(&mut self) -> Result<(), NetworkError> {
1559 Ok(())
1560 }
1561
1562 async fn stop(&self) -> Result<(), NetworkError> {
1563 self.stop_calls.fetch_add(1, Ordering::SeqCst);
1564 Ok(())
1565 }
1566
1567 fn local_identity(&self) -> NodeIdentity {
1568 self.identity.clone()
1569 }
1570
1571 fn local_addr(&self) -> PeerAddr {
1572 self.local_addr.clone()
1573 }
1574
1575 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
1576 self.peer_event_tx.subscribe()
1577 }
1578
1579 async fn peers(&self) -> Vec<NetworkPeer> {
1580 self.mock_peers.read().await.clone()
1581 }
1582
1583 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
1584 let target = format!("{addr}:{port}");
1585 TcpStream::connect(&target)
1586 .await
1587 .map_err(|e| NetworkError::DialFailed(format!("mock dial {target}: {e}")))
1588 }
1589
1590 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
1591 let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
1592 .await
1593 .map_err(|e| NetworkError::ListenFailed(format!("mock listen :{port}: {e}")))?;
1594
1595 let actual_port = listener.local_addr().unwrap().port();
1596 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
1597
1598 tokio::spawn(async move {
1599 loop {
1600 match listener.accept().await {
1601 Ok((stream, addr)) => {
1602 let conn = IncomingConnection {
1603 stream,
1604 remote_addr: addr.to_string(),
1605 remote_identity: String::new(),
1606 port: actual_port,
1607 };
1608 if tx.send(conn).await.is_err() {
1609 break;
1610 }
1611 }
1612 Err(e) => {
1613 tracing::debug!("mock listener error: {e}");
1614 break;
1615 }
1616 }
1617 }
1618 });
1619
1620 Ok(NetworkTcpListener {
1621 port: actual_port,
1622 incoming: rx,
1623 })
1624 }
1625
1626 async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
1627 Ok(())
1628 }
1629
1630 async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
1631 Err(NetworkError::Internal("mock: UDP not supported".into()))
1632 }
1633
1634 async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
1635 Ok(PingResult {
1636 latency: Duration::from_millis(1),
1637 connection: "direct".to_string(),
1638 peer_addr: None,
1639 })
1640 }
1641
1642 async fn health(&self) -> HealthInfo {
1643 HealthInfo {
1644 state: "running".to_string(),
1645 healthy: true,
1646 ..Default::default()
1647 }
1648 }
1649 }
1650
1651 fn make_loopback_peer(id: &str) -> NetworkPeer {
1654 NetworkPeer {
1655 id: id.to_string(),
1656 hostname: format!("truffle-test-{id}"),
1657 ip: "127.0.0.1".parse().unwrap(),
1658 online: true,
1659 cur_addr: Some("127.0.0.1:41641".to_string()),
1660 relay: None,
1661 os: Some("linux".to_string()),
1662 last_seen: Some("2026-03-25T12:00:00Z".to_string()),
1663 key_expiry: None,
1664 dns_name: None,
1665 }
1666 }
1667
1668 fn ws_config(port: u16) -> WsConfig {
1669 WsConfig {
1670 port,
1671 ping_interval: Duration::from_secs(300),
1672 pong_timeout: Duration::from_secs(300),
1673 ..Default::default()
1674 }
1675 }
1676
1677 async fn random_port() -> u16 {
1678 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1679 l.local_addr().unwrap().port()
1680 }
1681
1682 async fn make_test_node(
1684 id: &str,
1685 ws_port: u16,
1686 ) -> (
1687 Node<MockNetworkProvider>,
1688 broadcast::Sender<NetworkPeerEvent>,
1689 Arc<MockNetworkProvider>,
1690 ) {
1691 make_test_node_with_device_id(id, id, ws_port).await
1692 }
1693
1694 async fn make_test_node_with_device_id(
1697 id: &str,
1698 device_id: &str,
1699 ws_port: u16,
1700 ) -> (
1701 Node<MockNetworkProvider>,
1702 broadcast::Sender<NetworkPeerEvent>,
1703 Arc<MockNetworkProvider>,
1704 ) {
1705 let provider = MockNetworkProvider::new_with_device_id(id, device_id);
1706 let event_tx = provider.event_sender();
1707 let network = Arc::new(provider);
1708 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config(ws_port)));
1709 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1710 session.start().await;
1711
1712 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1713 let node = Node::from_parts(network.clone(), session, codec);
1714
1715 (node, event_tx, network)
1716 }
1717
1718 #[tokio::test]
1721 async fn test_node_builder_creates_node() {
1722 let ws_port = random_port().await;
1723 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1724
1725 let identity = node.local_info();
1726 assert_eq!(identity.tailscale_id, "node-1");
1727 assert_eq!(identity.device_id, "node-1");
1728 assert!(identity.tailscale_hostname.contains("node-1"));
1729 }
1730
1731 #[tokio::test]
1732 async fn test_node_peers_from_network() {
1733 let ws_port = random_port().await;
1734 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1735
1736 let peers = node.peers().await;
1738 assert!(peers.is_empty());
1739
1740 let peer = make_loopback_peer("peer-a");
1742 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1743 tokio::time::sleep(Duration::from_millis(50)).await;
1744
1745 let peers = node.peers().await;
1746 assert_eq!(peers.len(), 1);
1747 assert_eq!(peers[0].tailscale_id, "peer-a");
1748 assert!(
1749 peers[0].device_id.is_none(),
1750 "RFC 022: no ULID before identity"
1751 );
1752 assert!(peers[0].online);
1753 assert!(!peers[0].ws_connected);
1754 }
1755
1756 #[tokio::test]
1757 async fn test_node_send_to_unknown_peer_errors() {
1758 let ws_port = random_port().await;
1759 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1760
1761 let result = node.send("nonexistent", "test", b"hello").await;
1762 assert!(result.is_err());
1763 let err_str = result.unwrap_err().to_string();
1764 assert!(
1765 err_str.contains("unknown peer") || err_str.contains("not found"),
1766 "expected unknown peer error, got: {err_str}"
1767 );
1768 }
1769
1770 #[tokio::test]
1771 async fn test_node_send_wraps_in_envelope() {
1772 let codec = JsonCodec;
1775 let data = b"hello world";
1776 let envelope = Envelope::new("test-ns", "message", serde_json::Value::from(data.to_vec()))
1777 .with_timestamp();
1778
1779 let encoded = codec.encode(&envelope).unwrap();
1780 let decoded = codec.decode(&encoded).unwrap();
1781
1782 assert_eq!(decoded.namespace, "test-ns");
1783 assert_eq!(decoded.msg_type, "message");
1784 assert!(decoded.timestamp.is_some());
1785 }
1786
1787 #[tokio::test]
1788 async fn test_node_subscribe_filters_by_namespace() {
1789 let ws_port = random_port().await;
1790 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1791
1792 let _rx_chat = node.subscribe("chat");
1794 let _rx_ft = node.subscribe("ft");
1795
1796 let _rx_chat2 = node.subscribe("chat");
1798 }
1799
1800 #[tokio::test]
1801 async fn test_node_broadcast() {
1802 let ws_port = random_port().await;
1803 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1804
1805 node.broadcast("test", b"hello everyone").await;
1807 }
1808
1809 #[tokio::test]
1810 async fn test_node_open_tcp_resolves_peer() {
1811 let ws_port = random_port().await;
1812 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1813
1814 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1816 let tcp_port = listener.local_addr().unwrap().port();
1817
1818 let peer = make_loopback_peer("peer-tcp");
1820 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1821 tokio::time::sleep(Duration::from_millis(50)).await;
1822
1823 let accept_handle = tokio::spawn(async move {
1825 let (stream, _) = listener.accept().await.unwrap();
1826 stream
1827 });
1828
1829 let stream = node.open_tcp("peer-tcp", tcp_port).await;
1831 assert!(stream.is_ok(), "open_tcp failed: {:?}", stream.err());
1832
1833 let _ = accept_handle.await;
1834 }
1835
1836 #[tokio::test]
1837 async fn test_node_open_tcp_unknown_peer_errors() {
1838 let ws_port = random_port().await;
1839 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1840
1841 let result = node.open_tcp("nonexistent", 8080).await;
1842 assert!(result.is_err());
1843 let err_str = result.unwrap_err().to_string();
1844 assert!(
1845 err_str.contains("not found"),
1846 "expected peer not found error, got: {err_str}"
1847 );
1848 }
1849
1850 #[tokio::test]
1851 async fn test_node_ping_resolves_peer() {
1852 let ws_port = random_port().await;
1853 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1854
1855 let result = node.ping("peer-ping").await;
1857 assert!(result.is_err());
1858
1859 let peer = make_loopback_peer("peer-ping");
1861 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1862 tokio::time::sleep(Duration::from_millis(50)).await;
1863
1864 let result = node.ping("peer-ping").await;
1866 assert!(result.is_ok());
1867 assert_eq!(result.unwrap().latency, Duration::from_millis(1));
1868 }
1869
1870 #[tokio::test]
1871 async fn test_node_health() {
1872 let ws_port = random_port().await;
1873 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1874
1875 let health = node.health().await;
1876 assert!(health.healthy);
1877 assert_eq!(health.state, "running");
1878 }
1879
1880 #[tokio::test]
1881 async fn test_node_connect_quic_unknown_peer_errors() {
1882 let ws_port = random_port().await;
1883 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1884
1885 let result = node.connect_quic("peer", 4433).await;
1886 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
1887 }
1888
1889 #[tokio::test]
1890 async fn test_node_listen_tcp() {
1891 let ws_port = random_port().await;
1892 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1893
1894 let listener = node.listen_tcp(0).await;
1896 assert!(listener.is_ok(), "listen_tcp failed: {:?}", listener.err());
1897 }
1898
1899 #[tokio::test]
1900 async fn test_envelope_serialize_deserialize() {
1901 let envelope = Envelope::new("chat", "message", json!({"text": "hello"})).with_timestamp();
1902
1903 let bytes = envelope.serialize().unwrap();
1904 let decoded = Envelope::deserialize(&bytes).unwrap();
1905
1906 assert_eq!(decoded.namespace, "chat");
1907 assert_eq!(decoded.msg_type, "message");
1908 assert_eq!(decoded.payload["text"], "hello");
1909 assert!(decoded.timestamp.is_some());
1910 }
1911
1912 #[tokio::test]
1913 async fn test_envelope_codec_json() {
1914 let codec = JsonCodec;
1915 let envelope = Envelope::new("ft", "offer", json!({"file": "test.bin"}));
1916
1917 let encoded = codec.encode(&envelope).unwrap();
1918 let decoded = codec.decode(&encoded).unwrap();
1919
1920 assert_eq!(decoded.namespace, "ft");
1921 assert_eq!(decoded.payload["file"], "test.bin");
1922 }
1923
1924 #[tokio::test]
1925 async fn test_envelope_unknown_fields_ignored() {
1926 let json_bytes = br#"{
1927 "namespace": "v2",
1928 "msg_type": "new",
1929 "payload": {},
1930 "future_field": "ignored"
1931 }"#;
1932
1933 let codec = JsonCodec;
1934 let decoded = codec.decode(json_bytes).unwrap();
1935 assert_eq!(decoded.namespace, "v2");
1936 assert_eq!(decoded.msg_type, "new");
1937 }
1938
1939 #[tokio::test]
1940 async fn test_node_send_and_receive_roundtrip() {
1941 let port_a = random_port().await;
1943 let port_b = random_port().await;
1944
1945 let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
1946 let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
1947
1948 let peer_b = NetworkPeer {
1950 id: "node-b".to_string(),
1951 hostname: "truffle-test-node-b".to_string(),
1952 ip: "127.0.0.1".parse().unwrap(),
1953 online: true,
1954 cur_addr: Some("127.0.0.1:41641".to_string()),
1955 relay: None,
1956 os: None,
1957 last_seen: None,
1958 key_expiry: None,
1959 dns_name: None,
1960 };
1961 let peer_a = NetworkPeer {
1962 id: "node-a".to_string(),
1963 hostname: "truffle-test-node-a".to_string(),
1964 ip: "127.0.0.1".parse().unwrap(),
1965 online: true,
1966 cur_addr: Some("127.0.0.1:41641".to_string()),
1967 relay: None,
1968 os: None,
1969 last_seen: None,
1970 key_expiry: None,
1971 dns_name: None,
1972 };
1973
1974 let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
1975 let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
1976 tokio::time::sleep(Duration::from_millis(100)).await;
1977
1978 let mut rx = node_b.subscribe("test");
1980
1981 let send_result = node_a.send("node-b", "test", b"hello from a").await;
1984
1985 if send_result.is_ok() {
1993 let msg = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await;
1995 if let Ok(Ok(msg)) = msg {
1996 assert_eq!(msg.namespace, "test");
1997 }
1998 }
1999 }
2002
2003 #[tokio::test]
2006 async fn test_node_stop_shuts_down_provider_and_is_idempotent() {
2007 let ws_port = random_port().await;
2008 let (node, event_tx, network) = make_test_node("node-1", ws_port).await;
2009
2010 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-1")));
2011 tokio::time::sleep(Duration::from_millis(50)).await;
2012
2013 assert_eq!(network.stop_call_count(), 0);
2014 node.stop().await;
2015 assert_eq!(
2016 network.stop_call_count(),
2017 1,
2018 "stop() must shut down the provider"
2019 );
2020
2021 node.stop().await;
2023 assert_eq!(network.stop_call_count(), 1);
2024
2025 let err = node.send("peer-1", "test", b"hi").await.unwrap_err();
2027 assert!(matches!(err, NodeError::Stopped));
2028 }
2029
2030 #[tokio::test]
2031 async fn test_node_stop_closes_ws_connections() {
2032 let port_a = random_port().await;
2041 let (node_a, event_tx_a, _net_a) =
2042 make_test_node_with_device_id("node-a", "dev-node-a", port_a).await;
2043
2044 let self_peer = NetworkPeer {
2045 id: "node-a".to_string(),
2046 hostname: "truffle-test-node-a".to_string(),
2047 ip: "127.0.0.1".parse().unwrap(),
2048 online: true,
2049 cur_addr: Some("127.0.0.1:41641".to_string()),
2050 relay: None,
2051 os: None,
2052 last_seen: None,
2053 key_expiry: None,
2054 dns_name: None,
2055 };
2056 let _ = event_tx_a.send(NetworkPeerEvent::Joined(self_peer));
2057 tokio::time::sleep(Duration::from_millis(100)).await;
2058
2059 node_a
2060 .send("node-a", "test", b"hello from a")
2061 .await
2062 .expect("loopback self-dial should establish a WS connection");
2063
2064 let peers = node_a.peers().await;
2065 let entry = peers
2066 .iter()
2067 .find(|p| p.tailscale_id == "node-a")
2068 .expect("self peer discovered");
2069 assert!(
2070 entry.ws_connected,
2071 "send() should have established a WS connection"
2072 );
2073
2074 node_a.stop().await;
2075 let peers = node_a.peers().await;
2076 let entry = peers
2077 .iter()
2078 .find(|p| p.tailscale_id == "node-a")
2079 .expect("self peer still discovered");
2080 assert!(
2081 !entry.ws_connected,
2082 "stop() must close and un-mark WS connections"
2083 );
2084 }
2085
2086 async fn inject_peer_with_identity(
2092 node: &Node<MockNetworkProvider>,
2093 event_tx: &broadcast::Sender<NetworkPeerEvent>,
2094 tailscale_id: &str,
2095 device_id: &str,
2096 device_name: &str,
2097 ) {
2098 let peer = make_loopback_peer(tailscale_id);
2099 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2100 tokio::time::sleep(Duration::from_millis(30)).await;
2101
2102 let identity = crate::session::PeerIdentity {
2103 app_id: "test".into(),
2104 device_id: device_id.into(),
2105 device_name: device_name.into(),
2106 os: "linux".into(),
2107 tailscale_id: tailscale_id.into(),
2108 };
2109 assert!(
2110 node.session
2111 .test_stamp_identity(tailscale_id, identity)
2112 .await,
2113 "peer {tailscale_id} should exist in session registry before stamping identity"
2114 );
2115 }
2116
2117 #[tokio::test]
2118 async fn test_resolve_peer_id_by_device_id() {
2119 let ws_port = random_port().await;
2120 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2121
2122 inject_peer_with_identity(
2123 &node,
2124 &event_tx,
2125 "tailscale-abc",
2126 "01HZZZZZZZZZZZZZZZZZZZZZZZ",
2127 "Alice MacBook",
2128 )
2129 .await;
2130
2131 let resolved = node
2132 .resolve_peer_id("01HZZZZZZZZZZZZZZZZZZZZZZZ")
2133 .await
2134 .unwrap();
2135 assert_eq!(resolved, "01HZZZZZZZZZZZZZZZZZZZZZZZ");
2136 }
2137
2138 #[tokio::test]
2139 async fn test_resolve_peer_id_by_device_name() {
2140 let ws_port = random_port().await;
2141 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2142
2143 inject_peer_with_identity(
2144 &node,
2145 &event_tx,
2146 "tailscale-abc",
2147 "01HXYZXYZXYZXYZXYZXYZXYZXY",
2148 "Bob's Mac",
2149 )
2150 .await;
2151
2152 let resolved = node.resolve_peer_id("Bob's Mac").await.unwrap();
2153 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
2154 }
2155
2156 #[tokio::test]
2157 async fn test_resolve_peer_id_by_device_id_prefix() {
2158 let ws_port = random_port().await;
2159 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2160
2161 inject_peer_with_identity(
2162 &node,
2163 &event_tx,
2164 "tailscale-abc",
2165 "01HXYZXYZXYZXYZXYZXYZXYZXY",
2166 "laptop",
2167 )
2168 .await;
2169
2170 let resolved = node.resolve_peer_id("01HX").await.unwrap();
2173 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
2174 }
2175
2176 #[tokio::test]
2177 async fn test_resolve_peer_id_by_tailscale_id_legacy() {
2178 let ws_port = random_port().await;
2182 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2183
2184 inject_peer_with_identity(
2185 &node,
2186 &event_tx,
2187 "tailscale-legacy",
2188 "01HLEGACY0000000000000000X",
2189 "legacy box",
2190 )
2191 .await;
2192
2193 let resolved = node.resolve_peer_id("tailscale-legacy").await.unwrap();
2194 assert_eq!(resolved, "01HLEGACY0000000000000000X");
2195 }
2196
2197 #[tokio::test]
2198 async fn test_resolve_peer_id_unknown() {
2199 let ws_port = random_port().await;
2200 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2201 let result = node.resolve_peer_id("nope").await;
2202 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
2203 }
2204
2205 #[tokio::test]
2208 async fn test_resolve_peer_by_bare_name_before_hello() {
2209 let ws_port = random_port().await;
2213 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2214
2215 let mut peer = make_loopback_peer("nodeid-9");
2216 peer.hostname = "truffle-test-ec2-smoke".to_string();
2217 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2218 tokio::time::sleep(Duration::from_millis(50)).await;
2219
2220 assert_eq!(node.resolve_peer("ec2-smoke").await.unwrap().id, "nodeid-9");
2222 assert_eq!(node.resolve_peer("EC2 Smoke").await.unwrap().id, "nodeid-9");
2223 assert!(node.resolve_peer("other-box").await.is_err());
2225 }
2226
2227 #[tokio::test]
2228 async fn test_peers_device_name_strips_hostname_before_hello() {
2229 let ws_port = random_port().await;
2230 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2231
2232 let mut peer = make_loopback_peer("nodeid-9");
2233 peer.hostname = "truffle-test-ec2-smoke".to_string();
2234 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2235 tokio::time::sleep(Duration::from_millis(50)).await;
2236
2237 let peers = node.peers().await;
2238 assert_eq!(peers.len(), 1);
2239 assert!(peers[0].device_name.is_none());
2241 assert_eq!(peers[0].display_name, "ec2-smoke");
2242 assert_eq!(peers[0].hostname, "truffle-test-ec2-smoke");
2243 assert!(peers[0].device_id.is_none());
2244 }
2245
2246 #[tokio::test]
2247 async fn test_resolve_peer_stale_ref_is_peer_gone() {
2248 let ws_port = random_port().await;
2249 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2250
2251 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-9")));
2252 tokio::time::sleep(Duration::from_millis(50)).await;
2253
2254 let live_ref = node.peers().await[0].peer_ref.clone();
2255 assert_eq!(node.resolve_peer(&live_ref).await.unwrap().id, "peer-9");
2256
2257 let _ = event_tx.send(NetworkPeerEvent::Left("peer-9".to_string()));
2261 tokio::time::sleep(Duration::from_millis(50)).await;
2262 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-9")));
2263 tokio::time::sleep(Duration::from_millis(50)).await;
2264
2265 assert!(matches!(
2266 node.resolve_peer(&live_ref).await,
2267 Err(NodeError::PeerGone(_))
2268 ));
2269 assert!(matches!(
2272 node.resolve_peer("peer-9:99").await,
2273 Err(NodeError::PeerGone(_))
2274 ));
2275 assert!(matches!(
2278 node.peer(&live_ref, Some(2_000)).await,
2279 Err(NodeError::PeerGone(_))
2280 ));
2281 }
2282
2283 #[tokio::test]
2284 async fn test_resolve_peer_accepts_ip() {
2285 let ws_port = random_port().await;
2286 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2287
2288 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-a")));
2289 tokio::time::sleep(Duration::from_millis(50)).await;
2290
2291 let resolved = node.resolve_peer("127.0.0.1").await.unwrap();
2292 assert_eq!(resolved.id, "peer-a");
2293 assert_eq!(node.resolve_peer_id("127.0.0.1").await.unwrap(), "peer-a");
2296 }
2297
2298 #[tokio::test]
2299 async fn test_listen_tcp_rejects_reserved_ports() {
2300 let ws_port = random_port().await;
2301 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2302
2303 for port in [443u16, 9417] {
2304 let err = node.listen_tcp(port).await.unwrap_err();
2305 assert!(
2306 matches!(err, NodeError::ReservedPort(p) if p == port),
2307 "expected ReservedPort({port}), got: {err}"
2308 );
2309 }
2310 }
2311
2312 #[tokio::test]
2313 async fn test_listen_quic_rejects_reserved_and_ephemeral_ports() {
2314 let ws_port = random_port().await;
2315 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2316
2317 let err = node.listen_quic(443).await.unwrap_err();
2318 assert!(matches!(err, NodeError::ReservedPort(443)));
2319
2320 let err = node.listen_quic(0).await.unwrap_err();
2321 assert!(matches!(err, NodeError::NotImplemented(_)));
2322 }
2323
2324 #[tokio::test]
2325 async fn test_bind_udp_falls_back_to_direct_socket() {
2326 let ws_port = random_port().await;
2329 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2330
2331 let socket = node.bind_udp(0).await.unwrap();
2332 let addr = socket.local_addr().unwrap();
2333 assert_ne!(addr.port(), 0);
2334 }
2335
2336 #[test]
2337 fn test_raw_alpn_scoping() {
2338 assert_eq!(
2339 crate::transport::quic::raw_alpn("demo"),
2340 b"truffle-raw.demo".to_vec()
2341 );
2342 assert_eq!(
2343 crate::transport::quic::raw_alpn(""),
2344 b"truffle-raw".to_vec()
2345 );
2346 }
2347
2348 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2349 async fn test_connect_quic_roundtrip_via_raw_listener() {
2350 let ws_port = random_port().await;
2351 let (client_node, event_tx, _net) = make_test_node("cli", ws_port).await;
2352
2353 let server_network = Arc::new(MockNetworkProvider::new("srv"));
2356 let alpn = crate::transport::quic::raw_alpn("test");
2357 let listener = crate::transport::quic::listen_raw(&server_network, 0, &alpn)
2358 .await
2359 .unwrap();
2360 let port = listener.port();
2361 assert_ne!(port, 0);
2362
2363 let echo_task = tokio::spawn(async move {
2364 let conn = listener.accept().await.expect("no incoming connection");
2365 let mut stream = conn
2366 .accept_stream()
2367 .await
2368 .unwrap()
2369 .expect("connection closed before stream");
2370 let mut received = Vec::new();
2371 while let Some(chunk) = stream.read(1024).await.unwrap() {
2372 received.extend_from_slice(&chunk);
2373 }
2374 stream.write(&received).await.unwrap();
2375 stream.finish();
2376 tokio::time::sleep(Duration::from_millis(500)).await;
2379 conn.close();
2380 });
2381
2382 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("srv-peer")));
2384 tokio::time::sleep(Duration::from_millis(50)).await;
2385
2386 let conn = client_node.connect_quic("srv-peer", port).await.unwrap();
2387 let mut stream = conn.open_stream().await.unwrap();
2388 stream.write(b"hello quic").await.unwrap();
2389 stream.finish();
2390
2391 let mut echoed = Vec::new();
2392 while let Some(chunk) = stream.read(1024).await.unwrap() {
2393 echoed.extend_from_slice(&chunk);
2394 }
2395 assert_eq!(echoed, b"hello quic");
2396
2397 echo_task.await.unwrap();
2398 conn.close();
2399 }
2400
2401 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2402 async fn test_quic_raw_alpn_mismatch_rejected() {
2403 let server_network = Arc::new(MockNetworkProvider::new("srv"));
2406 let listener = crate::transport::quic::listen_raw(
2407 &server_network,
2408 0,
2409 &crate::transport::quic::raw_alpn("app-a"),
2410 )
2411 .await
2412 .unwrap();
2413 let port = listener.port();
2414
2415 let client_network = Arc::new(MockNetworkProvider::new("cli"));
2416 let result = crate::transport::quic::connect_raw(
2417 &client_network,
2418 "127.0.0.1",
2419 port,
2420 &crate::transport::quic::raw_alpn("app-b"),
2421 )
2422 .await;
2423
2424 assert!(
2425 result.is_err(),
2426 "cross-app QUIC connect must fail (ALPN mismatch)"
2427 );
2428 }
2429
2430 #[tokio::test]
2433 async fn test_rfc022_node_peer_resolves_and_wait_miss_returns_none() {
2434 let ws_port = random_port().await;
2435 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2436
2437 assert!(node.peer("nope", None).await.unwrap().is_none());
2439
2440 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-a")));
2441 tokio::time::sleep(Duration::from_millis(50)).await;
2442
2443 let p = node.peer("peer-a", None).await.unwrap().expect("found");
2444 assert_eq!(p.tailscale_id, "peer-a");
2445 assert!(p.device_id.is_none());
2446
2447 let start = std::time::Instant::now();
2449 let miss = node.peer("still-missing", Some(80)).await.unwrap();
2450 assert!(miss.is_none());
2451 assert!(start.elapsed() >= Duration::from_millis(70));
2452 }
2453
2454 #[test]
2455 fn test_rfc022_peer_projection_never_uses_tailscale_as_device_id() {
2456 use crate::session::PeerState;
2457 use std::net::Ipv4Addr;
2458
2459 let pre = PeerState {
2461 id: "ts-abc".into(),
2462 generation: 1,
2463 name: "truffle-app-laptop".into(),
2464 ip: Ipv4Addr::new(100, 64, 0, 1).into(),
2465 online: true,
2466 ws_connected: false,
2467 connection_type: "direct".into(),
2468 os: None,
2469 last_seen: None,
2470 identity: None,
2471 identity_suppressed: false,
2472 };
2473 let p = Peer::from(pre);
2474 assert!(p.device_id.is_none());
2475 assert_eq!(p.tailscale_id, "ts-abc");
2476 assert_eq!(p.peer_ref, "ts-abc:1");
2477 assert_eq!(p.display_name, "laptop");
2478 assert!(p
2479 .device_id
2480 .as_ref()
2481 .map(|d| d.as_str() != p.tailscale_id)
2482 .unwrap_or(true));
2483
2484 let post = PeerState {
2486 id: "ts-abc".into(),
2487 generation: 1,
2488 name: "truffle-app-laptop".into(),
2489 ip: Ipv4Addr::new(100, 64, 0, 1).into(),
2490 online: true,
2491 ws_connected: true,
2492 connection_type: "direct".into(),
2493 os: Some("darwin".into()),
2494 last_seen: None,
2495 identity: Some(crate::session::PeerIdentity {
2496 app_id: "app".into(),
2497 device_id: "01J4K9M2Z8AB3RNYQPW6H5TC0X".into(),
2498 device_name: "Alice's Mac".into(),
2499 os: "darwin".into(),
2500 tailscale_id: "ts-abc".into(),
2501 }),
2502 identity_suppressed: false,
2503 };
2504 let p = Peer::from(post);
2505 assert_eq!(p.device_id.as_deref(), Some("01J4K9M2Z8AB3RNYQPW6H5TC0X"));
2506 assert_ne!(p.device_id.as_deref().unwrap(), p.tailscale_id);
2507 assert_eq!(p.display_name, "Alice's Mac");
2508
2509 let suppressed = PeerState {
2511 id: "ts-xyz".into(),
2512 generation: 1,
2513 name: "truffle-app-other".into(),
2514 ip: Ipv4Addr::new(100, 64, 0, 2).into(),
2515 online: true,
2516 ws_connected: true,
2517 connection_type: "direct".into(),
2518 os: None,
2519 last_seen: None,
2520 identity: Some(crate::session::PeerIdentity {
2521 app_id: "app".into(),
2522 device_id: "01J4K9M2Z8AB3RNYQPW6H5TC0X".into(),
2523 device_name: "Clone".into(),
2524 os: "linux".into(),
2525 tailscale_id: "ts-xyz".into(),
2526 }),
2527 identity_suppressed: true,
2528 };
2529 let p = Peer::from(suppressed);
2530 assert!(
2531 p.device_id.is_none(),
2532 "suppressed claim must project null device_id"
2533 );
2534 }
2535}