fix: auto tunnel bugs

Fixes auto tunnel bug that can happen on startup
Fixes auto tunnel bug that didn't allow manual toggle override of tunnel while auto tunnel is active
Add basic foreground persistent notifications (optionally turned off via android settings)
This commit is contained in:
Zane Schepke 2024-11-07 23:03:00 -05:00
parent 7d810c7c3d
commit cab2945930
12 changed files with 100 additions and 70 deletions

View File

@ -9,3 +9,11 @@ annotation class Kernel
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Userspace
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TunnelShell
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppShell

View File

@ -25,9 +25,18 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TunnelModule {
@Provides
@Singleton
fun provideRootShell(@ApplicationContext context: Context): RootShell {
@TunnelShell
fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
@AppShell
fun provideAppRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@ -40,14 +49,14 @@ class TunnelModule {
@Provides
@Singleton
@Userspace
fun provideUserspaceBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
fun provideUserspaceBackend(@ApplicationContext context: Context, @TunnelShell rootShell: RootShell): Backend {
return GoBackend(context, RootTunnelActionHandler(rootShell))
}
@Provides
@Singleton
@Kernel
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
fun provideKernelBackend(@ApplicationContext context: Context, @TunnelShell rootShell: RootShell): Backend {
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell), RootTunnelActionHandler(rootShell))
}

View File

@ -12,6 +12,7 @@ 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.network.EthernetService
@ -25,7 +26,6 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import com.zaneschepke.wireguardautotunnel.util.extensions.isDown
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning
import dagger.hilt.android.AndroidEntryPoint
@ -34,7 +34,6 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
@ -50,6 +49,7 @@ class AutoTunnelService : LifecycleService() {
private val foregroundId = 122
@Inject
@AppShell
lateinit var rootShell: Provider<RootShell>
@Inject
@ -143,7 +143,7 @@ class AutoTunnelService : LifecycleService() {
super.onDestroy()
}
private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) {
private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) {
val notification =
notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id),
@ -209,28 +209,16 @@ class AutoTunnelService : LifecycleService() {
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Mobile data connection")
autoTunnelStateFlow.update {
it.copy(
isMobileDataConnected = true,
)
}
emitMobileDataConnected(true)
}
is NetworkStatus.CapabilitiesChanged -> {
autoTunnelStateFlow.update {
it.copy(
isMobileDataConnected = true,
)
}
emitMobileDataConnected(true)
Timber.i("Mobile data capabilities changed")
}
is NetworkStatus.Unavailable -> {
autoTunnelStateFlow.update {
it.copy(
isMobileDataConnected = false,
)
}
emitMobileDataConnected(false)
Timber.i("Lost mobile data connection")
}
}
@ -283,6 +271,7 @@ class AutoTunnelService : LifecycleService() {
old.map { it.isActive } != new.map { it.isActive }
},
) { settings, tunnels ->
Timber.d("Tunnels or settings changed!")
autoTunnelStateFlow.value.copy(
settings = settings,
tunnels = tunnels,
@ -299,7 +288,7 @@ class AutoTunnelService : LifecycleService() {
Timber.i("Starting vpn state watcher")
withContext(ioDispatcher) {
tunnelService.get().vpnState.distinctUntilChanged { old, new ->
old.tunnelConfig == new.tunnelConfig && old.status == new.status
old.tunnelConfig?.id == new.tunnelConfig?.id
}.collect { state ->
autoTunnelStateFlow.update {
it.copy(vpnState = state)
@ -367,7 +356,7 @@ class AutoTunnelService : LifecycleService() {
mobileDataJob = null
}
private fun updateEthernet(connected: Boolean) {
private fun emitEthernetConnected(connected: Boolean) {
autoTunnelStateFlow.update {
it.copy(
isEthernetConnected = connected,
@ -375,7 +364,7 @@ class AutoTunnelService : LifecycleService() {
}
}
private fun updateWifi(connected: Boolean) {
private fun emitWifiConnected(connected: Boolean) {
autoTunnelStateFlow.update {
it.copy(
isWifiConnected = connected,
@ -383,6 +372,22 @@ class AutoTunnelService : LifecycleService() {
}
}
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")
@ -390,16 +395,16 @@ class AutoTunnelService : LifecycleService() {
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Ethernet connection")
updateEthernet(true)
emitEthernetConnected(true)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Ethernet capabilities changed")
updateEthernet(true)
emitEthernetConnected(true)
}
is NetworkStatus.Unavailable -> {
updateEthernet(false)
emitEthernetConnected(false)
Timber.i("Lost Ethernet connection")
}
}
@ -414,12 +419,12 @@ class AutoTunnelService : LifecycleService() {
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Wi-Fi connection")
updateWifi(true)
emitWifiConnected(true)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed")
updateWifi(true)
emitWifiConnected(true)
val ssid = getWifiSSID(status.networkCapabilities)
ssid?.let { name ->
if (name.contains(Constants.UNREADABLE_SSID)) {
@ -428,16 +433,12 @@ class AutoTunnelService : LifecycleService() {
Timber.i("Detected valid SSID")
}
appDataRepository.appState.setCurrentSsid(name)
autoTunnelStateFlow.update {
it.copy(
currentNetworkSSID = name,
)
}
emitWifiSSID(name)
} ?: Timber.w("Failed to read ssid")
}
is NetworkStatus.Unavailable -> {
updateWifi(false)
emitWifiConnected(false)
Timber.i("Lost Wi-Fi connection")
}
}
@ -461,17 +462,17 @@ class AutoTunnelService : LifecycleService() {
private suspend fun handleNetworkEventChanges() {
withContext(ioDispatcher) {
Timber.i("Starting network event watcher")
autoTunnelStateFlow.collectLatest { watcherState ->
autoTunnelStateFlow.collect { watcherState ->
val autoTunnel = "Auto-tunnel watcher"
Timber.d("New watcher state!")
// delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY)
val activeTunnel = watcherState.vpnState.tunnelConfig
val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel()
val isTunnelDown = tunnelService.get().getState() == TunnelState.DOWN
when {
watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
if (watcherState.vpnState.isDown()) {
if (isTunnelDown) {
defaultTunnel?.let {
tunnelService.get().startTunnel(it)
}
@ -483,7 +484,7 @@ class AutoTunnelService : LifecycleService() {
val mobileDataTunnel = getMobileDataTunnel()
val tunnel =
mobileDataTunnel ?: defaultTunnel
if (watcherState.vpnState.isDown() || activeTunnel?.isMobileDataTunnel == false) {
if (isTunnelDown || activeTunnel?.isMobileDataTunnel == false) {
tunnel?.let {
tunnelService.get().startTunnel(it)
}
@ -492,7 +493,7 @@ class AutoTunnelService : LifecycleService() {
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
if (!watcherState.vpnState.isDown()) {
if (!isTunnelDown) {
activeTunnel?.let {
tunnelService.get().stopTunnel(it)
}
@ -502,20 +503,20 @@ class AutoTunnelService : LifecycleService() {
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("Untrusted wifi condition met")
if (activeTunnel == null || watcherState.isCurrentSSIDActiveTunnelNetwork() == false ||
watcherState.vpnState.isDown()
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 (watcherState.vpnState.isDown() || activeTunnel?.id != it.id) {
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 || watcherState.vpnState.isDown()) {
if (default?.name != tunnelService.get().name || isTunnelDown) {
default?.let {
tunnelService.get().startTunnel(it)
}
@ -528,22 +529,22 @@ class AutoTunnelService : LifecycleService() {
Timber.i(
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
)
if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on wifi condition met, turning vpn off",
)
if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i(
"$autoTunnel - tunnel off on no connectivity met, turning vpn off",
)
if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
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")

View File

@ -27,12 +27,14 @@ class ServiceManager
companion object : SingletonHolder<ServiceManager, Context>(::ServiceManager)
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
val intent = Intent(context, cls)
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
runCatching {
val intent = Intent(context, cls)
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}.onFailure { Timber.e(it) }
}
suspend fun startAutoTunnel(background: Boolean) {

View File

@ -62,7 +62,7 @@ class TunnelBackgroundService : LifecycleService() {
return notificationService.createNotification(
getString(R.string.vpn_channel_id),
getString(R.string.vpn_channel_name),
getString(R.string.tunnel_start_text),
getString(R.string.tunnel_running),
description = "",
)
}

View File

@ -25,6 +25,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Provider
@ -52,6 +53,8 @@ constructor(
private var statsJob: Job? = null
private val runningHandle = AtomicBoolean(false)
private suspend fun backend(): Any {
val settings = appDataRepository.settings.getSettings()
if (settings.isKernelEnabled) return kernelBackend.get()
@ -91,8 +94,14 @@ constructor(
override suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean): Result<TunnelState> {
return withContext(ioDispatcher) {
if (runningHandle.get() == true && tunnelConfig == vpnState.value.tunnelConfig) {
Timber.w("Tunnel already running")
return@withContext Result.success(vpnState.value.status)
}
runningHandle.set(true)
onBeforeStart(tunnelConfig)
if (background) startBackgroundService()
val settings = appDataRepository.settings.getSettings()
if (background || settings.isKernelEnabled) startBackgroundService()
setState(tunnelConfig, TunnelState.UP).onSuccess {
emitTunnelState(it)
}.onFailure {
@ -112,6 +121,7 @@ constructor(
onStopFailed()
}.also {
stopBackgroundService()
runningHandle.set(false)
}
}
}
@ -146,6 +156,7 @@ constructor(
}
cancelStatsJob()
resetBackendStatistics()
runningHandle.set(false)
}
private suspend fun shutDownActiveTunnel(config: TunnelConfig) {

View File

@ -39,6 +39,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@ -60,6 +61,7 @@ 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
@OptIn(ExperimentalFoundationApi::class)
@Composable
@ -213,7 +215,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
)
LazyColumn(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
verticalArrangement = Arrangement.spacedBy(5.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize().padding(it)

View File

@ -9,6 +9,7 @@ import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.FileUtils
@ -29,7 +30,7 @@ class SettingsViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
private val rootShell: Provider<RootShell>,
@AppShell private val rootShell: Provider<RootShell>,
private val fileUtils: FileUtils,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {

View File

@ -6,6 +6,7 @@ import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.StringValue
@ -23,7 +24,7 @@ class AutoTunnelViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
private val rootShell: Provider<RootShell>,
@AppShell private val rootShell: Provider<RootShell>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {

View File

@ -21,7 +21,6 @@ object Constants {
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
const val SUBSCRIPTION_TIMEOUT = 5_000L
const val FOCUS_REQUEST_DELAY = 500L
const val TRANSITION_ANIMATION_TIME = 200

View File

@ -4,8 +4,6 @@ import androidx.compose.ui.graphics.Color
import com.wireguard.android.util.RootShell
import com.wireguard.config.Peer
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
@ -23,10 +21,6 @@ fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
}
fun VpnState.isDown(): Boolean {
return this.status == TunnelState.DOWN
}
fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
// TODO add never connected status after duration
return this.latestHandshakeSeconds().let {

View File

@ -225,4 +225,6 @@
<string name="kernel_not_supported">Kernel not supported</string>
<string name="start_auto">Start auto-tunnel</string>
<string name="stop_auto">Stop auto-tunnel</string>
<string name="tunnel_running">Tunnel running</string>
<string name="monitoring_state_changes">Monitoring state changes</string>
</resources>