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/// Exit-side PIX supervision: the lifecycle state machine, the egress gate, and the per-session
16/// worker that drives them.
17pub(crate) mod supervision;
18#[cfg(feature = "telemetry")]
19mod telemetry;
20mod types;
21mod utils;
22
23pub use balancer::{AtomicSurbFlowEstimator, BalancerStateValues, MIN_BALANCER_SAMPLING_INTERVAL, SurbBalancerConfig};
24use hopr_api::types::internal::routing::RoutingOptions;
25pub use hopr_protocol_session::{AcknowledgementMode, flow_control::FlowControlConfig};
26pub use hopr_utils::network_types::types::*;
27pub use manager::{
28 DEFAULT_MAX_SSAS_PER_SSA_REQUEST, DEFAULT_SSAS_PER_SSA_REQUEST, DEPOSIT_DATA_REQUEST_TIMEOUT, DispatchResult,
29 DropReason, IncomingSessionPixConfig, MAX_ASSUMED_SESSION_PACKET_RATE, MAX_OVERLAPPING_BATCHES, MAX_SSA_BATCH_SIZE,
30 MIN_SURB_BUFFER_DURATION, PixToolbox, SessionManager, SessionManagerConfig, cycle_budget_for,
31 max_cycle_budget_for_quota, validate_incoming_session_pix_config,
32};
33pub use supervision::{FillRate, PixFillConfig, SupervisorConfig, validate_pix_supervision};
34/// The supervisor state machine and its event/action vocabulary, for `benches/supervisor_bench.rs`,
35/// and the egress gate, for `benches/gate_bench.rs`.
36///
37/// Behind the same `benchmark` gate as
38/// [`SessionManager::pre_populate_session`](crate::SessionManager::pre_populate_session), and for
39/// the same reason: a criterion bench is a separate crate, so a path that is `pub(crate)` is a path
40/// it cannot measure. Nothing here is part of the crate's supported surface.
41#[cfg(any(feature = "benchmark", test))]
42pub use supervision::{
43 GateBlockReason, GateClosed, GateVerdict, ServiceGate, SessionPixAction, SessionPixEvent, SessionPixSupervisor,
44};
45#[cfg(any(test, feature = "testing"))]
46pub mod testing;
47pub use hopr_api::types::internal::routing::DestinationRouting;
48pub use hopr_protocol_app::prelude::{ApplicationDataIn, ApplicationDataOut};
49pub use hopr_protocol_pix::{InvalidPixParams, PixParams};
50#[cfg(feature = "telemetry")]
51pub use telemetry::{SessionAckMode, SessionLifecycleState};
52#[cfg(any(test, feature = "testing"))]
53pub use testing::{MsgSender as MockMsgSender, SendMsg, mock_packet_planning, msg_type, start_msg_match};
54pub use types::{
55 AgreedSsaQuota, ClosureReason, DEFAULT_PIX_POLYS_PER_SSA, DEFAULT_PIX_SHARES_PER_POLY, DEFAULT_PIX_SSA_QUOTA,
56 DEFAULT_PIX_SURPLUS_SHARES, HoprSession, HoprSessionCapabilities, HoprSessionConfig, HoprSessionInPixEvent,
57 HoprSessionOutPixEvent, HoprStartProtocol, IncomingSession, LOCAL_PIX_SUITE, PIX_QUOTA_BYTES_PER_SHARE, ServiceId,
58 SessionAdmissionReply, SessionAdmissionSink, SessionId, SessionTarget,
59};
60#[cfg(feature = "runtime-tokio")]
61pub use utils::{transfer_session, transfer_session_datagram};
62
63/// Number of bytes that can be sent in a single Session protocol payload.
64///
65/// In other words, this is the effective payload capacity of a single Session segment.
66pub const SESSION_MTU: usize =
67 hopr_protocol_session::session_socket_mtu::<{ hopr_protocol_app::v1::ApplicationData::PAYLOAD_SIZE }>();
68
69/// Size of the HOPR SURB in bytes.
70///
71/// This is the re-export of [`hopr_crypto_packet::HoprSurb::SIZE`].
72pub const SURB_SIZE: usize = hopr_crypto_packet::HoprSurb::SIZE;
73
74flagset::flags! {
75 /// Individual capabilities of a Session.
76 #[repr(u8)]
77 #[derive(PartialOrd, Ord, strum::EnumString, strum::Display, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
78 pub enum Capability : u8 {
79 /// Frame segmentation.
80 Segmentation = 0b0000_1000,
81 /// Frame retransmission (ACK-based)
82 ///
83 /// Implies [`Segmentation`].
84 RetransmissionAck = 0b0000_1100,
85 /// Frame retransmission (NACK-based)
86 ///
87 /// Implies [`Segmentation`].
88 RetransmissionNack = 0b000_1010,
89 /// UDP-like behavior: disable packet buffering, and — on stateless (non-retransmitting)
90 /// sessions — preserve datagram boundaries by emitting each write as exactly one frame
91 /// (delivered to the peer as exactly one read), regardless of `frame_mtu`. Use this for
92 /// datagram-oriented targets (e.g. UDP/WireGuard) that must not have datagrams split or
93 /// coalesced. On reliable (retransmitting) sessions only the buffering behavior applies;
94 /// boundary preservation is stateless-only (the NACK missing-segment bitmap cannot address
95 /// the segments of an oversized datagram frame).
96 ///
97 /// Boundary preservation holds only up to the reader's buffer: a single read cannot return
98 /// more bytes than its buffer, so a datagram larger than the peer's read buffer is still
99 /// delivered in multiple reads. Callers must therefore size the read buffer for the largest
100 /// datagram they need preserved. In the UDP-forwarding path the datagram size is bounded at
101 /// ingress by the forwarding buffer (`HOPR_UDP_BUFFER_SIZE`), so it never exceeds it there;
102 /// a datagram-mode frame may otherwise be as large as `64 * SESSION_MTU`.
103 ///
104 /// Implies [`Segmentation`].
105 NoDelay = 0b0000_1001,
106 /// Disable SURB-based egress rate control.
107 ///
108 /// This applies only to the recipient of the Session (Exit).
109 ///
110 /// If not set, the lower half of additional data may contain information about the desired SURB buffer size.
111 NoRateControl = 0b0001_0000,
112 /// Indicates to the Session recipient (Exit) that this Session should use the PIX protocol.
113 ///
114 /// The Exit may still refuse the Session if the offered quota
115 /// (from this node's [`SsaShareGenerator`](hopr_protocol_pix::SsaShareGenerator)) falls
116 /// outside its acceptable range, or (with `enforce_pix`) if this flag is *absent*.
117 UsePIX = 0b0010_0000,
118 }
119}
120
121/// Set of Session [capabilities](Capability).
122pub type Capabilities = flagset::FlagSet<Capability>;
123
124/// Configuration for the session.
125///
126/// Relevant primarily for the client, since the server is only
127/// a reactive component in regard to the session concept.
128#[derive(Debug, PartialEq, Clone, smart_default::SmartDefault)]
129pub struct SessionClientConfig {
130 /// The forward path options for the session.
131 #[default(RoutingOptions::Hops(hopr_api::types::primitive::bounded::BoundedSize::MIN))]
132 pub forward_path_options: RoutingOptions,
133 /// The return path options for the session.
134 #[default(RoutingOptions::Hops(hopr_api::types::primitive::bounded::BoundedSize::MIN))]
135 pub return_path_options: RoutingOptions,
136 /// Capabilities offered by the session.
137 #[default(_code = "Capability::Segmentation.into()")]
138 pub capabilities: Capabilities,
139 /// Optional pseudonym used for the session. Mostly useful for testing only.
140 #[default(None)]
141 pub pseudonym: Option<hopr_api::types::internal::protocol::HoprPseudonym>,
142 /// Enable automatic SURB management for the Session.
143 #[default(Some(SurbBalancerConfig::default()))]
144 pub surb_management: Option<SurbBalancerConfig>,
145 /// Sets the maximum number of SURBs sent with Session data packets (if they fit).
146 ///
147 /// This does not affect `KeepAlive` messages used with SURB balancing, as they will always
148 /// carry the maximum number of SURBs possible. Setting this to a higher number will put additional CPU
149 /// pressure on the local node as it will generate the given number of SURBs for each data packet.
150 ///
151 /// The default `1` follows the balancer's target: data packets carry no SURBs while the
152 /// counterparty is estimated to have a full buffer. Values greater than `1` bypass this gate;
153 /// use them only if the underlying traffic is highly asymmetric. Setting this to `0` leaves
154 /// all SURB production to the SURB balancer (if configured).
155 ///
156 /// The maximum number is naturally limited by the maximum payload size of a HOPR packet.
157 ///
158 /// Default is `1`.
159 #[default(1)]
160 pub max_surbs_per_data_packet: usize,
161 /// Opt-in client-side send-window flow control for this session.
162 ///
163 /// `None` (the default) leaves the session unpaced — today's behaviour. `Some(..)` enables the
164 /// adaptive AIMD window on the entry (sending) side; use [`FlowControlConfig::default`] for the
165 /// verified clean profile or [`FlowControlConfig::robust`] for the tail-tolerance bundle. This is
166 /// the client's explicit dial (only meaningful on a reliable / `RetransmissionAck` session).
167 #[default(None)]
168 pub flow_control: Option<FlowControlConfig>,
169 /// Abandon the frame due next once the sequence has advanced this far past it, instead of
170 /// holding everything already received for the whole frame timeout.
171 ///
172 /// Head-of-line bound for this session's incoming direction. `None` inherits the node's
173 /// setting, `Some(0)` disables it here, `Some(n)` sets it.
174 ///
175 /// Worth setting per session because the right value tracks reordering depth -- throughput x
176 /// latency spread / frame size -- which is a property of the traffic, not of the node: a bulk
177 /// data session and a control session on the same node differ by orders of magnitude.
178 ///
179 /// Has no effect on a session carrying a retransmission capability, where a missing frame can
180 /// still be recovered and waiting for it is productive.
181 pub max_frames_behind_gap: Option<usize>,
182}
183
184#[cfg(test)]
185mod tests {
186 use hopr_api::types::{crypto_random::Randomizable, internal::prelude::HoprPseudonym};
187 use hopr_crypto_packet::prelude::HoprPacket;
188 use hopr_protocol_app::v1::ApplicationData;
189 use hopr_protocol_session::session_socket_mtu;
190 use hopr_protocol_start::{
191 ErrorIdentifier, KeepAliveMessage, StartChallenge, StartErrorReason, StartErrorType, StartEstablished,
192 StartInitiation,
193 };
194
195 use super::*;
196 use crate::types::HoprStartProtocol;
197
198 #[test]
199 fn test_session_mtu() {
200 assert_eq!(SESSION_MTU, session_socket_mtu::<{ ApplicationData::PAYLOAD_SIZE }>());
201 assert_eq!(1452, SESSION_MTU); // Needs to be changed when the Session MTU cap changes
202 }
203
204 #[test]
205 fn hopr_start_protocol_messages_must_fit_within_hopr_packet() -> anyhow::Result<()> {
206 let msg = HoprStartProtocol::StartSession(StartInitiation {
207 challenge: StartChallenge::MAX,
208 target: SessionTarget::TcpStream(SealedHost::Plain(
209 "example-of-a-very-very-long-second-level-name.on-a-very-very-long-domain-name.info:65530".parse()?,
210 )),
211 capabilities: Capabilities::full().into(),
212 additional_data: (u64::MAX as u128) << u64::BITS,
213 });
214
215 assert!(
216 msg.encode()?.1.len() <= HoprPacket::PAYLOAD_SIZE,
217 "StartSession must fit within {}",
218 HoprPacket::PAYLOAD_SIZE
219 );
220
221 let msg = HoprStartProtocol::SessionEstablished(StartEstablished {
222 orig_challenge: StartChallenge::MAX,
223 session_id: HoprPseudonym::random(),
224 });
225
226 assert!(
227 msg.encode()?.1.len() <= HoprPacket::PAYLOAD_SIZE,
228 "SessionEstablished must fit within {}",
229 HoprPacket::PAYLOAD_SIZE
230 );
231
232 let msg = HoprStartProtocol::SessionError(StartErrorType {
233 identifier: ErrorIdentifier::Challenge(StartChallenge::MAX),
234 reason: StartErrorReason::NoSlotsAvailable,
235 });
236
237 assert!(
238 msg.encode()?.1.len() <= HoprPacket::PAYLOAD_SIZE,
239 "SessionError must fit within {}",
240 HoprPacket::PAYLOAD_SIZE
241 );
242
243 let msg = HoprStartProtocol::KeepAlive(KeepAliveMessage {
244 session_id: HoprPseudonym::random(),
245 flags: None.into(),
246 additional_data: 0xffffffff,
247 });
248 assert!(
249 msg.encode()?.1.len() <= HoprPacket::PAYLOAD_SIZE,
250 "KeepAlive must fit within {}",
251 HoprPacket::PAYLOAD_SIZE
252 );
253
254 Ok(())
255 }
256
257 #[test]
258 fn hopr_start_protocol_message_session_initiation_message_should_allow_for_at_least_one_surb() -> anyhow::Result<()>
259 {
260 let msg = HoprStartProtocol::StartSession(StartInitiation {
261 challenge: StartChallenge::MAX,
262 target: SessionTarget::TcpStream(SealedHost::Plain(
263 "example-of-a-very-very-long-second-level-name.on-a-very-very-long-domain-name.info:65530".parse()?,
264 )),
265 capabilities: Capabilities::full().into(),
266 additional_data: (u64::MAX as u128) << u64::BITS,
267 });
268 let len = msg.encode()?.1.len();
269 assert!(
270 HoprPacket::max_surbs_with_message(len) >= 1,
271 "Hopr StartSession message size ({}) must allow for at least 1 SURB in packet",
272 len
273 );
274
275 Ok(())
276 }
277
278 #[test]
279 fn hopr_start_protocol_message_keep_alive_message_should_allow_for_maximum_surbs() -> anyhow::Result<()> {
280 let keep_alive = KeepAliveMessage {
281 session_id: HoprPseudonym::random(),
282 flags: None.into(),
283 additional_data: 0xffffffff,
284 };
285 let capacity = keep_alive.max_surbs()?;
286 let data = ApplicationData::try_from(HoprStartProtocol::KeepAlive(keep_alive))?;
287
288 // Eight generation-tagged SURBs fit in an empty packet, but the keep-alive and its tag
289 // together need one byte more than the remaining space.
290 assert_eq!(capacity, 7);
291 assert_eq!(capacity, HoprPacket::max_surbs_with_message(data.total_len()));
292
293 Ok(())
294 }
295}