Skip to main content

tor_ptmgr/
config.rs

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