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: 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() {