Skip to content

Commit

Permalink
Support tor w/either Netlayer or direct bind to SOCKS5 data port
Browse files Browse the repository at this point in the history
  • Loading branch information
fa2a5qj3 authored Jul 29, 2024
1 parent 84a2828 commit 3d44f37
Show file tree
Hide file tree
Showing 8 changed files with 377 additions and 154 deletions.
10 changes: 10 additions & 0 deletions common/src/main/java/haveno/common/config/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public class Config {
public static final String SEED_NODES = "seedNodes";
public static final String BAN_LIST = "banList";
public static final String NODE_PORT = "nodePort";
public static final String HIDDEN_SERVICE_ADDRESS = "hiddenServiceAddress";
public static final String USE_LOCALHOST_FOR_P2P = "useLocalhostForP2P";
public static final String MAX_CONNECTIONS = "maxConnections";
public static final String SOCKS_5_PROXY_XMR_ADDRESS = "socks5ProxyXmrAddress";
Expand Down Expand Up @@ -122,6 +123,7 @@ public class Config {
public static final String DEFAULT_REGTEST_HOST = "none";
public static final int DEFAULT_NUM_CONNECTIONS_FOR_BTC = 9; // down from BitcoinJ default of 12
static final String DEFAULT_CONFIG_FILE_NAME = "haveno.properties";
public static final String UNSPECIFIED_HIDDENSERVICE_ADDRESS = "placeholder.onion";

// Static fields that provide access to Config properties in locations where injecting
// a Config instance is not feasible. See Javadoc for corresponding static accessors.
Expand Down Expand Up @@ -151,6 +153,7 @@ public enum UseTorForXmr {
public final File appDataDir;
public final int walletRpcBindPort;
public final int nodePort;
public final String hiddenServiceAddress;
public final int maxMemory;
public final String logLevel;
public final List<String> bannedXmrNodes;
Expand Down Expand Up @@ -286,6 +289,12 @@ public Config(String defaultAppName, File defaultUserDataDir, String... args) {
.ofType(Integer.class)
.defaultsTo(9999);

ArgumentAcceptingOptionSpec<String> hiddenServiceAddressOpt =
parser.accepts(HIDDEN_SERVICE_ADDRESS, "Hidden Service Address to listen on")
.withRequiredArg()
.ofType(String.class)
.defaultsTo(UNSPECIFIED_HIDDENSERVICE_ADDRESS);

ArgumentAcceptingOptionSpec<Integer> walletRpcBindPortOpt =
parser.accepts(WALLET_RPC_BIND_PORT, "Port to bind the wallet RPC on")
.withRequiredArg()
Expand Down Expand Up @@ -670,6 +679,7 @@ public Config(String defaultAppName, File defaultUserDataDir, String... args) {
this.helpRequested = options.has(helpOpt);
this.configFile = configFile;
this.nodePort = options.valueOf(nodePortOpt);
this.hiddenServiceAddress = options.valueOf(hiddenServiceAddressOpt);
this.walletRpcBindPort = options.valueOf(walletRpcBindPortOpt);
this.maxMemory = options.valueOf(maxMemoryOpt);
this.logLevel = options.valueOf(logLevelOpt);
Expand Down
25 changes: 20 additions & 5 deletions p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
import haveno.network.p2p.network.NetworkNode;
import haveno.network.p2p.network.NewTor;
import haveno.network.p2p.network.RunningTor;
import haveno.network.p2p.network.DirectBindTor;
import haveno.network.p2p.network.TorMode;
import haveno.network.p2p.network.TorNetworkNode;
import haveno.network.p2p.network.TorNetworkNodeDirectBind;
import haveno.network.p2p.network.TorNetworkNodeNetlayer;
import java.io.File;
import javax.annotation.Nullable;

Expand All @@ -44,6 +46,7 @@ public NetworkNodeProvider(NetworkProtoResolver networkProtoResolver,
@Named(Config.MAX_CONNECTIONS) int maxConnections,
@Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P,
@Named(Config.NODE_PORT) int port,
@Named(Config.HIDDEN_SERVICE_ADDRESS) String hiddenServiceAddress,
@Named(Config.TOR_DIR) File torDir,
@Nullable @Named(Config.TORRC_FILE) File torrcFile,
@Named(Config.TORRC_OPTIONS) String torrcOptions,
Expand All @@ -62,10 +65,15 @@ public NetworkNodeProvider(NetworkProtoResolver networkProtoResolver,
torrcOptions,
controlHost,
controlPort,
hiddenServiceAddress,
password,
cookieFile,
useSafeCookieAuthentication);
networkNode = new TorNetworkNode(port, networkProtoResolver, streamIsolation, torMode, banFilter, maxConnections, controlHost);
if (torMode instanceof NewTor || torMode instanceof RunningTor) {
networkNode = new TorNetworkNodeNetlayer(port, networkProtoResolver, torMode, banFilter, maxConnections, streamIsolation, controlHost);
} else {
networkNode = new TorNetworkNodeDirectBind(port, networkProtoResolver, banFilter, maxConnections, hiddenServiceAddress);
}
}
}

Expand All @@ -75,12 +83,19 @@ private TorMode getTorMode(BridgeAddressProvider bridgeAddressProvider,
String torrcOptions,
String controlHost,
int controlPort,
String hiddenServiceAddress,
String password,
@Nullable File cookieFile,
boolean useSafeCookieAuthentication) {
return controlPort != Config.UNSPECIFIED_PORT ?
new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication) :
new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider);
if (!hiddenServiceAddress.equals(Config.UNSPECIFIED_HIDDENSERVICE_ADDRESS)) {
return new DirectBindTor();
}
else if (controlPort != Config.UNSPECIFIED_PORT) {
return new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication);
}
else {
return new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider);
}
}

@Override
Expand Down
2 changes: 2 additions & 0 deletions p2p/src/main/java/haveno/network/p2p/P2PModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import static haveno.common.config.Config.BAN_LIST;
import static haveno.common.config.Config.MAX_CONNECTIONS;
import static haveno.common.config.Config.NODE_PORT;
import static haveno.common.config.Config.HIDDEN_SERVICE_ADDRESS;
import static haveno.common.config.Config.REPUBLISH_MAILBOX_ENTRIES;
import static haveno.common.config.Config.SOCKS_5_PROXY_HTTP_ADDRESS;
import static haveno.common.config.Config.SOCKS_5_PROXY_XMR_ADDRESS;
Expand Down Expand Up @@ -87,6 +88,7 @@ protected void configure() {
bind(File.class).annotatedWith(named(TOR_DIR)).toInstance(config.torDir);

bind(int.class).annotatedWith(named(NODE_PORT)).toInstance(config.nodePort);
bind(String.class).annotatedWith(named(HIDDEN_SERVICE_ADDRESS)).toInstance(config.hiddenServiceAddress);

bindConstant().annotatedWith(named(MAX_CONNECTIONS)).to(config.maxConnections);

Expand Down
22 changes: 22 additions & 0 deletions p2p/src/main/java/haveno/network/p2p/network/DirectBindTor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package haveno.network.p2p.network;

import lombok.extern.slf4j.Slf4j;
import org.berndpruenster.netlayer.tor.Tor;

@Slf4j
public class DirectBindTor extends TorMode {

public DirectBindTor() {
super(null);
}

@Override
public Tor getTor() {
return null;
}

@Override
public String getHiddenServiceDirectory() {
return null;
}
}
150 changes: 9 additions & 141 deletions p2p/src/main/java/haveno/network/p2p/network/TorNetworkNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,66 +18,36 @@
package haveno.network.p2p.network;

import haveno.network.p2p.NodeAddress;
import haveno.network.utils.Utils;

import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.proto.network.NetworkProtoResolver;
import haveno.common.util.SingleThreadExecutorUtils;

import org.berndpruenster.netlayer.tor.HiddenServiceSocket;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import org.berndpruenster.netlayer.tor.TorSocket;

import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;

import java.security.SecureRandom;

import java.net.Socket;

import java.io.IOException;

import java.util.Base64;
import java.util.concurrent.ExecutorService;

import lombok.extern.slf4j.Slf4j;

import org.jetbrains.annotations.Nullable;

import static com.google.common.base.Preconditions.checkArgument;

@Slf4j
public class TorNetworkNode extends NetworkNode {
private static final long SHUT_DOWN_TIMEOUT = 2;

private final String torControlHost;
public abstract class TorNetworkNode extends NetworkNode {

private HiddenServiceSocket hiddenServiceSocket;
private Timer shutDownTimeoutTimer;
private Tor tor;
private TorMode torMode;
private boolean streamIsolation;
private Socks5Proxy socksProxy;
private boolean shutDownInProgress;
private boolean shutDownComplete;
private final ExecutorService executor;
protected final ExecutorService executor;

///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////

public TorNetworkNode(int servicePort,
NetworkProtoResolver networkProtoResolver,
boolean useStreamIsolation,
TorMode torMode,
@Nullable BanFilter banFilter,
int maxConnections, String torControlHost) {
int maxConnections) {
super(servicePort, networkProtoResolver, banFilter, maxConnections);
this.torMode = torMode;
this.streamIsolation = useStreamIsolation;
this.torControlHost = torControlHost;

executor = SingleThreadExecutorUtils.getSingleThreadExecutor("StartTor");
}

Expand All @@ -87,121 +57,19 @@ public TorNetworkNode(int servicePort,

@Override
public void start(@Nullable SetupListener setupListener) {
torMode.doRollingBackup();

if (setupListener != null)
addSetupListener(setupListener);

createTorAndHiddenService(Utils.findFreeSystemPort(), servicePort);
}

@Override
protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException {
checkArgument(peerNodeAddress.getHostName().endsWith(".onion"), "PeerAddress is not an onion address");
// If streamId is null stream isolation gets deactivated.
// Hidden services use stream isolation by default, so we pass null.
return new TorSocket(peerNodeAddress.getHostName(), peerNodeAddress.getPort(), torControlHost, null);
}

public Socks5Proxy getSocksProxy() {
try {
String stream = null;
if (streamIsolation) {
byte[] bytes = new byte[512]; // tor.getProxy creates a Sha256 hash
new SecureRandom().nextBytes(bytes);
stream = Base64.getEncoder().encodeToString(bytes);
}

if (socksProxy == null || streamIsolation) {
tor = Tor.getDefault();
socksProxy = tor != null ? tor.getProxy(torControlHost, stream) : null;
}
return socksProxy;
} catch (Throwable t) {
log.error("Error at getSocksProxy", t);
return null;
}
createTorAndHiddenService();
}

public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
log.info("TorNetworkNode shutdown started");
if (shutDownComplete) {
log.info("TorNetworkNode shutdown already completed");
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
return;
}
if (shutDownInProgress) {
log.warn("Ignoring request to shut down because shut down is in progress");
return;
}
shutDownInProgress = true;

shutDownTimeoutTimer = UserThread.runAfter(() -> {
log.error("A timeout occurred at shutDown");
shutDownComplete = true;
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
executor.shutdownNow();
}, SHUT_DOWN_TIMEOUT);

super.shutDown(() -> {
try {
tor = Tor.getDefault();
if (tor != null) {
tor.shutdown();
tor = null;
log.info("Tor shutdown completed");
}
executor.shutdownNow();
} catch (Throwable e) {
log.error("Shutdown torNetworkNode failed with exception", e);
} finally {
shutDownTimeoutTimer.stop();
shutDownComplete = true;
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
}
});
super.shutDown(shutDownCompleteHandler);
}

///////////////////////////////////////////////////////////////////////////////////////////
// Create tor and hidden service
///////////////////////////////////////////////////////////////////////////////////////////
public abstract Socks5Proxy getSocksProxy();

private void createTorAndHiddenService(int localPort, int servicePort) {
executor.submit(() -> {
try {
Tor.setDefault(torMode.getTor());
long ts = System.currentTimeMillis();
hiddenServiceSocket = new HiddenServiceSocket(localPort, torMode.getHiddenServiceDirectory(), servicePort);
nodeAddressProperty.set(new NodeAddress(hiddenServiceSocket.getServiceName() + ":" + hiddenServiceSocket.getHiddenServicePort()));
UserThread.execute(() -> setupListeners.forEach(SetupListener::onTorNodeReady));
hiddenServiceSocket.addReadyListener(socket -> {
log.info("\n################################################################\n" +
"Tor hidden service published after {} ms. Socket={}\n" +
"################################################################",
System.currentTimeMillis() - ts, socket);
UserThread.execute(() -> {
nodeAddressProperty.set(new NodeAddress(hiddenServiceSocket.getServiceName() + ":"
+ hiddenServiceSocket.getHiddenServicePort()));
startServer(socket);
setupListeners.forEach(SetupListener::onHiddenServicePublished);
});
return null;
});
} catch (TorCtlException e) {
log.error("Starting tor node failed", e);
if (e.getCause() instanceof IOException) {
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
} else {
UserThread.execute(() -> setupListeners.forEach(SetupListener::onRequestCustomBridges));
log.warn("We shutdown as starting tor with the default bridges failed. We request user to add custom bridges.");
shutDown(null);
}
} catch (IOException e) {
log.error("Could not connect to running Tor", e);
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
} catch (Throwable ignore) {
}
return null;
});
}
protected abstract Socket createSocket(NodeAddress peerNodeAddress) throws IOException;

protected abstract void createTorAndHiddenService();
}
Loading

0 comments on commit 3d44f37

Please sign in to comment.