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}