feat(android): Add support for per-app VPN configurable through MDM (#3657)

Refs #3613
This commit is contained in:
Jamil
2024-02-17 09:50:33 -08:00
committed by GitHub
parent 87f843dcfb
commit 91681fb15d
6 changed files with 53 additions and 25 deletions

View File

@@ -3,7 +3,6 @@ package dev.firezone.android.core.data
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import dev.firezone.android.BuildConfig
import dev.firezone.android.core.data.model.Config
import kotlinx.coroutines.CoroutineDispatcher
@@ -19,7 +18,6 @@ internal class RepositoryImpl
private val context: Context,
private val coroutineDispatcher: CoroutineDispatcher,
private val sharedPreferences: SharedPreferences,
private val appRestrictions: Bundle,
) : Repository {
override fun getConfigSync(): Config {
return Config(
@@ -69,15 +67,10 @@ internal class RepositoryImpl
override fun getToken(): Flow<String?> =
flow {
emit(
appRestrictions.getString(TOKEN_KEY, null)
?: sharedPreferences.getString(TOKEN_KEY, null),
)
emit(sharedPreferences.getString(TOKEN_KEY, null))
}.flowOn(coroutineDispatcher)
override fun getTokenSync(): String? =
appRestrictions.getString(TOKEN_KEY, null)
?: sharedPreferences.getString(TOKEN_KEY, null)
override fun getTokenSync(): String? = sharedPreferences.getString(TOKEN_KEY, null)
override fun getStateSync(): String? = sharedPreferences.getString(STATE_KEY, null)

View File

@@ -3,6 +3,7 @@ package dev.firezone.android.core.di
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import androidx.core.content.getSystemService
import dagger.Module
import dagger.Provides
@@ -16,6 +17,11 @@ import kotlinx.coroutines.CoroutineDispatcher
@Module
@InstallIn(SingletonComponent::class)
class DataModule {
@Provides
internal fun provideApplicationRestrictions(
@ApplicationContext context: Context,
): Bundle = (context.getSystemService(Context.RESTRICTIONS_SERVICE) as android.content.RestrictionsManager).applicationRestrictions
@Provides
internal fun provideRepository(
@ApplicationContext context: Context,
@@ -26,9 +32,5 @@ class DataModule {
context,
coroutineDispatcher,
sharedPreferences,
(
context.getSystemService(Context.RESTRICTIONS_SERVICE)
as android.content.RestrictionsManager
).applicationRestrictions,
)
}

View File

@@ -2,6 +2,7 @@
package dev.firezone.android.features.splash.ui
import android.content.Context
import android.os.Bundle
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@@ -10,7 +11,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dev.firezone.android.core.data.Repository
import dev.firezone.android.tunnel.TunnelService
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -21,6 +21,7 @@ internal class SplashViewModel
@Inject
constructor(
private val repo: Repository,
private val applicationRestrictions: Bundle,
) : ViewModel() {
private val actionMutableLiveData = MutableLiveData<ViewAction>()
val actionLiveData: LiveData<ViewAction> = actionMutableLiveData
@@ -32,15 +33,14 @@ internal class SplashViewModel
if (!hasVpnPermissions(context)) {
actionMutableLiveData.postValue(ViewAction.NavigateToVpnPermission)
} else {
repo.getToken().collect {
if (it.isNullOrBlank()) {
actionMutableLiveData.postValue(ViewAction.NavigateToSignIn)
} else {
// token will be re-read by the TunnelService
if (!TunnelService.isRunning(context)) TunnelService.start(context)
val token = applicationRestrictions.getString("token") ?: repo.getTokenSync()
if (token.isNullOrBlank()) {
actionMutableLiveData.postValue(ViewAction.NavigateToSignIn)
} else {
// token will be re-read by the TunnelService
if (!TunnelService.isRunning(context)) TunnelService.start(context)
actionMutableLiveData.postValue(ViewAction.NavigateToSession)
}
actionMutableLiveData.postValue(ViewAction.NavigateToSession)
}
}
}

View File

@@ -11,6 +11,7 @@ import android.content.Intent
import android.net.VpnService
import android.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
@@ -38,6 +39,9 @@ class TunnelService : VpnService() {
@Inject
internal lateinit var repo: Repository
@Inject
internal lateinit var appRestrictions: Bundle
@Inject
internal lateinit var moshi: Moshi
@@ -168,7 +172,7 @@ class TunnelService : VpnService() {
Log.d(TAG, "onDisconnect: $error")
Firebase.crashlytics.log("onDisconnect: $error")
// This is a no-op if the token is being read from MDM
// Clear any user tokens and actorNames
repo.clearToken()
repo.clearActorName()
@@ -214,7 +218,7 @@ class TunnelService : VpnService() {
}
private fun connect() {
val token = repo.getTokenSync()
val token = appRestrictions.getString("token") ?: repo.getTokenSync()
val config = repo.getConfigSync()
if (!token.isNullOrBlank()) {
@@ -321,6 +325,20 @@ class TunnelService : VpnService() {
Firebase.crashlytics.log("IPv6 Address: $tunnelIpv6Address")
addAddress(tunnelIpv6Address!!, 128)
appRestrictions.getString("allowedApplications")?.let {
Firebase.crashlytics.log("Allowed applications: $it")
it.split(",").forEach { p ->
addAllowedApplication(p.trim())
}
}
appRestrictions.getString("disallowedApplications")?.let {
Firebase.crashlytics.log("Disallowed applications: $it")
it.split(",").forEach { p ->
addDisallowedApplication(p.trim())
}
}
setSession(SESSION_NAME)
setMtu(MTU)
}.establish()!!.let {

View File

@@ -39,5 +39,9 @@
<!-- Managed Configuration -->
<string name="config_token_title">Token</string>
<string name="config_token_description">The token used for authentication. Set this to a service account token to achieve headless operation.</string>
<string name="config_token_description">The token used for authentication. Set this to a service account token to enable headless operation.</string>
<string name="config_allowed_applications_title">Allowed Applications</string>
<string name="config_allowed_applications_description">A comma-separated list of application package IDs that are allowed to use the Firezone tunnel. If this list is empty, all applications are allowed.</string>
<string name="config_disallowed_applications_title">Disallowed Applications</string>
<string name="config_disallowed_applications_description">A comma-separated list of application package IDs that are disallowed to use the Firezone tunnel and will be routed outside of it. If this list is empty, no applications are disallowed.</string>
</resources>

View File

@@ -7,4 +7,15 @@
android:restrictionType="string"
android:title="@string/config_token_title" />
<restriction
android:description="@string/config_allowed_applications_description"
android:key="allowedApplications"
android:restrictionType="string"
android:title="@string/config_allowed_applications_title" />
<restriction
android:description="@string/config_disallowed_applications_description"
android:key="disallowedApplications"
android:restrictionType="string"
android:title="@string/config_disallowed_applications_title" />
</restrictions>