mirror of
https://github.com/xtclovver/RKNHardering.git
synced 2026-07-09 17:19:25 +00:00
fiz: баги
This commit is contained in:
parent
e9b7bb6667
commit
ff6cb6b534
9 changed files with 442 additions and 25 deletions
|
|
@ -311,6 +311,7 @@ class MainActivity : AppCompatActivity() {
|
|||
private lateinit var verdictLabel: TextView
|
||||
private lateinit var verdictTitle: TextView
|
||||
private lateinit var verdictSubtitle: TextView
|
||||
private lateinit var verdictHomeRoutedRoamingNote: TextView
|
||||
private lateinit var hiddenLegacyCardsHost: LinearLayout
|
||||
private lateinit var btnPrivacyInfo: MaterialButton
|
||||
private val tiles = mutableMapOf<String, TileHolder>()
|
||||
|
|
@ -544,6 +545,7 @@ class MainActivity : AppCompatActivity() {
|
|||
verdictLabel = findViewById(R.id.verdictLabel)
|
||||
verdictTitle = findViewById(R.id.verdictTitle)
|
||||
verdictSubtitle = findViewById(R.id.verdictSubtitle)
|
||||
verdictHomeRoutedRoamingNote = findViewById(R.id.verdictHomeRoutedRoamingNote)
|
||||
hiddenLegacyCardsHost = findViewById(R.id.hiddenLegacyCardsHost)
|
||||
btnPrivacyInfo = findViewById(R.id.btnPrivacyInfo)
|
||||
btnPrivacyInfo.setOnClickListener { showPrivacyFooterDialog() }
|
||||
|
|
@ -1202,6 +1204,7 @@ class MainActivity : AppCompatActivity() {
|
|||
)
|
||||
activeCheckSettings = null
|
||||
lastCompletedResult = event.result
|
||||
refreshCompletedCategoryViews(event.result, event.privacyMode)
|
||||
displayVerdict(event.result, event.privacyMode)
|
||||
bindVerdictHero(event.result)
|
||||
if (animate) animateContentReveal(verdictHero)
|
||||
|
|
@ -1233,6 +1236,30 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun refreshCompletedCategoryViews(result: CheckResult, privacyMode: Boolean) {
|
||||
if (cardCdnPulling.isVisible || result.cdnPulling != CdnPullingResult.empty()) {
|
||||
displayCdnPulling(result.cdnPulling, privacyMode)
|
||||
updateTileFromCdn(result.cdnPulling)
|
||||
}
|
||||
|
||||
val icmpHasContent = result.icmpSpoofing.findings.isNotEmpty() ||
|
||||
result.icmpSpoofing.evidence.isNotEmpty() ||
|
||||
result.icmpSpoofing.detected ||
|
||||
result.icmpSpoofing.needsReview ||
|
||||
result.icmpSpoofing.hasError
|
||||
if (cardIcmpSpoofing.isVisible || icmpHasContent) {
|
||||
displayCategory(
|
||||
result.icmpSpoofing,
|
||||
cardIcmpSpoofing,
|
||||
iconIcmpSpoofing,
|
||||
statusIcmpSpoofing,
|
||||
findingsIcmpSpoofing,
|
||||
privacyMode,
|
||||
)
|
||||
updateTileFromCategory(CATEGORY_ICM, result.icmpSpoofing)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearStageContent() {
|
||||
geoIpInfoSection.removeAllViews()
|
||||
geoIpInfoSection.visibility = View.GONE
|
||||
|
|
@ -3021,18 +3048,7 @@ class MainActivity : AppCompatActivity() {
|
|||
textVerdictExplanation.text = narrative.explanation
|
||||
textVerdictExplanation.visibility = View.VISIBLE
|
||||
|
||||
val note = narrative.homeRoutedRoamingNote
|
||||
if (note != null) {
|
||||
textVerdictHomeRoutedRoamingNote.text = note
|
||||
textVerdictHomeRoutedRoamingNote.visibility = View.VISIBLE
|
||||
textVerdictHomeRoutedRoamingNote.setTextColor(onSurfaceColor())
|
||||
textVerdictHomeRoutedRoamingNote.setBackgroundResource(
|
||||
R.drawable.bg_verdict_home_routed_roaming_note,
|
||||
)
|
||||
} else {
|
||||
textVerdictHomeRoutedRoamingNote.text = ""
|
||||
textVerdictHomeRoutedRoamingNote.visibility = View.GONE
|
||||
}
|
||||
bindHomeRoutedRoamingNote(narrative.homeRoutedRoamingNote)
|
||||
|
||||
verdictDetailsContent.removeAllViews()
|
||||
addVerdictSection(
|
||||
|
|
@ -3055,6 +3071,24 @@ class MainActivity : AppCompatActivity() {
|
|||
updateVerdictDetailsButton()
|
||||
}
|
||||
|
||||
private fun bindHomeRoutedRoamingNote(note: String?) {
|
||||
if (note != null) {
|
||||
textVerdictHomeRoutedRoamingNote.text = note
|
||||
textVerdictHomeRoutedRoamingNote.visibility = View.VISIBLE
|
||||
textVerdictHomeRoutedRoamingNote.setTextColor(onSurfaceColor())
|
||||
textVerdictHomeRoutedRoamingNote.setBackgroundResource(
|
||||
R.drawable.bg_verdict_home_routed_roaming_note,
|
||||
)
|
||||
verdictHomeRoutedRoamingNote.text = note
|
||||
verdictHomeRoutedRoamingNote.visibility = View.VISIBLE
|
||||
} else {
|
||||
textVerdictHomeRoutedRoamingNote.text = ""
|
||||
textVerdictHomeRoutedRoamingNote.visibility = View.GONE
|
||||
verdictHomeRoutedRoamingNote.text = ""
|
||||
verdictHomeRoutedRoamingNote.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun addVerdictSection(title: String, content: List<View>) {
|
||||
if (content.isEmpty()) return
|
||||
|
||||
|
|
@ -3126,8 +3160,7 @@ class MainActivity : AppCompatActivity() {
|
|||
textVerdict.text = ""
|
||||
textVerdictExplanation.text = ""
|
||||
textVerdictExplanation.visibility = View.GONE
|
||||
textVerdictHomeRoutedRoamingNote.text = ""
|
||||
textVerdictHomeRoutedRoamingNote.visibility = View.GONE
|
||||
bindHomeRoutedRoamingNote(null)
|
||||
verdictDetailsDivider.visibility = View.GONE
|
||||
btnVerdictDetails.visibility = View.GONE
|
||||
btnVerdictDetails.text = getString(R.string.main_verdict_details)
|
||||
|
|
@ -3494,6 +3527,7 @@ class MainActivity : AppCompatActivity() {
|
|||
verdictLabel.text = getString(R.string.verdict_label)
|
||||
verdictTitle.text = getString(R.string.verdict_title_idle)
|
||||
verdictSubtitle.text = getString(R.string.verdict_subtitle_idle)
|
||||
bindHomeRoutedRoamingNote(null)
|
||||
}
|
||||
|
||||
private fun bindVerdictHeroRunning() {
|
||||
|
|
@ -3503,6 +3537,7 @@ class MainActivity : AppCompatActivity() {
|
|||
verdictLabel.text = getString(R.string.verdict_label)
|
||||
verdictTitle.text = getString(R.string.verdict_title_idle)
|
||||
verdictSubtitle.text = getString(R.string.verdict_subtitle_running)
|
||||
bindHomeRoutedRoamingNote(null)
|
||||
}
|
||||
|
||||
private fun bindVerdictHero(result: CheckResult) {
|
||||
|
|
|
|||
|
|
@ -283,9 +283,17 @@ object BypassChecker {
|
|||
}
|
||||
|
||||
internal fun proxyChecksNeedReview(proxyChecks: List<LocalProxyCheckResult>): Boolean {
|
||||
return proxyChecks.any {
|
||||
it.status != LocalProxyCheckStatus.SAME_IP &&
|
||||
it.status != LocalProxyCheckStatus.CONFIRMED_BYPASS
|
||||
return proxyChecks.any(::proxyCheckNeedsReview)
|
||||
}
|
||||
|
||||
private fun proxyCheckNeedsReview(proxyCheck: LocalProxyCheckResult): Boolean {
|
||||
return when (proxyCheck.status) {
|
||||
LocalProxyCheckStatus.SAME_IP,
|
||||
LocalProxyCheckStatus.CONFIRMED_BYPASS -> false
|
||||
LocalProxyCheckStatus.AUTH_REQUIRED ->
|
||||
proxyCheck.ownerStatus != LocalProxyOwnerStatus.UNRESOLVED
|
||||
LocalProxyCheckStatus.PROXY_IP_UNAVAILABLE,
|
||||
LocalProxyCheckStatus.DIRECT_IP_UNAVAILABLE -> true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import android.telephony.CellInfo
|
|||
import android.telephony.CellInfoGsm
|
||||
import android.telephony.CellInfoLte
|
||||
import android.telephony.CellInfoWcdma
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.TelephonyManager
|
||||
import android.telephony.gsm.GsmCellLocation
|
||||
import androidx.annotation.DoNotInline
|
||||
|
|
@ -49,6 +50,13 @@ object LocationSignalsChecker {
|
|||
val operatorName: String?,
|
||||
val isRoaming: Boolean?,
|
||||
val simMnc: String? = null,
|
||||
val isActiveDataSubscription: Boolean = false,
|
||||
val isDefaultDataSubscription: Boolean = false,
|
||||
)
|
||||
|
||||
private data class DataSubscriptionIds(
|
||||
val active: Int?,
|
||||
val default: Int?,
|
||||
)
|
||||
|
||||
private data class CellCollectionResult(
|
||||
|
|
@ -258,6 +266,7 @@ object LocationSignalsChecker {
|
|||
} else {
|
||||
null
|
||||
}
|
||||
val dataSubscriptionIds = getDataSubscriptionIds()
|
||||
|
||||
if (!subscriptions.isNullOrEmpty()) {
|
||||
return subscriptions.mapNotNull { info ->
|
||||
|
|
@ -282,6 +291,8 @@ object LocationSignalsChecker {
|
|||
?: subTm.simOperatorName?.takeIf { it.isNotEmpty() }
|
||||
?: subTm.networkOperatorName?.takeIf { it.isNotEmpty() },
|
||||
isRoaming = subTm.isNetworkRoaming,
|
||||
isActiveDataSubscription = info.subscriptionId == dataSubscriptionIds.active,
|
||||
isDefaultDataSubscription = info.subscriptionId == dataSubscriptionIds.default,
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
|
@ -306,6 +317,8 @@ object LocationSignalsChecker {
|
|||
operatorName = tm.simOperatorName?.takeIf { it.isNotEmpty() }
|
||||
?: tm.networkOperatorName?.takeIf { it.isNotEmpty() },
|
||||
isRoaming = tm.isNetworkRoaming,
|
||||
isActiveDataSubscription = dataSubscriptionIds.active != null,
|
||||
isDefaultDataSubscription = dataSubscriptionIds.default != null,
|
||||
)
|
||||
)
|
||||
}.getOrElse { emptyList() }
|
||||
|
|
@ -314,10 +327,34 @@ object LocationSignalsChecker {
|
|||
@Suppress("MissingPermission")
|
||||
private fun getActiveSubscriptions(context: Context): List<android.telephony.SubscriptionInfo>? {
|
||||
val subscriptionManager = context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE)
|
||||
as? android.telephony.SubscriptionManager
|
||||
as? SubscriptionManager
|
||||
return runCatching { subscriptionManager?.activeSubscriptionInfoList }.getOrNull()
|
||||
}
|
||||
|
||||
private fun getDataSubscriptionIds(): DataSubscriptionIds {
|
||||
val defaultDataSubscriptionId = runCatching {
|
||||
normalizeSubscriptionId(SubscriptionManager.getDefaultDataSubscriptionId())
|
||||
}.getOrNull()
|
||||
val activeDataSubscriptionId = runCatching {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
normalizeSubscriptionId(SubscriptionManager.getActiveDataSubscriptionId())
|
||||
} else {
|
||||
defaultDataSubscriptionId
|
||||
}
|
||||
}.getOrNull()
|
||||
return DataSubscriptionIds(
|
||||
active = activeDataSubscriptionId,
|
||||
default = defaultDataSubscriptionId,
|
||||
)
|
||||
}
|
||||
|
||||
private fun normalizeSubscriptionId(subscriptionId: Int): Int? {
|
||||
return subscriptionId.takeIf {
|
||||
it != SubscriptionManager.INVALID_SUBSCRIPTION_ID &&
|
||||
it != SubscriptionManager.DEFAULT_SUBSCRIPTION_ID
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun collectCellCandidates(
|
||||
context: Context,
|
||||
tm: TelephonyManager,
|
||||
|
|
@ -892,10 +929,7 @@ object LocationSignalsChecker {
|
|||
val evidence = mutableListOf<EvidenceItem>()
|
||||
var needsReview = false
|
||||
|
||||
// Pick "home" SIM for the home-routed-roaming heuristic. Prefer the
|
||||
// first SIM with a known MCC; fall back to the first card we have.
|
||||
val homeSim = snapshot.simCards.firstOrNull { !it.simMcc.isNullOrBlank() }
|
||||
?: snapshot.simCards.firstOrNull()
|
||||
val homeSim = selectHomeSim(snapshot)
|
||||
val homeSimCountryIsRussia = homeSim?.simMcc == RUSSIA_MCC
|
||||
val networkIsRussia = snapshot.networkMcc == RUSSIA_MCC
|
||||
val homeRoutedRoaming = networkIsRussia &&
|
||||
|
|
@ -1123,6 +1157,23 @@ object LocationSignalsChecker {
|
|||
)
|
||||
}
|
||||
|
||||
private fun selectHomeSim(snapshot: LocationSnapshot): SimCardInfo? {
|
||||
val simsWithMcc = snapshot.simCards.filter { !it.simMcc.isNullOrBlank() }
|
||||
return snapshot.simCards.firstOrNull { it.isActiveDataSubscription }
|
||||
?: snapshot.simCards.firstOrNull { it.isDefaultDataSubscription }
|
||||
?: simsWithMcc.firstOrNull { it.matchesRegisteredNetwork(snapshot) }
|
||||
?: simsWithMcc.firstOrNull()
|
||||
?: snapshot.simCards.firstOrNull()
|
||||
}
|
||||
|
||||
private fun SimCardInfo.matchesRegisteredNetwork(snapshot: LocationSnapshot): Boolean {
|
||||
if (simMcc.isNullOrBlank() || snapshot.networkMcc.isNullOrBlank()) return false
|
||||
if (simMcc != snapshot.networkMcc) return false
|
||||
return snapshot.networkMnc.isNullOrBlank() ||
|
||||
simMnc.isNullOrBlank() ||
|
||||
simMnc == snapshot.networkMnc
|
||||
}
|
||||
|
||||
private val MAC_ADDRESS_REGEX = Regex("^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$")
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -280,9 +280,15 @@ object VpnCheckRunner {
|
|||
// by the SIM home network.
|
||||
if (!result.detected && !result.needsReview) return result
|
||||
return result.copy(
|
||||
detected = false,
|
||||
needsReview = false,
|
||||
findings = result.findings.map { finding ->
|
||||
if (finding.needsReview) finding.copy(needsReview = false) else finding
|
||||
when {
|
||||
finding.isError -> finding.copy(needsReview = false)
|
||||
finding.detected || finding.needsReview ->
|
||||
finding.copy(detected = false, needsReview = false, isInformational = true)
|
||||
else -> finding
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -293,6 +299,7 @@ object VpnCheckRunner {
|
|||
// reply via ICMP cannot be treated as suspicious.
|
||||
if (!result.needsReview && result.evidence.none { it.detected }) return result
|
||||
return result.copy(
|
||||
detected = false,
|
||||
needsReview = false,
|
||||
evidence = result.evidence.map { item ->
|
||||
if (item.source == EvidenceSource.ICMP_SPOOFING && item.detected) {
|
||||
|
|
@ -300,7 +307,7 @@ object VpnCheckRunner {
|
|||
} else item
|
||||
},
|
||||
findings = result.findings.map { finding ->
|
||||
if (finding.needsReview) finding.copy(needsReview = false) else finding
|
||||
if (finding.needsReview) finding.copy(detected = false, needsReview = false, isInformational = true) else finding
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/verdictHero"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
@ -112,5 +113,18 @@
|
|||
android:layout_marginTop="10dp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verdictHomeRoutedRoamingNote"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/bg_verdict_home_routed_roaming_note"
|
||||
android:padding="10dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
tools:text="@string/narrative_home_routed_roaming_generic"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ import com.notcvnt.rknhardering.model.CategoryResult
|
|||
import com.notcvnt.rknhardering.model.Channel
|
||||
import com.notcvnt.rknhardering.model.CdnPullingResponse
|
||||
import com.notcvnt.rknhardering.model.CdnPullingResult
|
||||
import com.notcvnt.rknhardering.model.CheckResult
|
||||
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.IpCheckerGroupResult
|
||||
import com.notcvnt.rknhardering.model.IpCheckerResponse
|
||||
|
|
@ -26,8 +30,10 @@ import com.notcvnt.rknhardering.model.IpCheckerScope
|
|||
import com.notcvnt.rknhardering.model.IpComparisonResult
|
||||
import com.notcvnt.rknhardering.model.IpFamily
|
||||
import com.notcvnt.rknhardering.model.IpConsensusResult
|
||||
import com.notcvnt.rknhardering.model.LocationSignalsFacts
|
||||
import com.notcvnt.rknhardering.model.ObservedIp
|
||||
import com.notcvnt.rknhardering.model.TargetGroup
|
||||
import com.notcvnt.rknhardering.model.Verdict
|
||||
import org.junit.Before
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
|
|
@ -478,6 +484,102 @@ class MainActivityUiRenderingTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `completed event refreshes reconciled cdn and icmp cards`() {
|
||||
val activity = Robolectric.buildActivity(MainActivity::class.java).setup().get()
|
||||
val rawCdn = CdnPullingResult(
|
||||
detected = true,
|
||||
needsReview = true,
|
||||
summary = "Raw CDN signal",
|
||||
responses = listOf(
|
||||
CdnPullingResponse(
|
||||
targetLabel = "meduza.io",
|
||||
url = "https://meduza.io/cdn-cgi/trace",
|
||||
ip = "203.0.113.64",
|
||||
),
|
||||
),
|
||||
)
|
||||
val rawIcmp = CategoryResult(
|
||||
name = "icmp",
|
||||
detected = false,
|
||||
findings = listOf(Finding("Raw ICMP signal", needsReview = true)),
|
||||
needsReview = true,
|
||||
evidence = listOf(
|
||||
EvidenceItem(
|
||||
source = EvidenceSource.ICMP_SPOOFING,
|
||||
detected = true,
|
||||
confidence = EvidenceConfidence.MEDIUM,
|
||||
description = "Blocked target replied",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
invokePrivate<Unit>(activity, "displayCdnPulling", rawCdn, false)
|
||||
invokePrivate<Unit>(activity, "updateTileFromCdn", rawCdn)
|
||||
invokePrivate<Unit>(
|
||||
activity,
|
||||
"displayCategory",
|
||||
rawIcmp,
|
||||
activity.findViewById<MaterialCardView>(R.id.cardIcmpSpoofing),
|
||||
activity.findViewById<ImageView>(R.id.iconIcmpSpoofing),
|
||||
activity.findViewById<TextView>(R.id.statusIcmpSpoofing),
|
||||
activity.findViewById<LinearLayout>(R.id.findingsIcmpSpoofing),
|
||||
false,
|
||||
)
|
||||
invokePrivate<Unit>(activity, "updateTileFromCategory", "icmp", rawIcmp)
|
||||
|
||||
val finalCdn = rawCdn.copy(detected = false, needsReview = false, summary = "Reconciled CDN signal")
|
||||
val finalIcmp = rawIcmp.copy(
|
||||
findings = listOf(Finding("Reconciled ICMP signal", isInformational = true)),
|
||||
needsReview = false,
|
||||
evidence = emptyList(),
|
||||
)
|
||||
invokePrivate<Unit>(
|
||||
activity,
|
||||
"applyScanEvent",
|
||||
ScanEvent.Completed(
|
||||
checkResult(cdnPulling = finalCdn, icmpSpoofing = finalIcmp),
|
||||
privacyMode = false,
|
||||
),
|
||||
false,
|
||||
)
|
||||
|
||||
assertEquals(activity.getString(R.string.main_card_status_clean), activity.findViewById<TextView>(R.id.statusCdnPulling).text.toString())
|
||||
assertEquals(activity.getString(R.string.tile_hint_clean_count, 1), tileHint(activity, "cdn"))
|
||||
assertEquals(activity.getString(R.string.main_card_status_clean), activity.findViewById<TextView>(R.id.statusIcmpSpoofing).text.toString())
|
||||
assertEquals(activity.getString(R.string.tile_hint_clean), tileHint(activity, "icmp"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `completed event shows home routed roaming note in verdict hero`() {
|
||||
val activity = Robolectric.buildActivity(MainActivity::class.java).setup().get()
|
||||
val location = category("location").copy(
|
||||
locationFacts = LocationSignalsFacts(
|
||||
networkMcc = "250",
|
||||
networkIsRussia = true,
|
||||
homeSimMcc = "208",
|
||||
homeSimCountryIso = "FR",
|
||||
homeSimOperatorName = "Free Mobile",
|
||||
homeRoutedRoaming = true,
|
||||
),
|
||||
)
|
||||
|
||||
invokePrivate<Unit>(
|
||||
activity,
|
||||
"applyScanEvent",
|
||||
ScanEvent.Completed(
|
||||
checkResult(locationSignals = location),
|
||||
privacyMode = false,
|
||||
),
|
||||
false,
|
||||
)
|
||||
|
||||
val note = activity.findViewById<TextView>(R.id.verdictHomeRoutedRoamingNote)
|
||||
assertEquals(View.VISIBLE, note.visibility)
|
||||
assertTrueContains(note.text.toString(), "Free Mobile")
|
||||
assertTrueContains(note.text.toString(), "FR")
|
||||
}
|
||||
|
||||
private fun collectText(view: View): String {
|
||||
if (view is TextView) return view.text.toString()
|
||||
if (view !is ViewGroup) return ""
|
||||
|
|
@ -511,6 +613,46 @@ class MainActivityUiRenderingTest {
|
|||
)
|
||||
}
|
||||
|
||||
private fun checkResult(
|
||||
cdnPulling: CdnPullingResult = CdnPullingResult.empty(),
|
||||
icmpSpoofing: CategoryResult = category("icmp"),
|
||||
locationSignals: CategoryResult = category("location"),
|
||||
verdict: Verdict = Verdict.NOT_DETECTED,
|
||||
): CheckResult {
|
||||
return CheckResult(
|
||||
geoIp = category("geo"),
|
||||
ipComparison = IpComparisonResult(
|
||||
detected = false,
|
||||
summary = "",
|
||||
ruGroup = emptyIpCheckerGroup("RU"),
|
||||
nonRuGroup = emptyIpCheckerGroup("NON_RU"),
|
||||
),
|
||||
cdnPulling = cdnPulling,
|
||||
directSigns = category("direct"),
|
||||
indirectSigns = category("indirect"),
|
||||
locationSignals = locationSignals,
|
||||
bypassResult = BypassResult(
|
||||
proxyEndpoint = null,
|
||||
directIp = null,
|
||||
proxyIp = null,
|
||||
xrayApiScanResult = null,
|
||||
findings = emptyList(),
|
||||
detected = false,
|
||||
),
|
||||
verdict = verdict,
|
||||
icmpSpoofing = icmpSpoofing,
|
||||
ipConsensus = IpConsensusResult.empty(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun category(name: String): CategoryResult {
|
||||
return CategoryResult(
|
||||
name = name,
|
||||
detected = false,
|
||||
findings = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> getPrivateField(target: Any, name: String): T {
|
||||
val field = target::class.java.getDeclaredField(name)
|
||||
|
|
|
|||
|
|
@ -594,7 +594,7 @@ class BypassCheckerTest {
|
|||
|
||||
assertFalse(evaluation.confirmedBypass)
|
||||
assertEquals(LocalProxyCheckStatus.AUTH_REQUIRED, evaluation.proxyChecks.single().status)
|
||||
assertTrue(BypassChecker.proxyChecksNeedReview(evaluation.proxyChecks))
|
||||
assertFalse(BypassChecker.proxyChecksNeedReview(evaluation.proxyChecks))
|
||||
assertFalse(evidence.any { it.source == EvidenceSource.SPLIT_TUNNEL_BYPASS && it.detected })
|
||||
// AUTH_REQUIRED + UNRESOLVED owner is downgraded to informational so
|
||||
// the verdict isn't inflated by an unattributable listener that we
|
||||
|
|
@ -610,6 +610,17 @@ class BypassCheckerTest {
|
|||
assertFalse(evidence.any { it.source == EvidenceSource.LOCAL_PROXY })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auth required proxy with known owner still needs review`() {
|
||||
val proxyCheck = com.notcvnt.rknhardering.model.LocalProxyCheckResult(
|
||||
endpoint = ProxyEndpoint(host = "127.0.0.1", port = 10808, type = ProxyType.SOCKS5, authRequired = true),
|
||||
ownerStatus = LocalProxyOwnerStatus.AMBIGUOUS,
|
||||
status = LocalProxyCheckStatus.AUTH_REQUIRED,
|
||||
)
|
||||
|
||||
assertTrue(BypassChecker.proxyChecksNeedReview(listOf(proxyCheck)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `proxy evaluation inspects every candidate when proxy ip is unavailable`() = runBlocking {
|
||||
val findings = mutableListOf<Finding>()
|
||||
|
|
|
|||
|
|
@ -450,6 +450,73 @@ class LocationSignalsCheckerTest {
|
|||
assertTrue(result.findings.any { it.description == "SIM[0] Roaming: yes" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `active russian data sim prevents home routed roaming from inactive foreign sim`() {
|
||||
val result = LocationSignalsChecker.evaluate(
|
||||
snapshot(
|
||||
networkMcc = "250",
|
||||
networkCountryIso = "ru",
|
||||
networkOperatorName = "MegaFon",
|
||||
simCards = listOf(
|
||||
sim(
|
||||
slotIndex = 0,
|
||||
subscriptionId = 1,
|
||||
simMcc = "208",
|
||||
simCountryIso = "fr",
|
||||
operatorName = "Free Mobile",
|
||||
isRoaming = true,
|
||||
),
|
||||
sim(
|
||||
slotIndex = 1,
|
||||
subscriptionId = 2,
|
||||
simMcc = "250",
|
||||
simMnc = "02",
|
||||
simCountryIso = "ru",
|
||||
operatorName = "MegaFon",
|
||||
isRoaming = false,
|
||||
isActiveDataSubscription = true,
|
||||
isDefaultDataSubscription = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertFalse(result.locationFacts!!.homeRoutedRoaming)
|
||||
assertEquals("250", result.locationFacts!!.homeSimMcc)
|
||||
assertFalse(result.evidence.any { it.source == EvidenceSource.HOME_ROUTED_ROAMING })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `registered network sim is preferred over first foreign sim when data id is unavailable`() {
|
||||
val result = LocationSignalsChecker.evaluate(
|
||||
snapshot(
|
||||
networkMcc = "250",
|
||||
networkOperatorName = "MegaFon",
|
||||
simCards = listOf(
|
||||
sim(
|
||||
slotIndex = 0,
|
||||
subscriptionId = 1,
|
||||
simMcc = "208",
|
||||
simCountryIso = "fr",
|
||||
operatorName = "Free Mobile",
|
||||
isRoaming = true,
|
||||
),
|
||||
sim(
|
||||
slotIndex = 1,
|
||||
subscriptionId = 2,
|
||||
simMcc = "250",
|
||||
simCountryIso = "ru",
|
||||
operatorName = "MegaFon",
|
||||
isRoaming = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertFalse(result.locationFacts!!.homeRoutedRoaming)
|
||||
assertEquals("250", result.locationFacts!!.homeSimMcc)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty sim cards list produces no sim findings and medium confidence for non-ru network`() {
|
||||
val result = LocationSignalsChecker.evaluate(
|
||||
|
|
@ -475,16 +542,22 @@ class LocationSignalsCheckerTest {
|
|||
slotIndex: Int = 0,
|
||||
subscriptionId: Int = 1,
|
||||
simMcc: String? = "250",
|
||||
simMnc: String? = null,
|
||||
simCountryIso: String? = "ru",
|
||||
operatorName: String? = "MegaFon",
|
||||
isRoaming: Boolean? = false,
|
||||
isActiveDataSubscription: Boolean = false,
|
||||
isDefaultDataSubscription: Boolean = false,
|
||||
) = LocationSignalsChecker.SimCardInfo(
|
||||
slotIndex = slotIndex,
|
||||
subscriptionId = subscriptionId,
|
||||
simMcc = simMcc,
|
||||
simMnc = simMnc,
|
||||
simCountryIso = simCountryIso,
|
||||
operatorName = operatorName,
|
||||
isRoaming = isRoaming,
|
||||
isActiveDataSubscription = isActiveDataSubscription,
|
||||
isDefaultDataSubscription = isDefaultDataSubscription,
|
||||
)
|
||||
|
||||
private fun snapshot(
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ import com.notcvnt.rknhardering.model.Channel
|
|||
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.IpCheckerGroupResult
|
||||
import com.notcvnt.rknhardering.model.IpComparisonResult
|
||||
import com.notcvnt.rknhardering.model.LocationSignalsFacts
|
||||
import com.notcvnt.rknhardering.model.Verdict
|
||||
import com.notcvnt.rknhardering.network.DnsResolverConfig
|
||||
import com.notcvnt.rknhardering.model.TargetGroup
|
||||
|
|
@ -489,6 +491,80 @@ class VpnCheckRunnerTest {
|
|||
assertTrue(updates.any { it is CheckUpdate.CdnPullingReady })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `home routed roaming relaxes final cdn and icmp categories`() = runBlocking {
|
||||
val rawCdn = CdnPullingResult(
|
||||
detected = true,
|
||||
needsReview = true,
|
||||
summary = "CDN targets exposed foreign IP",
|
||||
responses = listOf(
|
||||
CdnPullingResponse(
|
||||
targetLabel = "meduza.io",
|
||||
url = "https://meduza.io/cdn-cgi/trace",
|
||||
ip = "203.0.113.64",
|
||||
ipv4 = "203.0.113.64",
|
||||
),
|
||||
),
|
||||
findings = listOf(Finding("meduza.io: IP: 203.0.113.64", detected = true, needsReview = true)),
|
||||
)
|
||||
val rawIcmp = category(
|
||||
name = "icmp",
|
||||
needsReview = true,
|
||||
evidence = listOf(
|
||||
EvidenceItem(
|
||||
source = EvidenceSource.ICMP_SPOOFING,
|
||||
detected = true,
|
||||
confidence = EvidenceConfidence.MEDIUM,
|
||||
description = "Blocked target replied",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
VpnCheckRunner.dependenciesOverride = VpnCheckRunner.Dependencies(
|
||||
geoIpCheck = { _, _ -> category("geo") },
|
||||
ipComparisonCheck = { _, _ -> emptyIpComparison() },
|
||||
cdnPullingCheck = { _, _, _ -> rawCdn },
|
||||
icmpSpoofingCheck = { _, _ -> rawIcmp },
|
||||
directCheck = { _, _ -> category("direct") },
|
||||
indirectCheck = { _, _, _, _ -> category("indirect") },
|
||||
locationCheck = { _, _, _ ->
|
||||
CategoryResult(
|
||||
name = "location",
|
||||
detected = false,
|
||||
findings = listOf(Finding("network_mcc_ru:true")),
|
||||
locationFacts = LocationSignalsFacts(
|
||||
networkMcc = "250",
|
||||
networkIsRussia = true,
|
||||
homeSimMcc = "208",
|
||||
homeSimCountryIso = "FR",
|
||||
homeRoutedRoaming = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
nativeCheck = { _ -> category("native") },
|
||||
bypassCheck = { _, _, _, _, _, _, _, _, _, _ ->
|
||||
error("BypassChecker should not run when split tunnel is disabled")
|
||||
},
|
||||
)
|
||||
|
||||
val result = VpnCheckRunner.run(
|
||||
context = context,
|
||||
settings = CheckSettings(
|
||||
splitTunnelEnabled = false,
|
||||
networkRequestsEnabled = true,
|
||||
cdnPullingEnabled = true,
|
||||
resolverConfig = DnsResolverConfig.system(),
|
||||
),
|
||||
)
|
||||
|
||||
assertFalse(result.cdnPulling.detected)
|
||||
assertFalse(result.cdnPulling.needsReview)
|
||||
assertTrue(result.cdnPulling.findings.none { it.detected || it.needsReview })
|
||||
assertFalse(result.icmpSpoofing.detected)
|
||||
assertFalse(result.icmpSpoofing.needsReview)
|
||||
assertTrue(result.icmpSpoofing.evidence.none { it.detected })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cdn pulling stays disabled when toggle is off`() = runBlocking {
|
||||
val updates = mutableListOf<CheckUpdate>()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue