Skip to main content

fips_core/transport/tor/
control.rs

1//! Tor control port client.
2//!
3//! Minimal async client for the Tor control protocol (control-spec).
4//! Implements AUTHENTICATE and GETINFO for monitoring the Tor daemon.
5
6use std::fmt;
7use std::path::{Path, PathBuf};
8
9use serde::Serialize;
10use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
11use tokio::net::TcpStream;
12#[cfg(unix)]
13use tokio::net::UnixStream;
14use tracing::debug;
15
16// ============================================================================
17// Error Type
18// ============================================================================
19
20/// Errors from the Tor control port client.
21#[derive(Debug)]
22pub enum TorControlError {
23    /// Failed to connect to the control port.
24    ConnectionFailed(String),
25    /// Authentication failed.
26    AuthFailed(String),
27    /// Protocol-level error (unexpected response format).
28    ProtocolError(String),
29    /// I/O error.
30    Io(std::io::Error),
31}
32
33impl fmt::Display for TorControlError {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::ConnectionFailed(msg) => write!(f, "control port connection failed: {}", msg),
37            Self::AuthFailed(msg) => write!(f, "control port auth failed: {}", msg),
38            Self::ProtocolError(msg) => write!(f, "control protocol error: {}", msg),
39            Self::Io(e) => write!(f, "control port I/O error: {}", e),
40        }
41    }
42}
43
44impl std::error::Error for TorControlError {}
45
46impl From<std::io::Error> for TorControlError {
47    fn from(e: std::io::Error) -> Self {
48        Self::Io(e)
49    }
50}
51
52// ============================================================================
53// Authentication
54// ============================================================================
55
56/// Control port authentication method.
57#[derive(Debug, Clone)]
58pub enum ControlAuth {
59    /// Cookie authentication — reads 32-byte cookie from file, sends as hex.
60    Cookie(PathBuf),
61    /// Password authentication — sends AUTHENTICATE "password".
62    Password(String),
63}
64
65impl ControlAuth {
66    /// Parse a control_auth config string into a ControlAuth value.
67    ///
68    /// - `"cookie"` or `"cookie:/path/to/cookie"` → Cookie auth
69    /// - `"password:secret"` → Password auth
70    pub fn from_config(auth_str: &str, default_cookie_path: &str) -> Result<Self, TorControlError> {
71        if auth_str == "cookie" {
72            Ok(Self::Cookie(PathBuf::from(default_cookie_path)))
73        } else if let Some(path) = auth_str.strip_prefix("cookie:") {
74            Ok(Self::Cookie(PathBuf::from(path)))
75        } else if let Some(password) = auth_str.strip_prefix("password:") {
76            Ok(Self::Password(password.to_string()))
77        } else {
78            Err(TorControlError::AuthFailed(format!(
79                "unknown control_auth format '{}': expected 'cookie', 'cookie:/path', or 'password:secret'",
80                auth_str
81            )))
82        }
83    }
84}
85
86// ============================================================================
87// Monitoring Info
88// ============================================================================
89
90/// Snapshot of Tor daemon status collected via control port GETINFO queries.
91#[derive(Debug, Clone, Serialize)]
92pub struct TorMonitoringInfo {
93    /// Bootstrap progress (0-100).
94    pub bootstrap: u8,
95    /// Whether Tor has at least one working circuit.
96    pub circuit_established: bool,
97    /// Total bytes read by Tor since startup.
98    pub traffic_read: u64,
99    /// Total bytes written by Tor since startup.
100    pub traffic_written: u64,
101    /// Network liveness: "up" or "down".
102    pub network_liveness: String,
103    /// Tor daemon version string.
104    pub version: String,
105    /// Whether Tor is in dormant mode (no recent activity).
106    pub dormant: bool,
107}
108
109// ============================================================================
110// Client
111// ============================================================================
112
113/// Async Tor control port client.
114///
115/// Maintains a persistent connection to the Tor daemon's control port.
116/// Supports both TCP (`host:port`) and Unix socket (`/path/to/socket`)
117/// connections. The connection must stay alive for the lifetime of
118/// ephemeral onion services (unless created with detach=true).
119pub struct TorControlClient {
120    reader: BufReader<Box<dyn AsyncRead + Unpin + Send>>,
121    writer: Box<dyn AsyncWrite + Unpin + Send>,
122}
123
124impl fmt::Debug for TorControlClient {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        f.debug_struct("TorControlClient").finish_non_exhaustive()
127    }
128}
129
130impl TorControlClient {
131    /// Connect to a Tor control port.
132    ///
133    /// The address can be either:
134    /// - A TCP address (`host:port` or `IP:port`) for TCP connections
135    /// - A filesystem path (starting with `/` or `./`) for Unix socket connections
136    ///
137    /// Unix sockets are preferred for security: they provide filesystem
138    /// permission-based access control and are not reachable from containers
139    /// unless explicitly mounted. The Debian default is `/run/tor/control`.
140    pub async fn connect(addr: &str) -> Result<Self, TorControlError> {
141        #[cfg(unix)]
142        if is_unix_socket_path(addr) {
143            return Self::connect_unix(addr).await;
144        }
145        Self::connect_tcp(addr).await
146    }
147
148    /// Connect via TCP to a control port at `host:port`.
149    async fn connect_tcp(addr: &str) -> Result<Self, TorControlError> {
150        let stream = TcpStream::connect(addr).await.map_err(|e| {
151            TorControlError::ConnectionFailed(format!(
152                "failed to connect to control port {}: {}",
153                addr, e
154            ))
155        })?;
156
157        let (read_half, write_half) = stream.into_split();
158
159        debug!(addr = %addr, transport = "tcp", "Connected to Tor control port");
160
161        Ok(Self {
162            reader: BufReader::new(Box::new(read_half)),
163            writer: Box::new(write_half),
164        })
165    }
166
167    /// Connect via Unix socket to a control port at the given path.
168    #[cfg(unix)]
169    async fn connect_unix(path: &str) -> Result<Self, TorControlError> {
170        let stream = UnixStream::connect(path).await.map_err(|e| {
171            TorControlError::ConnectionFailed(format!(
172                "failed to connect to control socket {}: {}",
173                path, e
174            ))
175        })?;
176
177        let (read_half, write_half) = stream.into_split();
178
179        debug!(path = %path, transport = "unix", "Connected to Tor control port");
180
181        Ok(Self {
182            reader: BufReader::new(Box::new(read_half)),
183            writer: Box::new(write_half),
184        })
185    }
186
187    /// Authenticate with the Tor daemon.
188    pub async fn authenticate(&mut self, auth: &ControlAuth) -> Result<(), TorControlError> {
189        let command = match auth {
190            ControlAuth::Cookie(path) => {
191                let cookie = read_cookie_file(path)?;
192                format!("AUTHENTICATE {}\r\n", hex::encode(cookie))
193            }
194            ControlAuth::Password(password) => {
195                // Escape quotes in password
196                let escaped = password.replace('\\', "\\\\").replace('"', "\\\"");
197                format!("AUTHENTICATE \"{}\"\r\n", escaped)
198            }
199        };
200
201        self.send_command(&command).await?;
202        let response = self.read_response().await?;
203
204        if response.code != 250 {
205            return Err(TorControlError::AuthFailed(format!(
206                "AUTHENTICATE failed: {} {}",
207                response.code, response.message
208            )));
209        }
210
211        debug!("Authenticated with Tor control port");
212        Ok(())
213    }
214
215    // ========================================================================
216    // Monitoring Queries
217    // ========================================================================
218
219    /// Issue a GETINFO query and return the value for the given key.
220    ///
221    /// Tor responds with `250-key=value` data lines. This extracts the
222    /// value for the requested key.
223    async fn getinfo(&mut self, key: &str) -> Result<String, TorControlError> {
224        let command = format!("GETINFO {}\r\n", key);
225        self.send_command(&command).await?;
226        let response = self.read_response().await?;
227
228        if response.code != 250 {
229            return Err(TorControlError::ProtocolError(format!(
230                "GETINFO {} failed: {} {}",
231                key, response.code, response.message
232            )));
233        }
234
235        let prefix = format!("{}=", key);
236        for line in &response.data_lines {
237            if let Some(value) = line.strip_prefix(&prefix) {
238                return Ok(value.to_string());
239            }
240        }
241
242        Err(TorControlError::ProtocolError(format!(
243            "GETINFO response missing key '{}'",
244            key
245        )))
246    }
247
248    /// Query Tor's bootstrap progress (0-100).
249    pub async fn get_bootstrap_phase(&mut self) -> Result<u8, TorControlError> {
250        let raw = self.getinfo("status/bootstrap-phase").await?;
251
252        // Value looks like: NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
253        if let Some(progress_start) = raw.find("PROGRESS=") {
254            let after = &raw[progress_start + 9..];
255            let digits: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
256            if let Ok(progress) = digits.parse::<u8>() {
257                return Ok(progress);
258            }
259        }
260
261        Err(TorControlError::ProtocolError(
262            "could not parse bootstrap progress".into(),
263        ))
264    }
265
266    /// Check whether Tor has established circuits (health check).
267    ///
268    /// Returns true if Tor has at least one working circuit, false otherwise.
269    pub async fn is_circuit_established(&mut self) -> Result<bool, TorControlError> {
270        let value = self.getinfo("status/circuit-established").await?;
271        Ok(value.trim() == "1")
272    }
273
274    /// Query total bytes read by Tor since startup.
275    pub async fn traffic_read(&mut self) -> Result<u64, TorControlError> {
276        let value = self.getinfo("traffic/read").await?;
277        value.trim().parse::<u64>().map_err(|_| {
278            TorControlError::ProtocolError(format!("invalid traffic/read value: '{}'", value))
279        })
280    }
281
282    /// Query total bytes written by Tor since startup.
283    pub async fn traffic_written(&mut self) -> Result<u64, TorControlError> {
284        let value = self.getinfo("traffic/written").await?;
285        value.trim().parse::<u64>().map_err(|_| {
286            TorControlError::ProtocolError(format!("invalid traffic/written value: '{}'", value))
287        })
288    }
289
290    /// Query whether Tor considers the network reachable.
291    ///
292    /// Returns `"up"` or `"down"`.
293    pub async fn network_liveness(&mut self) -> Result<String, TorControlError> {
294        self.getinfo("network-liveness").await
295    }
296
297    /// Query the Tor daemon version string.
298    pub async fn version(&mut self) -> Result<String, TorControlError> {
299        self.getinfo("version").await
300    }
301
302    /// Query whether Tor is in dormant mode (no recent activity).
303    pub async fn is_dormant(&mut self) -> Result<bool, TorControlError> {
304        let value = self.getinfo("dormant").await?;
305        Ok(value.trim() == "1")
306    }
307
308    /// Query Tor's SOCKS listener addresses.
309    ///
310    /// Returns a list of addresses Tor is listening on for SOCKS connections.
311    pub async fn socks_listeners(&mut self) -> Result<Vec<String>, TorControlError> {
312        let value = self.getinfo("net/listeners/socks").await?;
313        Ok(value
314            .split_whitespace()
315            .map(|s| s.trim_matches('"').to_string())
316            .collect())
317    }
318
319    /// Collect all monitoring info in a single batch of queries.
320    pub async fn monitoring_snapshot(&mut self) -> Result<TorMonitoringInfo, TorControlError> {
321        let bootstrap = self.get_bootstrap_phase().await.unwrap_or(0);
322        let circuit_established = self.is_circuit_established().await.unwrap_or(false);
323        let traffic_read = self.traffic_read().await.unwrap_or(0);
324        let traffic_written = self.traffic_written().await.unwrap_or(0);
325        let network_liveness = self
326            .network_liveness()
327            .await
328            .unwrap_or_else(|_| "unknown".into());
329        let version = self.version().await.unwrap_or_else(|_| "unknown".into());
330        let dormant = self.is_dormant().await.unwrap_or(false);
331
332        Ok(TorMonitoringInfo {
333            bootstrap,
334            circuit_established,
335            traffic_read,
336            traffic_written,
337            network_liveness,
338            version,
339            dormant,
340        })
341    }
342
343    // ========================================================================
344    // Protocol Helpers
345    // ========================================================================
346
347    /// Send a raw command string to the control port.
348    async fn send_command(&mut self, command: &str) -> Result<(), TorControlError> {
349        self.writer.write_all(command.as_bytes()).await?;
350        self.writer.flush().await?;
351        Ok(())
352    }
353
354    /// Read a complete response from the control port.
355    ///
356    /// Tor responses are line-based:
357    /// - `250-key=value` — mid-reply data line (more lines follow)
358    /// - `250 OK` — final line of a successful reply
359    /// - `5xx message` — error
360    ///
361    /// Returns the status code and collected data lines.
362    async fn read_response(&mut self) -> Result<ControlResponse, TorControlError> {
363        let mut data_lines = Vec::new();
364        let mut line_buf = String::new();
365
366        loop {
367            line_buf.clear();
368            let n = self.reader.read_line(&mut line_buf).await?;
369            if n == 0 {
370                return Err(TorControlError::ProtocolError(
371                    "control port connection closed".into(),
372                ));
373            }
374
375            let line = line_buf.trim_end_matches(['\r', '\n']);
376
377            if line.len() < 4 {
378                return Err(TorControlError::ProtocolError(format!(
379                    "response line too short: '{}'",
380                    line
381                )));
382            }
383
384            let code: u16 = line[..3].parse().map_err(|_| {
385                TorControlError::ProtocolError(format!("invalid response code in: '{}'", line))
386            })?;
387
388            let separator = line.as_bytes()[3];
389            let content = &line[4..];
390
391            match separator {
392                b'-' => {
393                    // Mid-reply data line
394                    data_lines.push(content.to_string());
395                }
396                b' ' => {
397                    // Final line
398                    return Ok(ControlResponse {
399                        code,
400                        message: content.to_string(),
401                        data_lines,
402                    });
403                }
404                b'+' => {
405                    // Multi-line data (dot-encoded). Read until lone "."
406                    data_lines.push(content.to_string());
407                    loop {
408                        line_buf.clear();
409                        let n = self.reader.read_line(&mut line_buf).await?;
410                        if n == 0 {
411                            return Err(TorControlError::ProtocolError(
412                                "connection closed during multi-line response".into(),
413                            ));
414                        }
415                        let dot_line = line_buf.trim_end_matches(['\r', '\n']);
416                        if dot_line == "." {
417                            break;
418                        }
419                        // Strip leading dot-escape
420                        let unescaped = dot_line.strip_prefix('.').unwrap_or(dot_line);
421                        data_lines.push(unescaped.to_string());
422                    }
423                }
424                _ => {
425                    return Err(TorControlError::ProtocolError(format!(
426                        "unexpected separator '{}' in: '{}'",
427                        separator as char, line
428                    )));
429                }
430            }
431        }
432    }
433}
434
435/// Parsed control port response.
436struct ControlResponse {
437    /// Status code (250 = success, 5xx = error).
438    code: u16,
439    /// Message from the final line.
440    message: String,
441    /// Data lines from mid-reply (250-) lines.
442    data_lines: Vec<String>,
443}
444
445// ============================================================================
446// Cookie File
447// ============================================================================
448
449/// Read a Tor control cookie file (32 bytes of raw binary).
450fn read_cookie_file(path: &Path) -> Result<Vec<u8>, TorControlError> {
451    let data = std::fs::read(path).map_err(|e| {
452        TorControlError::AuthFailed(format!(
453            "failed to read cookie file '{}': {}",
454            path.display(),
455            e
456        ))
457    })?;
458
459    if data.len() != 32 {
460        return Err(TorControlError::AuthFailed(format!(
461            "cookie file '{}' has {} bytes, expected 32",
462            path.display(),
463            data.len()
464        )));
465    }
466
467    Ok(data)
468}
469
470// ============================================================================
471// Unix Socket Detection
472// ============================================================================
473
474/// Detect whether a control address string is a Unix socket path.
475///
476/// Returns true if the string starts with `/` or `./`, indicating a
477/// filesystem path rather than a `host:port` TCP address.
478#[cfg(unix)]
479fn is_unix_socket_path(addr: &str) -> bool {
480    addr.starts_with('/') || addr.starts_with("./")
481}
482
483// ============================================================================
484// Hex Encoding (minimal, no dependency)
485// ============================================================================
486
487mod hex {
488    const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
489
490    pub fn encode(data: Vec<u8>) -> String {
491        let mut s = String::with_capacity(data.len() * 2);
492        for byte in data {
493            s.push(HEX_CHARS[(byte >> 4) as usize] as char);
494            s.push(HEX_CHARS[(byte & 0x0f) as usize] as char);
495        }
496        s
497    }
498}
499
500// ============================================================================
501// Tests
502// ============================================================================
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use crate::transport::tor::mock_control::{self, MockTorControlServer};
508    use tempfile::TempDir;
509
510    // === ControlAuth parsing ===
511
512    #[test]
513    fn test_control_auth_cookie_default() {
514        let auth = ControlAuth::from_config("cookie", "/var/run/tor/cookie").unwrap();
515        match auth {
516            ControlAuth::Cookie(path) => assert_eq!(path, Path::new("/var/run/tor/cookie")),
517            _ => panic!("expected Cookie"),
518        }
519    }
520
521    #[test]
522    fn test_control_auth_cookie_custom_path() {
523        let auth = ControlAuth::from_config("cookie:/tmp/my_cookie", "/default").unwrap();
524        match auth {
525            ControlAuth::Cookie(path) => assert_eq!(path, Path::new("/tmp/my_cookie")),
526            _ => panic!("expected Cookie"),
527        }
528    }
529
530    #[test]
531    fn test_control_auth_password() {
532        let auth = ControlAuth::from_config("password:mypass", "/default").unwrap();
533        match auth {
534            ControlAuth::Password(p) => assert_eq!(p, "mypass"),
535            _ => panic!("expected Password"),
536        }
537    }
538
539    #[test]
540    fn test_control_auth_invalid() {
541        let result = ControlAuth::from_config("unknown", "/default");
542        assert!(result.is_err());
543    }
544
545    // === Hex encoding ===
546
547    #[test]
548    fn test_hex_encode() {
549        assert_eq!(hex::encode(vec![0xde, 0xad, 0xbe, 0xef]), "deadbeef");
550        assert_eq!(hex::encode(vec![0x00, 0xff]), "00ff");
551    }
552
553    // === Unix socket path detection ===
554
555    #[cfg(unix)]
556    #[test]
557    fn test_is_unix_socket_path() {
558        assert!(is_unix_socket_path("/run/tor/control"));
559        assert!(is_unix_socket_path("/var/run/tor/control"));
560        assert!(is_unix_socket_path("./tor-control.sock"));
561        assert!(!is_unix_socket_path("127.0.0.1:9051"));
562        assert!(!is_unix_socket_path("tor-daemon:9051"));
563        assert!(!is_unix_socket_path("localhost:9051"));
564    }
565
566    #[cfg(unix)]
567    #[tokio::test]
568    async fn test_connect_unix_socket_nonexistent() {
569        let result = TorControlClient::connect("/tmp/nonexistent-tor-control.sock").await;
570        assert!(result.is_err());
571        let err = format!("{}", result.unwrap_err());
572        assert!(err.contains("control socket"));
573    }
574
575    #[cfg(unix)]
576    #[tokio::test]
577    async fn test_connect_unix_socket_roundtrip() {
578        // Create a Unix socket listener, accept a connection, respond to AUTHENTICATE
579        let dir = TempDir::new().unwrap();
580        let sock_path = dir.path().join("control.sock");
581        let sock_path_str = sock_path.to_str().unwrap().to_string();
582
583        let listener = tokio::net::UnixListener::bind(&sock_path).unwrap();
584
585        // Spawn a minimal control handler
586        let handle = tokio::spawn(async move {
587            let (stream, _) = listener.accept().await.unwrap();
588            let (reader, mut writer) = stream.into_split();
589            let mut reader = tokio::io::BufReader::new(reader);
590            let mut line = String::new();
591
592            // Read AUTHENTICATE
593            reader.read_line(&mut line).await.unwrap();
594            assert!(line.starts_with("AUTHENTICATE"));
595
596            use tokio::io::AsyncWriteExt;
597            writer.write_all(b"250 OK\r\n").await.unwrap();
598            writer.flush().await.unwrap();
599        });
600
601        let mut client = TorControlClient::connect(&sock_path_str).await.unwrap();
602        let auth = ControlAuth::Password("test".to_string());
603        client.authenticate(&auth).await.unwrap();
604
605        handle.await.unwrap();
606    }
607
608    // === Cookie file ===
609
610    #[test]
611    fn test_read_cookie_file_valid() {
612        let dir = TempDir::new().unwrap();
613        let path = dir.path().join("cookie");
614        let cookie_data = vec![0xAA; 32];
615        std::fs::write(&path, &cookie_data).unwrap();
616
617        let loaded = read_cookie_file(&path).unwrap();
618        assert_eq!(loaded, cookie_data);
619    }
620
621    #[test]
622    fn test_read_cookie_file_wrong_size() {
623        let dir = TempDir::new().unwrap();
624        let path = dir.path().join("cookie");
625        std::fs::write(&path, [0u8; 16]).unwrap();
626
627        assert!(read_cookie_file(&path).is_err());
628    }
629
630    #[test]
631    fn test_read_cookie_file_nonexistent() {
632        assert!(read_cookie_file(Path::new("/nonexistent/cookie")).is_err());
633    }
634
635    // === Control protocol (requires mock server) ===
636
637    #[tokio::test]
638    async fn test_authenticate_password() {
639        let mock = MockTorControlServer::start().await;
640        let mut client = TorControlClient::connect(&mock.addr().to_string())
641            .await
642            .unwrap();
643
644        let auth = ControlAuth::Password("testpass".to_string());
645        client.authenticate(&auth).await.unwrap();
646    }
647
648    #[tokio::test]
649    async fn test_authenticate_cookie() {
650        let mock = MockTorControlServer::start().await;
651
652        // Create a cookie file
653        let dir = TempDir::new().unwrap();
654        let cookie_path = dir.path().join("cookie");
655        std::fs::write(&cookie_path, [0xAA; 32]).unwrap();
656
657        let mut client = TorControlClient::connect(&mock.addr().to_string())
658            .await
659            .unwrap();
660        let auth = ControlAuth::Cookie(cookie_path);
661        client.authenticate(&auth).await.unwrap();
662    }
663
664    #[tokio::test]
665    async fn test_get_bootstrap_phase() {
666        let mock = MockTorControlServer::start().await;
667        let mut client = TorControlClient::connect(&mock.addr().to_string())
668            .await
669            .unwrap();
670
671        let auth = ControlAuth::Password("testpass".to_string());
672        client.authenticate(&auth).await.unwrap();
673
674        let progress = client.get_bootstrap_phase().await.unwrap();
675        assert_eq!(progress, 100);
676    }
677
678    #[tokio::test]
679    async fn test_auth_failure() {
680        let mock = MockTorControlServer::start_with_options(mock_control::MockOptions {
681            reject_auth: true,
682        })
683        .await;
684        let mut client = TorControlClient::connect(&mock.addr().to_string())
685            .await
686            .unwrap();
687
688        let auth = ControlAuth::Password("wrongpass".to_string());
689        let result = client.authenticate(&auth).await;
690        assert!(result.is_err());
691    }
692
693    #[tokio::test]
694    async fn test_connect_to_closed_port() {
695        // Bind and immediately drop to get a port that's closed
696        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
697        let addr = listener.local_addr().unwrap();
698        drop(listener);
699
700        let result = TorControlClient::connect(&addr.to_string()).await;
701        assert!(result.is_err());
702    }
703
704    // === Monitoring queries ===
705
706    #[tokio::test]
707    async fn test_is_circuit_established() {
708        let mock = MockTorControlServer::start().await;
709        let mut client = TorControlClient::connect(&mock.addr().to_string())
710            .await
711            .unwrap();
712        client
713            .authenticate(&ControlAuth::Password("test".into()))
714            .await
715            .unwrap();
716
717        assert!(client.is_circuit_established().await.unwrap());
718    }
719
720    #[tokio::test]
721    async fn test_traffic_counters() {
722        let mock = MockTorControlServer::start().await;
723        let mut client = TorControlClient::connect(&mock.addr().to_string())
724            .await
725            .unwrap();
726        client
727            .authenticate(&ControlAuth::Password("test".into()))
728            .await
729            .unwrap();
730
731        assert_eq!(client.traffic_read().await.unwrap(), 1048576);
732        assert_eq!(client.traffic_written().await.unwrap(), 524288);
733    }
734
735    #[tokio::test]
736    async fn test_network_liveness() {
737        let mock = MockTorControlServer::start().await;
738        let mut client = TorControlClient::connect(&mock.addr().to_string())
739            .await
740            .unwrap();
741        client
742            .authenticate(&ControlAuth::Password("test".into()))
743            .await
744            .unwrap();
745
746        assert_eq!(client.network_liveness().await.unwrap(), "up");
747    }
748
749    #[tokio::test]
750    async fn test_version() {
751        let mock = MockTorControlServer::start().await;
752        let mut client = TorControlClient::connect(&mock.addr().to_string())
753            .await
754            .unwrap();
755        client
756            .authenticate(&ControlAuth::Password("test".into()))
757            .await
758            .unwrap();
759
760        assert_eq!(client.version().await.unwrap(), "0.4.8.10");
761    }
762
763    #[tokio::test]
764    async fn test_dormant() {
765        let mock = MockTorControlServer::start().await;
766        let mut client = TorControlClient::connect(&mock.addr().to_string())
767            .await
768            .unwrap();
769        client
770            .authenticate(&ControlAuth::Password("test".into()))
771            .await
772            .unwrap();
773
774        assert!(!client.is_dormant().await.unwrap());
775    }
776
777    #[tokio::test]
778    async fn test_socks_listeners() {
779        let mock = MockTorControlServer::start().await;
780        let mut client = TorControlClient::connect(&mock.addr().to_string())
781            .await
782            .unwrap();
783        client
784            .authenticate(&ControlAuth::Password("test".into()))
785            .await
786            .unwrap();
787
788        let listeners = client.socks_listeners().await.unwrap();
789        assert_eq!(listeners, vec!["127.0.0.1:9050"]);
790    }
791
792    #[tokio::test]
793    async fn test_monitoring_snapshot() {
794        let mock = MockTorControlServer::start().await;
795        let mut client = TorControlClient::connect(&mock.addr().to_string())
796            .await
797            .unwrap();
798        client
799            .authenticate(&ControlAuth::Password("test".into()))
800            .await
801            .unwrap();
802
803        let info = client.monitoring_snapshot().await.unwrap();
804        assert_eq!(info.bootstrap, 100);
805        assert!(info.circuit_established);
806        assert_eq!(info.traffic_read, 1048576);
807        assert_eq!(info.traffic_written, 524288);
808        assert_eq!(info.network_liveness, "up");
809        assert_eq!(info.version, "0.4.8.10");
810        assert!(!info.dormant);
811    }
812}