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(crate) fn with_state_dir(mut self, dir: PathBuf) -> Self {
453 self.state_dir = dir;
454 self
455 }
456
457 pub(crate) fn with_ws_port(mut self, port: u16) -> Self {
460 self.ws_port = port;
461 self
462 }
463
464 pub fn state_dir(&self) -> &Path {
468 &self.state_dir
469 }
470
471 pub async fn stop(&self) {
488 if self.stopped.swap(true, std::sync::atomic::Ordering::SeqCst) {
489 tracing::debug!("node: stop() called on already-stopped node");
490 return;
491 }
492 tracing::info!("node: stopping");
493 self.session.shutdown().await;
495 if let Err(e) = self.network.stop().await {
497 tracing::warn!(error = %e, "node: network provider stop failed");
498 }
499 tracing::info!("node: stopped");
500 }
501
502 pub fn local_info(&self) -> NodeIdentity {
506 self.network.local_identity()
507 }
508
509 pub async fn peers(&self) -> Vec<Peer> {
516 let app_id = self.network.local_identity().app_id;
517 self.session
518 .peers()
519 .await
520 .into_iter()
521 .map(|s| {
522 let bare = s
527 .identity
528 .is_none()
529 .then(|| hostname_slug(&s.name, &app_id).map(str::to_string))
530 .flatten();
531 let mut peer = Peer::from(s);
532 if let Some(bare) = bare {
533 peer.device_name = bare;
534 }
535 peer
536 })
537 .collect()
538 }
539
540 pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
542 self.session.on_peer_change()
543 }
544
545 pub(crate) async fn resolve_peer(&self, peer_ref: &str) -> Result<PeerState, NodeError> {
553 let peers = self.session.peers().await;
554
555 for p in &peers {
559 if let Some(identity) = p.identity.as_ref() {
560 if identity.device_id == peer_ref || identity.device_name == peer_ref {
561 return Ok(p.clone());
562 }
563 }
564 if p.id == peer_ref || p.name == peer_ref || p.ip.to_string() == peer_ref {
565 return Ok(p.clone());
566 }
567 }
568
569 let app_id = self.network.local_identity().app_id;
575 let ref_slug = identity::slug(peer_ref, 255);
576 if !ref_slug.is_empty() {
577 for p in &peers {
578 if hostname_slug(&p.name, &app_id) == Some(ref_slug.as_str()) {
579 return Ok(p.clone());
580 }
581 }
582 }
583
584 if peer_ref.len() >= 4 {
588 let mut hits = peers.iter().filter(|p| {
589 p.identity
590 .as_ref()
591 .map(|i| i.device_id.starts_with(peer_ref))
592 .unwrap_or(false)
593 });
594 if let (Some(hit), None) = (hits.next(), hits.next()) {
595 return Ok(hit.clone());
596 }
597 }
598
599 Err(NodeError::PeerNotFound(peer_ref.to_string()))
600 }
601
602 pub async fn resolve_peer_ip(&self, peer_ref: &str) -> Result<IpAddr, NodeError> {
608 Ok(self.resolve_peer(peer_ref).await?.ip)
609 }
610
611 pub async fn resolve_peer_id(&self, peer_id: &str) -> Result<String, NodeError> {
630 let p = self.resolve_peer(peer_id).await?;
631 Ok(p.identity
632 .as_ref()
633 .map(|i| i.device_id.clone())
634 .unwrap_or_else(|| p.id.clone()))
635 }
636
637 pub async fn ping(&self, peer_id: &str) -> Result<PingResult, NodeError> {
644 let peer = self.resolve_peer(peer_id).await?;
645 let addr = peer.ip.to_string();
646 self.network.ping(&addr).await.map_err(NodeError::Network)
647 }
648
649 pub async fn health(&self) -> HealthInfo {
651 self.network.health().await
652 }
653
654 fn ensure_not_stopped(&self) -> Result<(), NodeError> {
661 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
662 return Err(NodeError::Stopped);
663 }
664 Ok(())
665 }
666
667 pub async fn send(&self, peer_id: &str, namespace: &str, data: &[u8]) -> Result<(), NodeError> {
675 self.ensure_not_stopped()?;
676 let payload = std::str::from_utf8(data)
682 .ok()
683 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
684 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
685
686 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
687
688 let encoded = self.codec.encode(&envelope)?;
689 self.session.send(peer_id, &encoded).await?;
690 Ok(())
691 }
692
693 pub async fn send_typed(
701 &self,
702 peer_id: &str,
703 namespace: &str,
704 msg_type: &str,
705 payload: &serde_json::Value,
706 ) -> Result<(), NodeError> {
707 self.ensure_not_stopped()?;
708 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
709 let encoded = self.codec.encode(&envelope)?;
710 self.session.send(peer_id, &encoded).await?;
711 Ok(())
712 }
713
714 pub async fn broadcast_typed(
717 &self,
718 namespace: &str,
719 msg_type: &str,
720 payload: &serde_json::Value,
721 ) {
722 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
723 tracing::debug!("node: broadcast after stop ignored");
724 return;
725 }
726 let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
727 match self.codec.encode(&envelope) {
728 Ok(encoded) => {
729 self.session.broadcast(&encoded).await;
730 }
731 Err(e) => {
732 tracing::error!("node: failed to encode broadcast envelope: {e}");
733 }
734 }
735 }
736
737 pub async fn broadcast(&self, namespace: &str, data: &[u8]) {
742 if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
743 tracing::debug!("node: broadcast after stop ignored");
744 return;
745 }
746 let payload = std::str::from_utf8(data)
747 .ok()
748 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
749 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
750
751 let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
752
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 fn subscribe(&self, namespace: &str) -> broadcast::Receiver<NamespacedMessage> {
769 {
771 let filters = self
772 .namespace_filters
773 .read()
774 .expect("namespace_filters std RwLock poisoned");
775 if let Some(tx) = filters.get(namespace) {
776 return tx.subscribe();
777 }
778 }
779
780 let mut filters = self
782 .namespace_filters
783 .write()
784 .expect("namespace_filters std RwLock poisoned");
785 if let Some(tx) = filters.get(namespace) {
787 return tx.subscribe();
788 }
789 let (tx, rx) = broadcast::channel(256);
790 filters.insert(namespace.to_string(), tx);
791 rx
792 }
793
794 pub async fn open_tcp(&self, peer_id: &str, port: u16) -> Result<TcpStream, NodeError> {
806 let peer = self.resolve_peer(peer_id).await?;
807 let addr = peer.ip.to_string();
808 self.network
809 .dial_tcp_opts(&addr, port, DialOpts { tls: Some(false) })
810 .await
811 .map_err(|e| NodeError::ConnectionFailed(e.to_string()))
812 }
813
814 pub async fn listen_tcp(&self, port: u16) -> Result<RawListener, NodeError> {
822 use crate::transport::tcp::TcpTransport;
823 use crate::transport::RawTransport;
824
825 ensure_port_unreserved(port, self.ws_port)?;
826 let tcp = TcpTransport::new(self.network.clone());
827 tcp.listen(port).await.map_err(NodeError::Transport)
828 }
829
830 pub async fn unlisten_tcp(&self, port: u16) -> Result<(), NodeError> {
835 self.network
836 .unlisten_tcp(port)
837 .await
838 .map_err(NodeError::Network)
839 }
840
841 pub async fn connect_quic(
850 &self,
851 peer_id: &str,
852 port: u16,
853 ) -> Result<QuicConnection, NodeError> {
854 let peer = self.resolve_peer(peer_id).await?;
855 let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
856 crate::transport::quic::connect_raw(&self.network, &peer.ip.to_string(), port, &alpn)
857 .await
858 .map_err(NodeError::Transport)
859 }
860
861 pub async fn listen_quic(&self, port: u16) -> Result<QuicListener, NodeError> {
869 ensure_port_unreserved(port, self.ws_port)?;
870 if port == 0 {
871 return Err(NodeError::NotImplemented(
872 "ephemeral (port 0) QUIC listeners are not supported over the tsnet relay yet — choose an explicit port"
873 .to_string(),
874 ));
875 }
876 let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
877 crate::transport::quic::listen_raw(&self.network, port, &alpn)
878 .await
879 .map_err(NodeError::Transport)
880 }
881
882 pub async fn bind_udp(&self, port: u16) -> Result<DatagramSocket, NodeError> {
892 use crate::transport::udp::{UdpConfig, UdpTransport};
893 use crate::transport::DatagramTransport;
894
895 let udp = UdpTransport::new(self.network.clone(), UdpConfig::default());
896 udp.bind(port).await.map_err(NodeError::Transport)
897 }
898}
899
900fn hostname_slug<'a>(hostname: &'a str, app_id: &str) -> Option<&'a str> {
903 hostname
904 .strip_prefix("truffle-")?
905 .strip_prefix(app_id)?
906 .strip_prefix('-')
907}
908
909fn ensure_port_unreserved(port: u16, ws_port: u16) -> Result<(), NodeError> {
912 if port == 443 || port == ws_port {
913 Err(NodeError::ReservedPort(port))
914 } else {
915 Ok(())
916 }
917}
918
919#[derive(Debug, Clone)]
940pub struct NodeBuilder {
941 app_id: Option<AppId>,
942 device_name: Option<DeviceName>,
943 device_id: Option<DeviceId>,
944 sidecar_path: Option<PathBuf>,
945 state_dir: Option<String>,
946 auth_key: Option<String>,
947 ephemeral: bool,
948 ws_port: u16,
949 idle_timeout_secs: Option<u64>,
950}
951
952fn atomic_write_string(path: &Path, content: &str) -> std::io::Result<()> {
961 let _parent = path.parent().ok_or_else(|| {
962 std::io::Error::new(
963 std::io::ErrorKind::InvalidInput,
964 "path has no parent directory",
965 )
966 })?;
967 let mut tmp = path.to_path_buf();
968 tmp.set_extension("tmp");
969 std::fs::write(&tmp, content)?;
970 #[cfg(unix)]
971 {
972 std::fs::rename(&tmp, path)?;
973 }
974 #[cfg(windows)]
975 {
976 let _ = std::fs::remove_file(path);
979 std::fs::rename(&tmp, path)?;
980 }
981 Ok(())
982}
983
984impl Default for NodeBuilder {
985 fn default() -> Self {
986 Self {
987 app_id: None,
988 device_name: None,
989 device_id: None,
990 sidecar_path: None,
991 state_dir: None,
992 auth_key: None,
993 ephemeral: false,
994 ws_port: 9417,
995 idle_timeout_secs: None,
996 }
997 }
998}
999
1000impl NodeBuilder {
1001 pub fn app_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1006 let raw: String = s.into();
1007 let app_id = AppId::parse(&raw)
1008 .map_err(|e| NodeError::BuildError(format!("invalid app_id: {e}")))?;
1009 self.app_id = Some(app_id);
1010 Ok(self)
1011 }
1012
1013 pub fn device_name(mut self, s: impl Into<String>) -> Self {
1018 self.device_name = Some(DeviceName::parse(s));
1019 self
1020 }
1021
1022 pub fn device_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1028 let raw: String = s.into();
1029 let device_id = DeviceId::parse(&raw)
1030 .map_err(|e| NodeError::BuildError(format!("invalid device_id: {e}")))?;
1031 self.device_id = Some(device_id);
1032 Ok(self)
1033 }
1034
1035 pub fn sidecar_path(mut self, path: impl Into<PathBuf>) -> Self {
1037 self.sidecar_path = Some(path.into());
1038 self
1039 }
1040
1041 pub fn state_dir(mut self, dir: &str) -> Self {
1043 self.state_dir = Some(dir.to_string());
1044 self
1045 }
1046
1047 pub fn auth_key(mut self, key: &str) -> Self {
1049 self.auth_key = Some(key.to_string());
1050 self
1051 }
1052
1053 pub fn ephemeral(mut self, val: bool) -> Self {
1055 self.ephemeral = val;
1056 self
1057 }
1058
1059 pub fn ws_port(mut self, port: u16) -> Self {
1061 self.ws_port = port;
1062 self
1063 }
1064
1065 pub fn idle_timeout_secs(mut self, secs: u64) -> Self {
1071 self.idle_timeout_secs = Some(secs);
1072 self
1073 }
1074
1075 fn prepare_config(&self) -> Result<TailscaleConfig, NodeError> {
1082 let binary_path = self
1084 .sidecar_path
1085 .clone()
1086 .ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
1087
1088 let app_id = self
1090 .app_id
1091 .clone()
1092 .ok_or_else(|| NodeError::BuildError("app_id is required".into()))?;
1093
1094 let device_name = match self.device_name.clone() {
1096 Some(name) => name,
1097 None => {
1098 let os_hostname = hostname::get()
1099 .map_err(|e| {
1100 NodeError::BuildError(format!(
1101 "device_name is unset and hostname::get() failed: {e}"
1102 ))
1103 })?
1104 .to_string_lossy()
1105 .into_owned();
1106 DeviceName::parse(os_hostname)
1107 }
1108 };
1109
1110 let tailscale_host = identity::tailscale_hostname(&app_id, &device_name);
1113
1114 let state_dir = self.state_dir.clone().unwrap_or_else(|| {
1117 let base = dirs::data_dir().unwrap_or_else(|| {
1118 tracing::warn!(
1119 "dirs::data_dir() returned None, falling back to std::env::temp_dir()"
1120 );
1121 std::env::temp_dir()
1122 });
1123 base.join("truffle")
1124 .join(app_id.as_str())
1125 .join(identity::slug(device_name.as_str(), 255))
1126 .to_string_lossy()
1127 .into_owned()
1128 });
1129
1130 std::fs::create_dir_all(&state_dir)?;
1132
1133 let device_id_file = Path::new(&state_dir).join("device-id.txt");
1138 let device_id = match self.device_id.clone() {
1139 Some(id) => {
1140 atomic_write_string(&device_id_file, id.as_str())?;
1142 id
1143 }
1144 None => {
1145 if device_id_file.exists() {
1146 let s = std::fs::read_to_string(&device_id_file)?.trim().to_string();
1147 DeviceId::parse(&s).map_err(|e| {
1148 NodeError::BuildError(format!(
1149 "device-id.txt at {device_id_file:?} contains an invalid ULID: {e}"
1150 ))
1151 })?
1152 } else {
1153 let id = DeviceId::generate();
1154 atomic_write_string(&device_id_file, id.as_str())?;
1155 id
1156 }
1157 }
1158 };
1159
1160 Ok(TailscaleConfig {
1161 binary_path,
1162 app_id: app_id.as_str().to_string(),
1163 device_id: device_id.as_str().to_string(),
1164 device_name: device_name.as_str().to_string(),
1165 hostname: tailscale_host,
1166 state_dir,
1167 auth_key: self.auth_key.clone(),
1168 ephemeral: if self.ephemeral { Some(true) } else { None },
1169 tags: None,
1170 idle_timeout_secs: self.idle_timeout_secs,
1171 })
1172 }
1173
1174 pub async fn build(self) -> Result<Node<TailscaleProvider>, NodeError> {
1185 let ws_port = self.ws_port;
1186 let config = self.prepare_config()?;
1187 let state_dir = PathBuf::from(&config.state_dir);
1188
1189 let mut provider = TailscaleProvider::new(config);
1190 provider.start().await.map_err(NodeError::Network)?;
1191
1192 let network = Arc::new(provider);
1193
1194 let ws_config = WsConfig {
1196 port: ws_port,
1197 ..Default::default()
1198 };
1199 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1200
1201 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1203 session.start().await;
1204
1205 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1207 let node = Node::from_parts(network, session, codec)
1208 .with_state_dir(state_dir)
1209 .with_ws_port(ws_port);
1210
1211 tracing::info!("node: started successfully");
1212 Ok(node)
1213 }
1214
1215 pub async fn build_with_auth_handler(
1227 self,
1228 on_auth: impl Fn(String) + Send + 'static,
1229 ) -> Result<Node<TailscaleProvider>, NodeError> {
1230 let ws_port = self.ws_port;
1231 let config = self.prepare_config()?;
1232 let state_dir = PathBuf::from(&config.state_dir);
1233
1234 let mut provider = TailscaleProvider::new(config);
1235
1236 let mut auth_rx = provider.peer_events();
1238
1239 let auth_task = tokio::spawn(async move {
1241 use crate::network::NetworkPeerEvent;
1242 loop {
1243 match auth_rx.recv().await {
1244 Ok(NetworkPeerEvent::AuthRequired { url }) => {
1245 on_auth(url);
1246 }
1247 Err(broadcast::error::RecvError::Closed) => break,
1248 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1249 _ => {} }
1251 }
1252 });
1253
1254 let start_result = provider.start().await.map_err(NodeError::Network);
1256
1257 auth_task.abort();
1259
1260 start_result?;
1261
1262 let network = Arc::new(provider);
1263
1264 let ws_config = WsConfig {
1266 port: ws_port,
1267 ..Default::default()
1268 };
1269 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1270
1271 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1273 session.start().await;
1274
1275 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1277 let node = Node::from_parts(network, session, codec)
1278 .with_state_dir(state_dir)
1279 .with_ws_port(ws_port);
1280
1281 tracing::info!("node: started successfully (with auth handler)");
1282 Ok(node)
1283 }
1284}
1285
1286#[cfg(test)]
1291mod tests {
1292 use super::*;
1293 use crate::network::{
1294 HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
1295 NetworkTcpListener, NetworkUdpSocket, PeerAddr,
1296 };
1297 use crate::transport::WsConfig;
1298 use serde_json::json;
1299 use std::sync::atomic::{AtomicUsize, Ordering};
1300 use std::time::Duration;
1301 use tokio::sync::{broadcast, mpsc};
1302
1303 struct MockNetworkProvider {
1306 identity: NodeIdentity,
1307 local_addr: PeerAddr,
1308 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
1309 mock_peers: Arc<RwLock<Vec<NetworkPeer>>>,
1311 stop_calls: Arc<AtomicUsize>,
1314 }
1315
1316 impl MockNetworkProvider {
1317 fn new(id: &str) -> Self {
1318 let (peer_event_tx, _) = broadcast::channel(64);
1319 Self {
1320 identity: NodeIdentity {
1321 app_id: "test".to_string(),
1322 device_id: id.to_string(),
1325 device_name: format!("Test Node {id}"),
1326 tailscale_hostname: format!("truffle-test-{id}"),
1327 tailscale_id: id.to_string(),
1328 dns_name: None,
1329 ip: Some("127.0.0.1".parse().unwrap()),
1330 },
1331 local_addr: PeerAddr {
1332 ip: Some("127.0.0.1".parse().unwrap()),
1333 hostname: format!("truffle-test-{id}"),
1334 dns_name: None,
1335 },
1336 peer_event_tx,
1337 mock_peers: Arc::new(RwLock::new(Vec::new())),
1338 stop_calls: Arc::new(AtomicUsize::new(0)),
1339 }
1340 }
1341
1342 fn event_sender(&self) -> broadcast::Sender<NetworkPeerEvent> {
1343 self.peer_event_tx.clone()
1344 }
1345
1346 fn stop_call_count(&self) -> usize {
1348 self.stop_calls.load(Ordering::SeqCst)
1349 }
1350 }
1351
1352 impl NetworkProvider for MockNetworkProvider {
1353 async fn start(&mut self) -> Result<(), NetworkError> {
1354 Ok(())
1355 }
1356
1357 async fn stop(&self) -> Result<(), NetworkError> {
1358 self.stop_calls.fetch_add(1, Ordering::SeqCst);
1359 Ok(())
1360 }
1361
1362 fn local_identity(&self) -> NodeIdentity {
1363 self.identity.clone()
1364 }
1365
1366 fn local_addr(&self) -> PeerAddr {
1367 self.local_addr.clone()
1368 }
1369
1370 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
1371 self.peer_event_tx.subscribe()
1372 }
1373
1374 async fn peers(&self) -> Vec<NetworkPeer> {
1375 self.mock_peers.read().await.clone()
1376 }
1377
1378 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
1379 let target = format!("{addr}:{port}");
1380 TcpStream::connect(&target)
1381 .await
1382 .map_err(|e| NetworkError::DialFailed(format!("mock dial {target}: {e}")))
1383 }
1384
1385 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
1386 let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
1387 .await
1388 .map_err(|e| NetworkError::ListenFailed(format!("mock listen :{port}: {e}")))?;
1389
1390 let actual_port = listener.local_addr().unwrap().port();
1391 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
1392
1393 tokio::spawn(async move {
1394 loop {
1395 match listener.accept().await {
1396 Ok((stream, addr)) => {
1397 let conn = IncomingConnection {
1398 stream,
1399 remote_addr: addr.to_string(),
1400 remote_identity: String::new(),
1401 port: actual_port,
1402 };
1403 if tx.send(conn).await.is_err() {
1404 break;
1405 }
1406 }
1407 Err(e) => {
1408 tracing::debug!("mock listener error: {e}");
1409 break;
1410 }
1411 }
1412 }
1413 });
1414
1415 Ok(NetworkTcpListener {
1416 port: actual_port,
1417 incoming: rx,
1418 })
1419 }
1420
1421 async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
1422 Ok(())
1423 }
1424
1425 async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
1426 Err(NetworkError::Internal("mock: UDP not supported".into()))
1427 }
1428
1429 async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
1430 Ok(PingResult {
1431 latency: Duration::from_millis(1),
1432 connection: "direct".to_string(),
1433 peer_addr: None,
1434 })
1435 }
1436
1437 async fn health(&self) -> HealthInfo {
1438 HealthInfo {
1439 state: "running".to_string(),
1440 healthy: true,
1441 ..Default::default()
1442 }
1443 }
1444 }
1445
1446 fn make_loopback_peer(id: &str) -> NetworkPeer {
1449 NetworkPeer {
1450 id: id.to_string(),
1451 hostname: format!("truffle-test-{id}"),
1452 ip: "127.0.0.1".parse().unwrap(),
1453 online: true,
1454 cur_addr: Some("127.0.0.1:41641".to_string()),
1455 relay: None,
1456 os: Some("linux".to_string()),
1457 last_seen: Some("2026-03-25T12:00:00Z".to_string()),
1458 key_expiry: None,
1459 dns_name: None,
1460 }
1461 }
1462
1463 fn ws_config(port: u16) -> WsConfig {
1464 WsConfig {
1465 port,
1466 ping_interval: Duration::from_secs(300),
1467 pong_timeout: Duration::from_secs(300),
1468 ..Default::default()
1469 }
1470 }
1471
1472 async fn random_port() -> u16 {
1473 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1474 l.local_addr().unwrap().port()
1475 }
1476
1477 async fn make_test_node(
1479 id: &str,
1480 ws_port: u16,
1481 ) -> (
1482 Node<MockNetworkProvider>,
1483 broadcast::Sender<NetworkPeerEvent>,
1484 Arc<MockNetworkProvider>,
1485 ) {
1486 let provider = MockNetworkProvider::new(id);
1487 let event_tx = provider.event_sender();
1488 let network = Arc::new(provider);
1489 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config(ws_port)));
1490 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1491 session.start().await;
1492
1493 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1494 let node = Node::from_parts(network.clone(), session, codec);
1495
1496 (node, event_tx, network)
1497 }
1498
1499 #[tokio::test]
1502 async fn test_node_builder_creates_node() {
1503 let ws_port = random_port().await;
1504 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1505
1506 let identity = node.local_info();
1507 assert_eq!(identity.tailscale_id, "node-1");
1508 assert_eq!(identity.device_id, "node-1");
1509 assert!(identity.tailscale_hostname.contains("node-1"));
1510 }
1511
1512 #[tokio::test]
1513 async fn test_node_peers_from_network() {
1514 let ws_port = random_port().await;
1515 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1516
1517 let peers = node.peers().await;
1519 assert!(peers.is_empty());
1520
1521 let peer = make_loopback_peer("peer-a");
1523 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1524 tokio::time::sleep(Duration::from_millis(50)).await;
1525
1526 let peers = node.peers().await;
1527 assert_eq!(peers.len(), 1);
1528 assert_eq!(peers[0].id, "peer-a");
1529 assert!(peers[0].online);
1530 assert!(!peers[0].ws_connected);
1531 }
1532
1533 #[tokio::test]
1534 async fn test_node_send_to_unknown_peer_errors() {
1535 let ws_port = random_port().await;
1536 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1537
1538 let result = node.send("nonexistent", "test", b"hello").await;
1539 assert!(result.is_err());
1540 let err_str = result.unwrap_err().to_string();
1541 assert!(
1542 err_str.contains("unknown peer") || err_str.contains("not found"),
1543 "expected unknown peer error, got: {err_str}"
1544 );
1545 }
1546
1547 #[tokio::test]
1548 async fn test_node_send_wraps_in_envelope() {
1549 let codec = JsonCodec;
1552 let data = b"hello world";
1553 let envelope = Envelope::new("test-ns", "message", serde_json::Value::from(data.to_vec()))
1554 .with_timestamp();
1555
1556 let encoded = codec.encode(&envelope).unwrap();
1557 let decoded = codec.decode(&encoded).unwrap();
1558
1559 assert_eq!(decoded.namespace, "test-ns");
1560 assert_eq!(decoded.msg_type, "message");
1561 assert!(decoded.timestamp.is_some());
1562 }
1563
1564 #[tokio::test]
1565 async fn test_node_subscribe_filters_by_namespace() {
1566 let ws_port = random_port().await;
1567 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1568
1569 let _rx_chat = node.subscribe("chat");
1571 let _rx_ft = node.subscribe("ft");
1572
1573 let _rx_chat2 = node.subscribe("chat");
1575 }
1576
1577 #[tokio::test]
1578 async fn test_node_broadcast() {
1579 let ws_port = random_port().await;
1580 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1581
1582 node.broadcast("test", b"hello everyone").await;
1584 }
1585
1586 #[tokio::test]
1587 async fn test_node_open_tcp_resolves_peer() {
1588 let ws_port = random_port().await;
1589 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1590
1591 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1593 let tcp_port = listener.local_addr().unwrap().port();
1594
1595 let peer = make_loopback_peer("peer-tcp");
1597 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1598 tokio::time::sleep(Duration::from_millis(50)).await;
1599
1600 let accept_handle = tokio::spawn(async move {
1602 let (stream, _) = listener.accept().await.unwrap();
1603 stream
1604 });
1605
1606 let stream = node.open_tcp("peer-tcp", tcp_port).await;
1608 assert!(stream.is_ok(), "open_tcp failed: {:?}", stream.err());
1609
1610 let _ = accept_handle.await;
1611 }
1612
1613 #[tokio::test]
1614 async fn test_node_open_tcp_unknown_peer_errors() {
1615 let ws_port = random_port().await;
1616 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1617
1618 let result = node.open_tcp("nonexistent", 8080).await;
1619 assert!(result.is_err());
1620 let err_str = result.unwrap_err().to_string();
1621 assert!(
1622 err_str.contains("not found"),
1623 "expected peer not found error, got: {err_str}"
1624 );
1625 }
1626
1627 #[tokio::test]
1628 async fn test_node_ping_resolves_peer() {
1629 let ws_port = random_port().await;
1630 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1631
1632 let result = node.ping("peer-ping").await;
1634 assert!(result.is_err());
1635
1636 let peer = make_loopback_peer("peer-ping");
1638 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1639 tokio::time::sleep(Duration::from_millis(50)).await;
1640
1641 let result = node.ping("peer-ping").await;
1643 assert!(result.is_ok());
1644 assert_eq!(result.unwrap().latency, Duration::from_millis(1));
1645 }
1646
1647 #[tokio::test]
1648 async fn test_node_health() {
1649 let ws_port = random_port().await;
1650 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1651
1652 let health = node.health().await;
1653 assert!(health.healthy);
1654 assert_eq!(health.state, "running");
1655 }
1656
1657 #[tokio::test]
1658 async fn test_node_connect_quic_unknown_peer_errors() {
1659 let ws_port = random_port().await;
1660 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1661
1662 let result = node.connect_quic("peer", 4433).await;
1663 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
1664 }
1665
1666 #[tokio::test]
1667 async fn test_node_listen_tcp() {
1668 let ws_port = random_port().await;
1669 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1670
1671 let listener = node.listen_tcp(0).await;
1673 assert!(listener.is_ok(), "listen_tcp failed: {:?}", listener.err());
1674 }
1675
1676 #[tokio::test]
1677 async fn test_envelope_serialize_deserialize() {
1678 let envelope = Envelope::new("chat", "message", json!({"text": "hello"})).with_timestamp();
1679
1680 let bytes = envelope.serialize().unwrap();
1681 let decoded = Envelope::deserialize(&bytes).unwrap();
1682
1683 assert_eq!(decoded.namespace, "chat");
1684 assert_eq!(decoded.msg_type, "message");
1685 assert_eq!(decoded.payload["text"], "hello");
1686 assert!(decoded.timestamp.is_some());
1687 }
1688
1689 #[tokio::test]
1690 async fn test_envelope_codec_json() {
1691 let codec = JsonCodec;
1692 let envelope = Envelope::new("ft", "offer", json!({"file": "test.bin"}));
1693
1694 let encoded = codec.encode(&envelope).unwrap();
1695 let decoded = codec.decode(&encoded).unwrap();
1696
1697 assert_eq!(decoded.namespace, "ft");
1698 assert_eq!(decoded.payload["file"], "test.bin");
1699 }
1700
1701 #[tokio::test]
1702 async fn test_envelope_unknown_fields_ignored() {
1703 let json_bytes = br#"{
1704 "namespace": "v2",
1705 "msg_type": "new",
1706 "payload": {},
1707 "future_field": "ignored"
1708 }"#;
1709
1710 let codec = JsonCodec;
1711 let decoded = codec.decode(json_bytes).unwrap();
1712 assert_eq!(decoded.namespace, "v2");
1713 assert_eq!(decoded.msg_type, "new");
1714 }
1715
1716 #[tokio::test]
1717 async fn test_node_send_and_receive_roundtrip() {
1718 let port_a = random_port().await;
1720 let port_b = random_port().await;
1721
1722 let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
1723 let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
1724
1725 let peer_b = NetworkPeer {
1727 id: "node-b".to_string(),
1728 hostname: "truffle-test-node-b".to_string(),
1729 ip: "127.0.0.1".parse().unwrap(),
1730 online: true,
1731 cur_addr: Some("127.0.0.1:41641".to_string()),
1732 relay: None,
1733 os: None,
1734 last_seen: None,
1735 key_expiry: None,
1736 dns_name: None,
1737 };
1738 let peer_a = NetworkPeer {
1739 id: "node-a".to_string(),
1740 hostname: "truffle-test-node-a".to_string(),
1741 ip: "127.0.0.1".parse().unwrap(),
1742 online: true,
1743 cur_addr: Some("127.0.0.1:41641".to_string()),
1744 relay: None,
1745 os: None,
1746 last_seen: None,
1747 key_expiry: None,
1748 dns_name: None,
1749 };
1750
1751 let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
1752 let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
1753 tokio::time::sleep(Duration::from_millis(100)).await;
1754
1755 let mut rx = node_b.subscribe("test");
1757
1758 let send_result = node_a.send("node-b", "test", b"hello from a").await;
1761
1762 if send_result.is_ok() {
1770 let msg = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await;
1772 if let Ok(Ok(msg)) = msg {
1773 assert_eq!(msg.namespace, "test");
1774 }
1775 }
1776 }
1779
1780 #[tokio::test]
1783 async fn test_node_stop_shuts_down_provider_and_is_idempotent() {
1784 let ws_port = random_port().await;
1785 let (node, event_tx, network) = make_test_node("node-1", ws_port).await;
1786
1787 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-1")));
1788 tokio::time::sleep(Duration::from_millis(50)).await;
1789
1790 assert_eq!(network.stop_call_count(), 0);
1791 node.stop().await;
1792 assert_eq!(
1793 network.stop_call_count(),
1794 1,
1795 "stop() must shut down the provider"
1796 );
1797
1798 node.stop().await;
1800 assert_eq!(network.stop_call_count(), 1);
1801
1802 let err = node.send("peer-1", "test", b"hi").await.unwrap_err();
1804 assert!(matches!(err, NodeError::Stopped));
1805 }
1806
1807 #[tokio::test]
1808 async fn test_node_stop_closes_ws_connections() {
1809 let port_a = random_port().await;
1812 let port_b = random_port().await;
1813
1814 let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
1815 let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
1816
1817 let peer_b = NetworkPeer {
1818 id: "node-b".to_string(),
1819 hostname: "truffle-test-node-b".to_string(),
1820 ip: "127.0.0.1".parse().unwrap(),
1821 online: true,
1822 cur_addr: Some("127.0.0.1:41641".to_string()),
1823 relay: None,
1824 os: None,
1825 last_seen: None,
1826 key_expiry: None,
1827 dns_name: None,
1828 };
1829 let peer_a = NetworkPeer {
1830 id: "node-a".to_string(),
1831 hostname: "truffle-test-node-a".to_string(),
1832 ip: "127.0.0.1".parse().unwrap(),
1833 online: true,
1834 cur_addr: Some("127.0.0.1:41641".to_string()),
1835 relay: None,
1836 os: None,
1837 last_seen: None,
1838 key_expiry: None,
1839 dns_name: None,
1840 };
1841
1842 let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
1843 let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
1844 tokio::time::sleep(Duration::from_millis(100)).await;
1845
1846 let _rx = node_b.subscribe("test");
1851
1852 node_a
1858 .send("node-b", "test", b"hello from a")
1859 .await
1860 .expect("loopback send should establish a WS connection");
1861
1862 let peers = node_a.peers().await;
1863 let b = peers
1864 .iter()
1865 .find(|p| p.tailscale_id == "node-b")
1866 .expect("peer b discovered");
1867 assert!(
1868 b.ws_connected,
1869 "send() should have established a WS connection to node-b"
1870 );
1871
1872 node_a.stop().await;
1873 let peers = node_a.peers().await;
1874 let b = peers
1875 .iter()
1876 .find(|p| p.tailscale_id == "node-b")
1877 .expect("peer b still discovered");
1878 assert!(
1879 !b.ws_connected,
1880 "stop() must close and un-mark WS connections"
1881 );
1882 }
1883
1884 async fn inject_peer_with_identity(
1890 node: &Node<MockNetworkProvider>,
1891 event_tx: &broadcast::Sender<NetworkPeerEvent>,
1892 tailscale_id: &str,
1893 device_id: &str,
1894 device_name: &str,
1895 ) {
1896 let peer = make_loopback_peer(tailscale_id);
1897 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1898 tokio::time::sleep(Duration::from_millis(30)).await;
1899
1900 let identity = crate::session::PeerIdentity {
1901 app_id: "test".into(),
1902 device_id: device_id.into(),
1903 device_name: device_name.into(),
1904 os: "linux".into(),
1905 tailscale_id: tailscale_id.into(),
1906 };
1907 assert!(
1908 node.session
1909 .test_stamp_identity(tailscale_id, identity)
1910 .await,
1911 "peer {tailscale_id} should exist in session registry before stamping identity"
1912 );
1913 }
1914
1915 #[tokio::test]
1916 async fn test_resolve_peer_id_by_device_id() {
1917 let ws_port = random_port().await;
1918 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1919
1920 inject_peer_with_identity(
1921 &node,
1922 &event_tx,
1923 "tailscale-abc",
1924 "01HZZZZZZZZZZZZZZZZZZZZZZZ",
1925 "Alice MacBook",
1926 )
1927 .await;
1928
1929 let resolved = node
1930 .resolve_peer_id("01HZZZZZZZZZZZZZZZZZZZZZZZ")
1931 .await
1932 .unwrap();
1933 assert_eq!(resolved, "01HZZZZZZZZZZZZZZZZZZZZZZZ");
1934 }
1935
1936 #[tokio::test]
1937 async fn test_resolve_peer_id_by_device_name() {
1938 let ws_port = random_port().await;
1939 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1940
1941 inject_peer_with_identity(
1942 &node,
1943 &event_tx,
1944 "tailscale-abc",
1945 "01HXYZXYZXYZXYZXYZXYZXYZXY",
1946 "Bob's Mac",
1947 )
1948 .await;
1949
1950 let resolved = node.resolve_peer_id("Bob's Mac").await.unwrap();
1951 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
1952 }
1953
1954 #[tokio::test]
1955 async fn test_resolve_peer_id_by_device_id_prefix() {
1956 let ws_port = random_port().await;
1957 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1958
1959 inject_peer_with_identity(
1960 &node,
1961 &event_tx,
1962 "tailscale-abc",
1963 "01HXYZXYZXYZXYZXYZXYZXYZXY",
1964 "laptop",
1965 )
1966 .await;
1967
1968 let resolved = node.resolve_peer_id("01HX").await.unwrap();
1971 assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
1972 }
1973
1974 #[tokio::test]
1975 async fn test_resolve_peer_id_by_tailscale_id_legacy() {
1976 let ws_port = random_port().await;
1980 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1981
1982 inject_peer_with_identity(
1983 &node,
1984 &event_tx,
1985 "tailscale-legacy",
1986 "01HLEGACY0000000000000000X",
1987 "legacy box",
1988 )
1989 .await;
1990
1991 let resolved = node.resolve_peer_id("tailscale-legacy").await.unwrap();
1992 assert_eq!(resolved, "01HLEGACY0000000000000000X");
1993 }
1994
1995 #[tokio::test]
1996 async fn test_resolve_peer_id_unknown() {
1997 let ws_port = random_port().await;
1998 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
1999 let result = node.resolve_peer_id("nope").await;
2000 assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
2001 }
2002
2003 #[tokio::test]
2006 async fn test_resolve_peer_by_bare_name_before_hello() {
2007 let ws_port = random_port().await;
2011 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2012
2013 let mut peer = make_loopback_peer("nodeid-9");
2014 peer.hostname = "truffle-test-ec2-smoke".to_string();
2015 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2016 tokio::time::sleep(Duration::from_millis(50)).await;
2017
2018 assert_eq!(node.resolve_peer("ec2-smoke").await.unwrap().id, "nodeid-9");
2020 assert_eq!(node.resolve_peer("EC2 Smoke").await.unwrap().id, "nodeid-9");
2021 assert!(node.resolve_peer("other-box").await.is_err());
2023 }
2024
2025 #[tokio::test]
2026 async fn test_peers_device_name_strips_hostname_before_hello() {
2027 let ws_port = random_port().await;
2028 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2029
2030 let mut peer = make_loopback_peer("nodeid-9");
2031 peer.hostname = "truffle-test-ec2-smoke".to_string();
2032 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2033 tokio::time::sleep(Duration::from_millis(50)).await;
2034
2035 let peers = node.peers().await;
2036 assert_eq!(peers.len(), 1);
2037 assert_eq!(peers[0].device_name, "ec2-smoke");
2039 assert_eq!(peers[0].name, "truffle-test-ec2-smoke");
2040 }
2041
2042 #[tokio::test]
2043 async fn test_resolve_peer_accepts_ip() {
2044 let ws_port = random_port().await;
2045 let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2046
2047 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-a")));
2048 tokio::time::sleep(Duration::from_millis(50)).await;
2049
2050 let resolved = node.resolve_peer("127.0.0.1").await.unwrap();
2051 assert_eq!(resolved.id, "peer-a");
2052 assert_eq!(node.resolve_peer_id("127.0.0.1").await.unwrap(), "peer-a");
2055 }
2056
2057 #[tokio::test]
2058 async fn test_listen_tcp_rejects_reserved_ports() {
2059 let ws_port = random_port().await;
2060 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2061
2062 for port in [443u16, 9417] {
2063 let err = node.listen_tcp(port).await.unwrap_err();
2064 assert!(
2065 matches!(err, NodeError::ReservedPort(p) if p == port),
2066 "expected ReservedPort({port}), got: {err}"
2067 );
2068 }
2069 }
2070
2071 #[tokio::test]
2072 async fn test_listen_quic_rejects_reserved_and_ephemeral_ports() {
2073 let ws_port = random_port().await;
2074 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2075
2076 let err = node.listen_quic(443).await.unwrap_err();
2077 assert!(matches!(err, NodeError::ReservedPort(443)));
2078
2079 let err = node.listen_quic(0).await.unwrap_err();
2080 assert!(matches!(err, NodeError::NotImplemented(_)));
2081 }
2082
2083 #[tokio::test]
2084 async fn test_bind_udp_falls_back_to_direct_socket() {
2085 let ws_port = random_port().await;
2088 let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2089
2090 let socket = node.bind_udp(0).await.unwrap();
2091 let addr = socket.local_addr().unwrap();
2092 assert_ne!(addr.port(), 0);
2093 }
2094
2095 #[test]
2096 fn test_raw_alpn_scoping() {
2097 assert_eq!(
2098 crate::transport::quic::raw_alpn("demo"),
2099 b"truffle-raw.demo".to_vec()
2100 );
2101 assert_eq!(
2102 crate::transport::quic::raw_alpn(""),
2103 b"truffle-raw".to_vec()
2104 );
2105 }
2106
2107 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2108 async fn test_connect_quic_roundtrip_via_raw_listener() {
2109 let ws_port = random_port().await;
2110 let (client_node, event_tx, _net) = make_test_node("cli", ws_port).await;
2111
2112 let server_network = Arc::new(MockNetworkProvider::new("srv"));
2115 let alpn = crate::transport::quic::raw_alpn("test");
2116 let listener = crate::transport::quic::listen_raw(&server_network, 0, &alpn)
2117 .await
2118 .unwrap();
2119 let port = listener.port();
2120 assert_ne!(port, 0);
2121
2122 let echo_task = tokio::spawn(async move {
2123 let conn = listener.accept().await.expect("no incoming connection");
2124 let mut stream = conn
2125 .accept_stream()
2126 .await
2127 .unwrap()
2128 .expect("connection closed before stream");
2129 let mut received = Vec::new();
2130 while let Some(chunk) = stream.read(1024).await.unwrap() {
2131 received.extend_from_slice(&chunk);
2132 }
2133 stream.write(&received).await.unwrap();
2134 stream.finish();
2135 tokio::time::sleep(Duration::from_millis(500)).await;
2138 conn.close();
2139 });
2140
2141 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("srv-peer")));
2143 tokio::time::sleep(Duration::from_millis(50)).await;
2144
2145 let conn = client_node.connect_quic("srv-peer", port).await.unwrap();
2146 let mut stream = conn.open_stream().await.unwrap();
2147 stream.write(b"hello quic").await.unwrap();
2148 stream.finish();
2149
2150 let mut echoed = Vec::new();
2151 while let Some(chunk) = stream.read(1024).await.unwrap() {
2152 echoed.extend_from_slice(&chunk);
2153 }
2154 assert_eq!(echoed, b"hello quic");
2155
2156 echo_task.await.unwrap();
2157 conn.close();
2158 }
2159
2160 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2161 async fn test_quic_raw_alpn_mismatch_rejected() {
2162 let server_network = Arc::new(MockNetworkProvider::new("srv"));
2165 let listener = crate::transport::quic::listen_raw(
2166 &server_network,
2167 0,
2168 &crate::transport::quic::raw_alpn("app-a"),
2169 )
2170 .await
2171 .unwrap();
2172 let port = listener.port();
2173
2174 let client_network = Arc::new(MockNetworkProvider::new("cli"));
2175 let result = crate::transport::quic::connect_raw(
2176 &client_network,
2177 "127.0.0.1",
2178 port,
2179 &crate::transport::quic::raw_alpn("app-b"),
2180 )
2181 .await;
2182
2183 assert!(
2184 result.is_err(),
2185 "cross-app QUIC connect must fail (ALPN mismatch)"
2186 );
2187 }
2188}