diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java index f443b01bd..91519a832 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java @@ -105,43 +105,46 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @ParametersNotNullByDefault abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { - private static final Logger LOG = getLogger(TorPlugin.class.getName()); + static final Logger LOG = getLogger(TorPlugin.class.getName()); - private static final String[] EVENTS = { + static final String[] EVENTS = { "CIRC", "ORCONN", "HS_DESC", "NOTICE", "WARN", "ERR" }; - private static final String OWNER = "__OwningControllerProcess"; - private static final int COOKIE_TIMEOUT_MS = 3000; - private static final int COOKIE_POLLING_INTERVAL_MS = 200; + static final String OWNER = "__OwningControllerProcess"; + static final int COOKIE_TIMEOUT_MS = 3000; + static final int COOKIE_POLLING_INTERVAL_MS = 200; private static final Pattern ONION_V3 = Pattern.compile("[a-z2-7]{56}"); private final Executor ioExecutor, wakefulIoExecutor; private final Executor connectionStatusExecutor; - private final NetworkManager networkManager; + final NetworkManager networkManager; private final LocationUtils locationUtils; private final SocketFactory torSocketFactory; - private final Clock clock; - private final BatteryManager batteryManager; + final Clock clock; + final BatteryManager batteryManager; private final Backoff backoff; private final TorRendezvousCrypto torRendezvousCrypto; - private final PluginCallback callback; + final PluginCallback callback; private final String architecture; private final CircumventionProvider circumventionProvider; private final ResourceProvider resourceProvider; private final long maxLatency; private final int maxIdleTime; private final int socketTimeout; - private final File torDirectory, geoIpFile, configFile; - private final int torSocksPort; - private final int torControlPort; - private final File doneFile, cookieFile; - private final AtomicBoolean used = new AtomicBoolean(false); + final File torDirectory; + final File geoIpFile; + final File configFile; + final int torSocksPort; + final int torControlPort; + private final File doneFile; + final File cookieFile; + final AtomicBoolean used = new AtomicBoolean(false); protected final PluginState state = new PluginState(); - private volatile Socket controlSocket = null; - private volatile TorControlConnection controlConnection = null; - private volatile Settings settings = null; + volatile Socket controlSocket = null; + volatile TorControlConnection controlConnection = null; + volatile Settings settings = null; protected abstract int getProcessId(); @@ -242,6 +245,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { Process torProcess; ProcessBuilder pb = new ProcessBuilder(torPath, "-f", configPath, OWNER, pid); + // TODO: pb.redirectErrorStream on Linux, too? Map env = pb.environment(); env.put("HOME", torDirectory.getAbsolutePath()); pb.directory(torDirectory); @@ -318,8 +322,9 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { bind(); } + // TODO: Remove after a reasonable migration period (added 2020-06-25) - private Settings migrateSettings(Settings settings) { + Settings migrateSettings(Settings settings) { int network = settings.getInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK); if (network == PREF_TOR_NETWORK_NEVER) { @@ -330,11 +335,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { return settings; } - private boolean assetsAreUpToDate() { + boolean assetsAreUpToDate() { return doneFile.lastModified() > getLastUpdateTime(); } - private void installAssets() throws PluginException { + void installAssets() throws PluginException { try { // The done file may already exist from a previous installation //noinspection ResultOfMethodCallIgnored @@ -389,20 +394,20 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { private InputStream getObfs4InputStream() throws IOException { InputStream in = resourceProvider - .getResourceInputStream("obfs4proxy_" + architecture, ".zip"); + .getResourceInputStream("obfs4proxy_" + "linux-x86_64", ".zip"); ZipInputStream zin = new ZipInputStream(in); if (zin.getNextEntry() == null) throw new IOException(); return zin; } - private static void append(StringBuilder strb, String name, int value) { + protected static void append(StringBuilder strb, String name, int value) { strb.append(name); strb.append(" "); strb.append(value); strb.append("\n"); } - private InputStream getConfigInputStream() { + protected InputStream getConfigInputStream() { StringBuilder strb = new StringBuilder(); append(strb, "ControlPort", torControlPort); append(strb, "CookieAuthentication", 1); @@ -415,7 +420,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { strb.toString().getBytes(Charset.forName("UTF-8"))); } - private void listFiles(File f) { + void listFiles(File f) { if (f.isDirectory()) { File[] children = f.listFiles(); if (children != null) for (File child : children) listFiles(child); @@ -424,7 +429,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } } - private byte[] read(File f) throws IOException { + byte[] read(File f) throws IOException { byte[] b = new byte[(int) f.length()]; FileInputStream in = new FileInputStream(f); try { @@ -440,7 +445,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } } - private void bind() { + void bind() { ioExecutor.execute(() -> { // If there's already a port number stored in config, reuse it String portString = settings.get(PREF_TOR_PORT); @@ -814,8 +819,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { }); } - private void updateConnectionStatus(NetworkStatus status, - boolean charging) { + void updateConnectionStatus(NetworkStatus status, + boolean charging) { connectionStatusExecutor.execute(() -> { if (!state.isTorRunning()) return; boolean online = status.isConnected(); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/system/WindowsSecureRandomProvider.java b/bramble-core/src/main/java/org/briarproject/bramble/system/WindowsSecureRandomProvider.java new file mode 100644 index 000000000..45dbec749 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/system/WindowsSecureRandomProvider.java @@ -0,0 +1,12 @@ +package org.briarproject.bramble.system; + +import javax.annotation.Nullable; +import java.security.Provider; + +public class WindowsSecureRandomProvider extends AbstractSecureRandomProvider { + @Nullable + @Override + public Provider getProvider() { + return null; + } +} diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/DesktopTorPluginFactory.java similarity index 75% rename from bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java rename to bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/DesktopTorPluginFactory.java index 8e3d05a71..84899df8c 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/DesktopTorPluginFactory.java @@ -32,13 +32,14 @@ import javax.net.SocketFactory; import static java.util.logging.Level.INFO; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.util.OsUtils.isLinux; +import static org.briarproject.bramble.util.OsUtils.isWindows; @Immutable @NotNullByDefault -public class UnixTorPluginFactory implements DuplexPluginFactory { +public class DesktopTorPluginFactory implements DuplexPluginFactory { private static final Logger LOG = - getLogger(UnixTorPluginFactory.class.getName()); + getLogger(DesktopTorPluginFactory.class.getName()); private static final int MAX_LATENCY = 30 * 1000; // 30 seconds private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds @@ -62,7 +63,7 @@ public class UnixTorPluginFactory implements DuplexPluginFactory { private final CryptoComponent crypto; @Inject - UnixTorPluginFactory(@IoExecutor Executor ioExecutor, + DesktopTorPluginFactory(@IoExecutor Executor ioExecutor, @WakefulIoExecutor Executor wakefulIoExecutor, NetworkManager networkManager, LocationUtils locationUtils, @@ -107,25 +108,33 @@ public class UnixTorPluginFactory implements DuplexPluginFactory { @Override public DuplexPlugin createPlugin(PluginCallback callback) { // Check that we have a Tor binary for this architecture - String architecture = null; if (isLinux()) { String arch = System.getProperty("os.arch"); if (LOG.isLoggable(INFO)) { LOG.info("System's os.arch is " + arch); } if (arch.equals("amd64")) { - architecture = "linux-x86_64"; + return createUnixPlugin(callback, "linux-x86_64"); } else if (arch.equals("aarch64")) { - architecture = "linux-aarch64"; + return createUnixPlugin(callback, "linux-aarch64"); } else if (arch.equals("arm")) { - architecture = "linux-armhf"; + return createUnixPlugin(callback, "linux-armhf"); } } - if (architecture == null) { - LOG.info("Tor is not supported on this architecture"); - return null; + if (isWindows()) { + String arch = System.getProperty("os.arch"); + if (LOG.isLoggable(INFO)) { + LOG.info("System's os.arch is " + arch); + } + if (arch.equals("amd64")) { + return createWindowsPlugin(callback, "windows-x86_64"); + } } + LOG.info("Tor is not supported on this architecture"); + return null; + } + private DuplexPlugin createUnixPlugin(PluginCallback callback, String architecture) { if (LOG.isLoggable(INFO)) { LOG.info("The selected architecture for Tor is " + architecture); } @@ -143,4 +152,21 @@ public class UnixTorPluginFactory implements DuplexPluginFactory { eventBus.addListener(plugin); return plugin; } + + private DuplexPlugin createWindowsPlugin(PluginCallback callback, String architecture) { + if (LOG.isLoggable(INFO)) { + LOG.info("The selected architecture for Tor is " + architecture); + } + + Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, + MAX_POLLING_INTERVAL, BACKOFF_BASE); + TorRendezvousCrypto torRendezvousCrypto = new TorRendezvousCryptoImpl(); + WindowsTorPlugin plugin = new WindowsTorPlugin(ioExecutor, wakefulIoExecutor, + networkManager, locationUtils, torSocketFactory, clock, + resourceProvider, circumventionProvider, batteryManager, + backoff, torRendezvousCrypto, callback, architecture, + MAX_LATENCY, MAX_IDLE_TIME, torDirectory, torSocksPort, torControlPort); + eventBus.addListener(plugin); + return plugin; + } } diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPlugin.java new file mode 100644 index 000000000..060b8d91a --- /dev/null +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPlugin.java @@ -0,0 +1,217 @@ +package org.briarproject.bramble.plugin.tor; + +import com.sun.jna.Library; +import com.sun.jna.Native; + +import net.freehaven.tor.control.TorControlConnection; +import org.briarproject.bramble.api.battery.BatteryManager; +import org.briarproject.bramble.api.network.NetworkManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.Backoff; +import org.briarproject.bramble.api.plugin.PluginCallback; +import org.briarproject.bramble.api.plugin.PluginException; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.api.system.LocationUtils; +import org.briarproject.bramble.api.system.ResourceProvider; + +import java.io.*; +import java.net.Socket; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Scanner; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import javax.net.SocketFactory; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; + +@NotNullByDefault +class WindowsTorPlugin extends JavaTorPlugin { + + WindowsTorPlugin(Executor ioExecutor, + Executor wakefulIoExecutor, + NetworkManager networkManager, + LocationUtils locationUtils, + SocketFactory torSocketFactory, + Clock clock, + ResourceProvider resourceProvider, + CircumventionProvider circumventionProvider, + BatteryManager batteryManager, + Backoff backoff, + TorRendezvousCrypto torRendezvousCrypto, + PluginCallback callback, + String architecture, + long maxLatency, + int maxIdleTime, + File torDirectory, + int torSocksPort, + int torControlPort) { + super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, + torSocketFactory, clock, resourceProvider, + circumventionProvider, batteryManager, backoff, + torRendezvousCrypto, callback, architecture, + maxLatency, maxIdleTime, torDirectory, torSocksPort, torControlPort); + } + + protected File getTorExecutableFile() { + return new File(torDirectory, "tor.exe"); + } + + protected InputStream getConfigInputStream() { + StringBuilder strb = new StringBuilder(); + append(strb, "ControlPort", torControlPort); + append(strb, "CookieAuthentication", 1); + append(strb, "DisableNetwork", 1); + append(strb, "RunAsDaemon", 1); + append(strb, "SafeSocks", 1); + append(strb, "SocksPort", torSocksPort); + InputStream inputStream = new ByteArrayInputStream( + strb.toString().getBytes(Charset.forName("UTF-8"))); + InputStream windowsPaths = new ByteArrayInputStream(getTorrcPaths()); + inputStream = new SequenceInputStream(inputStream, windowsPaths); + return inputStream; + } + + private byte[] getTorrcPaths() { + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append("GeoIPFile "); + sb.append(geoIpFile.getAbsolutePath()); + sb.append("\n"); + sb.append("GeoIPv6File "); + sb.append(geoIpFile.getAbsolutePath()); + sb.append("6"); + sb.append("\n"); + sb.append("DataDirectory "); + sb.append(torDirectory); + sb.append("\\.tor"); + return sb.toString().getBytes(StandardCharsets.UTF_8); + } + + @Override + public void start() throws PluginException { + /* + TODO: + - properly handle and throw PluginExceptions etc. + - absolute paths in Windows torrc (Linux too?) + - don't do 10 seconds sleep in main thread + */ + if (used.getAndSet(true)) throw new IllegalStateException(); + if (!torDirectory.exists()) { + if (!torDirectory.mkdirs()) { + LOG.warning("Could not create Tor directory."); + throw new PluginException(); + } + } + // Load the settings + settings = migrateSettings(callback.getSettings()); + // Install or update the assets if necessary + if (!assetsAreUpToDate()) installAssets(); + if (cookieFile.exists() && !cookieFile.delete()) + LOG.warning("Old auth cookie not deleted"); + // Start a new Tor process + LOG.info("Starting Tor"); + File torFile = getTorExecutableFile(); + String torPath = torFile.getAbsolutePath(); + String configPath = configFile.getAbsolutePath(); + String pid = String.valueOf(getProcessId()); + Executors.newSingleThreadExecutor().execute(new Runnable() { + @Override + public void run() { + Process torProcess; + ProcessBuilder pb = + new ProcessBuilder(torPath, "-f", configPath, OWNER, pid); + pb.redirectErrorStream(true); // logged only first line on Windows otherwise + Map env = pb.environment(); + env.put("HOME", torDirectory.getAbsolutePath()); + pb.directory(torDirectory); + try { + torProcess = pb.start(); + // Log the process's standard output until it detaches + if (LOG.isLoggable(INFO)) { + Scanner stdout = new Scanner(torProcess.getInputStream()); + while (stdout.hasNextLine()) { + if (stdout.hasNextLine()) { + LOG.info(stdout.nextLine()); + } + } + stdout.close(); + } + try { + // Wait for the process to detach or exit + int exit = torProcess.waitFor(); + if (exit != 0) { + if (LOG.isLoggable(WARNING)) + LOG.warning("Tor exited with value " + exit); + } + // Wait for the auth cookie file to be created/updated + long start = clock.currentTimeMillis(); + while (cookieFile.length() < 32) { + if (clock.currentTimeMillis() - start > COOKIE_TIMEOUT_MS) { + LOG.warning("Auth cookie not created"); + if (LOG.isLoggable(INFO)) listFiles(torDirectory); + } + Thread.sleep(COOKIE_POLLING_INTERVAL_MS); + } + LOG.info("Auth cookie created"); + } catch (InterruptedException e) { + LOG.warning("Interrupted while starting Tor"); + Thread.currentThread().interrupt(); + e.printStackTrace(); + } + } catch (SecurityException | IOException e) { + e.printStackTrace(); + } + } + }); + try { + TimeUnit.SECONDS.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + try { + // Open a control connection and authenticate using the cookie file + controlSocket = new Socket("127.0.0.1", torControlPort); + controlConnection = new TorControlConnection(controlSocket); + controlConnection.authenticate(read(cookieFile)); + // Tell Tor to exit when the control connection is closed + controlConnection.takeOwnership(); + controlConnection.resetConf(singletonList(OWNER)); + // Register to receive events from the Tor process + controlConnection.setEventHandler(this); + controlConnection.setEvents(asList(EVENTS)); + // Check whether Tor has already bootstrapped + String phase = controlConnection.getInfo("status/bootstrap-phase"); + if (phase != null && phase.contains("PROGRESS=100")) { + LOG.info("Tor has already bootstrapped"); + state.setBootstrapped(); + } + } catch (IOException e) { + throw new PluginException(e); + } + state.setStarted(); + // Check whether we're online + updateConnectionStatus(networkManager.getNetworkStatus(), + batteryManager.isCharging()); + // Bind a server socket to receive incoming hidden service connections + bind(); + } + + @Override + protected int getProcessId() { + return CLibrary.INSTANCE._getpid(); + } + + private interface CLibrary extends Library { + + CLibrary INSTANCE = Native.loadLibrary("msvcrt", CLibrary.class); + + int _getpid(); + } +} diff --git a/bramble-java/src/main/java/org/briarproject/bramble/system/DesktopSecureRandomModule.java b/bramble-java/src/main/java/org/briarproject/bramble/system/DesktopSecureRandomModule.java index 47dfe5b0a..449b2801b 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/system/DesktopSecureRandomModule.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/system/DesktopSecureRandomModule.java @@ -9,6 +9,7 @@ import dagger.Provides; import static org.briarproject.bramble.util.OsUtils.isLinux; import static org.briarproject.bramble.util.OsUtils.isMac; +import static org.briarproject.bramble.util.OsUtils.isWindows; @Module public class DesktopSecureRandomModule { @@ -18,7 +19,8 @@ public class DesktopSecureRandomModule { SecureRandomProvider provideSecureRandomProvider() { if (isLinux() || isMac()) return new UnixSecureRandomProvider(); - // TODO: Create a secure random provider for Windows + if (isWindows()) + return new WindowsSecureRandomProvider(); throw new UnsupportedOperationException(); } } diff --git a/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java b/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java index a8f76d91a..c466dbb55 100644 --- a/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java +++ b/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java @@ -107,7 +107,7 @@ public class BridgeTest extends BrambleTestCase { private final File torDir = getTestDirectory(); private final Params params; - private UnixTorPluginFactory factory; + private DesktopTorPluginFactory factory; public BridgeTest(Params params) { this.params = params; @@ -152,7 +152,7 @@ public class BridgeTest extends BrambleTestCase { return singletonList(params.bridge); } }; - factory = new UnixTorPluginFactory(ioExecutor, wakefulIoExecutor, + factory = new DesktopTorPluginFactory(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, eventBus, torSocketFactory, backoffFactory, resourceProvider, bridgeProvider, batteryManager, clock, torDir, DEFAULT_SOCKS_PORT,