fix: auto tunnel logic and speed

closes #466
This commit is contained in:
Zane Schepke 2024-11-30 11:00:22 -05:00
parent 64a7680b81
commit 70649383e0
15 changed files with 251 additions and 329 deletions

View File

@ -157,7 +157,7 @@
android:value="true" /> android:value="true" />
</service> </service>
<service <service
android:name=".service.foreground.AutoTunnelService" android:name=".service.foreground.autotunnel.AutoTunnelService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="systemExempted" android:foregroundServiceType="systemExempted"

View File

@ -1,8 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action {
START,
START_FOREGROUND,
STOP,
STOP_FOREGROUND,
}

View File

@ -1,107 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
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
data class AutoTunnelState(
val vpnState: VpnState = VpnState(),
val isWifiConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings(),
val tunnels: TunnelConfigs = emptyList(),
) {
fun isEthernetConditionMet(): Boolean {
return (
isEthernetConnected &&
settings.isTunnelOnEthernetEnabled
)
}
fun isMobileDataConditionMet(): Boolean {
return (
!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected
)
}
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
return (
!isEthernetConnected &&
!settings.isTunnelOnMobileDataEnabled &&
isMobileDataConnected &&
!isWifiConnected
)
}
fun isUntrustedWifiConditionMet(): Boolean {
return (
!isEthernetConnected &&
isWifiConnected &&
!isCurrentSSIDTrusted() &&
settings.isTunnelOnWifiEnabled
)
}
fun isTrustedWifiConditionMet(): Boolean {
return (
!isEthernetConnected &&
(
isWifiConnected &&
isCurrentSSIDTrusted()
)
)
}
fun isTunnelOffOnWifiConditionMet(): Boolean {
return (
!isEthernetConnected &&
(
isWifiConnected &&
!settings.isTunnelOnWifiEnabled
)
)
}
fun isTunnelOffOnNoConnectivityMet(): Boolean {
return (
!isEthernetConnected &&
!isWifiConnected &&
!isMobileDataConnected
)
}
fun isCurrentSSIDTrusted(): Boolean {
return if (settings.isWildcardsEnabled) {
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
} else {
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)
}
}
fun isCurrentSSIDActiveTunnelNetwork(): Boolean {
val currentTunnelNetworks = vpnState.tunnelConfig?.tunnelNetworks
return (
if (settings.isWildcardsEnabled) {
currentTunnelNetworks?.isMatchingToWildcardList(currentNetworkSSID)
} else {
currentTunnelNetworks?.contains(currentNetworkSSID)
}
) == true
}
fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return tunnels.firstOrNull {
if (settings.isWildcardsEnabled) {
it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID)
} else {
it.tunnelNetworks.contains(currentNetworkSSID)
}
}
}
}

View File

@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.util.SingletonHolder import com.zaneschepke.wireguardautotunnel.util.SingletonHolder
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject import jakarta.inject.Inject

View File

@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
sealed class AutoTunnelEvent {
data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
data class Stop(val tunnelConfig: TunnelConfig?) : AutoTunnelEvent()
data object DoNothing : AutoTunnelEvent()
}

View File

@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
import android.content.Intent import android.content.Intent
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
@ -15,6 +15,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
@ -248,7 +249,7 @@ class AutoTunnelService : LifecycleService() {
if (results.contains(false)) { if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure") Timber.i("Restarting VPN for ping failure")
val cooldown = vpnState.tunnelConfig.pingCooldown val cooldown = vpnState.tunnelConfig.pingCooldown
tunnelService.get().bounceTunnel(vpnState.tunnelConfig) tunnelService.get().bounceTunnel()
delay(cooldown ?: Constants.PING_COOLDOWN) delay(cooldown ?: Constants.PING_COOLDOWN)
continue continue
} }
@ -271,15 +272,15 @@ class AutoTunnelService : LifecycleService() {
old.map { it.isActive } != new.map { it.isActive } old.map { it.isActive } != new.map { it.isActive }
}, },
) { settings, tunnels -> ) { settings, tunnels ->
Timber.d("Tunnels or settings changed!") Pair(settings, tunnels)
autoTunnelStateFlow.value.copy( }.collect { pair ->
settings = settings, manageJobsBySettings(pair.first)
tunnels = tunnels, autoTunnelStateFlow.update {
) it.copy(
}.collect { settings = pair.first,
Timber.d("got new settings: ${it.settings}") tunnels = pair.second,
manageJobsBySettings(it.settings) )
autoTunnelStateFlow.emit(it) }
} }
} }
} }
@ -287,12 +288,12 @@ class AutoTunnelService : LifecycleService() {
private suspend fun watchForVpnStateChanges() { private suspend fun watchForVpnStateChanges() {
Timber.i("Starting vpn state watcher") Timber.i("Starting vpn state watcher")
withContext(ioDispatcher) { withContext(ioDispatcher) {
tunnelService.get().vpnState.distinctUntilChanged { old, new -> tunnelService.get().vpnState.collect { state ->
old.tunnelConfig?.id == new.tunnelConfig?.id
}.collect { state ->
autoTunnelStateFlow.update { autoTunnelStateFlow.update {
it.copy(vpnState = state) it.copy(vpnState = state)
} }
// TODO think about this
// What happens if we change the pinger setting while vpn is active?
state.tunnelConfig?.let { state.tunnelConfig?.let {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (it.isPingEnabled && !settings.isPingEnabled) { if (it.isPingEnabled && !settings.isPingEnabled) {
@ -455,100 +456,23 @@ class AutoTunnelService : LifecycleService() {
} }
} }
private suspend fun getMobileDataTunnel(): TunnelConfig? {
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
}
private suspend fun handleNetworkEventChanges() { private suspend fun handleNetworkEventChanges() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
Timber.i("Starting network event watcher") Timber.i("Starting auto-tunnel network event watcher")
autoTunnelStateFlow.collect { watcherState -> // allow manual overrides
val autoTunnel = "Auto-tunnel watcher" autoTunnelStateFlow.distinctUntilChanged { old, new ->
// delay for rapid network state changes and then collect latest old.copy(vpnState = new.vpnState) == new
delay(Constants.WATCHER_COLLECTION_DELAY) }.collect { watcherState ->
val activeTunnel = watcherState.vpnState.tunnelConfig when (val event = watcherState.asAutoTunnelEvent()) {
val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel() is AutoTunnelEvent.Start -> {
val isTunnelDown = tunnelService.get().getState() == TunnelState.DOWN Timber.d("Start tunnel ${event.tunnelConfig?.name}")
when { tunnelService.get().startTunnel(event.tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel())
watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
if (isTunnelDown) {
defaultTunnel?.let {
tunnelService.get().startTunnel(it)
}
}
} }
is AutoTunnelEvent.Stop -> {
watcherState.isMobileDataConditionMet() -> { Timber.d("Stop tunnel")
Timber.i("$autoTunnel - tunnel on mobile data condition met") tunnelService.get().stopTunnel()
val mobileDataTunnel = getMobileDataTunnel()
val tunnel =
mobileDataTunnel ?: defaultTunnel
if (isTunnelDown || activeTunnel?.isMobileDataTunnel == false) {
tunnel?.let {
tunnelService.get().startTunnel(it)
}
}
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
if (!isTunnelDown) {
activeTunnel?.let {
tunnelService.get().stopTunnel(it)
}
}
}
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("Untrusted wifi condition met")
if (activeTunnel == null || watcherState.isCurrentSSIDActiveTunnelNetwork() == false ||
isTunnelDown
) {
Timber.i(
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
)
watcherState.getTunnelWithMatchingTunnelNetwork()?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
if (isTunnelDown || activeTunnel?.id != it.id) {
tunnelService.get().startTunnel(it)
}
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
val default = appDataRepository.getPrimaryOrFirstTunnel()
if (default?.name != tunnelService.get().name || isTunnelDown) {
default?.let {
tunnelService.get().startTunnel(it)
}
}
}.invoke()
}
}
watcherState.isTrustedWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
)
if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on wifi condition met, turning vpn off",
)
if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
// TODO disable for this now
// watcherState.isTunnelOffOnNoConnectivityMet() -> {
// Timber.i(
// "$autoTunnel - tunnel off on no connectivity met, turning vpn off",
// )
// if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
// }
else -> {
Timber.i("$autoTunnel - no condition met")
} }
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
} }
} }
} }

View File

@ -0,0 +1,118 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
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
data class AutoTunnelState(
val vpnState: VpnState = VpnState(),
val isWifiConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings(),
val tunnels: TunnelConfigs = emptyList(),
) {
private fun isMobileDataActive(): Boolean {
return !isEthernetConnected && !isWifiConnected && isMobileDataConnected
}
private fun isMobileTunnelDataChangeNeeded(): Boolean {
val preferredTunnel = preferredMobileDataTunnel()
return preferredTunnel != null &&
vpnState.status.isUp() && preferredTunnel.id != vpnState.tunnelConfig?.id
}
private fun preferredMobileDataTunnel(): TunnelConfig? {
return tunnels.firstOrNull { it.isMobileDataTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun preferredWifiTunnel(): TunnelConfig? {
return getTunnelWithMatchingTunnelNetwork() ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun isWifiActive(): Boolean {
return !isEthernetConnected && isWifiConnected
}
private fun startOnEthernet(): Boolean {
return isEthernetConnected && settings.isTunnelOnEthernetEnabled && vpnState.status.isDown()
}
private fun stopOnMobileData(): Boolean {
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && vpnState.status.isUp()
}
private fun startOnMobileData(): Boolean {
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && vpnState.status.isDown()
}
private fun changeOnMobileData(): Boolean {
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && isMobileTunnelDataChangeNeeded()
}
private fun stopOnWifi(): Boolean {
return isWifiActive() && !settings.isTunnelOnWifiEnabled && vpnState.status.isUp()
}
private fun stopOnTrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isUp() && isCurrentSSIDTrusted()
}
private fun startOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isDown() && !isCurrentSSIDTrusted()
}
private fun changeOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isUp() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
}
private fun isWifiTunnelPreferred(): Boolean {
val preferred = preferredWifiTunnel()
val vpnTunnel = vpnState.tunnelConfig
return if (preferred != null && vpnTunnel != null) {
preferred.id == vpnTunnel.id
} else {
true
}
}
// TODO add shutdown on no connectivity
fun asAutoTunnelEvent(): AutoTunnelEvent {
return when {
// ethernet scenarios
startOnEthernet() -> AutoTunnelEvent.Start()
// mobile data scenarios
stopOnMobileData() -> AutoTunnelEvent.Stop(vpnState.tunnelConfig)
startOnMobileData() -> AutoTunnelEvent.Start(tunnels.firstOrNull { it.isMobileDataTunnel })
changeOnMobileData() -> AutoTunnelEvent.Start(preferredMobileDataTunnel())
// wifi scenarios
stopOnWifi() -> AutoTunnelEvent.Stop(vpnState.tunnelConfig)
stopOnTrustedWifi() -> AutoTunnelEvent.Stop(vpnState.tunnelConfig)
startOnUntrustedWifi() -> AutoTunnelEvent.Start(preferredWifiTunnel())
changeOnUntrustedWifi() -> AutoTunnelEvent.Start(preferredWifiTunnel())
else -> AutoTunnelEvent.DoNothing
}
}
private fun isCurrentSSIDTrusted(): Boolean {
return if (settings.isWildcardsEnabled) {
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
} else {
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)
}
}
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return tunnels.firstOrNull {
if (settings.isWildcardsEnabled) {
it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID)
} else {
it.tunnelNetworks.contains(currentNetworkSSID)
}
}
}
}

View File

@ -4,9 +4,8 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -47,7 +46,7 @@ class ShortcutsActivity : ComponentActivity() {
tunnelConfig?.let { tunnelConfig?.let {
when (intent.action) { when (intent.action) {
Action.START.name -> tunnelService.get().startTunnel(it, true) Action.START.name -> tunnelService.get().startTunnel(it, true)
Action.STOP.name -> tunnelService.get().stopTunnel(it) Action.STOP.name -> tunnelService.get().stopTunnel()
else -> Unit else -> Unit
} }
} }
@ -64,6 +63,11 @@ class ShortcutsActivity : ComponentActivity() {
finish() finish()
} }
enum class Action {
START,
STOP,
}
companion object { companion object {
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService" const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService" const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"

View File

@ -72,7 +72,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
val lastActive = appDataRepository.getStartTunnelConfig() val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let { tunnel -> lastActive?.let { tunnel ->
if (tunnel.isActive) { if (tunnel.isActive) {
tunnelService.get().stopTunnel(tunnel) tunnelService.get().stopTunnel()
} else { } else {
tunnelService.get().startTunnel(tunnel, true) tunnelService.get().startTunnel(tunnel, true)
} }

View File

@ -5,11 +5,12 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel { interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean = false): Result<TunnelState>
suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean = false)
suspend fun bounceTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> suspend fun stopTunnel()
suspend fun bounceTunnel()
val vpnState: StateFlow<VpnState> val vpnState: StateFlow<VpnState>

View File

@ -24,6 +24,14 @@ enum class TunnelState {
} }
} }
fun isDown(): Boolean {
return this == DOWN
}
fun isUp(): Boolean {
return this == UP
}
companion object { companion object {
fun from(state: Tunnel.State): TunnelState { fun from(state: Tunnel.State): TunnelState {
return when (state) { return when (state) {

View File

@ -21,11 +21,13 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel import org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@ -53,7 +55,7 @@ constructor(
private var statsJob: Job? = null private var statsJob: Job? = null
private val runningHandle = AtomicBoolean(false) private val mutex = Mutex()
private suspend fun backend(): Any { private suspend fun backend(): Any {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
@ -92,77 +94,58 @@ constructor(
} }
} }
override suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean): Result<TunnelState> { private fun isTunnelAlreadyRunning(tunnelConfig: TunnelConfig): Boolean {
return withContext(ioDispatcher) { val isRunning = tunnelConfig == _vpnState.value.tunnelConfig && _vpnState.value.status.isUp()
if (runningHandle.get() && tunnelConfig == vpnState.value.tunnelConfig) { if (isRunning) Timber.w("Tunnel already running")
Timber.w("Tunnel already running") return isRunning
return@withContext Result.success(vpnState.value.status) }
}
runningHandle.set(true) override suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean) {
onBeforeStart(tunnelConfig) if (tunnelConfig == null) return
val settings = appDataRepository.settings.getSettings() withContext(ioDispatcher) {
if (background || settings.isKernelEnabled) startBackgroundService() mutex.withLock {
setState(tunnelConfig, TunnelState.UP).onSuccess { if (isTunnelAlreadyRunning(tunnelConfig)) return@withContext
updateTunnelState(it) onBeforeStart(background)
}.onFailure { setState(tunnelConfig, TunnelState.UP).onSuccess {
Timber.e(it) startStatsJob()
onStartFailed() if (it.isUp()) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
updateTunnelState(it, tunnelConfig)
}.onFailure {
Timber.e(it)
}
} }
} }
} }
override suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> { override suspend fun stopTunnel() {
return withContext(ioDispatcher) { withContext(ioDispatcher) {
onBeforeStop(tunnelConfig) mutex.withLock {
setState(tunnelConfig, TunnelState.DOWN).onSuccess { if (_vpnState.value.status.isDown()) return@withContext
updateTunnelState(it) with(_vpnState.value) {
}.onFailure { if (tunnelConfig == null) return@withContext
Timber.e(it) setState(tunnelConfig, TunnelState.DOWN).onSuccess {
onStopFailed() updateTunnelState(it, null)
}.also { onStop(tunnelConfig)
stopBackgroundService() stopBackgroundService()
runningHandle.set(false) }.onFailure {
Timber.e(it)
}
}
} }
} }
} }
// use this when we just want to bounce tunnel and not change tunnelConfig active state override suspend fun bounceTunnel() {
override suspend fun bounceTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> { if (_vpnState.value.tunnelConfig == null) return
toggleTunnel(tunnelConfig) val config = _vpnState.value.tunnelConfig
delay(VPN_RESTART_DELAY) stopTunnel()
return toggleTunnel(tunnelConfig) startTunnel(config)
} }
private suspend fun toggleTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> { private suspend fun shutDownActiveTunnel() {
return withContext(ioDispatcher) {
setState(tunnelConfig, TunnelState.TOGGLE).onSuccess {
updateTunnelState(it)
resetBackendStatistics()
}.onFailure {
Timber.e(it)
}
}
}
private suspend fun onStopFailed() {
_vpnState.value.tunnelConfig?.let {
appDataRepository.tunnels.save(it.copy(isActive = true))
}
}
private suspend fun onStartFailed() {
_vpnState.value.tunnelConfig?.let {
appDataRepository.tunnels.save(it.copy(isActive = false))
}
cancelStatsJob()
resetBackendStatistics()
runningHandle.set(false)
}
private suspend fun shutDownActiveTunnel(config: TunnelConfig) {
with(_vpnState.value) { with(_vpnState.value) {
if (status == TunnelState.UP && tunnelConfig != config) { if (status.isUp()) {
tunnelConfig?.let { stopTunnel(it) } stopTunnel()
} }
} }
} }
@ -177,51 +160,35 @@ constructor(
serviceManager.requestTunnelTileUpdate() serviceManager.requestTunnelTileUpdate()
} }
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) { private suspend fun onBeforeStart(background: Boolean) {
shutDownActiveTunnel(tunnelConfig) shutDownActiveTunnel()
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
emitVpnStateConfig(tunnelConfig)
resetBackendStatistics() resetBackendStatistics()
startStatsJob() val settings = appDataRepository.settings.getSettings()
if (background || settings.isKernelEnabled) startBackgroundService()
} }
private suspend fun onBeforeStop(tunnelConfig: TunnelConfig) { private suspend fun onStop(tunnelConfig: TunnelConfig) {
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false)) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
cancelStatsJob() cancelStatsJob()
resetBackendStatistics() resetBackendStatistics()
} }
private fun updateTunnelState(state: TunnelState) { private fun updateTunnelState(state: TunnelState, tunnelConfig: TunnelConfig?) {
_vpnState.tryEmit( _vpnState.update {
_vpnState.value.copy( it.copy(status = state, tunnelConfig = tunnelConfig)
status = state, }
),
)
serviceManager.requestTunnelTileUpdate()
} }
private fun emitBackendStatistics(statistics: TunnelStatistics) { private fun emitBackendStatistics(statistics: TunnelStatistics) {
_vpnState.tryEmit( _vpnState.update {
_vpnState.value.copy( it.copy(statistics = statistics)
statistics = statistics, }
),
)
}
private fun emitVpnStateConfig(tunnelConfig: TunnelConfig) {
_vpnState.tryEmit(
_vpnState.value.copy(
tunnelConfig = tunnelConfig,
),
)
} }
private fun resetBackendStatistics() { private fun resetBackendStatistics() {
_vpnState.tryEmit( _vpnState.update {
_vpnState.value.copy( it.copy(statistics = null)
statistics = null, }
),
)
} }
override suspend fun getState(): TunnelState { override suspend fun getState(): TunnelState {
@ -265,16 +232,21 @@ constructor(
} }
override fun onStateChange(newState: Tunnel.State) { override fun onStateChange(newState: Tunnel.State) {
updateTunnelState(TunnelState.from(newState)) _vpnState.update {
it.copy(status = TunnelState.from(newState))
}
serviceManager.requestTunnelTileUpdate()
} }
override fun onStateChange(state: State) { override fun onStateChange(state: State) {
updateTunnelState(TunnelState.from(state)) _vpnState.update {
it.copy(status = TunnelState.from(state))
}
serviceManager.requestTunnelTileUpdate()
} }
companion object { companion object {
const val STATS_START_DELAY = 1_000L const val STATS_START_DELAY = 1_000L
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val VPN_RESTART_DELAY = 1_000L
} }
} }

View File

@ -150,7 +150,7 @@ class MainActivity : AppCompatActivity() {
navController, navController,
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) }, enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) }, exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
startDestination = (if (appUiState.generalState.isPinLockEnabled == true) Route.Lock else Route.Main), startDestination = (if (appUiState.generalState.isPinLockEnabled) Route.Lock else Route.Main),
) { ) {
composable<Route.Main> { composable<Route.Main> {
MainScreen( MainScreen(

View File

@ -155,7 +155,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context) val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context)
if (intent != null) return vpnActivity.launch(intent) if (intent != null) return vpnActivity.launch(intent)
if (!checked) viewModel.onTunnelStop(tunnel).also { return } if (!checked) viewModel.onTunnelStop().also { return }
viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled) viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled)
} }
@ -249,8 +249,8 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
key = { tunnel -> tunnel.id }, key = { tunnel -> tunnel.id },
) { tunnel -> ) { tunnel ->
val isActive = uiState.tunnels.any { val isActive = uiState.tunnels.any {
it.id == tunnel.id && it.id == uiState.vpnState.tunnelConfig?.id &&
it.isActive uiState.vpnState.status.isUp()
} }
val expanded = uiState.generalState.isTunnelStatsExpanded val expanded = uiState.generalState.isTunnelStatsExpanded
TunnelRowItem( TunnelRowItem(

View File

@ -72,9 +72,9 @@ constructor(
tunnelService.get().startTunnel(tunnelConfig, background) tunnelService.get().startTunnel(tunnelConfig, background)
} }
fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch { fun onTunnelStop() = viewModelScope.launch {
Timber.i("Stopping active tunnel") Timber.i("Stopping active tunnel")
tunnelService.get().stopTunnel(tunnel) tunnelService.get().stopTunnel()
} }
private fun generateQrCodeDefaultName(config: String): String { private fun generateQrCodeDefaultName(config: String): String {