diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/13.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/13.json new file mode 100644 index 0000000..85b0177 --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/13.json @@ -0,0 +1,267 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "ff209157b98a641c424f5086818ec585", + "entities": [ + { + "tableName": "Settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAutoTunnelEnabled", + "columnName": "is_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTunnelOnMobileDataEnabled", + "columnName": "is_tunnel_on_mobile_data_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trustedNetworkSSIDs", + "columnName": "trusted_network_ssids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isAlwaysOnVpnEnabled", + "columnName": "is_always_on_vpn_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTunnelOnEthernetEnabled", + "columnName": "is_tunnel_on_ethernet_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShortcutsEnabled", + "columnName": "is_shortcuts_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isTunnelOnWifiEnabled", + "columnName": "is_tunnel_on_wifi_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isKernelEnabled", + "columnName": "is_kernel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isRestoreOnBootEnabled", + "columnName": "is_restore_on_boot_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isMultiTunnelEnabled", + "columnName": "is_multi_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isPingEnabled", + "columnName": "is_ping_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isAmneziaEnabled", + "columnName": "is_amnezia_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isWildcardsEnabled", + "columnName": "is_wildcards_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isWifiNameByShellEnabled", + "columnName": "is_wifi_by_shell_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isStopOnNoInternetEnabled", + "columnName": "is_stop_on_no_internet_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isVpnKillSwitchEnabled", + "columnName": "is_vpn_kill_switch_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isKernelKillSwitchEnabled", + "columnName": "is_kernel_kill_switch_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isLanOnKillSwitchEnabled", + "columnName": "is_lan_on_kill_switch_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TunnelConfig", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wgQuick", + "columnName": "wg_quick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tunnelNetworks", + "columnName": "tunnel_networks", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isMobileDataTunnel", + "columnName": "is_mobile_data_tunnel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isPrimaryTunnel", + "columnName": "is_primary_tunnel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "amQuick", + "columnName": "am_quick", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isActive", + "columnName": "is_Active", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isPingEnabled", + "columnName": "is_ping_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "pingInterval", + "columnName": "ping_interval", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "null" + }, + { + "fieldPath": "pingCooldown", + "columnName": "ping_cooldown", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "null" + }, + { + "fieldPath": "pingIp", + "columnName": "ping_ip", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "null" + }, + { + "fieldPath": "isEthernetTunnel", + "columnName": "is_ethernet_tunnel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_TunnelConfig_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ff209157b98a641c424f5086818ec585')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt index 9a206b1..e038990 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -7,8 +7,11 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.core.os.LocaleListCompat import com.zaneschepke.logcatter.LogReader import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository +import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.IoDispatcher +import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState +import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv @@ -32,6 +35,12 @@ class WireGuardAutoTunnel : Application() { @Inject lateinit var appStateRepository: AppStateRepository + @Inject + lateinit var settingsRepository: SettingsRepository + + @Inject + lateinit var tunnelService: TunnelService + @Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher @@ -53,6 +62,10 @@ class WireGuardAutoTunnel : Application() { Timber.plant(ReleaseTree()) } applicationScope.launch { + if (!settingsRepository.getSettings().isKernelEnabled) { + tunnelService.setBackendState(BackendState.SERVICE_ACTIVE, emptyList()) + } + appStateRepository.getLocale()?.let { val locale = LocaleUtil.getLocaleFromPrefCode(it) val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(locale) @@ -69,6 +82,13 @@ class WireGuardAutoTunnel : Application() { } } + override fun onTerminate() { + applicationScope.launch { + tunnelService.setBackendState(BackendState.INACTIVE, emptyList()) + } + super.onTerminate() + } + companion object { lateinit var instance: WireGuardAutoTunnel private set diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt index 0df62b4..ca61ff4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt @@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig @Database( entities = [Settings::class, TunnelConfig::class], - version = 12, + version = 13, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -45,6 +45,10 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig from = 11, to = 12, ), + AutoMigration( + from = 12, + to = 13, + ), ], exportSchema = true, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt index 3cebf1a..fdd5ddc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt @@ -16,7 +16,7 @@ object Queries { VALUES ('false', 'false', - 'sampleSSID1,sampleSSID2', + '', 'false', 'false', 'false', diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt index 345198b..d3679a7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt @@ -65,4 +65,19 @@ data class Settings( defaultValue = "false", ) val isStopOnNoInternetEnabled: Boolean = false, + @ColumnInfo( + name = "is_vpn_kill_switch_enabled", + defaultValue = "false", + ) + val isVpnKillSwitchEnabled: Boolean = false, + @ColumnInfo( + name = "is_kernel_kill_switch_enabled", + defaultValue = "false", + ) + val isKernelKillSwitchEnabled: Boolean = false, + @ColumnInfo( + name = "is_lan_on_kill_switch_enabled", + defaultValue = "false", + ) + val isLanOnKillSwitchEnabled: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt index 77b899a..ca918ba 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt @@ -92,5 +92,14 @@ data class TunnelConfig( } const val AM_QUICK_DEFAULT = "" + + val IPV4_PUBLIC_NETWORKS = setOf( + "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", + "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", + "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", + "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", + "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", + "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4", + ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt index 1028ccc..b7a1841 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt @@ -17,10 +17,6 @@ interface AppStateRepository { suspend fun setBatteryOptimizationDisableShown(shown: Boolean) - suspend fun getCurrentSsid(): String? - - suspend fun setCurrentSsid(ssid: String) - suspend fun isTunnelStatsExpanded(): Boolean suspend fun setTunnelStatsExpanded(expanded: Boolean) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt index 075e1ed..3d0a5d3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt @@ -38,14 +38,6 @@ class DataStoreAppStateRepository( dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown) } - override suspend fun getCurrentSsid(): String? { - return dataStoreManager.getFromStore(DataStoreManager.currentSSID) - } - - override suspend fun setCurrentSsid(ssid: String) { - dataStoreManager.saveToDataStore(DataStoreManager.currentSSID, ssid) - } - override suspend fun isTunnelStatsExpanded(): Boolean { return dataStoreManager.getFromStore(DataStoreManager.tunnelStatsExpanded) ?: GeneralState.IS_TUNNEL_STATS_EXPANDED diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelService.kt index 07156c4..a9e93b7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelService.kt @@ -9,12 +9,16 @@ import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import com.wireguard.android.util.RootShell import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.module.AppShell import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager +import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelEvent +import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelState +import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState import com.zaneschepke.wireguardautotunnel.service.network.EthernetService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService @@ -23,6 +27,7 @@ import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.util.Constants +import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable @@ -32,8 +37,8 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.update @@ -63,7 +68,7 @@ class AutoTunnelService : LifecycleService() { lateinit var ethernetService: NetworkService @Inject - lateinit var appDataRepository: AppDataRepository + lateinit var appDataRepository: Provider @Inject lateinit var notificationService: NotificationService @@ -92,6 +97,7 @@ class AutoTunnelService : LifecycleService() { override fun onCreate() { super.onCreate() + serviceManager.autoTunnelService.complete(this) lifecycleScope.launch(mainImmediateDispatcher) { kotlin.runCatching { launchWatcherNotification() @@ -103,7 +109,6 @@ class AutoTunnelService : LifecycleService() { override fun onBind(intent: Intent): IBinder? { super.onBind(intent) - // We don't provide binding, so return null return null } @@ -119,9 +124,8 @@ class AutoTunnelService : LifecycleService() { launchWatcherNotification() initWakeLock() } - startSettingsJob() - startVpnStateJob() - startNetworkJobs() + startAutoTunnelJob() + startAutoTunnelStateJob() startPingStateJob() }.onFailure { Timber.e(it) @@ -129,11 +133,7 @@ class AutoTunnelService : LifecycleService() { } fun stop() { - wakeLock?.let { - if (it.isHeld) { - it.release() - } - } + wakeLock?.let { if (it.isHeld) it.release() } stopSelf() } @@ -160,48 +160,23 @@ class AutoTunnelService : LifecycleService() { } private fun initWakeLock() { - wakeLock = - (getSystemService(POWER_SERVICE) as PowerManager).run { - val tag = this.javaClass.name - newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { - try { - Timber.i("Initiating wakelock with 10 min timeout") - acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) - } finally { - release() - } + wakeLock = (getSystemService(POWER_SERVICE) as PowerManager).run { + val tag = this.javaClass.name + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { + try { + Timber.i("Initiating wakelock with 10 min timeout") + acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) + } finally { + release() } } - } - - private fun startSettingsJob() = lifecycleScope.launch { - watchForSettingsChanges() - } - - private fun startVpnStateJob() = lifecycleScope.launch { - watchForVpnStateChanges() - } - - private fun startWifiJob() = lifecycleScope.launch { - watchForWifiConnectivityChanges() - } - - private fun startMobileDataJob() = lifecycleScope.launch { - watchForMobileDataConnectivityChanges() - } - - private fun startEthernetJob() = lifecycleScope.launch { - watchForEthernetConnectivityChanges() + } } private fun startPingJob() = lifecycleScope.launch { watchForPingFailure() } - private fun startNetworkEventJob() = lifecycleScope.launch { - handleNetworkEventChanges() - } - private fun startPingStateJob() = lifecycleScope.launch { autoTunnelStateFlow.collect { if (it.isPingEnabled()) { @@ -212,30 +187,6 @@ class AutoTunnelService : LifecycleService() { } } - private suspend fun watchForMobileDataConnectivityChanges() { - withContext(ioDispatcher) { - Timber.i("Starting mobile data watcher") - mobileDataService.networkStatus.collect { status -> - when (status) { - is NetworkStatus.Available -> { - Timber.i("Gained Mobile data connection") - emitMobileDataConnected(true) - } - - is NetworkStatus.CapabilitiesChanged -> { - emitMobileDataConnected(true) - Timber.i("Mobile data capabilities changed") - } - - is NetworkStatus.Unavailable -> { - emitMobileDataConnected(false) - Timber.i("Lost mobile data connection") - } - } - } - } - } - private suspend fun watchForPingFailure() { withContext(ioDispatcher) { Timber.i("Starting ping watcher") @@ -274,136 +225,52 @@ class AutoTunnelService : LifecycleService() { } } - private suspend fun watchForSettingsChanges() { - Timber.i("Starting settings watcher") - withContext(ioDispatcher) { - appDataRepository.settings.getSettingsFlow().combine( - appDataRepository.tunnels.getTunnelConfigsFlow(), - ) { settings, tunnels -> - Pair(settings, tunnels) - }.collect { pair -> - autoTunnelStateFlow.update { - it.copy( - settings = pair.first, - tunnels = pair.second, - ) - } + private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) { + combine( + combineSettings(), + combineNetworkEventsJob(), + ) { double, networkState -> + AutoTunnelState(tunnelService.get().vpnState.value, networkState, double.first, double.second) + }.collect { state -> + autoTunnelStateFlow.update { + it.copy(state.vpnState, state.networkState, state.settings, state.tunnels) } } } - private suspend fun watchForVpnStateChanges() { - Timber.i("Starting vpn state watcher") - withContext(ioDispatcher) { - tunnelService.get().vpnState.collect { state -> - autoTunnelStateFlow.update { - it.copy(vpnState = state) - } - } - } - } - - private fun startNetworkJobs() { - Timber.i("Starting all network state jobs..") - startWifiJob() - startEthernetJob() - startMobileDataJob() - startNetworkEventJob() - } - private fun cancelAndResetPingJob() { pingJob?.cancelWithMessage("Ping job canceled") pingJob = null } - private fun emitEthernetConnected(connected: Boolean) { - autoTunnelStateFlow.update { - it.copy( - isEthernetConnected = connected, + private fun combineNetworkEventsJob(): Flow { + return combine( + wifiService.networkStatus, + mobileDataService.networkStatus, + ethernetService.networkStatus, + ) { wifi, mobileData, ethernet -> + NetworkState( + wifi.isConnected, + mobileData.isConnected, + ethernet.isConnected, + when (wifi) { + is NetworkStatus.CapabilitiesChanged -> getWifiSSID(wifi.networkCapabilities) + is NetworkStatus.Available -> autoTunnelStateFlow.value.networkState.wifiName + is NetworkStatus.Unavailable -> null + }, ) - } + }.distinctUntilChanged() } - private fun emitWifiConnected(connected: Boolean) { - autoTunnelStateFlow.update { - it.copy( - isWifiConnected = connected, - ) - } - } - - private fun emitWifiSSID(ssid: String) { - autoTunnelStateFlow.update { - it.copy( - currentNetworkSSID = ssid, - ) - } - } - - private fun emitMobileDataConnected(connected: Boolean) { - autoTunnelStateFlow.update { - it.copy( - isMobileDataConnected = connected, - ) - } - } - - private suspend fun watchForEthernetConnectivityChanges() { - withContext(ioDispatcher) { - Timber.i("Starting ethernet data watcher") - ethernetService.networkStatus.collect { status -> - when (status) { - is NetworkStatus.Available -> { - Timber.i("Gained Ethernet connection") - emitEthernetConnected(true) - } - - is NetworkStatus.CapabilitiesChanged -> { - Timber.i("Ethernet capabilities changed") - emitEthernetConnected(true) - } - - is NetworkStatus.Unavailable -> { - emitEthernetConnected(false) - Timber.i("Lost Ethernet connection") - } - } - } - } - } - - private suspend fun watchForWifiConnectivityChanges() { - withContext(ioDispatcher) { - Timber.i("Starting wifi watcher") - wifiService.networkStatus.collect { status -> - when (status) { - is NetworkStatus.Available -> { - Timber.i("Gained Wi-Fi connection") - emitWifiConnected(true) - } - - is NetworkStatus.CapabilitiesChanged -> { - Timber.i("Wifi capabilities changed") - emitWifiConnected(true) - val ssid = getWifiSSID(status.networkCapabilities) - ssid?.let { name -> - if (name.contains(Constants.UNREADABLE_SSID)) { - Timber.w("SSID unreadable: missing permissions") - } else { - Timber.i("Detected valid SSID") - } - appDataRepository.appState.setCurrentSsid(name) - emitWifiSSID(name) - } ?: Timber.w("Failed to read ssid") - } - - is NetworkStatus.Unavailable -> { - emitWifiConnected(false) - Timber.i("Lost Wi-Fi connection") - } - } - } - } + private fun combineSettings(): Flow> { + return combine( + appDataRepository.get().settings.getSettingsFlow(), + appDataRepository.get().tunnels.getTunnelConfigsFlow().distinctUntilChanged { old, new -> + old.map { it.isActive } != new.map { it.isActive } + }, + ) { settings, tunnels -> + Pair(settings, tunnels) + }.distinctUntilChanged() } private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? { @@ -411,26 +278,28 @@ class AutoTunnelService : LifecycleService() { with(autoTunnelStateFlow.value.settings) { if (isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName() wifiService.getNetworkName(networkCapabilities) - } - } - } - - private suspend fun handleNetworkEventChanges() { - withContext(ioDispatcher) { - Timber.i("Starting auto-tunnel network event watcher") - // ignore vpnState emits to allow manual overrides - autoTunnelStateFlow.distinctUntilChanged { old, new -> - old.copy(vpnState = new.vpnState) == new || old.tunnels.map { it.isActive } != new.tunnels.map { it.isActive } - }.collect { watcherState -> - when (val event = watcherState.asAutoTunnelEvent()) { - is AutoTunnelEvent.Start -> tunnelService.get().startTunnel( - event.tunnelConfig - ?: appDataRepository.getPrimaryOrFirstTunnel(), - ) - is AutoTunnelEvent.Stop -> tunnelService.get().stopTunnel() - AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met") + }.also { + if (it?.contains(Constants.UNREADABLE_SSID) == true) { + Timber.w("SSID unreadable: missing permissions") + } else { + Timber.i("Detected valid SSID") } } } } + + private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) { + Timber.i("Starting auto-tunnel network event watcher") + autoTunnelStateFlow.collect { watcherState -> + Timber.d("New auto tunnel state emitted") + when (val event = watcherState.asAutoTunnelEvent()) { + is AutoTunnelEvent.Start -> tunnelService.get().startTunnel( + event.tunnelConfig + ?: appDataRepository.get().getPrimaryOrFirstTunnel(), + ) + is AutoTunnelEvent.Stop -> tunnelService.get().stopTunnel() + AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met") + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelEvent.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/AutoTunnelEvent.kt similarity index 95% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelEvent.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/AutoTunnelEvent.kt index 5a08de3..ed4fb85 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelEvent.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/AutoTunnelEvent.kt @@ -1,4 +1,4 @@ -package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel +package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/AutoTunnelState.kt similarity index 77% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelState.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/AutoTunnelState.kt index f9fa3c3..d76569d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/AutoTunnelState.kt @@ -1,23 +1,21 @@ -package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel +package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList +import timber.log.Timber data class AutoTunnelState( val vpnState: VpnState = VpnState(), - val isWifiConnected: Boolean = false, - val isEthernetConnected: Boolean = false, - val isMobileDataConnected: Boolean = false, - val currentNetworkSSID: String = "", + val networkState: NetworkState = NetworkState(), val settings: Settings = Settings(), val tunnels: TunnelConfigs = emptyList(), ) { private fun isMobileDataActive(): Boolean { - return !isEthernetConnected && !isWifiConnected && isMobileDataConnected + return !networkState.isEthernetConnected && !networkState.isWifiConnected && networkState.isMobileDataConnected } private fun isMobileTunnelDataChangeNeeded(): Boolean { @@ -44,19 +42,19 @@ data class AutoTunnelState( } private fun isWifiActive(): Boolean { - return !isEthernetConnected && isWifiConnected + return !networkState.isEthernetConnected && networkState.isWifiConnected } private fun startOnEthernet(): Boolean { - return isEthernetConnected && settings.isTunnelOnEthernetEnabled && vpnState.status.isDown() + return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && vpnState.status.isDown() } private fun stopOnEthernet(): Boolean { - return isEthernetConnected && !settings.isTunnelOnEthernetEnabled && vpnState.status.isUp() + return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && vpnState.status.isUp() } fun isNoConnectivity(): Boolean { - return !isEthernetConnected && !isWifiConnected && !isMobileDataConnected + return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected } private fun stopOnMobileData(): Boolean { @@ -72,7 +70,7 @@ data class AutoTunnelState( } private fun changeOnEthernet(): Boolean { - return isEthernetConnected && settings.isTunnelOnEthernetEnabled && isEthernetTunnelChangeNeeded() + return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && isEthernetTunnelChangeNeeded() } private fun stopOnWifi(): Boolean { @@ -84,6 +82,7 @@ data class AutoTunnelState( } private fun startOnUntrustedWifi(): Boolean { + Timber.d("Is tunnel on wifi enabled ${settings.isTunnelOnWifiEnabled}") return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isDown() && !isCurrentSSIDTrusted() } @@ -120,19 +119,23 @@ data class AutoTunnelState( } private fun isCurrentSSIDTrusted(): Boolean { + return networkState.wifiName?.let { + hasTrustedWifiName(it) + } == true + } + + private fun hasTrustedWifiName(wifiName: String, wifiNames: List = settings.trustedNetworkSSIDs): Boolean { return if (settings.isWildcardsEnabled) { - settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID) + wifiNames.isMatchingToWildcardList(wifiName) } else { - settings.trustedNetworkSSIDs.contains(currentNetworkSSID) + wifiNames.contains(wifiName) } } private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? { - return tunnels.firstOrNull { - if (settings.isWildcardsEnabled) { - it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID) - } else { - it.tunnelNetworks.contains(currentNetworkSSID) + return networkState.wifiName?.let { wifiName -> + tunnels.firstOrNull { + hasTrustedWifiName(wifiName, it.tunnelNetworks) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/NetworkState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/NetworkState.kt new file mode 100644 index 0000000..35bf8d2 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/NetworkState.kt @@ -0,0 +1,8 @@ +package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model + +data class NetworkState( + val isWifiConnected: Boolean = false, + val isMobileDataConnected: Boolean = false, + val isEthernetConnected: Boolean = false, + val wifiName: String? = null, +) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt index fbfb931..d71fe63 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt @@ -10,7 +10,10 @@ import android.os.Build import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.map +import timber.log.Timber abstract class BaseNetworkService>( val context: Context, @@ -22,8 +25,17 @@ abstract class BaseNetworkService>( val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + fun checkHasCapability(networkCapability: Int): Boolean { + val network = connectivityManager.activeNetwork + val networkCapabilities = connectivityManager.getNetworkCapabilities(network) + return networkCapabilities?.hasTransport(networkCapability) == true + } + override val networkStatus = callbackFlow { + if (!checkHasCapability(networkCapability)) { + trySend(NetworkStatus.Unavailable()) + } val networkStatusCallback = when (Build.VERSION.SDK_INT) { in Build.VERSION_CODES.S..Int.MAX_VALUE -> { @@ -36,7 +48,7 @@ abstract class BaseNetworkService>( } override fun onLost(network: Network) { - trySend(NetworkStatus.Unavailable(network)) + trySend(NetworkStatus.Unavailable()) } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { @@ -57,7 +69,7 @@ abstract class BaseNetworkService>( } override fun onLost(network: Network) { - trySend(NetworkStatus.Unavailable(network)) + trySend(NetworkStatus.Unavailable()) } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { @@ -80,17 +92,20 @@ abstract class BaseNetworkService>( connectivityManager.registerNetworkCallback(request, networkStatusCallback) awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) } - } + }.catch { + Timber.e(it) + // conflate for backpressure + }.conflate() } inline fun Flow.map( - crossinline onUnavailable: suspend (network: Network) -> Result, + crossinline onUnavailable: suspend () -> Result, crossinline onAvailable: suspend (network: Network) -> Result, crossinline onCapabilitiesChanged: suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result, ): Flow = map { status -> when (status) { - is NetworkStatus.Unavailable -> onUnavailable(status.network) + is NetworkStatus.Unavailable -> onUnavailable() is NetworkStatus.Available -> onAvailable(status.network) is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkStatus.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkStatus.kt index 99930af..2c338e7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkStatus.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkStatus.kt @@ -4,10 +4,11 @@ import android.net.Network import android.net.NetworkCapabilities sealed class NetworkStatus { - class Available(val network: Network) : NetworkStatus() + abstract val isConnected: Boolean + class Available(val network: Network, override val isConnected: Boolean = true) : NetworkStatus() - class Unavailable(val network: Network) : NetworkStatus() + class Unavailable(override val isConnected: Boolean = false) : NetworkStatus() - class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities) : + class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities, override val isConnected: Boolean = true) : NetworkStatus() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/BackendState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/BackendState.kt new file mode 100644 index 0000000..ee3845b --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/BackendState.kt @@ -0,0 +1,7 @@ +package com.zaneschepke.wireguardautotunnel.service.tunnel + +enum class BackendState { + KILL_SWITCH_ACTIVE, + SERVICE_ACTIVE, + INACTIVE, +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt index 1cb55ab..7761073 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt @@ -12,13 +12,17 @@ interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel { suspend fun bounceTunnel() + suspend fun getBackendState(): BackendState + + suspend fun setBackendState(backendState: BackendState, allowedIps: Collection) + val vpnState: StateFlow suspend fun runningTunnelNames(): Set suspend fun getState(): TunnelState - fun cancelStatsJob() + fun cancelActiveTunnelJobs() - fun startStatsJob() + fun startActiveTunnelJobs() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index dd7ebf6..dd10821 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -12,15 +12,15 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics +import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState +import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex @@ -35,7 +35,7 @@ class WireGuardTunnel @Inject constructor( private val amneziaBackend: Provider, - tunnelConfigRepository: TunnelConfigRepository, + private val tunnelConfigRepository: TunnelConfigRepository, @Kernel private val kernelBackend: Provider, private val appDataRepository: AppDataRepository, @ApplicationScope private val applicationScope: CoroutineScope, @@ -44,22 +44,28 @@ constructor( ) : TunnelService { private val _vpnState = MutableStateFlow(VpnState()) - override val vpnState: StateFlow = _vpnState.combine( - tunnelConfigRepository.getTunnelConfigsFlow(), - ) { - vpnState, tunnels -> - vpnState.copy( - tunnelConfig = tunnels.firstOrNull { it.id == vpnState.tunnelConfig?.id }, - ) - }.stateIn(applicationScope, SharingStarted.Eagerly, VpnState()) + override val vpnState: StateFlow = _vpnState.asStateFlow() private var statsJob: Job? = null + private var tunnelChangesJob: Job? = null - private val mutex = Mutex() + @get:Synchronized @set:Synchronized + private var isKernelBackend: Boolean? = null + + private val tunnelControlMutex = Mutex() + + init { + applicationScope.launch(ioDispatcher) { + appDataRepository.settings.getSettingsFlow().collect { + isKernelBackend = it.isKernelEnabled + } + } + } private suspend fun backend(): Any { - val settings = appDataRepository.settings.getSettings() - if (settings.isKernelEnabled) return kernelBackend.get() + val isKernelEnabled = isKernelBackend + ?: appDataRepository.settings.getSettings().isKernelEnabled + if (isKernelEnabled) return kernelBackend.get() return amneziaBackend.get() } @@ -103,13 +109,15 @@ constructor( override suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean) { if (tunnelConfig == null) return withContext(ioDispatcher) { - mutex.withLock { - if (isTunnelAlreadyRunning(tunnelConfig)) return@withContext + if (isTunnelAlreadyRunning(tunnelConfig)) return@withContext + withServiceActive { onBeforeStart(background) - setState(tunnelConfig, TunnelState.UP).onSuccess { - startStatsJob() - if (it.isUp()) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true)) - updateTunnelState(it, tunnelConfig) + tunnelControlMutex.withLock { + setState(tunnelConfig, TunnelState.UP).onSuccess { + startActiveTunnelJobs() + if (it.isUp()) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true)) + updateTunnelState(it, tunnelConfig) + } }.onFailure { Timber.e(it) } @@ -119,10 +127,10 @@ constructor( override suspend fun stopTunnel() { withContext(ioDispatcher) { - mutex.withLock { - if (_vpnState.value.status.isDown()) return@withContext - with(_vpnState.value) { - if (tunnelConfig == null) return@withContext + if (_vpnState.value.status.isDown()) return@withContext + with(_vpnState.value) { + if (tunnelConfig == null) return@withContext + tunnelControlMutex.withLock { setState(tunnelConfig, TunnelState.DOWN).onSuccess { updateTunnelState(it, null) onStop(tunnelConfig) @@ -135,11 +143,64 @@ constructor( } } + private suspend fun toggleTunnel(tunnelConfig: TunnelConfig) { + withContext(ioDispatcher) { + tunnelControlMutex.withLock { + setState(tunnelConfig, TunnelState.TOGGLE) + } + } + } + + // utility to keep vpnService alive during rapid changes to prevent bad states + private suspend fun withServiceActive(callback: suspend () -> Unit) { + when (val backend = backend()) { + is org.amnezia.awg.backend.Backend -> { + val backendState = backend.backendState + if (backendState == org.amnezia.awg.backend.Backend.BackendState.INACTIVE) { + backend.setBackendState(org.amnezia.awg.backend.Backend.BackendState.SERVICE_ACTIVE, emptyList()) + } + callback() + } + is Backend -> { + callback() + } + } + } + override suspend fun bounceTunnel() { - if (_vpnState.value.tunnelConfig == null) return - val config = _vpnState.value.tunnelConfig - stopTunnel() - startTunnel(config) + _vpnState.value.tunnelConfig?.let { + withServiceActive { + toggleTunnel(it) + toggleTunnel(it) + } + } + } + + override suspend fun getBackendState(): BackendState { + return when (val backend = backend()) { + is org.amnezia.awg.backend.Backend -> { + backend.backendState.asBackendState() + } + is Backend -> { + BackendState.SERVICE_ACTIVE + } + else -> BackendState.INACTIVE + } + } + + override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection) { + kotlin.runCatching { + when (val backend = backend()) { + is org.amnezia.awg.backend.Backend -> { + backend.setBackendState(backendState.asAmBackendState(), allowedIps) + } + is Backend -> { + // TODO not yet implemented + Timber.d("Kernel backend state not yet implemented") + } + else -> Unit + } + } } private suspend fun shutDownActiveTunnel() { @@ -169,7 +230,7 @@ constructor( private suspend fun onStop(tunnelConfig: TunnelConfig) { appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false)) - cancelStatsJob() + cancelActiveTunnelJobs() resetBackendStatistics() } @@ -179,7 +240,13 @@ constructor( } } - private fun emitBackendStatistics(statistics: TunnelStatistics) { + private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) { + _vpnState.update { + it.copy(tunnelConfig = tunnelConfig) + } + } + + private fun updateBackendStatistics(statistics: TunnelStatistics) { _vpnState.update { it.copy(statistics = statistics) } @@ -199,12 +266,14 @@ constructor( } } - override fun cancelStatsJob() { + override fun cancelActiveTunnelJobs() { statsJob?.cancel() + tunnelChangesJob?.cancel() } - override fun startStatsJob() { + override fun startActiveTunnelJobs() { statsJob = startTunnelStatisticsJob() + tunnelChangesJob = startTunnelConfigChangesJob() } override fun getName(): String { @@ -216,11 +285,11 @@ constructor( delay(STATS_START_DELAY) while (true) { when (backend) { - is Backend -> emitBackendStatistics( + is Backend -> updateBackendStatistics( WireGuardStatistics(backend.getStatistics(this@WireGuardTunnel)), ) is org.amnezia.awg.backend.Backend -> { - emitBackendStatistics( + updateBackendStatistics( AmneziaStatistics( backend.getStatistics(this@WireGuardTunnel), ), @@ -231,6 +300,22 @@ constructor( } } + private fun startTunnelConfigChangesJob() = applicationScope.launch(ioDispatcher) { + tunnelConfigRepository.getTunnelConfigsFlow().collect { + with(_vpnState.value) { + if (status.isDown() || tunnelConfig == null) return@collect + val vpnConfigFromStorage = it.first { it.id == tunnelConfig.id } + val isRestartNeeded = vpnConfigFromStorage.wgQuick != tunnelConfig.wgQuick || + vpnConfigFromStorage.amQuick != tunnelConfig.amQuick + updateTunnelConfig(vpnConfigFromStorage) + if (isRestartNeeded) { + Timber.d("Bouncing tunnel on config change") + bounceTunnel() + } + } + } + } + override fun onStateChange(newState: Tunnel.State) { _vpnState.update { it.copy(status = TunnelState.from(newState)) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt index 18acd24..5b65a1d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt @@ -9,10 +9,12 @@ import com.wireguard.android.util.RootShell import com.zaneschepke.logcatter.LogReader import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.module.AppShell import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager +import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController @@ -34,6 +36,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.plus import kotlinx.coroutines.withContext +import timber.log.Timber import xyz.teamgravity.pin_lock_compose.PinManager import javax.inject.Inject import javax.inject.Provider @@ -80,7 +83,7 @@ constructor( init { viewModelScope.launch { initPin() - initAutoTunnel() + initServices() initTunnel() appReadyCheck() } @@ -94,7 +97,7 @@ constructor( } private suspend fun initTunnel() { - if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startStatsJob() + if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startActiveTunnelJobs() val activeTunnels = appDataRepository.tunnels.getActive() if (activeTunnels.isNotEmpty() && tunnelService.get().getState() == TunnelState.DOWN @@ -108,9 +111,12 @@ constructor( if (isPinEnabled) PinManager.initialize(WireGuardAutoTunnel.instance) } - private suspend fun initAutoTunnel() { - val settings = appDataRepository.settings.getSettings() - if (settings.isAutoTunnelEnabled) serviceManager.startAutoTunnel(false) + private suspend fun initServices() { + withContext(ioDispatcher) { + val settings = appDataRepository.settings.getSettings() + handleVpnKillSwitchChange(settings.isVpnKillSwitchEnabled) + if (settings.isAutoTunnelEnabled) serviceManager.startAutoTunnel(false) + } } fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) { @@ -170,10 +176,50 @@ constructor( } } + fun onToggleVpnKillSwitch(enabled: Boolean) = viewModelScope.launch { + with(uiState.value.settings) { + appDataRepository.settings.save( + copy( + isVpnKillSwitchEnabled = enabled, + isLanOnKillSwitchEnabled = if (enabled) isLanOnKillSwitchEnabled else false, + ), + ) + } + handleVpnKillSwitchChange(enabled) + } + + private suspend fun handleVpnKillSwitchChange(enabled: Boolean) { + withContext(ioDispatcher) { + if (enabled) { + Timber.d("Starting kill switch") + val allowedIps = if (appDataRepository.settings.getSettings().isLanOnKillSwitchEnabled) { + TunnelConfig.IPV4_PUBLIC_NETWORKS + } else { + emptySet() + } + tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps) + } else { + Timber.d("Sending shutdown of kill switch") + tunnelService.get().setBackendState(BackendState.SERVICE_ACTIVE, emptySet()) + } + } + } + + fun onToggleLanOnKillSwitch(enabled: Boolean) = viewModelScope.launch(ioDispatcher) { + appDataRepository.settings.save( + uiState.value.settings.copy( + isLanOnKillSwitchEnabled = enabled, + ), + ) + val allowedIps = if (enabled) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet() + Timber.d("Setting allowedIps $allowedIps") + tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps) + } + fun onToggleShortcutsEnabled() = viewModelScope.launch { with(uiState.value.settings) { appDataRepository.settings.save( - this.copy( + copy( isShortcutsEnabled = !isShortcutsEnabled, ), ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index 818227c..4678be1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -52,6 +52,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.displa import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme @@ -144,8 +145,8 @@ class MainActivity : AppCompatActivity() { ), ) }, - ) { - Box(modifier = Modifier.fillMaxSize().padding(it)) { + ) { padding -> + Box(modifier = Modifier.fillMaxSize().padding(padding)) { NavHost( navController, enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) }, @@ -207,6 +208,9 @@ class MainActivity : AppCompatActivity() { composable { ScannerScreen() } + composable { + KillSwitchScreen(appUiState, viewModel) + } } } } @@ -218,6 +222,6 @@ class MainActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() // save battery by not polling stats while app is closed - tunnelService.cancelStatsJob() + tunnelService.cancelActiveTunnelJobs() } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt index 4a7d714..7e085e1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt @@ -21,6 +21,9 @@ sealed class Route { @Serializable data object Display : Route() + @Serializable + data object KillSwitch : Route() + @Serializable data object Language : Route() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/VpnDeniedDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/permission/vpn/VpnDeniedDialog.kt similarity index 95% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/VpnDeniedDialog.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/permission/vpn/VpnDeniedDialog.kt index 04dc9c7..f2029dd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/VpnDeniedDialog.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/permission/vpn/VpnDeniedDialog.kt @@ -1,4 +1,4 @@ -package com.zaneschepke.wireguardautotunnel.ui.screens.main.components +package com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.MaterialTheme diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/permission/vpn/withVpnPermission.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/permission/vpn/withVpnPermission.kt new file mode 100644 index 0000000..f8791f4 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/permission/vpn/withVpnPermission.kt @@ -0,0 +1,38 @@ +package com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn + +import android.net.VpnService +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity.RESULT_OK +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext + +@Composable +inline fun withVpnPermission(crossinline onSuccess: (t: T) -> Unit): (t: T) -> Unit { + val context = LocalContext.current + + var showVpnPermissionDialog by remember { mutableStateOf(false) } + + val vpnActivity = + rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + onResult = { + if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true + }, + ) + + VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false }) + + return { + val intent = VpnService.prepare(context) + if (intent != null) { + vpnActivity.launch(intent) + } else { + onSuccess(it) + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/permission/withIgnoreBatteryOpt.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/permission/withIgnoreBatteryOpt.kt new file mode 100644 index 0000000..4a310d4 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/permission/withIgnoreBatteryOpt.kt @@ -0,0 +1,35 @@ +package com.zaneschepke.wireguardautotunnel.ui.common.permission + +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.zaneschepke.wireguardautotunnel.util.extensions.isBatteryOptimizationsDisabled + +@Composable +inline fun withIgnoreBatteryOpt(ignore: Boolean, crossinline callback: () -> Unit): () -> Unit { + val context = LocalContext.current + val batteryActivity = + rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { result: ActivityResult -> + // we only ask once + callback() + } + return { + if (ignore || context.isBatteryOptimizationsDisabled()) { + callback() + } else { + batteryActivity.launch( + Intent().apply { + action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + data = Uri.parse("package:${context.packageName}") + }, + ) + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index e12e6f2..5e24bf1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -1,13 +1,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main -import android.content.Intent -import android.net.Uri -import android.net.VpnService -import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity.RESULT_OK import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.detectTapGestures @@ -51,15 +45,15 @@ import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar +import com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn.withVpnPermission +import com.zaneschepke.wireguardautotunnel.ui.common.permission.withIgnoreBatteryOpt import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem -import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.util.Constants -import com.zaneschepke.wireguardautotunnel.util.extensions.isBatteryOptimizationsDisabled import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight @@ -73,30 +67,28 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) val snackbar = SnackbarController.current var showBottomSheet by remember { mutableStateOf(false) } - var showVpnPermissionDialog by remember { mutableStateOf(false) } var isFabVisible by rememberSaveable { mutableStateOf(true) } var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } var selectedTunnel by remember { mutableStateOf(null) } val isRunningOnTv = remember { context.isRunningOnTv() } + val startAutoTunnel = withVpnPermission { viewModel.onToggleAutoTunnel() } + val startTunnel = withVpnPermission { + viewModel.onTunnelStart(it, uiState.settings.isKernelEnabled) + } + val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) { + if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown() + if (uiState.settings.isKernelEnabled) { + viewModel.onToggleAutoTunnel() + } else { + startAutoTunnel.invoke(Unit) + } + } + val nestedScrollConnection = remember { NestedScrollListener({ isFabVisible = false }, { isFabVisible = true }) } - val vpnActivity = - rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult(), - onResult = { - if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true - }, - ) - val batteryActivity = - rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { result: ActivityResult -> - viewModel.setBatteryOptimizeDisableShown() - } - val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = { snackbar.showMessage( context.getString(R.string.error_no_file_explorer), @@ -112,8 +104,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) navController.navigate(Route.Scanner) } - VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false }) - if (showDeleteTunnelAlertDialog) { InfoDialog( onDismiss = { showDeleteTunnelAlertDialog = false }, @@ -128,35 +118,13 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) ) } - fun requestBatteryOptimizationsDisabled() { - val intent = - Intent().apply { - action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - data = Uri.parse("package:${context.packageName}") - } - batteryActivity.launch(intent) - } - - fun onAutoTunnelToggle() { - if (!uiState.generalState.isBatteryOptimizationDisableShown && - !context.isBatteryOptimizationsDisabled() && !isRunningOnTv - ) { - return requestBatteryOptimizationsDisabled() - } - val intent = if (!uiState.settings.isKernelEnabled) { - VpnService.prepare(context) - } else { - null - } - if (intent != null) return vpnActivity.launch(intent) - viewModel.onToggleAutoTunnel() - } - fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { - val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context) - if (intent != null) return vpnActivity.launch(intent) if (!checked) viewModel.onTunnelStop().also { return } - viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled) + if (uiState.settings.isKernelEnabled) { + viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled) + } else { + startTunnel.invoke(tunnel) + } } Scaffold( @@ -239,9 +207,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) } } else { item { - AutoTunnelRowItem(uiState, { - onAutoTunnelToggle() - }) + AutoTunnelRowItem(uiState) { + autoTunnelToggleBattery.invoke() + } } } items( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index 1a29ac8..f58f5b6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -12,13 +12,13 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ViewQuilt import androidx.compose.material.icons.filled.AppShortcut -import androidx.compose.material.icons.outlined.AdminPanelSettings import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.FolderZip import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material.icons.outlined.Pin import androidx.compose.material.icons.outlined.Restore +import androidx.compose.material.icons.outlined.VpnKeyOff import androidx.compose.material.icons.outlined.VpnLock import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -49,7 +49,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.Forwar import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings -import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.showToast @@ -179,18 +178,18 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: onClick = { appViewModel.onToggleAlwaysOnVPN() }, ), SelectionItem( - Icons.Outlined.AdminPanelSettings, + Icons.Outlined.VpnKeyOff, title = { Text( - stringResource(R.string.kill_switch), + stringResource(R.string.kill_switch_options), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), ) }, onClick = { - context.launchVpnSettings() + navController.navigate(Route.KillSwitch) }, trailing = { - ForwardButton { context.launchVpnSettings() } + ForwardButton { navController.navigate(Route.KillSwitch) } }, ), ), diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/KillSwitchScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/KillSwitchScreen.kt new file mode 100644 index 0000000..a5529db --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/KillSwitchScreen.kt @@ -0,0 +1,142 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AdminPanelSettings +import androidx.compose.material.icons.outlined.Lan +import androidx.compose.material.icons.outlined.VpnKey +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.ui.AppUiState +import com.zaneschepke.wireguardautotunnel.ui.AppViewModel +import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch +import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem +import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton +import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar +import com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn.withVpnPermission +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton +import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth + +@Composable +fun KillSwitchScreen(uiState: AppUiState, appViewModel: AppViewModel) { + val context = LocalContext.current + + val toggleVpnSwitch = withVpnPermission { appViewModel.onToggleVpnKillSwitch(it) } + + fun toggleVpnKillSwitch() { + with(uiState.settings) { + if (isVpnKillSwitchEnabled) { + appViewModel.onToggleVpnKillSwitch(false) + } else { + toggleVpnSwitch.invoke(true) + } + } + } + + fun toggleLanOnKillSwitch() { + with(uiState.settings) { + appViewModel.onToggleLanOnKillSwitch(!isLanOnKillSwitchEnabled) + } + } + + Scaffold( + topBar = { + TopNavBar(stringResource(R.string.kill_switch)) + }, + ) { padding -> + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), + modifier = + Modifier + .fillMaxSize().padding(padding) + .padding(top = 24.dp.scaledHeight()) + .padding(horizontal = 24.dp.scaledWidth()), + ) { + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.AdminPanelSettings, + title = { + Text( + stringResource(R.string.native_kill_switch), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + onClick = { context.launchVpnSettings() }, + trailing = { + ForwardButton { context.launchVpnSettings() } + }, + ), + ), + ) + SurfaceSelectionGroupButton( + buildList { + add( + SelectionItem( + Icons.Outlined.VpnKey, + title = { + Text( + stringResource(R.string.vpn_kill_switch), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + onClick = { + toggleVpnKillSwitch() + }, + trailing = { + ScaledSwitch( + uiState.settings.isVpnKillSwitchEnabled, + onClick = { + toggleVpnKillSwitch() + }, + ) + }, + ), + ) + if (uiState.settings.isVpnKillSwitchEnabled) { + add( + SelectionItem( + Icons.Outlined.Lan, + title = { + Text( + stringResource(R.string.allow_lan_traffic), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + onClick = { toggleLanOnKillSwitch() }, + description = { + Text( + stringResource(R.string.bypass_lan_for_kill_switch), + style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + uiState.settings.isLanOnKillSwitchEnabled, + onClick = { + toggleLanOnKillSwitch() + }, + ) + }, + ), + ) + } + }, + ) + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt index d3cdc28..6e9f9a8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt @@ -3,12 +3,14 @@ package com.zaneschepke.wireguardautotunnel.util.extensions import androidx.compose.ui.graphics.Color import com.wireguard.android.util.RootShell import com.wireguard.config.Peer +import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree import com.zaneschepke.wireguardautotunnel.ui.theme.Straw import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.NumberUtils +import org.amnezia.awg.backend.Backend import org.amnezia.awg.config.Config import timber.log.Timber import java.net.InetAddress @@ -85,3 +87,11 @@ fun RootShell.getCurrentWifiName(): String? { this.run(response, "dumpsys wifi | grep -o \"SSID: [^,]*\" | cut -d ' ' -f2- | tr -d '\"'") return response.lastOrNull() } + +fun Backend.BackendState.asBackendState(): BackendState { + return BackendState.valueOf(this.name) +} + +fun BackendState.asAmBackendState(): Backend.BackendState { + return Backend.BackendState.valueOf(this.name) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a6a33e0..fafc672 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -182,4 +182,9 @@ Stop tunnel on internet loss Ethernet tunnel Set as ethernet tunnel + Native kill switch + VPN kill switch + Kill switch options + Allow LAN traffic + Bypass LAN for kill switch diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 040b1ab..0513ee1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] accompanist = "0.36.0" activityCompose = "1.9.3" -amneziawgAndroid = "1.2.2" +amneziawgAndroid = "1.2.3" androidx-junit = "1.2.1" appcompat = "1.7.0" biometricKtx = "1.2.0-alpha05" @@ -10,7 +10,7 @@ coreKtx = "1.15.0" datastorePreferences = "1.1.1" desugar_jdk_libs = "2.1.3" espressoCore = "3.6.1" -hiltAndroid = "2.52" +hiltAndroid = "2.53" hiltNavigationCompose = "1.2.0" junit = "4.13.2" kotlinx-serialization-json = "1.7.3" @@ -21,9 +21,9 @@ pinLockCompose = "1.0.4" roomVersion = "2.6.1" timber = "5.0.1" tunnel = "1.2.1" -androidGradlePlugin = "8.7.2" -kotlin = "2.0.21" -ksp = "2.0.21-1.0.28" +androidGradlePlugin = "8.8.0-rc01" +kotlin = "2.1.0" +ksp = "2.1.0-1.0.29" composeBom = "2024.11.00" compose = "1.7.5" zxingAndroidEmbedded = "4.3.0" @@ -32,7 +32,7 @@ gradlePlugins-grgit = "5.3.0" #plugins material = "1.12.0" -gradlePlugins-ktlint="12.1.1" +gradlePlugins-ktlint="12.1.2" [libraries] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac7bef3..105b3e9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,8 @@ #Wed Oct 11 22:39:21 EDT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip -distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists