feat!: tun monitoring, move ping restarts to auto-tunnel w/recovery (#885)

This is a big one.. oops.

Main changes:
- Make ping monitor more robust and global, with ping target overrides of the default cloudflare fallback target per tunnel (for full tunnels, otherwise we ping the internal tun ip)
- Include ping restart recovery to prevent tun being down if dns failures happen after a bounce
- Ping monitoring itself remains per tunnel and works without auto tunnel active, but moves the restart feature back to be managed by and integrated with auto tunnel to prevent inconsistencies and conflicts
- Ping statistics can be optionally included to be displayed with tun statistics
- Adds the beginnings of monitoring logs for handshake and data packet failures for userspace tuns (to be incorporated with restarts/tun status later)
- Improve tun error notifications, adds ping restart notifications
- Major refactor of auto tunnel logic to make it more modular and extensible for new auto tunnel conditions
- A bunch of other stuff..
This commit is contained in:
Zane Schepke
2025-08-07 18:19:36 -04:00
committed by GitHub
parent 230cd0adb8
commit 38ecb0b66b
108 changed files with 2415 additions and 1308 deletions
@@ -1,39 +0,0 @@
package com.zaneschepke.networkmonitor
import android.net.Network
import android.net.NetworkCapabilities
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
// keep track of the currently active network(s)
class ActiveWifiStateManager {
private val _stateFlow =
MutableStateFlow(linkedMapOf<String, Pair<Network?, NetworkCapabilities?>>())
@Synchronized
fun put(key: String, value: Pair<Network?, NetworkCapabilities?>) {
_stateFlow.update { currentMap ->
linkedMapOf(*currentMap.toList().toTypedArray()).apply { put(key, value) }
}
}
@Synchronized
fun remove(key: String) {
_stateFlow.update { currentMap ->
linkedMapOf(*currentMap.toList().toTypedArray()).apply { remove(key) }
}
}
fun isEmpty(): Boolean = _stateFlow.value.isEmpty()
fun getLatestValue(): Pair<Network?, NetworkCapabilities?>? {
return _stateFlow.value.entries.lastOrNull()?.value
}
@Synchronized
fun clear() {
_stateFlow.update { currentMap ->
linkedMapOf(*currentMap.toList().toTypedArray()).apply { clear() }
}
}
}
@@ -17,9 +17,12 @@ import androidx.core.content.ContextCompat
import com.wireguard.android.util.RootShell
import com.zaneschepke.networkmonitor.shizuku.ShizukuShell
import com.zaneschepke.networkmonitor.util.*
import kotlinx.coroutines.*
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
class AndroidNetworkMonitor(
@@ -64,7 +67,8 @@ class AndroidNetworkMonitor(
appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
// Track active Wi-Fi networks, their capabilities, and last active network ID
private val activeWifiNetworks = ActiveWifiStateManager()
private val activeWifiNetworks =
ConcurrentHashMap<String, Pair<Network?, NetworkCapabilities?>>()
private val permissionsChangedFlow = MutableStateFlow(false)
@@ -193,7 +197,7 @@ class AndroidNetworkMonitor(
fun handleOnWifiAvailable(network: Network) {
Timber.d("Wi-Fi onAvailable: network=$network")
activeWifiNetworks.put(network.toString(), Pair(network, null))
activeWifiNetworks[network.toString()] = Pair(network, null)
trySend(TransportEvent.Available(network, detectionMethod))
}
@@ -202,7 +206,7 @@ class AndroidNetworkMonitor(
networkCapabilities: NetworkCapabilities,
) {
Timber.d("Wi-Fi onCapabilitiesChanged: network=$network")
activeWifiNetworks.put(network.toString(), Pair(network, networkCapabilities))
activeWifiNetworks[network.toString()] = Pair(network, networkCapabilities)
trySend(
TransportEvent.CapabilitiesChanged(network, networkCapabilities, detectionMethod)
)
@@ -418,7 +422,7 @@ class AndroidNetworkMonitor(
.also { Timber.d("Connectivity Status: $it") }
}
.distinctUntilChanged()
.shareIn(applicationScope, SharingStarted.WhileSubscribed(5000), replay = 1)
.shareIn(applicationScope, SharingStarted.Eagerly, replay = 1)
override fun checkPermissionsAndUpdateState() {
val action = actionPermissionCheck