fix(network): make non-VPN network selection robust + show failure reason

This commit is contained in:
klockky
2026-05-16 09:16:52 +03:00
parent 30b46a37b6
commit 6571d18696
3 changed files with 61 additions and 25 deletions
@@ -4,6 +4,7 @@ import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.util.Log
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
@@ -14,6 +15,10 @@ class MainActivity : FlutterActivity() {
private val channelName = "ru.komet.app/vpn_bypass"
private companion object {
const val LOG_TAG = "VpnBypass"
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
@@ -79,19 +84,28 @@ class MainActivity : FlutterActivity() {
return false
}
private data class Candidate(
val network: Network,
val iface: String?,
val transport: String,
val score: Int,
)
// Привязывает процесс к не-VPN сети: Wi-Fi → Ethernet → моб.
// Жёсткий фильтр — только исключение VPN-транспорта; INTERNET/NOT_VPN/
// VALIDATED лишь повышают приоритет (физическая сеть под активным VPN
// часто теряет эти capability, но через неё всё равно можно ходить).
private fun bindToNonVpnNetwork(): Map<String, Any?> {
val cm = connectivityManager()
var best: Network? = null
var bestIface: String? = null
var bestTransport: String? = null
var bestScore = -1
val networks = cm.allNetworks
val candidates = ArrayList<Candidate>()
for (network in cm.allNetworks) {
val caps = cm.getNetworkCapabilities(network) ?: continue
if (!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) continue
for (network in networks) {
val caps = cm.getNetworkCapabilities(network)
val iface = cm.getLinkProperties(network)?.interfaceName
Log.i(LOG_TAG, "net=$network iface=$iface caps=$caps")
if (caps == null) continue
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) continue
if (!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) continue
val baseScore = when {
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 3
@@ -104,27 +118,42 @@ class MainActivity : FlutterActivity() {
2 -> "ethernet"
else -> "cellular"
}
val internet =
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
val notVpn =
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
val validated =
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
val score = baseScore * 2 + if (validated) 1 else 0
if (score > bestScore) {
bestScore = score
best = network
bestIface = cm.getLinkProperties(network)?.interfaceName
bestTransport = transport
}
val score = baseScore * 8 +
(if (internet) 4 else 0) +
(if (notVpn) 2 else 0) +
(if (validated) 1 else 0)
candidates.add(Candidate(network, iface, transport, score))
}
val chosen = best
?: return mapOf("bound" to false, "reason" to "no_non_vpn_network")
candidates.sortByDescending { it.score }
Log.i(LOG_TAG, "candidates=${candidates.map { "${it.iface}:${it.score}" }}")
val ok = cm.bindProcessToNetwork(chosen)
return mapOf(
"bound" to ok,
"interface" to bestIface,
"transport" to bestTransport,
"reason" to if (ok) null else "bind_failed",
)
if (candidates.isEmpty()) {
return mapOf(
"bound" to false,
"reason" to "no_non_vpn_network(scanned=${networks.size})",
)
}
for (c in candidates) {
if (cm.bindProcessToNetwork(c.network)) {
Log.i(LOG_TAG, "bound to ${c.iface} (${c.transport})")
return mapOf(
"bound" to true,
"interface" to c.iface,
"transport" to c.transport,
"reason" to null,
)
}
Log.w(LOG_TAG, "bindProcessToNetwork failed for ${c.iface}")
}
return mapOf("bound" to false, "reason" to "bind_failed")
}
private fun unbindNetwork(): Map<String, Any?> {
+6
View File
@@ -96,6 +96,12 @@ class VpnBypassService {
reason: 'no_plugin',
);
}
if (result.bound) {
logger.i('VPN bypass: привязано к ${result.boundInterface} '
'(${result.transport})');
} else {
logger.w('VPN bypass: обойти не удалось (${result.reason})');
}
_eventController.add(result);
return result;
}
+2 -1
View File
@@ -161,7 +161,8 @@ class KometAppState extends State<KometApp> {
final msg = r.bound
? 'Соединение через VPN не работает — '
'используется ${r.boundInterface ?? r.transport ?? 'прямое подключение'}'
: 'Соединение через VPN не работает, обойти не удалось';
: 'Соединение через VPN не работает, обойти не удалось'
'${r.reason != null ? ' (${r.reason})' : ''}';
final now = DateTime.now();
if (msg == _lastVpnNotice &&