summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/fronts4
-rw-r--r--app/assets/fronts-amp4
-rw-r--r--app/assets/fronts-http3
-rw-r--r--app/assets/fronts-sqs3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java5
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/ClientTransportPluginProvider.java39
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java10
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java292
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/SnowflakePlugin.java328
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java84
-rw-r--r--app/src/main/res/values/strings.xml5
m---------bitmask-core-android0
14 files changed, 410 insertions, 372 deletions
diff --git a/app/assets/fronts b/app/assets/fronts
deleted file mode 100644
index acd1cd94..00000000
--- a/app/assets/fronts
+++ /dev/null
@@ -1,4 +0,0 @@
-snowflake-target https://1098762253.rsc.cdn77.org
-snowflake-fronts www.cdn77.com,www.phpmyadmin.net
-snowflake-stun stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443
-utls-imitate hellorandomizedalpn \ No newline at end of file
diff --git a/app/assets/fronts-amp b/app/assets/fronts-amp
new file mode 100644
index 00000000..94ec0212
--- /dev/null
+++ b/app/assets/fronts-amp
@@ -0,0 +1,4 @@
+url https://1098762253.rsc.cdn77.org
+fronts www.google.com
+ice stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443
+ampcache https://cdn.ampproject.org/
diff --git a/app/assets/fronts-http b/app/assets/fronts-http
new file mode 100644
index 00000000..57fa7ff9
--- /dev/null
+++ b/app/assets/fronts-http
@@ -0,0 +1,3 @@
+url https://1098762253.rsc.cdn77.org
+fronts www.cdn77.com,www.phpmyadmin.net
+ice stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443
diff --git a/app/assets/fronts-sqs b/app/assets/fronts-sqs
new file mode 100644
index 00000000..83f99a28
--- /dev/null
+++ b/app/assets/fronts-sqs
@@ -0,0 +1,3 @@
+ice stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443
+sqsqueue https://sqs.us-east-2.amazonaws.com/490393006362/snowflake-broker
+sqscreds eyJhd3MtYWNjZXNzLWtleS1pZCI6IkFLSUFYRUxOVE9FTkdCWVU3NkVBIiwiYXdzLXNlY3JldC1rZXkiOiJHdTlTWm9xTVg2WENIa2dZazlHZnBvYlpoVFZMdzNpL0N5cFdVVGZOIn0=
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
index 6b3ba348..cbe954db 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
@@ -28,6 +28,7 @@ import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getSavedProvider
import android.content.IntentFilter;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
@@ -44,6 +45,7 @@ import java.security.Security;
import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver;
import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.base.utils.ClientTransportPluginProvider;
import se.leap.bitmaskclient.base.utils.PRNGFixes;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
import se.leap.bitmaskclient.eip.EipSetupObserver;
@@ -61,12 +63,12 @@ public class BitmaskApp extends MultiDexApplication implements DefaultLifecycleO
private ProviderObservable providerObservable;
private DownloadBroadcastReceiver downloadBroadcastReceiver;
private TorStatusObservable torStatusObservable;
-
private ProviderSetupObservable providerSetupObservable;
private PreferenceHelper preferenceHelper;
+
@Override
public void onCreate() {
super.onCreate();
@@ -85,6 +87,7 @@ public class BitmaskApp extends MultiDexApplication implements DefaultLifecycleO
providerObservable.updateProvider(getSavedProviderFromSharedPreferences());
torStatusObservable = TorStatusObservable.getInstance();
providerSetupObservable = ProviderSetupObservable.getInstance();
+ ClientTransportPluginProvider.init(this);
EipSetupObserver.init(this);
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
index b8849c4d..e01542f5 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
@@ -44,6 +44,8 @@ public interface Constants {
String PREFERRED_CITY = "preferred_city";
// ATTENTION: this key is also used in bitmask-core for persistence
String COUNTRYCODE = "COUNTRYCODE";
+ // ATTENTION: this key is also used in bitmask-core
+ String GEOIP_LOOKUP_URL = "";
String USE_SNOWFLAKE = "use_snowflake";
String PREFER_UDP = "prefer_UDP";
String GATEWAY_PINNING = "gateway_pinning";
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/ClientTransportPluginProvider.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/ClientTransportPluginProvider.java
new file mode 100644
index 00000000..8e6387d4
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/ClientTransportPluginProvider.java
@@ -0,0 +1,39 @@
+package se.leap.bitmaskclient.base.utils;
+
+import android.content.Context;
+
+import org.torproject.jni.ClientTransportPluginInterface;
+
+import se.leap.bitmaskclient.tor.SnowflakePlugin;
+
+/**
+ * ClientTransportPluginProvider keeps a global reference of an implementation of ClientTransportPluginInterface.
+ * Currently the underlying plugin is utilizing IPtPtroxy and that library cannot be instantiated twice.
+ * Therefore we need keep a global reference to it and pass that around.
+ */
+public class ClientTransportPluginProvider {
+ private final SnowflakePlugin plugin;
+ private static ClientTransportPluginProvider instance = null;
+
+ private ClientTransportPluginProvider(Context context) throws IllegalStateException {
+ plugin = new SnowflakePlugin(context);
+ }
+
+ public static void init(Context context) {
+ if (instance == null) {
+ try {
+ instance = new ClientTransportPluginProvider(context);
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+
+ public static ClientTransportPluginInterface get() {
+ if (instance != null) {
+ return instance.plugin;
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
index 200617cf..38e32861 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
@@ -20,6 +20,7 @@ import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT;
import static se.leap.bitmaskclient.base.models.Constants.EXCLUDED_APPS;
import static se.leap.bitmaskclient.base.models.Constants.FIRST_TIME_USER_DATE;
import static se.leap.bitmaskclient.base.models.Constants.GATEWAY_PINNING;
+import static se.leap.bitmaskclient.base.models.Constants.GEOIP_LOOKUP_URL;
import static se.leap.bitmaskclient.base.models.Constants.LAST_DONATION_REMINDER_DATE;
import static se.leap.bitmaskclient.base.models.Constants.LAST_UPDATE_CHECK;
import static se.leap.bitmaskclient.base.models.Constants.LAST_USED_PROFILE;
@@ -218,7 +219,7 @@ public class PreferenceHelper {
try {
introducer = Introducer.fromUrl(BitmaskCoreProvider.getBitmaskMobile().getIntroducerURLByDomain(domain));
} catch (Exception e) {
- e.printStackTrace();
+ Log.w(TAG, "introducer lookup: " + e.getMessage());
}
Provider customProvider = new Provider(mainURL);
customProvider.setIntroducer(introducer);
@@ -670,6 +671,13 @@ public class PreferenceHelper {
putString(COUNTRYCODE, countryCode);
}
+ public static void setGeoIPLookupURL(String url) {
+ putString(GEOIP_LOOKUP_URL, url);
+ }
+ public static String getGeoIPLookupURL() {
+ return getString(GEOIP_LOOKUP_URL, null);
+ }
+
public static String getPreferredCity() {
return useObfuscationPinning() ? null : getString(PREFERRED_CITY, null);
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java b/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java
deleted file mode 100644
index ca71a6e2..00000000
--- a/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java
+++ /dev/null
@@ -1,292 +0,0 @@
-package se.leap.bitmaskclient.tor;
-/**
- * Copyright (c) 2024 LEAP Encryption Access Project and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.RETRY_AMP_CACHE_RENDEZVOUS;
-import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.RETRY_HTTP_RENDEZVOUS;
-
-import android.content.Context;
-import android.os.FileObserver;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.torproject.jni.ClientTransportPluginInterface;
-
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.lang.ref.WeakReference;
-import java.net.URL;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Random;
-import java.util.Scanner;
-import java.util.Vector;
-import java.util.concurrent.TimeoutException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import IPtProxy.Controller;
-import IPtProxy.IPtProxy;
-import IPtProxy.OnTransportEvents;
-
-public class ClientTransportPlugin implements ClientTransportPluginInterface, PropertyChangeListener {
- public static String TAG = ClientTransportPlugin.class.getSimpleName();
-
- private HashMap<String, String> mFronts;
- private final WeakReference<Context> contextRef;
- private long snowflakePort = -1;
- private FileObserver logFileObserver;
- private static final Pattern SNOWFLAKE_LOG_TIMESTAMP_PATTERN = Pattern.compile("((19|2[0-9])[0-9]{2}\\/\\d{1,2}\\/\\d{1,2} \\d{1,2}:\\d{1,2}:\\d{1,2}) ([\\S|\\s]+)");
- private TorStatusObservable.SnowflakeStatus snowflakeStatus;
- private String logfilePath;
- Handler handler;
- HandlerThread handlerThread;
- Controller controller;
-
- public ClientTransportPlugin(Context context) {
- this.contextRef = new WeakReference<>(context);
- handlerThread = new HandlerThread("clientTransportPlugin", Thread.MIN_PRIORITY);
- loadCdnFronts(context);
- controller = initializeController(context.getApplicationContext());
- }
-
- private Controller initializeController(Context context) {
- File ptDir = new File(context.getApplicationContext().getCacheDir(), "/pt_state");
- return IPtProxy.newController(ptDir.getAbsolutePath(), true, false, "DEBUG", new OnTransportEvents() {
- @Override
- public void connected(String s) { }
-
- @Override
- public void error(String s, Exception e) { }
-
- @Override
- public void stopped(String s, Exception e) { }
- });
- }
-
- @Override
- public void start() {
- Context context = contextRef.get();
- if (context == null) {
- return;
- }
- handlerThread.start();
- handler = new Handler(handlerThread.getLooper());
- TorStatusObservable.getInstance().addObserver(this);
- File logfile = new File(context.getApplicationContext().getCacheDir(), "snowflake.log");
- try {
- if (logfile.exists()) {
- logfile.delete();
- }
- logfile.createNewFile();
- } catch (IOException e) {
- e.printStackTrace();
- }
- this.logfilePath = logfile.getAbsolutePath();
- Random random = new Random();
- boolean useAmpCache = random.nextInt(2) == 0;
- startConnectionAttempt(useAmpCache, logfilePath);
- watchLogFile(logfile);
- }
-
- private void startConnectionAttempt(boolean useAmpCache, @NonNull String logfilePath) {
- //this is using the current, default Tor snowflake infrastructure
- String target = getCdnFront("snowflake-target");
- String fronts = getCdnFront("snowflake-fronts");
- String stunServer = getCdnFront("snowflake-stun");
- String ampCache = null;
- if (useAmpCache) {
- target = "https://snowflake-broker.torproject.net/";
- ampCache = "https://cdn.ampproject.org/";
- fronts = "www.google.com";
- }
-
- snowflakePort = startSnowflake(stunServer, target, fronts, ampCache, null, null, logfilePath, false, false, false, 5);
- Log.d(TAG, "startSnowflake running on port: " + snowflakePort);
- }
-
-/**
- StartSnowflake - Start IPtProxy's Snowflake client.
- @param ice Comma-separated list of ICE servers.
- @param url URL of signaling broker.
- @param fronts Comma-separated list of front domains.
- @param ampCache OPTIONAL. URL of AMP cache to use as a proxy for signaling.
- Only needed when you want to do the rendezvous over AMP instead of a domain fronted server.
- @param sqsQueueURL OPTIONAL. URL of SQS Queue to use as a proxy for signaling.
- @param sqsCredsStr OPTIONAL. Credentials to access SQS Queue
- @param logFile Name of log file. OPTIONAL. Defaults to no log.
- @param logToStateDir Resolve the log file relative to Tor's PT state dir.
- @param keepLocalAddresses Keep local LAN address ICE candidates.
- @param unsafeLogging Prevent logs from being scrubbed.
- @param maxPeers Capacity for number of multiplexed WebRTC peers. DEFAULTs to 1 if less than that.
- @return Port number where Snowflake will listen on, if no error happens during start up.
- */
- private long startSnowflake(String ice, String url, String fronts, String ampCache, String sqsQueueURL, String sqsCredsStr, String logFile, boolean logToStateDir, boolean keepLocalAddresses, boolean unsafeLogging, long maxPeers) {
- try {
- if (ice != null) controller.setSnowflakeIceServers(ice);
- if (url != null) controller.setSnowflakeBrokerUrl(url);
- if (fronts != null) controller.setSnowflakeFrontDomains(fronts);
- if (ampCache != null) controller.setSnowflakeAmpCacheUrl(ampCache);
- if (sqsQueueURL != null) controller.setSnowflakeSqsUrl(sqsQueueURL);
- if (sqsCredsStr != null) controller.setSnowflakeSqsCreds(sqsCredsStr);
- controller.setSnowflakeMaxPeers(5);
- controller.start(IPtProxy.Snowflake, "");
- return new URL("https://"+controller.localAddress(IPtProxy.Snowflake)).getPort();
- } catch (Exception e) {
- return -1;
- }
- }
-
- private void retryConnectionAttempt(boolean useAmpCache) {
- Log.d(TAG, ">> retryConnectionAttempt - " + (useAmpCache ? "amp cache" : "http domain fronting"));
- stopConnectionAttempt();
- startConnectionAttempt(useAmpCache, logfilePath);
- }
-
- private void watchLogFile(File logfile) {
- final Vector<String> lastBuffer = new Vector<>();
- logFileObserver = new FileObserver(logfile.getAbsolutePath()) {
- @Override
- public void onEvent(int event, @Nullable String name) {
- if (FileObserver.MODIFY == event) {
- try (Scanner scanner = new Scanner(logfile)) {
- Vector<String> currentBuffer = new Vector<>();
- while (scanner.hasNextLine()) {
- currentBuffer.add(scanner.nextLine());
- }
- if (lastBuffer.size() < currentBuffer.size()) {
- int startIndex = lastBuffer.size() > 0 ? lastBuffer.size() - 1 : 0;
- int endIndex = currentBuffer.size() - 1;
- Collection<String> newMessages = currentBuffer.subList(startIndex, endIndex);
- for (String message : newMessages) {
- logSnowflakeMessage(message);
- }
- lastBuffer.addAll(newMessages);
- }
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- }
- }
- }
- };
- logFileObserver.startWatching();
- }
-
- @Override
- public void stop() {
- stopConnectionAttempt();
- if (logFileObserver != null) {
- logFileObserver.stopWatching();
- logFileObserver = null;
- }
- TorStatusObservable.getInstance().deleteObserver(this);
- controller.stop(IPtProxy.Snowflake);
- controller = null;
- handlerThread.quit();
- handler = null;
- handlerThread = null;
- }
-
- private void stopConnectionAttempt() {
- controller.stop(IPtProxy.Snowflake);
- try {
- TorStatusObservable.waitUntil(this::isSnowflakeOff, 10);
- } catch (InterruptedException | TimeoutException e) {
- e.printStackTrace();
- }
- snowflakePort = -1;
- }
-
- private boolean isSnowflakeOff() {
- return TorStatusObservable.getSnowflakeStatus() == TorStatusObservable.SnowflakeStatus.STOPPED;
- }
-
- @Override
- public String getTorrc() {
- return "UseBridges 1\n" +
- "ClientTransportPlugin snowflake socks5 127.0.0.1:" + snowflakePort + "\n" +
- "Bridge snowflake 192.0.2.3:1";
- }
-
- private void loadCdnFronts(Context context) {
- if (mFronts == null) {
- mFronts = new HashMap<>();
- }
- try {
- BufferedReader reader = new BufferedReader(new InputStreamReader(context.getAssets().open("fronts")));
- String line;
- while (true) {
- line = reader.readLine();
- if (line == null) break;
- String[] front = line.split(" ");
- mFronts.put(front[0], front[1]);
- Log.d(TAG, "front: " + front[0] + ", " + front[1]);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- @Nullable
- private String getCdnFront(String service) {
- if (mFronts != null) {
- return mFronts.get(service);
- }
- return null;
- }
-
- private void logSnowflakeMessage(String message) {
- Matcher matcher = SNOWFLAKE_LOG_TIMESTAMP_PATTERN.matcher(message);
- if (matcher.matches()) {
- try {
- String strippedString = matcher.group(3).trim();
- if (!strippedString.isEmpty()) {
- TorStatusObservable.logSnowflakeMessage(contextRef.get(), strippedString);
- }
- } catch (IndexOutOfBoundsException | IllegalStateException | NullPointerException e) {
- e.printStackTrace();
- }
- } else {
- TorStatusObservable.logSnowflakeMessage(contextRef.get(), message);
- }
- }
-
- @Override
- public void propertyChange(PropertyChangeEvent evt) {
- if(TorStatusObservable.PROPERTY_CHANGE.equals(evt.getPropertyName())) {
- TorStatusObservable.SnowflakeStatus snowflakeStatus = TorStatusObservable.getSnowflakeStatus();
- if (snowflakeStatus == this.snowflakeStatus) {
- return;
- }
- if (snowflakeStatus == RETRY_HTTP_RENDEZVOUS) {
- handler.post(() -> retryConnectionAttempt(false));
- } else if (snowflakeStatus == RETRY_AMP_CACHE_RENDEZVOUS) {
- handler.post(() -> retryConnectionAttempt(true));
- }
- this.snowflakeStatus = snowflakeStatus;
- }
- }
-}
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/SnowflakePlugin.java b/app/src/main/java/se/leap/bitmaskclient/tor/SnowflakePlugin.java
new file mode 100644
index 00000000..04f43932
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/SnowflakePlugin.java
@@ -0,0 +1,328 @@
+package se.leap.bitmaskclient.tor;
+/**
+ * Copyright (c) 2026 LEAP Encryption Access Project and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import static se.leap.bitmaskclient.tor.SnowflakePlugin.RendezvousStrategy.HTTP;
+import static se.leap.bitmaskclient.tor.SnowflakePlugin.RendezvousStrategy.SQS;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.RECONNECTING;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.STOPPED;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+
+import org.jetbrains.annotations.Blocking;
+import org.torproject.jni.ClientTransportPluginInterface;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeoutException;
+
+import IPtProxy.Controller;
+import IPtProxy.IPtProxy;
+import IPtProxy.OnTransportEvents;
+import mobilemodels.BitmaskMobileCore;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.utils.BitmaskCoreProvider;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+
+public class SnowflakePlugin implements ClientTransportPluginInterface {
+ public static String TAG = SnowflakePlugin.class.getSimpleName();
+
+ enum RendezvousStrategy {
+ HTTP,
+ // sqs queue
+ SQS,
+ //amp cache
+ AMP
+ }
+
+ private HashMap<String, String> mFronts;
+ private final WeakReference<Context> contextRef;
+ Handler handler;
+ HandlerThread handlerThread;
+ Controller controller;
+
+ /**
+ * Constructs a SnowflakePlugin instance.
+ *
+ * @param context an arbitrary context
+ * @throws IllegalStateException if initialization of the IPtProxy controller fails
+ */
+ public SnowflakePlugin(Context context) throws IllegalStateException {
+ Log.d(TAG, "initialize ClientTransport Plugin");
+ controller = initializeController(context.getApplicationContext());
+ this.contextRef = new WeakReference<>(context.getApplicationContext());
+ handlerThread = new HandlerThread("clientTransportPlugin", Thread.MIN_PRIORITY);
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+ loadFronts(context.getApplicationContext());
+ }
+
+ private Controller initializeController(Context context) throws IllegalStateException {
+ File ptDir = new File(context.getCacheDir(), "/pt_state/");
+ Controller controller = IPtProxy.newController(ptDir.getAbsolutePath(), true, false, "DEBUG", new OnTransportEvents() {
+ @Override
+ public void connected(String s) {
+ Log.d(TAG, "snowflake status callback: connected");
+ TorStatusObservable.logSnowflakeMessage(context, context.getString(R.string.log_done));
+ TorStatusObservable.setSnowflakeStatus(TorStatusObservable.SnowflakeStatus.CONNECTED);
+ }
+
+ @Override
+ public void error(String s, Exception e) {
+ Log.d(TAG, "snowflake status callback: error - " + e.getMessage());
+ TorStatusObservable.logSnowflakeMessage(context, e.getMessage());
+ TorStatusObservable.logSnowflakeMessage(context, context.getString(R.string.state_reconnecting));
+ TorStatusObservable.setSnowflakeStatus(RECONNECTING, e.getMessage());
+ }
+
+ @Override
+ public void stopped(String s, Exception e) {
+ Log.d(TAG, "snowflake status callback: stopped");
+ if (e != null && e.getMessage() != null) {
+ Log.e(TAG, e.getMessage());
+ }
+ TorStatusObservable.logSnowflakeMessage(context, context.getString(R.string.state_noprocess));
+ TorStatusObservable.setSnowflakeStatus(STOPPED);
+ }
+ });
+
+ if (controller == null) {
+ throw new IllegalStateException("Failed to initialize IPtProxy controller");
+ }
+ return controller;
+ }
+
+ /**
+ * Starts Snowflake.
+ *
+ * This method selects the best rendezvous strategy based on the user's
+ * estimated broad geographical location and initiates the connection attempt.
+ */
+ @Override
+ public void start() {
+ Context context = contextRef.get();
+ if (context == null) {
+ return;
+ }
+
+ shortenLogs(context);
+ RendezvousStrategy strategy = selectRendezvousStrategy();
+ startConnectionAttempt(strategy);
+ }
+
+ private void shortenLogs(Context context) {
+ // ensure we keep the persisted logfile short
+ File logfile = new File(context.getApplicationContext().getCacheDir(), "/pt_state/" + IPtProxy.LogFileName);
+ try {
+ if (logfile.exists()) {
+ logfile.delete();
+ }
+ logfile.createNewFile();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Attempt to select best rendezvous strategy depending on users broad location.
+ * @return a snowflake RendezvousStrategy
+ */
+ private RendezvousStrategy selectRendezvousStrategy() {
+ Random random = new Random();
+ ArrayList<RendezvousStrategy> strategies = new ArrayList<>(Arrays.asList(RendezvousStrategy.values()));
+ String countryCode = getCountryCode();
+ if ("RU".equals(countryCode) || "CN".equals(countryCode)) {
+ strategies.remove(RendezvousStrategy.HTTP);
+ } else {
+ strategies.remove(SQS);
+ }
+ int randomIndex = random.nextInt(strategies.size());
+ return strategies.get(randomIndex);
+ }
+
+ /**
+ * Attempt to determine the country code from the users IP address
+ * @return country code or null if geoip lookup failed
+ */
+ private String getCountryCode() {
+ try {
+ String geoIPLookupURL = PreferenceHelper.getGeoIPLookupURL();
+ if (geoIPLookupURL != null) {
+ BitmaskMobileCore bm = BitmaskCoreProvider.getBitmaskMobile();
+ String stunServers = getFront(HTTP, "ice").replaceAll("stun:", "");
+ List<String> serverList = Arrays.asList(stunServers.split(","));
+ Collections.shuffle(serverList);
+ stunServers = String.join(",", serverList);
+ bm.setStunServers(stunServers);
+ bm.setCountryCodeLookupURL(geoIPLookupURL);
+ return bm.getGeolocation();
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+ return PreferenceHelper.getBaseCountry();
+ }
+
+ private void startConnectionAttempt(RendezvousStrategy strategy) {
+ try {
+ String target = null;
+ String fronts = null;
+ String ampCache = null;
+ String sqsQueue = null;
+ String sqsCreds = null;
+ String stunServer = getFront(strategy, "ice");
+ switch (strategy) {
+ case HTTP -> {
+ target = getFront(strategy, "url");
+ fronts = getFront(strategy, "fronts");
+ }
+ case AMP -> {
+ target = getFront(strategy, "url");
+ fronts = getFront(strategy, "fronts");
+ ampCache = getFront(strategy, "ampcache");
+ }
+ case SQS -> {
+ sqsCreds = getFront(strategy, "sqscreds");
+ sqsQueue = getFront(strategy, "sqsqueue");
+ }
+ }
+
+
+ startSnowflake(stunServer, target, fronts, ampCache, sqsQueue, sqsCreds, 5);
+ } catch (NullPointerException npe) {
+ Log.e(TAG, "failed to start Snowflake: " + npe.getMessage());
+ Context c = contextRef.get();
+ if (c != null) {
+ TorStatusObservable.logSnowflakeMessage(c, npe.getMessage());
+ }
+ TorStatusObservable.setSnowflakeStatus(STOPPED);
+ }
+ }
+
+/**
+ StartSnowflake - Start IPtProxy's Snowflake client.
+ @param ice Comma-separated list of ICE servers.
+ @param url URL of signaling broker.
+ @param fronts Comma-separated list of front domains.
+ @param ampCache OPTIONAL. URL of AMP cache to use as a proxy for signaling.
+ Only needed when you want to do the rendezvous over AMP instead of a domain fronted server.
+ @param sqsQueueURL OPTIONAL. URL of SQS Queue to use as a proxy for signaling.
+ @param sqsCredsStr OPTIONAL. Credentials to access SQS Queue
+ @param maxPeers Capacity for number of multiplexed WebRTC peers. DEFAULTs to 1 if less than that.
+ */
+ private void startSnowflake(String ice, String url, String fronts, String ampCache, String sqsQueueURL, String sqsCredsStr, long maxPeers) {
+ Context c = contextRef.get();
+ try {
+ Log.d(TAG, "start snowflake with ice " + ice + "\nurl: " + url + "\nfronts: " + fronts + "\nampCache: " + ampCache + "\nsqsQueue: " + sqsQueueURL);
+ if (ice != null) controller.setSnowflakeIceServers(ice);
+ if (url != null) controller.setSnowflakeBrokerUrl(url);
+ if (fronts != null) controller.setSnowflakeFrontDomains(fronts);
+ if (ampCache != null) controller.setSnowflakeAmpCacheUrl(ampCache);
+ if (sqsQueueURL != null) controller.setSnowflakeSqsUrl(sqsQueueURL);
+ if (sqsCredsStr != null) controller.setSnowflakeSqsCreds(sqsCredsStr);
+ controller.setSnowflakeMaxPeers(maxPeers);
+ controller.start(IPtProxy.Snowflake, "");
+ if (c != null) {
+ TorStatusObservable.logSnowflakeMessage(c, c.getString(R.string.snowflake_started));
+ }
+ TorStatusObservable.setSnowflakeStatus(TorStatusObservable.SnowflakeStatus.STARTED);
+ } catch (Exception e) {
+ Log.e(TAG, this.toString() + "failed to start Snowflake: " + e.getMessage());
+ if (c != null) {
+ TorStatusObservable.logSnowflakeMessage(c, e.getMessage());
+ }
+ TorStatusObservable.setSnowflakeStatus(STOPPED);
+ }
+ }
+
+ /**
+ * Stops Snowflake connection.
+ * This method is blocking the current thread until snowflake was stopped or the timeout reached.
+ */
+ @Blocking
+ @Override
+ public void stop() {
+ controller.stop(IPtProxy.Snowflake);
+ try {
+ TorStatusObservable.waitUntil(this::isSnowflakeOff, 10);
+ } catch (InterruptedException | TimeoutException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private boolean isSnowflakeOff() {
+ return TorStatusObservable.getSnowflakeStatus() == TorStatusObservable.SnowflakeStatus.STOPPED;
+ }
+
+ /**
+ * Retrieves the Tor configuration string for the Snowflake pluggable transport.
+ *
+ * @return a Tor configuration string if the controller is running, otherwise an empty string
+ */
+ @Override
+ public String getTorrc() {
+ if (controller.port(IPtProxy.Snowflake) <= 0) {
+ return "";
+ }
+ return "UseBridges 1\n" +
+ "ClientTransportPlugin snowflake socks5 127.0.0.1:" + controller.port(IPtProxy.Snowflake) + "\n" +
+ "Bridge snowflake 192.0.2.3:1";
+ }
+
+ private void loadFronts(Context context) {
+ if (mFronts == null) {
+ mFronts = new HashMap<>();
+ }
+ for (RendezvousStrategy strategy : RendezvousStrategy.values()) {
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(context.getAssets().open("fronts-"+strategy.toString().toLowerCase())));
+ String line;
+ while (true) {
+ line = reader.readLine();
+ if (line == null) break;
+ String[] front = line.split(" ");
+ mFronts.put(strategy+front[0], front[1]);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ private String getFront(RendezvousStrategy strategy, String arg) throws NullPointerException {
+ if (mFronts != null) {
+ String front = mFronts.get(strategy+arg);
+ if (front != null) {
+ return front;
+ }
+ }
+ throw new NullPointerException("expected value "+ arg + " for rendezvous strategy " + strategy + " not found.");
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java b/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java
index 4c6ddaba..d837f58c 100644
--- a/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java
@@ -33,6 +33,7 @@ import org.torproject.jni.TorService;
import java.util.concurrent.TimeoutException;
+import se.leap.bitmaskclient.base.utils.ClientTransportPluginProvider;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
public class TorServiceCommand {
@@ -159,7 +160,7 @@ public class TorServiceCommand {
if (PreferenceHelper.getUseSnowflake()) {
Log.d(TAG, "serviceConnection is still null");
if (!TorService.hasClientTransportPlugin()) {
- TorService.setClientTransportPlugin(new ClientTransportPlugin(context.getApplicationContext()));
+ TorService.setClientTransportPlugin(ClientTransportPluginProvider.get());
}
return new TorServiceConnection(context);
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java b/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java
index 5a49fda2..92e74a9e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java
@@ -15,13 +15,7 @@ package se.leap.bitmaskclient.tor;
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.BROKER_REPLIED_SUCCESS;
-import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.NEGOTIATING_RENDEZVOUS_VIA_AMP_CACHE;
-import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.NEGOTIATING_RENDEZVOUS_VIA_HTTP;
-import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.RETRY_AMP_CACHE_RENDEZVOUS;
-import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.RETRY_HTTP_RENDEZVOUS;
-import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.SENDING_DATA;
-import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.STARTED;
+
import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.STOPPED;
import android.content.Context;
@@ -59,12 +53,8 @@ public class TorStatusObservable {
public enum SnowflakeStatus {
STARTED,
- NEGOTIATING_RENDEZVOUS_VIA_HTTP,
- NEGOTIATING_RENDEZVOUS_VIA_AMP_CACHE,
- RETRY_HTTP_RENDEZVOUS,
- RETRY_AMP_CACHE_RENDEZVOUS,
- BROKER_REPLIED_SUCCESS,
- SENDING_DATA,
+ CONNECTED,
+ RECONNECTING,
STOPPED
}
@@ -72,21 +62,6 @@ public class TorStatusObservable {
// the TorService has sent the shutdown signal
private boolean cancelled = false;
- public static final String LOG_TAG_TOR = "[TOR]";
- public static final String LOG_TAG_SNOWFLAKE = "[SNOWFLAKE]";
- public static final String SNOWFLAKE_STARTED = "--- Starting Snowflake Client ---";
- public static final String SNOWFLAKE_STOPPED_COLLECTING = "---- SnowflakeConn: end collecting snowflakes ---";
- public static final String SNOWFLAKE_COPY_LOOP_STOPPED = "copy loop ended";
- public static final String SNOWFLAKE_SOCKS_ERROR = "SOCKS accept error";
- public static final String SNOWFLAKE_NEGOTIATING_HTTP = "Negotiating via HTTP rendezvous...";
- public static final String SNOWFLAKE_NEGOTIATING_AMP_CACHE = "Negotiating via AMP cache rendezvous...";
- public static final String SNOWFLAKE_CONNECTION_CLOSING = "WebRTC: Closing";
- public static final String SNOWFLAKE_HTTP_RESPONSE_200 = "HTTP rendezvous response: 200";
- public static final String SNOWFLAKE_AMP_CACHE_RESPONSE_200 = "AMP cache rendezvous response: 200";
-
- public static final String SNOWFLAKE_SENDING_DATA = "Traffic Bytes (in|out):";
-
-
private static TorStatusObservable instance;
private TorStatus status = TorStatus.OFF;
private SnowflakeStatus snowflakeStatus = STOPPED;
@@ -97,7 +72,6 @@ public class TorStatusObservable {
private int port = -1;
private int socksPort = -1;
private int bootstrapPercent = -1;
- private int retrySnowflakeRendezVous = 0;
private final Vector<String> lastLogs = new Vector<>(100);
private TorStatusObservable() {
@@ -120,6 +94,18 @@ public class TorStatusObservable {
return getInstance().snowflakeStatus;
}
+ public static void setSnowflakeStatus(TorStatusObservable.SnowflakeStatus snowflakeStatus) {
+ setSnowflakeStatus(snowflakeStatus, null);
+ }
+
+ public static void setSnowflakeStatus(TorStatusObservable.SnowflakeStatus snowflakeStatus, String snowflakeLog) {
+ if (snowflakeLog != null) {
+ getInstance().lastSnowflakeLog = snowflakeLog;
+ }
+ getInstance().snowflakeStatus = snowflakeStatus;
+ instance.notifyObservers();
+ }
+
/**
* Waits on the current Thread until a certain tor/snowflake status has been reached
* @param condition defines when wait should be interrupted
@@ -163,46 +149,8 @@ public class TorStatusObservable {
if (getInstance().status != TorStatus.OFF) {
getInstance().torNotificationManager.buildTorNotification(context, getStringForCurrentStatus(context), getNotificationLog(), getBootstrapProgress());
}
- //TODO: implement proper state signalling in IPtProxy
message = message.trim();
- if (SNOWFLAKE_STARTED.equals(message)) {
- getInstance().snowflakeStatus = STARTED;
- getInstance().lastSnowflakeLog = context.getString(R.string.snowflake_started);
- } else if (SNOWFLAKE_NEGOTIATING_HTTP.equals(message)) {
- getInstance().snowflakeStatus = NEGOTIATING_RENDEZVOUS_VIA_HTTP;
- getInstance().lastSnowflakeLog = context.getString(R.string.snowflake_negotiating_rendezvous_http);
- } else if (SNOWFLAKE_NEGOTIATING_AMP_CACHE.equals(message)) {
- getInstance().snowflakeStatus = NEGOTIATING_RENDEZVOUS_VIA_AMP_CACHE;
- getInstance().lastSnowflakeLog = context.getString(R.string.snowflake_negotiating_rendezvous_amp_cache);
- } else if (SNOWFLAKE_STOPPED_COLLECTING.equals(message) ||
- SNOWFLAKE_COPY_LOOP_STOPPED.equals(message) ||
- message.contains(SNOWFLAKE_SOCKS_ERROR)) {
- getInstance().snowflakeStatus = STOPPED;
- getInstance().lastSnowflakeLog = context.getString(R.string.snowflake_socks_error);
- } else if (SNOWFLAKE_CONNECTION_CLOSING.equals(message)) {
- if (getInstance().snowflakeStatus == NEGOTIATING_RENDEZVOUS_VIA_HTTP) {
- if (getInstance().retrySnowflakeRendezVous < 3) {
- getInstance().retrySnowflakeRendezVous += 1;
- } else {
- getInstance().retrySnowflakeRendezVous = 0;
- getInstance().snowflakeStatus = RETRY_AMP_CACHE_RENDEZVOUS;
- }
- } else if (getInstance().snowflakeStatus == NEGOTIATING_RENDEZVOUS_VIA_AMP_CACHE) {
- if (getInstance().retrySnowflakeRendezVous < 3) {
- getInstance().retrySnowflakeRendezVous += 1;
- } else {
- getInstance().retrySnowflakeRendezVous = 0;
- getInstance().snowflakeStatus = RETRY_HTTP_RENDEZVOUS;
- }
- }
- } else if (SNOWFLAKE_AMP_CACHE_RESPONSE_200.equals(message) || SNOWFLAKE_HTTP_RESPONSE_200.equals(message)) {
- getInstance().snowflakeStatus = BROKER_REPLIED_SUCCESS;
- getInstance().retrySnowflakeRendezVous = 0;
- getInstance().lastSnowflakeLog = context.getString(R.string.snowflake_broker_success);
- } else if (message.contains(SNOWFLAKE_SENDING_DATA)) {
- getInstance().snowflakeStatus = SENDING_DATA;
- getInstance().lastSnowflakeLog = context.getString(R.string.snowflake_sending_data);
- }
+ getInstance().lastSnowflakeLog = message;
Log.d(TAG, "snowflake status " + getInstance().snowflakeStatus);
instance.notifyObservers();
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 845af01c..7953caf5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -227,11 +227,6 @@
<string name="tor_status">Tor Status</string>
<string name="snowflake_status">Snowflake Status</string>
<string name="snowflake_started">Snowflake client started</string>
- <string name="snowflake_negotiating_rendezvous_http">Negotiating Snowflake proxy rendezvous (http)</string>
- <string name="snowflake_negotiating_rendezvous_amp_cache">Negotiating Snowflake proxy rendezvous (amp cache)</string>
- <string name="snowflake_socks_error">Snowflake SOCKS error</string>
- <string name="snowflake_broker_success">Snowflake proxy rendezvous successful</string>
- <string name="snowflake_sending_data">Sending data via Snowflake</string>
<string name="title_upcoming_connection_request">Upcoming Connection Request</string>
<string name="title_upcoming_request">Upcoming requests</string>
<string name="title_upcoming_request_summary">In the next panels Android will ask for your permission in form of a Connection Request and Notification Request.</string>
diff --git a/bitmask-core-android b/bitmask-core-android
-Subproject 2977b7402b5338677a716e08f5418cfc5d2e929
+Subproject fbb9817985dce1dcce7343c4dedc57508a2dd69