mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Close clearnet leak for Tor users during the startup settings race
Profile fetches and boot-time network callbacks could dial relays directly before the async settings load finished: Amber.settings starts as the data-class default (torMode = DISABLED), so the relay and Coil factories read 'Tor off' and picked the direct OkHttp client. The fail-closed SOCKS placeholder never got consulted. Seen in adb logs: kind-0 profile REQs went out over clearnet TLS a full ten seconds before the built-in Tor SOCKS port existed. Fix, at the factory decision point: - Preset the proxy synchronously from plain prefs in Application.onCreate: ORBOT gets the configured port, BUILTIN the fail-closed placeholder (SOCKS 127.0.0.1:<dynamic>, refused until Tor binds), DISABLED stays direct. - Add Amber.isSettingsLoaded, set when reloadApp() assigns the loaded settings. Until then both factories route through the proxy client instead of trusting the DISABLED default, so early dials (network callbacks, NotificationSubscription / ProfileSubscription / BunkerRequestUtils) hit the placeholder and retry rather than leak. Verified on device: cold start with BUILTIN Tor shows pre-Tor dials refused and every profile REQ flowing after the SOCKS port comes up.
This commit is contained in:
@@ -117,8 +117,25 @@ class Amber :
|
|||||||
|
|
||||||
var settings: AmberSettings = AmberSettings()
|
var settings: AmberSettings = AmberSettings()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* False until the first async settings load ([LocalPreferences.reloadApp]) lands. Until
|
||||||
|
* then [settings] is the data-class default, whose torMode (DISABLED) is a guess, not a
|
||||||
|
* decision — any dial in that window must route through the proxy client (preset from
|
||||||
|
* plain prefs in [onCreate]) instead of trusting the default and going to the clearnet.
|
||||||
|
*/
|
||||||
|
@Volatile var isSettingsLoaded = false
|
||||||
|
|
||||||
val factory = OkHttpWebSocket.Builder { url ->
|
val factory = OkHttpWebSocket.Builder { url ->
|
||||||
val useProxy = if (isPrivateIp(url.url)) false else settings.torMode != TorMode.DISABLED
|
val useProxy = when {
|
||||||
|
isPrivateIp(url.url) -> false
|
||||||
|
// Settings load asynchronously; until they land, torMode's default (DISABLED)
|
||||||
|
// is a guess, not a decision. Route through the proxy client — preset from
|
||||||
|
// plain prefs in onCreate — so a BUILTIN/ORBOT user's pre-Tor dials (network
|
||||||
|
// callbacks, profile fetches) hit the fail-closed placeholder instead of the
|
||||||
|
// clearnet.
|
||||||
|
!isSettingsLoaded -> true
|
||||||
|
else -> settings.torMode != TorMode.DISABLED
|
||||||
|
}
|
||||||
HttpClientManager.getHttpClient(useProxy)
|
HttpClientManager.getHttpClient(useProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,6 +371,12 @@ class Amber :
|
|||||||
}
|
}
|
||||||
|
|
||||||
instance = this
|
instance = this
|
||||||
|
// Before anything can dial: route the proxy client through the user's SOCKS
|
||||||
|
// setup (or the fail-closed placeholder for BUILTIN) read synchronously from
|
||||||
|
// plain prefs. Dials in the window before the async settings load — network
|
||||||
|
// callbacks, profile fetches — pick this up via the factory's !isSettingsLoaded
|
||||||
|
// gate; without it that window leaks clearnet traffic for Tor users.
|
||||||
|
LocalPreferences.presetProxyFromPrefs(this)
|
||||||
stats.createNotificationChannel()
|
stats.createNotificationChannel()
|
||||||
Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope))
|
Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope))
|
||||||
|
|
||||||
@@ -654,7 +677,11 @@ class Amber :
|
|||||||
.build()
|
.build()
|
||||||
val coilCallFactory = okhttp3.Call.Factory { request ->
|
val coilCallFactory = okhttp3.Call.Factory { request ->
|
||||||
val url = request.url.toString()
|
val url = request.url.toString()
|
||||||
val useProxy = if (isPrivateIp(url)) false else settings.torMode != TorMode.DISABLED
|
val useProxy = when {
|
||||||
|
isPrivateIp(url) -> false
|
||||||
|
!isSettingsLoaded -> true
|
||||||
|
else -> settings.torMode != TorMode.DISABLED
|
||||||
|
}
|
||||||
HttpClientManager.getHttpClient(useProxy).newCall(request)
|
HttpClientManager.getHttpClient(useProxy).newCall(request)
|
||||||
}
|
}
|
||||||
return ImageLoader.Builder(context)
|
return ImageLoader.Builder(context)
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ object LocalPreferences {
|
|||||||
accountCache.clear()
|
accountCache.clear()
|
||||||
warmAccountCache(context)
|
warmAccountCache(context)
|
||||||
context.settings = loadSettingsFromEncryptedStorage(context)
|
context.settings = loadSettingsFromEncryptedStorage(context)
|
||||||
|
context.isSettingsLoaded = true
|
||||||
context.settings.language?.let {
|
context.settings.language?.let {
|
||||||
AppCompatDelegate.setApplicationLocales(
|
AppCompatDelegate.setApplicationLocales(
|
||||||
LocaleListCompat.forLanguageTags(it),
|
LocaleListCompat.forLanguageTags(it),
|
||||||
@@ -251,6 +252,29 @@ object LocalPreferences {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronously presets the OkHttp proxy from plain (unencrypted) prefs, before any
|
||||||
|
* dial can happen. The full settings load is async; until it lands, the relay/Coil
|
||||||
|
* factories treat torMode as unknown and route through the proxy client — which only
|
||||||
|
* protects Tor users if that client already carries the SOCKS proxy (fail-closed
|
||||||
|
* placeholder for BUILTIN, the configured port for ORBOT).
|
||||||
|
*/
|
||||||
|
fun presetProxyFromPrefs(context: Context) {
|
||||||
|
val prefs = sharedPrefs(context)
|
||||||
|
val torMode = try {
|
||||||
|
TorMode.valueOf(prefs.getString(SettingsKeys.TOR_MODE.key, TorMode.DISABLED.name)!!)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
TorMode.DISABLED
|
||||||
|
}
|
||||||
|
when (torMode) {
|
||||||
|
TorMode.ORBOT -> HttpClientManager.setDefaultProxyOnPort(
|
||||||
|
prefs.getInt(SettingsKeys.PROXY_PORT.key, 9050),
|
||||||
|
)
|
||||||
|
TorMode.BUILTIN -> HttpClientManager.setDefaultProxyOnPort(TorManager.socksPort.value)
|
||||||
|
TorMode.DISABLED -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getStartServiceOnBoot(context: Context): Boolean = sharedPrefs(context).getBoolean(SettingsKeys.START_SERVICE_ON_BOOT.key, true)
|
fun getStartServiceOnBoot(context: Context): Boolean = sharedPrefs(context).getBoolean(SettingsKeys.START_SERVICE_ON_BOOT.key, true)
|
||||||
|
|
||||||
fun loadSettingsFromEncryptedStorage(context: Context = Amber.instance): AmberSettings {
|
fun loadSettingsFromEncryptedStorage(context: Context = Amber.instance): AmberSettings {
|
||||||
|
|||||||
Reference in New Issue
Block a user