1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
41use std::sync::Arc;
42use std::time::Duration;
43
44use futures_util::stream::SplitSink;
45use futures_util::stream::SplitStream;
46use futures_util::{SinkExt, StreamExt};
47use tokio::net::TcpStream;
48use tokio::sync::Mutex;
49use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
50use tokio_tungstenite::tungstenite::protocol::frame::CloseFrame;
51use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
52use tokio_tungstenite::tungstenite::Message;
53use tokio_tungstenite::WebSocketStream;
54
55use crate::network::{NetworkProvider, PeerAddr};
56use crate::session::hello::{
57 HelloEnvelope, HelloKind, PeerIdentity, CLOSE_APP_MISMATCH, CLOSE_HELLO_PROTOCOL,
58 CLOSE_IDENTITY_MISMATCH, HELLO_TIMEOUT,
59};
60
61use super::{
62 resolve_dial_addr, FramedStream, StreamListener, StreamTransport, TransportError, WsConfig,
63};
64
65pub struct WebSocketTransport<N: NetworkProvider> {
85 network: Arc<N>,
87 config: WsConfig,
89}
90
91impl<N: NetworkProvider + 'static> WebSocketTransport<N> {
92 pub fn new(network: Arc<N>, config: WsConfig) -> Self {
97 Self { network, config }
98 }
99
100 fn local_hello(&self) -> HelloEnvelope {
107 let identity = self.network.local_identity();
108 HelloEnvelope::new(PeerIdentity {
109 app_id: identity.app_id,
110 device_id: identity.device_id,
111 device_name: identity.device_name,
112 os: std::env::consts::OS.to_string(),
113 tailscale_id: identity.tailscale_id,
114 })
115 }
116
117 fn ws_protocol_config(&self) -> WebSocketConfig {
119 let mut config = WebSocketConfig::default();
120 config.max_message_size = Some(self.config.max_message_size);
121 config.max_frame_size = Some(self.config.max_message_size);
122 config
123 }
124}
125
126async fn close_ws_with_code(ws: &mut WebSocketStream<TcpStream>, code: u16, reason: &str) {
133 let close_frame = CloseFrame {
134 code: CloseCode::from(code),
135 reason: reason.to_string().into(),
136 };
137 let _ = ws.send(Message::Close(Some(close_frame))).await;
138 let _ = ws.close(None).await;
139}
140
141const MAX_CONTROL_FRAMES_BEFORE_HELLO: usize = 16;
146
147async fn receive_hello(
157 ws: &mut WebSocketStream<TcpStream>,
158) -> Result<HelloEnvelope, TransportError> {
159 let fut = async {
160 let mut control_frame_count: usize = 0;
161 loop {
162 match ws.next().await {
163 Some(Ok(Message::Text(text))) => {
164 return serde_json::from_str::<HelloEnvelope>(&text)
165 .map_err(|e| TransportError::HelloMalformed(format!("parse hello: {e}")));
166 }
167 Some(Ok(Message::Binary(data))) => {
168 return serde_json::from_slice::<HelloEnvelope>(&data)
169 .map_err(|e| TransportError::HelloMalformed(format!("parse hello: {e}")));
170 }
171 Some(Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_))) => {
172 control_frame_count += 1;
173 if control_frame_count > MAX_CONTROL_FRAMES_BEFORE_HELLO {
174 return Err(TransportError::HelloMalformed(
175 "too many control frames before hello".to_string(),
176 ));
177 }
178 continue;
179 }
180 Some(Ok(Message::Close(_))) => {
181 return Err(TransportError::HelloMalformed(
182 "peer closed connection before hello".to_string(),
183 ));
184 }
185 Some(Err(e)) => {
186 return Err(TransportError::HelloMalformed(format!(
187 "receive hello: {e}"
188 )));
189 }
190 None => {
191 return Err(TransportError::HelloMalformed(
192 "connection closed before hello".to_string(),
193 ));
194 }
195 }
196 }
197 };
198
199 match tokio::time::timeout(HELLO_TIMEOUT, fut).await {
200 Ok(inner) => inner,
201 Err(_) => Err(TransportError::HelloTimeout),
202 }
203}
204
205const MAX_APP_ID_LEN: usize = 32;
209const MAX_DEVICE_NAME_LEN: usize = 512;
210const MAX_DEVICE_ID_LEN: usize = 64;
211const MAX_TAILSCALE_ID_LEN: usize = 256;
212const MAX_OS_LEN: usize = 32;
213
214fn validate_hello(
217 remote: HelloEnvelope,
218 local_app_id: &str,
219) -> Result<PeerIdentity, TransportError> {
220 if remote.kind != HelloKind::Hello {
221 return Err(TransportError::HelloMalformed(format!(
222 "unexpected hello kind: {:?}",
223 remote.kind
224 )));
225 }
226 if remote.version < HelloEnvelope::MIN_SUPPORTED_VERSION {
227 return Err(TransportError::HelloMalformed(format!(
228 "unsupported hello version {} (minimum supported: {})",
229 remote.version,
230 HelloEnvelope::MIN_SUPPORTED_VERSION
231 )));
232 }
233 if remote.identity.app_id.len() > MAX_APP_ID_LEN
236 || remote.identity.device_name.len() > MAX_DEVICE_NAME_LEN
237 || remote.identity.device_id.len() > MAX_DEVICE_ID_LEN
238 || remote.identity.tailscale_id.len() > MAX_TAILSCALE_ID_LEN
239 || remote.identity.os.len() > MAX_OS_LEN
240 {
241 return Err(TransportError::HelloMalformed(
242 "identity field exceeds maximum allowed length".to_string(),
243 ));
244 }
245 if remote.identity.device_id.is_empty()
252 || remote.identity.device_id == remote.identity.tailscale_id
253 {
254 return Err(TransportError::HelloMalformed(
255 "identity device_id must be non-empty and distinct from tailscale_id".to_string(),
256 ));
257 }
258 if remote.identity.app_id != local_app_id {
259 return Err(TransportError::AppMismatch {
260 local: local_app_id.to_string(),
261 remote: remote.identity.app_id.clone(),
262 });
263 }
264 Ok(remote.identity)
265}
266
267#[derive(Debug, Default, serde::Deserialize)]
270struct AuthenticatedIdentity {
271 #[serde(default, rename = "nodeId")]
272 node_id: String,
273}
274
275fn verify_authenticated_identity(
293 claimed: &PeerIdentity,
294 authenticated: &str,
295) -> Result<(), TransportError> {
296 if authenticated.trim().is_empty() {
297 tracing::warn!(
298 claimed_tailscale_id = %claimed.tailscale_id,
299 "ws: no authenticated identity for incoming connection (mock/loopback or WhoIs failure); accepting hello claim unverified"
300 );
301 return Ok(());
302 }
303
304 let parsed = match serde_json::from_str::<AuthenticatedIdentity>(authenticated) {
305 Ok(parsed) => parsed,
306 Err(_) => {
307 tracing::warn!(
308 claimed_tailscale_id = %claimed.tailscale_id,
309 "ws: unparseable authenticated identity; accepting hello claim unverified"
310 );
311 return Ok(());
312 }
313 };
314
315 if parsed.node_id.is_empty() {
316 tracing::warn!(
317 claimed_tailscale_id = %claimed.tailscale_id,
318 "ws: authenticated identity carried no nodeId; accepting hello claim unverified"
319 );
320 return Ok(());
321 }
322
323 if parsed.node_id == claimed.tailscale_id {
324 Ok(())
325 } else {
326 Err(TransportError::IdentityMismatch {
327 claimed: claimed.tailscale_id.clone(),
328 authenticated: parsed.node_id,
329 })
330 }
331}
332
333async fn send_hello(
335 ws: &mut WebSocketStream<TcpStream>,
336 hello: &HelloEnvelope,
337) -> Result<(), TransportError> {
338 let payload =
339 serde_json::to_string(hello).map_err(|e| TransportError::Serialize(e.to_string()))?;
340 ws.send(Message::Text(payload.into()))
341 .await
342 .map_err(|e| TransportError::HandshakeFailed(format!("send hello: {e}")))
343}
344
345async fn client_hello_exchange(
349 ws: &mut WebSocketStream<TcpStream>,
350 local_hello: &HelloEnvelope,
351) -> Result<PeerIdentity, TransportError> {
352 if let Err(e) = send_hello(ws, local_hello).await {
354 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "local send failed").await;
355 return Err(e);
356 }
357
358 let remote = match receive_hello(ws).await {
360 Ok(envelope) => envelope,
361 Err(e) => {
362 match &e {
363 TransportError::HelloTimeout => {
364 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "hello timeout").await;
365 }
366 TransportError::HelloMalformed(_) => {
367 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "malformed hello").await;
368 }
369 _ => {}
370 }
371 return Err(e);
372 }
373 };
374
375 match validate_hello(remote, &local_hello.identity.app_id) {
377 Ok(identity) => Ok(identity),
378 Err(e) => {
379 match &e {
380 TransportError::AppMismatch { .. } => {
381 close_ws_with_code(ws, CLOSE_APP_MISMATCH, "app mismatch").await;
382 }
383 TransportError::HelloMalformed(_) => {
384 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "bad hello").await;
385 }
386 _ => {}
387 }
388 Err(e)
389 }
390 }
391}
392
393async fn server_hello_exchange(
402 ws: &mut WebSocketStream<TcpStream>,
403 local_hello: &HelloEnvelope,
404 authenticated_identity: &str,
405) -> Result<PeerIdentity, TransportError> {
406 let remote = match receive_hello(ws).await {
408 Ok(envelope) => envelope,
409 Err(e) => {
410 match &e {
411 TransportError::HelloTimeout => {
412 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "hello timeout").await;
413 }
414 TransportError::HelloMalformed(_) => {
415 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "malformed hello").await;
416 }
417 _ => {}
418 }
419 return Err(e);
420 }
421 };
422
423 let identity = match validate_hello(remote, &local_hello.identity.app_id) {
425 Ok(identity) => identity,
426 Err(e) => {
427 match &e {
428 TransportError::AppMismatch { .. } => {
429 close_ws_with_code(ws, CLOSE_APP_MISMATCH, "app mismatch").await;
430 }
431 TransportError::HelloMalformed(_) => {
432 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "bad hello").await;
433 }
434 _ => {}
435 }
436 return Err(e);
437 }
438 };
439
440 if let Err(e) = verify_authenticated_identity(&identity, authenticated_identity) {
445 close_ws_with_code(ws, CLOSE_IDENTITY_MISMATCH, "identity mismatch").await;
446 return Err(e);
447 }
448
449 if let Err(e) = send_hello(ws, local_hello).await {
451 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "local send failed").await;
452 return Err(e);
453 }
454
455 Ok(identity)
456}
457
458impl<N: NetworkProvider + 'static> StreamTransport for WebSocketTransport<N> {
459 type Stream = WsFramedStream;
460
461 async fn connect(&self, addr: &PeerAddr) -> Result<Self::Stream, TransportError> {
462 let dial_addr = resolve_dial_addr(addr);
464 tracing::debug!(addr = %dial_addr, port = self.config.port, "ws: dialing peer");
465
466 let tcp_stream = self
467 .network
468 .dial_tcp(&dial_addr, self.config.port)
469 .await
470 .map_err(|e| TransportError::ConnectFailed(format!("dial tcp: {e}")))?;
471
472 let ws_url = format!("ws://{dial_addr}:{}/ws", self.config.port);
474 let ws_config = self.ws_protocol_config();
475 let (mut ws, _response) =
476 tokio_tungstenite::client_async_with_config(ws_url, tcp_stream, Some(ws_config))
477 .await
478 .map_err(|e| TransportError::ConnectFailed(format!("ws upgrade: {e}")))?;
479
480 let local_hello = self.local_hello();
482 let remote_identity = match client_hello_exchange(&mut ws, &local_hello).await {
483 Ok(identity) => identity,
484 Err(e) => {
485 match &e {
486 TransportError::AppMismatch { local, remote } => {
487 tracing::info!(
488 local_app_id = %local,
489 remote_app_id = %remote,
490 "ws: closing connection — app_id mismatch"
491 );
492 }
493 TransportError::HelloTimeout => {
494 tracing::warn!("ws: hello timeout on outgoing connection");
495 }
496 TransportError::HelloMalformed(msg) => {
497 tracing::warn!(error = %msg, "ws: malformed hello on outgoing connection");
498 }
499 _ => {}
500 }
501 return Err(e);
502 }
503 };
504
505 tracing::info!(
506 remote_device_id = %remote_identity.device_id,
507 remote_device_name = %remote_identity.device_name,
508 remote_tailscale_id = %remote_identity.tailscale_id,
509 "ws: connected (hello exchanged)"
510 );
511
512 Ok(WsFramedStream::new(
514 ws,
515 remote_identity.tailscale_id.clone(),
516 Some(remote_identity),
517 dial_addr,
518 self.config.ping_interval,
519 self.config.pong_timeout,
520 ))
521 }
522
523 async fn listen(&self) -> Result<StreamListener<Self::Stream>, TransportError> {
524 let port = self.config.port;
525 tracing::debug!(port, "ws: starting listener");
526
527 let mut tcp_listener = self
529 .network
530 .listen_tcp(port)
531 .await
532 .map_err(|e| TransportError::ListenFailed(format!("listen tcp: {e}")))?;
533
534 let (tx, rx) = tokio::sync::mpsc::channel::<WsFramedStream>(64);
536 let local_hello = self.local_hello();
537 let ping_interval = self.config.ping_interval;
538 let pong_timeout = self.config.pong_timeout;
539 let ws_config = self.ws_protocol_config();
540 let handshake_timeout = self.config.handshake_timeout;
541 let handshake_permits = Arc::new(tokio::sync::Semaphore::new(
542 self.config.max_pending_handshakes,
543 ));
544
545 tokio::spawn(async move {
546 loop {
547 match tcp_listener.incoming.recv().await {
548 Some(incoming) => {
549 let permit = match handshake_permits.clone().try_acquire_owned() {
555 Ok(permit) => permit,
556 Err(_) => {
557 tracing::warn!(
558 remote = %incoming.remote_addr,
559 "ws: max pending handshakes reached; dropping incoming connection"
560 );
561 continue;
562 }
563 };
564
565 let tx = tx.clone();
566 let local_hello = local_hello.clone();
567 let remote_addr = incoming.remote_addr.clone();
568 let authenticated_identity = incoming.remote_identity.clone();
571 let ws_config = ws_config;
572
573 tokio::spawn(async move {
574 let handshake = async {
579 let mut ws = tokio_tungstenite::accept_async_with_config(
580 incoming.stream,
581 Some(ws_config),
582 )
583 .await
584 .map_err(|e| {
585 TransportError::HandshakeFailed(format!("ws upgrade: {e}"))
586 })?;
587 let identity = server_hello_exchange(
588 &mut ws,
589 &local_hello,
590 &authenticated_identity,
591 )
592 .await?;
593 Ok::<(WebSocketStream<TcpStream>, PeerIdentity), TransportError>((
594 ws, identity,
595 ))
596 };
597
598 let (ws, remote_identity) = match tokio::time::timeout(
599 handshake_timeout,
600 handshake,
601 )
602 .await
603 {
604 Ok(Ok(pair)) => pair,
605 Ok(Err(e)) => {
606 match &e {
607 TransportError::AppMismatch { local, remote } => {
608 tracing::info!(
609 remote_addr = %remote_addr,
610 local_app_id = %local,
611 remote_app_id = %remote,
612 "ws: closing incoming connection — app_id mismatch"
613 );
614 }
615 TransportError::HelloTimeout => {
616 tracing::warn!(
617 remote_addr = %remote_addr,
618 "ws: hello timeout on incoming connection"
619 );
620 }
621 TransportError::HelloMalformed(msg) => {
622 tracing::warn!(
623 remote_addr = %remote_addr,
624 error = %msg,
625 "ws: malformed hello on incoming connection"
626 );
627 }
628 TransportError::IdentityMismatch {
629 claimed,
630 authenticated,
631 } => {
632 tracing::warn!(
633 remote_addr = %remote_addr,
634 claimed = %claimed,
635 authenticated = %authenticated,
636 "ws: closing incoming connection — hello identity does not match authenticated Tailscale identity"
637 );
638 }
639 TransportError::HandshakeFailed(msg) => {
640 tracing::warn!(
641 remote = %remote_addr,
642 error = %msg,
643 "ws: upgrade failed"
644 );
645 }
646 _ => {
647 tracing::warn!(
648 remote_addr = %remote_addr,
649 "ws: hello exchange failed: {e}"
650 );
651 }
652 }
653 return;
654 }
655 Err(_) => {
656 tracing::warn!(
657 remote_addr = %remote_addr,
658 timeout = ?handshake_timeout,
659 "ws: handshake timed out before hello; dropping connection"
660 );
661 return;
662 }
663 };
664
665 drop(permit);
669
670 tracing::info!(
671 remote_device_id = %remote_identity.device_id,
672 remote_device_name = %remote_identity.device_name,
673 remote_tailscale_id = %remote_identity.tailscale_id,
674 remote_addr = %remote_addr,
675 "ws: accepted connection (hello exchanged)"
676 );
677
678 let stream = WsFramedStream::new(
679 ws,
680 remote_identity.tailscale_id.clone(),
681 Some(remote_identity),
682 remote_addr,
683 ping_interval,
684 pong_timeout,
685 );
686
687 if tx.send(stream).await.is_err() {
688 tracing::debug!("ws: listener channel closed");
689 }
690 });
691 }
692 None => {
693 tracing::debug!("ws: tcp listener channel closed");
694 break;
695 }
696 }
697 }
698 });
699
700 Ok(StreamListener::new(rx, port))
701 }
702}
703
704pub struct WsFramedStream {
721 write: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
723 read: SplitStream<WebSocketStream<TcpStream>>,
725 remote_peer_id: String,
728 remote_identity: Option<PeerIdentity>,
730 remote_addr: String,
732 heartbeat_handle: Option<tokio::task::JoinHandle<()>>,
734 last_pong: Arc<AtomicU64>,
736 closed: Arc<AtomicBool>,
738}
739
740impl std::fmt::Debug for WsFramedStream {
741 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
742 f.debug_struct("WsFramedStream")
743 .field("remote_peer_id", &self.remote_peer_id)
744 .field("remote_addr", &self.remote_addr)
745 .field("closed", &self.closed.load(Ordering::Relaxed))
746 .finish_non_exhaustive()
747 }
748}
749
750#[allow(unsafe_code)]
756unsafe impl Sync for WsFramedStream {}
757
758fn epoch_millis() -> u64 {
760 std::time::SystemTime::now()
761 .duration_since(std::time::UNIX_EPOCH)
762 .unwrap_or_default()
763 .as_millis() as u64
764}
765
766impl WsFramedStream {
767 fn new(
769 ws: WebSocketStream<TcpStream>,
770 remote_peer_id: String,
771 remote_identity: Option<PeerIdentity>,
772 remote_addr: String,
773 ping_interval: Duration,
774 pong_timeout: Duration,
775 ) -> Self {
776 let (write, read) = ws.split();
777 let write = Arc::new(Mutex::new(write));
778 let last_pong = Arc::new(AtomicU64::new(epoch_millis()));
779 let closed = Arc::new(AtomicBool::new(false));
780
781 let hb_write = write.clone();
783 let hb_last_pong = last_pong.clone();
784 let hb_closed = closed.clone();
785 let hb_addr = remote_addr.clone();
786 let heartbeat_handle = tokio::spawn(async move {
787 heartbeat_loop(
788 hb_write,
789 hb_last_pong,
790 hb_closed,
791 ping_interval,
792 pong_timeout,
793 &hb_addr,
794 )
795 .await;
796 });
797
798 Self {
799 write,
800 read,
801 remote_peer_id,
802 remote_identity,
803 remote_addr,
804 heartbeat_handle: Some(heartbeat_handle),
805 last_pong,
806 closed,
807 }
808 }
809
810 pub fn remote_peer_id(&self) -> &str {
815 &self.remote_peer_id
816 }
817
818 pub fn remote_identity(&self) -> Option<&PeerIdentity> {
823 self.remote_identity.as_ref()
824 }
825}
826
827async fn heartbeat_loop(
837 write: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
838 last_pong: Arc<AtomicU64>,
839 closed: Arc<AtomicBool>,
840 ping_interval: Duration,
841 pong_timeout: Duration,
842 remote_addr: &str,
843) {
844 let mut interval = tokio::time::interval(ping_interval);
845 interval.tick().await;
847
848 loop {
849 interval.tick().await;
850
851 if closed.load(Ordering::Acquire) {
853 return;
854 }
855
856 let last = last_pong.load(Ordering::Acquire);
858 let now = epoch_millis();
859 let elapsed = Duration::from_millis(now.saturating_sub(last));
860
861 if elapsed > pong_timeout {
862 tracing::warn!(
863 remote = %remote_addr,
864 elapsed = ?elapsed,
865 "heartbeat: pong timeout after {pong_timeout:?}"
866 );
867 closed.store(true, Ordering::Release);
869 let mut w = write.lock().await;
870 let _ = w.close().await;
871 return;
872 }
873
874 {
876 let mut w = write.lock().await;
877 let ping_data = b"truffle-ping".to_vec();
878 if let Err(e) = w.send(Message::Ping(ping_data.into())).await {
879 tracing::debug!(remote = %remote_addr, "heartbeat: ping send failed: {e}");
880 closed.store(true, Ordering::Release);
881 return;
882 }
883 }
884 }
885}
886
887impl FramedStream for WsFramedStream {
888 async fn send(&mut self, data: &[u8]) -> Result<(), TransportError> {
889 if self.closed.load(Ordering::Acquire) {
890 return Err(TransportError::ConnectionClosed(
891 "connection already closed".to_string(),
892 ));
893 }
894 let mut w = self.write.lock().await;
895 w.send(Message::Binary(data.to_vec().into()))
896 .await
897 .map_err(|e| TransportError::WebSocket(format!("send: {e}")))
898 }
899
900 async fn recv(&mut self) -> Result<Option<Vec<u8>>, TransportError> {
901 if self.closed.load(Ordering::Acquire) {
902 return Ok(None);
903 }
904 loop {
905 match self.read.next().await {
906 Some(Ok(Message::Binary(data))) => return Ok(Some(data.to_vec())),
907 Some(Ok(Message::Text(text))) => {
908 return Ok(Some(text.as_bytes().to_vec()));
910 }
911 Some(Ok(Message::Ping(_))) => {
912 continue;
915 }
916 Some(Ok(Message::Pong(_))) => {
917 self.last_pong.store(epoch_millis(), Ordering::Release);
919 continue;
920 }
921 Some(Ok(Message::Close(_))) => {
922 self.closed.store(true, Ordering::Release);
923 return Ok(None);
924 }
925 Some(Ok(Message::Frame(_))) => {
926 continue;
928 }
929 Some(Err(e)) => {
930 self.closed.store(true, Ordering::Release);
931 return Err(TransportError::WebSocket(format!("recv: {e}")));
932 }
933 None => {
934 self.closed.store(true, Ordering::Release);
935 return Ok(None);
936 }
937 }
938 }
939 }
940
941 async fn close(&mut self) -> Result<(), TransportError> {
942 if let Some(handle) = self.heartbeat_handle.take() {
944 handle.abort();
945 }
946
947 self.closed.store(true, Ordering::Release);
948
949 let mut w = self.write.lock().await;
950 w.close()
951 .await
952 .map_err(|e| TransportError::WebSocket(format!("close: {e}")))
953 }
954
955 fn peer_addr(&self) -> String {
956 self.remote_addr.clone()
957 }
958}
959
960impl Drop for WsFramedStream {
961 fn drop(&mut self) {
962 if let Some(handle) = self.heartbeat_handle.take() {
964 handle.abort();
965 }
966 }
967}
968
969#[cfg(test)]
974mod unit_tests {
975 use super::*;
976
977 #[test]
978 fn resolve_dial_addr_prefers_ip() {
979 let addr = PeerAddr {
980 ip: Some("100.64.0.1".parse().unwrap()),
981 hostname: "peer".to_string(),
982 dns_name: Some("peer.tailnet.ts.net".to_string()),
983 };
984 assert_eq!(resolve_dial_addr(&addr), "100.64.0.1");
985 }
986
987 #[test]
988 fn resolve_dial_addr_falls_back_to_dns() {
989 let addr = PeerAddr {
990 ip: None,
991 hostname: "peer".to_string(),
992 dns_name: Some("peer.tailnet.ts.net".to_string()),
993 };
994 assert_eq!(resolve_dial_addr(&addr), "peer.tailnet.ts.net");
995 }
996
997 #[test]
998 fn resolve_dial_addr_falls_back_to_hostname() {
999 let addr = PeerAddr {
1000 ip: None,
1001 hostname: "peer".to_string(),
1002 dns_name: None,
1003 };
1004 assert_eq!(resolve_dial_addr(&addr), "peer");
1005 }
1006
1007 fn valid_identity() -> crate::session::hello::PeerIdentity {
1010 crate::session::hello::PeerIdentity {
1011 app_id: "playground".to_string(),
1012 device_id: "01J4K9M2Z8AB3RNYQPW6H5TC0X".to_string(),
1013 device_name: "Alice's MacBook".to_string(),
1014 os: "darwin".to_string(),
1015 tailscale_id: "n1234567890.ts-node".to_string(),
1016 }
1017 }
1018
1019 #[test]
1020 fn validate_hello_accepts_normal_identity() {
1021 let envelope = crate::session::hello::HelloEnvelope::new(valid_identity());
1022 let result = validate_hello(envelope, "playground");
1023 assert!(result.is_ok(), "baseline valid hello should validate");
1024 }
1025
1026 #[test]
1027 fn validate_hello_rejects_device_id_equal_to_tailscale_id() {
1028 let mut identity = valid_identity();
1031 identity.device_id = identity.tailscale_id.clone();
1032 let envelope = crate::session::hello::HelloEnvelope::new(identity);
1033 match validate_hello(envelope, "playground") {
1034 Err(TransportError::HelloMalformed(msg)) => {
1035 assert!(
1036 msg.contains("distinct from tailscale_id"),
1037 "unexpected error: {msg}"
1038 );
1039 }
1040 other => panic!("expected HelloMalformed, got {other:?}"),
1041 }
1042 }
1043
1044 #[test]
1045 fn validate_hello_rejects_empty_device_id() {
1046 let mut identity = valid_identity();
1047 identity.device_id = String::new();
1048 let envelope = crate::session::hello::HelloEnvelope::new(identity);
1049 match validate_hello(envelope, "playground") {
1050 Err(TransportError::HelloMalformed(msg)) => {
1051 assert!(msg.contains("non-empty"), "unexpected error: {msg}");
1052 }
1053 other => panic!("expected HelloMalformed, got {other:?}"),
1054 }
1055 }
1056
1057 #[test]
1058 fn validate_hello_rejects_oversized_app_id() {
1059 let mut identity = valid_identity();
1060 identity.app_id = "a".repeat(MAX_APP_ID_LEN + 1);
1061 let envelope = crate::session::hello::HelloEnvelope::new(identity);
1062 let result = validate_hello(envelope, "playground");
1063 match result {
1064 Err(TransportError::HelloMalformed(msg)) => {
1065 assert!(
1066 msg.contains("maximum allowed length"),
1067 "unexpected error: {msg}"
1068 );
1069 }
1070 other => panic!("expected HelloMalformed, got {other:?}"),
1071 }
1072 }
1073
1074 #[test]
1075 fn validate_hello_rejects_oversized_device_name() {
1076 let mut identity = valid_identity();
1077 identity.device_name = "x".repeat(MAX_DEVICE_NAME_LEN + 1);
1078 let envelope = crate::session::hello::HelloEnvelope::new(identity);
1079 let result = validate_hello(envelope, "playground");
1080 assert!(matches!(result, Err(TransportError::HelloMalformed(_))));
1081 }
1082
1083 #[test]
1084 fn validate_hello_rejects_oversized_device_id() {
1085 let mut identity = valid_identity();
1086 identity.device_id = "x".repeat(MAX_DEVICE_ID_LEN + 1);
1087 let envelope = crate::session::hello::HelloEnvelope::new(identity);
1088 let result = validate_hello(envelope, "playground");
1089 assert!(matches!(result, Err(TransportError::HelloMalformed(_))));
1090 }
1091
1092 #[test]
1093 fn validate_hello_rejects_oversized_tailscale_id() {
1094 let mut identity = valid_identity();
1095 identity.tailscale_id = "x".repeat(MAX_TAILSCALE_ID_LEN + 1);
1096 let envelope = crate::session::hello::HelloEnvelope::new(identity);
1097 let result = validate_hello(envelope, "playground");
1098 assert!(matches!(result, Err(TransportError::HelloMalformed(_))));
1099 }
1100
1101 #[test]
1102 fn validate_hello_rejects_oversized_os() {
1103 let mut identity = valid_identity();
1104 identity.os = "x".repeat(MAX_OS_LEN + 1);
1105 let envelope = crate::session::hello::HelloEnvelope::new(identity);
1106 let result = validate_hello(envelope, "playground");
1107 assert!(matches!(result, Err(TransportError::HelloMalformed(_))));
1108 }
1109
1110 #[test]
1111 fn validate_hello_length_check_runs_before_app_id_mismatch() {
1112 let mut identity = valid_identity();
1117 identity.device_name = "x".repeat(MAX_DEVICE_NAME_LEN + 1);
1118 let envelope = crate::session::hello::HelloEnvelope::new(identity);
1119 let result = validate_hello(envelope, "playground");
1120 assert!(
1121 matches!(result, Err(TransportError::HelloMalformed(_))),
1122 "matched app_id must not let an oversized field through"
1123 );
1124 }
1125
1126 #[test]
1129 fn verify_identity_rejects_mismatched_node_id() {
1130 let identity = valid_identity();
1133 let authenticated = r#"{"dnsName":"mallory.ts.net","nodeId":"real-node-id"}"#;
1134 match verify_authenticated_identity(&identity, authenticated) {
1135 Err(TransportError::IdentityMismatch {
1136 claimed,
1137 authenticated,
1138 }) => {
1139 assert_eq!(claimed, "n1234567890.ts-node");
1140 assert_eq!(authenticated, "real-node-id");
1141 }
1142 other => panic!("expected IdentityMismatch, got {other:?}"),
1143 }
1144 }
1145
1146 #[test]
1147 fn verify_identity_accepts_matching_node_id() {
1148 let identity = valid_identity();
1149 let authenticated = r#"{"dnsName":"alice.ts.net","nodeId":"n1234567890.ts-node"}"#;
1150 assert!(
1151 verify_authenticated_identity(&identity, authenticated).is_ok(),
1152 "matching nodeId must be accepted"
1153 );
1154 }
1155
1156 #[test]
1157 fn verify_identity_accepts_empty_authenticated_string() {
1158 let identity = valid_identity();
1161 assert!(
1162 verify_authenticated_identity(&identity, "").is_ok(),
1163 "empty authenticated identity must fall open"
1164 );
1165 }
1166
1167 #[test]
1168 fn verify_identity_accepts_non_json_legacy_dns_value() {
1169 let identity = valid_identity();
1172 assert!(
1173 verify_authenticated_identity(&identity, "peer.tailnet.ts.net").is_ok(),
1174 "legacy plain-DNS-name authenticated value must fall open"
1175 );
1176 }
1177
1178 #[test]
1179 fn verify_identity_accepts_json_without_node_id() {
1180 let identity = valid_identity();
1182 let authenticated = r#"{"dnsName":"peer.ts.net"}"#;
1183 assert!(
1184 verify_authenticated_identity(&identity, authenticated).is_ok(),
1185 "missing nodeId must fall open"
1186 );
1187 }
1188
1189 #[test]
1190 fn verify_identity_rejects_empty_claim_against_real_node_id() {
1191 let mut identity = valid_identity();
1195 identity.tailscale_id = String::new();
1196 let authenticated = r#"{"nodeId":"real-node-id"}"#;
1197 match verify_authenticated_identity(&identity, authenticated) {
1198 Err(TransportError::IdentityMismatch {
1199 claimed,
1200 authenticated,
1201 }) => {
1202 assert_eq!(claimed, "");
1203 assert_eq!(authenticated, "real-node-id");
1204 }
1205 other => panic!("expected IdentityMismatch, got {other:?}"),
1206 }
1207 }
1208}