Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5f6a446
refactor(swap/api): reduce open-coded Result::ok
nabijaczleweli Oct 19, 2025
9c2fb37
refactor(asb): don't needlessly clone config.network.listen, spell sw…
nabijaczleweli Oct 19, 2025
40ce4c3
refactor(asb/network): lift onion_addresses collection
nabijaczleweli Oct 19, 2025
e74a93b
refactor(swap/tor): reduce TOR_*_TIMEOUT to const
nabijaczleweli Oct 21, 2025
f646ca8
refactor(swap/cli): discard Result with let _ = instead of .ok()
nabijaczleweli Oct 23, 2025
d0dca23
fix(swap/network): remove DNS leak if using Tor
nabijaczleweli Oct 22, 2025
12498ee
refactor(swap): deduplicate upgrading maybe_tor_client to TorTransport
nabijaczleweli Oct 24, 2025
e339c13
refactor(swap): deduplicate upgrading TorBackend to final transport
nabijaczleweli Oct 22, 2025
0601914
Add swap-tor crate to supersede swap and monero-rpc-pool open-coding …
nabijaczleweli Oct 24, 2025
7bc0a49
Perfuse TorBackend through swap
nabijaczleweli Oct 24, 2025
36cefcc
Perfuse TorBackend through monero-rpc-pool
nabijaczleweli Oct 24, 2025
bb5d524
Add TorBackend::Socks to connect to a Tor daemon over a TCP SOCKS5 proxy
nabijaczleweli Oct 24, 2025
e6149cb
Detect Whonix and Tails, connect to their Tors specially
nabijaczleweli Oct 24, 2025
a09a95f
Inform the user that (and why) Tor is forced-on (GUI). Don't ask the …
nabijaczleweli Oct 24, 2025
3df691b
fix(monero-rpc-pool): only try to bypass unforced Tor
nabijaczleweli Oct 24, 2025
626e041
Distribute required-on-Tails SOCKS5 proxy everywhere a TCP request is…
nabijaczleweli Oct 26, 2025
2a2661c
Forward required proxy to tauri as well
nabijaczleweli Oct 26, 2025
0922bd5
CHANGELOG
nabijaczleweli Nov 2, 2025
4265a5d
refactor: keep updaterProxy and torForcedExcuse in redux RPC store
nabijaczleweli Nov 3, 2025
90df02c
s: bb5d5247
nabijaczleweli Nov 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ members = [
"swap-orchestrator",
"swap-p2p",
"swap-serde",
"swap-tor",
"throttle",
]

Expand Down
10 changes: 9 additions & 1 deletion electrum-pool/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -546,13 +547,16 @@ 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<Cow<'static, str>>,
}

impl Default for ElectrumBalancerConfig {
fn default() -> Self {
Self {
request_timeout: 15,
min_retries: 15,
socks5: None,
}
}
}
Expand All @@ -573,6 +577,7 @@ impl ElectrumClientFactory<BdkElectrumClient<Client>> for BdkElectrumClientFacto
) -> Result<Arc<BdkElectrumClient<Client>>, Error> {
let client_config = ConfigBuilder::new()
.timeout(Some(config.request_timeout))
.socks5(config.socks5.as_ref().map(Socks5Config::new))
.retry(0)
.build();

Expand Down Expand Up @@ -947,6 +952,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())
Expand Down Expand Up @@ -1020,6 +1026,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())
Expand Down Expand Up @@ -1162,6 +1169,7 @@ mod tests {
let config = ElectrumBalancerConfig {
request_timeout: 15,
min_retries: 7,
socks5: None,
};

let factory = Arc::new(MockElectrumClientFactory::new());
Expand Down
2 changes: 2 additions & 0 deletions monero-rpc-pool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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"] }
Expand Down Expand Up @@ -69,6 +70,7 @@ tor-rtcompat = { workspace = true, features = ["tokio", "rustls"] }
monero = { workspace = true }
monero-rpc = { path = "../monero-rpc" }
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 }
Expand Down
4 changes: 2 additions & 2 deletions monero-rpc-pool/src/bin/stress_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await
.expect("Failed to bootstrap Tor client");

Some(client)
swap_tor::TorBackend::Arti(client)
} else {
None
swap_tor::TorBackend::None
};

// Start the pool server
Expand Down
4 changes: 2 additions & 2 deletions monero-rpc-pool/src/bin/stress_test_downloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await
.expect("Failed to bootstrap Tor client");

Some(client)
swap_tor::TorBackend::Arti(client)
} else {
None
swap_tor::TorBackend::None
};

// Start the pool server
Expand Down
29 changes: 8 additions & 21 deletions monero-rpc-pool/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,44 @@
use monero::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<TorClientArc>,
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<Option<TorClientArc>>,
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<Option<TorClientArc>>,
tor_client: TorBackend,
network: Network,
) -> Self {
Self::new_with_port_and_tor_client(
Expand Down
8 changes: 2 additions & 6 deletions monero-rpc-pool/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<TorClient<TokioRustlsRuntime>>;

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;
Expand All @@ -30,7 +26,7 @@ use proxy::{proxy_handler, stats_handler};
#[derive(Clone)]
pub struct AppState {
pub node_pool: Arc<NodePool>,
pub tor_client: Option<TorClientArc>,
pub tor_client: swap_tor::TorBackend,
pub connection_pool: crate::connection_pool::ConnectionPool,
}

Expand Down
4 changes: 2 additions & 2 deletions monero-rpc-pool/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
});

Some(client)
swap_tor::TorBackend::Arti(client)
} else {
None
swap_tor::TorBackend::None
};

let config = Config::new_with_port_and_tor_client(
Expand Down
68 changes: 21 additions & 47 deletions monero-rpc-pool/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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.
Expand All @@ -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<T: AsyncRead + AsyncWrite + Unpin + Send> HyperStream for T {}

#[derive(Debug)]
struct NoCertificateVerification;

Expand Down Expand Up @@ -137,6 +130,16 @@ pub async fn proxy_handler(State(state): State<AppState>, 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,
Expand All @@ -148,15 +151,7 @@ async fn proxy_to_multiple_nodes(
}

// Sort nodes to prioritize those with available connections
// Check if we're using Tor for this request
let use_tor = match &state.tor_client {
Some(tc)
if tc.bootstrap_status().ready_for_traffic() && !request.clearnet_whitelisted() =>
{
true
}
_ => false,
};
let use_tor = use_tor_for_request(state, &request);

// Create a vector of (node, has_connection) pairs
let mut nodes_with_availability = Vec::new();
Expand Down Expand Up @@ -321,7 +316,7 @@ async fn proxy_to_multiple_nodes(

/// Wraps a stream with TLS if HTTPS is being used
async fn maybe_wrap_with_tls(
stream: impl AsyncRead + AsyncWrite + Unpin + Send + 'static,
stream: impl HyperStream + 'static,
scheme: &str,
host: &str,
) -> Result<Box<dyn HyperStream>, SingleRequestError> {
Expand Down Expand Up @@ -461,19 +456,11 @@ async fn proxy_to_single_node(
) -> Result<Response, SingleRequestError> {
use crate::connection_pool::GuardedSender;

if request.clearnet_whitelisted() {
let use_tor = use_tor_for_request(state, &request);
if !use_tor && request.clearnet_whitelisted() {
tracing::trace!("Request is whitelisted, sending over clearnet");
}

let use_tor = match &state.tor_client {
Some(tc)
if tc.bootstrap_status().ready_for_traffic() && !request.clearnet_whitelisted() =>
{
true
}
_ => false,
};

let key = (node.0.clone(), node.1.clone(), node.2, use_tor);

// Try to reuse an idle HTTP connection first.
Expand All @@ -484,24 +471,11 @@ async fn proxy_to_single_node(
let address = (node.1.as_str(), node.2);

let maybe_tls_stream = timeout(TIMEOUT, async {
let no_tls_stream: Box<dyn HyperStream> = 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
})
Expand Down
Loading
Loading