Skip to content

Conversation

@nabijaczleweli
Copy link

@nabijaczleweli nabijaczleweli commented Oct 20, 2025

See commit messages for detail and full comparison logs, but in short:

  1. on whonix, all normal TCP traffic is routed through the Tor proxy (and there's no way to bypass this)
  2. we can use the system Tor's SOCKS5 socket directly as well, so
  3. in the settings GUI, force the "Use Tor" slider on, make it undisableable, and add a note about whonix
  4. hide the "Route Monero traffic through Tor" subtoggle
  5. in the TUI questionnaires, don't ask to enable the Tor hidden service, or what address to listen on
  6. instead of starting arti, call the system Tor proxy's SOCKS5 address

In principle, we could start a hidden service, but (a) the user needs to manually edit the configuration of both whonix VMs, (b) this still means talking to the Tor daemon with the control protocol, so (c) that means implementing the whole of the control protocol, and (d) the implementations which are currently in the wild do not support unix-domain socket backends (I did implement them in 2025-06 but the uptake has been slow), so (e) the resulting implementation is humongous (#391), so (f) this has been removed on request: #391 (comment)

AFAICT there's two code paths that can start arti and both are pre-empted by this.

For future extensibility, there are two levels of detexion:

  1. may_init_tor() should return false if standard TCP traffic is routed through Tor ‒ this disables starting arti
  2. existing_tor_config() can then return Some(...) if there's a Tor-capable SOCKS5 proxy to connect to instead of arti (and preferentially to plain TCP)

(For other systems (Tails?) the latter will most likely either reduce to if is_whonix() || is_tails() or else if is_tails() { Some(SocksServerAddress::Ip(...)) } )

(Also some cleanups at the front.)

Cf. #391, #453, https://bounties.monero.social/posts/180/0-789m-make-unstoppable-swap-whonix-friendly

a b c

@nabijaczleweli
Copy link
Author

nabijaczleweli commented Oct 20, 2025

@binarybaron's checklist:

  1. Not use any external dependencies (except widely used low level networking libraries) ‒ this holds. The only new dependency is tokio-socks which has 14M alltime and like 20k/d downloads; that's widely-used to me
  2. Create an internal adapter that wraps either an Arti client or a external Tor socks proxy ‒ this adapter is OrTransport, which means the SOCKS5 proxy doesn't touch the arti one at all, so this is a modularity win
  3. Drop support for hidden services for non arti clients due to the huge amount of complexity involved ‒ 👍
  4. Massively reduce the size of the diff ‒ a veritable butcher's cut from +10k-600 to +350-40, and a lot of that is trivial boilerplate

*/
function MoneroTorSettings() {
// Hide this setting if it's superseded by the global Tor connection
if (torForced) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React hooks must never be called conditionally

You need to move this below the hook calls.

@binarybaron
Copy link

@binarybaron's checklist:

  1. Not use any external dependencies (except widely used low level networking libraries) ‒ this holds. The only new dependency is tokio-socks which has 14M alltime and like 20k/d downloads; that's widely-used to me
  2. Create an internal adapter that wraps either an Arti client or a external Tor socks proxy ‒ this adapter is OrTransport, which means the SOCKS5 proxy doesn't touch the arti one at all, so this is a modularity win
  3. Drop support for hidden services for non arti clients due to the huge amount of complexity involved ‒ 👍
  4. Massively reduce the size of the diff ‒ a veritable butcher's cut from +10k-600 to +350-40, and a lot of that is trivial boilerplate

I'm happy to see you take this on!

  1. Create an internal adapter that wraps either an Arti client or a external Tor socks proxy ‒ this adapter is OrTransport, which means the SOCKS5 proxy doesn't touch the arti one at all, so this is a modularity win

The adapter should not be relying on libp2p as it will not only be used for libp2p but also for other things in the future (bdk, monero-rpc-pool). It should be more low level and expose an interface similar to arti-client (a subset)

Please also make sure to put as little business logic into the frontend as possible.

@nabijaczleweli nabijaczleweli force-pushed the another-bout branch 4 times, most recently from 673e9dc to 8f06edd Compare October 22, 2025 03:29
@nabijaczleweli
Copy link
Author

nabijaczleweli commented Oct 22, 2025

Since last we spoke, I back-propagated this as I think you'd hoped: I replaced the open-coded Option<Arc<TorClient<TokioRustlsRuntime>>>> with a new TorBackend enum, which pulled all the new logic back from the more UI-related callers, and it naturally pulled out even more, so front-end code should be even thinner. monero-rpc-pool, thus, also uses TorBackend instead of its Option<TorClientArc>.

I also tested in Tails. Tails userland operates in torsocks mode (nc github.com 443 and nc cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion 80 both work) with no way to call the underlying proxy. I detect Tails and add TorBackend::Torsocks that calls /onion3/cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd:80 address prefixes like /dns/cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion/tcp/80. See the Tails commit for how easy it is to add another backend, should it be required.

...and actually, whonix also does this. Since we don't have hidden services, we don't really need to talk to the proxy directly anymore either, and can treat whonix like Tails. This would drop basically all SOCKS5 and socket-related code w.l.o.g., so if you're looking to reduce this as far as possible...

I also found a privacy leak that bypasses Tor in some configurations. Please see the second commit from the top ("fix(swap/network): remove DNS leak if using Tor").

@binarybaron
Copy link

Sound good! Let me know once this is ready for review.

@nabijaczleweli
Copy link
Author

I also tested in Tails. Tails userland operates in torsocks mode (nc github.com 443 and nc cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion 80 both work) with no way to call the underlying proxy.

This is not true. It's supposed to be but it doesn't work on Tails (it works on whonix), and, even if you manage to resolve the address (notably /etc/resolv.conf says nameserver 127.0.0.1 but no-one seems to be listening on 127.0.0.1:53. it does work sometimes though), then nc ipaddr 80|443 fails instantly, curl errors out with ECONNREFUSED, and eigenwallet errors out with "failed to check for updates: error sending request for url (https://cdn.crabnebula.app/...)".

b c

However, since curl would be broken in basically 100% of cases, and vanilla curl won't resolve .onion addresses, curl is actually /usr/local/bin/curl:
d

Thus, the correct way to dial out on Tails is to connect to a SOCKS5 proxy at well-known address 127.0.0.1:9050, and not to use torsocks.

On whonix we can use torsocks instead of calling the tor daemon over SOCKS5 directly (which may reduce the TcpOrUnixStream cruft), or we may call over SOCKS5 instead of torsocks (thus dropping torsocks).

I'll re-evaluate which combination of features yields the best-reduced patchset.

@nabijaczleweli
Copy link
Author

commit 7011e7fdcbb0d01a58836fb099f51b7026a2bf98 (HEAD -> another-bout)
Author: наб <[email protected]>
Date:   Fri Oct 24 01:19:58 2025 +0200

    Remove TorBackend::Socks5's unix-domain socket back-end

 swap-tor/src/lib.rs    | 121 +++++++++++--------------------------------------------------------------------------------------------------------------
 swap/src/common/tor.rs |  18 ++++++------------
 2 files changed, 17 insertions(+), 122 deletions(-)

vs

commit b7037ccda18790cabb103d1cbe969963c0b1b9b7
Author: наб <[email protected]>
Date:   Fri Oct 24 01:05:44 2025 +0200

    Remove TorBackend::Torsocks

 monero-rpc-pool/src/tor.rs |  6 ++----
 swap-tor/Cargo.toml        |  2 +-
 swap-tor/src/lib.rs        | 77 -----------------------------------------------------------------------------
 swap/src/common/tor.rs     | 44 +++++++++++++++-----------------------------
 4 files changed, 18 insertions(+), 111 deletions(-)

so -105 vs -93. I will be retaining SOCKS5 for Tails and Torsocks for whonix.

@nabijaczleweli
Copy link
Author

Also, because the Tails userland can't call over TCP directly, we need to also tell the monero C++ backend to talk over SOCKS5 as well (thankfully it supports it, it's just a question of injecting that in the right place I think).

The same applies to the autoupdater in tauri:
d
but this originates in a plugin. I haven't yet evaluated if it can be coerced to talk over SOCKS5.

@binarybaron
Copy link

Also, because the Tails userland can't call over TCP directly, we need to also tell the monero C++ backend to talk over SOCKS5 as well (thankfully it supports it, it's just a question of injecting that in the right place I think).

The same applies to the autoupdater in tauri: d but this originates in a plugin. I haven't yet evaluated if it can be coerced to talk over SOCKS5.

The init function in bridge.rs (exposed by wallet2_api.h) allows us to set a proxy address.

@nabijaczleweli
Copy link
Author

yeah, I got to that, and I think all the other places that do network I/O, last night (it may even be part of the pushed WIP); this was mostly to prime future readers for why it Got Bigger again 😬

@nabijaczleweli
Copy link
Author

Apparently the default resolver rejects .onion addresses instead of not doing that (hickory-dns/hickory-dns#3331), so the Torsocks back-end needs to use a different resolver.

@nabijaczleweli nabijaczleweli force-pushed the another-bout branch 2 times, most recently from 9d80f95 to bc2854f Compare October 26, 2025 06:07
@nabijaczleweli nabijaczleweli changed the title Whonix support Tails+Whonix support Oct 26, 2025
@nabijaczleweli
Copy link
Author

nabijaczleweli commented Oct 26, 2025

@binarybaron PTAL. https://foreign.nabijaczleweli.xyz/pub/eigenwallet_3.2.0-rc.4_amd64.AppImage for your testing convenience and assorted screenshots below

Functionally this ended up being relatively thin (with a lot of deduplication wins), in taht both whonix and Tails end up using just SocksTransport, which only needs to support TCP so we get about without any wrappers, with a lot of fiddly hook injections: on tails Tails we need to cram a SOCKS5 proxy into every(? should be every, wfm and I don't see any non-TLS-related errors, which I assume is why I don't see any /dns/... swappers in my screenshots? they appear sometimes. idk) source of outgoing TCP calls.

whonix:
a
b
c

tails:
d
e
f

@binarybaron
Copy link

@binarybaron PTAL. https://foreign.nabijaczleweli.xyz/pub/eigenwallet_3.2.0-rc.4_amd64.AppImage for your testing convenience and assorted screenshots below

Functionally this ended up being relatively thin (with a lot of deduplication wins), in taht both whonix and Tails end up using just SocksTransport, which only needs to support TCP so we get about without any wrappers, with a lot of fiddly hook injections: on tails Tails we need to cram a SOCKS5 proxy into every(? should be every, wfm and I don't see any non-TLS-related errors, which I assume is why I don't see any /dns/... swappers in my screenshots? they appear sometimes. idk) source of outgoing TCP calls.

whonix: a b c

tails: d e f

Awesome! Thank you for your work.

I'll review this as soon as I find the time. I'll also have to get a x86 machine to test this.

@binarybaron
Copy link

@Einliterflasche Please also give this a review.

@binarybaron binarybaron changed the title Tails+Whonix support feat: Tails+Whonix support Oct 26, 2025
@nabijaczleweli
Copy link
Author

nabijaczleweli commented Oct 26, 2025

JS fetch() doesn't go through the proxy so there's no fiat rates on Tails. There's hopefully a tauri toggle to inject it somewhere (these look or this looks promising at first glance).

@nabijaczleweli
Copy link
Author

nabijaczleweli commented Oct 26, 2025

adding

{
  "app": {
    "windows": [
      {
        "proxyUrl": "socks5://127.0.0.1:9050"

to tauri.conf.json fixes it. we can edit this at run-time as well:
a

fixed, convenience AppImage updated

@binarybaron
Copy link

adding

{
  "app": {
    "windows": [
      {
        "proxyUrl": "socks5://127.0.0.1:9050"

to tauri.conf.json fixes it. we can edit this at run-time as well: a

fixed, convenience AppImage updated

Awesome progress, I'm confident we can merge this fairly soon!

Please consider joining our eigenwallet development Matrix room 💛

// Check for updates when component mounts
check()
// Check, CheckOptions for updates when component mounts
check({ proxy })

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to avoid the rust (getUpdaterProxy) -> js -> rust (check) roundtrip and do this in rust directly? There may be a way to configure the updater in src-tauri/lib.rs.

Copy link
Author

@nabijaczleweli nabijaczleweli Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately no. The TS API is thus:

async function check(options?: CheckOptions): Promise<Update | null> {
  convertToRustHeaders(options)

  const metadata = await invoke<UpdateMetadata | null>('plugin:updater|check', {
    ...options
  })
  return metadata ? new Update(metadata) : null
}

And calls

#[tauri::command]
pub(crate) async fn check<R: Runtime>(
    webview: Webview<R>,
    headers: Option<Vec<(String, String)>>,
    timeout: Option<u64>,
    proxy: Option<String>,
    target: Option<String>,
    allow_downgrades: Option<bool>,
) -> Result<Option<Metadata>> {
    let mut builder = webview.updater_builder();
    if let Some(headers) = headers {
        for (k, v) in headers {
            builder = builder.header(k, v)?;
        }
    }
    if let Some(timeout) = timeout {
        builder = builder.timeout(Duration::from_millis(timeout));
    }
    if let Some(ref proxy) = proxy {
        let url = Url::parse(proxy.as_str())?;
        builder = builder.proxy(url);
    }

so there's no other way to forward the proxy parameter (it doesn't look like it reads the proxy from the global tauri webview config either, and there's no plugin-global proxy config field).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be able to create a custom Tauri command that replicates the behaviour but I think that's too much effort for now.

@binarybaron
Copy link

Can you also run the formatter (just fmt) and add a changelog entry?

Copy link

@binarybaron binarybaron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments, mostly nitpicks. I really like the approach and I am impressed how fast you got a hold of the codebase!

@Einliterflasche will give a review too

@binarybaron
Copy link

Let me know if you'd be interested in contributing to the project again! We'd love to have you

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
…user about listening on TCP/Onion if the environment doesn't support it (TUI questionnaire)
Bypassing Tor on TorBackend::Socks breaks everything,
because /all/ traffic needs to go through the proxy
(normal connect() is broken on Tails)
@nabijaczleweli
Copy link
Author

All applied. I'm not opposed to completing more bounties or similar.

@binarybaron
Copy link

All applied. I'm not opposed to completing more bounties or similar.

We have a ton of compatibility isuses on Linux which have been hard for us to debug. We are also interested in building a TUI as an alternative for the Web based GUI. If you're interested in working on either of this things, let me know :)

type Output = tokio_util::compat::Compat<TcpStream>;
type Error = tokio_socks::Error;
type ListenerUpgrade = std::future::Pending<Result<Self::Output, Self::Error>>;
type Dial = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send + 'static>>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be type Dial = impl Future<Output = ...> + Send + 'static;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet:

error[E0658]: `impl Trait` in associated types is unstable
  --> swap-tor/src/lib.rs:79:17
   |
79 |     type Dial = impl Future<Output = Result<Self::Output, Self::Error>> + Send + 'static;
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: see issue #63063 <https://github.com/rust-lang/rust/issues/63063> for more information

error: item does not constrain `<Socks5Transport as libp2p::Transport>::Dial::{opaque#0}`
   --> swap-tor/src/lib.rs:104:8
    |
104 |     fn dial_as_listener(
    |        ^^^^^^^^^^^^^^^^
    |
    = note: consider removing `#[define_opaque]` or adding an empty `#[define_opaque()]`
note: this opaque type is supposed to be constrained
   --> swap-tor/src/lib.rs:79:17
    |
79  |     type Dial = impl Future<Output = Result<Self::Output, Self::Error>> + Send + 'static;
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0658`.
error: could not compile `swap-tor` (lib) due to 2 previous errors

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it can be type Dial = BoxFuture<'static, Result<Self::Output, Self::Error>>; which is already used in libp2p-tor for its type Dial. Applied that.

@binarybaron
Copy link

@Einliterflasche Please also test this on your x86 machine

@Einliterflasche
Copy link

Can confirm this works on the qubesos/whonix 17 workstatio.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants