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()
|
||||
|
||||
/**
|
||||
* 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 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)
|
||||
}
|
||||
|
||||
@@ -354,6 +371,12 @@ class Amber :
|
||||
}
|
||||
|
||||
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()
|
||||
Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope))
|
||||
|
||||
@@ -654,7 +677,11 @@ class Amber :
|
||||
.build()
|
||||
val coilCallFactory = okhttp3.Call.Factory { request ->
|
||||
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)
|
||||
}
|
||||
return ImageLoader.Builder(context)
|
||||
|
||||
@@ -244,6 +244,7 @@ object LocalPreferences {
|
||||
accountCache.clear()
|
||||
warmAccountCache(context)
|
||||
context.settings = loadSettingsFromEncryptedStorage(context)
|
||||
context.isSettingsLoaded = true
|
||||
context.settings.language?.let {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
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 loadSettingsFromEncryptedStorage(context: Context = Amber.instance): AmberSettings {
|
||||
|
||||
Reference in New Issue
Block a user