irtt_client/config.rs
1use std::{net::SocketAddr, time::Duration};
2
3use crate::{Authentication, ClientError};
4
5/// Protocol compatibility bound for a requested `server_fill` value, in UTF-8
6/// bytes.
7///
8/// This is the maximum encoded server-fill string accepted by this client and
9/// protocol decoder. [`SessionRequest::server_fill`] enforces the same bound
10/// before opening a session.
11pub use irtt_proto::MAX_SERVER_FILL_BYTES;
12use irtt_proto::{Clock, ReceivedStats, StampAt};
13
14pub(crate) const DEFAULT_PORT: u16 = 2112;
15/// Largest valid DSCP codepoint accepted by client configuration.
16///
17/// DSCP is a six-bit value in the range `0..=63`. The client shifts it left
18/// by two bits into the raw IP TOS / Traffic Class byte carried by
19/// [`irtt_proto::Params::dscp`] and applied to the socket during the active
20/// test phase.
21pub const MAX_DSCP_CODEPOINT: u8 = 63;
22/// Largest IPv4 TTL or IPv6 hop-limit value accepted by client configuration.
23///
24/// This is the public user-configuration bound for
25/// [`SocketConfig::ttl`]. A value of zero is rejected separately because socket
26/// TTL and hop-limit settings are configured as `1..=255`.
27pub const MAX_TTL: u32 = 255;
28/// Largest UDP payload length accepted by client configuration, in bytes.
29///
30/// This is the maximum UDP payload size excluding IP and UDP headers. It caps
31/// [`SessionRequest::length`] before protocol packets are encoded or sent.
32pub const MAX_UDP_PAYLOAD_LENGTH: u32 = 65_507;
33pub(crate) const DEFAULT_DURATION: Duration = Duration::from_secs(3);
34pub(crate) const DEFAULT_INTERVAL: Duration = Duration::from_secs(1);
35pub(crate) const DEFAULT_OPEN_TIMEOUTS: [Duration; 4] = [
36 Duration::from_secs(1),
37 Duration::from_secs(2),
38 Duration::from_secs(4),
39 Duration::from_secs(8),
40];
41pub(crate) const MIN_OPEN_TIMEOUT: Duration = Duration::from_millis(200);
42pub(crate) const DEFAULT_PROBE_TIMEOUT: Duration = Duration::from_secs(4);
43pub(crate) const DEFAULT_MAX_PENDING: usize = 4096;
44
45/// Configuration for opening and running an IRTT client session.
46///
47/// This type describes both the protocol parameters sent in the IRTT open
48/// request and the local client behavior used to drive the UDP socket. Values
49/// that are negotiated by the server are available after opening the session
50/// through [`NegotiationResult`](crate::NegotiationResult).
51///
52/// # Example
53///
54/// Configure a finite run with larger probes and accept documented server
55/// restrictions. Use `duration: None` for continuous mode, or
56/// [`RunMode::NoTest`] for negotiation without probes.
57///
58/// ```
59/// use std::time::Duration;
60///
61/// use irtt_client::{ClientConfig, NegotiationPolicy, OpenPolicy, SessionRequest};
62///
63/// let config = ClientConfig {
64/// request: SessionRequest {
65/// duration: Some(Duration::from_secs(10)),
66/// interval: Duration::from_millis(250),
67/// length: 1200,
68/// ..SessionRequest::default()
69/// },
70/// open: OpenPolicy {
71/// negotiation: NegotiationPolicy::Loose,
72/// ..OpenPolicy::default()
73/// },
74/// ..ClientConfig::default()
75/// };
76///
77/// assert_eq!(config.request.duration, Some(Duration::from_secs(10)));
78/// assert_eq!(config.request.interval, Duration::from_millis(250));
79/// assert_eq!(config.request.length, 1200);
80/// ```
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct ClientConfig {
83 /// Remote address families allowed during resolution and socket creation.
84 pub address_family: AddressFamily,
85 /// Local UDP socket properties.
86 pub socket: SocketConfig,
87 /// Protocol and session values requested from the server.
88 pub request: SessionRequest,
89 /// Open retry timeouts and negotiation policy.
90 pub open: OpenPolicy,
91 /// Concrete authentication for open, echo, and close packets.
92 ///
93 /// HMAC peers must use the same key. An empty HMAC key remains authenticated.
94 pub auth: Authentication,
95 /// Time after sending a probe before the client reports it as lost.
96 ///
97 /// This timeout is local client behavior; it is not negotiated with the
98 /// server. It must be greater than zero.
99 pub probe_timeout: Duration,
100 /// Maximum number of probes tracked as pending/timed-out/completed.
101 ///
102 /// This bounds memory used for reply classification and must be greater
103 /// than zero. A very small value can reject sends when replies are still
104 /// outstanding.
105 pub max_pending_probes: usize,
106}
107
108impl Default for ClientConfig {
109 fn default() -> Self {
110 Self {
111 address_family: AddressFamily::default(),
112 socket: SocketConfig::default(),
113 request: SessionRequest::default(),
114 open: OpenPolicy::default(),
115 auth: Authentication::Unauthenticated,
116 probe_timeout: DEFAULT_PROBE_TIMEOUT,
117 max_pending_probes: DEFAULT_MAX_PENDING,
118 }
119 }
120}
121
122/// Allowed remote address families. IPv6-only selection also sets IPV6_V6ONLY.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
124pub enum AddressFamily {
125 /// Accept IPv4 and IPv6 addresses.
126 #[default]
127 Any,
128 /// Accept only IPv4 addresses.
129 Ipv4,
130 /// Accept only IPv6 addresses and use IPv6-only sockets.
131 Ipv6,
132}
133
134/// Requested IRTT protocol and session semantics, reusable across endpoints.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct SessionRequest {
137 /// Requested run duration.
138 ///
139 /// `Some(duration)` requests a finite test and must be greater than zero.
140 /// `None` requests continuous mode and is encoded on the wire as a zero
141 /// duration. Use [`RunMode::NoTest`] when the caller wants negotiation only
142 /// without sending probes.
143 ///
144 /// The managed driver stops sending after the negotiated duration.
145 /// Low-level adapters leave run duration to the caller.
146 pub duration: Option<Duration>,
147 /// Requested spacing between probe sends.
148 ///
149 /// The interval is encoded as nanoseconds in the open request and must be
150 /// greater than zero. The server may return a different interval depending
151 /// on the negotiated policy and server restrictions.
152 ///
153 /// The managed driver uses the negotiated interval for cadence.
154 /// Low-level adapters send whenever called.
155 pub interval: Duration,
156 /// Requested echo packet payload length, in bytes.
157 ///
158 /// The value must fit within the UDP payload limit after protocol overhead.
159 /// A server may reduce the requested length during negotiation.
160 pub length: u32,
161 /// Requested server receive-statistics fields in echo replies.
162 pub received_stats: ReceivedStats,
163 /// Requested timestamp placement in echo replies.
164 pub stamp_at: StampAt,
165 /// Requested server clock sources for timestamp fields.
166 ///
167 /// [`Clock::Unspecified`] is the wire default meaning "no clock tag was
168 /// sent" and is not a client mode; opening a session with it is rejected.
169 /// Request [`Clock::Wall`], [`Clock::Monotonic`], or [`Clock::Both`].
170 pub clock: Clock,
171 /// Requested DSCP codepoint.
172 ///
173 /// This is the six-bit DSCP value, not the full traffic-class byte, and
174 /// must be less than or equal to [`MAX_DSCP_CODEPOINT`].
175 pub dscp: u8,
176 /// Optional server payload fill request.
177 ///
178 /// `None` leaves server fill behavior unspecified. `Some(value)` requests
179 /// a non-empty server fill mode/value and must not exceed
180 /// [`MAX_SERVER_FILL_BYTES`] bytes when UTF-8 encoded.
181 pub server_fill: Option<String>,
182 /// Whether opening the session should start a probe test or perform a
183 /// negotiation-only no-test exchange.
184 pub run_mode: RunMode,
185}
186
187impl Default for SessionRequest {
188 fn default() -> Self {
189 Self {
190 duration: Some(DEFAULT_DURATION),
191 interval: DEFAULT_INTERVAL,
192 length: 0,
193 received_stats: ReceivedStats::Both,
194 stamp_at: StampAt::Both,
195 clock: Clock::Both,
196 dscp: 0,
197 server_fill: None,
198 run_mode: RunMode::Normal,
199 }
200 }
201}
202
203/// Opening behavior shared by blocking, async, and managed clients.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct OpenPolicy {
206 /// Per-attempt receive timeouts used while opening the session.
207 ///
208 /// The client sends an open request for each entry until a valid open reply
209 /// is received. The list must not be empty, and each timeout must be at
210 /// least 200 ms.
211 pub timeouts: Vec<Duration>,
212 /// Policy for server changes to negotiable protocol parameters.
213 pub negotiation: NegotiationPolicy,
214}
215
216impl OpenPolicy {
217 pub(crate) fn validate(&self) -> Result<(), ClientError> {
218 if self.timeouts.is_empty() {
219 return Err(ClientError::NoOpenTimeouts);
220 }
221 for timeout in &self.timeouts {
222 if *timeout < MIN_OPEN_TIMEOUT {
223 return Err(ClientError::OpenTimeoutTooSmall {
224 timeout: *timeout,
225 minimum: MIN_OPEN_TIMEOUT,
226 });
227 }
228 }
229 Ok(())
230 }
231}
232
233impl Default for OpenPolicy {
234 fn default() -> Self {
235 Self {
236 timeouts: DEFAULT_OPEN_TIMEOUTS.to_vec(),
237 negotiation: NegotiationPolicy::Strict,
238 }
239 }
240}
241
242/// Local UDP socket options used by [`ClientConfig`].
243///
244/// These settings affect how the client binds and sends from the socket. They
245/// do not change the IRTT protocol parameters negotiated with the server.
246///
247/// # Example
248///
249/// Bind to a chosen local endpoint and set the outgoing IPv4 TTL:
250///
251/// ```
252/// use std::net::{Ipv4Addr, SocketAddr};
253///
254/// use irtt_client::{AddressFamily, ClientConfig, SocketConfig};
255///
256/// let config = ClientConfig {
257/// address_family: AddressFamily::Ipv4,
258/// socket: SocketConfig {
259/// bind_addr: Some(SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))),
260/// ttl: Some(32),
261/// ..SocketConfig::default()
262/// },
263/// ..ClientConfig::default()
264/// };
265///
266/// assert_eq!(config.socket.ttl, Some(32));
267/// assert_eq!(config.address_family, AddressFamily::Ipv4);
268/// ```
269#[derive(Debug, Clone, PartialEq, Eq, Default)]
270pub struct SocketConfig {
271 /// Local address to bind before connecting the UDP socket.
272 ///
273 /// `None` binds to an unspecified address with an ephemeral port matching
274 /// the selected remote address family.
275 pub bind_addr: Option<SocketAddr>,
276 /// Network interface to bind this socket to (`SO_BINDTODEVICE`).
277 #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
278 pub bind_to_device: Option<String>,
279 /// Firewall mark applied to outgoing packets (`SO_MARK`).
280 #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
281 pub mark: Option<u32>,
282 /// FreeBSD FIB used for routing this socket (`SO_SETFIB`).
283 #[cfg(target_os = "freebsd")]
284 pub fib: Option<u32>,
285 /// Optional IPv4 TTL or IPv6 hop limit applied to sent packets.
286 ///
287 /// `None` leaves the platform default unchanged. Values must fit in the
288 /// platform socket option range; [`MAX_TTL`] is the public configuration
289 /// bound used by this crate.
290 pub ttl: Option<u32>,
291}
292
293/// How strictly to handle server-side negotiation restrictions.
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295pub enum NegotiationPolicy {
296 /// Reject any negotiated parameter that is more restrictive or different
297 /// than requested.
298 Strict,
299 /// Accept documented server restrictions and report them in
300 /// [`NegotiationResult`](crate::NegotiationResult).
301 Loose,
302}
303
304/// Mode requested during the IRTT open exchange.
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub enum RunMode {
307 /// Open a probe session. The managed driver runs for the negotiated duration;
308 /// low-level adapters leave sending and stopping to the caller.
309 Normal,
310 /// Complete negotiation without running the echo probe test.
311 NoTest,
312}
313
314/// Bound for a single receive-drain operation.
315///
316/// This is used by lower-level callers that drive [`Client`](crate::Client)
317/// directly and want to cap how many datagrams are processed before returning
318/// to their own event loop.
319#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub struct RecvBudget {
321 /// Maximum number of datagrams to process before returning.
322 pub max_packets: usize,
323}
324
325impl Default for RecvBudget {
326 fn default() -> Self {
327 Self { max_packets: 64 }
328 }
329}