mirror of
https://github.com/xtclovver/RKNHardering.git
synced 2026-07-09 17:19:25 +00:00
feat: SNITCH проверка (β) (#49)
This commit is contained in:
parent
859f3ca515
commit
e2d2d1fb11
19 changed files with 981 additions and 3 deletions
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
|
|
@ -2,9 +2,9 @@
|
|||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<SelectionState runConfigName="start">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-04-08T19:55:08.785572700Z">
|
||||
<DropdownSelection timestamp="2026-05-06T21:44:57.162357200Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\usedcvnt\.android\avd\Pixel_9_Pro.avd" />
|
||||
|
|
|
|||
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
|
|
@ -1,4 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ internal object CheckResultMarkdownExportFormatter {
|
|||
appendCategorySection(builder, context.getString(R.string.main_card_direct_signs), result.directSigns, snapshot.privacyMode)
|
||||
appendCategorySection(builder, context.getString(R.string.main_card_indirect_signs), result.indirectSigns, snapshot.privacyMode)
|
||||
appendCategorySection(builder, context.getString(R.string.main_card_icmp_spoofing), result.icmpSpoofing, snapshot.privacyMode)
|
||||
appendCategorySection(builder, context.getString(R.string.main_card_rtt_triangulation), result.rttTriangulation, snapshot.privacyMode)
|
||||
appendCategorySection(builder, context.getString(R.string.main_card_location_signals), result.locationSignals, snapshot.privacyMode)
|
||||
appendIpChannelsSection(builder, result.ipConsensus, snapshot.privacyMode)
|
||||
appendBypassSection(builder, context, result.bypassResult, snapshot.privacyMode)
|
||||
|
|
@ -142,6 +143,12 @@ internal object CheckResultMarkdownExportFormatter {
|
|||
status = sectionStatusTag(result.icmpSpoofing.detected, result.icmpSpoofing.needsReview, result.icmpSpoofing.hasError),
|
||||
summary = buildCategorySummary(result.icmpSpoofing, snapshot.privacyMode),
|
||||
)
|
||||
appendSectionSummaryRow(
|
||||
builder,
|
||||
title = context.getString(R.string.main_card_rtt_triangulation),
|
||||
status = sectionStatusTag(result.rttTriangulation.detected, result.rttTriangulation.needsReview, result.rttTriangulation.hasError),
|
||||
summary = buildCategorySummary(result.rttTriangulation, snapshot.privacyMode),
|
||||
)
|
||||
appendSectionSummaryRow(
|
||||
builder,
|
||||
title = context.getString(R.string.main_card_location_signals),
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ object DebugDiagnosticsFormatter {
|
|||
appendCategory(builder, "directSigns", result.directSigns)
|
||||
appendCategory(builder, "indirectSigns", result.indirectSigns)
|
||||
appendCategory(builder, "icmpSpoofing", result.icmpSpoofing)
|
||||
appendCategory(builder, "rttTriangulation", result.rttTriangulation)
|
||||
appendIndirectPerformance(builder, result.indirectSigns)
|
||||
appendCategory(builder, "locationSignals", result.locationSignals)
|
||||
appendBypass(builder, result.bypassResult)
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ class MainActivity : AppCompatActivity() {
|
|||
INDIRECT,
|
||||
NATIVE_SIGNS,
|
||||
ICMP,
|
||||
RTT_TRIANGULATION,
|
||||
LOCATION,
|
||||
IP_CONSENSUS,
|
||||
BYPASS,
|
||||
|
|
@ -232,6 +233,10 @@ class MainActivity : AppCompatActivity() {
|
|||
private lateinit var iconIcmpSpoofing: ImageView
|
||||
private lateinit var statusIcmpSpoofing: TextView
|
||||
private lateinit var findingsIcmpSpoofing: LinearLayout
|
||||
private lateinit var cardRttTriangulation: MaterialCardView
|
||||
private lateinit var iconRttTriangulation: ImageView
|
||||
private lateinit var statusRttTriangulation: TextView
|
||||
private lateinit var findingsRttTriangulation: LinearLayout
|
||||
private lateinit var cardNativeSigns: MaterialCardView
|
||||
private lateinit var iconNativeSigns: ImageView
|
||||
private lateinit var statusNativeSigns: TextView
|
||||
|
|
@ -477,6 +482,10 @@ class MainActivity : AppCompatActivity() {
|
|||
iconIcmpSpoofing = findViewById(R.id.iconIcmpSpoofing)
|
||||
statusIcmpSpoofing = findViewById(R.id.statusIcmpSpoofing)
|
||||
findingsIcmpSpoofing = findViewById(R.id.findingsIcmpSpoofing)
|
||||
cardRttTriangulation = findViewById(R.id.cardRttTriangulation)
|
||||
iconRttTriangulation = findViewById(R.id.iconRttTriangulation)
|
||||
statusRttTriangulation = findViewById(R.id.statusRttTriangulation)
|
||||
findingsRttTriangulation = findViewById(R.id.findingsRttTriangulation)
|
||||
cardNativeSigns = findViewById(R.id.cardNativeSigns)
|
||||
iconNativeSigns = findViewById(R.id.iconNativeSigns)
|
||||
statusNativeSigns = findViewById(R.id.statusNativeSigns)
|
||||
|
|
@ -556,6 +565,7 @@ class MainActivity : AppCompatActivity() {
|
|||
Triple(CATEGORY_NAT, getString(R.string.main_card_native_signs), R.drawable.ic_lock),
|
||||
Triple(CATEGORY_STN, getString(R.string.main_card_call_transport), R.drawable.ic_call),
|
||||
Triple(CATEGORY_ICM, getString(R.string.main_card_icmp_spoofing), R.drawable.ic_network),
|
||||
Triple(CATEGORY_RTT, getString(R.string.main_card_rtt_triangulation), R.drawable.ic_pin),
|
||||
Triple(CATEGORY_LOC, getString(R.string.main_card_location_signals), R.drawable.ic_location_on),
|
||||
Triple(CATEGORY_BYP, getString(R.string.settings_split_tunnel), R.drawable.ic_call_split),
|
||||
)
|
||||
|
|
@ -595,6 +605,7 @@ class MainActivity : AppCompatActivity() {
|
|||
CATEGORY_NAT -> R.id.cardNativeSigns
|
||||
CATEGORY_STN -> R.id.cardCallTransport
|
||||
CATEGORY_ICM -> R.id.cardIcmpSpoofing
|
||||
CATEGORY_RTT -> R.id.cardRttTriangulation
|
||||
CATEGORY_LOC -> R.id.cardLocation
|
||||
CATEGORY_BYP -> R.id.cardBypass
|
||||
else -> error("Unknown category id: $id")
|
||||
|
|
@ -609,6 +620,7 @@ class MainActivity : AppCompatActivity() {
|
|||
CATEGORY_NAT -> R.id.headerNativeSigns
|
||||
CATEGORY_STN -> R.id.headerCallTransport
|
||||
CATEGORY_ICM -> R.id.headerIcmpSpoofing
|
||||
CATEGORY_RTT -> R.id.headerRttTriangulation
|
||||
CATEGORY_LOC -> R.id.headerLocation
|
||||
CATEGORY_BYP -> R.id.headerBypass
|
||||
else -> error("Unknown category id: $id")
|
||||
|
|
@ -623,6 +635,7 @@ class MainActivity : AppCompatActivity() {
|
|||
CATEGORY_NAT -> R.id.headerDotNativeSigns
|
||||
CATEGORY_STN -> R.id.headerDotCallTransport
|
||||
CATEGORY_ICM -> R.id.headerDotIcmpSpoofing
|
||||
CATEGORY_RTT -> R.id.headerDotRttTriangulation
|
||||
CATEGORY_LOC -> R.id.headerDotLocation
|
||||
CATEGORY_BYP -> R.id.headerDotBypass
|
||||
else -> error("Unknown category id: $id")
|
||||
|
|
@ -637,6 +650,7 @@ class MainActivity : AppCompatActivity() {
|
|||
CATEGORY_NAT -> R.id.headerIconNativeSigns
|
||||
CATEGORY_STN -> R.id.headerIconCallTransport
|
||||
CATEGORY_ICM -> R.id.headerIconIcmpSpoofing
|
||||
CATEGORY_RTT -> R.id.headerIconRttTriangulation
|
||||
CATEGORY_LOC -> R.id.headerIconLocation
|
||||
CATEGORY_BYP -> R.id.headerIconBypass
|
||||
else -> error("Unknown category id: $id")
|
||||
|
|
@ -651,6 +665,7 @@ class MainActivity : AppCompatActivity() {
|
|||
CATEGORY_NAT -> R.id.headerTitleNativeSigns
|
||||
CATEGORY_STN -> R.id.headerTitleCallTransport
|
||||
CATEGORY_ICM -> R.id.headerTitleIcmpSpoofing
|
||||
CATEGORY_RTT -> R.id.headerTitleRttTriangulation
|
||||
CATEGORY_LOC -> R.id.headerTitleLocation
|
||||
CATEGORY_BYP -> R.id.headerTitleBypass
|
||||
else -> error("Unknown category id: $id")
|
||||
|
|
@ -665,6 +680,7 @@ class MainActivity : AppCompatActivity() {
|
|||
CATEGORY_NAT -> R.id.headerHintNativeSigns
|
||||
CATEGORY_STN -> R.id.headerHintCallTransport
|
||||
CATEGORY_ICM -> R.id.headerHintIcmpSpoofing
|
||||
CATEGORY_RTT -> R.id.headerHintRttTriangulation
|
||||
CATEGORY_LOC -> R.id.headerHintLocation
|
||||
CATEGORY_BYP -> R.id.headerHintBypass
|
||||
else -> error("Unknown category id: $id")
|
||||
|
|
@ -679,6 +695,7 @@ class MainActivity : AppCompatActivity() {
|
|||
CATEGORY_NAT -> R.id.chevronNativeSigns
|
||||
CATEGORY_STN -> R.id.chevronCallTransport
|
||||
CATEGORY_ICM -> R.id.chevronIcmpSpoofing
|
||||
CATEGORY_RTT -> R.id.chevronRttTriangulation
|
||||
CATEGORY_LOC -> R.id.chevronLocation
|
||||
CATEGORY_BYP -> R.id.chevronBypass
|
||||
else -> error("Unknown category id: $id")
|
||||
|
|
@ -693,6 +710,7 @@ class MainActivity : AppCompatActivity() {
|
|||
CATEGORY_NAT -> R.id.bodyNativeSigns
|
||||
CATEGORY_STN -> R.id.bodyCallTransport
|
||||
CATEGORY_ICM -> R.id.bodyIcmpSpoofing
|
||||
CATEGORY_RTT -> R.id.bodyRttTriangulation
|
||||
CATEGORY_LOC -> R.id.bodyLocation
|
||||
CATEGORY_BYP -> R.id.bodyBypass
|
||||
else -> error("Unknown category id: $id")
|
||||
|
|
@ -1050,6 +1068,7 @@ class MainActivity : AppCompatActivity() {
|
|||
val callTransportProbeEnabled = prefs.getBoolean(SettingsActivity.PREF_CALL_TRANSPORT_PROBE_ENABLED, false)
|
||||
val cdnPullingEnabled = prefs.getBoolean(SettingsActivity.PREF_CDN_PULLING_ENABLED, false)
|
||||
val cdnPullingMeduzaEnabled = prefs.getBoolean(SettingsActivity.PREF_CDN_PULLING_MEDUZA_ENABLED, true)
|
||||
val rttTriangulationEnabled = prefs.getBoolean(SettingsActivity.PREF_RTT_TRIANGULATION_ENABLED, false)
|
||||
val tunProbeDebugEnabled = prefs.getBoolean(SettingsActivity.PREF_TUN_PROBE_DEBUG_ENABLED, false)
|
||||
val tunProbeModeOverride = com.notcvnt.rknhardering.probe.TunProbeModeOverride.fromPref(
|
||||
prefs.getString(
|
||||
|
|
@ -1078,6 +1097,7 @@ class MainActivity : AppCompatActivity() {
|
|||
callTransportProbeEnabled = callTransportProbeEnabled,
|
||||
cdnPullingEnabled = cdnPullingEnabled,
|
||||
cdnPullingMeduzaEnabled = cdnPullingMeduzaEnabled,
|
||||
rttTriangulationEnabled = rttTriangulationEnabled,
|
||||
tunProbeDebugEnabled = tunProbeDebugEnabled,
|
||||
tunProbeModeOverride = tunProbeModeOverride,
|
||||
resolverConfig = resolverConfig,
|
||||
|
|
@ -1246,6 +1266,9 @@ class MainActivity : AppCompatActivity() {
|
|||
findingsIcmpSpoofing.removeAllViews()
|
||||
findingsIcmpSpoofing.visibility = View.GONE
|
||||
|
||||
findingsRttTriangulation.removeAllViews()
|
||||
findingsRttTriangulation.visibility = View.GONE
|
||||
|
||||
textNativeSignsSummary.text = ""
|
||||
textNativeSignsSummary.visibility = View.GONE
|
||||
findingsNativeSigns.removeAllViews()
|
||||
|
|
@ -1271,6 +1294,9 @@ class MainActivity : AppCompatActivity() {
|
|||
if (settings.icmpSpoofingEnabled) {
|
||||
stages += RunningStage.ICMP
|
||||
}
|
||||
if (settings.rttTriangulationEnabled) {
|
||||
stages += RunningStage.RTT_TRIANGULATION
|
||||
}
|
||||
}
|
||||
stages += RunningStage.DIRECT
|
||||
stages += RunningStage.INDIRECT
|
||||
|
|
@ -1329,6 +1355,20 @@ class MainActivity : AppCompatActivity() {
|
|||
updateTileFromCategory(CATEGORY_ICM, update.result)
|
||||
if (animate) animateContentReveal(findingsIcmpSpoofing)
|
||||
}
|
||||
is CheckUpdate.RttTriangulationReady -> {
|
||||
markStageCompleted(RunningStage.RTT_TRIANGULATION)
|
||||
ensureCardVisible(cardRttTriangulation, animate = false)
|
||||
displayCategory(
|
||||
update.result,
|
||||
cardRttTriangulation,
|
||||
iconRttTriangulation,
|
||||
statusRttTriangulation,
|
||||
findingsRttTriangulation,
|
||||
activeCheckPrivacyMode,
|
||||
)
|
||||
updateTileFromCategory(CATEGORY_RTT, update.result)
|
||||
if (animate) animateContentReveal(findingsRttTriangulation)
|
||||
}
|
||||
is CheckUpdate.DirectSignsReady -> {
|
||||
markStageCompleted(RunningStage.DIRECT)
|
||||
ensureCardVisible(cardDirect, animate = false)
|
||||
|
|
@ -1414,6 +1454,7 @@ class MainActivity : AppCompatActivity() {
|
|||
RunningStage.INDIRECT -> CATEGORY_IND
|
||||
RunningStage.NATIVE_SIGNS -> CATEGORY_NAT
|
||||
RunningStage.ICMP -> CATEGORY_ICM
|
||||
RunningStage.RTT_TRIANGULATION -> CATEGORY_RTT
|
||||
RunningStage.LOCATION -> CATEGORY_LOC
|
||||
RunningStage.IP_CONSENSUS -> CATEGORY_IPS
|
||||
RunningStage.BYPASS -> CATEGORY_BYP
|
||||
|
|
@ -1447,6 +1488,14 @@ class MainActivity : AppCompatActivity() {
|
|||
findingsContainer = findingsIcmpSpoofing,
|
||||
hint = stageLoadingMessage(stage),
|
||||
)
|
||||
RunningStage.RTT_TRIANGULATION -> showCategoryLoading(
|
||||
stage = stage,
|
||||
card = cardRttTriangulation,
|
||||
icon = iconRttTriangulation,
|
||||
status = statusRttTriangulation,
|
||||
findingsContainer = findingsRttTriangulation,
|
||||
hint = stageLoadingMessage(stage),
|
||||
)
|
||||
RunningStage.DIRECT -> showCategoryLoading(
|
||||
stage = stage,
|
||||
card = cardDirect,
|
||||
|
|
@ -1569,6 +1618,13 @@ class MainActivity : AppCompatActivity() {
|
|||
findingsContainer = findingsIcmpSpoofing,
|
||||
message = stageStoppedMessage(stage),
|
||||
)
|
||||
RunningStage.RTT_TRIANGULATION -> showCategoryStopped(
|
||||
card = cardRttTriangulation,
|
||||
icon = iconRttTriangulation,
|
||||
status = statusRttTriangulation,
|
||||
findingsContainer = findingsRttTriangulation,
|
||||
message = stageStoppedMessage(stage),
|
||||
)
|
||||
RunningStage.DIRECT -> showCategoryStopped(
|
||||
card = cardDirect,
|
||||
icon = iconDirect,
|
||||
|
|
@ -1731,6 +1787,7 @@ class MainActivity : AppCompatActivity() {
|
|||
RunningStage.IP_COMPARISON -> getString(R.string.main_loading_ip_comparison)
|
||||
RunningStage.CDN_PULLING -> getString(R.string.main_loading_cdn_pulling)
|
||||
RunningStage.ICMP -> getString(R.string.main_loading_icmp)
|
||||
RunningStage.RTT_TRIANGULATION -> getString(R.string.main_loading_rtt_triangulation)
|
||||
RunningStage.DIRECT -> getString(R.string.main_loading_direct)
|
||||
RunningStage.INDIRECT -> getString(R.string.main_loading_indirect)
|
||||
RunningStage.NATIVE_SIGNS -> getString(R.string.main_loading_native_signs)
|
||||
|
|
@ -1753,6 +1810,7 @@ class MainActivity : AppCompatActivity() {
|
|||
RunningStage.IP_COMPARISON -> cardIpComparison
|
||||
RunningStage.CDN_PULLING -> cardCdnPulling
|
||||
RunningStage.ICMP -> cardIcmpSpoofing
|
||||
RunningStage.RTT_TRIANGULATION -> cardRttTriangulation
|
||||
RunningStage.DIRECT -> cardDirect
|
||||
RunningStage.INDIRECT -> cardIndirect
|
||||
RunningStage.NATIVE_SIGNS -> cardNativeSigns
|
||||
|
|
@ -1768,6 +1826,7 @@ class MainActivity : AppCompatActivity() {
|
|||
RunningStage.IP_COMPARISON -> statusIpComparison
|
||||
RunningStage.CDN_PULLING -> statusCdnPulling
|
||||
RunningStage.ICMP -> statusIcmpSpoofing
|
||||
RunningStage.RTT_TRIANGULATION -> statusRttTriangulation
|
||||
RunningStage.DIRECT -> statusDirect
|
||||
RunningStage.INDIRECT -> statusIndirect
|
||||
RunningStage.NATIVE_SIGNS -> statusNativeSigns
|
||||
|
|
@ -3090,6 +3149,7 @@ class MainActivity : AppCompatActivity() {
|
|||
private const val CATEGORY_IND = "ind"
|
||||
private const val CATEGORY_STN = "stn"
|
||||
private const val CATEGORY_ICM = "icmp"
|
||||
private const val CATEGORY_RTT = "rtt"
|
||||
private const val CATEGORY_LOC = "loc"
|
||||
private const val CATEGORY_BYP = "byp"
|
||||
private const val CATEGORY_NAT = "nat"
|
||||
|
|
@ -3350,6 +3410,10 @@ class MainActivity : AppCompatActivity() {
|
|||
if (findingsIcmpSpoofing.childCount > 0) return
|
||||
syncHintOnlyContainer(findingsIcmpSpoofing, previewMessageForCategory(id))
|
||||
}
|
||||
CATEGORY_RTT -> {
|
||||
if (findingsRttTriangulation.childCount > 0) return
|
||||
syncHintOnlyContainer(findingsRttTriangulation, previewMessageForCategory(id))
|
||||
}
|
||||
CATEGORY_LOC -> {
|
||||
if (locationInfoSection.childCount > 0 || findingsLocation.childCount > 0) return
|
||||
syncHintOnlyContainer(findingsLocation, previewMessageForCategory(id))
|
||||
|
|
@ -3372,6 +3436,7 @@ class MainActivity : AppCompatActivity() {
|
|||
CATEGORY_NAT -> getString(R.string.main_preview_native_signs)
|
||||
CATEGORY_STN -> getString(R.string.main_preview_call_transport)
|
||||
CATEGORY_ICM -> getString(R.string.main_preview_icmp)
|
||||
CATEGORY_RTT -> getString(R.string.main_preview_rtt_triangulation)
|
||||
CATEGORY_LOC -> getString(R.string.main_preview_location)
|
||||
CATEGORY_BYP -> getString(R.string.main_preview_bypass)
|
||||
else -> getString(R.string.tile_hint_placeholder)
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
const val PREF_CDN_PULLING_ENABLED = SettingsPrefs.PREF_CDN_PULLING_ENABLED
|
||||
const val PREF_CDN_PULLING_MEDUZA_ENABLED = SettingsPrefs.PREF_CDN_PULLING_MEDUZA_ENABLED
|
||||
const val PREF_CALL_TRANSPORT_PROBE_ENABLED = SettingsPrefs.PREF_CALL_TRANSPORT_PROBE_ENABLED
|
||||
const val PREF_RTT_TRIANGULATION_ENABLED = SettingsPrefs.PREF_RTT_TRIANGULATION_ENABLED
|
||||
const val PREF_TUN_PROBE_DEBUG_ENABLED = SettingsPrefs.PREF_TUN_PROBE_DEBUG_ENABLED
|
||||
const val PREF_TUN_PROBE_MODE_OVERRIDE = SettingsPrefs.PREF_TUN_PROBE_MODE_OVERRIDE
|
||||
const val PREF_DNS_RESOLVER_MODE = SettingsPrefs.PREF_DNS_RESOLVER_MODE
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ internal class SettingsNetworkFragment : Fragment(R.layout.fragment_settings_net
|
|||
private lateinit var switchCdnPullingMeduza: MaterialSwitch
|
||||
private lateinit var cardCallTransportProbe: MaterialCardView
|
||||
private lateinit var switchCallTransportProbe: MaterialSwitch
|
||||
private lateinit var cardRttTriangulation: MaterialCardView
|
||||
private lateinit var switchRttTriangulation: MaterialSwitch
|
||||
|
||||
private var suppressAutoUpdateToggleCallback = false
|
||||
private var suppressCdnPullingToggleCallback = false
|
||||
|
|
@ -55,6 +57,8 @@ internal class SettingsNetworkFragment : Fragment(R.layout.fragment_settings_net
|
|||
switchCdnPullingMeduza = view.findViewById(R.id.switchCdnPullingMeduza)
|
||||
cardCallTransportProbe = view.findViewById(R.id.cardCallTransportProbe)
|
||||
switchCallTransportProbe = view.findViewById(R.id.switchCallTransportProbe)
|
||||
cardRttTriangulation = view.findViewById(R.id.cardRttTriangulation)
|
||||
switchRttTriangulation = view.findViewById(R.id.switchRttTriangulation)
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
|
|
@ -64,11 +68,13 @@ internal class SettingsNetworkFragment : Fragment(R.layout.fragment_settings_net
|
|||
switchCdnPulling.isChecked = prefs.getBoolean(SettingsPrefs.PREF_CDN_PULLING_ENABLED, false)
|
||||
switchCdnPullingMeduza.isChecked = prefs.getBoolean(SettingsPrefs.PREF_CDN_PULLING_MEDUZA_ENABLED, true)
|
||||
switchCallTransportProbe.isChecked = prefs.getBoolean(SettingsPrefs.PREF_CALL_TRANSPORT_PROBE_ENABLED, false)
|
||||
switchRttTriangulation.isChecked = prefs.getBoolean(SettingsPrefs.PREF_RTT_TRIANGULATION_ENABLED, false)
|
||||
|
||||
updateAutoUpdateEnabled(networkRequestsEnabled)
|
||||
updateCdnPullingEnabled(networkRequestsEnabled)
|
||||
updateCdnPullingMeduzaVisible(switchCdnPulling.isChecked)
|
||||
updateCallTransportEnabled(networkRequestsEnabled)
|
||||
updateRttTriangulationEnabled(networkRequestsEnabled)
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
|
|
@ -81,6 +87,7 @@ internal class SettingsNetworkFragment : Fragment(R.layout.fragment_settings_net
|
|||
updateAutoUpdateEnabled(true)
|
||||
updateCdnPullingEnabled(true)
|
||||
updateCallTransportEnabled(true)
|
||||
updateRttTriangulationEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +113,10 @@ internal class SettingsNetworkFragment : Fragment(R.layout.fragment_settings_net
|
|||
switchCallTransportProbe.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit { putBoolean(SettingsPrefs.PREF_CALL_TRANSPORT_PROBE_ENABLED, isChecked) }
|
||||
}
|
||||
|
||||
switchRttTriangulation.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit { putBoolean(SettingsPrefs.PREF_RTT_TRIANGULATION_ENABLED, isChecked) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAutoUpdateEnabled(enabled: Boolean) {
|
||||
|
|
@ -134,6 +145,11 @@ internal class SettingsNetworkFragment : Fragment(R.layout.fragment_settings_net
|
|||
setViewAndChildrenEnabled(cardCallTransportProbe, enabled)
|
||||
}
|
||||
|
||||
private fun updateRttTriangulationEnabled(enabled: Boolean) {
|
||||
cardRttTriangulation.alpha = if (enabled) 1.0f else 0.5f
|
||||
setViewAndChildrenEnabled(cardRttTriangulation, enabled)
|
||||
}
|
||||
|
||||
private fun showCdnPullingWarning() {
|
||||
cdnWarningDialog = AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.settings_cdn_pulling_warning_title)
|
||||
|
|
@ -172,6 +188,7 @@ internal class SettingsNetworkFragment : Fragment(R.layout.fragment_settings_net
|
|||
updateAutoUpdateEnabled(false)
|
||||
updateCdnPullingEnabled(false)
|
||||
updateCallTransportEnabled(false)
|
||||
updateRttTriangulationEnabled(false)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
setNetworkRequestsSwitch(true)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ internal object SettingsPrefs {
|
|||
const val PREF_CDN_PULLING_ENABLED = "pref_cdn_pulling_enabled"
|
||||
const val PREF_CDN_PULLING_MEDUZA_ENABLED = "pref_cdn_pulling_meduza_enabled"
|
||||
const val PREF_CALL_TRANSPORT_PROBE_ENABLED = "pref_call_transport_probe_enabled"
|
||||
const val PREF_RTT_TRIANGULATION_ENABLED = "pref_rtt_triangulation_enabled"
|
||||
const val PREF_TUN_PROBE_DEBUG_ENABLED = "pref_tun_probe_debug_enabled"
|
||||
const val PREF_TUN_PROBE_MODE_OVERRIDE = "pref_tun_probe_mode_override"
|
||||
const val PREF_DNS_RESOLVER_MODE = "pref_dns_resolver_mode"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,328 @@
|
|||
package com.notcvnt.rknhardering.checker
|
||||
|
||||
import android.content.Context
|
||||
import com.notcvnt.rknhardering.R
|
||||
import com.notcvnt.rknhardering.ScanExecutionContext
|
||||
import com.notcvnt.rknhardering.model.CategoryResult
|
||||
import com.notcvnt.rknhardering.model.EvidenceConfidence
|
||||
import com.notcvnt.rknhardering.model.EvidenceItem
|
||||
import com.notcvnt.rknhardering.model.EvidenceSource
|
||||
import com.notcvnt.rknhardering.model.Finding
|
||||
import com.notcvnt.rknhardering.model.GeoIpFacts
|
||||
import com.notcvnt.rknhardering.network.DnsResolverConfig
|
||||
import com.notcvnt.rknhardering.network.ResolverNetworkStack
|
||||
import com.notcvnt.rknhardering.probe.SystemPingProber
|
||||
import com.notcvnt.rknhardering.util.HomeCountryResolver
|
||||
import java.io.IOException
|
||||
import java.net.Inet4Address
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal object RttTriangulationChecker {
|
||||
|
||||
private const val PING_COUNT = 4
|
||||
private const val THRESHOLD_HOME_MS = 80.0
|
||||
private const val JITTER_LIMIT_MS = 60.0
|
||||
|
||||
private val RU_TARGETS = listOf(
|
||||
"yandex.ru",
|
||||
"mail.ru",
|
||||
"vk.com",
|
||||
"sberbank.ru",
|
||||
"gosuslugi.ru",
|
||||
)
|
||||
|
||||
private val FOREIGN_TARGETS = listOf(
|
||||
"facebook.com",
|
||||
"github.com",
|
||||
"twitter.com",
|
||||
"reddit.com",
|
||||
"instagram.com",
|
||||
)
|
||||
|
||||
internal enum class TargetGroup { RU, FOREIGN }
|
||||
|
||||
internal data class TargetSpec(val host: String, val group: TargetGroup)
|
||||
|
||||
internal data class TargetOutcome(
|
||||
val spec: TargetSpec,
|
||||
val ip: String,
|
||||
val medianRtt: Double?,
|
||||
val jitter: Double?,
|
||||
val sent: Int,
|
||||
val received: Int,
|
||||
)
|
||||
|
||||
internal data class Dependencies(
|
||||
val resolveIpv4: (String, DnsResolverConfig) -> String = { host, resolverConfig ->
|
||||
val addresses = ResolverNetworkStack.lookup(
|
||||
hostname = host,
|
||||
config = resolverConfig,
|
||||
cancellationSignal = ScanExecutionContext.currentOrDefault().cancellationSignal,
|
||||
)
|
||||
val ipv4 = addresses.firstOrNull { it is Inet4Address }
|
||||
?: throw IOException("No IPv4 address resolved for $host")
|
||||
ipv4.hostAddress ?: throw IOException("Resolved IPv4 address is empty for $host")
|
||||
},
|
||||
val ping: suspend (String, Int) -> SystemPingProber.PingResult = { address, count ->
|
||||
SystemPingProber.probe(address = address, count = count)
|
||||
},
|
||||
)
|
||||
|
||||
@Volatile
|
||||
internal var dependenciesOverride: Dependencies? = null
|
||||
|
||||
suspend fun check(
|
||||
context: Context,
|
||||
resolverConfig: DnsResolverConfig,
|
||||
geoFacts: GeoIpFacts?,
|
||||
): CategoryResult = withContext(Dispatchers.IO) {
|
||||
val resolved = HomeCountryResolver.resolve(context, geoFacts)
|
||||
|
||||
if (resolved.country == null) {
|
||||
return@withContext CategoryResult(
|
||||
name = "RTT triangulation",
|
||||
detected = false,
|
||||
needsReview = true,
|
||||
findings = listOf(
|
||||
Finding(
|
||||
description = context.getString(R.string.checker_rtt_summary_inconclusive),
|
||||
needsReview = true,
|
||||
source = EvidenceSource.RTT_TRIANGULATION,
|
||||
confidence = EvidenceConfidence.LOW,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (resolved.country != "RU") {
|
||||
return@withContext CategoryResult(
|
||||
name = "RTT triangulation",
|
||||
detected = false,
|
||||
needsReview = true,
|
||||
findings = listOf(
|
||||
Finding(
|
||||
description = context.getString(R.string.checker_rtt_summary_unsupported_country),
|
||||
needsReview = true,
|
||||
source = EvidenceSource.RTT_TRIANGULATION,
|
||||
confidence = EvidenceConfidence.LOW,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val deps = dependenciesOverride ?: Dependencies()
|
||||
val allSpecs = RU_TARGETS.map { TargetSpec(it, TargetGroup.RU) } +
|
||||
FOREIGN_TARGETS.map { TargetSpec(it, TargetGroup.FOREIGN) }
|
||||
|
||||
val outcomes: List<TargetOutcome> = coroutineScope {
|
||||
allSpecs.map { spec ->
|
||||
async {
|
||||
val (ip, pingResult) = try {
|
||||
val address = deps.resolveIpv4(spec.host, resolverConfig)
|
||||
val result = deps.ping(address, PING_COUNT)
|
||||
address to result
|
||||
} catch (_: Throwable) {
|
||||
return@async TargetOutcome(
|
||||
spec = spec,
|
||||
ip = "unresolved",
|
||||
medianRtt = null,
|
||||
jitter = null,
|
||||
sent = 0,
|
||||
received = 0,
|
||||
)
|
||||
}
|
||||
val medianRtt = if (pingResult.received > 0) pingResult.avgRttMs else null
|
||||
val jitter = if (pingResult.received >= 2 &&
|
||||
pingResult.maxRttMs != null &&
|
||||
pingResult.minRttMs != null
|
||||
) {
|
||||
pingResult.maxRttMs - pingResult.minRttMs
|
||||
} else {
|
||||
null
|
||||
}
|
||||
TargetOutcome(
|
||||
spec = spec,
|
||||
ip = ip,
|
||||
medianRtt = medianRtt,
|
||||
jitter = jitter,
|
||||
sent = pingResult.sent,
|
||||
received = pingResult.received,
|
||||
)
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
|
||||
val ruOutcomes = outcomes.filter { it.spec.group == TargetGroup.RU }
|
||||
val foreignOutcomes = outcomes.filter { it.spec.group == TargetGroup.FOREIGN }
|
||||
|
||||
val homeMedian = median(ruOutcomes.mapNotNull { it.medianRtt })
|
||||
val foreignMedian = median(foreignOutcomes.mapNotNull { it.medianRtt })
|
||||
|
||||
val allReachable = outcomes.filter { it.medianRtt != null }
|
||||
val highJitterCount = allReachable.count { it.jitter != null && it.jitter > JITTER_LIMIT_MS }
|
||||
val highJitterMajority = allReachable.isNotEmpty() &&
|
||||
highJitterCount.toDouble() / allReachable.size > 0.5
|
||||
|
||||
val (detected, baseConfidence, needsReview, descriptionKey) = when {
|
||||
homeMedian == null && foreignMedian == null -> Quadruple(
|
||||
first = false,
|
||||
second = EvidenceConfidence.MEDIUM,
|
||||
third = true,
|
||||
fourth = R.string.checker_rtt_summary_unavailable,
|
||||
)
|
||||
homeMedian != null &&
|
||||
homeMedian > THRESHOLD_HOME_MS &&
|
||||
(foreignMedian == null || homeMedian > foreignMedian) -> Quadruple(
|
||||
first = true,
|
||||
second = EvidenceConfidence.MEDIUM,
|
||||
third = true,
|
||||
fourth = R.string.checker_rtt_summary_detected,
|
||||
)
|
||||
homeMedian != null &&
|
||||
homeMedian > THRESHOLD_HOME_MS &&
|
||||
foreignMedian != null &&
|
||||
homeMedian <= foreignMedian -> Quadruple(
|
||||
first = false,
|
||||
second = EvidenceConfidence.LOW,
|
||||
third = true,
|
||||
fourth = R.string.checker_rtt_summary_needs_review,
|
||||
)
|
||||
else -> Quadruple(
|
||||
first = false,
|
||||
second = EvidenceConfidence.MEDIUM,
|
||||
third = false,
|
||||
fourth = R.string.checker_rtt_summary_clean,
|
||||
)
|
||||
}
|
||||
|
||||
val finalConfidence = if (highJitterMajority) downgrade(baseConfidence) else baseConfidence
|
||||
|
||||
val mainFinding = Finding(
|
||||
description = context.getString(descriptionKey),
|
||||
detected = detected,
|
||||
needsReview = needsReview,
|
||||
source = EvidenceSource.RTT_TRIANGULATION,
|
||||
confidence = finalConfidence,
|
||||
)
|
||||
|
||||
val homeLabel = context.getString(R.string.checker_rtt_target_home)
|
||||
val foreignLabel = context.getString(R.string.checker_rtt_target_foreign)
|
||||
val unavailable = context.getString(R.string.checker_rtt_value_unavailable)
|
||||
|
||||
val mediansFinding = Finding(
|
||||
description = context.getString(
|
||||
R.string.checker_rtt_finding_medians,
|
||||
homeMedian?.let { String.format(Locale.US, "%.0f", it) } ?: unavailable,
|
||||
foreignMedian?.let { String.format(Locale.US, "%.0f", it) } ?: unavailable,
|
||||
THRESHOLD_HOME_MS,
|
||||
),
|
||||
isInformational = true,
|
||||
)
|
||||
|
||||
val perTargetFindings = outcomes.map { outcome ->
|
||||
val groupLabel = when (outcome.spec.group) {
|
||||
TargetGroup.RU -> homeLabel
|
||||
TargetGroup.FOREIGN -> foreignLabel
|
||||
}
|
||||
val description = if (outcome.medianRtt != null) {
|
||||
context.getString(
|
||||
R.string.checker_rtt_finding_target,
|
||||
groupLabel,
|
||||
outcome.spec.host,
|
||||
outcome.ip,
|
||||
outcome.received,
|
||||
outcome.sent,
|
||||
outcome.medianRtt,
|
||||
outcome.jitter ?: 0.0,
|
||||
)
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.checker_rtt_finding_target_unreachable,
|
||||
groupLabel,
|
||||
outcome.spec.host,
|
||||
outcome.ip,
|
||||
)
|
||||
}
|
||||
Finding(description = description, isInformational = true)
|
||||
}
|
||||
|
||||
val noteFindings = mutableListOf<Finding>()
|
||||
if (detected) {
|
||||
noteFindings += Finding(
|
||||
description = context.getString(R.string.checker_rtt_finding_threshold_note),
|
||||
isInformational = true,
|
||||
)
|
||||
}
|
||||
if (highJitterMajority) {
|
||||
noteFindings += Finding(
|
||||
description = context.getString(R.string.checker_rtt_finding_jitter_note),
|
||||
isInformational = true,
|
||||
)
|
||||
}
|
||||
|
||||
val allFindings = listOf(mainFinding, mediansFinding) + noteFindings + perTargetFindings
|
||||
|
||||
val evidence = outcomes.map { outcome ->
|
||||
val groupLabel = when (outcome.spec.group) {
|
||||
TargetGroup.RU -> context.getString(R.string.checker_rtt_target_home)
|
||||
TargetGroup.FOREIGN -> context.getString(R.string.checker_rtt_target_foreign)
|
||||
}
|
||||
val description = if (outcome.medianRtt != null) {
|
||||
context.getString(
|
||||
R.string.checker_rtt_evidence_template,
|
||||
"[$groupLabel] ${outcome.spec.host}",
|
||||
outcome.medianRtt,
|
||||
outcome.jitter ?: 0.0,
|
||||
)
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.checker_rtt_evidence_unreachable,
|
||||
"[$groupLabel] ${outcome.spec.host}",
|
||||
)
|
||||
}
|
||||
val evidenceDetected = detected &&
|
||||
outcome.spec.group == TargetGroup.RU &&
|
||||
outcome.medianRtt != null &&
|
||||
outcome.medianRtt > THRESHOLD_HOME_MS
|
||||
EvidenceItem(
|
||||
source = EvidenceSource.RTT_TRIANGULATION,
|
||||
detected = evidenceDetected,
|
||||
confidence = EvidenceConfidence.MEDIUM,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
|
||||
CategoryResult(
|
||||
name = "RTT triangulation",
|
||||
detected = detected,
|
||||
needsReview = needsReview,
|
||||
findings = allFindings,
|
||||
evidence = evidence,
|
||||
)
|
||||
}
|
||||
|
||||
private fun median(values: List<Double>): Double? {
|
||||
if (values.isEmpty()) return null
|
||||
val sorted = values.sorted()
|
||||
val mid = sorted.size / 2
|
||||
return if (sorted.size % 2 == 0) (sorted[mid - 1] + sorted[mid]) / 2.0 else sorted[mid]
|
||||
}
|
||||
|
||||
private fun downgrade(confidence: EvidenceConfidence): EvidenceConfidence = when (confidence) {
|
||||
EvidenceConfidence.HIGH -> EvidenceConfidence.MEDIUM
|
||||
EvidenceConfidence.MEDIUM -> EvidenceConfidence.LOW
|
||||
EvidenceConfidence.LOW -> EvidenceConfidence.LOW
|
||||
}
|
||||
|
||||
private data class Quadruple(
|
||||
val first: Boolean,
|
||||
val second: EvidenceConfidence,
|
||||
val third: Boolean,
|
||||
val fourth: Int,
|
||||
)
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ data class CheckSettings(
|
|||
val cdnPullingEnabled: Boolean = false,
|
||||
val cdnPullingMeduzaEnabled: Boolean = true,
|
||||
val icmpSpoofingEnabled: Boolean = true,
|
||||
val rttTriangulationEnabled: Boolean = false,
|
||||
val tunProbeDebugEnabled: Boolean = false,
|
||||
val tunProbeModeOverride: TunProbeModeOverride = TunProbeModeOverride.AUTO,
|
||||
val resolverConfig: DnsResolverConfig = DnsResolverConfig.system(),
|
||||
|
|
@ -50,6 +51,7 @@ sealed interface CheckUpdate {
|
|||
data class IpComparisonReady(val result: IpComparisonResult) : CheckUpdate
|
||||
data class CdnPullingReady(val result: CdnPullingResult) : CheckUpdate
|
||||
data class IcmpSpoofingReady(val result: CategoryResult) : CheckUpdate
|
||||
data class RttTriangulationReady(val result: CategoryResult) : CheckUpdate
|
||||
data class DirectSignsReady(val result: CategoryResult) : CheckUpdate
|
||||
data class IndirectSignsReady(val result: CategoryResult) : CheckUpdate
|
||||
data class LocationSignalsReady(val result: CategoryResult) : CheckUpdate
|
||||
|
|
@ -71,6 +73,8 @@ object VpnCheckRunner {
|
|||
{ ctx, resolverConfig, meduzaEnabled -> CdnPullingChecker.check(ctx, resolverConfig = resolverConfig, meduzaEnabled = meduzaEnabled) },
|
||||
val icmpSpoofingCheck: suspend (Context, DnsResolverConfig) -> CategoryResult =
|
||||
{ ctx, resolverConfig -> IcmpSpoofingChecker.check(ctx, resolverConfig) },
|
||||
val rttTriangulationCheck: suspend (Context, DnsResolverConfig, GeoIpFacts?) -> CategoryResult =
|
||||
{ ctx, cfg, geo -> RttTriangulationChecker.check(ctx, cfg, geo) },
|
||||
val underlyingProbe: suspend (
|
||||
Context,
|
||||
DnsResolverConfig,
|
||||
|
|
@ -191,6 +195,18 @@ object VpnCheckRunner {
|
|||
),
|
||||
),
|
||||
)
|
||||
fun rtt(error: Throwable): CategoryResult = CategoryResult(
|
||||
name = "RTT triangulation",
|
||||
detected = false,
|
||||
needsReview = true,
|
||||
findings = listOf(
|
||||
Finding(
|
||||
error.message ?: error::class.java.simpleName,
|
||||
isError = true,
|
||||
source = EvidenceSource.RTT_TRIANGULATION,
|
||||
),
|
||||
),
|
||||
)
|
||||
fun direct(context: Context, error: Throwable): CategoryResult = CategoryResult(
|
||||
name = context.getString(R.string.checker_direct_category_name),
|
||||
detected = false,
|
||||
|
|
@ -257,6 +273,13 @@ object VpnCheckRunner {
|
|||
}
|
||||
} else null
|
||||
|
||||
val rttTriangulationDeferred = if (settings.networkRequestsEnabled && settings.rttTriangulationEnabled) {
|
||||
safeAsync(context = Dispatchers.IO, fallback = { Fallbacks.rtt(it) }) {
|
||||
val geoFacts = geoIpDeferred?.await()?.geoFacts
|
||||
dependencies.rttTriangulationCheck(context, settings.resolverConfig, geoFacts)
|
||||
}
|
||||
} else null
|
||||
|
||||
val tunActiveProbeDeferred = if (settings.splitTunnelEnabled) {
|
||||
safeAsync(fallback = { Fallbacks.probe(it) }) {
|
||||
dependencies.underlyingProbe(
|
||||
|
|
@ -341,6 +364,14 @@ object VpnCheckRunner {
|
|||
}
|
||||
}
|
||||
}
|
||||
val rttTriangulationReadyDeferred = rttTriangulationDeferred?.let { deferred ->
|
||||
async {
|
||||
deferred.await().also { result ->
|
||||
executionContext.throwIfCancelled()
|
||||
onUpdate?.invoke(CheckUpdate.RttTriangulationReady(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
val directReadyDeferred = async {
|
||||
directDeferred.await().also { result ->
|
||||
executionContext.throwIfCancelled()
|
||||
|
|
@ -399,6 +430,11 @@ object VpnCheckRunner {
|
|||
detected = false,
|
||||
findings = emptyList(),
|
||||
)
|
||||
val emptyRttTriangulation = CategoryResult(
|
||||
name = context.getString(R.string.main_card_rtt_triangulation),
|
||||
detected = false,
|
||||
findings = emptyList(),
|
||||
)
|
||||
val emptyBypass = BypassResult(
|
||||
proxyEndpoint = null,
|
||||
proxyOwner = null,
|
||||
|
|
@ -415,6 +451,7 @@ object VpnCheckRunner {
|
|||
val ipComparison = ipComparisonReadyDeferred?.await() ?: emptyIpComparison
|
||||
val cdnPulling = cdnPullingReadyDeferred?.await() ?: emptyCdnPulling
|
||||
val icmpSpoofing = icmpSpoofingReadyDeferred?.await() ?: emptyIcmpSpoofing
|
||||
val rttTriangulation = rttTriangulationReadyDeferred?.await() ?: emptyRttTriangulation
|
||||
val directSigns = directReadyDeferred.await()
|
||||
val indirectSigns = indirectReadyDeferred.await()
|
||||
val locationSignals = locationReadyDeferred.await()
|
||||
|
|
@ -463,6 +500,7 @@ object VpnCheckRunner {
|
|||
tunProbeDiagnostics = tunProbeResult?.tunProbeDiagnostics,
|
||||
nativeSigns = nativeSigns,
|
||||
icmpSpoofing = icmpSpoofing,
|
||||
rttTriangulation = rttTriangulation,
|
||||
ipConsensus = ipConsensus,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ enum class EvidenceSource {
|
|||
DIRECT_NETWORK_CAPABILITIES,
|
||||
INDIRECT_NETWORK_CAPABILITIES,
|
||||
ICMP_SPOOFING,
|
||||
RTT_TRIANGULATION,
|
||||
SYSTEM_PROXY,
|
||||
INSTALLED_APP,
|
||||
VPN_SERVICE_DECLARATION,
|
||||
|
|
@ -347,5 +348,10 @@ data class CheckResult(
|
|||
detected = false,
|
||||
findings = emptyList(),
|
||||
),
|
||||
val rttTriangulation: CategoryResult = CategoryResult(
|
||||
name = "",
|
||||
detected = false,
|
||||
findings = emptyList(),
|
||||
),
|
||||
val ipConsensus: IpConsensusResult = IpConsensusResult.empty(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.notcvnt.rknhardering.util
|
||||
|
||||
import android.content.Context
|
||||
import android.telephony.TelephonyManager
|
||||
import com.notcvnt.rknhardering.model.GeoIpFacts
|
||||
|
||||
internal object HomeCountryResolver {
|
||||
|
||||
data class Resolved(
|
||||
val country: String?,
|
||||
val sources: List<String>,
|
||||
val conflict: Boolean,
|
||||
)
|
||||
|
||||
fun resolve(context: Context, geoFacts: GeoIpFacts?): Resolved {
|
||||
val geo = geoFacts?.countryCode?.trim()?.uppercase()?.takeIf { it.isNotEmpty() }
|
||||
if (geo != null) {
|
||||
return Resolved(geo, listOf("geoip"), conflict = false)
|
||||
}
|
||||
|
||||
val telephony = linkedMapOf<String, String>()
|
||||
runCatching {
|
||||
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
|
||||
tm?.simCountryIso?.trim()?.uppercase()?.takeIf { it.isNotEmpty() }
|
||||
?.let { telephony["sim"] = it }
|
||||
tm?.networkCountryIso?.trim()?.uppercase()?.takeIf { it.isNotEmpty() }
|
||||
?.let { telephony["network"] = it }
|
||||
}
|
||||
|
||||
if (telephony.isEmpty()) return Resolved(null, emptyList(), false)
|
||||
|
||||
val unique = telephony.values.toSet()
|
||||
return if (unique.size == 1) {
|
||||
Resolved(unique.single(), telephony.keys.toList(), conflict = false)
|
||||
} else {
|
||||
Resolved(null, telephony.keys.toList(), conflict = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1204,6 +1204,139 @@
|
|||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardRttTriangulation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
app:cardBackgroundColor="@android:color/transparent"
|
||||
app:cardCornerRadius="0dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/headerRttTriangulation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_category_header_collapsed"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="48dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingTop="11dp"
|
||||
android:paddingEnd="14dp"
|
||||
android:paddingBottom="11dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/headerDotRttTriangulation"
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="8dp"
|
||||
android:background="@drawable/indicator_status_neutral" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/headerIconRttTriangulation"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:contentDescription="@null"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_pin"
|
||||
app:tint="?android:attr/textColorSecondary" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headerTitleRttTriangulation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="RTT-триангуляция" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headerHintRttTriangulation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="12sp"
|
||||
tools:text="Измерение задержек RTT" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/chevronRttTriangulation"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@null"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_chevron_right"
|
||||
app:tint="?android:attr/textColorSecondary" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bodyRttTriangulation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_category_body"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="14dp"
|
||||
android:paddingBottom="14dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iconRttTriangulation"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="@null"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusRttTriangulation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/findingsRttTriangulation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="vertical" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardLocation"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -246,5 +246,52 @@
|
|||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardRttTriangulation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceContainer"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_rtt_triangulation_title"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/settings_rtt_triangulation_description"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switchRttTriangulation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="false" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@
|
|||
<string name="main_preview_native_signs">بررسی اینترفیسهای بومی، مسیرها و نشانههای سطحپایین کتابخانهها انجام خواهد شد.</string>
|
||||
<string name="main_preview_call_transport">پاسخهای STUN serverها و نشانههای نشت call transport بررسی خواهند شد.</string>
|
||||
<string name="main_preview_icmp">resolve و ping میزبانهای مسدود و کنترلی از طریق مسیر فعال انجام خواهد شد.</string>
|
||||
<string name="main_preview_rtt_triangulation">سرویسهای محلی و خارجی پینگ خواهند شد تا تاخیر مقایسه شود.</string>
|
||||
<string name="main_preview_location">جمعآوری نشانههای اپراتور، دکل و Wi-Fiهای نزدیک انجام خواهد شد.</string>
|
||||
<string name="main_preview_bypass">جستوجوی split tunnel bypass محلی و proxy endpointهای در دسترس انجام خواهد شد.</string>
|
||||
|
||||
|
|
@ -204,6 +205,7 @@
|
|||
<string name="main_loading_ip_comparison">در حال مقایسه IP عمومی از طریق چند سرویس مستقل.</string>
|
||||
<string name="main_loading_cdn_pulling">در حال بررسی اینکه endpointهای مسدود یا CDN-backed چه دادهای درباره شبکه شما برمیگردانند.</string>
|
||||
<string name="main_loading_icmp">در حال resolve و ping کردن میزبانهای مسدود و کنترلی از طریق مسیر فعال.</string>
|
||||
<string name="main_loading_rtt_triangulation">در حال پینگ سرویسهای محلی و خارجی برای مقایسه تاخیر.</string>
|
||||
<string name="main_loading_direct">در حال جستوجوی نشانههای مستقیم VPN، proxy و کلاینتهای نصبشده.</string>
|
||||
<string name="main_loading_indirect">در حال بررسی اینترفیسها، DNS، مسیرها و نشانههای فنی محلی.</string>
|
||||
<string name="main_loading_native_signs">در حال بررسی اینترفیسهای بومی، مسیرها و نشانههای سطحپایین کتابخانهها.</string>
|
||||
|
|
@ -271,6 +273,26 @@
|
|||
<string name="checker_icmp_evidence_both">%1$s پاسخ داد و RTT برای %2$s پایینتر از آستانه مورد انتظار ماند</string>
|
||||
<string name="checker_icmp_evidence_blocked_replied">%1$s روی مسیر فعال از طریق ICMP پاسخ داد</string>
|
||||
<string name="checker_icmp_evidence_control_too_fast">RTT برای %1$s روی مسیر فعال پایینتر از آستانه مورد انتظار ماند</string>
|
||||
<string name="main_card_rtt_triangulation">مثلثبندی RTT β</string>
|
||||
<string name="main_card_rtt_triangulation_description">شناسایی VPN با مقایسهٔ تاخیر به سرویسهای محلی و خارجی</string>
|
||||
<string name="checker_rtt_summary_clean">الگوی تاخیر با منطقهٔ شما مطابقت دارد</string>
|
||||
<string name="checker_rtt_summary_detected">سرویسهای محلی کندتر از خارجی هستند — احتمال تونل</string>
|
||||
<string name="checker_rtt_summary_needs_review">تاخیر غیرمعمول است اما قطعی نیست</string>
|
||||
<string name="checker_rtt_summary_inconclusive">کشور خانگی شناسایی نشد</string>
|
||||
<string name="checker_rtt_summary_unsupported_country">در این نسخه فقط روسیه پشتیبانی میشود</string>
|
||||
<string name="checker_rtt_summary_unavailable">اندازهگیری RTT در دسترس نیست</string>
|
||||
<string name="checker_rtt_target_home">محلی</string>
|
||||
<string name="checker_rtt_target_foreign">خارجی</string>
|
||||
<string name="checker_rtt_evidence_template">%1$s — %2$.0f ms (لرزش %3$.0f ms)</string>
|
||||
<string name="checker_rtt_evidence_unreachable">%1$s — در دسترس نیست</string>
|
||||
<string name="checker_rtt_finding_target">%1$s %2$s (%3$s): پاسخها %4$d/%5$d، avg %6$.0f ms، jitter %7$.0f ms</string>
|
||||
<string name="checker_rtt_finding_target_unreachable">%1$s %2$s (%3$s): در دسترس نیست</string>
|
||||
<string name="checker_rtt_finding_medians">میانه تاخیر: محلی %1$s ms، خارجی %2$s ms (حد %3$.0f ms)</string>
|
||||
<string name="checker_rtt_finding_threshold_note">میانه محلی بالاتر از حد، یعنی ترافیک پیش از رسیدن به سرویسهای محلی، از کشور خارج میشود.</string>
|
||||
<string name="checker_rtt_finding_jitter_note">jitter بالا در اکثر مقاصد — اطمینان کاهش یافت.</string>
|
||||
<string name="checker_rtt_value_unavailable">ندارد</string>
|
||||
<string name="settings_rtt_triangulation_title">مثلثبندی RTT</string>
|
||||
<string name="settings_rtt_triangulation_description">آزمایشی: با پینگ سرویسهای محلی و خارجی، VPN را از طریق ناهنجاریهای تاخیر شناسایی میکند. ممکن است در شبکههای موبایل، ماهوارهای، CGNAT و شلوغ نتایج اشتباه بدهد.</string>
|
||||
|
||||
<string name="narrative_meaning_detected">نتیجه قرمز یعنی بررسی خودکار نشانههای کافی از دور زدن را جمع کرده است.</string>
|
||||
<string name="narrative_meaning_needs_review">نتیجه زرد یعنی نشانههایی وجود دارد، اما نیاز به بررسی دستی دارد.</string>
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@
|
|||
<string name="main_preview_native_signs">Будет выполнена проверка нативных интерфейсов, маршрутов и низкоуровневых сигналов библиотек.</string>
|
||||
<string name="main_preview_call_transport">Будут проверены ответы STUN-серверов и сигналы утечки call transport.</string>
|
||||
<string name="main_preview_icmp">Будет выполнен резолв и пинг заблокированной и контрольной цели через активный маршрут.</string>
|
||||
<string name="main_preview_rtt_triangulation">Будут пингнуты локальные и зарубежные сервисы для сравнения задержек.</string>
|
||||
<string name="main_preview_location">Будет выполнен сбор сигналов оператора, вышек и ближайших Wi-Fi точек.</string>
|
||||
<string name="main_preview_bypass">Будет выполнен поиск локального split tunnel bypass и доступных proxy endpoints.</string>
|
||||
|
||||
|
|
@ -204,6 +205,7 @@
|
|||
<string name="main_loading_ip_comparison">Сравниваем внешний IP через несколько независимых сервисов.</string>
|
||||
<string name="main_loading_cdn_pulling">Проверяем, какие сетевые данные вернут заблокированные или CDN-backed endpoints.</string>
|
||||
<string name="main_loading_icmp">Резолвим и пингуем заблокированную и контрольную цели через активный маршрут.</string>
|
||||
<string name="main_loading_rtt_triangulation">Пингуем локальные и зарубежные сервисы для сравнения задержек.</string>
|
||||
<string name="main_loading_direct">Ищем прямые признаки VPN, прокси и установленных клиентов.</string>
|
||||
<string name="main_loading_indirect">Проверяем интерфейсы, DNS, маршруты и локальные технические сигналы.</string>
|
||||
<string name="main_loading_native_signs">Проверяем нативные интерфейсы, маршруты и низкоуровневые сигналы библиотек.</string>
|
||||
|
|
@ -557,6 +559,26 @@
|
|||
<string name="checker_icmp_evidence_both">%1$s ответил, а RTT у %2$s оказался ниже ожидаемого порога</string>
|
||||
<string name="checker_icmp_evidence_blocked_replied">%1$s ответил по ICMP через активный маршрут</string>
|
||||
<string name="checker_icmp_evidence_control_too_fast">%1$s показал RTT ниже ожидаемого порога на активном маршруте</string>
|
||||
<string name="main_card_rtt_triangulation">RTT-триангуляция β</string>
|
||||
<string name="main_card_rtt_triangulation_description">Детекция VPN по задержкам до отечественных и зарубежных сервисов</string>
|
||||
<string name="checker_rtt_summary_clean">Профиль задержек соответствует региону</string>
|
||||
<string name="checker_rtt_summary_detected">Локальные сервисы медленнее зарубежных — возможен туннель</string>
|
||||
<string name="checker_rtt_summary_needs_review">Задержки нестандартные, требуется ручная проверка</string>
|
||||
<string name="checker_rtt_summary_inconclusive">Не удалось определить домашнюю страну</string>
|
||||
<string name="checker_rtt_summary_unsupported_country">RTT-триангуляция в этой сборке поддерживает только РФ</string>
|
||||
<string name="checker_rtt_summary_unavailable">Замеры RTT недоступны</string>
|
||||
<string name="checker_rtt_target_home">Локальный</string>
|
||||
<string name="checker_rtt_target_foreign">Зарубежный</string>
|
||||
<string name="checker_rtt_evidence_template">%1$s — %2$.0f мс (jitter %3$.0f мс)</string>
|
||||
<string name="checker_rtt_evidence_unreachable">%1$s — недоступен</string>
|
||||
<string name="checker_rtt_finding_target">%1$s %2$s (%3$s): ответов %4$d/%5$d, avg %6$.0f мс, jitter %7$.0f мс</string>
|
||||
<string name="checker_rtt_finding_target_unreachable">%1$s %2$s (%3$s): недоступен</string>
|
||||
<string name="checker_rtt_finding_medians">Медиана задержек: локальные %1$s мс, зарубежные %2$s мс (порог %3$.0f мс)</string>
|
||||
<string name="checker_rtt_finding_threshold_note">Локальная медиана выше порога означает, что трафик выходит за пределы страны прежде чем достичь локальных сервисов.</string>
|
||||
<string name="checker_rtt_finding_jitter_note">Высокий jitter у большинства таргетов — уверенность понижена.</string>
|
||||
<string name="checker_rtt_value_unavailable">н/д</string>
|
||||
<string name="settings_rtt_triangulation_title">RTT-триангуляция</string>
|
||||
<string name="settings_rtt_triangulation_description">Эксперимент: пингует локальные и зарубежные сервисы для детекции VPN по аномалиям задержки. Возможны ложные срабатывания в мобильных, спутниковых, CGNAT и перегруженных сетях.</string>
|
||||
<string name="checker_native_card_title">Нативные низкоуровневые проверки β</string>
|
||||
<string name="checker_native_unavailable">Нативная библиотека не загружена; проверки недоступны.</string>
|
||||
<string name="checker_native_unavailable_with_reason">Нативная библиотека не загружена: %1$s</string>
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@
|
|||
<string name="main_preview_native_signs">将检查原生接口、路由和底层库信号。</string>
|
||||
<string name="main_preview_call_transport">将检查 STUN 服务器响应和 call transport 泄漏信号。</string>
|
||||
<string name="main_preview_icmp">将通过当前活动路由解析并 ping 被封锁和对照主机。</string>
|
||||
<string name="main_preview_rtt_triangulation">将 ping 本地和境外服务以比较往返延迟。</string>
|
||||
<string name="main_preview_location">将收集运营商、基站和附近 Wi-Fi 的迹象。</string>
|
||||
<string name="main_preview_bypass">将搜索本地 split tunnel bypass 和可用的 proxy endpoint。</string>
|
||||
|
||||
|
|
@ -204,6 +205,7 @@
|
|||
<string name="main_loading_ip_comparison">正在通过多个独立服务比较公网 IP。</string>
|
||||
<string name="main_loading_cdn_pulling">正在检查被封锁或 CDN 支持的端点会返回哪些网络数据。</string>
|
||||
<string name="main_loading_icmp">正在通过当前活动路由解析并 ping 被封锁和对照主机。</string>
|
||||
<string name="main_loading_rtt_triangulation">正在 ping 本地和境外服务以比较往返延迟。</string>
|
||||
<string name="main_loading_direct">正在查找 VPN、代理和已安装客户端的直接迹象。</string>
|
||||
<string name="main_loading_indirect">正在检查接口、DNS、路由和本地技术迹象。</string>
|
||||
<string name="main_loading_native_signs">正在检查原生接口、路由和底层库信号。</string>
|
||||
|
|
@ -270,6 +272,26 @@
|
|||
<string name="checker_icmp_evidence_both">%1$s 已响应,且 %2$s 的 RTT 一直低于预期阈值</string>
|
||||
<string name="checker_icmp_evidence_blocked_replied">%1$s 在当前活动路由上通过 ICMP 返回了响应</string>
|
||||
<string name="checker_icmp_evidence_control_too_fast">%1$s 的 RTT 在当前活动路由上一直低于预期阈值</string>
|
||||
<string name="main_card_rtt_triangulation">RTT 三角定位 β</string>
|
||||
<string name="main_card_rtt_triangulation_description">通过比较本地与境外服务的延迟检测 VPN</string>
|
||||
<string name="checker_rtt_summary_clean">延迟特征与您所在地区一致</string>
|
||||
<string name="checker_rtt_summary_detected">本地服务慢于境外 — 可能存在隧道</string>
|
||||
<string name="checker_rtt_summary_needs_review">延迟异常但无法确定</string>
|
||||
<string name="checker_rtt_summary_inconclusive">无法确定主国家</string>
|
||||
<string name="checker_rtt_summary_unsupported_country">本版本仅支持俄罗斯</string>
|
||||
<string name="checker_rtt_summary_unavailable">RTT 探测不可用</string>
|
||||
<string name="checker_rtt_target_home">本地</string>
|
||||
<string name="checker_rtt_target_foreign">境外</string>
|
||||
<string name="checker_rtt_evidence_template">%1$s — %2$.0f 毫秒 (抖动 %3$.0f 毫秒)</string>
|
||||
<string name="checker_rtt_evidence_unreachable">%1$s — 无法连接</string>
|
||||
<string name="checker_rtt_finding_target">%1$s %2$s (%3$s): 回应 %4$d/%5$d, avg %6$.0f 毫秒, jitter %7$.0f 毫秒</string>
|
||||
<string name="checker_rtt_finding_target_unreachable">%1$s %2$s (%3$s): 无法连接</string>
|
||||
<string name="checker_rtt_finding_medians">延迟中位数: 本地 %1$s 毫秒, 境外 %2$s 毫秒 (阈值 %3$.0f 毫秒)</string>
|
||||
<string name="checker_rtt_finding_threshold_note">本地中位数高于阈值表示流量在到达本地服务之前已离开本国。</string>
|
||||
<string name="checker_rtt_finding_jitter_note">大多数目标的 jitter 较高 — 置信度已降低。</string>
|
||||
<string name="checker_rtt_value_unavailable">无</string>
|
||||
<string name="settings_rtt_triangulation_title">RTT 三角定位</string>
|
||||
<string name="settings_rtt_triangulation_description">实验性:通过 ping 本地与境外服务,根据延迟异常检测 VPN。在移动、卫星、CGNAT 或拥塞网络中可能误报。</string>
|
||||
|
||||
<string name="narrative_meaning_detected">红色结论表示自动检查已经收集到足够的绕过迹象。</string>
|
||||
<string name="narrative_meaning_needs_review">黄色结论表示存在一些迹象,但仍需人工复核。</string>
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@
|
|||
<string name="main_preview_native_signs">Native interfaces, routes, and low-level library signals will be checked.</string>
|
||||
<string name="main_preview_call_transport">STUN server responses and call transport leak signals will be checked.</string>
|
||||
<string name="main_preview_icmp">Blocked and control hosts will be resolved and pinged through the active route.</string>
|
||||
<string name="main_preview_rtt_triangulation">Local and foreign services will be pinged to compare round-trip latency.</string>
|
||||
<string name="main_preview_location">Carrier, cell tower, and nearby Wi-Fi signals will be collected.</string>
|
||||
<string name="main_preview_bypass">A search for local split tunnel bypasses and available proxy endpoints will be performed.</string>
|
||||
|
||||
|
|
@ -203,6 +204,7 @@
|
|||
<string name="main_loading_ip_comparison">Comparing the public IP through multiple independent services.</string>
|
||||
<string name="main_loading_cdn_pulling">Checking what blocked or CDN-backed endpoints return about your network.</string>
|
||||
<string name="main_loading_icmp">Resolving and pinging blocked and control hosts through the active route.</string>
|
||||
<string name="main_loading_rtt_triangulation">Pinging local and foreign services to compare round-trip latency.</string>
|
||||
<string name="main_loading_direct">Looking for direct signs of VPNs, proxies, and installed clients.</string>
|
||||
<string name="main_loading_indirect">Checking interfaces, DNS, routes, and local technical signals.</string>
|
||||
<string name="main_loading_native_signs">Checking native interfaces, routes, and low-level library signals.</string>
|
||||
|
|
@ -552,6 +554,26 @@
|
|||
<string name="checker_icmp_evidence_both">%1$s replied and %2$s RTT stayed below the expected threshold</string>
|
||||
<string name="checker_icmp_evidence_blocked_replied">%1$s replied over ICMP on the active route</string>
|
||||
<string name="checker_icmp_evidence_control_too_fast">%1$s RTT stayed below the expected threshold on the active route</string>
|
||||
<string name="main_card_rtt_triangulation">RTT triangulation β</string>
|
||||
<string name="main_card_rtt_triangulation_description">Detects VPN by comparing latency to local vs foreign services</string>
|
||||
<string name="checker_rtt_summary_clean">Latency profile matches your region</string>
|
||||
<string name="checker_rtt_summary_detected">Local services slower than foreign — possible tunnel</string>
|
||||
<string name="checker_rtt_summary_needs_review">Latency unusual but inconclusive</string>
|
||||
<string name="checker_rtt_summary_inconclusive">Home country undetermined</string>
|
||||
<string name="checker_rtt_summary_unsupported_country">RTT triangulation supports RU only in this build</string>
|
||||
<string name="checker_rtt_summary_unavailable">RTT probes unavailable</string>
|
||||
<string name="checker_rtt_target_home">Local</string>
|
||||
<string name="checker_rtt_target_foreign">Foreign</string>
|
||||
<string name="checker_rtt_evidence_template">%1$s — %2$.0f ms (jitter %3$.0f ms)</string>
|
||||
<string name="checker_rtt_evidence_unreachable">%1$s — unreachable</string>
|
||||
<string name="checker_rtt_finding_target">%1$s %2$s (%3$s): %4$d/%5$d replies, avg %6$.0f ms, jitter %7$.0f ms</string>
|
||||
<string name="checker_rtt_finding_target_unreachable">%1$s %2$s (%3$s): unreachable</string>
|
||||
<string name="checker_rtt_finding_medians">Median latency: local %1$s ms, foreign %2$s ms (threshold %3$.0f ms)</string>
|
||||
<string name="checker_rtt_finding_threshold_note">A local median above the threshold suggests traffic is leaving the country before reaching local services.</string>
|
||||
<string name="checker_rtt_finding_jitter_note">High jitter on the majority of targets — confidence reduced.</string>
|
||||
<string name="checker_rtt_value_unavailable">n/a</string>
|
||||
<string name="settings_rtt_triangulation_title">RTT triangulation</string>
|
||||
<string name="settings_rtt_triangulation_description">Experimental: pings local and foreign servers to detect VPN by latency anomalies. May produce false positives on cellular, satellite, CGNAT, or congested networks.</string>
|
||||
<string name="checker_native_card_title">Native low-level checks β</string>
|
||||
<string name="checker_native_unavailable">Native library not loaded; native checks are unavailable.</string>
|
||||
<string name="checker_native_unavailable_with_reason">Native library not loaded: %1$s</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,208 @@
|
|||
package com.notcvnt.rknhardering.checker
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.notcvnt.rknhardering.model.EvidenceConfidence
|
||||
import com.notcvnt.rknhardering.model.GeoIpFacts
|
||||
import com.notcvnt.rknhardering.network.DnsResolverConfig
|
||||
import com.notcvnt.rknhardering.probe.SystemPingProber
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import java.io.IOException
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class RttTriangulationCheckerTest {
|
||||
|
||||
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||
private val ruGeoFacts = GeoIpFacts(countryCode = "RU")
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
RttTriangulationChecker.dependenciesOverride = null
|
||||
}
|
||||
|
||||
// Case 1: no geoFacts, no SIM/network (Robolectric TelephonyManager returns empty ISOs)
|
||||
// → country undetermined → detected=false, needsReview=true, finding contains "country undetermined"
|
||||
@Test
|
||||
fun `home_country_unknown_returns_inconclusive`() = runBlocking {
|
||||
val result = RttTriangulationChecker.check(
|
||||
context = context,
|
||||
resolverConfig = DnsResolverConfig.system(),
|
||||
geoFacts = null,
|
||||
)
|
||||
|
||||
assertFalse(result.detected)
|
||||
assertTrue(result.needsReview)
|
||||
assertTrue(result.findings.any { "country undetermined" in it.description })
|
||||
}
|
||||
|
||||
// Case 2: RU 25ms, foreign 120ms, country=RU
|
||||
// homeMedian=25 ≤ 80 → "no anomaly" branch → detected=false, needsReview=false
|
||||
@Test
|
||||
fun `home_fast_foreign_slow_clean`() = runBlocking {
|
||||
RttTriangulationChecker.dependenciesOverride = uniformDependencies(
|
||||
ruAvgMs = 25.0,
|
||||
foreignAvgMs = 120.0,
|
||||
)
|
||||
|
||||
val result = RttTriangulationChecker.check(
|
||||
context = context,
|
||||
resolverConfig = DnsResolverConfig.system(),
|
||||
geoFacts = ruGeoFacts,
|
||||
)
|
||||
|
||||
assertFalse(result.detected)
|
||||
assertFalse(result.needsReview)
|
||||
}
|
||||
|
||||
// Case 3: RU 150ms, foreign 30ms, country=RU
|
||||
// homeMedian=150 > 80, foreignMedian=30, homeMedian > foreignMedian → detected=true, MEDIUM
|
||||
@Test
|
||||
fun `home_slow_foreign_fast_detected_medium`() = runBlocking {
|
||||
RttTriangulationChecker.dependenciesOverride = uniformDependencies(
|
||||
ruAvgMs = 150.0,
|
||||
foreignAvgMs = 30.0,
|
||||
)
|
||||
|
||||
val result = RttTriangulationChecker.check(
|
||||
context = context,
|
||||
resolverConfig = DnsResolverConfig.system(),
|
||||
geoFacts = ruGeoFacts,
|
||||
)
|
||||
|
||||
assertTrue(result.detected)
|
||||
val mainFinding = result.findings.first()
|
||||
assertEquals(EvidenceConfidence.MEDIUM, mainFinding.confidence)
|
||||
}
|
||||
|
||||
// Case 4: RU 150ms, foreign 250ms, country=RU
|
||||
// homeMedian=150 > 80, foreignMedian=250, homeMedian <= foreignMedian → detected=false, LOW, needsReview=true
|
||||
@Test
|
||||
fun `home_slow_foreign_slower_low_confidence`() = runBlocking {
|
||||
RttTriangulationChecker.dependenciesOverride = uniformDependencies(
|
||||
ruAvgMs = 150.0,
|
||||
foreignAvgMs = 250.0,
|
||||
)
|
||||
|
||||
val result = RttTriangulationChecker.check(
|
||||
context = context,
|
||||
resolverConfig = DnsResolverConfig.system(),
|
||||
geoFacts = ruGeoFacts,
|
||||
)
|
||||
|
||||
assertFalse(result.detected)
|
||||
assertTrue(result.needsReview)
|
||||
val mainFinding = result.findings.first()
|
||||
assertEquals(EvidenceConfidence.LOW, mainFinding.confidence)
|
||||
}
|
||||
|
||||
// Case 5: RU 150ms with high jitter, foreign 30ms with high jitter
|
||||
// homeMedian=150 > foreignMedian=30 → MEDIUM before jitter check
|
||||
// All 10 targets have jitter=80 > 60 → 10/10 = 100% > 50% → downgrade MEDIUM→LOW
|
||||
//
|
||||
// RU: avg=150, min=70, max=150 → jitter=80 > 60
|
||||
// Foreign: avg=30, min=0, max=80 → jitter=80 > 60
|
||||
@Test
|
||||
fun `high_jitter_downgrades_confidence`() = runBlocking {
|
||||
val ruHosts = setOf("yandex.ru", "mail.ru", "vk.com", "sberbank.ru", "gosuslugi.ru")
|
||||
RttTriangulationChecker.dependenciesOverride = RttTriangulationChecker.Dependencies(
|
||||
resolveIpv4 = { host, _ -> "10.0.${if (host in ruHosts) "1" else "2"}.1" },
|
||||
ping = { address, _ ->
|
||||
if (address.startsWith("10.0.1")) {
|
||||
makePingResult(received = 3, avg = 150.0, min = 70.0, max = 150.0)
|
||||
} else {
|
||||
makePingResult(received = 3, avg = 30.0, min = 0.0, max = 80.0)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
val result = RttTriangulationChecker.check(
|
||||
context = context,
|
||||
resolverConfig = DnsResolverConfig.system(),
|
||||
geoFacts = ruGeoFacts,
|
||||
)
|
||||
|
||||
assertTrue(result.detected)
|
||||
val mainFinding = result.findings.first()
|
||||
assertEquals(EvidenceConfidence.LOW, mainFinding.confidence)
|
||||
}
|
||||
|
||||
// Case 6: all resolve/ping fail → homeMedian=null, foreignMedian=null → detected=false, needsReview=true, "unavailable"
|
||||
@Test
|
||||
fun `all_unreachable_unavailable`() = runBlocking {
|
||||
RttTriangulationChecker.dependenciesOverride = RttTriangulationChecker.Dependencies(
|
||||
resolveIpv4 = { host, _ -> throw IOException("network unreachable for $host") },
|
||||
ping = { _, _ -> error("should not be called") },
|
||||
)
|
||||
|
||||
val result = RttTriangulationChecker.check(
|
||||
context = context,
|
||||
resolverConfig = DnsResolverConfig.system(),
|
||||
geoFacts = ruGeoFacts,
|
||||
)
|
||||
|
||||
assertFalse(result.detected)
|
||||
assertTrue(result.needsReview)
|
||||
assertTrue(result.findings.any { "unavailable" in it.description })
|
||||
}
|
||||
|
||||
// Case 7: countryCode="DE" → detected=false, finding contains "only RU supported"
|
||||
@Test
|
||||
fun `non_ru_country_inconclusive`() = runBlocking {
|
||||
val deGeoFacts = GeoIpFacts(countryCode = "DE")
|
||||
|
||||
val result = RttTriangulationChecker.check(
|
||||
context = context,
|
||||
resolverConfig = DnsResolverConfig.system(),
|
||||
geoFacts = deGeoFacts,
|
||||
)
|
||||
|
||||
assertFalse(result.detected)
|
||||
assertTrue(result.findings.any { "RU only" in it.description })
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
/**
|
||||
* Builds Dependencies where all RU targets return [ruAvgMs] and all foreign targets return
|
||||
* [foreignAvgMs]. Jitter is low (max - min = 10 ms) so it never triggers the downgrade rule.
|
||||
*/
|
||||
private fun uniformDependencies(
|
||||
ruAvgMs: Double,
|
||||
foreignAvgMs: Double,
|
||||
): RttTriangulationChecker.Dependencies {
|
||||
val ruHosts = setOf("yandex.ru", "mail.ru", "vk.com", "sberbank.ru", "gosuslugi.ru")
|
||||
return RttTriangulationChecker.Dependencies(
|
||||
resolveIpv4 = { host, _ -> "10.0.${if (host in ruHosts) "1" else "2"}.1" },
|
||||
ping = { address, _ ->
|
||||
val avg = if (address.startsWith("10.0.1")) ruAvgMs else foreignAvgMs
|
||||
// jitter = max - min = 10 ms ≤ 60 → no downgrade
|
||||
makePingResult(received = 3, avg = avg, min = avg - 5.0, max = avg + 5.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun makePingResult(
|
||||
received: Int,
|
||||
avg: Double? = null,
|
||||
min: Double? = null,
|
||||
max: Double? = null,
|
||||
): SystemPingProber.PingResult {
|
||||
return SystemPingProber.PingResult(
|
||||
address = "0.0.0.0",
|
||||
sent = 4,
|
||||
received = received,
|
||||
minRttMs = min,
|
||||
avgRttMs = avg,
|
||||
maxRttMs = max,
|
||||
exitCode = if (received > 0) 0 else 1,
|
||||
rawOutput = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue