From 5e19949d716987efbb41ddd09da05ab7235831f4 Mon Sep 17 00:00:00 2001 From: Philipp Hoenisch Date: Tue, 20 Oct 2020 11:43:13 +1100 Subject: [PATCH] Add library to create a Tor service via Tor control port --- xmr-btc/Cargo.toml | 12 +++-- xmr-btc/src/lib.rs | 1 + xmr-btc/src/tor.rs | 104 +++++++++++++++++++++++++++++++++++++++++++ xmr-btc/tests/tor.rs | 64 ++++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 xmr-btc/src/tor.rs create mode 100644 xmr-btc/tests/tor.rs diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index c25060d0..767e8d49 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -12,20 +12,26 @@ cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", curve25519-dalek = "2" ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat"] } ed25519-dalek = "1.0.0-pre.4" # Cannot be 1 because they depend on curve25519-dalek version 3 +lazy_static = "1.4" miniscript = "1" monero = "0.9" rand = "0.7" +reqwest = { version = "0.10", default-features = false, features = ["socks"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" sha2 = "0.9" thiserror = "1" +tokio = { version = "0.2", default-features = false, features = ["blocking", "macros", "rt-core", "time", "rt-threaded"] } +torut = "0.1" tracing = "0.1" [dev-dependencies] base64 = "0.12" bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "d402b36d3d6406150e3bfb71492ff4a0a7cb290e" } futures = "0.3" +hyper = "0.13" monero-harness = { path = "../monero-harness" } -reqwest = { version = "0.10", default-features = false } +spectral = "0.6" testcontainers = "0.10" -tokio = { version = "0.2", default-features = false, features = ["blocking", "macros", "rt-core", "time", "rt-threaded"] } tracing = "0.1" -tracing-subscriber = "0.2" +tracing-subscriber = "0.2" \ No newline at end of file diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 790cb477..f4638dc4 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -49,4 +49,5 @@ pub mod alice; pub mod bitcoin; pub mod bob; pub mod monero; +pub mod tor; pub mod transport; diff --git a/xmr-btc/src/tor.rs b/xmr-btc/src/tor.rs new file mode 100644 index 00000000..fbc9f191 --- /dev/null +++ b/xmr-btc/src/tor.rs @@ -0,0 +1,104 @@ +use anyhow::{anyhow, bail, Result}; +use lazy_static::lazy_static; +use std::{ + future::Future, + net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, +}; +use tokio::net::TcpStream; +use torut::{ + control::{AsyncEvent, AuthenticatedConn, ConnError, UnauthenticatedConn}, + onion::TorSecretKeyV3, +}; + +lazy_static! { + /// The default TOR socks5 proxy address, `127.0.0.1:9050`. + pub static ref TOR_PROXY_ADDR: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9050); + /// The default TOR Controller Protocol address, `127.0.0.1:9051`. + pub static ref TOR_CP_ADDR: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9051)); +} + +/// checks if tor is running +async fn tor_running() -> Result<()> { + // Make sure you are running tor and this is your socks port + let proxy = reqwest::Proxy::all(format!("socks5h://{}", *TOR_PROXY_ADDR).as_str()) + .expect("tor proxy should be there"); + let client = reqwest::Client::builder() + .proxy(proxy) + .build() + .expect("should be able to build reqwest client"); + + let res = client.get("https://check.torproject.org").send().await?; + + let text = res.text().await?; + let is_tor = text.contains("Congratulations. This browser is configured to use Tor."); + + if is_tor { + Ok(()) + } else { + bail!("Tor is currently not running") + } +} + +type Handler = fn(AsyncEvent<'_>) -> Box> + Unpin>; + +#[allow(missing_debug_implementations)] +pub struct AuthenticatedConnection(AuthenticatedConn); + +impl AuthenticatedConnection { + async fn init_unauthenticated_connection() -> Result> { + // try to connect to local tor service via control port + let sock = TcpStream::connect(*TOR_CP_ADDR).await?; + let unauthenticated_connection = UnauthenticatedConn::new(sock); + Ok(unauthenticated_connection) + } + + /// Create a new authenticated connection to your local Tor service + pub async fn new() -> Result { + tor_running().await?; + + let mut unauthenticated_connection = match Self::init_unauthenticated_connection().await { + Err(_) => bail!("Tor instance not running"), + Ok(unauthenticated_connection) => unauthenticated_connection, + }; + + let tor_info = match unauthenticated_connection.load_protocol_info().await { + Ok(info) => info, + Err(_) => bail!("Failed to load protocol info from Tor."), + }; + let tor_auth_data = tor_info + .make_auth_data()? + .expect("Failed to make auth data."); + + // Get an authenticated connection to the Tor via the Tor Controller protocol. + if unauthenticated_connection + .authenticate(&tor_auth_data) + .await + .is_err() + { + bail!("Failed to authenticate with Tor") + } + let authenticated_connection = unauthenticated_connection.into_authenticated().await; + + Ok(AuthenticatedConnection(authenticated_connection)) + } + + /// Add an ephemeral tor service on localhost with the provided key + pub async fn add_service(&mut self, port: u16, tor_key: &TorSecretKeyV3) -> Result<()> { + self.0 + .add_onion_v3( + tor_key, + false, + false, + false, + None, + &mut [( + port, + SocketAddr::new(IpAddr::from(Ipv4Addr::new(127, 0, 0, 1)), port), + )] + .iter(), + ) + .await + .map_err(|_| anyhow!("Could not add onion service."))?; + Ok(()) + } +} diff --git a/xmr-btc/tests/tor.rs b/xmr-btc/tests/tor.rs new file mode 100644 index 00000000..ff667231 --- /dev/null +++ b/xmr-btc/tests/tor.rs @@ -0,0 +1,64 @@ +use anyhow::Result; +use hyper::service::{make_service_fn, service_fn}; +use reqwest::StatusCode; +use spectral::prelude::*; +use std::convert::Infallible; +use tokio::sync::oneshot::Receiver; +use torut::onion::TorSecretKeyV3; +use xmr_btc::tor::{AuthenticatedConnection, TOR_PROXY_ADDR}; + +async fn hello_world( + _req: hyper::Request, +) -> Result, Infallible> { + Ok(hyper::Response::new("Hello World".into())) +} + +fn start_test_service(port: u16, rx: Receiver<()>) { + let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(hello_world)) }); + let addr = ([127, 0, 0, 1], port).into(); + let server = hyper::Server::bind(&addr).serve(make_svc); + let graceful = server.with_graceful_shutdown(async { + rx.await.ok(); + }); + tokio::spawn(async { + // server.await.unwrap(); + if let Err(e) = graceful.await { + eprintln!("server error: {}", e); + } + }); +} + +#[tokio::test] +async fn test_tor_control_port() -> Result<()> { + // Setup test HTTP Server + let (tx, rx) = tokio::sync::oneshot::channel::<()>(); + let port = 8080; + start_test_service(port, rx); + + // Connect to local Tor service + let mut authenticated_connection = AuthenticatedConnection::new().await?; + + // Expose an onion service that re-directs to the echo server. + let tor_secret_key_v3 = TorSecretKeyV3::generate(); + authenticated_connection + .add_service(port, &tor_secret_key_v3) + .await?; + + // Test if Tor service forwards to HTTP Server + + let proxy = reqwest::Proxy::all(format!("socks5h://{}", *TOR_PROXY_ADDR).as_str()) + .expect("tor proxy should be there"); + let client = reqwest::Client::builder().proxy(proxy).build().unwrap(); + let onion_address = tor_secret_key_v3.public().get_onion_address().to_string(); + let onion_url = format!("http://{}:8080", onion_address); + + let res = client.get(&onion_url).send().await?; + assert_that(&res.status()).is_equal_to(StatusCode::OK); + + let text = res.text().await?; + assert_that!(text).contains("Hello World"); + + // gracefully shut down server + let _ = tx.send(()); + Ok(()) +}