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::{DialOpts, HealthInfo, NetworkProvider, NodeIdentity, PingResult};
59use crate::session::{PeerEvent, PeerRegistry, PeerState};
60use crate::transport::quic::{QuicConnection, QuicListener};
61use crate::transport::websocket::WebSocketTransport;
62use crate::transport::{DatagramSocket, RawListener, WsConfig};
63
64#[derive(Debug, Clone)]
73pub struct NamespacedMessage {
74 pub from: String,
76 pub namespace: String,
78 pub msg_type: String,
80 pub payload: serde_json::Value,
82 pub timestamp: Option<u64>,
84}
85
86#[derive(Debug, Clone)]
102pub struct Peer {
103 pub id: String,
108 pub name: String,
111 pub device_id: String,
115 pub device_name: String,
119 pub tailscale_id: String,
122 pub ip: IpAddr,
124 pub online: bool,
126 pub ws_connected: bool,
128 pub connection_type: String,
130 pub os: Option<String>,
133 pub last_seen: Option<String>,
135}
136
137impl From<PeerState> for Peer {
138 fn from(s: PeerState) -> Self {
139 let (device_id, device_name, os) = match s.identity.as_ref() {
140 Some(identity) => (
141 identity.device_id.clone(),
142 identity.device_name.clone(),
143 Some(identity.os.clone()),
144 ),
145 None => (s.id.clone(), s.name.clone(), s.os.clone()),
146 };
147 let legacy_id = s
148 .identity
149 .as_ref()
150 .map(|i| i.device_id.clone())
151 .unwrap_or_else(|| s.id.clone());
152 Self {
153 id: legacy_id,
154 name: s.name,
155 device_id,
156 device_name,
157 tailscale_id: s.id,
158 ip: s.ip,
159 online: s.online,
160 ws_connected: s.ws_connected,
161 connection_type: s.connection_type,
162 os,
163 last_seen: s.last_seen,
164 }
165 }
166}
167
168#[derive(Debug, thiserror::Error)]
174pub enum NodeError {
175 #[error("peer not found: {0}")]
177 PeerNotFound(String),
178
179 #[error("connection failed: {0}")]
181 ConnectionFailed(String),
182
183 #[error("send failed: {0}")]
185 SendFailed(String),
186
187 #[error("envelope error: {0}")]
189 Envelope(#[from] EnvelopeError),
190
191 #[error("session error: {0}")]
193 Session(#[from] crate::session::SessionError),
194
195 #[error("network error: {0}")]
197 Network(#[from] crate::network::NetworkError),
198
199 #[error("transport error: {0}")]
201 Transport(#[from] crate::transport::TransportError),
202
203 #[error("not implemented: {0}")]
205 NotImplemented(String),
206
207 #[error("port {0} is reserved by truffle (443 = sidecar TLS, or the session WebSocket port)")]
209 ReservedPort(u16),
210
211 #[error("node stopped")]
213 Stopped,
214
215 #[error("build error: {0}")]
217 BuildError(String),
218
219 #[error("io error: {0}")]
221 Io(#[from] std::io::Error),
222}
223
224pub struct Node<N: NetworkProvider + 'static> {
240 pub(crate) network: Arc<N>,
242 session: Arc<PeerRegistry<N>>,
244 codec: Arc<dyn EnvelopeCodec>,
246 #[allow(dead_code)]
249 incoming_tx: broadcast::Sender<NamespacedMessage>,
250 namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
258 pub(crate) file_transfer_state: FileTransferState,
260 state_dir: PathBuf,
263 ws_port: u16,
266 pub(crate) proxy_state: crate::proxy::ProxyState,
268 stopped: std::sync::atomic::AtomicBool,
272}
273
274impl<N: NetworkProvider + 'static> Node<N> {
275 pub(crate) fn from_parts(
281 network: Arc<N>,
282 session: Arc<PeerRegistry<N>>,
283 codec: Arc<dyn EnvelopeCodec>,
284 ) -> Self {
285 let (incoming_tx, _) = broadcast::channel(1024);
286 let namespace_filters: Arc<
287 StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>,
288 > = Arc::new(StdRwLock::new(HashMap::new()));
289
290 let node = Self {
291 network,
292 session: session.clone(),
293 codec: codec.clone(),
294 incoming_tx: incoming_tx.clone(),
295 namespace_filters: namespace_filters.clone(),
296 file_transfer_state: FileTransferState::new(),
297 state_dir: PathBuf::new(),
298 ws_port: 9417,
299 proxy_state: crate::proxy::ProxyState::new(),
300 stopped: std::sync::atomic::AtomicBool::new(false),
301 };
302
303 node.spawn_envelope_router(session, codec, incoming_tx, namespace_filters);
305
306 node
307 }
308
309 fn spawn_envelope_router(
313 &self,
314 session: Arc<PeerRegistry<N>>,
315 codec: Arc<dyn EnvelopeCodec>,
316 incoming_tx: broadcast::Sender<NamespacedMessage>,
317 namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
318 ) {
319 let mut rx = session.subscribe();
320
321 tokio::spawn(async move {
322 loop {
323 match rx.recv().await {
324 Ok(msg) => {
325 if let Ok(envelope) = codec.decode(&msg.data) {
326 let namespaced = NamespacedMessage {
327 from: msg.from,
328 namespace: envelope.namespace.clone(),
329 msg_type: envelope.msg_type,
330 payload: envelope.payload,
331 timestamp: envelope.timestamp,
332 };
333
334 tracing::debug!(
335 from = %namespaced.from,
336 namespace = %namespaced.namespace,
337 msg_type = %namespaced.msg_type,
338 "envelope router: dispatching message"
339 );
340
341 let _ = incoming_tx.send(namespaced.clone());
343
344 let filters = namespace_filters
348 .read()
349 .expect("namespace_filters std RwLock poisoned");
350 let _has_subscriber = filters.contains_key(&namespaced.namespace);
351 if let Some(tx) = filters.get(&namespaced.namespace) {
352 let send_result = tx.send(namespaced);
353 tracing::debug!(
354 namespace = %envelope.namespace,
355 subscriber_count = tx.receiver_count(),
356 sent = send_result.is_ok(),
357 "envelope router: sent to namespace subscriber"
358 );
359 } else {
360 tracing::debug!(
361 namespace = %envelope.namespace,
362 "envelope router: no subscriber for namespace"
363 );
364 }
365 } else {
366 tracing::warn!(
367 from = %msg.from,
368 data_len = msg.data.len(),
369 "node: failed to decode envelope from incoming message"
370 );
371 }
372 }
373 Err(broadcast::error::RecvError::Lagged(n)) => {
374 tracing::warn!(
375 missed = n,
376 "node: envelope router lagged, missed {n} messages"
377 );
378 continue;
379 }
380 Err(broadcast::error::RecvError::Closed) => {
381 tracing::debug!("node: session incoming channel closed, router exiting");
382 break;
383 }
384 }
385 }
386 });
387 }
388
389 pub fn builder() -> NodeBuilder {
393 NodeBuilder::default()
394 }
395
396 pub fn file_transfer(&self) -> file_transfer::FileTransfer<'_, N> {
403 file_transfer::FileTransfer::new(self)
404 }
405
406 pub fn proxy(&self) -> crate::proxy::Proxy<'_, N> {
413 crate::proxy::Proxy::new(self)
414 }
415
416 pub fn synced_store<T>(
425 self: &Arc<Self>,
426 store_id: &str,
427 ) -> Arc<crate::synced_store::SyncedStore<T>>
428 where
429 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
430 {
431 crate::synced_store::SyncedStore::new(self.clone(), store_id)
432 }
433
434 pub fn synced_store_with_backend<T>(
439 self: &Arc<Self>,
440 store_id: &str,
441 backend: std::sync::Arc<dyn crate::synced_store::StoreBackend>,
442 ) -> Arc<crate::synced_store::SyncedStore<T>>
443 where
444 T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
445 {
446 crate::synced_store::SyncedStore::new_with_backend(self.clone(), store_id, backend)
447 }
448
449 pub async fn crdt_doc(
457 self: &Arc<Self>,
458 doc_id: &str,
459 ) -> Result<Arc<crate::crdt_doc::CrdtDoc>, crate::crdt_doc::CrdtDocError> {
460 crate::crdt_doc::CrdtDoc::new(self.clone(), doc_id).await
461 }
462
463 pub async fn crdt_doc_with_backend(
468 self: &Arc<Self>,
469 doc_id: &str,
470 backend: Arc<dyn crate::crdt_doc::CrdtBackend>,
471 ) -> Result<Arc<crate::crdt_doc::CrdtDoc>, crate::crdt_doc::CrdtDocError> {
472 crate::crdt_doc::CrdtDoc::new_with_backend(self.clone(), doc_id, backend).await
473 }
474
475 pub(crate) fn with_state_dir(mut self, dir: PathBuf) -> Self {
479 self.state_dir = dir;
480 self
481 }
482
483 pub(crate) fn with_ws_port(mut self, port: u16) -> Self {
486 self.ws_port = port;
487 self
488 }
489
490 pub fn state_dir(&self) -> &Path {
494 &self.state_dir
495 }
496
497 pub async fn stop(&self) {
514 if self.stopped.swap(true, std::sync::atomic::Ordering::SeqCst) {
515 tracing::debug!("node: stop() called on already-stopped node");
516 return;
517 }
518 tracing::info!("node: stopping");
519 self.session.shutdown().await;
521 if let Err(e) = self.network.stop().await {
523 tracing::warn!(error = %e, "node: network provider stop failed");
524 }
525 tracing::info!("node: stopped");
526 }
527
528 pub fn local_info(&self) -> NodeIdentity {
532 self.network.local_identity()
533 }
534
535 pub async fn peers(&self) -> Vec<Peer> {
542 let app_id = self.network.local_identity().app_id;
543 self.session
544 .peers()
545 .await
546 .into_iter()
547 .map(|s| {
548 let bare = s
553 .identity
554 .is_none()
555 .then(|| hostname_slug(&s.name, &app_id).map(str::to_string))
556 .flatten();
557 let mut peer = Peer::from(s);
558 if let Some(bare) = bare {
559 peer.device_name = bare;
560 }
561 peer
562 })
563 .collect()
564 }
565
566 pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
568 self.session.on_peer_change()
569 }
570
571 pub(crate) async fn resolve_peer(&self, peer_ref: &str) -> Result<PeerState, NodeError> {
579 let peers = self.session.peers().await;
580
581 for p in &peers {
585 if let Some(identity) = p.identity.as_ref() {
586 if identity.device_id == peer_ref || identity.device_name == peer_ref {
587 return Ok(p.clone());
588 }
589 }
590 if p.id == peer_ref || p.name == peer_ref || p.ip.to_string() == peer_ref {
591 return Ok(p.clone());
592 }
593 }
594
595 let app_id = self.network.local_identity().app_id;
601 let ref_slug = identity::slug(peer_ref, 255);
602 if !ref_slug.is_empty() {
603 for p in &peers {
604 if hostname_slug(&p.name, &app_id) == Some(ref_slug.as_str()) {
605 return Ok(p.clone());
606 }
607 }
608 }
609
610 if peer_ref.len() >= 4 {
614 let mut hits = peers.iter().filter(|p| {
615 p.identity
616 .as_ref()
617 .map(|i| i.device_id.starts_with(peer_ref))
618 .unwrap_or(false)
619 });
620 if let (Some(hit), None) = (hits.next(), hits.next()) {
621 return Ok(hit.clone());
622 }
623 }
624
625 Err(NodeError::PeerNotFound(peer_ref.to_string()))
626 }
627
628 pub async fn resolve_peer_ip(&self, peer_ref: &str) -> Result<IpAddr, NodeError> {
634 Ok(self.resolve_peer(peer_ref).await?.ip)
635 }
636
637 pub async fn resolve_peer_id(&self, peer_id: &str) -> Result<String, NodeError> {
656 let p = self.resolve_peer(peer_id).await?;
657 Ok(p.identity
658 .as_ref()
659 .map(|i| i.device_id.clone())
660 .unwrap_or_else(|| p.id.clone()))
661 }
662
663 pub async fn ping(&self, peer_id: &str) -> Result<PingResult, NodeError> {
670 let peer = self.resolve_peer(peer_id).await?;
671 let addr = peer.ip.to_string();
672 self.network.ping(&addr).await.map_err(NodeError::Network)
673 }
674
675 pub async fn health(&self) -> HealthInfo {
677 self.network.health().await
678 }
679
680 fn ensure_not_stopped(&self) -> Result<(), NodeError> {
687 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
688 return Err(NodeError::Stopped);
689 }
690 Ok(())
691 }
692
693 pub async fn send(&self, peer_id: &str, namespace: &str, data: &[u8]) -> Result<(), NodeError> {
701 self.ensure_not_stopped()?;
702 let payload = std::str::from_utf8(data)
708 .ok()
709 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
710 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
711
712 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
713
714 let encoded = self.codec.encode(&envelope)?;
715 self.session.send(peer_id, &encoded).await?;
716 Ok(())
717 }
718
719 pub async fn send_typed(
727 &self,
728 peer_id: &str,
729 namespace: &str,
730 msg_type: &str,
731 payload: &serde_json::Value,
732 ) -> Result<(), NodeError> {
733 self.ensure_not_stopped()?;
734 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
735 let encoded = self.codec.encode(&envelope)?;
736 self.session.send(peer_id, &encoded).await?;
737 Ok(())
738 }
739
740 pub async fn broadcast_typed(
743 &self,
744 namespace: &str,
745 msg_type: &str,
746 payload: &serde_json::Value,
747 ) {
748 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
749 tracing::debug!("node: broadcast after stop ignored");
750 return;
751 }
752 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
753 match self.codec.encode(&envelope) {
754 Ok(encoded) => {
755 self.session.broadcast(&encoded).await;
756 }
757 Err(e) => {
758 tracing::error!("node: failed to encode broadcast envelope: {e}");
759 }
760 }
761 }
762
763 pub async fn broadcast(&self, namespace: &str, data: &[u8]) {
768 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
769 tracing::debug!("node: broadcast after stop ignored");
770 return;
771 }
772 let payload = std::str::from_utf8(data)
773 .ok()
774 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
775 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
776
777 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
778
779 match self.codec.encode(&envelope) {
780 Ok(encoded) => {
781 self.session.broadcast(&encoded).await;
782 }
783 Err(e) => {
784 tracing::error!("node: failed to encode broadcast envelope: {e}");
785 }
786 }
787 }
788
789 pub fn subscribe(&self, namespace: &str) -> broadcast::Receiver<NamespacedMessage> {
795 {
797 let filters = self
798 .namespace_filters
799 .read()
800 .expect("namespace_filters std RwLock poisoned");
801 if let Some(tx) = filters.get(namespace) {
802 return tx.subscribe();
803 }
804 }
805
806 let mut filters = self
808 .namespace_filters
809 .write()
810 .expect("namespace_filters std RwLock poisoned");
811 if let Some(tx) = filters.get(namespace) {
813 return tx.subscribe();
814 }
815 let (tx, rx) = broadcast::channel(256);
816 filters.insert(namespace.to_string(), tx);
817 rx
818 }
819
820 pub async fn open_tcp(&self, peer_id: &str, port: u16) -> Result<TcpStream, NodeError> {
832 let peer = self.resolve_peer(peer_id).await?;
833 let addr = peer.ip.to_string();
834 self.network
835 .dial_tcp_opts(&addr, port, DialOpts { tls: Some(false) })
836 .await
837 .map_err(|e| NodeError::ConnectionFailed(e.to_string()))
838 }
839
840 pub async fn listen_tcp(&self, port: u16) -> Result<RawListener, NodeError> {
848 use crate::transport::tcp::TcpTransport;
849 use crate::transport::RawTransport;
850
851 ensure_port_unreserved(port, self.ws_port)?;
852 let tcp = TcpTransport::new(self.network.clone());
853 tcp.listen(port).await.map_err(NodeError::Transport)
854 }
855
856 pub async fn unlisten_tcp(&self, port: u16) -> Result<(), NodeError> {
861 self.network
862 .unlisten_tcp(port)
863 .await
864 .map_err(NodeError::Network)
865 }
866
867 pub async fn connect_quic(
876 &self,
877 peer_id: &str,
878 port: u16,
879 ) -> Result<QuicConnection, NodeError> {
880 let peer = self.resolve_peer(peer_id).await?;
881 let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
882 crate::transport::quic::connect_raw(&self.network, &peer.ip.to_string(), port, &alpn)
883 .await
884 .map_err(NodeError::Transport)
885 }
886
887 pub async fn listen_quic(&self, port: u16) -> Result<QuicListener, NodeError> {
895 ensure_port_unreserved(port, self.ws_port)?;
896 if port == 0 {
897 return Err(NodeError::NotImplemented(
898 "ephemeral (port 0) QUIC listeners are not supported over the tsnet relay yet — choose an explicit port"
899 .to_string(),
900 ));
901 }
902 let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
903 crate::transport::quic::listen_raw(&self.network, port, &alpn)
904 .await
905 .map_err(NodeError::Transport)
906 }
907
908 pub async fn bind_udp(&self, port: u16) -> Result<DatagramSocket, NodeError> {
918 use crate::transport::udp::{UdpConfig, UdpTransport};
919 use crate::transport::DatagramTransport;
920
921 let udp = UdpTransport::new(self.network.clone(), UdpConfig::default());
922 udp.bind(port).await.map_err(NodeError::Transport)
923 }
924}
925
926fn hostname_slug<'a>(hostname: &'a str, app_id: &str) -> Option<&'a str> {
929 hostname
930 .strip_prefix("truffle-")?
931 .strip_prefix(app_id)?
932 .strip_prefix('-')
933}
934
935fn ensure_port_unreserved(port: u16, ws_port: u16) -> Result<(), NodeError> {
938 if port == 443 || port == ws_port {
939 Err(NodeError::ReservedPort(port))
940 } else {
941 Ok(())
942 }
943}
944
945#[derive(Debug, Clone)]
966pub struct NodeBuilder {
967 app_id: Option<AppId>,
968 device_name: Option<DeviceName>,
969 device_id: Option<DeviceId>,
970 sidecar_path: Option<PathBuf>,
971 state_dir: Option<String>,
972 auth_key: Option<String>,
973 ephemeral: bool,
974 ws_port: u16,
975 idle_timeout_secs: Option<u64>,
976}
977
978fn atomic_write_string(path: &Path, content: &str) -> std::io::Result<()> {
987 let _parent = path.parent().ok_or_else(|| {
988 std::io::Error::new(
989 std::io::ErrorKind::InvalidInput,
990 "path has no parent directory",
991 )
992 })?;
993 let mut tmp = path.to_path_buf();
994 tmp.set_extension("tmp");
995 std::fs::write(&tmp, content)?;
996 #[cfg(unix)]
997 {
998 std::fs::rename(&tmp, path)?;
999 }
1000 #[cfg(windows)]
1001 {
1002 let _ = std::fs::remove_file(path);
1005 std::fs::rename(&tmp, path)?;
1006 }
1007 Ok(())
1008}
1009
1010impl Default for NodeBuilder {
1011 fn default() -> Self {
1012 Self {
1013 app_id: None,
1014 device_name: None,
1015 device_id: None,
1016 sidecar_path: None,
1017 state_dir: None,
1018 auth_key: None,
1019 ephemeral: false,
1020 ws_port: 9417,
1021 idle_timeout_secs: None,
1022 }
1023 }
1024}
1025
1026impl NodeBuilder {
1027 pub fn app_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1032 let raw: String = s.into();
1033 let app_id = AppId::parse(&raw)
1034 .map_err(|e| NodeError::BuildError(format!("invalid app_id: {e}")))?;
1035 self.app_id = Some(app_id);
1036 Ok(self)
1037 }
1038
1039 pub fn device_name(mut self, s: impl Into<String>) -> Self {
1044 self.device_name = Some(DeviceName::parse(s));
1045 self
1046 }
1047
1048 pub fn device_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1054 let raw: String = s.into();
1055 let device_id = DeviceId::parse(&raw)
1056 .map_err(|e| NodeError::BuildError(format!("invalid device_id: {e}")))?;
1057 self.device_id = Some(device_id);
1058 Ok(self)
1059 }
1060
1061 pub fn sidecar_path(mut self, path: impl Into<PathBuf>) -> Self {
1063 self.sidecar_path = Some(path.into());
1064 self
1065 }
1066
1067 pub fn state_dir(mut self, dir: &str) -> Self {
1069 self.state_dir = Some(dir.to_string());
1070 self
1071 }
1072
1073 pub fn auth_key(mut self, key: &str) -> Self {
1075 self.auth_key = Some(key.to_string());
1076 self
1077 }
1078
1079 pub fn ephemeral(mut self, val: bool) -> Self {
1081 self.ephemeral = val;
1082 self
1083 }
1084
1085 pub fn ws_port(mut self, port: u16) -> Self {
1087 self.ws_port = port;
1088 self
1089 }
1090
1091 pub fn idle_timeout_secs(mut self, secs: u64) -> Self {
1097 self.idle_timeout_secs = Some(secs);
1098 self
1099 }
1100
1101 fn prepare_config(&self) -> Result<TailscaleConfig, NodeError> {
1108 let binary_path = self
1110 .sidecar_path
1111 .clone()
1112 .ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
1113
1114 let app_id = self
1116 .app_id
1117 .clone()
1118 .ok_or_else(|| NodeError::BuildError("app_id is required".into()))?;
1119
1120 let device_name = match self.device_name.clone() {
1122 Some(name) => name,
1123 None => {
1124 let os_hostname = hostname::get()
1125 .map_err(|e| {
1126 NodeError::BuildError(format!(
1127 "device_name is unset and hostname::get() failed: {e}"
1128 ))
1129 })?
1130 .to_string_lossy()
1131 .into_owned();
1132 DeviceName::parse(os_hostname)
1133 }
1134 };
1135
1136 let tailscale_host = identity::tailscale_hostname(&app_id, &device_name);
1139
1140 let state_dir = self.state_dir.clone().unwrap_or_else(|| {
1143 let base = dirs::data_dir().unwrap_or_else(|| {
1144 tracing::warn!(
1145 "dirs::data_dir() returned None, falling back to std::env::temp_dir()"
1146 );
1147 std::env::temp_dir()
1148 });
1149 base.join("truffle")
1150 .join(app_id.as_str())
1151 .join(identity::slug(device_name.as_str(), 255))
1152 .to_string_lossy()
1153 .into_owned()
1154 });
1155
1156 std::fs::create_dir_all(&state_dir)?;
1158
1159 let device_id_file = Path::new(&state_dir).join("device-id.txt");
1164 let device_id = match self.device_id.clone() {
1165 Some(id) => {
1166 atomic_write_string(&device_id_file, id.as_str())?;
1168 id
1169 }
1170 None => {
1171 if device_id_file.exists() {
1172 let s = std::fs::read_to_string(&device_id_file)?.trim().to_string();
1173 DeviceId::parse(&s).map_err(|e| {
1174 NodeError::BuildError(format!(
1175 "device-id.txt at {device_id_file:?} contains an invalid ULID: {e}"
1176 ))
1177 })?
1178 } else {
1179 let id = DeviceId::generate();
1180 atomic_write_string(&device_id_file, id.as_str())?;
1181 id
1182 }
1183 }
1184 };
1185
1186 Ok(TailscaleConfig {
1187 binary_path,
1188 app_id: app_id.as_str().to_string(),
1189 device_id: device_id.as_str().to_string(),
1190 device_name: device_name.as_str().to_string(),
1191 hostname: tailscale_host,
1192 state_dir,
1193 auth_key: self.auth_key.clone(),
1194 ephemeral: if self.ephemeral { Some(true) } else { None },
1195 tags: None,
1196 idle_timeout_secs: self.idle_timeout_secs,
1197 })
1198 }
1199
1200 pub async fn build(self) -> Result<Node<TailscaleProvider>, NodeError> {
1211 let ws_port = self.ws_port;
1212 let config = self.prepare_config()?;
1213 let state_dir = PathBuf::from(&config.state_dir);
1214
1215 let mut provider = TailscaleProvider::new(config);
1216 provider.start().await.map_err(NodeError::Network)?;
1217
1218 let network = Arc::new(provider);
1219
1220 let ws_config = WsConfig {
1222 port: ws_port,
1223 ..Default::default()
1224 };
1225 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1226
1227 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1229 session.start().await;
1230
1231 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1233 let node = Node::from_parts(network, session, codec)
1234 .with_state_dir(state_dir)
1235 .with_ws_port(ws_port);
1236
1237 tracing::info!("node: started successfully");
1238 Ok(node)
1239 }
1240
1241 pub async fn build_with_auth_handler(
1253 self,
1254 on_auth: impl Fn(String) + Send + 'static,
1255 ) -> Result<Node<TailscaleProvider>, NodeError> {
1256 let ws_port = self.ws_port;
1257 let config = self.prepare_config()?;
1258 let state_dir = PathBuf::from(&config.state_dir);
1259
1260 let mut provider = TailscaleProvider::new(config);
1261
1262 let mut auth_rx = provider.peer_events();
1264
1265 let auth_task = tokio::spawn(async move {
1267 use crate::network::NetworkPeerEvent;
1268 loop {
1269 match auth_rx.recv().await {
1270 Ok(NetworkPeerEvent::AuthRequired { url }) => {
1271 on_auth(url);
1272 }
1273 Err(broadcast::error::RecvError::Closed) => break,
1274 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1275 _ => {} }
1277 }
1278 });
1279
1280 let start_result = provider.start().await.map_err(NodeError::Network);
1282
1283 auth_task.abort();
1285
1286 start_result?;
1287
1288 let network = Arc::new(provider);
1289
1290 let ws_config = WsConfig {
1292 port: ws_port,
1293 ..Default::default()
1294 };
1295 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1296
1297 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1299 session.start().await;
1300
1301 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1303 let node = Node::from_parts(network, session, codec)
1304 .with_state_dir(state_dir)
1305 .with_ws_port(ws_port);
1306
1307 tracing::info!("node: started successfully (with auth handler)");
1308 Ok(node)
1309 }
1310}
1311
1312#[cfg(test)]
1317mod tests {
1318 use super::*;
1319 use crate::network::{
1320 HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
1321 NetworkTcpListener, NetworkUdpSocket, PeerAddr,
1322 };
1323 use crate::transport::WsConfig;
1324 use serde_json::json;
1325 use std::sync::atomic::{AtomicUsize, Ordering};
1326 use std::time::Duration;
1327 use tokio::sync::{broadcast, mpsc};
1328
1329 struct MockNetworkProvider {
1332 identity: NodeIdentity,
1333 local_addr: PeerAddr,
1334 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
1335 mock_peers: Arc<RwLock<Vec<NetworkPeer>>>,
1337 stop_calls: Arc<AtomicUsize>,
1340 }
1341
1342 impl MockNetworkProvider {
1343 fn new(id: &str) -> Self {
1344 let (peer_event_tx, _) = broadcast::channel(64);
1345 Self {
1346 identity: NodeIdentity {
1347 app_id: "test".to_string(),
1348 device_id: id.to_string(),
1351 device_name: format!("Test Node {id}"),
1352 tailscale_hostname: format!("truffle-test-{id}"),
1353 tailscale_id: id.to_string(),
1354 dns_name: None,
1355 ip: Some("127.0.0.1".parse().unwrap()),
1356 },
1357 local_addr: PeerAddr {
1358 ip: Some("127.0.0.1".parse().unwrap()),
1359 hostname: format!("truffle-test-{id}"),
1360 dns_name: None,
1361 },
1362 peer_event_tx,
1363 mock_peers: Arc::new(RwLock::new(Vec::new())),
1364 stop_calls: Arc::new(AtomicUsize::new(0)),
1365 }
1366 }
1367
1368 fn event_sender(&self) -> broadcast::Sender<NetworkPeerEvent> {
1369 self.peer_event_tx.clone()
1370 }
1371
1372 fn stop_call_count(&self) -> usize {
1374 self.stop_calls.load(Ordering::SeqCst)
1375 }
1376 }
1377
1378 impl NetworkProvider for MockNetworkProvider {
1379 async fn start(&mut self) -> Result<(), NetworkError> {
1380 Ok(())
1381 }
1382
1383 async fn stop(&self) -> Result<(), NetworkError> {
1384 self.stop_calls.fetch_add(1, Ordering::SeqCst);
1385 Ok(())
1386 }
1387
1388 fn local_identity(&self) -> NodeIdentity {
1389 self.identity.clone()
1390 }
1391
1392 fn local_addr(&self) -> PeerAddr {
1393 self.local_addr.clone()
1394 }
1395
1396 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
1397 self.peer_event_tx.subscribe()
1398 }
1399
1400 async fn peers(&self) -> Vec<NetworkPeer> {
1401 self.mock_peers.read().await.clone()
1402 }
1403
1404 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
1405 let target = format!("{addr}:{port}");
1406 TcpStream::connect(&target)
1407 .await
1408 .map_err(|e| NetworkError::DialFailed(format!("mock dial {target}: {e}")))
1409 }
1410
1411 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
1412 let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
1413 .await
1414 .map_err(|e| NetworkError::ListenFailed(format!("mock listen :{port}: {e}")))?;
1415
1416 let actual_port = listener.local_addr().unwrap().port();
1417 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
1418
1419 tokio::spawn(async move {
1420 loop {
1421 match listener.accept().await {
1422 Ok((stream, addr)) => {
1423 let conn = IncomingConnection {
1424 stream,
1425 remote_addr: addr.to_string(),
1426 remote_identity: String::new(),
1427 port: actual_port,
1428 };
1429 if tx.send(conn).await.is_err() {
1430 break;
1431 }
1432 }
1433 Err(e) => {
1434 tracing::debug!("mock listener error: {e}");
1435 break;
1436 }
1437 }
1438 }
1439 });
1440
1441 Ok(NetworkTcpListener {
1442 port: actual_port,
1443 incoming: rx,
1444 })
1445 }
1446
1447 async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
1448 Ok(())
1449 }
1450
1451 async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
1452 Err(NetworkError::Internal("mock: UDP not supported".into()))
1453 }
1454
1455 async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
1456 Ok(PingResult {
1457 latency: Duration::from_millis(1),
1458 connection: "direct".to_string(),
1459 peer_addr: None,
1460 })
1461 }
1462
1463 async fn health(&self) -> HealthInfo {
1464 HealthInfo {
1465 state: "running".to_string(),
1466 healthy: true,
1467 ..Default::default()
1468 }
1469 }
1470 }
1471
1472 fn make_loopback_peer(id: &str) -> NetworkPeer {
1475 NetworkPeer {
1476 id: id.to_string(),
1477 hostname: format!("truffle-test-{id}"),
1478 ip: "127.0.0.1".parse().unwrap(),
1479 online: true,
1480 cur_addr: Some("127.0.0.1:41641".to_string()),
1481 relay: None,
1482 os: Some("linux".to_string()),
1483 last_seen: Some("2026-03-25T12:00:00Z".to_string()),
1484 key_expiry: None,
1485 dns_name: None,
1486 }
1487 }
1488
1489 fn ws_config(port: u16) -> WsConfig {
1490 WsConfig {
1491 port,
1492 ping_interval: Duration::from_secs(300),
1493 pong_timeout: Duration::from_secs(300),
1494 ..Default::default()
1495 }
1496 }
1497
1498 async fn random_port() -> u16 {
1499 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1500 l.local_addr().unwrap().port()
1501 }
1502
1503 async fn make_test_node(
1505 id: &str,
1506 ws_port: u16,
1507 ) -> (
1508 Node<MockNetworkProvider>,
1509 broadcast::Sender<NetworkPeerEvent>,
1510 Arc<MockNetworkProvider>,
1511 ) {
1512 let provider = MockNetworkProvider::new(id);
1513 let event_tx = provider.event_sender();
1514 let network = Arc::new(provider);
1515 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config(ws_port)));
1516 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1517 session.start().await;
1518
1519 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1520 let node = Node::from_parts(network.clone(), session, codec);
1521
1522 (node, event_tx, network)
1523 }
1524
1525 #[tokio::test]
1528 async fn test_node_builder_creates_node() {
1529 let ws_port = random_port().await;
1530 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1531
1532 let identity = node.local_info();
1533 assert_eq!(identity.tailscale_id, "node-1");
1534 assert_eq!(identity.device_id, "node-1");
1535 assert!(identity.tailscale_hostname.contains("node-1"));
1536 }
1537
1538 #[tokio::test]
1539 async fn test_node_peers_from_network() {
1540 let ws_port = random_port().await;
1541 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1542
1543 let peers = node.peers().await;
1545 assert!(peers.is_empty());
1546
1547 let peer = make_loopback_peer("peer-a");
1549 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1550 tokio::time::sleep(Duration::from_millis(50)).await;
1551
1552 let peers = node.peers().await;
1553 assert_eq!(peers.len(), 1);
1554 assert_eq!(peers[0].id, "peer-a");
1555 assert!(peers[0].online);
1556 assert!(!peers[0].ws_connected);
1557 }
1558
1559 #[tokio::test]
1560 async fn test_node_send_to_unknown_peer_errors() {
1561 let ws_port = random_port().await;
1562 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1563
1564 let result = node.send("nonexistent", "test", b"hello").await;
1565 assert!(result.is_err());
1566 let err_str = result.unwrap_err().to_string();
1567 assert!(
1568 err_str.contains("unknown peer") || err_str.contains("not found"),
1569 "expected unknown peer error, got: {err_str}"
1570 );
1571 }
1572
1573 #[tokio::test]
1574 async fn test_node_send_wraps_in_envelope() {
1575 let codec = JsonCodec;
1578 let data = b"hello world";
1579 let envelope = Envelope::new("test-ns", "message", serde_json::Value::from(data.to_vec()))
1580 .with_timestamp();
1581
1582 let encoded = codec.encode(&envelope).unwrap();
1583 let decoded = codec.decode(&encoded).unwrap();
1584
1585 assert_eq!(decoded.namespace, "test-ns");
1586 assert_eq!(decoded.msg_type, "message");
1587 assert!(decoded.timestamp.is_some());
1588 }
1589
1590 #[tokio::test]
1591 async fn test_node_subscribe_filters_by_namespace() {
1592 let ws_port = random_port().await;
1593 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1594
1595 let _rx_chat = node.subscribe("chat");
1597 let _rx_ft = node.subscribe("ft");
1598
1599 let _rx_chat2 = node.subscribe("chat");
1601 }
1602
1603 #[tokio::test]
1604 async fn test_node_broadcast() {
1605 let ws_port = random_port().await;
1606 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1607
1608 node.broadcast("test", b"hello everyone").await;
1610 }
1611
1612 #[tokio::test]
1613 async fn test_node_open_tcp_resolves_peer() {
1614 let ws_port = random_port().await;
1615 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1616
1617 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1619 let tcp_port = listener.local_addr().unwrap().port();
1620
1621 let peer = make_loopback_peer("peer-tcp");
1623 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1624 tokio::time::sleep(Duration::from_millis(50)).await;
1625
1626 let accept_handle = tokio::spawn(async move {
1628 let (stream, _) = listener.accept().await.unwrap();
1629 stream
1630 });
1631
1632 let stream = node.open_tcp("peer-tcp", tcp_port).await;
1634 assert!(stream.is_ok(), "open_tcp failed: {:?}", stream.err());
1635
1636 let _ = accept_handle.await;
1637 }
1638
1639 #[tokio::test]
1640 async fn test_node_open_tcp_unknown_peer_errors() {
1641 let ws_port = random_port().await;
1642 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1643
1644 let result = node.open_tcp("nonexistent", 8080).await;
1645 assert!(result.is_err());
1646 let err_str = result.unwrap_err().to_string();
1647 assert!(
1648 err_str.contains("not found"),
1649 "expected peer not found error, got: {err_str}"
1650 );
1651 }
1652
1653 #[tokio::test]
1654 async fn test_node_ping_resolves_peer() {
1655 let ws_port = random_port().await;
1656 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1657
1658 let result = node.ping("peer-ping").await;
1660 assert!(result.is_err());
1661
1662 let peer = make_loopback_peer("peer-ping");
1664 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1665 tokio::time::sleep(Duration::from_millis(50)).await;
1666
1667 let result = node.ping("peer-ping").await;
1669 assert!(result.is_ok());
1670 assert_eq!(result.unwrap().latency, Duration::from_millis(1));
1671 }
1672
1673 #[tokio::test]
1674 async fn test_node_health() {
1675 let ws_port = random_port().await;
1676 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1677
1678 let health = node.health().await;
1679 assert!(health.healthy);
1680 assert_eq!(health.state, "running");
1681 }
1682
1683 #[tokio::test]
1684 async fn test_node_connect_quic_unknown_peer_errors() {
1685 let ws_port = random_port().await;
1686 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1687
1688 let result = node.connect_quic("peer", 4433).await;
1689 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
1690 }
1691
1692 #[tokio::test]
1693 async fn test_node_listen_tcp() {
1694 let ws_port = random_port().await;
1695 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1696
1697 let listener = node.listen_tcp(0).await;
1699 assert!(listener.is_ok(), "listen_tcp failed: {:?}", listener.err());
1700 }
1701
1702 #[tokio::test]
1703 async fn test_envelope_serialize_deserialize() {
1704 let envelope = Envelope::new("chat", "message", json!({"text": "hello"})).with_timestamp();
1705
1706 let bytes = envelope.serialize().unwrap();
1707 let decoded = Envelope::deserialize(&bytes).unwrap();
1708
1709 assert_eq!(decoded.namespace, "chat");
1710 assert_eq!(decoded.msg_type, "message");
1711 assert_eq!(decoded.payload["text"], "hello");
1712 assert!(decoded.timestamp.is_some());
1713 }
1714
1715 #[tokio::test]
1716 async fn test_envelope_codec_json() {
1717 let codec = JsonCodec;
1718 let envelope = Envelope::new("ft", "offer", json!({"file": "test.bin"}));
1719
1720 let encoded = codec.encode(&envelope).unwrap();
1721 let decoded = codec.decode(&encoded).unwrap();
1722
1723 assert_eq!(decoded.namespace, "ft");
1724 assert_eq!(decoded.payload["file"], "test.bin");
1725 }
1726
1727 #[tokio::test]
1728 async fn test_envelope_unknown_fields_ignored() {
1729 let json_bytes = br#"{
1730 "namespace": "v2",
1731 "msg_type": "new",
1732 "payload": {},
1733 "future_field": "ignored"
1734 }"#;
1735
1736 let codec = JsonCodec;
1737 let decoded = codec.decode(json_bytes).unwrap();
1738 assert_eq!(decoded.namespace, "v2");
1739 assert_eq!(decoded.msg_type, "new");
1740 }
1741
1742 #[tokio::test]
1743 async fn test_node_send_and_receive_roundtrip() {
1744 let port_a = random_port().await;
1746 let port_b = random_port().await;
1747
1748 let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
1749 let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
1750
1751 let peer_b = NetworkPeer {
1753 id: "node-b".to_string(),
1754 hostname: "truffle-test-node-b".to_string(),
1755 ip: "127.0.0.1".parse().unwrap(),
1756 online: true,
1757 cur_addr: Some("127.0.0.1:41641".to_string()),
1758 relay: None,
1759 os: None,
1760 last_seen: None,
1761 key_expiry: None,
1762 dns_name: None,
1763 };
1764 let peer_a = NetworkPeer {
1765 id: "node-a".to_string(),
1766 hostname: "truffle-test-node-a".to_string(),
1767 ip: "127.0.0.1".parse().unwrap(),
1768 online: true,
1769 cur_addr: Some("127.0.0.1:41641".to_string()),
1770 relay: None,
1771 os: None,
1772 last_seen: None,
1773 key_expiry: None,
1774 dns_name: None,
1775 };
1776
1777 let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
1778 let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
1779 tokio::time::sleep(Duration::from_millis(100)).await;
1780
1781 let mut rx = node_b.subscribe("test");
1783
1784 let send_result = node_a.send("node-b", "test", b"hello from a").await;
1787
1788 if send_result.is_ok() {
1796 let msg = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await;
1798 if let Ok(Ok(msg)) = msg {
1799 assert_eq!(msg.namespace, "test");
1800 }
1801 }
1802 }
1805
1806 #[tokio::test]
1809 async fn test_node_stop_shuts_down_provider_and_is_idempotent() {
1810 let ws_port = random_port().await;
1811 let (node, event_tx, network) = make_test_node("node-1", ws_port).await;
1812
1813 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-1")));
1814 tokio::time::sleep(Duration::from_millis(50)).await;
1815
1816 assert_eq!(network.stop_call_count(), 0);
1817 node.stop().await;
1818 assert_eq!(
1819 network.stop_call_count(),
1820 1,
1821 "stop() must shut down the provider"
1822 );
1823
1824 node.stop().await;
1826 assert_eq!(network.stop_call_count(), 1);
1827
1828 let err = node.send("peer-1", "test", b"hi").await.unwrap_err();
1830 assert!(matches!(err, NodeError::Stopped));
1831 }
1832
1833 #[tokio::test]
1834 async fn test_node_stop_closes_ws_connections() {
1835 let port_a = random_port().await;
1838 let port_b = random_port().await;
1839
1840 let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
1841 let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
1842
1843 let peer_b = NetworkPeer {
1844 id: "node-b".to_string(),
1845 hostname: "truffle-test-node-b".to_string(),
1846 ip: "127.0.0.1".parse().unwrap(),
1847 online: true,
1848 cur_addr: Some("127.0.0.1:41641".to_string()),
1849 relay: None,
1850 os: None,
1851 last_seen: None,
1852 key_expiry: None,
1853 dns_name: None,
1854 };
1855 let peer_a = NetworkPeer {
1856 id: "node-a".to_string(),
1857 hostname: "truffle-test-node-a".to_string(),
1858 ip: "127.0.0.1".parse().unwrap(),
1859 online: true,
1860 cur_addr: Some("127.0.0.1:41641".to_string()),
1861 relay: None,
1862 os: None,
1863 last_seen: None,
1864 key_expiry: None,
1865 dns_name: None,
1866 };
1867
1868 let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
1869 let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
1870 tokio::time::sleep(Duration::from_millis(100)).await;
1871
1872 let _rx = node_b.subscribe("test");
1877
1878 node_a
1884 .send("node-b", "test", b"hello from a")
1885 .await
1886 .expect("loopback send should establish a WS connection");
1887
1888 let peers = node_a.peers().await;
1889 let b = peers
1890 .iter()
1891 .find(|p| p.tailscale_id == "node-b")
1892 .expect("peer b discovered");
1893 assert!(
1894 b.ws_connected,
1895 "send() should have established a WS connection to node-b"
1896 );
1897
1898 node_a.stop().await;
1899 let peers = node_a.peers().await;
1900 let b = peers
1901 .iter()
1902 .find(|p| p.tailscale_id == "node-b")
1903 .expect("peer b still discovered");
1904 assert!(
1905 !b.ws_connected,
1906 "stop() must close and un-mark WS connections"
1907 );
1908 }
1909
1910 async fn inject_peer_with_identity(
1916 node: &Node<MockNetworkProvider>,
1917 event_tx: &broadcast::Sender<NetworkPeerEvent>,
1918 tailscale_id: &str,
1919 device_id: &str,
1920 device_name: &str,
1921 ) {
1922 let peer = make_loopback_peer(tailscale_id);
1923 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1924 tokio::time::sleep(Duration::from_millis(30)).await;
1925
1926 let identity = crate::session::PeerIdentity {
1927 app_id: "test".into(),
1928 device_id: device_id.into(),
1929 device_name: device_name.into(),
1930 os: "linux".into(),
1931 tailscale_id: tailscale_id.into(),
1932 };
1933 assert!(
1934 node.session
1935 .test_stamp_identity(tailscale_id, identity)
1936 .await,
1937 "peer {tailscale_id} should exist in session registry before stamping identity"
1938 );
1939 }
1940
1941 #[tokio::test]
1942 async fn test_resolve_peer_id_by_device_id() {
1943 let ws_port = random_port().await;
1944 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1945
1946 inject_peer_with_identity(
1947 &node,
1948 &event_tx,
1949 "tailscale-abc",
1950 "01HZZZZZZZZZZZZZZZZZZZZZZZ",
1951 "Alice MacBook",
1952 )
1953 .await;
1954
1955 let resolved = node
1956 .resolve_peer_id("01HZZZZZZZZZZZZZZZZZZZZZZZ")
1957 .await
1958 .unwrap();
1959 assert_eq!(resolved, "01HZZZZZZZZZZZZZZZZZZZZZZZ");
1960 }
1961
1962 #[tokio::test]
1963 async fn test_resolve_peer_id_by_device_name() {
1964 let ws_port = random_port().await;
1965 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1966
1967 inject_peer_with_identity(
1968 &node,
1969 &event_tx,
1970 "tailscale-abc",
1971 "01HXYZXYZXYZXYZXYZXYZXYZXY",
1972 "Bob's Mac",
1973 )
1974 .await;
1975
1976 let resolved = node.resolve_peer_id("Bob's Mac").await.unwrap();
1977 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
1978 }
1979
1980 #[tokio::test]
1981 async fn test_resolve_peer_id_by_device_id_prefix() {
1982 let ws_port = random_port().await;
1983 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1984
1985 inject_peer_with_identity(
1986 &node,
1987 &event_tx,
1988 "tailscale-abc",
1989 "01HXYZXYZXYZXYZXYZXYZXYZXY",
1990 "laptop",
1991 )
1992 .await;
1993
1994 let resolved = node.resolve_peer_id("01HX").await.unwrap();
1997 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
1998 }
1999
2000 #[tokio::test]
2001 async fn test_resolve_peer_id_by_tailscale_id_legacy() {
2002 let ws_port = random_port().await;
2006 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2007
2008 inject_peer_with_identity(
2009 &node,
2010 &event_tx,
2011 "tailscale-legacy",
2012 "01HLEGACY0000000000000000X",
2013 "legacy box",
2014 )
2015 .await;
2016
2017 let resolved = node.resolve_peer_id("tailscale-legacy").await.unwrap();
2018 assert_eq!(resolved, "01HLEGACY0000000000000000X");
2019 }
2020
2021 #[tokio::test]
2022 async fn test_resolve_peer_id_unknown() {
2023 let ws_port = random_port().await;
2024 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2025 let result = node.resolve_peer_id("nope").await;
2026 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
2027 }
2028
2029 #[tokio::test]
2032 async fn test_resolve_peer_by_bare_name_before_hello() {
2033 let ws_port = random_port().await;
2037 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2038
2039 let mut peer = make_loopback_peer("nodeid-9");
2040 peer.hostname = "truffle-test-ec2-smoke".to_string();
2041 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2042 tokio::time::sleep(Duration::from_millis(50)).await;
2043
2044 assert_eq!(node.resolve_peer("ec2-smoke").await.unwrap().id, "nodeid-9");
2046 assert_eq!(node.resolve_peer("EC2 Smoke").await.unwrap().id, "nodeid-9");
2047 assert!(node.resolve_peer("other-box").await.is_err());
2049 }
2050
2051 #[tokio::test]
2052 async fn test_peers_device_name_strips_hostname_before_hello() {
2053 let ws_port = random_port().await;
2054 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2055
2056 let mut peer = make_loopback_peer("nodeid-9");
2057 peer.hostname = "truffle-test-ec2-smoke".to_string();
2058 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2059 tokio::time::sleep(Duration::from_millis(50)).await;
2060
2061 let peers = node.peers().await;
2062 assert_eq!(peers.len(), 1);
2063 assert_eq!(peers[0].device_name, "ec2-smoke");
2065 assert_eq!(peers[0].name, "truffle-test-ec2-smoke");
2066 }
2067
2068 #[tokio::test]
2069 async fn test_resolve_peer_accepts_ip() {
2070 let ws_port = random_port().await;
2071 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2072
2073 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-a")));
2074 tokio::time::sleep(Duration::from_millis(50)).await;
2075
2076 let resolved = node.resolve_peer("127.0.0.1").await.unwrap();
2077 assert_eq!(resolved.id, "peer-a");
2078 assert_eq!(node.resolve_peer_id("127.0.0.1").await.unwrap(), "peer-a");
2081 }
2082
2083 #[tokio::test]
2084 async fn test_listen_tcp_rejects_reserved_ports() {
2085 let ws_port = random_port().await;
2086 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2087
2088 for port in [443u16, 9417] {
2089 let err = node.listen_tcp(port).await.unwrap_err();
2090 assert!(
2091 matches!(err, NodeError::ReservedPort(p) if p == port),
2092 "expected ReservedPort({port}), got: {err}"
2093 );
2094 }
2095 }
2096
2097 #[tokio::test]
2098 async fn test_listen_quic_rejects_reserved_and_ephemeral_ports() {
2099 let ws_port = random_port().await;
2100 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2101
2102 let err = node.listen_quic(443).await.unwrap_err();
2103 assert!(matches!(err, NodeError::ReservedPort(443)));
2104
2105 let err = node.listen_quic(0).await.unwrap_err();
2106 assert!(matches!(err, NodeError::NotImplemented(_)));
2107 }
2108
2109 #[tokio::test]
2110 async fn test_bind_udp_falls_back_to_direct_socket() {
2111 let ws_port = random_port().await;
2114 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2115
2116 let socket = node.bind_udp(0).await.unwrap();
2117 let addr = socket.local_addr().unwrap();
2118 assert_ne!(addr.port(), 0);
2119 }
2120
2121 #[test]
2122 fn test_raw_alpn_scoping() {
2123 assert_eq!(
2124 crate::transport::quic::raw_alpn("demo"),
2125 b"truffle-raw.demo".to_vec()
2126 );
2127 assert_eq!(
2128 crate::transport::quic::raw_alpn(""),
2129 b"truffle-raw".to_vec()
2130 );
2131 }
2132
2133 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2134 async fn test_connect_quic_roundtrip_via_raw_listener() {
2135 let ws_port = random_port().await;
2136 let (client_node, event_tx, _net) = make_test_node("cli", ws_port).await;
2137
2138 let server_network = Arc::new(MockNetworkProvider::new("srv"));
2141 let alpn = crate::transport::quic::raw_alpn("test");
2142 let listener = crate::transport::quic::listen_raw(&server_network, 0, &alpn)
2143 .await
2144 .unwrap();
2145 let port = listener.port();
2146 assert_ne!(port, 0);
2147
2148 let echo_task = tokio::spawn(async move {
2149 let conn = listener.accept().await.expect("no incoming connection");
2150 let mut stream = conn
2151 .accept_stream()
2152 .await
2153 .unwrap()
2154 .expect("connection closed before stream");
2155 let mut received = Vec::new();
2156 while let Some(chunk) = stream.read(1024).await.unwrap() {
2157 received.extend_from_slice(&chunk);
2158 }
2159 stream.write(&received).await.unwrap();
2160 stream.finish();
2161 tokio::time::sleep(Duration::from_millis(500)).await;
2164 conn.close();
2165 });
2166
2167 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("srv-peer")));
2169 tokio::time::sleep(Duration::from_millis(50)).await;
2170
2171 let conn = client_node.connect_quic("srv-peer", port).await.unwrap();
2172 let mut stream = conn.open_stream().await.unwrap();
2173 stream.write(b"hello quic").await.unwrap();
2174 stream.finish();
2175
2176 let mut echoed = Vec::new();
2177 while let Some(chunk) = stream.read(1024).await.unwrap() {
2178 echoed.extend_from_slice(&chunk);
2179 }
2180 assert_eq!(echoed, b"hello quic");
2181
2182 echo_task.await.unwrap();
2183 conn.close();
2184 }
2185
2186 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2187 async fn test_quic_raw_alpn_mismatch_rejected() {
2188 let server_network = Arc::new(MockNetworkProvider::new("srv"));
2191 let listener = crate::transport::quic::listen_raw(
2192 &server_network,
2193 0,
2194 &crate::transport::quic::raw_alpn("app-a"),
2195 )
2196 .await
2197 .unwrap();
2198 let port = listener.port();
2199
2200 let client_network = Arc::new(MockNetworkProvider::new("cli"));
2201 let result = crate::transport::quic::connect_raw(
2202 &client_network,
2203 "127.0.0.1",
2204 port,
2205 &crate::transport::quic::raw_alpn("app-b"),
2206 )
2207 .await;
2208
2209 assert!(
2210 result.is_err(),
2211 "cross-app QUIC connect must fail (ALPN mismatch)"
2212 );
2213 }
2214}