From 2c11e361d342e32a48b6f1b9101be3641e41a2a7 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Sat, 30 Mar 2024 10:51:40 +0100 Subject: [PATCH] Let Tor pick control port To select the Tor control port, we bind to a random port, close it and pass the port number to Tor. It can happen that Tor tries to bind to the port before it is closed. In this case, the Tor startup will fail because Tor will never print a line containing "[notice] Opened Control listener connection (ready) on ". Up until now, Bisq monitored the Tor log file to know when the control port is ready. Now Tor writes the control port to a file and Bisq reads that file. Ref: #1798 --- .../tor/common/torrc/BaseTorrcGenerator.java | 10 +++-- .../src/main/java/bisq/tor/TorService.java | 39 ++++++++++++------- .../bisq/tor/TorrcClientConfigFactory.java | 6 +-- .../bisq/tor/process/NativeTorProcess.java | 23 ++++++----- .../process/TorStartupFailedException.java | 4 ++ 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/network/tor/tor-common/src/main/java/bisq/network/tor/common/torrc/BaseTorrcGenerator.java b/network/tor/tor-common/src/main/java/bisq/network/tor/common/torrc/BaseTorrcGenerator.java index 51ac97d6c9..2986c9b9c1 100644 --- a/network/tor/tor-common/src/main/java/bisq/network/tor/common/torrc/BaseTorrcGenerator.java +++ b/network/tor/tor-common/src/main/java/bisq/network/tor/common/torrc/BaseTorrcGenerator.java @@ -25,15 +25,16 @@ @Builder public class BaseTorrcGenerator implements TorrcConfigGenerator { + private static final String CONTROL_PORT_WRITE_TO_FILE_CONFIG_KEY = "ControlPortWriteToFile"; private final Path dataDirPath; - private final int controlPort; + private final Path controlPortWriteFile; private final String hashedControlPassword; private final boolean isTestNetwork; - public BaseTorrcGenerator(Path dataDirPath, int controlPort, String hashedControlPassword, boolean isTestNetwork) { + public BaseTorrcGenerator(Path dataDirPath, Path controlPortWriteFile, String hashedControlPassword, boolean isTestNetwork) { this.dataDirPath = dataDirPath; - this.controlPort = controlPort; + this.controlPortWriteFile = controlPortWriteFile; this.hashedControlPassword = hashedControlPassword; this.isTestNetwork = isTestNetwork; } @@ -43,7 +44,8 @@ public Map generate() { Map torConfigMap = new HashMap<>(); torConfigMap.put("DataDirectory", dataDirPath.toAbsolutePath().toString()); - torConfigMap.put("ControlPort", "127.0.0.1:" + controlPort); + torConfigMap.put("ControlPort", "127.0.0.1:auto"); + torConfigMap.put(CONTROL_PORT_WRITE_TO_FILE_CONFIG_KEY, controlPortWriteFile.toAbsolutePath().toString()); torConfigMap.put("HashedControlPassword", hashedControlPassword); String logLevel = isTestNetwork ? "debug" : "notice"; diff --git a/network/tor/tor/src/main/java/bisq/tor/TorService.java b/network/tor/tor/src/main/java/bisq/tor/TorService.java index bc8a17c26b..568f7461fc 100644 --- a/network/tor/tor/src/main/java/bisq/tor/TorService.java +++ b/network/tor/tor/src/main/java/bisq/tor/TorService.java @@ -27,6 +27,7 @@ import bisq.tor.installer.TorInstaller; import bisq.tor.onionservice.CreateOnionServiceResponse; import bisq.tor.onionservice.OnionServicePublishService; +import bisq.tor.process.ControlPortReadyWaiter; import bisq.tor.process.NativeTorProcess; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import lombok.extern.slf4j.Slf4j; @@ -74,9 +75,8 @@ public CompletableFuture initialize() { installTorIfNotUpToDate(); - int controlPort = NetworkUtils.findFreeSystemPort(); PasswordDigest hashedControlPassword = PasswordDigest.generateDigest(); - createTorrcConfigFile(torDataDirPath, controlPort, hashedControlPassword); + createTorrcConfigFile(torDataDirPath, hashedControlPassword); var nativeTorProcess = new NativeTorProcess(torDataDirPath); torProcess = Optional.of(nativeTorProcess); @@ -89,19 +89,29 @@ public CompletableFuture initialize() { } } - nativeTorProcess.start(); - nativeTorProcess.waitUntilControlPortReady(); + Path controlDirPath = torDataDirPath.resolve(NativeTorProcess.CONTROL_DIR_NAME); + var controlPortReadyWaiter = new ControlPortReadyWaiter(controlDirPath); + controlPortReadyWaiter.initialize(); + + CompletableFuture completableFuture = new CompletableFuture<>(); + + controlPortReadyWaiter.getPortCompletableFuture() + .thenAccept(controlPort -> { + nativeTorController.connect(controlPort, hashedControlPassword); + nativeTorController.bindTorToConnection(); - nativeTorController.connect(controlPort, hashedControlPassword); - nativeTorController.bindTorToConnection(); + nativeTorController.enableTorNetworking(); + nativeTorController.waitUntilBootstrapped(); - nativeTorController.enableTorNetworking(); - nativeTorController.waitUntilBootstrapped(); + int port = socksPort.orElseThrow(); + torSocksProxyFactory = Optional.of(new TorSocksProxyFactory(port)); - int socksPort = this.socksPort.orElseThrow(); - torSocksProxyFactory = Optional.of(new TorSocksProxyFactory(socksPort)); + completableFuture.complete(true); + }); + + nativeTorProcess.start(); - return CompletableFuture.completedFuture(true); + return completableFuture; } @Override @@ -126,8 +136,8 @@ public CompletableFuture createOnionService(int port int localPort = localServerSocket.getLocalPort(); return onionServicePublishService.publish(privateOpenSshKey, onionAddressString, port, localPort) .thenApply(onionAddress -> { - log.info("Tor hidden service Ready. Took {} ms. Onion address={}", - System.currentTimeMillis() - ts, onionAddress); + log.info("Tor hidden service Ready. Took {} ms. Onion address={}", + System.currentTimeMillis() - ts, onionAddress); return new CreateOnionServiceResponse(localServerSocket, onionAddress); } ); @@ -160,14 +170,13 @@ private void installTorIfNotUpToDate() { torInstaller.installIfNotUpToDate(); } - private void createTorrcConfigFile(Path dataDir, int controlPort, PasswordDigest hashedControlPassword) { + private void createTorrcConfigFile(Path dataDir, PasswordDigest hashedControlPassword) { int socksPort = NetworkUtils.findFreeSystemPort(); this.socksPort = Optional.of(socksPort); TorrcClientConfigFactory torrcClientConfigFactory = TorrcClientConfigFactory.builder() .isTestNetwork(transportConfig.isTestNetwork()) .dataDir(dataDir) - .controlPort(controlPort) .socksPort(socksPort) .hashedControlPassword(hashedControlPassword) .build(); diff --git a/network/tor/tor/src/main/java/bisq/tor/TorrcClientConfigFactory.java b/network/tor/tor/src/main/java/bisq/tor/TorrcClientConfigFactory.java index 1f75f94d02..a0f156ad89 100644 --- a/network/tor/tor/src/main/java/bisq/tor/TorrcClientConfigFactory.java +++ b/network/tor/tor/src/main/java/bisq/tor/TorrcClientConfigFactory.java @@ -21,6 +21,7 @@ import bisq.network.tor.common.torrc.ClientTorrcGenerator; import bisq.network.tor.common.torrc.TestNetworkTorrcGenerator; import bisq.network.tor.common.torrc.TorrcConfigGenerator; +import bisq.tor.process.NativeTorProcess; import lombok.Builder; import net.freehaven.tor.control.PasswordDigest; @@ -33,18 +34,15 @@ public class TorrcClientConfigFactory { private final boolean isTestNetwork; private final Path dataDir; - private final int controlPort; private final int socksPort; private final PasswordDigest hashedControlPassword; public TorrcClientConfigFactory(boolean isTestNetwork, Path dataDir, - int controlPort, int socksPort, PasswordDigest hashedControlPassword) { this.isTestNetwork = isTestNetwork; this.dataDir = dataDir; - this.controlPort = controlPort; this.socksPort = socksPort; this.hashedControlPassword = hashedControlPassword; } @@ -71,7 +69,7 @@ private TorrcConfigGenerator clientTorrcGenerator() { private TorrcConfigGenerator baseTorrcGenerator() { return BaseTorrcGenerator.builder() .dataDirPath(dataDir) - .controlPort(controlPort) + .controlPortWriteFile(dataDir.resolve(NativeTorProcess.CONTROL_DIR_NAME).resolve("control")) .hashedControlPassword(hashedControlPassword.getHashedPassword()) .isTestNetwork(isTestNetwork) .build(); diff --git a/network/tor/tor/src/main/java/bisq/tor/process/NativeTorProcess.java b/network/tor/tor/src/main/java/bisq/tor/process/NativeTorProcess.java index ba74468ef2..a5c08d2834 100644 --- a/network/tor/tor/src/main/java/bisq/tor/process/NativeTorProcess.java +++ b/network/tor/tor/src/main/java/bisq/tor/process/NativeTorProcess.java @@ -24,16 +24,18 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; -import java.util.*; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; @Slf4j public class NativeTorProcess { public static final String ARG_OWNER_PID = "__OwningControllerProcess"; + public static final String CONTROL_DIR_NAME = "control"; private final Path torDataDirPath; private final Path torBinaryPath; @@ -48,6 +50,7 @@ public NativeTorProcess(Path torDataDirPath) { } public void start() { + createTorControlDirectory(); String absoluteTorrcPathAsString = torrcPath.toAbsolutePath().toString(); String ownerPid = Pid.getMyPid(); @@ -107,14 +110,14 @@ public void waitUntilExited() { }); } - private String computeLdPreloadVariable() { - File[] sharedLibraries = torDataDirPath.toFile() - .listFiles((file, fileName) -> fileName.contains(".so.")); - Objects.requireNonNull(sharedLibraries); - - return Arrays.stream(sharedLibraries) - .map(File::getAbsolutePath) - .collect(Collectors.joining(":")); + private void createTorControlDirectory() { + File controlDirFile = torDataDirPath.resolve(CONTROL_DIR_NAME).toFile(); + if (!controlDirFile.exists()) { + boolean isSuccess = controlDirFile.mkdirs(); + if (!isSuccess) { + throw new TorStartupFailedException("Couldn't create Tor control directory."); + } + } } private Future createLogFileCreationWaiter() { diff --git a/network/tor/tor/src/main/java/bisq/tor/process/TorStartupFailedException.java b/network/tor/tor/src/main/java/bisq/tor/process/TorStartupFailedException.java index e8e6518d3c..744ce37a0d 100644 --- a/network/tor/tor/src/main/java/bisq/tor/process/TorStartupFailedException.java +++ b/network/tor/tor/src/main/java/bisq/tor/process/TorStartupFailedException.java @@ -18,6 +18,10 @@ package bisq.tor.process; public class TorStartupFailedException extends RuntimeException { + public TorStartupFailedException(String message) { + super(message); + } + public TorStartupFailedException(Throwable cause) { super(cause); }