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}