1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
use std::{collections::TryReserveError, io, time::Duration};
use thiserror::Error;
/// Failure reported while configuring, opening, running, receiving, closing, or
/// joining an IRTT client session.
///
/// Configuration validation errors usually require changing
/// [`ClientConfig`](crate::ClientConfig) or related options before retrying.
/// Network, protocol, and runtime errors may be transient, but can also reflect
/// peer incompatibility or an invalid session state.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ClientError {
/// A Tokio-backed client operation was polled without a current runtime.
#[cfg(feature = "tokio")]
#[error("Tokio AsyncClient requires a current Tokio runtime")]
NoTokioRuntime,
/// The configured server address could not be resolved to a socket address.
///
/// Check the host, port, address-family restrictions, and name service
/// configuration before retrying.
#[error("failed to resolve server address {addr:?}")]
Resolve { addr: String },
/// A UDP socket operation failed while binding, connecting, sending, or
/// receiving.
///
/// This can be a transient network or OS error, but may also reflect local
/// permissions, routing, or socket configuration.
#[error("socket error: {0}")]
Socket(#[from] io::Error),
/// Applying or reading a socket option failed.
///
/// This is usually caused by an unsupported option, an invalid local value,
/// or a platform-specific socket limitation. Changing socket configuration
/// is more likely to help than retrying unchanged.
#[error("socket option error while trying to {operation} for {remote}: {source}")]
SocketOption {
operation: &'static str,
remote: std::net::SocketAddr,
source: io::Error,
},
/// Restoring the configured read timeout after open negotiation failed.
///
/// The prepared session was not committed, and the client remains
/// connected rather than partially open.
#[error("failed to restore configured socket read timeout after open negotiation: {source}")]
ReadTimeoutRestore { source: io::Error },
/// A packet failed IRTT protocol parsing or authentication.
///
/// Likely causes include malformed input, unsupported protocol data, or an
/// HMAC mismatch between client and server.
#[error("protocol error: {0}")]
Protocol(#[from] irtt_proto::ProtoError),
/// No valid open reply was received before all configured open attempts
/// timed out.
///
/// Retrying can succeed when the server or network is temporarily
/// unavailable. Persistent failures usually require changing the address,
/// authentication key, server configuration, or timeout settings.
#[error("all open requests timed out")]
OpenTimeout,
/// An open-attempt timeout is below the client's supported minimum.
///
/// Increase the configured timeout before retrying.
#[error("open timeout {timeout:?} is below the minimum {minimum:?}")]
OpenTimeoutTooSmall {
timeout: Duration,
minimum: Duration,
},
/// The configured open-attempt timeout list is empty.
///
/// Provide at least one timeout before opening the session.
#[error("open_timeouts must not be empty")]
NoOpenTimeouts,
/// The server explicitly rejected the open request.
///
/// Retrying unchanged is unlikely to help unless the server configuration or
/// load has changed.
#[error("server rejected the open request")]
ServerRejected,
/// A no-test open request received a reply that did not have no-test close
/// semantics.
///
/// This indicates an incompatible or unexpected peer response.
#[error("unexpected no-test open reply")]
UnexpectedNoTestReply,
/// A no-test open reply included a connection token.
///
/// No-test negotiation must not establish a probe session, so a non-zero
/// token indicates an incompatible or unexpected peer response.
#[error("no-test reply included a non-zero connection token: {token}")]
NonZeroNoTestToken { token: u64 },
/// The peer replied with a protocol version different from the requested
/// version.
///
/// This is a protocol compatibility error and requires compatible client
/// and server versions.
#[error("protocol version mismatch: requested {requested}, received {received}")]
ProtocolVersionMismatch { requested: i64, received: i64 },
/// The server returned a zero connection token for a normal test session.
///
/// A normal session requires a non-zero token to correlate echo traffic, so
/// this indicates an invalid peer response.
#[error("server returned a zero connection token")]
ZeroToken,
/// Negotiated parameters were rejected by the configured negotiation policy.
///
/// Use a less restrictive configuration or
/// [`NegotiationPolicy::Loose`](crate::NegotiationPolicy::Loose) when the
/// caller accepts documented server restrictions.
#[error("strict negotiation rejected changed params: {reason}")]
NegotiationRejected { reason: String },
/// The requested operation requires an open session, but the client is only
/// connected.
#[error("client is not open")]
NotOpen,
/// The session is already open.
///
/// Continue running, receiving, or closing the existing session instead of
/// opening it again.
#[error("client session is already open")]
AlreadyOpen,
/// The no-test session has already completed.
///
/// Create a new client session to perform another negotiation.
#[error("client session is already completed")]
AlreadyCompleted,
/// The session is already closed.
///
/// Create a new client session before opening or running another test.
#[error("client session is already closed")]
AlreadyClosed,
/// A duration could not be represented as nanoseconds for scheduling or
/// protocol encoding.
///
/// Use smaller configured or negotiated durations.
#[error("duration is too large to encode as nanoseconds")]
DurationOverflow,
/// A client-maintained counter would overflow.
///
/// This is a runtime limit rather than a network failure. Start a new
/// session if the test duration or send volume reaches this limit.
#[error("counter {counter} overflowed")]
CounterOverflow { counter: &'static str },
/// Sending another probe would exceed the configured pending-probe limit.
///
/// Increase [`ClientConfig::max_pending_probes`](crate::ClientConfig::max_pending_probes),
/// reduce send rate, or receive replies and timeouts more often before
/// sending again.
#[error("pending probe limit exceeded ({limit})")]
PendingLimitExceeded { limit: usize },
/// A wrapping wire sequence is still represented by an in-flight probe.
///
/// The existing probe must be replied to or timed out before that sequence
/// can be reused.
#[error("wire sequence {seq} is still pending")]
PendingSequenceCollision { seq: u32 },
/// A prepared probe no longer matches the session's next wire sequence.
///
/// Prepare the probe again after any intervening successful send.
#[error(
"prepared probe sequence {prepared_seq} is stale; next wire sequence is {next_wire_seq}"
)]
StalePreparedProbe {
prepared_seq: u32,
next_wire_seq: u32,
},
/// Reserving memory required by an operation failed before any datagram was
/// sent or logical protocol state was changed.
#[error("failed to reserve memory for {operation}: {source}")]
AllocationFailed {
operation: &'static str,
#[source]
source: TryReserveError,
},
/// UDP reported successful acceptance of a datagram with an unexpected
/// byte count.
///
/// The send is committed locally before this transport invariant failure
/// is returned because the kernel reported success.
#[error("UDP accepted {actual} bytes for a {expected}-byte datagram")]
DatagramLengthMismatch { expected: usize, actual: usize },
/// Client configuration failed validation before the requested operation.
///
/// The reported reason names a value that must be changed before retrying.
#[error("invalid configuration: {reason}")]
InvalidConfig { reason: String },
}