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. Returns `0` (off) or `1` (on).
79 /// Wire: `hold`
80 HoldQuery,
81
82 /// Set the hold flag on — future restarts will pause until released.
83 /// Wire: `hold on`
84 HoldOn,
85
86 /// Clear the hold flag.
87 /// Wire: `hold off`
88 HoldOff,
89
90 /// Release from hold state and start OpenVPN. Does not change the
91 /// hold flag itself.
92 /// Wire: `hold release`
93 HoldRelease,
94
95 // ── Authentication ───────────────────────────────────────────
96 /// Supply a username for the given auth type.
97 /// Wire: `username "Auth" myuser`
98 Username {
99 /// Which credential set this username belongs to.
100 auth_type: AuthType,
101 /// The username value.
102 value: String,
103 },
104
105 /// Supply a password for the given auth type. The value is escaped
106 /// and double-quoted per the OpenVPN config-file lexer rules.
107 /// Wire: `password "Private Key" "foo\"bar"`
108 Password {
109 /// Which credential set this password belongs to.
110 auth_type: AuthType,
111 /// The password value (will be escaped on the wire).
112 value: String,
113 },
114
115 /// Set the auth-retry strategy.
116 /// Wire: `auth-retry interact`
117 AuthRetry(AuthRetryMode),
118
119 /// Forget all passwords entered during this management session.
120 /// Wire: `forget-passwords`
121 ForgetPasswords,
122
123 // ── Challenge-response authentication ────────────────────────
124 /// Respond to a CRV1 dynamic challenge.
125 /// Wire: `password "Auth" "CRV1::state_id::response"`
126 ChallengeResponse {
127 /// The opaque state ID from the `>PASSWORD:` CRV1 notification.
128 state_id: String,
129 /// The user's response to the challenge.
130 response: String,
131 },
132
133 /// Respond to a static challenge (SC).
134 /// Wire: `password "Auth" "SCRV1::base64_password::base64_response"`
135 ///
136 /// The caller must pre-encode password and response as base64 —
137 /// this crate does not include a base64 dependency.
138 StaticChallengeResponse {
139 /// Base64-encoded password.
140 password_b64: String,
141 /// Base64-encoded challenge response.
142 response_b64: String,
143 },
144
145 // ── Interactive prompts (OpenVPN 2.1+) ───────────────────────
146 /// Respond to a `>NEED-OK:` prompt.
147 /// Wire: `needok token-insertion-request ok` / `needok ... cancel`
148 NeedOk {
149 /// The prompt name from the `>NEED-OK:` notification.
150 name: String,
151 /// Accept or cancel.
152 response: NeedOkResponse,
153 },
154
155 /// Respond to a `>NEED-STR:` prompt with a string value.
156 /// Wire: `needstr name "John"`
157 NeedStr {
158 /// The prompt name from the `>NEED-STR:` notification.
159 name: String,
160 /// The string value to send (will be escaped on the wire).
161 value: String,
162 },
163
164 // ── PKCS#11 (OpenVPN 2.1+) ──────────────────────────────────
165 /// Query available PKCS#11 certificate count.
166 /// Wire: `pkcs11-id-count`
167 Pkcs11IdCount,
168
169 /// Retrieve a PKCS#11 certificate by index.
170 /// Wire: `pkcs11-id-get 1`
171 Pkcs11IdGet(u32),
172
173 // ── External key / RSA signature (OpenVPN 2.3+) ──────────────
174 /// Provide an RSA signature in response to `>RSA_SIGN:`.
175 /// This is a multi-line command: the encoder writes `rsa-sig`,
176 /// then each base64 line, then `END`.
177 RsaSig {
178 /// Base64-encoded signature lines.
179 base64_lines: Vec<String>,
180 },
181
182 // ── Client management (server mode, OpenVPN 2.1+) ────────────
183 /// Authorize a `>CLIENT:CONNECT` or `>CLIENT:REAUTH` and push config
184 /// directives. Multi-line command: header, config lines, `END`.
185 /// An empty `config_lines` produces a null block (header + immediate END),
186 /// which is equivalent to `client-auth-nt` in effect.
187 ClientAuth {
188 /// Client ID from the `>CLIENT:` notification.
189 cid: u64,
190 /// Key ID from the `>CLIENT:` notification.
191 kid: u64,
192 /// Config directives to push (e.g. `push "route ..."`).
193 config_lines: Vec<String>,
194 },
195
196 /// Authorize a client without pushing any config.
197 /// Wire: `client-auth-nt {CID} {KID}`
198 ClientAuthNt {
199 /// Client ID.
200 cid: u64,
201 /// Key ID.
202 kid: u64,
203 },
204
205 /// Deny a `>CLIENT:CONNECT` or `>CLIENT:REAUTH`.
206 /// Wire: `client-deny {CID} {KID} "reason" ["client-reason"]`
207 ClientDeny {
208 /// Client ID.
209 cid: u64,
210 /// Key ID.
211 kid: u64,
212 /// Server-side reason string (logged but not sent to client).
213 reason: String,
214 /// Optional message sent to the client as part of AUTH_FAILED.
215 client_reason: Option<String>,
216 },
217
218 /// Immediately kill a client session by CID.
219 /// Wire: `client-kill {CID}`
220 ClientKill {
221 /// Client ID.
222 cid: u64,
223 },
224
225 /// Push a packet filter to a specific client. Multi-line command:
226 /// header, filter block, `END`. Requires `--management-client-pf`.
227 ClientPf {
228 /// Client ID.
229 cid: u64,
230 /// Packet filter rules (e.g. `[CLIENTS ACCEPT]`, `+10.0.0.0/8`).
231 filter_lines: Vec<String>,
232 },
233
234 // ── Remote/Proxy override ────────────────────────────────────
235 /// Respond to a `>REMOTE:` notification (requires `--management-query-remote`).
236 /// Wire: `remote ACCEPT` / `remote SKIP` / `remote MOD host port`
237 Remote(RemoteAction),
238
239 /// Respond to a `>PROXY:` notification (requires `--management-query-proxy`).
240 /// Wire: `proxy NONE` / `proxy HTTP host port [nct]` / `proxy SOCKS host port`
241 Proxy(ProxyAction),
242
243 // ── Server statistics ─────────────────────────────────────────
244 /// Request aggregated server stats.
245 /// Wire: `load-stats`
246 /// Response: `SUCCESS: nclients=N,bytesin=N,bytesout=N`
247 LoadStats,
248
249 // ── Extended client management (OpenVPN 2.5+) ────────────────
250 /// Defer authentication for a client, allowing async auth backends.
251 /// Wire: `client-pending-auth {CID} {KID} {TIMEOUT} {EXTRA}`
252 ClientPendingAuth {
253 /// Client ID.
254 cid: u64,
255 /// Key ID.
256 kid: u64,
257 /// Timeout in seconds before the pending auth expires.
258 timeout: u32,
259 /// Extra opaque string passed to the auth backend.
260 extra: String,
261 },
262
263 /// Extended deny with optional redirect URL (OpenVPN 2.5+).
264 /// Wire: `client-deny-v2 {CID} {KID} "reason" ["client-reason"] ["redirect-url"]`
265 ClientDenyV2 {
266 /// Client ID.
267 cid: u64,
268 /// Key ID.
269 kid: u64,
270 /// Server-side reason string.
271 reason: String,
272 /// Optional message sent to the client.
273 client_reason: Option<String>,
274 /// Optional URL the client should be redirected to.
275 redirect_url: Option<String>,
276 },
277
278 /// Respond to a challenge-response authentication (OpenVPN 2.5+).
279 /// Wire: `cr-response {CID} {KID} {RESPONSE}`
280 CrResponse {
281 /// Client ID.
282 cid: u64,
283 /// Key ID.
284 kid: u64,
285 /// The challenge-response answer.
286 response: String,
287 },
288
289 // ── External certificate (OpenVPN 2.4+) ──────────────────────
290 /// Supply an external certificate in response to `>NEED-CERTIFICATE`.
291 /// Multi-line command: header, PEM lines, `END`.
292 /// Wire: `certificate\n{pem_lines}\nEND`
293 Certificate {
294 /// PEM-encoded certificate lines.
295 pem_lines: Vec<String>,
296 },
297
298 // ── Windows service bypass message ──────────────────────────
299 /// (Windows only) Send a bypass message to the OpenVPN service.
300 /// Wire: `bypass-message "message"`
301 BypassMessage(String),
302
303 // ── Management interface authentication ────────────────────────
304 /// Authenticate to the management interface itself. Sent as a bare
305 /// line (no command prefix, no quoting) in response to
306 /// [`crate::OvpnMessage::PasswordPrompt`].
307 /// Wire: `{password}\n`
308 ManagementPassword(String),
309
310 // ── Session lifecycle ────────────────────────────────────────
311 /// Close the management session. OpenVPN keeps running and resumes
312 /// listening for new management connections.
313 Exit,
314
315 /// Identical to `Exit`.
316 Quit,
317
318 // ── Escape hatch ─────────────────────────────────────────────
319 /// Send a raw command string for anything not yet modeled above.
320 /// The decoder expects a `SUCCESS:`/`ERROR:` response.
321 Raw(String),
322
323 /// Send a raw command string, expecting a multi-line (END-terminated)
324 /// response.
325 ///
326 /// Like [`Raw`], the string is sanitized (newlines/NUL stripped)
327 /// before sending. Unlike `Raw`, the decoder accumulates the response
328 /// into [`OvpnMessage::MultiLine`].
329 RawMultiLine(String),
330}
331
332/// What kind of response the decoder should expect after a given command.
333/// This is the core of the command-tracking mechanism that resolves the
334/// protocol's ambiguity around single-line vs. multi-line responses.
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336pub(crate) enum ResponseKind {
337 /// Expect a `SUCCESS:` or `ERROR:` line.
338 SuccessOrError,
339
340 /// Expect multiple lines terminated by a bare `END`.
341 MultiLine,
342
343 /// Expect a single non-SUCCESS/ERROR value line (e.g. bare `hold` → "0").
344 SingleValue,
345
346 /// No response expected (connection may close).
347 NoResponse,
348}
349
350impl OvpnCommand {
351 /// Determine what kind of response this command produces, so the
352 /// decoder knows how to frame the next incoming bytes.
353 pub(crate) fn expected_response(&self) -> ResponseKind {
354 match self {
355 // These always produce multi-line (END-terminated) responses.
356 Self::Status(_) | Self::Version | Self::Help | Self::Net => ResponseKind::MultiLine,
357
358 // state/log/echo: depends on the specific sub-mode.
359 Self::StateStream(mode) | Self::Log(mode) | Self::Echo(mode) => match mode {
360 StreamMode::All | StreamMode::OnAll | StreamMode::Recent(_) => {
361 ResponseKind::MultiLine
362 }
363 StreamMode::On | StreamMode::Off => ResponseKind::SuccessOrError,
364 },
365
366 // Bare `state` returns a single comma-delimited state line.
367 Self::State => ResponseKind::SingleValue,
368
369 // Bare `hold` returns "0" or "1".
370 Self::HoldQuery => ResponseKind::SingleValue,
371
372 // `pkcs11-id-get N` returns a single PKCS11ID-ENTRY line.
373 Self::Pkcs11IdGet(_) => ResponseKind::SingleValue,
374
375 // Raw multi-line expects END-terminated response.
376 Self::RawMultiLine(_) => ResponseKind::MultiLine,
377
378 // exit/quit close the connection.
379 Self::Exit | Self::Quit => ResponseKind::NoResponse,
380
381 // Everything else (including Raw) produces SUCCESS: or ERROR:.
382 _ => ResponseKind::SuccessOrError,
383 }
384 }
385}