Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Tor support #72

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 89 additions & 8 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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 }
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
98 changes: 98 additions & 0 deletions src/tor.rs
Original file line number Diff line number Diff line change
@@ -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<JoinHandle<Result<u8, libtor::Error>>>,
/// 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<u16>) -> 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<u16> = 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));
}
}
114 changes: 111 additions & 3 deletions src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
//! to automagically provide encrypted and authenticated channels.
//!

#[cfg(feature = "tor")]
use crate::tor::TorProxy;
use crate::{
error::Error,
message,
Expand Down Expand Up @@ -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<KKTransport, Error> {
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<KKChannel, Error> {
let (cli_act_1, msg_1) =
KKHandshakeActOne::initiator(my_noise_privkey, their_noise_pubkey)?;

Expand All @@ -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
Expand Down Expand Up @@ -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<dyn std::error::Error>> {
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() {
Expand Down