Merge branch 'android-geo' into 'master'

Don't start Tor in countries that are known to block it

See the javadoc for getCurrentCountry for details on how we get the country. In particular, it seems Google already has a hidden SystemService that does this, but we can't use it so we have to code our own. Luckily the solution I came up with independently yesterday almost exactly matches what they do.

Also, the hardcoded list of countries that block tor is incomplete; I will try to find a more complete list.
This commit is contained in:
akwizgran
2014-03-06 12:45:05 +00:00
7 changed files with 201 additions and 3 deletions

View File

@@ -13,6 +13,7 @@ import org.briarproject.api.plugins.duplex.DuplexPluginConfig;
import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
import org.briarproject.api.plugins.simplex.SimplexPluginConfig;
import org.briarproject.api.plugins.simplex.SimplexPluginFactory;
import org.briarproject.api.system.LocationUtils;
import org.briarproject.plugins.droidtooth.DroidtoothPluginFactory;
import org.briarproject.plugins.tcp.LanTcpPluginFactory;
import org.briarproject.plugins.tor.TorPluginFactory;
@@ -39,12 +40,13 @@ public class AndroidPluginsModule extends AbstractModule {
DuplexPluginConfig getDuplexPluginConfig(
@PluginExecutor Executor pluginExecutor,
AndroidExecutor androidExecutor, Context appContext,
CryptoComponent crypto, ShutdownManager shutdownManager) {
CryptoComponent crypto, LocationUtils locationUtils,
ShutdownManager shutdownManager) {
DuplexPluginFactory droidtooth = new DroidtoothPluginFactory(
pluginExecutor, androidExecutor, appContext,
crypto.getSecureRandom());
DuplexPluginFactory tor = new TorPluginFactory(pluginExecutor,
appContext, shutdownManager);
appContext, locationUtils, shutdownManager);
DuplexPluginFactory lan = new LanTcpPluginFactory(pluginExecutor);
final Collection<DuplexPluginFactory> factories =
Arrays.asList(droidtooth, tor, lan);

View File

@@ -0,0 +1,39 @@
package org.briarproject.plugins.tor;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
import org.briarproject.api.system.LocationUtils;
public class TorNetworkMetadata {
private static final Logger LOG =
Logger.getLogger(TorNetworkMetadata.class.getName());
// for country codes see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
// below list from https://trac.torproject.org/projects/tor/wiki/doc/OONI/censorshipwiki
// TODO: get a more complete list
public static final Set<String> BLOCKED_IN_COUNTRIES = new HashSet<String>(Arrays.asList(
"CN",
"IR",
"SY",
//"ET", // possibly lifted - https://metrics.torproject.org/users.html?graph=userstats-relay-country&start=2012-02-08&end=2014-02-06&country=et&events=off#userstats-relay-country
//"KZ", // unclear due to botnet - https://metrics.torproject.org/users.html?graph=userstats-relay-country&start=2012-02-08&end=2014-02-06&country=kz&events=off#userstats-relay-country
//"PH", // unclear due to botnet - https://metrics.torproject.org/users.html?graph=userstats-relay-country&start=2012-02-08&end=2014-02-06&country=ph&events=off#userstats-relay-country
//"AE", // unclear due to botnet - https://metrics.torproject.org/users.html?graph=userstats-relay-country&start=2012-02-08&end=2014-02-06&country=ae&events=off#userstats-relay-country
//"GB", // for testing
"ZZ"
));
public static boolean isTorProbablyBlocked(LocationUtils locationUtils) {
String countryCode = locationUtils.getCurrentCountry();
if (BLOCKED_IN_COUNTRIES.contains(countryCode)) {
LOG.info("Tor is probably blocked in your country: " + countryCode);
return true;
}
return false;
}
}

View File

@@ -44,6 +44,7 @@ import org.briarproject.util.StringUtils;
import socks.Socks5Proxy;
import socks.SocksSocket;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -275,6 +276,7 @@ class TorPlugin implements DuplexPlugin, EventHandler {
out.close();
}
@SuppressLint("NewApi")
private boolean setExecutable(File f) {
if(Build.VERSION.SDK_INT >= 9) {
return f.setExecutable(true, true);

View File

@@ -1,30 +1,39 @@
package org.briarproject.plugins.tor;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import org.briarproject.api.TransportId;
import org.briarproject.api.lifecycle.ShutdownManager;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
import org.briarproject.api.system.LocationUtils;
import org.briarproject.plugins.AndroidPluginsModule;
import org.briarproject.plugins.tor.TorNetworkMetadata;
import android.content.Context;
import android.os.Build;
public class TorPluginFactory implements DuplexPluginFactory {
private static final Logger LOG =
Logger.getLogger(TorPluginFactory.class.getName());
private static final int MAX_FRAME_LENGTH = 1024;
private static final long MAX_LATENCY = 60 * 1000; // 1 minute
private static final long POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
private final Executor pluginExecutor;
private final Context appContext;
private final LocationUtils locationUtils;
private final ShutdownManager shutdownManager;
public TorPluginFactory(Executor pluginExecutor, Context appContext,
ShutdownManager shutdownManager) {
LocationUtils locationUtils, ShutdownManager shutdownManager) {
this.pluginExecutor = pluginExecutor;
this.appContext = appContext;
this.locationUtils = locationUtils;
this.shutdownManager = shutdownManager;
}
@@ -35,6 +44,11 @@ public class TorPluginFactory implements DuplexPluginFactory {
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
// Check that we have a Tor binary for this architecture
if(!Build.CPU_ABI.startsWith("armeabi")) return null;
// Check that we don't know that Tor is blocked here
if (TorNetworkMetadata.isTorProbablyBlocked(locationUtils)) {
LOG.info("Tor has been pre-emptively disabled since it is probably blocked");
return null;
}
return new TorPlugin(pluginExecutor,appContext, shutdownManager,
callback, MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
}

View File

@@ -0,0 +1,126 @@
package org.briarproject.system;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import org.briarproject.api.system.LocationUtils;
import roboguice.inject.ContextSingleton;
import android.annotation.SuppressLint;
import android.content.Context;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import com.google.inject.Inject;
@ContextSingleton
class AndroidLocationUtils implements LocationUtils {
private static final Logger LOG =
Logger.getLogger(AndroidLocationUtils.class.getName());
final Context context;
@Inject
public AndroidLocationUtils(Context context) {
this.context = context;
}
/**
* This guesses the current country from the first of these sources that
* succeeds (also in order of likelihood of being correct):
*
* <ul>
* <li>Phone network. This works even when no SIM card is inserted, or a
* foreign SIM card is inserted.</li>
* <li><del>Location service (GPS/WiFi/etc).</del> <em>This is disabled for
* now, until we figure out an offline method of converting a long/lat
* into a country code, that doesn't involve a network call.</em>
* <li>SIM card. This is only an heuristic and assumes the user is not
* roaming.</li>
* <li>User Locale. This is an even worse heuristic.</li>
* </ul>
*
* Note: this is very similar to <a href="https://android.googlesource.com/platform/frameworks/base/+/cd92588%5E/location/java/android/location/CountryDetector.java">
* this API</a> except it seems that Google doesn't want us to use it for
* some reason - both that class and {@code Context.COUNTRY_CODE} are
* annotated {@code @hide}.
*/
@SuppressLint("DefaultLocale")
@Override
public String getCurrentCountry() {
String countryCode;
countryCode = getCountryFromPhoneNetwork();
if (!TextUtils.isEmpty(countryCode)) {
return countryCode.toUpperCase(); // android api gives lowercase for some reason
}
// When we enable this, we will need to add ACCESS_FINE_LOCATION
//countryCode = getCountryFromLocation();
//if (!TextUtils.isEmpty(countryCode)) {
// return countryCode;
//}
countryCode = getCountryFromSimCard();
if (!TextUtils.isEmpty(countryCode)) {
LOG.info("Could not determine current country; fall back to SIM card country.");
return countryCode.toUpperCase(); // android api gives lowercase for some reason
}
LOG.info("Could not determine current country; fall back to user-defined locale.");
return Locale.getDefault().getCountry();
}
String getCountryFromPhoneNetwork() {
TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
return tm.getNetworkCountryIso();
}
String getCountryFromSimCard() {
TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
return tm.getSimCountryIso();
}
// TODO: this is not currently used, because it involves a network call
// it should be possible to determine country just from the long/lat, but
// this would involve something like tzdata for countries.
String getCountryFromLocation() {
Location location = getLastKnownLocation();
if (location == null) return null;
Geocoder code = new Geocoder(context);
try {
List<Address> addresses = code.getFromLocation(location.getLatitude(), location.getLongitude(), 1);
if (addresses.isEmpty()) return null;
return addresses.get(0).getCountryCode();
} catch (IOException e) {
return null;
}
}
/**
* Returns the last location from all location providers.
* Since we're only checking the country, we don't care about the accuracy.
* If we ever need the accuracy, we can do something like:
* https://code.google.com/p/android-protips-location/source/browse/trunk\
* /src/com/radioactiveyak/location_best_practices/utils/GingerbreadLastLocationFinder.java
*/
Location getLastKnownLocation() {
LocationManager locationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
Location bestResult = null;
long bestTime = Long.MIN_VALUE;
for (String provider: locationManager.getAllProviders()) {
Location location = locationManager.getLastKnownLocation(provider);
if (location == null) continue;
long time = location.getTime();
if (time > bestTime) {
bestResult = location;
bestTime = time;
}
}
return bestResult;
}
}

View File

@@ -2,6 +2,7 @@ package org.briarproject.system;
import org.briarproject.api.system.Clock;
import org.briarproject.api.system.FileUtils;
import org.briarproject.api.system.LocationUtils;
import org.briarproject.api.system.SeedProvider;
import org.briarproject.api.system.Timer;
@@ -14,5 +15,6 @@ public class AndroidSystemModule extends AbstractModule {
bind(Timer.class).to(SystemTimer.class);
bind(SeedProvider.class).to(AndroidSeedProvider.class);
bind(FileUtils.class).to(AndroidFileUtils.class);
bind(LocationUtils.class).to(AndroidLocationUtils.class);
}
}

View File

@@ -0,0 +1,13 @@
package org.briarproject.api.system;
public interface LocationUtils {
/** Get the country the device is currently-located in, or "" if it cannot
* be determined. Should never return {@code null}.
*
* <p>The country codes are formatted upper-case and as per <a href="
* https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2">ISO 3166-1 alpha 2</a>.
*/
String getCurrentCountry();
}