Skip to content

Instantly share code, notes, and snippets.

@fa2a5qj3
Last active July 16, 2024 21:33
Show Gist options
  • Save fa2a5qj3/57219580e7ce746aa45ac6edf4682238 to your computer and use it in GitHub Desktop.
Save fa2a5qj3/57219580e7ce746aa45ac6edf4682238 to your computer and use it in GitHub Desktop.
seednode configuration issue

Netlayer was architected to create on-the-fly "ephemeral" hidden services by programming the control port. [ref JesusMcCloud/netlayer#7]

The advantage for the non-technical user, they do not need do anything to set up tor, its all taken care of by the app. Disadvantage, they cannot configure the hidden service via torrc (i.e. the problem we are facing).

Haveno has two modes in the P2P network layer, NewTor and RunningTor: NewTor is typically the casual user, tor binary is provided and invoked. RunningTor is typically the experienced service operator who has tor service already running.

Both modes currently create ephemeral hidden service.

RunningTor could be modified to just bind simply to a specified tor HS data port. I made an experimental patch which does this, instead of using Netlayer/Jtorctl libraries it talks directly to the tor data port using SOCKS5 and listens on the tor HS port for incoming messages. [note: hardcoded expectation that the tor data port is 9050] It does not use the control port at all, so it needs to be told what the HiddenService address and HS port is (the one configured in torrc via HiddenServiceDir & HiddenServicePort).

I set up my torrc per @BoldSuck instructions, with MetricsPort enabled, a hidden service set up, and IntroDoSDefense and PoWDefensesEnabled.

I check tor supports those options by checking tor --version and tor --list-modules

I start this seednode with the following commandline: ./haveno-seednode --hiddenServiceAddress=fesdzgb5avqounqhqveaysdsd4urwdreqt77tmsyicsrug35cpcwxzyd.onion --nodePort=8000

Then I connect to the seednode from haveno-desktop by specifying seedNodes=fesdzgb5avqounqhqveaysdsd4urwdreqt77tmsyicsrug35cpcwxzyd.onion:8000

I check the DoS stats by curl -s http://127.0.0.1:9035/metrics | grep -i dos

torrc:

RunAsDaemon 1
SOCKSPort 9050
#ControlPort 9051
Log notice syslog

CookieAuthentication 0
CookieAuthFileGroupReadable 1
DataDirectoryGroupReadable 1

SafeSocks 0
HiddenServiceStatistics 0
AvoidDiskWrites 1

MetricsPort 127.0.0.1:9035
MetricsPortPolicy accept 127.0.0.1

HiddenServiceDir /var/lib/tor/hidden_service
HiddenServicePort 8000 127.0.0.1:8000
#HiddenServicePort 8000 [::1]:8000

# HiddenService options are per onion service:
HiddenServiceEnableIntroDoSDefense 1
#HiddenServiceEnableIntroDoSBurstPerSec 200     # (Default: 200)
#HiddenServiceEnableIntroDoSRatePerSec 25       # (Default: 25)

HiddenServicePoWDefensesEnabled 1
#HiddenServicePoWQueueRate 250          # (Default: 250)
#HiddenServicePoWQueueBurst 2500        # (Default: 2500)
#CompiledProofOfWorkHash auto           # (Default: auto)
diff --git a/common/src/main/java/haveno/common/config/Config.java b/common/src/main/java/haveno/common/config/Config.java
index 9147d535..71f5436e 100644
--- a/common/src/main/java/haveno/common/config/Config.java
+++ b/common/src/main/java/haveno/common/config/Config.java
@@ -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";
@@ -151,6 +152,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 +288,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("placeholder.onion");
+
ArgumentAcceptingOptionSpec<Integer> walletRpcBindPortOpt =
parser.accepts(WALLET_RPC_BIND_PORT, "Port to bind the wallet RPC on")
.withRequiredArg()
@@ -670,6 +678,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);
diff --git a/p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java b/p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java
index 676db83f..a0548b6d 100644
--- a/p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java
+++ b/p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java
@@ -44,6 +44,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,
@@ -65,7 +66,7 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
password,
cookieFile,
useSafeCookieAuthentication);
- networkNode = new TorNetworkNode(port, networkProtoResolver, streamIsolation, torMode, banFilter, maxConnections, controlHost);
+ networkNode = new TorNetworkNode(hiddenServiceAddress, port, networkProtoResolver, streamIsolation, torMode, banFilter, maxConnections, controlHost);
}
}
diff --git a/p2p/src/main/java/haveno/network/p2p/P2PModule.java b/p2p/src/main/java/haveno/network/p2p/P2PModule.java
index 50ce7d56..b13c2ccb 100644
--- a/p2p/src/main/java/haveno/network/p2p/P2PModule.java
+++ b/p2p/src/main/java/haveno/network/p2p/P2PModule.java
@@ -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);
diff --git a/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNode.java b/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNode.java
index 58842ed1..5d4b2703 100644
--- a/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNode.java
+++ b/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNode.java
@@ -17,28 +17,23 @@
package haveno.network.p2p.network;
+import haveno.common.util.Hex;
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.net.InetAddress;
+import java.net.ServerSocket;
import java.io.IOException;
-import java.util.Base64;
+import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import lombok.extern.slf4j.Slf4j;
@@ -52,13 +47,11 @@ public class TorNetworkNode extends NetworkNode {
private static final long SHUT_DOWN_TIMEOUT = 2;
private final String torControlHost;
+ private final String serviceAddress;
- 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;
@@ -67,13 +60,14 @@ public class TorNetworkNode extends NetworkNode {
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
- public TorNetworkNode(int servicePort,
+ public TorNetworkNode(String hiddenServiceAddress, int servicePort,
NetworkProtoResolver networkProtoResolver,
boolean useStreamIsolation,
TorMode torMode,
@Nullable BanFilter banFilter,
int maxConnections, String torControlHost) {
super(servicePort, networkProtoResolver, banFilter, maxConnections);
+ this.serviceAddress = hiddenServiceAddress;
this.torMode = torMode;
this.streamIsolation = useStreamIsolation;
this.torControlHost = torControlHost;
@@ -92,33 +86,50 @@ public class TorNetworkNode extends NetworkNode {
if (setupListener != null)
addSetupListener(setupListener);
- createTorAndHiddenService(Utils.findFreeSystemPort(), servicePort);
+ createTorAndHiddenService(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);
+ // 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(), 9050);
+ 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");
}
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);
+ Socks5Proxy prox = new Socks5Proxy(InetAddress.getLoopbackAddress(), 9050);
+ prox.resolveAddrLocally(false);
+ return prox;
+ } catch (Exception e) {
+ log.warn(e.toString());
return null;
}
}
@@ -145,12 +156,6 @@ public class TorNetworkNode extends NetworkNode {
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);
@@ -166,36 +171,23 @@ public class TorNetworkNode extends NetworkNode {
// Create tor and hidden service
///////////////////////////////////////////////////////////////////////////////////////////
- private void createTorAndHiddenService(int localPort, int servicePort) {
+ private void createTorAndHiddenService(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()));
+ // 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));
- 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);
- }
+ 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 running Tor", e);
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
diff --git a/p2p/src/test/java/haveno/network/p2p/network/TorNetworkNodeTest.java b/p2p/src/test/java/haveno/network/p2p/network/TorNetworkNodeTest.java
index f74a9d34..fda93464 100644
--- a/p2p/src/test/java/haveno/network/p2p/network/TorNetworkNodeTest.java
+++ b/p2p/src/test/java/haveno/network/p2p/network/TorNetworkNodeTest.java
@@ -50,7 +50,7 @@ public class TorNetworkNodeTest {
public void testTorNodeBeforeSecondReady() throws InterruptedException, IOException {
latch = new CountDownLatch(1);
int port = 9001;
- TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false,
+ TorNetworkNode node1 = new TorNetworkNode("serviceAddress", port, TestUtils.getNetworkProtoResolver(), false,
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
node1.start(new SetupListener() {
@Override
@@ -77,7 +77,7 @@ public class TorNetworkNodeTest {
latch = new CountDownLatch(1);
int port2 = 9002;
- TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false,
+ TorNetworkNode node2 = new TorNetworkNode("serviceAddress", port2, TestUtils.getNetworkProtoResolver(), false,
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
node2.start(new SetupListener() {
@Override
@@ -135,7 +135,7 @@ public class TorNetworkNodeTest {
public void testTorNodeAfterBothReady() throws InterruptedException, IOException {
latch = new CountDownLatch(2);
int port = 9001;
- TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false,
+ TorNetworkNode node1 = new TorNetworkNode("serviceAddress", port, TestUtils.getNetworkProtoResolver(), false,
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
node1.start(new SetupListener() {
@Override
@@ -161,7 +161,7 @@ public class TorNetworkNodeTest {
});
int port2 = 9002;
- TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false,
+ TorNetworkNode node2 = new TorNetworkNode("serviceAddress", port2, TestUtils.getNetworkProtoResolver(), false,
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
node2.start(new SetupListener() {
@Override
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment