Factor out Tor wrapper from plugin

This commit is contained in:
akwizgran
2022-05-10 14:25:21 +01:00
parent b7003a3587
commit 01b1741e83
16 changed files with 1177 additions and 868 deletions

View File

@@ -5,6 +5,7 @@ import android.app.Application;
import org.briarproject.bramble.api.battery.BatteryManager; import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventExecutor;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.network.NetworkManager; import org.briarproject.bramble.api.network.NetworkManager;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
@@ -16,8 +17,9 @@ import org.briarproject.bramble.api.plugin.TorSocksPort;
import org.briarproject.bramble.api.system.AndroidWakeLockManager; import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.ResourceProvider;
import org.briarproject.bramble.api.system.WakefulIoExecutor; import org.briarproject.bramble.api.system.WakefulIoExecutor;
import org.briarproject.bramble.plugin.tor.wrapper.AndroidTorWrapper;
import org.briarproject.bramble.plugin.tor.wrapper.TorWrapper;
import org.briarproject.nullsafety.NotNullByDefault; import org.briarproject.nullsafety.NotNullByDefault;
import java.io.File; import java.io.File;
@@ -28,6 +30,7 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.net.SocketFactory; import javax.net.SocketFactory;
import static android.os.Build.VERSION.SDK_INT;
import static org.briarproject.bramble.util.AndroidUtils.getSupportedArchitectures; import static org.briarproject.bramble.util.AndroidUtils.getSupportedArchitectures;
@Immutable @Immutable
@@ -39,13 +42,13 @@ public class AndroidTorPluginFactory extends TorPluginFactory {
@Inject @Inject
AndroidTorPluginFactory(@IoExecutor Executor ioExecutor, AndroidTorPluginFactory(@IoExecutor Executor ioExecutor,
@EventExecutor Executor eventExecutor,
@WakefulIoExecutor Executor wakefulIoExecutor, @WakefulIoExecutor Executor wakefulIoExecutor,
NetworkManager networkManager, NetworkManager networkManager,
LocationUtils locationUtils, LocationUtils locationUtils,
EventBus eventBus, EventBus eventBus,
SocketFactory torSocketFactory, SocketFactory torSocketFactory,
BackoffFactory backoffFactory, BackoffFactory backoffFactory,
ResourceProvider resourceProvider,
CircumventionProvider circumventionProvider, CircumventionProvider circumventionProvider,
BatteryManager batteryManager, BatteryManager batteryManager,
Clock clock, Clock clock,
@@ -55,8 +58,8 @@ public class AndroidTorPluginFactory extends TorPluginFactory {
@TorControlPort int torControlPort, @TorControlPort int torControlPort,
Application app, Application app,
AndroidWakeLockManager wakeLockManager) { AndroidWakeLockManager wakeLockManager) {
super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, super(ioExecutor, eventExecutor, wakefulIoExecutor, networkManager,
eventBus, torSocketFactory, backoffFactory, resourceProvider, locationUtils, eventBus, torSocketFactory, backoffFactory,
circumventionProvider, batteryManager, clock, crypto, circumventionProvider, batteryManager, clock, crypto,
torDirectory, torSocksPort, torControlPort); torDirectory, torSocksPort, torControlPort);
this.app = app; this.app = app;
@@ -79,12 +82,18 @@ public class AndroidTorPluginFactory extends TorPluginFactory {
TorPlugin createPluginInstance(Backoff backoff, TorPlugin createPluginInstance(Backoff backoff,
TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback, TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback,
String architecture) { String architecture) {
return new AndroidTorPlugin(ioExecutor, TorWrapper tor = new AndroidTorWrapper(app, wakeLockManager,
wakefulIoExecutor, app, networkManager, locationUtils, ioExecutor, eventExecutor, architecture, torDirectory,
torSocketFactory, clock, resourceProvider, torSocksPort, torControlPort);
circumventionProvider, batteryManager, wakeLockManager, // Android versions 7.1 and newer can verify Let's Encrypt TLS certs
backoff, torRendezvousCrypto, callback, architecture, // signed with the IdentTrust DST Root X3 certificate. Older versions
MAX_LATENCY, MAX_IDLE_TIME, torDirectory, torSocksPort, // of Android consider the certificate to have expired at the end of
torControlPort); // September 2021.
boolean canVerifyLetsEncryptCerts = SDK_INT >= 25;
return new TorPlugin(ioExecutor, wakefulIoExecutor,
networkManager, locationUtils, torSocketFactory,
circumventionProvider, batteryManager, backoff,
torRendezvousCrypto, tor, callback, MAX_LATENCY,
MAX_IDLE_TIME, canVerifyLetsEncryptCerts);
} }
} }

View File

@@ -1,46 +1,36 @@
package org.briarproject.bramble.plugin.tor; package org.briarproject.bramble.plugin.tor.wrapper;
import android.app.Application; import android.app.Application;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.os.Build;
import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.network.NetworkManager;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.system.AndroidWakeLock; import org.briarproject.bramble.api.system.AndroidWakeLock;
import org.briarproject.bramble.api.system.AndroidWakeLockManager; import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.ResourceProvider;
import org.briarproject.bramble.util.AndroidUtils;
import org.briarproject.nullsafety.MethodsNotNullByDefault;
import org.briarproject.nullsafety.ParametersNotNullByDefault;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import javax.net.SocketFactory;
import androidx.annotation.ChecksSdkIntAtLeast;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
@MethodsNotNullByDefault @NotNullByDefault
@ParametersNotNullByDefault public class AndroidTorWrapper extends AbstractTorWrapper {
class AndroidTorPlugin extends TorPlugin {
private static final List<String> LIBRARY_ARCHITECTURES = private static final List<String> LIBRARY_ARCHITECTURES =
asList("armeabi-v7a", "arm64-v8a", "x86", "x86_64"); asList("armeabi-v7a", "arm64-v8a", "x86", "x86_64");
@@ -50,37 +40,22 @@ class AndroidTorPlugin extends TorPlugin {
private static final String SNOWFLAKE_LIB_NAME = "libsnowflake.so"; private static final String SNOWFLAKE_LIB_NAME = "libsnowflake.so";
private static final Logger LOG = private static final Logger LOG =
getLogger(AndroidTorPlugin.class.getName()); getLogger(AndroidTorWrapper.class.getName());
private final Application app; private final Application app;
private final AndroidWakeLock wakeLock; private final AndroidWakeLock wakeLock;
private final File torLib, obfs4Lib, snowflakeLib; private final File torLib, obfs4Lib, snowflakeLib;
AndroidTorPlugin(Executor ioExecutor, public AndroidTorWrapper(Application app,
Executor wakefulIoExecutor,
Application app,
NetworkManager networkManager,
LocationUtils locationUtils,
SocketFactory torSocketFactory,
Clock clock,
ResourceProvider resourceProvider,
CircumventionProvider circumventionProvider,
BatteryManager batteryManager,
AndroidWakeLockManager wakeLockManager, AndroidWakeLockManager wakeLockManager,
Backoff backoff, Executor ioExecutor,
TorRendezvousCrypto torRendezvousCrypto, Executor eventExecutor,
PluginCallback callback,
String architecture, String architecture,
long maxLatency,
int maxIdleTime,
File torDirectory, File torDirectory,
int torSocksPort, int torSocksPort,
int torControlPort) { int torControlPort) {
super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, super(ioExecutor, eventExecutor, architecture, torDirectory,
torSocketFactory, clock, resourceProvider, torSocksPort, torControlPort);
circumventionProvider, batteryManager, backoff,
torRendezvousCrypto, callback, architecture, maxLatency,
maxIdleTime, torDirectory, torSocksPort, torControlPort);
this.app = app; this.app = app;
wakeLock = wakeLockManager.createWakeLock("TorPlugin"); wakeLock = wakeLockManager.createWakeLock("TorPlugin");
String nativeLibDir = app.getApplicationInfo().nativeLibraryDir; String nativeLibDir = app.getApplicationInfo().nativeLibraryDir;
@@ -106,22 +81,30 @@ class AndroidTorPlugin extends TorPlugin {
} }
@Override @Override
protected void enableNetwork(boolean enable) throws IOException { public InputStream getResourceInputStream(String name, String extension) {
Resources res = app.getResources();
// Extension is ignored on Android, resources are retrieved without it
int resId = res.getIdentifier(name, "raw", app.getPackageName());
return res.openRawResource(resId);
}
@Override
public void enableNetwork(boolean enable) throws IOException {
if (enable) wakeLock.acquire(); if (enable) wakeLock.acquire();
super.enableNetwork(enable); try {
if (!enable) wakeLock.release(); super.enableNetwork(enable);
} finally {
if (!enable) wakeLock.release();
}
} }
@Override @Override
@ChecksSdkIntAtLeast(api = 25) public void stop() throws IOException {
protected boolean canVerifyLetsEncryptCerts() { try {
return SDK_INT >= 25; super.stop();
} } finally {
wakeLock.release();
@Override }
public void stop() {
super.stop();
wakeLock.release();
} }
@Override @Override
@@ -136,8 +119,8 @@ class AndroidTorPlugin extends TorPlugin {
@Override @Override
protected File getSnowflakeExecutableFile() { protected File getSnowflakeExecutableFile() {
return snowflakeLib.exists() return snowflakeLib.exists() ? snowflakeLib
? snowflakeLib : super.getSnowflakeExecutableFile(); : super.getSnowflakeExecutableFile();
} }
@Override @Override
@@ -184,6 +167,7 @@ class AndroidTorPlugin extends TorPlugin {
} }
List<String> libPaths = getSupportedLibraryPaths(libName); List<String> libPaths = getSupportedLibraryPaths(libName);
for (File apk : findApkFiles(sourceDir)) { for (File apk : findApkFiles(sourceDir)) {
@SuppressWarnings("IOStreamConstructor")
ZipInputStream zin = new ZipInputStream(new FileInputStream(apk)); ZipInputStream zin = new ZipInputStream(new FileInputStream(apk));
for (ZipEntry e = zin.getNextEntry(); e != null; for (ZipEntry e = zin.getNextEntry(); e != null;
e = zin.getNextEntry()) { e = zin.getNextEntry()) {
@@ -228,11 +212,22 @@ class AndroidTorPlugin extends TorPlugin {
*/ */
private List<String> getSupportedLibraryPaths(String libName) { private List<String> getSupportedLibraryPaths(String libName) {
List<String> architectures = new ArrayList<>(); List<String> architectures = new ArrayList<>();
for (String abi : AndroidUtils.getSupportedArchitectures()) { for (String abi : getSupportedArchitectures()) {
if (LIBRARY_ARCHITECTURES.contains(abi)) { if (LIBRARY_ARCHITECTURES.contains(abi)) {
architectures.add("lib/" + abi + "/" + libName); architectures.add("lib/" + abi + "/" + libName);
} }
} }
return architectures; return architectures;
} }
private Collection<String> getSupportedArchitectures() {
List<String> abis = new ArrayList<>();
if (SDK_INT >= 21) {
abis.addAll(asList(Build.SUPPORTED_ABIS));
} else {
abis.add(Build.CPU_ABI);
if (Build.CPU_ABI2 != null) abis.add(Build.CPU_ABI2);
}
return abis;
}
} }

View File

@@ -40,9 +40,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe; import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject; import javax.inject.Inject;
@@ -288,8 +288,10 @@ class PluginManagerImpl implements PluginManager, Service {
private class Callback implements PluginCallback { private class Callback implements PluginCallback {
private final TransportId id; private final TransportId id;
private final AtomicReference<State> state = private final Object stateLock = new Object();
new AtomicReference<>(STARTING_STOPPING);
@GuardedBy("lock")
private State state = STARTING_STOPPING;
private Callback(TransportId id) { private Callback(TransportId id) {
this.id = id; this.id = id;
@@ -343,22 +345,26 @@ class PluginManagerImpl implements PluginManager, Service {
@Override @Override
public void pluginStateChanged(State newState) { public void pluginStateChanged(State newState) {
State oldState = state.getAndSet(newState); synchronized (stateLock) {
if (newState != oldState) { if (newState != state) {
if (LOG.isLoggable(INFO)) { State oldState = state;
LOG.info(id + " changed from state " + oldState state = newState;
+ " to " + newState); if (LOG.isLoggable(INFO)) {
LOG.info(id + " changed from state " + oldState
+ " to " + newState);
}
eventBus.broadcast(new TransportStateEvent(id, newState));
if (newState == ACTIVE) {
eventBus.broadcast(new TransportActiveEvent(id));
} else if (oldState == ACTIVE) {
eventBus.broadcast(new TransportInactiveEvent(id));
}
} else if (newState == DISABLED) {
// Broadcast an event even though the state hasn't changed,
// as the reasons for the plugin being disabled may have
// changed
eventBus.broadcast(new TransportStateEvent(id, newState));
} }
eventBus.broadcast(new TransportStateEvent(id, newState));
if (newState == ACTIVE) {
eventBus.broadcast(new TransportActiveEvent(id));
} else if (oldState == ACTIVE) {
eventBus.broadcast(new TransportInactiveEvent(id));
}
} else if (newState == DISABLED) {
// Broadcast an event even though the state hasn't changed, as
// the reasons for the plugin being disabled may have changed
eventBus.broadcast(new TransportStateEvent(id, newState));
} }
} }

View File

@@ -1,8 +1,5 @@
package org.briarproject.bramble.plugin.tor; package org.briarproject.bramble.plugin.tor;
import net.freehaven.tor.control.EventHandler;
import net.freehaven.tor.control.TorControlConnection;
import org.briarproject.bramble.PoliteExecutor; import org.briarproject.bramble.PoliteExecutor;
import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.battery.BatteryManager; import org.briarproject.bramble.api.battery.BatteryManager;
@@ -27,30 +24,21 @@ import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint; import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent; import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.ResourceProvider;
import org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType; import org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType;
import org.briarproject.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.plugin.tor.wrapper.TorWrapper;
import org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.HiddenServiceProperties;
import org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.TorState;
import org.briarproject.nullsafety.InterfaceNotNullByDefault;
import org.briarproject.nullsafety.NotNullByDefault; import org.briarproject.nullsafety.NotNullByDefault;
import org.briarproject.nullsafety.ParametersNotNullByDefault;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -63,13 +51,9 @@ import javax.net.SocketFactory;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static net.freehaven.tor.control.TorControlCommands.HS_ADDRESS;
import static net.freehaven.tor.control.TorControlCommands.HS_PRIVKEY;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED; import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
import static org.briarproject.bramble.api.plugin.Plugin.State.ENABLING; import static org.briarproject.bramble.api.plugin.Plugin.State.ENABLING;
@@ -94,33 +78,16 @@ import static org.briarproject.bramble.api.plugin.TorConstants.REASON_MOBILE_DAT
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.SNOWFLAKE; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.SNOWFLAKE;
import static org.briarproject.bramble.plugin.tor.TorRendezvousCrypto.SEED_BYTES; import static org.briarproject.bramble.plugin.tor.TorRendezvousCrypto.SEED_BYTES;
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.bramble.util.IoUtils.tryToClose; import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.PrivacyUtils.scrubOnion; import static org.briarproject.bramble.util.PrivacyUtils.scrubOnion;
import static org.briarproject.bramble.util.StringUtils.UTF_8;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.nullsafety.NullSafety.requireNonNull;
@MethodsNotNullByDefault @InterfaceNotNullByDefault
@ParametersNotNullByDefault class TorPlugin implements DuplexPlugin, EventListener {
abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
protected static final Logger LOG = getLogger(TorPlugin.class.getName()); protected static final Logger LOG = getLogger(TorPlugin.class.getName());
private static final String[] EVENTS = {
"CIRC",
"ORCONN",
"STATUS_GENERAL",
"STATUS_CLIENT",
"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;
private static final Pattern ONION_V3 = Pattern.compile("[a-z2-7]{56}"); private static final Pattern ONION_V3 = Pattern.compile("[a-z2-7]{56}");
protected final Executor ioExecutor; protected final Executor ioExecutor;
@@ -129,91 +96,63 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private final NetworkManager networkManager; private final NetworkManager networkManager;
private final LocationUtils locationUtils; private final LocationUtils locationUtils;
private final SocketFactory torSocketFactory; private final SocketFactory torSocketFactory;
private final Clock clock; private final CircumventionProvider circumventionProvider;
private final BatteryManager batteryManager; private final BatteryManager batteryManager;
private final Backoff backoff; private final Backoff backoff;
private final TorRendezvousCrypto torRendezvousCrypto; private final TorRendezvousCrypto torRendezvousCrypto;
private final TorWrapper tor;
private final PluginCallback callback; private final PluginCallback callback;
private final String architecture;
private final CircumventionProvider circumventionProvider;
private final ResourceProvider resourceProvider;
private final long maxLatency; private final long maxLatency;
private final int maxIdleTime; private final int maxIdleTime;
private final boolean canVerifyLetsEncryptCerts;
private final int socketTimeout; private final int socketTimeout;
private final File torDirectory;
private final File configFile;
private final int torSocksPort;
private final int torControlPort;
private final File doneFile, cookieFile;
private final AtomicBoolean used = new AtomicBoolean(false); private final AtomicBoolean used = new AtomicBoolean(false);
protected final PluginState state = new PluginState(); protected final PluginState state = new PluginState();
private volatile Socket controlSocket = null;
private volatile TorControlConnection controlConnection = null;
private volatile Settings settings = null; private volatile Settings settings = null;
protected abstract int getProcessId();
protected abstract long getLastUpdateTime();
TorPlugin(Executor ioExecutor, TorPlugin(Executor ioExecutor,
Executor wakefulIoExecutor, Executor wakefulIoExecutor,
NetworkManager networkManager, NetworkManager networkManager,
LocationUtils locationUtils, LocationUtils locationUtils,
SocketFactory torSocketFactory, SocketFactory torSocketFactory,
Clock clock,
ResourceProvider resourceProvider,
CircumventionProvider circumventionProvider, CircumventionProvider circumventionProvider,
BatteryManager batteryManager, BatteryManager batteryManager,
Backoff backoff, Backoff backoff,
TorRendezvousCrypto torRendezvousCrypto, TorRendezvousCrypto torRendezvousCrypto,
TorWrapper tor,
PluginCallback callback, PluginCallback callback,
String architecture,
long maxLatency, long maxLatency,
int maxIdleTime, int maxIdleTime,
File torDirectory, boolean canVerifyLetsEncryptCerts) {
int torSocksPort,
int torControlPort) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.wakefulIoExecutor = wakefulIoExecutor; this.wakefulIoExecutor = wakefulIoExecutor;
this.networkManager = networkManager; this.networkManager = networkManager;
this.locationUtils = locationUtils; this.locationUtils = locationUtils;
this.torSocketFactory = torSocketFactory; this.torSocketFactory = torSocketFactory;
this.clock = clock;
this.resourceProvider = resourceProvider;
this.circumventionProvider = circumventionProvider; this.circumventionProvider = circumventionProvider;
this.batteryManager = batteryManager; this.batteryManager = batteryManager;
this.backoff = backoff; this.backoff = backoff;
this.torRendezvousCrypto = torRendezvousCrypto; this.torRendezvousCrypto = torRendezvousCrypto;
this.tor = tor;
this.callback = callback; this.callback = callback;
this.architecture = architecture;
this.maxLatency = maxLatency; this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime; this.maxIdleTime = maxIdleTime;
if (maxIdleTime > Integer.MAX_VALUE / 2) this.canVerifyLetsEncryptCerts = canVerifyLetsEncryptCerts;
if (maxIdleTime > Integer.MAX_VALUE / 2) {
socketTimeout = Integer.MAX_VALUE; socketTimeout = Integer.MAX_VALUE;
else socketTimeout = maxIdleTime * 2; } else {
this.torDirectory = torDirectory; socketTimeout = maxIdleTime * 2;
this.torSocksPort = torSocksPort; }
this.torControlPort = torControlPort;
configFile = new File(torDirectory, "torrc");
doneFile = new File(torDirectory, "done");
cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
// Don't execute more than one connection status check at a time // Don't execute more than one connection status check at a time
connectionStatusExecutor = connectionStatusExecutor =
new PoliteExecutor("TorPlugin", ioExecutor, 1); new PoliteExecutor("TorPlugin", ioExecutor, 1);
} tor.setStateObserver(torState -> {
State s = state.getState(torState);
protected File getTorExecutableFile() { if (s == ACTIVE) backoff.reset();
return new File(torDirectory, "tor"); callback.pluginStateChanged(s);
} });
protected File getObfs4ExecutableFile() {
return new File(torDirectory, "obfs4proxy");
}
protected File getSnowflakeExecutableFile() {
return new File(torDirectory, "snowflake");
} }
@Override @Override
@@ -234,89 +173,18 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override @Override
public void start() throws PluginException { public void start() throws PluginException {
if (used.getAndSet(true)) throw new IllegalStateException(); 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 // Load the settings
settings = callback.getSettings(); settings = callback.getSettings();
// Start Tor
try { try {
// Install or update the assets if necessary tor.start();
if (!assetsAreUpToDate()) installAssets();
// Start from the default config every time
extract(getConfigInputStream(), configFile);
} catch (IOException e) {
throw new PluginException(e);
}
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());
Process torProcess;
ProcessBuilder pb =
new ProcessBuilder(torPath, "-f", configPath, OWNER, pid);
Map<String, String> env = pb.environment();
env.put("HOME", torDirectory.getAbsolutePath());
pb.directory(torDirectory);
pb.redirectErrorStream(true);
try {
torProcess = pb.start();
} catch (SecurityException | IOException e) {
throw new PluginException(e);
}
try {
// Wait for the Tor process to start
waitForTorToStart(torProcess);
// 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);
throw new PluginException();
}
//noinspection BusyWait
Thread.sleep(COOKIE_POLLING_INTERVAL_MS);
}
LOG.info("Auth cookie created");
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOG.warning("Interrupted while starting Tor"); LOG.warning("Interrupted while starting Tor");
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new PluginException(); throw new PluginException();
}
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 info = controlConnection.getInfo("status/bootstrap-phase");
if (info != null && info.contains("PROGRESS=100")) {
LOG.info("Tor has already bootstrapped");
state.setBootstrapped();
}
// Check whether Tor has already built a circuit
info = controlConnection.getInfo("status/circuit-established");
if ("1".equals(info)) {
LOG.info("Tor has already built a circuit");
state.setCircuitBuilt(true);
}
} catch (IOException e) { } catch (IOException e) {
throw new PluginException(e); throw new PluginException(e);
} }
state.setStarted();
// Check whether we're online // Check whether we're online
updateConnectionStatus(networkManager.getNetworkStatus(), updateConnectionStatus(networkManager.getNetworkStatus(),
batteryManager.isCharging()); batteryManager.isCharging());
@@ -324,130 +192,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
bind(); bind();
} }
private boolean assetsAreUpToDate() {
return doneFile.lastModified() > getLastUpdateTime();
}
private void installAssets() throws IOException {
// The done file may already exist from a previous installation
//noinspection ResultOfMethodCallIgnored
doneFile.delete();
installTorExecutable();
installObfs4Executable();
installSnowflakeExecutable();
if (!doneFile.createNewFile())
LOG.warning("Failed to create done file");
}
protected void extract(InputStream in, File dest) throws IOException {
OutputStream out = new FileOutputStream(dest);
copyAndClose(in, out);
}
protected void installTorExecutable() throws IOException {
if (LOG.isLoggable(INFO))
LOG.info("Installing Tor binary for " + architecture);
File torFile = getTorExecutableFile();
extract(getExecutableInputStream("tor"), torFile);
if (!torFile.setExecutable(true, true)) throw new IOException();
}
protected void installObfs4Executable() throws IOException {
if (LOG.isLoggable(INFO))
LOG.info("Installing obfs4proxy binary for " + architecture);
File obfs4File = getObfs4ExecutableFile();
extract(getExecutableInputStream("obfs4proxy"), obfs4File);
if (!obfs4File.setExecutable(true, true)) throw new IOException();
}
protected void installSnowflakeExecutable() throws IOException {
if (LOG.isLoggable(INFO))
LOG.info("Installing snowflake binary for " + architecture);
File snowflakeFile = getSnowflakeExecutableFile();
extract(getExecutableInputStream("snowflake"), snowflakeFile);
if (!snowflakeFile.setExecutable(true, true)) throw new IOException();
}
private InputStream getExecutableInputStream(String basename) {
String ext = getExecutableExtension();
return requireNonNull(resourceProvider
.getResourceInputStream(architecture + "/" + basename, ext));
}
protected String getExecutableExtension() {
return "";
}
private static void append(StringBuilder strb, String name, Object value) {
strb.append(name);
strb.append(" ");
strb.append(value);
strb.append("\n");
}
private InputStream getConfigInputStream() {
File dataDirectory = new File(torDirectory, ".tor");
StringBuilder strb = new StringBuilder();
append(strb, "ControlPort", torControlPort);
append(strb, "CookieAuthentication", 1);
append(strb, "DataDirectory", dataDirectory.getAbsolutePath());
append(strb, "DisableNetwork", 1);
append(strb, "RunAsDaemon", 1);
append(strb, "SafeSocks", 1);
append(strb, "SocksPort", torSocksPort);
strb.append("GeoIPFile\n");
strb.append("GeoIPv6File\n");
append(strb, "ConnectionPadding", 0);
String obfs4Path = getObfs4ExecutableFile().getAbsolutePath();
append(strb, "ClientTransportPlugin obfs4 exec", obfs4Path);
append(strb, "ClientTransportPlugin meek_lite exec", obfs4Path);
String snowflakePath = getSnowflakeExecutableFile().getAbsolutePath();
append(strb, "ClientTransportPlugin snowflake exec", snowflakePath);
return new ByteArrayInputStream(strb.toString().getBytes(UTF_8));
}
private void listFiles(File f) {
if (f.isDirectory()) {
File[] children = f.listFiles();
if (children != null) for (File child : children) listFiles(child);
} else {
LOG.info(f.getAbsolutePath() + " " + f.length());
}
}
private byte[] read(File f) throws IOException {
byte[] b = new byte[(int) f.length()];
FileInputStream in = new FileInputStream(f);
try {
int offset = 0;
while (offset < b.length) {
int read = in.read(b, offset, b.length - offset);
if (read == -1) throw new EOFException();
offset += read;
}
return b;
} finally {
tryToClose(in, LOG, WARNING);
}
}
protected void waitForTorToStart(Process torProcess)
throws InterruptedException, PluginException {
Scanner stdout = new Scanner(torProcess.getInputStream());
// Log the first line of stdout (contains Tor and library versions)
if (stdout.hasNextLine()) LOG.info(stdout.nextLine());
// Read the process's stdout (and redirected stderr) until it detaches
while (stdout.hasNextLine()) stdout.nextLine();
stdout.close();
// 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);
throw new PluginException();
}
}
private void bind() { private void bind() {
ioExecutor.execute(() -> { ioExecutor.execute(() -> {
// If there's already a port number stored in config, reuse it // If there's already a port number stored in config, reuse it
@@ -471,9 +215,9 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
return; return;
} }
// Store the port number // Store the port number
String localPort = String.valueOf(ss.getLocalPort()); int localPort = ss.getLocalPort();
Settings s = new Settings(); Settings s = new Settings();
s.put(PREF_TOR_PORT, localPort); s.put(PREF_TOR_PORT, String.valueOf(localPort));
callback.mergeSettings(s); callback.mergeSettings(s);
// Create a hidden service if necessary // Create a hidden service if necessary
ioExecutor.execute(() -> publishHiddenService(localPort)); ioExecutor.execute(() -> publishHiddenService(localPort));
@@ -483,48 +227,28 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}); });
} }
private void publishHiddenService(String port) { private void publishHiddenService(int localPort) {
if (!state.isTorRunning()) return; if (!tor.isTorRunning()) return;
String privKey3 = settings.get(HS_PRIVATE_KEY_V3); String privKey = settings.get(HS_PRIVATE_KEY_V3);
publishV3HiddenService(port, privKey3);
}
private void publishV3HiddenService(String port, @Nullable String privKey) {
LOG.info("Creating v3 hidden service"); LOG.info("Creating v3 hidden service");
Map<Integer, String> portLines = singletonMap(80, "127.0.0.1:" + port); HiddenServiceProperties hsProps;
Map<String, String> response;
try { try {
// Use the control connection to set up the hidden service hsProps = tor.publishHiddenService(localPort, 80, privKey);
if (privKey == null) {
response = controlConnection.addOnion("NEW:ED25519-V3",
portLines, null);
} else {
response = controlConnection.addOnion(privKey, portLines);
}
} catch (IOException e) { } catch (IOException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
return; return;
} }
if (!response.containsKey(HS_ADDRESS)) {
LOG.warning("Tor did not return a hidden service address");
return;
}
if (privKey == null && !response.containsKey(HS_PRIVKEY)) {
LOG.warning("Tor did not return a private key");
return;
}
String onion3 = response.get(HS_ADDRESS);
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO)) {
LOG.info("V3 hidden service " + scrubOnion(onion3)); LOG.info("V3 hidden service " + scrubOnion(hsProps.onion));
} }
if (privKey == null) { if (privKey == null) {
// Publish the hidden service's onion hostname in transport props // Publish the hidden service's onion hostname in transport props
TransportProperties p = new TransportProperties(); TransportProperties p = new TransportProperties();
p.put(PROP_ONION_V3, onion3); p.put(PROP_ONION_V3, hsProps.onion);
callback.mergeLocalProperties(p); callback.mergeLocalProperties(p);
// Save the hidden service's private key for next time // Save the hidden service's private key for next time
Settings s = new Settings(); Settings s = new Settings();
s.put(HS_PRIVATE_KEY_V3, response.get(HS_PRIVKEY)); s.put(HS_PRIVATE_KEY_V3, hsProps.privKey);
callback.mergeSettings(s); callback.mergeSettings(s);
} }
} }
@@ -547,50 +271,28 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
} }
} }
protected void enableNetwork(boolean enable) throws IOException {
if (!state.enableNetwork(enable)) return; // Unchanged
controlConnection.setConf("DisableNetwork", enable ? "0" : "1");
}
private void enableBridges(List<BridgeType> bridgeTypes, String countryCode) private void enableBridges(List<BridgeType> bridgeTypes, String countryCode)
throws IOException { throws IOException {
if (!state.setBridgeTypes(bridgeTypes)) return; // Unchanged
if (bridgeTypes.isEmpty()) { if (bridgeTypes.isEmpty()) {
controlConnection.setConf("UseBridges", "0"); tor.disableBridges();
controlConnection.resetConf(singletonList("Bridge"));
} else { } else {
Collection<String> conf = new ArrayList<>(); List<String> bridges = new ArrayList<>();
conf.add("UseBridges 1");
boolean letsEncrypt = canVerifyLetsEncryptCerts();
for (BridgeType bridgeType : bridgeTypes) { for (BridgeType bridgeType : bridgeTypes) {
conf.addAll(circumventionProvider bridges.addAll(circumventionProvider.getBridges(bridgeType,
.getBridges(bridgeType, countryCode, letsEncrypt)); countryCode, canVerifyLetsEncryptCerts));
} }
controlConnection.setConf(conf); tor.enableBridges(bridges);
} }
} }
/**
* Returns true if this device can verify Let's Encrypt certificates signed
* with the IdentTrust DST Root X3 certificate, which expired at the end of
* September 2021.
*/
protected boolean canVerifyLetsEncryptCerts() {
return true;
}
@Override @Override
public void stop() { public void stop() {
ServerSocket ss = state.setStopped(); ServerSocket ss = state.setStopped();
tryToClose(ss, LOG, WARNING); tryToClose(ss, LOG, WARNING);
if (controlSocket != null && controlConnection != null) { try {
try { tor.stop();
LOG.info("Stopping Tor"); } catch (IOException e) {
controlConnection.shutdownTor("TERM"); logException(LOG, WARNING, e);
controlSocket.close();
} catch (IOException e) {
logException(LOG, WARNING, e);
}
} }
} }
@@ -701,6 +403,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
TransportProperties remoteProperties = new TransportProperties(); TransportProperties remoteProperties = new TransportProperties();
remoteProperties.put(PROP_ONION_V3, remoteOnion); remoteProperties.put(PROP_ONION_V3, remoteOnion);
try { try {
@SuppressWarnings("resource")
ServerSocket ss = new ServerSocket(); ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress("127.0.0.1", 0)); ss.bind(new InetSocketAddress("127.0.0.1", 0));
int port = ss.getLocalPort(); int port = ss.getLocalPort();
@@ -717,9 +420,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
LOG.info("Rendezvous server socket closed"); LOG.info("Rendezvous server socket closed");
} }
}); });
Map<Integer, String> portLines = tor.publishHiddenService(port, 80, blob);
singletonMap(80, "127.0.0.1:" + port);
controlConnection.addOnion(blob, portLines);
return new RendezvousEndpoint() { return new RendezvousEndpoint() {
@Override @Override
@@ -729,8 +430,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
controlConnection.delOnion(localOnion); try {
tryToClose(ss, LOG, WARNING); tor.removeHiddenService(localOnion);
} finally {
tryToClose(ss, LOG, WARNING);
}
} }
}; };
} catch (IOException e) { } catch (IOException e) {
@@ -739,121 +443,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
} }
} }
@Override
public void circuitStatus(String status, String id, String path) {
// In case of races between receiving CIRCUIT_ESTABLISHED and setting
// DisableNetwork, set our circuitBuilt flag if not already set
if (status.equals("BUILT") && state.setCircuitBuilt(true)) {
LOG.info("Circuit built");
backoff.reset();
}
}
@Override
public void streamStatus(String status, String id, String target) {
}
@Override
public void orConnStatus(String status, String orName) {
if (LOG.isLoggable(INFO)) LOG.info("OR connection " + status);
if (status.equals("CONNECTED")) state.onOrConnectionConnected();
else if (status.equals("CLOSED")) state.onOrConnectionClosed();
}
@Override
public void bandwidthUsed(long read, long written) {
}
@Override
public void newDescriptors(List<String> orList) {
}
@Override
public void message(String severity, String msg) {
if (LOG.isLoggable(INFO)) LOG.info(severity + " " + msg);
}
@Override
public void unrecognized(String type, String msg) {
if (type.equals("STATUS_CLIENT")) {
handleClientStatus(removeSeverity(msg));
} else if (type.equals("STATUS_GENERAL")) {
handleGeneralStatus(removeSeverity(msg));
} else if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) {
String[] parts = msg.split(" ");
if (parts.length < 2) {
LOG.warning("Failed to parse HS_DESC UPLOADED event");
} else if (LOG.isLoggable(INFO)) {
LOG.info("V3 descriptor uploaded for " + scrubOnion(parts[1]));
}
}
}
@Override
public void controlConnectionClosed() {
if (state.isTorRunning()) {
// TODO: Restart the Tor process
LOG.warning("Control connection closed");
}
}
private String removeSeverity(String msg) {
return msg.replaceFirst("[^ ]+ ", "");
}
private void handleClientStatus(String msg) {
if (msg.startsWith("BOOTSTRAP PROGRESS=100")) {
LOG.info("Bootstrapped");
state.setBootstrapped();
backoff.reset();
} else if (msg.startsWith("CIRCUIT_ESTABLISHED")) {
if (state.setCircuitBuilt(true)) {
LOG.info("Circuit built");
backoff.reset();
}
} else if (msg.startsWith("CIRCUIT_NOT_ESTABLISHED")) {
if (state.setCircuitBuilt(false)) {
LOG.info("Circuit not built");
// TODO: Disable and re-enable network to prompt Tor to rebuild
// its guard/bridge connections? This will also close any
// established circuits, which might still be functioning
}
}
}
private void handleGeneralStatus(String msg) {
if (msg.startsWith("CLOCK_JUMPED")) {
Long time = parseLongArgument(msg, "TIME");
if (time != null && LOG.isLoggable(WARNING)) {
LOG.warning("Clock jumped " + time + " seconds");
}
} else if (msg.startsWith("CLOCK_SKEW")) {
Long skew = parseLongArgument(msg, "SKEW");
if (skew != null && LOG.isLoggable(WARNING)) {
LOG.warning("Clock is skewed by " + skew + " seconds");
}
}
}
@Nullable
private Long parseLongArgument(String msg, String argName) {
String[] args = msg.split(" ");
for (String arg : args) {
if (arg.startsWith(argName + "=")) {
try {
return Long.parseLong(arg.substring(argName.length() + 1));
} catch (NumberFormatException e) {
break;
}
}
}
if (LOG.isLoggable(WARNING)) {
LOG.warning("Failed to parse " + argName + " from '" + msg + "'");
}
return null;
}
@Override @Override
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) { if (e instanceof SettingsUpdatedEvent) {
@@ -876,7 +465,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private void updateConnectionStatus(NetworkStatus status, private void updateConnectionStatus(NetworkStatus status,
boolean charging) { boolean charging) {
connectionStatusExecutor.execute(() -> { connectionStatusExecutor.execute(() -> {
if (!state.isTorRunning()) return; if (!tor.isTorRunning()) return;
boolean online = status.isConnected(); boolean online = status.isConnected();
boolean wifi = status.isWifi(); boolean wifi = status.isWifi();
boolean ipv6Only = status.isIpv6Only(); boolean ipv6Only = status.isIpv6Only();
@@ -960,41 +549,22 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
try { try {
if (enableNetwork) { if (enableNetwork) {
enableBridges(bridgeTypes, country); enableBridges(bridgeTypes, country);
enableConnectionPadding(enableConnectionPadding); tor.enableConnectionPadding(enableConnectionPadding);
enableIpv6(ipv6Only); tor.enableIpv6(ipv6Only);
} }
enableNetwork(enableNetwork); tor.enableNetwork(enableNetwork);
} catch (IOException e) { } catch (IOException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
} }
}); });
} }
private void enableConnectionPadding(boolean enable) throws IOException {
if (!state.enableConnectionPadding(enable)) return; // Unchanged
controlConnection.setConf("ConnectionPadding", enable ? "1" : "0");
}
private void enableIpv6(boolean enable) throws IOException {
if (!state.enableIpv6(enable)) return; // Unchanged
controlConnection.setConf("ClientUseIPv4", enable ? "0" : "1");
controlConnection.setConf("ClientUseIPv6", enable ? "1" : "0");
}
@ThreadSafe @ThreadSafe
@NotNullByDefault @NotNullByDefault
protected class PluginState { private class PluginState {
@GuardedBy("this") @GuardedBy("this")
private boolean started = false, private boolean settingsChecked = false;
stopped = false,
networkInitialised = false,
networkEnabled = false,
paddingEnabled = false,
ipv6Enabled = false,
bootstrapped = false,
circuitBuilt = false,
settingsChecked = false;
@GuardedBy("this") @GuardedBy("this")
private int reasonsDisabled = 0; private int reasonsDisabled = 0;
@@ -1003,84 +573,13 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Nullable @Nullable
private ServerSocket serverSocket = null; private ServerSocket serverSocket = null;
@GuardedBy("this")
private int orConnectionsConnected = 0;
@GuardedBy("this")
private List<BridgeType> bridgeTypes = emptyList();
private synchronized void setStarted() {
started = true;
callback.pluginStateChanged(getState());
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private synchronized boolean isTorRunning() {
return started && !stopped;
}
@Nullable @Nullable
private synchronized ServerSocket setStopped() { private synchronized ServerSocket setStopped() {
stopped = true;
ServerSocket ss = serverSocket; ServerSocket ss = serverSocket;
serverSocket = null; serverSocket = null;
callback.pluginStateChanged(getState());
return ss; return ss;
} }
private synchronized void setBootstrapped() {
boolean wasBootstrapped = bootstrapped;
bootstrapped = true;
if (!wasBootstrapped) callback.pluginStateChanged(getState());
}
/**
* Sets the `circuitBuilt` flag and returns true if the flag has
* changed.
*/
private synchronized boolean setCircuitBuilt(boolean built) {
if (built == circuitBuilt) return false; // Unchanged
circuitBuilt = built;
callback.pluginStateChanged(getState());
return true; // Changed
}
/**
* Sets the `networkEnabled` flag and returns true if the flag has
* changed.
*/
private synchronized boolean enableNetwork(boolean enable) {
boolean wasInitialised = networkInitialised;
boolean wasEnabled = networkEnabled;
networkInitialised = true;
networkEnabled = enable;
if (!enable) circuitBuilt = false;
if (!wasInitialised || enable != wasEnabled) {
callback.pluginStateChanged(getState());
}
return enable != wasEnabled;
}
/**
* Sets the `paddingEnabled` flag and returns true if the flag has
* changed. Doesn't affect getState().
*/
private synchronized boolean enableConnectionPadding(boolean enable) {
if (enable == paddingEnabled) return false; // Unchanged
paddingEnabled = enable;
return true; // Changed
}
/**
* Sets the `ipv6Enabled` flag and returns true if the flag has
* changed. Doesn't affect getState().
*/
private synchronized boolean enableIpv6(boolean enable) {
if (enable == ipv6Enabled) return false; // Unchanged
ipv6Enabled = enable;
return true; // Changed
}
private synchronized void setReasonsDisabled(int reasons) { private synchronized void setReasonsDisabled(int reasons) {
boolean wasChecked = settingsChecked; boolean wasChecked = settingsChecked;
settingsChecked = true; settingsChecked = true;
@@ -1093,7 +592,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
// Doesn't affect getState() // Doesn't affect getState()
private synchronized boolean setServerSocket(ServerSocket ss) { private synchronized boolean setServerSocket(ServerSocket ss) {
if (stopped || serverSocket != null) return false; if (serverSocket != null || !tor.isTorRunning()) return false;
serverSocket = ss; serverSocket = ss;
return true; return true;
} }
@@ -1103,57 +602,22 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (serverSocket == ss) serverSocket = null; if (serverSocket == ss) serverSocket = null;
} }
/** private synchronized State getState() {
* Sets the list of bridge types being used and returns true if the return getState(tor.getTorState());
* list has changed. The list is empty if bridges are disabled.
* Doesn't affect getState().
*/
private synchronized boolean setBridgeTypes(List<BridgeType> types) {
if (types.equals(bridgeTypes)) return false; // Unchanged
bridgeTypes = types;
return true; // Changed
} }
private synchronized State getState() { private synchronized State getState(TorState torState) {
if (!started || stopped || !settingsChecked) { if (torState == TorState.STARTING_STOPPING || !settingsChecked) {
return STARTING_STOPPING; return STARTING_STOPPING;
} }
if (reasonsDisabled != 0) return DISABLED; if (reasonsDisabled != 0) return DISABLED;
if (!networkInitialised) return ENABLING; if (torState == TorState.CONNECTING) return ENABLING;
if (!networkEnabled) return INACTIVE; if (torState == TorState.CONNECTED) return ACTIVE;
return bootstrapped && circuitBuilt && orConnectionsConnected > 0 return INACTIVE;
? ACTIVE : ENABLING;
} }
private synchronized int getReasonsDisabled() { private synchronized int getReasonsDisabled() {
return getState() == DISABLED ? reasonsDisabled : 0; return getState() == DISABLED ? reasonsDisabled : 0;
} }
private synchronized void onOrConnectionConnected() {
int oldConnected = orConnectionsConnected;
orConnectionsConnected++;
logOrConnections();
if (oldConnected == 0) callback.pluginStateChanged(getState());
}
private synchronized void onOrConnectionClosed() {
int oldConnected = orConnectionsConnected;
orConnectionsConnected--;
if (orConnectionsConnected < 0) {
LOG.warning("Count was zero before connection closed");
orConnectionsConnected = 0;
}
logOrConnections();
if (orConnectionsConnected == 0 && oldConnected != 0) {
callback.pluginStateChanged(getState());
}
}
@GuardedBy("this")
private void logOrConnections() {
if (LOG.isLoggable(INFO)) {
LOG.info(orConnectionsConnected + " OR connections connected");
}
}
} }
} }

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.tor;
import org.briarproject.bramble.api.battery.BatteryManager; import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventExecutor;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.network.NetworkManager; import org.briarproject.bramble.api.network.NetworkManager;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
@@ -17,7 +18,6 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.ResourceProvider;
import org.briarproject.bramble.api.system.WakefulIoExecutor; import org.briarproject.bramble.api.system.WakefulIoExecutor;
import org.briarproject.nullsafety.NotNullByDefault; import org.briarproject.nullsafety.NotNullByDefault;
@@ -45,13 +45,12 @@ abstract class TorPluginFactory implements DuplexPluginFactory {
private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins
private static final double BACKOFF_BASE = 1.2; private static final double BACKOFF_BASE = 1.2;
protected final Executor ioExecutor, wakefulIoExecutor; protected final Executor ioExecutor, eventExecutor, wakefulIoExecutor;
protected final NetworkManager networkManager; protected final NetworkManager networkManager;
protected final LocationUtils locationUtils; protected final LocationUtils locationUtils;
protected final EventBus eventBus; protected final EventBus eventBus;
protected final SocketFactory torSocketFactory; protected final SocketFactory torSocketFactory;
protected final BackoffFactory backoffFactory; protected final BackoffFactory backoffFactory;
protected final ResourceProvider resourceProvider;
protected final CircumventionProvider circumventionProvider; protected final CircumventionProvider circumventionProvider;
protected final BatteryManager batteryManager; protected final BatteryManager batteryManager;
protected final Clock clock; protected final Clock clock;
@@ -61,13 +60,13 @@ abstract class TorPluginFactory implements DuplexPluginFactory {
protected final int torControlPort; protected final int torControlPort;
TorPluginFactory(@IoExecutor Executor ioExecutor, TorPluginFactory(@IoExecutor Executor ioExecutor,
@EventExecutor Executor eventExecutor,
@WakefulIoExecutor Executor wakefulIoExecutor, @WakefulIoExecutor Executor wakefulIoExecutor,
NetworkManager networkManager, NetworkManager networkManager,
LocationUtils locationUtils, LocationUtils locationUtils,
EventBus eventBus, EventBus eventBus,
SocketFactory torSocketFactory, SocketFactory torSocketFactory,
BackoffFactory backoffFactory, BackoffFactory backoffFactory,
ResourceProvider resourceProvider,
CircumventionProvider circumventionProvider, CircumventionProvider circumventionProvider,
BatteryManager batteryManager, BatteryManager batteryManager,
Clock clock, Clock clock,
@@ -76,13 +75,13 @@ abstract class TorPluginFactory implements DuplexPluginFactory {
@TorSocksPort int torSocksPort, @TorSocksPort int torSocksPort,
@TorControlPort int torControlPort) { @TorControlPort int torControlPort) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.eventExecutor = eventExecutor;
this.wakefulIoExecutor = wakefulIoExecutor; this.wakefulIoExecutor = wakefulIoExecutor;
this.networkManager = networkManager; this.networkManager = networkManager;
this.locationUtils = locationUtils; this.locationUtils = locationUtils;
this.eventBus = eventBus; this.eventBus = eventBus;
this.torSocketFactory = torSocketFactory; this.torSocketFactory = torSocketFactory;
this.backoffFactory = backoffFactory; this.backoffFactory = backoffFactory;
this.resourceProvider = resourceProvider;
this.circumventionProvider = circumventionProvider; this.circumventionProvider = circumventionProvider;
this.batteryManager = batteryManager; this.batteryManager = batteryManager;
this.clock = clock; this.clock = clock;

View File

@@ -0,0 +1,673 @@
package org.briarproject.bramble.plugin.tor.wrapper;
import net.freehaven.tor.control.EventHandler;
import net.freehaven.tor.control.TorControlConnection;
import org.briarproject.nullsafety.InterfaceNotNullByDefault;
import org.briarproject.nullsafety.NotNullByDefault;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static net.freehaven.tor.control.TorControlCommands.HS_ADDRESS;
import static net.freehaven.tor.control.TorControlCommands.HS_PRIVKEY;
import static org.briarproject.bramble.plugin.tor.wrapper.TorUtils.UTF_8;
import static org.briarproject.bramble.plugin.tor.wrapper.TorUtils.copyAndClose;
import static org.briarproject.bramble.plugin.tor.wrapper.TorUtils.scrubOnion;
import static org.briarproject.bramble.plugin.tor.wrapper.TorUtils.tryToClose;
import static org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.TorState.CONNECTED;
import static org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.TorState.CONNECTING;
import static org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.TorState.DISABLED;
import static org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.TorState.STARTING_STOPPING;
import static org.briarproject.nullsafety.NullSafety.requireNonNull;
@InterfaceNotNullByDefault
abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
private static final String[] EVENTS = {
"CIRC",
"ORCONN",
"STATUS_GENERAL",
"STATUS_CLIENT",
"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;
protected final Executor ioExecutor;
protected final Executor eventExecutor;
private final String architecture;
private final File torDirectory, configFile, doneFile, cookieFile;
private final int torSocksPort;
private final int torControlPort;
private final AtomicBoolean used = new AtomicBoolean(false);
protected final NetworkState state = new NetworkState();
private volatile Socket controlSocket = null;
private volatile TorControlConnection controlConnection = null;
protected abstract int getProcessId();
protected abstract long getLastUpdateTime();
protected abstract InputStream getResourceInputStream(String name,
String extension);
AbstractTorWrapper(Executor ioExecutor,
Executor eventExecutor,
String architecture,
File torDirectory,
int torSocksPort,
int torControlPort) {
this.ioExecutor = ioExecutor;
this.eventExecutor = eventExecutor;
this.architecture = architecture;
this.torDirectory = torDirectory;
this.torSocksPort = torSocksPort;
this.torControlPort = torControlPort;
configFile = new File(torDirectory, "torrc");
doneFile = new File(torDirectory, "done");
cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
}
protected File getTorExecutableFile() {
return new File(torDirectory, "tor");
}
protected File getObfs4ExecutableFile() {
return new File(torDirectory, "obfs4proxy");
}
protected File getSnowflakeExecutableFile() {
return new File(torDirectory, "snowflake");
}
@Override
public void setStateObserver(@Nullable StateObserver stateObserver) {
state.setObserver(stateObserver);
}
@Override
public void start() throws IOException, InterruptedException {
if (used.getAndSet(true)) throw new IllegalStateException();
if (!torDirectory.exists()) {
if (!torDirectory.mkdirs()) {
throw new IOException("Could not create Tor directory");
}
}
// Install or update the assets if necessary
if (!assetsAreUpToDate()) installAssets();
// Start from the default config every time
extract(getConfigInputStream(), configFile);
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());
Process torProcess;
ProcessBuilder pb =
new ProcessBuilder(torPath, "-f", configPath, OWNER, pid);
Map<String, String> env = pb.environment();
env.put("HOME", torDirectory.getAbsolutePath());
pb.directory(torDirectory);
pb.redirectErrorStream(true);
try {
torProcess = pb.start();
} catch (SecurityException e) {
throw new IOException(e);
}
// Wait for the Tor process to start
waitForTorToStart(torProcess);
// Wait for the auth cookie file to be created/updated
long start = System.currentTimeMillis();
while (cookieFile.length() < 32) {
if (System.currentTimeMillis() - start > COOKIE_TIMEOUT_MS) {
throw new IOException("Auth cookie not created");
}
//noinspection BusyWait
Thread.sleep(COOKIE_POLLING_INTERVAL_MS);
}
LOG.info("Auth cookie created");
// 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 info = controlConnection.getInfo("status/bootstrap-phase");
if (info != null && info.contains("PROGRESS=100")) {
LOG.info("Tor has already bootstrapped");
state.setBootstrapped();
}
// Check whether Tor has already built a circuit
info = controlConnection.getInfo("status/circuit-established");
if ("1".equals(info)) {
LOG.info("Tor has already built a circuit");
state.setCircuitBuilt(true);
}
state.setStarted();
}
private boolean assetsAreUpToDate() {
return doneFile.lastModified() > getLastUpdateTime();
}
private void installAssets() throws IOException {
// The done file may already exist from a previous installation
//noinspection ResultOfMethodCallIgnored
doneFile.delete();
installTorExecutable();
installObfs4Executable();
installSnowflakeExecutable();
extract(getConfigInputStream(), configFile);
if (!doneFile.createNewFile()) {
LOG.warning("Failed to create done file");
}
}
protected void extract(InputStream in, File dest) throws IOException {
@SuppressWarnings("IOStreamConstructor")
OutputStream out = new FileOutputStream(dest);
copyAndClose(in, out);
}
protected void installTorExecutable() throws IOException {
if (LOG.isLoggable(INFO)) {
LOG.info("Installing Tor binary for " + architecture);
}
File torFile = getTorExecutableFile();
extract(getExecutableInputStream("tor"), torFile);
if (!torFile.setExecutable(true, true)) throw new IOException();
}
protected void installObfs4Executable() throws IOException {
if (LOG.isLoggable(INFO)) {
LOG.info("Installing obfs4proxy binary for " + architecture);
}
File obfs4File = getObfs4ExecutableFile();
extract(getExecutableInputStream("obfs4proxy"), obfs4File);
if (!obfs4File.setExecutable(true, true)) throw new IOException();
}
protected void installSnowflakeExecutable() throws IOException {
if (LOG.isLoggable(INFO)) {
LOG.info("Installing snowflake binary for " + architecture);
}
File snowflakeFile = getSnowflakeExecutableFile();
extract(getExecutableInputStream("snowflake"), snowflakeFile);
if (!snowflakeFile.setExecutable(true, true)) throw new IOException();
}
private InputStream getExecutableInputStream(String basename) {
String ext = getExecutableExtension();
return requireNonNull(
getResourceInputStream(architecture + "/" + basename, ext));
}
protected String getExecutableExtension() {
return "";
}
private static void append(StringBuilder strb, String name, Object value) {
strb.append(name);
strb.append(" ");
strb.append(value);
strb.append("\n");
}
private InputStream getConfigInputStream() {
File dataDirectory = new File(torDirectory, ".tor");
StringBuilder strb = new StringBuilder();
append(strb, "ControlPort", torControlPort);
append(strb, "CookieAuthentication", 1);
append(strb, "DataDirectory", dataDirectory.getAbsolutePath());
append(strb, "DisableNetwork", 1);
append(strb, "RunAsDaemon", 1);
append(strb, "SafeSocks", 1);
append(strb, "SocksPort", torSocksPort);
strb.append("GeoIPFile\n");
strb.append("GeoIPv6File\n");
append(strb, "ConnectionPadding", 0);
String obfs4Path = getObfs4ExecutableFile().getAbsolutePath();
append(strb, "ClientTransportPlugin obfs4 exec", obfs4Path);
append(strb, "ClientTransportPlugin meek_lite exec", obfs4Path);
String snowflakePath = getSnowflakeExecutableFile().getAbsolutePath();
append(strb, "ClientTransportPlugin snowflake exec", snowflakePath);
return new ByteArrayInputStream(strb.toString().getBytes(UTF_8));
}
private byte[] read(File f) throws IOException {
byte[] b = new byte[(int) f.length()];
FileInputStream in = new FileInputStream(f);
try {
int offset = 0;
while (offset < b.length) {
int read = in.read(b, offset, b.length - offset);
if (read == -1) throw new EOFException();
offset += read;
}
return b;
} finally {
tryToClose(in, LOG, WARNING);
}
}
protected void waitForTorToStart(Process torProcess)
throws InterruptedException, IOException {
Scanner stdout = new Scanner(torProcess.getInputStream());
// Log the first line of stdout (contains Tor and library versions)
if (stdout.hasNextLine()) LOG.info(stdout.nextLine());
// Read the process's stdout (and redirected stderr) until it detaches
while (stdout.hasNextLine()) stdout.nextLine();
stdout.close();
// Wait for the process to detach or exit
int exit = torProcess.waitFor();
if (exit != 0) throw new IOException("Tor exited with value " + exit);
}
@Override
public HiddenServiceProperties publishHiddenService(int localPort,
int remotePort, @Nullable String privKey) throws IOException {
Map<Integer, String> portLines =
singletonMap(remotePort, "127.0.0.1:" + localPort);
// Use the control connection to set up the hidden service
Map<String, String> response;
if (privKey == null) {
response = getControlConnection().addOnion("NEW:ED25519-V3",
portLines, null);
} else {
response = getControlConnection().addOnion(privKey, portLines);
}
if (!response.containsKey(HS_ADDRESS)) {
throw new IOException("Missing hidden service address");
}
if (privKey == null && !response.containsKey(HS_PRIVKEY)) {
throw new IOException("Missing private key");
}
String onion = response.get(HS_ADDRESS);
if (privKey == null) privKey = response.get(HS_PRIVKEY);
return new HiddenServiceProperties(onion, privKey);
}
@Override
public void removeHiddenService(String onion) throws IOException {
getControlConnection().delOnion(onion);
}
@Override
public void enableNetwork(boolean enable) throws IOException {
if (!state.enableNetwork(enable)) return; // Unchanged
getControlConnection().setConf("DisableNetwork", enable ? "0" : "1");
}
@Override
public void enableBridges(List<String> bridges) throws IOException {
if (!state.setBridges(bridges)) return; // Unchanged
List<String> conf = new ArrayList<>(bridges.size() + 1);
conf.add("UseBridges 1");
conf.addAll(bridges);
getControlConnection().setConf(conf);
}
@Override
public void disableBridges() throws IOException {
if (!state.setBridges(emptyList())) return; // Unchanged
getControlConnection().setConf("UseBridges", "0");
}
@Override
public void stop() throws IOException {
state.setStopped();
if (controlSocket != null && controlConnection != null) {
LOG.info("Stopping Tor");
try {
controlConnection.shutdownTor("TERM");
} finally {
tryToClose(controlSocket, LOG, WARNING);
}
}
}
@Override
public void circuitStatus(String status, String id, String path) {
// In case of races between receiving CIRCUIT_ESTABLISHED and setting
// DisableNetwork, set our circuitBuilt flag if not already set
if (status.equals("BUILT") && state.setCircuitBuilt(true)) {
LOG.info("Circuit built");
}
}
@Override
public void streamStatus(String status, String id, String target) {
}
@Override
public void orConnStatus(String status, String orName) {
if (LOG.isLoggable(INFO)) LOG.info("OR connection " + status);
if (status.equals("CONNECTED")) state.onOrConnectionConnected();
else if (status.equals("CLOSED")) state.onOrConnectionClosed();
}
@Override
public void bandwidthUsed(long read, long written) {
}
@Override
public void newDescriptors(List<String> orList) {
}
@Override
public void message(String severity, String msg) {
if (LOG.isLoggable(INFO)) LOG.info(severity + " " + msg);
}
@Override
public void unrecognized(String type, String msg) {
if (type.equals("STATUS_CLIENT")) {
handleClientStatus(removeSeverity(msg));
} else if (type.equals("STATUS_GENERAL")) {
handleGeneralStatus(removeSeverity(msg));
} else if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) {
String[] parts = msg.split(" ");
if (parts.length < 2) {
LOG.warning("Failed to parse HS_DESC UPLOADED event");
} else if (LOG.isLoggable(INFO)) {
LOG.info("V3 descriptor uploaded for " + scrubOnion(parts[1]));
}
}
}
private String removeSeverity(String msg) {
return msg.replaceFirst("[^ ]+ ", "");
}
private void handleClientStatus(String msg) {
if (msg.startsWith("BOOTSTRAP PROGRESS=100")) {
LOG.info("Bootstrapped");
state.setBootstrapped();
} else if (msg.startsWith("CIRCUIT_ESTABLISHED")) {
if (state.setCircuitBuilt(true)) LOG.info("Circuit built");
} else if (msg.startsWith("CIRCUIT_NOT_ESTABLISHED")) {
if (state.setCircuitBuilt(false)) {
LOG.info("Circuit not built");
// TODO: Disable and re-enable network to prompt Tor to rebuild
// its guard/bridge connections? This will also close any
// established circuits, which might still be functioning
}
}
}
private void handleGeneralStatus(String msg) {
if (msg.startsWith("CLOCK_JUMPED")) {
Long time = parseLongArgument(msg, "TIME");
if (time != null && LOG.isLoggable(WARNING)) {
LOG.warning("Clock jumped " + time + " seconds");
}
} else if (msg.startsWith("CLOCK_SKEW")) {
Long skew = parseLongArgument(msg, "SKEW");
if (skew != null && LOG.isLoggable(WARNING)) {
LOG.warning("Clock is skewed by " + skew + " seconds");
}
}
}
@Nullable
private Long parseLongArgument(String msg, String argName) {
String[] args = msg.split(" ");
for (String arg : args) {
if (arg.startsWith(argName + "=")) {
try {
return Long.parseLong(arg.substring(argName.length() + 1));
} catch (NumberFormatException e) {
break;
}
}
}
if (LOG.isLoggable(WARNING)) {
LOG.warning("Failed to parse " + argName + " from '" + msg + "'");
}
return null;
}
@Override
public void controlConnectionClosed() {
if (state.isTorRunning()) {
// TODO: Restart the Tor process
LOG.warning("Control connection closed");
}
}
@Override
public void enableConnectionPadding(boolean enable) throws IOException {
if (!state.enableConnectionPadding(enable)) return; // Unchanged
getControlConnection().setConf("ConnectionPadding", enable ? "1" : "0");
}
@Override
public void enableIpv6(boolean enable) throws IOException {
if (!state.enableIpv6(enable)) return; // Unchanged
getControlConnection().setConf("ClientUseIPv4", enable ? "0" : "1");
getControlConnection().setConf("ClientUseIPv6", enable ? "1" : "0");
}
@Override
public TorState getTorState() {
return state.getState();
}
@Override
public boolean isTorRunning() {
return state.isTorRunning();
}
private TorControlConnection getControlConnection() throws IOException {
TorControlConnection controlConnection = this.controlConnection;
if (controlConnection == null) {
throw new IOException("Control connection not opened");
}
return controlConnection;
}
@ThreadSafe
@NotNullByDefault
private class NetworkState {
@GuardedBy("this")
@Nullable
private StateObserver observer = null;
@GuardedBy("this")
private boolean started = false,
stopped = false,
networkInitialised = false,
networkEnabled = false,
paddingEnabled = false,
ipv6Enabled = false,
bootstrapped = false,
circuitBuilt = false;
@GuardedBy("this")
private int orConnectionsConnected = 0;
@GuardedBy("this")
@Nullable
private TorState state = null;
private synchronized void setObserver(
@Nullable StateObserver observer) {
this.observer = observer;
}
@GuardedBy("this")
private void updateObserver() {
TorState newState = getState();
if (newState != state) {
state = newState;
// Post the new state to the event executor. The contract of
// this executor is to execute tasks in the order they're
// submitted, so state changes will be observed in the correct
// order but outside the lock
if (observer != null) {
eventExecutor.execute(() ->
observer.observeState(newState));
}
}
}
@GuardedBy("this")
private List<String> bridges = emptyList();
private synchronized void setStarted() {
started = true;
updateObserver();
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private synchronized boolean isTorRunning() {
return started && !stopped;
}
private synchronized void setStopped() {
stopped = true;
updateObserver();
}
private synchronized void setBootstrapped() {
if (bootstrapped) return;
bootstrapped = true;
updateObserver();
}
/**
* Sets the `circuitBuilt` flag and returns true if the flag has
* changed.
*/
private synchronized boolean setCircuitBuilt(boolean built) {
if (built == circuitBuilt) return false; // Unchanged
circuitBuilt = built;
updateObserver();
return true; // Changed
}
/**
* Sets the `networkEnabled` flag and returns true if the flag has
* changed.
*/
private synchronized boolean enableNetwork(boolean enable) {
boolean wasInitialised = networkInitialised;
boolean wasEnabled = networkEnabled;
networkInitialised = true;
networkEnabled = enable;
if (!enable) circuitBuilt = false;
if (!wasInitialised || enable != wasEnabled) {
updateObserver();
}
return enable != wasEnabled;
}
/**
* Sets the `paddingEnabled` flag and returns true if the flag has
* changed. Doesn't affect getState().
*/
private synchronized boolean enableConnectionPadding(boolean enable) {
if (enable == paddingEnabled) return false; // Unchanged
paddingEnabled = enable;
return true; // Changed
}
/**
* Sets the `ipv6Enabled` flag and returns true if the flag has
* changed. Doesn't affect getState().
*/
private synchronized boolean enableIpv6(boolean enable) {
if (enable == ipv6Enabled) return false; // Unchanged
ipv6Enabled = enable;
return true; // Changed
}
/**
* Sets the list of bridges being used and returns true if the
* list has changed. The list is empty if bridges are disabled.
* Doesn't affect getState().
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private synchronized boolean setBridges(List<String> bridges) {
if (this.bridges.equals(bridges)) return false; // Unchanged
this.bridges = bridges;
return true; // Changed
}
private synchronized TorState getState() {
if (!started || stopped) return STARTING_STOPPING;
if (!networkInitialised) return CONNECTING;
if (!networkEnabled) return DISABLED;
return bootstrapped && circuitBuilt && orConnectionsConnected > 0
? CONNECTED : CONNECTING;
}
private synchronized void onOrConnectionConnected() {
int oldConnected = orConnectionsConnected;
orConnectionsConnected++;
logOrConnections();
if (oldConnected == 0) updateObserver();
}
private synchronized void onOrConnectionClosed() {
int oldConnected = orConnectionsConnected;
orConnectionsConnected--;
if (orConnectionsConnected < 0) {
LOG.warning("Count was zero before connection closed");
orConnectionsConnected = 0;
}
logOrConnections();
if (orConnectionsConnected == 0 && oldConnected != 0) {
updateObserver();
}
}
@GuardedBy("this")
private void logOrConnections() {
if (LOG.isLoggable(INFO)) {
LOG.info(orConnectionsConnected + " OR connections connected");
}
}
}
}

View File

@@ -0,0 +1,66 @@
package org.briarproject.bramble.plugin.tor.wrapper;
import org.briarproject.nullsafety.NotNullByDefault;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.LOG;
@NotNullByDefault
class TorUtils {
@SuppressWarnings("CharsetObjectCanBeUsed")
static final Charset UTF_8 = Charset.forName("UTF-8");
static String scrubOnion(String onion) {
// Keep first three characters of onion address
return onion.substring(0, 3) + "[scrubbed]";
}
static void copyAndClose(InputStream in, OutputStream out) {
byte[] buf = new byte[4096];
try {
while (true) {
int read = in.read(buf);
if (read == -1) break;
out.write(buf, 0, read);
}
in.close();
out.flush();
out.close();
} catch (IOException e) {
tryToClose(in, LOG, WARNING);
tryToClose(out, LOG, WARNING);
}
}
static void tryToClose(@Nullable Closeable c, Logger logger, Level level) {
try {
if (c != null) c.close();
} catch (IOException e) {
logException(logger, level, e);
}
}
static void tryToClose(@Nullable Socket s, Logger logger, Level level) {
try {
if (s != null) s.close();
} catch (IOException e) {
logException(logger, level, e);
}
}
private static void logException(Logger logger, Level level, Throwable t) {
if (logger.isLoggable(level)) logger.log(level, t.toString(), t);
}
}

View File

@@ -0,0 +1,153 @@
package org.briarproject.bramble.plugin.tor.wrapper;
import org.briarproject.nullsafety.NotNullByDefault;
import java.io.IOException;
import java.util.List;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.util.logging.Logger.getLogger;
@NotNullByDefault
public interface TorWrapper {
Logger LOG = getLogger(TorWrapper.class.getName());
/**
* Starts the Tor process.
* <p>
* This method must only be called once. To restart the Tor process, stop
* this wrapper instance and then create a new instance.
*/
void start() throws IOException, InterruptedException;
/**
* Tell the Tor process to stop and returns without waiting for the
* process to exit.
*/
void stop() throws IOException;
/**
* Sets an observer for observing the state of the wrapper, replacing any
* existing observer, or removes any existing observer if the argument is
* null.
*/
void setStateObserver(@Nullable StateObserver stateObserver);
/**
* Returns the current state of the wrapper.
*/
TorState getTorState();
/**
* Returns true if the wrapper has been {@link #start() started} and not
* yet {@link #stop()} stopped.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean isTorRunning();
/**
* Publishes an ephemeral hidden service.
*
* @param localPort The local port on which the service is listening.
* @param remotePort The port number that clients of the service will see.
* @param privateKey The private key of the hidden service, in the form
* returned by a previous call to this method, or null if a new service
* should be created.
*/
HiddenServiceProperties publishHiddenService(int localPort,
int remotePort, @Nullable String privateKey) throws IOException;
/**
* Removes (unpublishes) an ephemeral hidden service that was created by
* calling {@link #publishHiddenService(int, int, String)}.
*/
void removeHiddenService(String onion) throws IOException;
/**
* Enables or disables the Tor process's network connection. The network
* connection is disabled by default.
*/
void enableNetwork(boolean enable) throws IOException;
/**
* Configures Tor to use the given list of bridges for connecting to the
* Tor network. Bridges are not used by default.
* <p>
* Each item in the list should be a bridge line in the same
* format that would be used in a torrc file (including the Bridge keyword).
*/
void enableBridges(List<String> bridges) throws IOException;
/**
* Configures Tor not to use bridges for connecting to the Tor network.
* Bridges are not used by default.
*/
void disableBridges() throws IOException;
/**
* Enables or disables connection padding. Padding is disabled by default.
*/
void enableConnectionPadding(boolean enable) throws IOException;
/**
* Configures Tor to use IPv6 or IPv4 for connecting to the Tor network.
* IPv4 is used by default.
*/
void enableIpv6(boolean ipv6Only) throws IOException;
/**
* The state of the Tor wrapper.
*/
enum TorState {
/**
* The Tor process is either starting or stopping.
*/
STARTING_STOPPING,
/**
* The Tor process has started, its network connection is enabled, and
* it is connecting (or reconnecting) to the Tor network.
*/
CONNECTING,
/**
* The Tor process has started, its network connection is enabled, and
* it has connected to the Tor network. In this state it should be
* possible to make connections via the SOCKS port.
*/
CONNECTED,
/**
* The Tor process has started but its network connection is disabled.
*/
DISABLED
}
/**
* An interface for observing changes to the {@link TorState state} of the
* Tor process.
*/
interface StateObserver {
/**
* This method is called whenever the state of the Tor process changes.
* The call happens on the event executor supplied to the wrapper's
* constructor.
*/
void observeState(TorState s);
}
class HiddenServiceProperties {
public final String onion, privKey;
HiddenServiceProperties(String onion, String privKey) {
this.onion = onion;
this.privKey = privKey;
}
}
}

View File

@@ -1,62 +0,0 @@
package org.briarproject.bramble.plugin.tor;
import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.network.NetworkManager;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.ResourceProvider;
import org.briarproject.nullsafety.NotNullByDefault;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.CodeSource;
import java.util.concurrent.Executor;
import javax.net.SocketFactory;
@NotNullByDefault
abstract class JavaTorPlugin extends TorPlugin {
JavaTorPlugin(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);
}
@Override
protected long getLastUpdateTime() {
CodeSource codeSource =
getClass().getProtectionDomain().getCodeSource();
if (codeSource == null) throw new AssertionError("CodeSource null");
try {
URI path = codeSource.getLocation().toURI();
File file = new File(path);
return file.lastModified();
} catch (URISyntaxException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -1,60 +0,0 @@
package org.briarproject.bramble.plugin.tor;
import com.sun.jna.Library;
import com.sun.jna.Native;
import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.network.NetworkManager;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.ResourceProvider;
import org.briarproject.nullsafety.NotNullByDefault;
import java.io.File;
import java.util.concurrent.Executor;
import javax.net.SocketFactory;
@NotNullByDefault
class UnixTorPlugin extends JavaTorPlugin {
UnixTorPlugin(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);
}
@Override
protected int getProcessId() {
return CLibrary.INSTANCE.getpid();
}
private interface CLibrary extends Library {
CLibrary INSTANCE = Native.loadLibrary("c", CLibrary.class);
int getpid();
}
}

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.tor;
import org.briarproject.bramble.api.battery.BatteryManager; import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventExecutor;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.network.NetworkManager; import org.briarproject.bramble.api.network.NetworkManager;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
@@ -13,8 +14,9 @@ import org.briarproject.bramble.api.plugin.TorDirectory;
import org.briarproject.bramble.api.plugin.TorSocksPort; import org.briarproject.bramble.api.plugin.TorSocksPort;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.ResourceProvider;
import org.briarproject.bramble.api.system.WakefulIoExecutor; import org.briarproject.bramble.api.system.WakefulIoExecutor;
import org.briarproject.bramble.plugin.tor.wrapper.TorWrapper;
import org.briarproject.bramble.plugin.tor.wrapper.UnixTorWrapper;
import org.briarproject.nullsafety.NotNullByDefault; import org.briarproject.nullsafety.NotNullByDefault;
import java.io.File; import java.io.File;
@@ -34,13 +36,13 @@ public class UnixTorPluginFactory extends TorPluginFactory {
@Inject @Inject
UnixTorPluginFactory(@IoExecutor Executor ioExecutor, UnixTorPluginFactory(@IoExecutor Executor ioExecutor,
@EventExecutor Executor eventExecutor,
@WakefulIoExecutor Executor wakefulIoExecutor, @WakefulIoExecutor Executor wakefulIoExecutor,
NetworkManager networkManager, NetworkManager networkManager,
LocationUtils locationUtils, LocationUtils locationUtils,
EventBus eventBus, EventBus eventBus,
SocketFactory torSocketFactory, SocketFactory torSocketFactory,
BackoffFactory backoffFactory, BackoffFactory backoffFactory,
ResourceProvider resourceProvider,
CircumventionProvider circumventionProvider, CircumventionProvider circumventionProvider,
BatteryManager batteryManager, BatteryManager batteryManager,
Clock clock, Clock clock,
@@ -48,8 +50,8 @@ public class UnixTorPluginFactory extends TorPluginFactory {
@TorDirectory File torDirectory, @TorDirectory File torDirectory,
@TorSocksPort int torSocksPort, @TorSocksPort int torSocksPort,
@TorControlPort int torControlPort) { @TorControlPort int torControlPort) {
super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, super(ioExecutor, eventExecutor, wakefulIoExecutor, networkManager,
eventBus, torSocketFactory, backoffFactory, resourceProvider, locationUtils, eventBus, torSocketFactory, backoffFactory,
circumventionProvider, batteryManager, clock, crypto, circumventionProvider, batteryManager, clock, crypto,
torDirectory, torSocksPort, torControlPort); torDirectory, torSocksPort, torControlPort);
} }
@@ -62,6 +64,7 @@ public class UnixTorPluginFactory extends TorPluginFactory {
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO)) {
LOG.info("System's os.arch is " + arch); LOG.info("System's os.arch is " + arch);
} }
//noinspection IfCanBeSwitch
if (arch.equals("amd64")) return "x86_64"; if (arch.equals("amd64")) return "x86_64";
else if (arch.equals("aarch64")) return "aarch64"; else if (arch.equals("aarch64")) return "aarch64";
else if (arch.equals("arm")) return "armhf"; else if (arch.equals("arm")) return "armhf";
@@ -72,11 +75,11 @@ public class UnixTorPluginFactory extends TorPluginFactory {
TorPlugin createPluginInstance(Backoff backoff, TorPlugin createPluginInstance(Backoff backoff,
TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback, TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback,
String architecture) { String architecture) {
return new UnixTorPlugin(ioExecutor, wakefulIoExecutor, TorWrapper tor = new UnixTorWrapper(ioExecutor, eventExecutor,
networkManager, locationUtils, torSocketFactory, clock, architecture, torDirectory, torSocksPort, torControlPort);
resourceProvider, circumventionProvider, batteryManager, return new TorPlugin(ioExecutor, wakefulIoExecutor, networkManager,
backoff, torRendezvousCrypto, callback, architecture, locationUtils, torSocketFactory, circumventionProvider,
MAX_LATENCY, MAX_IDLE_TIME, torDirectory, torSocksPort, batteryManager, backoff, torRendezvousCrypto, tor, callback,
torControlPort); MAX_LATENCY, MAX_IDLE_TIME, true);
} }
} }

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.tor;
import org.briarproject.bramble.api.battery.BatteryManager; import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventExecutor;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.network.NetworkManager; import org.briarproject.bramble.api.network.NetworkManager;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
@@ -13,8 +14,9 @@ import org.briarproject.bramble.api.plugin.TorDirectory;
import org.briarproject.bramble.api.plugin.TorSocksPort; import org.briarproject.bramble.api.plugin.TorSocksPort;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.ResourceProvider;
import org.briarproject.bramble.api.system.WakefulIoExecutor; import org.briarproject.bramble.api.system.WakefulIoExecutor;
import org.briarproject.bramble.plugin.tor.wrapper.TorWrapper;
import org.briarproject.bramble.plugin.tor.wrapper.WindowsTorWrapper;
import org.briarproject.nullsafety.NotNullByDefault; import org.briarproject.nullsafety.NotNullByDefault;
import java.io.File; import java.io.File;
@@ -34,13 +36,13 @@ public class WindowsTorPluginFactory extends TorPluginFactory {
@Inject @Inject
WindowsTorPluginFactory(@IoExecutor Executor ioExecutor, WindowsTorPluginFactory(@IoExecutor Executor ioExecutor,
@EventExecutor Executor eventExecutor,
@WakefulIoExecutor Executor wakefulIoExecutor, @WakefulIoExecutor Executor wakefulIoExecutor,
NetworkManager networkManager, NetworkManager networkManager,
LocationUtils locationUtils, LocationUtils locationUtils,
EventBus eventBus, EventBus eventBus,
SocketFactory torSocketFactory, SocketFactory torSocketFactory,
BackoffFactory backoffFactory, BackoffFactory backoffFactory,
ResourceProvider resourceProvider,
CircumventionProvider circumventionProvider, CircumventionProvider circumventionProvider,
BatteryManager batteryManager, BatteryManager batteryManager,
Clock clock, Clock clock,
@@ -48,8 +50,8 @@ public class WindowsTorPluginFactory extends TorPluginFactory {
@TorDirectory File torDirectory, @TorDirectory File torDirectory,
@TorSocksPort int torSocksPort, @TorSocksPort int torSocksPort,
@TorControlPort int torControlPort) { @TorControlPort int torControlPort) {
super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, super(ioExecutor, eventExecutor, wakefulIoExecutor, networkManager,
eventBus, torSocketFactory, backoffFactory, resourceProvider, locationUtils, eventBus, torSocketFactory, backoffFactory,
circumventionProvider, batteryManager, clock, crypto, circumventionProvider, batteryManager, clock, crypto,
torDirectory, torSocksPort, torControlPort); torDirectory, torSocksPort, torControlPort);
} }
@@ -70,11 +72,11 @@ public class WindowsTorPluginFactory extends TorPluginFactory {
TorPlugin createPluginInstance(Backoff backoff, TorPlugin createPluginInstance(Backoff backoff,
TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback, TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback,
String architecture) { String architecture) {
return new WindowsTorPlugin(ioExecutor, wakefulIoExecutor, TorWrapper tor = new WindowsTorWrapper(ioExecutor, eventExecutor,
networkManager, locationUtils, torSocketFactory, clock, architecture, torDirectory, torSocksPort, torControlPort);
resourceProvider, circumventionProvider, batteryManager, return new TorPlugin(ioExecutor, wakefulIoExecutor, networkManager,
backoff, torRendezvousCrypto, callback, architecture, locationUtils, torSocketFactory, circumventionProvider,
MAX_LATENCY, MAX_IDLE_TIME, torDirectory, torSocksPort, batteryManager, backoff, torRendezvousCrypto, tor, callback,
torControlPort); MAX_LATENCY, MAX_IDLE_TIME, true);
} }
} }

View File

@@ -0,0 +1,47 @@
package org.briarproject.bramble.plugin.tor.wrapper;
import org.briarproject.nullsafety.NotNullByDefault;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.CodeSource;
import java.util.concurrent.Executor;
import static org.briarproject.nullsafety.NullSafety.requireNonNull;
@NotNullByDefault
abstract class JavaTorWrapper extends AbstractTorWrapper {
JavaTorWrapper(Executor ioExecutor,
Executor eventExecutor,
String architecture,
File torDirectory,
int torSocksPort,
int torControlPort) {
super(ioExecutor, eventExecutor, architecture, torDirectory,
torSocksPort, torControlPort);
}
@Override
protected long getLastUpdateTime() {
CodeSource codeSource =
getClass().getProtectionDomain().getCodeSource();
if (codeSource == null) throw new AssertionError("CodeSource null");
try {
URI path = codeSource.getLocation().toURI();
File file = new File(path);
return file.lastModified();
} catch (URISyntaxException e) {
throw new AssertionError(e);
}
}
@Override
protected InputStream getResourceInputStream(String name,
String extension) {
ClassLoader cl = getClass().getClassLoader();
return requireNonNull(cl.getResourceAsStream(name + extension));
}
}

View File

@@ -0,0 +1,35 @@
package org.briarproject.bramble.plugin.tor.wrapper;
import com.sun.jna.Library;
import com.sun.jna.Native;
import org.briarproject.nullsafety.NotNullByDefault;
import java.io.File;
import java.util.concurrent.Executor;
@NotNullByDefault
public class UnixTorWrapper extends JavaTorWrapper {
public UnixTorWrapper(Executor ioExecutor,
Executor eventExecutor,
String architecture,
File torDirectory,
int torSocksPort,
int torControlPort) {
super(ioExecutor, eventExecutor, architecture, torDirectory,
torSocksPort, torControlPort);
}
@Override
protected int getProcessId() {
return CLibrary.INSTANCE.getpid();
}
private interface CLibrary extends Library {
CLibrary INSTANCE = Native.loadLibrary("c", CLibrary.class);
int getpid();
}
}

View File

@@ -1,54 +1,29 @@
package org.briarproject.bramble.plugin.tor; package org.briarproject.bramble.plugin.tor.wrapper;
import com.sun.jna.platform.win32.Kernel32; import com.sun.jna.platform.win32.Kernel32;
import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.network.NetworkManager;
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 org.briarproject.nullsafety.NotNullByDefault; import org.briarproject.nullsafety.NotNullByDefault;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.Scanner; import java.util.Scanner;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import javax.net.SocketFactory;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
@NotNullByDefault @NotNullByDefault
class WindowsTorPlugin extends JavaTorPlugin { public class WindowsTorWrapper extends JavaTorWrapper {
WindowsTorPlugin(Executor ioExecutor, public WindowsTorWrapper(Executor ioExecutor,
Executor wakefulIoExecutor, Executor eventExecutor,
NetworkManager networkManager,
LocationUtils locationUtils,
SocketFactory torSocketFactory,
Clock clock,
ResourceProvider resourceProvider,
CircumventionProvider circumventionProvider,
BatteryManager batteryManager,
Backoff backoff,
TorRendezvousCrypto torRendezvousCrypto,
PluginCallback callback,
String architecture, String architecture,
long maxLatency,
int maxIdleTime,
File torDirectory, File torDirectory,
int torSocksPort, int torSocksPort,
int torControlPort) { int torControlPort) {
super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, super(ioExecutor, eventExecutor, architecture, torDirectory,
torSocketFactory, clock, resourceProvider, torSocksPort, torControlPort);
circumventionProvider, batteryManager, backoff,
torRendezvousCrypto, callback, architecture,
maxLatency, maxIdleTime, torDirectory, torSocksPort,
torControlPort);
} }
@Override @Override
@@ -58,7 +33,7 @@ class WindowsTorPlugin extends JavaTorPlugin {
@Override @Override
protected void waitForTorToStart(Process torProcess) protected void waitForTorToStart(Process torProcess)
throws InterruptedException, PluginException { throws InterruptedException, IOException {
// On Windows the RunAsDaemon option has no effect, so Tor won't detach. // On Windows the RunAsDaemon option has no effect, so Tor won't detach.
// Wait for the control port to be opened, then continue to read its // Wait for the control port to be opened, then continue to read its
// stdout and stderr in a background thread until it exits. // stdout and stderr in a background thread until it exits.
@@ -91,7 +66,7 @@ class WindowsTorPlugin extends JavaTorPlugin {
} }
}); });
// Wait for the startup result // Wait for the startup result
if (!success.take()) throw new PluginException(); if (!success.take()) throw new IOException();
} }
@Override @Override

View File

@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.Multiset;
import org.briarproject.bramble.api.battery.BatteryManager; import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventExecutor;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.network.NetworkManager; import org.briarproject.bramble.api.network.NetworkManager;
import org.briarproject.bramble.api.plugin.BackoffFactory; import org.briarproject.bramble.api.plugin.BackoffFactory;
@@ -113,6 +114,9 @@ public class BridgeTest extends BrambleTestCase {
@IoExecutor @IoExecutor
Executor ioExecutor; Executor ioExecutor;
@Inject @Inject
@EventExecutor
Executor eventExecutor;
@Inject
@WakefulIoExecutor @WakefulIoExecutor
Executor wakefulIoExecutor; Executor wakefulIoExecutor;
@Inject @Inject
@@ -185,9 +189,9 @@ public class BridgeTest extends BrambleTestCase {
return singletonList(params.bridge); return singletonList(params.bridge);
} }
}; };
factory = new UnixTorPluginFactory(ioExecutor, wakefulIoExecutor, factory = new UnixTorPluginFactory(ioExecutor, eventExecutor,
networkManager, locationUtils, eventBus, torSocketFactory, wakefulIoExecutor, networkManager, locationUtils, eventBus,
backoffFactory, resourceProvider, bridgeProvider, torSocketFactory, backoffFactory, bridgeProvider,
batteryManager, clock, crypto, torDir, torSocksPort, batteryManager, clock, crypto, torDir, torSocksPort,
torControlPort); torControlPort);
} }
@@ -207,7 +211,7 @@ public class BridgeTest extends BrambleTestCase {
DuplexPlugin duplexPlugin = DuplexPlugin duplexPlugin =
factory.createPlugin(new TestPluginCallback()); factory.createPlugin(new TestPluginCallback());
assertNotNull(duplexPlugin); assertNotNull(duplexPlugin);
UnixTorPlugin plugin = (UnixTorPlugin) duplexPlugin; TorPlugin plugin = (TorPlugin) duplexPlugin;
LOG.warning("Testing " + params.bridge); LOG.warning("Testing " + params.bridge);
try { try {