Skip to main content

hopr_transport_session/
lib.rs

1//! [`HoprSession`] object providing the session functionality over the HOPR transport
2//!
3//! The session proxies the user interactions with the transport to hide the
4//! advanced interactions and functionality.
5//!
6//! The [`SessionManager`] allows for automatic management of sessions via the Start protocol.
7//!
8//! This crate implements [RFC-0007](https://github.com/hoprnet/rfc/tree/main/rfcs/RFC-0007-session-protocol).
9
10pub(crate) mod balancer;
11pub mod counters;
12pub mod errors;
13pub mod flow_control;
14mod manager;
15#[cfg(feature = "telemetry")]
16mod telemetry;
17mod types;
18mod utils;
19
20pub use balancer::{AtomicSurbFlowEstimator, BalancerStateValues, MIN_BALANCER_SAMPLING_INTERVAL, SurbBalancerConfig};
21use hopr_api::types::internal::routing::RoutingOptions;
22pub use hopr_protocol_session::{AcknowledgementMode, flow_control::FlowControlConfig};
23pub use hopr_utils::network_types::types::*;
24pub use manager::{
25    DEFAULT_MAX_SSAS_PER_SSA_REQUEST, DEFAULT_SSAS_PER_SSA_REQUEST, DispatchResult, IncomingSessionPixConfig,
26    MAX_SSA_BATCH_SIZE, MIN_SURB_BUFFER_DURATION, PixToolbox, SessionManager, SessionManagerConfig,
27};
28#[cfg(any(test, feature = "testing"))]
29pub mod testing;
30pub use hopr_api::types::internal::routing::DestinationRouting;
31pub use hopr_protocol_app::prelude::{ApplicationDataIn, ApplicationDataOut};
32pub use hopr_protocol_pix::{InvalidPixParams, PixParams};
33#[cfg(feature = "telemetry")]
34pub use telemetry::{SessionAckMode, SessionLifecycleState};
35#[cfg(any(test, feature = "testing"))]
36pub use testing::{MsgSender as MockMsgSender, SendMsg, mock_packet_planning, msg_type, start_msg_match};
37pub use types::{
38    AgreedSsaQuota, DEFAULT_PIX_POLYS_PER_SSA, DEFAULT_PIX_SHARES_PER_POLY, DEFAULT_PIX_SSA_QUOTA,
39    DEFAULT_PIX_SURPLUS_SHARES, HoprSession, HoprSessionCapabilities, HoprSessionConfig, HoprSessionInPixEvent,
40    HoprSessionOutPixEvent, HoprStartProtocol, IncomingSession, LOCAL_PIX_SUITE, ServiceId, SessionId, SessionTarget,
41};
42#[cfg(feature = "runtime-tokio")]
43pub use utils::transfer_session;
44
45/// Number of bytes that can be sent in a single Session protocol payload.
46///
47/// In other words, this is the effective payload capacity of a single Session segment.
48pub const SESSION_MTU: usize =
49    hopr_protocol_session::session_socket_mtu::<{ hopr_protocol_app::v1::ApplicationData::PAYLOAD_SIZE }>();
50
51/// Size of the HOPR SURB in bytes.
52///
53/// This is the re-export of [`hopr_crypto_packet::HoprSurb::SIZE`].
54pub const SURB_SIZE: usize = hopr_crypto_packet::HoprSurb::SIZE;
55
56flagset::flags! {
57    /// Individual capabilities of a Session.
58    #[repr(u8)]
59    #[derive(PartialOrd, Ord, strum::EnumString, strum::Display, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
60    pub enum Capability : u8 {
61        /// Frame segmentation.
62        Segmentation = 0b0000_1000,
63        /// Frame retransmission (ACK-based)
64        ///
65        /// Implies [`Segmentation`].
66        RetransmissionAck = 0b0000_1100,
67        /// Frame retransmission (NACK-based)
68        ///
69        /// Implies [`Segmentation`].
70        RetransmissionNack = 0b000_1010,
71        /// Disable packet buffering.
72        ///
73        /// Implies [`Segmentation`].
74        NoDelay = 0b0000_1001,
75        /// Disable SURB-based egress rate control.
76        ///
77        /// This applies only to the recipient of the Session (Exit).
78        ///
79        /// If not set, the lower half of additional data may contain information about the desired SURB buffer size.
80        NoRateControl = 0b0001_0000,
81        /// Indicates to the Session recipient (Exit) that this Session should use the PIX protocol.
82        ///
83        /// The upper half of additional data may be used to configure the PIX protocol parameters.
84        UsePIX = 0b0010_0000,
85    }
86}
87
88/// Set of Session [capabilities](Capability).
89pub type Capabilities = flagset::FlagSet<Capability>;
90
91/// Configuration for the session.
92///
93/// Relevant primarily for the client, since the server is only
94/// a reactive component in regard to the session concept.
95#[derive(Debug, PartialEq, Clone, smart_default::SmartDefault)]
96pub struct SessionClientConfig {
97    /// The forward path options for the session.
98    #[default(RoutingOptions::Hops(hopr_api::types::primitive::bounded::BoundedSize::MIN))]
99    pub forward_path_options: RoutingOptions,
100    /// The return path options for the session.
101    #[default(RoutingOptions::Hops(hopr_api::types::primitive::bounded::BoundedSize::MIN))]
102    pub return_path_options: RoutingOptions,
103    /// Capabilities offered by the session.
104    #[default(_code = "Capability::Segmentation.into()")]
105    pub capabilities: Capabilities,
106    /// Optional pseudonym used for the session. Mostly useful for testing only.
107    #[default(None)]
108    pub pseudonym: Option<hopr_api::types::internal::protocol::HoprPseudonym>,
109    /// Enable automatic SURB management for the Session.
110    #[default(Some(SurbBalancerConfig::default()))]
111    pub surb_management: Option<SurbBalancerConfig>,
112    /// If set, the maximum number of possible SURBs will always be sent with Session data packets (if they fit).
113    ///
114    /// This does not affect `KeepAlive` messages used with SURB balancing, as they will always
115    /// carry the maximum number of SURBs possible. Setting this to `true` will put additional CPU
116    /// pressure on the local node as it will generate the maximum number of SURBs for each data packet.
117    ///
118    /// Set this to `true` only when the underlying traffic is highly asymmetric.
119    ///
120    /// Default is `false`.
121    #[default(false)]
122    pub always_max_out_surbs: bool,
123    /// PIX parameters for SSAs.
124    ///
125    /// When not set, the Session will not advertise any PIX capability and may
126    /// get refused by the Exit (if it requires PIX).
127    ///
128    /// The Exit may also refuse to accept the Session if the given values
129    /// evaluate to a PIX quota that is not within Exit's acceptable PIX quota range.
130    ///
131    /// These are not free parameters: the shares this node puts on the wire come from the installed
132    /// [`SsaShareGenerator`](hopr_protocol_pix::SsaShareGenerator), so
133    /// [`SessionManager::new_session`] refuses any value that disagrees with it rather than
134    /// advertising dimensions it cannot honour. Setting this is therefore an assertion about the
135    /// node's own PIX configuration — build it with
136    /// [`PixParams::try_from_config`](hopr_protocol_pix::PixParams::try_from_config) over that
137    /// generator's config if you do not want to restate it.
138    ///
139    /// The fourth component, the curve suite, is fixed by how this node was built rather than
140    /// configured; [`LOCAL_PIX_SUITE`] names it for anyone restating the values by hand.
141    ///
142    /// Defaults to `None`.
143    pub pix_ssa_quota: Option<PixParams>,
144    /// Opt-in client-side send-window flow control for this session.
145    ///
146    /// `None` (the default) leaves the session unpaced — today's behaviour. `Some(..)` enables the
147    /// adaptive AIMD window on the entry (sending) side; use [`FlowControlConfig::default`] for the
148    /// verified clean profile or [`FlowControlConfig::robust`] for the tail-tolerance bundle. This is
149    /// the client's explicit dial (only meaningful on a reliable / `RetransmissionAck` session).
150    #[default(None)]
151    pub flow_control: Option<FlowControlConfig>,
152    /// Abandon the frame due next once the sequence has advanced this far past it, instead of
153    /// holding everything already received for the whole frame timeout.
154    ///
155    /// Head-of-line bound for this session's incoming direction. `None` inherits the node's
156    /// setting, `Some(0)` disables it here, `Some(n)` sets it.
157    ///
158    /// Worth setting per session because the right value tracks reordering depth -- throughput x
159    /// latency spread / frame size -- which is a property of the traffic, not of the node: a bulk
160    /// data session and a control session on the same node differ by orders of magnitude.
161    ///
162    /// Has no effect on a session carrying a retransmission capability, where a missing frame can
163    /// still be recovered and waiting for it is productive.
164    pub max_frames_behind_gap: Option<usize>,
165}
166
167#[cfg(test)]
168mod tests {
169    use hopr_api::types::{crypto_random::Randomizable, internal::prelude::HoprPseudonym};
170    use hopr_crypto_packet::prelude::HoprPacket;
171    use hopr_protocol_app::v1::ApplicationData;
172    use hopr_protocol_session::session_socket_mtu;
173    use hopr_protocol_start::{
174        ErrorIdentifier, KeepAliveMessage, StartChallenge, StartErrorReason, StartErrorType, StartEstablished,
175        StartInitiation,
176    };
177
178    use super::*;
179    use crate::types::HoprStartProtocol;
180
181    #[test]
182    fn test_session_mtu() {
183        assert_eq!(SESSION_MTU, session_socket_mtu::<{ ApplicationData::PAYLOAD_SIZE }>());
184        assert_eq!(1020, SESSION_MTU); // Needs to be changed when HOPR packet payload size changes
185    }
186
187    #[test]
188    fn hopr_start_protocol_messages_must_fit_within_hopr_packet() -> anyhow::Result<()> {
189        let msg = HoprStartProtocol::StartSession(StartInitiation {
190            challenge: StartChallenge::MAX,
191            target: SessionTarget::TcpStream(SealedHost::Plain(
192                "example-of-a-very-very-long-second-level-name.on-a-very-very-long-domain-name.info:65530".parse()?,
193            )),
194            capabilities: Capabilities::full().into(),
195            additional_data: 0xffffffff,
196        });
197
198        assert!(
199            msg.encode()?.1.len() <= HoprPacket::PAYLOAD_SIZE,
200            "StartSession must fit within {}",
201            HoprPacket::PAYLOAD_SIZE
202        );
203
204        let msg = HoprStartProtocol::SessionEstablished(StartEstablished {
205            orig_challenge: StartChallenge::MAX,
206            session_id: HoprPseudonym::random(),
207        });
208
209        assert!(
210            msg.encode()?.1.len() <= HoprPacket::PAYLOAD_SIZE,
211            "SessionEstablished must fit within {}",
212            HoprPacket::PAYLOAD_SIZE
213        );
214
215        let msg = HoprStartProtocol::SessionError(StartErrorType {
216            identifier: ErrorIdentifier::Challenge(StartChallenge::MAX),
217            reason: StartErrorReason::NoSlotsAvailable,
218        });
219
220        assert!(
221            msg.encode()?.1.len() <= HoprPacket::PAYLOAD_SIZE,
222            "SessionError must fit within {}",
223            HoprPacket::PAYLOAD_SIZE
224        );
225
226        let msg = HoprStartProtocol::KeepAlive(KeepAliveMessage {
227            session_id: HoprPseudonym::random(),
228            flags: None.into(),
229            additional_data: 0xffffffff,
230        });
231        assert!(
232            msg.encode()?.1.len() <= HoprPacket::PAYLOAD_SIZE,
233            "KeepAlive must fit within {}",
234            HoprPacket::PAYLOAD_SIZE
235        );
236
237        Ok(())
238    }
239
240    #[test]
241    fn hopr_start_protocol_message_session_initiation_message_should_allow_for_at_least_one_surb() -> anyhow::Result<()>
242    {
243        let msg = HoprStartProtocol::StartSession(StartInitiation {
244            challenge: StartChallenge::MAX,
245            target: SessionTarget::TcpStream(SealedHost::Plain(
246                "example-of-a-very-very-long-second-level-name.on-a-very-very-long-domain-name.info:65530".parse()?,
247            )),
248            capabilities: Capabilities::full().into(),
249            additional_data: 0xffffffff,
250        });
251        let len = msg.encode()?.1.len();
252        assert!(
253            HoprPacket::max_surbs_with_message(len) >= 1,
254            "Hopr StartSession message size ({}) must allow for at least 1 SURB in packet",
255            len
256        );
257
258        Ok(())
259    }
260
261    #[test]
262    fn hopr_start_protocol_message_keep_alive_message_should_allow_for_maximum_surbs() -> anyhow::Result<()> {
263        let msg = HoprStartProtocol::KeepAlive(KeepAliveMessage {
264            session_id: HoprPseudonym::random(),
265            flags: None.into(),
266            additional_data: 0xffffffff,
267        });
268        let len = msg.encode()?.1.len();
269        assert_eq!(
270            KeepAliveMessage::<SessionId>::MIN_SURBS_PER_MESSAGE,
271            HoprPacket::MAX_SURBS_IN_PACKET
272        );
273        assert!(
274            HoprPacket::max_surbs_with_message(len) >= HoprPacket::MAX_SURBS_IN_PACKET,
275            "Hopr KeepAlive message size ({}) must allow for at least {} SURBs in packet",
276            len,
277            HoprPacket::MAX_SURBS_IN_PACKET
278        );
279
280        Ok(())
281    }
282}