RKNHardering
Android-приложение для обнаружения VPN и прокси на устройстве. Реализует методику РКН по выявлению средств обхода блокировок.
Минимальная версия Android: 8.0 (API 26).
Вы можете скачать наш проект с:
| GitHub | F-Droid |
|---|---|
|
|
Нужна помощь сообщества / Community Help Wanted
RU
Этот проект документирует методы обнаружения VPN и прокси на Android-устройствах. Однако обратная задача, как предотвратить детектирование наличия VPN, исследована значительно хуже.
Я ищу людей, готовых помочь собрать, систематизировать и протестировать информацию о способах обхода детектов, включая, но не ограничиваясь:
- Маскировка сетевых интерфейсов (как скрыть
tun0,wg0и другие VPN-подобные интерфейсы отNetworkInterface.getNetworkInterfaces()и/proc/net/route) - Подмена NetworkCapabilities (способы убрать
TRANSPORT_VPN,IS_VPN,VpnTransportInfoиз ответовConnectivityManager) - Скрытие от dumpsys (предотвращение утечки информации через
dumpsys vpn_managementиdumpsys activity services android.net.VpnService) - MTU-нормализация (выставление стандартного MTU (1500) для туннельных интерфейсов на различных клиентах)
- DNS-утечки (предотвращение обнаружения loopback/private DNS при активном VPN)
- Скрытие localhost-прокси (как предотвратить обнаружение через
/proc/net/tcpи сканирование портов) - Обход нативных проверок (противодействие JNI-проверкам через
/proc/self/maps,getifaddrs(),dlsym) - Маскировка установленных приложений (скрытие пакетов VPN-приложений от
PackageManager)
Если вы обладаете знаниями в этих областях, пожалуйста, откройте Issue или Pull Request, либо напишите в чат Matrix/Telegram с описанием метода, условий применимости и ограничений. Любая информация ценна — от теоретических идей до работающих PoC.
EN
This project documents methods for detecting VPNs and proxies on Android devices. However, the inverse problem how to prevent the detection of an active VPN has been studied much less thoroughly.
I am looking for people willing to help collect, organize, and test information about ways to bypass detection, including, but not limited to:
- Network interface masking (how to hide
tun0,wg0, and other VPN-like interfaces fromNetworkInterface.getNetworkInterfaces()and/proc/net/route) - NetworkCapabilities spoofing (ways to remove
TRANSPORT_VPN,IS_VPN, andVpnTransportInfofromConnectivityManagerresponses) - Hiding from dumpsys (preventing information leakage through
dumpsys vpn_managementanddumpsys activity services android.net.VpnService) - MTU normalization (setting a standard MTU of 1500 for tunnel interfaces across different clients)
- DNS leaks (preventing detection of loopback/private DNS while a VPN is active)
- Hiding localhost proxies (how to prevent detection via
/proc/net/tcpand port scanning) - Bypassing native checks (countering JNI-based checks through
/proc/self/maps,getifaddrs(), anddlsym) - Masking installed applications (hiding VPN app packages from
PackageManager)
If you have expertise in these areas, please open an Issue or Сhat in Matrix/Telegram, the conditions under which it applies, and its limitations. Any information is valuable, from theoretical ideas to working PoCs.
Архитектура
Независимые модули проверки запускаются параллельно. Итоговый вердикт рассчитывается в VerdictEngine.
IpComparisonChecker сохраняется в результат и показывается в UI как диагностический блок. Напрямую в VerdictEngine не участвует, но его данные поступают в IpConsensusBuilder.
VpnCheckRunner
├── GeoIpChecker — GeoIP + hosting/proxy-сигналы
├── IpComparisonChecker — RU/не-RU IP-чекеры (диагностика)
├── DirectSignsChecker — NetworkCapabilities, системный proxy, TUN-проб, установленные VPN apps
├── IndirectSignsChecker — интерфейсы, маршруты, DNS, dumpsys, proxy-tech signals
├── CallTransportChecker — STUN/MTProto (утечки и доступность)
├── CdnPullingChecker — HTTPS-запросы к CDN/redirector
├── LocationSignalsChecker — MCC/SIM/cell/Wi-Fi/BeaconDB
├── BypassChecker — localhost proxy, Xray gRPC API, Clash/sing-box REST API, SOCKS5-auth проба, underlying-network leak
├── RttTriangulationChecker — SNITCH (β): RTT-триангуляция по RU/иностранным хостам
├── IcmpSpoofingChecker — ICMP-спуфинг оператора (заблокированный хост отвечает)
├── DomainReachabilityChecker — DNS→TCP→TLS pipeline для детекта DPI-блокировки
├── NativeSignsChecker — JNI-проверки (маршруты, интерфейсы, host-route /32, TUN/TAP по типу, хуки, root, эмулятор, изоляция)
└── IpConsensusBuilder — кросс-модульный IP-консенсус
└── VerdictEngine — логика итогового вердикта
Модули проверки
1. GeoIP (GeoIpChecker)
Источники:
https://api.ipapi.is/— основной источник полей GeoIP и сигналов proxy/VPN/Tor/datacenterhttps://www.iplocate.io/api/lookup— fallback-источник полей GeoIP и дополнительный голос за hosting (privacy.is_hosting)
Логика:
| Сигнал | Что делает код | Итог |
|---|---|---|
countryCode != RU |
IP считается иностранным | needsReview, если одновременно нет hosting и proxy |
hosting |
Используется majority vote по совместимым ответам одного и того же IP (ipapi.is, iplocate.io) |
detected = true, если большинство совместимых источников говорят hosting=true |
proxy |
Используются совместимые HTTPS-провайдеры (ipapi.is, iplocate.io) |
detected = true, если хотя бы один совместимый провайдер говорит о proxy/VPN/Tor |
country, isp, org, as, query |
Берутся из ipapi.is, а недостающие поля заполняются из iplocate.io только для совместимого IP |
не влияют напрямую |
Итог категории:
detected = isHosting || isProxyneedsReview = foreignIp && !isHosting && !isProxy
Таймаут соединения и чтения для HTTP(S)-запросов: 10 секунд. GeoIpChecker использует только HTTPS-провайдеры и возвращает ошибку только если ни один GeoIP-провайдер не ответил данными.
2. Сравнение IP-чекеров (IpComparisonChecker)
Модуль сравнивает ответы RU- и не-RU публичных IP-чекеров. Отображается в UI как диагностический блок. Напрямую в VerdictEngine не участвует, но его данные поступают в IpConsensusBuilder, результаты которого используются в R3.
Группы сервисов:
| Группа | Сервисы |
|---|---|
RU |
Yandex IPv4, 2ip.ru, Yandex IPv6 |
NON_RU |
ifconfig.me IPv4, ifconfig.me IPv6, checkip.amazonaws.com, ipify, ip.sb IPv4, ip.sb IPv6 |
Логика:
- внутри каждой группы строится
canonicalIp, если сервисы согласованы; - несовпадение IP внутри группы, частичные ответы и конфликт семейств
IPv4/IPv6переводят группу вneedsReviewилиdetectedв зависимости от полноты данных; - общий
detectedставится только если обе группы дали полный консенсус внутри себя, но RU- и не-RU группы вернули разные canonical IP; - ожидаемые ошибки IPv6-эндпоинтов могут игнорироваться и не ломают консенсус IPv4.
3. Прямые признаки (DirectSignsChecker)
Системные признаки без активного сетевого сканирования localhost.
3.1 NetworkCapabilities (checkVpnTransport)
API: ConnectivityManager.getNetworkCapabilities(activeNetwork)
| Проверка | Метод/поле | Итог |
|---|---|---|
NetworkCapabilities.TRANSPORT_VPN |
caps.hasTransport(TRANSPORT_VPN) |
detected = true |
IS_VPN |
caps.toString().contains("IS_VPN") |
detected = true |
VpnTransportInfo |
caps.toString().contains("VpnTransportInfo") |
detected = true |
IS_VPN и VpnTransportInfo проверяются через строковое представление NetworkCapabilities.
При наличии VpnTransportInfo (API 29+, через reflection getType()) в findings добавляется тип транспорта: SERVICE (VpnService-приложение), PLATFORM (always-on / IKEv2), LEGACY (legacy VPN framework) или OEM. Это информационное поле, оно не влияет на detected/needsReview.
3.2 Системный proxy (checkSystemProxy)
Используются:
System.getProperty("http.proxyHost")с fallback наProxy.getDefaultHost()System.getProperty("http.proxyPort")с fallback наProxy.getDefaultPort()System.getProperty("socksProxyHost")System.getProperty("socksProxyPort")ConnectivityManager.getDefaultProxy()ConnectivityManager.allNetworks+ConnectivityManager.getLinkProperties(network).httpProxyProxyInfo.getPacFileUrl(),ProxyInfo.getExclusionList(), на API 30+ такжеProxyInfo.isValid()
Логика:
| Состояние | Итог |
|---|---|
| host отсутствует | proxy считается ненастроенным |
| host есть, но порт невалиден | needsReview = true |
| host есть и порт валиден | detected = true |
| порт относится к известным proxy-портам | добавляется отдельная находка |
ProxyInfo содержит PAC URL |
detected = true |
ProxyInfo задан на конкретной сети |
в вывод добавляется отдельная строка с interface name |
ProxyInfo содержит exclusion list |
exclusions показываются пользователю |
на API 30+ !ProxyInfo.isValid() |
needsReview = true без detected-evidence |
на отслеживаемых сетях ProxyInfo не найден |
добавляется агрегированная строка ProxyInfo ...: не обнаружен |
Известные proxy-порты: 80, 443, 1080, 3127, 3128, 4080, 5555, 7000, 7044, 8000, 8080, 8081, 8082, 8888, 9000, 9050, 9051, 9150, 12345, а также диапазон 16000..16100.
3.3 TUN Active Probe (checkTunActiveProbe)
Если при инициализации обнаружен TUN-интерфейс, UnderlyingNetworkProber отправляет HTTP-запросы через VPN-сеть к RU- и non-RU целям. При расхождении IP (DNS path mismatch) или если приложение явно исключено из per-app VPN (tun0 есть, но vpnActive = false) — detected = true. Этот сигнал поступает в VerdictEngine через EvidenceSource.TUN_ACTIVE_PROBE.
3.4 Установленные VPN/Proxy-приложения (InstalledVpnAppDetector)
Модуль проверяет три источника:
- известные сигнатуры пакетов из
VpnAppCatalog; - приложения, которые объявляют
VpnService.SERVICE_INTERFACEчерезPackageManager.queryIntentServices; - приложения с "VPN" в названии.
Это диагностические сигналы установки или декларации VpnService, а не подтверждение активного туннеля. Совпадения переводят категорию в needsReview, но сами по себе не делают DirectSignsChecker.detected = true.
4. Косвенные признаки (IndirectSignsChecker)
4.1 Capability NOT_VPN (checkNotVpnCapability)
ConnectivityManager.getNetworkCapabilities(activeNetwork).toString() проверяется на наличие строки NOT_VPN.
| Результат | Итог |
|---|---|
NOT_VPN присутствует |
норма |
NOT_VPN отсутствует |
detected = true |
4.2 Сетевые интерфейсы (checkNetworkInterfaces)
API: NetworkInterface.getNetworkInterfaces(). Проверяются активные (isUp) интерфейсы.
Паттерны VPN-подобных интерфейсов:
tun\d+tap\d+wg\d+ppp\d+utun\d*— TUN в стиле macOS/iOSzt.*— ZeroTiertailscale\d*— Tailscalesvpn\d*— Pulse Secure / Ivantigre\d+— GRE-туннелиl2tp\d+— L2TPhe-ipv6.*— IPv6-туннель Hurricane Electric(ipsec|xfrm).*— IPsec / ядерный XFRM
Любой активный интерфейс, попавший под эти паттерны, даёт detected = true.
4.3 Аномалия MTU (checkMtu)
Логика:
| Условие | Итог |
|---|---|
VPN-подобный интерфейс с MTU 1..1499 |
detected = true |
Нестандартный активный интерфейс (не wlan.*, rmnet.*, eth.*, lo) с MTU 1..1499 |
detected = true |
4.4 Маршрутизация (checkRoutingTable)
Источник данных:
- в первую очередь
LinkProperties.routesиз Android API; - fallback:
/proc/net/route, если через API не удалось получить default route.
Детекты:
- default route через нестандартный интерфейс;
- выделенные non-default routes через VPN/нестандартный интерфейс;
- паттерн split tunneling: одновременно видны tunnel routes и обычный default route через стандартную сеть.
Default route через wlan.*, rmnet.*, eth.*, lo считается нормой, если сама сеть не помечена как VPN.
4.5 DNS (checkDns)
API: ConnectivityManager.getLinkProperties(activeNetwork).dnsServers.
DNS оценивается вместе со snapshot underlying-сетей, если они доступны.
| Сигнал | Итог |
|---|---|
loopback DNS (127.x.x.x, ::1) |
detected = true |
| private DNS, унаследованный из той же private/ULA-подсети основной non-VPN сети | норма |
| private DNS при активном VPN и отличии от underlying сети | detected = true |
| private DNS без достаточного контекста | needsReview = true |
| public DNS, заменённый при активном VPN | needsReview = true |
link-local (169.254.x.x, fe80::/10) |
информационно |
4.6 Дополнительные proxy-технические сигналы (checkProxyTechnicalSignals)
Проверяются:
- установленные proxy-only утилиты из
VpnAppCatalogс сигналомLOCAL_PROXYбезVPN_SERVICE; - локальные listeners из
/proc/net/tcp,/proc/net/tcp6,/proc/net/udp,/proc/net/udp6на известных proxy-портах; - большое число localhost listeners на высоких портах.
Логика:
- listener на известном localhost proxy-порту даёт
detected = true; - наличие proxy-only утилиты или множества localhost listeners даёт
needsReview = true.
Отдельно фиксируется ограничение: проверки процессов, iptables/pf и системных сертификатов неполны без root/privileged access.
4.7 dumpsys vpn_management (checkDumpsysVpn)
Только Android 12+ (API 31+). Запускается dumpsys vpn_management.
Если парсер (VpnDumpsysParser) находит активные записи VPN, они дают detected = true. Из записей извлекается пакет, затем он сопоставляется с VpnAppCatalog:
- известный пакет: высокая уверенность;
- неизвестный пакет:
detected = trueи одновременноneedsReview = true.
Пустой вывод, Permission Denial или недоступность сервиса считаются отсутствием детекта.
4.8 dumpsys activity services android.net.VpnService (checkDumpsysVpnService)
Запускается dumpsys activity services android.net.VpnService.
Если найдены активные VpnService, создаются activeApps и evidence:
- известный пакет из каталога: высокая уверенность;
- неизвестный пакет:
detected = trueиneedsReview = true.
Пустой вывод или отсутствие записей VpnService детекта не дают.
5. Сигналы местоположения (LocationSignalsChecker)
Модуль собирает признаки, подтверждающие, что устройство физически находится в РФ или, наоборот, что telephony-сигналы выглядят нетипично.
Источники:
TelephonyManager.networkOperator,networkCountryIso,networkOperatorNameTelephonyManager.simOperator,simCountryIso,isNetworkRoamingrequestCellInfoUpdate/allCellInfoWifiManager.scanResultsи текущийBSSIDBeaconDB(https://api.beacondb.net/v1/geolocate) для cell/Wi-Fi geolocation- reverse geocoding для
countryCode
Разрешения:
ACCESS_FINE_LOCATIONнужен для cell lookup;- на Android 13+
NEARBY_WIFI_DEVICESнужен для Wi-Fi lookup.
Логика:
| Сигнал | Итог |
|---|---|
networkMcc == 250 |
добавляется служебная находка network_mcc_ru:true |
BeaconDB/reverse geocode вернул RU |
добавляются cell_country_ru:true и location_country_ru:true |
networkMcc != 250 |
needsReview = true |
| отсутствие разрешений или radio data | информационно |
В текущей реализации LocationSignalsChecker.detected всегда false. Его основная роль в VerdictEngine — подтверждать Россию и усиливать иностранный GeoIP-сигнал.
6. Bypass-проверка (BypassChecker)
Проверки запускаются параллельно:
ProxyScannerXrayApiScannerClashApiScannerUnderlyingNetworkProber
6.1 Сканер прокси (ProxyScanner + ProxyProber)
Сканируются 127.0.0.1 и ::1.
Режимы:
| Режим | Описание |
|---|---|
AUTO |
сначала популярные порты, затем полный диапазон |
MANUAL |
проверка одного указанного порта |
Популярные порты в AUTO формируются из VpnAppCatalog.localhostProxyPorts и дополнительно включают 1081, 7890, 7891.
Полное сканирование:
- диапазон
1024..65535 - параллельность
200 - таймаут соединения
80 мс - таймаут чтения
120 мс
Определяются только proxy без аутентификации:
| Тип | Как определяется |
|---|---|
SOCKS5 |
greeting 0x05 0x01 0x00 и ответ 0x05 0x00 |
HTTP CONNECT |
CONNECT ifconfig.me:443 HTTP/1.1 и ответ HTTP/1.x 200 |
Открытый localhost proxy сам по себе не считается подтверждённым обходом: он фиксируется как needsReview. Подтверждение обхода ставится только если удалось получить прямой IP и IP через proxy, и они различаются.
Дополнительно:
- если найден
SOCKS5, но HTTP-получение IP через него не удалось и порт не похож на Xray, запускаетсяMtProtoProber; - успешный MTProto probe добавляет информативную находку, но не влияет на итоговый verdict.
Проба аутентификации (ProxyProber, опциональная). Включается настройкой «Probe local proxy authentication» (pref_proxy_auth_probe_enabled, по умолчанию выключена). Применяется только к SOCKS5-эндпоинтам на loopback-адресах:
- перебор словаря слабых учётных данных (RFC 1929): пустая пара,
admin/admin,user/password,proxy/proxy,test/testи т.д. — только если прокси требует аутентификацию; - проба
UDP ASSOCIATEна прокси без аутентификации.
Успешный подбор кредов или открытый UDP ASSOCIATE дают detected = true (EvidenceSource.PROXY_AUTH_BYPASS, входит в HARD_DETECT_BYPASS).
6.2 Сканер Xray gRPC API (XrayApiScanner + XrayApiClient)
Сканируются 127.0.0.1 и ::1.
Параметры:
- диапазон
1024..65535 - параллельность
100 - TCP connect timeout
200 мс - gRPC deadline
2000 мсс повтором на увеличенном дедлайне
Проверка выполняется не через сырой HTTP/2 preface, а через реальный gRPC-вызов HandlerServiceGrpc.listOutbounds(...).
При успехе:
- endpoint даёт
detected = true; - в findings добавляются до 10 summary по outbound'ам (
tag,protocol,address,port,sni) и счётчик оставшихся.
6.3 Underlying network leak / VPN network binding (UnderlyingNetworkProber)
Если на устройстве активен VPN, модуль:
- перебирает все
ConnectivityManager.allNetworks; - ищет internet-capable сеть без
TRANSPORT_VPN; - привязывает HTTP(S)-запросы к этой сети;
- запрашивает публичный IP через
ifconfig.me,checkip.amazonaws.com,ipv4-internet.yandex.net,ipv6-internet.yandex.net.
Если underlying-сеть доступна при активном VPN, это трактуется как VPN gateway leak и даёт detected = true.
6.4 Сканер REST API Clash/sing-box (ClashApiScanner + ClashApiClient)
Опциональная проверка, настройка «Clash/sing-box REST API scan» (pref_clash_api_scan_enabled, по умолчанию включена). Сканирует loopback (127.0.0.1, ::1) на REST API менеджеров Clash, mihomo и sing-box.
Параметры:
- порты
9090,19090,9091,9097 - TCP connect probe
200 мс, далее connect/read600 мс
Логика:
GET /configs— если возвращается валидный JSON, API считается живым;GET /connections— изmetadata.destinationIPизвлекаются IP VPN-серверов (до 10 уникальных);GET /proxies— собираются имена прокси-узлов.
Живой API или непустой список IP назначений дают detected = true (EvidenceSource.CLASH_API, входит в HARD_DETECT_BYPASS).
Итог категории:
detected = confirmed split tunnel || xrayApiFound || clashApiFound || proxyAuthBypass || vpnGatewayLeak || vpnNetworkBindingneedsReview = true, если найден открытый proxy, но подтверждения обхода нет
7. CDN Pulling (CdnPullingChecker)
Отправляет HTTPS-запросы к известным endpoint-ам типа trace и redirector (Google Video, Cloudflare trace, Meduza), чтобы узнать, какой публичный IP или метаданные сети возвращаются. Различия в ответах могут указывать на туннелирование или проксирование.
8. Call Transport (CallTransportChecker)
Проверяет доступность UDP/STUN (как глобальных, так и региональных), а также TCP-доступность Telegram MTProto через локальные прокси-серверы. Это позволяет выявить утечки трафика мимо стандартных туннелей или обнаружить подмену IP-адресов.
9. SNITCH — RTT-триангуляция (RttTriangulationChecker) β
Отправляет ICMP ping к набору российских и иностранных хостов и сравнивает медианные задержки.
Российские цели: yandex.ru, mail.ru, vk.com, sberbank.ru, gosuslugi.ru.
Иностранные цели: facebook.com, github.com, twitter.com, reddit.com, instagram.com.
Логика:
- если медианный RTT до RU-хостов превышает порог (
80 мс) — устройство, вероятно, не находится в РФ; - высокий jitter (> 60 мс) снижает уверенность вывода;
- результат переводит вердикт в
NEEDS_REVIEW, но сам по себе не даётDETECTED.
Проверка опциональна и отключена по умолчанию.
10. ICMP Spoofing (IcmpSpoofingChecker)
Проверяет, не подменяет ли оператор ICMP-ответы на заблокированные хосты.
Цели по умолчанию:
instagram.com— заблокированный хост (BLOCKED);google.com— контрольный хост (CONTROL).
Цели конфигурируемы через пользовательские проверки. Для работы нужна хотя бы одна пара BLOCKED + CONTROL.
Логика:
| Условие | Итог |
|---|---|
| BLOCKED-хост ответил на ping | needsReview = true — возможен ICMP-спуфинг оператора |
| CONTROL-хост ответил с RTT < 10 мс | needsReview = true — подозрительно низкая задержка (возможно, локальный перехват) |
| Оба условия одновременно | needsReview = true с усиленным сигналом |
| CONTROL-хост не ответил | результат неопределённый (inconclusive) |
| Ничего из вышеперечисленного | норма |
IcmpSpoofingChecker.detected всегда false. Результат может перевести вердикт из NOT_DETECTED в NEEDS_REVIEW через R6 в VerdictEngine. Включён по умолчанию. При home-routed роуминге сигналы автоматически подавляются.
11. Доступность доменов (DomainReachabilityChecker)
Проверяет каждый домен из пользовательского списка по цепочке DNS → TCP → TLS:
| Шаг | Детектируемая блокировка | Таймаут |
|---|---|---|
| DNS | NXDOMAIN, timeout | 8 с |
| TCP :443 | Connection refused, timeout | 8 с |
| TLS (SNI) | Connection reset — признак DPI | 10 с |
TLS-шаг использует trust-all X.509 manager, потому что цель — обнаружить сброс соединения DPI, а не проверить сертификат. SSLHandshakeException из-за невалидного сертификата трактуется как успех TLS.
Результаты не влияют на вердикт. Модуль отключён по умолчанию (domainReachabilityEnabled = false) и активируется только если задан непустой список доменов в настройках пользовательской проверки.
12. Native Signs (NativeSignsChecker)
Выполняет низкоуровневые JNI-проверки прямо из C++:
- Перечисление нативных интерфейсов и работа
getifaddrs() - Прямой парсинг
/proc/net/route - Сканирование
/proc/self/mapsна известные признаки hook-ов - Целостность разрешения символов
libc(dlsym) - Обнаружение root (su binary, параметры magisk, selinux, rw /system и др.)
Нативные находки транслируются в needsReview или общие indirect-признаки.
12.1 TUN/TAP по типу интерфейса
Для каждого интерфейса читается /sys/class/net/<name>/type. Значение 65534 (ARPHRD_TUNTAP) у активного интерфейса, имя которого не совпадает с известными VPN-паттернами, означает TUN/TAP, маскирующийся под обычный интерфейс. Итог: detected = true (EvidenceSource.NATIVE_INTERFACE).
12.2 Host-route /32 эвристика
В таблице маршрутов (NETLINK) ищется не-default маршрут с префиксом /32 (IPv4) или /128 (IPv6) к публичному маршрутизируемому адресу через физический интерфейс (wlan0, rmnet, eth0). Это классический хост-маршрут VPN-клиента к IP своего сервера в обход туннеля — утечка реального IP VPN-сервера. Итог: detected = true (EvidenceSource.NATIVE_ROUTE).
12.3 Детектор эмулятора
JNI-проверки (nativeDetectEmulator): QEMU system properties (ro.kernel.qemu*, ro.boot.qemu), goldfish/ranchu hardware, pipe-устройства (/dev/qemu_pipe, /dev/socket/genyd для Genymotion), goldfish-драйвер в /proc/tty/drivers, артефакты BlueStacks. Дополнительно — Build-эвристика (FINGERPRINT, MODEL, HARDWARE, PRODUCT, MANUFACTURER == "Genymotion").
В эмуляторе сетевые тесты ненадёжны, поэтому итог — needsReview = true (EvidenceSource.NATIVE_EMULATOR), никогда detected.
12.4 Детектор изоляции
Определяются контексты, в которых VPN другого пользователя/профиля невидим сетевым детекторам:
- вторичный пользователь Android (
userId > 0, извлекается из путиdataDir); - клон приложения / dual-app (
userId == 999или диапазон950..959MIUI); - рабочий профиль (
DevicePolicyManager.isProfileOwnerApp).
Любой из сигналов даёт needsReview = true (EvidenceSource.SANDBOX_ISOLATION), никогда detected.
Вердикт (VerdictEngine)
VerdictEngine использует не все собранные блоки одинаково.
R1 — безусловный детект через bypass-evidence:
Если хотя бы одно detected-evidence имеет источник SPLIT_TUNNEL_BYPASS, XRAY_API, VPN_GATEWAY_LEAK или VPN_NETWORK_BINDING → DETECTED.
R3 — IP-консенсус:
IpConsensusBuilder агрегирует сигналы из GeoIP, IpComparison, CDN Pulling, TUN-проба, bypass и callTransportLeaks. Если определён geoAxis (иностранные IP, geo-country mismatch или Warp-индикатор) и одновременно имеет место probeTargetDivergence, probeTargetDirectDivergence или crossChannelMismatch → DETECTED.
R4 — локация vs GeoIP:
- Если location-сигналы подтверждают РФ (
network_mcc_ru:true,cell_country_ru:trueилиlocation_country_ru:true), аGeoIPодновременно фиксирует иностранный IP (outsideRu = true) —DETECTED, кроме случая home-routed роуминга. - Если location подтверждает РФ, а GeoIP показывает hosting/proxy без иностранного IP и нет других сигналов —
NEEDS_REVIEW.
Флаг expectedRoamingExit (определяется HomeNetworkCatalog по MCC/MNC SIM-карты и ASN) защищает от ложных срабатываний при международном роуминге с маршрутизацией через домашнего оператора.
R5 — трёхосевая матрица (geo × direct × indirect):
geoHit=GeoIP.outsideRu == true(кроме роуминга)directHit= detected-evidence изDIRECT_NETWORK_CAPABILITIESилиSYSTEM_PROXYindirectHit= detected-evidence изINDIRECT_NETWORK_CAPABILITIES,ACTIVE_VPN,NETWORK_INTERFACE,ROUTING,DNS,PROXY_TECHNICAL_SIGNAL,NATIVE_INTERFACE,NATIVE_ROUTE,NATIVE_JVM_MISMATCH
| Geo | Direct | Indirect | Вердикт |
|---|---|---|---|
| нет | нет | нет | NOT_DETECTED |
| нет | да | нет | NOT_DETECTED |
| нет | нет | да | NOT_DETECTED |
| да | нет | нет | NEEDS_REVIEW |
| нет | да | да | NEEDS_REVIEW (если geo доступно), иначе DETECTED |
| да | да | нет | DETECTED |
| да | нет | да | DETECTED |
| да | да | да | DETECTED |
R6 — fallback к NEEDS_REVIEW:
Если матрица дала NOT_DETECTED, но выполнено хотя бы одно условие — результат повышается до NEEDS_REVIEW:
bypassResult.needsReview(открытый proxy без подтверждения обхода)directSigns.needsReviewилиindirectSigns.needsReviewlocationSignalHit(location.detected && !expectedRoamingExit)- actionable leak из
CallTransportChecker(статусNEEDS_REVIEWне через local proxy) icmpSpoofing.needsReviewNativeSignsCheckerнашёл маркеры хуков (NATIVE_HOOK_MARKERS) или нарушение целостности (NATIVE_LIBRARY_INTEGRITY)ipConsensus.needsReview,ipConsensus.channelConflictне пуст,ipConsensus.probeTargetDivergenceTUN_ACTIVE_PROBEevidence сdetected = false(tun есть, но VPN не активен для приложения)
Примечания:
IpComparisonCheckerчерезIpConsensusBuilderтеперь косвенно участвует в R3;- сигналы
INSTALLED_APPиVPN_SERVICE_DECLARATIONне входят в матрицу и остаются диагностическими; DomainReachabilityCheckerне влияет на вердикт.
Сборка
Требования: JDK 17+, Android SDK с Build Tools для API 36.
./gradlew assembleDebug
Благодарности
runetfreedom — за per-app-split-bypass-poc, на основе которого реализована детекция per-app split bypass.