diff --git a/build.gradle b/build.gradle index 5133b58a762..cac738ea197 100644 --- a/build.gradle +++ b/build.gradle @@ -125,7 +125,10 @@ configure(project(':common')) { configure(project(':p2p')) { dependencies { compile project(':common') - compile('com.github.JesusMcCloud.netlayer:tor.native:0.4.7.1.1') { + compile('com.github.JesusMcCloud.netlayer:tor.native:0.6') { + exclude(module: 'slf4j-api') + } + compile('com.github.JesusMcCloud.netlayer:tor.external:0.6') { exclude(module: 'slf4j-api') } compile('org.apache.httpcomponents:httpclient:4.5.3') { diff --git a/core/src/main/java/bisq/core/app/BisqEnvironment.java b/core/src/main/java/bisq/core/app/BisqEnvironment.java index 96846c5167c..b7ddc32faaf 100644 --- a/core/src/main/java/bisq/core/app/BisqEnvironment.java +++ b/core/src/main/java/bisq/core/app/BisqEnvironment.java @@ -193,9 +193,10 @@ private static String appDataDir(String userDataDir, String appName) { protected final String btcNodes, seedNodes, ignoreDevMsg, useDevPrivilegeKeys, useDevMode, useTorForBtc, rpcUser, rpcPassword, rpcPort, rpcBlockNotificationPort, dumpBlockchainData, fullDaoNode, myAddress, banList, dumpStatistics, maxMemory, socks5ProxyBtcAddress, - torRcFile, torRcOptions, + torRcFile, torRcOptions, externalTorControlPort, externalTorPassword, externalTorCookieFile, socks5ProxyHttpAddress, useAllProvidedNodes, numConnectionForBtc, genesisTxId, genesisBlockHeight, referralId, daoActivated; + protected final boolean externalTorUseSafeCookieAuthentication; public BisqEnvironment(OptionSet options) { this(new JOptCommandLinePropertySource(BISQ_COMMANDLINE_PROPERTY_SOURCE_NAME, checkNotNull( @@ -274,6 +275,18 @@ public BisqEnvironment(PropertySource commandLineProperties) { torRcOptions = commandLineProperties.containsProperty(NetworkOptionKeys.TORRC_OPTIONS) ? (String) commandLineProperties.getProperty(NetworkOptionKeys.TORRC_OPTIONS) : ""; + externalTorControlPort = commandLineProperties.containsProperty(NetworkOptionKeys.EXTERNAL_TOR_CONTROL_PORT) ? + (String) commandLineProperties.getProperty(NetworkOptionKeys.EXTERNAL_TOR_CONTROL_PORT) : + ""; + externalTorPassword = commandLineProperties.containsProperty(NetworkOptionKeys.EXTERNAL_TOR_PASSWORD) ? + (String) commandLineProperties.getProperty(NetworkOptionKeys.EXTERNAL_TOR_PASSWORD) : + ""; + externalTorCookieFile = commandLineProperties.containsProperty(NetworkOptionKeys.EXTERNAL_TOR_COOKIE_FILE) ? + (String) commandLineProperties.getProperty(NetworkOptionKeys.EXTERNAL_TOR_COOKIE_FILE) : + ""; + externalTorUseSafeCookieAuthentication = commandLineProperties.containsProperty(NetworkOptionKeys.EXTERNAL_TOR_USE_SAFECOOKIE) ? + true : + false; //RpcOptionKeys rpcUser = commandLineProperties.containsProperty(DaoOptionKeys.RPC_USER) ? @@ -444,6 +457,11 @@ private PropertySource defaultProperties() { setProperty(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS, socks5ProxyHttpAddress); setProperty(NetworkOptionKeys.TORRC_FILE, torRcFile); setProperty(NetworkOptionKeys.TORRC_OPTIONS, torRcOptions); + setProperty(NetworkOptionKeys.EXTERNAL_TOR_CONTROL_PORT, externalTorControlPort); + setProperty(NetworkOptionKeys.EXTERNAL_TOR_PASSWORD, externalTorPassword); + setProperty(NetworkOptionKeys.EXTERNAL_TOR_COOKIE_FILE, externalTorCookieFile); + if (externalTorUseSafeCookieAuthentication) + setProperty(NetworkOptionKeys.EXTERNAL_TOR_USE_SAFECOOKIE, "true"); setProperty(AppOptionKeys.APP_DATA_DIR_KEY, appDataDir); setProperty(AppOptionKeys.DESKTOP_WITH_HTTP_API, desktopWithHttpApi); diff --git a/core/src/main/java/bisq/core/app/BisqExecutable.java b/core/src/main/java/bisq/core/app/BisqExecutable.java index e4ac9a1a4d4..c2adbe7e2b8 100644 --- a/core/src/main/java/bisq/core/app/BisqExecutable.java +++ b/core/src/main/java/bisq/core/app/BisqExecutable.java @@ -363,6 +363,24 @@ protected void customizeOptionParsing(OptionParser parser) { description("A list of torrc-entries to amend to Bisqs torrc. Note that torrc-entries, which are critical to Bisqs flawless operation, cannot be overwritten. [torrc options line, torrc option, ...]", "")) .withRequiredArg() .withValuesConvertedBy(RegexMatcher.regex("^([^\\s,]+\\s[^,]+,?\\s*)+$")); + parser.accepts(NetworkOptionKeys.EXTERNAL_TOR_CONTROL_PORT, + description("The control port of an already running Tor service to be used by Bisq [port].", "")) + .availableUnless(NetworkOptionKeys.TORRC_FILE, NetworkOptionKeys.TORRC_OPTIONS) + .withRequiredArg() + .ofType(int.class); + parser.accepts(NetworkOptionKeys.EXTERNAL_TOR_PASSWORD, + description("The password for controlling the already running Tor service.", "")) + .availableIf(NetworkOptionKeys.EXTERNAL_TOR_CONTROL_PORT) + .withRequiredArg(); + parser.accepts(NetworkOptionKeys.EXTERNAL_TOR_COOKIE_FILE, + description("The cookie file for authenticating against the already running Tor service. Use in conjunction with --" + NetworkOptionKeys.EXTERNAL_TOR_USE_SAFECOOKIE, "")) + .availableIf(NetworkOptionKeys.EXTERNAL_TOR_CONTROL_PORT) + .availableUnless(NetworkOptionKeys.EXTERNAL_TOR_PASSWORD) + .withRequiredArg() + .withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, PathProperties.READABLE)); + parser.accepts(NetworkOptionKeys.EXTERNAL_TOR_USE_SAFECOOKIE, + description("Use the SafeCookie method when authenticating to the already running Tor service.", "")) + .availableIf(NetworkOptionKeys.EXTERNAL_TOR_COOKIE_FILE); //AppOptionKeys parser.accepts(AppOptionKeys.USER_DATA_DIR_KEY, diff --git a/gradle/witness/gradle-witness.gradle b/gradle/witness/gradle-witness.gradle index dd3119d1251..6c58e785697 100644 --- a/gradle/witness/gradle-witness.gradle +++ b/gradle/witness/gradle-witness.gradle @@ -20,7 +20,8 @@ dependencyVerification { 'de.jensd:fontawesomefx-commons:5539bb3335ecb822dbf928546f57766eeb9f1516cc1417a064b5709629612149', 'com.googlecode.jcsv:jcsv:73ca7d715e90c8d2c2635cc284543b038245a34f70790660ed590e157b8714a2', 'com.github.sarxos:webcam-capture:d960b7ea8ec3ddf2df0725ef214c3fccc9699ea7772df37f544e1f8e4fd665f6', - 'com.github.JesusMcCloud.netlayer:tor.native:0ad92f93c509a200a61cedbe0010d014f35ab57bcf131a4e268e1914e66be2e0', + 'com.github.JesusMcCloud.netlayer:tor.native:f1bf0096f9eb6020645a65d91aa530d15aef97e69cc5a79d7b2405421f74700a', + 'com.github.JesusMcCloud.netlayer:tor.external:cfba681398c191a1906d6d023a3be28a8fa9b1f4eee52e966daf7b1ae630414f', 'org.apache.httpcomponents:httpclient:db3d1b6c2d6a5e5ad47577ad61854e2f0e0936199b8e05eb541ed52349263135', 'net.sf.jopt-simple:jopt-simple:6f45c00908265947c39221035250024f2caec9a15c1c8cf553ebeecee289f342', 'org.fxmisc.easybind:easybind:666af296dda6de68751668a62661571b5238ac6f1c07c8a204fc6f902b222aaf', @@ -37,11 +38,11 @@ dependencyVerification { 'com.google.code.findbugs:jsr305:c885ce34249682bc0236b4a7d56efcc12048e6135a5baf7a9cde8ad8cda13fcd', 'com.google.guava:guava:36a666e3b71ae7f0f0dca23654b67e086e6c93d192f60ba5dfd5519db6c288c8', 'com.google.inject:guice:9b9df27a5b8c7864112b4137fd92b36c3f1395bfe57be42fedf2f520ead1a93e', - 'com.github.JesusMcCloud.netlayer:tor:4a6a6102331c35e7ad2a574cf81ddab89fc1256305805e82c5af1f542f336629', - 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:b306e0e6735841e31e320bf3260c71d60fc35057cfa87895f23251ee260a64a8', - 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:169ee5879cba8444499243ceea5e6a2cb6ecea5424211cc819f0704501154b35', + 'com.github.JesusMcCloud.netlayer:tor:ac8465b7dda30ea920ec31a6bde42df7e88bee0282e805ce2797628938e3cf0b', + 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:193ab7813e4d249f2ea4fc1b968fea8c2126bcbeeb5d6127050ce1b93dbaa7c2', + 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:877b59bbe466b24a88275a71fd06cd97359d2085420f6f1ac1d766afa8116001', 'io.github.microutils:kotlin-logging:4992504fd3c6ecdf9ed10874b9508e758bb908af9e9d7af19a61e9afb6b7e27a', - 'org.jetbrains.kotlin:kotlin-stdlib:f0595b9ed88ddc6fd66bddf68c56c6f2f6c4b17faa51e43e478acad32b05303e', + 'org.jetbrains.kotlin:kotlin-stdlib:4ff0fcb97f4983b4aaba12668c24ad21b08460915db1b021d8f1d8bee687f21c', 'org.jetbrains:annotations:ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478', 'org.bouncycastle:bcpg-jdk15on:de3355b821fc81dd32e1f3f560d5b3eca1c678fd2400011d0bfc69fb91bcde85', 'commons-io:commons-io:cc6a41dc3eaacc9e440a6bd0d2890b20d36b4ee408fe2d67122f328bb6e01581', @@ -63,14 +64,13 @@ dependencyVerification { 'com.lambdaworks:scrypt:9a82d218099fb14c10c0e86e7eefeebd8c104de920acdc47b8b4b7a686fb73b4', 'com.google.zxing:core:11aae8fd974ab25faa8208be50468eb12349cd239e93e7c797377fa13e381729', 'com.cedricwalter:tor-binary-geoip:fbd7656a262607e5a73016e048d5270cbabcd4639a1795b4b4e762df8877429d', - 'com.github.JesusMcCloud:jtorctl:c6ef92e46074d8d26db718ce0fe4b64b8cf7b934b7377d164c5d613b4cd7b847', - 'org.apache.commons:commons-compress:a778bbd659722889245fc52a0ec2873fbbb89ec661bc1ad3dc043c0757c784c4', + 'com.github.JesusMcCloud:jtorctl:ba71601cbe50474ccc39a17bc6f7880c1412d8d19b94d37aee69ea2917f72046', + 'org.apache.commons:commons-compress:5f2df1e467825e4cac5996d44890c4201c000b43c0b23cffc0782d28a0beb9b0', 'org.tukaani:xz:a594643d73cc01928cf6ca5ce100e094ea9d73af760a5d4fb6b75fa673ecec96', 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', 'net.jcip:jcip-annotations:be5805392060c71474bf6c9a67a099471274d30b83eef84bfc4e0889a4f1dcc0', 'org.bitcoinj:orchid:f836325cfa0466a011cb755c9b0fee6368487a2352eb45f4306ad9e4c18de080', 'com.squareup.okhttp:okhttp:b4c943138fcef2bcc9d2006b2250c4aabbedeafc5947ed7c0af7fd103ceb2707', - 'org.objenesis:objenesis:5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d', 'com.squareup.okio:okio:114bdc1f47338a68bcbc95abf2f5cdc72beeec91812f2fcd7b521c1937876266', ] } diff --git a/p2p/src/main/java/bisq/network/NetworkOptionKeys.java b/p2p/src/main/java/bisq/network/NetworkOptionKeys.java index 2d2bb0f641c..c92ab86e59a 100644 --- a/p2p/src/main/java/bisq/network/NetworkOptionKeys.java +++ b/p2p/src/main/java/bisq/network/NetworkOptionKeys.java @@ -31,4 +31,8 @@ public class NetworkOptionKeys { public static final String SOCKS_5_PROXY_HTTP_ADDRESS = "socks5ProxyHttpAddress"; public static final String TORRC_OPTIONS = "torrcOptions"; public static final String TORRC_FILE = "torrcFile"; + public static final String EXTERNAL_TOR_CONTROL_PORT = "torControlPort"; + public static final String EXTERNAL_TOR_PASSWORD = "torControlPassword"; + public static final String EXTERNAL_TOR_COOKIE_FILE = "torControlCookieFile"; + public static final String EXTERNAL_TOR_USE_SAFECOOKIE = "torControlUseSafeCookieAuth"; } diff --git a/p2p/src/main/java/bisq/network/p2p/NetworkNodeProvider.java b/p2p/src/main/java/bisq/network/p2p/NetworkNodeProvider.java index 20938753aed..9f696e733c1 100644 --- a/p2p/src/main/java/bisq/network/p2p/NetworkNodeProvider.java +++ b/p2p/src/main/java/bisq/network/p2p/NetworkNodeProvider.java @@ -21,6 +21,8 @@ import bisq.network.p2p.network.BridgeAddressProvider; import bisq.network.p2p.network.LocalhostNetworkNode; import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.network.NewTor; +import bisq.network.p2p.network.RunningTor; import bisq.network.p2p.network.TorNetworkNode; import bisq.common.proto.network.NetworkProtoResolver; @@ -44,10 +46,17 @@ public NetworkNodeProvider(NetworkProtoResolver networkProtoResolver, @Named(NetworkOptionKeys.PORT_KEY) int port, @Named(NetworkOptionKeys.TOR_DIR) File torDir, @Named(NetworkOptionKeys.TORRC_FILE) String torrcFile, - @Named(NetworkOptionKeys.TORRC_OPTIONS) String torrcOptions) { + @Named(NetworkOptionKeys.TORRC_OPTIONS) String torrcOptions, + @Named(NetworkOptionKeys.EXTERNAL_TOR_CONTROL_PORT) String controlPort, + @Named(NetworkOptionKeys.EXTERNAL_TOR_PASSWORD) String password, + @Named(NetworkOptionKeys.EXTERNAL_TOR_COOKIE_FILE) String cookieFile, + @Named(NetworkOptionKeys.EXTERNAL_TOR_USE_SAFECOOKIE) boolean useSafeCookieAuthentication ) { networkNode = useLocalhostForP2P ? new LocalhostNetworkNode(address, port, networkProtoResolver) : - new TorNetworkNode(port, torDir, networkProtoResolver, bridgeAddressProvider, torrcFile, torrcOptions); + new TorNetworkNode(port, torDir, networkProtoResolver, + !controlPort.isEmpty() ? + new RunningTor(torDir, Integer.parseInt(controlPort), password, cookieFile, useSafeCookieAuthentication) : + new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider.getBridgeAddresses())); } @Override diff --git a/p2p/src/main/java/bisq/network/p2p/P2PModule.java b/p2p/src/main/java/bisq/network/p2p/P2PModule.java index 4e0d0903345..dfd677cd22a 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PModule.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PModule.java @@ -90,5 +90,9 @@ protected void configure() { bindConstant().annotatedWith(named(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS)).to(environment.getRequiredProperty(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS)); bindConstant().annotatedWith(named(NetworkOptionKeys.TORRC_FILE)).to(environment.getRequiredProperty(NetworkOptionKeys.TORRC_FILE)); bindConstant().annotatedWith(named(NetworkOptionKeys.TORRC_OPTIONS)).to(environment.getRequiredProperty(NetworkOptionKeys.TORRC_OPTIONS)); + bindConstant().annotatedWith(named(NetworkOptionKeys.EXTERNAL_TOR_CONTROL_PORT)).to(environment.getRequiredProperty(NetworkOptionKeys.EXTERNAL_TOR_CONTROL_PORT)); + bindConstant().annotatedWith(named(NetworkOptionKeys.EXTERNAL_TOR_PASSWORD)).to(environment.getRequiredProperty(NetworkOptionKeys.EXTERNAL_TOR_PASSWORD)); + bindConstant().annotatedWith(named(NetworkOptionKeys.EXTERNAL_TOR_COOKIE_FILE)).to(environment.getRequiredProperty(NetworkOptionKeys.EXTERNAL_TOR_COOKIE_FILE)); + bindConstant().annotatedWith(named(NetworkOptionKeys.EXTERNAL_TOR_USE_SAFECOOKIE)).to(environment.containsProperty(NetworkOptionKeys.EXTERNAL_TOR_USE_SAFECOOKIE) ? true : false); } } diff --git a/p2p/src/main/java/bisq/network/p2p/network/NewTor.java b/p2p/src/main/java/bisq/network/p2p/network/NewTor.java new file mode 100644 index 00000000000..a5cdb1018cc --- /dev/null +++ b/p2p/src/main/java/bisq/network/p2p/network/NewTor.java @@ -0,0 +1,121 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.network; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.stream.Collectors; + +import org.berndpruenster.netlayer.tor.NativeTor; +import org.berndpruenster.netlayer.tor.Tor; +import org.berndpruenster.netlayer.tor.TorCtlException; +import org.berndpruenster.netlayer.tor.Torrc; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class creates a brand new instance of the Tor onion router. + * + * When asked, the class checks, whether command line parameters such as + * --torrcFile and --torrcOptions are set and if so, takes these settings into + * account. Then, a fresh set of Tor binaries is installed and Tor is launched. + * Finally, a {@link Tor} instance is returned for further use. + * + * @author Florian Reimair + * + */ +public class NewTor extends TorMode { + + private static final Logger log = LoggerFactory.getLogger(NewTor.class); + + private final String torrcFile; + private final String torrcOptions; + private final Collection bridgeEntries; + private final File torWorkikngDirectory; + + public NewTor(File torWorkingDirectory, String torrcFile, String torrcOptions, Collection bridgeEntries) { + this.torrcFile = torrcFile; + this.torrcOptions = torrcOptions; + this.bridgeEntries = bridgeEntries; + this.torWorkikngDirectory = torWorkingDirectory; + } + + @Override + public Tor getTor() throws IOException, TorCtlException { + long ts1 = new Date().getTime(); + + if (bridgeEntries != null) + log.info("Using bridges: {}", bridgeEntries.stream().collect(Collectors.joining(","))); + + Torrc override = null; + + // check if the user wants to provide his own torrc file + if (!"".equals(torrcFile)) { + try { + override = new Torrc(new FileInputStream(new File(torrcFile))); + } catch (IOException e) { + log.error("custom torrc file not found ('{}'). Proceeding with defaults.", torrcFile); + } + } + + // check if the user wants to temporarily add to the default torrc file + LinkedHashMap torrcOptionsMap = new LinkedHashMap<>(); + if (!"".equals(torrcOptions)) { + Arrays.asList(torrcOptions.split(",")).forEach(line -> { + line = line.trim(); + if (line.matches("^[^\\s]+\\s.+")) { + String[] tmp = line.split("\\s", 2); + torrcOptionsMap.put(tmp[0].trim(), tmp[1].trim()); + } else { + log.error("custom torrc override parse error ('{}'). Proceeding without custom overrides.", line); + torrcOptionsMap.clear(); + } + }); + } + + // assemble final override options + if (!torrcOptionsMap.isEmpty()) + // check for custom torrcFile + if (override != null) + // and merge the contents + override = new Torrc(override.getInputStream$tor_native(), torrcOptionsMap); + else + override = new Torrc(torrcOptionsMap); + + log.info("Starting tor"); + NativeTor result = new NativeTor(torWorkikngDirectory, bridgeEntries, override); + log.info( + "\n################################################################\n" + + "Tor started after {} ms. Start publishing hidden service.\n" + + "################################################################", + (new Date().getTime() - ts1)); // takes usually a few seconds + + return result; + } + + @Override + public String getHiddenServiceDirectory() { + return ""; + } + +} diff --git a/p2p/src/main/java/bisq/network/p2p/network/RunningTor.java b/p2p/src/main/java/bisq/network/p2p/network/RunningTor.java new file mode 100644 index 00000000000..62c5f43b10a --- /dev/null +++ b/p2p/src/main/java/bisq/network/p2p/network/RunningTor.java @@ -0,0 +1,87 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.network; + +import java.io.File; +import java.io.IOException; +import java.util.Date; + +import org.berndpruenster.netlayer.tor.ExternalTor; +import org.berndpruenster.netlayer.tor.Tor; +import org.berndpruenster.netlayer.tor.TorCtlException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class creates a brand new instance of the Tor onion router. + * + * When asked, the class checks for the authentication method selected and + * connects to the given control port. Finally, a {@link Tor} instance is + * returned for further use. + * + * @author Florian Reimair + * + */ +public class RunningTor extends TorMode { + + private static final Logger log = LoggerFactory.getLogger(RunningTor.class); + private final int controlPort; + private final String password; + private final String torDir; + private final File cookieFile; + private final boolean useSafeCookieAuthentication; + + + public RunningTor(final File torDir, final int controlPort, final String password, final String cookieFile, + final boolean useSafeCookieAuthentication) { + this.torDir = torDir.getAbsolutePath(); + this.controlPort = controlPort; + this.password = password; + this.cookieFile = new File(cookieFile); + this.useSafeCookieAuthentication = useSafeCookieAuthentication; + } + + @Override + public Tor getTor() throws IOException, TorCtlException { + long ts1 = new Date().getTime(); + + log.info("Connecting to running tor"); + + Tor result; + if (!password.isEmpty()) + result = new ExternalTor(controlPort, password); + else if (cookieFile.exists()) + result = new ExternalTor(controlPort, cookieFile, useSafeCookieAuthentication); + else + result = new ExternalTor(controlPort); + + log.info( + "\n################################################################\n" + + "Tor started after {} ms. Start publishing hidden service.\n" + + "################################################################", + (new Date().getTime() - ts1)); // takes usually a few seconds + + return result; + } + + @Override + public String getHiddenServiceDirectory() { + return torDir + File.separator + "externalTorHiddenService"; + } + +} diff --git a/p2p/src/main/java/bisq/network/p2p/network/TorMode.java b/p2p/src/main/java/bisq/network/p2p/network/TorMode.java new file mode 100644 index 00000000000..594312d51fc --- /dev/null +++ b/p2p/src/main/java/bisq/network/p2p/network/TorMode.java @@ -0,0 +1,60 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.network; + +import java.io.File; +import java.io.IOException; + +import org.berndpruenster.netlayer.tor.Tor; +import org.berndpruenster.netlayer.tor.TorCtlException; + +/** + * Holds information on how tor should be created and delivers a respective + * {@link Tor} object when asked. + * + * @author Florian Reimair + * + */ +public abstract class TorMode { + + /** + * Returns a fresh {@link Tor} object. + * + * @param torDir points to the place, where we will persist private key and + * address data + * @return a fresh instance of {@link Tor} + * @throws IOException + * @throws TorCtlException + */ + public abstract Tor getTor() throws IOException, TorCtlException; + + /** + * {@link NativeTor}'s inner workings prepend its Tor installation path and some + * other stuff to the hiddenServiceDir, thus, selecting nothing (i.e. + * "") as a hidden service directory is fine. {@link ExternalTor}, + * however, does not have a Tor installation path and thus, takes the hidden + * service path literally. Hence, we set + * "torDir/ephemeralHiddenService" as the hidden service directory. + * + * @return "" in {@link NewTor} Mode, + * "torDir/ephemeralHiddenService" in {@link RunningTor} + * mode + */ + public abstract String getHiddenServiceDirectory(); + +} diff --git a/p2p/src/main/java/bisq/network/p2p/network/TorNetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/TorNetworkNode.java index 1ea39456184..940eec53176 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/TorNetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/TorNetworkNode.java @@ -28,11 +28,9 @@ import bisq.common.util.Utilities; import org.berndpruenster.netlayer.tor.HiddenServiceSocket; -import org.berndpruenster.netlayer.tor.NativeTor; import org.berndpruenster.netlayer.tor.Tor; import org.berndpruenster.netlayer.tor.TorCtlException; import org.berndpruenster.netlayer.tor.TorSocket; -import org.berndpruenster.netlayer.tor.Torrc; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; @@ -52,11 +50,8 @@ import java.nio.file.Paths; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.util.Arrays; import java.util.Date; -import java.util.LinkedHashMap; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -80,27 +75,22 @@ public class TorNetworkNode extends NetworkNode { private HiddenServiceSocket hiddenServiceSocket; private final File torDir; - private final BridgeAddressProvider bridgeAddressProvider; private Timer shutDownTimeoutTimer; private int restartCounter; @SuppressWarnings("FieldCanBeLocal") private MonadicBinding allShutDown; private Tor tor; - private String torrcFile = ""; - private String torrcOptions = ""; - + private TorMode torMode; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - public TorNetworkNode(int servicePort, File torDir, NetworkProtoResolver networkProtoResolver, BridgeAddressProvider bridgeAddressProvider, String torrcFile, String torrcOptions) { + public TorNetworkNode(int servicePort, File torDir, NetworkProtoResolver networkProtoResolver, TorMode torMode) { super(servicePort, networkProtoResolver); this.torDir = torDir; - this.bridgeAddressProvider = bridgeAddressProvider; - this.torrcFile = torrcFile; - this.torrcOptions = torrcOptions; + this.torMode = torMode; } @@ -119,7 +109,7 @@ public void start(@Nullable SetupListener setupListener) { createExecutorService(); // Create the tor node (takes about 6 sec.) - createTorAndHiddenService(torDir, Utils.findFreeSystemPort(), servicePort, bridgeAddressProvider.getBridgeAddresses()); + createTorAndHiddenService(Utils.findFreeSystemPort(), servicePort); } @Override @@ -244,62 +234,18 @@ private void restartTor(String errorMessage) { // create tor /////////////////////////////////////////////////////////////////////////////////////////// - private void createTorAndHiddenService(File torDir, int localPort, int servicePort, @Nullable List bridgeEntries) { + private void createTorAndHiddenService(int localPort, int servicePort) { Log.traceCall(); - if (bridgeEntries != null) - log.info("Using bridges: {}", bridgeEntries.stream().collect(Collectors.joining(","))); ListenableFuture future = executorService.submit(() -> { try { - long ts1 = new Date().getTime(); - - Torrc override = null; - - // check if the user wants to provide his own torrc file - if(!"".equals(torrcFile)) { - try { - override = new Torrc(new FileInputStream(new File(torrcFile))); - } catch(IOException e) { - log.error("custom torrc file not found ('{}'). Proceeding with defaults.", torrcFile); - } - } - - // check if the user wants to temporarily add to the default torrc file - LinkedHashMap torrcOptionsMap = new LinkedHashMap<>(); - if(!"".equals(torrcOptions)) { - Arrays.asList(torrcOptions.split(",")).forEach(line -> { - line = line.trim(); - if(line.matches("^[^\\s]+\\s.+")) { - String[] tmp = line.split("\\s", 2); - torrcOptionsMap.put(tmp[0].trim(), tmp[1].trim()); - } - else { - log.error("custom torrc override parse error ('{}'). Proceeding without custom overrides.", line); - torrcOptionsMap.clear(); - } - }); - } - - // assemble final override options - if(!torrcOptionsMap.isEmpty()) - // check for custom torrcFile - if(override != null) - // and merge the contents - override = new Torrc(override.getInputStream$tor(), torrcOptionsMap); - else - override = new Torrc(torrcOptionsMap); - - log.info("Starting tor"); - Tor.setDefault(new NativeTor(torDir, bridgeEntries, override)); - log.info("\n################################################################\n" + - "Tor started after {} ms. Start publishing hidden service.\n" + - "################################################################", - (new Date().getTime() - ts1)); // takes usually a few seconds - + // get tor + Tor.setDefault(torMode.getTor()); UserThread.execute(() -> setupListeners.stream().forEach(SetupListener::onTorNodeReady)); + // start hidden service long ts2 = new Date().getTime(); - hiddenServiceSocket = new HiddenServiceSocket(localPort, "", servicePort); + hiddenServiceSocket = new HiddenServiceSocket(localPort, torMode.getHiddenServiceDirectory(), servicePort); hiddenServiceSocket.addReadyListener(socket -> { try { log.info("\n################################################################\n" + @@ -330,6 +276,14 @@ public void run() { } catch (TorCtlException e) { log.error("Tor node creation failed: " + (e.getCause() != null ? e.getCause().toString() : e.toString())); restartTor(e.getMessage()); + } catch (IOException e) { + log.error("Could not connect to running Tor: " + + e.getMessage()); + + // Seems a bit harsh, but since we cannot connect to Tor, we cannot do nothing + // furthermore, we have no hidden services started yet, so there is no graceful + // shutdown needed either + System.exit(1); } catch (Throwable ignore) { } diff --git a/p2p/src/test/java/bisq/network/p2p/network/TorNetworkNodeTest.java b/p2p/src/test/java/bisq/network/p2p/network/TorNetworkNodeTest.java index 720c5ceb323..8d29cc3d623 100644 --- a/p2p/src/test/java/bisq/network/p2p/network/TorNetworkNodeTest.java +++ b/p2p/src/test/java/bisq/network/p2p/network/TorNetworkNodeTest.java @@ -26,7 +26,7 @@ import java.io.File; import java.io.IOException; - +import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import org.slf4j.Logger; @@ -53,7 +53,9 @@ public class TorNetworkNodeTest { public void testTorNodeBeforeSecondReady() throws InterruptedException, IOException { latch = new CountDownLatch(1); int port = 9001; - TorNetworkNode node1 = new TorNetworkNode(port, new File("torNode_" + port), TestUtils.getNetworkProtoResolver(), null, "", ""); + TorNetworkNode node1 = new TorNetworkNode(port, new File("torNode_" + port), + TestUtils.getNetworkProtoResolver(), + new NewTor(new File("torNode_" + port), "", "", new ArrayList())); node1.start(new SetupListener() { @Override public void onTorNodeReady() { @@ -79,7 +81,9 @@ public void onRequestCustomBridges() { latch = new CountDownLatch(1); int port2 = 9002; - TorNetworkNode node2 = new TorNetworkNode(port2, new File("torNode_" + port2), TestUtils.getNetworkProtoResolver(), null, "", ""); + TorNetworkNode node2 = new TorNetworkNode(port2, new File("torNode_" + port2), + TestUtils.getNetworkProtoResolver(), + new NewTor(new File("torNode_" + port), "", "", new ArrayList())); node2.start(new SetupListener() { @Override public void onTorNodeReady() { @@ -136,7 +140,9 @@ public void onFailure(@NotNull Throwable throwable) { public void testTorNodeAfterBothReady() throws InterruptedException, IOException { latch = new CountDownLatch(2); int port = 9001; - TorNetworkNode node1 = new TorNetworkNode(port, new File("torNode_" + port), TestUtils.getNetworkProtoResolver(), null, "", ""); + TorNetworkNode node1 = new TorNetworkNode(port, new File("torNode_" + port), + TestUtils.getNetworkProtoResolver(), + new NewTor(new File("torNode_" + port), "", "", new ArrayList())); node1.start(new SetupListener() { @Override public void onTorNodeReady() { @@ -161,7 +167,9 @@ public void onRequestCustomBridges() { }); int port2 = 9002; - TorNetworkNode node2 = new TorNetworkNode(port2, new File("torNode_" + port), TestUtils.getNetworkProtoResolver(), null, "", ""); + TorNetworkNode node2 = new TorNetworkNode(port2, new File("torNode_" + port), + TestUtils.getNetworkProtoResolver(), + new NewTor(new File("torNode_" + port), "", "", new ArrayList())); node2.start(new SetupListener() { @Override public void onTorNodeReady() {