diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/RepositoryImpl.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/RepositoryImpl.kt index b09ab0fb4..38d9ce791 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/RepositoryImpl.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/RepositoryImpl.kt @@ -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 = 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) diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/di/DataModule.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/di/DataModule.kt index e9b5eccbc..dc78177f2 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/di/DataModule.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/di/DataModule.kt @@ -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, ) } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashViewModel.kt index cbaab093f..478e9fb3c 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashViewModel.kt @@ -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() val actionLiveData: LiveData = 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) } } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt index 33f960790..4e2be6cd2 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt @@ -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 { diff --git a/kotlin/android/app/src/main/res/values/strings.xml b/kotlin/android/app/src/main/res/values/strings.xml index 9d46ab0ca..778a15ebe 100644 --- a/kotlin/android/app/src/main/res/values/strings.xml +++ b/kotlin/android/app/src/main/res/values/strings.xml @@ -39,5 +39,9 @@ Token - The token used for authentication. Set this to a service account token to achieve headless operation. + The token used for authentication. Set this to a service account token to enable headless operation. + Allowed Applications + 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. + Disallowed Applications + 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. diff --git a/kotlin/android/app/src/main/res/xml/app_restrictions.xml b/kotlin/android/app/src/main/res/xml/app_restrictions.xml index d25a45169..761527b9a 100644 --- a/kotlin/android/app/src/main/res/xml/app_restrictions.xml +++ b/kotlin/android/app/src/main/res/xml/app_restrictions.xml @@ -7,4 +7,15 @@ android:restrictionType="string" android:title="@string/config_token_title" /> + + +