mirror of
https://github.com/xtclovver/RKNHardering.git
synced 2026-05-04 23:50:06 +00:00
381 lines
14 KiB
Markdown
381 lines
14 KiB
Markdown
> **Language:** [Русский](../README.md) | [English](README.en.md) | [فارسی](README.fa.md) | [中文](README.zh-CN.md)
|
|
|
|
# RKNHardering
|
|
|
|
Android app for detecting VPNs and proxies on a device. Implements the Roskomnadzor-style methodology for identifying censorship circumvention tools.
|
|
|
|
Minimum Android version: 8.0 (API 26).
|
|
|
|
## Architecture
|
|
|
|
Six independent check modules run in parallel. The final verdict is calculated in `VerdictEngine`.
|
|
|
|
`IpComparisonChecker` is stored in the result and shown in the UI as a diagnostic block, but in the current version it does not participate in `VerdictEngine`.
|
|
|
|
```text
|
|
VpnCheckRunner
|
|
├── GeoIpChecker — GeoIP + hosting/proxy signals
|
|
├── IpComparisonChecker — RU/non-RU IP checkers (diagnostics)
|
|
├── DirectSignsChecker — NetworkCapabilities, system proxy, installed VPN apps
|
|
├── IndirectSignsChecker — interfaces, routes, DNS, dumpsys, proxy-tech signals
|
|
├── LocationSignalsChecker — MCC/SIM/cell/Wi-Fi/BeaconDB
|
|
└── BypassChecker — localhost proxy, Xray gRPC API, underlying-network leak
|
|
└── VerdictEngine — final verdict logic
|
|
```
|
|
|
|
---
|
|
|
|
## Check Modules
|
|
|
|
### 1. GeoIP (`GeoIpChecker`)
|
|
|
|
Sources:
|
|
|
|
- `https://api.ipapi.is/` — primary source for GeoIP fields and proxy/VPN/Tor/datacenter signals
|
|
- `https://www.iplocate.io/api/lookup` — fallback source for GeoIP fields and an additional vote for hosting (`privacy.is_hosting`)
|
|
|
|
Logic:
|
|
|
|
| Signal | What the code does | Result |
|
|
|--------|--------------------|--------|
|
|
| `countryCode != RU` | The IP is treated as foreign | `needsReview` if neither `hosting` nor `proxy` is present |
|
|
| `hosting` | Uses majority vote across compatible answers for the same IP (`ipapi.is`, `iplocate.io`) | `detected = true` if most compatible sources say `hosting=true` |
|
|
| `proxy` | Uses compatible HTTPS providers (`ipapi.is`, `iplocate.io`) | `detected = true` if at least one compatible provider reports proxy/VPN/Tor |
|
|
| `country`, `isp`, `org`, `as`, `query` | Taken from `ipapi.is`, and missing fields are filled from `iplocate.io` only for a compatible IP | no direct effect |
|
|
|
|
Final category result:
|
|
|
|
- `detected = isHosting || isProxy`
|
|
- `needsReview = foreignIp && !isHosting && !isProxy`
|
|
|
|
HTTP(S) connect and read timeout: 10 seconds. `GeoIpChecker` uses only HTTPS providers and returns an error only if no GeoIP provider returns data.
|
|
|
|
---
|
|
|
|
### 2. IP checker comparison (`IpComparisonChecker`)
|
|
|
|
This module compares responses from RU and non-RU public IP checkers. It is a diagnostic block: it is shown in the UI, but currently does not participate in `VerdictEngine`.
|
|
|
|
Service groups:
|
|
|
|
| Group | Services |
|
|
|-------|----------|
|
|
| `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` |
|
|
|
|
Logic:
|
|
|
|
- inside each group, a `canonicalIp` is built if the services agree;
|
|
- IP mismatches within a group, partial responses, and `IPv4/IPv6` family conflicts move the group to `needsReview` or `detected` depending on data completeness;
|
|
- the overall `detected` flag is set only if both groups reached full internal consensus but RU and non-RU groups returned different canonical IPs;
|
|
- expected IPv6 endpoint errors can be ignored and do not break IPv4 consensus.
|
|
|
|
---
|
|
|
|
### 3. Direct signs (`DirectSignsChecker`)
|
|
|
|
System signs without active localhost network scanning.
|
|
|
|
#### 3.1 NetworkCapabilities (`checkVpnTransport`)
|
|
|
|
API: `ConnectivityManager.getNetworkCapabilities(activeNetwork)`
|
|
|
|
| Check | Method/field | Result |
|
|
|-------|--------------|--------|
|
|
| `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` and `VpnTransportInfo` are checked through the string representation of `NetworkCapabilities`.
|
|
|
|
#### 3.2 System proxy (`checkSystemProxy`)
|
|
|
|
Uses:
|
|
|
|
- `System.getProperty("http.proxyHost")` with fallback to `Proxy.getDefaultHost()`
|
|
- `System.getProperty("http.proxyPort")` with fallback to `Proxy.getDefaultPort()`
|
|
- `System.getProperty("socksProxyHost")`
|
|
- `System.getProperty("socksProxyPort")`
|
|
|
|
Logic:
|
|
|
|
| State | Result |
|
|
|-------|--------|
|
|
| host is missing | proxy is treated as not configured |
|
|
| host exists but port is invalid | `needsReview = true` |
|
|
| host exists and port is valid | `detected = true` |
|
|
| port matches a known proxy port | an extra finding is added |
|
|
|
|
Known proxy ports: `80`, `443`, `1080`, `3127`, `3128`, `4080`, `5555`, `7000`, `7044`, `8000`, `8080`, `8081`, `8082`, `8888`, `9000`, `9050`, `9051`, `9150`, `12345`, and the range `16000..16100`.
|
|
|
|
#### 3.3 Installed VPN/Proxy apps (`InstalledVpnAppDetector`)
|
|
|
|
The module checks two sources:
|
|
|
|
- known package signatures from [`VpnAppCatalog`](../app/src/main/java/com/notcvnt/rknhardering/vpn/VpnAppCatalog.kt);
|
|
- apps that declare `VpnService.SERVICE_INTERFACE` through `PackageManager.queryIntentServices`.
|
|
|
|
These are diagnostic signals of installation or `VpnService` declaration, not confirmation of an active tunnel. Matches move the category into `needsReview`, but do not by themselves make `DirectSignsChecker.detected = true`.
|
|
|
|
---
|
|
|
|
### 4. Indirect signs (`IndirectSignsChecker`)
|
|
|
|
#### 4.1 `NOT_VPN` capability (`checkNotVpnCapability`)
|
|
|
|
`ConnectivityManager.getNetworkCapabilities(activeNetwork).toString()` is checked for the presence of `NOT_VPN`.
|
|
|
|
| Result | Outcome |
|
|
|--------|---------|
|
|
| `NOT_VPN` present | normal |
|
|
| `NOT_VPN` absent | `detected = true` |
|
|
|
|
#### 4.2 Network interfaces (`checkNetworkInterfaces`)
|
|
|
|
API: `NetworkInterface.getNetworkInterfaces()`. Only active (`isUp`) interfaces are checked.
|
|
|
|
VPN-like interface patterns:
|
|
|
|
- `tun\d+`
|
|
- `tap\d+`
|
|
- `wg\d+`
|
|
- `ppp\d+`
|
|
- `ipsec.*`
|
|
|
|
Any active interface matching these patterns yields `detected = true`.
|
|
|
|
#### 4.3 MTU anomaly (`checkMtu`)
|
|
|
|
Logic:
|
|
|
|
| Condition | Result |
|
|
|-----------|--------|
|
|
| VPN-like interface with MTU `1..1499` | `detected = true` |
|
|
| Non-standard active interface (not `wlan.*`, `rmnet.*`, `eth.*`, `lo`) with MTU `1..1499` | `detected = true` |
|
|
|
|
#### 4.4 Routing (`checkRoutingTable`)
|
|
|
|
Data sources:
|
|
|
|
- primarily `LinkProperties.routes` from the Android API;
|
|
- fallback: `/proc/net/route` if the default route could not be obtained via API.
|
|
|
|
Detections:
|
|
|
|
- default route through a non-standard interface;
|
|
- dedicated non-default routes through VPN/non-standard interfaces;
|
|
- split tunneling pattern: tunnel routes visible together with a normal default route through a standard network.
|
|
|
|
A default route through `wlan.*`, `rmnet.*`, `eth.*`, `lo` is treated as normal if the network itself is not marked as VPN.
|
|
|
|
#### 4.5 DNS (`checkDns`)
|
|
|
|
API: `ConnectivityManager.getLinkProperties(activeNetwork).dnsServers`.
|
|
|
|
DNS is evaluated together with underlying network snapshots when they are available.
|
|
|
|
| Signal | Result |
|
|
|--------|--------|
|
|
| loopback DNS (`127.x.x.x`, `::1`) | `detected = true` |
|
|
| private DNS inherited from the same private/ULA subnet as the main non-VPN network | normal |
|
|
| private DNS while VPN is active and different from the underlying network | `detected = true` |
|
|
| private DNS without enough context | `needsReview = true` |
|
|
| public DNS replaced while VPN is active | `needsReview = true` |
|
|
| link-local (`169.254.x.x`, `fe80::/10`) | informational |
|
|
|
|
#### 4.6 Additional proxy-technical signals (`checkProxyTechnicalSignals`)
|
|
|
|
Checks:
|
|
|
|
- installed proxy-only utilities from `VpnAppCatalog` with `LOCAL_PROXY` signal but without `VPN_SERVICE`;
|
|
- local listeners from `/proc/net/tcp`, `/proc/net/tcp6`, `/proc/net/udp`, `/proc/net/udp6` on known proxy ports;
|
|
- a large number of localhost listeners on high ports.
|
|
|
|
Logic:
|
|
|
|
- a listener on a known localhost proxy port yields `detected = true`;
|
|
- a proxy-only utility or many localhost listeners yields `needsReview = true`.
|
|
|
|
A separate limitation is recorded: checks for processes, `iptables`/`pf`, and system certificates are incomplete without root/privileged access.
|
|
|
|
#### 4.7 `dumpsys vpn_management` (`checkDumpsysVpn`)
|
|
|
|
Android 12+ only (API 31+). Runs `dumpsys vpn_management`.
|
|
|
|
If the parser (`VpnDumpsysParser`) finds active VPN entries, they yield `detected = true`. A package is extracted from the entries and matched against `VpnAppCatalog`:
|
|
|
|
- known package: high confidence;
|
|
- unknown package: `detected = true` and also `needsReview = true`.
|
|
|
|
Empty output, `Permission Denial`, or service unavailability are treated as no detection.
|
|
|
|
#### 4.8 `dumpsys activity services android.net.VpnService` (`checkDumpsysVpnService`)
|
|
|
|
Runs `dumpsys activity services android.net.VpnService`.
|
|
|
|
If active `VpnService` instances are found, `activeApps` and evidence are created:
|
|
|
|
- known package from the catalog: high confidence;
|
|
- unknown package: `detected = true` and `needsReview = true`.
|
|
|
|
Empty output or no `VpnService` entries produce no detection.
|
|
|
|
---
|
|
|
|
### 5. Location signals (`LocationSignalsChecker`)
|
|
|
|
This module collects signs confirming that the device is physically in Russia or, conversely, that telephony signals look atypical.
|
|
|
|
Sources:
|
|
|
|
- `TelephonyManager.networkOperator`, `networkCountryIso`, `networkOperatorName`
|
|
- `TelephonyManager.simOperator`, `simCountryIso`, `isNetworkRoaming`
|
|
- `requestCellInfoUpdate` / `allCellInfo`
|
|
- `WifiManager.scanResults` and current `BSSID`
|
|
- `BeaconDB` (`https://api.beacondb.net/v1/geolocate`) for cell/Wi-Fi geolocation
|
|
- reverse geocoding for `countryCode`
|
|
|
|
Permissions:
|
|
|
|
- `ACCESS_FINE_LOCATION` is needed for cell lookup;
|
|
- on Android 13+, `NEARBY_WIFI_DEVICES` is needed for Wi-Fi lookup.
|
|
|
|
Logic:
|
|
|
|
| Signal | Result |
|
|
|--------|--------|
|
|
| `networkMcc == 250` | adds the internal finding `network_mcc_ru:true` |
|
|
| `BeaconDB`/reverse geocode returned `RU` | adds `cell_country_ru:true` and `location_country_ru:true` |
|
|
| `networkMcc != 250` | `needsReview = true` |
|
|
| missing permissions or radio data | informational |
|
|
|
|
In the current implementation, `LocationSignalsChecker.detected` is always `false`. Its main role in `VerdictEngine` is to confirm Russia and strengthen a foreign GeoIP signal.
|
|
|
|
---
|
|
|
|
### 6. Bypass check (`BypassChecker`)
|
|
|
|
Three checks run in parallel:
|
|
|
|
- `ProxyScanner`
|
|
- `XrayApiScanner`
|
|
- `UnderlyingNetworkProber`
|
|
|
|
#### 6.1 Proxy scanner (`ProxyScanner` + `ProxyProber`)
|
|
|
|
Scans `127.0.0.1` and `::1`.
|
|
|
|
Modes:
|
|
|
|
| Mode | Description |
|
|
|------|-------------|
|
|
| `AUTO` | first common ports, then full range |
|
|
| `MANUAL` | check a single specified port |
|
|
|
|
Popular ports in `AUTO` are built from `VpnAppCatalog.localhostProxyPorts` and additionally include `1081`, `7890`, `7891`.
|
|
|
|
Full scan:
|
|
|
|
- range `1024..65535`
|
|
- parallelism `200`
|
|
- connect timeout `80 ms`
|
|
- read timeout `120 ms`
|
|
|
|
Only proxies without authentication are detected:
|
|
|
|
| Type | How it is detected |
|
|
|------|--------------------|
|
|
| `SOCKS5` | greeting `0x05 0x01 0x00` and reply `0x05 0x00` |
|
|
| `HTTP CONNECT` | `CONNECT ifconfig.me:443 HTTP/1.1` and reply `HTTP/1.x 200` |
|
|
|
|
An open localhost proxy is not treated as confirmed bypass by itself: it is recorded as `needsReview`. Bypass confirmation is set only if both a direct IP and a proxy IP can be obtained and they differ.
|
|
|
|
Additionally:
|
|
|
|
- if `SOCKS5` is found, but HTTP IP retrieval through it fails and the port does not look like Xray, `MtProtoProber` is launched;
|
|
- a successful MTProto probe adds an informative finding, but does not affect the final verdict.
|
|
|
|
#### 6.2 Xray gRPC API scanner (`XrayApiScanner` + `XrayApiClient`)
|
|
|
|
Scans `127.0.0.1` and `::1`.
|
|
|
|
Parameters:
|
|
|
|
- range `1024..65535`
|
|
- parallelism `100`
|
|
- TCP connect timeout `200 ms`
|
|
- gRPC deadline `2000 ms` with retry on an increased deadline
|
|
|
|
The check is performed not through a raw HTTP/2 preface, but through a real gRPC call `HandlerServiceGrpc.listOutbounds(...)`.
|
|
|
|
On success:
|
|
|
|
- the endpoint yields `detected = true`;
|
|
- findings include up to 10 outbound summaries (`tag`, `protocol`, `address`, `port`, `sni`) plus a counter for remaining ones.
|
|
|
|
#### 6.3 Underlying network leak / VPN network binding (`UnderlyingNetworkProber`)
|
|
|
|
If VPN is active on the device, the module:
|
|
|
|
- iterates through all `ConnectivityManager.allNetworks`;
|
|
- looks for an internet-capable network without `TRANSPORT_VPN`;
|
|
- binds HTTP(S) requests to that network;
|
|
- requests the public IP through `ifconfig.me`, `checkip.amazonaws.com`, `ipv4-internet.yandex.net`, `ipv6-internet.yandex.net`.
|
|
|
|
If the underlying network is reachable while VPN is active, this is treated as `VPN gateway leak` and yields `detected = true`.
|
|
|
|
Final category result:
|
|
|
|
- `detected = confirmed split tunnel || xrayApiFound || vpnGatewayLeak || vpnNetworkBinding`
|
|
- `needsReview = true` if an open proxy is found but the bypass is not confirmed
|
|
|
|
---
|
|
|
|
## Verdict (`VerdictEngine`)
|
|
|
|
`VerdictEngine` does not use all collected blocks equally.
|
|
|
|
First, unconditional rules are applied:
|
|
|
|
1. `DETECTED` if bypass evidence contains `SPLIT_TUNNEL_BYPASS`.
|
|
2. `DETECTED` if `XRAY_API` is found.
|
|
3. `DETECTED` if `VPN_GATEWAY_LEAK` is found.
|
|
4. `DETECTED` if location signals confirm Russia (`network_mcc_ru:true`, `cell_country_ru:true`, or `location_country_ru:true`) while `GeoIP` simultaneously reports a foreign signal.
|
|
|
|
Then a matrix is computed:
|
|
|
|
- `geoMatrixHit` = foreign GeoIP signal (`geoIp.needsReview` or `GEO_IP` evidence)
|
|
- `directMatrixHit` = evidence from `DIRECT_NETWORK_CAPABILITIES` or `SYSTEM_PROXY`
|
|
- `indirectMatrixHit` = evidence from `INDIRECT_NETWORK_CAPABILITIES`, `ACTIVE_VPN`, `NETWORK_INTERFACE`, `ROUTING`, `DNS`, `PROXY_TECHNICAL_SIGNAL`
|
|
|
|
Combinations:
|
|
|
|
| Geo | Direct | Indirect | Verdict |
|
|
|-----|--------|----------|---------|
|
|
| no | no | no | `NOT_DETECTED` |
|
|
| no | yes | no | `NOT_DETECTED` |
|
|
| no | no | yes | `NOT_DETECTED` |
|
|
| yes | no | no | `NEEDS_REVIEW` |
|
|
| no | yes | yes | `NEEDS_REVIEW` |
|
|
| any other combination | | | `DETECTED` |
|
|
|
|
Notes:
|
|
|
|
- `IpComparisonChecker` currently does not participate in `VerdictEngine`;
|
|
- `INSTALLED_APP` and `VPN_SERVICE_DECLARATION` signals are also not part of the matrix and remain diagnostic.
|
|
|
|
---
|
|
|
|
## Build
|
|
|
|
Requirements: JDK 17+, Android SDK with Build Tools for API 36.
|
|
|
|
```bash
|
|
./gradlew assembleDebug
|
|
```
|
|
|
|
---
|
|
|
|
## Acknowledgements
|
|
|
|
[runetfreedom](https://github.com/runetfreedom) — for [per-app-split-bypass-poc](https://github.com/runetfreedom/per-app-split-bypass-poc), which the per-app split bypass detection is based on.
|