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}