1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
34use std::sync::Arc;
35use std::time::Duration;
36
37use futures_util::stream::SplitSink;
38use futures_util::stream::SplitStream;
39use futures_util::{SinkExt, StreamExt};
40use tokio::net::TcpStream;
41use tokio::sync::Mutex;
42use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
43use tokio_tungstenite::tungstenite::protocol::frame::CloseFrame;
44use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
45use tokio_tungstenite::tungstenite::Message;
46use tokio_tungstenite::WebSocketStream;
47
48use crate::network::{NetworkProvider, PeerAddr};
49use crate::session::hello::{
50 HelloEnvelope, HelloKind, PeerIdentity, CLOSE_APP_MISMATCH, CLOSE_HELLO_PROTOCOL, HELLO_TIMEOUT,
51};
52
53use super::{
54 resolve_dial_addr, FramedStream, StreamListener, StreamTransport, TransportError, WsConfig,
55};
56
57pub struct WebSocketTransport<N: NetworkProvider> {
77 network: Arc<N>,
79 config: WsConfig,
81}
82
83impl<N: NetworkProvider + 'static> WebSocketTransport<N> {
84 pub fn new(network: Arc<N>, config: WsConfig) -> Self {
89 Self { network, config }
90 }
91
92 fn local_hello(&self) -> HelloEnvelope {
99 let identity = self.network.local_identity();
100 HelloEnvelope::new(PeerIdentity {
101 app_id: identity.app_id,
102 device_id: identity.device_id,
103 device_name: identity.device_name,
104 os: std::env::consts::OS.to_string(),
105 tailscale_id: identity.tailscale_id,
106 })
107 }
108
109 fn ws_protocol_config(&self) -> WebSocketConfig {
111 let mut config = WebSocketConfig::default();
112 config.max_message_size = Some(self.config.max_message_size);
113 config.max_frame_size = Some(self.config.max_message_size);
114 config
115 }
116}
117
118async fn close_ws_with_code(ws: &mut WebSocketStream<TcpStream>, code: u16, reason: &str) {
125 let close_frame = CloseFrame {
126 code: CloseCode::from(code),
127 reason: reason.to_string().into(),
128 };
129 let _ = ws.send(Message::Close(Some(close_frame))).await;
130 let _ = ws.close(None).await;
131}
132
133const MAX_CONTROL_FRAMES_BEFORE_HELLO: usize = 16;
138
139async fn receive_hello(
149 ws: &mut WebSocketStream<TcpStream>,
150) -> Result<HelloEnvelope, TransportError> {
151 let fut = async {
152 let mut control_frame_count: usize = 0;
153 loop {
154 match ws.next().await {
155 Some(Ok(Message::Text(text))) => {
156 return serde_json::from_str::<HelloEnvelope>(&text)
157 .map_err(|e| TransportError::HelloMalformed(format!("parse hello: {e}")));
158 }
159 Some(Ok(Message::Binary(data))) => {
160 return serde_json::from_slice::<HelloEnvelope>(&data)
161 .map_err(|e| TransportError::HelloMalformed(format!("parse hello: {e}")));
162 }
163 Some(Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_))) => {
164 control_frame_count += 1;
165 if control_frame_count > MAX_CONTROL_FRAMES_BEFORE_HELLO {
166 return Err(TransportError::HelloMalformed(
167 "too many control frames before hello".to_string(),
168 ));
169 }
170 continue;
171 }
172 Some(Ok(Message::Close(_))) => {
173 return Err(TransportError::HelloMalformed(
174 "peer closed connection before hello".to_string(),
175 ));
176 }
177 Some(Err(e)) => {
178 return Err(TransportError::HelloMalformed(format!(
179 "receive hello: {e}"
180 )));
181 }
182 None => {
183 return Err(TransportError::HelloMalformed(
184 "connection closed before hello".to_string(),
185 ));
186 }
187 }
188 }
189 };
190
191 match tokio::time::timeout(HELLO_TIMEOUT, fut).await {
192 Ok(inner) => inner,
193 Err(_) => Err(TransportError::HelloTimeout),
194 }
195}
196
197const MAX_APP_ID_LEN: usize = 32;
201const MAX_DEVICE_NAME_LEN: usize = 512;
202const MAX_DEVICE_ID_LEN: usize = 64;
203const MAX_TAILSCALE_ID_LEN: usize = 256;
204const MAX_OS_LEN: usize = 32;
205
206fn validate_hello(
209 remote: HelloEnvelope,
210 local_app_id: &str,
211) -> Result<PeerIdentity, TransportError> {
212 if remote.kind != HelloKind::Hello {
213 return Err(TransportError::HelloMalformed(format!(
214 "unexpected hello kind: {:?}",
215 remote.kind
216 )));
217 }
218 if remote.version < HelloEnvelope::MIN_SUPPORTED_VERSION {
219 return Err(TransportError::HelloMalformed(format!(
220 "unsupported hello version {} (minimum supported: {})",
221 remote.version,
222 HelloEnvelope::MIN_SUPPORTED_VERSION
223 )));
224 }
225 if remote.identity.app_id.len() > MAX_APP_ID_LEN
228 || remote.identity.device_name.len() > MAX_DEVICE_NAME_LEN
229 || remote.identity.device_id.len() > MAX_DEVICE_ID_LEN
230 || remote.identity.tailscale_id.len() > MAX_TAILSCALE_ID_LEN
231 || remote.identity.os.len() > MAX_OS_LEN
232 {
233 return Err(TransportError::HelloMalformed(
234 "identity field exceeds maximum allowed length".to_string(),
235 ));
236 }
237 if remote.identity.app_id != local_app_id {
238 return Err(TransportError::AppMismatch {
239 local: local_app_id.to_string(),
240 remote: remote.identity.app_id.clone(),
241 });
242 }
243 Ok(remote.identity)
244}
245
246async fn send_hello(
248 ws: &mut WebSocketStream<TcpStream>,
249 hello: &HelloEnvelope,
250) -> Result<(), TransportError> {
251 let payload =
252 serde_json::to_string(hello).map_err(|e| TransportError::Serialize(e.to_string()))?;
253 ws.send(Message::Text(payload.into()))
254 .await
255 .map_err(|e| TransportError::HandshakeFailed(format!("send hello: {e}")))
256}
257
258async fn client_hello_exchange(
262 ws: &mut WebSocketStream<TcpStream>,
263 local_hello: &HelloEnvelope,
264) -> Result<PeerIdentity, TransportError> {
265 if let Err(e) = send_hello(ws, local_hello).await {
267 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "local send failed").await;
268 return Err(e);
269 }
270
271 let remote = match receive_hello(ws).await {
273 Ok(envelope) => envelope,
274 Err(e) => {
275 match &e {
276 TransportError::HelloTimeout => {
277 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "hello timeout").await;
278 }
279 TransportError::HelloMalformed(_) => {
280 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "malformed hello").await;
281 }
282 _ => {}
283 }
284 return Err(e);
285 }
286 };
287
288 match validate_hello(remote, &local_hello.identity.app_id) {
290 Ok(identity) => Ok(identity),
291 Err(e) => {
292 match &e {
293 TransportError::AppMismatch { .. } => {
294 close_ws_with_code(ws, CLOSE_APP_MISMATCH, "app mismatch").await;
295 }
296 TransportError::HelloMalformed(_) => {
297 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "bad hello").await;
298 }
299 _ => {}
300 }
301 Err(e)
302 }
303 }
304}
305
306async fn server_hello_exchange(
309 ws: &mut WebSocketStream<TcpStream>,
310 local_hello: &HelloEnvelope,
311) -> Result<PeerIdentity, TransportError> {
312 let remote = match receive_hello(ws).await {
314 Ok(envelope) => envelope,
315 Err(e) => {
316 match &e {
317 TransportError::HelloTimeout => {
318 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "hello timeout").await;
319 }
320 TransportError::HelloMalformed(_) => {
321 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "malformed hello").await;
322 }
323 _ => {}
324 }
325 return Err(e);
326 }
327 };
328
329 let identity = match validate_hello(remote, &local_hello.identity.app_id) {
331 Ok(identity) => identity,
332 Err(e) => {
333 match &e {
334 TransportError::AppMismatch { .. } => {
335 close_ws_with_code(ws, CLOSE_APP_MISMATCH, "app mismatch").await;
336 }
337 TransportError::HelloMalformed(_) => {
338 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "bad hello").await;
339 }
340 _ => {}
341 }
342 return Err(e);
343 }
344 };
345
346 if let Err(e) = send_hello(ws, local_hello).await {
348 close_ws_with_code(ws, CLOSE_HELLO_PROTOCOL, "local send failed").await;
349 return Err(e);
350 }
351
352 Ok(identity)
353}
354
355impl<N: NetworkProvider + 'static> StreamTransport for WebSocketTransport<N> {
356 type Stream = WsFramedStream;
357
358 async fn connect(&self, addr: &PeerAddr) -> Result<Self::Stream, TransportError> {
359 let dial_addr = resolve_dial_addr(addr);
361 tracing::debug!(addr = %dial_addr, port = self.config.port, "ws: dialing peer");
362
363 let tcp_stream = self
364 .network
365 .dial_tcp(&dial_addr, self.config.port)
366 .await
367 .map_err(|e| TransportError::ConnectFailed(format!("dial tcp: {e}")))?;
368
369 let ws_url = format!("ws://{dial_addr}:{}/ws", self.config.port);
371 let ws_config = self.ws_protocol_config();
372 let (mut ws, _response) =
373 tokio_tungstenite::client_async_with_config(ws_url, tcp_stream, Some(ws_config))
374 .await
375 .map_err(|e| TransportError::ConnectFailed(format!("ws upgrade: {e}")))?;
376
377 let local_hello = self.local_hello();
379 let remote_identity = match client_hello_exchange(&mut ws, &local_hello).await {
380 Ok(identity) => identity,
381 Err(e) => {
382 match &e {
383 TransportError::AppMismatch { local, remote } => {
384 tracing::info!(
385 local_app_id = %local,
386 remote_app_id = %remote,
387 "ws: closing connection — app_id mismatch"
388 );
389 }
390 TransportError::HelloTimeout => {
391 tracing::warn!("ws: hello timeout on outgoing connection");
392 }
393 TransportError::HelloMalformed(msg) => {
394 tracing::warn!(error = %msg, "ws: malformed hello on outgoing connection");
395 }
396 _ => {}
397 }
398 return Err(e);
399 }
400 };
401
402 tracing::info!(
403 remote_device_id = %remote_identity.device_id,
404 remote_device_name = %remote_identity.device_name,
405 remote_tailscale_id = %remote_identity.tailscale_id,
406 "ws: connected (hello exchanged)"
407 );
408
409 Ok(WsFramedStream::new(
411 ws,
412 remote_identity.tailscale_id.clone(),
413 Some(remote_identity),
414 dial_addr,
415 self.config.ping_interval,
416 self.config.pong_timeout,
417 ))
418 }
419
420 async fn listen(&self) -> Result<StreamListener<Self::Stream>, TransportError> {
421 let port = self.config.port;
422 tracing::debug!(port, "ws: starting listener");
423
424 let mut tcp_listener = self
426 .network
427 .listen_tcp(port)
428 .await
429 .map_err(|e| TransportError::ListenFailed(format!("listen tcp: {e}")))?;
430
431 let (tx, rx) = tokio::sync::mpsc::channel::<WsFramedStream>(64);
433 let local_hello = self.local_hello();
434 let ping_interval = self.config.ping_interval;
435 let pong_timeout = self.config.pong_timeout;
436 let ws_config = self.ws_protocol_config();
437
438 tokio::spawn(async move {
439 loop {
440 match tcp_listener.incoming.recv().await {
441 Some(incoming) => {
442 let tx = tx.clone();
443 let local_hello = local_hello.clone();
444 let remote_addr = incoming.remote_addr.clone();
445 let ws_config = ws_config;
446
447 tokio::spawn(async move {
448 let mut ws = match tokio_tungstenite::accept_async_with_config(
450 incoming.stream,
451 Some(ws_config),
452 )
453 .await
454 {
455 Ok(ws) => ws,
456 Err(e) => {
457 tracing::warn!(
458 remote = %remote_addr,
459 "ws: upgrade failed: {e}"
460 );
461 return;
462 }
463 };
464
465 let remote_identity = match server_hello_exchange(&mut ws, &local_hello)
467 .await
468 {
469 Ok(identity) => identity,
470 Err(e) => {
471 match &e {
472 TransportError::AppMismatch { local, remote } => {
473 tracing::info!(
474 remote_addr = %remote_addr,
475 local_app_id = %local,
476 remote_app_id = %remote,
477 "ws: closing incoming connection — app_id mismatch"
478 );
479 }
480 TransportError::HelloTimeout => {
481 tracing::warn!(
482 remote_addr = %remote_addr,
483 "ws: hello timeout on incoming connection"
484 );
485 }
486 TransportError::HelloMalformed(msg) => {
487 tracing::warn!(
488 remote_addr = %remote_addr,
489 error = %msg,
490 "ws: malformed hello on incoming connection"
491 );
492 }
493 _ => {
494 tracing::warn!(
495 remote_addr = %remote_addr,
496 "ws: hello exchange failed: {e}"
497 );
498 }
499 }
500 return;
501 }
502 };
503
504 tracing::info!(
505 remote_device_id = %remote_identity.device_id,
506 remote_device_name = %remote_identity.device_name,
507 remote_tailscale_id = %remote_identity.tailscale_id,
508 remote_addr = %remote_addr,
509 "ws: accepted connection (hello exchanged)"
510 );
511
512 let stream = WsFramedStream::new(
513 ws,
514 remote_identity.tailscale_id.clone(),
515 Some(remote_identity),
516 remote_addr,
517 ping_interval,
518 pong_timeout,
519 );
520
521 if tx.send(stream).await.is_err() {
522 tracing::debug!("ws: listener channel closed");
523 }
524 });
525 }
526 None => {
527 tracing::debug!("ws: tcp listener channel closed");
528 break;
529 }
530 }
531 }
532 });
533
534 Ok(StreamListener::new(rx, port))
535 }
536}
537
538pub struct WsFramedStream {
555 write: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
557 read: SplitStream<WebSocketStream<TcpStream>>,
559 remote_peer_id: String,
562 remote_identity: Option<PeerIdentity>,
564 remote_addr: String,
566 heartbeat_handle: Option<tokio::task::JoinHandle<()>>,
568 last_pong: Arc<AtomicU64>,
570 closed: Arc<AtomicBool>,
572}
573
574impl std::fmt::Debug for WsFramedStream {
575 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
576 f.debug_struct("WsFramedStream")
577 .field("remote_peer_id", &self.remote_peer_id)
578 .field("remote_addr", &self.remote_addr)
579 .field("closed", &self.closed.load(Ordering::Relaxed))
580 .finish_non_exhaustive()
581 }
582}
583
584unsafe impl Sync for WsFramedStream {}
590
591fn epoch_millis() -> u64 {
593 std::time::SystemTime::now()
594 .duration_since(std::time::UNIX_EPOCH)
595 .unwrap_or_default()
596 .as_millis() as u64
597}
598
599impl WsFramedStream {
600 fn new(
602 ws: WebSocketStream<TcpStream>,
603 remote_peer_id: String,
604 remote_identity: Option<PeerIdentity>,
605 remote_addr: String,
606 ping_interval: Duration,
607 pong_timeout: Duration,
608 ) -> Self {
609 let (write, read) = ws.split();
610 let write = Arc::new(Mutex::new(write));
611 let last_pong = Arc::new(AtomicU64::new(epoch_millis()));
612 let closed = Arc::new(AtomicBool::new(false));
613
614 let hb_write = write.clone();
616 let hb_last_pong = last_pong.clone();
617 let hb_closed = closed.clone();
618 let hb_addr = remote_addr.clone();
619 let heartbeat_handle = tokio::spawn(async move {
620 heartbeat_loop(
621 hb_write,
622 hb_last_pong,
623 hb_closed,
624 ping_interval,
625 pong_timeout,
626 &hb_addr,
627 )
628 .await;
629 });
630
631 Self {
632 write,
633 read,
634 remote_peer_id,
635 remote_identity,
636 remote_addr,
637 heartbeat_handle: Some(heartbeat_handle),
638 last_pong,
639 closed,
640 }
641 }
642
643 pub fn remote_peer_id(&self) -> &str {
648 &self.remote_peer_id
649 }
650
651 pub fn remote_identity(&self) -> Option<&PeerIdentity> {
656 self.remote_identity.as_ref()
657 }
658}
659
660async fn heartbeat_loop(
670 write: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
671 last_pong: Arc<AtomicU64>,
672 closed: Arc<AtomicBool>,
673 ping_interval: Duration,
674 pong_timeout: Duration,
675 remote_addr: &str,
676) {
677 let mut interval = tokio::time::interval(ping_interval);
678 interval.tick().await;
680
681 loop {
682 interval.tick().await;
683
684 if closed.load(Ordering::Acquire) {
686 return;
687 }
688
689 let last = last_pong.load(Ordering::Acquire);
691 let now = epoch_millis();
692 let elapsed = Duration::from_millis(now.saturating_sub(last));
693
694 if elapsed > pong_timeout {
695 tracing::warn!(
696 remote = %remote_addr,
697 elapsed = ?elapsed,
698 "heartbeat: pong timeout after {pong_timeout:?}"
699 );
700 closed.store(true, Ordering::Release);
702 let mut w = write.lock().await;
703 let _ = w.close().await;
704 return;
705 }
706
707 {
709 let mut w = write.lock().await;
710 let ping_data = b"truffle-ping".to_vec();
711 if let Err(e) = w.send(Message::Ping(ping_data.into())).await {
712 tracing::debug!(remote = %remote_addr, "heartbeat: ping send failed: {e}");
713 closed.store(true, Ordering::Release);
714 return;
715 }
716 }
717 }
718}
719
720impl FramedStream for WsFramedStream {
721 async fn send(&mut self, data: &[u8]) -> Result<(), TransportError> {
722 if self.closed.load(Ordering::Acquire) {
723 return Err(TransportError::ConnectionClosed(
724 "connection already closed".to_string(),
725 ));
726 }
727 let mut w = self.write.lock().await;
728 w.send(Message::Binary(data.to_vec().into()))
729 .await
730 .map_err(|e| TransportError::WebSocket(format!("send: {e}")))
731 }
732
733 async fn recv(&mut self) -> Result<Option<Vec<u8>>, TransportError> {
734 if self.closed.load(Ordering::Acquire) {
735 return Ok(None);
736 }
737 loop {
738 match self.read.next().await {
739 Some(Ok(Message::Binary(data))) => return Ok(Some(data.to_vec())),
740 Some(Ok(Message::Text(text))) => {
741 return Ok(Some(text.as_bytes().to_vec()));
743 }
744 Some(Ok(Message::Ping(_))) => {
745 continue;
748 }
749 Some(Ok(Message::Pong(_))) => {
750 self.last_pong.store(epoch_millis(), Ordering::Release);
752 continue;
753 }
754 Some(Ok(Message::Close(_))) => {
755 self.closed.store(true, Ordering::Release);
756 return Ok(None);
757 }
758 Some(Ok(Message::Frame(_))) => {
759 continue;
761 }
762 Some(Err(e)) => {
763 self.closed.store(true, Ordering::Release);
764 return Err(TransportError::WebSocket(format!("recv: {e}")));
765 }
766 None => {
767 self.closed.store(true, Ordering::Release);
768 return Ok(None);
769 }
770 }
771 }
772 }
773
774 async fn close(&mut self) -> Result<(), TransportError> {
775 if let Some(handle) = self.heartbeat_handle.take() {
777 handle.abort();
778 }
779
780 self.closed.store(true, Ordering::Release);
781
782 let mut w = self.write.lock().await;
783 w.close()
784 .await
785 .map_err(|e| TransportError::WebSocket(format!("close: {e}")))
786 }
787
788 fn peer_addr(&self) -> String {
789 self.remote_addr.clone()
790 }
791}
792
793impl Drop for WsFramedStream {
794 fn drop(&mut self) {
795 if let Some(handle) = self.heartbeat_handle.take() {
797 handle.abort();
798 }
799 }
800}
801
802#[cfg(test)]
807mod unit_tests {
808 use super::*;
809
810 #[test]
811 fn resolve_dial_addr_prefers_ip() {
812 let addr = PeerAddr {
813 ip: Some("100.64.0.1".parse().unwrap()),
814 hostname: "peer".to_string(),
815 dns_name: Some("peer.tailnet.ts.net".to_string()),
816 };
817 assert_eq!(resolve_dial_addr(&addr), "100.64.0.1");
818 }
819
820 #[test]
821 fn resolve_dial_addr_falls_back_to_dns() {
822 let addr = PeerAddr {
823 ip: None,
824 hostname: "peer".to_string(),
825 dns_name: Some("peer.tailnet.ts.net".to_string()),
826 };
827 assert_eq!(resolve_dial_addr(&addr), "peer.tailnet.ts.net");
828 }
829
830 #[test]
831 fn resolve_dial_addr_falls_back_to_hostname() {
832 let addr = PeerAddr {
833 ip: None,
834 hostname: "peer".to_string(),
835 dns_name: None,
836 };
837 assert_eq!(resolve_dial_addr(&addr), "peer");
838 }
839
840 fn valid_identity() -> crate::session::hello::PeerIdentity {
843 crate::session::hello::PeerIdentity {
844 app_id: "playground".to_string(),
845 device_id: "01J4K9M2Z8AB3RNYQPW6H5TC0X".to_string(),
846 device_name: "Alice's MacBook".to_string(),
847 os: "darwin".to_string(),
848 tailscale_id: "n1234567890.ts-node".to_string(),
849 }
850 }
851
852 #[test]
853 fn validate_hello_accepts_normal_identity() {
854 let envelope = crate::session::hello::HelloEnvelope::new(valid_identity());
855 let result = validate_hello(envelope, "playground");
856 assert!(result.is_ok(), "baseline valid hello should validate");
857 }
858
859 #[test]
860 fn validate_hello_rejects_oversized_app_id() {
861 let mut identity = valid_identity();
862 identity.app_id = "a".repeat(MAX_APP_ID_LEN + 1);
863 let envelope = crate::session::hello::HelloEnvelope::new(identity);
864 let result = validate_hello(envelope, "playground");
865 match result {
866 Err(TransportError::HelloMalformed(msg)) => {
867 assert!(
868 msg.contains("maximum allowed length"),
869 "unexpected error: {msg}"
870 );
871 }
872 other => panic!("expected HelloMalformed, got {other:?}"),
873 }
874 }
875
876 #[test]
877 fn validate_hello_rejects_oversized_device_name() {
878 let mut identity = valid_identity();
879 identity.device_name = "x".repeat(MAX_DEVICE_NAME_LEN + 1);
880 let envelope = crate::session::hello::HelloEnvelope::new(identity);
881 let result = validate_hello(envelope, "playground");
882 assert!(matches!(result, Err(TransportError::HelloMalformed(_))));
883 }
884
885 #[test]
886 fn validate_hello_rejects_oversized_device_id() {
887 let mut identity = valid_identity();
888 identity.device_id = "x".repeat(MAX_DEVICE_ID_LEN + 1);
889 let envelope = crate::session::hello::HelloEnvelope::new(identity);
890 let result = validate_hello(envelope, "playground");
891 assert!(matches!(result, Err(TransportError::HelloMalformed(_))));
892 }
893
894 #[test]
895 fn validate_hello_rejects_oversized_tailscale_id() {
896 let mut identity = valid_identity();
897 identity.tailscale_id = "x".repeat(MAX_TAILSCALE_ID_LEN + 1);
898 let envelope = crate::session::hello::HelloEnvelope::new(identity);
899 let result = validate_hello(envelope, "playground");
900 assert!(matches!(result, Err(TransportError::HelloMalformed(_))));
901 }
902
903 #[test]
904 fn validate_hello_rejects_oversized_os() {
905 let mut identity = valid_identity();
906 identity.os = "x".repeat(MAX_OS_LEN + 1);
907 let envelope = crate::session::hello::HelloEnvelope::new(identity);
908 let result = validate_hello(envelope, "playground");
909 assert!(matches!(result, Err(TransportError::HelloMalformed(_))));
910 }
911
912 #[test]
913 fn validate_hello_length_check_runs_before_app_id_mismatch() {
914 let mut identity = valid_identity();
919 identity.device_name = "x".repeat(MAX_DEVICE_NAME_LEN + 1);
920 let envelope = crate::session::hello::HelloEnvelope::new(identity);
921 let result = validate_hello(envelope, "playground");
922 assert!(
923 matches!(result, Err(TransportError::HelloMalformed(_))),
924 "matched app_id must not let an oversized field through"
925 );
926 }
927}