1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
30use std::sync::Arc;
31use std::time::Duration;
32
33use futures_util::stream::SplitSink;
34use futures_util::stream::SplitStream;
35use futures_util::{SinkExt, StreamExt};
36use tokio::net::TcpStream;
37use tokio::sync::Mutex;
38use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
39use tokio_tungstenite::tungstenite::Message;
40use tokio_tungstenite::WebSocketStream;
41
42use crate::network::{NetworkProvider, PeerAddr};
43
44use super::{
45 resolve_dial_addr, FramedStream, Handshake, StreamListener, StreamTransport, TransportError,
46 WsConfig, PROTOCOL_VERSION,
47};
48
49const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
51
52pub struct WebSocketTransport<N: NetworkProvider> {
72 network: Arc<N>,
74 config: WsConfig,
76}
77
78impl<N: NetworkProvider + 'static> WebSocketTransport<N> {
79 pub fn new(network: Arc<N>, config: WsConfig) -> Self {
84 Self { network, config }
85 }
86
87 fn local_handshake(&self) -> Handshake {
89 let identity = self.network.local_identity();
90 Handshake {
91 peer_id: identity.id,
92 capabilities: vec!["ws".to_string(), "binary".to_string()],
93 protocol_version: PROTOCOL_VERSION,
94 }
95 }
96
97 fn ws_protocol_config(&self) -> WebSocketConfig {
99 let mut config = WebSocketConfig::default();
100 config.max_message_size = Some(self.config.max_message_size);
101 config.max_frame_size = Some(self.config.max_message_size);
102 config
103 }
104
105 async fn client_handshake(
107 ws: &mut WebSocketStream<TcpStream>,
108 local_hs: &Handshake,
109 ) -> Result<Handshake, TransportError> {
110 let hs_json = serde_json::to_string(local_hs)
112 .map_err(|e| TransportError::Serialize(e.to_string()))?;
113 ws.send(Message::Text(hs_json.into()))
114 .await
115 .map_err(|e| TransportError::HandshakeFailed(format!("send handshake: {e}")))?;
116
117 let remote_hs = receive_handshake(ws).await?;
119
120 if remote_hs.protocol_version != PROTOCOL_VERSION {
122 return Err(TransportError::VersionMismatch {
123 local: PROTOCOL_VERSION,
124 remote: remote_hs.protocol_version,
125 });
126 }
127
128 Ok(remote_hs)
129 }
130}
131
132async fn receive_handshake(
134 ws: &mut WebSocketStream<TcpStream>,
135) -> Result<Handshake, TransportError> {
136 match ws.next().await {
137 Some(Ok(Message::Text(text))) => serde_json::from_str::<Handshake>(&text)
138 .map_err(|e| TransportError::HandshakeFailed(format!("parse handshake: {e}"))),
139 Some(Ok(other)) => Err(TransportError::HandshakeFailed(format!(
140 "expected text frame for handshake, got: {other:?}"
141 ))),
142 Some(Err(e)) => Err(TransportError::HandshakeFailed(format!(
143 "receive handshake: {e}"
144 ))),
145 None => Err(TransportError::HandshakeFailed(
146 "connection closed before handshake".to_string(),
147 )),
148 }
149}
150
151impl<N: NetworkProvider + 'static> StreamTransport for WebSocketTransport<N> {
152 type Stream = WsFramedStream;
153
154 async fn connect(&self, addr: &PeerAddr) -> Result<Self::Stream, TransportError> {
155 let dial_addr = resolve_dial_addr(addr);
157 tracing::debug!(addr = %dial_addr, port = self.config.port, "ws: dialing peer");
158
159 let tcp_stream = self
160 .network
161 .dial_tcp(&dial_addr, self.config.port)
162 .await
163 .map_err(|e| TransportError::ConnectFailed(format!("dial tcp: {e}")))?;
164
165 let ws_url = format!("ws://{dial_addr}:{}/ws", self.config.port);
167 let ws_config = self.ws_protocol_config();
168 let (mut ws, _response) =
169 tokio_tungstenite::client_async_with_config(ws_url, tcp_stream, Some(ws_config))
170 .await
171 .map_err(|e| TransportError::ConnectFailed(format!("ws upgrade: {e}")))?;
172
173 let local_hs = self.local_handshake();
175 let remote_hs = tokio::time::timeout(HANDSHAKE_TIMEOUT, Self::client_handshake(&mut ws, &local_hs))
176 .await
177 .map_err(|_| TransportError::Timeout("handshake timed out".to_string()))??;
178
179 tracing::info!(
180 remote_peer = %remote_hs.peer_id,
181 remote_version = remote_hs.protocol_version,
182 "ws: connected"
183 );
184
185 Ok(WsFramedStream::new(
187 ws,
188 remote_hs.peer_id,
189 dial_addr,
190 self.config.ping_interval,
191 self.config.pong_timeout,
192 ))
193 }
194
195 async fn listen(&self) -> Result<StreamListener<Self::Stream>, TransportError> {
196 let port = self.config.port;
197 tracing::debug!(port, "ws: starting listener");
198
199 let mut tcp_listener = self
201 .network
202 .listen_tcp(port)
203 .await
204 .map_err(|e| TransportError::ListenFailed(format!("listen tcp: {e}")))?;
205
206 let (tx, rx) = tokio::sync::mpsc::channel::<WsFramedStream>(64);
208 let local_hs = self.local_handshake();
209 let ping_interval = self.config.ping_interval;
210 let pong_timeout = self.config.pong_timeout;
211 let ws_config = self.ws_protocol_config();
212
213 tokio::spawn(async move {
214 loop {
215 match tcp_listener.incoming.recv().await {
216 Some(incoming) => {
217 let tx = tx.clone();
218 let local_hs = local_hs.clone();
219 let remote_addr = incoming.remote_addr.clone();
220 let ws_config = ws_config;
221
222 tokio::spawn(async move {
223 let mut ws =
225 match tokio_tungstenite::accept_async_with_config(
226 incoming.stream,
227 Some(ws_config),
228 )
229 .await
230 {
231 Ok(ws) => ws,
232 Err(e) => {
233 tracing::warn!(
234 remote = %remote_addr,
235 "ws: upgrade failed: {e}"
236 );
237 return;
238 }
239 };
240
241 let remote_hs = match tokio::time::timeout(
243 HANDSHAKE_TIMEOUT,
244 server_handshake_standalone(&mut ws, &local_hs),
245 )
246 .await
247 {
248 Ok(Ok(hs)) => hs,
249 Ok(Err(e)) => {
250 tracing::warn!(
251 remote = %remote_addr,
252 "ws: handshake failed: {e}"
253 );
254 return;
255 }
256 Err(_) => {
257 tracing::warn!(
258 remote = %remote_addr,
259 "ws: handshake timed out"
260 );
261 return;
262 }
263 };
264
265 tracing::info!(
266 remote_peer = %remote_hs.peer_id,
267 remote_addr = %remote_addr,
268 "ws: accepted connection"
269 );
270
271 let stream = WsFramedStream::new(
272 ws,
273 remote_hs.peer_id,
274 remote_addr,
275 ping_interval,
276 pong_timeout,
277 );
278
279 if tx.send(stream).await.is_err() {
280 tracing::debug!("ws: listener channel closed");
281 }
282 });
283 }
284 None => {
285 tracing::debug!("ws: tcp listener channel closed");
286 break;
287 }
288 }
289 }
290 });
291
292 Ok(StreamListener::new(rx, port))
293 }
294}
295
296async fn server_handshake_standalone(
298 ws: &mut WebSocketStream<TcpStream>,
299 local_hs: &Handshake,
300) -> Result<Handshake, TransportError> {
301 let remote_hs = receive_handshake(ws).await?;
303
304 if remote_hs.protocol_version != PROTOCOL_VERSION {
306 return Err(TransportError::VersionMismatch {
307 local: PROTOCOL_VERSION,
308 remote: remote_hs.protocol_version,
309 });
310 }
311
312 let hs_json = serde_json::to_string(local_hs)
314 .map_err(|e| TransportError::Serialize(e.to_string()))?;
315 ws.send(Message::Text(hs_json.into()))
316 .await
317 .map_err(|e| TransportError::HandshakeFailed(format!("send handshake: {e}")))?;
318
319 Ok(remote_hs)
320}
321
322pub struct WsFramedStream {
339 write: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
341 read: SplitStream<WebSocketStream<TcpStream>>,
343 remote_peer_id: String,
345 remote_addr: String,
347 heartbeat_handle: Option<tokio::task::JoinHandle<()>>,
349 last_pong: Arc<AtomicU64>,
351 closed: Arc<AtomicBool>,
353}
354
355impl std::fmt::Debug for WsFramedStream {
356 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357 f.debug_struct("WsFramedStream")
358 .field("remote_peer_id", &self.remote_peer_id)
359 .field("remote_addr", &self.remote_addr)
360 .field("closed", &self.closed.load(Ordering::Relaxed))
361 .finish_non_exhaustive()
362 }
363}
364
365unsafe impl Sync for WsFramedStream {}
371
372fn epoch_millis() -> u64 {
374 std::time::SystemTime::now()
375 .duration_since(std::time::UNIX_EPOCH)
376 .unwrap_or_default()
377 .as_millis() as u64
378}
379
380impl WsFramedStream {
381 fn new(
383 ws: WebSocketStream<TcpStream>,
384 remote_peer_id: String,
385 remote_addr: String,
386 ping_interval: Duration,
387 pong_timeout: Duration,
388 ) -> Self {
389 let (write, read) = ws.split();
390 let write = Arc::new(Mutex::new(write));
391 let last_pong = Arc::new(AtomicU64::new(epoch_millis()));
392 let closed = Arc::new(AtomicBool::new(false));
393
394 let hb_write = write.clone();
396 let hb_last_pong = last_pong.clone();
397 let hb_closed = closed.clone();
398 let hb_addr = remote_addr.clone();
399 let heartbeat_handle = tokio::spawn(async move {
400 heartbeat_loop(hb_write, hb_last_pong, hb_closed, ping_interval, pong_timeout, &hb_addr)
401 .await;
402 });
403
404 Self {
405 write,
406 read,
407 remote_peer_id,
408 remote_addr,
409 heartbeat_handle: Some(heartbeat_handle),
410 last_pong,
411 closed,
412 }
413 }
414
415 pub fn remote_peer_id(&self) -> &str {
417 &self.remote_peer_id
418 }
419}
420
421async fn heartbeat_loop(
431 write: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
432 last_pong: Arc<AtomicU64>,
433 closed: Arc<AtomicBool>,
434 ping_interval: Duration,
435 pong_timeout: Duration,
436 remote_addr: &str,
437) {
438 let mut interval = tokio::time::interval(ping_interval);
439 interval.tick().await;
441
442 loop {
443 interval.tick().await;
444
445 if closed.load(Ordering::Acquire) {
447 return;
448 }
449
450 let last = last_pong.load(Ordering::Acquire);
452 let now = epoch_millis();
453 let elapsed = Duration::from_millis(now.saturating_sub(last));
454
455 if elapsed > pong_timeout {
456 tracing::warn!(
457 remote = %remote_addr,
458 elapsed = ?elapsed,
459 "heartbeat: pong timeout after {pong_timeout:?}"
460 );
461 closed.store(true, Ordering::Release);
463 let mut w = write.lock().await;
464 let _ = w.close().await;
465 return;
466 }
467
468 {
470 let mut w = write.lock().await;
471 let ping_data = b"truffle-ping".to_vec();
472 if let Err(e) = w.send(Message::Ping(ping_data.into())).await {
473 tracing::debug!(remote = %remote_addr, "heartbeat: ping send failed: {e}");
474 closed.store(true, Ordering::Release);
475 return;
476 }
477 }
478 }
479}
480
481impl FramedStream for WsFramedStream {
482 async fn send(&mut self, data: &[u8]) -> Result<(), TransportError> {
483 if self.closed.load(Ordering::Acquire) {
484 return Err(TransportError::ConnectionClosed(
485 "connection already closed".to_string(),
486 ));
487 }
488 let mut w = self.write.lock().await;
489 w.send(Message::Binary(data.to_vec().into()))
490 .await
491 .map_err(|e| TransportError::WebSocket(format!("send: {e}")))
492 }
493
494 async fn recv(&mut self) -> Result<Option<Vec<u8>>, TransportError> {
495 if self.closed.load(Ordering::Acquire) {
496 return Ok(None);
497 }
498 loop {
499 match self.read.next().await {
500 Some(Ok(Message::Binary(data))) => return Ok(Some(data.to_vec())),
501 Some(Ok(Message::Text(text))) => {
502 return Ok(Some(text.as_bytes().to_vec()));
504 }
505 Some(Ok(Message::Ping(_))) => {
506 continue;
509 }
510 Some(Ok(Message::Pong(_))) => {
511 self.last_pong.store(epoch_millis(), Ordering::Release);
513 continue;
514 }
515 Some(Ok(Message::Close(_))) => {
516 self.closed.store(true, Ordering::Release);
517 return Ok(None);
518 }
519 Some(Ok(Message::Frame(_))) => {
520 continue;
522 }
523 Some(Err(e)) => {
524 self.closed.store(true, Ordering::Release);
525 return Err(TransportError::WebSocket(format!("recv: {e}")));
526 }
527 None => {
528 self.closed.store(true, Ordering::Release);
529 return Ok(None);
530 }
531 }
532 }
533 }
534
535 async fn close(&mut self) -> Result<(), TransportError> {
536 if let Some(handle) = self.heartbeat_handle.take() {
538 handle.abort();
539 }
540
541 self.closed.store(true, Ordering::Release);
542
543 let mut w = self.write.lock().await;
544 w.close()
545 .await
546 .map_err(|e| TransportError::WebSocket(format!("close: {e}")))
547 }
548
549 fn peer_addr(&self) -> String {
550 self.remote_addr.clone()
551 }
552}
553
554impl Drop for WsFramedStream {
555 fn drop(&mut self) {
556 if let Some(handle) = self.heartbeat_handle.take() {
558 handle.abort();
559 }
560 }
561}
562
563#[cfg(test)]
568mod unit_tests {
569 use super::*;
570
571 #[test]
572 fn resolve_dial_addr_prefers_ip() {
573 let addr = PeerAddr {
574 ip: Some("100.64.0.1".parse().unwrap()),
575 hostname: "peer".to_string(),
576 dns_name: Some("peer.tailnet.ts.net".to_string()),
577 };
578 assert_eq!(resolve_dial_addr(&addr), "100.64.0.1");
579 }
580
581 #[test]
582 fn resolve_dial_addr_falls_back_to_dns() {
583 let addr = PeerAddr {
584 ip: None,
585 hostname: "peer".to_string(),
586 dns_name: Some("peer.tailnet.ts.net".to_string()),
587 };
588 assert_eq!(resolve_dial_addr(&addr), "peer.tailnet.ts.net");
589 }
590
591 #[test]
592 fn resolve_dial_addr_falls_back_to_hostname() {
593 let addr = PeerAddr {
594 ip: None,
595 hostname: "peer".to_string(),
596 dns_name: None,
597 };
598 assert_eq!(resolve_dial_addr(&addr), "peer");
599 }
600}