evaluateHostRoutes выдавал ложное срабатывание "Host route ... (VPN
server IP leak)" на адресе собственного транспортного интерфейса.
netlinkRouteDump делает RTM_GETROUTE с NLM_F_DUMP без указания таблицы,
поэтому в дамп попадают записи таблицы local (table 255), которые ядро
автоматически создаёт для каждого настроенного адреса интерфейса:
local 12.233.114.164 dev ccmni1 table local proto kernel scope host
src 12.233.114.164
Такая запись имеет prefixLen=32, type=local, scope=host, dst==prefsrc.
Если адрес интерфейса попадает в публично-маршрутизируемый диапазон
(например 12.0.0.0/8 у сотовых модемов ccmni MediaTek), все условия
фильтра проходили и checker рапортовал утечку.
Добавлен isKernelManagedLocalRoute: исключает type local/broadcast/
anycast/multicast, scope host и dst==prefsrc. Настоящая утечка VPN —
unicast-маршрут к чужому публичному IP (dst!=prefsrc) в main/policy
таблицах — детектируется как прежде.
* fix(ui): wire IP-consensus result into its category tile
The IpConsensusReady handler only marked the stage completed and never
called updateTileFromIpConsensus(), unlike every sibling updateTileFrom*
branch. As a result the IP-consensus tile (CATEGORY_IPS) never reflected
detected/review/clean signals. Call the already-written updater with the
event payload.
* chore(quality): low-risk SonarCloud code-smell cleanups
Behavior-preserving fixes for SonarCloud findings, verified by build +
unit tests. No detector signal, threshold, port, or verdict logic changed.
Kotlin:
- extract duplicated string literals to consts (S1192): "*.*.*.*",
"SHA-512", "meduza.io", "<none>", sing-box core type
- if-throw -> require()/check() for validation (S6532): Ed25519 decode,
GeoIp HTTP status
- empty catch blocks documented (S108); collapsible if merged (S1066)
- isExpanded() -> val property (S6512) with call-site update
- drop unused lambda parameter (S1172)
C++ (native_signs_probe.cpp, native_curl_probe.cpp):
- read-only netlink/diag pointers made pointer-to-const (S5350)
- split multi-variable declarations (S1659)
- sizeof/sizeof -> std::size for JNI method table (S7127)
- ipv6 prefix loop converted to range-for (S5566)
- progress-callback state pointer made const (S5350)
Riskier findings (cognitive complexity, too-many-params, deep nesting,
flag-guarded null ops that the Kotlin compiler cannot smart-cast) were
left in place. Genuine false positives (C++20/23 suggestions under a
C++17 toolchain, JNI function-pointer decay, intentional reinterpret_cast
in syscall code, sentinel IPs) were marked False Positive / Accepted in
SonarCloud rather than changed.
Приложение определяет, что запущено в эмуляторе или изолированном/
клонированном контексте (вторичный пользователь, рабочий профиль,
dual-app), и поднимает NEEDS_REVIEW. Сетевые детекторы слепы к VPN,
скрытому в другом контексте, поэтому собственная изолированная/
эмулированная среда сама по себе является поводом для ревью.
Что добавлено:
- Native JNI-проба nativeDetectEmulator (QEMU/goldfish/Genymotion/
BlueStacks) в native_signs_probe.cpp + регистрация в kMethods.
- Bridge NativeSignsBridge.detectEmulator с тест-override.
- Парсер NativeEmulatorFinding в NativeInterfaceProbe.
- Блоки evaluateEmulator (native HIGH + Build-эвристика MEDIUM) и
evaluateIsolation (вторичный пользователь / клон / рабочий профиль)
в NativeSignsChecker, вшитые в check().
- Источники NATIVE_EMULATOR и SANDBOX_ISOLATION в EvidenceSource,
заведены только в NATIVE_REVIEW_SOURCES VerdictEngine (R6): вклад
строго NEEDS_REVIEW, никогда DETECTED.
- Локализация 5 строк (en/ru/fa/zh-rCN).
- Тесты: парсер, оба эвалуатора, два кейса R6 в VerdictEngineTest.
Контроль качества (адверсариальная проверка): убрана ложноположительная
эвристика MANUFACTURER=="unknown" (легитимные AOSP/бюджетные устройства),
удалён мёртвый path-фоллбэк в clone-детекции (userId уже выводится из
того же пути тем же regex через extractUserId).
Проверено: ./gradlew :app:testDebugUnitTest :app:lintDebug
:app:assembleDebug — BUILD SUCCESSFUL, native-либы для arm64-v8a/
armeabi-v7a/x86_64, без MissingTranslation.
* fix: Security upgrade & bug fixes for profiles
* fix: bugs, security and warnings
* chore: README update
* test: expand vpn/ characterization coverage
Add VpnAppClassifierTest (marker priority, case sensitivity, go version
extraction) and extend VpnAppCatalogTest with catalog integrity
invariants, VpnDumpsysParserTest with parser corpus cases,
VpnApkInspectorTest with split-apk/path-edge cases,
VpnAppMetadataScannerTest with null-package/permission/format cases.
Tests pin current behavior before refactoring; no production changes.
* test: fix data race in CdnPullingCheckerTest label collection
fetchEndpoint stubs are invoked from parallel coroutines, but the tests
collected labels into a plain ArrayList, causing intermittent
ArrayIndexOutOfBoundsException. Use CopyOnWriteArrayList.
* refactor: single source of truth for interface name patterns
NetworkInterfaceNameNormalizer no longer redeclares its own base-pattern
list; it now uses NetworkInterfacePatterns.STACKED_BASE_INTERFACES, a
deliberate strict subset of STANDARD_INTERFACES (seth/dummy excluded to
keep v4- de-stacking behavior byte-identical). Pinned the divergence and
classification behavior in NetworkInterfaceNameNormalizerTest.
* refactor: extract strict IP-literal validators into probe/IpLiterals
Move the strict charset+InetAddress validators verbatim out of
CdnPullingClient; the client keeps one-line delegates so all call sites
and tests stay untouched. PublicIpClient.looksLikeIp is deliberately NOT
unified: its loose heuristic is required for mail.ru HTML extraction
(now documented). Add IpLiteralsTest pinning edge cases including the
JDK quirk where IPv4-mapped IPv6 literals parse as Inet4Address.
* refactor: deduplicate checker/ plumbing
- Share a single SignalOutcome data class instead of identical nested
copies in DirectSignsChecker and IndirectSignsChecker.
- GeoIpChecker: extract fetchBuiltinProvider, the common
fetch/validate/error skeleton of the five builtin providers; each
provider keeps its own URL construction and field-path cascades.
- BypassChecker: extract startProxyScanAsync/startXrayScanAsync so
check() is orchestration only; progress-callback wiring unchanged.
No thresholds, signals, finding order or result structures changed.
IndirectSigns/LocationSignals/CallTransport check() bodies were already
step-decomposed and are deliberately left as is.
* refactor: collapse VpnCheckRunner await/publish blocks into publishOnReady
Replace 11 identical await -> throwIfCancelled -> publish wrapper blocks
with a generic publishOnReady helper using CheckUpdate constructor
references. Same supervisorScope receiver, same null contract, same
publish order; BypassProgress, IpConsensusReady and VerdictReady paths
untouched.
* test: golden snapshots and section-parity net for export formatters
Pin the byte-exact output of all three CheckResult formatters (Markdown,
JSON, debug diagnostics) for the rich fixture, privacy mode on and off,
with UTC/Locale.US pinned and a fixed timestamp. Actual outputs are
dumped to build/export-golden-actual/ for easy regeneration after an
intentional format change. The parity test additionally asserts every
populated section appears in every formatter, guarding the three
independent traversals against silent drift.
No shared traversal/visitor is introduced on purpose: the formatters
diverge deliberately (separators, masking policy, flag emission), so
unification would change export bytes.
* test: add markdown golden files missed by the global *.md gitignore
Allow-list app/src/test/resources/export/golden/*.md so golden
regeneration keeps working.
* test: pin custom-check editor read/write semantics before decomposition
Round-trip test (non-default config in every section survives open-save
unchanged), defaults test (untouched new profile saves the exact literal
defaults from saveAndExit), and official-fork test (saving a profile
whose official flag survived the storage trust chain produces a new id,
an edited-suffix name, cleared marketplaceInfo and sourceProfileId).
* refactor: decompose custom-check editor into per-section binders
SettingsCustomCheckEditorFragment (1593 -> 422 lines) now delegates each
checker section to a SectionBinder in customcheck/ui/editor/: bind()
loads the profile slice into the body views, collect() reconstructs it
with the exact legacy defaults, summary() renders the collapsed line.
Shared plumbing extracted alongside: EndpointPills (pill rows),
InlineEndpointSection (add/edit wiring for the three
InlineEndpointEditorController-backed sections), EditorSectionBinders
(visual order). Reachability domains moved into their binder; the
generic custom-domain list, section accordion state, dependency
enforcement and the save/fork flow stay in the fragment.
Read/write semantics are pinned by SettingsCustomCheckEditorRoundTripTest
(previous commit) and were not changed. Dead makeDialogContainer/
makeDialogEdit helpers dropped.
* refactor: replace MainActivity tile id-mappers with a static tile table
ui/main/CategoryTiles declares the twelve category tiles (id, header
title/icon resources, per-tile view ids) in accordion order; MainActivity
category constants now alias the table ids and setupCategoryAccordion/
createTileHolder consume TileSpec directly. Removes the eight mechanical
when-mappers (~130 lines). Tile behavior, the tiles field and TileHolder
member names are unchanged (MainActivityUiRenderingTest reflection net
passes unmodified).
* refactor: extract scan-report export flow into MainExportController
ui/main/MainExportController owns the format/action dialogs, the two
CreateDocument launchers, clipboard copy and document writing, moved
verbatim from MainActivity. The controller is constructed as an activity
field initializer, preserving the pre-STARTED launcher registration
timing; the export snapshot and the debug-clipboard toggle stay in the
activity and are supplied via lambdas.
* fix: NoSuchElementException race in ScanCancellationSignal.cancel
callbacks.entries.toList() takes the size==1 fast path through first(),
which throws NoSuchElementException when a checker unregisters its
callback concurrently. Snapshot through the concurrent map iterator
instead; invocation-after-clear ordering and the register() double-check
contract are unchanged. Surfaced as an intermittent failure of
VpnCheckRunnerTest cancelled-execution test.
* refactor: deduplicate MainActivity loading-stage view mapping
showLoadingCardForStage and markLoadingStagesCancelled repeated the same
stage -> card/icon/status/findings/info-section bundle twice; both now
share a single categoryViewsForStage table. bindCardStoppedState mirrors
bindCardLoadingState, removing the four copies of the stopped-state
icon/status block. Per-stage behavior is unchanged.
Deliberate deviation from the original phase plan: a separate
LoadingStageCoordinator class would have needed ~10 injected callbacks
for the same effect; in-place deduplication wins on risk and size.
* fix: preserve {ip}-placeholder GeoIP provider URLs across profile round trip
UrlSanitizer.sanitizeHttpsUrl runs the URL through java.net.URI, which
throws on the illegal { } chars, so any custom GeoIP provider whose URL
embeds the documented {ip} placeholder was silently dropped on every
profile save/load. Add sanitizeGeoIpProviderUrl, which validates a copy
with {ip} replaced by a public IP literal (8.8.8.8) and returns the
original string when it passes. Wire deserializeGeoIp to it.
The {ip} substitution only exists in GeoIpChecker.fetchCustomProvider;
IpComparison/CdnPulling send URLs verbatim, so they keep the generic
sanitizer. canonicalHash semantics unchanged.
* fix: localize IP-channels markdown export header
CheckResultMarkdownExportFormatter hardcoded the Russian '## IP каналы' section header, leaking Russian into en/fa/zh exports. Use the existing R.string.ip_channels_title resource via the Context the formatter already receives. Goldens regenerated (US locale -> 'IP channels').
* fix: localize remaining Russian literals in Markdown export formatter
Replace hardcoded RU strings in CheckResultMarkdownExportFormatter with
context.getString lookups so en/fa/zh exports no longer leak Russian:
- IP-channels table column headers (Channel/Country/Sources)
- "Flags:" prefix
- operator-whitelist section (title, Detected yes/no, control suffix,
Duration label/ms unit, available/unavailable)
Add export_* string resources to all four locales (en/ru/fa/zh-rCN) and
drop the object-level AVAILABLE/UNAVAILABLE consts. Regenerate export
golden snapshots (now render under Locale.US in English) and update the
operator-whitelist unit-test assertions accordingly.
* refactor: extract main render environment and FindingViewFactory
* refactor: extract CategoryCardRenderer from MainActivity
* refactor: extract IP-comparison and CDN-pulling renderers
* refactor: extract call-transport, bypass, IP-channels and reachability renderers
* refactor: extract VerdictRenderer and finish MainActivity render decomposition