A ra.common.network.NetworkService that runs Tor embedded: the real, official Tor
Project binary (the same "Expert Bundle" Tor Browser itself ships), downloaded once,
cryptographically verified, and spawned/owned directly by this library - no system Tor
daemon, no Orbot, no third-party embedding library, no Kotlin. Used as the Tor protocol
service by 1m5-core-java (network.onemfive.core.protocol.TorProtocolService) and,
through it, as the SOCKS/DNS transport bitcoin-client-java's BitcoinJClient routes
Bitcoin P2P traffic and DNS-seed lookups over.
This library never looks for, or falls back to, some other already-running Tor instance. The embedded process it spawns itself is the only Tor it ever uses.
TORClientService.start():
TorBinaryresolves the current OS/architecture, downloads the matching official Tor Project Expert Bundle into a local cache (~/.1m5/tor-bin/, first run only), and verifies its SHA-256 against a value pinned inTorBinary's own source - not fetched from the network alongside the download (see "Trust model" below).EmbeddedTorspawns it via a plainProcessBuilder, with a generatedtorrc(SocksPort auto,ControlPort auto, realCookieAuthentication 1,__OwningControllerProcess <this JVM's pid>so Tor exits itself if this process ever dies uncleanly), authenticates over the control port using this repo's own hand-portedTORControlConnection, and blocks until Tor reports 100% bootstrap before returning - a process that's merely running but not yet bootstrapped has no real circuits.TorSocksRelayis started on its fixed local port and pointed at whatever SOCKS port the embedded process actually bound (SocksPort autopicks one at random); every consumer of this node's Tor connectivity - including this class's own outbound HTTP fetches - goes through that relay, never straight to the embedded process.- The existing hidden-service logic (
TORHiddenService,TOREventHandler) runs unchanged against the now-ready control connection.
No install steps, no torrc to edit, no daemon to start - start() either succeeds with
a fully bootstrapped, privately-owned Tor process, or fails cleanly (UNAVAILABLE).
The SHA-256 values in TorBinary were copied in only after verifying Tor Project's own
signed sha256sums-signed-build.txt against the Tor Browser Developers signing key
(fingerprint EF6E286DDA85EA2A4BA7DE684E2C6E8793298290) on 2026-09-26, for Tor
15.0.23. From that point on, TorBinary.java - reviewed and version-controlled like
any other source file - is the actual trust anchor; the download itself is never trusted
on its own. Re-verify the same way (gpg --verify against that key) before bumping the
pinned version.
Verified end-to-end in this environment: linux-x86_64 - real download, checksum
match, process spawn, full bootstrap (real circuits, including Tor's newer CONFLUX
circuits), a real hidden service created and published, and clean shutdown with no
orphaned process. macOS (x86_64/aarch64) and Windows (x86_64/i686) checksums are pinned
from the same verified, signed manifest but have not been executed/tested on those
platforms - linux-i686/windows-i686 likewise.
This is not a substitute for independent security review. Before this library is relied on anywhere a privacy failure has real consequences, it should go through actual third-party security review - a coding session isn't that, however carefully the trust chain above was built.
Deliberately not handled here, even though the Tor Project publishes Android Expert
Bundles too: Android blocks executing a binary extracted into app-writable storage
(W^X on API 29+), so a downloaded-and-cached binary the way TorBinary does it for
desktop cannot simply be exec'd on Android. An Android consumer must instead package the
binary as jniLibs/<abi>/libtor.so at APK-build time (the same trick tor-android
itself uses) and drive it with this repo's own EmbeddedTor/TORControlConnection
logic directly, given an already-executable path - see 1m5-remnant's :transport-tor
for where that adapter lives; no separate tor-android artifact is needed.
EmbeddedTor and TorBinary.Provisioned (and its constructor) are public specifically
so this is possible: an Android caller builds a Provisioned from a binary path it
discovered itself and hands it straight to EmbeddedTor.start(), skipping
TorBinary.resolve() (which stays desktop-only, package-private).
Not supported as it breaks the privacy model.
| Constant | Port | Purpose |
|---|---|---|
TORClientService.PORT_SOCKS_RELAY |
9052 | TorSocksRelay's own listening port - every consumer of this node's Tor connectivity connects here, including this class's own HTTP fetches. |
| Embedded process's SOCKS port | dynamic | SocksPort auto - chosen by Tor itself each run, discovered via GETINFO net/listeners/socks, never a fixed guess. |
| Embedded process's control port | dynamic | ControlPort auto - written to a private per-run file (control_port) EmbeddedTor reads back; real CookieAuthentication 1 (control_auth_cookie), not disabled. |
TorSocksRelay is a small loopback-only SOCKS5 server in front of the embedded process,
and exists so this class's own code is always in the path of every connection this node
makes over Tor, which is what lets it answer two things a bare "is the process alive"
check can't:
egressLikelyBlocked()- true once the relay has seen at least 10 recent connection attempts through it and 8+ of them failed. A process that's up but whose outbound connections keep failing - e.g. an exit relay's policy blocking a non-web port like Bitcoin's 8333 - looks fine to a plain liveness check; this is what actually answers "is Tor egress usable right now."TorProtocolService.egressLikelyBlocked()andrelayProxy()(aProxyobject pointed at the relay, for any caller that just wants a live SOCKS proxy) are built on this.resolve(hostname, timeout)- resolves a hostname via Tor's own SOCKS5RESOLVEextension against the embedded process, never local/system DNS. A plainjava.net.Proxy/SocketFactoryonly affectsSocket/URLConnectionconnects, notInetAddressresolution - so a caller that needs to look a hostname up (not connect to it), like bitcoinj's DNS-seed peer discovery, would otherwise leak that lookup outside Tor even while every actual peer connection is correctly proxied.BitcoinJClient'sProxiedDnsSeedDiscovery(inbitcoin-client-java) is built on this. Answers with a single address per call (Tor's extension has no A-record-set equivalent), so a caller wanting several candidates calls it once per hostname it already knows about.
The relay only implements the SOCKS5 CONNECT command as a server (no BIND/UDP
ASSOCIATE); resolve() is a separate, direct client call to the embedded process -
the JDK's Proxy/Socket SOCKS support has no API for a non-CONNECT command, so
TorSocksRelay speaks that part of the protocol itself.
- Embedded Tor -
TorBinary(provisions and verifies the official Tor Project binary) +EmbeddedTor(spawns and owns the process, real cookie auth, blocks until 100% bootstrap) replace the old local-daemon requirement entirely;LocalTorDetectorremoved.TORClientService's own outbound HTTP fetches now route throughTorSocksRelaytoo (previously bypassed it directly to the daemon's SOCKS port). TorSocksRelay.resolve(hostname, timeout)- Tor's SOCKS5RESOLVEextension, for proxied DNS lookups with no connection attached.TorSocksRelay- a real local SOCKS5 relay (PORT_SOCKS_RELAY, 9052) every Tor consumer connects through, with connection-outcome tracking (egressLikelyBlocked()).getNetwork()convenience; used as the Tor protocol service by1m5-core-java(network.onemfive.core.protocol.TorProtocolService).- Modern
maven-surefire-pluginso the JUnit 5 tests actually run. - Added
DESIGN.md,TODO.md.