Skip to main content

tor_ptmgr/
ipc.rs

1//! Launching pluggable transport binaries and communicating with them.
2//!
3//! This module contains utilities to launch pluggable transports supporting pt-spec.txt
4//! version 1, and communicate with them in order to specify configuration parameters and
5//! receive updates as to the current state of the PT.
6
7use crate::PtClientMethod;
8use crate::err;
9use crate::err::PtError;
10use futures::StreamExt;
11use futures::channel::mpsc::Receiver;
12use itertools::Itertools;
13use std::borrow::Cow;
14use std::collections::HashMap;
15use std::ffi::OsString;
16use std::io::{BufRead, BufReader};
17use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
18use std::path::PathBuf;
19use std::process::{Child, Command, Stdio};
20use std::str::FromStr;
21use std::sync::Arc;
22use std::{io, thread};
23use tor_basic_utils::PathExt as _;
24use tor_error::{internal, warn_report};
25use tor_linkspec::PtTransportName;
26use tor_rtcompat::{Runtime, SleepProviderExt};
27use tor_socksproto::SocksVersion;
28use tracing::{debug, error, info, trace, warn};
29use web_time_compat::{Duration, Instant, InstantExt};
30
31/// Amount of time we give a pluggable transport child process to exit gracefully.
32const GRACEFUL_EXIT_TIME: Duration = Duration::from_secs(5);
33/// Default timeout for PT binary startup.
34const PT_START_TIMEOUT: Duration = Duration::from_secs(30);
35/// Size for the buffer storing pluggable transport stdout lines.
36const PT_STDIO_BUFFER: usize = 64;
37
38/// An arbitrary key/value status update from a pluggable transport.
39#[derive(PartialEq, Eq, Debug, Clone)]
40pub struct PtStatus {
41    /// Arbitrary key-value data about the state of this transport, from the binary running
42    /// said transport.
43    // NOTE(eta): This is assumed to not have duplicate keys.
44    data: HashMap<String, String>,
45}
46
47/// A message sent from a pluggable transport child process.
48///
49/// For more in-depth information about these messages, consult pt-spec.txt.
50#[derive(PartialEq, Eq, Debug, Clone)]
51#[non_exhaustive]
52#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
53pub enum PtMessage {
54    /// `VERSION-ERROR`: No compatible pluggable transport specification version was provided.
55    VersionError(String),
56    /// `VERSION`: Specifies the version the binary is using for the IPC protocol.
57    Version(String),
58    /// `ENV-ERROR`: Reports an error with the provided environment variables.
59    EnvError(String),
60    /// `PROXY DONE`: The configured proxy was correctly initialised.
61    ProxyDone,
62    /// `PROXY-ERROR`: An error was encountered setting up the configured proxy.
63    ProxyError(String),
64    /// `CMETHOD`: A client transport has been launched.
65    ClientTransportLaunched {
66        /// The name of the launched transport.
67        transport: PtTransportName,
68        /// The protocol used ('socks4' or 'socks5').
69        protocol: String,
70        /// An address to connect via this transport.
71        /// (This should be localhost.)
72        endpoint: SocketAddr,
73    },
74    /// `CMETHOD-ERROR`: An error was encountered setting up a client transport.
75    ClientTransportFailed {
76        /// The name of the transport.
77        transport: PtTransportName,
78        /// The error message.
79        message: String,
80    },
81    /// `CMETHODS DONE`: All client transports that are supported have been launched.
82    ClientTransportsDone,
83    /// `SMETHOD`: A server transport has been launched.
84    ServerTransportLaunched {
85        /// The name of the launched transport.
86        transport: PtTransportName,
87        /// The endpoint clients should use the reach the transport.
88        endpoint: SocketAddr,
89        /// Additional per-transport information.
90        // NOTE(eta): This assumes it actually is k/v and repeated keys aren't allowed...
91        options: HashMap<String, String>,
92    },
93    /// `SMETHOD-ERROR`: An error was encountered setting up a server transport.
94    ServerTransportFailed {
95        /// The name of the transport.
96        transport: PtTransportName,
97        /// The error message.
98        message: String,
99    },
100    /// `SMETHODS DONE`: All server transports that are supported have been launched.
101    ServerTransportsDone,
102    /// `LOG`: A log message.
103    Log {
104        /// The severity (one of 'error', 'warning', 'notice', 'info', 'debug').
105        severity: String,
106        /// The log message.
107        message: String,
108    },
109    /// `STATUS`: Arbitrary key/value status messages.
110    Status(PtStatus),
111    /// A line containing an unknown command.
112    Unknown(String),
113}
114
115/// Parse a value (something on the RHS of an =), which could be a CString as defined by
116/// control-spec.txt ยง2. Returns (value, unparsed rest of string).
117fn parse_one_value(from: &str) -> Result<(String, &str), &'static str> {
118    let first_char = from.chars().next();
119    Ok(if first_char.is_none() {
120        (String::new(), "")
121    } else if let Some('"') = first_char {
122        // This is a CString, so we're going to need to parse it char-by-char.
123        // FIXME(eta): This currently doesn't parse octal escape codes, even though the spec says
124        //             we should. That's finicky, though, and probably not used.
125        let mut ret = String::new();
126        let mut chars = from.chars();
127        assert_eq!(chars.next(), Some('"')); // discard "
128        loop {
129            let ch = chars.next().ok_or("ran out of input parsing CString")?;
130            match ch {
131                '\\' => match chars
132                    .next()
133                    .ok_or("encountered trailing backslash in CString")?
134                {
135                    'n' => ret.push('\n'),
136                    'r' => ret.push('\r'),
137                    't' => ret.push('\t'),
138                    '0'..='8' => return Err("attempted unsupported octal escape code"),
139                    ch2 => ret.push(ch2),
140                },
141                '"' => break,
142                _ => ret.push(ch),
143            }
144        }
145        (ret, chars.as_str())
146    } else {
147        // Simple: just find the space
148        if let Some((start, rest)) = from.split_once(' ') {
149            (start.to_string(), rest)
150        } else {
151            (from.to_string(), "")
152        }
153    })
154}
155
156/// Chomp one key/value pair off a list of smethod args.
157/// Returns (k, v, unparsed rest of string).
158/// Will also chomp the comma at the end, if there is one.
159fn parse_one_smethod_arg(args: &str) -> Result<(String, String, &str), &'static str> {
160    // NOTE(eta): Apologies for this looking a bit gnarly. Ideally, this is what you'd use
161    //            something like `nom` for, but I didn't want to bring in a dep just for this.
162
163    let mut key = String::new();
164    let mut val = String::new();
165    // If true, we're reading the value, not the key.
166    let mut reading_val = false;
167    let mut chars = args.chars();
168    while let Some(c) = chars.next() {
169        let target = if reading_val { &mut val } else { &mut key };
170        match c {
171            '\\' => {
172                let c = chars
173                    .next()
174                    .ok_or("smethod arg terminates with backslash")?;
175                target.push(c);
176            }
177            '=' => {
178                if reading_val {
179                    return Err("encountered = while parsing value");
180                }
181                reading_val = true;
182            }
183            ',' => break,
184            c => target.push(c),
185        }
186    }
187    if !reading_val {
188        return Err("ran out of chars parsing smethod arg");
189    }
190    Ok((key, val, chars.as_str()))
191}
192
193impl FromStr for PtMessage {
194    type Err = Cow<'static, str>;
195
196    // NOTE(eta): This, of course, implies that the PT IPC communications are valid UTF-8.
197    //            This assumption might turn out to be false.
198    #[allow(clippy::cognitive_complexity)]
199    fn from_str(s: &str) -> Result<Self, Self::Err> {
200        // TODO(eta): Maybe tolerate additional whitespace (using `split_whitespace`)?.
201        //            This requires modified words.join() logic, though.
202        let mut words = s.split(' ');
203        let first_word = words.next().ok_or_else(|| Cow::from("empty line"))?;
204        Ok(match first_word {
205            "VERSION-ERROR" => {
206                let rest = words.join(" ");
207                Self::VersionError(rest)
208            }
209            "VERSION" => {
210                let vers = words.next().ok_or_else(|| Cow::from("no version"))?;
211                Self::Version(vers.into())
212            }
213            "ENV-ERROR" => {
214                let rest = words.join(" ");
215                Self::EnvError(rest)
216            }
217            "PROXY" => match words.next() {
218                Some("DONE") => Self::ProxyDone,
219                _ => Self::Unknown(s.into()),
220            },
221            "PROXY-ERROR" => {
222                let rest = words.join(" ");
223                Self::ProxyError(rest)
224            }
225            "CMETHOD" => {
226                let transport = words.next().ok_or_else(|| Cow::from("no transport"))?;
227                let protocol = words.next().ok_or_else(|| Cow::from("no protocol"))?;
228                let endpoint = words
229                    .next()
230                    .ok_or_else(|| Cow::from("no endpoint"))?
231                    .parse::<SocketAddr>()
232                    .map_err(|e| Cow::from(format!("failed to parse endpoint: {}", e)))?;
233                if !endpoint.ip().is_loopback() {
234                    return Err(Cow::from(format!(
235                        "CMETHOD endpoint {endpoint} was not localhost"
236                    )));
237                }
238                Self::ClientTransportLaunched {
239                    transport: transport
240                        .parse()
241                        .map_err(|_| Cow::from("bad transport ID"))?,
242                    protocol: protocol.to_string(),
243                    endpoint,
244                }
245            }
246            "CMETHOD-ERROR" => {
247                let transport = words.next().ok_or_else(|| Cow::from("no transport"))?;
248                let rest = words.join(" ");
249                Self::ClientTransportFailed {
250                    transport: transport
251                        .parse()
252                        .map_err(|_| Cow::from("bad transport ID"))?,
253                    message: rest,
254                }
255            }
256            "CMETHODS" => match words.next() {
257                Some("DONE") => Self::ClientTransportsDone,
258                _ => Self::Unknown(s.into()),
259            },
260            "SMETHOD" => {
261                let transport = words.next().ok_or_else(|| Cow::from("no transport"))?;
262                let endpoint = words
263                    .next()
264                    .ok_or_else(|| Cow::from("no endpoint"))?
265                    .parse::<SocketAddr>()
266                    .map_err(|e| Cow::from(format!("failed to parse endpoint: {}", e)))?;
267                // The SMETHOD endpoint is the place where _clients_ connect, and it shouldn't be localhost.
268                let mut parsed_args = HashMap::new();
269
270                // NOTE(eta): pt-spec.txt seems to imply these options can't contain spaces, so
271                //            we work under that assumption.
272                //            It also doesn't actually parse them out -- but seeing as the API to
273                //            feed these back in will want them as separated k/v pairs, I think
274                //            it makes sense to here.
275                for option in words {
276                    if let Some(mut args) = option.strip_prefix("ARGS:") {
277                        while !args.is_empty() {
278                            let (k, v, rest) = parse_one_smethod_arg(args).map_err(|e| {
279                                Cow::from(format!("failed to parse SMETHOD ARGS: {}", e))
280                            })?;
281                            if parsed_args.contains_key(&k) {
282                                // At least check our assumption that this is actually k/v
283                                // and not Vec<(String, String)>.
284                                warn!("PT SMETHOD arguments contain repeated key {}!", k);
285                            }
286                            parsed_args.insert(k, v);
287                            args = rest;
288                        }
289                    }
290                }
291                Self::ServerTransportLaunched {
292                    transport: transport
293                        .parse()
294                        .map_err(|_| Cow::from("bad transport ID"))?,
295                    endpoint,
296                    options: parsed_args,
297                }
298            }
299            "SMETHOD-ERROR" => {
300                let transport = words.next().ok_or_else(|| Cow::from("no transport"))?;
301                let rest = words.join(" ");
302                Self::ServerTransportFailed {
303                    transport: transport
304                        .parse()
305                        .map_err(|_| Cow::from("bad transport ID"))?,
306                    message: rest,
307                }
308            }
309            "SMETHODS" => match words.next() {
310                Some("DONE") => Self::ServerTransportsDone,
311                _ => Self::Unknown(s.into()),
312            },
313            "LOG" => {
314                let severity = words
315                    .next()
316                    .ok_or_else(|| Cow::from("no severity"))?
317                    .strip_prefix("SEVERITY=")
318                    .ok_or_else(|| Cow::from("badly formatted severity"))?;
319                let message = words.join(" ");
320                let message = parse_one_value(
321                    message
322                        .strip_prefix("MESSAGE=")
323                        .ok_or_else(|| Cow::from("no or badly formatted message"))?,
324                )
325                .map_err(Cow::from)?
326                .0;
327                Self::Log {
328                    severity: severity.into(),
329                    message,
330                }
331            }
332            "STATUS" => {
333                let mut ret = HashMap::new();
334                let message = words.join(" ");
335                let mut message = &message as &str;
336                while !message.is_empty() {
337                    let (k, rest) = message
338                        .split_once('=')
339                        .ok_or_else(|| Cow::from(format!("failed to find = in '{}'", message)))?;
340                    if rest.is_empty() {
341                        return Err(Cow::from("key with no value"));
342                    }
343                    let (v, rest) = parse_one_value(rest).map_err(Cow::from)?;
344                    if ret.contains_key(k) {
345                        // At least check our assumption that this is actually k/v
346                        // and not Vec<(String, String)>.
347                        warn!("STATUS contains repeated key {}!", k);
348                    }
349                    ret.insert(k.to_owned(), v);
350                    message = rest;
351                    if let Some(remainder) = message.strip_prefix(" ") {
352                        message = remainder;
353                    }
354                }
355                Self::Status(PtStatus { data: ret })
356            }
357            _ => Self::Unknown(s.into()),
358        })
359    }
360}
361
362use sealed::*;
363/// Sealed trait to protect private types and default trait implementations
364pub(crate) mod sealed {
365    use super::*;
366
367    /// A handle to receive lines from a pluggable transport process' stdout asynchronously.
368    //
369    // FIXME(eta): This currently spawns an OS thread, since there's no other way to do this without
370    //             being async-runtime dependent (or adding process spawning to tor-rtcompat).
371    #[derive(Debug)]
372    pub struct AsyncPtChild {
373        /// Channel to receive lines from the child process stdout.
374        stdout: Receiver<io::Result<String>>,
375        /// Identifier to put in logging messages.
376        pub identifier: String,
377    }
378
379    impl AsyncPtChild {
380        /// Wrap an OS child process by spawning a worker thread to forward output from the child
381        /// to the asynchronous runtime via use of a channel.
382        pub fn new(mut child: Child, identifier: String) -> Result<Self, PtError> {
383            let (stdin, stdout) = (
384                child.stdin.take().ok_or_else(|| {
385                    PtError::Internal(internal!("Created child process without stdin pipe"))
386                })?,
387                child.stdout.take().ok_or_else(|| {
388                    PtError::Internal(internal!("Created child process without stdout pipe"))
389                })?,
390            );
391            // TODO RELAY #1649 We don't use a tor_memquota::mq_queue here yet
392            let (mut tx, rx) = tor_async_utils::mpsc_channel_no_memquota(PT_STDIO_BUFFER);
393            let ident = identifier.clone();
394            #[allow(clippy::cognitive_complexity)]
395            thread::spawn(move || {
396                let reader = BufReader::new(stdout);
397                let _stdin = stdin;
398                let mut noted_full = false;
399                // Forward lines from the blocking reader to the async channel.
400                for line in reader.lines() {
401                    let err = line.is_err();
402                    match &line {
403                        Ok(l) => trace!("<-- PT {}: {:?}", ident, l),
404                        Err(e) => trace!("<-- PT {}: Error: {:?}", ident, e),
405                    }
406                    if let Err(e) = tx.try_send(line) {
407                        if e.is_disconnected() {
408                            debug!("PT {} is disconnected; shutting it down.", ident);
409                            // Channel dropped, so shut down the pluggable transport process.
410                            break;
411                        }
412                        // The other kind of error is "full", which we can't do anything about.
413                        // Just throw the line away.
414                        if !noted_full {
415                            noted_full = true; // warn only once per PT.
416                            warn!(
417                                "Bug: Message queue for PT {} became full; dropping message",
418                                ident
419                            );
420                        }
421                    }
422                    if err {
423                        // Encountered an error reading, so ensure the process is shut down (it's
424                        // probably "broken pipe" anyway, so this is slightly redundant, but the
425                        // rest of the code assumes errors are nonrecoverable).
426                        break;
427                    }
428                }
429                // Has it already quit? If so, just exit now.
430                if let Ok(Some(_)) = child.try_wait() {
431                    // FIXME(eta): We currently throw away the exit code, which might be useful
432                    //             for debugging purposes!
433                    debug!("PT {} has exited.", ident);
434                    return;
435                }
436                // Otherwise, tell it to exit.
437                // Dropping stdin should tell the PT to exit, since we set the correct environment
438                // variable for that to happen.
439                trace!("Asking PT {} to exit, nicely.", ident);
440                drop(_stdin);
441                // Give it some time to exit.
442                thread::sleep(GRACEFUL_EXIT_TIME);
443                match child.try_wait() {
444                    Ok(None) => {
445                        // Kill it.
446                        debug!("Sending kill signal to PT {}", ident);
447                        if let Err(e) = child.kill() {
448                            warn_report!(e, "Failed to kill() spawned PT {}", ident);
449                        }
450                    }
451                    Ok(Some(_)) => {
452                        debug!("PT {} shut down successfully.", ident);
453                    } // It exited.
454                    Err(e) => {
455                        warn_report!(e, "Failed to call try_wait() on spawned PT {}", ident);
456                    }
457                }
458            });
459            Ok(AsyncPtChild {
460                stdout: rx,
461                identifier,
462            })
463        }
464
465        /// Receive a message from the pluggable transport binary asynchronously.
466        ///
467        /// Note: This will convert `PtMessage::Log` into a tracing log call automatically.
468        #[allow(clippy::cognitive_complexity)] // due to tracing
469        pub async fn recv(&mut self) -> err::Result<PtMessage> {
470            loop {
471                match self.stdout.next().await {
472                    None => return Err(PtError::ChildGone),
473                    Some(Ok(line)) => {
474                        let line =
475                            line.parse::<PtMessage>()
476                                .map_err(|e| PtError::IpcParseFailed {
477                                    line,
478                                    error: e.into(),
479                                })?;
480                        if let PtMessage::Log { severity, message } = line {
481                            // FIXME(eta): I wanted to make this integrate with `tracing` more nicely,
482                            //             but gave up after 15 minutes of clicking through spaghetti.
483                            match &severity as &str {
484                                "error" => error!("[pt {}] {}", self.identifier, message),
485                                "warning" => warn!("[pt {}] {}", self.identifier, message),
486                                "notice" => info!("[pt {}] {}", self.identifier, message),
487                                "info" => debug!("[pt {}] {}", self.identifier, message),
488                                "debug" => trace!("[pt {}] {}", self.identifier, message),
489                                x => warn!("[pt] {} {} {}", self.identifier, x, message),
490                            }
491                        } else {
492                            return Ok(line);
493                        }
494                    }
495                    Some(Err(e)) => {
496                        return Err(PtError::ChildReadFailed(Arc::new(e)));
497                    }
498                }
499            }
500        }
501    }
502
503    /// Defines some helper methods that are required later on
504    #[async_trait::async_trait]
505    pub trait PluggableTransportPrivate {
506        /// Return the [`AsyncPtChild`] if it exists
507        fn inner(&mut self) -> Result<&mut AsyncPtChild, PtError>;
508
509        /// Set the [`AsyncPtChild`]
510        fn set_inner(&mut self, newval: Option<AsyncPtChild>);
511
512        /// Return a loggable identifier for this transport.
513        fn identifier(&self) -> &str;
514
515        /// Checks whether a transport is specified in our specific parameters
516        fn specific_params_contains(&self, transport: &PtTransportName) -> bool;
517
518        /// Common handler for `ClientTransportLaunched` and `ServerTransportLaunched`
519        fn common_transport_launched_handler(
520            &self,
521            protocol: Option<String>,
522            transport: PtTransportName,
523            endpoint: SocketAddr,
524            methods: &mut HashMap<PtTransportName, PtClientMethod>,
525        ) -> Result<(), PtError> {
526            if !self.specific_params_contains(&transport) {
527                return Err(PtError::ProtocolViolation(format!(
528                    "binary launched unwanted transport '{}'",
529                    transport
530                )));
531            }
532            let protocol = match protocol {
533                Some(protocol_str) => match &protocol_str as &str {
534                    "socks4" => SocksVersion::V4,
535                    "socks5" => SocksVersion::V5,
536                    x => {
537                        return Err(PtError::ProtocolViolation(format!(
538                            "unknown CMETHOD protocol '{}'",
539                            x
540                        )));
541                    }
542                },
543                None => SocksVersion::V5,
544            };
545            let method = PtClientMethod {
546                kind: protocol,
547                endpoint,
548            };
549            info!("Transport '{}' uses method {:?}", transport, method);
550            methods.insert(transport, method);
551            Ok(())
552        }
553
554        /// Attempt to launch the PT and return the corresponding `[AsyncPtChild]`
555        fn get_child_from_pt_launch(
556            inner: &Option<AsyncPtChild>,
557            transports: &Vec<PtTransportName>,
558            binary_path: &PathBuf,
559            arguments: &[String],
560            all_env_vars: HashMap<OsString, OsString>,
561        ) -> Result<AsyncPtChild, PtError> {
562            if inner.is_some() {
563                let warning_msg =
564                    format!("Attempted to launch PT binary for {:?} twice.", transports);
565                warn!("{warning_msg}");
566                // WARN: this may not be the correct error to throw here
567                return Err(PtError::ChildProtocolViolation(warning_msg));
568            }
569            info!(
570                "Launching pluggable transport at {} for {:?}",
571                binary_path.display_lossy(),
572                transports
573            );
574            let child = Command::new(binary_path)
575                .args(arguments.iter())
576                .envs(all_env_vars)
577                .stdout(Stdio::piped())
578                .stdin(Stdio::piped())
579                .spawn()
580                .map_err(|e| PtError::ChildSpawnFailed {
581                    path: binary_path.clone(),
582                    error: Arc::new(e),
583                })?;
584
585            let identifier = crate::managed::pt_identifier(binary_path)?;
586            AsyncPtChild::new(child, identifier)
587        }
588
589        /// Consolidates some of the [`PtMessage`] potential matches to
590        /// deduplicate code
591        ///
592        /// Note that getting a [`PtMessage`] from this method implies that
593        /// the method was unable to match it and thus you should continue handling
594        /// the message. Getting [`None`] after error handling means that a match
595        /// was found and the appropriate action was successfully taken, and you don't
596        /// need to worry about it.
597        async fn try_match_common_messages<R: Runtime>(
598            &self,
599            rt: &R,
600            deadline: Instant,
601            async_child: &mut AsyncPtChild,
602        ) -> Result<Option<PtMessage>, PtError> {
603            match rt
604                .timeout(
605                    // FIXME(eta): It'd be nice if SleepProviderExt took an `Instant` natively.
606                    deadline.saturating_duration_since(Instant::get()),
607                    async_child.recv(),
608                )
609                .await
610                .map_err(|_| PtError::Timeout)??
611            {
612                PtMessage::ClientTransportFailed { transport, message }
613                | PtMessage::ServerTransportFailed { transport, message } => {
614                    warn!(
615                        "PT {} unable to launch {}. It said: {:?}",
616                        async_child.identifier, transport, message
617                    );
618                    return Err(PtError::TransportGaveError {
619                        transport: transport.to_string(),
620                        message,
621                    });
622                }
623                PtMessage::VersionError(e) => {
624                    if e != "no-version" {
625                        warn!("weird VERSION-ERROR: {}", e);
626                    }
627                    return Err(PtError::UnsupportedVersion);
628                }
629                PtMessage::Version(vers) => {
630                    if vers != "1" {
631                        return Err(PtError::ProtocolViolation(format!(
632                            "stated version is {}, asked for 1",
633                            vers
634                        )));
635                    }
636                    Ok(None)
637                }
638                PtMessage::EnvError(e) => return Err(PtError::ChildProtocolViolation(e)),
639                PtMessage::ProxyError(e) => return Err(PtError::ProxyError(e)),
640                // TODO(eta): We don't do anything with these right now!
641                PtMessage::Status(_) => Ok(None),
642                PtMessage::Unknown(x) => {
643                    warn!("unknown PT line: {}", x);
644                    Ok(None)
645                }
646                // Return the PtMessage as it is for further processing
647                // TODO: handle [`PtError::ProtocolViolation`] here somehow
648                x => {
649                    return Ok(Some(x));
650                }
651            }
652        }
653    }
654}
655
656/// Common parameters passed to a pluggable transport.
657#[derive(PartialEq, Eq, Clone, Debug, derive_builder::Builder)]
658pub struct PtCommonParameters {
659    /// A path where the launched PT can store state.
660    state_location: PathBuf,
661    /// An IPv4 address to bind outgoing connections to (if specified).
662    ///
663    /// Leaving this out will mean the PT uses a sane default.
664    #[builder(default)]
665    outbound_bind_v4: Option<Ipv4Addr>,
666    /// An IPv6 address to bind outgoing connections to (if specified).
667    ///
668    /// Leaving this out will mean the PT uses a sane default.
669    #[builder(default)]
670    outbound_bind_v6: Option<Ipv6Addr>,
671    /// The maximum time we should wait for a pluggable transport binary to report successful
672    /// initialization. If `None`, a default value is used.
673    #[builder(default)]
674    timeout: Option<Duration>,
675}
676
677impl PtCommonParameters {
678    /// Return a new `PtCommonParametersBuilder` for constructing a set of parameters.
679    pub fn builder() -> PtCommonParametersBuilder {
680        PtCommonParametersBuilder::default()
681    }
682
683    /// Convert these parameters into a set of environment variables to be passed to the PT binary
684    /// in accordance with the specification.
685    fn common_environment_variables(&self) -> HashMap<OsString, OsString> {
686        let mut ret = HashMap::new();
687        ret.insert("TOR_PT_MANAGED_TRANSPORT_VER".into(), "1".into());
688        ret.insert(
689            "TOR_PT_STATE_LOCATION".into(),
690            self.state_location.clone().into_os_string(),
691        );
692        ret.insert("TOR_PT_EXIT_ON_STDIN_CLOSE".into(), "1".into());
693        if let Some(v4) = self.outbound_bind_v4 {
694            ret.insert(
695                "TOR_PT_OUTBOUND_BIND_ADDRESS_V4".into(),
696                v4.to_string().into(),
697            );
698        }
699        if let Some(v6) = self.outbound_bind_v6 {
700            // pt-spec.txt: "IPv6 addresses MUST always be wrapped in square brackets."
701            ret.insert(
702                "TOR_PT_OUTBOUND_BIND_ADDRESS_V6".into(),
703                format!("[{}]", v6).into(),
704            );
705        }
706        ret
707    }
708}
709
710/// Parameters passed only to a pluggable transport client.
711#[derive(PartialEq, Eq, Clone, Debug, derive_builder::Builder)]
712pub struct PtClientParameters {
713    /// A SOCKS URI specifying a proxy to use.
714    #[builder(default)]
715    proxy_uri: Option<String>,
716    /// A list of transports to initialise.
717    ///
718    /// The PT launch will fail if all transports are not successfully initialised.
719    transports: Vec<PtTransportName>,
720}
721
722impl PtClientParameters {
723    /// Return a new `PtClientParametersBuilder` for constructing a set of parameters.
724    pub fn builder() -> PtClientParametersBuilder {
725        PtClientParametersBuilder::default()
726    }
727
728    /// Convert these parameters into a set of environment variables to be passed to the PT binary
729    /// in accordance with the specification.
730    fn environment_variables(
731        &self,
732        common_params: &PtCommonParameters,
733    ) -> HashMap<OsString, OsString> {
734        let mut ret = common_params.common_environment_variables();
735        if let Some(ref proxy_uri) = self.proxy_uri {
736            ret.insert("TOR_PT_PROXY".into(), proxy_uri.clone().into());
737        }
738        ret.insert(
739            "TOR_PT_CLIENT_TRANSPORTS".into(),
740            self.transports.iter().join(",").into(),
741        );
742        ret
743    }
744}
745
746/// Parameters passed only to a pluggable transport server.
747#[derive(PartialEq, Eq, Clone, Debug, derive_builder::Builder)]
748pub struct PtServerParameters {
749    /// A list of transports to initialise.
750    ///
751    /// The PT launch will fail if all transports are not successfully initialised.
752    transports: Vec<PtTransportName>,
753    /// Transport options for each server transport
754    #[builder(default)]
755    server_transport_options: String,
756    /// Set host:port on which the server transport should listen for connections
757    #[builder(default)]
758    server_bindaddr: String,
759    /// Set host:port on which the server transport should forward requests
760    #[builder(default)]
761    server_orport: Option<String>,
762    /// Set host:port on which the server transport should forward requests (extended ORPORT)
763    #[builder(default)]
764    server_extended_orport: Option<String>,
765}
766
767impl PtServerParameters {
768    /// Return a new `PtServerParametersBuilder` for constructing a set of parameters.
769    pub fn builder() -> PtServerParametersBuilder {
770        PtServerParametersBuilder::default()
771    }
772
773    /// Convert these parameters into a set of environment variables to be passed to the PT binary
774    /// in accordance with the specification.
775    fn environment_variables(
776        &self,
777        common_params: &PtCommonParameters,
778    ) -> HashMap<OsString, OsString> {
779        let mut ret = common_params.common_environment_variables();
780        ret.insert(
781            "TOR_PT_SERVER_TRANSPORTS".into(),
782            self.transports.iter().join(",").into(),
783        );
784        ret.insert(
785            "TOR_PT_SERVER_TRANSPORT_OPTIONS".into(),
786            self.server_transport_options.clone().into(),
787        );
788        ret.insert(
789            "TOR_PT_SERVER_BINDADDR".into(),
790            self.server_bindaddr.clone().into(),
791        );
792        if let Some(ref server_orport) = self.server_orport {
793            ret.insert("TOR_PT_ORPORT".into(), server_orport.into());
794        }
795        if let Some(ref server_extended_orport) = self.server_extended_orport {
796            ret.insert(
797                "TOR_PT_EXTENDED_SERVER_PORT".into(),
798                server_extended_orport.into(),
799            );
800        }
801        ret
802    }
803}
804
805/// Common functionality implemented to allow code reuse
806#[async_trait::async_trait]
807#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
808pub trait PluggableTransport: PluggableTransportPrivate {
809    /// Get all client methods returned by the binary, if it has been launched.
810    ///
811    /// If it hasn't been launched, the returned map will be empty.
812    // TODO(eta): Actually figure out a way to expose this more stably.
813    fn transport_methods(&self) -> &HashMap<PtTransportName, PtClientMethod>;
814
815    /// Get the next [`PtMessage`] from the running transport. It is recommended to call this
816    /// in a loop once a PT has been launched, in order to forward log messages and find out about
817    /// status updates.
818    //
819    // FIXME(eta): This API will probably go away and get replaced with something better.
820    //             In particular, we'd want to cache `Status` messages from before this method
821    //             was called.
822    async fn next_message(&mut self) -> err::Result<PtMessage> {
823        let inner = self.inner()?;
824        let ret = inner.recv().await;
825        if let Err(PtError::ChildGone) | Err(PtError::ChildReadFailed { .. }) = &ret {
826            // FIXME(eta): Currently this lets the caller still think the methods work by calling
827            //             transport_methods.
828            debug!(
829                "PT {}: Received {:?}; shutting down.",
830                self.identifier(),
831                ret
832            );
833            self.set_inner(None);
834        }
835        ret
836    }
837}
838/// A pluggable transport binary in a child process.
839///
840/// These start out inert, and must be launched with [`PluggableClientTransport::launch`] in order
841/// to be useful.
842#[derive(Debug)]
843pub struct PluggableClientTransport {
844    /// The currently running child, if there is one.
845    inner: Option<AsyncPtChild>,
846    /// The path to the binary to run.
847    pub(crate) binary_path: PathBuf,
848    /// Arguments to pass to the binary.
849    arguments: Vec<String>,
850    /// Configured parameters.
851    common_params: PtCommonParameters,
852    /// Configured client-only parameters.
853    client_params: PtClientParameters,
854    /// Information about client methods obtained from the PT.
855    cmethods: HashMap<PtTransportName, PtClientMethod>,
856}
857
858impl PluggableTransport for PluggableClientTransport {
859    fn transport_methods(&self) -> &HashMap<PtTransportName, PtClientMethod> {
860        &self.cmethods
861    }
862}
863
864impl PluggableTransportPrivate for PluggableClientTransport {
865    fn inner(&mut self) -> Result<&mut AsyncPtChild, PtError> {
866        self.inner.as_mut().ok_or(PtError::ChildGone)
867    }
868    fn set_inner(&mut self, newval: Option<AsyncPtChild>) {
869        self.inner = newval;
870    }
871    fn identifier(&self) -> &str {
872        match &self.inner {
873            Some(child) => &child.identifier,
874            None => "<not yet launched>",
875        }
876    }
877    fn specific_params_contains(&self, transport: &PtTransportName) -> bool {
878        self.client_params.transports.contains(transport)
879    }
880}
881
882impl PluggableClientTransport {
883    /// Create a new pluggable transport wrapper, wrapping the binary at `binary_path` and passing
884    /// the `params` to it.
885    ///
886    /// You must call [`PluggableClientTransport::launch`] to actually run the PT.
887    pub fn new(
888        binary_path: PathBuf,
889        arguments: Vec<String>,
890        common_params: PtCommonParameters,
891        client_params: PtClientParameters,
892    ) -> Self {
893        Self {
894            common_params,
895            client_params,
896            arguments,
897            binary_path,
898            inner: None,
899            cmethods: Default::default(),
900        }
901    }
902
903    /// Launch the pluggable transport, executing the binary.
904    ///
905    /// Will return an error if the launch fails, one of the transports fail, not all transports
906    /// were launched, or the launch times out.
907    pub async fn launch<R: Runtime>(&mut self, rt: R) -> err::Result<()> {
908        let all_env_vars = self
909            .client_params
910            .environment_variables(&self.common_params);
911
912        let mut async_child =
913            <PluggableClientTransport as PluggableTransportPrivate>::get_child_from_pt_launch(
914                &self.inner,
915                &self.client_params.transports,
916                &self.binary_path,
917                &self.arguments,
918                all_env_vars,
919            )?;
920
921        let deadline = Instant::get() + self.common_params.timeout.unwrap_or(PT_START_TIMEOUT);
922        let mut cmethods = HashMap::new();
923        let mut proxy_done = self.client_params.proxy_uri.is_none();
924
925        loop {
926            match self
927                .try_match_common_messages(&rt, deadline, &mut async_child)
928                .await
929            {
930                Ok(maybe_message) => {
931                    if let Some(message) = maybe_message {
932                        match message {
933                            PtMessage::ClientTransportLaunched {
934                                transport,
935                                protocol,
936                                endpoint,
937                            } => {
938                                self.common_transport_launched_handler(
939                                    Some(protocol),
940                                    transport,
941                                    endpoint,
942                                    &mut cmethods,
943                                )?;
944                            }
945                            PtMessage::ProxyDone => {
946                                if proxy_done {
947                                    return Err(PtError::ProtocolViolation(
948                                        "binary initiated proxy when not asked (or twice)".into(),
949                                    ));
950                                }
951                                info!("PT binary now proxying connections via supplied URI");
952                                proxy_done = true;
953                            }
954                            // TODO: unify most of the handling of ClientTransportsDone with ServerTransportsDone
955                            PtMessage::ClientTransportsDone => {
956                                let unsupported = self
957                                    .client_params
958                                    .transports
959                                    .iter()
960                                    .filter(|&x| !cmethods.contains_key(x))
961                                    .map(|x| x.to_string())
962                                    .collect::<Vec<_>>();
963                                if !unsupported.is_empty() {
964                                    warn!(
965                                        "PT binary failed to initialise transports: {:?}",
966                                        unsupported
967                                    );
968                                    return Err(PtError::ClientTransportsUnsupported(unsupported));
969                                }
970                                info!("PT binary initialisation done");
971                                break;
972                            }
973                            x => {
974                                return Err(PtError::ProtocolViolation(format!(
975                                    "received unexpected {:?}",
976                                    x
977                                )));
978                            }
979                        }
980                    }
981                }
982                Err(e) => return Err(e),
983            }
984        }
985        self.cmethods = cmethods;
986        self.inner = Some(async_child);
987        // TODO(eta): We need to expose the log and status messages after this function exits!
988        Ok(())
989    }
990}
991
992/// A pluggable transport server binary in a child process.
993///
994/// These start out inert, and must be launched with [`PluggableServerTransport::launch`] in order
995/// to be useful.
996#[derive(Debug)]
997pub struct PluggableServerTransport {
998    /// The currently running child, if there is one.
999    inner: Option<AsyncPtChild>,
1000    /// The path to the binary to run.
1001    pub(crate) binary_path: PathBuf,
1002    /// Arguments to pass to the binary.
1003    arguments: Vec<String>,
1004    /// Configured parameters.
1005    common_params: PtCommonParameters,
1006    /// Configured server-only parameters.
1007    server_params: PtServerParameters,
1008    /// Information about server methods obtained from the PT.
1009    smethods: HashMap<PtTransportName, PtClientMethod>,
1010}
1011
1012impl PluggableTransportPrivate for PluggableServerTransport {
1013    fn inner(&mut self) -> Result<&mut AsyncPtChild, PtError> {
1014        self.inner.as_mut().ok_or(PtError::ChildGone)
1015    }
1016    fn set_inner(&mut self, newval: Option<AsyncPtChild>) {
1017        self.inner = newval;
1018    }
1019    fn identifier(&self) -> &str {
1020        match &self.inner {
1021            Some(child) => &child.identifier,
1022            None => "<not yet launched>",
1023        }
1024    }
1025    fn specific_params_contains(&self, transport: &PtTransportName) -> bool {
1026        self.server_params.transports.contains(transport)
1027    }
1028}
1029
1030impl PluggableTransport for PluggableServerTransport {
1031    fn transport_methods(&self) -> &HashMap<PtTransportName, PtClientMethod> {
1032        &self.smethods
1033    }
1034}
1035
1036impl PluggableServerTransport {
1037    /// Create a new pluggable transport wrapper, wrapping the binary at `binary_path` and passing
1038    /// the `params` to it.
1039    ///
1040    /// You must call [`PluggableServerTransport::launch`] to actually run the PT.
1041    pub fn new(
1042        binary_path: PathBuf,
1043        arguments: Vec<String>,
1044        common_params: PtCommonParameters,
1045        server_params: PtServerParameters,
1046    ) -> Self {
1047        Self {
1048            common_params,
1049            server_params,
1050            arguments,
1051            binary_path,
1052            inner: None,
1053            smethods: Default::default(),
1054        }
1055    }
1056
1057    /// Launch the pluggable transport, executing the binary.
1058    ///
1059    /// Will return an error if the launch fails, one of the transports fail, not all transports
1060    /// were launched, or the launch times out.
1061    pub async fn launch<R: Runtime>(&mut self, rt: R) -> err::Result<()> {
1062        let all_env_vars = self
1063            .server_params
1064            .environment_variables(&self.common_params);
1065
1066        let mut async_child =
1067            <PluggableServerTransport as PluggableTransportPrivate>::get_child_from_pt_launch(
1068                &self.inner,
1069                &self.server_params.transports,
1070                &self.binary_path,
1071                &self.arguments,
1072                all_env_vars,
1073            )?;
1074
1075        let deadline = Instant::get() + self.common_params.timeout.unwrap_or(PT_START_TIMEOUT);
1076        let mut smethods = HashMap::new();
1077
1078        loop {
1079            match self
1080                .try_match_common_messages(&rt, deadline, &mut async_child)
1081                .await
1082            {
1083                Ok(maybe_message) => {
1084                    if let Some(message) = maybe_message {
1085                        match message {
1086                            PtMessage::ServerTransportLaunched {
1087                                transport,
1088                                endpoint,
1089                                options: _,
1090                            } => {
1091                                self.common_transport_launched_handler(
1092                                    None,
1093                                    transport,
1094                                    endpoint,
1095                                    &mut smethods,
1096                                )?;
1097                            }
1098                            PtMessage::ServerTransportsDone => {
1099                                let unsupported = self
1100                                    .server_params
1101                                    .transports
1102                                    .iter()
1103                                    .filter(|&x| !smethods.contains_key(x))
1104                                    .map(|x| x.to_string())
1105                                    .collect::<Vec<_>>();
1106                                if !unsupported.is_empty() {
1107                                    warn!(
1108                                        "PT binary failed to initialise transports: {:?}",
1109                                        unsupported
1110                                    );
1111                                    return Err(PtError::ClientTransportsUnsupported(unsupported));
1112                                }
1113                                info!("PT binary initialisation done");
1114                                break;
1115                            }
1116                            x => {
1117                                return Err(PtError::ProtocolViolation(format!(
1118                                    "received unexpected {:?}",
1119                                    x
1120                                )));
1121                            }
1122                        }
1123                    }
1124                }
1125                Err(e) => return Err(e),
1126            }
1127        }
1128        self.smethods = smethods;
1129        self.inner = Some(async_child);
1130        // TODO(eta): We need to expose the log and status messages after this function exits!
1131        Ok(())
1132    }
1133}
1134
1135#[cfg(test)]
1136mod test {
1137    // @@ begin test lint list maintained by maint/add_warning @@
1138    #![allow(clippy::bool_assert_comparison)]
1139    #![allow(clippy::clone_on_copy)]
1140    #![allow(clippy::dbg_macro)]
1141    #![allow(clippy::mixed_attributes_style)]
1142    #![allow(clippy::print_stderr)]
1143    #![allow(clippy::print_stdout)]
1144    #![allow(clippy::single_char_pattern)]
1145    #![allow(clippy::unwrap_used)]
1146    #![allow(clippy::unchecked_time_subtraction)]
1147    #![allow(clippy::useless_vec)]
1148    #![allow(clippy::needless_pass_by_value)]
1149    #![allow(clippy::string_slice)] // See arti#2571
1150    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
1151
1152    use crate::ipc::{PtMessage, PtStatus};
1153    use std::borrow::Cow;
1154    use std::collections::HashMap;
1155    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
1156
1157    #[test]
1158    fn it_parses_spec_examples() {
1159        assert_eq!(
1160            "VERSION-ERROR no-version".parse(),
1161            Ok(PtMessage::VersionError("no-version".into()))
1162        );
1163        assert_eq!("VERSION 1".parse(), Ok(PtMessage::Version("1".into())));
1164        assert_eq!(
1165            "ENV-ERROR No TOR_PT_AUTH_COOKIE_FILE when TOR_PT_EXTENDED_SERVER_PORT set".parse(),
1166            Ok(PtMessage::EnvError(
1167                "No TOR_PT_AUTH_COOKIE_FILE when TOR_PT_EXTENDED_SERVER_PORT set".into()
1168            ))
1169        );
1170        assert_eq!("PROXY DONE".parse(), Ok(PtMessage::ProxyDone));
1171        assert_eq!(
1172            "PROXY-ERROR SOCKS 4 upstream proxies unsupported".parse(),
1173            Ok(PtMessage::ProxyError(
1174                "SOCKS 4 upstream proxies unsupported".into()
1175            ))
1176        );
1177        assert_eq!(
1178            "CMETHOD trebuchet socks5 127.0.0.1:19999".parse(),
1179            Ok(PtMessage::ClientTransportLaunched {
1180                transport: "trebuchet".parse().unwrap(),
1181                protocol: "socks5".to_string(),
1182                endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 19999)
1183            })
1184        );
1185        assert_eq!(
1186            "CMETHOD-ERROR trebuchet no rocks available".parse(),
1187            Ok(PtMessage::ClientTransportFailed {
1188                transport: "trebuchet".parse().unwrap(),
1189                message: "no rocks available".to_string()
1190            })
1191        );
1192        assert_eq!("CMETHODS DONE".parse(), Ok(PtMessage::ClientTransportsDone));
1193        assert_eq!(
1194            "SMETHOD trebuchet 198.51.100.1:19999".parse(),
1195            Ok(PtMessage::ServerTransportLaunched {
1196                transport: "trebuchet".parse().unwrap(),
1197                endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)), 19999),
1198                options: Default::default()
1199            })
1200        );
1201        let mut map = HashMap::new();
1202        map.insert("N".to_string(), "13".to_string());
1203        assert_eq!(
1204            "SMETHOD rot_by_N 198.51.100.1:2323 ARGS:N=13".parse(),
1205            Ok(PtMessage::ServerTransportLaunched {
1206                transport: "rot_by_N".parse().unwrap(),
1207                endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)), 2323),
1208                options: map
1209            })
1210        );
1211        let mut map = HashMap::new();
1212        map.insert(
1213            "cert".to_string(),
1214            "HszPy3vWfjsESCEOo9ZBkRv6zQ/1mGHzc8arF0y2SpwFr3WhsMu8rK0zyaoyERfbz3ddFw".to_string(),
1215        );
1216        map.insert("iat-mode".to_string(), "0".to_string());
1217        assert_eq!(
1218            "SMETHOD obfs4 198.51.100.1:43734 ARGS:cert=HszPy3vWfjsESCEOo9ZBkRv6zQ/1mGHzc8arF0y2SpwFr3WhsMu8rK0zyaoyERfbz3ddFw,iat-mode=0".parse(),
1219            Ok(PtMessage::ServerTransportLaunched {
1220                transport: "obfs4".parse().unwrap(),
1221                endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)), 43734),
1222                options: map
1223            })
1224        );
1225        assert_eq!(
1226            "SMETHOD-ERROR trebuchet no cows available".parse(),
1227            Ok(PtMessage::ServerTransportFailed {
1228                transport: "trebuchet".parse().unwrap(),
1229                message: "no cows available".to_string()
1230            })
1231        );
1232        assert_eq!(
1233            "LOG SEVERITY=debug MESSAGE=\"Connected to bridge A\"".parse(),
1234            Ok(PtMessage::Log {
1235                severity: "debug".to_string(),
1236                message: "Connected to bridge A".to_string()
1237            })
1238        );
1239        assert_eq!(
1240            "LOG SEVERITY=debug MESSAGE=\"\\r\\n\\t\"".parse(),
1241            Ok(PtMessage::Log {
1242                severity: "debug".to_string(),
1243                message: "\r\n\t".to_string()
1244            })
1245        );
1246        assert_eq!(
1247            "LOG SEVERITY=debug MESSAGE=".parse(),
1248            Ok(PtMessage::Log {
1249                severity: "debug".to_string(),
1250                message: "".to_string()
1251            })
1252        );
1253        assert_eq!(
1254            "LOG SEVERITY=debug MESSAGE=\"\\a\"".parse::<PtMessage>(),
1255            Ok(PtMessage::Log {
1256                severity: "debug".to_string(),
1257                message: "a".to_string()
1258            })
1259        );
1260
1261        for i in 0..9 {
1262            let msg = format!("LOG SEVERITY=debug MESSAGE=\"\\{i}\"");
1263            assert_eq!(
1264                msg.parse::<PtMessage>(),
1265                Err(Cow::from("attempted unsupported octal escape code"))
1266            );
1267        }
1268        assert_eq!(
1269            "SMETHOD obfs4 198.51.100.1:43734 ARGS:iat-mode=0\\".parse::<PtMessage>(),
1270            Err(Cow::from(
1271                "failed to parse SMETHOD ARGS: smethod arg terminates with backslash"
1272            ))
1273        );
1274        assert_eq!(
1275            "SMETHOD obfs4 198.51.100.1:43734 ARGS:iat-mode=fo=o".parse::<PtMessage>(),
1276            Err(Cow::from(
1277                "failed to parse SMETHOD ARGS: encountered = while parsing value"
1278            ))
1279        );
1280        assert_eq!(
1281            "SMETHOD obfs4 198.51.100.1:43734 ARGS:iat-mode".parse::<PtMessage>(),
1282            Err(Cow::from(
1283                "failed to parse SMETHOD ARGS: ran out of chars parsing smethod arg"
1284            ))
1285        );
1286
1287        let mut map = HashMap::new();
1288        map.insert("ADDRESS".to_string(), "198.51.100.123:1234".to_string());
1289        map.insert("CONNECT".to_string(), "Success".to_string());
1290        assert_eq!(
1291            "STATUS ADDRESS=198.51.100.123:1234 CONNECT=Success".parse(),
1292            Ok(PtMessage::Status(PtStatus { data: map }))
1293        );
1294
1295        let mut map = HashMap::new();
1296        map.insert("ADDRESS".to_string(), "198.51.100.123:1234".to_string());
1297        map.insert("CONNECT".to_string(), "Success".to_string());
1298        map.insert("TRANSPORT".to_string(), "obfs4".to_string());
1299        assert_eq!(
1300            "STATUS TRANSPORT=obfs4 ADDRESS=198.51.100.123:1234 CONNECT=Success".parse(),
1301            Ok(PtMessage::Status(PtStatus { data: map }))
1302        );
1303
1304        let mut map = HashMap::new();
1305        map.insert("ADDRESS".to_string(), "198.51.100.222:2222".to_string());
1306        map.insert("CONNECT".to_string(), "Failed".to_string());
1307        map.insert("FINGERPRINT".to_string(), "<Fingerprint>".to_string());
1308        map.insert("ERRSTR".to_string(), "Connection refused".to_string());
1309        assert_eq!(
1310            "STATUS ADDRESS=198.51.100.222:2222 CONNECT=Failed FINGERPRINT=<Fingerprint> ERRSTR=\"Connection refused\"".parse(),
1311            Ok(PtMessage::Status(PtStatus {
1312                data: map
1313            }))
1314        );
1315    }
1316}