tor_ptmgr/
config.rs

1//! Configuration logic for tor-ptmgr.
2
3use std::net::SocketAddr;
4
5use derive_builder::Builder;
6use serde::{Deserialize, Serialize};
7use tor_config::{impl_standard_builder, ConfigBuildError};
8use tor_config_path::CfgPath;
9use tor_linkspec::PtTransportName;
10
11#[cfg(feature = "tor-channel-factory")]
12use {crate::PtClientMethod, tor_socksproto::SocksVersion};
13
14/// A single pluggable transport.
15///
16/// Pluggable transports are programs that transform and obfuscate traffic on
17/// the network between a Tor client and a Tor bridge, so that an adversary
18/// cannot recognize it as Tor traffic.
19///
20/// A pluggable transport can be either _managed_ (run as an external process
21/// that we launch and monitor), or _unmanaged_ (running on a local port, not
22/// controlled by Arti).
23#[derive(Clone, Debug, Builder, Eq, PartialEq)]
24#[builder(derive(Debug, Serialize, Deserialize))]
25#[builder(build_fn(error = "ConfigBuildError", validate = "Self::validate"))]
26pub struct TransportConfig {
27    /// Names of the transport protocols that we are willing to use from this transport.
28    ///
29    /// (These protocols are arbitrary identifiers that describe which protocols
30    /// we want. They must match names that the binary knows how to provide.)
31    //
32    // NOTE(eta): This doesn't use the list builder stuff, because you're not likely to
33    //            set this field more than once.
34    pub(crate) protocols: Vec<PtTransportName>,
35
36    /// The path to the binary to run, if any.
37    ///
38    /// This needs to be the path to some executable file on disk.
39    ///
40    /// Present only for managed transports.
41    #[builder(default, setter(strip_option))]
42    pub(crate) path: Option<CfgPath>,
43
44    /// One or more command-line arguments to pass to the binary.
45    ///
46    /// Meaningful only for managed transports.
47    // TODO: Should this be OsString? That's a pain to parse...
48    //
49    // NOTE(eta): This doesn't use the list builder stuff, because you're not likely to
50    //            set this field more than once.
51    #[builder(default)]
52    pub(crate) arguments: Vec<String>,
53
54    /// The location at which to contact this transport.
55    ///
56    /// Present only for unmanaged transports.
57    #[builder(default, setter(strip_option))]
58    pub(crate) proxy_addr: Option<SocketAddr>,
59
60    /// If true, launch this transport on startup.  Otherwise, we launch
61    /// it on demand.
62    ///
63    /// Meaningful only for managed transports.
64    #[builder(default)]
65    pub(crate) run_on_startup: bool,
66}
67
68impl_standard_builder! { TransportConfig: !Default }
69
70impl TransportConfigBuilder {
71    /// Inspect the list of protocols (ie, transport names)
72    ///
73    /// If none have yet been specified, returns an empty list.
74    pub fn get_protocols(&self) -> &[PtTransportName] {
75        self.protocols.as_deref().unwrap_or_default()
76    }
77
78    /// Make sure that this builder is internally consistent.
79    fn validate(&self) -> Result<(), ConfigBuildError> {
80        // `path` can only be set if the `managed-pts` feature is enabled
81        #[cfg(not(feature = "managed-pts"))]
82        if self.path.is_some() {
83            return Err(ConfigBuildError::NoCompileTimeSupport {
84                field: "path".into(),
85                problem:
86                    "Indicates a managed transport, but support is not enabled by cargo features"
87                        .into(),
88            });
89        }
90
91        match (&self.path, &self.proxy_addr) {
92            (Some(_), Some(_)) => Err(ConfigBuildError::Inconsistent {
93                fields: vec!["path".into(), "proxy_addr".into()],
94                problem: "Cannot provide both path and proxy_addr".into(),
95            }),
96            // TODO: There is no ConfigBuildError for "one of two fields is missing."
97            (None, None) => Err(ConfigBuildError::MissingField {
98                field: "{path or proxy_addr}".into(),
99            }),
100            (None, Some(_)) => {
101                if self.arguments.as_ref().is_some_and(|v| !v.is_empty()) {
102                    Err(ConfigBuildError::Inconsistent {
103                        fields: vec!["proxy_addr".into(), "arguments".into()],
104                        problem: "Cannot provide arguments for an unmanaged transport".into(),
105                    })
106                } else if self.run_on_startup.is_some() {
107                    Err(ConfigBuildError::Inconsistent {
108                        fields: vec!["proxy_addr".into(), "run_on_startup".into()],
109                        problem: "run_on_startup is meaningless for an unmanaged transport".into(),
110                    })
111                } else {
112                    Ok(())
113                }
114            }
115            (Some(_), None) => Ok(()),
116        }
117    }
118}
119
120/// The pluggable transport structure used internally. This is more type-safe than working with
121/// `TransportConfig` directly, since we can't change `TransportConfig` as it's part of the public
122/// API.
123#[derive(Clone, Debug, Eq, PartialEq)]
124pub(crate) enum TransportOptions {
125    /// Options for a managed PT transport.
126    #[cfg(feature = "managed-pts")]
127    Managed(ManagedTransportOptions),
128    /// Options for an unmanaged PT transport.
129    Unmanaged(UnmanagedTransportOptions),
130}
131
132impl TryFrom<TransportConfig> for TransportOptions {
133    type Error = tor_error::Bug;
134    fn try_from(config: TransportConfig) -> Result<Self, Self::Error> {
135        // We rely on the validation performed in `TransportConfigBuilder::validate` to ensure that
136        // mutually exclusive options were not set. We could do validation again here, but it would
137        // be error-prone to duplicate the validation logic. We also couldn't check things like if
138        // `run_on_startup` was `Some`/`None`, since that's only available to the builder.
139
140        if let Some(path) = config.path {
141            cfg_if::cfg_if! {
142                if #[cfg(feature = "managed-pts")] {
143                    Ok(TransportOptions::Managed(ManagedTransportOptions {
144                        protocols: config.protocols,
145                        path,
146                        arguments: config.arguments,
147                        run_on_startup: config.run_on_startup,
148                    }))
149                } else {
150                    let _ = path;
151                    Err(tor_error::internal!(
152                        "Path is set but 'managed-pts' feature is not enabled. How did this pass builder validation?"
153                    ))
154                }
155            }
156        } else if let Some(proxy_addr) = config.proxy_addr {
157            Ok(TransportOptions::Unmanaged(UnmanagedTransportOptions {
158                protocols: config.protocols,
159                proxy_addr,
160            }))
161        } else {
162            Err(tor_error::internal!(
163                "Neither path nor proxy are set. How did this pass builder validation?"
164            ))
165        }
166    }
167}
168
169/// A pluggable transport that is run as an external process that we launch and monitor.
170#[cfg(feature = "managed-pts")]
171#[derive(Clone, Debug, Eq, PartialEq)]
172pub(crate) struct ManagedTransportOptions {
173    /// See [TransportConfig::protocols].
174    pub(crate) protocols: Vec<PtTransportName>,
175
176    /// See [TransportConfig::path].
177    pub(crate) path: CfgPath,
178
179    /// See [TransportConfig::arguments].
180    pub(crate) arguments: Vec<String>,
181
182    /// See [TransportConfig::run_on_startup].
183    pub(crate) run_on_startup: bool,
184}
185
186/// A pluggable transport running on a local port, not controlled by Arti.
187#[derive(Clone, Debug, Eq, PartialEq)]
188pub(crate) struct UnmanagedTransportOptions {
189    /// See [TransportConfig::protocols].
190    pub(crate) protocols: Vec<PtTransportName>,
191
192    /// See [TransportConfig::proxy_addr].
193    pub(crate) proxy_addr: SocketAddr,
194}
195
196impl UnmanagedTransportOptions {
197    /// A client method that can be used to contact this transport.
198    #[cfg(feature = "tor-channel-factory")]
199    pub(crate) fn cmethod(&self) -> PtClientMethod {
200        PtClientMethod {
201            // TODO: Someday we might want to support other protocols;
202            // but for now, let's see if we can get away with just socks5.
203            kind: SocksVersion::V5,
204            endpoint: self.proxy_addr,
205        }
206    }
207
208    /// Return true if this transport is configured on localhost.
209    pub(crate) fn is_localhost(&self) -> bool {
210        self.proxy_addr.ip().is_loopback()
211    }
212}