An open API service indexing awesome lists of open source software.

https://github.com/xtclovver/rknhardering


https://github.com/xtclovver/rknhardering

Last synced: 3 months ago
JSON representation

Awesome Lists containing this project

README

          

> **Язык / Language:** [Русский](README.md) | [English](docs/README.en.md) | [فارسی](docs/README.fa.md) | [中文](docs/README.zh-CN.md)

# RKNHardering

Android-приложение для обнаружения VPN и прокси на устройстве. Реализует методику РКН по выявлению средств обхода блокировок.

Минимальная версия Android: 8.0 (API 26).

## Архитектура

Шесть независимых модулей проверки запускаются параллельно. Итоговый вердикт рассчитывается в `VerdictEngine`.

`IpComparisonChecker` сохраняется в результат и показывается в UI как диагностический блок, но в текущей версии не участвует в `VerdictEngine`.

```text
VpnCheckRunner
├── GeoIpChecker — GeoIP + hosting/proxy-сигналы
├── IpComparisonChecker — RU/не-RU IP-чекеры (диагностика)
├── DirectSignsChecker — NetworkCapabilities, системный proxy, установленные VPN apps
├── IndirectSignsChecker — интерфейсы, маршруты, DNS, dumpsys, proxy-tech signals
├── LocationSignalsChecker — MCC/SIM/cell/Wi-Fi/BeaconDB
└── BypassChecker — localhost proxy, Xray gRPC API, underlying-network leak
└── VerdictEngine — логика итогового вердикта
```

---

## Модули проверки

### 1. GeoIP (`GeoIpChecker`)

Источники:

- `https://api.ipapi.is/` — основной источник полей GeoIP и сигналов proxy/VPN/Tor/datacenter
- `https://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 || isProxy`
- `needsReview = foreignIp && !isHosting && !isProxy`

Таймаут соединения и чтения для HTTP(S)-запросов: 10 секунд. `GeoIpChecker` использует только HTTPS-провайдеры и возвращает ошибку только если ни один GeoIP-провайдер не ответил данными.

---

### 2. Сравнение IP-чекеров (`IpComparisonChecker`)

Модуль сравнивает ответы RU- и не-RU публичных IP-чекеров. Это диагностический блок: он отображается в UI, но сейчас не участвует в `VerdictEngine`.

Группы сервисов:

| Группа | Сервисы |
|--------|---------|
| `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`.

#### 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).httpProxy`
- `ProxyInfo.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 Установленные VPN/Proxy-приложения (`InstalledVpnAppDetector`)

Модуль проверяет два источника:

- известные сигнатуры пакетов из [`VpnAppCatalog`](app/src/main/java/com/notcvnt/rknhardering/vpn/VpnAppCatalog.kt);
- приложения, которые объявляют `VpnService.SERVICE_INTERFACE` через `PackageManager.queryIntentServices`.

Это диагностические сигналы установки или декларации `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+`
- `ipsec.*`

Любой активный интерфейс, попавший под эти паттерны, даёт `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`, `networkOperatorName`
- `TelephonyManager.simOperator`, `simCountryIso`, `isNetworkRoaming`
- `requestCellInfoUpdate` / `allCellInfo`
- `WifiManager.scanResults` и текущий `BSSID`
- `BeaconDB` (`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`)

Три проверки запускаются параллельно:

- `ProxyScanner`
- `XrayApiScanner`
- `UnderlyingNetworkProber`

#### 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.

#### 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`.

Итог категории:

- `detected = confirmed split tunnel || xrayApiFound || vpnGatewayLeak || vpnNetworkBinding`
- `needsReview = true`, если найден открытый proxy, но подтверждения обхода нет

---

## Вердикт (`VerdictEngine`)

`VerdictEngine` использует не все собранные блоки одинаково.

Сначала применяются безусловные правила:

1. `DETECTED`, если в bypass-evidence есть `SPLIT_TUNNEL_BYPASS`.
2. `DETECTED`, если найден `XRAY_API`.
3. `DETECTED`, если найден `VPN_GATEWAY_LEAK`.
4. `DETECTED`, если location-сигналы подтверждают РФ (`network_mcc_ru:true`, `cell_country_ru:true` или `location_country_ru:true`), а `GeoIP` одновременно даёт иностранный сигнал.

После этого считается матрица:

- `geoMatrixHit` = иностранный GeoIP-сигнал (`geoIp.needsReview` или evidence `GEO_IP`)
- `directMatrixHit` = evidence из `DIRECT_NETWORK_CAPABILITIES` или `SYSTEM_PROXY`
- `indirectMatrixHit` = evidence из `INDIRECT_NETWORK_CAPABILITIES`, `ACTIVE_VPN`, `NETWORK_INTERFACE`, `ROUTING`, `DNS`, `PROXY_TECHNICAL_SIGNAL`

Комбинации:

| Geo | Direct | Indirect | Вердикт |
|-----|--------|----------|---------|
| нет | нет | нет | `NOT_DETECTED` |
| нет | да | нет | `NOT_DETECTED` |
| нет | нет | да | `NOT_DETECTED` |
| да | нет | нет | `NEEDS_REVIEW` |
| нет | да | да | `NEEDS_REVIEW` |
| любые остальные комбинации | | | `DETECTED` |

Примечания:

- `IpComparisonChecker` сейчас не участвует в `VerdictEngine`;
- сигналы `INSTALLED_APP` и `VPN_SERVICE_DECLARATION` тоже не входят в матрицу и остаются диагностическими.

---

## Сборка

Требования: JDK 17+, Android SDK с Build Tools для API 36.

```bash
./gradlew assembleDebug
```

---

## Благодарности

[runetfreedom](https://github.com/runetfreedom) — за [per-app-split-bypass-poc](https://github.com/runetfreedom/per-app-split-bypass-poc), на основе которого реализована детекция per-app split bypass.