Add system HTTP proxy support to libsignal-net.

Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
andrew-signal
2025-04-11 18:58:07 -05:00
committed by Cody Henthorne
parent 8e880fe117
commit 74c6e76808
8 changed files with 101 additions and 18 deletions

View File

@@ -97,6 +97,7 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
networkInterceptors = emptyList(), networkInterceptors = emptyList(),
dns = Optional.of(SignalServiceNetworkAccess.DNS), dns = Optional.of(SignalServiceNetworkAccess.DNS),
signalProxy = Optional.empty(), signalProxy = Optional.empty(),
systemHttpProxy = Optional.empty(),
zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS), zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS),
genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS), genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS),
backupServerPublicParams = Base64.decode(BuildConfig.BACKUP_SERVER_PUBLIC_PARAMS), backupServerPublicParams = Base64.decode(BuildConfig.BACKUP_SERVER_PUBLIC_PARAMS),

View File

@@ -7,6 +7,7 @@ import okhttp3.OkHttpClient
import org.signal.core.util.billing.BillingApi import org.signal.core.util.billing.BillingApi
import org.signal.core.util.concurrent.DeadlockDetector import org.signal.core.util.concurrent.DeadlockDetector
import org.signal.core.util.concurrent.LatestValueObservable import org.signal.core.util.concurrent.LatestValueObservable
import org.signal.core.util.orNull
import org.signal.core.util.resettableLazy import org.signal.core.util.resettableLazy
import org.signal.libsignal.net.Network import org.signal.libsignal.net.Network
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
@@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.components.TypingStatusSender
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl
import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.PendingRetryReceiptCache import org.thoughtcrime.securesms.database.PendingRetryReceiptCache
import org.thoughtcrime.securesms.dependencies.AppDependencies.authWebSocket
import org.thoughtcrime.securesms.groups.GroupsV2Authorization import org.thoughtcrime.securesms.groups.GroupsV2Authorization
import org.thoughtcrime.securesms.jobmanager.JobManager import org.thoughtcrime.securesms.jobmanager.JobManager
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository import org.thoughtcrime.securesms.megaphone.MegaphoneRepository
@@ -378,6 +380,16 @@ object AppDependencies {
networkModule.openConnections() networkModule.openConnections()
} }
fun onSystemHttpProxyChange(host: String?, port: Int?): Boolean {
val currentSystemProxy = signalServiceNetworkAccess.getConfiguration().systemHttpProxy.orNull()
return if (currentSystemProxy?.host != host || currentSystemProxy?.port != port) {
resetNetwork()
true
} else {
false
}
}
interface Provider { interface Provider {
fun providePushServiceSocket(signalServiceConfiguration: SignalServiceConfiguration, groupsV2Operations: GroupsV2Operations): PushServiceSocket fun providePushServiceSocket(signalServiceConfiguration: SignalServiceConfiguration, groupsV2Operations: GroupsV2Operations): PushServiceSocket
fun provideGroupsV2Operations(signalServiceConfiguration: SignalServiceConfiguration): GroupsV2Operations fun provideGroupsV2Operations(signalServiceConfiguration: SignalServiceConfiguration): GroupsV2Operations

View File

@@ -4,6 +4,7 @@ import android.app.Application
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.ProxyInfo
import android.os.IBinder import android.os.IBinder
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
@@ -86,16 +87,29 @@ class IncomingMessageObserver(private val context: Application, private val auth
private val lock: ReentrantLock = ReentrantLock() private val lock: ReentrantLock = ReentrantLock()
private val connectionNecessarySemaphore = Semaphore(0) private val connectionNecessarySemaphore = Semaphore(0)
private val networkConnectionListener = NetworkConnectionListener(context) { isNetworkUnavailable -> private var previousProxyInfo: ProxyInfo? = null
lock.withLock { private val networkConnectionListener = NetworkConnectionListener(
AppDependencies.libsignalNetwork.onNetworkChange() context,
if (isNetworkUnavailable()) { { isNetworkUnavailable ->
Log.w(TAG, "Lost network connection. Resetting the drained state.") lock.withLock {
decryptionDrained = false AppDependencies.libsignalNetwork.onNetworkChange()
if (isNetworkUnavailable()) {
Log.w(TAG, "Lost network connection. Resetting the drained state.")
decryptionDrained = false
}
connectionNecessarySemaphore.release()
} }
connectionNecessarySemaphore.release() },
{ proxyInfo ->
if (proxyInfo != previousProxyInfo) {
val networkReset = AppDependencies.onSystemHttpProxyChange(proxyInfo?.host, proxyInfo?.port)
if (networkReset) {
Log.i(TAG, "System proxy configuration changed, network reset.")
}
}
previousProxyInfo = proxyInfo
} }
} )
private val messageContentProcessor = MessageContentProcessor(context) private val messageContentProcessor = MessageContentProcessor(context)

View File

@@ -10,7 +10,9 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network import android.net.Network
import android.net.ProxyInfo
import android.os.Build import android.os.Build
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
@@ -24,7 +26,7 @@ import org.thoughtcrime.securesms.util.ServiceUtil
* API 28+ only runs on lost networks, so it provides a conditional that's always true because that is guaranteed by the call site. * API 28+ only runs on lost networks, so it provides a conditional that's always true because that is guaranteed by the call site.
* Earlier versions use [NetworkConstraint.isMet] to query the current network state upon receiving the broadcast. * Earlier versions use [NetworkConstraint.isMet] to query the current network state upon receiving the broadcast.
*/ */
class NetworkConnectionListener(private val context: Context, private val onNetworkLost: (() -> Boolean) -> Unit) { class NetworkConnectionListener(private val context: Context, private val onNetworkLost: (() -> Boolean) -> Unit, private val onProxySettingsChanged: ((ProxyInfo?) -> Unit)) {
companion object { companion object {
private val TAG = Log.tag(NetworkConnectionListener::class.java) private val TAG = Log.tag(NetworkConnectionListener::class.java)
} }
@@ -55,6 +57,12 @@ class NetworkConnectionListener(private val context: Context, private val onNetw
Log.d(TAG, "ConnectivityManager.NetworkCallback onLost()") Log.d(TAG, "ConnectivityManager.NetworkCallback onLost()")
onNetworkLost { true } onNetworkLost { true }
} }
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
super.onLinkPropertiesChanged(network, linkProperties)
Log.d(TAG, "ConnectivityManager.NetworkCallback onLinkPropertiesChanged()")
onProxySettingsChanged(linkProperties.httpProxy)
}
} }
private val connectionReceiver = object : BroadcastReceiver() { private val connectionReceiver = object : BroadcastReceiver() {

View File

@@ -1,6 +1,9 @@
package org.thoughtcrime.securesms.push package org.thoughtcrime.securesms.push
import android.content.Context import android.content.Context
import android.net.ConnectivityManager
import android.os.Build
import androidx.core.content.ContextCompat
import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.PhoneNumberUtil
import okhttp3.CipherSuite import okhttp3.CipherSuite
import okhttp3.ConnectionSpec import okhttp3.ConnectionSpec
@@ -20,6 +23,7 @@ import org.thoughtcrime.securesms.net.SequentialDns
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor
import org.thoughtcrime.securesms.net.StaticDns import org.thoughtcrime.securesms.net.StaticDns
import org.whispersystems.signalservice.api.push.TrustStore import org.whispersystems.signalservice.api.push.TrustStore
import org.whispersystems.signalservice.internal.configuration.HttpProxy
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
@@ -28,6 +32,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url
import java.io.IOException import java.io.IOException
import java.util.Optional import java.util.Optional
import android.net.Proxy as AndroidProxy
/** /**
* Provides a [SignalServiceConfiguration] to be used with our service layer. * Provides a [SignalServiceConfiguration] to be used with our service layer.
@@ -133,6 +138,28 @@ class SignalServiceNetworkAccess(context: Context) {
.build() .build()
private val APP_CONNECTION_SPEC = ConnectionSpec.MODERN_TLS private val APP_CONNECTION_SPEC = ConnectionSpec.MODERN_TLS
@Suppress("DEPRECATION")
private fun getSystemHttpProxy(context: Context): HttpProxy? {
return if (Build.VERSION.SDK_INT >= 23) {
val connectivityManager = ContextCompat.getSystemService(context, ConnectivityManager::class.java) ?: return null
connectivityManager
.activeNetwork
?.let { connectivityManager.getLinkProperties(it)?.httpProxy }
?.takeIf { !it.exclusionList.contains(BuildConfig.SIGNAL_URL.stripProtocol()) }
?.let { proxy -> HttpProxy(proxy.host, proxy.port) }
} else {
val host: String? = AndroidProxy.getHost(context)
val port: Int = AndroidProxy.getPort(context)
if (host != null) {
HttpProxy(host, port)
} else {
null
}
}
}
} }
private val serviceTrustStore: TrustStore = SignalServiceTrustStore(context) private val serviceTrustStore: TrustStore = SignalServiceTrustStore(context)
@@ -187,6 +214,7 @@ class SignalServiceNetworkAccess(context: Context) {
networkInterceptors = interceptors, networkInterceptors = interceptors,
dns = Optional.of(DNS), dns = Optional.of(DNS),
signalProxy = Optional.empty(), signalProxy = Optional.empty(),
systemHttpProxy = Optional.empty(),
zkGroupServerPublicParams = zkGroupServerPublicParams, zkGroupServerPublicParams = zkGroupServerPublicParams,
genericServerPublicParams = genericServerPublicParams, genericServerPublicParams = genericServerPublicParams,
backupServerPublicParams = backupServerPublicParams, backupServerPublicParams = backupServerPublicParams,
@@ -246,6 +274,7 @@ class SignalServiceNetworkAccess(context: Context) {
networkInterceptors = interceptors, networkInterceptors = interceptors,
dns = Optional.of(DNS), dns = Optional.of(DNS),
signalProxy = if (SignalStore.proxy.isProxyEnabled) Optional.ofNullable(SignalStore.proxy.proxy) else Optional.empty(), signalProxy = if (SignalStore.proxy.isProxyEnabled) Optional.ofNullable(SignalStore.proxy.proxy) else Optional.empty(),
systemHttpProxy = Optional.ofNullable(getSystemHttpProxy(context)),
zkGroupServerPublicParams = zkGroupServerPublicParams, zkGroupServerPublicParams = zkGroupServerPublicParams,
genericServerPublicParams = genericServerPublicParams, genericServerPublicParams = genericServerPublicParams,
backupServerPublicParams = backupServerPublicParams, backupServerPublicParams = backupServerPublicParams,
@@ -316,6 +345,7 @@ class SignalServiceNetworkAccess(context: Context) {
networkInterceptors = interceptors, networkInterceptors = interceptors,
dns = Optional.of(DNS), dns = Optional.of(DNS),
signalProxy = Optional.empty(), signalProxy = Optional.empty(),
systemHttpProxy = Optional.empty(),
zkGroupServerPublicParams = zkGroupServerPublicParams, zkGroupServerPublicParams = zkGroupServerPublicParams,
genericServerPublicParams = genericServerPublicParams, genericServerPublicParams = genericServerPublicParams,
backupServerPublicParams = backupServerPublicParams, backupServerPublicParams = backupServerPublicParams,

View File

@@ -0,0 +1,6 @@
package org.whispersystems.signalservice.internal.configuration
/**
* HTTP Proxy configuration from Android OS configuration.
*/
class HttpProxy(val host: String, val port: Int)

View File

@@ -17,6 +17,7 @@ data class SignalServiceConfiguration(
val networkInterceptors: List<Interceptor>, val networkInterceptors: List<Interceptor>,
val dns: Optional<Dns>, val dns: Optional<Dns>,
val signalProxy: Optional<SignalProxy>, val signalProxy: Optional<SignalProxy>,
val systemHttpProxy: Optional<HttpProxy>,
val zkGroupServerPublicParams: ByteArray, val zkGroupServerPublicParams: ByteArray,
val genericServerPublicParams: ByteArray, val genericServerPublicParams: ByteArray,
val backupServerPublicParams: ByteArray, val backupServerPublicParams: ByteArray,

View File

@@ -30,16 +30,27 @@ fun Network.transformAndSetRemoteConfig(remoteConfig: Map<String, Any>) {
* Helper method to apply settings from the SignalServiceConfiguration. * Helper method to apply settings from the SignalServiceConfiguration.
*/ */
fun Network.applyConfiguration(config: SignalServiceConfiguration) { fun Network.applyConfiguration(config: SignalServiceConfiguration) {
val proxy = config.signalProxy.orNull() val signalProxy = config.signalProxy.orNull()
val systemHttpProxy = config.systemHttpProxy.orNull()
if (proxy == null) { when {
this.clearProxy() (signalProxy != null) -> {
} else { try {
try { this.setProxy(signalProxy.host, signalProxy.port)
this.setProxy(proxy.host, proxy.port) } catch (e: IOException) {
} catch (e: IOException) { Log.e(TAG, "Invalid proxy configuration set! Failing connections until changed.")
Log.e(TAG, "Invalid proxy configuration set! Failing connections until changed.") this.setInvalidProxy()
this.setInvalidProxy() }
}
(systemHttpProxy != null) -> {
try {
this.setProxy("http", systemHttpProxy.host, systemHttpProxy.port, "", "")
} catch (e: IOException) {
// The Android settings screen where this is set explicitly calls out that apps are allowed to
// ignore the HTTP Proxy setting, so if using the specified proxy would cause us to break, let's
// try just ignoring it and seeing if that still lets us connect.
Log.w(TAG, "Failed to set system HTTP proxy, ignoring and continuing...")
}
} }
} }