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. 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}