Pier Angelo Vendrame pushed to branch tor-browser-153.4.0esr-16.0-1 at The Tor Project / Applications / Tor Browser Commits: e8c774f3 by Elena at 2026-09-28T19:10:00+02:00 fixup! TB 44806: Implement the tor integration in Rust. TB 44930: Implement the commands on the Rust control port Implemented bridge line parsing. - - - - - 1b15f99e by Elena at 2026-09-28T19:10:00+02:00 fixup! TB 44806: Implement the tor integration in Rust. TB 44930: Implement the commands on the Rust control port Implemented the parsers to get PTs. - - - - - eb5427bb by Elena at 2026-09-28T19:10:01+02:00 fixup! TB 44806: Implement the tor integration in Rust. TB 44930: Implement the commands on the Rust control port Implemented the GETCONF commands. - - - - - 343a6dfa by Elena at 2026-09-28T19:10:02+02:00 fixup! TB 44806: Implement the tor integration in Rust. TB 44930: Implement the commands on the Rust control port Implemented the SETCONF. - - - - - 12 changed files: - Cargo.lock - toolkit/components/tor-integration/tor_provider/Cargo.toml - + toolkit/components/tor-integration/tor_provider/src/ctor/controller/commands/get_conf.rs - toolkit/components/tor-integration/tor_provider/src/ctor/controller/commands/mod.rs - + toolkit/components/tor-integration/tor_provider/src/ctor/controller/commands/set_conf.rs - toolkit/components/tor-integration/tor_provider/src/ctor/controller/controller.rs - toolkit/components/tor-integration/tor_provider/src/ctor/controller/error.rs - toolkit/components/tor-integration/tor_provider/src/ctor/controller/mod.rs - + toolkit/components/tor-integration/tor_provider/src/ctor/controller/parsers/bridge_line.rs - + toolkit/components/tor-integration/tor_provider/src/ctor/controller/parsers/client_transport_plugin.rs - toolkit/components/tor-integration/tor_provider/src/ctor/controller/parsers/mod.rs - + toolkit/components/tor-integration/tor_provider/src/ctor/controller/types.rs Changes: ===================================== Cargo.lock ===================================== @@ -8173,8 +8173,12 @@ version = "0.1.0" dependencies = [ "bytes", "hex", + "itoa", + "lazy_static", "log", "memchr", + "regex", + "serde", "thiserror 2.0.12", ] ===================================== toolkit/components/tor-integration/tor_provider/Cargo.toml ===================================== @@ -7,6 +7,10 @@ edition = "2021" [dependencies] bytes = "1.4.0" hex = "0.4.3" +itoa = "1.0.15" +lazy_static = "1" log = "0.4" memchr = "2.7.4" +regex = "1" +serde = { version = "1.0", features = ["derive"] } thiserror = "2" ===================================== toolkit/components/tor-integration/tor_provider/src/ctor/controller/commands/get_conf.rs ===================================== @@ -0,0 +1,264 @@ +// Licensed under the Apache License, Version 2.0, +// <http://apache.org/licenses/LICENSE-2.0> or the MIT license +// <http://opensource.org/licenses/MIT>, at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use std::iter::once; + +use super::Command; +use crate::ctor::{ + controller::{parsers::*, types::*, ControllerError}, + reply_parser::{DetailReplyLine, Reply}, +}; + +pub fn bridges() -> Result<Command<Vec<Bridge>>, ControllerError> { + get_conf("Bridge", |line| { + parse_bridge_line(line).map_err(|e| ControllerError::MalformedReply(e.to_string())) + }) +} + +pub fn client_transport_plugins() -> Result<Command<Vec<PluggableTransport>>, ControllerError> { + get_conf("ClientTransportPlugin", parse_client_transport_plugin) +} + +fn get_conf<F, T>(key: &'static str, converter: F) -> Result<Command<Vec<T>>, ControllerError> +where + F: Fn(&[u8]) -> Result<T, ControllerError> + 'static, +{ + Ok(Command { + command: format!("GETCONF {}\r\n", key), + handler: Box::new(move |reply| parse_get_conf(key.as_bytes(), &converter, reply)), + }) +} + +/// Parse and convert the output of the GETCONF command for a single keyword. +/// +/// We currently query and parse only one key at the time, but this parser +/// tolerates keywords we did not ask (it silently ignores them). +fn parse_get_conf<F, T>(key: &[u8], converter: &F, reply: Reply) -> Result<Vec<T>, ControllerError> +where + F: Fn(&[u8]) -> Result<T, ControllerError> + 'static, +{ + if let Some(e) = ControllerError::from_reply(&reply) { + return Err(e); + } + + let check_code = |code| { + if code != 250 { + Err(ControllerError::MalformedReply(format!( + "Expected the 250 status code, got '{}'", + code + ))) + } else { + Ok(()) + } + }; + + check_code(reply.end_line().code)?; + + // Special case: "default value semantically different from an empty string" + // is just "250 KEYWORD CRLF" without '=' (see the specs). + if reply.details().is_empty() && reply.end_line().line.eq_ignore_ascii_case(key) { + return Ok(Vec::new()); + } + + // Key followed by '='. + let prefix_len = key.len() + 1; + + let details = reply.details().iter().map(|l| match l { + DetailReplyLine::MidReplyLine { code, line } => { + check_code(*code)?; + Ok(line.as_ref()) + } + DetailReplyLine::DataReplyLine { .. } => Err(ControllerError::MalformedReply( + String::from("GETCONF does not allow data lines"), + )), + }); + + let all_lines = details.chain(once(Ok(reply.end_line().line.as_ref()))); + + all_lines + .filter_map(|line| match line { + Ok(l) => { + if l.len() >= prefix_len + && l[0..key.len()].eq_ignore_ascii_case(key) + && l[key.len()] == b'=' + { + Some(converter(&l[prefix_len..])) + } else { + None + } + } + Err(e) => Some(Err(e)), + }) + .collect() +} + +#[cfg(test)] +mod tests { + use std::assert_matches; + + use super::*; + use crate::ctor::reply_parser::make_reply; + + fn converter(v: &[u8]) -> Result<String, ControllerError> { + Ok(String::from_utf8_lossy(v).into_owned()) + } + + #[test] + fn simple() { + assert_eq!( + parse_get_conf(b"test", &converter, make_reply(b"250 test=value\r\n")).unwrap(), + vec!["value"] + ); + } + + #[test] + fn case_insensitivity() { + assert_eq!( + parse_get_conf(b"test", &converter, make_reply(b"250 TEST=value\r\n")).unwrap(), + vec!["value"] + ); + assert_eq!( + parse_get_conf(b"Test", &converter, make_reply(b"250 TEST=value\r\n")).unwrap(), + vec!["value"] + ); + assert_eq!( + parse_get_conf(b"TeSt", &converter, make_reply(b"250 tESt=value\r\n")).unwrap(), + vec!["value"] + ); + } + + #[test] + fn array() { + assert_eq!( + parse_get_conf( + b"test", + &converter, + make_reply(b"250-test=value 1\r\n250 test=value 2\r\n") + ) + .unwrap(), + vec!["value 1", "value 2"] + ); + + assert_eq!( + parse_get_conf( + b"Test", + &converter, + make_reply(b"250-test=VALUE 1\r\n250 TEST=value 2\r\n") + ) + .unwrap(), + vec!["VALUE 1", "value 2"] + ); + } + + #[test] + fn empty() { + assert!( + parse_get_conf(b"test", &converter, make_reply(b"250 test\r\n")) + .unwrap() + .is_empty(), + ); + assert!( + parse_get_conf(b"Test", &converter, make_reply(b"250 TEST\r\n")) + .unwrap() + .is_empty(), + ); + } + + #[test] + fn other_keys() { + assert_eq!( + parse_get_conf( + b"test", + &converter, + make_reply(b"250-test 1=a\r\n250-test=b\r\n250-test=c\r\n250 test 2=d\r\n") + ) + .unwrap(), + vec!["b", "c"] + ); + + assert!(parse_get_conf( + b"C", + &converter, + make_reply(b"250-A=Apple\r\n250 B=Banana\r\n"), + ) + .unwrap() + .is_empty(),); + } + + #[test] + fn tor_error() { + assert_eq!( + parse_get_conf( + b"test", + &converter, + make_reply(b"552 Unrecognized configuration key \"test\"\r\n") + ) + .unwrap_err(), + ControllerError::TorError { + code: 552, + message: String::from("Unrecognized configuration key \"test\"") + } + ); + } + + #[test] + fn no_250() { + assert_matches!( + parse_get_conf( + b"test", + &converter, + make_reply(b"251 test=value\r\n") + ) + .unwrap_err(), + ControllerError::MalformedReply(_) + ); + assert_matches!( + parse_get_conf( + b"test", + &converter, + make_reply(b"251-test1=value\r\n250 test2=value2\r\n") + ) + .unwrap_err(), + ControllerError::MalformedReply(_) + ); + assert_matches!( + parse_get_conf( + b"test", + &converter, + make_reply(b"250-test1=value\r\n251 test1=value2\r\n") + ) + .unwrap_err(), + ControllerError::MalformedReply(_) + ); + } + + #[test] + fn no_multiline_getconf() { + assert_matches!( + parse_get_conf( + b"test", + &converter, + make_reply(b"250+test=\r\nsome value\r\n.\r\n250 OK\r\n") + ) + .unwrap_err(), + ControllerError::MalformedReply(_) + ); + } + + #[test] + fn converter_error() { + assert_eq!( + parse_get_conf::<_, String>( + b"test", + &(|_| Err(ControllerError::WrongFormat(String::from( + "Something is fishy" + )))), + make_reply(b"250 test=value\r\n") + ) + .unwrap_err(), + ControllerError::WrongFormat(String::from("Something is fishy")) + ); + } +} ===================================== toolkit/components/tor-integration/tor_provider/src/ctor/controller/commands/mod.rs ===================================== @@ -5,8 +5,10 @@ mod authenticate; mod command; +pub mod get_conf; mod reset_conf; mod save_conf; +mod set_conf; mod set_events; mod signal; mod take_ownership; @@ -15,6 +17,7 @@ pub use authenticate::authenticate; pub use command::Command; pub use reset_conf::reset_owning_controller_process; pub use save_conf::save_conf; +pub use set_conf::set_conf; pub use set_events::set_events; pub use signal::signal_newnym; pub use take_ownership::take_ownership; ===================================== toolkit/components/tor-integration/tor_provider/src/ctor/controller/commands/set_conf.rs ===================================== @@ -0,0 +1,243 @@ +// Licensed under the Apache License, Version 2.0, +// <http://apache.org/licenses/LICENSE-2.0> or the MIT license +// <http://opensource.org/licenses/MIT>, at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use lazy_static::lazy_static; +use regex::Regex; + +use super::Command; +use crate::ctor::controller::{parsers::*, ConfValue, ControllerError}; + +lazy_static! { + // The SETCONF command's documentation lists `keyword` for keys, and + // keyword is defined as `1*ALPHA`, so it should not include numbers. + // However, as a matter of fact, several settings (including the ones we + // use) have numbers, therefore we also accept them. + static ref KEY_REGEX: Regex = + Regex::new("^[a-zA-Z0-9_]+$").expect("The regex is hardcoded, it should be good."); +} + +pub fn set_conf(values: &[(&str, ConfValue)]) -> Result<Command<u16>, ControllerError> { + Ok(Command { + command: make_setconf_command(values)?, + handler: Box::new(parse_ack), + }) +} + +fn make_setconf_command(values: &[(&str, ConfValue)]) -> Result<String, ControllerError> { + let mut res = String::from("SETCONF"); + res.reserve(256); + for (key, value) in values.iter() { + if !KEY_REGEX.is_match(key) { + return Err(ControllerError::InvalidArgument(format!( + "invalid key: '{}'", + key + ))); + } + match value { + ConfValue::Bool(v) => push_kv(key, if *v { "1" } else { "0" }, false, &mut res), + ConfValue::Int(i) => { + push_k(key, &mut res); + res.push('='); + let mut buf = itoa::Buffer::new(); + res.push_str(buf.format(*i)); + } + ConfValue::String(s) => push_kv(key, s, true, &mut res), + ConfValue::Array(a) => { + if a.is_empty() { + push_k(key, &mut res); + } else { + for v in a.iter() { + push_kv(key, v, true, &mut res); + } + } + } + ConfValue::Null => push_k(key, &mut res), + } + } + res.push_str("\r\n"); + Ok(res) +} + +fn push_k(key: &str, dest: &mut String) { + dest.push(' '); + dest.push_str(key); +} + +fn push_kv(key: &str, val: &str, escape: bool, dest: &mut String) { + push_k(key, dest); + dest.push('='); + if escape { + tor_escape_into(val, dest); + } else { + dest.push_str(val); + } +} + +#[cfg(test)] +mod tests { + use std::assert_matches; + + use super::*; + + #[test] + fn test_bool() { + let vals = &[ + ("test", ConfValue::Bool(true)), + ("AA", ConfValue::Bool(false)), + ]; + assert_eq!( + make_setconf_command(vals).unwrap(), + "SETCONF test=1 AA=0\r\n" + ); + } + + #[test] + fn test_int() { + let vals = &[ + ("aaa", ConfValue::Int(42)), + ("bBbB", ConfValue::Int(0)), + ("cc", ConfValue::Int(-3)), + ]; + assert_eq!( + make_setconf_command(vals).unwrap(), + "SETCONF aaa=42 bBbB=0 cc=-3\r\n" + ); + } + + #[test] + fn test_string() { + let vals = &[("Test123", ConfValue::String("a b\\c"))]; + assert_eq!( + make_setconf_command(vals).unwrap(), + "SETCONF Test123=\"a b\\\\c\"\r\n" + ); + } + + #[test] + fn test_array() { + let vals = &[ + ("empty", ConfValue::Array(vec![])), + ("list", ConfValue::Array(vec!["one", "two two"])), + ]; + assert_eq!( + make_setconf_command(vals).unwrap(), + "SETCONF empty list=\"one\" list=\"two two\"\r\n" + ); + } + + #[test] + fn test_null() { + let vals = &[("test", ConfValue::Null)]; + assert_eq!(make_setconf_command(vals).unwrap(), "SETCONF test\r\n"); + } + + #[test] + fn mixed_values() { + let vals = &[ + ("b", ConfValue::Bool(false)), + ("a", ConfValue::Int(-7)), + ("c", ConfValue::String("x y")), + ("d", ConfValue::Null), + ]; + assert_eq!( + make_setconf_command(vals).unwrap(), + "SETCONF b=0 a=-7 c=\"x y\" d\r\n" + ); + } + + #[test] + fn repeated_key() { + let vals = &[ + ("bridge", ConfValue::String("obfs4")), + ("bridge", ConfValue::String("snowflake")), + ("bridge", ConfValue::String("meek")), + ]; + assert_eq!( + make_setconf_command(vals).unwrap(), + "SETCONF bridge=\"obfs4\" bridge=\"snowflake\" bridge=\"meek\"\r\n" + ); + } + + #[test] + fn realistic_configs() { + // No bridges + assert_eq!( + make_setconf_command(&[ + ("UseBridges", ConfValue::Bool(false)), + ("Bridge", ConfValue::Null), + ]) + .unwrap(), + "SETCONF UseBridges=0 Bridge\r\n" + ); + + // One bridge + assert_eq!( + make_setconf_command(&[ + ("UseBridges", ConfValue::Bool(true)), + ("Bridge", ConfValue::Array(vec!["1.2.3.4:443"])) + ]) + .unwrap(), + "SETCONF UseBridges=1 Bridge=\"1.2.3.4:443\"\r\n" + ); + + // Some bridges + assert_eq!( + make_setconf_command(&[ + ("UseBridges", ConfValue::Bool(true)), + ( + "Bridge", + ConfValue::Array(vec![ + "1.2.3.4:443", + "obfs4 5.6.7.8:9999 0123456789012345678901234567890123456789 iat-mode=0", + ]), + ), + ]) + .unwrap(), + concat!( + "SETCONF ", + "UseBridges=1 ", + "Bridge=\"1.2.3.4:443\" ", + "Bridge=\"obfs4 5.6.7.8:9999 0123456789012345678901234567890123456789 iat-mode=0\"", + "\r\n" + ) + ); + + // No firewall + assert_eq!( + make_setconf_command(&[("ReachableAddresses", ConfValue::Null)]).unwrap(), + "SETCONF ReachableAddresses\r\n" + ); + + // Firewall that only allows 80 and 443 + assert_eq!( + make_setconf_command(&[("ReachableAddresses", ConfValue::String("*:80,*:443"))]) + .unwrap(), + "SETCONF ReachableAddresses=\"*:80,*:443\"\r\n" + ); + + // No proxies + assert_eq!( + make_setconf_command(&[ + ("Socks4Proxy", ConfValue::Null), + ("Socks5Proxy", ConfValue::Null), + ("Socks5ProxyUsername", ConfValue::Null), + ("Socks5ProxyPassword", ConfValue::Null), + ("HTTPSProxy", ConfValue::Null), + ("HTTPSProxyAuthenticator", ConfValue::Null), + ]) + .unwrap(), + "SETCONF Socks4Proxy Socks5Proxy Socks5ProxyUsername Socks5ProxyPassword HTTPSProxy HTTPSProxyAuthenticator\r\n" + ); + } + + #[test] + fn invalid_key() { + let val = &[("invalid\r\nkey", ConfValue::Null)]; + assert_matches!( + make_setconf_command(val).unwrap_err(), + ControllerError::InvalidArgument(_) + ); + } +} ===================================== toolkit/components/tor-integration/tor_provider/src/ctor/controller/controller.rs ===================================== @@ -8,6 +8,7 @@ use bytes::Bytes; use super::{ commands::{self, Command}, error::ControllerError, + types::*, }; use crate::ctor::{ control_port::{ControlPortInterface, ControlSocketError}, @@ -102,16 +103,35 @@ impl<CP: ControlPortInterface> TorController<CP> { // Connection management + pub fn set_conf( + &self, + values: &[(&str, ConfValue)], + handler: Box<dyn FnOnce(Result<u16, ControllerError>)>, + ) { + self.send_command(commands::set_conf(values), handler); + } + pub fn save_conf(&self, handler: Box<dyn FnOnce(Result<u16, ControllerError>)>) { self.send_command(commands::save_conf(), handler); } + pub fn get_bridges(&self, handler: Box<dyn FnOnce(Result<Vec<Bridge>, ControllerError>)>) { + self.send_command(commands::get_conf::bridges(), handler); + } + // Circuit display // Onion authentication // Miscellaneous + pub fn get_pluggable_transports( + &self, + handler: Box<dyn FnOnce(Result<Vec<PluggableTransport>, ControllerError>)>, + ) { + self.send_command(commands::get_conf::client_transport_plugins(), handler); + } + pub fn signal_newnym(&self, handler: Box<dyn FnOnce(Result<u16, ControllerError>)>) { self.send_command(commands::signal_newnym(), handler); } ===================================== toolkit/components/tor-integration/tor_provider/src/ctor/controller/error.rs ===================================== @@ -32,6 +32,8 @@ pub enum ControllerError { KeyNotFound(String), #[error("malformed reply: {0}")] MalformedReply(String), + #[error("the argument passed to the command was not valid: {0}")] + InvalidArgument(String), } impl ControllerError { ===================================== toolkit/components/tor-integration/tor_provider/src/ctor/controller/mod.rs ===================================== @@ -7,6 +7,8 @@ mod commands; mod controller; mod error; mod parsers; +mod types; pub use controller::*; pub use error::*; +pub use types::*; ===================================== toolkit/components/tor-integration/tor_provider/src/ctor/controller/parsers/bridge_line.rs ===================================== @@ -0,0 +1,358 @@ +// Licensed under the Apache License, Version 2.0, +// <http://apache.org/licenses/LICENSE-2.0> or the MIT license +// <http://opensource.org/licenses/MIT>, at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use hex; +use lazy_static::lazy_static; +use regex::bytes::Regex; +use std::net::SocketAddr; +use thiserror::Error; + +use crate::ctor::controller::types::Bridge; + +lazy_static! { + // In C-tor's parse_bridge_line (src/app/config/config.c), the transport is + // parsed as a potential C identifier (starts with letter/underscores and + // can contain any additional number of letters, numbers and underscores, + // as defined in src/lib/string/util_string.c). + // + // The logic to distinguish between the fingerprint and args is the + // following one (from C-tor's source code): + // - if the bridge does not use a transport, it cannot have arguments. + // If after the address there is anything, it must be the fingerprint; + // - otherwise, if there is a = sign, there is not a fingerprint. + // + // We do not use arguments in any way, so we do not enforce their format. + // However, if we see a valid hex sequence, we still need to validate its + // length, to make sure it actually is a fingerprint. + // + // We are not strict on spaces in case this is used with data provided + // directly by users. + static ref BRIDGE_REGEX: Regex = Regex::new(r"^ *(?:(?<transport>[A-Za-z_][A-Za-z0-9_]*) +)?(?<addr>[0-9a-fA-F\.\[\]\:]+:\d{1,5})(?: +(?<fingerprint>[0-9a-fA-F]+))?(?: +(?<args>.+?))? *$") + .expect("The regex is hardcoded, it should be good."); +} + +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum BridgeParseError { + #[error("the bridge line has a wrong format and could not be parsed")] + InvalidFormat, + #[error("invalid address")] + InvalidAddress, + #[error("port 0 is not valid for bridges")] + InvalidPort, + #[error("invalid length of the bridge fingerprint")] + InvalidFingerprint, + #[error("arguments can be supplied only when a transport is in use")] + ArgsWithoutTransport, +} + +pub fn parse_bridge_line(value: &[u8]) -> Result<Bridge, BridgeParseError> { + let caps = BRIDGE_REGEX + .captures(value) + .ok_or(BridgeParseError::InvalidFormat)?; + + let transport = caps + .name("transport") + .map(|m| String::from_utf8_lossy(m.as_bytes()).into_owned()); + + let address: SocketAddr = caps + .name("addr") + .and_then(|a| str::from_utf8(a.as_bytes()).ok()) + // This cannot really happen since the regex matched... + .ok_or(BridgeParseError::InvalidFormat)? + .parse() + // Rust validation is very generic, so it is not even worth to + // include in our error. + .map_err(|_| BridgeParseError::InvalidAddress)?; + if address.port() == 0 { + return Err(BridgeParseError::InvalidPort); + } + + let fingerprint = caps + .name("fingerprint") + .and_then(|m| { + let mut fp = [0u8; 20]; + // We match only hex characters in the regex, so this should error + // only if the size is wrong. + match hex::decode_to_slice(m.as_bytes(), &mut fp[..]) { + Ok(_) => Some(Ok(fp)), + Err(_) => Some(Err(BridgeParseError::InvalidFingerprint)), + } + }) + .transpose()?; + + let args = caps.name("args"); + if transport.is_none() && args.is_some() { + // src/app/config/config.c, parse_bridge_line: "If transports are + // disabled, next field must be a fingerprint.". + return Err(BridgeParseError::ArgsWithoutTransport); + } + + Ok(Bridge { + transport, + address, + fingerprint, + args: args.map(|m| m.as_bytes().to_vec()), + }) +} + +#[cfg(test)] +mod tests { + use std::{ + assert_eq, + net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}, + }; + + use super::*; + + #[test] + fn vanilla() { + { + let bridge = parse_bridge_line(b"192.168.1.4:443").unwrap(); + assert_eq!(bridge.transport, None); + assert_eq!( + bridge.address, + SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 4), 443).into() + ); + assert_eq!(bridge.fingerprint, None); + assert_eq!(bridge.args, None); + } + { + let bridge = parse_bridge_line(b"[dead:BEEF::1234]:443").unwrap(); + assert_eq!(bridge.transport, None); + assert_eq!( + bridge.address, + SocketAddrV6::new( + Ipv6Addr::new(0xdead, 0xbeef, 0, 0, 0, 0, 0, 0x1234), + 443, + 0, + 0 + ) + .into() + ); + assert_eq!(bridge.fingerprint, None); + assert_eq!(bridge.args, None); + } + } + + #[test] + fn transport() { + { + let bridge = parse_bridge_line(b"test 1.2.3.4:567").unwrap(); + assert_eq!(bridge.transport, Some(String::from("test"))); + assert_eq!( + bridge.address, + SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 567).into() + ); + assert_eq!(bridge.fingerprint, None); + assert_eq!(bridge.args, None); + } + { + let bridge = parse_bridge_line(b"test [cafe:CAFE::BaBe]:567").unwrap(); + assert_eq!(bridge.transport, Some(String::from("test"))); + assert_eq!( + bridge.address, + SocketAddrV6::new( + Ipv6Addr::new(0xcafe, 0xcafe, 0, 0, 0, 0, 0, 0xbabe), + 567, + 0, + 0 + ) + .into() + ); + assert_eq!(bridge.fingerprint, None); + assert_eq!(bridge.args, None); + } + } + + #[test] + fn transport_fingerprint() { + { + let bridge = parse_bridge_line( + b"obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D", + ) + .unwrap(); + assert_eq!(bridge.transport, Some(String::from("obfs4"))); + assert_eq!( + bridge.address, + SocketAddrV4::new(Ipv4Addr::new(37, 218, 245, 14), 38224).into() + ); + assert_eq!( + bridge.fingerprint.as_ref().map(|fp| &fp[..]), + Some( + hex::decode("D9A82D2F9C2F65A18407B1D2B764F130847F8B5D") + .unwrap() + .as_slice() + ) + ); + assert_eq!(bridge.args, None); + } + { + let bridge = parse_bridge_line( + b"obfs4 [1234:56:789::abcd]:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D", + ) + .unwrap(); + assert_eq!(bridge.transport, Some(String::from("obfs4"))); + assert_eq!( + bridge.address, + SocketAddrV6::new( + Ipv6Addr::new(0x1234, 0x56, 0x789, 0, 0, 0, 0, 0xabcd), + 38224, + 0, + 0 + ) + .into() + ); + assert_eq!( + bridge.fingerprint.as_ref().map(|fp| &fp[..]), + Some( + hex::decode("D9A82D2F9C2F65A18407B1D2B764F130847F8B5D") + .unwrap() + .as_slice() + ) + ); + assert_eq!(bridge.args, None); + } + } + + #[test] + fn all() { + { + let bridge = parse_bridge_line( + b"obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D iat-mode=0", + ) + .unwrap(); + assert_eq!(bridge.transport, Some(String::from("obfs4"))); + assert_eq!( + bridge.address, + SocketAddrV4::new(Ipv4Addr::new(37, 218, 245, 14), 38224).into() + ); + assert_eq!( + bridge.fingerprint.as_ref().map(|fp| &fp[..]), + Some( + hex::decode("D9A82D2F9C2F65A18407B1D2B764F130847F8B5D") + .unwrap() + .as_slice() + ) + ); + assert_eq!(&bridge.args.unwrap(), b"iat-mode=0"); + } + { + let bridge = parse_bridge_line( + b"obfs4 [1234:56:789:0a:00b:000c:d:eeee]:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D iat-mode=0", + ) + .unwrap(); + assert_eq!(bridge.transport, Some(String::from("obfs4"))); + assert_eq!( + bridge.address, + SocketAddrV6::new( + Ipv6Addr::new(0x1234, 0x56, 0x789, 0xa, 0xb, 0xc, 0xd, 0xeeee), + 38224, + 0, + 0 + ) + .into() + ); + assert_eq!( + bridge.fingerprint.as_ref().map(|fp| &fp[..]), + Some( + hex::decode("D9A82D2F9C2F65A18407B1D2B764F130847F8B5D") + .unwrap() + .as_slice() + ) + ); + assert_eq!(&bridge.args.unwrap(), b"iat-mode=0"); + } + } + + #[test] + fn no_fingerprint() { + let bridge = parse_bridge_line(b"test 1.2.3.4:567 iat-mode=0").unwrap(); + assert_eq!(bridge.transport.unwrap(), "test"); + assert_eq!( + bridge.address, + SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 567).into() + ); + assert_eq!(bridge.fingerprint, None); + assert_eq!(&bridge.args.unwrap(), b"iat-mode=0"); + } + + #[test] + fn multiple_args_and_spaces() { + let bridge = parse_bridge_line( + b" obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D iat-mode=0 other-arg spaces-at-the-end ", + ) + .unwrap(); + assert_eq!( + &bridge.args.unwrap(), + b"iat-mode=0 other-arg spaces-at-the-end" + ); + } + + #[test] + fn failures() { + // Only IP address without a port. Does not even match the regex. + assert_eq!( + parse_bridge_line(b"192.168.1.4").unwrap_err(), + BridgeParseError::InvalidFormat, + ); + // Invalid port (and again, does not match the regex). + assert_eq!( + parse_bridge_line(b"192.168.1.4:100000").unwrap_err(), + BridgeParseError::InvalidFormat, + ); + // No address + assert_eq!( + parse_bridge_line(b"obfs4 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D").unwrap_err(), + BridgeParseError::InvalidFormat, + ); + // No address and bad fingerprint (bad no address prevails). + assert_eq!( + parse_bridge_line(b"obfs4 aaa1234").unwrap_err(), + BridgeParseError::InvalidFormat, + ); + + assert_eq!( + parse_bridge_line(b"192.168.1.375:1234").unwrap_err(), + BridgeParseError::InvalidAddress, + ); + assert_eq!( + parse_bridge_line(b"192.168.1.4:0").unwrap_err(), + BridgeParseError::InvalidPort, + ); + assert_eq!( + parse_bridge_line(b"192.168.1.4:99999").unwrap_err(), + BridgeParseError::InvalidAddress, + ); + assert_eq!( + parse_bridge_line(b"192.168.1.4:65536").unwrap_err(), + BridgeParseError::InvalidAddress, + ); + + assert_eq!( + parse_bridge_line(b"1.2.3.4:443 aaaaaaaaa").unwrap_err(), + BridgeParseError::InvalidFingerprint + ); + assert_eq!( + parse_bridge_line( + b"1.2.3.4:443 0123456789012345678901234567890123456789aaaaa" + ) + .unwrap_err(), + BridgeParseError::InvalidFingerprint + ); + + assert_eq!( + parse_bridge_line( + b"1.2.3.4:443 0123456789012345678901234567890123456789 key=value" + ).unwrap_err(), + BridgeParseError::ArgsWithoutTransport, + ); + assert_eq!( + parse_bridge_line( + b"1.2.3.4:443 key=value" + ).unwrap_err(), + BridgeParseError::ArgsWithoutTransport, + ); + } +} ===================================== toolkit/components/tor-integration/tor_provider/src/ctor/controller/parsers/client_transport_plugin.rs ===================================== @@ -0,0 +1,299 @@ +// Licensed under the Apache License, Version 2.0, +// <http://apache.org/licenses/LICENSE-2.0> or the MIT license +// <http://opensource.org/licenses/MIT>, at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use lazy_static::lazy_static; +use regex::bytes::{Captures, Regex}; + +use super::tor_unescape; +use crate::ctor::controller::{ClientTransportPlugin, ControllerError, PluggableTransport}; + +lazy_static! { + // man 1 tor: ClientTransportPlugin transport socks4|socks5 IP:PORT + static ref SOCKS_REGEX: Regex = + Regex::new(r"^(?<transport>[A-Za-z_][A-Za-z0-9_,]*) (?<protocol>socks[45]) (?<address>(?:[\d\.]{7,15}|\[[\da-fA-F:]+\]):\d{1,5})$") + .expect("The regex is hardcoded, it should be good."); + // man 1 tor: transport exec path-to-binary [options] + static ref EXEC_REGEX: Regex = Regex::new(r#"^(?<transport>[A-Za-z_][A-Za-z0-9_,]*) exec (?<path>"(?:[^"\\]|\\.)*"|[^ ]+)(?: (?<options>.*?) *)?$"#) + .expect("The regex is hardcoded, it should be good."); +} + +pub fn parse_client_transport_plugin(line: &[u8]) -> Result<PluggableTransport, ControllerError> { + if let Some(socks_line) = SOCKS_REGEX.captures(line) { + parse_socks(&socks_line) + } else if let Some(exec_line) = EXEC_REGEX.captures(line) { + parse_exec(&exec_line) + } else { + Err(ControllerError::WrongFormat( + String::from_utf8_lossy(line).into_owned(), + )) + } +} + +fn get_transports(c: &Captures) -> Result<Vec<String>, ControllerError> { + let transports = c + .name("transport") + .ok_or(ControllerError::KeyNotFound(String::from("transport")))? + .as_bytes() + .split(|b| *b == b',') + .map(|t| String::from_utf8_lossy(t).into_owned()) + .collect::<Vec<_>>(); + if transports.is_empty() || transports.iter().any(|t| t.is_empty()) { + return Err(ControllerError::MalformedReply(String::from( + "empty transports", + ))); + } + Ok(transports) +} + +fn parse_socks(socks_line: &Captures) -> Result<PluggableTransport, ControllerError> { + let address = str::from_utf8( + socks_line + .name("address") + .ok_or(ControllerError::KeyNotFound(String::from("address")))? + .as_bytes(), + ) + .expect("The regex matches only ASCII characters (hence valid UTF-8)") + .parse() + .map_err(|e| ControllerError::MalformedReply(format!("{}", e)))?; + let plugin = match socks_line + .name("protocol") + .ok_or(ControllerError::KeyNotFound(String::from("protocol")))? + .as_bytes() + { + b"socks4" => ClientTransportPlugin::Socks4(address), + b"socks5" => ClientTransportPlugin::Socks5(address), + _ => unreachable!("We validated this with the regex"), + }; + Ok(PluggableTransport { + transports: get_transports(&socks_line)?, + plugin, + }) +} + +fn parse_exec(exec_line: &Captures) -> Result<PluggableTransport, ControllerError> { + let options = exec_line + .name("options") + .map(|o| o.as_bytes()) + .and_then(|s| if s.is_empty() { None } else { Some(s.into()) }); + let path = tor_unescape( + exec_line + .name("path") + .ok_or(ControllerError::KeyNotFound(String::from("path")))? + .as_bytes(), + ) + .map_err(|e| ControllerError::MalformedReply(e.to_string()))? + .into_owned(); + if path.is_empty() { + return Err(ControllerError::MalformedReply(String::from("empty path"))); + } + Ok(PluggableTransport { + transports: get_transports(&exec_line)?, + plugin: ClientTransportPlugin::Executable { path, options }, + }) +} + +#[cfg(test)] +mod tests { + use std::{ + assert_matches, + net::{Ipv4Addr, SocketAddrV4}, + }; + + use super::*; + + #[test] + fn socks() { + { + let pt = + parse_client_transport_plugin(b"mytransport,mypt2 socks4 127.0.0.1:9052").unwrap(); + assert_eq!(pt.transports, vec!["mytransport", "mypt2"]); + assert_eq!( + pt.plugin, + ClientTransportPlugin::Socks4( + SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 9052).into() + ) + ); + } + { + let pt = parse_client_transport_plugin(b"pt3,pt_4 socks5 10.0.1.2:9052").unwrap(); + assert_eq!(pt.transports, vec!["pt3", "pt_4"]); + assert_eq!( + pt.plugin, + ClientTransportPlugin::Socks5( + SocketAddrV4::new(Ipv4Addr::new(10, 0, 1, 2), 9052).into() + ) + ); + } + { + let pt = parse_client_transport_plugin( + b"mytransport,mypt2 socks4 [38e5:09fc:2080:673d:9ab8:4545:08db:36c2]:1234", + ) + .unwrap(); + assert_eq!(pt.transports, vec!["mytransport", "mypt2"]); + assert_eq!( + pt.plugin, + ClientTransportPlugin::Socks4( + "[38e5:09fc:2080:673d:9ab8:4545:08db:36c2]:1234" + .parse() + .unwrap() + ) + ); + } + { + let pt = parse_client_transport_plugin( + b"s5pt socks5 [dad7:ddf4:7fc4:ef92:4bb2:ceb9:05b9:9161]:5555", + ) + .unwrap(); + assert_eq!(pt.transports, vec!["s5pt"]); + assert_eq!( + pt.plugin, + ClientTransportPlugin::Socks5( + "[dad7:ddf4:7fc4:ef92:4bb2:ceb9:05b9:9161]:5555" + .parse() + .unwrap() + ) + ); + } + } + + #[test] + fn exec_multiple_transports() { + let pt = parse_client_transport_plugin(b"obfs4,meek exec lyrebird").unwrap(); + assert_eq!( + pt.transports, + vec![String::from("obfs4"), String::from("meek")] + ); + assert_eq!( + pt.plugin, + ClientTransportPlugin::Executable { + path: Vec::from(b"lyrebird"), + options: None, + } + ); + } + + #[test] + fn exec_with_spaces() { + let pt = + parse_client_transport_plugin(b"snowflake exec \"pluggable transports/snowflake\"") + .unwrap(); + assert_eq!(pt.transports, vec![String::from("snowflake")]); + assert_eq!( + pt.plugin, + ClientTransportPlugin::Executable { + path: Vec::from("pluggable transports/snowflake"), + options: None, + } + ) + } + + #[test] + fn exec_windows_path() { + let pt = parse_client_transport_plugin( + b"snowflake exec \"C:\\\\Program Files\\\\Tor Project \\\\snowflake.exe\"", + ) + .unwrap(); + assert_eq!(pt.transports, vec![String::from("snowflake")]); + assert_eq!( + pt.plugin, + ClientTransportPlugin::Executable { + path: Vec::from("C:\\Program Files\\Tor Project \\snowflake.exe"), + options: None, + } + ) + } + + #[test] + fn exec_with_options() { + let pt = parse_client_transport_plugin( + b"conjure,coupdetat exec conjure-client -registerURL https://registration.refraction.network/api ", + ) + .unwrap(); + assert_eq!( + pt.transports, + vec![String::from("conjure"), String::from("coupdetat")] + ); + assert_eq!( + pt.plugin, + ClientTransportPlugin::Executable { + path: Vec::from("conjure-client"), + options: Some(b"-registerURL https://registration.refraction.network/api".into()) + } + ) + } + + #[test] + fn invalid() { + assert_matches!( + parse_client_transport_plugin(b"transport socks2 127.0.0.1:9052").unwrap_err(), + ControllerError::WrongFormat(_) + ); + } + + #[test] + fn socks_errors() { + assert_matches!( + parse_client_transport_plugin(b"transport socks4").unwrap_err(), + ControllerError::WrongFormat(_) + ); + + assert_matches!( + parse_client_transport_plugin(b"transport socks4 123.45.6.7.8:123456").unwrap_err(), + ControllerError::WrongFormat(_) + ); + + assert_matches!( + parse_client_transport_plugin(b", socks4 123.45.6.7.8:1234").unwrap_err(), + ControllerError::WrongFormat(_) + ); + + assert_matches!( + parse_client_transport_plugin(b"transport socks4 1234.5.6.7.8:123").unwrap_err(), + ControllerError::MalformedReply(_) + ); + + assert_matches!( + parse_client_transport_plugin(b"transport socks4 273.12.34.56:789").unwrap_err(), + ControllerError::MalformedReply(_) + ); + + assert_matches!( + parse_client_transport_plugin(b"transport socks4 123.12.34.56:78999").unwrap_err(), + ControllerError::MalformedReply(_) + ); + } + + #[test] + fn exec_errors() { + assert_matches!( + parse_client_transport_plugin(b"transport exec").unwrap_err(), + ControllerError::WrongFormat(_) + ); + + assert_matches!( + parse_client_transport_plugin(b"transport exec \"\"").unwrap_err(), + ControllerError::MalformedReply(_) + ); + + assert_matches!( + parse_client_transport_plugin(b"transport exec \"unterminated").unwrap_err(), + ControllerError::MalformedReply(_) + ); + + assert_matches!( + parse_client_transport_plugin(b"transport exec \"invalid hex \\xAz\"").unwrap_err(), + ControllerError::MalformedReply(_) + ); + + assert_matches!( + parse_client_transport_plugin(b", exec mypt.exe").unwrap_err(), + ControllerError::WrongFormat(_) + ); + assert_matches!( + parse_client_transport_plugin(b"garbage trans,port exec mypt.exe").unwrap_err(), + ControllerError::WrongFormat(_) + ); + } +} ===================================== toolkit/components/tor-integration/tor_provider/src/ctor/controller/parsers/mod.rs ===================================== @@ -4,6 +4,8 @@ // copied, modified, or distributed except according to those terms. mod ack; +mod bridge_line; +mod client_transport_plugin; mod escape; mod unescape; @@ -11,5 +13,7 @@ mod unescape; mod tests; pub use ack::parse_ack; +pub use bridge_line::parse_bridge_line; +pub use client_transport_plugin::parse_client_transport_plugin; pub use escape::tor_escape_into; pub use unescape::tor_unescape; ===================================== toolkit/components/tor-integration/tor_provider/src/ctor/controller/types.rs ===================================== @@ -0,0 +1,40 @@ +// Licensed under the Apache License, Version 2.0, +// <http://apache.org/licenses/LICENSE-2.0> or the MIT license +// <http://opensource.org/licenses/MIT>, at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use serde::Serialize; +use std::net::SocketAddr; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Bridge { + pub transport: Option<String>, + pub address: SocketAddr, + pub fingerprint: Option<[u8; 20]>, + pub args: Option<Vec<u8>>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum ClientTransportPlugin { + Executable { + path: Vec<u8>, + options: Option<Vec<u8>>, + }, + Socks4(SocketAddr), + Socks5(SocketAddr), +} + +#[derive(Debug, Clone)] +pub enum ConfValue<'a> { + Bool(bool), + Int(i32), + String(&'a str), + Array(Vec<&'a str>), + Null, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct PluggableTransport { + pub transports: Vec<String>, + pub plugin: ClientTransportPlugin, +} View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/189176e... -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/189176e... You're receiving this email because of your account on gitlab.torproject.org. Manage all notifications: https://gitlab.torproject.org/-/profile/notifications | Help: https://gitlab.torproject.org/help
participants (1)
-
Pier Angelo Vendrame (@pierov)