Add an embedded Tor daemon (kmp-tor) with an onion toggle in the sidebar
drawer and on the pre-login Splash/Auth screens. When enabled, every
connection — relay WebSockets, Coil images, ExoPlayer media, NIP-05/NIP-11,
LNURL/zaps, Blossom uploads, link previews, exchange rates, DM media, live
metrics — routes through Tor's SOCKS proxy.
Routing is fail-closed: from the moment the toggle flips, new clients point
at a dead loopback port until bootstrap completes, and existing clients have
their in-flight requests cancelled and keep-alive pools evicted so no
pre-toggle socket survives. OkHttp passes unresolved hostnames to SOCKS
proxies, so DNS also resolves inside Tor.
- TorManager: kmp-tor runtime in a foreground service; state machine
(Off/Starting %/On/Stopping/Error) observed by all toggles; start retries
around kmp-tor's fixed 1s service-bind timeout, which a busy main thread
misses during cold start
- TorPreferences: device-level pref so the toggle works pre-login and
survives logout
- HttpClientFactory: single proxy choke point; client registry +
setTorSocks() invalidation; new getRelayClient/getLoopbackClient/
getDmMediaClient
- Relay/RelayPool: clients resolved through providers on every (re)connect;
suspendForTorSwitch/resumeAfterTorSwitch tear down sockets and suppress
reconnects during bootstrap so the 5-min relay cooldown can't trip; local
relay stays direct (Tor refuses loopback/LAN targets)
- Captured-client fixes so a mid-session toggle can't leak: Coil delegating
Call.Factory (it caches its lambda forever), ZapSender, DmBubble,
NwcRepository, SplashViewModel raw client, ExchangeRateRepository refetch
- Cold start with Tor on: relay init gated on bootstrap
- kmp-tor pinned to 2.4.x: 2.5+ needs Kotlin 2.2 metadata, unreadable by
this project's Kotlin 2.0.21
Verified on-device by socket-inode audit: app process held 43 sockets, all
to 127.0.0.1:<socks>; only the tor child process held external connections.
Known gaps: Breez/Spark SDK (own Rust networking) and ML Kit model
downloads bypass OkHttp and are not routed.