tor_interface/
censorship_circumvention.rs

1// standard
2use std::net::SocketAddr;
3use std::path::PathBuf;
4use std::str::FromStr;
5use std::sync::OnceLock;
6
7// extern crates
8use regex::Regex;
9
10#[derive(Clone, Debug)]
11/// Configuration for a pluggable-transport
12pub struct PluggableTransportConfig {
13    transports: Vec<String>,
14    path_to_binary: PathBuf,
15    options: Vec<String>,
16}
17
18#[derive(thiserror::Error, Debug)]
19/// Error returned on failure to construct a [`PluggableTransportConfig`]
20pub enum PluggableTransportConfigError {
21    #[error("pluggable transport name '{0}' is invalid")]
22    /// transport names must be a valid C identifier
23    TransportNameInvalid(String),
24    #[error("unable to use '{0}' as pluggable transport binary path, {1}")]
25    /// configuration only allows aboslute paths to binaries
26    BinaryPathInvalid(String, String),
27}
28
29// per the PT spec: https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/main/releases/PTSpecV1.0/pt-1_0.txt
30static TRANSPORT_PATTERN: OnceLock<Regex> = OnceLock::new();
31fn init_transport_pattern() -> Regex {
32    Regex::new(r"(?m)^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap()
33}
34
35/// Configuration struct for a pluggable-transport which conforms to the v1.0 pluggable-transport [specification](https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/main/releases/PTSpecV1.0/pt-1_0.txt)
36impl PluggableTransportConfig {
37    /// Construct a new `PluggableTransportConfig`. Each `transport` string must be a [valid C identifier](https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/c92e59a9fa6ba11c181f4c5ec9d533eaa7d9d7f3/releases/PTSpecV1.0/pt-1_0.txt#L144) while `path_to_binary` must be an absolute path.
38    pub fn new(
39        transports: Vec<String>,
40        path_to_binary: PathBuf,
41    ) -> Result<Self, PluggableTransportConfigError> {
42        let transport_pattern = TRANSPORT_PATTERN.get_or_init(init_transport_pattern);
43        // validate each transport
44        for transport in &transports {
45            if !transport_pattern.is_match(transport) {
46                return Err(PluggableTransportConfigError::TransportNameInvalid(
47                    transport.clone(),
48                ));
49            }
50        }
51
52        // pluggable transport path must be absolute so we can fix it up for individual
53        // TorProvider implementations
54        if !path_to_binary.is_absolute() {
55            return Err(PluggableTransportConfigError::BinaryPathInvalid(
56                format!("{:?}", path_to_binary.display()),
57                "must be an absolute path".to_string(),
58            ));
59        }
60
61        Ok(Self {
62            transports,
63            path_to_binary,
64            options: Default::default(),
65        })
66    }
67
68    /// Get a reference to this `PluggableTransportConfig`'s list of transports.
69    pub fn transports(&self) -> &Vec<String> {
70        &self.transports
71    }
72
73    /// Get a reference to this `PluggableTransportConfig`'s `PathBuf` containing the absolute path to the pluggable-transport binary.
74    pub fn path_to_binary(&self) -> &PathBuf {
75        &self.path_to_binary
76    }
77
78    /// Get a reference to this `PluggableTransportConfig`'s list of command-line options
79    pub fn options(&self) -> &Vec<String> {
80        &self.options
81    }
82
83    /// Add a command-line option used to invoke this pluggable-transport.
84    pub fn add_option(&mut self, arg: String) {
85        self.options.push(arg);
86    }
87}
88
89/// Configuration for a bridge line to be used with a pluggable-transport
90#[derive(Clone, Debug)]
91pub struct BridgeLine {
92    transport: String,
93    address: SocketAddr,
94    fingerprint: Option<String>,
95    keyvalues: Vec<(String, String)>,
96}
97
98#[derive(thiserror::Error, Debug)]
99/// Error returned on failure to construct a [`BridgeLine`]
100pub enum BridgeLineError {
101    #[error("bridge line '{0}' missing transport")]
102    /// Provided bridge line missing transport
103    TransportMissing(String),
104
105    #[error("bridge line '{0}' missing address")]
106    /// Provided bridge line missing address
107    AddressMissing(String),
108
109    #[error("bridge line '{0}' missing fingerprint")]
110    /// Provided bridge line missing fingerprint
111    FingerprintMissing(String),
112
113    #[error("transport name '{0}' is invalid")]
114    /// Invalid transport name (must be a valid C identifier)
115    TransportNameInvalid(String),
116
117    #[error("address '{0}' cannot be parsed as IP:PORT")]
118    /// Provided bridge line's address not parseable
119    AddressParseFailed(String),
120
121    #[error("key=value '{0}' is invalid")]
122    /// A key/value pair in invalid format
123    KeyValueInvalid(String),
124
125    #[error("bridge address port must not be 0")]
126    /// Invalid bridge address port
127    AddressPortInvalid,
128
129    #[error("fingerprint '{0}' is invalid")]
130    /// Fingerprint is not parseable (must be length 40 base16 string)
131    FingerprintInvalid(String),
132}
133
134/// A `BridgeLine` contains the information required to connect to a bridge through the means of a particular pluggable-transport (defined in a `PluggableTransportConfi`). For more information, see:
135/// - [https://tb-manual.torproject.org/bridges/](https://tb-manual.torproject.org/bridges/)
136impl BridgeLine {
137    /// Construct a new `BridgeLine` from its constituent parts. The `transport` argument must be a valid C identifier and must have an associated `transport` defined in an associated `PluggableTransportConfig`. The `address` must have a non-zero port. The `fingerprint` is a length 40 base16-encoded string. Finally, the keys in the `keyvalues` list must not contain space (` `) or equal (`=`) characters.
138    ///
139    /// In practice, bridge lines are distributed as entire strings so most consumers of these APIs are not likely to need this particular function.
140    pub fn new(
141        transport: String,
142        address: SocketAddr,
143        fingerprint: Option<String>,
144        keyvalues: Vec<(String, String)>,
145    ) -> Result<BridgeLine, BridgeLineError> {
146        let transport_pattern = TRANSPORT_PATTERN.get_or_init(init_transport_pattern);
147
148        // transports have a particular pattern
149        if !transport_pattern.is_match(&transport) {
150            return Err(BridgeLineError::TransportNameInvalid(transport));
151        }
152
153        // port can't be 0
154        if address.port() == 0 {
155            return Err(BridgeLineError::AddressPortInvalid);
156        }
157
158        let fingerprint = if let Some(fingerprint) = fingerprint {
159            // fingerprint should be a sha1 hash
160            if !Self::is_bridge_fingerprint_pattern(&fingerprint) {
161                return Err(BridgeLineError::FingerprintInvalid(fingerprint));
162            }
163            Some(fingerprint)
164        } else {
165            None
166        };
167
168        // validate key-values
169        for (key, value) in &keyvalues {
170            if key.contains(' ') || key.contains('=') || key.is_empty() {
171                return Err(BridgeLineError::KeyValueInvalid(format!("{key}={value}")));
172            }
173        }
174
175        Ok(Self {
176            transport,
177            address,
178            fingerprint,
179            keyvalues,
180        })
181    }
182
183    fn is_bridge_fingerprint_pattern(fingerprint: &str) -> bool {
184        static BRIDGE_FINGERPRINT_PATTERN: OnceLock<Regex> = OnceLock::new();
185        let bridge_fingerprint_pattern = BRIDGE_FINGERPRINT_PATTERN
186            .get_or_init(|| Regex::new(r"(?m)^[0-9a-fA-F]{40}$").unwrap());
187
188        bridge_fingerprint_pattern.is_match(fingerprint)
189    }
190
191    /// Get a reference to this `BridgeLine`'s transport field.
192    pub fn transport(&self) -> &String {
193        &self.transport
194    }
195
196    /// Get a reference to this `BridgeLine`'s address field.
197    pub fn address(&self) -> &SocketAddr {
198        &self.address
199    }
200
201    /// Get a reference to this `BridgeLine`'s fingerprint field.
202    pub fn fingerprint(&self) -> &Option<String> {
203        &self.fingerprint
204    }
205
206    /// Get a reference to this `BridgeLine`'s key/values field.
207    pub fn keyvalues(&self) -> &Vec<(String, String)> {
208        &self.keyvalues
209    }
210
211    #[cfg(feature = "legacy-tor-provider")]
212    /// Serialise this `BridgeLine` to the value set via `SETCONF Bridge...` legacy c-tor control-port command.
213    pub fn as_legacy_tor_setconf_value(&self) -> String {
214        let transport = &self.transport;
215        let address = self.address.to_string();
216        let keyvalues: Vec<String> = self
217            .keyvalues
218            .iter()
219            .map(|(key, value)| format!("{key}={value}"))
220            .collect();
221        let keyvalues = keyvalues.join(" ");
222
223        if let Some(fingerprint) = &self.fingerprint {
224            format!("{transport} {address} {fingerprint} {keyvalues}")
225        } else {
226            format!("{transport} {address} {keyvalues}")
227        }
228    }
229}
230
231impl FromStr for BridgeLine {
232    type Err = BridgeLineError;
233    fn from_str(s: &str) -> Result<Self, Self::Err> {
234        let mut tokens = s.split(' ').peekable();
235        // get transport name
236        let transport = if let Some(transport) = tokens.next() {
237            transport
238        } else {
239            return Err(BridgeLineError::TransportMissing(s.to_string()));
240        };
241        // get bridge address
242        let address = if let Some(address) = tokens.next() {
243            if let Ok(address) = SocketAddr::from_str(address) {
244                address
245            } else {
246                return Err(BridgeLineError::AddressParseFailed(address.to_string()));
247            }
248        } else {
249            return Err(BridgeLineError::AddressMissing(s.to_string()));
250        };
251        // get the bridge fingerprint
252        let fingerprint = if let Some(fingerprint) = tokens.peek() {
253            if Self::is_bridge_fingerprint_pattern(fingerprint) {
254                Some(tokens.next().unwrap().to_string())
255            } else {
256                None
257            }
258        } else {
259            None
260        };
261
262        // get the bridge options
263        static BRIDGE_OPTION_PATTERN: OnceLock<Regex> = OnceLock::new();
264        let bridge_option_pattern = BRIDGE_OPTION_PATTERN
265            .get_or_init(|| Regex::new(r"(?m)^(?<key>[^=]+)=(?<value>.*)$").unwrap());
266
267        let mut keyvalues: Vec<(String, String)> = Default::default();
268        for keyvalue in tokens {
269            if let Some(caps) = bridge_option_pattern.captures(keyvalue) {
270                let key = caps
271                    .name("key")
272                    .expect("missing key group")
273                    .as_str()
274                    .to_string();
275                let value = caps
276                    .name("value")
277                    .expect("missing value group")
278                    .as_str()
279                    .to_string();
280                keyvalues.push((key, value));
281            } else {
282                return Err(BridgeLineError::KeyValueInvalid(keyvalue.to_string()));
283            }
284        }
285
286        BridgeLine::new(transport.to_string(), address, fingerprint, keyvalues)
287    }
288}