openvpn_mgmt_codec/command.rs
1use std::str::FromStr;
2
3use crate::auth::{AuthRetryMode, AuthType};
4use crate::kill_target::KillTarget;
5use crate::need_ok::NeedOkResponse;
6use crate::proxy_action::ProxyAction;
7use crate::redacted::Redacted;
8use crate::remote_action::RemoteAction;
9use crate::signal::Signal;
10use crate::status_format::StatusFormat;
11use crate::stream_mode::StreamMode;
12
13/// Every command the management interface accepts, modeled as a typed enum.
14///
15/// The encoder handles all serialization — escaping, quoting, multi-line
16/// block framing — so callers never assemble raw strings. The `Raw` variant
17/// exists as an escape hatch for commands not yet modeled here.
18///
19/// Sensitive fields (passwords, tokens, challenge responses) are wrapped in
20/// [`Redacted`] so they are masked in [`Debug`] and [`Display`](std::fmt::Display)
21/// output. Use [`Redacted::expose`] to access the raw value for wire encoding.
22#[derive(Debug, Clone, PartialEq, Eq, strum::IntoStaticStr)]
23#[strum(serialize_all = "kebab-case")]
24pub enum OvpnCommand {
25 // ── Informational ────────────────────────────────────────────
26 /// Request connection status in the given format.
27 /// Wire: `status` / `status 2` / `status 3`
28 Status(StatusFormat),
29
30 /// Print current state (single comma-delimited line).
31 /// Wire: `state`
32 State,
33
34 /// Control real-time state notifications and/or dump history.
35 /// Wire: `state on` / `state off` / `state all` / `state on all` / `state 3`
36 StateStream(StreamMode),
37
38 /// Print the OpenVPN and management interface version.
39 /// Wire: `version`
40 Version,
41
42 /// Show the PID of the OpenVPN process.
43 /// Wire: `pid`
44 Pid,
45
46 /// List available management commands.
47 /// Wire: `help`
48 Help,
49
50 /// Get or set the log verbosity level (0–15).
51 /// `Verb(None)` queries the current level; `Verb(Some(n))` sets it.
52 /// Wire: `verb` / `verb 4`
53 Verb(Option<u8>),
54
55 /// Get or set the mute threshold (suppress repeating messages).
56 /// Wire: `mute` / `mute 40`
57 Mute(Option<u32>),
58
59 /// (Windows only) Show network adapter list and routing table.
60 /// Wire: `net`
61 Net,
62
63 // ── Real-time notification control ───────────────────────────
64 /// Control real-time log streaming and/or dump log history.
65 /// Wire: `log on` / `log off` / `log all` / `log on all` / `log 20`
66 Log(StreamMode),
67
68 /// Control real-time echo parameter notifications.
69 /// Wire: `echo on` / `echo off` / `echo all` / `echo on all`
70 Echo(StreamMode),
71
72 /// Enable/disable byte count notifications at N-second intervals.
73 /// Pass 0 to disable.
74 /// Wire: `bytecount 5` / `bytecount 0`
75 ByteCount(u32),
76
77 // ── Connection control ───────────────────────────────────────
78 /// Send a signal to the OpenVPN daemon.
79 /// Wire: `signal SIGUSR1`
80 Signal(Signal),
81
82 /// Kill a specific client connection (server mode).
83 /// Wire: `kill Test-Client` / `kill 1.2.3.4:4000`
84 Kill(KillTarget),
85
86 /// Query the current hold flag.
87 /// Wire: `hold`
88 /// Response: `SUCCESS: hold=0` or `SUCCESS: hold=1`
89 HoldQuery,
90
91 /// Set the hold flag on — future restarts will pause until released.
92 /// Wire: `hold on`
93 HoldOn,
94
95 /// Clear the hold flag.
96 /// Wire: `hold off`
97 HoldOff,
98
99 /// Release from hold state and start OpenVPN. Does not change the
100 /// hold flag itself.
101 /// Wire: `hold release`
102 HoldRelease,
103
104 // ── Authentication ───────────────────────────────────────────
105 /// Supply a username for the given auth type.
106 /// Wire: `username "Auth" myuser`
107 Username {
108 /// Which credential set this username belongs to.
109 auth_type: AuthType,
110 /// The username value (redacted in debug output).
111 value: Redacted,
112 },
113
114 /// Supply a password for the given auth type. The value is escaped
115 /// and double-quoted per the OpenVPN config-file lexer rules.
116 /// Wire: `password "Private Key" "foo\"bar"`
117 Password {
118 /// Which credential set this password belongs to.
119 auth_type: AuthType,
120 /// The password value (redacted in debug output, escaped on the wire).
121 value: Redacted,
122 },
123
124 /// Set the auth-retry strategy.
125 /// Wire: `auth-retry interact`
126 AuthRetry(AuthRetryMode),
127
128 /// Forget all passwords entered during this management session.
129 /// Wire: `forget-passwords`
130 ForgetPasswords,
131
132 // ── Challenge-response authentication ────────────────────────
133 /// Respond to a CRV1 dynamic challenge.
134 /// Wire: `password "Auth" "CRV1::state_id::response"`
135 ChallengeResponse {
136 /// The opaque state ID from the `>PASSWORD:` CRV1 notification.
137 state_id: String,
138 /// The user's response to the challenge (redacted in debug output).
139 response: Redacted,
140 },
141
142 /// Respond to a static challenge (SC).
143 /// Wire: `password "Auth" "SCRV1::base64_password::base64_response"`
144 ///
145 /// The caller must pre-encode password and response as base64 —
146 /// this crate does not include a base64 dependency.
147 StaticChallengeResponse {
148 /// Base64-encoded password (redacted in debug output).
149 password_b64: Redacted,
150 /// Base64-encoded challenge response (redacted in debug output).
151 response_b64: Redacted,
152 },
153
154 // ── Interactive prompts (OpenVPN 2.1+) ───────────────────────
155 /// Respond to a `>NEED-OK:` prompt.
156 /// Wire: `needok token-insertion-request ok` / `needok ... cancel`
157 NeedOk {
158 /// The prompt name from the `>NEED-OK:` notification.
159 name: String,
160 /// Accept or cancel.
161 response: NeedOkResponse,
162 },
163
164 /// Respond to a `>NEED-STR:` prompt with a string value.
165 /// Wire: `needstr name "John"`
166 NeedStr {
167 /// The prompt name from the `>NEED-STR:` notification.
168 name: String,
169 /// The string value to send (will be escaped on the wire).
170 value: String,
171 },
172
173 // ── PKCS#11 (OpenVPN 2.1+) ──────────────────────────────────
174 /// Query available PKCS#11 certificate count.
175 /// Wire: `pkcs11-id-count`
176 Pkcs11IdCount,
177
178 /// Retrieve a PKCS#11 certificate by index.
179 /// Wire: `pkcs11-id-get 1`
180 Pkcs11IdGet(u32),
181
182 // ── External key / RSA signature (OpenVPN 2.3+) ──────────────
183 /// Provide an RSA signature in response to `>RSA_SIGN:`.
184 /// This is a multi-line command: the encoder writes `rsa-sig`,
185 /// then each base64 line, then `END`.
186 RsaSig {
187 /// Base64-encoded signature lines.
188 base64_lines: Vec<String>,
189 },
190
191 // ── Client management (server mode, OpenVPN 2.1+) ────────────
192 /// Authorize a `>CLIENT:CONNECT` or `>CLIENT:REAUTH` and push config
193 /// directives. Multi-line command: header, config lines, `END`.
194 /// An empty `config_lines` produces a null block (header + immediate END),
195 /// which is equivalent to `client-auth-nt` in effect.
196 ClientAuth {
197 /// Client ID from the `>CLIENT:` notification.
198 cid: u64,
199 /// Key ID from the `>CLIENT:` notification.
200 kid: u64,
201 /// Config directives to push (e.g. `push "route ..."`).
202 config_lines: Vec<String>,
203 },
204
205 /// Authorize a client without pushing any config.
206 /// Wire: `client-auth-nt {CID} {KID}`
207 ClientAuthNt {
208 /// Client ID.
209 cid: u64,
210 /// Key ID.
211 kid: u64,
212 },
213
214 /// Deny a `>CLIENT:CONNECT` or `>CLIENT:REAUTH`.
215 /// Wire: `client-deny {CID} {KID} "reason" ["client-reason"]`
216 ClientDeny {
217 /// Client ID.
218 cid: u64,
219 /// Key ID.
220 kid: u64,
221 /// Server-side reason string (logged but not sent to client).
222 reason: String,
223 /// Optional message sent to the client as part of AUTH_FAILED.
224 client_reason: Option<String>,
225 },
226
227 /// Kill a client session by CID, optionally with a custom message.
228 /// Wire: `client-kill {CID}` or `client-kill {CID} {message}`
229 /// Default message is `RESTART` if omitted.
230 ClientKill {
231 /// Client ID.
232 cid: u64,
233 /// Optional kill message (e.g. `"HALT"`, `"RESTART"`). Defaults to
234 /// `RESTART` on the server if `None`.
235 message: Option<String>,
236 },
237
238 // ── Remote/Proxy override ────────────────────────────────────
239 /// Respond to a `>REMOTE:` notification (requires `--management-query-remote`).
240 /// Wire: `remote ACCEPT` / `remote SKIP` / `remote MOD host port`
241 Remote(RemoteAction),
242
243 /// Respond to a `>PROXY:` notification (requires `--management-query-proxy`).
244 /// Wire: `proxy NONE` / `proxy HTTP host port [nct]` / `proxy SOCKS host port`
245 Proxy(ProxyAction),
246
247 // ── Server statistics ─────────────────────────────────────────
248 /// Request aggregated server stats.
249 /// Wire: `load-stats`
250 /// Response: `SUCCESS: nclients=N,bytesin=N,bytesout=N`
251 LoadStats,
252
253 // ── Extended client management (OpenVPN 2.5+) ────────────────
254 /// Defer authentication for a client, allowing async auth backends.
255 /// Wire: `client-pending-auth {CID} {KID} {EXTRA} {TIMEOUT}`
256 ClientPendingAuth {
257 /// Client ID.
258 cid: u64,
259 /// Key ID.
260 kid: u64,
261 /// Extra opaque string passed to the auth backend.
262 extra: String,
263 /// Timeout in seconds before the pending auth expires.
264 timeout: u32,
265 },
266
267 /// Respond to a CR_TEXT challenge (client-side, OpenVPN 2.6+).
268 /// Wire: `cr-response {base64-response}`
269 CrResponse {
270 /// The base64-encoded challenge-response answer (redacted in debug output).
271 response: Redacted,
272 },
273
274 // ── External certificate (OpenVPN 2.4+) ──────────────────────
275 /// Supply an external certificate in response to `>NEED-CERTIFICATE`.
276 /// Multi-line command: header, PEM lines, `END`.
277 /// Wire: `certificate\n{pem_lines}\nEND`
278 Certificate {
279 /// PEM-encoded certificate lines.
280 pem_lines: Vec<String>,
281 },
282
283 // ── Management interface authentication ────────────────────────
284 /// Authenticate to the management interface itself. Sent as a bare
285 /// line (no command prefix, no quoting) in response to
286 /// [`crate::OvpnMessage::PasswordPrompt`].
287 /// Wire: `{password}\n`
288 ManagementPassword(Redacted),
289
290 // ── Session lifecycle ────────────────────────────────────────
291 /// Close the management session. OpenVPN keeps running and resumes
292 /// listening for new management connections.
293 Exit,
294
295 /// Identical to `Exit`.
296 Quit,
297
298 // ── Escape hatch ─────────────────────────────────────────────
299 /// Send a raw command string for anything not yet modeled above.
300 /// The decoder expects a `SUCCESS:`/`ERROR:` response.
301 Raw(String),
302
303 /// Send a raw command string, expecting a multi-line (END-terminated)
304 /// response.
305 ///
306 /// Like [`Raw`], the string is passed through the encoder's wire-safety
307 /// gate before sending (see [`crate::EncoderMode`]). Unlike `Raw`, the
308 /// decoder accumulates the response into [`OvpnMessage::MultiLine`].
309 RawMultiLine(String),
310}
311
312/// What kind of response the decoder should expect after a given command.
313/// This is the core of the command-tracking mechanism that resolves the
314/// protocol's ambiguity around single-line vs. multi-line responses.
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub(crate) enum ResponseKind {
317 /// Expect a `SUCCESS:` or `ERROR:` line.
318 SuccessOrError,
319
320 /// Expect multiple lines terminated by a bare `END`.
321 MultiLine,
322
323 /// No response expected (connection may close).
324 NoResponse,
325}
326
327impl OvpnCommand {
328 /// Determine what kind of response this command produces, so the
329 /// decoder knows how to frame the next incoming bytes.
330 pub(crate) fn expected_response(&self) -> ResponseKind {
331 match self {
332 // These always produce multi-line (END-terminated) responses.
333 Self::Status(_) | Self::Version | Self::Help | Self::Net => ResponseKind::MultiLine,
334
335 // state/log/echo: depends on the specific sub-mode.
336 Self::StateStream(mode) | Self::Log(mode) | Self::Echo(mode) => match mode {
337 StreamMode::All | StreamMode::OnAll | StreamMode::Recent(_) => {
338 ResponseKind::MultiLine
339 }
340 StreamMode::On | StreamMode::Off => ResponseKind::SuccessOrError,
341 },
342
343 // Bare `state` returns state history (END-terminated).
344 Self::State => ResponseKind::MultiLine,
345
346 // Raw multi-line expects END-terminated response.
347 Self::RawMultiLine(_) => ResponseKind::MultiLine,
348
349 // exit/quit close the connection.
350 Self::Exit | Self::Quit => ResponseKind::NoResponse,
351
352 // Everything else (including Raw) produces SUCCESS: or ERROR:.
353 _ => ResponseKind::SuccessOrError,
354 }
355 }
356}
357
358impl FromStr for OvpnCommand {
359 type Err = String;
360
361 /// Parse a human-readable command string into an [`OvpnCommand`].
362 ///
363 /// This accepts the same syntax used by interactive management clients:
364 /// a command name followed by space-separated arguments.
365 ///
366 /// Commands that cannot be represented as a single line (multi-line bodies
367 /// like `rsa-sig`, `client-auth` config lines, `certificate` PEM) are
368 /// parsed with comma-separated lines in the argument position.
369 ///
370 /// Unrecognized commands fall through to [`OvpnCommand::Raw`].
371 ///
372 /// # Examples
373 ///
374 /// ```
375 /// use openvpn_mgmt_codec::OvpnCommand;
376 ///
377 /// let cmd: OvpnCommand = "version".parse().unwrap();
378 /// assert_eq!(cmd, OvpnCommand::Version);
379 ///
380 /// let cmd: OvpnCommand = "state on all".parse().unwrap();
381 /// assert_eq!(cmd, OvpnCommand::StateStream(openvpn_mgmt_codec::StreamMode::OnAll));
382 /// ```
383 fn from_str(line: &str) -> Result<Self, Self::Err> {
384 let line = line.trim();
385 let (cmd, args) = line
386 .split_once(char::is_whitespace)
387 .map(|(c, a)| (c, a.trim()))
388 .unwrap_or((line, ""));
389
390 match cmd {
391 // ── Informational ────────────────────────────────────────
392 "version" => Ok(Self::Version),
393 "pid" => Ok(Self::Pid),
394 "help" => Ok(Self::Help),
395 "net" => Ok(Self::Net),
396 "load-stats" => Ok(Self::LoadStats),
397
398 "status" => match args {
399 "" | "1" => Ok(Self::Status(StatusFormat::V1)),
400 "2" => Ok(Self::Status(StatusFormat::V2)),
401 "3" => Ok(Self::Status(StatusFormat::V3)),
402 _ => Err(format!("invalid status format: {args} (use 1, 2, or 3)")),
403 },
404
405 "state" => match args {
406 "" => Ok(Self::State),
407 other => other.parse::<StreamMode>().map(Self::StateStream),
408 },
409
410 "log" => args.parse::<StreamMode>().map(Self::Log),
411 "echo" => args.parse::<StreamMode>().map(Self::Echo),
412
413 "verb" => {
414 if args.is_empty() {
415 Ok(Self::Verb(None))
416 } else {
417 args.parse::<u8>()
418 .map(|n| Self::Verb(Some(n)))
419 .map_err(|_| format!("invalid verbosity: {args} (0-15)"))
420 }
421 }
422
423 "mute" => {
424 if args.is_empty() {
425 Ok(Self::Mute(None))
426 } else {
427 args.parse::<u32>()
428 .map(|n| Self::Mute(Some(n)))
429 .map_err(|_| format!("invalid mute value: {args}"))
430 }
431 }
432
433 "bytecount" => args
434 .parse::<u32>()
435 .map(Self::ByteCount)
436 .map_err(|_| format!("bytecount requires a number, got: {args}")),
437
438 // ── Connection control ───────────────────────────────────
439 "signal" => args.parse::<Signal>().map(Self::Signal),
440
441 "kill" => {
442 if args.is_empty() {
443 return Err("kill requires a target (common name or proto:ip:port)".into());
444 }
445 let parts: Vec<&str> = args.splitn(3, ':').collect();
446 if parts.len() == 3
447 && let Ok(port) = parts[2].parse::<u16>()
448 {
449 return Ok(Self::Kill(KillTarget::Address {
450 protocol: parts[0].to_string(),
451 ip: parts[1].to_string(),
452 port,
453 }));
454 }
455 Ok(Self::Kill(KillTarget::CommonName(args.to_string())))
456 }
457
458 "hold" => match args {
459 "" => Ok(Self::HoldQuery),
460 "on" => Ok(Self::HoldOn),
461 "off" => Ok(Self::HoldOff),
462 "release" => Ok(Self::HoldRelease),
463 _ => Err(format!("invalid hold argument: {args}")),
464 },
465
466 // ── Authentication ───────────────────────────────────────
467 "username" => {
468 let (auth_type, value) = args
469 .split_once(char::is_whitespace)
470 .ok_or("usage: username <auth-type> <value>")?;
471 Ok(Self::Username {
472 auth_type: auth_type.parse().unwrap(),
473 value: value.trim().into(),
474 })
475 }
476
477 "password" => {
478 let (auth_type, value) = args
479 .split_once(char::is_whitespace)
480 .ok_or("usage: password <auth-type> <value>")?;
481 Ok(Self::Password {
482 auth_type: auth_type.parse().unwrap(),
483 value: value.trim().into(),
484 })
485 }
486
487 "auth-retry" => args.parse::<AuthRetryMode>().map(Self::AuthRetry),
488
489 "forget-passwords" => Ok(Self::ForgetPasswords),
490
491 // ── Interactive prompts ──────────────────────────────────
492 "needok" => {
493 let (name, resp) = args
494 .rsplit_once(char::is_whitespace)
495 .ok_or("usage: needok <name> ok|cancel")?;
496 let response = match resp {
497 "ok" => NeedOkResponse::Ok,
498 "cancel" => NeedOkResponse::Cancel,
499 _ => return Err(format!("invalid needok response: {resp} (use ok/cancel)")),
500 };
501 Ok(Self::NeedOk {
502 name: name.trim().to_string(),
503 response,
504 })
505 }
506
507 "needstr" => {
508 let (name, value) = args
509 .split_once(char::is_whitespace)
510 .ok_or("usage: needstr <name> <value>")?;
511 Ok(Self::NeedStr {
512 name: name.to_string(),
513 value: value.trim().to_string(),
514 })
515 }
516
517 // ── PKCS#11 ─────────────────────────────────────────────
518 "pkcs11-id-count" => Ok(Self::Pkcs11IdCount),
519
520 "pkcs11-id-get" => args
521 .parse::<u32>()
522 .map(Self::Pkcs11IdGet)
523 .map_err(|_| format!("pkcs11-id-get requires a number, got: {args}")),
524
525 // ── Client management (server mode) ─────────────────────
526 "client-auth" => {
527 let mut parts = args.splitn(3, char::is_whitespace);
528 let cid = parts
529 .next()
530 .ok_or("usage: client-auth <cid> <kid> [config-lines]")?
531 .parse::<u64>()
532 .map_err(|_| "cid must be a number")?;
533 let kid = parts
534 .next()
535 .ok_or("usage: client-auth <cid> <kid> [config-lines]")?
536 .parse::<u64>()
537 .map_err(|_| "kid must be a number")?;
538 let config_lines = match parts.next() {
539 Some(rest) => rest.split(',').map(|s| s.trim().to_string()).collect(),
540 None => vec![],
541 };
542 Ok(Self::ClientAuth {
543 cid,
544 kid,
545 config_lines,
546 })
547 }
548
549 "client-auth-nt" => {
550 let (cid_s, kid_s) = args
551 .split_once(char::is_whitespace)
552 .ok_or("usage: client-auth-nt <cid> <kid>")?;
553 Ok(Self::ClientAuthNt {
554 cid: cid_s.parse().map_err(|_| "cid must be a number")?,
555 kid: kid_s.trim().parse().map_err(|_| "kid must be a number")?,
556 })
557 }
558
559 "client-deny" => {
560 let mut parts = args.splitn(4, char::is_whitespace);
561 let cid = parts
562 .next()
563 .ok_or("usage: client-deny <cid> <kid> <reason> [client-reason]")?
564 .parse::<u64>()
565 .map_err(|_| "cid must be a number")?;
566 let kid = parts
567 .next()
568 .ok_or("usage: client-deny <cid> <kid> <reason> [client-reason]")?
569 .parse::<u64>()
570 .map_err(|_| "kid must be a number")?;
571 let reason = parts
572 .next()
573 .ok_or("usage: client-deny <cid> <kid> <reason> [client-reason]")?
574 .to_string();
575 let client_reason = parts.next().map(|s| s.to_string());
576 Ok(Self::ClientDeny {
577 cid,
578 kid,
579 reason,
580 client_reason,
581 })
582 }
583
584 "client-kill" => {
585 let (cid_str, message) = match args.split_once(char::is_whitespace) {
586 Some((c, m)) => (c, Some(m.trim().to_string())),
587 None => (args, None),
588 };
589 let cid = cid_str
590 .parse::<u64>()
591 .map_err(|_| format!("client-kill requires a CID number, got: {cid_str}"))?;
592 Ok(Self::ClientKill { cid, message })
593 }
594
595 // ── Remote/Proxy override ────────────────────────────────
596 "remote" => match args.split_whitespace().collect::<Vec<_>>().as_slice() {
597 ["accept" | "ACCEPT"] => Ok(Self::Remote(RemoteAction::Accept)),
598 ["skip" | "SKIP"] => Ok(Self::Remote(RemoteAction::Skip)),
599 ["mod" | "MOD", host, port] => Ok(Self::Remote(RemoteAction::Modify {
600 host: host.to_string(),
601 port: port.parse().map_err(|_| "port must be a number")?,
602 })),
603 _ => Err("usage: remote accept|skip|mod <host> <port>".into()),
604 },
605
606 "proxy" => match args.split_whitespace().collect::<Vec<_>>().as_slice() {
607 ["none" | "NONE"] => Ok(Self::Proxy(ProxyAction::None)),
608 ["http" | "HTTP", host, port] => Ok(Self::Proxy(ProxyAction::Http {
609 host: host.to_string(),
610 port: port.parse().map_err(|_| "port must be a number")?,
611 non_cleartext_only: false,
612 })),
613 ["http" | "HTTP", host, port, "nct"] => Ok(Self::Proxy(ProxyAction::Http {
614 host: host.to_string(),
615 port: port.parse().map_err(|_| "port must be a number")?,
616 non_cleartext_only: true,
617 })),
618 ["socks" | "SOCKS", host, port] => Ok(Self::Proxy(ProxyAction::Socks {
619 host: host.to_string(),
620 port: port.parse().map_err(|_| "port must be a number")?,
621 })),
622 _ => Err("usage: proxy none|http <host> <port> [nct]|socks <host> <port>".into()),
623 },
624
625 // ── Raw multi-line ──────────────────────────────────────
626 "raw-ml" => {
627 if args.is_empty() {
628 return Err("usage: raw-ml <command>".into());
629 }
630 Ok(Self::RawMultiLine(args.to_string()))
631 }
632
633 // ── Lifecycle ────────────────────────────────────────────
634 "exit" => Ok(Self::Exit),
635 "quit" => Ok(Self::Quit),
636
637 // ── Fallback: send as raw command ────────────────────────
638 _ => Ok(Self::Raw(line.to_string())),
639 }
640 }
641}
642
643/// The standard startup sequence that most management clients send after
644/// connecting.
645///
646/// This is the pattern used by `node-openvpn` and other clients: enable
647/// log streaming, request the PID, start byte-count notifications, and
648/// release the hold so OpenVPN begins connecting.
649///
650/// # Arguments
651///
652/// * `bytecount_interval` — seconds between `>BYTECOUNT:` notifications
653/// (pass `0` to skip enabling byte counts).
654///
655/// # Examples
656///
657/// ```
658/// use openvpn_mgmt_codec::command::connection_sequence;
659/// use openvpn_mgmt_codec::OvpnCommand;
660///
661/// let cmds = connection_sequence(5);
662/// assert!(cmds.iter().any(|c| matches!(c, OvpnCommand::HoldRelease)));
663/// ```
664///
665/// To send these over a framed connection:
666///
667/// ```no_run
668/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
669/// use tokio::net::TcpStream;
670/// use tokio_util::codec::Framed;
671/// use futures::SinkExt;
672/// use openvpn_mgmt_codec::{OvpnCodec, OvpnCommand};
673/// use openvpn_mgmt_codec::command::connection_sequence;
674///
675/// let stream = TcpStream::connect("127.0.0.1:7505").await?;
676/// let mut framed = Framed::new(stream, OvpnCodec::new());
677///
678/// for cmd in connection_sequence(5) {
679/// framed.send(cmd).await?;
680/// }
681/// # Ok(())
682/// # }
683/// ```
684pub fn connection_sequence(bytecount_interval: u32) -> Vec<OvpnCommand> {
685 let mut cmds = vec![
686 OvpnCommand::Log(StreamMode::OnAll),
687 OvpnCommand::StateStream(StreamMode::OnAll),
688 OvpnCommand::Pid,
689 ];
690 if bytecount_interval > 0 {
691 cmds.push(OvpnCommand::ByteCount(bytecount_interval));
692 }
693 cmds.push(OvpnCommand::HoldRelease);
694 cmds
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700
701 #[test]
702 fn into_static_str_labels() {
703 let label: &str = (&OvpnCommand::State).into();
704 assert_eq!(label, "state");
705
706 let label: &str = (&OvpnCommand::ForgetPasswords).into();
707 assert_eq!(label, "forget-passwords");
708
709 let label: &str = (&OvpnCommand::ByteCount(5)).into();
710 assert_eq!(label, "byte-count");
711 }
712
713 // ── FromStr: informational commands ──────────────────────────
714
715 #[test]
716 fn parse_simple_commands() {
717 assert_eq!("version".parse(), Ok(OvpnCommand::Version));
718 assert_eq!("pid".parse(), Ok(OvpnCommand::Pid));
719 assert_eq!("help".parse(), Ok(OvpnCommand::Help));
720 assert_eq!("net".parse(), Ok(OvpnCommand::Net));
721 assert_eq!("load-stats".parse(), Ok(OvpnCommand::LoadStats));
722 assert_eq!("forget-passwords".parse(), Ok(OvpnCommand::ForgetPasswords));
723 assert_eq!("pkcs11-id-count".parse(), Ok(OvpnCommand::Pkcs11IdCount));
724 assert_eq!("exit".parse(), Ok(OvpnCommand::Exit));
725 assert_eq!("quit".parse(), Ok(OvpnCommand::Quit));
726 }
727
728 #[test]
729 fn parse_status() {
730 assert_eq!("status".parse(), Ok(OvpnCommand::Status(StatusFormat::V1)));
731 assert_eq!(
732 "status 1".parse(),
733 Ok(OvpnCommand::Status(StatusFormat::V1))
734 );
735 assert_eq!(
736 "status 2".parse(),
737 Ok(OvpnCommand::Status(StatusFormat::V2))
738 );
739 assert_eq!(
740 "status 3".parse(),
741 Ok(OvpnCommand::Status(StatusFormat::V3))
742 );
743 assert!("status 4".parse::<OvpnCommand>().is_err());
744 }
745
746 // ── FromStr: state / log / echo stream modes ─────────────────
747
748 #[test]
749 fn parse_state_bare() {
750 assert_eq!("state".parse(), Ok(OvpnCommand::State));
751 }
752
753 #[test]
754 fn parse_state_stream_modes() {
755 assert_eq!(
756 "state on".parse(),
757 Ok(OvpnCommand::StateStream(StreamMode::On))
758 );
759 assert_eq!(
760 "state off".parse(),
761 Ok(OvpnCommand::StateStream(StreamMode::Off))
762 );
763 assert_eq!(
764 "state all".parse(),
765 Ok(OvpnCommand::StateStream(StreamMode::All))
766 );
767 assert_eq!(
768 "state on all".parse(),
769 Ok(OvpnCommand::StateStream(StreamMode::OnAll))
770 );
771 assert_eq!(
772 "state 5".parse(),
773 Ok(OvpnCommand::StateStream(StreamMode::Recent(5)))
774 );
775 }
776
777 #[test]
778 fn parse_log_and_echo() {
779 assert_eq!("log on".parse(), Ok(OvpnCommand::Log(StreamMode::On)));
780 assert_eq!(
781 "log on all".parse(),
782 Ok(OvpnCommand::Log(StreamMode::OnAll))
783 );
784 assert_eq!("echo off".parse(), Ok(OvpnCommand::Echo(StreamMode::Off)));
785 assert_eq!(
786 "echo 10".parse(),
787 Ok(OvpnCommand::Echo(StreamMode::Recent(10)))
788 );
789 }
790
791 // ── FromStr: verb / mute / bytecount ─────────────────────────
792
793 #[test]
794 fn parse_verb() {
795 assert_eq!("verb".parse(), Ok(OvpnCommand::Verb(None)));
796 assert_eq!("verb 4".parse(), Ok(OvpnCommand::Verb(Some(4))));
797 assert!("verb abc".parse::<OvpnCommand>().is_err());
798 }
799
800 #[test]
801 fn parse_mute() {
802 assert_eq!("mute".parse(), Ok(OvpnCommand::Mute(None)));
803 assert_eq!("mute 40".parse(), Ok(OvpnCommand::Mute(Some(40))));
804 assert!("mute abc".parse::<OvpnCommand>().is_err());
805 }
806
807 #[test]
808 fn parse_bytecount() {
809 assert_eq!("bytecount 5".parse(), Ok(OvpnCommand::ByteCount(5)));
810 assert_eq!("bytecount 0".parse(), Ok(OvpnCommand::ByteCount(0)));
811 assert!("bytecount".parse::<OvpnCommand>().is_err());
812 }
813
814 // ── FromStr: signal ──────────────────────────────────────────
815
816 #[test]
817 fn parse_signal() {
818 assert_eq!(
819 "signal SIGHUP".parse(),
820 Ok(OvpnCommand::Signal(Signal::SigHup))
821 );
822 assert_eq!(
823 "signal SIGTERM".parse(),
824 Ok(OvpnCommand::Signal(Signal::SigTerm))
825 );
826 assert_eq!(
827 "signal SIGUSR1".parse(),
828 Ok(OvpnCommand::Signal(Signal::SigUsr1))
829 );
830 assert_eq!(
831 "signal SIGUSR2".parse(),
832 Ok(OvpnCommand::Signal(Signal::SigUsr2))
833 );
834 assert!("signal SIGKILL".parse::<OvpnCommand>().is_err());
835 }
836
837 // ── FromStr: kill ────────────────────────────────────────────
838
839 #[test]
840 fn parse_kill_common_name() {
841 assert_eq!(
842 "kill TestClient".parse(),
843 Ok(OvpnCommand::Kill(KillTarget::CommonName(
844 "TestClient".to_string()
845 )))
846 );
847 }
848
849 #[test]
850 fn parse_kill_address() {
851 assert_eq!(
852 "kill tcp:1.2.3.4:4000".parse(),
853 Ok(OvpnCommand::Kill(KillTarget::Address {
854 protocol: "tcp".to_string(),
855 ip: "1.2.3.4".to_string(),
856 port: 4000,
857 }))
858 );
859 }
860
861 #[test]
862 fn parse_kill_empty_is_err() {
863 assert!("kill".parse::<OvpnCommand>().is_err());
864 }
865
866 // ── FromStr: hold ────────────────────────────────────────────
867
868 #[test]
869 fn parse_hold() {
870 assert_eq!("hold".parse(), Ok(OvpnCommand::HoldQuery));
871 assert_eq!("hold on".parse(), Ok(OvpnCommand::HoldOn));
872 assert_eq!("hold off".parse(), Ok(OvpnCommand::HoldOff));
873 assert_eq!("hold release".parse(), Ok(OvpnCommand::HoldRelease));
874 assert!("hold bogus".parse::<OvpnCommand>().is_err());
875 }
876
877 // ── FromStr: authentication ──────────────────────────────────
878
879 #[test]
880 fn parse_username() {
881 let cmd: OvpnCommand = "username Auth alice".parse().unwrap();
882 assert_eq!(
883 cmd,
884 OvpnCommand::Username {
885 auth_type: AuthType::Auth,
886 value: "alice".into(),
887 }
888 );
889 }
890
891 #[test]
892 fn parse_password() {
893 let cmd: OvpnCommand = "password PrivateKey s3cret".parse().unwrap();
894 assert_eq!(
895 cmd,
896 OvpnCommand::Password {
897 auth_type: AuthType::PrivateKey,
898 value: "s3cret".into(),
899 }
900 );
901 }
902
903 #[test]
904 fn parse_username_missing_value_is_err() {
905 assert!("username".parse::<OvpnCommand>().is_err());
906 assert!("username Auth".parse::<OvpnCommand>().is_err());
907 }
908
909 #[test]
910 fn parse_auth_retry() {
911 assert_eq!(
912 "auth-retry none".parse(),
913 Ok(OvpnCommand::AuthRetry(AuthRetryMode::None))
914 );
915 assert_eq!(
916 "auth-retry interact".parse(),
917 Ok(OvpnCommand::AuthRetry(AuthRetryMode::Interact))
918 );
919 assert_eq!(
920 "auth-retry nointeract".parse(),
921 Ok(OvpnCommand::AuthRetry(AuthRetryMode::NoInteract))
922 );
923 assert!("auth-retry bogus".parse::<OvpnCommand>().is_err());
924 }
925
926 // ── FromStr: interactive prompts ─────────────────────────────
927
928 #[test]
929 fn parse_needok() {
930 assert_eq!(
931 "needok token-insertion ok".parse(),
932 Ok(OvpnCommand::NeedOk {
933 name: "token-insertion".to_string(),
934 response: NeedOkResponse::Ok,
935 })
936 );
937 assert_eq!(
938 "needok token-insertion cancel".parse(),
939 Ok(OvpnCommand::NeedOk {
940 name: "token-insertion".to_string(),
941 response: NeedOkResponse::Cancel,
942 })
943 );
944 assert!("needok".parse::<OvpnCommand>().is_err());
945 assert!("needok name bogus".parse::<OvpnCommand>().is_err());
946 }
947
948 #[test]
949 fn parse_needstr() {
950 assert_eq!(
951 "needstr prompt-name John".parse(),
952 Ok(OvpnCommand::NeedStr {
953 name: "prompt-name".to_string(),
954 value: "John".to_string(),
955 })
956 );
957 assert!("needstr".parse::<OvpnCommand>().is_err());
958 }
959
960 // ── FromStr: PKCS#11 ─────────────────────────────────────────
961
962 #[test]
963 fn parse_pkcs11_id_get() {
964 assert_eq!("pkcs11-id-get 1".parse(), Ok(OvpnCommand::Pkcs11IdGet(1)));
965 assert!("pkcs11-id-get abc".parse::<OvpnCommand>().is_err());
966 }
967
968 // ── FromStr: client management ───────────────────────────────
969
970 #[test]
971 fn parse_client_auth() {
972 assert_eq!(
973 "client-auth 42 7".parse(),
974 Ok(OvpnCommand::ClientAuth {
975 cid: 42,
976 kid: 7,
977 config_lines: vec![],
978 })
979 );
980 }
981
982 #[test]
983 fn parse_client_auth_with_config() {
984 let cmd: OvpnCommand = "client-auth 1 2 push route 10.0.0.0,ifconfig-push 10.0.1.1"
985 .parse()
986 .unwrap();
987 assert_eq!(
988 cmd,
989 OvpnCommand::ClientAuth {
990 cid: 1,
991 kid: 2,
992 config_lines: vec![
993 "push route 10.0.0.0".to_string(),
994 "ifconfig-push 10.0.1.1".to_string(),
995 ],
996 }
997 );
998 }
999
1000 #[test]
1001 fn parse_client_auth_nt() {
1002 assert_eq!(
1003 "client-auth-nt 5 3".parse(),
1004 Ok(OvpnCommand::ClientAuthNt { cid: 5, kid: 3 })
1005 );
1006 assert!("client-auth-nt abc 3".parse::<OvpnCommand>().is_err());
1007 }
1008
1009 #[test]
1010 fn parse_client_deny() {
1011 assert_eq!(
1012 "client-deny 1 2 rejected".parse(),
1013 Ok(OvpnCommand::ClientDeny {
1014 cid: 1,
1015 kid: 2,
1016 reason: "rejected".to_string(),
1017 client_reason: None,
1018 })
1019 );
1020 assert_eq!(
1021 "client-deny 1 2 rejected sorry".parse(),
1022 Ok(OvpnCommand::ClientDeny {
1023 cid: 1,
1024 kid: 2,
1025 reason: "rejected".to_string(),
1026 client_reason: Some("sorry".to_string()),
1027 })
1028 );
1029 }
1030
1031 #[test]
1032 fn parse_client_kill() {
1033 assert_eq!(
1034 "client-kill 99".parse(),
1035 Ok(OvpnCommand::ClientKill {
1036 cid: 99,
1037 message: None,
1038 })
1039 );
1040 assert_eq!(
1041 "client-kill 99 HALT".parse(),
1042 Ok(OvpnCommand::ClientKill {
1043 cid: 99,
1044 message: Some("HALT".to_string()),
1045 })
1046 );
1047 assert!("client-kill abc".parse::<OvpnCommand>().is_err());
1048 }
1049
1050 // ── FromStr: remote / proxy ──────────────────────────────────
1051
1052 #[test]
1053 fn parse_remote() {
1054 assert_eq!(
1055 "remote accept".parse(),
1056 Ok(OvpnCommand::Remote(RemoteAction::Accept))
1057 );
1058 assert_eq!(
1059 "remote SKIP".parse(),
1060 Ok(OvpnCommand::Remote(RemoteAction::Skip))
1061 );
1062 assert_eq!(
1063 "remote MOD example.com 443".parse(),
1064 Ok(OvpnCommand::Remote(RemoteAction::Modify {
1065 host: "example.com".to_string(),
1066 port: 443,
1067 }))
1068 );
1069 assert!("remote".parse::<OvpnCommand>().is_err());
1070 }
1071
1072 #[test]
1073 fn parse_proxy() {
1074 assert_eq!(
1075 "proxy none".parse(),
1076 Ok(OvpnCommand::Proxy(ProxyAction::None))
1077 );
1078 assert_eq!(
1079 "proxy HTTP proxy.local 8080".parse(),
1080 Ok(OvpnCommand::Proxy(ProxyAction::Http {
1081 host: "proxy.local".to_string(),
1082 port: 8080,
1083 non_cleartext_only: false,
1084 }))
1085 );
1086 assert_eq!(
1087 "proxy http proxy.local 8080 nct".parse(),
1088 Ok(OvpnCommand::Proxy(ProxyAction::Http {
1089 host: "proxy.local".to_string(),
1090 port: 8080,
1091 non_cleartext_only: true,
1092 }))
1093 );
1094 assert_eq!(
1095 "proxy socks socks.local 1080".parse(),
1096 Ok(OvpnCommand::Proxy(ProxyAction::Socks {
1097 host: "socks.local".to_string(),
1098 port: 1080,
1099 }))
1100 );
1101 assert!("proxy".parse::<OvpnCommand>().is_err());
1102 }
1103
1104 // ── FromStr: raw / raw-ml / fallback ─────────────────────────
1105
1106 #[test]
1107 fn parse_raw_ml() {
1108 assert_eq!(
1109 "raw-ml some-cmd".parse(),
1110 Ok(OvpnCommand::RawMultiLine("some-cmd".to_string()))
1111 );
1112 assert!("raw-ml".parse::<OvpnCommand>().is_err());
1113 }
1114
1115 #[test]
1116 fn parse_unrecognized_falls_through_to_raw() {
1117 assert_eq!(
1118 "unknown-cmd foo bar".parse(),
1119 Ok(OvpnCommand::Raw("unknown-cmd foo bar".to_string()))
1120 );
1121 }
1122
1123 #[test]
1124 fn parse_trims_whitespace() {
1125 assert_eq!(" version ".parse(), Ok(OvpnCommand::Version));
1126 assert_eq!(
1127 " state on ".parse(),
1128 Ok(OvpnCommand::StateStream(StreamMode::On))
1129 );
1130 }
1131}