mirror of
https://github.com/boldsuck/haveno.git
synced 2024-12-22 20:19:21 +00:00
Support tor w/either Netlayer or direct bind to SOCKS5 data port
This commit is contained in:
parent
84a2828e90
commit
3d44f3777c
8 changed files with 377 additions and 154 deletions
|
@ -77,6 +77,7 @@ public class Config {
|
||||||
public static final String SEED_NODES = "seedNodes";
|
public static final String SEED_NODES = "seedNodes";
|
||||||
public static final String BAN_LIST = "banList";
|
public static final String BAN_LIST = "banList";
|
||||||
public static final String NODE_PORT = "nodePort";
|
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 USE_LOCALHOST_FOR_P2P = "useLocalhostForP2P";
|
||||||
public static final String MAX_CONNECTIONS = "maxConnections";
|
public static final String MAX_CONNECTIONS = "maxConnections";
|
||||||
public static final String SOCKS_5_PROXY_XMR_ADDRESS = "socks5ProxyXmrAddress";
|
public static final String SOCKS_5_PROXY_XMR_ADDRESS = "socks5ProxyXmrAddress";
|
||||||
|
@ -122,6 +123,7 @@ public class Config {
|
||||||
public static final String DEFAULT_REGTEST_HOST = "none";
|
public static final String DEFAULT_REGTEST_HOST = "none";
|
||||||
public static final int DEFAULT_NUM_CONNECTIONS_FOR_BTC = 9; // down from BitcoinJ default of 12
|
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";
|
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
|
// Static fields that provide access to Config properties in locations where injecting
|
||||||
// a Config instance is not feasible. See Javadoc for corresponding static accessors.
|
// a Config instance is not feasible. See Javadoc for corresponding static accessors.
|
||||||
|
@ -151,6 +153,7 @@ public class Config {
|
||||||
public final File appDataDir;
|
public final File appDataDir;
|
||||||
public final int walletRpcBindPort;
|
public final int walletRpcBindPort;
|
||||||
public final int nodePort;
|
public final int nodePort;
|
||||||
|
public final String hiddenServiceAddress;
|
||||||
public final int maxMemory;
|
public final int maxMemory;
|
||||||
public final String logLevel;
|
public final String logLevel;
|
||||||
public final List<String> bannedXmrNodes;
|
public final List<String> bannedXmrNodes;
|
||||||
|
@ -286,6 +289,12 @@ public class Config {
|
||||||
.ofType(Integer.class)
|
.ofType(Integer.class)
|
||||||
.defaultsTo(9999);
|
.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 =
|
ArgumentAcceptingOptionSpec<Integer> walletRpcBindPortOpt =
|
||||||
parser.accepts(WALLET_RPC_BIND_PORT, "Port to bind the wallet RPC on")
|
parser.accepts(WALLET_RPC_BIND_PORT, "Port to bind the wallet RPC on")
|
||||||
.withRequiredArg()
|
.withRequiredArg()
|
||||||
|
@ -670,6 +679,7 @@ public class Config {
|
||||||
this.helpRequested = options.has(helpOpt);
|
this.helpRequested = options.has(helpOpt);
|
||||||
this.configFile = configFile;
|
this.configFile = configFile;
|
||||||
this.nodePort = options.valueOf(nodePortOpt);
|
this.nodePort = options.valueOf(nodePortOpt);
|
||||||
|
this.hiddenServiceAddress = options.valueOf(hiddenServiceAddressOpt);
|
||||||
this.walletRpcBindPort = options.valueOf(walletRpcBindPortOpt);
|
this.walletRpcBindPort = options.valueOf(walletRpcBindPortOpt);
|
||||||
this.maxMemory = options.valueOf(maxMemoryOpt);
|
this.maxMemory = options.valueOf(maxMemoryOpt);
|
||||||
this.logLevel = options.valueOf(logLevelOpt);
|
this.logLevel = options.valueOf(logLevelOpt);
|
||||||
|
|
|
@ -28,8 +28,10 @@ import haveno.network.p2p.network.LocalhostNetworkNode;
|
||||||
import haveno.network.p2p.network.NetworkNode;
|
import haveno.network.p2p.network.NetworkNode;
|
||||||
import haveno.network.p2p.network.NewTor;
|
import haveno.network.p2p.network.NewTor;
|
||||||
import haveno.network.p2p.network.RunningTor;
|
import haveno.network.p2p.network.RunningTor;
|
||||||
|
import haveno.network.p2p.network.DirectBindTor;
|
||||||
import haveno.network.p2p.network.TorMode;
|
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 java.io.File;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
@ -44,6 +46,7 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
|
||||||
@Named(Config.MAX_CONNECTIONS) int maxConnections,
|
@Named(Config.MAX_CONNECTIONS) int maxConnections,
|
||||||
@Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P,
|
@Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P,
|
||||||
@Named(Config.NODE_PORT) int port,
|
@Named(Config.NODE_PORT) int port,
|
||||||
|
@Named(Config.HIDDEN_SERVICE_ADDRESS) String hiddenServiceAddress,
|
||||||
@Named(Config.TOR_DIR) File torDir,
|
@Named(Config.TOR_DIR) File torDir,
|
||||||
@Nullable @Named(Config.TORRC_FILE) File torrcFile,
|
@Nullable @Named(Config.TORRC_FILE) File torrcFile,
|
||||||
@Named(Config.TORRC_OPTIONS) String torrcOptions,
|
@Named(Config.TORRC_OPTIONS) String torrcOptions,
|
||||||
|
@ -62,10 +65,15 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
|
||||||
torrcOptions,
|
torrcOptions,
|
||||||
controlHost,
|
controlHost,
|
||||||
controlPort,
|
controlPort,
|
||||||
|
hiddenServiceAddress,
|
||||||
password,
|
password,
|
||||||
cookieFile,
|
cookieFile,
|
||||||
useSafeCookieAuthentication);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,12 +83,19 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
|
||||||
String torrcOptions,
|
String torrcOptions,
|
||||||
String controlHost,
|
String controlHost,
|
||||||
int controlPort,
|
int controlPort,
|
||||||
|
String hiddenServiceAddress,
|
||||||
String password,
|
String password,
|
||||||
@Nullable File cookieFile,
|
@Nullable File cookieFile,
|
||||||
boolean useSafeCookieAuthentication) {
|
boolean useSafeCookieAuthentication) {
|
||||||
return controlPort != Config.UNSPECIFIED_PORT ?
|
if (!hiddenServiceAddress.equals(Config.UNSPECIFIED_HIDDENSERVICE_ADDRESS)) {
|
||||||
new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication) :
|
return new DirectBindTor();
|
||||||
new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider);
|
}
|
||||||
|
else if (controlPort != Config.UNSPECIFIED_PORT) {
|
||||||
|
return new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -26,6 +26,7 @@ import haveno.common.config.Config;
|
||||||
import static haveno.common.config.Config.BAN_LIST;
|
import static haveno.common.config.Config.BAN_LIST;
|
||||||
import static haveno.common.config.Config.MAX_CONNECTIONS;
|
import static haveno.common.config.Config.MAX_CONNECTIONS;
|
||||||
import static haveno.common.config.Config.NODE_PORT;
|
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.REPUBLISH_MAILBOX_ENTRIES;
|
||||||
import static haveno.common.config.Config.SOCKS_5_PROXY_HTTP_ADDRESS;
|
import static haveno.common.config.Config.SOCKS_5_PROXY_HTTP_ADDRESS;
|
||||||
import static haveno.common.config.Config.SOCKS_5_PROXY_XMR_ADDRESS;
|
import static haveno.common.config.Config.SOCKS_5_PROXY_XMR_ADDRESS;
|
||||||
|
@ -87,6 +88,7 @@ public class P2PModule extends AppModule {
|
||||||
bind(File.class).annotatedWith(named(TOR_DIR)).toInstance(config.torDir);
|
bind(File.class).annotatedWith(named(TOR_DIR)).toInstance(config.torDir);
|
||||||
|
|
||||||
bind(int.class).annotatedWith(named(NODE_PORT)).toInstance(config.nodePort);
|
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);
|
bindConstant().annotatedWith(named(MAX_CONNECTIONS)).to(config.maxConnections);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,50 +18,26 @@
|
||||||
package haveno.network.p2p.network;
|
package haveno.network.p2p.network;
|
||||||
|
|
||||||
import haveno.network.p2p.NodeAddress;
|
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.proto.network.NetworkProtoResolver;
|
||||||
import haveno.common.util.SingleThreadExecutorUtils;
|
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 com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TorNetworkNode extends NetworkNode {
|
public abstract class TorNetworkNode extends NetworkNode {
|
||||||
private static final long SHUT_DOWN_TIMEOUT = 2;
|
|
||||||
|
|
||||||
private final String torControlHost;
|
protected final ExecutorService executor;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Constructor
|
// Constructor
|
||||||
|
@ -69,15 +45,9 @@ public class TorNetworkNode extends NetworkNode {
|
||||||
|
|
||||||
public TorNetworkNode(int servicePort,
|
public TorNetworkNode(int servicePort,
|
||||||
NetworkProtoResolver networkProtoResolver,
|
NetworkProtoResolver networkProtoResolver,
|
||||||
boolean useStreamIsolation,
|
|
||||||
TorMode torMode,
|
|
||||||
@Nullable BanFilter banFilter,
|
@Nullable BanFilter banFilter,
|
||||||
int maxConnections, String torControlHost) {
|
int maxConnections) {
|
||||||
super(servicePort, networkProtoResolver, banFilter, maxConnections);
|
super(servicePort, networkProtoResolver, banFilter, maxConnections);
|
||||||
this.torMode = torMode;
|
|
||||||
this.streamIsolation = useStreamIsolation;
|
|
||||||
this.torControlHost = torControlHost;
|
|
||||||
|
|
||||||
executor = SingleThreadExecutorUtils.getSingleThreadExecutor("StartTor");
|
executor = SingleThreadExecutorUtils.getSingleThreadExecutor("StartTor");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,121 +57,19 @@ public class TorNetworkNode extends NetworkNode {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(@Nullable SetupListener setupListener) {
|
public void start(@Nullable SetupListener setupListener) {
|
||||||
torMode.doRollingBackup();
|
|
||||||
|
|
||||||
if (setupListener != null)
|
if (setupListener != null)
|
||||||
addSetupListener(setupListener);
|
addSetupListener(setupListener);
|
||||||
|
|
||||||
createTorAndHiddenService(Utils.findFreeSystemPort(), servicePort);
|
createTorAndHiddenService();
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
|
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
|
||||||
log.info("TorNetworkNode shutdown started");
|
super.shutDown(shutDownCompleteHandler);
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
public abstract Socks5Proxy getSocksProxy();
|
||||||
// Create tor and hidden service
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
private void createTorAndHiddenService(int localPort, int servicePort) {
|
protected abstract Socket createSocket(NodeAddress peerNodeAddress) throws IOException;
|
||||||
executor.submit(() -> {
|
|
||||||
try {
|
protected abstract void createTorAndHiddenService();
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
package haveno.network.p2p.network;
|
||||||
|
|
||||||
|
import haveno.common.util.Hex;
|
||||||
|
import haveno.network.p2p.NodeAddress;
|
||||||
|
|
||||||
|
import haveno.common.UserThread;
|
||||||
|
import haveno.common.proto.network.NetworkProtoResolver;
|
||||||
|
|
||||||
|
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
|
||||||
|
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class TorNetworkNodeDirectBind extends TorNetworkNode {
|
||||||
|
|
||||||
|
private static final int TOR_DATA_PORT = 9050; // TODO: config option?
|
||||||
|
private final String serviceAddress;
|
||||||
|
|
||||||
|
public TorNetworkNodeDirectBind(int servicePort,
|
||||||
|
NetworkProtoResolver networkProtoResolver,
|
||||||
|
@Nullable BanFilter banFilter,
|
||||||
|
int maxConnections, String hiddenServiceAddress) {
|
||||||
|
super(servicePort, networkProtoResolver, banFilter, maxConnections);
|
||||||
|
this.serviceAddress = hiddenServiceAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
|
||||||
|
super.shutDown(() -> {
|
||||||
|
log.info("TorNetworkNode shutdown already completed");
|
||||||
|
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socks5Proxy getSocksProxy() {
|
||||||
|
Socks5Proxy proxy = new Socks5Proxy(InetAddress.getLoopbackAddress(), TOR_DATA_PORT);
|
||||||
|
proxy.resolveAddrLocally(false);
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException {
|
||||||
|
// https://www.ietf.org/rfc1928.txt SOCKS5 Protocol
|
||||||
|
try {
|
||||||
|
checkArgument(peerNodeAddress.getHostName().endsWith(".onion"), "PeerAddress is not an onion address");
|
||||||
|
Socket sock = new Socket(InetAddress.getLoopbackAddress(), TOR_DATA_PORT);
|
||||||
|
sock.getOutputStream().write(Hex.decode("050100"));
|
||||||
|
String response = Hex.encode(sock.getInputStream().readNBytes(2));
|
||||||
|
if (!response.equalsIgnoreCase("0500")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String connect_details = "050100033E" + Hex.encode(peerNodeAddress.getHostName().getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder connect_port = new StringBuilder(Integer.toHexString(peerNodeAddress.getPort()));
|
||||||
|
while (connect_port.length() < 4) connect_port.insert(0, "0");
|
||||||
|
connect_details = connect_details + connect_port;
|
||||||
|
sock.getOutputStream().write(Hex.decode(connect_details));
|
||||||
|
response = Hex.encode(sock.getInputStream().readNBytes(10));
|
||||||
|
if (response.substring(0, 2).equalsIgnoreCase("05") && response.substring(2, 4).equalsIgnoreCase("00")) {
|
||||||
|
return sock; // success
|
||||||
|
}
|
||||||
|
if (response.substring(2, 4).equalsIgnoreCase("04")) {
|
||||||
|
log.warn("Host unreachable: {}", peerNodeAddress);
|
||||||
|
} else {
|
||||||
|
log.warn("SOCKS error code received {} expected 00", response.substring(2, 4));
|
||||||
|
}
|
||||||
|
if (!response.substring(0, 2).equalsIgnoreCase("05")) {
|
||||||
|
log.warn("unexpected response, this isn't a SOCKS5 proxy?: {} {}", response, response.substring(0, 2));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(e.toString());
|
||||||
|
}
|
||||||
|
throw new IOException("createSocket failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createTorAndHiddenService() {
|
||||||
|
executor.submit(() -> {
|
||||||
|
try {
|
||||||
|
// listener for incoming messages at the hidden service
|
||||||
|
ServerSocket socket = new ServerSocket(servicePort);
|
||||||
|
nodeAddressProperty.set(new NodeAddress(serviceAddress + ":" + servicePort));
|
||||||
|
log.info("\n################################################################\n" +
|
||||||
|
"Tor hidden service published: {} Port: {}\n" +
|
||||||
|
"################################################################",
|
||||||
|
serviceAddress, servicePort);
|
||||||
|
UserThread.execute(() -> setupListeners.forEach(SetupListener::onTorNodeReady));
|
||||||
|
UserThread.runAfter(() -> {
|
||||||
|
nodeAddressProperty.set(new NodeAddress(serviceAddress + ":" + servicePort));
|
||||||
|
startServer(socket);
|
||||||
|
setupListeners.forEach(SetupListener::onHiddenServicePublished);
|
||||||
|
}, 3);
|
||||||
|
return null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Could not connect to external Tor", e);
|
||||||
|
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
package haveno.network.p2p.network;
|
||||||
|
|
||||||
|
import haveno.common.Timer;
|
||||||
|
import haveno.network.p2p.NodeAddress;
|
||||||
|
|
||||||
|
import haveno.common.UserThread;
|
||||||
|
import haveno.common.proto.network.NetworkProtoResolver;
|
||||||
|
|
||||||
|
import haveno.network.utils.Utils;
|
||||||
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class TorNetworkNodeNetlayer extends TorNetworkNode {
|
||||||
|
|
||||||
|
private static final long SHUT_DOWN_TIMEOUT = 2;
|
||||||
|
|
||||||
|
private HiddenServiceSocket hiddenServiceSocket;
|
||||||
|
private boolean streamIsolation;
|
||||||
|
private Socks5Proxy socksProxy;
|
||||||
|
protected TorMode torMode;
|
||||||
|
private Tor tor;
|
||||||
|
private final String torControlHost;
|
||||||
|
private Timer shutDownTimeoutTimer;
|
||||||
|
private boolean shutDownInProgress;
|
||||||
|
private boolean shutDownComplete;
|
||||||
|
|
||||||
|
public TorNetworkNodeNetlayer(int servicePort,
|
||||||
|
NetworkProtoResolver networkProtoResolver,
|
||||||
|
TorMode torMode,
|
||||||
|
@Nullable BanFilter banFilter,
|
||||||
|
int maxConnections,
|
||||||
|
boolean useStreamIsolation,
|
||||||
|
String torControlHost) {
|
||||||
|
super(servicePort, networkProtoResolver, banFilter, maxConnections);
|
||||||
|
this.torControlHost = torControlHost;
|
||||||
|
this.streamIsolation = useStreamIsolation;
|
||||||
|
this.torMode = torMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(@Nullable SetupListener setupListener) {
|
||||||
|
torMode.doRollingBackup();
|
||||||
|
super.start(setupListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createTorAndHiddenService() {
|
||||||
|
int localPort = Utils.findFreeSystemPort();
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,8 +50,13 @@ public class TorNetworkNodeTest {
|
||||||
public void testTorNodeBeforeSecondReady() throws InterruptedException, IOException {
|
public void testTorNodeBeforeSecondReady() throws InterruptedException, IOException {
|
||||||
latch = new CountDownLatch(1);
|
latch = new CountDownLatch(1);
|
||||||
int port = 9001;
|
int port = 9001;
|
||||||
TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false,
|
TorNetworkNode node1 = new TorNetworkNodeNetlayer(port,
|
||||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
TestUtils.getNetworkProtoResolver(),
|
||||||
|
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||||
|
null,
|
||||||
|
12,
|
||||||
|
false,
|
||||||
|
"127.0.0.1");
|
||||||
node1.start(new SetupListener() {
|
node1.start(new SetupListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTorNodeReady() {
|
public void onTorNodeReady() {
|
||||||
|
@ -77,8 +82,13 @@ public class TorNetworkNodeTest {
|
||||||
|
|
||||||
latch = new CountDownLatch(1);
|
latch = new CountDownLatch(1);
|
||||||
int port2 = 9002;
|
int port2 = 9002;
|
||||||
TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false,
|
TorNetworkNode node2 = new TorNetworkNodeNetlayer(port2,
|
||||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
TestUtils.getNetworkProtoResolver(),
|
||||||
|
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||||
|
null,
|
||||||
|
12,
|
||||||
|
false,
|
||||||
|
"127.0.0.1");
|
||||||
node2.start(new SetupListener() {
|
node2.start(new SetupListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTorNodeReady() {
|
public void onTorNodeReady() {
|
||||||
|
@ -135,8 +145,13 @@ public class TorNetworkNodeTest {
|
||||||
public void testTorNodeAfterBothReady() throws InterruptedException, IOException {
|
public void testTorNodeAfterBothReady() throws InterruptedException, IOException {
|
||||||
latch = new CountDownLatch(2);
|
latch = new CountDownLatch(2);
|
||||||
int port = 9001;
|
int port = 9001;
|
||||||
TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false,
|
TorNetworkNode node1 = new TorNetworkNodeNetlayer(port,
|
||||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
TestUtils.getNetworkProtoResolver(),
|
||||||
|
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||||
|
null,
|
||||||
|
12,
|
||||||
|
false,
|
||||||
|
"127.0.0.1");
|
||||||
node1.start(new SetupListener() {
|
node1.start(new SetupListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTorNodeReady() {
|
public void onTorNodeReady() {
|
||||||
|
@ -161,8 +176,12 @@ public class TorNetworkNodeTest {
|
||||||
});
|
});
|
||||||
|
|
||||||
int port2 = 9002;
|
int port2 = 9002;
|
||||||
TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false,
|
TorNetworkNode node2 = new TorNetworkNodeNetlayer(port2, TestUtils.getNetworkProtoResolver(),
|
||||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||||
|
null,
|
||||||
|
12,
|
||||||
|
false,
|
||||||
|
"127.0.0.1");
|
||||||
node2.start(new SetupListener() {
|
node2.start(new SetupListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTorNodeReady() {
|
public void onTorNodeReady() {
|
||||||
|
|
Loading…
Reference in a new issue