diff --git a/network/tor-local-network/src/integrationTest/java/bisq/tor/local_network/PrivateTorNetworkTests.java b/network/tor-local-network/src/integrationTest/java/bisq/tor/local_network/PrivateTorNetworkTests.java
new file mode 100644
index 0000000000..f4c3f35af3
--- /dev/null
+++ b/network/tor-local-network/src/integrationTest/java/bisq/tor/local_network/PrivateTorNetworkTests.java
@@ -0,0 +1,47 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.tor.local_network;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+public class PrivateTorNetworkTests {
+ private static final String PASSPHRASE = "my_passphrase";
+
+ @Test
+ public void createTestNetwork(@TempDir Path tempDir) throws IOException, InterruptedException {
+ new TorNetwork(tempDir)
+ .addDirAuth(PASSPHRASE)
+ .addDirAuth(PASSPHRASE)
+ .addDirAuth(PASSPHRASE)
+
+ .addRelay()
+ .addRelay()
+ .addRelay()
+ .addRelay()
+ .addRelay()
+
+ .addClient()
+ .addClient()
+
+ .start();
+ }
+}
diff --git a/network/tor-local-network/src/main/java/bisq/tor/local_network/TorNetwork.java b/network/tor-local-network/src/main/java/bisq/tor/local_network/TorNetwork.java
new file mode 100644
index 0000000000..5085aac577
--- /dev/null
+++ b/network/tor-local-network/src/main/java/bisq/tor/local_network/TorNetwork.java
@@ -0,0 +1,171 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.tor.local_network;
+
+import bisq.common.util.NetworkUtils;
+import bisq.tor.local_network.da.DirectoryAuthorityFactory;
+import bisq.tor.local_network.torrc.ClientTorrcGenerator;
+import bisq.tor.local_network.torrc.DirectoryAuthorityTorrcGenerator;
+import bisq.tor.local_network.torrc.RelayTorrcGenerator;
+import bisq.tor.local_network.torrc.TorrcFileGenerator;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+
+public class TorNetwork {
+
+ private final Path rootDataDir;
+ private final DirectoryAuthorityFactory dirAuthFactory = new DirectoryAuthorityFactory();
+ private final Set directoryAuthorities = new HashSet<>();
+ private final Set relays = new HashSet<>();
+ private final Set clients = new HashSet<>();
+
+ private final Set allTorProcesses = new HashSet<>();
+
+ private int dirAuthIndex;
+ private int relayIndex;
+ private int clientIndex;
+
+ public TorNetwork(Path rootDataDir) {
+ this.rootDataDir = rootDataDir;
+ }
+
+ public TorNetwork addDirAuth(String passphrase) throws IOException, InterruptedException {
+ String nickname = "da" + dirAuthIndex++;
+
+ Path nodeDataDir = rootDataDir.resolve(nickname);
+ boolean isSuccess = nodeDataDir.toFile().mkdir();
+ if (!isSuccess) {
+ throw new IllegalStateException("Couldn't create data directory for " + nickname);
+ }
+
+ var dirAuth = TorNode.builder()
+ .type(TorNode.Type.DIRECTORY_AUTHORITY)
+ .nickname(nickname)
+ .dataDir(nodeDataDir)
+ .controlPort(NetworkUtils.findFreeSystemPort())
+ .orPort(NetworkUtils.findFreeSystemPort())
+ .dirPort(NetworkUtils.findFreeSystemPort())
+ .build();
+ dirAuthFactory.createDirectoryAuthority(dirAuth, passphrase);
+ directoryAuthorities.add(dirAuth);
+
+ return this;
+ }
+
+ public TorNetwork addRelay() {
+ String nickname = "relay" + relayIndex++;
+
+ Path nodeDataDir = rootDataDir.resolve(nickname);
+ boolean isSuccess = nodeDataDir.toFile().mkdir();
+ if (!isSuccess) {
+ throw new IllegalStateException("Couldn't create data directory for " + nickname);
+ }
+
+ TorNode firstRelay = TorNode.builder()
+ .type(TorNode.Type.RELAY)
+ .nickname(nickname)
+ .dataDir(nodeDataDir)
+
+ .controlPort(NetworkUtils.findFreeSystemPort())
+ .orPort(NetworkUtils.findFreeSystemPort())
+ .dirPort(NetworkUtils.findFreeSystemPort())
+
+ .build();
+ relays.add(firstRelay);
+ return this;
+ }
+
+ public TorNetwork addClient() {
+ String nickname = "client" + clientIndex++;
+
+ Path nodeDataDir = rootDataDir.resolve(nickname);
+ boolean isSuccess = nodeDataDir.toFile().mkdir();
+ if (!isSuccess) {
+ throw new IllegalStateException("Couldn't create data directory for " + nickname);
+ }
+
+ TorNode firstClient = TorNode.builder()
+ .type(TorNode.Type.CLIENT)
+ .nickname(nickname)
+ .dataDir(nodeDataDir)
+
+ .controlPort(NetworkUtils.findFreeSystemPort())
+ .orPort(NetworkUtils.findFreeSystemPort())
+ .dirPort(NetworkUtils.findFreeSystemPort())
+
+ .build();
+ clients.add(firstClient);
+ return this;
+ }
+
+ public void start() throws IOException {
+ generateTorrcFiles();
+ startProcesses();
+ }
+
+ private void generateTorrcFiles() throws IOException {
+ Set allDAs = dirAuthFactory.getAllDirectoryAuthorities();
+ for (TorNode da : allDAs) {
+ var torDaTorrcGenerator = new DirectoryAuthorityTorrcGenerator(da);
+ var torrcFileGenerator = new TorrcFileGenerator(torDaTorrcGenerator, allDAs);
+ torrcFileGenerator.generate();
+ }
+
+ for (TorNode relay : relays) {
+ var relayTorrcGenerator = new RelayTorrcGenerator(relay);
+ var torrcFileGenerator = new TorrcFileGenerator(relayTorrcGenerator, allDAs);
+ torrcFileGenerator.generate();
+ }
+
+ for (TorNode client : clients) {
+ var clientTorrcGenerator = new ClientTorrcGenerator(client);
+ var torrcFileGenerator = new TorrcFileGenerator(clientTorrcGenerator, allDAs);
+ torrcFileGenerator.generate();
+ }
+ }
+
+ private void startProcesses() throws IOException {
+ for (TorNode directoryAuthority : directoryAuthorities) {
+ Process process = createAndStartTorProcess(directoryAuthority);
+ allTorProcesses.add(process);
+ }
+
+ for (TorNode relay : relays) {
+ Process process = createAndStartTorProcess(relay);
+ allTorProcesses.add(process);
+ }
+
+ for (TorNode client : clients) {
+ Process process = createAndStartTorProcess(client);
+ allTorProcesses.add(process);
+ }
+ }
+
+ private Process createAndStartTorProcess(TorNode torNode) throws IOException {
+ String absoluteTorrcPathAsString = torNode.getTorrcPath()
+ .toAbsolutePath()
+ .toString();
+ var processBuilder = new ProcessBuilder("tor", "-f", absoluteTorrcPathAsString);
+ processBuilder.redirectError(ProcessBuilder.Redirect.DISCARD);
+ processBuilder.redirectOutput(ProcessBuilder.Redirect.DISCARD);
+ return processBuilder.start();
+ }
+}