netconf_rust/config.rs
1use std::path::PathBuf;
2use std::time::Duration;
3
4use crate::codec::CodecConfig;
5
6/// Host key verification mode, mirroring OpenSSH's `StrictHostKeyChecking`.
7#[derive(Debug, Clone, Default)]
8pub enum HostKeyVerification {
9 /// Reject connections to hosts not already in `known_hosts`.
10 Strict,
11 /// Accept and learn unknown host keys; reject changed keys (OpenSSH default).
12 #[default]
13 AcceptNew,
14 /// Accept all host keys without verification (insecure).
15 Disabled,
16}
17
18/// SSH transport settings exposed to users.
19///
20/// These map to the corresponding fields in `russh::client::Config`.
21/// Fields left as `None` use the russh defaults.
22#[derive(Debug, Clone)]
23pub(crate) struct SshConfig {
24 /// Time after which the connection is garbage-collected.
25 pub inactivity_timeout: Option<Duration>,
26 /// If nothing is received from the server for this amount of time, send a keepalive message.
27 pub keepalive_interval: Option<Duration>,
28 /// If this many keepalives have been sent without reply, close the connection.
29 pub keepalive_max: Option<usize>,
30 /// Enable `TCP_NODELAY` (disable Nagle's algorithm).
31 pub nodelay: Option<bool>,
32 /// SSH flow-control window size.
33 pub window_size: Option<u32>,
34 /// Maximum SSH packet size.
35 pub maximum_packet_size: Option<u32>,
36 /// Host key verification mode.
37 pub host_key_verification: HostKeyVerification,
38 /// Custom path to `known_hosts` file. `None` uses `~/.ssh/known_hosts`.
39 pub known_hosts_path: Option<PathBuf>,
40}
41
42#[derive(Debug, Clone)]
43pub(crate) struct JumphostConfig {
44 pub host: String,
45 pub port: u16,
46 pub username: String,
47 pub password: String,
48}
49
50impl Default for SshConfig {
51 fn default() -> Self {
52 Self {
53 inactivity_timeout: None,
54 keepalive_interval: Some(Duration::from_secs(10)),
55 keepalive_max: Some(3),
56 nodelay: None,
57 window_size: None,
58 maximum_packet_size: None,
59 host_key_verification: HostKeyVerification::AcceptNew,
60 known_hosts_path: None,
61 }
62 }
63}
64
65/// Configuration for a NETCONF session.
66///
67/// Use [`Config::builder()`] for a fluent API, or [`Config::default()`]
68/// for sensible defaults.
69///
70/// # Example
71///
72/// ```
73/// use std::time::Duration;
74/// use netconf_rust::Config;
75///
76/// let config = Config::builder()
77/// .hello_timeout(Duration::from_secs(10))
78/// .connect_timeout(Duration::from_secs(5))
79/// .nodelay(true)
80/// .build();
81/// ```
82#[derive(Debug, Clone)]
83pub struct Config {
84 pub(crate) codec: CodecConfig,
85 pub(crate) connect_timeout: Option<Duration>,
86 pub(crate) hello_timeout: Option<Duration>,
87 pub(crate) rpc_timeout: Option<Duration>,
88 pub(crate) ssh: SshConfig,
89 pub(crate) jumphost: Option<JumphostConfig>,
90}
91
92impl Default for Config {
93 fn default() -> Self {
94 Self {
95 codec: CodecConfig::default(),
96 connect_timeout: None,
97 hello_timeout: Some(Duration::from_secs(30)),
98 rpc_timeout: None,
99 ssh: SshConfig::default(),
100 jumphost: None,
101 }
102 }
103}
104
105impl Config {
106 pub fn builder() -> ConfigBuilder {
107 ConfigBuilder {
108 config: Config::default(),
109 }
110 }
111
112 /// Maximum message size limit, or `None` for unlimited.
113 pub fn max_message_size(&self) -> Option<usize> {
114 self.codec.max_message_size
115 }
116
117 /// Timeout for the TCP/SSH connect phase.
118 pub fn connect_timeout(&self) -> Option<Duration> {
119 self.connect_timeout
120 }
121
122 /// Timeout for the NETCONF hello exchange.
123 pub fn hello_timeout(&self) -> Option<Duration> {
124 self.hello_timeout
125 }
126
127 /// Timeout for individual RPC operations.
128 pub fn rpc_timeout(&self) -> Option<Duration> {
129 self.rpc_timeout
130 }
131}
132
133#[derive(Debug, Clone)]
134pub struct ConfigBuilder {
135 config: Config,
136}
137
138impl ConfigBuilder {
139 /// Set the maximum NETCONF message size. `None` means unlimited.
140 pub fn max_message_size(mut self, size: usize) -> Self {
141 self.config.codec.max_message_size = Some(size);
142 self
143 }
144
145 /// Set the timeout for the TCP/SSH connect phase.
146 ///
147 /// When set, [`Session::connect_with_config`](crate::Session::connect_with_config)
148 /// will fail with [`TransportError::Timeout`](crate::error::TransportError::Timeout)
149 /// if the SSH connection is not established within this duration.
150 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
151 self.config.connect_timeout = Some(timeout);
152 self
153 }
154
155 /// Set the timeout for the NETCONF hello exchange.
156 ///
157 /// Some vendors (e.g. Nokia SR OS) can silently stall during the hello
158 /// exchange — for example, if the client hello contains an invalid
159 /// namespace, the server waits indefinitely for a valid hello without
160 /// returning an error. Without a timeout, the connection hangs forever.
161 ///
162 /// When set, the hello exchange will fail with
163 /// [`TransportError::Timeout`](crate::error::TransportError::Timeout)
164 /// if the server does not complete the hello within this duration.
165 pub fn hello_timeout(mut self, timeout: Duration) -> Self {
166 self.config.hello_timeout = Some(timeout);
167 self
168 }
169
170 /// Set the timeout for individual RPC operations (send + wait for reply).
171 ///
172 /// When set, [`RpcFuture::response()`](crate::RpcFuture::response) will fail
173 /// with [`TransportError::Timeout`](crate::error::TransportError::Timeout)
174 /// if the server does not reply within this duration.
175 pub fn rpc_timeout(mut self, timeout: Duration) -> Self {
176 self.config.rpc_timeout = Some(timeout);
177 self
178 }
179
180 /// Set the SSH inactivity timeout (garbage-collect idle connections).
181 pub fn inactivity_timeout(mut self, timeout: Duration) -> Self {
182 self.config.ssh.inactivity_timeout = Some(timeout);
183 self
184 }
185
186 /// Set the SSH keepalive interval (detect dead peers).
187 pub fn keepalive_interval(mut self, interval: Duration) -> Self {
188 self.config.ssh.keepalive_interval = Some(interval);
189 self
190 }
191
192 /// Set the maximum number of missed keepalives before disconnect.
193 pub fn keepalive_max(mut self, max: usize) -> Self {
194 self.config.ssh.keepalive_max = Some(max);
195 self
196 }
197
198 /// Enable or disable `TCP_NODELAY` on the SSH socket.
199 pub fn nodelay(mut self, nodelay: bool) -> Self {
200 self.config.ssh.nodelay = Some(nodelay);
201 self
202 }
203
204 /// Set the SSH flow-control window size.
205 pub fn window_size(mut self, size: u32) -> Self {
206 self.config.ssh.window_size = Some(size);
207 self
208 }
209
210 /// Set the maximum SSH packet size.
211 pub fn maximum_packet_size(mut self, size: u32) -> Self {
212 self.config.ssh.maximum_packet_size = Some(size);
213 self
214 }
215
216 /// Set the host key verification mode.
217 pub fn host_key_verification(mut self, mode: HostKeyVerification) -> Self {
218 self.config.ssh.host_key_verification = mode;
219 self
220 }
221
222 /// Set a custom path to the `known_hosts` file.
223 pub fn known_hosts_path(mut self, path: impl Into<PathBuf>) -> Self {
224 self.config.ssh.known_hosts_path = Some(path.into());
225 self
226 }
227
228 /// Disable host key verification entirely (insecure — use only for testing).
229 pub fn danger_disable_host_key_verification(mut self) -> Self {
230 self.config.ssh.host_key_verification = HostKeyVerification::Disabled;
231 self
232 }
233
234 /// Route the connection through an SSH jumphost (bastion host).
235 ///
236 /// The library will first SSH into the jumphost, open a `direct-tcpip`
237 /// channel to the target device, then establish the NETCONF SSH session
238 /// over that tunnel. The jumphost inherits all SSH settings
239 /// (host key verification, known_hosts, keepalive, etc.) from this config.
240 pub fn jumphost(
241 mut self,
242 host: impl Into<String>,
243 port: u16,
244 username: impl Into<String>,
245 password: impl Into<String>,
246 ) -> Self {
247 self.config.jumphost = Some(JumphostConfig {
248 host: host.into(),
249 port,
250 username: username.into(),
251 password: password.into(),
252 });
253 self
254 }
255
256 pub fn build(self) -> Config {
257 self.config
258 }
259}