Skip to main content

netconf_rust/
config.rs

1use std::path::PathBuf;
2use std::time::Duration;
3
4use crate::codec::{CodecConfig, LenientChunkedFraming};
5
6/// Default SSH flow-control window size (2 MB, the russh default).
7///
8/// Controls how much data the remote side can send before waiting for a
9/// window adjustment. Larger values improve throughput for big NETCONF
10/// responses at the cost of higher memory usage.
11pub const DEFAULT_WINDOW_SIZE: u32 = 2_097_152;
12
13/// Default maximum SSH packet size (32 KB, the russh default).
14///
15/// Each SSH data packet is at most this many bytes. Larger values reduce
16/// per-packet overhead but increase the minimum buffer allocation.
17pub const DEFAULT_MAXIMUM_PACKET_SIZE: u32 = 32_768;
18
19/// Default capacity of the internal channel used by [`Session::rpc_stream()`](crate::Session::rpc_stream).
20///
21/// This controls how many chunks the background reader can buffer before
22/// applying backpressure. Higher values smooth out bursty reads at the
23/// cost of memory (each buffered chunk is one SSH packet worth of data).
24pub const DEFAULT_STREAM_BUFFER_CAPACITY: usize = 32;
25
26/// Host key verification mode, mirroring OpenSSH's `StrictHostKeyChecking`.
27#[derive(Debug, Clone, Default)]
28pub enum HostKeyVerification {
29    /// Reject connections to hosts not already in `known_hosts`.
30    Strict,
31    /// Accept and learn unknown host keys; reject changed keys (OpenSSH default).
32    #[default]
33    AcceptNew,
34    /// Accept all host keys without verification (insecure).
35    Disabled,
36}
37
38/// SSH transport settings exposed to users.
39///
40/// These map to the corresponding fields in `russh::client::Config`.
41/// Fields left as `None` use the russh defaults.
42#[derive(Debug, Clone)]
43pub(crate) struct SshConfig {
44    /// Time after which the connection is garbage-collected.
45    pub inactivity_timeout: Option<Duration>,
46    /// If nothing is received from the server for this amount of time, send a keepalive message.
47    pub keepalive_interval: Option<Duration>,
48    /// If this many keepalives have been sent without reply, close the connection.
49    pub keepalive_max: Option<usize>,
50    /// Enable `TCP_NODELAY` (disable Nagle's algorithm).
51    pub nodelay: Option<bool>,
52    /// SSH flow-control window size.
53    pub window_size: Option<u32>,
54    /// Maximum SSH packet size.
55    pub maximum_packet_size: Option<u32>,
56    /// Host key verification mode.
57    pub host_key_verification: HostKeyVerification,
58    /// Custom path to `known_hosts` file. `None` uses `~/.ssh/known_hosts`.
59    pub known_hosts_path: Option<PathBuf>,
60}
61
62#[derive(Debug, Clone)]
63pub(crate) struct JumphostConfig {
64    pub host: String,
65    pub port: u16,
66    pub username: String,
67    pub password: String,
68}
69
70impl Default for SshConfig {
71    fn default() -> Self {
72        Self {
73            inactivity_timeout: None,
74            keepalive_interval: Some(Duration::from_secs(10)),
75            keepalive_max: Some(3),
76            nodelay: None,
77            window_size: None,
78            maximum_packet_size: None,
79            host_key_verification: HostKeyVerification::AcceptNew,
80            known_hosts_path: None,
81        }
82    }
83}
84
85/// Configuration for a NETCONF session.
86///
87/// Use [`Config::builder()`] for a fluent API, or [`Config::default()`]
88/// for sensible defaults.
89///
90/// # Example
91///
92/// ```
93/// use std::time::Duration;
94/// use netconf_rust::Config;
95///
96/// let config = Config::builder()
97///     .hello_timeout(Duration::from_secs(10))
98///     .connect_timeout(Duration::from_secs(5))
99///     .nodelay(true)
100///     .build();
101/// ```
102#[derive(Debug, Clone)]
103pub struct Config {
104    pub(crate) codec: CodecConfig,
105    pub(crate) connect_timeout: Option<Duration>,
106    pub(crate) hello_timeout: Option<Duration>,
107    pub(crate) rpc_timeout: Option<Duration>,
108    pub(crate) ssh: SshConfig,
109    pub(crate) jumphost: Option<JumphostConfig>,
110    pub(crate) stream_buffer_capacity: usize,
111}
112
113impl Default for Config {
114    fn default() -> Self {
115        Self {
116            codec: CodecConfig::default(),
117            connect_timeout: None,
118            hello_timeout: Some(Duration::from_secs(30)),
119            rpc_timeout: None,
120            ssh: SshConfig::default(),
121            jumphost: None,
122            stream_buffer_capacity: DEFAULT_STREAM_BUFFER_CAPACITY,
123        }
124    }
125}
126
127impl Config {
128    pub fn builder() -> ConfigBuilder {
129        ConfigBuilder {
130            config: Config::default(),
131        }
132    }
133
134    /// Maximum message size limit, or `None` for unlimited.
135    pub fn max_message_size(&self) -> Option<usize> {
136        self.codec.max_message_size
137    }
138
139    /// Timeout for the TCP/SSH connect phase.
140    pub fn connect_timeout(&self) -> Option<Duration> {
141        self.connect_timeout
142    }
143
144    /// Timeout for the NETCONF hello exchange.
145    pub fn hello_timeout(&self) -> Option<Duration> {
146        self.hello_timeout
147    }
148
149    /// Timeout for individual RPC operations.
150    pub fn rpc_timeout(&self) -> Option<Duration> {
151        self.rpc_timeout
152    }
153
154    /// SSH flow-control window size.
155    ///
156    /// Returns the configured value, or [`DEFAULT_WINDOW_SIZE`] if not set.
157    pub fn window_size(&self) -> u32 {
158        self.ssh.window_size.unwrap_or(DEFAULT_WINDOW_SIZE)
159    }
160
161    /// Maximum SSH packet size.
162    ///
163    /// Returns the configured value, or [`DEFAULT_MAXIMUM_PACKET_SIZE`] if not set.
164    pub fn maximum_packet_size(&self) -> u32 {
165        self.ssh
166            .maximum_packet_size
167            .unwrap_or(DEFAULT_MAXIMUM_PACKET_SIZE)
168    }
169
170    /// Capacity of the internal channel used by streaming RPCs.
171    pub fn stream_buffer_capacity(&self) -> usize {
172        self.stream_buffer_capacity
173    }
174
175    /// Lenient chunked framing recovery mode.
176    pub fn lenient_chunked_framing(&self) -> LenientChunkedFraming {
177        self.codec.lenient_chunked_framing
178    }
179}
180
181#[derive(Debug, Clone)]
182pub struct ConfigBuilder {
183    config: Config,
184}
185
186impl ConfigBuilder {
187    /// Set the maximum NETCONF message size. `None` means unlimited.
188    pub fn max_message_size(mut self, size: usize) -> Self {
189        self.config.codec.max_message_size = Some(size);
190        self
191    }
192
193    /// Set the timeout for the TCP/SSH connect phase.
194    ///
195    /// When set, [`Session::connect_with_config`](crate::Session::connect_with_config)
196    /// will fail with [`TransportError::Timeout`](crate::error::TransportError::Timeout)
197    /// if the SSH connection is not established within this duration.
198    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
199        self.config.connect_timeout = Some(timeout);
200        self
201    }
202
203    /// Set the timeout for the NETCONF hello exchange.
204    ///
205    /// Some vendors (e.g. Nokia SR OS) can silently stall during the hello
206    /// exchange — for example, if the client hello contains an invalid
207    /// namespace, the server waits indefinitely for a valid hello without
208    /// returning an error. Without a timeout, the connection hangs forever.
209    ///
210    /// When set, the hello exchange will fail with
211    /// [`TransportError::Timeout`](crate::error::TransportError::Timeout)
212    /// if the server does not complete the hello within this duration.
213    pub fn hello_timeout(mut self, timeout: Duration) -> Self {
214        self.config.hello_timeout = Some(timeout);
215        self
216    }
217
218    /// Set the timeout for individual RPC operations (send + wait for reply).
219    ///
220    /// When set, [`RpcFuture::response()`](crate::RpcFuture::response) will fail
221    /// with [`TransportError::Timeout`](crate::error::TransportError::Timeout)
222    /// if the server does not reply within this duration.
223    pub fn rpc_timeout(mut self, timeout: Duration) -> Self {
224        self.config.rpc_timeout = Some(timeout);
225        self
226    }
227
228    /// Set the SSH inactivity timeout (garbage-collect idle connections).
229    pub fn inactivity_timeout(mut self, timeout: Duration) -> Self {
230        self.config.ssh.inactivity_timeout = Some(timeout);
231        self
232    }
233
234    /// Set the SSH keepalive interval (detect dead peers).
235    pub fn keepalive_interval(mut self, interval: Duration) -> Self {
236        self.config.ssh.keepalive_interval = Some(interval);
237        self
238    }
239
240    /// Set the maximum number of missed keepalives before disconnect.
241    pub fn keepalive_max(mut self, max: usize) -> Self {
242        self.config.ssh.keepalive_max = Some(max);
243        self
244    }
245
246    /// Enable or disable `TCP_NODELAY` on the SSH socket.
247    pub fn nodelay(mut self, nodelay: bool) -> Self {
248        self.config.ssh.nodelay = Some(nodelay);
249        self
250    }
251
252    /// Set the SSH flow-control window size.
253    ///
254    /// Controls how much data the remote side can send before waiting for a
255    /// window adjustment. Larger values improve throughput for big NETCONF
256    /// responses at the cost of higher memory usage.
257    ///
258    /// Default: [`DEFAULT_WINDOW_SIZE`] (2 MB).
259    pub fn window_size(mut self, size: u32) -> Self {
260        self.config.ssh.window_size = Some(size);
261        self
262    }
263
264    /// Set the maximum SSH packet size.
265    ///
266    /// Each SSH data packet is at most this many bytes. Larger values reduce
267    /// per-packet overhead but increase the minimum buffer allocation.
268    ///
269    /// Default: [`DEFAULT_MAXIMUM_PACKET_SIZE`] (32 KB).
270    pub fn maximum_packet_size(mut self, size: u32) -> Self {
271        self.config.ssh.maximum_packet_size = Some(size);
272        self
273    }
274
275    /// Set the capacity of the internal channel used by
276    /// [`Session::rpc_stream()`](crate::Session::rpc_stream).
277    ///
278    /// This controls how many chunks the background reader can buffer before
279    /// applying backpressure. Higher values smooth out bursty reads at the
280    /// cost of memory.
281    ///
282    /// Default: [`DEFAULT_STREAM_BUFFER_CAPACITY`] (32).
283    ///
284    /// # Panics
285    ///
286    /// Panics if `capacity` is 0.
287    pub fn stream_buffer_capacity(mut self, capacity: usize) -> Self {
288        assert!(
289            capacity > 0,
290            "stream_buffer_capacity must be greater than 0"
291        );
292        self.config.stream_buffer_capacity = capacity;
293        self
294    }
295
296    /// Enable lenient chunked framing recovery.
297    ///
298    /// When set to a value other than [`LenientChunkedFraming::Off`], the
299    /// decoder tolerates incorrect chunk sizes from routers that mis-report
300    /// the byte count in RFC 6242 chunk headers.
301    pub fn lenient_chunked_framing(mut self, mode: LenientChunkedFraming) -> Self {
302        self.config.codec.lenient_chunked_framing = mode;
303        self
304    }
305
306    /// Set the host key verification mode.
307    pub fn host_key_verification(mut self, mode: HostKeyVerification) -> Self {
308        self.config.ssh.host_key_verification = mode;
309        self
310    }
311
312    /// Set a custom path to the `known_hosts` file.
313    pub fn known_hosts_path(mut self, path: impl Into<PathBuf>) -> Self {
314        self.config.ssh.known_hosts_path = Some(path.into());
315        self
316    }
317
318    /// Disable host key verification entirely (insecure — use only for testing).
319    pub fn danger_disable_host_key_verification(mut self) -> Self {
320        self.config.ssh.host_key_verification = HostKeyVerification::Disabled;
321        self
322    }
323
324    /// Route the connection through an SSH jumphost (bastion host).
325    ///
326    /// The library will first SSH into the jumphost, open a `direct-tcpip`
327    /// channel to the target device, then establish the NETCONF SSH session
328    /// over that tunnel. The jumphost inherits all SSH settings
329    /// (host key verification, known_hosts, keepalive, etc.) from this config.
330    pub fn jumphost(
331        mut self,
332        host: impl Into<String>,
333        port: u16,
334        username: impl Into<String>,
335        password: impl Into<String>,
336    ) -> Self {
337        self.config.jumphost = Some(JumphostConfig {
338            host: host.into(),
339            port,
340            username: username.into(),
341            password: password.into(),
342        });
343        self
344    }
345
346    pub fn build(self) -> Config {
347        self.config
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn default_getters_return_expected_values() {
357        let config = Config::default();
358        assert_eq!(config.window_size(), DEFAULT_WINDOW_SIZE);
359        assert_eq!(config.maximum_packet_size(), DEFAULT_MAXIMUM_PACKET_SIZE);
360        assert_eq!(
361            config.stream_buffer_capacity(),
362            DEFAULT_STREAM_BUFFER_CAPACITY
363        );
364    }
365
366    #[test]
367    fn builder_overrides_are_reflected_in_getters() {
368        let config = Config::builder()
369            .window_size(1_000_000)
370            .maximum_packet_size(16_384)
371            .stream_buffer_capacity(64)
372            .build();
373        assert_eq!(config.window_size(), 1_000_000);
374        assert_eq!(config.maximum_packet_size(), 16_384);
375        assert_eq!(config.stream_buffer_capacity(), 64);
376    }
377
378    #[test]
379    #[should_panic(expected = "stream_buffer_capacity must be greater than 0")]
380    fn stream_buffer_capacity_zero_panics() {
381        Config::builder().stream_buffer_capacity(0);
382    }
383}