From ab75952a812ac35ba4b311b0c6d1486afe849a88 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Mon, 4 Oct 2021 13:09:54 +0200 Subject: [PATCH 1/2] Add Tor support --- Cargo.toml | 8 ++++ src/lib.rs | 3 ++ src/tor.rs | 98 ++++++++++++++++++++++++++++++++++++++++ src/transport.rs | 114 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 src/tor.rs diff --git a/Cargo.toml b/Cargo.toml index 1f96df0..4ae99a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ exclude = [".github/", "fuzz"] [features] # Get access to internal APIs from the fuzzing framework fuzz = [] +tor = ["libtor", "socks"] [dependencies] serde = { version = "1.0", features = ["derive"] } @@ -20,8 +21,15 @@ serde_json = "1.0" revault_tx = { git = "https://github.com/revault/revault_tx", features = ["use-serde"] } bitcoin = { version = "0.27", features = ["use-serde"] } snow = { version = "0.7", default-features = false, features = ["libsodium-resolver"] } +socks = { version = "0.3.3", optional = true } # Used for Noise crypto and generating pubkeys sodiumoxide = { version = "0.2", features = ["serde"] } log = "0.4" + +# We need to use vendored-openssl on Windows +[target.'cfg(target_os = "windows")'.dependencies] +libtor = { version = "46.6", optional = true, features = ["vendored-openssl"] } +[target.'cfg(not(target_os = "windows"))'.dependencies] +libtor = { version = "46.6", optional = true } diff --git a/src/lib.rs b/src/lib.rs index 7b8d4ee..2bef5cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,5 +14,8 @@ pub mod transport; mod error; pub use error::Error; +#[cfg(feature = "tor")] +pub mod tor; + pub use revault_tx::bitcoin; pub use sodiumoxide; diff --git a/src/tor.rs b/src/tor.rs new file mode 100644 index 0000000..8edf389 --- /dev/null +++ b/src/tor.rs @@ -0,0 +1,98 @@ +//! Tor wrapper +//! +//! Contains useful methods for starting the Tor daemon +use libtor::{ + log::{LogDestination, LogLevel}, + Tor, TorFlag, +}; +use std::thread::JoinHandle; + +// Libtor doesn't like msvc ¯\_(ツ)_/¯ +#[cfg(target_env = "msvc")] +compile_error!("Tor feature can't be used with msvc. Use mingw instead."); + +/// Result of the `start_tor` method. Contains useful info +/// about the Tor daemon running +#[derive(Debug)] +pub struct TorProxy { + /// JoinHandle of the Tor daemon + pub tor_handle: Option>>, + /// Host of the SOCKS5 proxy + pub host: String, + /// Socks port used by the Tor daemon + pub socks_port: u16, + /// Data directory used by the Tor daemon + pub data_directory: String, +} + +impl TorProxy { + /// Starts the Tor daemon using the provided data_directory and socks_port. If + /// no socks_port is provided, Tor will pick one, which will be available in + /// the `TorProxy` structure + // TODO: maybe add the control port as well? It might be useful. + pub fn start_tor(data_directory: String, socks_port: Option) -> Self { + let log_file = format!("{}/log", data_directory); + let mut tor = Tor::new(); + tor.flag(TorFlag::LogTo( + LogLevel::Notice, + LogDestination::File(log_file.clone()), + )) + .flag(TorFlag::DataDirectory(data_directory.clone())) + // Otherwise tor will catch our attempts to shut down processes... + .flag(TorFlag::Custom("__DisableSignalHandlers 1".into())); + + if let Some(port) = socks_port { + tor.flag(TorFlag::SocksPort(port)); + } else { + tor.flag(TorFlag::Custom("SocksPort auto".into())); + } + + let tor_handle = tor.start_background().into(); + + let socks_port = socks_port.unwrap_or_else(|| { + // Alright, we need to discover which socks port we're using + // Let's grep the log file :) + use std::io::Read; + let needle = "Socks listener listening on port "; + for _ in 0..15 { + let mut haystack = String::new(); + let port: Option = std::fs::File::open(&log_file) + .ok() + .and_then(|mut f| f.read_to_string(&mut haystack).ok()) + .and_then(|_| haystack.find(needle)) + .and_then(|i| haystack[i + needle.len()..].splitn(2, '.').next()) + .and_then(|s| s.parse().ok()); + if let Some(port) = port { + return port; + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + panic!("Can't find socks_port in logfile"); + }); + + TorProxy { + tor_handle, + host: "127.0.0.1".into(), + socks_port, + data_directory, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[ignore] + fn start_tor() { + // FIXME: Well, this is not testing much. Ignored for now, but it might + // be useful for debugging purposes. + // Note that you can't have multiple tor running in the same process, + // so if you want to start this you need to make sure that `cargo test` + // is not running other tests that start tor (only test_transport_kk_tor + // for now). + TorProxy::start_tor("/tmp/tor-revault-net".into(), None); + std::thread::sleep(std::time::Duration::from_secs(10)); + } +} diff --git a/src/transport.rs b/src/transport.rs index 9c8c6d8..45bd755 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -4,6 +4,8 @@ //! to automagically provide encrypted and authenticated channels. //! +#[cfg(feature = "tor")] +use crate::tor::TorProxy; use crate::{ error::Error, message, @@ -36,7 +38,43 @@ impl KKTransport { let timeout = Duration::from_secs(20); let mut stream = TcpStream::connect_timeout(&addr, timeout)?; stream.set_read_timeout(Some(timeout))?; + let channel = KKTransport::perform_client_handshake( + &mut stream, + my_noise_privkey, + their_noise_pubkey, + )?; + Ok(KKTransport { stream, channel }) + } + + #[cfg(feature = "tor")] + /// Connect to server at given tor address using the provided SOCKS5 proxy, + /// and enact Noise handshake with given private key. + /// Sets a read timeout of 20 seconds. + pub fn tor_connect( + addr: &str, + proxy: &TorProxy, + my_noise_privkey: &SecretKey, + their_noise_pubkey: &PublicKey, + ) -> Result { + let mut stream = + socks::Socks5Stream::connect(&format!("{}:{}", proxy.host, proxy.socks_port), addr)? + .into_inner(); + let timeout = Duration::from_secs(20); + stream.set_read_timeout(Some(timeout))?; + let channel = KKTransport::perform_client_handshake( + &mut stream, + my_noise_privkey, + their_noise_pubkey, + )?; + Ok(KKTransport { stream, channel }) + } + // Used by connect() and tor_connect() to perform the handshake + fn perform_client_handshake( + stream: &mut TcpStream, + my_noise_privkey: &SecretKey, + their_noise_pubkey: &PublicKey, + ) -> Result { let (cli_act_1, msg_1) = KKHandshakeActOne::initiator(my_noise_privkey, their_noise_pubkey)?; @@ -49,8 +87,7 @@ impl KKTransport { let msg_act_2 = KKMessageActTwo(msg_2); let cli_act_2 = KKHandshakeActTwo::initiator(cli_act_1, &msg_act_2)?; - let channel = KKChannel::from_handshake(cli_act_2)?; - Ok(KKTransport { stream, channel }) + KKChannel::from_handshake(cli_act_2).map_err(|e| e.into()) } /// Accept an incoming connection and immediately perform the noise KK handshake @@ -189,7 +226,78 @@ impl KKTransport { mod tests { use super::*; use sodiumoxide::crypto::box_::curve25519xsalsa20poly1305::gen_keypair; - use std::{collections::BTreeMap, str::FromStr, thread}; + use std::{collections::BTreeMap, fs, process::Command, str::FromStr, thread}; + + #[test] + #[cfg(feature = "tor")] + fn test_transport_kk_tor() { + let ((client_pubkey, client_privkey), (server_pubkey, server_privkey)) = + (gen_keypair(), gen_keypair()); + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let server_addr = listener.local_addr().unwrap(); + + let datadir = "scratch_test_datadir"; + // Clean from previous run + fs::remove_dir_all(&datadir).unwrap_or_else(|_| ()); + fs::create_dir(&datadir).unwrap(); + let mut file = fs::File::create(format!("{}/torrc", datadir)).unwrap(); + let torrc = format!( + r#"HiddenServiceDir {0}/hidden_service/ +HiddenServicePort 19051 127.0.0.1:{1} +DataDirectory {0}/server +Log notice file {0}/server/log +SOCKSPort 0"#, + datadir, + server_addr.port(), + ); + file.write_all(torrc.as_bytes()).unwrap(); + let mut hidden_service_process = Command::new("tor") + .args(&["-f", &format!("{}/torrc", datadir)]) + .spawn() + .expect("Tor failed to start"); + + let msg = "Test message".as_bytes(); + + // hidden_service_process won't be killed if we panic here, so + // instead of unwrapping directly I'm using `?` in a closure + // and unwrapping the result after killing tor. + // This way if there's an error we don't leave dangling tors around + let c = || -> Result<_, Box> { + let client_proxy = TorProxy::start_tor(format!("{}/client/", datadir).into(), None); + + // server thread + let server_thread = thread::spawn(move || { + let my_noise_privkey = server_privkey; + let their_noise_pubkey = client_pubkey; + let mut server_transport = + KKTransport::accept(&listener, &my_noise_privkey, &[their_noise_pubkey])?; + server_transport.read() + }); + + // Giving tor a bit of time to start... + std::thread::sleep(std::time::Duration::from_secs(30)); + let hidden_service_onion = + fs::read_to_string(format!("{}/hidden_service/hostname", datadir))?; + let hidden_service_address = format!("{}:19051", hidden_service_onion.trim_end()); + + // client thread + let mut cli_channel = KKTransport::tor_connect( + &hidden_service_address, + &client_proxy, + &client_privkey, + &server_pubkey, + )?; + cli_channel.write(&msg)?; + + Ok(server_thread + .join() + .map_err(|_| String::from("Error joining thread"))??) + }; + + let received_msg = c(); + hidden_service_process.kill().unwrap_or_else(|_| {}); + assert_eq!(msg, received_msg.unwrap().as_slice()); + } #[test] fn test_transport_kk() { From 00d1efc8342dd390356590af0bea8eea03dec8bd Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Mon, 4 Oct 2021 13:10:16 +0200 Subject: [PATCH 2/2] Fix CI tests Divide the "tests" job in 3 different jobs, one for OS. Needed because different OSes need different compilation methods for tor, as well as different methods for installing the tor binary. Windows is compiled without the tor feature, and cross-compiled from linux with the tor feature. --- .github/workflows/rust.yml | 97 ++++++++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 8 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 20b7043..8ebf4fe 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -3,17 +3,13 @@ name: CI on: [pull_request] jobs: - tests: + linux-tests: strategy: matrix: toolchain: - nightly - 1.43 - os: - - ubuntu-latest - - macOS-latest - - windows-latest - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest steps: - name: Checkout source code uses: actions/checkout@v2 @@ -23,14 +19,99 @@ jobs: toolchain: ${{ matrix.toolchain }} override: true profile: minimal + # We need to download Tor for the tests. Not needed if + # you don't run the tor-specific tests, which need to + # start a tor HS from command line. + - name: Download Tor + run: sudo apt-get install -y tor - name: Build on Rust ${{ matrix.toolchain }} - run: cargo build --verbose --color always + run: cargo build --all-features --verbose --color always - name: Test on Rust ${{ matrix.toolchain }} run: cargo test --all-features --verbose --color always - name: Fuzz - if: matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' + if: matrix.toolchain == 'nightly' run: ./fuzz/run.sh + macos-tests: + strategy: + matrix: + toolchain: + - nightly + - 1.43 + runs-on: macOS-latest + steps: + - name: Checkout source code + uses: actions/checkout@v2 + - name: Install Rust ${{ matrix.toolchain }} toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + override: true + profile: minimal + # We need to download Tor for the tests. Not needed if + # you don't run the tor-specific tests, which need to + # start a tor HS from command line. + - name: Download deps + run: brew install tor autoconf automake + - name: Build on Rust ${{ matrix.toolchain }} + run: cargo build --all-features -vv --color always + - name: Test on Rust ${{ matrix.toolchain }} + run: cargo test --all-features -vv --color always + + windows-tests: + strategy: + matrix: + toolchain: + - nightly + - 1.43 + runs-on: windows-latest + steps: + - name: Checkout source code + uses: actions/checkout@v2 + - name: Install Rust ${{ matrix.toolchain }} toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + override: true + profile: minimal + - name: Build on Rust ${{ matrix.toolchain }} + # We can't compile tor on windows, cross-compile only :) + run: cargo build --verbose --color always + - name: Test on Rust ${{ matrix.toolchain }} + run: cargo test --verbose --color always + + # We only cross compile revualt_net with the tor feature for windows, + # but we don't run any test. In the future we could download the artifact + # from CI and try to run it *somehow*, at the moment I think the tests + # are Unix dependent anyways. + windows-cross-compile-tor: + strategy: + matrix: + toolchain: + - nightly + - 1.43 + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v2 + - name: Install needed deps + run: sudo apt-get update && sudo apt-get install -y mingw-w64 tar + - name: Install Rust ${{ matrix.toolchain }} toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + target: x86_64-pc-windows-gnu + override: true + profile: minimal + # libsodium build.rs is broken: https://github.com/sodiumoxide/sodiumoxide/issues/377 + # We need to manually download libsodium and give it to cargo while compiling + # Note that we could use the libsodium.a already provided in sodiumoxide, but it's tricky to find + # FIXME: we are not verifying sigs!! In CI who cares but don't forget to verify them in real life lol + - name: Download libsodium + run: wget https://download.libsodium.org/libsodium/releases/libsodium-1.0.18-mingw.tar.gz && tar xvf libsodium-1.0.18-mingw.tar.gz + - name: Build on Rust ${{ matrix.toolchain }} + run: SODIUM_LIB_DIR=$PWD/libsodium-win64/lib/ cargo build -vv --color always --all-features --target x86_64-pc-windows-gnu + rustfmt_check: runs-on: ubuntu-latest steps: