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(),
dns = Optional.of(SignalServiceNetworkAccess.DNS),
signalProxy = Optional.empty(),
systemHttpProxy = Optional.empty(),
zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS),
genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_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.concurrent.DeadlockDetector
import org.signal.core.util.concurrent.LatestValueObservable
import org.signal.core.util.orNull
import org.signal.core.util.resettableLazy
import org.signal.libsignal.net.Network
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.database.DatabaseObserver
import org.thoughtcrime.securesms.database.PendingRetryReceiptCache
import org.thoughtcrime.securesms.dependencies.AppDependencies.authWebSocket
import org.thoughtcrime.securesms.groups.GroupsV2Authorization
import org.thoughtcrime.securesms.jobmanager.JobManager
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository
@@ -378,6 +380,16 @@ object AppDependencies {
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 {
fun providePushServiceSocket(signalServiceConfiguration: SignalServiceConfiguration, groupsV2Operations: GroupsV2Operations): PushServiceSocket
fun provideGroupsV2Operations(signalServiceConfiguration: SignalServiceConfiguration): GroupsV2Operations

View File

@@ -4,6 +4,7 @@ import android.app.Application
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.ProxyInfo
import android.os.IBinder
import androidx.annotation.VisibleForTesting
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 connectionNecessarySemaphore = Semaphore(0)
private val networkConnectionListener = NetworkConnectionListener(context) { isNetworkUnavailable ->
lock.withLock {
AppDependencies.libsignalNetwork.onNetworkChange()
if (isNetworkUnavailable()) {
Log.w(TAG, "Lost network connection. Resetting the drained state.")
decryptionDrained = false
private var previousProxyInfo: ProxyInfo? = null
private val networkConnectionListener = NetworkConnectionListener(
context,
{ isNetworkUnavailable ->
lock.withLock {
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)

View File

@@ -10,7 +10,9 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.ProxyInfo
import android.os.Build
import org.signal.core.util.logging.Log
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.
* 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 {
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()")
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() {

View File

@@ -1,6 +1,9 @@
package org.thoughtcrime.securesms.push
import android.content.Context
import android.net.ConnectivityManager
import android.os.Build
import androidx.core.content.ContextCompat
import com.google.i18n.phonenumbers.PhoneNumberUtil
import okhttp3.CipherSuite
import okhttp3.ConnectionSpec
@@ -20,6 +23,7 @@ import org.thoughtcrime.securesms.net.SequentialDns
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor
import org.thoughtcrime.securesms.net.StaticDns
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.SignalCdsiUrl
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 java.io.IOException
import java.util.Optional
import android.net.Proxy as AndroidProxy
/**
* Provides a [SignalServiceConfiguration] to be used with our service layer.
@@ -133,6 +138,28 @@ class SignalServiceNetworkAccess(context: Context) {
.build()
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)
@@ -187,6 +214,7 @@ class SignalServiceNetworkAccess(context: Context) {
networkInterceptors = interceptors,
dns = Optional.of(DNS),
signalProxy = Optional.empty(),
systemHttpProxy = Optional.empty(),
zkGroupServerPublicParams = zkGroupServerPublicParams,
genericServerPublicParams = genericServerPublicParams,
backupServerPublicParams = backupServerPublicParams,
@@ -246,6 +274,7 @@ class SignalServiceNetworkAccess(context: Context) {
networkInterceptors = interceptors,
dns = Optional.of(DNS),
signalProxy = if (SignalStore.proxy.isProxyEnabled) Optional.ofNullable(SignalStore.proxy.proxy) else Optional.empty(),
systemHttpProxy = Optional.ofNullable(getSystemHttpProxy(context)),
zkGroupServerPublicParams = zkGroupServerPublicParams,
genericServerPublicParams = genericServerPublicParams,
backupServerPublicParams = backupServerPublicParams,
@@ -316,6 +345,7 @@ class SignalServiceNetworkAccess(context: Context) {
networkInterceptors = interceptors,
dns = Optional.of(DNS),
signalProxy = Optional.empty(),
systemHttpProxy = Optional.empty(),
zkGroupServerPublicParams = zkGroupServerPublicParams,
genericServerPublicParams = genericServerPublicParams,
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 dns: Optional<Dns>,
val signalProxy: Optional<SignalProxy>,
val systemHttpProxy: Optional<HttpProxy>,
val zkGroupServerPublicParams: ByteArray,
val genericServerPublicParams: 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.
*/
fun Network.applyConfiguration(config: SignalServiceConfiguration) {
val proxy = config.signalProxy.orNull()
val signalProxy = config.signalProxy.orNull()
val systemHttpProxy = config.systemHttpProxy.orNull()
if (proxy == null) {
this.clearProxy()
} else {
try {
this.setProxy(proxy.host, proxy.port)
} catch (e: IOException) {
Log.e(TAG, "Invalid proxy configuration set! Failing connections until changed.")
this.setInvalidProxy()
when {
(signalProxy != null) -> {
try {
this.setProxy(signalProxy.host, signalProxy.port)
} catch (e: IOException) {
Log.e(TAG, "Invalid proxy configuration set! Failing connections until changed.")
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...")
}
}
}