Skip to main content

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, PartialEq)]
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    /// Serialise this `BridgeLine` to the value set via `SETCONF Bridge...` legacy c-tor control-port command.
212    pub fn as_legacy_tor_setconf_value(&self) -> String {
213        let transport = &self.transport;
214        let address = self.address.to_string();
215        let keyvalues: Vec<String> = self
216            .keyvalues
217            .iter()
218            .map(|(key, value)| format!("{key}={value}"))
219            .collect();
220        let keyvalues = keyvalues.join(" ");
221
222        if let Some(fingerprint) = &self.fingerprint {
223            format!("{transport} {address} {fingerprint} {keyvalues}")
224        } else {
225            format!("{transport} {address} {keyvalues}")
226        }
227    }
228}
229
230impl ToString for BridgeLine {
231    fn to_string(&self) -> String {
232        self.as_legacy_tor_setconf_value()
233    }
234}
235
236impl FromStr for BridgeLine {
237    type Err = BridgeLineError;
238    fn from_str(s: &str) -> Result<Self, Self::Err> {
239        let mut tokens = s.split(' ').peekable();
240        // get transport name
241        let transport = if let Some(transport) = tokens.next() {
242            transport
243        } else {
244            return Err(BridgeLineError::TransportMissing(s.to_string()));
245        };
246        // get bridge address
247        let address = if let Some(address) = tokens.next() {
248            if let Ok(address) = SocketAddr::from_str(address) {
249                address
250            } else {
251                return Err(BridgeLineError::AddressParseFailed(address.to_string()));
252            }
253        } else {
254            return Err(BridgeLineError::AddressMissing(s.to_string()));
255        };
256        // get the bridge fingerprint
257        let fingerprint = if let Some(fingerprint) = tokens.peek() {
258            if Self::is_bridge_fingerprint_pattern(fingerprint) {
259                Some(tokens.next().unwrap().to_string())
260            } else {
261                None
262            }
263        } else {
264            None
265        };
266
267        // get the bridge options
268        static BRIDGE_OPTION_PATTERN: OnceLock<Regex> = OnceLock::new();
269        let bridge_option_pattern = BRIDGE_OPTION_PATTERN
270            .get_or_init(|| Regex::new(r"(?m)^(?<key>[^=]+)=(?<value>.*)$").unwrap());
271
272        let mut keyvalues: Vec<(String, String)> = Default::default();
273        for keyvalue in tokens {
274            if let Some(caps) = bridge_option_pattern.captures(keyvalue) {
275                let key = caps
276                    .name("key")
277                    .expect("missing key group")
278                    .as_str()
279                    .to_string();
280                let value = caps
281                    .name("value")
282                    .expect("missing value group")
283                    .as_str()
284                    .to_string();
285                keyvalues.push((key, value));
286            } else {
287                return Err(BridgeLineError::KeyValueInvalid(keyvalue.to_string()));
288            }
289        }
290
291        BridgeLine::new(transport.to_string(), address, fingerprint, keyvalues)
292    }
293}