diff --git a/bramble-android/build.gradle b/bramble-android/build.gradle index f2f2ba5de5f51e7cd07a3af9251ae5a532bc2c02..fe202931dc6b0d6792dd8cd161139653d9bdc3ce 100644 --- a/bramble-android/build.gradle +++ b/bramble-android/build.gradle @@ -11,6 +11,8 @@ android { versionCode 10011 versionName "1.0.11" consumerProguardFiles 'proguard-rules.txt' + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } compileOptions { @@ -31,6 +33,12 @@ dependencies { annotationProcessor 'com.google.dagger:dagger-compiler:2.0.2' compileOnly 'javax.annotation:jsr250-api:1.0' + + androidTestImplementation project(path: ':bramble-api', configuration: 'testOutput') + androidTestImplementation project(path: ':bramble-core', configuration: 'testOutput') + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestAnnotationProcessor 'com.google.dagger:dagger-compiler:2.0.2' + androidTestCompileOnly 'javax.annotation:jsr250-api:1.0' } dependencyVerification { diff --git a/bramble-android/src/androidTest/java/org/briarproject/bramble/IntegrationTestComponent.java b/bramble-android/src/androidTest/java/org/briarproject/bramble/IntegrationTestComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..bb5875ac3387e72e974766dc8d2203c198145a7f --- /dev/null +++ b/bramble-android/src/androidTest/java/org/briarproject/bramble/IntegrationTestComponent.java @@ -0,0 +1,23 @@ +package org.briarproject.bramble; + +import org.briarproject.bramble.event.EventModule; +import org.briarproject.bramble.plugin.PluginModule; +import org.briarproject.bramble.plugin.tor.BridgeTest; +import org.briarproject.bramble.system.SystemModule; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component(modules = { + BrambleAndroidModule.class, + PluginModule.class, // needed for BackoffFactory + EventModule.class, + SystemModule.class, +}) +public interface IntegrationTestComponent { + + void inject(BridgeTest init); + +} diff --git a/bramble-android/src/androidTest/java/org/briarproject/bramble/plugin/tor/BridgeTest.java b/bramble-android/src/androidTest/java/org/briarproject/bramble/plugin/tor/BridgeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0cb2d4232e0355462aea44aab0c83c9944a9c533 --- /dev/null +++ b/bramble-android/src/androidTest/java/org/briarproject/bramble/plugin/tor/BridgeTest.java @@ -0,0 +1,126 @@ +package org.briarproject.bramble.plugin.tor; + +import android.content.Context; +import android.support.test.runner.AndroidJUnit4; + +import org.briarproject.bramble.DaggerIntegrationTestComponent; +import org.briarproject.bramble.IntegrationTestComponent; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.plugin.BackoffFactory; +import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.api.system.LocationUtils; +import org.briarproject.bramble.test.BrambleTestCase; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.logging.Logger; + +import javax.inject.Inject; +import javax.net.SocketFactory; + +import static android.support.test.InstrumentationRegistry.getTargetContext; +import static java.util.Collections.singletonList; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +@RunWith(AndroidJUnit4.class) +public class BridgeTest extends BrambleTestCase { + + private final static long TIMEOUT = SECONDS.toMillis(23); + + private final static Logger LOG = + Logger.getLogger(BridgeTest.class.getSimpleName()); + + @Inject + EventBus eventBus; + @Inject + BackoffFactory backoffFactory; + @Inject + Clock clock; + + private final Context appContext = getTargetContext(); + private final CircumventionProvider circumventionProvider; + private final List<String> bridges; + private TorPluginFactory factory; + private volatile int currentBridge = 0; + + public BridgeTest() { + super(); + circumventionProvider = new CircumventionProvider() { + @Override + public boolean isTorProbablyBlocked(String countryCode) { + return true; + } + + @Override + public boolean doBridgesWork(String countryCode) { + return true; + } + + @Override + public List<String> getBridges() { + return singletonList(bridges.get(currentBridge)); + } + }; + bridges = new CircumventionProviderImpl(appContext).getBridges(); + } + + @Before + public void setUp() { + IntegrationTestComponent component = + DaggerIntegrationTestComponent.builder().build(); + component.inject(this); + + Executor ioExecutor = Executors.newCachedThreadPool(); + ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1); + LocationUtils locationUtils = () -> "US"; + SocketFactory torSocketFactory = SocketFactory.getDefault(); + + factory = new TorPluginFactory(ioExecutor, scheduler, appContext, + locationUtils, eventBus, torSocketFactory, + backoffFactory, circumventionProvider, clock); + } + + @Test + public void testBridges() throws Exception { + assertTrue(bridges.size() > 0); + + for (int i = 0; i < bridges.size(); i++) { + testBridge(i); + } + } + + private void testBridge(int bridge) throws Exception { + DuplexPlugin duplexPlugin = + factory.createPlugin(new TorPluginCallBack()); + assertNotNull(duplexPlugin); + TorPlugin plugin = (TorPlugin) duplexPlugin; + + currentBridge = bridge; + LOG.warning("Testing " + bridges.get(currentBridge)); + try { + plugin.start(); + long start = clock.currentTimeMillis(); + while (clock.currentTimeMillis() - start < TIMEOUT) { + if (plugin.isRunning()) return; + clock.sleep(500); + } + if (!plugin.isRunning()) { + fail("Could not connect to Tor within timeout."); + } + } finally { + plugin.stop(); + } + } + +} diff --git a/bramble-android/src/androidTest/java/org/briarproject/bramble/plugin/tor/TorPluginCallBack.java b/bramble-android/src/androidTest/java/org/briarproject/bramble/plugin/tor/TorPluginCallBack.java new file mode 100644 index 0000000000000000000000000000000000000000..320e8f02fa24eb78109683485d70bc3379de688e --- /dev/null +++ b/bramble-android/src/androidTest/java/org/briarproject/bramble/plugin/tor/TorPluginCallBack.java @@ -0,0 +1,54 @@ +package org.briarproject.bramble.plugin.tor; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback; +import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.settings.Settings; + +@NotNullByDefault +public class TorPluginCallBack implements DuplexPluginCallback { + + @Override + public void incomingConnectionCreated(DuplexTransportConnection d) { + + } + + @Override + public void outgoingConnectionCreated(ContactId c, + DuplexTransportConnection d) { + + } + + @Override + public Settings getSettings() { + return new Settings(); + } + + @Override + public TransportProperties getLocalProperties() { + return new TransportProperties(); + } + + @Override + public void mergeSettings(Settings s) { + + } + + @Override + public void mergeLocalProperties(TransportProperties p) { + + } + + @Override + public void transportEnabled() { + + } + + @Override + public void transportDisabled() { + + } + +} diff --git a/bramble-android/src/main/java/org/briarproject/bramble/BrambleAndroidModule.java b/bramble-android/src/main/java/org/briarproject/bramble/BrambleAndroidModule.java index 1ec2563482dab905b11ea6f6ea20422ef6390d05..cb563508ae415e569d9fe81e5c225842b83f24ca 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/BrambleAndroidModule.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/BrambleAndroidModule.java @@ -1,11 +1,25 @@ package org.briarproject.bramble; +import android.app.Application; + +import org.briarproject.bramble.plugin.tor.CircumventionProvider; +import org.briarproject.bramble.plugin.tor.CircumventionProviderImpl; import org.briarproject.bramble.system.AndroidSystemModule; +import javax.inject.Singleton; + import dagger.Module; +import dagger.Provides; @Module(includes = { AndroidSystemModule.class }) public class BrambleAndroidModule { + + @Provides + @Singleton + CircumventionProvider provideCircumventionProvider(Application app) { + return new CircumventionProviderImpl(app.getApplicationContext()); + } + } diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/CircumventionProvider.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/CircumventionProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..c1acfff7ee9d4219a1b2391dabfea584e25cb091 --- /dev/null +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/CircumventionProvider.java @@ -0,0 +1,30 @@ +package org.briarproject.bramble.plugin.tor; + +import org.briarproject.bramble.api.lifecycle.IoExecutor; + +import java.util.List; + +public interface CircumventionProvider { + + /** + * Countries where Tor is blocked, i.e. vanilla Tor connection won't work. + * + * See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + * and https://trac.torproject.org/projects/tor/wiki/doc/OONI/censorshipwiki + */ + String[] BLOCKED = {"CN", "IR", "EG", "BY", "TR", "SY", "VE"}; + + /** + * Countries where vanilla bridge connection are likely to work. + * Should be a subset of {@link #BLOCKED}. + */ + String[] BRIDGES = { "EG", "BY", "TR", "SY", "VE" }; + + boolean isTorProbablyBlocked(String countryCode); + + boolean doBridgesWork(String countryCode); + + @IoExecutor + List<String> getBridges(); + +} diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/CircumventionProviderImpl.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/CircumventionProviderImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..6a9999b9cbf99da81290a512d57087a4eebc8bd8 --- /dev/null +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/CircumventionProviderImpl.java @@ -0,0 +1,68 @@ +package org.briarproject.bramble.plugin.tor; + +import android.content.Context; +import android.content.res.Resources; + +import org.briarproject.bramble.api.lifecycle.IoExecutor; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Scanner; +import java.util.Set; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +public class CircumventionProviderImpl implements CircumventionProvider { + + private final static String BRIDGE_FILE_NAME = "bridges"; + + private final Context ctx; + @Nullable + private volatile List<String> bridges = null; + + @Inject + public CircumventionProviderImpl(Context ctx) { + this.ctx = ctx; + } + + private static final Set<String> BLOCKED_IN_COUNTRIES = + new HashSet<>(Arrays.asList(BLOCKED)); + private static final Set<String> BRIDGES_WORK_IN_COUNTRIES = + new HashSet<>(Arrays.asList(BRIDGES)); + + @Override + public boolean isTorProbablyBlocked(String countryCode) { + return BLOCKED_IN_COUNTRIES.contains(countryCode); + } + + @Override + public boolean doBridgesWork(String countryCode) { + return BRIDGES_WORK_IN_COUNTRIES.contains(countryCode); + } + + @Override + @IoExecutor + public List<String> getBridges() { + if (this.bridges != null) return this.bridges; + + Resources res = ctx.getResources(); + int resId = res.getIdentifier(BRIDGE_FILE_NAME, "raw", + ctx.getPackageName()); + InputStream is = ctx.getResources().openRawResource(resId); + Scanner scanner = new Scanner(is); + + List<String> bridges = new ArrayList<>(); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (!line.startsWith("#")) bridges.add(line); + } + scanner.close(); + this.bridges = bridges; + return bridges; + } + +} diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorNetworkMetadata.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorNetworkMetadata.java deleted file mode 100644 index e6151d57402152c170e4df476bd66080c2f62d3d..0000000000000000000000000000000000000000 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorNetworkMetadata.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.briarproject.bramble.plugin.tor; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -class TorNetworkMetadata { - - // See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 - // and https://trac.torproject.org/projects/tor/wiki/doc/OONI/censorshipwiki - // TODO: get a more complete list - private static final Set<String> BLOCKED_IN_COUNTRIES = - new HashSet<>(Arrays.asList("CN", "IR", "SY", "ZZ")); - - static boolean isTorProbablyBlocked(String countryCode) { - return BLOCKED_IN_COUNTRIES.contains(countryCode); - } -} diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java index 015056b49b8861fd86192becb8f73823e41b0442..03e9bf1f2de5d35d6ebba58554c526e36cb57111 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java @@ -50,7 +50,9 @@ import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -119,6 +121,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener { private final Backoff backoff; private final DuplexPluginCallback callback; private final String architecture; + private final CircumventionProvider circumventionProvider; private final int maxLatency, maxIdleTime, socketTimeout; private final ConnectionStatus connectionStatus; private final File torDirectory, torFile, geoIpFile, configFile; @@ -138,7 +141,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener { Context appContext, LocationUtils locationUtils, SocketFactory torSocketFactory, Clock clock, Backoff backoff, DuplexPluginCallback callback, String architecture, - int maxLatency, int maxIdleTime) { + CircumventionProvider circumventionProvider, int maxLatency, int maxIdleTime) { this.ioExecutor = ioExecutor; this.scheduler = scheduler; this.appContext = appContext; @@ -148,6 +151,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener { this.backoff = backoff; this.callback = callback; this.architecture = architecture; + this.circumventionProvider = circumventionProvider; this.maxLatency = maxLatency; this.maxIdleTime = maxIdleTime; if (maxIdleTime > Integer.MAX_VALUE / 2) @@ -498,6 +502,17 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } } + private void enableBridges(boolean enable) throws IOException { + if (enable) { + Collection<String> conf = new ArrayList<>(); + conf.add("UseBridges 1"); + conf.addAll(circumventionProvider.getBridges()); + controlConnection.setConf(conf); + } else { + controlConnection.setConf("UseBridges", "0"); + } + } + @Override public void stop() { running = false; @@ -662,8 +677,8 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener { boolean online = net != null && net.isConnected(); boolean wifi = online && net.getType() == TYPE_WIFI; String country = locationUtils.getCurrentCountry(); - boolean blocked = TorNetworkMetadata.isTorProbablyBlocked( - country); + boolean blocked = + circumventionProvider.isTorProbablyBlocked(country); Settings s = callback.getSettings(); int network = s.getInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_ALWAYS); @@ -677,15 +692,22 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener { if (!online) { LOG.info("Disabling network, device is offline"); enableNetwork(false); - } else if (blocked) { - LOG.info("Disabling network, country is blocked"); - enableNetwork(false); } else if (network == PREF_TOR_NETWORK_NEVER || (network == PREF_TOR_NETWORK_WIFI && !wifi)) { LOG.info("Disabling network due to data setting"); enableNetwork(false); + } else if (blocked) { + if (circumventionProvider.doBridgesWork(country)) { + LOG.info("Enabling network, using bridges"); + enableBridges(true); + enableNetwork(true); + } else { + LOG.info("Disabling network, country is blocked"); + enableNetwork(false); + } } else { LOG.info("Enabling network"); + enableBridges(false); enableNetwork(true); } } catch (IOException e) { diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPluginFactory.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPluginFactory.java index c380cbb0c96b91ca415d4a300ea511e6ab0363b6..a7feb23722f73aa7d77825fe4ecb3c8532c6e205 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPluginFactory.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPluginFactory.java @@ -43,12 +43,14 @@ public class TorPluginFactory implements DuplexPluginFactory { private final EventBus eventBus; private final SocketFactory torSocketFactory; private final BackoffFactory backoffFactory; + private final CircumventionProvider circumventionProvider; private final Clock clock; public TorPluginFactory(Executor ioExecutor, ScheduledExecutorService scheduler, Context appContext, LocationUtils locationUtils, EventBus eventBus, SocketFactory torSocketFactory, BackoffFactory backoffFactory, + CircumventionProvider circumventionProvider, Clock clock) { this.ioExecutor = ioExecutor; this.scheduler = scheduler; @@ -57,6 +59,7 @@ public class TorPluginFactory implements DuplexPluginFactory { this.eventBus = eventBus; this.torSocketFactory = torSocketFactory; this.backoffFactory = backoffFactory; + this.circumventionProvider = circumventionProvider; this.clock = clock; } @@ -95,7 +98,7 @@ public class TorPluginFactory implements DuplexPluginFactory { MAX_POLLING_INTERVAL, BACKOFF_BASE); TorPlugin plugin = new TorPlugin(ioExecutor, scheduler, appContext, locationUtils, torSocketFactory, clock, backoff, callback, - architecture, MAX_LATENCY, MAX_IDLE_TIME); + architecture, circumventionProvider, MAX_LATENCY, MAX_IDLE_TIME); eventBus.addListener(plugin); return plugin; } diff --git a/bramble-android/src/main/res/raw/bridges b/bramble-android/src/main/res/raw/bridges new file mode 100644 index 0000000000000000000000000000000000000000..dc3d6a581bf23bed1b24443bace0344b132daff9 --- /dev/null +++ b/bramble-android/src/main/res/raw/bridges @@ -0,0 +1,5 @@ +Bridge 131.252.210.150:8081 0E858AC201BF0F3FA3C462F64844CBFFC7297A42 +Bridge 67.205.189.122:8443 12D64D5D44E20169585E7378580C0D33A872AD98 +Bridge 45.32.148.146:8443 0CE016FB2462D8BF179AE71F7D702D09DEAC3F1D +Bridge 148.251.90.59:7510 019F727CA6DCA6CA5C90B55E477B7D87981E75BC +#Bridge 128.105.214.161:8081 1E326AAFB3FCB515015250D8FCCC8E37F91A153B \ No newline at end of file diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/IoExecutor.java b/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/IoExecutor.java index c63e6a8c7f2d79408bddceafe87edb4e48d67bd6..6ec68922bdb341460efc3f48eb1fc07cba26869c 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/IoExecutor.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/IoExecutor.java @@ -12,7 +12,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * Annotation for injecting the executor for long-running IO tasks. Also used - * for annotating methods that should run on the UI executor. + * for annotating methods that should run on the IO executor. * <p> * The contract of this executor is that tasks may be run concurrently, and * submitting a task will never block. Tasks may run indefinitely. Tasks diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index 03470f4f264ebe8ff221bb33dd4d91579f811091..565a2c6d3582dabacfd526c498961845f174aeaf 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -25,6 +25,7 @@ import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.api.system.Scheduler; import org.briarproject.bramble.plugin.bluetooth.AndroidBluetoothPluginFactory; import org.briarproject.bramble.plugin.tcp.AndroidLanTcpPluginFactory; +import org.briarproject.bramble.plugin.tor.CircumventionProvider; import org.briarproject.bramble.plugin.tor.TorPluginFactory; import org.briarproject.bramble.util.AndroidUtils; import org.briarproject.bramble.util.StringUtils; @@ -99,14 +100,14 @@ public class AppModule { AndroidExecutor androidExecutor, SecureRandom random, SocketFactory torSocketFactory, BackoffFactory backoffFactory, Application app, LocationUtils locationUtils, EventBus eventBus, - Clock clock) { + CircumventionProvider circumventionProvider, Clock clock) { Context appContext = app.getApplicationContext(); DuplexPluginFactory bluetooth = new AndroidBluetoothPluginFactory(ioExecutor, androidExecutor, appContext, random, eventBus, backoffFactory); DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, scheduler, appContext, locationUtils, eventBus, torSocketFactory, - backoffFactory, clock); + backoffFactory, circumventionProvider, clock); DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor, scheduler, backoffFactory, appContext); Collection<DuplexPluginFactory> duplex = asList(bluetooth, tor, lan);