{"id":48618851,"url":"https://github.com/xtclovver/rknhardering","last_synced_at":"2026-04-16T04:03:32.824Z","repository":{"id":349756252,"uuid":"1203251892","full_name":"xtclovver/RKNHardering","owner":"xtclovver","description":null,"archived":false,"fork":false,"pushed_at":"2026-04-07T13:09:23.000Z","size":141,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-07T13:13:32.727Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/xtclovver.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-06T21:46:23.000Z","updated_at":"2026-04-07T13:03:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/xtclovver/RKNHardering","commit_stats":null,"previous_names":["xtclovver/rknhardering"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/xtclovver/RKNHardering","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xtclovver%2FRKNHardering","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xtclovver%2FRKNHardering/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xtclovver%2FRKNHardering/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xtclovver%2FRKNHardering/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/xtclovver","download_url":"https://codeload.github.com/xtclovver/RKNHardering/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xtclovver%2FRKNHardering/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31583290,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-08T14:31:17.711Z","status":"online","status_checked_at":"2026-04-09T02:00:06.848Z","response_time":112,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-09T03:01:12.895Z","updated_at":"2026-04-16T04:03:32.809Z","avatar_url":"https://github.com/xtclovver.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003e **Язык / Language:** [Русский](README.md) | [English](docs/README.en.md) | [فارسی](docs/README.fa.md) | [中文](docs/README.zh-CN.md)\n\n# RKNHardering\n\nAndroid-приложение для обнаружения VPN и прокси на устройстве. Реализует методику РКН по выявлению средств обхода блокировок.\n\nМинимальная версия Android: 8.0 (API 26).\n\n## Архитектура\n\nШесть независимых модулей проверки запускаются параллельно. Итоговый вердикт рассчитывается в `VerdictEngine`.\n\n`IpComparisonChecker` сохраняется в результат и показывается в UI как диагностический блок, но в текущей версии не участвует в `VerdictEngine`.\n\n```text\nVpnCheckRunner\n├── GeoIpChecker           — GeoIP + hosting/proxy-сигналы\n├── IpComparisonChecker    — RU/не-RU IP-чекеры (диагностика)\n├── DirectSignsChecker     — NetworkCapabilities, системный proxy, установленные VPN apps\n├── IndirectSignsChecker   — интерфейсы, маршруты, DNS, dumpsys, proxy-tech signals\n├── LocationSignalsChecker — MCC/SIM/cell/Wi-Fi/BeaconDB\n└── BypassChecker          — localhost proxy, Xray gRPC API, underlying-network leak\n        └── VerdictEngine  — логика итогового вердикта\n```\n\n---\n\n## Модули проверки\n\n### 1. GeoIP (`GeoIpChecker`)\n\nИсточники:\n\n- `https://api.ipapi.is/` — основной источник полей GeoIP и сигналов proxy/VPN/Tor/datacenter\n- `https://www.iplocate.io/api/lookup` — fallback-источник полей GeoIP и дополнительный голос за hosting (`privacy.is_hosting`)\n\nЛогика:\n\n| Сигнал | Что делает код | Итог |\n|--------|----------------|------|\n| `countryCode != RU` | IP считается иностранным | `needsReview`, если одновременно нет `hosting` и `proxy` |\n| `hosting` | Используется majority vote по совместимым ответам одного и того же IP (`ipapi.is`, `iplocate.io`) | `detected = true`, если большинство совместимых источников говорят `hosting=true` |\n| `proxy` | Используются совместимые HTTPS-провайдеры (`ipapi.is`, `iplocate.io`) | `detected = true`, если хотя бы один совместимый провайдер говорит о proxy/VPN/Tor |\n| `country`, `isp`, `org`, `as`, `query` | Берутся из `ipapi.is`, а недостающие поля заполняются из `iplocate.io` только для совместимого IP | не влияют напрямую |\n\nИтог категории:\n\n- `detected = isHosting || isProxy`\n- `needsReview = foreignIp \u0026\u0026 !isHosting \u0026\u0026 !isProxy`\n\nТаймаут соединения и чтения для HTTP(S)-запросов: 10 секунд. `GeoIpChecker` использует только HTTPS-провайдеры и возвращает ошибку только если ни один GeoIP-провайдер не ответил данными.\n\n---\n\n### 2. Сравнение IP-чекеров (`IpComparisonChecker`)\n\nМодуль сравнивает ответы RU- и не-RU публичных IP-чекеров. Это диагностический блок: он отображается в UI, но сейчас не участвует в `VerdictEngine`.\n\nГруппы сервисов:\n\n| Группа | Сервисы |\n|--------|---------|\n| `RU` | `Yandex IPv4`, `2ip.ru`, `Yandex IPv6` |\n| `NON_RU` | `ifconfig.me IPv4`, `ifconfig.me IPv6`, `checkip.amazonaws.com`, `ipify`, `ip.sb IPv4`, `ip.sb IPv6` |\n\nЛогика:\n\n- внутри каждой группы строится `canonicalIp`, если сервисы согласованы;\n- несовпадение IP внутри группы, частичные ответы и конфликт семейств `IPv4/IPv6` переводят группу в `needsReview` или `detected` в зависимости от полноты данных;\n- общий `detected` ставится только если обе группы дали полный консенсус внутри себя, но RU- и не-RU группы вернули разные canonical IP;\n- ожидаемые ошибки IPv6-эндпоинтов могут игнорироваться и не ломают консенсус IPv4.\n\n---\n\n### 3. Прямые признаки (`DirectSignsChecker`)\n\nСистемные признаки без активного сетевого сканирования localhost.\n\n#### 3.1 NetworkCapabilities (`checkVpnTransport`)\n\nAPI: `ConnectivityManager.getNetworkCapabilities(activeNetwork)`\n\n| Проверка | Метод/поле | Итог |\n|----------|------------|------|\n| `NetworkCapabilities.TRANSPORT_VPN` | `caps.hasTransport(TRANSPORT_VPN)` | `detected = true` |\n| `IS_VPN` | `caps.toString().contains(\"IS_VPN\")` | `detected = true` |\n| `VpnTransportInfo` | `caps.toString().contains(\"VpnTransportInfo\")` | `detected = true` |\n\n`IS_VPN` и `VpnTransportInfo` проверяются через строковое представление `NetworkCapabilities`.\n\n#### 3.2 Системный proxy (`checkSystemProxy`)\n\nИспользуются:\n\n- `System.getProperty(\"http.proxyHost\")` с fallback на `Proxy.getDefaultHost()`\n- `System.getProperty(\"http.proxyPort\")` с fallback на `Proxy.getDefaultPort()`\n- `System.getProperty(\"socksProxyHost\")`\n- `System.getProperty(\"socksProxyPort\")`\n- `ConnectivityManager.getDefaultProxy()`\n- `ConnectivityManager.allNetworks` + `ConnectivityManager.getLinkProperties(network).httpProxy`\n- `ProxyInfo.getPacFileUrl()`, `ProxyInfo.getExclusionList()`, на API 30+ также `ProxyInfo.isValid()`\n\nЛогика:\n\n| Состояние | Итог |\n|-----------|------|\n| host отсутствует | proxy считается ненастроенным |\n| host есть, но порт невалиден | `needsReview = true` |\n| host есть и порт валиден | `detected = true` |\n| порт относится к известным proxy-портам | добавляется отдельная находка |\n| `ProxyInfo` содержит `PAC URL` | `detected = true` |\n| `ProxyInfo` задан на конкретной сети | в вывод добавляется отдельная строка с interface name |\n| `ProxyInfo` содержит exclusion list | exclusions показываются пользователю |\n| на API 30+ `!ProxyInfo.isValid()` | `needsReview = true` без detected-evidence |\n| на отслеживаемых сетях `ProxyInfo` не найден | добавляется агрегированная строка `ProxyInfo ...: не обнаружен` |\n\nИзвестные proxy-порты: `80`, `443`, `1080`, `3127`, `3128`, `4080`, `5555`, `7000`, `7044`, `8000`, `8080`, `8081`, `8082`, `8888`, `9000`, `9050`, `9051`, `9150`, `12345`, а также диапазон `16000..16100`.\n\n#### 3.3 Установленные VPN/Proxy-приложения (`InstalledVpnAppDetector`)\n\nМодуль проверяет два источника:\n\n- известные сигнатуры пакетов из [`VpnAppCatalog`](app/src/main/java/com/notcvnt/rknhardering/vpn/VpnAppCatalog.kt);\n- приложения, которые объявляют `VpnService.SERVICE_INTERFACE` через `PackageManager.queryIntentServices`.\n\nЭто диагностические сигналы установки или декларации `VpnService`, а не подтверждение активного туннеля. Совпадения переводят категорию в `needsReview`, но сами по себе не делают `DirectSignsChecker.detected = true`.\n\n---\n\n### 4. Косвенные признаки (`IndirectSignsChecker`)\n\n#### 4.1 Capability `NOT_VPN` (`checkNotVpnCapability`)\n\n`ConnectivityManager.getNetworkCapabilities(activeNetwork).toString()` проверяется на наличие строки `NOT_VPN`.\n\n| Результат | Итог |\n|-----------|------|\n| `NOT_VPN` присутствует | норма |\n| `NOT_VPN` отсутствует | `detected = true` |\n\n#### 4.2 Сетевые интерфейсы (`checkNetworkInterfaces`)\n\nAPI: `NetworkInterface.getNetworkInterfaces()`. Проверяются активные (`isUp`) интерфейсы.\n\nПаттерны VPN-подобных интерфейсов:\n\n- `tun\\d+`\n- `tap\\d+`\n- `wg\\d+`\n- `ppp\\d+`\n- `ipsec.*`\n\nЛюбой активный интерфейс, попавший под эти паттерны, даёт `detected = true`.\n\n#### 4.3 Аномалия MTU (`checkMtu`)\n\nЛогика:\n\n| Условие | Итог |\n|---------|------|\n| VPN-подобный интерфейс с MTU `1..1499` | `detected = true` |\n| Нестандартный активный интерфейс (не `wlan.*`, `rmnet.*`, `eth.*`, `lo`) с MTU `1..1499` | `detected = true` |\n\n#### 4.4 Маршрутизация (`checkRoutingTable`)\n\nИсточник данных:\n\n- в первую очередь `LinkProperties.routes` из Android API;\n- fallback: `/proc/net/route`, если через API не удалось получить default route.\n\nДетекты:\n\n- default route через нестандартный интерфейс;\n- выделенные non-default routes через VPN/нестандартный интерфейс;\n- паттерн split tunneling: одновременно видны tunnel routes и обычный default route через стандартную сеть.\n\nDefault route через `wlan.*`, `rmnet.*`, `eth.*`, `lo` считается нормой, если сама сеть не помечена как VPN.\n\n#### 4.5 DNS (`checkDns`)\n\nAPI: `ConnectivityManager.getLinkProperties(activeNetwork).dnsServers`.\n\nDNS оценивается вместе со snapshot underlying-сетей, если они доступны.\n\n| Сигнал | Итог |\n|--------|------|\n| loopback DNS (`127.x.x.x`, `::1`) | `detected = true` |\n| private DNS, унаследованный из той же private/ULA-подсети основной non-VPN сети | норма |\n| private DNS при активном VPN и отличии от underlying сети | `detected = true` |\n| private DNS без достаточного контекста | `needsReview = true` |\n| public DNS, заменённый при активном VPN | `needsReview = true` |\n| link-local (`169.254.x.x`, `fe80::/10`) | информационно |\n\n#### 4.6 Дополнительные proxy-технические сигналы (`checkProxyTechnicalSignals`)\n\nПроверяются:\n\n- установленные proxy-only утилиты из `VpnAppCatalog` с сигналом `LOCAL_PROXY` без `VPN_SERVICE`;\n- локальные listeners из `/proc/net/tcp`, `/proc/net/tcp6`, `/proc/net/udp`, `/proc/net/udp6` на известных proxy-портах;\n- большое число localhost listeners на высоких портах.\n\nЛогика:\n\n- listener на известном localhost proxy-порту даёт `detected = true`;\n- наличие proxy-only утилиты или множества localhost listeners даёт `needsReview = true`.\n\nОтдельно фиксируется ограничение: проверки процессов, `iptables`/`pf` и системных сертификатов неполны без root/privileged access.\n\n#### 4.7 `dumpsys vpn_management` (`checkDumpsysVpn`)\n\nТолько Android 12+ (API 31+). Запускается `dumpsys vpn_management`.\n\nЕсли парсер (`VpnDumpsysParser`) находит активные записи VPN, они дают `detected = true`. Из записей извлекается пакет, затем он сопоставляется с `VpnAppCatalog`:\n\n- известный пакет: высокая уверенность;\n- неизвестный пакет: `detected = true` и одновременно `needsReview = true`.\n\nПустой вывод, `Permission Denial` или недоступность сервиса считаются отсутствием детекта.\n\n#### 4.8 `dumpsys activity services android.net.VpnService` (`checkDumpsysVpnService`)\n\nЗапускается `dumpsys activity services android.net.VpnService`.\n\nЕсли найдены активные `VpnService`, создаются `activeApps` и evidence:\n\n- известный пакет из каталога: высокая уверенность;\n- неизвестный пакет: `detected = true` и `needsReview = true`.\n\nПустой вывод или отсутствие записей `VpnService` детекта не дают.\n\n---\n\n### 5. Сигналы местоположения (`LocationSignalsChecker`)\n\nМодуль собирает признаки, подтверждающие, что устройство физически находится в РФ или, наоборот, что telephony-сигналы выглядят нетипично.\n\nИсточники:\n\n- `TelephonyManager.networkOperator`, `networkCountryIso`, `networkOperatorName`\n- `TelephonyManager.simOperator`, `simCountryIso`, `isNetworkRoaming`\n- `requestCellInfoUpdate` / `allCellInfo`\n- `WifiManager.scanResults` и текущий `BSSID`\n- `BeaconDB` (`https://api.beacondb.net/v1/geolocate`) для cell/Wi-Fi geolocation\n- reverse geocoding для `countryCode`\n\nРазрешения:\n\n- `ACCESS_FINE_LOCATION` нужен для cell lookup;\n- на Android 13+ `NEARBY_WIFI_DEVICES` нужен для Wi-Fi lookup.\n\nЛогика:\n\n| Сигнал | Итог |\n|--------|------|\n| `networkMcc == 250` | добавляется служебная находка `network_mcc_ru:true` |\n| `BeaconDB`/reverse geocode вернул `RU` | добавляются `cell_country_ru:true` и `location_country_ru:true` |\n| `networkMcc != 250` | `needsReview = true` |\n| отсутствие разрешений или radio data | информационно |\n\nВ текущей реализации `LocationSignalsChecker.detected` всегда `false`. Его основная роль в `VerdictEngine` — подтверждать Россию и усиливать иностранный GeoIP-сигнал.\n\n---\n\n### 6. Bypass-проверка (`BypassChecker`)\n\nТри проверки запускаются параллельно:\n\n- `ProxyScanner`\n- `XrayApiScanner`\n- `UnderlyingNetworkProber`\n\n#### 6.1 Сканер прокси (`ProxyScanner` + `ProxyProber`)\n\nСканируются `127.0.0.1` и `::1`.\n\nРежимы:\n\n| Режим | Описание |\n|-------|----------|\n| `AUTO` | сначала популярные порты, затем полный диапазон |\n| `MANUAL` | проверка одного указанного порта |\n\nПопулярные порты в `AUTO` формируются из `VpnAppCatalog.localhostProxyPorts` и дополнительно включают `1081`, `7890`, `7891`.\n\nПолное сканирование:\n\n- диапазон `1024..65535`\n- параллельность `200`\n- таймаут соединения `80 мс`\n- таймаут чтения `120 мс`\n\nОпределяются только proxy без аутентификации:\n\n| Тип | Как определяется |\n|-----|------------------|\n| `SOCKS5` | greeting `0x05 0x01 0x00` и ответ `0x05 0x00` |\n| `HTTP CONNECT` | `CONNECT ifconfig.me:443 HTTP/1.1` и ответ `HTTP/1.x 200` |\n\nОткрытый localhost proxy сам по себе не считается подтверждённым обходом: он фиксируется как `needsReview`. Подтверждение обхода ставится только если удалось получить прямой IP и IP через proxy, и они различаются.\n\nДополнительно:\n\n- если найден `SOCKS5`, но HTTP-получение IP через него не удалось и порт не похож на Xray, запускается `MtProtoProber`;\n- успешный MTProto probe добавляет информативную находку, но не влияет на итоговый verdict.\n\n#### 6.2 Сканер Xray gRPC API (`XrayApiScanner` + `XrayApiClient`)\n\nСканируются `127.0.0.1` и `::1`.\n\nПараметры:\n\n- диапазон `1024..65535`\n- параллельность `100`\n- TCP connect timeout `200 мс`\n- gRPC deadline `2000 мс` с повтором на увеличенном дедлайне\n\nПроверка выполняется не через сырой HTTP/2 preface, а через реальный gRPC-вызов `HandlerServiceGrpc.listOutbounds(...)`.\n\nПри успехе:\n\n- endpoint даёт `detected = true`;\n- в findings добавляются до 10 summary по outbound'ам (`tag`, `protocol`, `address`, `port`, `sni`) и счётчик оставшихся.\n\n#### 6.3 Underlying network leak / VPN network binding (`UnderlyingNetworkProber`)\n\nЕсли на устройстве активен VPN, модуль:\n\n- перебирает все `ConnectivityManager.allNetworks`;\n- ищет internet-capable сеть без `TRANSPORT_VPN`;\n- привязывает HTTP(S)-запросы к этой сети;\n- запрашивает публичный IP через `ifconfig.me`, `checkip.amazonaws.com`, `ipv4-internet.yandex.net`, `ipv6-internet.yandex.net`.\n\nЕсли underlying-сеть доступна при активном VPN, это трактуется как `VPN gateway leak` и даёт `detected = true`.\n\nИтог категории:\n\n- `detected = confirmed split tunnel || xrayApiFound || vpnGatewayLeak || vpnNetworkBinding`\n- `needsReview = true`, если найден открытый proxy, но подтверждения обхода нет\n\n---\n\n## Вердикт (`VerdictEngine`)\n\n`VerdictEngine` использует не все собранные блоки одинаково.\n\nСначала применяются безусловные правила:\n\n1. `DETECTED`, если в bypass-evidence есть `SPLIT_TUNNEL_BYPASS`.\n2. `DETECTED`, если найден `XRAY_API`.\n3. `DETECTED`, если найден `VPN_GATEWAY_LEAK`.\n4. `DETECTED`, если location-сигналы подтверждают РФ (`network_mcc_ru:true`, `cell_country_ru:true` или `location_country_ru:true`), а `GeoIP` одновременно даёт иностранный сигнал.\n\nПосле этого считается матрица:\n\n- `geoMatrixHit` = иностранный GeoIP-сигнал (`geoIp.needsReview` или evidence `GEO_IP`)\n- `directMatrixHit` = evidence из `DIRECT_NETWORK_CAPABILITIES` или `SYSTEM_PROXY`\n- `indirectMatrixHit` = evidence из `INDIRECT_NETWORK_CAPABILITIES`, `ACTIVE_VPN`, `NETWORK_INTERFACE`, `ROUTING`, `DNS`, `PROXY_TECHNICAL_SIGNAL`\n\nКомбинации:\n\n| Geo | Direct | Indirect | Вердикт |\n|-----|--------|----------|---------|\n| нет | нет | нет | `NOT_DETECTED` |\n| нет | да | нет | `NOT_DETECTED` |\n| нет | нет | да | `NOT_DETECTED` |\n| да | нет | нет | `NEEDS_REVIEW` |\n| нет | да | да | `NEEDS_REVIEW` |\n| любые остальные комбинации | | | `DETECTED` |\n\nПримечания:\n\n- `IpComparisonChecker` сейчас не участвует в `VerdictEngine`;\n- сигналы `INSTALLED_APP` и `VPN_SERVICE_DECLARATION` тоже не входят в матрицу и остаются диагностическими.\n\n---\n\n## Сборка\n\nТребования: JDK 17+, Android SDK с Build Tools для API 36.\n\n```bash\n./gradlew assembleDebug\n```\n\n---\n\n## Благодарности\n\n[runetfreedom](https://github.com/runetfreedom) — за [per-app-split-bypass-poc](https://github.com/runetfreedom/per-app-split-bypass-poc), на основе которого реализована детекция per-app split bypass.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxtclovver%2Frknhardering","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fxtclovver%2Frknhardering","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxtclovver%2Frknhardering/lists"}