Skip to content

Commit cfb2b79

Browse files
Add Tor support
1 parent a4b2ca9 commit cfb2b79

File tree

4 files changed

+219
-3
lines changed

4 files changed

+219
-3
lines changed

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ exclude = [".github/", "fuzz"]
1212
[features]
1313
# Get access to internal APIs from the fuzzing framework
1414
fuzz = []
15+
tor = ["libtor", "socks"]
1516

1617
[dependencies]
1718
serde = { version = "1.0", features = ["derive"] }
@@ -20,8 +21,15 @@ serde_json = "1.0"
2021
revault_tx = { git = "https://github.com/revault/revault_tx", features = ["use-serde"] }
2122
bitcoin = { version = "0.27", features = ["use-serde"] }
2223
snow = { version = "0.7", default-features = false, features = ["libsodium-resolver"] }
24+
socks = { version = "0.3.3", optional = true }
2325

2426
# Used for Noise crypto and generating pubkeys
2527
sodiumoxide = { version = "0.2", features = ["serde"] }
2628

2729
log = "0.4"
30+
31+
# We need to use vendored-openssl on Windows
32+
[target.'cfg(target_os = "windows")'.dependencies]
33+
libtor = { version = "46.6", optional = true, features = ["vendored-openssl"] }
34+
[target.'cfg(not(target_os = "windows"))'.dependencies]
35+
libtor = { version = "46.6", optional = true }

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@ pub mod transport;
1414
mod error;
1515
pub use error::Error;
1616

17+
#[cfg(feature = "tor")]
18+
pub mod tor;
19+
1720
pub use revault_tx::bitcoin;
1821
pub use sodiumoxide;

src/tor.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//! Tor wrapper
2+
//!
3+
//! Contains useful methods for starting the Tor daemon
4+
use libtor::{
5+
log::{LogDestination, LogLevel},
6+
Tor, TorFlag,
7+
};
8+
use std::thread::JoinHandle;
9+
10+
// Libtor doesn't like msvc ¯\_(ツ)_/¯
11+
#[cfg(target_env = "msvc")]
12+
compile_error!("Tor feature can't be used with msvc. Use mingw instead.");
13+
14+
/// Result of the `start_tor` method. Contains useful info
15+
/// about the Tor daemon running
16+
#[derive(Debug)]
17+
pub struct TorProxy {
18+
/// JoinHandle of the Tor daemon
19+
pub tor_handle: Option<JoinHandle<Result<u8, libtor::Error>>>,
20+
/// Host and port of the SOCKS5 proxy
21+
pub address: String,
22+
/// Socks port used by the Tor daemon
23+
pub socks_port: u16,
24+
/// Data directory used by the Tor daemon
25+
pub data_directory: String,
26+
}
27+
28+
impl TorProxy {
29+
/// Starts the Tor daemon using the provided data_directory and socks_port. If
30+
/// no socks_port is provided, Tor will pick one, which will be available in
31+
/// the `TorProxy` structure
32+
// TODO: maybe add the control port as well? It might be useful.
33+
pub fn start_tor(data_directory: String, socks_port: Option<u16>) -> Self {
34+
let log_file = format!("{}/log", data_directory);
35+
let mut tor = Tor::new();
36+
tor.flag(TorFlag::LogTo(
37+
LogLevel::Notice,
38+
LogDestination::File(log_file.clone()),
39+
))
40+
.flag(TorFlag::DataDirectory(data_directory.clone()))
41+
// Otherwise tor will catch our attempts to shut down processes...
42+
.flag(TorFlag::Custom("__DisableSignalHandlers 1".into()));
43+
44+
if let Some(port) = socks_port {
45+
tor.flag(TorFlag::SocksPort(port));
46+
} else {
47+
tor.flag(TorFlag::Custom("SocksPort auto".into()));
48+
}
49+
50+
let tor_handle = tor.start_background().into();
51+
52+
let socks_port = socks_port.unwrap_or_else(|| {
53+
// Alright, we need to discover which socks port we're using
54+
// Let's grep the log file :)
55+
use std::io::Read;
56+
let needle = "Socks listener listening on port ";
57+
for _ in 0..15 {
58+
let mut haystack = String::new();
59+
let port: Option<u16> = std::fs::File::open(&log_file)
60+
.ok()
61+
.and_then(|mut f| f.read_to_string(&mut haystack).ok())
62+
.and_then(|_| haystack.find(needle))
63+
.and_then(|i| haystack[i + needle.len()..].splitn(2, '.').next())
64+
.and_then(|s| s.parse().ok());
65+
if let Some(port) = port {
66+
return port;
67+
}
68+
std::thread::sleep(std::time::Duration::from_millis(100));
69+
}
70+
panic!("Can't find socks_port in logfile");
71+
});
72+
73+
let address = format!("127.0.0.1:{}", socks_port);
74+
TorProxy {
75+
tor_handle,
76+
address,
77+
socks_port,
78+
data_directory,
79+
}
80+
}
81+
}
82+
83+
#[cfg(test)]
84+
mod tests {
85+
use super::*;
86+
87+
#[test]
88+
#[ignore]
89+
fn start_tor() {
90+
// FIXME: Well, this is not testing much. Ignored for now, but it might
91+
// be useful for debugging purposes.
92+
// Note that you can't have multiple tor running in the same process,
93+
// so if you want to start this you need to make sure that `cargo test`
94+
// is not running other tests that start tor (only test_transport_kk_tor
95+
// for now).
96+
TorProxy::start_tor("/tmp/tor-revault-net".into(), None);
97+
std::thread::sleep(std::time::Duration::from_secs(10));
98+
}
99+
}

src/transport.rs

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
//! to automagically provide encrypted and authenticated channels.
55
//!
66
7+
#[cfg(feature = "tor")]
8+
use crate::tor::TorProxy;
79
use crate::{
810
error::Error,
911
message,
@@ -36,7 +38,41 @@ impl KKTransport {
3638
let timeout = Duration::from_secs(20);
3739
let mut stream = TcpStream::connect_timeout(&addr, timeout)?;
3840
stream.set_read_timeout(Some(timeout))?;
41+
let channel = KKTransport::perform_client_handshake(
42+
&mut stream,
43+
my_noise_privkey,
44+
their_noise_pubkey,
45+
)?;
46+
Ok(KKTransport { stream, channel })
47+
}
48+
49+
#[cfg(feature = "tor")]
50+
/// Connect to server at given tor address using the provided SOCKS5 proxy,
51+
/// and enact Noise handshake with given private key.
52+
/// Sets a read timeout of 20 seconds.
53+
pub fn tor_connect(
54+
addr: &str,
55+
proxy: &TorProxy,
56+
my_noise_privkey: &SecretKey,
57+
their_noise_pubkey: &PublicKey,
58+
) -> Result<KKTransport, Error> {
59+
let mut stream = socks::Socks5Stream::connect(&proxy.address, addr)?.into_inner();
60+
let timeout = Duration::from_secs(20);
61+
stream.set_read_timeout(Some(timeout))?;
62+
let channel = KKTransport::perform_client_handshake(
63+
&mut stream,
64+
my_noise_privkey,
65+
their_noise_pubkey,
66+
)?;
67+
Ok(KKTransport { stream, channel })
68+
}
3969

70+
// Used by connect() and tor_connect() to perform the handshake
71+
fn perform_client_handshake(
72+
stream: &mut TcpStream,
73+
my_noise_privkey: &SecretKey,
74+
their_noise_pubkey: &PublicKey,
75+
) -> Result<KKChannel, Error> {
4076
let (cli_act_1, msg_1) =
4177
KKHandshakeActOne::initiator(my_noise_privkey, their_noise_pubkey)?;
4278

@@ -49,8 +85,7 @@ impl KKTransport {
4985

5086
let msg_act_2 = KKMessageActTwo(msg_2);
5187
let cli_act_2 = KKHandshakeActTwo::initiator(cli_act_1, &msg_act_2)?;
52-
let channel = KKChannel::from_handshake(cli_act_2)?;
53-
Ok(KKTransport { stream, channel })
88+
KKChannel::from_handshake(cli_act_2).map_err(|e| e.into())
5489
}
5590

5691
/// Accept an incoming connection and immediately perform the noise KK handshake
@@ -189,7 +224,78 @@ impl KKTransport {
189224
mod tests {
190225
use super::*;
191226
use sodiumoxide::crypto::box_::curve25519xsalsa20poly1305::gen_keypair;
192-
use std::{collections::BTreeMap, str::FromStr, thread};
227+
use std::{collections::BTreeMap, fs, process::Command, str::FromStr, thread};
228+
229+
#[test]
230+
#[cfg(feature = "tor")]
231+
fn test_transport_kk_tor() {
232+
let ((client_pubkey, client_privkey), (server_pubkey, server_privkey)) =
233+
(gen_keypair(), gen_keypair());
234+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
235+
let server_addr = listener.local_addr().unwrap();
236+
237+
let datadir = "scratch_test_datadir";
238+
// Clean from previous run
239+
fs::remove_dir_all(&datadir).unwrap_or_else(|_| ());
240+
fs::create_dir(&datadir).unwrap();
241+
let mut file = fs::File::create(format!("{}/torrc", datadir)).unwrap();
242+
let torrc = format!(
243+
r#"HiddenServiceDir {0}/hidden_service/
244+
HiddenServicePort 19051 127.0.0.1:{1}
245+
DataDirectory {0}/server
246+
Log notice file {0}/server/log
247+
SOCKSPort 0"#,
248+
datadir,
249+
server_addr.port(),
250+
);
251+
file.write_all(torrc.as_bytes()).unwrap();
252+
let mut hidden_service_process = Command::new("tor")
253+
.args(&["-f", &format!("{}/torrc", datadir)])
254+
.spawn()
255+
.expect("Tor failed to start");
256+
257+
let msg = "Test message".as_bytes();
258+
259+
// hidden_service_process won't be killed if we panic here, so
260+
// instead of unwrapping directly I'm using `?` in a closure
261+
// and unwrapping the result after killing tor.
262+
// This way if there's an error we don't leave dangling tors around
263+
let c = || -> Result<_, Box<dyn std::error::Error>> {
264+
let client_proxy = TorProxy::start_tor(format!("{}/client/", datadir).into(), None);
265+
266+
// server thread
267+
let server_thread = thread::spawn(move || {
268+
let my_noise_privkey = server_privkey;
269+
let their_noise_pubkey = client_pubkey;
270+
let mut server_transport =
271+
KKTransport::accept(&listener, &my_noise_privkey, &[their_noise_pubkey])?;
272+
server_transport.read()
273+
});
274+
275+
// Giving tor a bit of time to start...
276+
std::thread::sleep(std::time::Duration::from_secs(30));
277+
let hidden_service_onion =
278+
fs::read_to_string(format!("{}/hidden_service/hostname", datadir))?;
279+
let hidden_service_address = format!("{}:19051", hidden_service_onion.trim_end());
280+
281+
// client thread
282+
let mut cli_channel = KKTransport::tor_connect(
283+
&hidden_service_address,
284+
&client_proxy,
285+
&client_privkey,
286+
&server_pubkey,
287+
)?;
288+
cli_channel.write(&msg)?;
289+
290+
Ok(server_thread
291+
.join()
292+
.map_err(|_| String::from("Error joining thread"))??)
293+
};
294+
295+
let received_msg = c();
296+
hidden_service_process.kill().unwrap_or_else(|_| {});
297+
assert_eq!(msg, received_msg.unwrap().as_slice());
298+
}
193299

194300
#[test]
195301
fn test_transport_kk() {

0 commit comments

Comments
 (0)