mirror of
https://github.com/xtclovver/RKNHardering.git
synced 2026-04-28 04:29:51 +00:00
fix: 404 в TUN active probe + уборка debug информации из UI. #37
Some checks are pending
CI / build (push) Waiting to run
Some checks are pending
CI / build (push) Waiting to run
This commit is contained in:
parent
0219391b22
commit
fbf28868e3
12 changed files with 285 additions and 123 deletions
|
|
@ -601,7 +601,6 @@ object BypassChecker {
|
|||
)
|
||||
}
|
||||
val usedTransportOnlyFallback = addTransportOnlyFinding(context, result, findings)
|
||||
addDebugTunProbeFindings(context, result, findings)
|
||||
|
||||
if (result.activeNetworkIsVpn == false) {
|
||||
when {
|
||||
|
|
@ -843,40 +842,6 @@ object BypassChecker {
|
|||
return true
|
||||
}
|
||||
|
||||
private fun addDebugTunProbeFindings(
|
||||
context: Context,
|
||||
result: UnderlyingNetworkProber.ProbeResult,
|
||||
findings: MutableList<Finding>,
|
||||
) {
|
||||
val diagnostics = result.tunProbeDiagnostics ?: return
|
||||
diagnostics.vpnPath?.let { vpnPath ->
|
||||
findings.add(
|
||||
Finding(
|
||||
description = TunProbeDiagnosticsFormatter.formatUiSummary(
|
||||
context = context,
|
||||
pathLabel = context.getString(R.string.checker_tun_probe_path_vpn),
|
||||
modeOverride = diagnostics.modeOverride,
|
||||
path = vpnPath,
|
||||
),
|
||||
isInformational = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
diagnostics.underlyingPath?.let { underlyingPath ->
|
||||
findings.add(
|
||||
Finding(
|
||||
description = TunProbeDiagnosticsFormatter.formatUiSummary(
|
||||
context = context,
|
||||
pathLabel = context.getString(R.string.checker_tun_probe_path_underlying),
|
||||
modeOverride = diagnostics.modeOverride,
|
||||
path = underlyingPath,
|
||||
),
|
||||
isInformational = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveProxyOwner(context: Context, proxyEndpoint: ProxyEndpoint): ProxyOwnerMatch {
|
||||
val listeners = LocalSocketInspector.collect(context, protocols = setOf("tcp", "tcp6"))
|
||||
return matchProxyOwner(proxyEndpoint, listeners)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import android.net.Proxy
|
|||
import android.net.ProxyInfo
|
||||
import android.os.Build
|
||||
import com.notcvnt.rknhardering.R
|
||||
import com.notcvnt.rknhardering.TunProbeDiagnosticsFormatter
|
||||
import com.notcvnt.rknhardering.model.CategoryResult
|
||||
import com.notcvnt.rknhardering.model.EvidenceConfidence
|
||||
import com.notcvnt.rknhardering.model.EvidenceItem
|
||||
|
|
@ -80,7 +79,6 @@ object DirectSignsChecker {
|
|||
tunActiveProbeResult
|
||||
?.takeIf { it.vpnActive }
|
||||
?.let { result ->
|
||||
addDebugTunProbeFinding(context, result, findings)
|
||||
val tunActiveProbeOutcome = reportTunActiveProbe(context, result, findings, evidence)
|
||||
detected = detected || tunActiveProbeOutcome.detected
|
||||
needsReview = needsReview || tunActiveProbeOutcome.needsReview
|
||||
|
|
@ -693,16 +691,15 @@ object DirectSignsChecker {
|
|||
continue
|
||||
}
|
||||
val comparison = target.comparison
|
||||
val transportOnly = comparison?.usedCurlCompatibleFallback() == true &&
|
||||
comparison.curlCompatible.transportDiagnostics.resolveStrategy != TunProbeResolveStrategy.KOTLIN_INJECTED
|
||||
findings.add(
|
||||
Finding(
|
||||
description = context.getString(
|
||||
when {
|
||||
transportOnly -> R.string.checker_bypass_tun_probe_success_transport_only
|
||||
comparison?.usedCurlCompatibleFallback() == true -> R.string.checker_bypass_tun_probe_success_curl_compatible
|
||||
else -> R.string.checker_bypass_tun_probe_success
|
||||
if (comparison?.usedCurlCompatibleFallback() == true) {
|
||||
R.string.checker_bypass_tun_probe_success_proxy_target
|
||||
} else {
|
||||
R.string.checker_bypass_tun_probe_success_direct_target
|
||||
},
|
||||
targetGroupLabel(context, target.targetGroup),
|
||||
vpnIp,
|
||||
),
|
||||
isInformational = true,
|
||||
|
|
@ -712,6 +709,8 @@ object DirectSignsChecker {
|
|||
|
||||
val hasDnsPathMismatch = comparison?.dnsPathMismatch == true
|
||||
if (hasDnsPathMismatch) {
|
||||
val transportOnly = comparison.usedCurlCompatibleFallback() &&
|
||||
comparison.curlCompatible.transportDiagnostics.resolveStrategy != TunProbeResolveStrategy.KOTLIN_INJECTED
|
||||
val confidence = if (transportOnly) EvidenceConfidence.MEDIUM else EvidenceConfidence.HIGH
|
||||
evidence.add(
|
||||
EvidenceItem(
|
||||
|
|
@ -738,32 +737,19 @@ object DirectSignsChecker {
|
|||
return SignalOutcome(detected = detected, needsReview = needsReview)
|
||||
}
|
||||
|
||||
private fun addDebugTunProbeFinding(
|
||||
context: Context,
|
||||
result: UnderlyingNetworkProber.ProbeResult,
|
||||
findings: MutableList<Finding>,
|
||||
) {
|
||||
val diagnostics = result.tunProbeDiagnostics ?: return
|
||||
val vpnPath = diagnostics.vpnPath ?: return
|
||||
findings.add(
|
||||
Finding(
|
||||
description = TunProbeDiagnosticsFormatter.formatUiSummary(
|
||||
context = context,
|
||||
pathLabel = context.getString(R.string.checker_tun_probe_path_vpn),
|
||||
modeOverride = diagnostics.modeOverride,
|
||||
path = vpnPath,
|
||||
),
|
||||
isInformational = true,
|
||||
source = EvidenceSource.TUN_ACTIVE_PROBE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun isKnownProxyPort(port: String?): Boolean {
|
||||
val value = port?.toIntOrNull() ?: return false
|
||||
return value in KNOWN_PROXY_PORTS || KNOWN_PROXY_PORT_RANGES.any { value in it }
|
||||
}
|
||||
|
||||
private fun targetGroupLabel(
|
||||
context: Context,
|
||||
targetGroup: com.notcvnt.rknhardering.model.TargetGroup,
|
||||
): String = when (targetGroup) {
|
||||
com.notcvnt.rknhardering.model.TargetGroup.RU -> context.getString(R.string.ip_channels_target_ru)
|
||||
com.notcvnt.rknhardering.model.TargetGroup.NON_RU -> context.getString(R.string.ip_channels_target_non_ru)
|
||||
}
|
||||
|
||||
private fun Throwable.renderMessage(): String {
|
||||
return message?.takeIf { it.isNotBlank() } ?: javaClass.simpleName
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ object IfconfigClient {
|
|||
IpEndpointSpec("https://ifconfig.me/ip", IpEndpointFamilyHint.IPV4),
|
||||
IpEndpointSpec("https://checkip.amazonaws.com", IpEndpointFamilyHint.IPV4),
|
||||
IpEndpointSpec("https://ip.mail.ru", IpEndpointFamilyHint.IPV4),
|
||||
IpEndpointSpec("https://api-ipv4.ip.sb/ip", IpEndpointFamilyHint.IPV4),
|
||||
IpEndpointSpec("https://api4.ipify.org", IpEndpointFamilyHint.IPV4),
|
||||
IpEndpointSpec("https://api6.ipify.org", IpEndpointFamilyHint.IPV6),
|
||||
)
|
||||
|
|
@ -87,13 +88,13 @@ object IfconfigClient {
|
|||
resolverConfig: DnsResolverConfig = DnsResolverConfig.system(),
|
||||
modeOverride: TunProbeModeOverride = TunProbeModeOverride.AUTO,
|
||||
collectTrace: Boolean = false,
|
||||
targetHost: String? = null,
|
||||
targetUrls: List<String>? = null,
|
||||
okHttpRetryCount: Int = ResolverNetworkStack.OKHTTP_RETRY_COUNT,
|
||||
nativeCurlRetryCount: Int = ResolverNetworkStack.NATIVE_CURL_RETRY_COUNT,
|
||||
executionContext: ScanExecutionContext = ScanExecutionContext.currentOrDefault(),
|
||||
): PublicIpNetworkComparison = withContext(Dispatchers.IO) {
|
||||
val endpoints = if (targetHost != null) {
|
||||
listOf(IpEndpointSpec("https://$targetHost", IpEndpointFamilyHint.IPV4))
|
||||
val endpoints = if (!targetUrls.isNullOrEmpty()) {
|
||||
targetUrls.map(::IpEndpointSpec)
|
||||
} else {
|
||||
ENDPOINTS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ data class PerTargetProbe(
|
|||
val error: String? = null,
|
||||
)
|
||||
|
||||
private data class ProbeTarget(
|
||||
val displayHost: String,
|
||||
val urls: List<String>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Detects whether a non-VPN (underlying) network is reachable from this app.
|
||||
*
|
||||
|
|
@ -51,7 +56,7 @@ object UnderlyingNetworkProber {
|
|||
DnsResolverConfig,
|
||||
Boolean,
|
||||
TunProbeModeOverride,
|
||||
String?,
|
||||
List<String>?,
|
||||
) -> PublicIpNetworkComparison = ::fetchIpViaNetworkComparison,
|
||||
)
|
||||
|
||||
|
|
@ -77,16 +82,39 @@ object UnderlyingNetworkProber {
|
|||
val activeNetworkIsVpn: Boolean? = null,
|
||||
val tunProbeDiagnostics: TunProbeDiagnostics? = null,
|
||||
) {
|
||||
val vpnIp: String? get() = ruTarget.vpnIp ?: nonRuTarget.vpnIp
|
||||
val underlyingIp: String? get() = ruTarget.directIp ?: nonRuTarget.directIp
|
||||
private fun preferredComparisonTarget(): PerTargetProbe? {
|
||||
val candidates = listOf(nonRuTarget, ruTarget).filter {
|
||||
it.vpnIp != null || it.directIp != null
|
||||
}
|
||||
return candidates.firstOrNull { it.vpnIp != null && it.directIp != null && it.vpnIp != it.directIp }
|
||||
?: candidates.firstOrNull { it.targetGroup == com.notcvnt.rknhardering.model.TargetGroup.NON_RU }
|
||||
?: candidates.firstOrNull()
|
||||
}
|
||||
|
||||
val vpnIp: String? get() = preferredComparisonTarget()?.vpnIp
|
||||
val underlyingIp: String? get() = preferredComparisonTarget()?.directIp
|
||||
val vpnIpComparison: PublicIpNetworkComparison?
|
||||
get() = ruTarget.comparison ?: nonRuTarget.comparison
|
||||
get() = preferredComparisonTarget()?.comparison ?: ruTarget.comparison ?: nonRuTarget.comparison
|
||||
val underlyingIpComparison: PublicIpNetworkComparison?
|
||||
get() = ruTarget.comparison ?: nonRuTarget.comparison
|
||||
get() = preferredComparisonTarget()?.comparison ?: ruTarget.comparison ?: nonRuTarget.comparison
|
||||
}
|
||||
|
||||
private const val RU_PROBE_HOST = "ifconfig.yandex.ru"
|
||||
private const val NON_RU_PROBE_HOST = "api.ipify.org"
|
||||
private val RU_PROBE_TARGET = ProbeTarget(
|
||||
displayHost = "ipv4-internet.yandex.net",
|
||||
urls = listOf(
|
||||
"https://ipv4-internet.yandex.net/api/v0/ip",
|
||||
"https://ip.mail.ru",
|
||||
),
|
||||
)
|
||||
private val NON_RU_PROBE_TARGET = ProbeTarget(
|
||||
displayHost = "api-ipv4.ip.sb",
|
||||
urls = listOf(
|
||||
"https://api-ipv4.ip.sb/ip",
|
||||
"https://checkip.amazonaws.com",
|
||||
"https://ifconfig.me/ip",
|
||||
"https://api4.ipify.org",
|
||||
),
|
||||
)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun probe(
|
||||
|
|
@ -130,26 +158,26 @@ object UnderlyingNetworkProber {
|
|||
resolverConfig,
|
||||
debugEnabled,
|
||||
modeOverride,
|
||||
RU_PROBE_HOST,
|
||||
RU_PROBE_TARGET.urls,
|
||||
)
|
||||
val nonRuVpnComparison = dependencies.comparisonFetcher(
|
||||
vpnNetwork,
|
||||
resolverConfig,
|
||||
debugEnabled,
|
||||
modeOverride,
|
||||
NON_RU_PROBE_HOST,
|
||||
NON_RU_PROBE_TARGET.urls,
|
||||
)
|
||||
val vpnError = ruVpnComparison.selectedError ?: nonRuVpnComparison.selectedError
|
||||
|
||||
val ruVpnProbe = PerTargetProbe(
|
||||
targetHost = RU_PROBE_HOST,
|
||||
targetHost = RU_PROBE_TARGET.displayHost,
|
||||
targetGroup = com.notcvnt.rknhardering.model.TargetGroup.RU,
|
||||
vpnIp = ruVpnComparison.selectedIp,
|
||||
comparison = ruVpnComparison,
|
||||
error = ruVpnComparison.selectedError,
|
||||
)
|
||||
val nonRuVpnProbe = PerTargetProbe(
|
||||
targetHost = NON_RU_PROBE_HOST,
|
||||
targetHost = NON_RU_PROBE_TARGET.displayHost,
|
||||
targetGroup = com.notcvnt.rknhardering.model.TargetGroup.NON_RU,
|
||||
vpnIp = nonRuVpnComparison.selectedIp,
|
||||
comparison = nonRuVpnComparison,
|
||||
|
|
@ -199,7 +227,7 @@ object UnderlyingNetworkProber {
|
|||
resolverConfig,
|
||||
debugEnabled,
|
||||
modeOverride,
|
||||
RU_PROBE_HOST,
|
||||
RU_PROBE_TARGET.urls,
|
||||
)
|
||||
ruUnderlyingComparison = ruResult
|
||||
ruUnderlyingIp = ruResult.selectedIp
|
||||
|
|
@ -211,7 +239,7 @@ object UnderlyingNetworkProber {
|
|||
resolverConfig,
|
||||
debugEnabled,
|
||||
modeOverride,
|
||||
NON_RU_PROBE_HOST,
|
||||
NON_RU_PROBE_TARGET.urls,
|
||||
)
|
||||
nonRuUnderlyingComparison = nonRuResult
|
||||
nonRuUnderlyingIp = nonRuResult.selectedIp
|
||||
|
|
@ -228,12 +256,12 @@ object UnderlyingNetworkProber {
|
|||
val ruTarget = ruVpnProbe.copy(
|
||||
directIp = ruUnderlyingIp,
|
||||
comparison = ruUnderlyingComparison ?: ruVpnComparison,
|
||||
error = if (ruUnderlyingIp == null && ruUnderlyingError != null) ruUnderlyingError else ruVpnComparison.selectedError,
|
||||
error = ruVpnComparison.selectedError,
|
||||
)
|
||||
val nonRuTarget = nonRuVpnProbe.copy(
|
||||
directIp = nonRuUnderlyingIp,
|
||||
comparison = nonRuUnderlyingComparison ?: nonRuVpnComparison,
|
||||
error = if (nonRuUnderlyingIp == null && nonRuUnderlyingError != null) nonRuUnderlyingError else nonRuVpnComparison.selectedError,
|
||||
error = nonRuVpnComparison.selectedError,
|
||||
)
|
||||
|
||||
ProbeResult(
|
||||
|
|
@ -288,7 +316,7 @@ object UnderlyingNetworkProber {
|
|||
resolverConfig: DnsResolverConfig,
|
||||
debugEnabled: Boolean,
|
||||
modeOverride: TunProbeModeOverride,
|
||||
targetHost: String? = null,
|
||||
targetUrls: List<String>? = null,
|
||||
): PublicIpNetworkComparison {
|
||||
val fallbackBinding = networkSnapshot.interfaceName
|
||||
?.takeIf { it.isNotBlank() }
|
||||
|
|
@ -300,7 +328,7 @@ object UnderlyingNetworkProber {
|
|||
resolverConfig = resolverConfig,
|
||||
modeOverride = modeOverride,
|
||||
collectTrace = debugEnabled,
|
||||
targetHost = targetHost,
|
||||
targetUrls = targetUrls,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -449,8 +449,12 @@
|
|||
</plurals>
|
||||
<string name="checker_bypass_vpn_not_active">Underlying network: VPN فعال نیست، این بررسی لازم نیست</string>
|
||||
<string name="checker_bypass_vpn_network_binding">VPN network binding: برنامه هم به مسیر پیشفرض غیر VPN و هم به VPN Network دسترسی دارد (VPN IP: %1$s, IP پیشفرض: %2$s)</string>
|
||||
<string name="checker_bypass_tun_probe_success">TUN active probe: درخواست از طریق VPN Network، IP %1$s را برگرداند (TUN یک اینترفیس زنده و قابل مسیریابی است)</string>
|
||||
<string name="checker_bypass_tun_probe_success_transport_only">TUN active probe: مسیر transport-only سازگار با curl، IP %1$s را برگرداند (SO_BINDTODEVICE + DNS سیستم)</string>
|
||||
<string name="checker_bypass_tun_probe_success">کاوش فعال TUN: آیپی %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_direct">کاوش فعال TUN، مستقیم: آیپی %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_proxy">کاوش فعال TUN، پراکسی: آیپی %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_direct_target">کاوش فعال TUN، %1$s، مستقیم: آیپی %2$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_proxy_target">کاوش فعال TUN، %1$s، از طریق پراکسی: آیپی %2$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_transport_only">کاوش فعال TUN: آیپی %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_failure">TUN active probe: درخواست از طریق VPN Network در دسترس نیست (اینترفیس مسدود یا filtered است)</string>
|
||||
<string name="checker_bypass_tun_probe_failure_reason">بررسی فعال TUN: درخواست از طریق VPN Network با خطا شکست خورد (%1$s)</string>
|
||||
<string name="checker_bypass_tun_probe_dns_mismatch">بررسی فعال TUN: strict same-path شکست خورد (%1$s)، اما مسیر transport-only سازگار با curl موفق شد؛ احتمال mismatch در مسیر DNS وجود دارد</string>
|
||||
|
|
@ -471,7 +475,7 @@
|
|||
<string name="checker_tun_probe_status_succeeded">succeeded</string>
|
||||
<string name="checker_tun_probe_status_failed">failed</string>
|
||||
<string name="checker_tun_probe_status_skipped">skipped</string>
|
||||
<string name="checker_bypass_tun_probe_success_curl_compatible">کاوش فعال TUN: مسیر curl-compatible آیپی %1$s را برگرداند</string>
|
||||
<string name="checker_bypass_tun_probe_success_curl_compatible">کاوش فعال TUN: آیپی %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_dns_mismatch_curl_compatible">کاوش فعال TUN: strict same-path شکست خورد (%1$s)، اما مسیر curl-compatible موفق شد؛ احتمال mismatch در DNS-path زیاد است</string>
|
||||
<string name="checker_bypass_curl_compatible_used">مقایسه split tunnel از fallback نوع curl-compatible برای این مسیرها استفاده کرد: %1$s</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -454,9 +454,13 @@
|
|||
</plurals>
|
||||
<string name="checker_bypass_vpn_not_active">Underlying network: VPN не активен, проверка не требуется</string>
|
||||
<string name="checker_bypass_vpn_network_binding">VPN network binding: приложение видит и VPN Network, и non-VPN default сеть (VPN IP: %1$s, direct IP: %2$s)</string>
|
||||
<string name="checker_bypass_tun_probe_success">TUN активный зонд: запрос через VPN Network вернул IP %1$s (TUN — живой маршрутизируемый интерфейс)</string>
|
||||
<string name="checker_bypass_tun_probe_success_transport_only">TUN активный зонд: curl-compatible transport-only путь вернул IP %1$s (SO_BINDTODEVICE + системный DNS)</string>
|
||||
<string name="checker_bypass_tun_probe_success_curl_compatible">TUN активный зонд: curl-compatible путь вернул IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success">TUN активный зонд: IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_direct">TUN активный зонд, напрямую: IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_proxy">TUN активный зонд, прокси: IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_direct_target">TUN активный зонд, %1$s, напрямую: IP %2$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_proxy_target">TUN активный зонд, %1$s, через прокси: IP %2$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_transport_only">TUN активный зонд: IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_curl_compatible">TUN активный зонд: IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_failure">TUN активный зонд: запрос через VPN Network недоступен (интерфейс заблокирован или filtered)</string>
|
||||
<string name="checker_bypass_tun_probe_failure_reason">TUN активный зонд: запрос через VPN Network завершился ошибкой (%1$s)</string>
|
||||
<string name="checker_bypass_tun_probe_dns_mismatch">TUN активный зонд: strict same-path завершился ошибкой (%1$s), но curl-compatible transport-only путь сработал; вероятен mismatch DNS-path</string>
|
||||
|
|
|
|||
|
|
@ -447,9 +447,13 @@
|
|||
</plurals>
|
||||
<string name="checker_bypass_vpn_not_active">Underlying network:VPN 未激活,无需执行此检查</string>
|
||||
<string name="checker_bypass_vpn_network_binding">VPN network binding:应用同时访问到了默认非 VPN 路径和 VPN Network(VPN IP:%1$s,默认 IP:%2$s)</string>
|
||||
<string name="checker_bypass_tun_probe_success">TUN active probe:通过 VPN Network 发起的请求返回了 IP %1$s(TUN 是一个可路由的活动接口)</string>
|
||||
<string name="checker_bypass_tun_probe_success_transport_only">TUN active probe:curl-compatible transport-only 路径返回了 IP %1$s(SO_BINDTODEVICE + system DNS)</string>
|
||||
<string name="checker_bypass_tun_probe_success_curl_compatible">TUN 活跃探测:curl-compatible 路径返回了 IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success">TUN 活跃探测:IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_direct">TUN 活跃探测,直连:IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_proxy">TUN 活跃探测,代理:IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_direct_target">TUN 活跃探测,%1$s,直连:IP %2$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_proxy_target">TUN 活跃探测,%1$s,经代理:IP %2$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_transport_only">TUN 活跃探测:IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_curl_compatible">TUN 活跃探测:IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_failure">TUN active probe:通过 VPN Network 发起的请求不可用(接口被阻止或被 filtered)</string>
|
||||
<string name="checker_bypass_tun_probe_failure_reason">TUN 活跃探测:通过 VPN Network 发起的请求失败(%1$s)</string>
|
||||
<string name="checker_bypass_tun_probe_dns_mismatch">TUN 活跃探测:strict same-path 失败(%1$s),但 curl-compatible transport-only 路径成功;很可能存在 DNS path mismatch</string>
|
||||
|
|
|
|||
|
|
@ -450,9 +450,13 @@
|
|||
</plurals>
|
||||
<string name="checker_bypass_vpn_not_active">Underlying network: VPN is not active, the check is not required</string>
|
||||
<string name="checker_bypass_vpn_network_binding">VPN network binding: the app can reach both the default non-VPN path and VPN Network (VPN IP: %1$s, default IP: %2$s)</string>
|
||||
<string name="checker_bypass_tun_probe_success">TUN active probe: a request via VPN Network returned IP %1$s (TUN is a live routable interface)</string>
|
||||
<string name="checker_bypass_tun_probe_success_transport_only">TUN active probe: a curl-compatible transport-only path returned IP %1$s (SO_BINDTODEVICE + system DNS)</string>
|
||||
<string name="checker_bypass_tun_probe_success_curl_compatible">TUN active probe: a curl-compatible path returned IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success">TUN active probe: IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_direct">TUN active probe, direct: IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_proxy">TUN active probe, proxy: IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_direct_target">TUN active probe, %1$s, direct: IP %2$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_proxy_target">TUN active probe, %1$s, via proxy: IP %2$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_transport_only">TUN active probe: IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success_curl_compatible">TUN active probe: IP %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_failure">TUN active probe: a request via VPN Network is unavailable (the interface is blocked or filtered)</string>
|
||||
<string name="checker_bypass_tun_probe_failure_reason">TUN active probe: a request via VPN Network failed (%1$s)</string>
|
||||
<string name="checker_bypass_tun_probe_dns_mismatch">TUN active probe: strict same-path failed (%1$s), but the curl-compatible transport-only path succeeded; DNS path mismatch is likely</string>
|
||||
|
|
|
|||
|
|
@ -238,6 +238,39 @@ class BypassCheckerTest {
|
|||
assertTrue(findings.any { it.needsReview && it.description.contains("different IP families") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `gateway leak prefers non ru target when ru target masks the mismatch`() {
|
||||
val findings = mutableListOf<Finding>()
|
||||
val evidence = mutableListOf<EvidenceItem>()
|
||||
|
||||
val outcome = BypassChecker.reportUnderlyingNetworkResult(
|
||||
context = context,
|
||||
result = UnderlyingNetworkProber.ProbeResult(
|
||||
vpnActive = true,
|
||||
underlyingReachable = true,
|
||||
ruTarget = PerTargetProbe(
|
||||
targetHost = "ipv4-internet.yandex.net",
|
||||
targetGroup = TargetGroup.RU,
|
||||
vpnIp = "37.112.0.10",
|
||||
directIp = "37.112.0.10",
|
||||
),
|
||||
nonRuTarget = PerTargetProbe(
|
||||
targetHost = "api-ipv4.ip.sb",
|
||||
targetGroup = TargetGroup.NON_RU,
|
||||
vpnIp = "37.112.0.10",
|
||||
directIp = "157.180.0.10",
|
||||
),
|
||||
activeNetworkIsVpn = true,
|
||||
),
|
||||
findings = findings,
|
||||
evidence = evidence,
|
||||
)
|
||||
|
||||
assertTrue(outcome.detected)
|
||||
assertFalse(outcome.needsReview)
|
||||
assertTrue(evidence.any { it.source == EvidenceSource.VPN_GATEWAY_LEAK && it.detected })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `gateway leak falls back to needs review when vpn comparison relies on curl compatible fallback`() {
|
||||
val findings = mutableListOf<Finding>()
|
||||
|
|
@ -315,8 +348,8 @@ class BypassCheckerTest {
|
|||
assertFalse(evidence.any { it.source == EvidenceSource.VPN_GATEWAY_LEAK && it.detected })
|
||||
assertTrue(findings.any { it.needsReview && it.source == EvidenceSource.VPN_GATEWAY_LEAK })
|
||||
assertTrue(findings.any { it.isInformational && it.description.contains("curl-compatible transport-only fallback") })
|
||||
assertTrue(findings.any { it.isInformational && it.description.contains("VPN path debug") })
|
||||
assertTrue(findings.any { it.isInformational && it.description.contains("underlying path debug") })
|
||||
assertFalse(findings.any { it.isInformational && it.description.contains("VPN path debug") })
|
||||
assertFalse(findings.any { it.isInformational && it.description.contains("underlying path debug") })
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -385,21 +385,14 @@ class DirectSignsCheckerTest {
|
|||
result.findings.any {
|
||||
it.isInformational &&
|
||||
it.source == EvidenceSource.TUN_ACTIVE_PROBE &&
|
||||
it.description.contains("SO_BINDTODEVICE + system DNS")
|
||||
},
|
||||
)
|
||||
assertTrue(
|
||||
result.findings.any {
|
||||
it.isInformational &&
|
||||
it.description.contains("effective mode Curl-compatible") &&
|
||||
it.description.contains("selected mode Curl-compatible") &&
|
||||
it.description.contains("dnsPathMismatch true")
|
||||
(it.description.contains("proxy") || it.description.contains("прокси")) &&
|
||||
(it.description.contains("RU") || it.description.contains("Не-RU"))
|
||||
},
|
||||
)
|
||||
assertFalse(
|
||||
result.findings.any {
|
||||
it.isInformational &&
|
||||
it.description.contains("effective mode Auto")
|
||||
it.description.contains("VPN path debug")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -360,6 +360,82 @@ class IfconfigClientTest {
|
|||
assertFalse(comparison.dnsPathMismatch)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `network comparison uses exact target url for strict and curl compatible probes`() {
|
||||
val requestedEndpoints = mutableListOf<String>()
|
||||
PublicIpClient.fetchIpOverride = { endpoint, _, _, _, binding ->
|
||||
requestedEndpoints += "strict:$endpoint:${binding?.javaClass?.simpleName}"
|
||||
when (binding) {
|
||||
is ResolverBinding.AndroidNetworkBinding -> Result.failure(IOException("strict failed"))
|
||||
null -> Result.failure(IOException("unexpected unbound path"))
|
||||
else -> Result.failure(IOException("unexpected binding"))
|
||||
}
|
||||
}
|
||||
NativeCurlBridge.executeOverride = { request ->
|
||||
requestedEndpoints += "curl:${request.url}"
|
||||
NativeCurlResponse(
|
||||
curlCode = 0,
|
||||
httpCode = 200,
|
||||
body = "203.0.113.57",
|
||||
)
|
||||
}
|
||||
|
||||
val comparison = kotlinx.coroutines.runBlocking {
|
||||
IfconfigClient.fetchIpViaNetworkComparison(
|
||||
primaryBinding = ResolverBinding.AndroidNetworkBinding(newNetwork(212)),
|
||||
fallbackBinding = ResolverBinding.OsDeviceBinding(
|
||||
interfaceName = "tun0",
|
||||
dnsMode = ResolverBinding.DnsMode.SYSTEM,
|
||||
),
|
||||
resolverConfig = DnsResolverConfig.system(),
|
||||
targetUrls = listOf("https://ipv4-internet.yandex.net/api/v0/ip"),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(PublicIpProbeMode.CURL_COMPATIBLE, comparison.selectedMode)
|
||||
assertTrue(
|
||||
requestedEndpoints.contains(
|
||||
"strict:https://ipv4-internet.yandex.net/api/v0/ip:AndroidNetworkBinding",
|
||||
),
|
||||
)
|
||||
assertTrue(requestedEndpoints.contains("curl:https://ipv4-internet.yandex.net/api/v0/ip"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `network comparison falls back to next custom target url`() {
|
||||
val strictEndpoints = mutableListOf<String>()
|
||||
PublicIpClient.fetchIpOverride = { endpoint, _, _, _, binding ->
|
||||
strictEndpoints += endpoint
|
||||
when {
|
||||
binding is ResolverBinding.AndroidNetworkBinding && endpoint.contains("checkip.amazonaws.com") ->
|
||||
Result.failure(IOException("connection closed"))
|
||||
binding is ResolverBinding.AndroidNetworkBinding && endpoint.contains("api-ipv4.ip.sb") ->
|
||||
Result.success("203.0.113.58")
|
||||
binding == null -> Result.failure(IOException("unexpected unbound path"))
|
||||
else -> Result.failure(IOException("unexpected binding"))
|
||||
}
|
||||
}
|
||||
|
||||
val comparison = kotlinx.coroutines.runBlocking {
|
||||
IfconfigClient.fetchIpViaNetworkComparison(
|
||||
primaryBinding = ResolverBinding.AndroidNetworkBinding(newNetwork(213)),
|
||||
fallbackBinding = null,
|
||||
resolverConfig = DnsResolverConfig.system(),
|
||||
targetUrls = listOf(
|
||||
"https://checkip.amazonaws.com",
|
||||
"https://api-ipv4.ip.sb/ip",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(PublicIpProbeMode.STRICT_SAME_PATH, comparison.selectedMode)
|
||||
assertEquals("203.0.113.58", comparison.selectedIp)
|
||||
assertEquals(
|
||||
listOf("https://checkip.amazonaws.com", "https://api-ipv4.ip.sb/ip"),
|
||||
strictEndpoints,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `curl compatible uses injected resolve for direct resolver`() {
|
||||
var observedBinding: ResolverBinding? = null
|
||||
|
|
|
|||
|
|
@ -107,12 +107,22 @@ class UnderlyingNetworkProberTest {
|
|||
),
|
||||
comparisons = mapOf(
|
||||
vpnNetwork to mapOf(
|
||||
"ifconfig.yandex.ru" to successfulComparison("198.51.100.10"),
|
||||
"api.ipify.org" to successfulComparison("203.0.113.10"),
|
||||
listOf("https://ipv4-internet.yandex.net/api/v0/ip", "https://ip.mail.ru") to successfulComparison("198.51.100.10"),
|
||||
listOf(
|
||||
"https://api-ipv4.ip.sb/ip",
|
||||
"https://checkip.amazonaws.com",
|
||||
"https://ifconfig.me/ip",
|
||||
"https://api4.ipify.org",
|
||||
) to successfulComparison("203.0.113.10"),
|
||||
),
|
||||
wifiNetwork to mapOf(
|
||||
"ifconfig.yandex.ru" to successfulComparison("203.0.113.1"),
|
||||
"api.ipify.org" to successfulComparison("203.0.113.2"),
|
||||
listOf("https://ipv4-internet.yandex.net/api/v0/ip", "https://ip.mail.ru") to successfulComparison("203.0.113.1"),
|
||||
listOf(
|
||||
"https://api-ipv4.ip.sb/ip",
|
||||
"https://checkip.amazonaws.com",
|
||||
"https://ifconfig.me/ip",
|
||||
"https://api4.ipify.org",
|
||||
) to successfulComparison("203.0.113.2"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -128,8 +138,10 @@ class UnderlyingNetworkProberTest {
|
|||
assertEquals("203.0.113.10", result.nonRuTarget.vpnIp)
|
||||
assertEquals("203.0.113.1", result.ruTarget.directIp)
|
||||
assertEquals("203.0.113.2", result.nonRuTarget.directIp)
|
||||
assertEquals("ifconfig.yandex.ru", result.ruTarget.targetHost)
|
||||
assertEquals("api.ipify.org", result.nonRuTarget.targetHost)
|
||||
assertEquals("ipv4-internet.yandex.net", result.ruTarget.targetHost)
|
||||
assertEquals("api-ipv4.ip.sb", result.nonRuTarget.targetHost)
|
||||
assertEquals("203.0.113.10", result.vpnIp)
|
||||
assertEquals("203.0.113.2", result.underlyingIp)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -140,8 +152,13 @@ class UnderlyingNetworkProberTest {
|
|||
snapshots = listOf(snapshot(vpnNetwork, "tun0", hasVpnTransport = true)),
|
||||
comparisons = mapOf(
|
||||
vpnNetwork to mapOf(
|
||||
"ifconfig.yandex.ru" to failureComparison("RU endpoint timeout"),
|
||||
"api.ipify.org" to successfulComparison("203.0.113.10"),
|
||||
listOf("https://ipv4-internet.yandex.net/api/v0/ip", "https://ip.mail.ru") to failureComparison("RU endpoint timeout"),
|
||||
listOf(
|
||||
"https://api-ipv4.ip.sb/ip",
|
||||
"https://checkip.amazonaws.com",
|
||||
"https://ifconfig.me/ip",
|
||||
"https://api4.ipify.org",
|
||||
) to successfulComparison("203.0.113.10"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -166,8 +183,13 @@ class UnderlyingNetworkProberTest {
|
|||
snapshots = listOf(snapshot(vpnNetwork, "tun0", hasVpnTransport = true)),
|
||||
comparisons = mapOf(
|
||||
vpnNetwork to mapOf(
|
||||
"ifconfig.yandex.ru" to successfulComparison("198.51.100.10"),
|
||||
"api.ipify.org" to successfulComparison("203.0.113.10"),
|
||||
listOf("https://ipv4-internet.yandex.net/api/v0/ip", "https://ip.mail.ru") to successfulComparison("198.51.100.10"),
|
||||
listOf(
|
||||
"https://api-ipv4.ip.sb/ip",
|
||||
"https://checkip.amazonaws.com",
|
||||
"https://ifconfig.me/ip",
|
||||
"https://api4.ipify.org",
|
||||
) to successfulComparison("203.0.113.10"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -185,7 +207,7 @@ class UnderlyingNetworkProberTest {
|
|||
private fun installDependencies(
|
||||
activeNetwork: Network?,
|
||||
snapshots: List<UnderlyingNetworkProber.NetworkSnapshot>,
|
||||
comparisons: Map<Network, Map<String, PublicIpNetworkComparison>>,
|
||||
comparisons: Map<Network, Map<List<String>, PublicIpNetworkComparison>>,
|
||||
) {
|
||||
UnderlyingNetworkProber.dependenciesOverride = UnderlyingNetworkProber.Dependencies(
|
||||
initNativeCurl = {},
|
||||
|
|
@ -195,14 +217,56 @@ class UnderlyingNetworkProberTest {
|
|||
networks = snapshots,
|
||||
)
|
||||
},
|
||||
comparisonFetcher = { snapshot, _, _, _, targetHost ->
|
||||
val host = requireNotNull(targetHost)
|
||||
comparisons[snapshot.network]?.get(host)
|
||||
?: failureComparison("Missing test comparison for ${snapshot.network} $host")
|
||||
comparisonFetcher = { snapshot, _, _, _, targetUrls ->
|
||||
val urls = requireNotNull(targetUrls)
|
||||
comparisons[snapshot.network]?.get(urls)
|
||||
?: failureComparison("Missing test comparison for ${snapshot.network} $urls")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `probe keeps vpn error separate from underlying error`() = runBlocking {
|
||||
val vpnNetwork = newNetwork(305)
|
||||
val wifiNetwork = newNetwork(306)
|
||||
installDependencies(
|
||||
activeNetwork = vpnNetwork,
|
||||
snapshots = listOf(
|
||||
snapshot(vpnNetwork, "tun0", hasVpnTransport = true),
|
||||
snapshot(wifiNetwork, "wlan0", hasVpnTransport = false),
|
||||
),
|
||||
comparisons = mapOf(
|
||||
vpnNetwork to mapOf(
|
||||
listOf("https://ipv4-internet.yandex.net/api/v0/ip", "https://ip.mail.ru") to successfulComparison("198.51.100.10"),
|
||||
listOf(
|
||||
"https://api-ipv4.ip.sb/ip",
|
||||
"https://checkip.amazonaws.com",
|
||||
"https://ifconfig.me/ip",
|
||||
"https://api4.ipify.org",
|
||||
) to failureComparison("vpn non-ru timeout"),
|
||||
),
|
||||
wifiNetwork to mapOf(
|
||||
listOf("https://ipv4-internet.yandex.net/api/v0/ip", "https://ip.mail.ru") to successfulComparison("203.0.113.1"),
|
||||
listOf(
|
||||
"https://api-ipv4.ip.sb/ip",
|
||||
"https://checkip.amazonaws.com",
|
||||
"https://ifconfig.me/ip",
|
||||
"https://api4.ipify.org",
|
||||
) to failureComparison("underlying non-ru timeout"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = UnderlyingNetworkProber.probe(
|
||||
context = context,
|
||||
resolverConfig = DnsResolverConfig.system(),
|
||||
)
|
||||
|
||||
assertEquals(null, result.ruTarget.error)
|
||||
assertEquals("vpn non-ru timeout", result.nonRuTarget.error)
|
||||
assertEquals("underlying non-ru timeout", result.underlyingError)
|
||||
}
|
||||
|
||||
private fun snapshot(
|
||||
network: Network,
|
||||
interfaceName: String,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue