Merge branch '314-tor-sleep' into 'master'

Hold a wake lock while Tor is connected to the internet

This is a partial fix for #314. As noted on that ticket, if a Tor connection is lost for any reason other than the device sleeping, the plugin won't try to replace the lost connection. I'm leaving the ticket open until that more general issue is solved.

The Tor plugin uses several variables to keep track of its connectivity status. This patch refactors those variables into an inner class to improve readability and ensure they're accessed atomically. However, it's still possible for the plugin's state to become inconsistent with the state of the Tor process. For example, calls to updateConnectionStatus() may run concurrently on the IO executor, so their calls to enableNetwork() may be interleaved. As usual, locking would solve this problem but create the potential for deadlock, so I won't try to solve it in this patch.

See merge request !168
This commit is contained in:
akwizgran
2016-05-03 14:45:42 +00:00
2 changed files with 98 additions and 59 deletions

View File

@@ -24,6 +24,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Since API 23, this is needed to add contacts via Bluetooth -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

View File

@@ -7,6 +7,7 @@ import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.FileObserver;
import android.os.PowerManager;
import net.freehaven.tor.control.EventHandler;
import net.freehaven.tor.control.TorControlConnection;
@@ -49,23 +50,21 @@ import java.util.List;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.zip.ZipInputStream;
import static android.content.Context.CONNECTIVITY_SERVICE;
import static android.content.Context.MODE_PRIVATE;
import static android.content.Context.POWER_SERVICE;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.EXTRA_NO_CONNECTIVITY;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.os.PowerManager.PARTIAL_WAKE_LOCK;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
class TorPlugin implements DuplexPlugin, EventHandler,
EventListener {
class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
static final TransportId ID = new TransportId("tor");
@@ -90,17 +89,12 @@ class TorPlugin implements DuplexPlugin, EventHandler,
private final DuplexPluginCallback callback;
private final String architecture;
private final int maxLatency, maxIdleTime, pollingInterval, socketTimeout;
private final ConnectionStatus connectionStatus;
private final File torDirectory, torFile, geoIpFile, configFile, doneFile;
private final File cookieFile, hostnameFile;
private final AtomicBoolean circuitBuilt;
private final AtomicInteger descriptorsPublished;
private volatile boolean running = false, networkEnabled = false;
private volatile boolean bootstrapped = false;
private volatile boolean connectedToWifi = false;
private volatile boolean online = false;
private volatile long descriptorsPublishedTime = Long.MAX_VALUE;
private final PowerManager.WakeLock wakeLock;
private volatile boolean running = false;
private volatile ServerSocket socket = null;
private volatile Socket controlSocket = null;
private volatile TorControlConnection controlConnection = null;
@@ -123,6 +117,7 @@ class TorPlugin implements DuplexPlugin, EventHandler,
if (maxIdleTime > Integer.MAX_VALUE / 2)
socketTimeout = Integer.MAX_VALUE;
else socketTimeout = maxIdleTime * 2;
connectionStatus = new ConnectionStatus(pollingInterval);
torDirectory = appContext.getDir("tor", MODE_PRIVATE);
torFile = new File(torDirectory, "tor");
geoIpFile = new File(torDirectory, "geoip");
@@ -130,8 +125,10 @@ class TorPlugin implements DuplexPlugin, EventHandler,
doneFile = new File(torDirectory, "done");
cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
hostnameFile = new File(torDirectory, "hs/hostname");
circuitBuilt = new AtomicBoolean(false);
descriptorsPublished = new AtomicInteger(0);
Object o = appContext.getSystemService(POWER_SERVICE);
PowerManager pm = (PowerManager) o;
wakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, "TorPlugin");
wakeLock.setReferenceCounted(false);
}
public TransportId getId() {
@@ -229,7 +226,7 @@ class TorPlugin implements DuplexPlugin, EventHandler,
String phase = controlConnection.getInfo("status/bootstrap-phase");
if (phase != null && phase.contains("PROGRESS=100")) {
LOG.info("Tor has already bootstrapped");
bootstrapped = true;
connectionStatus.setBootstrapped();
sendCrashReports();
}
}
@@ -487,15 +484,13 @@ class TorPlugin implements DuplexPlugin, EventHandler,
private void enableNetwork(boolean enable) throws IOException {
if (!running) return;
if (LOG.isLoggable(INFO)) LOG.info("Enabling network: " + enable);
if (!enable) {
circuitBuilt.set(false);
descriptorsPublished.set(0);
descriptorsPublishedTime = Long.MAX_VALUE;
callback.transportDisabled();
}
networkEnabled = enable;
if (enable) wakeLock.acquire();
connectionStatus.enableNetwork(enable);
controlConnection.setConf("DisableNetwork", enable ? "0" : "1");
if (!enable) {
callback.transportDisabled();
wakeLock.release();
}
}
public void stop() throws IOException {
@@ -517,10 +512,11 @@ class TorPlugin implements DuplexPlugin, EventHandler,
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
wakeLock.release();
}
public boolean isRunning() {
return running && networkEnabled && bootstrapped && circuitBuilt.get();
return running && connectionStatus.isConnected();
}
public boolean shouldPoll() {
@@ -533,16 +529,13 @@ class TorPlugin implements DuplexPlugin, EventHandler,
public void poll(Collection<ContactId> connected) {
if (!isRunning()) return;
if (descriptorsPublished.get() >= MIN_DESCRIPTORS_PUBLISHED) {
long now = clock.currentTimeMillis();
if (now - descriptorsPublishedTime >= 2 * pollingInterval) {
LOG.info("Hidden service descriptor published, not polling");
return;
}
if (connectionStatus.shouldPoll(clock.currentTimeMillis())) {
// TODO: Pass properties to connectAndCallBack()
for (ContactId c : callback.getRemoteProperties().keySet())
if (!connected.contains(c)) connectAndCallBack(c);
} else {
LOG.info("Hidden service descriptor published, not polling");
}
// TODO: Pass properties to connectAndCallBack()
for (ContactId c : callback.getRemoteProperties().keySet())
if (!connected.contains(c)) connectAndCallBack(c);
}
private void connectAndCallBack(final ContactId c) {
@@ -604,7 +597,8 @@ class TorPlugin implements DuplexPlugin, EventHandler,
}
public void circuitStatus(String status, String id, String path) {
if (status.equals("BUILT") && !circuitBuilt.getAndSet(true)) {
if (status.equals("BUILT") &&
connectionStatus.getAndSetCircuitBuilt()) {
LOG.info("First circuit built");
if (isRunning()) callback.transportEnabled();
}
@@ -626,20 +620,15 @@ class TorPlugin implements DuplexPlugin, EventHandler,
public void message(String severity, String msg) {
if (LOG.isLoggable(INFO)) LOG.info(severity + " " + msg);
if (severity.equals("NOTICE") && msg.startsWith("Bootstrapped 100%")) {
bootstrapped = true;
connectionStatus.setBootstrapped();
sendCrashReports();
if (isRunning()) callback.transportEnabled();
}
}
public void unrecognized(String type, String msg) {
if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) {
int descriptors = descriptorsPublished.incrementAndGet();
if (descriptors == MIN_DESCRIPTORS_PUBLISHED) {
LOG.info("Hidden service descriptor published");
descriptorsPublishedTime = clock.currentTimeMillis();
}
}
if (type.equals("HS_DESC") && msg.startsWith("UPLOADED"))
connectionStatus.descriptorPublished(clock.currentTimeMillis());
}
private static class WriteObserver extends FileObserver {
@@ -661,7 +650,7 @@ class TorPlugin implements DuplexPlugin, EventHandler,
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {
if (((SettingsUpdatedEvent) e).getNamespace().equals("tor")) {
// Wifi setting may have been updated
LOG.info("Tor settings updated");
updateConnectionStatus();
}
}
@@ -672,17 +661,23 @@ class TorPlugin implements DuplexPlugin, EventHandler,
public void run() {
if (!running) return;
Object o = appContext.getSystemService(CONNECTIVITY_SERVICE);
ConnectivityManager cm = (ConnectivityManager) o;
NetworkInfo net = cm.getActiveNetworkInfo();
boolean online = net != null && net.isConnected();
boolean wifi = online && net.getType() == TYPE_WIFI;
String country = locationUtils.getCurrentCountry();
if (LOG.isLoggable(INFO)) {
LOG.info("Online: " + online);
if ("".equals(country)) LOG.info("Country code unknown");
else LOG.info("Country code: " + country);
}
boolean blocked = TorNetworkMetadata.isTorProbablyBlocked(
country);
Settings s = callback.getSettings();
boolean useMobileData = s.getBoolean("torOverMobile", true);
if (LOG.isLoggable(INFO)) {
LOG.info("Online: " + online + ", wifi: " + wifi);
if ("".equals(country)) LOG.info("Country code unknown");
else LOG.info("Country code: " + country);
}
try {
if (!online) {
LOG.info("Disabling network, device is offline");
@@ -690,10 +685,11 @@ class TorPlugin implements DuplexPlugin, EventHandler,
} else if (blocked) {
LOG.info("Disabling network, country is blocked");
enableNetwork(false);
} else if (!useMobileData & !connectedToWifi) {
} else if (!wifi && !useMobileData) {
LOG.info("Disabling network due to data setting");
enableNetwork(false);
} else {
LOG.info("Enabling network");
enableNetwork(true);
}
} catch (IOException e) {
@@ -709,15 +705,57 @@ class TorPlugin implements DuplexPlugin, EventHandler,
@Override
public void onReceive(Context ctx, Intent i) {
if (!running) return;
online = !i.getBooleanExtra(EXTRA_NO_CONNECTIVITY, false);
// Some devices fail to set EXTRA_NO_CONNECTIVITY, double check
Object o = ctx.getSystemService(CONNECTIVITY_SERVICE);
ConnectivityManager cm = (ConnectivityManager) o;
NetworkInfo net = cm.getActiveNetworkInfo();
if (net == null || !net.isConnected()) online = false;
connectedToWifi = (net != null && net.getType() == TYPE_WIFI
&& net.isConnected());
updateConnectionStatus();
if (CONNECTIVITY_ACTION.equals(i.getAction())) {
LOG.info("Detected connectivity change");
updateConnectionStatus();
}
}
}
private static class ConnectionStatus {
private final int pollingInterval;
// All of the following are locking: this
private boolean networkEnabled = false;
private boolean bootstrapped = false, circuitBuilt = false;
private int descriptorsPublished = 0;
private long descriptorsPublishedTime = Long.MAX_VALUE;
private ConnectionStatus(int pollingInterval) {
this.pollingInterval = pollingInterval;
}
private synchronized void setBootstrapped() {
bootstrapped = true;
}
private synchronized boolean getAndSetCircuitBuilt() {
boolean firstCircuit = !circuitBuilt;
circuitBuilt = true;
return firstCircuit;
}
private synchronized void descriptorPublished(long now) {
descriptorsPublished++;
if (descriptorsPublished == MIN_DESCRIPTORS_PUBLISHED)
descriptorsPublishedTime = now;
}
private synchronized void enableNetwork(boolean enable) {
networkEnabled = enable;
circuitBuilt = false;
descriptorsPublished = 0;
descriptorsPublishedTime = Long.MAX_VALUE;
}
private synchronized boolean isConnected() {
return networkEnabled && bootstrapped && circuitBuilt;
}
private synchronized boolean shouldPoll(long now) {
return descriptorsPublished < MIN_DESCRIPTORS_PUBLISHED
|| now - descriptorsPublishedTime < 2 * pollingInterval;
}
}
}