diff --git a/CHANGELOG.md b/CHANGELOG.md index cfa7ad98c1..d2a443d8d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - ASB + GUI + CLI + SWAP: Split high-verbosity tracing into separate hourly-rotating JSON log files per subsystem to reduce noise and aid debugging: `tracing*.log` (core things), `tracing-tor*.log` (purely tor related), `tracing-libp2p*.log` (low level networking), `tracing-monero-wallet*.log` (low level Monero wallet related). `swap-all.log` remains for non-verbose logs. - ASB: Fix an issue where we would not redeem the Bitcoin and force a refund even though it was still possible to do so. - GUI: Potentially fix issue here swaps would not be displayed +- SWAP-TOR: New crate unifying the existing Arti and new SOCKS5 Tor back-ends. +- MONERO-RPC-POOL + ASB + CLI: Use SWAP-TOR. +- SWAP: Remove DNS leak if using Tor. +- SWAP-TOR + MONERO-RPC-POOL + CLI + GUI: Detect Whonix/Tails and use their system Tor daemons to connect over Tor; this cannot be disabled. +- SWAP-TOR + MONERO-RPC-POOL + CLI + GUI: Where required (Tails), use SOCKS5 proxy to dial out always. ## [3.2.7] - 2025-10-28 diff --git a/Cargo.lock b/Cargo.lock index 3b5199bd38..9b5faaca01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1170,6 +1170,7 @@ dependencies = [ "swap-env", "swap-proptest", "swap-serde", + "swap-tor", "thiserror 1.0.69", "tokio", "tracing", @@ -6487,8 +6488,10 @@ dependencies = [ "serde_json", "sqlx", "swap-serde", + "swap-tor", "tokio", "tokio-rustls 0.26.4", + "tokio-socks", "tor-rtcompat", "tower-http", "tracing", @@ -6591,6 +6594,7 @@ dependencies = [ "monero-wallet 0.1.0 (git+https://github.com/kayabaNerve/monero-oxide.git?branch=rpc-rewrite)", "monero-wallet-ng", "swap-core", + "swap-tor", "throttle", "tokio", "tracing", @@ -10636,6 +10640,7 @@ dependencies = [ "swap-p2p", "swap-proptest", "swap-serde", + "swap-tor", "tauri", "tempfile", "testcontainers", @@ -10768,6 +10773,7 @@ dependencies = [ "serde", "swap-fs", "swap-serde", + "swap-tor", "thiserror 1.0.69", "time", "toml 0.9.8", @@ -10906,6 +10912,23 @@ dependencies = [ "url", ] +[[package]] +name = "swap-tor" +version = "3.2.0-rc.4" +dependencies = [ + "anyhow", + "arti-client", + "data-encoding", + "futures", + "libp2p", + "once_cell", + "tokio", + "tokio-socks", + "tokio-util", + "tor-rtcompat", + "tracing", +] + [[package]] name = "swift-rs" version = "1.0.7" @@ -11782,6 +11805,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -13411,6 +13446,7 @@ dependencies = [ "serde_json", "swap", "swap-p2p", + "swap-tor", "tauri", "tauri-build", "tauri-plugin-cli", diff --git a/Cargo.toml b/Cargo.toml index 27bbbe56cd..7c311f9ac1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "swap-p2p", "swap-proptest", "swap-serde", + "swap-tor", "throttle", "tracing-ext", ] diff --git a/README.md b/README.md index caa36c3867..e709db6095 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This is the monorepo containing the source code for all of our core projects: - [`tauri`](src-tauri/) contains the tauri bindings between binaries and user interface - and other crates we use in our binaries -If you're just here for the software, head over to the [releases](https://github.com/eigenwallet/core/releases/latest) tab and grab the binary for your operating system! If you're just looking for documentation, check out our [docs page](https://docs.eigenwallet.org/) or our [github docs](dev-docs/README.md). +If you're just here for the software, head over to the [releases](https://github.com/eigenwallet/core/releases/latest) tab and grab the binary for your operating system! If you're just looking for documentation, check out our [docs page](https://docs.unstoppableswap.net/) or our [github docs](dev-docs/README.md). Join our [Matrix room](https://matrix.to/#/#unstoppableswap-core:matrix.org) to follow development more closely. diff --git a/bitcoin-wallet/Cargo.toml b/bitcoin-wallet/Cargo.toml index dd4f62af4a..1f7f35a06c 100644 --- a/bitcoin-wallet/Cargo.toml +++ b/bitcoin-wallet/Cargo.toml @@ -23,6 +23,7 @@ rust_decimal = { version = "1", features = ["serde-float"] } serde = { workspace = true } serde_json = { workspace = true } swap-env = { path = "../swap-env" } +swap-tor = { path = "../swap-tor" } swap-proptest = { path = "../swap-proptest" } swap-serde = { path = "../swap-serde" } thiserror = { workspace = true } diff --git a/bitcoin-wallet/src/wallet.rs b/bitcoin-wallet/src/wallet.rs index 3648d866b6..912de35f2d 100644 --- a/bitcoin-wallet/src/wallet.rs +++ b/bitcoin-wallet/src/wallet.rs @@ -19,10 +19,11 @@ use bitcoin::bip32::Xpriv; use bitcoin::{psbt::Psbt as PartiallySignedTransaction, Address, Amount, Transaction, Txid}; use bitcoin::{Psbt, ScriptBuf, Weight}; use derive_builder::Builder; -use electrum_pool::ElectrumBalancer; +use electrum_pool::{ElectrumBalancer, ElectrumBalancerConfig}; use moka; use rust_decimal::prelude::*; use rust_decimal::Decimal; +use std::borrow::Cow; use std::collections::BTreeMap; use std::collections::HashMap; use std::fmt::Debug; @@ -1612,7 +1613,16 @@ where impl Client { /// Create a new client with multiple electrum servers for load balancing. pub async fn new(electrum_rpc_urls: &[String], sync_interval: Duration) -> Result { - let balancer = ElectrumBalancer::new(electrum_rpc_urls.to_vec()).await?; + let balancer = ElectrumBalancer::new_with_config( + electrum_rpc_urls.to_vec(), + ElectrumBalancerConfig { + socks5: swap_tor::TOR_ENVIRONMENT + .and_then(|ste| ste.electrum_proxy()) + .map(Cow::from), + ..Default::default() + }, + ) + .await?; Ok(Self { inner: Arc::new(balancer), @@ -2582,8 +2592,11 @@ mod mempool_client { _ => bail!("mempool.space fee estimation unsupported for network"), }; - let client = reqwest::Client::builder() - .timeout(HTTP_TIMEOUT) + let mut client = reqwest::Client::builder().timeout(HTTP_TIMEOUT); + if let Some(proxy) = swap_tor::TOR_ENVIRONMENT.and_then(|ste| ste.reqwest_proxy()) { + client = client.proxy(reqwest::Proxy::all(proxy)?); + } + let client = client .build() .context("Failed to build mempool.space HTTP client")?; diff --git a/docs/components/SwapProviderTable.tsx b/docs/components/SwapProviderTable.tsx index 92f5723132..3e0fb1544e 100644 --- a/docs/components/SwapProviderTable.tsx +++ b/docs/components/SwapProviderTable.tsx @@ -7,7 +7,7 @@ export default function SwapMakerTable() { } async function getMakers() { - const response = await fetch("https://api.eigenwallet.org/api/list"); + const response = await fetch("https://api.unstoppableswap.net/api/list"); const data = await response.json(); return data; } diff --git a/electrum-pool/src/lib.rs b/electrum-pool/src/lib.rs index f24af78b4d..6a579e1ffc 100644 --- a/electrum-pool/src/lib.rs +++ b/electrum-pool/src/lib.rs @@ -1,9 +1,10 @@ use backoff::{Error as BackoffError, ExponentialBackoff}; -use bdk_electrum::electrum_client::{Client, ConfigBuilder, ElectrumApi, Error}; +use bdk_electrum::electrum_client::{Client, ConfigBuilder, ElectrumApi, Error, Socks5Config}; use bdk_electrum::BdkElectrumClient; use bitcoin::Transaction; use futures::future::join_all; use once_cell::sync::OnceCell; +use std::borrow::Cow; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, RwLock}; use std::time::Duration; @@ -544,6 +545,8 @@ pub struct ElectrumBalancerConfig { pub request_timeout: u8, /// Minimum number of retry attempts across all nodes pub min_retries: usize, + /// Address of SOCKS5 proxy in `127.0.0.1:9050` format + pub socks5: Option>, } impl Default for ElectrumBalancerConfig { @@ -551,6 +554,7 @@ impl Default for ElectrumBalancerConfig { Self { request_timeout: 15, min_retries: 10, + socks5: None, } } } @@ -577,6 +581,7 @@ impl ElectrumClientFactory> for BdkElectrumClientFacto // // Setting it to 0 causes some bugs, see: https://github.com/bitcoindevkit/rust-electrum-client/issues/186 .retry(1) + .socks5(config.socks5.as_ref().map(Socks5Config::new)) .build(); let client = Client::from_config(url, client_config).map_err(|e| { @@ -950,6 +955,7 @@ mod tests { let config = ElectrumBalancerConfig { request_timeout: 5, min_retries: 0, + socks5: None, }; let balancer = ElectrumBalancer::new_with_config_and_factory(urls, config, factory.clone()) @@ -1023,6 +1029,7 @@ mod tests { let config = ElectrumBalancerConfig { request_timeout: 5, min_retries: 1, + socks5: None, }; let balancer = ElectrumBalancer::new_with_config_and_factory(urls, config, factory.clone()) @@ -1165,6 +1172,7 @@ mod tests { let config = ElectrumBalancerConfig { request_timeout: 15, min_retries: 7, + socks5: None, }; let factory = Arc::new(MockElectrumClientFactory::new()); diff --git a/monero-harness/src/lib.rs b/monero-harness/src/lib.rs index 0a7dee5eca..4855b8f2a6 100644 --- a/monero-harness/src/lib.rs +++ b/monero-harness/src/lib.rs @@ -438,9 +438,10 @@ impl MoneroWallet { // Use Mainnet network type – regtest daemon accepts mainnet prefixes // and this avoids address-parsing errors when calling daemon RPCs. - let wallet = WalletHandle::open_or_create( + let wallet = WalletHandle::open_or_create::( wallet_path.display().to_string(), daemon, + None, monero_address::Network::Mainnet, background_sync, ) @@ -467,7 +468,11 @@ impl MoneroWallet { } /// Get address at a given account and subaddress index. - pub async fn address_at(&self, account_index: u32, address_index: u32) -> Result { + pub async fn address_at( + &self, + account_index: u32, + address_index: u32, + ) -> Result { Ok(self.wallet.address(account_index, address_index).await?) } diff --git a/monero-oxide-ext/src/lib.rs b/monero-oxide-ext/src/lib.rs index 0517cafcd0..d28b2bca85 100644 --- a/monero-oxide-ext/src/lib.rs +++ b/monero-oxide-ext/src/lib.rs @@ -41,7 +41,7 @@ impl PrivateKey { impl fmt::Display for PrivateKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", hex::encode(&self.as_bytes())) + write!(f, "{}", hex::encode(self.as_bytes())) } } @@ -122,7 +122,7 @@ pub mod serde_compressed_edwards { .ok_or_else(|| serde::de::Error::invalid_length(i, &"expected 32 bytes"))?; } Ok(PublicKey::from_slice(&bytes) - .map_err(|e| serde::de::Error::custom(e))? + .map_err(serde::de::Error::custom)? .point) } } @@ -200,7 +200,7 @@ impl From for PublicKey { impl fmt::Display for PublicKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", hex::encode(&self.as_bytes())) + write!(f, "{}", hex::encode(self.as_bytes())) } } diff --git a/monero-rpc-pool/Cargo.toml b/monero-rpc-pool/Cargo.toml index c87d4b6813..8979706a9c 100644 --- a/monero-rpc-pool/Cargo.toml +++ b/monero-rpc-pool/Cargo.toml @@ -35,6 +35,7 @@ tracing-subscriber = { workspace = true } # Async runtime crossbeam = "0.8.4" tokio = { workspace = true, features = ["full"] } +tokio-socks = "0.5" # Serialization chrono = { version = "0.4", features = ["serde"] } @@ -63,6 +64,7 @@ tor-rtcompat = { workspace = true, features = ["tokio", "rustls"] } # Monero/Project specific monero-address = { workspace = true } swap-serde = { path = "../swap-serde" } +swap-tor = { path = "../swap-tor" } # Optional dependencies (for features) cuprate-epee-encoding = { git = "https://github.com/Cuprate/cuprate.git", optional = true } diff --git a/monero-rpc-pool/src/bin/stress_test.rs b/monero-rpc-pool/src/bin/stress_test.rs index be1b5096bf..97ed0d9b25 100644 --- a/monero-rpc-pool/src/bin/stress_test.rs +++ b/monero-rpc-pool/src/bin/stress_test.rs @@ -77,9 +77,9 @@ async fn main() -> Result<(), Box> { .await .expect("Failed to bootstrap Tor client"); - Some(client) + swap_tor::TorBackend::Arti(client) } else { - None + swap_tor::TorBackend::None }; // Start the pool server diff --git a/monero-rpc-pool/src/bin/stress_test_downloader.rs b/monero-rpc-pool/src/bin/stress_test_downloader.rs index 443b3b984f..4c024b3543 100644 --- a/monero-rpc-pool/src/bin/stress_test_downloader.rs +++ b/monero-rpc-pool/src/bin/stress_test_downloader.rs @@ -133,9 +133,9 @@ async fn main() -> Result<(), Box> { .await .expect("Failed to bootstrap Tor client"); - Some(client) + swap_tor::TorBackend::Arti(client) } else { - None + swap_tor::TorBackend::None }; // Start the pool server diff --git a/monero-rpc-pool/src/config.rs b/monero-rpc-pool/src/config.rs index 77d02ce132..eb7c1144f3 100644 --- a/monero-rpc-pool/src/config.rs +++ b/monero-rpc-pool/src/config.rs @@ -1,57 +1,44 @@ use monero_address::Network; use std::path::PathBuf; +use swap_tor::TorBackend; -use crate::TorClientArc; - -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Config { pub host: String, pub port: u16, pub data_dir: PathBuf, - pub tor_client: Option, + pub tor_client: TorBackend, pub network: Network, } -impl std::fmt::Debug for Config { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Config") - .field("host", &self.host) - .field("port", &self.port) - .field("data_dir", &self.data_dir) - .field("tor_client", &self.tor_client.is_some()) - .field("network", &self.network) - .finish() - } -} - impl Config { pub fn new_with_port(host: String, port: u16, data_dir: PathBuf, network: Network) -> Self { - Self::new_with_port_and_tor_client(host, port, data_dir, None, network) + Self::new_with_port_and_tor_client(host, port, data_dir, TorBackend::None, network) } pub fn new_with_port_and_tor_client( host: String, port: u16, data_dir: PathBuf, - tor_client: impl Into>, + tor_client: TorBackend, network: Network, ) -> Self { Self { host, port, data_dir, - tor_client: tor_client.into(), + tor_client, network, } } pub fn new_random_port(data_dir: PathBuf, network: Network) -> Self { - Self::new_random_port_with_tor_client(data_dir, None, network) + Self::new_random_port_with_tor_client(data_dir, TorBackend::None, network) } pub fn new_random_port_with_tor_client( data_dir: PathBuf, - tor_client: impl Into>, + tor_client: TorBackend, network: Network, ) -> Self { Self::new_with_port_and_tor_client( diff --git a/monero-rpc-pool/src/lib.rs b/monero-rpc-pool/src/lib.rs index 395dc9b9b1..47784964d1 100644 --- a/monero-rpc-pool/src/lib.rs +++ b/monero-rpc-pool/src/lib.rs @@ -1,25 +1,21 @@ use std::sync::Arc; use anyhow::Result; -use arti_client::TorClient; use axum::{ routing::{any, get}, Router, }; use tokio::task::JoinHandle; -use tor_rtcompat::tokio::TokioRustlsRuntime; use tower_http::cors::CorsLayer; use tracing::{error, info}; -/// Type alias for the Tor client used throughout the crate -pub type TorClientArc = Arc>; - pub mod config; pub mod connection_pool; pub mod database; pub mod pool; pub mod proxy; +pub(crate) mod tor; pub mod types; use config::Config; @@ -30,7 +26,7 @@ use proxy::{proxy_handler, stats_handler}; #[derive(Clone)] pub struct AppState { pub node_pool: Arc, - pub tor_client: Option, + pub tor_client: swap_tor::TorBackend, pub connection_pool: crate::connection_pool::ConnectionPool, } diff --git a/monero-rpc-pool/src/main.rs b/monero-rpc-pool/src/main.rs index 0470fc183f..de004528ad 100644 --- a/monero-rpc-pool/src/main.rs +++ b/monero-rpc-pool/src/main.rs @@ -74,9 +74,9 @@ async fn main() -> Result<(), Box> { } }); - Some(client) + swap_tor::TorBackend::Arti(client) } else { - None + swap_tor::TorBackend::None }; let config = Config::new_with_port_and_tor_client( diff --git a/monero-rpc-pool/src/proxy.rs b/monero-rpc-pool/src/proxy.rs index ce3e2584f2..ff756231a3 100644 --- a/monero-rpc-pool/src/proxy.rs +++ b/monero-rpc-pool/src/proxy.rs @@ -9,11 +9,7 @@ use hyper_util::rt::TokioIo; use std::pin::Pin; use std::sync::Arc; use std::time::Duration; -use tokio::net::TcpStream; -use tokio::{ - io::{AsyncRead, AsyncWrite}, - time::timeout, -}; +use tokio::time::timeout; use tokio_rustls::rustls::{ client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, @@ -22,6 +18,7 @@ use tokio_rustls::rustls::{ }; use tracing::{error, info_span, Instrument}; +use crate::tor::*; use crate::AppState; /// wallet2.h has a default timeout of 3 minutes + 30 seconds. @@ -32,10 +29,6 @@ static TIMEOUT: Duration = Duration::from_secs(3 * 60 + 30).checked_div(2).unwra /// If the main node does not finish within this period, we start a hedged request. static SOFT_TIMEOUT: Duration = TIMEOUT.checked_div(2).unwrap(); -/// Trait alias for a stream that can be used with hyper -trait HyperStream: AsyncRead + AsyncWrite + Unpin + Send {} -impl HyperStream for T {} - #[derive(Debug)] struct NoCertificateVerification; @@ -137,6 +130,16 @@ pub async fn proxy_handler(State(state): State, request: Request) -> R } } +/// Check if we're using Tor for this request +/// +/// Use Tor if: +/// 1. the environment can *only* route clearnet traffic over Tor +/// 2. it's enabled, ready, and the request didn't ask to be routed over clearnet +fn use_tor_for_request(state: &AppState, request: &CloneableRequest) -> bool { + state.tor_client.masquerade_clearnet() + || (state.tor_client.ready_for_traffic() && !request.clearnet_whitelisted()) +} + /// Given a Vec of nodes, proxy the given request to multiple nodes until we get a successful response async fn proxy_to_multiple_nodes( state: &AppState, @@ -148,15 +151,7 @@ async fn proxy_to_multiple_nodes( } // Sort nodes to prioritize those with available connections - // Check if we're using Tor for this request - let use_tor = match &state.tor_client { - Some(tc) - if tc.bootstrap_status().ready_for_traffic() && !request.clearnet_whitelisted() => - { - true - } - _ => false, - }; + let use_tor = use_tor_for_request(state, &request); // Create a vector of (node, has_connection) pairs let mut nodes_with_availability = Vec::new(); @@ -321,7 +316,7 @@ async fn proxy_to_multiple_nodes( /// Wraps a stream with TLS if HTTPS is being used async fn maybe_wrap_with_tls( - stream: impl AsyncRead + AsyncWrite + Unpin + Send + 'static, + stream: impl HyperStream + 'static, scheme: &str, host: &str, ) -> Result, SingleRequestError> { @@ -461,19 +456,11 @@ async fn proxy_to_single_node( ) -> Result { use crate::connection_pool::GuardedSender; - if request.clearnet_whitelisted() { + let use_tor = use_tor_for_request(state, &request); + if !use_tor && request.clearnet_whitelisted() { tracing::trace!("Request is whitelisted, sending over clearnet"); } - let use_tor = match &state.tor_client { - Some(tc) - if tc.bootstrap_status().ready_for_traffic() && !request.clearnet_whitelisted() => - { - true - } - _ => false, - }; - let key = (node.0.clone(), node.1.clone(), node.2, use_tor); // Try to reuse an idle HTTP connection first. @@ -484,24 +471,11 @@ async fn proxy_to_single_node( let address = (node.1.as_str(), node.2); let maybe_tls_stream = timeout(TIMEOUT, async { - let no_tls_stream: Box = if use_tor { - let tor_client = state.tor_client.as_ref().ok_or_else(|| { - SingleRequestError::ConnectionError("Tor requested but client missing".into()) - })?; - - let stream = tor_client - .connect(address) - .await - .map_err(|e| SingleRequestError::ConnectionError(format!("{e:?}")))?; - - Box::new(stream) - } else { - let stream = TcpStream::connect(address) - .await - .map_err(|e| SingleRequestError::ConnectionError(format!("{e:?}")))?; - - Box::new(stream) - }; + let no_tls_stream = match use_tor { + true => state.tor_client.connect(address).await, + false => swap_tor::TorBackend::None.connect(address).await, + } + .map_err(|e| SingleRequestError::ConnectionError(format!("{:?}", e)))?; maybe_wrap_with_tls(no_tls_stream, &node.0, &node.1).await }) diff --git a/monero-rpc-pool/src/tor.rs b/monero-rpc-pool/src/tor.rs new file mode 100644 index 0000000000..ff8b351b15 --- /dev/null +++ b/monero-rpc-pool/src/tor.rs @@ -0,0 +1,89 @@ +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use swap_tor::TorBackend; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_socks::TargetAddr; + +/// Trait alias for a stream that can be used with hyper +pub trait HyperStream: AsyncRead + AsyncWrite + Unpin + Send {} +impl HyperStream for T {} + +#[allow(async_fn_in_trait)] +pub trait TorBackendRpc { + fn is_some(&self) -> bool; + fn ready_for_traffic(&self) -> bool; + fn masquerade_clearnet(&self) -> bool; + async fn connect(&self, address: (&str, u16)) -> anyhow::Result>; +} +impl TorBackendRpc for TorBackend { + fn is_some(&self) -> bool { + !matches!(self, TorBackend::None) + } + + fn ready_for_traffic(&self) -> bool { + match self { + TorBackend::Arti(arti) => arti.bootstrap_status().ready_for_traffic(), + TorBackend::Socks(..) => true, + TorBackend::None => false, + } + } + + fn masquerade_clearnet(&self) -> bool { + match self { + TorBackend::Arti(..) | TorBackend::None => false, + TorBackend::Socks(..) => true, + } + } + + async fn connect(&self, address: (&str, u16)) -> anyhow::Result> { + match self { + TorBackend::Arti(tor_client) => Ok(Box::new(tor_client.connect(address).await?)), + TorBackend::Socks(proxy) => Ok(Box::new(proxy.proxy(pair_to_socks(address)).await?)), + TorBackend::None => Ok(Box::new(tokio::net::TcpStream::connect(address).await?)), + } + } +} + +// Parse order matches tokio::net::ToSocketAddrs +fn pair_to_socks((host, port): (&'_ str, u16)) -> TargetAddr<'_> { + if let Ok(addr) = host.parse::() { + TargetAddr::Ip(SocketAddr::new(addr.into(), 10)) + } else if let Ok(addr) = host.parse::() { + TargetAddr::Ip(SocketAddr::new(addr.into(), 10)) + } else { + TargetAddr::Domain(host.into(), port) + } +} +#[cfg(test)] +mod tests { + use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; + use tokio_socks::TargetAddr; + + #[test] + fn pair_to_socks() { + assert_eq!( + [ + ("ip.tld", 10), + ("dns.ip4.tld", 11), + ("dns.ip6.tld", 12), + ( + "cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion", + 13 + ), + ("127.0.0.1", 10), + ("::1", 10), + ] + .map(super::pair_to_socks), + [ + TargetAddr::Domain("ip.tld".into(), 10), + TargetAddr::Domain("dns.ip4.tld".into(), 11), + TargetAddr::Domain("dns.ip6.tld".into(), 12), + TargetAddr::Domain( + "cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion".into(), + 13, + ), + TargetAddr::Ip(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 10)), + TargetAddr::Ip(SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 10)), + ], + ); + } +} diff --git a/monero-sys/src/lib.rs b/monero-sys/src/lib.rs index 812d4ccb23..83c3d10d50 100644 --- a/monero-sys/src/lib.rs +++ b/monero-sys/src/lib.rs @@ -233,13 +233,14 @@ impl WalletHandle { } /// Open an existing wallet or create a new one, with a random seed. - pub async fn open_or_create( + pub async fn open_or_create>( path: String, daemon: Daemon, + proxy_address: Option, network: monero_address::Network, background_sync: bool, ) -> anyhow::Result { - Self::open_or_create_with_password(path, None, daemon, network, background_sync).await + Self::open_or_create_with_password(path, None, daemon, proxy_address, network, background_sync).await } /// Common implementation used by all `open_*` helpers. @@ -309,14 +310,16 @@ impl WalletHandle { /// Opens an existing wallet or creates a new one with the specified password. /// Uses password-based encryption for the wallet file. /// If no password is provided, the wallet will be unencrypted. - pub async fn open_or_create_with_password( + pub async fn open_or_create_with_password>( path: String, password: impl Into>, daemon: Daemon, + proxy_address: Option, network: monero_address::Network, background_sync: bool, ) -> anyhow::Result { let password: Option = password.into(); + let proxy_address = proxy_address.map(Into::into); Self::open_with(path.clone(), daemon.clone(), move |manager| { manager.open_or_create_wallet( @@ -325,6 +328,7 @@ impl WalletHandle { network, background_sync, daemon.clone(), + proxy_address.as_deref(), ) }) .await @@ -333,14 +337,17 @@ impl WalletHandle { /// Opens an existing wallet or recovers it from a mnemonic seed. /// If the wallet exists at the path, it opens the existing wallet. /// Otherwise, it creates a new wallet by recovering from the provided seed. - pub async fn open_or_create_from_seed( + pub async fn open_or_create_from_seed>( path: String, mnemonic: String, network: monero_address::Network, restore_height: u64, background_sync: bool, daemon: Daemon, + proxy_address: Option, ) -> anyhow::Result { + let proxy_address = proxy_address.map(Into::into); + Self::open_or_create_from_seed_with_password( path, mnemonic, @@ -349,11 +356,12 @@ impl WalletHandle { restore_height, background_sync, daemon, + proxy_address.as_deref(), ) .await } - pub async fn open_or_create_from_seed_with_password( + pub async fn open_or_create_from_seed_with_password>( path: String, mnemonic: String, password: impl Into>, @@ -361,8 +369,10 @@ impl WalletHandle { restore_height: u64, background_sync: bool, daemon: Daemon, + proxy_address: Option, ) -> anyhow::Result { let password = password.into(); + let proxy_address = proxy_address.map(Into::into); Self::open_with(path.clone(), daemon.clone(), move |manager| { if manager.wallet_exists(&path)? { @@ -372,6 +382,7 @@ impl WalletHandle { network, background_sync, daemon.clone(), + proxy_address.as_deref(), ) } else { manager.recover_wallet( @@ -382,6 +393,7 @@ impl WalletHandle { restore_height, background_sync, daemon.clone(), + proxy_address.as_deref(), ) } }) @@ -392,7 +404,7 @@ impl WalletHandle { /// If the wallet exists at the path, it opens the existing wallet. /// Otherwise, it creates a new wallet from the provided cryptographic keys. #[allow(clippy::too_many_arguments)] - pub async fn open_or_create_from_keys( + pub async fn open_or_create_from_keys>( path: String, password: Option, network: monero_address::Network, @@ -402,7 +414,10 @@ impl WalletHandle { restore_height: u64, background_sync: bool, daemon: Daemon, + proxy_address: Option, ) -> anyhow::Result { + let proxy_address = proxy_address.map(Into::into); + Self::open_with(path.clone(), daemon.clone(), move |manager| { manager.open_or_create_wallet_from_keys( &path, @@ -414,6 +429,7 @@ impl WalletHandle { restore_height, background_sync, daemon.clone(), + proxy_address.as_deref(), ) }) .await @@ -1281,6 +1297,7 @@ impl WalletManager { network: monero_address::Network, background_sync: bool, daemon: Daemon, + proxy_address: Option<&str>, ) -> anyhow::Result { tracing::debug!(%path, "Opening or creating wallet"); @@ -1295,6 +1312,7 @@ impl WalletManager { network, background_sync, daemon, + proxy_address, Box::new(TraceListener::new(path.to_string())), ) .context(format!("Failed to open wallet `{}`", &path)); @@ -1327,7 +1345,7 @@ impl WalletManager { } let raw_wallet = RawWallet::new(wallet_pointer); - let wallet = FfiWallet::new(raw_wallet, background_sync, daemon) + let wallet = FfiWallet::new(raw_wallet, background_sync, daemon, proxy_address) .context(format!("Failed to initialize wallet `{}`", &path))?; Ok(wallet) @@ -1346,6 +1364,7 @@ impl WalletManager { restore_height: u64, background_sync: bool, daemon: Daemon, + proxy_address: Option<&str>, ) -> Result { tracing::debug!(%path, "Creating wallet from keys"); @@ -1359,6 +1378,7 @@ impl WalletManager { network, background_sync, daemon.clone(), + proxy_address, Box::new(TraceListener::new(path.to_string())), ) .context(format!("Failed to open wallet `{}`", &path)); @@ -1411,7 +1431,7 @@ impl WalletManager { let raw_wallet = RawWallet::new(wallet_pointer); tracing::debug!(path=%path, "Created wallet from keys, initializing"); - let wallet = FfiWallet::new(raw_wallet, background_sync, daemon) + let wallet = FfiWallet::new(raw_wallet, background_sync, daemon, proxy_address) .context(format!("Failed to initialize wallet `{}` from keys", &path))?; Ok(wallet) @@ -1428,6 +1448,7 @@ impl WalletManager { restore_height: u64, background_sync: bool, daemon: Daemon, + proxy_address: Option<&str>, ) -> anyhow::Result { tracing::debug!(%path, "Recovering wallet from seed"); @@ -1452,7 +1473,7 @@ impl WalletManager { .context("Failed to recover wallet from seed: FFI call failed with exception")?; let raw_wallet = RawWallet::new(wallet_pointer); - let wallet = FfiWallet::new(raw_wallet, background_sync, daemon) + let wallet = FfiWallet::new(raw_wallet, background_sync, daemon, proxy_address) .context(format!("Failed to initialize wallet `{}` from seed", &path))?; Ok(wallet) @@ -1482,6 +1503,7 @@ impl WalletManager { network_type: monero_address::Network, background_sync: bool, daemon: Daemon, + proxy_address: Option<&str>, listener: Box, ) -> anyhow::Result { tracing::debug!(%path, "Opening wallet"); @@ -1509,7 +1531,7 @@ impl WalletManager { let raw_wallet = RawWallet::new(wallet_pointer); - let wallet = FfiWallet::new(raw_wallet, background_sync, daemon) + let wallet = FfiWallet::new(raw_wallet, background_sync, daemon, proxy_address) .context("Failed to initialize re-opened wallet")?; wallet.add_listener(listener); @@ -1639,7 +1661,7 @@ impl FfiWallet { const MAIN_ACCOUNT_INDEX: u32 = 0; /// Create and initialize new wallet from a raw C++ wallet pointer. - fn new(inner: RawWallet, background_sync: bool, daemon: Daemon) -> anyhow::Result { + fn new(inner: RawWallet, background_sync: bool, daemon: Daemon, proxy_address: Option<&str>,) -> anyhow::Result { if inner.inner.is_null() { anyhow::bail!("Failed to create wallet: got null pointer"); } @@ -1659,7 +1681,7 @@ impl FfiWallet { backoff(None, None), || { wallet - .init(&daemon) + .init(&daemon, proxy_address) .context("Failed to initialize wallet") .map_err(backoff::Error::transient) }, @@ -1893,15 +1915,15 @@ impl FfiWallet { map } - /// Does not actuallyt sync the wallet, use any of the refresh methods to do that. - fn init(&mut self, daemon: &Daemon) -> anyhow::Result<()> { + /// Does not actually sync the wallet, use any of the refresh methods to do that. + fn init(&mut self, daemon: &Daemon, proxy_address: Option<&str>) -> anyhow::Result<()> { let daemon_address = format!("{}:{}", daemon.hostname, daemon.port); tracing::debug!(%daemon_address, ssl=%daemon.ssl, "Initializing wallet"); let_cxx_string!(daemon_address = daemon_address); let_cxx_string!(daemon_username = ""); let_cxx_string!(daemon_password = ""); - let_cxx_string!(proxy_address = ""); + let_cxx_string!(proxy_address = proxy_address.unwrap_or("")); let raw_wallet = &mut self.inner; diff --git a/monero-wallet/Cargo.toml b/monero-wallet/Cargo.toml index cc2efb82cc..64a05ee8a1 100644 --- a/monero-wallet/Cargo.toml +++ b/monero-wallet/Cargo.toml @@ -13,6 +13,7 @@ monero-address = { workspace = true } monero-oxide-ext = { path = "../monero-oxide-ext" } monero-sys = { path = "../monero-sys" } swap-core = { path = "../swap-core" } +swap-tor = { path = "../swap-tor" } throttle = { path = "../throttle" } uuid = { workspace = true } diff --git a/monero-wallet/src/wallets.rs b/monero-wallet/src/wallets.rs index a86bb874ff..3a1f643d35 100644 --- a/monero-wallet/src/wallets.rs +++ b/monero-wallet/src/wallets.rs @@ -66,6 +66,7 @@ impl Wallets { let main_wallet = Wallet::open_or_create( wallet_dir.join(&main_wallet_name).display().to_string(), daemon.clone(), + swap_tor::TOR_ENVIRONMENT.and_then(|ste| ste.wallet2_proxy()), network, true, ) @@ -245,6 +246,7 @@ impl Wallets { // We don't sync the swap wallet, just import the transaction false, daemon, + swap_tor::TOR_ENVIRONMENT.and_then(|ste| ste.wallet2_proxy()), ) .await .context(format!( diff --git a/src-gui/src/renderer/api.ts b/src-gui/src/renderer/api.ts index 2811bc1315..974a0923e4 100644 --- a/src-gui/src/renderer/api.ts +++ b/src-gui/src/renderer/api.ts @@ -17,7 +17,7 @@ import { setAlerts } from "store/features/alertsSlice"; import logger from "utils/logger"; import { setConversation } from "store/features/conversationsSlice"; -const PUBLIC_REGISTRY_API_BASE_URL = "https://api.eigenwallet.org"; +const PUBLIC_REGISTRY_API_BASE_URL = "https://api.unstoppableswap.net"; async function fetchAlertsViaHttp(): Promise { const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/alerts`); diff --git a/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx b/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx index f2802c42a8..e0b0207b29 100644 --- a/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx +++ b/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx @@ -15,10 +15,11 @@ import { import SystemUpdateIcon from "@mui/icons-material/SystemUpdate"; import { check, Update, DownloadEvent } from "@tauri-apps/plugin-updater"; import { useSnackbar } from "notistack"; +import { useAppSelector } from "store/hooks"; import { relaunch } from "@tauri-apps/plugin-process"; const GITHUB_RELEASES_URL = "https://github.com/eigenwallet/core/releases"; -const HOMEPAGE_URL = "https://eigenwallet.org/"; +const HOMEPAGE_URL = "https://unstoppableswap.net/"; interface DownloadProgress { contentLength: number | null; @@ -61,10 +62,11 @@ export default function UpdaterDialog() { const [downloadProgress, setDownloadProgress] = useState(null); const { enqueueSnackbar } = useSnackbar(); + const proxy = useAppSelector((s) => s.rpc.state.updaterProxy); useEffect(() => { // Check for updates when component mounts - check() + check({ proxy: proxy === null ? undefined : proxy! }) .then((updateResponse) => { console.log("updateResponse", updateResponse); setAvailableUpdate(updateResponse); diff --git a/src-gui/src/renderer/components/other/ContactInfoBox.tsx b/src-gui/src/renderer/components/other/ContactInfoBox.tsx index 600de59d21..0b56589a87 100644 --- a/src-gui/src/renderer/components/other/ContactInfoBox.tsx +++ b/src-gui/src/renderer/components/other/ContactInfoBox.tsx @@ -36,7 +36,7 @@ export default function ContactInfoBox() { - + diff --git a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx index 4672c25cc9..7aa46dea69 100644 --- a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx +++ b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx @@ -45,7 +45,7 @@ import { RefundPolicy, } from "store/features/settingsSlice"; import { Blockchain, Network } from "store/types"; -import { useAppDispatch, useNodes, useSettings } from "store/hooks"; +import { useAppDispatch, useAppSelector, useNodes, useSettings } from "store/hooks"; import ValidatedTextField from "renderer/components/other/ValidatedTextField"; import HelpIcon from "@mui/icons-material/HelpOutline"; import { ReactNode, useState } from "react"; @@ -709,19 +709,27 @@ export function TorSettings() { const torEnabled = useSettings((settings) => settings.enableTor); const handleChange = (event: React.ChangeEvent) => dispatch(setTorEnabled(event.target.checked)); - const status = (state: boolean) => (state === true ? "enabled" : "disabled"); + const torForced = useAppSelector((s) => s.rpc.state.torForcedExcuse); return ( - + ); @@ -740,7 +748,9 @@ function MoneroTorSettings() { dispatch(setEnableMoneroTor(event.target.checked)); // Hide this setting if Tor is disabled entirely - if (!torEnabled) { + // Hide this setting if it's superseded by the global Tor connection + const torForced = useAppSelector((s) => s.rpc.state.torForcedExcuse); + if (!torEnabled || torForced) { return null; } diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index f1501e122d..e06aed4b87 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -61,6 +61,7 @@ import { import { rpcSetSwapInfo, rpcSetSwapInfosLoaded, + rpcSetTorNetworkConfig, approvalRequestsReplaced, timelockChangeEventReceived, } from "store/features/rpcSlice"; @@ -95,6 +96,7 @@ const DONATION_ADDRESS_STAGENET = /// /// Get the key from: /// - https://github.com/eigenwallet/core/blob/master/utils/gpg_keys/binarybaron.asc +/// - https://unstoppableswap.net/binarybaron.asc const DONATION_ADDRESS_MAINNET_SIG = ` -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 @@ -215,6 +217,12 @@ export async function buyXmr() { } export async function initializeContext() { + store.dispatch( + rpcSetTorNetworkConfig( + await invokeNoArgs<[string, string | null]>("get_tor_network_config"), + ), + ); + const network = getNetwork(); const testnet = isTestnet(); const useTor = store.getState().settings.enableTor; diff --git a/src-gui/src/store/features/rpcSlice.ts b/src-gui/src/store/features/rpcSlice.ts index c0a893f32f..95cd52df5a 100644 --- a/src-gui/src/store/features/rpcSlice.ts +++ b/src-gui/src/store/features/rpcSlice.ts @@ -29,6 +29,8 @@ interface State { background: { [key: string]: TauriBackgroundProgress; }; + torForcedExcuse: string; + updaterProxy: string | null; } export enum ContextStatusType { @@ -53,6 +55,8 @@ const initialState: RPCSlice = { moneroRecovery: null, background: {}, approvalRequests: {}, + torForcedExcuse: "", + updaterProxy: null, }, }; @@ -127,6 +131,12 @@ export const rpcSlice = createSlice({ ) { slice.state.background[action.payload.id] = action.payload.event; }, + rpcSetTorNetworkConfig( + slice, + action: PayloadAction<[string, string | null]>, + ) { + [slice.state.torForcedExcuse, slice.state.updaterProxy] = action.payload; + }, }, }); @@ -141,6 +151,7 @@ export const { approvalEventReceived, approvalRequestsReplaced, backgroundProgressEventReceived, + rpcSetTorNetworkConfig, } = rpcSlice.actions; export default rpcSlice.reducer; diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 58e15b73b9..807dbe9ff4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -15,6 +15,7 @@ tauri-build = { version = "2.*", features = ["config-json5"] } [dependencies] swap = { path = "../swap", features = [ "tauri" ] } swap-p2p = { path = "../swap-p2p" } +swap-tor = { path = "../swap-tor" } dfx-swiss-sdk = { workspace = true } rustls = { version = "0.23.26", default-features = false, features = ["ring"] } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index df9dedc918..573e99bf58 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -78,7 +78,8 @@ macro_rules! generate_command_handlers { get_context_status, get_monero_subaddresses, create_monero_subaddress, - set_monero_subaddress_label + set_monero_subaddress_label, + get_tor_network_config, ] }; } @@ -209,6 +210,15 @@ pub async fn get_context_status(state: tauri::State<'_, State>) -> Result, +) -> Result<(String, Option<&'static str>), String> { + let tor_forced_excuse = swap_tor::TOR_ENVIRONMENT.map_or(String::new(), |ste| ste.excuse()); + let updater_proxy = swap_tor::TOR_ENVIRONMENT.and_then(|ste| ste.reqwest_proxy()); + Ok((tor_forced_excuse, updater_proxy)) +} + #[tauri::command] pub async fn resolve_approval_request( args: ResolveApprovalArgs, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 02daf3629b..eb62a752f0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -91,6 +91,13 @@ pub fn run() { builder = builder.plugin(tauri_plugin_cli::init()); } + let mut context = tauri::generate_context!(); + if let Some(proxy) = swap_tor::TOR_ENVIRONMENT.and_then(|ste| ste.reqwest_proxy()) { + for window in &mut context.config_mut().app.windows { + window.proxy_url = Some(proxy.parse().expect("URL is valid")); + } + } + builder .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()) @@ -100,7 +107,7 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .invoke_handler(generate_command_handlers!()) .setup(setup) - .build(tauri::generate_context!()) + .build(context) .expect("error while building tauri application") .run(|app, event| match event { RunEvent::Exit | RunEvent::ExitRequested { .. } => { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index df67d17818..157376108b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -23,7 +23,7 @@ } ], "security": { - "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://api.eigenwallet.org https://api.coingecko.com" + "dangerousDisableAssetCspModification": true } }, "bundle": { diff --git a/swap-asb/src/main.rs b/swap-asb/src/main.rs index 2e315cbbb6..14f42f27da 100644 --- a/swap-asb/src/main.rs +++ b/swap-asb/src/main.rs @@ -14,7 +14,6 @@ use anyhow::{bail, Context, Result}; use comfy_table::Table; -use libp2p::Swarm; use monero_sys::Daemon; use rust_decimal::prelude::FromPrimitive; use rust_decimal::Decimal; @@ -27,7 +26,7 @@ mod command; use command::{parse_args, Arguments, Command}; use swap::asb::rpc::RpcServer; use swap::asb::{cancel, punish, redeem, refund, safely_abort, EventLoop, ExchangeRate, Finality}; -use swap::common::tor::{bootstrap_tor_client, create_tor_client}; +use swap::common::tor::{create_tor_client, TorBackendSwap}; use swap::common::tracing_util::Format; use swap::common::{self, get_logs, warn_if_outdated}; use swap::database::{open_db, AccessMode}; @@ -233,9 +232,8 @@ pub async fn main() -> Result<()> { let namespace = XmrBtcNamespace::from_is_testnet(testnet); // Initialize and bootstrap Tor client - let tor_client = create_tor_client(&config.data.dir).await?; - bootstrap_tor_client(tor_client.clone(), None).await?; - let tor_client = tor_client.into(); + let tor_client = create_tor_client(&config.data.dir, true).await?; + tor_client.bootstrap(None).await?; let (mut swarm, onion_addresses) = swarm::asb( &seed, @@ -251,8 +249,8 @@ pub async fn main() -> Result<()> { config.tor.hidden_service_num_intro_points, )?; - for listen in config.network.listen.clone() { - if let Err(e) = Swarm::listen_on(&mut swarm, listen.clone()) { + for listen in &config.network.listen { + if let Err(e) = swarm.listen_on(listen.clone()) { tracing::warn!("Failed to listen on network interface {}: {}. Consider removing it from the config.", listen, e); } } diff --git a/swap-env/Cargo.toml b/swap-env/Cargo.toml index d5c3d95147..9a80ad5589 100644 --- a/swap-env/Cargo.toml +++ b/swap-env/Cargo.toml @@ -15,6 +15,7 @@ rust_decimal = { workspace = true } serde = { workspace = true } swap-fs = { path = "../swap-fs" } swap-serde = { path = "../swap-serde" } +swap-tor = { path = "../swap-tor" } thiserror = { workspace = true } time = "0.3" toml = { workspace = true } diff --git a/swap-env/src/prompt.rs b/swap-env/src/prompt.rs index f59aa8f5c6..a6a470e805 100644 --- a/swap-env/src/prompt.rs +++ b/swap-env/src/prompt.rs @@ -38,6 +38,13 @@ pub fn bitcoin_confirmation_target(default_target: u16) -> Result { /// Prompt user for listen addresses pub fn listen_addresses(default_listen_address: &Multiaddr) -> Result> { + if !swap_tor::TOR_ENVIRONMENT + .map(|ste| ste.can_listen_tcp()) + .unwrap_or(true) + { + return Ok(vec![]); + } + let listen_addresses = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter multiaddresses (comma separated) on which asb should list for peer-to-peer communications or hit return to use default") .default(default_listen_address.to_string()) @@ -131,6 +138,13 @@ pub fn monero_daemon_url() -> Result> { /// Prompt user for Tor hidden service registration pub fn tor_hidden_service() -> Result { + if !swap_tor::TOR_ENVIRONMENT + .map(|ste| ste.can_listen_onion()) + .unwrap_or(true) + { + return Ok(false); + } + print_info_box([ "Your ASB needs to be reachable from the outside world to provide quotes to takers.", "Your ASB can run a hidden service for itself. It'll be reachable at an .onion address.", diff --git a/swap-tor/Cargo.toml b/swap-tor/Cargo.toml new file mode 100644 index 0000000000..1624151454 --- /dev/null +++ b/swap-tor/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "swap-tor" +version = "3.2.0-rc.4" +authors = ["наб "] +edition = "2021" +description = "Arti/SOCKS5 Tor back-end." + +[dependencies] +anyhow = { workspace = true } +arti-client = { workspace = true, features = ["static-sqlite", "tokio", "rustls", "onion-service-service"] } +data-encoding = "2.6" +futures = { workspace = true } +libp2p = { workspace = true, features = ["tcp", "dns", "tokio"] } +once_cell = { workspace = true } + +# Tokio +tokio = { workspace = true, features = ["net"] } +tokio-socks = "0.5" +tokio-util = { workspace = true } + +tor-rtcompat = { workspace = true, features = ["tokio"] } +tracing = { workspace = true } diff --git a/swap-tor/src/lib.rs b/swap-tor/src/lib.rs new file mode 100644 index 0000000000..399bf70a3e --- /dev/null +++ b/swap-tor/src/lib.rs @@ -0,0 +1,246 @@ +use arti_client::TorClient; +use futures::future::BoxFuture; +use libp2p::core::multiaddr::Protocol; +use libp2p::core::transport::{ListenerId, TransportEvent}; +use libp2p::{Multiaddr, Transport, TransportError}; +use std::fs; +use std::net::SocketAddr; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use tokio::net::TcpStream; +use tokio_socks::tcp::Socks5Stream; +use tokio_socks::TargetAddr; +use tor_rtcompat::tokio::TokioRustlsRuntime; + +fn onion3_to_dotonion(service: &[u8; 35]) -> String { + let mut domain = data_encoding::BASE32.encode(service).to_lowercase(); + domain.push_str(".onion"); + domain +} +fn multi_to_socks(addr: &Multiaddr) -> Option> { + let mut addr = addr.iter(); + match (addr.next()?, addr.next()) { + ( + Protocol::Dns(domain) | Protocol::Dns4(domain) | Protocol::Dns6(domain), + Some(Protocol::Tcp(port)), + ) => Some(TargetAddr::Domain(domain.into_owned().into(), port)), + (Protocol::Onion3(service), _) => Some(TargetAddr::Domain( + onion3_to_dotonion(service.hash()).into(), + service.port(), + )), + (Protocol::Ip4(ip), Some(Protocol::Tcp(port))) => { + Some(TargetAddr::Ip(SocketAddr::from((ip, port)))) + } + (Protocol::Ip6(ip), Some(Protocol::Tcp(port))) => { + Some(TargetAddr::Ip(SocketAddr::from((ip, port)))) + } + _ => None, + } +} +#[cfg(test)] +mod tests { + use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; + use tokio_socks::TargetAddr; + + const MULTIS: [&str; 6] = [ + "/dns/ip.tld/tcp/10", + "/dns4/dns.ip4.tld/tcp/11", + "/dns6/dns.ip6.tld/tcp/12", + "/onion3/cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd:13", + "/ip4/127.0.0.1/tcp/10", + "/ip6/::1/tcp/10", + ]; + + #[test] + fn multi_to_socks() { + assert_eq!( + MULTIS.map(|ma| super::multi_to_socks(&ma.parse().unwrap()).unwrap()), + [ + TargetAddr::Domain("ip.tld".into(), 10), + TargetAddr::Domain("dns.ip4.tld".into(), 11), + TargetAddr::Domain("dns.ip6.tld".into(), 12), + TargetAddr::Domain( + "cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion".into(), + 13, + ), + TargetAddr::Ip(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 10)), + TargetAddr::Ip(SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 10)), + ], + ); + } +} + +pub struct Socks5Transport(SocksServerAddress); +impl Transport for Socks5Transport { + type Output = tokio_util::compat::Compat; + type Error = tokio_socks::Error; + type ListenerUpgrade = std::future::Pending>; + type Dial = BoxFuture<'static, Result>; + + fn listen_on( + &mut self, + _: ListenerId, + addr: Multiaddr, + ) -> Result<(), TransportError> { + Err(TransportError::MultiaddrNotSupported(addr)) + } + + fn remove_listener(&mut self, _: ListenerId) -> bool { + false + } + + fn dial(&mut self, addr: Multiaddr) -> Result> { + let target = multi_to_socks(&addr).ok_or(TransportError::MultiaddrNotSupported(addr))?; + let proxy = self.0; + + Ok(Box::pin(async move { + Ok(tokio_util::compat::TokioAsyncReadCompatExt::compat( + proxy.proxy(target).await?, + )) + })) + } + + fn dial_as_listener( + &mut self, + addr: Multiaddr, + ) -> Result> { + self.dial(addr) + } + + fn poll( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll> { + Poll::Pending + } + + fn address_translation(&self, _: &Multiaddr, _: &Multiaddr) -> Option { + None + } +} + +#[derive(Debug, Copy, Clone)] +pub struct SocksServerAddress(pub SocketAddr); + +impl SocksServerAddress { + pub fn transport(self) -> Socks5Transport { + tracing::debug!("Using SOCKS5 proxy at {:?}", self.0); + Socks5Transport(self) + } + + pub async fn connect(&self) -> std::io::Result { + TcpStream::connect(self.0).await + } + + pub async fn proxy(&self, target: TargetAddr<'_>) -> Result { + Socks5Stream::connect_with_socket(self.connect().await?, target) + .await + .map(Socks5Stream::into_inner) + } +} + +pub type TcpTransport = libp2p::dns::tokio::Transport; + +#[derive(Clone)] +pub enum TorBackend { + /// Private Tor client + Arti(Arc>), + /// Talking through a Tor SOCKS5 proxy + Socks(SocksServerAddress), + /// No Tor at all + None, +} + +impl std::fmt::Debug for TorBackend { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + f.write_str(match self { + TorBackend::Arti(..) => "Arti", + TorBackend::Socks(..) => "Socks", + TorBackend::None => "None", + }) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum SpecialTorEnvironment { + /// Torsocksed userland, Tor control and SOCKS5 in `$TOR_...`, well-known SOCKS5 at `127.0.0.1:9050` + /// + /// The `$TOR_...` configuration uses unix-domain sockets which we'd have to wrap ourselves + /// + /// We can't support a hypothetical `TorsocksTransport` that'd substitute + /// /onion3/cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd:13 + /// with + /// /dns/cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion/tcp/13 + /// then forward to TcpTransport, + /// because [hickory-resolver refuses to resolve `.onion` addresses](https://github.com/hickory-dns/hickory-dns/issues/3331). + Whonix, + /// Well-known SOCKS5 at `127.0.0.1:9050`, cf. `/usr/local/bin/curl` + /// + /// Userland pretends it's torsocksed but dialling actually doesn't work at all; *all* network traffic must go through SOCKS5. + Tails, +} + +pub static TOR_ENVIRONMENT: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + if fs::exists("/usr/share/whonix/marker").unwrap_or(false) { + Some(SpecialTorEnvironment::Whonix) + } else if fs::read_to_string("/etc/os-release") + .unwrap_or_default() + .contains(r#"ID="tails""#) + { + Some(SpecialTorEnvironment::Tails) + } else { + None + } + }); + +impl SpecialTorEnvironment { + pub fn backend(self) -> TorBackend { + match self { + Self::Whonix | Self::Tails => TorBackend::Socks(SocksServerAddress( + (std::net::Ipv4Addr::LOCALHOST, 9050).into(), + )), + } + } + + /// `Some("ip4:port")` or `None`/`Some("")` + pub fn wallet2_proxy(self) -> Option<&'static str> { + self.electrum_proxy() + } + + /// `Some("ip4:port")` or `None` + pub fn electrum_proxy(self) -> Option<&'static str> { + match self { + Self::Tails => Some("127.0.0.1:9050"), + _ => None, + } + } + + /// `Some("socks5://ip4:port")` or `None` + pub fn reqwest_proxy(self) -> Option<&'static str> { + match self { + Self::Tails => Some("socks5://127.0.0.1:9050"), + _ => None, + } + } + + /// `true` if listening on an address like `/ip4/0.0.0.0/tcp/9939` is possible in this environment + pub fn can_listen_tcp(self) -> bool { + match self { + Self::Whonix | Self::Tails => false, + } + } + + /// `true` if listening on an address like `/onion3/whatever` is possible in this environment + pub fn can_listen_onion(self) -> bool { + match self { + Self::Whonix | Self::Tails => false, + } + } + + /// Explain to the user why Tor is always on + pub fn excuse(self) -> String { + format!("Under {self:?}, the app always uses the global Tor connection.") + } +} diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 1c10e74922..1f818eee4a 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -70,6 +70,7 @@ swap-machine = { path = "../swap-machine" } swap-p2p = { path = "../swap-p2p" } swap-proptest = { path = "../swap-proptest" } swap-serde = { path = "../swap-serde" } +swap-tor = { path = "../swap-tor" } tauri = { version = "2.0", features = ["config-json5"], optional = true, default-features = false } time = "0.3" url = { workspace = true } diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index 2cdaaf9aed..ebcf346843 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -15,12 +15,10 @@ use swap_env::env; use swap_feed::LatestRate; pub mod transport { - use std::sync::Arc; - - use arti_client::{config::onion_service::OnionServiceConfigBuilder, TorClient}; - use libp2p::{core::transport::OptionalTransport, dns, identity, tcp, Transport}; + use crate::common::tor::TorBackendSwap; + use arti_client::config::onion_service::OnionServiceConfigBuilder; + use libp2p::{identity, Transport}; use libp2p_tor::AddressConversion; - use tor_rtcompat::tokio::TokioRustlsRuntime; use super::*; @@ -33,20 +31,22 @@ pub mod transport { /// /// If you pass in a `None` for `maybe_tor_client`, the ASB will not use Tor at all. /// - /// If you pass in a `Some(tor_client)`, the ASB will listen on an onion service and return + /// If you pass in a `Arti(tor_client)`, the ASB will listen on an onion service and return /// the onion address. If it fails to listen on the onion address, it will only use tor for /// dialing and not listening. pub fn new( identity: &identity::Keypair, - maybe_tor_client: Option>>, + maybe_tor_client: swap_tor::TorBackend, register_hidden_service: bool, num_intro_points: u8, ) -> Result { - let (maybe_tor_transport, onion_addresses) = if let Some(tor_client) = maybe_tor_client { - let mut tor_transport = - libp2p_tor::TorTransport::from_client(tor_client, AddressConversion::DnsOnly); + let mut onion_addresses = vec![]; + let transport = + maybe_tor_client.into_transport(AddressConversion::DnsOnly, |arti_tor_transport| { + if !register_hidden_service { + return; + } - let addresses = if register_hidden_service { let onion_service_config = OnionServiceConfigBuilder::default() .nickname( ASB_ONION_SERVICE_NICKNAME @@ -57,35 +57,24 @@ pub mod transport { .build() .expect("We specified a valid nickname"); - match tor_transport.add_onion_service(onion_service_config, ASB_ONION_SERVICE_PORT) + match arti_tor_transport + .add_onion_service(onion_service_config, ASB_ONION_SERVICE_PORT) { Ok(addr) => { tracing::debug!( %addr, "Setting up onion service for libp2p to listen on" ); - vec![addr] + onion_addresses.push(addr) } Err(err) => { tracing::warn!(error=%err, "Failed to listen on onion address"); - vec![] } } - } else { - vec![] - }; - - (OptionalTransport::some(tor_transport), addresses) - } else { - (OptionalTransport::none(), vec![]) - }; - - let tcp = maybe_tor_transport - .or_transport(tcp::tokio::Transport::new(tcp::Config::new().nodelay(true))); - let tcp_with_dns = dns::tokio::Transport::system(tcp)?; + })?; Ok(( - authenticate_and_multiplex(tcp_with_dns.boxed(), identity)?, + authenticate_and_multiplex(transport.boxed(), identity)?, onion_addresses, )) } diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index 368dad11fa..04abd4e889 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -3,7 +3,7 @@ pub mod tauri_bindings; use crate::cli::api::tauri_bindings::{ContextStatus, SeedChoice}; use crate::cli::command::{Bitcoin, Monero}; -use crate::common::tor::{bootstrap_tor_client, create_tor_client}; +use crate::common::tor::{create_tor_client, TorBackendSwap}; use crate::common::tracing_util::Format; use crate::database::{open_db, AccessMode}; use crate::network::rendezvous::XmrBtcNamespace; @@ -11,7 +11,6 @@ use crate::protocol::Database; use crate::seed::Seed; use crate::{common, monero}; use anyhow::{bail, Context as AnyContext, Error, Result}; -use arti_client::TorClient; use futures::future::try_join_all; use libp2p::{Multiaddr, PeerId}; use std::fmt; @@ -20,11 +19,11 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Once}; use swap_env::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet}; use swap_fs::system_data_dir; +use swap_tor::{TorBackend, TOR_ENVIRONMENT}; use tauri_bindings::{MoneroNodeConfig, TauriBackgroundProgress, TauriEmitter, TauriHandle}; use tokio::sync::{broadcast, broadcast::Sender, Mutex as TokioMutex, RwLock}; use tokio::task::JoinHandle; use tokio_util::task::AbortOnDropHandle; -use tor_rtcompat::tokio::TokioRustlsRuntime; use uuid::Uuid; use super::watcher::Watcher; @@ -242,7 +241,7 @@ mod context { pub tauri_handle: Option, pub(super) bitcoin_wallet: Arc>>>, pub monero_manager: Arc>>>, - pub(super) tor_client: Arc>>>>, + pub(super) tor_client: Arc>, #[allow(dead_code)] pub(super) monero_rpc_pool_handle: Arc>>>, pub(super) event_loop_state: Arc>>, @@ -267,7 +266,7 @@ mod context { tauri_handle, bitcoin_wallet: Arc::new(RwLock::new(None)), monero_manager: Arc::new(RwLock::new(None)), - tor_client: Arc::new(RwLock::new(None)), + tor_client: Arc::new(RwLock::new(TorBackend::None)), monero_rpc_pool_handle: Arc::new(RwLock::new(None)), event_loop_state: Arc::new(RwLock::new(None)), } @@ -319,12 +318,12 @@ mod context { } /// Get the Tor client, returning an error if not initialized - pub async fn try_get_tor_client(&self) -> Result>> { - self.tor_client - .read() - .await - .clone() - .context("Tor client not initialized") + pub async fn try_get_tor_client(&self) -> Result { + match self.tor_client.read().await.clone() { + TorBackend::None => None, + ret => Some(ret), + } + .context("Tor client not initialized") } /// Get the event loop handle, returning an error if not initialized @@ -357,7 +356,7 @@ mod context { swap_lock: SwapLock::new().into(), tasks: PendingTaskList::default().into(), tauri_handle: None, - tor_client: Arc::new(RwLock::new(None)), + tor_client: Arc::new(RwLock::new(TorBackend::None)), monero_rpc_pool_handle: Arc::new(RwLock::new(None)), event_loop_state: Arc::new(RwLock::new(None)), } @@ -583,28 +582,18 @@ mod builder { let future_unbootstrapped_tor_client_rpc_pool = { let tauri_handle = self.tauri_handle.clone(); async move { - let unbootstrapped_tor_client = if self.tor { - match create_tor_client(&base_data_dir).await.inspect_err(|err| { - tracing::warn!(%err, "Failed to create Tor client. We will continue without Tor"); - }) { - Ok(client) => Some(client), - Err(_) => None, - } - } else { - tracing::warn!("Internal Tor client not enabled, skipping initialization"); - None - }; + let unbootstrapped_tor_client = create_tor_client(&base_data_dir, self.tor).await + .inspect(|client| if matches!(client, TorBackend::None) { tracing::warn!("Internal Tor client not enabled, skipping initialization"); }) + .inspect_err(|err| tracing::warn!(%err, "Failed to create Tor client. We will continue without Tor")) + .unwrap_or(TorBackend::None); // Start Monero RPC pool server let (server_info, status_receiver, pool_handle) = monero_rpc_pool::start_server_with_random_port( monero_rpc_pool::config::Config::new_random_port_with_tor_client( base_data_dir.join("monero-rpc-pool"), - if self.enable_monero_tor { - unbootstrapped_tor_client.clone() - } else { - None - }, + unbootstrapped_tor_client + .clone_for_monero_rpc(self.enable_monero_tor), match self.is_testnet { true => monero::Network::Stagenet, false => monero::Network::Mainnet, @@ -615,18 +604,15 @@ mod builder { // Bootstrap Tor client in background let bootstrap_tor_client_task = AbortOnDropHandle::new(tokio::spawn({ - let unbootstrapped_tor_client = unbootstrapped_tor_client.clone(); + let tor_client = unbootstrapped_tor_client.clone(); let tauri_handle = tauri_handle.clone(); async move { - if let Some(tor_client) = unbootstrapped_tor_client { - bootstrap_tor_client(tor_client.clone(), tauri_handle.clone()) + let _ = tor_client.bootstrap(tauri_handle) .await .inspect_err(|err| { tracing::warn!(%err, "Failed to bootstrap Tor client. It will remain unbootstrapped"); - }) - .ok(); - } + }); } })); @@ -918,12 +904,14 @@ mod wallet { data_dir: &PathBuf, env_config: EnvConfig, daemon: &monero_sys::Daemon, + proxy_address: Option<&str>, ) -> Result { let wallet_path = data_dir.join("swap-tool-blockchain-monitoring-wallet"); let wallet = monero::Wallet::open_or_create( wallet_path.display().to_string(), daemon.clone(), + proxy_address, env_config.monero_network, true, ) @@ -968,6 +956,7 @@ mod wallet { database: &monero_sys::Database, ) -> Result<(monero_sys::WalletHandle, Seed), Error> { let eigenwallet_wallets_dir = eigenwallet_data_dir.join("wallets"); + let proxy_address = TOR_ENVIRONMENT.and_then(|ste| ste.wallet2_proxy()); let wallet = match seed_choice { Some(mut seed_choice) => { @@ -1013,6 +1002,7 @@ mod wallet { Some(password) }, daemon.clone(), + proxy_address, env_config.monero_network, true, ) @@ -1040,6 +1030,7 @@ mod wallet { restore_height.into(), true, daemon.clone(), + proxy_address, ) .await .context("Failed to create wallet from provided seed")? @@ -1123,6 +1114,7 @@ mod wallet { wallet_path.clone(), password, daemon.clone(), + proxy_address, env_config.monero_network, true, ) @@ -1135,6 +1127,7 @@ mod wallet { legacy_data_dir, env_config, daemon, + proxy_address, ) .await?; let seed = Seed::from_file_or_generate(legacy_data_dir) @@ -1161,9 +1154,13 @@ mod wallet { // If we don't have a seed choice, we use the legacy wallet // This is used for the CLI to monitor the blockchain None => { - let wallet = - request_and_open_monero_wallet_legacy(legacy_data_dir, env_config, daemon) - .await?; + let wallet = request_and_open_monero_wallet_legacy( + legacy_data_dir, + env_config, + daemon, + proxy_address, + ) + .await?; let seed = Seed::from_file_or_generate(legacy_data_dir) .await .context("Failed to extract seed from wallet")?; diff --git a/swap/src/cli/api/request.rs b/swap/src/cli/api/request.rs index befde35328..387acaac00 100644 --- a/swap/src/cli/api/request.rs +++ b/swap/src/cli/api/request.rs @@ -1748,10 +1748,14 @@ impl CheckMoneroNodeArgs { }; static CLIENT: LazyLock = LazyLock::new(|| { - reqwest::Client::builder() + let mut client = reqwest::Client::builder() // This function is called very frequently, so we set the timeout to be short .timeout(Duration::from_secs(5)) - .https_only(false) + .https_only(false); + if let Some(proxy) = swap_tor::TOR_ENVIRONMENT.and_then(|ste| ste.reqwest_proxy()) { + client = client.proxy(reqwest::Proxy::all(proxy).expect("proxy to be valid")); +} + client .build() .expect("reqwest client to work") }); diff --git a/swap/src/cli/transport.rs b/swap/src/cli/transport.rs index 7965c8443e..0c6cba1e87 100644 --- a/swap/src/cli/transport.rs +++ b/swap/src/cli/transport.rs @@ -1,15 +1,10 @@ -use std::sync::Arc; - +use crate::common::tor::TorBackendSwap; use crate::network::transport::authenticate_and_multiplex; use anyhow::Result; -use arti_client::TorClient; use libp2p::core::muxing::StreamMuxerBox; -use libp2p::core::transport::{Boxed, OptionalTransport}; -use libp2p::dns; -use libp2p::tcp; +use libp2p::core::transport::Boxed; use libp2p::{identity, PeerId, Transport}; -use libp2p_tor::{AddressConversion, TorTransport}; -use tor_rtcompat::tokio::TokioRustlsRuntime; +use libp2p_tor::AddressConversion; /// Creates the libp2p transport for the swap CLI. /// @@ -21,20 +16,8 @@ use tor_rtcompat::tokio::TokioRustlsRuntime; /// TCP transport. pub fn new( identity: &identity::Keypair, - maybe_tor_client: Option>>, + maybe_tor_client: swap_tor::TorBackend, ) -> Result> { - let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true)); - let tcp_with_dns = dns::tokio::Transport::system(tcp)?; - - let maybe_tor_transport: OptionalTransport = match maybe_tor_client { - Some(client) => OptionalTransport::some(libp2p_tor::TorTransport::from_client( - client, - AddressConversion::IpAndDns, - )), - None => OptionalTransport::none(), - }; - - let transport = maybe_tor_transport.or_transport(tcp_with_dns).boxed(); - - authenticate_and_multiplex(transport, identity) + let transport = maybe_tor_client.into_transport(AddressConversion::IpAndDns, |_| {})?; + authenticate_and_multiplex(transport.boxed(), identity) } diff --git a/swap/src/common/mod.rs b/swap/src/common/mod.rs index 2f5ebc9d45..24636ccf32 100644 --- a/swap/src/common/mod.rs +++ b/swap/src/common/mod.rs @@ -14,7 +14,11 @@ const LATEST_RELEASE_URL: &str = "https://github.com/eigenwallet/core/releases/l /// Check the latest release from GitHub and warn if we are not on the latest version. pub async fn warn_if_outdated(current_version: &str) -> anyhow::Result<()> { // Visit the Github releases page and check which url we are redirected to - let response = reqwest::get(LATEST_RELEASE_URL).await?; + let mut client = reqwest::Client::builder(); + if let Some(proxy) = swap_tor::TOR_ENVIRONMENT.and_then(|ste| ste.reqwest_proxy()) { + client = client.proxy(reqwest::Proxy::all(proxy)?); + } + let response = client.build()?.get(LATEST_RELEASE_URL).send().await?; let download_url = response.url(); let segments = download_url diff --git a/swap/src/common/tor.rs b/swap/src/common/tor.rs index 14db42362e..83b24a658b 100644 --- a/swap/src/common/tor.rs +++ b/swap/src/common/tor.rs @@ -1,20 +1,104 @@ -use std::sync::Arc; -use std::{path::Path, time::Duration}; +use std::{path::Path, sync::Arc, time::Duration}; use crate::cli::api::tauri_bindings::{ TauriBackgroundProgress, TauriEmitter, TauriHandle, TorBootstrapStatus, }; use arti_client::{config::TorClientConfigBuilder, status::BootstrapStatus, Error, TorClient}; use futures::StreamExt; +use libp2p::core::transport::{OptionalTransport, OrTransport}; +use libp2p::Transport; +use libp2p_tor::{AddressConversion, TorTransport}; +use swap_tor::*; use tor_rtcompat::tokio::TokioRustlsRuntime; -static TOR_CONNECT_TIMEOUT: Duration = Duration::from_secs(30); -static TOR_RESOLVE_TIMEOUT: Duration = Duration::from_secs(20); +/// Creates an unbootstrapped Tor client or connects to well-known Tor daemon, depending on configuration. +/// +/// 1. if on a system with special Tor requirements (Whonix, Tails): call the daemon appropriately +/// 2. if the caller requests (user enables) `tor`: prepare an Arti client +/// 3. `None` +pub async fn create_tor_client(data_dir: &Path, tor: bool) -> Result { + Ok(if let Some(ste) = *TOR_ENVIRONMENT { + tracing::info!("On {ste:?}, not starting Tor"); + ste.backend() + } else if tor { + TorBackend::Arti(Arc::new(create_arti_tor_client(data_dir).await?)) + } else { + TorBackend::None + }) +} + +#[allow(async_fn_in_trait)] +pub trait TorBackendSwap { + async fn bootstrap(&self, tauri_handle: Option) -> anyhow::Result<()>; + fn clone_for_monero_rpc(&self, enable_monero_tor: bool) -> TorBackend; + fn into_transport( + self, + arti_address_conversion: AddressConversion, + arti_transport_hook: impl FnOnce(&mut TorTransport), + ) -> std::io::Result; +} +type IntoTransportT = OrTransport< + OrTransport, OptionalTransport>, + TcpTransport, +>; +impl TorBackendSwap for TorBackend { + async fn bootstrap(&self, tauri_handle: Option) -> anyhow::Result<()> { + match self { + TorBackend::Arti(arti) => bootstrap_arti_tor_client(arti, tauri_handle).await?, + TorBackend::Socks(addr) => { + addr.connect().await?; // validate the remote is actually listening + } + TorBackend::None => {} + } + Ok(()) + } + + /// Obey `enable_monero_tor` if it's meaningful on the current system. + fn clone_for_monero_rpc(&self, enable_monero_tor: bool) -> TorBackend { + match self { + TorBackend::Arti(..) if enable_monero_tor => self.clone(), + TorBackend::Arti(..) => TorBackend::None, + TorBackend::Socks(..) | TorBackend::None => self.clone(), + } + } -/// Creates an unbootstrapped Tor client -pub async fn create_tor_client( - data_dir: &Path, -) -> Result>, Error> { + fn into_transport( + self, + arti_address_conversion: AddressConversion, + arti_transport_hook: impl FnOnce(&mut TorTransport), + ) -> std::io::Result { + fn plain_transport() -> std::io::Result { + let tcp = libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::new().nodelay(true)); + libp2p::dns::tokio::Transport::system(tcp) + } + let tcp_with_dns = plain_transport()?; + + let tor = match self { + TorBackend::Arti(tor_client) => { + let mut tor_transport = + TorTransport::from_client(tor_client, arti_address_conversion); + arti_transport_hook(&mut tor_transport); + OrTransport::new( + OptionalTransport::some(tor_transport), + OptionalTransport::none(), + ) + } + TorBackend::Socks(universal_config) => OrTransport::new( + OptionalTransport::none(), + OptionalTransport::some(universal_config.transport()), + ), + TorBackend::None => { + OrTransport::new(OptionalTransport::none(), OptionalTransport::none()) + } + }; + Ok(tor.or_transport(tcp_with_dns)) + } +} + +const TOR_CONNECT_TIMEOUT: Duration = Duration::from_secs(30); +const TOR_RESOLVE_TIMEOUT: Duration = Duration::from_secs(20); + +async fn create_arti_tor_client(data_dir: &Path) -> Result, Error> { // We store the Tor state in the data directory let data_dir = data_dir.join("tor"); let state_dir = data_dir.join("state"); @@ -48,17 +132,15 @@ pub async fn create_tor_client( tracing::debug!("Creating unbootstrapped Tor client"); - let tor_client = TorClient::with_runtime(runtime) + TorClient::with_runtime(runtime) .config(config) .create_unbootstrapped_async() - .await?; - - Ok(Arc::new(tor_client)) + .await } /// Bootstraps an existing Tor client -pub async fn bootstrap_tor_client( - tor_client: Arc>, +async fn bootstrap_arti_tor_client( + tor_client: &TorClient, tauri_handle: Option, ) -> Result<(), Error> { let mut bootstrap_events = tor_client.bootstrap_events(); diff --git a/swap/src/network/swarm.rs b/swap/src/network/swarm.rs index 71fd79b947..90ddd554fe 100644 --- a/swap/src/network/swarm.rs +++ b/swap/src/network/swarm.rs @@ -3,17 +3,14 @@ use crate::network::rendezvous::XmrBtcNamespace; use crate::seed::Seed; use crate::{asb, cli}; use anyhow::Result; -use arti_client::TorClient; use libp2p::swarm::NetworkBehaviour; use libp2p::{identity, Multiaddr, Swarm}; use libp2p::{PeerId, SwarmBuilder}; use std::fmt::Debug; -use std::sync::Arc; use std::time::Duration; use swap_core::bitcoin; use swap_env::env; use swap_p2p::libp2p_ext::MultiAddrExt; -use tor_rtcompat::tokio::TokioRustlsRuntime; // We keep connections open for 15 minutes const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60 * 15); @@ -28,7 +25,7 @@ pub fn asb( env_config: env::Config, namespace: XmrBtcNamespace, rendezvous_addrs: &[Multiaddr], - maybe_tor_client: Option>>, + maybe_tor_client: swap_tor::TorBackend, register_hidden_service: bool, num_intro_points: u8, ) -> Result<(Swarm>, Vec)> @@ -81,7 +78,7 @@ where pub async fn cli( identity: identity::Keypair, - maybe_tor_client: Option>>, + maybe_tor_client: swap_tor::TorBackend, behaviour: T, ) -> Result> where