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}