quic_reverse/
config.rs

1// Copyright 2024-2026 Farlight Networks, LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Session configuration.
16
17use quic_reverse_control::Features;
18use std::time::Duration;
19
20/// Configuration for a quic-reverse session.
21#[derive(Debug, Clone)]
22pub struct Config {
23    /// Protocol versions supported by this session, in preference order.
24    ///
25    /// The highest mutually supported version will be selected during negotiation.
26    pub supported_versions: Vec<u16>,
27
28    /// Feature flags to advertise during negotiation.
29    ///
30    /// The intersection of both peers' features will be used.
31    pub features: Features,
32
33    /// Timeout for open requests awaiting a response.
34    ///
35    /// If no `OpenResponse` is received within this duration, the request fails.
36    pub open_timeout: Duration,
37
38    /// Timeout for binding a stream after receiving an accepted `OpenResponse`.
39    ///
40    /// If no stream with the matching logical stream ID arrives within this
41    /// duration, the request fails.
42    pub stream_bind_timeout: Duration,
43
44    /// Timeout for the entire negotiation handshake.
45    ///
46    /// If negotiation doesn't complete within this duration, the session fails.
47    pub negotiation_timeout: Duration,
48
49    /// Maximum number of pending open requests.
50    ///
51    /// Attempts to open more streams than this limit will block until
52    /// existing requests complete.
53    pub max_inflight_opens: usize,
54
55    /// Maximum number of concurrent active streams.
56    ///
57    /// New open requests will be rejected if this limit is reached.
58    pub max_concurrent_streams: usize,
59
60    /// Optional ping interval for keep-alive.
61    ///
62    /// If set, `Ping` messages will be sent at this interval when no
63    /// other traffic is occurring. Requires the `PING_PONG` feature.
64    pub ping_interval: Option<Duration>,
65
66    /// Timeout for ping responses.
67    ///
68    /// If a `Pong` is not received within this duration after sending a `Ping`,
69    /// the ping is considered failed. Defaults to 10 seconds.
70    pub ping_timeout: Duration,
71
72    /// Agent identifier sent in `Hello` messages.
73    ///
74    /// This is optional and used for debugging and diagnostics.
75    pub agent: Option<String>,
76}
77
78impl Default for Config {
79    fn default() -> Self {
80        Self {
81            supported_versions: vec![1],
82            features: Features::empty(),
83            open_timeout: Duration::from_secs(30),
84            stream_bind_timeout: Duration::from_secs(10),
85            negotiation_timeout: Duration::from_secs(30),
86            max_inflight_opens: 100,
87            max_concurrent_streams: 1000,
88            ping_interval: None,
89            ping_timeout: Duration::from_secs(10),
90            agent: None,
91        }
92    }
93}
94
95impl Config {
96    /// Creates a new configuration with default values.
97    #[must_use]
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Sets the supported protocol versions.
103    #[must_use]
104    pub fn with_versions(mut self, versions: Vec<u16>) -> Self {
105        self.supported_versions = versions;
106        self
107    }
108
109    /// Sets the feature flags.
110    #[must_use]
111    pub const fn with_features(mut self, features: Features) -> Self {
112        self.features = features;
113        self
114    }
115
116    /// Sets the open request timeout.
117    #[must_use]
118    pub const fn with_open_timeout(mut self, timeout: Duration) -> Self {
119        self.open_timeout = timeout;
120        self
121    }
122
123    /// Sets the stream binding timeout.
124    #[must_use]
125    pub const fn with_stream_bind_timeout(mut self, timeout: Duration) -> Self {
126        self.stream_bind_timeout = timeout;
127        self
128    }
129
130    /// Sets the negotiation timeout.
131    #[must_use]
132    pub const fn with_negotiation_timeout(mut self, timeout: Duration) -> Self {
133        self.negotiation_timeout = timeout;
134        self
135    }
136
137    /// Sets the maximum number of inflight open requests.
138    #[must_use]
139    pub const fn with_max_inflight_opens(mut self, max: usize) -> Self {
140        self.max_inflight_opens = max;
141        self
142    }
143
144    /// Sets the maximum number of concurrent streams.
145    #[must_use]
146    pub const fn with_max_concurrent_streams(mut self, max: usize) -> Self {
147        self.max_concurrent_streams = max;
148        self
149    }
150
151    /// Sets the ping interval for keep-alive.
152    #[must_use]
153    pub fn with_ping_interval(mut self, interval: Duration) -> Self {
154        self.ping_interval = Some(interval);
155        self.features |= Features::PING_PONG;
156        self
157    }
158
159    /// Sets the ping timeout.
160    #[must_use]
161    pub const fn with_ping_timeout(mut self, timeout: Duration) -> Self {
162        self.ping_timeout = timeout;
163        self
164    }
165
166    /// Sets the agent identifier.
167    #[must_use]
168    pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
169        self.agent = Some(agent.into());
170        self
171    }
172
173    /// Validates the configuration.
174    ///
175    /// # Errors
176    ///
177    /// Returns an error if the configuration is invalid.
178    pub fn validate(&self) -> Result<(), ConfigError> {
179        if self.supported_versions.is_empty() {
180            return Err(ConfigError::NoSupportedVersions);
181        }
182
183        if self.max_inflight_opens == 0 {
184            return Err(ConfigError::InvalidLimit("max_inflight_opens must be > 0"));
185        }
186
187        if self.max_concurrent_streams == 0 {
188            return Err(ConfigError::InvalidLimit(
189                "max_concurrent_streams must be > 0",
190            ));
191        }
192
193        Ok(())
194    }
195}
196
197/// Configuration validation errors.
198#[derive(Debug, thiserror::Error)]
199pub enum ConfigError {
200    /// No supported protocol versions specified.
201    #[error("at least one protocol version must be supported")]
202    NoSupportedVersions,
203
204    /// Invalid limit value.
205    #[error("invalid limit: {0}")]
206    InvalidLimit(&'static str),
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn default_config_is_valid() {
215        let config = Config::default();
216        assert!(config.validate().is_ok());
217    }
218
219    #[test]
220    fn config_builder() {
221        let config = Config::new()
222            .with_versions(vec![1, 2])
223            .with_features(Features::STRUCTURED_METADATA)
224            .with_open_timeout(Duration::from_secs(60))
225            .with_max_concurrent_streams(500)
226            .with_agent("test/1.0");
227
228        assert_eq!(config.supported_versions, vec![1, 2]);
229        assert!(config.features.contains(Features::STRUCTURED_METADATA));
230        assert_eq!(config.open_timeout, Duration::from_secs(60));
231        assert_eq!(config.max_concurrent_streams, 500);
232        assert_eq!(config.agent.as_deref(), Some("test/1.0"));
233    }
234
235    #[test]
236    fn ping_interval_enables_feature() {
237        let config = Config::new().with_ping_interval(Duration::from_secs(30));
238        assert!(config.features.contains(Features::PING_PONG));
239    }
240
241    #[test]
242    fn empty_versions_is_invalid() {
243        let config = Config::new().with_versions(vec![]);
244        assert!(matches!(
245            config.validate(),
246            Err(ConfigError::NoSupportedVersions)
247        ));
248    }
249
250    #[test]
251    fn zero_limits_are_invalid() {
252        let config = Config::new().with_max_inflight_opens(0);
253        assert!(matches!(
254            config.validate(),
255            Err(ConfigError::InvalidLimit(_))
256        ));
257
258        let config = Config::new().with_max_concurrent_streams(0);
259        assert!(matches!(
260            config.validate(),
261            Err(ConfigError::InvalidLimit(_))
262        ));
263    }
264}