Skip to main content

protocol/session/
mod.rs

1//! Connection state machines for both ends of the device link, without I/O.
2//!
3//! A [`HostSession`] runs in the daemon and an [`AgentSession`] in a device
4//! agent. The caller feeds bytes read from the socket into `receive`, writes
5//! whatever `poll_transmit` returns, calls `handle_timeout` at `poll_timeout`,
6//! and reads what happened from `poll_event`. Every method that depends on
7//! time takes `now`; the sessions never read the clock themselves.
8//!
9//! The agent speaks first: its hello carries the versions it speaks, its
10//! capabilities and its maximum frame size. The host answers with a welcome
11//! naming the highest version both speak and the smaller maximum frame size,
12//! or with the reason it refuses the connection before closing it.
13
14mod agent;
15mod host;
16
17use core::fmt;
18use std::time::Duration;
19
20pub use agent::{AgentConfig, AgentEvent, AgentSession, Responded};
21pub use host::{HostConfig, HostEvent, HostSession};
22
23use crate::capability::Capabilities;
24use crate::error::{Error, code};
25use crate::frame::MIN_MAX_FRAME;
26
27/// How long a peer may stay silent before a ping goes out.
28pub const PING_INTERVAL: Duration = Duration::from_secs(1);
29
30/// How long a peer may stay silent before it is declared dead. With pings
31/// every second, three unanswered ones; the spec's bound for reporting a
32/// killed agent is five seconds.
33pub const PEER_TIMEOUT: Duration = Duration::from_secs(3);
34
35/// How long past a request's deadline the host waits for the agent's own
36/// deadline error before reporting one itself.
37pub const DEADLINE_GRACE: Duration = Duration::from_millis(500);
38
39/// How long the host waits before it sends a failed automatic resync again;
40/// the wait doubles with every failure in a row, up to [`RESYNC_BACKOFF_MAX`].
41pub const RESYNC_BACKOFF: Duration = Duration::from_millis(100);
42
43/// The longest wait between two tries of an automatic resync.
44pub const RESYNC_BACKOFF_MAX: Duration = Duration::from_secs(2);
45
46/// The highest version in both lists, whatever their order.
47pub fn select_version(ours: &[u16], theirs: &[u16]) -> Option<u16> {
48    ours.iter().filter(|v| theirs.contains(v)).max().copied()
49}
50
51/// Where a session is, without the data each state holds.
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53enum Phase {
54    Handshake,
55    Ready,
56    Closed,
57}
58
59/// What the handshake settled.
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct Negotiated {
62    /// The protocol version both sides use.
63    pub version: u16,
64    /// The maximum frame payload both sides accept.
65    pub max_frame: u32,
66    /// What the agent advertised.
67    pub capabilities: Capabilities,
68    /// The agent's name and version from its hello.
69    pub agent: String,
70    pub host_versions: Vec<u16>,
71    pub agent_versions: Vec<u16>,
72}
73
74/// Why a handshake failed.
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub enum HandshakeError {
77    /// The two sides share no protocol version.
78    NoCommonVersion { host: Vec<u16>, agent: Vec<u16> },
79    /// The hello's token is not the one the host started the agent with.
80    TokenMismatch,
81    /// The negotiated maximum frame size is below [`MIN_MAX_FRAME`].
82    FrameLimitTooSmall { limit: u32 },
83    /// The host selected a version the agent does not speak.
84    VersionNotOffered { version: u16, offered: Vec<u16> },
85    /// The host refused the connection (seen by the agent).
86    Refused(Error),
87}
88
89impl HandshakeError {
90    /// The error the host sends in its welcome.
91    pub fn to_error(&self) -> Error {
92        match self {
93            HandshakeError::NoCommonVersion { .. } | HandshakeError::VersionNotOffered { .. } => {
94                Error::new(code::VERSION_MISMATCH, self.to_string())
95            }
96            HandshakeError::TokenMismatch => Error::new(code::TOKEN_MISMATCH, self.to_string()),
97            HandshakeError::FrameLimitTooSmall { .. } => {
98                Error::new(code::FRAME_LIMIT_TOO_SMALL, self.to_string())
99            }
100            HandshakeError::Refused(error) => error.clone(),
101        }
102    }
103}
104
105impl fmt::Display for HandshakeError {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            HandshakeError::NoCommonVersion { host, agent } => write!(
109                f,
110                "no common protocol version: the daemon speaks {host:?} and the agent speaks {agent:?}; redeploy the agent that matches this daemon"
111            ),
112            HandshakeError::TokenMismatch => f.write_str(
113                "the agent's hello carries another per-launch token, so this daemon did not start it",
114            ),
115            HandshakeError::FrameLimitTooSmall { limit } => write!(
116                f,
117                "the negotiated maximum frame size of {limit} bytes is below the protocol minimum of {MIN_MAX_FRAME} bytes"
118            ),
119            HandshakeError::VersionNotOffered { version, offered } => write!(
120                f,
121                "the daemon selected protocol version {version}, which the agent does not speak (it speaks {offered:?})"
122            ),
123            HandshakeError::Refused(error) => write!(f, "the daemon refused the connection: {error}"),
124        }
125    }
126}
127
128impl std::error::Error for HandshakeError {}
129
130/// Why a session closed.
131#[derive(Clone, Debug, PartialEq, Eq)]
132pub enum DisconnectReason {
133    /// The other side closed the connection.
134    Eof,
135    /// Nothing arrived from the other side for this long.
136    Silent(Duration),
137    /// The handshake failed.
138    Handshake(HandshakeError),
139    /// A message could not be decoded or was not expected.
140    Protocol(String),
141    /// This side closed the session.
142    Closed,
143}
144
145impl fmt::Display for DisconnectReason {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        match self {
148            DisconnectReason::Eof => f.write_str("the other side closed the connection"),
149            DisconnectReason::Silent(silence) => write!(
150                f,
151                "nothing arrived from the other side for {:.1} s",
152                silence.as_secs_f64()
153            ),
154            DisconnectReason::Handshake(e) => write!(f, "handshake failed: {e}"),
155            DisconnectReason::Protocol(detail) => write!(f, "protocol error: {detail}"),
156            DisconnectReason::Closed => f.write_str("the session was closed"),
157        }
158    }
159}