Skip to main content

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}