diff options
| -rw-r--r-- | app/assets/fronts | 4 | ||||
| -rw-r--r-- | app/assets/fronts-amp | 4 | ||||
| -rw-r--r-- | app/assets/fronts-http | 3 | ||||
| -rw-r--r-- | app/assets/fronts-sqs | 3 | ||||
| -rw-r--r-- | app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java | 5 | ||||
| -rw-r--r-- | app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java | 2 | ||||
| -rw-r--r-- | app/src/main/java/se/leap/bitmaskclient/base/utils/ClientTransportPluginProvider.java | 39 | ||||
| -rw-r--r-- | app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java | 10 | ||||
| -rw-r--r-- | app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java | 292 | ||||
| -rw-r--r-- | app/src/main/java/se/leap/bitmaskclient/tor/SnowflakePlugin.java | 328 | ||||
| -rw-r--r-- | app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java | 3 | ||||
| -rw-r--r-- | app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java | 84 | ||||
| -rw-r--r-- | app/src/main/res/values/strings.xml | 5 | ||||
| m--------- | bitmask-core-android | 0 |
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 |
