Skip to main content

irtt_client/
config.rs

1use std::{net::SocketAddr, time::Duration};
2
3/// Protocol compatibility bound for a requested `server_fill` value, in UTF-8
4/// bytes.
5///
6/// This is the maximum encoded server-fill string accepted by this client and
7/// protocol decoder. [`ClientConfig::server_fill`] enforces the same bound
8/// before opening a session.
9pub use irtt_proto::MAX_SERVER_FILL_BYTES;
10use irtt_proto::{Clock, ReceivedStats, StampAt};
11
12pub(crate) const DEFAULT_PORT: u16 = 2112;
13/// Largest valid DSCP codepoint accepted by client configuration.
14///
15/// DSCP is a six-bit value in the range `0..=63`. The client shifts it left
16/// by two bits into the raw IP TOS / Traffic Class byte carried by
17/// [`irtt_proto::Params::dscp`] and applied to the socket during the active
18/// test phase.
19pub const MAX_DSCP_CODEPOINT: u8 = 63;
20/// Largest IPv4 TTL or IPv6 hop-limit value accepted by client configuration.
21///
22/// This is the public user-configuration bound for
23/// [`SocketConfig::ttl`]. A value of zero is rejected separately because socket
24/// TTL and hop-limit settings are configured as `1..=255`.
25pub const MAX_TTL: u32 = 255;
26/// Largest UDP payload length accepted by client configuration, in bytes.
27///
28/// This is the maximum UDP payload size excluding IP and UDP headers. It caps
29/// [`ClientConfig::length`] before protocol packets are encoded or sent.
30pub const MAX_UDP_PAYLOAD_LENGTH: u32 = 65_507;
31pub(crate) const DEFAULT_DURATION: Duration = Duration::from_secs(3);
32pub(crate) const DEFAULT_INTERVAL: Duration = Duration::from_secs(1);
33pub(crate) const DEFAULT_OPEN_TIMEOUTS: [Duration; 4] = [
34    Duration::from_secs(1),
35    Duration::from_secs(2),
36    Duration::from_secs(4),
37    Duration::from_secs(8),
38];
39pub(crate) const MIN_OPEN_TIMEOUT: Duration = Duration::from_millis(200);
40pub(crate) const DEFAULT_PROBE_TIMEOUT: Duration = Duration::from_secs(4);
41pub(crate) const DEFAULT_MAX_PENDING: usize = 4096;
42
43/// Configuration for opening and running an IRTT client session.
44///
45/// This type describes both the protocol parameters sent in the IRTT open
46/// request and the local client behavior used to drive the UDP socket. Values
47/// that are negotiated by the server are available after opening the session
48/// through [`NegotiatedParams`](crate::NegotiatedParams).
49///
50/// # Example
51///
52/// Configure a finite run with larger probes and accept documented server
53/// restrictions. Use `duration: None` for continuous mode, or
54/// [`RunMode::NoTest`] for negotiation without probes.
55///
56/// ```
57/// use std::time::Duration;
58///
59/// use irtt_client::{ClientConfig, NegotiationPolicy};
60///
61/// let config = ClientConfig {
62///     server_addr: "example.net:2112".into(),
63///     duration: Some(Duration::from_secs(10)),
64///     interval: Duration::from_millis(250),
65///     length: 1200,
66///     negotiation_policy: NegotiationPolicy::Loose,
67///     ..ClientConfig::default()
68/// };
69///
70/// assert_eq!(config.duration, Some(Duration::from_secs(10)));
71/// assert_eq!(config.interval, Duration::from_millis(250));
72/// assert_eq!(config.length, 1200);
73/// ```
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct ClientConfig {
76    /// Remote server name or address.
77    ///
78    /// If no port is present, the default IRTT port 2112 is used. IPv6
79    /// literals may be supplied either bracketed or unbracketed when the
80    /// default port should be used.
81    pub server_addr: String,
82    /// Requested run duration.
83    ///
84    /// `Some(duration)` requests a finite test and must be greater than zero.
85    /// `None` requests continuous mode and is encoded on the wire as a zero
86    /// duration. Use [`RunMode::NoTest`] when the caller wants negotiation only
87    /// without sending probes.
88    pub duration: Option<Duration>,
89    /// Requested spacing between probe sends.
90    ///
91    /// The interval is encoded as nanoseconds in the open request and must be
92    /// greater than zero. The server may return a different interval depending
93    /// on the negotiated policy and server restrictions.
94    pub interval: Duration,
95    /// Requested echo packet payload length, in bytes.
96    ///
97    /// The value must fit within the UDP payload limit after protocol overhead.
98    /// A server may reduce the requested length during negotiation.
99    pub length: u32,
100    /// Requested server receive-statistics fields in echo replies.
101    pub received_stats: ReceivedStats,
102    /// Requested timestamp placement in echo replies.
103    pub stamp_at: StampAt,
104    /// Requested server clock sources for timestamp fields.
105    ///
106    /// [`Clock::Unspecified`] is the wire default meaning "no clock tag was
107    /// sent" and is not a client mode; opening a session with it is rejected.
108    /// Request [`Clock::Wall`], [`Clock::Monotonic`], or [`Clock::Both`].
109    pub clock: Clock,
110    /// Requested DSCP codepoint.
111    ///
112    /// This is the six-bit DSCP value, not the full traffic-class byte, and
113    /// must be less than or equal to [`MAX_DSCP_CODEPOINT`].
114    pub dscp: u8,
115    /// Optional HMAC key used to authenticate IRTT packets.
116    ///
117    /// When present, open, echo, and close packets are encoded and decoded with
118    /// HMAC authentication. The peer must be configured with the same key.
119    pub hmac_key: Option<Vec<u8>>,
120    /// Optional server payload fill request.
121    ///
122    /// `None` leaves server fill behavior unspecified. `Some(value)` requests
123    /// a non-empty server fill mode/value and must not exceed
124    /// [`MAX_SERVER_FILL_BYTES`] bytes when UTF-8 encoded.
125    pub server_fill: Option<String>,
126    /// Per-attempt receive timeouts used while opening the session.
127    ///
128    /// The client sends an open request for each entry until a valid open reply
129    /// is received. The list must not be empty, and each timeout must be at
130    /// least 200 ms.
131    pub open_timeouts: Vec<Duration>,
132    /// Whether opening the session should start a probe test or perform a
133    /// negotiation-only no-test exchange.
134    pub run_mode: RunMode,
135    /// Policy for server changes to negotiable protocol parameters.
136    pub negotiation_policy: NegotiationPolicy,
137    /// Local UDP socket configuration.
138    pub socket_config: SocketConfig,
139    /// Time after sending a probe before the client reports it as lost.
140    ///
141    /// This timeout is local client behavior; it is not negotiated with the
142    /// server. It must be greater than zero.
143    pub probe_timeout: Duration,
144    /// Maximum number of probes tracked as pending/timed-out/completed.
145    ///
146    /// This bounds memory used for reply classification and must be greater
147    /// than zero. A very small value can reject sends when replies are still
148    /// outstanding.
149    pub max_pending_probes: usize,
150}
151
152/// Authentication settings used by client sessions.
153///
154/// `ClientConfig` keeps its existing top-level HMAC field for source
155/// compatibility. Managed multi-target callers can use this smaller type for
156/// per-target auth overrides without carrying a full independent
157/// [`ClientConfig`].
158#[derive(Debug, Clone, PartialEq, Eq, Default)]
159pub struct ClientAuthConfig {
160    /// Optional HMAC key used to authenticate IRTT packets.
161    pub hmac_key: Option<Vec<u8>>,
162}
163
164impl From<Option<Vec<u8>>> for ClientAuthConfig {
165    fn from(hmac_key: Option<Vec<u8>>) -> Self {
166        Self { hmac_key }
167    }
168}
169
170impl From<ClientAuthConfig> for Option<Vec<u8>> {
171    fn from(auth: ClientAuthConfig) -> Self {
172        auth.hmac_key
173    }
174}
175
176impl Default for ClientConfig {
177    fn default() -> Self {
178        Self {
179            server_addr: format!("127.0.0.1:{DEFAULT_PORT}"),
180            duration: Some(DEFAULT_DURATION),
181            interval: DEFAULT_INTERVAL,
182            length: 0,
183            received_stats: ReceivedStats::Both,
184            stamp_at: StampAt::Both,
185            clock: Clock::Both,
186            dscp: 0,
187            hmac_key: None,
188            server_fill: None,
189            open_timeouts: DEFAULT_OPEN_TIMEOUTS.to_vec(),
190            run_mode: RunMode::Normal,
191            negotiation_policy: NegotiationPolicy::Strict,
192            socket_config: SocketConfig::default(),
193            probe_timeout: DEFAULT_PROBE_TIMEOUT,
194            max_pending_probes: DEFAULT_MAX_PENDING,
195        }
196    }
197}
198
199/// Local UDP socket options used by [`ClientConfig`].
200///
201/// These settings affect how the client binds, resolves, and receives from the
202/// socket. They do not change the IRTT protocol parameters negotiated with the
203/// server.
204///
205/// # Example
206///
207/// Bind to a chosen local endpoint and set the outgoing IPv4 TTL:
208///
209/// ```
210/// use std::net::{Ipv4Addr, SocketAddr};
211///
212/// use irtt_client::{ClientConfig, SocketConfig};
213///
214/// let config = ClientConfig {
215///     socket_config: SocketConfig {
216///         bind_addr: Some(SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))),
217///         ttl: Some(32),
218///         ipv4_only: true,
219///         ..SocketConfig::default()
220///     },
221///     ..ClientConfig::default()
222/// };
223///
224/// assert_eq!(config.socket_config.ttl, Some(32));
225/// assert!(config.socket_config.ipv4_only);
226/// ```
227#[derive(Debug, Clone, PartialEq, Eq, Default)]
228pub struct SocketConfig {
229    /// Local address to bind before connecting the UDP socket.
230    ///
231    /// `None` binds to an unspecified address with an ephemeral port matching
232    /// the selected remote address family.
233    pub bind_addr: Option<SocketAddr>,
234    /// Optional IPv4 TTL or IPv6 hop limit applied to sent packets.
235    ///
236    /// `None` leaves the platform default unchanged. Values must fit in the
237    /// platform socket option range; [`MAX_TTL`] is the public configuration
238    /// bound used by this crate.
239    pub ttl: Option<u32>,
240    /// Restrict name resolution to IPv4 addresses.
241    pub ipv4_only: bool,
242    /// Restrict name resolution to IPv6 addresses and set IPV6_V6ONLY for IPv6
243    /// sockets.
244    pub ipv6_only: bool,
245    /// Socket read timeout used after the session is open.
246    ///
247    /// `None` leaves reads blocking for APIs that perform a single receive.
248    /// Managed sessions may replace `None` or long timeouts with a short
249    /// timeout so cooperative cancellation can be observed promptly.
250    pub recv_timeout: Option<Duration>,
251}
252
253/// How strictly to handle server-side negotiation restrictions.
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum NegotiationPolicy {
256    /// Reject any negotiated parameter that is more restrictive or different
257    /// than requested.
258    Strict,
259    /// Accept documented server restrictions and report them in
260    /// [`NegotiatedParams`](crate::NegotiatedParams).
261    Loose,
262}
263
264/// Mode requested during the IRTT open exchange.
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum RunMode {
267    /// Open a session and send echo probes for the negotiated run duration.
268    Normal,
269    /// Complete negotiation without running the echo probe test.
270    NoTest,
271}
272
273/// Bound for a single receive-drain operation.
274///
275/// This is used by lower-level callers that drive [`Client`](crate::Client)
276/// directly and want to cap how many datagrams are processed before returning
277/// to their own event loop.
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub struct RecvBudget {
280    /// Maximum number of datagrams to process before returning.
281    pub max_packets: usize,
282}
283
284impl Default for RecvBudget {
285    fn default() -> Self {
286        Self { max_packets: 64 }
287    }
288}