1use std::{
2 convert::Into,
3 fmt::{Debug, Formatter},
4 pin::Pin,
5 task::{Context, Poll},
6 time::Duration,
7};
8
9use futures::{SinkExt, StreamExt, TryStreamExt};
10use hopr_api::types::{
11 internal::{prelude::HoprPseudonym, routing::DestinationRouting},
12 primitive::errors::GeneralError,
13};
14use hopr_protocol_app::prelude::{ApplicationData, ApplicationDataIn, ApplicationDataOut, ReservedTag, Tag};
15#[cfg(feature = "telemetry")]
16use hopr_protocol_session::NoopTracker;
17use hopr_protocol_session::{
18 AcknowledgementMode, AcknowledgementState, AcknowledgementStateConfig, ReliableSocket, SessionSocketConfig,
19 UnreliableSocket,
20};
21use hopr_protocol_start::StartProtocol;
22use hopr_utils::network_types::{
23 prelude::SealedHost,
24 utils::{AsyncWriteSink, DuplexIO},
25};
26use tracing::{debug, instrument};
27
28use crate::{Capabilities, Capability, errors::TransportSessionError};
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct ByteCapabilities(pub Capabilities);
33
34impl TryFrom<u8> for ByteCapabilities {
35 type Error = GeneralError;
36
37 fn try_from(value: u8) -> Result<Self, Self::Error> {
38 Capabilities::new(value)
39 .map(Self)
40 .map_err(|_| GeneralError::ParseError("capabilities".into()))
41 }
42}
43
44impl From<ByteCapabilities> for u8 {
45 fn from(value: ByteCapabilities) -> Self {
46 *value.0.as_ref()
47 }
48}
49
50impl From<ByteCapabilities> for Capabilities {
51 fn from(value: ByteCapabilities) -> Self {
52 value.0
53 }
54}
55
56impl From<Capabilities> for ByteCapabilities {
57 fn from(value: Capabilities) -> Self {
58 Self(value)
59 }
60}
61
62impl AsRef<Capabilities> for ByteCapabilities {
63 fn as_ref(&self) -> &Capabilities {
64 &self.0
65 }
66}
67
68pub type HoprStartProtocol = StartProtocol<SessionId, SessionTarget, ByteCapabilities>;
70
71pub const SESSION_APPLICATION_TAG: Tag = Tag::Reserved(ReservedTag::Session as u64);
74
75pub type SessionId = HoprPseudonym;
80
81pub(crate) fn caps_to_ack_mode(caps: Capabilities) -> AcknowledgementMode {
82 if caps.contains(Capability::RetransmissionAck | Capability::RetransmissionNack) {
83 AcknowledgementMode::Both
84 } else if caps.contains(Capability::RetransmissionAck) {
85 AcknowledgementMode::Full
86 } else {
87 AcknowledgementMode::Partial
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)]
93pub enum ClosureReason {
94 WriteClosed,
96 EmptyRead,
98 Eviction,
100}
101
102trait AsyncReadWrite: futures::AsyncWrite + futures::AsyncRead + Send + Unpin {}
104impl<T: futures::AsyncWrite + futures::AsyncRead + Send + Unpin> AsyncReadWrite for T {}
105
106pub type ServiceId = u32;
112
113#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
116pub enum SessionTarget {
117 UdpStream(SealedHost),
119 TcpStream(SealedHost),
121 ExitNode(ServiceId),
123}
124
125#[derive(Debug)]
128pub struct IncomingSession {
129 pub session: HoprSession,
131 pub target: SessionTarget,
133}
134
135#[derive(Copy, Clone, Debug, PartialEq, Eq, smart_default::SmartDefault, serde::Serialize)]
137pub struct HoprSessionConfig {
138 #[default(Capabilities::empty())]
142 pub capabilities: Capabilities,
143 #[default(1500)]
147 pub frame_mtu: usize,
148 #[default(Duration::from_millis(800))]
152 #[serde(with = "humantime_serde")]
153 pub frame_timeout: Duration,
154 #[default(0)]
159 pub max_buffered_segments: usize,
160}
161
162#[pin_project::pin_project]
167pub struct HoprSession {
168 id: SessionId,
169 #[pin]
170 inner: Box<dyn AsyncReadWrite>,
171 routing: DestinationRouting,
172 cfg: HoprSessionConfig,
173 on_close: Option<Box<dyn FnOnce(SessionId, ClosureReason) + Send + Sync>>,
174}
175
176pub(crate) const SESSION_SOCKET_CAPACITY: usize = 16384;
177
178impl HoprSession {
179 #[tracing::instrument(skip_all, fields(id, routing, cfg, session_id = %id))]
187 pub fn new<Tx, Rx>(
188 id: SessionId,
189 routing: DestinationRouting,
190 cfg: HoprSessionConfig,
191 hopr: (Tx, Rx),
192 on_close: Option<Box<dyn FnOnce(SessionId, ClosureReason) + Send + Sync>>,
193 ) -> Result<Self, TransportSessionError>
194 where
195 Tx: futures::Sink<(DestinationRouting, ApplicationDataOut)> + Send + Unpin + 'static,
196 Rx: futures::Stream<Item = ApplicationDataIn> + Send + Unpin + 'static,
197 Tx::Error: std::error::Error + Send + Sync,
198 {
199 let routing_clone = routing.clone();
200
201 #[cfg(feature = "telemetry")]
202 let (session_id_write, session_id_read) = (id, id);
203
204 let transport = DuplexIO(
206 AsyncWriteSink::<{ ApplicationData::PAYLOAD_SIZE }, _>(hopr.0.sink_map_err(std::io::Error::other).with(
207 move |buf: Box<[u8]>| {
208 #[cfg(feature = "telemetry")]
209 crate::telemetry::record_session_write(&session_id_write, buf.len());
210 futures::future::ready(
213 ApplicationData::new(SESSION_APPLICATION_TAG, buf.into_vec())
214 .map(|data| (routing_clone.clone(), ApplicationDataOut::with_no_packet_info(data)))
215 .map_err(std::io::Error::other),
216 )
217 },
218 )),
219 hopr.1
222 .map(move |data| {
223 #[cfg(feature = "telemetry")]
224 crate::telemetry::record_session_read(&session_id_read, data.data.plain_text.len());
225 Ok::<_, std::io::Error>(data.data.plain_text)
226 })
227 .into_async_read(),
228 );
229
230 let inner: Box<dyn AsyncReadWrite> = if cfg.capabilities.contains(Capability::Segmentation) {
232 let socket_cfg = SessionSocketConfig {
233 frame_size: cfg.frame_mtu,
234 frame_timeout: cfg.frame_timeout,
235 capacity: SESSION_SOCKET_CAPACITY,
236 flush_immediately: cfg.capabilities.contains(Capability::NoDelay),
237 max_buffered_segments: cfg.max_buffered_segments,
238 ..Default::default()
239 };
240
241 if cfg.capabilities.contains(Capability::RetransmissionAck)
244 || cfg.capabilities.contains(Capability::RetransmissionNack)
245 {
246 let ack_cfg = AcknowledgementStateConfig {
248 expected_packet_latency: Duration::from_millis(200),
253 mode: caps_to_ack_mode(cfg.capabilities),
254 backoff_base: 0.2,
255 max_incoming_frame_retries: 1,
256 max_outgoing_frame_retries: 2,
257 ..Default::default()
258 };
259
260 debug!(?socket_cfg, ?ack_cfg, "opening new stateful session socket");
261
262 Box::new(ReliableSocket::new(
263 transport,
264 AcknowledgementState::<{ ApplicationData::PAYLOAD_SIZE }>::new(id, ack_cfg),
265 socket_cfg,
266 #[cfg(feature = "telemetry")]
267 NoopTracker,
268 )?)
269 } else {
270 debug!(?socket_cfg, "opening new stateless session socket");
271
272 Box::new(UnreliableSocket::<{ ApplicationData::PAYLOAD_SIZE }>::new_stateless(
273 id,
274 transport,
275 socket_cfg,
276 #[cfg(feature = "telemetry")]
277 NoopTracker,
278 )?)
279 }
280 } else {
281 debug!("opening raw session socket");
282 Box::new(transport)
283 };
284
285 Ok(Self {
286 id,
287 inner,
288 routing,
289 cfg,
290 on_close,
291 })
292 }
293
294 pub fn id(&self) -> &SessionId {
296 &self.id
297 }
298
299 pub fn routing(&self) -> &DestinationRouting {
301 &self.routing
302 }
303
304 pub fn config(&self) -> &HoprSessionConfig {
306 &self.cfg
307 }
308}
309
310impl std::fmt::Debug for HoprSession {
311 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
312 f.debug_struct("Session")
313 .field("id", &self.id)
314 .field("routing", &self.routing)
315 .finish_non_exhaustive()
316 }
317}
318
319impl futures::AsyncRead for HoprSession {
320 #[instrument(name = "Session::poll_read", level = "trace", skip_all, fields(session_id = %self.id), ret)]
321 fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<std::io::Result<usize>> {
322 let this = self.project();
323 let read = futures::ready!(this.inner.poll_read(cx, buf))?;
324 if read == 0 {
325 tracing::trace!("hopr session empty read");
326 if let Some(notifier) = this.on_close.take() {
328 tracing::trace!("notifying read half closure of session");
329 notifier(*this.id, ClosureReason::EmptyRead);
330 }
331 }
332 Poll::Ready(Ok(read))
333 }
334}
335
336impl futures::AsyncWrite for HoprSession {
337 #[instrument(name = "Session::poll_write", level = "trace", skip_all, fields(session_id = %self.id), ret)]
338 fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<std::io::Result<usize>> {
339 self.project().inner.poll_write(cx, buf)
340 }
341
342 #[instrument(name = "Session::poll_flush", level = "trace", skip_all, fields(session_id = %self.id), ret)]
343 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
344 self.project().inner.poll_flush(cx)
345 }
346
347 #[instrument(name = "Session::poll_close", level = "trace", skip_all, fields(session_id = %self.id), ret)]
348 fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
349 let this = self.project();
350 futures::ready!(this.inner.poll_close(cx))?;
351 tracing::trace!("hopr session closed");
352
353 #[cfg(feature = "telemetry")]
354 crate::telemetry::set_session_state(this.id, crate::telemetry::SessionLifecycleState::Closing);
355
356 if let Some(notifier) = this.on_close.take() {
357 tracing::trace!("notifying write half closure of session");
358 notifier(*this.id, ClosureReason::WriteClosed);
359 }
360
361 Poll::Ready(Ok(()))
362 }
363}
364
365#[cfg(feature = "runtime-tokio")]
366impl tokio::io::AsyncRead for HoprSession {
367 fn poll_read(
368 mut self: Pin<&mut Self>,
369 cx: &mut Context<'_>,
370 buf: &mut tokio::io::ReadBuf<'_>,
371 ) -> Poll<std::io::Result<()>> {
372 let slice = buf.initialize_unfilled();
373 let n = std::task::ready!(futures::AsyncRead::poll_read(self.as_mut(), cx, slice))?;
374 buf.advance(n);
375 Poll::Ready(Ok(()))
376 }
377}
378
379#[cfg(feature = "runtime-tokio")]
380impl tokio::io::AsyncWrite for HoprSession {
381 fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<Result<usize, std::io::Error>> {
382 futures::AsyncWrite::poll_write(self.as_mut(), cx, buf)
383 }
384
385 fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
386 futures::AsyncWrite::poll_flush(self.as_mut(), cx)
387 }
388
389 fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
390 futures::AsyncWrite::poll_close(self.as_mut(), cx)
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use anyhow::Context;
397 use futures::{AsyncReadExt, AsyncWriteExt};
398 use hopr_api::types::{
399 crypto::prelude::*, crypto_random::Randomizable, internal::routing::RoutingOptions, primitive::prelude::*,
400 };
401
402 use super::*;
403
404 #[test]
407 fn byte_capabilities_roundtrip_via_u8() -> anyhow::Result<()> {
408 let flags: Capabilities = Capability::Segmentation.into();
409 let caps = ByteCapabilities::from(flags);
410 let byte_val: u8 = caps.into();
411 let restored = ByteCapabilities::try_from(byte_val)?;
412 assert_eq!(caps, restored);
413 Ok(())
414 }
415
416 #[test]
417 fn byte_capabilities_invalid_bits_are_rejected() {
418 assert!(ByteCapabilities::try_from(0xFF_u8).is_err());
420 }
421
422 #[test]
423 fn byte_capabilities_empty_is_zero() {
424 let caps = ByteCapabilities::from(Capabilities::empty());
425 let byte_val: u8 = caps.into();
426 assert_eq!(byte_val, 0);
427 }
428
429 #[test]
430 fn byte_capabilities_combined_flags() -> anyhow::Result<()> {
431 let caps: Capabilities = Capability::Segmentation | Capability::NoRateControl;
432 let byte_caps = ByteCapabilities::from(caps);
433 let byte_val: u8 = byte_caps.into();
434 let restored = ByteCapabilities::try_from(byte_val)?;
435 assert_eq!(*restored.as_ref(), caps);
436 Ok(())
437 }
438
439 #[test]
442 fn caps_to_ack_mode_both_when_ack_and_nack() {
443 let caps: Capabilities = Capability::RetransmissionAck | Capability::RetransmissionNack;
444 assert_eq!(caps_to_ack_mode(caps), AcknowledgementMode::Both);
445 }
446
447 #[test]
448 fn caps_to_ack_mode_full_when_only_ack() {
449 let caps: Capabilities = Capability::RetransmissionAck.into();
450 assert_eq!(caps_to_ack_mode(caps), AcknowledgementMode::Full);
451 }
452
453 #[test]
454 fn caps_to_ack_mode_partial_when_no_retransmission() {
455 let caps: Capabilities = Capability::Segmentation.into();
456 assert_eq!(caps_to_ack_mode(caps), AcknowledgementMode::Partial);
457 }
458
459 #[test]
460 fn caps_to_ack_mode_partial_when_empty() {
461 assert_eq!(caps_to_ack_mode(Capabilities::empty()), AcknowledgementMode::Partial);
462 }
463
464 #[test]
465 fn caps_to_ack_mode_should_be_partial_when_only_nack() {
466 let caps: Capabilities = Capability::RetransmissionNack.into();
467 assert_eq!(caps_to_ack_mode(caps), AcknowledgementMode::Partial);
468 }
469
470 #[test]
473 fn closure_reason_display_values_are_stable() {
474 let reasons = [
475 ClosureReason::WriteClosed,
476 ClosureReason::EmptyRead,
477 ClosureReason::Eviction,
478 ];
479 insta::assert_debug_snapshot!(reasons);
480 }
481
482 #[test]
485 fn hopr_session_config_default_snapshot() {
486 let cfg = HoprSessionConfig::default();
487 insta::assert_yaml_snapshot!(cfg);
488 }
489
490 #[test]
493 fn session_target_variants_debug_snapshot() -> anyhow::Result<()> {
494 let targets: Vec<SessionTarget> = vec![
495 SessionTarget::UdpStream(SealedHost::Plain(
496 "127.0.0.1:8080".parse().context("parsing UDP target")?,
497 )),
498 SessionTarget::TcpStream(SealedHost::Plain("10.0.0.1:443".parse().context("parsing TCP target")?)),
499 SessionTarget::ExitNode(42),
500 ];
501 insta::assert_debug_snapshot!(targets);
502 Ok(())
503 }
504
505 #[test]
508 fn session_id_display_and_debug_should_be_identical() {
509 let id = HoprPseudonym::random();
510 assert_eq!(format!("{id}"), format!("{id:?}"));
511 }
512
513 #[test]
514 fn session_id_hash_eq_consistency() {
515 use std::collections::HashSet;
516 let pseudonym = HoprPseudonym::random();
517 let id1: SessionId = pseudonym;
518 let id2: SessionId = pseudonym;
519 let id3: SessionId = HoprPseudonym::random();
520
521 let mut set = HashSet::new();
522 set.insert(id1);
523 assert!(set.contains(&id2));
524 assert!(!set.contains(&id3), "different pseudonym should not be in the set");
525 }
526
527 #[test_log::test(tokio::test)]
530 async fn test_session_bidirectional_flow_without_segmentation() -> anyhow::Result<()> {
531 let dst: Address = (&ChainKeypair::random()).into();
532 let id: SessionId = HoprPseudonym::random();
533 const DATA_LEN: usize = 5000;
534
535 let (alice_tx, bob_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
536 let (bob_tx, alice_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
537
538 let mut alice_session = HoprSession::new(
539 id,
540 DestinationRouting::forward_only(dst, RoutingOptions::Hops(0.try_into()?)),
541 Default::default(),
542 (
543 alice_tx,
544 alice_rx
545 .map(|(_, data)| ApplicationDataIn {
546 data: data.data,
547 packet_info: Default::default(),
548 })
549 .inspect(|d| debug!("alice rcvd: {}", d.data.total_len())),
550 ),
551 None,
552 )?;
553
554 let mut bob_session = HoprSession::new(
555 id,
556 DestinationRouting::Return(id.into()),
557 Default::default(),
558 (
559 bob_tx,
560 bob_rx
561 .map(|(_, data)| ApplicationDataIn {
562 data: data.data,
563 packet_info: Default::default(),
564 })
565 .inspect(|d| debug!("bob rcvd: {}", d.data.total_len())),
566 ),
567 None,
568 )?;
569
570 let alice_sent = hopr_api::types::crypto_random::random_bytes::<DATA_LEN>();
571 let bob_sent = hopr_api::types::crypto_random::random_bytes::<DATA_LEN>();
572
573 let mut bob_recv = [0u8; DATA_LEN];
574 let mut alice_recv = [0u8; DATA_LEN];
575
576 tokio::time::timeout(Duration::from_secs(1), alice_session.write_all(&alice_sent))
577 .await
578 .context("alice write failed")?
579 .context("alice write timed out")?;
580 alice_session.flush().await?;
581
582 tokio::time::timeout(Duration::from_secs(1), bob_session.write_all(&bob_sent))
583 .await
584 .context("bob write failed")?
585 .context("bob write timed out")?;
586 bob_session.flush().await?;
587
588 tokio::time::timeout(Duration::from_secs(1), bob_session.read_exact(&mut bob_recv))
589 .await
590 .context("bob read failed")?
591 .context("bob read timed out")?;
592
593 tokio::time::timeout(Duration::from_secs(1), alice_session.read_exact(&mut alice_recv))
594 .await
595 .context("alice read failed")?
596 .context("alice read timed out")?;
597
598 assert_eq!(&alice_sent, bob_recv.as_slice());
599 assert_eq!(bob_sent, alice_recv);
600
601 Ok(())
602 }
603
604 #[test_log::test(tokio::test)]
605 async fn test_session_bidirectional_flow_with_segmentation() -> anyhow::Result<()> {
606 let dst: Address = (&ChainKeypair::random()).into();
607 let id: SessionId = HoprPseudonym::random();
608 const DATA_LEN: usize = 5000;
609
610 let (alice_tx, bob_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
611 let (bob_tx, alice_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
612
613 let mut alice_session = HoprSession::new(
614 id,
615 DestinationRouting::forward_only(dst, RoutingOptions::Hops(0.try_into()?)),
616 HoprSessionConfig {
617 capabilities: Capability::Segmentation.into(),
618 ..Default::default()
619 },
620 (
621 alice_tx,
622 alice_rx
623 .map(|(_, data)| ApplicationDataIn {
624 data: data.data,
625 packet_info: Default::default(),
626 })
627 .inspect(|d| debug!("alice rcvd: {}", d.data.total_len())),
628 ),
629 None,
630 )?;
631
632 let mut bob_session = HoprSession::new(
633 id,
634 DestinationRouting::Return(id.into()),
635 HoprSessionConfig {
636 capabilities: Capability::Segmentation.into(),
637 ..Default::default()
638 },
639 (
640 bob_tx,
641 bob_rx
642 .map(|(_, data)| ApplicationDataIn {
643 data: data.data,
644 packet_info: Default::default(),
645 })
646 .inspect(|d| debug!("bob rcvd: {}", d.data.total_len())),
647 ),
648 None,
649 )?;
650
651 let alice_sent = hopr_api::types::crypto_random::random_bytes::<DATA_LEN>();
652 let bob_sent = hopr_api::types::crypto_random::random_bytes::<DATA_LEN>();
653
654 let mut bob_recv = [0u8; DATA_LEN];
655 let mut alice_recv = [0u8; DATA_LEN];
656
657 tokio::time::timeout(Duration::from_secs(1), alice_session.write_all(&alice_sent))
658 .await
659 .context("alice write failed")?
660 .context("alice write timed out")?;
661 alice_session.flush().await?;
662
663 tokio::time::timeout(Duration::from_secs(1), bob_session.write_all(&bob_sent))
664 .await
665 .context("bob write failed")?
666 .context("bob write timed out")?;
667 bob_session.flush().await?;
668
669 tokio::time::timeout(Duration::from_secs(1), bob_session.read_exact(&mut bob_recv))
670 .await
671 .context("bob read failed")?
672 .context("bob read timed out")?;
673
674 tokio::time::timeout(Duration::from_secs(1), alice_session.read_exact(&mut alice_recv))
675 .await
676 .context("alice read failed")?
677 .context("alice read timed out")?;
678
679 assert_eq!(alice_sent, bob_recv);
680 assert_eq!(bob_sent, alice_recv);
681
682 Ok(())
683 }
684}