diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 739fe386..9477b09c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,9 +46,20 @@ jobs: build_test: runs-on: ubuntu-latest steps: + - name: Checkout sources uses: actions/checkout@v2 + - name: Install and stop tor in case it was running + run: | + sudo apt install software-properties-common + sudo curl https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | sudo gpg --import + sudo gpg --export A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89 | sudo apt-key add - + sudo add-apt-repository 'deb https://deb.torproject.org/torproject.org bionic main' + sudo apt update + sudo apt install tor deb.torproject.org-keyring + sudo /etc/init.d/tor stop + - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index 313d3126..254496f0 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" authors = ["CoBloX Team "] edition = "2018" +# TODO: Check for stale dependencies, this looks like its a bit of a mess. + [dependencies] anyhow = "1" async-trait = "0.1" @@ -17,10 +19,13 @@ genawaiter = "0.99.1" miniscript = { version = "1", features = ["serde"] } monero = { version = "0.9", features = ["serde_support"] } 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", features = ["time"] } +tokio = { version = "0.2", default-features = false, features = ["blocking", "macros", "rt-core", "time", "rt-threaded"] } +torut = { version = "0.1", optional = true } tracing = "0.1" [dev-dependencies] @@ -28,11 +33,18 @@ backoff = { version = "0.2", features = ["tokio"] } base64 = "0.12" bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "7ff30a559ab57cc3aa71189e71433ef6b2a6c3a2" } futures = "0.3" +hyper = "0.13" monero-harness = { path = "../monero-harness" } reqwest = { version = "0.10", default-features = false } serde_cbor = "0.11" sled = "0.34" +port_check = "0.1" +spectral = "0.6" +tempfile = "3" 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" + +[features] +default = [] +tor = ["torut"] diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 4af89d72..dcef49ba 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -50,6 +50,8 @@ pub mod bitcoin; pub mod bob; pub mod monero; pub mod serde; +#[cfg(feature = "tor")] +pub mod tor; pub mod transport; use async_trait::async_trait; diff --git a/xmr-btc/src/tor.rs b/xmr-btc/src/tor.rs new file mode 100644 index 00000000..16e95ed1 --- /dev/null +++ b/xmr-btc/src/tor.rs @@ -0,0 +1,117 @@ +use anyhow::{anyhow, bail, Result}; +use std::{ + future::Future, + net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, +}; +use tokio::net::TcpStream; +use torut::{ + control::{AsyncEvent, AuthenticatedConn, ConnError, UnauthenticatedConn}, + onion::TorSecretKeyV3, +}; + +#[derive(Debug, Clone, Copy)] +pub struct UnauthenticatedConnection { + tor_proxy_address: SocketAddrV4, + tor_control_port_address: SocketAddr, +} + +impl Default for UnauthenticatedConnection { + fn default() -> Self { + Self { + tor_proxy_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9050), + tor_control_port_address: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9051)), + } + } +} + +impl UnauthenticatedConnection { + pub fn with_ports(proxy_port: u16, control_port: u16) -> Self { + Self { + tor_proxy_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, proxy_port), + tor_control_port_address: SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::LOCALHOST, + control_port, + )), + } + } + + /// checks if tor is running + async fn assert_tor_running(&self) -> Result<()> { + // Make sure you are running tor and this is your socks port + let proxy = reqwest::Proxy::all(format!("socks5h://{}", self.tor_proxy_address).as_str()) + .map_err(|_| anyhow!("tor proxy should be there"))?; + let client = reqwest::Client::builder().proxy(proxy).build()?; + + let res = client.get("https://check.torproject.org").send().await?; + let text = res.text().await?; + + if !text.contains("Congratulations. This browser is configured to use Tor.") { + bail!("Tor is currently not running") + } + + Ok(()) + } + + async fn init_unauthenticated_connection(&self) -> Result> { + // Connect to local tor service via control port + let sock = TcpStream::connect(self.tor_control_port_address).await?; + let uc = UnauthenticatedConn::new(sock); + Ok(uc) + } + + /// Create a new authenticated connection to your local Tor service + pub async fn init_authenticated_connection(self) -> Result { + self.assert_tor_running().await?; + + let mut uc = self + .init_unauthenticated_connection() + .await + .map_err(|_| anyhow!("Tor instance not running."))?; + + let tor_info = uc + .load_protocol_info() + .await + .map_err(|_| anyhow!("Failed to load protocol info from Tor."))?; + + let tor_auth_data = tor_info + .make_auth_data()? + .ok_or_else(|| anyhow!("Failed to make auth data."))?; + + // Get an authenticated connection to the Tor via the Tor Controller protocol. + uc.authenticate(&tor_auth_data) + .await + .map_err(|_| anyhow!("Failed to authenticate with Tor"))?; + + Ok(AuthenticatedConnection { + authenticated_connection: uc.into_authenticated().await, + }) + } +} + +type Handler = fn(AsyncEvent<'_>) -> Box> + Unpin>; + +#[allow(missing_debug_implementations)] +pub struct AuthenticatedConnection { + authenticated_connection: AuthenticatedConn, +} + +impl AuthenticatedConnection { + /// 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.authenticated_connection + .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(|e| anyhow!("Could not add onion service.: {:#?}", e)) + } +} diff --git a/xmr-btc/tests/tor.rs b/xmr-btc/tests/tor.rs new file mode 100644 index 00000000..5020e30e --- /dev/null +++ b/xmr-btc/tests/tor.rs @@ -0,0 +1,130 @@ +#[cfg(feature = "tor")] +mod tor_test { + + use anyhow::Result; + use hyper::service::{make_service_fn, service_fn}; + use reqwest::StatusCode; + use spectral::prelude::*; + use std::{convert::Infallible, fs}; + use tempfile::{Builder, NamedTempFile}; + use tokio::sync::oneshot::Receiver; + use torut::{ + onion::TorSecretKeyV3, + utils::{run_tor, AutoKillChild}, + }; + use tracing_subscriber::util::SubscriberInitExt; + use xmr_btc::tor::UnauthenticatedConnection; + + 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 { + if let Err(e) = graceful.await { + eprintln!("server error: {}", e); + } + }); + + tracing::info!("Test server started at port: {}", port); + } + + fn run_tmp_tor() -> Result<(AutoKillChild, u16, u16, NamedTempFile)> { + // we create an empty torrc file to not use the system one + let temp_torrc = Builder::new().tempfile()?; + let torrc_file = format!("{}", fs::canonicalize(temp_torrc.path())?.display()); + tracing::info!("Temp torrc file created at: {}", torrc_file); + + let control_port = if port_check::is_local_port_free(9051) { + 9051 + } else { + port_check::free_local_port().unwrap() + }; + let proxy_port = if port_check::is_local_port_free(9050) { + 9050 + } else { + port_check::free_local_port().unwrap() + }; + + let child = run_tor( + "tor", + &mut [ + "--CookieAuthentication", + "1", + "--ControlPort", + control_port.to_string().as_str(), + "--SocksPort", + proxy_port.to_string().as_str(), + "-f", + &torrc_file, + ] + .iter(), + )?; + tracing::info!("Tor running with pid: {}", child.id()); + let child = AutoKillChild::new(child); + Ok((child, control_port, proxy_port, temp_torrc)) + } + + #[tokio::test] + async fn test_tor_control_port() -> Result<()> { + let _guard = tracing_subscriber::fmt() + .with_env_filter("info") + .set_default(); + + // start tmp tor + let (_child, control_port, proxy_port, _tmp_torrc) = run_tmp_tor()?; + + // 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 = + UnauthenticatedConnection::with_ports(proxy_port, control_port) + .init_authenticated_connection() + .await?; + + tracing::info!("Tor authenticated."); + + // 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://127.0.0.1:{}", proxy_port).as_str()) + .expect("tor proxy should be there"); + let client = reqwest::Client::builder().proxy(proxy).build()?; + let onion_address = tor_secret_key_v3.public().get_onion_address().to_string(); + let onion_url = format!("http://{}:8080", onion_address); + + tracing::info!("Tor service added: {}", onion_url); + + 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"); + tracing::info!( + "Local server called via Tor proxy. Its response is: {}", + text + ); + + // gracefully shut down server + let _ = tx.send(()); + Ok(()) + } +}