From 4cfa02e0a36759185decdbf920a9e1502fd32ccc Mon Sep 17 00:00:00 2001 From: cyBerta Date: Mon, 12 Dec 2022 13:10:05 +0100 Subject: implement better snowflake connection setup handling in case either http domain fronting or amp cache is blocked --- .../bitmaskclient/tor/ClientTransportPlugin.java | 80 +++++++++++++++++++--- .../bitmaskclient/tor/TorStatusObservable.java | 69 ++++++++++++++++--- .../leap/bitmaskclient/testutils/MockHelper.java | 4 +- 3 files changed, 132 insertions(+), 21 deletions(-) (limited to 'app/src') diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java b/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java index 5f7fa74a..092635d0 100644 --- a/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java +++ b/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java @@ -15,10 +15,17 @@ package se.leap.bitmaskclient.tor; * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + +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; @@ -31,6 +38,9 @@ import java.io.InputStreamReader; import java.lang.ref.WeakReference; import java.util.Collection; import java.util.HashMap; +import java.util.Observable; +import java.util.Observer; +import java.util.Random; import java.util.Scanner; import java.util.Vector; import java.util.concurrent.TimeoutException; @@ -39,7 +49,7 @@ import java.util.regex.Pattern; import IPtProxy.IPtProxy; -public class ClientTransportPlugin implements ClientTransportPluginInterface { +public class ClientTransportPlugin implements ClientTransportPluginInterface, Observer { public static String TAG = ClientTransportPlugin.class.getSimpleName(); private HashMap mFronts; @@ -47,9 +57,14 @@ public class ClientTransportPlugin implements ClientTransportPluginInterface { 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; public ClientTransportPlugin(Context context) { this.contextRef = new WeakReference<>(context); + handlerThread = new HandlerThread("clientTransportPlugin", Thread.MIN_PRIORITY); loadCdnFronts(context); } @@ -59,6 +74,9 @@ public class ClientTransportPlugin implements ClientTransportPluginInterface { if (context == null) { return; } + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); + TorStatusObservable.getInstance().addObserver(this); File logfile = new File(context.getApplicationContext().getCacheDir(), "snowflake.log"); Log.d(TAG, "logfile at " + logfile.getAbsolutePath()); try { @@ -69,15 +87,34 @@ public class ClientTransportPlugin implements ClientTransportPluginInterface { } 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 front = getCdnFront("snowflake-front"); String stunServer = getCdnFront("snowflake-stun"); Log.d(TAG, "startSnowflake. target: " + target + ", front:" + front + ", stunServer" + stunServer); - snowflakePort = IPtProxy.startSnowflake(stunServer, target, front, null, logfile.getAbsolutePath(), false, false, true, 5); + String ampCache = null; + if (useAmpCache) { + Log.d(TAG, "using ampcache for rendez-vous"); + target = "https://snowflake-broker.torproject.net/"; + ampCache = "https://cdn.ampproject.org/"; + front = "www.google.com"; + } + snowflakePort = IPtProxy.startSnowflake(stunServer, target, front, ampCache, logfilePath, false, false, true, 5); Log.d(TAG, "startSnowflake running on port: " + snowflakePort); - watchLogFile(logfile); + } + + private void retryConnectionAttempt(boolean useAmpCache) { + Log.d(TAG, ">> retryConnectionAttempt - " + (useAmpCache ? "amp cache" : "http domain fronting")); + stopConnectionAttempt(); + startConnectionAttempt(useAmpCache, logfilePath); } private void watchLogFile(File logfile) { @@ -111,6 +148,18 @@ public class ClientTransportPlugin implements ClientTransportPluginInterface { @Override public void stop() { + stopConnectionAttempt(); + if (logFileObserver != null) { + logFileObserver.stopWatching(); + logFileObserver = null; + } + TorStatusObservable.getInstance().deleteObserver(this); + handlerThread.quit(); + handler = null; + handlerThread = null; + } + + private void stopConnectionAttempt() { IPtProxy.stopSnowflake(); try { TorStatusObservable.waitUntil(this::isSnowflakeOff, 10); @@ -118,14 +167,10 @@ public class ClientTransportPlugin implements ClientTransportPluginInterface { e.printStackTrace(); } snowflakePort = -1; - if (logFileObserver != null) { - logFileObserver.stopWatching(); - logFileObserver = null; - } } private boolean isSnowflakeOff() { - return TorStatusObservable.getSnowflakeStatus() == TorStatusObservable.SnowflakeStatus.OFF; + return TorStatusObservable.getSnowflakeStatus() == TorStatusObservable.SnowflakeStatus.STOPPED; } @Override @@ -170,11 +215,28 @@ public class ClientTransportPlugin implements ClientTransportPluginInterface { if (strippedString.length() > 0) { TorStatusObservable.logSnowflakeMessage(contextRef.get(), strippedString); } - } catch (IndexOutOfBoundsException | IllegalStateException e) { + } catch (IndexOutOfBoundsException | IllegalStateException | NullPointerException e) { e.printStackTrace(); } } else { TorStatusObservable.logSnowflakeMessage(contextRef.get(), message); } } + + @Override + public void update(Observable o, Object arg) { + if (o instanceof TorStatusObservable) { + TorStatusObservable.SnowflakeStatus snowflakeStatus = TorStatusObservable.getSnowflakeStatus(); + if (snowflakeStatus == this.snowflakeStatus) { + return; + } + Log.d(TAG, "clientTransportPlugin: snowflake status " + this.snowflakeStatus); + 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/TorStatusObservable.java b/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java index 7eee1a9d..845d1789 100644 --- a/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java +++ b/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java @@ -15,6 +15,14 @@ package se.leap.bitmaskclient.tor; * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +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.STARTED; +import static se.leap.bitmaskclient.tor.TorStatusObservable.SnowflakeStatus.STOPPED; + import android.content.Context; import android.util.Log; @@ -46,8 +54,13 @@ public class TorStatusObservable extends Observable { } public enum SnowflakeStatus { - ON, - OFF + STARTED, + NEGOTIATING_RENDEZVOUS_VIA_HTTP, + NEGOTIATING_RENDEZVOUS_VIA_AMP_CACHE, + RETRY_HTTP_RENDEZVOUS, + RETRY_AMP_CACHE_RENDEZVOUS, + BROKER_REPLIED_SUCCESS, + STOPPED } // indicates if the user has cancelled Tor, the actual TorStatus can still be different until @@ -60,17 +73,23 @@ public class TorStatusObservable extends Observable { 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"; private static TorStatusObservable instance; private TorStatus status = TorStatus.OFF; - private SnowflakeStatus snowflakeStatus = SnowflakeStatus.OFF; + private SnowflakeStatus snowflakeStatus = STOPPED; private final TorNotificationManager torNotificationManager; private String lastError; private String lastTorLog = ""; private String lastSnowflakeLog = ""; private int port = -1; private int bootstrapPercent = -1; - private Vector lastLogs = new Vector<>(100); + private int retrySnowflakeRendezVous = 0; + private final Vector lastLogs = new Vector<>(100); private TorStatusObservable() { torNotificationManager = new TorNotificationManager(); @@ -128,14 +147,44 @@ public class TorStatusObservable extends Observable { getInstance().torNotificationManager.buildTorNotification(context, getStringForCurrentStatus(context), getNotificationLog(), getBootstrapProgress()); } //TODO: implement proper state signalling in IPtProxy - if (SNOWFLAKE_STARTED.equals(message.trim())) { + message = message.trim(); + if (SNOWFLAKE_STARTED.equals(message)) { Log.d(TAG, "snowflakeStatus ON"); - getInstance().snowflakeStatus = SnowflakeStatus.ON; - } else if (SNOWFLAKE_STOPPED_COLLECTING.equals(message.trim()) || - SNOWFLAKE_COPY_LOOP_STOPPED.equals(message.trim()) || - message.trim().contains(SNOWFLAKE_SOCKS_ERROR)) { + getInstance().snowflakeStatus = STARTED; + } else if (SNOWFLAKE_NEGOTIATING_HTTP.equals(message)) { + Log.d(TAG, "snowflake negotiating via http"); + getInstance().snowflakeStatus = NEGOTIATING_RENDEZVOUS_VIA_HTTP; + } else if (SNOWFLAKE_NEGOTIATING_AMP_CACHE.equals(message)) { + Log.d(TAG, "snowflake negotiating via amp cache"); + getInstance().snowflakeStatus = NEGOTIATING_RENDEZVOUS_VIA_AMP_CACHE; + } else if (SNOWFLAKE_STOPPED_COLLECTING.equals(message) || + SNOWFLAKE_COPY_LOOP_STOPPED.equals(message) || + message.contains(SNOWFLAKE_SOCKS_ERROR)) { Log.d(TAG, "snowflakeStatus OFF"); - getInstance().snowflakeStatus = SnowflakeStatus.OFF; + getInstance().snowflakeStatus = STOPPED; + } else if (SNOWFLAKE_CONNECTION_CLOSING.equals(message)) { + Log.d(TAG, "snowflake connection closing..."); + if (getInstance().snowflakeStatus == NEGOTIATING_RENDEZVOUS_VIA_HTTP) { + if (getInstance().retrySnowflakeRendezVous < 3) { + getInstance().retrySnowflakeRendezVous += 1; + } else { + getInstance().retrySnowflakeRendezVous = 0; + getInstance().snowflakeStatus = RETRY_AMP_CACHE_RENDEZVOUS; + Log.d(TAG, "snowflake retry amp cache"); + } + } 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; + Log.d(TAG, "snowflake retry http domain fronting"); + } + } + } else if (SNOWFLAKE_AMP_CACHE_RESPONSE_200.equals(message) || SNOWFLAKE_HTTP_RESPONSE_200.equals(message)) { + getInstance().snowflakeStatus = BROKER_REPLIED_SUCCESS; + getInstance().retrySnowflakeRendezVous = 0; + Log.d(TAG, "snowflake broker replied success"); } instance.setChanged(); instance.notifyObservers(); diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java b/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java index e14fa4c3..651aa345 100644 --- a/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java @@ -550,9 +550,9 @@ public class MockHelper { }); when(TorStatusObservable.getSnowflakeStatus()).thenAnswer((Answer) invocation -> { if (waitUntilSuccess.get()) { - return TorStatusObservable.SnowflakeStatus.ON; + return TorStatusObservable.SnowflakeStatus.STARTED; } - return TorStatusObservable.SnowflakeStatus.OFF; + return TorStatusObservable.SnowflakeStatus.STOPPED; }); if (exception != null) { -- cgit v1.2.3