Skip to main content

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}