From 30214faae581f511652c63b586c9a3f38b5cad8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 14 Jan 2026 20:21:13 +0100 Subject: [PATCH 01/21] Revert "security(tauri): enable Content Security Policy (#851)" This reverts commit 8bbdb488c2da47ff9e8a71f25035f75dcc9f36ed. --- README.md | 2 +- docs/components/SwapProviderTable.tsx | 2 +- src-gui/src/renderer/api.ts | 2 +- src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx | 2 +- src-gui/src/renderer/components/other/ContactInfoBox.tsx | 2 +- src-gui/src/renderer/rpc.ts | 1 + src-tauri/tauri.conf.json | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) 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/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/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..05f6a7d53e 100644 --- a/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx +++ b/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx @@ -18,7 +18,7 @@ import { useSnackbar } from "notistack"; 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; 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/rpc.ts b/src-gui/src/renderer/rpc.ts index f1501e122d..44993d9db5 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -95,6 +95,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 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": { From 537e7936651683224f193f8582da119278431eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Sun, 19 Oct 2025 19:04:07 +0200 Subject: [PATCH 02/21] refactor(swap/api): reduce open-coded Result::ok --- swap/src/cli/api.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index 368dad11fa..54e61f6141 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -584,12 +584,9 @@ mod builder { 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| { + 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, - } + }).ok() } else { tracing::warn!("Internal Tor client not enabled, skipping initialization"); None From 27d54b325f96cd94f7d40ecb4fc6a42b92dffea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Sun, 19 Oct 2025 19:25:46 +0200 Subject: [PATCH 03/21] refactor(asb): don't needlessly clone config.network.listen, spell swarm.listen_on() normally --- swap-asb/src/main.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/swap-asb/src/main.rs b/swap-asb/src/main.rs index 2e315cbbb6..adbd55eeb7 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; @@ -251,8 +250,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); } } From afac39d5ca7210b4240180773e5ed4a7e92109c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Sun, 19 Oct 2025 20:49:58 +0200 Subject: [PATCH 04/21] refactor(asb/network): lift onion_addresses collection --- swap/src/asb/network.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index 2cdaaf9aed..78d867cd6f 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -42,11 +42,12 @@ pub mod transport { 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 onion_addresses = vec![]; + let maybe_tor_transport = if let Some(tor_client) = maybe_tor_client { let mut tor_transport = libp2p_tor::TorTransport::from_client(tor_client, AddressConversion::DnsOnly); - let addresses = if register_hidden_service { + if register_hidden_service { let onion_service_config = OnionServiceConfigBuilder::default() .nickname( ASB_ONION_SERVICE_NICKNAME @@ -64,20 +65,17 @@ pub mod transport { %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) + OptionalTransport::some(tor_transport) } else { - (OptionalTransport::none(), vec![]) + OptionalTransport::none() }; let tcp = maybe_tor_transport From c7943470433c2a5de04f37f2dd1cb1de5fc92777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Tue, 21 Oct 2025 18:01:00 +0200 Subject: [PATCH 05/21] refactor(swap/tor): reduce TOR_*_TIMEOUT to const --- swap/src/common/tor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/swap/src/common/tor.rs b/swap/src/common/tor.rs index 14db42362e..1da7753abe 100644 --- a/swap/src/common/tor.rs +++ b/swap/src/common/tor.rs @@ -8,8 +8,8 @@ use arti_client::{config::TorClientConfigBuilder, status::BootstrapStatus, Error use futures::StreamExt; use tor_rtcompat::tokio::TokioRustlsRuntime; -static TOR_CONNECT_TIMEOUT: Duration = Duration::from_secs(30); -static TOR_RESOLVE_TIMEOUT: Duration = Duration::from_secs(20); +const TOR_CONNECT_TIMEOUT: Duration = Duration::from_secs(30); +const TOR_RESOLVE_TIMEOUT: Duration = Duration::from_secs(20); /// Creates an unbootstrapped Tor client pub async fn create_tor_client( From c6d99c6b1637650b82068b89133f176743ea7cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Thu, 23 Oct 2025 23:19:07 +0200 Subject: [PATCH 06/21] refactor(swap/cli): discard Result with let _ = instead of .ok() --- swap/src/cli/api.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index 54e61f6141..5f3e7158c0 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -617,12 +617,11 @@ mod builder { async move { if let Some(tor_client) = unbootstrapped_tor_client { - bootstrap_tor_client(tor_client.clone(), tauri_handle.clone()) + let _ = bootstrap_tor_client(tor_client.clone(), tauri_handle.clone()) .await .inspect_err(|err| { tracing::warn!(%err, "Failed to bootstrap Tor client. It will remain unbootstrapped"); - }) - .ok(); + }); } } })); From 347e1f1e7bcd2c5742265e84c609d8d20aaa5f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 22 Oct 2025 05:06:15 +0200 Subject: [PATCH 07/21] fix(swap/network): remove DNS leak if using Tor asb::network::transport::new() would use DNS(Tor or TCP), which will resolve DNS queries over plaintext first, before calling them over Tor Cf. cli::transport::new() which correctly does Tor or DNS(TCP) Fix the former to do the latter, delegating domain resolution over Tor as well --- swap/src/asb/network.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index 78d867cd6f..21ce67830d 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -78,12 +78,14 @@ pub mod transport { OptionalTransport::none() }; - let tcp = maybe_tor_transport - .or_transport(tcp::tokio::Transport::new(tcp::Config::new().nodelay(true))); + let tcp = 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( + maybe_tor_transport.or_transport(tcp_with_dns).boxed(), + identity, + )?, onion_addresses, )) } From 3340016cdf70e424e253a0c08951d5cd85cd0799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Fri, 24 Oct 2025 19:22:53 +0200 Subject: [PATCH 08/21] refactor(swap): deduplicate upgrading maybe_tor_client to TorTransport --- swap/src/asb/network.rs | 25 +++++++++++++------------ swap/src/cli/transport.rs | 14 +++++--------- swap/src/common/tor.rs | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index 21ce67830d..eefaf2f6af 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -17,8 +17,9 @@ use swap_feed::LatestRate; pub mod transport { use std::sync::Arc; + use crate::common::tor::tor_client_to_transport; use arti_client::{config::onion_service::OnionServiceConfigBuilder, TorClient}; - use libp2p::{core::transport::OptionalTransport, dns, identity, tcp, Transport}; + use libp2p::{dns, identity, tcp, Transport}; use libp2p_tor::AddressConversion; use tor_rtcompat::tokio::TokioRustlsRuntime; @@ -43,11 +44,14 @@ pub mod transport { num_intro_points: u8, ) -> Result { let mut onion_addresses = vec![]; - let maybe_tor_transport = if let Some(tor_client) = maybe_tor_client { - let mut tor_transport = - libp2p_tor::TorTransport::from_client(tor_client, AddressConversion::DnsOnly); + let maybe_tor_transport = tor_client_to_transport( + maybe_tor_client, + AddressConversion::DnsOnly, + |arti_tor_transport| { + if !register_hidden_service { + return; + } - if register_hidden_service { let onion_service_config = OnionServiceConfigBuilder::default() .nickname( ASB_ONION_SERVICE_NICKNAME @@ -58,7 +62,8 @@ 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!( @@ -71,12 +76,8 @@ pub mod transport { tracing::warn!(error=%err, "Failed to listen on onion address"); } } - } - - OptionalTransport::some(tor_transport) - } else { - OptionalTransport::none() - }; + }, + ); let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true)); let tcp_with_dns = dns::tokio::Transport::system(tcp)?; diff --git a/swap/src/cli/transport.rs b/swap/src/cli/transport.rs index 7965c8443e..5981766135 100644 --- a/swap/src/cli/transport.rs +++ b/swap/src/cli/transport.rs @@ -1,14 +1,15 @@ use std::sync::Arc; +use crate::common::tor::tor_client_to_transport; 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::core::transport::Boxed; use libp2p::dns; use libp2p::tcp; use libp2p::{identity, PeerId, Transport}; -use libp2p_tor::{AddressConversion, TorTransport}; +use libp2p_tor::AddressConversion; use tor_rtcompat::tokio::TokioRustlsRuntime; /// Creates the libp2p transport for the swap CLI. @@ -26,13 +27,8 @@ pub fn new( 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 maybe_tor_transport = + tor_client_to_transport(maybe_tor_client, AddressConversion::IpAndDns, |_| {}); let transport = maybe_tor_transport.or_transport(tcp_with_dns).boxed(); diff --git a/swap/src/common/tor.rs b/swap/src/common/tor.rs index 1da7753abe..e94db24286 100644 --- a/swap/src/common/tor.rs +++ b/swap/src/common/tor.rs @@ -6,8 +6,26 @@ use crate::cli::api::tauri_bindings::{ }; use arti_client::{config::TorClientConfigBuilder, status::BootstrapStatus, Error, TorClient}; use futures::StreamExt; +use libp2p::core::transport::OptionalTransport; +use libp2p_tor::{AddressConversion, TorTransport}; use tor_rtcompat::tokio::TokioRustlsRuntime; +pub fn tor_client_to_transport( + tor_client: Option>>, + arti_address_conversion: AddressConversion, + arti_transport_hook: impl FnOnce(&mut TorTransport), +) -> OptionalTransport { + let tor = match tor_client { + Some(tor_client) => { + let mut tor_transport = TorTransport::from_client(tor_client, arti_address_conversion); + arti_transport_hook(&mut tor_transport); + OptionalTransport::some(tor_transport) + } + None => OptionalTransport::none(), + }; + tor +} + const TOR_CONNECT_TIMEOUT: Duration = Duration::from_secs(30); const TOR_RESOLVE_TIMEOUT: Duration = Duration::from_secs(20); From 259b40ae370a661c88ccf753b742add34e793305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 22 Oct 2025 05:29:12 +0200 Subject: [PATCH 09/21] refactor(swap): deduplicate upgrading TorBackend to final transport --- swap/src/asb/network.rs | 14 ++++---------- swap/src/cli/transport.rs | 13 ++----------- swap/src/common/tor.rs | 19 ++++++++++++++----- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index eefaf2f6af..0e376ced8d 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -19,7 +19,7 @@ pub mod transport { use crate::common::tor::tor_client_to_transport; use arti_client::{config::onion_service::OnionServiceConfigBuilder, TorClient}; - use libp2p::{dns, identity, tcp, Transport}; + use libp2p::{identity, Transport}; use libp2p_tor::AddressConversion; use tor_rtcompat::tokio::TokioRustlsRuntime; @@ -44,7 +44,7 @@ pub mod transport { num_intro_points: u8, ) -> Result { let mut onion_addresses = vec![]; - let maybe_tor_transport = tor_client_to_transport( + let transport = tor_client_to_transport( maybe_tor_client, AddressConversion::DnsOnly, |arti_tor_transport| { @@ -77,16 +77,10 @@ pub mod transport { } } }, - ); - - let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true)); - let tcp_with_dns = dns::tokio::Transport::system(tcp)?; + )?; Ok(( - authenticate_and_multiplex( - maybe_tor_transport.or_transport(tcp_with_dns).boxed(), - identity, - )?, + authenticate_and_multiplex(transport.boxed(), identity)?, onion_addresses, )) } diff --git a/swap/src/cli/transport.rs b/swap/src/cli/transport.rs index 5981766135..6aaf4eb2d8 100644 --- a/swap/src/cli/transport.rs +++ b/swap/src/cli/transport.rs @@ -6,8 +6,6 @@ use anyhow::Result; use arti_client::TorClient; use libp2p::core::muxing::StreamMuxerBox; use libp2p::core::transport::Boxed; -use libp2p::dns; -use libp2p::tcp; use libp2p::{identity, PeerId, Transport}; use libp2p_tor::AddressConversion; use tor_rtcompat::tokio::TokioRustlsRuntime; @@ -24,13 +22,6 @@ pub fn new( identity: &identity::Keypair, maybe_tor_client: Option>>, ) -> 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 = - tor_client_to_transport(maybe_tor_client, AddressConversion::IpAndDns, |_| {}); - - let transport = maybe_tor_transport.or_transport(tcp_with_dns).boxed(); - - authenticate_and_multiplex(transport, identity) + let transport = tor_client_to_transport(maybe_tor_client, AddressConversion::IpAndDns, |_| {})?; + authenticate_and_multiplex(transport.boxed(), identity) } diff --git a/swap/src/common/tor.rs b/swap/src/common/tor.rs index e94db24286..091d499df0 100644 --- a/swap/src/common/tor.rs +++ b/swap/src/common/tor.rs @@ -6,16 +6,25 @@ use crate::cli::api::tauri_bindings::{ }; use arti_client::{config::TorClientConfigBuilder, status::BootstrapStatus, Error, TorClient}; use futures::StreamExt; -use libp2p::core::transport::OptionalTransport; +use libp2p::core::transport::{OptionalTransport, OrTransport}; +use libp2p::Transport; use libp2p_tor::{AddressConversion, TorTransport}; use tor_rtcompat::tokio::TokioRustlsRuntime; +type TcpTransport = libp2p::dns::tokio::Transport; +type IntoTransportT = OrTransport, TcpTransport>; pub fn tor_client_to_transport( - tor_client: Option>>, + maybe_tor_client: Option>>, arti_address_conversion: AddressConversion, arti_transport_hook: impl FnOnce(&mut TorTransport), -) -> OptionalTransport { - let tor = match tor_client { +) -> 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 maybe_tor_client { Some(tor_client) => { let mut tor_transport = TorTransport::from_client(tor_client, arti_address_conversion); arti_transport_hook(&mut tor_transport); @@ -23,7 +32,7 @@ pub fn tor_client_to_transport( } None => OptionalTransport::none(), }; - tor + Ok(tor.or_transport(tcp_with_dns)) } const TOR_CONNECT_TIMEOUT: Duration = Duration::from_secs(30); From f7a7c6bf31f3f42947e7b79be274a05f686b5680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Fri, 24 Oct 2025 20:15:15 +0200 Subject: [PATCH 10/21] Add swap-tor crate to supersede swap and monero-rpc-pool open-coding a unified TorBackend --- Cargo.toml | 1 + swap-tor/Cargo.toml | 20 ++++++++++++++++++++ swap-tor/src/lib.rs | 24 ++++++++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 swap-tor/Cargo.toml create mode 100644 swap-tor/src/lib.rs 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/swap-tor/Cargo.toml b/swap-tor/Cargo.toml new file mode 100644 index 0000000000..d99eae13ae --- /dev/null +++ b/swap-tor/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "swap-tor" +version = "3.2.0-rc.4" +authors = ["наб "] +edition = "2021" +description = "Arti Tor back-end." + +[dependencies] +anyhow = { workspace = true } +arti-client = { workspace = true, features = ["static-sqlite", "tokio", "rustls", "onion-service-service"] } +data-encoding = "2.6" +libp2p = { workspace = true, features = ["tcp", "dns", "tokio"] } + +# 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..3ee63e27eb --- /dev/null +++ b/swap-tor/src/lib.rs @@ -0,0 +1,24 @@ +use arti_client::TorClient; +use libp2p::core::multiaddr::Protocol; +use libp2p::core::transport::{ListenerId, TransportEvent}; +use std::sync::Arc; +use tor_rtcompat::tokio::TokioRustlsRuntime; + +pub type TcpTransport = libp2p::dns::tokio::Transport; + +#[derive(Clone)] +pub enum TorBackend { + /// Private Tor client + Arti(Arc>), + /// 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::None => "None", + }) + } +} From 520689b53f20137733525738f9f6d6435c5bfcf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Fri, 24 Oct 2025 20:17:13 +0200 Subject: [PATCH 11/21] Perfuse TorBackend through swap --- Cargo.lock | 28 ++++++++++++ swap-asb/src/main.rs | 7 ++- swap/Cargo.toml | 1 + swap/src/asb/network.rs | 20 +++------ swap/src/cli/api.rs | 50 +++++++++------------ swap/src/cli/transport.rs | 10 ++--- swap/src/common/tor.rs | 93 +++++++++++++++++++++++++++------------ swap/src/network/swarm.rs | 7 +-- 8 files changed, 131 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b5199bd38..863791b77d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10636,6 +10636,7 @@ dependencies = [ "swap-p2p", "swap-proptest", "swap-serde", + "swap-tor", "tauri", "tempfile", "testcontainers", @@ -10906,6 +10907,21 @@ dependencies = [ "url", ] +[[package]] +name = "swap-tor" +version = "3.2.0-rc.4" +dependencies = [ + "anyhow", + "arti-client", + "data-encoding", + "libp2p", + "tokio", + "tokio-socks", + "tokio-util", + "tor-rtcompat", + "tracing", +] + [[package]] name = "swift-rs" version = "1.0.7" @@ -11782,6 +11798,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" diff --git a/swap-asb/src/main.rs b/swap-asb/src/main.rs index adbd55eeb7..14f42f27da 100644 --- a/swap-asb/src/main.rs +++ b/swap-asb/src/main.rs @@ -26,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}; @@ -232,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, 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 0e376ced8d..ebcf346843 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -15,13 +15,10 @@ use swap_env::env; use swap_feed::LatestRate; pub mod transport { - use std::sync::Arc; - - use crate::common::tor::tor_client_to_transport; - use arti_client::{config::onion_service::OnionServiceConfigBuilder, TorClient}; + 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::*; @@ -34,20 +31,18 @@ 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 mut onion_addresses = vec![]; - let transport = tor_client_to_transport( - maybe_tor_client, - AddressConversion::DnsOnly, - |arti_tor_transport| { + let transport = + maybe_tor_client.into_transport(AddressConversion::DnsOnly, |arti_tor_transport| { if !register_hidden_service { return; } @@ -76,8 +71,7 @@ pub mod transport { tracing::warn!(error=%err, "Failed to listen on onion address"); } } - }, - )?; + })?; Ok(( authenticate_and_multiplex(transport.boxed(), identity)?, diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index 5f3e7158c0..f6a4a3dfd8 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; 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,24 +582,21 @@ 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 { - create_tor_client(&base_data_dir).await.inspect_err(|err| { - tracing::warn!(%err, "Failed to create Tor client. We will continue without Tor"); - }).ok() - } 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 + match unbootstrapped_tor_client + .clone_for_monero_rpc(self.enable_monero_tor) + { + TorBackend::Arti(arti) => Some(arti), + TorBackend::None => None, }, match self.is_testnet { true => monero::Network::Stagenet, @@ -612,17 +608,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 { - let _ = 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"); }); - } } })); diff --git a/swap/src/cli/transport.rs b/swap/src/cli/transport.rs index 6aaf4eb2d8..0c6cba1e87 100644 --- a/swap/src/cli/transport.rs +++ b/swap/src/cli/transport.rs @@ -1,14 +1,10 @@ -use std::sync::Arc; - -use crate::common::tor::tor_client_to_transport; +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; use libp2p::{identity, PeerId, Transport}; use libp2p_tor::AddressConversion; -use tor_rtcompat::tokio::TokioRustlsRuntime; /// Creates the libp2p transport for the swap CLI. /// @@ -20,8 +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 transport = tor_client_to_transport(maybe_tor_client, AddressConversion::IpAndDns, |_| {})?; + let transport = maybe_tor_client.into_transport(AddressConversion::IpAndDns, |_| {})?; authenticate_and_multiplex(transport.boxed(), identity) } diff --git a/swap/src/common/tor.rs b/swap/src/common/tor.rs index 091d499df0..e9551e7c41 100644 --- a/swap/src/common/tor.rs +++ b/swap/src/common/tor.rs @@ -9,39 +9,78 @@ 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; -type TcpTransport = libp2p::dns::tokio::Transport; +/// Creates an unbootstrapped Tor client or connects to well-known Tor daemon, depending on configuration. +/// +/// 1. if the caller requests (user enables) `tor`: prepare an Arti client +/// 2. `None` +pub async fn create_tor_client(data_dir: &Path, tor: bool) -> Result { + Ok(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, TcpTransport>; -pub fn tor_client_to_transport( - maybe_tor_client: Option>>, - 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) +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::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::None => self.clone(), + } } - let tcp_with_dns = plain_transport()?; - let tor = match maybe_tor_client { - Some(tor_client) => { - let mut tor_transport = TorTransport::from_client(tor_client, arti_address_conversion); - arti_transport_hook(&mut tor_transport); - OptionalTransport::some(tor_transport) + 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) } - None => OptionalTransport::none(), - }; - Ok(tor.or_transport(tcp_with_dns)) + 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); + OptionalTransport::some(tor_transport) + } + TorBackend::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); -/// Creates an unbootstrapped Tor client -pub async fn create_tor_client( - data_dir: &Path, -) -> Result>, Error> { +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"); @@ -75,17 +114,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 From 6bed8cdb72cd7e6b7710f96aed1fb8b21d7d5171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Fri, 24 Oct 2025 20:19:29 +0200 Subject: [PATCH 12/21] Perfuse TorBackend through monero-rpc-pool --- Cargo.lock | 1 + monero-harness/src/lib.rs | 6 ++- monero-rpc-pool/Cargo.toml | 1 + monero-rpc-pool/src/bin/stress_test.rs | 4 +- .../src/bin/stress_test_downloader.rs | 4 +- monero-rpc-pool/src/config.rs | 29 +++------- monero-rpc-pool/src/lib.rs | 8 +-- monero-rpc-pool/src/main.rs | 4 +- monero-rpc-pool/src/proxy.rs | 54 ++++--------------- monero-rpc-pool/src/tor.rs | 32 +++++++++++ swap/src/cli/api.rs | 8 +-- 11 files changed, 67 insertions(+), 84 deletions(-) create mode 100644 monero-rpc-pool/src/tor.rs diff --git a/Cargo.lock b/Cargo.lock index 863791b77d..162f92e149 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6487,6 +6487,7 @@ dependencies = [ "serde_json", "sqlx", "swap-serde", + "swap-tor", "tokio", "tokio-rustls 0.26.4", "tor-rtcompat", diff --git a/monero-harness/src/lib.rs b/monero-harness/src/lib.rs index 0a7dee5eca..6e5166b4ca 100644 --- a/monero-harness/src/lib.rs +++ b/monero-harness/src/lib.rs @@ -467,7 +467,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-rpc-pool/Cargo.toml b/monero-rpc-pool/Cargo.toml index c87d4b6813..c6fcacd525 100644 --- a/monero-rpc-pool/Cargo.toml +++ b/monero-rpc-pool/Cargo.toml @@ -63,6 +63,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..43631cf954 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; @@ -149,14 +142,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 = state.tor_client.ready_for_traffic() && !request.clearnet_whitelisted(); // Create a vector of (node, has_connection) pairs let mut nodes_with_availability = Vec::new(); @@ -321,7 +307,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> { @@ -465,14 +451,7 @@ async fn proxy_to_single_node( 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 use_tor = state.tor_client.ready_for_traffic() && !request.clearnet_whitelisted(); let key = (node.0.clone(), node.1.clone(), node.2, use_tor); @@ -484,24 +463,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..8f7cd6aa67 --- /dev/null +++ b/monero-rpc-pool/src/tor.rs @@ -0,0 +1,32 @@ +use swap_tor::TorBackend; +use tokio::io::{AsyncRead, AsyncWrite}; + +/// 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; + 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::None => false, + } + } + + async fn connect(&self, address: (&str, u16)) -> anyhow::Result> { + match self { + TorBackend::Arti(tor_client) => Ok(Box::new(tor_client.connect(address).await?)), + TorBackend::None => Ok(Box::new(tokio::net::TcpStream::connect(address).await?)), + } + } +} diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index f6a4a3dfd8..48fb72589c 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -592,12 +592,8 @@ mod builder { 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"), - match unbootstrapped_tor_client - .clone_for_monero_rpc(self.enable_monero_tor) - { - TorBackend::Arti(arti) => Some(arti), - TorBackend::None => None, - }, + unbootstrapped_tor_client + .clone_for_monero_rpc(self.enable_monero_tor), match self.is_testnet { true => monero::Network::Stagenet, false => monero::Network::Mainnet, From 9543f28064cafe65bf20423d58786260220f912f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Fri, 24 Oct 2025 20:30:16 +0200 Subject: [PATCH 13/21] Add TorBackend::Socks to connect to a Tor daemon over a TCP SOCKS5 proxy --- Cargo.lock | 2 + monero-rpc-pool/Cargo.toml | 1 + monero-rpc-pool/src/tor.rs | 49 +++++++++++++ swap-tor/Cargo.toml | 3 +- swap-tor/src/lib.rs | 139 +++++++++++++++++++++++++++++++++++++ swap/src/common/tor.rs | 26 +++++-- 6 files changed, 213 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 162f92e149..99458a40d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6490,6 +6490,7 @@ dependencies = [ "swap-tor", "tokio", "tokio-rustls 0.26.4", + "tokio-socks", "tor-rtcompat", "tower-http", "tracing", @@ -10915,6 +10916,7 @@ dependencies = [ "anyhow", "arti-client", "data-encoding", + "futures", "libp2p", "tokio", "tokio-socks", diff --git a/monero-rpc-pool/Cargo.toml b/monero-rpc-pool/Cargo.toml index c6fcacd525..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"] } diff --git a/monero-rpc-pool/src/tor.rs b/monero-rpc-pool/src/tor.rs index 8f7cd6aa67..657e04112b 100644 --- a/monero-rpc-pool/src/tor.rs +++ b/monero-rpc-pool/src/tor.rs @@ -1,5 +1,7 @@ +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 {} @@ -19,6 +21,7 @@ impl TorBackendRpc for TorBackend { fn ready_for_traffic(&self) -> bool { match self { TorBackend::Arti(arti) => arti.bootstrap_status().ready_for_traffic(), + TorBackend::Socks(..) => true, TorBackend::None => false, } } @@ -26,7 +29,53 @@ impl TorBackendRpc for TorBackend { 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/swap-tor/Cargo.toml b/swap-tor/Cargo.toml index d99eae13ae..f5ec816bba 100644 --- a/swap-tor/Cargo.toml +++ b/swap-tor/Cargo.toml @@ -3,12 +3,13 @@ name = "swap-tor" version = "3.2.0-rc.4" authors = ["наб "] edition = "2021" -description = "Arti Tor back-end." +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"] } # Tokio diff --git a/swap-tor/src/lib.rs b/swap-tor/src/lib.rs index 3ee63e27eb..34899b5f7e 100644 --- a/swap-tor/src/lib.rs +++ b/swap-tor/src/lib.rs @@ -1,15 +1,153 @@ 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, } @@ -18,6 +156,7 @@ 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", }) } diff --git a/swap/src/common/tor.rs b/swap/src/common/tor.rs index e9551e7c41..072d91b54d 100644 --- a/swap/src/common/tor.rs +++ b/swap/src/common/tor.rs @@ -1,5 +1,4 @@ -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, @@ -34,11 +33,17 @@ pub trait TorBackendSwap { arti_transport_hook: impl FnOnce(&mut TorTransport), ) -> std::io::Result; } -type IntoTransportT = OrTransport, TcpTransport>; +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(()) @@ -49,7 +54,7 @@ impl TorBackendSwap for TorBackend { match self { TorBackend::Arti(..) if enable_monero_tor => self.clone(), TorBackend::Arti(..) => TorBackend::None, - TorBackend::None => self.clone(), + TorBackend::Socks(..) | TorBackend::None => self.clone(), } } @@ -69,9 +74,18 @@ impl TorBackendSwap for TorBackend { let mut tor_transport = TorTransport::from_client(tor_client, arti_address_conversion); arti_transport_hook(&mut tor_transport); - OptionalTransport::some(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()) } - TorBackend::None => OptionalTransport::none(), }; Ok(tor.or_transport(tcp_with_dns)) } From a89b5a8d235b90c3478069e2b33de3e2098b5feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Fri, 24 Oct 2025 21:14:30 +0200 Subject: [PATCH 14/21] Detect Whonix and Tails, connect to their Tors specially --- swap-tor/Cargo.toml | 1 + swap-tor/src/lib.rs | 43 ++++++++++++++++++++++++++++++++++++++++++ swap/src/common/tor.rs | 10 +++++++--- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/swap-tor/Cargo.toml b/swap-tor/Cargo.toml index f5ec816bba..1624151454 100644 --- a/swap-tor/Cargo.toml +++ b/swap-tor/Cargo.toml @@ -11,6 +11,7 @@ arti-client = { workspace = true, features = ["static-sqlite", "tokio", "rustls" data-encoding = "2.6" futures = { workspace = true } libp2p = { workspace = true, features = ["tcp", "dns", "tokio"] } +once_cell = { workspace = true } # Tokio tokio = { workspace = true, features = ["net"] } diff --git a/swap-tor/src/lib.rs b/swap-tor/src/lib.rs index 34899b5f7e..bb6c4c26ab 100644 --- a/swap-tor/src/lib.rs +++ b/swap-tor/src/lib.rs @@ -161,3 +161,46 @@ impl std::fmt::Debug for TorBackend { }) } } + +#[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(), + )), + } + } +} diff --git a/swap/src/common/tor.rs b/swap/src/common/tor.rs index 072d91b54d..83b24a658b 100644 --- a/swap/src/common/tor.rs +++ b/swap/src/common/tor.rs @@ -13,10 +13,14 @@ use tor_rtcompat::tokio::TokioRustlsRuntime; /// Creates an unbootstrapped Tor client or connects to well-known Tor daemon, depending on configuration. /// -/// 1. if the caller requests (user enables) `tor`: prepare an Arti client -/// 2. `None` +/// 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 tor { + 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 From 95596503bc27f1f1904cc1fce8e354f02e216c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Fri, 24 Oct 2025 21:41:10 +0200 Subject: [PATCH 15/21] Inform the user that (and why) Tor is forced-on (GUI). Don't ask the user about listening on TCP/Onion if the environment doesn't support it (TUI questionnaire) --- Cargo.lock | 3 +++ .../components/pages/help/SettingsBox.tsx | 19 ++++++++++++++----- src-gui/src/renderer/rpc.ts | 4 ++++ src-tauri/Cargo.toml | 1 + src-tauri/src/commands.rs | 10 +++++++++- swap-env/Cargo.toml | 1 + swap-env/src/prompt.rs | 14 ++++++++++++++ swap-tor/src/lib.rs | 19 +++++++++++++++++++ 8 files changed, 65 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99458a40d5..b147c45de1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10771,6 +10771,7 @@ dependencies = [ "serde", "swap-fs", "swap-serde", + "swap-tor", "thiserror 1.0.69", "time", "toml 0.9.8", @@ -10918,6 +10919,7 @@ dependencies = [ "data-encoding", "futures", "libp2p", + "once_cell", "tokio", "tokio-socks", "tokio-util", @@ -13442,6 +13444,7 @@ dependencies = [ "serde_json", "swap", "swap-p2p", + "swap-tor", "tauri", "tauri-build", "tauri-plugin-cli", diff --git a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx index 4672c25cc9..6b82caa7b9 100644 --- a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx +++ b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx @@ -63,7 +63,7 @@ import { getNetwork } from "store/config"; import { currencySymbol } from "utils/formatUtils"; import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox"; import { isValidMultiAddressWithPeerId } from "utils/parseUtils"; -import { getNodeStatus } from "renderer/rpc"; +import { getNodeStatus, getTorForcedExcuse } from "renderer/rpc"; import { setStatus } from "store/features/nodesSlice"; import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField"; import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField"; @@ -704,24 +704,32 @@ function NodeTable({ ); } +const torForced = await getTorForcedExcuse(); export function TorSettings() { const dispatch = useAppDispatch(); const torEnabled = useSettings((settings) => settings.enableTor); const handleChange = (event: React.ChangeEvent) => dispatch(setTorEnabled(event.target.checked)); - const status = (state: boolean) => (state === true ? "enabled" : "disabled"); return ( - + ); @@ -740,7 +748,8 @@ 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 + if (!torEnabled || torForced) { return null; } diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index 44993d9db5..c49f3cf8cf 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -408,6 +408,10 @@ export async function checkContextStatus(): Promise { return await invokeNoArgs("get_context_status"); } +export async function getTorForcedExcuse(): Promise { + return await invokeNoArgs("get_tor_forced_excuse"); +} + export async function getLogsOfSwap( swapId: string, redact: boolean, 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..88dfedf1fa 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_forced_excuse, ] }; } @@ -209,6 +210,13 @@ pub async fn get_context_status(state: tauri::State<'_, State>) -> Result) -> Result { + Ok(swap_tor::TOR_ENVIRONMENT + .map(|ste| ste.excuse()) + .unwrap_or(String::new())) +} + #[tauri::command] pub async fn resolve_approval_request( args: ResolveApprovalArgs, 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/src/lib.rs b/swap-tor/src/lib.rs index bb6c4c26ab..0e48d56e16 100644 --- a/swap-tor/src/lib.rs +++ b/swap-tor/src/lib.rs @@ -203,4 +203,23 @@ impl SpecialTorEnvironment { )), } } + + /// `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.") + } } From 99a8b62f0d23d0c38699b590ca63dcc6e0326f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Fri, 24 Oct 2025 21:54:21 +0200 Subject: [PATCH 16/21] fix(monero-rpc-pool): only try to bypass unforced Tor Bypassing Tor on TorBackend::Socks breaks everything, because /all/ traffic needs to go through the proxy (normal connect() is broken on Tails) --- monero-rpc-pool/src/proxy.rs | 18 +++++++++++++----- monero-rpc-pool/src/tor.rs | 8 ++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/monero-rpc-pool/src/proxy.rs b/monero-rpc-pool/src/proxy.rs index 43631cf954..ff756231a3 100644 --- a/monero-rpc-pool/src/proxy.rs +++ b/monero-rpc-pool/src/proxy.rs @@ -130,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, @@ -141,8 +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 = state.tor_client.ready_for_traffic() && !request.clearnet_whitelisted(); + let use_tor = use_tor_for_request(state, &request); // Create a vector of (node, has_connection) pairs let mut nodes_with_availability = Vec::new(); @@ -447,12 +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 = state.tor_client.ready_for_traffic() && !request.clearnet_whitelisted(); - let key = (node.0.clone(), node.1.clone(), node.2, use_tor); // Try to reuse an idle HTTP connection first. diff --git a/monero-rpc-pool/src/tor.rs b/monero-rpc-pool/src/tor.rs index 657e04112b..ff8b351b15 100644 --- a/monero-rpc-pool/src/tor.rs +++ b/monero-rpc-pool/src/tor.rs @@ -11,6 +11,7 @@ impl HyperStream for T {} 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 { @@ -26,6 +27,13 @@ impl TorBackendRpc for TorBackend { } } + 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?)), From b2a14c21f48db55b420cbc25b5f1c439a3adaeec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Sun, 26 Oct 2025 05:41:23 +0100 Subject: [PATCH 17/21] Distribute required-on-Tails SOCKS5 proxy everywhere a TCP request is made --- Cargo.lock | 2 + bitcoin-wallet/Cargo.toml | 1 + bitcoin-wallet/src/wallet.rs | 21 ++++++-- electrum-pool/src/lib.rs | 10 +++- monero-harness/src/lib.rs | 3 +- monero-sys/src/lib.rs | 52 +++++++++++++------ monero-wallet/Cargo.toml | 1 + monero-wallet/src/wallets.rs | 2 + .../modal/updater/UpdaterDialog.tsx | 11 +++- src-gui/src/renderer/rpc.ts | 4 ++ src-tauri/src/commands.rs | 6 +++ swap-tor/src/lib.rs | 21 ++++++++ swap/src/cli/api.rs | 19 +++++-- swap/src/cli/api/request.rs | 8 ++- swap/src/common/mod.rs | 6 ++- 15 files changed, 137 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b147c45de1..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", @@ -6593,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", 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/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 6e5166b4ca..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, ) 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/components/modal/updater/UpdaterDialog.tsx b/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx index 05f6a7d53e..ffc67900a8 100644 --- a/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx +++ b/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx @@ -13,8 +13,14 @@ import { Link, } from "@mui/material"; import SystemUpdateIcon from "@mui/icons-material/SystemUpdate"; -import { check, Update, DownloadEvent } from "@tauri-apps/plugin-updater"; +import { + check, + CheckOptions, + Update, + DownloadEvent, +} from "@tauri-apps/plugin-updater"; import { useSnackbar } from "notistack"; +import { getUpdaterProxy } from "renderer/rpc"; import { relaunch } from "@tauri-apps/plugin-process"; const GITHUB_RELEASES_URL = "https://github.com/eigenwallet/core/releases"; @@ -56,6 +62,7 @@ function LinearProgressWithLabel( ); } +const proxy = await getUpdaterProxy(); export default function UpdaterDialog() { const [availableUpdate, setAvailableUpdate] = useState(null); const [downloadProgress, setDownloadProgress] = @@ -64,7 +71,7 @@ export default function UpdaterDialog() { 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/rpc.ts b/src-gui/src/renderer/rpc.ts index c49f3cf8cf..e3dbf9f2fd 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -412,6 +412,10 @@ export async function getTorForcedExcuse(): Promise { return await invokeNoArgs("get_tor_forced_excuse"); } +export async function getUpdaterProxy(): Promise { + return await invokeNoArgs("get_updater_proxy"); +} + export async function getLogsOfSwap( swapId: string, redact: boolean, diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 88dfedf1fa..f26c8fb981 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -80,6 +80,7 @@ macro_rules! generate_command_handlers { create_monero_subaddress, set_monero_subaddress_label, get_tor_forced_excuse, + get_updater_proxy, ] }; } @@ -217,6 +218,11 @@ pub async fn get_tor_forced_excuse(_: tauri::State<'_, State>) -> Result) -> Result, String> { + Ok(swap_tor::TOR_ENVIRONMENT.and_then(|ste| ste.reqwest_proxy())) +} + #[tauri::command] pub async fn resolve_approval_request( args: ResolveApprovalArgs, diff --git a/swap-tor/src/lib.rs b/swap-tor/src/lib.rs index 0e48d56e16..399bf70a3e 100644 --- a/swap-tor/src/lib.rs +++ b/swap-tor/src/lib.rs @@ -204,6 +204,27 @@ impl SpecialTorEnvironment { } } + /// `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 { diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index 48fb72589c..04abd4e889 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -19,7 +19,7 @@ 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; +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; @@ -904,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, ) @@ -954,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) => { @@ -999,6 +1002,7 @@ mod wallet { Some(password) }, daemon.clone(), + proxy_address, env_config.monero_network, true, ) @@ -1026,6 +1030,7 @@ mod wallet { restore_height.into(), true, daemon.clone(), + proxy_address, ) .await .context("Failed to create wallet from provided seed")? @@ -1109,6 +1114,7 @@ mod wallet { wallet_path.clone(), password, daemon.clone(), + proxy_address, env_config.monero_network, true, ) @@ -1121,6 +1127,7 @@ mod wallet { legacy_data_dir, env_config, daemon, + proxy_address, ) .await?; let seed = Seed::from_file_or_generate(legacy_data_dir) @@ -1147,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/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 From 107b113befb578939af5d260e858c99a527c89a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Sun, 26 Oct 2025 20:50:53 +0100 Subject: [PATCH 18/21] Forward required proxy to tauri as well --- src-tauri/src/lib.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 { .. } => { From 620df828234c160b6c83a60631f3e3683356c99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Sun, 2 Nov 2025 20:43:28 +0100 Subject: [PATCH 19/21] CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) 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 From 436543501a38fd468925cf679902cc1a6bc81561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Mon, 3 Nov 2025 02:09:12 +0100 Subject: [PATCH 20/21] refactor: keep updaterProxy and torForcedExcuse in redux RPC store --- .../components/modal/updater/UpdaterDialog.tsx | 11 +++-------- .../components/pages/help/SettingsBox.tsx | 7 ++++--- src-gui/src/renderer/rpc.ts | 15 +++++++-------- src-gui/src/store/features/rpcSlice.ts | 11 +++++++++++ src-tauri/src/commands.rs | 18 +++++++----------- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx b/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx index ffc67900a8..e0b0207b29 100644 --- a/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx +++ b/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx @@ -13,14 +13,9 @@ import { Link, } from "@mui/material"; import SystemUpdateIcon from "@mui/icons-material/SystemUpdate"; -import { - check, - CheckOptions, - Update, - DownloadEvent, -} from "@tauri-apps/plugin-updater"; +import { check, Update, DownloadEvent } from "@tauri-apps/plugin-updater"; import { useSnackbar } from "notistack"; -import { getUpdaterProxy } from "renderer/rpc"; +import { useAppSelector } from "store/hooks"; import { relaunch } from "@tauri-apps/plugin-process"; const GITHUB_RELEASES_URL = "https://github.com/eigenwallet/core/releases"; @@ -62,12 +57,12 @@ function LinearProgressWithLabel( ); } -const proxy = await getUpdaterProxy(); export default function UpdaterDialog() { const [availableUpdate, setAvailableUpdate] = useState(null); const [downloadProgress, setDownloadProgress] = useState(null); const { enqueueSnackbar } = useSnackbar(); + const proxy = useAppSelector((s) => s.rpc.state.updaterProxy); useEffect(() => { // Check for updates when component mounts diff --git a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx index 6b82caa7b9..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"; @@ -63,7 +63,7 @@ import { getNetwork } from "store/config"; import { currencySymbol } from "utils/formatUtils"; import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox"; import { isValidMultiAddressWithPeerId } from "utils/parseUtils"; -import { getNodeStatus, getTorForcedExcuse } from "renderer/rpc"; +import { getNodeStatus } from "renderer/rpc"; import { setStatus } from "store/features/nodesSlice"; import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField"; import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField"; @@ -704,12 +704,12 @@ function NodeTable({ ); } -const torForced = await getTorForcedExcuse(); export function TorSettings() { const dispatch = useAppDispatch(); const torEnabled = useSettings((settings) => settings.enableTor); const handleChange = (event: React.ChangeEvent) => dispatch(setTorEnabled(event.target.checked)); + const torForced = useAppSelector((s) => s.rpc.state.torForcedExcuse); return ( @@ -749,6 +749,7 @@ function MoneroTorSettings() { // Hide this setting if Tor is disabled entirely // 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 e3dbf9f2fd..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"; @@ -216,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; @@ -408,14 +415,6 @@ export async function checkContextStatus(): Promise { return await invokeNoArgs("get_context_status"); } -export async function getTorForcedExcuse(): Promise { - return await invokeNoArgs("get_tor_forced_excuse"); -} - -export async function getUpdaterProxy(): Promise { - return await invokeNoArgs("get_updater_proxy"); -} - export async function getLogsOfSwap( swapId: string, redact: boolean, 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/src/commands.rs b/src-tauri/src/commands.rs index f26c8fb981..573e99bf58 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -79,8 +79,7 @@ macro_rules! generate_command_handlers { get_monero_subaddresses, create_monero_subaddress, set_monero_subaddress_label, - get_tor_forced_excuse, - get_updater_proxy, + get_tor_network_config, ] }; } @@ -212,15 +211,12 @@ pub async fn get_context_status(state: tauri::State<'_, State>) -> Result) -> Result { - Ok(swap_tor::TOR_ENVIRONMENT - .map(|ste| ste.excuse()) - .unwrap_or(String::new())) -} - -#[tauri::command] -pub async fn get_updater_proxy(_: tauri::State<'_, State>) -> Result, String> { - Ok(swap_tor::TOR_ENVIRONMENT.and_then(|ste| ste.reqwest_proxy())) +pub async fn get_tor_network_config( + _: tauri::State<'_, State>, +) -> 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] From 3d610be1518cbfe00859859950c5cfb13ad27c47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 14 Jan 2026 16:49:32 +0100 Subject: [PATCH 21/21] clippy --- monero-oxide-ext/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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())) } }