fix(network): make non-VPN network selection robust + show failure reason
This commit is contained in:
@@ -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?> {
|
||||
|
||||
@@ -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
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user