mirror of
https://github.com/xtclovver/RKNHardering.git
synced 2026-07-09 17:19:25 +00:00
parent
c739864632
commit
2e5f64c7e4
10 changed files with 220 additions and 10 deletions
|
|
@ -116,6 +116,50 @@ internal fun maskInfoValue(value: String, privacyMode: Boolean): String {
|
|||
return if (privacyMode) maskIpsInText(value) else value
|
||||
}
|
||||
|
||||
private const val CALL_TRANSPORT_NO_STUN_RESPONSE_MARKER = "did not receive a STUN response"
|
||||
private const val CALL_TRANSPORT_TELEGRAM_DC_UNREACHABLE_MARKER = "did not expose a reachable Telegram DC"
|
||||
|
||||
internal fun formatCallTransportReason(
|
||||
context: android.content.Context,
|
||||
leak: CallTransportLeakResult,
|
||||
privacyMode: Boolean,
|
||||
): String? {
|
||||
val summary = leak.summary.trim()
|
||||
return when {
|
||||
leak.status == CallTransportStatus.BASELINE || leak.status == CallTransportStatus.NEEDS_REVIEW -> null
|
||||
summary.contains(CALL_TRANSPORT_NO_STUN_RESPONSE_MARKER, ignoreCase = true) ->
|
||||
buildCallTransportReason(
|
||||
base = context.getString(R.string.main_card_call_transport_reason_no_response),
|
||||
detail = summaryDetailAfterMarker(summary, CALL_TRANSPORT_NO_STUN_RESPONSE_MARKER),
|
||||
privacyMode = privacyMode,
|
||||
)
|
||||
summary.contains(CALL_TRANSPORT_TELEGRAM_DC_UNREACHABLE_MARKER, ignoreCase = true) ->
|
||||
buildCallTransportReason(
|
||||
base = context.getString(R.string.main_card_call_transport_reason_telegram_dc_unreachable),
|
||||
detail = summaryDetailAfterMarker(summary, CALL_TRANSPORT_TELEGRAM_DC_UNREACHABLE_MARKER),
|
||||
privacyMode = privacyMode,
|
||||
)
|
||||
summary.contains("experimental trace is disabled in release builds", ignoreCase = true) ->
|
||||
context.getString(R.string.main_card_call_transport_reason_release_only)
|
||||
summary.contains("targets are unavailable", ignoreCase = true) ||
|
||||
summary.contains("target catalog is unavailable", ignoreCase = true) ->
|
||||
context.getString(R.string.main_card_call_transport_reason_targets_unavailable)
|
||||
else -> maskInfoValue(summary, privacyMode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildCallTransportReason(base: String, detail: String?, privacyMode: Boolean): String {
|
||||
val maskedDetail = detail
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { maskInfoValue(it, privacyMode) }
|
||||
return if (maskedDetail.isNullOrBlank()) base else "$base: $maskedDetail"
|
||||
}
|
||||
|
||||
private fun summaryDetailAfterMarker(summary: String, marker: String): String? {
|
||||
val tail = summary.substringAfter(marker, missingDelimiterValue = "").trim()
|
||||
return tail.removePrefix(":").trim().takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
private fun isUniqueLocalIpv6(address: Inet6Address): Boolean {
|
||||
val firstByte = address.address.firstOrNull()?.toInt()?.and(0xff) ?: return false
|
||||
return (firstByte and 0xfe) == 0xfc
|
||||
|
|
@ -1712,6 +1756,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
val statusLabel = when (leak.status) {
|
||||
CallTransportStatus.BASELINE -> getString(R.string.main_card_call_transport_status_baseline)
|
||||
CallTransportStatus.NO_SIGNAL -> getString(R.string.main_card_call_transport_status_no_signal)
|
||||
CallTransportStatus.NEEDS_REVIEW -> getString(R.string.main_card_call_transport_status_needs_review)
|
||||
CallTransportStatus.UNSUPPORTED -> getString(R.string.main_card_call_transport_status_unsupported)
|
||||
|
|
@ -1727,9 +1772,10 @@ class MainActivity : AppCompatActivity() {
|
|||
CallTransportService.WHATSAPP -> "WhatsApp"
|
||||
}
|
||||
val statusColor = when (leak.status) {
|
||||
CallTransportStatus.BASELINE -> R.color.status_green
|
||||
CallTransportStatus.NEEDS_REVIEW -> R.color.status_amber
|
||||
CallTransportStatus.ERROR -> R.color.status_amber
|
||||
CallTransportStatus.NO_SIGNAL -> R.color.status_green
|
||||
CallTransportStatus.NO_SIGNAL -> R.color.md_on_surface_variant
|
||||
CallTransportStatus.UNSUPPORTED -> R.color.md_on_surface_variant
|
||||
}
|
||||
|
||||
|
|
@ -1739,9 +1785,10 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
val indicator = TextView(this).apply {
|
||||
text = when (leak.status) {
|
||||
CallTransportStatus.BASELINE -> "\u2713"
|
||||
CallTransportStatus.NEEDS_REVIEW -> "?"
|
||||
CallTransportStatus.ERROR -> "\u26A0"
|
||||
CallTransportStatus.NO_SIGNAL -> "\u2713"
|
||||
CallTransportStatus.NO_SIGNAL -> "\u2014"
|
||||
CallTransportStatus.UNSUPPORTED -> "\u2014"
|
||||
}
|
||||
setTextColor(ContextCompat.getColor(this@MainActivity, statusColor))
|
||||
|
|
@ -1761,6 +1808,19 @@ class MainActivity : AppCompatActivity() {
|
|||
headerRow.addView(headerText)
|
||||
container.addView(headerRow)
|
||||
|
||||
formatCallTransportReason(this, leak, privacyMode)?.let { reason ->
|
||||
val reasonColor = when (leak.status) {
|
||||
CallTransportStatus.ERROR -> R.color.status_amber
|
||||
else -> R.color.md_on_surface_variant
|
||||
}
|
||||
container.addView(TextView(this).apply {
|
||||
text = reason
|
||||
textSize = 12f
|
||||
setPadding(22.dp, 2.dp, 0, 0)
|
||||
setTextColor(ContextCompat.getColor(this@MainActivity, reasonColor))
|
||||
})
|
||||
}
|
||||
|
||||
val target = leak.targetHost
|
||||
if (target != null) {
|
||||
val port = leak.targetPort
|
||||
|
|
|
|||
|
|
@ -479,7 +479,7 @@ object CallTransportChecker {
|
|||
) {
|
||||
CallTransportStatus.NEEDS_REVIEW
|
||||
} else {
|
||||
CallTransportStatus.NO_SIGNAL
|
||||
CallTransportStatus.BASELINE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -680,8 +680,9 @@ object CallTransportChecker {
|
|||
return when (status) {
|
||||
CallTransportStatus.NEEDS_REVIEW -> 4
|
||||
CallTransportStatus.ERROR -> 3
|
||||
CallTransportStatus.NO_SIGNAL -> 2
|
||||
CallTransportStatus.UNSUPPORTED -> 1
|
||||
CallTransportStatus.BASELINE -> 2
|
||||
CallTransportStatus.NO_SIGNAL -> 1
|
||||
CallTransportStatus.UNSUPPORTED -> 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -717,6 +718,7 @@ object CallTransportChecker {
|
|||
}
|
||||
CallTransportStatus.NO_SIGNAL,
|
||||
CallTransportStatus.UNSUPPORTED,
|
||||
CallTransportStatus.BASELINE,
|
||||
-> Unit
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ enum class CallTransportProbeKind {
|
|||
}
|
||||
|
||||
enum class CallTransportStatus {
|
||||
BASELINE,
|
||||
NO_SIGNAL,
|
||||
NEEDS_REVIEW,
|
||||
UNSUPPORTED,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ object ResolverNetworkStack {
|
|||
config: DnsResolverConfig,
|
||||
binding: ResolverBinding? = null,
|
||||
): List<InetAddress> {
|
||||
literalToInetAddress(hostname)?.let { return listOf(it) }
|
||||
return dns(config, binding).lookup(hostname)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,13 +83,18 @@
|
|||
<string name="main_card_direct_signs">Прямые признаки</string>
|
||||
<string name="main_card_indirect_signs">Косвенные признаки</string>
|
||||
<string name="main_card_call_transport">STUN-серверы</string>
|
||||
<string name="main_card_call_transport_status_no_signal">Нет сигнала</string>
|
||||
<string name="main_card_call_transport_status_baseline">Ответ есть</string>
|
||||
<string name="main_card_call_transport_status_no_signal">Нет ответа</string>
|
||||
<string name="main_card_call_transport_status_needs_review">Требует проверки</string>
|
||||
<string name="main_card_call_transport_status_unsupported">Не поддерживается</string>
|
||||
<string name="main_card_call_transport_status_error">Ошибка</string>
|
||||
<string name="main_card_call_transport_path_active">Активная сеть</string>
|
||||
<string name="main_card_call_transport_path_underlying">Базовая сеть</string>
|
||||
<string name="main_card_call_transport_path_proxy">Локальный прокси</string>
|
||||
<string name="main_card_call_transport_reason_no_response">STUN-ответ не получен</string>
|
||||
<string name="main_card_call_transport_reason_telegram_dc_unreachable">Telegram DC недостижим через локальный SOCKS5-прокси</string>
|
||||
<string name="main_card_call_transport_reason_release_only">Трассировка WhatsApp доступна только в debug или experimental сборке</string>
|
||||
<string name="main_card_call_transport_reason_targets_unavailable">Цели для этой проверки недоступны</string>
|
||||
<string name="main_card_location_signals">Сигналы местоположения</string>
|
||||
<string name="main_verdict_details">Подробнее</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -84,13 +84,18 @@
|
|||
<string name="main_card_direct_signs">Direct signs</string>
|
||||
<string name="main_card_indirect_signs">Indirect signs</string>
|
||||
<string name="main_card_call_transport">STUN servers</string>
|
||||
<string name="main_card_call_transport_status_no_signal">No signal</string>
|
||||
<string name="main_card_call_transport_status_baseline">Responded</string>
|
||||
<string name="main_card_call_transport_status_no_signal">No response</string>
|
||||
<string name="main_card_call_transport_status_needs_review">Needs review</string>
|
||||
<string name="main_card_call_transport_status_unsupported">Unsupported</string>
|
||||
<string name="main_card_call_transport_status_error">Error</string>
|
||||
<string name="main_card_call_transport_path_active">Active network</string>
|
||||
<string name="main_card_call_transport_path_underlying">Underlying network</string>
|
||||
<string name="main_card_call_transport_path_proxy">Local proxy</string>
|
||||
<string name="main_card_call_transport_reason_no_response">No STUN response</string>
|
||||
<string name="main_card_call_transport_reason_telegram_dc_unreachable">Telegram DC is unreachable through the local SOCKS5 proxy</string>
|
||||
<string name="main_card_call_transport_reason_release_only">WhatsApp tracing is available only in debug or experimental builds</string>
|
||||
<string name="main_card_call_transport_reason_targets_unavailable">Targets for this check are unavailable</string>
|
||||
<string name="main_card_location_signals">Location signals</string>
|
||||
<string name="main_verdict_details">Details</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
package com.notcvnt.rknhardering
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.notcvnt.rknhardering.model.CallTransportLeakResult
|
||||
import com.notcvnt.rknhardering.model.CallTransportNetworkPath
|
||||
import com.notcvnt.rknhardering.model.CallTransportProbeKind
|
||||
import com.notcvnt.rknhardering.model.CallTransportService
|
||||
import com.notcvnt.rknhardering.model.CallTransportStatus
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class MainActivityCallTransportReasonTest {
|
||||
|
||||
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||
|
||||
@Test
|
||||
fun `formatCallTransportReason localizes stun timeout`() {
|
||||
val leak = leak(
|
||||
status = CallTransportStatus.NO_SIGNAL,
|
||||
summary = "Telegram call transport via active network did not receive a STUN response: timeout",
|
||||
)
|
||||
|
||||
val reason = formatCallTransportReason(context, leak, privacyMode = false)
|
||||
|
||||
assertEquals(
|
||||
"${context.getString(R.string.main_card_call_transport_reason_no_response)}: timeout",
|
||||
reason,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `formatCallTransportReason localizes release only whatsapp`() {
|
||||
val leak = leak(
|
||||
service = CallTransportService.WHATSAPP,
|
||||
status = CallTransportStatus.UNSUPPORTED,
|
||||
summary = "WhatsApp experimental trace is disabled in release builds",
|
||||
)
|
||||
|
||||
val reason = formatCallTransportReason(context, leak, privacyMode = false)
|
||||
|
||||
assertEquals(context.getString(R.string.main_card_call_transport_reason_release_only), reason)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `formatCallTransportReason masks ip details in privacy mode`() {
|
||||
val leak = leak(
|
||||
status = CallTransportStatus.NO_SIGNAL,
|
||||
summary = "Telegram call transport via active network did not receive a STUN response: timeout from 203.0.113.10",
|
||||
)
|
||||
|
||||
val reason = formatCallTransportReason(context, leak, privacyMode = true)
|
||||
|
||||
assertEquals(
|
||||
"${context.getString(R.string.main_card_call_transport_reason_no_response)}: timeout from 203.0.*.*",
|
||||
reason,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `formatCallTransportReason keeps needs review without extra text`() {
|
||||
val leak = leak(
|
||||
status = CallTransportStatus.BASELINE,
|
||||
summary = "Telegram call transport via active network: STUN endpoint responded",
|
||||
)
|
||||
|
||||
assertNull(formatCallTransportReason(context, leak, privacyMode = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `formatCallTransportReason keeps review status without extra text`() {
|
||||
val leak = leak(
|
||||
status = CallTransportStatus.NEEDS_REVIEW,
|
||||
summary = "Telegram call transport via active network: STUN endpoint responded",
|
||||
)
|
||||
|
||||
assertNull(formatCallTransportReason(context, leak, privacyMode = false))
|
||||
}
|
||||
|
||||
private fun leak(
|
||||
service: CallTransportService = CallTransportService.TELEGRAM,
|
||||
status: CallTransportStatus,
|
||||
summary: String,
|
||||
): CallTransportLeakResult {
|
||||
return CallTransportLeakResult(
|
||||
service = service,
|
||||
probeKind = CallTransportProbeKind.DIRECT_UDP_STUN,
|
||||
networkPath = CallTransportNetworkPath.ACTIVE,
|
||||
status = status,
|
||||
summary = summary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ class CallTransportCheckerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `probeDirect keeps active path as baseline signal`() {
|
||||
fun `probeDirect marks active path baseline when stun responds without leak`() {
|
||||
CallTransportChecker.dependenciesOverride = CallTransportChecker.Dependencies(
|
||||
loadCatalog = { _, _ -> catalogWithTelegramTarget() },
|
||||
loadPaths = {
|
||||
|
|
@ -57,7 +57,7 @@ class CallTransportCheckerTest {
|
|||
val results = runBlockingProbeDirect(experimental = false)
|
||||
|
||||
val telegram = results.first { it.service == CallTransportService.TELEGRAM }
|
||||
assertEquals(CallTransportStatus.NO_SIGNAL, telegram.status)
|
||||
assertEquals(CallTransportStatus.BASELINE, telegram.status)
|
||||
assertEquals(CallTransportProbeKind.DIRECT_UDP_STUN, telegram.probeKind)
|
||||
assertEquals(CallTransportNetworkPath.ACTIVE, telegram.networkPath)
|
||||
assertEquals("149.154.167.51", telegram.targetHost)
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class VpnCheckRunnerTest {
|
|||
)
|
||||
|
||||
assertTrue(result.indirectSigns.callTransportLeaks.any { it.status == CallTransportStatus.NEEDS_REVIEW })
|
||||
assertEquals(Verdict.NEEDS_REVIEW, result.verdict)
|
||||
assertEquals(Verdict.NOT_DETECTED, result.verdict)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.notcvnt.rknhardering.network
|
||||
|
||||
import okhttp3.Dns
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.net.UnknownHostException
|
||||
|
||||
class ResolverNetworkStackTest {
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
ResolverNetworkStack.dnsFactoryOverride = null
|
||||
ResolverNetworkStack.resetForTests()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `lookup bypasses configured dns for ip literals`() {
|
||||
var overrideCalls = 0
|
||||
ResolverNetworkStack.dnsFactoryOverride = { _, _ ->
|
||||
overrideCalls += 1
|
||||
object : Dns {
|
||||
override fun lookup(hostname: String) = throw UnknownHostException("NXDOMAIN")
|
||||
}
|
||||
}
|
||||
|
||||
val resolved = ResolverNetworkStack.lookup(
|
||||
hostname = "149.154.167.51",
|
||||
config = DnsResolverConfig(
|
||||
mode = DnsResolverMode.DOH,
|
||||
preset = DnsResolverPreset.CUSTOM,
|
||||
customDohUrl = "https://dns.google/dns-query",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(listOf("149.154.167.51"), resolved.mapNotNull { it.hostAddress })
|
||||
assertEquals(0, overrideCalls)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue