From e5b8b83f852298698ad9edb00e48daa5fabf205e Mon Sep 17 00:00:00 2001 From: Benjamin Erhart Date: Thu, 23 Jan 2025 18:00:22 +0100 Subject: [PATCH] Issue #1193: Added support for mixing bridge types with custom bridges Meek, Obfs4 and Webtunnel can now be used all in parallel. --- app-tv/build.gradle | 7 ++ .../android/CustomBridgeBottomSheet.kt | 3 +- build.gradle | 2 +- gradle/libs.versions.toml | 9 +- intentintegrator/build.gradle | 7 ++ orbotservice/build.gradle | 7 ++ .../android/service/OrbotService.java | 62 +++++++------- .../torproject/android/service/util/Bridge.kt | 85 +++++++++++++++++++ 8 files changed, 147 insertions(+), 35 deletions(-) create mode 100644 orbotservice/src/main/java/org/torproject/android/service/util/Bridge.kt diff --git a/app-tv/build.gradle b/app-tv/build.gradle index b067d05a5..2552e147c 100644 --- a/app-tv/build.gradle +++ b/app-tv/build.gradle @@ -1,3 +1,6 @@ +plugins { + alias(libs.plugins.kotlin.android) +} apply from: "../commons.gradle" android { @@ -41,6 +44,9 @@ android { archivesBaseName = "Orbot-TV-$versionName" } } + kotlinOptions { + jvmTarget = '17' + } } configurations { @@ -65,6 +71,7 @@ dependencies { implementation(libs.androidx.palette) implementation(libs.androidx.recyclerview) implementation(libs.apl.appintro) + implementation libs.androidx.core.ktx androidTestImplementation(libs.fastlane.screengrab) } diff --git a/app/src/main/java/org/torproject/android/CustomBridgeBottomSheet.kt b/app/src/main/java/org/torproject/android/CustomBridgeBottomSheet.kt index 2d5991e9a..3a2a63c3f 100644 --- a/app/src/main/java/org/torproject/android/CustomBridgeBottomSheet.kt +++ b/app/src/main/java/org/torproject/android/CustomBridgeBottomSheet.kt @@ -12,7 +12,7 @@ class CustomBridgeBottomSheet(private val callbacks: ConnectionHelperCallbacks) OrbotBottomSheetDialogFragment() { companion object { const val TAG = "CustomBridgeBottomSheet" - private const val bridgeStatement = "obfs4" + private val bridgeStatement = Regex("(obfs4|meek|webtunnel)") } private lateinit var btnAction: Button @@ -42,5 +42,4 @@ class CustomBridgeBottomSheet(private val callbacks: ConnectionHelperCallbacks) btnAction.isEnabled = !(etBridges.text.isEmpty() || !etBridges.text.contains(bridgeStatement)) } - } \ No newline at end of file diff --git a/build.gradle b/build.gradle index aa4c23fa4..eb13c78e0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id "com.android.application" version "8.8.0" apply false - id "org.jetbrains.kotlin.android" version "2.0.0" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index da71b408a..165c46103 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ androidx-leanback-tab = "1.1.0-beta01" androidx-localbroadcast = "1.1.0" androidx-palette = "1.0.0" androidx-preference = "1.2.1" -androidx-recyclerview = "1.3.2" +androidx-recyclerview = "1.4.0" androidx-work = "2.10.0" apl-appintro = "v4.2.3" espresso-core = "3.6.1" @@ -23,11 +23,12 @@ guardian-jtorctl = "0.4.8.13" junit = "4.13.2" junit-version = "1.2.1" navigation-fragment-ktx = "2.8.5" -retrofit = "2.9.0" -rootbeer-lib = "0.1.0" +retrofit = "2.11.0" +rootbeer-lib = "0.1.1" tor-android = "0.4.8.13" pcap-core = "1.8.2" pcap-factory = "1.8.2" +kotlin = "2.1.0" [libraries] android-material = { group = "com.google.android.material", name = "material", version.ref = "android-material" } @@ -64,3 +65,5 @@ rootbeer-lib = { module = "com.scottyab:rootbeer-lib", version.ref = "rootbeer-l tor-android = { group = "info.guardianproject", name = "tor-android", version.ref = "tor-android" } pcap-core = { group = "org.pcap4j", name = "pcap4j-core", version.ref = "pcap-core" } pcap-factory = { group = "org.pcap4j", name = "pcap4j-packetfactory-static", version.ref = "pcap-factory" } +[plugins] +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/intentintegrator/build.gradle b/intentintegrator/build.gradle index 044f89fd6..473767a73 100644 --- a/intentintegrator/build.gradle +++ b/intentintegrator/build.gradle @@ -1,3 +1,6 @@ +plugins { + alias(libs.plugins.kotlin.android) +} apply plugin: "com.android.library" android { @@ -21,8 +24,12 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + kotlinOptions { + jvmTarget = '17' + } } dependencies { implementation(libs.androidx.appcompat) + implementation libs.androidx.core.ktx } \ No newline at end of file diff --git a/orbotservice/build.gradle b/orbotservice/build.gradle index 621e4c88e..eb4c02902 100644 --- a/orbotservice/build.gradle +++ b/orbotservice/build.gradle @@ -1,3 +1,6 @@ +plugins { + alias(libs.plugins.kotlin.android) +} apply plugin: "com.android.library" android { @@ -36,6 +39,9 @@ android { textReport false xmlReport false } + kotlinOptions { + jvmTarget = '17' + } } dependencies { @@ -54,4 +60,5 @@ dependencies { implementation(libs.pcap.factory) // implementation(files("../libs/geoip.jar")) + implementation libs.androidx.core.ktx } diff --git a/orbotservice/src/main/java/org/torproject/android/service/OrbotService.java b/orbotservice/src/main/java/org/torproject/android/service/OrbotService.java index 5e89dc669..0c9f965f7 100644 --- a/orbotservice/src/main/java/org/torproject/android/service/OrbotService.java +++ b/orbotservice/src/main/java/org/torproject/android/service/OrbotService.java @@ -39,6 +39,7 @@ import net.freehaven.tor.control.TorControlCommands; import net.freehaven.tor.control.TorControlConnection; +import org.torproject.android.service.util.Bridge; import org.torproject.android.service.util.CustomTorResourceInstaller; import org.torproject.android.service.util.PowerConnectionReceiver; import org.torproject.android.service.util.Prefs; @@ -137,16 +138,6 @@ public void stopped(String s, Exception e) { return mIptProxy; } - /** - * @param bridgeList bridges that were manually entered into Orbot settings - * @return Array with each bridge as an element, no whitespace entries see issue #289... - */ - private static String[] parseBridgesFromSettings(String bridgeList) { - // this regex replaces lines that only contain whitespace with an empty String - bridgeList = bridgeList.trim().replaceAll("(?m)^[ \t]*\r?\n", ""); - return bridgeList.split("\\n"); - } - public void debug(String msg) { Log.d(TAG, msg); @@ -299,12 +290,12 @@ private void stopTorAsync(boolean showNotification) { // todo this needs to handle a lot of different cases that haven't been defined yet // todo particularly this is true for the smart connection case... if (connectionPathway.startsWith(Prefs.PATHWAY_SNOWFLAKE) || Prefs.getPrefSmartTrySnowflake()) { - // mIptProxy.stop mIptProxy.stop(IPtProxy.Snowflake); - } else if (connectionPathway.equals(Prefs.PATHWAY_CUSTOM) || Prefs.getPrefSmartTryObfs4() != null) { - // IPtProxy.stopLyrebird(); + } + else if (connectionPathway.equals(Prefs.PATHWAY_CUSTOM) || Prefs.getPrefSmartTryObfs4() != null) { + mIptProxy.stop(IPtProxy.MeekLite); mIptProxy.stop(IPtProxy.Obfs4); - + mIptProxy.stop(IPtProxy.Webtunnel); } stopTor(); @@ -1204,7 +1195,7 @@ private StringBuffer processSettingsImpl(StringBuffer extraLines) throws IOExcep if (pathway.startsWith(Prefs.PATHWAY_SNOWFLAKE) || Prefs.getPrefSmartTrySnowflake()) { extraLines = processSettingsImplSnowflake(extraLines); } else if (pathway.equals(Prefs.PATHWAY_CUSTOM) || Prefs.getPrefSmartTryObfs4() != null) { - extraLines = processSettingsImplObfs4(extraLines); + extraLines = processSettingsLyrebird(extraLines); } } var fileGeoIP = new File(appBinHome, GEOIP_ASSET_KEY); @@ -1274,20 +1265,32 @@ private StringBuffer processSettingsImplSnowflake(StringBuffer extraLines) { return extraLines; } - private StringBuffer processSettingsImplObfs4(StringBuffer extraLines) { - Log.d(TAG, "in obfs4 torrc config"); - extraLines.append("ClientTransportPlugin obfs4 socks5 127.0.0.1:" + mIptProxy.port(IPtProxy.Obfs4)).append('\n'); + private StringBuffer processSettingsLyrebird(StringBuffer extraLines) { + Log.d(TAG, "in Lyrebird torrc config"); + + var customBridges = getCustomBridges(); - var bridgeList = ""; - if (Prefs.getConnectionPathway().equals(Prefs.PATHWAY_CUSTOM)) { - bridgeList = Prefs.getBridgesList(); - } else bridgeList = Prefs.getPrefSmartTryObfs4(); - var customBridges = parseBridgesFromSettings(bridgeList); - for (var b : customBridges) + for (String transport : Bridge.getTransports(customBridges)) { + extraLines + .append(String.format(Locale.US, "ClientTransportPlugin %s socks5 127.0.0.1:%d", + transport, mIptProxy.port(transport))) + .append('\n'); + } + + for (var b : customBridges) { extraLines.append("Bridge ").append(b).append("\n"); + } + return extraLines; } + private List getCustomBridges() { + return Bridge.parseBridges( + Prefs.getConnectionPathway().equals(Prefs.PATHWAY_CUSTOM) + ? Prefs.getBridgesList() + : Prefs.getPrefSmartTryObfs4()); + } + private StringBuffer processSettingsImplDirectPathway(StringBuffer extraLines) { var prefs = Prefs.getSharedPrefs(getApplicationContext()); extraLines.append("UseBridges 0").append('\n'); @@ -1528,11 +1531,12 @@ public void run() { } else if (connectionPathway.equals(Prefs.PATHWAY_SNOWFLAKE_AMP)) { startSnowflakeClientAmpRendezvous(); } else if (connectionPathway.equals(Prefs.PATHWAY_CUSTOM) || Prefs.getPrefSmartTryObfs4() != null) { - //IPtProxy.startLyrebird("DEBUG", false, false, null); - try { - mIptProxy.start(IPtProxy.Obfs4,""); - } catch (Exception e) { - throw new RuntimeException(e); + for (var transport : Bridge.getTransports(getCustomBridges())) { + try { + mIptProxy.start(transport, ""); + } catch (Exception e) { + throw new RuntimeException(e); + } } } startTor(); diff --git a/orbotservice/src/main/java/org/torproject/android/service/util/Bridge.kt b/orbotservice/src/main/java/org/torproject/android/service/util/Bridge.kt new file mode 100644 index 000000000..a8a530923 --- /dev/null +++ b/orbotservice/src/main/java/org/torproject/android/service/util/Bridge.kt @@ -0,0 +1,85 @@ +package org.torproject.android.service.util + +/** + * Parser for bridge lines. + */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +class Bridge(var raw: String) { + + val rawPieces + get() = raw.split(" ") + + val transport + get() = rawPieces.firstOrNull() + + val address + get() = rawPieces.getOrNull(1) + + val ip + get() = address?.split(":")?.firstOrNull() + + val port + get() = address?.split(":")?.lastOrNull()?.toInt() + + val fingerprint1 + get() = rawPieces.getOrNull(2) + + val fingerprint2 + get() = rawPieces.firstOrNull { it.startsWith("fingerprint=") } + ?.split("=")?.lastOrNull() + + val url + get() = rawPieces.firstOrNull { it.startsWith("url=") } + ?.split("=")?.lastOrNull() + + val front + get() = rawPieces.firstOrNull { it.startsWith("front=") } + ?.split("=")?.lastOrNull() + + val fronts + get() = rawPieces.firstOrNull { it.startsWith("fronts=") } + ?.split("=")?.lastOrNull()?.split(",")?.filter { it.isNotEmpty() } + + val cert + get() = rawPieces.firstOrNull { it.startsWith("cert=") } + ?.split("=")?.lastOrNull() + + val iatMode + get() = rawPieces.firstOrNull { it.startsWith("iat-mode=") } + ?.split("=")?.lastOrNull() + + val ice + get() = rawPieces.firstOrNull { it.startsWith("ice=") } + ?.split("=")?.lastOrNull() + + val utlsImitate + get() = rawPieces.firstOrNull { it.startsWith("utls-imitate=") } + ?.split("=")?.lastOrNull() + + val ver + get() = rawPieces.firstOrNull { it.startsWith("ver=") } + ?.split("=")?.lastOrNull() + + + override fun toString(): String { + return raw + } + + companion object { + + @JvmStatic + fun parseBridges(bridges: String): List { + return bridges + .split("\n") + .mapNotNull { + val b = it.trim() + if (b.isNotEmpty()) Bridge(b) else null + } + } + + @JvmStatic + fun getTransports(bridges: List): Set { + return bridges.mapNotNull { it.transport }.toSet() + } + } +} \ No newline at end of file