mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-11-16 15:58:08 +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 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";
|
||||
|
@ -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.
|
||||
|
@ -151,6 +153,7 @@ public class Config {
|
|||
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;
|
||||
|
@ -286,6 +289,12 @@ public class Config {
|
|||
.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()
|
||||
|
@ -670,6 +679,7 @@ public class Config {
|
|||
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);
|
||||
|
|
|
@ -28,8 +28,10 @@ import haveno.network.p2p.network.LocalhostNetworkNode;
|
|||
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;
|
||||
|
||||
|
@ -44,6 +46,7 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
|
|||
@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,
|
||||
|
@ -62,10 +65,15 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,12 +83,19 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
|
|||
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
|
||||
|
|
|
@ -26,6 +26,7 @@ import haveno.common.config.Config;
|
|||
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;
|
||||
|
@ -87,6 +88,7 @@ public class P2PModule extends AppModule {
|
|||
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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
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;
|
||||
public abstract class TorNetworkNode extends NetworkNode {
|
||||
|
||||
private final String torControlHost;
|
||||
|
||||
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
|
||||
|
@ -69,15 +45,9 @@ public class TorNetworkNode extends NetworkNode {
|
|||
|
||||
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");
|
||||
}
|
||||
|
||||
|
@ -87,121 +57,19 @@ public class TorNetworkNode extends NetworkNode {
|
|||
|
||||
@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();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
latch = new CountDownLatch(1);
|
||||
int port = 9001;
|
||||
TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false,
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
||||
TorNetworkNode node1 = new TorNetworkNodeNetlayer(port,
|
||||
TestUtils.getNetworkProtoResolver(),
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||
null,
|
||||
12,
|
||||
false,
|
||||
"127.0.0.1");
|
||||
node1.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
|
@ -77,8 +82,13 @@ public class TorNetworkNodeTest {
|
|||
|
||||
latch = new CountDownLatch(1);
|
||||
int port2 = 9002;
|
||||
TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false,
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
||||
TorNetworkNode node2 = new TorNetworkNodeNetlayer(port2,
|
||||
TestUtils.getNetworkProtoResolver(),
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||
null,
|
||||
12,
|
||||
false,
|
||||
"127.0.0.1");
|
||||
node2.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
|
@ -135,8 +145,13 @@ public class TorNetworkNodeTest {
|
|||
public void testTorNodeAfterBothReady() throws InterruptedException, IOException {
|
||||
latch = new CountDownLatch(2);
|
||||
int port = 9001;
|
||||
TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false,
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
||||
TorNetworkNode node1 = new TorNetworkNodeNetlayer(port,
|
||||
TestUtils.getNetworkProtoResolver(),
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||
null,
|
||||
12,
|
||||
false,
|
||||
"127.0.0.1");
|
||||
node1.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
|
@ -161,8 +176,12 @@ public class TorNetworkNodeTest {
|
|||
});
|
||||
|
||||
int port2 = 9002;
|
||||
TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false,
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
||||
TorNetworkNode node2 = new TorNetworkNodeNetlayer(port2, TestUtils.getNetworkProtoResolver(),
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||
null,
|
||||
12,
|
||||
false,
|
||||
"127.0.0.1");
|
||||
node2.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
|
|
Loading…
Reference in a new issue