From 091586fbc4a581c7532f45572043739ab3b85178 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 15 May 2023 02:35:01 +0000 Subject: [PATCH 001/184] build: moved tor_crypto.rs and tor_controller.rs to own tor-interface crate --- tor-interface/CMakeLists.txt | 7 + tor-interface/Cargo.toml | 20 + tor-interface/src/error.rs | 164 +++ tor-interface/src/lib.rs | 3 + tor-interface/src/tor_controller.rs | 1952 +++++++++++++++++++++++++++ tor-interface/src/tor_crypto.rs | 708 ++++++++++ 6 files changed, 2854 insertions(+) create mode 100644 tor-interface/CMakeLists.txt create mode 100644 tor-interface/Cargo.toml create mode 100644 tor-interface/src/error.rs create mode 100644 tor-interface/src/lib.rs create mode 100644 tor-interface/src/tor_controller.rs create mode 100644 tor-interface/src/tor_crypto.rs diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt new file mode 100644 index 000000000..7b7e8f28f --- /dev/null +++ b/tor-interface/CMakeLists.txt @@ -0,0 +1,7 @@ +add_test(NAME tor_interface_test + COMMAND cargo test ${CARGO_FLAGS} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) +set_tests_properties(tor_interface_test + PROPERTIES ENVIRONMENT "RUSTFLAGS=${RUSTFLAGS};CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR};RUST_BACKTRACE=full" +) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml new file mode 100644 index 000000000..de80494fb --- /dev/null +++ b/tor-interface/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "tor-interface" +authors = ["Richard Pospesel "] +version = "0.0.1" +rust-version = "1.66" +edition = "2021" + +[dependencies] +data-encoding = "2.3.2" +data-encoding-macro = "0.1.12" +rand = "0.8.5" +rand_core = "0.6.3" +regex = "1.5.5" +rust-crypto = "^0.2" +signature = "1.5.0" +socks = "0.3.4" +tor-llcrypto = { version = "0.2.0", features = ["relay"] } +url = "2.2.2" +serial_test = "0.9.0" +which = "4.3.0" diff --git a/tor-interface/src/error.rs b/tor-interface/src/error.rs new file mode 100644 index 000000000..6073bd1e9 --- /dev/null +++ b/tor-interface/src/error.rs @@ -0,0 +1,164 @@ +use std::fmt; + +pub struct Error { + message: String, + file: &'static str, + line: u32, + function: &'static str, +} + +impl Error { + pub fn new(message: String, file: &'static str, line: u32, function: &'static str) -> Self { + Self { + message, + line, + function, + file, + } + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}({}:{}): {}", + self.function, self.file, self.line, self.message + ) + } +} + +pub trait ToError { + fn to_error(self, file: &'static str, line: u32, function: &'static str) -> Error; +} + +impl ToError for T +where + T: std::string::ToString, +{ + fn to_error(self, file: &'static str, line: u32, function: &'static str) -> Error { + Error { + message: self.to_string(), + line, + function, + file, + } + } +} + +impl ToError for Error { + fn to_error(self, _file: &'static str, _line: u32, _function: &'static str) -> Error { + self + } +} + +pub type Result = core::result::Result; + +#[macro_export] +macro_rules! function { + () => {{ + fn f() {} + fn type_name_of(_: T) -> &'static str { + std::any::type_name::() + } + let name = type_name_of(f); + &name[..name.len() - 3] + }}; +} + +#[macro_export] +macro_rules! to_error { + ($err:tt) => {{ + let line = std::line!(); + let function = function!(); + let file = std::file!(); + + use $crate::error::ToError; + $err.to_error(file, line, function) + }}; +} + +#[macro_export] +macro_rules! bail { + ($msg:literal) => { + { + return Err(to_error!($msg)); + } + }; + ($err:expr) => { + { + return Err(to_error!($err)); + } + }; + ($fmt:literal, $($arg:tt)*) => { + { + let message = std::format!($fmt, $($arg)*); + return Err(to_error!(message)); + } + }; +} + +#[macro_export] +macro_rules! resolve { + ($result:expr) => { + match $result { + Ok(val) => val, + Err(err) => bail!(err), + } + }; +} + +#[macro_export] +macro_rules! ensure { + ($condition:expr) => { + if !($condition as bool) { + bail!(std::format!("requirement `{}` failed", std::stringify!($condition))); + } + }; + ($condition:expr, $msg:literal) => { + if !($condition as bool) { + bail!($msg); + } + }; + ($condition:expr, $fmt:literal, $($arg:tt)*) => { + if !($condition as bool) { + bail!($fmt, $($arg)*); + } + }; +} + +#[macro_export] +macro_rules! ensure_not_null { + ($ptr:expr) => { + if $ptr.is_null() { + bail!(std::format!("`{}` must not be null", std::stringify!($ptr))); + } + }; +} + +#[macro_export] +macro_rules! ensure_equal { + ($left:expr, $right:expr) => { + let left_val = $left; + let right_val = $right; + if left_val != right_val { + bail!(std::format!( + "`{}` must equal `{}` but found left: {:?}, right: {:?}", + std::stringify!($left), + std::stringify!($right), + left_val, + right_val + )); + } + }; +} + +#[macro_export] +macro_rules! ensure_ok { + ($result:expr) => { + match $result { + Ok(_) => {} + Err(err) => bail!(err), + } + }; +} diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs new file mode 100644 index 000000000..6357dba9e --- /dev/null +++ b/tor-interface/src/lib.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod tor_controller; +pub mod tor_crypto; diff --git a/tor-interface/src/tor_controller.rs b/tor-interface/src/tor_controller.rs new file mode 100644 index 000000000..87dfc1b5c --- /dev/null +++ b/tor-interface/src/tor_controller.rs @@ -0,0 +1,1952 @@ +// standard +use std::cmp::Ordering; +use std::collections::VecDeque; +use std::default::Default; +use std::fs; +use std::fs::File; +use std::io::{BufRead, BufReader, ErrorKind, Read, Write}; +use std::iter; +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::ops::Drop; +use std::option::Option; +use std::path::Path; +use std::process; +use std::process::{Child, ChildStdout, Command, Stdio}; +use std::str::FromStr; +use std::string::ToString; +use std::sync::{atomic, Arc, Mutex}; +use std::time::{Duration, Instant}; + +// extern crates +use rand::distributions::Alphanumeric; +use rand::rngs::OsRng; +use rand::Rng; +use regex::Regex; +#[cfg(test)] +use serial_test::serial; +use socks::Socks5Stream; +use url::Host; + +// internal crates +use crate::error::Result; +use crate::tor_crypto::*; +use crate::*; + +// get the name of our tor executable +pub const fn tor_exe_name() -> &'static str { + if cfg!(windows) { + "tor.exe" + } else { + "tor" + } +} + +// securely generate password using OsRng +fn generate_password(length: usize) -> String { + let password: String = iter::repeat(()) + .map(|()| OsRng.sample(Alphanumeric)) + .map(char::from) + .take(length) + .collect(); + + password +} + +fn read_control_port_file(control_port_file: &Path) -> Result { + // open file + let mut file = resolve!(File::open(control_port_file)); + + // bail if the file is larger than expected + let metadata = resolve!(file.metadata()); + ensure!( + metadata.len() < 1024, + "control port file larger than expected: {} bytes", + metadata.len() + ); + + // read contents to string + let mut contents = String::new(); + resolve!(file.read_to_string(&mut contents)); + + if contents.starts_with("PORT=") { + let addr_string = &contents.trim_end()["PORT=".len()..]; + return Ok(resolve!(SocketAddr::from_str(addr_string))); + } + bail!( + "could not parse '{}' as control port file", + control_port_file.display() + ); +} + +// Encapsulates the tor daemon process +struct TorProcess { + control_addr: SocketAddr, + process: Child, + password: String, + // stdout data + stdout_lines: Arc>>, +} + +impl TorProcess { + // pub fn new(data_directory: &Path) -> Result { + pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { + ensure!(tor_bin_path.is_absolute()); + ensure!(data_directory.is_absolute()); + + // create data directory if it doesn't exist + if !data_directory.exists() { + resolve!(fs::create_dir_all(data_directory)); + } else { + ensure!( + !data_directory.is_file(), + "received data_directory '{}' is a file not a path", + data_directory.display() + ); + } + + // construct paths to torrc files + let default_torrc = data_directory.join("default_torrc"); + let torrc = data_directory.join("torrc"); + let control_port_file = data_directory.join("control_port"); + + // TODO: should we nuke the existing torrc between runs? Do we want + // users setting custom nonsense in there? + // construct default torrc + // - daemon determines socks port and only allows clients to connect to onion services + // - minimize writes to disk + // - start with network disabled by default + if !default_torrc.exists() { + const DEFAULT_TORRC_CONTENT: &str = "SocksPort auto OnionTrafficOnly\n\ + AvoidDiskWrites 1\n\ + DisableNetwork 1\n\n"; + + let mut default_torrc_file = resolve!(File::create(&default_torrc)); + resolve!(default_torrc_file.write_all(DEFAULT_TORRC_CONTENT.as_bytes())); + } + + // create empty torrc for user + if !torrc.exists() { + let _ = File::create(&torrc); + } + + // remove any existing control_port_file + if control_port_file.exists() { + ensure!( + control_port_file.is_file(), + "control port file '{}' exists but is a directory", + control_port_file.display() + ); + resolve!(fs::remove_file(&control_port_file)); + } + + const CONTROL_PORT_PASSWORD_LENGTH: usize = 32usize; + let password = generate_password(CONTROL_PORT_PASSWORD_LENGTH); + let password_hash = hash_tor_password(&password)?; + + let mut process = resolve!(Command::new(tor_bin_path.as_os_str()) + .stdout(Stdio::piped()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + // point to our above written torrc file + .arg("--defaults-torrc") + .arg(default_torrc) + // location of torrc + .arg("--torrc-file") + .arg(torrc) + // root data directory + .arg("DataDirectory") + .arg(data_directory) + // daemon will assign us a port, and we will + // read it from the control port file + .arg("ControlPort") + .arg("auto") + // control port file destination + .arg("ControlPortWriteToFile") + .arg(control_port_file.clone()) + // use password authentication to prevent other apps + // from modifying our daemon's settings + .arg("HashedControlPassword") + .arg(password_hash) + // tor process will shut down after this process shuts down + // to avoid orphaned tor daemon + .arg("__OwningControllerProcess") + .arg(process::id().to_string()) + .spawn()); + + let mut control_addr = None; + let start = Instant::now(); + + // try and read the control port from the control port file + // or abort after 5 seconds + // TODO: make this timeout configurable? + while control_addr.is_none() && start.elapsed() < Duration::from_secs(5) { + if control_port_file.exists() { + control_addr = Some(read_control_port_file(control_port_file.as_path())?); + resolve!(fs::remove_file(&control_port_file)); + } + } + + let control_addr = match control_addr { + Some(control_addr) => control_addr, + None => bail!( + "failed to read control addr from '{}'", + control_port_file.display() + ), + }; + + let stdout_lines: Arc>> = Default::default(); + + { + let stdout_lines = Arc::downgrade(&stdout_lines); + let stdout = BufReader::new(match process.stdout.take() { + Some(stdout) => stdout, + None => unreachable!(), + }); + + resolve!(std::thread::Builder::new() + .name("tor_stdout_reader".to_string()) + .spawn(move || { + TorProcess::read_stdout_task(&stdout_lines, stdout); + })); + } + + Ok(TorProcess { + control_addr, + process, + password, + stdout_lines, + }) + } + + fn read_stdout_task( + stdout_lines: &std::sync::Weak>>, + mut stdout: BufReader, + ) { + while let Some(stdout_lines) = stdout_lines.upgrade() { + let mut line = String::default(); + // read line + if stdout.read_line(&mut line).is_ok() { + // remove trailing '\n' + line.pop(); + // then acquire the lock on the line buffer + let mut stdout_lines = match stdout_lines.lock() { + Ok(stdout_lines) => stdout_lines, + Err(_) => unreachable!(), + }; + stdout_lines.push(line); + } + } + } + + fn wait_log_lines(&mut self) -> Vec { + let mut lines = match self.stdout_lines.lock() { + Ok(lines) => lines, + Err(_) => unreachable!(), + }; + std::mem::take(&mut lines) + } +} + +impl Drop for TorProcess { + fn drop(&mut self) { + let _ = self.process.kill(); + } +} + +pub struct ControlStream { + stream: TcpStream, + closed_by_remote: bool, + pending_data: Vec, + pending_lines: VecDeque, + pending_reply: Vec, + reading_multiline_value: bool, + // regexes used to parse control port responses + single_line_data: Regex, + multi_line_data: Regex, + end_reply_line: Regex, +} + +type StatusCode = u32; +struct Reply { + status_code: StatusCode, + reply_lines: Vec, +} + +impl ControlStream { + pub fn new(addr: &SocketAddr, read_timeout: Duration) -> Result { + ensure!( + read_timeout != Duration::ZERO, + "read_timeout must not be zero" + ); + + let stream = resolve!(TcpStream::connect(addr)); + resolve!(stream.set_read_timeout(Some(read_timeout))); + + // pre-allocate a kilobyte for the read buffer + const READ_BUFFER_SIZE: usize = 1024; + let pending_data = Vec::with_capacity(READ_BUFFER_SIZE); + + let single_line_data = resolve!(Regex::new(r"^\d\d\d-.*")); + let multi_line_data = resolve!(Regex::new(r"^\d\d\d+.*")); + let end_reply_line = resolve!(Regex::new(r"^\d\d\d .*")); + + Ok(ControlStream { + stream, + closed_by_remote: false, + pending_data, + pending_lines: Default::default(), + pending_reply: Default::default(), + reading_multiline_value: false, + // regex + single_line_data, + multi_line_data, + end_reply_line, + }) + } + + #[cfg(test)] + fn closed_by_remote(&mut self) -> bool { + self.closed_by_remote + } + + fn read_line(&mut self) -> Result> { + // read pending bytes from stream until we have a line to return + while self.pending_lines.is_empty() { + let byte_count = self.pending_data.len(); + match self.stream.read_to_end(&mut self.pending_data) { + Err(err) => { + if err.kind() == ErrorKind::WouldBlock || err.kind() == ErrorKind::TimedOut { + if byte_count == self.pending_data.len() { + return Ok(None); + } + } else { + bail!(err); + } + } + Ok(0usize) => { + self.closed_by_remote = true; + bail!("stream closed by remote") + } + Ok(_count) => (), + } + + // split our read buffer into individual lines + let mut begin = 0; + for index in 1..self.pending_data.len() { + if self.pending_data[index - 1] == b'\r' && self.pending_data[index] == b'\n' { + let end = index - 1; + // view into byte vec of just the found line + let line_view: &[u8] = &self.pending_data[begin..end]; + // convert to string + let line_string = resolve!(std::str::from_utf8(line_view)).to_string(); + + // save in pending list + self.pending_lines.push_back(line_string); + // update begin (and skip over \r\n) + begin = end + 2; + } + } + // leave any leftover bytes in the buffer for the next call + self.pending_data.drain(0..begin); + } + + Ok(self.pending_lines.pop_front()) + } + + fn read_reply(&mut self) -> Result> { + loop { + let current_line = match self.read_line() { + Ok(Some(line)) => line, + Ok(None) => return Ok(None), + Err(err) => return Err(err), + }; + + // make sure the status code matches (if we are not in the + // middle of a multi-line read + if let Some(first_line) = self.pending_reply.first() { + if !self.reading_multiline_value { + ensure!(first_line[0..3] == current_line[0..3]); + } + } + + // end of a response + if self.end_reply_line.is_match(¤t_line) { + ensure!(!self.reading_multiline_value); + self.pending_reply.push(current_line); + break; + // single line data from getinfo and friends + } else if self.single_line_data.is_match(¤t_line) { + ensure!(!self.reading_multiline_value); + self.pending_reply.push(current_line); + // begin of multiline data from getinfo and friends + } else if self.multi_line_data.is_match(¤t_line) { + ensure!(!self.reading_multiline_value); + self.pending_reply.push(current_line); + self.reading_multiline_value = true; + // multiline data to be squashed to a single entry + } else { + ensure!(self.reading_multiline_value); + // don't bother writing the end of multiline token + if current_line == "." { + self.reading_multiline_value = false; + } else { + let multiline = match self.pending_reply.last_mut() { + Some(multiline) => multiline, + None => unreachable!(), + }; + multiline.push('\n'); + multiline.push_str(¤t_line); + } + } + } + + // take ownership of the reply lines + let mut reply_lines: Vec = Default::default(); + std::mem::swap(&mut self.pending_reply, &mut reply_lines); + + // parse out the response code for easier matching + let status_code_string = match reply_lines.first() { + Some(line) => line[0..3].to_string(), + None => unreachable!(), + }; + let status_code: u32 = resolve!(status_code_string.parse()); + + // strip the redundant status code form start of lines + for line in reply_lines.iter_mut() { + println!(">>> {}", line); + if line.starts_with(&status_code_string) { + *line = line[4..].to_string(); + } + } + + Ok(Some(Reply { + status_code, + reply_lines, + })) + } + + pub fn write(&mut self, cmd: &str) -> Result<()> { + println!("<<< {}", cmd); + if let Err(err) = write!(self.stream, "{}\r\n", cmd) { + self.closed_by_remote = true; + bail!(err); + } + Ok(()) + } +} + +// Per-command data +#[derive(Default)] +pub struct AddOnionFlags { + pub discard_pk: bool, + pub detach: bool, + pub v3_auth: bool, + pub non_anonymous: bool, + pub max_streams_close_circuit: bool, +} + +#[derive(Default)] +pub struct OnionClientAuthAddFlags { + pub permanent: bool, +} + +// see version-spec.txt +pub struct Version { + pub major: u32, + pub minor: u32, + pub micro: u32, + pub patch_level: u32, + pub status_tag: Option, +} + +impl Version { + fn status_tag_pattern_is_match(status_tag: &str) -> bool { + if status_tag.is_empty() { + return false; + } + + for c in status_tag.chars() { + if c.is_whitespace() { + return false; + } + } + true + } + + fn new( + major: u32, + minor: u32, + micro: u32, + patch_level: Option, + status_tag: Option<&str>, + ) -> Result { + let status_tag = if let Some(status_tag) = status_tag { + ensure!(Self::status_tag_pattern_is_match(status_tag)); + Some(status_tag.to_string()) + } else { + None + }; + + Ok(Version { + major, + minor, + micro, + patch_level: patch_level.unwrap_or(0u32), + status_tag, + }) + } +} + +impl FromStr for Version { + type Err = error::Error; + + fn from_str(s: &str) -> Result { + // MAJOR.MINOR.MICRO[.PATCHLEVEL][-STATUS_TAG][ (EXTRA_INFO)]* + let mut tokens = s.split(' '); + let (major, minor, micro, patch_level, status_tag) = if let Some(version_status_tag) = + tokens.next() + { + let mut tokens = version_status_tag.split('-'); + let (major, minor, micro, patch_level) = if let Some(version) = tokens.next() { + let mut tokens = version.split('.'); + let major: u32 = if let Some(major) = tokens.next() { + match major.parse() { + Ok(major) => major, + Err(_) => bail!("failed to parse '{}' as MAJOR portion of version", major), + } + } else { + bail!("failed to find MAJOR portion of version"); + }; + let minor: u32 = if let Some(minor) = tokens.next() { + match minor.parse() { + Ok(minor) => minor, + Err(_) => bail!("failed to parse '{}' as MINOR portion of version", minor), + } + } else { + bail!("failed to find MINOR portion of version"); + }; + let micro: u32 = if let Some(micro) = tokens.next() { + match micro.parse() { + Ok(micro) => micro, + Err(_) => bail!("failed to parse '{}' as MICRO portion of version", micro), + } + } else { + bail!("failed to find MICRO portion of version"); + }; + let patch_level: u32 = if let Some(patch_level) = tokens.next() { + match patch_level.parse() { + Ok(patch_level) => patch_level, + Err(_) => bail!( + "failed to parse '{}' as PATCHLEVEL portion of version", + patch_level + ), + } + } else { + 0u32 + }; + (major, minor, micro, patch_level) + } else { + unreachable!(); + }; + let status_tag = tokens.next().map(|status_tag| status_tag.to_string()); + + (major, minor, micro, patch_level, status_tag) + } else { + bail!("failed to find MAJOR.MINOR.MICRO.[PATCH_LEVEL][-STATUS_TAG] portion of version"); + }; + for extra_info in tokens { + if !extra_info.starts_with('(') || !extra_info.ends_with(')') { + bail!("failed to parse '{}' as [ (EXTRA_INFO)]", extra_info); + } + } + Ok(Version { + major, + minor, + micro, + patch_level, + status_tag, + }) + } +} + +impl ToString for Version { + fn to_string(&self) -> String { + match &self.status_tag { + Some(status_tag) => format!( + "{}.{}.{}.{}-{}", + self.major, self.minor, self.micro, self.patch_level, status_tag + ), + None => format!( + "{}.{}.{}.{}", + self.major, self.minor, self.micro, self.patch_level + ), + } + } +} + +impl PartialEq for Version { + fn eq(&self, other: &Self) -> bool { + self.major == other.major + && self.minor == other.minor + && self.micro == other.micro + && self.patch_level == other.patch_level + && self.status_tag == other.status_tag + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + if let Some(order) = self.major.partial_cmp(&other.major) { + if order != Ordering::Equal { + return Some(order); + } + } + + if let Some(order) = self.minor.partial_cmp(&other.minor) { + if order != Ordering::Equal { + return Some(order); + } + } + + if let Some(order) = self.micro.partial_cmp(&other.micro) { + if order != Ordering::Equal { + return Some(order); + } + } + + if let Some(order) = self.patch_level.partial_cmp(&other.patch_level) { + if order != Ordering::Equal { + return Some(order); + } + } + + // version-spect.txt *does* say that we should compare tags lexicgraphically + // if all of the version numbers are the same when comparing, but we are + // going to diverge here and say we can only compare tags for equality. + // + // In practice we will be comparing tor daemon tags against tagless (stable) + // versions so this shouldn't be an issue + + if self.status_tag == other.status_tag { + return Some(Ordering::Equal); + } + + None + } +} + +enum AsyncEvent { + Unknown { + lines: Vec, + }, + StatusClient { + severity: String, + action: String, + arguments: Vec<(String, String)>, + }, + HsDesc { + action: String, + hs_address: V3OnionServiceId, + }, +} + +struct TorController { + // underlying control stream + control_stream: ControlStream, + // list of async replies to be handled + async_replies: Vec, + // regex for parsing events + status_event_pattern: Regex, + status_event_argument_pattern: Regex, + hs_desc_pattern: Regex, +} + +impl TorController { + pub fn new(control_stream: ControlStream) -> Result { + let status_event_pattern = resolve!(Regex::new( + r#"^STATUS_CLIENT (?PNOTICE|WARN|ERR) (?P[A-Za-z]+)"# + )); + let status_event_argument_pattern = resolve!(Regex::new( + r#"(?P[A-Z]+)=(?P[A-Za-z0-9_]+|"[^"]+")"# + )); + let hs_desc_pattern = resolve!(Regex::new( + r#"HS_DESC (?PREQUESTED|UPLOAD|RECEIVED|UPLOADED|IGNORE|FAILED|CREATED) (?P[a-z2-7]{56})"# + )); + + Ok(TorController { + control_stream, + async_replies: Default::default(), + // regex + status_event_pattern, + status_event_argument_pattern, + hs_desc_pattern, + }) + } + + // return curently available events, does not block waiting + // for an event + fn wait_async_replies(&mut self) -> Result> { + let mut replies: Vec = Default::default(); + // take any previously received async replies + std::mem::swap(&mut self.async_replies, &mut replies); + + // and keep consuming until none are available + loop { + if let Some(reply) = self.control_stream.read_reply()? { + replies.push(reply); + } else { + // no more replies immediately available so return + return Ok(replies); + } + } + } + + fn reply_to_event(&self, reply: &mut Reply) -> Result { + ensure!( + reply.status_code == 650u32, + "received unexpected synchrynous reply" + ); + + // not sure this is what we want but yolo + let reply_text = reply.reply_lines.join(" "); + if let Some(caps) = self.status_event_pattern.captures(&reply_text) { + let severity = match caps.name("severity") { + Some(severity) => severity.as_str(), + None => unreachable!(), + }; + let action = match caps.name("action") { + Some(action) => action.as_str(), + None => unreachable!(), + }; + + let mut arguments: Vec<(String, String)> = Default::default(); + for caps in self + .status_event_argument_pattern + .captures_iter(&reply_text) + { + let key = match caps.name("key") { + Some(key) => key.as_str(), + None => unreachable!(), + }; + let value = { + let value = match caps.name("value") { + Some(value) => value.as_str(), + None => unreachable!(), + }; + if value.starts_with('\"') && value.ends_with('\"') { + &value[1..value.len() - 1] + } else { + value + } + }; + arguments.push((key.to_string(), value.to_string())); + } + + return Ok(AsyncEvent::StatusClient { + severity: severity.to_string(), + action: action.to_string(), + arguments, + }); + } + + if let Some(caps) = self.hs_desc_pattern.captures(&reply_text) { + let action = match caps.name("action") { + Some(action) => action.as_str(), + None => unreachable!(), + }; + let hs_address = match caps.name("hsaddress") { + Some(hs_address) => hs_address.as_str(), + None => unreachable!(), + }; + + if let Ok(hs_address) = V3OnionServiceId::from_string(hs_address) { + return Ok(AsyncEvent::HsDesc { + action: action.to_string(), + hs_address, + }); + } + } + + // no luck parsing reply, just return full text + let mut reply_lines: Vec = Default::default(); + std::mem::swap(&mut reply_lines, &mut reply.reply_lines); + + Ok(AsyncEvent::Unknown { lines: reply_lines }) + } + + pub fn wait_async_events(&mut self) -> Result> { + let mut async_replies = self.wait_async_replies()?; + let mut async_events: Vec = Default::default(); + + for reply in async_replies.iter_mut() { + async_events.push(self.reply_to_event(reply)?); + } + + Ok(async_events) + } + + // wait for a sync reply, save off async replies for later + fn wait_sync_reply(&mut self) -> Result { + loop { + if let Some(reply) = self.control_stream.read_reply()? { + match reply.status_code { + 650u32 => self.async_replies.push(reply), + _ => return Ok(reply), + } + } + } + } + + fn write_command(&mut self, text: &str) -> Result { + self.control_stream.write(text)?; + self.wait_sync_reply() + } + + // + // Tor Commands + // + // The section where we can find the specification in control-spec.txt + // for the underlying command is listed in parentheses + // + // Each of these command wrapper methods block until completion + // + + // SETCONF (3.1) + fn setconf_cmd(&mut self, key_values: &[(&str, &str)]) -> Result { + ensure!(!key_values.is_empty()); + let mut command_buffer = vec!["SETCONF".to_string()]; + + for (key, value) in key_values.iter() { + command_buffer.push(format!("{}={}", key, value)); + } + let command = command_buffer.join(" "); + + self.write_command(&command) + } + + // GETCONF (3.3) + #[cfg(test)] + fn getconf_cmd(&mut self, keywords: &[&str]) -> Result { + ensure!(!keywords.is_empty()); + let command = format!("GETCONF {}", keywords.join(" ")); + + self.write_command(&command) + } + + // SETEVENTS (3.4) + fn setevents_cmd(&mut self, event_codes: &[&str]) -> Result { + ensure!(!event_codes.is_empty()); + let command = format!("SETEVENTS {}", event_codes.join(" ")); + + self.write_command(&command) + } + + // AUTHENTICATE (3.5) + fn authenticate_cmd(&mut self, password: &str) -> Result { + let command = format!("AUTHENTICATE \"{}\"", password); + + self.write_command(&command) + } + + // GETINFO (3.9) + fn getinfo_cmd(&mut self, keywords: &[&str]) -> Result { + ensure!(!keywords.is_empty()); + let command = format!("GETINFO {}", keywords.join(" ")); + + self.write_command(&command) + } + + // ADD_ONION (3.27) + fn add_onion_cmd( + &mut self, + key: Option<&Ed25519PrivateKey>, + flags: &AddOnionFlags, + max_streams: Option, + virt_port: u16, + target: Option, + client_auth: Option<&[X25519PublicKey]>, + ) -> Result { + let mut command_buffer = vec!["ADD_ONION".to_string()]; + + // set our key or request a new one + if let Some(key) = key { + command_buffer.push(key.to_key_blob()); + } else { + command_buffer.push("NEW:ED25519-V3".to_string()); + } + + // set our flags + let mut flag_buffer: Vec<&str> = Default::default(); + if flags.discard_pk { + flag_buffer.push("DiscardPK"); + } + if flags.detach { + flag_buffer.push("Detach"); + } + if flags.v3_auth { + flag_buffer.push("V3Auth"); + } + if flags.non_anonymous { + flag_buffer.push("NonAnonymous"); + } + if flags.max_streams_close_circuit { + flag_buffer.push("MaxStreamsCloseCircuit"); + } + + if !flag_buffer.is_empty() { + command_buffer.push(format!("Flags={}", flag_buffer.join(","))); + } + + // set max concurrent streams + if let Some(max_streams) = max_streams { + command_buffer.push(format!("MaxStreams={}", max_streams)); + } + + // set our onion service target + if let Some(target) = target { + command_buffer.push(format!("Port={},{}", virt_port, target)); + } else { + command_buffer.push(format!("Port={}", virt_port)); + } + // setup client auth + if let Some(client_auth) = client_auth { + for key in client_auth.iter() { + command_buffer.push(format!("ClientAuthV3={}", key.to_base32())); + } + } + + // finally send the command + let command = command_buffer.join(" "); + + self.write_command(&command) + } + + // DEL_ONION (3.38) + fn del_onion_cmd(&mut self, service_id: &V3OnionServiceId) -> Result { + let command = format!("DEL_ONION {}", service_id.to_string()); + + self.write_command(&command) + } + + // ONION_CLIENT_AUTH_ADD (3.30) + fn onion_client_auth_add_cmd( + &mut self, + service_id: &V3OnionServiceId, + private_key: &X25519PrivateKey, + client_name: Option, + flags: &OnionClientAuthAddFlags, + ) -> Result { + let mut command_buffer = vec!["ONION_CLIENT_AUTH_ADD".to_string()]; + + // set the onion service id + command_buffer.push(service_id.to_string()); + + // set our client's private key + command_buffer.push(format!("x25519:{}", private_key.to_base64())); + + if let Some(client_name) = client_name { + command_buffer.push(format!("ClientName={}", client_name)); + } + + if flags.permanent { + command_buffer.push("Flags=Permanent".to_string()); + } + + // finally send command + let command = command_buffer.join(" "); + + self.write_command(&command) + } + + // ONION_CLIENT_AUTH_REMOVE (3.31) + fn onion_client_auth_remove_cmd(&mut self, service_id: &V3OnionServiceId) -> Result { + let command = format!("ONION_CLIENT_AUTH_REMOVE {}", service_id.to_string()); + + self.write_command(&command) + } + + // + // Public high-level typesafe command method wrappers + // + + pub fn setconf(&mut self, key_values: &[(&str, &str)]) -> Result<()> { + let reply = self.setconf_cmd(key_values)?; + + match reply.status_code { + 250u32 => Ok(()), + code => bail!("{} {}", code, reply.reply_lines.join("\n")), + } + } + + #[cfg(test)] + pub fn getconf(&mut self, keywords: &[&str]) -> Result> { + let reply = self.getconf_cmd(keywords)?; + + match reply.status_code { + 250u32 => { + let mut key_values: Vec<(String, String)> = Default::default(); + for line in reply.reply_lines { + match line.find('=') { + Some(index) => key_values + .push((line[0..index].to_string(), line[index + 1..].to_string())), + None => key_values.push((line, String::new())), + } + } + Ok(key_values) + } + code => bail!("{} {}", code, reply.reply_lines.join("\n")), + } + } + + pub fn setevents(&mut self, events: &[&str]) -> Result<()> { + let reply = self.setevents_cmd(events)?; + + match reply.status_code { + 250u32 => Ok(()), + code => bail!("{} {}", code, reply.reply_lines.join("\n")), + } + } + + pub fn authenticate(&mut self, password: &str) -> Result<()> { + let reply = self.authenticate_cmd(password)?; + + match reply.status_code { + 250u32 => Ok(()), + code => bail!("{} {}", code, reply.reply_lines.join("\n")), + } + } + + pub fn getinfo(&mut self, keywords: &[&str]) -> Result> { + let reply = self.getinfo_cmd(keywords)?; + + match reply.status_code { + 250u32 => { + let mut key_values: Vec<(String, String)> = Default::default(); + for line in reply.reply_lines { + match line.find('=') { + Some(index) => key_values + .push((line[0..index].to_string(), line[index + 1..].to_string())), + None => { + if line != "OK" { + key_values.push((line, String::new())) + } + } + } + } + Ok(key_values) + } + code => bail!("{} {}", code, reply.reply_lines.join("\n")), + } + } + + pub fn add_onion( + &mut self, + key: Option<&Ed25519PrivateKey>, + flags: &AddOnionFlags, + max_streams: Option, + virt_port: u16, + target: Option, + client_auth: Option<&[X25519PublicKey]>, + ) -> Result<(Option, V3OnionServiceId)> { + let reply = self.add_onion_cmd(key, flags, max_streams, virt_port, target, client_auth)?; + + let mut private_key: Option = None; + let mut service_id: Option = None; + + match reply.status_code { + 250u32 => { + for line in reply.reply_lines { + if let Some(mut index) = line.find("ServiceID=") { + ensure!(service_id.is_none(), "received duplicate service ids"); + index += "ServiceId=".len(); + service_id = Some(V3OnionServiceId::from_string(&line[index..])?); + } else if let Some(mut index) = line.find("PrivateKey=") { + ensure!(private_key.is_none(), "received duplicate private keys"); + index += "PrivateKey=".len(); + private_key = Some(Ed25519PrivateKey::from_key_blob(&line[index..])?); + } else if line.contains("ClientAuthV3=") { + ensure!( + !client_auth.unwrap_or_default().is_empty(), + "received unexpected ClientAuthV3 keys" + ); + } else if !line.contains("OK") { + bail!("received unexpected reply line: '{}'", line); + } + } + } + code => bail!("{} {}", code, reply.reply_lines.join("\n")), + } + + if flags.discard_pk { + ensure!( + private_key.is_none(), + "private key should have been discarded" + ); + } else { + ensure!(private_key.is_some(), "did not return private key"); + } + + match service_id { + Some(service_id) => Ok((private_key, service_id)), + None => bail!("did not receive a serviceid"), + } + } + + pub fn del_onion(&mut self, service_id: &V3OnionServiceId) -> Result<()> { + let reply = self.del_onion_cmd(service_id)?; + + match reply.status_code { + 250u32 => Ok(()), + code => bail!("{} {}", code, reply.reply_lines.join("\n")), + } + } + + // more specific encapulsation of specific command invocations + + pub fn getinfo_net_listeners_socks(&mut self) -> Result> { + let response = self.getinfo(&["net/listeners/socks"])?; + for (key, value) in response.iter() { + if key.as_str() == "net/listeners/socks" { + if value.is_empty() { + return Ok(Default::default()); + } + // get our list of double-quoted strings + let listeners: Vec<&str> = value.split(' ').collect(); + let mut result: Vec = Default::default(); + for socket_addr in listeners.iter() { + ensure!(socket_addr.starts_with('\"') && socket_addr.ends_with('\"')); + + // remove leading/trailing double quote + let stripped = &socket_addr[1..socket_addr.len() - 1]; + result.push(resolve!(SocketAddr::from_str(stripped))); + } + return Ok(result); + } + } + bail!("did not find a 'net/listeners/socks' key/value"); + } + + pub fn getinfo_version(&mut self) -> Result { + let response = self.getinfo(&["version"])?; + for (key, value) in response.iter() { + if key.as_str() == "version" { + return Version::from_str(value); + } + } + bail!("did not find a 'version' key/value"); + } + + pub fn onion_client_auth_add( + &mut self, + service_id: &V3OnionServiceId, + private_key: &X25519PrivateKey, + client_name: Option, + flags: &OnionClientAuthAddFlags, + ) -> Result<()> { + let reply = self.onion_client_auth_add_cmd(service_id, private_key, client_name, flags)?; + + match reply.status_code { + 250u32..=252u32 => Ok(()), + code => bail!("{} {}", code, reply.reply_lines.join("\n")), + } + } + + #[allow(dead_code)] + pub fn onion_client_auth_remove(&mut self, service_id: &V3OnionServiceId) -> Result<()> { + let reply = self.onion_client_auth_remove_cmd(service_id)?; + + match reply.status_code { + 250u32..=251u32 => Ok(()), + code => bail!("{} {}", code, reply.reply_lines.join("\n")), + } + } +} + +pub struct CircuitToken { + username: String, + password: String, +} + +impl CircuitToken { + #[allow(dead_code)] + pub fn new(first_party: Host) -> CircuitToken { + const CIRCUIT_TOKEN_PASSWORD_LENGTH: usize = 32usize; + let username = first_party.to_string(); + let password = generate_password(CIRCUIT_TOKEN_PASSWORD_LENGTH); + + CircuitToken { username, password } + } +} + +pub struct OnionStream { + stream: TcpStream, + peer_addr: Option, +} + +impl OnionStream { + pub fn nodelay(&self) -> Result { + self.stream.nodelay() + } + + pub fn peer_addr(&self) -> Option<&V3OnionServiceId> { + self.peer_addr.as_ref() + } + + pub fn read_timeout(&self) -> Result, std::io::Error> { + self.stream.read_timeout() + } + + pub fn set_nodelay(&self, nodelay: bool) -> Result<(), std::io::Error> { + self.stream.set_nodelay(nodelay) + } + + pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { + self.stream.set_nonblocking(nonblocking) + } + + pub fn set_read_timeout(&self, dur: Option) -> Result<(), std::io::Error> { + self.stream.set_read_timeout(dur) + } + + pub fn set_write_timeout(&self, dur: Option) -> Result<(), std::io::Error> { + self.stream.set_write_timeout(dur) + } + + pub fn shutdown(&self, how: std::net::Shutdown) -> Result<(), std::io::Error> { + self.stream.shutdown(how) + } + + pub fn take_error(&self) -> Result, std::io::Error> { + self.stream.take_error() + } + + pub fn write_timeout(&self) -> Result, std::io::Error> { + self.stream.write_timeout() + } + + pub fn try_clone(&self) -> Result { + Ok(OnionStream { + stream: resolve!(self.stream.try_clone()), + peer_addr: self.peer_addr.clone(), + }) + } +} + +// pass-through to underlying Read stream +impl Read for OnionStream { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.stream.read(buf) + } +} + +// pass-through to underlying Write stream +impl Write for OnionStream { + fn write(&mut self, buf: &[u8]) -> Result { + self.stream.write(buf) + } + + fn flush(&mut self) -> Result<(), std::io::Error> { + self.stream.flush() + } +} + +impl From for TcpStream { + fn from(onion_stream: OnionStream) -> Self { + onion_stream.stream + } +} + +pub struct OnionListener { + listener: TcpListener, + is_active: Arc, +} + +impl OnionListener { + pub fn set_nonblocking(&self, nonblocking: bool) -> Result<()> { + resolve!(self.listener.set_nonblocking(nonblocking)); + Ok(()) + } + + pub fn accept(&self) -> Result> { + match self.listener.accept() { + Ok((stream, _socket_addr)) => Ok(Some(OnionStream { + stream, + peer_addr: None, + })), + Err(err) => { + if err.kind() == ErrorKind::WouldBlock { + Ok(None) + } else { + bail!(err); + } + } + } + } +} + +impl Drop for OnionListener { + fn drop(&mut self) { + self.is_active.store(false, atomic::Ordering::Relaxed); + } +} + +pub enum Event { + BootstrapStatus { + progress: u32, + tag: String, + summary: String, + }, + BootstrapComplete, + LogReceived { + line: String, + }, + OnionServicePublished { + service_id: V3OnionServiceId, + }, +} + +pub struct TorManager { + daemon: TorProcess, + controller: TorController, + socks_listener: Option, + // list of open onion services their is_active flag + onion_services: Vec<(V3OnionServiceId, Arc)>, +} + +impl TorManager { + pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { + // launch tor + let daemon = TorProcess::new(tor_bin_path, data_directory)?; + // open a control stream + let control_stream = ControlStream::new(&daemon.control_addr, Duration::from_millis(16))?; + + // create a controler + let mut controller = TorController::new(control_stream)?; + + // authenticate + controller.authenticate(&daemon.password)?; + + let min_required_version: Version = Version::new(0u32, 4u32, 6u32, Some(1u32), None)?; + + let version = controller.getinfo_version()?; + + ensure!( + version >= min_required_version, + "tor daemon not new enough; must be at least version {}", + min_required_version.to_string() + ); + + // register for STATUS_CLIENT async events + controller.setevents(&["STATUS_CLIENT", "HS_DESC"])?; + + Ok(TorManager { + daemon, + controller, + socks_listener: None, + onion_services: Default::default(), + }) + } + + pub fn update(&mut self) -> Result> { + let mut i = 0; + while i < self.onion_services.len() { + if !self.onion_services[i].1.load(atomic::Ordering::Relaxed) { + let entry = self.onion_services.swap_remove(i); + let service_id = entry.0; + + println!("deleting {}", service_id.to_string()); + self.controller.del_onion(&service_id)?; + } else { + i += 1; + } + } + + let mut events: Vec = Default::default(); + for async_event in self.controller.wait_async_events()?.iter() { + match async_event { + AsyncEvent::StatusClient { + severity, + action, + arguments, + } => { + if severity == "NOTICE" && action == "BOOTSTRAP" { + let mut progress: u32 = 0; + let mut tag: String = Default::default(); + let mut summary: String = Default::default(); + for (key, val) in arguments.iter() { + match key.as_str() { + "PROGRESS" => progress = resolve!(val.parse()), + "TAG" => tag = val.to_string(), + "SUMMARY" => summary = val.to_string(), + _ => {} // ignore unexpected arguments + } + } + events.push(Event::BootstrapStatus { + progress, + tag, + summary, + }); + if progress == 100u32 { + events.push(Event::BootstrapComplete); + } + } + } + AsyncEvent::HsDesc { action, hs_address } => { + if action == "UPLOADED" { + events.push(Event::OnionServicePublished { + service_id: hs_address.clone(), + }); + } + } + AsyncEvent::Unknown { lines } => { + println!("Received Unknown Event:"); + for line in lines.iter() { + println!(" {}", line); + } + } + } + } + + for log_line in self.daemon.wait_log_lines().iter_mut() { + events.push(Event::LogReceived { + line: std::mem::take(log_line), + }); + } + + Ok(events) + } + + #[allow(dead_code)] + pub fn version(&mut self) -> Result { + self.controller.getinfo_version() + } + + pub fn bootstrap(&mut self) -> Result<()> { + self.controller.setconf(&[("DisableNetwork", "0")]) + } + + pub fn add_client_auth( + &mut self, + service_id: &V3OnionServiceId, + client_auth: &X25519PrivateKey, + ) -> Result<()> { + self.controller + .onion_client_auth_add(service_id, client_auth, None, &Default::default()) + } + + #[allow(dead_code)] + pub fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<()> { + self.controller.onion_client_auth_remove(service_id) + } + + // connect to an onion service and returns OnionStream + pub fn connect( + &mut self, + service_id: &V3OnionServiceId, + virt_port: u16, + circuit: Option, + ) -> Result { + if self.socks_listener.is_none() { + let mut listeners = self.controller.getinfo_net_listeners_socks()?; + ensure!( + !listeners.is_empty(), + "no available socks listener to connect through" + ); + self.socks_listener = Some(listeners.swap_remove(0)); + } + + let socks_listener = match self.socks_listener { + Some(socks_listener) => socks_listener, + None => unreachable!(), + }; + + // our onion domain + let target = + socks::TargetAddr::Domain(format!("{}.onion", service_id.to_string()), virt_port); + // readwrite stream + let stream = match &circuit { + None => resolve!(Socks5Stream::connect(socks_listener, target)), + Some(circuit) => resolve!(Socks5Stream::connect_with_password( + socks_listener, + target, + &circuit.username, + &circuit.password + )), + }; + + Ok(OnionStream { + stream: stream.into_inner(), + peer_addr: Some(service_id.clone()), + }) + } + + // stand up an onion service and return an OnionListener + pub fn listener( + &mut self, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorized_clients: Option<&[X25519PublicKey]>, + ) -> Result { + // try to bind to a local address, let OS pick our port + let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); + let listener = resolve!(TcpListener::bind(socket_addr)); + let socket_addr = resolve!(listener.local_addr()); + + let mut flags = AddOnionFlags { + discard_pk: true, + ..Default::default() + }; + if authorized_clients.is_some() { + flags.v3_auth = true; + } + + // start onion service + let (_, service_id) = self.controller.add_onion( + Some(private_key), + &flags, + None, + virt_port, + Some(socket_addr), + authorized_clients, + )?; + + let is_active = Arc::new(atomic::AtomicBool::new(true)); + self.onion_services + .push((service_id, Arc::clone(&is_active))); + + Ok(OnionListener { + listener, + is_active, + }) + } +} + +#[test] +#[serial] +fn test_tor_controller() -> Result<()> { + let tor_path = resolve!(which::which(tor_exe_name())); + let mut data_path = std::env::temp_dir(); + data_path.push("test_tor_controller"); + let tor_process = TorProcess::new(&tor_path, &data_path)?; + + // create a scope to ensure tor_controller is dropped + { + let control_stream = + ControlStream::new(&tor_process.control_addr, Duration::from_millis(16))?; + + // create a tor controller and send authentication command + let mut tor_controller = TorController::new(control_stream)?; + tor_controller.authenticate_cmd(&tor_process.password)?; + ensure!( + tor_controller + .authenticate_cmd("invalid password")? + .status_code + == 515u32 + ); + + // tor controller should have shutdown the connection after failed authentication + if tor_controller + .authenticate_cmd(&tor_process.password) + .is_ok() + { + bail!("expected failure due to closed connection"); + } + ensure!(tor_controller.control_stream.closed_by_remote()); + } + // now create a second controller + { + let control_stream = + ControlStream::new(&tor_process.control_addr, Duration::from_millis(16))?; + + // create a tor controller and send authentication command + // all async events are just printed to stdout + let mut tor_controller = TorController::new(control_stream)?; + tor_controller.authenticate(&tor_process.password)?; + + // ensure everything is matching our default_torrc settings + let vals = tor_controller.getconf(&["SocksPort", "AvoidDiskWrites", "DisableNetwork"])?; + for (key, value) in vals.iter() { + let expected = match key.as_str() { + "SocksPort" => "auto OnionTrafficOnly", + "AvoidDiskWrites" => "1", + "DisableNetwork" => "1", + _ => bail!("unexpected returned key: {}", key), + }; + ensure!(value == expected); + } + + let vals = tor_controller.getinfo(&["version", "config-file", "config-text"])?; + let mut expected_torrc_path = data_path.clone(); + expected_torrc_path.push("torrc"); + let mut expected_control_port_path = data_path.clone(); + expected_control_port_path.push("control_port"); + for (key, value) in vals.iter() { + match key.as_str() { + "version" => ensure!(resolve!(Regex::new(r"\d+\.\d+\.\d+\.\d+")).is_match(&value)), + "config-file" => ensure!(Path::new(&value) == expected_torrc_path), + "config-text" => ensure!( + value.to_string() + == format!( + "\nControlPort auto\nControlPortWriteToFile {}\nDataDirectory {}", + expected_control_port_path.display(), + data_path.display() + ) + ), + _ => bail!("unexpected returned key: {}", key), + } + } + + tor_controller.setevents(&["STATUS_CLIENT"])?; + // begin bootstrap + tor_controller.setconf(&[("DisableNetwork", "0")])?; + + // add an onoin service + let (private_key, service_id) = + match tor_controller.add_onion(None, &Default::default(), None, 22, None, None)? { + (Some(private_key), service_id) => (private_key, service_id), + _ => bail!("add_onion did not return expected values"), + }; + println!("private_key: {}", private_key.to_key_blob()); + println!("service_id: {}", service_id.to_string()); + + if let Ok(()) = tor_controller.del_onion(&V3OnionServiceId::from_string( + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd", + )?) { + bail!("deleting unknown onion should have failed"); + } + + // delete our new onion + tor_controller.del_onion(&service_id)?; + + if let Ok(listeners) = tor_controller.getinfo_net_listeners_socks() { + println!("listeners: "); + for sock_addr in listeners.iter() { + println!(" {}", sock_addr); + } + } + + tor_controller.getinfo_net_listeners_socks()?; + + // print our event names available to tor + if let Ok(names) = tor_controller.getinfo(&["events/names"]) { + for (key, value) in names.iter() { + println!("{} : {}", key, value); + } + } + + let stop_time = Instant::now() + std::time::Duration::from_secs(5); + while stop_time > Instant::now() { + for async_event in tor_controller.wait_async_events()?.iter() { + match async_event { + AsyncEvent::Unknown { lines } => { + println!("Unknown: {}", lines.join("\n")); + } + AsyncEvent::StatusClient { + severity, + action, + arguments, + } => { + println!("STATUS_CLIENT severity={}, action={}", severity, action); + for (key, value) in arguments.iter() { + println!(" {}='{}'", key, value); + } + } + AsyncEvent::HsDesc { action, hs_address } => { + println!( + "HS_DESC action={}, hsaddress={}", + action, + hs_address.to_string() + ); + } + } + } + } + } + + Ok(()) +} + +#[test] +fn test_version() -> Result<()> { + ensure!(Version::from_str("1.2.3")? == Version::new(1, 2, 3, None, None)?); + ensure!(Version::from_str("1.2.3.4")? == Version::new(1, 2, 3, Some(4), None)?); + ensure!(Version::from_str("1.2.3-test")? == Version::new(1, 2, 3, None, Some("test"))?); + ensure!(Version::from_str("1.2.3.4-test")? == Version::new(1, 2, 3, Some(4), Some("test"))?); + ensure!(Version::from_str("1.2.3 (extra_info)")? == Version::new(1, 2, 3, None, None)?); + ensure!(Version::from_str("1.2.3.4 (extra_info)")? == Version::new(1, 2, 3, Some(4), None)?); + ensure!( + Version::from_str("1.2.3.4-tag (extra_info)")? + == Version::new(1, 2, 3, Some(4), Some("tag"))? + ); + + ensure!( + Version::from_str("1.2.3.4-tag (extra_info) (extra_info)")? + == Version::new(1, 2, 3, Some(4), Some("tag"))? + ); + + match Version::new(1, 2, 3, Some(4), Some("spaced tag")) { + Ok(_) => bail!("expected failure"), + Err(err) => println!("{:?}", err), + } + + match Version::new(1, 2, 3, Some(4), Some("" /* empty tag */)) { + Ok(_) => bail!("expected failure"), + Err(err) => println!("{:?}", err), + } + + match Version::from_str("") { + Ok(_) => bail!("expected failure"), + Err(err) => println!("{:?}", err), + } + + match Version::from_str("1.2") { + Ok(_) => bail!("expected failure"), + Err(err) => println!("{:?}", err), + } + + match Version::from_str("1.2-foo") { + Ok(_) => bail!("expected failure"), + Err(err) => println!("{:?}", err), + } + + match Version::from_str("1.2.3.4-foo bar") { + Ok(_) => bail!("expected failure"), + Err(err) => println!("{:?}", err), + } + + match Version::from_str("1.2.3.4-foo bar (extra_info)") { + Ok(_) => bail!("expected failure"), + Err(err) => println!("{:?}", err), + } + + match Version::from_str("1.2.3.4-foo (extra_info) badtext") { + Ok(_) => bail!("expected failure"), + Err(err) => println!("{:?}", err), + } + + ensure!(Version::new(0, 0, 0, Some(0), None)? < Version::new(1, 0, 0, Some(0), None)?); + ensure!(Version::new(0, 0, 0, Some(0), None)? < Version::new(0, 1, 0, Some(0), None)?); + ensure!(Version::new(0, 0, 0, Some(0), None)? < Version::new(0, 0, 1, Some(0), None)?); + + // ensure status tags make comparison between equal versions (apart from + // tags) unknowable + let zero_version = Version::new(0, 0, 0, Some(0), None)?; + let zero_version_tag = Version::new(0, 0, 0, Some(0), Some("tag"))?; + + ensure!(!(zero_version < zero_version_tag)); + ensure!(!(zero_version <= zero_version_tag)); + ensure!(!(zero_version > zero_version_tag)); + ensure!(!(zero_version >= zero_version_tag)); + + Ok(()) +} + +#[test] +#[serial] +fn test_tor_manager() -> Result<()> { + let tor_path = resolve!(which::which(tor_exe_name())); + let mut data_path = std::env::temp_dir(); + data_path.push("test_tor_manager"); + + let mut tor = TorManager::new(&tor_path, &data_path)?; + println!("version : {}", tor.version()?.to_string()); + tor.bootstrap()?; + + let mut received_log = false; + let mut bootstrap_complete = false; + while !bootstrap_complete { + for event in tor.update()?.iter() { + match event { + Event::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + Event::BootstrapComplete => { + println!("Bootstrap Complete!"); + bootstrap_complete = true; + } + Event::LogReceived { line } => { + received_log = true; + println!("--- {}", line); + } + _ => {} + } + } + } + ensure!( + received_log, + "should have received a log line from tor daemon" + ); + + Ok(()) +} + +#[test] +#[serial] +fn test_onion_service() -> Result<()> { + let tor_path = resolve!(which::which(tor_exe_name())); + let mut data_path = std::env::temp_dir(); + data_path.push("test_onion_service"); + + let mut tor = TorManager::new(&tor_path, &data_path)?; + + // for 30secs for bootstrap + tor.bootstrap()?; + + let mut bootstrap_complete = false; + while !bootstrap_complete { + for event in tor.update()?.iter() { + match event { + Event::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + Event::BootstrapComplete => { + println!("Bootstrap Complete!"); + bootstrap_complete = true; + } + Event::LogReceived { line } => { + println!("--- {}", line); + } + _ => {} + } + } + } + + // vanilla V3 onion service + { + // create an onion service for this test + let private_key = Ed25519PrivateKey::generate(); + + println!("Starting and listening to onion service"); + const VIRT_PORT: u16 = 42069u16; + let listener = tor.listener(&private_key, VIRT_PORT, None)?; + + let mut onion_published = false; + while !onion_published { + for event in tor.update()?.iter() { + match event { + Event::LogReceived { line } => { + println!("--- {}", line); + } + Event::OnionServicePublished { service_id } => { + let expected_service_id = V3OnionServiceId::from_private_key(&private_key); + if expected_service_id == *service_id { + println!("Onion Service {} published", service_id.to_string()); + onion_published = true; + } + } + _ => {} + } + } + } + + const MESSAGE: &str = "Hello World!"; + + { + let service_id = V3OnionServiceId::from_private_key(&private_key); + + println!("Connecting to onion service"); + let mut client = tor.connect(&service_id, VIRT_PORT, None)?; + println!("Client writing message: '{}'", MESSAGE); + resolve!(client.write_all(MESSAGE.as_bytes())); + resolve!(client.flush()); + println!("End of client scope"); + } + + if let Some(mut server) = listener.accept()? { + println!("Server reading message"); + let mut buffer = Vec::new(); + resolve!(server.read_to_end(&mut buffer)); + let msg = resolve!(String::from_utf8(buffer)); + + ensure!(MESSAGE == msg); + println!("Message received: '{}'", msg); + } else { + bail!("no listener"); + } + } + + // authenticated onion service + { + // create an onion service for this test + let private_key = Ed25519PrivateKey::generate(); + + let private_auth_key = X25519PrivateKey::generate(); + let public_auth_key = X25519PublicKey::from_private_key(&private_auth_key); + + println!("Starting and listening to authenticated onion service"); + const VIRT_PORT: u16 = 42069u16; + let listener = tor.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]))?; + + let mut onion_published = false; + while !onion_published { + for event in tor.update()?.iter() { + match event { + Event::LogReceived { line } => { + println!("--- {}", line); + } + Event::OnionServicePublished { service_id } => { + let expected_service_id = V3OnionServiceId::from_private_key(&private_key); + if expected_service_id == *service_id { + println!( + "Authenticated Onion Service {} published", + service_id.to_string() + ); + onion_published = true; + } + } + _ => {} + } + } + } + + const MESSAGE: &str = "Hello World!"; + + { + let service_id = V3OnionServiceId::from_private_key(&private_key); + + println!("Connecting to onion service (should fail)"); + if tor.connect(&service_id, VIRT_PORT, None).is_ok() { + bail!( + "should not able to connect to an authenticated onion service without auth key" + ); + } + + println!("Add auth key for onion service"); + tor.add_client_auth(&service_id, &private_auth_key)?; + + println!("Connecting to onion service with authentication"); + let mut client = tor.connect(&service_id, VIRT_PORT, None)?; + + println!("Client writing message: '{}'", MESSAGE); + resolve!(client.write_all(MESSAGE.as_bytes())); + resolve!(client.flush()); + println!("End of client scope"); + + println!("Remove auth key for onion service"); + tor.remove_client_auth(&service_id)?; + } + + if let Some(mut server) = listener.accept()? { + println!("Server reading message"); + let mut buffer = Vec::new(); + resolve!(server.read_to_end(&mut buffer)); + let msg = resolve!(String::from_utf8(buffer)); + + ensure!(MESSAGE == msg); + println!("Message received: '{}'", msg); + } else { + bail!("no listener"); + } + } + Ok(()) +} diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs new file mode 100644 index 000000000..f6e398c43 --- /dev/null +++ b/tor-interface/src/tor_crypto.rs @@ -0,0 +1,708 @@ +// standard +use std::convert::TryInto; +use std::str; + +// extern crates +use crypto::digest::Digest; +use crypto::sha1::Sha1; +use crypto::sha3::Sha3; +use data_encoding::{BASE32, BASE32_NOPAD, BASE64, HEXUPPER}; +use data_encoding_macro::new_encoding; +use rand::rngs::OsRng; +use rand::RngCore; +use signature::Verifier; +use tor_llcrypto::pk::keymanip::*; +use tor_llcrypto::util::rand_compat::RngCompatExt; +use tor_llcrypto::*; + +// internal modules +use crate::error::Result; +use crate::*; + +/// The number of bytes in an ed25519 secret key +/// cbindgen:ignore +pub const ED25519_PRIVATE_KEY_SIZE: usize = 64; +/// The number of bytes in an ed25519 public key +/// cbindgen:ignore +pub const ED25519_PUBLIC_KEY_SIZE: usize = 32; +/// The number of bytes in an ed25519 signature +/// cbindgen:ignore +pub const ED25519_SIGNATURE_SIZE: usize = 64; +/// The number of bytes needed to store onion service id as an ASCII c-string (not including null-terminator) +pub const V3_ONION_SERVICE_ID_LENGTH: usize = 56; +/// The number of bytes needed to store onion service id as an ASCII c-string (including null-terminator) +pub const V3_ONION_SERVICE_ID_SIZE: usize = V3_ONION_SERVICE_ID_LENGTH + 1; +/// The number of bytes needed to store base64 encoded ed25519 private key as an ASCII c-string (not including null-terminator) +pub const ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH: usize = 88; +/// key klob header string +const ED25519_PRIVATE_KEYBLOB_HEADER: &str = "ED25519-V3:"; +/// The number of bytes needed to store the keyblob header +pub const ED25519_PRIVATE_KEYBLOB_HEADER_LENGTH: usize = 11; +/// The number of bytes needed to store ed25519 private keyblob as an ASCII c-string (not including a null terminator) +pub const ED25519_PRIVATE_KEYBLOB_LENGTH: usize = + ED25519_PRIVATE_KEYBLOB_HEADER_LENGTH + ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH; +/// The number of bytes needed to store ed25519 private keyblob as an ASCII c-string (including a null terminator) +pub const ED25519_PRIVATE_KEYBLOB_SIZE: usize = ED25519_PRIVATE_KEYBLOB_LENGTH + 1; +// number of bytes in an onion service id after base32 decode +const V3_ONION_SERVICE_ID_RAW_SIZE: usize = 35; +// byte index of the start of the public key checksum +const V3_ONION_SERVICE_ID_CHECKSUM_OFFSET: usize = 32; +// byte index of the v3 onion service version +const V3_ONION_SERVICE_ID_VERSION_OFFSET: usize = 34; +/// The number of bytes in a v3 service id's truncated checksum +const TRUNCATED_CHECKSUM_SIZE: usize = 2; +/// The number of bytes in an x25519 private key +/// cbindgen:ignore +pub const X25519_PRIVATE_KEY_SIZE: usize = 32; +/// The number of bytes in an x25519 publickey +/// cbindgen:ignore +pub const X25519_PUBLIC_KEY_SIZE: usize = 32; +/// The number of bytes needed to store base64 encoded x25519 private key as an ASCII c-string (not including null-terminator) +pub const X25519_PRIVATE_KEYBLOB_BASE64_LENGTH: usize = 44; +/// The number of bytes needed to store base64 encoded x25519 private key as an ASCII c-string (including a null terminator) +pub const X25519_PRIVATE_KEYBLOB_BASE64_SIZE: usize = X25519_PRIVATE_KEYBLOB_BASE64_LENGTH + 1; +/// The number of bytes needed to store base32 encoded x25519 public key as an ASCII c-string (not including null-terminator) +pub const X25519_PUBLIC_KEYBLOB_BASE32_LENGTH: usize = 52; +/// The number of bytes needed to store bsae32 encoded x25519 public key as an ASCII c-string (including a null terminator) +pub const X25519_PUBLIC_KEYBLOB_BASE32_SIZE: usize = X25519_PUBLIC_KEYBLOB_BASE32_LENGTH + 1; + +const ONION_BASE32: data_encoding::Encoding = new_encoding! { + symbols: "abcdefghijklmnopqrstuvwxyz234567", + padding: '=', +}; + +const SHA1_BYTES: usize = 160 / 8; +const S2K_RFC2440_SPECIFIER_LEN: usize = 9; + +// see https://github.com/torproject/torspec/blob/main/rend-spec-v3.txt#L2143 +fn calc_truncated_checksum( + public_key: &[u8; ED25519_PUBLIC_KEY_SIZE], +) -> [u8; TRUNCATED_CHECKSUM_SIZE] { + // space for full checksum + const SHA256_BYTES: usize = 256 / 8; + let mut hash_bytes = [0u8; SHA256_BYTES]; + + let mut hasher = Sha3::sha3_256(); + assert_eq!(SHA256_BYTES, hasher.output_bytes()); + + // calculate checksum + hasher.input(b".onion checksum"); + hasher.input(public_key); + hasher.input(&[0x03u8]); + hasher.result(&mut hash_bytes); + + [hash_bytes[0], hash_bytes[1]] +} + +// Free functions + +fn hash_tor_password_with_salt( + salt: &[u8; S2K_RFC2440_SPECIFIER_LEN], + password: &str, +) -> Result { + if salt[S2K_RFC2440_SPECIFIER_LEN - 1] != 0x60 { + bail!( + "last byte in salt must be '0x60', received '{:#02X}'", + salt[S2K_RFC2440_SPECIFIER_LEN - 1] + ); + } + + // tor-specific rfc 2440 constants + const EXPBIAS: u8 = 6u8; + const C: u8 = 0x60; // salt[S2K_RFC2440_SPECIFIER_LEN - 1] + const COUNT: usize = (16usize + ((C & 15u8) as usize)) << ((C >> 4) + EXPBIAS); + + // squash together our hash input + let mut input: Vec = Default::default(); + // append salt (sans the 'C' constant') + input.extend_from_slice(&salt[0..S2K_RFC2440_SPECIFIER_LEN - 1]); + // append password bytes + input.extend_from_slice(password.as_bytes()); + + let input = input.as_slice(); + let input_len = input.len(); + + let mut sha1 = Sha1::new(); + let mut count = COUNT; + while count > 0 { + if count > input_len { + sha1.input(input); + count -= input_len; + } else { + sha1.input(&input[0..count]); + break; + } + } + + let mut key = [0u8; SHA1_BYTES]; + sha1.result(key.as_mut_slice()); + + let mut hash = "16:".to_string(); + HEXUPPER.encode_append(salt, &mut hash); + HEXUPPER.encode_append(&key, &mut hash); + + Ok(hash) +} + +pub fn hash_tor_password(password: &str) -> Result { + let mut salt = [0x00u8; S2K_RFC2440_SPECIFIER_LEN]; + OsRng.fill_bytes(&mut salt); + salt[S2K_RFC2440_SPECIFIER_LEN - 1] = 0x60u8; + + hash_tor_password_with_salt(&salt, password) +} + +// Struct deinitions + +pub struct Ed25519PrivateKey { + expanded_secret_key: pk::ed25519::ExpandedSecretKey, +} + +#[derive(Clone)] +pub struct Ed25519PublicKey { + public_key: pk::ed25519::PublicKey, +} + +#[derive(Clone)] +pub struct Ed25519Signature { + signature: pk::ed25519::Signature, +} + +#[derive(Clone)] +pub struct X25519PrivateKey { + secret_key: pk::curve25519::StaticSecret, +} + +#[derive(Clone)] +pub struct X25519PublicKey { + public_key: pk::curve25519::PublicKey, +} + +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct V3OnionServiceId { + data: [u8; V3_ONION_SERVICE_ID_LENGTH], +} + +// Ed25519 Private Key + +impl Ed25519PrivateKey { + pub fn generate() -> Ed25519PrivateKey { + let secret_key = pk::ed25519::SecretKey::generate(&mut rand_core::OsRng.rng_compat()); + + Ed25519PrivateKey { + expanded_secret_key: pk::ed25519::ExpandedSecretKey::from(&secret_key), + } + } + + // according to nickm, any 64 byte string here is allowed + pub fn from_raw(raw: &[u8; ED25519_PRIVATE_KEY_SIZE]) -> Ed25519PrivateKey { + Ed25519PrivateKey { + expanded_secret_key: match pk::ed25519::ExpandedSecretKey::from_bytes(raw) { + Ok(expanded_secret_key) => expanded_secret_key, + Err(_) => unreachable!(), + }, + } + } + + pub fn from_key_blob(key_blob: &str) -> Result { + if key_blob.len() != ED25519_PRIVATE_KEYBLOB_LENGTH { + bail!( + "expects string of length '{}'; received string with length '{}'", + ED25519_PRIVATE_KEYBLOB_LENGTH, + key_blob.len() + ); + } + + if !key_blob.starts_with(ED25519_PRIVATE_KEYBLOB_HEADER) { + bail!( + "expects string that begins with '{}'; received '{}'", + &ED25519_PRIVATE_KEYBLOB_HEADER, + &key_blob + ); + } + + let base64_key: &str = &key_blob[ED25519_PRIVATE_KEYBLOB_HEADER.len()..]; + let private_key_data = resolve!(BASE64.decode(base64_key.as_bytes())); + let private_key_data_len = private_key_data.len(); + let private_key_data_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = match private_key_data.try_into() + { + Ok(private_key_data) => private_key_data, + Err(_) => bail!( + "expects decoded private key length '{}'; actual '{}'", + ED25519_PRIVATE_KEY_SIZE, + private_key_data_len + ), + }; + + Ok(Ed25519PrivateKey::from_raw(&private_key_data_raw)) + } + + fn from_private_x25519(x25519_private: &X25519PrivateKey) -> Result<(Ed25519PrivateKey, u8)> { + if let Some((result, signbit)) = + convert_curve25519_to_ed25519_private(&x25519_private.secret_key) + { + return Ok(( + Ed25519PrivateKey { + expanded_secret_key: result, + }, + signbit, + )); + } + bail!("couldn't convert key"); + } + + pub fn to_key_blob(&self) -> String { + let mut key_blob = ED25519_PRIVATE_KEYBLOB_HEADER.to_string(); + key_blob.push_str(&BASE64.encode(&self.expanded_secret_key.to_bytes())); + + key_blob + } + + pub fn sign_message_ex( + &self, + public_key: &Ed25519PublicKey, + message: &[u8], + ) -> Ed25519Signature { + let signature = self + .expanded_secret_key + .sign(message, &public_key.public_key); + Ed25519Signature { signature } + } + + pub fn sign_message(&self, message: &[u8]) -> Ed25519Signature { + let public_key = Ed25519PublicKey::from_private_key(self); + self.sign_message_ex(&public_key, message) + } + + pub fn to_bytes(&self) -> [u8; ED25519_PRIVATE_KEY_SIZE] { + self.expanded_secret_key.to_bytes() + } +} + +impl PartialEq for Ed25519PrivateKey { + fn eq(&self, other: &Self) -> bool { + self.to_bytes().eq(&other.to_bytes()) + } +} + +impl Clone for Ed25519PrivateKey { + fn clone(&self) -> Ed25519PrivateKey { + Ed25519PrivateKey::from_raw(&self.to_bytes()) + } +} + +// Ed25519 Public Key + +impl Ed25519PublicKey { + pub fn from_raw(raw: &[u8; ED25519_PUBLIC_KEY_SIZE]) -> Result { + Ok(Ed25519PublicKey { + public_key: resolve!(pk::ed25519::PublicKey::from_bytes(raw)), + }) + } + + pub fn from_service_id(service_id: &V3OnionServiceId) -> Result { + // decode base32 encoded service id + let mut decoded_service_id = [0u8; V3_ONION_SERVICE_ID_RAW_SIZE]; + let decoded_byte_count = + match ONION_BASE32.decode_mut(service_id.as_bytes(), &mut decoded_service_id) { + Ok(decoded_byte_count) => decoded_byte_count, + Err(_) => bail!( + "failed to decode '{}' as V3OnionServiceId", + service_id.to_string() + ), + }; + if decoded_byte_count != V3_ONION_SERVICE_ID_RAW_SIZE { + bail!( + "decoded byte count is '{}', expected '{}'", + decoded_byte_count, + V3_ONION_SERVICE_ID_RAW_SIZE + ); + } + + Ok(Ed25519PublicKey { + public_key: resolve!(pk::ed25519::PublicKey::from_bytes( + &decoded_service_id[0..ED25519_PUBLIC_KEY_SIZE] + )), + }) + } + + pub fn from_private_key(private_key: &Ed25519PrivateKey) -> Ed25519PublicKey { + Ed25519PublicKey { + public_key: pk::ed25519::PublicKey::from(&private_key.expanded_secret_key), + } + } + + fn from_public_x25519( + public_x25519: &X25519PublicKey, + signbit: u8, + ) -> Result { + ensure!(signbit == 0u8 || signbit == 1u8); + Ok(Ed25519PublicKey { + public_key: match convert_curve25519_to_ed25519_public( + &public_x25519.public_key, + signbit, + ) { + Some(public_key) => public_key, + None => bail!("failed to convert public key"), + }, + }) + } + + pub fn to_base32(&self) -> String { + BASE32.encode(self.as_bytes()) + } + + pub fn as_bytes(&self) -> &[u8; ED25519_PUBLIC_KEY_SIZE] { + self.public_key.as_bytes() + } +} + +impl PartialEq for Ed25519PublicKey { + fn eq(&self, other: &Self) -> bool { + self.public_key.eq(&other.public_key) + } +} + +// Ed25519 Signature + +impl Ed25519Signature { + pub fn from_raw(raw: &[u8; ED25519_SIGNATURE_SIZE]) -> Result { + Ok(Ed25519Signature { + signature: resolve!(pk::ed25519::Signature::from_bytes(raw)), + }) + } + + pub fn verify(&self, message: &[u8], public_key: &Ed25519PublicKey) -> bool { + if let Ok(()) = public_key.public_key.verify(message, &self.signature) { + return true; + } + false + } + + // derives an ed25519 public key from the provided x25519 public key and signbit, then + // verifies this signature using said ed25519 public key + pub fn verify_x25519(&self, message: &[u8], public_key: &X25519PublicKey, signbit: u8) -> bool { + if signbit != 0u8 && signbit != 1u8 { + return false; + } + + if let Ok(public_key) = Ed25519PublicKey::from_public_x25519(public_key, signbit) { + return self.verify(message, &public_key); + } + false + } + + pub fn to_bytes(&self) -> [u8; ED25519_SIGNATURE_SIZE] { + self.signature.to_bytes() + } +} + +impl PartialEq for Ed25519Signature { + fn eq(&self, other: &Self) -> bool { + self.signature.eq(&other.signature) + } +} + +// X25519 Private Key + +impl X25519PrivateKey { + pub fn generate() -> X25519PrivateKey { + X25519PrivateKey { + secret_key: pk::curve25519::StaticSecret::new(rand_core::OsRng.rng_compat()), + } + } + + pub fn from_raw(raw: &[u8; X25519_PRIVATE_KEY_SIZE]) -> X25519PrivateKey { + X25519PrivateKey { + secret_key: pk::curve25519::StaticSecret::from(*raw), + } + } + + // a base64 encoded keyblob + pub fn from_base64(base64: &str) -> Result { + ensure!(base64.len() == X25519_PRIVATE_KEYBLOB_BASE64_LENGTH, + "X25519PrivateKey::from_base64(): expects string of length '{}'; received string with length '{}'", X25519_PRIVATE_KEYBLOB_BASE64_LENGTH, base64.len()); + + let private_key_data = resolve!(BASE64.decode(base64.as_bytes())); + let private_key_data_len = private_key_data.len(); + let private_key_data_raw: [u8; X25519_PRIVATE_KEY_SIZE] = match private_key_data.try_into() + { + Ok(private_key_data) => private_key_data, + Err(_) => bail!( + "expects decoded private key length '{}'; actual '{}'", + X25519_PRIVATE_KEY_SIZE, + private_key_data_len + ), + }; + + Ok(X25519PrivateKey::from_raw(&private_key_data_raw)) + } + + // security note: only ever sign messages the private key owner controls the contents of! + // this function first derives an ed25519 private key from the provided x25519 private key + // and signs the message, returning the signature and signbit needed to calculate the + // ed25519 public key from our x25519 private key's associated x25519 public key + pub fn sign_message(&self, message: &[u8]) -> Result<(Ed25519Signature, u8)> { + let (ed25519_private, signbit) = Ed25519PrivateKey::from_private_x25519(self)?; + Ok((ed25519_private.sign_message(message), signbit)) + } + + pub fn to_base64(&self) -> String { + BASE64.encode(&self.secret_key.to_bytes()) + } + + pub fn to_bytes(&self) -> [u8; X25519_PRIVATE_KEY_SIZE] { + self.secret_key.to_bytes() + } +} + +// X25519 Public Key +impl X25519PublicKey { + pub fn from_private_key(private_key: &X25519PrivateKey) -> X25519PublicKey { + X25519PublicKey { + public_key: pk::curve25519::PublicKey::from(&private_key.secret_key), + } + } + + pub fn from_raw(raw: &[u8; X25519_PUBLIC_KEY_SIZE]) -> X25519PublicKey { + X25519PublicKey { + public_key: pk::curve25519::PublicKey::from(*raw), + } + } + + pub fn from_base32(base32: &str) -> Result { + ensure!(base32.len() == X25519_PUBLIC_KEYBLOB_BASE32_LENGTH, + "X25519PublicKey::from_base32(): expects string of length '{}'; received '{}' with length '{}'", X25519_PUBLIC_KEYBLOB_BASE32_LENGTH, base32, base32.len()); + + let public_key_data = resolve!(BASE32_NOPAD.decode(base32.as_bytes())); + let public_key_data_len = public_key_data.len(); + let public_key_data_raw: [u8; X25519_PUBLIC_KEY_SIZE] = match public_key_data.try_into() { + Ok(public_key_data) => public_key_data, + Err(_) => bail!( + "expects decoded public key length '{}'; actual '{}'", + X25519_PUBLIC_KEY_SIZE, + public_key_data_len + ), + }; + + Ok(X25519PublicKey::from_raw(&public_key_data_raw)) + } + + pub fn to_base32(&self) -> String { + BASE32_NOPAD.encode(self.public_key.as_bytes()) + } + + pub fn as_bytes(&self) -> &[u8; X25519_PUBLIC_KEY_SIZE] { + self.public_key.as_bytes() + } +} + +// Onion Service Id + +impl V3OnionServiceId { + pub fn from_string(service_id: &str) -> Result { + if !V3OnionServiceId::is_valid(service_id) { + bail!("'{}' is not a valid v3 onion service id", &service_id); + } + Ok(V3OnionServiceId { + data: resolve!(service_id.as_bytes().try_into()), + }) + } + + pub fn from_public_key(public_key: &Ed25519PublicKey) -> V3OnionServiceId { + let mut raw_service_id = [0u8; V3_ONION_SERVICE_ID_RAW_SIZE]; + + raw_service_id[..ED25519_PUBLIC_KEY_SIZE].copy_from_slice(&public_key.as_bytes()[..]); + let truncated_checksum = calc_truncated_checksum(public_key.as_bytes()); + raw_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET] = truncated_checksum[0]; + raw_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET + 1] = truncated_checksum[1]; + raw_service_id[V3_ONION_SERVICE_ID_VERSION_OFFSET] = 0x03u8; + + let mut service_id = [0u8; V3_ONION_SERVICE_ID_LENGTH]; + // panics on wrong buffer size, but given our constant buffer sizes should be fine + ONION_BASE32.encode_mut(&raw_service_id, &mut service_id); + + V3OnionServiceId { data: service_id } + } + + pub fn from_private_key(private_key: &Ed25519PrivateKey) -> V3OnionServiceId { + Self::from_public_key(&Ed25519PublicKey::from_private_key(private_key)) + } + + pub fn is_valid(service_id: &str) -> bool { + if service_id.len() != V3_ONION_SERVICE_ID_LENGTH { + return false; + } + + let mut decoded_service_id = [0u8; V3_ONION_SERVICE_ID_RAW_SIZE]; + match ONION_BASE32.decode_mut(service_id.as_bytes(), &mut decoded_service_id) { + Ok(decoded_byte_count) => { + // ensure right size + if decoded_byte_count != V3_ONION_SERVICE_ID_RAW_SIZE { + return false; + } + // ensure correct version + if decoded_service_id[V3_ONION_SERVICE_ID_VERSION_OFFSET] != 0x03 { + return false; + } + // copy public key into own buffer + let mut public_key = [0u8; ED25519_PUBLIC_KEY_SIZE]; + public_key[..].copy_from_slice(&decoded_service_id[..ED25519_PUBLIC_KEY_SIZE]); + // ensure checksum is correct + let truncated_checksum = calc_truncated_checksum(&public_key); + if truncated_checksum[0] != decoded_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET] + || truncated_checksum[1] + != decoded_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET + 1] + { + return false; + } + true + } + Err(_) => false, + } + } + + pub fn as_bytes(&self) -> &[u8; V3_ONION_SERVICE_ID_LENGTH] { + &self.data + } +} + +impl ToString for V3OnionServiceId { + fn to_string(&self) -> String { + return unsafe { str::from_utf8_unchecked(&self.data).to_string() }; + } +} + +impl std::fmt::Debug for V3OnionServiceId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_string()) + } +} + +#[test] +fn test_ed25519() -> Result<()> { + let private_key_blob = "ED25519-V3:YE3GZtDmc+izGijWKgeVRabbXqK456JKKGONDBhV+kPBVKa2mHVQqnRTVuFXe3inU3YW6qvc7glYEwe9rK0LhQ=="; + let private_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = [ + 0x60u8, 0x4du8, 0xc6u8, 0x66u8, 0xd0u8, 0xe6u8, 0x73u8, 0xe8u8, 0xb3u8, 0x1au8, 0x28u8, + 0xd6u8, 0x2au8, 0x07u8, 0x95u8, 0x45u8, 0xa6u8, 0xdbu8, 0x5eu8, 0xa2u8, 0xb8u8, 0xe7u8, + 0xa2u8, 0x4au8, 0x28u8, 0x63u8, 0x8du8, 0x0cu8, 0x18u8, 0x55u8, 0xfau8, 0x43u8, 0xc1u8, + 0x54u8, 0xa6u8, 0xb6u8, 0x98u8, 0x75u8, 0x50u8, 0xaau8, 0x74u8, 0x53u8, 0x56u8, 0xe1u8, + 0x57u8, 0x7bu8, 0x78u8, 0xa7u8, 0x53u8, 0x76u8, 0x16u8, 0xeau8, 0xabu8, 0xdcu8, 0xeeu8, + 0x09u8, 0x58u8, 0x13u8, 0x07u8, 0xbdu8, 0xacu8, 0xadu8, 0x0bu8, 0x85u8, + ]; + let public_raw: [u8; ED25519_PUBLIC_KEY_SIZE] = [ + 0xf2u8, 0xfdu8, 0xa2u8, 0xdbu8, 0xf3u8, 0x80u8, 0xa6u8, 0xbau8, 0x74u8, 0xa4u8, 0x90u8, + 0xe1u8, 0x45u8, 0x55u8, 0xeeu8, 0xb9u8, 0x32u8, 0xa0u8, 0x5cu8, 0x39u8, 0x5au8, 0xe2u8, + 0x02u8, 0x83u8, 0x55u8, 0x27u8, 0x89u8, 0x6au8, 0x1fu8, 0x2fu8, 0x3du8, 0xc5u8, + ]; + let public_base32 = "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCQ===="; + let service_id_string = "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd"; + assert!(V3OnionServiceId::is_valid(&service_id_string)); + + let mut message = [0x00u8; 256]; + let null_message = [0x00u8; 256]; + for (i, ptr) in message.iter_mut().enumerate() { + *ptr = i as u8; + } + let signature_raw: [u8; ED25519_SIGNATURE_SIZE] = [ + 0xa6u8, 0xd6u8, 0xc6u8, 0x1au8, 0x03u8, 0xbcu8, 0x43u8, 0x6fu8, 0x38u8, 0x53u8, 0x94u8, + 0xcdu8, 0xdcu8, 0x86u8, 0x0au8, 0x88u8, 0x64u8, 0x43u8, 0x1du8, 0x18u8, 0x84u8, 0x30u8, + 0x2fu8, 0xcdu8, 0xa6u8, 0x79u8, 0xcau8, 0x87u8, 0xd0u8, 0x29u8, 0xe7u8, 0x2bu8, 0x32u8, + 0x9bu8, 0xa2u8, 0xa4u8, 0x3cu8, 0x74u8, 0x6au8, 0x08u8, 0x67u8, 0x0eu8, 0x63u8, 0x60u8, + 0xcbu8, 0x46u8, 0x22u8, 0x55u8, 0x43u8, 0x5bu8, 0x84u8, 0x68u8, 0x0fu8, 0x47u8, 0xceu8, + 0x6cu8, 0xd2u8, 0xb8u8, 0xebu8, 0xfeu8, 0xf6u8, 0x9eu8, 0x97u8, 0x0au8, + ]; + + // test the golden path first + let service_id = V3OnionServiceId::from_string(&service_id_string)?; + + let private_key = Ed25519PrivateKey::from_raw(&private_raw); + assert!(private_key == Ed25519PrivateKey::from_key_blob(&private_key_blob)?); + assert!(private_key_blob == private_key.to_key_blob()); + + let public_key = Ed25519PublicKey::from_raw(&public_raw)?; + assert!(public_key == Ed25519PublicKey::from_service_id(&service_id)?); + assert!(public_key == Ed25519PublicKey::from_private_key(&private_key)); + assert!(service_id == V3OnionServiceId::from_public_key(&public_key)); + assert!(public_base32 == public_key.to_base32()); + + let signature = private_key.sign_message(&message); + assert!(signature == Ed25519Signature::from_raw(&signature_raw)?); + assert!(signature.verify(&message, &public_key)); + assert!(!signature.verify(&null_message, &public_key)); + + // some invalid service ids + assert!(!V3OnionServiceId::is_valid("")); + assert!(!V3OnionServiceId::is_valid( + " + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + )); + assert!(!V3OnionServiceId::is_valid( + "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCZSJYD" + )); + + // generate a new key, get the public key and sign/verify a message + let private_key = Ed25519PrivateKey::generate(); + let public_key = Ed25519PublicKey::from_private_key(&private_key); + let signature = private_key.sign_message(&message); + assert!(signature.verify(&message, &public_key)); + + Ok(()) +} + +#[test] +fn test_password_hash() -> Result<()> { + let salt1: [u8; S2K_RFC2440_SPECIFIER_LEN] = [ + 0xbeu8, 0x2au8, 0x25u8, 0x1du8, 0xe6u8, 0x2cu8, 0xb2u8, 0x7au8, 0x60u8, + ]; + let hash1 = hash_tor_password_with_salt(&salt1, "abcdefghijklmnopqrstuvwxyz")?; + assert!(hash1 == "16:BE2A251DE62CB27A60AC9178A937990E8ED0AB662FA82A5C7DE3EBB23A"); + + let salt2: [u8; S2K_RFC2440_SPECIFIER_LEN] = [ + 0x36u8, 0x73u8, 0x0eu8, 0xefu8, 0xd1u8, 0x8cu8, 0x60u8, 0xd6u8, 0x60u8, + ]; + let hash2 = hash_tor_password_with_salt(&salt2, "password")?; + assert!(hash2 == "16:36730EEFD18C60D66052E7EA535438761C0928D316EEA56A190C99B50A"); + + // ensure same password is hashed to different things + assert!(hash_tor_password("password")? != hash_tor_password("password")?); + + Ok(()) +} + +#[test] +fn test_x25519() -> Result<()> { + // private/public key pair + const SECRET_BASE64: &str = "0GeSReJXdNcgvWRQdnDXhJGdu5UiwP2fefgT93/oqn0="; + const SECRET_RAW: [u8; X25519_PRIVATE_KEY_SIZE] = [ + 0xd0u8, 0x67u8, 0x92u8, 0x45u8, 0xe2u8, 0x57u8, 0x74u8, 0xd7u8, 0x20u8, 0xbdu8, 0x64u8, + 0x50u8, 0x76u8, 0x70u8, 0xd7u8, 0x84u8, 0x91u8, 0x9du8, 0xbbu8, 0x95u8, 0x22u8, 0xc0u8, + 0xfdu8, 0x9fu8, 0x79u8, 0xf8u8, 0x13u8, 0xf7u8, 0x7fu8, 0xe8u8, 0xaau8, 0x7du8, + ]; + const PUBLIC_BASE32: &str = "AEXCBCEDJ5KU34YGGMZ7PVHVDEA7D7YB7VQAPJTMTZGRJLN3JASA"; + const PUBLIC_RAW: [u8; X25519_PUBLIC_KEY_SIZE] = [ + 0x01u8, 0x2eu8, 0x20u8, 0x88u8, 0x83u8, 0x4fu8, 0x55u8, 0x4du8, 0xf3u8, 0x06u8, 0x33u8, + 0x33u8, 0xf7u8, 0xd4u8, 0xf5u8, 0x19u8, 0x01u8, 0xf1u8, 0xffu8, 0x01u8, 0xfdu8, 0x60u8, + 0x07u8, 0xa6u8, 0x6cu8, 0x9eu8, 0x4du8, 0x14u8, 0xadu8, 0xbbu8, 0x48u8, 0x24u8, + ]; + + // ensure we can convert from raw as expected + ensure!(&X25519PrivateKey::from_raw(&SECRET_RAW).to_base64() == SECRET_BASE64); + ensure!(&X25519PublicKey::from_raw(&PUBLIC_RAW).to_base32() == PUBLIC_BASE32); + + // ensure we can round-trip as expected + ensure!(&X25519PrivateKey::from_base64(&SECRET_BASE64)?.to_base64() == SECRET_BASE64); + ensure!(&X25519PublicKey::from_base32(&PUBLIC_BASE32)?.to_base32() == PUBLIC_BASE32); + + // ensure we generate the expected public key from private key + let private_key = X25519PrivateKey::from_base64(&SECRET_BASE64)?; + let public_key = X25519PublicKey::from_private_key(&private_key); + ensure!(public_key.to_base32() == PUBLIC_BASE32); + + let message = b"All around me are familiar faces"; + + let (signature, signbit) = private_key.sign_message(message)?; + ensure!(signature.verify_x25519(message, &public_key, signbit)); + + Ok(()) +} From ed69caec03e6c9b028629c2ff4739ca0b0e8ca2e Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 20 May 2023 20:45:30 +0000 Subject: [PATCH 002/184] tor-interface: refactored tor-interface modules; converted crate error handling to use thiserror crate; convertd test code to use anyhow crate --- tor-interface/Cargo.toml | 6 +- tor-interface/src/error.rs | 299 +++++---- tor-interface/src/tor_controller.rs | 987 +++++++++++++++++----------- tor-interface/src/tor_crypto.rs | 249 ++++--- 4 files changed, 914 insertions(+), 627 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index de80494fb..7a9073e49 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -16,5 +16,9 @@ signature = "1.5.0" socks = "0.3.4" tor-llcrypto = { version = "0.2.0", features = ["relay"] } url = "2.2.2" +thiserror = "1.0" + +[dev-dependencies] +anyhow = "1.0" serial_test = "0.9.0" -which = "4.3.0" +which = "4.3.0" \ No newline at end of file diff --git a/tor-interface/src/error.rs b/tor-interface/src/error.rs index 6073bd1e9..756b2523a 100644 --- a/tor-interface/src/error.rs +++ b/tor-interface/src/error.rs @@ -1,164 +1,175 @@ -use std::fmt; - -pub struct Error { - message: String, - file: &'static str, - line: u32, - function: &'static str, +#[derive(thiserror::Error, Debug)] +pub enum TorCryptoError { + #[error("{0}")] + ParseError(String), + #[error("{0}")] + ConversionError(String), } -impl Error { - pub fn new(message: String, file: &'static str, line: u32, function: &'static str) -> Self { - Self { - message, - line, - function, - file, - } - } -} +#[derive(thiserror::Error, Debug)] +pub enum TorProcessError { + #[error("failed to read control port file")] + ControlPortFileReadFailed(#[source] std::io::Error), -impl fmt::Debug for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}({}:{}): {}", - self.function, self.file, self.line, self.message - ) - } -} + #[error("provided control port file '{0}' larger than expected ({1} bytes)")] + ControlPortFileTooLarge(String, u64), -pub trait ToError { - fn to_error(self, file: &'static str, line: u32, function: &'static str) -> Error; -} + #[error("failed to parse '{0}' as control port file")] + ControlPortFileContentsInvalid(String), -impl ToError for T -where - T: std::string::ToString, -{ - fn to_error(self, file: &'static str, line: u32, function: &'static str) -> Error { - Error { - message: self.to_string(), - line, - function, - file, - } - } -} + #[error("provided tor bin path '{0}' must be an absolute path")] + TorBinPathNotAbsolute(String), -impl ToError for Error { - fn to_error(self, _file: &'static str, _line: u32, _function: &'static str) -> Error { - self - } -} + #[error("provided data directory '{0}' must be an absolute path")] + TorDataDirectoryPathNotAbsolute(String), -pub type Result = core::result::Result; - -#[macro_export] -macro_rules! function { - () => {{ - fn f() {} - fn type_name_of(_: T) -> &'static str { - std::any::type_name::() - } - let name = type_name_of(f); - &name[..name.len() - 3] - }}; -} + #[error("failed to create data directory")] + DataDirectoryCreationFailed(#[source] std::io::Error), -#[macro_export] -macro_rules! to_error { - ($err:tt) => {{ - let line = std::line!(); - let function = function!(); - let file = std::file!(); + #[error("file exists in provided data directory path '{0}'")] + DataDirectoryPathExistsAsFile(String), - use $crate::error::ToError; - $err.to_error(file, line, function) - }}; -} + #[error("failed to create default_torrc file")] + DefaultTorrcFileCreationFailed(#[source] std::io::Error), -#[macro_export] -macro_rules! bail { - ($msg:literal) => { - { - return Err(to_error!($msg)); - } - }; - ($err:expr) => { - { - return Err(to_error!($err)); - } - }; - ($fmt:literal, $($arg:tt)*) => { - { - let message = std::format!($fmt, $($arg)*); - return Err(to_error!(message)); - } - }; -} + #[error("failed to write default_torrc file")] + DefaultTorrcFileWriteFailed(#[source] std::io::Error), + + #[error("failed to create torrc file")] + TorrcFileCreationFailed(#[source] std::io::Error), + + #[error("failed to remove control_port file")] + ControlPortFileDeleteFailed(#[source] std::io::Error), + + #[error("failed to start tor process")] + TorProcessStartFailed(#[source] std::io::Error), -#[macro_export] -macro_rules! resolve { - ($result:expr) => { - match $result { - Ok(val) => val, - Err(err) => bail!(err), - } - }; + #[error("failed to read control addr from control_file '{0}'")] + ControlPortFileMissing(String), + + #[error("unable to take tor process stdout")] + TorProcessStdoutTakeFailed(), + + #[error("failed to spawn tor process stdout read thread")] + StdoutReadThreadSpawnFailed(#[source] std::io::Error), } -#[macro_export] -macro_rules! ensure { - ($condition:expr) => { - if !($condition as bool) { - bail!(std::format!("requirement `{}` failed", std::stringify!($condition))); - } - }; - ($condition:expr, $msg:literal) => { - if !($condition as bool) { - bail!($msg); - } - }; - ($condition:expr, $fmt:literal, $($arg:tt)*) => { - if !($condition as bool) { - bail!($fmt, $($arg)*); - } - }; +#[derive(thiserror::Error, Debug)] +pub enum ControlStreamError { + #[error("control stream read timeout must not be zero")] + ReadTimeoutZero(), + + #[error("could not connect to control port")] + CreationFailed(#[source] std::io::Error), + + #[error("configure control port socket failed")] + ConfigurationFailed(#[source] std::io::Error), + + #[error("control port parsing regex creation failed")] + ParsingRegexCreationFailed(#[source] regex::Error), + + #[error("control port stream read failure")] + ReadFailed(#[source] std::io::Error), + + #[error("control port stream closed by remote")] + ClosedByRemote(), + + #[error("received control port response invalid utf8")] + InvalidResponse(#[source] std::str::Utf8Error), + + #[error("failed to parse control port reply: {0}")] + ReplyParseFailed(String), + + #[error("control port stream write failure")] + WriteFailed(#[source] std::io::Error), } -#[macro_export] -macro_rules! ensure_not_null { - ($ptr:expr) => { - if $ptr.is_null() { - bail!(std::format!("`{}` must not be null", std::stringify!($ptr))); - } - }; +#[derive(thiserror::Error, Debug)] +pub enum TorVersionError { + #[error("{}", .0)] + ParseError(String), } -#[macro_export] -macro_rules! ensure_equal { - ($left:expr, $right:expr) => { - let left_val = $left; - let right_val = $right; - if left_val != right_val { - bail!(std::format!( - "`{}` must equal `{}` but found left: {:?}, right: {:?}", - std::stringify!($left), - std::stringify!($right), - left_val, - right_val - )); - } - }; +#[derive(thiserror::Error, Debug)] +pub enum TorControllerError { + #[error("response regex creation failed")] + ParsingRegexCreationFailed(#[source] regex::Error), + + #[error("control stream read reply failed")] + ReadReplyFailed(#[source] ControlStreamError), + + #[error("unexpected synchronous reply recieved")] + UnexpectedSynchonousReplyReceived(), + + #[error("control stream write command failed")] + WriteCommandFailed(#[source] ControlStreamError), + + #[error("invalid command arguments: {0}")] + InvalidCommandArguments(String), + + #[error("command failed: {0} {}", .1.join("\n"))] + CommandReturnedError(u32, Vec), + + #[error("failed to parse command reply: {0}")] + CommandReplyParseFailed(String), + + #[error("failed to parse received tor version")] + TorVersionParseFailed(#[source] TorVersionError), } -#[macro_export] -macro_rules! ensure_ok { - ($result:expr) => { - match $result { - Ok(_) => {} - Err(err) => bail!(err), - } - }; +#[derive(thiserror::Error, Debug)] +pub enum TorManagerError { + #[error("failed to create TorProcess object")] + TorProcessCreationFailed(#[source] TorProcessError), + + #[error("failed to create ControlStream object")] + ControlStreamCreationFailed(#[source] ControlStreamError), + + #[error("failed to create TorController object")] + TorControllerCreationFailed(#[source] TorControllerError), + + #[error("failed to authenticate with the tor process")] + TorProcessAuthenticationFailed(#[source] TorControllerError), + + #[error("failed to determine the tor process version")] + GetInfoVersionFailed(#[source] TorControllerError), + + #[error("tor process version to old; found {0} but must be at least {1}")] + TorProcessTooOld(String, String), + + #[error("failed to register for STATUS_CLIENT and HS_DESC events")] + SetEventsFailed(#[source] TorControllerError), + + #[error("failed to delete unused onion service")] + DelOnionFailed(#[source] TorControllerError), + + #[error("failed waiting for async events")] + WaitAsyncEventsFailed(#[source] TorControllerError), + + #[error("failed to begin bootstrap")] + SetConfDisableNetwork0Failed(#[source] TorControllerError), + + #[error("failed to add client auth for onion service")] + OnionClientAuthAddFailed(#[source] TorControllerError), + + #[error("failed to remove client auth from onion service")] + OnionClientAuthRemoveFailed(#[source] TorControllerError), + + #[error("failed to get socks listener")] + GetInfoNetListenersSocksFailed(#[source] TorControllerError), + + #[error("no socks listeners available to connect through")] + NoSocksListenersFound(), + + #[error("unable to connect to socks listener")] + Socks5ConnectionFailed(#[source] std::io::Error), + + #[error("unable to bind TCP listener")] + TcpListenerBindFailed(#[source] std::io::Error), + + #[error("unable to get TCP listener's local address")] + TcpListenerLocalAddrFailed(#[source] std::io::Error), + + #[error("faild to create onion service")] + AddOnionFailed(#[source] TorControllerError), } diff --git a/tor-interface/src/tor_controller.rs b/tor-interface/src/tor_controller.rs index 87dfc1b5c..7905f902f 100644 --- a/tor-interface/src/tor_controller.rs +++ b/tor-interface/src/tor_controller.rs @@ -28,10 +28,17 @@ use socks::Socks5Stream; use url::Host; // internal crates -use crate::error::Result; +use crate::error::ControlStreamError; +use crate::error::ControlStreamError::*; +use crate::error::TorControllerError; +use crate::error::TorControllerError::*; +use crate::error::TorManagerError; +use crate::error::TorManagerError::*; +use crate::error::TorProcessError; +use crate::error::TorProcessError::*; +use crate::error::TorVersionError; +use crate::error::TorVersionError::*; use crate::tor_crypto::*; -use crate::*; - // get the name of our tor executable pub const fn tor_exe_name() -> &'static str { if cfg!(windows) { @@ -52,30 +59,34 @@ fn generate_password(length: usize) -> String { password } -fn read_control_port_file(control_port_file: &Path) -> Result { +fn read_control_port_file(control_port_file: &Path) -> Result { // open file - let mut file = resolve!(File::open(control_port_file)); + let mut file = File::open(control_port_file).map_err(ControlPortFileReadFailed)?; // bail if the file is larger than expected - let metadata = resolve!(file.metadata()); - ensure!( - metadata.len() < 1024, - "control port file larger than expected: {} bytes", - metadata.len() - ); + let metadata = file.metadata().map_err(ControlPortFileReadFailed)?; + if metadata.len() >= 1024 { + return Err(ControlPortFileTooLarge( + format!("{}", control_port_file.display()), + metadata.len(), + )); + } // read contents to string let mut contents = String::new(); - resolve!(file.read_to_string(&mut contents)); + file.read_to_string(&mut contents) + .map_err(ControlPortFileReadFailed)?; if contents.starts_with("PORT=") { let addr_string = &contents.trim_end()["PORT=".len()..]; - return Ok(resolve!(SocketAddr::from_str(addr_string))); + if let Ok(addr) = SocketAddr::from_str(addr_string) { + return Ok(addr); + } } - bail!( - "could not parse '{}' as control port file", + Err(ControlPortFileContentsInvalid(format!( + "{}", control_port_file.display() - ); + ))) } // Encapsulates the tor daemon process @@ -88,20 +99,25 @@ struct TorProcess { } impl TorProcess { - // pub fn new(data_directory: &Path) -> Result { - pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { - ensure!(tor_bin_path.is_absolute()); - ensure!(data_directory.is_absolute()); + pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { + if tor_bin_path.is_relative() { + return Err(TorBinPathNotAbsolute(format!("{}", tor_bin_path.display()))); + } + if data_directory.is_relative() { + return Err(TorDataDirectoryPathNotAbsolute(format!( + "{}", + data_directory.display() + ))); + } // create data directory if it doesn't exist if !data_directory.exists() { - resolve!(fs::create_dir_all(data_directory)); - } else { - ensure!( - !data_directory.is_file(), - "received data_directory '{}' is a file not a path", + fs::create_dir_all(data_directory).map_err(DataDirectoryCreationFailed)?; + } else if data_directory.is_file() { + return Err(DataDirectoryPathExistsAsFile(format!( + "{}", data_directory.display() - ); + ))); } // construct paths to torrc files @@ -120,30 +136,28 @@ impl TorProcess { AvoidDiskWrites 1\n\ DisableNetwork 1\n\n"; - let mut default_torrc_file = resolve!(File::create(&default_torrc)); - resolve!(default_torrc_file.write_all(DEFAULT_TORRC_CONTENT.as_bytes())); + let mut default_torrc_file = + File::create(&default_torrc).map_err(DefaultTorrcFileCreationFailed)?; + default_torrc_file + .write_all(DEFAULT_TORRC_CONTENT.as_bytes()) + .map_err(DefaultTorrcFileWriteFailed)?; } // create empty torrc for user if !torrc.exists() { - let _ = File::create(&torrc); + let _ = File::create(&torrc).map_err(TorrcFileCreationFailed)?; } // remove any existing control_port_file if control_port_file.exists() { - ensure!( - control_port_file.is_file(), - "control port file '{}' exists but is a directory", - control_port_file.display() - ); - resolve!(fs::remove_file(&control_port_file)); + fs::remove_file(&control_port_file).map_err(ControlPortFileDeleteFailed)?; } const CONTROL_PORT_PASSWORD_LENGTH: usize = 32usize; let password = generate_password(CONTROL_PORT_PASSWORD_LENGTH); - let password_hash = hash_tor_password(&password)?; + let password_hash = hash_tor_password(&password); - let mut process = resolve!(Command::new(tor_bin_path.as_os_str()) + let mut process = Command::new(tor_bin_path.as_os_str()) .stdout(Stdio::piped()) .stdin(Stdio::null()) .stderr(Stdio::null()) @@ -171,7 +185,8 @@ impl TorProcess { // to avoid orphaned tor daemon .arg("__OwningControllerProcess") .arg(process::id().to_string()) - .spawn()); + .spawn() + .map_err(TorProcessStartFailed)?; let mut control_addr = None; let start = Instant::now(); @@ -182,16 +197,18 @@ impl TorProcess { while control_addr.is_none() && start.elapsed() < Duration::from_secs(5) { if control_port_file.exists() { control_addr = Some(read_control_port_file(control_port_file.as_path())?); - resolve!(fs::remove_file(&control_port_file)); + fs::remove_file(&control_port_file).map_err(ControlPortFileDeleteFailed)?; } } let control_addr = match control_addr { Some(control_addr) => control_addr, - None => bail!( - "failed to read control addr from '{}'", - control_port_file.display() - ), + None => { + return Err(ControlPortFileMissing(format!( + "{}", + control_port_file.display() + ))) + } }; let stdout_lines: Arc>> = Default::default(); @@ -200,14 +217,15 @@ impl TorProcess { let stdout_lines = Arc::downgrade(&stdout_lines); let stdout = BufReader::new(match process.stdout.take() { Some(stdout) => stdout, - None => unreachable!(), + None => return Err(TorProcessStdoutTakeFailed()), }); - resolve!(std::thread::Builder::new() + std::thread::Builder::new() .name("tor_stdout_reader".to_string()) .spawn(move || { TorProcess::read_stdout_task(&stdout_lines, stdout); - })); + }) + .map_err(StdoutReadThreadSpawnFailed)?; } Ok(TorProcess { @@ -273,22 +291,29 @@ struct Reply { } impl ControlStream { - pub fn new(addr: &SocketAddr, read_timeout: Duration) -> Result { - ensure!( - read_timeout != Duration::ZERO, - "read_timeout must not be zero" - ); + pub fn new( + addr: &SocketAddr, + read_timeout: Duration, + ) -> Result { + if read_timeout.is_zero() { + return Err(ReadTimeoutZero()); + } - let stream = resolve!(TcpStream::connect(addr)); - resolve!(stream.set_read_timeout(Some(read_timeout))); + let stream = TcpStream::connect(addr).map_err(CreationFailed)?; + stream + .set_read_timeout(Some(read_timeout)) + .map_err(ConfigurationFailed)?; // pre-allocate a kilobyte for the read buffer const READ_BUFFER_SIZE: usize = 1024; let pending_data = Vec::with_capacity(READ_BUFFER_SIZE); - let single_line_data = resolve!(Regex::new(r"^\d\d\d-.*")); - let multi_line_data = resolve!(Regex::new(r"^\d\d\d+.*")); - let end_reply_line = resolve!(Regex::new(r"^\d\d\d .*")); + let single_line_data = + Regex::new(r"^\d\d\d-.*").map_err(ControlStreamError::ParsingRegexCreationFailed)?; + let multi_line_data = + Regex::new(r"^\d\d\d+.*").map_err(ControlStreamError::ParsingRegexCreationFailed)?; + let end_reply_line = + Regex::new(r"^\d\d\d .*").map_err(ControlStreamError::ParsingRegexCreationFailed)?; Ok(ControlStream { stream, @@ -309,7 +334,7 @@ impl ControlStream { self.closed_by_remote } - fn read_line(&mut self) -> Result> { + fn read_line(&mut self) -> Result, ControlStreamError> { // read pending bytes from stream until we have a line to return while self.pending_lines.is_empty() { let byte_count = self.pending_data.len(); @@ -320,12 +345,12 @@ impl ControlStream { return Ok(None); } } else { - bail!(err); + return Err(ControlStreamError::ReadFailed(err)); } } Ok(0usize) => { self.closed_by_remote = true; - bail!("stream closed by remote") + return Err(ControlStreamError::ClosedByRemote()); } Ok(_count) => (), } @@ -338,10 +363,10 @@ impl ControlStream { // view into byte vec of just the found line let line_view: &[u8] = &self.pending_data[begin..end]; // convert to string - let line_string = resolve!(std::str::from_utf8(line_view)).to_string(); + let line_string = std::str::from_utf8(line_view).map_err(InvalidResponse)?; // save in pending list - self.pending_lines.push_back(line_string); + self.pending_lines.push_back(line_string.to_string()); // update begin (and skip over \r\n) begin = end + 2; } @@ -353,45 +378,73 @@ impl ControlStream { Ok(self.pending_lines.pop_front()) } - fn read_reply(&mut self) -> Result> { + fn read_reply(&mut self) -> Result, ControlStreamError> { loop { - let current_line = match self.read_line() { - Ok(Some(line)) => line, - Ok(None) => return Ok(None), - Err(err) => return Err(err), + let current_line = match self.read_line()? { + Some(line) => line, + None => return Ok(None), }; // make sure the status code matches (if we are not in the // middle of a multi-line read if let Some(first_line) = self.pending_reply.first() { if !self.reading_multiline_value { - ensure!(first_line[0..3] == current_line[0..3]); + let first_status_code = &first_line[0..3]; + let current_status_code = ¤t_line[0..3]; + if first_status_code != current_status_code { + return Err(ReplyParseFailed(format!( + "mismatched status codes, {} != {}", + first_status_code, current_status_code + ))); + } } } // end of a response if self.end_reply_line.is_match(¤t_line) { - ensure!(!self.reading_multiline_value); + if self.reading_multiline_value { + return Err(ReplyParseFailed( + "found multi-line end reply but not reading a multi-line reply".to_string(), + )); + } self.pending_reply.push(current_line); break; // single line data from getinfo and friends } else if self.single_line_data.is_match(¤t_line) { - ensure!(!self.reading_multiline_value); + if self.reading_multiline_value { + return Err(ReplyParseFailed( + "found single-line reply but still reading a multi-line reply".to_string(), + )); + } self.pending_reply.push(current_line); // begin of multiline data from getinfo and friends } else if self.multi_line_data.is_match(¤t_line) { - ensure!(!self.reading_multiline_value); + if self.reading_multiline_value { + return Err(ReplyParseFailed( + "found multi-line start reply but still reading a multi-line reply" + .to_string(), + )); + } self.pending_reply.push(current_line); self.reading_multiline_value = true; // multiline data to be squashed to a single entry } else { - ensure!(self.reading_multiline_value); + if !self.reading_multiline_value { + return Err(ReplyParseFailed( + "found a multi-line intermediate reply but not reading a multi-line reply" + .to_string(), + )); + } // don't bother writing the end of multiline token if current_line == "." { self.reading_multiline_value = false; } else { let multiline = match self.pending_reply.last_mut() { Some(multiline) => multiline, + // if our logic here is right, then + // self.reading_multiline_value == !self.pending_reply.is_empty() + // should always be true regardless of the data received + // from the control port None => unreachable!(), }; multiline.push('\n'); @@ -407,11 +460,20 @@ impl ControlStream { // parse out the response code for easier matching let status_code_string = match reply_lines.first() { Some(line) => line[0..3].to_string(), + // the lines have already been parsed+validated in the above loop None => unreachable!(), }; - let status_code: u32 = resolve!(status_code_string.parse()); + let status_code: u32 = match status_code_string.parse() { + Ok(status_code) => status_code, + Err(_) => { + return Err(ReplyParseFailed(format!( + "unable to parse '{}' as status code", + status_code_string + ))) + } + }; - // strip the redundant status code form start of lines + // strip the redundant status code from start of lines for line in reply_lines.iter_mut() { println!(">>> {}", line); if line.starts_with(&status_code_string) { @@ -425,11 +487,11 @@ impl ControlStream { })) } - pub fn write(&mut self, cmd: &str) -> Result<()> { + pub fn write(&mut self, cmd: &str) -> Result<(), ControlStreamError> { println!("<<< {}", cmd); if let Err(err) = write!(self.stream, "{}\r\n", cmd) { self.closed_by_remote = true; - bail!(err); + return Err(ControlStreamError::WriteFailed(err)); } Ok(()) } @@ -451,7 +513,8 @@ pub struct OnionClientAuthAddFlags { } // see version-spec.txt -pub struct Version { +#[derive(Clone)] +pub struct TorVersion { pub major: u32, pub minor: u32, pub micro: u32, @@ -459,7 +522,7 @@ pub struct Version { pub status_tag: Option, } -impl Version { +impl TorVersion { fn status_tag_pattern_is_match(status_tag: &str) -> bool { if status_tag.is_empty() { return false; @@ -479,15 +542,20 @@ impl Version { micro: u32, patch_level: Option, status_tag: Option<&str>, - ) -> Result { + ) -> Result { let status_tag = if let Some(status_tag) = status_tag { - ensure!(Self::status_tag_pattern_is_match(status_tag)); - Some(status_tag.to_string()) + if Self::status_tag_pattern_is_match(status_tag) { + Some(status_tag.to_string()) + } else { + return Err(TorVersionError::ParseError( + "tor version status tag may not be empty or contain white-space".to_string(), + )); + } } else { None }; - Ok(Version { + Ok(TorVersion { major, minor, micro, @@ -497,79 +565,106 @@ impl Version { } } -impl FromStr for Version { - type Err = error::Error; +impl FromStr for TorVersion { + type Err = TorVersionError; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { // MAJOR.MINOR.MICRO[.PATCHLEVEL][-STATUS_TAG][ (EXTRA_INFO)]* let mut tokens = s.split(' '); - let (major, minor, micro, patch_level, status_tag) = if let Some(version_status_tag) = - tokens.next() - { - let mut tokens = version_status_tag.split('-'); - let (major, minor, micro, patch_level) = if let Some(version) = tokens.next() { - let mut tokens = version.split('.'); - let major: u32 = if let Some(major) = tokens.next() { - match major.parse() { - Ok(major) => major, - Err(_) => bail!("failed to parse '{}' as MAJOR portion of version", major), - } - } else { - bail!("failed to find MAJOR portion of version"); - }; - let minor: u32 = if let Some(minor) = tokens.next() { - match minor.parse() { - Ok(minor) => minor, - Err(_) => bail!("failed to parse '{}' as MINOR portion of version", minor), - } - } else { - bail!("failed to find MINOR portion of version"); - }; - let micro: u32 = if let Some(micro) = tokens.next() { - match micro.parse() { - Ok(micro) => micro, - Err(_) => bail!("failed to parse '{}' as MICRO portion of version", micro), - } - } else { - bail!("failed to find MICRO portion of version"); - }; - let patch_level: u32 = if let Some(patch_level) = tokens.next() { - match patch_level.parse() { - Ok(patch_level) => patch_level, - Err(_) => bail!( - "failed to parse '{}' as PATCHLEVEL portion of version", - patch_level - ), - } + let (major, minor, micro, patch_level, status_tag) = + if let Some(version_status_tag) = tokens.next() { + let mut tokens = version_status_tag.split('-'); + let (major, minor, micro, patch_level) = if let Some(version) = tokens.next() { + let mut tokens = version.split('.'); + let major: u32 = if let Some(major) = tokens.next() { + match major.parse() { + Ok(major) => major, + Err(_) => { + return Err(ParseError(format!( + "failed to parse '{}' as MAJOR portion of tor version", + major + ))) + } + } + } else { + return Err(ParseError( + "failed to find MAJOR portion of tor version".to_string(), + )); + }; + let minor: u32 = if let Some(minor) = tokens.next() { + match minor.parse() { + Ok(minor) => minor, + Err(_) => { + return Err(ParseError(format!( + "failed to parse '{}' as MINOR portion of tor version", + minor + ))) + } + } + } else { + return Err(ParseError( + "failed to find MINOR portion of tor version".to_string(), + )); + }; + let micro: u32 = if let Some(micro) = tokens.next() { + match micro.parse() { + Ok(micro) => micro, + Err(_) => { + return Err(ParseError(format!( + "failed to parse '{}' as MICRO portion of tor version", + micro + ))) + } + } + } else { + return Err(ParseError( + "failed to find MICRO portion of tor version".to_string(), + )); + }; + let patch_level: u32 = if let Some(patch_level) = tokens.next() { + match patch_level.parse() { + Ok(patch_level) => patch_level, + Err(_) => { + return Err(ParseError(format!( + "failed to parse '{}' as PATCHLEVEL portion of tor version", + patch_level + ))) + } + } + } else { + 0u32 + }; + (major, minor, micro, patch_level) } else { - 0u32 + // if there were '-' the previous next() would have returned the enire string + unreachable!(); }; - (major, minor, micro, patch_level) + let status_tag = tokens.next().map(|status_tag| status_tag.to_string()); + + (major, minor, micro, patch_level, status_tag) } else { + // if there were no ' ' character the previou snext() would have returned the enire string unreachable!(); }; - let status_tag = tokens.next().map(|status_tag| status_tag.to_string()); - - (major, minor, micro, patch_level, status_tag) - } else { - bail!("failed to find MAJOR.MINOR.MICRO.[PATCH_LEVEL][-STATUS_TAG] portion of version"); - }; for extra_info in tokens { if !extra_info.starts_with('(') || !extra_info.ends_with(')') { - bail!("failed to parse '{}' as [ (EXTRA_INFO)]", extra_info); + return Err(ParseError(format!( + "failed to parse '{}' as [ (EXTRA_INFO)]", + extra_info + ))); } } - Ok(Version { + TorVersion::new( major, minor, micro, - patch_level, - status_tag, - }) + Some(patch_level), + status_tag.as_deref(), + ) } } -impl ToString for Version { +impl ToString for TorVersion { fn to_string(&self) -> String { match &self.status_tag { Some(status_tag) => format!( @@ -584,7 +679,7 @@ impl ToString for Version { } } -impl PartialEq for Version { +impl PartialEq for TorVersion { fn eq(&self, other: &Self) -> bool { self.major == other.major && self.minor == other.minor @@ -594,7 +689,7 @@ impl PartialEq for Version { } } -impl PartialOrd for Version { +impl PartialOrd for TorVersion { fn partial_cmp(&self, other: &Self) -> Option { if let Some(order) = self.major.partial_cmp(&other.major) { if order != Ordering::Equal { @@ -662,16 +757,16 @@ struct TorController { } impl TorController { - pub fn new(control_stream: ControlStream) -> Result { - let status_event_pattern = resolve!(Regex::new( - r#"^STATUS_CLIENT (?PNOTICE|WARN|ERR) (?P[A-Za-z]+)"# - )); - let status_event_argument_pattern = resolve!(Regex::new( - r#"(?P[A-Z]+)=(?P[A-Za-z0-9_]+|"[^"]+")"# - )); - let hs_desc_pattern = resolve!(Regex::new( + pub fn new(control_stream: ControlStream) -> Result { + let status_event_pattern = + Regex::new(r#"^STATUS_CLIENT (?PNOTICE|WARN|ERR) (?P[A-Za-z]+)"#) + .map_err(TorControllerError::ParsingRegexCreationFailed)?; + let status_event_argument_pattern = + Regex::new(r#"(?P[A-Z]+)=(?P[A-Za-z0-9_]+|"[^"]+")"#) + .map_err(TorControllerError::ParsingRegexCreationFailed)?; + let hs_desc_pattern = Regex::new( r#"HS_DESC (?PREQUESTED|UPLOAD|RECEIVED|UPLOADED|IGNORE|FAILED|CREATED) (?P[a-z2-7]{56})"# - )); + ).map_err(TorControllerError::ParsingRegexCreationFailed)?; Ok(TorController { control_stream, @@ -685,14 +780,14 @@ impl TorController { // return curently available events, does not block waiting // for an event - fn wait_async_replies(&mut self) -> Result> { + fn wait_async_replies(&mut self) -> Result, TorControllerError> { let mut replies: Vec = Default::default(); // take any previously received async replies std::mem::swap(&mut self.async_replies, &mut replies); // and keep consuming until none are available loop { - if let Some(reply) = self.control_stream.read_reply()? { + if let Some(reply) = self.control_stream.read_reply().map_err(ReadReplyFailed)? { replies.push(reply); } else { // no more replies immediately available so return @@ -701,11 +796,10 @@ impl TorController { } } - fn reply_to_event(&self, reply: &mut Reply) -> Result { - ensure!( - reply.status_code == 650u32, - "received unexpected synchrynous reply" - ); + fn reply_to_event(&self, reply: &mut Reply) -> Result { + if reply.status_code != 650u32 { + return Err(UnexpectedSynchonousReplyReceived()); + } // not sure this is what we want but yolo let reply_text = reply.reply_lines.join(" "); @@ -774,7 +868,7 @@ impl TorController { Ok(AsyncEvent::Unknown { lines: reply_lines }) } - pub fn wait_async_events(&mut self) -> Result> { + pub fn wait_async_events(&mut self) -> Result, TorControllerError> { let mut async_replies = self.wait_async_replies()?; let mut async_events: Vec = Default::default(); @@ -786,9 +880,9 @@ impl TorController { } // wait for a sync reply, save off async replies for later - fn wait_sync_reply(&mut self) -> Result { + fn wait_sync_reply(&mut self) -> Result { loop { - if let Some(reply) = self.control_stream.read_reply()? { + if let Some(reply) = self.control_stream.read_reply().map_err(ReadReplyFailed)? { match reply.status_code { 650u32 => self.async_replies.push(reply), _ => return Ok(reply), @@ -797,8 +891,10 @@ impl TorController { } } - fn write_command(&mut self, text: &str) -> Result { - self.control_stream.write(text)?; + fn write_command(&mut self, text: &str) -> Result { + self.control_stream + .write(text) + .map_err(WriteCommandFailed)?; self.wait_sync_reply() } @@ -812,8 +908,12 @@ impl TorController { // // SETCONF (3.1) - fn setconf_cmd(&mut self, key_values: &[(&str, &str)]) -> Result { - ensure!(!key_values.is_empty()); + fn setconf_cmd(&mut self, key_values: &[(&str, &str)]) -> Result { + if key_values.is_empty() { + return Err(TorControllerError::InvalidCommandArguments( + "SETCONF key-value pairs list must not be empty".to_string(), + )); + } let mut command_buffer = vec!["SETCONF".to_string()]; for (key, value) in key_values.iter() { @@ -826,31 +926,43 @@ impl TorController { // GETCONF (3.3) #[cfg(test)] - fn getconf_cmd(&mut self, keywords: &[&str]) -> Result { - ensure!(!keywords.is_empty()); + fn getconf_cmd(&mut self, keywords: &[&str]) -> Result { + if keywords.is_empty() { + return Err(TorControllerError::InvalidCommandArguments( + "GETCONF keywords list must not be empty".to_string(), + )); + } let command = format!("GETCONF {}", keywords.join(" ")); self.write_command(&command) } // SETEVENTS (3.4) - fn setevents_cmd(&mut self, event_codes: &[&str]) -> Result { - ensure!(!event_codes.is_empty()); + fn setevents_cmd(&mut self, event_codes: &[&str]) -> Result { + if event_codes.is_empty() { + return Err(TorControllerError::InvalidCommandArguments( + "SETEVENTS event codes list mut not be empty".to_string(), + )); + } let command = format!("SETEVENTS {}", event_codes.join(" ")); self.write_command(&command) } // AUTHENTICATE (3.5) - fn authenticate_cmd(&mut self, password: &str) -> Result { + fn authenticate_cmd(&mut self, password: &str) -> Result { let command = format!("AUTHENTICATE \"{}\"", password); self.write_command(&command) } // GETINFO (3.9) - fn getinfo_cmd(&mut self, keywords: &[&str]) -> Result { - ensure!(!keywords.is_empty()); + fn getinfo_cmd(&mut self, keywords: &[&str]) -> Result { + if keywords.is_empty() { + return Err(TorControllerError::InvalidCommandArguments( + "GETINFO keywords list must not be empty".to_string(), + )); + } let command = format!("GETINFO {}", keywords.join(" ")); self.write_command(&command) @@ -865,7 +977,7 @@ impl TorController { virt_port: u16, target: Option, client_auth: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { let mut command_buffer = vec!["ADD_ONION".to_string()]; // set our key or request a new one @@ -922,7 +1034,10 @@ impl TorController { } // DEL_ONION (3.38) - fn del_onion_cmd(&mut self, service_id: &V3OnionServiceId) -> Result { + fn del_onion_cmd( + &mut self, + service_id: &V3OnionServiceId, + ) -> Result { let command = format!("DEL_ONION {}", service_id.to_string()); self.write_command(&command) @@ -935,7 +1050,7 @@ impl TorController { private_key: &X25519PrivateKey, client_name: Option, flags: &OnionClientAuthAddFlags, - ) -> Result { + ) -> Result { let mut command_buffer = vec!["ONION_CLIENT_AUTH_ADD".to_string()]; // set the onion service id @@ -959,7 +1074,10 @@ impl TorController { } // ONION_CLIENT_AUTH_REMOVE (3.31) - fn onion_client_auth_remove_cmd(&mut self, service_id: &V3OnionServiceId) -> Result { + fn onion_client_auth_remove_cmd( + &mut self, + service_id: &V3OnionServiceId, + ) -> Result { let command = format!("ONION_CLIENT_AUTH_REMOVE {}", service_id.to_string()); self.write_command(&command) @@ -969,17 +1087,23 @@ impl TorController { // Public high-level typesafe command method wrappers // - pub fn setconf(&mut self, key_values: &[(&str, &str)]) -> Result<()> { + pub fn setconf(&mut self, key_values: &[(&str, &str)]) -> Result<(), TorControllerError> { let reply = self.setconf_cmd(key_values)?; match reply.status_code { 250u32 => Ok(()), - code => bail!("{} {}", code, reply.reply_lines.join("\n")), + code => Err(TorControllerError::CommandReturnedError( + code, + reply.reply_lines, + )), } } #[cfg(test)] - pub fn getconf(&mut self, keywords: &[&str]) -> Result> { + pub fn getconf( + &mut self, + keywords: &[&str], + ) -> Result, TorControllerError> { let reply = self.getconf_cmd(keywords)?; match reply.status_code { @@ -994,29 +1118,41 @@ impl TorController { } Ok(key_values) } - code => bail!("{} {}", code, reply.reply_lines.join("\n")), + code => Err(TorControllerError::CommandReturnedError( + code, + reply.reply_lines, + )), } } - pub fn setevents(&mut self, events: &[&str]) -> Result<()> { + pub fn setevents(&mut self, events: &[&str]) -> Result<(), TorControllerError> { let reply = self.setevents_cmd(events)?; match reply.status_code { 250u32 => Ok(()), - code => bail!("{} {}", code, reply.reply_lines.join("\n")), + code => Err(TorControllerError::CommandReturnedError( + code, + reply.reply_lines, + )), } } - pub fn authenticate(&mut self, password: &str) -> Result<()> { + pub fn authenticate(&mut self, password: &str) -> Result<(), TorControllerError> { let reply = self.authenticate_cmd(password)?; match reply.status_code { 250u32 => Ok(()), - code => bail!("{} {}", code, reply.reply_lines.join("\n")), + code => Err(TorControllerError::CommandReturnedError( + code, + reply.reply_lines, + )), } } - pub fn getinfo(&mut self, keywords: &[&str]) -> Result> { + pub fn getinfo( + &mut self, + keywords: &[&str], + ) -> Result, TorControllerError> { let reply = self.getinfo_cmd(keywords)?; match reply.status_code { @@ -1035,7 +1171,10 @@ impl TorController { } Ok(key_values) } - code => bail!("{} {}", code, reply.reply_lines.join("\n")), + code => Err(TorControllerError::CommandReturnedError( + code, + reply.reply_lines, + )), } } @@ -1047,7 +1186,7 @@ impl TorController { virt_port: u16, target: Option, client_auth: Option<&[X25519PublicKey]>, - ) -> Result<(Option, V3OnionServiceId)> { + ) -> Result<(Option, V3OnionServiceId), TorControllerError> { let reply = self.add_onion_cmd(key, flags, max_streams, virt_port, target, client_auth)?; let mut private_key: Option = None; @@ -1057,53 +1196,96 @@ impl TorController { 250u32 => { for line in reply.reply_lines { if let Some(mut index) = line.find("ServiceID=") { - ensure!(service_id.is_none(), "received duplicate service ids"); + if service_id.is_some() { + return Err(TorControllerError::CommandReplyParseFailed( + "received duplicate ServiceID entries".to_string(), + )); + } index += "ServiceId=".len(); - service_id = Some(V3OnionServiceId::from_string(&line[index..])?); + let service_id_string = &line[index..]; + service_id = match V3OnionServiceId::from_string(service_id_string) { + Ok(service_id) => Some(service_id), + Err(_) => { + return Err(TorControllerError::CommandReplyParseFailed(format!( + "could not parse '{}' as V3OnionServiceId", + service_id_string + ))) + } + } } else if let Some(mut index) = line.find("PrivateKey=") { - ensure!(private_key.is_none(), "received duplicate private keys"); + if private_key.is_some() { + return Err(TorControllerError::CommandReplyParseFailed( + "received duplicate PrivateKey entries".to_string(), + )); + } index += "PrivateKey=".len(); - private_key = Some(Ed25519PrivateKey::from_key_blob(&line[index..])?); + let key_blob_string = &line[index..]; + private_key = match Ed25519PrivateKey::from_key_blob(key_blob_string) { + Ok(private_key) => Some(private_key), + Err(_) => { + return Err(TorControllerError::CommandReplyParseFailed(format!( + "could not parse {} as Ed25519PrivateKey", + key_blob_string + ))) + } + }; } else if line.contains("ClientAuthV3=") { - ensure!( - !client_auth.unwrap_or_default().is_empty(), - "received unexpected ClientAuthV3 keys" - ); + if client_auth.unwrap_or_default().is_empty() { + return Err(TorControllerError::CommandReplyParseFailed( + "recieved unexpected ClientAuthV3 keys".to_string(), + )); + } } else if !line.contains("OK") { - bail!("received unexpected reply line: '{}'", line); + return Err(TorControllerError::CommandReplyParseFailed(format!( + "received unexpected reply line '{}'", + line + ))); } } } - code => bail!("{} {}", code, reply.reply_lines.join("\n")), + code => { + return Err(TorControllerError::CommandReturnedError( + code, + reply.reply_lines, + )) + } } if flags.discard_pk { - ensure!( - private_key.is_none(), - "private key should have been discarded" - ); - } else { - ensure!(private_key.is_some(), "did not return private key"); + if private_key.is_some() { + return Err(TorControllerError::CommandReplyParseFailed( + "PrivateKey response should have been discard".to_string(), + )); + } + } else if private_key.is_none() { + return Err(TorControllerError::CommandReplyParseFailed( + "did not receive a PrivateKey".to_string(), + )); } match service_id { Some(service_id) => Ok((private_key, service_id)), - None => bail!("did not receive a serviceid"), + None => Err(TorControllerError::CommandReplyParseFailed( + "did not receive a ServiceID".to_string(), + )), } } - pub fn del_onion(&mut self, service_id: &V3OnionServiceId) -> Result<()> { + pub fn del_onion(&mut self, service_id: &V3OnionServiceId) -> Result<(), TorControllerError> { let reply = self.del_onion_cmd(service_id)?; match reply.status_code { 250u32 => Ok(()), - code => bail!("{} {}", code, reply.reply_lines.join("\n")), + code => Err(TorControllerError::CommandReturnedError( + code, + reply.reply_lines, + )), } } // more specific encapulsation of specific command invocations - pub fn getinfo_net_listeners_socks(&mut self) -> Result> { + pub fn getinfo_net_listeners_socks(&mut self) -> Result, TorControllerError> { let response = self.getinfo(&["net/listeners/socks"])?; for (key, value) in response.iter() { if key.as_str() == "net/listeners/socks" { @@ -1114,26 +1296,43 @@ impl TorController { let listeners: Vec<&str> = value.split(' ').collect(); let mut result: Vec = Default::default(); for socket_addr in listeners.iter() { - ensure!(socket_addr.starts_with('\"') && socket_addr.ends_with('\"')); + if !socket_addr.starts_with('\"') || !socket_addr.ends_with('\"') { + return Err(TorControllerError::CommandReplyParseFailed(format!( + "could not parse '{}' as socket address", + socket_addr + ))); + } // remove leading/trailing double quote let stripped = &socket_addr[1..socket_addr.len() - 1]; - result.push(resolve!(SocketAddr::from_str(stripped))); + result.push(match SocketAddr::from_str(stripped) { + Ok(result) => result, + Err(_) => { + return Err(TorControllerError::CommandReplyParseFailed(format!( + "could not parse '{}' as socket address", + socket_addr + ))) + } + }); } return Ok(result); } } - bail!("did not find a 'net/listeners/socks' key/value"); + Err(TorControllerError::CommandReplyParseFailed( + "reply did not find a 'net/listeners/socks' key/value".to_string(), + )) } - pub fn getinfo_version(&mut self) -> Result { + pub fn getinfo_version(&mut self) -> Result { let response = self.getinfo(&["version"])?; for (key, value) in response.iter() { if key.as_str() == "version" { - return Version::from_str(value); + return TorVersion::from_str(value).map_err(TorVersionParseFailed); } } - bail!("did not find a 'version' key/value"); + Err(TorControllerError::CommandReplyParseFailed( + "did not find a 'version' key/value".to_string(), + )) } pub fn onion_client_auth_add( @@ -1142,22 +1341,31 @@ impl TorController { private_key: &X25519PrivateKey, client_name: Option, flags: &OnionClientAuthAddFlags, - ) -> Result<()> { + ) -> Result<(), TorControllerError> { let reply = self.onion_client_auth_add_cmd(service_id, private_key, client_name, flags)?; match reply.status_code { 250u32..=252u32 => Ok(()), - code => bail!("{} {}", code, reply.reply_lines.join("\n")), + code => Err(TorControllerError::CommandReturnedError( + code, + reply.reply_lines, + )), } } #[allow(dead_code)] - pub fn onion_client_auth_remove(&mut self, service_id: &V3OnionServiceId) -> Result<()> { + pub fn onion_client_auth_remove( + &mut self, + service_id: &V3OnionServiceId, + ) -> Result<(), TorControllerError> { let reply = self.onion_client_auth_remove_cmd(service_id)?; match reply.status_code { 250u32..=251u32 => Ok(()), - code => bail!("{} {}", code, reply.reply_lines.join("\n")), + code => Err(TorControllerError::CommandReturnedError( + code, + reply.reply_lines, + )), } } } @@ -1224,9 +1432,9 @@ impl OnionStream { self.stream.write_timeout() } - pub fn try_clone(&self) -> Result { + pub fn try_clone(&self) -> Result { Ok(OnionStream { - stream: resolve!(self.stream.try_clone()), + stream: self.stream.try_clone()?, peer_addr: self.peer_addr.clone(), }) } @@ -1262,12 +1470,11 @@ pub struct OnionListener { } impl OnionListener { - pub fn set_nonblocking(&self, nonblocking: bool) -> Result<()> { - resolve!(self.listener.set_nonblocking(nonblocking)); - Ok(()) + pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { + self.listener.set_nonblocking(nonblocking) } - pub fn accept(&self) -> Result> { + pub fn accept(&self) -> Result, std::io::Error> { match self.listener.accept() { Ok((stream, _socket_addr)) => Ok(Some(OnionStream { stream, @@ -1277,7 +1484,7 @@ impl OnionListener { if err.kind() == ErrorKind::WouldBlock { Ok(None) } else { - bail!(err); + Err(err) } } } @@ -1307,62 +1514,85 @@ pub enum Event { pub struct TorManager { daemon: TorProcess, + version: TorVersion, controller: TorController, socks_listener: Option, - // list of open onion services their is_active flag + // list of open onion services and their is_active flag onion_services: Vec<(V3OnionServiceId, Arc)>, } impl TorManager { - pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { + pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { // launch tor - let daemon = TorProcess::new(tor_bin_path, data_directory)?; + let daemon = + TorProcess::new(tor_bin_path, data_directory).map_err(TorProcessCreationFailed)?; // open a control stream - let control_stream = ControlStream::new(&daemon.control_addr, Duration::from_millis(16))?; + let control_stream = ControlStream::new(&daemon.control_addr, Duration::from_millis(16)) + .map_err(ControlStreamCreationFailed)?; // create a controler - let mut controller = TorController::new(control_stream)?; + let mut controller = + TorController::new(control_stream).map_err(TorControllerCreationFailed)?; // authenticate - controller.authenticate(&daemon.password)?; - - let min_required_version: Version = Version::new(0u32, 4u32, 6u32, Some(1u32), None)?; + controller + .authenticate(&daemon.password) + .map_err(TorProcessAuthenticationFailed)?; + + let min_required_version: TorVersion = TorVersion { + major: 0u32, + minor: 4u32, + micro: 6u32, + patch_level: 1u32, + status_tag: None, + }; - let version = controller.getinfo_version()?; + let version = controller.getinfo_version().map_err(GetInfoVersionFailed)?; - ensure!( - version >= min_required_version, - "tor daemon not new enough; must be at least version {}", - min_required_version.to_string() - ); + if version < min_required_version { + return Err(TorManagerError::TorProcessTooOld( + version.to_string(), + min_required_version.to_string(), + )); + } // register for STATUS_CLIENT async events - controller.setevents(&["STATUS_CLIENT", "HS_DESC"])?; + controller + .setevents(&["STATUS_CLIENT", "HS_DESC"]) + .map_err(SetEventsFailed)?; Ok(TorManager { daemon, + version, controller, socks_listener: None, onion_services: Default::default(), }) } - pub fn update(&mut self) -> Result> { + pub fn update(&mut self) -> Result, TorManagerError> { let mut i = 0; while i < self.onion_services.len() { + // remove onion services with no active listeners if !self.onion_services[i].1.load(atomic::Ordering::Relaxed) { let entry = self.onion_services.swap_remove(i); let service_id = entry.0; - println!("deleting {}", service_id.to_string()); - self.controller.del_onion(&service_id)?; + self.controller + .del_onion(&service_id) + .map_err(DelOnionFailed)?; } else { i += 1; } } let mut events: Vec = Default::default(); - for async_event in self.controller.wait_async_events()?.iter() { + for async_event in self + .controller + .wait_async_events() + .map_err(WaitAsyncEventsFailed)? + .iter() + { match async_event { AsyncEvent::StatusClient { severity, @@ -1375,7 +1605,7 @@ impl TorManager { let mut summary: String = Default::default(); for (key, val) in arguments.iter() { match key.as_str() { - "PROGRESS" => progress = resolve!(val.parse()), + "PROGRESS" => progress = val.parse().unwrap_or(0u32), "TAG" => tag = val.to_string(), "SUMMARY" => summary = val.to_string(), _ => {} // ignore unexpected arguments @@ -1417,26 +1647,33 @@ impl TorManager { } #[allow(dead_code)] - pub fn version(&mut self) -> Result { - self.controller.getinfo_version() + pub fn version(&mut self) -> TorVersion { + self.version.clone() } - pub fn bootstrap(&mut self) -> Result<()> { - self.controller.setconf(&[("DisableNetwork", "0")]) + pub fn bootstrap(&mut self) -> Result<(), TorManagerError> { + self.controller + .setconf(&[("DisableNetwork", "0")]) + .map_err(SetConfDisableNetwork0Failed) } pub fn add_client_auth( &mut self, service_id: &V3OnionServiceId, client_auth: &X25519PrivateKey, - ) -> Result<()> { + ) -> Result<(), TorManagerError> { self.controller .onion_client_auth_add(service_id, client_auth, None, &Default::default()) + .map_err(SetConfDisableNetwork0Failed) } - #[allow(dead_code)] - pub fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<()> { - self.controller.onion_client_auth_remove(service_id) + pub fn remove_client_auth( + &mut self, + service_id: &V3OnionServiceId, + ) -> Result<(), TorManagerError> { + self.controller + .onion_client_auth_remove(service_id) + .map_err(OnionClientAuthRemoveFailed) } // connect to an onion service and returns OnionStream @@ -1445,13 +1682,15 @@ impl TorManager { service_id: &V3OnionServiceId, virt_port: u16, circuit: Option, - ) -> Result { + ) -> Result { if self.socks_listener.is_none() { - let mut listeners = self.controller.getinfo_net_listeners_socks()?; - ensure!( - !listeners.is_empty(), - "no available socks listener to connect through" - ); + let mut listeners = self + .controller + .getinfo_net_listeners_socks() + .map_err(GetInfoNetListenersSocksFailed)?; + if listeners.is_empty() { + return Err(TorManagerError::NoSocksListenersFound()); + } self.socks_listener = Some(listeners.swap_remove(0)); } @@ -1465,14 +1704,15 @@ impl TorManager { socks::TargetAddr::Domain(format!("{}.onion", service_id.to_string()), virt_port); // readwrite stream let stream = match &circuit { - None => resolve!(Socks5Stream::connect(socks_listener, target)), - Some(circuit) => resolve!(Socks5Stream::connect_with_password( + None => Socks5Stream::connect(socks_listener, target), + Some(circuit) => Socks5Stream::connect_with_password( socks_listener, target, &circuit.username, - &circuit.password - )), - }; + &circuit.password, + ), + } + .map_err(Socks5ConnectionFailed)?; Ok(OnionStream { stream: stream.into_inner(), @@ -1486,11 +1726,11 @@ impl TorManager { private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { // try to bind to a local address, let OS pick our port let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); - let listener = resolve!(TcpListener::bind(socket_addr)); - let socket_addr = resolve!(listener.local_addr()); + let listener = TcpListener::bind(socket_addr).map_err(TcpListenerBindFailed)?; + let socket_addr = listener.local_addr().map_err(TcpListenerLocalAddrFailed)?; let mut flags = AddOnionFlags { discard_pk: true, @@ -1501,14 +1741,17 @@ impl TorManager { } // start onion service - let (_, service_id) = self.controller.add_onion( - Some(private_key), - &flags, - None, - virt_port, - Some(socket_addr), - authorized_clients, - )?; + let (_, service_id) = self + .controller + .add_onion( + Some(private_key), + &flags, + None, + virt_port, + Some(socket_addr), + authorized_clients, + ) + .map_err(AddOnionFailed)?; let is_active = Arc::new(atomic::AtomicBool::new(true)); self.onion_services @@ -1523,8 +1766,8 @@ impl TorManager { #[test] #[serial] -fn test_tor_controller() -> Result<()> { - let tor_path = resolve!(which::which(tor_exe_name())); +fn test_tor_controller() -> anyhow::Result<()> { + let tor_path = which::which(tor_exe_name())?; let mut data_path = std::env::temp_dir(); data_path.push("test_tor_controller"); let tor_process = TorProcess::new(&tor_path, &data_path)?; @@ -1537,7 +1780,7 @@ fn test_tor_controller() -> Result<()> { // create a tor controller and send authentication command let mut tor_controller = TorController::new(control_stream)?; tor_controller.authenticate_cmd(&tor_process.password)?; - ensure!( + assert!( tor_controller .authenticate_cmd("invalid password")? .status_code @@ -1545,13 +1788,13 @@ fn test_tor_controller() -> Result<()> { ); // tor controller should have shutdown the connection after failed authentication - if tor_controller - .authenticate_cmd(&tor_process.password) - .is_ok() - { - bail!("expected failure due to closed connection"); - } - ensure!(tor_controller.control_stream.closed_by_remote()); + assert!( + tor_controller + .authenticate_cmd(&tor_process.password) + .is_err(), + "expected failure due to closed connection" + ); + assert!(tor_controller.control_stream.closed_by_remote()); } // now create a second controller { @@ -1570,9 +1813,9 @@ fn test_tor_controller() -> Result<()> { "SocksPort" => "auto OnionTrafficOnly", "AvoidDiskWrites" => "1", "DisableNetwork" => "1", - _ => bail!("unexpected returned key: {}", key), + _ => panic!("unexpected returned key: {}", key), }; - ensure!(value == expected); + assert!(value == expected); } let vals = tor_controller.getinfo(&["version", "config-file", "config-text"])?; @@ -1582,9 +1825,9 @@ fn test_tor_controller() -> Result<()> { expected_control_port_path.push("control_port"); for (key, value) in vals.iter() { match key.as_str() { - "version" => ensure!(resolve!(Regex::new(r"\d+\.\d+\.\d+\.\d+")).is_match(&value)), - "config-file" => ensure!(Path::new(&value) == expected_torrc_path), - "config-text" => ensure!( + "version" => assert!(Regex::new(r"\d+\.\d+\.\d+\.\d+")?.is_match(&value)), + "config-file" => assert!(Path::new(&value) == expected_torrc_path), + "config-text" => assert!( value.to_string() == format!( "\nControlPort auto\nControlPortWriteToFile {}\nDataDirectory {}", @@ -1592,7 +1835,7 @@ fn test_tor_controller() -> Result<()> { data_path.display() ) ), - _ => bail!("unexpected returned key: {}", key), + _ => panic!("unexpected returned key: {}", key), } } @@ -1604,34 +1847,31 @@ fn test_tor_controller() -> Result<()> { let (private_key, service_id) = match tor_controller.add_onion(None, &Default::default(), None, 22, None, None)? { (Some(private_key), service_id) => (private_key, service_id), - _ => bail!("add_onion did not return expected values"), + _ => panic!("add_onion did not return expected values"), }; println!("private_key: {}", private_key.to_key_blob()); println!("service_id: {}", service_id.to_string()); - if let Ok(()) = tor_controller.del_onion(&V3OnionServiceId::from_string( - "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd", - )?) { - bail!("deleting unknown onion should have failed"); - } + assert!( + tor_controller + .del_onion(&V3OnionServiceId::from_string( + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd" + )?) + .is_err(), + "deleting unknown onion should have failed" + ); // delete our new onion tor_controller.del_onion(&service_id)?; - if let Ok(listeners) = tor_controller.getinfo_net_listeners_socks() { - println!("listeners: "); - for sock_addr in listeners.iter() { - println!(" {}", sock_addr); - } + println!("listeners: "); + for sock_addr in tor_controller.getinfo_net_listeners_socks()?.iter() { + println!(" {}", sock_addr); } - tor_controller.getinfo_net_listeners_socks()?; - // print our event names available to tor - if let Ok(names) = tor_controller.getinfo(&["events/names"]) { - for (key, value) in names.iter() { - println!("{} : {}", key, value); - } + for (key, value) in tor_controller.getinfo(&["events/names"])?.iter() { + println!("{} : {}", key, value); } let stop_time = Instant::now() + std::time::Duration::from_secs(5); @@ -1667,89 +1907,61 @@ fn test_tor_controller() -> Result<()> { } #[test] -fn test_version() -> Result<()> { - ensure!(Version::from_str("1.2.3")? == Version::new(1, 2, 3, None, None)?); - ensure!(Version::from_str("1.2.3.4")? == Version::new(1, 2, 3, Some(4), None)?); - ensure!(Version::from_str("1.2.3-test")? == Version::new(1, 2, 3, None, Some("test"))?); - ensure!(Version::from_str("1.2.3.4-test")? == Version::new(1, 2, 3, Some(4), Some("test"))?); - ensure!(Version::from_str("1.2.3 (extra_info)")? == Version::new(1, 2, 3, None, None)?); - ensure!(Version::from_str("1.2.3.4 (extra_info)")? == Version::new(1, 2, 3, Some(4), None)?); - ensure!( - Version::from_str("1.2.3.4-tag (extra_info)")? - == Version::new(1, 2, 3, Some(4), Some("tag"))? +fn test_version() -> anyhow::Result<()> { + assert!(TorVersion::from_str("1.2.3")? == TorVersion::new(1, 2, 3, None, None)?); + assert!(TorVersion::from_str("1.2.3.4")? == TorVersion::new(1, 2, 3, Some(4), None)?); + assert!(TorVersion::from_str("1.2.3-test")? == TorVersion::new(1, 2, 3, None, Some("test"))?); + assert!( + TorVersion::from_str("1.2.3.4-test")? == TorVersion::new(1, 2, 3, Some(4), Some("test"))? ); - - ensure!( - Version::from_str("1.2.3.4-tag (extra_info) (extra_info)")? - == Version::new(1, 2, 3, Some(4), Some("tag"))? + assert!(TorVersion::from_str("1.2.3 (extra_info)")? == TorVersion::new(1, 2, 3, None, None)?); + assert!( + TorVersion::from_str("1.2.3.4 (extra_info)")? == TorVersion::new(1, 2, 3, Some(4), None)? + ); + assert!( + TorVersion::from_str("1.2.3.4-tag (extra_info)")? + == TorVersion::new(1, 2, 3, Some(4), Some("tag"))? ); - match Version::new(1, 2, 3, Some(4), Some("spaced tag")) { - Ok(_) => bail!("expected failure"), - Err(err) => println!("{:?}", err), - } - - match Version::new(1, 2, 3, Some(4), Some("" /* empty tag */)) { - Ok(_) => bail!("expected failure"), - Err(err) => println!("{:?}", err), - } - - match Version::from_str("") { - Ok(_) => bail!("expected failure"), - Err(err) => println!("{:?}", err), - } - - match Version::from_str("1.2") { - Ok(_) => bail!("expected failure"), - Err(err) => println!("{:?}", err), - } - - match Version::from_str("1.2-foo") { - Ok(_) => bail!("expected failure"), - Err(err) => println!("{:?}", err), - } - - match Version::from_str("1.2.3.4-foo bar") { - Ok(_) => bail!("expected failure"), - Err(err) => println!("{:?}", err), - } - - match Version::from_str("1.2.3.4-foo bar (extra_info)") { - Ok(_) => bail!("expected failure"), - Err(err) => println!("{:?}", err), - } - - match Version::from_str("1.2.3.4-foo (extra_info) badtext") { - Ok(_) => bail!("expected failure"), - Err(err) => println!("{:?}", err), - } + assert!( + TorVersion::from_str("1.2.3.4-tag (extra_info) (extra_info)")? + == TorVersion::new(1, 2, 3, Some(4), Some("tag"))? + ); - ensure!(Version::new(0, 0, 0, Some(0), None)? < Version::new(1, 0, 0, Some(0), None)?); - ensure!(Version::new(0, 0, 0, Some(0), None)? < Version::new(0, 1, 0, Some(0), None)?); - ensure!(Version::new(0, 0, 0, Some(0), None)? < Version::new(0, 0, 1, Some(0), None)?); + assert!(TorVersion::new(1, 2, 3, Some(4), Some("spaced tag")).is_err()); + assert!(TorVersion::new(1, 2, 3, Some(4), Some("" /* empty tag */)).is_err()); + assert!(TorVersion::from_str("").is_err()); + assert!(TorVersion::from_str("1.2").is_err()); + assert!(TorVersion::from_str("1.2-foo").is_err()); + assert!(TorVersion::from_str("1.2.3.4-foo bar").is_err()); + assert!(TorVersion::from_str("1.2.3.4-foo bar (extra_info)").is_err()); + assert!(TorVersion::from_str("1.2.3.4-foo (extra_info) badtext").is_err()); + assert!(TorVersion::new(0, 0, 0, Some(0), None)? < TorVersion::new(1, 0, 0, Some(0), None)?); + assert!(TorVersion::new(0, 0, 0, Some(0), None)? < TorVersion::new(0, 1, 0, Some(0), None)?); + assert!(TorVersion::new(0, 0, 0, Some(0), None)? < TorVersion::new(0, 0, 1, Some(0), None)?); // ensure status tags make comparison between equal versions (apart from // tags) unknowable - let zero_version = Version::new(0, 0, 0, Some(0), None)?; - let zero_version_tag = Version::new(0, 0, 0, Some(0), Some("tag"))?; + let zero_version = TorVersion::new(0, 0, 0, Some(0), None)?; + let zero_version_tag = TorVersion::new(0, 0, 0, Some(0), Some("tag"))?; - ensure!(!(zero_version < zero_version_tag)); - ensure!(!(zero_version <= zero_version_tag)); - ensure!(!(zero_version > zero_version_tag)); - ensure!(!(zero_version >= zero_version_tag)); + assert!(!(zero_version < zero_version_tag)); + assert!(!(zero_version <= zero_version_tag)); + assert!(!(zero_version > zero_version_tag)); + assert!(!(zero_version >= zero_version_tag)); Ok(()) } #[test] #[serial] -fn test_tor_manager() -> Result<()> { - let tor_path = resolve!(which::which(tor_exe_name())); +fn test_tor_manager() -> anyhow::Result<()> { + let tor_path = which::which(tor_exe_name())?; let mut data_path = std::env::temp_dir(); data_path.push("test_tor_manager"); let mut tor = TorManager::new(&tor_path, &data_path)?; - println!("version : {}", tor.version()?.to_string()); + println!("version : {}", tor.version().to_string()); tor.bootstrap()?; let mut received_log = false; @@ -1777,7 +1989,7 @@ fn test_tor_manager() -> Result<()> { } } } - ensure!( + assert!( received_log, "should have received a log line from tor daemon" ); @@ -1787,8 +1999,8 @@ fn test_tor_manager() -> Result<()> { #[test] #[serial] -fn test_onion_service() -> Result<()> { - let tor_path = resolve!(which::which(tor_exe_name())); +fn test_onion_service() -> anyhow::Result<()> { + let tor_path = which::which(tor_exe_name())?; let mut data_path = std::env::temp_dir(); data_path.push("test_onion_service"); @@ -1857,21 +2069,21 @@ fn test_onion_service() -> Result<()> { println!("Connecting to onion service"); let mut client = tor.connect(&service_id, VIRT_PORT, None)?; println!("Client writing message: '{}'", MESSAGE); - resolve!(client.write_all(MESSAGE.as_bytes())); - resolve!(client.flush()); + client.write_all(MESSAGE.as_bytes())?; + client.flush()?; println!("End of client scope"); } if let Some(mut server) = listener.accept()? { println!("Server reading message"); let mut buffer = Vec::new(); - resolve!(server.read_to_end(&mut buffer)); - let msg = resolve!(String::from_utf8(buffer)); + server.read_to_end(&mut buffer)?; + let msg = String::from_utf8(buffer)?; - ensure!(MESSAGE == msg); + assert!(MESSAGE == msg); println!("Message received: '{}'", msg); } else { - bail!("no listener"); + panic!("no listener"); } } @@ -1915,11 +2127,10 @@ fn test_onion_service() -> Result<()> { let service_id = V3OnionServiceId::from_private_key(&private_key); println!("Connecting to onion service (should fail)"); - if tor.connect(&service_id, VIRT_PORT, None).is_ok() { - bail!( - "should not able to connect to an authenticated onion service without auth key" - ); - } + assert!( + tor.connect(&service_id, VIRT_PORT, None).is_err(), + "should not able to connect to an authenticated onion service without auth key" + ); println!("Add auth key for onion service"); tor.add_client_auth(&service_id, &private_auth_key)?; @@ -1928,8 +2139,8 @@ fn test_onion_service() -> Result<()> { let mut client = tor.connect(&service_id, VIRT_PORT, None)?; println!("Client writing message: '{}'", MESSAGE); - resolve!(client.write_all(MESSAGE.as_bytes())); - resolve!(client.flush()); + client.write_all(MESSAGE.as_bytes())?; + client.flush()?; println!("End of client scope"); println!("Remove auth key for onion service"); @@ -1939,13 +2150,13 @@ fn test_onion_service() -> Result<()> { if let Some(mut server) = listener.accept()? { println!("Server reading message"); let mut buffer = Vec::new(); - resolve!(server.read_to_end(&mut buffer)); - let msg = resolve!(String::from_utf8(buffer)); + server.read_to_end(&mut buffer)?; + let msg = String::from_utf8(buffer)?; - ensure!(MESSAGE == msg); + assert!(MESSAGE == msg); println!("Message received: '{}'", msg); } else { - bail!("no listener"); + panic!("no listener"); } } Ok(()) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index f6e398c43..7d716fc6f 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -16,8 +16,7 @@ use tor_llcrypto::util::rand_compat::RngCompatExt; use tor_llcrypto::*; // internal modules -use crate::error::Result; -use crate::*; +use crate::error::TorCryptoError; /// The number of bytes in an ed25519 secret key /// cbindgen:ignore @@ -96,16 +95,8 @@ fn calc_truncated_checksum( // Free functions -fn hash_tor_password_with_salt( - salt: &[u8; S2K_RFC2440_SPECIFIER_LEN], - password: &str, -) -> Result { - if salt[S2K_RFC2440_SPECIFIER_LEN - 1] != 0x60 { - bail!( - "last byte in salt must be '0x60', received '{:#02X}'", - salt[S2K_RFC2440_SPECIFIER_LEN - 1] - ); - } +fn hash_tor_password_with_salt(salt: &[u8; S2K_RFC2440_SPECIFIER_LEN], password: &str) -> String { + assert!(salt[S2K_RFC2440_SPECIFIER_LEN - 1] == 0x60); // tor-specific rfc 2440 constants const EXPBIAS: u8 = 6u8; @@ -141,10 +132,10 @@ fn hash_tor_password_with_salt( HEXUPPER.encode_append(salt, &mut hash); HEXUPPER.encode_append(&key, &mut hash); - Ok(hash) + hash } -pub fn hash_tor_password(password: &str) -> Result { +pub fn hash_tor_password(password: &str) -> String { let mut salt = [0x00u8; S2K_RFC2440_SPECIFIER_LEN]; OsRng.fill_bytes(&mut salt); salt[S2K_RFC2440_SPECIFIER_LEN - 1] = 0x60u8; @@ -204,51 +195,64 @@ impl Ed25519PrivateKey { } } - pub fn from_key_blob(key_blob: &str) -> Result { + pub fn from_key_blob(key_blob: &str) -> Result { if key_blob.len() != ED25519_PRIVATE_KEYBLOB_LENGTH { - bail!( + return Err(TorCryptoError::ParseError(format!( "expects string of length '{}'; received string with length '{}'", ED25519_PRIVATE_KEYBLOB_LENGTH, key_blob.len() - ); + ))); } if !key_blob.starts_with(ED25519_PRIVATE_KEYBLOB_HEADER) { - bail!( + return Err(TorCryptoError::ParseError(format!( "expects string that begins with '{}'; received '{}'", - &ED25519_PRIVATE_KEYBLOB_HEADER, - &key_blob - ); + &ED25519_PRIVATE_KEYBLOB_HEADER, &key_blob + ))); } let base64_key: &str = &key_blob[ED25519_PRIVATE_KEYBLOB_HEADER.len()..]; - let private_key_data = resolve!(BASE64.decode(base64_key.as_bytes())); + let private_key_data = match BASE64.decode(base64_key.as_bytes()) { + Ok(private_key_data) => private_key_data, + Err(_) => { + return Err(TorCryptoError::ParseError(format!( + "could not parse '{}' as base64", + base64_key + ))) + } + }; let private_key_data_len = private_key_data.len(); let private_key_data_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = match private_key_data.try_into() { Ok(private_key_data) => private_key_data, - Err(_) => bail!( - "expects decoded private key length '{}'; actual '{}'", - ED25519_PRIVATE_KEY_SIZE, - private_key_data_len - ), + Err(_) => { + return Err(TorCryptoError::ParseError(format!( + "expects decoded private key length '{}'; actual '{}'", + ED25519_PRIVATE_KEY_SIZE, private_key_data_len + ))) + } }; Ok(Ed25519PrivateKey::from_raw(&private_key_data_raw)) } - fn from_private_x25519(x25519_private: &X25519PrivateKey) -> Result<(Ed25519PrivateKey, u8)> { + fn from_private_x25519( + x25519_private: &X25519PrivateKey, + ) -> Result<(Ed25519PrivateKey, u8), TorCryptoError> { if let Some((result, signbit)) = convert_curve25519_to_ed25519_private(&x25519_private.secret_key) { - return Ok(( + Ok(( Ed25519PrivateKey { expanded_secret_key: result, }, signbit, - )); + )) + } else { + Err(TorCryptoError::ConversionError( + "could not convert x25519 private key to ed25519 private key".to_string(), + )) } - bail!("couldn't convert key"); } pub fn to_key_blob(&self) -> String { @@ -294,36 +298,48 @@ impl Clone for Ed25519PrivateKey { // Ed25519 Public Key impl Ed25519PublicKey { - pub fn from_raw(raw: &[u8; ED25519_PUBLIC_KEY_SIZE]) -> Result { + pub fn from_raw( + raw: &[u8; ED25519_PUBLIC_KEY_SIZE], + ) -> Result { Ok(Ed25519PublicKey { - public_key: resolve!(pk::ed25519::PublicKey::from_bytes(raw)), + public_key: match pk::ed25519::PublicKey::from_bytes(raw) { + Ok(public_key) => public_key, + Err(_) => { + return Err(TorCryptoError::ConversionError( + "failed to create ed25519 public key from bytes".to_string(), + )) + } + }, }) } - pub fn from_service_id(service_id: &V3OnionServiceId) -> Result { + pub fn from_service_id( + service_id: &V3OnionServiceId, + ) -> Result { // decode base32 encoded service id let mut decoded_service_id = [0u8; V3_ONION_SERVICE_ID_RAW_SIZE]; let decoded_byte_count = match ONION_BASE32.decode_mut(service_id.as_bytes(), &mut decoded_service_id) { Ok(decoded_byte_count) => decoded_byte_count, - Err(_) => bail!( - "failed to decode '{}' as V3OnionServiceId", - service_id.to_string() - ), + Err(_) => { + return Err(TorCryptoError::ConversionError(format!( + "failed to decode '{}' as V3OnionServiceId", + service_id.to_string() + ))) + } }; if decoded_byte_count != V3_ONION_SERVICE_ID_RAW_SIZE { - bail!( + return Err(TorCryptoError::ConversionError(format!( "decoded byte count is '{}', expected '{}'", - decoded_byte_count, - V3_ONION_SERVICE_ID_RAW_SIZE - ); + decoded_byte_count, V3_ONION_SERVICE_ID_RAW_SIZE + ))); } - Ok(Ed25519PublicKey { - public_key: resolve!(pk::ed25519::PublicKey::from_bytes( - &decoded_service_id[0..ED25519_PUBLIC_KEY_SIZE] - )), - }) + Ed25519PublicKey::from_raw( + decoded_service_id[0..ED25519_PUBLIC_KEY_SIZE] + .try_into() + .unwrap(), + ) } pub fn from_private_key(private_key: &Ed25519PrivateKey) -> Ed25519PublicKey { @@ -335,17 +351,21 @@ impl Ed25519PublicKey { fn from_public_x25519( public_x25519: &X25519PublicKey, signbit: u8, - ) -> Result { - ensure!(signbit == 0u8 || signbit == 1u8); - Ok(Ed25519PublicKey { - public_key: match convert_curve25519_to_ed25519_public( - &public_x25519.public_key, - signbit, - ) { - Some(public_key) => public_key, - None => bail!("failed to convert public key"), - }, - }) + ) -> Result { + if signbit != 0u8 && signbit != 1u8 { + Err(TorCryptoError::ConversionError(format!( + "invalid value of signbit, must be 0 or 1 but found {}", + signbit + ))) + } else { + match convert_curve25519_to_ed25519_public(&public_x25519.public_key, signbit) { + Some(public_key) => Ok(Ed25519PublicKey { public_key }), + None => Err(TorCryptoError::ConversionError( + "failed to create ed25519 public key from x25519 public key and signbit" + .to_string(), + )), + } + } } pub fn to_base32(&self) -> String { @@ -366,9 +386,18 @@ impl PartialEq for Ed25519PublicKey { // Ed25519 Signature impl Ed25519Signature { - pub fn from_raw(raw: &[u8; ED25519_SIGNATURE_SIZE]) -> Result { + pub fn from_raw( + raw: &[u8; ED25519_SIGNATURE_SIZE], + ) -> Result { Ok(Ed25519Signature { - signature: resolve!(pk::ed25519::Signature::from_bytes(raw)), + signature: match pk::ed25519::Signature::from_bytes(raw) { + Ok(signature) => signature, + Err(_) => { + return Err(TorCryptoError::ConversionError( + "failed to create ed25519 signature from bytes".to_string(), + )) + } + }, }) } @@ -419,20 +448,34 @@ impl X25519PrivateKey { } // a base64 encoded keyblob - pub fn from_base64(base64: &str) -> Result { - ensure!(base64.len() == X25519_PRIVATE_KEYBLOB_BASE64_LENGTH, - "X25519PrivateKey::from_base64(): expects string of length '{}'; received string with length '{}'", X25519_PRIVATE_KEYBLOB_BASE64_LENGTH, base64.len()); + pub fn from_base64(base64: &str) -> Result { + if base64.len() != X25519_PRIVATE_KEYBLOB_BASE64_LENGTH { + return Err(TorCryptoError::ParseError(format!( + "expects string of length '{}'; received string with length '{}'", + X25519_PRIVATE_KEYBLOB_BASE64_LENGTH, + base64.len() + ))); + } - let private_key_data = resolve!(BASE64.decode(base64.as_bytes())); + let private_key_data = match BASE64.decode(base64.as_bytes()) { + Ok(private_key_data) => private_key_data, + Err(_) => { + return Err(TorCryptoError::ParseError(format!( + "could not parse '{}' as base64", + base64 + ))) + } + }; let private_key_data_len = private_key_data.len(); let private_key_data_raw: [u8; X25519_PRIVATE_KEY_SIZE] = match private_key_data.try_into() { Ok(private_key_data) => private_key_data, - Err(_) => bail!( - "expects decoded private key length '{}'; actual '{}'", - X25519_PRIVATE_KEY_SIZE, - private_key_data_len - ), + Err(_) => { + return Err(TorCryptoError::ParseError(format!( + "expects decoded private key length '{}'; actual '{}'", + X25519_PRIVATE_KEY_SIZE, private_key_data_len + ))) + } }; Ok(X25519PrivateKey::from_raw(&private_key_data_raw)) @@ -442,7 +485,7 @@ impl X25519PrivateKey { // this function first derives an ed25519 private key from the provided x25519 private key // and signs the message, returning the signature and signbit needed to calculate the // ed25519 public key from our x25519 private key's associated x25519 public key - pub fn sign_message(&self, message: &[u8]) -> Result<(Ed25519Signature, u8)> { + pub fn sign_message(&self, message: &[u8]) -> Result<(Ed25519Signature, u8), TorCryptoError> { let (ed25519_private, signbit) = Ed25519PrivateKey::from_private_x25519(self)?; Ok((ed25519_private.sign_message(message), signbit)) } @@ -470,19 +513,34 @@ impl X25519PublicKey { } } - pub fn from_base32(base32: &str) -> Result { - ensure!(base32.len() == X25519_PUBLIC_KEYBLOB_BASE32_LENGTH, - "X25519PublicKey::from_base32(): expects string of length '{}'; received '{}' with length '{}'", X25519_PUBLIC_KEYBLOB_BASE32_LENGTH, base32, base32.len()); + pub fn from_base32(base32: &str) -> Result { + if base32.len() != X25519_PUBLIC_KEYBLOB_BASE32_LENGTH { + return Err(TorCryptoError::ParseError(format!( + "expects string of length '{}'; received '{}' with length '{}'", + X25519_PUBLIC_KEYBLOB_BASE32_LENGTH, + base32, + base32.len() + ))); + } - let public_key_data = resolve!(BASE32_NOPAD.decode(base32.as_bytes())); + let public_key_data = match BASE32_NOPAD.decode(base32.as_bytes()) { + Ok(public_key_data) => public_key_data, + Err(_) => { + return Err(TorCryptoError::ParseError(format!( + "failed to decode '{}' as X25519PublicKey", + base32 + ))) + } + }; let public_key_data_len = public_key_data.len(); let public_key_data_raw: [u8; X25519_PUBLIC_KEY_SIZE] = match public_key_data.try_into() { Ok(public_key_data) => public_key_data, - Err(_) => bail!( - "expects decoded public key length '{}'; actual '{}'", - X25519_PUBLIC_KEY_SIZE, - public_key_data_len - ), + Err(_) => { + return Err(TorCryptoError::ParseError(format!( + "expects decoded public key length '{}'; actual '{}'", + X25519_PUBLIC_KEY_SIZE, public_key_data_len + ))) + } }; Ok(X25519PublicKey::from_raw(&public_key_data_raw)) @@ -500,12 +558,15 @@ impl X25519PublicKey { // Onion Service Id impl V3OnionServiceId { - pub fn from_string(service_id: &str) -> Result { + pub fn from_string(service_id: &str) -> Result { if !V3OnionServiceId::is_valid(service_id) { - bail!("'{}' is not a valid v3 onion service id", &service_id); + return Err(TorCryptoError::ParseError(format!( + "'{}' is not a valid v3 onion service id", + service_id + ))); } Ok(V3OnionServiceId { - data: resolve!(service_id.as_bytes().try_into()), + data: service_id.as_bytes().try_into().unwrap(), }) } @@ -580,7 +641,7 @@ impl std::fmt::Debug for V3OnionServiceId { } #[test] -fn test_ed25519() -> Result<()> { +fn test_ed25519() -> Result<(), anyhow::Error> { let private_key_blob = "ED25519-V3:YE3GZtDmc+izGijWKgeVRabbXqK456JKKGONDBhV+kPBVKa2mHVQqnRTVuFXe3inU3YW6qvc7glYEwe9rK0LhQ=="; let private_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = [ 0x60u8, 0x4du8, 0xc6u8, 0x66u8, 0xd0u8, 0xe6u8, 0x73u8, 0xe8u8, 0xb3u8, 0x1au8, 0x28u8, @@ -651,27 +712,27 @@ fn test_ed25519() -> Result<()> { } #[test] -fn test_password_hash() -> Result<()> { +fn test_password_hash() -> Result<(), anyhow::Error> { let salt1: [u8; S2K_RFC2440_SPECIFIER_LEN] = [ 0xbeu8, 0x2au8, 0x25u8, 0x1du8, 0xe6u8, 0x2cu8, 0xb2u8, 0x7au8, 0x60u8, ]; - let hash1 = hash_tor_password_with_salt(&salt1, "abcdefghijklmnopqrstuvwxyz")?; + let hash1 = hash_tor_password_with_salt(&salt1, "abcdefghijklmnopqrstuvwxyz"); assert!(hash1 == "16:BE2A251DE62CB27A60AC9178A937990E8ED0AB662FA82A5C7DE3EBB23A"); let salt2: [u8; S2K_RFC2440_SPECIFIER_LEN] = [ 0x36u8, 0x73u8, 0x0eu8, 0xefu8, 0xd1u8, 0x8cu8, 0x60u8, 0xd6u8, 0x60u8, ]; - let hash2 = hash_tor_password_with_salt(&salt2, "password")?; + let hash2 = hash_tor_password_with_salt(&salt2, "password"); assert!(hash2 == "16:36730EEFD18C60D66052E7EA535438761C0928D316EEA56A190C99B50A"); // ensure same password is hashed to different things - assert!(hash_tor_password("password")? != hash_tor_password("password")?); + assert!(hash_tor_password("password") != hash_tor_password("password")); Ok(()) } #[test] -fn test_x25519() -> Result<()> { +fn test_x25519() -> Result<(), anyhow::Error> { // private/public key pair const SECRET_BASE64: &str = "0GeSReJXdNcgvWRQdnDXhJGdu5UiwP2fefgT93/oqn0="; const SECRET_RAW: [u8; X25519_PRIVATE_KEY_SIZE] = [ @@ -687,22 +748,22 @@ fn test_x25519() -> Result<()> { ]; // ensure we can convert from raw as expected - ensure!(&X25519PrivateKey::from_raw(&SECRET_RAW).to_base64() == SECRET_BASE64); - ensure!(&X25519PublicKey::from_raw(&PUBLIC_RAW).to_base32() == PUBLIC_BASE32); + assert!(&X25519PrivateKey::from_raw(&SECRET_RAW).to_base64() == SECRET_BASE64); + assert!(&X25519PublicKey::from_raw(&PUBLIC_RAW).to_base32() == PUBLIC_BASE32); // ensure we can round-trip as expected - ensure!(&X25519PrivateKey::from_base64(&SECRET_BASE64)?.to_base64() == SECRET_BASE64); - ensure!(&X25519PublicKey::from_base32(&PUBLIC_BASE32)?.to_base32() == PUBLIC_BASE32); + assert!(&X25519PrivateKey::from_base64(&SECRET_BASE64)?.to_base64() == SECRET_BASE64); + assert!(&X25519PublicKey::from_base32(&PUBLIC_BASE32)?.to_base32() == PUBLIC_BASE32); // ensure we generate the expected public key from private key let private_key = X25519PrivateKey::from_base64(&SECRET_BASE64)?; let public_key = X25519PublicKey::from_private_key(&private_key); - ensure!(public_key.to_base32() == PUBLIC_BASE32); + assert!(public_key.to_base32() == PUBLIC_BASE32); let message = b"All around me are familiar faces"; let (signature, signbit) = private_key.sign_message(message)?; - ensure!(signature.verify_x25519(message, &public_key, signbit)); + assert!(signature.verify_x25519(message, &public_key, signbit)); Ok(()) } From 6d2b3fe4949b96936b93163a54cbc9e885930a7f Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 26 May 2023 20:49:22 +0000 Subject: [PATCH 003/184] tor_crypto: made signbit an actual enum type rather than using a u8 --- tor-interface/src/tor_crypto.rs | 64 +++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 7d716fc6f..3832b2016 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -174,6 +174,20 @@ pub struct V3OnionServiceId { data: [u8; V3_ONION_SERVICE_ID_LENGTH], } +pub enum SignBit { + Zero, + One, +} + +impl From for u8 { + fn from(signbit: SignBit) -> Self { + match signbit { + SignBit::Zero => 0u8, + SignBit::One => 1u8, + } + } +} + // Ed25519 Private Key impl Ed25519PrivateKey { @@ -238,7 +252,7 @@ impl Ed25519PrivateKey { fn from_private_x25519( x25519_private: &X25519PrivateKey, - ) -> Result<(Ed25519PrivateKey, u8), TorCryptoError> { + ) -> Result<(Ed25519PrivateKey, SignBit), TorCryptoError> { if let Some((result, signbit)) = convert_curve25519_to_ed25519_private(&x25519_private.secret_key) { @@ -246,7 +260,16 @@ impl Ed25519PrivateKey { Ed25519PrivateKey { expanded_secret_key: result, }, - signbit, + match signbit { + 0u8 => SignBit::Zero, + 1u8 => SignBit::One, + invalid_signbit => { + return Err(TorCryptoError::ConversionError(format!( + "convert_curve25519_to_ed25519_private() returned invalid signbit: {}", + invalid_signbit + ))) + } + }, )) } else { Err(TorCryptoError::ConversionError( @@ -350,21 +373,14 @@ impl Ed25519PublicKey { fn from_public_x25519( public_x25519: &X25519PublicKey, - signbit: u8, + signbit: SignBit, ) -> Result { - if signbit != 0u8 && signbit != 1u8 { - Err(TorCryptoError::ConversionError(format!( - "invalid value of signbit, must be 0 or 1 but found {}", - signbit - ))) - } else { - match convert_curve25519_to_ed25519_public(&public_x25519.public_key, signbit) { - Some(public_key) => Ok(Ed25519PublicKey { public_key }), - None => Err(TorCryptoError::ConversionError( - "failed to create ed25519 public key from x25519 public key and signbit" - .to_string(), - )), - } + match convert_curve25519_to_ed25519_public(&public_x25519.public_key, signbit.into()) { + Some(public_key) => Ok(Ed25519PublicKey { public_key }), + None => Err(TorCryptoError::ConversionError( + "failed to create ed25519 public key from x25519 public key and signbit" + .to_string(), + )), } } @@ -410,11 +426,12 @@ impl Ed25519Signature { // derives an ed25519 public key from the provided x25519 public key and signbit, then // verifies this signature using said ed25519 public key - pub fn verify_x25519(&self, message: &[u8], public_key: &X25519PublicKey, signbit: u8) -> bool { - if signbit != 0u8 && signbit != 1u8 { - return false; - } - + pub fn verify_x25519( + &self, + message: &[u8], + public_key: &X25519PublicKey, + signbit: SignBit, + ) -> bool { if let Ok(public_key) = Ed25519PublicKey::from_public_x25519(public_key, signbit) { return self.verify(message, &public_key); } @@ -485,7 +502,10 @@ impl X25519PrivateKey { // this function first derives an ed25519 private key from the provided x25519 private key // and signs the message, returning the signature and signbit needed to calculate the // ed25519 public key from our x25519 private key's associated x25519 public key - pub fn sign_message(&self, message: &[u8]) -> Result<(Ed25519Signature, u8), TorCryptoError> { + pub fn sign_message( + &self, + message: &[u8], + ) -> Result<(Ed25519Signature, SignBit), TorCryptoError> { let (ed25519_private, signbit) = Ed25519PrivateKey::from_private_x25519(self)?; Ok((ed25519_private.sign_message(message), signbit)) } From eb2286790efaf0f04ee8fee7ceb227cb22538c32 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 28 May 2023 19:44:24 +0000 Subject: [PATCH 004/184] tor-interface: added various fmt::Debug and From trait implementations --- tor-interface/src/tor_crypto.rs | 44 ++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 3832b2016..5f52e7774 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -174,6 +174,7 @@ pub struct V3OnionServiceId { data: [u8; V3_ONION_SERVICE_ID_LENGTH], } +#[derive(Clone, Copy)] pub enum SignBit { Zero, One, @@ -188,6 +189,25 @@ impl From for u8 { } } +impl From for bool { + fn from(signbit: SignBit) -> Self { + match signbit { + SignBit::Zero => false, + SignBit::One => true, + } + } +} + +impl From for SignBit { + fn from(signbit: bool) -> Self { + if signbit { + SignBit::One + } else { + SignBit::Zero + } + } +} + // Ed25519 Private Key impl Ed25519PrivateKey { @@ -250,7 +270,7 @@ impl Ed25519PrivateKey { Ok(Ed25519PrivateKey::from_raw(&private_key_data_raw)) } - fn from_private_x25519( + pub fn from_private_x25519( x25519_private: &X25519PrivateKey, ) -> Result<(Ed25519PrivateKey, SignBit), TorCryptoError> { if let Some((result, signbit)) = @@ -318,6 +338,12 @@ impl Clone for Ed25519PrivateKey { } } +impl std::fmt::Debug for Ed25519PrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "--- ed25519 private key ---") + } +} + // Ed25519 Public Key impl Ed25519PublicKey { @@ -519,6 +545,12 @@ impl X25519PrivateKey { } } +impl std::fmt::Debug for X25519PrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "--- x25519 private key ---") + } +} + // X25519 Public Key impl X25519PublicKey { pub fn from_private_key(private_key: &X25519PrivateKey) -> X25519PublicKey { @@ -570,11 +602,21 @@ impl X25519PublicKey { BASE32_NOPAD.encode(self.public_key.as_bytes()) } + pub fn to_string(&self) -> String { + self.to_base32() + } + pub fn as_bytes(&self) -> &[u8; X25519_PUBLIC_KEY_SIZE] { self.public_key.as_bytes() } } +impl std::fmt::Debug for X25519PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_string()) + } +} + // Onion Service Id impl V3OnionServiceId { From 3551c7f968596253d094de6f135783b30a893170 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 29 May 2023 00:20:10 +0000 Subject: [PATCH 005/184] tor-interface: migrated various structs into their own modules --- tor-interface/src/error.rs | 175 --- tor-interface/src/lib.rs | 5 +- tor-interface/src/tor_control_stream.rs | 265 ++++ tor-interface/src/tor_controller.rs | 1555 ++--------------------- tor-interface/src/tor_crypto.rs | 88 +- tor-interface/src/tor_manager.rs | 685 ++++++++++ tor-interface/src/tor_process.rs | 297 +++++ tor-interface/src/tor_version.rs | 276 ++++ 8 files changed, 1678 insertions(+), 1668 deletions(-) delete mode 100644 tor-interface/src/error.rs create mode 100644 tor-interface/src/tor_control_stream.rs create mode 100644 tor-interface/src/tor_manager.rs create mode 100644 tor-interface/src/tor_process.rs create mode 100644 tor-interface/src/tor_version.rs diff --git a/tor-interface/src/error.rs b/tor-interface/src/error.rs deleted file mode 100644 index 756b2523a..000000000 --- a/tor-interface/src/error.rs +++ /dev/null @@ -1,175 +0,0 @@ -#[derive(thiserror::Error, Debug)] -pub enum TorCryptoError { - #[error("{0}")] - ParseError(String), - #[error("{0}")] - ConversionError(String), -} - -#[derive(thiserror::Error, Debug)] -pub enum TorProcessError { - #[error("failed to read control port file")] - ControlPortFileReadFailed(#[source] std::io::Error), - - #[error("provided control port file '{0}' larger than expected ({1} bytes)")] - ControlPortFileTooLarge(String, u64), - - #[error("failed to parse '{0}' as control port file")] - ControlPortFileContentsInvalid(String), - - #[error("provided tor bin path '{0}' must be an absolute path")] - TorBinPathNotAbsolute(String), - - #[error("provided data directory '{0}' must be an absolute path")] - TorDataDirectoryPathNotAbsolute(String), - - #[error("failed to create data directory")] - DataDirectoryCreationFailed(#[source] std::io::Error), - - #[error("file exists in provided data directory path '{0}'")] - DataDirectoryPathExistsAsFile(String), - - #[error("failed to create default_torrc file")] - DefaultTorrcFileCreationFailed(#[source] std::io::Error), - - #[error("failed to write default_torrc file")] - DefaultTorrcFileWriteFailed(#[source] std::io::Error), - - #[error("failed to create torrc file")] - TorrcFileCreationFailed(#[source] std::io::Error), - - #[error("failed to remove control_port file")] - ControlPortFileDeleteFailed(#[source] std::io::Error), - - #[error("failed to start tor process")] - TorProcessStartFailed(#[source] std::io::Error), - - #[error("failed to read control addr from control_file '{0}'")] - ControlPortFileMissing(String), - - #[error("unable to take tor process stdout")] - TorProcessStdoutTakeFailed(), - - #[error("failed to spawn tor process stdout read thread")] - StdoutReadThreadSpawnFailed(#[source] std::io::Error), -} - -#[derive(thiserror::Error, Debug)] -pub enum ControlStreamError { - #[error("control stream read timeout must not be zero")] - ReadTimeoutZero(), - - #[error("could not connect to control port")] - CreationFailed(#[source] std::io::Error), - - #[error("configure control port socket failed")] - ConfigurationFailed(#[source] std::io::Error), - - #[error("control port parsing regex creation failed")] - ParsingRegexCreationFailed(#[source] regex::Error), - - #[error("control port stream read failure")] - ReadFailed(#[source] std::io::Error), - - #[error("control port stream closed by remote")] - ClosedByRemote(), - - #[error("received control port response invalid utf8")] - InvalidResponse(#[source] std::str::Utf8Error), - - #[error("failed to parse control port reply: {0}")] - ReplyParseFailed(String), - - #[error("control port stream write failure")] - WriteFailed(#[source] std::io::Error), -} - -#[derive(thiserror::Error, Debug)] -pub enum TorVersionError { - #[error("{}", .0)] - ParseError(String), -} - -#[derive(thiserror::Error, Debug)] -pub enum TorControllerError { - #[error("response regex creation failed")] - ParsingRegexCreationFailed(#[source] regex::Error), - - #[error("control stream read reply failed")] - ReadReplyFailed(#[source] ControlStreamError), - - #[error("unexpected synchronous reply recieved")] - UnexpectedSynchonousReplyReceived(), - - #[error("control stream write command failed")] - WriteCommandFailed(#[source] ControlStreamError), - - #[error("invalid command arguments: {0}")] - InvalidCommandArguments(String), - - #[error("command failed: {0} {}", .1.join("\n"))] - CommandReturnedError(u32, Vec), - - #[error("failed to parse command reply: {0}")] - CommandReplyParseFailed(String), - - #[error("failed to parse received tor version")] - TorVersionParseFailed(#[source] TorVersionError), -} - -#[derive(thiserror::Error, Debug)] -pub enum TorManagerError { - #[error("failed to create TorProcess object")] - TorProcessCreationFailed(#[source] TorProcessError), - - #[error("failed to create ControlStream object")] - ControlStreamCreationFailed(#[source] ControlStreamError), - - #[error("failed to create TorController object")] - TorControllerCreationFailed(#[source] TorControllerError), - - #[error("failed to authenticate with the tor process")] - TorProcessAuthenticationFailed(#[source] TorControllerError), - - #[error("failed to determine the tor process version")] - GetInfoVersionFailed(#[source] TorControllerError), - - #[error("tor process version to old; found {0} but must be at least {1}")] - TorProcessTooOld(String, String), - - #[error("failed to register for STATUS_CLIENT and HS_DESC events")] - SetEventsFailed(#[source] TorControllerError), - - #[error("failed to delete unused onion service")] - DelOnionFailed(#[source] TorControllerError), - - #[error("failed waiting for async events")] - WaitAsyncEventsFailed(#[source] TorControllerError), - - #[error("failed to begin bootstrap")] - SetConfDisableNetwork0Failed(#[source] TorControllerError), - - #[error("failed to add client auth for onion service")] - OnionClientAuthAddFailed(#[source] TorControllerError), - - #[error("failed to remove client auth from onion service")] - OnionClientAuthRemoveFailed(#[source] TorControllerError), - - #[error("failed to get socks listener")] - GetInfoNetListenersSocksFailed(#[source] TorControllerError), - - #[error("no socks listeners available to connect through")] - NoSocksListenersFound(), - - #[error("unable to connect to socks listener")] - Socks5ConnectionFailed(#[source] std::io::Error), - - #[error("unable to bind TCP listener")] - TcpListenerBindFailed(#[source] std::io::Error), - - #[error("unable to get TCP listener's local address")] - TcpListenerLocalAddrFailed(#[source] std::io::Error), - - #[error("faild to create onion service")] - AddOnionFailed(#[source] TorControllerError), -} diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index 6357dba9e..a9a61637b 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -1,3 +1,6 @@ -pub mod error; +pub mod tor_control_stream; pub mod tor_controller; pub mod tor_crypto; +pub mod tor_manager; +pub mod tor_process; +pub mod tor_version; diff --git a/tor-interface/src/tor_control_stream.rs b/tor-interface/src/tor_control_stream.rs new file mode 100644 index 000000000..b091480fc --- /dev/null +++ b/tor-interface/src/tor_control_stream.rs @@ -0,0 +1,265 @@ +// standard +use std::collections::VecDeque; +use std::default::Default; +use std::io::{ErrorKind, Read, Write}; +use std::net::{SocketAddr, TcpStream}; +use std::option::Option; +use std::string::ToString; +use std::time::Duration; + +// extern crates +use regex::Regex; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("control stream read timeout must not be zero")] + ReadTimeoutZero(), + + #[error("could not connect to control port")] + CreationFailed(#[source] std::io::Error), + + #[error("configure control port socket failed")] + ConfigurationFailed(#[source] std::io::Error), + + #[error("control port parsing regex creation failed")] + ParsingRegexCreationFailed(#[source] regex::Error), + + #[error("control port stream read failure")] + ReadFailed(#[source] std::io::Error), + + #[error("control port stream closed by remote")] + ClosedByRemote(), + + #[error("received control port response invalid utf8")] + InvalidResponse(#[source] std::str::Utf8Error), + + #[error("failed to parse control port reply: {0}")] + ReplyParseFailed(String), + + #[error("control port stream write failure")] + WriteFailed(#[source] std::io::Error), +} + +pub(crate) struct ControlStream { + stream: TcpStream, + closed_by_remote: bool, + pending_data: Vec, + pending_lines: VecDeque, + pending_reply: Vec, + reading_multiline_value: bool, + // regexes used to parse control port responses + single_line_data: Regex, + multi_line_data: Regex, + end_reply_line: Regex, +} + +type StatusCode = u32; +pub(crate) struct Reply { + pub status_code: StatusCode, + pub reply_lines: Vec, +} + +impl ControlStream { + pub fn new(addr: &SocketAddr, read_timeout: Duration) -> Result { + if read_timeout.is_zero() { + return Err(Error::ReadTimeoutZero()); + } + + let stream = TcpStream::connect(addr).map_err(Error::CreationFailed)?; + stream + .set_read_timeout(Some(read_timeout)) + .map_err(Error::ConfigurationFailed)?; + + // pre-allocate a kilobyte for the read buffer + const READ_BUFFER_SIZE: usize = 1024; + let pending_data = Vec::with_capacity(READ_BUFFER_SIZE); + + let single_line_data = + Regex::new(r"^\d\d\d-.*").map_err(Error::ParsingRegexCreationFailed)?; + let multi_line_data = + Regex::new(r"^\d\d\d+.*").map_err(Error::ParsingRegexCreationFailed)?; + let end_reply_line = + Regex::new(r"^\d\d\d .*").map_err(Error::ParsingRegexCreationFailed)?; + + Ok(ControlStream { + stream, + closed_by_remote: false, + pending_data, + pending_lines: Default::default(), + pending_reply: Default::default(), + reading_multiline_value: false, + // regex + single_line_data, + multi_line_data, + end_reply_line, + }) + } + + #[cfg(test)] + pub(crate) fn closed_by_remote(&mut self) -> bool { + self.closed_by_remote + } + + fn read_line(&mut self) -> Result, Error> { + // read pending bytes from stream until we have a line to return + while self.pending_lines.is_empty() { + let byte_count = self.pending_data.len(); + match self.stream.read_to_end(&mut self.pending_data) { + Err(err) => { + if err.kind() == ErrorKind::WouldBlock || err.kind() == ErrorKind::TimedOut { + if byte_count == self.pending_data.len() { + return Ok(None); + } + } else { + return Err(Error::ReadFailed(err)); + } + } + Ok(0usize) => { + self.closed_by_remote = true; + return Err(Error::ClosedByRemote()); + } + Ok(_count) => (), + } + + // split our read buffer into individual lines + let mut begin = 0; + for index in 1..self.pending_data.len() { + if self.pending_data[index - 1] == b'\r' && self.pending_data[index] == b'\n' { + let end = index - 1; + // view into byte vec of just the found line + let line_view: &[u8] = &self.pending_data[begin..end]; + // convert to string + let line_string = + std::str::from_utf8(line_view).map_err(Error::InvalidResponse)?; + + // save in pending list + self.pending_lines.push_back(line_string.to_string()); + // update begin (and skip over \r\n) + begin = end + 2; + } + } + // leave any leftover bytes in the buffer for the next call + self.pending_data.drain(0..begin); + } + + Ok(self.pending_lines.pop_front()) + } + + pub fn read_reply(&mut self) -> Result, Error> { + loop { + let current_line = match self.read_line()? { + Some(line) => line, + None => return Ok(None), + }; + + // make sure the status code matches (if we are not in the + // middle of a multi-line read + if let Some(first_line) = self.pending_reply.first() { + if !self.reading_multiline_value { + let first_status_code = &first_line[0..3]; + let current_status_code = ¤t_line[0..3]; + if first_status_code != current_status_code { + return Err(Error::ReplyParseFailed(format!( + "mismatched status codes, {} != {}", + first_status_code, current_status_code + ))); + } + } + } + + // end of a response + if self.end_reply_line.is_match(¤t_line) { + if self.reading_multiline_value { + return Err(Error::ReplyParseFailed( + "found multi-line end reply but not reading a multi-line reply".to_string(), + )); + } + self.pending_reply.push(current_line); + break; + // single line data from getinfo and friends + } else if self.single_line_data.is_match(¤t_line) { + if self.reading_multiline_value { + return Err(Error::ReplyParseFailed( + "found single-line reply but still reading a multi-line reply".to_string(), + )); + } + self.pending_reply.push(current_line); + // begin of multiline data from getinfo and friends + } else if self.multi_line_data.is_match(¤t_line) { + if self.reading_multiline_value { + return Err(Error::ReplyParseFailed( + "found multi-line start reply but still reading a multi-line reply" + .to_string(), + )); + } + self.pending_reply.push(current_line); + self.reading_multiline_value = true; + // multiline data to be squashed to a single entry + } else { + if !self.reading_multiline_value { + return Err(Error::ReplyParseFailed( + "found a multi-line intermediate reply but not reading a multi-line reply" + .to_string(), + )); + } + // don't bother writing the end of multiline token + if current_line == "." { + self.reading_multiline_value = false; + } else { + let multiline = match self.pending_reply.last_mut() { + Some(multiline) => multiline, + // if our logic here is right, then + // self.reading_multiline_value == !self.pending_reply.is_empty() + // should always be true regardless of the data received + // from the control port + None => unreachable!(), + }; + multiline.push('\n'); + multiline.push_str(¤t_line); + } + } + } + + // take ownership of the reply lines + let mut reply_lines: Vec = Default::default(); + std::mem::swap(&mut self.pending_reply, &mut reply_lines); + + // parse out the response code for easier matching + let status_code_string = match reply_lines.first() { + Some(line) => line[0..3].to_string(), + // the lines have already been parsed+validated in the above loop + None => unreachable!(), + }; + let status_code: u32 = match status_code_string.parse() { + Ok(status_code) => status_code, + Err(_) => { + return Err(Error::ReplyParseFailed(format!( + "unable to parse '{}' as status code", + status_code_string + ))) + } + }; + + // strip the redundant status code from start of lines + for line in reply_lines.iter_mut() { + println!(">>> {}", line); + if line.starts_with(&status_code_string) { + *line = line[4..].to_string(); + } + } + + Ok(Some(Reply { + status_code, + reply_lines, + })) + } + + pub fn write(&mut self, cmd: &str) -> Result<(), Error> { + println!("<<< {}", cmd); + if let Err(err) = write!(self.stream, "{}\r\n", cmd) { + self.closed_by_remote = true; + return Err(Error::WriteFailed(err)); + } + Ok(()) + } +} diff --git a/tor-interface/src/tor_controller.rs b/tor-interface/src/tor_controller.rs index 7905f902f..4b8ffa780 100644 --- a/tor-interface/src/tor_controller.rs +++ b/tor-interface/src/tor_controller.rs @@ -1,505 +1,56 @@ // standard -use std::cmp::Ordering; -use std::collections::VecDeque; use std::default::Default; -use std::fs; -use std::fs::File; -use std::io::{BufRead, BufReader, ErrorKind, Read, Write}; -use std::iter; -use std::net::{SocketAddr, TcpListener, TcpStream}; -use std::ops::Drop; +use std::net::SocketAddr; use std::option::Option; +#[cfg(test)] use std::path::Path; -use std::process; -use std::process::{Child, ChildStdout, Command, Stdio}; use std::str::FromStr; use std::string::ToString; -use std::sync::{atomic, Arc, Mutex}; +#[cfg(test)] use std::time::{Duration, Instant}; // extern crates -use rand::distributions::Alphanumeric; -use rand::rngs::OsRng; -use rand::Rng; use regex::Regex; #[cfg(test)] use serial_test::serial; -use socks::Socks5Stream; -use url::Host; // internal crates -use crate::error::ControlStreamError; -use crate::error::ControlStreamError::*; -use crate::error::TorControllerError; -use crate::error::TorControllerError::*; -use crate::error::TorManagerError; -use crate::error::TorManagerError::*; -use crate::error::TorProcessError; -use crate::error::TorProcessError::*; -use crate::error::TorVersionError; -use crate::error::TorVersionError::*; +use crate::tor_control_stream::*; use crate::tor_crypto::*; -// get the name of our tor executable -pub const fn tor_exe_name() -> &'static str { - if cfg!(windows) { - "tor.exe" - } else { - "tor" - } -} - -// securely generate password using OsRng -fn generate_password(length: usize) -> String { - let password: String = iter::repeat(()) - .map(|()| OsRng.sample(Alphanumeric)) - .map(char::from) - .take(length) - .collect(); - - password -} - -fn read_control_port_file(control_port_file: &Path) -> Result { - // open file - let mut file = File::open(control_port_file).map_err(ControlPortFileReadFailed)?; - - // bail if the file is larger than expected - let metadata = file.metadata().map_err(ControlPortFileReadFailed)?; - if metadata.len() >= 1024 { - return Err(ControlPortFileTooLarge( - format!("{}", control_port_file.display()), - metadata.len(), - )); - } - - // read contents to string - let mut contents = String::new(); - file.read_to_string(&mut contents) - .map_err(ControlPortFileReadFailed)?; - - if contents.starts_with("PORT=") { - let addr_string = &contents.trim_end()["PORT=".len()..]; - if let Ok(addr) = SocketAddr::from_str(addr_string) { - return Ok(addr); - } - } - Err(ControlPortFileContentsInvalid(format!( - "{}", - control_port_file.display() - ))) -} - -// Encapsulates the tor daemon process -struct TorProcess { - control_addr: SocketAddr, - process: Child, - password: String, - // stdout data - stdout_lines: Arc>>, -} - -impl TorProcess { - pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { - if tor_bin_path.is_relative() { - return Err(TorBinPathNotAbsolute(format!("{}", tor_bin_path.display()))); - } - if data_directory.is_relative() { - return Err(TorDataDirectoryPathNotAbsolute(format!( - "{}", - data_directory.display() - ))); - } - - // create data directory if it doesn't exist - if !data_directory.exists() { - fs::create_dir_all(data_directory).map_err(DataDirectoryCreationFailed)?; - } else if data_directory.is_file() { - return Err(DataDirectoryPathExistsAsFile(format!( - "{}", - data_directory.display() - ))); - } - - // construct paths to torrc files - let default_torrc = data_directory.join("default_torrc"); - let torrc = data_directory.join("torrc"); - let control_port_file = data_directory.join("control_port"); - - // TODO: should we nuke the existing torrc between runs? Do we want - // users setting custom nonsense in there? - // construct default torrc - // - daemon determines socks port and only allows clients to connect to onion services - // - minimize writes to disk - // - start with network disabled by default - if !default_torrc.exists() { - const DEFAULT_TORRC_CONTENT: &str = "SocksPort auto OnionTrafficOnly\n\ - AvoidDiskWrites 1\n\ - DisableNetwork 1\n\n"; - - let mut default_torrc_file = - File::create(&default_torrc).map_err(DefaultTorrcFileCreationFailed)?; - default_torrc_file - .write_all(DEFAULT_TORRC_CONTENT.as_bytes()) - .map_err(DefaultTorrcFileWriteFailed)?; - } - - // create empty torrc for user - if !torrc.exists() { - let _ = File::create(&torrc).map_err(TorrcFileCreationFailed)?; - } - - // remove any existing control_port_file - if control_port_file.exists() { - fs::remove_file(&control_port_file).map_err(ControlPortFileDeleteFailed)?; - } - - const CONTROL_PORT_PASSWORD_LENGTH: usize = 32usize; - let password = generate_password(CONTROL_PORT_PASSWORD_LENGTH); - let password_hash = hash_tor_password(&password); - - let mut process = Command::new(tor_bin_path.as_os_str()) - .stdout(Stdio::piped()) - .stdin(Stdio::null()) - .stderr(Stdio::null()) - // point to our above written torrc file - .arg("--defaults-torrc") - .arg(default_torrc) - // location of torrc - .arg("--torrc-file") - .arg(torrc) - // root data directory - .arg("DataDirectory") - .arg(data_directory) - // daemon will assign us a port, and we will - // read it from the control port file - .arg("ControlPort") - .arg("auto") - // control port file destination - .arg("ControlPortWriteToFile") - .arg(control_port_file.clone()) - // use password authentication to prevent other apps - // from modifying our daemon's settings - .arg("HashedControlPassword") - .arg(password_hash) - // tor process will shut down after this process shuts down - // to avoid orphaned tor daemon - .arg("__OwningControllerProcess") - .arg(process::id().to_string()) - .spawn() - .map_err(TorProcessStartFailed)?; - - let mut control_addr = None; - let start = Instant::now(); - - // try and read the control port from the control port file - // or abort after 5 seconds - // TODO: make this timeout configurable? - while control_addr.is_none() && start.elapsed() < Duration::from_secs(5) { - if control_port_file.exists() { - control_addr = Some(read_control_port_file(control_port_file.as_path())?); - fs::remove_file(&control_port_file).map_err(ControlPortFileDeleteFailed)?; - } - } - - let control_addr = match control_addr { - Some(control_addr) => control_addr, - None => { - return Err(ControlPortFileMissing(format!( - "{}", - control_port_file.display() - ))) - } - }; - - let stdout_lines: Arc>> = Default::default(); +#[cfg(test)] +use crate::tor_process::*; +use crate::tor_version::*; - { - let stdout_lines = Arc::downgrade(&stdout_lines); - let stdout = BufReader::new(match process.stdout.take() { - Some(stdout) => stdout, - None => return Err(TorProcessStdoutTakeFailed()), - }); +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("response regex creation failed")] + ParsingRegexCreationFailed(#[source] regex::Error), - std::thread::Builder::new() - .name("tor_stdout_reader".to_string()) - .spawn(move || { - TorProcess::read_stdout_task(&stdout_lines, stdout); - }) - .map_err(StdoutReadThreadSpawnFailed)?; - } + #[error("control stream read reply failed")] + ReadReplyFailed(#[source] crate::tor_control_stream::Error), - Ok(TorProcess { - control_addr, - process, - password, - stdout_lines, - }) - } + #[error("unexpected synchronous reply recieved")] + UnexpectedSynchonousReplyReceived(), - fn read_stdout_task( - stdout_lines: &std::sync::Weak>>, - mut stdout: BufReader, - ) { - while let Some(stdout_lines) = stdout_lines.upgrade() { - let mut line = String::default(); - // read line - if stdout.read_line(&mut line).is_ok() { - // remove trailing '\n' - line.pop(); - // then acquire the lock on the line buffer - let mut stdout_lines = match stdout_lines.lock() { - Ok(stdout_lines) => stdout_lines, - Err(_) => unreachable!(), - }; - stdout_lines.push(line); - } - } - } + #[error("control stream write command failed")] + WriteCommandFailed(#[source] crate::tor_control_stream::Error), - fn wait_log_lines(&mut self) -> Vec { - let mut lines = match self.stdout_lines.lock() { - Ok(lines) => lines, - Err(_) => unreachable!(), - }; - std::mem::take(&mut lines) - } -} + #[error("invalid command arguments: {0}")] + InvalidCommandArguments(String), -impl Drop for TorProcess { - fn drop(&mut self) { - let _ = self.process.kill(); - } -} + #[error("command failed: {0} {}", .1.join("\n"))] + CommandReturnedError(u32, Vec), -pub struct ControlStream { - stream: TcpStream, - closed_by_remote: bool, - pending_data: Vec, - pending_lines: VecDeque, - pending_reply: Vec, - reading_multiline_value: bool, - // regexes used to parse control port responses - single_line_data: Regex, - multi_line_data: Regex, - end_reply_line: Regex, -} + #[error("failed to parse command reply: {0}")] + CommandReplyParseFailed(String), -type StatusCode = u32; -struct Reply { - status_code: StatusCode, - reply_lines: Vec, -} - -impl ControlStream { - pub fn new( - addr: &SocketAddr, - read_timeout: Duration, - ) -> Result { - if read_timeout.is_zero() { - return Err(ReadTimeoutZero()); - } - - let stream = TcpStream::connect(addr).map_err(CreationFailed)?; - stream - .set_read_timeout(Some(read_timeout)) - .map_err(ConfigurationFailed)?; - - // pre-allocate a kilobyte for the read buffer - const READ_BUFFER_SIZE: usize = 1024; - let pending_data = Vec::with_capacity(READ_BUFFER_SIZE); - - let single_line_data = - Regex::new(r"^\d\d\d-.*").map_err(ControlStreamError::ParsingRegexCreationFailed)?; - let multi_line_data = - Regex::new(r"^\d\d\d+.*").map_err(ControlStreamError::ParsingRegexCreationFailed)?; - let end_reply_line = - Regex::new(r"^\d\d\d .*").map_err(ControlStreamError::ParsingRegexCreationFailed)?; - - Ok(ControlStream { - stream, - closed_by_remote: false, - pending_data, - pending_lines: Default::default(), - pending_reply: Default::default(), - reading_multiline_value: false, - // regex - single_line_data, - multi_line_data, - end_reply_line, - }) - } - - #[cfg(test)] - fn closed_by_remote(&mut self) -> bool { - self.closed_by_remote - } - - fn read_line(&mut self) -> Result, ControlStreamError> { - // read pending bytes from stream until we have a line to return - while self.pending_lines.is_empty() { - let byte_count = self.pending_data.len(); - match self.stream.read_to_end(&mut self.pending_data) { - Err(err) => { - if err.kind() == ErrorKind::WouldBlock || err.kind() == ErrorKind::TimedOut { - if byte_count == self.pending_data.len() { - return Ok(None); - } - } else { - return Err(ControlStreamError::ReadFailed(err)); - } - } - Ok(0usize) => { - self.closed_by_remote = true; - return Err(ControlStreamError::ClosedByRemote()); - } - Ok(_count) => (), - } - - // split our read buffer into individual lines - let mut begin = 0; - for index in 1..self.pending_data.len() { - if self.pending_data[index - 1] == b'\r' && self.pending_data[index] == b'\n' { - let end = index - 1; - // view into byte vec of just the found line - let line_view: &[u8] = &self.pending_data[begin..end]; - // convert to string - let line_string = std::str::from_utf8(line_view).map_err(InvalidResponse)?; - - // save in pending list - self.pending_lines.push_back(line_string.to_string()); - // update begin (and skip over \r\n) - begin = end + 2; - } - } - // leave any leftover bytes in the buffer for the next call - self.pending_data.drain(0..begin); - } - - Ok(self.pending_lines.pop_front()) - } - - fn read_reply(&mut self) -> Result, ControlStreamError> { - loop { - let current_line = match self.read_line()? { - Some(line) => line, - None => return Ok(None), - }; - - // make sure the status code matches (if we are not in the - // middle of a multi-line read - if let Some(first_line) = self.pending_reply.first() { - if !self.reading_multiline_value { - let first_status_code = &first_line[0..3]; - let current_status_code = ¤t_line[0..3]; - if first_status_code != current_status_code { - return Err(ReplyParseFailed(format!( - "mismatched status codes, {} != {}", - first_status_code, current_status_code - ))); - } - } - } - - // end of a response - if self.end_reply_line.is_match(¤t_line) { - if self.reading_multiline_value { - return Err(ReplyParseFailed( - "found multi-line end reply but not reading a multi-line reply".to_string(), - )); - } - self.pending_reply.push(current_line); - break; - // single line data from getinfo and friends - } else if self.single_line_data.is_match(¤t_line) { - if self.reading_multiline_value { - return Err(ReplyParseFailed( - "found single-line reply but still reading a multi-line reply".to_string(), - )); - } - self.pending_reply.push(current_line); - // begin of multiline data from getinfo and friends - } else if self.multi_line_data.is_match(¤t_line) { - if self.reading_multiline_value { - return Err(ReplyParseFailed( - "found multi-line start reply but still reading a multi-line reply" - .to_string(), - )); - } - self.pending_reply.push(current_line); - self.reading_multiline_value = true; - // multiline data to be squashed to a single entry - } else { - if !self.reading_multiline_value { - return Err(ReplyParseFailed( - "found a multi-line intermediate reply but not reading a multi-line reply" - .to_string(), - )); - } - // don't bother writing the end of multiline token - if current_line == "." { - self.reading_multiline_value = false; - } else { - let multiline = match self.pending_reply.last_mut() { - Some(multiline) => multiline, - // if our logic here is right, then - // self.reading_multiline_value == !self.pending_reply.is_empty() - // should always be true regardless of the data received - // from the control port - None => unreachable!(), - }; - multiline.push('\n'); - multiline.push_str(¤t_line); - } - } - } - - // take ownership of the reply lines - let mut reply_lines: Vec = Default::default(); - std::mem::swap(&mut self.pending_reply, &mut reply_lines); - - // parse out the response code for easier matching - let status_code_string = match reply_lines.first() { - Some(line) => line[0..3].to_string(), - // the lines have already been parsed+validated in the above loop - None => unreachable!(), - }; - let status_code: u32 = match status_code_string.parse() { - Ok(status_code) => status_code, - Err(_) => { - return Err(ReplyParseFailed(format!( - "unable to parse '{}' as status code", - status_code_string - ))) - } - }; - - // strip the redundant status code from start of lines - for line in reply_lines.iter_mut() { - println!(">>> {}", line); - if line.starts_with(&status_code_string) { - *line = line[4..].to_string(); - } - } - - Ok(Some(Reply { - status_code, - reply_lines, - })) - } - - pub fn write(&mut self, cmd: &str) -> Result<(), ControlStreamError> { - println!("<<< {}", cmd); - if let Err(err) = write!(self.stream, "{}\r\n", cmd) { - self.closed_by_remote = true; - return Err(ControlStreamError::WriteFailed(err)); - } - Ok(()) - } + #[error("failed to parse received tor version")] + TorVersionParseFailed(#[source] crate::tor_version::Error), } // Per-command data #[derive(Default)] -pub struct AddOnionFlags { +pub(crate) struct AddOnionFlags { pub discard_pk: bool, pub detach: bool, pub v3_auth: bool, @@ -508,229 +59,11 @@ pub struct AddOnionFlags { } #[derive(Default)] -pub struct OnionClientAuthAddFlags { +pub(crate) struct OnionClientAuthAddFlags { pub permanent: bool, } -// see version-spec.txt -#[derive(Clone)] -pub struct TorVersion { - pub major: u32, - pub minor: u32, - pub micro: u32, - pub patch_level: u32, - pub status_tag: Option, -} - -impl TorVersion { - fn status_tag_pattern_is_match(status_tag: &str) -> bool { - if status_tag.is_empty() { - return false; - } - - for c in status_tag.chars() { - if c.is_whitespace() { - return false; - } - } - true - } - - fn new( - major: u32, - minor: u32, - micro: u32, - patch_level: Option, - status_tag: Option<&str>, - ) -> Result { - let status_tag = if let Some(status_tag) = status_tag { - if Self::status_tag_pattern_is_match(status_tag) { - Some(status_tag.to_string()) - } else { - return Err(TorVersionError::ParseError( - "tor version status tag may not be empty or contain white-space".to_string(), - )); - } - } else { - None - }; - - Ok(TorVersion { - major, - minor, - micro, - patch_level: patch_level.unwrap_or(0u32), - status_tag, - }) - } -} - -impl FromStr for TorVersion { - type Err = TorVersionError; - - fn from_str(s: &str) -> Result { - // MAJOR.MINOR.MICRO[.PATCHLEVEL][-STATUS_TAG][ (EXTRA_INFO)]* - let mut tokens = s.split(' '); - let (major, minor, micro, patch_level, status_tag) = - if let Some(version_status_tag) = tokens.next() { - let mut tokens = version_status_tag.split('-'); - let (major, minor, micro, patch_level) = if let Some(version) = tokens.next() { - let mut tokens = version.split('.'); - let major: u32 = if let Some(major) = tokens.next() { - match major.parse() { - Ok(major) => major, - Err(_) => { - return Err(ParseError(format!( - "failed to parse '{}' as MAJOR portion of tor version", - major - ))) - } - } - } else { - return Err(ParseError( - "failed to find MAJOR portion of tor version".to_string(), - )); - }; - let minor: u32 = if let Some(minor) = tokens.next() { - match minor.parse() { - Ok(minor) => minor, - Err(_) => { - return Err(ParseError(format!( - "failed to parse '{}' as MINOR portion of tor version", - minor - ))) - } - } - } else { - return Err(ParseError( - "failed to find MINOR portion of tor version".to_string(), - )); - }; - let micro: u32 = if let Some(micro) = tokens.next() { - match micro.parse() { - Ok(micro) => micro, - Err(_) => { - return Err(ParseError(format!( - "failed to parse '{}' as MICRO portion of tor version", - micro - ))) - } - } - } else { - return Err(ParseError( - "failed to find MICRO portion of tor version".to_string(), - )); - }; - let patch_level: u32 = if let Some(patch_level) = tokens.next() { - match patch_level.parse() { - Ok(patch_level) => patch_level, - Err(_) => { - return Err(ParseError(format!( - "failed to parse '{}' as PATCHLEVEL portion of tor version", - patch_level - ))) - } - } - } else { - 0u32 - }; - (major, minor, micro, patch_level) - } else { - // if there were '-' the previous next() would have returned the enire string - unreachable!(); - }; - let status_tag = tokens.next().map(|status_tag| status_tag.to_string()); - - (major, minor, micro, patch_level, status_tag) - } else { - // if there were no ' ' character the previou snext() would have returned the enire string - unreachable!(); - }; - for extra_info in tokens { - if !extra_info.starts_with('(') || !extra_info.ends_with(')') { - return Err(ParseError(format!( - "failed to parse '{}' as [ (EXTRA_INFO)]", - extra_info - ))); - } - } - TorVersion::new( - major, - minor, - micro, - Some(patch_level), - status_tag.as_deref(), - ) - } -} - -impl ToString for TorVersion { - fn to_string(&self) -> String { - match &self.status_tag { - Some(status_tag) => format!( - "{}.{}.{}.{}-{}", - self.major, self.minor, self.micro, self.patch_level, status_tag - ), - None => format!( - "{}.{}.{}.{}", - self.major, self.minor, self.micro, self.patch_level - ), - } - } -} - -impl PartialEq for TorVersion { - fn eq(&self, other: &Self) -> bool { - self.major == other.major - && self.minor == other.minor - && self.micro == other.micro - && self.patch_level == other.patch_level - && self.status_tag == other.status_tag - } -} - -impl PartialOrd for TorVersion { - fn partial_cmp(&self, other: &Self) -> Option { - if let Some(order) = self.major.partial_cmp(&other.major) { - if order != Ordering::Equal { - return Some(order); - } - } - - if let Some(order) = self.minor.partial_cmp(&other.minor) { - if order != Ordering::Equal { - return Some(order); - } - } - - if let Some(order) = self.micro.partial_cmp(&other.micro) { - if order != Ordering::Equal { - return Some(order); - } - } - - if let Some(order) = self.patch_level.partial_cmp(&other.patch_level) { - if order != Ordering::Equal { - return Some(order); - } - } - - // version-spect.txt *does* say that we should compare tags lexicgraphically - // if all of the version numbers are the same when comparing, but we are - // going to diverge here and say we can only compare tags for equality. - // - // In practice we will be comparing tor daemon tags against tagless (stable) - // versions so this shouldn't be an issue - - if self.status_tag == other.status_tag { - return Some(Ordering::Equal); - } - - None - } -} - -enum AsyncEvent { +pub(crate) enum AsyncEvent { Unknown { lines: Vec, }, @@ -745,7 +78,7 @@ enum AsyncEvent { }, } -struct TorController { +pub(crate) struct TorController { // underlying control stream control_stream: ControlStream, // list of async replies to be handled @@ -757,16 +90,16 @@ struct TorController { } impl TorController { - pub fn new(control_stream: ControlStream) -> Result { + pub fn new(control_stream: ControlStream) -> Result { let status_event_pattern = Regex::new(r#"^STATUS_CLIENT (?PNOTICE|WARN|ERR) (?P[A-Za-z]+)"#) - .map_err(TorControllerError::ParsingRegexCreationFailed)?; + .map_err(Error::ParsingRegexCreationFailed)?; let status_event_argument_pattern = Regex::new(r#"(?P[A-Z]+)=(?P[A-Za-z0-9_]+|"[^"]+")"#) - .map_err(TorControllerError::ParsingRegexCreationFailed)?; + .map_err(Error::ParsingRegexCreationFailed)?; let hs_desc_pattern = Regex::new( r#"HS_DESC (?PREQUESTED|UPLOAD|RECEIVED|UPLOADED|IGNORE|FAILED|CREATED) (?P[a-z2-7]{56})"# - ).map_err(TorControllerError::ParsingRegexCreationFailed)?; + ).map_err(Error::ParsingRegexCreationFailed)?; Ok(TorController { control_stream, @@ -780,14 +113,18 @@ impl TorController { // return curently available events, does not block waiting // for an event - fn wait_async_replies(&mut self) -> Result, TorControllerError> { + fn wait_async_replies(&mut self) -> Result, Error> { let mut replies: Vec = Default::default(); // take any previously received async replies std::mem::swap(&mut self.async_replies, &mut replies); // and keep consuming until none are available loop { - if let Some(reply) = self.control_stream.read_reply().map_err(ReadReplyFailed)? { + if let Some(reply) = self + .control_stream + .read_reply() + .map_err(Error::ReadReplyFailed)? + { replies.push(reply); } else { // no more replies immediately available so return @@ -796,9 +133,9 @@ impl TorController { } } - fn reply_to_event(&self, reply: &mut Reply) -> Result { + fn reply_to_event(&self, reply: &mut Reply) -> Result { if reply.status_code != 650u32 { - return Err(UnexpectedSynchonousReplyReceived()); + return Err(Error::UnexpectedSynchonousReplyReceived()); } // not sure this is what we want but yolo @@ -868,7 +205,7 @@ impl TorController { Ok(AsyncEvent::Unknown { lines: reply_lines }) } - pub fn wait_async_events(&mut self) -> Result, TorControllerError> { + pub fn wait_async_events(&mut self) -> Result, Error> { let mut async_replies = self.wait_async_replies()?; let mut async_events: Vec = Default::default(); @@ -880,9 +217,13 @@ impl TorController { } // wait for a sync reply, save off async replies for later - fn wait_sync_reply(&mut self) -> Result { + fn wait_sync_reply(&mut self) -> Result { loop { - if let Some(reply) = self.control_stream.read_reply().map_err(ReadReplyFailed)? { + if let Some(reply) = self + .control_stream + .read_reply() + .map_err(Error::ReadReplyFailed)? + { match reply.status_code { 650u32 => self.async_replies.push(reply), _ => return Ok(reply), @@ -891,10 +232,10 @@ impl TorController { } } - fn write_command(&mut self, text: &str) -> Result { + fn write_command(&mut self, text: &str) -> Result { self.control_stream .write(text) - .map_err(WriteCommandFailed)?; + .map_err(Error::WriteCommandFailed)?; self.wait_sync_reply() } @@ -908,9 +249,9 @@ impl TorController { // // SETCONF (3.1) - fn setconf_cmd(&mut self, key_values: &[(&str, &str)]) -> Result { + fn setconf_cmd(&mut self, key_values: &[(&str, &str)]) -> Result { if key_values.is_empty() { - return Err(TorControllerError::InvalidCommandArguments( + return Err(Error::InvalidCommandArguments( "SETCONF key-value pairs list must not be empty".to_string(), )); } @@ -926,9 +267,9 @@ impl TorController { // GETCONF (3.3) #[cfg(test)] - fn getconf_cmd(&mut self, keywords: &[&str]) -> Result { + fn getconf_cmd(&mut self, keywords: &[&str]) -> Result { if keywords.is_empty() { - return Err(TorControllerError::InvalidCommandArguments( + return Err(Error::InvalidCommandArguments( "GETCONF keywords list must not be empty".to_string(), )); } @@ -938,9 +279,9 @@ impl TorController { } // SETEVENTS (3.4) - fn setevents_cmd(&mut self, event_codes: &[&str]) -> Result { + fn setevents_cmd(&mut self, event_codes: &[&str]) -> Result { if event_codes.is_empty() { - return Err(TorControllerError::InvalidCommandArguments( + return Err(Error::InvalidCommandArguments( "SETEVENTS event codes list mut not be empty".to_string(), )); } @@ -950,16 +291,16 @@ impl TorController { } // AUTHENTICATE (3.5) - fn authenticate_cmd(&mut self, password: &str) -> Result { + fn authenticate_cmd(&mut self, password: &str) -> Result { let command = format!("AUTHENTICATE \"{}\"", password); self.write_command(&command) } // GETINFO (3.9) - fn getinfo_cmd(&mut self, keywords: &[&str]) -> Result { + fn getinfo_cmd(&mut self, keywords: &[&str]) -> Result { if keywords.is_empty() { - return Err(TorControllerError::InvalidCommandArguments( + return Err(Error::InvalidCommandArguments( "GETINFO keywords list must not be empty".to_string(), )); } @@ -977,7 +318,7 @@ impl TorController { virt_port: u16, target: Option, client_auth: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { let mut command_buffer = vec!["ADD_ONION".to_string()]; // set our key or request a new one @@ -1034,10 +375,7 @@ impl TorController { } // DEL_ONION (3.38) - fn del_onion_cmd( - &mut self, - service_id: &V3OnionServiceId, - ) -> Result { + fn del_onion_cmd(&mut self, service_id: &V3OnionServiceId) -> Result { let command = format!("DEL_ONION {}", service_id.to_string()); self.write_command(&command) @@ -1050,7 +388,7 @@ impl TorController { private_key: &X25519PrivateKey, client_name: Option, flags: &OnionClientAuthAddFlags, - ) -> Result { + ) -> Result { let mut command_buffer = vec!["ONION_CLIENT_AUTH_ADD".to_string()]; // set the onion service id @@ -1077,7 +415,7 @@ impl TorController { fn onion_client_auth_remove_cmd( &mut self, service_id: &V3OnionServiceId, - ) -> Result { + ) -> Result { let command = format!("ONION_CLIENT_AUTH_REMOVE {}", service_id.to_string()); self.write_command(&command) @@ -1087,23 +425,17 @@ impl TorController { // Public high-level typesafe command method wrappers // - pub fn setconf(&mut self, key_values: &[(&str, &str)]) -> Result<(), TorControllerError> { + pub fn setconf(&mut self, key_values: &[(&str, &str)]) -> Result<(), Error> { let reply = self.setconf_cmd(key_values)?; match reply.status_code { 250u32 => Ok(()), - code => Err(TorControllerError::CommandReturnedError( - code, - reply.reply_lines, - )), + code => Err(Error::CommandReturnedError(code, reply.reply_lines)), } } #[cfg(test)] - pub fn getconf( - &mut self, - keywords: &[&str], - ) -> Result, TorControllerError> { + pub fn getconf(&mut self, keywords: &[&str]) -> Result, Error> { let reply = self.getconf_cmd(keywords)?; match reply.status_code { @@ -1118,41 +450,29 @@ impl TorController { } Ok(key_values) } - code => Err(TorControllerError::CommandReturnedError( - code, - reply.reply_lines, - )), + code => Err(Error::CommandReturnedError(code, reply.reply_lines)), } } - pub fn setevents(&mut self, events: &[&str]) -> Result<(), TorControllerError> { + pub fn setevents(&mut self, events: &[&str]) -> Result<(), Error> { let reply = self.setevents_cmd(events)?; match reply.status_code { 250u32 => Ok(()), - code => Err(TorControllerError::CommandReturnedError( - code, - reply.reply_lines, - )), + code => Err(Error::CommandReturnedError(code, reply.reply_lines)), } } - pub fn authenticate(&mut self, password: &str) -> Result<(), TorControllerError> { + pub fn authenticate(&mut self, password: &str) -> Result<(), Error> { let reply = self.authenticate_cmd(password)?; match reply.status_code { 250u32 => Ok(()), - code => Err(TorControllerError::CommandReturnedError( - code, - reply.reply_lines, - )), + code => Err(Error::CommandReturnedError(code, reply.reply_lines)), } } - pub fn getinfo( - &mut self, - keywords: &[&str], - ) -> Result, TorControllerError> { + pub fn getinfo(&mut self, keywords: &[&str]) -> Result, Error> { let reply = self.getinfo_cmd(keywords)?; match reply.status_code { @@ -1171,10 +491,7 @@ impl TorController { } Ok(key_values) } - code => Err(TorControllerError::CommandReturnedError( - code, - reply.reply_lines, - )), + code => Err(Error::CommandReturnedError(code, reply.reply_lines)), } } @@ -1186,7 +503,7 @@ impl TorController { virt_port: u16, target: Option, client_auth: Option<&[X25519PublicKey]>, - ) -> Result<(Option, V3OnionServiceId), TorControllerError> { + ) -> Result<(Option, V3OnionServiceId), Error> { let reply = self.add_onion_cmd(key, flags, max_streams, virt_port, target, client_auth)?; let mut private_key: Option = None; @@ -1197,7 +514,7 @@ impl TorController { for line in reply.reply_lines { if let Some(mut index) = line.find("ServiceID=") { if service_id.is_some() { - return Err(TorControllerError::CommandReplyParseFailed( + return Err(Error::CommandReplyParseFailed( "received duplicate ServiceID entries".to_string(), )); } @@ -1206,7 +523,7 @@ impl TorController { service_id = match V3OnionServiceId::from_string(service_id_string) { Ok(service_id) => Some(service_id), Err(_) => { - return Err(TorControllerError::CommandReplyParseFailed(format!( + return Err(Error::CommandReplyParseFailed(format!( "could not parse '{}' as V3OnionServiceId", service_id_string ))) @@ -1214,7 +531,7 @@ impl TorController { } } else if let Some(mut index) = line.find("PrivateKey=") { if private_key.is_some() { - return Err(TorControllerError::CommandReplyParseFailed( + return Err(Error::CommandReplyParseFailed( "received duplicate PrivateKey entries".to_string(), )); } @@ -1223,7 +540,7 @@ impl TorController { private_key = match Ed25519PrivateKey::from_key_blob(key_blob_string) { Ok(private_key) => Some(private_key), Err(_) => { - return Err(TorControllerError::CommandReplyParseFailed(format!( + return Err(Error::CommandReplyParseFailed(format!( "could not parse {} as Ed25519PrivateKey", key_blob_string ))) @@ -1231,61 +548,53 @@ impl TorController { }; } else if line.contains("ClientAuthV3=") { if client_auth.unwrap_or_default().is_empty() { - return Err(TorControllerError::CommandReplyParseFailed( + return Err(Error::CommandReplyParseFailed( "recieved unexpected ClientAuthV3 keys".to_string(), )); } } else if !line.contains("OK") { - return Err(TorControllerError::CommandReplyParseFailed(format!( + return Err(Error::CommandReplyParseFailed(format!( "received unexpected reply line '{}'", line ))); } } } - code => { - return Err(TorControllerError::CommandReturnedError( - code, - reply.reply_lines, - )) - } + code => return Err(Error::CommandReturnedError(code, reply.reply_lines)), } if flags.discard_pk { if private_key.is_some() { - return Err(TorControllerError::CommandReplyParseFailed( + return Err(Error::CommandReplyParseFailed( "PrivateKey response should have been discard".to_string(), )); } } else if private_key.is_none() { - return Err(TorControllerError::CommandReplyParseFailed( + return Err(Error::CommandReplyParseFailed( "did not receive a PrivateKey".to_string(), )); } match service_id { Some(service_id) => Ok((private_key, service_id)), - None => Err(TorControllerError::CommandReplyParseFailed( + None => Err(Error::CommandReplyParseFailed( "did not receive a ServiceID".to_string(), )), } } - pub fn del_onion(&mut self, service_id: &V3OnionServiceId) -> Result<(), TorControllerError> { + pub fn del_onion(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> { let reply = self.del_onion_cmd(service_id)?; match reply.status_code { 250u32 => Ok(()), - code => Err(TorControllerError::CommandReturnedError( - code, - reply.reply_lines, - )), + code => Err(Error::CommandReturnedError(code, reply.reply_lines)), } } // more specific encapulsation of specific command invocations - pub fn getinfo_net_listeners_socks(&mut self) -> Result, TorControllerError> { + pub fn getinfo_net_listeners_socks(&mut self) -> Result, Error> { let response = self.getinfo(&["net/listeners/socks"])?; for (key, value) in response.iter() { if key.as_str() == "net/listeners/socks" { @@ -1297,7 +606,7 @@ impl TorController { let mut result: Vec = Default::default(); for socket_addr in listeners.iter() { if !socket_addr.starts_with('\"') || !socket_addr.ends_with('\"') { - return Err(TorControllerError::CommandReplyParseFailed(format!( + return Err(Error::CommandReplyParseFailed(format!( "could not parse '{}' as socket address", socket_addr ))); @@ -1308,7 +617,7 @@ impl TorController { result.push(match SocketAddr::from_str(stripped) { Ok(result) => result, Err(_) => { - return Err(TorControllerError::CommandReplyParseFailed(format!( + return Err(Error::CommandReplyParseFailed(format!( "could not parse '{}' as socket address", socket_addr ))) @@ -1318,19 +627,19 @@ impl TorController { return Ok(result); } } - Err(TorControllerError::CommandReplyParseFailed( + Err(Error::CommandReplyParseFailed( "reply did not find a 'net/listeners/socks' key/value".to_string(), )) } - pub fn getinfo_version(&mut self) -> Result { + pub fn getinfo_version(&mut self) -> Result { let response = self.getinfo(&["version"])?; for (key, value) in response.iter() { if key.as_str() == "version" { - return TorVersion::from_str(value).map_err(TorVersionParseFailed); + return TorVersion::from_str(value).map_err(Error::TorVersionParseFailed); } } - Err(TorControllerError::CommandReplyParseFailed( + Err(Error::CommandReplyParseFailed( "did not find a 'version' key/value".to_string(), )) } @@ -1341,429 +650,26 @@ impl TorController { private_key: &X25519PrivateKey, client_name: Option, flags: &OnionClientAuthAddFlags, - ) -> Result<(), TorControllerError> { + ) -> Result<(), Error> { let reply = self.onion_client_auth_add_cmd(service_id, private_key, client_name, flags)?; match reply.status_code { 250u32..=252u32 => Ok(()), - code => Err(TorControllerError::CommandReturnedError( - code, - reply.reply_lines, - )), + code => Err(Error::CommandReturnedError(code, reply.reply_lines)), } } #[allow(dead_code)] - pub fn onion_client_auth_remove( - &mut self, - service_id: &V3OnionServiceId, - ) -> Result<(), TorControllerError> { + pub fn onion_client_auth_remove(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> { let reply = self.onion_client_auth_remove_cmd(service_id)?; match reply.status_code { 250u32..=251u32 => Ok(()), - code => Err(TorControllerError::CommandReturnedError( - code, - reply.reply_lines, - )), + code => Err(Error::CommandReturnedError(code, reply.reply_lines)), } } } -pub struct CircuitToken { - username: String, - password: String, -} - -impl CircuitToken { - #[allow(dead_code)] - pub fn new(first_party: Host) -> CircuitToken { - const CIRCUIT_TOKEN_PASSWORD_LENGTH: usize = 32usize; - let username = first_party.to_string(); - let password = generate_password(CIRCUIT_TOKEN_PASSWORD_LENGTH); - - CircuitToken { username, password } - } -} - -pub struct OnionStream { - stream: TcpStream, - peer_addr: Option, -} - -impl OnionStream { - pub fn nodelay(&self) -> Result { - self.stream.nodelay() - } - - pub fn peer_addr(&self) -> Option<&V3OnionServiceId> { - self.peer_addr.as_ref() - } - - pub fn read_timeout(&self) -> Result, std::io::Error> { - self.stream.read_timeout() - } - - pub fn set_nodelay(&self, nodelay: bool) -> Result<(), std::io::Error> { - self.stream.set_nodelay(nodelay) - } - - pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { - self.stream.set_nonblocking(nonblocking) - } - - pub fn set_read_timeout(&self, dur: Option) -> Result<(), std::io::Error> { - self.stream.set_read_timeout(dur) - } - - pub fn set_write_timeout(&self, dur: Option) -> Result<(), std::io::Error> { - self.stream.set_write_timeout(dur) - } - - pub fn shutdown(&self, how: std::net::Shutdown) -> Result<(), std::io::Error> { - self.stream.shutdown(how) - } - - pub fn take_error(&self) -> Result, std::io::Error> { - self.stream.take_error() - } - - pub fn write_timeout(&self) -> Result, std::io::Error> { - self.stream.write_timeout() - } - - pub fn try_clone(&self) -> Result { - Ok(OnionStream { - stream: self.stream.try_clone()?, - peer_addr: self.peer_addr.clone(), - }) - } -} - -// pass-through to underlying Read stream -impl Read for OnionStream { - fn read(&mut self, buf: &mut [u8]) -> Result { - self.stream.read(buf) - } -} - -// pass-through to underlying Write stream -impl Write for OnionStream { - fn write(&mut self, buf: &[u8]) -> Result { - self.stream.write(buf) - } - - fn flush(&mut self) -> Result<(), std::io::Error> { - self.stream.flush() - } -} - -impl From for TcpStream { - fn from(onion_stream: OnionStream) -> Self { - onion_stream.stream - } -} - -pub struct OnionListener { - listener: TcpListener, - is_active: Arc, -} - -impl OnionListener { - pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { - self.listener.set_nonblocking(nonblocking) - } - - pub fn accept(&self) -> Result, std::io::Error> { - match self.listener.accept() { - Ok((stream, _socket_addr)) => Ok(Some(OnionStream { - stream, - peer_addr: None, - })), - Err(err) => { - if err.kind() == ErrorKind::WouldBlock { - Ok(None) - } else { - Err(err) - } - } - } - } -} - -impl Drop for OnionListener { - fn drop(&mut self) { - self.is_active.store(false, atomic::Ordering::Relaxed); - } -} - -pub enum Event { - BootstrapStatus { - progress: u32, - tag: String, - summary: String, - }, - BootstrapComplete, - LogReceived { - line: String, - }, - OnionServicePublished { - service_id: V3OnionServiceId, - }, -} - -pub struct TorManager { - daemon: TorProcess, - version: TorVersion, - controller: TorController, - socks_listener: Option, - // list of open onion services and their is_active flag - onion_services: Vec<(V3OnionServiceId, Arc)>, -} - -impl TorManager { - pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { - // launch tor - let daemon = - TorProcess::new(tor_bin_path, data_directory).map_err(TorProcessCreationFailed)?; - // open a control stream - let control_stream = ControlStream::new(&daemon.control_addr, Duration::from_millis(16)) - .map_err(ControlStreamCreationFailed)?; - - // create a controler - let mut controller = - TorController::new(control_stream).map_err(TorControllerCreationFailed)?; - - // authenticate - controller - .authenticate(&daemon.password) - .map_err(TorProcessAuthenticationFailed)?; - - let min_required_version: TorVersion = TorVersion { - major: 0u32, - minor: 4u32, - micro: 6u32, - patch_level: 1u32, - status_tag: None, - }; - - let version = controller.getinfo_version().map_err(GetInfoVersionFailed)?; - - if version < min_required_version { - return Err(TorManagerError::TorProcessTooOld( - version.to_string(), - min_required_version.to_string(), - )); - } - - // register for STATUS_CLIENT async events - controller - .setevents(&["STATUS_CLIENT", "HS_DESC"]) - .map_err(SetEventsFailed)?; - - Ok(TorManager { - daemon, - version, - controller, - socks_listener: None, - onion_services: Default::default(), - }) - } - - pub fn update(&mut self) -> Result, TorManagerError> { - let mut i = 0; - while i < self.onion_services.len() { - // remove onion services with no active listeners - if !self.onion_services[i].1.load(atomic::Ordering::Relaxed) { - let entry = self.onion_services.swap_remove(i); - let service_id = entry.0; - - self.controller - .del_onion(&service_id) - .map_err(DelOnionFailed)?; - } else { - i += 1; - } - } - - let mut events: Vec = Default::default(); - for async_event in self - .controller - .wait_async_events() - .map_err(WaitAsyncEventsFailed)? - .iter() - { - match async_event { - AsyncEvent::StatusClient { - severity, - action, - arguments, - } => { - if severity == "NOTICE" && action == "BOOTSTRAP" { - let mut progress: u32 = 0; - let mut tag: String = Default::default(); - let mut summary: String = Default::default(); - for (key, val) in arguments.iter() { - match key.as_str() { - "PROGRESS" => progress = val.parse().unwrap_or(0u32), - "TAG" => tag = val.to_string(), - "SUMMARY" => summary = val.to_string(), - _ => {} // ignore unexpected arguments - } - } - events.push(Event::BootstrapStatus { - progress, - tag, - summary, - }); - if progress == 100u32 { - events.push(Event::BootstrapComplete); - } - } - } - AsyncEvent::HsDesc { action, hs_address } => { - if action == "UPLOADED" { - events.push(Event::OnionServicePublished { - service_id: hs_address.clone(), - }); - } - } - AsyncEvent::Unknown { lines } => { - println!("Received Unknown Event:"); - for line in lines.iter() { - println!(" {}", line); - } - } - } - } - - for log_line in self.daemon.wait_log_lines().iter_mut() { - events.push(Event::LogReceived { - line: std::mem::take(log_line), - }); - } - - Ok(events) - } - - #[allow(dead_code)] - pub fn version(&mut self) -> TorVersion { - self.version.clone() - } - - pub fn bootstrap(&mut self) -> Result<(), TorManagerError> { - self.controller - .setconf(&[("DisableNetwork", "0")]) - .map_err(SetConfDisableNetwork0Failed) - } - - pub fn add_client_auth( - &mut self, - service_id: &V3OnionServiceId, - client_auth: &X25519PrivateKey, - ) -> Result<(), TorManagerError> { - self.controller - .onion_client_auth_add(service_id, client_auth, None, &Default::default()) - .map_err(SetConfDisableNetwork0Failed) - } - - pub fn remove_client_auth( - &mut self, - service_id: &V3OnionServiceId, - ) -> Result<(), TorManagerError> { - self.controller - .onion_client_auth_remove(service_id) - .map_err(OnionClientAuthRemoveFailed) - } - - // connect to an onion service and returns OnionStream - pub fn connect( - &mut self, - service_id: &V3OnionServiceId, - virt_port: u16, - circuit: Option, - ) -> Result { - if self.socks_listener.is_none() { - let mut listeners = self - .controller - .getinfo_net_listeners_socks() - .map_err(GetInfoNetListenersSocksFailed)?; - if listeners.is_empty() { - return Err(TorManagerError::NoSocksListenersFound()); - } - self.socks_listener = Some(listeners.swap_remove(0)); - } - - let socks_listener = match self.socks_listener { - Some(socks_listener) => socks_listener, - None => unreachable!(), - }; - - // our onion domain - let target = - socks::TargetAddr::Domain(format!("{}.onion", service_id.to_string()), virt_port); - // readwrite stream - let stream = match &circuit { - None => Socks5Stream::connect(socks_listener, target), - Some(circuit) => Socks5Stream::connect_with_password( - socks_listener, - target, - &circuit.username, - &circuit.password, - ), - } - .map_err(Socks5ConnectionFailed)?; - - Ok(OnionStream { - stream: stream.into_inner(), - peer_addr: Some(service_id.clone()), - }) - } - - // stand up an onion service and return an OnionListener - pub fn listener( - &mut self, - private_key: &Ed25519PrivateKey, - virt_port: u16, - authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result { - // try to bind to a local address, let OS pick our port - let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); - let listener = TcpListener::bind(socket_addr).map_err(TcpListenerBindFailed)?; - let socket_addr = listener.local_addr().map_err(TcpListenerLocalAddrFailed)?; - - let mut flags = AddOnionFlags { - discard_pk: true, - ..Default::default() - }; - if authorized_clients.is_some() { - flags.v3_auth = true; - } - - // start onion service - let (_, service_id) = self - .controller - .add_onion( - Some(private_key), - &flags, - None, - virt_port, - Some(socket_addr), - authorized_clients, - ) - .map_err(AddOnionFailed)?; - - let is_active = Arc::new(atomic::AtomicBool::new(true)); - self.onion_services - .push((service_id, Arc::clone(&is_active))); - - Ok(OnionListener { - listener, - is_active, - }) - } -} - #[test] #[serial] fn test_tor_controller() -> anyhow::Result<()> { @@ -1775,11 +681,11 @@ fn test_tor_controller() -> anyhow::Result<()> { // create a scope to ensure tor_controller is dropped { let control_stream = - ControlStream::new(&tor_process.control_addr, Duration::from_millis(16))?; + ControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?; // create a tor controller and send authentication command let mut tor_controller = TorController::new(control_stream)?; - tor_controller.authenticate_cmd(&tor_process.password)?; + tor_controller.authenticate_cmd(tor_process.get_password())?; assert!( tor_controller .authenticate_cmd("invalid password")? @@ -1790,7 +696,7 @@ fn test_tor_controller() -> anyhow::Result<()> { // tor controller should have shutdown the connection after failed authentication assert!( tor_controller - .authenticate_cmd(&tor_process.password) + .authenticate_cmd(tor_process.get_password()) .is_err(), "expected failure due to closed connection" ); @@ -1799,12 +705,12 @@ fn test_tor_controller() -> anyhow::Result<()> { // now create a second controller { let control_stream = - ControlStream::new(&tor_process.control_addr, Duration::from_millis(16))?; + ControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?; // create a tor controller and send authentication command // all async events are just printed to stdout let mut tor_controller = TorController::new(control_stream)?; - tor_controller.authenticate(&tor_process.password)?; + tor_controller.authenticate(tor_process.get_password())?; // ensure everything is matching our default_torrc settings let vals = tor_controller.getconf(&["SocksPort", "AvoidDiskWrites", "DisableNetwork"])?; @@ -1902,262 +808,5 @@ fn test_tor_controller() -> anyhow::Result<()> { } } } - - Ok(()) -} - -#[test] -fn test_version() -> anyhow::Result<()> { - assert!(TorVersion::from_str("1.2.3")? == TorVersion::new(1, 2, 3, None, None)?); - assert!(TorVersion::from_str("1.2.3.4")? == TorVersion::new(1, 2, 3, Some(4), None)?); - assert!(TorVersion::from_str("1.2.3-test")? == TorVersion::new(1, 2, 3, None, Some("test"))?); - assert!( - TorVersion::from_str("1.2.3.4-test")? == TorVersion::new(1, 2, 3, Some(4), Some("test"))? - ); - assert!(TorVersion::from_str("1.2.3 (extra_info)")? == TorVersion::new(1, 2, 3, None, None)?); - assert!( - TorVersion::from_str("1.2.3.4 (extra_info)")? == TorVersion::new(1, 2, 3, Some(4), None)? - ); - assert!( - TorVersion::from_str("1.2.3.4-tag (extra_info)")? - == TorVersion::new(1, 2, 3, Some(4), Some("tag"))? - ); - - assert!( - TorVersion::from_str("1.2.3.4-tag (extra_info) (extra_info)")? - == TorVersion::new(1, 2, 3, Some(4), Some("tag"))? - ); - - assert!(TorVersion::new(1, 2, 3, Some(4), Some("spaced tag")).is_err()); - assert!(TorVersion::new(1, 2, 3, Some(4), Some("" /* empty tag */)).is_err()); - assert!(TorVersion::from_str("").is_err()); - assert!(TorVersion::from_str("1.2").is_err()); - assert!(TorVersion::from_str("1.2-foo").is_err()); - assert!(TorVersion::from_str("1.2.3.4-foo bar").is_err()); - assert!(TorVersion::from_str("1.2.3.4-foo bar (extra_info)").is_err()); - assert!(TorVersion::from_str("1.2.3.4-foo (extra_info) badtext").is_err()); - assert!(TorVersion::new(0, 0, 0, Some(0), None)? < TorVersion::new(1, 0, 0, Some(0), None)?); - assert!(TorVersion::new(0, 0, 0, Some(0), None)? < TorVersion::new(0, 1, 0, Some(0), None)?); - assert!(TorVersion::new(0, 0, 0, Some(0), None)? < TorVersion::new(0, 0, 1, Some(0), None)?); - - // ensure status tags make comparison between equal versions (apart from - // tags) unknowable - let zero_version = TorVersion::new(0, 0, 0, Some(0), None)?; - let zero_version_tag = TorVersion::new(0, 0, 0, Some(0), Some("tag"))?; - - assert!(!(zero_version < zero_version_tag)); - assert!(!(zero_version <= zero_version_tag)); - assert!(!(zero_version > zero_version_tag)); - assert!(!(zero_version >= zero_version_tag)); - - Ok(()) -} - -#[test] -#[serial] -fn test_tor_manager() -> anyhow::Result<()> { - let tor_path = which::which(tor_exe_name())?; - let mut data_path = std::env::temp_dir(); - data_path.push("test_tor_manager"); - - let mut tor = TorManager::new(&tor_path, &data_path)?; - println!("version : {}", tor.version().to_string()); - tor.bootstrap()?; - - let mut received_log = false; - let mut bootstrap_complete = false; - while !bootstrap_complete { - for event in tor.update()?.iter() { - match event { - Event::BootstrapStatus { - progress, - tag, - summary, - } => println!( - "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", - progress, tag, summary - ), - Event::BootstrapComplete => { - println!("Bootstrap Complete!"); - bootstrap_complete = true; - } - Event::LogReceived { line } => { - received_log = true; - println!("--- {}", line); - } - _ => {} - } - } - } - assert!( - received_log, - "should have received a log line from tor daemon" - ); - - Ok(()) -} - -#[test] -#[serial] -fn test_onion_service() -> anyhow::Result<()> { - let tor_path = which::which(tor_exe_name())?; - let mut data_path = std::env::temp_dir(); - data_path.push("test_onion_service"); - - let mut tor = TorManager::new(&tor_path, &data_path)?; - - // for 30secs for bootstrap - tor.bootstrap()?; - - let mut bootstrap_complete = false; - while !bootstrap_complete { - for event in tor.update()?.iter() { - match event { - Event::BootstrapStatus { - progress, - tag, - summary, - } => println!( - "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", - progress, tag, summary - ), - Event::BootstrapComplete => { - println!("Bootstrap Complete!"); - bootstrap_complete = true; - } - Event::LogReceived { line } => { - println!("--- {}", line); - } - _ => {} - } - } - } - - // vanilla V3 onion service - { - // create an onion service for this test - let private_key = Ed25519PrivateKey::generate(); - - println!("Starting and listening to onion service"); - const VIRT_PORT: u16 = 42069u16; - let listener = tor.listener(&private_key, VIRT_PORT, None)?; - - let mut onion_published = false; - while !onion_published { - for event in tor.update()?.iter() { - match event { - Event::LogReceived { line } => { - println!("--- {}", line); - } - Event::OnionServicePublished { service_id } => { - let expected_service_id = V3OnionServiceId::from_private_key(&private_key); - if expected_service_id == *service_id { - println!("Onion Service {} published", service_id.to_string()); - onion_published = true; - } - } - _ => {} - } - } - } - - const MESSAGE: &str = "Hello World!"; - - { - let service_id = V3OnionServiceId::from_private_key(&private_key); - - println!("Connecting to onion service"); - let mut client = tor.connect(&service_id, VIRT_PORT, None)?; - println!("Client writing message: '{}'", MESSAGE); - client.write_all(MESSAGE.as_bytes())?; - client.flush()?; - println!("End of client scope"); - } - - if let Some(mut server) = listener.accept()? { - println!("Server reading message"); - let mut buffer = Vec::new(); - server.read_to_end(&mut buffer)?; - let msg = String::from_utf8(buffer)?; - - assert!(MESSAGE == msg); - println!("Message received: '{}'", msg); - } else { - panic!("no listener"); - } - } - - // authenticated onion service - { - // create an onion service for this test - let private_key = Ed25519PrivateKey::generate(); - - let private_auth_key = X25519PrivateKey::generate(); - let public_auth_key = X25519PublicKey::from_private_key(&private_auth_key); - - println!("Starting and listening to authenticated onion service"); - const VIRT_PORT: u16 = 42069u16; - let listener = tor.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]))?; - - let mut onion_published = false; - while !onion_published { - for event in tor.update()?.iter() { - match event { - Event::LogReceived { line } => { - println!("--- {}", line); - } - Event::OnionServicePublished { service_id } => { - let expected_service_id = V3OnionServiceId::from_private_key(&private_key); - if expected_service_id == *service_id { - println!( - "Authenticated Onion Service {} published", - service_id.to_string() - ); - onion_published = true; - } - } - _ => {} - } - } - } - - const MESSAGE: &str = "Hello World!"; - - { - let service_id = V3OnionServiceId::from_private_key(&private_key); - - println!("Connecting to onion service (should fail)"); - assert!( - tor.connect(&service_id, VIRT_PORT, None).is_err(), - "should not able to connect to an authenticated onion service without auth key" - ); - - println!("Add auth key for onion service"); - tor.add_client_auth(&service_id, &private_auth_key)?; - - println!("Connecting to onion service with authentication"); - let mut client = tor.connect(&service_id, VIRT_PORT, None)?; - - println!("Client writing message: '{}'", MESSAGE); - client.write_all(MESSAGE.as_bytes())?; - client.flush()?; - println!("End of client scope"); - - println!("Remove auth key for onion service"); - tor.remove_client_auth(&service_id)?; - } - - if let Some(mut server) = listener.accept()? { - println!("Server reading message"); - let mut buffer = Vec::new(); - server.read_to_end(&mut buffer)?; - let msg = String::from_utf8(buffer)?; - - assert!(MESSAGE == msg); - println!("Message received: '{}'", msg); - } else { - panic!("no listener"); - } - } Ok(()) } diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 5f52e7774..109a89b8a 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -1,5 +1,6 @@ // standard use std::convert::TryInto; +use std::iter; use std::str; // extern crates @@ -8,15 +9,22 @@ use crypto::sha1::Sha1; use crypto::sha3::Sha3; use data_encoding::{BASE32, BASE32_NOPAD, BASE64, HEXUPPER}; use data_encoding_macro::new_encoding; +use rand::distributions::Alphanumeric; use rand::rngs::OsRng; +use rand::Rng; use rand::RngCore; use signature::Verifier; use tor_llcrypto::pk::keymanip::*; use tor_llcrypto::util::rand_compat::RngCompatExt; use tor_llcrypto::*; -// internal modules -use crate::error::TorCryptoError; +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("{0}")] + ParseError(String), + #[error("{0}")] + ConversionError(String), +} /// The number of bytes in an ed25519 secret key /// cbindgen:ignore @@ -95,6 +103,17 @@ fn calc_truncated_checksum( // Free functions +// securely generate password using OsRng +pub(crate) fn generate_password(length: usize) -> String { + let password: String = iter::repeat(()) + .map(|()| OsRng.sample(Alphanumeric)) + .map(char::from) + .take(length) + .collect(); + + password +} + fn hash_tor_password_with_salt(salt: &[u8; S2K_RFC2440_SPECIFIER_LEN], password: &str) -> String { assert!(salt[S2K_RFC2440_SPECIFIER_LEN - 1] == 0x60); @@ -229,9 +248,9 @@ impl Ed25519PrivateKey { } } - pub fn from_key_blob(key_blob: &str) -> Result { + pub fn from_key_blob(key_blob: &str) -> Result { if key_blob.len() != ED25519_PRIVATE_KEYBLOB_LENGTH { - return Err(TorCryptoError::ParseError(format!( + return Err(Error::ParseError(format!( "expects string of length '{}'; received string with length '{}'", ED25519_PRIVATE_KEYBLOB_LENGTH, key_blob.len() @@ -239,7 +258,7 @@ impl Ed25519PrivateKey { } if !key_blob.starts_with(ED25519_PRIVATE_KEYBLOB_HEADER) { - return Err(TorCryptoError::ParseError(format!( + return Err(Error::ParseError(format!( "expects string that begins with '{}'; received '{}'", &ED25519_PRIVATE_KEYBLOB_HEADER, &key_blob ))); @@ -249,7 +268,7 @@ impl Ed25519PrivateKey { let private_key_data = match BASE64.decode(base64_key.as_bytes()) { Ok(private_key_data) => private_key_data, Err(_) => { - return Err(TorCryptoError::ParseError(format!( + return Err(Error::ParseError(format!( "could not parse '{}' as base64", base64_key ))) @@ -260,7 +279,7 @@ impl Ed25519PrivateKey { { Ok(private_key_data) => private_key_data, Err(_) => { - return Err(TorCryptoError::ParseError(format!( + return Err(Error::ParseError(format!( "expects decoded private key length '{}'; actual '{}'", ED25519_PRIVATE_KEY_SIZE, private_key_data_len ))) @@ -272,7 +291,7 @@ impl Ed25519PrivateKey { pub fn from_private_x25519( x25519_private: &X25519PrivateKey, - ) -> Result<(Ed25519PrivateKey, SignBit), TorCryptoError> { + ) -> Result<(Ed25519PrivateKey, SignBit), Error> { if let Some((result, signbit)) = convert_curve25519_to_ed25519_private(&x25519_private.secret_key) { @@ -284,7 +303,7 @@ impl Ed25519PrivateKey { 0u8 => SignBit::Zero, 1u8 => SignBit::One, invalid_signbit => { - return Err(TorCryptoError::ConversionError(format!( + return Err(Error::ConversionError(format!( "convert_curve25519_to_ed25519_private() returned invalid signbit: {}", invalid_signbit ))) @@ -292,7 +311,7 @@ impl Ed25519PrivateKey { }, )) } else { - Err(TorCryptoError::ConversionError( + Err(Error::ConversionError( "could not convert x25519 private key to ed25519 private key".to_string(), )) } @@ -347,14 +366,12 @@ impl std::fmt::Debug for Ed25519PrivateKey { // Ed25519 Public Key impl Ed25519PublicKey { - pub fn from_raw( - raw: &[u8; ED25519_PUBLIC_KEY_SIZE], - ) -> Result { + pub fn from_raw(raw: &[u8; ED25519_PUBLIC_KEY_SIZE]) -> Result { Ok(Ed25519PublicKey { public_key: match pk::ed25519::PublicKey::from_bytes(raw) { Ok(public_key) => public_key, Err(_) => { - return Err(TorCryptoError::ConversionError( + return Err(Error::ConversionError( "failed to create ed25519 public key from bytes".to_string(), )) } @@ -362,23 +379,21 @@ impl Ed25519PublicKey { }) } - pub fn from_service_id( - service_id: &V3OnionServiceId, - ) -> Result { + pub fn from_service_id(service_id: &V3OnionServiceId) -> Result { // decode base32 encoded service id let mut decoded_service_id = [0u8; V3_ONION_SERVICE_ID_RAW_SIZE]; let decoded_byte_count = match ONION_BASE32.decode_mut(service_id.as_bytes(), &mut decoded_service_id) { Ok(decoded_byte_count) => decoded_byte_count, Err(_) => { - return Err(TorCryptoError::ConversionError(format!( + return Err(Error::ConversionError(format!( "failed to decode '{}' as V3OnionServiceId", service_id.to_string() ))) } }; if decoded_byte_count != V3_ONION_SERVICE_ID_RAW_SIZE { - return Err(TorCryptoError::ConversionError(format!( + return Err(Error::ConversionError(format!( "decoded byte count is '{}', expected '{}'", decoded_byte_count, V3_ONION_SERVICE_ID_RAW_SIZE ))); @@ -400,10 +415,10 @@ impl Ed25519PublicKey { fn from_public_x25519( public_x25519: &X25519PublicKey, signbit: SignBit, - ) -> Result { + ) -> Result { match convert_curve25519_to_ed25519_public(&public_x25519.public_key, signbit.into()) { Some(public_key) => Ok(Ed25519PublicKey { public_key }), - None => Err(TorCryptoError::ConversionError( + None => Err(Error::ConversionError( "failed to create ed25519 public key from x25519 public key and signbit" .to_string(), )), @@ -428,14 +443,12 @@ impl PartialEq for Ed25519PublicKey { // Ed25519 Signature impl Ed25519Signature { - pub fn from_raw( - raw: &[u8; ED25519_SIGNATURE_SIZE], - ) -> Result { + pub fn from_raw(raw: &[u8; ED25519_SIGNATURE_SIZE]) -> Result { Ok(Ed25519Signature { signature: match pk::ed25519::Signature::from_bytes(raw) { Ok(signature) => signature, Err(_) => { - return Err(TorCryptoError::ConversionError( + return Err(Error::ConversionError( "failed to create ed25519 signature from bytes".to_string(), )) } @@ -491,9 +504,9 @@ impl X25519PrivateKey { } // a base64 encoded keyblob - pub fn from_base64(base64: &str) -> Result { + pub fn from_base64(base64: &str) -> Result { if base64.len() != X25519_PRIVATE_KEYBLOB_BASE64_LENGTH { - return Err(TorCryptoError::ParseError(format!( + return Err(Error::ParseError(format!( "expects string of length '{}'; received string with length '{}'", X25519_PRIVATE_KEYBLOB_BASE64_LENGTH, base64.len() @@ -503,7 +516,7 @@ impl X25519PrivateKey { let private_key_data = match BASE64.decode(base64.as_bytes()) { Ok(private_key_data) => private_key_data, Err(_) => { - return Err(TorCryptoError::ParseError(format!( + return Err(Error::ParseError(format!( "could not parse '{}' as base64", base64 ))) @@ -514,7 +527,7 @@ impl X25519PrivateKey { { Ok(private_key_data) => private_key_data, Err(_) => { - return Err(TorCryptoError::ParseError(format!( + return Err(Error::ParseError(format!( "expects decoded private key length '{}'; actual '{}'", X25519_PRIVATE_KEY_SIZE, private_key_data_len ))) @@ -528,10 +541,7 @@ impl X25519PrivateKey { // this function first derives an ed25519 private key from the provided x25519 private key // and signs the message, returning the signature and signbit needed to calculate the // ed25519 public key from our x25519 private key's associated x25519 public key - pub fn sign_message( - &self, - message: &[u8], - ) -> Result<(Ed25519Signature, SignBit), TorCryptoError> { + pub fn sign_message(&self, message: &[u8]) -> Result<(Ed25519Signature, SignBit), Error> { let (ed25519_private, signbit) = Ed25519PrivateKey::from_private_x25519(self)?; Ok((ed25519_private.sign_message(message), signbit)) } @@ -565,9 +575,9 @@ impl X25519PublicKey { } } - pub fn from_base32(base32: &str) -> Result { + pub fn from_base32(base32: &str) -> Result { if base32.len() != X25519_PUBLIC_KEYBLOB_BASE32_LENGTH { - return Err(TorCryptoError::ParseError(format!( + return Err(Error::ParseError(format!( "expects string of length '{}'; received '{}' with length '{}'", X25519_PUBLIC_KEYBLOB_BASE32_LENGTH, base32, @@ -578,7 +588,7 @@ impl X25519PublicKey { let public_key_data = match BASE32_NOPAD.decode(base32.as_bytes()) { Ok(public_key_data) => public_key_data, Err(_) => { - return Err(TorCryptoError::ParseError(format!( + return Err(Error::ParseError(format!( "failed to decode '{}' as X25519PublicKey", base32 ))) @@ -588,7 +598,7 @@ impl X25519PublicKey { let public_key_data_raw: [u8; X25519_PUBLIC_KEY_SIZE] = match public_key_data.try_into() { Ok(public_key_data) => public_key_data, Err(_) => { - return Err(TorCryptoError::ParseError(format!( + return Err(Error::ParseError(format!( "expects decoded public key length '{}'; actual '{}'", X25519_PUBLIC_KEY_SIZE, public_key_data_len ))) @@ -620,9 +630,9 @@ impl std::fmt::Debug for X25519PublicKey { // Onion Service Id impl V3OnionServiceId { - pub fn from_string(service_id: &str) -> Result { + pub fn from_string(service_id: &str) -> Result { if !V3OnionServiceId::is_valid(service_id) { - return Err(TorCryptoError::ParseError(format!( + return Err(Error::ParseError(format!( "'{}' is not a valid v3 onion service id", service_id ))); diff --git a/tor-interface/src/tor_manager.rs b/tor-interface/src/tor_manager.rs new file mode 100644 index 000000000..dadb368a0 --- /dev/null +++ b/tor-interface/src/tor_manager.rs @@ -0,0 +1,685 @@ +// standard +use std::default::Default; +use std::io::{ErrorKind, Read, Write}; +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::ops::Drop; +use std::option::Option; +use std::path::Path; +use std::string::ToString; +use std::sync::{atomic, Arc}; +use std::time::Duration; + +// extern crates +#[cfg(test)] +use serial_test::serial; +use socks::Socks5Stream; +use url::Host; + +// internal crates +use crate::tor_control_stream::*; +use crate::tor_controller::*; +use crate::tor_crypto::*; +use crate::tor_process::*; +use crate::tor_version::*; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("failed to create TorProcess object")] + TorProcessCreationFailed(#[source] crate::tor_process::Error), + + #[error("failed to create ControlStream object")] + ControlStreamCreationFailed(#[source] crate::tor_control_stream::Error), + + #[error("failed to create TorController object")] + TorControllerCreationFailed(#[source] crate::tor_controller::Error), + + #[error("failed to authenticate with the tor process")] + TorProcessAuthenticationFailed(#[source] crate::tor_controller::Error), + + #[error("failed to determine the tor process version")] + GetInfoVersionFailed(#[source] crate::tor_controller::Error), + + #[error("tor process version to old; found {0} but must be at least {1}")] + TorProcessTooOld(String, String), + + #[error("failed to register for STATUS_CLIENT and HS_DESC events")] + SetEventsFailed(#[source] crate::tor_controller::Error), + + #[error("failed to delete unused onion service")] + DelOnionFailed(#[source] crate::tor_controller::Error), + + #[error("failed waiting for async events")] + WaitAsyncEventsFailed(#[source] crate::tor_controller::Error), + + #[error("failed to begin bootstrap")] + SetConfDisableNetwork0Failed(#[source] crate::tor_controller::Error), + + #[error("failed to add client auth for onion service")] + OnionClientAuthAddFailed(#[source] crate::tor_controller::Error), + + #[error("failed to remove client auth from onion service")] + OnionClientAuthRemoveFailed(#[source] crate::tor_controller::Error), + + #[error("failed to get socks listener")] + GetInfoNetListenersSocksFailed(#[source] crate::tor_controller::Error), + + #[error("no socks listeners available to connect through")] + NoSocksListenersFound(), + + #[error("unable to connect to socks listener")] + Socks5ConnectionFailed(#[source] std::io::Error), + + #[error("unable to bind TCP listener")] + TcpListenerBindFailed(#[source] std::io::Error), + + #[error("unable to get TCP listener's local address")] + TcpListenerLocalAddrFailed(#[source] std::io::Error), + + #[error("failed to create onion service")] + AddOnionFailed(#[source] crate::tor_controller::Error), +} + +pub struct CircuitToken { + username: String, + password: String, +} + +impl CircuitToken { + #[allow(dead_code)] + pub fn new(first_party: Host) -> CircuitToken { + const CIRCUIT_TOKEN_PASSWORD_LENGTH: usize = 32usize; + let username = first_party.to_string(); + let password = generate_password(CIRCUIT_TOKEN_PASSWORD_LENGTH); + + CircuitToken { username, password } + } +} + +pub struct OnionStream { + stream: TcpStream, + peer_addr: Option, +} + +impl OnionStream { + pub fn nodelay(&self) -> Result { + self.stream.nodelay() + } + + pub fn peer_addr(&self) -> Option<&V3OnionServiceId> { + self.peer_addr.as_ref() + } + + pub fn read_timeout(&self) -> Result, std::io::Error> { + self.stream.read_timeout() + } + + pub fn set_nodelay(&self, nodelay: bool) -> Result<(), std::io::Error> { + self.stream.set_nodelay(nodelay) + } + + pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { + self.stream.set_nonblocking(nonblocking) + } + + pub fn set_read_timeout(&self, dur: Option) -> Result<(), std::io::Error> { + self.stream.set_read_timeout(dur) + } + + pub fn set_write_timeout(&self, dur: Option) -> Result<(), std::io::Error> { + self.stream.set_write_timeout(dur) + } + + pub fn shutdown(&self, how: std::net::Shutdown) -> Result<(), std::io::Error> { + self.stream.shutdown(how) + } + + pub fn take_error(&self) -> Result, std::io::Error> { + self.stream.take_error() + } + + pub fn write_timeout(&self) -> Result, std::io::Error> { + self.stream.write_timeout() + } + + pub fn try_clone(&self) -> Result { + Ok(OnionStream { + stream: self.stream.try_clone()?, + peer_addr: self.peer_addr.clone(), + }) + } +} + +// pass-through to underlying Read stream +impl Read for OnionStream { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.stream.read(buf) + } +} + +// pass-through to underlying Write stream +impl Write for OnionStream { + fn write(&mut self, buf: &[u8]) -> Result { + self.stream.write(buf) + } + + fn flush(&mut self) -> Result<(), std::io::Error> { + self.stream.flush() + } +} + +impl From for TcpStream { + fn from(onion_stream: OnionStream) -> Self { + onion_stream.stream + } +} + +pub struct OnionListener { + listener: TcpListener, + is_active: Arc, +} + +impl OnionListener { + pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { + self.listener.set_nonblocking(nonblocking) + } + + pub fn accept(&self) -> Result, std::io::Error> { + match self.listener.accept() { + Ok((stream, _socket_addr)) => Ok(Some(OnionStream { + stream, + peer_addr: None, + })), + Err(err) => { + if err.kind() == ErrorKind::WouldBlock { + Ok(None) + } else { + Err(err) + } + } + } + } +} + +impl Drop for OnionListener { + fn drop(&mut self) { + self.is_active.store(false, atomic::Ordering::Relaxed); + } +} + +pub enum Event { + BootstrapStatus { + progress: u32, + tag: String, + summary: String, + }, + BootstrapComplete, + LogReceived { + line: String, + }, + OnionServicePublished { + service_id: V3OnionServiceId, + }, +} + +pub struct TorManager { + daemon: TorProcess, + version: TorVersion, + controller: TorController, + socks_listener: Option, + // list of open onion services and their is_active flag + onion_services: Vec<(V3OnionServiceId, Arc)>, +} + +impl TorManager { + pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { + // launch tor + let daemon = TorProcess::new(tor_bin_path, data_directory) + .map_err(Error::TorProcessCreationFailed)?; + // open a control stream + let control_stream = + ControlStream::new(daemon.get_control_addr(), Duration::from_millis(16)) + .map_err(Error::ControlStreamCreationFailed)?; + + // create a controler + let mut controller = + TorController::new(control_stream).map_err(Error::TorControllerCreationFailed)?; + + // authenticate + controller + .authenticate(daemon.get_password()) + .map_err(Error::TorProcessAuthenticationFailed)?; + + let min_required_version: TorVersion = TorVersion { + major: 0u32, + minor: 4u32, + micro: 6u32, + patch_level: 1u32, + status_tag: None, + }; + + let version = controller + .getinfo_version() + .map_err(Error::GetInfoVersionFailed)?; + + if version < min_required_version { + return Err(Error::TorProcessTooOld( + version.to_string(), + min_required_version.to_string(), + )); + } + + // register for STATUS_CLIENT async events + controller + .setevents(&["STATUS_CLIENT", "HS_DESC"]) + .map_err(Error::SetEventsFailed)?; + + Ok(TorManager { + daemon, + version, + controller, + socks_listener: None, + onion_services: Default::default(), + }) + } + + pub fn update(&mut self) -> Result, Error> { + let mut i = 0; + while i < self.onion_services.len() { + // remove onion services with no active listeners + if !self.onion_services[i].1.load(atomic::Ordering::Relaxed) { + let entry = self.onion_services.swap_remove(i); + let service_id = entry.0; + + self.controller + .del_onion(&service_id) + .map_err(Error::DelOnionFailed)?; + } else { + i += 1; + } + } + + let mut events: Vec = Default::default(); + for async_event in self + .controller + .wait_async_events() + .map_err(Error::WaitAsyncEventsFailed)? + .iter() + { + match async_event { + AsyncEvent::StatusClient { + severity, + action, + arguments, + } => { + if severity == "NOTICE" && action == "BOOTSTRAP" { + let mut progress: u32 = 0; + let mut tag: String = Default::default(); + let mut summary: String = Default::default(); + for (key, val) in arguments.iter() { + match key.as_str() { + "PROGRESS" => progress = val.parse().unwrap_or(0u32), + "TAG" => tag = val.to_string(), + "SUMMARY" => summary = val.to_string(), + _ => {} // ignore unexpected arguments + } + } + events.push(Event::BootstrapStatus { + progress, + tag, + summary, + }); + if progress == 100u32 { + events.push(Event::BootstrapComplete); + } + } + } + AsyncEvent::HsDesc { action, hs_address } => { + if action == "UPLOADED" { + events.push(Event::OnionServicePublished { + service_id: hs_address.clone(), + }); + } + } + AsyncEvent::Unknown { lines } => { + println!("Received Unknown Event:"); + for line in lines.iter() { + println!(" {}", line); + } + } + } + } + + for log_line in self.daemon.wait_log_lines().iter_mut() { + events.push(Event::LogReceived { + line: std::mem::take(log_line), + }); + } + + Ok(events) + } + + #[allow(dead_code)] + pub fn version(&mut self) -> TorVersion { + self.version.clone() + } + + pub fn bootstrap(&mut self) -> Result<(), Error> { + self.controller + .setconf(&[("DisableNetwork", "0")]) + .map_err(Error::SetConfDisableNetwork0Failed) + } + + pub fn add_client_auth( + &mut self, + service_id: &V3OnionServiceId, + client_auth: &X25519PrivateKey, + ) -> Result<(), Error> { + self.controller + .onion_client_auth_add(service_id, client_auth, None, &Default::default()) + .map_err(Error::OnionClientAuthAddFailed) + } + + pub fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> { + self.controller + .onion_client_auth_remove(service_id) + .map_err(Error::OnionClientAuthRemoveFailed) + } + + // connect to an onion service and returns OnionStream + pub fn connect( + &mut self, + service_id: &V3OnionServiceId, + virt_port: u16, + circuit: Option, + ) -> Result { + if self.socks_listener.is_none() { + let mut listeners = self + .controller + .getinfo_net_listeners_socks() + .map_err(Error::GetInfoNetListenersSocksFailed)?; + if listeners.is_empty() { + return Err(Error::NoSocksListenersFound()); + } + self.socks_listener = Some(listeners.swap_remove(0)); + } + + let socks_listener = match self.socks_listener { + Some(socks_listener) => socks_listener, + None => unreachable!(), + }; + + // our onion domain + let target = + socks::TargetAddr::Domain(format!("{}.onion", service_id.to_string()), virt_port); + // readwrite stream + let stream = match &circuit { + None => Socks5Stream::connect(socks_listener, target), + Some(circuit) => Socks5Stream::connect_with_password( + socks_listener, + target, + &circuit.username, + &circuit.password, + ), + } + .map_err(Error::Socks5ConnectionFailed)?; + + Ok(OnionStream { + stream: stream.into_inner(), + peer_addr: Some(service_id.clone()), + }) + } + + // stand up an onion service and return an OnionListener + pub fn listener( + &mut self, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorized_clients: Option<&[X25519PublicKey]>, + ) -> Result { + // try to bind to a local address, let OS pick our port + let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); + let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; + let socket_addr = listener + .local_addr() + .map_err(Error::TcpListenerLocalAddrFailed)?; + + let mut flags = AddOnionFlags { + discard_pk: true, + ..Default::default() + }; + if authorized_clients.is_some() { + flags.v3_auth = true; + } + + // start onion service + let (_, service_id) = self + .controller + .add_onion( + Some(private_key), + &flags, + None, + virt_port, + Some(socket_addr), + authorized_clients, + ) + .map_err(Error::AddOnionFailed)?; + + let is_active = Arc::new(atomic::AtomicBool::new(true)); + self.onion_services + .push((service_id, Arc::clone(&is_active))); + + Ok(OnionListener { + listener, + is_active, + }) + } +} + +#[test] +#[serial] +fn test_tor_manager() -> anyhow::Result<()> { + let tor_path = which::which(tor_exe_name())?; + let mut data_path = std::env::temp_dir(); + data_path.push("test_tor_manager"); + + let mut tor = TorManager::new(&tor_path, &data_path)?; + println!("version : {}", tor.version().to_string()); + tor.bootstrap()?; + + let mut received_log = false; + let mut bootstrap_complete = false; + while !bootstrap_complete { + for event in tor.update()?.iter() { + match event { + Event::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + Event::BootstrapComplete => { + println!("Bootstrap Complete!"); + bootstrap_complete = true; + } + Event::LogReceived { line } => { + received_log = true; + println!("--- {}", line); + } + _ => {} + } + } + } + assert!( + received_log, + "should have received a log line from tor daemon" + ); + + Ok(()) +} + +#[test] +#[serial] +fn test_onion_service() -> anyhow::Result<()> { + let tor_path = which::which(tor_exe_name())?; + let mut data_path = std::env::temp_dir(); + data_path.push("test_onion_service"); + + let mut tor = TorManager::new(&tor_path, &data_path)?; + + // for 30secs for bootstrap + tor.bootstrap()?; + + let mut bootstrap_complete = false; + while !bootstrap_complete { + for event in tor.update()?.iter() { + match event { + Event::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + Event::BootstrapComplete => { + println!("Bootstrap Complete!"); + bootstrap_complete = true; + } + Event::LogReceived { line } => { + println!("--- {}", line); + } + _ => {} + } + } + } + + // vanilla V3 onion service + { + // create an onion service for this test + let private_key = Ed25519PrivateKey::generate(); + + println!("Starting and listening to onion service"); + const VIRT_PORT: u16 = 42069u16; + let listener = tor.listener(&private_key, VIRT_PORT, None)?; + + let mut onion_published = false; + while !onion_published { + for event in tor.update()?.iter() { + match event { + Event::LogReceived { line } => { + println!("--- {}", line); + } + Event::OnionServicePublished { service_id } => { + let expected_service_id = V3OnionServiceId::from_private_key(&private_key); + if expected_service_id == *service_id { + println!("Onion Service {} published", service_id.to_string()); + onion_published = true; + } + } + _ => {} + } + } + } + + const MESSAGE: &str = "Hello World!"; + + { + let service_id = V3OnionServiceId::from_private_key(&private_key); + + println!("Connecting to onion service"); + let mut client = tor.connect(&service_id, VIRT_PORT, None)?; + println!("Client writing message: '{}'", MESSAGE); + client.write_all(MESSAGE.as_bytes())?; + client.flush()?; + println!("End of client scope"); + } + + if let Some(mut server) = listener.accept()? { + println!("Server reading message"); + let mut buffer = Vec::new(); + server.read_to_end(&mut buffer)?; + let msg = String::from_utf8(buffer)?; + + assert!(MESSAGE == msg); + println!("Message received: '{}'", msg); + } else { + panic!("no listener"); + } + } + + // authenticated onion service + { + // create an onion service for this test + let private_key = Ed25519PrivateKey::generate(); + + let private_auth_key = X25519PrivateKey::generate(); + let public_auth_key = X25519PublicKey::from_private_key(&private_auth_key); + + println!("Starting and listening to authenticated onion service"); + const VIRT_PORT: u16 = 42069u16; + let listener = tor.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]))?; + + let mut onion_published = false; + while !onion_published { + for event in tor.update()?.iter() { + match event { + Event::LogReceived { line } => { + println!("--- {}", line); + } + Event::OnionServicePublished { service_id } => { + let expected_service_id = V3OnionServiceId::from_private_key(&private_key); + if expected_service_id == *service_id { + println!( + "Authenticated Onion Service {} published", + service_id.to_string() + ); + onion_published = true; + } + } + _ => {} + } + } + } + + const MESSAGE: &str = "Hello World!"; + + { + let service_id = V3OnionServiceId::from_private_key(&private_key); + + println!("Connecting to onion service (should fail)"); + assert!( + tor.connect(&service_id, VIRT_PORT, None).is_err(), + "should not able to connect to an authenticated onion service without auth key" + ); + + println!("Add auth key for onion service"); + tor.add_client_auth(&service_id, &private_auth_key)?; + + println!("Connecting to onion service with authentication"); + let mut client = tor.connect(&service_id, VIRT_PORT, None)?; + + println!("Client writing message: '{}'", MESSAGE); + client.write_all(MESSAGE.as_bytes())?; + client.flush()?; + println!("End of client scope"); + + println!("Remove auth key for onion service"); + tor.remove_client_auth(&service_id)?; + } + + if let Some(mut server) = listener.accept()? { + println!("Server reading message"); + let mut buffer = Vec::new(); + server.read_to_end(&mut buffer)?; + let msg = String::from_utf8(buffer)?; + + assert!(MESSAGE == msg); + println!("Message received: '{}'", msg); + } else { + panic!("no listener"); + } + } + Ok(()) +} diff --git a/tor-interface/src/tor_process.rs b/tor-interface/src/tor_process.rs new file mode 100644 index 000000000..9223e9d67 --- /dev/null +++ b/tor-interface/src/tor_process.rs @@ -0,0 +1,297 @@ +// standard +use std::default::Default; +use std::fs; +use std::fs::File; +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::SocketAddr; +use std::ops::Drop; +use std::path::Path; +use std::process; +use std::process::{Child, ChildStdout, Command, Stdio}; +use std::str::FromStr; +use std::string::ToString; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +// internal crates +use crate::tor_crypto::*; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("failed to read control port file")] + ControlPortFileReadFailed(#[source] std::io::Error), + + #[error("provided control port file '{0}' larger than expected ({1} bytes)")] + ControlPortFileTooLarge(String, u64), + + #[error("failed to parse '{0}' as control port file")] + ControlPortFileContentsInvalid(String), + + #[error("provided tor bin path '{0}' must be an absolute path")] + TorBinPathNotAbsolute(String), + + #[error("provided data directory '{0}' must be an absolute path")] + TorDataDirectoryPathNotAbsolute(String), + + #[error("failed to create data directory")] + DataDirectoryCreationFailed(#[source] std::io::Error), + + #[error("file exists in provided data directory path '{0}'")] + DataDirectoryPathExistsAsFile(String), + + #[error("failed to create default_torrc file")] + DefaultTorrcFileCreationFailed(#[source] std::io::Error), + + #[error("failed to write default_torrc file")] + DefaultTorrcFileWriteFailed(#[source] std::io::Error), + + #[error("failed to create torrc file")] + TorrcFileCreationFailed(#[source] std::io::Error), + + #[error("failed to remove control_port file")] + ControlPortFileDeleteFailed(#[source] std::io::Error), + + #[error("failed to start tor process")] + TorProcessStartFailed(#[source] std::io::Error), + + #[error("failed to read control addr from control_file '{0}'")] + ControlPortFileMissing(String), + + #[error("unable to take tor process stdout")] + TorProcessStdoutTakeFailed(), + + #[error("failed to spawn tor process stdout read thread")] + StdoutReadThreadSpawnFailed(#[source] std::io::Error), +} + +// get the name of our tor executable +pub const fn tor_exe_name() -> &'static str { + if cfg!(windows) { + "tor.exe" + } else { + "tor" + } +} + +fn read_control_port_file(control_port_file: &Path) -> Result { + // open file + let mut file = File::open(control_port_file).map_err(Error::ControlPortFileReadFailed)?; + + // bail if the file is larger than expected + let metadata = file.metadata().map_err(Error::ControlPortFileReadFailed)?; + if metadata.len() >= 1024 { + return Err(Error::ControlPortFileTooLarge( + format!("{}", control_port_file.display()), + metadata.len(), + )); + } + + // read contents to string + let mut contents = String::new(); + file.read_to_string(&mut contents) + .map_err(Error::ControlPortFileReadFailed)?; + + if contents.starts_with("PORT=") { + let addr_string = &contents.trim_end()["PORT=".len()..]; + if let Ok(addr) = SocketAddr::from_str(addr_string) { + return Ok(addr); + } + } + Err(Error::ControlPortFileContentsInvalid(format!( + "{}", + control_port_file.display() + ))) +} + +// Encapsulates the tor daemon process +pub(crate) struct TorProcess { + control_addr: SocketAddr, + process: Child, + password: String, + // stdout data + stdout_lines: Arc>>, +} + +impl TorProcess { + pub fn get_control_addr(&self) -> &SocketAddr { + return &self.control_addr; + } + + pub fn get_password(&self) -> &String { + return &self.password; + } + + pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { + if tor_bin_path.is_relative() { + return Err(Error::TorBinPathNotAbsolute(format!( + "{}", + tor_bin_path.display() + ))); + } + if data_directory.is_relative() { + return Err(Error::TorDataDirectoryPathNotAbsolute(format!( + "{}", + data_directory.display() + ))); + } + + // create data directory if it doesn't exist + if !data_directory.exists() { + fs::create_dir_all(data_directory).map_err(Error::DataDirectoryCreationFailed)?; + } else if data_directory.is_file() { + return Err(Error::DataDirectoryPathExistsAsFile(format!( + "{}", + data_directory.display() + ))); + } + + // construct paths to torrc files + let default_torrc = data_directory.join("default_torrc"); + let torrc = data_directory.join("torrc"); + let control_port_file = data_directory.join("control_port"); + + // TODO: should we nuke the existing torrc between runs? Do we want + // users setting custom nonsense in there? + // construct default torrc + // - daemon determines socks port and only allows clients to connect to onion services + // - minimize writes to disk + // - start with network disabled by default + if !default_torrc.exists() { + const DEFAULT_TORRC_CONTENT: &str = "SocksPort auto OnionTrafficOnly\n\ + AvoidDiskWrites 1\n\ + DisableNetwork 1\n\n"; + + let mut default_torrc_file = + File::create(&default_torrc).map_err(Error::DefaultTorrcFileCreationFailed)?; + default_torrc_file + .write_all(DEFAULT_TORRC_CONTENT.as_bytes()) + .map_err(Error::DefaultTorrcFileWriteFailed)?; + } + + // create empty torrc for user + if !torrc.exists() { + let _ = File::create(&torrc).map_err(Error::TorrcFileCreationFailed)?; + } + + // remove any existing control_port_file + if control_port_file.exists() { + fs::remove_file(&control_port_file).map_err(Error::ControlPortFileDeleteFailed)?; + } + + const CONTROL_PORT_PASSWORD_LENGTH: usize = 32usize; + let password = generate_password(CONTROL_PORT_PASSWORD_LENGTH); + let password_hash = hash_tor_password(&password); + + let mut process = Command::new(tor_bin_path.as_os_str()) + .stdout(Stdio::piped()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + // point to our above written torrc file + .arg("--defaults-torrc") + .arg(default_torrc) + // location of torrc + .arg("--torrc-file") + .arg(torrc) + // root data directory + .arg("DataDirectory") + .arg(data_directory) + // daemon will assign us a port, and we will + // read it from the control port file + .arg("ControlPort") + .arg("auto") + // control port file destination + .arg("ControlPortWriteToFile") + .arg(control_port_file.clone()) + // use password authentication to prevent other apps + // from modifying our daemon's settings + .arg("HashedControlPassword") + .arg(password_hash) + // tor process will shut down after this process shuts down + // to avoid orphaned tor daemon + .arg("__OwningControllerProcess") + .arg(process::id().to_string()) + .spawn() + .map_err(Error::TorProcessStartFailed)?; + + let mut control_addr = None; + let start = Instant::now(); + + // try and read the control port from the control port file + // or abort after 5 seconds + // TODO: make this timeout configurable? + while control_addr.is_none() && start.elapsed() < Duration::from_secs(5) { + if control_port_file.exists() { + control_addr = Some(read_control_port_file(control_port_file.as_path())?); + fs::remove_file(&control_port_file).map_err(Error::ControlPortFileDeleteFailed)?; + } + } + + let control_addr = match control_addr { + Some(control_addr) => control_addr, + None => { + return Err(Error::ControlPortFileMissing(format!( + "{}", + control_port_file.display() + ))) + } + }; + + let stdout_lines: Arc>> = Default::default(); + + { + let stdout_lines = Arc::downgrade(&stdout_lines); + let stdout = BufReader::new(match process.stdout.take() { + Some(stdout) => stdout, + None => return Err(Error::TorProcessStdoutTakeFailed()), + }); + + std::thread::Builder::new() + .name("tor_stdout_reader".to_string()) + .spawn(move || { + TorProcess::read_stdout_task(&stdout_lines, stdout); + }) + .map_err(Error::StdoutReadThreadSpawnFailed)?; + } + + Ok(TorProcess { + control_addr, + process, + password, + stdout_lines, + }) + } + + fn read_stdout_task( + stdout_lines: &std::sync::Weak>>, + mut stdout: BufReader, + ) { + while let Some(stdout_lines) = stdout_lines.upgrade() { + let mut line = String::default(); + // read line + if stdout.read_line(&mut line).is_ok() { + // remove trailing '\n' + line.pop(); + // then acquire the lock on the line buffer + let mut stdout_lines = match stdout_lines.lock() { + Ok(stdout_lines) => stdout_lines, + Err(_) => unreachable!(), + }; + stdout_lines.push(line); + } + } + } + + pub fn wait_log_lines(&mut self) -> Vec { + let mut lines = match self.stdout_lines.lock() { + Ok(lines) => lines, + Err(_) => unreachable!(), + }; + std::mem::take(&mut lines) + } +} + +impl Drop for TorProcess { + fn drop(&mut self) { + let _ = self.process.kill(); + } +} diff --git a/tor-interface/src/tor_version.rs b/tor-interface/src/tor_version.rs new file mode 100644 index 000000000..7e00ed113 --- /dev/null +++ b/tor-interface/src/tor_version.rs @@ -0,0 +1,276 @@ +// standard +use std::cmp::Ordering; +use std::option::Option; +use std::str::FromStr; +use std::string::ToString; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("{}", .0)] + ParseError(String), +} + +// see version-spec.txt +#[derive(Clone)] +pub struct TorVersion { + pub major: u32, + pub minor: u32, + pub micro: u32, + pub patch_level: u32, + pub status_tag: Option, +} + +impl TorVersion { + fn status_tag_pattern_is_match(status_tag: &str) -> bool { + if status_tag.is_empty() { + return false; + } + + for c in status_tag.chars() { + if c.is_whitespace() { + return false; + } + } + true + } + + fn new( + major: u32, + minor: u32, + micro: u32, + patch_level: Option, + status_tag: Option<&str>, + ) -> Result { + let status_tag = if let Some(status_tag) = status_tag { + if Self::status_tag_pattern_is_match(status_tag) { + Some(status_tag.to_string()) + } else { + return Err(Error::ParseError( + "tor version status tag may not be empty or contain white-space".to_string(), + )); + } + } else { + None + }; + + Ok(TorVersion { + major, + minor, + micro, + patch_level: patch_level.unwrap_or(0u32), + status_tag, + }) + } +} + +impl FromStr for TorVersion { + type Err = Error; + + fn from_str(s: &str) -> Result { + // MAJOR.MINOR.MICRO[.PATCHLEVEL][-STATUS_TAG][ (EXTRA_INFO)]* + let mut tokens = s.split(' '); + let (major, minor, micro, patch_level, status_tag) = + if let Some(version_status_tag) = tokens.next() { + let mut tokens = version_status_tag.split('-'); + let (major, minor, micro, patch_level) = if let Some(version) = tokens.next() { + let mut tokens = version.split('.'); + let major: u32 = if let Some(major) = tokens.next() { + match major.parse() { + Ok(major) => major, + Err(_) => { + return Err(Error::ParseError(format!( + "failed to parse '{}' as MAJOR portion of tor version", + major + ))) + } + } + } else { + return Err(Error::ParseError( + "failed to find MAJOR portion of tor version".to_string(), + )); + }; + let minor: u32 = if let Some(minor) = tokens.next() { + match minor.parse() { + Ok(minor) => minor, + Err(_) => { + return Err(Error::ParseError(format!( + "failed to parse '{}' as MINOR portion of tor version", + minor + ))) + } + } + } else { + return Err(Error::ParseError( + "failed to find MINOR portion of tor version".to_string(), + )); + }; + let micro: u32 = if let Some(micro) = tokens.next() { + match micro.parse() { + Ok(micro) => micro, + Err(_) => { + return Err(Error::ParseError(format!( + "failed to parse '{}' as MICRO portion of tor version", + micro + ))) + } + } + } else { + return Err(Error::ParseError( + "failed to find MICRO portion of tor version".to_string(), + )); + }; + let patch_level: u32 = if let Some(patch_level) = tokens.next() { + match patch_level.parse() { + Ok(patch_level) => patch_level, + Err(_) => { + return Err(Error::ParseError(format!( + "failed to parse '{}' as PATCHLEVEL portion of tor version", + patch_level + ))) + } + } + } else { + 0u32 + }; + (major, minor, micro, patch_level) + } else { + // if there were '-' the previous next() would have returned the enire string + unreachable!(); + }; + let status_tag = tokens.next().map(|status_tag| status_tag.to_string()); + + (major, minor, micro, patch_level, status_tag) + } else { + // if there were no ' ' character the previou snext() would have returned the enire string + unreachable!(); + }; + for extra_info in tokens { + if !extra_info.starts_with('(') || !extra_info.ends_with(')') { + return Err(Error::ParseError(format!( + "failed to parse '{}' as [ (EXTRA_INFO)]", + extra_info + ))); + } + } + TorVersion::new( + major, + minor, + micro, + Some(patch_level), + status_tag.as_deref(), + ) + } +} + +impl ToString for TorVersion { + fn to_string(&self) -> String { + match &self.status_tag { + Some(status_tag) => format!( + "{}.{}.{}.{}-{}", + self.major, self.minor, self.micro, self.patch_level, status_tag + ), + None => format!( + "{}.{}.{}.{}", + self.major, self.minor, self.micro, self.patch_level + ), + } + } +} + +impl PartialEq for TorVersion { + fn eq(&self, other: &Self) -> bool { + self.major == other.major + && self.minor == other.minor + && self.micro == other.micro + && self.patch_level == other.patch_level + && self.status_tag == other.status_tag + } +} + +impl PartialOrd for TorVersion { + fn partial_cmp(&self, other: &Self) -> Option { + if let Some(order) = self.major.partial_cmp(&other.major) { + if order != Ordering::Equal { + return Some(order); + } + } + + if let Some(order) = self.minor.partial_cmp(&other.minor) { + if order != Ordering::Equal { + return Some(order); + } + } + + if let Some(order) = self.micro.partial_cmp(&other.micro) { + if order != Ordering::Equal { + return Some(order); + } + } + + if let Some(order) = self.patch_level.partial_cmp(&other.patch_level) { + if order != Ordering::Equal { + return Some(order); + } + } + + // version-spect.txt *does* say that we should compare tags lexicgraphically + // if all of the version numbers are the same when comparing, but we are + // going to diverge here and say we can only compare tags for equality. + // + // In practice we will be comparing tor daemon tags against tagless (stable) + // versions so this shouldn't be an issue + + if self.status_tag == other.status_tag { + return Some(Ordering::Equal); + } + + None + } +} + +#[test] +fn test_version() -> anyhow::Result<()> { + assert!(TorVersion::from_str("1.2.3")? == TorVersion::new(1, 2, 3, None, None)?); + assert!(TorVersion::from_str("1.2.3.4")? == TorVersion::new(1, 2, 3, Some(4), None)?); + assert!(TorVersion::from_str("1.2.3-test")? == TorVersion::new(1, 2, 3, None, Some("test"))?); + assert!( + TorVersion::from_str("1.2.3.4-test")? == TorVersion::new(1, 2, 3, Some(4), Some("test"))? + ); + assert!(TorVersion::from_str("1.2.3 (extra_info)")? == TorVersion::new(1, 2, 3, None, None)?); + assert!( + TorVersion::from_str("1.2.3.4 (extra_info)")? == TorVersion::new(1, 2, 3, Some(4), None)? + ); + assert!( + TorVersion::from_str("1.2.3.4-tag (extra_info)")? + == TorVersion::new(1, 2, 3, Some(4), Some("tag"))? + ); + + assert!( + TorVersion::from_str("1.2.3.4-tag (extra_info) (extra_info)")? + == TorVersion::new(1, 2, 3, Some(4), Some("tag"))? + ); + + assert!(TorVersion::new(1, 2, 3, Some(4), Some("spaced tag")).is_err()); + assert!(TorVersion::new(1, 2, 3, Some(4), Some("" /* empty tag */)).is_err()); + assert!(TorVersion::from_str("").is_err()); + assert!(TorVersion::from_str("1.2").is_err()); + assert!(TorVersion::from_str("1.2-foo").is_err()); + assert!(TorVersion::from_str("1.2.3.4-foo bar").is_err()); + assert!(TorVersion::from_str("1.2.3.4-foo bar (extra_info)").is_err()); + assert!(TorVersion::from_str("1.2.3.4-foo (extra_info) badtext").is_err()); + assert!(TorVersion::new(0, 0, 0, Some(0), None)? < TorVersion::new(1, 0, 0, Some(0), None)?); + assert!(TorVersion::new(0, 0, 0, Some(0), None)? < TorVersion::new(0, 1, 0, Some(0), None)?); + assert!(TorVersion::new(0, 0, 0, Some(0), None)? < TorVersion::new(0, 0, 1, Some(0), None)?); + + // ensure status tags make comparison between equal versions (apart from + // tags) unknowable + let zero_version = TorVersion::new(0, 0, 0, Some(0), None)?; + let zero_version_tag = TorVersion::new(0, 0, 0, Some(0), Some("tag"))?; + + assert!(!(zero_version < zero_version_tag)); + assert!(!(zero_version <= zero_version_tag)); + assert!(!(zero_version > zero_version_tag)); + assert!(!(zero_version >= zero_version_tag)); + + Ok(()) +} From a44eb14c1ce080eeba97ab9c88aee1369021fd10 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 29 May 2023 03:11:18 +0000 Subject: [PATCH 006/184] gosling, honk-rpc, tor-interface: fixed clippy warnings --- tor-interface/src/tor_crypto.rs | 6 +----- tor-interface/src/tor_process.rs | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 109a89b8a..652be2455 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -612,10 +612,6 @@ impl X25519PublicKey { BASE32_NOPAD.encode(self.public_key.as_bytes()) } - pub fn to_string(&self) -> String { - self.to_base32() - } - pub fn as_bytes(&self) -> &[u8; X25519_PUBLIC_KEY_SIZE] { self.public_key.as_bytes() } @@ -623,7 +619,7 @@ impl X25519PublicKey { impl std::fmt::Debug for X25519PublicKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.to_string()) + write!(f, "{}", self.to_base32()) } } diff --git a/tor-interface/src/tor_process.rs b/tor-interface/src/tor_process.rs index 9223e9d67..7e3fa068a 100644 --- a/tor-interface/src/tor_process.rs +++ b/tor-interface/src/tor_process.rs @@ -114,11 +114,11 @@ pub(crate) struct TorProcess { impl TorProcess { pub fn get_control_addr(&self) -> &SocketAddr { - return &self.control_addr; + &self.control_addr } pub fn get_password(&self) -> &String { - return &self.password; + &self.password } pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { From 2f15547f4266508795b27d83c44bf3ee845e7754 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Wed, 31 May 2023 02:53:35 +0000 Subject: [PATCH 007/184] tor-interface: added comment justifying the min required version check --- tor-interface/src/tor_manager.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tor-interface/src/tor_manager.rs b/tor-interface/src/tor_manager.rs index dadb368a0..2fea7c998 100644 --- a/tor-interface/src/tor_manager.rs +++ b/tor-interface/src/tor_manager.rs @@ -249,6 +249,7 @@ impl TorManager { .authenticate(daemon.get_password()) .map_err(Error::TorProcessAuthenticationFailed)?; + // min required version for v3 client auth (see control-spec.txt) let min_required_version: TorVersion = TorVersion { major: 0u32, minor: 4u32, From 8aa9d29f5c1628ce35de27c64b0d9dcceac36c79 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 18 Jun 2023 17:20:41 +0000 Subject: [PATCH 008/184] tor-interface: added initial tor traits to generalise TorManager type --- tor-interface/src/lib.rs | 1 + tor-interface/src/tor_provider.rs | 115 ++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 tor-interface/src/tor_provider.rs diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index a9a61637b..dbb12f610 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -3,4 +3,5 @@ pub mod tor_controller; pub mod tor_crypto; pub mod tor_manager; pub mod tor_process; +pub mod tor_provider; pub mod tor_version; diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs new file mode 100644 index 000000000..11bdd0137 --- /dev/null +++ b/tor-interface/src/tor_provider.rs @@ -0,0 +1,115 @@ +// standard +use std::io::{Read, Write}; +use std::net::TcpStream; +use std::ops::{Deref, DerefMut}; + +// internal crates +use crate::tor_crypto::*; + +pub enum TorEvent { + BootstrapStatus { + progress: u32, + tag: String, + summary: String, + }, + BootstrapComplete, + LogReceived { + line: String, + }, + OnionServicePublished { + service_id: V3OnionServiceId, + }, +} + +pub trait CircuitToken {} + +// +// OnionStream Implementation +// + +pub struct OnionStream { + stream: TcpStream, + onion_addr: Option, +} + +impl Deref for OnionStream { + type Target = TcpStream; + fn deref(&self) -> &Self::Target { + &self.stream + } +} + +impl DerefMut for OnionStream { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.stream + } +} + +impl From for TcpStream { + fn from(onion_stream: OnionStream) -> Self { + onion_stream.stream + } +} + +impl Read for OnionStream { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.stream.read(buf) + } +} + +impl Write for OnionStream { + fn write(&mut self, buf: &[u8]) -> Result { + self.stream.write(buf) + } + + fn flush(&mut self) -> Result<(), std::io::Error> { + self.stream.flush() + } +} + +impl OnionStream { + pub fn new(stream: TcpStream, onion_addr: Option) -> Self { + Self { stream, onion_addr } + } + + pub fn onion_addr(&self) -> Option { + self.onion_addr.clone() + } + + pub fn try_clone(&self) -> Result { + Ok(Self { + stream: self.stream.try_clone()?, + onion_addr: self.onion_addr.clone(), + }) + } +} + +pub trait OnionListener { + fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error>; + fn accept(&self) -> Result, std::io::Error>; +} + +pub trait TorProvider { + type Error; + + fn update(&mut self) -> Result, Self::Error>; + fn bootstrap(&mut self) -> Result<(), Self::Error>; + fn add_client_auth( + &mut self, + service_id: &V3OnionServiceId, + client_auth: &X25519PrivateKey, + ) -> Result<(), Self::Error>; + fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Self::Error>; + fn connect( + &mut self, + service_id: &V3OnionServiceId, + virt_port: u16, + circuit: Option, + ) -> Result; + fn listener( + &mut self, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorized_clients: Option<&[X25519PublicKey]>, + ) -> Result; +} From 0c03ba43d5278006ad2eff1be2be4db425804304 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 18 Jun 2023 17:27:12 +0000 Subject: [PATCH 009/184] tor-interface, gosling: implemented new tor provider interfaces and fixed callers --- tor-interface/src/tor_manager.rs | 210 ++++++++++--------------------- 1 file changed, 64 insertions(+), 146 deletions(-) diff --git a/tor-interface/src/tor_manager.rs b/tor-interface/src/tor_manager.rs index 2fea7c998..611b00377 100644 --- a/tor-interface/src/tor_manager.rs +++ b/tor-interface/src/tor_manager.rs @@ -1,7 +1,7 @@ // standard use std::default::Default; -use std::io::{ErrorKind, Read, Write}; -use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::io::ErrorKind; +use std::net::{SocketAddr, TcpListener}; use std::ops::Drop; use std::option::Option; use std::path::Path; @@ -20,6 +20,7 @@ use crate::tor_control_stream::*; use crate::tor_controller::*; use crate::tor_crypto::*; use crate::tor_process::*; +use crate::tor_provider::*; use crate::tor_version::*; #[derive(thiserror::Error, Debug)] @@ -79,116 +80,44 @@ pub enum Error { AddOnionFailed(#[source] crate::tor_controller::Error), } -pub struct CircuitToken { +// +// CircuitToken Implementation +// +pub struct TorDaemonCircuitToken { username: String, password: String, } -impl CircuitToken { +impl TorDaemonCircuitToken { #[allow(dead_code)] - pub fn new(first_party: Host) -> CircuitToken { + pub fn new(first_party: Host) -> TorDaemonCircuitToken { const CIRCUIT_TOKEN_PASSWORD_LENGTH: usize = 32usize; let username = first_party.to_string(); let password = generate_password(CIRCUIT_TOKEN_PASSWORD_LENGTH); - CircuitToken { username, password } + TorDaemonCircuitToken { username, password } } } -pub struct OnionStream { - stream: TcpStream, - peer_addr: Option, -} - -impl OnionStream { - pub fn nodelay(&self) -> Result { - self.stream.nodelay() - } - - pub fn peer_addr(&self) -> Option<&V3OnionServiceId> { - self.peer_addr.as_ref() - } - - pub fn read_timeout(&self) -> Result, std::io::Error> { - self.stream.read_timeout() - } - - pub fn set_nodelay(&self, nodelay: bool) -> Result<(), std::io::Error> { - self.stream.set_nodelay(nodelay) - } - - pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { - self.stream.set_nonblocking(nonblocking) - } - - pub fn set_read_timeout(&self, dur: Option) -> Result<(), std::io::Error> { - self.stream.set_read_timeout(dur) - } - - pub fn set_write_timeout(&self, dur: Option) -> Result<(), std::io::Error> { - self.stream.set_write_timeout(dur) - } - - pub fn shutdown(&self, how: std::net::Shutdown) -> Result<(), std::io::Error> { - self.stream.shutdown(how) - } - - pub fn take_error(&self) -> Result, std::io::Error> { - self.stream.take_error() - } - - pub fn write_timeout(&self) -> Result, std::io::Error> { - self.stream.write_timeout() - } - - pub fn try_clone(&self) -> Result { - Ok(OnionStream { - stream: self.stream.try_clone()?, - peer_addr: self.peer_addr.clone(), - }) - } -} +impl CircuitToken for TorDaemonCircuitToken {} -// pass-through to underlying Read stream -impl Read for OnionStream { - fn read(&mut self, buf: &mut [u8]) -> Result { - self.stream.read(buf) - } -} - -// pass-through to underlying Write stream -impl Write for OnionStream { - fn write(&mut self, buf: &[u8]) -> Result { - self.stream.write(buf) - } - - fn flush(&mut self) -> Result<(), std::io::Error> { - self.stream.flush() - } -} +// +// TorDaemonOnionListener +// -impl From for TcpStream { - fn from(onion_stream: OnionStream) -> Self { - onion_stream.stream - } -} - -pub struct OnionListener { +pub struct TorDaemonOnionListener { listener: TcpListener, is_active: Arc, } -impl OnionListener { - pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { +impl OnionListener for TorDaemonOnionListener { + fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { self.listener.set_nonblocking(nonblocking) } - pub fn accept(&self) -> Result, std::io::Error> { + fn accept(&self) -> Result, std::io::Error> { match self.listener.accept() { - Ok((stream, _socket_addr)) => Ok(Some(OnionStream { - stream, - peer_addr: None, - })), + Ok((stream, _socket_addr)) => Ok(Some(OnionStream::new(stream, None))), Err(err) => { if err.kind() == ErrorKind::WouldBlock { Ok(None) @@ -200,28 +129,13 @@ impl OnionListener { } } -impl Drop for OnionListener { +impl Drop for TorDaemonOnionListener { fn drop(&mut self) { self.is_active.store(false, atomic::Ordering::Relaxed); } } -pub enum Event { - BootstrapStatus { - progress: u32, - tag: String, - summary: String, - }, - BootstrapComplete, - LogReceived { - line: String, - }, - OnionServicePublished { - service_id: V3OnionServiceId, - }, -} - -pub struct TorManager { +pub struct LegacyTorClient { daemon: TorProcess, version: TorVersion, controller: TorController, @@ -230,8 +144,8 @@ pub struct TorManager { onion_services: Vec<(V3OnionServiceId, Arc)>, } -impl TorManager { - pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { +impl LegacyTorClient { + pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { // launch tor let daemon = TorProcess::new(tor_bin_path, data_directory) .map_err(Error::TorProcessCreationFailed)?; @@ -274,7 +188,7 @@ impl TorManager { .setevents(&["STATUS_CLIENT", "HS_DESC"]) .map_err(Error::SetEventsFailed)?; - Ok(TorManager { + Ok(LegacyTorClient { daemon, version, controller, @@ -283,7 +197,16 @@ impl TorManager { }) } - pub fn update(&mut self) -> Result, Error> { + #[allow(dead_code)] + pub fn version(&mut self) -> TorVersion { + self.version.clone() + } +} + +impl TorProvider for LegacyTorClient { + type Error = Error; + + fn update(&mut self) -> Result, Error> { let mut i = 0; while i < self.onion_services.len() { // remove onion services with no active listeners @@ -299,7 +222,7 @@ impl TorManager { } } - let mut events: Vec = Default::default(); + let mut events: Vec = Default::default(); for async_event in self .controller .wait_async_events() @@ -324,19 +247,19 @@ impl TorManager { _ => {} // ignore unexpected arguments } } - events.push(Event::BootstrapStatus { + events.push(TorEvent::BootstrapStatus { progress, tag, summary, }); if progress == 100u32 { - events.push(Event::BootstrapComplete); + events.push(TorEvent::BootstrapComplete); } } } AsyncEvent::HsDesc { action, hs_address } => { if action == "UPLOADED" { - events.push(Event::OnionServicePublished { + events.push(TorEvent::OnionServicePublished { service_id: hs_address.clone(), }); } @@ -351,7 +274,7 @@ impl TorManager { } for log_line in self.daemon.wait_log_lines().iter_mut() { - events.push(Event::LogReceived { + events.push(TorEvent::LogReceived { line: std::mem::take(log_line), }); } @@ -359,18 +282,13 @@ impl TorManager { Ok(events) } - #[allow(dead_code)] - pub fn version(&mut self) -> TorVersion { - self.version.clone() - } - - pub fn bootstrap(&mut self) -> Result<(), Error> { + fn bootstrap(&mut self) -> Result<(), Error> { self.controller .setconf(&[("DisableNetwork", "0")]) .map_err(Error::SetConfDisableNetwork0Failed) } - pub fn add_client_auth( + fn add_client_auth( &mut self, service_id: &V3OnionServiceId, client_auth: &X25519PrivateKey, @@ -380,18 +298,18 @@ impl TorManager { .map_err(Error::OnionClientAuthAddFailed) } - pub fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> { + fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> { self.controller .onion_client_auth_remove(service_id) .map_err(Error::OnionClientAuthRemoveFailed) } // connect to an onion service and returns OnionStream - pub fn connect( + fn connect( &mut self, service_id: &V3OnionServiceId, virt_port: u16, - circuit: Option, + circuit: Option, ) -> Result { if self.socks_listener.is_none() { let mut listeners = self @@ -424,19 +342,19 @@ impl TorManager { } .map_err(Error::Socks5ConnectionFailed)?; - Ok(OnionStream { - stream: stream.into_inner(), - peer_addr: Some(service_id.clone()), - }) + Ok(OnionStream::new( + stream.into_inner(), + Some(service_id.clone()), + )) } - // stand up an onion service and return an OnionListener - pub fn listener( + // stand up an onion service and return an TorDaemonOnionListener + fn listener( &mut self, private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { // try to bind to a local address, let OS pick our port let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; @@ -469,7 +387,7 @@ impl TorManager { self.onion_services .push((service_id, Arc::clone(&is_active))); - Ok(OnionListener { + Ok(TorDaemonOnionListener { listener, is_active, }) @@ -483,7 +401,7 @@ fn test_tor_manager() -> anyhow::Result<()> { let mut data_path = std::env::temp_dir(); data_path.push("test_tor_manager"); - let mut tor = TorManager::new(&tor_path, &data_path)?; + let mut tor = LegacyTorClient::new(&tor_path, &data_path)?; println!("version : {}", tor.version().to_string()); tor.bootstrap()?; @@ -492,7 +410,7 @@ fn test_tor_manager() -> anyhow::Result<()> { while !bootstrap_complete { for event in tor.update()?.iter() { match event { - Event::BootstrapStatus { + TorEvent::BootstrapStatus { progress, tag, summary, @@ -500,11 +418,11 @@ fn test_tor_manager() -> anyhow::Result<()> { "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", progress, tag, summary ), - Event::BootstrapComplete => { + TorEvent::BootstrapComplete => { println!("Bootstrap Complete!"); bootstrap_complete = true; } - Event::LogReceived { line } => { + TorEvent::LogReceived { line } => { received_log = true; println!("--- {}", line); } @@ -527,7 +445,7 @@ fn test_onion_service() -> anyhow::Result<()> { let mut data_path = std::env::temp_dir(); data_path.push("test_onion_service"); - let mut tor = TorManager::new(&tor_path, &data_path)?; + let mut tor = LegacyTorClient::new(&tor_path, &data_path)?; // for 30secs for bootstrap tor.bootstrap()?; @@ -536,7 +454,7 @@ fn test_onion_service() -> anyhow::Result<()> { while !bootstrap_complete { for event in tor.update()?.iter() { match event { - Event::BootstrapStatus { + TorEvent::BootstrapStatus { progress, tag, summary, @@ -544,11 +462,11 @@ fn test_onion_service() -> anyhow::Result<()> { "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", progress, tag, summary ), - Event::BootstrapComplete => { + TorEvent::BootstrapComplete => { println!("Bootstrap Complete!"); bootstrap_complete = true; } - Event::LogReceived { line } => { + TorEvent::LogReceived { line } => { println!("--- {}", line); } _ => {} @@ -569,10 +487,10 @@ fn test_onion_service() -> anyhow::Result<()> { while !onion_published { for event in tor.update()?.iter() { match event { - Event::LogReceived { line } => { + TorEvent::LogReceived { line } => { println!("--- {}", line); } - Event::OnionServicePublished { service_id } => { + TorEvent::OnionServicePublished { service_id } => { let expected_service_id = V3OnionServiceId::from_private_key(&private_key); if expected_service_id == *service_id { println!("Onion Service {} published", service_id.to_string()); @@ -626,10 +544,10 @@ fn test_onion_service() -> anyhow::Result<()> { while !onion_published { for event in tor.update()?.iter() { match event { - Event::LogReceived { line } => { + TorEvent::LogReceived { line } => { println!("--- {}", line); } - Event::OnionServicePublished { service_id } => { + TorEvent::OnionServicePublished { service_id } => { let expected_service_id = V3OnionServiceId::from_private_key(&private_key); if expected_service_id == *service_id { println!( From 622c22cc7ac97c5f57ff88ee40901b07b6798e14 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 18 Jun 2023 22:07:12 +0000 Subject: [PATCH 010/184] tor-crypto: converted V3OnionServiceId to_string() to std::fmt::Display implementation --- tor-interface/src/tor_controller.rs | 4 ++-- tor-interface/src/tor_crypto.rs | 10 +++++----- tor-interface/src/tor_manager.rs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tor-interface/src/tor_controller.rs b/tor-interface/src/tor_controller.rs index 4b8ffa780..67ae8afdd 100644 --- a/tor-interface/src/tor_controller.rs +++ b/tor-interface/src/tor_controller.rs @@ -376,7 +376,7 @@ impl TorController { // DEL_ONION (3.38) fn del_onion_cmd(&mut self, service_id: &V3OnionServiceId) -> Result { - let command = format!("DEL_ONION {}", service_id.to_string()); + let command = format!("DEL_ONION {}", service_id); self.write_command(&command) } @@ -416,7 +416,7 @@ impl TorController { &mut self, service_id: &V3OnionServiceId, ) -> Result { - let command = format!("ONION_CLIENT_AUTH_REMOVE {}", service_id.to_string()); + let command = format!("ONION_CLIENT_AUTH_REMOVE {}", service_id); self.write_command(&command) } diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 652be2455..5f00b70dc 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -388,7 +388,7 @@ impl Ed25519PublicKey { Err(_) => { return Err(Error::ConversionError(format!( "failed to decode '{}' as V3OnionServiceId", - service_id.to_string() + service_id ))) } }; @@ -696,15 +696,15 @@ impl V3OnionServiceId { } } -impl ToString for V3OnionServiceId { - fn to_string(&self) -> String { - return unsafe { str::from_utf8_unchecked(&self.data).to_string() }; +impl std::fmt::Display for V3OnionServiceId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + unsafe { write!(f, "{}", str::from_utf8_unchecked(&self.data)) } } } impl std::fmt::Debug for V3OnionServiceId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.to_string()) + unsafe { write!(f, "{}", str::from_utf8_unchecked(&self.data)) } } } diff --git a/tor-interface/src/tor_manager.rs b/tor-interface/src/tor_manager.rs index 611b00377..12108b087 100644 --- a/tor-interface/src/tor_manager.rs +++ b/tor-interface/src/tor_manager.rs @@ -329,7 +329,7 @@ impl TorProvider for LegacyTorCli // our onion domain let target = - socks::TargetAddr::Domain(format!("{}.onion", service_id.to_string()), virt_port); + socks::TargetAddr::Domain(format!("{}.onion", service_id), virt_port); // readwrite stream let stream = match &circuit { None => Socks5Stream::connect(socks_listener, target), From 55456f341cf97a5963a02d718659405149feec1e Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 18 Jun 2023 22:24:01 +0000 Subject: [PATCH 011/184] tor-interface, gosling: OnionStream now exposes custom TargetAddr for peer_addr and local_addr --- tor-interface/src/tor_manager.rs | 21 +++++++++++++----- tor-interface/src/tor_provider.rs | 36 +++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/tor-interface/src/tor_manager.rs b/tor-interface/src/tor_manager.rs index 12108b087..2fb967486 100644 --- a/tor-interface/src/tor_manager.rs +++ b/tor-interface/src/tor_manager.rs @@ -1,6 +1,8 @@ // standard use std::default::Default; use std::io::ErrorKind; +#[cfg(test)] +use std::io::{Read,Write}; use std::net::{SocketAddr, TcpListener}; use std::ops::Drop; use std::option::Option; @@ -108,6 +110,7 @@ impl CircuitToken for TorDaemonCircuitToken {} pub struct TorDaemonOnionListener { listener: TcpListener, is_active: Arc, + onion_addr: OnionAddr, } impl OnionListener for TorDaemonOnionListener { @@ -117,7 +120,11 @@ impl OnionListener for TorDaemonOnionListener { fn accept(&self) -> Result, std::io::Error> { match self.listener.accept() { - Ok((stream, _socket_addr)) => Ok(Some(OnionStream::new(stream, None))), + Ok((stream, _socket_addr)) => Ok(Some(OnionStream{ + stream, + local_addr: Some(self.onion_addr.clone()), + peer_addr: None, + })), Err(err) => { if err.kind() == ErrorKind::WouldBlock { Ok(None) @@ -342,10 +349,11 @@ impl TorProvider for LegacyTorCli } .map_err(Error::Socks5ConnectionFailed)?; - Ok(OnionStream::new( - stream.into_inner(), - Some(service_id.clone()), - )) + Ok(OnionStream{ + stream: stream.into_inner(), + local_addr: None, + peer_addr: Some(TargetAddr::OnionService(OnionAddr::V3(service_id.clone(), virt_port))), + }) } // stand up an onion service and return an TorDaemonOnionListener @@ -370,6 +378,8 @@ impl TorProvider for LegacyTorCli flags.v3_auth = true; } + let onion_addr = OnionAddr::V3(V3OnionServiceId::from_private_key(private_key), virt_port); + // start onion service let (_, service_id) = self .controller @@ -390,6 +400,7 @@ impl TorProvider for LegacyTorCli Ok(TorDaemonOnionListener { listener, is_active, + onion_addr, }) } } diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 11bdd0137..cb4c7f12a 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -6,6 +6,26 @@ use std::ops::{Deref, DerefMut}; // internal crates use crate::tor_crypto::*; +#[derive(Clone, Debug)] +pub enum OnionAddr { + V3(V3OnionServiceId, u16), +} + +impl std::fmt::Display for OnionAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OnionAddr::V3(service_id, port) => write!(f, "{}:{}", service_id, port), + } + } +} + +#[derive(Clone, Debug)] +pub enum TargetAddr { + Ip(std::net::SocketAddr), + Domain(String, u16), + OnionService(OnionAddr), +} + pub enum TorEvent { BootstrapStatus { progress: u32, @@ -28,8 +48,9 @@ pub trait CircuitToken {} // pub struct OnionStream { - stream: TcpStream, - onion_addr: Option, + pub(crate) stream: TcpStream, + pub(crate) local_addr: Option, + pub(crate) peer_addr: Option, } impl Deref for OnionStream { @@ -68,18 +89,19 @@ impl Write for OnionStream { } impl OnionStream { - pub fn new(stream: TcpStream, onion_addr: Option) -> Self { - Self { stream, onion_addr } + pub fn peer_addr(&self) -> Option { + self.peer_addr.clone() } - pub fn onion_addr(&self) -> Option { - self.onion_addr.clone() + pub fn local_addr(&self) -> Option { + None } pub fn try_clone(&self) -> Result { Ok(Self { stream: self.stream.try_clone()?, - onion_addr: self.onion_addr.clone(), + local_addr: self.local_addr.clone(), + peer_addr: self.peer_addr.clone(), }) } } From 5ab195a05a21828601323aa1cf3dae458eaa4500 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 18 Jun 2023 22:49:07 +0000 Subject: [PATCH 012/184] tor-interface: renamed various modules to include legacy in the name in preparation for alternate backends --- .../{tor_manager.rs => legacy_tor_client.rs} | 35 ++++++++++--------- ...stream.rs => legacy_tor_control_stream.rs} | 0 ...controller.rs => legacy_tor_controller.rs} | 15 ++++---- .../{tor_process.rs => legacy_tor_process.rs} | 0 .../{tor_version.rs => legacy_tor_version.rs} | 0 tor-interface/src/lib.rs | 10 +++--- 6 files changed, 31 insertions(+), 29 deletions(-) rename tor-interface/src/{tor_manager.rs => legacy_tor_client.rs} (94%) rename tor-interface/src/{tor_control_stream.rs => legacy_tor_control_stream.rs} (100%) rename tor-interface/src/{tor_controller.rs => legacy_tor_controller.rs} (98%) rename tor-interface/src/{tor_process.rs => legacy_tor_process.rs} (100%) rename tor-interface/src/{tor_version.rs => legacy_tor_version.rs} (100%) diff --git a/tor-interface/src/tor_manager.rs b/tor-interface/src/legacy_tor_client.rs similarity index 94% rename from tor-interface/src/tor_manager.rs rename to tor-interface/src/legacy_tor_client.rs index 2fb967486..191b57775 100644 --- a/tor-interface/src/tor_manager.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -18,53 +18,54 @@ use socks::Socks5Stream; use url::Host; // internal crates -use crate::tor_control_stream::*; -use crate::tor_controller::*; +use crate::legacy_tor_control_stream::*; +use crate::legacy_tor_controller::*; +use crate::legacy_tor_process::*; +use crate::legacy_tor_version::*; use crate::tor_crypto::*; -use crate::tor_process::*; use crate::tor_provider::*; -use crate::tor_version::*; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("failed to create TorProcess object")] - TorProcessCreationFailed(#[source] crate::tor_process::Error), + TorProcessCreationFailed(#[source] crate::legacy_tor_process::Error), #[error("failed to create ControlStream object")] - ControlStreamCreationFailed(#[source] crate::tor_control_stream::Error), + ControlStreamCreationFailed(#[source] crate::legacy_tor_control_stream::Error), #[error("failed to create TorController object")] - TorControllerCreationFailed(#[source] crate::tor_controller::Error), + TorControllerCreationFailed(#[source] crate::legacy_tor_controller::Error), #[error("failed to authenticate with the tor process")] - TorProcessAuthenticationFailed(#[source] crate::tor_controller::Error), + TorProcessAuthenticationFailed(#[source] crate::legacy_tor_controller::Error), #[error("failed to determine the tor process version")] - GetInfoVersionFailed(#[source] crate::tor_controller::Error), + GetInfoVersionFailed(#[source] crate::legacy_tor_controller::Error), #[error("tor process version to old; found {0} but must be at least {1}")] TorProcessTooOld(String, String), #[error("failed to register for STATUS_CLIENT and HS_DESC events")] - SetEventsFailed(#[source] crate::tor_controller::Error), + SetEventsFailed(#[source] crate::legacy_tor_controller::Error), #[error("failed to delete unused onion service")] - DelOnionFailed(#[source] crate::tor_controller::Error), + DelOnionFailed(#[source] crate::legacy_tor_controller::Error), #[error("failed waiting for async events")] - WaitAsyncEventsFailed(#[source] crate::tor_controller::Error), + WaitAsyncEventsFailed(#[source] crate::legacy_tor_controller::Error), #[error("failed to begin bootstrap")] - SetConfDisableNetwork0Failed(#[source] crate::tor_controller::Error), + SetConfDisableNetwork0Failed(#[source] crate::legacy_tor_controller::Error), #[error("failed to add client auth for onion service")] - OnionClientAuthAddFailed(#[source] crate::tor_controller::Error), + OnionClientAuthAddFailed(#[source] crate::legacy_tor_controller::Error), #[error("failed to remove client auth from onion service")] - OnionClientAuthRemoveFailed(#[source] crate::tor_controller::Error), + OnionClientAuthRemoveFailed(#[source] crate::legacy_tor_controller::Error), #[error("failed to get socks listener")] - GetInfoNetListenersSocksFailed(#[source] crate::tor_controller::Error), + GetInfoNetListenersSocksFailed(#[source] crate::legacy_tor_controller::Error), #[error("no socks listeners available to connect through")] NoSocksListenersFound(), @@ -79,7 +80,7 @@ pub enum Error { TcpListenerLocalAddrFailed(#[source] std::io::Error), #[error("failed to create onion service")] - AddOnionFailed(#[source] crate::tor_controller::Error), + AddOnionFailed(#[source] crate::legacy_tor_controller::Error), } // diff --git a/tor-interface/src/tor_control_stream.rs b/tor-interface/src/legacy_tor_control_stream.rs similarity index 100% rename from tor-interface/src/tor_control_stream.rs rename to tor-interface/src/legacy_tor_control_stream.rs diff --git a/tor-interface/src/tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs similarity index 98% rename from tor-interface/src/tor_controller.rs rename to tor-interface/src/legacy_tor_controller.rs index 67ae8afdd..1471e4268 100644 --- a/tor-interface/src/tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -15,11 +15,12 @@ use regex::Regex; use serial_test::serial; // internal crates -use crate::tor_control_stream::*; -use crate::tor_crypto::*; +use crate::legacy_tor_control_stream::*; #[cfg(test)] -use crate::tor_process::*; -use crate::tor_version::*; +use crate::legacy_tor_process::*; +use crate::legacy_tor_version::*; +use crate::tor_crypto::*; + #[derive(thiserror::Error, Debug)] pub enum Error { @@ -27,13 +28,13 @@ pub enum Error { ParsingRegexCreationFailed(#[source] regex::Error), #[error("control stream read reply failed")] - ReadReplyFailed(#[source] crate::tor_control_stream::Error), + ReadReplyFailed(#[source] crate::legacy_tor_control_stream::Error), #[error("unexpected synchronous reply recieved")] UnexpectedSynchonousReplyReceived(), #[error("control stream write command failed")] - WriteCommandFailed(#[source] crate::tor_control_stream::Error), + WriteCommandFailed(#[source] crate::legacy_tor_control_stream::Error), #[error("invalid command arguments: {0}")] InvalidCommandArguments(String), @@ -45,7 +46,7 @@ pub enum Error { CommandReplyParseFailed(String), #[error("failed to parse received tor version")] - TorVersionParseFailed(#[source] crate::tor_version::Error), + TorVersionParseFailed(#[source] crate::legacy_tor_version::Error), } // Per-command data diff --git a/tor-interface/src/tor_process.rs b/tor-interface/src/legacy_tor_process.rs similarity index 100% rename from tor-interface/src/tor_process.rs rename to tor-interface/src/legacy_tor_process.rs diff --git a/tor-interface/src/tor_version.rs b/tor-interface/src/legacy_tor_version.rs similarity index 100% rename from tor-interface/src/tor_version.rs rename to tor-interface/src/legacy_tor_version.rs diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index dbb12f610..c719e8812 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -1,7 +1,7 @@ -pub mod tor_control_stream; -pub mod tor_controller; +pub mod legacy_tor_client; +pub mod legacy_tor_control_stream; +pub mod legacy_tor_controller; +pub mod legacy_tor_process; +pub mod legacy_tor_version; pub mod tor_crypto; -pub mod tor_manager; -pub mod tor_process; pub mod tor_provider; -pub mod tor_version; From 08cfec4d89c4dbac6b81bff50ac7efbe756a9928 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 18 Jun 2023 23:02:48 +0000 Subject: [PATCH 013/184] tor-interface: reduced visibility of internal modules --- tor-interface/src/legacy_tor_client.rs | 23 +++++++++++----------- tor-interface/src/legacy_tor_controller.rs | 3 +-- tor-interface/src/legacy_tor_process.rs | 9 --------- tor-interface/src/lib.rs | 8 ++++---- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 191b57775..c9a040ef8 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -2,7 +2,7 @@ use std::default::Default; use std::io::ErrorKind; #[cfg(test)] -use std::io::{Read,Write}; +use std::io::{Read, Write}; use std::net::{SocketAddr, TcpListener}; use std::ops::Drop; use std::option::Option; @@ -25,7 +25,6 @@ use crate::legacy_tor_version::*; use crate::tor_crypto::*; use crate::tor_provider::*; - #[derive(thiserror::Error, Debug)] pub enum Error { #[error("failed to create TorProcess object")] @@ -121,7 +120,7 @@ impl OnionListener for TorDaemonOnionListener { fn accept(&self) -> Result, std::io::Error> { match self.listener.accept() { - Ok((stream, _socket_addr)) => Ok(Some(OnionStream{ + Ok((stream, _socket_addr)) => Ok(Some(OnionStream { stream, local_addr: Some(self.onion_addr.clone()), peer_addr: None, @@ -163,8 +162,8 @@ impl LegacyTorClient { .map_err(Error::ControlStreamCreationFailed)?; // create a controler - let mut controller = - TorController::new(control_stream).map_err(Error::TorControllerCreationFailed)?; + let mut controller = LegacyTorController::new(control_stream) + .map_err(Error::LegacyTorControllerCreationFailed)?; // authenticate controller @@ -336,8 +335,7 @@ impl TorProvider for LegacyTorCli }; // our onion domain - let target = - socks::TargetAddr::Domain(format!("{}.onion", service_id), virt_port); + let target = socks::TargetAddr::Domain(format!("{}.onion", service_id), virt_port); // readwrite stream let stream = match &circuit { None => Socks5Stream::connect(socks_listener, target), @@ -350,10 +348,13 @@ impl TorProvider for LegacyTorCli } .map_err(Error::Socks5ConnectionFailed)?; - Ok(OnionStream{ + Ok(OnionStream { stream: stream.into_inner(), local_addr: None, - peer_addr: Some(TargetAddr::OnionService(OnionAddr::V3(service_id.clone(), virt_port))), + peer_addr: Some(TargetAddr::OnionService(OnionAddr::V3( + service_id.clone(), + virt_port, + ))), }) } @@ -409,7 +410,7 @@ impl TorProvider for LegacyTorCli #[test] #[serial] fn test_tor_manager() -> anyhow::Result<()> { - let tor_path = which::which(tor_exe_name())?; + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); data_path.push("test_tor_manager"); @@ -453,7 +454,7 @@ fn test_tor_manager() -> anyhow::Result<()> { #[test] #[serial] fn test_onion_service() -> anyhow::Result<()> { - let tor_path = which::which(tor_exe_name())?; + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); data_path.push("test_onion_service"); diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index 1471e4268..a5f044fc3 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -21,7 +21,6 @@ use crate::legacy_tor_process::*; use crate::legacy_tor_version::*; use crate::tor_crypto::*; - #[derive(thiserror::Error, Debug)] pub enum Error { #[error("response regex creation failed")] @@ -674,7 +673,7 @@ impl TorController { #[test] #[serial] fn test_tor_controller() -> anyhow::Result<()> { - let tor_path = which::which(tor_exe_name())?; + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); data_path.push("test_tor_controller"); let tor_process = TorProcess::new(&tor_path, &data_path)?; diff --git a/tor-interface/src/legacy_tor_process.rs b/tor-interface/src/legacy_tor_process.rs index 7e3fa068a..a3e8254cf 100644 --- a/tor-interface/src/legacy_tor_process.rs +++ b/tor-interface/src/legacy_tor_process.rs @@ -64,15 +64,6 @@ pub enum Error { StdoutReadThreadSpawnFailed(#[source] std::io::Error), } -// get the name of our tor executable -pub const fn tor_exe_name() -> &'static str { - if cfg!(windows) { - "tor.exe" - } else { - "tor" - } -} - fn read_control_port_file(control_port_file: &Path) -> Result { // open file let mut file = File::open(control_port_file).map_err(Error::ControlPortFileReadFailed)?; diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index c719e8812..f483178b2 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -1,7 +1,7 @@ pub mod legacy_tor_client; -pub mod legacy_tor_control_stream; -pub mod legacy_tor_controller; -pub mod legacy_tor_process; -pub mod legacy_tor_version; +mod legacy_tor_control_stream; +mod legacy_tor_controller; +mod legacy_tor_process; +mod legacy_tor_version; pub mod tor_crypto; pub mod tor_provider; From 4b898c66cf5caba033972fc0a62e3b31d492ea62 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 18 Jun 2023 23:46:06 +0000 Subject: [PATCH 014/184] tor-interface: renamed many types to include 'Legacy' in the name --- tor-interface/src/legacy_tor_client.rs | 66 +++++++------- .../src/legacy_tor_control_stream.rs | 8 +- tor-interface/src/legacy_tor_controller.rs | 24 +++--- tor-interface/src/legacy_tor_process.rs | 24 +++--- tor-interface/src/legacy_tor_version.rs | 85 ++++++++++++------- 5 files changed, 113 insertions(+), 94 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index c9a040ef8..78860be72 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -27,23 +27,23 @@ use crate::tor_provider::*; #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("failed to create TorProcess object")] - TorProcessCreationFailed(#[source] crate::legacy_tor_process::Error), + #[error("failed to create LegacyTorProcess object")] + LegacyTorProcessCreationFailed(#[source] crate::legacy_tor_process::Error), - #[error("failed to create ControlStream object")] - ControlStreamCreationFailed(#[source] crate::legacy_tor_control_stream::Error), + #[error("failed to create LegacyControlStream object")] + LegacyControlStreamCreationFailed(#[source] crate::legacy_tor_control_stream::Error), - #[error("failed to create TorController object")] - TorControllerCreationFailed(#[source] crate::legacy_tor_controller::Error), + #[error("failed to create LegacyTorController object")] + LegacyTorControllerCreationFailed(#[source] crate::legacy_tor_controller::Error), #[error("failed to authenticate with the tor process")] - TorProcessAuthenticationFailed(#[source] crate::legacy_tor_controller::Error), + LegacyTorProcessAuthenticationFailed(#[source] crate::legacy_tor_controller::Error), #[error("failed to determine the tor process version")] GetInfoVersionFailed(#[source] crate::legacy_tor_controller::Error), #[error("tor process version to old; found {0} but must be at least {1}")] - TorProcessTooOld(String, String), + LegacyTorProcessTooOld(String, String), #[error("failed to register for STATUS_CLIENT and HS_DESC events")] SetEventsFailed(#[source] crate::legacy_tor_controller::Error), @@ -85,35 +85,35 @@ pub enum Error { // // CircuitToken Implementation // -pub struct TorDaemonCircuitToken { +pub struct LegacyCircuitToken { username: String, password: String, } -impl TorDaemonCircuitToken { +impl LegacyCircuitToken { #[allow(dead_code)] - pub fn new(first_party: Host) -> TorDaemonCircuitToken { + pub fn new(first_party: Host) -> LegacyCircuitToken { const CIRCUIT_TOKEN_PASSWORD_LENGTH: usize = 32usize; let username = first_party.to_string(); let password = generate_password(CIRCUIT_TOKEN_PASSWORD_LENGTH); - TorDaemonCircuitToken { username, password } + LegacyCircuitToken { username, password } } } -impl CircuitToken for TorDaemonCircuitToken {} +impl CircuitToken for LegacyCircuitToken {} // -// TorDaemonOnionListener +// LegacyOnionListener // -pub struct TorDaemonOnionListener { +pub struct LegacyOnionListener { listener: TcpListener, is_active: Arc, onion_addr: OnionAddr, } -impl OnionListener for TorDaemonOnionListener { +impl OnionListener for LegacyOnionListener { fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { self.listener.set_nonblocking(nonblocking) } @@ -136,16 +136,16 @@ impl OnionListener for TorDaemonOnionListener { } } -impl Drop for TorDaemonOnionListener { +impl Drop for LegacyOnionListener { fn drop(&mut self) { self.is_active.store(false, atomic::Ordering::Relaxed); } } pub struct LegacyTorClient { - daemon: TorProcess, - version: TorVersion, - controller: TorController, + daemon: LegacyTorProcess, + version: LegacyTorVersion, + controller: LegacyTorController, socks_listener: Option, // list of open onion services and their is_active flag onion_services: Vec<(V3OnionServiceId, Arc)>, @@ -154,12 +154,12 @@ pub struct LegacyTorClient { impl LegacyTorClient { pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { // launch tor - let daemon = TorProcess::new(tor_bin_path, data_directory) - .map_err(Error::TorProcessCreationFailed)?; + let daemon = LegacyTorProcess::new(tor_bin_path, data_directory) + .map_err(Error::LegacyTorProcessCreationFailed)?; // open a control stream let control_stream = - ControlStream::new(daemon.get_control_addr(), Duration::from_millis(16)) - .map_err(Error::ControlStreamCreationFailed)?; + LegacyControlStream::new(daemon.get_control_addr(), Duration::from_millis(16)) + .map_err(Error::LegacyControlStreamCreationFailed)?; // create a controler let mut controller = LegacyTorController::new(control_stream) @@ -168,10 +168,10 @@ impl LegacyTorClient { // authenticate controller .authenticate(daemon.get_password()) - .map_err(Error::TorProcessAuthenticationFailed)?; + .map_err(Error::LegacyTorProcessAuthenticationFailed)?; // min required version for v3 client auth (see control-spec.txt) - let min_required_version: TorVersion = TorVersion { + let min_required_version = LegacyTorVersion { major: 0u32, minor: 4u32, micro: 6u32, @@ -184,7 +184,7 @@ impl LegacyTorClient { .map_err(Error::GetInfoVersionFailed)?; if version < min_required_version { - return Err(Error::TorProcessTooOld( + return Err(Error::LegacyTorProcessTooOld( version.to_string(), min_required_version.to_string(), )); @@ -205,12 +205,12 @@ impl LegacyTorClient { } #[allow(dead_code)] - pub fn version(&mut self) -> TorVersion { + pub fn version(&mut self) -> LegacyTorVersion { self.version.clone() } } -impl TorProvider for LegacyTorClient { +impl TorProvider for LegacyTorClient { type Error = Error; fn update(&mut self) -> Result, Error> { @@ -316,7 +316,7 @@ impl TorProvider for LegacyTorCli &mut self, service_id: &V3OnionServiceId, virt_port: u16, - circuit: Option, + circuit: Option, ) -> Result { if self.socks_listener.is_none() { let mut listeners = self @@ -358,13 +358,13 @@ impl TorProvider for LegacyTorCli }) } - // stand up an onion service and return an TorDaemonOnionListener + // stand up an onion service and return an LegacyOnionListener fn listener( &mut self, private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { // try to bind to a local address, let OS pick our port let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; @@ -399,7 +399,7 @@ impl TorProvider for LegacyTorCli self.onion_services .push((service_id, Arc::clone(&is_active))); - Ok(TorDaemonOnionListener { + Ok(LegacyOnionListener { listener, is_active, onion_addr, diff --git a/tor-interface/src/legacy_tor_control_stream.rs b/tor-interface/src/legacy_tor_control_stream.rs index b091480fc..518b6430b 100644 --- a/tor-interface/src/legacy_tor_control_stream.rs +++ b/tor-interface/src/legacy_tor_control_stream.rs @@ -40,7 +40,7 @@ pub enum Error { WriteFailed(#[source] std::io::Error), } -pub(crate) struct ControlStream { +pub(crate) struct LegacyControlStream { stream: TcpStream, closed_by_remote: bool, pending_data: Vec, @@ -59,8 +59,8 @@ pub(crate) struct Reply { pub reply_lines: Vec, } -impl ControlStream { - pub fn new(addr: &SocketAddr, read_timeout: Duration) -> Result { +impl LegacyControlStream { + pub fn new(addr: &SocketAddr, read_timeout: Duration) -> Result { if read_timeout.is_zero() { return Err(Error::ReadTimeoutZero()); } @@ -81,7 +81,7 @@ impl ControlStream { let end_reply_line = Regex::new(r"^\d\d\d .*").map_err(Error::ParsingRegexCreationFailed)?; - Ok(ControlStream { + Ok(LegacyControlStream { stream, closed_by_remote: false, pending_data, diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index a5f044fc3..5556924ef 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -78,9 +78,9 @@ pub(crate) enum AsyncEvent { }, } -pub(crate) struct TorController { +pub(crate) struct LegacyTorController { // underlying control stream - control_stream: ControlStream, + control_stream: LegacyControlStream, // list of async replies to be handled async_replies: Vec, // regex for parsing events @@ -89,8 +89,8 @@ pub(crate) struct TorController { hs_desc_pattern: Regex, } -impl TorController { - pub fn new(control_stream: ControlStream) -> Result { +impl LegacyTorController { + pub fn new(control_stream: LegacyControlStream) -> Result { let status_event_pattern = Regex::new(r#"^STATUS_CLIENT (?PNOTICE|WARN|ERR) (?P[A-Za-z]+)"#) .map_err(Error::ParsingRegexCreationFailed)?; @@ -101,7 +101,7 @@ impl TorController { r#"HS_DESC (?PREQUESTED|UPLOAD|RECEIVED|UPLOADED|IGNORE|FAILED|CREATED) (?P[a-z2-7]{56})"# ).map_err(Error::ParsingRegexCreationFailed)?; - Ok(TorController { + Ok(LegacyTorController { control_stream, async_replies: Default::default(), // regex @@ -632,11 +632,11 @@ impl TorController { )) } - pub fn getinfo_version(&mut self) -> Result { + pub fn getinfo_version(&mut self) -> Result { let response = self.getinfo(&["version"])?; for (key, value) in response.iter() { if key.as_str() == "version" { - return TorVersion::from_str(value).map_err(Error::TorVersionParseFailed); + return LegacyTorVersion::from_str(value).map_err(Error::TorVersionParseFailed); } } Err(Error::CommandReplyParseFailed( @@ -676,15 +676,15 @@ fn test_tor_controller() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); data_path.push("test_tor_controller"); - let tor_process = TorProcess::new(&tor_path, &data_path)?; + let tor_process = LegacyTorProcess::new(&tor_path, &data_path)?; // create a scope to ensure tor_controller is dropped { let control_stream = - ControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?; + LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?; // create a tor controller and send authentication command - let mut tor_controller = TorController::new(control_stream)?; + let mut tor_controller = LegacyTorController::new(control_stream)?; tor_controller.authenticate_cmd(tor_process.get_password())?; assert!( tor_controller @@ -705,11 +705,11 @@ fn test_tor_controller() -> anyhow::Result<()> { // now create a second controller { let control_stream = - ControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?; + LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?; // create a tor controller and send authentication command // all async events are just printed to stdout - let mut tor_controller = TorController::new(control_stream)?; + let mut tor_controller = LegacyTorController::new(control_stream)?; tor_controller.authenticate(tor_process.get_password())?; // ensure everything is matching our default_torrc settings diff --git a/tor-interface/src/legacy_tor_process.rs b/tor-interface/src/legacy_tor_process.rs index a3e8254cf..90c9e2392 100644 --- a/tor-interface/src/legacy_tor_process.rs +++ b/tor-interface/src/legacy_tor_process.rs @@ -51,14 +51,14 @@ pub enum Error { #[error("failed to remove control_port file")] ControlPortFileDeleteFailed(#[source] std::io::Error), - #[error("failed to start tor process")] - TorProcessStartFailed(#[source] std::io::Error), + #[error("failed to start legacy tor process")] + LegacyTorProcessStartFailed(#[source] std::io::Error), #[error("failed to read control addr from control_file '{0}'")] ControlPortFileMissing(String), - #[error("unable to take tor process stdout")] - TorProcessStdoutTakeFailed(), + #[error("unable to take legacy tor process stdout")] + LegacyTorProcessStdoutTakeFailed(), #[error("failed to spawn tor process stdout read thread")] StdoutReadThreadSpawnFailed(#[source] std::io::Error), @@ -95,7 +95,7 @@ fn read_control_port_file(control_port_file: &Path) -> Result } // Encapsulates the tor daemon process -pub(crate) struct TorProcess { +pub(crate) struct LegacyTorProcess { control_addr: SocketAddr, process: Child, password: String, @@ -103,7 +103,7 @@ pub(crate) struct TorProcess { stdout_lines: Arc>>, } -impl TorProcess { +impl LegacyTorProcess { pub fn get_control_addr(&self) -> &SocketAddr { &self.control_addr } @@ -112,7 +112,7 @@ impl TorProcess { &self.password } - pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { + pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { if tor_bin_path.is_relative() { return Err(Error::TorBinPathNotAbsolute(format!( "{}", @@ -202,7 +202,7 @@ impl TorProcess { .arg("__OwningControllerProcess") .arg(process::id().to_string()) .spawn() - .map_err(Error::TorProcessStartFailed)?; + .map_err(Error::LegacyTorProcessStartFailed)?; let mut control_addr = None; let start = Instant::now(); @@ -233,18 +233,18 @@ impl TorProcess { let stdout_lines = Arc::downgrade(&stdout_lines); let stdout = BufReader::new(match process.stdout.take() { Some(stdout) => stdout, - None => return Err(Error::TorProcessStdoutTakeFailed()), + None => return Err(Error::LegacyTorProcessStdoutTakeFailed()), }); std::thread::Builder::new() .name("tor_stdout_reader".to_string()) .spawn(move || { - TorProcess::read_stdout_task(&stdout_lines, stdout); + LegacyTorProcess::read_stdout_task(&stdout_lines, stdout); }) .map_err(Error::StdoutReadThreadSpawnFailed)?; } - Ok(TorProcess { + Ok(LegacyTorProcess { control_addr, process, password, @@ -281,7 +281,7 @@ impl TorProcess { } } -impl Drop for TorProcess { +impl Drop for LegacyTorProcess { fn drop(&mut self) { let _ = self.process.kill(); } diff --git a/tor-interface/src/legacy_tor_version.rs b/tor-interface/src/legacy_tor_version.rs index 7e00ed113..780f45b41 100644 --- a/tor-interface/src/legacy_tor_version.rs +++ b/tor-interface/src/legacy_tor_version.rs @@ -12,7 +12,7 @@ pub enum Error { // see version-spec.txt #[derive(Clone)] -pub struct TorVersion { +pub struct LegacyTorVersion { pub major: u32, pub minor: u32, pub micro: u32, @@ -20,7 +20,7 @@ pub struct TorVersion { pub status_tag: Option, } -impl TorVersion { +impl LegacyTorVersion { fn status_tag_pattern_is_match(status_tag: &str) -> bool { if status_tag.is_empty() { return false; @@ -40,7 +40,7 @@ impl TorVersion { micro: u32, patch_level: Option, status_tag: Option<&str>, - ) -> Result { + ) -> Result { let status_tag = if let Some(status_tag) = status_tag { if Self::status_tag_pattern_is_match(status_tag) { Some(status_tag.to_string()) @@ -53,7 +53,7 @@ impl TorVersion { None }; - Ok(TorVersion { + Ok(LegacyTorVersion { major, minor, micro, @@ -63,10 +63,10 @@ impl TorVersion { } } -impl FromStr for TorVersion { +impl FromStr for LegacyTorVersion { type Err = Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { // MAJOR.MINOR.MICRO[.PATCHLEVEL][-STATUS_TAG][ (EXTRA_INFO)]* let mut tokens = s.split(' '); let (major, minor, micro, patch_level, status_tag) = @@ -152,7 +152,7 @@ impl FromStr for TorVersion { ))); } } - TorVersion::new( + LegacyTorVersion::new( major, minor, micro, @@ -162,7 +162,7 @@ impl FromStr for TorVersion { } } -impl ToString for TorVersion { +impl ToString for LegacyTorVersion { fn to_string(&self) -> String { match &self.status_tag { Some(status_tag) => format!( @@ -177,7 +177,7 @@ impl ToString for TorVersion { } } -impl PartialEq for TorVersion { +impl PartialEq for LegacyTorVersion { fn eq(&self, other: &Self) -> bool { self.major == other.major && self.minor == other.minor @@ -187,7 +187,7 @@ impl PartialEq for TorVersion { } } -impl PartialOrd for TorVersion { +impl PartialOrd for LegacyTorVersion { fn partial_cmp(&self, other: &Self) -> Option { if let Some(order) = self.major.partial_cmp(&other.major) { if order != Ordering::Equal { @@ -230,42 +230,61 @@ impl PartialOrd for TorVersion { #[test] fn test_version() -> anyhow::Result<()> { - assert!(TorVersion::from_str("1.2.3")? == TorVersion::new(1, 2, 3, None, None)?); - assert!(TorVersion::from_str("1.2.3.4")? == TorVersion::new(1, 2, 3, Some(4), None)?); - assert!(TorVersion::from_str("1.2.3-test")? == TorVersion::new(1, 2, 3, None, Some("test"))?); + assert!(LegacyTorVersion::from_str("1.2.3")? == LegacyTorVersion::new(1, 2, 3, None, None)?); assert!( - TorVersion::from_str("1.2.3.4-test")? == TorVersion::new(1, 2, 3, Some(4), Some("test"))? + LegacyTorVersion::from_str("1.2.3.4")? == LegacyTorVersion::new(1, 2, 3, Some(4), None)? ); - assert!(TorVersion::from_str("1.2.3 (extra_info)")? == TorVersion::new(1, 2, 3, None, None)?); assert!( - TorVersion::from_str("1.2.3.4 (extra_info)")? == TorVersion::new(1, 2, 3, Some(4), None)? + LegacyTorVersion::from_str("1.2.3-test")? + == LegacyTorVersion::new(1, 2, 3, None, Some("test"))? ); assert!( - TorVersion::from_str("1.2.3.4-tag (extra_info)")? - == TorVersion::new(1, 2, 3, Some(4), Some("tag"))? + LegacyTorVersion::from_str("1.2.3.4-test")? + == LegacyTorVersion::new(1, 2, 3, Some(4), Some("test"))? + ); + assert!( + LegacyTorVersion::from_str("1.2.3 (extra_info)")? + == LegacyTorVersion::new(1, 2, 3, None, None)? + ); + assert!( + LegacyTorVersion::from_str("1.2.3.4 (extra_info)")? + == LegacyTorVersion::new(1, 2, 3, Some(4), None)? + ); + assert!( + LegacyTorVersion::from_str("1.2.3.4-tag (extra_info)")? + == LegacyTorVersion::new(1, 2, 3, Some(4), Some("tag"))? ); assert!( - TorVersion::from_str("1.2.3.4-tag (extra_info) (extra_info)")? - == TorVersion::new(1, 2, 3, Some(4), Some("tag"))? + LegacyTorVersion::from_str("1.2.3.4-tag (extra_info) (extra_info)")? + == LegacyTorVersion::new(1, 2, 3, Some(4), Some("tag"))? ); - assert!(TorVersion::new(1, 2, 3, Some(4), Some("spaced tag")).is_err()); - assert!(TorVersion::new(1, 2, 3, Some(4), Some("" /* empty tag */)).is_err()); - assert!(TorVersion::from_str("").is_err()); - assert!(TorVersion::from_str("1.2").is_err()); - assert!(TorVersion::from_str("1.2-foo").is_err()); - assert!(TorVersion::from_str("1.2.3.4-foo bar").is_err()); - assert!(TorVersion::from_str("1.2.3.4-foo bar (extra_info)").is_err()); - assert!(TorVersion::from_str("1.2.3.4-foo (extra_info) badtext").is_err()); - assert!(TorVersion::new(0, 0, 0, Some(0), None)? < TorVersion::new(1, 0, 0, Some(0), None)?); - assert!(TorVersion::new(0, 0, 0, Some(0), None)? < TorVersion::new(0, 1, 0, Some(0), None)?); - assert!(TorVersion::new(0, 0, 0, Some(0), None)? < TorVersion::new(0, 0, 1, Some(0), None)?); + assert!(LegacyTorVersion::new(1, 2, 3, Some(4), Some("spaced tag")).is_err()); + assert!(LegacyTorVersion::new(1, 2, 3, Some(4), Some("" /* empty tag */)).is_err()); + assert!(LegacyTorVersion::from_str("").is_err()); + assert!(LegacyTorVersion::from_str("1.2").is_err()); + assert!(LegacyTorVersion::from_str("1.2-foo").is_err()); + assert!(LegacyTorVersion::from_str("1.2.3.4-foo bar").is_err()); + assert!(LegacyTorVersion::from_str("1.2.3.4-foo bar (extra_info)").is_err()); + assert!(LegacyTorVersion::from_str("1.2.3.4-foo (extra_info) badtext").is_err()); + assert!( + LegacyTorVersion::new(0, 0, 0, Some(0), None)? + < LegacyTorVersion::new(1, 0, 0, Some(0), None)? + ); + assert!( + LegacyTorVersion::new(0, 0, 0, Some(0), None)? + < LegacyTorVersion::new(0, 1, 0, Some(0), None)? + ); + assert!( + LegacyTorVersion::new(0, 0, 0, Some(0), None)? + < LegacyTorVersion::new(0, 0, 1, Some(0), None)? + ); // ensure status tags make comparison between equal versions (apart from // tags) unknowable - let zero_version = TorVersion::new(0, 0, 0, Some(0), None)?; - let zero_version_tag = TorVersion::new(0, 0, 0, Some(0), Some("tag"))?; + let zero_version = LegacyTorVersion::new(0, 0, 0, Some(0), None)?; + let zero_version_tag = LegacyTorVersion::new(0, 0, 0, Some(0), Some("tag"))?; assert!(!(zero_version < zero_version_tag)); assert!(!(zero_version <= zero_version_tag)); From 53e5cb1f72383bc46f85c1c132c76191234ef339 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 18 Jun 2023 23:57:07 +0000 Subject: [PATCH 015/184] tor-interface: fixed clippy errors --- tor-interface/src/legacy_tor_controller.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index 5556924ef..e4e9c4c08 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -39,7 +39,7 @@ pub enum Error { InvalidCommandArguments(String), #[error("command failed: {0} {}", .1.join("\n"))] - CommandReturnedError(u32, Vec), + CommandFailed(u32, Vec), #[error("failed to parse command reply: {0}")] CommandReplyParseFailed(String), @@ -430,7 +430,7 @@ impl LegacyTorController { match reply.status_code { 250u32 => Ok(()), - code => Err(Error::CommandReturnedError(code, reply.reply_lines)), + code => Err(Error::CommandFailed(code, reply.reply_lines)), } } @@ -450,7 +450,7 @@ impl LegacyTorController { } Ok(key_values) } - code => Err(Error::CommandReturnedError(code, reply.reply_lines)), + code => Err(Error::CommandFailed(code, reply.reply_lines)), } } @@ -459,7 +459,7 @@ impl LegacyTorController { match reply.status_code { 250u32 => Ok(()), - code => Err(Error::CommandReturnedError(code, reply.reply_lines)), + code => Err(Error::CommandFailed(code, reply.reply_lines)), } } @@ -468,7 +468,7 @@ impl LegacyTorController { match reply.status_code { 250u32 => Ok(()), - code => Err(Error::CommandReturnedError(code, reply.reply_lines)), + code => Err(Error::CommandFailed(code, reply.reply_lines)), } } @@ -491,7 +491,7 @@ impl LegacyTorController { } Ok(key_values) } - code => Err(Error::CommandReturnedError(code, reply.reply_lines)), + code => Err(Error::CommandFailed(code, reply.reply_lines)), } } @@ -560,7 +560,7 @@ impl LegacyTorController { } } } - code => return Err(Error::CommandReturnedError(code, reply.reply_lines)), + code => return Err(Error::CommandFailed(code, reply.reply_lines)), } if flags.discard_pk { @@ -588,7 +588,7 @@ impl LegacyTorController { match reply.status_code { 250u32 => Ok(()), - code => Err(Error::CommandReturnedError(code, reply.reply_lines)), + code => Err(Error::CommandFailed(code, reply.reply_lines)), } } @@ -655,7 +655,7 @@ impl LegacyTorController { match reply.status_code { 250u32..=252u32 => Ok(()), - code => Err(Error::CommandReturnedError(code, reply.reply_lines)), + code => Err(Error::CommandFailed(code, reply.reply_lines)), } } @@ -665,7 +665,7 @@ impl LegacyTorController { match reply.status_code { 250u32..=251u32 => Ok(()), - code => Err(Error::CommandReturnedError(code, reply.reply_lines)), + code => Err(Error::CommandFailed(code, reply.reply_lines)), } } } From 8e2249385c8904f103da9a08f15c90a0d0dbfd22 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 19 Jun 2023 00:36:31 +0000 Subject: [PATCH 016/184] tor-interface: implemented skeleton for MockTorClient TorProvider --- tor-interface/src/lib.rs | 1 + tor-interface/src/mock_tor_client.rs | 69 ++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tor-interface/src/mock_tor_client.rs diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index f483178b2..5cef0c573 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -3,5 +3,6 @@ mod legacy_tor_control_stream; mod legacy_tor_controller; mod legacy_tor_process; mod legacy_tor_version; +pub mod mock_tor_client; pub mod tor_crypto; pub mod tor_provider; diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs new file mode 100644 index 000000000..b74840f4c --- /dev/null +++ b/tor-interface/src/mock_tor_client.rs @@ -0,0 +1,69 @@ +// standard + +// internal crates +use crate::tor_crypto::*; +use crate::tor_provider::*; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Function not implemented")] + NotImplemented(), +} + +pub struct MockCircuitToken {} +impl CircuitToken for MockCircuitToken {} + +pub struct MockOnionListener {} + +impl OnionListener for MockOnionListener { + fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { + Ok(()) + } + fn accept(&self) -> Result, std::io::Error> { + Ok(None) + } +} + +pub struct MockTorClient {} + +impl TorProvider for MockTorClient { + type Error = Error; + + fn update(&mut self) -> Result, Self::Error> { + Err(Error::NotImplemented()) + } + + fn bootstrap(&mut self) -> Result<(), Self::Error> { + Err(Error::NotImplemented()) + } + + fn add_client_auth( + &mut self, + service_id: &V3OnionServiceId, + clien_auth: &X25519PrivateKey, + ) -> Result<(), Self::Error> { + Err(Error::NotImplemented()) + } + + fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Self::Error> { + Err(Error::NotImplemented()) + } + + fn connect( + &mut self, + service_id: &V3OnionServiceId, + virt_port: u16, + circuit: Option, + ) -> Result { + Err(Error::NotImplemented()) + } + + fn listener( + &mut self, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorized_clients: Option<&[X25519PublicKey]>, + ) -> Result { + Err(Error::NotImplemented()) + } +} From f098e21877781d9f710eb17be6cb59833482ed82 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 19 Jun 2023 02:27:16 +0000 Subject: [PATCH 017/184] tor-interface: refactored OnionAddr to more closely align with SocketAddr --- tor-interface/src/legacy_tor_client.rs | 9 +++++--- tor-interface/src/tor_provider.rs | 29 ++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 78860be72..46e0485df 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -351,10 +351,10 @@ impl TorProvider for LegacyTorClient { Ok(OnionStream { stream: stream.into_inner(), local_addr: None, - peer_addr: Some(TargetAddr::OnionService(OnionAddr::V3( + peer_addr: Some(TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3::new( service_id.clone(), virt_port, - ))), + )))), }) } @@ -380,7 +380,10 @@ impl TorProvider for LegacyTorClient { flags.v3_auth = true; } - let onion_addr = OnionAddr::V3(V3OnionServiceId::from_private_key(private_key), virt_port); + let onion_addr = OnionAddr::V3(OnionAddrV3::new( + V3OnionServiceId::from_private_key(private_key), + virt_port, + )); // start onion service let (_, service_id) = self diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index cb4c7f12a..9ddedb441 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -6,15 +6,40 @@ use std::ops::{Deref, DerefMut}; // internal crates use crate::tor_crypto::*; +#[derive(Clone, Debug)] +pub struct OnionAddrV3 { + service_id: V3OnionServiceId, + virt_port: u16, +} + +impl OnionAddrV3 { + pub fn new(service_id: V3OnionServiceId, virt_port: u16) -> OnionAddrV3 { + OnionAddrV3 { + service_id, + virt_port, + } + } + + pub fn service_id(&self) -> &V3OnionServiceId { + &self.service_id + } +} + +impl std::fmt::Display for OnionAddrV3 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.service_id, self.virt_port) + } +} + #[derive(Clone, Debug)] pub enum OnionAddr { - V3(V3OnionServiceId, u16), + V3(OnionAddrV3), } impl std::fmt::Display for OnionAddr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - OnionAddr::V3(service_id, port) => write!(f, "{}:{}", service_id, port), + OnionAddr::V3(onion_addr) => onion_addr.fmt(f), } } } From 312184f208da4b877f2d9d39964ecd8a4a81644e Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 19 Jun 2023 18:11:56 +0000 Subject: [PATCH 018/184] tor-interface: derived more helper traits for OnionAddr, OnionAddrV3 and their use tor-crypto tpyes --- tor-interface/src/tor_crypto.rs | 4 ++-- tor-interface/src/tor_provider.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 5f00b70dc..6c717a039 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -183,12 +183,12 @@ pub struct X25519PrivateKey { secret_key: pk::curve25519::StaticSecret, } -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq, Hash)] pub struct X25519PublicKey { public_key: pk::curve25519::PublicKey, } -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct V3OnionServiceId { data: [u8; V3_ONION_SERVICE_ID_LENGTH], } diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 9ddedb441..682fb7e8d 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -6,7 +6,7 @@ use std::ops::{Deref, DerefMut}; // internal crates use crate::tor_crypto::*; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct OnionAddrV3 { service_id: V3OnionServiceId, virt_port: u16, @@ -31,7 +31,7 @@ impl std::fmt::Display for OnionAddrV3 { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum OnionAddr { V3(OnionAddrV3), } From cd5ccc6ed7c4a2bf25f8d1ef71dfcc65fef51140 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 19 Jun 2023 18:12:19 +0000 Subject: [PATCH 019/184] tor-interface: initial MockTorClient implementation --- tor-interface/src/mock_tor_client.rs | 472 ++++++++++++++++++++++++++- 1 file changed, 458 insertions(+), 14 deletions(-) diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index b74840f4c..db26b7e34 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -1,4 +1,10 @@ // standard +use std::collections::BTreeMap; +use std::io::ErrorKind; +#[cfg(test)] +use std::io::{Read, Write}; +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::sync::{atomic, Arc, Mutex}; // internal crates use crate::tor_crypto::*; @@ -6,56 +12,242 @@ use crate::tor_provider::*; #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("Function not implemented")] - NotImplemented(), + #[error("client not bootstrapped")] + ClientNotBootstrapped(), + + #[error("client already bootstrapped")] + ClientAlreadyBootstrapped(), + + #[error("onion service not found: {}", .0)] + OnionServiceNotFound(OnionAddr), + + #[error("onion service not published: {}", .0)] + OnionServiceNoPublished(OnionAddr), + + #[error("onion service requires onion auth")] + OnionServiceRequiresOnionAuth(), + + #[error("provided onion auth key invalid")] + OnionServiceAuthInvalid(), + + #[error("unable to bind TCP listener")] + TcpListenerBindFailed(#[source] std::io::Error), + + #[error("unable to get TCP listener's local adress")] + TcpListenerLocalAddrFailed(#[source] std::io::Error), } pub struct MockCircuitToken {} impl CircuitToken for MockCircuitToken {} -pub struct MockOnionListener {} +pub struct MockOnionListener { + listener: std::net::TcpListener, + is_active: Arc, + onion_addr: OnionAddr, +} impl OnionListener for MockOnionListener { fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { - Ok(()) + self.listener.set_nonblocking(nonblocking) } fn accept(&self) -> Result, std::io::Error> { - Ok(None) + match self.listener.accept() { + Ok((stream, _socket_addr)) => Ok(Some(OnionStream { + stream, + local_addr: Some(self.onion_addr.clone()), + peer_addr: None, + })), + Err(err) => { + if err.kind() == ErrorKind::WouldBlock { + Ok(None) + } else { + Err(err) + } + } + } + } +} + +impl Drop for MockOnionListener { + fn drop(&mut self) { + self.is_active.store(false, atomic::Ordering::Relaxed); + } +} + +struct MockTorNetwork { + onion_services: BTreeMap, SocketAddr)>, +} + +impl MockTorNetwork { + const fn new() -> MockTorNetwork { + MockTorNetwork { + onion_services: BTreeMap::new(), + } + } + + fn connect_to_onion( + &mut self, + service_id: &V3OnionServiceId, + virt_port: u16, + client_auth: Option<&X25519PublicKey>, + ) -> Result { + let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port)); + if let Some((client_auth_keys, socket_addr)) = self.onion_services.get(&onion_addr) { + match (client_auth_keys.len(), client_auth) { + (0, None) => (), + (_, None) => return Err(Error::OnionServiceRequiresOnionAuth()), + (0, Some(_)) => return Err(Error::OnionServiceAuthInvalid()), + (_, Some(client_auth)) => { + if !client_auth_keys.contains(client_auth) { + return Err(Error::OnionServiceAuthInvalid()); + } + } + } + + if let Ok(stream) = TcpStream::connect(socket_addr) { + Ok(OnionStream { + stream, + local_addr: None, + peer_addr: Some(TargetAddr::OnionService(onion_addr)), + }) + } else { + Err(Error::OnionServiceNotFound(onion_addr)) + } + } else { + Err(Error::OnionServiceNoPublished(onion_addr)) + } + } + + fn start_onion( + &mut self, + service_id: V3OnionServiceId, + virt_port: u16, + client_auth_keys: Vec, + address: SocketAddr, + ) { + let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id, virt_port)); + self.onion_services + .insert(onion_addr, (client_auth_keys, address)); } + + fn stop_onion(&mut self, onion_addr: &OnionAddr) { + self.onion_services.remove(onion_addr); + } +} + +static MOCK_TOR_NETWORK: Mutex = Mutex::new(MockTorNetwork::new()); + +pub struct MockTorClient { + events: Vec, + bootstrapped: bool, + client_auth_keys: BTreeMap, + onion_services: Vec<(OnionAddr, Arc)>, } -pub struct MockTorClient {} +impl MockTorClient { + pub fn new() -> MockTorClient { + let mut events: Vec = Default::default(); + let line = "[notice] MockTorClient running".to_string(); + events.push(TorEvent::LogReceived { line }); + + MockTorClient { + events, + bootstrapped: false, + client_auth_keys: Default::default(), + onion_services: Default::default(), + } + } +} + +impl Default for MockTorClient { + fn default() -> Self { + Self::new() + } +} impl TorProvider for MockTorClient { type Error = Error; fn update(&mut self) -> Result, Self::Error> { - Err(Error::NotImplemented()) + match MOCK_TOR_NETWORK.lock() { + Ok(mut mock_tor_network) => { + let mut i = 0; + while i < self.onion_services.len() { + // remove onion services with no active listeners + if !self.onion_services[i].1.load(atomic::Ordering::Relaxed) { + let entry = self.onion_services.swap_remove(i); + let onion_addr = entry.0; + mock_tor_network.stop_onion(&onion_addr); + } else { + i += 1; + } + } + } + Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"), + } + + Ok(std::mem::take(&mut self.events)) } fn bootstrap(&mut self) -> Result<(), Self::Error> { - Err(Error::NotImplemented()) + if self.bootstrapped { + Err(Error::ClientAlreadyBootstrapped()) + } else { + self.events.push(TorEvent::BootstrapStatus { + progress: 0u32, + tag: "start".to_string(), + summary: "bootstrapping started".to_string(), + }); + self.events.push(TorEvent::BootstrapStatus { + progress: 50u32, + tag: "middle".to_string(), + summary: "bootstrapping continues".to_string(), + }); + self.events.push(TorEvent::BootstrapStatus { + progress: 100u32, + tag: "finished".to_string(), + summary: "bootstrapping completed".to_string(), + }); + self.events.push(TorEvent::BootstrapComplete); + self.bootstrapped = true; + Ok(()) + } } fn add_client_auth( &mut self, service_id: &V3OnionServiceId, - clien_auth: &X25519PrivateKey, + client_auth: &X25519PrivateKey, ) -> Result<(), Self::Error> { - Err(Error::NotImplemented()) + let client_auth_public = X25519PublicKey::from_private_key(client_auth); + if let Some(key) = self.client_auth_keys.get_mut(service_id) { + *key = client_auth_public; + } else { + self.client_auth_keys + .insert(service_id.clone(), client_auth_public); + } + Ok(()) } fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Self::Error> { - Err(Error::NotImplemented()) + self.client_auth_keys.remove(service_id); + Ok(()) } fn connect( &mut self, service_id: &V3OnionServiceId, virt_port: u16, - circuit: Option, + _circuit: Option, ) -> Result { - Err(Error::NotImplemented()) + let client_auth = self.client_auth_keys.get(service_id); + + match MOCK_TOR_NETWORK.lock() { + Ok(mut mock_tor_network) => { + mock_tor_network.connect_to_onion(service_id, virt_port, client_auth) + } + Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"), + } } fn listener( @@ -64,6 +256,258 @@ impl TorProvider for MockTorClient { virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, ) -> Result { - Err(Error::NotImplemented()) + // convert inputs to relevant types + let service_id = V3OnionServiceId::from_private_key(private_key); + let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port)); + let authorized_clients: Vec = match authorized_clients { + Some(keys) => keys.into(), + None => Default::default(), + }; + + // try to bind to a local address, let OS pick our port + let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); + let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; + let socket_addr = listener + .local_addr() + .map_err(Error::TcpListenerLocalAddrFailed)?; + + // register the onion service with the mock tor network + match MOCK_TOR_NETWORK.lock() { + Ok(mut mock_tor_network) => mock_tor_network.start_onion( + service_id.clone(), + virt_port, + authorized_clients, + socket_addr, + ), + Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"), + } + + // init flag for signaling when listener goes out of scope so we can tear down onion service + let is_active = Arc::new(atomic::AtomicBool::new(true)); + self.onion_services + .push((onion_addr.clone(), Arc::clone(&is_active))); + + // onion service published event + self.events + .push(TorEvent::OnionServicePublished { service_id }); + + Ok(MockOnionListener { + listener, + is_active, + onion_addr, + }) + } +} + +impl Drop for MockTorClient { + fn drop(&mut self) { + // remove all our onion services + match MOCK_TOR_NETWORK.lock() { + Ok(mut mock_tor_network) => { + for entry in self.onion_services.iter() { + let onion_addr = &entry.0; + mock_tor_network.stop_onion(onion_addr); + } + } + Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"), + } + } +} + +#[test] +fn test_mock_client() -> anyhow::Result<()> { + let mut tor = MockTorClient::new(); + tor.bootstrap()?; + + let mut received_log = false; + let mut bootstrap_complete = false; + while !bootstrap_complete { + for event in tor.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Bootstrap Complete!"); + bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + received_log = true; + println!("--- {}", line); + } + _ => {} + } + } + } + assert!( + received_log, + "should have received a log line from tor daemon" + ); + + Ok(()) +} + +#[test] +fn test_mock_onion_service() -> anyhow::Result<()> { + let mut tor = MockTorClient::new(); + + // for 30secs for bootstrap + tor.bootstrap()?; + + let mut bootstrap_complete = false; + while !bootstrap_complete { + for event in tor.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Bootstrap Complete!"); + bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + _ => {} + } + } + } + + // vanilla V3 onion service + { + // create an onion service for this test + let private_key = Ed25519PrivateKey::generate(); + + println!("Starting and listening to onion service"); + const VIRT_PORT: u16 = 42069u16; + let listener = tor.listener(&private_key, VIRT_PORT, None)?; + + let mut onion_published = false; + while !onion_published { + for event in tor.update()?.iter() { + match event { + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + TorEvent::OnionServicePublished { service_id } => { + let expected_service_id = V3OnionServiceId::from_private_key(&private_key); + if expected_service_id == *service_id { + println!("Onion Service {} published", service_id.to_string()); + onion_published = true; + } + } + _ => {} + } + } + } + + const MESSAGE: &str = "Hello World!"; + + { + let service_id = V3OnionServiceId::from_private_key(&private_key); + + println!("Connecting to onion service"); + let mut client = tor.connect(&service_id, VIRT_PORT, None)?; + println!("Client writing message: '{}'", MESSAGE); + client.write_all(MESSAGE.as_bytes())?; + client.flush()?; + println!("End of client scope"); + } + + if let Some(mut server) = listener.accept()? { + println!("Server reading message"); + let mut buffer = Vec::new(); + server.read_to_end(&mut buffer)?; + let msg = String::from_utf8(buffer)?; + + assert!(MESSAGE == msg); + println!("Message received: '{}'", msg); + } else { + panic!("no listener"); + } + } + + // authenticated onion service + { + // create an onion service for this test + let private_key = Ed25519PrivateKey::generate(); + + let private_auth_key = X25519PrivateKey::generate(); + let public_auth_key = X25519PublicKey::from_private_key(&private_auth_key); + + println!("Starting and listening to authenticated onion service"); + const VIRT_PORT: u16 = 42069u16; + let listener = tor.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]))?; + + let mut onion_published = false; + while !onion_published { + for event in tor.update()?.iter() { + match event { + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + TorEvent::OnionServicePublished { service_id } => { + let expected_service_id = V3OnionServiceId::from_private_key(&private_key); + if expected_service_id == *service_id { + println!( + "Authenticated Onion Service {} published", + service_id.to_string() + ); + onion_published = true; + } + } + _ => {} + } + } + } + + const MESSAGE: &str = "Hello World!"; + + { + let service_id = V3OnionServiceId::from_private_key(&private_key); + + println!("Connecting to onion service (should fail)"); + assert!( + tor.connect(&service_id, VIRT_PORT, None).is_err(), + "should not able to connect to an authenticated onion service without auth key" + ); + + println!("Add auth key for onion service"); + tor.add_client_auth(&service_id, &private_auth_key)?; + + println!("Connecting to onion service with authentication"); + let mut client = tor.connect(&service_id, VIRT_PORT, None)?; + + println!("Client writing message: '{}'", MESSAGE); + client.write_all(MESSAGE.as_bytes())?; + client.flush()?; + println!("End of client scope"); + + println!("Remove auth key for onion service"); + tor.remove_client_auth(&service_id)?; + } + + if let Some(mut server) = listener.accept()? { + println!("Server reading message"); + let mut buffer = Vec::new(); + server.read_to_end(&mut buffer)?; + let msg = String::from_utf8(buffer)?; + + assert!(MESSAGE == msg); + println!("Message received: '{}'", msg); + } else { + panic!("no listener"); + } } + Ok(()) } From b7ea86b227a7744a83ac926e7fddcecc0ba9c716 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 23 Jun 2023 20:56:37 +0000 Subject: [PATCH 020/184] tor-interface: refactor to enable dynamic usage of a virtual TorProvider object - CircuitToken is now just a usize; leave it to TorProvier implementations to generate and manage them - OnionListener is now a pointer-to-implementation wrapper, trait moved to OnionListnerImpl - Update LegacyTorClient to use new interface definitions --- tor-interface/Cargo.toml | 1 - tor-interface/src/legacy_tor_client.rs | 60 ++++++++++++++++++-------- tor-interface/src/mock_tor_client.rs | 25 +++++++---- tor-interface/src/tor_provider.rs | 27 +++++++++--- 4 files changed, 81 insertions(+), 32 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 7a9073e49..6f7453e91 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -15,7 +15,6 @@ rust-crypto = "^0.2" signature = "1.5.0" socks = "0.3.4" tor-llcrypto = { version = "0.2.0", features = ["relay"] } -url = "2.2.2" thiserror = "1.0" [dev-dependencies] diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 46e0485df..2b6897657 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -1,4 +1,6 @@ // standard +use std::boxed::Box; +use std::collections::BTreeMap; use std::default::Default; use std::io::ErrorKind; #[cfg(test)] @@ -15,7 +17,6 @@ use std::time::Duration; #[cfg(test)] use serial_test::serial; use socks::Socks5Stream; -use url::Host; // internal crates use crate::legacy_tor_control_stream::*; @@ -69,6 +70,9 @@ pub enum Error { #[error("no socks listeners available to connect through")] NoSocksListenersFound(), + #[error("invalid circuit token")] + CircuitTokenInvalid(), + #[error("unable to connect to socks listener")] Socks5ConnectionFailed(#[source] std::io::Error), @@ -92,17 +96,16 @@ pub struct LegacyCircuitToken { impl LegacyCircuitToken { #[allow(dead_code)] - pub fn new(first_party: Host) -> LegacyCircuitToken { + pub fn new() -> LegacyCircuitToken { + const CIRCUIT_TOKEN_USERNAME_LENGTH: usize = 32usize; const CIRCUIT_TOKEN_PASSWORD_LENGTH: usize = 32usize; - let username = first_party.to_string(); + let username = generate_password(CIRCUIT_TOKEN_USERNAME_LENGTH); let password = generate_password(CIRCUIT_TOKEN_PASSWORD_LENGTH); LegacyCircuitToken { username, password } } } -impl CircuitToken for LegacyCircuitToken {} - // // LegacyOnionListener // @@ -113,7 +116,7 @@ pub struct LegacyOnionListener { onion_addr: OnionAddr, } -impl OnionListener for LegacyOnionListener { +impl OnionListenerImpl for LegacyOnionListener { fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { self.listener.set_nonblocking(nonblocking) } @@ -149,6 +152,9 @@ pub struct LegacyTorClient { socks_listener: Option, // list of open onion services and their is_active flag onion_services: Vec<(V3OnionServiceId, Arc)>, + // our list of circuit tokens for the tor daemon + circuit_token_counter: usize, + circuit_tokens: BTreeMap } impl LegacyTorClient { @@ -201,6 +207,8 @@ impl LegacyTorClient { controller, socks_listener: None, onion_services: Default::default(), + circuit_token_counter: 0usize, + circuit_tokens: Default::default(), }) } @@ -210,7 +218,7 @@ impl LegacyTorClient { } } -impl TorProvider for LegacyTorClient { +impl TorProvider for LegacyTorClient { type Error = Error; fn update(&mut self) -> Result, Error> { @@ -316,7 +324,7 @@ impl TorProvider for LegacyTorClient { &mut self, service_id: &V3OnionServiceId, virt_port: u16, - circuit: Option, + circuit: Option, ) -> Result { if self.socks_listener.is_none() { let mut listeners = self @@ -339,12 +347,17 @@ impl TorProvider for LegacyTorClient { // readwrite stream let stream = match &circuit { None => Socks5Stream::connect(socks_listener, target), - Some(circuit) => Socks5Stream::connect_with_password( - socks_listener, - target, - &circuit.username, - &circuit.password, - ), + Some(circuit) => { + if let Some(circuit) = self.circuit_tokens.get(circuit) { + Socks5Stream::connect_with_password( + socks_listener, + target, + &circuit.username, + &circuit.password) + } else { + return Err(Error::CircuitTokenInvalid()); + } + }, } .map_err(Error::Socks5ConnectionFailed)?; @@ -364,7 +377,7 @@ impl TorProvider for LegacyTorClient { private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { // try to bind to a local address, let OS pick our port let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; @@ -402,11 +415,24 @@ impl TorProvider for LegacyTorClient { self.onion_services .push((service_id, Arc::clone(&is_active))); - Ok(LegacyOnionListener { + let onion_listener = Box::new(LegacyOnionListener { listener, is_active, onion_addr, - }) + }); + + Ok(OnionListener{onion_listener}) + } + + fn generate_token(&mut self) -> CircuitToken { + let new_token = self.circuit_token_counter; + self.circuit_token_counter += 1; + self.circuit_tokens.insert(new_token, LegacyCircuitToken::new()); + new_token + } + + fn release_token(&mut self, circuit_token: CircuitToken) { + self.circuit_tokens.remove(&circuit_token); } } diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index db26b7e34..16083bb3e 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -37,16 +37,13 @@ pub enum Error { TcpListenerLocalAddrFailed(#[source] std::io::Error), } -pub struct MockCircuitToken {} -impl CircuitToken for MockCircuitToken {} - pub struct MockOnionListener { listener: std::net::TcpListener, is_active: Arc, onion_addr: OnionAddr, } -impl OnionListener for MockOnionListener { +impl OnionListenerImpl for MockOnionListener { fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { self.listener.set_nonblocking(nonblocking) } @@ -165,7 +162,7 @@ impl Default for MockTorClient { } } -impl TorProvider for MockTorClient { +impl TorProvider for MockTorClient { type Error = Error; fn update(&mut self) -> Result, Self::Error> { @@ -238,7 +235,7 @@ impl TorProvider for MockTorClient { &mut self, service_id: &V3OnionServiceId, virt_port: u16, - _circuit: Option, + _circuit: Option, ) -> Result { let client_auth = self.client_auth_keys.get(service_id); @@ -255,7 +252,7 @@ impl TorProvider for MockTorClient { private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { // convert inputs to relevant types let service_id = V3OnionServiceId::from_private_key(private_key); let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port)); @@ -291,11 +288,21 @@ impl TorProvider for MockTorClient { self.events .push(TorEvent::OnionServicePublished { service_id }); - Ok(MockOnionListener { + let onion_listener = Box::new(MockOnionListener { listener, is_active, onion_addr, - }) + }); + + Ok(OnionListener{onion_listener}) + } + + fn generate_token(&mut self) -> CircuitToken { + 0usize + } + + fn release_token(&mut self, _token: CircuitToken) { + } } diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 682fb7e8d..f994e3812 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -1,4 +1,5 @@ // standard +use std::boxed::Box; use std::io::{Read, Write}; use std::net::TcpStream; use std::ops::{Deref, DerefMut}; @@ -66,7 +67,7 @@ pub enum TorEvent { }, } -pub trait CircuitToken {} +pub type CircuitToken = usize; // // OnionStream Implementation @@ -131,12 +132,26 @@ impl OnionStream { } } -pub trait OnionListener { +pub trait OnionListenerImpl : Send { fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error>; fn accept(&self) -> Result, std::io::Error>; } -pub trait TorProvider { +pub struct OnionListener { + pub(crate) onion_listener: Box, +} + +impl OnionListener { + pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { + self.onion_listener.set_nonblocking(nonblocking) + } + + pub fn accept(&self) -> Result, std::io::Error> { + self.onion_listener.accept() + } +} + +pub trait TorProvider : Send { type Error; fn update(&mut self) -> Result, Self::Error>; @@ -151,12 +166,14 @@ pub trait TorProvider { &mut self, service_id: &V3OnionServiceId, virt_port: u16, - circuit: Option, + circuit: Option, ) -> Result; fn listener( &mut self, private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result; + ) -> Result; + fn generate_token(&mut self) -> CircuitToken; + fn release_token(&mut self, token: CircuitToken); } From b2e2980fb6752dec9418f1882deb8da70a07b7c7 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 23 Jun 2023 22:17:00 +0000 Subject: [PATCH 021/184] tor-interface, gosling: TorProvider trait now uses a generic error type, updated gosling context to use a TorProvider rather than a specific implementation --- tor-interface/src/legacy_tor_client.rs | 67 +++++++++++++++++--------- tor-interface/src/mock_tor_client.rs | 34 +++++++------ tor-interface/src/tor_provider.rs | 22 +++++---- 3 files changed, 76 insertions(+), 47 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 2b6897657..ea774299c 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -1,6 +1,7 @@ // standard use std::boxed::Box; use std::collections::BTreeMap; +use std::convert::From; use std::default::Default; use std::io::ErrorKind; #[cfg(test)] @@ -24,6 +25,7 @@ use crate::legacy_tor_controller::*; use crate::legacy_tor_process::*; use crate::legacy_tor_version::*; use crate::tor_crypto::*; +use crate::tor_provider; use crate::tor_provider::*; #[derive(thiserror::Error, Debug)] @@ -86,17 +88,22 @@ pub enum Error { AddOnionFailed(#[source] crate::legacy_tor_controller::Error), } +impl From for crate::tor_provider::Error { + fn from(error: Error) -> Self { + crate::tor_provider::Error::Generic(error.to_string()) + } +} + // // CircuitToken Implementation // -pub struct LegacyCircuitToken { +struct LegacyCircuitToken { username: String, password: String, } impl LegacyCircuitToken { - #[allow(dead_code)] - pub fn new() -> LegacyCircuitToken { + fn new() -> LegacyCircuitToken { const CIRCUIT_TOKEN_USERNAME_LENGTH: usize = 32usize; const CIRCUIT_TOKEN_PASSWORD_LENGTH: usize = 32usize; let username = generate_password(CIRCUIT_TOKEN_USERNAME_LENGTH); @@ -106,6 +113,12 @@ impl LegacyCircuitToken { } } +impl Default for LegacyCircuitToken { + fn default() -> Self { + Self::new() + } +} + // // LegacyOnionListener // @@ -154,7 +167,7 @@ pub struct LegacyTorClient { onion_services: Vec<(V3OnionServiceId, Arc)>, // our list of circuit tokens for the tor daemon circuit_token_counter: usize, - circuit_tokens: BTreeMap + circuit_tokens: BTreeMap, } impl LegacyTorClient { @@ -219,9 +232,7 @@ impl LegacyTorClient { } impl TorProvider for LegacyTorClient { - type Error = Error; - - fn update(&mut self) -> Result, Error> { + fn update(&mut self) -> Result, tor_provider::Error> { let mut i = 0; while i < self.onion_services.len() { // remove onion services with no active listeners @@ -297,26 +308,32 @@ impl TorProvider for LegacyTorClient { Ok(events) } - fn bootstrap(&mut self) -> Result<(), Error> { - self.controller + fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { + Ok(self + .controller .setconf(&[("DisableNetwork", "0")]) - .map_err(Error::SetConfDisableNetwork0Failed) + .map_err(Error::SetConfDisableNetwork0Failed)?) } fn add_client_auth( &mut self, service_id: &V3OnionServiceId, client_auth: &X25519PrivateKey, - ) -> Result<(), Error> { - self.controller + ) -> Result<(), tor_provider::Error> { + Ok(self + .controller .onion_client_auth_add(service_id, client_auth, None, &Default::default()) - .map_err(Error::OnionClientAuthAddFailed) + .map_err(Error::OnionClientAuthAddFailed)?) } - fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> { - self.controller + fn remove_client_auth( + &mut self, + service_id: &V3OnionServiceId, + ) -> Result<(), tor_provider::Error> { + Ok(self + .controller .onion_client_auth_remove(service_id) - .map_err(Error::OnionClientAuthRemoveFailed) + .map_err(Error::OnionClientAuthRemoveFailed)?) } // connect to an onion service and returns OnionStream @@ -325,14 +342,14 @@ impl TorProvider for LegacyTorClient { service_id: &V3OnionServiceId, virt_port: u16, circuit: Option, - ) -> Result { + ) -> Result { if self.socks_listener.is_none() { let mut listeners = self .controller .getinfo_net_listeners_socks() .map_err(Error::GetInfoNetListenersSocksFailed)?; if listeners.is_empty() { - return Err(Error::NoSocksListenersFound()); + return Err(Error::NoSocksListenersFound())?; } self.socks_listener = Some(listeners.swap_remove(0)); } @@ -353,11 +370,12 @@ impl TorProvider for LegacyTorClient { socks_listener, target, &circuit.username, - &circuit.password) + &circuit.password, + ) } else { - return Err(Error::CircuitTokenInvalid()); + return Err(Error::CircuitTokenInvalid())?; } - }, + } } .map_err(Error::Socks5ConnectionFailed)?; @@ -377,7 +395,7 @@ impl TorProvider for LegacyTorClient { private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { // try to bind to a local address, let OS pick our port let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; @@ -421,13 +439,14 @@ impl TorProvider for LegacyTorClient { onion_addr, }); - Ok(OnionListener{onion_listener}) + Ok(OnionListener { onion_listener }) } fn generate_token(&mut self) -> CircuitToken { let new_token = self.circuit_token_counter; self.circuit_token_counter += 1; - self.circuit_tokens.insert(new_token, LegacyCircuitToken::new()); + self.circuit_tokens + .insert(new_token, LegacyCircuitToken::new()); new_token } diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index 16083bb3e..276c00eaf 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -8,6 +8,7 @@ use std::sync::{atomic, Arc, Mutex}; // internal crates use crate::tor_crypto::*; +use crate::tor_provider; use crate::tor_provider::*; #[derive(thiserror::Error, Debug)] @@ -37,6 +38,12 @@ pub enum Error { TcpListenerLocalAddrFailed(#[source] std::io::Error), } +impl From for crate::tor_provider::Error { + fn from(error: Error) -> Self { + crate::tor_provider::Error::Generic(error.to_string()) + } +} + pub struct MockOnionListener { listener: std::net::TcpListener, is_active: Arc, @@ -163,9 +170,7 @@ impl Default for MockTorClient { } impl TorProvider for MockTorClient { - type Error = Error; - - fn update(&mut self) -> Result, Self::Error> { + fn update(&mut self) -> Result, tor_provider::Error> { match MOCK_TOR_NETWORK.lock() { Ok(mut mock_tor_network) => { let mut i = 0; @@ -186,9 +191,9 @@ impl TorProvider for MockTorClient { Ok(std::mem::take(&mut self.events)) } - fn bootstrap(&mut self) -> Result<(), Self::Error> { + fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { if self.bootstrapped { - Err(Error::ClientAlreadyBootstrapped()) + Err(Error::ClientAlreadyBootstrapped())? } else { self.events.push(TorEvent::BootstrapStatus { progress: 0u32, @@ -215,7 +220,7 @@ impl TorProvider for MockTorClient { &mut self, service_id: &V3OnionServiceId, client_auth: &X25519PrivateKey, - ) -> Result<(), Self::Error> { + ) -> Result<(), tor_provider::Error> { let client_auth_public = X25519PublicKey::from_private_key(client_auth); if let Some(key) = self.client_auth_keys.get_mut(service_id) { *key = client_auth_public; @@ -226,7 +231,10 @@ impl TorProvider for MockTorClient { Ok(()) } - fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Self::Error> { + fn remove_client_auth( + &mut self, + service_id: &V3OnionServiceId, + ) -> Result<(), tor_provider::Error> { self.client_auth_keys.remove(service_id); Ok(()) } @@ -236,12 +244,12 @@ impl TorProvider for MockTorClient { service_id: &V3OnionServiceId, virt_port: u16, _circuit: Option, - ) -> Result { + ) -> Result { let client_auth = self.client_auth_keys.get(service_id); match MOCK_TOR_NETWORK.lock() { Ok(mut mock_tor_network) => { - mock_tor_network.connect_to_onion(service_id, virt_port, client_auth) + Ok(mock_tor_network.connect_to_onion(service_id, virt_port, client_auth)?) } Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"), } @@ -252,7 +260,7 @@ impl TorProvider for MockTorClient { private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { // convert inputs to relevant types let service_id = V3OnionServiceId::from_private_key(private_key); let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port)); @@ -294,16 +302,14 @@ impl TorProvider for MockTorClient { onion_addr, }); - Ok(OnionListener{onion_listener}) + Ok(OnionListener { onion_listener }) } fn generate_token(&mut self) -> CircuitToken { 0usize } - fn release_token(&mut self, _token: CircuitToken) { - - } + fn release_token(&mut self, _token: CircuitToken) {} } impl Drop for MockTorClient { diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index f994e3812..b9c582d01 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -132,7 +132,7 @@ impl OnionStream { } } -pub trait OnionListenerImpl : Send { +pub trait OnionListenerImpl: Send { fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error>; fn accept(&self) -> Result, std::io::Error>; } @@ -151,29 +151,33 @@ impl OnionListener { } } -pub trait TorProvider : Send { - type Error; +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("{0}")] + Generic(String), +} - fn update(&mut self) -> Result, Self::Error>; - fn bootstrap(&mut self) -> Result<(), Self::Error>; +pub trait TorProvider: Send { + fn update(&mut self) -> Result, Error>; + fn bootstrap(&mut self) -> Result<(), Error>; fn add_client_auth( &mut self, service_id: &V3OnionServiceId, client_auth: &X25519PrivateKey, - ) -> Result<(), Self::Error>; - fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Self::Error>; + ) -> Result<(), Error>; + fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error>; fn connect( &mut self, service_id: &V3OnionServiceId, virt_port: u16, circuit: Option, - ) -> Result; + ) -> Result; fn listener( &mut self, private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result; + ) -> Result; fn generate_token(&mut self) -> CircuitToken; fn release_token(&mut self, token: CircuitToken); } From 3503304ddbef395c61c25f92c525028c19edb562 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 23 Jun 2023 22:43:51 +0000 Subject: [PATCH 022/184] tor-interface, gosling: updated tests to take a Box --- tor-interface/src/legacy_tor_client.rs | 203 +------------------------ tor-interface/src/mock_tor_client.rs | 196 +----------------------- tor-interface/src/tor_provider.rs | 194 +++++++++++++++++++++++ 3 files changed, 203 insertions(+), 390 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index ea774299c..ac16642e7 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -4,8 +4,6 @@ use std::collections::BTreeMap; use std::convert::From; use std::default::Default; use std::io::ErrorKind; -#[cfg(test)] -use std::io::{Read, Write}; use std::net::{SocketAddr, TcpListener}; use std::ops::Drop; use std::option::Option; @@ -457,209 +455,20 @@ impl TorProvider for LegacyTorClient { #[test] #[serial] -fn test_tor_manager() -> anyhow::Result<()> { +fn test_legacy_bootstrap() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); - data_path.push("test_tor_manager"); - - let mut tor = LegacyTorClient::new(&tor_path, &data_path)?; - println!("version : {}", tor.version().to_string()); - tor.bootstrap()?; - - let mut received_log = false; - let mut bootstrap_complete = false; - while !bootstrap_complete { - for event in tor.update()?.iter() { - match event { - TorEvent::BootstrapStatus { - progress, - tag, - summary, - } => println!( - "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", - progress, tag, summary - ), - TorEvent::BootstrapComplete => { - println!("Bootstrap Complete!"); - bootstrap_complete = true; - } - TorEvent::LogReceived { line } => { - received_log = true; - println!("--- {}", line); - } - _ => {} - } - } - } - assert!( - received_log, - "should have received a log line from tor daemon" - ); + data_path.push("test_legacy_bootstrap"); - Ok(()) + tor_provider::bootstrap_test(Box::new(LegacyTorClient::new(&tor_path, &data_path)?)) } #[test] #[serial] -fn test_onion_service() -> anyhow::Result<()> { +fn test_legacy_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); - data_path.push("test_onion_service"); - - let mut tor = LegacyTorClient::new(&tor_path, &data_path)?; - - // for 30secs for bootstrap - tor.bootstrap()?; - - let mut bootstrap_complete = false; - while !bootstrap_complete { - for event in tor.update()?.iter() { - match event { - TorEvent::BootstrapStatus { - progress, - tag, - summary, - } => println!( - "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", - progress, tag, summary - ), - TorEvent::BootstrapComplete => { - println!("Bootstrap Complete!"); - bootstrap_complete = true; - } - TorEvent::LogReceived { line } => { - println!("--- {}", line); - } - _ => {} - } - } - } - - // vanilla V3 onion service - { - // create an onion service for this test - let private_key = Ed25519PrivateKey::generate(); - - println!("Starting and listening to onion service"); - const VIRT_PORT: u16 = 42069u16; - let listener = tor.listener(&private_key, VIRT_PORT, None)?; - - let mut onion_published = false; - while !onion_published { - for event in tor.update()?.iter() { - match event { - TorEvent::LogReceived { line } => { - println!("--- {}", line); - } - TorEvent::OnionServicePublished { service_id } => { - let expected_service_id = V3OnionServiceId::from_private_key(&private_key); - if expected_service_id == *service_id { - println!("Onion Service {} published", service_id.to_string()); - onion_published = true; - } - } - _ => {} - } - } - } - - const MESSAGE: &str = "Hello World!"; - - { - let service_id = V3OnionServiceId::from_private_key(&private_key); - - println!("Connecting to onion service"); - let mut client = tor.connect(&service_id, VIRT_PORT, None)?; - println!("Client writing message: '{}'", MESSAGE); - client.write_all(MESSAGE.as_bytes())?; - client.flush()?; - println!("End of client scope"); - } - - if let Some(mut server) = listener.accept()? { - println!("Server reading message"); - let mut buffer = Vec::new(); - server.read_to_end(&mut buffer)?; - let msg = String::from_utf8(buffer)?; - - assert!(MESSAGE == msg); - println!("Message received: '{}'", msg); - } else { - panic!("no listener"); - } - } - - // authenticated onion service - { - // create an onion service for this test - let private_key = Ed25519PrivateKey::generate(); - - let private_auth_key = X25519PrivateKey::generate(); - let public_auth_key = X25519PublicKey::from_private_key(&private_auth_key); - - println!("Starting and listening to authenticated onion service"); - const VIRT_PORT: u16 = 42069u16; - let listener = tor.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]))?; - - let mut onion_published = false; - while !onion_published { - for event in tor.update()?.iter() { - match event { - TorEvent::LogReceived { line } => { - println!("--- {}", line); - } - TorEvent::OnionServicePublished { service_id } => { - let expected_service_id = V3OnionServiceId::from_private_key(&private_key); - if expected_service_id == *service_id { - println!( - "Authenticated Onion Service {} published", - service_id.to_string() - ); - onion_published = true; - } - } - _ => {} - } - } - } + data_path.push("test_legacy_onion_service"); - const MESSAGE: &str = "Hello World!"; - - { - let service_id = V3OnionServiceId::from_private_key(&private_key); - - println!("Connecting to onion service (should fail)"); - assert!( - tor.connect(&service_id, VIRT_PORT, None).is_err(), - "should not able to connect to an authenticated onion service without auth key" - ); - - println!("Add auth key for onion service"); - tor.add_client_auth(&service_id, &private_auth_key)?; - - println!("Connecting to onion service with authentication"); - let mut client = tor.connect(&service_id, VIRT_PORT, None)?; - - println!("Client writing message: '{}'", MESSAGE); - client.write_all(MESSAGE.as_bytes())?; - client.flush()?; - println!("End of client scope"); - - println!("Remove auth key for onion service"); - tor.remove_client_auth(&service_id)?; - } - - if let Some(mut server) = listener.accept()? { - println!("Server reading message"); - let mut buffer = Vec::new(); - server.read_to_end(&mut buffer)?; - let msg = String::from_utf8(buffer)?; - - assert!(MESSAGE == msg); - println!("Message received: '{}'", msg); - } else { - panic!("no listener"); - } - } - Ok(()) + tor_provider::onion_service_test(Box::new(LegacyTorClient::new(&tor_path, &data_path)?)) } diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index 276c00eaf..93d38f1ed 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -1,8 +1,6 @@ // standard use std::collections::BTreeMap; use std::io::ErrorKind; -#[cfg(test)] -use std::io::{Read, Write}; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::sync::{atomic, Arc, Mutex}; @@ -328,199 +326,11 @@ impl Drop for MockTorClient { } #[test] -fn test_mock_client() -> anyhow::Result<()> { - let mut tor = MockTorClient::new(); - tor.bootstrap()?; - - let mut received_log = false; - let mut bootstrap_complete = false; - while !bootstrap_complete { - for event in tor.update()?.iter() { - match event { - TorEvent::BootstrapStatus { - progress, - tag, - summary, - } => println!( - "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", - progress, tag, summary - ), - TorEvent::BootstrapComplete => { - println!("Bootstrap Complete!"); - bootstrap_complete = true; - } - TorEvent::LogReceived { line } => { - received_log = true; - println!("--- {}", line); - } - _ => {} - } - } - } - assert!( - received_log, - "should have received a log line from tor daemon" - ); - - Ok(()) +fn test_mock_bootstrap() -> anyhow::Result<()> { + tor_provider::bootstrap_test(Box::new(MockTorClient::new())) } #[test] fn test_mock_onion_service() -> anyhow::Result<()> { - let mut tor = MockTorClient::new(); - - // for 30secs for bootstrap - tor.bootstrap()?; - - let mut bootstrap_complete = false; - while !bootstrap_complete { - for event in tor.update()?.iter() { - match event { - TorEvent::BootstrapStatus { - progress, - tag, - summary, - } => println!( - "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", - progress, tag, summary - ), - TorEvent::BootstrapComplete => { - println!("Bootstrap Complete!"); - bootstrap_complete = true; - } - TorEvent::LogReceived { line } => { - println!("--- {}", line); - } - _ => {} - } - } - } - - // vanilla V3 onion service - { - // create an onion service for this test - let private_key = Ed25519PrivateKey::generate(); - - println!("Starting and listening to onion service"); - const VIRT_PORT: u16 = 42069u16; - let listener = tor.listener(&private_key, VIRT_PORT, None)?; - - let mut onion_published = false; - while !onion_published { - for event in tor.update()?.iter() { - match event { - TorEvent::LogReceived { line } => { - println!("--- {}", line); - } - TorEvent::OnionServicePublished { service_id } => { - let expected_service_id = V3OnionServiceId::from_private_key(&private_key); - if expected_service_id == *service_id { - println!("Onion Service {} published", service_id.to_string()); - onion_published = true; - } - } - _ => {} - } - } - } - - const MESSAGE: &str = "Hello World!"; - - { - let service_id = V3OnionServiceId::from_private_key(&private_key); - - println!("Connecting to onion service"); - let mut client = tor.connect(&service_id, VIRT_PORT, None)?; - println!("Client writing message: '{}'", MESSAGE); - client.write_all(MESSAGE.as_bytes())?; - client.flush()?; - println!("End of client scope"); - } - - if let Some(mut server) = listener.accept()? { - println!("Server reading message"); - let mut buffer = Vec::new(); - server.read_to_end(&mut buffer)?; - let msg = String::from_utf8(buffer)?; - - assert!(MESSAGE == msg); - println!("Message received: '{}'", msg); - } else { - panic!("no listener"); - } - } - - // authenticated onion service - { - // create an onion service for this test - let private_key = Ed25519PrivateKey::generate(); - - let private_auth_key = X25519PrivateKey::generate(); - let public_auth_key = X25519PublicKey::from_private_key(&private_auth_key); - - println!("Starting and listening to authenticated onion service"); - const VIRT_PORT: u16 = 42069u16; - let listener = tor.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]))?; - - let mut onion_published = false; - while !onion_published { - for event in tor.update()?.iter() { - match event { - TorEvent::LogReceived { line } => { - println!("--- {}", line); - } - TorEvent::OnionServicePublished { service_id } => { - let expected_service_id = V3OnionServiceId::from_private_key(&private_key); - if expected_service_id == *service_id { - println!( - "Authenticated Onion Service {} published", - service_id.to_string() - ); - onion_published = true; - } - } - _ => {} - } - } - } - - const MESSAGE: &str = "Hello World!"; - - { - let service_id = V3OnionServiceId::from_private_key(&private_key); - - println!("Connecting to onion service (should fail)"); - assert!( - tor.connect(&service_id, VIRT_PORT, None).is_err(), - "should not able to connect to an authenticated onion service without auth key" - ); - - println!("Add auth key for onion service"); - tor.add_client_auth(&service_id, &private_auth_key)?; - - println!("Connecting to onion service with authentication"); - let mut client = tor.connect(&service_id, VIRT_PORT, None)?; - - println!("Client writing message: '{}'", MESSAGE); - client.write_all(MESSAGE.as_bytes())?; - client.flush()?; - println!("End of client scope"); - - println!("Remove auth key for onion service"); - tor.remove_client_auth(&service_id)?; - } - - if let Some(mut server) = listener.accept()? { - println!("Server reading message"); - let mut buffer = Vec::new(); - server.read_to_end(&mut buffer)?; - let msg = String::from_utf8(buffer)?; - - assert!(MESSAGE == msg); - println!("Message received: '{}'", msg); - } else { - panic!("no listener"); - } - } - Ok(()) + tor_provider::onion_service_test(Box::new(MockTorClient::new())) } diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index b9c582d01..8d1283227 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -181,3 +181,197 @@ pub trait TorProvider: Send { fn generate_token(&mut self) -> CircuitToken; fn release_token(&mut self, token: CircuitToken); } + +#[cfg(test)] +pub(crate) fn bootstrap_test(mut tor: Box) -> anyhow::Result<()> { + tor.bootstrap()?; + + let mut received_log = false; + let mut bootstrap_complete = false; + while !bootstrap_complete { + for event in tor.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Bootstrap Complete!"); + bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + received_log = true; + println!("--- {}", line); + } + _ => {} + } + } + } + assert!( + received_log, + "should have received a log line from tor provider" + ); + + Ok(()) +} + +#[cfg(test)] +pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Result<()> { + tor.bootstrap()?; + + let mut bootstrap_complete = false; + while !bootstrap_complete { + for event in tor.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Bootstrap Complete!"); + bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + _ => {} + } + } + } + + // vanilla V3 onion service + { + // create an onion service for this test + let private_key = Ed25519PrivateKey::generate(); + + println!("Starting and listening to onion service"); + const VIRT_PORT: u16 = 42069u16; + let listener = tor.listener(&private_key, VIRT_PORT, None)?; + + let mut onion_published = false; + while !onion_published { + for event in tor.update()?.iter() { + match event { + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + TorEvent::OnionServicePublished { service_id } => { + let expected_service_id = V3OnionServiceId::from_private_key(&private_key); + if expected_service_id == *service_id { + println!("Onion Service {} published", service_id.to_string()); + onion_published = true; + } + } + _ => {} + } + } + } + + const MESSAGE: &str = "Hello World!"; + + { + let service_id = V3OnionServiceId::from_private_key(&private_key); + + println!("Connecting to onion service"); + let mut client = tor.connect(&service_id, VIRT_PORT, None)?; + println!("Client writing message: '{}'", MESSAGE); + client.write_all(MESSAGE.as_bytes())?; + client.flush()?; + println!("End of client scope"); + } + + if let Some(mut server) = listener.accept()? { + println!("Server reading message"); + let mut buffer = Vec::new(); + server.read_to_end(&mut buffer)?; + let msg = String::from_utf8(buffer)?; + + assert!(MESSAGE == msg); + println!("Message received: '{}'", msg); + } else { + panic!("no listener"); + } + } + + // authenticated onion service + { + // create an onion service for this test + let private_key = Ed25519PrivateKey::generate(); + + let private_auth_key = X25519PrivateKey::generate(); + let public_auth_key = X25519PublicKey::from_private_key(&private_auth_key); + + println!("Starting and listening to authenticated onion service"); + const VIRT_PORT: u16 = 42069u16; + let listener = tor.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]))?; + + let mut onion_published = false; + while !onion_published { + for event in tor.update()?.iter() { + match event { + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + TorEvent::OnionServicePublished { service_id } => { + let expected_service_id = V3OnionServiceId::from_private_key(&private_key); + if expected_service_id == *service_id { + println!( + "Authenticated Onion Service {} published", + service_id.to_string() + ); + onion_published = true; + } + } + _ => {} + } + } + } + + const MESSAGE: &str = "Hello World!"; + + { + let service_id = V3OnionServiceId::from_private_key(&private_key); + + println!("Connecting to onion service (should fail)"); + assert!( + tor.connect(&service_id, VIRT_PORT, None).is_err(), + "should not able to connect to an authenticated onion service without auth key" + ); + + println!("Add auth key for onion service"); + tor.add_client_auth(&service_id, &private_auth_key)?; + + println!("Connecting to onion service with authentication"); + let mut client = tor.connect(&service_id, VIRT_PORT, None)?; + + println!("Client writing message: '{}'", MESSAGE); + client.write_all(MESSAGE.as_bytes())?; + client.flush()?; + println!("End of client scope"); + + println!("Remove auth key for onion service"); + tor.remove_client_auth(&service_id)?; + } + + if let Some(mut server) = listener.accept()? { + println!("Server reading message"); + let mut buffer = Vec::new(); + server.read_to_end(&mut buffer)?; + let msg = String::from_utf8(buffer)?; + + assert!(MESSAGE == msg); + println!("Message received: '{}'", msg); + } else { + panic!("no listener"); + } + } + Ok(()) +} From 24edb04176e957894d76cb705433775a277f4f3f Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 24 Jun 2023 19:09:06 +0000 Subject: [PATCH 023/184] build: added 'offline-test' feature to gosling for automation --- tor-interface/Cargo.toml | 5 ++++- tor-interface/src/legacy_tor_client.rs | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 6f7453e91..19d51511a 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -20,4 +20,7 @@ thiserror = "1.0" [dev-dependencies] anyhow = "1.0" serial_test = "0.9.0" -which = "4.3.0" \ No newline at end of file +which = "4.3.0" + +[features] +offline-test = [] diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index ac16642e7..82f943234 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -455,6 +455,7 @@ impl TorProvider for LegacyTorClient { #[test] #[serial] +#[cfg(not(feature="offline-test"))] fn test_legacy_bootstrap() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); @@ -465,6 +466,7 @@ fn test_legacy_bootstrap() -> anyhow::Result<()> { #[test] #[serial] +#[cfg(not(feature="offline-test"))] fn test_legacy_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); From 89ec9984ade16036522edc2f2880c485eff95484 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Thu, 29 Jun 2023 01:56:43 +0000 Subject: [PATCH 024/184] build: added separate coverage-offline make target --- tor-interface/src/legacy_tor_controller.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index e4e9c4c08..594c4ba78 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -672,6 +672,7 @@ impl LegacyTorController { #[test] #[serial] +#[cfg(not(feature="offline-test"))] fn test_tor_controller() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); From 69c6a3c934150a4396c81a5c173679053d9f5340 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 1 Jul 2023 05:11:29 +0000 Subject: [PATCH 025/184] build: updated system to support offline test targets --- tor-interface/CMakeLists.txt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 7b7e8f28f..941c7658f 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -1,7 +1,9 @@ -add_test(NAME tor_interface_test - COMMAND cargo test ${CARGO_FLAGS} +add_custom_target(tor_interface_cargo_test + COMMAND RUSTFLAGS=${RUSTFLAGS} CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) -set_tests_properties(tor_interface_test - PROPERTIES ENVIRONMENT "RUSTFLAGS=${RUSTFLAGS};CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR};RUST_BACKTRACE=full" -) + +add_custom_target(tor_interface_cargo_test_offline + COMMAND RUSTFLAGS=${RUSTFLAGS} CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} --features offline-test + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) \ No newline at end of file From 5ea6391e618b2bc7b8536baa2d9ff383189a5aef Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 1 Jul 2023 05:25:24 +0000 Subject: [PATCH 026/184] format: fixed format errors --- tor-interface/src/legacy_tor_client.rs | 4 ++-- tor-interface/src/legacy_tor_controller.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 82f943234..eb429ad53 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -455,7 +455,7 @@ impl TorProvider for LegacyTorClient { #[test] #[serial] -#[cfg(not(feature="offline-test"))] +#[cfg(not(feature = "offline-test"))] fn test_legacy_bootstrap() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); @@ -466,7 +466,7 @@ fn test_legacy_bootstrap() -> anyhow::Result<()> { #[test] #[serial] -#[cfg(not(feature="offline-test"))] +#[cfg(not(feature = "offline-test"))] fn test_legacy_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index 594c4ba78..899f0584a 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -672,7 +672,7 @@ impl LegacyTorController { #[test] #[serial] -#[cfg(not(feature="offline-test"))] +#[cfg(not(feature = "offline-test"))] fn test_tor_controller() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); From 80fedb8e8a9e84cd9e19c84d8638d7a118b35a0b Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 16 Jul 2023 02:55:03 +0000 Subject: [PATCH 027/184] build: fix cmake target dependencies so that Rust source changes trigger rebuilds --- tor-interface/CMakeLists.txt | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 941c7658f..3681e7924 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -1,9 +1,30 @@ +set(tor_interface_sources + src/legacy_tor_client.rs + src/legacy_tor_control_stream.rs + src/legacy_tor_controller.rs + src/legacy_tor_process.rs + src/legacy_tor_version.rs + src/lib.rs + src/mock_tor_client.rs + src/tor_crypto.rs + src/tor_provider.rs) + +# +# build target +# +add_custom_target(tor_interface_target + DEPENDS ${tor_interface_sources} + COMMAND CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} cargo build ${CARGO_FLAGS} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + +# cargo test add_custom_target(tor_interface_cargo_test - COMMAND RUSTFLAGS=${RUSTFLAGS} CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} + COMMAND RUSTFLAGS=${RUSTFLAGS} CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) +# cargo test (offline) add_custom_target(tor_interface_cargo_test_offline - COMMAND RUSTFLAGS=${RUSTFLAGS} CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} --features offline-test + COMMAND RUSTFLAGS=${RUSTFLAGS} CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} --features offline-test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) \ No newline at end of file From fd87e23282f2561706cfb851cc3aafa86834b01d Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 24 Jul 2023 04:40:56 +0000 Subject: [PATCH 028/184] gosling: moved TorProvider integration tests to gosling/tests/tor_provier.rs --- tor-interface/src/legacy_tor_client.rs | 22 --- tor-interface/src/mock_tor_client.rs | 10 -- tor-interface/src/tor_provider.rs | 194 -------------------- tor-interface/tests/tor_provider.rs | 236 +++++++++++++++++++++++++ 4 files changed, 236 insertions(+), 226 deletions(-) create mode 100644 tor-interface/tests/tor_provider.rs diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index eb429ad53..219becb37 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -13,8 +13,6 @@ use std::sync::{atomic, Arc}; use std::time::Duration; // extern crates -#[cfg(test)] -use serial_test::serial; use socks::Socks5Stream; // internal crates @@ -453,24 +451,4 @@ impl TorProvider for LegacyTorClient { } } -#[test] -#[serial] -#[cfg(not(feature = "offline-test"))] -fn test_legacy_bootstrap() -> anyhow::Result<()> { - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - let mut data_path = std::env::temp_dir(); - data_path.push("test_legacy_bootstrap"); - tor_provider::bootstrap_test(Box::new(LegacyTorClient::new(&tor_path, &data_path)?)) -} - -#[test] -#[serial] -#[cfg(not(feature = "offline-test"))] -fn test_legacy_onion_service() -> anyhow::Result<()> { - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - let mut data_path = std::env::temp_dir(); - data_path.push("test_legacy_onion_service"); - - tor_provider::onion_service_test(Box::new(LegacyTorClient::new(&tor_path, &data_path)?)) -} diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index 93d38f1ed..88e9e9e32 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -324,13 +324,3 @@ impl Drop for MockTorClient { } } } - -#[test] -fn test_mock_bootstrap() -> anyhow::Result<()> { - tor_provider::bootstrap_test(Box::new(MockTorClient::new())) -} - -#[test] -fn test_mock_onion_service() -> anyhow::Result<()> { - tor_provider::onion_service_test(Box::new(MockTorClient::new())) -} diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 8d1283227..b9c582d01 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -181,197 +181,3 @@ pub trait TorProvider: Send { fn generate_token(&mut self) -> CircuitToken; fn release_token(&mut self, token: CircuitToken); } - -#[cfg(test)] -pub(crate) fn bootstrap_test(mut tor: Box) -> anyhow::Result<()> { - tor.bootstrap()?; - - let mut received_log = false; - let mut bootstrap_complete = false; - while !bootstrap_complete { - for event in tor.update()?.iter() { - match event { - TorEvent::BootstrapStatus { - progress, - tag, - summary, - } => println!( - "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", - progress, tag, summary - ), - TorEvent::BootstrapComplete => { - println!("Bootstrap Complete!"); - bootstrap_complete = true; - } - TorEvent::LogReceived { line } => { - received_log = true; - println!("--- {}", line); - } - _ => {} - } - } - } - assert!( - received_log, - "should have received a log line from tor provider" - ); - - Ok(()) -} - -#[cfg(test)] -pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Result<()> { - tor.bootstrap()?; - - let mut bootstrap_complete = false; - while !bootstrap_complete { - for event in tor.update()?.iter() { - match event { - TorEvent::BootstrapStatus { - progress, - tag, - summary, - } => println!( - "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", - progress, tag, summary - ), - TorEvent::BootstrapComplete => { - println!("Bootstrap Complete!"); - bootstrap_complete = true; - } - TorEvent::LogReceived { line } => { - println!("--- {}", line); - } - _ => {} - } - } - } - - // vanilla V3 onion service - { - // create an onion service for this test - let private_key = Ed25519PrivateKey::generate(); - - println!("Starting and listening to onion service"); - const VIRT_PORT: u16 = 42069u16; - let listener = tor.listener(&private_key, VIRT_PORT, None)?; - - let mut onion_published = false; - while !onion_published { - for event in tor.update()?.iter() { - match event { - TorEvent::LogReceived { line } => { - println!("--- {}", line); - } - TorEvent::OnionServicePublished { service_id } => { - let expected_service_id = V3OnionServiceId::from_private_key(&private_key); - if expected_service_id == *service_id { - println!("Onion Service {} published", service_id.to_string()); - onion_published = true; - } - } - _ => {} - } - } - } - - const MESSAGE: &str = "Hello World!"; - - { - let service_id = V3OnionServiceId::from_private_key(&private_key); - - println!("Connecting to onion service"); - let mut client = tor.connect(&service_id, VIRT_PORT, None)?; - println!("Client writing message: '{}'", MESSAGE); - client.write_all(MESSAGE.as_bytes())?; - client.flush()?; - println!("End of client scope"); - } - - if let Some(mut server) = listener.accept()? { - println!("Server reading message"); - let mut buffer = Vec::new(); - server.read_to_end(&mut buffer)?; - let msg = String::from_utf8(buffer)?; - - assert!(MESSAGE == msg); - println!("Message received: '{}'", msg); - } else { - panic!("no listener"); - } - } - - // authenticated onion service - { - // create an onion service for this test - let private_key = Ed25519PrivateKey::generate(); - - let private_auth_key = X25519PrivateKey::generate(); - let public_auth_key = X25519PublicKey::from_private_key(&private_auth_key); - - println!("Starting and listening to authenticated onion service"); - const VIRT_PORT: u16 = 42069u16; - let listener = tor.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]))?; - - let mut onion_published = false; - while !onion_published { - for event in tor.update()?.iter() { - match event { - TorEvent::LogReceived { line } => { - println!("--- {}", line); - } - TorEvent::OnionServicePublished { service_id } => { - let expected_service_id = V3OnionServiceId::from_private_key(&private_key); - if expected_service_id == *service_id { - println!( - "Authenticated Onion Service {} published", - service_id.to_string() - ); - onion_published = true; - } - } - _ => {} - } - } - } - - const MESSAGE: &str = "Hello World!"; - - { - let service_id = V3OnionServiceId::from_private_key(&private_key); - - println!("Connecting to onion service (should fail)"); - assert!( - tor.connect(&service_id, VIRT_PORT, None).is_err(), - "should not able to connect to an authenticated onion service without auth key" - ); - - println!("Add auth key for onion service"); - tor.add_client_auth(&service_id, &private_auth_key)?; - - println!("Connecting to onion service with authentication"); - let mut client = tor.connect(&service_id, VIRT_PORT, None)?; - - println!("Client writing message: '{}'", MESSAGE); - client.write_all(MESSAGE.as_bytes())?; - client.flush()?; - println!("End of client scope"); - - println!("Remove auth key for onion service"); - tor.remove_client_auth(&service_id)?; - } - - if let Some(mut server) = listener.accept()? { - println!("Server reading message"); - let mut buffer = Vec::new(); - server.read_to_end(&mut buffer)?; - let msg = String::from_utf8(buffer)?; - - assert!(MESSAGE == msg); - println!("Message received: '{}'", msg); - } else { - panic!("no listener"); - } - } - Ok(()) -} diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs new file mode 100644 index 000000000..6f39a4427 --- /dev/null +++ b/tor-interface/tests/tor_provider.rs @@ -0,0 +1,236 @@ +// stanndard +use std::io::{Read, Write}; + +// extern crates +use serial_test::serial; + +// internal crates +use tor_interface::tor_crypto::*; +use tor_interface::tor_provider::*; +use tor_interface::mock_tor_client::*; +use tor_interface::legacy_tor_client::*; + +pub(crate) fn bootstrap_test(mut tor: Box) -> anyhow::Result<()> { + tor.bootstrap()?; + + let mut received_log = false; + let mut bootstrap_complete = false; + while !bootstrap_complete { + for event in tor.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Bootstrap Complete!"); + bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + received_log = true; + println!("--- {}", line); + } + _ => {} + } + } + } + assert!( + received_log, + "should have received a log line from tor provider" + ); + + Ok(()) +} + +pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Result<()> { + tor.bootstrap()?; + + let mut bootstrap_complete = false; + while !bootstrap_complete { + for event in tor.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Bootstrap Complete!"); + bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + _ => {} + } + } + } + + // vanilla V3 onion service + { + // create an onion service for this test + let private_key = Ed25519PrivateKey::generate(); + + println!("Starting and listening to onion service"); + const VIRT_PORT: u16 = 42069u16; + let listener = tor.listener(&private_key, VIRT_PORT, None)?; + + let mut onion_published = false; + while !onion_published { + for event in tor.update()?.iter() { + match event { + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + TorEvent::OnionServicePublished { service_id } => { + let expected_service_id = V3OnionServiceId::from_private_key(&private_key); + if expected_service_id == *service_id { + println!("Onion Service {} published", service_id.to_string()); + onion_published = true; + } + } + _ => {} + } + } + } + + const MESSAGE: &str = "Hello World!"; + + { + let service_id = V3OnionServiceId::from_private_key(&private_key); + + println!("Connecting to onion service"); + let mut client = tor.connect(&service_id, VIRT_PORT, None)?; + println!("Client writing message: '{}'", MESSAGE); + client.write_all(MESSAGE.as_bytes())?; + client.flush()?; + println!("End of client scope"); + } + + if let Some(mut server) = listener.accept()? { + println!("Server reading message"); + let mut buffer = Vec::new(); + server.read_to_end(&mut buffer)?; + let msg = String::from_utf8(buffer)?; + + assert!(MESSAGE == msg); + println!("Message received: '{}'", msg); + } else { + panic!("no listener"); + } + } + + // authenticated onion service + { + // create an onion service for this test + let private_key = Ed25519PrivateKey::generate(); + + let private_auth_key = X25519PrivateKey::generate(); + let public_auth_key = X25519PublicKey::from_private_key(&private_auth_key); + + println!("Starting and listening to authenticated onion service"); + const VIRT_PORT: u16 = 42069u16; + let listener = tor.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]))?; + + let mut onion_published = false; + while !onion_published { + for event in tor.update()?.iter() { + match event { + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + TorEvent::OnionServicePublished { service_id } => { + let expected_service_id = V3OnionServiceId::from_private_key(&private_key); + if expected_service_id == *service_id { + println!( + "Authenticated Onion Service {} published", + service_id.to_string() + ); + onion_published = true; + } + } + _ => {} + } + } + } + + const MESSAGE: &str = "Hello World!"; + + { + let service_id = V3OnionServiceId::from_private_key(&private_key); + + println!("Connecting to onion service (should fail)"); + assert!( + tor.connect(&service_id, VIRT_PORT, None).is_err(), + "should not able to connect to an authenticated onion service without auth key" + ); + + println!("Add auth key for onion service"); + tor.add_client_auth(&service_id, &private_auth_key)?; + + println!("Connecting to onion service with authentication"); + let mut client = tor.connect(&service_id, VIRT_PORT, None)?; + + println!("Client writing message: '{}'", MESSAGE); + client.write_all(MESSAGE.as_bytes())?; + client.flush()?; + println!("End of client scope"); + + println!("Remove auth key for onion service"); + tor.remove_client_auth(&service_id)?; + } + + if let Some(mut server) = listener.accept()? { + println!("Server reading message"); + let mut buffer = Vec::new(); + server.read_to_end(&mut buffer)?; + let msg = String::from_utf8(buffer)?; + + assert!(MESSAGE == msg); + println!("Message received: '{}'", msg); + } else { + panic!("no listener"); + } + } + Ok(()) +} + + +#[test] +fn test_mock_bootstrap() -> anyhow::Result<()> { + bootstrap_test(Box::new(MockTorClient::new())) +} + +#[test] +fn test_mock_onion_service() -> anyhow::Result<()> { + onion_service_test(Box::new(MockTorClient::new())) +} + +#[test] +#[serial] +#[cfg(not(feature = "offline-test"))] +fn test_legacy_bootstrap() -> anyhow::Result<()> { + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push("test_legacy_bootstrap"); + + bootstrap_test(Box::new(LegacyTorClient::new(&tor_path, &data_path)?)) +} + +#[test] +#[serial] +#[cfg(not(feature = "offline-test"))] +fn test_legacy_onion_service() -> anyhow::Result<()> { + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push("test_legacy_onion_service"); + + onion_service_test(Box::new(LegacyTorClient::new(&tor_path, &data_path)?)) +} \ No newline at end of file From 36e3afe96ea0bead3c50c69b2edc80df48488131 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 24 Jul 2023 05:12:58 +0000 Subject: [PATCH 029/184] tor-interface: re-ran make format --- tor-interface/src/legacy_tor_client.rs | 2 -- tor-interface/tests/tor_provider.rs | 7 +++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 219becb37..2e313ee45 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -450,5 +450,3 @@ impl TorProvider for LegacyTorClient { self.circuit_tokens.remove(&circuit_token); } } - - diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 6f39a4427..f77ee2a9e 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -5,10 +5,10 @@ use std::io::{Read, Write}; use serial_test::serial; // internal crates +use tor_interface::legacy_tor_client::*; +use tor_interface::mock_tor_client::*; use tor_interface::tor_crypto::*; use tor_interface::tor_provider::*; -use tor_interface::mock_tor_client::*; -use tor_interface::legacy_tor_client::*; pub(crate) fn bootstrap_test(mut tor: Box) -> anyhow::Result<()> { tor.bootstrap()?; @@ -202,7 +202,6 @@ pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Resul Ok(()) } - #[test] fn test_mock_bootstrap() -> anyhow::Result<()> { bootstrap_test(Box::new(MockTorClient::new())) @@ -233,4 +232,4 @@ fn test_legacy_onion_service() -> anyhow::Result<()> { data_path.push("test_legacy_onion_service"); onion_service_test(Box::new(LegacyTorClient::new(&tor_path, &data_path)?)) -} \ No newline at end of file +} From 652bb1291250dfde0caf76adf459fd002d3fcbfe Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Tue, 25 Jul 2023 01:08:46 +0000 Subject: [PATCH 030/184] tor-interface: test refactor - migrated integration tests to tor-interface/tests - moved various free functions to be private functions of their relevant types - added Debug implementations for various tor_crypto types --- tor-interface/src/legacy_tor_process.rs | 94 +++++++++- tor-interface/src/tor_crypto.rs | 239 ++++-------------------- tor-interface/tests/tor_crypto.rs | 125 +++++++++++++ 3 files changed, 252 insertions(+), 206 deletions(-) create mode 100644 tor-interface/tests/tor_crypto.rs diff --git a/tor-interface/src/legacy_tor_process.rs b/tor-interface/src/legacy_tor_process.rs index 90c9e2392..70febfe80 100644 --- a/tor-interface/src/legacy_tor_process.rs +++ b/tor-interface/src/legacy_tor_process.rs @@ -13,8 +13,15 @@ use std::string::ToString; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; +// extern crates +use crypto::digest::Digest; +use crypto::sha1::Sha1; +use data_encoding::HEXUPPER; +use rand::rngs::OsRng; +use rand::RngCore; + // internal crates -use crate::tor_crypto::*; +use crate::tor_crypto::generate_password; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -104,6 +111,60 @@ pub(crate) struct LegacyTorProcess { } impl LegacyTorProcess { + const S2K_RFC2440_SPECIFIER_LEN: usize = 9; + + fn hash_tor_password_with_salt( + salt: &[u8; Self::S2K_RFC2440_SPECIFIER_LEN], + password: &str, + ) -> String { + assert_eq!(salt[Self::S2K_RFC2440_SPECIFIER_LEN - 1], 0x60); + + // tor-specific rfc 2440 constants + const EXPBIAS: u8 = 6u8; + const C: u8 = 0x60; // salt[S2K_RFC2440_SPECIFIER_LEN - 1] + const COUNT: usize = (16usize + ((C & 15u8) as usize)) << ((C >> 4) + EXPBIAS); + + // squash together our hash input + let mut input: Vec = Default::default(); + // append salt (sans the 'C' constant') + input.extend_from_slice(&salt[0..Self::S2K_RFC2440_SPECIFIER_LEN - 1]); + // append password bytes + input.extend_from_slice(password.as_bytes()); + + let input = input.as_slice(); + let input_len = input.len(); + + let mut sha1 = Sha1::new(); + let mut count = COUNT; + while count > 0 { + if count > input_len { + sha1.input(input); + count -= input_len; + } else { + sha1.input(&input[0..count]); + break; + } + } + + const SHA1_BYTES: usize = 160 / 8; + let mut key = [0u8; SHA1_BYTES]; + sha1.result(key.as_mut_slice()); + + let mut hash = "16:".to_string(); + HEXUPPER.encode_append(salt, &mut hash); + HEXUPPER.encode_append(&key, &mut hash); + + hash + } + + fn hash_tor_password(password: &str) -> String { + let mut salt = [0x00u8; Self::S2K_RFC2440_SPECIFIER_LEN]; + OsRng.fill_bytes(&mut salt); + salt[Self::S2K_RFC2440_SPECIFIER_LEN - 1] = 0x60u8; + + Self::hash_tor_password_with_salt(&salt, password) + } + pub fn get_control_addr(&self) -> &SocketAddr { &self.control_addr } @@ -171,7 +232,7 @@ impl LegacyTorProcess { const CONTROL_PORT_PASSWORD_LENGTH: usize = 32usize; let password = generate_password(CONTROL_PORT_PASSWORD_LENGTH); - let password_hash = hash_tor_password(&password); + let password_hash = Self::hash_tor_password(&password); let mut process = Command::new(tor_bin_path.as_os_str()) .stdout(Stdio::piped()) @@ -286,3 +347,32 @@ impl Drop for LegacyTorProcess { let _ = self.process.kill(); } } + +#[test] +fn test_password_hash() -> Result<(), anyhow::Error> { + let salt1: [u8; LegacyTorProcess::S2K_RFC2440_SPECIFIER_LEN] = [ + 0xbeu8, 0x2au8, 0x25u8, 0x1du8, 0xe6u8, 0x2cu8, 0xb2u8, 0x7au8, 0x60u8, + ]; + let hash1 = LegacyTorProcess::hash_tor_password_with_salt(&salt1, "abcdefghijklmnopqrstuvwxyz"); + assert_eq!( + hash1, + "16:BE2A251DE62CB27A60AC9178A937990E8ED0AB662FA82A5C7DE3EBB23A" + ); + + let salt2: [u8; LegacyTorProcess::S2K_RFC2440_SPECIFIER_LEN] = [ + 0x36u8, 0x73u8, 0x0eu8, 0xefu8, 0xd1u8, 0x8cu8, 0x60u8, 0xd6u8, 0x60u8, + ]; + let hash2 = LegacyTorProcess::hash_tor_password_with_salt(&salt2, "password"); + assert_eq!( + hash2, + "16:36730EEFD18C60D66052E7EA535438761C0928D316EEA56A190C99B50A" + ); + + // ensure same password is hashed to different things + assert_ne!( + LegacyTorProcess::hash_tor_password("password"), + LegacyTorProcess::hash_tor_password("password") + ); + + Ok(()) +} diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 6c717a039..ce2a27acc 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -5,14 +5,12 @@ use std::str; // extern crates use crypto::digest::Digest; -use crypto::sha1::Sha1; use crypto::sha3::Sha3; -use data_encoding::{BASE32, BASE32_NOPAD, BASE64, HEXUPPER}; +use data_encoding::{BASE32, BASE32_NOPAD, BASE64}; use data_encoding_macro::new_encoding; use rand::distributions::Alphanumeric; use rand::rngs::OsRng; use rand::Rng; -use rand::RngCore; use signature::Verifier; use tor_llcrypto::pk::keymanip::*; use tor_llcrypto::util::rand_compat::RngCompatExt; @@ -78,29 +76,6 @@ const ONION_BASE32: data_encoding::Encoding = new_encoding! { padding: '=', }; -const SHA1_BYTES: usize = 160 / 8; -const S2K_RFC2440_SPECIFIER_LEN: usize = 9; - -// see https://github.com/torproject/torspec/blob/main/rend-spec-v3.txt#L2143 -fn calc_truncated_checksum( - public_key: &[u8; ED25519_PUBLIC_KEY_SIZE], -) -> [u8; TRUNCATED_CHECKSUM_SIZE] { - // space for full checksum - const SHA256_BYTES: usize = 256 / 8; - let mut hash_bytes = [0u8; SHA256_BYTES]; - - let mut hasher = Sha3::sha3_256(); - assert_eq!(SHA256_BYTES, hasher.output_bytes()); - - // calculate checksum - hasher.input(b".onion checksum"); - hasher.input(public_key); - hasher.input(&[0x03u8]); - hasher.result(&mut hash_bytes); - - [hash_bytes[0], hash_bytes[1]] -} - // Free functions // securely generate password using OsRng @@ -114,54 +89,6 @@ pub(crate) fn generate_password(length: usize) -> String { password } -fn hash_tor_password_with_salt(salt: &[u8; S2K_RFC2440_SPECIFIER_LEN], password: &str) -> String { - assert!(salt[S2K_RFC2440_SPECIFIER_LEN - 1] == 0x60); - - // tor-specific rfc 2440 constants - const EXPBIAS: u8 = 6u8; - const C: u8 = 0x60; // salt[S2K_RFC2440_SPECIFIER_LEN - 1] - const COUNT: usize = (16usize + ((C & 15u8) as usize)) << ((C >> 4) + EXPBIAS); - - // squash together our hash input - let mut input: Vec = Default::default(); - // append salt (sans the 'C' constant') - input.extend_from_slice(&salt[0..S2K_RFC2440_SPECIFIER_LEN - 1]); - // append password bytes - input.extend_from_slice(password.as_bytes()); - - let input = input.as_slice(); - let input_len = input.len(); - - let mut sha1 = Sha1::new(); - let mut count = COUNT; - while count > 0 { - if count > input_len { - sha1.input(input); - count -= input_len; - } else { - sha1.input(&input[0..count]); - break; - } - } - - let mut key = [0u8; SHA1_BYTES]; - sha1.result(key.as_mut_slice()); - - let mut hash = "16:".to_string(); - HEXUPPER.encode_append(salt, &mut hash); - HEXUPPER.encode_append(&key, &mut hash); - - hash -} - -pub fn hash_tor_password(password: &str) -> String { - let mut salt = [0x00u8; S2K_RFC2440_SPECIFIER_LEN]; - OsRng.fill_bytes(&mut salt); - salt[S2K_RFC2440_SPECIFIER_LEN - 1] = 0x60u8; - - hash_tor_password_with_salt(&salt, password) -} - // Struct deinitions pub struct Ed25519PrivateKey { @@ -440,6 +367,12 @@ impl PartialEq for Ed25519PublicKey { } } +impl std::fmt::Debug for Ed25519PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.public_key.fmt(f) + } +} + // Ed25519 Signature impl Ed25519Signature { @@ -488,6 +421,12 @@ impl PartialEq for Ed25519Signature { } } +impl std::fmt::Debug for Ed25519Signature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.signature.fmt(f) + } +} + // X25519 Private Key impl X25519PrivateKey { @@ -626,6 +565,26 @@ impl std::fmt::Debug for X25519PublicKey { // Onion Service Id impl V3OnionServiceId { + // see https://github.com/torproject/torspec/blob/main/rend-spec-v3.txt#L2143 + fn calc_truncated_checksum( + public_key: &[u8; ED25519_PUBLIC_KEY_SIZE], + ) -> [u8; TRUNCATED_CHECKSUM_SIZE] { + // space for full checksum + const SHA256_BYTES: usize = 256 / 8; + let mut hash_bytes = [0u8; SHA256_BYTES]; + + let mut hasher = Sha3::sha3_256(); + assert_eq!(SHA256_BYTES, hasher.output_bytes()); + + // calculate checksum + hasher.input(b".onion checksum"); + hasher.input(public_key); + hasher.input(&[0x03u8]); + hasher.result(&mut hash_bytes); + + [hash_bytes[0], hash_bytes[1]] + } + pub fn from_string(service_id: &str) -> Result { if !V3OnionServiceId::is_valid(service_id) { return Err(Error::ParseError(format!( @@ -642,7 +601,7 @@ impl V3OnionServiceId { let mut raw_service_id = [0u8; V3_ONION_SERVICE_ID_RAW_SIZE]; raw_service_id[..ED25519_PUBLIC_KEY_SIZE].copy_from_slice(&public_key.as_bytes()[..]); - let truncated_checksum = calc_truncated_checksum(public_key.as_bytes()); + let truncated_checksum = Self::calc_truncated_checksum(public_key.as_bytes()); raw_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET] = truncated_checksum[0]; raw_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET + 1] = truncated_checksum[1]; raw_service_id[V3_ONION_SERVICE_ID_VERSION_OFFSET] = 0x03u8; @@ -678,7 +637,7 @@ impl V3OnionServiceId { let mut public_key = [0u8; ED25519_PUBLIC_KEY_SIZE]; public_key[..].copy_from_slice(&decoded_service_id[..ED25519_PUBLIC_KEY_SIZE]); // ensure checksum is correct - let truncated_checksum = calc_truncated_checksum(&public_key); + let truncated_checksum = Self::calc_truncated_checksum(&public_key); if truncated_checksum[0] != decoded_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET] || truncated_checksum[1] != decoded_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET + 1] @@ -707,131 +666,3 @@ impl std::fmt::Debug for V3OnionServiceId { unsafe { write!(f, "{}", str::from_utf8_unchecked(&self.data)) } } } - -#[test] -fn test_ed25519() -> Result<(), anyhow::Error> { - let private_key_blob = "ED25519-V3:YE3GZtDmc+izGijWKgeVRabbXqK456JKKGONDBhV+kPBVKa2mHVQqnRTVuFXe3inU3YW6qvc7glYEwe9rK0LhQ=="; - let private_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = [ - 0x60u8, 0x4du8, 0xc6u8, 0x66u8, 0xd0u8, 0xe6u8, 0x73u8, 0xe8u8, 0xb3u8, 0x1au8, 0x28u8, - 0xd6u8, 0x2au8, 0x07u8, 0x95u8, 0x45u8, 0xa6u8, 0xdbu8, 0x5eu8, 0xa2u8, 0xb8u8, 0xe7u8, - 0xa2u8, 0x4au8, 0x28u8, 0x63u8, 0x8du8, 0x0cu8, 0x18u8, 0x55u8, 0xfau8, 0x43u8, 0xc1u8, - 0x54u8, 0xa6u8, 0xb6u8, 0x98u8, 0x75u8, 0x50u8, 0xaau8, 0x74u8, 0x53u8, 0x56u8, 0xe1u8, - 0x57u8, 0x7bu8, 0x78u8, 0xa7u8, 0x53u8, 0x76u8, 0x16u8, 0xeau8, 0xabu8, 0xdcu8, 0xeeu8, - 0x09u8, 0x58u8, 0x13u8, 0x07u8, 0xbdu8, 0xacu8, 0xadu8, 0x0bu8, 0x85u8, - ]; - let public_raw: [u8; ED25519_PUBLIC_KEY_SIZE] = [ - 0xf2u8, 0xfdu8, 0xa2u8, 0xdbu8, 0xf3u8, 0x80u8, 0xa6u8, 0xbau8, 0x74u8, 0xa4u8, 0x90u8, - 0xe1u8, 0x45u8, 0x55u8, 0xeeu8, 0xb9u8, 0x32u8, 0xa0u8, 0x5cu8, 0x39u8, 0x5au8, 0xe2u8, - 0x02u8, 0x83u8, 0x55u8, 0x27u8, 0x89u8, 0x6au8, 0x1fu8, 0x2fu8, 0x3du8, 0xc5u8, - ]; - let public_base32 = "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCQ===="; - let service_id_string = "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd"; - assert!(V3OnionServiceId::is_valid(&service_id_string)); - - let mut message = [0x00u8; 256]; - let null_message = [0x00u8; 256]; - for (i, ptr) in message.iter_mut().enumerate() { - *ptr = i as u8; - } - let signature_raw: [u8; ED25519_SIGNATURE_SIZE] = [ - 0xa6u8, 0xd6u8, 0xc6u8, 0x1au8, 0x03u8, 0xbcu8, 0x43u8, 0x6fu8, 0x38u8, 0x53u8, 0x94u8, - 0xcdu8, 0xdcu8, 0x86u8, 0x0au8, 0x88u8, 0x64u8, 0x43u8, 0x1du8, 0x18u8, 0x84u8, 0x30u8, - 0x2fu8, 0xcdu8, 0xa6u8, 0x79u8, 0xcau8, 0x87u8, 0xd0u8, 0x29u8, 0xe7u8, 0x2bu8, 0x32u8, - 0x9bu8, 0xa2u8, 0xa4u8, 0x3cu8, 0x74u8, 0x6au8, 0x08u8, 0x67u8, 0x0eu8, 0x63u8, 0x60u8, - 0xcbu8, 0x46u8, 0x22u8, 0x55u8, 0x43u8, 0x5bu8, 0x84u8, 0x68u8, 0x0fu8, 0x47u8, 0xceu8, - 0x6cu8, 0xd2u8, 0xb8u8, 0xebu8, 0xfeu8, 0xf6u8, 0x9eu8, 0x97u8, 0x0au8, - ]; - - // test the golden path first - let service_id = V3OnionServiceId::from_string(&service_id_string)?; - - let private_key = Ed25519PrivateKey::from_raw(&private_raw); - assert!(private_key == Ed25519PrivateKey::from_key_blob(&private_key_blob)?); - assert!(private_key_blob == private_key.to_key_blob()); - - let public_key = Ed25519PublicKey::from_raw(&public_raw)?; - assert!(public_key == Ed25519PublicKey::from_service_id(&service_id)?); - assert!(public_key == Ed25519PublicKey::from_private_key(&private_key)); - assert!(service_id == V3OnionServiceId::from_public_key(&public_key)); - assert!(public_base32 == public_key.to_base32()); - - let signature = private_key.sign_message(&message); - assert!(signature == Ed25519Signature::from_raw(&signature_raw)?); - assert!(signature.verify(&message, &public_key)); - assert!(!signature.verify(&null_message, &public_key)); - - // some invalid service ids - assert!(!V3OnionServiceId::is_valid("")); - assert!(!V3OnionServiceId::is_valid( - " - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - )); - assert!(!V3OnionServiceId::is_valid( - "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCZSJYD" - )); - - // generate a new key, get the public key and sign/verify a message - let private_key = Ed25519PrivateKey::generate(); - let public_key = Ed25519PublicKey::from_private_key(&private_key); - let signature = private_key.sign_message(&message); - assert!(signature.verify(&message, &public_key)); - - Ok(()) -} - -#[test] -fn test_password_hash() -> Result<(), anyhow::Error> { - let salt1: [u8; S2K_RFC2440_SPECIFIER_LEN] = [ - 0xbeu8, 0x2au8, 0x25u8, 0x1du8, 0xe6u8, 0x2cu8, 0xb2u8, 0x7au8, 0x60u8, - ]; - let hash1 = hash_tor_password_with_salt(&salt1, "abcdefghijklmnopqrstuvwxyz"); - assert!(hash1 == "16:BE2A251DE62CB27A60AC9178A937990E8ED0AB662FA82A5C7DE3EBB23A"); - - let salt2: [u8; S2K_RFC2440_SPECIFIER_LEN] = [ - 0x36u8, 0x73u8, 0x0eu8, 0xefu8, 0xd1u8, 0x8cu8, 0x60u8, 0xd6u8, 0x60u8, - ]; - let hash2 = hash_tor_password_with_salt(&salt2, "password"); - assert!(hash2 == "16:36730EEFD18C60D66052E7EA535438761C0928D316EEA56A190C99B50A"); - - // ensure same password is hashed to different things - assert!(hash_tor_password("password") != hash_tor_password("password")); - - Ok(()) -} - -#[test] -fn test_x25519() -> Result<(), anyhow::Error> { - // private/public key pair - const SECRET_BASE64: &str = "0GeSReJXdNcgvWRQdnDXhJGdu5UiwP2fefgT93/oqn0="; - const SECRET_RAW: [u8; X25519_PRIVATE_KEY_SIZE] = [ - 0xd0u8, 0x67u8, 0x92u8, 0x45u8, 0xe2u8, 0x57u8, 0x74u8, 0xd7u8, 0x20u8, 0xbdu8, 0x64u8, - 0x50u8, 0x76u8, 0x70u8, 0xd7u8, 0x84u8, 0x91u8, 0x9du8, 0xbbu8, 0x95u8, 0x22u8, 0xc0u8, - 0xfdu8, 0x9fu8, 0x79u8, 0xf8u8, 0x13u8, 0xf7u8, 0x7fu8, 0xe8u8, 0xaau8, 0x7du8, - ]; - const PUBLIC_BASE32: &str = "AEXCBCEDJ5KU34YGGMZ7PVHVDEA7D7YB7VQAPJTMTZGRJLN3JASA"; - const PUBLIC_RAW: [u8; X25519_PUBLIC_KEY_SIZE] = [ - 0x01u8, 0x2eu8, 0x20u8, 0x88u8, 0x83u8, 0x4fu8, 0x55u8, 0x4du8, 0xf3u8, 0x06u8, 0x33u8, - 0x33u8, 0xf7u8, 0xd4u8, 0xf5u8, 0x19u8, 0x01u8, 0xf1u8, 0xffu8, 0x01u8, 0xfdu8, 0x60u8, - 0x07u8, 0xa6u8, 0x6cu8, 0x9eu8, 0x4du8, 0x14u8, 0xadu8, 0xbbu8, 0x48u8, 0x24u8, - ]; - - // ensure we can convert from raw as expected - assert!(&X25519PrivateKey::from_raw(&SECRET_RAW).to_base64() == SECRET_BASE64); - assert!(&X25519PublicKey::from_raw(&PUBLIC_RAW).to_base32() == PUBLIC_BASE32); - - // ensure we can round-trip as expected - assert!(&X25519PrivateKey::from_base64(&SECRET_BASE64)?.to_base64() == SECRET_BASE64); - assert!(&X25519PublicKey::from_base32(&PUBLIC_BASE32)?.to_base32() == PUBLIC_BASE32); - - // ensure we generate the expected public key from private key - let private_key = X25519PrivateKey::from_base64(&SECRET_BASE64)?; - let public_key = X25519PublicKey::from_private_key(&private_key); - assert!(public_key.to_base32() == PUBLIC_BASE32); - - let message = b"All around me are familiar faces"; - - let (signature, signbit) = private_key.sign_message(message)?; - assert!(signature.verify_x25519(message, &public_key, signbit)); - - Ok(()) -} diff --git a/tor-interface/tests/tor_crypto.rs b/tor-interface/tests/tor_crypto.rs new file mode 100644 index 000000000..f69193997 --- /dev/null +++ b/tor-interface/tests/tor_crypto.rs @@ -0,0 +1,125 @@ +// internal crates +use tor_interface::tor_crypto::*; + +#[test] +fn test_ed25519() -> Result<(), anyhow::Error> { + let private_key_blob = "ED25519-V3:YE3GZtDmc+izGijWKgeVRabbXqK456JKKGONDBhV+kPBVKa2mHVQqnRTVuFXe3inU3YW6qvc7glYEwe9rK0LhQ=="; + let private_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = [ + 0x60u8, 0x4du8, 0xc6u8, 0x66u8, 0xd0u8, 0xe6u8, 0x73u8, 0xe8u8, 0xb3u8, 0x1au8, 0x28u8, + 0xd6u8, 0x2au8, 0x07u8, 0x95u8, 0x45u8, 0xa6u8, 0xdbu8, 0x5eu8, 0xa2u8, 0xb8u8, 0xe7u8, + 0xa2u8, 0x4au8, 0x28u8, 0x63u8, 0x8du8, 0x0cu8, 0x18u8, 0x55u8, 0xfau8, 0x43u8, 0xc1u8, + 0x54u8, 0xa6u8, 0xb6u8, 0x98u8, 0x75u8, 0x50u8, 0xaau8, 0x74u8, 0x53u8, 0x56u8, 0xe1u8, + 0x57u8, 0x7bu8, 0x78u8, 0xa7u8, 0x53u8, 0x76u8, 0x16u8, 0xeau8, 0xabu8, 0xdcu8, 0xeeu8, + 0x09u8, 0x58u8, 0x13u8, 0x07u8, 0xbdu8, 0xacu8, 0xadu8, 0x0bu8, 0x85u8, + ]; + let public_raw: [u8; ED25519_PUBLIC_KEY_SIZE] = [ + 0xf2u8, 0xfdu8, 0xa2u8, 0xdbu8, 0xf3u8, 0x80u8, 0xa6u8, 0xbau8, 0x74u8, 0xa4u8, 0x90u8, + 0xe1u8, 0x45u8, 0x55u8, 0xeeu8, 0xb9u8, 0x32u8, 0xa0u8, 0x5cu8, 0x39u8, 0x5au8, 0xe2u8, + 0x02u8, 0x83u8, 0x55u8, 0x27u8, 0x89u8, 0x6au8, 0x1fu8, 0x2fu8, 0x3du8, 0xc5u8, + ]; + let public_base32 = "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCQ===="; + let service_id_string = "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd"; + assert!(V3OnionServiceId::is_valid(&service_id_string)); + + let mut message = [0x00u8; 256]; + let null_message = [0x00u8; 256]; + for (i, ptr) in message.iter_mut().enumerate() { + *ptr = i as u8; + } + let signature_raw: [u8; ED25519_SIGNATURE_SIZE] = [ + 0xa6u8, 0xd6u8, 0xc6u8, 0x1au8, 0x03u8, 0xbcu8, 0x43u8, 0x6fu8, 0x38u8, 0x53u8, 0x94u8, + 0xcdu8, 0xdcu8, 0x86u8, 0x0au8, 0x88u8, 0x64u8, 0x43u8, 0x1du8, 0x18u8, 0x84u8, 0x30u8, + 0x2fu8, 0xcdu8, 0xa6u8, 0x79u8, 0xcau8, 0x87u8, 0xd0u8, 0x29u8, 0xe7u8, 0x2bu8, 0x32u8, + 0x9bu8, 0xa2u8, 0xa4u8, 0x3cu8, 0x74u8, 0x6au8, 0x08u8, 0x67u8, 0x0eu8, 0x63u8, 0x60u8, + 0xcbu8, 0x46u8, 0x22u8, 0x55u8, 0x43u8, 0x5bu8, 0x84u8, 0x68u8, 0x0fu8, 0x47u8, 0xceu8, + 0x6cu8, 0xd2u8, 0xb8u8, 0xebu8, 0xfeu8, 0xf6u8, 0x9eu8, 0x97u8, 0x0au8, + ]; + + // test the golden path first + let service_id = V3OnionServiceId::from_string(&service_id_string)?; + + let private_key = Ed25519PrivateKey::from_raw(&private_raw); + assert_eq!( + private_key, + Ed25519PrivateKey::from_key_blob(&private_key_blob)? + ); + assert_eq!(private_key_blob, private_key.to_key_blob()); + + let public_key = Ed25519PublicKey::from_raw(&public_raw)?; + assert_eq!(public_key, Ed25519PublicKey::from_service_id(&service_id)?); + assert_eq!(public_key, Ed25519PublicKey::from_private_key(&private_key)); + assert_eq!(service_id, V3OnionServiceId::from_public_key(&public_key)); + assert_eq!(public_base32, public_key.to_base32()); + + let signature = private_key.sign_message(&message); + assert_eq!(signature, Ed25519Signature::from_raw(&signature_raw)?); + assert!(signature.verify(&message, &public_key)); + assert!(!signature.verify(&null_message, &public_key)); + + // some invalid service ids + assert!(!V3OnionServiceId::is_valid("")); + assert!(!V3OnionServiceId::is_valid( + " + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + )); + assert!(!V3OnionServiceId::is_valid( + "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCZSJYD" + )); + + // generate a new key, get the public key and sign/verify a message + let private_key = Ed25519PrivateKey::generate(); + let public_key = Ed25519PublicKey::from_private_key(&private_key); + let signature = private_key.sign_message(&message); + assert!(signature.verify(&message, &public_key)); + + Ok(()) +} + +#[test] +fn test_x25519() -> Result<(), anyhow::Error> { + // private/public key pair + const SECRET_BASE64: &str = "0GeSReJXdNcgvWRQdnDXhJGdu5UiwP2fefgT93/oqn0="; + const SECRET_RAW: [u8; X25519_PRIVATE_KEY_SIZE] = [ + 0xd0u8, 0x67u8, 0x92u8, 0x45u8, 0xe2u8, 0x57u8, 0x74u8, 0xd7u8, 0x20u8, 0xbdu8, 0x64u8, + 0x50u8, 0x76u8, 0x70u8, 0xd7u8, 0x84u8, 0x91u8, 0x9du8, 0xbbu8, 0x95u8, 0x22u8, 0xc0u8, + 0xfdu8, 0x9fu8, 0x79u8, 0xf8u8, 0x13u8, 0xf7u8, 0x7fu8, 0xe8u8, 0xaau8, 0x7du8, + ]; + const PUBLIC_BASE32: &str = "AEXCBCEDJ5KU34YGGMZ7PVHVDEA7D7YB7VQAPJTMTZGRJLN3JASA"; + const PUBLIC_RAW: [u8; X25519_PUBLIC_KEY_SIZE] = [ + 0x01u8, 0x2eu8, 0x20u8, 0x88u8, 0x83u8, 0x4fu8, 0x55u8, 0x4du8, 0xf3u8, 0x06u8, 0x33u8, + 0x33u8, 0xf7u8, 0xd4u8, 0xf5u8, 0x19u8, 0x01u8, 0xf1u8, 0xffu8, 0x01u8, 0xfdu8, 0x60u8, + 0x07u8, 0xa6u8, 0x6cu8, 0x9eu8, 0x4du8, 0x14u8, 0xadu8, 0xbbu8, 0x48u8, 0x24u8, + ]; + + // ensure we can convert from raw as expected + assert_eq!( + &X25519PrivateKey::from_raw(&SECRET_RAW).to_base64(), + SECRET_BASE64 + ); + assert_eq!( + &X25519PublicKey::from_raw(&PUBLIC_RAW).to_base32(), + PUBLIC_BASE32 + ); + + // ensure we can round-trip as expected + assert_eq!( + &X25519PrivateKey::from_base64(&SECRET_BASE64)?.to_base64(), + SECRET_BASE64 + ); + assert_eq!( + &X25519PublicKey::from_base32(&PUBLIC_BASE32)?.to_base32(), + PUBLIC_BASE32 + ); + + // ensure we generate the expected public key from private key + let private_key = X25519PrivateKey::from_base64(&SECRET_BASE64)?; + let public_key = X25519PublicKey::from_private_key(&private_key); + assert_eq!(public_key.to_base32(), PUBLIC_BASE32); + + let message = b"All around me are familiar faces"; + + let (signature, signbit) = private_key.sign_message(message)?; + assert!(signature.verify_x25519(message, &public_key, signbit)); + + Ok(()) +} From 3414a28ee6545f5eece0874387da27d53536a521 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 26 Aug 2023 21:06:00 +0000 Subject: [PATCH 031/184] tor-interface: added initial fuzz support for the tor_crypto module --- tor-interface/CMakeLists.txt | 10 ++++++- tor-interface/fuzz/.gitignore | 4 +++ tor-interface/fuzz/Cargo.toml | 27 +++++++++++++++++++ .../fuzz/fuzz_targets/fuzz_crypto.rs | 7 +++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tor-interface/fuzz/.gitignore create mode 100644 tor-interface/fuzz/Cargo.toml create mode 100644 tor-interface/fuzz/fuzz_targets/fuzz_crypto.rs diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 3681e7924..0a94da8f0 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -27,4 +27,12 @@ add_custom_target(tor_interface_cargo_test add_custom_target(tor_interface_cargo_test_offline COMMAND RUSTFLAGS=${RUSTFLAGS} CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} --features offline-test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} -) \ No newline at end of file +) + +# +# fuzz target +# +add_custom_target(tor_interface_cargo_fuzz_crypto + COMMAND RUSTFLAGS=${RUSTFLAGS} CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUST_BACKTRACE=full cargo fuzz run fuzz_crypto ${CARGO_FLAGS} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/tor-interface/fuzz/.gitignore b/tor-interface/fuzz/.gitignore new file mode 100644 index 000000000..1a45eee77 --- /dev/null +++ b/tor-interface/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/tor-interface/fuzz/Cargo.toml b/tor-interface/fuzz/Cargo.toml new file mode 100644 index 000000000..3140b811d --- /dev/null +++ b/tor-interface/fuzz/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tor-interface-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.tor-interface] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "fuzz_crypto" +path = "fuzz_targets/fuzz_crypto.rs" +test = false +doc = false diff --git a/tor-interface/fuzz/fuzz_targets/fuzz_crypto.rs b/tor-interface/fuzz/fuzz_targets/fuzz_crypto.rs new file mode 100644 index 000000000..43a88c14f --- /dev/null +++ b/tor-interface/fuzz/fuzz_targets/fuzz_crypto.rs @@ -0,0 +1,7 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // fuzzed code goes here +}); From 16abcb8b91c7436387faf39e2818621cdde0be94 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 26 Aug 2023 23:33:48 +0000 Subject: [PATCH 032/184] tor-interface: update the from_raw() method on Ed25519PrivateKey to validate the provided bytes are usable private key --- tor-interface/src/tor_crypto.rs | 27 ++++++++++++++++++--------- tor-interface/tests/tor_crypto.rs | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index ce2a27acc..a275d1a19 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -22,6 +22,8 @@ pub enum Error { ParseError(String), #[error("{0}")] ConversionError(String), + #[error("signature error")] + SignatureError, } /// The number of bytes in an ed25519 secret key @@ -165,14 +167,18 @@ impl Ed25519PrivateKey { } } - // according to nickm, any 64 byte string here is allowed - pub fn from_raw(raw: &[u8; ED25519_PRIVATE_KEY_SIZE]) -> Ed25519PrivateKey { - Ed25519PrivateKey { - expanded_secret_key: match pk::ed25519::ExpandedSecretKey::from_bytes(raw) { - Ok(expanded_secret_key) => expanded_secret_key, - Err(_) => unreachable!(), - }, + pub fn from_raw(raw: &[u8; ED25519_PRIVATE_KEY_SIZE]) -> Result { + if let Ok(expanded_secret_key) = pk::ed25519::ExpandedSecretKey::from_bytes(raw) { + let public_key = pk::ed25519::PublicKey::from(&expanded_secret_key); + // verify signature of a test message + // see: https://gitlab.torproject.org/tpo/core/arti/-/issues/1021 + const MESSAGE: &[u8] = &[0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8]; + let signature = expanded_secret_key.sign(MESSAGE, &public_key); + if let Ok(()) = public_key.verify(MESSAGE, &signature) { + return Ok(Ed25519PrivateKey{ expanded_secret_key }); + } } + Err(Error::SignatureError) } pub fn from_key_blob(key_blob: &str) -> Result { @@ -213,7 +219,7 @@ impl Ed25519PrivateKey { } }; - Ok(Ed25519PrivateKey::from_raw(&private_key_data_raw)) + Ed25519PrivateKey::from_raw(&private_key_data_raw) } pub fn from_private_x25519( @@ -280,7 +286,10 @@ impl PartialEq for Ed25519PrivateKey { impl Clone for Ed25519PrivateKey { fn clone(&self) -> Ed25519PrivateKey { - Ed25519PrivateKey::from_raw(&self.to_bytes()) + match Ed25519PrivateKey::from_raw(&self.to_bytes()) { + Ok(ed25519_private_key) => ed25519_private_key, + Err(_) => unreachable!(), + } } } diff --git a/tor-interface/tests/tor_crypto.rs b/tor-interface/tests/tor_crypto.rs index f69193997..108824b02 100644 --- a/tor-interface/tests/tor_crypto.rs +++ b/tor-interface/tests/tor_crypto.rs @@ -38,7 +38,7 @@ fn test_ed25519() -> Result<(), anyhow::Error> { // test the golden path first let service_id = V3OnionServiceId::from_string(&service_id_string)?; - let private_key = Ed25519PrivateKey::from_raw(&private_raw); + let private_key = Ed25519PrivateKey::from_raw(&private_raw)?; assert_eq!( private_key, Ed25519PrivateKey::from_key_blob(&private_key_blob)? @@ -72,6 +72,22 @@ fn test_ed25519() -> Result<(), anyhow::Error> { let signature = private_key.sign_message(&message); assert!(signature.verify(&message, &public_key)); + // test invalid private key blob returns an error + // https://gitlab.torproject.org/tpo/core/arti/-/issues/1021 + let private_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = [ + 0x2eu8, 0x26u8, 0x0au8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x0au8, 0x77u8, 0x77u8, + 0x77u8, 0x77u8, 0x5du8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, + 0x82u8, 0xb4u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, + 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0xffu8, + 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, + 0xffu8, 0xffu8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x82u8, 0x88u8, + ]; + match Ed25519PrivateKey::from_raw(&private_raw) { + Ok(_) => panic!("invalid key accepted"), + Err(tor_interface::tor_crypto::Error::SignatureError) => (), + Err(err) => panic!("unexpectd error: {:?}", err), + } + Ok(()) } From bf757bc83acdd37cb3b98331605833b3b3249fea Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 27 Aug 2023 18:05:11 +0000 Subject: [PATCH 033/184] tor-interface: updated Ed25519PrivateKey::from_raw() function to check bytes 0 and 31 have correct bits for an expanded ed25519 private key --- tor-interface/src/tor_crypto.rs | 22 ++++++++--------- tor-interface/tests/tor_crypto.rs | 39 +++++++++++++++++-------------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index a275d1a19..12c1a1d63 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -22,8 +22,8 @@ pub enum Error { ParseError(String), #[error("{0}")] ConversionError(String), - #[error("signature error")] - SignatureError, + #[error("invalid key")] + KeyInvalid, } /// The number of bytes in an ed25519 secret key @@ -168,17 +168,17 @@ impl Ed25519PrivateKey { } pub fn from_raw(raw: &[u8; ED25519_PRIVATE_KEY_SIZE]) -> Result { - if let Ok(expanded_secret_key) = pk::ed25519::ExpandedSecretKey::from_bytes(raw) { - let public_key = pk::ed25519::PublicKey::from(&expanded_secret_key); - // verify signature of a test message - // see: https://gitlab.torproject.org/tpo/core/arti/-/issues/1021 - const MESSAGE: &[u8] = &[0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8]; - let signature = expanded_secret_key.sign(MESSAGE, &public_key); - if let Ok(()) = public_key.verify(MESSAGE, &signature) { - return Ok(Ed25519PrivateKey{ expanded_secret_key }); + // Verify the provided bytes have bits set correctly + // see: https://gitlab.torproject.org/tpo/core/arti/-/issues/1021 + if raw[0] == raw[0] & 248 && + raw[31] == (raw[31] & 63) | 64 { + match pk::ed25519::ExpandedSecretKey::from_bytes(raw) { + Ok(expanded_secret_key) => Ok(Ed25519PrivateKey{ expanded_secret_key }), + Err(_) => Err(Error::KeyInvalid), } + } else { + Err(Error::KeyInvalid) } - Err(Error::SignatureError) } pub fn from_key_blob(key_blob: &str) -> Result { diff --git a/tor-interface/tests/tor_crypto.rs b/tor-interface/tests/tor_crypto.rs index 108824b02..54aed8fe5 100644 --- a/tor-interface/tests/tor_crypto.rs +++ b/tor-interface/tests/tor_crypto.rs @@ -5,17 +5,20 @@ use tor_interface::tor_crypto::*; fn test_ed25519() -> Result<(), anyhow::Error> { let private_key_blob = "ED25519-V3:YE3GZtDmc+izGijWKgeVRabbXqK456JKKGONDBhV+kPBVKa2mHVQqnRTVuFXe3inU3YW6qvc7glYEwe9rK0LhQ=="; let private_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = [ - 0x60u8, 0x4du8, 0xc6u8, 0x66u8, 0xd0u8, 0xe6u8, 0x73u8, 0xe8u8, 0xb3u8, 0x1au8, 0x28u8, - 0xd6u8, 0x2au8, 0x07u8, 0x95u8, 0x45u8, 0xa6u8, 0xdbu8, 0x5eu8, 0xa2u8, 0xb8u8, 0xe7u8, - 0xa2u8, 0x4au8, 0x28u8, 0x63u8, 0x8du8, 0x0cu8, 0x18u8, 0x55u8, 0xfau8, 0x43u8, 0xc1u8, - 0x54u8, 0xa6u8, 0xb6u8, 0x98u8, 0x75u8, 0x50u8, 0xaau8, 0x74u8, 0x53u8, 0x56u8, 0xe1u8, - 0x57u8, 0x7bu8, 0x78u8, 0xa7u8, 0x53u8, 0x76u8, 0x16u8, 0xeau8, 0xabu8, 0xdcu8, 0xeeu8, - 0x09u8, 0x58u8, 0x13u8, 0x07u8, 0xbdu8, 0xacu8, 0xadu8, 0x0bu8, 0x85u8, + 0x60u8, 0x4du8, 0xc6u8, 0x66u8, 0xd0u8, 0xe6u8, 0x73u8, 0xe8u8, + 0xb3u8, 0x1au8, 0x28u8, 0xd6u8, 0x2au8, 0x07u8, 0x95u8, 0x45u8, + 0xa6u8, 0xdbu8, 0x5eu8, 0xa2u8, 0xb8u8, 0xe7u8, 0xa2u8, 0x4au8, + 0x28u8, 0x63u8, 0x8du8, 0x0cu8, 0x18u8, 0x55u8, 0xfau8, 0x43u8, + 0xc1u8, 0x54u8, 0xa6u8, 0xb6u8, 0x98u8, 0x75u8, 0x50u8, 0xaau8, + 0x74u8, 0x53u8, 0x56u8, 0xe1u8, 0x57u8, 0x7bu8, 0x78u8, 0xa7u8, + 0x53u8, 0x76u8, 0x16u8, 0xeau8, 0xabu8, 0xdcu8, 0xeeu8, 0x09u8, + 0x58u8, 0x13u8, 0x07u8, 0xbdu8, 0xacu8, 0xadu8, 0x0bu8, 0x85u8, ]; let public_raw: [u8; ED25519_PUBLIC_KEY_SIZE] = [ - 0xf2u8, 0xfdu8, 0xa2u8, 0xdbu8, 0xf3u8, 0x80u8, 0xa6u8, 0xbau8, 0x74u8, 0xa4u8, 0x90u8, - 0xe1u8, 0x45u8, 0x55u8, 0xeeu8, 0xb9u8, 0x32u8, 0xa0u8, 0x5cu8, 0x39u8, 0x5au8, 0xe2u8, - 0x02u8, 0x83u8, 0x55u8, 0x27u8, 0x89u8, 0x6au8, 0x1fu8, 0x2fu8, 0x3du8, 0xc5u8, + 0xf2u8, 0xfdu8, 0xa2u8, 0xdbu8, 0xf3u8, 0x80u8, 0xa6u8, 0xbau8, + 0x74u8, 0xa4u8, 0x90u8, 0xe1u8, 0x45u8, 0x55u8, 0xeeu8, 0xb9u8, + 0x32u8, 0xa0u8, 0x5cu8, 0x39u8, 0x5au8, 0xe2u8, 0x02u8, 0x83u8, + 0x55u8, 0x27u8, 0x89u8, 0x6au8, 0x1fu8, 0x2fu8, 0x3du8, 0xc5u8, ]; let public_base32 = "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCQ===="; let service_id_string = "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd"; @@ -27,12 +30,14 @@ fn test_ed25519() -> Result<(), anyhow::Error> { *ptr = i as u8; } let signature_raw: [u8; ED25519_SIGNATURE_SIZE] = [ - 0xa6u8, 0xd6u8, 0xc6u8, 0x1au8, 0x03u8, 0xbcu8, 0x43u8, 0x6fu8, 0x38u8, 0x53u8, 0x94u8, - 0xcdu8, 0xdcu8, 0x86u8, 0x0au8, 0x88u8, 0x64u8, 0x43u8, 0x1du8, 0x18u8, 0x84u8, 0x30u8, - 0x2fu8, 0xcdu8, 0xa6u8, 0x79u8, 0xcau8, 0x87u8, 0xd0u8, 0x29u8, 0xe7u8, 0x2bu8, 0x32u8, - 0x9bu8, 0xa2u8, 0xa4u8, 0x3cu8, 0x74u8, 0x6au8, 0x08u8, 0x67u8, 0x0eu8, 0x63u8, 0x60u8, - 0xcbu8, 0x46u8, 0x22u8, 0x55u8, 0x43u8, 0x5bu8, 0x84u8, 0x68u8, 0x0fu8, 0x47u8, 0xceu8, - 0x6cu8, 0xd2u8, 0xb8u8, 0xebu8, 0xfeu8, 0xf6u8, 0x9eu8, 0x97u8, 0x0au8, + 0xa6u8, 0xd6u8, 0xc6u8, 0x1au8, 0x03u8, 0xbcu8, 0x43u8, 0x6fu8, + 0x38u8, 0x53u8, 0x94u8, 0xcdu8, 0xdcu8, 0x86u8, 0x0au8, 0x88u8, + 0x64u8, 0x43u8, 0x1du8, 0x18u8, 0x84u8, 0x30u8, 0x2fu8, 0xcdu8, + 0xa6u8, 0x79u8, 0xcau8, 0x87u8, 0xd0u8, 0x29u8, 0xe7u8, 0x2bu8, + 0x32u8, 0x9bu8, 0xa2u8, 0xa4u8, 0x3cu8, 0x74u8, 0x6au8, 0x08u8, + 0x67u8, 0x0eu8, 0x63u8, 0x60u8, 0xcbu8, 0x46u8, 0x22u8, 0x55u8, + 0x43u8, 0x5bu8, 0x84u8, 0x68u8, 0x0fu8, 0x47u8, 0xceu8, 0x6cu8, + 0xd2u8, 0xb8u8, 0xebu8, 0xfeu8, 0xf6u8, 0x9eu8, 0x97u8, 0x0au8, ]; // test the golden path first @@ -84,8 +89,8 @@ fn test_ed25519() -> Result<(), anyhow::Error> { ]; match Ed25519PrivateKey::from_raw(&private_raw) { Ok(_) => panic!("invalid key accepted"), - Err(tor_interface::tor_crypto::Error::SignatureError) => (), - Err(err) => panic!("unexpectd error: {:?}", err), + Err(tor_interface::tor_crypto::Error::KeyInvalid) => (), + Err(err) => panic!("unexpected error: {:?}", err), } Ok(()) From df475d34fffcf93770ec14513db33c2d5657c278 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 27 Aug 2023 21:44:49 +0000 Subject: [PATCH 034/184] tor-interface: implement PartialEq for X25119PrivateKey --- tor-interface/src/tor_crypto.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 12c1a1d63..f636a89b4 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -503,6 +503,12 @@ impl X25519PrivateKey { } } +impl PartialEq for X25519PrivateKey { + fn eq(&self, other: &Self) -> bool { + self.secret_key.to_bytes() == other.secret_key.to_bytes() + } +} + impl std::fmt::Debug for X25519PrivateKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "--- x25519 private key ---") From 2e5c2e125856bab9e0ecffa89002e38e51367793 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 28 Aug 2023 00:05:21 +0000 Subject: [PATCH 035/184] tor-interface: updated the from_raw() method on X25519PrivateKey to valiate the provided bytes are usuable privte key --- tor-interface/src/tor_crypto.rs | 12 ++++++++---- tor-interface/tests/tor_crypto.rs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index f636a89b4..aa7a8b533 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -445,9 +445,13 @@ impl X25519PrivateKey { } } - pub fn from_raw(raw: &[u8; X25519_PRIVATE_KEY_SIZE]) -> X25519PrivateKey { - X25519PrivateKey { - secret_key: pk::curve25519::StaticSecret::from(*raw), + pub fn from_raw(raw: &[u8; X25519_PRIVATE_KEY_SIZE]) -> Result { + // see: https://docs.rs/x25519-dalek/2.0.0-pre.1/src/x25519_dalek/x25519.rs.html#197 + if raw[0] == raw[0] & 240 && + raw[31] == (raw[31] & 127) | 64 { + Ok(X25519PrivateKey { secret_key: pk::curve25519::StaticSecret::from(*raw) }) + } else { + Err(Error::KeyInvalid) } } @@ -482,7 +486,7 @@ impl X25519PrivateKey { } }; - Ok(X25519PrivateKey::from_raw(&private_key_data_raw)) + X25519PrivateKey::from_raw(&private_key_data_raw) } // security note: only ever sign messages the private key owner controls the contents of! diff --git a/tor-interface/tests/tor_crypto.rs b/tor-interface/tests/tor_crypto.rs index 54aed8fe5..49c01fd7a 100644 --- a/tor-interface/tests/tor_crypto.rs +++ b/tor-interface/tests/tor_crypto.rs @@ -114,7 +114,7 @@ fn test_x25519() -> Result<(), anyhow::Error> { // ensure we can convert from raw as expected assert_eq!( - &X25519PrivateKey::from_raw(&SECRET_RAW).to_base64(), + &X25519PrivateKey::from_raw(&SECRET_RAW)?.to_base64(), SECRET_BASE64 ); assert_eq!( From f35be448134e8062bed962dd833ec7c31a298836 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 28 Aug 2023 00:42:05 +0000 Subject: [PATCH 036/184] tor-interface: implmented fuzzing function for tor_interface::tor_crypto module --- tor-interface/fuzz/Cargo.toml | 2 +- .../fuzz/fuzz_targets/fuzz_crypto.rs | 157 +++++++++++++++++- 2 files changed, 156 insertions(+), 3 deletions(-) diff --git a/tor-interface/fuzz/Cargo.toml b/tor-interface/fuzz/Cargo.toml index 3140b811d..dbc71cc02 100644 --- a/tor-interface/fuzz/Cargo.toml +++ b/tor-interface/fuzz/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" cargo-fuzz = true [dependencies] -libfuzzer-sys = "0.4" +libfuzzer-sys = { version = "0.4.0", features = ["arbitrary-derive"] } [dependencies.tor-interface] path = ".." diff --git a/tor-interface/fuzz/fuzz_targets/fuzz_crypto.rs b/tor-interface/fuzz/fuzz_targets/fuzz_crypto.rs index 43a88c14f..978acdc6a 100644 --- a/tor-interface/fuzz/fuzz_targets/fuzz_crypto.rs +++ b/tor-interface/fuzz/fuzz_targets/fuzz_crypto.rs @@ -1,7 +1,160 @@ #![no_main] +// tor_interface +use tor_interface::tor_crypto::*; + +// fuzzing use libfuzzer_sys::fuzz_target; +use libfuzzer_sys::arbitrary; +use libfuzzer_sys::arbitrary::Arbitrary; + +#[derive(Arbitrary, Debug)] +struct CryptoData<'a> { + ed25519_public_raw: [u8; 32], + onion_service_id: &'a str, + x25519_public_raw: [u8; 32], + message_1: &'a [u8], + message_2: &'a [u8], + ed25519_private_raw_1: [u8; 64], + ed25519_private_raw_2: [u8; 64], + x25519_private_raw_1: [u8; 32], + x25519_private_raw_2: [u8; 32], +} + +fuzz_target!(|data: CryptoData| { + + // + // ed25519 tests + // + + // ensure random bytes don't break ed25519public from_raw + let _ = Ed25519PublicKey::from_raw(&data.ed25519_public_raw); + + // ensure random string doesn't break v3onionserviceid from_string + let _ = V3OnionServiceId::from_string(data.onion_service_id); + + // ensure random bytes don't break x25519public from_raw + let _ = X25519PublicKey::from_raw(&data.x25519_public_raw); + + // try to build key from raw binary blob, return early if invalid + if let Ok(ed25519_private_1) = Ed25519PrivateKey::from_raw(&data.ed25519_private_raw_1) { + // ensure key round-trips through keyblob representation + assert_eq!(Ed25519PrivateKey::from_key_blob(ed25519_private_1.to_key_blob().as_ref()).unwrap(), ed25519_private_1); + + // ensure key round-trips through raw bytes representation + match Ed25519PrivateKey::from_raw(&ed25519_private_1.to_bytes()) { + Ok(ed25519_private) => assert_eq!(ed25519_private, ed25519_private_1), + Err(err) => panic!("{:?}", err), + } + + // derive private keys public key + let ed25519_public_1 = Ed25519PublicKey::from_private_key(&ed25519_private_1); + + // compare onion service id derivation from public vs privat ekey + assert_eq!(V3OnionServiceId::from_private_key(&ed25519_private_1), V3OnionServiceId::from_public_key(&ed25519_public_1)); + let onion_service_id_1 = V3OnionServiceId::from_public_key(&ed25519_public_1); + // ensure service id round-trips through string representation + assert_eq!(V3OnionServiceId::from_string(&onion_service_id_1.to_string()).unwrap(), onion_service_id_1); + + // ensure public key round-trips through service id + assert_eq!(ed25519_public_1, Ed25519PublicKey::from_service_id(&V3OnionServiceId::from_public_key(&ed25519_public_1)).unwrap()); + + // ensure key round-trips through raw bytes representation + assert_eq!(ed25519_public_1, Ed25519PublicKey::from_raw(ed25519_public_1.as_bytes()).unwrap()); + + // sign and verify a message + let ed25519_signature_1 = ed25519_private_1.sign_message(data.message_1); + assert!(ed25519_signature_1.verify(data.message_1, &ed25519_public_1)); + // verify signature does not work for unrelated message + if data.message_1 != data.message_2 { + assert!(!ed25519_signature_1.verify(data.message_2, &ed25519_public_1)); + } + + // ensure we can't verfify another key's signature + if data.ed25519_private_raw_1 != data.ed25519_private_raw_2 { + // try to build key from raw binary blob, return early if invalid + if let Ok(ed25519_private_2) = Ed25519PrivateKey::from_raw(&data.ed25519_private_raw_2) { + + // ensure key round-trips through keyblob representation + assert_eq!(Ed25519PrivateKey::from_key_blob(ed25519_private_2.to_key_blob().as_ref()).unwrap(), ed25519_private_2); + + // ensure key round-trips through raw bytes representation + match Ed25519PrivateKey::from_raw(&ed25519_private_2.to_bytes()) { + Ok(ed25519_private) => assert_eq!(ed25519_private, ed25519_private_2), + Err(err) => panic!("{:?}", err), + } + + // derive private key's public key + let ed25519_public_2 = Ed25519PublicKey::from_private_key(&ed25519_private_2); + + // compare onion service id derivation from public vs privat ekey + assert_eq!(V3OnionServiceId::from_private_key(&ed25519_private_2), V3OnionServiceId::from_public_key(&ed25519_public_2)); + let onion_service_id_2 = V3OnionServiceId::from_public_key(&ed25519_public_2); + // ensure service id round-trips through string representation + assert_eq!(V3OnionServiceId::from_string(&onion_service_id_2.to_string()).unwrap(), onion_service_id_2); + + // ensure public key round-trips through service id + assert_eq!(ed25519_public_2, Ed25519PublicKey::from_service_id(&V3OnionServiceId::from_public_key(&ed25519_public_2)).unwrap()); + + // ensure key round-trips through raw bytes representation + assert_eq!(ed25519_public_2, Ed25519PublicKey::from_raw(ed25519_public_2.as_bytes()).unwrap()); + + + // sign and verify a message + let ed25519_signature_2 = ed25519_private_2.sign_message(data.message_2); + assert!(ed25519_signature_2.verify(data.message_2, &ed25519_public_2)); + + // verify signature does not work for unrelated message + if data.message_1 != data.message_2 { + assert!(!ed25519_signature_2.verify(data.message_1, &ed25519_public_2)); + } + + // verify we cannot verify signatures using the wrong public keys + if ed25519_public_1 != ed25519_public_2 { + assert!(!ed25519_signature_1.verify(data.message_1, &ed25519_public_2)); + assert!(!ed25519_signature_2.verify(data.message_2, &ed25519_public_1)); + } + } + } + } + + // + // x25519 tests + // + + if let Ok(x25519_private_1) = X25519PrivateKey::from_raw(&data.x25519_private_raw_1) { + // ensure round-trips through byte representation + assert_eq!(x25519_private_1, X25519PrivateKey::from_raw(&x25519_private_1.to_bytes()).unwrap()); + assert_eq!(data.x25519_private_raw_1, x25519_private_1.to_bytes()); + // ensure round-trips through base64 representation + assert_eq!(x25519_private_1, X25519PrivateKey::from_base64(&x25519_private_1.to_base64()).unwrap()); + + // ensure converts to e25519 without issue + let _ = Ed25519PrivateKey::from_private_x25519(&x25519_private_1).unwrap(); + + let x25519_public_1 = X25519PublicKey::from_private_key(&x25519_private_1); + // ensure round-trips through byte representation + assert_eq!(x25519_public_1, X25519PublicKey::from_raw(x25519_public_1.as_bytes())); + // ensure round-trips through base32 representation + assert_eq!(x25519_public_1, X25519PublicKey::from_base32(&x25519_public_1.to_base32()).unwrap()); + + if let Ok(x25519_private_2) = X25519PrivateKey::from_raw(&data.x25519_private_raw_2) { + // ensure round-trips through byte representation + assert_eq!(x25519_private_2, X25519PrivateKey::from_raw(&x25519_private_2.to_bytes()).unwrap()); + assert_eq!(data.x25519_private_raw_2, x25519_private_2.to_bytes()); + // ensure round-trips through base64 representation + assert_eq!(x25519_private_2, X25519PrivateKey::from_base64(&x25519_private_2.to_base64()).unwrap()); + + // ensure converts to e25519 without issue + let _ = Ed25519PrivateKey::from_private_x25519(&x25519_private_2).unwrap(); + + let x25519_public_2 = X25519PublicKey::from_private_key(&x25519_private_2); + // ensure round-trips through byte representation + assert_eq!(x25519_public_2, X25519PublicKey::from_raw(x25519_public_2.as_bytes())); + // ensure round-trips through base32 representation + assert_eq!(x25519_public_2, X25519PublicKey::from_base32(&x25519_public_2.to_base32()).unwrap()); + } + } + -fuzz_target!(|data: &[u8]| { - // fuzzed code goes here }); From 95c2dd8fe4c285419e7719b79e78ad7196fc9091 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 17 Sep 2023 22:31:08 +0000 Subject: [PATCH 037/184] tor-interface: implement Debug for TorEvent enum --- tor-interface/src/tor_provider.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index b9c582d01..34f34a1e6 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -52,6 +52,7 @@ pub enum TargetAddr { OnionService(OnionAddr), } +#[derive(Debug)] pub enum TorEvent { BootstrapStatus { progress: u32, From 71c2fce7ae7b77fd762265b8069d6ed3c498a71f Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 18 Sep 2023 00:11:44 +0000 Subject: [PATCH 038/184] honk-rpc, tor-interface, gosling, gosling-ffi: set all crate versions to 0.1.0 --- tor-interface/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 19d51511a..73cb1c9db 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tor-interface" authors = ["Richard Pospesel "] -version = "0.0.1" +version = "0.1.0" rust-version = "1.66" edition = "2021" From b09139c25a63e36237087a68b12172071077b2fd Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 18 Nov 2023 23:45:39 +0000 Subject: [PATCH 039/184] tor-interface: fix formatting errors --- tor-interface/src/tor_crypto.rs | 14 +++++++------ tor-interface/tests/tor_crypto.rs | 35 +++++++++++++------------------ 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index aa7a8b533..8a9960aab 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -170,10 +170,11 @@ impl Ed25519PrivateKey { pub fn from_raw(raw: &[u8; ED25519_PRIVATE_KEY_SIZE]) -> Result { // Verify the provided bytes have bits set correctly // see: https://gitlab.torproject.org/tpo/core/arti/-/issues/1021 - if raw[0] == raw[0] & 248 && - raw[31] == (raw[31] & 63) | 64 { + if raw[0] == raw[0] & 248 && raw[31] == (raw[31] & 63) | 64 { match pk::ed25519::ExpandedSecretKey::from_bytes(raw) { - Ok(expanded_secret_key) => Ok(Ed25519PrivateKey{ expanded_secret_key }), + Ok(expanded_secret_key) => Ok(Ed25519PrivateKey { + expanded_secret_key, + }), Err(_) => Err(Error::KeyInvalid), } } else { @@ -447,9 +448,10 @@ impl X25519PrivateKey { pub fn from_raw(raw: &[u8; X25519_PRIVATE_KEY_SIZE]) -> Result { // see: https://docs.rs/x25519-dalek/2.0.0-pre.1/src/x25519_dalek/x25519.rs.html#197 - if raw[0] == raw[0] & 240 && - raw[31] == (raw[31] & 127) | 64 { - Ok(X25519PrivateKey { secret_key: pk::curve25519::StaticSecret::from(*raw) }) + if raw[0] == raw[0] & 240 && raw[31] == (raw[31] & 127) | 64 { + Ok(X25519PrivateKey { + secret_key: pk::curve25519::StaticSecret::from(*raw), + }) } else { Err(Error::KeyInvalid) } diff --git a/tor-interface/tests/tor_crypto.rs b/tor-interface/tests/tor_crypto.rs index 49c01fd7a..b85a573af 100644 --- a/tor-interface/tests/tor_crypto.rs +++ b/tor-interface/tests/tor_crypto.rs @@ -5,20 +5,17 @@ use tor_interface::tor_crypto::*; fn test_ed25519() -> Result<(), anyhow::Error> { let private_key_blob = "ED25519-V3:YE3GZtDmc+izGijWKgeVRabbXqK456JKKGONDBhV+kPBVKa2mHVQqnRTVuFXe3inU3YW6qvc7glYEwe9rK0LhQ=="; let private_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = [ - 0x60u8, 0x4du8, 0xc6u8, 0x66u8, 0xd0u8, 0xe6u8, 0x73u8, 0xe8u8, - 0xb3u8, 0x1au8, 0x28u8, 0xd6u8, 0x2au8, 0x07u8, 0x95u8, 0x45u8, - 0xa6u8, 0xdbu8, 0x5eu8, 0xa2u8, 0xb8u8, 0xe7u8, 0xa2u8, 0x4au8, - 0x28u8, 0x63u8, 0x8du8, 0x0cu8, 0x18u8, 0x55u8, 0xfau8, 0x43u8, - 0xc1u8, 0x54u8, 0xa6u8, 0xb6u8, 0x98u8, 0x75u8, 0x50u8, 0xaau8, - 0x74u8, 0x53u8, 0x56u8, 0xe1u8, 0x57u8, 0x7bu8, 0x78u8, 0xa7u8, - 0x53u8, 0x76u8, 0x16u8, 0xeau8, 0xabu8, 0xdcu8, 0xeeu8, 0x09u8, - 0x58u8, 0x13u8, 0x07u8, 0xbdu8, 0xacu8, 0xadu8, 0x0bu8, 0x85u8, + 0x60u8, 0x4du8, 0xc6u8, 0x66u8, 0xd0u8, 0xe6u8, 0x73u8, 0xe8u8, 0xb3u8, 0x1au8, 0x28u8, + 0xd6u8, 0x2au8, 0x07u8, 0x95u8, 0x45u8, 0xa6u8, 0xdbu8, 0x5eu8, 0xa2u8, 0xb8u8, 0xe7u8, + 0xa2u8, 0x4au8, 0x28u8, 0x63u8, 0x8du8, 0x0cu8, 0x18u8, 0x55u8, 0xfau8, 0x43u8, 0xc1u8, + 0x54u8, 0xa6u8, 0xb6u8, 0x98u8, 0x75u8, 0x50u8, 0xaau8, 0x74u8, 0x53u8, 0x56u8, 0xe1u8, + 0x57u8, 0x7bu8, 0x78u8, 0xa7u8, 0x53u8, 0x76u8, 0x16u8, 0xeau8, 0xabu8, 0xdcu8, 0xeeu8, + 0x09u8, 0x58u8, 0x13u8, 0x07u8, 0xbdu8, 0xacu8, 0xadu8, 0x0bu8, 0x85u8, ]; let public_raw: [u8; ED25519_PUBLIC_KEY_SIZE] = [ - 0xf2u8, 0xfdu8, 0xa2u8, 0xdbu8, 0xf3u8, 0x80u8, 0xa6u8, 0xbau8, - 0x74u8, 0xa4u8, 0x90u8, 0xe1u8, 0x45u8, 0x55u8, 0xeeu8, 0xb9u8, - 0x32u8, 0xa0u8, 0x5cu8, 0x39u8, 0x5au8, 0xe2u8, 0x02u8, 0x83u8, - 0x55u8, 0x27u8, 0x89u8, 0x6au8, 0x1fu8, 0x2fu8, 0x3du8, 0xc5u8, + 0xf2u8, 0xfdu8, 0xa2u8, 0xdbu8, 0xf3u8, 0x80u8, 0xa6u8, 0xbau8, 0x74u8, 0xa4u8, 0x90u8, + 0xe1u8, 0x45u8, 0x55u8, 0xeeu8, 0xb9u8, 0x32u8, 0xa0u8, 0x5cu8, 0x39u8, 0x5au8, 0xe2u8, + 0x02u8, 0x83u8, 0x55u8, 0x27u8, 0x89u8, 0x6au8, 0x1fu8, 0x2fu8, 0x3du8, 0xc5u8, ]; let public_base32 = "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCQ===="; let service_id_string = "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd"; @@ -30,14 +27,12 @@ fn test_ed25519() -> Result<(), anyhow::Error> { *ptr = i as u8; } let signature_raw: [u8; ED25519_SIGNATURE_SIZE] = [ - 0xa6u8, 0xd6u8, 0xc6u8, 0x1au8, 0x03u8, 0xbcu8, 0x43u8, 0x6fu8, - 0x38u8, 0x53u8, 0x94u8, 0xcdu8, 0xdcu8, 0x86u8, 0x0au8, 0x88u8, - 0x64u8, 0x43u8, 0x1du8, 0x18u8, 0x84u8, 0x30u8, 0x2fu8, 0xcdu8, - 0xa6u8, 0x79u8, 0xcau8, 0x87u8, 0xd0u8, 0x29u8, 0xe7u8, 0x2bu8, - 0x32u8, 0x9bu8, 0xa2u8, 0xa4u8, 0x3cu8, 0x74u8, 0x6au8, 0x08u8, - 0x67u8, 0x0eu8, 0x63u8, 0x60u8, 0xcbu8, 0x46u8, 0x22u8, 0x55u8, - 0x43u8, 0x5bu8, 0x84u8, 0x68u8, 0x0fu8, 0x47u8, 0xceu8, 0x6cu8, - 0xd2u8, 0xb8u8, 0xebu8, 0xfeu8, 0xf6u8, 0x9eu8, 0x97u8, 0x0au8, + 0xa6u8, 0xd6u8, 0xc6u8, 0x1au8, 0x03u8, 0xbcu8, 0x43u8, 0x6fu8, 0x38u8, 0x53u8, 0x94u8, + 0xcdu8, 0xdcu8, 0x86u8, 0x0au8, 0x88u8, 0x64u8, 0x43u8, 0x1du8, 0x18u8, 0x84u8, 0x30u8, + 0x2fu8, 0xcdu8, 0xa6u8, 0x79u8, 0xcau8, 0x87u8, 0xd0u8, 0x29u8, 0xe7u8, 0x2bu8, 0x32u8, + 0x9bu8, 0xa2u8, 0xa4u8, 0x3cu8, 0x74u8, 0x6au8, 0x08u8, 0x67u8, 0x0eu8, 0x63u8, 0x60u8, + 0xcbu8, 0x46u8, 0x22u8, 0x55u8, 0x43u8, 0x5bu8, 0x84u8, 0x68u8, 0x0fu8, 0x47u8, 0xceu8, + 0x6cu8, 0xd2u8, 0xb8u8, 0xebu8, 0xfeu8, 0xf6u8, 0x9eu8, 0x97u8, 0x0au8, ]; // test the golden path first From f600194ebc4a1092ca6defea29898e070ea20ae5 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 3 Dec 2023 22:14:30 +0000 Subject: [PATCH 040/184] tor-interface: updated Cargo.toml file with more metadata --- tor-interface/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 73cb1c9db..08b6ba503 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -4,6 +4,10 @@ authors = ["Richard Pospesel "] version = "0.1.0" rust-version = "1.66" edition = "2021" +license = "BSD-3-Clause" +description = "A library providing a Rust interface to interact with the legacy tor daemon" +keywords = ["tor", "anonymity"] +repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] data-encoding = "2.3.2" From 085f185c495b81c146a731c4cf6de14dd48cce7f Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 18 Dec 2023 21:52:54 +0000 Subject: [PATCH 041/184] tor-interface: replaced rust-crypto crate usage with sha1 and sha3 --- tor-interface/Cargo.toml | 3 ++- tor-interface/src/legacy_tor_process.rs | 11 ++++------- tor-interface/src/tor_crypto.rs | 17 ++++++----------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 08b6ba503..dab077678 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -15,7 +15,8 @@ data-encoding-macro = "0.1.12" rand = "0.8.5" rand_core = "0.6.3" regex = "1.5.5" -rust-crypto = "^0.2" +sha1 = "0.10.6" +sha3 = "0.10.8" signature = "1.5.0" socks = "0.3.4" tor-llcrypto = { version = "0.2.0", features = ["relay"] } diff --git a/tor-interface/src/legacy_tor_process.rs b/tor-interface/src/legacy_tor_process.rs index 70febfe80..351c67498 100644 --- a/tor-interface/src/legacy_tor_process.rs +++ b/tor-interface/src/legacy_tor_process.rs @@ -14,11 +14,10 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; // extern crates -use crypto::digest::Digest; -use crypto::sha1::Sha1; use data_encoding::HEXUPPER; use rand::rngs::OsRng; use rand::RngCore; +use sha1::{Digest, Sha1}; // internal crates use crate::tor_crypto::generate_password; @@ -138,17 +137,15 @@ impl LegacyTorProcess { let mut count = COUNT; while count > 0 { if count > input_len { - sha1.input(input); + sha1.update(input); count -= input_len; } else { - sha1.input(&input[0..count]); + sha1.update(&input[0..count]); break; } } - const SHA1_BYTES: usize = 160 / 8; - let mut key = [0u8; SHA1_BYTES]; - sha1.result(key.as_mut_slice()); + let key = sha1.finalize(); let mut hash = "16:".to_string(); HEXUPPER.encode_append(salt, &mut hash); diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 8a9960aab..a5e599d8d 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -4,13 +4,12 @@ use std::iter; use std::str; // extern crates -use crypto::digest::Digest; -use crypto::sha3::Sha3; use data_encoding::{BASE32, BASE32_NOPAD, BASE64}; use data_encoding_macro::new_encoding; use rand::distributions::Alphanumeric; use rand::rngs::OsRng; use rand::Rng; +use sha3::{Digest, Sha3_256}; use signature::Verifier; use tor_llcrypto::pk::keymanip::*; use tor_llcrypto::util::rand_compat::RngCompatExt; @@ -590,18 +589,14 @@ impl V3OnionServiceId { fn calc_truncated_checksum( public_key: &[u8; ED25519_PUBLIC_KEY_SIZE], ) -> [u8; TRUNCATED_CHECKSUM_SIZE] { - // space for full checksum - const SHA256_BYTES: usize = 256 / 8; - let mut hash_bytes = [0u8; SHA256_BYTES]; - let mut hasher = Sha3::sha3_256(); - assert_eq!(SHA256_BYTES, hasher.output_bytes()); + let mut hasher = Sha3_256::new(); // calculate checksum - hasher.input(b".onion checksum"); - hasher.input(public_key); - hasher.input(&[0x03u8]); - hasher.result(&mut hash_bytes); + hasher.update(b".onion checksum"); + hasher.update(public_key); + hasher.update(&[0x03u8]); + let hash_bytes = hasher.finalize(); [hash_bytes[0], hash_bytes[1]] } From 3d3cedf8dd76191daab908395794a925e6f4b79d Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 18 Dec 2023 22:00:45 +0000 Subject: [PATCH 042/184] honk-rpc, tor-ingterface: fix clippy errors --- tor-interface/src/tor_crypto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index a5e599d8d..709344b56 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -595,7 +595,7 @@ impl V3OnionServiceId { // calculate checksum hasher.update(b".onion checksum"); hasher.update(public_key); - hasher.update(&[0x03u8]); + hasher.update([0x03u8]); let hash_bytes = hasher.finalize(); [hash_bytes[0], hash_bytes[1]] From 3870f5a2e07c723cb32f6880d91ed15914ca16dc Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 18 Dec 2023 22:04:03 +0000 Subject: [PATCH 043/184] tor-interface, gosling-ffi, test: fixed various formatting errors --- tor-interface/src/tor_crypto.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 709344b56..2ddeae411 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -589,7 +589,6 @@ impl V3OnionServiceId { fn calc_truncated_checksum( public_key: &[u8; ED25519_PUBLIC_KEY_SIZE], ) -> [u8; TRUNCATED_CHECKSUM_SIZE] { - let mut hasher = Sha3_256::new(); // calculate checksum From 8caaa7810ccad5d7e1feb8be79ae8a621d25e648 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 23 Dec 2023 18:14:05 +0000 Subject: [PATCH 044/184] tor-interface, gosling-ffi: renamed some size constants to be mutally consistent; updated dependants and tests --- tor-interface/src/tor_crypto.rs | 50 ++++++++++++++++----------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 2ddeae411..ba39cc21c 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -35,20 +35,20 @@ pub const ED25519_PUBLIC_KEY_SIZE: usize = 32; /// cbindgen:ignore pub const ED25519_SIGNATURE_SIZE: usize = 64; /// The number of bytes needed to store onion service id as an ASCII c-string (not including null-terminator) -pub const V3_ONION_SERVICE_ID_LENGTH: usize = 56; +pub const V3_ONION_SERVICE_ID_STRING_LENGTH: usize = 56; /// The number of bytes needed to store onion service id as an ASCII c-string (including null-terminator) -pub const V3_ONION_SERVICE_ID_SIZE: usize = V3_ONION_SERVICE_ID_LENGTH + 1; +pub const V3_ONION_SERVICE_ID_STRING_SIZE: usize = V3_ONION_SERVICE_ID_STRING_LENGTH + 1; /// The number of bytes needed to store base64 encoded ed25519 private key as an ASCII c-string (not including null-terminator) pub const ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH: usize = 88; /// key klob header string -const ED25519_PRIVATE_KEYBLOB_HEADER: &str = "ED25519-V3:"; +const ED25519_PRIVATE_KEY_KEYBLOB_HEADER: &str = "ED25519-V3:"; /// The number of bytes needed to store the keyblob header -pub const ED25519_PRIVATE_KEYBLOB_HEADER_LENGTH: usize = 11; +pub const ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH: usize = 11; /// The number of bytes needed to store ed25519 private keyblob as an ASCII c-string (not including a null terminator) -pub const ED25519_PRIVATE_KEYBLOB_LENGTH: usize = - ED25519_PRIVATE_KEYBLOB_HEADER_LENGTH + ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH; +pub const ED25519_PRIVATE_KEY_KEYBLOB_LENGTH: usize = + ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH + ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH; /// The number of bytes needed to store ed25519 private keyblob as an ASCII c-string (including a null terminator) -pub const ED25519_PRIVATE_KEYBLOB_SIZE: usize = ED25519_PRIVATE_KEYBLOB_LENGTH + 1; +pub const ED25519_PRIVATE_KEY_KEYBLOB_SIZE: usize = ED25519_PRIVATE_KEY_KEYBLOB_LENGTH + 1; // number of bytes in an onion service id after base32 decode const V3_ONION_SERVICE_ID_RAW_SIZE: usize = 35; // byte index of the start of the public key checksum @@ -64,13 +64,13 @@ pub const X25519_PRIVATE_KEY_SIZE: usize = 32; /// cbindgen:ignore pub const X25519_PUBLIC_KEY_SIZE: usize = 32; /// The number of bytes needed to store base64 encoded x25519 private key as an ASCII c-string (not including null-terminator) -pub const X25519_PRIVATE_KEYBLOB_BASE64_LENGTH: usize = 44; +pub const X25519_PRIVATE_KEY_BASE64_LENGTH: usize = 44; /// The number of bytes needed to store base64 encoded x25519 private key as an ASCII c-string (including a null terminator) -pub const X25519_PRIVATE_KEYBLOB_BASE64_SIZE: usize = X25519_PRIVATE_KEYBLOB_BASE64_LENGTH + 1; +pub const X25519_PRIVATE_KEY_BASE64_SIZE: usize = X25519_PRIVATE_KEY_BASE64_LENGTH + 1; /// The number of bytes needed to store base32 encoded x25519 public key as an ASCII c-string (not including null-terminator) -pub const X25519_PUBLIC_KEYBLOB_BASE32_LENGTH: usize = 52; +pub const X25519_PUBLIC_KEY_BASE32_LENGTH: usize = 52; /// The number of bytes needed to store bsae32 encoded x25519 public key as an ASCII c-string (including a null terminator) -pub const X25519_PUBLIC_KEYBLOB_BASE32_SIZE: usize = X25519_PUBLIC_KEYBLOB_BASE32_LENGTH + 1; +pub const X25519_PUBLIC_KEY_BASE32_SIZE: usize = X25519_PUBLIC_KEY_BASE32_LENGTH + 1; const ONION_BASE32: data_encoding::Encoding = new_encoding! { symbols: "abcdefghijklmnopqrstuvwxyz234567", @@ -118,7 +118,7 @@ pub struct X25519PublicKey { #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct V3OnionServiceId { - data: [u8; V3_ONION_SERVICE_ID_LENGTH], + data: [u8; V3_ONION_SERVICE_ID_STRING_LENGTH], } #[derive(Clone, Copy)] @@ -182,22 +182,22 @@ impl Ed25519PrivateKey { } pub fn from_key_blob(key_blob: &str) -> Result { - if key_blob.len() != ED25519_PRIVATE_KEYBLOB_LENGTH { + if key_blob.len() != ED25519_PRIVATE_KEY_KEYBLOB_LENGTH { return Err(Error::ParseError(format!( "expects string of length '{}'; received string with length '{}'", - ED25519_PRIVATE_KEYBLOB_LENGTH, + ED25519_PRIVATE_KEY_KEYBLOB_LENGTH, key_blob.len() ))); } - if !key_blob.starts_with(ED25519_PRIVATE_KEYBLOB_HEADER) { + if !key_blob.starts_with(ED25519_PRIVATE_KEY_KEYBLOB_HEADER) { return Err(Error::ParseError(format!( "expects string that begins with '{}'; received '{}'", - &ED25519_PRIVATE_KEYBLOB_HEADER, &key_blob + &ED25519_PRIVATE_KEY_KEYBLOB_HEADER, &key_blob ))); } - let base64_key: &str = &key_blob[ED25519_PRIVATE_KEYBLOB_HEADER.len()..]; + let base64_key: &str = &key_blob[ED25519_PRIVATE_KEY_KEYBLOB_HEADER.len()..]; let private_key_data = match BASE64.decode(base64_key.as_bytes()) { Ok(private_key_data) => private_key_data, Err(_) => { @@ -251,7 +251,7 @@ impl Ed25519PrivateKey { } pub fn to_key_blob(&self) -> String { - let mut key_blob = ED25519_PRIVATE_KEYBLOB_HEADER.to_string(); + let mut key_blob = ED25519_PRIVATE_KEY_KEYBLOB_HEADER.to_string(); key_blob.push_str(&BASE64.encode(&self.expanded_secret_key.to_bytes())); key_blob @@ -458,10 +458,10 @@ impl X25519PrivateKey { // a base64 encoded keyblob pub fn from_base64(base64: &str) -> Result { - if base64.len() != X25519_PRIVATE_KEYBLOB_BASE64_LENGTH { + if base64.len() != X25519_PRIVATE_KEY_BASE64_LENGTH { return Err(Error::ParseError(format!( "expects string of length '{}'; received string with length '{}'", - X25519_PRIVATE_KEYBLOB_BASE64_LENGTH, + X25519_PRIVATE_KEY_BASE64_LENGTH, base64.len() ))); } @@ -535,10 +535,10 @@ impl X25519PublicKey { } pub fn from_base32(base32: &str) -> Result { - if base32.len() != X25519_PUBLIC_KEYBLOB_BASE32_LENGTH { + if base32.len() != X25519_PUBLIC_KEY_BASE32_LENGTH { return Err(Error::ParseError(format!( "expects string of length '{}'; received '{}' with length '{}'", - X25519_PUBLIC_KEYBLOB_BASE32_LENGTH, + X25519_PUBLIC_KEY_BASE32_LENGTH, base32, base32.len() ))); @@ -621,7 +621,7 @@ impl V3OnionServiceId { raw_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET + 1] = truncated_checksum[1]; raw_service_id[V3_ONION_SERVICE_ID_VERSION_OFFSET] = 0x03u8; - let mut service_id = [0u8; V3_ONION_SERVICE_ID_LENGTH]; + let mut service_id = [0u8; V3_ONION_SERVICE_ID_STRING_LENGTH]; // panics on wrong buffer size, but given our constant buffer sizes should be fine ONION_BASE32.encode_mut(&raw_service_id, &mut service_id); @@ -633,7 +633,7 @@ impl V3OnionServiceId { } pub fn is_valid(service_id: &str) -> bool { - if service_id.len() != V3_ONION_SERVICE_ID_LENGTH { + if service_id.len() != V3_ONION_SERVICE_ID_STRING_LENGTH { return false; } @@ -665,7 +665,7 @@ impl V3OnionServiceId { } } - pub fn as_bytes(&self) -> &[u8; V3_ONION_SERVICE_ID_LENGTH] { + pub fn as_bytes(&self) -> &[u8; V3_ONION_SERVICE_ID_STRING_LENGTH] { &self.data } } From b0307e76e2f25e3e62bf2bf96cf1f236a16b751a Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 23 Dec 2023 18:15:53 +0000 Subject: [PATCH 045/184] tor-interface, gosling-ffi: bumped crate version due to api-breaking change --- tor-interface/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index dab077678..e14598ce7 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tor-interface" authors = ["Richard Pospesel "] -version = "0.1.0" +version = "0.2.0" rust-version = "1.66" edition = "2021" license = "BSD-3-Clause" From 81446458f5b9cf89b84d36384f51440ec0c8afd0 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Thu, 28 Dec 2023 13:31:23 +0000 Subject: [PATCH 046/184] build: updated Makefile and verified each of our make targets --- tor-interface/CMakeLists.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 0a94da8f0..09509a3c4 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -14,18 +14,18 @@ set(tor_interface_sources # add_custom_target(tor_interface_target DEPENDS ${tor_interface_sources} - COMMAND CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} cargo build ${CARGO_FLAGS} + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} cargo build ${CARGO_FLAGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) # cargo test add_custom_target(tor_interface_cargo_test - COMMAND RUSTFLAGS=${RUSTFLAGS} CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) # cargo test (offline) add_custom_target(tor_interface_cargo_test_offline - COMMAND RUSTFLAGS=${RUSTFLAGS} CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} --features offline-test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} --features offline-test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) @@ -33,6 +33,6 @@ add_custom_target(tor_interface_cargo_test_offline # fuzz target # add_custom_target(tor_interface_cargo_fuzz_crypto - COMMAND RUSTFLAGS=${RUSTFLAGS} CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUST_BACKTRACE=full cargo fuzz run fuzz_crypto ${CARGO_FLAGS} + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo fuzz run fuzz_crypto ${CARGO_FLAGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) From 616834beaf07f357f83f68f2696bd7033c9ae7fd Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 29 Dec 2023 12:13:41 +0000 Subject: [PATCH 047/184] tor-interface: removed control-port read/write printlns --- tor-interface/src/legacy_tor_control_stream.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tor-interface/src/legacy_tor_control_stream.rs b/tor-interface/src/legacy_tor_control_stream.rs index 518b6430b..c63327a33 100644 --- a/tor-interface/src/legacy_tor_control_stream.rs +++ b/tor-interface/src/legacy_tor_control_stream.rs @@ -242,7 +242,6 @@ impl LegacyControlStream { // strip the redundant status code from start of lines for line in reply_lines.iter_mut() { - println!(">>> {}", line); if line.starts_with(&status_code_string) { *line = line[4..].to_string(); } @@ -255,7 +254,6 @@ impl LegacyControlStream { } pub fn write(&mut self, cmd: &str) -> Result<(), Error> { - println!("<<< {}", cmd); if let Err(err) = write!(self.stream, "{}\r\n", cmd) { self.closed_by_remote = true; return Err(Error::WriteFailed(err)); From 8cb858c336f2580af94f335f9a56dccda041419e Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 26 Jan 2024 17:28:38 +0000 Subject: [PATCH 048/184] build: update cargo+cmake integration, properly setup targets and dependencies --- tor-interface/CMakeLists.txt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 09509a3c4..88b39c675 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -1,7 +1,8 @@ set(tor_interface_sources + Cargo.toml src/legacy_tor_client.rs - src/legacy_tor_control_stream.rs src/legacy_tor_controller.rs + src/legacy_tor_control_stream.rs src/legacy_tor_process.rs src/legacy_tor_version.rs src/lib.rs @@ -9,13 +10,20 @@ set(tor_interface_sources src/tor_crypto.rs src/tor_provider.rs) +set(tor_interface_outputs + ${CARGO_TARGET_DIR}/${CARGO_PROFILE}/libtor_interface.d + ${CARGO_TARGET_DIR}/${CARGO_PROFILE}/libtor_interface.rlib) + # # build target # -add_custom_target(tor_interface_target +add_custom_command( DEPENDS ${tor_interface_sources} + OUTPUT ${tor_interface_outputs} COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} cargo build ${CARGO_FLAGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) +add_custom_target(tor_interface_target + DEPENDS ${tor_interface_outputs}) # cargo test add_custom_target(tor_interface_cargo_test From e5c16dc320f1cf6c0dc358a036c6073f90cff384 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 17 Feb 2024 13:32:25 +0000 Subject: [PATCH 049/184] build: move dependency handling of build and test targets into cmake targets --- tor-interface/CMakeLists.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 88b39c675..e9b30ba8b 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -25,17 +25,19 @@ add_custom_command( add_custom_target(tor_interface_target DEPENDS ${tor_interface_outputs}) -# cargo test -add_custom_target(tor_interface_cargo_test +# cargo test target +add_custom_target(tor_interface_cargo_test_target COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) +add_dependencies(test_target tor_interface_cargo_test_target) -# cargo test (offline) -add_custom_target(tor_interface_cargo_test_offline +# cargo test (offline) target +add_custom_target(tor_interface_cargo_test_offline_target COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} --features offline-test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) +add_dependencies(test_offline_target tor_interface_cargo_test_offline_target) # # fuzz target From 243c7b419608b905c7112f1e1175ef72daa600b8 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 19 Feb 2024 14:57:01 +0000 Subject: [PATCH 050/184] build: reduced required rustc version to 1.63 (from 1.66) --- tor-interface/Cargo.toml | 34 +++++++------- tor-interface/fuzz/Cargo.toml | 3 +- tor-interface/src/mock_tor_client.rs | 70 +++++++++++++++++----------- 3 files changed, 63 insertions(+), 44 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index e14598ce7..44ca759c3 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "tor-interface" authors = ["Richard Pospesel "] -version = "0.2.0" -rust-version = "1.66" +version = "0.2.1" +rust-version = "1.63" edition = "2021" license = "BSD-3-Clause" description = "A library providing a Rust interface to interact with the legacy tor daemon" @@ -10,22 +10,24 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -data-encoding = "2.3.2" -data-encoding-macro = "0.1.12" -rand = "0.8.5" -rand_core = "0.6.3" -regex = "1.5.5" -sha1 = "0.10.6" -sha3 = "0.10.8" -signature = "1.5.0" -socks = "0.3.4" -tor-llcrypto = { version = "0.2.0", features = ["relay"] } -thiserror = "1.0" +data-encoding = "^2" +data-encoding-macro = "^0.1" +rand = "^0.8" +rand_core = "^0.6" +regex = ">= 1.9, <= 1.9.6" +sha1 = "^0.10" +sha3 = "^0.10" +signature = "^1.5" +socks = "^0.3" +thiserror = "^1" +time-macros = ">= 0.2, <= 0.2.8" +tor-llcrypto = { version = ">= 0.3, <= 0.4.4", features = ["relay"] } [dev-dependencies] -anyhow = "1.0" -serial_test = "0.9.0" -which = "4.3.0" +anyhow = "^1" +dashmap = ">= 5, <= 5.4.0" +serial_test = "0.9.*" +which = ">= 4.4.2, <= 5.0.0" [features] offline-test = [] diff --git a/tor-interface/fuzz/Cargo.toml b/tor-interface/fuzz/Cargo.toml index dbc71cc02..0960929fb 100644 --- a/tor-interface/fuzz/Cargo.toml +++ b/tor-interface/fuzz/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "tor-interface-fuzz" version = "0.0.0" +rust-version = "1.63" publish = false edition = "2021" @@ -8,7 +9,7 @@ edition = "2021" cargo-fuzz = true [dependencies] -libfuzzer-sys = { version = "0.4.0", features = ["arbitrary-derive"] } +libfuzzer-sys = { version = "^0.4", features = ["arbitrary-derive"] } [dependencies.tor-interface] path = ".." diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index 88e9e9e32..7bc33d021 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -21,7 +21,7 @@ pub enum Error { OnionServiceNotFound(OnionAddr), #[error("onion service not published: {}", .0)] - OnionServiceNoPublished(OnionAddr), + OnionServiceNotPublished(OnionAddr), #[error("onion service requires onion auth")] OnionServiceRequiresOnionAuth(), @@ -77,13 +77,13 @@ impl Drop for MockOnionListener { } struct MockTorNetwork { - onion_services: BTreeMap, SocketAddr)>, + onion_services: Option, SocketAddr)>>, } impl MockTorNetwork { const fn new() -> MockTorNetwork { MockTorNetwork { - onion_services: BTreeMap::new(), + onion_services: None, } } @@ -94,29 +94,35 @@ impl MockTorNetwork { client_auth: Option<&X25519PublicKey>, ) -> Result { let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port)); - if let Some((client_auth_keys, socket_addr)) = self.onion_services.get(&onion_addr) { - match (client_auth_keys.len(), client_auth) { - (0, None) => (), - (_, None) => return Err(Error::OnionServiceRequiresOnionAuth()), - (0, Some(_)) => return Err(Error::OnionServiceAuthInvalid()), - (_, Some(client_auth)) => { - if !client_auth_keys.contains(client_auth) { - return Err(Error::OnionServiceAuthInvalid()); + + match &mut self.onion_services { + Some(onion_services) => { + if let Some((client_auth_keys, socket_addr)) = onion_services.get(&onion_addr) { + match (client_auth_keys.len(), client_auth) { + (0, None) => (), + (_, None) => return Err(Error::OnionServiceRequiresOnionAuth()), + (0, Some(_)) => return Err(Error::OnionServiceAuthInvalid()), + (_, Some(client_auth)) => { + if !client_auth_keys.contains(client_auth) { + return Err(Error::OnionServiceAuthInvalid()); + } + } } - } - } - if let Ok(stream) = TcpStream::connect(socket_addr) { - Ok(OnionStream { - stream, - local_addr: None, - peer_addr: Some(TargetAddr::OnionService(onion_addr)), - }) - } else { - Err(Error::OnionServiceNotFound(onion_addr)) - } - } else { - Err(Error::OnionServiceNoPublished(onion_addr)) + if let Ok(stream) = TcpStream::connect(socket_addr) { + Ok(OnionStream { + stream, + local_addr: None, + peer_addr: Some(TargetAddr::OnionService(onion_addr)), + }) + } else { + Err(Error::OnionServiceNotFound(onion_addr)) + } + } else { + Err(Error::OnionServiceNotPublished(onion_addr)) + } + }, + None => Err(Error::OnionServiceNotPublished(onion_addr)) } } @@ -128,12 +134,22 @@ impl MockTorNetwork { address: SocketAddr, ) { let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id, virt_port)); - self.onion_services - .insert(onion_addr, (client_auth_keys, address)); + match &mut self.onion_services { + Some(onion_services) => { + onion_services.insert(onion_addr, (client_auth_keys, address)); + }, + None => { + let mut onion_services = BTreeMap::new(); + onion_services.insert(onion_addr, (client_auth_keys, address)); + self.onion_services = Some(onion_services); + } + } } fn stop_onion(&mut self, onion_addr: &OnionAddr) { - self.onion_services.remove(onion_addr); + if let Some(onion_services) = &mut self.onion_services { + onion_services.remove(onion_addr); + } } } From 70a633c355d3cde5ac8425d2d2c9862dd9baa540 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 24 Feb 2024 15:16:56 +0000 Subject: [PATCH 051/184] cargo: added Cargo.lock files to source control --- tor-interface/fuzz/Cargo.lock | 1110 +++++++++++++++++++++++++++++++++ 1 file changed, 1110 insertions(+) create mode 100644 tor-interface/fuzz/Cargo.lock diff --git a/tor-interface/fuzz/Cargo.lock b/tor-interface/fuzz/Cargo.lock new file mode 100644 index 000000000..28a059fec --- /dev/null +++ b/tor-interface/fuzz/Cargo.lock @@ -0,0 +1,1110 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "data-encoding-macro" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c01c06f5f429efdf2bae21eb67c28b3df3cf85b7dd2d8ef09c0838dac5d33e" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0047d07f2c89b17dd631c80450d69841a6b5d7fb17278cbc43d7e4cfcf2576f3" +dependencies = [ + "data-encoding", + "syn 1.0.109", +] + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "merlin", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "enum-ordinalize" +version = "3.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "fluid-let" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "749cff877dc1af878a0b31a41dd221a753634401ea0ef2f87b62d3171522485a" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "merlin" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e261cf0f8b3c42ded9f7d2bb59dea03aa52bc8a1cbc7482f9fc3fd1229d3b42" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der", + "pkcs8", + "spki", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.12", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "regex" +version = "1.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "rsa" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a77d189da1fee555ad95b7e50e7457d91c0e089ec68ca69ad2989413bbdab4" +dependencies = [ + "byteorder", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature 2.2.0", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "safelog" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a9e9f807e0ec5a1657bb2f23fa52c5c5a33e7c3d01ff97cf36e35ac6215bcc" +dependencies = [ + "derive_more", + "educe", + "either", + "fluid-let", + "thiserror", +] + +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + +[[package]] +name = "tor-interface" +version = "0.2.0" +dependencies = [ + "data-encoding", + "data-encoding-macro", + "rand 0.8.5", + "rand_core 0.6.4", + "regex", + "sha1", + "sha3", + "signature 1.6.4", + "socks", + "thiserror", + "time-macros", + "tor-llcrypto", +] + +[[package]] +name = "tor-interface-fuzz" +version = "0.0.0" +dependencies = [ + "libfuzzer-sys", + "tor-interface", +] + +[[package]] +name = "tor-llcrypto" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7787f0eb0962b514316c2527799ce019ac90acc8c6db63844cff3282eb3f5904" +dependencies = [ + "aes", + "arrayref", + "base64ct", + "ctr", + "curve25519-dalek", + "derive_more", + "digest 0.10.7", + "ed25519-dalek", + "getrandom 0.2.12", + "hex", + "rand_core 0.5.1", + "rand_core 0.6.4", + "rsa", + "safelog", + "serde", + "sha1", + "sha2 0.10.8", + "sha3", + "signature 1.6.4", + "simple_asn1", + "subtle", + "thiserror", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.49", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "x25519-dalek" +version = "2.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] From 8409d3ec9222ce81ffe3cec3afea407885f4fb41 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 3 Mar 2024 11:40:32 +0000 Subject: [PATCH 052/184] build: updated cmake build files to be more idiomatic; added various configuration options --- tor-interface/CMakeLists.txt | 46 +++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index e9b30ba8b..98f3c6d8d 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -25,24 +25,32 @@ add_custom_command( add_custom_target(tor_interface_target DEPENDS ${tor_interface_outputs}) -# cargo test target -add_custom_target(tor_interface_cargo_test_target - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} -) -add_dependencies(test_target tor_interface_cargo_test_target) +if (ENABLE_TESTS) + if (ENABLE_ONLINE_TESTS) + # + # cargo test target + # + add_test(NAME tor_interface_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() + # + # cargo test (offline) target + # + add_test(NAME tor_interface_offline_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} --features offline-test + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) -# cargo test (offline) target -add_custom_target(tor_interface_cargo_test_offline_target - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} --features offline-test - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} -) -add_dependencies(test_offline_target tor_interface_cargo_test_offline_target) + # + # fuzz target + # + if (ENABLE_FUZZ_TESTS) + add_test(NAME tor_interface_crypto_cargo_fuzz_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo fuzz run fuzz_crypto ${CARGO_FLAGS} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() +endif() -# -# fuzz target -# -add_custom_target(tor_interface_cargo_fuzz_crypto - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo fuzz run fuzz_crypto ${CARGO_FLAGS} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} -) From 355fad89fc7204e50ae277f11b8ae385bc604137 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 8 Mar 2024 21:20:54 +0000 Subject: [PATCH 053/184] workflow: update fuzz actions to use cmake targets --- tor-interface/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 98f3c6d8d..7900e8a76 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -48,7 +48,7 @@ if (ENABLE_TESTS) # if (ENABLE_FUZZ_TESTS) add_test(NAME tor_interface_crypto_cargo_fuzz_test - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo fuzz run fuzz_crypto ${CARGO_FLAGS} + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo fuzz run fuzz_crypto -- -max_total_time=${FUZZ_TEST_MAX_TOTAL_TIME} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) endif() From 062ec4cc0142732aaa974c95426b916782885721 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 11 Mar 2024 02:16:20 +0000 Subject: [PATCH 054/184] build: removed fuzz .gitignore files --- tor-interface/fuzz/.gitignore | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 tor-interface/fuzz/.gitignore diff --git a/tor-interface/fuzz/.gitignore b/tor-interface/fuzz/.gitignore deleted file mode 100644 index 1a45eee77..000000000 --- a/tor-interface/fuzz/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target -corpus -artifacts -coverage From adc5835498644dfcb5a3fe7d2a24a9f6de9601a4 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Wed, 13 Mar 2024 20:09:04 +0000 Subject: [PATCH 055/184] gosling: separate protocol and crate version; update fuzz tests to reference protocol version --- tor-interface/fuzz/Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/fuzz/Cargo.lock b/tor-interface/fuzz/Cargo.lock index 28a059fec..6ce6b3d74 100644 --- a/tor-interface/fuzz/Cargo.lock +++ b/tor-interface/fuzz/Cargo.lock @@ -916,7 +916,7 @@ dependencies = [ [[package]] name = "tor-interface" -version = "0.2.0" +version = "0.2.1" dependencies = [ "data-encoding", "data-encoding-macro", From 72dedc1c0082c4d2da43029f0e9abcc817a1ba67 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 15 Mar 2024 21:09:07 +0000 Subject: [PATCH 056/184] cgosling: updated cbindgen dependency and disabled unnecessary features --- tor-interface/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 44ca759c3..96d7d3598 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -21,7 +21,7 @@ signature = "^1.5" socks = "^0.3" thiserror = "^1" time-macros = ">= 0.2, <= 0.2.8" -tor-llcrypto = { version = ">= 0.3, <= 0.4.4", features = ["relay"] } +tor-llcrypto = { version = "0.4.4", features = ["relay"] } [dev-dependencies] anyhow = "^1" From 960798e8c047dc7b9190f72bd46eeef129ccddee Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 23 Mar 2024 20:56:00 +0000 Subject: [PATCH 057/184] build: updated Rust version minimium requirements to 1.70 --- tor-interface/Cargo.toml | 32 +++++++++++++++----------------- tor-interface/fuzz/Cargo.toml | 3 +-- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 96d7d3598..5771db069 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -2,7 +2,7 @@ name = "tor-interface" authors = ["Richard Pospesel "] version = "0.2.1" -rust-version = "1.63" +rust-version = "1.70" edition = "2021" license = "BSD-3-Clause" description = "A library providing a Rust interface to interact with the legacy tor daemon" @@ -10,24 +10,22 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -data-encoding = "^2" -data-encoding-macro = "^0.1" -rand = "^0.8" -rand_core = "^0.6" -regex = ">= 1.9, <= 1.9.6" -sha1 = "^0.10" -sha3 = "^0.10" -signature = "^1.5" -socks = "^0.3" -thiserror = "^1" -time-macros = ">= 0.2, <= 0.2.8" -tor-llcrypto = { version = "0.4.4", features = ["relay"] } +data-encoding = "2.0" +data-encoding-macro = "0.1" +rand = "0.8" +rand_core = "0.6" +regex = "1.9" +sha1 = "0.10" +sha3 = "0.10" +signature = "1.5" +socks = "0.3" +thiserror = "1.0" +tor-llcrypto = { version = "0.4", features = ["relay"] } [dev-dependencies] -anyhow = "^1" -dashmap = ">= 5, <= 5.4.0" -serial_test = "0.9.*" -which = ">= 4.4.2, <= 5.0.0" +anyhow = "1.0" +serial_test = "0.9" +which = "4.4" [features] offline-test = [] diff --git a/tor-interface/fuzz/Cargo.toml b/tor-interface/fuzz/Cargo.toml index 0960929fb..84a91cc22 100644 --- a/tor-interface/fuzz/Cargo.toml +++ b/tor-interface/fuzz/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "tor-interface-fuzz" version = "0.0.0" -rust-version = "1.63" publish = false edition = "2021" @@ -9,7 +8,7 @@ edition = "2021" cargo-fuzz = true [dependencies] -libfuzzer-sys = { version = "^0.4", features = ["arbitrary-derive"] } +libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] } [dependencies.tor-interface] path = ".." From 51998bd3b7d3de5594d0b4d500da84f3cc05e6d3 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Tue, 26 Mar 2024 03:22:05 +0000 Subject: [PATCH 058/184] tor-interface: update tor-llcrypto dependency to 0.7.0 --- tor-interface/Cargo.toml | 3 +- tor-interface/fuzz/Cargo.lock | 251 ++++++++------------- tor-interface/src/legacy_tor_controller.rs | 2 +- tor-interface/src/tor_crypto.rs | 98 +++++--- tor-interface/tests/tor_crypto.rs | 8 +- 5 files changed, 161 insertions(+), 201 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 5771db069..45823b390 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] +curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" rand = "0.8" @@ -20,7 +21,7 @@ sha3 = "0.10" signature = "1.5" socks = "0.3" thiserror = "1.0" -tor-llcrypto = { version = "0.4", features = ["relay"] } +tor-llcrypto = { version = "0.7", features = ["relay"] } [dev-dependencies] anyhow = "1.0" diff --git a/tor-interface/fuzz/Cargo.lock b/tor-interface/fuzz/Cargo.lock index 6ce6b3d74..ce3e9755e 100644 --- a/tor-interface/fuzz/Cargo.lock +++ b/tor-interface/fuzz/Cargo.lock @@ -32,12 +32,6 @@ dependencies = [ "derive_arbitrary", ] -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - [[package]] name = "autocfg" version = "1.1.0" @@ -50,15 +44,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -149,17 +134,32 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "3.2.0" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "platforms", + "rustc_version", "subtle", "zeroize", ] +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + [[package]] name = "data-encoding" version = "2.5.0" @@ -188,9 +188,9 @@ dependencies = [ [[package]] name = "der" -version = "0.6.1" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ "const-oid", "pem-rfc7468", @@ -221,47 +221,40 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "const-oid", "crypto-common", ] [[package]] name = "ed25519" -version = "1.5.3" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "signature 1.6.4", + "pkcs8", + "signature 2.2.0", ] [[package]] name = "ed25519-dalek" -version = "1.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", "merlin", - "rand 0.7.3", + "rand_core", "serde", - "sha2 0.9.9", + "sha2", + "subtle", "zeroize", ] @@ -296,6 +289,12 @@ dependencies = [ "syn 2.0.49", ] +[[package]] +name = "fiat-crypto" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f" + [[package]] name = "fluid-let" version = "1.0.0" @@ -312,17 +311,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.12" @@ -332,7 +320,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -430,13 +418,13 @@ checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "merlin" -version = "2.0.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e261cf0f8b3c42ded9f7d2bb59dea03aa52bc8a1cbc7482f9fc3fd1229d3b42" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" dependencies = [ "byteorder", "keccak", - "rand_core 0.5.1", + "rand_core", "zeroize", ] @@ -463,7 +451,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand", "smallvec", "zeroize", ] @@ -504,43 +492,42 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - [[package]] name = "pem-rfc7468" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ "base64ct", ] [[package]] name = "pkcs1" -version = "0.4.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ "der", "pkcs8", "spki", - "zeroize", ] [[package]] name = "pkcs8" -version = "0.9.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "spki", ] +[[package]] +name = "platforms" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -565,19 +552,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - [[package]] name = "rand" version = "0.8.5" @@ -585,18 +559,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "rand_chacha", + "rand_core", ] [[package]] @@ -606,16 +570,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", + "rand_core", ] [[package]] @@ -624,16 +579,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.12", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", + "getrandom", ] [[package]] @@ -667,20 +613,20 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "rsa" -version = "0.8.2" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a77d189da1fee555ad95b7e50e7457d91c0e089ec68ca69ad2989413bbdab4" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" dependencies = [ - "byteorder", - "digest 0.10.7", + "const-oid", + "digest", "num-bigint-dig", "num-integer", - "num-iter", "num-traits", "pkcs1", "pkcs8", - "rand_core 0.6.4", + "rand_core", "signature 2.2.0", + "spki", "subtle", "zeroize", ] @@ -696,9 +642,9 @@ dependencies = [ [[package]] name = "safelog" -version = "0.2.2" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a9e9f807e0ec5a1657bb2f23fa52c5c5a33e7c3d01ff97cf36e35ac6215bcc" +checksum = "b4dd088c4f8f20154e72ef45c78b31b1225b19b448dd3b0f37d605de1b8b8ef5" dependencies = [ "derive_more", "educe", @@ -741,20 +687,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", + "digest", ] [[package]] @@ -765,7 +698,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] @@ -774,7 +707,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest 0.10.7", + "digest", "keccak", ] @@ -790,8 +723,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", + "digest", + "rand_core", ] [[package]] @@ -831,9 +764,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spki" -version = "0.6.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -918,17 +851,17 @@ dependencies = [ name = "tor-interface" version = "0.2.1" dependencies = [ + "curve25519-dalek", "data-encoding", "data-encoding-macro", - "rand 0.8.5", - "rand_core 0.6.4", + "rand", + "rand_core", "regex", "sha1", "sha3", "signature 1.6.4", "socks", "thiserror", - "time-macros", "tor-llcrypto", ] @@ -942,29 +875,28 @@ dependencies = [ [[package]] name = "tor-llcrypto" -version = "0.4.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7787f0eb0962b514316c2527799ce019ac90acc8c6db63844cff3282eb3f5904" +checksum = "982feadd8fc89aa703dda1d3aeda626f13bde731d61eefbf0844e4771e98d496" dependencies = [ "aes", - "arrayref", "base64ct", "ctr", "curve25519-dalek", "derive_more", - "digest 0.10.7", + "digest", "ed25519-dalek", - "getrandom 0.2.12", + "educe", + "getrandom", "hex", - "rand_core 0.5.1", - "rand_core 0.6.4", + "rand_core", "rsa", "safelog", "serde", "sha1", - "sha2 0.10.8", + "sha2", "sha3", - "signature 1.6.4", + "signature 2.2.0", "simple_asn1", "subtle", "thiserror", @@ -990,12 +922,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1080,12 +1006,13 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "x25519-dalek" -version = "2.0.0-pre.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core 0.6.4", + "rand_core", + "serde", "zeroize", ] diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index 899f0584a..403c68649 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -537,7 +537,7 @@ impl LegacyTorController { } index += "PrivateKey=".len(); let key_blob_string = &line[index..]; - private_key = match Ed25519PrivateKey::from_key_blob(key_blob_string) { + private_key = match Ed25519PrivateKey::from_key_blob_legacy(key_blob_string) { Ok(private_key) => Some(private_key), Err(_) => { return Err(Error::CommandReplyParseFailed(format!( diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index ba39cc21c..fef54e4b0 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -4,15 +4,14 @@ use std::iter; use std::str; // extern crates +use curve25519_dalek::Scalar; use data_encoding::{BASE32, BASE32_NOPAD, BASE64}; use data_encoding_macro::new_encoding; use rand::distributions::Alphanumeric; use rand::rngs::OsRng; use rand::Rng; use sha3::{Digest, Sha3_256}; -use signature::Verifier; use tor_llcrypto::pk::keymanip::*; -use tor_llcrypto::util::rand_compat::RngCompatExt; use tor_llcrypto::*; #[derive(thiserror::Error, Debug)] @@ -93,7 +92,7 @@ pub(crate) fn generate_password(length: usize) -> String { // Struct deinitions pub struct Ed25519PrivateKey { - expanded_secret_key: pk::ed25519::ExpandedSecretKey, + expanded_keypair: pk::ed25519::ExpandedKeypair, } #[derive(Clone)] @@ -155,33 +154,64 @@ impl From for SignBit { } } +// which validation method to use when constructing an ed25519 expanded key from +// a byte array +enum FromRawValidationMethod { + // expanded ed25519 keys coming from legacy c-tor daemon; the scalar portion + // is clamped, but not reduced + LegacyCTor, + // expanded ed25519 keys coming from ed25519-dalek crate; the scalar portion + // has been clamped AND reduced + Ed25519Dalek, +} + // Ed25519 Private Key impl Ed25519PrivateKey { pub fn generate() -> Ed25519PrivateKey { - let secret_key = pk::ed25519::SecretKey::generate(&mut rand_core::OsRng.rng_compat()); + let csprng = &mut OsRng; + let keypair = pk::ed25519::Keypair::generate(csprng); Ed25519PrivateKey { - expanded_secret_key: pk::ed25519::ExpandedSecretKey::from(&secret_key), + expanded_keypair: pk::ed25519::ExpandedKeypair::from(&keypair), } } - pub fn from_raw(raw: &[u8; ED25519_PRIVATE_KEY_SIZE]) -> Result { - // Verify the provided bytes have bits set correctly - // see: https://gitlab.torproject.org/tpo/core/arti/-/issues/1021 - if raw[0] == raw[0] & 248 && raw[31] == (raw[31] & 63) | 64 { - match pk::ed25519::ExpandedSecretKey::from_bytes(raw) { - Ok(expanded_secret_key) => Ok(Ed25519PrivateKey { - expanded_secret_key, - }), - Err(_) => Err(Error::KeyInvalid), + fn from_raw_impl(raw: &[u8; ED25519_PRIVATE_KEY_SIZE], method: FromRawValidationMethod) -> Result { + // see: https://gitlab.torproject.org/tpo/core/arti/-/issues/1343 + match method { + FromRawValidationMethod::LegacyCTor => { + // Verify the scalar portion of the expanded key has been clamped + // see: https://gitlab.torproject.org/tpo/core/arti/-/issues/1021 + if !(raw[0] == raw[0] & 248 && raw[31] == (raw[31] & 63) | 64) { + return Err(Error::KeyInvalid); + } + }, + FromRawValidationMethod::Ed25519Dalek => { + // Verify the scalar is non-zero and it has been reduced + let scalar: [u8; 32] = raw[..32].try_into().unwrap(); + if scalar.iter().all(|&x| x == 0x00u8) { + return Err(Error::KeyInvalid); + } + let reduced_scalar = Scalar::from_bytes_mod_order(scalar.clone()).to_bytes(); + if scalar != reduced_scalar { + return Err(Error::KeyInvalid); + } } + } + + if let Some(expanded_keypair) = pk::ed25519::ExpandedKeypair::from_secret_key_bytes(raw.clone()) { + Ok(Ed25519PrivateKey{expanded_keypair}) } else { Err(Error::KeyInvalid) } } - pub fn from_key_blob(key_blob: &str) -> Result { + pub fn from_raw(raw: &[u8; ED25519_PRIVATE_KEY_SIZE]) -> Result { + Self::from_raw_impl(raw, FromRawValidationMethod::Ed25519Dalek) + } + + fn from_key_blob_impl(key_blob: &str, method: FromRawValidationMethod) -> Result { if key_blob.len() != ED25519_PRIVATE_KEY_KEYBLOB_LENGTH { return Err(Error::ParseError(format!( "expects string of length '{}'; received string with length '{}'", @@ -219,7 +249,15 @@ impl Ed25519PrivateKey { } }; - Ed25519PrivateKey::from_raw(&private_key_data_raw) + Ed25519PrivateKey::from_raw_impl(&private_key_data_raw, method) + } + + pub (crate) fn from_key_blob_legacy(key_blob: &str) -> Result { + Self::from_key_blob_impl(key_blob, FromRawValidationMethod::LegacyCTor) + } + + pub fn from_key_blob(key_blob: &str) -> Result { + Self::from_key_blob_impl(key_blob, FromRawValidationMethod::Ed25519Dalek) } pub fn from_private_x25519( @@ -230,7 +268,7 @@ impl Ed25519PrivateKey { { Ok(( Ed25519PrivateKey { - expanded_secret_key: result, + expanded_keypair: result, }, match signbit { 0u8 => SignBit::Zero, @@ -252,19 +290,19 @@ impl Ed25519PrivateKey { pub fn to_key_blob(&self) -> String { let mut key_blob = ED25519_PRIVATE_KEY_KEYBLOB_HEADER.to_string(); - key_blob.push_str(&BASE64.encode(&self.expanded_secret_key.to_bytes())); + key_blob.push_str(&BASE64.encode(&self.expanded_keypair.to_secret_key_bytes())); key_blob } pub fn sign_message_ex( &self, - public_key: &Ed25519PublicKey, + _public_key: &Ed25519PublicKey, message: &[u8], ) -> Ed25519Signature { let signature = self - .expanded_secret_key - .sign(message, &public_key.public_key); + .expanded_keypair + .sign(message); Ed25519Signature { signature } } @@ -274,7 +312,7 @@ impl Ed25519PrivateKey { } pub fn to_bytes(&self) -> [u8; ED25519_PRIVATE_KEY_SIZE] { - self.expanded_secret_key.to_bytes() + self.expanded_keypair.to_secret_key_bytes() } } @@ -344,7 +382,7 @@ impl Ed25519PublicKey { pub fn from_private_key(private_key: &Ed25519PrivateKey) -> Ed25519PublicKey { Ed25519PublicKey { - public_key: pk::ed25519::PublicKey::from(&private_key.expanded_secret_key), + public_key: private_key.expanded_keypair.public().clone() } } @@ -387,19 +425,12 @@ impl std::fmt::Debug for Ed25519PublicKey { impl Ed25519Signature { pub fn from_raw(raw: &[u8; ED25519_SIGNATURE_SIZE]) -> Result { Ok(Ed25519Signature { - signature: match pk::ed25519::Signature::from_bytes(raw) { - Ok(signature) => signature, - Err(_) => { - return Err(Error::ConversionError( - "failed to create ed25519 signature from bytes".to_string(), - )) - } - }, + signature: pk::ed25519::Signature::from_bytes(raw) }) } pub fn verify(&self, message: &[u8], public_key: &Ed25519PublicKey) -> bool { - if let Ok(()) = public_key.public_key.verify(message, &self.signature) { + if let Ok(()) = public_key.public_key.verify_strict(message, &self.signature) { return true; } false @@ -440,8 +471,9 @@ impl std::fmt::Debug for Ed25519Signature { impl X25519PrivateKey { pub fn generate() -> X25519PrivateKey { + let csprng = &mut OsRng; X25519PrivateKey { - secret_key: pk::curve25519::StaticSecret::new(rand_core::OsRng.rng_compat()), + secret_key: pk::curve25519::StaticSecret::random_from_rng(csprng), } } diff --git a/tor-interface/tests/tor_crypto.rs b/tor-interface/tests/tor_crypto.rs index b85a573af..eb406f145 100644 --- a/tor-interface/tests/tor_crypto.rs +++ b/tor-interface/tests/tor_crypto.rs @@ -3,11 +3,11 @@ use tor_interface::tor_crypto::*; #[test] fn test_ed25519() -> Result<(), anyhow::Error> { - let private_key_blob = "ED25519-V3:YE3GZtDmc+izGijWKgeVRabbXqK456JKKGONDBhV+kPBVKa2mHVQqnRTVuFXe3inU3YW6qvc7glYEwe9rK0LhQ=="; + let private_key_blob = "ED25519-V3:rP3u8mZaKohap0lKsB8Z8qXbXqK456JKKGONDBhV+gPBVKa2mHVQqnRTVuFXe3inU3YW6qvc7glYEwe9rK0LhQ=="; let private_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = [ - 0x60u8, 0x4du8, 0xc6u8, 0x66u8, 0xd0u8, 0xe6u8, 0x73u8, 0xe8u8, 0xb3u8, 0x1au8, 0x28u8, - 0xd6u8, 0x2au8, 0x07u8, 0x95u8, 0x45u8, 0xa6u8, 0xdbu8, 0x5eu8, 0xa2u8, 0xb8u8, 0xe7u8, - 0xa2u8, 0x4au8, 0x28u8, 0x63u8, 0x8du8, 0x0cu8, 0x18u8, 0x55u8, 0xfau8, 0x43u8, 0xc1u8, + 0xacu8, 0xfdu8, 0xeeu8, 0xf2u8, 0x66u8, 0x5au8, 0x2au8, 0x88u8, 0x5au8, 0xa7u8, 0x49u8, + 0x4au8, 0xb0u8, 0x1fu8, 0x19u8, 0xf2u8, 0xa5u8, 0xdbu8, 0x5eu8, 0xa2u8, 0xb8u8, 0xe7u8, + 0xa2u8, 0x4au8, 0x28u8, 0x63u8, 0x8du8, 0x0cu8, 0x18u8, 0x55u8, 0xfau8, 0x03u8, 0xc1u8, 0x54u8, 0xa6u8, 0xb6u8, 0x98u8, 0x75u8, 0x50u8, 0xaau8, 0x74u8, 0x53u8, 0x56u8, 0xe1u8, 0x57u8, 0x7bu8, 0x78u8, 0xa7u8, 0x53u8, 0x76u8, 0x16u8, 0xeau8, 0xabu8, 0xdcu8, 0xeeu8, 0x09u8, 0x58u8, 0x13u8, 0x07u8, 0xbdu8, 0xacu8, 0xadu8, 0x0bu8, 0x85u8, From fec73ba476dfb9222318019078bc2b6dee422cd0 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Thu, 28 Mar 2024 01:52:54 +0000 Subject: [PATCH 059/184] tor-interface: fixed a few clippy errors --- tor-interface/src/tor_crypto.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index fef54e4b0..cf8e48e4d 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -193,14 +193,14 @@ impl Ed25519PrivateKey { if scalar.iter().all(|&x| x == 0x00u8) { return Err(Error::KeyInvalid); } - let reduced_scalar = Scalar::from_bytes_mod_order(scalar.clone()).to_bytes(); + let reduced_scalar = Scalar::from_bytes_mod_order(scalar).to_bytes(); if scalar != reduced_scalar { return Err(Error::KeyInvalid); } } } - if let Some(expanded_keypair) = pk::ed25519::ExpandedKeypair::from_secret_key_bytes(raw.clone()) { + if let Some(expanded_keypair) = pk::ed25519::ExpandedKeypair::from_secret_key_bytes(*raw) { Ok(Ed25519PrivateKey{expanded_keypair}) } else { Err(Error::KeyInvalid) @@ -382,7 +382,7 @@ impl Ed25519PublicKey { pub fn from_private_key(private_key: &Ed25519PrivateKey) -> Ed25519PublicKey { Ed25519PublicKey { - public_key: private_key.expanded_keypair.public().clone() + public_key: *private_key.expanded_keypair.public() } } From f93e06408def07a3bba6aa3b8e39c2096d47632f Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 29 Mar 2024 18:55:54 +0000 Subject: [PATCH 060/184] tor-interface: replace inline constant math in tor_crypto with hard-coded values and static_asserts for better cbindgen constant generation --- tor-interface/Cargo.toml | 1 + tor-interface/src/tor_crypto.rs | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 45823b390..bd3bafa5d 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -20,6 +20,7 @@ sha1 = "0.10" sha3 = "0.10" signature = "1.5" socks = "0.3" +static_assertions = "1.1" thiserror = "1.0" tor-llcrypto = { version = "0.7", features = ["relay"] } diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index cf8e48e4d..ced1876bb 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -11,6 +11,7 @@ use rand::distributions::Alphanumeric; use rand::rngs::OsRng; use rand::Rng; use sha3::{Digest, Sha3_256}; +use static_assertions::const_assert_eq; use tor_llcrypto::pk::keymanip::*; use tor_llcrypto::*; @@ -36,18 +37,21 @@ pub const ED25519_SIGNATURE_SIZE: usize = 64; /// The number of bytes needed to store onion service id as an ASCII c-string (not including null-terminator) pub const V3_ONION_SERVICE_ID_STRING_LENGTH: usize = 56; /// The number of bytes needed to store onion service id as an ASCII c-string (including null-terminator) -pub const V3_ONION_SERVICE_ID_STRING_SIZE: usize = V3_ONION_SERVICE_ID_STRING_LENGTH + 1; +pub const V3_ONION_SERVICE_ID_STRING_SIZE: usize = 57; +const_assert_eq!(V3_ONION_SERVICE_ID_STRING_SIZE, V3_ONION_SERVICE_ID_STRING_LENGTH + 1); /// The number of bytes needed to store base64 encoded ed25519 private key as an ASCII c-string (not including null-terminator) pub const ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH: usize = 88; /// key klob header string const ED25519_PRIVATE_KEY_KEYBLOB_HEADER: &str = "ED25519-V3:"; /// The number of bytes needed to store the keyblob header pub const ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH: usize = 11; +const_assert_eq!(ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH, ED25519_PRIVATE_KEY_KEYBLOB_HEADER.len()); /// The number of bytes needed to store ed25519 private keyblob as an ASCII c-string (not including a null terminator) -pub const ED25519_PRIVATE_KEY_KEYBLOB_LENGTH: usize = - ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH + ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH; +pub const ED25519_PRIVATE_KEY_KEYBLOB_LENGTH: usize = 99; +const_assert_eq!(ED25519_PRIVATE_KEY_KEYBLOB_LENGTH, ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH + ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH); /// The number of bytes needed to store ed25519 private keyblob as an ASCII c-string (including a null terminator) -pub const ED25519_PRIVATE_KEY_KEYBLOB_SIZE: usize = ED25519_PRIVATE_KEY_KEYBLOB_LENGTH + 1; +pub const ED25519_PRIVATE_KEY_KEYBLOB_SIZE: usize = 100; +const_assert_eq!(ED25519_PRIVATE_KEY_KEYBLOB_SIZE, ED25519_PRIVATE_KEY_KEYBLOB_LENGTH + 1); // number of bytes in an onion service id after base32 decode const V3_ONION_SERVICE_ID_RAW_SIZE: usize = 35; // byte index of the start of the public key checksum @@ -65,11 +69,13 @@ pub const X25519_PUBLIC_KEY_SIZE: usize = 32; /// The number of bytes needed to store base64 encoded x25519 private key as an ASCII c-string (not including null-terminator) pub const X25519_PRIVATE_KEY_BASE64_LENGTH: usize = 44; /// The number of bytes needed to store base64 encoded x25519 private key as an ASCII c-string (including a null terminator) -pub const X25519_PRIVATE_KEY_BASE64_SIZE: usize = X25519_PRIVATE_KEY_BASE64_LENGTH + 1; +pub const X25519_PRIVATE_KEY_BASE64_SIZE: usize = 45; +const_assert_eq!(X25519_PRIVATE_KEY_BASE64_SIZE, X25519_PRIVATE_KEY_BASE64_LENGTH + 1); /// The number of bytes needed to store base32 encoded x25519 public key as an ASCII c-string (not including null-terminator) pub const X25519_PUBLIC_KEY_BASE32_LENGTH: usize = 52; /// The number of bytes needed to store bsae32 encoded x25519 public key as an ASCII c-string (including a null terminator) -pub const X25519_PUBLIC_KEY_BASE32_SIZE: usize = X25519_PUBLIC_KEY_BASE32_LENGTH + 1; +pub const X25519_PUBLIC_KEY_BASE32_SIZE: usize = 53; +const_assert_eq!(X25519_PUBLIC_KEY_BASE32_SIZE, X25519_PUBLIC_KEY_BASE32_LENGTH + 1); const ONION_BASE32: data_encoding::Encoding = new_encoding! { symbols: "abcdefghijklmnopqrstuvwxyz234567", From c1d4c192aee508844b678007f5ad3a45c7b6662f Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 30 Mar 2024 19:45:32 +0000 Subject: [PATCH 061/184] tor-interface: fixed typo in X25519_PUBLIC_KEY_BASE32_SIZE comment --- tor-interface/src/tor_crypto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index ced1876bb..4ff792910 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -73,7 +73,7 @@ pub const X25519_PRIVATE_KEY_BASE64_SIZE: usize = 45; const_assert_eq!(X25519_PRIVATE_KEY_BASE64_SIZE, X25519_PRIVATE_KEY_BASE64_LENGTH + 1); /// The number of bytes needed to store base32 encoded x25519 public key as an ASCII c-string (not including null-terminator) pub const X25519_PUBLIC_KEY_BASE32_LENGTH: usize = 52; -/// The number of bytes needed to store bsae32 encoded x25519 public key as an ASCII c-string (including a null terminator) +/// The number of bytes needed to store base32 encoded x25519 public key as an ASCII c-string (including a null terminator) pub const X25519_PUBLIC_KEY_BASE32_SIZE: usize = 53; const_assert_eq!(X25519_PUBLIC_KEY_BASE32_SIZE, X25519_PUBLIC_KEY_BASE32_LENGTH + 1); From 8ce8ab9d8212bb8913e3a1a05fd6900c9a6d22f8 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 1 Apr 2024 19:21:33 +0000 Subject: [PATCH 062/184] build: removed offline tests in preparation for future feature flags --- tor-interface/CMakeLists.txt | 37 ++++++++-------------- tor-interface/Cargo.toml | 3 -- tor-interface/src/legacy_tor_controller.rs | 1 - tor-interface/tests/tor_provider.rs | 2 -- 4 files changed, 14 insertions(+), 29 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 7900e8a76..baf6f8d36 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -22,35 +22,26 @@ add_custom_command( OUTPUT ${tor_interface_outputs} COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} cargo build ${CARGO_FLAGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + add_custom_target(tor_interface_target DEPENDS ${tor_interface_outputs}) +# +# cargo test target +# if (ENABLE_TESTS) - if (ENABLE_ONLINE_TESTS) - # - # cargo test target - # - add_test(NAME tor_interface_cargo_test - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - endif() - # - # cargo test (offline) target - # add_test(NAME tor_interface_offline_cargo_test - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} --features offline-test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) - - # - # fuzz target - # - if (ENABLE_FUZZ_TESTS) - add_test(NAME tor_interface_crypto_cargo_fuzz_test - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo fuzz run fuzz_crypto -- -max_total_time=${FUZZ_TEST_MAX_TOTAL_TIME} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - endif() endif() +# +# fuzz target +# +if (ENABLE_FUZZ_TESTS) + add_test(NAME tor_interface_crypto_cargo_fuzz_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo fuzz run fuzz_crypto -- -max_total_time=${FUZZ_TEST_MAX_TOTAL_TIME} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) +endif() diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index bd3bafa5d..7c79b4563 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -28,6 +28,3 @@ tor-llcrypto = { version = "0.7", features = ["relay"] } anyhow = "1.0" serial_test = "0.9" which = "4.4" - -[features] -offline-test = [] diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index 403c68649..dc41ee7d9 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -672,7 +672,6 @@ impl LegacyTorController { #[test] #[serial] -#[cfg(not(feature = "offline-test"))] fn test_tor_controller() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index f77ee2a9e..f0aa16915 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -214,7 +214,6 @@ fn test_mock_onion_service() -> anyhow::Result<()> { #[test] #[serial] -#[cfg(not(feature = "offline-test"))] fn test_legacy_bootstrap() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); @@ -225,7 +224,6 @@ fn test_legacy_bootstrap() -> anyhow::Result<()> { #[test] #[serial] -#[cfg(not(feature = "offline-test"))] fn test_legacy_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); From b78630b4be86ba97086f097b47f72ea7cb9d27fb Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 1 Apr 2024 20:13:05 +0000 Subject: [PATCH 063/184] build: fixed names of old 'offline' tests --- tor-interface/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index baf6f8d36..f71a40936 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -30,7 +30,7 @@ add_custom_target(tor_interface_target # cargo test target # if (ENABLE_TESTS) - add_test(NAME tor_interface_offline_cargo_test + add_test(NAME tor_interface_cargo_test COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) From d3b1ed9c3a736f5b8c5854d344bc2afb85854e7a Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 29 Mar 2024 15:37:44 +0000 Subject: [PATCH 064/184] build: added ENABLE_LEGACY_TOR_PROVIDER cmake build flag --- tor-interface/CMakeLists.txt | 18 ++++++++++++++++-- tor-interface/Cargo.toml | 2 ++ tor-interface/src/lib.rs | 5 +++++ tor-interface/src/tor_crypto.rs | 9 +++++++-- tor-interface/tests/tor_provider.rs | 3 +++ 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index f71a40936..19c752fbb 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -14,13 +14,27 @@ set(tor_interface_outputs ${CARGO_TARGET_DIR}/${CARGO_PROFILE}/libtor_interface.d ${CARGO_TARGET_DIR}/${CARGO_PROFILE}/libtor_interface.rlib) +# +# tor-interface crate feature flags +# + +set(TOR_INTERFACE_FEATURE_LIST) +if (ENABLE_LEGACY_TOR_PROVIDER) + list(APPEND TOR_INTERFACE_FEATURE_LIST "legacy-tor-provider") +endif() + +list(JOIN TOR_INTERFACE_FEATURE_LIST "," TOR_INTERFACE_FEATURES) +if (TOR_INTERFACE_FEATURES) + set(TOR_INTERFACE_FEATURES "--features" "${TOR_INTERFACE_FEATURES}") +endif() + # # build target # add_custom_command( DEPENDS ${tor_interface_sources} OUTPUT ${tor_interface_outputs} - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} cargo build ${CARGO_FLAGS} + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} cargo build ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) add_custom_target(tor_interface_target @@ -31,7 +45,7 @@ add_custom_target(tor_interface_target # if (ENABLE_TESTS) add_test(NAME tor_interface_cargo_test - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) endif() diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 7c79b4563..a1dd1c819 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -28,3 +28,5 @@ tor-llcrypto = { version = "0.7", features = ["relay"] } anyhow = "1.0" serial_test = "0.9" which = "4.4" +[features] +legacy-tor-provider = [] diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index 5cef0c573..98b102b01 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -1,7 +1,12 @@ +#[cfg(feature = "legacy-tor-provider")] pub mod legacy_tor_client; +#[cfg(feature = "legacy-tor-provider")] mod legacy_tor_control_stream; +#[cfg(feature = "legacy-tor-provider")] mod legacy_tor_controller; +#[cfg(feature = "legacy-tor-provider")] mod legacy_tor_process; +#[cfg(feature = "legacy-tor-provider")] mod legacy_tor_version; pub mod mock_tor_client; pub mod tor_crypto; diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 4ff792910..1882f2315 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -1,14 +1,15 @@ // standard use std::convert::TryInto; -use std::iter; use std::str; // extern crates use curve25519_dalek::Scalar; use data_encoding::{BASE32, BASE32_NOPAD, BASE64}; use data_encoding_macro::new_encoding; +#[cfg(feature = "legacy-tor-provider")] use rand::distributions::Alphanumeric; use rand::rngs::OsRng; +#[cfg(feature = "legacy-tor-provider")] use rand::Rng; use sha3::{Digest, Sha3_256}; use static_assertions::const_assert_eq; @@ -85,8 +86,9 @@ const ONION_BASE32: data_encoding::Encoding = new_encoding! { // Free functions // securely generate password using OsRng +#[cfg(feature = "legacy-tor-provider")] pub(crate) fn generate_password(length: usize) -> String { - let password: String = iter::repeat(()) + let password: String = std::iter::repeat(()) .map(|()| OsRng.sample(Alphanumeric)) .map(char::from) .take(length) @@ -165,6 +167,7 @@ impl From for SignBit { enum FromRawValidationMethod { // expanded ed25519 keys coming from legacy c-tor daemon; the scalar portion // is clamped, but not reduced + #[cfg(feature = "legacy-tor-provider")] LegacyCTor, // expanded ed25519 keys coming from ed25519-dalek crate; the scalar portion // has been clamped AND reduced @@ -186,6 +189,7 @@ impl Ed25519PrivateKey { fn from_raw_impl(raw: &[u8; ED25519_PRIVATE_KEY_SIZE], method: FromRawValidationMethod) -> Result { // see: https://gitlab.torproject.org/tpo/core/arti/-/issues/1343 match method { + #[cfg(feature = "legacy-tor-provider")] FromRawValidationMethod::LegacyCTor => { // Verify the scalar portion of the expanded key has been clamped // see: https://gitlab.torproject.org/tpo/core/arti/-/issues/1021 @@ -258,6 +262,7 @@ impl Ed25519PrivateKey { Ed25519PrivateKey::from_raw_impl(&private_key_data_raw, method) } + #[cfg(feature = "legacy-tor-provider")] pub (crate) fn from_key_blob_legacy(key_blob: &str) -> Result { Self::from_key_blob_impl(key_blob, FromRawValidationMethod::LegacyCTor) } diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index f0aa16915..739d6ebb3 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -5,6 +5,7 @@ use std::io::{Read, Write}; use serial_test::serial; // internal crates +#[cfg(feature = "legacy-tor-provider")] use tor_interface::legacy_tor_client::*; use tor_interface::mock_tor_client::*; use tor_interface::tor_crypto::*; @@ -214,6 +215,7 @@ fn test_mock_onion_service() -> anyhow::Result<()> { #[test] #[serial] +#[cfg(feature = "legacy-tor-provider")] fn test_legacy_bootstrap() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); @@ -224,6 +226,7 @@ fn test_legacy_bootstrap() -> anyhow::Result<()> { #[test] #[serial] +#[cfg(feature = "legacy-tor-provider")] fn test_legacy_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); From cf2a4615e07c5035dc081c52f5b83908c770e45b Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 1 Apr 2024 21:39:06 +0000 Subject: [PATCH 065/184] build: added ENABLE_MOCK_TOR_PROVIDER cmake build flag --- tor-interface/CMakeLists.txt | 3 +++ tor-interface/Cargo.toml | 2 ++ tor-interface/src/lib.rs | 1 + tor-interface/tests/tor_provider.rs | 3 +++ 4 files changed, 9 insertions(+) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 19c752fbb..22c82d5fc 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -19,6 +19,9 @@ set(tor_interface_outputs # set(TOR_INTERFACE_FEATURE_LIST) +if (ENABLE_MOCK_TOR_PROVIDER) + list(APPEND TOR_INTERFACE_FEATURE_LIST "mock-tor-provider") +endif() if (ENABLE_LEGACY_TOR_PROVIDER) list(APPEND TOR_INTERFACE_FEATURE_LIST "legacy-tor-provider") endif() diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index a1dd1c819..e14fb2179 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -28,5 +28,7 @@ tor-llcrypto = { version = "0.7", features = ["relay"] } anyhow = "1.0" serial_test = "0.9" which = "4.4" + [features] +mock-tor-provider = [] legacy-tor-provider = [] diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index 98b102b01..3f0d7dd51 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -8,6 +8,7 @@ mod legacy_tor_controller; mod legacy_tor_process; #[cfg(feature = "legacy-tor-provider")] mod legacy_tor_version; +#[cfg(feature = "mock-tor-provider")] pub mod mock_tor_client; pub mod tor_crypto; pub mod tor_provider; diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 739d6ebb3..728990410 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -7,6 +7,7 @@ use serial_test::serial; // internal crates #[cfg(feature = "legacy-tor-provider")] use tor_interface::legacy_tor_client::*; +#[cfg(feature = "mock-tor-provider")] use tor_interface::mock_tor_client::*; use tor_interface::tor_crypto::*; use tor_interface::tor_provider::*; @@ -204,11 +205,13 @@ pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Resul } #[test] +#[cfg(feature = "mock-tor-provider")] fn test_mock_bootstrap() -> anyhow::Result<()> { bootstrap_test(Box::new(MockTorClient::new())) } #[test] +#[cfg(feature = "mock-tor-provider")] fn test_mock_onion_service() -> anyhow::Result<()> { onion_service_test(Box::new(MockTorClient::new())) } From 8b4dcfbccecf9561e9e3e95de7c5d74bbd6f4baa Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Thu, 4 Apr 2024 16:00:53 +0000 Subject: [PATCH 066/184] test: added -- --nocapture flags to all cargo tests for easier debugging --- tor-interface/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 22c82d5fc..868e3e3b4 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -48,7 +48,7 @@ add_custom_target(tor_interface_target # if (ENABLE_TESTS) add_test(NAME tor_interface_cargo_test - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) endif() From 15ed7ce6dff1f955f6bb04ee31d9bdbea51126e1 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Wed, 1 May 2024 15:23:38 +0000 Subject: [PATCH 067/184] tor-interface: update tor-llcrypto to 0.18.0 --- tor-interface/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index e14fb2179..7be428715 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -22,7 +22,7 @@ signature = "1.5" socks = "0.3" static_assertions = "1.1" thiserror = "1.0" -tor-llcrypto = { version = "0.7", features = ["relay"] } +tor-llcrypto = { version = "0.18", features = ["relay"] } [dev-dependencies] anyhow = "1.0" From 81915aee6b5be6d103f13940af77e775e04d5e9b Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 13 Apr 2024 20:06:41 +0000 Subject: [PATCH 068/184] tor-interface: split apart tests and genericized TorProvider tests --- tor-interface/tests/tor_provider.rs | 176 ++++++++++++++++++++++++---- 1 file changed, 156 insertions(+), 20 deletions(-) diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 728990410..f2932f082 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -48,24 +48,49 @@ pub(crate) fn bootstrap_test(mut tor: Box) -> anyhow::Result<() Ok(()) } -pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Result<()> { - tor.bootstrap()?; +pub(crate) fn basic_onion_service_test(mut server_provider: Box, mut client_provider: Box) -> anyhow::Result<()> { - let mut bootstrap_complete = false; - while !bootstrap_complete { - for event in tor.update()?.iter() { + server_provider.bootstrap()?; + client_provider.bootstrap()?; + + let mut server_provider_bootstrap_complete = false; + let mut client_provider_bootstrap_complete = false; + + while !server_provider_bootstrap_complete || !client_provider_bootstrap_complete { + for event in server_provider.update()?.iter() { match event { TorEvent::BootstrapStatus { progress, tag, summary, } => println!( - "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + "Server Provider BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", progress, tag, summary ), TorEvent::BootstrapComplete => { - println!("Bootstrap Complete!"); - bootstrap_complete = true; + println!("Server Provider Bootstrap Complete!"); + server_provider_bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + _ => {} + } + } + + for event in client_provider.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "Client Provider BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Client Provider Bootstrap Complete!"); + client_provider_bootstrap_complete = true; } TorEvent::LogReceived { line } => { println!("--- {}", line); @@ -77,6 +102,8 @@ pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Resul // vanilla V3 onion service { + let tor = &mut server_provider; + // create an onion service for this test let private_key = Ed25519PrivateKey::generate(); @@ -106,10 +133,23 @@ pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Resul const MESSAGE: &str = "Hello World!"; { + let tor = &mut client_provider; let service_id = V3OnionServiceId::from_private_key(&private_key); println!("Connecting to onion service"); - let mut client = tor.connect(&service_id, VIRT_PORT, None)?; + let mut attempt_count = 0; + let mut client = loop { + match tor.connect(&service_id, VIRT_PORT, None) { + Ok(client) => break client, + Err(err) => { + println!("connect error: {:?}", err); + attempt_count += 1; + if attempt_count == 3 { + panic!("failed to connect :("); + } + } + } + }; println!("Client writing message: '{}'", MESSAGE); client.write_all(MESSAGE.as_bytes())?; client.flush()?; @@ -122,12 +162,66 @@ pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Resul server.read_to_end(&mut buffer)?; let msg = String::from_utf8(buffer)?; - assert!(MESSAGE == msg); + assert_eq!(MESSAGE, msg); println!("Message received: '{}'", msg); } else { panic!("no listener"); } } + Ok(()) +} + +pub(crate) fn authenticated_onion_service_test(mut server_provider: Box, mut client_provider: Box) -> anyhow::Result<()> { + + server_provider.bootstrap()?; + client_provider.bootstrap()?; + + let mut server_provider_bootstrap_complete = false; + let mut client_provider_bootstrap_complete = false; + + while !server_provider_bootstrap_complete || !client_provider_bootstrap_complete { + for event in server_provider.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "Server Provider BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Server Provider Bootstrap Complete!"); + server_provider_bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + _ => {} + } + } + + for event in client_provider.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "Client Provider BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Client Provider Bootstrap Complete!"); + client_provider_bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + _ => {} + } + } + } // authenticated onion service { @@ -139,11 +233,11 @@ pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Resul println!("Starting and listening to authenticated onion service"); const VIRT_PORT: u16 = 42069u16; - let listener = tor.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]))?; + let listener = server_provider.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]))?; let mut onion_published = false; while !onion_published { - for event in tor.update()?.iter() { + for event in server_provider.update()?.iter() { match event { TorEvent::LogReceived { line } => { println!("--- {}", line); @@ -170,15 +264,15 @@ pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Resul println!("Connecting to onion service (should fail)"); assert!( - tor.connect(&service_id, VIRT_PORT, None).is_err(), + client_provider.connect(&service_id, VIRT_PORT, None).is_err(), "should not able to connect to an authenticated onion service without auth key" ); println!("Add auth key for onion service"); - tor.add_client_auth(&service_id, &private_auth_key)?; + client_provider.add_client_auth(&service_id, &private_auth_key)?; println!("Connecting to onion service with authentication"); - let mut client = tor.connect(&service_id, VIRT_PORT, None)?; + let mut client = client_provider.connect(&service_id, VIRT_PORT, None)?; println!("Client writing message: '{}'", MESSAGE); client.write_all(MESSAGE.as_bytes())?; @@ -186,7 +280,7 @@ pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Resul println!("End of client scope"); println!("Remove auth key for onion service"); - tor.remove_client_auth(&service_id)?; + client_provider.remove_client_auth(&service_id)?; } if let Some(mut server) = listener.accept()? { @@ -204,6 +298,10 @@ pub(crate) fn onion_service_test(mut tor: Box) -> anyhow::Resul Ok(()) } +// +// Mock TorProvider tests +// + #[test] #[cfg(feature = "mock-tor-provider")] fn test_mock_bootstrap() -> anyhow::Result<()> { @@ -212,10 +310,24 @@ fn test_mock_bootstrap() -> anyhow::Result<()> { #[test] #[cfg(feature = "mock-tor-provider")] -fn test_mock_onion_service() -> anyhow::Result<()> { - onion_service_test(Box::new(MockTorClient::new())) +fn test_mock_basic_onion_service() -> anyhow::Result<()> { + let server_provider = Box::new(MockTorClient::new()); + let client_provider = Box::new(MockTorClient::new()); + basic_onion_service_test(server_provider, client_provider) +} + +#[test] +#[cfg(feature = "mock-tor-provider")] +fn test_mock_authenticated_onion_service() -> anyhow::Result<()> { + let server_provider = Box::new(MockTorClient::new()); + let client_provider = Box::new(MockTorClient::new()); + authenticated_onion_service_test(server_provider, client_provider) } +// +// Legacy TorProvider tests +// + #[test] #[serial] #[cfg(feature = "legacy-tor-provider")] @@ -232,8 +344,32 @@ fn test_legacy_bootstrap() -> anyhow::Result<()> { #[cfg(feature = "legacy-tor-provider")] fn test_legacy_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); - data_path.push("test_legacy_onion_service"); + data_path.push("test_legacy_onion_service_server"); + let server_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); - onion_service_test(Box::new(LegacyTorClient::new(&tor_path, &data_path)?)) + let mut data_path = std::env::temp_dir(); + data_path.push("test_legacy_onion_service_cient"); + let client_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); + + basic_onion_service_test(server_provider, client_provider) } + +#[test] +#[serial] +#[cfg(feature = "legacy-tor-provider")] +fn test_legacy_client_auth_onion_service() -> anyhow::Result<()> { + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + + let mut data_path = std::env::temp_dir(); + data_path.push("test_legacy_authenticated_onion_service_server"); + let server_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); + + let mut data_path = std::env::temp_dir(); + data_path.push("test_legacy_authenticated_onion_service_cient"); + let client_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); + + authenticated_onion_service_test(server_provider, client_provider) +} + From 8beeb039fbd2c1445b96d4adc42bbe79b4cfcc8c Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Tue, 2 Apr 2024 20:10:52 +0000 Subject: [PATCH 069/184] tor-interface: add arti-client based TorProvider stub gated on ENABLE_ARTI_CLIENT_TOR_PROVIDER feature flag --- tor-interface/CMakeLists.txt | 4 + tor-interface/Cargo.toml | 2 + tor-interface/src/arti_client_tor_client.rs | 90 +++++++++++++++++++++ tor-interface/src/lib.rs | 2 + 4 files changed, 98 insertions(+) create mode 100644 tor-interface/src/arti_client_tor_client.rs diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 868e3e3b4..c4679ee22 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -1,5 +1,6 @@ set(tor_interface_sources Cargo.toml + src/arti_client_tor_client.rs src/legacy_tor_client.rs src/legacy_tor_controller.rs src/legacy_tor_control_stream.rs @@ -25,6 +26,9 @@ endif() if (ENABLE_LEGACY_TOR_PROVIDER) list(APPEND TOR_INTERFACE_FEATURE_LIST "legacy-tor-provider") endif() +if (ENABLE_ARTI_CLIENT_TOR_PROVIDER) + list(APPEND TOR_INTERFACE_FEATURE_LIST "arti-client-tor-provider") +endif() list(JOIN TOR_INTERFACE_FEATURE_LIST "," TOR_INTERFACE_FEATURES) if (TOR_INTERFACE_FEATURES) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 7be428715..2116447da 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] +arti-client = { version = "0.18", optional = true } curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" @@ -30,5 +31,6 @@ serial_test = "0.9" which = "4.4" [features] +arti-client-tor-provider = ["arti-client"] mock-tor-provider = [] legacy-tor-provider = [] diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs new file mode 100644 index 000000000..e4d039f35 --- /dev/null +++ b/tor-interface/src/arti_client_tor_client.rs @@ -0,0 +1,90 @@ +// standard +use std::io::ErrorKind; + +// internal crates +use crate::tor_crypto::*; +use crate::tor_provider; +use crate::tor_provider::*; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("not implemented")] + NotImplemented(), +} + +impl From for crate::tor_provider::Error { + fn from(error: Error) -> Self { + crate::tor_provider::Error::Generic(error.to_string()) + } +} + +pub struct ArtiClientOnionListener { +} + +impl OnionListenerImpl for ArtiClientOnionListener { + fn set_nonblocking(&self, _nonblocking: bool) -> Result<(), std::io::Error> { + Err(std::io::Error::new(ErrorKind::Other, "not implemented")) + } + fn accept(&self) -> Result, std::io::Error> { + Err(std::io::Error::new(ErrorKind::Other, "not implemented")) + } +} + +#[derive(Default)] +pub struct ArtiClientTorClient { +} + +impl ArtiClientTorClient { + +} + +impl TorProvider for ArtiClientTorClient { + fn update(&mut self) -> Result, tor_provider::Error> { + Err(Error::NotImplemented().into()) + } + + fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { + Err(Error::NotImplemented().into()) + } + + fn add_client_auth( + &mut self, + _service_id: &V3OnionServiceId, + _client_auth: &X25519PrivateKey, + ) -> Result<(), tor_provider::Error> { + Err(Error::NotImplemented().into()) + } + + fn remove_client_auth( + &mut self, + _service_id: &V3OnionServiceId, + ) -> Result<(), tor_provider::Error> { + Err(Error::NotImplemented().into()) + } + + fn connect( + &mut self, + _service_id: &V3OnionServiceId, + _virt_port: u16, + _circuit: Option, + ) -> Result { + Err(Error::NotImplemented().into()) + } + + fn listener( + &mut self, + _private_key: &Ed25519PrivateKey, + _virt_port: u16, + _authorized_clients: Option<&[X25519PublicKey]>, + ) -> Result { + Err(Error::NotImplemented().into()) + } + + fn generate_token(&mut self) -> CircuitToken { + 0usize + } + + fn release_token(&mut self, _token: CircuitToken) { + + } +} diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index 3f0d7dd51..926ea8405 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "arti-client-tor-provider")] +pub mod arti_client_tor_client; #[cfg(feature = "legacy-tor-provider")] pub mod legacy_tor_client; #[cfg(feature = "legacy-tor-provider")] From 3b443ec97a7aef84aba619117bbd3e0c65238fd2 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Wed, 3 Apr 2024 19:03:43 +0000 Subject: [PATCH 070/184] tor-interface: implement ArtiClientTorCLient::new() method --- tor-interface/Cargo.toml | 6 ++- tor-interface/src/arti_client_tor_client.rs | 43 ++++++++++++++++++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 2116447da..7efcadde8 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { version = "0.18", optional = true } +arti-client = { version = "0.18", features = ["tokio"], optional = true} curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" @@ -23,7 +23,9 @@ signature = "1.5" socks = "0.3" static_assertions = "1.1" thiserror = "1.0" +tokio = { version = "1", optional = true } tor-llcrypto = { version = "0.18", features = ["relay"] } +tor-rtcompat = { version = "0", optional = true } [dev-dependencies] anyhow = "1.0" @@ -31,6 +33,6 @@ serial_test = "0.9" which = "4.4" [features] -arti-client-tor-provider = ["arti-client"] +arti-client-tor-provider = ["arti-client", "tokio", "tor-rtcompat"] mock-tor-provider = [] legacy-tor-provider = [] diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index e4d039f35..58f5bf888 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -1,5 +1,13 @@ // standard use std::io::ErrorKind; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +//extern +use tokio::*; +use arti_client::{BootstrapBehavior, TorClient}; +use arti_client::config::{CfgPath, TorClientConfigBuilder}; +use tor_rtcompat::PreferredRuntime; // internal crates use crate::tor_crypto::*; @@ -10,6 +18,10 @@ use crate::tor_provider::*; pub enum Error { #[error("not implemented")] NotImplemented(), + #[error("arti-client config-builder error: {0}")] + ArtiClientConfigBuilderError(#[source] arti_client::config::ConfigBuildError), + #[error("arti-client error: {0}")] + ArtiClientError(#[source] arti_client::Error), } impl From for crate::tor_provider::Error { @@ -30,12 +42,39 @@ impl OnionListenerImpl for ArtiClientOnionListener { } } -#[derive(Default)] pub struct ArtiClientTorClient { + tokio_runtime: Arc, + arti_client: TorClient, } impl ArtiClientTorClient { - + pub fn new(tokio_runtime: Arc, data_directory: &Path) -> Result { + + let arti_client = tokio_runtime.block_on(async { + // set custom config options + let mut config_builder: TorClientConfigBuilder = Default::default(); + + // manually set arti data directory so we can have multiple concurrent instances and control + // where it writes + config_builder.storage().cache_dir(CfgPath::new_literal(PathBuf::from(data_directory))); + config_builder.storage().state_dir(CfgPath::new_literal(PathBuf::from(data_directory))); + + let config = match config_builder.build() { + Ok(config) => config, + Err(err) => return Err(err).map_err(Error::ArtiClientConfigBuilderError), + }; + + TorClient::builder() + .config(config) + .bootstrap_behavior(BootstrapBehavior::Manual) + .create_unbootstrapped().map_err(Error::ArtiClientError) + })?; + + Ok(Self { + tokio_runtime, + arti_client, + }) + } } impl TorProvider for ArtiClientTorClient { From 2127d0dc153bab9876a9b30d3eef62574af25c29 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Thu, 4 Apr 2024 15:58:37 +0000 Subject: [PATCH 071/184] tor-interface: add bootstrap support to ArtiClientTorClient --- tor-interface/Cargo.toml | 3 +- tor-interface/src/arti_client_tor_client.rs | 78 ++++++++++++++++++--- 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 7efcadde8..679f30801 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -24,6 +24,7 @@ socks = "0.3" static_assertions = "1.1" thiserror = "1.0" tokio = { version = "1", optional = true } +tokio-stream = { version = "0.1", optional = true } tor-llcrypto = { version = "0.18", features = ["relay"] } tor-rtcompat = { version = "0", optional = true } @@ -33,6 +34,6 @@ serial_test = "0.9" which = "4.4" [features] -arti-client-tor-provider = ["arti-client", "tokio", "tor-rtcompat"] +arti-client-tor-provider = ["arti-client", "tokio", "tokio-stream", "tor-rtcompat"] mock-tor-provider = [] legacy-tor-provider = [] diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 58f5bf888..7abdb8162 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -1,10 +1,12 @@ // standard use std::io::ErrorKind; +use std::ops::DerefMut; use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; //extern use tokio::*; +use tokio_stream::StreamExt; use arti_client::{BootstrapBehavior, TorClient}; use arti_client::config::{CfgPath, TorClientConfigBuilder}; use tor_rtcompat::PreferredRuntime; @@ -45,19 +47,25 @@ impl OnionListenerImpl for ArtiClientOnionListener { pub struct ArtiClientTorClient { tokio_runtime: Arc, arti_client: TorClient, + pending_events: Arc>>, } impl ArtiClientTorClient { - pub fn new(tokio_runtime: Arc, data_directory: &Path) -> Result { + pub fn new(tokio_runtime: Arc, root_data_directory: &Path) -> Result { let arti_client = tokio_runtime.block_on(async { // set custom config options let mut config_builder: TorClientConfigBuilder = Default::default(); - // manually set arti data directory so we can have multiple concurrent instances and control - // where it writes - config_builder.storage().cache_dir(CfgPath::new_literal(PathBuf::from(data_directory))); - config_builder.storage().state_dir(CfgPath::new_literal(PathBuf::from(data_directory))); + // manually set arti cache and data directories so we can have + // multiple concurrent instances and control where it writes + let mut cache_dir = PathBuf::from(root_data_directory); + cache_dir.push("cache"); + config_builder.storage().cache_dir(CfgPath::new_literal(cache_dir)); + + let mut data_dir = PathBuf::from(root_data_directory); + data_dir.push("data"); + config_builder.storage().state_dir(CfgPath::new_literal(data_dir)); let config = match config_builder.build() { Ok(config) => config, @@ -68,22 +76,76 @@ impl ArtiClientTorClient { .config(config) .bootstrap_behavior(BootstrapBehavior::Manual) .create_unbootstrapped().map_err(Error::ArtiClientError) + + // TODO: implement TorEvent::LogReceived events once upstream issue is resolved: + // https://gitlab.torproject.org/tpo/core/arti/-/issues/1356 })?; + let pending_events = std::vec![ + TorEvent::LogReceived { line: "Starting arti-client TorProvider".to_string() } + ]; + let pending_events = Arc::new(Mutex::new(pending_events)); + Ok(Self { tokio_runtime, arti_client, + pending_events, }) } } impl TorProvider for ArtiClientTorClient { fn update(&mut self) -> Result, tor_provider::Error> { - Err(Error::NotImplemented().into()) + std::thread::sleep(std::time::Duration::from_millis(16)); + match self.pending_events.lock() { + Ok(mut pending_events) => { + Ok(std::mem::take(pending_events.deref_mut())) + } + Err(_) => unreachable!("another thread panicked while holding this pending_events mutex"), + } } fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { - Err(Error::NotImplemented().into()) + // save progress events + let mut bootstrap_events = self.arti_client.bootstrap_events(); + let pending_events = self.pending_events.clone(); + self.tokio_runtime.spawn(async move { + while let Some(evt) = bootstrap_events.next().await { + match pending_events.lock() { + Ok(mut pending_events) => { + pending_events.push(TorEvent::BootstrapStatus { + progress: (evt.as_frac().clamp(0.0f32, 1.0f32) * 100f32) as u32, + tag: "no-tag".to_string(), + summary: "no summary".to_string(), + }); + // TODO: properly handle evt.blocked() with a new TorEvent::Error or something + }, + Err(_) => unreachable!("another thread panicked while holding this pending_events mutex"), + } + } + }); + + // initiate bootstrap + let arti_client = self.arti_client.clone(); + let pending_events = self.pending_events.clone(); + self.tokio_runtime.spawn(async move { + match arti_client.bootstrap().await { + Ok(()) => { + match pending_events.lock() { + Ok(mut pending_events) => { + pending_events.push(TorEvent::BootstrapComplete); + return; + }, + Err(_) => unreachable!("another thread panicked while holding this pending_events mutex"), + } + }, + Err(_err) => { + // TODO: add an error event to TorEvent + }, + } + }); + + Ok(()) } fn add_client_auth( From b4a98ddbb5264d18646e48f1983ded7d92d8c07d Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 5 Apr 2024 05:26:28 +0000 Subject: [PATCH 072/184] tor-interface: expose inner expanded_keypair on Ed25519PrivateKey when arti-client-tor-provider feature enabled --- tor-interface/src/tor_crypto.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 1882f2315..4cf7c2980 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -325,6 +325,11 @@ impl Ed25519PrivateKey { pub fn to_bytes(&self) -> [u8; ED25519_PRIVATE_KEY_SIZE] { self.expanded_keypair.to_secret_key_bytes() } + + #[cfg(feature = "arti-client-tor-provider")] + pub(crate) fn inner(&self) -> &pk::ed25519::ExpandedKeypair { + &self.expanded_keypair + } } impl PartialEq for Ed25519PrivateKey { From fa92a831defd639abce274a727f2d39cccb5b4d2 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 6 Apr 2024 19:03:28 +0000 Subject: [PATCH 073/184] tor-interface: implement listener() method on ArtiClientTorClient --- tor-interface/Cargo.toml | 14 +- tor-interface/src/arti_client_tor_client.rs | 276 ++++++++++++++++++-- 2 files changed, 263 insertions(+), 27 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 679f30801..e1760b756 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,10 +10,11 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { version = "0.18", features = ["tokio"], optional = true} +arti-client = { version = "0.18", features = ["experimental-api", "onion-service-service", "tokio"], optional = true} curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" +fs-mistrust = { version = "0", optional = true } rand = "0.8" rand_core = "0.6" regex = "1.9" @@ -24,8 +25,15 @@ socks = "0.3" static_assertions = "1.1" thiserror = "1.0" tokio = { version = "1", optional = true } -tokio-stream = { version = "0.1", optional = true } +tokio-stream = { version = "0", optional = true } +tor-cell = { version = "0", optional = true } +tor-config = { version = "0", optional = true } +tor-hscrypto = { version = "0", optional = true } +tor-hsservice = { version = "0", optional = true } +tor-keymgr = { version = "0", optional = true, features = ["keymgr"] } tor-llcrypto = { version = "0.18", features = ["relay"] } +tor-persist = { version = "0", optional = true } +tor-proto = { version = "0", optional = true } tor-rtcompat = { version = "0", optional = true } [dev-dependencies] @@ -34,6 +42,6 @@ serial_test = "0.9" which = "4.4" [features] -arti-client-tor-provider = ["arti-client", "tokio", "tokio-stream", "tor-rtcompat"] +arti-client-tor-provider = ["arti-client", "fs-mistrust", "tokio", "tokio-stream", "tor-cell", "tor-config", "tor-hscrypto", "tor-hsservice", "tor-keymgr", "tor-persist", "tor-proto", "tor-rtcompat"] mock-tor-provider = [] legacy-tor-provider = [] diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 7abdb8162..4dd5f6de1 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -1,14 +1,26 @@ // standard use std::io::ErrorKind; +use std::net::{SocketAddr, TcpListener}; use std::ops::DerefMut; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; //extern -use tokio::*; -use tokio_stream::StreamExt; use arti_client::{BootstrapBehavior, TorClient}; use arti_client::config::{CfgPath, TorClientConfigBuilder}; +use fs_mistrust::Mistrust; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::runtime; +use tokio_stream::StreamExt; +use tor_cell::relaycell::msg::{Connected}; +use tor_keymgr::{ArtiEphemeralKeystore, KeyMgrBuilder, KeystoreSelector}; +use tor_hscrypto::pk::HsIdKeypair; +use tor_hsservice::{HsIdKeypairSpecifier, HsNickname, OnionService, RunningOnionService}; +use tor_hsservice::config::{OnionServiceConfigBuilder}; +use tor_llcrypto::pk::ed25519::ExpandedKeypair; +use tor_persist::state_dir::StateDirectory; +use tor_proto::stream::IncomingStreamRequest; use tor_rtcompat::PreferredRuntime; // internal crates @@ -20,10 +32,38 @@ use crate::tor_provider::*; pub enum Error { #[error("not implemented")] NotImplemented(), + + #[error("unable to bind TCP listener")] + TcpListenerBindFailed(#[source] std::io::Error), + + #[error("unable to get TCP listener's local address")] + TcpListenerLocalAddrFailed(#[source] std::io::Error), + #[error("arti-client config-builder error: {0}")] ArtiClientConfigBuilderError(#[source] arti_client::config::ConfigBuildError), + #[error("arti-client error: {0}")] ArtiClientError(#[source] arti_client::Error), + + #[error("tor-keymgr error: {0}")] + TorKeyMgrError(#[source] tor_keymgr::Error), + + // TODO: the 'real' error (tor_keymgr::mgr::KeyMgrBuilderError) isn't + // actually defined anywhere from what I can tell + #[error("failed to build KeyMgr")] + TorKeyMgrBuilderError(), + + #[error("unexpected key found when inserting HsIdKeypair")] + KeyMgrInsertionFailure(), + + #[error("onion-service config-builder error: {0}")] + OnionServiceConfigBuilderError(#[source] tor_config::ConfigBuildError), + + #[error("tor-persist error: {0}")] + TorPersistError(#[source] tor_persist::Error), + + #[error("tor-hsservice startup error: {0}")] + TorHsServiceStartupError(#[source] tor_hsservice::StartupError), } impl From for crate::tor_provider::Error { @@ -33,44 +73,92 @@ impl From for crate::tor_provider::Error { } pub struct ArtiClientOnionListener { + listener: std::net::TcpListener, + onion_addr: OnionAddr, + // onion service terminates when this is dropped, we don't actually use it + // for anything after construction + _onion_service: Arc, } impl OnionListenerImpl for ArtiClientOnionListener { - fn set_nonblocking(&self, _nonblocking: bool) -> Result<(), std::io::Error> { - Err(std::io::Error::new(ErrorKind::Other, "not implemented")) + fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { + self.listener.set_nonblocking(nonblocking) } fn accept(&self) -> Result, std::io::Error> { - Err(std::io::Error::new(ErrorKind::Other, "not implemented")) + match self.listener.accept() { + Ok((stream, _socket_addr)) => Ok(Some(OnionStream { + stream, + local_addr: Some(self.onion_addr.clone()), + peer_addr: None, + })), + Err(err) => { + if err.kind() == ErrorKind::WouldBlock { + Ok(None) + } else { + Err(err) + } + } + } } } pub struct ArtiClientTorClient { tokio_runtime: Arc, arti_client: TorClient, + state_dir: PathBuf, + fs_mistrust: Mistrust, pending_events: Arc>>, } +// used to forward traffic to/from arti to local tcp streams +async fn forward_stream(mut reader: R, mut writer: W) -> () +where + R: tokio::io::AsyncRead + Unpin, + W: tokio::io::AsyncWrite + Unpin, +{ + let mut buf = [0u8; 1024]; + loop { + let count = match reader.read(&mut buf).await { + Ok(0) => break, + Ok(count) => count, + Err(_) => break, + }; + + match writer.write_all(&buf[0..count]).await { + Ok(()) => (), + Err(_) => break, + } + match writer.flush().await { + Ok(()) => (), + Err(_) => break, + } + } +} + impl ArtiClientTorClient { pub fn new(tokio_runtime: Arc, root_data_directory: &Path) -> Result { - let arti_client = tokio_runtime.block_on(async { - // set custom config options - let mut config_builder: TorClientConfigBuilder = Default::default(); + // set custom config options + let mut config_builder: TorClientConfigBuilder = Default::default(); - // manually set arti cache and data directories so we can have - // multiple concurrent instances and control where it writes - let mut cache_dir = PathBuf::from(root_data_directory); - cache_dir.push("cache"); - config_builder.storage().cache_dir(CfgPath::new_literal(cache_dir)); + // manually set arti cache and data directories so we can have + // multiple concurrent instances and control where it writes + let mut cache_dir = PathBuf::from(root_data_directory); + cache_dir.push("cache"); + config_builder.storage().cache_dir(CfgPath::new_literal(cache_dir)); - let mut data_dir = PathBuf::from(root_data_directory); - data_dir.push("data"); - config_builder.storage().state_dir(CfgPath::new_literal(data_dir)); + let mut state_dir = PathBuf::from(root_data_directory); + state_dir.push("state"); + config_builder.storage().state_dir(CfgPath::new_literal(state_dir.clone())); - let config = match config_builder.build() { - Ok(config) => config, - Err(err) => return Err(err).map_err(Error::ArtiClientConfigBuilderError), - }; + let config = match config_builder.build() { + Ok(config) => config, + Err(err) => return Err(err).map_err(Error::ArtiClientConfigBuilderError), + }; + + let fs_mistrust = config.fs_mistrust().clone(); + + let arti_client = tokio_runtime.block_on(async { TorClient::builder() .config(config) @@ -89,6 +177,8 @@ impl ArtiClientTorClient { Ok(Self { tokio_runtime, arti_client, + state_dir, + fs_mistrust, pending_events, }) } @@ -174,11 +264,149 @@ impl TorProvider for ArtiClientTorClient { fn listener( &mut self, - _private_key: &Ed25519PrivateKey, - _virt_port: u16, - _authorized_clients: Option<&[X25519PublicKey]>, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorized_clients: Option<&[X25519PublicKey]>, ) -> Result { - Err(Error::NotImplemented().into()) + + // client auth is not implemented yet + if authorized_clients.is_some() { + return Err(Error::NotImplemented().into()); + } + + // try to bind to a local address, let OS pick our port + let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); + let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; + let socket_addr = listener + .local_addr() + .map_err(Error::TcpListenerLocalAddrFailed)?; + + // create a new ephemeral store for storing our onion service keys + let ephemeral_store: ArtiEphemeralKeystore = ArtiEphemeralKeystore::new("ephemeral".to_string()); + + let keymgr = match KeyMgrBuilder::default() + .default_store(Box::new(ephemeral_store)) + .build() { + Ok(keymgr) => keymgr, + Err(_) => return Err(Error::TorKeyMgrBuilderError().into()), + }; + + // generate a nickname to identify this onion service + let service_id = V3OnionServiceId::from_private_key(private_key); + let hs_nickname = match HsNickname::new(service_id.to_string()) { + Ok(nickname) => nickname, + Err(_) => panic!("v3 onion service id string representation should be a valid HsNickname"), + }; + + let hs_id_spec = HsIdKeypairSpecifier::new(hs_nickname.clone()); + // generate a new HsIdKeypair (from an Ed25519PrivateKey) + // clone() isn't implemented for ExpandedKeypair >:[ + let secret_key_bytes = private_key.inner().to_secret_key_bytes(); + let expanded_keypair = ExpandedKeypair::from_secret_key_bytes(secret_key_bytes).unwrap().into(); + + // write the HsIdKeypair to keymgr + // TODO: for now this should return Ok(None) unless we persist the ephemeral store longer-term (ie for client auth keys in the future) + match keymgr.insert::(expanded_keypair, &hs_id_spec, KeystoreSelector::Default) { + Ok(None) => (), // expected + Ok(Some(_)) => return Err(Error::KeyMgrInsertionFailure().into()), + Err(err) => Err(err).map_err(Error::TorKeyMgrError)?, + } + + // create an OnionServiceConfig with the ephemeral nickname + let onion_service_config = match OnionServiceConfigBuilder::default() + .nickname(hs_nickname) + .build() { + Ok(onion_service_config) => onion_service_config, + Err(err) => Err(err).map_err(Error::OnionServiceConfigBuilderError)?, + }; + + // create OnionService + let state_dir = match StateDirectory::new(self.state_dir.as_path(), &self.fs_mistrust) { + Ok(state_dir) => state_dir, + Err(err) => Err(err).map_err(Error::TorPersistError)?, + }; + let onion_service = OnionService::new(onion_service_config, Arc::new(keymgr), &state_dir).map_err(Error::TorHsServiceStartupError)?; + + let onion_addr = OnionAddr::V3(OnionAddrV3::new( + service_id.clone(), + virt_port, + )); + + // launch the OnionService and get a Stream of RendRequest + let runtime = self.arti_client.runtime().clone(); + let dirmgr = self.arti_client.dirmgr().clone().upcast_arc(); + let hs_circ_pool = self.arti_client.hs_circ_pool().clone(); + // TODO: maybe make this not block, and instead we update the ArtiOnionListener to have like an + // Arc>> which this task sets and on setting *then* we push + // the OnionServicePublished event + let (onion_service, mut rend_requests) = self.tokio_runtime.block_on(async move { + onion_service.launch(runtime.clone(), dirmgr, hs_circ_pool) + }).map_err(Error::TorHsServiceStartupError)?; + + match self.pending_events.lock() { + Ok(mut pending_events) => { + pending_events.push(TorEvent::OnionServicePublished {service_id}); + } + Err(_) => unreachable!("another thread panicked while holding this pending_events mutex"), + } + + // start a task which accepts every RendRequest to get a StreamRequest + self.tokio_runtime.spawn(async move { + while let Some(request) = rend_requests.next().await { + let mut stream_requests = match request.accept().await { + Ok(stream_requests) => stream_requests, + // TODO: probably not our problem? + _ => return, + }; + // spawn a new task to consume the stream requsts + tokio::task::spawn(async move { + while let Some(stream_request) = stream_requests.next().await { + let should_accept = if let IncomingStreamRequest::Begin(begin) = stream_request.request() { + // we only accept connections on the virt port + begin.port() == virt_port + } else { + false + }; + + if should_accept { + let (client_reader, client_writer) = match stream_request.accept(Connected::new_empty()).await { + Ok(data_stream) => data_stream.split(), + // TODO: probably not our problem? + _ => continue, + }; + + let (local_reader, local_writer) = match TcpStream::connect(socket_addr).await { + Ok(tcp_stream) => tcp_stream.into_split(), + // TODO: possibly our problem? + _ => continue, + }; + // now spawn new tasks to forward traffic to/from theonion listener + + // read from connected client and write to local socket + tokio::task::spawn(async move { + forward_stream(client_reader, local_writer).await; + }); + // read from local socket and write to connected client + tokio::task::spawn(async move { + forward_stream(local_reader, client_writer).await; + }); + } else { + // either requesting the wrong port or the wrong type of stream request + let _ = stream_request.shutdown_circuit(); + } + } + }); + } + }); + + // return our OnionListener + let onion_listener = Box::new(ArtiClientOnionListener { + listener, + onion_addr, + _onion_service: onion_service, + }); + + Ok(OnionListener { onion_listener }) } fn generate_token(&mut self) -> CircuitToken { From 44867dd4a9954222db58bdd6a18fd973bf071ead Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Thu, 11 Apr 2024 23:32:28 +0000 Subject: [PATCH 074/184] tor-interface: implement connect() method on ArtiClientTorClient --- tor-interface/Cargo.toml | 4 +- tor-interface/src/arti_client_tor_client.rs | 151 +++++++++++++++----- tor-interface/src/tor_provider.rs | 6 +- 3 files changed, 126 insertions(+), 35 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index e1760b756..13b57b5ea 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { version = "0.18", features = ["experimental-api", "onion-service-service", "tokio"], optional = true} +arti-client = { version = "0.18", features = ["experimental-api", "onion-service-client", "onion-service-service", "tokio"], optional = true} curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" @@ -33,7 +33,7 @@ tor-hsservice = { version = "0", optional = true } tor-keymgr = { version = "0", optional = true, features = ["keymgr"] } tor-llcrypto = { version = "0.18", features = ["relay"] } tor-persist = { version = "0", optional = true } -tor-proto = { version = "0", optional = true } +tor-proto = { version = "0", features = ["stream-ctrl"], optional = true } tor-rtcompat = { version = "0", optional = true } [dev-dependencies] diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 4dd5f6de1..d8cbdbb4d 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -1,6 +1,6 @@ // standard use std::io::ErrorKind; -use std::net::{SocketAddr, TcpListener}; +use std::net::{SocketAddr}; use std::ops::DerefMut; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; @@ -10,7 +10,7 @@ use arti_client::{BootstrapBehavior, TorClient}; use arti_client::config::{CfgPath, TorClientConfigBuilder}; use fs_mistrust::Mistrust; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpStream; +use tokio::net::{TcpListener, TcpStream}; use tokio::runtime; use tokio_stream::StreamExt; use tor_cell::relaycell::msg::{Connected}; @@ -20,7 +20,8 @@ use tor_hsservice::{HsIdKeypairSpecifier, HsNickname, OnionService, RunningOnion use tor_hsservice::config::{OnionServiceConfigBuilder}; use tor_llcrypto::pk::ed25519::ExpandedKeypair; use tor_persist::state_dir::StateDirectory; -use tor_proto::stream::IncomingStreamRequest; +use tor_proto::circuit::ClientCirc; +use tor_proto::stream::{ClientStreamCtrl, IncomingStreamRequest}; use tor_rtcompat::PreferredRuntime; // internal crates @@ -39,6 +40,15 @@ pub enum Error { #[error("unable to get TCP listener's local address")] TcpListenerLocalAddrFailed(#[source] std::io::Error), + #[error("unable to accept connection on TCP Listener")] + TcpListenerAcceptFailed(#[source] std::io::Error), + + #[error("unable to connect to TCP listener")] + TcpStreamConnectFailed(#[source] std::io::Error), + + #[error("unable to convert tokio::TcpStream to std::net::TcpStream")] + TcpStreamIntoFailed(#[source] std::io::Error), + #[error("arti-client config-builder error: {0}")] ArtiClientConfigBuilderError(#[source] arti_client::config::ConfigBuildError), @@ -111,7 +121,7 @@ pub struct ArtiClientTorClient { } // used to forward traffic to/from arti to local tcp streams -async fn forward_stream(mut reader: R, mut writer: W) -> () +async fn forward_stream(circuit: Arc, mut reader: R, mut writer: W) -> () where R: tokio::io::AsyncRead + Unpin, W: tokio::io::AsyncWrite + Unpin, @@ -121,18 +131,23 @@ where let count = match reader.read(&mut buf).await { Ok(0) => break, Ok(count) => count, - Err(_) => break, + Err(_err) => break, }; match writer.write_all(&buf[0..count]).await { Ok(()) => (), - Err(_) => break, + Err(_err) => break, } match writer.flush().await { Ok(()) => (), - Err(_) => break, + Err(_err) => break, } } + // TODO: remove this yield once upstream issue is resovled: + // https://gitlab.torproject.org/tpo/core/arti/-/issues/1370 + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + // kill the circuit + circuit.terminate(); } impl ArtiClientTorClient { @@ -151,6 +166,11 @@ impl ArtiClientTorClient { state_dir.push("state"); config_builder.storage().state_dir(CfgPath::new_literal(state_dir.clone())); + // disable access to clearnet addresses and enable access to onion services + config_builder.address_filter() + .allow_local_addrs(false) + .allow_onion_addrs(true); + let config = match config_builder.build() { Ok(config) => config, Err(err) => return Err(err).map_err(Error::ArtiClientConfigBuilderError), @@ -255,11 +275,66 @@ impl TorProvider for ArtiClientTorClient { fn connect( &mut self, - _service_id: &V3OnionServiceId, - _virt_port: u16, - _circuit: Option, + service_id: &V3OnionServiceId, + virt_port: u16, + circuit: Option, ) -> Result { - Err(Error::NotImplemented().into()) + + // stream isolation not implemented yet + if circuit.is_some() { + return Err(Error::NotImplemented().into()); + } + + // connect to onion service + let target = (format!("{}.onion", service_id), virt_port); + let arti_client = self.arti_client.clone(); + let data_stream = self.tokio_runtime.block_on(async move { + arti_client.connect(target).await + }).map_err(Error::ArtiClientError)?; + + // start a task to forward traffic from returned data stream + // and tcp socket + let client_stream = self.tokio_runtime.block_on(async move { + let circuit = data_stream.ctrl().circuit().unwrap(); + let (data_reader, data_writer) = data_stream.split(); + + // try to bind to a local address, let OS pick our port + let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); + let server_listener = TcpListener::bind(socket_addr).await.map_err(Error::TcpListenerBindFailed)?; + // await future after a client connects + let server_accept_future = server_listener.accept(); + let socket_addr = server_listener + .local_addr() + .map_err(Error::TcpListenerLocalAddrFailed)?; + + // client stream will ultimatley be returned from connect() + let client_stream = TcpStream::connect(socket_addr).await.map_err(Error::TcpStreamConnectFailed)?; + // client has connected so now get the server's tcp stream + let (server_stream, _socket_addr) = server_accept_future.await.map_err(Error::TcpListenerAcceptFailed)?; + let (tcp_reader, tcp_writer) = server_stream.into_split(); + + // now spawn new tasks to forward traffic to/from local listener + tokio::task::spawn({ + let circuit = circuit.clone(); + async move { + forward_stream(circuit, tcp_reader, data_writer).await; + } + }); + tokio::task::spawn(async move { + forward_stream(circuit, data_reader, tcp_writer).await; + }); + Ok::(client_stream) + })?; + + let stream = client_stream.into_std().map_err(Error::TcpStreamIntoFailed)?; + Ok(OnionStream { + stream, + local_addr: None, + peer_addr: Some(TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3::new( + service_id.clone(), + virt_port, + )))), + }) } fn listener( @@ -276,14 +351,14 @@ impl TorProvider for ArtiClientTorClient { // try to bind to a local address, let OS pick our port let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); - let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; + // TODO: make this one async too + let listener = std::net::TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; let socket_addr = listener .local_addr() .map_err(Error::TcpListenerLocalAddrFailed)?; // create a new ephemeral store for storing our onion service keys let ephemeral_store: ArtiEphemeralKeystore = ArtiEphemeralKeystore::new("ephemeral".to_string()); - let keymgr = match KeyMgrBuilder::default() .default_store(Box::new(ephemeral_store)) .build() { @@ -336,19 +411,26 @@ impl TorProvider for ArtiClientTorClient { let runtime = self.arti_client.runtime().clone(); let dirmgr = self.arti_client.dirmgr().clone().upcast_arc(); let hs_circ_pool = self.arti_client.hs_circ_pool().clone(); - // TODO: maybe make this not block, and instead we update the ArtiOnionListener to have like an - // Arc>> which this task sets and on setting *then* we push - // the OnionServicePublished event - let (onion_service, mut rend_requests) = self.tokio_runtime.block_on(async move { - onion_service.launch(runtime.clone(), dirmgr, hs_circ_pool) - }).map_err(Error::TorHsServiceStartupError)?; - match self.pending_events.lock() { - Ok(mut pending_events) => { - pending_events.push(TorEvent::OnionServicePublished {service_id}); + let (onion_service, mut rend_requests) = onion_service.launch(runtime, dirmgr, hs_circ_pool).map_err(Error::TorHsServiceStartupError)?; + + // start a task to signal onion service published + let pending_events = self.pending_events.clone(); + let mut status_events = onion_service.status_events(); + self.tokio_runtime.spawn(async move { + while let Some(evt) = status_events.next().await { + match evt.state() { + tor_hsservice::status::State::Running => match pending_events.lock() { + Ok(mut pending_events) => { + pending_events.push(TorEvent::OnionServicePublished{service_id}); + return; + }, + Err(_) => unreachable!("another thread panicked while holding this pending_events mutex"), + }, + _ => (), + } } - Err(_) => unreachable!("another thread panicked while holding this pending_events mutex"), - } + }); // start a task which accepts every RendRequest to get a StreamRequest self.tokio_runtime.spawn(async move { @@ -369,26 +451,31 @@ impl TorProvider for ArtiClientTorClient { }; if should_accept { - let (client_reader, client_writer) = match stream_request.accept(Connected::new_empty()).await { - Ok(data_stream) => data_stream.split(), - // TODO: probably not our problem? + let data_stream = match stream_request.accept(Connected::new_empty()).await { + Ok(data_stream) => data_stream, + // TODO: probably not our problem _ => continue, }; + let circuit = data_stream.ctrl().circuit().unwrap(); + let (data_reader, data_writer) = data_stream.split(); - let (local_reader, local_writer) = match TcpStream::connect(socket_addr).await { + let (tcp_reader, tcp_writer) = match TcpStream::connect(socket_addr).await { Ok(tcp_stream) => tcp_stream.into_split(), // TODO: possibly our problem? _ => continue, }; - // now spawn new tasks to forward traffic to/from theonion listener + // now spawn new tasks to forward traffic to/from the onion listener // read from connected client and write to local socket - tokio::task::spawn(async move { - forward_stream(client_reader, local_writer).await; + tokio::task::spawn({ + let circuit = circuit.clone(); + async move { + forward_stream(circuit, data_reader, tcp_writer).await; + } }); // read from local socket and write to connected client tokio::task::spawn(async move { - forward_stream(local_reader, client_writer).await; + forward_stream(circuit, tcp_reader, data_writer).await; }); } else { // either requesting the wrong port or the wrong type of stream request diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 34f34a1e6..e6f39373f 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -24,11 +24,15 @@ impl OnionAddrV3 { pub fn service_id(&self) -> &V3OnionServiceId { &self.service_id } + + pub fn virt_port(&self) -> u16 { + self.virt_port + } } impl std::fmt::Display for OnionAddrV3 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}", self.service_id, self.virt_port) + write!(f, "{}.onion:{}", self.service_id, self.virt_port) } } From f5b90b3e43f8a4084bc03c897b5a4df8fd408498 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 13 Apr 2024 20:07:44 +0000 Subject: [PATCH 075/184] tor-interface: added test cases for ArtiClientTorProvider --- tor-interface/tests/tor_provider.rs | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index f2932f082..b733dd3f2 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -1,10 +1,16 @@ // stanndard use std::io::{Read, Write}; +#[cfg(feature = "arti-client-tor-provider")] +use std::sync::Arc; // extern crates use serial_test::serial; +#[cfg(feature = "arti-client-tor-provider")] +use tokio::*; // internal crates +#[cfg(feature = "arti-client-tor-provider")] +use tor_interface::arti_client_tor_client::*; #[cfg(feature = "legacy-tor-provider")] use tor_interface::legacy_tor_client::*; #[cfg(feature = "mock-tor-provider")] @@ -373,3 +379,55 @@ fn test_legacy_client_auth_onion_service() -> anyhow::Result<()> { authenticated_onion_service_test(server_provider, client_provider) } +// +// Arti TorProvider tests +// + +#[test] +#[serial] +#[cfg(feature = "arti-client-tor-provider")] +fn test_arti_client_bootstrap() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + let mut data_path = std::env::temp_dir(); + data_path.push("test_arti_bootstrap"); + let tor_provider = Box::new(ArtiClientTorClient::new(runtime, &data_path).unwrap()); + + bootstrap_test(tor_provider) +} + + +#[test] +#[cfg(feature = "arti-client-tor-provider")] +fn test_arti_client_basic_onion_service() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + let mut data_path = std::env::temp_dir(); + data_path.push("test_arti_basic_onion_service_server"); + let server_provider = Box::new(ArtiClientTorClient::new(runtime.clone(), &data_path).unwrap()); + + let mut data_path = std::env::temp_dir(); + data_path.push("test_arti_basic_onion_service_client"); + let client_provider = Box::new(ArtiClientTorClient::new(runtime.clone(), &data_path).unwrap()); + + basic_onion_service_test(server_provider, client_provider) +} + +/* +TODO: re-enable once client-auth is available in arti +#[test] +#[serial] +#[cfg(feature = "arti-client-tor-provider")] +fn test_arti_authenticated_onion_service() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + + let mut data_path = std::env::temp_dir(); + data_path.push("test_arti_basic_onion_service_server"); + let server_provider = Box::new(ArtiClientTorClient::new(runtime.clone(), &data_path).unwrap()); + + let mut data_path = std::env::temp_dir(); + data_path.push("test_arti_basic_onion_service_client"); + let client_provider = Box::new(ArtiClientTorClient::new(runtime.clone(), &data_path).unwrap()); + + authenticated_onion_service_test(server_provider, client_provider) +} +*/ + From c1556a4bfe29e93f5ded34ff1b1b89e04fca2954 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 13 Apr 2024 20:08:28 +0000 Subject: [PATCH 076/184] tor-interface: added mixed-mode tests with LegacyTorClient and ArtiCloientTorClient --- tor-interface/tests/tor_provider.rs | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index b733dd3f2..602ef576d 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -431,3 +431,44 @@ fn test_arti_authenticated_onion_service() -> anyhow::Result<()> { } */ +// +// Mixed Arti/Legacy TorProvider tests +// + +#[test] +#[serial] +#[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] +fn test_arti_legacy_basic_onion_service() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + + let mut data_path = std::env::temp_dir(); + data_path.push("test_arti_legacy_basic_onion_service_server"); + let server_provider = Box::new(ArtiClientTorClient::new(runtime, &data_path)?); + + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push("test_arti_legacy_basic_onion_service_client"); + let client_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); + + basic_onion_service_test(server_provider, client_provider) +} + +#[test] +#[serial] +#[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] +fn test_legacy_arti_basic_onion_service() -> anyhow::Result<()> { + + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push("test_legacy_arty_basic_onion_service_client"); + let server_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); + + + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + + let mut data_path = std::env::temp_dir(); + data_path.push("test_legacy_arti_basic_onion_service_server"); + let client_provider = Box::new(ArtiClientTorClient::new(runtime, &data_path)?); + + basic_onion_service_test(server_provider, client_provider) +} From 0340173322a7b54609bd45aad9f2e91d553141e3 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 25 May 2024 12:51:30 +0000 Subject: [PATCH 077/184] tor-interface: add timeout and alive flag check to forward_stream() function --- tor-interface/Cargo.toml | 2 +- tor-interface/src/arti_client_tor_client.rs | 67 ++++++++++++--------- tor-interface/tests/tor_provider.rs | 2 +- 3 files changed, 40 insertions(+), 31 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 13b57b5ea..9e865cd1f 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -24,7 +24,7 @@ signature = "1.5" socks = "0.3" static_assertions = "1.1" thiserror = "1.0" -tokio = { version = "1", optional = true } +tokio = { version = "1", features = ["macros"], optional = true } tokio-stream = { version = "0", optional = true } tor-cell = { version = "0", optional = true } tor-config = { version = "0", optional = true } diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index d8cbdbb4d..25fa96813 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -4,6 +4,7 @@ use std::net::{SocketAddr}; use std::ops::DerefMut; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicBool, Ordering}; //extern use arti_client::{BootstrapBehavior, TorClient}; @@ -20,8 +21,7 @@ use tor_hsservice::{HsIdKeypairSpecifier, HsNickname, OnionService, RunningOnion use tor_hsservice::config::{OnionServiceConfigBuilder}; use tor_llcrypto::pk::ed25519::ExpandedKeypair; use tor_persist::state_dir::StateDirectory; -use tor_proto::circuit::ClientCirc; -use tor_proto::stream::{ClientStreamCtrl, IncomingStreamRequest}; +use tor_proto::stream::IncomingStreamRequest; use tor_rtcompat::PreferredRuntime; // internal crates @@ -121,33 +121,42 @@ pub struct ArtiClientTorClient { } // used to forward traffic to/from arti to local tcp streams -async fn forward_stream(circuit: Arc, mut reader: R, mut writer: W) -> () +async fn forward_stream(alive: Arc, mut reader: R, mut writer: W) -> () where R: tokio::io::AsyncRead + Unpin, W: tokio::io::AsyncWrite + Unpin, { + let read_timeout = std::time::Duration::from_millis(100); let mut buf = [0u8; 1024]; - loop { - let count = match reader.read(&mut buf).await { - Ok(0) => break, - Ok(count) => count, - Err(_err) => break, - }; - match writer.write_all(&buf[0..count]).await { - Ok(()) => (), - Err(_err) => break, - } - match writer.flush().await { - Ok(()) => (), - Err(_err) => break, + while alive.load(Ordering::Relaxed) { + tokio::select! { + count = reader.read(&mut buf) => match count { + // end of stream + Ok(0) => break, + // read N bytes + Ok(count) => { + // forward traffic + match writer.write_all(&buf[0..count]).await { + Ok(()) => (), + Err(_err) => break, + } + match writer.flush().await { + Ok(()) => (), + Err(_err) => break, + } + }, + // read failed + Err(_err) => break, + }, + _ = tokio::time::sleep(read_timeout.clone()) => match writer.flush().await { + Ok(()) => (), + Err(_err) => break, + } } } - // TODO: remove this yield once upstream issue is resovled: - // https://gitlab.torproject.org/tpo/core/arti/-/issues/1370 - tokio::time::sleep(std::time::Duration::from_secs(10)).await; - // kill the circuit - circuit.terminate(); + // signal pump death + alive.store(false, Ordering::Relaxed); } impl ArtiClientTorClient { @@ -295,7 +304,6 @@ impl TorProvider for ArtiClientTorClient { // start a task to forward traffic from returned data stream // and tcp socket let client_stream = self.tokio_runtime.block_on(async move { - let circuit = data_stream.ctrl().circuit().unwrap(); let (data_reader, data_writer) = data_stream.split(); // try to bind to a local address, let OS pick our port @@ -314,14 +322,15 @@ impl TorProvider for ArtiClientTorClient { let (tcp_reader, tcp_writer) = server_stream.into_split(); // now spawn new tasks to forward traffic to/from local listener + let pump_alive = Arc::new(AtomicBool::new(true)); tokio::task::spawn({ - let circuit = circuit.clone(); + let pump_alive = pump_alive.clone(); async move { - forward_stream(circuit, tcp_reader, data_writer).await; + forward_stream(pump_alive, tcp_reader, data_writer).await; } }); tokio::task::spawn(async move { - forward_stream(circuit, data_reader, tcp_writer).await; + forward_stream(pump_alive, data_reader, tcp_writer).await; }); Ok::(client_stream) })?; @@ -456,7 +465,6 @@ impl TorProvider for ArtiClientTorClient { // TODO: probably not our problem _ => continue, }; - let circuit = data_stream.ctrl().circuit().unwrap(); let (data_reader, data_writer) = data_stream.split(); let (tcp_reader, tcp_writer) = match TcpStream::connect(socket_addr).await { @@ -466,16 +474,17 @@ impl TorProvider for ArtiClientTorClient { }; // now spawn new tasks to forward traffic to/from the onion listener + let pump_alive = Arc::new(AtomicBool::new(true)); // read from connected client and write to local socket tokio::task::spawn({ - let circuit = circuit.clone(); + let pump_alive = pump_alive.clone(); async move { - forward_stream(circuit, data_reader, tcp_writer).await; + forward_stream(pump_alive, data_reader,tcp_writer).await; } }); // read from local socket and write to connected client tokio::task::spawn(async move { - forward_stream(circuit, tcp_reader, data_writer).await; + forward_stream(pump_alive, tcp_reader, data_writer).await; }); } else { // either requesting the wrong port or the wrong type of stream request diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 602ef576d..a6bcf8445 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -6,7 +6,7 @@ use std::sync::Arc; // extern crates use serial_test::serial; #[cfg(feature = "arti-client-tor-provider")] -use tokio::*; +use tokio::runtime; // internal crates #[cfg(feature = "arti-client-tor-provider")] From 92626a3fe9087e22b915aa32e593a82840ad3069 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 25 May 2024 15:08:41 +0000 Subject: [PATCH 078/184] build: remove Cargo.lock from fuzz projects and add to local .gitignore --- tor-interface/fuzz/.gitignore | 1 + tor-interface/fuzz/Cargo.lock | 1037 --------------------------------- 2 files changed, 1 insertion(+), 1037 deletions(-) create mode 100644 tor-interface/fuzz/.gitignore delete mode 100644 tor-interface/fuzz/Cargo.lock diff --git a/tor-interface/fuzz/.gitignore b/tor-interface/fuzz/.gitignore new file mode 100644 index 000000000..03314f77b --- /dev/null +++ b/tor-interface/fuzz/.gitignore @@ -0,0 +1 @@ +Cargo.lock diff --git a/tor-interface/fuzz/Cargo.lock b/tor-interface/fuzz/Cargo.lock deleted file mode 100644 index ce3e9755e..000000000 --- a/tor-interface/fuzz/Cargo.lock +++ /dev/null @@ -1,1037 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", - "zeroize", -] - -[[package]] -name = "aho-corasick" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" -dependencies = [ - "memchr", -] - -[[package]] -name = "arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "jobserver", - "libc", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", - "zeroize", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "platforms", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.49", -] - -[[package]] -name = "data-encoding" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" - -[[package]] -name = "data-encoding-macro" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c01c06f5f429efdf2bae21eb67c28b3df3cf85b7dd2d8ef09c0838dac5d33e" -dependencies = [ - "data-encoding", - "data-encoding-macro-internal", -] - -[[package]] -name = "data-encoding-macro-internal" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0047d07f2c89b17dd631c80450d69841a6b5d7fb17278cbc43d7e4cfcf2576f3" -dependencies = [ - "data-encoding", - "syn 1.0.109", -] - -[[package]] -name = "der" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "derive_arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.49", -] - -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.109", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature 2.2.0", -] - -[[package]] -name = "ed25519-dalek" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" -dependencies = [ - "curve25519-dalek", - "ed25519", - "merlin", - "rand_core", - "serde", - "sha2", - "subtle", - "zeroize", -] - -[[package]] -name = "educe" -version = "0.4.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "either" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" - -[[package]] -name = "enum-ordinalize" -version = "3.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.49", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f" - -[[package]] -name = "fluid-let" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749cff877dc1af878a0b31a41dd221a753634401ea0ef2f87b62d3171522485a" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "itoa" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" - -[[package]] -name = "jobserver" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "keccak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -dependencies = [ - "spin", -] - -[[package]] -name = "libc" -version = "0.2.153" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" - -[[package]] -name = "libfuzzer-sys" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" -dependencies = [ - "arbitrary", - "cc", - "once_cell", -] - -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - -[[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "memchr" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" - -[[package]] -name = "merlin" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" -dependencies = [ - "byteorder", - "keccak", - "rand_core", - "zeroize", -] - -[[package]] -name = "num-bigint" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "platforms" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro2" -version = "1.0.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "regex" -version = "1.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" - -[[package]] -name = "rsa" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core", - "signature 2.2.0", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "safelog" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4dd088c4f8f20154e72ef45c78b31b1225b19b448dd3b0f37d605de1b8b8ef5" -dependencies = [ - "derive_more", - "educe", - "either", - "fluid-let", - "thiserror", -] - -[[package]] -name = "semver" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" - -[[package]] -name = "serde" -version = "1.0.196" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.196" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.49", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha3" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" -dependencies = [ - "digest", - "keccak", -] - -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "simple_asn1" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror", - "time", -] - -[[package]] -name = "smallvec" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" - -[[package]] -name = "socks" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" -dependencies = [ - "byteorder", - "libc", - "winapi", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.49", -] - -[[package]] -name = "time" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" -dependencies = [ - "itoa", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" - -[[package]] -name = "time-macros" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" -dependencies = [ - "time-core", -] - -[[package]] -name = "tor-interface" -version = "0.2.1" -dependencies = [ - "curve25519-dalek", - "data-encoding", - "data-encoding-macro", - "rand", - "rand_core", - "regex", - "sha1", - "sha3", - "signature 1.6.4", - "socks", - "thiserror", - "tor-llcrypto", -] - -[[package]] -name = "tor-interface-fuzz" -version = "0.0.0" -dependencies = [ - "libfuzzer-sys", - "tor-interface", -] - -[[package]] -name = "tor-llcrypto" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982feadd8fc89aa703dda1d3aeda626f13bde731d61eefbf0844e4771e98d496" -dependencies = [ - "aes", - "base64ct", - "ctr", - "curve25519-dalek", - "derive_more", - "digest", - "ed25519-dalek", - "educe", - "getrandom", - "hex", - "rand_core", - "rsa", - "safelog", - "serde", - "sha1", - "sha2", - "sha3", - "signature 2.2.0", - "simple_asn1", - "subtle", - "thiserror", - "x25519-dalek", - "zeroize", -] - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.49", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.49", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "x25519-dalek" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" -dependencies = [ - "curve25519-dalek", - "rand_core", - "serde", - "zeroize", -] - -[[package]] -name = "zeroize" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.49", -] From 4bce3fef5ab3e3c4d399702440065d037aefb29f Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 25 May 2024 20:46:09 +0000 Subject: [PATCH 079/184] test: reformat test workflow to use a matrix --- tor-interface/CMakeLists.txt | 56 +++++++++++++++++++++++++++-- tor-interface/tests/tor_crypto.rs | 4 +-- tor-interface/tests/tor_provider.rs | 10 +++--- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index c4679ee22..20c05aa54 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -51,8 +51,60 @@ add_custom_target(tor_interface_target # cargo test target # if (ENABLE_TESTS) - add_test(NAME tor_interface_cargo_test - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + if (ENABLE_MOCK_TOR_PROVIDER) + add_test(NAME tor_interface_mock_bootstrap_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mock_bootstrap ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_mock_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mock_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_mock_authenticated_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mock_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() + + if (ENABLE_LEGACY_TOR_PROVIDER) + add_test(NAME tor_interface_legacy_bootstrap_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_legacy_bootstrap ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_legacy_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_legacy_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_legacy_authenticated_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_legacy_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() + + if (ENABLE_ARTI_CLIENT_TOR_PROVIDER) + add_test(NAME tor_interface_arti_client_bootstrap_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_arti_client_bootstrap ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_arti_client_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_arti_client_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() + + if (ENABLE_LEGACY_TOR_PROVIDER AND ENABLE_ARTI_CLIENT_TOR_PROVIDER) + add_test(NAME tor_interface_mixed_arti_client_legacy_bootstrap_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_arti_client_legacy_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_mixed_legacy_arti_client_bootstrap_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_legacy_arti_client_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() + + add_test(NAME tor_interface_crypto_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_crypto_ ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) endif() diff --git a/tor-interface/tests/tor_crypto.rs b/tor-interface/tests/tor_crypto.rs index eb406f145..522605c8c 100644 --- a/tor-interface/tests/tor_crypto.rs +++ b/tor-interface/tests/tor_crypto.rs @@ -2,7 +2,7 @@ use tor_interface::tor_crypto::*; #[test] -fn test_ed25519() -> Result<(), anyhow::Error> { +fn test_crypto_ed25519() -> Result<(), anyhow::Error> { let private_key_blob = "ED25519-V3:rP3u8mZaKohap0lKsB8Z8qXbXqK456JKKGONDBhV+gPBVKa2mHVQqnRTVuFXe3inU3YW6qvc7glYEwe9rK0LhQ=="; let private_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = [ 0xacu8, 0xfdu8, 0xeeu8, 0xf2u8, 0x66u8, 0x5au8, 0x2au8, 0x88u8, 0x5au8, 0xa7u8, 0x49u8, @@ -92,7 +92,7 @@ fn test_ed25519() -> Result<(), anyhow::Error> { } #[test] -fn test_x25519() -> Result<(), anyhow::Error> { +fn test_crypto_x25519() -> Result<(), anyhow::Error> { // private/public key pair const SECRET_BASE64: &str = "0GeSReJXdNcgvWRQdnDXhJGdu5UiwP2fefgT93/oqn0="; const SECRET_RAW: [u8; X25519_PRIVATE_KEY_SIZE] = [ diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index a6bcf8445..63dd0266f 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -316,7 +316,7 @@ fn test_mock_bootstrap() -> anyhow::Result<()> { #[test] #[cfg(feature = "mock-tor-provider")] -fn test_mock_basic_onion_service() -> anyhow::Result<()> { +fn test_mock_onion_service() -> anyhow::Result<()> { let server_provider = Box::new(MockTorClient::new()); let client_provider = Box::new(MockTorClient::new()); basic_onion_service_test(server_provider, client_provider) @@ -365,7 +365,7 @@ fn test_legacy_onion_service() -> anyhow::Result<()> { #[test] #[serial] #[cfg(feature = "legacy-tor-provider")] -fn test_legacy_client_auth_onion_service() -> anyhow::Result<()> { +fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); @@ -398,7 +398,7 @@ fn test_arti_client_bootstrap() -> anyhow::Result<()> { #[test] #[cfg(feature = "arti-client-tor-provider")] -fn test_arti_client_basic_onion_service() -> anyhow::Result<()> { +fn test_arti_client_onion_service() -> anyhow::Result<()> { let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); let mut data_path = std::env::temp_dir(); data_path.push("test_arti_basic_onion_service_server"); @@ -438,7 +438,7 @@ fn test_arti_authenticated_onion_service() -> anyhow::Result<()> { #[test] #[serial] #[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] -fn test_arti_legacy_basic_onion_service() -> anyhow::Result<()> { +fn test_mixed_arti_client_legacy_onion_service() -> anyhow::Result<()> { let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); let mut data_path = std::env::temp_dir(); @@ -456,7 +456,7 @@ fn test_arti_legacy_basic_onion_service() -> anyhow::Result<()> { #[test] #[serial] #[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] -fn test_legacy_arti_basic_onion_service() -> anyhow::Result<()> { +fn test_mixed_legacy_arti_client_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); From 781e61c83ecb068ac5f51e5ff44443d159e2af89 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 26 May 2024 14:11:13 +0000 Subject: [PATCH 080/184] tor-interface: tweak forward_traffic function to allow limited additional pumping when the other half of the data pump completes --- tor-interface/src/arti_client_tor_client.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 25fa96813..0a0347741 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -126,10 +126,18 @@ where R: tokio::io::AsyncRead + Unpin, W: tokio::io::AsyncWrite + Unpin, { + // allow 100ms timeout on reads to verify writer is still good let read_timeout = std::time::Duration::from_millis(100); + // allow additional retries in the event the other half of the pump + // dies; keep pumping data until our read times out 3 times + let mut remaining_retries = 3; let mut buf = [0u8; 1024]; - while alive.load(Ordering::Relaxed) { + loop { + if !alive.load(Ordering::Relaxed) && remaining_retries == 0 { + break; + } + tokio::select! { count = reader.read(&mut buf) => match count { // end of stream @@ -150,7 +158,13 @@ where Err(_err) => break, }, _ = tokio::time::sleep(read_timeout.clone()) => match writer.flush().await { - Ok(()) => (), + Ok(()) => { + // so long as our writer and reader are good, we should + // allow a few additional data pump attempts + if !alive.load(Ordering::Relaxed) { + remaining_retries -= 1; + } + }, Err(_err) => break, } } From 5c920536510d4c8de3c6f2bc92c9db67a31d7fcc Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 26 May 2024 16:59:14 +0000 Subject: [PATCH 081/184] tor-interface: changed function signature of TorProvider::connect() to accept a TargetAddr instead of a service id and port tuple --- tor-interface/src/arti_client_tor_client.rs | 22 ++++++++++++--------- tor-interface/src/legacy_tor_client.rs | 21 ++++++++++---------- tor-interface/src/mock_tor_client.rs | 14 +++++++++---- tor-interface/src/tor_provider.rs | 13 ++++++++---- tor-interface/tests/tor_provider.rs | 6 +++--- 5 files changed, 46 insertions(+), 30 deletions(-) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 0a0347741..9de8c6723 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -7,7 +7,7 @@ use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicBool, Ordering}; //extern -use arti_client::{BootstrapBehavior, TorClient}; +use arti_client::{BootstrapBehavior, DangerouslyIntoTorAddr, IntoTorAddr, TorClient}; use arti_client::config::{CfgPath, TorClientConfigBuilder}; use fs_mistrust::Mistrust; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -55,6 +55,9 @@ pub enum Error { #[error("arti-client error: {0}")] ArtiClientError(#[source] arti_client::Error), + #[error("arti-client tor-addr error: {0}")] + ArtiClientTorAddrError(#[source] arti_client::TorAddrError), + #[error("tor-keymgr error: {0}")] TorKeyMgrError(#[source] tor_keymgr::Error), @@ -298,8 +301,7 @@ impl TorProvider for ArtiClientTorClient { fn connect( &mut self, - service_id: &V3OnionServiceId, - virt_port: u16, + target: TargetAddr, circuit: Option, ) -> Result { @@ -309,10 +311,15 @@ impl TorProvider for ArtiClientTorClient { } // connect to onion service - let target = (format!("{}.onion", service_id), virt_port); + let arti_target = match target.clone() { + TargetAddr::Ip(socket_addr) => socket_addr.into_tor_addr_dangerously(), + TargetAddr::Domain(domain, port) => (domain, port).into_tor_addr(), + TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3{service_id, virt_port})) => (format!("{}.onion", service_id), virt_port).into_tor_addr(), + }.map_err(Error::ArtiClientTorAddrError)?; + let arti_client = self.arti_client.clone(); let data_stream = self.tokio_runtime.block_on(async move { - arti_client.connect(target).await + arti_client.connect(arti_target).await }).map_err(Error::ArtiClientError)?; // start a task to forward traffic from returned data stream @@ -353,10 +360,7 @@ impl TorProvider for ArtiClientTorClient { Ok(OnionStream { stream, local_addr: None, - peer_addr: Some(TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3::new( - service_id.clone(), - virt_port, - )))), + peer_addr: Some(target), }) } diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 2e313ee45..f0681ae85 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -335,8 +335,7 @@ impl TorProvider for LegacyTorClient { // connect to an onion service and returns OnionStream fn connect( &mut self, - service_id: &V3OnionServiceId, - virt_port: u16, + target: TargetAddr, circuit: Option, ) -> Result { if self.socks_listener.is_none() { @@ -355,16 +354,21 @@ impl TorProvider for LegacyTorClient { None => unreachable!(), }; - // our onion domain - let target = socks::TargetAddr::Domain(format!("{}.onion", service_id), virt_port); + // our target + let socks_target = match target.clone() { + TargetAddr::Ip(socket_addr) => socks::TargetAddr::Ip(socket_addr), + TargetAddr::Domain(domain, port) => socks::TargetAddr::Domain(domain, port), + TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3{service_id, virt_port})) => socks::TargetAddr::Domain(format!("{}.onion", service_id), virt_port), + }; + // readwrite stream let stream = match &circuit { - None => Socks5Stream::connect(socks_listener, target), + None => Socks5Stream::connect(socks_listener, socks_target), Some(circuit) => { if let Some(circuit) = self.circuit_tokens.get(circuit) { Socks5Stream::connect_with_password( socks_listener, - target, + socks_target, &circuit.username, &circuit.password, ) @@ -378,10 +382,7 @@ impl TorProvider for LegacyTorClient { Ok(OnionStream { stream: stream.into_inner(), local_addr: None, - peer_addr: Some(TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3::new( - service_id.clone(), - virt_port, - )))), + peer_addr: Some(target), }) } diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index 7bc33d021..929ecc713 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -34,6 +34,9 @@ pub enum Error { #[error("unable to get TCP listener's local adress")] TcpListenerLocalAddrFailed(#[source] std::io::Error), + + #[error("not implemented")] + NotImplemented(), } impl From for crate::tor_provider::Error { @@ -255,15 +258,18 @@ impl TorProvider for MockTorClient { fn connect( &mut self, - service_id: &V3OnionServiceId, - virt_port: u16, + target: TargetAddr, _circuit: Option, ) -> Result { - let client_auth = self.client_auth_keys.get(service_id); + let (service_id, virt_port) = match target { + TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3{service_id, virt_port})) => (service_id, virt_port), + _ => return Err(Error::NotImplemented().into()), + }; + let client_auth = self.client_auth_keys.get(&service_id); match MOCK_TOR_NETWORK.lock() { Ok(mut mock_tor_network) => { - Ok(mock_tor_network.connect_to_onion(service_id, virt_port, client_auth)?) + Ok(mock_tor_network.connect_to_onion(&service_id, virt_port, client_auth)?) } Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"), } diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index e6f39373f..5e9524e6d 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -9,8 +9,8 @@ use crate::tor_crypto::*; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct OnionAddrV3 { - service_id: V3OnionServiceId, - virt_port: u16, + pub(crate) service_id: V3OnionServiceId, + pub(crate) virt_port: u16, } impl OnionAddrV3 { @@ -56,6 +56,12 @@ pub enum TargetAddr { OnionService(OnionAddr), } +impl From<(V3OnionServiceId, u16)> for TargetAddr { + fn from(target_tuple: (V3OnionServiceId, u16)) -> Self { + TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3::new(target_tuple.0, target_tuple.1))) + } +} + #[derive(Debug)] pub enum TorEvent { BootstrapStatus { @@ -173,8 +179,7 @@ pub trait TorProvider: Send { fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error>; fn connect( &mut self, - service_id: &V3OnionServiceId, - virt_port: u16, + target: TargetAddr, circuit: Option, ) -> Result; fn listener( diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 63dd0266f..9cc671809 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -145,7 +145,7 @@ pub(crate) fn basic_onion_service_test(mut server_provider: Box println!("Connecting to onion service"); let mut attempt_count = 0; let mut client = loop { - match tor.connect(&service_id, VIRT_PORT, None) { + match tor.connect((service_id.clone(), VIRT_PORT).into(), None) { Ok(client) => break client, Err(err) => { println!("connect error: {:?}", err); @@ -270,7 +270,7 @@ pub(crate) fn authenticated_onion_service_test(mut server_provider: Box Date: Fri, 31 May 2024 15:52:16 +0000 Subject: [PATCH 082/184] build: add corpus to fuzz test local .gitignore's --- tor-interface/fuzz/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/tor-interface/fuzz/.gitignore b/tor-interface/fuzz/.gitignore index 03314f77b..c3aaf27e7 100644 --- a/tor-interface/fuzz/.gitignore +++ b/tor-interface/fuzz/.gitignore @@ -1 +1,2 @@ Cargo.lock +corpus From 565374ff05ac53e1ba0ce560c0dd6c68b4a50e3c Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 1 Jun 2024 20:10:11 +0000 Subject: [PATCH 083/184] build: ran 'make format' --- tor-interface/src/arti_client_tor_client.rs | 184 ++++++++++++-------- tor-interface/src/legacy_tor_client.rs | 5 +- tor-interface/src/legacy_tor_controller.rs | 3 +- tor-interface/src/mock_tor_client.rs | 11 +- tor-interface/src/tor_crypto.rs | 59 +++++-- tor-interface/src/tor_provider.rs | 5 +- tor-interface/tests/tor_provider.rs | 25 +-- 7 files changed, 182 insertions(+), 110 deletions(-) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 9de8c6723..51bd02baf 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -1,24 +1,24 @@ // standard use std::io::ErrorKind; -use std::net::{SocketAddr}; +use std::net::SocketAddr; use std::ops::DerefMut; use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; //extern -use arti_client::{BootstrapBehavior, DangerouslyIntoTorAddr, IntoTorAddr, TorClient}; use arti_client::config::{CfgPath, TorClientConfigBuilder}; +use arti_client::{BootstrapBehavior, DangerouslyIntoTorAddr, IntoTorAddr, TorClient}; use fs_mistrust::Mistrust; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::runtime; use tokio_stream::StreamExt; -use tor_cell::relaycell::msg::{Connected}; -use tor_keymgr::{ArtiEphemeralKeystore, KeyMgrBuilder, KeystoreSelector}; +use tor_cell::relaycell::msg::Connected; use tor_hscrypto::pk::HsIdKeypair; +use tor_hsservice::config::OnionServiceConfigBuilder; use tor_hsservice::{HsIdKeypairSpecifier, HsNickname, OnionService, RunningOnionService}; -use tor_hsservice::config::{OnionServiceConfigBuilder}; +use tor_keymgr::{ArtiEphemeralKeystore, KeyMgrBuilder, KeystoreSelector}; use tor_llcrypto::pk::ed25519::ExpandedKeypair; use tor_persist::state_dir::StateDirectory; use tor_proto::stream::IncomingStreamRequest; @@ -177,8 +177,10 @@ where } impl ArtiClientTorClient { - pub fn new(tokio_runtime: Arc, root_data_directory: &Path) -> Result { - + pub fn new( + tokio_runtime: Arc, + root_data_directory: &Path, + ) -> Result { // set custom config options let mut config_builder: TorClientConfigBuilder = Default::default(); @@ -186,14 +188,19 @@ impl ArtiClientTorClient { // multiple concurrent instances and control where it writes let mut cache_dir = PathBuf::from(root_data_directory); cache_dir.push("cache"); - config_builder.storage().cache_dir(CfgPath::new_literal(cache_dir)); + config_builder + .storage() + .cache_dir(CfgPath::new_literal(cache_dir)); let mut state_dir = PathBuf::from(root_data_directory); state_dir.push("state"); - config_builder.storage().state_dir(CfgPath::new_literal(state_dir.clone())); + config_builder + .storage() + .state_dir(CfgPath::new_literal(state_dir.clone())); // disable access to clearnet addresses and enable access to onion services - config_builder.address_filter() + config_builder + .address_filter() .allow_local_addrs(false) .allow_onion_addrs(true); @@ -205,19 +212,19 @@ impl ArtiClientTorClient { let fs_mistrust = config.fs_mistrust().clone(); let arti_client = tokio_runtime.block_on(async { - TorClient::builder() .config(config) .bootstrap_behavior(BootstrapBehavior::Manual) - .create_unbootstrapped().map_err(Error::ArtiClientError) + .create_unbootstrapped() + .map_err(Error::ArtiClientError) // TODO: implement TorEvent::LogReceived events once upstream issue is resolved: // https://gitlab.torproject.org/tpo/core/arti/-/issues/1356 })?; - let pending_events = std::vec![ - TorEvent::LogReceived { line: "Starting arti-client TorProvider".to_string() } - ]; + let pending_events = std::vec![TorEvent::LogReceived { + line: "Starting arti-client TorProvider".to_string() + }]; let pending_events = Arc::new(Mutex::new(pending_events)); Ok(Self { @@ -234,10 +241,10 @@ impl TorProvider for ArtiClientTorClient { fn update(&mut self) -> Result, tor_provider::Error> { std::thread::sleep(std::time::Duration::from_millis(16)); match self.pending_events.lock() { - Ok(mut pending_events) => { - Ok(std::mem::take(pending_events.deref_mut())) + Ok(mut pending_events) => Ok(std::mem::take(pending_events.deref_mut())), + Err(_) => { + unreachable!("another thread panicked while holding this pending_events mutex") } - Err(_) => unreachable!("another thread panicked while holding this pending_events mutex"), } } @@ -255,8 +262,10 @@ impl TorProvider for ArtiClientTorClient { summary: "no summary".to_string(), }); // TODO: properly handle evt.blocked() with a new TorEvent::Error or something - }, - Err(_) => unreachable!("another thread panicked while holding this pending_events mutex"), + } + Err(_) => unreachable!( + "another thread panicked while holding this pending_events mutex" + ), } } }); @@ -266,18 +275,18 @@ impl TorProvider for ArtiClientTorClient { let pending_events = self.pending_events.clone(); self.tokio_runtime.spawn(async move { match arti_client.bootstrap().await { - Ok(()) => { - match pending_events.lock() { - Ok(mut pending_events) => { - pending_events.push(TorEvent::BootstrapComplete); - return; - }, - Err(_) => unreachable!("another thread panicked while holding this pending_events mutex"), + Ok(()) => match pending_events.lock() { + Ok(mut pending_events) => { + pending_events.push(TorEvent::BootstrapComplete); + return; } + Err(_) => unreachable!( + "another thread panicked while holding this pending_events mutex" + ), }, Err(_err) => { // TODO: add an error event to TorEvent - }, + } } }); @@ -304,7 +313,6 @@ impl TorProvider for ArtiClientTorClient { target: TargetAddr, circuit: Option, ) -> Result { - // stream isolation not implemented yet if circuit.is_some() { return Err(Error::NotImplemented().into()); @@ -314,13 +322,18 @@ impl TorProvider for ArtiClientTorClient { let arti_target = match target.clone() { TargetAddr::Ip(socket_addr) => socket_addr.into_tor_addr_dangerously(), TargetAddr::Domain(domain, port) => (domain, port).into_tor_addr(), - TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3{service_id, virt_port})) => (format!("{}.onion", service_id), virt_port).into_tor_addr(), - }.map_err(Error::ArtiClientTorAddrError)?; + TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3 { + service_id, + virt_port, + })) => (format!("{}.onion", service_id), virt_port).into_tor_addr(), + } + .map_err(Error::ArtiClientTorAddrError)?; let arti_client = self.arti_client.clone(); - let data_stream = self.tokio_runtime.block_on(async move { - arti_client.connect(arti_target).await - }).map_err(Error::ArtiClientError)?; + let data_stream = self + .tokio_runtime + .block_on(async move { arti_client.connect(arti_target).await }) + .map_err(Error::ArtiClientError)?; // start a task to forward traffic from returned data stream // and tcp socket @@ -329,7 +342,9 @@ impl TorProvider for ArtiClientTorClient { // try to bind to a local address, let OS pick our port let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); - let server_listener = TcpListener::bind(socket_addr).await.map_err(Error::TcpListenerBindFailed)?; + let server_listener = TcpListener::bind(socket_addr) + .await + .map_err(Error::TcpListenerBindFailed)?; // await future after a client connects let server_accept_future = server_listener.accept(); let socket_addr = server_listener @@ -337,9 +352,13 @@ impl TorProvider for ArtiClientTorClient { .map_err(Error::TcpListenerLocalAddrFailed)?; // client stream will ultimatley be returned from connect() - let client_stream = TcpStream::connect(socket_addr).await.map_err(Error::TcpStreamConnectFailed)?; + let client_stream = TcpStream::connect(socket_addr) + .await + .map_err(Error::TcpStreamConnectFailed)?; // client has connected so now get the server's tcp stream - let (server_stream, _socket_addr) = server_accept_future.await.map_err(Error::TcpListenerAcceptFailed)?; + let (server_stream, _socket_addr) = server_accept_future + .await + .map_err(Error::TcpListenerAcceptFailed)?; let (tcp_reader, tcp_writer) = server_stream.into_split(); // now spawn new tasks to forward traffic to/from local listener @@ -356,7 +375,9 @@ impl TorProvider for ArtiClientTorClient { Ok::(client_stream) })?; - let stream = client_stream.into_std().map_err(Error::TcpStreamIntoFailed)?; + let stream = client_stream + .into_std() + .map_err(Error::TcpStreamIntoFailed)?; Ok(OnionStream { stream, local_addr: None, @@ -370,7 +391,6 @@ impl TorProvider for ArtiClientTorClient { virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, ) -> Result { - // client auth is not implemented yet if authorized_clients.is_some() { return Err(Error::NotImplemented().into()); @@ -379,16 +399,19 @@ impl TorProvider for ArtiClientTorClient { // try to bind to a local address, let OS pick our port let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); // TODO: make this one async too - let listener = std::net::TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; + let listener = + std::net::TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; let socket_addr = listener .local_addr() .map_err(Error::TcpListenerLocalAddrFailed)?; // create a new ephemeral store for storing our onion service keys - let ephemeral_store: ArtiEphemeralKeystore = ArtiEphemeralKeystore::new("ephemeral".to_string()); + let ephemeral_store: ArtiEphemeralKeystore = + ArtiEphemeralKeystore::new("ephemeral".to_string()); let keymgr = match KeyMgrBuilder::default() .default_store(Box::new(ephemeral_store)) - .build() { + .build() + { Ok(keymgr) => keymgr, Err(_) => return Err(Error::TorKeyMgrBuilderError().into()), }; @@ -397,18 +420,23 @@ impl TorProvider for ArtiClientTorClient { let service_id = V3OnionServiceId::from_private_key(private_key); let hs_nickname = match HsNickname::new(service_id.to_string()) { Ok(nickname) => nickname, - Err(_) => panic!("v3 onion service id string representation should be a valid HsNickname"), + Err(_) => { + panic!("v3 onion service id string representation should be a valid HsNickname") + } }; let hs_id_spec = HsIdKeypairSpecifier::new(hs_nickname.clone()); // generate a new HsIdKeypair (from an Ed25519PrivateKey) // clone() isn't implemented for ExpandedKeypair >:[ let secret_key_bytes = private_key.inner().to_secret_key_bytes(); - let expanded_keypair = ExpandedKeypair::from_secret_key_bytes(secret_key_bytes).unwrap().into(); + let expanded_keypair = ExpandedKeypair::from_secret_key_bytes(secret_key_bytes) + .unwrap() + .into(); // write the HsIdKeypair to keymgr // TODO: for now this should return Ok(None) unless we persist the ephemeral store longer-term (ie for client auth keys in the future) - match keymgr.insert::(expanded_keypair, &hs_id_spec, KeystoreSelector::Default) { + match keymgr.insert::(expanded_keypair, &hs_id_spec, KeystoreSelector::Default) + { Ok(None) => (), // expected Ok(Some(_)) => return Err(Error::KeyMgrInsertionFailure().into()), Err(err) => Err(err).map_err(Error::TorKeyMgrError)?, @@ -417,7 +445,8 @@ impl TorProvider for ArtiClientTorClient { // create an OnionServiceConfig with the ephemeral nickname let onion_service_config = match OnionServiceConfigBuilder::default() .nickname(hs_nickname) - .build() { + .build() + { Ok(onion_service_config) => onion_service_config, Err(err) => Err(err).map_err(Error::OnionServiceConfigBuilderError)?, }; @@ -427,19 +456,19 @@ impl TorProvider for ArtiClientTorClient { Ok(state_dir) => state_dir, Err(err) => Err(err).map_err(Error::TorPersistError)?, }; - let onion_service = OnionService::new(onion_service_config, Arc::new(keymgr), &state_dir).map_err(Error::TorHsServiceStartupError)?; + let onion_service = OnionService::new(onion_service_config, Arc::new(keymgr), &state_dir) + .map_err(Error::TorHsServiceStartupError)?; - let onion_addr = OnionAddr::V3(OnionAddrV3::new( - service_id.clone(), - virt_port, - )); + let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port)); // launch the OnionService and get a Stream of RendRequest let runtime = self.arti_client.runtime().clone(); let dirmgr = self.arti_client.dirmgr().clone().upcast_arc(); let hs_circ_pool = self.arti_client.hs_circ_pool().clone(); - let (onion_service, mut rend_requests) = onion_service.launch(runtime, dirmgr, hs_circ_pool).map_err(Error::TorHsServiceStartupError)?; + let (onion_service, mut rend_requests) = onion_service + .launch(runtime, dirmgr, hs_circ_pool) + .map_err(Error::TorHsServiceStartupError)?; // start a task to signal onion service published let pending_events = self.pending_events.clone(); @@ -449,10 +478,12 @@ impl TorProvider for ArtiClientTorClient { match evt.state() { tor_hsservice::status::State::Running => match pending_events.lock() { Ok(mut pending_events) => { - pending_events.push(TorEvent::OnionServicePublished{service_id}); + pending_events.push(TorEvent::OnionServicePublished { service_id }); return; - }, - Err(_) => unreachable!("another thread panicked while holding this pending_events mutex"), + } + Err(_) => unreachable!( + "another thread panicked while holding this pending_events mutex" + ), }, _ => (), } @@ -470,26 +501,29 @@ impl TorProvider for ArtiClientTorClient { // spawn a new task to consume the stream requsts tokio::task::spawn(async move { while let Some(stream_request) = stream_requests.next().await { - let should_accept = if let IncomingStreamRequest::Begin(begin) = stream_request.request() { - // we only accept connections on the virt port - begin.port() == virt_port - } else { - false - }; + let should_accept = + if let IncomingStreamRequest::Begin(begin) = stream_request.request() { + // we only accept connections on the virt port + begin.port() == virt_port + } else { + false + }; if should_accept { - let data_stream = match stream_request.accept(Connected::new_empty()).await { - Ok(data_stream) => data_stream, - // TODO: probably not our problem - _ => continue, - }; + let data_stream = + match stream_request.accept(Connected::new_empty()).await { + Ok(data_stream) => data_stream, + // TODO: probably not our problem + _ => continue, + }; let (data_reader, data_writer) = data_stream.split(); - let (tcp_reader, tcp_writer) = match TcpStream::connect(socket_addr).await { - Ok(tcp_stream) => tcp_stream.into_split(), - // TODO: possibly our problem? - _ => continue, - }; + let (tcp_reader, tcp_writer) = + match TcpStream::connect(socket_addr).await { + Ok(tcp_stream) => tcp_stream.into_split(), + // TODO: possibly our problem? + _ => continue, + }; // now spawn new tasks to forward traffic to/from the onion listener let pump_alive = Arc::new(AtomicBool::new(true)); @@ -497,7 +531,7 @@ impl TorProvider for ArtiClientTorClient { tokio::task::spawn({ let pump_alive = pump_alive.clone(); async move { - forward_stream(pump_alive, data_reader,tcp_writer).await; + forward_stream(pump_alive, data_reader, tcp_writer).await; } }); // read from local socket and write to connected client @@ -527,7 +561,5 @@ impl TorProvider for ArtiClientTorClient { 0usize } - fn release_token(&mut self, _token: CircuitToken) { - - } + fn release_token(&mut self, _token: CircuitToken) {} } diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index f0681ae85..9cf1c96a2 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -358,7 +358,10 @@ impl TorProvider for LegacyTorClient { let socks_target = match target.clone() { TargetAddr::Ip(socket_addr) => socks::TargetAddr::Ip(socket_addr), TargetAddr::Domain(domain, port) => socks::TargetAddr::Domain(domain, port), - TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3{service_id, virt_port})) => socks::TargetAddr::Domain(format!("{}.onion", service_id), virt_port), + TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3 { + service_id, + virt_port, + })) => socks::TargetAddr::Domain(format!("{}.onion", service_id), virt_port), }; // readwrite stream diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index dc41ee7d9..56d9e231b 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -537,7 +537,8 @@ impl LegacyTorController { } index += "PrivateKey=".len(); let key_blob_string = &line[index..]; - private_key = match Ed25519PrivateKey::from_key_blob_legacy(key_blob_string) { + private_key = match Ed25519PrivateKey::from_key_blob_legacy(key_blob_string) + { Ok(private_key) => Some(private_key), Err(_) => { return Err(Error::CommandReplyParseFailed(format!( diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index 929ecc713..b61425f92 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -124,8 +124,8 @@ impl MockTorNetwork { } else { Err(Error::OnionServiceNotPublished(onion_addr)) } - }, - None => Err(Error::OnionServiceNotPublished(onion_addr)) + } + None => Err(Error::OnionServiceNotPublished(onion_addr)), } } @@ -140,7 +140,7 @@ impl MockTorNetwork { match &mut self.onion_services { Some(onion_services) => { onion_services.insert(onion_addr, (client_auth_keys, address)); - }, + } None => { let mut onion_services = BTreeMap::new(); onion_services.insert(onion_addr, (client_auth_keys, address)); @@ -262,7 +262,10 @@ impl TorProvider for MockTorClient { _circuit: Option, ) -> Result { let (service_id, virt_port) = match target { - TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3{service_id, virt_port})) => (service_id, virt_port), + TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3 { + service_id, + virt_port, + })) => (service_id, virt_port), _ => return Err(Error::NotImplemented().into()), }; let client_auth = self.client_auth_keys.get(&service_id); diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 4cf7c2980..4f574fb23 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -39,20 +39,32 @@ pub const ED25519_SIGNATURE_SIZE: usize = 64; pub const V3_ONION_SERVICE_ID_STRING_LENGTH: usize = 56; /// The number of bytes needed to store onion service id as an ASCII c-string (including null-terminator) pub const V3_ONION_SERVICE_ID_STRING_SIZE: usize = 57; -const_assert_eq!(V3_ONION_SERVICE_ID_STRING_SIZE, V3_ONION_SERVICE_ID_STRING_LENGTH + 1); +const_assert_eq!( + V3_ONION_SERVICE_ID_STRING_SIZE, + V3_ONION_SERVICE_ID_STRING_LENGTH + 1 +); /// The number of bytes needed to store base64 encoded ed25519 private key as an ASCII c-string (not including null-terminator) pub const ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH: usize = 88; /// key klob header string const ED25519_PRIVATE_KEY_KEYBLOB_HEADER: &str = "ED25519-V3:"; /// The number of bytes needed to store the keyblob header pub const ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH: usize = 11; -const_assert_eq!(ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH, ED25519_PRIVATE_KEY_KEYBLOB_HEADER.len()); +const_assert_eq!( + ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH, + ED25519_PRIVATE_KEY_KEYBLOB_HEADER.len() +); /// The number of bytes needed to store ed25519 private keyblob as an ASCII c-string (not including a null terminator) pub const ED25519_PRIVATE_KEY_KEYBLOB_LENGTH: usize = 99; -const_assert_eq!(ED25519_PRIVATE_KEY_KEYBLOB_LENGTH, ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH + ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH); +const_assert_eq!( + ED25519_PRIVATE_KEY_KEYBLOB_LENGTH, + ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH + ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH +); /// The number of bytes needed to store ed25519 private keyblob as an ASCII c-string (including a null terminator) pub const ED25519_PRIVATE_KEY_KEYBLOB_SIZE: usize = 100; -const_assert_eq!(ED25519_PRIVATE_KEY_KEYBLOB_SIZE, ED25519_PRIVATE_KEY_KEYBLOB_LENGTH + 1); +const_assert_eq!( + ED25519_PRIVATE_KEY_KEYBLOB_SIZE, + ED25519_PRIVATE_KEY_KEYBLOB_LENGTH + 1 +); // number of bytes in an onion service id after base32 decode const V3_ONION_SERVICE_ID_RAW_SIZE: usize = 35; // byte index of the start of the public key checksum @@ -71,12 +83,18 @@ pub const X25519_PUBLIC_KEY_SIZE: usize = 32; pub const X25519_PRIVATE_KEY_BASE64_LENGTH: usize = 44; /// The number of bytes needed to store base64 encoded x25519 private key as an ASCII c-string (including a null terminator) pub const X25519_PRIVATE_KEY_BASE64_SIZE: usize = 45; -const_assert_eq!(X25519_PRIVATE_KEY_BASE64_SIZE, X25519_PRIVATE_KEY_BASE64_LENGTH + 1); +const_assert_eq!( + X25519_PRIVATE_KEY_BASE64_SIZE, + X25519_PRIVATE_KEY_BASE64_LENGTH + 1 +); /// The number of bytes needed to store base32 encoded x25519 public key as an ASCII c-string (not including null-terminator) pub const X25519_PUBLIC_KEY_BASE32_LENGTH: usize = 52; /// The number of bytes needed to store base32 encoded x25519 public key as an ASCII c-string (including a null terminator) pub const X25519_PUBLIC_KEY_BASE32_SIZE: usize = 53; -const_assert_eq!(X25519_PUBLIC_KEY_BASE32_SIZE, X25519_PUBLIC_KEY_BASE32_LENGTH + 1); +const_assert_eq!( + X25519_PUBLIC_KEY_BASE32_SIZE, + X25519_PUBLIC_KEY_BASE32_LENGTH + 1 +); const ONION_BASE32: data_encoding::Encoding = new_encoding! { symbols: "abcdefghijklmnopqrstuvwxyz234567", @@ -186,7 +204,10 @@ impl Ed25519PrivateKey { } } - fn from_raw_impl(raw: &[u8; ED25519_PRIVATE_KEY_SIZE], method: FromRawValidationMethod) -> Result { + fn from_raw_impl( + raw: &[u8; ED25519_PRIVATE_KEY_SIZE], + method: FromRawValidationMethod, + ) -> Result { // see: https://gitlab.torproject.org/tpo/core/arti/-/issues/1343 match method { #[cfg(feature = "legacy-tor-provider")] @@ -196,7 +217,7 @@ impl Ed25519PrivateKey { if !(raw[0] == raw[0] & 248 && raw[31] == (raw[31] & 63) | 64) { return Err(Error::KeyInvalid); } - }, + } FromRawValidationMethod::Ed25519Dalek => { // Verify the scalar is non-zero and it has been reduced let scalar: [u8; 32] = raw[..32].try_into().unwrap(); @@ -211,7 +232,7 @@ impl Ed25519PrivateKey { } if let Some(expanded_keypair) = pk::ed25519::ExpandedKeypair::from_secret_key_bytes(*raw) { - Ok(Ed25519PrivateKey{expanded_keypair}) + Ok(Ed25519PrivateKey { expanded_keypair }) } else { Err(Error::KeyInvalid) } @@ -221,7 +242,10 @@ impl Ed25519PrivateKey { Self::from_raw_impl(raw, FromRawValidationMethod::Ed25519Dalek) } - fn from_key_blob_impl(key_blob: &str, method: FromRawValidationMethod) -> Result { + fn from_key_blob_impl( + key_blob: &str, + method: FromRawValidationMethod, + ) -> Result { if key_blob.len() != ED25519_PRIVATE_KEY_KEYBLOB_LENGTH { return Err(Error::ParseError(format!( "expects string of length '{}'; received string with length '{}'", @@ -263,7 +287,7 @@ impl Ed25519PrivateKey { } #[cfg(feature = "legacy-tor-provider")] - pub (crate) fn from_key_blob_legacy(key_blob: &str) -> Result { + pub(crate) fn from_key_blob_legacy(key_blob: &str) -> Result { Self::from_key_blob_impl(key_blob, FromRawValidationMethod::LegacyCTor) } @@ -311,9 +335,7 @@ impl Ed25519PrivateKey { _public_key: &Ed25519PublicKey, message: &[u8], ) -> Ed25519Signature { - let signature = self - .expanded_keypair - .sign(message); + let signature = self.expanded_keypair.sign(message); Ed25519Signature { signature } } @@ -398,7 +420,7 @@ impl Ed25519PublicKey { pub fn from_private_key(private_key: &Ed25519PrivateKey) -> Ed25519PublicKey { Ed25519PublicKey { - public_key: *private_key.expanded_keypair.public() + public_key: *private_key.expanded_keypair.public(), } } @@ -441,12 +463,15 @@ impl std::fmt::Debug for Ed25519PublicKey { impl Ed25519Signature { pub fn from_raw(raw: &[u8; ED25519_SIGNATURE_SIZE]) -> Result { Ok(Ed25519Signature { - signature: pk::ed25519::Signature::from_bytes(raw) + signature: pk::ed25519::Signature::from_bytes(raw), }) } pub fn verify(&self, message: &[u8], public_key: &Ed25519PublicKey) -> bool { - if let Ok(()) = public_key.public_key.verify_strict(message, &self.signature) { + if let Ok(()) = public_key + .public_key + .verify_strict(message, &self.signature) + { return true; } false diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 5e9524e6d..ebb0980a8 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -58,7 +58,10 @@ pub enum TargetAddr { impl From<(V3OnionServiceId, u16)> for TargetAddr { fn from(target_tuple: (V3OnionServiceId, u16)) -> Self { - TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3::new(target_tuple.0, target_tuple.1))) + TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3::new( + target_tuple.0, + target_tuple.1, + ))) } } diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 9cc671809..4b1d67320 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -54,8 +54,10 @@ pub(crate) fn bootstrap_test(mut tor: Box) -> anyhow::Result<() Ok(()) } -pub(crate) fn basic_onion_service_test(mut server_provider: Box, mut client_provider: Box) -> anyhow::Result<()> { - +pub(crate) fn basic_onion_service_test( + mut server_provider: Box, + mut client_provider: Box, +) -> anyhow::Result<()> { server_provider.bootstrap()?; client_provider.bootstrap()?; @@ -177,8 +179,10 @@ pub(crate) fn basic_onion_service_test(mut server_provider: Box Ok(()) } -pub(crate) fn authenticated_onion_service_test(mut server_provider: Box, mut client_provider: Box) -> anyhow::Result<()> { - +pub(crate) fn authenticated_onion_service_test( + mut server_provider: Box, + mut client_provider: Box, +) -> anyhow::Result<()> { server_provider.bootstrap()?; client_provider.bootstrap()?; @@ -239,7 +243,8 @@ pub(crate) fn authenticated_onion_service_test(mut server_provider: Box anyhow::Result<()> { bootstrap_test(tor_provider) } - #[test] #[cfg(feature = "arti-client-tor-provider")] fn test_arti_client_onion_service() -> anyhow::Result<()> { @@ -457,13 +464,11 @@ fn test_mixed_arti_client_legacy_onion_service() -> anyhow::Result<()> { #[serial] #[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] fn test_mixed_legacy_arti_client_onion_service() -> anyhow::Result<()> { - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_arty_basic_onion_service_client"); let server_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); - let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); let mut data_path = std::env::temp_dir(); From 0915869dad102aced477feb0d825ebde0a508311 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 1 Jun 2024 22:32:04 +0000 Subject: [PATCH 084/184] build: added 'artifacts' to fuzz tests local .gitignore --- tor-interface/fuzz/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/tor-interface/fuzz/.gitignore b/tor-interface/fuzz/.gitignore index c3aaf27e7..c7af9f5ef 100644 --- a/tor-interface/fuzz/.gitignore +++ b/tor-interface/fuzz/.gitignore @@ -1,2 +1,3 @@ Cargo.lock corpus +artifacts From 959bf5b56ad665a78f6a0f4cc54185096520a794 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 15 Jun 2024 04:12:08 +0000 Subject: [PATCH 085/184] tor-interface: implement FromStr for TargetAddr --- tor-interface/CMakeLists.txt | 8 + tor-interface/Cargo.toml | 2 + tor-interface/src/arti_client_tor_client.rs | 4 +- tor-interface/src/legacy_tor_client.rs | 4 +- tor-interface/src/tor_provider.rs | 132 +++++++++++++- tor-interface/tests/tor_provider.rs | 188 ++++++++++++++++++++ 6 files changed, 334 insertions(+), 4 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 20c05aa54..f6ec28c5d 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -103,10 +103,18 @@ if (ENABLE_TESTS) ) endif() + # cryptography add_test(NAME tor_interface_crypto_cargo_test COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_crypto_ ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) + + # tor provider utils + add_test(NAME tor_interface_tor_provider_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_tor_provider_ ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() # diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 9e865cd1f..cb413701b 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -14,7 +14,9 @@ arti-client = { version = "0.18", features = ["experimental-api", "onion-service curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" +domain = "0.10" fs-mistrust = { version = "0", optional = true } +idna = "1" rand = "0.8" rand_core = "0.6" regex = "1.9" diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 51bd02baf..3a8117c1f 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -321,7 +321,9 @@ impl TorProvider for ArtiClientTorClient { // connect to onion service let arti_target = match target.clone() { TargetAddr::Ip(socket_addr) => socket_addr.into_tor_addr_dangerously(), - TargetAddr::Domain(domain, port) => (domain, port).into_tor_addr(), + TargetAddr::Domain(domain_addr) => { + (domain_addr.domain(), domain_addr.port()).into_tor_addr() + } TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3 { service_id, virt_port, diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 9cf1c96a2..ffc14dc52 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -357,7 +357,9 @@ impl TorProvider for LegacyTorClient { // our target let socks_target = match target.clone() { TargetAddr::Ip(socket_addr) => socks::TargetAddr::Ip(socket_addr), - TargetAddr::Domain(domain, port) => socks::TargetAddr::Domain(domain, port), + TargetAddr::Domain(domain_addr) => { + socks::TargetAddr::Domain(domain_addr.domain().to_string(), domain_addr.port()) + } TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3 { service_id, virt_port, diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index ebb0980a8..e94fba73c 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -1,8 +1,16 @@ // standard use std::boxed::Box; use std::io::{Read, Write}; -use std::net::TcpStream; +use std::net::{SocketAddr, TcpStream}; use std::ops::{Deref, DerefMut}; +use std::str::FromStr; +use std::sync::OnceLock; + +// extern crates +use domain::base::name::Name; +use idna::uts46::{Hyphens, Uts46}; +use idna::{domain_to_ascii_cow, AsciiDenyList}; +use regex::Regex; // internal crates use crate::tor_crypto::*; @@ -41,6 +49,39 @@ pub enum OnionAddr { V3(OnionAddrV3), } +#[derive(thiserror::Error, Debug)] +pub enum OnionAddrParseError { + #[error("Failed to parse '{0}' as OnionAddr")] + Generic(String), +} + +impl FromStr for OnionAddr { + type Err = OnionAddrParseError; + fn from_str(s: &str) -> Result { + static ONION_SERVICE_PATTERN: OnceLock = OnceLock::new(); + let onion_service_pattern = ONION_SERVICE_PATTERN.get_or_init(|| { + Regex::new(r"(?m)^(?P[a-z2-7]{56})\.onion:(?P[1-9][0-9]{0,4})$") + .unwrap() + }); + + if let Some(caps) = onion_service_pattern.captures(s.to_lowercase().as_ref()) { + let service_id = caps + .name("service_id") + .expect("missing service_id group") + .as_str() + .to_lowercase(); + let port = caps.name("port").expect("missing port group").as_str(); + if let (Ok(service_id), Ok(port)) = ( + V3OnionServiceId::from_string(service_id.as_ref()), + u16::from_str(port), + ) { + return Ok(OnionAddr::V3(OnionAddrV3::new(service_id, port))); + } + } + Err(Self::Err::Generic(s.to_string())) + } +} + impl std::fmt::Display for OnionAddr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -49,11 +90,76 @@ impl std::fmt::Display for OnionAddr { } } +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DomainAddr { + domain: String, + port: u16, +} + +impl DomainAddr { + pub fn domain(&self) -> &str { + self.domain.as_ref() + } + + pub fn port(&self) -> u16 { + self.port + } +} + +impl std::fmt::Display for DomainAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let uts46: Uts46 = Default::default(); + let (ui_str, _err) = uts46.to_user_interface( + self.domain.as_str().as_bytes(), + AsciiDenyList::URL, + Hyphens::Allow, + |_, _, _| -> bool { false }, + ); + write!(f, "{}:{}", ui_str, self.port) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum DomainAddrParseError { + #[error("Unable to parse '{0}' as DomainAddr")] + Generic(String), +} + +impl FromStr for DomainAddr { + type Err = DomainAddrParseError; + fn from_str(s: &str) -> Result { + static DOMAIN_PATTERN: OnceLock = OnceLock::new(); + let domain_pattern = DOMAIN_PATTERN + .get_or_init(|| Regex::new(r"(?m)^(?P.*):(?P[1-9][0-9]{0,4})$").unwrap()); + if let Some(caps) = domain_pattern.captures(s) { + let domain = caps + .name("domain") + .expect("missing service_id group") + .as_str(); + let port = caps.name("port").expect("missing port group").as_str(); + + if let (Ok(domain), Ok(port)) = ( + domain_to_ascii_cow(domain.as_bytes(), AsciiDenyList::URL), + u16::from_str(port), + ) { + let domain = domain.to_string(); + if let Ok(domain) = Name::>::from_str(domain.as_ref()) { + return Ok(Self { + domain: domain.to_string(), + port, + }); + } + } + } + Err(DomainAddrParseError::Generic(s.to_string())) + } +} + #[derive(Clone, Debug)] pub enum TargetAddr { Ip(std::net::SocketAddr), - Domain(String, u16), OnionService(OnionAddr), + Domain(DomainAddr), } impl From<(V3OnionServiceId, u16)> for TargetAddr { @@ -65,6 +171,28 @@ impl From<(V3OnionServiceId, u16)> for TargetAddr { } } +#[derive(thiserror::Error, Debug)] +pub enum TargetAddrParseError { + #[error("Unable to parse '{0}' as TargetAddr")] + Generic(String), +} + +impl FromStr for TargetAddr { + type Err = TargetAddrParseError; + fn from_str(s: &str) -> Result { + if let Ok(socket_addr) = SocketAddr::from_str(s) { + return Ok(TargetAddr::Ip(socket_addr)); + } else if let Ok(onion_addr) = OnionAddr::from_str(s) { + return Ok(TargetAddr::OnionService(onion_addr)); + } else if let Ok(domain_addr) = DomainAddr::from_str(s) { + if !domain_addr.domain().ends_with(".onion") { + return Ok(TargetAddr::Domain(domain_addr)); + } + } + Err(TargetAddrParseError::Generic(s.to_string())) + } +} + #[derive(Debug)] pub enum TorEvent { BootstrapStatus { diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 4b1d67320..a27397702 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -1,5 +1,6 @@ // stanndard use std::io::{Read, Write}; +use std::str::FromStr; #[cfg(feature = "arti-client-tor-provider")] use std::sync::Arc; @@ -477,3 +478,190 @@ fn test_mixed_legacy_arti_client_onion_service() -> anyhow::Result<()> { basic_onion_service_test(server_provider, client_provider) } + +#[test] +fn test_tor_provider_target_addr() -> anyhow::Result<()> { + let valid_ip_addr: &[&str] = &[ + "192.168.1.1:80", + "10.0.0.1:443", + "172.16.0.1:8080", + "8.8.8.8:53", + "255.255.255.255:65535", + "0.0.0.0:22", + "192.168.0.254:21", + "127.0.0.1:3306", + "1.1.1.1:123", + "224.0.0.1:554", + "169.254.0.1:179", + "203.0.113.1:80", + "198.51.100.1:443", + "100.64.0.1:8080", + "192.0.2.1:53", + "192.88.99.1:22", + "192.0.0.1:21", + "240.0.0.1:3306", + "198.18.0.1:123", + "233.252.0.1:554", + "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80", + "[2001:db8:85a3::8a2e:370:7334]:443", + "[::1]:8080", + "[::ffff:192.168.1.1]:53", + "[2001:0db8::1]:22", + "[fe80::1ff:fe23:4567:890a]:21", + "[2001:db8::1:0:0:1]:3306", + "[2001:0db8:0000:0042:0000:8a2e:0370:7334]:123", + "[ff02::1]:554", + "[fe80::abcd:ef01:2345:6789]:179", + "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80", + "[2001:db8:85a3::8a2e:370:7334]:443", + "[::1]:8080", + "[::ffff:c0a8:101]:53", + "[2001:db8::1:0:0:1]:22", + "[fe80::1ff:fe23:4567:890a]:21", + "[2001:db8:0000:0042:0000:8a2e:0370:7334]:3306", + "[ff02::1]:123", + "[fe80::abcd:ef01:2345:6789]:554", + "[2001:db8::1]:179", + ]; + + for target_addr_str in valid_ip_addr { + match TargetAddr::from_str(target_addr_str) { + Ok(TargetAddr::Ip(socket_addr)) => println!("{} => {}", target_addr_str, socket_addr), + Ok(TargetAddr::OnionService(onion_addr)) => panic!( + "unexpected conversion: {} => OnionService({})", + target_addr_str, onion_addr + ), + Ok(TargetAddr::Domain(domain_addr)) => panic!( + "unexpected conversion: {} => DomainAddr({})", + target_addr_str, domain_addr + ), + Err(err) => Err(err)?, + } + } + + let valid_onion_addr: &[&str] = &[ + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:65535", + "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCZSJYD.onion:1", + ]; + + for target_addr_str in valid_onion_addr { + match TargetAddr::from_str(target_addr_str) { + Ok(TargetAddr::Ip(socket_addr)) => panic!( + "unexpected conversion: {} => Ip({})", + target_addr_str, socket_addr + ), + Ok(TargetAddr::OnionService(onion_addr)) => { + println!("{} => {}", target_addr_str, onion_addr) + } + Ok(TargetAddr::Domain(domain_addr)) => panic!( + "unexpected conversion: {} => DomainAddr({})", + target_addr_str, domain_addr + ), + Err(err) => Err(err)?, + } + } + + let valid_domain_addr: &[&str] = &[ + "example.com:80", + "subdomain.example.com:443", + "xn--e1afmkfd.xn--p1ai:8080", // domain in Punycode for "пример.рф" + "xn--fsqu00a.xn--0zwm56d:53", // domain in Punycode for "例子.测试" + "münich.com:22", // domain with UTF-8 characters + "xn--mnich-kva.com:21", // Punycode for "münich.com" + "exämple.com:3306", // domain with UTF-8 characters + "xn--exmple-cua.com:123", // Punycode for "exämple.com" + "例子.com:554", // domain with UTF-8 characters + "xn--fsqu00a.com:179", // Punycode for "例子.com" + "täst.de:80", // domain with UTF-8 characters + "xn--tst-qla.de:443", // Punycode for "täst.de" + "xn--fiqs8s:80", // Punycode for "中国" + "xn--wgbh1c:8080", // Punycode for "مصر" + "münster.de:22", // domain with UTF-8 characters + "xn--mnster-3ya.de:21", // Punycode for "münster.de" + "bücher.com:3306", // domain with UTF-8 characters + "xn--bcher-kva.com:123", // Punycode for "bücher.com" + "xn--vermgensberatung-pwb.com:554", // Punycode for "vermögensberatung.com" + // Max Length + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:65535" + ]; + + for target_addr_str in valid_domain_addr { + match TargetAddr::from_str(target_addr_str) { + Ok(TargetAddr::Ip(socket_addr)) => panic!( + "unexpected conversion: {} => SocketAddr({})", + target_addr_str, socket_addr + ), + Ok(TargetAddr::OnionService(onion_addr)) => panic!( + "unexpected conversion: {} => OnionService({})", + target_addr_str, onion_addr + ), + Ok(TargetAddr::Domain(domain_addr)) => { + println!("{} => {}", target_addr_str, domain_addr) + } + Err(err) => Err(err)?, + } + } + + let invalid_target_addr: &[&str] = &[ + // ipv4-ish + "192.168.1.1:99999", // Port number out of range + "192.168.1.1:abc", // Invalid port number + "192.168.1.1:", // Missing port number + "192.168.1.1: 80", // Space in port number + "192.168.1.1:80a", // Non-numeric characters in port number + // ipv6-ish + "[2001:db8:::1]:80", // Triple colons + "[2001:db8:85a3::8a2e:370:7334:1234::abcd]:80", // Too many groups + "[2001:db8:85a3::8a2e:370g:7334]:80", // Invalid character in group + "[2001:db8:85a3::8a2e:370:7334]:99999", // Port number out of range + "[2001:db8:85a3:8a2e:370:7334]:80", // Missing double colons + "[::12345]:80", // Excessive leading zeroes + "[2001:db8:85a3::8a2e:370:7334:]:80", // Trailing colon + "[2001:db8:85a3::8a2e:370:7334]", // Missing port number + "2001:db8:85a3::8a2e:370:7334:80", // Missing square brackets + "[2001:db8:85a3::8a2e:370:7334]: 80", // Space in port number + "[2001:db8:85a3::8a2e:370:7334]:80a", // Non-numeric characters in port number + // onion service-ish + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd234567.onion:80", // Too long for v3 + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxcz.onion:443", // Too short for v3 + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:99999", // Port number out of range + "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrst.onion:21", // Invalid characters + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:abc", // Invalid port number + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion: 80", // Space in port number + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:80a", // Non-numeric characters in port number + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion:80", // Invalid service id + // domain-ish + "example..com:80", // Double dots + "exa mple.com:53", // Space in domain + "example.com:99999", // Port number out of range + "exaample.com:abc", // Invalid port number + "exaample.com:", // Missing port number + "exaample.com: 80", // Space in port number + "ex@mple.com:80", // Special character in domain + "example.com:80a", // Non-numeric characters in port number + "exämple..com:80", // UTF-8 with double dot + "xn--exmple-cua.com: 80", // Punycode with space in port number + "xn--exmple-cua.com:80a", // Punycode with non-numeric port + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com:65535", // Label too long + ]; + + for target_addr_str in invalid_target_addr { + match TargetAddr::from_str(target_addr_str) { + Ok(TargetAddr::Ip(socket_addr)) => panic!( + "unexpected conversion: {} => SocketAddr({})", + target_addr_str, socket_addr + ), + Ok(TargetAddr::OnionService(onion_addr)) => panic!( + "unexpected conversion: {} => OnionService({})", + target_addr_str, onion_addr + ), + Ok(TargetAddr::Domain(domain_addr)) => panic!( + "unexpected conversion: {} => DomainAddr({})", + target_addr_str, domain_addr + ), + Err(_) => (), + } + } + + Ok(()) +} From d6e25b6ca121318b4e327a0b1a7f6862f7633077 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 15 Jun 2024 04:40:55 +0000 Subject: [PATCH 086/184] tor-interface: implement TryFrom<(String, u16)> for DomainAddr --- tor-interface/src/tor_provider.rs | 38 +++++++++++++++++++------------ 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index e94fba73c..01b730bf9 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -1,5 +1,6 @@ // standard use std::boxed::Box; +use std::convert::TryFrom; use std::io::{Read, Write}; use std::net::{SocketAddr, TcpStream}; use std::ops::{Deref, DerefMut}; @@ -125,6 +126,24 @@ pub enum DomainAddrParseError { Generic(String), } +impl TryFrom<(String, u16)> for DomainAddr { + type Error = DomainAddrParseError; + + fn try_from(value: (String, u16)) -> Result { + let (domain, port) = (&value.0, value.1); + if let Ok(domain) = domain_to_ascii_cow(domain.as_bytes(), AsciiDenyList::URL) { + let domain = domain.to_string(); + if let Ok(domain) = Name::>::from_str(domain.as_ref()) { + return Ok(Self { + domain: domain.to_string(), + port, + }); + } + } + Err(DomainAddrParseError::Generic(format!("{}:{}", domain, port))) + } +} + impl FromStr for DomainAddr { type Err = DomainAddrParseError; fn from_str(s: &str) -> Result { @@ -134,21 +153,12 @@ impl FromStr for DomainAddr { if let Some(caps) = domain_pattern.captures(s) { let domain = caps .name("domain") - .expect("missing service_id group") - .as_str(); + .expect("missing domain group") + .as_str() + .to_string(); let port = caps.name("port").expect("missing port group").as_str(); - - if let (Ok(domain), Ok(port)) = ( - domain_to_ascii_cow(domain.as_bytes(), AsciiDenyList::URL), - u16::from_str(port), - ) { - let domain = domain.to_string(); - if let Ok(domain) = Name::>::from_str(domain.as_ref()) { - return Ok(Self { - domain: domain.to_string(), - port, - }); - } + if let Ok(port) = u16::from_str(port) { + return Self::try_from((domain, port)); } } Err(DomainAddrParseError::Generic(s.to_string())) From 90f85b8aaf40528e984d147f8214f87cb3b3e44e Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 15 Jun 2024 05:31:53 +0000 Subject: [PATCH 087/184] tor-interface: implement Display for TargetAddr --- tor-interface/src/tor_provider.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 01b730bf9..05cce5b3b 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -203,6 +203,16 @@ impl FromStr for TargetAddr { } } +impl std::fmt::Display for TargetAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TargetAddr::Ip(socket_addr) => socket_addr.fmt(f), + TargetAddr::OnionService(onion_addr) => onion_addr.fmt(f), + TargetAddr::Domain(domain_addr) => domain_addr.fmt(f), + } + } +} + #[derive(Debug)] pub enum TorEvent { BootstrapStatus { From 90beeb6ff88da138e6019dcbcf44f84caaa00df8 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 16 Jun 2024 00:09:35 +0000 Subject: [PATCH 088/184] tor-interface: allow TorProvider's to connect to non-onion service targets --- tor-interface/src/legacy_tor_controller.rs | 2 +- tor-interface/src/legacy_tor_process.rs | 4 ++-- tor-interface/src/mock_tor_client.rs | 19 ++++++++++++++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index 56d9e231b..99f86f1cb 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -717,7 +717,7 @@ fn test_tor_controller() -> anyhow::Result<()> { let vals = tor_controller.getconf(&["SocksPort", "AvoidDiskWrites", "DisableNetwork"])?; for (key, value) in vals.iter() { let expected = match key.as_str() { - "SocksPort" => "auto OnionTrafficOnly", + "SocksPort" => "auto", "AvoidDiskWrites" => "1", "DisableNetwork" => "1", _ => panic!("unexpected returned key: {}", key), diff --git a/tor-interface/src/legacy_tor_process.rs b/tor-interface/src/legacy_tor_process.rs index 351c67498..d6e9f8a71 100644 --- a/tor-interface/src/legacy_tor_process.rs +++ b/tor-interface/src/legacy_tor_process.rs @@ -202,11 +202,11 @@ impl LegacyTorProcess { // TODO: should we nuke the existing torrc between runs? Do we want // users setting custom nonsense in there? // construct default torrc - // - daemon determines socks port and only allows clients to connect to onion services + // - daemon determines socks port // - minimize writes to disk // - start with network disabled by default if !default_torrc.exists() { - const DEFAULT_TORRC_CONTENT: &str = "SocksPort auto OnionTrafficOnly\n\ + const DEFAULT_TORRC_CONTENT: &str = "SocksPort auto\n\ AvoidDiskWrites 1\n\ DisableNetwork 1\n\n"; diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index b61425f92..45e86b2b5 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -35,6 +35,9 @@ pub enum Error { #[error("unable to get TCP listener's local adress")] TcpListenerLocalAddrFailed(#[source] std::io::Error), + #[error("unable to connect to {}", .0)] + ConnectFailed(TargetAddr), + #[error("not implemented")] NotImplemented(), } @@ -163,6 +166,7 @@ pub struct MockTorClient { bootstrapped: bool, client_auth_keys: BTreeMap, onion_services: Vec<(OnionAddr, Arc)>, + loopback: TcpListener, } impl MockTorClient { @@ -171,11 +175,16 @@ impl MockTorClient { let line = "[notice] MockTorClient running".to_string(); events.push(TorEvent::LogReceived { line }); + + let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); + let listener = TcpListener::bind(socket_addr).expect("tcplistener bind failed"); + MockTorClient { events, bootstrapped: false, client_auth_keys: Default::default(), onion_services: Default::default(), + loopback: listener, } } } @@ -266,7 +275,15 @@ impl TorProvider for MockTorClient { service_id, virt_port, })) => (service_id, virt_port), - _ => return Err(Error::NotImplemented().into()), + target_address => if let Ok(stream) = TcpStream::connect(self.loopback.local_addr().expect("loopback local_addr failed")) { + return Ok(OnionStream { + stream, + local_addr: None, + peer_addr: Some(target_address), + }); + } else { + return Err(Error::ConnectFailed(target_address).into()); + }, }; let client_auth = self.client_auth_keys.get(&service_id); From 9555448c9367911e5f5596dfbf826ae0611361ca Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 16 Jun 2024 22:21:44 +0000 Subject: [PATCH 089/184] tor-interface: LegacyTorClient now takes a LegacyTorClientConfig --- tor-interface/src/legacy_tor_client.rs | 131 +++++++++++++++---------- tor-interface/tests/tor_provider.rs | 45 +++++++-- 2 files changed, 118 insertions(+), 58 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index ffc14dc52..1a34ea8f8 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -7,7 +7,7 @@ use std::io::ErrorKind; use std::net::{SocketAddr, TcpListener}; use std::ops::Drop; use std::option::Option; -use std::path::Path; +use std::path::PathBuf; use std::string::ToString; use std::sync::{atomic, Arc}; use std::time::Duration; @@ -82,6 +82,9 @@ pub enum Error { #[error("failed to create onion service")] AddOnionFailed(#[source] crate::legacy_tor_controller::Error), + + #[error("not implemented")] + NotImplemented(), } impl From for crate::tor_provider::Error { @@ -154,6 +157,27 @@ impl Drop for LegacyOnionListener { } } +// +// LegacyTorClientConfig +// + +#[derive(Clone)] +pub enum LegacyTorClientConfig { + SystemTor { + tor_socks_addr: SocketAddr, + tor_control_addr: SocketAddr, + tor_control_passwd: String, + }, + BundledTor { + tor_bin_path: PathBuf, + data_directory: PathBuf, + }, +} + +// +// LegacyTorClient +// + pub struct LegacyTorClient { daemon: LegacyTorProcess, version: LegacyTorVersion, @@ -167,58 +191,63 @@ pub struct LegacyTorClient { } impl LegacyTorClient { - pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { - // launch tor - let daemon = LegacyTorProcess::new(tor_bin_path, data_directory) - .map_err(Error::LegacyTorProcessCreationFailed)?; - // open a control stream - let control_stream = - LegacyControlStream::new(daemon.get_control_addr(), Duration::from_millis(16)) - .map_err(Error::LegacyControlStreamCreationFailed)?; - - // create a controler - let mut controller = LegacyTorController::new(control_stream) - .map_err(Error::LegacyTorControllerCreationFailed)?; - - // authenticate - controller - .authenticate(daemon.get_password()) - .map_err(Error::LegacyTorProcessAuthenticationFailed)?; - - // min required version for v3 client auth (see control-spec.txt) - let min_required_version = LegacyTorVersion { - major: 0u32, - minor: 4u32, - micro: 6u32, - patch_level: 1u32, - status_tag: None, - }; - - let version = controller - .getinfo_version() - .map_err(Error::GetInfoVersionFailed)?; + pub fn new(config: LegacyTorClientConfig) -> Result { + match config { + LegacyTorClientConfig::BundledTor{tor_bin_path, data_directory} => { + // launch tor + let daemon = LegacyTorProcess::new(tor_bin_path.as_path(), data_directory.as_path()) + .map_err(Error::LegacyTorProcessCreationFailed)?; + // open a control stream + let control_stream = + LegacyControlStream::new(daemon.get_control_addr(), Duration::from_millis(16)) + .map_err(Error::LegacyControlStreamCreationFailed)?; + + // create a controler + let mut controller = LegacyTorController::new(control_stream) + .map_err(Error::LegacyTorControllerCreationFailed)?; + + // authenticate + controller + .authenticate(daemon.get_password()) + .map_err(Error::LegacyTorProcessAuthenticationFailed)?; + + // min required version for v3 client auth (see control-spec.txt) + let min_required_version = LegacyTorVersion { + major: 0u32, + minor: 4u32, + micro: 6u32, + patch_level: 1u32, + status_tag: None, + }; + + let version = controller + .getinfo_version() + .map_err(Error::GetInfoVersionFailed)?; + + if version < min_required_version { + return Err(Error::LegacyTorProcessTooOld( + version.to_string(), + min_required_version.to_string(), + )); + } - if version < min_required_version { - return Err(Error::LegacyTorProcessTooOld( - version.to_string(), - min_required_version.to_string(), - )); + // register for STATUS_CLIENT async events + controller + .setevents(&["STATUS_CLIENT", "HS_DESC"]) + .map_err(Error::SetEventsFailed)?; + + Ok(LegacyTorClient { + daemon, + version, + controller, + socks_listener: None, + onion_services: Default::default(), + circuit_token_counter: 0usize, + circuit_tokens: Default::default(), + }) + }, + LegacyTorClientConfig::SystemTor{..} => Err(Error::NotImplemented()) } - - // register for STATUS_CLIENT async events - controller - .setevents(&["STATUS_CLIENT", "HS_DESC"]) - .map_err(Error::SetEventsFailed)?; - - Ok(LegacyTorClient { - daemon, - version, - controller, - socks_listener: None, - onion_services: Default::default(), - circuit_token_counter: 0usize, - circuit_tokens: Default::default(), - }) } #[allow(dead_code)] diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index a27397702..2ca6ae11a 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -351,7 +351,12 @@ fn test_legacy_bootstrap() -> anyhow::Result<()> { let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_bootstrap"); - bootstrap_test(Box::new(LegacyTorClient::new(&tor_path, &data_path)?)) + let tor_config = LegacyTorClientConfig::BundledTor{ + tor_bin_path: tor_path, + data_directory: data_path, + }; + + bootstrap_test(Box::new(LegacyTorClient::new(tor_config)?)) } #[test] @@ -362,11 +367,19 @@ fn test_legacy_onion_service() -> anyhow::Result<()> { let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_onion_service_server"); - let server_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); + let tor_config = LegacyTorClientConfig::BundledTor{ + tor_bin_path: tor_path.clone(), + data_directory: data_path, + }; + let server_provider = Box::new(LegacyTorClient::new(tor_config)?); let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_onion_service_cient"); - let client_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); + let tor_config = LegacyTorClientConfig::BundledTor{ + tor_bin_path: tor_path, + data_directory: data_path, + }; + let client_provider = Box::new(LegacyTorClient::new(tor_config)?); basic_onion_service_test(server_provider, client_provider) } @@ -379,11 +392,20 @@ fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_authenticated_onion_service_server"); - let server_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); + let tor_config = LegacyTorClientConfig::BundledTor{ + tor_bin_path: tor_path.clone(), + data_directory: data_path, + }; + let server_provider = Box::new(LegacyTorClient::new(tor_config)?); let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_authenticated_onion_service_cient"); - let client_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); + let tor_config = LegacyTorClientConfig::BundledTor{ + tor_bin_path: tor_path, + data_directory: data_path, + }; + let client_provider = Box::new(LegacyTorClient::new(tor_config)?); + authenticated_onion_service_test(server_provider, client_provider) } @@ -456,7 +478,11 @@ fn test_mixed_arti_client_legacy_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); data_path.push("test_arti_legacy_basic_onion_service_client"); - let client_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); + let tor_config = LegacyTorClientConfig::BundledTor{ + tor_bin_path: tor_path, + data_directory: data_path, + }; + let client_provider = Box::new(LegacyTorClient::new(tor_config)?); basic_onion_service_test(server_provider, client_provider) } @@ -466,9 +492,14 @@ fn test_mixed_arti_client_legacy_onion_service() -> anyhow::Result<()> { #[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] fn test_mixed_legacy_arti_client_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_arty_basic_onion_service_client"); - let server_provider = Box::new(LegacyTorClient::new(&tor_path, &data_path)?); + let tor_config = LegacyTorClientConfig::BundledTor{ + tor_bin_path: tor_path, + data_directory: data_path, + }; + let server_provider = Box::new(LegacyTorClient::new(tor_config)?); let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); From 402b8431ef447f9f0feb14286640ed5d0fbdd663 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 16 Jun 2024 22:53:53 +0000 Subject: [PATCH 090/184] tor-interface: refactor to make daemon process optional --- tor-interface/src/legacy_tor_client.rs | 100 +++++++++++++------------ 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 1a34ea8f8..b177ef9bb 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -179,7 +179,7 @@ pub enum LegacyTorClientConfig { // pub struct LegacyTorClient { - daemon: LegacyTorProcess, + daemon: Option, version: LegacyTorVersion, controller: LegacyTorController, socks_listener: Option, @@ -192,7 +192,7 @@ pub struct LegacyTorClient { impl LegacyTorClient { pub fn new(config: LegacyTorClientConfig) -> Result { - match config { + let (daemon, mut controller, password) = match config { LegacyTorClientConfig::BundledTor{tor_bin_path, data_directory} => { // launch tor let daemon = LegacyTorProcess::new(tor_bin_path.as_path(), data_directory.as_path()) @@ -203,51 +203,55 @@ impl LegacyTorClient { .map_err(Error::LegacyControlStreamCreationFailed)?; // create a controler - let mut controller = LegacyTorController::new(control_stream) + let controller = LegacyTorController::new(control_stream) .map_err(Error::LegacyTorControllerCreationFailed)?; - // authenticate - controller - .authenticate(daemon.get_password()) - .map_err(Error::LegacyTorProcessAuthenticationFailed)?; - - // min required version for v3 client auth (see control-spec.txt) - let min_required_version = LegacyTorVersion { - major: 0u32, - minor: 4u32, - micro: 6u32, - patch_level: 1u32, - status_tag: None, - }; - - let version = controller - .getinfo_version() - .map_err(Error::GetInfoVersionFailed)?; - - if version < min_required_version { - return Err(Error::LegacyTorProcessTooOld( - version.to_string(), - min_required_version.to_string(), - )); - } - - // register for STATUS_CLIENT async events - controller - .setevents(&["STATUS_CLIENT", "HS_DESC"]) - .map_err(Error::SetEventsFailed)?; - - Ok(LegacyTorClient { - daemon, - version, - controller, - socks_listener: None, - onion_services: Default::default(), - circuit_token_counter: 0usize, - circuit_tokens: Default::default(), - }) + let password = daemon.get_password().to_string(); + (Some(daemon), controller, password) }, - LegacyTorClientConfig::SystemTor{..} => Err(Error::NotImplemented()) + LegacyTorClientConfig::SystemTor{..} => return Err(Error::NotImplemented()) + }; + + // authenticate + controller + .authenticate(&password) + .map_err(Error::LegacyTorProcessAuthenticationFailed)?; + + // min required version for v3 client auth (see control-spec.txt) + let min_required_version = LegacyTorVersion { + major: 0u32, + minor: 4u32, + micro: 6u32, + patch_level: 1u32, + status_tag: None, + }; + + // verify version is recent enough + let version = controller + .getinfo_version() + .map_err(Error::GetInfoVersionFailed)?; + + if version < min_required_version { + return Err(Error::LegacyTorProcessTooOld( + version.to_string(), + min_required_version.to_string(), + )); } + + // register for STATUS_CLIENT async events + controller + .setevents(&["STATUS_CLIENT", "HS_DESC"]) + .map_err(Error::SetEventsFailed)?; + + Ok(LegacyTorClient { + daemon, + version, + controller, + socks_listener: None, + onion_services: Default::default(), + circuit_token_counter: 0usize, + circuit_tokens: Default::default(), + }) } #[allow(dead_code)] @@ -324,10 +328,12 @@ impl TorProvider for LegacyTorClient { } } - for log_line in self.daemon.wait_log_lines().iter_mut() { - events.push(TorEvent::LogReceived { - line: std::mem::take(log_line), - }); + if let Some(daemon) = &mut self.daemon { + for log_line in daemon.wait_log_lines().iter_mut() { + events.push(TorEvent::LogReceived { + line: std::mem::take(log_line), + }); + } } Ok(events) From aefa8cad16d5f0fb4d8143d9e91aa5ccf8c15b63 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 16 Jun 2024 23:12:20 +0000 Subject: [PATCH 091/184] tor-interface: refactor to make socks_listener optionally setable in LegacyTorClient::new() --- tor-interface/src/legacy_tor_client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index b177ef9bb..201e9edf3 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -192,7 +192,7 @@ pub struct LegacyTorClient { impl LegacyTorClient { pub fn new(config: LegacyTorClientConfig) -> Result { - let (daemon, mut controller, password) = match config { + let (daemon, mut controller, password, socks_listener) = match config { LegacyTorClientConfig::BundledTor{tor_bin_path, data_directory} => { // launch tor let daemon = LegacyTorProcess::new(tor_bin_path.as_path(), data_directory.as_path()) @@ -207,7 +207,7 @@ impl LegacyTorClient { .map_err(Error::LegacyTorControllerCreationFailed)?; let password = daemon.get_password().to_string(); - (Some(daemon), controller, password) + (Some(daemon), controller, password, None) }, LegacyTorClientConfig::SystemTor{..} => return Err(Error::NotImplemented()) }; @@ -247,7 +247,7 @@ impl LegacyTorClient { daemon, version, controller, - socks_listener: None, + socks_listener, onion_services: Default::default(), circuit_token_counter: 0usize, circuit_tokens: Default::default(), From 7e3daa8446da597dadc2597d282650d9cd458c79 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 16 Jun 2024 23:27:47 +0000 Subject: [PATCH 092/184] tor-interface: explcitly save off bootstrap status and check in LegacyTorProvider connect() and listen() methods --- tor-interface/src/legacy_tor_client.rs | 28 ++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 201e9edf3..8ab754bb6 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -83,6 +83,9 @@ pub enum Error { #[error("failed to create onion service")] AddOnionFailed(#[source] crate::legacy_tor_controller::Error), + #[error("tor not bootstrapped")] + LegacyTorNotBootstrapped(), + #[error("not implemented")] NotImplemented(), } @@ -182,6 +185,7 @@ pub struct LegacyTorClient { daemon: Option, version: LegacyTorVersion, controller: LegacyTorController, + bootstrapped: bool, socks_listener: Option, // list of open onion services and their is_active flag onion_services: Vec<(V3OnionServiceId, Arc)>, @@ -192,7 +196,7 @@ pub struct LegacyTorClient { impl LegacyTorClient { pub fn new(config: LegacyTorClientConfig) -> Result { - let (daemon, mut controller, password, socks_listener) = match config { + let (daemon, mut controller, password, bootstrapped, socks_listener) = match config { LegacyTorClientConfig::BundledTor{tor_bin_path, data_directory} => { // launch tor let daemon = LegacyTorProcess::new(tor_bin_path.as_path(), data_directory.as_path()) @@ -207,7 +211,7 @@ impl LegacyTorClient { .map_err(Error::LegacyTorControllerCreationFailed)?; let password = daemon.get_password().to_string(); - (Some(daemon), controller, password, None) + (Some(daemon), controller, password, false, None) }, LegacyTorClientConfig::SystemTor{..} => return Err(Error::NotImplemented()) }; @@ -247,6 +251,7 @@ impl LegacyTorClient { daemon, version, controller, + bootstrapped, socks_listener, onion_services: Default::default(), circuit_token_counter: 0usize, @@ -309,6 +314,7 @@ impl TorProvider for LegacyTorClient { }); if progress == 100u32 { events.push(TorEvent::BootstrapComplete); + self.bootstrapped = true; } } } @@ -340,10 +346,12 @@ impl TorProvider for LegacyTorClient { } fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { - Ok(self - .controller - .setconf(&[("DisableNetwork", "0")]) - .map_err(Error::SetConfDisableNetwork0Failed)?) + if !self.bootstrapped { + self.controller + .setconf(&[("DisableNetwork", "0")]) + .map_err(Error::SetConfDisableNetwork0Failed)?; + } + Ok(()) } fn add_client_auth( @@ -373,6 +381,10 @@ impl TorProvider for LegacyTorClient { target: TargetAddr, circuit: Option, ) -> Result { + if !self.bootstrapped { + return Err(Error::LegacyTorNotBootstrapped().into()); + } + if self.socks_listener.is_none() { let mut listeners = self .controller @@ -433,6 +445,10 @@ impl TorProvider for LegacyTorClient { virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, ) -> Result { + if !self.bootstrapped { + return Err(Error::LegacyTorNotBootstrapped().into()); + } + // try to bind to a local address, let OS pick our port let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; From b6618f159099bb7cf73aaca8b75b36ac086a7a35 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 17 Jun 2024 01:29:41 +0000 Subject: [PATCH 093/184] tor-interface: add support for manually specifying socks address, control port address, and control password --- tor-interface/CMakeLists.txt | 8 ++ tor-interface/src/legacy_tor_client.rs | 24 ++++- tor-interface/tests/tor_provider.rs | 125 +++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 4 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index f6ec28c5d..f4e980f58 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -79,6 +79,14 @@ if (ENABLE_TESTS) COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_legacy_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) + add_test(NAME tor_interface_system_legacy_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_system_legacy_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_system_legacy_authenticated_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_system_legacy_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) endif() if (ENABLE_ARTI_CLIENT_TOR_PROVIDER) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 8ab754bb6..356ddcf90 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -196,7 +196,7 @@ pub struct LegacyTorClient { impl LegacyTorClient { pub fn new(config: LegacyTorClientConfig) -> Result { - let (daemon, mut controller, password, bootstrapped, socks_listener) = match config { + let (daemon, mut controller, password, socks_listener) = match config { LegacyTorClientConfig::BundledTor{tor_bin_path, data_directory} => { // launch tor let daemon = LegacyTorProcess::new(tor_bin_path.as_path(), data_directory.as_path()) @@ -211,9 +211,20 @@ impl LegacyTorClient { .map_err(Error::LegacyTorControllerCreationFailed)?; let password = daemon.get_password().to_string(); - (Some(daemon), controller, password, false, None) + (Some(daemon), controller, password, None) + }, + LegacyTorClientConfig::SystemTor{tor_socks_addr, tor_control_addr, tor_control_passwd} => { + // open a control stream + let control_stream = + LegacyControlStream::new(&tor_control_addr, Duration::from_millis(16)) + .map_err(Error::LegacyControlStreamCreationFailed)?; + + // create a controler + let controller = LegacyTorController::new(control_stream) + .map_err(Error::LegacyTorControllerCreationFailed)?; + + (None, controller, tor_control_passwd, Some(tor_socks_addr)) }, - LegacyTorClientConfig::SystemTor{..} => return Err(Error::NotImplemented()) }; // authenticate @@ -251,7 +262,7 @@ impl LegacyTorClient { daemon, version, controller, - bootstrapped, + bootstrapped: false, socks_listener, onion_services: Default::default(), circuit_token_counter: 0usize, @@ -335,11 +346,16 @@ impl TorProvider for LegacyTorClient { } if let Some(daemon) = &mut self.daemon { + // bundled tor gives us log-lines for log_line in daemon.wait_log_lines().iter_mut() { events.push(TorEvent::LogReceived { line: std::mem::take(log_line), }); } + } else if !self.bootstrapped { + // system tor needs to send a bootstrap complete event *once* + events.push(TorEvent::BootstrapComplete); + self.bootstrapped = true; } Ok(events) diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 2ca6ae11a..481877b45 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -1,5 +1,11 @@ // stanndard +#[cfg(feature = "legacy-tor-provider")] +use std::fs::File; use std::io::{Read, Write}; +#[cfg(feature = "legacy-tor-provider")] +use std::process; +#[cfg(feature = "legacy-tor-provider")] +use std::process::{Child, Command, Stdio}; use std::str::FromStr; #[cfg(feature = "arti-client-tor-provider")] use std::sync::Arc; @@ -410,6 +416,125 @@ fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { authenticated_onion_service_test(server_provider, client_provider) } +// +// System Legacy TorProvider tests +// + +#[cfg(test)] +fn start_system_tor_daemon(tor_path: &std::ffi::OsStr, name: &str, control_port: u16, socks_port: u16) -> anyhow::Result { + + let mut data_path = std::env::temp_dir(); + data_path.push(name); + std::fs::create_dir_all(&data_path)?; + let default_torrc = data_path.join("default_torrc"); + { let _ = File::create(&default_torrc)?; } + let torrc = data_path.join("torrc"); + { let _ = File::create(&torrc)?; } + + let tor_daemon = Command::new(tor_path) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + // point to our above written torrc file + .arg("--defaults-torrc") + .arg(default_torrc) + // location of torrc + .arg("--torrc-file") + .arg(torrc) + // enable networking + .arg("DisableNetwork") + .arg("0") + // root data directory + .arg("DataDirectory") + .arg(data_path) + // daemon will assign us a port, and we will + // read it from the control port file + .arg("ControlPort") + .arg(control_port.to_string()) + // password: foobar1 + .arg("HashedControlPassword") + .arg("16:E807DCE69AFE9979600760C9758B95ADB2F95E8740478AEA5356C95358") + // socks port + .arg("SocksPort") + .arg(socks_port.to_string()) + // tor process will shut down after this process shuts down + // to avoid orphaned tor daemon + .arg("__OwningControllerProcess") + .arg(process::id().to_string()) + .spawn()?; + + + Ok(tor_daemon) +} + +#[test] +#[serial] +#[cfg(feature = "legacy-tor-provider")] +fn test_system_legacy_onion_service() -> anyhow::Result<()> { + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + + let mut server_tor_daemon = start_system_tor_daemon(tor_path.as_os_str(), "test_system_legacy_onion_service_server", 9251u16, 9250u16)?; + let mut client_tor_daemon = start_system_tor_daemon(tor_path.as_os_str(), "test_system_legacy_onion_service_client", 9351u16, 9350u16)?; + + // give daemons time to start + std::thread::sleep(std::time::Duration::from_secs(5)); + + let tor_config = LegacyTorClientConfig::SystemTor{ + tor_socks_addr: std::net::SocketAddr::from_str("127.0.0.1:9250")?, + tor_control_addr: std::net::SocketAddr::from_str("127.0.0.1:9251")?, + tor_control_passwd: "password".to_string(), + }; + let server_provider = Box::new(LegacyTorClient::new(tor_config)?); + + let tor_config = LegacyTorClientConfig::SystemTor{ + tor_socks_addr: std::net::SocketAddr::from_str("127.0.0.1:9350")?, + tor_control_addr: std::net::SocketAddr::from_str("127.0.0.1:9351")?, + tor_control_passwd: "password".to_string(), + }; + let client_provider = Box::new(LegacyTorClient::new(tor_config)?); + + basic_onion_service_test(server_provider, client_provider)?; + + server_tor_daemon.kill()?; + client_tor_daemon.kill()?; + + Ok(()) +} + +#[test] +#[serial] +#[cfg(feature = "legacy-tor-provider")] +fn test_system_legacy_authenticated_onion_service() -> anyhow::Result<()> { + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + + let mut server_tor_daemon = start_system_tor_daemon(tor_path.as_os_str(), "test_system_legacy_authenticated_onion_service_server", 9251u16, 9250u16)?; + let mut client_tor_daemon = start_system_tor_daemon(tor_path.as_os_str(), "test_system_legacy_authenticated_onion_service_client", 9351u16, 9350u16)?; + + // give daemons time to start + std::thread::sleep(std::time::Duration::from_secs(5)); + + let tor_config = LegacyTorClientConfig::SystemTor{ + tor_socks_addr: std::net::SocketAddr::from_str("127.0.0.1:9250")?, + tor_control_addr: std::net::SocketAddr::from_str("127.0.0.1:9251")?, + tor_control_passwd: "password".to_string(), + }; + let server_provider = Box::new(LegacyTorClient::new(tor_config)?); + + let tor_config = LegacyTorClientConfig::SystemTor{ + tor_socks_addr: std::net::SocketAddr::from_str("127.0.0.1:9350")?, + tor_control_addr: std::net::SocketAddr::from_str("127.0.0.1:9351")?, + tor_control_passwd: "password".to_string(), + }; + let client_provider = Box::new(LegacyTorClient::new(tor_config)?); + + authenticated_onion_service_test(server_provider, client_provider)?; + + server_tor_daemon.kill()?; + client_tor_daemon.kill()?; + + Ok(()) +} + // // Arti TorProvider tests // From b9d27ba21c68c5fc3aee5e63e9a7febaf28e6e9f Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 23 Jun 2024 18:56:01 +0000 Subject: [PATCH 094/184] tor-interface: removed spurious newline from default_torrc --- tor-interface/src/legacy_tor_process.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/src/legacy_tor_process.rs b/tor-interface/src/legacy_tor_process.rs index d6e9f8a71..9e1a9fde2 100644 --- a/tor-interface/src/legacy_tor_process.rs +++ b/tor-interface/src/legacy_tor_process.rs @@ -208,7 +208,7 @@ impl LegacyTorProcess { if !default_torrc.exists() { const DEFAULT_TORRC_CONTENT: &str = "SocksPort auto\n\ AvoidDiskWrites 1\n\ - DisableNetwork 1\n\n"; + DisableNetwork 1\n"; let mut default_torrc_file = File::create(&default_torrc).map_err(Error::DefaultTorrcFileCreationFailed)?; From 3f1b410ec51d40743c58774826379d05bb4d2eb9 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 22 Jun 2024 03:02:19 +0000 Subject: [PATCH 095/184] tor-interface: added missing catchall test --- tor-interface/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index f4e980f58..dc825cbab 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -123,6 +123,11 @@ if (ENABLE_TESTS) WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) + # catchall + add_test(NAME tor_interface_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) endif() # From c36ddfaacbe2f577d901cb64321ac1854d1862e9 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 22 Jun 2024 00:18:01 +0000 Subject: [PATCH 096/184] tor-interface: update signature of SETCONF related functions in LegacyTorController --- tor-interface/src/legacy_tor_client.rs | 2 +- tor-interface/src/legacy_tor_controller.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 356ddcf90..7fa6bd055 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -364,7 +364,7 @@ impl TorProvider for LegacyTorClient { fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { if !self.bootstrapped { self.controller - .setconf(&[("DisableNetwork", "0")]) + .setconf(&[("DisableNetwork", "0".to_string())]) .map_err(Error::SetConfDisableNetwork0Failed)?; } Ok(()) diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index 99f86f1cb..f457db43b 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -249,7 +249,7 @@ impl LegacyTorController { // // SETCONF (3.1) - fn setconf_cmd(&mut self, key_values: &[(&str, &str)]) -> Result { + fn setconf_cmd(&mut self, key_values: &[(&str, String)]) -> Result { if key_values.is_empty() { return Err(Error::InvalidCommandArguments( "SETCONF key-value pairs list must not be empty".to_string(), @@ -258,7 +258,7 @@ impl LegacyTorController { let mut command_buffer = vec!["SETCONF".to_string()]; for (key, value) in key_values.iter() { - command_buffer.push(format!("{}={}", key, value)); + command_buffer.push(format!("{}=\"{}\"", key, value.trim())); } let command = command_buffer.join(" "); @@ -425,7 +425,7 @@ impl LegacyTorController { // Public high-level typesafe command method wrappers // - pub fn setconf(&mut self, key_values: &[(&str, &str)]) -> Result<(), Error> { + pub fn setconf(&mut self, key_values: &[(&str, String)]) -> Result<(), Error> { let reply = self.setconf_cmd(key_values)?; match reply.status_code { @@ -748,7 +748,7 @@ fn test_tor_controller() -> anyhow::Result<()> { tor_controller.setevents(&["STATUS_CLIENT"])?; // begin bootstrap - tor_controller.setconf(&[("DisableNetwork", "0")])?; + tor_controller.setconf(&[("DisableNetwork", "0".to_string())])?; // add an onoin service let (private_key, service_id) = From d74e068d5caac29e7360699ffa50942f676ddfb0 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Tue, 25 Jun 2024 01:44:15 +0000 Subject: [PATCH 097/184] tor-interface: set legacy tor daemon working directory to its data directory --- tor-interface/src/legacy_tor_process.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tor-interface/src/legacy_tor_process.rs b/tor-interface/src/legacy_tor_process.rs index 9e1a9fde2..b37affa19 100644 --- a/tor-interface/src/legacy_tor_process.rs +++ b/tor-interface/src/legacy_tor_process.rs @@ -235,6 +235,8 @@ impl LegacyTorProcess { .stdout(Stdio::piped()) .stdin(Stdio::null()) .stderr(Stdio::null()) + // set working directory to data directory + .current_dir(data_directory) // point to our above written torrc file .arg("--defaults-torrc") .arg(default_torrc) From 7a3a62be85182424bdf10c7737b8cff501d0daec Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Tue, 2 Jul 2024 17:55:46 +0000 Subject: [PATCH 098/184] tor-interface: fix bug where control-port QuotedStrings were not escaping backslash or quotes --- tor-interface/src/legacy_tor_controller.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index f457db43b..d305a9efc 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -89,6 +89,12 @@ pub(crate) struct LegacyTorController { hs_desc_pattern: Regex, } +fn quoted_string(string: &str) -> String { + // replace \ with \\ and " with \" + // see: https://spec.torproject.org/control-spec/message-format.html?highlight=QuotedString#description-format + string.replace("\\", "\\\\").replace("\"", "\\\"") +} + impl LegacyTorController { pub fn new(control_stream: LegacyControlStream) -> Result { let status_event_pattern = @@ -258,7 +264,7 @@ impl LegacyTorController { let mut command_buffer = vec!["SETCONF".to_string()]; for (key, value) in key_values.iter() { - command_buffer.push(format!("{}=\"{}\"", key, value.trim())); + command_buffer.push(format!("{}=\"{}\"", key, quoted_string(value.trim()))); } let command = command_buffer.join(" "); @@ -292,7 +298,7 @@ impl LegacyTorController { // AUTHENTICATE (3.5) fn authenticate_cmd(&mut self, password: &str) -> Result { - let command = format!("AUTHENTICATE \"{}\"", password); + let command = format!("AUTHENTICATE \"{}\"", quoted_string(password)); self.write_command(&command) } From d02977a30fef6eeeecd03bbfa1855a099128b762 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 21 Jun 2024 20:45:48 +0000 Subject: [PATCH 099/184] tor-interface: stubbed out additional configuration options for bundled LegacyTorClient --- tor-interface/src/legacy_tor_client.rs | 26 ++++++---- tor-interface/src/tor_provider.rs | 70 ++++++++++++++++++++++++++ tor-interface/tests/tor_provider.rs | 28 +++++++++++ 3 files changed, 115 insertions(+), 9 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 7fa6bd055..843fe9328 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -50,7 +50,7 @@ pub enum Error { #[error("failed to delete unused onion service")] DelOnionFailed(#[source] crate::legacy_tor_controller::Error), - #[error("failed waiting for async events")] + #[error("failed waiting for async events: {0}")] WaitAsyncEventsFailed(#[source] crate::legacy_tor_controller::Error), #[error("failed to begin bootstrap")] @@ -164,17 +164,21 @@ impl Drop for LegacyOnionListener { // LegacyTorClientConfig // -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum LegacyTorClientConfig { + BundledTor { + tor_bin_path: PathBuf, + data_directory: PathBuf, + proxy_settings: Option, + allowed_ports: Option>, + pluggable_transports: Option>, + bridge_lines: Option>, + }, SystemTor { tor_socks_addr: SocketAddr, tor_control_addr: SocketAddr, tor_control_passwd: String, }, - BundledTor { - tor_bin_path: PathBuf, - data_directory: PathBuf, - }, } // @@ -196,8 +200,8 @@ pub struct LegacyTorClient { impl LegacyTorClient { pub fn new(config: LegacyTorClientConfig) -> Result { - let (daemon, mut controller, password, socks_listener) = match config { - LegacyTorClientConfig::BundledTor{tor_bin_path, data_directory} => { + let (daemon, mut controller, password, socks_listener) = match &config { + LegacyTorClientConfig::BundledTor{tor_bin_path, data_directory, ..} => { // launch tor let daemon = LegacyTorProcess::new(tor_bin_path.as_path(), data_directory.as_path()) .map_err(Error::LegacyTorProcessCreationFailed)?; @@ -223,7 +227,7 @@ impl LegacyTorClient { let controller = LegacyTorController::new(control_stream) .map_err(Error::LegacyTorControllerCreationFailed)?; - (None, controller, tor_control_passwd, Some(tor_socks_addr)) + (None, controller, tor_control_passwd.clone(), Some(tor_socks_addr.clone())) }, }; @@ -253,6 +257,10 @@ impl LegacyTorClient { )); } + if let LegacyTorClientConfig::BundledTor{proxy_settings: _, allowed_ports: _, pluggable_transports: _, bridge_lines: _, ..} = config { + + } + // register for STATUS_CLIENT async events controller .setevents(&["STATUS_CLIENT", "HS_DESC"]) diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 05cce5b3b..639670087 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -4,6 +4,7 @@ use std::convert::TryFrom; use std::io::{Read, Write}; use std::net::{SocketAddr, TcpStream}; use std::ops::{Deref, DerefMut}; +use std::path::PathBuf; use std::str::FromStr; use std::sync::OnceLock; @@ -16,6 +17,10 @@ use regex::Regex; // internal crates use crate::tor_crypto::*; +// +// OnionAddr +// + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct OnionAddrV3 { pub(crate) service_id: V3OnionServiceId, @@ -91,6 +96,10 @@ impl std::fmt::Display for OnionAddr { } } +// +// DomainAddr +// + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DomainAddr { domain: String, @@ -165,6 +174,10 @@ impl FromStr for DomainAddr { } } +// +// TargetAddr +// + #[derive(Clone, Debug)] pub enum TargetAddr { Ip(std::net::SocketAddr), @@ -294,6 +307,10 @@ impl OnionStream { } } +// +// Onion Listener +// + pub trait OnionListenerImpl: Send { fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error>; fn accept(&self) -> Result, std::io::Error>; @@ -313,6 +330,59 @@ impl OnionListener { } } +// +// ProxyConfig +// + +#[derive(Clone, Debug)] +pub struct Socks4ProxyConfig { + pub(crate) address: TargetAddr, +} + +#[derive(Clone, Debug)] +pub struct Socks5ProxyConfig { + pub(crate) address: TargetAddr, + pub(crate) username: Option, + pub(crate) password: Option, +} + +#[derive(Clone, Debug)] +pub struct HttpsProxyConfig { + pub(crate) address: TargetAddr, + pub(crate) username: Option, + pub(crate) password: Option, +} + +#[derive(Clone, Debug)] +pub enum ProxyConfig { + Socks4(Socks4ProxyConfig), + Socks5(Socks5ProxyConfig), + Https(HttpsProxyConfig), +} + +// +// PluggableTransportConfig +// + +#[derive(Clone, Debug)] +pub struct PluggableTransportConfig { + transports: Vec, + path_to_binary: PathBuf, + options: Vec +} + +// +// BridgeSettings +// + +#[derive(Clone, Debug)] +pub struct BridgeLine { + transport: String, + address: SocketAddr, + fingerprint: String, + keyvalues: Vec<(String,String)>, +} + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("{0}")] diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 481877b45..3521c5ca4 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -360,6 +360,10 @@ fn test_legacy_bootstrap() -> anyhow::Result<()> { let tor_config = LegacyTorClientConfig::BundledTor{ tor_bin_path: tor_path, data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, }; bootstrap_test(Box::new(LegacyTorClient::new(tor_config)?)) @@ -376,6 +380,10 @@ fn test_legacy_onion_service() -> anyhow::Result<()> { let tor_config = LegacyTorClientConfig::BundledTor{ tor_bin_path: tor_path.clone(), data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, }; let server_provider = Box::new(LegacyTorClient::new(tor_config)?); @@ -384,6 +392,10 @@ fn test_legacy_onion_service() -> anyhow::Result<()> { let tor_config = LegacyTorClientConfig::BundledTor{ tor_bin_path: tor_path, data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, }; let client_provider = Box::new(LegacyTorClient::new(tor_config)?); @@ -401,6 +413,10 @@ fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { let tor_config = LegacyTorClientConfig::BundledTor{ tor_bin_path: tor_path.clone(), data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, }; let server_provider = Box::new(LegacyTorClient::new(tor_config)?); @@ -409,6 +425,10 @@ fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { let tor_config = LegacyTorClientConfig::BundledTor{ tor_bin_path: tor_path, data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, }; let client_provider = Box::new(LegacyTorClient::new(tor_config)?); @@ -606,6 +626,10 @@ fn test_mixed_arti_client_legacy_onion_service() -> anyhow::Result<()> { let tor_config = LegacyTorClientConfig::BundledTor{ tor_bin_path: tor_path, data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, }; let client_provider = Box::new(LegacyTorClient::new(tor_config)?); @@ -623,6 +647,10 @@ fn test_mixed_legacy_arti_client_onion_service() -> anyhow::Result<()> { let tor_config = LegacyTorClientConfig::BundledTor{ tor_bin_path: tor_path, data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, }; let server_provider = Box::new(LegacyTorClient::new(tor_config)?); From 66e8833026545783ddba707f4536f3757a250e5c Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Fri, 21 Jun 2024 22:38:18 +0000 Subject: [PATCH 100/184] tor-interface: add support for bundled LegacyTorClient proxy settings --- tor-interface/src/legacy_tor_client.rs | 49 +++++++++++++- tor-interface/src/tor_provider.rs | 89 ++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 843fe9328..844d10507 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -56,6 +56,9 @@ pub enum Error { #[error("failed to begin bootstrap")] SetConfDisableNetwork0Failed(#[source] crate::legacy_tor_controller::Error), + #[error("failed to setconf")] + SetConfFailed(#[source] crate::legacy_tor_controller::Error), + #[error("failed to add client auth for onion service")] OnionClientAuthAddFailed(#[source] crate::legacy_tor_controller::Error), @@ -257,8 +260,52 @@ impl LegacyTorClient { )); } - if let LegacyTorClientConfig::BundledTor{proxy_settings: _, allowed_ports: _, pluggable_transports: _, bridge_lines: _, ..} = config { + // configure tor client + if let LegacyTorClientConfig::BundledTor{proxy_settings, allowed_ports: _, pluggable_transports: _, bridge_lines: _, ..} = config { + // configure proxy + match proxy_settings { + Some(ProxyConfig::Socks4(Socks4ProxyConfig{address})) => { + controller + .setconf(&[("Socks4Proxy", address.to_string())]) + .map_err(Error::SetConfFailed)?; + }, + Some(ProxyConfig::Socks5(Socks5ProxyConfig{address, username, password})) => { + controller + .setconf(&[("Socks5Proxy", address.to_string())]) + .map_err(Error::SetConfFailed)?; + let username = username.unwrap_or("".to_string()); + if !username.is_empty() { + controller + .setconf(&[("Socks5ProxyUsername", username.to_string())]) + .map_err(Error::SetConfFailed)?; + } + let password = password.unwrap_or("".to_string()); + if !password.is_empty() { + controller + .setconf(&[("Socks5ProxyPassword", password.to_string())]) + .map_err(Error::SetConfFailed)?; + } + }, + Some(ProxyConfig::Https(HttpsProxyConfig{address, username, password})) => { + controller + .setconf(&[("HTTPSProxy", address.to_string())]) + .map_err(Error::SetConfFailed)?; + let username = username.unwrap_or("".to_string()); + let password = password.unwrap_or("".to_string()); + if !username.is_empty() || !password.is_empty() { + let authenticator = format!("{}:{}", username, password); + controller + .setconf(&[("HTTPSProxyAuthenticator", authenticator)]) + .map_err(Error::SetConfFailed)?; + } + }, + None => (), + } + // configure firewall + + // configure pluggable transports + // configure bridge lines } // register for STATUS_CLIENT async events diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 639670087..fa094c7da 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -334,11 +334,32 @@ impl OnionListener { // ProxyConfig // +#[derive(thiserror::Error, Debug)] +pub enum ProxyConfigError { + #[error("{0}")] + Generic(String), +} + #[derive(Clone, Debug)] pub struct Socks4ProxyConfig { pub(crate) address: TargetAddr, } +impl Socks4ProxyConfig { + pub fn new(address: TargetAddr) -> Result { + let port = match &address { + TargetAddr::Ip(addr) => addr.port(), + TargetAddr::Domain(addr) => addr.port(), + TargetAddr::OnionService(_) => return Err(ProxyConfigError::Generic("proxy address may not be onion service".to_string())), + }; + if port == 0 { + return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); + } + + Ok(Self{address}) + } +} + #[derive(Clone, Debug)] pub struct Socks5ProxyConfig { pub(crate) address: TargetAddr, @@ -346,6 +367,34 @@ pub struct Socks5ProxyConfig { pub(crate) password: Option, } +impl Socks5ProxyConfig { + pub fn new(address: TargetAddr, username: Option, password: Option) -> Result { + let port = match &address { + TargetAddr::Ip(addr) => addr.port(), + TargetAddr::Domain(addr) => addr.port(), + TargetAddr::OnionService(_) => return Err(ProxyConfigError::Generic("proxy address may not be onion service".to_string())), + }; + if port == 0 { + return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); + } + + // username must be less than 255 bytes + if let Some(username) = &username { + if username.len() > 255 { + return Err(ProxyConfigError::Generic("socks5 username must be <= 255 bytes".to_string())); + } + } + // password must be less than 255 bytes + if let Some(password) = &password { + if password.len() > 255 { + return Err(ProxyConfigError::Generic("socks5 password must be <= 255 bytes".to_string())); + } + } + + Ok(Self{address, username, password}) + } +} + #[derive(Clone, Debug)] pub struct HttpsProxyConfig { pub(crate) address: TargetAddr, @@ -353,6 +402,28 @@ pub struct HttpsProxyConfig { pub(crate) password: Option, } +impl HttpsProxyConfig { + pub fn new(address: TargetAddr, username: Option, password: Option) -> Result { + let port = match &address { + TargetAddr::Ip(addr) => addr.port(), + TargetAddr::Domain(addr) => addr.port(), + TargetAddr::OnionService(_) => return Err(ProxyConfigError::Generic("proxy address may not be onion service".to_string())), + }; + if port == 0 { + return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); + } + + // username may not contain ':' character (per RFC 2617) + if let Some(username) = &username { + if username.contains(':') { + return Err(ProxyConfigError::Generic("username may not contain ':' character".to_string())); + } + } + + Ok(Self{address, username, password}) + } +} + #[derive(Clone, Debug)] pub enum ProxyConfig { Socks4(Socks4ProxyConfig), @@ -360,6 +431,24 @@ pub enum ProxyConfig { Https(HttpsProxyConfig), } +impl From for ProxyConfig { + fn from(config: Socks4ProxyConfig) -> Self { + ProxyConfig::Socks4(config) + } +} + +impl From for ProxyConfig { + fn from(config: Socks5ProxyConfig) -> Self { + ProxyConfig::Socks5(config) + } +} + +impl From for ProxyConfig { + fn from(config: HttpsProxyConfig) -> Self { + ProxyConfig::Https(config) + } +} + // // PluggableTransportConfig // From 9737a307a0aea7cfe2c945f1b71bde5ca1a154fa Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 22 Jun 2024 00:16:20 +0000 Subject: [PATCH 101/184] tor-interface: add support for bundled LegacyTorClient firewall settings --- tor-interface/src/legacy_tor_client.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 844d10507..69ae3e1fe 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -261,7 +261,7 @@ impl LegacyTorClient { } // configure tor client - if let LegacyTorClientConfig::BundledTor{proxy_settings, allowed_ports: _, pluggable_transports: _, bridge_lines: _, ..} = config { + if let LegacyTorClientConfig::BundledTor{proxy_settings, allowed_ports, pluggable_transports: _, bridge_lines: _, ..} = config { // configure proxy match proxy_settings { Some(ProxyConfig::Socks4(Socks4ProxyConfig{address})) => { @@ -302,7 +302,13 @@ impl LegacyTorClient { None => (), } // configure firewall - + if let Some(allowed_ports) = allowed_ports { + let allowed_addresses: Vec = allowed_ports.iter().map(|port| format!("*{{}}:{port}")).collect(); + let allowed_addresses = allowed_addresses.join(", "); + controller + .setconf(&[("ReachableAddresses", allowed_addresses)]) + .map_err(Error::SetConfFailed)?; + } // configure pluggable transports // configure bridge lines From 0b540aea4213d18297b19232b9547a2f68e1e9e7 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 22 Jun 2024 03:04:11 +0000 Subject: [PATCH 102/184] tor-interface: add support for bundled LegacyTorClient pluggable transport settings --- tor-interface/src/legacy_tor_client.rs | 88 +++++++++++++++++++++++++- tor-interface/src/tor_provider.rs | 49 ++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 69ae3e1fe..a6358a19a 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -89,6 +89,27 @@ pub enum Error { #[error("tor not bootstrapped")] LegacyTorNotBootstrapped(), + #[error("{0}")] + PluggableTransportConfigDirectoryCreationFailed(#[source] std::io::Error), + + #[error("unable to create pluggable-transport directory because file with same name already exists: {0:?}")] + PluggableTransportDirectoryNameCollision(PathBuf), + + #[error("{0}")] + PluggableTransportSymlinkRemovalFailed(#[source] std::io::Error), + + #[error("{0}")] + PluggableTransportSymlinkCreationFailed(#[source] std::io::Error), + + #[error("pluggable transport binary name not representable as utf8: {0:?}")] + PluggableTransportBinaryNameNotUtf8Representnable(std::ffi::OsString), + + #[error("{0}")] + PluggableTransportConfigError(#[source] crate::tor_provider::PluggableTransportConfigError), + + #[error("pluggable transport multiply defines '{0}' bridge transport type")] + BridgeTransportTypeMultiplyDefined(String), + #[error("not implemented")] NotImplemented(), } @@ -261,7 +282,7 @@ impl LegacyTorClient { } // configure tor client - if let LegacyTorClientConfig::BundledTor{proxy_settings, allowed_ports, pluggable_transports: _, bridge_lines: _, ..} = config { + if let LegacyTorClientConfig::BundledTor{data_directory, proxy_settings, allowed_ports, pluggable_transports, bridge_lines: _, ..} = config { // configure proxy match proxy_settings { Some(ProxyConfig::Socks4(Socks4ProxyConfig{address})) => { @@ -310,6 +331,71 @@ impl LegacyTorClient { .map_err(Error::SetConfFailed)?; } // configure pluggable transports + let mut supported_transports: std::collections::BTreeSet = Default::default(); + if let Some(pluggable_transports) = pluggable_transports { + // Legacy tor daemon cannot be configured to use pluggable-transports which + // exist in paths containing spaces. To work around this, we create a known, safe + // path in the tor daemon's working directory, and soft-link the provided + // binary path to this safe location. Finally, we configure tor to use the soft-linked + // binary in the ClientTransportPlugin setconf call. + + // create pluggable-transport directory + let mut pt_directory = data_directory.clone(); + pt_directory.push("pluggable-transports"); + if !std::path::Path::exists(&pt_directory) { + // path does not exist so create it + std::fs::create_dir(&pt_directory).map_err(Error::PluggableTransportConfigDirectoryCreationFailed)?; + } else if !std::path::Path::is_dir(&pt_directory) { + // path exists but it is not a directory + return Err(Error::PluggableTransportDirectoryNameCollision(pt_directory)); + } + + // symlink all our pts and configure tor + let mut conf: Vec<(&str, String)> = Default::default(); + for pt_settings in &pluggable_transports { + // symlink absolute path of pt binary to pt_directory in tor's working + // directory + let path_to_binary = pt_settings.path_to_binary(); + let binary_name = path_to_binary.file_name().expect("file_name should be absolute path"); + let mut pt_symlink = pt_directory.clone(); + pt_symlink.push(binary_name); + let binary_name = if let Some(binary_name) = binary_name.to_str() { + binary_name + } else { + return Err(Error::PluggableTransportBinaryNameNotUtf8Representnable(binary_name.to_os_string())); + }; + + // remove any file that may exist with the same name + if std::path::Path::exists(&pt_symlink) { + std::fs::remove_file(&pt_symlink).map_err(Error::PluggableTransportSymlinkRemovalFailed)?; + } + + // create new symlink + #[cfg(windows)] + std::os::windows::fs::symlink_file(path_to_binary, &pt_symlink).map_err(Error::PluggableTransportSymlinkCreationFailed)?; + #[cfg(unix)] + std::os::unix::fs::symlink(path_to_binary, &pt_symlink).map_err(Error::PluggableTransportSymlinkCreationFailed)?; + + // verify a bridge-type support has not been defined for multiple pluggable-transports + for transport in pt_settings.transports() { + if supported_transports.contains(transport) { + return Err(Error::BridgeTransportTypeMultiplyDefined(transport.to_string())); + } + supported_transports.insert(transport.to_string()); + } + + // finally construct our setconf value + let transports = pt_settings.transports().join(","); + use std::path::MAIN_SEPARATOR; + let path_to_binary = format!("pluggable-transports{MAIN_SEPARATOR}{binary_name}"); + let options = pt_settings.options().join(" "); + + let value = format!("{transports} exec {path_to_binary} {options}"); + conf.push(("ClientTransportPlugin", value)); + } + controller.setconf(conf.as_slice()) + .map_err(Error::SetConfFailed)?; + } // configure bridge lines } diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index fa094c7da..8a35f5b7e 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -460,6 +460,55 @@ pub struct PluggableTransportConfig { options: Vec } +#[derive(thiserror::Error, Debug)] +pub enum PluggableTransportConfigError { + #[error("pluggable transport name '{0}' is invalid")] + TransportNameInvalid(String), + #[error("unable to use '{0}' as pluggable transport binary path, {1}")] + BinaryPathInvalid(String, String) +} + +impl PluggableTransportConfig{ + pub fn new(transports: Vec, path_to_binary: PathBuf) -> Result { + // per the PT spec: https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/main/releases/PTSpecV1.0/pt-1_0.txt + static TRANSPORT_PATTERN: OnceLock = OnceLock::new(); + let transport_pattern = TRANSPORT_PATTERN.get_or_init(|| { + Regex::new(r"(?m)^[a-zA-Z_][a-zA-Z0-9_]*$") + .unwrap() + }); + // validate each transport + for transport in &transports { + if !transport_pattern.is_match(&transport) { + return Err(PluggableTransportConfigError::TransportNameInvalid(transport.clone())); + } + } + + // pluggable transport path must be absolute so we can fix it up for individual + // TorProvider implementations + if !path_to_binary.is_absolute() { + return Err(PluggableTransportConfigError::BinaryPathInvalid(format!("{:?}", path_to_binary.display()), "must be an absolute path".to_string())); + } + + Ok(Self{transports, path_to_binary, options: Default::default()}) + } + + pub fn transports(&self) -> &Vec { + &self.transports + } + + pub fn path_to_binary(&self) -> &PathBuf { + &self.path_to_binary + } + + pub fn options(&self) -> &Vec { + &self.options + } + + pub fn add_option(&mut self, arg: String) { + self.options.push(arg); + } +} + // // BridgeSettings // From 5387a15c7dd830f7f8b2d9b4dc16cefa3039f23b Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sat, 22 Jun 2024 20:44:34 +0000 Subject: [PATCH 103/184] tor-interface: add support for bundled LegacyTorClient bridge settings --- tor-interface/src/legacy_tor_client.rs | 19 ++- tor-interface/src/tor_provider.rs | 158 +++++++++++++++++++++++-- 2 files changed, 167 insertions(+), 10 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index a6358a19a..f0e11fc9e 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -110,6 +110,9 @@ pub enum Error { #[error("pluggable transport multiply defines '{0}' bridge transport type")] BridgeTransportTypeMultiplyDefined(String), + #[error("bridge transport '{0}' not supported by pluggable transport configuration")] + BridgeTransportNotSupported(String), + #[error("not implemented")] NotImplemented(), } @@ -282,7 +285,7 @@ impl LegacyTorClient { } // configure tor client - if let LegacyTorClientConfig::BundledTor{data_directory, proxy_settings, allowed_ports, pluggable_transports, bridge_lines: _, ..} = config { + if let LegacyTorClientConfig::BundledTor{data_directory, proxy_settings, allowed_ports, pluggable_transports, bridge_lines, ..} = config { // configure proxy match proxy_settings { Some(ProxyConfig::Socks4(Socks4ProxyConfig{address})) => { @@ -396,8 +399,20 @@ impl LegacyTorClient { controller.setconf(conf.as_slice()) .map_err(Error::SetConfFailed)?; } - // configure bridge lines + if let Some(bridge_lines) = bridge_lines { + let mut conf: Vec<(&str, String)> = Default::default(); + for bridge_line in &bridge_lines { + if !supported_transports.contains(bridge_line.transport()) { + return Err(Error::BridgeTransportNotSupported(bridge_line.transport().to_string())); + } + let value = bridge_line.as_legacy_tor_setconf_value(); + conf.push(("Bridge", value)); + } + conf.push(("UseBridges", "1".to_string())); + controller.setconf(conf.as_slice()) + .map_err(Error::SetConfFailed)?; + } } // register for STATUS_CLIENT async events diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 8a35f5b7e..7e3b422c9 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -468,14 +468,15 @@ pub enum PluggableTransportConfigError { BinaryPathInvalid(String, String) } -impl PluggableTransportConfig{ - pub fn new(transports: Vec, path_to_binary: PathBuf) -> Result { - // per the PT spec: https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/main/releases/PTSpecV1.0/pt-1_0.txt - static TRANSPORT_PATTERN: OnceLock = OnceLock::new(); - let transport_pattern = TRANSPORT_PATTERN.get_or_init(|| { - Regex::new(r"(?m)^[a-zA-Z_][a-zA-Z0-9_]*$") - .unwrap() - }); +// per the PT spec: https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/main/releases/PTSpecV1.0/pt-1_0.txt +static TRANSPORT_PATTERN: OnceLock = OnceLock::new(); +fn init_transport_pattern() -> Regex { + Regex::new(r"(?m)^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap() +} + +impl PluggableTransportConfig { + pub fn new(transports: Vec, path_to_binary: PathBuf) -> Result { + let transport_pattern = TRANSPORT_PATTERN.get_or_init(init_transport_pattern); // validate each transport for transport in &transports { if !transport_pattern.is_match(&transport) { @@ -521,6 +522,147 @@ pub struct BridgeLine { keyvalues: Vec<(String,String)>, } +#[derive(thiserror::Error, Debug)] +pub enum BridgeLineError { + #[error("bridge line '{0}' missing transport")] + TransportMissing(String), + + #[error("bridge line '{0}' missing address")] + AddressMissing(String), + + #[error("bridge line '{0}' missing fingerprint")] + FingerprintMissing(String), + + #[error("transport name '{0}' is invalid")] + TransportNameInvalid(String), + + #[error("address '{0}' cannot be parsed as IP:PORT")] + AddressParseFailed(String), + + #[error("key=value '{0}' is invalid")] + KeyValueInvalid(String), + + #[error("bridge address port must not be 0")] + AddressPortInvalid, + + #[error("fingerprint '{0}' is invalid")] + FingerprintInvalid(String), +} + +impl BridgeLine { + pub fn new(transport: String, address: SocketAddr, fingerprint: String, keyvalues: Vec<(String,String)>) -> Result { + let transport_pattern = TRANSPORT_PATTERN.get_or_init(init_transport_pattern); + + // transports have a particular pattern + if !transport_pattern.is_match(&transport) { + return Err(BridgeLineError::TransportNameInvalid(transport)); + } + + // port can't be 0 + if address.port() == 0 { + return Err(BridgeLineError::AddressPortInvalid); + } + + static BRIDGE_FINGERPRINT_PATTERN: OnceLock = OnceLock::new(); + let bridge_fingerprint_pattern = BRIDGE_FINGERPRINT_PATTERN.get_or_init(|| { + Regex::new(r"(?m)^[0-9a-fA-F]{40}$") + .unwrap() + }); + + // fingerprint should be a sha1 hash + if !bridge_fingerprint_pattern.is_match(&fingerprint) { + return Err(BridgeLineError::FingerprintInvalid(fingerprint)); + } + + // validate key-values + for (key, value) in &keyvalues { + if key.contains(' ') + || key.contains('=') + || key.len() == 0 { + return Err(BridgeLineError::KeyValueInvalid(format!("{key}={value}"))); + } + } + + Ok(Self{transport, address, fingerprint, keyvalues}) + } + + pub fn transport(&self) -> &String { + &self.transport + } + + pub fn address(&self) -> &SocketAddr { + &self.address + } + + pub fn fingerprint(&self) -> &String { + &self.fingerprint + } + + pub fn keyvalues(&self) -> &Vec<(String,String)> { + &self.keyvalues + } + + #[cfg(feature = "legacy-tor-provider")] + pub fn as_legacy_tor_setconf_value(&self) -> String { + let transport = &self.transport; + let address = self.address.to_string(); + let fingerprint = self.fingerprint.to_string(); + let keyvalues: Vec = self.keyvalues.iter().map(|(key,value)| format!("{key}={value}")).collect(); + let keyvalues = keyvalues.join(" "); + + format!("{transport} {address} {fingerprint} {keyvalues}") + } +} + +impl FromStr for BridgeLine { + type Err = BridgeLineError; + fn from_str(s: &str) -> Result { + let mut tokens = s.split(' '); + // get transport name + let transport = if let Some(transport) = tokens.next() { + transport + } else { + return Err(BridgeLineError::TransportMissing(s.to_string())); + }; + // get bridge address + let address = if let Some(address) = tokens.next() { + if let Ok(address) = SocketAddr::from_str(address) { + address + } else { + return Err(BridgeLineError::AddressParseFailed(address.to_string())); + } + } else { + return Err(BridgeLineError::AddressMissing(s.to_string())); + }; + // get the bridge fingerprint + let fingerprint = if let Some(fingerprint) = tokens.next() { + fingerprint + } else { + return Err(BridgeLineError::FingerprintMissing(s.to_string())); + }; + + // get the bridge options + static BRIDGE_OPTION_PATTERN: OnceLock = OnceLock::new(); + let bridge_option_pattern = BRIDGE_OPTION_PATTERN.get_or_init(|| { + Regex::new(r"(?m)^(?[^=]+)=(?.*)$") + .unwrap() + }); + + let mut keyvalues: Vec<(String,String)> = Default::default(); + while let Some(keyvalue) = tokens.next() { + if let Some(caps) = bridge_option_pattern.captures(&keyvalue) { + let key = caps.name("key").expect("missing key group").as_str().to_string(); + let value = caps.name("value").expect("missing value group").as_str().to_string(); + keyvalues.push((key,value)); + } else { + return Err(BridgeLineError::KeyValueInvalid(keyvalue.to_string())); + } + } + + BridgeLine::new(transport.to_string(), address, fingerprint.to_string(), keyvalues) + } +} + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("{0}")] From d3b88e691cdc7766e98d70e87eb477c2f6683e94 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 23 Jun 2024 00:39:05 +0000 Subject: [PATCH 104/184] tor-interface: add bootstrap test using pluggable transport --- tor-interface/CMakeLists.txt | 9 +++++- tor-interface/tests/tor_provider.rs | 45 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index dc825cbab..34ddb5f85 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -87,6 +87,13 @@ if (ENABLE_TESTS) COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_system_legacy_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) + if (ENABLE_TOR_EXPERT_BUNDLE) + add_test(NAME tor_interface_legacy_pluggable_transport_bootstrap_cargo_test + COMMAND env TEB_PATH=${TEB_PATH} CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_legacy_pluggable_transport_bootstrap ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + set_tests_properties(tor_interface_legacy_pluggable_transport_bootstrap_cargo_test PROPERTIES FIXTURES_REQUIRED tor_expert_bundle_target_fixture) + endif() endif() if (ENABLE_ARTI_CLIENT_TOR_PROVIDER) @@ -125,7 +132,7 @@ if (ENABLE_TESTS) # catchall add_test(NAME tor_interface_cargo_test - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + COMMAND env TEB_PATH=${TEB_PATH} CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) endif() diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 3521c5ca4..80062b067 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -369,6 +369,47 @@ fn test_legacy_bootstrap() -> anyhow::Result<()> { bootstrap_test(Box::new(LegacyTorClient::new(tor_config)?)) } +#[test] +#[serial] +#[cfg(feature = "legacy-tor-provider")] +fn test_legacy_pluggable_transport_bootstrap() -> anyhow::Result<()> { + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push("test_legacy_pluggable_transport_bootstrap"); + + + // find the lyrebird bin + let teb_path = std::env::var("TEB_PATH")?; + if teb_path.is_empty() { + println!("TEB_PATH environment variable empty, so skipping test_legacy_pluggable_transport_bootstrap()"); + return Ok(()); + } + let mut lyrebird_path = std::path::PathBuf::from(&teb_path); + let lyrebird_bin = format!("lyrebird{}", std::env::consts::EXE_SUFFIX); + lyrebird_path.push(lyrebird_bin.clone()); + assert!(std::path::Path::exists(&lyrebird_path)); + assert!(std::path::Path::is_file(&lyrebird_path)); + + // configure lyrebird pluggable transport + let pluggable_transport = PluggableTransportConfig::new( + vec!["obfs4".to_string()], + lyrebird_path)?; + + // obfs4 bridgeline + let bridge_line = BridgeLine::from_str("obfs4 207.172.185.193:22223 F34AC0CDBC06918E54292A474578C99834A58893 cert=MjqosoyVylLQuLo4LH+eQ5hS7Z44s2CaMfQbIjJtn4bGRnvLv8ldSvSED5JpvWSxm09XXg iat-mode=0")?; + + let tor_config = LegacyTorClientConfig::BundledTor{ + tor_bin_path: tor_path, + data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: Some(vec![pluggable_transport]), + bridge_lines: Some(vec![bridge_line]), + }; + + bootstrap_test(Box::new(LegacyTorClient::new(tor_config)?)) +} + #[test] #[serial] #[cfg(feature = "legacy-tor-provider")] @@ -663,6 +704,10 @@ fn test_mixed_legacy_arti_client_onion_service() -> anyhow::Result<()> { basic_onion_service_test(server_provider, client_provider) } +// +// Misc Utils +// + #[test] fn test_tor_provider_target_addr() -> anyhow::Result<()> { let valid_ip_addr: &[&str] = &[ From 568bd90443966a0d188a4289a7b674dbb3b76ed3 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Tue, 2 Jul 2024 20:15:06 +0000 Subject: [PATCH 105/184] tor-interface: force more restrictive version requirements to ensure 1.70 rust compatibility and fix build break --- tor-interface/Cargo.toml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index cb413701b..9dc8dad30 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,11 +10,11 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { version = "0.18", features = ["experimental-api", "onion-service-client", "onion-service-service", "tokio"], optional = true} +arti-client = { version = "0.18.0", features = ["experimental-api", "onion-service-client", "onion-service-service", "tokio"], optional = true} curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" -domain = "0.10" +domain = "<= 0.10.0" fs-mistrust = { version = "0", optional = true } idna = "1" rand = "0.8" @@ -28,15 +28,15 @@ static_assertions = "1.1" thiserror = "1.0" tokio = { version = "1", features = ["macros"], optional = true } tokio-stream = { version = "0", optional = true } -tor-cell = { version = "0", optional = true } -tor-config = { version = "0", optional = true } -tor-hscrypto = { version = "0", optional = true } -tor-hsservice = { version = "0", optional = true } -tor-keymgr = { version = "0", optional = true, features = ["keymgr"] } -tor-llcrypto = { version = "0.18", features = ["relay"] } -tor-persist = { version = "0", optional = true } -tor-proto = { version = "0", features = ["stream-ctrl"], optional = true } -tor-rtcompat = { version = "0", optional = true } +tor-cell = { version = "0.18.0", optional = true } +tor-config = { version = "0.18.0", optional = true } +tor-hscrypto = { version = "0.18.0", optional = true } +tor-hsservice = { version = "0.18.0", optional = true } +tor-keymgr = { version = "0.18.0", optional = true, features = ["keymgr"] } +tor-llcrypto = { version = "0.18.0", features = ["relay"] } +tor-persist = { version = "0.18.0", optional = true } +tor-proto = { version = "0.18.0", features = ["stream-ctrl"], optional = true } +tor-rtcompat = { version = "0.18.0", optional = true } [dev-dependencies] anyhow = "1.0" From 9941154296699b79704a41f8c19c997499f5e6e4 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Tue, 2 Jul 2024 20:21:55 +0000 Subject: [PATCH 106/184] tor-interface: updated version to 0.3.0 --- tor-interface/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 9dc8dad30..c443dbf8e 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tor-interface" authors = ["Richard Pospesel "] -version = "0.2.1" +version = "0.3.0" rust-version = "1.70" edition = "2021" license = "BSD-3-Clause" From 6d1a7c3548fdd90f0ca92eb96017985eca7a9f31 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 21 Jul 2024 00:19:26 +0000 Subject: [PATCH 107/184] tor-interface, gosling, cgosling, test: re-ran make format --- tor-interface/src/legacy_tor_client.rs | 136 +++++++++++++++------- tor-interface/src/mock_tor_client.rs | 25 ++-- tor-interface/src/tor_provider.rs | 154 ++++++++++++++++++------- tor-interface/tests/tor_provider.rs | 76 +++++++----- 4 files changed, 270 insertions(+), 121 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index f0e11fc9e..93e0cd2c0 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -228,10 +228,15 @@ pub struct LegacyTorClient { impl LegacyTorClient { pub fn new(config: LegacyTorClientConfig) -> Result { let (daemon, mut controller, password, socks_listener) = match &config { - LegacyTorClientConfig::BundledTor{tor_bin_path, data_directory, ..} => { + LegacyTorClientConfig::BundledTor { + tor_bin_path, + data_directory, + .. + } => { // launch tor - let daemon = LegacyTorProcess::new(tor_bin_path.as_path(), data_directory.as_path()) - .map_err(Error::LegacyTorProcessCreationFailed)?; + let daemon = + LegacyTorProcess::new(tor_bin_path.as_path(), data_directory.as_path()) + .map_err(Error::LegacyTorProcessCreationFailed)?; // open a control stream let control_stream = LegacyControlStream::new(daemon.get_control_addr(), Duration::from_millis(16)) @@ -243,8 +248,12 @@ impl LegacyTorClient { let password = daemon.get_password().to_string(); (Some(daemon), controller, password, None) - }, - LegacyTorClientConfig::SystemTor{tor_socks_addr, tor_control_addr, tor_control_passwd} => { + } + LegacyTorClientConfig::SystemTor { + tor_socks_addr, + tor_control_addr, + tor_control_passwd, + } => { // open a control stream let control_stream = LegacyControlStream::new(&tor_control_addr, Duration::from_millis(16)) @@ -254,8 +263,13 @@ impl LegacyTorClient { let controller = LegacyTorController::new(control_stream) .map_err(Error::LegacyTorControllerCreationFailed)?; - (None, controller, tor_control_passwd.clone(), Some(tor_socks_addr.clone())) - }, + ( + None, + controller, + tor_control_passwd.clone(), + Some(tor_socks_addr.clone()), + ) + } }; // authenticate @@ -285,53 +299,72 @@ impl LegacyTorClient { } // configure tor client - if let LegacyTorClientConfig::BundledTor{data_directory, proxy_settings, allowed_ports, pluggable_transports, bridge_lines, ..} = config { + if let LegacyTorClientConfig::BundledTor { + data_directory, + proxy_settings, + allowed_ports, + pluggable_transports, + bridge_lines, + .. + } = config + { // configure proxy match proxy_settings { - Some(ProxyConfig::Socks4(Socks4ProxyConfig{address})) => { + Some(ProxyConfig::Socks4(Socks4ProxyConfig { address })) => { controller - .setconf(&[("Socks4Proxy", address.to_string())]) - .map_err(Error::SetConfFailed)?; - }, - Some(ProxyConfig::Socks5(Socks5ProxyConfig{address, username, password})) => { + .setconf(&[("Socks4Proxy", address.to_string())]) + .map_err(Error::SetConfFailed)?; + } + Some(ProxyConfig::Socks5(Socks5ProxyConfig { + address, + username, + password, + })) => { controller - .setconf(&[("Socks5Proxy", address.to_string())]) - .map_err(Error::SetConfFailed)?; + .setconf(&[("Socks5Proxy", address.to_string())]) + .map_err(Error::SetConfFailed)?; let username = username.unwrap_or("".to_string()); if !username.is_empty() { controller - .setconf(&[("Socks5ProxyUsername", username.to_string())]) - .map_err(Error::SetConfFailed)?; + .setconf(&[("Socks5ProxyUsername", username.to_string())]) + .map_err(Error::SetConfFailed)?; } let password = password.unwrap_or("".to_string()); if !password.is_empty() { controller - .setconf(&[("Socks5ProxyPassword", password.to_string())]) - .map_err(Error::SetConfFailed)?; + .setconf(&[("Socks5ProxyPassword", password.to_string())]) + .map_err(Error::SetConfFailed)?; } - }, - Some(ProxyConfig::Https(HttpsProxyConfig{address, username, password})) => { + } + Some(ProxyConfig::Https(HttpsProxyConfig { + address, + username, + password, + })) => { controller - .setconf(&[("HTTPSProxy", address.to_string())]) - .map_err(Error::SetConfFailed)?; + .setconf(&[("HTTPSProxy", address.to_string())]) + .map_err(Error::SetConfFailed)?; let username = username.unwrap_or("".to_string()); let password = password.unwrap_or("".to_string()); if !username.is_empty() || !password.is_empty() { let authenticator = format!("{}:{}", username, password); controller - .setconf(&[("HTTPSProxyAuthenticator", authenticator)]) - .map_err(Error::SetConfFailed)?; + .setconf(&[("HTTPSProxyAuthenticator", authenticator)]) + .map_err(Error::SetConfFailed)?; } - }, + } None => (), } // configure firewall if let Some(allowed_ports) = allowed_ports { - let allowed_addresses: Vec = allowed_ports.iter().map(|port| format!("*{{}}:{port}")).collect(); + let allowed_addresses: Vec = allowed_ports + .iter() + .map(|port| format!("*{{}}:{port}")) + .collect(); let allowed_addresses = allowed_addresses.join(", "); controller - .setconf(&[("ReachableAddresses", allowed_addresses)]) - .map_err(Error::SetConfFailed)?; + .setconf(&[("ReachableAddresses", allowed_addresses)]) + .map_err(Error::SetConfFailed)?; } // configure pluggable transports let mut supported_transports: std::collections::BTreeSet = Default::default(); @@ -347,10 +380,13 @@ impl LegacyTorClient { pt_directory.push("pluggable-transports"); if !std::path::Path::exists(&pt_directory) { // path does not exist so create it - std::fs::create_dir(&pt_directory).map_err(Error::PluggableTransportConfigDirectoryCreationFailed)?; + std::fs::create_dir(&pt_directory) + .map_err(Error::PluggableTransportConfigDirectoryCreationFailed)?; } else if !std::path::Path::is_dir(&pt_directory) { // path exists but it is not a directory - return Err(Error::PluggableTransportDirectoryNameCollision(pt_directory)); + return Err(Error::PluggableTransportDirectoryNameCollision( + pt_directory, + )); } // symlink all our pts and configure tor @@ -359,30 +395,39 @@ impl LegacyTorClient { // symlink absolute path of pt binary to pt_directory in tor's working // directory let path_to_binary = pt_settings.path_to_binary(); - let binary_name = path_to_binary.file_name().expect("file_name should be absolute path"); + let binary_name = path_to_binary + .file_name() + .expect("file_name should be absolute path"); let mut pt_symlink = pt_directory.clone(); pt_symlink.push(binary_name); let binary_name = if let Some(binary_name) = binary_name.to_str() { binary_name } else { - return Err(Error::PluggableTransportBinaryNameNotUtf8Representnable(binary_name.to_os_string())); + return Err(Error::PluggableTransportBinaryNameNotUtf8Representnable( + binary_name.to_os_string(), + )); }; // remove any file that may exist with the same name if std::path::Path::exists(&pt_symlink) { - std::fs::remove_file(&pt_symlink).map_err(Error::PluggableTransportSymlinkRemovalFailed)?; + std::fs::remove_file(&pt_symlink) + .map_err(Error::PluggableTransportSymlinkRemovalFailed)?; } // create new symlink #[cfg(windows)] - std::os::windows::fs::symlink_file(path_to_binary, &pt_symlink).map_err(Error::PluggableTransportSymlinkCreationFailed)?; + std::os::windows::fs::symlink_file(path_to_binary, &pt_symlink) + .map_err(Error::PluggableTransportSymlinkCreationFailed)?; #[cfg(unix)] - std::os::unix::fs::symlink(path_to_binary, &pt_symlink).map_err(Error::PluggableTransportSymlinkCreationFailed)?; + std::os::unix::fs::symlink(path_to_binary, &pt_symlink) + .map_err(Error::PluggableTransportSymlinkCreationFailed)?; // verify a bridge-type support has not been defined for multiple pluggable-transports for transport in pt_settings.transports() { if supported_transports.contains(transport) { - return Err(Error::BridgeTransportTypeMultiplyDefined(transport.to_string())); + return Err(Error::BridgeTransportTypeMultiplyDefined( + transport.to_string(), + )); } supported_transports.insert(transport.to_string()); } @@ -390,28 +435,33 @@ impl LegacyTorClient { // finally construct our setconf value let transports = pt_settings.transports().join(","); use std::path::MAIN_SEPARATOR; - let path_to_binary = format!("pluggable-transports{MAIN_SEPARATOR}{binary_name}"); + let path_to_binary = + format!("pluggable-transports{MAIN_SEPARATOR}{binary_name}"); let options = pt_settings.options().join(" "); let value = format!("{transports} exec {path_to_binary} {options}"); conf.push(("ClientTransportPlugin", value)); } - controller.setconf(conf.as_slice()) - .map_err(Error::SetConfFailed)?; + controller + .setconf(conf.as_slice()) + .map_err(Error::SetConfFailed)?; } // configure bridge lines if let Some(bridge_lines) = bridge_lines { let mut conf: Vec<(&str, String)> = Default::default(); for bridge_line in &bridge_lines { if !supported_transports.contains(bridge_line.transport()) { - return Err(Error::BridgeTransportNotSupported(bridge_line.transport().to_string())); + return Err(Error::BridgeTransportNotSupported( + bridge_line.transport().to_string(), + )); } let value = bridge_line.as_legacy_tor_setconf_value(); conf.push(("Bridge", value)); } conf.push(("UseBridges", "1".to_string())); - controller.setconf(conf.as_slice()) - .map_err(Error::SetConfFailed)?; + controller + .setconf(conf.as_slice()) + .map_err(Error::SetConfFailed)?; } } diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index 45e86b2b5..bbddcf285 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -175,7 +175,6 @@ impl MockTorClient { let line = "[notice] MockTorClient running".to_string(); events.push(TorEvent::LogReceived { line }); - let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); let listener = TcpListener::bind(socket_addr).expect("tcplistener bind failed"); @@ -275,15 +274,21 @@ impl TorProvider for MockTorClient { service_id, virt_port, })) => (service_id, virt_port), - target_address => if let Ok(stream) = TcpStream::connect(self.loopback.local_addr().expect("loopback local_addr failed")) { - return Ok(OnionStream { - stream, - local_addr: None, - peer_addr: Some(target_address), - }); - } else { - return Err(Error::ConnectFailed(target_address).into()); - }, + target_address => { + if let Ok(stream) = TcpStream::connect( + self.loopback + .local_addr() + .expect("loopback local_addr failed"), + ) { + return Ok(OnionStream { + stream, + local_addr: None, + peer_addr: Some(target_address), + }); + } else { + return Err(Error::ConnectFailed(target_address).into()); + } + } }; let client_auth = self.client_auth_keys.get(&service_id); diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 7e3b422c9..907f83c5d 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -149,7 +149,10 @@ impl TryFrom<(String, u16)> for DomainAddr { }); } } - Err(DomainAddrParseError::Generic(format!("{}:{}", domain, port))) + Err(DomainAddrParseError::Generic(format!( + "{}:{}", + domain, port + ))) } } @@ -350,13 +353,17 @@ impl Socks4ProxyConfig { let port = match &address { TargetAddr::Ip(addr) => addr.port(), TargetAddr::Domain(addr) => addr.port(), - TargetAddr::OnionService(_) => return Err(ProxyConfigError::Generic("proxy address may not be onion service".to_string())), + TargetAddr::OnionService(_) => { + return Err(ProxyConfigError::Generic( + "proxy address may not be onion service".to_string(), + )) + } }; if port == 0 { return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); } - Ok(Self{address}) + Ok(Self { address }) } } @@ -368,11 +375,19 @@ pub struct Socks5ProxyConfig { } impl Socks5ProxyConfig { - pub fn new(address: TargetAddr, username: Option, password: Option) -> Result { + pub fn new( + address: TargetAddr, + username: Option, + password: Option, + ) -> Result { let port = match &address { TargetAddr::Ip(addr) => addr.port(), TargetAddr::Domain(addr) => addr.port(), - TargetAddr::OnionService(_) => return Err(ProxyConfigError::Generic("proxy address may not be onion service".to_string())), + TargetAddr::OnionService(_) => { + return Err(ProxyConfigError::Generic( + "proxy address may not be onion service".to_string(), + )) + } }; if port == 0 { return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); @@ -381,17 +396,25 @@ impl Socks5ProxyConfig { // username must be less than 255 bytes if let Some(username) = &username { if username.len() > 255 { - return Err(ProxyConfigError::Generic("socks5 username must be <= 255 bytes".to_string())); + return Err(ProxyConfigError::Generic( + "socks5 username must be <= 255 bytes".to_string(), + )); } } // password must be less than 255 bytes if let Some(password) = &password { if password.len() > 255 { - return Err(ProxyConfigError::Generic("socks5 password must be <= 255 bytes".to_string())); + return Err(ProxyConfigError::Generic( + "socks5 password must be <= 255 bytes".to_string(), + )); } } - Ok(Self{address, username, password}) + Ok(Self { + address, + username, + password, + }) } } @@ -403,11 +426,19 @@ pub struct HttpsProxyConfig { } impl HttpsProxyConfig { - pub fn new(address: TargetAddr, username: Option, password: Option) -> Result { + pub fn new( + address: TargetAddr, + username: Option, + password: Option, + ) -> Result { let port = match &address { TargetAddr::Ip(addr) => addr.port(), TargetAddr::Domain(addr) => addr.port(), - TargetAddr::OnionService(_) => return Err(ProxyConfigError::Generic("proxy address may not be onion service".to_string())), + TargetAddr::OnionService(_) => { + return Err(ProxyConfigError::Generic( + "proxy address may not be onion service".to_string(), + )) + } }; if port == 0 { return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); @@ -416,11 +447,17 @@ impl HttpsProxyConfig { // username may not contain ':' character (per RFC 2617) if let Some(username) = &username { if username.contains(':') { - return Err(ProxyConfigError::Generic("username may not contain ':' character".to_string())); + return Err(ProxyConfigError::Generic( + "username may not contain ':' character".to_string(), + )); } } - Ok(Self{address, username, password}) + Ok(Self { + address, + username, + password, + }) } } @@ -457,7 +494,7 @@ impl From for ProxyConfig { pub struct PluggableTransportConfig { transports: Vec, path_to_binary: PathBuf, - options: Vec + options: Vec, } #[derive(thiserror::Error, Debug)] @@ -465,7 +502,7 @@ pub enum PluggableTransportConfigError { #[error("pluggable transport name '{0}' is invalid")] TransportNameInvalid(String), #[error("unable to use '{0}' as pluggable transport binary path, {1}")] - BinaryPathInvalid(String, String) + BinaryPathInvalid(String, String), } // per the PT spec: https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/main/releases/PTSpecV1.0/pt-1_0.txt @@ -475,34 +512,46 @@ fn init_transport_pattern() -> Regex { } impl PluggableTransportConfig { - pub fn new(transports: Vec, path_to_binary: PathBuf) -> Result { + pub fn new( + transports: Vec, + path_to_binary: PathBuf, + ) -> Result { let transport_pattern = TRANSPORT_PATTERN.get_or_init(init_transport_pattern); // validate each transport for transport in &transports { if !transport_pattern.is_match(&transport) { - return Err(PluggableTransportConfigError::TransportNameInvalid(transport.clone())); + return Err(PluggableTransportConfigError::TransportNameInvalid( + transport.clone(), + )); } } // pluggable transport path must be absolute so we can fix it up for individual // TorProvider implementations if !path_to_binary.is_absolute() { - return Err(PluggableTransportConfigError::BinaryPathInvalid(format!("{:?}", path_to_binary.display()), "must be an absolute path".to_string())); + return Err(PluggableTransportConfigError::BinaryPathInvalid( + format!("{:?}", path_to_binary.display()), + "must be an absolute path".to_string(), + )); } - Ok(Self{transports, path_to_binary, options: Default::default()}) + Ok(Self { + transports, + path_to_binary, + options: Default::default(), + }) } pub fn transports(&self) -> &Vec { - &self.transports + &self.transports } pub fn path_to_binary(&self) -> &PathBuf { - &self.path_to_binary + &self.path_to_binary } pub fn options(&self) -> &Vec { - &self.options + &self.options } pub fn add_option(&mut self, arg: String) { @@ -519,7 +568,7 @@ pub struct BridgeLine { transport: String, address: SocketAddr, fingerprint: String, - keyvalues: Vec<(String,String)>, + keyvalues: Vec<(String, String)>, } #[derive(thiserror::Error, Debug)] @@ -550,7 +599,12 @@ pub enum BridgeLineError { } impl BridgeLine { - pub fn new(transport: String, address: SocketAddr, fingerprint: String, keyvalues: Vec<(String,String)>) -> Result { + pub fn new( + transport: String, + address: SocketAddr, + fingerprint: String, + keyvalues: Vec<(String, String)>, + ) -> Result { let transport_pattern = TRANSPORT_PATTERN.get_or_init(init_transport_pattern); // transports have a particular pattern @@ -564,10 +618,8 @@ impl BridgeLine { } static BRIDGE_FINGERPRINT_PATTERN: OnceLock = OnceLock::new(); - let bridge_fingerprint_pattern = BRIDGE_FINGERPRINT_PATTERN.get_or_init(|| { - Regex::new(r"(?m)^[0-9a-fA-F]{40}$") - .unwrap() - }); + let bridge_fingerprint_pattern = BRIDGE_FINGERPRINT_PATTERN + .get_or_init(|| Regex::new(r"(?m)^[0-9a-fA-F]{40}$").unwrap()); // fingerprint should be a sha1 hash if !bridge_fingerprint_pattern.is_match(&fingerprint) { @@ -576,14 +628,17 @@ impl BridgeLine { // validate key-values for (key, value) in &keyvalues { - if key.contains(' ') - || key.contains('=') - || key.len() == 0 { + if key.contains(' ') || key.contains('=') || key.len() == 0 { return Err(BridgeLineError::KeyValueInvalid(format!("{key}={value}"))); } } - Ok(Self{transport, address, fingerprint, keyvalues}) + Ok(Self { + transport, + address, + fingerprint, + keyvalues, + }) } pub fn transport(&self) -> &String { @@ -598,7 +653,7 @@ impl BridgeLine { &self.fingerprint } - pub fn keyvalues(&self) -> &Vec<(String,String)> { + pub fn keyvalues(&self) -> &Vec<(String, String)> { &self.keyvalues } @@ -607,7 +662,11 @@ impl BridgeLine { let transport = &self.transport; let address = self.address.to_string(); let fingerprint = self.fingerprint.to_string(); - let keyvalues: Vec = self.keyvalues.iter().map(|(key,value)| format!("{key}={value}")).collect(); + let keyvalues: Vec = self + .keyvalues + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect(); let keyvalues = keyvalues.join(" "); format!("{transport} {address} {fingerprint} {keyvalues}") @@ -643,23 +702,34 @@ impl FromStr for BridgeLine { // get the bridge options static BRIDGE_OPTION_PATTERN: OnceLock = OnceLock::new(); - let bridge_option_pattern = BRIDGE_OPTION_PATTERN.get_or_init(|| { - Regex::new(r"(?m)^(?[^=]+)=(?.*)$") - .unwrap() - }); + let bridge_option_pattern = BRIDGE_OPTION_PATTERN + .get_or_init(|| Regex::new(r"(?m)^(?[^=]+)=(?.*)$").unwrap()); - let mut keyvalues: Vec<(String,String)> = Default::default(); + let mut keyvalues: Vec<(String, String)> = Default::default(); while let Some(keyvalue) = tokens.next() { if let Some(caps) = bridge_option_pattern.captures(&keyvalue) { - let key = caps.name("key").expect("missing key group").as_str().to_string(); - let value = caps.name("value").expect("missing value group").as_str().to_string(); - keyvalues.push((key,value)); + let key = caps + .name("key") + .expect("missing key group") + .as_str() + .to_string(); + let value = caps + .name("value") + .expect("missing value group") + .as_str() + .to_string(); + keyvalues.push((key, value)); } else { return Err(BridgeLineError::KeyValueInvalid(keyvalue.to_string())); } } - BridgeLine::new(transport.to_string(), address, fingerprint.to_string(), keyvalues) + BridgeLine::new( + transport.to_string(), + address, + fingerprint.to_string(), + keyvalues, + ) } } diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 80062b067..29e29a45d 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -357,7 +357,7 @@ fn test_legacy_bootstrap() -> anyhow::Result<()> { let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_bootstrap"); - let tor_config = LegacyTorClientConfig::BundledTor{ + let tor_config = LegacyTorClientConfig::BundledTor { tor_bin_path: tor_path, data_directory: data_path, proxy_settings: None, @@ -377,7 +377,6 @@ fn test_legacy_pluggable_transport_bootstrap() -> anyhow::Result<()> { let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_pluggable_transport_bootstrap"); - // find the lyrebird bin let teb_path = std::env::var("TEB_PATH")?; if teb_path.is_empty() { @@ -391,14 +390,13 @@ fn test_legacy_pluggable_transport_bootstrap() -> anyhow::Result<()> { assert!(std::path::Path::is_file(&lyrebird_path)); // configure lyrebird pluggable transport - let pluggable_transport = PluggableTransportConfig::new( - vec!["obfs4".to_string()], - lyrebird_path)?; + let pluggable_transport = + PluggableTransportConfig::new(vec!["obfs4".to_string()], lyrebird_path)?; // obfs4 bridgeline let bridge_line = BridgeLine::from_str("obfs4 207.172.185.193:22223 F34AC0CDBC06918E54292A474578C99834A58893 cert=MjqosoyVylLQuLo4LH+eQ5hS7Z44s2CaMfQbIjJtn4bGRnvLv8ldSvSED5JpvWSxm09XXg iat-mode=0")?; - let tor_config = LegacyTorClientConfig::BundledTor{ + let tor_config = LegacyTorClientConfig::BundledTor { tor_bin_path: tor_path, data_directory: data_path, proxy_settings: None, @@ -418,7 +416,7 @@ fn test_legacy_onion_service() -> anyhow::Result<()> { let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_onion_service_server"); - let tor_config = LegacyTorClientConfig::BundledTor{ + let tor_config = LegacyTorClientConfig::BundledTor { tor_bin_path: tor_path.clone(), data_directory: data_path, proxy_settings: None, @@ -430,7 +428,7 @@ fn test_legacy_onion_service() -> anyhow::Result<()> { let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_onion_service_cient"); - let tor_config = LegacyTorClientConfig::BundledTor{ + let tor_config = LegacyTorClientConfig::BundledTor { tor_bin_path: tor_path, data_directory: data_path, proxy_settings: None, @@ -451,7 +449,7 @@ fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_authenticated_onion_service_server"); - let tor_config = LegacyTorClientConfig::BundledTor{ + let tor_config = LegacyTorClientConfig::BundledTor { tor_bin_path: tor_path.clone(), data_directory: data_path, proxy_settings: None, @@ -463,7 +461,7 @@ fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_authenticated_onion_service_cient"); - let tor_config = LegacyTorClientConfig::BundledTor{ + let tor_config = LegacyTorClientConfig::BundledTor { tor_bin_path: tor_path, data_directory: data_path, proxy_settings: None, @@ -473,7 +471,6 @@ fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { }; let client_provider = Box::new(LegacyTorClient::new(tor_config)?); - authenticated_onion_service_test(server_provider, client_provider) } @@ -482,15 +479,23 @@ fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { // #[cfg(test)] -fn start_system_tor_daemon(tor_path: &std::ffi::OsStr, name: &str, control_port: u16, socks_port: u16) -> anyhow::Result { - +fn start_system_tor_daemon( + tor_path: &std::ffi::OsStr, + name: &str, + control_port: u16, + socks_port: u16, +) -> anyhow::Result { let mut data_path = std::env::temp_dir(); data_path.push(name); std::fs::create_dir_all(&data_path)?; let default_torrc = data_path.join("default_torrc"); - { let _ = File::create(&default_torrc)?; } + { + let _ = File::create(&default_torrc)?; + } let torrc = data_path.join("torrc"); - { let _ = File::create(&torrc)?; } + { + let _ = File::create(&torrc)?; + } let tor_daemon = Command::new(tor_path) .stdout(Stdio::null()) @@ -524,7 +529,6 @@ fn start_system_tor_daemon(tor_path: &std::ffi::OsStr, name: &str, control_port: .arg(process::id().to_string()) .spawn()?; - Ok(tor_daemon) } @@ -534,20 +538,30 @@ fn start_system_tor_daemon(tor_path: &std::ffi::OsStr, name: &str, control_port: fn test_system_legacy_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - let mut server_tor_daemon = start_system_tor_daemon(tor_path.as_os_str(), "test_system_legacy_onion_service_server", 9251u16, 9250u16)?; - let mut client_tor_daemon = start_system_tor_daemon(tor_path.as_os_str(), "test_system_legacy_onion_service_client", 9351u16, 9350u16)?; + let mut server_tor_daemon = start_system_tor_daemon( + tor_path.as_os_str(), + "test_system_legacy_onion_service_server", + 9251u16, + 9250u16, + )?; + let mut client_tor_daemon = start_system_tor_daemon( + tor_path.as_os_str(), + "test_system_legacy_onion_service_client", + 9351u16, + 9350u16, + )?; // give daemons time to start std::thread::sleep(std::time::Duration::from_secs(5)); - let tor_config = LegacyTorClientConfig::SystemTor{ + let tor_config = LegacyTorClientConfig::SystemTor { tor_socks_addr: std::net::SocketAddr::from_str("127.0.0.1:9250")?, tor_control_addr: std::net::SocketAddr::from_str("127.0.0.1:9251")?, tor_control_passwd: "password".to_string(), }; let server_provider = Box::new(LegacyTorClient::new(tor_config)?); - let tor_config = LegacyTorClientConfig::SystemTor{ + let tor_config = LegacyTorClientConfig::SystemTor { tor_socks_addr: std::net::SocketAddr::from_str("127.0.0.1:9350")?, tor_control_addr: std::net::SocketAddr::from_str("127.0.0.1:9351")?, tor_control_passwd: "password".to_string(), @@ -568,20 +582,30 @@ fn test_system_legacy_onion_service() -> anyhow::Result<()> { fn test_system_legacy_authenticated_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - let mut server_tor_daemon = start_system_tor_daemon(tor_path.as_os_str(), "test_system_legacy_authenticated_onion_service_server", 9251u16, 9250u16)?; - let mut client_tor_daemon = start_system_tor_daemon(tor_path.as_os_str(), "test_system_legacy_authenticated_onion_service_client", 9351u16, 9350u16)?; + let mut server_tor_daemon = start_system_tor_daemon( + tor_path.as_os_str(), + "test_system_legacy_authenticated_onion_service_server", + 9251u16, + 9250u16, + )?; + let mut client_tor_daemon = start_system_tor_daemon( + tor_path.as_os_str(), + "test_system_legacy_authenticated_onion_service_client", + 9351u16, + 9350u16, + )?; // give daemons time to start std::thread::sleep(std::time::Duration::from_secs(5)); - let tor_config = LegacyTorClientConfig::SystemTor{ + let tor_config = LegacyTorClientConfig::SystemTor { tor_socks_addr: std::net::SocketAddr::from_str("127.0.0.1:9250")?, tor_control_addr: std::net::SocketAddr::from_str("127.0.0.1:9251")?, tor_control_passwd: "password".to_string(), }; let server_provider = Box::new(LegacyTorClient::new(tor_config)?); - let tor_config = LegacyTorClientConfig::SystemTor{ + let tor_config = LegacyTorClientConfig::SystemTor { tor_socks_addr: std::net::SocketAddr::from_str("127.0.0.1:9350")?, tor_control_addr: std::net::SocketAddr::from_str("127.0.0.1:9351")?, tor_control_passwd: "password".to_string(), @@ -664,7 +688,7 @@ fn test_mixed_arti_client_legacy_onion_service() -> anyhow::Result<()> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); data_path.push("test_arti_legacy_basic_onion_service_client"); - let tor_config = LegacyTorClientConfig::BundledTor{ + let tor_config = LegacyTorClientConfig::BundledTor { tor_bin_path: tor_path, data_directory: data_path, proxy_settings: None, @@ -685,7 +709,7 @@ fn test_mixed_legacy_arti_client_onion_service() -> anyhow::Result<()> { let mut data_path = std::env::temp_dir(); data_path.push("test_legacy_arty_basic_onion_service_client"); - let tor_config = LegacyTorClientConfig::BundledTor{ + let tor_config = LegacyTorClientConfig::BundledTor { tor_bin_path: tor_path, data_directory: data_path, proxy_settings: None, From 55ae36c1e6580b3adfa6b61f2e3763811280ab30 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Thu, 18 Jul 2024 01:54:08 +0000 Subject: [PATCH 108/184] tor-interface: fix test compile failure when not using legacy-tor-provider feature not enabled --- tor-interface/tests/tor_provider.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 29e29a45d..8d3fa5c5d 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -479,6 +479,7 @@ fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { // #[cfg(test)] +#[cfg(feature = "legacy-tor-provider")] fn start_system_tor_daemon( tor_path: &std::ffi::OsStr, name: &str, From 2eb5020647b88889581d78c25190ac630357761a Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 21 Jul 2024 03:22:53 +0000 Subject: [PATCH 109/184] tor-interface: remove unused and now unnecessary Ed25519PrivateKey::sign_message_ex() method --- tor-interface/src/tor_crypto.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 4f574fb23..5c27d826e 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -330,20 +330,11 @@ impl Ed25519PrivateKey { key_blob } - pub fn sign_message_ex( - &self, - _public_key: &Ed25519PublicKey, - message: &[u8], - ) -> Ed25519Signature { + pub fn sign_message(&self, message: &[u8]) -> Ed25519Signature { let signature = self.expanded_keypair.sign(message); Ed25519Signature { signature } } - pub fn sign_message(&self, message: &[u8]) -> Ed25519Signature { - let public_key = Ed25519PublicKey::from_private_key(self); - self.sign_message_ex(&public_key, message) - } - pub fn to_bytes(&self) -> [u8; ED25519_PRIVATE_KEY_SIZE] { self.expanded_keypair.to_secret_key_bytes() } From 778a012e671b55b673f7607668528177406ea896 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Sun, 21 Jul 2024 22:00:07 +0000 Subject: [PATCH 110/184] tor-interface: removed unused Ed25519PublicKey::to_base32() method --- tor-interface/src/tor_crypto.rs | 6 +----- tor-interface/tests/tor_crypto.rs | 2 -- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 5c27d826e..8457f4502 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -4,7 +4,7 @@ use std::str; // extern crates use curve25519_dalek::Scalar; -use data_encoding::{BASE32, BASE32_NOPAD, BASE64}; +use data_encoding::{BASE32_NOPAD, BASE64}; use data_encoding_macro::new_encoding; #[cfg(feature = "legacy-tor-provider")] use rand::distributions::Alphanumeric; @@ -428,10 +428,6 @@ impl Ed25519PublicKey { } } - pub fn to_base32(&self) -> String { - BASE32.encode(self.as_bytes()) - } - pub fn as_bytes(&self) -> &[u8; ED25519_PUBLIC_KEY_SIZE] { self.public_key.as_bytes() } diff --git a/tor-interface/tests/tor_crypto.rs b/tor-interface/tests/tor_crypto.rs index 522605c8c..5fda4e23b 100644 --- a/tor-interface/tests/tor_crypto.rs +++ b/tor-interface/tests/tor_crypto.rs @@ -17,7 +17,6 @@ fn test_crypto_ed25519() -> Result<(), anyhow::Error> { 0xe1u8, 0x45u8, 0x55u8, 0xeeu8, 0xb9u8, 0x32u8, 0xa0u8, 0x5cu8, 0x39u8, 0x5au8, 0xe2u8, 0x02u8, 0x83u8, 0x55u8, 0x27u8, 0x89u8, 0x6au8, 0x1fu8, 0x2fu8, 0x3du8, 0xc5u8, ]; - let public_base32 = "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCQ===="; let service_id_string = "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd"; assert!(V3OnionServiceId::is_valid(&service_id_string)); @@ -49,7 +48,6 @@ fn test_crypto_ed25519() -> Result<(), anyhow::Error> { assert_eq!(public_key, Ed25519PublicKey::from_service_id(&service_id)?); assert_eq!(public_key, Ed25519PublicKey::from_private_key(&private_key)); assert_eq!(service_id, V3OnionServiceId::from_public_key(&public_key)); - assert_eq!(public_base32, public_key.to_base32()); let signature = private_key.sign_message(&message); assert_eq!(signature, Ed25519Signature::from_raw(&signature_raw)?); From 18e421bea5eaee0e2d0d48dc1e8467e8d23fe49f Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 24 Jul 2024 03:22:57 +0000 Subject: [PATCH 111/184] tor-interface: fixed bug where DomainAddr could be constructed with a ".onion" suffix --- tor-interface/src/tor_provider.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 907f83c5d..6dd7032e7 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -143,10 +143,13 @@ impl TryFrom<(String, u16)> for DomainAddr { if let Ok(domain) = domain_to_ascii_cow(domain.as_bytes(), AsciiDenyList::URL) { let domain = domain.to_string(); if let Ok(domain) = Name::>::from_str(domain.as_ref()) { - return Ok(Self { - domain: domain.to_string(), - port, - }); + let domain = domain.to_string(); + if !domain.ends_with(".onion") { + return Ok(Self { + domain, + port, + }); + } } } Err(DomainAddrParseError::Generic(format!( @@ -211,9 +214,7 @@ impl FromStr for TargetAddr { } else if let Ok(onion_addr) = OnionAddr::from_str(s) { return Ok(TargetAddr::OnionService(onion_addr)); } else if let Ok(domain_addr) = DomainAddr::from_str(s) { - if !domain_addr.domain().ends_with(".onion") { - return Ok(TargetAddr::Domain(domain_addr)); - } + return Ok(TargetAddr::Domain(domain_addr)); } Err(TargetAddrParseError::Generic(s.to_string())) } From 084df5956b12c69149a22c064fafaf224c3c8eeb Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 24 Jul 2024 03:42:10 +0000 Subject: [PATCH 112/184] tor-interface: fixed bug where OnionStream::local_addr() was not implemented --- tor-interface/src/tor_provider.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 6dd7032e7..3fbda0199 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -299,7 +299,7 @@ impl OnionStream { } pub fn local_addr(&self) -> Option { - None + self.local_addr.clone() } pub fn try_clone(&self) -> Result { From 6c06275d9b8a4b9f6af1e2743820b886e2c91b4b Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 24 Jul 2024 03:45:43 +0000 Subject: [PATCH 113/184] tor-interface: renamed TargetAddr::Ip to TargetAddr::Socket for consistency --- tor-interface/src/arti_client_tor_client.rs | 2 +- tor-interface/src/legacy_tor_client.rs | 2 +- tor-interface/src/tor_provider.rs | 12 ++++++------ tor-interface/tests/tor_provider.rs | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 3a8117c1f..b63a669a9 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -320,7 +320,7 @@ impl TorProvider for ArtiClientTorClient { // connect to onion service let arti_target = match target.clone() { - TargetAddr::Ip(socket_addr) => socket_addr.into_tor_addr_dangerously(), + TargetAddr::Socket(socket_addr) => socket_addr.into_tor_addr_dangerously(), TargetAddr::Domain(domain_addr) => { (domain_addr.domain(), domain_addr.port()).into_tor_addr() } diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 93e0cd2c0..8aaa45603 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -631,7 +631,7 @@ impl TorProvider for LegacyTorClient { // our target let socks_target = match target.clone() { - TargetAddr::Ip(socket_addr) => socks::TargetAddr::Ip(socket_addr), + TargetAddr::Socket(socket_addr) => socks::TargetAddr::Ip(socket_addr), TargetAddr::Domain(domain_addr) => { socks::TargetAddr::Domain(domain_addr.domain().to_string(), domain_addr.port()) } diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 3fbda0199..18302951e 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -186,7 +186,7 @@ impl FromStr for DomainAddr { #[derive(Clone, Debug)] pub enum TargetAddr { - Ip(std::net::SocketAddr), + Socket(std::net::SocketAddr), OnionService(OnionAddr), Domain(DomainAddr), } @@ -210,7 +210,7 @@ impl FromStr for TargetAddr { type Err = TargetAddrParseError; fn from_str(s: &str) -> Result { if let Ok(socket_addr) = SocketAddr::from_str(s) { - return Ok(TargetAddr::Ip(socket_addr)); + return Ok(TargetAddr::Socket(socket_addr)); } else if let Ok(onion_addr) = OnionAddr::from_str(s) { return Ok(TargetAddr::OnionService(onion_addr)); } else if let Ok(domain_addr) = DomainAddr::from_str(s) { @@ -223,7 +223,7 @@ impl FromStr for TargetAddr { impl std::fmt::Display for TargetAddr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - TargetAddr::Ip(socket_addr) => socket_addr.fmt(f), + TargetAddr::Socket(socket_addr) => socket_addr.fmt(f), TargetAddr::OnionService(onion_addr) => onion_addr.fmt(f), TargetAddr::Domain(domain_addr) => domain_addr.fmt(f), } @@ -352,7 +352,7 @@ pub struct Socks4ProxyConfig { impl Socks4ProxyConfig { pub fn new(address: TargetAddr) -> Result { let port = match &address { - TargetAddr::Ip(addr) => addr.port(), + TargetAddr::Socket(addr) => addr.port(), TargetAddr::Domain(addr) => addr.port(), TargetAddr::OnionService(_) => { return Err(ProxyConfigError::Generic( @@ -382,7 +382,7 @@ impl Socks5ProxyConfig { password: Option, ) -> Result { let port = match &address { - TargetAddr::Ip(addr) => addr.port(), + TargetAddr::Socket(addr) => addr.port(), TargetAddr::Domain(addr) => addr.port(), TargetAddr::OnionService(_) => { return Err(ProxyConfigError::Generic( @@ -433,7 +433,7 @@ impl HttpsProxyConfig { password: Option, ) -> Result { let port = match &address { - TargetAddr::Ip(addr) => addr.port(), + TargetAddr::Socket(addr) => addr.port(), TargetAddr::Domain(addr) => addr.port(), TargetAddr::OnionService(_) => { return Err(ProxyConfigError::Generic( diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 8d3fa5c5d..dca26e375 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -780,7 +780,7 @@ fn test_tor_provider_target_addr() -> anyhow::Result<()> { for target_addr_str in valid_ip_addr { match TargetAddr::from_str(target_addr_str) { - Ok(TargetAddr::Ip(socket_addr)) => println!("{} => {}", target_addr_str, socket_addr), + Ok(TargetAddr::Socket(socket_addr)) => println!("{} => {}", target_addr_str, socket_addr), Ok(TargetAddr::OnionService(onion_addr)) => panic!( "unexpected conversion: {} => OnionService({})", target_addr_str, onion_addr @@ -800,7 +800,7 @@ fn test_tor_provider_target_addr() -> anyhow::Result<()> { for target_addr_str in valid_onion_addr { match TargetAddr::from_str(target_addr_str) { - Ok(TargetAddr::Ip(socket_addr)) => panic!( + Ok(TargetAddr::Socket(socket_addr)) => panic!( "unexpected conversion: {} => Ip({})", target_addr_str, socket_addr ), @@ -841,7 +841,7 @@ fn test_tor_provider_target_addr() -> anyhow::Result<()> { for target_addr_str in valid_domain_addr { match TargetAddr::from_str(target_addr_str) { - Ok(TargetAddr::Ip(socket_addr)) => panic!( + Ok(TargetAddr::Socket(socket_addr)) => panic!( "unexpected conversion: {} => SocketAddr({})", target_addr_str, socket_addr ), @@ -901,7 +901,7 @@ fn test_tor_provider_target_addr() -> anyhow::Result<()> { for target_addr_str in invalid_target_addr { match TargetAddr::from_str(target_addr_str) { - Ok(TargetAddr::Ip(socket_addr)) => panic!( + Ok(TargetAddr::Socket(socket_addr)) => panic!( "unexpected conversion: {} => SocketAddr({})", target_addr_str, socket_addr ), From fdc968515a9cf01aac56a0b2b158baf2791e22aa Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 24 Jul 2024 03:46:56 +0000 Subject: [PATCH 114/184] tor-interface: merged various parse errors into one --- tor-interface/src/tor_provider.rs | 54 +++++++++++++------------------ 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 18302951e..ddd5b352b 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -17,6 +17,19 @@ use regex::Regex; // internal crates use crate::tor_crypto::*; + +/// Various `tor_provider` errors. +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Failed to parse '{0}' as {1}")] + /// Failure parsing some string into a type + ParseFailure(String, String), + + #[error("{0}")] + /// Other miscellaneous error + Generic(String), +} + // // OnionAddr // @@ -55,14 +68,8 @@ pub enum OnionAddr { V3(OnionAddrV3), } -#[derive(thiserror::Error, Debug)] -pub enum OnionAddrParseError { - #[error("Failed to parse '{0}' as OnionAddr")] - Generic(String), -} - impl FromStr for OnionAddr { - type Err = OnionAddrParseError; + type Err = Error; fn from_str(s: &str) -> Result { static ONION_SERVICE_PATTERN: OnceLock = OnceLock::new(); let onion_service_pattern = ONION_SERVICE_PATTERN.get_or_init(|| { @@ -84,7 +91,7 @@ impl FromStr for OnionAddr { return Ok(OnionAddr::V3(OnionAddrV3::new(service_id, port))); } } - Err(Self::Err::Generic(s.to_string())) + Err(Self::Err::ParseFailure(s.to_string(), "OnionAddr".to_string())) } } @@ -129,14 +136,8 @@ impl std::fmt::Display for DomainAddr { } } -#[derive(thiserror::Error, Debug)] -pub enum DomainAddrParseError { - #[error("Unable to parse '{0}' as DomainAddr")] - Generic(String), -} - impl TryFrom<(String, u16)> for DomainAddr { - type Error = DomainAddrParseError; + type Error = Error; fn try_from(value: (String, u16)) -> Result { let (domain, port) = (&value.0, value.1); @@ -152,15 +153,15 @@ impl TryFrom<(String, u16)> for DomainAddr { } } } - Err(DomainAddrParseError::Generic(format!( + Err(Self::Error::ParseFailure(format!( "{}:{}", domain, port - ))) + ), "DomainAddr".to_string())) } } impl FromStr for DomainAddr { - type Err = DomainAddrParseError; + type Err = Error; fn from_str(s: &str) -> Result { static DOMAIN_PATTERN: OnceLock = OnceLock::new(); let domain_pattern = DOMAIN_PATTERN @@ -176,7 +177,7 @@ impl FromStr for DomainAddr { return Self::try_from((domain, port)); } } - Err(DomainAddrParseError::Generic(s.to_string())) + Err(Self::Err::ParseFailure(s.to_string(), "DomainAddr".to_string())) } } @@ -200,14 +201,8 @@ impl From<(V3OnionServiceId, u16)> for TargetAddr { } } -#[derive(thiserror::Error, Debug)] -pub enum TargetAddrParseError { - #[error("Unable to parse '{0}' as TargetAddr")] - Generic(String), -} - impl FromStr for TargetAddr { - type Err = TargetAddrParseError; + type Err = Error; fn from_str(s: &str) -> Result { if let Ok(socket_addr) = SocketAddr::from_str(s) { return Ok(TargetAddr::Socket(socket_addr)); @@ -216,7 +211,7 @@ impl FromStr for TargetAddr { } else if let Ok(domain_addr) = DomainAddr::from_str(s) { return Ok(TargetAddr::Domain(domain_addr)); } - Err(TargetAddrParseError::Generic(s.to_string())) + Err(Self::Err::ParseFailure(s.to_string(), "TargetAddr".to_string())) } } @@ -734,11 +729,6 @@ impl FromStr for BridgeLine { } } -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("{0}")] - Generic(String), -} pub trait TorProvider: Send { fn update(&mut self) -> Result, Error>; From cc2a09dd4cc23adb03379b78b2e769e176607b11 Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 24 Jul 2024 05:32:44 +0000 Subject: [PATCH 115/184] tor-interface: de-duplicated copy-pasted OnionListener logic in favor of using a lambda for cleanup --- tor-interface/src/arti_client_tor_client.rs | 41 +------------- tor-interface/src/legacy_tor_client.rs | 52 +----------------- tor-interface/src/mock_tor_client.rs | 44 +-------------- tor-interface/src/tor_provider.rs | 61 ++++++++++++++++++--- 4 files changed, 61 insertions(+), 137 deletions(-) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index b63a669a9..ce177aa80 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -1,5 +1,4 @@ // standard -use std::io::ErrorKind; use std::net::SocketAddr; use std::ops::DerefMut; use std::path::{Path, PathBuf}; @@ -85,36 +84,6 @@ impl From for crate::tor_provider::Error { } } -pub struct ArtiClientOnionListener { - listener: std::net::TcpListener, - onion_addr: OnionAddr, - // onion service terminates when this is dropped, we don't actually use it - // for anything after construction - _onion_service: Arc, -} - -impl OnionListenerImpl for ArtiClientOnionListener { - fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { - self.listener.set_nonblocking(nonblocking) - } - fn accept(&self) -> Result, std::io::Error> { - match self.listener.accept() { - Ok((stream, _socket_addr)) => Ok(Some(OnionStream { - stream, - local_addr: Some(self.onion_addr.clone()), - peer_addr: None, - })), - Err(err) => { - if err.kind() == ErrorKind::WouldBlock { - Ok(None) - } else { - Err(err) - } - } - } - } -} - pub struct ArtiClientTorClient { tokio_runtime: Arc, arti_client: TorClient, @@ -549,14 +518,8 @@ impl TorProvider for ArtiClientTorClient { } }); - // return our OnionListener - let onion_listener = Box::new(ArtiClientOnionListener { - listener, - onion_addr, - _onion_service: onion_service, - }); - - Ok(OnionListener { onion_listener }) + // onion-service is torn down when `onion_service` is dropped + Ok(OnionListener::new::>(listener, onion_addr, onion_service, |_|{})) } fn generate_token(&mut self) -> CircuitToken { diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 8aaa45603..bb50bbb9b 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -1,11 +1,8 @@ // standard -use std::boxed::Box; use std::collections::BTreeMap; use std::convert::From; use std::default::Default; -use std::io::ErrorKind; use std::net::{SocketAddr, TcpListener}; -use std::ops::Drop; use std::option::Option; use std::path::PathBuf; use std::string::ToString; @@ -148,45 +145,6 @@ impl Default for LegacyCircuitToken { } } -// -// LegacyOnionListener -// - -pub struct LegacyOnionListener { - listener: TcpListener, - is_active: Arc, - onion_addr: OnionAddr, -} - -impl OnionListenerImpl for LegacyOnionListener { - fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { - self.listener.set_nonblocking(nonblocking) - } - - fn accept(&self) -> Result, std::io::Error> { - match self.listener.accept() { - Ok((stream, _socket_addr)) => Ok(Some(OnionStream { - stream, - local_addr: Some(self.onion_addr.clone()), - peer_addr: None, - })), - Err(err) => { - if err.kind() == ErrorKind::WouldBlock { - Ok(None) - } else { - Err(err) - } - } - } - } -} - -impl Drop for LegacyOnionListener { - fn drop(&mut self) { - self.is_active.store(false, atomic::Ordering::Relaxed); - } -} - // // LegacyTorClientConfig // @@ -714,13 +672,9 @@ impl TorProvider for LegacyTorClient { self.onion_services .push((service_id, Arc::clone(&is_active))); - let onion_listener = Box::new(LegacyOnionListener { - listener, - is_active, - onion_addr, - }); - - Ok(OnionListener { onion_listener }) + Ok(OnionListener::new(listener, onion_addr, is_active, |is_active| { + is_active.store(false, atomic::Ordering::Relaxed); + })) } fn generate_token(&mut self) -> CircuitToken { diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index bbddcf285..b489406b4 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -1,6 +1,5 @@ // standard use std::collections::BTreeMap; -use std::io::ErrorKind; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::sync::{atomic, Arc, Mutex}; @@ -48,40 +47,6 @@ impl From for crate::tor_provider::Error { } } -pub struct MockOnionListener { - listener: std::net::TcpListener, - is_active: Arc, - onion_addr: OnionAddr, -} - -impl OnionListenerImpl for MockOnionListener { - fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { - self.listener.set_nonblocking(nonblocking) - } - fn accept(&self) -> Result, std::io::Error> { - match self.listener.accept() { - Ok((stream, _socket_addr)) => Ok(Some(OnionStream { - stream, - local_addr: Some(self.onion_addr.clone()), - peer_addr: None, - })), - Err(err) => { - if err.kind() == ErrorKind::WouldBlock { - Ok(None) - } else { - Err(err) - } - } - } - } -} - -impl Drop for MockOnionListener { - fn drop(&mut self) { - self.is_active.store(false, atomic::Ordering::Relaxed); - } -} - struct MockTorNetwork { onion_services: Option, SocketAddr)>>, } @@ -341,13 +306,10 @@ impl TorProvider for MockTorClient { self.events .push(TorEvent::OnionServicePublished { service_id }); - let onion_listener = Box::new(MockOnionListener { - listener, - is_active, - onion_addr, - }); - Ok(OnionListener { onion_listener }) + Ok(OnionListener::new(listener, onion_addr, is_active, |is_active| { + is_active.store(false, atomic::Ordering::Relaxed); + })) } fn generate_token(&mut self) -> CircuitToken { diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index ddd5b352b..05211d2d1 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -1,8 +1,9 @@ // standard +use std::any::Any; use std::boxed::Box; use std::convert::TryFrom; use std::io::{Read, Write}; -use std::net::{SocketAddr, TcpStream}; +use std::net::{SocketAddr, TcpListener, TcpStream}; use std::ops::{Deref, DerefMut}; use std::path::PathBuf; use std::str::FromStr; @@ -310,22 +311,66 @@ impl OnionStream { // Onion Listener // -pub trait OnionListenerImpl: Send { - fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error>; - fn accept(&self) -> Result, std::io::Error>; -} pub struct OnionListener { - pub(crate) onion_listener: Box, + pub(crate) listener: TcpListener, + pub(crate) onion_addr: OnionAddr, + pub(crate) data: Option>, + pub(crate) drop: Option) + Send>>, } impl OnionListener { + pub(crate) fn new( + listener: TcpListener, + onion_addr: OnionAddr, + data: T, + mut drop: impl FnMut(T) + 'static + Send) -> Self { + // marshall our data into an Any + let data: Option> = Some(Box::new(data)); + // marhsall our drop into a function which takes an Any + let drop: Option) + Send>> = Some(Box::new(move |data: Box| { + // encapsulate extracting our data from the Any + if let Ok(data) = data.downcast::() { + // and call our provided drop + drop(*data); + } + })); + + Self{ + listener, + onion_addr, + data, + drop, + } + } + pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { - self.onion_listener.set_nonblocking(nonblocking) + self.listener.set_nonblocking(nonblocking) } pub fn accept(&self) -> Result, std::io::Error> { - self.onion_listener.accept() + match self.listener.accept() { + Ok((stream, _socket_addr)) => Ok(Some(OnionStream { + stream, + local_addr: Some(self.onion_addr.clone()), + peer_addr: None, + })), + Err(err) => { + if err.kind() == std::io::ErrorKind::WouldBlock { + Ok(None) + } else { + Err(err) + } + } + } + } +} + +impl Drop for OnionListener { + fn drop(&mut self) { + if let (Some(data), Some(mut drop)) = (self.data.take(), self.drop.take()) { + drop(data) + } } } From 6e6f97d8b967dcd1370952eb36524b66153cdb28 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 22 Jul 2024 02:04:38 +0000 Subject: [PATCH 116/184] tor-interface: add crate README.md --- tor-interface/README.md | 65 ++++++++++++++++++++++++++++++++++++++++ tor-interface/src/lib.rs | 2 ++ 2 files changed, 67 insertions(+) create mode 100644 tor-interface/README.md diff --git a/tor-interface/README.md b/tor-interface/README.md new file mode 100644 index 000000000..c44f30bac --- /dev/null +++ b/tor-interface/README.md @@ -0,0 +1,65 @@ +# Tor-Interface + +Developer-friendly crate providing connectivity to the [Tor Network](https://en.wikipedia.org/wiki/Tor_(network)) and functionality for interacting with Tor-specific cryptographic types. + +This crate is *not* meant to be a general purpose Tor Controller nor does it aim to expose all of the functionality of the underlying Tor implementations. This crate also does not implement any of the Tor Network functionality itself, instead wrapping lower-level implementations. + +## Overview + +The `tor-interface` crate provides the `TorProvider` trait with 3 concrete implementations: + +- ArtiClientTorClient: an experimental wrapper around the [`arti-client`](https://crates.io/crates/arti-client) crate; enabled using the **arti-client-tor-provider** feature flag. +- LegacyTorClient: a wrapper around either an owned or system-provided legacy c-tor daemon (aka 'little-t tor') with some basic configuration options; enabled using the **legacy-tor-provider** feature flag. +- MockTorClient: an in-process, mock implementation which makes no actual connections outside of localhost; enabled with the **mock-tor-provider** feature flag. + +The `TorProvider` trait defines methods for connecting to various types of target addresses (ip, domains, and onion-services) and for creating onion-services. + +## ⚠ Warning ⚠ + +The **arti-client-tor-provider** feature is experimental is not fully implemented. It also depends on the [`arti-client`](https://crates.io/crates/arti-client) crate which is still under active development and is generally not yet ready for production use. + +## Usage + +The following code snippet creates a `LegacyTorClient` which starts a bundled tor daemon, bootstraps, and attempts to connect to [www.example.com](www.example.com). + +```rust +# use std::str::FromStr; +# use std::net::TcpStream; +# use tor_interface::legacy_tor_client::{LegacyTorClient, LegacyTorClientConfig}; +# use tor_interface::tor_provider::{OnionStream, TargetAddr, TorEvent, TorProvider}; +# return; +// construct legacy tor client config +let tor_path = std::path::PathBuf::from_str("/usr/bin/tor").unwrap(); +let mut data_path = std::env::temp_dir(); +data_path.push("tor_data"); + +let tor_config = LegacyTorClientConfig::BundledTor { + tor_bin_path: tor_path, + data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, +}; +// create client from config +let mut tor_client = LegacyTorClient::new(tor_config).unwrap(); + +// bootstrap tor +let mut bootstrap_complete = false; +while !bootstrap_complete { + for event in tor_client.update().unwrap().iter() { + match event { + TorEvent::BootstrapComplete => { + bootstrap_complete = true; + }, + _ => {}, + } + } +} + +// connect to example.com +let target_addr = TargetAddr::from_str("www.example.com:80").unwrap(); +let mut stream: OnionStream = tor_client.connect(target_addr, None).unwrap(); +// and convert to a std::net::TcpStream +let stream: TcpStream = stream.into(); +``` diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index 926ea8405..34cdd02cf 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -1,3 +1,5 @@ +#![doc = include_str!("../README.md")] + #[cfg(feature = "arti-client-tor-provider")] pub mod arti_client_tor_client; #[cfg(feature = "legacy-tor-provider")] From e07bfb7850e7217e3e99419f6904c26c47dcd096 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 22 Jul 2024 03:41:23 +0000 Subject: [PATCH 117/184] tor-interface: migrate proxy configuration implementation to seperate proxy module --- tor-interface/CMakeLists.txt | 1 + tor-interface/src/legacy_tor_client.rs | 1 + tor-interface/src/lib.rs | 2 + tor-interface/src/proxy.rs | 151 ++++++++++++++++++++++++ tor-interface/src/tor_provider.rs | 153 ------------------------- 5 files changed, 155 insertions(+), 153 deletions(-) create mode 100644 tor-interface/src/proxy.rs diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 34ddb5f85..4f31d92be 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -8,6 +8,7 @@ set(tor_interface_sources src/legacy_tor_version.rs src/lib.rs src/mock_tor_client.rs + src/proxy.rs src/tor_crypto.rs src/tor_provider.rs) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index bb50bbb9b..a81aa9750 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -17,6 +17,7 @@ use crate::legacy_tor_control_stream::*; use crate::legacy_tor_controller::*; use crate::legacy_tor_process::*; use crate::legacy_tor_version::*; +use crate::proxy::*; use crate::tor_crypto::*; use crate::tor_provider; use crate::tor_provider::*; diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index 34cdd02cf..d9eecb0dd 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -14,5 +14,7 @@ mod legacy_tor_process; mod legacy_tor_version; #[cfg(feature = "mock-tor-provider")] pub mod mock_tor_client; +#[cfg(feature = "legacy-tor-provider")] +pub mod proxy; pub mod tor_crypto; pub mod tor_provider; diff --git a/tor-interface/src/proxy.rs b/tor-interface/src/proxy.rs new file mode 100644 index 000000000..b4d6157ca --- /dev/null +++ b/tor-interface/src/proxy.rs @@ -0,0 +1,151 @@ +// internal crates +use crate::tor_provider::TargetAddr; + +#[derive(thiserror::Error, Debug)] +pub enum ProxyConfigError { + #[error("{0}")] + Generic(String), +} + +#[derive(Clone, Debug)] +pub struct Socks4ProxyConfig { + pub(crate) address: TargetAddr, +} + +impl Socks4ProxyConfig { + pub fn new(address: TargetAddr) -> Result { + let port = match &address { + TargetAddr::Socket(addr) => addr.port(), + TargetAddr::Domain(addr) => addr.port(), + TargetAddr::OnionService(_) => { + return Err(ProxyConfigError::Generic( + "proxy address may not be onion service".to_string(), + )) + } + }; + if port == 0 { + return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); + } + + Ok(Self { address }) + } +} + +#[derive(Clone, Debug)] +pub struct Socks5ProxyConfig { + pub(crate) address: TargetAddr, + pub(crate) username: Option, + pub(crate) password: Option, +} + +impl Socks5ProxyConfig { + pub fn new( + address: TargetAddr, + username: Option, + password: Option, + ) -> Result { + let port = match &address { + TargetAddr::Socket(addr) => addr.port(), + TargetAddr::Domain(addr) => addr.port(), + TargetAddr::OnionService(_) => { + return Err(ProxyConfigError::Generic( + "proxy address may not be onion service".to_string(), + )) + } + }; + if port == 0 { + return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); + } + + // username must be less than 255 bytes + if let Some(username) = &username { + if username.len() > 255 { + return Err(ProxyConfigError::Generic( + "socks5 username must be <= 255 bytes".to_string(), + )); + } + } + // password must be less than 255 bytes + if let Some(password) = &password { + if password.len() > 255 { + return Err(ProxyConfigError::Generic( + "socks5 password must be <= 255 bytes".to_string(), + )); + } + } + + Ok(Self { + address, + username, + password, + }) + } +} + +#[derive(Clone, Debug)] +pub struct HttpsProxyConfig { + pub(crate) address: TargetAddr, + pub(crate) username: Option, + pub(crate) password: Option, +} + +impl HttpsProxyConfig { + pub fn new( + address: TargetAddr, + username: Option, + password: Option, + ) -> Result { + let port = match &address { + TargetAddr::Socket(addr) => addr.port(), + TargetAddr::Domain(addr) => addr.port(), + TargetAddr::OnionService(_) => { + return Err(ProxyConfigError::Generic( + "proxy address may not be onion service".to_string(), + )) + } + }; + if port == 0 { + return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); + } + + // username may not contain ':' character (per RFC 2617) + if let Some(username) = &username { + if username.contains(':') { + return Err(ProxyConfigError::Generic( + "username may not contain ':' character".to_string(), + )); + } + } + + Ok(Self { + address, + username, + password, + }) + } +} + +#[derive(Clone, Debug)] +pub enum ProxyConfig { + Socks4(Socks4ProxyConfig), + Socks5(Socks5ProxyConfig), + Https(HttpsProxyConfig), +} + +impl From for ProxyConfig { + fn from(config: Socks4ProxyConfig) -> Self { + ProxyConfig::Socks4(config) + } +} + +impl From for ProxyConfig { + fn from(config: Socks5ProxyConfig) -> Self { + ProxyConfig::Socks5(config) + } +} + +impl From for ProxyConfig { + fn from(config: HttpsProxyConfig) -> Self { + ProxyConfig::Https(config) + } +} \ No newline at end of file diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 05211d2d1..1d0972e97 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -374,159 +374,6 @@ impl Drop for OnionListener { } } -// -// ProxyConfig -// - -#[derive(thiserror::Error, Debug)] -pub enum ProxyConfigError { - #[error("{0}")] - Generic(String), -} - -#[derive(Clone, Debug)] -pub struct Socks4ProxyConfig { - pub(crate) address: TargetAddr, -} - -impl Socks4ProxyConfig { - pub fn new(address: TargetAddr) -> Result { - let port = match &address { - TargetAddr::Socket(addr) => addr.port(), - TargetAddr::Domain(addr) => addr.port(), - TargetAddr::OnionService(_) => { - return Err(ProxyConfigError::Generic( - "proxy address may not be onion service".to_string(), - )) - } - }; - if port == 0 { - return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); - } - - Ok(Self { address }) - } -} - -#[derive(Clone, Debug)] -pub struct Socks5ProxyConfig { - pub(crate) address: TargetAddr, - pub(crate) username: Option, - pub(crate) password: Option, -} - -impl Socks5ProxyConfig { - pub fn new( - address: TargetAddr, - username: Option, - password: Option, - ) -> Result { - let port = match &address { - TargetAddr::Socket(addr) => addr.port(), - TargetAddr::Domain(addr) => addr.port(), - TargetAddr::OnionService(_) => { - return Err(ProxyConfigError::Generic( - "proxy address may not be onion service".to_string(), - )) - } - }; - if port == 0 { - return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); - } - - // username must be less than 255 bytes - if let Some(username) = &username { - if username.len() > 255 { - return Err(ProxyConfigError::Generic( - "socks5 username must be <= 255 bytes".to_string(), - )); - } - } - // password must be less than 255 bytes - if let Some(password) = &password { - if password.len() > 255 { - return Err(ProxyConfigError::Generic( - "socks5 password must be <= 255 bytes".to_string(), - )); - } - } - - Ok(Self { - address, - username, - password, - }) - } -} - -#[derive(Clone, Debug)] -pub struct HttpsProxyConfig { - pub(crate) address: TargetAddr, - pub(crate) username: Option, - pub(crate) password: Option, -} - -impl HttpsProxyConfig { - pub fn new( - address: TargetAddr, - username: Option, - password: Option, - ) -> Result { - let port = match &address { - TargetAddr::Socket(addr) => addr.port(), - TargetAddr::Domain(addr) => addr.port(), - TargetAddr::OnionService(_) => { - return Err(ProxyConfigError::Generic( - "proxy address may not be onion service".to_string(), - )) - } - }; - if port == 0 { - return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); - } - - // username may not contain ':' character (per RFC 2617) - if let Some(username) = &username { - if username.contains(':') { - return Err(ProxyConfigError::Generic( - "username may not contain ':' character".to_string(), - )); - } - } - - Ok(Self { - address, - username, - password, - }) - } -} - -#[derive(Clone, Debug)] -pub enum ProxyConfig { - Socks4(Socks4ProxyConfig), - Socks5(Socks5ProxyConfig), - Https(HttpsProxyConfig), -} - -impl From for ProxyConfig { - fn from(config: Socks4ProxyConfig) -> Self { - ProxyConfig::Socks4(config) - } -} - -impl From for ProxyConfig { - fn from(config: Socks5ProxyConfig) -> Self { - ProxyConfig::Socks5(config) - } -} - -impl From for ProxyConfig { - fn from(config: HttpsProxyConfig) -> Self { - ProxyConfig::Https(config) - } -} - // // PluggableTransportConfig // From aff70e5710e3ffc67c041f267a8fed7cc8f96d87 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 22 Jul 2024 02:03:47 +0000 Subject: [PATCH 118/184] tor-interface: drafted in-source documentation of tor_crypto's public API surface --- tor-interface/src/lib.rs | 1 + tor-interface/src/tor_crypto.rs | 115 ++++++++++++++++++++++++++------ 2 files changed, 94 insertions(+), 22 deletions(-) diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index d9eecb0dd..58805026c 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -16,5 +16,6 @@ mod legacy_tor_version; pub mod mock_tor_client; #[cfg(feature = "legacy-tor-provider")] pub mod proxy; +/// Tor-specific cryptographic primitives, operations, and conversion functions. pub mod tor_crypto; pub mod tor_provider; diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 8457f4502..52b3d263c 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -16,12 +16,18 @@ use static_assertions::const_assert_eq; use tor_llcrypto::pk::keymanip::*; use tor_llcrypto::*; +/// Represents various errors that can occur in the tor_crypto module. #[derive(thiserror::Error, Debug)] pub enum Error { + /// A error encountered converting a String to a tor_crypto type #[error("{0}")] ParseError(String), + + /// An error encountered converting between tor_crypto types #[error("{0}")] ConversionError(String), + + /// An error encountered converting from a raw byte representation #[error("invalid key")] KeyInvalid, } @@ -117,35 +123,46 @@ pub(crate) fn generate_password(length: usize) -> String { // Struct deinitions +/// An ed25519 private key. +/// +/// This key type is used with [`crate::tor_provider::TorProvider`] trait for hosting onion-services and can be convertd to an [`Ed25519PublicKey`]. It can also be used to sign messages and create an [`Ed25519Signature`]. pub struct Ed25519PrivateKey { expanded_keypair: pk::ed25519::ExpandedKeypair, } +/// An ed25519 public key. +/// +/// This key type is derived from [`Ed25519PrivateKey`] and can be converted to a [`V3OnionServiceId`]. It can also be used to verify a [`Ed25519Signature`]. #[derive(Clone)] pub struct Ed25519PublicKey { public_key: pk::ed25519::PublicKey, } +/// An ed25519 cryptographic signature #[derive(Clone)] pub struct Ed25519Signature { signature: pk::ed25519::Signature, } +/// An x25519 private key #[derive(Clone)] pub struct X25519PrivateKey { secret_key: pk::curve25519::StaticSecret, } +/// An x25519 public key #[derive(Clone, PartialEq, Eq, Hash)] pub struct X25519PublicKey { public_key: pk::curve25519::PublicKey, } +/// A v3 onion-service id #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct V3OnionServiceId { data: [u8; V3_ONION_SERVICE_ID_STRING_LENGTH], } +/// An enum representing a single bit #[derive(Clone, Copy)] pub enum SignBit { Zero, @@ -192,9 +209,9 @@ enum FromRawValidationMethod { Ed25519Dalek, } -// Ed25519 Private Key - +/// A wrapper around `tor_llcrypto::pk::ed25519::ExpandedKeypair`. impl Ed25519PrivateKey { + /// Securely generate a new `Ed25519PrivateKey`. pub fn generate() -> Ed25519PrivateKey { let csprng = &mut OsRng; let keypair = pk::ed25519::Keypair::generate(csprng); @@ -238,6 +255,9 @@ impl Ed25519PrivateKey { } } + /// Attempt to create an `Ed25519PrivateKey` from an array of bytes. Not all byte buffers of the required size can create a valid `Ed25519PrivateKey`. Only buffers derived from [`Ed25519PrivateKey::to_bytes()`] are required to convert correctly. + /// + /// To securely generate a valid `Ed25519PrivateKey`, use [`Ed25519PrivateKey::generate()`]. pub fn from_raw(raw: &[u8; ED25519_PRIVATE_KEY_SIZE]) -> Result { Self::from_raw_impl(raw, FromRawValidationMethod::Ed25519Dalek) } @@ -291,10 +311,15 @@ impl Ed25519PrivateKey { Self::from_key_blob_impl(key_blob, FromRawValidationMethod::LegacyCTor) } + /// Create an `Ed25519PrivateKey` from a [`String`] in the legacy c-tor daemon key blob format used in the `ADD_ONION` control-port command. From the c-tor control [specification](https://spec.torproject.org/control-spec/commands.html#add_onion): + /// > For a "ED25519-V3" key is the Base64 encoding of the concatenation of the 32-byte ed25519 secret scalar in little-endian and the 32-byte ed25519 PRF secret. + /// + /// Only key blob strings derived from [`Ed25519PrivateKey::to_key_blob()`] are required to convert correctly. pub fn from_key_blob(key_blob: &str) -> Result { Self::from_key_blob_impl(key_blob, FromRawValidationMethod::Ed25519Dalek) } + /// Construct an `Ed25519PrivateKEy` from an [`X25519PrivateKey`]. pub fn from_private_x25519( x25519_private: &X25519PrivateKey, ) -> Result<(Ed25519PrivateKey, SignBit), Error> { @@ -323,6 +348,7 @@ impl Ed25519PrivateKey { } } + /// Write `Ed25519PrivateKey` to a c-tor key blob formatted [`String`]. pub fn to_key_blob(&self) -> String { let mut key_blob = ED25519_PRIVATE_KEY_KEYBLOB_HEADER.to_string(); key_blob.push_str(&BASE64.encode(&self.expanded_keypair.to_secret_key_bytes())); @@ -330,11 +356,15 @@ impl Ed25519PrivateKey { key_blob } + /// Sign the provided message and return an [`Ed25519Signature`]. + /// ## ⚠ Warning ⚠ + ///Only ever sign messages the private key owner controls the contents of! pub fn sign_message(&self, message: &[u8]) -> Ed25519Signature { let signature = self.expanded_keypair.sign(message); Ed25519Signature { signature } } + /// Convert this private key to an array of bytes. pub fn to_bytes(&self) -> [u8; ED25519_PRIVATE_KEY_SIZE] { self.expanded_keypair.to_secret_key_bytes() } @@ -366,22 +396,19 @@ impl std::fmt::Debug for Ed25519PrivateKey { } } -// Ed25519 Public Key - +/// A wrapper around `tor_llcrypto::pk::ed25519::PublicKey` impl Ed25519PublicKey { + /// Construct an `Ed25519PublicKey` from an array of bytes. Not all byte buffers of the required size can create a valid `Ed25519PublicKey`. Only buffers derived from [`Ed25519PublicKey::as_bytes()`] are required to convert correctly. pub fn from_raw(raw: &[u8; ED25519_PUBLIC_KEY_SIZE]) -> Result { Ok(Ed25519PublicKey { public_key: match pk::ed25519::PublicKey::from_bytes(raw) { Ok(public_key) => public_key, - Err(_) => { - return Err(Error::ConversionError( - "failed to create ed25519 public key from bytes".to_string(), - )) - } + Err(_) => return Err(Error::KeyInvalid), }, }) } + /// Construct an `Ed25519PublicKey` from a [`V3OnionServiceId`]. pub fn from_service_id(service_id: &V3OnionServiceId) -> Result { // decode base32 encoded service id let mut decoded_service_id = [0u8; V3_ONION_SERVICE_ID_RAW_SIZE]; @@ -409,6 +436,7 @@ impl Ed25519PublicKey { ) } + /// Construct an `Ed25519PublicKey` from an [`Ed25519PrivateKey`]. pub fn from_private_key(private_key: &Ed25519PrivateKey) -> Ed25519PublicKey { Ed25519PublicKey { public_key: *private_key.expanded_keypair.public(), @@ -428,6 +456,7 @@ impl Ed25519PublicKey { } } + /// View this public key as an array of bytes pub fn as_bytes(&self) -> &[u8; ED25519_PUBLIC_KEY_SIZE] { self.public_key.as_bytes() } @@ -445,15 +474,18 @@ impl std::fmt::Debug for Ed25519PublicKey { } } -// Ed25519 Signature +/// A wrapper around `tor_llcrypto::pk::ed25519::Signature` impl Ed25519Signature { + /// Construct an `Ed25519Signature` from an array of bytes. pub fn from_raw(raw: &[u8; ED25519_SIGNATURE_SIZE]) -> Result { + // todo: message cannot fail so should not return a Result<> Ok(Ed25519Signature { signature: pk::ed25519::Signature::from_bytes(raw), }) } + /// Verify this `Ed25519Signature` for the given message and [`Ed25519PublicKey`]. pub fn verify(&self, message: &[u8], public_key: &Ed25519PublicKey) -> bool { if let Ok(()) = public_key .public_key @@ -464,8 +496,7 @@ impl Ed25519Signature { false } - // derives an ed25519 public key from the provided x25519 public key and signbit, then - // verifies this signature using said ed25519 public key + /// Verify this `Ed25519Signature` for the given message, [`X25519PublicKey`], and [`SignBit`]. This signature must have been created by first converting an [`X25519PrivateKey`] to a [`Ed25519PrivateKey`] and [`SignBit`], and then signing the message using this [`Ed25519PrivateKey`]. This method verifies the signature using the [`Ed25519PublicKey`] derived from the provided [`X25519PublicKey`] and [`SignBit`]. pub fn verify_x25519( &self, message: &[u8], @@ -478,6 +509,7 @@ impl Ed25519Signature { false } + /// Convert this signature to an array of bytes pub fn to_bytes(&self) -> [u8; ED25519_SIGNATURE_SIZE] { self.signature.to_bytes() } @@ -495,9 +527,9 @@ impl std::fmt::Debug for Ed25519Signature { } } -// X25519 Private Key - +/// A wrapper around `tor_llcrypto::pk::curve25519::StaticSecret` impl X25519PrivateKey { + /// Securely generate a new `X25519PrivateKey` pub fn generate() -> X25519PrivateKey { let csprng = &mut OsRng; X25519PrivateKey { @@ -505,6 +537,9 @@ impl X25519PrivateKey { } } + /// Attempt to create an `X25519PrivateKey` from an array of bytes. Not all byte buffers of the required size can create a valid `X25519PrivateKey`. Only buffers derived from [`X25519PrivateKey::to_bytes()`] are required to convert correctly. + /// + /// To securely generate a valid `X25519PrivateKey`, use [`X25519PrivateKey::generate()`]. pub fn from_raw(raw: &[u8; X25519_PRIVATE_KEY_SIZE]) -> Result { // see: https://docs.rs/x25519-dalek/2.0.0-pre.1/src/x25519_dalek/x25519.rs.html#197 if raw[0] == raw[0] & 240 && raw[31] == (raw[31] & 127) | 64 { @@ -516,8 +551,14 @@ impl X25519PrivateKey { } } - // a base64 encoded keyblob + /// Create an `X25519PrivateKey` from a [`String`] in the legacy c-tor daemon key blob format used in the `ONION_CLIENT_AUTH_ADD` control-port command. From the c-tor control [specification](https://spec.torproject.org/control-spec/commands.html#onion_client_auth_add): + /// > ```text + /// > PrivateKeyBlob = base64 encoding of x25519 key + /// > ``` + /// + /// Only key blob strings derived from [`X25519PrivateKey::to_base64()`] are required to convert correctly. pub fn from_base64(base64: &str) -> Result { + // todo: see if this should be from/to key blob like with ed25519 rather than base64 if base64.len() != X25519_PRIVATE_KEY_BASE64_LENGTH { return Err(Error::ParseError(format!( "expects string of length '{}'; received string with length '{}'", @@ -550,19 +591,23 @@ impl X25519PrivateKey { X25519PrivateKey::from_raw(&private_key_data_raw) } - // security note: only ever sign messages the private key owner controls the contents of! - // this function first derives an ed25519 private key from the provided x25519 private key - // and signs the message, returning the signature and signbit needed to calculate the - // ed25519 public key from our x25519 private key's associated x25519 public key + /// Sign the provided message and return an [`Ed25519Signature`] and [`SignBit`]. + /// + /// This method first converts this `X25519PrivateKey` to an [`Ed25519PrivateKey`] and [`SignBit`]. Then, the message is signed using the derived [`Ed25519PrivateKey`]. To verify the signature, both the [`X25519PublicKey`] and this calculated [`SignBit`] are required. + /// + /// ## ⚠ Warning ⚠ + ///Only ever sign messages the private key owner controls the contents of! pub fn sign_message(&self, message: &[u8]) -> Result<(Ed25519Signature, SignBit), Error> { let (ed25519_private, signbit) = Ed25519PrivateKey::from_private_x25519(self)?; Ok((ed25519_private.sign_message(message), signbit)) } + /// Write `X25519PrivateKey` to a base64 encocded [`String`]. pub fn to_base64(&self) -> String { BASE64.encode(&self.secret_key.to_bytes()) } + /// Convert this private key to an array of bytes. pub fn to_bytes(&self) -> [u8; X25519_PRIVATE_KEY_SIZE] { self.secret_key.to_bytes() } @@ -580,20 +625,29 @@ impl std::fmt::Debug for X25519PrivateKey { } } -// X25519 Public Key +/// A wrapper around `tor_llcrypto::pk::curve25519::PublicKey` impl X25519PublicKey { + /// Construct an `X25519PublicKey` from an [`X25519PrivateKey`]. pub fn from_private_key(private_key: &X25519PrivateKey) -> X25519PublicKey { X25519PublicKey { public_key: pk::curve25519::PublicKey::from(&private_key.secret_key), } } + /// Construct an `X25519PublicKey` from an array of bytes. pub fn from_raw(raw: &[u8; X25519_PUBLIC_KEY_SIZE]) -> X25519PublicKey { X25519PublicKey { public_key: pk::curve25519::PublicKey::from(*raw), } } + /// Create an `X25519PublicKey` from a [`String`] in the legacy c-tor daemon key base32 format used in the `ADD_ONION` control-port command. From the c-tor control [specification](https://spec.torproject.org/control-spec/commands.html#add_onion): + /// > ```text + /// > V3Key = The client's base32-encoded x25519 public key, using only the key + /// > part of rend-spec-v3.txt section G.1.2 (v3 only). + /// > ``` + /// + /// Only key base32 strings derived from [`X25519PublicKey::to_base32()`] are required to convert correctly. pub fn from_base32(base32: &str) -> Result { if base32.len() != X25519_PUBLIC_KEY_BASE32_LENGTH { return Err(Error::ParseError(format!( @@ -627,10 +681,12 @@ impl X25519PublicKey { Ok(X25519PublicKey::from_raw(&public_key_data_raw)) } + /// Write `X25519PublicKey` to a base32 encocded [`String`]. pub fn to_base32(&self) -> String { BASE32_NOPAD.encode(self.public_key.as_bytes()) } + /// View this public key as an array of bytes pub fn as_bytes(&self) -> &[u8; X25519_PUBLIC_KEY_SIZE] { self.public_key.as_bytes() } @@ -642,8 +698,7 @@ impl std::fmt::Debug for X25519PublicKey { } } -// Onion Service Id - +/// Strongly-typed representation of a v3 onion-service id impl V3OnionServiceId { // see https://github.com/torproject/torspec/blob/main/rend-spec-v3.txt#L2143 fn calc_truncated_checksum( @@ -660,6 +715,18 @@ impl V3OnionServiceId { [hash_bytes[0], hash_bytes[1]] } + /// Create a `V3OnionServiceId` from a [`String`] in the version 3 onion service digest format. From the tor address [specification](https://spec.torproject.org/address-spec.html#onion): + /// > ```text + /// > onion_address = base32(PUBKEY | CHECKSUM | VERSION) + /// > CHECKSUM = H(".onion checksum" | PUBKEY | VERSION)[:2] + /// > + /// > where: + /// > - PUBKEY is the 32 bytes ed25519 master pubkey of the onion service. + /// > - VERSION is a one byte version field (default value '\x03') + /// > - ".onion checksum" is a constant string + /// > - H is SHA3-256 + /// > - CHECKSUM is truncated to two bytes before inserting it in onion_address + /// > ``` pub fn from_string(service_id: &str) -> Result { if !V3OnionServiceId::is_valid(service_id) { return Err(Error::ParseError(format!( @@ -672,6 +739,7 @@ impl V3OnionServiceId { }) } + /// Create a `V3OnionServiceId` from an [`Ed25519PublicKey`]. pub fn from_public_key(public_key: &Ed25519PublicKey) -> V3OnionServiceId { let mut raw_service_id = [0u8; V3_ONION_SERVICE_ID_RAW_SIZE]; @@ -688,10 +756,12 @@ impl V3OnionServiceId { V3OnionServiceId { data: service_id } } + /// Create a `V3OnionServiceId` from an [`Ed25519PrivateKey`]. pub fn from_private_key(private_key: &Ed25519PrivateKey) -> V3OnionServiceId { Self::from_public_key(&Ed25519PublicKey::from_private_key(private_key)) } + /// Determine if the provided string is a valid representation of a `V3OnionServiceId` pub fn is_valid(service_id: &str) -> bool { if service_id.len() != V3_ONION_SERVICE_ID_STRING_LENGTH { return false; @@ -725,6 +795,7 @@ impl V3OnionServiceId { } } + /// View this service id as an array of bytes pub fn as_bytes(&self) -> &[u8; V3_ONION_SERVICE_ID_STRING_LENGTH] { &self.data } From b0c47fbbf8e534d37d351463e0051104083804a9 Mon Sep 17 00:00:00 2001 From: Richard Pospesel Date: Mon, 22 Jul 2024 04:57:07 +0000 Subject: [PATCH 119/184] tor-interface: draft in-source documentation of proxy's public API surface --- tor-interface/src/lib.rs | 1 + tor-interface/src/proxy.rs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index 58805026c..9e37ef30b 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -15,6 +15,7 @@ mod legacy_tor_version; #[cfg(feature = "mock-tor-provider")] pub mod mock_tor_client; #[cfg(feature = "legacy-tor-provider")] +/// Proxy settings pub mod proxy; /// Tor-specific cryptographic primitives, operations, and conversion functions. pub mod tor_crypto; diff --git a/tor-interface/src/proxy.rs b/tor-interface/src/proxy.rs index b4d6157ca..b0f84bf6b 100644 --- a/tor-interface/src/proxy.rs +++ b/tor-interface/src/proxy.rs @@ -2,17 +2,21 @@ use crate::tor_provider::TargetAddr; #[derive(thiserror::Error, Debug)] +/// Error type for the proxy module pub enum ProxyConfigError { #[error("{0}")] + /// An error returned when constructing a proxy configuration with invalid parameters Generic(String), } #[derive(Clone, Debug)] +/// Configuration for a SOCKS4 proxy pub struct Socks4ProxyConfig { pub(crate) address: TargetAddr, } impl Socks4ProxyConfig { + /// Construct a new `Socks4ProxyConfig`. The `address` argument must not be a [`crate::tor_provider::TargetAddr::OnionService`] and its port must not be 0. pub fn new(address: TargetAddr) -> Result { let port = match &address { TargetAddr::Socket(addr) => addr.port(), @@ -32,6 +36,7 @@ impl Socks4ProxyConfig { } #[derive(Clone, Debug)] +/// Configuration for a SOCKS5 proxy pub struct Socks5ProxyConfig { pub(crate) address: TargetAddr, pub(crate) username: Option, @@ -39,6 +44,7 @@ pub struct Socks5ProxyConfig { } impl Socks5ProxyConfig { + /// Construct a new `Socks5ProxyConfig`. The `address` argument must not be a [`crate::tor_provider::TargetAddr::OnionService`] and its port must not be 0. The `username` and `password` arguments, if present, must each be less than 256 bytes long. pub fn new( address: TargetAddr, username: Option, @@ -83,6 +89,7 @@ impl Socks5ProxyConfig { } #[derive(Clone, Debug)] +/// Configuration for an HTTP CONNECT proxy (`HTTPSProxy` in c-tor torrc configuration) pub struct HttpsProxyConfig { pub(crate) address: TargetAddr, pub(crate) username: Option, @@ -90,6 +97,7 @@ pub struct HttpsProxyConfig { } impl HttpsProxyConfig { + /// Construct a new `HttpsProxyConfig`. The `address` argument must not be a [`crate::tor_provider::TargetAddr::OnionService`] and its port must not be 0. The `username` argument, if present, must not contain the `:` (colon) character. pub fn new( address: TargetAddr, username: Option, @@ -126,9 +134,13 @@ impl HttpsProxyConfig { } #[derive(Clone, Debug)] +/// An enum representing a possible proxy server configuration with address and possible credentials. pub enum ProxyConfig { + /// A SOCKS4 proxy Socks4(Socks4ProxyConfig), + /// A SOCKS5 proxy Socks5(Socks5ProxyConfig), + /// An HTTP CONNECT proxy Https(HttpsProxyConfig), } @@ -148,4 +160,4 @@ impl From for ProxyConfig { fn from(config: HttpsProxyConfig) -> Self { ProxyConfig::Https(config) } -} \ No newline at end of file +} From 9b435ecb5d000be286ca1f8a2edd3c5dbc93f7bb Mon Sep 17 00:00:00 2001 From: Morgan Date: Tue, 23 Jul 2024 01:20:53 +0000 Subject: [PATCH 120/184] tor-interface: migrate pluggable-transport and bridge line configuration implementation to separate censorship_circumvention module --- tor-interface/CMakeLists.txt | 1 + tor-interface/src/censorship_circumvention.rs | 247 +++++++++++++++++ tor-interface/src/legacy_tor_client.rs | 3 +- tor-interface/src/lib.rs | 3 + tor-interface/src/tor_provider.rs | 248 ------------------ tor-interface/tests/tor_provider.rs | 2 + 6 files changed, 255 insertions(+), 249 deletions(-) create mode 100644 tor-interface/src/censorship_circumvention.rs diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 4f31d92be..74debbeb0 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -1,6 +1,7 @@ set(tor_interface_sources Cargo.toml src/arti_client_tor_client.rs + src/censorship_circumvention.rs src/legacy_tor_client.rs src/legacy_tor_controller.rs src/legacy_tor_control_stream.rs diff --git a/tor-interface/src/censorship_circumvention.rs b/tor-interface/src/censorship_circumvention.rs new file mode 100644 index 000000000..f34050dbe --- /dev/null +++ b/tor-interface/src/censorship_circumvention.rs @@ -0,0 +1,247 @@ +// standard +use std::net::SocketAddr; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::OnceLock; + +// extern crates +use regex::Regex; + +#[derive(Clone, Debug)] +pub struct PluggableTransportConfig { + transports: Vec, + path_to_binary: PathBuf, + options: Vec, +} + +#[derive(thiserror::Error, Debug)] +pub enum PluggableTransportConfigError { + #[error("pluggable transport name '{0}' is invalid")] + TransportNameInvalid(String), + #[error("unable to use '{0}' as pluggable transport binary path, {1}")] + BinaryPathInvalid(String, String), +} + +// per the PT spec: https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/main/releases/PTSpecV1.0/pt-1_0.txt +static TRANSPORT_PATTERN: OnceLock = OnceLock::new(); +fn init_transport_pattern() -> Regex { + Regex::new(r"(?m)^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap() +} + +impl PluggableTransportConfig { + pub fn new( + transports: Vec, + path_to_binary: PathBuf, + ) -> Result { + let transport_pattern = TRANSPORT_PATTERN.get_or_init(init_transport_pattern); + // validate each transport + for transport in &transports { + if !transport_pattern.is_match(&transport) { + return Err(PluggableTransportConfigError::TransportNameInvalid( + transport.clone(), + )); + } + } + + // pluggable transport path must be absolute so we can fix it up for individual + // TorProvider implementations + if !path_to_binary.is_absolute() { + return Err(PluggableTransportConfigError::BinaryPathInvalid( + format!("{:?}", path_to_binary.display()), + "must be an absolute path".to_string(), + )); + } + + Ok(Self { + transports, + path_to_binary, + options: Default::default(), + }) + } + + pub fn transports(&self) -> &Vec { + &self.transports + } + + pub fn path_to_binary(&self) -> &PathBuf { + &self.path_to_binary + } + + pub fn options(&self) -> &Vec { + &self.options + } + + pub fn add_option(&mut self, arg: String) { + self.options.push(arg); + } +} + +#[derive(Clone, Debug)] +pub struct BridgeLine { + transport: String, + address: SocketAddr, + fingerprint: String, + keyvalues: Vec<(String, String)>, +} + +#[derive(thiserror::Error, Debug)] +pub enum BridgeLineError { + #[error("bridge line '{0}' missing transport")] + TransportMissing(String), + + #[error("bridge line '{0}' missing address")] + AddressMissing(String), + + #[error("bridge line '{0}' missing fingerprint")] + FingerprintMissing(String), + + #[error("transport name '{0}' is invalid")] + TransportNameInvalid(String), + + #[error("address '{0}' cannot be parsed as IP:PORT")] + AddressParseFailed(String), + + #[error("key=value '{0}' is invalid")] + KeyValueInvalid(String), + + #[error("bridge address port must not be 0")] + AddressPortInvalid, + + #[error("fingerprint '{0}' is invalid")] + FingerprintInvalid(String), +} + +impl BridgeLine { + pub fn new( + transport: String, + address: SocketAddr, + fingerprint: String, + keyvalues: Vec<(String, String)>, + ) -> Result { + let transport_pattern = TRANSPORT_PATTERN.get_or_init(init_transport_pattern); + + // transports have a particular pattern + if !transport_pattern.is_match(&transport) { + return Err(BridgeLineError::TransportNameInvalid(transport)); + } + + // port can't be 0 + if address.port() == 0 { + return Err(BridgeLineError::AddressPortInvalid); + } + + static BRIDGE_FINGERPRINT_PATTERN: OnceLock = OnceLock::new(); + let bridge_fingerprint_pattern = BRIDGE_FINGERPRINT_PATTERN + .get_or_init(|| Regex::new(r"(?m)^[0-9a-fA-F]{40}$").unwrap()); + + // fingerprint should be a sha1 hash + if !bridge_fingerprint_pattern.is_match(&fingerprint) { + return Err(BridgeLineError::FingerprintInvalid(fingerprint)); + } + + // validate key-values + for (key, value) in &keyvalues { + if key.contains(' ') || key.contains('=') || key.len() == 0 { + return Err(BridgeLineError::KeyValueInvalid(format!("{key}={value}"))); + } + } + + Ok(Self { + transport, + address, + fingerprint, + keyvalues, + }) + } + + pub fn transport(&self) -> &String { + &self.transport + } + + pub fn address(&self) -> &SocketAddr { + &self.address + } + + pub fn fingerprint(&self) -> &String { + &self.fingerprint + } + + pub fn keyvalues(&self) -> &Vec<(String, String)> { + &self.keyvalues + } + + #[cfg(feature = "legacy-tor-provider")] + pub fn as_legacy_tor_setconf_value(&self) -> String { + let transport = &self.transport; + let address = self.address.to_string(); + let fingerprint = self.fingerprint.to_string(); + let keyvalues: Vec = self + .keyvalues + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect(); + let keyvalues = keyvalues.join(" "); + + format!("{transport} {address} {fingerprint} {keyvalues}") + } +} + +impl FromStr for BridgeLine { + type Err = BridgeLineError; + fn from_str(s: &str) -> Result { + let mut tokens = s.split(' '); + // get transport name + let transport = if let Some(transport) = tokens.next() { + transport + } else { + return Err(BridgeLineError::TransportMissing(s.to_string())); + }; + // get bridge address + let address = if let Some(address) = tokens.next() { + if let Ok(address) = SocketAddr::from_str(address) { + address + } else { + return Err(BridgeLineError::AddressParseFailed(address.to_string())); + } + } else { + return Err(BridgeLineError::AddressMissing(s.to_string())); + }; + // get the bridge fingerprint + let fingerprint = if let Some(fingerprint) = tokens.next() { + fingerprint + } else { + return Err(BridgeLineError::FingerprintMissing(s.to_string())); + }; + + // get the bridge options + static BRIDGE_OPTION_PATTERN: OnceLock = OnceLock::new(); + let bridge_option_pattern = BRIDGE_OPTION_PATTERN + .get_or_init(|| Regex::new(r"(?m)^(?[^=]+)=(?.*)$").unwrap()); + + let mut keyvalues: Vec<(String, String)> = Default::default(); + while let Some(keyvalue) = tokens.next() { + if let Some(caps) = bridge_option_pattern.captures(&keyvalue) { + let key = caps + .name("key") + .expect("missing key group") + .as_str() + .to_string(); + let value = caps + .name("value") + .expect("missing value group") + .as_str() + .to_string(); + keyvalues.push((key, value)); + } else { + return Err(BridgeLineError::KeyValueInvalid(keyvalue.to_string())); + } + } + + BridgeLine::new( + transport.to_string(), + address, + fingerprint.to_string(), + keyvalues, + ) + } +} \ No newline at end of file diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index a81aa9750..d4384bf7b 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -13,6 +13,7 @@ use std::time::Duration; use socks::Socks5Stream; // internal crates +use crate::censorship_circumvention::*; use crate::legacy_tor_control_stream::*; use crate::legacy_tor_controller::*; use crate::legacy_tor_process::*; @@ -103,7 +104,7 @@ pub enum Error { PluggableTransportBinaryNameNotUtf8Representnable(std::ffi::OsString), #[error("{0}")] - PluggableTransportConfigError(#[source] crate::tor_provider::PluggableTransportConfigError), + PluggableTransportConfigError(#[source] crate::censorship_circumvention::PluggableTransportConfigError), #[error("pluggable transport multiply defines '{0}' bridge transport type")] BridgeTransportTypeMultiplyDefined(String), diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index 9e37ef30b..6b63e70c0 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -3,6 +3,9 @@ #[cfg(feature = "arti-client-tor-provider")] pub mod arti_client_tor_client; #[cfg(feature = "legacy-tor-provider")] +/// Censorship circumvention configuration for pluggable-transports and bridge settings +pub mod censorship_circumvention; +#[cfg(feature = "legacy-tor-provider")] pub mod legacy_tor_client; #[cfg(feature = "legacy-tor-provider")] mod legacy_tor_control_stream; diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 1d0972e97..c130231f6 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -5,7 +5,6 @@ use std::convert::TryFrom; use std::io::{Read, Write}; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::ops::{Deref, DerefMut}; -use std::path::PathBuf; use std::str::FromStr; use std::sync::OnceLock; @@ -374,253 +373,6 @@ impl Drop for OnionListener { } } -// -// PluggableTransportConfig -// - -#[derive(Clone, Debug)] -pub struct PluggableTransportConfig { - transports: Vec, - path_to_binary: PathBuf, - options: Vec, -} - -#[derive(thiserror::Error, Debug)] -pub enum PluggableTransportConfigError { - #[error("pluggable transport name '{0}' is invalid")] - TransportNameInvalid(String), - #[error("unable to use '{0}' as pluggable transport binary path, {1}")] - BinaryPathInvalid(String, String), -} - -// per the PT spec: https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/main/releases/PTSpecV1.0/pt-1_0.txt -static TRANSPORT_PATTERN: OnceLock = OnceLock::new(); -fn init_transport_pattern() -> Regex { - Regex::new(r"(?m)^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap() -} - -impl PluggableTransportConfig { - pub fn new( - transports: Vec, - path_to_binary: PathBuf, - ) -> Result { - let transport_pattern = TRANSPORT_PATTERN.get_or_init(init_transport_pattern); - // validate each transport - for transport in &transports { - if !transport_pattern.is_match(&transport) { - return Err(PluggableTransportConfigError::TransportNameInvalid( - transport.clone(), - )); - } - } - - // pluggable transport path must be absolute so we can fix it up for individual - // TorProvider implementations - if !path_to_binary.is_absolute() { - return Err(PluggableTransportConfigError::BinaryPathInvalid( - format!("{:?}", path_to_binary.display()), - "must be an absolute path".to_string(), - )); - } - - Ok(Self { - transports, - path_to_binary, - options: Default::default(), - }) - } - - pub fn transports(&self) -> &Vec { - &self.transports - } - - pub fn path_to_binary(&self) -> &PathBuf { - &self.path_to_binary - } - - pub fn options(&self) -> &Vec { - &self.options - } - - pub fn add_option(&mut self, arg: String) { - self.options.push(arg); - } -} - -// -// BridgeSettings -// - -#[derive(Clone, Debug)] -pub struct BridgeLine { - transport: String, - address: SocketAddr, - fingerprint: String, - keyvalues: Vec<(String, String)>, -} - -#[derive(thiserror::Error, Debug)] -pub enum BridgeLineError { - #[error("bridge line '{0}' missing transport")] - TransportMissing(String), - - #[error("bridge line '{0}' missing address")] - AddressMissing(String), - - #[error("bridge line '{0}' missing fingerprint")] - FingerprintMissing(String), - - #[error("transport name '{0}' is invalid")] - TransportNameInvalid(String), - - #[error("address '{0}' cannot be parsed as IP:PORT")] - AddressParseFailed(String), - - #[error("key=value '{0}' is invalid")] - KeyValueInvalid(String), - - #[error("bridge address port must not be 0")] - AddressPortInvalid, - - #[error("fingerprint '{0}' is invalid")] - FingerprintInvalid(String), -} - -impl BridgeLine { - pub fn new( - transport: String, - address: SocketAddr, - fingerprint: String, - keyvalues: Vec<(String, String)>, - ) -> Result { - let transport_pattern = TRANSPORT_PATTERN.get_or_init(init_transport_pattern); - - // transports have a particular pattern - if !transport_pattern.is_match(&transport) { - return Err(BridgeLineError::TransportNameInvalid(transport)); - } - - // port can't be 0 - if address.port() == 0 { - return Err(BridgeLineError::AddressPortInvalid); - } - - static BRIDGE_FINGERPRINT_PATTERN: OnceLock = OnceLock::new(); - let bridge_fingerprint_pattern = BRIDGE_FINGERPRINT_PATTERN - .get_or_init(|| Regex::new(r"(?m)^[0-9a-fA-F]{40}$").unwrap()); - - // fingerprint should be a sha1 hash - if !bridge_fingerprint_pattern.is_match(&fingerprint) { - return Err(BridgeLineError::FingerprintInvalid(fingerprint)); - } - - // validate key-values - for (key, value) in &keyvalues { - if key.contains(' ') || key.contains('=') || key.len() == 0 { - return Err(BridgeLineError::KeyValueInvalid(format!("{key}={value}"))); - } - } - - Ok(Self { - transport, - address, - fingerprint, - keyvalues, - }) - } - - pub fn transport(&self) -> &String { - &self.transport - } - - pub fn address(&self) -> &SocketAddr { - &self.address - } - - pub fn fingerprint(&self) -> &String { - &self.fingerprint - } - - pub fn keyvalues(&self) -> &Vec<(String, String)> { - &self.keyvalues - } - - #[cfg(feature = "legacy-tor-provider")] - pub fn as_legacy_tor_setconf_value(&self) -> String { - let transport = &self.transport; - let address = self.address.to_string(); - let fingerprint = self.fingerprint.to_string(); - let keyvalues: Vec = self - .keyvalues - .iter() - .map(|(key, value)| format!("{key}={value}")) - .collect(); - let keyvalues = keyvalues.join(" "); - - format!("{transport} {address} {fingerprint} {keyvalues}") - } -} - -impl FromStr for BridgeLine { - type Err = BridgeLineError; - fn from_str(s: &str) -> Result { - let mut tokens = s.split(' '); - // get transport name - let transport = if let Some(transport) = tokens.next() { - transport - } else { - return Err(BridgeLineError::TransportMissing(s.to_string())); - }; - // get bridge address - let address = if let Some(address) = tokens.next() { - if let Ok(address) = SocketAddr::from_str(address) { - address - } else { - return Err(BridgeLineError::AddressParseFailed(address.to_string())); - } - } else { - return Err(BridgeLineError::AddressMissing(s.to_string())); - }; - // get the bridge fingerprint - let fingerprint = if let Some(fingerprint) = tokens.next() { - fingerprint - } else { - return Err(BridgeLineError::FingerprintMissing(s.to_string())); - }; - - // get the bridge options - static BRIDGE_OPTION_PATTERN: OnceLock = OnceLock::new(); - let bridge_option_pattern = BRIDGE_OPTION_PATTERN - .get_or_init(|| Regex::new(r"(?m)^(?[^=]+)=(?.*)$").unwrap()); - - let mut keyvalues: Vec<(String, String)> = Default::default(); - while let Some(keyvalue) = tokens.next() { - if let Some(caps) = bridge_option_pattern.captures(&keyvalue) { - let key = caps - .name("key") - .expect("missing key group") - .as_str() - .to_string(); - let value = caps - .name("value") - .expect("missing value group") - .as_str() - .to_string(); - keyvalues.push((key, value)); - } else { - return Err(BridgeLineError::KeyValueInvalid(keyvalue.to_string())); - } - } - - BridgeLine::new( - transport.to_string(), - address, - fingerprint.to_string(), - keyvalues, - ) - } -} - pub trait TorProvider: Send { fn update(&mut self) -> Result, Error>; diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index dca26e375..e3b8a10a6 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -19,6 +19,8 @@ use tokio::runtime; #[cfg(feature = "arti-client-tor-provider")] use tor_interface::arti_client_tor_client::*; #[cfg(feature = "legacy-tor-provider")] +use tor_interface::censorship_circumvention::*; +#[cfg(feature = "legacy-tor-provider")] use tor_interface::legacy_tor_client::*; #[cfg(feature = "mock-tor-provider")] use tor_interface::mock_tor_client::*; From f6b200866d10feba8a5c165799f8514957d7ce89 Mon Sep 17 00:00:00 2001 From: Morgan Date: Tue, 23 Jul 2024 02:58:31 +0000 Subject: [PATCH 121/184] tor-interface: drafted in-source documentation of censorship_circumvention's public API surface --- tor-interface/src/censorship_circumvention.rs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tor-interface/src/censorship_circumvention.rs b/tor-interface/src/censorship_circumvention.rs index f34050dbe..f1f1eb915 100644 --- a/tor-interface/src/censorship_circumvention.rs +++ b/tor-interface/src/censorship_circumvention.rs @@ -8,6 +8,7 @@ use std::sync::OnceLock; use regex::Regex; #[derive(Clone, Debug)] +/// Configuration for a pluggable-transport pub struct PluggableTransportConfig { transports: Vec, path_to_binary: PathBuf, @@ -15,10 +16,13 @@ pub struct PluggableTransportConfig { } #[derive(thiserror::Error, Debug)] +/// Error returned on failure to construct a [`PluggableTransportConfig`] pub enum PluggableTransportConfigError { #[error("pluggable transport name '{0}' is invalid")] + /// transport names must be a valid C identifier TransportNameInvalid(String), #[error("unable to use '{0}' as pluggable transport binary path, {1}")] + /// configuration only allows aboslute paths to binaries BinaryPathInvalid(String, String), } @@ -28,7 +32,9 @@ fn init_transport_pattern() -> Regex { Regex::new(r"(?m)^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap() } +/// 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) impl PluggableTransportConfig { + /// 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. pub fn new( transports: Vec, path_to_binary: PathBuf, @@ -59,23 +65,28 @@ impl PluggableTransportConfig { }) } + /// Get a reference to this `PluggableTransportConfig`'s list of transports. pub fn transports(&self) -> &Vec { &self.transports } + /// Get a reference to this `PluggableTransportConfig`'s `PathBuf` containing the absolute path to the pluggable-transport binary. pub fn path_to_binary(&self) -> &PathBuf { &self.path_to_binary } + /// Get a reference to this `PluggableTransportConfig`'s list of command-line options pub fn options(&self) -> &Vec { &self.options } + /// Add a command-line option used to invoke this pluggable-transport. pub fn add_option(&mut self, arg: String) { self.options.push(arg); } } +/// Configuration for a bridge line to be used with a pluggable-transport #[derive(Clone, Debug)] pub struct BridgeLine { transport: String, @@ -85,33 +96,47 @@ pub struct BridgeLine { } #[derive(thiserror::Error, Debug)] +/// Error returned on failure to construct a [`BridgeLine`] pub enum BridgeLineError { #[error("bridge line '{0}' missing transport")] + /// Provided bridge line missing transport TransportMissing(String), #[error("bridge line '{0}' missing address")] + /// Provided bridge line missing address AddressMissing(String), #[error("bridge line '{0}' missing fingerprint")] + /// Provided bridge line missing fingerprint FingerprintMissing(String), #[error("transport name '{0}' is invalid")] + /// Invalid transport name (must be a valid C identifier) TransportNameInvalid(String), #[error("address '{0}' cannot be parsed as IP:PORT")] + /// Provided bridge line's address not parseable AddressParseFailed(String), #[error("key=value '{0}' is invalid")] + /// A key/value pair in invalid format KeyValueInvalid(String), #[error("bridge address port must not be 0")] + /// Invalid bridge address port AddressPortInvalid, #[error("fingerprint '{0}' is invalid")] + /// Fingerprint is not parseable (must be length 40 base16 string) FingerprintInvalid(String), } +/// 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: +/// - [https://tb-manual.torproject.org/bridges/](https://tb-manual.torproject.org/bridges/) impl BridgeLine { + /// Construct a new `BridgeLine` from its constiuent 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. + /// + /// In practice, bridge lines are distributed as entire strings so most consumers of these APIs are not likely to need this particular function. pub fn new( transport: String, address: SocketAddr, @@ -154,23 +179,28 @@ impl BridgeLine { }) } + /// Get a reference to this `BridgeLine`'s transport field. pub fn transport(&self) -> &String { &self.transport } + /// Get a reference to this `BridgeLine`'s address field. pub fn address(&self) -> &SocketAddr { &self.address } + /// Get a reference to this `BridgeLine`'s fingerprint field. pub fn fingerprint(&self) -> &String { &self.fingerprint } + /// Get a reference to this `BridgeLine`'s key/values field. pub fn keyvalues(&self) -> &Vec<(String, String)> { &self.keyvalues } #[cfg(feature = "legacy-tor-provider")] + /// Serialise this `BridgeLine` to the value set via `SETCONF Bridge...` legacy c-tor control-port command. pub fn as_legacy_tor_setconf_value(&self) -> String { let transport = &self.transport; let address = self.address.to_string(); From 5bd401d12beb4a7558ba9afec4e65a311c1196e3 Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 26 Jul 2024 22:22:26 +0000 Subject: [PATCH 122/184] tor-interface: drafted in-source documentation of tor_provider's public API surface --- tor-interface/src/lib.rs | 1 + tor-interface/src/tor_provider.rs | 63 +++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index 6b63e70c0..3c4869107 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -22,4 +22,5 @@ pub mod mock_tor_client; pub mod proxy; /// Tor-specific cryptographic primitives, operations, and conversion functions. pub mod tor_crypto; +/// Traits and types for connecting to the Tor Network. pub mod tor_provider; diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index c130231f6..7b2bd3629 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -34,6 +34,9 @@ pub enum Error { // OnionAddr // +/// A version 3 onion service address. +/// +/// Version 3 Onion Service addresses const of a [`crate::tor_crypto::V3OnionServiceId`] and a 16-bit port number. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct OnionAddrV3 { pub(crate) service_id: V3OnionServiceId, @@ -41,6 +44,7 @@ pub struct OnionAddrV3 { } impl OnionAddrV3 { + /// Create a new `OnionAddrV3` from a [`crate::tor_crypto::V3OnionServiceId`] and port number. pub fn new(service_id: V3OnionServiceId, virt_port: u16) -> OnionAddrV3 { OnionAddrV3 { service_id, @@ -48,10 +52,12 @@ impl OnionAddrV3 { } } + /// Return the service id associated with this onion address. pub fn service_id(&self) -> &V3OnionServiceId { &self.service_id } + /// Return the port numebr associated with this onion address. pub fn virt_port(&self) -> u16 { self.virt_port } @@ -63,6 +69,7 @@ impl std::fmt::Display for OnionAddrV3 { } } +/// An onion service address analog to [`std::net::SocketAddr`] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum OnionAddr { V3(OnionAddrV3), @@ -107,17 +114,23 @@ impl std::fmt::Display for OnionAddr { // DomainAddr // +/// A domain name analog to `std::net::SocketAddr` +/// +/// A `DomainAddr` must not end in ".onion" #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DomainAddr { domain: String, port: u16, } +/// A `DomainAddr` has a domain name (scuh as `www.example.com`) and a port impl DomainAddr { + /// Returns the domain name associated with this domain address. pub fn domain(&self) -> &str { self.domain.as_ref() } + /// Returns the port number associated with this domain address. pub fn port(&self) -> u16 { self.port } @@ -185,10 +198,14 @@ impl FromStr for DomainAddr { // TargetAddr // +/// An enum representing the various types of addresses a [`TorProvider`] implementation may connect to. #[derive(Clone, Debug)] pub enum TargetAddr { + /// An ip address and port Socket(std::net::SocketAddr), + /// An onion-service id and virtual port OnionService(OnionAddr), + /// A domain name and port Domain(DomainAddr), } @@ -225,28 +242,42 @@ impl std::fmt::Display for TargetAddr { } } +/// Various events possibly returned by a [`TorProvider`] implementation's `update()` method. #[derive(Debug)] pub enum TorEvent { + /// A status update received connecting to the Tor Network. BootstrapStatus { + /// A number from 0 to 100 for how through the bootstrap process the `TorProvider` is. progress: u32, + /// A short string to identify the current phase of the bootstrap process. tag: String, + /// A longer string with a summary of the current phase of the bootstrap process. summary: String, }, + /// Indicates successful connection to the Tor Network. The [`TorProvider::connect()`] and [`TorProvider::listener()`] methods may now be used. BootstrapComplete, + /// Messages which may be useful for troubleshooting. LogReceived { + /// A message line: String, }, + /// An onion-service has been published to the Tor Network and may now be reachable by clients. OnionServicePublished { + /// The service-id of the onion-service which has been published. service_id: V3OnionServiceId, }, } +/// A `CircuitToken` is used to specify circuits used to connect to clearnet services. pub type CircuitToken = usize; // -// OnionStream Implementation +// Onion Stream // +/// A wrapper around a [`std::net::TcpStream`] with some Tor-specific customisations +/// +/// An onion-listener can be constructed using the [`TorProvider::connect()`] method. pub struct OnionStream { pub(crate) stream: TcpStream, pub(crate) local_addr: Option, @@ -289,14 +320,17 @@ impl Write for OnionStream { } impl OnionStream { + /// Returns the target address of the remote peer of this onion connection. pub fn peer_addr(&self) -> Option { self.peer_addr.clone() } + /// Returns the onion address of the local connection for an incoming onion-service connection. Returns `None` for outgoing connections. pub fn local_addr(&self) -> Option { self.local_addr.clone() } + /// Tries to clone the underlying connection and data. A simple pass-through to [`std::net::TcpStream::try_clone()`]. pub fn try_clone(&self) -> Result { Ok(Self { stream: self.stream.try_clone()?, @@ -310,7 +344,9 @@ impl OnionStream { // Onion Listener // - +/// A wrapper around a [`std::net::TcpListener`] with some Tor-specific customisations. +/// +/// An onion-listener can be constructed using the [`TorProvider::listener()`] method. pub struct OnionListener { pub(crate) listener: TcpListener, pub(crate) onion_addr: OnionAddr, @@ -319,6 +355,7 @@ pub struct OnionListener { } impl OnionListener { + /// Construct an `OnionListener`. The `data` and `drop` parameters are to allow custom `TorProvider` implementations their own data and cleanup procedures. pub(crate) fn new( listener: TcpListener, onion_addr: OnionAddr, @@ -343,10 +380,12 @@ impl OnionListener { } } + /// Moves the underlying `TcpListener` into or out of nonblocking mode. pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { self.listener.set_nonblocking(nonblocking) } + /// Accept a new incoming connection from this listener. pub fn accept(&self) -> Result, std::io::Error> { match self.listener.accept() { Ok((stream, _socket_addr)) => Ok(Some(OnionStream { @@ -373,27 +412,43 @@ impl Drop for OnionListener { } } - +/// The `TorProvider` trait allows for high-level Tor Network functionality. Implementations ay connect to the Tor Network, anonymously connect to both clearnet and onion-service endpoints, and host onion-services. pub trait TorProvider: Send { + /// Process and return `TorEvent`s handled by this `TorProvider`. fn update(&mut self) -> Result, Error>; + /// Begin connecting to the Tor Network. fn bootstrap(&mut self) -> Result<(), Error>; + /// Add v3 onion-service authorisation credentials, allowing this `TorProvider` to connect to an onion-service whose service-descriptor is encrypted using the assocciated x25519 public key. fn add_client_auth( &mut self, service_id: &V3OnionServiceId, client_auth: &X25519PrivateKey, ) -> Result<(), Error>; + /// Remove a previously added client authorisation credential. This `TorProvider` will be unable to connect to the onion-service associated with the removed credentail. fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error>; + /// Anonymously connect to the address specified by `target` over the Tor Network and return the associated [`OnionStream`]. + /// + /// When conecting to clearnet targets, an optional [`CircuitToken`] may be used to enforce usage of different circuits through the Tor Network. If `circuit` is `None`, the default circuit is used. + /// + ///Connections made with different `CircuitToken`s are required to use different circuits through the Tor Network. However, connections made with identical `CircuitToken`s are *not* required to use identical circuits through the Tor Network. + /// + /// Specifying a circuit token when connecting to an onion-service has no effect on the resulting circuit. fn connect( &mut self, target: TargetAddr, circuit: Option, ) -> Result; + /// Anonymously start an onion-service and return the associated [`OnionListener`]. + /// + ///The resulting onion-service will not be reachable by clients until [`TorProvider::update()`] returns a [`TorEvent::OnionServicePublished`] event. The optional `authorised_clients` parameter may be used to require client authorisation keys to connect to resulting onion-service. For further information, see the Tor Project's onion-services [client-auth documentation](https://community.torproject.org/onion-services/advanced/client-auth). fn listener( &mut self, private_key: &Ed25519PrivateKey, virt_port: u16, - authorized_clients: Option<&[X25519PublicKey]>, + authorised_clients: Option<&[X25519PublicKey]>, ) -> Result; + /// Create a new [`CircuitToken`]. fn generate_token(&mut self) -> CircuitToken; + /// Releaes a previously generated [`CircuitToken`]. fn release_token(&mut self, token: CircuitToken); } From 14b046e9744889bfea860bbef69cd1a78c6aeb39 Mon Sep 17 00:00:00 2001 From: Morgan Date: Sun, 28 Jul 2024 00:50:54 +0000 Subject: [PATCH 123/184] tor-interface: drafted in-source documentation of mock_tor_client, legacy_tor_client, and arti_client_tor_client's public API surface --- tor-interface/src/arti_client_tor_client.rs | 5 +++++ tor-interface/src/legacy_tor_client.rs | 8 ++++++++ tor-interface/src/lib.rs | 3 +++ tor-interface/src/mock_tor_client.rs | 8 ++++++++ 4 files changed, 24 insertions(+) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index ce177aa80..4ae18f61e 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -28,6 +28,7 @@ use crate::tor_crypto::*; use crate::tor_provider; use crate::tor_provider::*; +/// [`ArtiClientTorClient`]-specific error type #[derive(thiserror::Error, Debug)] pub enum Error { #[error("not implemented")] @@ -84,6 +85,9 @@ impl From for crate::tor_provider::Error { } } +/// The `ArtiClientTorClient` is an in-process [`arti-client`](https://crates.io/crates/arti-client)-based [`TorProvider`]. +/// +/// pub struct ArtiClientTorClient { tokio_runtime: Arc, arti_client: TorClient, @@ -146,6 +150,7 @@ where } impl ArtiClientTorClient { + /// Construct a new `ArtiClientTorClient` which uses a [Tokio](https://crates.io/crates/tokio) runtime internally for all async operations. pub fn new( tokio_runtime: Arc, root_data_directory: &Path, diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index d4384bf7b..febdb8343 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -23,6 +23,7 @@ use crate::tor_crypto::*; use crate::tor_provider; use crate::tor_provider::*; +/// [`LegacyTorClient`]-specific error type #[derive(thiserror::Error, Debug)] pub enum Error { #[error("failed to create LegacyTorProcess object")] @@ -172,6 +173,11 @@ pub enum LegacyTorClientConfig { // LegacyTorClient // +/// A `LegacyTorClient` implements the [`TorProvider`] trait using a legacy c-tor daemon backend. +/// +/// The tor process can either be launched and owned by `LegacyTorClient`, or it can use an already running tor-daemon. When using an already runnng tor-daemon, the [`TorProvider::bootstrap()`] automatically succeeds, presuming the connected tor-daemon has successfully bootstrapped. +/// +/// The minimum supported c-tor is version 0.4.6.1. pub struct LegacyTorClient { daemon: Option, version: LegacyTorVersion, @@ -186,6 +192,7 @@ pub struct LegacyTorClient { } impl LegacyTorClient { + /// Construct a new `LegacyTorClient` from a [`LegacyTorClientConfig`]. pub fn new(config: LegacyTorClientConfig) -> Result { let (daemon, mut controller, password, socks_listener) = match &config { LegacyTorClientConfig::BundledTor { @@ -443,6 +450,7 @@ impl LegacyTorClient { } #[allow(dead_code)] + /// Get the version of the connected c-tor daemon. pub fn version(&mut self) -> LegacyTorVersion { self.version.clone() } diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index 3c4869107..1dd83c253 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -1,10 +1,12 @@ #![doc = include_str!("../README.md")] +/// Implementation of an in-process [`arti-client`](https://crates.io/crates/arti-client)-based `TorProvider` #[cfg(feature = "arti-client-tor-provider")] pub mod arti_client_tor_client; #[cfg(feature = "legacy-tor-provider")] /// Censorship circumvention configuration for pluggable-transports and bridge settings pub mod censorship_circumvention; +/// Implementation of an out-of-process legacy [c-tor daemon](https://gitlab.torproject.org/tpo/core/tor)-based `TorProvider` #[cfg(feature = "legacy-tor-provider")] pub mod legacy_tor_client; #[cfg(feature = "legacy-tor-provider")] @@ -15,6 +17,7 @@ mod legacy_tor_controller; mod legacy_tor_process; #[cfg(feature = "legacy-tor-provider")] mod legacy_tor_version; +/// Implementation of a local, in-process, mock `TorProvider` for testing. #[cfg(feature = "mock-tor-provider")] pub mod mock_tor_client; #[cfg(feature = "legacy-tor-provider")] diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index b489406b4..ec546a37b 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -8,6 +8,8 @@ use crate::tor_crypto::*; use crate::tor_provider; use crate::tor_provider::*; + +/// [`MockTorClient`]-specific error type #[derive(thiserror::Error, Debug)] pub enum Error { #[error("client not bootstrapped")] @@ -126,6 +128,11 @@ impl MockTorNetwork { static MOCK_TOR_NETWORK: Mutex = Mutex::new(MockTorNetwork::new()); +/// A mock `TorProvider` implementation for testing. +/// +/// `MockTorClient` implements the [`TorProvider`] trait. It creates a fake, in-process Tor Network using local socekts and listeners. No actual traffic ever leaves the local host. +/// +/// Mock onion-services can be created, connected to, and communiccated with. Connecting to clearnet targets always succeeds by connecting to single local endpoint, but will never send any traffic to connecting clients. pub struct MockTorClient { events: Vec, bootstrapped: bool, @@ -135,6 +142,7 @@ pub struct MockTorClient { } impl MockTorClient { + /// Construt a new `MockTorClient`. pub fn new() -> MockTorClient { let mut events: Vec = Default::default(); let line = "[notice] MockTorClient running".to_string(); From d7bc144a6b87cf0d0e90231766f1115ac87ddbd9 Mon Sep 17 00:00:00 2001 From: Morgan Date: Sun, 28 Jul 2024 01:09:38 +0000 Subject: [PATCH 124/184] tor-interface: made LegacyTorVersion public and drafted in-source doucmentation for its public API surface --- tor-interface/src/legacy_tor_client.rs | 1 - tor-interface/src/legacy_tor_version.rs | 16 +++++++++------- tor-interface/src/lib.rs | 3 ++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index febdb8343..9e01c5fda 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -449,7 +449,6 @@ impl LegacyTorClient { }) } - #[allow(dead_code)] /// Get the version of the connected c-tor daemon. pub fn version(&mut self) -> LegacyTorVersion { self.version.clone() diff --git a/tor-interface/src/legacy_tor_version.rs b/tor-interface/src/legacy_tor_version.rs index 780f45b41..86789ef00 100644 --- a/tor-interface/src/legacy_tor_version.rs +++ b/tor-interface/src/legacy_tor_version.rs @@ -4,20 +4,21 @@ use std::option::Option; use std::str::FromStr; use std::string::ToString; +/// `LegacyTorVersion`-specific error type #[derive(thiserror::Error, Debug)] pub enum Error { #[error("{}", .0)] ParseError(String), } -// see version-spec.txt +/// Type representing a legacy c-tor daemon's version number. This version conforms c-tor's [version-spec](https://spec.torproject.org/version-spec.htm). #[derive(Clone)] pub struct LegacyTorVersion { - pub major: u32, - pub minor: u32, - pub micro: u32, - pub patch_level: u32, - pub status_tag: Option, + pub(crate) major: u32, + pub(crate) minor: u32, + pub(crate) micro: u32, + pub(crate) patch_level: u32, + pub(crate) status_tag: Option, } impl LegacyTorVersion { @@ -34,7 +35,8 @@ impl LegacyTorVersion { true } - fn new( + /// Construct a new `LegacyTorVersion` object. + pub fn new( major: u32, minor: u32, micro: u32, diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index 1dd83c253..dea3f8a6b 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -15,8 +15,9 @@ mod legacy_tor_control_stream; mod legacy_tor_controller; #[cfg(feature = "legacy-tor-provider")] mod legacy_tor_process; +/// Legacy c-tor daemon version. #[cfg(feature = "legacy-tor-provider")] -mod legacy_tor_version; +pub mod legacy_tor_version; /// Implementation of a local, in-process, mock `TorProvider` for testing. #[cfg(feature = "mock-tor-provider")] pub mod mock_tor_client; From fe34a19c6af96612b4bdd10a6dbab42d62412644 Mon Sep 17 00:00:00 2001 From: Morgan Date: Sun, 28 Jul 2024 18:19:29 +0000 Subject: [PATCH 125/184] tor-interface: update arti-client crate and dependencies to 0.20.0 --- tor-interface/Cargo.toml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index c443dbf8e..2fbf553e1 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { version = "0.18.0", features = ["experimental-api", "onion-service-client", "onion-service-service", "tokio"], optional = true} +arti-client = { version = "0.20.0", features = ["experimental-api", "onion-service-client", "onion-service-service", "tokio"], optional = true} curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" @@ -28,15 +28,15 @@ static_assertions = "1.1" thiserror = "1.0" tokio = { version = "1", features = ["macros"], optional = true } tokio-stream = { version = "0", optional = true } -tor-cell = { version = "0.18.0", optional = true } -tor-config = { version = "0.18.0", optional = true } -tor-hscrypto = { version = "0.18.0", optional = true } -tor-hsservice = { version = "0.18.0", optional = true } -tor-keymgr = { version = "0.18.0", optional = true, features = ["keymgr"] } -tor-llcrypto = { version = "0.18.0", features = ["relay"] } -tor-persist = { version = "0.18.0", optional = true } -tor-proto = { version = "0.18.0", features = ["stream-ctrl"], optional = true } -tor-rtcompat = { version = "0.18.0", optional = true } +tor-cell = { version = "0.20.0", optional = true } +tor-config = { version = "0.20.0", optional = true } +tor-hscrypto = { version = "0.20.0", optional = true } +tor-hsservice = { version = "0.20.0", optional = true } +tor-keymgr = { version = "0.20.0", optional = true, features = ["keymgr"] } +tor-llcrypto = { version = "0.20.0", features = ["relay"] } +tor-persist = { version = "0.20.0", optional = true } +tor-proto = { version = "0.20.0", features = ["stream-ctrl"], optional = true } +tor-rtcompat = { version = "0.20.0", optional = true } [dev-dependencies] anyhow = "1.0" From f53d31049214777b6783a62e37f3b4c586f88eb9 Mon Sep 17 00:00:00 2001 From: Morgan Date: Sun, 28 Jul 2024 22:17:57 +0000 Subject: [PATCH 126/184] tor-interface: fix forward_stream() function type annotations --- tor-interface/src/arti_client_tor_client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 4ae18f61e..0ea3ab9a8 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -99,8 +99,8 @@ pub struct ArtiClientTorClient { // used to forward traffic to/from arti to local tcp streams async fn forward_stream(alive: Arc, mut reader: R, mut writer: W) -> () where - R: tokio::io::AsyncRead + Unpin, - W: tokio::io::AsyncWrite + Unpin, + R: AsyncReadExt + Unpin, + W: AsyncWriteExt + Unpin, { // allow 100ms timeout on reads to verify writer is still good let read_timeout = std::time::Duration::from_millis(100); From 737faf6745c47b9d04a6b1d5a02247c4fce67e80 Mon Sep 17 00:00:00 2001 From: Morgan Date: Sat, 24 Aug 2024 19:47:20 +0000 Subject: [PATCH 127/184] honk-rpc, tor-interface, gosling, cgosling: version updates - updated honk-rpc version to 0.3.0 - updated tor-interface version to 0.4.0 - updated gosling version to 0.3.0 - updated cgosling version to 0.3.1 - added morgan to list of authors --- tor-interface/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 2fbf553e1..cc1b73b0c 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tor-interface" -authors = ["Richard Pospesel "] -version = "0.3.0" +authors = ["morgan ", "Richard Pospesel "] +version = "0.4.0" rust-version = "1.70" edition = "2021" license = "BSD-3-Clause" From f1ee4747b035f70e65517e6c0343f7501acbf805 Mon Sep 17 00:00:00 2001 From: Morgan Date: Tue, 3 Sep 2024 03:55:41 +0000 Subject: [PATCH 128/184] gosling: fix various build warnings/test failures when no tor-provider feature is specified --- tor-interface/README.md | 7 +------ tor-interface/tests/tor_provider.rs | 3 +++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/tor-interface/README.md b/tor-interface/README.md index c44f30bac..abdb38c54 100644 --- a/tor-interface/README.md +++ b/tor-interface/README.md @@ -22,12 +22,7 @@ The **arti-client-tor-provider** feature is experimental is not fully implemente The following code snippet creates a `LegacyTorClient` which starts a bundled tor daemon, bootstraps, and attempts to connect to [www.example.com](www.example.com). -```rust -# use std::str::FromStr; -# use std::net::TcpStream; -# use tor_interface::legacy_tor_client::{LegacyTorClient, LegacyTorClientConfig}; -# use tor_interface::tor_provider::{OnionStream, TargetAddr, TorEvent, TorProvider}; -# return; +```rust,ignore // construct legacy tor client config let tor_path = std::path::PathBuf::from_str("/usr/bin/tor").unwrap(); let mut data_path = std::env::temp_dir(); diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index e3b8a10a6..1449a0357 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -27,6 +27,7 @@ use tor_interface::mock_tor_client::*; use tor_interface::tor_crypto::*; use tor_interface::tor_provider::*; +#[allow(dead_code)] pub(crate) fn bootstrap_test(mut tor: Box) -> anyhow::Result<()> { tor.bootstrap()?; @@ -63,6 +64,7 @@ pub(crate) fn bootstrap_test(mut tor: Box) -> anyhow::Result<() Ok(()) } +#[allow(dead_code)] pub(crate) fn basic_onion_service_test( mut server_provider: Box, mut client_provider: Box, @@ -188,6 +190,7 @@ pub(crate) fn basic_onion_service_test( Ok(()) } +#[allow(dead_code)] pub(crate) fn authenticated_onion_service_test( mut server_provider: Box, mut client_provider: Box, From 1abfd024ed1ef884079b552b4ba8aca6798b2f30 Mon Sep 17 00:00:00 2001 From: Morgan Date: Tue, 3 Sep 2024 03:16:42 +0000 Subject: [PATCH 129/184] tor-interface: updated arti-client to 0.22.0 --- tor-interface/Cargo.toml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index cc1b73b0c..962487ad8 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { version = "0.20.0", features = ["experimental-api", "onion-service-client", "onion-service-service", "tokio"], optional = true} +arti-client = { version = "0.22.0", features = ["experimental-api", "onion-service-client", "onion-service-service", "tokio"], optional = true} curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" @@ -28,15 +28,15 @@ static_assertions = "1.1" thiserror = "1.0" tokio = { version = "1", features = ["macros"], optional = true } tokio-stream = { version = "0", optional = true } -tor-cell = { version = "0.20.0", optional = true } -tor-config = { version = "0.20.0", optional = true } -tor-hscrypto = { version = "0.20.0", optional = true } -tor-hsservice = { version = "0.20.0", optional = true } -tor-keymgr = { version = "0.20.0", optional = true, features = ["keymgr"] } -tor-llcrypto = { version = "0.20.0", features = ["relay"] } -tor-persist = { version = "0.20.0", optional = true } -tor-proto = { version = "0.20.0", features = ["stream-ctrl"], optional = true } -tor-rtcompat = { version = "0.20.0", optional = true } +tor-cell = { version = "0.22.0", optional = true } +tor-config = { version = "0.22.0", optional = true } +tor-hscrypto = { version = "0.22.0", optional = true } +tor-hsservice = { version = "0.22.0", optional = true } +tor-keymgr = { version = "0.22.0", optional = true, features = ["keymgr"] } +tor-llcrypto = { version = "0.22.0", features = ["relay"] } +tor-persist = { version = "0.22.0", optional = true } +tor-proto = { version = "0.22.0", features = ["stream-ctrl"], optional = true } +tor-rtcompat = { version = "0.22.0", optional = true } [dev-dependencies] anyhow = "1.0" From 5e6fff41613359a5ebab602efd52585ca76c1706 Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 6 Sep 2024 22:51:49 +0000 Subject: [PATCH 130/184] tor-interface: replaced deprecated OnionService::new() call with OnionServiceBuilder --- tor-interface/src/arti_client_tor_client.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 0ea3ab9a8..53affc428 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -16,7 +16,7 @@ use tokio_stream::StreamExt; use tor_cell::relaycell::msg::Connected; use tor_hscrypto::pk::HsIdKeypair; use tor_hsservice::config::OnionServiceConfigBuilder; -use tor_hsservice::{HsIdKeypairSpecifier, HsNickname, OnionService, RunningOnionService}; +use tor_hsservice::{HsIdKeypairSpecifier, HsNickname, OnionServiceBuilder, RunningOnionService}; use tor_keymgr::{ArtiEphemeralKeystore, KeyMgrBuilder, KeystoreSelector}; use tor_llcrypto::pk::ed25519::ExpandedKeypair; use tor_persist::state_dir::StateDirectory; @@ -432,8 +432,16 @@ impl TorProvider for ArtiClientTorClient { Ok(state_dir) => state_dir, Err(err) => Err(err).map_err(Error::TorPersistError)?, }; - let onion_service = OnionService::new(onion_service_config, Arc::new(keymgr), &state_dir) - .map_err(Error::TorHsServiceStartupError)?; + + let onion_service = match OnionServiceBuilder::default() + .config(onion_service_config) + .keymgr(Arc::new(keymgr)) + .state_dir(state_dir) + .build() + { + Ok(onion_service) => onion_service, + Err(err) => Err(err).map_err(Error::TorHsServiceStartupError)?, + }; let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port)); From 3deb1455b4742c8adc69afc58f7d463b7161348e Mon Sep 17 00:00:00 2001 From: Morgan Date: Sat, 7 Sep 2024 00:12:16 +0000 Subject: [PATCH 131/184] tor-interface: added support for configuring onion-services with client-auth/restricted-discovery keys to ArtiClientTorClient --- tor-interface/CMakeLists.txt | 5 ++++ tor-interface/Cargo.toml | 2 +- tor-interface/src/arti_client_tor_client.rs | 33 ++++++++++++++++----- tor-interface/src/tor_crypto.rs | 6 ++++ tor-interface/tests/tor_provider.rs | 26 ++++++++++++++++ 5 files changed, 64 insertions(+), 8 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 74debbeb0..c51459c75 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -118,6 +118,11 @@ if (ENABLE_TESTS) COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_legacy_arti_client_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) + add_test(NAME tor_interface_mixed_arti_client_legacy_authenticated_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_arti_client_legacy_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() # cryptography diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 962487ad8..51dcd6e97 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -31,7 +31,7 @@ tokio-stream = { version = "0", optional = true } tor-cell = { version = "0.22.0", optional = true } tor-config = { version = "0.22.0", optional = true } tor-hscrypto = { version = "0.22.0", optional = true } -tor-hsservice = { version = "0.22.0", optional = true } +tor-hsservice = { version = "0.22.0", optional = true, features = ["restricted-discovery"] } tor-keymgr = { version = "0.22.0", optional = true, features = ["keymgr"] } tor-llcrypto = { version = "0.22.0", features = ["relay"] } tor-persist = { version = "0.22.0", optional = true } diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 53affc428..a0057c87e 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -2,6 +2,7 @@ use std::net::SocketAddr; use std::ops::DerefMut; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; @@ -16,6 +17,7 @@ use tokio_stream::StreamExt; use tor_cell::relaycell::msg::Connected; use tor_hscrypto::pk::HsIdKeypair; use tor_hsservice::config::OnionServiceConfigBuilder; +use tor_hsservice::config::restricted_discovery::HsClientNickname; use tor_hsservice::{HsIdKeypairSpecifier, HsNickname, OnionServiceBuilder, RunningOnionService}; use tor_keymgr::{ArtiEphemeralKeystore, KeyMgrBuilder, KeystoreSelector}; use tor_llcrypto::pk::ed25519::ExpandedKeypair; @@ -367,10 +369,6 @@ impl TorProvider for ArtiClientTorClient { virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, ) -> Result { - // client auth is not implemented yet - if authorized_clients.is_some() { - return Err(Error::NotImplemented().into()); - } // try to bind to a local address, let OS pick our port let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); @@ -419,9 +417,30 @@ impl TorProvider for ArtiClientTorClient { } // create an OnionServiceConfig with the ephemeral nickname - let onion_service_config = match OnionServiceConfigBuilder::default() - .nickname(hs_nickname) - .build() + let mut onion_service_config_builder = OnionServiceConfigBuilder::default(); + onion_service_config_builder.nickname(hs_nickname); + + // add authorised client keys if they exist + if let Some(authorized_clients) = authorized_clients { + if !authorized_clients.is_empty() { + let restricted_discovery_config = onion_service_config_builder + .restricted_discovery(); + restricted_discovery_config.enabled(true); + + for (i, key) in authorized_clients.iter().enumerate() { + let nickname = format!("client_{i}"); + restricted_discovery_config + .static_keys() + .access() + .push(( + HsClientNickname::from_str(nickname.as_str()).unwrap(), + key.inner().clone().into(), + )); + } + } + } + + let onion_service_config = match onion_service_config_builder.build() { Ok(onion_service_config) => onion_service_config, Err(err) => Err(err).map_err(Error::OnionServiceConfigBuilderError)?, diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 52b3d263c..13c052e78 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -690,6 +690,12 @@ impl X25519PublicKey { pub fn as_bytes(&self) -> &[u8; X25519_PUBLIC_KEY_SIZE] { self.public_key.as_bytes() } + + #[cfg(feature = "arti-client-tor-provider")] + pub(crate) fn inner(&self) -> &pk::curve25519::PublicKey { + &self.public_key + } + } impl std::fmt::Debug for X25519PublicKey { diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 1449a0357..11bf68f70 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -734,6 +734,32 @@ fn test_mixed_legacy_arti_client_onion_service() -> anyhow::Result<()> { basic_onion_service_test(server_provider, client_provider) } +#[test] +#[serial] +#[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] +fn test_mixed_arti_client_legacy_authenticated_onion_service() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + + let mut data_path = std::env::temp_dir(); + data_path.push("test_mixed_arti_client_legacy_authenticated_onion_service_server"); + let server_provider = Box::new(ArtiClientTorClient::new(runtime, &data_path)?); + + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push("test_mixed_arti_client_legacy_authenticated_onion_service_client"); + let tor_config = LegacyTorClientConfig::BundledTor { + tor_bin_path: tor_path, + data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, + }; + let client_provider = Box::new(LegacyTorClient::new(tor_config)?); + + authenticated_onion_service_test(server_provider, client_provider) +} + // // Misc Utils // From 13b79c582c7b7ff70578559dfd9cf1c7c7ac82aa Mon Sep 17 00:00:00 2001 From: Morgan Date: Sun, 22 Sep 2024 20:33:39 +0000 Subject: [PATCH 132/184] tor-interface: add explicit bootstrapped flag to gracefully handle receipt of out-of-order bootstrap events --- tor-interface/src/arti_client_tor_client.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index a0057c87e..16f9bc3b1 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -96,6 +96,7 @@ pub struct ArtiClientTorClient { state_dir: PathBuf, fs_mistrust: Mistrust, pending_events: Arc>>, + bootstrapped: Arc, } // used to forward traffic to/from arti to local tcp streams @@ -209,6 +210,7 @@ impl ArtiClientTorClient { state_dir, fs_mistrust, pending_events, + bootstrapped: Arc::new(AtomicBool::new(false)), }) } } @@ -228,8 +230,12 @@ impl TorProvider for ArtiClientTorClient { // save progress events let mut bootstrap_events = self.arti_client.bootstrap_events(); let pending_events = self.pending_events.clone(); + let bootstrapped = self.bootstrapped.clone(); self.tokio_runtime.spawn(async move { while let Some(evt) = bootstrap_events.next().await { + if bootstrapped.load(Ordering::Relaxed) { + break; + } match pending_events.lock() { Ok(mut pending_events) => { pending_events.push(TorEvent::BootstrapStatus { @@ -249,11 +255,18 @@ impl TorProvider for ArtiClientTorClient { // initiate bootstrap let arti_client = self.arti_client.clone(); let pending_events = self.pending_events.clone(); + let bootstrapped = self.bootstrapped.clone(); self.tokio_runtime.spawn(async move { match arti_client.bootstrap().await { Ok(()) => match pending_events.lock() { Ok(mut pending_events) => { + pending_events.push(TorEvent::BootstrapStatus { + progress: 100, + tag: "no-tag".to_string(), + summary: "no summary".to_string(), + }); pending_events.push(TorEvent::BootstrapComplete); + bootstrapped.store(true, Ordering::Relaxed); return; } Err(_) => unreachable!( From c964cacfbce794d0d93be9855cb38a5f1b2e48c8 Mon Sep 17 00:00:00 2001 From: Morgan Date: Sun, 6 Oct 2024 00:11:32 +0000 Subject: [PATCH 133/184] tor-interface: updated arti-client to 0.23.0 --- tor-interface/Cargo.toml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 51dcd6e97..c0cf0bf92 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { version = "0.22.0", features = ["experimental-api", "onion-service-client", "onion-service-service", "tokio"], optional = true} +arti-client = { version = "0.23.0", features = ["experimental-api", "onion-service-client", "onion-service-service", "tokio"], optional = true} curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" @@ -28,15 +28,15 @@ static_assertions = "1.1" thiserror = "1.0" tokio = { version = "1", features = ["macros"], optional = true } tokio-stream = { version = "0", optional = true } -tor-cell = { version = "0.22.0", optional = true } -tor-config = { version = "0.22.0", optional = true } -tor-hscrypto = { version = "0.22.0", optional = true } -tor-hsservice = { version = "0.22.0", optional = true, features = ["restricted-discovery"] } -tor-keymgr = { version = "0.22.0", optional = true, features = ["keymgr"] } -tor-llcrypto = { version = "0.22.0", features = ["relay"] } -tor-persist = { version = "0.22.0", optional = true } -tor-proto = { version = "0.22.0", features = ["stream-ctrl"], optional = true } -tor-rtcompat = { version = "0.22.0", optional = true } +tor-cell = { version = "0.23.0", optional = true } +tor-config = { version = "0.23.0", optional = true } +tor-hscrypto = { version = "0.23.0", optional = true } +tor-hsservice = { version = "0.23.0", optional = true, features = ["restricted-discovery"] } +tor-keymgr = { version = "0.23.0", optional = true, features = ["keymgr"] } +tor-llcrypto = { version = "0.23.0", features = ["relay"] } +tor-persist = { version = "0.23.0", optional = true } +tor-proto = { version = "0.23.0", features = ["stream-ctrl"], optional = true } +tor-rtcompat = { version = "0.23.0", optional = true } [dev-dependencies] anyhow = "1.0" From 3c3137b8a441bc3a8b5a7f43f4cf5f6442a22d7f Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 9 Sep 2024 00:09:36 +0000 Subject: [PATCH 134/184] tor-interface: use arti_client::TorClient::launch_onion_service_with_hsid() method to launch onion services --- tor-interface/Cargo.toml | 7 +- tor-interface/src/arti_client_tor_client.rs | 103 +++++--------------- 2 files changed, 24 insertions(+), 86 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index c0cf0bf92..c4c533369 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,12 +10,11 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { version = "0.23.0", features = ["experimental-api", "onion-service-client", "onion-service-service", "tokio"], optional = true} +arti-client = { version = "0.23.0", features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" domain = "<= 0.10.0" -fs-mistrust = { version = "0", optional = true } idna = "1" rand = "0.8" rand_core = "0.6" @@ -30,11 +29,9 @@ tokio = { version = "1", features = ["macros"], optional = true } tokio-stream = { version = "0", optional = true } tor-cell = { version = "0.23.0", optional = true } tor-config = { version = "0.23.0", optional = true } -tor-hscrypto = { version = "0.23.0", optional = true } tor-hsservice = { version = "0.23.0", optional = true, features = ["restricted-discovery"] } tor-keymgr = { version = "0.23.0", optional = true, features = ["keymgr"] } tor-llcrypto = { version = "0.23.0", features = ["relay"] } -tor-persist = { version = "0.23.0", optional = true } tor-proto = { version = "0.23.0", features = ["stream-ctrl"], optional = true } tor-rtcompat = { version = "0.23.0", optional = true } @@ -44,6 +41,6 @@ serial_test = "0.9" which = "4.4" [features] -arti-client-tor-provider = ["arti-client", "fs-mistrust", "tokio", "tokio-stream", "tor-cell", "tor-config", "tor-hscrypto", "tor-hsservice", "tor-keymgr", "tor-persist", "tor-proto", "tor-rtcompat"] +arti-client-tor-provider = ["arti-client", "tokio", "tokio-stream", "tor-cell", "tor-config", "tor-hsservice", "tor-keymgr", "tor-proto", "tor-rtcompat"] mock-tor-provider = [] legacy-tor-provider = [] diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 16f9bc3b1..03ee9014e 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -9,19 +9,17 @@ use std::sync::{Arc, Mutex}; //extern use arti_client::config::{CfgPath, TorClientConfigBuilder}; use arti_client::{BootstrapBehavior, DangerouslyIntoTorAddr, IntoTorAddr, TorClient}; -use fs_mistrust::Mistrust; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::runtime; use tokio_stream::StreamExt; use tor_cell::relaycell::msg::Connected; -use tor_hscrypto::pk::HsIdKeypair; +use tor_config::ExplicitOrAuto; +use tor_llcrypto::pk::ed25519::ExpandedKeypair; use tor_hsservice::config::OnionServiceConfigBuilder; use tor_hsservice::config::restricted_discovery::HsClientNickname; -use tor_hsservice::{HsIdKeypairSpecifier, HsNickname, OnionServiceBuilder, RunningOnionService}; -use tor_keymgr::{ArtiEphemeralKeystore, KeyMgrBuilder, KeystoreSelector}; -use tor_llcrypto::pk::ed25519::ExpandedKeypair; -use tor_persist::state_dir::StateDirectory; +use tor_hsservice::{HsNickname, RunningOnionService}; +use tor_keymgr::{config::arti::ArtiKeystoreKind, KeystoreSelector}; use tor_proto::stream::IncomingStreamRequest; use tor_rtcompat::PreferredRuntime; @@ -60,25 +58,14 @@ pub enum Error { #[error("arti-client tor-addr error: {0}")] ArtiClientTorAddrError(#[source] arti_client::TorAddrError), + #[error("arti-client onion-service startup error: {0}")] + ArtiClientOnionServiceLaunchError(#[source] arti_client::Error), + #[error("tor-keymgr error: {0}")] TorKeyMgrError(#[source] tor_keymgr::Error), - // TODO: the 'real' error (tor_keymgr::mgr::KeyMgrBuilderError) isn't - // actually defined anywhere from what I can tell - #[error("failed to build KeyMgr")] - TorKeyMgrBuilderError(), - - #[error("unexpected key found when inserting HsIdKeypair")] - KeyMgrInsertionFailure(), - #[error("onion-service config-builder error: {0}")] OnionServiceConfigBuilderError(#[source] tor_config::ConfigBuildError), - - #[error("tor-persist error: {0}")] - TorPersistError(#[source] tor_persist::Error), - - #[error("tor-hsservice startup error: {0}")] - TorHsServiceStartupError(#[source] tor_hsservice::StartupError), } impl From for crate::tor_provider::Error { @@ -93,8 +80,6 @@ impl From for crate::tor_provider::Error { pub struct ArtiClientTorClient { tokio_runtime: Arc, arti_client: TorClient, - state_dir: PathBuf, - fs_mistrust: Mistrust, pending_events: Arc>>, bootstrapped: Arc, } @@ -167,13 +152,15 @@ impl ArtiClientTorClient { cache_dir.push("cache"); config_builder .storage() - .cache_dir(CfgPath::new_literal(cache_dir)); + .cache_dir(CfgPath::new_literal(cache_dir)) + .keystore() + .primary().kind(ExplicitOrAuto::Explicit(ArtiKeystoreKind::Ephemeral)); let mut state_dir = PathBuf::from(root_data_directory); state_dir.push("state"); config_builder .storage() - .state_dir(CfgPath::new_literal(state_dir.clone())); + .state_dir(CfgPath::new_literal(state_dir)); // disable access to clearnet addresses and enable access to onion services config_builder @@ -186,8 +173,6 @@ impl ArtiClientTorClient { Err(err) => return Err(err).map_err(Error::ArtiClientConfigBuilderError), }; - let fs_mistrust = config.fs_mistrust().clone(); - let arti_client = tokio_runtime.block_on(async { TorClient::builder() .config(config) @@ -207,8 +192,6 @@ impl ArtiClientTorClient { Ok(Self { tokio_runtime, arti_client, - state_dir, - fs_mistrust, pending_events, bootstrapped: Arc::new(AtomicBool::new(false)), }) @@ -392,17 +375,6 @@ impl TorProvider for ArtiClientTorClient { .local_addr() .map_err(Error::TcpListenerLocalAddrFailed)?; - // create a new ephemeral store for storing our onion service keys - let ephemeral_store: ArtiEphemeralKeystore = - ArtiEphemeralKeystore::new("ephemeral".to_string()); - let keymgr = match KeyMgrBuilder::default() - .default_store(Box::new(ephemeral_store)) - .build() - { - Ok(keymgr) => keymgr, - Err(_) => return Err(Error::TorKeyMgrBuilderError().into()), - }; - // generate a nickname to identify this onion service let service_id = V3OnionServiceId::from_private_key(private_key); let hs_nickname = match HsNickname::new(service_id.to_string()) { @@ -411,27 +383,16 @@ impl TorProvider for ArtiClientTorClient { panic!("v3 onion service id string representation should be a valid HsNickname") } }; - - let hs_id_spec = HsIdKeypairSpecifier::new(hs_nickname.clone()); // generate a new HsIdKeypair (from an Ed25519PrivateKey) // clone() isn't implemented for ExpandedKeypair >:[ let secret_key_bytes = private_key.inner().to_secret_key_bytes(); - let expanded_keypair = ExpandedKeypair::from_secret_key_bytes(secret_key_bytes) - .unwrap() - .into(); - - // write the HsIdKeypair to keymgr - // TODO: for now this should return Ok(None) unless we persist the ephemeral store longer-term (ie for client auth keys in the future) - match keymgr.insert::(expanded_keypair, &hs_id_spec, KeystoreSelector::Default) - { - Ok(None) => (), // expected - Ok(Some(_)) => return Err(Error::KeyMgrInsertionFailure().into()), - Err(err) => Err(err).map_err(Error::TorKeyMgrError)?, - } + let hs_id_keypair = ExpandedKeypair::from_secret_key_bytes(secret_key_bytes) + .unwrap(); // create an OnionServiceConfig with the ephemeral nickname let mut onion_service_config_builder = OnionServiceConfigBuilder::default(); - onion_service_config_builder.nickname(hs_nickname); + onion_service_config_builder + .nickname(hs_nickname); // add authorised client keys if they exist if let Some(authorized_clients) = authorized_clients { @@ -459,42 +420,21 @@ impl TorProvider for ArtiClientTorClient { Err(err) => Err(err).map_err(Error::OnionServiceConfigBuilderError)?, }; - // create OnionService - let state_dir = match StateDirectory::new(self.state_dir.as_path(), &self.fs_mistrust) { - Ok(state_dir) => state_dir, - Err(err) => Err(err).map_err(Error::TorPersistError)?, - }; - - let onion_service = match OnionServiceBuilder::default() - .config(onion_service_config) - .keymgr(Arc::new(keymgr)) - .state_dir(state_dir) - .build() - { - Ok(onion_service) => onion_service, - Err(err) => Err(err).map_err(Error::TorHsServiceStartupError)?, - }; - - let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port)); - - // launch the OnionService and get a Stream of RendRequest - let runtime = self.arti_client.runtime().clone(); - let dirmgr = self.arti_client.dirmgr().clone().upcast_arc(); - let hs_circ_pool = self.arti_client.hs_circ_pool().clone(); - - let (onion_service, mut rend_requests) = onion_service - .launch(runtime, dirmgr, hs_circ_pool) - .map_err(Error::TorHsServiceStartupError)?; + let (onion_service, mut rend_requests) = self.arti_client + .launch_onion_service_with_hsid(onion_service_config, hs_id_keypair.into()) + .map_err(Error::ArtiClientOnionServiceLaunchError)?; // start a task to signal onion service published let pending_events = self.pending_events.clone(); let mut status_events = onion_service.status_events(); + let service_id_clone = service_id.clone(); + self.tokio_runtime.spawn(async move { while let Some(evt) = status_events.next().await { match evt.state() { tor_hsservice::status::State::Running => match pending_events.lock() { Ok(mut pending_events) => { - pending_events.push(TorEvent::OnionServicePublished { service_id }); + pending_events.push(TorEvent::OnionServicePublished { service_id: service_id_clone }); return; } Err(_) => unreachable!( @@ -563,6 +503,7 @@ impl TorProvider for ArtiClientTorClient { } }); + let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id, virt_port)); // onion-service is torn down when `onion_service` is dropped Ok(OnionListener::new::>(listener, onion_addr, onion_service, |_|{})) } From 2182d6935948ebf4d2f316bc27c1d373ba9d7f68 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 9 Sep 2024 03:16:42 +0000 Subject: [PATCH 135/184] tor-interface: implement add_client_auth() and remove_client_auth() methods for ArtiClientTorClient --- tor-interface/CMakeLists.txt | 9 +++++- tor-interface/src/arti_client_tor_client.rs | 20 +++++++++---- tor-interface/src/tor_crypto.rs | 5 ++++ tor-interface/tests/tor_provider.rs | 33 +++++++++++++++++---- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index c51459c75..8460e0b79 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -107,6 +107,10 @@ if (ENABLE_TESTS) COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_arti_client_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) + add_test(NAME tor_interface_arti_authenticated_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_arti_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) endif() if (ENABLE_LEGACY_TOR_PROVIDER AND ENABLE_ARTI_CLIENT_TOR_PROVIDER) @@ -122,7 +126,10 @@ if (ENABLE_TESTS) COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_arti_client_legacy_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) - + add_test(NAME tor_interface_mixed_legacy_arti_client_authenticated_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_legacy_arti_client_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) endif() # cryptography diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 03ee9014e..7afd6c9de 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -267,17 +267,27 @@ impl TorProvider for ArtiClientTorClient { fn add_client_auth( &mut self, - _service_id: &V3OnionServiceId, - _client_auth: &X25519PrivateKey, + service_id: &V3OnionServiceId, + client_auth: &X25519PrivateKey, ) -> Result<(), tor_provider::Error> { - Err(Error::NotImplemented().into()) + let ed25519_public = Ed25519PublicKey::from_service_id(service_id).unwrap(); + let hs_id = ed25519_public.as_bytes().clone(); + + self.arti_client.insert_service_discovery_key(KeystoreSelector::Primary, hs_id.into(), client_auth.inner().clone().into()).map_err(Error::ArtiClientError)?; + + Ok(()) } fn remove_client_auth( &mut self, - _service_id: &V3OnionServiceId, + service_id: &V3OnionServiceId, ) -> Result<(), tor_provider::Error> { - Err(Error::NotImplemented().into()) + let ed25519_public = Ed25519PublicKey::from_service_id(service_id).unwrap(); + let hs_id = ed25519_public.as_bytes().clone(); + + self.arti_client.remove_service_discovery_key(KeystoreSelector::Primary, hs_id.into()).map_err(Error::ArtiClientError)?; + + Ok(()) } fn connect( diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 13c052e78..4b921699a 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -611,6 +611,11 @@ impl X25519PrivateKey { pub fn to_bytes(&self) -> [u8; X25519_PRIVATE_KEY_SIZE] { self.secret_key.to_bytes() } + + #[cfg(feature = "arti-client-tor-provider")] + pub(crate) fn inner(&self) -> &pk::curve25519::StaticSecret { + &self.secret_key + } } impl PartialEq for X25519PrivateKey { diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 11bf68f70..35976e7c8 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -657,8 +657,6 @@ fn test_arti_client_onion_service() -> anyhow::Result<()> { basic_onion_service_test(server_provider, client_provider) } -/* -TODO: re-enable once client-auth is available in arti #[test] #[serial] #[cfg(feature = "arti-client-tor-provider")] @@ -666,16 +664,15 @@ fn test_arti_authenticated_onion_service() -> anyhow::Result<()> { let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); let mut data_path = std::env::temp_dir(); - data_path.push("test_arti_basic_onion_service_server"); + data_path.push("test_arti_authenticated_onion_service_server"); let server_provider = Box::new(ArtiClientTorClient::new(runtime.clone(), &data_path).unwrap()); let mut data_path = std::env::temp_dir(); - data_path.push("test_arti_basic_onion_service_client"); + data_path.push("test_arti_authenticated_onion_service_client"); let client_provider = Box::new(ArtiClientTorClient::new(runtime.clone(), &data_path).unwrap()); authenticated_onion_service_test(server_provider, client_provider) } -*/ // // Mixed Arti/Legacy TorProvider tests @@ -760,6 +757,32 @@ fn test_mixed_arti_client_legacy_authenticated_onion_service() -> anyhow::Result authenticated_onion_service_test(server_provider, client_provider) } +#[test] +#[serial] +#[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] +fn test_mixed_legacy_arti_client_authenticated_onion_service() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push("test_mixed_legacy_arti_client_authenticated_onion_service_server"); + let tor_config = LegacyTorClientConfig::BundledTor { + tor_bin_path: tor_path, + data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, + }; + let server_provider = Box::new(LegacyTorClient::new(tor_config)?); + + let mut data_path = std::env::temp_dir(); + data_path.push("test_mixed_legacy_arti_client_authenticated_onion_service_client"); + let client_provider = Box::new(ArtiClientTorClient::new(runtime, &data_path)?); + + authenticated_onion_service_test(server_provider, client_provider) +} + // // Misc Utils // From af17c01e8df0ffd3750a13fb588031526ef86a8f Mon Sep 17 00:00:00 2001 From: Morgan Date: Sun, 6 Oct 2024 02:48:58 +0000 Subject: [PATCH 136/184] tor-interface: temporarily update arti-client related crates to revision 0956b386580d5d75983a8faff4e3043654936698 - this revision replaces the simple_asn1 crate with der_parser crate to work around bug exposed when time/large-dates feature is enabled - required until upstream 0.24.0 is ready --- tor-interface/Cargo.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index c4c533369..9e3f460d6 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { version = "0.23.0", features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} +arti-client = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" @@ -27,13 +27,13 @@ static_assertions = "1.1" thiserror = "1.0" tokio = { version = "1", features = ["macros"], optional = true } tokio-stream = { version = "0", optional = true } -tor-cell = { version = "0.23.0", optional = true } -tor-config = { version = "0.23.0", optional = true } -tor-hsservice = { version = "0.23.0", optional = true, features = ["restricted-discovery"] } -tor-keymgr = { version = "0.23.0", optional = true, features = ["keymgr"] } -tor-llcrypto = { version = "0.23.0", features = ["relay"] } -tor-proto = { version = "0.23.0", features = ["stream-ctrl"], optional = true } -tor-rtcompat = { version = "0.23.0", optional = true } +tor-cell = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", optional = true } +tor-config = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", optional = true } +tor-hsservice = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", optional = true, features = ["restricted-discovery"] } +tor-keymgr = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", optional = true, features = ["keymgr"] } +tor-llcrypto = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", features = ["relay"] } +tor-proto = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", features = ["stream-ctrl"], optional = true } +tor-rtcompat = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", optional = true } [dev-dependencies] anyhow = "1.0" From 2a2b3e1a899770915a8417ecbc531e67f06ffb5c Mon Sep 17 00:00:00 2001 From: Morgan Date: Tue, 22 Oct 2024 04:07:39 +0000 Subject: [PATCH 137/184] tor-interface: create builder functions for various TorProvider implementations in tests --- tor-interface/tests/tor_provider.rs | 497 +++++++++++----------------- 1 file changed, 200 insertions(+), 297 deletions(-) diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 35976e7c8..3eb1fe322 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -2,6 +2,7 @@ #[cfg(feature = "legacy-tor-provider")] use std::fs::File; use std::io::{Read, Write}; +use std::ops::Drop; #[cfg(feature = "legacy-tor-provider")] use std::process; #[cfg(feature = "legacy-tor-provider")] @@ -27,6 +28,163 @@ use tor_interface::mock_tor_client::*; use tor_interface::tor_crypto::*; use tor_interface::tor_provider::*; +// +// TorProvider Factory Functions +// + +// purely in-process mock tor provider +#[cfg(test)] +#[cfg(feature = "mock-tor-provider")] +fn build_mock_tor_provider() -> anyhow::Result> { + Ok(Box::new(MockTorClient::new())) +} + +// out-of-process c-tor owned by this process +#[cfg(test)] +#[cfg(feature = "legacy-tor-provider")] +fn build_bundled_legacy_tor_provider(name: &str) -> anyhow::Result> { + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push(name); + + let tor_config = LegacyTorClientConfig::BundledTor { + tor_bin_path: tor_path, + data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, + }; + + Ok(Box::new(LegacyTorClient::new(tor_config)?)) +} + +// out-of-process pt-using c-tor owned by this process +#[cfg(test)] +#[cfg(feature = "legacy-tor-provider")] +fn build_bundled_pt_legacy_tor_provider(name: &str) -> anyhow::Result>> { + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push(name); + + // find the lyrebird bin + let teb_path = std::env::var("TEB_PATH")?; + if teb_path.is_empty() { + println!("TEB_PATH environment variable empty, so skipping test_legacy_pluggable_transport_bootstrap()"); + return Ok(None); + } + let mut lyrebird_path = std::path::PathBuf::from(&teb_path); + let lyrebird_bin = format!("lyrebird{}", std::env::consts::EXE_SUFFIX); + lyrebird_path.push(lyrebird_bin.clone()); + assert!(std::path::Path::exists(&lyrebird_path)); + assert!(std::path::Path::is_file(&lyrebird_path)); + + // configure lyrebird pluggable transport + let pluggable_transport = + PluggableTransportConfig::new(vec!["obfs4".to_string()], lyrebird_path)?; + + // obfs4 bridgeline + let bridge_line = BridgeLine::from_str("obfs4 207.172.185.193:22223 F34AC0CDBC06918E54292A474578C99834A58893 cert=MjqosoyVylLQuLo4LH+eQ5hS7Z44s2CaMfQbIjJtn4bGRnvLv8ldSvSED5JpvWSxm09XXg iat-mode=0")?; + + let tor_config = LegacyTorClientConfig::BundledTor { + tor_bin_path: tor_path, + data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: Some(vec![pluggable_transport]), + bridge_lines: Some(vec![bridge_line]), + }; + + Ok(Some(Box::new(LegacyTorClient::new(tor_config)?))) +} + +#[cfg(feature = "legacy-tor-provider")] +struct TorProcess {child: Child} +#[cfg(feature = "legacy-tor-provider")] +impl Drop for TorProcess { + fn drop(&mut self) -> () { + let _ = self.child.kill(); + } +} + +#[cfg(test)] +#[cfg(feature = "legacy-tor-provider")] +fn build_system_legacy_tor_provider( + name: &str, + control_port: u16, + socks_port: u16, +) -> anyhow::Result<(Box, TorProcess)> { + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + + let mut data_path = std::env::temp_dir(); + data_path.push(name); + std::fs::create_dir_all(&data_path)?; + let default_torrc = data_path.join("default_torrc"); + { + let _ = File::create(&default_torrc)?; + } + let torrc = data_path.join("torrc"); + { + let _ = File::create(&torrc)?; + } + + let tor_daemon = TorProcess { child: Command::new(tor_path) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + // point to our above written torrc file + .arg("--defaults-torrc") + .arg(default_torrc) + // location of torrc + .arg("--torrc-file") + .arg(torrc) + // enable networking + .arg("DisableNetwork") + .arg("0") + // root data directory + .arg("DataDirectory") + .arg(data_path) + // control port + .arg("ControlPort") + .arg(control_port.to_string()) + // password: foobar1 + .arg("HashedControlPassword") + .arg("16:E807DCE69AFE9979600760C9758B95ADB2F95E8740478AEA5356C95358") + // socks port + .arg("SocksPort") + .arg(socks_port.to_string()) + // tor process will shut down after this process shuts down + // to avoid orphaned tor daemon + .arg("__OwningControllerProcess") + .arg(process::id().to_string()) + .spawn()? + }; + // give daemons time to start + std::thread::sleep(std::time::Duration::from_secs(5)); + + let tor_config = LegacyTorClientConfig::SystemTor { + tor_socks_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{socks_port}").as_str())?, + tor_control_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{control_port}").as_str())?, + tor_control_passwd: "password".to_string(), + }; + let tor_provider = Box::new(LegacyTorClient::new(tor_config)?); + + Ok((tor_provider, tor_daemon)) +} + +#[cfg(test)] +#[cfg(feature = "arti-client-tor-provider")] +fn build_arti_client_tor_provider(runtime: Arc, name: &str) -> anyhow::Result> { + + let mut data_path = std::env::temp_dir(); + data_path.push(name); + Ok(Box::new(ArtiClientTorClient::new(runtime, &data_path)?)) +} + +// +// Test Functions +// + #[allow(dead_code)] pub(crate) fn bootstrap_test(mut tor: Box) -> anyhow::Result<()> { tor.bootstrap()?; @@ -331,22 +489,22 @@ pub(crate) fn authenticated_onion_service_test( #[test] #[cfg(feature = "mock-tor-provider")] fn test_mock_bootstrap() -> anyhow::Result<()> { - bootstrap_test(Box::new(MockTorClient::new())) + bootstrap_test(build_mock_tor_provider()?) } #[test] #[cfg(feature = "mock-tor-provider")] fn test_mock_onion_service() -> anyhow::Result<()> { - let server_provider = Box::new(MockTorClient::new()); - let client_provider = Box::new(MockTorClient::new()); + let server_provider = build_mock_tor_provider()?; + let client_provider = build_mock_tor_provider()?; basic_onion_service_test(server_provider, client_provider) } #[test] #[cfg(feature = "mock-tor-provider")] fn test_mock_authenticated_onion_service() -> anyhow::Result<()> { - let server_provider = Box::new(MockTorClient::new()); - let client_provider = Box::new(MockTorClient::new()); + let server_provider = build_mock_tor_provider()?; + let client_provider = build_mock_tor_provider()?; authenticated_onion_service_test(server_provider, client_provider) } @@ -358,90 +516,30 @@ fn test_mock_authenticated_onion_service() -> anyhow::Result<()> { #[serial] #[cfg(feature = "legacy-tor-provider")] fn test_legacy_bootstrap() -> anyhow::Result<()> { - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - let mut data_path = std::env::temp_dir(); - data_path.push("test_legacy_bootstrap"); - - let tor_config = LegacyTorClientConfig::BundledTor { - tor_bin_path: tor_path, - data_directory: data_path, - proxy_settings: None, - allowed_ports: None, - pluggable_transports: None, - bridge_lines: None, - }; - - bootstrap_test(Box::new(LegacyTorClient::new(tor_config)?)) + let tor_provider = build_bundled_legacy_tor_provider("test_legacy_bootstrap")?; + bootstrap_test(tor_provider) } #[test] #[serial] #[cfg(feature = "legacy-tor-provider")] fn test_legacy_pluggable_transport_bootstrap() -> anyhow::Result<()> { - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - let mut data_path = std::env::temp_dir(); - data_path.push("test_legacy_pluggable_transport_bootstrap"); + let tor_provider = build_bundled_pt_legacy_tor_provider("test_legacy_pluggable_transport_bootstrap")?; - // find the lyrebird bin - let teb_path = std::env::var("TEB_PATH")?; - if teb_path.is_empty() { - println!("TEB_PATH environment variable empty, so skipping test_legacy_pluggable_transport_bootstrap()"); - return Ok(()); + if let Some(tor_provider) = tor_provider { + bootstrap_test(tor_provider)? } - let mut lyrebird_path = std::path::PathBuf::from(&teb_path); - let lyrebird_bin = format!("lyrebird{}", std::env::consts::EXE_SUFFIX); - lyrebird_path.push(lyrebird_bin.clone()); - assert!(std::path::Path::exists(&lyrebird_path)); - assert!(std::path::Path::is_file(&lyrebird_path)); - - // configure lyrebird pluggable transport - let pluggable_transport = - PluggableTransportConfig::new(vec!["obfs4".to_string()], lyrebird_path)?; - - // obfs4 bridgeline - let bridge_line = BridgeLine::from_str("obfs4 207.172.185.193:22223 F34AC0CDBC06918E54292A474578C99834A58893 cert=MjqosoyVylLQuLo4LH+eQ5hS7Z44s2CaMfQbIjJtn4bGRnvLv8ldSvSED5JpvWSxm09XXg iat-mode=0")?; - - let tor_config = LegacyTorClientConfig::BundledTor { - tor_bin_path: tor_path, - data_directory: data_path, - proxy_settings: None, - allowed_ports: None, - pluggable_transports: Some(vec![pluggable_transport]), - bridge_lines: Some(vec![bridge_line]), - }; - - bootstrap_test(Box::new(LegacyTorClient::new(tor_config)?)) + Ok(()) } #[test] #[serial] #[cfg(feature = "legacy-tor-provider")] fn test_legacy_onion_service() -> anyhow::Result<()> { - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - - let mut data_path = std::env::temp_dir(); - data_path.push("test_legacy_onion_service_server"); - let tor_config = LegacyTorClientConfig::BundledTor { - tor_bin_path: tor_path.clone(), - data_directory: data_path, - proxy_settings: None, - allowed_ports: None, - pluggable_transports: None, - bridge_lines: None, - }; - let server_provider = Box::new(LegacyTorClient::new(tor_config)?); - - let mut data_path = std::env::temp_dir(); - data_path.push("test_legacy_onion_service_cient"); - let tor_config = LegacyTorClientConfig::BundledTor { - tor_bin_path: tor_path, - data_directory: data_path, - proxy_settings: None, - allowed_ports: None, - pluggable_transports: None, - bridge_lines: None, - }; - let client_provider = Box::new(LegacyTorClient::new(tor_config)?); + let server_provider = build_bundled_legacy_tor_provider( + "test_legacy_onion_service_server")?; + let client_provider = build_bundled_legacy_tor_provider( + "test_legacy_onion_service_client")?; basic_onion_service_test(server_provider, client_provider) } @@ -450,31 +548,8 @@ fn test_legacy_onion_service() -> anyhow::Result<()> { #[serial] #[cfg(feature = "legacy-tor-provider")] fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - - let mut data_path = std::env::temp_dir(); - data_path.push("test_legacy_authenticated_onion_service_server"); - let tor_config = LegacyTorClientConfig::BundledTor { - tor_bin_path: tor_path.clone(), - data_directory: data_path, - proxy_settings: None, - allowed_ports: None, - pluggable_transports: None, - bridge_lines: None, - }; - let server_provider = Box::new(LegacyTorClient::new(tor_config)?); - - let mut data_path = std::env::temp_dir(); - data_path.push("test_legacy_authenticated_onion_service_cient"); - let tor_config = LegacyTorClientConfig::BundledTor { - tor_bin_path: tor_path, - data_directory: data_path, - proxy_settings: None, - allowed_ports: None, - pluggable_transports: None, - bridge_lines: None, - }; - let client_provider = Box::new(LegacyTorClient::new(tor_config)?); + let server_provider = build_bundled_legacy_tor_provider("test_legacy_authenticated_onion_service_server")?; + let client_provider = build_bundled_legacy_tor_provider("test_legacy_authenticated_onion_service_client")?; authenticated_onion_service_test(server_provider, client_provider) } @@ -483,101 +558,22 @@ fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { // System Legacy TorProvider tests // -#[cfg(test)] -#[cfg(feature = "legacy-tor-provider")] -fn start_system_tor_daemon( - tor_path: &std::ffi::OsStr, - name: &str, - control_port: u16, - socks_port: u16, -) -> anyhow::Result { - let mut data_path = std::env::temp_dir(); - data_path.push(name); - std::fs::create_dir_all(&data_path)?; - let default_torrc = data_path.join("default_torrc"); - { - let _ = File::create(&default_torrc)?; - } - let torrc = data_path.join("torrc"); - { - let _ = File::create(&torrc)?; - } - - let tor_daemon = Command::new(tor_path) - .stdout(Stdio::null()) - .stdin(Stdio::null()) - .stderr(Stdio::null()) - // point to our above written torrc file - .arg("--defaults-torrc") - .arg(default_torrc) - // location of torrc - .arg("--torrc-file") - .arg(torrc) - // enable networking - .arg("DisableNetwork") - .arg("0") - // root data directory - .arg("DataDirectory") - .arg(data_path) - // daemon will assign us a port, and we will - // read it from the control port file - .arg("ControlPort") - .arg(control_port.to_string()) - // password: foobar1 - .arg("HashedControlPassword") - .arg("16:E807DCE69AFE9979600760C9758B95ADB2F95E8740478AEA5356C95358") - // socks port - .arg("SocksPort") - .arg(socks_port.to_string()) - // tor process will shut down after this process shuts down - // to avoid orphaned tor daemon - .arg("__OwningControllerProcess") - .arg(process::id().to_string()) - .spawn()?; - - Ok(tor_daemon) -} #[test] #[serial] #[cfg(feature = "legacy-tor-provider")] fn test_system_legacy_onion_service() -> anyhow::Result<()> { - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - - let mut server_tor_daemon = start_system_tor_daemon( - tor_path.as_os_str(), + let server_provider = build_system_legacy_tor_provider( "test_system_legacy_onion_service_server", 9251u16, - 9250u16, - )?; - let mut client_tor_daemon = start_system_tor_daemon( - tor_path.as_os_str(), + 9250u16)?; + + let client_provider = build_system_legacy_tor_provider( "test_system_legacy_onion_service_client", 9351u16, - 9350u16, - )?; - - // give daemons time to start - std::thread::sleep(std::time::Duration::from_secs(5)); - - let tor_config = LegacyTorClientConfig::SystemTor { - tor_socks_addr: std::net::SocketAddr::from_str("127.0.0.1:9250")?, - tor_control_addr: std::net::SocketAddr::from_str("127.0.0.1:9251")?, - tor_control_passwd: "password".to_string(), - }; - let server_provider = Box::new(LegacyTorClient::new(tor_config)?); - - let tor_config = LegacyTorClientConfig::SystemTor { - tor_socks_addr: std::net::SocketAddr::from_str("127.0.0.1:9350")?, - tor_control_addr: std::net::SocketAddr::from_str("127.0.0.1:9351")?, - tor_control_passwd: "password".to_string(), - }; - let client_provider = Box::new(LegacyTorClient::new(tor_config)?); + 9350u16)?; - basic_onion_service_test(server_provider, client_provider)?; - - server_tor_daemon.kill()?; - client_tor_daemon.kill()?; + basic_onion_service_test(server_provider.0, client_provider.0)?; Ok(()) } @@ -586,42 +582,17 @@ fn test_system_legacy_onion_service() -> anyhow::Result<()> { #[serial] #[cfg(feature = "legacy-tor-provider")] fn test_system_legacy_authenticated_onion_service() -> anyhow::Result<()> { - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - - let mut server_tor_daemon = start_system_tor_daemon( - tor_path.as_os_str(), + let server_provider = build_system_legacy_tor_provider( "test_system_legacy_authenticated_onion_service_server", 9251u16, - 9250u16, - )?; - let mut client_tor_daemon = start_system_tor_daemon( - tor_path.as_os_str(), + 9250u16)?; + + let client_provider = build_system_legacy_tor_provider( "test_system_legacy_authenticated_onion_service_client", 9351u16, - 9350u16, - )?; + 9350u16)?; - // give daemons time to start - std::thread::sleep(std::time::Duration::from_secs(5)); - - let tor_config = LegacyTorClientConfig::SystemTor { - tor_socks_addr: std::net::SocketAddr::from_str("127.0.0.1:9250")?, - tor_control_addr: std::net::SocketAddr::from_str("127.0.0.1:9251")?, - tor_control_passwd: "password".to_string(), - }; - let server_provider = Box::new(LegacyTorClient::new(tor_config)?); - - let tor_config = LegacyTorClientConfig::SystemTor { - tor_socks_addr: std::net::SocketAddr::from_str("127.0.0.1:9350")?, - tor_control_addr: std::net::SocketAddr::from_str("127.0.0.1:9351")?, - tor_control_passwd: "password".to_string(), - }; - let client_provider = Box::new(LegacyTorClient::new(tor_config)?); - - authenticated_onion_service_test(server_provider, client_provider)?; - - server_tor_daemon.kill()?; - client_tor_daemon.kill()?; + authenticated_onion_service_test(server_provider.0, client_provider.0)?; Ok(()) } @@ -635,10 +606,8 @@ fn test_system_legacy_authenticated_onion_service() -> anyhow::Result<()> { #[cfg(feature = "arti-client-tor-provider")] fn test_arti_client_bootstrap() -> anyhow::Result<()> { let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); - let mut data_path = std::env::temp_dir(); - data_path.push("test_arti_bootstrap"); - let tor_provider = Box::new(ArtiClientTorClient::new(runtime, &data_path).unwrap()); + let tor_provider = build_arti_client_tor_provider(runtime, "test_arti_client_bootstrap")?; bootstrap_test(tor_provider) } @@ -646,13 +615,9 @@ fn test_arti_client_bootstrap() -> anyhow::Result<()> { #[cfg(feature = "arti-client-tor-provider")] fn test_arti_client_onion_service() -> anyhow::Result<()> { let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); - let mut data_path = std::env::temp_dir(); - data_path.push("test_arti_basic_onion_service_server"); - let server_provider = Box::new(ArtiClientTorClient::new(runtime.clone(), &data_path).unwrap()); - let mut data_path = std::env::temp_dir(); - data_path.push("test_arti_basic_onion_service_client"); - let client_provider = Box::new(ArtiClientTorClient::new(runtime.clone(), &data_path).unwrap()); + let server_provider = build_arti_client_tor_provider(runtime.clone(), "test_arti_basic_onion_service_server")?; + let client_provider = build_arti_client_tor_provider(runtime.clone(), "test_arti_basic_onion_service_client")?; basic_onion_service_test(server_provider, client_provider) } @@ -663,13 +628,8 @@ fn test_arti_client_onion_service() -> anyhow::Result<()> { fn test_arti_authenticated_onion_service() -> anyhow::Result<()> { let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); - let mut data_path = std::env::temp_dir(); - data_path.push("test_arti_authenticated_onion_service_server"); - let server_provider = Box::new(ArtiClientTorClient::new(runtime.clone(), &data_path).unwrap()); - - let mut data_path = std::env::temp_dir(); - data_path.push("test_arti_authenticated_onion_service_client"); - let client_provider = Box::new(ArtiClientTorClient::new(runtime.clone(), &data_path).unwrap()); + let server_provider = build_arti_client_tor_provider(runtime.clone(), "test_arti_authenticated_onion_service_server")?; + let client_provider = build_arti_client_tor_provider(runtime.clone(), "test_arti_authenticated_onion_service_client")?; authenticated_onion_service_test(server_provider, client_provider) } @@ -684,22 +644,8 @@ fn test_arti_authenticated_onion_service() -> anyhow::Result<()> { fn test_mixed_arti_client_legacy_onion_service() -> anyhow::Result<()> { let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); - let mut data_path = std::env::temp_dir(); - data_path.push("test_arti_legacy_basic_onion_service_server"); - let server_provider = Box::new(ArtiClientTorClient::new(runtime, &data_path)?); - - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - let mut data_path = std::env::temp_dir(); - data_path.push("test_arti_legacy_basic_onion_service_client"); - let tor_config = LegacyTorClientConfig::BundledTor { - tor_bin_path: tor_path, - data_directory: data_path, - proxy_settings: None, - allowed_ports: None, - pluggable_transports: None, - bridge_lines: None, - }; - let client_provider = Box::new(LegacyTorClient::new(tor_config)?); + let server_provider = build_arti_client_tor_provider(runtime, "test_mixed_arti_client_legacy_onion_service_server")?; + let client_provider = build_bundled_legacy_tor_provider("test_mixed_arti_client_legacy_onion_service_client")?; basic_onion_service_test(server_provider, client_provider) } @@ -708,25 +654,10 @@ fn test_mixed_arti_client_legacy_onion_service() -> anyhow::Result<()> { #[serial] #[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] fn test_mixed_legacy_arti_client_onion_service() -> anyhow::Result<()> { - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - - let mut data_path = std::env::temp_dir(); - data_path.push("test_legacy_arty_basic_onion_service_client"); - let tor_config = LegacyTorClientConfig::BundledTor { - tor_bin_path: tor_path, - data_directory: data_path, - proxy_settings: None, - allowed_ports: None, - pluggable_transports: None, - bridge_lines: None, - }; - let server_provider = Box::new(LegacyTorClient::new(tor_config)?); - let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); - let mut data_path = std::env::temp_dir(); - data_path.push("test_legacy_arti_basic_onion_service_server"); - let client_provider = Box::new(ArtiClientTorClient::new(runtime, &data_path)?); + let server_provider = build_bundled_legacy_tor_provider("test_mixed_legacy_arti_client_onion_service_server")?; + let client_provider = build_arti_client_tor_provider(runtime, "test_mixed_legacy_arti_client_onion_service_client")?; basic_onion_service_test(server_provider, client_provider) } @@ -737,22 +668,8 @@ fn test_mixed_legacy_arti_client_onion_service() -> anyhow::Result<()> { fn test_mixed_arti_client_legacy_authenticated_onion_service() -> anyhow::Result<()> { let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); - let mut data_path = std::env::temp_dir(); - data_path.push("test_mixed_arti_client_legacy_authenticated_onion_service_server"); - let server_provider = Box::new(ArtiClientTorClient::new(runtime, &data_path)?); - - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - let mut data_path = std::env::temp_dir(); - data_path.push("test_mixed_arti_client_legacy_authenticated_onion_service_client"); - let tor_config = LegacyTorClientConfig::BundledTor { - tor_bin_path: tor_path, - data_directory: data_path, - proxy_settings: None, - allowed_ports: None, - pluggable_transports: None, - bridge_lines: None, - }; - let client_provider = Box::new(LegacyTorClient::new(tor_config)?); + let server_provider = build_arti_client_tor_provider(runtime, "test_mixed_arti_client_legacy_authenticated_onion_service_server")?; + let client_provider = build_bundled_legacy_tor_provider("test_mixed_arti_client_legacy_authenticated_onion_service_client")?; authenticated_onion_service_test(server_provider, client_provider) } @@ -763,22 +680,8 @@ fn test_mixed_arti_client_legacy_authenticated_onion_service() -> anyhow::Result fn test_mixed_legacy_arti_client_authenticated_onion_service() -> anyhow::Result<()> { let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); - let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; - let mut data_path = std::env::temp_dir(); - data_path.push("test_mixed_legacy_arti_client_authenticated_onion_service_server"); - let tor_config = LegacyTorClientConfig::BundledTor { - tor_bin_path: tor_path, - data_directory: data_path, - proxy_settings: None, - allowed_ports: None, - pluggable_transports: None, - bridge_lines: None, - }; - let server_provider = Box::new(LegacyTorClient::new(tor_config)?); - - let mut data_path = std::env::temp_dir(); - data_path.push("test_mixed_legacy_arti_client_authenticated_onion_service_client"); - let client_provider = Box::new(ArtiClientTorClient::new(runtime, &data_path)?); + let server_provider = build_arti_client_tor_provider(runtime, "test_mixed_legacy_arti_client_authenticated_onion_service_server")?; + let client_provider = build_bundled_legacy_tor_provider("test_mixed_legacy_arti_client_authenticated_onion_service_client")?; authenticated_onion_service_test(server_provider, client_provider) } From 01f00966dc236761e8e0fa4b0435456869fffa58 Mon Sep 17 00:00:00 2001 From: Morgan Date: Sun, 27 Oct 2024 21:41:45 +0000 Subject: [PATCH 138/184] tor-interface: add some connect retries to the client-authentication test --- tor-interface/tests/tor_provider.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 3eb1fe322..6819395dc 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -455,8 +455,19 @@ pub(crate) fn authenticated_onion_service_test( client_provider.add_client_auth(&service_id, &private_auth_key)?; println!("Connecting to onion service with authentication"); - let mut client = - client_provider.connect((service_id.clone(), VIRT_PORT).into(), None)?; + let mut attempt_count = 0; + let mut client = loop { + match client_provider.connect((service_id.clone(), VIRT_PORT).into(), None) { + Ok(client) => break client, + Err(err) => { + println!("connect error: {:?}", err); + attempt_count += 1; + if attempt_count == 3 { + panic!("failed to connect :("); + } + } + } + }; println!("Client writing message: '{}'", MESSAGE); client.write_all(MESSAGE.as_bytes())?; From 331bb6125d944e9b03f5b805223f5534fc42f80c Mon Sep 17 00:00:00 2001 From: Morgan Date: Sat, 2 Nov 2024 18:06:13 +0000 Subject: [PATCH 139/184] tor-interface: update arti-client and friends to 0.24.0 --- tor-interface/Cargo.toml | 16 ++++++++-------- tor-interface/src/arti_client_tor_client.rs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 9e3f460d6..683d522da 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} +arti-client = { version = "0.24.0", features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" @@ -27,13 +27,13 @@ static_assertions = "1.1" thiserror = "1.0" tokio = { version = "1", features = ["macros"], optional = true } tokio-stream = { version = "0", optional = true } -tor-cell = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", optional = true } -tor-config = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", optional = true } -tor-hsservice = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", optional = true, features = ["restricted-discovery"] } -tor-keymgr = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", optional = true, features = ["keymgr"] } -tor-llcrypto = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", features = ["relay"] } -tor-proto = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", features = ["stream-ctrl"], optional = true } -tor-rtcompat = { git = "https://gitlab.torproject.org/tpo/core/arti.git", rev = "0956b386580d5d75983a8faff4e3043654936698", optional = true } +tor-cell = { version = "0.24.0", optional = true } +tor-config = { version = "0.24.0", optional = true } +tor-hsservice = { version = "0.24.0", optional = true, features = ["restricted-discovery"] } +tor-keymgr = { version = "0.24.0", optional = true, features = ["keymgr"] } +tor-llcrypto = { version = "0.24.0", features = ["relay"] } +tor-proto = { version = "0.24.0", features = ["stream-ctrl"], optional = true } +tor-rtcompat = { version = "0.24.0", optional = true } [dev-dependencies] anyhow = "1.0" diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 7afd6c9de..8bd6a1932 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -19,7 +19,7 @@ use tor_llcrypto::pk::ed25519::ExpandedKeypair; use tor_hsservice::config::OnionServiceConfigBuilder; use tor_hsservice::config::restricted_discovery::HsClientNickname; use tor_hsservice::{HsNickname, RunningOnionService}; -use tor_keymgr::{config::arti::ArtiKeystoreKind, KeystoreSelector}; +use tor_keymgr::{config::ArtiKeystoreKind, KeystoreSelector}; use tor_proto::stream::IncomingStreamRequest; use tor_rtcompat::PreferredRuntime; From 80569a4b49f284dbc3f3e02b7c0cc63f5e75341d Mon Sep 17 00:00:00 2001 From: Morgan Date: Sat, 16 Nov 2024 21:53:17 +0000 Subject: [PATCH 140/184] tor-interface, gosling, cgosling: version updates - updated tor-interface to 0.5.0 - updated gosling, cgsoling to 0.4.0 --- tor-interface/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 683d522da..fdb23873e 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tor-interface" authors = ["morgan ", "Richard Pospesel "] -version = "0.4.0" +version = "0.5.0" rust-version = "1.70" edition = "2021" license = "BSD-3-Clause" From 18bdb87086b028ee51c928574983431700519dbf Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 28 Nov 2024 22:53:08 +0000 Subject: [PATCH 141/184] tor-interface: rename test_arti_authenticated_onion_service to test_arti_client_authenticated_onion_service --- tor-interface/CMakeLists.txt | 2 +- tor-interface/tests/tor_provider.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 8460e0b79..df8ab1387 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -108,7 +108,7 @@ if (ENABLE_TESTS) WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) add_test(NAME tor_interface_arti_authenticated_onion_service_cargo_test - COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_arti_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_arti_client_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) endif() diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 6819395dc..94cf5443c 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -636,7 +636,7 @@ fn test_arti_client_onion_service() -> anyhow::Result<()> { #[test] #[serial] #[cfg(feature = "arti-client-tor-provider")] -fn test_arti_authenticated_onion_service() -> anyhow::Result<()> { +fn test_arti_client_authenticated_onion_service() -> anyhow::Result<()> { let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); let server_provider = build_arti_client_tor_provider(runtime.clone(), "test_arti_authenticated_onion_service_server")?; From 1c3eb23cf7c0318df4e86493e65846115089e4d7 Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 29 Nov 2024 03:36:12 +0000 Subject: [PATCH 142/184] tor-interface: implement Debug trait for OnionStream --- tor-interface/src/tor_provider.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 7b2bd3629..ae26a4bf2 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -278,6 +278,7 @@ pub type CircuitToken = usize; /// A wrapper around a [`std::net::TcpStream`] with some Tor-specific customisations /// /// An onion-listener can be constructed using the [`TorProvider::connect()`] method. +#[derive(Debug)] pub struct OnionStream { pub(crate) stream: TcpStream, pub(crate) local_addr: Option, From e289c4a293f8be0af70d689c461603bab1f363f0 Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 29 Nov 2024 03:22:03 +0000 Subject: [PATCH 143/184] tor-interface: add outgoing connection attempts to each supported TargetAddr variant in bootstrap_test() --- tor-interface/tests/tor_provider.rs | 34 ++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 94cf5443c..413bb6a33 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -186,7 +186,7 @@ fn build_arti_client_tor_provider(runtime: Arc, name: &str) -> // #[allow(dead_code)] -pub(crate) fn bootstrap_test(mut tor: Box) -> anyhow::Result<()> { +pub(crate) fn bootstrap_test(mut tor: Box, skip_connect_tests: bool) -> anyhow::Result<()> { tor.bootstrap()?; let mut received_log = false; @@ -219,6 +219,30 @@ pub(crate) fn bootstrap_test(mut tor: Box) -> anyhow::Result<() "should have received a log line from tor provider" ); + // + // Attempt to connect to various endpoints + // + + if !skip_connect_tests { + + // example.com + let stream = tor.connect(TargetAddr::from_str("www.example.com:80")?, None)?; + println!("stream: {stream:?}"); + + // google dns (ipv4) + let stream = tor.connect(TargetAddr::from_str("8.8.8.8:53")?, None)?; + println!("stream: {stream:?}"); + + // google dns (ipv6) + let stream = tor.connect(TargetAddr::from_str("[2001:4860:4860::8888]:53")?, None)?; + println!("stream: {stream:?}"); + + // riseup onion service + let stream = tor.connect(TargetAddr::from_str("vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion:80")?, None)?; + println!("stream: {stream:?}"); + + } + Ok(()) } @@ -500,7 +524,7 @@ pub(crate) fn authenticated_onion_service_test( #[test] #[cfg(feature = "mock-tor-provider")] fn test_mock_bootstrap() -> anyhow::Result<()> { - bootstrap_test(build_mock_tor_provider()?) + bootstrap_test(build_mock_tor_provider()?, true) } #[test] @@ -528,7 +552,7 @@ fn test_mock_authenticated_onion_service() -> anyhow::Result<()> { #[cfg(feature = "legacy-tor-provider")] fn test_legacy_bootstrap() -> anyhow::Result<()> { let tor_provider = build_bundled_legacy_tor_provider("test_legacy_bootstrap")?; - bootstrap_test(tor_provider) + bootstrap_test(tor_provider, false) } #[test] @@ -538,7 +562,7 @@ fn test_legacy_pluggable_transport_bootstrap() -> anyhow::Result<()> { let tor_provider = build_bundled_pt_legacy_tor_provider("test_legacy_pluggable_transport_bootstrap")?; if let Some(tor_provider) = tor_provider { - bootstrap_test(tor_provider)? + bootstrap_test(tor_provider, false)? } Ok(()) } @@ -619,7 +643,7 @@ fn test_arti_client_bootstrap() -> anyhow::Result<()> { let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); let tor_provider = build_arti_client_tor_provider(runtime, "test_arti_client_bootstrap")?; - bootstrap_test(tor_provider) + bootstrap_test(tor_provider, false) } #[test] From 6465dc58950b4954150fa15ee16422d03fef4705 Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 29 Nov 2024 04:09:09 +0000 Subject: [PATCH 144/184] tor-interface: migrated TargetAddr tests to seperate tor_utils.rs file --- tor-interface/tests/tor_provider.rs | 192 +-------------------------- tor-interface/tests/tor_utils.rs | 196 ++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 191 deletions(-) create mode 100644 tor-interface/tests/tor_utils.rs diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 413bb6a33..162f7a383 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -2,6 +2,7 @@ #[cfg(feature = "legacy-tor-provider")] use std::fs::File; use std::io::{Read, Write}; +#[cfg(feature = "legacy-tor-provider")] use std::ops::Drop; #[cfg(feature = "legacy-tor-provider")] use std::process; @@ -720,194 +721,3 @@ fn test_mixed_legacy_arti_client_authenticated_onion_service() -> anyhow::Result authenticated_onion_service_test(server_provider, client_provider) } - -// -// Misc Utils -// - -#[test] -fn test_tor_provider_target_addr() -> anyhow::Result<()> { - let valid_ip_addr: &[&str] = &[ - "192.168.1.1:80", - "10.0.0.1:443", - "172.16.0.1:8080", - "8.8.8.8:53", - "255.255.255.255:65535", - "0.0.0.0:22", - "192.168.0.254:21", - "127.0.0.1:3306", - "1.1.1.1:123", - "224.0.0.1:554", - "169.254.0.1:179", - "203.0.113.1:80", - "198.51.100.1:443", - "100.64.0.1:8080", - "192.0.2.1:53", - "192.88.99.1:22", - "192.0.0.1:21", - "240.0.0.1:3306", - "198.18.0.1:123", - "233.252.0.1:554", - "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80", - "[2001:db8:85a3::8a2e:370:7334]:443", - "[::1]:8080", - "[::ffff:192.168.1.1]:53", - "[2001:0db8::1]:22", - "[fe80::1ff:fe23:4567:890a]:21", - "[2001:db8::1:0:0:1]:3306", - "[2001:0db8:0000:0042:0000:8a2e:0370:7334]:123", - "[ff02::1]:554", - "[fe80::abcd:ef01:2345:6789]:179", - "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80", - "[2001:db8:85a3::8a2e:370:7334]:443", - "[::1]:8080", - "[::ffff:c0a8:101]:53", - "[2001:db8::1:0:0:1]:22", - "[fe80::1ff:fe23:4567:890a]:21", - "[2001:db8:0000:0042:0000:8a2e:0370:7334]:3306", - "[ff02::1]:123", - "[fe80::abcd:ef01:2345:6789]:554", - "[2001:db8::1]:179", - ]; - - for target_addr_str in valid_ip_addr { - match TargetAddr::from_str(target_addr_str) { - Ok(TargetAddr::Socket(socket_addr)) => println!("{} => {}", target_addr_str, socket_addr), - Ok(TargetAddr::OnionService(onion_addr)) => panic!( - "unexpected conversion: {} => OnionService({})", - target_addr_str, onion_addr - ), - Ok(TargetAddr::Domain(domain_addr)) => panic!( - "unexpected conversion: {} => DomainAddr({})", - target_addr_str, domain_addr - ), - Err(err) => Err(err)?, - } - } - - let valid_onion_addr: &[&str] = &[ - "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:65535", - "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCZSJYD.onion:1", - ]; - - for target_addr_str in valid_onion_addr { - match TargetAddr::from_str(target_addr_str) { - Ok(TargetAddr::Socket(socket_addr)) => panic!( - "unexpected conversion: {} => Ip({})", - target_addr_str, socket_addr - ), - Ok(TargetAddr::OnionService(onion_addr)) => { - println!("{} => {}", target_addr_str, onion_addr) - } - Ok(TargetAddr::Domain(domain_addr)) => panic!( - "unexpected conversion: {} => DomainAddr({})", - target_addr_str, domain_addr - ), - Err(err) => Err(err)?, - } - } - - let valid_domain_addr: &[&str] = &[ - "example.com:80", - "subdomain.example.com:443", - "xn--e1afmkfd.xn--p1ai:8080", // domain in Punycode for "пример.рф" - "xn--fsqu00a.xn--0zwm56d:53", // domain in Punycode for "例子.测试" - "münich.com:22", // domain with UTF-8 characters - "xn--mnich-kva.com:21", // Punycode for "münich.com" - "exämple.com:3306", // domain with UTF-8 characters - "xn--exmple-cua.com:123", // Punycode for "exämple.com" - "例子.com:554", // domain with UTF-8 characters - "xn--fsqu00a.com:179", // Punycode for "例子.com" - "täst.de:80", // domain with UTF-8 characters - "xn--tst-qla.de:443", // Punycode for "täst.de" - "xn--fiqs8s:80", // Punycode for "中国" - "xn--wgbh1c:8080", // Punycode for "مصر" - "münster.de:22", // domain with UTF-8 characters - "xn--mnster-3ya.de:21", // Punycode for "münster.de" - "bücher.com:3306", // domain with UTF-8 characters - "xn--bcher-kva.com:123", // Punycode for "bücher.com" - "xn--vermgensberatung-pwb.com:554", // Punycode for "vermögensberatung.com" - // Max Length - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:65535" - ]; - - for target_addr_str in valid_domain_addr { - match TargetAddr::from_str(target_addr_str) { - Ok(TargetAddr::Socket(socket_addr)) => panic!( - "unexpected conversion: {} => SocketAddr({})", - target_addr_str, socket_addr - ), - Ok(TargetAddr::OnionService(onion_addr)) => panic!( - "unexpected conversion: {} => OnionService({})", - target_addr_str, onion_addr - ), - Ok(TargetAddr::Domain(domain_addr)) => { - println!("{} => {}", target_addr_str, domain_addr) - } - Err(err) => Err(err)?, - } - } - - let invalid_target_addr: &[&str] = &[ - // ipv4-ish - "192.168.1.1:99999", // Port number out of range - "192.168.1.1:abc", // Invalid port number - "192.168.1.1:", // Missing port number - "192.168.1.1: 80", // Space in port number - "192.168.1.1:80a", // Non-numeric characters in port number - // ipv6-ish - "[2001:db8:::1]:80", // Triple colons - "[2001:db8:85a3::8a2e:370:7334:1234::abcd]:80", // Too many groups - "[2001:db8:85a3::8a2e:370g:7334]:80", // Invalid character in group - "[2001:db8:85a3::8a2e:370:7334]:99999", // Port number out of range - "[2001:db8:85a3:8a2e:370:7334]:80", // Missing double colons - "[::12345]:80", // Excessive leading zeroes - "[2001:db8:85a3::8a2e:370:7334:]:80", // Trailing colon - "[2001:db8:85a3::8a2e:370:7334]", // Missing port number - "2001:db8:85a3::8a2e:370:7334:80", // Missing square brackets - "[2001:db8:85a3::8a2e:370:7334]: 80", // Space in port number - "[2001:db8:85a3::8a2e:370:7334]:80a", // Non-numeric characters in port number - // onion service-ish - "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd234567.onion:80", // Too long for v3 - "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxcz.onion:443", // Too short for v3 - "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:99999", // Port number out of range - "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrst.onion:21", // Invalid characters - "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:abc", // Invalid port number - "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion: 80", // Space in port number - "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:80a", // Non-numeric characters in port number - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion:80", // Invalid service id - // domain-ish - "example..com:80", // Double dots - "exa mple.com:53", // Space in domain - "example.com:99999", // Port number out of range - "exaample.com:abc", // Invalid port number - "exaample.com:", // Missing port number - "exaample.com: 80", // Space in port number - "ex@mple.com:80", // Special character in domain - "example.com:80a", // Non-numeric characters in port number - "exämple..com:80", // UTF-8 with double dot - "xn--exmple-cua.com: 80", // Punycode with space in port number - "xn--exmple-cua.com:80a", // Punycode with non-numeric port - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com:65535", // Label too long - ]; - - for target_addr_str in invalid_target_addr { - match TargetAddr::from_str(target_addr_str) { - Ok(TargetAddr::Socket(socket_addr)) => panic!( - "unexpected conversion: {} => SocketAddr({})", - target_addr_str, socket_addr - ), - Ok(TargetAddr::OnionService(onion_addr)) => panic!( - "unexpected conversion: {} => OnionService({})", - target_addr_str, onion_addr - ), - Ok(TargetAddr::Domain(domain_addr)) => panic!( - "unexpected conversion: {} => DomainAddr({})", - target_addr_str, domain_addr - ), - Err(_) => (), - } - } - - Ok(()) -} diff --git a/tor-interface/tests/tor_utils.rs b/tor-interface/tests/tor_utils.rs new file mode 100644 index 000000000..0cb9e6253 --- /dev/null +++ b/tor-interface/tests/tor_utils.rs @@ -0,0 +1,196 @@ +// std +use std::str::FromStr; + +// internal crates +use tor_interface::tor_provider::*; + +// +// Misc Utils +// + +#[test] +fn test_tor_provider_target_addr() -> anyhow::Result<()> { + let valid_ip_addr: &[&str] = &[ + "192.168.1.1:80", + "10.0.0.1:443", + "172.16.0.1:8080", + "8.8.8.8:53", + "255.255.255.255:65535", + "0.0.0.0:22", + "192.168.0.254:21", + "127.0.0.1:3306", + "1.1.1.1:123", + "224.0.0.1:554", + "169.254.0.1:179", + "203.0.113.1:80", + "198.51.100.1:443", + "100.64.0.1:8080", + "192.0.2.1:53", + "192.88.99.1:22", + "192.0.0.1:21", + "240.0.0.1:3306", + "198.18.0.1:123", + "233.252.0.1:554", + "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80", + "[2001:db8:85a3::8a2e:370:7334]:443", + "[::1]:8080", + "[::ffff:192.168.1.1]:53", + "[2001:0db8::1]:22", + "[fe80::1ff:fe23:4567:890a]:21", + "[2001:db8::1:0:0:1]:3306", + "[2001:0db8:0000:0042:0000:8a2e:0370:7334]:123", + "[ff02::1]:554", + "[fe80::abcd:ef01:2345:6789]:179", + "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80", + "[2001:db8:85a3::8a2e:370:7334]:443", + "[::1]:8080", + "[::ffff:c0a8:101]:53", + "[2001:db8::1:0:0:1]:22", + "[fe80::1ff:fe23:4567:890a]:21", + "[2001:db8:0000:0042:0000:8a2e:0370:7334]:3306", + "[ff02::1]:123", + "[fe80::abcd:ef01:2345:6789]:554", + "[2001:db8::1]:179", + ]; + + for target_addr_str in valid_ip_addr { + match TargetAddr::from_str(target_addr_str) { + Ok(TargetAddr::Socket(socket_addr)) => println!("{} => {}", target_addr_str, socket_addr), + Ok(TargetAddr::OnionService(onion_addr)) => panic!( + "unexpected conversion: {} => OnionService({})", + target_addr_str, onion_addr + ), + Ok(TargetAddr::Domain(domain_addr)) => panic!( + "unexpected conversion: {} => DomainAddr({})", + target_addr_str, domain_addr + ), + Err(err) => Err(err)?, + } + } + + let valid_onion_addr: &[&str] = &[ + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:65535", + "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCZSJYD.onion:1", + ]; + + for target_addr_str in valid_onion_addr { + match TargetAddr::from_str(target_addr_str) { + Ok(TargetAddr::Socket(socket_addr)) => panic!( + "unexpected conversion: {} => Ip({})", + target_addr_str, socket_addr + ), + Ok(TargetAddr::OnionService(onion_addr)) => { + println!("{} => {}", target_addr_str, onion_addr) + } + Ok(TargetAddr::Domain(domain_addr)) => panic!( + "unexpected conversion: {} => DomainAddr({})", + target_addr_str, domain_addr + ), + Err(err) => Err(err)?, + } + } + + let valid_domain_addr: &[&str] = &[ + "example.com:80", + "subdomain.example.com:443", + "xn--e1afmkfd.xn--p1ai:8080", // domain in Punycode for "пример.рф" + "xn--fsqu00a.xn--0zwm56d:53", // domain in Punycode for "例子.测试" + "münich.com:22", // domain with UTF-8 characters + "xn--mnich-kva.com:21", // Punycode for "münich.com" + "exämple.com:3306", // domain with UTF-8 characters + "xn--exmple-cua.com:123", // Punycode for "exämple.com" + "例子.com:554", // domain with UTF-8 characters + "xn--fsqu00a.com:179", // Punycode for "例子.com" + "täst.de:80", // domain with UTF-8 characters + "xn--tst-qla.de:443", // Punycode for "täst.de" + "xn--fiqs8s:80", // Punycode for "中国" + "xn--wgbh1c:8080", // Punycode for "مصر" + "münster.de:22", // domain with UTF-8 characters + "xn--mnster-3ya.de:21", // Punycode for "münster.de" + "bücher.com:3306", // domain with UTF-8 characters + "xn--bcher-kva.com:123", // Punycode for "bücher.com" + "xn--vermgensberatung-pwb.com:554", // Punycode for "vermögensberatung.com" + // Max Length + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:65535" + ]; + + for target_addr_str in valid_domain_addr { + match TargetAddr::from_str(target_addr_str) { + Ok(TargetAddr::Socket(socket_addr)) => panic!( + "unexpected conversion: {} => SocketAddr({})", + target_addr_str, socket_addr + ), + Ok(TargetAddr::OnionService(onion_addr)) => panic!( + "unexpected conversion: {} => OnionService({})", + target_addr_str, onion_addr + ), + Ok(TargetAddr::Domain(domain_addr)) => { + println!("{} => {}", target_addr_str, domain_addr) + } + Err(err) => Err(err)?, + } + } + + let invalid_target_addr: &[&str] = &[ + // ipv4-ish + "192.168.1.1:99999", // Port number out of range + "192.168.1.1:abc", // Invalid port number + "192.168.1.1:", // Missing port number + "192.168.1.1: 80", // Space in port number + "192.168.1.1:80a", // Non-numeric characters in port number + // ipv6-ish + "[2001:db8:::1]:80", // Triple colons + "[2001:db8:85a3::8a2e:370:7334:1234::abcd]:80", // Too many groups + "[2001:db8:85a3::8a2e:370g:7334]:80", // Invalid character in group + "[2001:db8:85a3::8a2e:370:7334]:99999", // Port number out of range + "[2001:db8:85a3:8a2e:370:7334]:80", // Missing double colons + "[::12345]:80", // Excessive leading zeroes + "[2001:db8:85a3::8a2e:370:7334:]:80", // Trailing colon + "[2001:db8:85a3::8a2e:370:7334]", // Missing port number + "2001:db8:85a3::8a2e:370:7334:80", // Missing square brackets + "[2001:db8:85a3::8a2e:370:7334]: 80", // Space in port number + "[2001:db8:85a3::8a2e:370:7334]:80a", // Non-numeric characters in port number + // onion service-ish + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd234567.onion:80", // Too long for v3 + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxcz.onion:443", // Too short for v3 + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:99999", // Port number out of range + "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrst.onion:21", // Invalid characters + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:abc", // Invalid port number + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion: 80", // Space in port number + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:80a", // Non-numeric characters in port number + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion:80", // Invalid service id + // domain-ish + "example..com:80", // Double dots + "exa mple.com:53", // Space in domain + "example.com:99999", // Port number out of range + "exaample.com:abc", // Invalid port number + "exaample.com:", // Missing port number + "exaample.com: 80", // Space in port number + "ex@mple.com:80", // Special character in domain + "example.com:80a", // Non-numeric characters in port number + "exämple..com:80", // UTF-8 with double dot + "xn--exmple-cua.com: 80", // Punycode with space in port number + "xn--exmple-cua.com:80a", // Punycode with non-numeric port + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com:65535", // Label too long + ]; + + for target_addr_str in invalid_target_addr { + match TargetAddr::from_str(target_addr_str) { + Ok(TargetAddr::Socket(socket_addr)) => panic!( + "unexpected conversion: {} => SocketAddr({})", + target_addr_str, socket_addr + ), + Ok(TargetAddr::OnionService(onion_addr)) => panic!( + "unexpected conversion: {} => OnionService({})", + target_addr_str, onion_addr + ), + Ok(TargetAddr::Domain(domain_addr)) => panic!( + "unexpected conversion: {} => DomainAddr({})", + target_addr_str, domain_addr + ), + Err(_) => (), + } + } + + Ok(()) +} From d37349d0f3ab12c9662b0f0548b2339d28b9659f Mon Sep 17 00:00:00 2001 From: Morgan Date: Sat, 19 Oct 2024 23:10:04 +0000 Subject: [PATCH 145/184] tor-interface: implement stub ArtiTorClient TorProvider --- tor-interface/CMakeLists.txt | 4 ++ tor-interface/Cargo.toml | 1 + tor-interface/src/arti_tor_client.rs | 85 ++++++++++++++++++++++++++++ tor-interface/src/lib.rs | 7 ++- 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 tor-interface/src/arti_tor_client.rs diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index df8ab1387..d92a7d397 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -1,6 +1,7 @@ set(tor_interface_sources Cargo.toml src/arti_client_tor_client.rs + src/arti_tor_client.rs src/censorship_circumvention.rs src/legacy_tor_client.rs src/legacy_tor_controller.rs @@ -31,6 +32,9 @@ endif() if (ENABLE_ARTI_CLIENT_TOR_PROVIDER) list(APPEND TOR_INTERFACE_FEATURE_LIST "arti-client-tor-provider") endif() +if (ENABLE_ARTI_TOR_PROVIDER) + list(APPEND TOR_INTERFACE_FEATURE_LIST "arti-tor-provider") +endif() list(JOIN TOR_INTERFACE_FEATURE_LIST "," TOR_INTERFACE_FEATURES) if (TOR_INTERFACE_FEATURES) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index fdb23873e..56b1d55dc 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -42,5 +42,6 @@ which = "4.4" [features] arti-client-tor-provider = ["arti-client", "tokio", "tokio-stream", "tor-cell", "tor-config", "tor-hsservice", "tor-keymgr", "tor-proto", "tor-rtcompat"] +arti-tor-provider = [] mock-tor-provider = [] legacy-tor-provider = [] diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs new file mode 100644 index 000000000..6a01a92bc --- /dev/null +++ b/tor-interface/src/arti_tor_client.rs @@ -0,0 +1,85 @@ +// internal crates +use crate::tor_crypto::*; +use crate::tor_provider; +use crate::tor_provider::*; + + +/// [`ArtiTorClient`]-specific error type +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("not implemented")] + NotImplemented(), +} + +impl From for crate::tor_provider::Error { + fn from(error: Error) -> Self { + crate::tor_provider::Error::Generic(error.to_string()) + } +} + +#[derive(Clone, Debug)] +pub enum ArtiTorClientConfig { + Bundled { + }, + System { + + }, +} + +pub struct ArtiTorClient { + +} + +impl ArtiTorClient { + pub fn new(config: ArtiTorClientConfig) -> Result { + Err(Error::NotImplemented().into()) + } +} + +impl TorProvider for ArtiTorClient { + fn update(&mut self) -> Result, tor_provider::Error> { + Err(Error::NotImplemented().into()) + } + + fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { + Err(Error::NotImplemented().into()) + } + + fn add_client_auth( + &mut self, + _service_id: &V3OnionServiceId, + _client_auth: &X25519PrivateKey, + ) -> Result<(), tor_provider::Error> { + Err(Error::NotImplemented().into()) + } + + fn remove_client_auth( + &mut self, + _service_id: &V3OnionServiceId, + ) -> Result<(), tor_provider::Error> { + Err(Error::NotImplemented().into()) + } + + fn connect( + &mut self, + _target: TargetAddr, + _circuit: Option, + ) -> Result { + Err(Error::NotImplemented().into()) + } + + fn listener( + &mut self, + _private_key: &Ed25519PrivateKey, + _virt_port: u16, + _authorized_clients: Option<&[X25519PublicKey]>, + ) -> Result { + Err(Error::NotImplemented().into()) + } + + fn generate_token(&mut self) -> CircuitToken { + 0usize + } + + fn release_token(&mut self, _token: CircuitToken) {} +} diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index dea3f8a6b..35c5ade99 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -3,8 +3,11 @@ /// Implementation of an in-process [`arti-client`](https://crates.io/crates/arti-client)-based `TorProvider` #[cfg(feature = "arti-client-tor-provider")] pub mod arti_client_tor_client; -#[cfg(feature = "legacy-tor-provider")] +/// Implementation of an out-of-process [`arti`](https://crates.io/crates/arti)-based `TorProvider` +#[cfg(feature = "arti-tor-provider")] +pub mod arti_tor_client; /// Censorship circumvention configuration for pluggable-transports and bridge settings +#[cfg(feature = "legacy-tor-provider")] pub mod censorship_circumvention; /// Implementation of an out-of-process legacy [c-tor daemon](https://gitlab.torproject.org/tpo/core/tor)-based `TorProvider` #[cfg(feature = "legacy-tor-provider")] @@ -21,8 +24,8 @@ pub mod legacy_tor_version; /// Implementation of a local, in-process, mock `TorProvider` for testing. #[cfg(feature = "mock-tor-provider")] pub mod mock_tor_client; -#[cfg(feature = "legacy-tor-provider")] /// Proxy settings +#[cfg(feature = "legacy-tor-provider")] pub mod proxy; /// Tor-specific cryptographic primitives, operations, and conversion functions. pub mod tor_crypto; From a506c0e1bf27f500c00a408ca0d7cd1b2d20b90a Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 28 Nov 2024 18:47:14 +0000 Subject: [PATCH 146/184] tor-interface: implement currently failing bootstrap test for ArtiTorClient TorProvider --- tor-interface/CMakeLists.txt | 7 ++++++ tor-interface/src/arti_tor_client.rs | 9 ++++++-- tor-interface/tests/tor_provider.rs | 32 +++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index d92a7d397..1f67a260f 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -117,6 +117,13 @@ if (ENABLE_TESTS) ) endif() + if (ENABLE_ARTI_TOR_PROVIDER) + add_test(NAME tor_interface_arti_bootstrap_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_arti_bootstrap ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() + if (ENABLE_LEGACY_TOR_PROVIDER AND ENABLE_ARTI_CLIENT_TOR_PROVIDER) add_test(NAME tor_interface_mixed_arti_client_legacy_bootstrap_cargo_test COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_arti_client_legacy_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs index 6a01a92bc..cb9e3b36a 100644 --- a/tor-interface/src/arti_tor_client.rs +++ b/tor-interface/src/arti_tor_client.rs @@ -1,3 +1,6 @@ +// std +use std::path::PathBuf; + // internal crates use crate::tor_crypto::*; use crate::tor_provider; @@ -19,9 +22,11 @@ impl From for crate::tor_provider::Error { #[derive(Clone, Debug)] pub enum ArtiTorClientConfig { - Bundled { + BundledArti { + arti_bin_path: PathBuf, + data_directory: PathBuf, }, - System { + SystemArti { }, } diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 162f7a383..adba014a5 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -20,6 +20,8 @@ use tokio::runtime; // internal crates #[cfg(feature = "arti-client-tor-provider")] use tor_interface::arti_client_tor_client::*; +#[cfg(feature = "arti-tor-provider")] +use tor_interface::arti_tor_client::*; #[cfg(feature = "legacy-tor-provider")] use tor_interface::censorship_circumvention::*; #[cfg(feature = "legacy-tor-provider")] @@ -182,6 +184,20 @@ fn build_arti_client_tor_provider(runtime: Arc, name: &str) -> Ok(Box::new(ArtiClientTorClient::new(runtime, &data_path)?)) } +#[cfg(test)] +#[cfg(feature = "arti-tor-provider")] +fn build_arti_tor_provider(name: &str) -> anyhow::Result> { + let arti_path = which::which(format!("arti{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push(name); + + let arti_config = ArtiTorClientConfig::BundledArti { + arti_bin_path: arti_path, + data_directory: data_path, + }; + + Ok(Box::new(ArtiTorClient::new(arti_config)?)) +} // // Test Functions // @@ -634,7 +650,7 @@ fn test_system_legacy_authenticated_onion_service() -> anyhow::Result<()> { } // -// Arti TorProvider tests +// Arti-Client TorProvider tests // #[test] @@ -670,6 +686,20 @@ fn test_arti_client_authenticated_onion_service() -> anyhow::Result<()> { authenticated_onion_service_test(server_provider, client_provider) } +// +// Arti TorProvider tests +// + +#[test] +#[serial] +#[cfg(feature = "arti-tor-provider")] +fn test_arti_bootstrap() -> anyhow::Result<()> { + let tor_provider = build_arti_tor_provider("test_arti_bootstrap")?; + bootstrap_test(tor_provider, false) +} + +// + // // Mixed Arti/Legacy TorProvider tests // From 246adc4304559286866eb84315ba862fc5abbce8 Mon Sep 17 00:00:00 2001 From: Morgan Date: Sun, 27 Oct 2024 17:54:55 +0000 Subject: [PATCH 147/184] tor-interface: implement arti_process for ArtiTorClient --- tor-interface/src/arti_process.rs | 113 ++++++++++++++++++++++++++++++ tor-interface/src/lib.rs | 2 + 2 files changed, 115 insertions(+) create mode 100644 tor-interface/src/arti_process.rs diff --git a/tor-interface/src/arti_process.rs b/tor-interface/src/arti_process.rs new file mode 100644 index 000000000..e9be68e01 --- /dev/null +++ b/tor-interface/src/arti_process.rs @@ -0,0 +1,113 @@ +// standard +use std::fs; +use std::fs::File; +use std::io::Write; +use std::ops::Drop; +use std::process; +use std::process::{Child, ChildStdout, Command, Stdio}; +use std::path::Path; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("not implemented")] + NotImplemented(), + + #[error("provided arti bin path '{0}' must be an absolute path")] + ArtiBinPathNotAbsolute(String), + + #[error("provided data directory '{0}' must be an absolute path")] + ArtiDataDirectoryPathNotAbsolute(String), + + #[error("failed to create data directory")] + ArtiDataDirectoryCreationFailed(#[source] std::io::Error), + + #[error("file exists in provided data directory path '{0}'")] + ArtiDataDirectoryPathExistsAsFile(String), + + #[error("failed to create arti.toml file")] + ArtiTomlFileCreationFailed(#[source] std::io::Error), + + #[error("failed to write arti.toml file")] + ArtiTomlFileWriteFailed(#[source] std::io::Error), + + #[error("failed to start arti process")] + ArtiProcessStartFailed(#[source] std::io::Error), +} + +pub(crate) struct ArtiProcess { + process: Child, +} + +impl ArtiProcess { + pub fn new(arti_bin_path: &Path, data_directory: &Path) -> Result { + // verify provided paths are absolute + if arti_bin_path.is_relative() { + return Err(Error::ArtiBinPathNotAbsolute(format!( + "{}", + arti_bin_path.display() + ))); + } + if data_directory.is_relative() { + return Err(Error::ArtiDataDirectoryPathNotAbsolute(format!( + "{}", + data_directory.display() + ))); + } + + // create data directory if it doesn't exist + if !data_directory.exists() { + fs::create_dir_all(data_directory).map_err(Error::ArtiDataDirectoryCreationFailed)?; + } else if data_directory.is_file() { + return Err(Error::ArtiDataDirectoryPathExistsAsFile(format!( + "{}", + data_directory.display() + ))); + } + + // construct paths to arti files file + let arti_toml = data_directory.join("arti.toml"); + + // write arti.toml settings file + if !arti_toml.exists() { + let cache_dir = data_directory.join("cache").display().to_string(); + let state_dir = data_directory.join("state").display().to_string(); + + let arti_toml_content = format!("\ + [storage]\n\ + cache_dir = \"{cache_dir}\"\n\ + state_dir = \"{state_dir}\"\n\ + [storage.keystore]\n\ + enabled = true\n\ + [storage.keystore.primary]\n\ + kind = \"ephemeral\"\n\ + "); + + let mut arti_toml_file = + File::create(&arti_toml).map_err(Error::ArtiTomlFileCreationFailed)?; + arti_toml_file + .write_all(arti_toml_content.as_bytes()) + .map_err(Error::ArtiTomlFileWriteFailed)?; + } + + + let mut process = Command::new(arti_bin_path.as_os_str()) + .stdout(Stdio::piped()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + // set working directory to data directory + .current_dir(data_directory) + // point to our above written arti.toml file + .arg("--config") + .arg(arti_toml) + .spawn() + .map_err(Error::ArtiProcessStartFailed)?; + + Ok(ArtiProcess { process }) + } +} + +impl Drop for ArtiProcess { + fn drop(&mut self) { + let _ = self.process.kill(); + } +} diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs index 35c5ade99..d334e5a79 100644 --- a/tor-interface/src/lib.rs +++ b/tor-interface/src/lib.rs @@ -6,6 +6,8 @@ pub mod arti_client_tor_client; /// Implementation of an out-of-process [`arti`](https://crates.io/crates/arti)-based `TorProvider` #[cfg(feature = "arti-tor-provider")] pub mod arti_tor_client; +#[cfg(feature = "arti-tor-provider")] +pub mod arti_process; /// Censorship circumvention configuration for pluggable-transports and bridge settings #[cfg(feature = "legacy-tor-provider")] pub mod censorship_circumvention; From 3a860f87827092cb6ef827c12e2971e018467b3a Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 28 Nov 2024 19:52:28 +0000 Subject: [PATCH 148/184] tor-interface: ArtiClient now creates an ArtiProcess from ArtiTorClientConfig::BundledArti --- tor-interface/CMakeLists.txt | 1 + tor-interface/src/arti_process.rs | 55 +++++++++++++++------------- tor-interface/src/arti_tor_client.rs | 27 ++++++++++++-- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index 1f67a260f..e7aad06b9 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -1,6 +1,7 @@ set(tor_interface_sources Cargo.toml src/arti_client_tor_client.rs + src/arti_process.rs src/arti_tor_client.rs src/censorship_circumvention.rs src/legacy_tor_client.rs diff --git a/tor-interface/src/arti_process.rs b/tor-interface/src/arti_process.rs index e9be68e01..974caf3e1 100644 --- a/tor-interface/src/arti_process.rs +++ b/tor-interface/src/arti_process.rs @@ -67,41 +67,46 @@ impl ArtiProcess { // construct paths to arti files file let arti_toml = data_directory.join("arti.toml"); - // write arti.toml settings file - if !arti_toml.exists() { - let cache_dir = data_directory.join("cache").display().to_string(); - let state_dir = data_directory.join("state").display().to_string(); - - let arti_toml_content = format!("\ - [storage]\n\ - cache_dir = \"{cache_dir}\"\n\ - state_dir = \"{state_dir}\"\n\ - [storage.keystore]\n\ - enabled = true\n\ - [storage.keystore.primary]\n\ - kind = \"ephemeral\"\n\ - "); - - let mut arti_toml_file = - File::create(&arti_toml).map_err(Error::ArtiTomlFileCreationFailed)?; - arti_toml_file - .write_all(arti_toml_content.as_bytes()) - .map_err(Error::ArtiTomlFileWriteFailed)?; - } - - - let mut process = Command::new(arti_bin_path.as_os_str()) - .stdout(Stdio::piped()) + // write arti.toml settings file (always overwrite) + let cache_dir = data_directory.join("cache").display().to_string(); + let state_dir = data_directory.join("state").display().to_string(); + let rpc_listen = data_directory.join("SOCKET").display().to_string(); + + let arti_toml_content = format!("\ + [storage]\n\ + cache_dir = \"{cache_dir}\"\n\ + state_dir = \"{state_dir}\"\n\ + [storage.keystore]\n\ + enabled = true\n\ + [storage.keystore.primary]\n\ + kind = \"ephemeral\"\n\ + [storage.permissions]\n\ + dangerously_trust_everyone = true\n\ + [rpc]\n + rpc_listen = \"{rpc_listen}\"\n + "); + + let mut arti_toml_file = + File::create(&arti_toml).map_err(Error::ArtiTomlFileCreationFailed)?; + arti_toml_file + .write_all(arti_toml_content.as_bytes()) + .map_err(Error::ArtiTomlFileWriteFailed)?; + + let process = Command::new(arti_bin_path.as_os_str()) + .stdout(Stdio::inherit()) .stdin(Stdio::null()) .stderr(Stdio::null()) // set working directory to data directory .current_dir(data_directory) + // proxy subcommand + .arg("proxy") // point to our above written arti.toml file .arg("--config") .arg(arti_toml) .spawn() .map_err(Error::ArtiProcessStartFailed)?; + Ok(ArtiProcess { process }) } } diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs index cb9e3b36a..66a1332fc 100644 --- a/tor-interface/src/arti_tor_client.rs +++ b/tor-interface/src/arti_tor_client.rs @@ -5,11 +5,14 @@ use std::path::PathBuf; use crate::tor_crypto::*; use crate::tor_provider; use crate::tor_provider::*; - +use crate::arti_process::*; /// [`ArtiTorClient`]-specific error type #[derive(thiserror::Error, Debug)] pub enum Error { + #[error("failed to create ArtiProcess object: {0}")] + ArtiProcessCreationFailed(#[source] crate::arti_process::Error), + #[error("not implemented")] NotImplemented(), } @@ -32,12 +35,30 @@ pub enum ArtiTorClientConfig { } pub struct ArtiTorClient { - + daemon: Option } impl ArtiTorClient { pub fn new(config: ArtiTorClientConfig) -> Result { - Err(Error::NotImplemented().into()) + let (daemon) = match &config { + ArtiTorClientConfig::BundledArti { + arti_bin_path, + data_directory, + } => { + // launch arti + let daemon = + ArtiProcess::new(arti_bin_path.as_path(), data_directory.as_path()) + .map_err(Error::ArtiProcessCreationFailed)?; + (daemon) + }, + _ => { + return Err(Error::NotImplemented().into()) + } + }; + + Ok(ArtiTorClient { + daemon: Some(daemon) + }) } } From c537dd74b9e6fb721edef2787105070c481b8149 Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 29 Nov 2024 01:20:40 +0000 Subject: [PATCH 149/184] tor-interface: initial art-rpc-client-core integration --- tor-interface/Cargo.toml | 3 +- tor-interface/src/arti_process.rs | 12 ++++++-- tor-interface/src/arti_tor_client.rs | 42 ++++++++++++++++++++++++---- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 56b1d55dc..9f726155c 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] arti-client = { version = "0.24.0", features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} +arti-rpc-client-core = { version = "0.24.0", optional = true } curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" @@ -42,6 +43,6 @@ which = "4.4" [features] arti-client-tor-provider = ["arti-client", "tokio", "tokio-stream", "tor-cell", "tor-config", "tor-hsservice", "tor-keymgr", "tor-proto", "tor-rtcompat"] -arti-tor-provider = [] +arti-tor-provider = ["arti-rpc-client-core"] mock-tor-provider = [] legacy-tor-provider = [] diff --git a/tor-interface/src/arti_process.rs b/tor-interface/src/arti_process.rs index 974caf3e1..89a822777 100644 --- a/tor-interface/src/arti_process.rs +++ b/tor-interface/src/arti_process.rs @@ -6,6 +6,7 @@ use std::ops::Drop; use std::process; use std::process::{Child, ChildStdout, Command, Stdio}; use std::path::Path; +use std::time::{Duration, Instant}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -36,6 +37,7 @@ pub enum Error { pub(crate) struct ArtiProcess { process: Child, + connect_string: String, } impl ArtiProcess { @@ -82,7 +84,7 @@ impl ArtiProcess { kind = \"ephemeral\"\n\ [storage.permissions]\n\ dangerously_trust_everyone = true\n\ - [rpc]\n + [rpc]\n\ rpc_listen = \"{rpc_listen}\"\n "); @@ -93,6 +95,7 @@ impl ArtiProcess { .map_err(Error::ArtiTomlFileWriteFailed)?; let process = Command::new(arti_bin_path.as_os_str()) + // TODO: make this pipe() and fwd log events .stdout(Stdio::inherit()) .stdin(Stdio::null()) .stderr(Stdio::null()) @@ -106,8 +109,13 @@ impl ArtiProcess { .spawn() .map_err(Error::ArtiProcessStartFailed)?; + let connect_string = format!("unix:{rpc_listen}"); - Ok(ArtiProcess { process }) + Ok(ArtiProcess { process, connect_string }) + } + + pub fn connect_string(&self) -> &str { + self.connect_string.as_str() } } diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs index 66a1332fc..6352d24c2 100644 --- a/tor-interface/src/arti_tor_client.rs +++ b/tor-interface/src/arti_tor_client.rs @@ -1,5 +1,9 @@ // std use std::path::PathBuf; +use std::time::{Duration, Instant}; + +// extern +use arti_rpc_client_core::{ObjectId, RpcConn, RpcConnBuilder}; // internal crates use crate::tor_crypto::*; @@ -13,6 +17,9 @@ pub enum Error { #[error("failed to create ArtiProcess object: {0}")] ArtiProcessCreationFailed(#[source] crate::arti_process::Error), + #[error("failed to connect to ArtiProcess after {0:?}")] + ArtiRpcConnectFailed(std::time::Duration), + #[error("not implemented")] NotImplemented(), } @@ -35,12 +42,13 @@ pub enum ArtiTorClientConfig { } pub struct ArtiTorClient { - daemon: Option + daemon: Option, + rpc_conn: RpcConn, } impl ArtiTorClient { pub fn new(config: ArtiTorClientConfig) -> Result { - let (daemon) = match &config { + let (daemon, rpc_conn) = match &config { ArtiTorClientConfig::BundledArti { arti_bin_path, data_directory, @@ -49,7 +57,27 @@ impl ArtiTorClient { let daemon = ArtiProcess::new(arti_bin_path.as_path(), data_directory.as_path()) .map_err(Error::ArtiProcessCreationFailed)?; - (daemon) + + let builder = RpcConnBuilder::from_connect_string(daemon.connect_string()).unwrap(); + + let rpc_conn = { + // try to open an rpc conneciton for 5 seconds beore giving up + let timeout = Duration::from_secs(5); + let mut rpc_conn: Option = None; + + let start = Instant::now(); + while rpc_conn.is_none() && start.elapsed() < timeout { + rpc_conn = builder.connect().map_or(None, |rpc_conn| Some(rpc_conn)); + } + + if let Some(rpc_conn) = rpc_conn { + rpc_conn + } else { + return Err(Error::ArtiRpcConnectFailed(timeout))? + } + }; + + (daemon, rpc_conn) }, _ => { return Err(Error::NotImplemented().into()) @@ -57,18 +85,20 @@ impl ArtiTorClient { }; Ok(ArtiTorClient { - daemon: Some(daemon) + daemon: Some(daemon), + rpc_conn, }) } } impl TorProvider for ArtiTorClient { fn update(&mut self) -> Result, tor_provider::Error> { - Err(Error::NotImplemented().into()) + Ok(Default::default()) } fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { - Err(Error::NotImplemented().into()) + // TODO: seems no way to start arti without automatically bootstrapping + Ok(()) } fn add_client_auth( From ca2c1f751795e53330681255ab6069e15ba524fc Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 29 Nov 2024 01:53:53 +0000 Subject: [PATCH 150/184] tor-interface: implement fake-bootstrapping in ArtiTorClient --- tor-interface/src/arti_tor_client.rs | 43 ++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs index 6352d24c2..e59fc5eea 100644 --- a/tor-interface/src/arti_tor_client.rs +++ b/tor-interface/src/arti_tor_client.rs @@ -1,5 +1,7 @@ // std +use std::ops::DerefMut; use std::path::PathBuf; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; // extern @@ -44,6 +46,9 @@ pub enum ArtiTorClientConfig { pub struct ArtiTorClient { daemon: Option, rpc_conn: RpcConn, + pending_events: Arc>>, + bootstrapped: bool, + } impl ArtiTorClient { @@ -84,20 +89,54 @@ impl ArtiTorClient { } }; - Ok(ArtiTorClient { + let pending_events = std::vec![TorEvent::LogReceived { + line: "Starting arti-client TorProvider".to_string() + }]; + let pending_events = Arc::new(Mutex::new(pending_events)); + + Ok(Self { daemon: Some(daemon), rpc_conn, + pending_events, + bootstrapped: false, }) } } impl TorProvider for ArtiTorClient { fn update(&mut self) -> Result, tor_provider::Error> { - Ok(Default::default()) + std::thread::sleep(std::time::Duration::from_millis(16)); + match self.pending_events.lock() { + Ok(mut pending_events) => Ok(std::mem::take(pending_events.deref_mut())), + Err(_) => { + unreachable!("another thread panicked while holding this pending_events mutex") + } + } } fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { // TODO: seems no way to start arti without automatically bootstrapping + if !self.bootstrapped { + match self.pending_events.lock() { + Ok(mut pending_events) => { + pending_events.push(TorEvent::BootstrapStatus { + progress: 0, + tag: "no-tag".to_string(), + summary: "no summary".to_string(), + }); + pending_events.push(TorEvent::BootstrapStatus { + progress: 100, + tag: "no-tag".to_string(), + summary: "no summary".to_string(), + }); + pending_events.push(TorEvent::BootstrapComplete); + } + Err(_) => unreachable!( + "another thread panicked while holding this pending_events mutex" + ), + } + self.bootstrapped = true; + } Ok(()) } From 0885d02e0b146600d7484e53a1817656b7773ff9 Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 29 Nov 2024 03:38:57 +0000 Subject: [PATCH 151/184] tor-interface: mostly implement ArtiTorClient::connect() --- tor-interface/src/arti_tor_client.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs index e59fc5eea..ef274a9c6 100644 --- a/tor-interface/src/arti_tor_client.rs +++ b/tor-interface/src/arti_tor_client.rs @@ -22,6 +22,12 @@ pub enum Error { #[error("failed to connect to ArtiProcess after {0:?}")] ArtiRpcConnectFailed(std::time::Duration), + #[error("arti not bootstrapped")] + ArtiNotBootstrapped(), + + #[error("failed to connect: {0}")] + ArtiOpenStreamFailed(#[source] arti_rpc_client_core::StreamError), + #[error("not implemented")] NotImplemented(), } @@ -157,10 +163,27 @@ impl TorProvider for ArtiTorClient { fn connect( &mut self, - _target: TargetAddr, + target: TargetAddr, _circuit: Option, ) -> Result { - Err(Error::NotImplemented().into()) + if !self.bootstrapped { + return Err(Error::ArtiNotBootstrapped().into()); + } + + let (host, port) = match &target { + TargetAddr::Socket(socket_addr) => (format!("{:?}", socket_addr.ip()), socket_addr.port()), + TargetAddr::OnionService(OnionAddr::V3(onion_addr)) => (format!("{}.onion", onion_addr.service_id()), onion_addr.virt_port()), + TargetAddr::Domain(domain_addr) => (domain_addr.domain().to_string(), domain_addr.port()), + }; + + let stream = self.rpc_conn.open_stream(None, (host.as_str(), port), "") + .map_err(Error::ArtiOpenStreamFailed)?; + + Ok(OnionStream { + stream, + local_addr: None, + peer_addr: Some(target), + }) } fn listener( From a26063306ef904890a833db26077a4b446df17af Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 29 Nov 2024 03:54:12 +0000 Subject: [PATCH 152/184] tor-interface: add support for circuit isolation to ArtiTorProvider --- tor-interface/src/arti_tor_client.rs | 40 ++++++++++++++++++++++++---- tor-interface/src/tor_crypto.rs | 6 ++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs index ef274a9c6..28d6701d5 100644 --- a/tor-interface/src/arti_tor_client.rs +++ b/tor-interface/src/arti_tor_client.rs @@ -1,4 +1,5 @@ // std +use std::collections::BTreeMap; use std::ops::DerefMut; use std::path::PathBuf; use std::sync::{Arc, Mutex}; @@ -28,6 +29,9 @@ pub enum Error { #[error("failed to connect: {0}")] ArtiOpenStreamFailed(#[source] arti_rpc_client_core::StreamError), + #[error("invalid circuit token: {0}")] + CircuitTokenInvalid(CircuitToken), + #[error("not implemented")] NotImplemented(), } @@ -54,7 +58,9 @@ pub struct ArtiTorClient { rpc_conn: RpcConn, pending_events: Arc>>, bootstrapped: bool, - + // our list of circuit tokens for the arti daemon + circuit_token_counter: usize, + circuit_tokens: BTreeMap, } impl ArtiTorClient { @@ -105,6 +111,8 @@ impl ArtiTorClient { rpc_conn, pending_events, bootstrapped: false, + circuit_token_counter: 0, + circuit_tokens: Default::default(), }) } } @@ -164,19 +172,32 @@ impl TorProvider for ArtiTorClient { fn connect( &mut self, target: TargetAddr, - _circuit: Option, + circuit_token: Option, ) -> Result { if !self.bootstrapped { return Err(Error::ArtiNotBootstrapped().into()); } + // convert TargetAddr to (String, u16) tuple let (host, port) = match &target { TargetAddr::Socket(socket_addr) => (format!("{:?}", socket_addr.ip()), socket_addr.port()), TargetAddr::OnionService(OnionAddr::V3(onion_addr)) => (format!("{}.onion", onion_addr.service_id()), onion_addr.virt_port()), TargetAddr::Domain(domain_addr) => (domain_addr.domain().to_string(), domain_addr.port()), }; - let stream = self.rpc_conn.open_stream(None, (host.as_str(), port), "") + // map circuit_token to isolation string for arti + let isolation = if let Some(circuit_token) = circuit_token { + if let Some(isolation) = self.circuit_tokens.get(&circuit_token) { + isolation.as_str() + } else { + return Err(Error::CircuitTokenInvalid(circuit_token))?; + } + } else { + "" + }; + + // connect to target + let stream = self.rpc_conn.open_stream(None, (host.as_str(), port), isolation) .map_err(Error::ArtiOpenStreamFailed)?; Ok(OnionStream { @@ -196,8 +217,17 @@ impl TorProvider for ArtiTorClient { } fn generate_token(&mut self) -> CircuitToken { - 0usize + const ISOLATION_TOKEN_LEN: usize = 32; + let new_token = self.circuit_token_counter; + self.circuit_token_counter += 1; + self.circuit_tokens.insert( + new_token, + generate_password(ISOLATION_TOKEN_LEN)); + + new_token } - fn release_token(&mut self, _token: CircuitToken) {} + fn release_token(&mut self, token: CircuitToken) { + self.circuit_tokens.remove(&token); + } } diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index 4b921699a..d61f04635 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -6,10 +6,10 @@ use std::str; use curve25519_dalek::Scalar; use data_encoding::{BASE32_NOPAD, BASE64}; use data_encoding_macro::new_encoding; -#[cfg(feature = "legacy-tor-provider")] +#[cfg(any(feature = "legacy-tor-provider", feature = "arti-tor-provider"))] use rand::distributions::Alphanumeric; use rand::rngs::OsRng; -#[cfg(feature = "legacy-tor-provider")] +#[cfg(any(feature = "legacy-tor-provider", feature = "arti-tor-provider"))] use rand::Rng; use sha3::{Digest, Sha3_256}; use static_assertions::const_assert_eq; @@ -110,7 +110,7 @@ const ONION_BASE32: data_encoding::Encoding = new_encoding! { // Free functions // securely generate password using OsRng -#[cfg(feature = "legacy-tor-provider")] +#[cfg(any(feature = "legacy-tor-provider", feature = "arti-tor-provider"))] pub(crate) fn generate_password(length: usize) -> String { let password: String = std::iter::repeat(()) .map(|()| OsRng.sample(Alphanumeric)) From 6056bb04699f84107b8f9f5b2a7a9ddebe3cc44d Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 29 Nov 2024 23:36:53 +0000 Subject: [PATCH 153/184] tor-interface: forward arti stdout lines to TorEvent::LogLine in ArtiTorClient --- tor-interface/src/arti_process.rs | 56 +++++++++++++++++++++++----- tor-interface/src/arti_tor_client.rs | 27 ++++++++++++-- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/tor-interface/src/arti_process.rs b/tor-interface/src/arti_process.rs index 89a822777..ffa8631db 100644 --- a/tor-interface/src/arti_process.rs +++ b/tor-interface/src/arti_process.rs @@ -1,11 +1,12 @@ // standard use std::fs; use std::fs::File; -use std::io::Write; +use std::io::{BufRead, BufReader, Write}; use std::ops::Drop; use std::process; use std::process::{Child, ChildStdout, Command, Stdio}; use std::path::Path; +use std::sync::{Mutex, Weak}; use std::time::{Duration, Instant}; #[derive(thiserror::Error, Debug)] @@ -19,20 +20,26 @@ pub enum Error { #[error("provided data directory '{0}' must be an absolute path")] ArtiDataDirectoryPathNotAbsolute(String), - #[error("failed to create data directory")] + #[error("failed to create data directory: {0}")] ArtiDataDirectoryCreationFailed(#[source] std::io::Error), #[error("file exists in provided data directory path '{0}'")] ArtiDataDirectoryPathExistsAsFile(String), - #[error("failed to create arti.toml file")] + #[error("failed to create arti.toml file: {0}")] ArtiTomlFileCreationFailed(#[source] std::io::Error), - #[error("failed to write arti.toml file")] + #[error("failed to write arti.toml file: {0}")] ArtiTomlFileWriteFailed(#[source] std::io::Error), - #[error("failed to start arti process")] + #[error("failed to start arti process: {0}")] ArtiProcessStartFailed(#[source] std::io::Error), + + #[error("unable to take arti process stdout")] + ArtiProcessStdoutTakeFailed(), + + #[error("failed to spawn arti process stdout read thread: {0}")] + ArtiStdoutReadThreadSpawnFailed(#[source] std::io::Error), } pub(crate) struct ArtiProcess { @@ -41,7 +48,7 @@ pub(crate) struct ArtiProcess { } impl ArtiProcess { - pub fn new(arti_bin_path: &Path, data_directory: &Path) -> Result { + pub fn new(arti_bin_path: &Path, data_directory: &Path, stdout_lines: Weak>>) -> Result { // verify provided paths are absolute if arti_bin_path.is_relative() { return Err(Error::ArtiBinPathNotAbsolute(format!( @@ -94,9 +101,8 @@ impl ArtiProcess { .write_all(arti_toml_content.as_bytes()) .map_err(Error::ArtiTomlFileWriteFailed)?; - let process = Command::new(arti_bin_path.as_os_str()) - // TODO: make this pipe() and fwd log events - .stdout(Stdio::inherit()) + let mut process = Command::new(arti_bin_path.as_os_str()) + .stdout(Stdio::piped()) .stdin(Stdio::null()) .stderr(Stdio::null()) // set working directory to data directory @@ -109,6 +115,18 @@ impl ArtiProcess { .spawn() .map_err(Error::ArtiProcessStartFailed)?; + // spawn a task to read stdout lines and forward to list + let stdout = BufReader::new(match process.stdout.take() { + Some(stdout) => stdout, + None => return Err(Error::ArtiProcessStdoutTakeFailed()), + }); + std::thread::Builder::new() + .name("arti_stdout_reader".to_string()) + .spawn(move || { + ArtiProcess::read_stdout_task(&stdout_lines, stdout); + }) + .map_err(Error::ArtiStdoutReadThreadSpawnFailed)?; + let connect_string = format!("unix:{rpc_listen}"); Ok(ArtiProcess { process, connect_string }) @@ -117,6 +135,26 @@ impl ArtiProcess { pub fn connect_string(&self) -> &str { self.connect_string.as_str() } + + fn read_stdout_task( + stdout_lines: &std::sync::Weak>>, + mut stdout: BufReader, + ) { + while let Some(stdout_lines) = stdout_lines.upgrade() { + let mut line = String::default(); + // read line + if stdout.read_line(&mut line).is_ok() { + // remove trailing '\n' + line.pop(); + // then acquire the lock on the line buffer + let mut stdout_lines = match stdout_lines.lock() { + Ok(stdout_lines) => stdout_lines, + Err(_) => unreachable!(), + }; + stdout_lines.push(line); + } + } + } } impl Drop for ArtiProcess { diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs index 28d6701d5..585ecc462 100644 --- a/tor-interface/src/arti_tor_client.rs +++ b/tor-interface/src/arti_tor_client.rs @@ -56,6 +56,7 @@ pub enum ArtiTorClientConfig { pub struct ArtiTorClient { daemon: Option, rpc_conn: RpcConn, + pending_log_lines: Arc>>, pending_events: Arc>>, bootstrapped: bool, // our list of circuit tokens for the arti daemon @@ -65,14 +66,17 @@ pub struct ArtiTorClient { impl ArtiTorClient { pub fn new(config: ArtiTorClientConfig) -> Result { + let pending_log_lines: Arc>> = Default::default(); + let (daemon, rpc_conn) = match &config { ArtiTorClientConfig::BundledArti { arti_bin_path, data_directory, } => { + // launch arti let daemon = - ArtiProcess::new(arti_bin_path.as_path(), data_directory.as_path()) + ArtiProcess::new(arti_bin_path.as_path(), data_directory.as_path(), Arc::downgrade(&pending_log_lines)) .map_err(Error::ArtiProcessCreationFailed)?; let builder = RpcConnBuilder::from_connect_string(daemon.connect_string()).unwrap(); @@ -109,6 +113,7 @@ impl ArtiTorClient { Ok(Self { daemon: Some(daemon), rpc_conn, + pending_log_lines, pending_events, bootstrapped: false, circuit_token_counter: 0, @@ -120,12 +125,28 @@ impl ArtiTorClient { impl TorProvider for ArtiTorClient { fn update(&mut self) -> Result, tor_provider::Error> { std::thread::sleep(std::time::Duration::from_millis(16)); - match self.pending_events.lock() { - Ok(mut pending_events) => Ok(std::mem::take(pending_events.deref_mut())), + let mut tor_events = match self.pending_events.lock() { + Ok(mut pending_events) => std::mem::take(pending_events.deref_mut()), Err(_) => { unreachable!("another thread panicked while holding this pending_events mutex") } + }; + // take our log lines + let mut log_lines = match self.pending_log_lines.lock() { + Ok(mut pending_log_lines) => std::mem::take(pending_log_lines.deref_mut()), + Err(_) => { + unreachable!("another thread panicked while holding this pending_log_lines mutex") + } + }; + + // append raw lines as TorEvent + for log_line in log_lines.iter_mut() { + tor_events.push(TorEvent::LogReceived { + line: std::mem::take(log_line), + }); } + + Ok(tor_events) } fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { From 8db52553b12674d05ba99f1b0529088c34e92ff8 Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 29 Nov 2024 04:32:01 +0000 Subject: [PATCH 154/184] workflows, tor-interface: stubbed out/enabled tests exercising ArtiTorClient --- tor-interface/CMakeLists.txt | 23 ++++++++++-- tor-interface/tests/tor_provider.rs | 56 ++++++++++++++++++++++++++--- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index e7aad06b9..aa7ade6e3 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -126,11 +126,11 @@ if (ENABLE_TESTS) endif() if (ENABLE_LEGACY_TOR_PROVIDER AND ENABLE_ARTI_CLIENT_TOR_PROVIDER) - add_test(NAME tor_interface_mixed_arti_client_legacy_bootstrap_cargo_test + add_test(NAME tor_interface_mixed_arti_client_legacy_onion_service_cargo_test COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_arti_client_legacy_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) - add_test(NAME tor_interface_mixed_legacy_arti_client_bootstrap_cargo_test + add_test(NAME tor_interface_mixed_legacy_arti_client_onion_service_cargo_test COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_legacy_arti_client_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) @@ -144,6 +144,25 @@ if (ENABLE_TESTS) ) endif() + if (ENABLE_LEGACY_TOR_PROVIDER AND ENABLE_ARTI_TOR_PROVIDER) + # add_test(NAME tor_interface_mixed_arti_legacy_onion_service_cargo_test + # COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_arti_legacy_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + # WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + # ) + add_test(NAME tor_interface_mixed_legacy_arti_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_legacy_arti_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + # add_test(NAME tor_interface_mixed_arti_legacy_authenticated_onion_service_cargo_test + # COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_arti_legacy_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + # WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + # ) + # add_test(NAME tor_interface_mixed_legacy_arti_authenticated_onion_service_cargo_test + # COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_legacy_arti_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + # WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + # ) + endif() + # cryptography add_test(NAME tor_interface_crypto_cargo_test COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_crypto_ ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index adba014a5..c9b3c45bf 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -699,9 +699,7 @@ fn test_arti_bootstrap() -> anyhow::Result<()> { } // - -// -// Mixed Arti/Legacy TorProvider tests +// Mixed Arti-Client/Legacy TorProvider tests // #[test] @@ -746,8 +744,56 @@ fn test_mixed_arti_client_legacy_authenticated_onion_service() -> anyhow::Result fn test_mixed_legacy_arti_client_authenticated_onion_service() -> anyhow::Result<()> { let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); - let server_provider = build_arti_client_tor_provider(runtime, "test_mixed_legacy_arti_client_authenticated_onion_service_server")?; - let client_provider = build_bundled_legacy_tor_provider("test_mixed_legacy_arti_client_authenticated_onion_service_client")?; + let server_provider = build_bundled_legacy_tor_provider("test_mixed_legacy_arti_client_authenticated_onion_service_server")?; + let client_provider = build_arti_client_tor_provider(runtime, "test_mixed_legacy_arti_client_authenticated_onion_service_client")?; authenticated_onion_service_test(server_provider, client_provider) } + +// +// Mixed Arti/Legacy TorProvider tests +// + +// #[test] +// #[serial] +// #[cfg(all(feature = "arti-tor-provider", feature = "legacy-tor-provider"))] +// fn test_mixed_arti_legacy_onion_service() -> anyhow::Result<()> { +// let server_provider = build_arti_tor_provider("test_mixed_arti_legacy_onion_service_server")?; +// let client_provider = build_bundled_legacy_tor_provider("test_mixed_arti_legacy_onion_service_client")?; + +// basic_onion_service_test(server_provider, client_provider) +// } + +#[test] +#[serial] +#[cfg(all(feature = "arti-tor-provider", feature = "legacy-tor-provider"))] +fn test_mixed_legacy_arti_onion_service() -> anyhow::Result<()> { + let server_provider = build_bundled_legacy_tor_provider("test_mixed_legacy_arti_onion_service_server")?; + let client_provider = build_arti_tor_provider("test_mixed_legacy_arti_onion_service_client")?; + + basic_onion_service_test(server_provider, client_provider) +} + +// #[test] +// #[serial] +// #[cfg(all(feature = "arti-tor-provider", feature = "legacy-tor-provider"))] +// fn test_mixed_arti_legacy_authenticated_onion_service() -> anyhow::Result<()> { +// let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + +// let server_provider = build_arti_tor_provider("test_mixed_arti_legacy_authenticated_onion_service_server")?; +// let client_provider = build_bundled_legacy_tor_provider("test_mixed_arti_legacy_authenticated_onion_service_client")?; + +// authenticated_onion_service_test(server_provider, client_provider) +// } + +// #[test] +// #[serial] +// #[cfg(all(feature = "arti-tor-provider", feature = "legacy-tor-provider"))] +// fn test_mixed_legacy_arti_authenticated_onion_service() -> anyhow::Result<()> { +// let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + +// let server_provider = build_bundled_legacy_tor_provider("test_mixed_legacy_arti_authenticated_onion_service_server")?; +// let client_provider = build_arti_tor_provider("test_mixed_legacy_arti_authenticated_onion_service_client")?; + +// authenticated_onion_service_test(server_provider, client_provider) +// } From 0831901ec3edaef755734aa9626e6557303548af Mon Sep 17 00:00:00 2001 From: Morgan Date: Sat, 8 Feb 2025 22:03:03 +0000 Subject: [PATCH 155/184] extern, tor-interface, cgosling: update tor-expert-bundle to latest and example obfs4 bridge in tests --- tor-interface/tests/tor_provider.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index c9b3c45bf..d7ec1f1d7 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -87,7 +87,7 @@ fn build_bundled_pt_legacy_tor_provider(name: &str) -> anyhow::Result Date: Sat, 8 Feb 2025 15:34:36 +0000 Subject: [PATCH 156/184] tor-interface: updated arti to 0.27.0 --- tor-interface/Cargo.toml | 18 ++--- tor-interface/src/arti_process.rs | 101 ++++++++++++++++++++++----- tor-interface/src/arti_tor_client.rs | 16 +++-- 3 files changed, 101 insertions(+), 34 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 9f726155c..797c27b32 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,8 +10,8 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { version = "0.24.0", features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} -arti-rpc-client-core = { version = "0.24.0", optional = true } +arti-client = { version = "0.27.0", features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} +arti-rpc-client-core = { version = "0.27.0", optional = true } curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" @@ -28,13 +28,13 @@ static_assertions = "1.1" thiserror = "1.0" tokio = { version = "1", features = ["macros"], optional = true } tokio-stream = { version = "0", optional = true } -tor-cell = { version = "0.24.0", optional = true } -tor-config = { version = "0.24.0", optional = true } -tor-hsservice = { version = "0.24.0", optional = true, features = ["restricted-discovery"] } -tor-keymgr = { version = "0.24.0", optional = true, features = ["keymgr"] } -tor-llcrypto = { version = "0.24.0", features = ["relay"] } -tor-proto = { version = "0.24.0", features = ["stream-ctrl"], optional = true } -tor-rtcompat = { version = "0.24.0", optional = true } +tor-cell = { version = "0.27.0", optional = true } +tor-config = { version = "0.27.0", optional = true } +tor-hsservice = { version = "0.27.0", optional = true, features = ["restricted-discovery"] } +tor-keymgr = { version = "0.27.0", optional = true, features = ["keymgr"] } +tor-llcrypto = { version = "0.27.0", features = ["relay"] } +tor-proto = { version = "0.27.0", features = ["stream-ctrl"], optional = true } +tor-rtcompat = { version = "0.27.0", optional = true } [dev-dependencies] anyhow = "1.0" diff --git a/tor-interface/src/arti_process.rs b/tor-interface/src/arti_process.rs index ffa8631db..a4ce8489c 100644 --- a/tor-interface/src/arti_process.rs +++ b/tor-interface/src/arti_process.rs @@ -1,19 +1,16 @@ // standard use std::fs; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; use std::fs::File; use std::io::{BufRead, BufReader, Write}; use std::ops::Drop; -use std::process; use std::process::{Child, ChildStdout, Command, Stdio}; use std::path::Path; use std::sync::{Mutex, Weak}; -use std::time::{Duration, Instant}; #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("not implemented")] - NotImplemented(), - #[error("provided arti bin path '{0}' must be an absolute path")] ArtiBinPathNotAbsolute(String), @@ -26,12 +23,21 @@ pub enum Error { #[error("file exists in provided data directory path '{0}'")] ArtiDataDirectoryPathExistsAsFile(String), + #[error("unable to set permissions for data directory: {0}")] + ArtiDataDirectorySetPermissionsFailed(#[source] std::io::Error), + #[error("failed to create arti.toml file: {0}")] ArtiTomlFileCreationFailed(#[source] std::io::Error), #[error("failed to write arti.toml file: {0}")] ArtiTomlFileWriteFailed(#[source] std::io::Error), + #[error("failed to create rpc.toml file: {0}")] + RpcTomlFileCreationFailed(#[source] std::io::Error), + + #[error("failed to write rpc.toml file: {0}")] + RpcTomlFileWriteFailed(#[source] std::io::Error), + #[error("failed to start arti process: {0}")] ArtiProcessStartFailed(#[source] std::io::Error), @@ -73,28 +79,89 @@ impl ArtiProcess { ))); } - // construct paths to arti files file - let arti_toml = data_directory.join("arti.toml"); + // arti data directory must not be world-writable on unix platforms when using a unix domain socket endpoint + if cfg!(unix) { + let perms = PermissionsExt::from_mode(0o700); + fs::set_permissions(data_directory, perms).map_err(Error::ArtiDataDirectorySetPermissionsFailed)?; + } - // write arti.toml settings file (always overwrite) + // construct paths to arti files file + let arti_toml = data_directory.join("arti.toml").display().to_string(); let cache_dir = data_directory.join("cache").display().to_string(); let state_dir = data_directory.join("state").display().to_string(); - let rpc_listen = data_directory.join("SOCKET").display().to_string(); - let arti_toml_content = format!("\ + let mut arti_toml_content = format!("\ + [rpc]\n\ + enable = true\n\n\ + [rpc.listen.user-default]\n\ + enable = false\n\n\ + [rpc.listen.system-default]\n\ + enable = false\n\n\ [storage]\n\ cache_dir = \"{cache_dir}\"\n\ - state_dir = \"{state_dir}\"\n\ + state_dir = \"{state_dir}\"\n\n\ [storage.keystore]\n\ - enabled = true\n\ + enabled = true\n\n\ [storage.keystore.primary]\n\ - kind = \"ephemeral\"\n\ + kind = \"ephemeral\"\n\n\ [storage.permissions]\n\ - dangerously_trust_everyone = true\n\ - [rpc]\n\ - rpc_listen = \"{rpc_listen}\"\n + dangerously_trust_everyone = true\n\n\ "); + let connect_string = if cfg!(unix) { + // use domain socket for unix + let unix_rpc_toml_path = data_directory.join("rpc.toml").display().to_string(); + + arti_toml_content.push_str(format!("\ + [rpc.listen.unix-point]\n\ + enable = true\n\ + file = \"{unix_rpc_toml_path}\"\n\n\ + ").as_str()); + + let socket_path = data_directory.join("rpc.socket").display().to_string(); + + let unix_rpc_toml_content = format!("\ + [connect]\n\ + socket = \"unix:{socket_path}\"\n\ + auth = \"none\"\n\ + "); + + let mut unix_rpc_toml_file = + File::create(&unix_rpc_toml_path).map_err(Error::RpcTomlFileCreationFailed)?; + unix_rpc_toml_file + .write_all(unix_rpc_toml_content.as_bytes()) + .map_err(Error::RpcTomlFileWriteFailed)?; + + unix_rpc_toml_path + } else { + // use tcp socket everywhere else + let tcp_rpc_toml_path = data_directory.join("rpc.toml").display().to_string(); + + arti_toml_content.push_str(format!("\ + [rpc.listen.tcp-point]\n\ + enable = true\n\ + file = \"{tcp_rpc_toml_path}\"\n\n\ + ").as_str()); + + let cookie_path = data_directory.join("rpc.cookie").display().to_string(); + + const RPC_PORT: u16 = 18929; + + let tcp_rpc_toml_content = format!("\ + [connect]\n\ + socket = \"inet:127.0.0.1:{RPC_PORT}\"\n\ + auth = {{ cookie = {{ path = \"{cookie_path}\" }} }}\n\ + "); + + let mut tcp_rpc_toml_file = + File::create(&tcp_rpc_toml_path).map_err(Error::RpcTomlFileCreationFailed)?; + tcp_rpc_toml_file + .write_all(tcp_rpc_toml_content.as_bytes()) + .map_err(Error::RpcTomlFileWriteFailed)?; + + tcp_rpc_toml_path + }; + let mut arti_toml_file = File::create(&arti_toml).map_err(Error::ArtiTomlFileCreationFailed)?; arti_toml_file @@ -127,8 +194,6 @@ impl ArtiProcess { }) .map_err(Error::ArtiStdoutReadThreadSpawnFailed)?; - let connect_string = format!("unix:{rpc_listen}"); - Ok(ArtiProcess { process, connect_string }) } diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs index 585ecc462..63e3943ad 100644 --- a/tor-interface/src/arti_tor_client.rs +++ b/tor-interface/src/arti_tor_client.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; // extern -use arti_rpc_client_core::{ObjectId, RpcConn, RpcConnBuilder}; +use arti_rpc_client_core::{RpcConn, RpcConnBuilder}; // internal crates use crate::tor_crypto::*; @@ -54,7 +54,7 @@ pub enum ArtiTorClientConfig { } pub struct ArtiTorClient { - daemon: Option, + _daemon: Option, rpc_conn: RpcConn, pending_log_lines: Arc>>, pending_events: Arc>>, @@ -79,15 +79,17 @@ impl ArtiTorClient { ArtiProcess::new(arti_bin_path.as_path(), data_directory.as_path(), Arc::downgrade(&pending_log_lines)) .map_err(Error::ArtiProcessCreationFailed)?; - let builder = RpcConnBuilder::from_connect_string(daemon.connect_string()).unwrap(); - let rpc_conn = { - // try to open an rpc conneciton for 5 seconds beore giving up + // try to open an rpc connnection for 5 seconds beore giving up let timeout = Duration::from_secs(5); let mut rpc_conn: Option = None; let start = Instant::now(); while rpc_conn.is_none() && start.elapsed() < timeout { + + let mut builder = RpcConnBuilder::new(); + builder.prepend_literal_path(daemon.connect_string().into()); + rpc_conn = builder.connect().map_or(None, |rpc_conn| Some(rpc_conn)); } @@ -106,12 +108,12 @@ impl ArtiTorClient { }; let pending_events = std::vec![TorEvent::LogReceived { - line: "Starting arti-client TorProvider".to_string() + line: "Starting arti TorProvider".to_string() }]; let pending_events = Arc::new(Mutex::new(pending_events)); Ok(Self { - daemon: Some(daemon), + _daemon: Some(daemon), rpc_conn, pending_log_lines, pending_events, From 2d75a3308ba478b74969f52f4edb1c87b6515778 Mon Sep 17 00:00:00 2001 From: Morgan Date: Tue, 10 Jun 2025 19:47:14 +0000 Subject: [PATCH 157/184] tor-interface: updated arti to 0.31.0 --- tor-interface/Cargo.toml | 22 +++++++++++----------- tor-interface/src/legacy_tor_process.rs | 4 ++-- tor-interface/src/tor_crypto.rs | 22 +++++++++------------- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 797c27b32..c28b92366 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,15 +10,15 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { version = "0.27.0", features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} -arti-rpc-client-core = { version = "0.27.0", optional = true } +arti-client = { version = "0.31.0", features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} +arti-rpc-client-core = { version = "0.31.0", optional = true } curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" domain = "<= 0.10.0" idna = "1" -rand = "0.8" -rand_core = "0.6" +rand = "0.9" +rand_core = "0.9" regex = "1.9" sha1 = "0.10" sha3 = "0.10" @@ -28,13 +28,13 @@ static_assertions = "1.1" thiserror = "1.0" tokio = { version = "1", features = ["macros"], optional = true } tokio-stream = { version = "0", optional = true } -tor-cell = { version = "0.27.0", optional = true } -tor-config = { version = "0.27.0", optional = true } -tor-hsservice = { version = "0.27.0", optional = true, features = ["restricted-discovery"] } -tor-keymgr = { version = "0.27.0", optional = true, features = ["keymgr"] } -tor-llcrypto = { version = "0.27.0", features = ["relay"] } -tor-proto = { version = "0.27.0", features = ["stream-ctrl"], optional = true } -tor-rtcompat = { version = "0.27.0", optional = true } +tor-cell = { version = "0.31.0", optional = true } +tor-config = { version = "0.31.0", optional = true } +tor-hsservice = { version = "0.31.0", optional = true, features = ["restricted-discovery"] } +tor-keymgr = { version = "0.31.0", optional = true, features = ["keymgr"] } +tor-llcrypto = { version = "0.31.0", features = ["relay"] } +tor-proto = { version = "0.31.0", features = ["stream-ctrl"], optional = true } +tor-rtcompat = { version = "0.31.0", optional = true } [dev-dependencies] anyhow = "1.0" diff --git a/tor-interface/src/legacy_tor_process.rs b/tor-interface/src/legacy_tor_process.rs index b37affa19..074748439 100644 --- a/tor-interface/src/legacy_tor_process.rs +++ b/tor-interface/src/legacy_tor_process.rs @@ -15,7 +15,6 @@ use std::time::{Duration, Instant}; // extern crates use data_encoding::HEXUPPER; -use rand::rngs::OsRng; use rand::RngCore; use sha1::{Digest, Sha1}; @@ -156,7 +155,8 @@ impl LegacyTorProcess { fn hash_tor_password(password: &str) -> String { let mut salt = [0x00u8; Self::S2K_RFC2440_SPECIFIER_LEN]; - OsRng.fill_bytes(&mut salt); + let csprng = &mut tor_llcrypto::rng::CautiousRng; + csprng.fill_bytes(&mut salt); salt[Self::S2K_RFC2440_SPECIFIER_LEN - 1] = 0x60u8; Self::hash_tor_password_with_salt(&salt, password) diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs index d61f04635..72f7b27fa 100644 --- a/tor-interface/src/tor_crypto.rs +++ b/tor-interface/src/tor_crypto.rs @@ -7,8 +7,7 @@ use curve25519_dalek::Scalar; use data_encoding::{BASE32_NOPAD, BASE64}; use data_encoding_macro::new_encoding; #[cfg(any(feature = "legacy-tor-provider", feature = "arti-tor-provider"))] -use rand::distributions::Alphanumeric; -use rand::rngs::OsRng; +use rand::distr::Alphanumeric; #[cfg(any(feature = "legacy-tor-provider", feature = "arti-tor-provider"))] use rand::Rng; use sha3::{Digest, Sha3_256}; @@ -109,11 +108,11 @@ const ONION_BASE32: data_encoding::Encoding = new_encoding! { // Free functions -// securely generate password using OsRng +// securely generate password using CautionsRng #[cfg(any(feature = "legacy-tor-provider", feature = "arti-tor-provider"))] pub(crate) fn generate_password(length: usize) -> String { let password: String = std::iter::repeat(()) - .map(|()| OsRng.sample(Alphanumeric)) + .map(|()| tor_llcrypto::rng::CautiousRng.sample(Alphanumeric)) .map(char::from) .take(length) .collect(); @@ -151,7 +150,7 @@ pub struct X25519PrivateKey { } /// An x25519 public key -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq)] pub struct X25519PublicKey { public_key: pk::curve25519::PublicKey, } @@ -213,7 +212,7 @@ enum FromRawValidationMethod { impl Ed25519PrivateKey { /// Securely generate a new `Ed25519PrivateKey`. pub fn generate() -> Ed25519PrivateKey { - let csprng = &mut OsRng; + let csprng = &mut tor_llcrypto::rng::CautiousRng; let keypair = pk::ed25519::Keypair::generate(csprng); Ed25519PrivateKey { @@ -487,13 +486,10 @@ impl Ed25519Signature { /// Verify this `Ed25519Signature` for the given message and [`Ed25519PublicKey`]. pub fn verify(&self, message: &[u8], public_key: &Ed25519PublicKey) -> bool { - if let Ok(()) = public_key + public_key .public_key - .verify_strict(message, &self.signature) - { - return true; - } - false + .verify(message, &self.signature) + .is_ok() } /// Verify this `Ed25519Signature` for the given message, [`X25519PublicKey`], and [`SignBit`]. This signature must have been created by first converting an [`X25519PrivateKey`] to a [`Ed25519PrivateKey`] and [`SignBit`], and then signing the message using this [`Ed25519PrivateKey`]. This method verifies the signature using the [`Ed25519PublicKey`] derived from the provided [`X25519PublicKey`] and [`SignBit`]. @@ -531,7 +527,7 @@ impl std::fmt::Debug for Ed25519Signature { impl X25519PrivateKey { /// Securely generate a new `X25519PrivateKey` pub fn generate() -> X25519PrivateKey { - let csprng = &mut OsRng; + let csprng = &mut tor_llcrypto::rng::CautiousRng; X25519PrivateKey { secret_key: pk::curve25519::StaticSecret::random_from_rng(csprng), } From cff9b6ec6042b56b2f363ed44b685e6022de34ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 24 Jun 2025 16:17:17 +0200 Subject: [PATCH 158/184] tor-interface: fix test_legacy_pluggable_transport_bootstrap() when $TEB_PATH not available --- tor-interface/tests/tor_provider.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index d7ec1f1d7..cac07bc05 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -71,7 +71,7 @@ fn build_bundled_pt_legacy_tor_provider(name: &str) -> anyhow::Result Date: Mon, 9 Jun 2025 18:08:30 +0200 Subject: [PATCH 159/184] tor-interface: fix comments --- tor-interface/src/legacy_tor_client.rs | 2 +- tor-interface/src/mock_tor_client.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 9e01c5fda..534d11ce3 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -633,7 +633,7 @@ impl TorProvider for LegacyTorClient { }) } - // stand up an onion service and return an LegacyOnionListener + // stand up an onion service and return an OnionListener fn listener( &mut self, private_key: &Ed25519PrivateKey, diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index ec546a37b..d9a32e53f 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -142,7 +142,7 @@ pub struct MockTorClient { } impl MockTorClient { - /// Construt a new `MockTorClient`. + /// Construct a new `MockTorClient`. pub fn new() -> MockTorClient { let mut events: Vec = Default::default(); let line = "[notice] MockTorClient running".to_string(); From 3abd4179cfee89779be225286568fa2b88320f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 24 Jun 2025 15:58:22 +0200 Subject: [PATCH 160/184] tor-interface: flatten redundant double-init --- tor-interface/src/legacy_tor_client.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 534d11ce3..3cda2199d 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -651,13 +651,11 @@ impl TorProvider for LegacyTorClient { .local_addr() .map_err(Error::TcpListenerLocalAddrFailed)?; - let mut flags = AddOnionFlags { + let flags = AddOnionFlags { discard_pk: true, + v3_auth: authorized_clients.is_some(), ..Default::default() }; - if authorized_clients.is_some() { - flags.v3_auth = true; - } let onion_addr = OnionAddr::V3(OnionAddrV3::new( V3OnionServiceId::from_private_key(private_key), From 6e87770189d97fc90cc8cf29f2d9b83416615567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 10 Jun 2025 09:57:27 +0200 Subject: [PATCH 161/184] tor-interface: unify 250 checking in LegacyTorController --- tor-interface/src/legacy_tor_controller.rs | 180 +++++++++------------ 1 file changed, 76 insertions(+), 104 deletions(-) diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index d305a9efc..c79c845a2 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -95,6 +95,13 @@ fn quoted_string(string: &str) -> String { string.replace("\\", "\\\\").replace("\"", "\\\"") } +fn reply_ok(reply: Reply) -> Result { + match reply.status_code { + 250u32 => Ok(reply), + code => Err(Error::CommandFailed(code, reply.reply_lines)), + } +} + impl LegacyTorController { pub fn new(control_stream: LegacyControlStream) -> Result { let status_event_pattern = @@ -432,73 +439,48 @@ impl LegacyTorController { // pub fn setconf(&mut self, key_values: &[(&str, String)]) -> Result<(), Error> { - let reply = self.setconf_cmd(key_values)?; - - match reply.status_code { - 250u32 => Ok(()), - code => Err(Error::CommandFailed(code, reply.reply_lines)), - } + self.setconf_cmd(key_values).and_then(reply_ok).map(|_| ()) } #[cfg(test)] pub fn getconf(&mut self, keywords: &[&str]) -> Result, Error> { - let reply = self.getconf_cmd(keywords)?; - - match reply.status_code { - 250u32 => { - let mut key_values: Vec<(String, String)> = Default::default(); - for line in reply.reply_lines { - match line.find('=') { - Some(index) => key_values - .push((line[0..index].to_string(), line[index + 1..].to_string())), - None => key_values.push((line, String::new())), - } - } - Ok(key_values) + let reply = self.getconf_cmd(keywords).and_then(reply_ok)?; + + let mut key_values: Vec<(String, String)> = Default::default(); + for line in reply.reply_lines { + match line.find('=') { + Some(index) => key_values + .push((line[0..index].to_string(), line[index + 1..].to_string())), + None => key_values.push((line, String::new())), } - code => Err(Error::CommandFailed(code, reply.reply_lines)), } + Ok(key_values) } pub fn setevents(&mut self, events: &[&str]) -> Result<(), Error> { - let reply = self.setevents_cmd(events)?; - - match reply.status_code { - 250u32 => Ok(()), - code => Err(Error::CommandFailed(code, reply.reply_lines)), - } + self.setevents_cmd(events).and_then(reply_ok).map(|_| ()) } pub fn authenticate(&mut self, password: &str) -> Result<(), Error> { - let reply = self.authenticate_cmd(password)?; - - match reply.status_code { - 250u32 => Ok(()), - code => Err(Error::CommandFailed(code, reply.reply_lines)), - } + self.authenticate_cmd(password).and_then(reply_ok).map(|_| ()) } pub fn getinfo(&mut self, keywords: &[&str]) -> Result, Error> { - let reply = self.getinfo_cmd(keywords)?; - - match reply.status_code { - 250u32 => { - let mut key_values: Vec<(String, String)> = Default::default(); - for line in reply.reply_lines { - match line.find('=') { - Some(index) => key_values - .push((line[0..index].to_string(), line[index + 1..].to_string())), - None => { - if line != "OK" { - key_values.push((line, String::new())) - } - } + let reply = self.getinfo_cmd(keywords).and_then(reply_ok)?; + + let mut key_values: Vec<(String, String)> = Default::default(); + for line in reply.reply_lines { + match line.find('=') { + Some(index) => key_values + .push((line[0..index].to_string(), line[index + 1..].to_string())), + None => { + if line != "OK" { + key_values.push((line, String::new())) } } - Ok(key_values) } - code => Err(Error::CommandFailed(code, reply.reply_lines)), } + Ok(key_values) } pub fn add_onion( @@ -510,64 +492,59 @@ impl LegacyTorController { target: Option, client_auth: Option<&[X25519PublicKey]>, ) -> Result<(Option, V3OnionServiceId), Error> { - let reply = self.add_onion_cmd(key, flags, max_streams, virt_port, target, client_auth)?; + let reply = self.add_onion_cmd(key, flags, max_streams, virt_port, target, client_auth).and_then(reply_ok)?; let mut private_key: Option = None; let mut service_id: Option = None; - match reply.status_code { - 250u32 => { - for line in reply.reply_lines { - if let Some(mut index) = line.find("ServiceID=") { - if service_id.is_some() { - return Err(Error::CommandReplyParseFailed( - "received duplicate ServiceID entries".to_string(), - )); - } - index += "ServiceId=".len(); - let service_id_string = &line[index..]; - service_id = match V3OnionServiceId::from_string(service_id_string) { - Ok(service_id) => Some(service_id), - Err(_) => { - return Err(Error::CommandReplyParseFailed(format!( - "could not parse '{}' as V3OnionServiceId", - service_id_string - ))) - } - } - } else if let Some(mut index) = line.find("PrivateKey=") { - if private_key.is_some() { - return Err(Error::CommandReplyParseFailed( - "received duplicate PrivateKey entries".to_string(), - )); - } - index += "PrivateKey=".len(); - let key_blob_string = &line[index..]; - private_key = match Ed25519PrivateKey::from_key_blob_legacy(key_blob_string) - { - Ok(private_key) => Some(private_key), - Err(_) => { - return Err(Error::CommandReplyParseFailed(format!( - "could not parse {} as Ed25519PrivateKey", - key_blob_string - ))) - } - }; - } else if line.contains("ClientAuthV3=") { - if client_auth.unwrap_or_default().is_empty() { - return Err(Error::CommandReplyParseFailed( - "recieved unexpected ClientAuthV3 keys".to_string(), - )); - } - } else if !line.contains("OK") { + for line in reply.reply_lines { + if let Some(mut index) = line.find("ServiceID=") { + if service_id.is_some() { + return Err(Error::CommandReplyParseFailed( + "received duplicate ServiceID entries".to_string(), + )); + } + index += "ServiceId=".len(); + let service_id_string = &line[index..]; + service_id = match V3OnionServiceId::from_string(service_id_string) { + Ok(service_id) => Some(service_id), + Err(_) => { return Err(Error::CommandReplyParseFailed(format!( - "received unexpected reply line '{}'", - line - ))); + "could not parse '{}' as V3OnionServiceId", + service_id_string + ))) + } + } + } else if let Some(mut index) = line.find("PrivateKey=") { + if private_key.is_some() { + return Err(Error::CommandReplyParseFailed( + "received duplicate PrivateKey entries".to_string(), + )); + } + index += "PrivateKey=".len(); + let key_blob_string = &line[index..]; + private_key = match Ed25519PrivateKey::from_key_blob_legacy(key_blob_string) + { + Ok(private_key) => Some(private_key), + Err(_) => { + return Err(Error::CommandReplyParseFailed(format!( + "could not parse {} as Ed25519PrivateKey", + key_blob_string + ))) } + }; + } else if line.contains("ClientAuthV3=") { + if client_auth.unwrap_or_default().is_empty() { + return Err(Error::CommandReplyParseFailed( + "recieved unexpected ClientAuthV3 keys".to_string(), + )); } + } else if !line.contains("OK") { + return Err(Error::CommandReplyParseFailed(format!( + "received unexpected reply line '{}'", + line + ))); } - code => return Err(Error::CommandFailed(code, reply.reply_lines)), } if flags.discard_pk { @@ -591,12 +568,7 @@ impl LegacyTorController { } pub fn del_onion(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> { - let reply = self.del_onion_cmd(service_id)?; - - match reply.status_code { - 250u32 => Ok(()), - code => Err(Error::CommandFailed(code, reply.reply_lines)), - } + self.del_onion_cmd(service_id).and_then(reply_ok).map(|_| ()) } // more specific encapulsation of specific command invocations From 79c124ef7d238f04048cde821ab3359d72f9fb32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Mon, 9 Jun 2025 21:34:07 +0200 Subject: [PATCH 162/184] tor-interface: send commands in one, instead of two, writes Before: write(3, "AUTHENTICATE \"\"", 15) = 15 write(3, "\r\n", 2) = 2 recvfrom(3, "250 OK\r\n", 1024, 0, NULL, NULL) = 8 write(3, "GETINFO version", 15) = 15 recvfrom(3, "250-version=0.4.8.16\r\n250 OK\r\n", 1024, 0, NULL, NULL) = 30 write(3, "SETEVENTS STATUS_CLIENT HS_DESC", 31) = 31 After: writev(3, [{iov_base="AUTHENTICATE \"\"", iov_len=15}, {iov_base="\r\n", iov_len=2}], 2) = 17 recvfrom(3, "250 OK\r\n", 1024, 0, NULL, NULL) = 8 writev(3, [{iov_base="GETINFO version", iov_len=15}, {iov_base="\r\n", iov_len=2}], 2) = 17 recvfrom(3, "250-version=0.4.8.16\r\n250 OK\r\n", 1024, 0, NULL, NULL) = 30 writev(3, [{iov_base="SETEVENTS STATUS_CLIENT HS_DESC", iov_len=31}, {iov_base="\r\n", iov_len=2}], 2) = 33 --- tor-interface/src/legacy_tor_control_stream.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tor-interface/src/legacy_tor_control_stream.rs b/tor-interface/src/legacy_tor_control_stream.rs index c63327a33..881202cf7 100644 --- a/tor-interface/src/legacy_tor_control_stream.rs +++ b/tor-interface/src/legacy_tor_control_stream.rs @@ -1,7 +1,7 @@ // standard use std::collections::VecDeque; use std::default::Default; -use std::io::{ErrorKind, Read, Write}; +use std::io::{ErrorKind, IoSlice, Read, Write}; use std::net::{SocketAddr, TcpStream}; use std::option::Option; use std::string::ToString; @@ -254,10 +254,24 @@ impl LegacyControlStream { } pub fn write(&mut self, cmd: &str) -> Result<(), Error> { - if let Err(err) = write!(self.stream, "{}\r\n", cmd) { + if let Err(err) = write_all_vectored(&mut self.stream, &mut [IoSlice::new(cmd.as_bytes()), IoSlice::new(b"\r\n")]) { self.closed_by_remote = true; return Err(Error::WriteFailed(err)); } Ok(()) } } + +// Implementation taken from std::io::Read::write_all_vectored() +// TODO: remove once stabilised +fn write_all_vectored(write: &mut W, mut bufs: &mut [IoSlice<'_>]) -> std::io::Result<()> { + while !bufs.is_empty() { + match write.write_vectored(bufs) { + Ok(0) => return Err(std::io::Error::new(std::io::ErrorKind::WriteZero, "failed to write whole buffer")), + Ok(n) => IoSlice::advance_slices(&mut bufs, n), + Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {} + Err(e) => return Err(e), + } + } + Ok(()) +} From bf2274cc0d3e715e70ac6476024e2af2691bb152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Mon, 9 Jun 2025 22:00:52 +0200 Subject: [PATCH 163/184] tor-interface: support cookie auth in LegacyTorClientConfig --- tor-interface/src/legacy_tor_client.rs | 31 +++++++++++++++------ tor-interface/src/legacy_tor_controller.rs | 32 +++++++++++++++++++++- tor-interface/tests/tor_provider.rs | 2 +- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 3cda2199d..a68e496c3 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -74,6 +74,9 @@ pub enum Error { #[error("invalid circuit token")] CircuitTokenInvalid(), + #[error("unable to read cookie file: {1:?}")] + CookieReadingFailed(#[source] std::io::Error, PathBuf), + #[error("unable to connect to socks listener")] Socks5ConnectionFailed(#[source] std::io::Error), @@ -165,10 +168,17 @@ pub enum LegacyTorClientConfig { SystemTor { tor_socks_addr: SocketAddr, tor_control_addr: SocketAddr, - tor_control_passwd: String, + tor_control_auth: Option, }, } +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum TorAuth { + Password(String), + Cookie(PathBuf), + CookieData([u8; 32]), +} + // // LegacyTorClient // @@ -193,8 +203,8 @@ pub struct LegacyTorClient { impl LegacyTorClient { /// Construct a new `LegacyTorClient` from a [`LegacyTorClientConfig`]. - pub fn new(config: LegacyTorClientConfig) -> Result { - let (daemon, mut controller, password, socks_listener) = match &config { + pub fn new(mut config: LegacyTorClientConfig) -> Result { + let (daemon, mut controller, auth, socks_listener) = match &mut config { LegacyTorClientConfig::BundledTor { tor_bin_path, data_directory, @@ -214,12 +224,12 @@ impl LegacyTorClient { .map_err(Error::LegacyTorControllerCreationFailed)?; let password = daemon.get_password().to_string(); - (Some(daemon), controller, password, None) + (Some(daemon), controller, Some(TorAuth::Password(password)), None) } LegacyTorClientConfig::SystemTor { tor_socks_addr, tor_control_addr, - tor_control_passwd, + tor_control_auth, } => { // open a control stream let control_stream = @@ -233,16 +243,19 @@ impl LegacyTorClient { ( None, controller, - tor_control_passwd.clone(), + tor_control_auth.take(), Some(tor_socks_addr.clone()), ) } }; // authenticate - controller - .authenticate(&password) - .map_err(Error::LegacyTorProcessAuthenticationFailed)?; + match auth { + None => controller.authenticate(""), + Some(TorAuth::Password(pass)) => controller.authenticate(&pass), + Some(TorAuth::Cookie(file)) => controller.authenticate_cookie(crate::legacy_tor_controller::read_cookie(&file).map_err(|e| Error::CookieReadingFailed(e, file))?), + Some(TorAuth::CookieData(cookie)) => controller.authenticate_cookie(cookie), + }.map_err(Error::LegacyTorProcessAuthenticationFailed)?; // min required version for v3 client auth (see control-spec.txt) let min_required_version = LegacyTorVersion { diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index c79c845a2..31eea26de 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -1,8 +1,8 @@ // standard use std::default::Default; +use std::io::{Read, Write}; use std::net::SocketAddr; use std::option::Option; -#[cfg(test)] use std::path::Path; use std::str::FromStr; use std::string::ToString; @@ -95,6 +95,22 @@ fn quoted_string(string: &str) -> String { string.replace("\\", "\\\\").replace("\"", "\\\"") } + +// All authentication cookies are 32 bytes long. Controllers MUST NOT +// use the contents of a non-32-byte-long file as an authentication +// cookie. +pub(crate) fn read_cookie(from: &Path) -> std::io::Result<[u8; 32]> { + let mut f = std::fs::File::open(from)?; + let mut ret = [0u8; 32]; + f.read_exact(&mut ret[..])?; + let mut nonce = [0u8; 1]; + if f.read_exact(&mut nonce[..]).is_ok() { + Err(std::io::Error::new(std::io::ErrorKind::FileTooLarge, "cookies are 32 bytes")) + } else { + Ok(ret) + } +} + fn reply_ok(reply: Reply) -> Result { match reply.status_code { 250u32 => Ok(reply), @@ -310,6 +326,16 @@ impl LegacyTorController { self.write_command(&command) } + // AUTHENTICATE (3.5) + fn authenticate_cmd_cookie(&mut self, cookie: [u8; 32]) -> Result { + let mut command = b"AUTHENTICATE "[..].to_owned(); + for b in cookie { + write!(&mut command, "{:02x}", b).map_err(|e| Error::InvalidCommandArguments(e.to_string()))?; + } + + self.write_command(unsafe { str::from_utf8_unchecked(&command) }) + } + // GETINFO (3.9) fn getinfo_cmd(&mut self, keywords: &[&str]) -> Result { if keywords.is_empty() { @@ -465,6 +491,10 @@ impl LegacyTorController { self.authenticate_cmd(password).and_then(reply_ok).map(|_| ()) } + pub fn authenticate_cookie(&mut self, data: [u8; 32]) -> Result<(), Error> { + self.authenticate_cmd_cookie(data).and_then(reply_ok).map(|_| ()) + } + pub fn getinfo(&mut self, keywords: &[&str]) -> Result, Error> { let reply = self.getinfo_cmd(keywords).and_then(reply_ok)?; diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index cac07bc05..7f40f7685 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -168,7 +168,7 @@ fn build_system_legacy_tor_provider( let tor_config = LegacyTorClientConfig::SystemTor { tor_socks_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{socks_port}").as_str())?, tor_control_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{control_port}").as_str())?, - tor_control_passwd: "password".to_string(), + tor_control_auth: Some(TorAuth::Password("password".to_string())), }; let tor_provider = Box::new(LegacyTorClient::new(tor_config)?); From 3bba669e0031bda87ee857019c9e5f88cf81b100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Mon, 9 Jun 2025 23:43:10 +0200 Subject: [PATCH 164/184] tor-interface: use COOKIEFILE if provided Transaction log on auto-auth: writev(3, [{iov_base="PROTOCOLINFO 1", iov_len=14}, {iov_base="\r\n", iov_len=2}], 2) = 16 recvfrom(3, "250-PROTOCOLINFO 1\r\n250-AUTH MET"..., 1024, 0, NULL, NULL) = 158 recvfrom(3, 0x55dc1327f59e, 866, 0, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable) openat(AT_FDCWD, "/home/nabijaczleweli/.tor/coo kie \\\" \320\266 \n 2", O_RDONLY|O_CLOEXEC) = 4 statx(4, "", AT_STATX_SYNC_AS_STAT|AT_EMPTY_PATH, STATX_ALL, {stx_mask=STATX_ALL|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFREG|0600, stx_size=32, ...}) = 0 read(4, "\203\245-ds\200k\27^G\255\34q\371\240\376QuE\254\226\243\247\221\375\355\313\232\26\316o\343", 32) = 32 read(4, "", 32) = 0 close(4) = 0 writev(3, [{iov_base="AUTHENTICATE 83a52d6473806b175e4"..., iov_len=77}, {iov_base="\r\n", iov_len=2}], 2) = 79 recvfrom(3, "250 OK\r\n", 1024, 0, NULL, NULL) = 8 recvfrom(3, 0x55dc1327f508, 1016, 0, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable) writev(3, [{iov_base="GETINFO version", iov_len=15}, {iov_base="\r\n", iov_len=2}], 2) = 17 recvfrom(3, "250-version=0.4.7.16\r\n250 OK\r\n", 1024, 0, NULL, NULL) = 30 recvfrom(3, 0x55dc1327f51e, 994, 0, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable) No changes if an auth method is provided --- tor-interface/src/legacy_tor_client.rs | 2 +- tor-interface/src/legacy_tor_controller.rs | 161 ++++++++++++++++++++- tor-interface/tests/tor_provider.rs | 103 +++++++++---- 3 files changed, 232 insertions(+), 34 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index a68e496c3..c4a14c8bd 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -251,7 +251,7 @@ impl LegacyTorClient { // authenticate match auth { - None => controller.authenticate(""), + None => controller.authenticate_auto(), Some(TorAuth::Password(pass)) => controller.authenticate(&pass), Some(TorAuth::Cookie(file)) => controller.authenticate_cookie(crate::legacy_tor_controller::read_cookie(&file).map_err(|e| Error::CookieReadingFailed(e, file))?), Some(TorAuth::CookieData(cookie)) => controller.authenticate_cookie(cookie), diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index 31eea26de..ed602fbcc 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -1,9 +1,9 @@ // standard use std::default::Default; -use std::io::{Read, Write}; use std::net::SocketAddr; use std::option::Option; -use std::path::Path; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; use std::str::FromStr; use std::string::ToString; #[cfg(test)] @@ -46,6 +46,9 @@ pub enum Error { #[error("failed to parse received tor version")] TorVersionParseFailed(#[source] crate::legacy_tor_version::Error), + + #[error("unable to read cookie file: {1:?}")] + CookieReadingFailed(#[source] std::io::Error, PathBuf), } // Per-command data @@ -78,6 +81,14 @@ pub(crate) enum AsyncEvent { }, } +#[derive(Default, Debug, PartialEq, Eq)] +struct ProtocolInfo { + auth_cookie: bool, + auth_safecookie: bool, + auth_null: bool, + cookiefile: PathBuf, +} + pub(crate) struct LegacyTorController { // underlying control stream control_stream: LegacyControlStream, @@ -87,6 +98,7 @@ pub(crate) struct LegacyTorController { status_event_pattern: Regex, status_event_argument_pattern: Regex, hs_desc_pattern: Regex, + protocolinfo_data: Option, } fn quoted_string(string: &str) -> String { @@ -118,6 +130,113 @@ fn reply_ok(reply: Reply) -> Result { } } + +// https://raw.githubusercontent.com/torproject/torspec/refs/heads/main/control-spec.txt +// 250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE="/home/nabijaczleweli/.tor/control_auth_cookie" +// 250-AUTH METHODS=HASHEDPASSWORD +// 250-AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="/home/nabijaczleweli/.tor/coo kie \\\" \320\266 \n 2" +// 250-AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="/home/nabijaczleweli/.tor/C/\001\002\003\004\005\006\007\010\t\n\013\014\r\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037 !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\177\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377" +// 250-AUTH METHODS=NULL +fn parse_auth_methods(auth: &str) -> ProtocolInfo { + let mut ret = ProtocolInfo::default(); + let mut two = auth["AUTH METHODS=".len()..].splitn(2, ' '); + if let Some(methods) = two.next() { + for m in methods.split(',') { + match m { + "COOKIE" => ret.auth_cookie = true, + "SAFECOOKIE" => ret.auth_safecookie = true, + "NULL" => ret.auth_null = true, + _ => {} + } + } + } + let remainder = two.next(); + if (ret.auth_cookie || ret.auth_safecookie) && remainder.map(|r| r.starts_with("COOKIEFILE=\"")).unwrap_or(false) { + let mut remainder = remainder.unwrap()["COOKIEFILE=\"".len()..].as_bytes(); + + let mut path = vec![]; + // https://datatracker.ietf.org/doc/html/rfc2822 qcontent + while let Some(mut byte) = remainder.get(0).copied() { + if byte == b'"' { + break; + } + remainder = &remainder[1..]; + if byte == b'\\' { + let mut consume = 1; + match (remainder.get(0), remainder.get(1), remainder.get(2)) { + (Some(b't'), ..) => byte = b'\t', + (Some(b'n'), ..) => byte = b'\n', + (Some(b'r'), ..) => byte = b'\r', + (Some(b'\"'), ..) => byte = b'\"', + (Some(b'\''), ..) => byte = b'\'', + (Some(b'\\'), ..) => byte = b'\\', + (Some(h @ b'0'..=b'3'), Some(t @ b'0'..=b'7'), Some(u @ b'0'..=b'7')) => { + byte = ((h - b'0') << 6) | ((t - b'0') << 3) | (u - b'0'); + consume = 3; + } + _ => { + path.clear(); + break; + } + } + remainder = &remainder[consume..]; + } + path.push(byte); + } + // On UNIX, paths are sequences of non-0 bytes. We know this. + // On tor/Win32, paths are sequences of ASCII bytes(?): https://101010.pl/@nabijaczleweli/114655491521731646 + #[cfg(unix)] + { + use std::ffi::OsString; + use std::os::unix::ffi::OsStringExt; + ret.cookiefile = OsString::from_vec(path.into()).into(); + } + #[cfg(not(unix))] + { + // TODO: string_from_utf8_lossy_owned + ret.cookiefile = String::from_utf8_lossy(&path).into(); + } + } + ret +} + +#[cfg(test)] +#[test] +fn parse_auth_methods_test() { + assert_eq!(parse_auth_methods(r####"AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE="/home/nabijaczleweli/.tor/control_auth_cookie""####), ProtocolInfo { + auth_cookie: true, + auth_safecookie: true, + auth_null: false, + cookiefile: Path::new("/home/nabijaczleweli/.tor/control_auth_cookie").to_owned(), + }); + assert_eq!(parse_auth_methods(r####"AUTH METHODS=HASHEDPASSWORD"####), ProtocolInfo::default()); + assert_eq!(parse_auth_methods(r####"AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="/home/nabijaczleweli/.tor/coo kie \\\" \320\266 \n 2""####), ProtocolInfo { + auth_cookie: true, + auth_safecookie: true, + auth_null: false, + cookiefile: Path::new("/home/nabijaczleweli/.tor/coo kie \\\" ж \n 2").to_owned(), + }); + #[cfg(unix)] + { + use std::ffi::OsString; + use std::os::unix::ffi::OsStringExt; + let mut buf = b"/home/nabijaczleweli/.tor/C/"[..].to_owned(); + for b in 1..=0xFF { + buf.push(b); + } + assert_eq!(parse_auth_methods(r####"AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="/home/nabijaczleweli/.tor/C/\001\002\003\004\005\006\007\010\t\n\013\014\r\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037 !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\177\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377""####), ProtocolInfo { + auth_cookie: true, + auth_safecookie: true, + auth_null: false, + cookiefile: OsString::from_vec(buf).into(), + }); + } + assert_eq!(parse_auth_methods(r####"AUTH METHODS=NULL"####), ProtocolInfo { + auth_null: true, + ..ProtocolInfo::default() + }); +} + impl LegacyTorController { pub fn new(control_stream: LegacyControlStream) -> Result { let status_event_pattern = @@ -137,6 +256,7 @@ impl LegacyTorController { status_event_pattern, status_event_argument_pattern, hs_desc_pattern, + protocolinfo_data: None, }) } @@ -336,6 +456,11 @@ impl LegacyTorController { self.write_command(unsafe { str::from_utf8_unchecked(&command) }) } + // PROTOCOLINFO (3.21) + fn protocolinfo_cmd(&mut self) -> Result { + self.write_command("PROTOCOLINFO 1") + } + // GETINFO (3.9) fn getinfo_cmd(&mut self, keywords: &[&str]) -> Result { if keywords.is_empty() { @@ -491,10 +616,42 @@ impl LegacyTorController { self.authenticate_cmd(password).and_then(reply_ok).map(|_| ()) } + fn ensure_protocolinfo(&mut self) { + if self.protocolinfo_data.is_some() { + return; + } + + // https://raw.githubusercontent.com/torproject/torspec/refs/heads/main/control-spec.txt + // 250-VERSION Tor=\"0.4.7.16\" + match self.protocolinfo_cmd() { + Ok(reply) if reply.status_code == 250 => { + self.protocolinfo_data = Some(reply.reply_lines.iter() + .find(|l| l.starts_with("AUTH METHODS=")) + .map(|auth| parse_auth_methods(auth)) + .unwrap_or_default()); + } + _ => self.protocolinfo_data = Some(Default::default()), + } + } + pub fn authenticate_cookie(&mut self, data: [u8; 32]) -> Result<(), Error> { self.authenticate_cmd_cookie(data).and_then(reply_ok).map(|_| ()) } + pub fn authenticate_auto(&mut self) -> Result<(), Error> { + self.ensure_protocolinfo(); + let Some(pi) = self.protocolinfo_data.as_ref() + else { unreachable!() }; + + if pi.auth_null { + self.authenticate("") + } else if pi.auth_cookie && pi.cookiefile != Path::new("") { + self.authenticate_cookie(read_cookie(&pi.cookiefile).map_err(|e| Error::CookieReadingFailed(e, pi.cookiefile.clone()))?) + } else { + self.authenticate("") // fallback + } + } + pub fn getinfo(&mut self, keywords: &[&str]) -> Result, Error> { let reply = self.getinfo_cmd(keywords).and_then(reply_ok)?; diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 7f40f7685..e5791cffa 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -109,14 +109,13 @@ impl Drop for TorProcess { let _ = self.child.kill(); } } - -#[cfg(test)] #[cfg(feature = "legacy-tor-provider")] -fn build_system_legacy_tor_provider( +fn build_system_legacy_tor &mut Command>( name: &str, control_port: u16, socks_port: u16, -) -> anyhow::Result<(Box, TorProcess)> { + auth: A +) -> anyhow::Result { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); @@ -131,7 +130,7 @@ fn build_system_legacy_tor_provider( let _ = File::create(&torrc)?; } - let tor_daemon = TorProcess { child: Command::new(tor_path) + let tor_daemon = TorProcess { child: auth(data_path.clone(), Command::new(tor_path) .stdout(Stdio::null()) .stdin(Stdio::null()) .stderr(Stdio::null()) @@ -150,20 +149,32 @@ fn build_system_legacy_tor_provider( // control port .arg("ControlPort") .arg(control_port.to_string()) - // password: foobar1 - .arg("HashedControlPassword") - .arg("16:E807DCE69AFE9979600760C9758B95ADB2F95E8740478AEA5356C95358") // socks port .arg("SocksPort") .arg(socks_port.to_string()) // tor process will shut down after this process shuts down // to avoid orphaned tor daemon .arg("__OwningControllerProcess") - .arg(process::id().to_string()) + .arg(process::id().to_string())) .spawn()? }; // give daemons time to start std::thread::sleep(std::time::Duration::from_secs(5)); + Ok(tor_daemon) +} + +#[cfg(test)] +#[cfg(feature = "legacy-tor-provider")] +fn build_system_legacy_tor_provider_password( + name: &str, + control_port: u16, + socks_port: u16, +) -> anyhow::Result<(Box, TorProcess)> { + let tor_daemon = build_system_legacy_tor(name, control_port, socks_port, |_, cmd| + // password: foobar1 + cmd.arg("HashedControlPassword") + .arg("16:E807DCE69AFE9979600760C9758B95ADB2F95E8740478AEA5356C95358") + )?; let tor_config = LegacyTorClientConfig::SystemTor { tor_socks_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{socks_port}").as_str())?, @@ -175,6 +186,32 @@ fn build_system_legacy_tor_provider( Ok((tor_provider, tor_daemon)) } +#[cfg(test)] +#[cfg(feature = "legacy-tor-provider")] +fn build_system_legacy_tor_provider_cookie( + name: &str, + control_port: u16, + socks_port: u16, +) -> anyhow::Result<(Box, TorProcess)> { + let mut cookiefile = std::path::PathBuf::new(); + let tor_daemon = build_system_legacy_tor(name, control_port, socks_port, |data_dir, cmd| { + cookiefile = data_dir.join("cookie"); + cmd.arg("CookieAuthentication") + .arg("1") + .arg("CookieAuthFile") + .arg(&cookiefile) + })?; + + let tor_config = LegacyTorClientConfig::SystemTor { + tor_socks_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{socks_port}").as_str())?, + tor_control_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{control_port}").as_str())?, + tor_control_auth: Some(TorAuth::Cookie(cookiefile)), + }; + let tor_provider = Box::new(LegacyTorClient::new(tor_config)?); + + Ok((tor_provider, tor_daemon)) +} + #[cfg(test)] #[cfg(feature = "arti-client-tor-provider")] fn build_arti_client_tor_provider(runtime: Arc, name: &str) -> anyhow::Result> { @@ -615,17 +652,19 @@ fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { #[serial] #[cfg(feature = "legacy-tor-provider")] fn test_system_legacy_onion_service() -> anyhow::Result<()> { - let server_provider = build_system_legacy_tor_provider( - "test_system_legacy_onion_service_server", - 9251u16, - 9250u16)?; - - let client_provider = build_system_legacy_tor_provider( - "test_system_legacy_onion_service_client", - 9351u16, - 9350u16)?; - - basic_onion_service_test(server_provider.0, client_provider.0)?; + for (backend, name) in [build_system_legacy_tor_provider_password, build_system_legacy_tor_provider_cookie].into_iter().zip(["password", "cookie"]) { + let server_provider = backend( + &format!("test_system_legacy_onion_service_server_{}", name), + 9251u16, + 9250u16)?; + + let client_provider = backend( + &format!("test_system_legacy_onion_service_client_{}", name), + 9351u16, + 9350u16)?; + + basic_onion_service_test(server_provider.0, client_provider.0)?; + } Ok(()) } @@ -634,17 +673,19 @@ fn test_system_legacy_onion_service() -> anyhow::Result<()> { #[serial] #[cfg(feature = "legacy-tor-provider")] fn test_system_legacy_authenticated_onion_service() -> anyhow::Result<()> { - let server_provider = build_system_legacy_tor_provider( - "test_system_legacy_authenticated_onion_service_server", - 9251u16, - 9250u16)?; - - let client_provider = build_system_legacy_tor_provider( - "test_system_legacy_authenticated_onion_service_client", - 9351u16, - 9350u16)?; - - authenticated_onion_service_test(server_provider.0, client_provider.0)?; + for (backend, name) in [build_system_legacy_tor_provider_password, build_system_legacy_tor_provider_cookie].into_iter().zip(["password", "cookie"]) { + let server_provider = backend( + &format!("test_system_legacy_authenticated_onion_service_server_{}", name), + 9251u16, + 9250u16)?; + + let client_provider = backend( + &format!("test_system_legacy_authenticated_onion_service_client_{}", name), + 9351u16, + 9350u16)?; + + authenticated_onion_service_test(server_provider.0, client_provider.0)?; + } Ok(()) } From 3462ea7ca0afc0acdad7beabc43abb997aef7838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 10 Jun 2025 01:11:51 +0200 Subject: [PATCH 165/184] tor-interface: use SAFECOOKIE authentication when possible This avoids leaking the secret and using an unknown Tor server COOKIE may supposedly get removed in future hmac is the only new dependency, sha2 was already in the dep tree --- tor-interface/Cargo.toml | 4 +- tor-interface/src/legacy_tor_controller.rs | 100 ++++++++++++++++++++- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index c28b92366..03f6f5848 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -35,6 +35,8 @@ tor-keymgr = { version = "0.31.0", optional = true, features = ["keymgr"] } tor-llcrypto = { version = "0.31.0", features = ["relay"] } tor-proto = { version = "0.31.0", features = ["stream-ctrl"], optional = true } tor-rtcompat = { version = "0.31.0", optional = true } +hmac = { version = "0.12", optional = true } +sha2 = { version = "0.10", optional = true } [dev-dependencies] anyhow = "1.0" @@ -45,4 +47,4 @@ which = "4.4" arti-client-tor-provider = ["arti-client", "tokio", "tokio-stream", "tor-cell", "tor-config", "tor-hsservice", "tor-keymgr", "tor-proto", "tor-rtcompat"] arti-tor-provider = ["arti-rpc-client-core"] mock-tor-provider = [] -legacy-tor-provider = [] +legacy-tor-provider = ["hmac", "sha2"] diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index ed602fbcc..5fdcfa946 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -10,6 +10,9 @@ use std::string::ToString; use std::time::{Duration, Instant}; // extern crates +use hmac::Mac; +use rand::rngs::OsRng; +use rand::TryRngCore; use regex::Regex; #[cfg(test)] use serial_test::serial; @@ -49,6 +52,15 @@ pub enum Error { #[error("unable to read cookie file: {1:?}")] CookieReadingFailed(#[source] std::io::Error, PathBuf), + + #[error("[SAFE]COOKIE authentication not supported")] + CookiesNotSupported(), + + #[error("impostor sent invalid SAFECOOKIE HMAC")] + BadCookieHash(), + + #[error("failed to generate random data")] + RngError(#[source] ::Error), } // Per-command data @@ -123,6 +135,23 @@ pub(crate) fn read_cookie(from: &Path) -> std::io::Result<[u8; 32]> { } } +fn tonibble(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => 0xA + (c - b'a'), + b'A'..=b'F' => 0xA + (c - b'A'), + _ => unreachable!(), + } +} + +fn hmac_sha256(key: &str, blob1: &[u8], blob2: &[u8], blob3: &[u8]) -> hmac::Hmac { + let mut hmac = hmac::Hmac::new_from_slice(key.as_bytes()).unwrap(); + hmac.update(blob1); + hmac.update(blob2); + hmac.update(blob3); + hmac +} + fn reply_ok(reply: Reply) -> Result { match reply.status_code { 250u32 => Ok(reply), @@ -447,7 +476,7 @@ impl LegacyTorController { } // AUTHENTICATE (3.5) - fn authenticate_cmd_cookie(&mut self, cookie: [u8; 32]) -> Result { + fn authenticate_cmd_cookie(&mut self, cookie: &[u8]) -> Result { let mut command = b"AUTHENTICATE "[..].to_owned(); for b in cookie { write!(&mut command, "{:02x}", b).map_err(|e| Error::InvalidCommandArguments(e.to_string()))?; @@ -456,6 +485,16 @@ impl LegacyTorController { self.write_command(unsafe { str::from_utf8_unchecked(&command) }) } + // AUTHCHALLENGE (3.24) + fn authchallenge_cmd(&mut self, client_nonce: &[u8]) -> Result { + let mut command = b"AUTHCHALLENGE SAFECOOKIE "[..].to_owned(); + for b in client_nonce { + write!(&mut command, "{:02x}", b).map_err(|e| Error::InvalidCommandArguments(e.to_string()))?; + } + + self.write_command(unsafe { str::from_utf8_unchecked(&command) }) + } + // PROTOCOLINFO (3.21) fn protocolinfo_cmd(&mut self) -> Result { self.write_command("PROTOCOLINFO 1") @@ -634,8 +673,63 @@ impl LegacyTorController { } } + fn authenticate_safecookie(&mut self, data: [u8; 32]) -> Result { + let mut client_nonce = [0u8; 32]; + OsRng.try_fill_bytes(&mut client_nonce).map_err(Error::RngError)?; + let reply = self.authchallenge_cmd(&client_nonce).and_then(reply_ok)?; + + if reply.reply_lines.len() != 1 || !reply.reply_lines[0].starts_with("AUTHCHALLENGE SERVERHASH=") { + return Err(Error::CommandReplyParseFailed(reply.reply_lines.get(0).cloned().unwrap_or_else(|| "[no response]".to_string()))); + } + let mut chunks = reply.reply_lines[0]["AUTHCHALLENGE SERVERHASH=".len()..].splitn(2, ' '); + + let sh = chunks.next().map(|sh| sh.as_bytes()) + .filter(|sh| sh.len() % 64 == 0) + .filter(|sh| sh.iter().all(|c| matches!(c, b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F'))) + .ok_or_else(|| Error::CommandReplyParseFailed(reply.reply_lines[0].clone()))?; + let mut server_hash = Vec::new(); + server_hash.resize(sh.len() / 2, 0); + for (hilo, dest) in sh.chunks_exact(2).zip(server_hash.iter_mut()) { + *dest = tonibble(hilo[0]) << 4 | tonibble(hilo[1]); + } + + let sn = chunks.next().map(|sh| sh.as_bytes()) + .filter(|sn| sn.starts_with(b"SERVERNONCE=")) + .map(|sn| &sn[b"SERVERNONCE=".len()..]) + .filter(|sh| sh.len() == 64) + .filter(|sh| sh.iter().all(|c| matches!(c, b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F'))) + .ok_or_else(|| Error::CommandReplyParseFailed(reply.reply_lines[0].clone()))?; + let mut server_nonce = [0u8; 32]; + for (hilo, dest) in sn.chunks_exact(2).zip(server_nonce.iter_mut()) { + *dest = tonibble(hilo[0]) << 4 | tonibble(hilo[1]); + } + + hmac_sha256("Tor safe cookie authentication server-to-controller hash", &data, &client_nonce, &server_nonce) + .verify_slice(&server_hash).map_err(|_| Error::BadCookieHash())?; + + self.authenticate_cmd_cookie( + hmac_sha256("Tor safe cookie authentication controller-to-server hash", &data, &client_nonce, &server_nonce).finalize().into_bytes().as_slice()) + } + pub fn authenticate_cookie(&mut self, data: [u8; 32]) -> Result<(), Error> { - self.authenticate_cmd_cookie(data).and_then(reply_ok).map(|_| ()) + self.ensure_protocolinfo(); + let Some(pi) = self.protocolinfo_data.as_ref() + else { unreachable!() }; + + let reply = if pi.auth_safecookie && pi.auth_cookie { + match self.authenticate_safecookie(data) { + r @ Ok(_) | r @ Err(Error::BadCookieHash()) => r?, + _ => self.authenticate_cmd_cookie(&data)?, + } + } else if pi.auth_safecookie { + self.authenticate_safecookie(data)? + } else if pi.auth_cookie { + self.authenticate_cmd_cookie(&data)? + } else { + return Err(Error::CookiesNotSupported()); + }; + + reply_ok(reply).map(|_| ()) } pub fn authenticate_auto(&mut self) -> Result<(), Error> { @@ -645,7 +739,7 @@ impl LegacyTorController { if pi.auth_null { self.authenticate("") - } else if pi.auth_cookie && pi.cookiefile != Path::new("") { + } else if (pi.auth_cookie || pi.auth_safecookie) && pi.cookiefile != Path::new("") { self.authenticate_cookie(read_cookie(&pi.cookiefile).map_err(|e| Error::CookieReadingFailed(e, pi.cookiefile.clone()))?) } else { self.authenticate("") // fallback From 57714e686e30c26b797a17ac427ae9650104f9f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 10 Jun 2025 21:47:32 +0200 Subject: [PATCH 166/184] tor-interface: simplify post-auth setup flow if possible Don't send GETINFO version if we already PROTOCOLINFOed and got VERSION back --- tor-interface/src/legacy_tor_controller.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index 5fdcfa946..f725c5e82 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -111,6 +111,7 @@ pub(crate) struct LegacyTorController { status_event_argument_pattern: Regex, hs_desc_pattern: Regex, protocolinfo_data: Option, + version: Option, } fn quoted_string(string: &str) -> String { @@ -286,6 +287,7 @@ impl LegacyTorController { status_event_argument_pattern, hs_desc_pattern, protocolinfo_data: None, + version: None, }) } @@ -664,6 +666,10 @@ impl LegacyTorController { // 250-VERSION Tor=\"0.4.7.16\" match self.protocolinfo_cmd() { Ok(reply) if reply.status_code == 250 => { + if let Some(vers) = reply.reply_lines.iter().find(|l| l.starts_with("VERSION Tor=\"")) { + self.version = vers["VERSION Tor=\"".len()..].split('\"').next().and_then(|s| <_>::from_str(s).ok()); + } + self.protocolinfo_data = Some(reply.reply_lines.iter() .find(|l| l.starts_with("AUTH METHODS=")) .map(|auth| parse_auth_methods(auth)) @@ -893,6 +899,10 @@ impl LegacyTorController { } pub fn getinfo_version(&mut self) -> Result { + if let Some(vers) = self.version.take() { + return Ok(vers); + } + let response = self.getinfo(&["version"])?; for (key, value) in response.iter() { if key.as_str() == "version" { From 95464b133cb5bf69ec184484f74d8b6da769f58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 11 Jun 2025 17:17:12 +0200 Subject: [PATCH 167/184] tor-interface: move instead of unnecessary cloning --- tor-interface/src/legacy_tor_client.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index c4a14c8bd..22ea33171 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -490,7 +490,6 @@ impl TorProvider for LegacyTorClient { .controller .wait_async_events() .map_err(Error::WaitAsyncEventsFailed)? - .iter() { match async_event { AsyncEvent::StatusClient { @@ -502,11 +501,11 @@ impl TorProvider for LegacyTorClient { let mut progress: u32 = 0; let mut tag: String = Default::default(); let mut summary: String = Default::default(); - for (key, val) in arguments.iter() { + for (key, val) in arguments { match key.as_str() { "PROGRESS" => progress = val.parse().unwrap_or(0u32), - "TAG" => tag = val.to_string(), - "SUMMARY" => summary = val.to_string(), + "TAG" => tag = val, + "SUMMARY" => summary = val, _ => {} // ignore unexpected arguments } } @@ -524,7 +523,7 @@ impl TorProvider for LegacyTorClient { AsyncEvent::HsDesc { action, hs_address } => { if action == "UPLOADED" { events.push(TorEvent::OnionServicePublished { - service_id: hs_address.clone(), + service_id: hs_address, }); } } From 78c79a0ea54d79b9b0192dbb9d262d6670b67728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Mon, 30 Jun 2025 22:04:04 +0200 Subject: [PATCH 168/184] tor-provider: genericise TorProvider over Stream/Listener, OnionListener over Stream, OnionStream --- tor-interface/src/arti_client_tor_client.rs | 11 +- tor-interface/src/arti_tor_client.rs | 9 +- tor-interface/src/legacy_tor_client.rs | 11 +- tor-interface/src/mock_tor_client.rs | 15 +- tor-interface/src/tor_provider.rs | 343 ++++++++++++++++++-- tor-interface/tests/tor_provider.rs | 43 ++- 6 files changed, 364 insertions(+), 68 deletions(-) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 8bd6a1932..ab979d40b 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -199,6 +199,9 @@ impl ArtiClientTorClient { } impl TorProvider for ArtiClientTorClient { + type Stream = TcpOnionStream; + type Listener = TcpOnionListener; + fn update(&mut self) -> Result, tor_provider::Error> { std::thread::sleep(std::time::Duration::from_millis(16)); match self.pending_events.lock() { @@ -294,7 +297,7 @@ impl TorProvider for ArtiClientTorClient { &mut self, target: TargetAddr, circuit: Option, - ) -> Result { + ) -> Result { // stream isolation not implemented yet if circuit.is_some() { return Err(Error::NotImplemented().into()); @@ -362,7 +365,7 @@ impl TorProvider for ArtiClientTorClient { let stream = client_stream .into_std() .map_err(Error::TcpStreamIntoFailed)?; - Ok(OnionStream { + Ok(TcpOnionStream { stream, local_addr: None, peer_addr: Some(target), @@ -374,7 +377,7 @@ impl TorProvider for ArtiClientTorClient { private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { // try to bind to a local address, let OS pick our port let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); @@ -515,7 +518,7 @@ impl TorProvider for ArtiClientTorClient { let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id, virt_port)); // onion-service is torn down when `onion_service` is dropped - Ok(OnionListener::new::>(listener, onion_addr, onion_service, |_|{})) + Ok(TcpOnionListener::new::>(listener, onion_addr, onion_service, |_|{})) } fn generate_token(&mut self) -> CircuitToken { diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs index 63e3943ad..a1f513af3 100644 --- a/tor-interface/src/arti_tor_client.rs +++ b/tor-interface/src/arti_tor_client.rs @@ -125,6 +125,9 @@ impl ArtiTorClient { } impl TorProvider for ArtiTorClient { + type Stream = TcpOnionStream; + type Listener = TcpOnionListener; + fn update(&mut self) -> Result, tor_provider::Error> { std::thread::sleep(std::time::Duration::from_millis(16)); let mut tor_events = match self.pending_events.lock() { @@ -196,7 +199,7 @@ impl TorProvider for ArtiTorClient { &mut self, target: TargetAddr, circuit_token: Option, - ) -> Result { + ) -> Result { if !self.bootstrapped { return Err(Error::ArtiNotBootstrapped().into()); } @@ -223,7 +226,7 @@ impl TorProvider for ArtiTorClient { let stream = self.rpc_conn.open_stream(None, (host.as_str(), port), isolation) .map_err(Error::ArtiOpenStreamFailed)?; - Ok(OnionStream { + Ok(TcpOnionStream { stream, local_addr: None, peer_addr: Some(target), @@ -235,7 +238,7 @@ impl TorProvider for ArtiTorClient { _private_key: &Ed25519PrivateKey, _virt_port: u16, _authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { Err(Error::NotImplemented().into()) } diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 22ea33171..00ba7b9aa 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -469,6 +469,9 @@ impl LegacyTorClient { } impl TorProvider for LegacyTorClient { + type Stream = TcpOnionStream; + type Listener = TcpOnionListener; + fn update(&mut self) -> Result, tor_provider::Error> { let mut i = 0; while i < self.onion_services.len() { @@ -587,7 +590,7 @@ impl TorProvider for LegacyTorClient { &mut self, target: TargetAddr, circuit: Option, - ) -> Result { + ) -> Result { if !self.bootstrapped { return Err(Error::LegacyTorNotBootstrapped().into()); } @@ -638,7 +641,7 @@ impl TorProvider for LegacyTorClient { } .map_err(Error::Socks5ConnectionFailed)?; - Ok(OnionStream { + Ok(TcpOnionStream { stream: stream.into_inner(), local_addr: None, peer_addr: Some(target), @@ -651,7 +654,7 @@ impl TorProvider for LegacyTorClient { private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { if !self.bootstrapped { return Err(Error::LegacyTorNotBootstrapped().into()); } @@ -691,7 +694,7 @@ impl TorProvider for LegacyTorClient { self.onion_services .push((service_id, Arc::clone(&is_active))); - Ok(OnionListener::new(listener, onion_addr, is_active, |is_active| { + Ok(TcpOnionListener::new(listener, onion_addr, is_active, |is_active| { is_active.store(false, atomic::Ordering::Relaxed); })) } diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index d9a32e53f..d5b9a27c9 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -65,7 +65,7 @@ impl MockTorNetwork { service_id: &V3OnionServiceId, virt_port: u16, client_auth: Option<&X25519PublicKey>, - ) -> Result { + ) -> Result<::Stream, Error> { let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port)); match &mut self.onion_services { @@ -83,7 +83,7 @@ impl MockTorNetwork { } if let Ok(stream) = TcpStream::connect(socket_addr) { - Ok(OnionStream { + Ok(TcpOnionStream { stream, local_addr: None, peer_addr: Some(TargetAddr::OnionService(onion_addr)), @@ -168,6 +168,9 @@ impl Default for MockTorClient { } impl TorProvider for MockTorClient { + type Stream = TcpOnionStream; + type Listener = TcpOnionListener; + fn update(&mut self) -> Result, tor_provider::Error> { match MOCK_TOR_NETWORK.lock() { Ok(mut mock_tor_network) => { @@ -241,7 +244,7 @@ impl TorProvider for MockTorClient { &mut self, target: TargetAddr, _circuit: Option, - ) -> Result { + ) -> Result { let (service_id, virt_port) = match target { TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3 { service_id, @@ -253,7 +256,7 @@ impl TorProvider for MockTorClient { .local_addr() .expect("loopback local_addr failed"), ) { - return Ok(OnionStream { + return Ok(TcpOnionStream { stream, local_addr: None, peer_addr: Some(target_address), @@ -278,7 +281,7 @@ impl TorProvider for MockTorClient { private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, - ) -> Result { + ) -> Result { // convert inputs to relevant types let service_id = V3OnionServiceId::from_private_key(private_key); let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port)); @@ -315,7 +318,7 @@ impl TorProvider for MockTorClient { .push(TorEvent::OnionServicePublished { service_id }); - Ok(OnionListener::new(listener, onion_addr, is_active, |is_active| { + Ok(TcpOnionListener::new(listener, onion_addr, is_active, |is_active| { is_active.store(false, atomic::Ordering::Relaxed); })) } diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index ae26a4bf2..d0dbacfe2 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -7,6 +7,10 @@ use std::net::{SocketAddr, TcpListener, TcpStream}; use std::ops::{Deref, DerefMut}; use std::str::FromStr; use std::sync::OnceLock; +#[cfg(unix)] +use std::os::unix::io::{IntoRawFd, RawFd}; +#[cfg(windows)] +use std::os::windows::io::{IntoRawSocket, RawSocket}; // extern crates use domain::base::name::Name; @@ -275,70 +279,232 @@ pub type CircuitToken = usize; // Onion Stream // +#[cfg(unix)] +pub type OnionStreamIntoRaw = RawFd; +#[cfg(windows)] +pub type OnionStreamIntoRaw = RawSocket; + /// A wrapper around a [`std::net::TcpStream`] with some Tor-specific customisations /// /// An onion-listener can be constructed using the [`TorProvider::connect()`] method. +pub trait OnionStream: Send + Read + Write + std::fmt::Debug { + /// Returns the target address of the remote peer of this onion connection. + fn peer_addr(&self) -> Option; + + /// Returns the onion address of the local connection for an incoming onion-service connection. Returns `None` for outgoing connections. + fn local_addr(&self) -> Option; + + /// Tries to clone the underlying connection and data. A simple pass-through to [`std::net::TcpStream::try_clone()`]. + fn try_clone(&self) -> std::io::Result where Self: Sized; + + /// Moves the underlying `TcpStream` into or out of nonblocking mode. + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()>; + + /// Consume stream and return the underlying raw handle. + fn into_raw(self) -> OnionStreamIntoRaw; +} + #[derive(Debug)] -pub struct OnionStream { +pub struct TcpOnionStream { pub(crate) stream: TcpStream, pub(crate) local_addr: Option, pub(crate) peer_addr: Option, } -impl Deref for OnionStream { +impl Deref for TcpOnionStream { type Target = TcpStream; fn deref(&self) -> &Self::Target { &self.stream } } -impl DerefMut for OnionStream { +impl DerefMut for TcpOnionStream { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.stream } } -impl From for TcpStream { - fn from(onion_stream: OnionStream) -> Self { +impl From for TcpStream { + fn from(onion_stream: TcpOnionStream) -> Self { onion_stream.stream } } -impl Read for OnionStream { - fn read(&mut self, buf: &mut [u8]) -> Result { +impl Read for TcpOnionStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.stream.read(buf) } + fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result { + self.stream.read_vectored(bufs) + } + fn read_to_end(&mut self, buf: &mut Vec) -> std::io::Result { + self.stream.read_to_end(buf) + } + fn read_to_string(&mut self, buf: &mut String) -> std::io::Result { + self.stream.read_to_string(buf) + } + fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> { + self.stream.read_exact(buf) + } } -impl Write for OnionStream { - fn write(&mut self, buf: &[u8]) -> Result { +impl Write for TcpOnionStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { self.stream.write(buf) } - - fn flush(&mut self) -> Result<(), std::io::Error> { + fn flush(&mut self) -> std::io::Result<()> { self.stream.flush() } + fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result { + self.stream.write_vectored(bufs) + } + fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + self.stream.write_all(buf) + } + fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> { + self.stream.write_fmt(fmt) + } } -impl OnionStream { - /// Returns the target address of the remote peer of this onion connection. - pub fn peer_addr(&self) -> Option { +impl OnionStream for TcpOnionStream { + fn peer_addr(&self) -> Option { self.peer_addr.clone() } - /// Returns the onion address of the local connection for an incoming onion-service connection. Returns `None` for outgoing connections. - pub fn local_addr(&self) -> Option { + fn local_addr(&self) -> Option { self.local_addr.clone() } - /// Tries to clone the underlying connection and data. A simple pass-through to [`std::net::TcpStream::try_clone()`]. - pub fn try_clone(&self) -> Result { + fn try_clone(&self) -> std::io::Result where Self: Sized { Ok(Self { stream: self.stream.try_clone()?, local_addr: self.local_addr.clone(), peer_addr: self.peer_addr.clone(), }) } + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + self.stream.set_nonblocking(nonblocking) + } + + fn into_raw(self) -> OnionStreamIntoRaw { + #[cfg(unix)] + return self.stream.into_raw_fd(); + #[cfg(windows)] + return self.stream.into_raw_stream(); + } +} + +pub struct BoxOnionStream { + data: Box, + + peer_addr: fn(&Box) -> Option, + local_addr: fn(&Box) -> Option, + try_clone: fn(&Box) -> std::io::Result, + set_nonblocking: fn(&Box, bool) -> std::io::Result<()>, + into_raw: fn(Box) -> OnionStreamIntoRaw, + + read: fn(&mut Box, &mut [u8]) -> std::io::Result, + read_vectored: fn(&mut Box, &mut [std::io::IoSliceMut<'_>]) -> std::io::Result, + read_to_end: fn(&mut Box, &mut Vec) -> std::io::Result, + read_to_string: fn(&mut Box, &mut String) -> std::io::Result, + read_exact: fn(&mut Box, &mut [u8]) -> std::io::Result<()>, + + write: fn(&mut Box, &[u8]) -> std::io::Result, + flush: fn(&mut Box) -> std::io::Result<()>, + write_vectored: fn(&mut Box, &[std::io::IoSlice<'_>]) -> std::io::Result, + write_all: fn(&mut Box, &[u8]) -> std::io::Result<()>, + write_fmt: fn(&mut Box, std::fmt::Arguments<'_>) -> std::io::Result<()>, +} + +impl std::fmt::Debug for BoxOnionStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_str("BoxOnionStream") + } +} + +impl BoxOnionStream { + pub fn new(s: S) -> Self { + Self { + data: Box::new(s), + + peer_addr: |slf| slf.downcast_ref::().unwrap().peer_addr(), + local_addr: |slf| slf.downcast_ref::().unwrap().local_addr(), + try_clone: |slf| slf.downcast_ref::().unwrap().try_clone().map(BoxOnionStream::new), + set_nonblocking: |slf, nonblocking| slf.downcast_ref::().unwrap().set_nonblocking(nonblocking), + into_raw: |slf| slf.downcast::().unwrap().into_raw(), + + read: |slf, buf| slf.downcast_mut::().unwrap().read(buf), + read_vectored: |slf, bufs| slf.downcast_mut::().unwrap().read_vectored(bufs), + read_to_end: |slf, buf| slf.downcast_mut::().unwrap().read_to_end(buf), + read_to_string: |slf, buf| slf.downcast_mut::().unwrap().read_to_string(buf), + read_exact: |slf, buf| slf.downcast_mut::().unwrap().read_exact(buf), + + write: |slf, buf| slf.downcast_mut::().unwrap().write(buf), + flush: |slf| slf.downcast_mut::().unwrap().flush(), + write_vectored: |slf, bufs| slf.downcast_mut::().unwrap().write_vectored(bufs), + write_all: |slf, buf| slf.downcast_mut::().unwrap().write_all(buf), + write_fmt: |slf, fmt| slf.downcast_mut::().unwrap().write_fmt(fmt), + } + } +} + +impl Read for BoxOnionStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + (self.read)(&mut self.data, buf) + } + fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result { + (self.read_vectored)(&mut self.data, bufs) + } + fn read_to_end(&mut self, buf: &mut Vec) -> std::io::Result { + (self.read_to_end)(&mut self.data, buf) + } + fn read_to_string(&mut self, buf: &mut String) -> std::io::Result { + (self.read_to_string)(&mut self.data, buf) + } + fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> { + (self.read_exact)(&mut self.data, buf) + } +} + +impl Write for BoxOnionStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + (self.write)(&mut self.data, buf) + } + fn flush(&mut self) -> std::io::Result<()> { + (self.flush)(&mut self.data) + } + fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result { + (self.write_vectored)(&mut self.data, bufs) + } + fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + (self.write_all)(&mut self.data, buf) + } + fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> { + (self.write_fmt)(&mut self.data, fmt) + } +} + +impl OnionStream for BoxOnionStream { + fn peer_addr(&self) -> Option { + (self.peer_addr)(&self.data) + } + + fn local_addr(&self) -> Option { + (self.local_addr)(&self.data) + } + + fn try_clone(&self) -> std::io::Result where Self: Sized { + (self.try_clone)(&self.data) + } + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + (self.set_nonblocking)(&self.data, nonblocking) + } + + fn into_raw(self) -> OnionStreamIntoRaw { + (self.into_raw)(self.data) + } } // @@ -348,15 +514,25 @@ impl OnionStream { /// A wrapper around a [`std::net::TcpListener`] with some Tor-specific customisations. /// /// An onion-listener can be constructed using the [`TorProvider::listener()`] method. -pub struct OnionListener { +pub trait OnionListener: Send { + type Stream: OnionStream; + + /// Moves the underlying `TcpListener` into or out of nonblocking mode. + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()>; + + /// Accept a new incoming connection from this listener. + fn accept(&self) -> std::io::Result>; +} + +pub struct TcpOnionListener { pub(crate) listener: TcpListener, pub(crate) onion_addr: OnionAddr, pub(crate) data: Option>, pub(crate) drop: Option) + Send>>, } -impl OnionListener { - /// Construct an `OnionListener`. The `data` and `drop` parameters are to allow custom `TorProvider` implementations their own data and cleanup procedures. +impl TcpOnionListener { + /// Construct an `TcpOnionListener`. The `data` and `drop` parameters are to allow custom `TorProvider` implementations their own data and cleanup procedures. pub(crate) fn new( listener: TcpListener, onion_addr: OnionAddr, @@ -380,16 +556,18 @@ impl OnionListener { drop, } } +} - /// Moves the underlying `TcpListener` into or out of nonblocking mode. - pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), std::io::Error> { +impl OnionListener for TcpOnionListener { + type Stream = TcpOnionStream; + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { self.listener.set_nonblocking(nonblocking) } - /// Accept a new incoming connection from this listener. - pub fn accept(&self) -> Result, std::io::Error> { + fn accept(&self) -> std::io::Result> { match self.listener.accept() { - Ok((stream, _socket_addr)) => Ok(Some(OnionStream { + Ok((stream, _socket_addr)) => Ok(Some(TcpOnionStream { stream, local_addr: Some(self.onion_addr.clone()), peer_addr: None, @@ -405,7 +583,7 @@ impl OnionListener { } } -impl Drop for OnionListener { +impl Drop for TcpOnionListener { fn drop(&mut self) { if let (Some(data), Some(mut drop)) = (self.data.take(), self.drop.take()) { drop(data) @@ -413,8 +591,41 @@ impl Drop for OnionListener { } } +pub struct BoxOnionListener { + data: Box, + + set_nonblocking: fn(&Box, bool) -> std::io::Result<()>, + accept: fn(&Box) -> std::io::Result::Stream>>, +} + +impl BoxOnionListener { + pub fn new(l: L) -> Self { + Self { + data: Box::new(l), + + set_nonblocking: |slf, nonblocking| slf.downcast_ref::().unwrap().set_nonblocking(nonblocking), + accept: |slf| slf.downcast_ref::().unwrap().accept().map(|r| r.map(BoxOnionStream::new)), + } + } +} + +impl OnionListener for BoxOnionListener { + type Stream = BoxOnionStream; + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + (self.set_nonblocking)(&self.data, nonblocking) + } + + fn accept(&self) -> std::io::Result> { + (self.accept)(&self.data) + } +} + /// The `TorProvider` trait allows for high-level Tor Network functionality. Implementations ay connect to the Tor Network, anonymously connect to both clearnet and onion-service endpoints, and host onion-services. pub trait TorProvider: Send { + type Stream: OnionStream; + type Listener: OnionListener; + /// Process and return `TorEvent`s handled by this `TorProvider`. fn update(&mut self) -> Result, Error>; /// Begin connecting to the Tor Network. @@ -438,7 +649,7 @@ pub trait TorProvider: Send { &mut self, target: TargetAddr, circuit: Option, - ) -> Result; + ) -> Result; /// Anonymously start an onion-service and return the associated [`OnionListener`]. /// ///The resulting onion-service will not be reachable by clients until [`TorProvider::update()`] returns a [`TorEvent::OnionServicePublished`] event. The optional `authorised_clients` parameter may be used to require client authorisation keys to connect to resulting onion-service. For further information, see the Tor Project's onion-services [client-auth documentation](https://community.torproject.org/onion-services/advanced/client-auth). @@ -447,9 +658,83 @@ pub trait TorProvider: Send { private_key: &Ed25519PrivateKey, virt_port: u16, authorised_clients: Option<&[X25519PublicKey]>, - ) -> Result; + ) -> Result; /// Create a new [`CircuitToken`]. fn generate_token(&mut self) -> CircuitToken; /// Releaes a previously generated [`CircuitToken`]. fn release_token(&mut self, token: CircuitToken); } + + +pub struct BoxTorProvider { + data: Box, + + update: fn(&mut Box) -> Result, Error>, + bootstrap: fn(&mut Box) -> Result<(), Error>, + add_client_auth: fn(&mut Box, &V3OnionServiceId, &X25519PrivateKey) -> Result<(), Error>, + remove_client_auth: fn(&mut Box, &V3OnionServiceId) -> Result<(), Error>, + connect: fn(&mut Box, TargetAddr, Option) -> Result<::Stream, Error>, + listener: fn(&mut Box, &Ed25519PrivateKey, u16, Option<&[X25519PublicKey]>) -> Result<::Listener, Error>, + generate_token: fn(&mut Box) -> CircuitToken, + release_token: fn(&mut Box, CircuitToken), +} + +impl BoxTorProvider { + pub fn new(p: P) -> Self { + Self { + data: Box::new(p), + + update: |slf| slf.downcast_mut::

().unwrap().update(), + bootstrap: |slf| slf.downcast_mut::

().unwrap().bootstrap(), + add_client_auth: |slf, service_id, client_auth| slf.downcast_mut::

().unwrap().add_client_auth(service_id, client_auth), + remove_client_auth: |slf, service_id| slf.downcast_mut::

().unwrap().remove_client_auth(service_id), + connect: |slf, target, circuit| slf.downcast_mut::

().unwrap().connect(target, circuit).map(BoxOnionStream::new), + listener: |slf, private_key, virt_port, authorised_clients| slf.downcast_mut::

().unwrap().listener(private_key, virt_port, authorised_clients).map(BoxOnionListener::new), + generate_token: |slf| slf.downcast_mut::

().unwrap().generate_token(), + release_token: |slf, token| slf.downcast_mut::

().unwrap().release_token(token), + } + } +} + +impl TorProvider for BoxTorProvider { + type Stream = BoxOnionStream; + type Listener = BoxOnionListener; + + fn update(&mut self) -> Result, Error> { + (self.update)(&mut self.data) + } + fn bootstrap(&mut self) -> Result<(), Error> { + (self.bootstrap)(&mut self.data) + } + fn add_client_auth( + &mut self, + service_id: &V3OnionServiceId, + client_auth: &X25519PrivateKey, + ) -> Result<(), Error> { + (self.add_client_auth)(&mut self.data, service_id, client_auth) + } + fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> { + (self.remove_client_auth)(&mut self.data, service_id) + } + fn connect( + &mut self, + target: TargetAddr, + circuit: Option, + ) -> Result { + (self.connect)(&mut self.data, target, circuit) + } + fn listener( + &mut self, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorised_clients: Option<&[X25519PublicKey]>, + ) -> Result { + (self.listener)(&mut self.data, private_key, virt_port, authorised_clients) + } + fn generate_token(&mut self) -> CircuitToken { + (self.generate_token)(&mut self.data) + } + fn release_token(&mut self, token: CircuitToken) { + (self.release_token)(&mut self.data, token) + } +} diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index e5791cffa..1fea38f7b 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -38,14 +38,14 @@ use tor_interface::tor_provider::*; // purely in-process mock tor provider #[cfg(test)] #[cfg(feature = "mock-tor-provider")] -fn build_mock_tor_provider() -> anyhow::Result> { - Ok(Box::new(MockTorClient::new())) +fn build_mock_tor_provider() -> anyhow::Result { + Ok(MockTorClient::new()) } // out-of-process c-tor owned by this process #[cfg(test)] #[cfg(feature = "legacy-tor-provider")] -fn build_bundled_legacy_tor_provider(name: &str) -> anyhow::Result> { +fn build_bundled_legacy_tor_provider(name: &str) -> anyhow::Result { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); data_path.push(name); @@ -59,13 +59,13 @@ fn build_bundled_legacy_tor_provider(name: &str) -> anyhow::Result anyhow::Result>> { +fn build_bundled_pt_legacy_tor_provider(name: &str) -> anyhow::Result> { let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); data_path.push(name); @@ -98,7 +98,7 @@ fn build_bundled_pt_legacy_tor_provider(name: &str) -> anyhow::Result anyhow::Result<(Box, TorProcess)> { +) -> anyhow::Result<(LegacyTorClient, TorProcess)> { let tor_daemon = build_system_legacy_tor(name, control_port, socks_port, |_, cmd| // password: foobar1 cmd.arg("HashedControlPassword") @@ -181,7 +181,7 @@ fn build_system_legacy_tor_provider_password( tor_control_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{control_port}").as_str())?, tor_control_auth: Some(TorAuth::Password("password".to_string())), }; - let tor_provider = Box::new(LegacyTorClient::new(tor_config)?); + let tor_provider = LegacyTorClient::new(tor_config)?; Ok((tor_provider, tor_daemon)) } @@ -192,7 +192,7 @@ fn build_system_legacy_tor_provider_cookie( name: &str, control_port: u16, socks_port: u16, -) -> anyhow::Result<(Box, TorProcess)> { +) -> anyhow::Result<(LegacyTorClient, TorProcess)> { let mut cookiefile = std::path::PathBuf::new(); let tor_daemon = build_system_legacy_tor(name, control_port, socks_port, |data_dir, cmd| { cookiefile = data_dir.join("cookie"); @@ -207,23 +207,22 @@ fn build_system_legacy_tor_provider_cookie( tor_control_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{control_port}").as_str())?, tor_control_auth: Some(TorAuth::Cookie(cookiefile)), }; - let tor_provider = Box::new(LegacyTorClient::new(tor_config)?); + let tor_provider = LegacyTorClient::new(tor_config)?; Ok((tor_provider, tor_daemon)) } #[cfg(test)] #[cfg(feature = "arti-client-tor-provider")] -fn build_arti_client_tor_provider(runtime: Arc, name: &str) -> anyhow::Result> { - +fn build_arti_client_tor_provider(runtime: Arc, name: &str) -> anyhow::Result { let mut data_path = std::env::temp_dir(); data_path.push(name); - Ok(Box::new(ArtiClientTorClient::new(runtime, &data_path)?)) + Ok(ArtiClientTorClient::new(runtime, &data_path)?) } #[cfg(test)] #[cfg(feature = "arti-tor-provider")] -fn build_arti_tor_provider(name: &str) -> anyhow::Result> { +fn build_arti_tor_provider(name: &str) -> anyhow::Result { let arti_path = which::which(format!("arti{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); data_path.push(name); @@ -233,14 +232,14 @@ fn build_arti_tor_provider(name: &str) -> anyhow::Result> { data_directory: data_path, }; - Ok(Box::new(ArtiTorClient::new(arti_config)?)) + Ok(ArtiTorClient::new(arti_config)?) } // // Test Functions // #[allow(dead_code)] -pub(crate) fn bootstrap_test(mut tor: Box, skip_connect_tests: bool) -> anyhow::Result<()> { +pub(crate) fn bootstrap_test(mut tor: P, skip_connect_tests: bool) -> anyhow::Result<()> { tor.bootstrap()?; let mut received_log = false; @@ -301,9 +300,9 @@ pub(crate) fn bootstrap_test(mut tor: Box, skip_connect_tests: } #[allow(dead_code)] -pub(crate) fn basic_onion_service_test( - mut server_provider: Box, - mut client_provider: Box, +pub(crate) fn basic_onion_service_test( + mut server_provider: P1, + mut client_provider: P2, ) -> anyhow::Result<()> { server_provider.bootstrap()?; client_provider.bootstrap()?; @@ -427,9 +426,9 @@ pub(crate) fn basic_onion_service_test( } #[allow(dead_code)] -pub(crate) fn authenticated_onion_service_test( - mut server_provider: Box, - mut client_provider: Box, +pub(crate) fn authenticated_onion_service_test( + mut server_provider: P1, + mut client_provider: P2, ) -> anyhow::Result<()> { server_provider.bootstrap()?; client_provider.bootstrap()?; From e5fda65b29dd2676a94e613d5c6302b4b433fa29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 1 Jul 2025 03:51:14 +0200 Subject: [PATCH 169/184] Devirtualise TcpOnionListener --- tor-interface/src/arti_client_tor_client.rs | 18 ++++++- tor-interface/src/legacy_tor_client.rs | 4 +- tor-interface/src/mock_tor_client.rs | 4 +- tor-interface/src/tor_provider.rs | 60 +++++++-------------- 4 files changed, 38 insertions(+), 48 deletions(-) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index ab979d40b..147495440 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -200,7 +200,7 @@ impl ArtiClientTorClient { impl TorProvider for ArtiClientTorClient { type Stream = TcpOnionStream; - type Listener = TcpOnionListener; + type Listener = ArtiClientOnionListener; fn update(&mut self) -> Result, tor_provider::Error> { std::thread::sleep(std::time::Duration::from_millis(16)); @@ -518,7 +518,7 @@ impl TorProvider for ArtiClientTorClient { let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id, virt_port)); // onion-service is torn down when `onion_service` is dropped - Ok(TcpOnionListener::new::>(listener, onion_addr, onion_service, |_|{})) + Ok(ArtiClientOnionListener(TcpOnionListenerBase(listener, onion_addr), onion_service)) } fn generate_token(&mut self) -> CircuitToken { @@ -527,3 +527,17 @@ impl TorProvider for ArtiClientTorClient { fn release_token(&mut self, _token: CircuitToken) {} } + +pub struct ArtiClientOnionListener(TcpOnionListenerBase, #[allow(dead_code)] Arc); + +impl OnionListener for ArtiClientOnionListener { + type Stream = TcpOnionStream; + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + self.0.set_nonblocking(nonblocking) + } + + fn accept(&self) -> std::io::Result> { + self.0.accept() + } +} diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 00ba7b9aa..0eb7bb6f4 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -694,9 +694,7 @@ impl TorProvider for LegacyTorClient { self.onion_services .push((service_id, Arc::clone(&is_active))); - Ok(TcpOnionListener::new(listener, onion_addr, is_active, |is_active| { - is_active.store(false, atomic::Ordering::Relaxed); - })) + Ok(TcpOnionListener(TcpOnionListenerBase(listener, onion_addr), is_active)) } fn generate_token(&mut self) -> CircuitToken { diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index d5b9a27c9..86ff31cbf 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -318,9 +318,7 @@ impl TorProvider for MockTorClient { .push(TorEvent::OnionServicePublished { service_id }); - Ok(TcpOnionListener::new(listener, onion_addr, is_active, |is_active| { - is_active.store(false, atomic::Ordering::Relaxed); - })) + Ok(TcpOnionListener(TcpOnionListenerBase(listener, onion_addr), is_active)) } fn generate_token(&mut self) -> CircuitToken { diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index d0dbacfe2..09435cbb9 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -6,7 +6,7 @@ use std::io::{Read, Write}; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use std::sync::OnceLock; +use std::sync::{atomic, Arc, OnceLock}; #[cfg(unix)] use std::os::unix::io::{IntoRawFd, RawFd}; #[cfg(windows)] @@ -524,52 +524,22 @@ pub trait OnionListener: Send { fn accept(&self) -> std::io::Result>; } -pub struct TcpOnionListener { - pub(crate) listener: TcpListener, - pub(crate) onion_addr: OnionAddr, - pub(crate) data: Option>, - pub(crate) drop: Option) + Send>>, -} - -impl TcpOnionListener { - /// Construct an `TcpOnionListener`. The `data` and `drop` parameters are to allow custom `TorProvider` implementations their own data and cleanup procedures. - pub(crate) fn new( - listener: TcpListener, - onion_addr: OnionAddr, - data: T, - mut drop: impl FnMut(T) + 'static + Send) -> Self { - // marshall our data into an Any - let data: Option> = Some(Box::new(data)); - // marhsall our drop into a function which takes an Any - let drop: Option) + Send>> = Some(Box::new(move |data: Box| { - // encapsulate extracting our data from the Any - if let Ok(data) = data.downcast::() { - // and call our provided drop - drop(*data); - } - })); +pub(crate) struct TcpOnionListenerBase(pub TcpListener, pub OnionAddr); - Self{ - listener, - onion_addr, - data, - drop, - } - } -} +pub struct TcpOnionListener(pub(crate) TcpOnionListenerBase, pub(crate) Arc); -impl OnionListener for TcpOnionListener { +impl OnionListener for TcpOnionListenerBase { type Stream = TcpOnionStream; fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { - self.listener.set_nonblocking(nonblocking) + self.0.set_nonblocking(nonblocking) } fn accept(&self) -> std::io::Result> { - match self.listener.accept() { + match self.0.accept() { Ok((stream, _socket_addr)) => Ok(Some(TcpOnionStream { stream, - local_addr: Some(self.onion_addr.clone()), + local_addr: Some(self.1.clone()), peer_addr: None, })), Err(err) => { @@ -583,11 +553,21 @@ impl OnionListener for TcpOnionListener { } } +impl OnionListener for TcpOnionListener { + type Stream = TcpOnionStream; + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + self.0.set_nonblocking(nonblocking) + } + + fn accept(&self) -> std::io::Result> { + self.0.accept() + } +} + impl Drop for TcpOnionListener { fn drop(&mut self) { - if let (Some(data), Some(mut drop)) = (self.data.take(), self.drop.take()) { - drop(data) - } + self.1.store(false, atomic::Ordering::Relaxed) } } From 470cb579d155cd85f291107bf54bf7b42a0c7e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 1 Jul 2025 05:06:23 +0200 Subject: [PATCH 170/184] tor-provider: provide ArtiClientTorClient via ArtiClientOnionStream --- tor-interface/src/arti_client_tor_client.rs | 127 ++++++++++++-------- 1 file changed, 78 insertions(+), 49 deletions(-) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 147495440..f41eec2fb 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -1,17 +1,20 @@ // standard +use std::future::Future; +use std::io::{Read, Write}; use std::net::SocketAddr; use std::ops::DerefMut; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll, Waker}; //extern use arti_client::config::{CfgPath, TorClientConfigBuilder}; -use arti_client::{BootstrapBehavior, DangerouslyIntoTorAddr, IntoTorAddr, TorClient}; +use arti_client::{BootstrapBehavior, DangerouslyIntoTorAddr, DataStream, IntoTorAddr, TorClient}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::{TcpListener, TcpStream}; -use tokio::runtime; +use tokio::net::TcpStream; +use tokio::{pin, runtime}; use tokio_stream::StreamExt; use tor_cell::relaycell::msg::Connected; use tor_config::ExplicitOrAuto; @@ -199,7 +202,7 @@ impl ArtiClientTorClient { } impl TorProvider for ArtiClientTorClient { - type Stream = TcpOnionStream; + type Stream = ArtiClientOnionStream; type Listener = ArtiClientOnionListener; fn update(&mut self) -> Result, tor_provider::Error> { @@ -322,51 +325,10 @@ impl TorProvider for ArtiClientTorClient { .block_on(async move { arti_client.connect(arti_target).await }) .map_err(Error::ArtiClientError)?; - // start a task to forward traffic from returned data stream - // and tcp socket - let client_stream = self.tokio_runtime.block_on(async move { - let (data_reader, data_writer) = data_stream.split(); - - // try to bind to a local address, let OS pick our port - let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); - let server_listener = TcpListener::bind(socket_addr) - .await - .map_err(Error::TcpListenerBindFailed)?; - // await future after a client connects - let server_accept_future = server_listener.accept(); - let socket_addr = server_listener - .local_addr() - .map_err(Error::TcpListenerLocalAddrFailed)?; - - // client stream will ultimatley be returned from connect() - let client_stream = TcpStream::connect(socket_addr) - .await - .map_err(Error::TcpStreamConnectFailed)?; - // client has connected so now get the server's tcp stream - let (server_stream, _socket_addr) = server_accept_future - .await - .map_err(Error::TcpListenerAcceptFailed)?; - let (tcp_reader, tcp_writer) = server_stream.into_split(); - - // now spawn new tasks to forward traffic to/from local listener - let pump_alive = Arc::new(AtomicBool::new(true)); - tokio::task::spawn({ - let pump_alive = pump_alive.clone(); - async move { - forward_stream(pump_alive, tcp_reader, data_writer).await; - } - }); - tokio::task::spawn(async move { - forward_stream(pump_alive, data_reader, tcp_writer).await; - }); - Ok::(client_stream) - })?; - - let stream = client_stream - .into_std() - .map_err(Error::TcpStreamIntoFailed)?; - Ok(TcpOnionStream { - stream, + Ok(ArtiClientOnionStream { + tokio_runtime: self.tokio_runtime.clone(), + data_stream, + nonblocking: AtomicBool::new(false), local_addr: None, peer_addr: Some(target), }) @@ -528,6 +490,73 @@ impl TorProvider for ArtiClientTorClient { fn release_token(&mut self, _token: CircuitToken) {} } +#[derive(Debug)] +pub struct ArtiClientOnionStream { + tokio_runtime: Arc, + data_stream: DataStream, + + nonblocking: AtomicBool, + peer_addr: Option, + local_addr: Option, +} + +macro_rules! fwd { + ($self:expr, $func:tt, $($args:expr),*) => {{ + pin! { + let fut = $self.data_stream.$func($($args),*); + } + if $self.nonblocking.load(Ordering::Relaxed) { + match fut.poll(&mut Context::from_waker(Waker::noop())) { + Poll::Ready(ret) => ret, + Poll::Pending => Err(std::io::Error::new(std::io::ErrorKind::WouldBlock, "")), + } + } else { + $self.tokio_runtime.block_on(fut) + } + }} +} + +impl Read for ArtiClientOnionStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + fwd!(self, read, buf) + } +} + +impl Write for ArtiClientOnionStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + fwd!(self, write, buf) + } + fn flush(&mut self) -> std::io::Result<()> { + fwd!(self, flush, ) + } + fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result { + fwd!(self, write_vectored, bufs) + } +} + +impl OnionStream for ArtiClientOnionStream { + fn peer_addr(&self) -> Option { + self.peer_addr.clone() + } + + fn local_addr(&self) -> Option { + self.local_addr.clone() + } + + fn try_clone(&self) -> std::io::Result where Self: Sized { + Err(std::io::Error::new(std::io::ErrorKind::Other, "not available")) + } + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + self.nonblocking.store(nonblocking, Ordering::Relaxed); + Ok(()) + } + + fn into_raw(self) -> OnionStreamIntoRaw { + unimplemented!() + } +} + pub struct ArtiClientOnionListener(TcpOnionListenerBase, #[allow(dead_code)] Arc); impl OnionListener for ArtiClientOnionListener { From f1c157e84c1ee37322b202a2d226a97c672f074e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 1 Jul 2025 21:52:06 +0200 Subject: [PATCH 171/184] tor-provider: provide ArtiClientTorClient via ArtiClientOnionListener --- tor-interface/Cargo.toml | 3 +- tor-interface/src/arti_client_tor_client.rs | 208 ++++++++------------ 2 files changed, 80 insertions(+), 131 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 03f6f5848..b25462110 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -28,6 +28,7 @@ static_assertions = "1.1" thiserror = "1.0" tokio = { version = "1", features = ["macros"], optional = true } tokio-stream = { version = "0", optional = true } +tokio-mpmc = { version = "0.2", optional = true } tor-cell = { version = "0.31.0", optional = true } tor-config = { version = "0.31.0", optional = true } tor-hsservice = { version = "0.31.0", optional = true, features = ["restricted-discovery"] } @@ -44,7 +45,7 @@ serial_test = "0.9" which = "4.4" [features] -arti-client-tor-provider = ["arti-client", "tokio", "tokio-stream", "tor-cell", "tor-config", "tor-hsservice", "tor-keymgr", "tor-proto", "tor-rtcompat"] +arti-client-tor-provider = ["arti-client", "tokio", "tokio-stream", "tokio-mpmc", "tor-cell", "tor-config", "tor-hsservice", "tor-keymgr", "tor-proto", "tor-rtcompat"] arti-tor-provider = ["arti-rpc-client-core"] mock-tor-provider = [] legacy-tor-provider = ["hmac", "sha2"] diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index f41eec2fb..b6eaf0753 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -1,7 +1,6 @@ // standard use std::future::Future; use std::io::{Read, Write}; -use std::net::SocketAddr; use std::ops::DerefMut; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -13,15 +12,16 @@ use std::task::{Context, Poll, Waker}; use arti_client::config::{CfgPath, TorClientConfigBuilder}; use arti_client::{BootstrapBehavior, DangerouslyIntoTorAddr, DataStream, IntoTorAddr, TorClient}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpStream; use tokio::{pin, runtime}; +use tokio_mpmc as mpmc; use tokio_stream::StreamExt; use tor_cell::relaycell::msg::Connected; use tor_config::ExplicitOrAuto; use tor_llcrypto::pk::ed25519::ExpandedKeypair; use tor_hsservice::config::OnionServiceConfigBuilder; use tor_hsservice::config::restricted_discovery::HsClientNickname; -use tor_hsservice::{HsNickname, RunningOnionService}; +use tor_hsservice::status::State; +use tor_hsservice::{HsNickname, RunningOnionService, StreamRequest}; use tor_keymgr::{config::ArtiKeystoreKind, KeystoreSelector}; use tor_proto::stream::IncomingStreamRequest; use tor_rtcompat::PreferredRuntime; @@ -87,59 +87,6 @@ pub struct ArtiClientTorClient { bootstrapped: Arc, } -// used to forward traffic to/from arti to local tcp streams -async fn forward_stream(alive: Arc, mut reader: R, mut writer: W) -> () -where - R: AsyncReadExt + Unpin, - W: AsyncWriteExt + Unpin, -{ - // allow 100ms timeout on reads to verify writer is still good - let read_timeout = std::time::Duration::from_millis(100); - // allow additional retries in the event the other half of the pump - // dies; keep pumping data until our read times out 3 times - let mut remaining_retries = 3; - let mut buf = [0u8; 1024]; - - loop { - if !alive.load(Ordering::Relaxed) && remaining_retries == 0 { - break; - } - - tokio::select! { - count = reader.read(&mut buf) => match count { - // end of stream - Ok(0) => break, - // read N bytes - Ok(count) => { - // forward traffic - match writer.write_all(&buf[0..count]).await { - Ok(()) => (), - Err(_err) => break, - } - match writer.flush().await { - Ok(()) => (), - Err(_err) => break, - } - }, - // read failed - Err(_err) => break, - }, - _ = tokio::time::sleep(read_timeout.clone()) => match writer.flush().await { - Ok(()) => { - // so long as our writer and reader are good, we should - // allow a few additional data pump attempts - if !alive.load(Ordering::Relaxed) { - remaining_retries -= 1; - } - }, - Err(_err) => break, - } - } - } - // signal pump death - alive.store(false, Ordering::Relaxed); -} - impl ArtiClientTorClient { /// Construct a new `ArtiClientTorClient` which uses a [Tokio](https://crates.io/crates/tokio) runtime internally for all async operations. pub fn new( @@ -340,16 +287,6 @@ impl TorProvider for ArtiClientTorClient { virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, ) -> Result { - - // try to bind to a local address, let OS pick our port - let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); - // TODO: make this one async too - let listener = - std::net::TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; - let socket_addr = listener - .local_addr() - .map_err(Error::TcpListenerLocalAddrFailed)?; - // generate a nickname to identify this onion service let service_id = V3OnionServiceId::from_private_key(private_key); let hs_nickname = match HsNickname::new(service_id.to_string()) { @@ -389,13 +326,10 @@ impl TorProvider for ArtiClientTorClient { } } - let onion_service_config = match onion_service_config_builder.build() - { - Ok(onion_service_config) => onion_service_config, - Err(err) => Err(err).map_err(Error::OnionServiceConfigBuilderError)?, - }; + let onion_service_config = onion_service_config_builder.build() + .map_err(Error::OnionServiceConfigBuilderError)?; - let (onion_service, mut rend_requests) = self.arti_client + let (onion_service, rend_requests) = self.arti_client .launch_onion_service_with_hsid(onion_service_config, hs_id_keypair.into()) .map_err(Error::ArtiClientOnionServiceLaunchError)?; @@ -421,66 +355,27 @@ impl TorProvider for ArtiClientTorClient { } }); - // start a task which accepts every RendRequest to get a StreamRequest + let (sender, receiver) = mpmc::channel(1); self.tokio_runtime.spawn(async move { - while let Some(request) = rend_requests.next().await { - let mut stream_requests = match request.accept().await { - Ok(stream_requests) => stream_requests, - // TODO: probably not our problem? - _ => return, - }; - // spawn a new task to consume the stream requsts - tokio::task::spawn(async move { - while let Some(stream_request) = stream_requests.next().await { - let should_accept = - if let IncomingStreamRequest::Begin(begin) = stream_request.request() { - // we only accept connections on the virt port - begin.port() == virt_port - } else { - false - }; - - if should_accept { - let data_stream = - match stream_request.accept(Connected::new_empty()).await { - Ok(data_stream) => data_stream, - // TODO: probably not our problem - _ => continue, - }; - let (data_reader, data_writer) = data_stream.split(); - - let (tcp_reader, tcp_writer) = - match TcpStream::connect(socket_addr).await { - Ok(tcp_stream) => tcp_stream.into_split(), - // TODO: possibly our problem? - _ => continue, - }; - // now spawn new tasks to forward traffic to/from the onion listener - - let pump_alive = Arc::new(AtomicBool::new(true)); - // read from connected client and write to local socket - tokio::task::spawn({ - let pump_alive = pump_alive.clone(); - async move { - forward_stream(pump_alive, data_reader, tcp_writer).await; - } - }); - // read from local socket and write to connected client - tokio::task::spawn(async move { - forward_stream(pump_alive, tcp_reader, data_writer).await; - }); - } else { - // either requesting the wrong port or the wrong type of stream request - let _ = stream_request.shutdown_circuit(); - } - } - }); + let mut stream_requests = tor_hsservice::handle_rend_requests(rend_requests); + while let Some(stream_request) = stream_requests.next().await { + if sender.send(stream_request).await.is_err() { + return; + } } }); let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id, virt_port)); // onion-service is torn down when `onion_service` is dropped - Ok(ArtiClientOnionListener(TcpOnionListenerBase(listener, onion_addr), onion_service)) + Ok(ArtiClientOnionListener { + tokio_runtime: self.tokio_runtime.clone(), + stream_requests: receiver, + virt_port, + + nonblocking: AtomicBool::new(false), + onion_service, + onion_addr, + }) } fn generate_token(&mut self) -> CircuitToken { @@ -557,16 +452,69 @@ impl OnionStream for ArtiClientOnionStream { } } -pub struct ArtiClientOnionListener(TcpOnionListenerBase, #[allow(dead_code)] Arc); +pub struct ArtiClientOnionListener { + tokio_runtime: Arc, + stream_requests: mpmc::Receiver, + virt_port: u16, + + nonblocking: AtomicBool, + onion_service: Arc, + onion_addr: OnionAddr, +} impl OnionListener for ArtiClientOnionListener { - type Stream = TcpOnionStream; + type Stream = ArtiClientOnionStream; fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { - self.0.set_nonblocking(nonblocking) + self.nonblocking.store(nonblocking, Ordering::Relaxed); + Ok(()) } fn accept(&self) -> std::io::Result> { - self.0.accept() + pin! { + let fut = async { + while let Some(stream_request) = self.stream_requests.recv().await.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { + let should_accept = + if let IncomingStreamRequest::Begin(begin) = stream_request.request() { + // we only accept connections on the virt port + begin.port() == self.virt_port + } else { + false + }; + + if should_accept { + let data_stream = + match stream_request.accept(Connected::new_empty()).await { + Ok(data_stream) => data_stream, + // TODO: probably not our problem + _ => continue, + }; + + return Ok(Some(ArtiClientOnionStream { + tokio_runtime: self.tokio_runtime.clone(), + data_stream, + nonblocking: AtomicBool::new(false), + local_addr: Some(self.onion_addr.clone()), + peer_addr: None, + })); + } else { + // either requesting the wrong port or the wrong type of stream request + let _ = stream_request.shutdown_circuit(); + } + } + match self.onion_service.status().state() { + s @ State::Shutdown | s @ State::DegradedUnreachable | s @ State::Broken => Err(std::io::Error::new(std::io::ErrorKind::Other, format!("{:?}", s))), + _ => Ok(None), + } + }; + } + if self.nonblocking.load(Ordering::Relaxed) { + match fut.poll(&mut Context::from_waker(Waker::noop())) { + Poll::Ready(ret) => ret, + Poll::Pending => Ok(None), + } + } else { + self.tokio_runtime.block_on(fut) + } } } From 9f9163b4acf2bb27f5ccc2dc8747f7c44574af43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Mon, 9 Jun 2025 06:51:42 +0200 Subject: [PATCH 172/184] tor-interface: use TcpOrUnixStream and SocketAddrOrUnixSocketAddr, allow using Legacy Tor over unix-domain sockets Requires: https://github.com/sfackler/rust-socks/pull/22 --- tor-interface/Cargo.toml | 4 +- tor-interface/src/arti_tor_client.rs | 6 +-- tor-interface/src/legacy_tor_client.rs | 27 +++++++------ .../src/legacy_tor_control_stream.rs | 10 ++--- tor-interface/src/legacy_tor_controller.rs | 39 ++++++++++++------- tor-interface/src/legacy_tor_process.rs | 28 ++++++++++--- tor-interface/src/mock_tor_client.rs | 10 ++--- tor-interface/src/tor_provider.rs | 37 +++++++++--------- tor-interface/tests/tor_provider.rs | 8 ++-- 9 files changed, 100 insertions(+), 69 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index b25462110..082bb45c9 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -23,7 +23,9 @@ regex = "1.9" sha1 = "0.10" sha3 = "0.10" signature = "1.5" -socks = "0.3" +# socks = "0.3" +# socks = { path = "../../../../../rust-socks" } +socks = { git = "https://github.com/nabijaczleweli/rust-socks" } static_assertions = "1.1" thiserror = "1.0" tokio = { version = "1", features = ["macros"], optional = true } diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs index a1f513af3..1b9937bab 100644 --- a/tor-interface/src/arti_tor_client.rs +++ b/tor-interface/src/arti_tor_client.rs @@ -125,7 +125,7 @@ impl ArtiTorClient { } impl TorProvider for ArtiTorClient { - type Stream = TcpOnionStream; + type Stream = TcpOrUnixOnionStream; type Listener = TcpOnionListener; fn update(&mut self) -> Result, tor_provider::Error> { @@ -226,8 +226,8 @@ impl TorProvider for ArtiTorClient { let stream = self.rpc_conn.open_stream(None, (host.as_str(), port), isolation) .map_err(Error::ArtiOpenStreamFailed)?; - Ok(TcpOnionStream { - stream, + Ok(TcpOrUnixOnionStream { + stream: stream.into(), local_addr: None, peer_addr: Some(target), }) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 0eb7bb6f4..6388afef3 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -10,7 +10,7 @@ use std::sync::{atomic, Arc}; use std::time::Duration; // extern crates -use socks::Socks5Stream; +use socks::{SocketAddrOrUnixSocketAddr, Socks5Stream}; // internal crates use crate::censorship_circumvention::*; @@ -166,8 +166,8 @@ pub enum LegacyTorClientConfig { bridge_lines: Option>, }, SystemTor { - tor_socks_addr: SocketAddr, - tor_control_addr: SocketAddr, + tor_socks_addr: SocketAddrOrUnixSocketAddr, + tor_control_addr: SocketAddrOrUnixSocketAddr, tor_control_auth: Option, }, } @@ -193,7 +193,7 @@ pub struct LegacyTorClient { version: LegacyTorVersion, controller: LegacyTorController, bootstrapped: bool, - socks_listener: Option, + socks_listener: Option, // list of open onion services and their is_active flag onion_services: Vec<(V3OnionServiceId, Arc)>, // our list of circuit tokens for the tor daemon @@ -232,9 +232,8 @@ impl LegacyTorClient { tor_control_auth, } => { // open a control stream - let control_stream = - LegacyControlStream::new(&tor_control_addr, Duration::from_millis(16)) - .map_err(Error::LegacyControlStreamCreationFailed)?; + let control_stream = LegacyControlStream::new(tor_control_addr, Duration::from_millis(16)) + .map_err(Error::LegacyControlStreamCreationFailed)?; // create a controler let controller = LegacyTorController::new(control_stream) @@ -244,7 +243,7 @@ impl LegacyTorClient { None, controller, tor_control_auth.take(), - Some(tor_socks_addr.clone()), + Some(tor_socks_addr.clone().into()), ) } }; @@ -469,7 +468,7 @@ impl LegacyTorClient { } impl TorProvider for LegacyTorClient { - type Stream = TcpOnionStream; + type Stream = TcpOrUnixOnionStream; type Listener = TcpOnionListener; fn update(&mut self) -> Result, tor_provider::Error> { @@ -603,10 +602,10 @@ impl TorProvider for LegacyTorClient { if listeners.is_empty() { return Err(Error::NoSocksListenersFound())?; } - self.socks_listener = Some(listeners.swap_remove(0)); + self.socks_listener = Some(listeners.swap_remove(0).into()); } - let socks_listener = match self.socks_listener { + let socks_listener = match self.socks_listener.as_ref() { Some(socks_listener) => socks_listener, None => unreachable!(), }; @@ -625,10 +624,10 @@ impl TorProvider for LegacyTorClient { // readwrite stream let stream = match &circuit { - None => Socks5Stream::connect(socks_listener, socks_target), + None => Socks5Stream::connect_either(socks_listener, socks_target), Some(circuit) => { if let Some(circuit) = self.circuit_tokens.get(circuit) { - Socks5Stream::connect_with_password( + Socks5Stream::connect_either_with_password( socks_listener, socks_target, &circuit.username, @@ -641,7 +640,7 @@ impl TorProvider for LegacyTorClient { } .map_err(Error::Socks5ConnectionFailed)?; - Ok(TcpOnionStream { + Ok(TcpOrUnixOnionStream { stream: stream.into_inner(), local_addr: None, peer_addr: Some(target), diff --git a/tor-interface/src/legacy_tor_control_stream.rs b/tor-interface/src/legacy_tor_control_stream.rs index 881202cf7..d7b4610a1 100644 --- a/tor-interface/src/legacy_tor_control_stream.rs +++ b/tor-interface/src/legacy_tor_control_stream.rs @@ -1,14 +1,14 @@ // standard use std::collections::VecDeque; use std::default::Default; -use std::io::{ErrorKind, IoSlice, Read, Write}; -use std::net::{SocketAddr, TcpStream}; +use std::io::{ErrorKind, Read, Write, IoSlice}; use std::option::Option; use std::string::ToString; use std::time::Duration; // extern crates use regex::Regex; +use socks::{SocketAddrOrUnixSocketAddr, TcpOrUnixStream}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -41,7 +41,7 @@ pub enum Error { } pub(crate) struct LegacyControlStream { - stream: TcpStream, + stream: TcpOrUnixStream, closed_by_remote: bool, pending_data: Vec, pending_lines: VecDeque, @@ -60,12 +60,12 @@ pub(crate) struct Reply { } impl LegacyControlStream { - pub fn new(addr: &SocketAddr, read_timeout: Duration) -> Result { + pub fn new>(addr: T, read_timeout: Duration) -> Result { if read_timeout.is_zero() { return Err(Error::ReadTimeoutZero()); } - let stream = TcpStream::connect(addr).map_err(Error::CreationFailed)?; + let stream = TcpOrUnixStream::connect(addr).map_err(Error::CreationFailed)?; stream .set_read_timeout(Some(read_timeout)) .map_err(Error::ConfigurationFailed)?; diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs index f725c5e82..eaf65ecd3 100644 --- a/tor-interface/src/legacy_tor_controller.rs +++ b/tor-interface/src/legacy_tor_controller.rs @@ -943,10 +943,22 @@ impl LegacyTorController { #[test] #[serial] fn test_tor_controller() -> anyhow::Result<()> { + test_tor_controller_impl(false) +} +#[test] +#[serial] +#[cfg(unix)] +fn test_tor_controller_unix() -> anyhow::Result<()> { + test_tor_controller_impl(true) +} +#[cfg(test)] +fn test_tor_controller_impl(unix: bool) -> anyhow::Result<()> { + use std::borrow::Cow; + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; let mut data_path = std::env::temp_dir(); data_path.push("test_tor_controller"); - let tor_process = LegacyTorProcess::new(&tor_path, &data_path)?; + let tor_process = LegacyTorProcess::new_unix(&tor_path, &data_path, unix)?; // create a scope to ensure tor_controller is dropped { @@ -956,11 +968,11 @@ fn test_tor_controller() -> anyhow::Result<()> { // create a tor controller and send authentication command let mut tor_controller = LegacyTorController::new(control_stream)?; tor_controller.authenticate_cmd(tor_process.get_password())?; - assert!( + assert_eq!( tor_controller .authenticate_cmd("invalid password")? - .status_code - == 515u32 + .status_code, + 515u32 ); // tor controller should have shutdown the connection after failed authentication @@ -991,7 +1003,7 @@ fn test_tor_controller() -> anyhow::Result<()> { "DisableNetwork" => "1", _ => panic!("unexpected returned key: {}", key), }; - assert!(value == expected); + assert_eq!(value, expected); } let vals = tor_controller.getinfo(&["version", "config-file", "config-text"])?; @@ -1002,14 +1014,15 @@ fn test_tor_controller() -> anyhow::Result<()> { for (key, value) in vals.iter() { match key.as_str() { "version" => assert!(Regex::new(r"\d+\.\d+\.\d+\.\d+")?.is_match(&value)), - "config-file" => assert!(Path::new(&value) == expected_torrc_path), - "config-text" => assert!( - value.to_string() - == format!( - "\nControlPort auto\nControlPortWriteToFile {}\nDataDirectory {}", - expected_control_port_path.display(), - data_path.display() - ) + "config-file" => assert_eq!(Path::new(&value), expected_torrc_path), + "config-text" => assert_eq!( + value.to_string(), + format!( + "\nControlPort {}\nControlPortWriteToFile {}\nDataDirectory {}", + if unix { Cow::Owned(format!("unix:{}", expected_control_port_path.with_file_name("control.sock").display())) } else { Cow::Borrowed("auto") }, + expected_control_port_path.display(), + data_path.display() + ) ), _ => panic!("unexpected returned key: {}", key), } diff --git a/tor-interface/src/legacy_tor_process.rs b/tor-interface/src/legacy_tor_process.rs index 074748439..08ff9ae23 100644 --- a/tor-interface/src/legacy_tor_process.rs +++ b/tor-interface/src/legacy_tor_process.rs @@ -1,4 +1,5 @@ // standard +use std::borrow::Cow; use std::default::Default; use std::fs; use std::fs::File; @@ -12,11 +13,14 @@ use std::str::FromStr; use std::string::ToString; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; +#[cfg(unix)] +use std::os::unix::net::SocketAddr as UnixSocketAddr; // extern crates use data_encoding::HEXUPPER; use rand::RngCore; use sha1::{Digest, Sha1}; +use socks::SocketAddrOrUnixSocketAddr; // internal crates use crate::tor_crypto::generate_password; @@ -69,7 +73,7 @@ pub enum Error { StdoutReadThreadSpawnFailed(#[source] std::io::Error), } -fn read_control_port_file(control_port_file: &Path) -> Result { +fn read_control_port_file(control_port_file: &Path) -> Result { // open file let mut file = File::open(control_port_file).map_err(Error::ControlPortFileReadFailed)?; @@ -90,7 +94,14 @@ fn read_control_port_file(control_port_file: &Path) -> Result if contents.starts_with("PORT=") { let addr_string = &contents.trim_end()["PORT=".len()..]; if let Ok(addr) = SocketAddr::from_str(addr_string) { - return Ok(addr); + return Ok(addr.into()); + } + } + #[cfg(unix)] + if contents.starts_with("UNIX_PORT=") { + let addr_string = &contents.trim_end()["UNIX_PORT=".len()..]; + if let Ok(addr) = UnixSocketAddr::from_pathname(addr_string) { + return Ok(addr.into()); } } Err(Error::ControlPortFileContentsInvalid(format!( @@ -101,7 +112,7 @@ fn read_control_port_file(control_port_file: &Path) -> Result // Encapsulates the tor daemon process pub(crate) struct LegacyTorProcess { - control_addr: SocketAddr, + control_addr: SocketAddrOrUnixSocketAddr, process: Child, password: String, // stdout data @@ -162,7 +173,7 @@ impl LegacyTorProcess { Self::hash_tor_password_with_salt(&salt, password) } - pub fn get_control_addr(&self) -> &SocketAddr { + pub fn get_control_addr(&self) -> &SocketAddrOrUnixSocketAddr { &self.control_addr } @@ -171,6 +182,11 @@ impl LegacyTorProcess { } pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { + Self::new_unix(tor_bin_path, data_directory, false) + } + + /// Unix mode is test/debug only + pub(crate) fn new_unix(tor_bin_path: &Path, data_directory: &Path, unix: bool) -> Result { if tor_bin_path.is_relative() { return Err(Error::TorBinPathNotAbsolute(format!( "{}", @@ -249,10 +265,10 @@ impl LegacyTorProcess { // daemon will assign us a port, and we will // read it from the control port file .arg("ControlPort") - .arg("auto") + .arg(&*if unix { Cow::Owned(format!("unix:{}", data_directory.join("control.sock").display())) } else { Cow::Borrowed("auto") }) // control port file destination .arg("ControlPortWriteToFile") - .arg(control_port_file.clone()) + .arg(&control_port_file) // use password authentication to prevent other apps // from modifying our daemon's settings .arg("HashedControlPassword") diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index 86ff31cbf..ec01e2748 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -83,8 +83,8 @@ impl MockTorNetwork { } if let Ok(stream) = TcpStream::connect(socket_addr) { - Ok(TcpOnionStream { - stream, + Ok(TcpOrUnixOnionStream { + stream: stream.into(), local_addr: None, peer_addr: Some(TargetAddr::OnionService(onion_addr)), }) @@ -168,7 +168,7 @@ impl Default for MockTorClient { } impl TorProvider for MockTorClient { - type Stream = TcpOnionStream; + type Stream = TcpOrUnixOnionStream; type Listener = TcpOnionListener; fn update(&mut self) -> Result, tor_provider::Error> { @@ -256,8 +256,8 @@ impl TorProvider for MockTorClient { .local_addr() .expect("loopback local_addr failed"), ) { - return Ok(TcpOnionStream { - stream, + return Ok(TcpOrUnixOnionStream { + stream: stream.into(), local_addr: None, peer_addr: Some(target_address), }); diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 09435cbb9..513488756 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -3,7 +3,7 @@ use std::any::Any; use std::boxed::Box; use std::convert::TryFrom; use std::io::{Read, Write}; -use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::net::{SocketAddr, TcpListener}; use std::ops::{Deref, DerefMut}; use std::str::FromStr; use std::sync::{atomic, Arc, OnceLock}; @@ -17,6 +17,7 @@ use domain::base::name::Name; use idna::uts46::{Hyphens, Uts46}; use idna::{domain_to_ascii_cow, AsciiDenyList}; use regex::Regex; +pub use socks::TcpOrUnixStream; // internal crates use crate::tor_crypto::*; @@ -284,7 +285,7 @@ pub type OnionStreamIntoRaw = RawFd; #[cfg(windows)] pub type OnionStreamIntoRaw = RawSocket; -/// A wrapper around a [`std::net::TcpStream`] with some Tor-specific customisations +/// A wrapper around a [`TcpOrUnixStream`] with some Tor-specific customisations /// /// An onion-listener can be constructed using the [`TorProvider::connect()`] method. pub trait OnionStream: Send + Read + Write + std::fmt::Debug { @@ -294,10 +295,10 @@ pub trait OnionStream: Send + Read + Write + std::fmt::Debug { /// Returns the onion address of the local connection for an incoming onion-service connection. Returns `None` for outgoing connections. fn local_addr(&self) -> Option; - /// Tries to clone the underlying connection and data. A simple pass-through to [`std::net::TcpStream::try_clone()`]. + /// Tries to clone the underlying connection and data. A simple pass-through to [`TcpOrUnixStream::try_clone()`]. fn try_clone(&self) -> std::io::Result where Self: Sized; - /// Moves the underlying `TcpStream` into or out of nonblocking mode. + /// Moves the underlying `TcpOrUnixStream` into or out of nonblocking mode. fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()>; /// Consume stream and return the underlying raw handle. @@ -305,32 +306,32 @@ pub trait OnionStream: Send + Read + Write + std::fmt::Debug { } #[derive(Debug)] -pub struct TcpOnionStream { - pub(crate) stream: TcpStream, +pub struct TcpOrUnixOnionStream { + pub(crate) stream: TcpOrUnixStream, pub(crate) local_addr: Option, pub(crate) peer_addr: Option, } -impl Deref for TcpOnionStream { - type Target = TcpStream; +impl Deref for TcpOrUnixOnionStream { + type Target = TcpOrUnixStream; fn deref(&self) -> &Self::Target { &self.stream } } -impl DerefMut for TcpOnionStream { +impl DerefMut for TcpOrUnixOnionStream { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.stream } } -impl From for TcpStream { - fn from(onion_stream: TcpOnionStream) -> Self { +impl From for TcpOrUnixStream { + fn from(onion_stream: TcpOrUnixOnionStream) -> Self { onion_stream.stream } } -impl Read for TcpOnionStream { +impl Read for TcpOrUnixOnionStream { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.stream.read(buf) } @@ -348,7 +349,7 @@ impl Read for TcpOnionStream { } } -impl Write for TcpOnionStream { +impl Write for TcpOrUnixOnionStream { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.stream.write(buf) } @@ -366,7 +367,7 @@ impl Write for TcpOnionStream { } } -impl OnionStream for TcpOnionStream { +impl OnionStream for TcpOrUnixOnionStream { fn peer_addr(&self) -> Option { self.peer_addr.clone() } @@ -529,7 +530,7 @@ pub(crate) struct TcpOnionListenerBase(pub TcpListener, pub OnionAddr); pub struct TcpOnionListener(pub(crate) TcpOnionListenerBase, pub(crate) Arc); impl OnionListener for TcpOnionListenerBase { - type Stream = TcpOnionStream; + type Stream = TcpOrUnixOnionStream; fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { self.0.set_nonblocking(nonblocking) @@ -537,8 +538,8 @@ impl OnionListener for TcpOnionListenerBase { fn accept(&self) -> std::io::Result> { match self.0.accept() { - Ok((stream, _socket_addr)) => Ok(Some(TcpOnionStream { - stream, + Ok((stream, _socket_addr)) => Ok(Some(TcpOrUnixOnionStream { + stream: stream.into(), local_addr: Some(self.1.clone()), peer_addr: None, })), @@ -554,7 +555,7 @@ impl OnionListener for TcpOnionListenerBase { } impl OnionListener for TcpOnionListener { - type Stream = TcpOnionStream; + type Stream = TcpOrUnixOnionStream; fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { self.0.set_nonblocking(nonblocking) diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index 1fea38f7b..ba9893322 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -177,8 +177,8 @@ fn build_system_legacy_tor_provider_password( )?; let tor_config = LegacyTorClientConfig::SystemTor { - tor_socks_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{socks_port}").as_str())?, - tor_control_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{control_port}").as_str())?, + tor_socks_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{socks_port}").as_str())?.into(), + tor_control_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{control_port}").as_str())?.into(), tor_control_auth: Some(TorAuth::Password("password".to_string())), }; let tor_provider = LegacyTorClient::new(tor_config)?; @@ -203,8 +203,8 @@ fn build_system_legacy_tor_provider_cookie( })?; let tor_config = LegacyTorClientConfig::SystemTor { - tor_socks_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{socks_port}").as_str())?, - tor_control_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{control_port}").as_str())?, + tor_socks_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{socks_port}").as_str())?.into(), + tor_control_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{control_port}").as_str())?.into(), tor_control_auth: Some(TorAuth::Cookie(cookiefile)), }; let tor_provider = LegacyTorClient::new(tor_config)?; From 30cb1d1756ee467d14fee40207b36bcd6af116bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Mon, 9 Jun 2025 19:56:02 +0200 Subject: [PATCH 173/184] tor-interface: add LegacyTorClientConfig::system_from_environment() Parsing standard $TOR_SOCKS_{IPC_PATH,HOST+PORT}, $TOR_CONTROL_{IPC_PATH,HOST+PORT}, $TOR_CONTROL_{PASSWD,COOKIE_AUTH_FILE} variables These are documented by upstream: https://gitlab.torproject.org/tpo/applications/wiki/-/blob/master/Environment-variables-and-related-preferences.md and used in the wild, like by whonix: https://www.whonix.org/wiki/Dev/Project_friendly_applications_best_practices#Tor_Settings_Autodetection --- tor-interface/src/legacy_tor_client.rs | 102 +++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 6388afef3..1a8ac61e6 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -2,12 +2,15 @@ use std::collections::BTreeMap; use std::convert::From; use std::default::Default; -use std::net::{SocketAddr, TcpListener}; +use std::net::{IpAddr, SocketAddr, TcpListener}; use std::option::Option; use std::path::PathBuf; +use std::str::FromStr; use std::string::ToString; use std::sync::{atomic, Arc}; use std::time::Duration; +#[cfg(unix)] +use std::os::unix::net::SocketAddr as UnixSocketAddr; // extern crates use socks::{SocketAddrOrUnixSocketAddr, Socks5Stream}; @@ -179,6 +182,95 @@ pub enum TorAuth { CookieData([u8; 32]), } +impl LegacyTorClientConfig { + fn one(ipc: &str, host: &str, port: &str) -> Option { + #[cfg(unix)] + if let Some(p) = std::env::var_os(ipc) { + return Some(UnixSocketAddr::from_pathname(p).ok()?.into()); + } + match (std::env::var(host), std::env::var(port)) { + (Ok(h), Ok(p)) => Some(SocketAddr::new(IpAddr::from_str(&h).ok()?, u16::from_str(&p).ok()?).into()), + _ => None, + } + } + + /// Consult `$TOR_SOCKS_{IPC_PATH,HOST+PORT}`, `$TOR_CONTROL_{IPC_PATH,HOST+PORT}` and `$TOR_CONTROL_{PASSWD,COOKIE_AUTH_FILE}` + /// + /// `$TOR_SOCKS_IPC_PATH` and `$TOR_CONTROL_IPC_PATH` are ignored if `cfg(not(unix))`, + /// and take precedence if `cfg(unix)`. + /// + /// `$TOR_CONTROL_PASSWD` takes precedence over `$TOR_CONTROL_COOKIE_AUTH_FILE` + pub fn system_from_environment() -> Option { + Some(LegacyTorClientConfig::SystemTor { + tor_socks_addr: Self::one("TOR_SOCKS_IPC_PATH", "TOR_SOCKS_HOST", "TOR_SOCKS_PORT")?, + tor_control_addr: Self::one("TOR_CONTROL_IPC_PATH", "TOR_CONTROL_HOST", "TOR_CONTROL_PORT")?, + tor_control_auth: match (std::env::var("TOR_CONTROL_PASSWD"), std::env::var_os("TOR_CONTROL_COOKIE_AUTH_FILE")) { + (Ok(pass), _) => Some(TorAuth::Password(pass)), + (Err(std::env::VarError::NotUnicode(_)), _) => return None, + (_, Some(cookie)) => Some(TorAuth::Cookie(cookie.into())), + _ => None, + } + }) + } +} + +#[test] +fn system_from_environment() { + fn flatten(conf: Option) -> Option<(SocketAddrOrUnixSocketAddr, SocketAddrOrUnixSocketAddr, Option)> { + conf.and_then(|c| match c { + LegacyTorClientConfig::BundledTor { .. } => None, + LegacyTorClientConfig::SystemTor { tor_socks_addr, tor_control_addr, tor_control_auth } => Some((tor_socks_addr, tor_control_addr, tor_control_auth)) + }) + } + + for var in ["TOR_SOCKS_IPC_PATH", "TOR_SOCKS_HOST", "TOR_SOCKS_PORT", "TOR_CONTROL_IPC_PATH", "TOR_CONTROL_HOST", "TOR_CONTROL_PORT", "TOR_CONTROL_PASSWD", "TOR_CONTROL_COOKIE_AUTH_FILE"] { + unsafe { std::env::remove_var(var) }; + } + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), None); + + std::env::set_var("TOR_SOCKS_HOST", "1.1.1.1"); + std::env::set_var("TOR_SOCKS_PORT", "9050"); + std::env::set_var("TOR_CONTROL_HOST", "2.2.2.2"); + std::env::set_var("TOR_CONTROL_PORT", "9051"); + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), Some(( + SocketAddr::from(([1, 1, 1, 1], 9050)).into(), + SocketAddr::from(([2, 2, 2, 2], 9051)).into(), + None, + ))); + + unsafe { std::env::set_var("TOR_CONTROL_PASSWD", std::ffi::OsStr::from_encoded_bytes_unchecked(b"\xFF")) }; + std::env::set_var("TOR_CONTROL_COOKIE_AUTH_FILE", "/cookie"); + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), None); + + std::env::set_var("TOR_CONTROL_PASSWD", "pass"); + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), Some(( + SocketAddr::from(([1, 1, 1, 1], 9050)).into(), + SocketAddr::from(([2, 2, 2, 2], 9051)).into(), + Some(TorAuth::Password("pass".to_string())), + ))); + + unsafe { std::env::remove_var("TOR_CONTROL_PASSWD") }; + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), Some(( + SocketAddr::from(([1, 1, 1, 1], 9050)).into(), + SocketAddr::from(([2, 2, 2, 2], 9051)).into(), + Some(TorAuth::Cookie("/cookie".into())), + ))); + + std::env::set_var("TOR_SOCKS_IPC_PATH", "/sock"); + #[cfg(not(unix))] + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), Some(( + SocketAddr::from(([1, 1, 1, 1], 9050)).into(), + SocketAddr::from(([2, 2, 2, 2], 9051)).into(), + Some(TorAuth::Cookie("/cookie".into())), + ))); + #[cfg(unix)] + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), Some(( + UnixSocketAddr::from_pathname("/sock").unwrap().into(), + SocketAddr::from(([2, 2, 2, 2], 9051)).into(), + Some(TorAuth::Cookie("/cookie".into())), + ))); +} + // // LegacyTorClient // @@ -204,7 +296,7 @@ pub struct LegacyTorClient { impl LegacyTorClient { /// Construct a new `LegacyTorClient` from a [`LegacyTorClientConfig`]. pub fn new(mut config: LegacyTorClientConfig) -> Result { - let (daemon, mut controller, auth, socks_listener) = match &mut config { + let (daemon, mut controller, mut auth, socks_listener) = match &mut config { LegacyTorClientConfig::BundledTor { tor_bin_path, data_directory, @@ -249,11 +341,11 @@ impl LegacyTorClient { }; // authenticate - match auth { + match auth.as_mut() { None => controller.authenticate_auto(), Some(TorAuth::Password(pass)) => controller.authenticate(&pass), - Some(TorAuth::Cookie(file)) => controller.authenticate_cookie(crate::legacy_tor_controller::read_cookie(&file).map_err(|e| Error::CookieReadingFailed(e, file))?), - Some(TorAuth::CookieData(cookie)) => controller.authenticate_cookie(cookie), + Some(TorAuth::Cookie(file)) => controller.authenticate_cookie(crate::legacy_tor_controller::read_cookie(&file).map_err(|e| Error::CookieReadingFailed(e, std::mem::take(file)))?), + Some(TorAuth::CookieData(cookie)) => controller.authenticate_cookie(*cookie), }.map_err(Error::LegacyTorProcessAuthenticationFailed)?; // min required version for v3 client auth (see control-spec.txt) From 655f65ae660ceee0bdd56e6a928c5a312f0733b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 24 Jun 2025 15:49:32 +0200 Subject: [PATCH 174/184] tor-interface: add optional binding address parameter to TorProvider::listener() This is a hard requirement for downstreams like UnstoppableSwap --- tor-interface/src/arti_client_tor_client.rs | 2 ++ tor-interface/src/arti_tor_client.rs | 2 ++ tor-interface/src/legacy_tor_client.rs | 18 +++++++++--------- tor-interface/src/mock_tor_client.rs | 3 ++- tor-interface/src/tor_provider.rs | 10 ++++++---- tor-interface/tests/tor_provider.rs | 4 ++-- 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index b6eaf0753..732095cf2 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -1,6 +1,7 @@ // standard use std::future::Future; use std::io::{Read, Write}; +use std::net::SocketAddr; use std::ops::DerefMut; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -286,6 +287,7 @@ impl TorProvider for ArtiClientTorClient { private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, + _bind_addr: Option, ) -> Result { // generate a nickname to identify this onion service let service_id = V3OnionServiceId::from_private_key(private_key); diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs index 1b9937bab..361736cd0 100644 --- a/tor-interface/src/arti_tor_client.rs +++ b/tor-interface/src/arti_tor_client.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::ops::DerefMut; use std::path::PathBuf; +use std::net::SocketAddr; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -238,6 +239,7 @@ impl TorProvider for ArtiTorClient { _private_key: &Ed25519PrivateKey, _virt_port: u16, _authorized_clients: Option<&[X25519PublicKey]>, + _bind_addr: Option, ) -> Result { Err(Error::NotImplemented().into()) } diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 1a8ac61e6..6a72a958e 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -745,15 +745,15 @@ impl TorProvider for LegacyTorClient { private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, + bind_addr: Option, ) -> Result { if !self.bootstrapped { return Err(Error::LegacyTorNotBootstrapped().into()); } // try to bind to a local address, let OS pick our port - let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); - let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; - let socket_addr = listener + let listener = TcpListener::bind(bind_addr.unwrap_or(([127, 0, 0, 1], 0u16).into())).map_err(Error::TcpListenerBindFailed)?; + let bind_addr = listener .local_addr() .map_err(Error::TcpListenerLocalAddrFailed)?; @@ -763,11 +763,6 @@ impl TorProvider for LegacyTorClient { ..Default::default() }; - let onion_addr = OnionAddr::V3(OnionAddrV3::new( - V3OnionServiceId::from_private_key(private_key), - virt_port, - )); - // start onion service let (_, service_id) = self .controller @@ -776,11 +771,16 @@ impl TorProvider for LegacyTorClient { &flags, None, virt_port, - Some(socket_addr), + Some(bind_addr), authorized_clients, ) .map_err(Error::AddOnionFailed)?; + let onion_addr = OnionAddr::V3(OnionAddrV3::new( + V3OnionServiceId::from_private_key(private_key), + virt_port, + )); + let is_active = Arc::new(atomic::AtomicBool::new(true)); self.onion_services .push((service_id, Arc::clone(&is_active))); diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs index ec01e2748..eded7daa3 100644 --- a/tor-interface/src/mock_tor_client.rs +++ b/tor-interface/src/mock_tor_client.rs @@ -281,6 +281,7 @@ impl TorProvider for MockTorClient { private_key: &Ed25519PrivateKey, virt_port: u16, authorized_clients: Option<&[X25519PublicKey]>, + bind_addr: Option, ) -> Result { // convert inputs to relevant types let service_id = V3OnionServiceId::from_private_key(private_key); @@ -291,7 +292,7 @@ impl TorProvider for MockTorClient { }; // try to bind to a local address, let OS pick our port - let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); + let socket_addr = bind_addr.unwrap_or(SocketAddr::from(([127, 0, 0, 1], 0u16))); let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; let socket_addr = listener .local_addr() diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 513488756..486ba6455 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -633,12 +633,13 @@ pub trait TorProvider: Send { ) -> Result; /// Anonymously start an onion-service and return the associated [`OnionListener`]. /// - ///The resulting onion-service will not be reachable by clients until [`TorProvider::update()`] returns a [`TorEvent::OnionServicePublished`] event. The optional `authorised_clients` parameter may be used to require client authorisation keys to connect to resulting onion-service. For further information, see the Tor Project's onion-services [client-auth documentation](https://community.torproject.org/onion-services/advanced/client-auth). + ///The resulting onion-service will not be reachable by clients until [`TorProvider::update()`] returns a [`TorEvent::OnionServicePublished`] event. The optional `authorised_clients` parameter may be used to require client authorisation keys to connect to resulting onion-service. `bind_addr` may be used to force a specific address and port. For further information, see the Tor Project's onion-services [client-auth documentation](https://community.torproject.org/onion-services/advanced/client-auth). fn listener( &mut self, private_key: &Ed25519PrivateKey, virt_port: u16, authorised_clients: Option<&[X25519PublicKey]>, + bind_addr: Option, ) -> Result; /// Create a new [`CircuitToken`]. fn generate_token(&mut self) -> CircuitToken; @@ -655,7 +656,7 @@ pub struct BoxTorProvider { add_client_auth: fn(&mut Box, &V3OnionServiceId, &X25519PrivateKey) -> Result<(), Error>, remove_client_auth: fn(&mut Box, &V3OnionServiceId) -> Result<(), Error>, connect: fn(&mut Box, TargetAddr, Option) -> Result<::Stream, Error>, - listener: fn(&mut Box, &Ed25519PrivateKey, u16, Option<&[X25519PublicKey]>) -> Result<::Listener, Error>, + listener: fn(&mut Box, &Ed25519PrivateKey, u16, Option<&[X25519PublicKey]>, Option) -> Result<::Listener, Error>, generate_token: fn(&mut Box) -> CircuitToken, release_token: fn(&mut Box, CircuitToken), } @@ -670,7 +671,7 @@ impl BoxTorProvider { add_client_auth: |slf, service_id, client_auth| slf.downcast_mut::

().unwrap().add_client_auth(service_id, client_auth), remove_client_auth: |slf, service_id| slf.downcast_mut::

().unwrap().remove_client_auth(service_id), connect: |slf, target, circuit| slf.downcast_mut::

().unwrap().connect(target, circuit).map(BoxOnionStream::new), - listener: |slf, private_key, virt_port, authorised_clients| slf.downcast_mut::

().unwrap().listener(private_key, virt_port, authorised_clients).map(BoxOnionListener::new), + listener: |slf, private_key, virt_port, authorised_clients, bind_addr| slf.downcast_mut::

().unwrap().listener(private_key, virt_port, authorised_clients, bind_addr).map(BoxOnionListener::new), generate_token: |slf| slf.downcast_mut::

().unwrap().generate_token(), release_token: |slf, token| slf.downcast_mut::

().unwrap().release_token(token), } @@ -709,8 +710,9 @@ impl TorProvider for BoxTorProvider { private_key: &Ed25519PrivateKey, virt_port: u16, authorised_clients: Option<&[X25519PublicKey]>, + bind_addr: Option, ) -> Result { - (self.listener)(&mut self.data, private_key, virt_port, authorised_clients) + (self.listener)(&mut self.data, private_key, virt_port, authorised_clients, bind_addr) } fn generate_token(&mut self) -> CircuitToken { (self.generate_token)(&mut self.data) diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs index ba9893322..206357680 100644 --- a/tor-interface/tests/tor_provider.rs +++ b/tor-interface/tests/tor_provider.rs @@ -363,7 +363,7 @@ pub(crate) fn basic_onion_service_test( println!("Starting and listening to onion service"); const VIRT_PORT: u16 = 42069u16; - let listener = tor.listener(&private_key, VIRT_PORT, None)?; + let listener = tor.listener(&private_key, VIRT_PORT, None, None)?; let mut onion_published = false; while !onion_published { @@ -491,7 +491,7 @@ pub(crate) fn authenticated_onion_service_test println!("Starting and listening to authenticated onion service"); const VIRT_PORT: u16 = 42069u16; let listener = - server_provider.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]))?; + server_provider.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]), None)?; let mut onion_published = false; while !onion_published { From c94cbee0328d24f51a3863d338fe38bc719deec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Mon, 9 Jun 2025 20:13:25 +0200 Subject: [PATCH 175/184] tor-interface: add examples These use the legacy Tor provider for inbound and outbound connections tor ControlPort unix:/home/nabijaczleweli/uwu/sock/sock SocksPort unix:/home/nabijaczleweli/uwu/sock/socks & TOR_SOCKS_IPC_PATH=/home/nabijaczleweli/uwu/sock/socks TOR_CONTROL_IPC_PATH=/home/nabijaczleweli/uwu/sock/sock target/debug/examples/legacy-tor-provider-provider does a crude HTTP request TOR_SOCKS_IPC_PATH=/home/nabijaczleweli/uwu/sock/socks TOR_CONTROL_IPC_PATH=/home/nabijaczleweli/uwu/sock/sock target/debug/examples/legacy-tor-provider-listener starts a crude HTTP server that can be observed with torsocks curl mxmy...ctu.onion TCP connections also work, of course --- tor-interface/CMakeLists.txt | 21 ++++++++++++ .../examples/legacy-tor-provider-listener.rs | 24 ++++++++++++++ .../examples/legacy-tor-provider-provider.rs | 33 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 tor-interface/examples/legacy-tor-provider-listener.rs create mode 100644 tor-interface/examples/legacy-tor-provider-provider.rs diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt index aa7ade6e3..6d168a6a9 100644 --- a/tor-interface/CMakeLists.txt +++ b/tor-interface/CMakeLists.txt @@ -101,6 +101,27 @@ if (ENABLE_TESTS) ) set_tests_properties(tor_interface_legacy_pluggable_transport_bootstrap_cargo_test PROPERTIES FIXTURES_REQUIRED tor_expert_bundle_target_fixture) endif() + if (BUILD_EXAMPLES) + set(tor_interface_legacy_tor_provider_listener_example_outputs + ${CMAKE_CURRENT_BINARY_DIR}/${CARGO_PROFILE}/examples/legacy-tor-provider-listener${CMAKE_EXECUTABLE_SUFFIX}) + add_custom_command( + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/examples/legacy-tor-provider-listener.rs ${tor_interface_sources} + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${CARGO_PROFILE}/examples/legacy-tor-provider-listener${CMAKE_EXECUTABLE_SUFFIX} + COMMAND env CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} RUSTFLAGS=${RUSTFLAGS} cargo build --example legacy-tor-provider-listener ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + add_custom_target(tor_interface_legacy_tor_provider_listener_example ALL + DEPENDS ${tor_interface_legacy_tor_provider_listener_example_outputs}) + + set(tor_interface_legacy_tor_provider_provider_example_outputs + ${CMAKE_CURRENT_BINARY_DIR}/${CARGO_PROFILE}/examples/legacy-tor-provider-provider${CMAKE_EXECUTABLE_SUFFIX}) + add_custom_command( + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/examples/legacy-tor-provider-provider.rs ${tor_interface_sources} + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${CARGO_PROFILE}/examples/legacy-tor-provider-provider${CMAKE_EXECUTABLE_SUFFIX} + COMMAND env CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} RUSTFLAGS=${RUSTFLAGS} cargo build --example legacy-tor-provider-provider ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + add_custom_target(tor_interface_legacy_tor_provider_provider_example ALL + DEPENDS ${tor_interface_legacy_tor_provider_provider_example_outputs}) + endif() endif() if (ENABLE_ARTI_CLIENT_TOR_PROVIDER) diff --git a/tor-interface/examples/legacy-tor-provider-listener.rs b/tor-interface/examples/legacy-tor-provider-listener.rs new file mode 100644 index 000000000..30618608e --- /dev/null +++ b/tor-interface/examples/legacy-tor-provider-listener.rs @@ -0,0 +1,24 @@ +use std::io::Write; +use tor_interface::legacy_tor_client::{LegacyTorClientConfig, LegacyTorClient}; +use tor_interface::tor_crypto::Ed25519PrivateKey; +use tor_interface::tor_provider::{OnionListener, TorProvider}; + +fn main() { + let mut client = LegacyTorClient::new(LegacyTorClientConfig::system_from_environment().expect("No configuration in the environment")).unwrap(); + client.bootstrap().unwrap(); + println!("{:?}", client.update().unwrap()); + + let pk = Ed25519PrivateKey::generate(); + let ol = client.listener(&pk, 80, None, None).unwrap(); + println!("http://{}.onion", tor_interface::tor_crypto::V3OnionServiceId::from_private_key(&pk)); + + loop { + for u in client.update().unwrap() { + println!("{:?}", u); + } + if let Some(mut peer) = ol.accept().unwrap() { + println!("{:?}", &peer); + peer.write_all(format!("HTTP/1.1 200 OK\r\n\r\n{:?}\n", peer).as_bytes()).unwrap(); + } + } +} diff --git a/tor-interface/examples/legacy-tor-provider-provider.rs b/tor-interface/examples/legacy-tor-provider-provider.rs new file mode 100644 index 000000000..a72fe245f --- /dev/null +++ b/tor-interface/examples/legacy-tor-provider-provider.rs @@ -0,0 +1,33 @@ +use std::io::{BufRead, BufReader, Write}; +use std::str::FromStr; +use tor_interface::legacy_tor_client::{LegacyTorClient, LegacyTorClientConfig}; +use tor_interface::tor_provider::{OnionStream, TargetAddr, TorProvider}; + +fn read_headers(os: S) { + for l in BufReader::new(os).lines().map(Result::unwrap) { + if l.is_empty() { + return; + } + println!("{}", l); + } +} + +fn main() { + let mut client = LegacyTorClient::new(LegacyTorClientConfig::system_from_environment().expect("No configuration in the environment")).unwrap(); + client.bootstrap().unwrap(); + println!("{:?}", client.update().unwrap()); + + let mut sess = client.connect(TargetAddr::from_str("cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion:80").unwrap(), None).unwrap(); + dbg!(&sess); + sess.write(b"GET / HTTP/1.1\r\nHost: cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion\r\nUser-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.1)\r\nAccept: text/html\r\nAccept-Language: en-US, en; q=0.5\r\n\r\n").unwrap(); + sess.flush().unwrap(); + read_headers(sess); + dbg!(client.update().unwrap()); + + let mut sess = client.connect(TargetAddr::from_str("nabijaczleweli.xyz:80").unwrap(), None).unwrap(); + dbg!(&sess); + sess.write(b"GET / HTTP/1.1\r\nHost: nabijaczleweli.xyz\r\nUser-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.1)\r\nAccept: text/html\r\nAccept-Language: en-US, en; q=0.5\r\n\r\n").unwrap(); + sess.flush().unwrap(); + read_headers(sess); + dbg!(client.update().unwrap()); +} From b934f1ca9f0109ee88960f7e29dca1d9d4df7541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 24 Jun 2025 14:33:24 +0200 Subject: [PATCH 176/184] tor-interface: #[derive(ZeroizeOnDrop)] TorAuth --- tor-interface/Cargo.toml | 3 ++- tor-interface/src/legacy_tor_client.rs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index 082bb45c9..a2d9a27bd 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -40,6 +40,7 @@ tor-proto = { version = "0.31.0", features = ["stream-ctrl"], optional = true } tor-rtcompat = { version = "0.31.0", optional = true } hmac = { version = "0.12", optional = true } sha2 = { version = "0.10", optional = true } +zeroize = { version = "1.8", optional = true, features = ["derive"] } [dev-dependencies] anyhow = "1.0" @@ -50,4 +51,4 @@ which = "4.4" arti-client-tor-provider = ["arti-client", "tokio", "tokio-stream", "tokio-mpmc", "tor-cell", "tor-config", "tor-hsservice", "tor-keymgr", "tor-proto", "tor-rtcompat"] arti-tor-provider = ["arti-rpc-client-core"] mock-tor-provider = [] -legacy-tor-provider = ["hmac", "sha2"] +legacy-tor-provider = ["hmac", "sha2", "zeroize"] diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs index 6a72a958e..b3e0681e8 100644 --- a/tor-interface/src/legacy_tor_client.rs +++ b/tor-interface/src/legacy_tor_client.rs @@ -9,6 +9,7 @@ use std::str::FromStr; use std::string::ToString; use std::sync::{atomic, Arc}; use std::time::Duration; +use zeroize::ZeroizeOnDrop; #[cfg(unix)] use std::os::unix::net::SocketAddr as UnixSocketAddr; @@ -175,9 +176,10 @@ pub enum LegacyTorClientConfig { }, } -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ZeroizeOnDrop)] pub enum TorAuth { Password(String), + #[zeroize(skip)] Cookie(PathBuf), CookieData([u8; 32]), } From 2098c9363449fbc0c2747c3bf2617c6514d128eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 24 Jun 2025 15:45:00 +0200 Subject: [PATCH 177/184] tor-interface: add TcpOnionListener::try_clone_inner() --- tor-interface/src/tor_provider.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 486ba6455..0d1fd6032 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -566,6 +566,16 @@ impl OnionListener for TcpOnionListener { } } +impl TcpOnionListener { + /// `TcpListener::try_clone()` the inner listener + /// + /// The lifetime of the hidden service itself is still bound to this object, + /// but the resulting [`TcpListener`] may be polled/`accept`ed independently + pub fn try_clone_inner(&self) -> std::io::Result { + self.0.0.try_clone() + } +} + impl Drop for TcpOnionListener { fn drop(&mut self) { self.1.store(false, atomic::Ordering::Relaxed) From 48098baf93d0803f570b366e0791656a9d67ed4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 24 Jun 2025 15:51:49 +0200 Subject: [PATCH 178/184] tor-interface: add OnionListener::address() --- tor-interface/src/arti_client_tor_client.rs | 4 ++++ tor-interface/src/tor_provider.rs | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs index 732095cf2..3dbbc1166 100644 --- a/tor-interface/src/arti_client_tor_client.rs +++ b/tor-interface/src/arti_client_tor_client.rs @@ -519,4 +519,8 @@ impl OnionListener for ArtiClientOnionListener { self.tokio_runtime.block_on(fut) } } + + fn address(&self) -> &OnionAddr { + &self.onion_addr + } } diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index 0d1fd6032..a86ea7e81 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -523,6 +523,9 @@ pub trait OnionListener: Send { /// Accept a new incoming connection from this listener. fn accept(&self) -> std::io::Result>; + + /// Address this listener is listening on + fn address(&self) -> &OnionAddr; } pub(crate) struct TcpOnionListenerBase(pub TcpListener, pub OnionAddr); @@ -552,6 +555,10 @@ impl OnionListener for TcpOnionListenerBase { } } } + + fn address(&self) -> &OnionAddr { + &self.1 + } } impl OnionListener for TcpOnionListener { @@ -564,6 +571,10 @@ impl OnionListener for TcpOnionListener { fn accept(&self) -> std::io::Result> { self.0.accept() } + + fn address(&self) -> &OnionAddr { + &self.0.address() + } } impl TcpOnionListener { @@ -587,6 +598,7 @@ pub struct BoxOnionListener { set_nonblocking: fn(&Box, bool) -> std::io::Result<()>, accept: fn(&Box) -> std::io::Result::Stream>>, + address: fn(&Box) -> &OnionAddr, } impl BoxOnionListener { @@ -596,6 +608,7 @@ impl BoxOnionListener { set_nonblocking: |slf, nonblocking| slf.downcast_ref::().unwrap().set_nonblocking(nonblocking), accept: |slf| slf.downcast_ref::().unwrap().accept().map(|r| r.map(BoxOnionStream::new)), + address: |slf| slf.downcast_ref::().unwrap().address(), } } } @@ -610,6 +623,10 @@ impl OnionListener for BoxOnionListener { fn accept(&self) -> std::io::Result> { (self.accept)(&self.data) } + + fn address(&self) -> &OnionAddr { + (self.address)(&self.data) + } } /// The `TorProvider` trait allows for high-level Tor Network functionality. Implementations ay connect to the Tor Network, anonymously connect to both clearnet and onion-service endpoints, and host onion-services. From 95ac49d1d222a7417b264c6cfdd933c8d8a41907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 24 Jun 2025 15:55:27 +0200 Subject: [PATCH 179/184] tor-interface: #[derive(Clone, Debug, PartialEq, Eq)] TargetAddr --- tor-interface/src/tor_provider.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs index a86ea7e81..53e04b40e 100644 --- a/tor-interface/src/tor_provider.rs +++ b/tor-interface/src/tor_provider.rs @@ -204,7 +204,7 @@ impl FromStr for DomainAddr { // /// An enum representing the various types of addresses a [`TorProvider`] implementation may connect to. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum TargetAddr { /// An ip address and port Socket(std::net::SocketAddr), From ca675433b943556985837e0e131b565d91527817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 6 Aug 2025 21:08:09 +0200 Subject: [PATCH 180/184] Adapt to work under core --- tor-interface/Cargo.toml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml index a2d9a27bd..1ea368ce9 100644 --- a/tor-interface/Cargo.toml +++ b/tor-interface/Cargo.toml @@ -10,8 +10,8 @@ keywords = ["tor", "anonymity"] repository = "https://github.com/blueprint-freespeech/gosling" [dependencies] -arti-client = { version = "0.31.0", features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} -arti-rpc-client-core = { version = "0.31.0", optional = true } +arti-client = { workspace = true, features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} +arti-rpc-client-core = { version = "0.32.0", optional = true } curve25519-dalek = "4.1" data-encoding = "2.0" data-encoding-macro = "0.1" @@ -31,13 +31,13 @@ thiserror = "1.0" tokio = { version = "1", features = ["macros"], optional = true } tokio-stream = { version = "0", optional = true } tokio-mpmc = { version = "0.2", optional = true } -tor-cell = { version = "0.31.0", optional = true } -tor-config = { version = "0.31.0", optional = true } -tor-hsservice = { version = "0.31.0", optional = true, features = ["restricted-discovery"] } -tor-keymgr = { version = "0.31.0", optional = true, features = ["keymgr"] } -tor-llcrypto = { version = "0.31.0", features = ["relay"] } -tor-proto = { version = "0.31.0", features = ["stream-ctrl"], optional = true } -tor-rtcompat = { version = "0.31.0", optional = true } +tor-cell = { workspace = true, optional = true } +tor-config = { version = "0.32.0", optional = true } +tor-hsservice = { workspace = true, optional = true, features = ["restricted-discovery"] } +tor-keymgr = { version = "0.32.0", optional = true, features = ["keymgr"] } +tor-llcrypto = { version = "0.32.0", features = ["relay"] } +tor-proto = { workspace = true, features = ["stream-ctrl"], optional = true } +tor-rtcompat = { workspace = true, optional = true } hmac = { version = "0.12", optional = true } sha2 = { version = "0.10", optional = true } zeroize = { version = "1.8", optional = true, features = ["derive"] } From c7b344ff3c4617849a387354e041fb1e5ebfc0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Sun, 8 Jun 2025 04:25:38 +0200 Subject: [PATCH 181/184] Make unstoppable swap whonix friendly Detect if running on whonix accd'g to documentation https://www.whonix.org/wiki/Dev/Project_friendly_applications_best_practices#Programmatically_Detecting_Whonix by checking if /usr/share/whonix/marker exists In that case, never initialise a Tor connection to avoid tunneling Tor over Tor, instead log INFO On whonix, not using Tor The settings GUI is adapted to (a) force the slider on (b) disable it (c) note that everything's always on Tor on whonix in the help asb: nabijaczleweli@tarta:~/uwu/core$ ./target/release/asb --testnet start 2025-06-08T02:10:18.426428165Z INFO Initialized tracing. General logs will be written to swap-all.log, and verbose logs to tracing*.log level_filter=debug logs_dir=testnet/logs 2025-06-08T02:10:18.426499966Z INFO Setting up context binary="asb" version="preview-1-gacd9ade-dirty" os="linux" arch="x86_64" 2025-06-08T02:10:18.426566003Z DEBUG Reading in seed from testnet/seed.pem 2025-06-08T02:10:18.42659969Z DEBUG Using existing sqlite database. 2025-06-08T02:10:18.427903356Z DEBUG Opening Monero wallet 2025-06-08T02:10:49.253500345Z DEBUG Created Monero wallet monero_wallet_name=asb-wallet 2025-06-08T02:10:49.338552856Z INFO Monero wallet address monero_address=9tWysVoWvpGR33qBZoTJcq4HzzQoe49HiStVNx5oMVJu6wKzsBYjy1xdegiVYBMZyp3i1kuXmDySqYmJRieKmW4nSWx1kNm 2025-06-08T02:10:49.383891181Z WARN The Monero balance is 0, make sure to deposit funds at monero_address=9tWysVoWvpGR33qBZoTJcq4HzzQoe49HiStVNx5oMVJu6wKzsBYjy1xdegiVYBMZyp3i1kuXmDySqYmJRieKmW4nSWx1kNm 2025-06-08T02:10:49.383950078Z DEBUG Opening Bitcoin wallet 2025-06-08T02:10:50.20519519Z INFO Starting initial Bitcoin wallet scan. This might take a while... 2025-06-08T02:10:51.33690081Z DEBUG Full scanning Bitcoin wallet, currently at index 0. We will scan around 500 in total. 2025-06-08T02:10:51.337269269Z DEBUG Full scanning Bitcoin wallet, currently at index 1. We will scan around 500 in total. 2025-06-08T02:10:52.38770465Z DEBUG Full scanning Bitcoin wallet, currently at index 52. We will scan around 500 in total. 2025-06-08T02:10:53.909561884Z DEBUG Full scanning Bitcoin wallet, currently at index 103. We will scan around 500 in total. 2025-06-08T02:10:54.687464795Z DEBUG Full scanning Bitcoin wallet, currently at index 153. We will scan around 500 in total. 2025-06-08T02:10:56.313618739Z DEBUG Full scanning Bitcoin wallet, currently at index 203. We will scan around 500 in total. 2025-06-08T02:10:57.145982225Z DEBUG Full scanning Bitcoin wallet, currently at index 254. We will scan around 500 in total. 2025-06-08T02:10:58.886810752Z DEBUG Full scanning Bitcoin wallet, currently at index 305. We will scan around 500 in total. 2025-06-08T02:11:00.572357489Z DEBUG Full scanning Bitcoin wallet, currently at index 356. We will scan around 500 in total. 2025-06-08T02:11:02.395423731Z DEBUG Full scanning Bitcoin wallet, currently at index 432. We will scan around 532 in total. 2025-06-08T02:11:19.093438653Z DEBUG Not syncing because there are no spks in our wallet 2025-06-08T02:11:19.093475438Z DEBUG Starting to sync Bitcoin wallet with 0 concurrent chunks and batch size of 32 2025-06-08T02:11:19.12235274Z INFO Bitcoin wallet balance bitcoin_balance=0 BTC 2025-06-08T02:11:19.375064128Z DEBUG Bootstrapping Tor client 2025-06-08T02:11:20.677955503Z DEBUG Connected to Kraken websocket API 2025-06-08T02:11:20.678051027Z DEBUG Subscribed to updates for ticker 2025-06-08T02:11:24.434699921Z DEBUG Setting up onion service for libp2p to listen on addr=/onion3/6b5rqgvgofdi7qx6um25hfitolhxkxnfqxlogicqxhl7xjtxsvrpv6yd:9939 2025-06-08T02:11:24.566743497Z INFO Network layer initialized peer_id=12D3KooWHGUUHbxUpBXjfL1u6JWknBfETHkCEbWFozhf9nHcHi9K 2025-06-08T02:11:24.60886221Z INFO New listen address reported address=/onion3/6b5rqgvgofdi7qx6um25hfitolhxkxnfqxlogicqxhl7xjtxsvrpv6yd:9939 2025-06-08T02:11:24.613757136Z INFO New listen address reported address=/ip4/127.0.0.1/tcp/9939 2025-06-08T02:11:24.613836425Z INFO New listen address reported address=/ip4/192.168.1.250/tcp/9939 2025-06-08T02:11:24.6139116Z INFO New listen address reported address=/ip4/10.0.2.2/tcp/9939 nabijaczleweli@tarta:~/uwu/core$ unshare -rm root@tarta:~/uwu/core# mount -t tmpfs tmpfs /usr/share root@tarta:~/uwu/core# mkdir /usr/share/whonix root@tarta:~/uwu/core# > /usr/share/whonix/marker root@tarta:~/uwu/core# ./target/release/asb --testnet start 2025-06-08T02:12:07.15259209Z INFO Initialized tracing. General logs will be written to swap-all.log, and verbose logs to tracing*.log level_filter=debug logs_dir=testnet/logs 2025-06-08T02:12:07.152648706Z INFO Setting up context binary="asb" version="preview-1-gacd9ade-dirty" os="linux" arch="x86_64" 2025-06-08T02:12:07.152718078Z DEBUG Reading in seed from testnet/seed.pem 2025-06-08T02:12:07.1527521Z DEBUG Using existing sqlite database. 2025-06-08T02:12:07.15392353Z DEBUG Opening Monero wallet 2025-06-08T02:12:20.274522678Z INFO Monero wallet address monero_address=9tWysVoWvpGR33qBZoTJcq4HzzQoe49HiStVNx5oMVJu6wKzsBYjy1xdegiVYBMZyp3i1kuXmDySqYmJRieKmW4nSWx1kNm 2025-06-08T02:12:20.27523474Z WARN The Monero balance is 0, make sure to deposit funds at monero_address=9tWysVoWvpGR33qBZoTJcq4HzzQoe49HiStVNx5oMVJu6wKzsBYjy1xdegiVYBMZyp3i1kuXmDySqYmJRieKmW4nSWx1kNm 2025-06-08T02:12:20.360353998Z DEBUG Opening Bitcoin wallet 2025-06-08T02:12:20.555861074Z DEBUG Loading existing Bitcoin wallet from database 2025-06-08T02:12:20.802818807Z DEBUG Not syncing because there are no spks in our wallet 2025-06-08T02:12:20.802858811Z DEBUG Starting to sync Bitcoin wallet with 0 concurrent chunks and batch size of 32 2025-06-08T02:12:20.802982132Z INFO Bitcoin wallet balance bitcoin_balance=0 BTC 2025-06-08T02:12:20.803068289Z INFO On whonix, not using Tor 2025-06-08T02:12:20.897994249Z INFO Network layer initialized peer_id=12D3KooWHGUUHbxUpBXjfL1u6JWknBfETHkCEbWFozhf9nHcHi9K 2025-06-08T02:12:21.086817444Z DEBUG Connected to Kraken websocket API 2025-06-08T02:12:21.111092228Z INFO New listen address reported address=/ip4/127.0.0.1/tcp/9939 2025-06-08T02:12:21.111314184Z INFO New listen address reported address=/ip4/192.168.1.250/tcp/9939 2025-06-08T02:12:21.111465536Z INFO New listen address reported address=/ip4/10.0.2.2/tcp/9939 2025-06-08T02:12:21.14171648Z DEBUG Subscribed to updates for ticker swap: nabijaczleweli@tarta:~/uwu/core$ ./target/release/swap --testnet resume --enable-tor --swap-id 69420694206942069420694206942069 2025-06-08T02:24:20.130489739Z INFO Initialized tracing. General logs will be written to swap-all.log, and verbose logs to tracing*.log level_filter=info logs_dir=/home/nabijaczleweli/.local/share/xmr-btc-swap/cli/testnet/logs 2025-06-08T02:24:20.130752975Z INFO Setting up context binary="cli" version="preview-1-gacd9ade-dirty" os="linux" arch="x86_64" 2025-06-08T02:24:20.275872264Z DEBUG Bootstrapping Tor client ^C nabijaczleweli@tarta:~/uwu/core$ unshare -rm root@tarta:~/uwu/core# mount -t tmpfs tmpfs /usr/share root@tarta:~/uwu/core# mkdir /usr/share/whonix root@tarta:~/uwu/core# > /usr/share/whonix/marker root@tarta:~/uwu/core# ./target/release/swap --testnet resume --enable-tor --swap-id 69420694206942069420694206942069 2025-06-08T02:24:37.62796112Z INFO Initialized tracing. General logs will be written to swap-all.log, and verbose logs to tracing*.log level_filter=info logs_dir=/home/nabijaczleweli/.local/share/xmr-btc-swap/cli/testnet/logs 2025-06-08T02:24:37.628228005Z INFO Setting up context binary="cli" version="preview-1-gacd9ade-dirty" os="linux" arch="x86_64" 2025-06-08T02:24:37.642638339Z INFO On whonix, not using Tor ^C Bounty: https://bounties.monero.social/posts/180/0-789m-make-unstoppable-swap-whonix-friendly --- Cargo.lock | 1 + .../renderer/components/pages/help/SettingsBox.tsx | 8 +++++--- src-gui/src/renderer/rpc.ts | 5 +++++ src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 7 +++++++ swap-asb/src/main.rs | 12 ++++++++---- swap-env/src/env.rs | 9 +++++++++ swap/src/cli/api.rs | 7 ++++++- 8 files changed, 42 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 038b62f22..243ed3fe5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13247,6 +13247,7 @@ dependencies = [ "serde", "serde_json", "swap", + "swap-env", "tauri", "tauri-build", "tauri-plugin-cli", diff --git a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx index 230bf5d7f..05b4b867c 100644 --- a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx +++ b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx @@ -62,6 +62,7 @@ import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox"; import { isValidMultiAddressWithPeerId } from "utils/parseUtils"; import { getNodeStatus } from "renderer/rpc"; import { setStatus } from "store/features/nodesSlice"; +import { getTorForced } from "../../../rpc"; const PLACEHOLDER_ELECTRUM_RPC_URL = "ssl://blockstream.info:700"; const PLACEHOLDER_MONERO_NODE_URL = "http://xmr-node.cakewallet.com:18081"; @@ -694,24 +695,25 @@ function NodeTable({ ); } +const torForced = await getTorForced(); export function TorSettings() { const dispatch = useAppDispatch(); const torEnabled = useSettings((settings) => settings.enableTor); const handleChange = (event: React.ChangeEvent) => dispatch(setTorEnabled(event.target.checked)); - const status = (state: boolean) => (state === true ? "enabled" : "disabled"); return ( - + ); diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index e9fe8eff2..26b7d046f 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -280,6 +280,11 @@ export async function checkContextAvailability(): Promise { return available; } +export async function getTorForced(): Promise { + const forced = await invokeNoArgs("get_tor_forced"); + return forced; +} + export async function getLogsOfSwap( swapId: string, redact: boolean, diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 71bd04ba5..0facb23a4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ rustls = { version = "0.23.26", default-features = false, features = ["ring"] } serde = { workspace = true } serde_json = { workspace = true } swap = { path = "../swap", features = [ "tauri" ] } +swap-env = { path = "../swap-env" } tauri = { version = "^2.0.0", features = [ "config-json5" ] } tauri-plugin-clipboard-manager = "^2.0.0" tauri-plugin-dialog = "2.2.2" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4cd8c3947..812a99f73 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -22,6 +22,7 @@ use swap::cli::{ }, command::Bitcoin, }; +use swap_env::env::may_init_tor; use tauri::{async_runtime::RwLock, Manager, RunEvent}; use tauri_plugin_dialog::DialogExt; use zip::{write::SimpleFileOptions, ZipWriter}; @@ -180,6 +181,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ get_balance, get_monero_addresses, + get_tor_forced, get_swap_info, get_swap_infos_all, withdraw_btc, @@ -276,6 +278,11 @@ async fn is_context_available(state: tauri::State<'_, State>) -> Result>) -> Result { + Ok(!may_init_tor()) +} + #[tauri::command] async fn check_monero_node( args: CheckMoneroNodeArgs, diff --git a/swap-asb/src/main.rs b/swap-asb/src/main.rs index f9d3e2fea..e4f7d8cdc 100644 --- a/swap-asb/src/main.rs +++ b/swap-asb/src/main.rs @@ -39,7 +39,7 @@ use swap::protocol::{Database, State}; use swap::seed::Seed; use swap::{bitcoin, monero}; use swap_env::config::{ - initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized, + may_init_tor, initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized, }; use swap_feed; use tracing_subscriber::filter::LevelFilter; @@ -218,9 +218,13 @@ pub async fn main() -> Result<()> { let namespace = XmrBtcNamespace::from_is_testnet(testnet); // Initialize and bootstrap Tor client - let tor_client = create_tor_client(&config.data.dir).await?; - bootstrap_tor_client(tor_client.clone(), None).await?; - let tor_client = tor_client.into(); + let tor_client = if may_init_tor() { + let tor_client = create_tor_client(&config.data.dir).await?; + bootstrap_tor_client(tor_client.clone(), None).await?; + Some(tor_client.into()) + } else { + None + }; let (mut swarm, onion_addresses) = swarm::asb( &seed, diff --git a/swap-env/src/env.rs b/swap-env/src/env.rs index 6158274ce..eda657d80 100644 --- a/swap-env/src/env.rs +++ b/swap-env/src/env.rs @@ -1,6 +1,7 @@ use crate::config::Config as AsbConfig; use serde::Serialize; use std::cmp::max; +use std::fs; use std::time::Duration; use time::ext::NumericalStdDuration; @@ -136,6 +137,14 @@ pub fn new(is_testnet: bool, asb_config: &AsbConfig) -> Config { } } +pub fn may_init_tor() -> bool { + let is_whonix = fs::exists("/usr/share/whonix/marker").unwrap_or(false); + if is_whonix { + tracing::info!("On whonix, not starting Tor"); + } + !is_whonix +} + #[cfg(test)] mod tests { use super::*; diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index 843eac4a9..32466b9fe 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -17,7 +17,7 @@ use std::fmt; use std::future::Future; use std::path::{Path, PathBuf}; use std::sync::{Arc, Once}; -use swap_env::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet}; +use swap_env::env::{may_init_tor, Config as EnvConfig, GetConfig, Mainnet, Testnet}; use swap_fs::system_data_dir; use tauri_bindings::{ MoneroNodeConfig, TauriBackgroundProgress, TauriContextStatusEvent, TauriEmitter, TauriHandle, @@ -486,6 +486,11 @@ impl ContextBuilder { }; let bootstrap_tor_client_task = async { + // Don't init a tor client unless we should use it. + if !may_init_tor() { + return Ok(None); + } + // Bootstrap the Tor client if we have one match unbootstrapped_tor_client.clone() { Some(tor_client) => { From 09e4123a0832c383753818dc402e5c558367bf91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Thu, 12 Jun 2025 22:52:51 +0200 Subject: [PATCH 182/184] Use system Tor daemon for listening for connections on whonix. Skip irrelevant/would-be-misleading parts of the questionnaire on whonix --- Cargo.lock | 186 +++++++++++++++++++++++++++++++------- swap-asb/src/main.rs | 3 +- swap-env/src/env.rs | 6 +- swap-env/src/prompt.rs | 9 ++ swap/Cargo.toml | 1 + swap/src/asb/network.rs | 74 +++++++++++++-- swap/src/common/tor.rs | 13 +++ swap/src/network/swarm.rs | 3 + swap/tests/harness/mod.rs | 1 + 9 files changed, 253 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 243ed3fe5..c3c4f1869 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,7 +300,7 @@ dependencies = [ "once_cell", "postage", "rand 0.9.2", - "safelog", + "safelog 0.4.7", "serde", "thiserror 2.0.12", "time 0.3.41", @@ -318,7 +318,7 @@ dependencies = [ "tor-hsservice", "tor-keymgr", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-memquota", "tor-netdir", "tor-netdoc", @@ -2937,6 +2937,19 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "domain" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd50aea158e9a57c9c9075ca7a3dfa4c08d9a468b405832383876f9df85379b" +dependencies = [ + "bytes", + "octseq", + "pin-project-lite", + "rand 0.8.5", + "time 0.3.41", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -5487,6 +5500,20 @@ dependencies = [ "void", ] +[[package]] +name = "libp2p-community-tor-interface" +version = "0.1.0" +source = "git+https://github.com/nabijaczleweli/libp2p-tor-interface#e7a37fc32a054de2d19fa334d5c02a10972876de" +dependencies = [ + "anyhow", + "futures", + "libp2p", + "thiserror 1.0.69", + "tokio", + "tor-interface", + "tracing", +] + [[package]] name = "libp2p-connection-limits" version = "0.3.1" @@ -7115,6 +7142,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "octseq" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126c3ca37c9c44cec575247f43a3e4374d8927684f129d2beeb0d2cef262fe12" +dependencies = [ + "bytes", +] + [[package]] name = "oid-registry" version = "0.6.1" @@ -9101,6 +9137,19 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "safelog" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4e1c994fbc7521a5003e5c1c54304654ea0458881e777f6e2638520c2de8c5" +dependencies = [ + "derive_more 2.0.1", + "educe", + "either", + "fluid-let", + "thiserror 2.0.12", +] + [[package]] name = "same-file" version = "1.0.6" @@ -9978,6 +10027,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "socks" +version = "0.3.4" +source = "git+https://github.com/nabijaczleweli/rust-socks#a1182ee5024d06cba2104a1145dd648e10a90468" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + [[package]] name = "softbuffer" version = "0.4.6" @@ -10511,6 +10570,7 @@ dependencies = [ "hex", "jsonrpsee", "libp2p", + "libp2p-community-tor-interface", "libp2p-tor", "mockito", "moka", @@ -11822,10 +11882,10 @@ dependencies = [ "digest 0.10.7", "educe", "getrandom 0.3.3", - "safelog", + "safelog 0.4.7", "thiserror 2.0.12", "tor-error", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "zeroize", ] @@ -11851,7 +11911,7 @@ dependencies = [ "tor-error", "tor-hscrypto", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-memquota", "tor-protover", "tor-units", @@ -11870,7 +11930,7 @@ dependencies = [ "thiserror 2.0.12", "tor-bytes", "tor-checkable", - "tor-llcrypto", + "tor-llcrypto 0.32.0", ] [[package]] @@ -11887,7 +11947,7 @@ dependencies = [ "oneshot-fused-workaround", "postage", "rand 0.9.2", - "safelog", + "safelog 0.4.7", "serde", "thiserror 2.0.12", "tor-async-utils", @@ -11896,7 +11956,7 @@ dependencies = [ "tor-config", "tor-error", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-memquota", "tor-netdir", "tor-proto", @@ -11915,7 +11975,7 @@ dependencies = [ "humantime", "signature 2.2.0", "thiserror 2.0.12", - "tor-llcrypto", + "tor-llcrypto 0.32.0", ] [[package]] @@ -11940,7 +12000,7 @@ dependencies = [ "pin-project", "rand 0.9.2", "retry-error", - "safelog", + "safelog 0.4.7", "serde", "static_assertions", "thiserror 2.0.12", @@ -12018,7 +12078,7 @@ dependencies = [ "digest 0.10.7", "hex", "thiserror 2.0.12", - "tor-llcrypto", + "tor-llcrypto 0.32.0", ] [[package]] @@ -12041,7 +12101,7 @@ dependencies = [ "tor-error", "tor-hscrypto", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-netdoc", "tor-proto", "tor-rtcompat", @@ -12073,7 +12133,7 @@ dependencies = [ "postage", "rand 0.9.2", "rusqlite", - "safelog", + "safelog 0.4.7", "scopeguard", "serde", "serde_json", @@ -12091,7 +12151,7 @@ dependencies = [ "tor-dirclient", "tor-error", "tor-guardmgr", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-netdir", "tor-netdoc", "tor-persist", @@ -12148,7 +12208,7 @@ dependencies = [ "pin-project", "postage", "rand 0.9.2", - "safelog", + "safelog 0.4.7", "serde", "strum 0.27.2", "thiserror 2.0.12", @@ -12157,7 +12217,7 @@ dependencies = [ "tor-config", "tor-error", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-netdir", "tor-netdoc", "tor-persist", @@ -12184,7 +12244,7 @@ dependencies = [ "postage", "rand 0.9.2", "retry-error", - "safelog", + "safelog 0.4.7", "slotmap-careful", "strum 0.27.2", "thiserror 2.0.12", @@ -12200,7 +12260,7 @@ dependencies = [ "tor-hscrypto", "tor-keymgr", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-memquota", "tor-netdir", "tor-netdoc", @@ -12226,7 +12286,7 @@ dependencies = [ "itertools 0.14.0", "paste", "rand 0.9.2", - "safelog", + "safelog 0.4.7", "serde", "signature 2.2.0", "subtle", @@ -12235,7 +12295,7 @@ dependencies = [ "tor-bytes", "tor-error", "tor-key-forge", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-memquota", "tor-units", "void", @@ -12269,7 +12329,7 @@ dependencies = [ "rand 0.9.2", "rand_core 0.9.3", "retry-error", - "safelog", + "safelog 0.4.7", "serde", "serde_with 3.14.0", "strum 0.27.2", @@ -12286,7 +12346,7 @@ dependencies = [ "tor-hscrypto", "tor-keymgr", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-log-ratelim", "tor-netdir", "tor-netdoc", @@ -12299,6 +12359,30 @@ dependencies = [ "void", ] +[[package]] +name = "tor-interface" +version = "0.5.0" +source = "git+https://github.com/nabijaczleweli/gosling?rev=65da8990e33c674ed8abeb10b454b4a39463d81d#65da8990e33c674ed8abeb10b454b4a39463d81d" +dependencies = [ + "curve25519-dalek 4.1.3", + "data-encoding", + "data-encoding-macro", + "domain", + "hmac", + "idna", + "rand 0.9.2", + "rand_core 0.9.3", + "regex", + "sha1", + "sha2 0.10.9", + "sha3", + "signature 1.6.4", + "socks", + "static_assertions", + "thiserror 1.0.69", + "tor-llcrypto 0.31.0", +] + [[package]] name = "tor-key-forge" version = "0.32.0" @@ -12316,7 +12400,7 @@ dependencies = [ "tor-cert", "tor-checkable", "tor-error", - "tor-llcrypto", + "tor-llcrypto 0.32.0", ] [[package]] @@ -12349,7 +12433,7 @@ dependencies = [ "tor-error", "tor-hscrypto", "tor-key-forge", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-persist", "tracing", "visibility", @@ -12370,7 +12454,7 @@ dependencies = [ "derive_more 2.0.1", "hex", "itertools 0.14.0", - "safelog", + "safelog 0.4.7", "serde", "serde_with 3.14.0", "strum 0.27.2", @@ -12378,11 +12462,49 @@ dependencies = [ "tor-basic-utils", "tor-bytes", "tor-config", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-memquota", "tor-protover", ] +[[package]] +name = "tor-llcrypto" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4def0c5674fa67c5b8a8fb8814c0381570e4866842c233ac98982e78779339bf" +dependencies = [ + "aes", + "base64ct", + "ctr", + "curve25519-dalek 4.1.3", + "der-parser 10.0.0", + "derive_more 2.0.1", + "digest 0.10.7", + "ed25519-dalek 2.2.0", + "educe", + "getrandom 0.3.3", + "hex", + "once_cell", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_core 0.6.4", + "rand_core 0.9.3", + "rand_jitter", + "rdrand", + "rsa", + "safelog 0.4.8", + "serde", + "sha1", + "sha2 0.10.9", + "sha3", + "signature 2.2.0", + "subtle", + "thiserror 2.0.12", + "visibility", + "x25519-dalek", + "zeroize", +] + [[package]] name = "tor-llcrypto" version = "0.32.0" @@ -12407,7 +12529,7 @@ dependencies = [ "rand_jitter", "rdrand", "rsa", - "safelog", + "safelog 0.4.7", "serde", "sha1", "sha2 0.10.9", @@ -12486,7 +12608,7 @@ dependencies = [ "tor-error", "tor-hscrypto", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-netdoc", "tor-protover", "tor-units", @@ -12529,7 +12651,7 @@ dependencies = [ "tor-error", "tor-hscrypto", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-protover", "tor-units", "void", @@ -12594,7 +12716,7 @@ dependencies = [ "postage", "rand 0.9.2", "rand_core 0.9.3", - "safelog", + "safelog 0.4.7", "slotmap-careful", "smallvec", "static_assertions", @@ -12613,7 +12735,7 @@ dependencies = [ "tor-error", "tor-hscrypto", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0", "tor-log-ratelim", "tor-memquota", "tor-protover", @@ -12718,7 +12840,7 @@ dependencies = [ "caret", "derive-deftly 1.1.0", "educe", - "safelog", + "safelog 0.4.7", "subtle", "thiserror 2.0.12", "tor-bytes", diff --git a/swap-asb/src/main.rs b/swap-asb/src/main.rs index e4f7d8cdc..477b82c63 100644 --- a/swap-asb/src/main.rs +++ b/swap-asb/src/main.rs @@ -236,11 +236,12 @@ pub async fn main() -> Result<()> { namespace, &rendezvous_addrs, tor_client, + &config.data.dir, config.tor.register_hidden_service, config.tor.hidden_service_num_intro_points, )?; - for listen in config.network.listen.clone() { + for listen in &config.network.listen { if let Err(e) = Swarm::listen_on(&mut swarm, listen.clone()) { tracing::warn!("Failed to listen on network interface {}: {}. Consider removing it from the config.", listen, e); } diff --git a/swap-env/src/env.rs b/swap-env/src/env.rs index eda657d80..1c00eaf3b 100644 --- a/swap-env/src/env.rs +++ b/swap-env/src/env.rs @@ -137,8 +137,12 @@ pub fn new(is_testnet: bool, asb_config: &AsbConfig) -> Config { } } +pub fn is_whonix() -> bool { + fs::exists("/usr/share/whonix/marker").unwrap_or(false) +} + pub fn may_init_tor() -> bool { - let is_whonix = fs::exists("/usr/share/whonix/marker").unwrap_or(false); + let is_whonix = is_whonix(); if is_whonix { tracing::info!("On whonix, not starting Tor"); } diff --git a/swap-env/src/prompt.rs b/swap-env/src/prompt.rs index 38b95df6f..1297fdd67 100644 --- a/swap-env/src/prompt.rs +++ b/swap-env/src/prompt.rs @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf}; use crate::defaults::{ default_rendezvous_points, DEFAULT_MAX_BUY_AMOUNT, DEFAULT_MIN_BUY_AMOUNT, DEFAULT_SPREAD, }; +use crate::env::may_init_tor; use anyhow::{bail, Context, Result}; use dialoguer::Confirm; use dialoguer::{theme::ColorfulTheme, Input, Select}; @@ -36,6 +37,10 @@ pub fn bitcoin_confirmation_target(default_target: u16) -> Result { /// Prompt user for listen addresses pub fn listen_addresses(default_listen_address: &Multiaddr) -> Result> { + if !may_init_tor() { + return Ok(vec![]); + } + let listen_addresses = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter multiaddresses (comma separated) on which asb should list for peer-to-peer communications or hit return to use default") .default(format!("{}", default_listen_address)) @@ -122,6 +127,10 @@ pub fn monero_daemon_url() -> Result> { /// Prompt user for Tor hidden service registration pub fn tor_hidden_service() -> Result { + if !may_init_tor() { + return Ok(true); + } + println!("Your ASB needs to be reachable from the outside world to provide quotes to takers."); println!( "Your ASB can run a hidden service for itself. It'll be reachable at an .onion address." diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 8c9852381..78e8f4f57 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -47,6 +47,7 @@ hex = { workspace = true } jsonrpsee = { workspace = true, features = ["server"] } libp2p = { workspace = true, features = ["tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa"] } libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service"] } +libp2p-community-tor-interface = { git = "https://github.com/nabijaczleweli/libp2p-tor-interface", features = ["legacy-tor-provider"] } moka = { version = "0.12", features = ["sync", "future"] } monero = { workspace = true } monero-rpc = { path = "../monero-rpc" } diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index 16c62b42a..5f5308425 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -22,12 +22,15 @@ use swap_feed::LatestRate; use uuid::Uuid; pub mod transport { - use std::sync::Arc; + use std::sync::{Arc, Mutex}; + use std::path::Path; + use std::fs; + use std::io::Write; use arti_client::{config::onion_service::OnionServiceConfigBuilder, TorClient}; - use libp2p::{core::transport::OptionalTransport, dns, identity, tcp, Transport}; - use libp2p_tor::AddressConversion; + use libp2p::{core::transport::{OptionalTransport, OrTransport}, dns, identity, tcp, Transport}; use tor_rtcompat::tokio::TokioRustlsRuntime; + use crate::common::tor::existing_tor_config; use super::*; @@ -36,6 +39,16 @@ pub mod transport { type OnionTransportWithAddresses = (Boxed<(PeerId, StreamMuxerBox)>, Vec); + fn mode600(m: &mut fs::OpenOptions) -> &mut fs::OpenOptions { + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + m.mode(0o600) + } + #[cfg(not(unix))] + m + } + /// Creates the libp2p transport for the ASB. /// /// If you pass in a `None` for `maybe_tor_client`, the ASB will not use Tor at all. @@ -46,12 +59,51 @@ pub mod transport { pub fn new( identity: &identity::Keypair, maybe_tor_client: Option>>, + config_data_dir: &Path, register_hidden_service: bool, num_intro_points: u8, ) -> Result { - let (maybe_tor_transport, onion_addresses) = if let Some(tor_client) = maybe_tor_client { + let (yesmaybe, maybe_tor_transport, onion_addresses) = if let Some((reuse_config, bindaddr)) = existing_tor_config() { + let client = libp2p_community_tor_interface::tor_interface::legacy_tor_client::LegacyTorClient::new(reuse_config)?; + let mut tor_transport = libp2p_community_tor_interface::TorInterfaceTransport::from_provider( + libp2p_community_tor_interface::AddressConversion::DnsOnly, Arc::new(Mutex::new(client)), None)?; + + let pk_path = config_data_dir.join(ASB_ONION_SERVICE_NICKNAME).with_extension("pk"); + let loaded_pk = fs::read_to_string(&pk_path).ok() + .and_then(|pk| libp2p_community_tor_interface::tor_interface::tor_crypto::Ed25519PrivateKey::from_key_blob(pk.lines().next()?).ok()); + + let addresses = if register_hidden_service { + match tor_transport.add_customised_onion_service(loaded_pk.as_ref(), ASB_ONION_SERVICE_PORT, None, bindaddr) + { + Ok((addr, pk)) => { + tracing::debug!( + %addr, + "Setting up onion service for libp2p to listen on" + ); + if loaded_pk.is_none() { + let writeback = pk.to_key_blob(); + let _ = mode600(fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true)) + .open(&pk_path) + .and_then(|mut f| f.write_all(writeback.as_bytes()).and_then(|_| f.write_all(b"\n"))); + } + vec![addr] + } + Err(err) => { + tracing::warn!(error=%err, "Failed to listen on onion address"); + vec![] + } + } + } else { + vec![] + }; + + (true, OrTransport::new(OptionalTransport::none(), OptionalTransport::some(tor_transport)), addresses) + } else if let Some(tor_client) = maybe_tor_client { let mut tor_transport = - libp2p_tor::TorTransport::from_client(tor_client, AddressConversion::DnsOnly); + libp2p_tor::TorTransport::from_client(tor_client, libp2p_tor::AddressConversion::DnsOnly); let addresses = if register_hidden_service { let onion_service_config = OnionServiceConfigBuilder::default() @@ -82,13 +134,17 @@ pub mod transport { vec![] }; - (OptionalTransport::some(tor_transport), addresses) + (true, OrTransport::new(OptionalTransport::some(tor_transport), OptionalTransport::none()), addresses) } else { - (OptionalTransport::none(), vec![]) + (false, OrTransport::new(OptionalTransport::none(), OptionalTransport::none()), vec![]) }; - let tcp = maybe_tor_transport - .or_transport(tcp::tokio::Transport::new(tcp::Config::new().nodelay(true))); + let tcp = OrTransport::new(maybe_tor_transport, + if yesmaybe { + OptionalTransport::none() + } else { + OptionalTransport::some(tcp::tokio::Transport::new(tcp::Config::new().nodelay(true))) + }); let tcp_with_dns = dns::tokio::Transport::system(tcp)?; Ok(( diff --git a/swap/src/common/tor.rs b/swap/src/common/tor.rs index 7b61b3513..f0a438556 100644 --- a/swap/src/common/tor.rs +++ b/swap/src/common/tor.rs @@ -6,8 +6,21 @@ use crate::cli::api::tauri_bindings::{ }; use arti_client::{config::TorClientConfigBuilder, status::BootstrapStatus, Error, TorClient}; use futures::StreamExt; +use swap_env::env::is_whonix; use tor_rtcompat::tokio::TokioRustlsRuntime; +pub fn existing_tor_config() -> Option<( + libp2p_community_tor_interface::tor_interface::legacy_tor_client::LegacyTorClientConfig, + std::net::SocketAddr, +)> { + if is_whonix() { + Some((libp2p_community_tor_interface::tor_interface::legacy_tor_client::LegacyTorClientConfig::system_from_environment().expect("whonix always has $TOR_... set"), + ([0, 0, 0, 0], 9939).into())) + } else { + None + } +} + /// Creates an unbootstrapped Tor client pub async fn create_tor_client( data_dir: &Path, diff --git a/swap/src/network/swarm.rs b/swap/src/network/swarm.rs index 2e40668e1..74ab08665 100644 --- a/swap/src/network/swarm.rs +++ b/swap/src/network/swarm.rs @@ -9,6 +9,7 @@ use libp2p::swarm::NetworkBehaviour; use libp2p::SwarmBuilder; use libp2p::{identity, Multiaddr, Swarm}; use std::fmt::Debug; +use std::path::Path; use std::sync::Arc; use std::time::Duration; use swap_env::env; @@ -25,6 +26,7 @@ pub fn asb( namespace: XmrBtcNamespace, rendezvous_addrs: &[Multiaddr], maybe_tor_client: Option>>, + config_data_dir: &Path, register_hidden_service: bool, num_intro_points: u8, ) -> Result<(Swarm>, Vec)> @@ -57,6 +59,7 @@ where let (transport, onion_addresses) = asb::transport::new( &identity, maybe_tor_client, + config_data_dir, register_hidden_service, num_intro_points, )?; diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index 3ef4ce87a..7f9103f56 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -262,6 +262,7 @@ async fn start_alice( XmrBtcNamespace::Testnet, &[], None, + &db_path, false, 1, ) From 58d59348cb85ecd7fecc6c9f013a83d99f712c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 6 Aug 2025 21:43:44 +0200 Subject: [PATCH 183/184] Apply https://github.com/nabijaczleweli/libp2p-tor bce21996c7c3cfcc9f640244aa14d51f8ea8d9ee..df7553d1671679e2d7b86defae8f91f2b86b4135 --- libp2p-tor/Cargo.toml | 9 + libp2p-tor/README.md | 7 + .../examples/ping-onion-tor-interface.rs | 146 ++++++ libp2p-tor/src/{ => arti}/address.rs | 0 libp2p-tor/src/arti/mod.rs | 413 ++++++++++++++++ libp2p-tor/src/{ => arti}/provider.rs | 0 libp2p-tor/src/lib.rs | 451 +----------------- libp2p-tor/src/tor/address.rs | 161 +++++++ libp2p-tor/src/tor/mod.rs | 349 ++++++++++++++ libp2p-tor/src/tor/provider.rs | 161 +++++++ 10 files changed, 1270 insertions(+), 427 deletions(-) create mode 100644 libp2p-tor/examples/ping-onion-tor-interface.rs rename libp2p-tor/src/{ => arti}/address.rs (100%) create mode 100644 libp2p-tor/src/arti/mod.rs rename libp2p-tor/src/{ => arti}/provider.rs (100%) create mode 100644 libp2p-tor/src/tor/address.rs create mode 100644 libp2p-tor/src/tor/mod.rs create mode 100644 libp2p-tor/src/tor/provider.rs diff --git a/libp2p-tor/Cargo.toml b/libp2p-tor/Cargo.toml index 2dfc376cb..210aea82d 100644 --- a/libp2p-tor/Cargo.toml +++ b/libp2p-tor/Cargo.toml @@ -15,6 +15,8 @@ thiserror = { workspace = true } tokio = { workspace = true } arti-client = { workspace = true, features = ["tokio", "rustls", "onion-service-client", "static-sqlite"] } +# tor-interface = { git = "https://github.com/nabijaczleweli/gosling", rev = "32988e5770c12f1b48b865c158509473123eae90", optional = true } +tor-interface = { path = "../tor-interface", optional = true } libp2p = { workspace = true, features = ["tokio", "tcp", "tls"] } data-encoding = { version = "2.6.0" } @@ -37,11 +39,18 @@ listen-onion-service = [ "dep:tor-cell", "dep:tor-proto", ] +arti-client-tor-provider = ["tor-interface", "tor-interface/arti-client-tor-provider"] +legacy-tor-provider = ["tor-interface", "tor-interface/legacy-tor-provider"] +mock-tor-provider = ["tor-interface", "tor-interface/mock-tor-provider"] [[example]] name = "ping-onion" required-features = ["listen-onion-service"] +[[example]] +name = "ping-onion-tor-interface" +required-features = ["tor-interface"] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/libp2p-tor/README.md b/libp2p-tor/README.md index ecb7ecbc5..36af54031 100644 --- a/libp2p-tor/README.md +++ b/libp2p-tor/README.md @@ -43,6 +43,13 @@ let mut transport = libp2p_tor::TorTransport::bootstrapped().await?; // we have achieved tor connection let _conn = transport.dial(address)?.await?; ``` +```rust +let address = "/dns/www.torproject.org/tcp/1000".parse()?; +let mut provider = libp2p_tor_interface::tor_interface::/* whichever one you want */; +let mut transport = libp2p_tor_interface::TorInterfaceTransport::from_provider(Default::default(), Arc::new(Mutex::new(provider)), None); +// we have achieved tor connection +let _conn = transport.dial(address)?.await?; +``` ### About diff --git a/libp2p-tor/examples/ping-onion-tor-interface.rs b/libp2p-tor/examples/ping-onion-tor-interface.rs new file mode 100644 index 000000000..b5710f186 --- /dev/null +++ b/libp2p-tor/examples/ping-onion-tor-interface.rs @@ -0,0 +1,146 @@ +// Copyright 2022 Hannes Furmans +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Ping-Onion example +//! +//! See ../src/tutorial.rs for a step-by-step guide building the example below. +//! +//! This example requires two seperate computers, one of which has to be reachable from the +//! internet. +//! +//! On the first computer run: +//! ```sh +//! cargo run --example ping +//! ``` +//! +//! It will print the PeerId and the listening addresses, e.g. `Listening on +//! "/ip4/0.0.0.0/tcp/24915"` +//! +//! Make sure that the first computer is reachable under one of these ip addresses and port. +//! +//! On the second computer run: +//! ```sh +//! cargo run --example ping-onion -- /ip4/123.45.67.89/tcp/24915 +//! ``` +//! +//! The two nodes establish a connection, negotiate the ping protocol +//! and begin pinging each other over Tor. + +use futures::StreamExt; +use libp2p::core::upgrade::Version; +use libp2p::Transport; +use libp2p::{ + core::muxing::StreamMuxerBox, + identity, noise, + swarm::{NetworkBehaviour, SwarmEvent}, + yamux, Multiaddr, PeerId, SwarmBuilder, +}; +use std::error::Error; +use std::sync::{Arc, Mutex}; +use tor_interface::tor_crypto::Ed25519PrivateKey; + +/// Create a transport +/// Returns a tuple of the transport and the onion address we can instruct it to listen on +async fn onion_transport( + keypair: identity::Keypair, +) -> Result< + ( + libp2p::core::transport::Boxed<(PeerId, libp2p::core::muxing::StreamMuxerBox)>, + Multiaddr, + ), + Box, +> { + let provider = libp2p_community_tor::tor_interface::legacy_tor_client::LegacyTorClient::new( + libp2p_community_tor::tor_interface::legacy_tor_client::LegacyTorClientConfig::system_from_environment().expect("Configure $TOR_... to talk to"))?; + + let mut transport = libp2p_community_tor::TorInterfaceTransport::from_provider( + libp2p_community_tor::AddressConversion::IpAndDns, Arc::new(Mutex::new(provider)), None)?; + + let onion_listen_address = transport.add_onion_service(&Ed25519PrivateKey::generate(), 999, None, None).unwrap(); + + let auth_upgrade = noise::Config::new(&keypair)?; + let multiplex_upgrade = yamux::Config::default(); + + let transport = transport + .boxed() + .upgrade(Version::V1) + .authenticate(auth_upgrade) + .multiplex(multiplex_upgrade) + .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) + .boxed(); + + Ok((transport, onion_listen_address)) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + + let local_key = identity::Keypair::generate_ed25519(); + let local_peer_id = PeerId::from(local_key.public()); + + println!("Local peer id: {local_peer_id}"); + + let (transport, onion_listen_address) = onion_transport(local_key).await?; + + let mut swarm = SwarmBuilder::with_new_identity() + .with_tokio() + .with_other_transport(|_| transport) + .unwrap() + .with_behaviour(|_| Behaviour { + ping: libp2p::ping::Behaviour::default(), + }) + .unwrap() + .build(); + + // Dial the peer identified by the multi-address given as the second + // command-line argument, if any. + if let Some(addr) = std::env::args().nth(1) { + let remote: Multiaddr = addr.parse()?; + swarm.dial(remote)?; + println!("Dialed {addr}") + } else { + // If we are not dialing, we need to listen + // Tell the swarm to listen on a specific onion address + swarm.listen_on(onion_listen_address).unwrap(); + } + + loop { + match swarm.select_next_some().await { + SwarmEvent::ConnectionEstablished { + endpoint, peer_id, .. + } => { + println!("Connection established with {peer_id} on {endpoint:?}"); + } + SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { + println!("Outgoing connection error with {peer_id:?}: {error:?}"); + } + SwarmEvent::NewListenAddr { address, .. } => println!("Listening on {address:?}"), + SwarmEvent::Behaviour(event) => println!("{event:?}"), + _ => {} + } + } +} + +/// Our network behaviour. +#[derive(NetworkBehaviour)] +struct Behaviour { + ping: libp2p::ping::Behaviour, +} diff --git a/libp2p-tor/src/address.rs b/libp2p-tor/src/arti/address.rs similarity index 100% rename from libp2p-tor/src/address.rs rename to libp2p-tor/src/arti/address.rs diff --git a/libp2p-tor/src/arti/mod.rs b/libp2p-tor/src/arti/mod.rs new file mode 100644 index 000000000..2d30a6cbd --- /dev/null +++ b/libp2p-tor/src/arti/mod.rs @@ -0,0 +1,413 @@ +use arti_client::{TorClient, TorClientBuilder}; +use futures::future::BoxFuture; +use libp2p::{ + core::transport::{ListenerId, TransportEvent}, + Multiaddr, Transport, TransportError, +}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use thiserror::Error; +use tor_rtcompat::tokio::TokioRustlsRuntime; + +// We only need these imports if the `listen-onion-service` feature is enabled +#[cfg(feature = "listen-onion-service")] +use std::collections::HashMap; +#[cfg(feature = "listen-onion-service")] +use std::str::FromStr; +#[cfg(feature = "listen-onion-service")] +use tor_cell::relaycell::msg::{Connected, End, EndReason}; +#[cfg(feature = "listen-onion-service")] +use tor_hsservice::{ + handle_rend_requests, status::OnionServiceStatus, HsId, OnionServiceConfig, + RunningOnionService, StreamRequest, +}; +#[cfg(feature = "listen-onion-service")] +use tor_proto::stream::IncomingStreamRequest; + +mod address; +mod provider; + +use address::{dangerous_extract, safe_extract}; +pub use provider::TokioTorStream; + +pub type TorError = arti_client::Error; + +type PendingUpgrade = BoxFuture<'static, Result>; +#[cfg(feature = "listen-onion-service")] +type OnionServiceStream = futures::stream::BoxStream<'static, StreamRequest>; +#[cfg(feature = "listen-onion-service")] +type OnionServiceStatusStream = futures::stream::BoxStream<'static, OnionServiceStatus>; + +/// Struct representing an onion address we are listening on for libp2p connections. +#[cfg(feature = "listen-onion-service")] +struct TorListener { + #[allow(dead_code)] // We need to own this to keep the RunningOnionService alive + /// The onion service we are listening on + service: Arc, + /// The stream of status updates for the onion service + status_stream: OnionServiceStatusStream, + /// The stream incoming [`StreamRequest`]s + request_stream: OnionServiceStream, + + /// The port we are listening on + port: u16, + /// The onion address we are listening on + onion_address: Multiaddr, + /// Whether we have already announced this address + announced: bool, +} + +/// Mode of address conversion. +/// Refer tor [arti_client::TorAddr](https://docs.rs/arti-client/latest/arti_client/struct.TorAddr.html) for details +#[derive(Debug, Clone, Copy, Hash, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum AddressConversion { + /// Uses only DNS for address resolution (default). + #[default] + DnsOnly, + /// Uses IP and DNS for addresses. + IpAndDns, +} + +pub struct TorTransport { + pub conversion_mode: AddressConversion, + + /// The Tor client. + client: Arc>, + + /// Onion services we are listening on. + #[cfg(feature = "listen-onion-service")] + listeners: HashMap, + + /// Onion services we are running but currently not listening on + #[cfg(feature = "listen-onion-service")] + services: Vec<(Arc, OnionServiceStream)>, +} + +impl TorTransport { + /// Creates a new `TorClientBuilder`. + /// + /// # Panics + /// Panics if the current runtime is not a `TokioRustlsRuntime`. + pub fn builder() -> TorClientBuilder { + let runtime = + TokioRustlsRuntime::current().expect("Couldn't get the current tokio rustls runtime"); + TorClient::with_runtime(runtime) + } + + /// Creates a bootstrapped `TorTransport` + /// + /// # Errors + /// Could return error emitted during Tor bootstrap by Arti. + pub async fn bootstrapped() -> Result { + let builder = Self::builder(); + let ret = Self::from_builder(&builder, AddressConversion::DnsOnly)?; + ret.bootstrap().await?; + Ok(ret) + } + + /// Builds a `TorTransport` from an Arti `TorClientBuilder` but does not bootstrap it. + /// + /// # Errors + /// Could return error emitted during creation of the `TorClient`. + pub fn from_builder( + builder: &TorClientBuilder, + conversion_mode: AddressConversion, + ) -> Result { + let client = Arc::new(builder.create_unbootstrapped()?); + + Ok(Self::from_client(client, conversion_mode)) + } + + /// Builds a `TorTransport` from an existing Arti `TorClient`. + pub fn from_client( + client: Arc>, + conversion_mode: AddressConversion, + ) -> Self { + Self { + conversion_mode, + client, + #[cfg(feature = "listen-onion-service")] + listeners: HashMap::new(), + #[cfg(feature = "listen-onion-service")] + services: Vec::new(), + } + } + + /// Bootstraps the `TorTransport` into the Tor network. + /// + /// # Errors + /// Could return error emitted during bootstrap by Arti. + pub async fn bootstrap(&self) -> Result<(), TorError> { + self.client.bootstrap().await + } + + /// Set the address conversion mode + #[must_use] + pub fn with_address_conversion(mut self, conversion_mode: AddressConversion) -> Self { + self.conversion_mode = conversion_mode; + self + } + + /// Call this function to instruct the transport to listen on a specific onion address + /// You need to call this function **before** calling `listen_on` + /// + /// # Returns + /// Returns the Multiaddr of the onion address that the transport can be instructed to listen on + /// To actually listen on the address, you need to call [`listen_on`] with the returned address + /// + /// # Errors + /// Returns an error if we cannot get the onion address of the service + #[cfg(feature = "listen-onion-service")] + pub fn add_onion_service( + &mut self, + svc_cfg: OnionServiceConfig, + port: u16, + ) -> anyhow::Result { + let (service, request_stream) = self.client.launch_onion_service(svc_cfg)?; + let request_stream = Box::pin(handle_rend_requests(request_stream)); + + let multiaddr = service + .onion_address() + .ok_or_else(|| anyhow::anyhow!("Onion service has no onion address"))? + .to_multiaddr(port); + + self.services.push((service, request_stream)); + + Ok(multiaddr) + } +} + +#[derive(Debug, Error)] +pub enum TorTransportError { + #[error(transparent)] + Client(#[from] TorError), + #[cfg(feature = "listen-onion-service")] + #[error(transparent)] + Service(#[from] tor_hsservice::ClientError), + #[cfg(feature = "listen-onion-service")] + #[error("Stream closed before receiving data")] + StreamClosed, + #[cfg(feature = "listen-onion-service")] + #[error("Stream port does not match listener port")] + StreamPortMismatch, + #[cfg(feature = "listen-onion-service")] + #[error("Onion service is broken")] + Broken, +} + +#[cfg(feature = "listen-onion-service")] +trait HsIdExt { + fn to_multiaddr(&self, port: u16) -> Multiaddr; +} + +#[cfg(feature = "listen-onion-service")] +impl HsIdExt for HsId { + /// Convert an `HsId` to a `Multiaddr` + fn to_multiaddr(&self, port: u16) -> Multiaddr { + let onion_domain = self.to_string(); + let onion_without_dot_onion = onion_domain + .split('.') + .nth(0) + .expect("Display formatting of HsId to contain .onion suffix"); + let multiaddress_string = format!("/onion3/{onion_without_dot_onion}:{port}"); + + Multiaddr::from_str(&multiaddress_string) + .expect("A valid onion address to be convertible to a Multiaddr") + } +} + +impl Transport for TorTransport { + type Output = TokioTorStream; + type Error = TorTransportError; + type Dial = BoxFuture<'static, Result>; + type ListenerUpgrade = PendingUpgrade; + + #[cfg(not(feature = "listen-onion-service"))] + fn listen_on( + &mut self, + _id: ListenerId, + onion_address: Multiaddr, + ) -> Result<(), TransportError> { + // If the `listen-onion-service` feature is not enabled, we do not support listening + Err(TransportError::MultiaddrNotSupported(onion_address.clone())) + } + + #[cfg(feature = "listen-onion-service")] + fn listen_on( + &mut self, + id: ListenerId, + onion_address: Multiaddr, + ) -> Result<(), TransportError> { + // If the address is not an onion3 address, return an error + let Some(libp2p::multiaddr::Protocol::Onion3(address)) = onion_address.into_iter().nth(0) + else { + return Err(TransportError::MultiaddrNotSupported(onion_address.clone())); + }; + + // Find the running onion service that matches the requested address + // If we find it, remove it from [`services`] and insert it into [`listeners`] + let position = self + .services + .iter() + .position(|(service, _)| { + service.onion_address().map_or(false, |name| { + name.to_multiaddr(address.port()) == onion_address + }) + }) + .ok_or_else(|| TransportError::MultiaddrNotSupported(onion_address.clone()))?; + + let (service, request_stream) = self.services.remove(position); + + let status_stream = Box::pin(service.status_events()); + + self.listeners.insert( + id, + TorListener { + service, + request_stream, + onion_address: onion_address.clone(), + port: address.port(), + status_stream, + announced: false, + }, + ); + + Ok(()) + } + + // We do not support removing listeners if the `listen-onion-service` feature is not enabled + #[cfg(not(feature = "listen-onion-service"))] + fn remove_listener(&mut self, _id: ListenerId) -> bool { + false + } + + #[cfg(feature = "listen-onion-service")] + fn remove_listener(&mut self, id: ListenerId) -> bool { + // Take the listener out of the map. This will stop listening on onion service for libp2p connections (we will not poll it anymore) + // However, we will not stop the onion service itself because we might want to reuse it later + // The onion service will be stopped when the transport is dropped + if let Some(listener) = self.listeners.remove(&id) { + self.services + .push((listener.service, listener.request_stream)); + return true; + } + + false + } + + fn dial(&mut self, addr: Multiaddr) -> Result> { + let maybe_tor_addr = match self.conversion_mode { + AddressConversion::DnsOnly => safe_extract(&addr), + AddressConversion::IpAndDns => dangerous_extract(&addr), + }; + + let tor_address = + maybe_tor_addr.ok_or(TransportError::MultiaddrNotSupported(addr.clone()))?; + let onion_client = self.client.clone(); + + Ok(Box::pin(async move { + let stream = onion_client.connect(tor_address).await?; + + tracing::debug!(%addr, "Established connection to peer through Tor"); + + Ok(TokioTorStream::from(stream)) + })) + } + + fn dial_as_listener( + &mut self, + addr: Multiaddr, + ) -> Result> { + self.dial(addr) + } + + fn address_translation(&self, _listen: &Multiaddr, _observed: &Multiaddr) -> Option { + None + } + + #[cfg(not(feature = "listen-onion-service"))] + fn poll( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + // If the `listen-onion-service` feature is not enabled, we do not support listening + Poll::Pending + } + + #[cfg(feature = "listen-onion-service")] + fn poll( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + for (listener_id, listener) in &mut self.listeners { + // Check if the service has any new statuses + if let Poll::Ready(Some(status)) = listener.status_stream.as_mut().poll_next(cx) { + tracing::debug!( + status = ?status.state(), + address = listener.onion_address.to_string(), + "Onion service status changed" + ); + } + + // Check if we have already announced this address, if not, do it now + if !listener.announced { + listener.announced = true; + + // We announce the address here to the swarm even though we technically cannot guarantee + // that the address is reachable yet from the outside. We might not have registered the + // onion service fully yet (introduction points, hsdir, ...) + // + // However, we need to announce it now because otherwise libp2p might not poll the listener + // again and we will not be able to announce it later. + // TODO: Find out why this is the case, if this is intended behaviour or a bug + return Poll::Ready(TransportEvent::NewAddress { + listener_id: *listener_id, + listen_addr: listener.onion_address.clone(), + }); + } + + match listener.request_stream.as_mut().poll_next(cx) { + Poll::Ready(Some(request)) => { + let port = listener.port; + let upgrade: PendingUpgrade = Box::pin(async move { + // Check if the port matches what we expect + if let IncomingStreamRequest::Begin(begin) = request.request() { + if begin.port() != port { + // Reject the connection with CONNECTREFUSED + request + .reject(End::new_with_reason(EndReason::CONNECTREFUSED)) + .await?; + + return Err(TorTransportError::StreamPortMismatch); + } + } + + // Accept the stream and forward it to the swarm + let data_stream = request.accept(Connected::new_empty()).await?; + Ok(TokioTorStream::from(data_stream)) + }); + + return Poll::Ready(TransportEvent::Incoming { + listener_id: *listener_id, + upgrade, + local_addr: listener.onion_address.clone(), + send_back_addr: listener.onion_address.clone(), + }); + } + + // The stream has ended + // This means that the onion service was shut down, and we will not receive any more connections on it + Poll::Ready(None) => { + return Poll::Ready(TransportEvent::ListenerClosed { + listener_id: *listener_id, + reason: Ok(()), + }); + } + Poll::Pending => {} + } + } + + Poll::Pending + } +} diff --git a/libp2p-tor/src/provider.rs b/libp2p-tor/src/arti/provider.rs similarity index 100% rename from libp2p-tor/src/provider.rs rename to libp2p-tor/src/arti/provider.rs diff --git a/libp2p-tor/src/lib.rs b/libp2p-tor/src/lib.rs index 1c426080f..f15e135ce 100644 --- a/libp2p-tor/src/lib.rs +++ b/libp2p-tor/src/lib.rs @@ -38,7 +38,7 @@ //! This crate uses tokio with rustls for its runtime and TLS implementation. //! No other combinations are supported. //! -//! ## Example +//! ## Examples //! ```no_run //! use libp2p::core::Transport; //! # async fn test_func() -> Result<(), Box> { @@ -50,431 +50,28 @@ //! # } //! # tokio_test::block_on(test_func()); //! ``` +//! +//! ```no_run +//! use libp2p::core::Transport; +//! use std::sync::{Arc, Mutex}; +//! use libp2p_tor::tor_interface::tor_provider::TorProvider; +//! # async fn test_func() -> Result<(), Box> { +//! let address = "/dns/www.torproject.org/tcp/1000".parse()?; +//! let mut provider = libp2p_tor::tor_interface::legacy_tor_client::LegacyTorClient::new( +//! libp2p_tor::tor_interface::legacy_tor_client::LegacyTorClientConfig::system_from_environment().unwrap())?; +//! provider.bootstrap()?; +//! let mut transport = libp2p_tor::TorInterfaceTransport::from_provider(Default::default(), Arc::new(Mutex::new(provider)), None)?; +//! // we have achieved tor connection +//! let _conn = transport.dial(address)?.await?; +//! # Ok(()) +//! # } +//! # tokio_test::block_on(test_func()); +//! ``` -use arti_client::{TorClient, TorClientBuilder}; -use futures::future::BoxFuture; -use libp2p::{ - core::transport::{ListenerId, TransportEvent}, - Multiaddr, Transport, TransportError, -}; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; -use thiserror::Error; -use tor_rtcompat::tokio::TokioRustlsRuntime; - -// We only need these imports if the `listen-onion-service` feature is enabled -#[cfg(feature = "listen-onion-service")] -use std::collections::HashMap; -#[cfg(feature = "listen-onion-service")] -use std::str::FromStr; -#[cfg(feature = "listen-onion-service")] -use tor_cell::relaycell::msg::{Connected, End, EndReason}; -#[cfg(feature = "listen-onion-service")] -use tor_hsservice::{ - handle_rend_requests, status::OnionServiceStatus, HsId, OnionServiceConfig, - RunningOnionService, StreamRequest, -}; -#[cfg(feature = "listen-onion-service")] -use tor_proto::stream::IncomingStreamRequest; - -mod address; -mod provider; - -use address::{dangerous_extract, safe_extract}; -pub use provider::TokioTorStream; - -pub type TorError = arti_client::Error; - -type PendingUpgrade = BoxFuture<'static, Result>; -#[cfg(feature = "listen-onion-service")] -type OnionServiceStream = futures::stream::BoxStream<'static, StreamRequest>; -#[cfg(feature = "listen-onion-service")] -type OnionServiceStatusStream = futures::stream::BoxStream<'static, OnionServiceStatus>; - -/// Struct representing an onion address we are listening on for libp2p connections. -#[cfg(feature = "listen-onion-service")] -struct TorListener { - #[allow(dead_code)] // We need to own this to keep the RunningOnionService alive - /// The onion service we are listening on - service: Arc, - /// The stream of status updates for the onion service - status_stream: OnionServiceStatusStream, - /// The stream incoming [`StreamRequest`]s - request_stream: OnionServiceStream, - - /// The port we are listening on - port: u16, - /// The onion address we are listening on - onion_address: Multiaddr, - /// Whether we have already announced this address - announced: bool, -} - -/// Mode of address conversion. -/// Refer tor [arti_client::TorAddr](https://docs.rs/arti-client/latest/arti_client/struct.TorAddr.html) for details -#[derive(Debug, Clone, Copy, Hash, Default, PartialEq, Eq, PartialOrd, Ord)] -pub enum AddressConversion { - /// Uses only DNS for address resolution (default). - #[default] - DnsOnly, - /// Uses IP and DNS for addresses. - IpAndDns, -} - -pub struct TorTransport { - pub conversion_mode: AddressConversion, - - /// The Tor client. - client: Arc>, - - /// Onion services we are listening on. - #[cfg(feature = "listen-onion-service")] - listeners: HashMap, - - /// Onion services we are running but currently not listening on - #[cfg(feature = "listen-onion-service")] - services: Vec<(Arc, OnionServiceStream)>, -} - -impl TorTransport { - /// Creates a new `TorClientBuilder`. - /// - /// # Panics - /// Panics if the current runtime is not a `TokioRustlsRuntime`. - pub fn builder() -> TorClientBuilder { - let runtime = - TokioRustlsRuntime::current().expect("Couldn't get the current tokio rustls runtime"); - TorClient::with_runtime(runtime) - } - - /// Creates a bootstrapped `TorTransport` - /// - /// # Errors - /// Could return error emitted during Tor bootstrap by Arti. - pub async fn bootstrapped() -> Result { - let builder = Self::builder(); - let ret = Self::from_builder(&builder, AddressConversion::DnsOnly)?; - ret.bootstrap().await?; - Ok(ret) - } - - /// Creates an unbootstrapped `TorTransport`. It will bootstrap in the background. - /// This can silently fail - pub async fn unbootstrapped() -> Result { - let builder = Self::builder(); - let ret = Self::from_builder(&builder, AddressConversion::DnsOnly)?; - let bootstrap_client = ret.client.clone(); - tokio::spawn(async move { - if let Err(e) = bootstrap_client.bootstrap().await { - tracing::error!("Tor bootstrap failed: {}", e); - } - }); - Ok(ret) - } - - /// Builds a `TorTransport` from an Arti `TorClientBuilder` but does not bootstrap it. - /// - /// # Errors - /// Could return error emitted during creation of the `TorClient`. - pub fn from_builder( - builder: &TorClientBuilder, - conversion_mode: AddressConversion, - ) -> Result { - let client = Arc::new(builder.create_unbootstrapped()?); - - Ok(Self::from_client(client, conversion_mode)) - } - - /// Builds a `TorTransport` from an existing Arti `TorClient`. - pub fn from_client( - client: Arc>, - conversion_mode: AddressConversion, - ) -> Self { - Self { - conversion_mode, - client, - #[cfg(feature = "listen-onion-service")] - listeners: HashMap::new(), - #[cfg(feature = "listen-onion-service")] - services: Vec::new(), - } - } - - /// Bootstraps the `TorTransport` into the Tor network. - /// - /// # Errors - /// Could return error emitted during bootstrap by Arti. - pub async fn bootstrap(&self) -> Result<(), TorError> { - self.client.bootstrap().await - } - - /// Set the address conversion mode - #[must_use] - pub fn with_address_conversion(mut self, conversion_mode: AddressConversion) -> Self { - self.conversion_mode = conversion_mode; - self - } - - /// Call this function to instruct the transport to listen on a specific onion address - /// You need to call this function **before** calling `listen_on` - /// - /// # Returns - /// Returns the Multiaddr of the onion address that the transport can be instructed to listen on - /// To actually listen on the address, you need to call [`listen_on`] with the returned address - /// - /// # Errors - /// Returns an error if we cannot get the onion address of the service - #[cfg(feature = "listen-onion-service")] - pub fn add_onion_service( - &mut self, - svc_cfg: OnionServiceConfig, - port: u16, - ) -> anyhow::Result { - let (service, request_stream) = self.client.launch_onion_service(svc_cfg)?; - let request_stream = Box::pin(handle_rend_requests(request_stream)); - - let multiaddr = service - .onion_address() - .ok_or_else(|| anyhow::anyhow!("Onion service has no onion address"))? - .to_multiaddr(port); - - self.services.push((service, request_stream)); - - Ok(multiaddr) - } -} - -#[derive(Debug, Error)] -pub enum TorTransportError { - #[error(transparent)] - Client(#[from] TorError), - #[cfg(feature = "listen-onion-service")] - #[error(transparent)] - Service(#[from] tor_hsservice::ClientError), - #[cfg(feature = "listen-onion-service")] - #[error("Stream closed before receiving data")] - StreamClosed, - #[cfg(feature = "listen-onion-service")] - #[error("Stream port does not match listener port")] - StreamPortMismatch, - #[cfg(feature = "listen-onion-service")] - #[error("Onion service is broken")] - Broken, -} - -#[cfg(feature = "listen-onion-service")] -trait HsIdExt { - fn to_multiaddr(&self, port: u16) -> Multiaddr; -} - -#[cfg(feature = "listen-onion-service")] -impl HsIdExt for HsId { - /// Convert an `HsId` to a `Multiaddr` - fn to_multiaddr(&self, port: u16) -> Multiaddr { - let onion_domain = self.to_string(); - let onion_without_dot_onion = onion_domain - .split('.') - .nth(0) - .expect("Display formatting of HsId to contain .onion suffix"); - let multiaddress_string = format!("/onion3/{onion_without_dot_onion}:{port}"); - - Multiaddr::from_str(&multiaddress_string) - .expect("A valid onion address to be convertible to a Multiaddr") - } -} - -impl Transport for TorTransport { - type Output = TokioTorStream; - type Error = TorTransportError; - type Dial = BoxFuture<'static, Result>; - type ListenerUpgrade = PendingUpgrade; - - #[cfg(not(feature = "listen-onion-service"))] - fn listen_on( - &mut self, - _id: ListenerId, - onion_address: Multiaddr, - ) -> Result<(), TransportError> { - // If the `listen-onion-service` feature is not enabled, we do not support listening - Err(TransportError::MultiaddrNotSupported(onion_address.clone())) - } - - #[cfg(feature = "listen-onion-service")] - fn listen_on( - &mut self, - id: ListenerId, - onion_address: Multiaddr, - ) -> Result<(), TransportError> { - // If the address is not an onion3 address, return an error - let Some(libp2p::multiaddr::Protocol::Onion3(address)) = onion_address.into_iter().nth(0) - else { - return Err(TransportError::MultiaddrNotSupported(onion_address.clone())); - }; - - // Find the running onion service that matches the requested address - // If we find it, remove it from [`services`] and insert it into [`listeners`] - let position = self - .services - .iter() - .position(|(service, _)| { - service.onion_address().map_or(false, |name| { - name.to_multiaddr(address.port()) == onion_address - }) - }) - .ok_or_else(|| TransportError::MultiaddrNotSupported(onion_address.clone()))?; - - let (service, request_stream) = self.services.remove(position); - - let status_stream = Box::pin(service.status_events()); - - self.listeners.insert( - id, - TorListener { - service, - request_stream, - onion_address: onion_address.clone(), - port: address.port(), - status_stream, - announced: false, - }, - ); - - Ok(()) - } - - // We do not support removing listeners if the `listen-onion-service` feature is not enabled - #[cfg(not(feature = "listen-onion-service"))] - fn remove_listener(&mut self, _id: ListenerId) -> bool { - false - } - - #[cfg(feature = "listen-onion-service")] - fn remove_listener(&mut self, id: ListenerId) -> bool { - // Take the listener out of the map. This will stop listening on onion service for libp2p connections (we will not poll it anymore) - // However, we will not stop the onion service itself because we might want to reuse it later - // The onion service will be stopped when the transport is dropped - if let Some(listener) = self.listeners.remove(&id) { - self.services - .push((listener.service, listener.request_stream)); - return true; - } - - false - } - - fn dial(&mut self, addr: Multiaddr) -> Result> { - let maybe_tor_addr = match self.conversion_mode { - AddressConversion::DnsOnly => safe_extract(&addr), - AddressConversion::IpAndDns => dangerous_extract(&addr), - }; - - let tor_address = - maybe_tor_addr.ok_or(TransportError::MultiaddrNotSupported(addr.clone()))?; - let onion_client = self.client.clone(); - - Ok(Box::pin(async move { - let stream = onion_client.connect(tor_address).await?; - - tracing::debug!(%addr, "Established connection to peer through Tor"); - - Ok(TokioTorStream::from(stream)) - })) - } - - fn dial_as_listener( - &mut self, - addr: Multiaddr, - ) -> Result> { - self.dial(addr) - } - - fn address_translation(&self, _listen: &Multiaddr, _observed: &Multiaddr) -> Option { - None - } - - #[cfg(not(feature = "listen-onion-service"))] - fn poll( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll> { - // If the `listen-onion-service` feature is not enabled, we do not support listening - Poll::Pending - } - - #[cfg(feature = "listen-onion-service")] - fn poll( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - for (listener_id, listener) in &mut self.listeners { - // Check if the service has any new statuses - if let Poll::Ready(Some(status)) = listener.status_stream.as_mut().poll_next(cx) { - tracing::debug!( - status = ?status.state(), - address = listener.onion_address.to_string(), - "Onion service status changed" - ); - } - - // Check if we have already announced this address, if not, do it now - if !listener.announced { - listener.announced = true; - - // We announce the address here to the swarm even though we technically cannot guarantee - // that the address is reachable yet from the outside. We might not have registered the - // onion service fully yet (introduction points, hsdir, ...) - // - // However, we need to announce it now because otherwise libp2p might not poll the listener - // again and we will not be able to announce it later. - // TODO: Find out why this is the case, if this is intended behaviour or a bug - return Poll::Ready(TransportEvent::NewAddress { - listener_id: *listener_id, - listen_addr: listener.onion_address.clone(), - }); - } - - match listener.request_stream.as_mut().poll_next(cx) { - Poll::Ready(Some(request)) => { - let port = listener.port; - let upgrade: PendingUpgrade = Box::pin(async move { - // Check if the port matches what we expect - if let IncomingStreamRequest::Begin(begin) = request.request() { - if begin.port() != port { - // Reject the connection with CONNECTREFUSED - request - .reject(End::new_with_reason(EndReason::CONNECTREFUSED)) - .await?; - - return Err(TorTransportError::StreamPortMismatch); - } - } - - // Accept the stream and forward it to the swarm - let data_stream = request.accept(Connected::new_empty()).await?; - Ok(TokioTorStream::from(data_stream)) - }); - - return Poll::Ready(TransportEvent::Incoming { - listener_id: *listener_id, - upgrade, - local_addr: listener.onion_address.clone(), - send_back_addr: listener.onion_address.clone(), - }); - } - - // The stream has ended - // This means that the onion service was shut down, and we will not receive any more connections on it - Poll::Ready(None) => { - return Poll::Ready(TransportEvent::ListenerClosed { - listener_id: *listener_id, - reason: Ok(()), - }); - } - Poll::Pending => {} - } - } +mod arti; +pub use arti::*; - Poll::Pending - } -} +#[cfg(feature="tor-interface")] +mod tor; +#[cfg(feature="tor-interface")] +pub use tor::*; diff --git a/libp2p-tor/src/tor/address.rs b/libp2p-tor/src/tor/address.rs new file mode 100644 index 000000000..441006b16 --- /dev/null +++ b/libp2p-tor/src/tor/address.rs @@ -0,0 +1,161 @@ +// Copyright 2022 Hannes Furmans +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +use libp2p::{core::multiaddr::Protocol, Multiaddr}; +use tor_interface::tor_provider::{OnionAddr, OnionAddrV3, TargetAddr, DomainAddr}; +use tor_interface::tor_crypto::{V3OnionServiceId, Ed25519PublicKey}; +use std::net::SocketAddr; + +/// "Dangerously" extract a Tor address from the provided [`Multiaddr`]. +/// +/// Refer tor [arti_client::TorAddr](https://docs.rs/arti-client/latest/arti_client/struct.TorAddr.html) for details around the safety / privacy considerations. +pub fn dangerous_extract(multiaddr: &Multiaddr) -> Option { + if let Some(tor_addr) = safe_extract(multiaddr) { + return Some(tor_addr); + } + + let mut protocols = multiaddr.into_iter(); + + try_to_socket_addr(&protocols.next()?, &protocols.next()?) +} + +/// "Safely" extract a Tor address from the provided [`Multiaddr`]. +/// +/// Refer tor [arti_client::TorAddr](https://docs.rs/arti-client/latest/arti_client/struct.TorAddr.html) for details around the safety / privacy considerations. +pub fn safe_extract(multiaddr: &Multiaddr) -> Option { + let mut protocols = multiaddr.into_iter(); + + let (dom, port) = (protocols.next()?, protocols.next()); + try_to_domain_and_port(&dom, &port) +} + +fn try_to_domain_and_port<'a>( + maybe_domain: &'a Protocol, + maybe_port: &Option, +) -> Option { + match (maybe_domain, maybe_port) { + ( + Protocol::Dns(domain) | Protocol::Dns4(domain) | Protocol::Dns6(domain), + Some(Protocol::Tcp(port)), + ) => Some(TargetAddr::Domain(DomainAddr::try_from((domain.to_string(), *port)).ok()?.into())), + (Protocol::Onion3(domain), _) => + Some(TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3::new(V3OnionServiceId::from_public_key(&Ed25519PublicKey::from_raw(domain.hash()[..32].try_into().unwrap()).ok()?), domain.port())))), + _ => None, + } +} + +fn try_to_socket_addr(maybe_ip: &Protocol, maybe_port: &Protocol) -> Option { + match (maybe_ip, maybe_port) { + (Protocol::Ip4(ip), Protocol::Tcp(port)) => Some(TargetAddr::Socket(SocketAddr::from((*ip, *port)))), + (Protocol::Ip6(ip), Protocol::Tcp(port)) => Some(TargetAddr::Socket(SocketAddr::from((*ip, *port)))), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tor_interface::tor_provider::TargetAddr; + use std::str::FromStr; + + #[test] + fn extract_correct_address_from_dns() { + let addresses = [ + "/dns/ip.tld/tcp/10".parse().unwrap(), + "/dns4/dns.ip4.tld/tcp/11".parse().unwrap(), + "/dns6/dns.ip6.tld/tcp/12".parse().unwrap(), + "/onion3/cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd:13".parse().unwrap(), + ]; + + let actual = addresses + .iter() + .filter_map(safe_extract) + .collect::>(); + + assert_eq!( + &[ + TargetAddr::from_str("ip.tld:10").unwrap(), + TargetAddr::from_str("dns.ip4.tld:11").unwrap(), + TargetAddr::from_str("dns.ip6.tld:12").unwrap(), + TargetAddr::from_str("cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion:13").unwrap(), + ], + actual.as_slice() + ); + } + + #[test] + fn extract_correct_address_from_ips() { + let addresses = [ + "/ip4/127.0.0.1/tcp/10".parse().unwrap(), + "/ip6/::1/tcp/10".parse().unwrap(), + ]; + + let actual = addresses + .iter() + .filter_map(dangerous_extract) + .collect::>(); + + assert_eq!( + &[ + TargetAddr::from_str("127.0.0.1:10").unwrap(), + TargetAddr::from_str("[::1]:10").unwrap(), + ], + actual.as_slice() + ); + } + + #[test] + fn dangerous_extract_works_on_domains_too() { + let addresses = [ + "/dns/ip.tld/tcp/10".parse().unwrap(), + "/ip4/127.0.0.1/tcp/10".parse().unwrap(), + "/ip6/::1/tcp/10".parse().unwrap(), + ]; + + let actual = addresses + .iter() + .filter_map(dangerous_extract) + .collect::>(); + + assert_eq!( + &[ + TargetAddr::from_str("ip.tld:10").unwrap(), + TargetAddr::from_str("127.0.0.1:10").unwrap(), + TargetAddr::from_str("[::1]:10").unwrap(), + ], + actual.as_slice() + ); + } + + #[test] + fn detect_incorrect_address() { + let addresses = [ + "/tcp/10/udp/12".parse().unwrap(), + "/dns/ip.tld/dns4/ip.tld/dns6/ip.tld".parse().unwrap(), + "/tcp/10/ip4/1.1.1.1".parse().unwrap(), + ]; + + let all_correct = addresses.iter().map(safe_extract).all(|res| res.is_none()); + + assert!( + all_correct, + "During the parsing of the faulty addresses, there was an incorrectness" + ); + } +} diff --git a/libp2p-tor/src/tor/mod.rs b/libp2p-tor/src/tor/mod.rs new file mode 100644 index 000000000..7b6921418 --- /dev/null +++ b/libp2p-tor/src/tor/mod.rs @@ -0,0 +1,349 @@ +// Copyright 2022 Hannes Furmans +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![warn(clippy::pedantic)] +#![deny(unsafe_code)] + +use futures::future::BoxFuture; +use tor_interface::tor_provider::{self, CircuitToken, TcpOnionListener, TcpOrUnixOnionStream, TorProvider, OnionListener, OnionAddr}; +use tor_interface::tor_crypto::{V3OnionServiceId, Ed25519PrivateKey, X25519PublicKey}; +use libp2p::{ + core::transport::{ListenerId, TransportEvent}, + Multiaddr, Transport, TransportError, +}; + +use std::collections::{BTreeSet, HashMap}; +use std::str::FromStr; +use tokio::net::TcpListener; + +use std::pin::Pin; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::net::SocketAddr; +use std::task::{Context, Poll}; +use thiserror::Error; + +mod address; +mod provider; + +use address::{dangerous_extract, safe_extract}; +pub use provider::OnionStreamStream; + +use crate::AddressConversion; + +pub use tor_interface; + +/// Get a [`TorProvider`](`tor_provider::TorProvider`) from [`tor_interface`] +pub struct TorInterfaceTransport { + pub conversion_mode: AddressConversion, + pub provider: Arc>, + pub circuit: Option, + + /// Onion services we are listening on. + listeners: HashMap, + + /// Onion services we are running (implicitly excluded if ListenerId present) + services: Vec<(TcpOnionListener, Option)>, + + /// Services yet to be announced + waiting_to_announce: HashMap, + + event_backlog: Vec, + + /// Persistent list of services we already publish + /// + /// Tor delineates services by onion but libp2p does it by onion:port + published_services: BTreeSet, +} + +#[derive(Debug, Error)] +pub enum TorInterfaceTransportError { + #[error(transparent)] + Client(#[from] tor_provider::Error), + #[error(transparent)] + Io(#[from] std::io::Error), +} + +fn lock(m: &Mutex) -> MutexGuard<'_, T> { + match m.lock() { + Ok(o) => o, + Err(e) => e.into_inner(), + } +} + +fn bootstrap(provider: &mut T) -> Result<(), tor_provider::Error> { + match provider.bootstrap() { + Err(tor_provider::Error::Generic(s)) if s.ends_with(" already bootstrapped") => Ok(()), + res @ Ok(_) | res @ Err(_) => res, + } +} + +impl> TorInterfaceTransport { + /// Creates a new `TorClientBuilder`. + pub fn from_provider( + conversion_mode: AddressConversion, + provider: Arc>, + circuit: Option + ) -> Result { + bootstrap(&mut *lock(&provider))?; + Ok(Self { + conversion_mode: conversion_mode, + provider: provider, + circuit: circuit, + listeners: HashMap::new(), + services: Vec::new(), + waiting_to_announce: Default::default(), + event_backlog: Default::default(), + published_services: Default::default(), + }) + } + + /// Call this function to instruct the transport to listen on a specific onion address + /// You need to call this function **before** calling `listen_on` + /// + /// # Returns + /// Returns the Multiaddr of the onion address that the transport can be instructed to listen on + /// To actually listen on the address, you need to call [`listen_on()`] with the returned address + /// + /// # Blocks + /// If listening fails with an `LegacyTorNotBootstrapped` error, + /// `bootstrap()`s the provider and awaits bootstrap confirtmation + /// + /// # Errors + /// Returns an error if we couldn't talk to the tor daemon + pub fn add_onion_service( + &mut self, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorised_clients: Option<&[X25519PublicKey]>, + socket_addr: Option, + ) -> anyhow::Result { + let ol = self.listener_or_bootstrap(|p| p.listener(private_key, virt_port, authorised_clients, socket_addr))?; + ol.set_nonblocking(true)?; + + self.services.push((ol, None)); + + let svid = V3OnionServiceId::from_private_key(&private_key); + let multiaddr = svid.to_multiaddr(virt_port); + + Ok(multiaddr) + } + + fn listener_or_bootstrap Result>(&mut self, mut f: F) -> Result { + loop { + let attempt = f(&mut lock(&self.provider)); // Moving this into the match clause deadlocks (Guard still borrowed) + match attempt { + Err(tor_provider::Error::Generic(s)) if s.ends_with(" not bootstrapped") => { + bootstrap(&mut *lock(&self.provider))?; + self.event_backlog.extend(lock(&self.provider).update()?); + } + res @ Ok(_) | res @ Err(_) => return res, + } + } + } +} + +trait HsIdExt { + fn to_multiaddr(&self, port: u16) -> Multiaddr; +} + +impl HsIdExt for V3OnionServiceId { + /// Convert an `V3OnionServiceId` to a `Multiaddr` + fn to_multiaddr(&self, port: u16) -> Multiaddr { + // The internal representation of V3OnionServiceId is 52 characters, so we can't re-use it here. + let multiaddress_string = format!("/onion3/{self}:{port}"); + + Multiaddr::from_str(&multiaddress_string) + .expect("A valid onion address to be convertible to a Multiaddr") + } +} + +trait OnionAddrExt { + fn to_multiaddr(&self) -> Multiaddr; +} + +impl OnionAddrExt for OnionAddr { + fn to_multiaddr(&self) -> Multiaddr { + let OnionAddr::V3(v3) = self; + v3.service_id().to_multiaddr(v3.virt_port()) + } +} + +#[cfg(test)] +#[test] +fn to_multiaddr() { + use tor_interface::tor_crypto::Ed25519PublicKey; + use libp2p::multiaddr::multiaddr; + let test = V3OnionServiceId::from_public_key(&Ed25519PublicKey::from_raw(&[0; 32]).unwrap()).to_multiaddr(12345); + assert_eq!( + test, + multiaddr!(Onion3(( + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0xCD, 0x0E, 0x03 + ], + 12345 + ))) + ); + assert_eq!( + test.to_string(), + "/onion3/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd:12345" + ); +} + + +impl + Send + Sync + 'static> Transport for TorInterfaceTransport { + type Error = TorInterfaceTransportError; + type Output = OnionStreamStream; + type ListenerUpgrade = std::future::Ready>; + type Dial = BoxFuture<'static, Result>; + + fn listen_on( + &mut self, + id: ListenerId, + onion_address: Multiaddr, + ) -> Result<(), TransportError> { + // If the address is not an onion3 address, return an error + if !matches!(onion_address.into_iter().nth(0), Some(libp2p::multiaddr::Protocol::Onion3(_))) { + return Err(TransportError::MultiaddrNotSupported(onion_address)); + } + + // Find the running onion service that matches the requested address + // If we find it, tag it in [`services`] and insert it into [`listeners`] + let service = self + .services + .iter_mut() + .find(|(service, listener_id)| listener_id.is_none() && service.address().to_multiaddr() == onion_address); + let Some((service, listener_id)) = service + else { + return Err(TransportError::MultiaddrNotSupported(onion_address)); + }; + + + let listener = service.try_clone_inner().and_then(TcpListener::from_std).map_err(TorInterfaceTransportError::Io).map_err(TransportError::Other)?; + *listener_id = Some(id); + + self.listeners.insert(id, listener); + self.waiting_to_announce.insert(id, service.address().clone()); + + Ok(()) + } + + fn remove_listener(&mut self, id: ListenerId) -> bool { + // Take the listener out of the map. This will stop listening on onion service for libp2p connections (we will not poll it anymore) + // However, we will not stop the onion service itself because we might want to reuse it later + // The onion service will be stopped when the transport is dropped + if let Some(_) = self.listeners.remove(&id) { + let Some((_, listener_id)) = self.services.iter_mut().find(|(_, listener_id)| *listener_id == Some(id)) + else { unreachable!() }; + *listener_id = None; + self.waiting_to_announce.remove(&id); + return true; + } + + false + } + + fn dial(&mut self, addr: Multiaddr) -> Result> { + let maybe_tor_addr = match self.conversion_mode { + AddressConversion::DnsOnly => safe_extract(&addr), + AddressConversion::IpAndDns => dangerous_extract(&addr), + }; + + let Some(tor_address) = maybe_tor_addr + else { return Err(TransportError::MultiaddrNotSupported(addr)); }; + let provider = self.provider.clone(); + let circuit = self.circuit; + + Ok(Box::pin(async move { + let stream = lock(&provider).connect(tor_address, circuit).map_err(Self::Error::Client)?; + + tracing::debug!(%addr, "Established connection to peer through Tor"); + + OnionStreamStream::from_onion_stream(stream).map_err(Self::Error::Io) + })) + } + + fn dial_as_listener( + &mut self, + addr: Multiaddr, + ) -> Result> { + self.dial(addr) + } + + fn address_translation(&self, _: &Multiaddr, _: &Multiaddr) -> Option { + None + } + + fn poll( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + while !&self.event_backlog.is_empty() { + match self.event_backlog.swap_remove(0) { + tor_provider::TorEvent::BootstrapStatus { .. } => {} + tor_provider::TorEvent::BootstrapComplete => tracing::debug!("Tor bootstrap complete"), + tor_provider::TorEvent::LogReceived { line } => tracing::debug!(%line), + tor_provider::TorEvent::OnionServicePublished { service_id } => { self.published_services.insert(service_id); }, + } + } + + // This is HashMap::extract_if() but that's unstable rn; not perf-sensitive (self.waiting_to_announce.len() is almost always 0) + if let Some(listener_id) = self.waiting_to_announce.iter().find(|(_, addr)| { + let OnionAddr::V3(addr) = addr; + self.published_services.contains(addr.service_id()) + }).map(|(listener_id, _)| listener_id).copied() { + return Poll::Ready(TransportEvent::NewAddress { + listener_id, + listen_addr: self.waiting_to_announce.remove(&listener_id).unwrap(/*key from find()*/).to_multiaddr(), + }); + } + + let new_events = lock(&self.provider).update().unwrap_or(vec![]); + self.event_backlog.extend(new_events); + if !self.event_backlog.is_empty() { + return self.poll(cx); + } + + for (&listener_id, listener) in &mut self.listeners { + match listener.poll_accept(cx) { + Poll::Ready(Ok((caller, _))) => { + let service_addr = self.services.iter().find(|(_, li)| *li == Some(listener_id)).map(|(ol, _)| ol.address()); + let multi = service_addr.map(|ra| ra.to_multiaddr()).unwrap_or(Multiaddr::empty()); + + return Poll::Ready(TransportEvent::Incoming { + listener_id, + upgrade: std::future::ready(Ok((caller, service_addr.cloned()).into())), + local_addr: multi.clone(), + send_back_addr: multi, + }); + } + + Poll::Ready(Err(err)) => { + return Poll::Ready(TransportEvent::ListenerError { listener_id, error: err.into() }); + } + + Poll::Pending => {}, + } + } + + Poll::Pending + } +} diff --git a/libp2p-tor/src/tor/provider.rs b/libp2p-tor/src/tor/provider.rs new file mode 100644 index 000000000..bdab38af0 --- /dev/null +++ b/libp2p-tor/src/tor/provider.rs @@ -0,0 +1,161 @@ +// Copyright 2022 Hannes Furmans +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! The UnixStream implementation is backported and open-coded from +//! It should be removed when libp2p is updated + +use futures::{AsyncRead, AsyncWrite}; +use tor_interface::tor_provider::{OnionAddr, OnionStream, TargetAddr, TcpOrUnixOnionStream, TcpOrUnixStream}; +use libp2p::tcp::tokio::TcpStream; +#[cfg(unix)] +// use libp2p::unix_stream::tokio::UnixStream; +use tokio::net::UnixStream; + +#[derive(Debug)] +enum TcpOrUnix { + Tcp(TcpStream), + #[cfg(unix)] + Unix(UnixStream), +} + +#[derive(Debug)] +pub struct OnionStreamStream { + pub local_addr: Option, + pub peer_addr: Option, + stream: TcpOrUnix, +} + +impl From<(tokio::net::TcpStream, Option)> for OnionStreamStream { + fn from((stream, local_addr): (tokio::net::TcpStream, Option)) -> Self { + let stream = TcpOrUnix::Tcp(TcpStream(stream)); + Self { local_addr, peer_addr: None, stream } + } +} + +impl OnionStreamStream { + pub fn from_onion_stream(inner: TcpOrUnixOnionStream) -> std::io::Result { + let local_addr = inner.local_addr(); + let peer_addr = inner.peer_addr(); + inner.set_nonblocking(true)?; + let stream = match inner.into() { + TcpOrUnixStream::Tcp(sock) => TcpOrUnix::Tcp(TcpStream(tokio::net::TcpStream::from_std(sock)?)), + #[cfg(unix)] + TcpOrUnixStream::Unix(sock) => TcpOrUnix::Unix(UnixStream::from_std(sock.into())?), + }; + Ok(Self { local_addr, peer_addr, stream }) + } +} + +impl AsyncRead for OnionStreamStream { + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> std::task::Poll> { + match &mut self.stream { + TcpOrUnix::Tcp(sock) => AsyncRead::poll_read(std::pin::Pin::new(sock), cx, buf), + #[cfg(unix)] + // TcpOrUnix::Unix(sock) => AsyncRead::poll_read(std::pin::Pin::new(&mut sock), cx, buf), + TcpOrUnix::Unix(sock) => { + let mut read_buf = tokio::io::ReadBuf::new(buf); + futures::ready!(tokio::io::AsyncRead::poll_read(std::pin::Pin::new(sock), cx, &mut read_buf))?; + std::task::Poll::Ready(Ok(read_buf.filled().len())) + } + } + } + + fn poll_read_vectored( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + bufs: &mut [std::io::IoSliceMut<'_>], + ) -> std::task::Poll> { + match &mut self.stream { + TcpOrUnix::Tcp(ref mut sock) => AsyncRead::poll_read_vectored(std::pin::Pin::new(sock), cx, bufs), + #[cfg(unix)] + // TcpOrUnix::Unix(ref mut sock) => AsyncRead::poll_read_vectored(std::pin::Pin::new(sock), cx, bufs), + TcpOrUnix::Unix(_) => { + // From default impl + for b in bufs { + if !b.is_empty() { + return self.poll_read(cx, b); + } + } + + self.poll_read(cx, &mut []) + } + } + } +} + +impl AsyncWrite for OnionStreamStream { + #[inline] + fn poll_write( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + match &mut self.stream { + TcpOrUnix::Tcp(ref mut sock) => AsyncWrite::poll_write(std::pin::Pin::new(sock), cx, buf), + #[cfg(unix)] + // TcpOrUnix::Unix(sock) => AsyncWrite::poll_write(std::pin::Pin::new(sock), cx, buf), + TcpOrUnix::Unix(ref mut sock) => tokio::io::AsyncWrite::poll_write(std::pin::Pin::new(sock), cx, buf) + } + } + + #[inline] + fn poll_flush( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match &mut self.stream { + TcpOrUnix::Tcp(ref mut sock) => AsyncWrite::poll_flush(std::pin::Pin::new(sock), cx), + #[cfg(unix)] + // TcpOrUnix::Unix(sock) => AsyncWrite::poll_flush(std::pin::Pin::new(sock), cx), + TcpOrUnix::Unix(ref mut sock) => tokio::io::AsyncWrite::poll_flush(std::pin::Pin::new(sock), cx) + } + } + + #[inline] + fn poll_close( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match &mut self.stream { + TcpOrUnix::Tcp(ref mut sock) => AsyncWrite::poll_close(std::pin::Pin::new(sock), cx), + #[cfg(unix)] + // TcpOrUnix::Unix(sock) => AsyncWrite::poll_close(std::pin::Pin::new(sock), cx), + TcpOrUnix::Unix(ref mut sock) => tokio::io::AsyncWrite::poll_shutdown(std::pin::Pin::new(sock), cx) + } + } + + #[inline] + fn poll_write_vectored( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> std::task::Poll> { + match &mut self.stream { + TcpOrUnix::Tcp(ref mut sock) => AsyncWrite::poll_write_vectored(std::pin::Pin::new(sock), cx, bufs), + #[cfg(unix)] + // TcpOrUnix::Unix(sock) => AsyncWrite::poll_write_vectored(std::pin::Pin::new(sock), cx, bufs), + TcpOrUnix::Unix(ref mut sock) => tokio::io::AsyncWrite::poll_write_vectored(std::pin::Pin::new(sock), cx, bufs) + } + } +} From 3c61fef65939ea11795b554c0e7500385ba116c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Sun, 15 Jun 2025 17:41:45 +0200 Subject: [PATCH 184/184] Use tor-interface implementation unified into libp2p-tor --- Cargo.lock | 881 +++++++++++++++++++++------- libp2p-rendezvous-server/Cargo.toml | 2 +- swap/Cargo.toml | 3 +- swap/src/asb/network.rs | 36 +- swap/src/common/tor.rs | 4 +- 5 files changed, 700 insertions(+), 226 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3c4f1869..cbf704912 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,7 +291,7 @@ dependencies = [ "derive_builder_fork_arti", "derive_more 2.0.1", "educe", - "fs-mistrust", + "fs-mistrust 0.10.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "futures", "hostname-validator", "humantime", @@ -304,32 +304,55 @@ dependencies = [ "serde", "thiserror 2.0.12", "time 0.3.41", - "tor-async-utils", - "tor-basic-utils", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-chanmgr", "tor-circmgr", - "tor-config", - "tor-config-path", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config-path 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-dirmgr", - "tor-error", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-guardmgr", "tor-hsclient", - "tor-hscrypto", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-hsservice", - "tor-keymgr", + "tor-keymgr 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-memquota", "tor-netdir", "tor-netdoc", - "tor-persist", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-proto", "tor-protover", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] +[[package]] +name = "arti-rpc-client-core" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d4ceb10e4abdbe7708ceed407460b1076756d2d35bd450ad2614fe86c3847c" +dependencies = [ + "caret 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "derive_more 2.0.1", + "educe", + "fs-mistrust 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding", + "rand 0.9.2", + "serde", + "serde_json", + "thiserror 2.0.12", + "tor-config-path 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-rpc-connect", + "tor-socksproto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "void", +] + [[package]] name = "ascii" version = "1.1.0" @@ -1534,6 +1557,12 @@ dependencies = [ "serde", ] +[[package]] +name = "caret" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061dc3258f029feaf9ff02b43c6af5ea67a7dfaed5d2aef36204c812e614ef9c" + [[package]] name = "caret" version = "0.5.3" @@ -2497,6 +2526,19 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.11", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -3543,6 +3585,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-mistrust" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198b8f9ab4cff63b5c91e9e64edd4e6b43cd7fe7a52519a03c6c32ea0acfa557" +dependencies = [ + "derive_builder_fork_arti", + "dirs", + "libc", + "pwd-grp", + "serde", + "thiserror 2.0.12", + "walkdir", +] + [[package]] name = "fs-mistrust" version = "0.10.0" @@ -5500,20 +5557,6 @@ dependencies = [ "void", ] -[[package]] -name = "libp2p-community-tor-interface" -version = "0.1.0" -source = "git+https://github.com/nabijaczleweli/libp2p-tor-interface#e7a37fc32a054de2d19fa334d5c02a10972876de" -dependencies = [ - "anyhow", - "futures", - "libp2p", - "thiserror 1.0.69", - "tokio", - "tor-interface", - "tracing", -] - [[package]] name = "libp2p-connection-limits" version = "0.3.1" @@ -5935,8 +5978,9 @@ dependencies = [ "tokio-test", "tor-cell", "tor-hsservice", + "tor-interface", "tor-proto", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "tracing-subscriber", ] @@ -6463,7 +6507,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-test", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tower 0.5.2", "tower-http 0.6.6", "tracing", @@ -7181,6 +7225,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "oneshot-fused-workaround" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2948fd2414b613f9a97f8401270bd5d7638265ab940475cdbcfa28a0273d58" +dependencies = [ + "futures", +] + [[package]] name = "oneshot-fused-workaround" version = "0.2.3" @@ -8679,6 +8732,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" +[[package]] +name = "retry-error" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce97442758392c7e2a7716e06c514de75f0fe4b5a4b76e14ba1e5edfb7ba3512" + [[package]] name = "retry-error" version = "0.6.5" @@ -9689,6 +9748,20 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serial_test" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92761393ee4dc3ff8f4af487bd58f4307c9329bbedea02cac0089ad9c411e153" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot 0.12.4", + "serial_test_derive 0.9.0", +] + [[package]] name = "serial_test" version = "3.2.0" @@ -9700,7 +9773,19 @@ dependencies = [ "once_cell", "parking_lot 0.12.4", "scc", - "serial_test_derive", + "serial_test_derive 3.2.0", +] + +[[package]] +name = "serial_test_derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6f5d1c3087fb119617cff2966fe3808a80e5eb59a8c1601d5994d66f4346a5" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -10570,7 +10655,6 @@ dependencies = [ "hex", "jsonrpsee", "libp2p", - "libp2p-community-tor-interface", "libp2p-tor", "mockito", "moka", @@ -10595,7 +10679,7 @@ dependencies = [ "serde_cbor", "serde_json", "serde_with 1.14.0", - "serial_test", + "serial_test 3.2.0", "sha2 0.10.9", "sigma_fun", "sqlx", @@ -10616,7 +10700,7 @@ dependencies = [ "tokio-tar", "tokio-tungstenite", "tokio-util", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tower 0.4.13", "tower-http 0.3.5", "tracing", @@ -11634,6 +11718,20 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "tokio-mpmc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffadf729b08a5df966b11daa6faee399f72a4ddb00125c0e8853aa4e0f08006c" +dependencies = [ + "crossbeam-queue", + "futures", + "rand 0.9.2", + "thiserror 1.0.69", + "tokio", + "tracing", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -11839,6 +11937,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +[[package]] +name = "tor-async-utils" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bee2c4a8ea4cfe533bf284eef89d26f53ed0145854c54471734cb36e451f3e" +dependencies = [ + "derive-deftly 1.1.0", + "educe", + "futures", + "oneshot-fused-workaround 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project", + "postage", + "thiserror 2.0.12", + "void", +] + [[package]] name = "tor-async-utils" version = "0.32.0" @@ -11847,13 +11961,31 @@ dependencies = [ "derive-deftly 1.1.0", "educe", "futures", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "pin-project", "postage", "thiserror 2.0.12", "void", ] +[[package]] +name = "tor-basic-utils" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a4e9e4da7ce0f089aee75808db143c4d547ca6d4ffd5b15a1cb042c3082928" +dependencies = [ + "derive_more 2.0.1", + "hex", + "itertools 0.14.0", + "libc", + "paste", + "rand 0.9.2", + "rand_chacha 0.9.0", + "slab", + "smallvec", + "thiserror 2.0.12", +] + [[package]] name = "tor-basic-utils" version = "0.32.0" @@ -11872,6 +12004,24 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "tor-bytes" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a4a8401219d99b460c9bc001386366a4c50dc881a0abf346f04c1785f7e06a" +dependencies = [ + "bytes", + "derive-deftly 1.1.0", + "digest 0.10.7", + "educe", + "getrandom 0.3.3", + "safelog 0.4.8", + "thiserror 2.0.12", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize", +] + [[package]] name = "tor-bytes" version = "0.32.0" @@ -11884,8 +12034,8 @@ dependencies = [ "getrandom 0.3.3", "safelog 0.4.7", "thiserror 2.0.12", - "tor-error", - "tor-llcrypto 0.32.0", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "zeroize", ] @@ -11897,7 +12047,7 @@ dependencies = [ "amplify", "bitflags 2.9.1", "bytes", - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "derive-deftly 1.1.0", "derive_more 2.0.1", "educe", @@ -11905,32 +12055,48 @@ dependencies = [ "rand 0.9.2", "smallvec", "thiserror 2.0.12", - "tor-basic-utils", - "tor-bytes", - "tor-cert", - "tor-error", - "tor-hscrypto", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-cert 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-memquota", "tor-protover", - "tor-units", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "void", ] +[[package]] +name = "tor-cert" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9abc35321d207c3ffbfdc37feb0a9e186555ee49dfaa8079027115ce44491ff2" +dependencies = [ + "caret 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "derive_builder_fork_arti", + "derive_more 2.0.1", + "digest 0.10.7", + "thiserror 2.0.12", + "tor-bytes 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-checkable 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tor-cert" version = "0.32.0" source = "git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841#18111286b5830cda88af5df1950b5e24ee5a8841" dependencies = [ - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "derive_builder_fork_arti", "derive_more 2.0.1", "digest 0.10.7", "thiserror 2.0.12", - "tor-bytes", - "tor-checkable", - "tor-llcrypto 0.32.0", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-checkable 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", ] [[package]] @@ -11939,34 +12105,46 @@ version = "0.32.0" source = "git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841#18111286b5830cda88af5df1950b5e24ee5a8841" dependencies = [ "async-trait", - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "derive_builder_fork_arti", "derive_more 2.0.1", "educe", "futures", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "postage", "rand 0.9.2", "safelog 0.4.7", "serde", "thiserror 2.0.12", - "tor-async-utils", - "tor-basic-utils", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-cell", - "tor-config", - "tor-error", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-memquota", "tor-netdir", "tor-proto", - "tor-rtcompat", - "tor-socksproto", - "tor-units", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-socksproto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] +[[package]] +name = "tor-checkable" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e994401be86ecdaa8b17b176cd1562ddddc6778a47afd751e6db99ccadb8e6" +dependencies = [ + "humantime", + "signature 2.2.0", + "thiserror 2.0.12", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tor-checkable" version = "0.32.0" @@ -11975,7 +12153,7 @@ dependencies = [ "humantime", "signature 2.2.0", "thiserror 2.0.12", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", ] [[package]] @@ -11996,35 +12174,68 @@ dependencies = [ "humantime-serde", "itertools 0.14.0", "once_cell", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "pin-project", "rand 0.9.2", - "retry-error", + "retry-error 0.6.5 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "safelog 0.4.7", "serde", "static_assertions", "thiserror 2.0.12", - "tor-async-utils", - "tor-basic-utils", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-chanmgr", - "tor-config", - "tor-error", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-guardmgr", "tor-linkspec", "tor-memquota", "tor-netdir", "tor-netdoc", - "tor-persist", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-proto", "tor-protover", "tor-relay-selection", - "tor-rtcompat", - "tor-units", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", "weak-table", ] +[[package]] +name = "tor-config" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3280e6e26a30f94d752d55d273c307d2b819971ff4b830101d816990f9a5ec36" +dependencies = [ + "amplify", + "cfg-if", + "derive-deftly 1.1.0", + "derive_builder_fork_arti", + "educe", + "either", + "figment", + "fs-mistrust 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "futures", + "itertools 0.14.0", + "notify", + "paste", + "postage", + "regex", + "serde", + "serde-value", + "serde_ignored", + "strum 0.27.2", + "thiserror 2.0.12", + "toml 0.8.23", + "tor-basic-utils 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-rtcompat 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing", + "void", +] + [[package]] name = "tor-config" version = "0.32.0" @@ -12037,7 +12248,7 @@ dependencies = [ "educe", "either", "figment", - "fs-mistrust", + "fs-mistrust 0.10.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "futures", "itertools 0.14.0", "notify", @@ -12050,13 +12261,27 @@ dependencies = [ "strum 0.27.2", "thiserror 2.0.12", "toml 0.8.23", - "tor-basic-utils", - "tor-error", - "tor-rtcompat", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] +[[package]] +name = "tor-config-path" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5347bcbe96c660694fe52fb76e852d982d73fe0d92f4c4cb9eaa8427a5d52f17" +dependencies = [ + "directories", + "serde", + "shellexpand", + "thiserror 2.0.12", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-general-addr 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tor-config-path" version = "0.32.0" @@ -12066,8 +12291,8 @@ dependencies = [ "serde", "shellexpand", "thiserror 2.0.12", - "tor-error", - "tor-general-addr", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-general-addr 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", ] [[package]] @@ -12078,7 +12303,7 @@ dependencies = [ "digest 0.10.7", "hex", "thiserror 2.0.12", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", ] [[package]] @@ -12098,13 +12323,13 @@ dependencies = [ "memchr", "thiserror 2.0.12", "tor-circmgr", - "tor-error", - "tor-hscrypto", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-netdoc", "tor-proto", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", ] @@ -12120,7 +12345,7 @@ dependencies = [ "digest 0.10.7", "educe", "event-listener", - "fs-mistrust", + "fs-mistrust 0.10.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "fslock", "futures", "hex", @@ -12128,7 +12353,7 @@ dependencies = [ "humantime-serde", "itertools 0.14.0", "memmap2", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "paste", "postage", "rand 0.9.2", @@ -12142,23 +12367,40 @@ dependencies = [ "strum 0.27.2", "thiserror 2.0.12", "time 0.3.41", - "tor-async-utils", - "tor-basic-utils", - "tor-checkable", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-checkable 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-circmgr", - "tor-config", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-consdiff", "tor-dirclient", - "tor-error", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-guardmgr", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-netdir", "tor-netdoc", - "tor-persist", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-proto", "tor-protover", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tracing", +] + +[[package]] +name = "tor-error" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ae19c74564749c54e14e532ffb15f84807f734d17f452bb3ffb8b1957f06a2" +dependencies = [ + "derive_more 2.0.1", + "futures", + "paste", + "retry-error 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "static_assertions", + "strum 0.27.2", + "thiserror 2.0.12", "tracing", + "void", ] [[package]] @@ -12169,7 +12411,7 @@ dependencies = [ "derive_more 2.0.1", "futures", "paste", - "retry-error", + "retry-error 0.6.5 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "static_assertions", "strum 0.27.2", "thiserror 2.0.12", @@ -12177,6 +12419,17 @@ dependencies = [ "void", ] +[[package]] +name = "tor-general-addr" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad9c6e9147f4ee644c80c3b044813cf93a3f802279c49b06aac2f4f33555877" +dependencies = [ + "derive_more 2.0.1", + "thiserror 2.0.12", + "void", +] + [[package]] name = "tor-general-addr" version = "0.32.0" @@ -12204,7 +12457,7 @@ dependencies = [ "humantime-serde", "itertools 0.14.0", "num_enum", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "pin-project", "postage", "rand 0.9.2", @@ -12212,19 +12465,19 @@ dependencies = [ "serde", "strum 0.27.2", "thiserror 2.0.12", - "tor-async-utils", - "tor-basic-utils", - "tor-config", - "tor-error", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-netdir", "tor-netdoc", - "tor-persist", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-proto", "tor-relay-selection", - "tor-rtcompat", - "tor-units", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", ] @@ -12240,37 +12493,65 @@ dependencies = [ "either", "futures", "itertools 0.14.0", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "postage", "rand 0.9.2", - "retry-error", + "retry-error 0.6.5 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "safelog 0.4.7", "slotmap-careful", "strum 0.27.2", "thiserror 2.0.12", - "tor-async-utils", - "tor-basic-utils", - "tor-bytes", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-cell", - "tor-checkable", + "tor-checkable 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-circmgr", - "tor-config", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-dirclient", - "tor-error", - "tor-hscrypto", - "tor-keymgr", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-keymgr 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-memquota", "tor-netdir", "tor-netdoc", - "tor-persist", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-proto", "tor-protover", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", ] +[[package]] +name = "tor-hscrypto" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7469efe5d22466fcaaeec9506bf03426ce59c03ee1b8a7c8b316830b153b40a" +dependencies = [ + "data-encoding", + "derive_more 2.0.1", + "digest 0.10.7", + "hex", + "humantime", + "itertools 0.14.0", + "paste", + "rand 0.9.2", + "safelog 0.4.8", + "serde", + "signature 2.2.0", + "subtle", + "thiserror 2.0.12", + "tor-basic-utils 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-bytes 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-key-forge 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-units 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "void", +] + [[package]] name = "tor-hscrypto" version = "0.32.0" @@ -12291,13 +12572,13 @@ dependencies = [ "signature 2.2.0", "subtle", "thiserror 2.0.12", - "tor-basic-utils", - "tor-bytes", - "tor-error", - "tor-key-forge", - "tor-llcrypto 0.32.0", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-key-forge 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-memquota", - "tor-units", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "void", "zeroize", ] @@ -12316,7 +12597,7 @@ dependencies = [ "derive_more 2.0.1", "digest 0.10.7", "educe", - "fs-mistrust", + "fs-mistrust 0.10.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "futures", "growable-bloom-filter", "hex", @@ -12324,37 +12605,37 @@ dependencies = [ "itertools 0.14.0", "k12", "once_cell", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "postage", "rand 0.9.2", "rand_core 0.9.3", - "retry-error", + "retry-error 0.6.5 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "safelog 0.4.7", "serde", "serde_with 3.14.0", "strum 0.27.2", "thiserror 2.0.12", - "tor-async-utils", - "tor-basic-utils", - "tor-bytes", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-cell", "tor-circmgr", - "tor-config", - "tor-config-path", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config-path 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-dirclient", - "tor-error", - "tor-hscrypto", - "tor-keymgr", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-keymgr 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-log-ratelim", "tor-netdir", "tor-netdoc", - "tor-persist", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-proto", "tor-protover", "tor-relay-selection", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] @@ -12362,8 +12643,10 @@ dependencies = [ [[package]] name = "tor-interface" version = "0.5.0" -source = "git+https://github.com/nabijaczleweli/gosling?rev=65da8990e33c674ed8abeb10b454b4a39463d81d#65da8990e33c674ed8abeb10b454b4a39463d81d" dependencies = [ + "anyhow", + "arti-client", + "arti-rpc-client-core", "curve25519-dalek 4.1.3", "data-encoding", "data-encoding-macro", @@ -12373,6 +12656,7 @@ dependencies = [ "rand 0.9.2", "rand_core 0.9.3", "regex", + "serial_test 0.9.0", "sha1", "sha2 0.10.9", "sha3", @@ -12380,7 +12664,39 @@ dependencies = [ "socks", "static_assertions", "thiserror 1.0.69", - "tor-llcrypto 0.31.0", + "tokio", + "tokio-mpmc", + "tokio-stream", + "tor-cell", + "tor-config 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-hsservice", + "tor-keymgr 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-proto", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "which", + "zeroize", +] + +[[package]] +name = "tor-key-forge" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6697b58f1518757b975993d345a261387eb7a86c730cb542ad1ea68284155eaa" +dependencies = [ + "derive-deftly 1.1.0", + "derive_more 2.0.1", + "downcast-rs 2.0.1", + "paste", + "rand 0.9.2", + "signature 2.2.0", + "ssh-key", + "thiserror 2.0.12", + "tor-bytes 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-cert 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-checkable 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -12396,11 +12712,49 @@ dependencies = [ "signature 2.2.0", "ssh-key", "thiserror 2.0.12", - "tor-bytes", - "tor-cert", - "tor-checkable", - "tor-error", - "tor-llcrypto 0.32.0", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-cert 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-checkable 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", +] + +[[package]] +name = "tor-keymgr" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d765c0fd6b1910b427feb1b604fbc85e5768332e9be1e670bf64e698c92606" +dependencies = [ + "amplify", + "arrayvec", + "cfg-if", + "derive-deftly 1.1.0", + "derive_builder_fork_arti", + "derive_more 2.0.1", + "downcast-rs 2.0.1", + "dyn-clone", + "fs-mistrust 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glob-match", + "humantime", + "inventory", + "itertools 0.14.0", + "rand 0.9.2", + "serde", + "signature 2.2.0", + "ssh-key", + "thiserror 2.0.12", + "tor-basic-utils 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-bytes 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-config 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-config-path 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-hscrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-key-forge 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-persist 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing", + "walkdir", + "zeroize", ] [[package]] @@ -12416,7 +12770,7 @@ dependencies = [ "derive_more 2.0.1", "downcast-rs 2.0.1", "dyn-clone", - "fs-mistrust", + "fs-mistrust 0.10.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "glob-match", "humantime", "inventory", @@ -12426,15 +12780,15 @@ dependencies = [ "signature 2.2.0", "ssh-key", "thiserror 2.0.12", - "tor-basic-utils", - "tor-bytes", - "tor-config", - "tor-config-path", - "tor-error", - "tor-hscrypto", - "tor-key-forge", - "tor-llcrypto 0.32.0", - "tor-persist", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config-path 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-key-forge 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "visibility", "walkdir", @@ -12448,7 +12802,7 @@ source = "git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df195 dependencies = [ "base64ct", "by_address", - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "derive-deftly 1.1.0", "derive_builder_fork_arti", "derive_more 2.0.1", @@ -12459,19 +12813,19 @@ dependencies = [ "serde_with 3.14.0", "strum 0.27.2", "thiserror 2.0.12", - "tor-basic-utils", - "tor-bytes", - "tor-config", - "tor-llcrypto 0.32.0", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-memquota", "tor-protover", ] [[package]] name = "tor-llcrypto" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4def0c5674fa67c5b8a8fb8814c0381570e4866842c233ac98982e78779339bf" +checksum = "992c49dd4c285c52594858c0e92afe96531203dbe9bb29cfbe6937d94bb3c7ad" dependencies = [ "aes", "base64ct", @@ -12484,7 +12838,6 @@ dependencies = [ "educe", "getrandom 0.3.3", "hex", - "once_cell", "rand 0.9.2", "rand_chacha 0.9.0", "rand_core 0.6.4", @@ -12551,8 +12904,8 @@ dependencies = [ "futures", "humantime", "thiserror 2.0.12", - "tor-error", - "tor-rtcompat", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "weak-table", ] @@ -12574,12 +12927,12 @@ dependencies = [ "slotmap-careful", "static_assertions", "thiserror 2.0.12", - "tor-async-utils", - "tor-basic-utils", - "tor-config", - "tor-error", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-log-ratelim", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] @@ -12604,14 +12957,14 @@ dependencies = [ "strum 0.27.2", "thiserror 2.0.12", "time 0.3.41", - "tor-basic-utils", - "tor-error", - "tor-hscrypto", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-netdoc", "tor-protover", - "tor-units", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "typed-index-collections", ] @@ -12643,22 +12996,49 @@ dependencies = [ "thiserror 2.0.12", "time 0.3.41", "tinystr", - "tor-basic-utils", - "tor-bytes", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-cell", - "tor-cert", - "tor-checkable", - "tor-error", - "tor-hscrypto", + "tor-cert 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-checkable 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-protover", - "tor-units", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "void", "weak-table", "zeroize", ] +[[package]] +name = "tor-persist" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fabc9ba76dbe0ca3b254ed73480455a337c1941904f375d583efcdc57966f98" +dependencies = [ + "derive-deftly 1.1.0", + "derive_more 2.0.1", + "filetime", + "fs-mistrust 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "fslock", + "futures", + "itertools 0.14.0", + "oneshot-fused-workaround 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "paste", + "sanitize-filename", + "serde", + "serde_json", + "thiserror 2.0.12", + "time 0.3.41", + "tor-async-utils 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-basic-utils 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing", + "void", +] + [[package]] name = "tor-persist" version = "0.32.0" @@ -12668,21 +13048,21 @@ dependencies = [ "derive-deftly 1.1.0", "derive_more 2.0.1", "filetime", - "fs-mistrust", + "fs-mistrust 0.10.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "fslock", "fslock-guard", "futures", "itertools 0.14.0", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "paste", "sanitize-filename", "serde", "serde_json", "thiserror 2.0.12", "time 0.3.41", - "tor-async-utils", - "tor-basic-utils", - "tor-error", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] @@ -12696,7 +13076,7 @@ dependencies = [ "asynchronous-codec 0.7.0", "bitvec", "bytes", - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "cfg-if", "cipher", "coarsetime", @@ -12711,7 +13091,7 @@ dependencies = [ "hkdf", "hmac", "itertools 0.14.0", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "pin-project", "postage", "rand 0.9.2", @@ -12725,23 +13105,23 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tokio-util", - "tor-async-utils", - "tor-basic-utils", - "tor-bytes", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-cell", - "tor-cert", - "tor-checkable", - "tor-config", - "tor-error", - "tor-hscrypto", + "tor-cert 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-checkable 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto 0.32.0", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-log-ratelim", "tor-memquota", "tor-protover", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-rtmock", - "tor-units", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "typenum", "visibility", @@ -12754,11 +13134,11 @@ name = "tor-protover" version = "0.32.0" source = "git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841#18111286b5830cda88af5df1950b5e24ee5a8841" dependencies = [ - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "paste", "serde_with 3.14.0", "thiserror 2.0.12", - "tor-bytes", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", ] [[package]] @@ -12768,12 +13148,63 @@ source = "git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df195 dependencies = [ "rand 0.9.2", "serde", - "tor-basic-utils", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", "tor-netdir", "tor-netdoc", ] +[[package]] +name = "tor-rpc-connect" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985904e33200b9d2ef6e98863430855ecc45cd96c9811d839151c9651fdd306d" +dependencies = [ + "base16ct", + "cfg-if", + "derive_builder_fork_arti", + "derive_more 2.0.1", + "fs-mistrust 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.9.2", + "safelog 0.4.8", + "serde", + "serde_with 3.14.0", + "subtle", + "thiserror 2.0.12", + "tiny-keccak", + "toml 0.8.23", + "tor-basic-utils 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-config-path 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-general-addr 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing", + "zeroize", +] + +[[package]] +name = "tor-rtcompat" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75969f63c5147af49753e1c03339d342bc3e53e7412518e52702cc417cff729" +dependencies = [ + "async-trait", + "async_executors", + "asynchronous-codec 0.7.0", + "coarsetime", + "derive_more 2.0.1", + "dyn-clone", + "educe", + "futures", + "hex", + "libc", + "paste", + "pin-project", + "thiserror 2.0.12", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-general-addr 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing", + "void", +] + [[package]] name = "tor-rtcompat" version = "0.32.0" @@ -12797,8 +13228,8 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tokio-util", - "tor-error", - "tor-general-addr", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-general-addr 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] @@ -12817,34 +13248,62 @@ dependencies = [ "futures", "humantime", "itertools 0.14.0", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "pin-project", "priority-queue", "slotmap-careful", "strum 0.27.2", "thiserror 2.0.12", - "tor-error", - "tor-general-addr", - "tor-rtcompat", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-general-addr 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "tracing-test", "void", ] +[[package]] +name = "tor-socksproto" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b7d2a9b7394b8f2c683d282f4a0da08c056ed23b3bb9afdb84cb92d554c77b" +dependencies = [ + "amplify", + "caret 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "derive-deftly 1.1.0", + "educe", + "safelog 0.4.8", + "subtle", + "thiserror 2.0.12", + "tor-bytes 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tor-socksproto" version = "0.32.0" source = "git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841#18111286b5830cda88af5df1950b5e24ee5a8841" dependencies = [ "amplify", - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "derive-deftly 1.1.0", "educe", "safelog 0.4.7", "subtle", "thiserror 2.0.12", - "tor-bytes", - "tor-error", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", +] + +[[package]] +name = "tor-units" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f2bd3dc4f5defec5d4b9d152d911a3a852d08409558dd927ec8eb28e20f9de" +dependencies = [ + "derive_more 2.0.1", + "serde", + "thiserror 2.0.12", ] [[package]] @@ -13956,6 +14415,18 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "whoami" version = "1.6.0" diff --git a/libp2p-rendezvous-server/Cargo.toml b/libp2p-rendezvous-server/Cargo.toml index aae092818..5ddcee08f 100644 --- a/libp2p-rendezvous-server/Cargo.toml +++ b/libp2p-rendezvous-server/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" anyhow = "1" futures = { workspace = true } libp2p = { workspace = true, default-features = false, features = ["rendezvous", "tcp", "yamux", "dns", "noise", "ping", "websocket", "tokio", "macros"] } -libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service"] } +libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service", "legacy-tor-provider"] } tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros", "sync", "process", "fs", "net", "io-util"] } tor-hsservice = { workspace = true } tracing = { workspace = true, features = ["attributes"] } diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 78e8f4f57..8ac15a05b 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -46,8 +46,7 @@ futures = { workspace = true } hex = { workspace = true } jsonrpsee = { workspace = true, features = ["server"] } libp2p = { workspace = true, features = ["tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa"] } -libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service"] } -libp2p-community-tor-interface = { git = "https://github.com/nabijaczleweli/libp2p-tor-interface", features = ["legacy-tor-provider"] } +libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service", "legacy-tor-provider"] } moka = { version = "0.12", features = ["sync", "future"] } monero = { workspace = true } monero-rpc = { path = "../monero-rpc" } diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index 5f5308425..ee6580664 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -29,6 +29,7 @@ pub mod transport { use arti_client::{config::onion_service::OnionServiceConfigBuilder, TorClient}; use libp2p::{core::transport::{OptionalTransport, OrTransport}, dns, identity, tcp, Transport}; + use libp2p_tor::{AddressConversion, tor_interface}; use tor_rtcompat::tokio::TokioRustlsRuntime; use crate::common::tor::existing_tor_config; @@ -64,31 +65,34 @@ pub mod transport { num_intro_points: u8, ) -> Result { let (yesmaybe, maybe_tor_transport, onion_addresses) = if let Some((reuse_config, bindaddr)) = existing_tor_config() { - let client = libp2p_community_tor_interface::tor_interface::legacy_tor_client::LegacyTorClient::new(reuse_config)?; - let mut tor_transport = libp2p_community_tor_interface::TorInterfaceTransport::from_provider( - libp2p_community_tor_interface::AddressConversion::DnsOnly, Arc::new(Mutex::new(client)), None)?; + let client = tor_interface::legacy_tor_client::LegacyTorClient::new(reuse_config)?; + let mut tor_transport = libp2p_tor::TorInterfaceTransport::from_provider( + AddressConversion::DnsOnly, Arc::new(Mutex::new(client)), None)?; let pk_path = config_data_dir.join(ASB_ONION_SERVICE_NICKNAME).with_extension("pk"); - let loaded_pk = fs::read_to_string(&pk_path).ok() - .and_then(|pk| libp2p_community_tor_interface::tor_interface::tor_crypto::Ed25519PrivateKey::from_key_blob(pk.lines().next()?).ok()); + let pk = match fs::read_to_string(&pk_path).ok() + .and_then(|pk| tor_interface::tor_crypto::Ed25519PrivateKey::from_key_blob(pk.lines().next()?).ok()) { + Some(pk) => pk, + None => { + let pk = tor_interface::tor_crypto::Ed25519PrivateKey::generate(); + let _ = mode600(fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true)) + .open(&pk_path) + .and_then(|mut f| f.write_all(pk.to_key_blob().as_bytes()).and_then(|_| f.write_all(b"\n"))); + pk + } + }; let addresses = if register_hidden_service { - match tor_transport.add_customised_onion_service(loaded_pk.as_ref(), ASB_ONION_SERVICE_PORT, None, bindaddr) + match tor_transport.add_onion_service(&pk, ASB_ONION_SERVICE_PORT, None, Some(bindaddr)) { - Ok((addr, pk)) => { + Ok(addr) => { tracing::debug!( %addr, "Setting up onion service for libp2p to listen on" ); - if loaded_pk.is_none() { - let writeback = pk.to_key_blob(); - let _ = mode600(fs::OpenOptions::new() - .create(true) - .truncate(true) - .write(true)) - .open(&pk_path) - .and_then(|mut f| f.write_all(writeback.as_bytes()).and_then(|_| f.write_all(b"\n"))); - } vec![addr] } Err(err) => { diff --git a/swap/src/common/tor.rs b/swap/src/common/tor.rs index f0a438556..0bb76ae49 100644 --- a/swap/src/common/tor.rs +++ b/swap/src/common/tor.rs @@ -10,11 +10,11 @@ use swap_env::env::is_whonix; use tor_rtcompat::tokio::TokioRustlsRuntime; pub fn existing_tor_config() -> Option<( - libp2p_community_tor_interface::tor_interface::legacy_tor_client::LegacyTorClientConfig, + libp2p_tor::tor_interface::legacy_tor_client::LegacyTorClientConfig, std::net::SocketAddr, )> { if is_whonix() { - Some((libp2p_community_tor_interface::tor_interface::legacy_tor_client::LegacyTorClientConfig::system_from_environment().expect("whonix always has $TOR_... set"), + Some((libp2p_tor::tor_interface::legacy_tor_client::LegacyTorClientConfig::system_from_environment().expect("whonix always has $TOR_... set"), ([0, 0, 0, 0], 9939).into())) } else { None