Skip to main content

openvpn_mgmt_codec/
command.rs

1use crate::auth::{AuthRetryMode, AuthType};
2use crate::kill_target::KillTarget;
3use crate::need_ok::NeedOkResponse;
4use crate::proxy_action::ProxyAction;
5use crate::remote_action::RemoteAction;
6use crate::signal::Signal;
7use crate::status_format::StatusFormat;
8use crate::stream_mode::StreamMode;
9
10/// Every command the management interface accepts, modeled as a typed enum.
11///
12/// The encoder handles all serialization — escaping, quoting, multi-line
13/// block framing — so callers never assemble raw strings. The `Raw` variant
14/// exists as an escape hatch for commands not yet modeled here.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum OvpnCommand {
17    // ── Informational ────────────────────────────────────────────
18    /// Request connection status in the given format.
19    /// Wire: `status` / `status 2` / `status 3`
20    Status(StatusFormat),
21
22    /// Print current state (single comma-delimited line).
23    /// Wire: `state`
24    State,
25
26    /// Control real-time state notifications and/or dump history.
27    /// Wire: `state on` / `state off` / `state all` / `state on all` / `state 3`
28    StateStream(StreamMode),
29
30    /// Print the OpenVPN and management interface version.
31    /// Wire: `version`
32    Version,
33
34    /// Show the PID of the OpenVPN process.
35    /// Wire: `pid`
36    Pid,
37
38    /// List available management commands.
39    /// Wire: `help`
40    Help,
41
42    /// Get or set the log verbosity level (0–15).
43    /// `Verb(None)` queries the current level; `Verb(Some(n))` sets it.
44    /// Wire: `verb` / `verb 4`
45    Verb(Option<u8>),
46
47    /// Get or set the mute threshold (suppress repeating messages).
48    /// Wire: `mute` / `mute 40`
49    Mute(Option<u32>),
50
51    /// (Windows only) Show network adapter list and routing table.
52    /// Wire: `net`
53    Net,
54
55    // ── Real-time notification control ───────────────────────────
56    /// Control real-time log streaming and/or dump log history.
57    /// Wire: `log on` / `log off` / `log all` / `log on all` / `log 20`
58    Log(StreamMode),
59
60    /// Control real-time echo parameter notifications.
61    /// Wire: `echo on` / `echo off` / `echo all` / `echo on all`
62    Echo(StreamMode),
63
64    /// Enable/disable byte count notifications at N-second intervals.
65    /// Pass 0 to disable.
66    /// Wire: `bytecount 5` / `bytecount 0`
67    ByteCount(u32),
68
69    // ── Connection control ───────────────────────────────────────
70    /// Send a signal to the OpenVPN daemon.
71    /// Wire: `signal SIGUSR1`
72    Signal(Signal),
73
74    /// Kill a specific client connection (server mode).
75    /// Wire: `kill Test-Client` / `kill 1.2.3.4:4000`
76    Kill(KillTarget),
77
78    /// Query the current hold flag.
79    /// Wire: `hold`
80    /// Response: `SUCCESS: hold=0` or `SUCCESS: hold=1`
81    HoldQuery,
82
83    /// Set the hold flag on — future restarts will pause until released.
84    /// Wire: `hold on`
85    HoldOn,
86
87    /// Clear the hold flag.
88    /// Wire: `hold off`
89    HoldOff,
90
91    /// Release from hold state and start OpenVPN. Does not change the
92    /// hold flag itself.
93    /// Wire: `hold release`
94    HoldRelease,
95
96    // ── Authentication ───────────────────────────────────────────
97    /// Supply a username for the given auth type.
98    /// Wire: `username "Auth" myuser`
99    Username {
100        /// Which credential set this username belongs to.
101        auth_type: AuthType,
102        /// The username value.
103        value: String,
104    },
105
106    /// Supply a password for the given auth type. The value is escaped
107    /// and double-quoted per the OpenVPN config-file lexer rules.
108    /// Wire: `password "Private Key" "foo\"bar"`
109    Password {
110        /// Which credential set this password belongs to.
111        auth_type: AuthType,
112        /// The password value (will be escaped on the wire).
113        value: String,
114    },
115
116    /// Set the auth-retry strategy.
117    /// Wire: `auth-retry interact`
118    AuthRetry(AuthRetryMode),
119
120    /// Forget all passwords entered during this management session.
121    /// Wire: `forget-passwords`
122    ForgetPasswords,
123
124    // ── Challenge-response authentication ────────────────────────
125    /// Respond to a CRV1 dynamic challenge.
126    /// Wire: `password "Auth" "CRV1::state_id::response"`
127    ChallengeResponse {
128        /// The opaque state ID from the `>PASSWORD:` CRV1 notification.
129        state_id: String,
130        /// The user's response to the challenge.
131        response: String,
132    },
133
134    /// Respond to a static challenge (SC).
135    /// Wire: `password "Auth" "SCRV1::base64_password::base64_response"`
136    ///
137    /// The caller must pre-encode password and response as base64 —
138    /// this crate does not include a base64 dependency.
139    StaticChallengeResponse {
140        /// Base64-encoded password.
141        password_b64: String,
142        /// Base64-encoded challenge response.
143        response_b64: String,
144    },
145
146    // ── Interactive prompts (OpenVPN 2.1+) ───────────────────────
147    /// Respond to a `>NEED-OK:` prompt.
148    /// Wire: `needok token-insertion-request ok` / `needok ... cancel`
149    NeedOk {
150        /// The prompt name from the `>NEED-OK:` notification.
151        name: String,
152        /// Accept or cancel.
153        response: NeedOkResponse,
154    },
155
156    /// Respond to a `>NEED-STR:` prompt with a string value.
157    /// Wire: `needstr name "John"`
158    NeedStr {
159        /// The prompt name from the `>NEED-STR:` notification.
160        name: String,
161        /// The string value to send (will be escaped on the wire).
162        value: String,
163    },
164
165    // ── PKCS#11 (OpenVPN 2.1+) ──────────────────────────────────
166    /// Query available PKCS#11 certificate count.
167    /// Wire: `pkcs11-id-count`
168    Pkcs11IdCount,
169
170    /// Retrieve a PKCS#11 certificate by index.
171    /// Wire: `pkcs11-id-get 1`
172    Pkcs11IdGet(u32),
173
174    // ── External key / RSA signature (OpenVPN 2.3+) ──────────────
175    /// Provide an RSA signature in response to `>RSA_SIGN:`.
176    /// This is a multi-line command: the encoder writes `rsa-sig`,
177    /// then each base64 line, then `END`.
178    RsaSig {
179        /// Base64-encoded signature lines.
180        base64_lines: Vec<String>,
181    },
182
183    // ── Client management (server mode, OpenVPN 2.1+) ────────────
184    /// Authorize a `>CLIENT:CONNECT` or `>CLIENT:REAUTH` and push config
185    /// directives. Multi-line command: header, config lines, `END`.
186    /// An empty `config_lines` produces a null block (header + immediate END),
187    /// which is equivalent to `client-auth-nt` in effect.
188    ClientAuth {
189        /// Client ID from the `>CLIENT:` notification.
190        cid: u64,
191        /// Key ID from the `>CLIENT:` notification.
192        kid: u64,
193        /// Config directives to push (e.g. `push "route ..."`).
194        config_lines: Vec<String>,
195    },
196
197    /// Authorize a client without pushing any config.
198    /// Wire: `client-auth-nt {CID} {KID}`
199    ClientAuthNt {
200        /// Client ID.
201        cid: u64,
202        /// Key ID.
203        kid: u64,
204    },
205
206    /// Deny a `>CLIENT:CONNECT` or `>CLIENT:REAUTH`.
207    /// Wire: `client-deny {CID} {KID} "reason" ["client-reason"]`
208    ClientDeny {
209        /// Client ID.
210        cid: u64,
211        /// Key ID.
212        kid: u64,
213        /// Server-side reason string (logged but not sent to client).
214        reason: String,
215        /// Optional message sent to the client as part of AUTH_FAILED.
216        client_reason: Option<String>,
217    },
218
219    /// Kill a client session by CID, optionally with a custom message.
220    /// Wire: `client-kill {CID}` or `client-kill {CID} {message}`
221    /// Default message is `RESTART` if omitted.
222    ClientKill {
223        /// Client ID.
224        cid: u64,
225        /// Optional kill message (e.g. `"HALT"`, `"RESTART"`). Defaults to
226        /// `RESTART` on the server if `None`.
227        message: Option<String>,
228    },
229
230    // ── Remote/Proxy override ────────────────────────────────────
231    /// Respond to a `>REMOTE:` notification (requires `--management-query-remote`).
232    /// Wire: `remote ACCEPT` / `remote SKIP` / `remote MOD host port`
233    Remote(RemoteAction),
234
235    /// Respond to a `>PROXY:` notification (requires `--management-query-proxy`).
236    /// Wire: `proxy NONE` / `proxy HTTP host port [nct]` / `proxy SOCKS host port`
237    Proxy(ProxyAction),
238
239    // ── Server statistics ─────────────────────────────────────────
240    /// Request aggregated server stats.
241    /// Wire: `load-stats`
242    /// Response: `SUCCESS: nclients=N,bytesin=N,bytesout=N`
243    LoadStats,
244
245    // ── Extended client management (OpenVPN 2.5+) ────────────────
246    /// Defer authentication for a client, allowing async auth backends.
247    /// Wire: `client-pending-auth {CID} {KID} {EXTRA} {TIMEOUT}`
248    ClientPendingAuth {
249        /// Client ID.
250        cid: u64,
251        /// Key ID.
252        kid: u64,
253        /// Extra opaque string passed to the auth backend.
254        extra: String,
255        /// Timeout in seconds before the pending auth expires.
256        timeout: u32,
257    },
258
259    /// Respond to a CR_TEXT challenge (client-side, OpenVPN 2.6+).
260    /// Wire: `cr-response {base64-response}`
261    CrResponse {
262        /// The base64-encoded challenge-response answer.
263        response: String,
264    },
265
266    // ── External certificate (OpenVPN 2.4+) ──────────────────────
267    /// Supply an external certificate in response to `>NEED-CERTIFICATE`.
268    /// Multi-line command: header, PEM lines, `END`.
269    /// Wire: `certificate\n{pem_lines}\nEND`
270    Certificate {
271        /// PEM-encoded certificate lines.
272        pem_lines: Vec<String>,
273    },
274
275    // ── Management interface authentication ────────────────────────
276    /// Authenticate to the management interface itself. Sent as a bare
277    /// line (no command prefix, no quoting) in response to
278    /// [`crate::OvpnMessage::PasswordPrompt`].
279    /// Wire: `{password}\n`
280    ManagementPassword(String),
281
282    // ── Session lifecycle ────────────────────────────────────────
283    /// Close the management session. OpenVPN keeps running and resumes
284    /// listening for new management connections.
285    Exit,
286
287    /// Identical to `Exit`.
288    Quit,
289
290    // ── Escape hatch ─────────────────────────────────────────────
291    /// Send a raw command string for anything not yet modeled above.
292    /// The decoder expects a `SUCCESS:`/`ERROR:` response.
293    Raw(String),
294
295    /// Send a raw command string, expecting a multi-line (END-terminated)
296    /// response.
297    ///
298    /// Like [`Raw`], the string is sanitized (newlines/NUL stripped)
299    /// before sending. Unlike `Raw`, the decoder accumulates the response
300    /// into [`OvpnMessage::MultiLine`].
301    RawMultiLine(String),
302}
303
304/// What kind of response the decoder should expect after a given command.
305/// This is the core of the command-tracking mechanism that resolves the
306/// protocol's ambiguity around single-line vs. multi-line responses.
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub(crate) enum ResponseKind {
309    /// Expect a `SUCCESS:` or `ERROR:` line.
310    SuccessOrError,
311
312    /// Expect multiple lines terminated by a bare `END`.
313    MultiLine,
314
315    /// No response expected (connection may close).
316    NoResponse,
317}
318
319impl OvpnCommand {
320    /// Determine what kind of response this command produces, so the
321    /// decoder knows how to frame the next incoming bytes.
322    pub(crate) fn expected_response(&self) -> ResponseKind {
323        match self {
324            // These always produce multi-line (END-terminated) responses.
325            Self::Status(_) | Self::Version | Self::Help | Self::Net => ResponseKind::MultiLine,
326
327            // state/log/echo: depends on the specific sub-mode.
328            Self::StateStream(mode) | Self::Log(mode) | Self::Echo(mode) => match mode {
329                StreamMode::All | StreamMode::OnAll | StreamMode::Recent(_) => {
330                    ResponseKind::MultiLine
331                }
332                StreamMode::On | StreamMode::Off => ResponseKind::SuccessOrError,
333            },
334
335            // Bare `state` returns state history (END-terminated).
336            Self::State => ResponseKind::MultiLine,
337
338            // Raw multi-line expects END-terminated response.
339            Self::RawMultiLine(_) => ResponseKind::MultiLine,
340
341            // exit/quit close the connection.
342            Self::Exit | Self::Quit => ResponseKind::NoResponse,
343
344            // Everything else (including Raw) produces SUCCESS: or ERROR:.
345            _ => ResponseKind::SuccessOrError,
346        }
347    }
348}