Skip to main content

openvpn_mgmt_codec/
client.rs

1//! High-level management client with notification dispatch.
2//!
3//! [`ManagementClient`] wraps a `Framed<T, OvpnCodec>` transport and splits
4//! the multiplexed stream into two independent channels:
5//!
6//! - **Command methods** (`version`, `status`, `hold_release`, etc.) send a
7//!   command and return its response directly.
8//! - **Notifications** are forwarded to a [`tokio::sync::broadcast`] channel
9//!   that any number of subscribers can consume independently.
10//!
11//! # Example
12//!
13//! ```no_run
14//! use tokio::net::TcpStream;
15//! use tokio::sync::broadcast;
16//! use tokio_util::codec::Framed;
17//! use openvpn_mgmt_codec::{Notification, OvpnCodec, StatusFormat};
18//! use openvpn_mgmt_codec::client::ManagementClient;
19//!
20//! # async fn example() -> anyhow::Result<()> {
21//! let stream = TcpStream::connect("127.0.0.1:7505").await?;
22//! let framed = Framed::new(stream, OvpnCodec::new());
23//!
24//! // Create the broadcast channel — you control capacity and lifetime.
25//! let (notification_tx, _) = broadcast::channel::<Notification>(256);
26//! let mut rx = notification_tx.subscribe();
27//! let mut client = ManagementClient::new(framed, notification_tx);
28//!
29//! // Spawn a notification consumer
30//! tokio::spawn(async move {
31//!     while let Ok(notif) = rx.recv().await {
32//!         println!("notification: {notif:?}");
33//!     }
34//! });
35//!
36//! // Commands return their response directly
37//! let version = client.version().await?;
38//! println!("management version: {:?}", version.management_version());
39//!
40//! let status = client.status(StatusFormat::V3).await?;
41//! client.hold_release().await?;
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! # Extracting the transport
47//!
48//! Call [`ManagementClient::into_framed`] to recover the underlying
49//! `Framed<T, OvpnCodec>` when you need raw access or want to drop back
50//! to the low-level stream API.
51
52use std::io;
53
54use futures_util::{SinkExt, StreamExt};
55use tokio::sync::broadcast;
56use tokio_util::codec::Framed;
57
58use crate::auth::{AuthRetryMode, AuthType};
59use crate::client_deny::ClientDeny;
60use crate::codec::OvpnCodec;
61use crate::command::{OvpnCommand, RemoteEntryRange};
62use crate::kill_target::KillTarget;
63use crate::message::{Notification, OvpnMessage};
64use crate::need_ok::NeedOkResponse;
65use crate::parsed_response::{self, LoadStats, StateEntry};
66use crate::proxy_action::ProxyAction;
67use crate::redacted::Redacted;
68use crate::remote_action::RemoteAction;
69use crate::signal::Signal;
70use crate::status::{self, ClientStatistics, StatusResponse};
71use crate::status_format::StatusFormat;
72use crate::stream_mode::StreamMode;
73use crate::version_info::VersionInfo;
74
75/// Errors returned by [`ManagementClient`] command methods.
76#[derive(Debug, thiserror::Error)]
77pub enum ClientError {
78    /// The transport returned an I/O error.
79    #[error("transport error: {0}")]
80    Io(#[from] io::Error),
81
82    /// The connection was closed before a response arrived.
83    #[error("connection closed while awaiting response")]
84    ConnectionClosed,
85
86    /// The server returned `ERROR: {0}`.
87    #[error("server error: {0}")]
88    ServerError(String),
89
90    /// The response type did not match what the command expected.
91    #[error("unexpected response: {0:?}")]
92    UnexpectedResponse(OvpnMessage),
93
94    /// A `SUCCESS:` payload could not be parsed.
95    #[error("response parse error: {0}")]
96    ParseResponse(#[from] parsed_response::ParseResponseError),
97
98    /// A `status` response could not be parsed.
99    #[error("status parse error: {0}")]
100    ParseStatus(#[from] status::ParseStatusError),
101}
102
103/// A high-level client for the OpenVPN management interface.
104///
105/// See the [module documentation](self) for usage examples.
106pub struct ManagementClient<T> {
107    framed: Framed<T, OvpnCodec>,
108    notification_tx: broadcast::Sender<Notification>,
109}
110
111impl<T> ManagementClient<T>
112where
113    T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
114{
115    /// Wrap a framed transport with an existing broadcast sender for
116    /// notification dispatch.
117    ///
118    /// The caller creates the [`broadcast::channel`] and passes the sender
119    /// here. This gives full control over channel capacity and lifetime.
120    /// Call [`broadcast::Sender::subscribe`] on your copy of the sender to
121    /// create receivers — multiple independent subscribers are supported.
122    pub fn new(
123        framed: Framed<T, OvpnCodec>,
124        notification_tx: broadcast::Sender<Notification>,
125    ) -> Self {
126        Self {
127            framed,
128            notification_tx,
129        }
130    }
131
132    /// Recover the underlying framed transport.
133    pub fn into_framed(self) -> Framed<T, OvpnCodec> {
134        self.framed
135    }
136
137    // --- Internal helpers ---
138
139    /// Read frames until a non-notification message arrives. Interleaved
140    /// notifications are forwarded to the broadcast channel.
141    async fn recv_response(&mut self) -> Result<OvpnMessage, ClientError> {
142        loop {
143            let msg = self
144                .framed
145                .next()
146                .await
147                .ok_or(ClientError::ConnectionClosed)??;
148
149            match msg {
150                OvpnMessage::Notification(notification) => {
151                    // No active receivers is fine — notifications are best-effort.
152                    self.notification_tx
153                        .send(notification)
154                        .inspect_err(|error| {
155                            tracing::debug!(%error, "no notification subscribers");
156                        })
157                        .ok();
158                }
159                other => return Ok(other),
160            }
161        }
162    }
163
164    /// Send a command and read frames until a non-notification response
165    /// arrives.
166    async fn send_and_recv(&mut self, cmd: OvpnCommand) -> Result<OvpnMessage, ClientError> {
167        self.framed.send(cmd).await?;
168        self.recv_response().await
169    }
170
171    /// Send a command that expects `SUCCESS:` and return the payload string.
172    async fn send_expect_success(&mut self, cmd: OvpnCommand) -> Result<String, ClientError> {
173        match self.send_and_recv(cmd).await? {
174            OvpnMessage::Success(payload) => Ok(payload),
175            OvpnMessage::Error(msg) => Err(ClientError::ServerError(msg)),
176            other => Err(ClientError::UnexpectedResponse(other)),
177        }
178    }
179
180    /// Send a command that expects a multi-line response.
181    async fn send_expect_multi_line(
182        &mut self,
183        cmd: OvpnCommand,
184    ) -> Result<Vec<String>, ClientError> {
185        match self.send_and_recv(cmd).await? {
186            OvpnMessage::MultiLine(lines) => Ok(lines),
187            OvpnMessage::Error(msg) => Err(ClientError::ServerError(msg)),
188            other => Err(ClientError::UnexpectedResponse(other)),
189        }
190    }
191
192    /// Send a command that expects `SUCCESS:` and discard the payload.
193    async fn send_expect_ok(&mut self, cmd: OvpnCommand) -> Result<(), ClientError> {
194        self.send_expect_success(cmd).await?;
195        Ok(())
196    }
197
198    /// Send a stream-mode command (`log`, `state`, `echo`).
199    ///
200    /// History-returning modes produce `Some(lines)`, on/off modes
201    /// produce `None`.
202    async fn send_stream_command(
203        &mut self,
204        mode: StreamMode,
205        cmd: OvpnCommand,
206    ) -> Result<Option<Vec<String>>, ClientError> {
207        if mode.returns_history() {
208            Ok(Some(self.send_expect_multi_line(cmd).await?))
209        } else {
210            self.send_expect_ok(cmd).await?;
211            Ok(None)
212        }
213    }
214
215    // --- Public command methods ---
216
217    // -- Informational --
218
219    /// Query the connection status in the given format.
220    ///
221    /// Returns the raw multi-line response. Use [`status`](Self::status)
222    /// for a typed result.
223    pub async fn status_raw(&mut self, format: StatusFormat) -> Result<Vec<String>, ClientError> {
224        self.send_expect_multi_line(OvpnCommand::Status(format))
225            .await
226    }
227
228    /// Query and parse the server-mode connection status.
229    pub async fn status(&mut self, format: StatusFormat) -> Result<StatusResponse, ClientError> {
230        let lines = self.status_raw(format).await?;
231        Ok(status::parse_status(&lines)?)
232    }
233
234    /// Query and parse client-mode statistics.
235    pub async fn client_statistics(
236        &mut self,
237        format: StatusFormat,
238    ) -> Result<ClientStatistics, ClientError> {
239        let lines = self.status_raw(format).await?;
240        Ok(status::parse_client_statistics(&lines)?)
241    }
242
243    /// Query the current state as a multi-line history.
244    pub async fn state(&mut self) -> Result<Vec<StateEntry>, ClientError> {
245        let lines = self.send_expect_multi_line(OvpnCommand::State).await?;
246        Ok(parsed_response::parse_state_history(&lines)?)
247    }
248
249    /// Query the most recent state entry.
250    pub async fn current_state(&mut self) -> Result<StateEntry, ClientError> {
251        let lines = self.send_expect_multi_line(OvpnCommand::State).await?;
252        Ok(parsed_response::parse_current_state(&lines)?)
253    }
254
255    /// Control real-time state notifications.
256    ///
257    /// Streaming modes (`All`, `OnAll`, `Recent`) return accumulated history
258    /// lines. `On`/`Off` return `Ok(None)`.
259    pub async fn state_stream(
260        &mut self,
261        mode: StreamMode,
262    ) -> Result<Option<Vec<StateEntry>>, ClientError> {
263        match self
264            .send_stream_command(mode, OvpnCommand::StateStream(mode))
265            .await?
266        {
267            Some(lines) => Ok(Some(parsed_response::parse_state_history(&lines)?)),
268            None => Ok(None),
269        }
270    }
271
272    /// Query the OpenVPN and management interface version.
273    pub async fn version(&mut self) -> Result<VersionInfo, ClientError> {
274        let lines = self.send_expect_multi_line(OvpnCommand::Version).await?;
275        Ok(parsed_response::parse_version(&lines))
276    }
277
278    /// Set the management client version to announce feature support.
279    ///
280    /// For versions < 4 this produces no response from the server.
281    /// For versions >= 4 a `SUCCESS:` response is expected.
282    pub async fn set_version(&mut self, version: u32) -> Result<(), ClientError> {
283        let cmd = OvpnCommand::SetVersion(version);
284        if version < 4 {
285            self.framed.send(cmd).await?;
286            Ok(())
287        } else {
288            self.send_expect_ok(cmd).await
289        }
290    }
291
292    /// Query the PID of the OpenVPN process.
293    pub async fn pid(&mut self) -> Result<u32, ClientError> {
294        let payload = self.send_expect_success(OvpnCommand::Pid).await?;
295        Ok(parsed_response::parse_pid(&payload)?)
296    }
297
298    /// List available management commands.
299    pub async fn help(&mut self) -> Result<Vec<String>, ClientError> {
300        self.send_expect_multi_line(OvpnCommand::Help).await
301    }
302
303    /// Query or set the log verbosity level.
304    pub async fn verb(&mut self, level: Option<u8>) -> Result<String, ClientError> {
305        self.send_expect_success(OvpnCommand::Verb(level)).await
306    }
307
308    /// Query or set the mute threshold.
309    pub async fn mute(&mut self, threshold: Option<u32>) -> Result<String, ClientError> {
310        self.send_expect_success(OvpnCommand::Mute(threshold)).await
311    }
312
313    /// (Windows) Show network adapter list.
314    pub async fn net(&mut self) -> Result<Vec<String>, ClientError> {
315        self.send_expect_multi_line(OvpnCommand::Net).await
316    }
317
318    // -- Notification control --
319
320    /// Control real-time log streaming.
321    ///
322    /// Streaming modes return accumulated log history. `On`/`Off` return `Ok(None)`.
323    pub async fn log(&mut self, mode: StreamMode) -> Result<Option<Vec<String>>, ClientError> {
324        self.send_stream_command(mode, OvpnCommand::Log(mode)).await
325    }
326
327    /// Control real-time echo notifications.
328    pub async fn echo(&mut self, mode: StreamMode) -> Result<Option<Vec<String>>, ClientError> {
329        self.send_stream_command(mode, OvpnCommand::Echo(mode))
330            .await
331    }
332
333    /// Enable or disable byte count notifications at N-second intervals.
334    /// Pass 0 to disable.
335    pub async fn bytecount(&mut self, interval: u32) -> Result<(), ClientError> {
336        self.send_expect_ok(OvpnCommand::ByteCount(interval)).await
337    }
338
339    // -- Connection control --
340
341    /// Send a signal to the OpenVPN daemon.
342    pub async fn signal(&mut self, signal: Signal) -> Result<(), ClientError> {
343        self.send_expect_ok(OvpnCommand::Signal(signal)).await
344    }
345
346    /// Kill a specific client connection (server mode).
347    pub async fn kill(&mut self, target: KillTarget) -> Result<(), ClientError> {
348        self.send_expect_ok(OvpnCommand::Kill(target)).await
349    }
350
351    /// Query the current hold flag.
352    pub async fn hold_query(&mut self) -> Result<bool, ClientError> {
353        let payload = self.send_expect_success(OvpnCommand::HoldQuery).await?;
354        Ok(parsed_response::parse_hold(&payload)?)
355    }
356
357    /// Set the hold flag on.
358    pub async fn hold_on(&mut self) -> Result<(), ClientError> {
359        self.send_expect_ok(OvpnCommand::HoldOn).await
360    }
361
362    /// Clear the hold flag.
363    pub async fn hold_off(&mut self) -> Result<(), ClientError> {
364        self.send_expect_ok(OvpnCommand::HoldOff).await
365    }
366
367    /// Release from hold state and start OpenVPN.
368    pub async fn hold_release(&mut self) -> Result<(), ClientError> {
369        self.send_expect_ok(OvpnCommand::HoldRelease).await
370    }
371
372    // -- Authentication --
373
374    /// Supply a username for the given auth type.
375    pub async fn username(
376        &mut self,
377        auth_type: AuthType,
378        value: impl Into<String>,
379    ) -> Result<(), ClientError> {
380        self.send_expect_ok(OvpnCommand::Username {
381            auth_type,
382            value: Redacted::new(value.into()),
383        })
384        .await
385    }
386
387    /// Supply a password for the given auth type.
388    pub async fn password(
389        &mut self,
390        auth_type: AuthType,
391        value: impl Into<String>,
392    ) -> Result<(), ClientError> {
393        self.send_expect_ok(OvpnCommand::Password {
394            auth_type,
395            value: Redacted::new(value.into()),
396        })
397        .await
398    }
399
400    /// Set the auth-retry strategy.
401    pub async fn auth_retry(&mut self, mode: AuthRetryMode) -> Result<(), ClientError> {
402        self.send_expect_ok(OvpnCommand::AuthRetry(mode)).await
403    }
404
405    /// Forget all passwords entered during this management session.
406    pub async fn forget_passwords(&mut self) -> Result<(), ClientError> {
407        self.send_expect_ok(OvpnCommand::ForgetPasswords).await
408    }
409
410    /// Respond to a CRV1 dynamic challenge.
411    pub async fn challenge_response(
412        &mut self,
413        state_id: impl Into<String>,
414        response: impl Into<String>,
415    ) -> Result<(), ClientError> {
416        self.send_expect_ok(OvpnCommand::ChallengeResponse {
417            state_id: state_id.into(),
418            response: Redacted::new(response.into()),
419        })
420        .await
421    }
422
423    /// Respond to a static challenge.
424    pub async fn static_challenge_response(
425        &mut self,
426        password_b64: impl Into<String>,
427        response_b64: impl Into<String>,
428    ) -> Result<(), ClientError> {
429        self.send_expect_ok(OvpnCommand::StaticChallengeResponse {
430            password_b64: Redacted::new(password_b64.into()),
431            response_b64: Redacted::new(response_b64.into()),
432        })
433        .await
434    }
435
436    /// Respond to a CR_TEXT challenge.
437    pub async fn cr_response(&mut self, response: impl Into<String>) -> Result<(), ClientError> {
438        self.send_expect_ok(OvpnCommand::CrResponse {
439            response: Redacted::new(response.into()),
440        })
441        .await
442    }
443
444    // -- Interactive prompts --
445
446    /// Respond to a `>NEED-OK:` prompt.
447    pub async fn need_ok(
448        &mut self,
449        name: impl Into<String>,
450        response: NeedOkResponse,
451    ) -> Result<(), ClientError> {
452        self.send_expect_ok(OvpnCommand::NeedOk {
453            name: name.into(),
454            response,
455        })
456        .await
457    }
458
459    /// Respond to a `>NEED-STR:` prompt.
460    pub async fn need_str(
461        &mut self,
462        name: impl Into<String>,
463        value: impl Into<String>,
464    ) -> Result<(), ClientError> {
465        self.send_expect_ok(OvpnCommand::NeedStr {
466            name: name.into(),
467            value: value.into(),
468        })
469        .await
470    }
471
472    // -- PKCS#11 --
473
474    /// Query available PKCS#11 certificate count.
475    pub async fn pkcs11_id_count(&mut self) -> Result<String, ClientError> {
476        self.send_expect_success(OvpnCommand::Pkcs11IdCount).await
477    }
478
479    /// Retrieve a PKCS#11 certificate by index.
480    pub async fn pkcs11_id_get(&mut self, index: u32) -> Result<String, ClientError> {
481        self.send_expect_success(OvpnCommand::Pkcs11IdGet(index))
482            .await
483    }
484
485    // -- External key / signatures --
486
487    /// Provide an RSA signature in response to `>RSA_SIGN:`.
488    pub async fn rsa_sig(&mut self, base64_lines: Vec<String>) -> Result<(), ClientError> {
489        self.send_expect_ok(OvpnCommand::RsaSig { base64_lines })
490            .await
491    }
492
493    /// Provide a signature in response to `>PK_SIGN:`.
494    pub async fn pk_sig(&mut self, base64_lines: Vec<String>) -> Result<(), ClientError> {
495        self.send_expect_ok(OvpnCommand::PkSig { base64_lines })
496            .await
497    }
498
499    /// Supply an external certificate in response to `>NEED-CERTIFICATE:`.
500    pub async fn certificate(&mut self, pem_lines: Vec<String>) -> Result<(), ClientError> {
501        self.send_expect_ok(OvpnCommand::Certificate { pem_lines })
502            .await
503    }
504
505    // -- Client management (server mode) --
506
507    /// Authorize a client and push config directives.
508    pub async fn client_auth(
509        &mut self,
510        cid: u64,
511        kid: u64,
512        config_lines: Vec<String>,
513    ) -> Result<(), ClientError> {
514        self.send_expect_ok(OvpnCommand::ClientAuth {
515            cid,
516            kid,
517            config_lines,
518        })
519        .await
520    }
521
522    /// Authorize a client without pushing any config.
523    pub async fn client_auth_nt(&mut self, cid: u64, kid: u64) -> Result<(), ClientError> {
524        self.send_expect_ok(OvpnCommand::ClientAuthNt { cid, kid })
525            .await
526    }
527
528    /// Deny a client connection.
529    pub async fn client_deny(&mut self, deny: ClientDeny) -> Result<(), ClientError> {
530        self.send_expect_ok(OvpnCommand::ClientDeny(deny)).await
531    }
532
533    /// Kill a client session by CID.
534    pub async fn client_kill(
535        &mut self,
536        cid: u64,
537        message: Option<String>,
538    ) -> Result<(), ClientError> {
539        self.send_expect_ok(OvpnCommand::ClientKill { cid, message })
540            .await
541    }
542
543    /// Defer authentication for a client.
544    pub async fn client_pending_auth(
545        &mut self,
546        cid: u64,
547        kid: u64,
548        extra: impl Into<String>,
549        timeout: u32,
550    ) -> Result<(), ClientError> {
551        self.send_expect_ok(OvpnCommand::ClientPendingAuth {
552            cid,
553            kid,
554            extra: extra.into(),
555            timeout,
556        })
557        .await
558    }
559
560    // -- Remote / Proxy override --
561
562    /// Respond to a `>REMOTE:` notification.
563    pub async fn remote(&mut self, action: RemoteAction) -> Result<(), ClientError> {
564        self.send_expect_ok(OvpnCommand::Remote(action)).await
565    }
566
567    /// Respond to a `>PROXY:` notification.
568    pub async fn proxy(&mut self, action: ProxyAction) -> Result<(), ClientError> {
569        self.send_expect_ok(OvpnCommand::Proxy(action)).await
570    }
571
572    // -- Server statistics --
573
574    /// Request aggregated server stats.
575    pub async fn load_stats(&mut self) -> Result<LoadStats, ClientError> {
576        let payload = self.send_expect_success(OvpnCommand::LoadStats).await?;
577        Ok(parsed_response::parse_load_stats(&payload)?)
578    }
579
580    // -- ENV filter --
581
582    /// Set the env-var filter level for `>CLIENT:ENV` blocks.
583    pub async fn env_filter(&mut self, level: u32) -> Result<(), ClientError> {
584        self.send_expect_ok(OvpnCommand::EnvFilter(level)).await
585    }
586
587    // -- Remote entry queries --
588
589    /// Query the number of `--remote` entries.
590    pub async fn remote_entry_count(&mut self) -> Result<Vec<String>, ClientError> {
591        self.send_expect_multi_line(OvpnCommand::RemoteEntryCount)
592            .await
593    }
594
595    /// Retrieve `--remote` entries.
596    pub async fn remote_entry_get(
597        &mut self,
598        range: RemoteEntryRange,
599    ) -> Result<Vec<String>, ClientError> {
600        self.send_expect_multi_line(OvpnCommand::RemoteEntryGet(range))
601            .await
602    }
603
604    // -- Push updates (server mode) --
605
606    /// Broadcast a push option update to all connected clients.
607    pub async fn push_update_broad(
608        &mut self,
609        options: impl Into<String>,
610    ) -> Result<(), ClientError> {
611        self.send_expect_ok(OvpnCommand::PushUpdateBroad {
612            options: options.into(),
613        })
614        .await
615    }
616
617    /// Push an option update to a specific client.
618    pub async fn push_update_cid(
619        &mut self,
620        cid: u64,
621        options: impl Into<String>,
622    ) -> Result<(), ClientError> {
623        self.send_expect_ok(OvpnCommand::PushUpdateCid {
624            cid,
625            options: options.into(),
626        })
627        .await
628    }
629
630    // -- Management interface auth --
631
632    /// Authenticate to the management interface.
633    pub async fn management_password(
634        &mut self,
635        password: impl Into<String>,
636    ) -> Result<(), ClientError> {
637        self.send_expect_ok(OvpnCommand::ManagementPassword(Redacted::new(
638            password.into(),
639        )))
640        .await
641    }
642
643    // -- Session lifecycle --
644
645    /// Close the management session. Consumes the client since the
646    /// connection is no longer usable.
647    pub async fn exit(mut self) -> Result<(), ClientError> {
648        self.framed.send(OvpnCommand::Exit).await?;
649        Ok(())
650    }
651
652    // -- Raw escape hatch --
653
654    /// Send a raw command expecting `SUCCESS:`/`ERROR:`.
655    pub async fn raw(&mut self, command: impl Into<String>) -> Result<String, ClientError> {
656        self.send_expect_success(OvpnCommand::Raw(command.into()))
657            .await
658    }
659
660    /// Send a raw command expecting a multi-line response.
661    pub async fn raw_multi_line(
662        &mut self,
663        command: impl Into<String>,
664    ) -> Result<Vec<String>, ClientError> {
665        self.send_expect_multi_line(OvpnCommand::RawMultiLine(command.into()))
666            .await
667    }
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673    use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream};
674
675    /// Create a client connected to a mock transport. Returns the client,
676    /// the notification sender, and the server-side of the duplex stream.
677    fn mock_client() -> (
678        ManagementClient<DuplexStream>,
679        broadcast::Sender<Notification>,
680        DuplexStream,
681    ) {
682        let (client_stream, server_stream) = tokio::io::duplex(4096);
683        let framed = Framed::new(client_stream, OvpnCodec::new());
684        let (notification_tx, _) = broadcast::channel(64);
685        let client = ManagementClient::new(framed, notification_tx.clone());
686        (client, notification_tx, server_stream)
687    }
688
689    /// Write a sequence of lines to the server side and close it.
690    async fn server_respond(server: &mut DuplexStream, lines: &[&str]) {
691        for line in lines {
692            server.write_all(line.as_bytes()).await.unwrap();
693            server.write_all(b"\r\n").await.unwrap();
694        }
695    }
696
697    #[tokio::test]
698    async fn pid_returns_parsed_value() {
699        let (mut client, _notif, mut server) = mock_client();
700
701        let handle = tokio::spawn(async move {
702            let mut buf = vec![0u8; 64];
703            let _n = server.read(&mut buf).await.unwrap();
704            server_respond(&mut server, &["SUCCESS: pid=42"]).await;
705            server
706        });
707
708        let pid = client.pid().await.unwrap();
709        assert_eq!(pid, 42);
710        handle.await.unwrap();
711    }
712
713    #[tokio::test]
714    async fn notifications_forwarded_during_command() {
715        let (mut client, notif_tx, mut server) = mock_client();
716        let mut rx = notif_tx.subscribe();
717
718        let handle = tokio::spawn(async move {
719            let mut buf = vec![0u8; 64];
720            let _n = server.read(&mut buf).await.unwrap();
721            // Server sends a notification interleaved with the response.
722            server_respond(&mut server, &[">BYTECOUNT:1024,2048", "SUCCESS: pid=99"]).await;
723            server
724        });
725
726        let pid = client.pid().await.unwrap();
727        assert_eq!(pid, 99);
728
729        // The notification was forwarded to the broadcast channel.
730        let notif = rx.try_recv().unwrap();
731        assert!(
732            matches!(
733                notif,
734                Notification::ByteCount {
735                    bytes_in: 1024,
736                    bytes_out: 2048
737                }
738            ),
739            "expected ByteCount, got {notif:?}"
740        );
741
742        handle.await.unwrap();
743    }
744
745    #[tokio::test]
746    async fn server_error_maps_to_client_error() {
747        let (mut client, _notif, mut server) = mock_client();
748
749        let handle = tokio::spawn(async move {
750            let mut buf = vec![0u8; 64];
751            let _n = server.read(&mut buf).await.unwrap();
752            server_respond(&mut server, &["ERROR: command not allowed"]).await;
753            server
754        });
755
756        let err = client.hold_release().await.unwrap_err();
757        assert!(
758            matches!(&err, ClientError::ServerError(msg) if msg == "command not allowed"),
759            "expected ServerError, got {err:?}"
760        );
761
762        handle.await.unwrap();
763    }
764
765    #[tokio::test]
766    async fn version_returns_parsed_info() {
767        let (mut client, _notif, mut server) = mock_client();
768
769        let handle = tokio::spawn(async move {
770            let mut buf = vec![0u8; 64];
771            let _n = server.read(&mut buf).await.unwrap();
772            server_respond(
773                &mut server,
774                &[
775                    "OpenVPN Version: OpenVPN 2.6.9 x86_64-pc-linux-gnu",
776                    "Management Interface Version: 5",
777                    "END",
778                ],
779            )
780            .await;
781            server
782        });
783
784        let info = client.version().await.unwrap();
785        assert_eq!(info.management_version(), Some(5));
786        assert!(info.openvpn_version_line().unwrap().contains("2.6.9"));
787
788        handle.await.unwrap();
789    }
790
791    #[tokio::test]
792    async fn connection_closed_returns_error() {
793        let (mut client, _notif, server) = mock_client();
794
795        // Drop the server side immediately.
796        drop(server);
797
798        let err = client.pid().await.unwrap_err();
799        // Could be ConnectionClosed or Io depending on timing, both are acceptable.
800        assert!(
801            matches!(&err, ClientError::ConnectionClosed | ClientError::Io(_)),
802            "expected connection error, got {err:?}"
803        );
804    }
805
806    #[tokio::test]
807    async fn multiple_notification_subscribers() {
808        let (mut client, notif_tx, mut server) = mock_client();
809        let mut rx1 = notif_tx.subscribe();
810        let mut rx2 = notif_tx.subscribe();
811
812        let handle = tokio::spawn(async move {
813            let mut buf = vec![0u8; 64];
814            let _n = server.read(&mut buf).await.unwrap();
815            server_respond(
816                &mut server,
817                &[">HOLD:Waiting for hold release:5", "SUCCESS: pid=1"],
818            )
819            .await;
820            server
821        });
822
823        let pid = client.pid().await.unwrap();
824        assert_eq!(pid, 1);
825
826        // Both subscribers received the notification.
827        let n1 = rx1.try_recv().unwrap();
828        let n2 = rx2.try_recv().unwrap();
829        assert!(matches!(n1, Notification::Hold { .. }));
830        assert!(matches!(n2, Notification::Hold { .. }));
831
832        handle.await.unwrap();
833    }
834
835    #[tokio::test]
836    async fn load_stats_parsed() {
837        let (mut client, _notif, mut server) = mock_client();
838
839        let handle = tokio::spawn(async move {
840            let mut buf = vec![0u8; 64];
841            let _n = server.read(&mut buf).await.unwrap();
842            server_respond(
843                &mut server,
844                &["SUCCESS: nclients=3,bytesin=100000,bytesout=50000"],
845            )
846            .await;
847            server
848        });
849
850        let stats = client.load_stats().await.unwrap();
851        assert_eq!(stats.nclients, 3);
852        assert_eq!(stats.bytesin, 100_000);
853        assert_eq!(stats.bytesout, 50_000);
854
855        handle.await.unwrap();
856    }
857
858    #[tokio::test]
859    async fn hold_query_parsed() {
860        let (mut client, _notif, mut server) = mock_client();
861
862        let handle = tokio::spawn(async move {
863            let mut buf = vec![0u8; 64];
864            let _n = server.read(&mut buf).await.unwrap();
865            server_respond(&mut server, &["SUCCESS: hold=1"]).await;
866            server
867        });
868
869        assert!(client.hold_query().await.unwrap());
870
871        handle.await.unwrap();
872    }
873}